From 233a0418ff9be638d39d7bc8d026c6f829066305 Mon Sep 17 00:00:00 2001 From: limityan Date: Mon, 27 Apr 2026 23:02:18 +0800 Subject: [PATCH] feat(deep-review): enhance error and interruption experience Add comprehensive error handling, progress tracking, and recovery options for deep review interruptions and failures. - Add deepReviewExperience.ts utility for progress aggregation, error attribution, recovery plans, and degradation options - Extend DeepReviewActionBar with reviewer progress display, partial results visibility, error attribution cards, recovery plan preview, and context overflow degradation - Add retry options for resume_failed state (retry, switch model, view partial results) - Add token consumption estimate to consent dialog - Add friendly launch error classification in DeepReviewService - Add long-running hint after 3 minutes - Add i18n strings for all new UI elements (zh-CN, en-US, zh-TW) - Update DeepReviewService test for new error format --- .../agents/components/ReviewTeamPage.test.tsx | 30 +- .../src/flow_chat/components/ChatInput.tsx | 16 +- .../components/DeepReviewConsentDialog.scss | 8 + .../components/DeepReviewConsentDialog.tsx | 11 + .../components/btw/DeepReviewActionBar.scss | 218 ++++++++++ .../btw/DeepReviewActionBar.test.tsx | 42 ++ .../components/btw/DeepReviewActionBar.tsx | 372 ++++++++++++++++- .../services/DeepReviewService.test.ts | 43 +- .../flow_chat/services/DeepReviewService.ts | 71 +++- .../flow_chat/utils/deepReviewContinuation.ts | 2 +- .../flow_chat/utils/deepReviewExperience.ts | 391 ++++++++++++++++++ src/web-ui/src/locales/en-US/flow-chat.json | 39 +- src/web-ui/src/locales/zh-CN/flow-chat.json | 39 +- src/web-ui/src/locales/zh-TW/flow-chat.json | 39 +- 14 files changed, 1277 insertions(+), 44 deletions(-) create mode 100644 src/web-ui/src/flow_chat/utils/deepReviewExperience.ts diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx index e2f3a6b3f..3d285c586 100644 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx +++ b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx @@ -158,15 +158,23 @@ describeWithJsdom('ReviewTeamPage', () => { vi.clearAllMocks(); }); + async function waitForText(text: string, maxTicks = 20) { + for (let i = 0; i < maxTicks; i++) { + await act(async () => { + await Promise.resolve(); + }); + if (container.textContent?.includes(text)) return; + } + throw new Error(`waitForText: "${text}" not found after ${maxTicks} ticks`); + } + it('loads review team data only once on initial render', async () => { const { default: ReviewTeamPage } = await import('./ReviewTeamPage'); await act(async () => { root.render(); }); - await act(async () => { - await Promise.resolve(); - }); + await waitForText('Team Overview'); expect(loadDefaultReviewTeam).toHaveBeenCalledTimes(1); }); @@ -177,9 +185,7 @@ describeWithJsdom('ReviewTeamPage', () => { await act(async () => { root.render(); }); - await act(async () => { - await Promise.resolve(); - }); + await waitForText('Team Overview'); expect(container.textContent).toContain('Team Overview'); expect(container.textContent).toContain('Current Policy'); @@ -200,9 +206,7 @@ describeWithJsdom('ReviewTeamPage', () => { await act(async () => { root.render(); }); - await act(async () => { - await Promise.resolve(); - }); + await waitForText('Team Overview'); const settingsButton = Array.from(container.querySelectorAll('button')) .find((button) => button.textContent?.includes('Review settings')); @@ -227,9 +231,7 @@ describeWithJsdom('ReviewTeamPage', () => { await act(async () => { root.render(); }); - await act(async () => { - await Promise.resolve(); - }); + await waitForText('Team Overview'); const policyPanel = container.querySelector('.review-team-page__policy-panel'); expect(policyPanel).toBeTruthy(); @@ -287,9 +289,7 @@ describeWithJsdom('ReviewTeamPage', () => { await act(async () => { root.render(); }); - await act(async () => { - await Promise.resolve(); - }); + await waitForText('Logic'); const memberButton = Array.from(container.querySelectorAll('button')) .find((button) => button.textContent?.includes('Logic')); diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 372f1f22b..fb80040bd 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -7,10 +7,10 @@ import React, { useRef, useCallback, useEffect, useReducer, useState, useMemo } import { Trans, useTranslation } from 'react-i18next'; import { ArrowUp, Image, Maximize2, Minimize2, RotateCcw, Plus, X, Sparkles, Loader2, ChevronRight, Files, MessageSquarePlus } from 'lucide-react'; import { ContextDropZone, useContextStore } from '../../shared/context-system'; -import { useActiveSessionState } from '../hooks/useActiveSessionState'; +import { useActiveSessionState } from '@/flow_chat/hooks'; import { RichTextInput, type MentionState } from './RichTextInput'; import { FileMentionPicker } from './FileMentionPicker'; -import { globalEventBus } from '../../infrastructure/event-bus'; +import { globalEventBus } from '@/infrastructure'; import { useSessionDerivedState, useSessionStateMachine, @@ -20,7 +20,7 @@ import { SessionExecutionEvent } from '../state-machine/types'; import { ModelSelector } from './ModelSelector'; import { FlowChatStore } from '../store/FlowChatStore'; import type { FlowChatState } from '../types/flow-chat'; -import type { FileContext, DirectoryContext, ImageContext } from '../../shared/types/context'; +import type { FileContext, DirectoryContext, ImageContext } from '@/types/context.ts'; import { SmartRecommendations } from './smart-recommendations'; import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { WorkspaceKind } from '@/shared/types'; @@ -33,10 +33,11 @@ import { useMessageSender } from '../hooks/useMessageSender'; import { useChatInputState } from '../store/chatInputStateStore'; import { useInputHistoryStore } from '../store/inputHistoryStore'; import { startBtwThread } from '../services/BtwThreadService'; -import { FlowChatManager } from '../services/FlowChatManager'; +import { FlowChatManager } from '@/flow_chat'; import { DEEP_REVIEW_SLASH_COMMAND, buildDeepReviewPromptFromSlashCommand, + getDeepReviewLaunchErrorMessage, isDeepReviewSlashCommand, launchDeepReviewSession, } from '../services/DeepReviewService'; @@ -1137,8 +1138,7 @@ export const ChatInput: React.FC = ({ if (text.startsWith('/')) { const afterSlash = text.slice(1); const hasWhitespace = /\s/.test(afterSlash); - const firstToken = afterSlash.trimStart().split(/\s+/, 1)[0]?.toLowerCase?.() ?? ''; - const query = firstToken; + const query = afterSlash.trimStart().split(/\s+/, 1)[0]?.toLowerCase?.() ?? ''; const matchedMcpPrompt = resolveTypedMcpPromptCommand(text); // While the main session is running, expose a single quick action (/btw) via the same picker UX. @@ -1453,7 +1453,7 @@ export const ChatInput: React.FC = ({ dispatchInput({ type: 'ACTIVATE' }); dispatchInput({ type: 'SET_VALUE', payload: message }); notificationService.error( - error instanceof Error ? error.message : t('error.unknown'), + getDeepReviewLaunchErrorMessage(error, t, t('error.unknown')), { title: t('chatInput.deepreviewFailed', { defaultValue: 'Deep review failed' }), duration: 5000, @@ -2255,7 +2255,7 @@ export const ChatInput: React.FC = ({ const isCollapsedProcessing = !inputState.isActive && !!derivedState?.isProcessing; const petReplacesStopChrome = agentCompanionEnabled && isCollapsedProcessing; - const petStopClickable = petReplacesStopChrome && !!derivedState?.canCancel; + const petStopClickable = petReplacesStopChrome && derivedState?.canCancel; const collapsedPetSplitSend = petReplacesStopChrome && derivedState?.sendButtonMode === 'split'; diff --git a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss index cb3e69b56..ca709bd5d 100644 --- a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss +++ b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.scss @@ -143,6 +143,14 @@ line-height: 1.35; } +.deep-review-consent__token-estimate { + margin: 6px 0 0; + color: var(--color-text-muted); + font-size: 11px; + font-weight: 500; + font-variant-numeric: tabular-nums; +} + .deep-review-consent__footer { display: flex; align-items: center; diff --git a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx index 566a5f55b..c743b2706 100644 --- a/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx +++ b/src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useState } from 'react'; import { Clock, Coins, ShieldCheck, X } from 'lucide-react'; +import { estimateTokenConsumption, formatTokenCount } from '../utils/deepReviewExperience'; import { useTranslation } from 'react-i18next'; import { Button, Checkbox, Modal } from '@/component-library'; import { createLogger } from '@/shared/utils/logger'; @@ -106,6 +107,16 @@ export function useDeepReviewConsent(): DeepReviewConsentControls { {t('deepReviewConsent.costLabel', { defaultValue: 'Higher token usage' })}

{t('deepReviewConsent.cost')}

+

+ {(() => { + const est = estimateTokenConsumption(5); + return t('deepReviewConsent.estimatedTokens', { + min: formatTokenCount(est.min), + max: formatTokenCount(est.max), + defaultValue: 'Estimated: {{min}} - {{max}} tokens', + }); + })()} +

diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss index f5616065d..870734e4a 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss @@ -394,6 +394,224 @@ flex-shrink: 0; } + /* Progress tracking */ + &__progress { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 10px; + border-radius: 6px; + background: var(--element-bg-subtle); + font-size: 12px; + } + + &__progress-text { + color: var(--color-text-secondary); + font-weight: 500; + } + + &__elapsed { + margin-left: auto; + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; + } + + /* Partial results */ + &__partial-summary { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + border-radius: 8px; + background: var(--element-bg-subtle); + font-size: 12px; + } + + &__partial-count { + color: var(--color-text-secondary); + } + + &__partial-link { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: auto; + color: var(--color-accent-500, #60a5fa); + background: none; + border: none; + cursor: pointer; + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + transition: background 0.2s; + + &:hover { + background: color-mix(in srgb, var(--color-accent-500, #60a5fa) 10%, transparent); + } + } + + &__partial-detail { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px 12px; + border-radius: 8px; + background: var(--color-bg-primary); + border: 1px solid var(--border-base); + } + + &__partial-section { + font-size: 12px; + color: var(--color-text-secondary); + } + + &__partial-section-title { + font-weight: 500; + } + + /* Error attribution */ + &__attribution { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px 12px; + border-radius: 8px; + font-size: 12px; + + &--warning { + background: color-mix(in srgb, var(--color-warning, #f59e0b) 8%, var(--color-bg-secondary)); + border: 1px solid color-mix(in srgb, var(--color-warning, #f59e0b) 18%, transparent); + } + + &--error { + background: color-mix(in srgb, var(--color-error, #ef4444) 8%, var(--color-bg-secondary)); + border: 1px solid color-mix(in srgb, var(--color-error, #ef4444) 18%, transparent); + } + } + + &__attribution-message { + color: var(--color-text-secondary); + line-height: 1.5; + } + + &__attribution-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + } + + /* Recovery plan */ + &__recovery-plan { + display: flex; + flex-direction: column; + gap: 6px; + } + + &__recovery-plan-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 6px; + background: transparent; + border: 1px solid transparent; + color: var(--color-text-muted); + font-size: 12px; + cursor: pointer; + transition: background 0.2s, border-color 0.2s, color 0.2s; + width: fit-content; + + &:hover { + background: var(--element-bg-subtle); + border-color: var(--border-base); + color: var(--color-text-primary); + } + } + + &__recovery-plan-detail { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 12px; + border-radius: 8px; + background: var(--color-bg-primary); + border: 1px solid var(--border-base); + } + + &__recovery-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--color-text-secondary); + } + + &__recovery-icon { + &--preserve { + color: var(--color-success, #22c55e); + } + + &--rerun { + color: var(--color-accent-500, #60a5fa); + } + + &--skip { + color: var(--color-text-muted); + } + } + + /* Degradation options */ + &__degradation { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px 12px; + border-radius: 8px; + background: var(--color-bg-primary); + border: 1px solid var(--border-base); + } + + &__degradation-title { + font-size: 12px; + font-weight: 600; + color: var(--color-text-primary); + } + + &__degradation-option { + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px 10px; + border-radius: 6px; + background: var(--element-bg-subtle); + border: 1px solid transparent; + cursor: pointer; + text-align: left; + transition: background 0.2s, border-color 0.2s; + + &:hover:not(:disabled) { + background: var(--element-bg-base); + border-color: var(--border-base); + } + + &:disabled { + opacity: 0.45; + cursor: not-allowed; + } + } + + &__degradation-label { + font-size: 12px; + font-weight: 500; + color: var(--color-text-primary); + } + + &__degradation-desc { + font-size: 11px; + color: var(--color-text-muted); + } + /* Animations */ @keyframes deep-review-action-bar-spin { from { diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx index 17cd23fb4..3e85f4df1 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.test.tsx @@ -8,6 +8,10 @@ const eventBusEmitMock = vi.hoisted(() => vi.fn()); const confirmWarningMock = vi.hoisted(() => vi.fn()); vi.mock('react-i18next', () => ({ + initReactI18next: { + type: '3rdParty', + init: vi.fn(), + }, useTranslation: () => ({ t: (_key: string, options?: { defaultValue?: string }) => options?.defaultValue ?? _key, }), @@ -58,12 +62,50 @@ vi.mock('@/component-library/components/ConfirmDialog/confirmService', () => ({ vi.mock('@/shared/notification-system', () => ({ notificationService: { error: vi.fn(), + info: vi.fn(), + success: vi.fn(), }, })); vi.mock('@/shared/utils/logger', () => ({ createLogger: () => ({ error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + }), +})); + +vi.mock('../../store/FlowChatStore', () => ({ + flowChatStore: { + getState: () => ({ + sessions: new Map(), + activeSessionId: null, + }), + subscribe: () => () => {}, + }, +})); + +vi.mock('../../utils/deepReviewExperience', () => ({ + aggregateReviewerProgress: () => [], + buildReviewerProgressSummary: () => null, + extractPartialReviewData: () => null, + buildErrorAttribution: () => null, + buildRecoveryPlan: () => null, + evaluateDegradationOptions: () => [], +})); + +vi.mock('../../services/DeepReviewContinuationService', () => ({ + continueDeepReviewSession: vi.fn(), +})); + +vi.mock('@/shared/ai-errors/aiErrorPresenter', () => ({ + getAiErrorPresentation: () => ({ + category: 'network', + titleKey: 'test', + messageKey: 'test', + diagnostics: 'test diagnostics', + actions: [], }), })); diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.tsx b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.tsx index 6db609a55..6dbc3a5f7 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.tsx +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CheckCircle, @@ -13,6 +13,9 @@ import { Play, Copy, Info, + SkipForward, + RotateCcw, + Eye, } from 'lucide-react'; import { Button, Checkbox, Tooltip } from '@/component-library'; import { useReviewActionBarStore, type ReviewActionPhase } from '../../store/deepReviewActionBarStore'; @@ -26,6 +29,15 @@ import { notificationService } from '@/shared/notification-system'; import { createLogger } from '@/shared/utils/logger'; import { getAiErrorPresentation } from '@/shared/ai-errors/aiErrorPresenter'; import { confirmWarning } from '@/component-library/components/ConfirmDialog/confirmService'; +import { + aggregateReviewerProgress, + buildErrorAttribution, + buildRecoveryPlan, + buildReviewerProgressSummary, + evaluateDegradationOptions, + extractPartialReviewData, +} from '../../utils/deepReviewExperience'; +import { flowChatStore } from '../../store/FlowChatStore'; import './DeepReviewActionBar.scss'; const log = createLogger('DeepReviewActionBar'); @@ -84,6 +96,10 @@ export const ReviewActionBar: React.FC = () => { const [showCustomInput, setShowCustomInput] = useState(false); const [showRemediationList, setShowRemediationList] = useState(true); + const [showPartialResults, setShowPartialResults] = useState(false); + const [showRecoveryPlan, setShowRecoveryPlan] = useState(false); + const [elapsedMs, setElapsedMs] = useState(0); + const [longRunningNotified, setLongRunningNotified] = useState(false); const selectedCount = selectedRemediationIds.size; const totalCount = remediationItems.length; @@ -92,6 +108,70 @@ export const ReviewActionBar: React.FC = () => { const isDeepReview = reviewMode === 'deep'; const hasInterruption = isDeepReview && Boolean(interruption); + // ---- progress tracking ---- + const sessions = flowChatStore.getState().sessions; + const childSession = useMemo(() => { + if (!childSessionId) return null; + return Array.from(sessions.values()).find((s) => s.sessionId === childSessionId) ?? null; + }, [sessions, childSessionId]); + + const reviewerProgress = useMemo(() => { + if (!childSession || childSession.sessionKind !== 'deep_review') return []; + return aggregateReviewerProgress(childSession); + }, [childSession]); + + const progressSummary = useMemo(() => { + if (reviewerProgress.length === 0) return null; + return buildReviewerProgressSummary(reviewerProgress); + }, [reviewerProgress]); + + const partialResults = useMemo(() => { + if (!childSession || childSession.sessionKind !== 'deep_review') return null; + return extractPartialReviewData(childSession); + }, [childSession]); + + // ---- error attribution ---- + const errorAttribution = useMemo(() => { + if (!interruption) return null; + return buildErrorAttribution(interruption); + }, [interruption]); + + // ---- recovery plan ---- + const recoveryPlan = useMemo(() => { + if (!interruption) return null; + return buildRecoveryPlan(interruption); + }, [interruption]); + + // ---- degradation options ---- + const degradationOptions = useMemo(() => { + if (!interruption) return []; + return evaluateDegradationOptions(interruption); + }, [interruption]); + + // ---- long-running hint ---- + useEffect(() => { + if (phase !== 'fix_running' && phase !== 'resume_running') { + setElapsedMs(0); + setLongRunningNotified(false); + return; + } + const startTime = Date.now(); + const interval = setInterval(() => { + const elapsed = Date.now() - startTime; + setElapsedMs(elapsed); + if (elapsed > 3 * 60 * 1000 && !longRunningNotified) { + setLongRunningNotified(true); + notificationService.info( + t('deepReviewActionBar.longRunningHint', { + defaultValue: 'Review is still running. This may take a few more minutes.', + }), + { duration: 5000 }, + ); + } + }, 1000); + return () => clearInterval(interval); + }, [phase, longRunningNotified, t]); + const phaseConfig = PHASE_CONFIG[phase]; const PhaseIcon = phaseConfig.icon; @@ -287,6 +367,40 @@ export const ReviewActionBar: React.FC = () => { await handleStartFixing(false, remainingSet); }, [reviewData, childSessionId, remainingFixIds, store, handleStartFixing]); + const handleRetryResume = useCallback(async () => { + if (!interruption) return; + await handleContinueReview(); + }, [interruption, handleContinueReview]); + + const handleRetryWithDifferentModel = useCallback(async () => { + if (!interruption) return; + globalEventBus.emit('settings:open', { tab: 'models' }); + }, [interruption]); + + const handleViewPartialResults = useCallback(() => { + setShowPartialResults(true); + }, []); + + const handleDegradationAction = useCallback((type: string) => { + if (type === 'view_partial') { + setShowPartialResults(true); + } else if (type === 'reduce_reviewers') { + notificationService.info( + t('deepReviewActionBar.degradation.reduceReviewersPending', { + defaultValue: 'Reduced reviewer mode will be supported in a future update.', + }), + { duration: 3000 }, + ); + } else if (type === 'compress_context') { + notificationService.info( + t('deepReviewActionBar.degradation.compressContextPending', { + defaultValue: 'Context compression will be supported in a future update.', + }), + { duration: 3000 }, + ); + } + }, [t]); + const handleCopyDiagnostics = useCallback(async () => { const detail = interruption?.errorDetail; if (!detail) return; @@ -351,6 +465,34 @@ export const ReviewActionBar: React.FC = () => { }, [interruption, t]); const phaseTitle = useMemo(() => { + if (hasInterruption && interruption?.errorDetail && errorAttribution) { + const categoryLabel = t(errorAttribution.title, { defaultValue: errorAttribution.category }); + if (phase === 'review_interrupted') { + return t('deepReviewActionBar.reviewInterruptedWithReason', { + reason: categoryLabel, + defaultValue: `Deep review interrupted: ${categoryLabel}`, + }); + } + if (phase === 'resume_blocked') { + return t('deepReviewActionBar.resumeBlockedWithReason', { + reason: categoryLabel, + defaultValue: `Cannot continue: ${categoryLabel}`, + }); + } + if (phase === 'resume_failed') { + return t('deepReviewActionBar.resumeFailedWithReason', { + reason: categoryLabel, + defaultValue: `Continue failed: ${categoryLabel}`, + }); + } + if (phase === 'review_error') { + return t('deepReviewActionBar.reviewErrorWithReason', { + reason: categoryLabel, + defaultValue: `Review error: ${categoryLabel}`, + }); + } + } + switch (phase) { case 'review_completed': return t(isDeepReview ? 'reviewActionBar.reviewCompletedDeep' : 'reviewActionBar.reviewCompletedStandard', { @@ -395,7 +537,7 @@ export const ReviewActionBar: React.FC = () => { default: return ''; } - }, [phase, isDeepReview, t]); + }, [phase, isDeepReview, t, hasInterruption, interruption, errorAttribution]); if (dismissed || phase === 'idle' || !childSessionId) { return null; @@ -428,6 +570,189 @@ export const ReviewActionBar: React.FC = () => { )}
+ {/* Running progress */} + {(phase === 'fix_running' || phase === 'resume_running') && progressSummary && ( +
+ + {progressSummary.text} + + {elapsedMs > 0 && ( + + {t('deepReviewActionBar.elapsedTime', { + time: formatElapsedTime(elapsedMs), + defaultValue: `Running for ${formatElapsedTime(elapsedMs)}`, + })} + + )} +
+ )} + + {/* Partial results summary on interruption */} + {hasInterruption && progressSummary && progressSummary.completed > 0 && ( +
+ + {t('deepReviewActionBar.partialResultsDescription', { + completed: progressSummary.completed, + total: progressSummary.total, + defaultValue: '{{completed}}/{{total}} reviewers completed', + })} + + +
+ )} + + {/* Partial results detail */} + {showPartialResults && partialResults && ( +
+ {partialResults.completedIssues.length > 0 && ( +
+ + {t('deepReviewActionBar.partialIssues', { + count: partialResults.completedIssues.length, + defaultValue: '{{count}} issues found', + })} + +
+ )} + {partialResults.completedRemediationItems.length > 0 && ( +
+ + {t('deepReviewActionBar.partialRemediationItems', { + count: partialResults.completedRemediationItems.length, + defaultValue: '{{count}} remediation items', + })} + +
+ )} +
+ )} + + {/* Error attribution card */} + {hasInterruption && errorAttribution && ( +
+ + {t(errorAttribution.description, { defaultValue: '' })} + + {errorAttribution.actions.length > 0 && ( +
+ {errorAttribution.actions.map((action) => ( + + ))} +
+ )} +
+ )} + + {/* Recovery plan preview */} + {hasInterruption && recoveryPlan && ( +
+ + {showRecoveryPlan && ( +
+ {recoveryPlan.willPreserve.length > 0 && ( +
+ + + {t('deepReviewActionBar.recoveryPreserve', { + count: recoveryPlan.willPreserve.length, + defaultValue: '{{count}} completed reviewers will be preserved', + })} + +
+ )} + {recoveryPlan.willRerun.length > 0 && ( +
+ + + {t('deepReviewActionBar.recoveryRerun', { + count: recoveryPlan.willRerun.length, + defaultValue: '{{count}} reviewers will be rerun', + })} + +
+ )} + {recoveryPlan.willSkip.length > 0 && ( +
+ + + {t('deepReviewActionBar.recoverySkip', { + count: recoveryPlan.willSkip.length, + defaultValue: '{{count}} reviewers will be skipped', + })} + +
+ )} +
+ )} +
+ )} + + {/* Context overflow degradation options */} + {hasInterruption && interruption?.errorDetail?.category === 'context_overflow' && ( +
+ + {t('deepReviewActionBar.contextOverflowTitle', { + defaultValue: 'Context limit reached. Choose how to proceed:', + })} + + {degradationOptions.map((option) => ( + + ))} +
+ )} + {/* Remediation selection (only when review completed and has items) */} {phase === 'review_completed' && remediationItems.length > 0 && (
@@ -675,7 +1000,38 @@ export const ReviewActionBar: React.FC = () => { )} - {(phase === 'fix_completed' || phase === 'fix_failed' || phase === 'fix_timeout' || phase === 'review_error' || phase === 'resume_failed') && ( + {phase === 'resume_failed' && ( + <> + + + {partialResults?.hasPartialResults && ( + + )} + + )} + + {(phase === 'fix_completed' || phase === 'fix_failed' || phase === 'fix_timeout' || phase === 'review_error') && (