diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 7ff819aec..2c5de5dee 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -14,6 +14,9 @@ import type { SignalReportStatus, SignalReportsQueryParams, SignalReportsResponse, + SignalReportTask, + SignalTeamConfig, + SignalUserAutonomyConfig, SuggestedReviewersArtefact, Task, TaskRun, @@ -409,7 +412,7 @@ export class PostHogAPIClient { async listSignalSourceConfigs( projectId: number, ): Promise { - const urlPath = `/api/projects/${projectId}/signal_source_configs/`; + const urlPath = `/api/projects/${projectId}/signals/source_configs/`; const url = new URL(`${this.api.baseUrl}${urlPath}`); const response = await this.api.fetcher.fetch({ method: "get", @@ -436,7 +439,7 @@ export class PostHogAPIClient { config?: Record; }, ): Promise { - const urlPath = `/api/projects/${projectId}/signal_source_configs/`; + const urlPath = `/api/projects/${projectId}/signals/source_configs/`; const url = new URL(`${this.api.baseUrl}${urlPath}`); const response = await this.api.fetcher.fetch({ method: "post", @@ -463,7 +466,7 @@ export class PostHogAPIClient { configId: string, updates: { enabled: boolean }, ): Promise { - const urlPath = `/api/projects/${projectId}/signal_source_configs/${configId}/`; + const urlPath = `/api/projects/${projectId}/signals/source_configs/${configId}/`; const url = new URL(`${this.api.baseUrl}${urlPath}`); const response = await this.api.fetcher.fetch({ method: "patch", @@ -1181,7 +1184,7 @@ export class PostHogAPIClient { ): Promise { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_reports/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/`, ); if (params?.limit != null) { @@ -1206,7 +1209,7 @@ export class PostHogAPIClient { const response = await this.api.fetcher.fetch({ method: "get", url, - path: `/api/projects/${teamId}/signal_reports/`, + path: `/api/projects/${teamId}/signals/reports/`, }); if (!response.ok) { @@ -1223,9 +1226,9 @@ export class PostHogAPIClient { async getSignalProcessingState(): Promise { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_processing/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/processing/`, ); - const path = `/api/projects/${teamId}/signal_processing/`; + const path = `/api/projects/${teamId}/signals/processing/`; const response = await this.api.fetcher.fetch({ method: "get", @@ -1251,9 +1254,9 @@ export class PostHogAPIClient { ): Promise { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_reports/available_reviewers/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/available_reviewers/`, ); - const path = `/api/projects/${teamId}/signal_reports/available_reviewers/`; + const path = `/api/projects/${teamId}/signals/reports/available_reviewers/`; if (query?.trim()) { url.searchParams.set("query", query.trim()); @@ -1280,12 +1283,12 @@ export class PostHogAPIClient { try { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/signals/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/${reportId}/signals/`, ); const response = await this.api.fetcher.fetch({ method: "get", url, - path: `/api/projects/${teamId}/signal_reports/${reportId}/signals/`, + path: `/api/projects/${teamId}/signals/reports/${reportId}/signals/`, }); if (!response.ok) { @@ -1312,9 +1315,9 @@ export class PostHogAPIClient { ): Promise { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/artefacts/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/${reportId}/artefacts/`, ); - const path = `/api/projects/${teamId}/signal_reports/${reportId}/artefacts/`; + const path = `/api/projects/${teamId}/signals/reports/${reportId}/artefacts/`; try { const response = await this.api.fetcher.fetch({ @@ -1379,9 +1382,9 @@ export class PostHogAPIClient { ): Promise { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/state/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/${reportId}/state/`, ); - const path = `/api/projects/${teamId}/signal_reports/${reportId}/state/`; + const path = `/api/projects/${teamId}/signals/reports/${reportId}/state/`; const response = await this.api.fetcher.fetch({ method: "post", @@ -1406,9 +1409,9 @@ export class PostHogAPIClient { }> { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/${reportId}/`, ); - const path = `/api/projects/${teamId}/signal_reports/${reportId}/`; + const path = `/api/projects/${teamId}/signals/reports/${reportId}/`; const response = await this.api.fetcher.fetch({ method: "delete", @@ -1433,9 +1436,9 @@ export class PostHogAPIClient { }> { const teamId = await this.getTeamId(); const url = new URL( - `${this.api.baseUrl}/api/projects/${teamId}/signal_reports/${reportId}/reingest/`, + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/${reportId}/reingest/`, ); - const path = `/api/projects/${teamId}/signal_reports/${reportId}/reingest/`; + const path = `/api/projects/${teamId}/signals/reports/${reportId}/reingest/`; const response = await this.api.fetcher.fetch({ method: "post", @@ -1454,6 +1457,137 @@ export class PostHogAPIClient { }; } + async getSignalReportTasks( + reportId: string, + options?: { relationship?: SignalReportTask["relationship"] }, + ): Promise { + const teamId = await this.getTeamId(); + const url = new URL( + `${this.api.baseUrl}/api/projects/${teamId}/signals/reports/${reportId}/tasks/`, + ); + if (options?.relationship) { + url.searchParams.set("relationship", options.relationship); + } + const path = `/api/projects/${teamId}/signals/reports/${reportId}/tasks/`; + + const response = await this.api.fetcher.fetch({ + method: "get", + url, + path, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch signal report tasks: ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.results ?? []; + } + + async getSignalTeamConfig(): Promise { + const teamId = await this.getTeamId(); + const url = new URL( + `${this.api.baseUrl}/api/projects/${teamId}/signals/config/`, + ); + const path = `/api/projects/${teamId}/signals/config/`; + + const response = await this.api.fetcher.fetch({ + method: "get", + url, + path, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch signal team config: ${response.statusText}`, + ); + } + + return (await response.json()) as SignalTeamConfig; + } + + async updateSignalTeamConfig(updates: { + default_autostart_priority: string; + }): Promise { + const teamId = await this.getTeamId(); + const url = new URL( + `${this.api.baseUrl}/api/projects/${teamId}/signals/config/`, + ); + const path = `/api/projects/${teamId}/signals/config/`; + + const response = await this.api.fetcher.fetch({ + method: "post", + url, + path, + overrides: { + body: JSON.stringify(updates), + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to update signal team config: ${response.statusText}`, + ); + } + + return (await response.json()) as SignalTeamConfig; + } + + async getSignalUserAutonomyConfig(): Promise { + const url = new URL(`${this.api.baseUrl}/api/users/@me/signal_autonomy/`); + const path = "/api/users/@me/signal_autonomy/"; + + const response = await this.api.fetcher.fetch({ + method: "get", + url, + path, + }); + + return (await response.json()) as SignalUserAutonomyConfig; + } + + async updateSignalUserAutonomyConfig(updates: { + autostart_priority: string | null; + }): Promise { + const url = new URL(`${this.api.baseUrl}/api/users/@me/signal_autonomy/`); + const path = "/api/users/@me/signal_autonomy/"; + + const response = await this.api.fetcher.fetch({ + method: "post", + url, + path, + overrides: { + body: JSON.stringify(updates), + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to update signal user autonomy config: ${response.statusText}`, + ); + } + return (await response.json()) as SignalUserAutonomyConfig; + } + + async deleteSignalUserAutonomyConfig(): Promise { + const url = new URL(`${this.api.baseUrl}/api/users/@me/signal_autonomy/`); + const path = "/api/users/@me/signal_autonomy/"; + + const response = await this.api.fetcher.fetch({ + method: "delete", + url, + path, + }); + + if (!response.ok) { + throw new Error( + `Failed to delete signal user autonomy config: ${response.statusText}`, + ); + } + } + async getMcpServers(): Promise { const teamId = await this.getTeamId(); const url = new URL( diff --git a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx index 394fbfdba..762efef53 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalSourceToggles.tsx @@ -2,7 +2,6 @@ import { ArrowSquareOutIcon, BrainIcon, BugIcon, - CircleNotchIcon, GithubLogoIcon, KanbanIcon, TicketIcon, @@ -19,10 +18,7 @@ import { Text, Tooltip, } from "@radix-ui/themes"; -import type { - Evaluation, - SignalSourceConfig, -} from "@renderer/api/posthogClient"; +import type { Evaluation } from "@renderer/api/posthogClient"; import { memo, useCallback } from "react"; export interface SignalSourceValues { @@ -45,6 +41,24 @@ interface SignalSourceToggleCardProps { onSetup?: () => void; loading?: boolean; statusSection?: React.ReactNode; + syncStatus?: string | null; +} + +function syncStatusLabel(status: string | null | undefined): { + text: string; + color: string; +} | null { + if (!status) return null; + switch (status) { + case "running": + return { text: "Syncing…", color: "var(--amber-11)" }; + case "completed": + return { text: "Synced", color: "var(--green-11)" }; + case "failed": + return { text: "Sync failed", color: "var(--red-11)" }; + default: + return null; + } } const SignalSourceToggleCard = memo(function SignalSourceToggleCard({ @@ -59,7 +73,10 @@ const SignalSourceToggleCard = memo(function SignalSourceToggleCard({ onSetup, loading, statusSection, + syncStatus, }: SignalSourceToggleCardProps) { + const statusInfo = checked ? syncStatusLabel(syncStatus) : null; + return ( {labelSuffix} + {statusInfo && ( + + {statusInfo.text} + + )} {description} @@ -243,30 +265,6 @@ export const EvaluationsSection = memo(function EvaluationsSection({ ); }); -function SourceRunningIndicator({ - status, - message, -}: { - status: SignalSourceConfig["status"]; - message: string; -}) { - if (status !== "running") { - return null; - } - return ( - - - - {message} - - - ); -} - interface SignalSourceTogglesProps { value: SignalSourceValues; onToggle: (source: keyof SignalSourceValues, enabled: boolean) => void; @@ -274,10 +272,9 @@ interface SignalSourceTogglesProps { sourceStates?: Partial< Record< keyof SignalSourceValues, - { requiresSetup: boolean; loading: boolean } + { requiresSetup: boolean; loading: boolean; syncStatus?: string | null } > >; - sessionAnalysisStatus?: SignalSourceConfig["status"]; onSetup?: (source: keyof SignalSourceValues) => void; evaluations?: Evaluation[]; evaluationsUrl?: string; @@ -289,7 +286,6 @@ export function SignalSourceToggles({ onToggle, disabled, sourceStates, - sessionAnalysisStatus, onSetup, evaluations, evaluationsUrl, @@ -321,39 +317,23 @@ export function SignalSourceToggles({ return ( - } - label="PostHog Error Tracking" - description="Surface new issues, reopenings, and volume spikes" - checked={value.error_tracking} - onCheckedChange={toggleErrorTracking} - disabled={disabled} - /> } label="PostHog Session Replay" - labelSuffix={ - - Alpha - - } description="Analyze session recordings and event data for UX issues" checked={value.session_replay} onCheckedChange={toggleSessionReplay} disabled={disabled} - statusSection={ - value.session_replay ? ( - - ) : undefined - } + syncStatus={sourceStates?.session_replay?.syncStatus} + /> + } + label="PostHog Error Tracking" + description="Surface new issues, reopenings, and volume spikes" + checked={value.error_tracking} + onCheckedChange={toggleErrorTracking} + disabled={disabled} + syncStatus={sourceStates?.error_tracking?.syncStatus} /> {evaluations && evaluationsUrl && onToggleEvaluation && ( } @@ -383,6 +364,7 @@ export function SignalSourceToggles({ requiresSetup={sourceStates?.linear?.requiresSetup} onSetup={setupLinear} loading={sourceStates?.linear?.loading} + syncStatus={sourceStates?.linear?.syncStatus} /> } @@ -394,6 +376,7 @@ export function SignalSourceToggles({ requiresSetup={sourceStates?.zendesk?.requiresSetup} onSetup={setupZendesk} loading={sourceStates?.zendesk?.loading} + syncStatus={sourceStates?.zendesk?.syncStatus} /> ); diff --git a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx index 69b836032..9134f890b 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx @@ -7,21 +7,41 @@ import { XCircleIcon, } from "@phosphor-icons/react"; import { Flex, Spinner, Text, Tooltip } from "@radix-ui/themes"; -import type { SignalReportStatus, Task } from "@shared/types"; +import type { SignalReportStatus, SignalReportTask, Task } from "@shared/types"; import { useState } from "react"; -function useReportTask(reportId: string) { - return useAuthenticatedQuery( +const RELATIONSHIP_LABELS: Record = { + repo_selection: "Repository selection", + research: "Research task", + implementation: "Implementation task", +}; + +interface ReportTaskData { + task: Task; + relationship: SignalReportTask["relationship"]; +} + +function useReportTask(reportId: string, reportStatus: SignalReportStatus) { + const isActive = + reportStatus === "candidate" || + reportStatus === "in_progress" || + reportStatus === "pending_input"; + + return useAuthenticatedQuery( ["inbox", "report-task", reportId], async (client) => { - const tasks = (await client.getTasks({ - originProduct: "signal_report", - })) as unknown as Task[]; - return tasks.find((t) => t.signal_report === reportId) ?? null; + const reportTasks = await client.getSignalReportTasks(reportId, { + relationship: "research", + }); + const match = reportTasks[0]; + if (!match) return null; + const task = await client.getTask(match.task_id); + return { task, relationship: match.relationship }; }, { enabled: !!reportId, - staleTime: 10_000, + staleTime: isActive ? 5_000 : 10_000, + refetchInterval: isActive ? 5_000 : false, }, ); } @@ -80,9 +100,12 @@ export function ReportTaskLogs({ reportId, reportStatus, }: ReportTaskLogsProps) { - const { data: task, isLoading } = useReportTask(reportId); + const { data, isLoading } = useReportTask(reportId, reportStatus); const [expanded, setExpanded] = useState(false); + const task = data?.task ?? null; + const relationship = data?.relationship ?? null; + const showBar = isLoading || !!task || @@ -190,7 +213,7 @@ export function ReportTaskLogs({ > {status.icon} - Research task + {relationship ? RELATIONSHIP_LABELS[relationship] : "Research task"} - ) : sourceProductMeta ? ( - - - ) : ( diff --git a/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx b/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx index ab50dcb44..0072873f7 100644 --- a/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx +++ b/apps/code/src/renderer/features/inbox/components/utils/ReportCardContent.tsx @@ -2,8 +2,7 @@ import { SignalReportActionabilityBadge } from "@features/inbox/components/utils import { SignalReportPriorityBadge } from "@features/inbox/components/utils/SignalReportPriorityBadge"; import { SignalReportStatusBadge } from "@features/inbox/components/utils/SignalReportStatusBadge"; import { SignalReportSummaryMarkdown } from "@features/inbox/components/utils/SignalReportSummaryMarkdown"; -import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/source-product-icons"; -import { EyeIcon, LightningIcon, UsersIcon } from "@phosphor-icons/react"; +import { EyeIcon, LightningIcon } from "@phosphor-icons/react"; import { Badge, Flex, Text, Tooltip } from "@radix-ui/themes"; import type { SignalReport } from "@shared/types"; @@ -24,25 +23,9 @@ export function ReportCardContent({ { month: "short", day: "numeric" }, ); - const firstProduct = (report.source_products ?? [])[0]; - const sourceProductMeta = firstProduct - ? SOURCE_PRODUCT_META[firstProduct] - : null; - return ( - {sourceProductMeta && ( - - - - - - )} - - {report.relevant_user_count != null && - report.relevant_user_count > 0 && ( - - - - {report.relevant_user_count} user - {report.relevant_user_count !== 1 ? "s" : ""} - - - )} {updatedAtLabel} diff --git a/apps/code/src/renderer/features/inbox/devtools/inboxDemoConsole.ts b/apps/code/src/renderer/features/inbox/devtools/inboxDemoConsole.ts index f0d353c59..afa9876e1 100644 --- a/apps/code/src/renderer/features/inbox/devtools/inboxDemoConsole.ts +++ b/apps/code/src/renderer/features/inbox/devtools/inboxDemoConsole.ts @@ -35,7 +35,6 @@ function getDemoReports(): SignalReport[] { status: "ready", total_weight: 79, signal_count: 31, - relevant_user_count: 12, created_at: iso(1800), updated_at: iso(8), artefact_count: 3, @@ -48,7 +47,6 @@ function getDemoReports(): SignalReport[] { status: "ready", total_weight: 52, signal_count: 11, - relevant_user_count: 6, created_at: iso(2200), updated_at: iso(35), artefact_count: 2, @@ -61,7 +59,6 @@ function getDemoReports(): SignalReport[] { status: "ready", total_weight: 24, signal_count: 4, - relevant_user_count: 3, created_at: iso(3600), updated_at: iso(140), artefact_count: 1, diff --git a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts index 3b45bcfb7..8beec1314 100644 --- a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts +++ b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts @@ -12,6 +12,8 @@ import { toast } from "sonner"; import { useEvaluations } from "./useEvaluations"; import { useExternalDataSources } from "./useExternalDataSources"; import { useSignalSourceConfigs } from "./useSignalSourceConfigs"; +import { useSignalTeamConfig } from "./useSignalTeamConfig"; +import { useSignalUserAutonomyConfig } from "./useSignalUserAutonomyConfig"; type SourceProduct = SignalSourceConfig["source_product"]; type SourceType = SignalSourceConfig["source_type"]; @@ -96,6 +98,8 @@ export function useSignalSourceManager() { const { data: externalSources, isLoading: sourcesLoading } = useExternalDataSources(); const { data: evaluations } = useEvaluations(); + const { data: teamConfig } = useSignalTeamConfig(); + const { data: userAutonomyConfig } = useSignalUserAutonomyConfig(); // Optimistic overrides keyed by source product — only sources actively being // toggled get an entry, so unrelated sources never see a prop change. @@ -130,15 +134,6 @@ export function useSignalSourceManager() { [configs], ); - const sessionAnalysisStatus = useMemo(() => { - const config = configs?.find( - (c) => - c.source_product === "session_replay" && - c.source_type === "session_analysis_cluster", - ); - return config?.status ?? null; - }, [configs]); - // Merge: optimistic overrides take precedence over server values. const displayValues = useMemo(() => { if (Object.keys(optimistic).length === 0) return serverValues; @@ -149,19 +144,38 @@ export function useSignalSourceManager() { const states: Partial< Record< keyof SignalSourceValues, - { requiresSetup: boolean; loading: boolean } + { + requiresSetup: boolean; + loading: boolean; + syncStatus?: string | null; + } > > = {}; - for (const product of ["github", "linear", "zendesk"] as const) { - const hasExternalSource = !!findExternalSource(product); - const isEnabled = serverValues[product]; - states[product] = { - requiresSetup: !hasExternalSource && !isEnabled, - loading: !!loadingSources[product], - }; + for (const product of ALL_SOURCE_PRODUCTS) { + if ( + product === "github" || + product === "linear" || + product === "zendesk" + ) { + const hasExternalSource = !!findExternalSource(product); + const isEnabled = serverValues[product]; + const config = configs?.find((c) => c.source_product === product); + states[product] = { + requiresSetup: !hasExternalSource && !isEnabled, + loading: !!loadingSources[product], + syncStatus: config?.status ?? null, + }; + } else { + const config = configs?.find((c) => c.source_product === product); + states[product] = { + requiresSetup: false, + loading: false, + syncStatus: config?.status ?? null, + }; + } } return states; - }, [findExternalSource, serverValues, loadingSources]); + }, [findExternalSource, serverValues, loadingSources, configs]); const evaluationsUrl = useMemo(() => { if (!cloudRegion) return ""; @@ -402,10 +416,56 @@ export function useSignalSourceManager() { setSetupSource(null); }, []); + const handleUpdateAutostartPriority = useCallback( + async (priority: string) => { + if (!client) return; + try { + await client.updateSignalTeamConfig({ + default_autostart_priority: priority, + }); + await queryClient.invalidateQueries({ + queryKey: ["signals", "team-config"], + }); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : "Failed to update autostart priority"; + toast.error(message); + } + }, + [client, queryClient], + ); + + const handleUpdateUserAutonomyPriority = useCallback( + async (priority: string | null) => { + if (!client) return; + try { + if (priority === null) { + await client.deleteSignalUserAutonomyConfig(); + } else { + await client.updateSignalUserAutonomyConfig({ + autostart_priority: priority, + }); + } + await queryClient.invalidateQueries({ + queryKey: ["signals", "user-autonomy-config"], + }); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : "Failed to update autonomy setting"; + toast.error(message); + } + }, + [client, queryClient], + ); + return { displayValues, sourceStates, - sessionAnalysisStatus, + setupSource, isLoading, handleToggle, @@ -415,5 +475,9 @@ export function useSignalSourceManager() { evaluations: displayEvaluations, evaluationsUrl, handleToggleEvaluation, + teamConfig, + handleUpdateAutostartPriority, + userAutonomyConfig, + handleUpdateUserAutonomyPriority, }; } diff --git a/apps/code/src/renderer/features/inbox/hooks/useSignalTeamConfig.ts b/apps/code/src/renderer/features/inbox/hooks/useSignalTeamConfig.ts new file mode 100644 index 000000000..1183d82de --- /dev/null +++ b/apps/code/src/renderer/features/inbox/hooks/useSignalTeamConfig.ts @@ -0,0 +1,23 @@ +import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; +import type { SignalTeamConfig } from "@shared/types"; + +export function useSignalTeamConfig(options?: { + enabled?: boolean; + staleTime?: number; +}) { + return useAuthenticatedQuery( + ["signals", "team-config"], + async (client) => { + try { + return await client.getSignalTeamConfig(); + } catch { + // Team config may not exist yet + return null; + } + }, + { + enabled: options?.enabled ?? true, + staleTime: options?.staleTime ?? 30_000, + }, + ); +} diff --git a/apps/code/src/renderer/features/inbox/hooks/useSignalUserAutonomyConfig.ts b/apps/code/src/renderer/features/inbox/hooks/useSignalUserAutonomyConfig.ts new file mode 100644 index 000000000..39b29fde5 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/hooks/useSignalUserAutonomyConfig.ts @@ -0,0 +1,23 @@ +import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; +import type { SignalUserAutonomyConfig } from "@shared/types"; + +export function useSignalUserAutonomyConfig(options?: { + enabled?: boolean; + staleTime?: number; +}) { + return useAuthenticatedQuery( + ["signals", "user-autonomy-config"], + async (client) => { + try { + return await client.getSignalUserAutonomyConfig(); + } catch { + // 404 when user has opted out (no config record) + return null; + } + }, + { + enabled: options?.enabled ?? true, + staleTime: options?.staleTime ?? 30_000, + }, + ); +} diff --git a/apps/code/src/renderer/features/inbox/utils/buildSignalTaskPrompt.ts b/apps/code/src/renderer/features/inbox/utils/buildSignalTaskPrompt.ts index eb6144a66..a519bb31f 100644 --- a/apps/code/src/renderer/features/inbox/utils/buildSignalTaskPrompt.ts +++ b/apps/code/src/renderer/features/inbox/utils/buildSignalTaskPrompt.ts @@ -23,7 +23,7 @@ export function buildSignalTaskPrompt({ "", summary, "", - `**Signal strength:** ${report.signal_count} occurrences, ${report.relevant_user_count ?? 0} affected users`, + `**Signal strength:** ${report.signal_count} occurrences`, ]; if (signals.length > 0) { diff --git a/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts b/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts index 0bb61db6f..6042daec0 100644 --- a/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts +++ b/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts @@ -14,7 +14,6 @@ function makeReport(overrides: Partial = {}): SignalReport { status: "ready", total_weight: 50, signal_count: 10, - relevant_user_count: 5, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-01-02T00:00:00Z", artefact_count: 3, 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 53fe39ce9..fee11dc62 100644 --- a/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx @@ -1,14 +1,28 @@ import { DataSourceSetup } from "@features/inbox/components/DataSourceSetup"; import { SignalSourceToggles } from "@features/inbox/components/SignalSourceToggles"; import { useSignalSourceManager } from "@features/inbox/hooks/useSignalSourceManager"; -import { useMeQuery } from "@hooks/useMeQuery"; -import { Flex, Text } from "@radix-ui/themes"; +import { Box, Flex, Select, Text } from "@radix-ui/themes"; +import type { SignalReportPriority } from "@shared/types"; + +const PRIORITY_OPTIONS: { value: SignalReportPriority; label: string }[] = [ + { value: "P0", label: "P0 — Critical only" }, + { value: "P1", label: "P1 — High and above" }, + { value: "P2", label: "P2 — Medium and above" }, + { value: "P3", label: "P3 — Low and above" }, + { value: "P4", label: "P4 — All priorities" }, +]; + +const NEVER_VALUE = "__never__"; + +const USER_PRIORITY_OPTIONS: { value: string; label: string }[] = [ + ...PRIORITY_OPTIONS, + { value: NEVER_VALUE, label: "Never — opt out of auto-assigned tasks" }, +]; export function SignalSourcesSettings() { const { displayValues, sourceStates, - sessionAnalysisStatus, setupSource, isLoading, handleToggle, @@ -18,9 +32,11 @@ export function SignalSourcesSettings() { evaluations, evaluationsUrl, handleToggleEvaluation, + teamConfig, + handleUpdateAutostartPriority, + userAutonomyConfig, + handleUpdateUserAutonomyPriority, } = useSignalSourceManager(); - const { data: me } = useMeQuery(); - const isStaff = me?.is_staff ?? false; if (isLoading) { return ( @@ -30,6 +46,9 @@ export function SignalSourcesSettings() { ); } + const userPriorityValue = + userAutonomyConfig?.autostart_priority ?? NEVER_VALUE; + return ( @@ -44,20 +63,100 @@ export function SignalSourcesSettings() { onCancel={handleSetupCancel} /> ) : ( - void handleToggle(source, enabled)} - sourceStates={sourceStates} - sessionAnalysisStatus={sessionAnalysisStatus} - onSetup={handleSetup} - evaluations={isStaff ? evaluations : undefined} - evaluationsUrl={isStaff ? evaluationsUrl : undefined} - onToggleEvaluation={ - isStaff - ? (id, enabled) => void handleToggleEvaluation(id, enabled) - : undefined - } - /> + <> + void handleToggle(source, enabled)} + sourceStates={sourceStates} + onSetup={handleSetup} + evaluations={evaluations} + evaluationsUrl={evaluationsUrl} + onToggleEvaluation={(id, enabled) => + void handleToggleEvaluation(id, enabled) + } + /> + + {teamConfig && ( + + + + Auto-research priority + + + Automatically start research on signal reports at or above + this priority level. + + + void handleUpdateAutostartPriority(value) + } + > + + + {PRIORITY_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + + + + + )} + + + + + Your auto-start threshold + + + Automatically start tasks assigned to you for reports at or + above this priority. Choose "Never" to opt out + entirely. + + + void handleUpdateUserAutonomyPriority( + value === NEVER_VALUE ? null : value, + ) + } + > + + + {USER_PRIORITY_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + + + + + )} ); diff --git a/apps/code/src/shared/types.ts b/apps/code/src/shared/types.ts index 1e5751808..46c7b2056 100644 --- a/apps/code/src/shared/types.ts +++ b/apps/code/src/shared/types.ts @@ -264,7 +264,6 @@ export interface SignalReport { total_weight: number; signal_count: number; signals_at_run?: number; - relevant_user_count: number | null; created_at: string; updated_at: string; artefact_count: number; @@ -276,8 +275,6 @@ export interface SignalReport { already_addressed?: boolean | 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 { @@ -456,3 +453,24 @@ export interface SignalReportsQueryParams { /** Comma-separated PostHog user UUIDs — only returns reports with these suggested reviewers. */ suggested_reviewers?: string; } + +export interface SignalReportTask { + id: string; + relationship: "repo_selection" | "research" | "implementation"; + task_id: string; + created_at: string; +} + +export interface SignalTeamConfig { + id: string; + default_autostart_priority: SignalReportPriority; + created_at: string; + updated_at: string; +} + +export interface SignalUserAutonomyConfig { + id?: string; + autostart_priority: SignalReportPriority | null; + created_at?: string; + updated_at?: string; +}