Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,6 @@ export function InboxSignalsTab() {
isSearchActive={!!searchQuery.trim()}
livePolling={inboxPollingActive}
isFetching={isFetching}
readyCount={readyCount}
processingCount={processingCount}
pipelinePausedUntil={signalProcessingState?.paused_until}
reports={reports}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button, type ButtonProps } from "@components/ui/Button";
import { Tooltip as ActionTooltip } from "@components/ui/Tooltip";
import { useInboxBulkActions } from "@features/inbox/hooks/useInboxBulkActions";
import { useUpForReviewCount } from "@features/inbox/hooks/useInboxReports";
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
import { INBOX_REFETCH_INTERVAL_MS } from "@features/inbox/utils/inboxConstants";
import {
Expand Down Expand Up @@ -42,7 +43,6 @@ interface SignalsToolbarProps {
isSearchActive: boolean;
livePolling?: boolean;
isFetching?: boolean;
readyCount?: number;
processingCount?: number;
pipelinePausedUntil?: string | null;
searchDisabledReason?: string | null;
Expand Down Expand Up @@ -103,6 +103,12 @@ function formatPauseRemaining(pausedUntil: string): string {

const inboxLivePollingTooltip = `Inbox is focused – syncing reports every ${Math.round(INBOX_REFETCH_INTERVAL_MS / 1000)}s…`;

const UP_FOR_REVIEW_TOOLTIP =
"Reports assigned to you that are ready and immediately actionable.";

const INBOX_TOTAL_TOOLTIP =
"All reports currently in your inbox view, after any search, status, source, or reviewer filters you have applied.";

function bulkMenuItemTooltip(
explanationWhenEnabled: string,
headlineWhenDisabled: string,
Expand Down Expand Up @@ -255,7 +261,6 @@ export function SignalsToolbar({
isSearchActive,
livePolling = false,
isFetching = false,
readyCount,
processingCount = 0,
pipelinePausedUntil,
searchDisabledReason,
Expand All @@ -268,6 +273,15 @@ export function SignalsToolbar({
isDismissMutationPending = false,
onReportAction,
}: SignalsToolbarProps) {
// Shared with the sidebar badge: both call this hook with matching params
// and React Query dedupes the underlying fetch, so the two numbers cannot
// disagree even when the user has applied filters that hide some reports
// from the list below.
const upForReviewCount = useUpForReviewCount({
refetchInterval: livePolling ? INBOX_REFETCH_INTERVAL_MS : false,
refetchIntervalInBackground: false,
staleTime: livePolling ? INBOX_REFETCH_INTERVAL_MS : 15_000,
});
const searchQuery = useInboxSignalsFilterStore((s) => s.searchQuery);
const setSearchQuery = useInboxSignalsFilterStore((s) => s.setSearchQuery);
const [showSnoozeConfirm, setShowSnoozeConfirm] = useState(false);
Expand All @@ -291,21 +305,27 @@ export function SignalsToolbar({
reingestSelected,
} = useInboxBulkActions(reports, effectiveBulkIds);

const countLabel = isSearchActive
? `${filteredCount} of ${totalCount}`
: `${totalCount}`;

const pipelineHintParts = [
readyCount != null && processingCount > 0
? `${readyCount} up for review • ${processingCount} in research pipeline`
: null,
pipelinePausedUntil
? `Pipeline paused · resumes in ${formatPauseRemaining(pipelinePausedUntil)}`
: null,
].filter(Boolean);

const pipelineHint =
pipelineHintParts.length > 0 ? pipelineHintParts.join(" · ") : null;
// Sub-line: how many reports the user is browsing right now (search-filtered
// count when searching, raw inbox total otherwise) plus pipeline status. The
// headline above is the narrower "needs your action" count; this line is the
// wider "everything in your inbox" context.
const inboxSubParts: string[] = [];
if (isSearchActive) {
inboxSubParts.push(`${filteredCount} of ${totalCount} match search`);
} else {
inboxSubParts.push(
`${totalCount} ${totalCount === 1 ? "report" : "reports"} in inbox`,
);
if (processingCount > 0) {
inboxSubParts.push(`${processingCount} still processing`);
}
}
if (pipelinePausedUntil) {
inboxSubParts.push(
`Pipeline paused · resumes in ${formatPauseRemaining(pipelinePausedUntil)}`,
);
}
const inboxSubLine = inboxSubParts.join(" · ");

const multiSelectBulkActions = selectedCount > 1;

Expand Down Expand Up @@ -469,9 +489,11 @@ export function SignalsToolbar({
<Flex align="center" justify="between" gap="2">
<Flex direction="column" gap="0" className="min-w-0">
<Flex align="center" gap="2">
<Text color="gray" className="shrink-0 text-[12px]">
Reports ({countLabel})
</Text>
<Tooltip content={UP_FOR_REVIEW_TOOLTIP}>
<Text color="gray" className="shrink-0 cursor-help text-[12px]">
Up for review ({upForReviewCount})
</Text>
</Tooltip>
{livePolling ? (
<Tooltip content={inboxLivePollingTooltip}>
<span
Expand All @@ -492,11 +514,11 @@ export function SignalsToolbar({
</Tooltip>
) : null}
</Flex>
{pipelineHint && !isSearchActive ? (
<Text color="gray" className="text-[11px] opacity-80">
{pipelineHint}
<Tooltip content={INBOX_TOTAL_TOOLTIP}>
<Text color="gray" className="cursor-help text-[11px] opacity-80">
{inboxSubLine}
</Text>
) : null}
</Tooltip>
</Flex>
{onConfigureSources ? (
<button
Expand Down
24 changes: 24 additions & 0 deletions apps/code/src/renderer/features/inbox/hooks/useInboxReports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
useAuthStateValue,
} from "@features/auth/hooks/authQueries";
import { useInboxAvailableSuggestedReviewersStore } from "@features/inbox/stores/inboxAvailableSuggestedReviewersStore";
import { isReportUpForReview } from "@features/inbox/utils/filterReports";
import { INBOX_PIPELINE_STATUS_FILTER } from "@features/inbox/utils/inboxConstants";
import { useAuthenticatedInfiniteQuery } from "@hooks/useAuthenticatedInfiniteQuery";
import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery";
import type {
Expand Down Expand Up @@ -99,6 +101,28 @@ export function useInboxReportsInfinite(
return { ...query, allReports, totalCount };
}

/**
* Count of reports the current user needs to action right now: assigned to
* them, status `ready`, and `immediately_actionable`. Drives both the sidebar
* Inbox badge and the inbox toolbar headline so the two numbers always agree
* (React Query dedupes the underlying fetch by query key).
*/
export function useUpForReviewCount(options?: {
enabled?: boolean;
refetchInterval?: number | false | (() => number | false | undefined);
refetchIntervalInBackground?: boolean;
staleTime?: number;
}): number {
const { data } = useInboxReports(
{ status: INBOX_PIPELINE_STATUS_FILTER },
options,
);
return useMemo(
() => (data?.results ?? []).filter(isReportUpForReview).length,
[data?.results],
);
}

export function useInboxAvailableSuggestedReviewers(options?: {
enabled?: boolean;
staleTime?: number;
Expand Down
23 changes: 7 additions & 16 deletions apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { DotsCircleSpinner } from "@components/DotsCircleSpinner";
import { useCommandCenterStore } from "@features/command-center/stores/commandCenterStore";
import { useInboxReports } from "@features/inbox/hooks/useInboxReports";
import { isReportUpForReview } from "@features/inbox/utils/filterReports";
import {
INBOX_PIPELINE_STATUS_FILTER,
INBOX_REFETCH_INTERVAL_MS,
} from "@features/inbox/utils/inboxConstants";
import { useUpForReviewCount } from "@features/inbox/hooks/useInboxReports";
import { INBOX_REFETCH_INTERVAL_MS } from "@features/inbox/utils/inboxConstants";
import { getSessionService } from "@features/sessions/service/service";
import {
archiveTasksImperative,
Expand Down Expand Up @@ -68,16 +64,11 @@ function SidebarMenuComponent() {
activeView: view,
});
const inboxPollingActive = useRendererWindowFocusStore((s) => s.focused);
const { data: inboxProbe } = useInboxReports(
{ status: INBOX_PIPELINE_STATUS_FILTER },
{
refetchInterval: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : false,
refetchIntervalInBackground: false,
staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 15_000,
},
);
const inboxResults = inboxProbe?.results ?? [];
const inboxSignalCount = inboxResults.filter(isReportUpForReview).length;
const inboxSignalCount = useUpForReviewCount({
refetchInterval: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : false,
refetchIntervalInBackground: false,
staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 15_000,
});

const taskMap = new Map<string, Task>();
for (const task of allTasks) {
Expand Down