From d2806c7f102b7afdac6de2b0e72f325e6b147004 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Wed, 8 Apr 2026 13:28:19 +0300 Subject: [PATCH] sig: add paused status indicator to inbox toolbar --- apps/code/src/renderer/api/posthogClient.ts | 27 +++++++++++++ .../inbox/components/InboxSignalsTab.tsx | 10 +++++ .../inbox/components/list/SignalsToolbar.tsx | 39 ++++++++++++++++++- .../features/inbox/hooks/useInboxReports.ts | 15 +++++++ apps/code/src/shared/types.ts | 4 ++ 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index fe3c24aa8..4343fb3a7 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -3,6 +3,7 @@ import type { AvailableSuggestedReviewersResponse, SandboxEnvironment, SandboxEnvironmentInput, + SignalProcessingStateResponse, SignalReport, SignalReportArtefact, SignalReportArtefactsResponse, @@ -1025,6 +1026,32 @@ export class PostHogAPIClient { }; } + async getSignalProcessingState(): Promise { + const teamId = await this.getTeamId(); + const url = new URL( + `${this.api.baseUrl}/api/projects/${teamId}/signal_processing/`, + ); + const path = `/api/projects/${teamId}/signal_processing/`; + + const response = await this.api.fetcher.fetch({ + method: "get", + url, + path, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch signal processing state: ${response.statusText}`, + ); + } + + const data = await response.json(); + return { + paused_until: + typeof data?.paused_until === "string" ? data.paused_until : null, + }; + } + async getAvailableSuggestedReviewers( query?: string, ): Promise { diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 82c4bcd51..a7bb26e26 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -9,6 +9,7 @@ import { InboxSourcesDialog } from "@features/inbox/components/InboxSourcesDialo import { useInboxAvailableSuggestedReviewers, useInboxReportsInfinite, + useInboxSignalProcessingState, } from "@features/inbox/hooks/useInboxReports"; import { useSignalSourceConfigs } from "@features/inbox/hooks/useSignalSourceConfigs"; import { useInboxReportSelectionStore } from "@features/inbox/stores/inboxReportSelectionStore"; @@ -118,6 +119,13 @@ export function InboxSignalsTab() { [allReports, searchQuery], ); + const { data: signalProcessingState } = useInboxSignalProcessingState({ + enabled: isInboxView, + refetchInterval: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : false, + refetchIntervalInBackground: false, + staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 12_000, + }); + const readyCount = useMemo( () => allReports.filter((r) => r.status === "ready").length, [allReports], @@ -378,6 +386,7 @@ export function InboxSignalsTab() { livePolling={inboxPollingActive} readyCount={readyCount} processingCount={processingCount} + pipelinePausedUntil={signalProcessingState?.paused_until} reports={reports} /> @@ -447,6 +456,7 @@ export function InboxSignalsTab() { totalCount={0} filteredCount={0} isSearchActive={false} + pipelinePausedUntil={signalProcessingState?.paused_until} searchDisabledReason={searchDisabledReason} hideFilters /> diff --git a/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx b/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx index 825d75d1b..c2c311f78 100644 --- a/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx +++ b/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx @@ -32,11 +32,38 @@ interface SignalsToolbarProps { livePolling?: boolean; readyCount?: number; processingCount?: number; + pipelinePausedUntil?: string | null; searchDisabledReason?: string | null; hideFilters?: boolean; reports?: SignalReport[]; } +function formatPauseRemaining(pausedUntil: string): string { + const diffMs = new Date(pausedUntil).getTime() - Date.now(); + + if (diffMs <= 0) { + return "resuming soon"; + } + + const totalMinutes = Math.ceil(diffMs / 60_000); + + if (totalMinutes < 60) { + return `${totalMinutes}m`; + } + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + if (hours < 24) { + return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; + } + + const days = Math.floor(hours / 24); + const remainingHours = hours % 24; + + return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`; +} + export function SignalsToolbar({ totalCount, filteredCount, @@ -44,6 +71,7 @@ export function SignalsToolbar({ livePolling = false, readyCount, processingCount = 0, + pipelinePausedUntil, searchDisabledReason, hideFilters, reports = [], @@ -79,10 +107,17 @@ export function SignalsToolbar({ ? `${filteredCount} of ${totalCount}` : `${totalCount}`; - const pipelineHint = + const pipelineHintParts = [ readyCount != null && processingCount > 0 ? `${readyCount} ready · ${processingCount} in pipeline` - : null; + : null, + pipelinePausedUntil + ? `Pipeline paused · resumes in ${formatPauseRemaining(pipelinePausedUntil)}` + : "Pipeline running", + ].filter(Boolean); + + const pipelineHint = + pipelineHintParts.length > 0 ? pipelineHintParts.join(" · ") : null; const handleConfirmSuppress = async () => { const ok = await suppressSelected(); diff --git a/apps/code/src/renderer/features/inbox/hooks/useInboxReports.ts b/apps/code/src/renderer/features/inbox/hooks/useInboxReports.ts index f218afb12..312e386d9 100644 --- a/apps/code/src/renderer/features/inbox/hooks/useInboxReports.ts +++ b/apps/code/src/renderer/features/inbox/hooks/useInboxReports.ts @@ -7,6 +7,7 @@ import { useAuthenticatedInfiniteQuery } from "@hooks/useAuthenticatedInfiniteQu import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; import type { AvailableSuggestedReviewersResponse, + SignalProcessingStateResponse, SignalReportArtefactsResponse, SignalReportSignalsResponse, SignalReportsQueryParams, @@ -32,6 +33,7 @@ const reportKeys = { authIdentity ?? "anonymous", "available-reviewers", ] as const, + signalProcessingState: ["inbox", "signal-processing-state"] as const, }; export function useInboxReports( @@ -149,6 +151,19 @@ export function useInboxAvailableSuggestedReviewers(options?: { return query; } +export function useInboxSignalProcessingState(options?: { + enabled?: boolean; + refetchInterval?: number | false | (() => number | false | undefined); + refetchIntervalInBackground?: boolean; + staleTime?: number; +}) { + return useAuthenticatedQuery( + reportKeys.signalProcessingState, + (client) => client.getSignalProcessingState(), + options, + ); +} + export function useInboxReportArtefacts( reportId: string, options?: { enabled?: boolean }, diff --git a/apps/code/src/shared/types.ts b/apps/code/src/shared/types.ts index 5e7a83c44..3ce57024f 100644 --- a/apps/code/src/shared/types.ts +++ b/apps/code/src/shared/types.ts @@ -282,6 +282,10 @@ export interface SignalReportsResponse { count: number; } +export interface SignalProcessingStateResponse { + paused_until: string | null; +} + export interface AvailableSuggestedReviewersResponse { results: AvailableSuggestedReviewer[]; count: number;