diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index a70fb054..b749139a 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -16,6 +16,7 @@ import { useSnippetsStore } from '@/stores/useSnippetsStore'; import { appendInlineComments } from '@/lib/messages/inlineComments'; import { renderMagicPrompt } from '@/lib/magicPrompts'; import { startReviewFlow } from '@/lib/reviewFlow'; +import { ReviewFlowDialog, type ReviewFlowExecution } from '@/components/session/ReviewFlowDialog'; import { AttachedFilesList, AttachedVSCodeFileChips, ActiveEditorFileSuggestion } from './FileAttachment'; import ToolOutputDialog from './message/ToolOutputDialog'; import type { ToolPopupContent } from './message/types'; @@ -1014,6 +1015,8 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const projects = useProjectsStore((state) => state.projects); const activeProjectId = useProjectsStore((state) => state.activeProjectId); const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly); + const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false); + const [reviewFlowSubmitting, setReviewFlowSubmitting] = React.useState(false); const currentProviderId = useConfigStore((state) => state.currentProviderId); const currentModelId = useConfigStore((state) => state.currentModelId); @@ -1062,6 +1065,35 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo setImagePreviewOpen(open); }, [setImagePreviewOpen]); + const handleStartReviewFlow = React.useCallback(async (execution: ReviewFlowExecution) => { + if (!currentSessionId) return; + const directory = useSessionUIStore.getState().getDirectoryForSession(currentSessionId) || currentDirectory || ''; + if (!directory) { + toast.error(t('diffView.reviewDialog.toast.noSessionDirectory')); + return; + } + + setReviewFlowSubmitting(true); + try { + await startReviewFlow({ + originalSessionID: currentSessionId, + directory, + providerID: execution.providerID, + modelID: execution.modelID, + agent: execution.agent || undefined, + variant: execution.variant || undefined, + generateHandoff: execution.generateHandoff, + returnAfterHandoffRequest: execution.generateHandoff, + }); + setReviewDialogOpen(false); + } catch (error) { + console.error('[review-flow] failed to start review flow', error); + toast.error(error instanceof Error ? error.message : t('diffView.reviewDialog.toast.startFailed')); + } finally { + setReviewFlowSubmitting(false); + } + }, [currentSessionId, currentDirectory, t]); + const isDesktopExpanded = isExpandedInput && !isMobile; const chatInputRadius = 'var(--radius-xl)'; const useCompactChatPlaceholder = isMobile || isNarrowComposer; @@ -1940,24 +1972,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo return; } else if (commandName === 'handoff-review' && currentSessionId && !isMobile && !isVSCodeRuntime()) { - try { - const directory = useSessionUIStore.getState().getDirectoryForSession(currentSessionId) || currentDirectory || ''; - if (!directory) { - throw new Error('Session directory is unavailable'); - } - await startReviewFlow({ - originalSessionID: currentSessionId, - directory, - providerID: providerIdToSend, - modelID: modelIdToSend, - agent: agentNameToSend, - variant: variantToSend, - agentMentionName, - }); - scrollToBottom?.(); - } catch (error) { - console.error('[review-flow] failed to start review flow', error); - } + setReviewDialogOpen(true); return; } else if (commandName === 'plan-feature' && (currentSessionId || newSessionDraftOpen)) { @@ -4502,6 +4517,13 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo setLinkedIssue(null); }} /> + void; + projectDirectory: string | null; + submitting?: boolean; + onConfirm: (execution: ReviewFlowExecution) => Promise | void; +}; + +const getInitialExecution = (params: { + providerID: string; + modelID: string; + variant: string; + agent: string; +}): ReviewFlowExecution => ({ + providerID: params.providerID, + modelID: params.modelID, + variant: params.variant, + agent: params.agent, + generateHandoff: true, +}); + +export function ReviewFlowDialog({ + open, + onOpenChange, + projectDirectory, + submitting = false, + onConfirm, +}: ReviewFlowDialogProps) { + const { t } = useI18n(); + const loadProviders = useConfigStore((state) => state.loadProviders); + const loadConfigAgents = useConfigStore((state) => state.loadAgents); + const loadAgentsStoreAgents = useAgentsStore((state) => state.loadAgents); + const providers = useConfigStore((state) => state.providers); + const currentProviderID = useConfigStore((state) => state.currentProviderId); + const currentModelID = useConfigStore((state) => state.currentModelId); + const currentVariant = useConfigStore((state) => state.currentVariant || ''); + const currentAgentName = useConfigStore((state) => state.currentAgentName || ''); + + const [execution, setExecution] = React.useState(() => getInitialExecution({ + providerID: currentProviderID, + modelID: currentModelID, + variant: currentVariant, + agent: currentAgentName, + })); + + React.useEffect(() => { + if (!open) return; + void loadProviders({ directory: projectDirectory, source: 'reviewFlowDialog' }); + void loadConfigAgents({ directory: projectDirectory }); + void loadAgentsStoreAgents(); + }, [open, loadProviders, loadConfigAgents, loadAgentsStoreAgents, projectDirectory]); + + React.useEffect(() => { + if (!open) return; + setExecution(getInitialExecution({ + providerID: currentProviderID, + modelID: currentModelID, + variant: currentVariant, + agent: currentAgentName, + })); + }, [open, currentProviderID, currentModelID, currentVariant, currentAgentName]); + + React.useEffect(() => { + if (!open || providers.length === 0) return; + + const provider = providers.find((item) => item.id === execution.providerID) ?? providers[0]; + const models = Array.isArray(provider?.models) ? provider.models : []; + const hasModel = models.some((item) => item.id === execution.modelID); + const fallbackModelID = models[0]?.id ?? ''; + + if (provider?.id === execution.providerID && hasModel) return; + + setExecution((prev) => ({ + ...prev, + providerID: provider?.id ?? '', + modelID: hasModel ? prev.modelID : fallbackModelID, + variant: '', + })); + }, [open, providers, execution.providerID, execution.modelID]); + + const agentFilter = React.useCallback((agent: { mode?: string }) => isPrimaryMode(agent.mode), []); + + const variantOptions = React.useMemo(() => { + const provider = providers.find((item) => item.id === execution.providerID); + const model = provider?.models?.find((item) => item.id === execution.modelID) as { variants?: Record } | undefined; + return model?.variants ? Object.keys(model.variants) : []; + }, [providers, execution.providerID, execution.modelID]); + + const hasVariantOptions = variantOptions.length > 0; + + React.useEffect(() => { + if (hasVariantOptions || !execution.variant) return; + setExecution((prev) => ({ ...prev, variant: '' })); + }, [hasVariantOptions, execution.variant]); + + const canConfirm = execution.providerID.trim().length > 0 && execution.modelID.trim().length > 0; + + const handleSubmit = React.useCallback(() => { + if (!canConfirm || submitting) return; + void onConfirm(execution); + }, [canConfirm, submitting, onConfirm, execution]); + + React.useEffect(() => { + if (!open) return; + const onKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { + event.preventDefault(); + handleSubmit(); + } + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [open, handleSubmit]); + + return ( + { if (!submitting) onOpenChange(nextOpen); }}> + + + {t('diffView.reviewDialog.title')} + + {t('diffView.reviewDialog.description')} + + + +
+
+ {t('diffView.reviewDialog.info')} +
+ + + +
+ {t('chat.modelControls.model')} + { + setExecution((prev) => ({ ...prev, providerID, modelID, variant: '' })); + }} + /> +
+ +
+ {t('sessions.scheduledTasks.editor.thinkingLevel.label')} + setExecution((prev) => ({ ...prev, variant }))} + /> +
+ +
+ {t('sessions.scheduledTasks.editor.agent.label')} + setExecution((prev) => ({ ...prev, agent }))} + /> +
+
+ + + + + +
+
+ ); +} diff --git a/packages/ui/src/components/views/DiffView.tsx b/packages/ui/src/components/views/DiffView.tsx index 8ffcfbe6..e8974b19 100644 --- a/packages/ui/src/components/views/DiffView.tsx +++ b/packages/ui/src/components/views/DiffView.tsx @@ -25,6 +25,7 @@ import { getLanguageFromExtension, isImageFile } from '@/lib/toolHelpers'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { DiffViewToggle } from '@/components/chat/message/DiffViewToggle'; import type { DiffViewMode } from '@/components/chat/message/types'; +import { ReviewFlowDialog, type ReviewFlowExecution } from '@/components/session/ReviewFlowDialog'; import { PierreDiffViewer } from './PierreDiffViewer'; import { useDeviceInfo } from '@/lib/device'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; @@ -35,6 +36,9 @@ import { sessionEvents } from '@/lib/sessionEvents'; import { useI18n } from '@/lib/i18n'; import type { I18nKey } from '@/lib/i18n/store'; import { fileDiffFromPatch } from '@/lib/diff/patchFileDiff'; +import { isVSCodeRuntime } from '@/lib/desktop'; +import { startReviewFlow } from '@/lib/reviewFlow'; +import { useSessionUIStore } from '@/sync/session-ui-store'; import type { FileDiffMetadata } from '@pierre/diffs'; // Minimum width for side-by-side diff view (px) @@ -948,6 +952,8 @@ export const DiffView: React.FC = ({ const [mountedStackedFiles, setMountedStackedFiles] = React.useState>(() => new Set()); const [loadFullFiles, setLoadFullFiles] = React.useState(false); const [scrollRequestNonce, setScrollRequestNonce] = React.useState(0); + const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false); + const [reviewFlowSubmitting, setReviewFlowSubmitting] = React.useState(false); const pendingDiffFile = useUIStore((state) => state.pendingDiffFile); const pendingDiffStaged = useUIStore((state) => state.pendingDiffStaged); @@ -958,11 +964,13 @@ export const DiffView: React.FC = ({ const diffWrapLinesStore = useUIStore((state) => state.diffWrapLines); const setDiffWrapLines = useUIStore((state) => state.setDiffWrapLines); const openContextFileAtLine = useUIStore((state) => state.openContextFileAtLine); + const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const diffWrapLines = diffWrapLinesStore; const forcedStaged = diffScope === 'staged' ? true : diffScope === 'working' ? false : null; const activeDiffStaged = forcedStaged ?? displayFileStaged; const isMobileLayout = isMobile || screenWidth <= 768; + const showReviewAction = Boolean(currentSessionId) && !isMobileLayout && !isVSCodeRuntime(); const showFileSidebar = !hideStackedFileSidebar && !isMobileLayout && screenWidth >= 1024; const diffScrollRef = React.useRef(null); const fileSectionRefs = React.useRef(new Map()); @@ -1270,6 +1278,35 @@ export const DiffView: React.FC = ({ queueVisibleStackedFilesSync(); }, [cancelPendingScrollAlignment, changedFiles, queueVisibleStackedFilesSync]); + const handleStartReviewFlow = React.useCallback(async (execution: ReviewFlowExecution) => { + if (!currentSessionId) return; + const directory = useSessionUIStore.getState().getDirectoryForSession(currentSessionId) || effectiveDirectory || ''; + if (!directory) { + toast.error(t('diffView.reviewDialog.toast.noSessionDirectory')); + return; + } + + setReviewFlowSubmitting(true); + try { + await startReviewFlow({ + originalSessionID: currentSessionId, + directory, + providerID: execution.providerID, + modelID: execution.modelID, + agent: execution.agent || undefined, + variant: execution.variant || undefined, + generateHandoff: execution.generateHandoff, + returnAfterHandoffRequest: execution.generateHandoff, + }); + setReviewDialogOpen(false); + } catch (error) { + console.error('[review-flow] failed to start review flow', error); + toast.error(error instanceof Error ? error.message : t('diffView.reviewDialog.toast.startFailed')); + } finally { + setReviewFlowSubmitting(false); + } + }, [currentSessionId, effectiveDirectory, t]); + const scrollToFile = React.useCallback((path: string): boolean => { const node = fileSectionRefs.current.get(path); const scrollRoot = diffScrollRef.current; @@ -1582,6 +1619,25 @@ export const DiffView: React.FC = ({ )} + {changedFiles.length > 0 && showReviewAction && ( + + )} {changedFiles.length > 0 && ( @@ -1626,6 +1682,14 @@ export const DiffView: React.FC = ({ )} + + {renderContent()} ); diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index e2652df5..b40f388c 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -860,10 +860,12 @@ html:not(.dark) .chat-scroll { /* Diff toolbar: drop low-priority labels only when the context panel is genuinely tight. */ @container diff-toolbar (max-width: 28rem) { .diff-toolbar__scope-count, + .diff-toolbar__review-label, .diff-toolbar__expand-label { display: none; } + .diff-toolbar__review-button, .diff-toolbar__expand-button { padding-inline: 0.5rem; } diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 78f8ed1a..0559d1c7 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -1215,6 +1215,17 @@ export const dict = { 'diffView.actions.enableLineWrap': 'Enable line wrap', 'diffView.actions.openFileInEditorAtChange': 'Open this file in editor at change', 'diffView.actions.openFileAtFirstChangedLine': 'Open this file at first changed line', + 'diffView.actions.review': 'Review', + 'diffView.actions.reviewAria': 'Review changes', + 'diffView.reviewDialog.title': 'Review changes', + 'diffView.reviewDialog.description': 'Start a separate review session for the current changes.', + 'diffView.reviewDialog.generateHandoff': 'Generate handoff', + 'diffView.reviewDialog.info': 'This flow works best when started from the session where the changes were implemented.', + 'diffView.reviewDialog.actions.cancel': 'Cancel', + 'diffView.reviewDialog.actions.start': 'Review', + 'diffView.reviewDialog.actions.starting': 'Starting...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'Session directory is unavailable', + 'diffView.reviewDialog.toast.startFailed': 'Failed to start review flow', 'diffView.hunk.label': 'Hunks', 'diffView.hunk.stage': 'Stage', 'diffView.hunk.unstage': 'Unstage', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 931e3ef0..c0fd6d05 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1181,6 +1181,17 @@ export const dict: Record = { "diffView.actions.enableLineWrap": "Activar ajuste de línea", "diffView.actions.openFileInEditorAtChange": "Abrir este archivo en el editor en el cambio", "diffView.actions.openFileAtFirstChangedLine": "Abrir este archivo en la primera línea modificada", + 'diffView.actions.review': 'Revisar', + 'diffView.actions.reviewAria': 'Revisar cambios', + 'diffView.reviewDialog.title': 'Revisar cambios', + 'diffView.reviewDialog.description': 'Inicia una sesión de revisión separada para los cambios actuales.', + 'diffView.reviewDialog.generateHandoff': 'Generar handoff', + 'diffView.reviewDialog.info': 'Este flujo funciona mejor si se inicia desde la sesión donde se implementaron los cambios.', + 'diffView.reviewDialog.actions.cancel': 'Cancelar', + 'diffView.reviewDialog.actions.start': 'Revisar', + 'diffView.reviewDialog.actions.starting': 'Iniciando...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'El directorio de la sesión no está disponible', + 'diffView.reviewDialog.toast.startFailed': 'No se pudo iniciar el flujo de revisión', "diffView.hunk.label": "Fragmentos", "diffView.hunk.stage": "Preparar", "diffView.hunk.unstage": "Quitar", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 64221666..69411df4 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1088,6 +1088,17 @@ export const dict = { 'diffView.actions.enableLineWrap': 'Activer le retour à la ligne', 'diffView.actions.openFileInEditorAtChange': 'Ouvrez ce fichier dans l\'éditeur lors du changement', 'diffView.actions.openFileAtFirstChangedLine': 'Ouvrez ce fichier à la première ligne modifiée', + 'diffView.actions.review': 'Revoir', + 'diffView.actions.reviewAria': 'Revoir les changements', + 'diffView.reviewDialog.title': 'Revoir les changements', + 'diffView.reviewDialog.description': 'Démarre une session de revue séparée pour les changements actuels.', + 'diffView.reviewDialog.generateHandoff': 'Générer un handoff', + 'diffView.reviewDialog.info': 'Ce flux fonctionne mieux lorsqu’il est lancé depuis la session où les changements ont été implémentés.', + 'diffView.reviewDialog.actions.cancel': 'Annuler', + 'diffView.reviewDialog.actions.start': 'Revoir', + 'diffView.reviewDialog.actions.starting': 'Démarrage...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'Le dossier de session est indisponible', + 'diffView.reviewDialog.toast.startFailed': 'Impossible de démarrer le flux de revue', 'diffView.hunk.label': 'Sections', 'diffView.hunk.stage': 'Préparer', 'diffView.hunk.unstage': 'Retirer', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 07b33e96..9167e702 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1218,6 +1218,17 @@ export const dict: Record = { 'diffView.actions.enableLineWrap': '줄 바꿈 켜기', 'diffView.actions.openFileInEditorAtChange': '변경 위치에서 이 파일을 에디터로 열기', 'diffView.actions.openFileAtFirstChangedLine': '첫 변경 줄에서 이 파일 열기', + 'diffView.actions.review': '리뷰', + 'diffView.actions.reviewAria': '변경 사항 리뷰', + 'diffView.reviewDialog.title': '변경 사항 리뷰', + 'diffView.reviewDialog.description': '현재 변경 사항을 위한 별도 리뷰 세션을 시작합니다.', + 'diffView.reviewDialog.generateHandoff': '핸드오프 생성', + 'diffView.reviewDialog.info': '이 흐름은 변경 사항을 구현한 세션에서 시작할 때 가장 잘 작동합니다.', + 'diffView.reviewDialog.actions.cancel': '취소', + 'diffView.reviewDialog.actions.start': '리뷰', + 'diffView.reviewDialog.actions.starting': '시작 중...', + 'diffView.reviewDialog.toast.noSessionDirectory': '세션 디렉터리를 사용할 수 없습니다', + 'diffView.reviewDialog.toast.startFailed': '리뷰 흐름을 시작하지 못했습니다', 'diffView.hunk.label': '허크', 'diffView.hunk.stage': '스테이지', 'diffView.hunk.unstage': '스테이지 해제', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 91abcbfa..c4b5f2a9 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1415,6 +1415,17 @@ export const dict: Record = { 'diffView.actions.loadFullFiles': 'Wczytaj pełne pliki', 'diffView.actions.disableFullFiles': 'Nie wczytuj pełnych plików', 'diffView.actions.openFileAtFirstChangedLine': 'Otwórz plik na pierwszej zmienionej linii', + 'diffView.actions.review': 'Review', + 'diffView.actions.reviewAria': 'Przejrzyj zmiany', + 'diffView.reviewDialog.title': 'Przegląd zmian', + 'diffView.reviewDialog.description': 'Uruchom osobną sesję review dla bieżących zmian.', + 'diffView.reviewDialog.generateHandoff': 'Wygeneruj handoff', + 'diffView.reviewDialog.info': 'Ten flow działa najlepiej, gdy jest uruchomiony z sesji, w której zaimplementowano zmiany.', + 'diffView.reviewDialog.actions.cancel': 'Anuluj', + 'diffView.reviewDialog.actions.start': 'Review', + 'diffView.reviewDialog.actions.starting': 'Uruchamianie...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'Katalog sesji jest niedostępny', + 'diffView.reviewDialog.toast.startFailed': 'Nie udało się uruchomić flow review', 'diffView.hunk.label': 'Fragmenty', 'diffView.hunk.stage': 'Przygotuj', 'diffView.hunk.unstage': 'Cofnij', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index db35d4f9..b69e680c 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1181,6 +1181,17 @@ export const dict: Record = { "diffView.actions.enableLineWrap": "Ativar ajuste de linha", "diffView.actions.openFileInEditorAtChange": "Abrir este arquivo no editor nesta alteração", "diffView.actions.openFileAtFirstChangedLine": "Abrir este arquivo na primeira linha alterada", + 'diffView.actions.review': 'Revisar', + 'diffView.actions.reviewAria': 'Revisar alterações', + 'diffView.reviewDialog.title': 'Revisar alterações', + 'diffView.reviewDialog.description': 'Inicia uma sessão de revisão separada para as alterações atuais.', + 'diffView.reviewDialog.generateHandoff': 'Gerar handoff', + 'diffView.reviewDialog.info': 'Este fluxo funciona melhor quando iniciado na sessão em que as alterações foram implementadas.', + 'diffView.reviewDialog.actions.cancel': 'Cancelar', + 'diffView.reviewDialog.actions.start': 'Revisar', + 'diffView.reviewDialog.actions.starting': 'Iniciando...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'O diretório da sessão está indisponível', + 'diffView.reviewDialog.toast.startFailed': 'Falha ao iniciar o fluxo de revisão', "diffView.hunk.label": "Trechos", "diffView.hunk.stage": "Preparar", "diffView.hunk.unstage": "Remover", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index c0bec51b..4ef8d45e 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1181,6 +1181,17 @@ export const dict: Record = { "diffView.actions.enableLineWrap": "Увімкнути перенос рядків", "diffView.actions.openFileInEditorAtChange": "Відкрити цей файл у редакторі на зміні", "diffView.actions.openFileAtFirstChangedLine": "Відкрити цей файл у першому зміненому рядку", + 'diffView.actions.review': 'Ревʼю', + 'diffView.actions.reviewAria': 'Поревʼювати зміни', + 'diffView.reviewDialog.title': 'Ревʼю змін', + 'diffView.reviewDialog.description': 'Запустити окрему сесію ревʼю для поточних змін.', + 'diffView.reviewDialog.generateHandoff': 'Згенерувати handoff', + 'diffView.reviewDialog.info': 'Цей flow найкраще працює, якщо запускати його із сесії, де були імплементовані зміни.', + 'diffView.reviewDialog.actions.cancel': 'Скасувати', + 'diffView.reviewDialog.actions.start': 'Ревʼю', + 'diffView.reviewDialog.actions.starting': 'Запуск...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'Директорія сесії недоступна', + 'diffView.reviewDialog.toast.startFailed': 'Не вдалося запустити review flow', "diffView.hunk.label": "Шматки", "diffView.hunk.stage": "Додати", "diffView.hunk.unstage": "Прибрати", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index f02b0d90..02729520 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1181,6 +1181,17 @@ export const dict: Record = { 'diffView.actions.enableLineWrap': '开启自动换行', 'diffView.actions.openFileInEditorAtChange': '在编辑器中打开此文件并定位变更', 'diffView.actions.openFileAtFirstChangedLine': '在首个变更行打开此文件', + 'diffView.actions.review': 'Review', + 'diffView.actions.reviewAria': 'Review changes', + 'diffView.reviewDialog.title': 'Review changes', + 'diffView.reviewDialog.description': 'Start a separate review session for the current changes.', + 'diffView.reviewDialog.generateHandoff': 'Generate handoff', + 'diffView.reviewDialog.info': 'This flow works best if started in the session where the changes were implemented.', + 'diffView.reviewDialog.actions.cancel': 'Cancel', + 'diffView.reviewDialog.actions.start': 'Review', + 'diffView.reviewDialog.actions.starting': 'Starting...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'Session directory is unavailable', + 'diffView.reviewDialog.toast.startFailed': 'Failed to start review flow', 'diffView.hunk.label': '代码块', 'diffView.hunk.stage': '暂存', 'diffView.hunk.unstage': '取消暂存', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index ddf0c830..d9dd0a14 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -1191,6 +1191,17 @@ export const dict: Record = { 'diffView.actions.enableLineWrap': '開啟自動換行', 'diffView.actions.openFileInEditorAtChange': '在編輯器中開啟此檔案並定位變更', 'diffView.actions.openFileAtFirstChangedLine': '在首個變更行開啟此檔案', + 'diffView.actions.review': 'Review', + 'diffView.actions.reviewAria': 'Review changes', + 'diffView.reviewDialog.title': 'Review changes', + 'diffView.reviewDialog.description': 'Start a separate review session for the current changes.', + 'diffView.reviewDialog.generateHandoff': 'Generate handoff', + 'diffView.reviewDialog.info': 'This flow works best if started in the session where the changes were implemented.', + 'diffView.reviewDialog.actions.cancel': 'Cancel', + 'diffView.reviewDialog.actions.start': 'Review', + 'diffView.reviewDialog.actions.starting': 'Starting...', + 'diffView.reviewDialog.toast.noSessionDirectory': 'Session directory is unavailable', + 'diffView.reviewDialog.toast.startFailed': 'Failed to start review flow', 'diffView.hunk.label': '程式碼區塊', 'diffView.hunk.stage': '暫存', 'diffView.hunk.unstage': '取消暫存', diff --git a/packages/ui/src/lib/magicPrompts.ts b/packages/ui/src/lib/magicPrompts.ts index f8adeb37..60538697 100644 --- a/packages/ui/src/lib/magicPrompts.ts +++ b/packages/ui/src/lib/magicPrompts.ts @@ -32,6 +32,7 @@ export type MagicPromptId = | 'session.reviewHandoff.visible' | 'session.reviewHandoff.instructions' | 'session.reviewSession.visible' + | 'session.reviewSessionWithoutHandoff.visible' | 'session.reviewFeedbackToImplementer.visible' | 'session.implementationResponseToReviewer.visible' | 'session.plan.visible' @@ -650,6 +651,17 @@ Formatting: Focus on correctness, regressions, missing implementation, missing tests, and whether the implementation satisfies the stated intent. Provide concise, actionable feedback for the agent implementing the changes. {{handoff}}`, + }, + { + id: 'session.reviewSessionWithoutHandoff.visible', + title: 'Review Session Starter Prompt Without Handoff', + group: 'Session', + description: 'Visible user message sent to a generated review session when no implementation handoff is generated first.', + template: `Please review the current workspace changes. + +There is no generated implementation handoff. Infer the likely user intent from the current diff, recent session context if available, changed files, and surrounding code. Judge whether the implementation is correct for that inferred intent, and call out uncertainty explicitly when intent cannot be recovered. + +Focus on correctness, regressions, missing implementation, missing tests, and whether the implementation is the smallest maintainable way to satisfy the likely goal. Provide concise, actionable feedback for the agent implementing the changes.`, }, { id: 'session.reviewFeedbackToImplementer.visible', diff --git a/packages/ui/src/lib/reviewFlow.ts b/packages/ui/src/lib/reviewFlow.ts index 8a210533..3436adfa 100644 --- a/packages/ui/src/lib/reviewFlow.ts +++ b/packages/ui/src/lib/reviewFlow.ts @@ -35,6 +35,8 @@ type StartReviewFlowInput = SessionModelContext & { originalSessionID: string; directory: string; agentMentionName?: string; + generateHandoff?: boolean; + returnAfterHandoffRequest?: boolean; }; const isMessageCompleted = (message: Message): boolean => { @@ -207,15 +209,43 @@ const createOrReuseReviewSession = async (originalSessionID: string, directory: export const startReviewFlow = async (input: StartReviewFlowInput): Promise => { await waitForConnectionOrThrow(); - const visibleText = await renderMagicPrompt('session.reviewHandoff.visible'); - const instructionsText = await renderMagicPrompt('session.reviewHandoff.instructions'); - const startedAt = Date.now(); - await sendPlainMessage(input.originalSessionID, input.directory, visibleText, input, [ - { text: instructionsText, synthetic: true }, - ]); - const handoff = await waitForAssistantText(input.originalSessionID, input.directory, startedAt); + let reviewPrompt: string; + + if (input.generateHandoff ?? true) { + const visibleText = await renderMagicPrompt('session.reviewHandoff.visible'); + const instructionsText = await renderMagicPrompt('session.reviewHandoff.instructions'); + const startedAt = Date.now(); + await sendPlainMessage(input.originalSessionID, input.directory, visibleText, null, [ + { text: instructionsText, synthetic: true }, + ]); + + const continueFromHandoff = async (): Promise => { + const handoff = await waitForAssistantText(input.originalSessionID, input.directory, startedAt); + const handoffReviewPrompt = await renderMagicPrompt('session.reviewSession.visible', { handoff }); + const reviewSession = await createOrReuseReviewSession(input.originalSessionID, input.directory); + await sendPlainMessage(reviewSession.id, input.directory, handoffReviewPrompt, { + providerID: input.providerID, + modelID: input.modelID, + agent: input.agent, + variant: input.variant, + }); + openReviewSessionPanel(input.directory, reviewSession); + }; + + if (input.returnAfterHandoffRequest) { + void continueFromHandoff().catch((error) => { + console.error('[review-flow] failed to finish background review flow', error); + }); + return; + } + + await continueFromHandoff(); + return; + } else { + reviewPrompt = await renderMagicPrompt('session.reviewSessionWithoutHandoff.visible'); + } + const reviewSession = await createOrReuseReviewSession(input.originalSessionID, input.directory); - const reviewPrompt = await renderMagicPrompt('session.reviewSession.visible', { handoff }); await sendPlainMessage(reviewSession.id, input.directory, reviewPrompt, { providerID: input.providerID, modelID: input.modelID,