Add diff review flow dialog

This commit is contained in:
Bohdan Triapitsyn
2026-06-14 16:55:58 +03:00
parent 1762c1a289
commit 94ca3fda04
15 changed files with 468 additions and 26 deletions
+40 -18
View File
@@ -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<ChatInputProps> = ({ 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<ChatInputProps> = ({ 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<ChatInputProps> = ({ 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<ChatInputProps> = ({ onOpenSettings, scrollTo
setLinkedIssue(null);
}}
/>
<ReviewFlowDialog
open={reviewDialogOpen}
onOpenChange={setReviewDialogOpen}
projectDirectory={currentSessionDirectoryForSync ?? currentDirectory ?? null}
submitting={reviewFlowSubmitting}
onConfirm={handleStartReviewFlow}
/>
<ToolOutputDialog
popup={attachmentPreview}
onOpenChange={handleAttachmentPreviewOpenChange}
@@ -0,0 +1,213 @@
import React from 'react';
import { AgentSelector } from '@/components/sections/commands/AgentSelector';
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
import { ThinkingPill } from '@/components/session/ThinkingPill';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { isPrimaryMode } from '@/components/chat/mobileControlsUtils';
import { useI18n } from '@/lib/i18n';
import { useConfigStore } from '@/stores/useConfigStore';
import { useAgentsStore } from '@/stores/useAgentsStore';
export type ReviewFlowExecution = {
providerID: string;
modelID: string;
variant: string;
agent: string;
generateHandoff: boolean;
};
type ReviewFlowDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
projectDirectory: string | null;
submitting?: boolean;
onConfirm: (execution: ReviewFlowExecution) => Promise<void> | 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<ReviewFlowExecution>(() => 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<string, unknown> } | 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 (
<Dialog open={open} onOpenChange={(nextOpen) => { if (!submitting) onOpenChange(nextOpen); }}>
<DialogContent className="max-w-md overflow-visible">
<DialogHeader>
<DialogTitle>{t('diffView.reviewDialog.title')}</DialogTitle>
<DialogDescription>
{t('diffView.reviewDialog.description')}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
<div className="rounded-md border border-[color:color-mix(in_srgb,var(--status-info)_35%,var(--interactive-border))] bg-[color:color-mix(in_srgb,var(--status-info)_10%,var(--surface-background))] px-3 py-2 typography-meta text-foreground">
{t('diffView.reviewDialog.info')}
</div>
<label className="flex items-center gap-2 typography-ui-label text-foreground">
<Checkbox
checked={execution.generateHandoff}
onChange={(generateHandoff) => setExecution((prev) => ({ ...prev, generateHandoff }))}
disabled={submitting}
ariaLabel={t('diffView.reviewDialog.generateHandoff')}
/>
<span>{t('diffView.reviewDialog.generateHandoff')}</span>
</label>
<div className="flex min-w-0 flex-col gap-1.5">
<span className="typography-meta font-medium text-muted-foreground">{t('chat.modelControls.model')}</span>
<ModelSelector
providerId={execution.providerID}
modelId={execution.modelID}
className="max-w-[320px] justify-between"
dropdownPortalToBody
onChange={(providerID, modelID) => {
setExecution((prev) => ({ ...prev, providerID, modelID, variant: '' }));
}}
/>
</div>
<div className="flex flex-col gap-1.5">
<span className="typography-meta font-medium text-muted-foreground">{t('sessions.scheduledTasks.editor.thinkingLevel.label')}</span>
<ThinkingPill
value={execution.variant}
options={variantOptions}
disabled={!hasVariantOptions || submitting}
onChange={(variant) => setExecution((prev) => ({ ...prev, variant }))}
/>
</div>
<div className="flex flex-col gap-1.5">
<span className="typography-meta font-medium text-muted-foreground">{t('sessions.scheduledTasks.editor.agent.label')}</span>
<AgentSelector
agentName={execution.agent}
filter={agentFilter}
dropdownPortalToBody
onChange={(agent) => setExecution((prev) => ({ ...prev, agent }))}
/>
</div>
</div>
<DialogFooter>
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)} disabled={submitting}>
{t('diffView.reviewDialog.actions.cancel')}
</Button>
<Button size="sm" onClick={handleSubmit} disabled={!canConfirm || submitting}>
{submitting ? t('diffView.reviewDialog.actions.starting') : t('diffView.reviewDialog.actions.start')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -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<DiffViewProps> = ({
const [mountedStackedFiles, setMountedStackedFiles] = React.useState<Set<string>>(() => 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<DiffViewProps> = ({
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<HTMLElement | null>(null);
const fileSectionRefs = React.useRef(new Map<string, HTMLDivElement | null>());
@@ -1270,6 +1278,35 @@ export const DiffView: React.FC<DiffViewProps> = ({
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<DiffViewProps> = ({
</span>
</Button>
)}
{changedFiles.length > 0 && showReviewAction && (
<Button
variant="default"
size="sm"
onClick={() => setReviewDialogOpen(true)}
disabled={reviewFlowSubmitting}
className="diff-toolbar__review-button h-7 flex-shrink-0 gap-1.5 px-2"
aria-label={t('diffView.actions.reviewAria')}
>
{reviewFlowSubmitting ? (
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<Icon name="search-eye" className="size-4" />
)}
<span className="diff-toolbar__review-label typography-ui-label">
{t('diffView.actions.review')}
</span>
</Button>
)}
{changedFiles.length > 0 && (
<Tooltip>
<TooltipTrigger asChild>
@@ -1626,6 +1682,14 @@ export const DiffView: React.FC<DiffViewProps> = ({
)}
</div>
<ReviewFlowDialog
open={reviewDialogOpen}
onOpenChange={setReviewDialogOpen}
projectDirectory={effectiveDirectory ?? null}
submitting={reviewFlowSubmitting}
onConfirm={handleStartReviewFlow}
/>
{renderContent()}
</div>
);
+2
View File
@@ -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;
}
+11
View File
@@ -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',
+11
View File
@@ -1181,6 +1181,17 @@ export const dict: Record<I18nKey, string> = {
"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",
+11
View File
@@ -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 lorsquil 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',
+11
View File
@@ -1218,6 +1218,17 @@ export const dict: Record<I18nKey, string> = {
'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': '스테이지 해제',
+11
View File
@@ -1415,6 +1415,17 @@ export const dict: Record<I18nKey, string> = {
'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',
@@ -1181,6 +1181,17 @@ export const dict: Record<I18nKey, string> = {
"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",
+11
View File
@@ -1181,6 +1181,17 @@ export const dict: Record<I18nKey, string> = {
"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": "Прибрати",
@@ -1181,6 +1181,17 @@ export const dict: Record<I18nKey, string> = {
'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': '取消暂存',
@@ -1191,6 +1191,17 @@ export const dict: Record<I18nKey, string> = {
'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': '取消暫存',
+12
View File
@@ -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',
+38 -8
View File
@@ -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<void> => {
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<void> => {
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,