From ebdcc35c405acba9567c11b209fe3414ee80b5ef Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 1 Apr 2026 03:17:30 +0200 Subject: [PATCH 1/8] feat(inbox): error tracking source, LLM evaluations, source config improvements - Add error tracking as signal source with 3 sub-types toggled together - Replace non-functional LLM analytics toggle with evaluations list (polls 5s) - Evaluations link to Cloud for management (region-aware) - Fix re-render cascade: direct API calls, per-source optimistic state - Per-source onToggle API with memoized cards - Rounded toggle cards, GitHub OAuth flow - Add suggested reviewer + artefact types --- apps/code/src/renderer/api/posthogClient.ts | 55 ++- .../inbox/components/DataSourceSetup.tsx | 95 +++- .../inbox/components/InboxSignalsTab.tsx | 7 +- .../inbox/components/SignalSourceToggles.tsx | 205 +++++++-- .../features/inbox/hooks/useEvaluations.ts | 19 + .../inbox/hooks/useSignalSourceManager.ts | 415 +++++++++++------- .../onboarding/components/SignalsStep.tsx | 16 +- .../onboarding/hooks/useTutorialTour.ts | 8 +- .../utils/generateInstrumentationPrompt.ts | 8 +- .../sections/SignalSourcesSettings.tsx | 12 +- apps/code/src/shared/types.ts | 35 +- 11 files changed, 643 insertions(+), 232 deletions(-) create mode 100644 apps/code/src/renderer/features/inbox/hooks/useEvaluations.ts diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index ca863b679..e78e2339d 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -20,6 +20,8 @@ export type McpRecommendedServer = Schemas.RecommendedServer; export type McpServerInstallation = Schemas.MCPServerInstallation; +export type Evaluation = Schemas.Evaluation; + export interface SignalSourceConfig { id: string; source_product: @@ -27,8 +29,16 @@ export interface SignalSourceConfig { | "llm_analytics" | "github" | "linear" - | "zendesk"; - source_type: "session_analysis_cluster" | "evaluation" | "issue" | "ticket"; + | "zendesk" + | "error_tracking"; + source_type: + | "session_analysis_cluster" + | "evaluation" + | "issue" + | "ticket" + | "issue_created" + | "issue_reopened" + | "issue_spiking"; enabled: boolean; config: Record; created_at: string; @@ -223,17 +233,8 @@ export class PostHogAPIClient { async createSignalSourceConfig( projectId: number, options: { - source_product: - | "session_replay" - | "llm_analytics" - | "github" - | "linear" - | "zendesk"; - source_type: - | "session_analysis_cluster" - | "evaluation" - | "issue" - | "ticket"; + source_product: SignalSourceConfig["source_product"]; + source_type: SignalSourceConfig["source_type"]; enabled: boolean; config?: Record; }, @@ -287,6 +288,34 @@ export class PostHogAPIClient { return (await response.json()) as SignalSourceConfig; } + async listEvaluations(projectId: number): Promise { + const data = await this.api.get( + "/api/environments/{project_id}/evaluations/", + { + path: { project_id: projectId.toString() }, + query: { limit: 200 }, + }, + ); + return data.results ?? []; + } + + async updateEvaluation( + projectId: number, + evaluationId: string, + updates: { enabled: boolean }, + ): Promise { + return await this.api.patch( + "/api/environments/{project_id}/evaluations/{id}/", + { + path: { + project_id: projectId.toString(), + id: evaluationId, + }, + body: updates, + }, + ); + } + async listExternalDataSources( projectId: number, ): Promise { diff --git a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx index 9d365ee4f..fd2178ae0 100644 --- a/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx +++ b/apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx @@ -52,13 +52,41 @@ interface SetupFormProps { onCancel: () => void; } +const POLL_INTERVAL_GITHUB_MS = 3_000; +const POLL_TIMEOUT_GITHUB_MS = 300_000; + function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { const projectId = useAuthStateValue((state) => state.projectId); + const cloudRegion = useAuthStateValue((state) => state.cloudRegion); const client = useAuthenticatedClient(); const { githubIntegration, repositories, isLoadingRepos } = useRepositoryIntegration(); const [repo, setRepo] = useState(null); const [loading, setLoading] = useState(false); + const [connecting, setConnecting] = useState(false); + const pollTimerRef = useRef | null>(null); + const pollTimeoutRef = useRef | null>(null); + + const stopPolling = useCallback(() => { + if (pollTimerRef.current) { + clearInterval(pollTimerRef.current); + pollTimerRef.current = null; + } + if (pollTimeoutRef.current) { + clearTimeout(pollTimeoutRef.current); + pollTimeoutRef.current = null; + } + }, []); + + useEffect(() => stopPolling, [stopPolling]); + + // Stop polling once integration appears + useEffect(() => { + if (githubIntegration && connecting) { + stopPolling(); + setConnecting(false); + } + }, [githubIntegration, connecting, stopPolling]); // Auto-select the first repo once loaded useEffect(() => { @@ -67,6 +95,47 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { } }, [repo, repositories]); + const handleConnectGitHub = useCallback(async () => { + if (!cloudRegion || !projectId) return; + setConnecting(true); + try { + await trpcClient.githubIntegration.startFlow.mutate({ + region: cloudRegion, + projectId, + }); + + pollTimerRef.current = setInterval(async () => { + try { + if (!client) return; + // Trigger a refetch of integrations + const integrations = + await client.getIntegrationsForProject(projectId); + const hasGithub = integrations.some( + (i: { kind: string }) => i.kind === "github", + ); + if (hasGithub) { + stopPolling(); + setConnecting(false); + toast.success("GitHub connected"); + } + } catch { + // Ignore individual poll failures + } + }, POLL_INTERVAL_GITHUB_MS); + + pollTimeoutRef.current = setTimeout(() => { + stopPolling(); + setConnecting(false); + toast.error("Connection timed out. Please try again."); + }, POLL_TIMEOUT_GITHUB_MS); + } catch (error) { + setConnecting(false); + toast.error( + error instanceof Error ? error.message : "Failed to start GitHub flow", + ); + } + }, [cloudRegion, projectId, client, stopPolling]); + const handleSubmit = useCallback(async () => { if (!projectId || !client || !repo || !githubIntegration) return; @@ -97,10 +166,28 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) { if (!githubIntegration) { return ( - - No GitHub integration found. Please connect GitHub during onboarding - first. - + + + Connect your GitHub account to import issues as signals. + + + + + + ); } diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 2bb2a12f2..098edeecc 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -42,6 +42,7 @@ import { } from "@radix-ui/themes"; import { getCloudUrlFromRegion } from "@shared/constants/oauth"; import type { + SignalReportArtefact, SignalReportArtefactsResponse, SignalReportsQueryParams, } from "@shared/types"; @@ -201,7 +202,9 @@ export function InboxSignalsTab() { const artefactsQuery = useInboxReportArtefacts(selectedReport?.id ?? "", { enabled: !!selectedReport, }); - const visibleArtefacts = artefactsQuery.data?.results ?? []; + const visibleArtefacts = (artefactsQuery.data?.results ?? []).filter( + (a): a is SignalReportArtefact => a.type === "video_segment", + ); const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason; const showArtefactsUnavailable = !artefactsQuery.isLoading && @@ -543,7 +546,7 @@ export function InboxSignalsTab() { !showArtefactsUnavailable && visibleArtefacts.length === 0 && ( - No artefacts were returned for this signal. + No session segments available for this report. )} diff --git a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx index f7aae6e0a..07738a965 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx @@ -1,15 +1,27 @@ import { + ArrowSquareOutIcon, BrainIcon, + BugIcon, GithubLogoIcon, KanbanIcon, TicketIcon, VideoIcon, } from "@phosphor-icons/react"; -import { Box, Button, Flex, Spinner, Switch, Text } from "@radix-ui/themes"; +import { + Box, + Button, + Flex, + Link, + Spinner, + Switch, + Text, +} from "@radix-ui/themes"; +import type { Evaluation } from "@renderer/api/posthogClient"; +import { memo, useCallback } from "react"; export interface SignalSourceValues { session_replay: boolean; - llm_analytics: boolean; + error_tracking: boolean; github: boolean; linear: boolean; zendesk: boolean; @@ -27,7 +39,7 @@ interface SignalSourceToggleCardProps { loading?: boolean; } -function SignalSourceToggleCard({ +const SignalSourceToggleCard = memo(function SignalSourceToggleCard({ icon, label, description, @@ -44,6 +56,7 @@ function SignalSourceToggleCard({ style={{ backgroundColor: "var(--color-panel-solid)", border: "1px solid var(--gray-4)", + borderRadius: "var(--radius-3)", cursor: disabled || loading ? "default" : "pointer", }} onClick={ @@ -90,11 +103,116 @@ function SignalSourceToggleCard({ ); +}); + +interface EvaluationRowProps { + evaluation: Evaluation; + onToggle: (id: string, enabled: boolean) => void; +} + +const EvaluationRow = memo(function EvaluationRow({ + evaluation, + onToggle, +}: EvaluationRowProps) { + const handleChange = useCallback( + (checked: boolean) => onToggle(evaluation.id, checked), + [onToggle, evaluation.id], + ); + + return ( + + + {evaluation.name} + + + + ); +}); + +interface EvaluationsSectionProps { + evaluations: Evaluation[]; + evaluationsUrl: string; + onToggleEvaluation: (id: string, enabled: boolean) => void; } +export const EvaluationsSection = memo(function EvaluationsSection({ + evaluations, + evaluationsUrl, + onToggleEvaluation, +}: EvaluationsSectionProps) { + return ( + + + + + + + + + LLM evaluations + + + Ongoing evaluation of how your AI features are performing based on + defined criteria + + + + + + {evaluations.length > 0 ? ( + + {evaluations.map((evaluation) => ( + + ))} + + ) : ( + + No evaluations configured yet. + + )} + + + Manage evaluations in PostHog Cloud + + + + + + ); +}); + interface SignalSourceTogglesProps { value: SignalSourceValues; - onChange: (value: SignalSourceValues) => void; + onToggle: (source: keyof SignalSourceValues, enabled: boolean) => void; disabled?: boolean; sourceStates?: Partial< Record< @@ -103,68 +221,101 @@ interface SignalSourceTogglesProps { > >; onSetup?: (source: keyof SignalSourceValues) => void; + evaluations?: Evaluation[]; + evaluationsUrl?: string; + onToggleEvaluation?: (id: string, enabled: boolean) => void; } export function SignalSourceToggles({ value, - onChange, + onToggle, disabled, sourceStates, onSetup, + evaluations, + evaluationsUrl, + onToggleEvaluation, }: SignalSourceTogglesProps) { + const toggleSessionReplay = useCallback( + (checked: boolean) => onToggle("session_replay", checked), + [onToggle], + ); + const toggleErrorTracking = useCallback( + (checked: boolean) => onToggle("error_tracking", checked), + [onToggle], + ); + const toggleGithub = useCallback( + (checked: boolean) => onToggle("github", checked), + [onToggle], + ); + const toggleLinear = useCallback( + (checked: boolean) => onToggle("linear", checked), + [onToggle], + ); + const toggleZendesk = useCallback( + (checked: boolean) => onToggle("zendesk", checked), + [onToggle], + ); + const setupGithub = useCallback(() => onSetup?.("github"), [onSetup]); + const setupLinear = useCallback(() => onSetup?.("linear"), [onSetup]); + const setupZendesk = useCallback(() => onSetup?.("zendesk"), [onSetup]); + return ( } - label="Session replay" - description="Allow PostHog to watch session recordings for you, and spot UX issues." + label="PostHog Session Replay" + description="Analyze session recordings and event data for UX issues" checked={value.session_replay} - onCheckedChange={(checked) => - onChange({ ...value, session_replay: checked }) - } + onCheckedChange={toggleSessionReplay} disabled={disabled} /> } - label="LLM analytics" - description="Allow PostHog to evaluate live LLM traces for you, and flag anomalies." - checked={value.llm_analytics} - onCheckedChange={(checked) => - onChange({ ...value, llm_analytics: checked }) - } + icon={} + label="PostHog Error Tracking" + description="Surface new issues, reopenings, and volume spikes" + checked={value.error_tracking} + onCheckedChange={toggleErrorTracking} disabled={disabled} /> + {evaluations && evaluationsUrl && onToggleEvaluation && ( + + )} } - label="GitHub" - description="Allow PostHog to read GitHub issues for you, and highlight what needs attention." + label="GitHub Issues" + description="Monitor new issues and updates" checked={value.github} - onCheckedChange={(checked) => onChange({ ...value, github: checked })} + onCheckedChange={toggleGithub} disabled={disabled} requiresSetup={sourceStates?.github?.requiresSetup} - onSetup={() => onSetup?.("github")} + onSetup={setupGithub} loading={sourceStates?.github?.loading} /> } label="Linear" - description="Allow PostHog to read Linear issues for you, and pick out priorities." + description="Monitor new issues and updates" checked={value.linear} - onCheckedChange={(checked) => onChange({ ...value, linear: checked })} + onCheckedChange={toggleLinear} disabled={disabled} requiresSetup={sourceStates?.linear?.requiresSetup} - onSetup={() => onSetup?.("linear")} + onSetup={setupLinear} loading={sourceStates?.linear?.loading} /> } label="Zendesk" - description="Allow PostHog to investigate support tickets for you, and find follow-ups." + description="Monitor incoming support tickets" checked={value.zendesk} - onCheckedChange={(checked) => onChange({ ...value, zendesk: checked })} + onCheckedChange={toggleZendesk} disabled={disabled} requiresSetup={sourceStates?.zendesk?.requiresSetup} - onSetup={() => onSetup?.("zendesk")} + onSetup={setupZendesk} loading={sourceStates?.zendesk?.loading} /> diff --git a/apps/code/src/renderer/features/inbox/hooks/useEvaluations.ts b/apps/code/src/renderer/features/inbox/hooks/useEvaluations.ts new file mode 100644 index 000000000..dcd207e93 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/hooks/useEvaluations.ts @@ -0,0 +1,19 @@ +import { useAuthStore } from "@features/auth/stores/authStore"; +import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; +import type { Evaluation } from "@renderer/api/posthogClient"; + +const POLL_INTERVAL_MS = 5_000; + +export function useEvaluations() { + const projectId = useAuthStore((s) => s.projectId); + return useAuthenticatedQuery( + ["evaluations", projectId], + (client) => + projectId ? client.listEvaluations(projectId) : Promise.resolve([]), + { + enabled: !!projectId, + staleTime: POLL_INTERVAL_MS, + refetchInterval: POLL_INTERVAL_MS, + }, + ); +} diff --git a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts index dcb74a7aa..b4e532911 100644 --- a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts +++ b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts @@ -1,33 +1,45 @@ import { useAuthenticatedClient } from "@features/auth/hooks/authClient"; import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import type { SignalSourceValues } from "@features/inbox/components/SignalSourceToggles"; -import { useAuthenticatedMutation } from "@hooks/useAuthenticatedMutation"; +import type { + Evaluation, + SignalSourceConfig, +} from "@renderer/api/posthogClient"; +import { getCloudUrlFromRegion } from "@shared/constants/oauth"; import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { useEvaluations } from "./useEvaluations"; import { useExternalDataSources } from "./useExternalDataSources"; import { useSignalSourceConfigs } from "./useSignalSourceConfigs"; -type SourceProduct = - | "session_replay" - | "llm_analytics" - | "github" - | "linear" - | "zendesk"; -type SourceType = - | "session_analysis_cluster" - | "evaluation" - | "issue" - | "ticket"; - -const SOURCE_TYPE_MAP: Record = { +type SourceProduct = SignalSourceConfig["source_product"]; +type SourceType = SignalSourceConfig["source_type"]; + +const SOURCE_TYPE_MAP: Record< + Exclude, + SourceType +> = { session_replay: "session_analysis_cluster", - llm_analytics: "evaluation", github: "issue", linear: "issue", zendesk: "ticket", }; +const ERROR_TRACKING_SOURCE_TYPES: SourceType[] = [ + "issue_created", + "issue_reopened", + "issue_spiking", +]; + +const SOURCE_LABELS: Record = { + session_replay: "Session replay", + error_tracking: "Error tracking", + github: "GitHub Issues", + linear: "Linear Issues", + zendesk: "Zendesk Tickets", +}; + const DATA_WAREHOUSE_SOURCES: Record< string, { dwSourceType: string; requiredTable: string } @@ -37,23 +49,61 @@ const DATA_WAREHOUSE_SOURCES: Record< zendesk: { dwSourceType: "Zendesk", requiredTable: "tickets" }, }; -const ALL_SOURCE_PRODUCTS: SourceProduct[] = [ +const ALL_SOURCE_PRODUCTS: (keyof SignalSourceValues)[] = [ "session_replay", - "llm_analytics", + "error_tracking", "github", "linear", "zendesk", ]; +function computeValues( + configs: SignalSourceConfig[] | undefined, +): SignalSourceValues { + const result: SignalSourceValues = { + session_replay: false, + error_tracking: false, + github: false, + linear: false, + zendesk: false, + }; + if (!configs?.length) return result; + for (const product of ALL_SOURCE_PRODUCTS) { + if (product === "error_tracking") { + result.error_tracking = ERROR_TRACKING_SOURCE_TYPES.every((st) => + configs.some( + (c) => + c.source_product === "error_tracking" && + c.source_type === st && + c.enabled, + ), + ); + } else { + result[product] = configs.some( + (c) => c.source_product === product && c.enabled, + ); + } + } + return result; +} + export function useSignalSourceManager() { const projectId = useAuthStateValue((state) => state.projectId); + const cloudRegion = useAuthStateValue((state) => state.cloudRegion); const client = useAuthenticatedClient(); const queryClient = useQueryClient(); const { data: configs, isLoading: configsLoading } = useSignalSourceConfigs(); const { data: externalSources, isLoading: sourcesLoading } = useExternalDataSources(); - const savingRef = useRef(false); - const [optimistic, setOptimistic] = useState(null); + const { data: evaluations } = useEvaluations(); + + // Optimistic overrides keyed by source product — only sources actively being + // toggled get an entry, so unrelated sources never see a prop change. + const [optimistic, setOptimistic] = useState< + Partial> + >({}); + const pendingRef = useRef(new Set()); + const [setupSource, setSetupSource] = useState< "github" | "linear" | "zendesk" | null >(null); @@ -75,23 +125,16 @@ export function useSignalSourceManager() { [externalSources], ); - const serverValues = useMemo(() => { - const result: SignalSourceValues = { - session_replay: false, - llm_analytics: false, - github: false, - linear: false, - zendesk: false, - }; - for (const product of ALL_SOURCE_PRODUCTS) { - result[product] = !!configs?.some( - (c) => c.source_product === product && c.enabled, - ); - } - return result; - }, [configs]); + const serverValues = useMemo( + () => computeValues(configs), + [configs], + ); - const displayValues = optimistic ?? serverValues; + // Merge: optimistic overrides take precedence over server values. + const displayValues = useMemo(() => { + if (Object.keys(optimistic).length === 0) return serverValues; + return { ...serverValues, ...optimistic }; + }, [serverValues, optimistic]); const sourceStates = useMemo(() => { const states: Partial< @@ -111,29 +154,48 @@ export function useSignalSourceManager() { return states; }, [findExternalSource, serverValues, loadingSources]); - const createConfig = useAuthenticatedMutation( - ( - apiClient, - options: { - source_product: SourceProduct; - source_type: SourceType; - }, - ) => - projectId - ? apiClient.createSignalSourceConfig(projectId, { - ...options, - enabled: true, - }) - : Promise.reject(new Error("No project selected")), - ); + const evaluationsUrl = useMemo(() => { + if (!cloudRegion) return ""; + return `${getCloudUrlFromRegion(cloudRegion)}/llm-analytics/evaluations`; + }, [cloudRegion]); - const updateConfig = useAuthenticatedMutation( - (apiClient, options: { configId: string; enabled: boolean }) => - projectId - ? apiClient.updateSignalSourceConfig(projectId, options.configId, { - enabled: options.enabled, - }) - : Promise.reject(new Error("No project selected")), + // Optimistic evaluation state: map of evaluation ID to overridden enabled value + const [optimisticEvals, setOptimisticEvals] = useState< + Record + >({}); + + const displayEvaluations = useMemo(() => { + if (!evaluations) return []; + if (Object.keys(optimisticEvals).length === 0) return evaluations; + return evaluations.map((e) => + e.id in optimisticEvals ? { ...e, enabled: optimisticEvals[e.id] } : e, + ); + }, [evaluations, optimisticEvals]); + + const handleToggleEvaluation = useCallback( + async (evaluationId: string, enabled: boolean) => { + if (!client || !projectId) return; + + setOptimisticEvals((prev) => ({ ...prev, [evaluationId]: enabled })); + + try { + await client.updateEvaluation(projectId, evaluationId, { enabled }); + await queryClient.invalidateQueries({ queryKey: ["evaluations"] }); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : "Failed to toggle evaluation"; + toast.error(message); + } finally { + setOptimisticEvals((prev) => { + const next = { ...prev }; + delete next[evaluationId]; + return next; + }); + } + }, + [client, projectId, queryClient], ); const ensureRequiredTableSyncing = useCallback( @@ -183,148 +245,165 @@ export function useSignalSourceManager() { } }, []); - const handleSetupComplete = useCallback(async () => { - const completedSource = setupSource; - setSetupSource(null); - - // Create the signal source config for the source that was just connected - if (completedSource) { - const existing = configs?.find( - (c) => c.source_product === completedSource, - ); - if (!existing) { - try { - await createConfig.mutateAsync({ - source_product: completedSource, - source_type: SOURCE_TYPE_MAP[completedSource], - }); - } catch { - toast.error( - "Data source connected, but failed to enable signal source. Try toggling it on.", - ); + const invalidateAfterToggle = useCallback(async () => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: ["signals", "source-configs"], + }), + queryClient.invalidateQueries({ + queryKey: ["inbox", "signal-reports"], + }), + ]); + }, [queryClient]); + + // Toggle a single source product. Calls the API directly (no react-query + // mutation tracking) so intermediate loading/success states don't cause + // cascading re-renders. + const handleToggle = useCallback( + async (product: keyof SignalSourceValues, enabled: boolean) => { + if (!client || !projectId) return; + if (pendingRef.current.has(product)) return; + + // Warehouse sources without a connected external data source need setup first + if (enabled && product in DATA_WAREHOUSE_SOURCES) { + const hasExternalSource = !!findExternalSource(product); + if (!hasExternalSource) { + setSetupSource(product as "github" | "linear" | "zendesk"); + return; } - } else if (!existing.enabled) { + + setLoadingSources((prev) => ({ ...prev, [product]: true })); try { - await updateConfig.mutateAsync({ - configId: existing.id, - enabled: true, - }); - } catch { - toast.error( - "Data source connected, but failed to enable signal source. Try toggling it on.", - ); + await ensureRequiredTableSyncing(product); + } finally { + setLoadingSources((prev) => ({ ...prev, [product]: false })); } } - } - - await queryClient.invalidateQueries({ - queryKey: ["external-data-sources"], - }); - await queryClient.invalidateQueries({ - queryKey: ["signals", "source-configs"], - }); - await queryClient.invalidateQueries({ - queryKey: ["inbox", "signal-reports"], - }); - }, [queryClient, setupSource, configs, createConfig, updateConfig]); - const handleSetupCancel = useCallback(() => { - setSetupSource(null); - }, []); + // Optimistic update — only touches this one key + pendingRef.current.add(product); + setOptimistic((prev) => ({ ...prev, [product]: enabled })); - const handleChange = useCallback( - async (values: SignalSourceValues) => { - if (savingRef.current) return; + const label = SOURCE_LABELS[product]; - setOptimistic(values); try { - const operations: Array<() => Promise> = []; - - for (const product of ALL_SOURCE_PRODUCTS) { - const wanted = values[product]; - const current = serverValues[product]; - if (wanted === current) continue; - - // If enabling a warehouse source without an external data source, open setup - if (wanted && product in DATA_WAREHOUSE_SOURCES) { - const hasExternalSource = !!findExternalSource(product); - if (!hasExternalSource) { - setSetupSource(product as "github" | "linear" | "zendesk"); - return; - } - - // Ensure required table is syncing - setLoadingSources((prev) => ({ ...prev, [product]: true })); - try { - await ensureRequiredTableSyncing(product); - } finally { - setLoadingSources((prev) => ({ ...prev, [product]: false })); + if (product === "error_tracking") { + for (const sourceType of ERROR_TRACKING_SOURCE_TYPES) { + const existing = configs?.find( + (c) => + c.source_product === "error_tracking" && + c.source_type === sourceType, + ); + if (existing) { + await client.updateSignalSourceConfig(projectId, existing.id, { + enabled, + }); + } else if (enabled) { + await client.createSignalSourceConfig(projectId, { + source_product: "error_tracking", + source_type: sourceType, + enabled: true, + }); } } - + } else { const existing = configs?.find((c) => c.source_product === product); - - if (wanted && !existing) { - operations.push(() => - createConfig.mutateAsync({ - source_product: product, - source_type: SOURCE_TYPE_MAP[product], - }), - ); - } else if (existing) { - operations.push(() => - updateConfig.mutateAsync({ - configId: existing.id, - enabled: wanted, - }), - ); + if (existing) { + await client.updateSignalSourceConfig(projectId, existing.id, { + enabled, + }); + } else if (enabled) { + await client.createSignalSourceConfig(projectId, { + source_product: product, + source_type: + SOURCE_TYPE_MAP[ + product as Exclude< + SourceProduct, + "error_tracking" | "llm_analytics" + > + ], + enabled: true, + }); } } - if (operations.length === 0) { - return; - } - - savingRef.current = true; - const results = await Promise.allSettled(operations.map((op) => op())); - const failed = results.filter((r) => r.status === "rejected"); - if (failed.length > 0) { - toast.error("Failed to update signal sources. Please try again."); - return; - } - - await queryClient.invalidateQueries({ - queryKey: ["signals", "source-configs"], - }); - await queryClient.invalidateQueries({ - queryKey: ["inbox", "signal-reports"], - }); - } catch { - toast.error("Failed to update signal sources. Please try again."); + await invalidateAfterToggle(); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : `Failed to toggle ${label}`; + toast.error(message); } finally { - savingRef.current = false; - setOptimistic(null); + pendingRef.current.delete(product); + setOptimistic((prev) => { + const next = { ...prev }; + delete next[product]; + return next; + }); } }, [ - serverValues, + client, + projectId, configs, - createConfig, - updateConfig, - queryClient, findExternalSource, ensureRequiredTableSyncing, + invalidateAfterToggle, ], ); + const handleSetupComplete = useCallback(async () => { + const completedSource = setupSource; + setSetupSource(null); + + if (completedSource && client && projectId) { + const existing = configs?.find( + (c) => c.source_product === completedSource, + ); + try { + if (!existing) { + await client.createSignalSourceConfig(projectId, { + source_product: completedSource, + source_type: SOURCE_TYPE_MAP[completedSource], + enabled: true, + }); + } else if (!existing.enabled) { + await client.updateSignalSourceConfig(projectId, existing.id, { + enabled: true, + }); + } + } catch { + toast.error( + "Data source connected, but failed to enable signal source. Try toggling it on.", + ); + } + } + + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["external-data-sources"] }), + queryClient.invalidateQueries({ + queryKey: ["signals", "source-configs"], + }), + queryClient.invalidateQueries({ + queryKey: ["inbox", "signal-reports"], + }), + ]); + }, [queryClient, setupSource, configs, client, projectId]); + + const handleSetupCancel = useCallback(() => { + setSetupSource(null); + }, []); + return { displayValues, sourceStates, setupSource, isLoading, - handleChange, + handleToggle, handleSetup, handleSetupComplete, handleSetupCancel, + evaluations: displayEvaluations, + evaluationsUrl, + handleToggleEvaluation, }; } diff --git a/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx b/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx index 2b4bfae9b..a9139ad3c 100644 --- a/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/SignalsStep.tsx @@ -19,15 +19,18 @@ export function SignalsStep({ onNext, onBack }: SignalsStepProps) { sourceStates, setupSource, isLoading, - handleChange, + handleToggle, handleSetup, handleSetupComplete, handleSetupCancel, + evaluations, + evaluationsUrl, + handleToggleEvaluation, } = useSignalSourceManager(); const anyEnabled = displayValues.session_replay || - displayValues.llm_analytics || + displayValues.error_tracking || displayValues.github || displayValues.linear || displayValues.zendesk; @@ -96,10 +99,17 @@ export function SignalsStep({ onNext, onBack }: SignalsStepProps) { ) : ( void handleChange(v)} + onToggle={(source, enabled) => + void handleToggle(source, enabled) + } disabled={isLoading} sourceStates={sourceStates} onSetup={handleSetup} + evaluations={evaluations} + evaluationsUrl={evaluationsUrl} + onToggleEvaluation={(id, enabled) => + void handleToggleEvaluation(id, enabled) + } /> )} diff --git a/apps/code/src/renderer/features/onboarding/hooks/useTutorialTour.ts b/apps/code/src/renderer/features/onboarding/hooks/useTutorialTour.ts index e231d08f8..8bd42efbf 100644 --- a/apps/code/src/renderer/features/onboarding/hooks/useTutorialTour.ts +++ b/apps/code/src/renderer/features/onboarding/hooks/useTutorialTour.ts @@ -66,10 +66,6 @@ export function useTutorialTour() { configs?.some( (c) => c.source_product === "session_replay" && c.enabled, ) ?? true, - llm_analytics: - configs?.some( - (c) => c.source_product === "llm_analytics" && c.enabled, - ) ?? false, github: configs?.some((c) => c.source_product === "github" && c.enabled) ?? false, @@ -79,6 +75,10 @@ export function useTutorialTour() { zendesk: configs?.some((c) => c.source_product === "zendesk" && c.enabled) ?? false, + error_tracking: + configs?.some( + (c) => c.source_product === "error_tracking" && c.enabled, + ) ?? false, }), [configs], ); diff --git a/apps/code/src/renderer/features/onboarding/utils/generateInstrumentationPrompt.ts b/apps/code/src/renderer/features/onboarding/utils/generateInstrumentationPrompt.ts index 892f7da5e..406b5cab6 100644 --- a/apps/code/src/renderer/features/onboarding/utils/generateInstrumentationPrompt.ts +++ b/apps/code/src/renderer/features/onboarding/utils/generateInstrumentationPrompt.ts @@ -13,13 +13,7 @@ export function generateInstrumentationPrompt( ); } - if (signals.llm_analytics) { - parts.push( - "Set up LLM analytics by integrating PostHog's LLM tracing. Add the appropriate PostHog LLM wrapper for the LLM framework used in this project (e.g., OpenAI, Anthropic, LangChain). Ensure LLM calls are automatically traced.", - ); - } - - if (!signals.session_replay && !signals.llm_analytics) { + if (!signals.session_replay) { parts.push( "Check if the PostHog SDK is installed. If not, install it and initialize it with the project's API key. Set up basic event tracking.", ); diff --git a/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx index 855e7f60a..83187bb02 100644 --- a/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx @@ -9,10 +9,13 @@ export function SignalSourcesSettings() { sourceStates, setupSource, isLoading, - handleChange, + handleToggle, handleSetup, handleSetupComplete, handleSetupCancel, + evaluations, + evaluationsUrl, + handleToggleEvaluation, } = useSignalSourceManager(); if (isLoading) { @@ -39,9 +42,14 @@ export function SignalSourcesSettings() { ) : ( void handleChange(v)} + onToggle={(source, enabled) => void handleToggle(source, enabled)} sourceStates={sourceStates} onSetup={handleSetup} + evaluations={evaluations} + evaluationsUrl={evaluationsUrl} + onToggleEvaluation={(id, enabled) => + void handleToggleEvaluation(id, enabled) + } /> )} diff --git a/apps/code/src/shared/types.ts b/apps/code/src/shared/types.ts index 4abc873dd..7e973e670 100644 --- a/apps/code/src/shared/types.ts +++ b/apps/code/src/shared/types.ts @@ -189,6 +189,8 @@ export interface SignalReport { artefact_count: number; /** P0–P4 from actionability judgment when the report is researched */ priority?: SignalReportPriority | null; + /** Whether the current user is a suggested reviewer for this report (server-annotated). */ + is_suggested_reviewer?: boolean; } export interface SignalReportArtefactContent { @@ -207,6 +209,34 @@ export interface SignalReportArtefact { created_at: string; } +/** Artefact with `type: "suggested_reviewers"` — content is an enriched reviewer list. */ +export interface SuggestedReviewersArtefact { + id: string; + type: "suggested_reviewers"; + content: SuggestedReviewer[]; + created_at: string; +} + +export interface SuggestedReviewerCommit { + sha: string; + url: string; + reason: string; +} + +export interface SuggestedReviewerUser { + id: number; + uuid: string; + email: string; + first_name: string; +} + +export interface SuggestedReviewer { + github_login: string; + github_name: string | null; + relevant_commits: SuggestedReviewerCommit[]; + user: SuggestedReviewerUser | null; +} + interface MatchedSignalMetadata { parent_signal_id: string; match_query: string; @@ -243,7 +273,7 @@ export interface SignalReportSignalsResponse { } export interface SignalReportArtefactsResponse { - results: SignalReportArtefact[]; + results: (SignalReportArtefact | SuggestedReviewersArtefact)[]; count: number; unavailableReason?: | "forbidden" @@ -253,6 +283,7 @@ export interface SignalReportArtefactsResponse { } export type SignalReportOrderingField = + | "priority" | "signal_count" | "total_weight" | "created_at" @@ -261,7 +292,7 @@ export type SignalReportOrderingField = export interface SignalReportsQueryParams { limit?: number; offset?: number; - status?: CommaSeparatedSignalReportStatuses; + status?: CommaSeparatedSignalReportStatuses | string; /** * Comma-separated sort keys (prefix `-` for descending). `status` is semantic stage * rank (not lexicographic `status` column order). Also: `signal_count`, `total_weight`, From 37ceb3bb503e778edd16ec2da2b9e3495fa753da Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Wed, 1 Apr 2026 03:17:49 +0200 Subject: [PATCH 2/8] feat(inbox): match Cloud signal card wording and structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consistent SignalCardHeader: colored dot + "Product · Type" + Weight badge - Source-specific cards: error tracking (fingerprint), GitHub (labels, link), Zendesk (priority/status/tags), LLM analytics (model/provider/trace) - Cloud-exact source line labels and product colors - Type guards dispatch signals to correct card variant --- .../features/inbox/components/SignalCard.tsx | 486 +++++++++++++----- 1 file changed, 360 insertions(+), 126 deletions(-) diff --git a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx index 22a0fff82..acdec85d8 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx @@ -1,10 +1,9 @@ import { ArrowSquareOutIcon, - BugIcon, CaretDownIcon, CaretRightIcon, - GithubLogoIcon, TagIcon, + WarningIcon, } from "@phosphor-icons/react"; import { Badge, Box, Flex, Text } from "@radix-ui/themes"; import type { Signal } from "@shared/types"; @@ -12,10 +11,72 @@ import { useState } from "react"; const COLLAPSE_THRESHOLD = 300; -interface SignalCardProps { - signal: Signal; +// ── Source line labels (matching PostHog Cloud's signalCardSourceLine) ──────── + +const ERROR_TRACKING_TYPE_LABELS: Record = { + issue_created: "New issue", + issue_reopened: "Issue reopened", + issue_spiking: "Volume spike", +}; + +function signalCardSourceLine(signal: { + source_product: string; + source_type: string; +}): string { + const { source_product, source_type } = signal; + + if (source_product === "error_tracking") { + const typeLabel = + ERROR_TRACKING_TYPE_LABELS[source_type] ?? source_type.replace(/_/g, " "); + return `Error tracking · ${typeLabel}`; + } + if ( + source_product === "session_replay" && + source_type === "session_segment_cluster" + ) { + return "Session replay · Session segment cluster"; + } + if ( + source_product === "session_replay" && + source_type === "session_analysis_cluster" + ) { + return "Session replay · Session analysis cluster"; + } + if (source_product === "llm_analytics" && source_type === "evaluation") { + return "LLM analytics · Evaluation"; + } + if (source_product === "zendesk" && source_type === "ticket") { + return "Zendesk · Ticket"; + } + if (source_product === "github" && source_type === "issue") { + return "GitHub · Issue"; + } + if (source_product === "linear" && source_type === "issue") { + return "Linear · Issue"; + } + + const productLabel = source_product.replace(/_/g, " "); + const typeLabel = source_type.replace(/_/g, " "); + return `${productLabel} · ${typeLabel}`; +} + +// ── Source product color (matching Cloud's known product colors) ────────────── + +const SOURCE_PRODUCT_COLORS: Record = { + error_tracking: "var(--red-9)", + session_replay: "var(--amber-9)", + llm_analytics: "var(--purple-9)", + github: "var(--gray-11)", + linear: "var(--blue-9)", + zendesk: "var(--green-9)", +}; + +function sourceProductColor(product: string): string { + return SOURCE_PRODUCT_COLORS[product] ?? "var(--gray-9)"; } +// ── Shared utilities ───────────────────────────────────────────────────────── + interface GitHubLabelObject { name: string; color?: string; @@ -24,11 +85,26 @@ interface GitHubLabelObject { interface GitHubIssueExtra { html_url?: string; number?: number; - state?: string; labels?: string | GitHubLabelObject[]; created_at?: string; - updated_at?: string; - locked?: boolean; +} + +interface ZendeskTicketExtra { + url?: string; + priority?: string; + status?: string; + tags?: string[]; +} + +interface LlmEvalExtra { + evaluation_id?: string; + trace_id?: string; + model?: string; + provider?: string; +} + +interface ErrorTrackingExtra { + fingerprint?: string; } function resolveLabels( @@ -57,18 +133,6 @@ function resolveLabels( return []; } -function splitTitleBody(content: string): { title: string; body: string } { - const firstNewline = content.indexOf("\n"); - if (firstNewline === -1) return { title: content, body: "" }; - return { - title: content.slice(0, firstNewline).trim(), - body: content - .slice(firstNewline + 1) - .replace(/^[\n]+/, "") - .trim(), - }; -} - function truncateBody(body: string, maxLength = COLLAPSE_THRESHOLD): string { if (body.length <= maxLength) return body; const truncated = body.slice(0, maxLength); @@ -77,6 +141,73 @@ function truncateBody(body: string, maxLength = COLLAPSE_THRESHOLD): string { return `${truncated.slice(0, cutPoint)}…`; } +function parseExtra(raw: Record): Record { + if (typeof raw === "string") { + try { + return JSON.parse(raw) as Record; + } catch { + return {}; + } + } + return raw; +} + +// ── Type guards ────────────────────────────────────────────────────────────── + +function isGithubIssueExtra( + extra: Record, +): extra is Record & GitHubIssueExtra { + return "html_url" in extra && "number" in extra; +} + +function isZendeskTicketExtra( + extra: Record, +): extra is Record & ZendeskTicketExtra { + return "url" in extra && "priority" in extra; +} + +function isLlmEvalExtra( + extra: Record, +): extra is Record & LlmEvalExtra { + return "evaluation_id" in extra && "trace_id" in extra; +} + +function isErrorTrackingExtra( + extra: Record, +): extra is Record & ErrorTrackingExtra { + return typeof extra.fingerprint === "string"; +} + +// ── Shared components ──────────────────────────────────────────────────────── + +function SignalCardHeader({ signal }: { signal: Signal }) { + return ( + + + + {signalCardSourceLine(signal)} + + + + Weight: {signal.weight.toFixed(1)} + + + ); +} + function CollapsibleBody({ body }: { body: string }) { const [expanded, setExpanded] = useState(false); const isLong = body.length > COLLAPSE_THRESHOLD; @@ -108,154 +239,257 @@ function CollapsibleBody({ body }: { body: string }) { ); } -function parseExtra(raw: Record): GitHubIssueExtra { - if (typeof raw === "string") { - try { - return JSON.parse(raw) as GitHubIssueExtra; - } catch { - return {}; - } - } - return raw as GitHubIssueExtra; -} +// ── Source-specific cards ──────────────────────────────────────────────────── -function GitHubIssueSignalCard({ signal }: SignalCardProps) { - const extra = parseExtra(signal.extra); +function GitHubIssueSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: GitHubIssueExtra; +}) { const labels = resolveLabels(extra.labels); const issueUrl = extra.html_url ?? null; - const issueNumber = extra.number ?? null; - const { title, body } = splitTitleBody(signal.content); - - const titleContent = ( - <> - {issueNumber ? `#${issueNumber} ` : ""} - {title} - - ); return ( - + + + - - {issueUrl ? ( + + #{extra.number} + + {labels.map((label) => ( + + + {label.name} + + ))} + + {issueUrl && ( - {titleContent} + View on GitHub + - ) : ( - + {extra.created_at && ( + + Opened: {new Date(extra.created_at).toLocaleString()} + + )} + + ); +} + +function ZendeskTicketSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: ZendeskTicketExtra; +}) { + return ( + + + + + {extra.priority && ( + + Priority: {extra.priority} + + )} + {extra.status && ( + + Status: {extra.status} + + )} + {extra.tags?.map((tag) => ( + - {titleContent} - - )} - {issueUrl && ( + {tag} + + ))} + + {extra.url && ( + Open )} - - - {labels.length > 0 && ( - - - {labels.map((label) => ( - - {label.name} - - ))} - - )} - - {body && } - - - - w:{signal.weight.toFixed(2)} - - - {new Date(signal.timestamp).toLocaleString()} - - - ); } -function DefaultSignalCard({ signal }: SignalCardProps) { +function LlmEvalSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: LlmEvalExtra; +}) { return ( - + + + - - - - {signal.source_product} - - - {signal.source_type} - - + {extra.model && Model: {extra.model}} + {extra.model && extra.provider && ·} + {extra.provider && Provider: {extra.provider}} + {extra.trace_id && ( + + Trace:{" "} + {extra.trace_id.slice(0, 12)}... + + )} + + ); +} - - +function ErrorTrackingSignalCard({ + signal, + extra, +}: { + signal: Signal; + extra: ErrorTrackingExtra; +}) { + const fingerprint = extra.fingerprint ?? ""; + const fingerprintShort = + fingerprint.length > 14 ? `${fingerprint.slice(0, 14)}…` : fingerprint; - - - w:{signal.weight.toFixed(2)} - - - {new Date(signal.timestamp).toLocaleString()} - + return ( + + + + + + + + Fingerprint{" "} + + {fingerprintShort} + + + + {/* No "View issue" link in Code — error tracking lives in Cloud */} ); } -export function SignalCard({ signal }: SignalCardProps) { - if (signal.source_product === "github") { - return ; +function GenericSignalCard({ signal }: { signal: Signal }) { + return ( + + + + + {new Date(signal.timestamp).toLocaleString()} + + + ); +} + +// ── Main export ────────────────────────────────────────────────────────────── + +export function SignalCard({ signal }: { signal: Signal }) { + const extra = parseExtra(signal.extra); + + if ( + signal.source_product === "error_tracking" && + isErrorTrackingExtra(extra) + ) { + return ; + } + if (signal.source_product === "github" && isGithubIssueExtra(extra)) { + return ; + } + if (signal.source_product === "zendesk" && isZendeskTicketExtra(extra)) { + return ; + } + if (signal.source_product === "llm_analytics" && isLlmEvalExtra(extra)) { + return ; } - return ; + return ; } From 9dd6fb9ccdd8858f8afc754c4c82484eba8059d8 Mon Sep 17 00:00:00 2001 From: Alex Lebedev Date: Wed, 1 Apr 2026 19:51:36 +0200 Subject: [PATCH 3/8] feat(inbox): New report data in Inbox UI --- apps/code/src/renderer/api/posthogClient.ts | 53 +++++++++- .../inbox/components/InboxSignalsTab.tsx | 20 +++- .../features/inbox/components/SignalCard.tsx | 99 ++++++++++++++++--- apps/code/src/shared/types.ts | 22 ++++- 4 files changed, 175 insertions(+), 19 deletions(-) diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index e78e2339d..f55ea598f 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -1,6 +1,7 @@ import type { SandboxEnvironment, SandboxEnvironmentInput, + SignalFindingArtefact, SignalReportArtefact, SignalReportArtefactsResponse, SignalReportSignalsResponse, @@ -70,13 +71,58 @@ function optionalString(value: unknown): string | null { return typeof value === "string" ? value : null; } +function normalizeSignalFindingArtefact( + value: Record, +): SignalFindingArtefact | null { + const id = optionalString(value.id); + if (!id) return null; + + const contentValue = isObjectRecord(value.content) ? value.content : null; + if (!contentValue) return null; + + const signalId = optionalString(contentValue.signal_id); + if (!signalId) return null; + + return { + id, + type: "signal_finding", + created_at: optionalString(value.created_at) ?? new Date(0).toISOString(), + content: { + signal_id: signalId, + relevant_code_paths: Array.isArray(contentValue.relevant_code_paths) + ? contentValue.relevant_code_paths.filter( + (p: unknown): p is string => typeof p === "string", + ) + : [], + relevant_commit_hashes: isObjectRecord( + contentValue.relevant_commit_hashes, + ) + ? Object.fromEntries( + Object.entries(contentValue.relevant_commit_hashes).filter( + (e): e is [string, string] => typeof e[1] === "string", + ), + ) + : {}, + data_queried: optionalString(contentValue.data_queried) ?? "", + verified: + typeof contentValue.verified === "boolean" + ? contentValue.verified + : false, + }, + }; +} + function normalizeSignalReportArtefact( value: unknown, -): SignalReportArtefact | null { +): SignalReportArtefact | SignalFindingArtefact | null { if (!isObjectRecord(value)) { return null; } + if (optionalString(value.type) === "signal_finding") { + return normalizeSignalFindingArtefact(value); + } + const id = optionalString(value.id); if (!id) { return null; @@ -125,7 +171,10 @@ function parseSignalReportArtefactsPayload( const results = rawResults .map(normalizeSignalReportArtefact) - .filter((artefact): artefact is SignalReportArtefact => artefact !== null); + .filter( + (artefact): artefact is SignalReportArtefact | SignalFindingArtefact => + artefact !== null, + ); const count = typeof payload?.count === "number" ? payload.count : results.length; diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 098edeecc..437060463 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -42,6 +42,7 @@ import { } from "@radix-ui/themes"; import { getCloudUrlFromRegion } from "@shared/constants/oauth"; import type { + SignalFindingArtefact, SignalReportArtefact, SignalReportArtefactsResponse, SignalReportsQueryParams, @@ -202,9 +203,20 @@ export function InboxSignalsTab() { const artefactsQuery = useInboxReportArtefacts(selectedReport?.id ?? "", { enabled: !!selectedReport, }); - const visibleArtefacts = (artefactsQuery.data?.results ?? []).filter( + const allArtefacts = artefactsQuery.data?.results ?? []; + const visibleArtefacts = allArtefacts.filter( (a): a is SignalReportArtefact => a.type === "video_segment", ); + const signalFindings = useMemo(() => { + const map = new Map(); + for (const a of allArtefacts) { + if (a.type === "signal_finding") { + const finding = a as SignalFindingArtefact; + map.set(finding.content.signal_id, finding.content); + } + } + return map; + }, [allArtefacts]); const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason; const showArtefactsUnavailable = !artefactsQuery.isLoading && @@ -512,7 +524,11 @@ export function InboxSignalsTab() { {signals.map((signal) => ( - + ))} diff --git a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx index acdec85d8..fa625246b 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx @@ -2,11 +2,13 @@ import { ArrowSquareOutIcon, CaretDownIcon, CaretRightIcon, + CheckCircleIcon, + QuestionIcon, TagIcon, WarningIcon, } from "@phosphor-icons/react"; import { Badge, Box, Flex, Text } from "@radix-ui/themes"; -import type { Signal } from "@shared/types"; +import type { Signal, SignalFindingContent } from "@shared/types"; import { useState } from "react"; const COLLAPSE_THRESHOLD = 300; @@ -180,7 +182,34 @@ function isErrorTrackingExtra( // ── Shared components ──────────────────────────────────────────────────────── -function SignalCardHeader({ signal }: { signal: Signal }) { +function VerificationBadge({ verified }: { verified: boolean }) { + return ( + + {verified ? ( + + ) : ( + + )} + {verified ? "Verified" : "Unverified"} + + ); +} + +function SignalCardHeader({ + signal, + verified, +}: { + signal: Signal; + verified?: boolean; +}) { return ( + {verified !== undefined && } - + - + - + - + - + ; + return ( + + ); } if (signal.source_product === "github" && isGithubIssueExtra(extra)) { - return ; + return ( + + ); } if (signal.source_product === "zendesk" && isZendeskTicketExtra(extra)) { - return ; + return ( + + ); } if (signal.source_product === "llm_analytics" && isLlmEvalExtra(extra)) { - return ; + return ( + + ); } - return ; + return ; } diff --git a/apps/code/src/shared/types.ts b/apps/code/src/shared/types.ts index 7e973e670..a747b302d 100644 --- a/apps/code/src/shared/types.ts +++ b/apps/code/src/shared/types.ts @@ -209,6 +209,22 @@ export interface SignalReportArtefact { created_at: string; } +/** Artefact with `type: "signal_finding"` — per-signal research finding from the agentic report. */ +export interface SignalFindingArtefact { + id: string; + type: "signal_finding"; + content: SignalFindingContent; + created_at: string; +} + +export interface SignalFindingContent { + signal_id: string; + relevant_code_paths: string[]; + relevant_commit_hashes: Record; + data_queried: string; + verified: boolean; +} + /** Artefact with `type: "suggested_reviewers"` — content is an enriched reviewer list. */ export interface SuggestedReviewersArtefact { id: string; @@ -273,7 +289,11 @@ export interface SignalReportSignalsResponse { } export interface SignalReportArtefactsResponse { - results: (SignalReportArtefact | SuggestedReviewersArtefact)[]; + results: ( + | SignalReportArtefact + | SignalFindingArtefact + | SuggestedReviewersArtefact + )[]; count: number; unavailableReason?: | "forbidden" From b126ec51b1fe0e89d4749e769239990a47aaa775 Mon Sep 17 00:00:00 2001 From: Alex Lebedev Date: Wed, 1 Apr 2026 20:10:44 +0200 Subject: [PATCH 4/8] feat: Add actionability badge/explanation. --- apps/code/src/renderer/api/posthogClient.ts | 58 +++++++++++++- .../inbox/components/InboxSignalsTab.tsx | 77 +++++++++++++++++++ .../features/inbox/components/ReportCard.tsx | 4 + .../SignalReportActionabilityBadge.tsx | 57 ++++++++++++++ apps/code/src/shared/types.ts | 27 ++++++- 5 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 apps/code/src/renderer/features/inbox/components/SignalReportActionabilityBadge.tsx diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index f55ea598f..eaeb07c64 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -1,4 +1,5 @@ import type { + ActionabilityJudgmentArtefact, SandboxEnvironment, SandboxEnvironmentInput, SignalFindingArtefact, @@ -71,6 +72,43 @@ function optionalString(value: unknown): string | null { return typeof value === "string" ? value : null; } +const ACTIONABILITY_VALUES = new Set([ + "immediately_actionable", + "requires_human_input", + "not_actionable", +]); + +function normalizeActionabilityJudgmentArtefact( + value: Record, +): ActionabilityJudgmentArtefact | null { + const id = optionalString(value.id); + if (!id) return null; + + const contentValue = isObjectRecord(value.content) ? value.content : null; + if (!contentValue) return null; + + // Support both agentic ("actionability") and legacy ("choice") field names + const actionability = + optionalString(contentValue.actionability) ?? + optionalString(contentValue.choice); + if (!actionability || !ACTIONABILITY_VALUES.has(actionability)) return null; + + return { + id, + type: "actionability_judgment", + created_at: optionalString(value.created_at) ?? new Date(0).toISOString(), + content: { + explanation: optionalString(contentValue.explanation) ?? "", + actionability: + actionability as ActionabilityJudgmentArtefact["content"]["actionability"], + already_addressed: + typeof contentValue.already_addressed === "boolean" + ? contentValue.already_addressed + : false, + }, + }; +} + function normalizeSignalFindingArtefact( value: Record, ): SignalFindingArtefact | null { @@ -114,14 +152,22 @@ function normalizeSignalFindingArtefact( function normalizeSignalReportArtefact( value: unknown, -): SignalReportArtefact | SignalFindingArtefact | null { +): + | SignalReportArtefact + | ActionabilityJudgmentArtefact + | SignalFindingArtefact + | null { if (!isObjectRecord(value)) { return null; } - if (optionalString(value.type) === "signal_finding") { + const type = optionalString(value.type); + if (type === "signal_finding") { return normalizeSignalFindingArtefact(value); } + if (type === "actionability_judgment") { + return normalizeActionabilityJudgmentArtefact(value); + } const id = optionalString(value.id); if (!id) { @@ -172,8 +218,12 @@ function parseSignalReportArtefactsPayload( const results = rawResults .map(normalizeSignalReportArtefact) .filter( - (artefact): artefact is SignalReportArtefact | SignalFindingArtefact => - artefact !== null, + ( + artefact, + ): artefact is + | SignalReportArtefact + | ActionabilityJudgmentArtefact + | SignalFindingArtefact => artefact !== null, ); const count = typeof payload?.count === "number" ? payload.count : results.length; diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 437060463..7d2f29373 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -26,8 +26,11 @@ import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { useRepositoryIntegration } from "@hooks/useIntegrations"; import { ArrowSquareOutIcon, + CaretDownIcon, + CaretRightIcon, ClockIcon, Cloud as CloudIcon, + WarningIcon, XIcon, } from "@phosphor-icons/react"; import { @@ -42,6 +45,8 @@ import { } from "@radix-ui/themes"; import { getCloudUrlFromRegion } from "@shared/constants/oauth"; import type { + ActionabilityJudgmentArtefact, + ActionabilityJudgmentContent, SignalFindingArtefact, SignalReportArtefact, SignalReportArtefactsResponse, @@ -55,6 +60,7 @@ import { SignalsErrorState, SignalsLoadingState } from "./InboxEmptyStates"; import { InboxWarmingUpState } from "./InboxWarmingUpState"; import { ReportCard } from "./ReportCard"; import { SignalCard } from "./SignalCard"; +import { SignalReportActionabilityBadge } from "./SignalReportActionabilityBadge"; import { SignalReportPriorityBadge } from "./SignalReportPriorityBadge"; import { SignalReportSummaryMarkdown } from "./SignalReportSummaryMarkdown"; import { SignalsToolbar } from "./SignalsToolbar"; @@ -76,6 +82,33 @@ function getArtefactsUnavailableMessage( } } +function ActionabilityExplanation({ explanation }: { explanation: string }) { + const [expanded, setExpanded] = useState(false); + + return ( + + + {expanded && ( + + {explanation} + + )} + + ); +} + function LoadMoreTrigger({ hasNextPage, isFetchingNextPage, @@ -217,6 +250,15 @@ export function InboxSignalsTab() { } return map; }, [allArtefacts]); + const actionabilityJudgment = + useMemo((): ActionabilityJudgmentContent | null => { + for (const a of allArtefacts) { + if (a.type === "actionability_judgment") { + return (a as ActionabilityJudgmentArtefact).content; + } + } + return null; + }, [allArtefacts]); const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason; const showArtefactsUnavailable = !artefactsQuery.isLoading && @@ -504,6 +546,9 @@ export function InboxSignalsTab() { + {selectedReport.signal_count} occurrences @@ -512,6 +557,38 @@ export function InboxSignalsTab() { + {(selectedReport.already_addressed ?? + actionabilityJudgment?.already_addressed) && ( + + + + This issue may already be addressed in recent code + changes. + + + )} + + {actionabilityJudgment?.explanation && ( + + )} + {signals.length > 0 && ( + {/* 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/SignalReportActionabilityBadge.tsx b/apps/code/src/renderer/features/inbox/components/SignalReportActionabilityBadge.tsx new file mode 100644 index 000000000..3dbba2846 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/SignalReportActionabilityBadge.tsx @@ -0,0 +1,57 @@ +import type { SignalReportActionability } from "@shared/types"; +import type { ReactNode } from "react"; + +const ACTIONABILITY_CHIP_STYLE: Record< + SignalReportActionability, + { color: string; backgroundColor: string; borderColor: string; label: string } +> = { + immediately_actionable: { + color: "var(--green-11)", + backgroundColor: "var(--green-3)", + borderColor: "var(--green-6)", + label: "Actionable", + }, + requires_human_input: { + color: "var(--amber-11)", + backgroundColor: "var(--amber-3)", + borderColor: "var(--amber-6)", + label: "Needs input", + }, + not_actionable: { + color: "var(--gray-11)", + backgroundColor: "var(--gray-3)", + borderColor: "var(--gray-6)", + label: "Not actionable", + }, +}; + +interface SignalReportActionabilityBadgeProps { + actionability: SignalReportActionability | null | undefined; +} + +export function SignalReportActionabilityBadge({ + actionability, +}: SignalReportActionabilityBadgeProps): ReactNode { + if (actionability == null) { + return null; + } + + const s = ACTIONABILITY_CHIP_STYLE[actionability]; + if (!s) { + return null; + } + + return ( + + {s.label} + + ); +} diff --git a/apps/code/src/shared/types.ts b/apps/code/src/shared/types.ts index a747b302d..67a9efe42 100644 --- a/apps/code/src/shared/types.ts +++ b/apps/code/src/shared/types.ts @@ -164,6 +164,12 @@ export type SignalReportStatus = /** Actionability priority from the researched report (actionability judgment artefact). */ export type SignalReportPriority = "P0" | "P1" | "P2" | "P3" | "P4"; +/** Actionability choice from the researched report. */ +export type SignalReportActionability = + | "immediately_actionable" + | "requires_human_input" + | "not_actionable"; + /** * One or more `SignalReportStatus` values joined by commas, e.g. `potential` or `potential,candidate,ready`. * This looks horrendous but it's superb, trust me bro. @@ -187,8 +193,12 @@ export interface SignalReport { created_at: string; updated_at: string; artefact_count: number; - /** P0–P4 from actionability judgment when the report is researched */ + /** P0–P4 from priority judgment when the report is researched */ priority?: SignalReportPriority | null; + /** Actionability choice from the actionability judgment artefact. */ + actionability?: SignalReportActionability | null; + /** Whether the issue appears already fixed, from the actionability judgment artefact. */ + already_addressed?: boolean | null; /** Whether the current user is a suggested reviewer for this report (server-annotated). */ is_suggested_reviewer?: boolean; } @@ -209,6 +219,20 @@ export interface SignalReportArtefact { created_at: string; } +/** Artefact with `type: "actionability_judgment"` — actionability assessment from the agentic report. */ +export interface ActionabilityJudgmentArtefact { + id: string; + type: "actionability_judgment"; + content: ActionabilityJudgmentContent; + created_at: string; +} + +export interface ActionabilityJudgmentContent { + explanation: string; + actionability: SignalReportActionability; + already_addressed: boolean; +} + /** Artefact with `type: "signal_finding"` — per-signal research finding from the agentic report. */ export interface SignalFindingArtefact { id: string; @@ -291,6 +315,7 @@ export interface SignalReportSignalsResponse { export interface SignalReportArtefactsResponse { results: ( | SignalReportArtefact + | ActionabilityJudgmentArtefact | SignalFindingArtefact | SuggestedReviewersArtefact )[]; From 16ae90f0de57b6746e3beb794374da21dac073d1 Mon Sep 17 00:00:00 2001 From: Alex Lebedev Date: Wed, 1 Apr 2026 20:13:34 +0200 Subject: [PATCH 5/8] feat: Add priority judgement explanation. --- apps/code/src/renderer/api/posthogClient.ts | 31 +++++++++++++++++++ .../inbox/components/InboxSignalsTab.tsx | 29 +++++++++++++++-- apps/code/src/shared/types.ts | 14 +++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index eaeb07c64..4a6793f5e 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -1,5 +1,6 @@ import type { ActionabilityJudgmentArtefact, + PriorityJudgmentArtefact, SandboxEnvironment, SandboxEnvironmentInput, SignalFindingArtefact, @@ -72,6 +73,31 @@ function optionalString(value: unknown): string | null { return typeof value === "string" ? value : null; } +const PRIORITY_VALUES = new Set(["P0", "P1", "P2", "P3", "P4"]); + +function normalizePriorityJudgmentArtefact( + value: Record, +): PriorityJudgmentArtefact | null { + const id = optionalString(value.id); + if (!id) return null; + + const contentValue = isObjectRecord(value.content) ? value.content : null; + if (!contentValue) return null; + + const priority = optionalString(contentValue.priority); + if (!priority || !PRIORITY_VALUES.has(priority)) return null; + + return { + id, + type: "priority_judgment", + created_at: optionalString(value.created_at) ?? new Date(0).toISOString(), + content: { + explanation: optionalString(contentValue.explanation) ?? "", + priority: priority as PriorityJudgmentArtefact["content"]["priority"], + }, + }; +} + const ACTIONABILITY_VALUES = new Set([ "immediately_actionable", "requires_human_input", @@ -154,6 +180,7 @@ function normalizeSignalReportArtefact( value: unknown, ): | SignalReportArtefact + | PriorityJudgmentArtefact | ActionabilityJudgmentArtefact | SignalFindingArtefact | null { @@ -168,6 +195,9 @@ function normalizeSignalReportArtefact( if (type === "actionability_judgment") { return normalizeActionabilityJudgmentArtefact(value); } + if (type === "priority_judgment") { + return normalizePriorityJudgmentArtefact(value); + } const id = optionalString(value.id); if (!id) { @@ -222,6 +252,7 @@ function parseSignalReportArtefactsPayload( artefact, ): artefact is | SignalReportArtefact + | PriorityJudgmentArtefact | ActionabilityJudgmentArtefact | SignalFindingArtefact => artefact !== null, ); diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 7d2f29373..c861204fe 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -47,6 +47,7 @@ import { getCloudUrlFromRegion } from "@shared/constants/oauth"; import type { ActionabilityJudgmentArtefact, ActionabilityJudgmentContent, + PriorityJudgmentArtefact, SignalFindingArtefact, SignalReportArtefact, SignalReportArtefactsResponse, @@ -82,7 +83,13 @@ function getArtefactsUnavailableMessage( } } -function ActionabilityExplanation({ explanation }: { explanation: string }) { +function CollapsibleExplanation({ + label, + explanation, +}: { + label: string; + explanation: string; +}) { const [expanded, setExpanded] = useState(false); return ( @@ -93,7 +100,7 @@ function ActionabilityExplanation({ explanation }: { explanation: string }) { className="flex items-center gap-1 rounded px-1 py-0.5 font-medium text-[12px] text-gray-11 hover:bg-gray-3 hover:text-gray-12" > {expanded ? : } - Actionability reasoning + {label} {expanded && ( { + for (const a of allArtefacts) { + if (a.type === "priority_judgment") { + return (a as PriorityJudgmentArtefact).content.explanation || null; + } + } + return null; + }, [allArtefacts]); const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason; const showArtefactsUnavailable = !artefactsQuery.isLoading && @@ -584,11 +599,19 @@ export function InboxSignalsTab() { )} {actionabilityJudgment?.explanation && ( - )} + {priorityExplanation && ( + + )} + {signals.length > 0 && ( Date: Wed, 1 Apr 2026 20:27:31 +0200 Subject: [PATCH 6/8] feat: Improve the view, drop useless fields. --- .../inbox/components/InboxSignalsTab.tsx | 114 +++++++++++------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index c861204fe..6b826f1f8 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -35,7 +35,6 @@ import { } from "@phosphor-icons/react"; import { AlertDialog, - Badge, Box, Button, Flex, @@ -55,7 +54,14 @@ import type { } from "@shared/types"; import { useNavigationStore } from "@stores/navigationStore"; import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { toast } from "sonner"; import { SignalsErrorState, SignalsLoadingState } from "./InboxEmptyStates"; import { InboxWarmingUpState } from "./InboxWarmingUpState"; @@ -83,31 +89,50 @@ function getArtefactsUnavailableMessage( } } -function CollapsibleExplanation({ +function DetailRow({ label, + value, explanation, }: { label: string; - explanation: string; + value: ReactNode; + explanation?: string | null; }) { const [expanded, setExpanded] = useState(false); + const hasExplanation = !!explanation; return ( - - {expanded && ( + + + {label} + + {value} + {hasExplanation && ( + + )} + + {expanded && explanation && ( {explanation} @@ -557,20 +582,37 @@ export function InboxSignalsTab() { fallback="No summary available." variant="detail" /> - - - - - {selectedReport.signal_count} occurrences - - - {selectedReport.relevant_user_count ?? 0} affected users - - + {(selectedReport.actionability || selectedReport.priority) && ( + + {selectedReport.priority && ( + + } + explanation={priorityExplanation} + /> + )} + {selectedReport.actionability && ( + + } + explanation={actionabilityJudgment?.explanation} + /> + )} + + )} {(selectedReport.already_addressed ?? actionabilityJudgment?.already_addressed) && ( @@ -598,20 +640,6 @@ export function InboxSignalsTab() { )} - {actionabilityJudgment?.explanation && ( - - )} - - {priorityExplanation && ( - - )} - {signals.length > 0 && ( Date: Wed, 1 Apr 2026 20:48:14 +0200 Subject: [PATCH 7/8] feat: Add code paths. --- .../features/inbox/components/SignalCard.tsx | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx index fa625246b..1e070990b 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx @@ -275,10 +275,12 @@ function GitHubIssueSignalCard({ signal, extra, verified, + codePaths, }: { signal: Signal; extra: GitHubIssueExtra; verified?: boolean; + codePaths?: string[]; }) { const labels = resolveLabels(extra.labels); const issueUrl = extra.html_url ?? null; @@ -342,6 +344,7 @@ function GitHubIssueSignalCard({ Opened: {new Date(extra.created_at).toLocaleString()} )} + ); } @@ -350,10 +353,12 @@ function ZendeskTicketSignalCard({ signal, extra, verified, + codePaths, }: { signal: Signal; extra: ZendeskTicketExtra; verified?: boolean; + codePaths?: string[]; }) { return ( @@ -401,6 +406,7 @@ function ZendeskTicketSignalCard({ )} + ); } @@ -409,10 +415,12 @@ function LlmEvalSignalCard({ signal, extra, verified, + codePaths, }: { signal: Signal; extra: LlmEvalExtra; verified?: boolean; + codePaths?: string[]; }) { return ( @@ -439,6 +447,7 @@ function LlmEvalSignalCard({ {extra.trace_id.slice(0, 12)}... )} + ); } @@ -447,10 +456,12 @@ function ErrorTrackingSignalCard({ signal, extra, verified, + codePaths, }: { signal: Signal; extra: ErrorTrackingExtra; verified?: boolean; + codePaths?: string[]; }) { const fingerprint = extra.fingerprint ?? ""; const fingerprintShort = @@ -489,6 +500,7 @@ function ErrorTrackingSignalCard({ {/* No "View issue" link in Code — error tracking lives in Cloud */} + ); } @@ -496,9 +508,11 @@ function ErrorTrackingSignalCard({ function GenericSignalCard({ signal, verified, + codePaths, }: { signal: Signal; verified?: boolean; + codePaths?: string[]; }) { return ( @@ -511,6 +525,49 @@ function GenericSignalCard({ > {new Date(signal.timestamp).toLocaleString()} + + + ); +} + +function CodePathsCollapsible({ paths }: { paths: string[] }) { + const [expanded, setExpanded] = useState(false); + + if (paths.length === 0) return null; + + return ( + + + {expanded && ( + + {paths.map((raw) => { + const trimmed = raw.trim(); + const parenIdx = trimmed.indexOf(" ("); + const filePath = + parenIdx >= 0 ? trimmed.slice(0, parenIdx) : trimmed; + const comment = parenIdx >= 0 ? trimmed.slice(parenIdx + 1) : null; + return ( + + + {filePath} + + {comment && ( + + {comment} + + )} + + ); + })} + + )} ); } @@ -526,6 +583,7 @@ export function SignalCard({ }) { const extra = parseExtra(signal.extra); const verified = finding?.verified; + const codePaths = finding?.relevant_code_paths ?? []; if ( signal.source_product === "error_tracking" && @@ -536,6 +594,7 @@ export function SignalCard({ signal={signal} extra={extra} verified={verified} + codePaths={codePaths} /> ); } @@ -545,6 +604,7 @@ export function SignalCard({ signal={signal} extra={extra} verified={verified} + codePaths={codePaths} /> ); } @@ -554,13 +614,25 @@ export function SignalCard({ signal={signal} extra={extra} verified={verified} + codePaths={codePaths} /> ); } if (signal.source_product === "llm_analytics" && isLlmEvalExtra(extra)) { return ( - + ); } - return ; + return ( + + ); } From 39fc797a435f0f490e7bd7f21e8e752d9329ea5d Mon Sep 17 00:00:00 2001 From: Alex Lebedev Date: Wed, 1 Apr 2026 20:54:26 +0200 Subject: [PATCH 8/8] feat: Add data queried. --- .../features/inbox/components/SignalCard.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx index 1e070990b..804c84f86 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalCard.tsx @@ -276,11 +276,13 @@ function GitHubIssueSignalCard({ extra, verified, codePaths, + dataQueried, }: { signal: Signal; extra: GitHubIssueExtra; verified?: boolean; codePaths?: string[]; + dataQueried?: string; }) { const labels = resolveLabels(extra.labels); const issueUrl = extra.html_url ?? null; @@ -345,6 +347,7 @@ function GitHubIssueSignalCard({ )} + ); } @@ -354,11 +357,13 @@ function ZendeskTicketSignalCard({ extra, verified, codePaths, + dataQueried, }: { signal: Signal; extra: ZendeskTicketExtra; verified?: boolean; codePaths?: string[]; + dataQueried?: string; }) { return ( @@ -407,6 +412,7 @@ function ZendeskTicketSignalCard({ )} + ); } @@ -416,11 +422,13 @@ function LlmEvalSignalCard({ extra, verified, codePaths, + dataQueried, }: { signal: Signal; extra: LlmEvalExtra; verified?: boolean; codePaths?: string[]; + dataQueried?: string; }) { return ( @@ -448,6 +456,7 @@ function LlmEvalSignalCard({ )} + ); } @@ -457,11 +466,13 @@ function ErrorTrackingSignalCard({ extra, verified, codePaths, + dataQueried, }: { signal: Signal; extra: ErrorTrackingExtra; verified?: boolean; codePaths?: string[]; + dataQueried?: string; }) { const fingerprint = extra.fingerprint ?? ""; const fingerprintShort = @@ -501,6 +512,7 @@ function ErrorTrackingSignalCard({ {/* No "View issue" link in Code — error tracking lives in Cloud */} + ); } @@ -509,10 +521,12 @@ function GenericSignalCard({ signal, verified, codePaths, + dataQueried, }: { signal: Signal; verified?: boolean; codePaths?: string[]; + dataQueried?: string; }) { return ( @@ -526,6 +540,7 @@ function GenericSignalCard({ {new Date(signal.timestamp).toLocaleString()} + ); } @@ -572,6 +587,34 @@ function CodePathsCollapsible({ paths }: { paths: string[] }) { ); } +function DataQueriedCollapsible({ text }: { text: string }) { + const [expanded, setExpanded] = useState(false); + + if (!text) return null; + + return ( + + + {expanded && ( + + {text} + + )} + + ); +} + // ── Main export ────────────────────────────────────────────────────────────── export function SignalCard({ @@ -584,6 +627,7 @@ export function SignalCard({ const extra = parseExtra(signal.extra); const verified = finding?.verified; const codePaths = finding?.relevant_code_paths ?? []; + const dataQueried = finding?.data_queried ?? ""; if ( signal.source_product === "error_tracking" && @@ -595,6 +639,7 @@ export function SignalCard({ extra={extra} verified={verified} codePaths={codePaths} + dataQueried={dataQueried} /> ); } @@ -605,6 +650,7 @@ export function SignalCard({ extra={extra} verified={verified} codePaths={codePaths} + dataQueried={dataQueried} /> ); } @@ -615,6 +661,7 @@ export function SignalCard({ extra={extra} verified={verified} codePaths={codePaths} + dataQueried={dataQueried} /> ); } @@ -625,6 +672,7 @@ export function SignalCard({ extra={extra} verified={verified} codePaths={codePaths} + dataQueried={dataQueried} /> ); }