diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 011494253..a04731023 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -261,6 +261,17 @@ function optionalString(value: unknown): string | null { return typeof value === "string" ? value : null; } +/** Accepts string ids; some serializers may emit other primitives in edge cases. */ +function optionalArtefactId(value: unknown): string | null { + if (typeof value === "string" && value.length > 0) { + return value; + } + if (typeof value === "number" && Number.isFinite(value)) { + return String(value); + } + return null; +} + type AnyArtefact = | SignalReportArtefact | PriorityJudgmentArtefact @@ -278,13 +289,17 @@ const PRIORITY_VALUES = new Set(["P0", "P1", "P2", "P3", "P4"]); function normalizePriorityJudgmentArtefact( value: Record, ): PriorityJudgmentArtefact | null { - const id = optionalString(value.id); + const id = optionalArtefactId(value.id); if (!id) return null; const contentValue = isObjectRecord(value.content) ? value.content : null; if (!contentValue) return null; - const priority = optionalString(contentValue.priority); + const rawPriority = optionalString(contentValue.priority); + const priority = + rawPriority && /^p[0-4]$/i.test(rawPriority) + ? rawPriority.toUpperCase() + : rawPriority; if (!priority || !PRIORITY_VALUES.has(priority)) return null; return { @@ -307,7 +322,7 @@ const ACTIONABILITY_VALUES = new Set([ function normalizeActionabilityJudgmentArtefact( value: Record, ): ActionabilityJudgmentArtefact | null { - const id = optionalString(value.id); + const id = optionalArtefactId(value.id); if (!id) return null; const contentValue = isObjectRecord(value.content) ? value.content : null; @@ -338,7 +353,7 @@ function normalizeActionabilityJudgmentArtefact( function normalizeSignalFindingArtefact( value: Record, ): SignalFindingArtefact | null { - const id = optionalString(value.id); + const id = optionalArtefactId(value.id); if (!id) return null; const contentValue = isObjectRecord(value.content) ? value.content : null; @@ -428,7 +443,27 @@ function normalizeSignalReportArtefact(value: unknown): AnyArtefact | null { return normalizeDismissalArtefact(value); } - const id = optionalString(value.id); + // Infer structured artefacts when `type` is missing or does not match the API + // (shape matches; enums are still validated inside each normalizer). + const contentForInfer = isObjectRecord(value.content) ? value.content : null; + if (contentForInfer) { + if (optionalString(contentForInfer.signal_id)) { + const inferredFinding = normalizeSignalFindingArtefact(value); + if (inferredFinding) { + return inferredFinding; + } + } + const inferredPriority = normalizePriorityJudgmentArtefact(value); + if (inferredPriority) { + return inferredPriority; + } + const inferredActionability = normalizeActionabilityJudgmentArtefact(value); + if (inferredActionability) { + return inferredActionability; + } + } + + const id = optionalArtefactId(value.id); if (!id) { return null; } @@ -438,13 +473,16 @@ function normalizeSignalReportArtefact(value: unknown): AnyArtefact | null { optionalString(value.created_at) ?? new Date(0).toISOString(); // suggested_reviewers: content is an array of reviewer objects - if (type === "suggested_reviewers" && Array.isArray(value.content)) { - return { - id, - type: "suggested_reviewers" as const, - created_at, - content: value.content as SuggestedReviewersArtefact["content"], - }; + if (type === "suggested_reviewers") { + if (Array.isArray(value.content)) { + return { + id, + type: "suggested_reviewers" as const, + created_at, + content: value.content as SuggestedReviewersArtefact["content"], + }; + } + return null; } // video_segment and other artefacts with object content @@ -458,7 +496,22 @@ function normalizeSignalReportArtefact(value: unknown): AnyArtefact | null { // The backend may return empty content objects when binary decode fails. if (!content && !sessionId) { - return null; + return { + id, + type, + created_at, + content: { + session_id: "", + start_time: optionalString(contentValue.start_time) ?? "", + end_time: optionalString(contentValue.end_time) ?? "", + distinct_id: optionalString(contentValue.distinct_id) ?? "", + content: "", + distance_to_centroid: + typeof contentValue.distance_to_centroid === "number" + ? contentValue.distance_to_centroid + : null, + }, + }; } return { @@ -481,6 +534,7 @@ function normalizeSignalReportArtefact(value: unknown): AnyArtefact | null { function parseSignalReportArtefactsPayload( value: unknown, + debugContext?: { teamId: number; reportId: string }, ): SignalReportArtefactsResponse { const payload = isObjectRecord(value) ? value : null; const rawResults = Array.isArray(payload?.results) @@ -496,6 +550,30 @@ function parseSignalReportArtefactsPayload( typeof payload?.count === "number" ? payload.count : results.length; if (rawResults.length > 0 && results.length === 0) { + if (debugContext) { + const sample = rawResults.slice(0, 5).map((item) => { + if (!isObjectRecord(item)) { + return { shape: typeof item }; + } + const t = optionalString(item.type); + const content = item.content; + return { + type: t ?? "(missing)", + idKind: typeof item.id, + contentKind: Array.isArray(content) + ? "array" + : isObjectRecord(content) + ? "object" + : typeof content, + }; + }); + log.warn("Signal report artefacts payload did not match schema", { + teamId: debugContext.teamId, + reportId: debugContext.reportId, + rawCount: rawResults.length, + sample: sample, + }); + } return { results: [], count: 0, @@ -2040,14 +2118,10 @@ export class PostHogAPIClient { } const data = (await response.json()) as unknown; - const parsed = parseSignalReportArtefactsPayload(data); - - if (parsed.unavailableReason) { - log.warn("Signal report artefacts payload did not match schema", { - teamId, - reportId, - }); - } + const parsed = parseSignalReportArtefactsPayload(data, { + teamId, + reportId, + }); return parsed; } catch (error) { diff --git a/apps/code/src/renderer/components/ui/Tooltip.tsx b/apps/code/src/renderer/components/ui/Tooltip.tsx index 351dbd537..c6bfa4a96 100644 --- a/apps/code/src/renderer/components/ui/Tooltip.tsx +++ b/apps/code/src/renderer/components/ui/Tooltip.tsx @@ -43,11 +43,11 @@ export function Tooltip({ side={side} align={align} sideOffset={sideOffset} - className="dark flex items-center gap-[8px] rounded-[6px] border border-(--gray-4) bg-(--gray-2) px-[10px] py-[6px] text-(--gray-12) text-xs leading-[1.4]" + className="dark z-[200000] flex items-center gap-[8px] rounded-[6px] border border-(--gray-4) bg-(--gray-2) px-[10px] py-[6px] text-(--gray-12) text-xs leading-[1.4]" style={{ whiteSpace: isSimpleContent ? "nowrap" : "normal", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.25)", - zIndex: 9999, + zIndex: 200000, animationDuration: "150ms", animationTimingFunction: "ease-out", willChange: "transform, opacity", diff --git a/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx b/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx index c8680a9ca..a9fd59a3b 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx @@ -413,7 +413,7 @@ export function ReportDetailPane({ )} {/* ── Description ─────────────────────────────────────── */} - {report.status !== "ready" ? ( + {report.status !== "ready" && report.status !== "pending_input" ? (
} + icon={} label="Suppress" tooltipContent="Permanently suppress selected reports" disabledReason={suppressDisabledReason} @@ -603,7 +602,7 @@ export function SignalsToolbar({ - + Suppress reports @@ -629,7 +628,7 @@ export function SignalsToolbar({ {isSuppressing ? ( ) : ( - + )} Suppress diff --git a/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx b/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx index 83db11fb1..6d961a03e 100644 --- a/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx +++ b/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx @@ -62,7 +62,9 @@ export function ReportCardContent({ className="min-w-0 flex-1" > {prependBadges} - {!isReady && } + {!(isReady || report.status === "pending_input") && ( + + )} { }); describe("buildSignalReportListOrdering", () => { - it("puts status then suggested reviewer then descending field", () => { + it("puts suggested reviewer then status then descending field", () => { expect(buildSignalReportListOrdering("total_weight", "desc")).toBe( - "status,-is_suggested_reviewer,-total_weight", + "-is_suggested_reviewer,status,-total_weight", ); }); - it("puts status then suggested reviewer then ascending field", () => { + it("puts suggested reviewer then status then ascending field", () => { expect(buildSignalReportListOrdering("created_at", "asc")).toBe( - "status,-is_suggested_reviewer,created_at", + "-is_suggested_reviewer,status,created_at", ); }); it("works for signal_count", () => { expect(buildSignalReportListOrdering("signal_count", "desc")).toBe( - "status,-is_suggested_reviewer,-signal_count", + "-is_suggested_reviewer,status,-signal_count", ); }); }); diff --git a/apps/code/src/renderer/features/inbox/utils/filterReports.ts b/apps/code/src/renderer/features/inbox/utils/filterReports.ts index 82848f4ae..b08bafda8 100644 --- a/apps/code/src/renderer/features/inbox/utils/filterReports.ts +++ b/apps/code/src/renderer/features/inbox/utils/filterReports.ts @@ -47,8 +47,8 @@ export function buildStatusFilterParam(statuses: SignalReportStatus[]): string { /** * Comma-separated `ordering` for the signal report list API: - * 1. Status rank (ready first — semantic server-side rank, always applied) - * 2. Suggested reviewer (current user's reports first) + * 1. Suggested reviewer (current user's reports first) + * 2. Status rank (ready first — semantic server-side rank, always applied) * 3. Toolbar-selected field (priority, total_weight, created_at, etc.) */ export function buildSignalReportListOrdering( @@ -56,7 +56,7 @@ export function buildSignalReportListOrdering( direction: "asc" | "desc", ): string { const fieldKey = direction === "desc" ? `-${field}` : field; - return `status,-is_suggested_reviewer,${fieldKey}`; + return `-is_suggested_reviewer,status,${fieldKey}`; } export function buildSuggestedReviewerFilterParam( diff --git a/apps/code/src/renderer/features/sidebar/components/items/HomeItem.tsx b/apps/code/src/renderer/features/sidebar/components/items/HomeItem.tsx index 5fcef26b0..f00faa492 100644 --- a/apps/code/src/renderer/features/sidebar/components/items/HomeItem.tsx +++ b/apps/code/src/renderer/features/sidebar/components/items/HomeItem.tsx @@ -42,8 +42,8 @@ export function InboxItem({ isActive, onClick, signalCount }: InboxItemProps) { 0 - ? `${signalCount} actionable report${signalCount === 1 ? "" : "s"} assigned to you` - : "No actionable reports assigned to you yet" + ? `${signalCount} auto pull request${signalCount === 1 ? "" : "s"} assigned to you` + : "No auto pull requests assigned to you yet" } shortcut={formatHotkey(SHORTCUTS.INBOX)} side="right" @@ -55,20 +55,17 @@ export function InboxItem({ isActive, onClick, signalCount }: InboxItemProps) { } label={ - <> - Inbox + + Inbox {signalCount && signalCount > 0 ? ( {formatSignalCount(signalCount)} ) : null} - + } isActive={isActive} onClick={onClick} diff --git a/apps/code/src/renderer/styles/globals.css b/apps/code/src/renderer/styles/globals.css index 16aba59ab..0cd95184a 100644 --- a/apps/code/src/renderer/styles/globals.css +++ b/apps/code/src/renderer/styles/globals.css @@ -1069,6 +1069,17 @@ button, color: var(--gray-12) !important; } +.rt-BaseMenuItem[data-highlighted][data-variant="destructive"], +[role="menuitem"][data-highlighted][data-variant="destructive"] { + background-color: var(--red-3) !important; + color: var(--red-11) !important; +} + +.rt-BaseMenuItem[data-highlighted][data-variant="destructive"] svg, +[role="menuitem"][data-highlighted][data-variant="destructive"] svg { + color: var(--red-11) !important; +} + /* Select/Menu dropdown background matches theme */ .rt-SelectContent, .rt-BaseMenuContent {