diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index c66ef1217..040796173 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -953,6 +953,9 @@ export class PostHogAPIClient { if (params?.ordering) { url.searchParams.set("ordering", params.ordering); } + if (params?.source_product) { + url.searchParams.set("source_product", params.source_product); + } const response = await this.api.fetcher.fetch({ method: "get", diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 72e82d1a7..6da67984a 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -1,5 +1,4 @@ import { useAuthStateValue } from "@features/auth/hooks/authQueries"; -import { InboxLiveRail } from "@features/inbox/components/InboxLiveRail"; import { useInboxReportArtefacts, useInboxReportSignals, @@ -9,6 +8,7 @@ import { useSignalSourceConfigs } from "@features/inbox/hooks/useSignalSourceCon import { useInboxCloudTaskStore } from "@features/inbox/stores/inboxCloudTaskStore"; import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore"; import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore"; +import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore"; import { buildSignalTaskPrompt } from "@features/inbox/utils/buildSignalTaskPrompt"; import { buildSignalReportListOrdering, @@ -25,10 +25,17 @@ import { ArrowDownIcon, ArrowSquareOutIcon, ArrowsClockwiseIcon, + BrainIcon, + BugIcon, + CaretRightIcon, + CheckIcon, CircleNotchIcon, ClockIcon, Cloud as CloudIcon, GithubLogoIcon, + KanbanIcon, + TicketIcon, + VideoIcon, WarningIcon, XIcon, } from "@phosphor-icons/react"; @@ -50,7 +57,6 @@ import mailHog from "@renderer/assets/images/mail-hog.png"; import { getCloudUrlFromRegion } from "@shared/constants/oauth"; import type { SignalReportArtefact, - SignalReportArtefactsResponse, SignalReportsQueryParams, SuggestedReviewersArtefact, } from "@shared/types"; @@ -62,24 +68,132 @@ import { ReportCard } from "./ReportCard"; import { ReportTaskLogs } from "./ReportTaskLogs"; import { SignalCard } from "./SignalCard"; import { SignalReportPriorityBadge } from "./SignalReportPriorityBadge"; +import { SignalReportStatusBadge } from "./SignalReportStatusBadge"; import { SignalReportSummaryMarkdown } from "./SignalReportSummaryMarkdown"; import { SignalsToolbar } from "./SignalsToolbar"; -function getArtefactsUnavailableMessage( - reason: SignalReportArtefactsResponse["unavailableReason"], -): string { - switch (reason) { - case "forbidden": - return "Evidence could not be loaded with the current API permissions."; - case "not_found": - return "Evidence endpoint is unavailable for this signal in this environment."; - case "invalid_payload": - return "Evidence format was unexpected, so no artefacts could be shown."; - case "request_failed": - return "Evidence is temporarily unavailable. You can still create a task from this report."; - default: - return "Evidence is currently unavailable for this signal."; - } +function JudgmentBadges({ + safetyContent, + actionabilityContent, +}: { + safetyContent: Record | null; + actionabilityContent: Record | null; +}) { + const [expanded, setExpanded] = useState(false); + + const isSafe = + safetyContent?.safe === true || safetyContent?.judgment === "safe"; + const actionabilityJudgment = + (actionabilityContent?.judgment as string) ?? ""; + + const actionabilityLabel = + actionabilityJudgment === "immediately_actionable" + ? "Immediately actionable" + : actionabilityJudgment === "requires_human_input" + ? "Requires human input" + : "Not actionable"; + + const actionabilityColor = + actionabilityJudgment === "immediately_actionable" + ? "green" + : actionabilityJudgment === "requires_human_input" + ? "amber" + : "gray"; + + return ( + + + {expanded && ( + + {safetyContent?.explanation ? ( + + + Safety + + + {String(safetyContent.explanation)} + + + ) : null} + {actionabilityContent?.explanation ? ( + + + Actionability + + + {String(actionabilityContent.explanation)} + + + ) : null} + + )} + + ); } function LoadMoreTrigger({ @@ -203,10 +317,48 @@ function WelcomePane({ onEnableInbox }: { onEnableInbox: () => void }) { ); } +const SOURCE_ICON_MAP: Record< + string, + { icon: React.ReactNode; color: string; label: string } +> = { + session_replay: { + icon: , + color: "var(--amber-9)", + label: "Session replay", + }, + error_tracking: { + icon: , + color: "var(--red-9)", + label: "Error tracking", + }, + llm_analytics: { + icon: , + color: "var(--purple-9)", + label: "LLM analytics", + }, + github: { + icon: , + color: "var(--gray-11)", + label: "GitHub", + }, + linear: { + icon: , + color: "var(--blue-9)", + label: "Linear", + }, + zendesk: { + icon: , + color: "var(--green-9)", + label: "Zendesk", + }, +}; + function WarmingUpPane({ onConfigureSources, + enabledProducts, }: { onConfigureSources: () => void; + enabledProducts: string[]; }) { return ( Inbox is warming up @@ -243,15 +394,24 @@ function WarmingUpPane({ Reports will appear here as soon as signals come in. - + + {enabledProducts.map((sp) => { + const info = SOURCE_ICON_MAP[sp]; + return info ? ( + + {info.icon} + + ) : null; + })} + + ); @@ -300,9 +460,24 @@ export function InboxSignalsTab() { const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection); const searchQuery = useInboxSignalsFilterStore((s) => s.searchQuery); const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter); + const sourceProductFilter = useInboxSignalsFilterStore( + (s) => s.sourceProductFilter, + ); const { data: signalSourceConfigs } = useSignalSourceConfigs(); const hasSignalSources = signalSourceConfigs?.some((c) => c.enabled) ?? false; - const [sourcesDialogOpen, setSourcesDialogOpen] = useState(false); + const enabledProducts = useMemo(() => { + const seen = new Set(); + return (signalSourceConfigs ?? []) + .filter( + (c) => + c.enabled && + !seen.has(c.source_product) && + seen.add(c.source_product), + ) + .map((c) => c.source_product); + }, [signalSourceConfigs]); + const sourcesDialogOpen = useInboxSourcesDialogStore((s) => s.open); + const setSourcesDialogOpen = useInboxSourcesDialogStore((s) => s.setOpen); const windowFocused = useRendererWindowFocusStore((s) => s.focused); const isInboxView = useNavigationStore((s) => s.view.type === "inbox"); @@ -312,8 +487,12 @@ export function InboxSignalsTab() { (): SignalReportsQueryParams => ({ status: buildStatusFilterParam(statusFilter), ordering: buildSignalReportListOrdering(sortField, sortDirection), + source_product: + sourceProductFilter.length > 0 + ? sourceProductFilter.join(",") + : undefined, }), - [statusFilter, sortField, sortDirection], + [statusFilter, sortField, sortDirection, sourceProductFilter], ); const { @@ -381,8 +560,8 @@ export function InboxSignalsTab() { enabled: !!selectedReport, }); const allArtefacts = artefactsQuery.data?.results ?? []; - const visibleArtefacts = allArtefacts.filter( - (a): a is SignalReportArtefact => a.type !== "suggested_reviewers", + const videoSegments = allArtefacts.filter( + (a): a is SignalReportArtefact => a.type === "video_segment", ); const suggestedReviewers = useMemo(() => { const reviewerArtefact = allArtefacts.find( @@ -390,13 +569,21 @@ export function InboxSignalsTab() { ); return reviewerArtefact?.content ?? []; }, [allArtefacts]); - const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason; - const showArtefactsUnavailable = - !artefactsQuery.isLoading && - (!!artefactsQuery.error || !!artefactsUnavailableReason); - const artefactsUnavailableMessage = artefactsQuery.error - ? "Evidence could not be loaded right now. You can still create a task from this report." - : getArtefactsUnavailableMessage(artefactsUnavailableReason); + const judgments = useMemo(() => { + const safety = allArtefacts.find((a) => a.type === "safety_judgment"); + const actionability = allArtefacts.find( + (a) => a.type === "actionability_judgment", + ); + const safetyContent = + safety && !Array.isArray(safety.content) + ? (safety.content as unknown as Record) + : null; + const actionabilityContent = + actionability && !Array.isArray(actionability.content) + ? (actionability.content as unknown as Record) + : null; + return { safetyContent, actionabilityContent }; + }, [allArtefacts]); const signalsQuery = useInboxReportSignals(selectedReport?.id ?? "", { enabled: !!selectedReport, @@ -430,11 +617,11 @@ export function InboxSignalsTab() { if (!selectedReport) return null; return buildSignalTaskPrompt({ report: selectedReport, - artefacts: visibleArtefacts, + artefacts: videoSegments, signals, replayBaseUrl, }); - }, [selectedReport, visibleArtefacts, signals, replayBaseUrl]); + }, [selectedReport, videoSegments, signals, replayBaseUrl]); const handleCreateTask = () => { if (!selectedReport || selectedReport.status !== "ready") { @@ -527,7 +714,10 @@ export function InboxSignalsTab() { // ── Layout mode: full-width empty state vs two-pane ───────────────────── const hasReports = allReports.length > 0; - const showTwoPaneLayout = hasReports || !!searchQuery.trim(); + const hasActiveFilters = + sourceProductFilter.length > 0 || statusFilter.length < 5; + const showTwoPaneLayout = + hasReports || !!searchQuery.trim() || hasActiveFilters; // ── Determine right pane content (only used in two-pane mode) ────────── @@ -560,40 +750,38 @@ export function InboxSignalsTab() { - - - {cloudModeEnabled && ( + + + {cloudModeEnabled && ( + + )} + + {selectedReport && ( + )} - {!canActOnReport && selectedReport ? ( - - {selectedReport.status === "pending_input" - ? "This report needs input in PostHog before an agent can act on it." - : "Research is still running — you can read context below, then create a task when status is Ready."} - - ) : null} {/* ── Description ─────────────────────────────────────── */} - - - - - {selectedReport.signal_count} occurrences - - - {selectedReport.relevant_user_count ?? 0} affected users - - + {selectedReport.status !== "ready" ? ( + +
+ +
+
+ ) : ( + + )} + {suggestedReviewers.length > 0 && ( @@ -630,7 +823,12 @@ export function InboxSignalsTab() { {suggestedReviewers.map((reviewer) => ( - + @{reviewer.github_login} + + {reviewer.relevant_commits.length > 0 && ( + + {reviewer.relevant_commits.map((commit, i) => ( + + {i > 0 && ", "} + + + {commit.sha.slice(0, 7)} + + + + ))} + + )} ))} @@ -678,73 +896,65 @@ export function InboxSignalsTab() { )} - {/* ── Evidence ────────────────────────────────────────── */} - - - Evidence - - {artefactsQuery.isLoading && ( - - Loading evidence... - - )} - {showArtefactsUnavailable && ( - - {artefactsUnavailableMessage} - - )} - {!artefactsQuery.isLoading && - !showArtefactsUnavailable && - visibleArtefacts.length === 0 && ( - - No artefacts were returned for this signal. - - )} + {/* ── LLM judgments ──────────────────────────────────── */} + {(judgments.safetyContent || judgments.actionabilityContent) && ( + + )} - - {visibleArtefacts.map((artefact) => ( - - 0 && ( + + + Session segments + + + {videoSegments.map((artefact) => ( + - {artefact.content.content} - - - - - - {artefact.content.start_time - ? new Date( - artefact.content.start_time, - ).toLocaleString() - : "Unknown time"} - + + {artefact.content.content} + + + + + + {artefact.content.start_time + ? new Date( + artefact.content.start_time, + ).toLocaleString() + : "Unknown time"} + + + {replayBaseUrl && artefact.content.session_id && ( + + View replay + + + )} - {replayBaseUrl && artefact.content.session_id && ( - - View replay - - - )} - - - ))} - - + + ))} +
+ + )}
{/* ── Research task logs (bottom preview + overlay) ─────── */} @@ -810,7 +1020,15 @@ export function InboxSignalsTab() { leftPaneList = ( - No matching signals + No matching reports + + + ); + } else if (reports.length === 0 && hasActiveFilters) { + leftPaneList = ( + + + No reports match current filters ); @@ -883,7 +1101,6 @@ export function InboxSignalsTab() { style={{ height: "100%" }} > - {skeletonBackdrop} @@ -954,6 +1172,7 @@ export function InboxSignalsTab() { ) : ( setSourcesDialogOpen(true)} + enabledProducts={enabledProducts} /> )} diff --git a/apps/code/src/renderer/features/inbox/components/InboxView.tsx b/apps/code/src/renderer/features/inbox/components/InboxView.tsx index edfb34758..8c5b9948b 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxView.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxView.tsx @@ -1,4 +1,4 @@ -import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; +import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore"; import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; import { EnvelopeSimpleIcon, GearSixIcon } from "@phosphor-icons/react"; import { Box, Flex, Text } from "@radix-ui/themes"; @@ -6,7 +6,7 @@ import { useMemo } from "react"; import { InboxSignalsTab } from "./InboxSignalsTab"; export function InboxView() { - const openSettings = useSettingsDialogStore((s) => s.open); + const openSourcesDialog = useInboxSourcesDialogStore((s) => s.setOpen); const headerContent = useMemo( () => ( @@ -22,15 +22,15 @@ export function InboxView() { ), - [openSettings], + [openSourcesDialog], ); useSetHeaderContent(headerContent); diff --git a/apps/code/src/renderer/features/inbox/components/ReportCard.tsx b/apps/code/src/renderer/features/inbox/components/ReportCard.tsx index 9c4b70c36..5dba28277 100644 --- a/apps/code/src/renderer/features/inbox/components/ReportCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/ReportCard.tsx @@ -1,15 +1,33 @@ import { SignalReportPriorityBadge } from "@features/inbox/components/SignalReportPriorityBadge"; +import { SignalReportStatusBadge } from "@features/inbox/components/SignalReportStatusBadge"; import { SignalReportSummaryMarkdown } from "@features/inbox/components/SignalReportSummaryMarkdown"; +import { inboxStatusAccentCss } from "@features/inbox/utils/inboxSort"; import { - inboxStatusAccentCss, - inboxStatusLabel, -} from "@features/inbox/utils/inboxSort"; -import { UserIcon } from "@phosphor-icons/react"; + BrainIcon, + BugIcon, + EyeIcon, + GithubLogoIcon, + KanbanIcon, + TicketIcon, + VideoIcon, +} from "@phosphor-icons/react"; import { Flex, Text, Tooltip } from "@radix-ui/themes"; import type { SignalReport } from "@shared/types"; import { motion } from "framer-motion"; import type { KeyboardEvent, MouseEvent } from "react"; +const SOURCE_PRODUCT_ICONS: Record< + string, + { icon: React.ReactNode; color: string } +> = { + session_replay: { icon: , color: "var(--amber-9)" }, + error_tracking: { icon: , color: "var(--red-9)" }, + llm_analytics: { icon: , color: "var(--purple-9)" }, + github: { icon: , color: "var(--gray-11)" }, + linear: { icon: , color: "var(--blue-9)" }, + zendesk: { icon: , color: "var(--green-9)" }, +}; + interface ReportCardProps { report: SignalReport; isSelected: boolean; @@ -45,7 +63,6 @@ export function ReportCard({ : "light"; const accent = inboxStatusAccentCss(report.status); - const statusLabel = inboxStatusLabel(report.status); const isReady = report.status === "ready"; const handleActivate = (e: MouseEvent | KeyboardEvent): void => { @@ -75,26 +92,43 @@ export function ReportCard({ }} className="w-full cursor-pointer overflow-hidden border-gray-5 border-b py-2 pr-3 pl-2 text-left transition-colors hover:bg-gray-2" style={{ - backgroundColor: isSelected ? "var(--gray-3)" : "transparent", + backgroundColor: isSelected + ? "var(--gray-3)" + : report.is_suggested_reviewer + ? "var(--blue-2)" + : "transparent", boxShadow: `inset 3px 0 0 0 ${accent}`, }} > - {/* Bullet stays in its own column so title + badges wrap under each other, not under the dot */} - - + + {/* Source product icons — pt-1 (4px) centers 12px icons + with the title's 13px/~20px effective line height */} + + {(report.source_products ?? []).length > 0 ? ( + (report.source_products ?? []).map((sp) => { + const info = SOURCE_PRODUCT_ICONS[sp]; + return info ? ( + + {info.icon} + + ) : null; + }) + ) : ( + + )} + {report.title ?? "Untitled signal"} - - {statusLabel} - + {report.is_suggested_reviewer && ( @@ -129,21 +154,19 @@ export function ReportCard({ border: "1px solid var(--blue-6)", }} > - + )} {/* Summary is outside the title row so wrapped lines align with title text (bullet + gap), not the card edge */} -
+
diff --git a/apps/code/src/renderer/features/inbox/components/ReportTaskLogs.tsx b/apps/code/src/renderer/features/inbox/components/ReportTaskLogs.tsx index e8ce73713..9b75c7270 100644 --- a/apps/code/src/renderer/features/inbox/components/ReportTaskLogs.tsx +++ b/apps/code/src/renderer/features/inbox/components/ReportTaskLogs.tsx @@ -6,7 +6,7 @@ import { CircleNotchIcon, XCircleIcon, } from "@phosphor-icons/react"; -import { Flex, Spinner, Text } from "@radix-ui/themes"; +import { Flex, Spinner, Text, Tooltip } from "@radix-ui/themes"; import type { SignalReportStatus, Task } from "@shared/types"; import { useState } from "react"; @@ -86,6 +86,7 @@ export function ReportTaskLogs({ const showBar = isLoading || !!task || + reportStatus === "candidate" || reportStatus === "in_progress" || reportStatus === "ready"; @@ -95,8 +96,27 @@ export function ReportTaskLogs({ const hasTask = !isLoading && !!task; - // No task — simple in-flow status bar + // No task yet — show pipeline status with tooltip explaining what's happening if (!hasTask) { + let statusText: string; + let tooltipText: string; + if (isLoading) { + statusText = "Loading task…"; + tooltipText = "Checking if a research task exists for this report."; + } else if (reportStatus === "candidate") { + statusText = "Queued for research"; + tooltipText = + "This report has been queued. A repository will be selected and then an AI agent will research it."; + } else if (reportStatus === "in_progress") { + statusText = "Research is starting…"; + tooltipText = + "An AI research agent is being set up. Logs will appear here once the agent starts running."; + } else { + statusText = "Waiting for research task"; + tooltipText = + "No research task has been created yet. One will appear when the report is picked up for investigation."; + } + return ( - - - {isLoading - ? "Loading task…" - : reportStatus === "in_progress" - ? "Research is running…" - : "No research task yet."} - + + + + + {statusText} + + + ); } diff --git a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx index 34a6f3b9b..728152403 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx @@ -1,3 +1,4 @@ +import { MarkdownRenderer } from "@features/editor/components/MarkdownRenderer"; import { ArrowSquareOutIcon, BrainIcon, @@ -143,7 +144,20 @@ function truncateBody(body: string, maxLength = COLLAPSE_THRESHOLD): string { const truncated = body.slice(0, maxLength); const lastNewline = truncated.lastIndexOf("\n"); const cutPoint = lastNewline > maxLength * 0.5 ? lastNewline : maxLength; - return `${truncated.slice(0, cutPoint)}…`; + let result = truncated.slice(0, cutPoint); + // Close any open code fences so markdown renders cleanly + const fenceCount = (result.match(/^```/gm) || []).length; + if (fenceCount % 2 !== 0) { + // Trim trailing partial fence line (e.g. just "```" with no content after) + const lastFence = result.lastIndexOf("```"); + const afterFence = result.slice(lastFence + 3).trim(); + if (!afterFence) { + result = result.slice(0, lastFence).trimEnd(); + } else { + result += "\n```"; + } + } + return `${result}\n\n…`; } function parseExtra(raw: Record): Record { @@ -225,16 +239,16 @@ function SignalCardHeader({ signal }: { signal: Signal }) { function CollapsibleBody({ body }: { body: string }) { const [expanded, setExpanded] = useState(false); const isLong = body.length > COLLAPSE_THRESHOLD; + const displayBody = isLong && !expanded ? truncateBody(body) : body; return ( - - {isLong && !expanded ? truncateBody(body) : body} - + + {isLong && ( + ); + })} + + diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts index a186b420f..8298420f8 100644 --- a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts +++ b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts @@ -12,6 +12,14 @@ type SignalSortField = Extract< type SignalSortDirection = "asc" | "desc"; +export type SourceProduct = + | "session_replay" + | "error_tracking" + | "llm_analytics" + | "github" + | "linear" + | "zendesk"; + const DEFAULT_STATUS_FILTER: SignalReportStatus[] = [ "ready", "pending_input", @@ -25,6 +33,8 @@ interface InboxSignalsFilterState { sortDirection: SignalSortDirection; searchQuery: string; statusFilter: SignalReportStatus[]; + /** Empty array means "all sources" (no filter). */ + sourceProductFilter: SourceProduct[]; } interface InboxSignalsFilterActions { @@ -32,6 +42,7 @@ interface InboxSignalsFilterActions { setSearchQuery: (query: string) => void; setStatusFilter: (statuses: SignalReportStatus[]) => void; toggleStatus: (status: SignalReportStatus) => void; + toggleSourceProduct: (source: SourceProduct) => void; } type InboxSignalsFilterStore = InboxSignalsFilterState & @@ -44,6 +55,7 @@ export const useInboxSignalsFilterStore = create()( sortDirection: "asc", searchQuery: "", statusFilter: DEFAULT_STATUS_FILTER, + sourceProductFilter: [], setSort: (sortField, sortDirection) => set({ sortField, sortDirection }), setSearchQuery: (searchQuery) => set({ searchQuery }), setStatusFilter: (statusFilter) => set({ statusFilter }), @@ -55,6 +67,14 @@ export const useInboxSignalsFilterStore = create()( : [...current, status]; return { statusFilter: next.length > 0 ? next : current }; }), + toggleSourceProduct: (source) => + set((state) => { + const current = state.sourceProductFilter; + const next = current.includes(source) + ? current.filter((s) => s !== source) + : [...current, source]; + return { sourceProductFilter: next }; + }), }), { name: "inbox-signals-filter-storage", @@ -62,6 +82,7 @@ export const useInboxSignalsFilterStore = create()( sortField: state.sortField, sortDirection: state.sortDirection, statusFilter: state.statusFilter, + sourceProductFilter: state.sourceProductFilter, }), }, ), diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSourcesDialogStore.ts b/apps/code/src/renderer/features/inbox/stores/inboxSourcesDialogStore.ts new file mode 100644 index 000000000..362264fda --- /dev/null +++ b/apps/code/src/renderer/features/inbox/stores/inboxSourcesDialogStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface InboxSourcesDialogStore { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const useInboxSourcesDialogStore = create()( + (set) => ({ + open: false, + setOpen: (open) => set({ open }), + }), +); diff --git a/apps/code/src/renderer/features/inbox/utils/inboxSort.ts b/apps/code/src/renderer/features/inbox/utils/inboxSort.ts index 58c821a64..47c29005d 100644 --- a/apps/code/src/renderer/features/inbox/utils/inboxSort.ts +++ b/apps/code/src/renderer/features/inbox/utils/inboxSort.ts @@ -41,3 +41,57 @@ export function inboxStatusAccentCss(status: SignalReportStatus): string { return "var(--gray-8)"; } } + +/** Higher-contrast text color for status badges (step 11 instead of 9). */ +export function inboxStatusTextCss(status: SignalReportStatus): string { + switch (status) { + case "ready": + return "var(--green-11)"; + case "pending_input": + return "var(--violet-11)"; + case "in_progress": + return "var(--amber-11)"; + case "candidate": + return "var(--cyan-11)"; + case "potential": + return "var(--gray-11)"; + case "failed": + return "var(--red-11)"; + default: + return "var(--gray-11)"; + } +} + +export function inboxStatusBgCss(status: SignalReportStatus): string { + switch (status) { + case "ready": + return "var(--green-3)"; + case "pending_input": + return "var(--violet-3)"; + case "in_progress": + return "var(--amber-3)"; + case "candidate": + return "var(--cyan-3)"; + case "failed": + return "var(--red-3)"; + default: + return "var(--gray-3)"; + } +} + +export function inboxStatusBorderCss(status: SignalReportStatus): string { + switch (status) { + case "ready": + return "var(--green-6)"; + case "pending_input": + return "var(--violet-6)"; + case "in_progress": + return "var(--amber-6)"; + case "candidate": + return "var(--cyan-6)"; + case "failed": + return "var(--red-6)"; + default: + return "var(--gray-6)"; + } +} diff --git a/apps/code/src/shared/types.ts b/apps/code/src/shared/types.ts index 64147bba4..e0bce5e0f 100644 --- a/apps/code/src/shared/types.ts +++ b/apps/code/src/shared/types.ts @@ -192,6 +192,8 @@ export interface SignalReport { priority?: SignalReportPriority | null; /** Whether the current user is a suggested reviewer for this report (server-annotated). */ is_suggested_reviewer?: boolean; + /** Distinct source products contributing signals to this report (e.g. "session_replay", "error_tracking"). */ + source_products?: string[]; } export interface SignalReportArtefactContent { @@ -300,4 +302,6 @@ export interface SignalReportsQueryParams { * `created_at`, `updated_at`, `id`. Example: `status,-total_weight`. */ ordering?: string; + /** Comma-separated source products — only returns reports with signals from these sources. */ + source_product?: string; }