feat: add last-turn diff view
Adds a Last turn scope to DiffView that renders OpenCode snapshot diffs from the latest user message summary without re-fetching git contents. The view hides Review in that mode and carries the selected diff scope through main and context-panel navigation. Connects latest-turn changed-file chips in chat to the snapshot diff view on desktop and mobile, while keeping older turn chips static/read-only to avoid misleading affordances and extra subscriptions. Updates localized labels and empty states plus changelog. Validation: bun run type-check (packages/ui); bun run lint (packages/ui).
This commit is contained in:
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Diff/Chat: added a Last turn mode to the Diff view, and latest-turn changed-file chips in chat now open that snapshot while older turn chips stay read-only.
|
||||||
|
|
||||||
## [1.14.1] - 2026-07-07
|
## [1.14.1] - 2026-07-07
|
||||||
|
|
||||||
- Chat: finished agent replies can now show a short recap and a suggested next message, with separate settings for each and a Small Model setting for choosing the utility model used for those helpers.
|
- Chat: finished agent replies can now show a short recap and a suggested next message, with separate settings for each and a Small Model setting for choosing the utility model used for those helpers.
|
||||||
|
|||||||
@@ -752,6 +752,7 @@ const TurnBlock = React.memo(({
|
|||||||
activityOwnerMessageId,
|
activityOwnerMessageId,
|
||||||
isFirstAssistantInTurn: isFirstAssistant,
|
isFirstAssistantInTurn: isFirstAssistant,
|
||||||
isLastAssistantInTurn: isLastAssistant,
|
isLastAssistantInTurn: isLastAssistant,
|
||||||
|
isLatestTurn: isLastTurn,
|
||||||
isWorking: isLastTurn && sessionIsWorking && (
|
isWorking: isLastTurn && sessionIsWorking && (
|
||||||
chatRenderMode === 'sorted'
|
chatRenderMode === 'sorted'
|
||||||
? hasAnchoredActivitySegment
|
? hasAnchoredActivitySegment
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import { Popover } from '@base-ui/react/popover';
|
|||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useIsGitRepo } from '@/stores/useGitStore';
|
import { useIsGitRepo } from '@/stores/useGitStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
|
||||||
import {
|
import {
|
||||||
type ChangedFile,
|
type ChangedFile,
|
||||||
type ChangedFileEntry,
|
type ChangedFileEntry,
|
||||||
FILE_EDIT_TOOLS,
|
FILE_EDIT_TOOLS,
|
||||||
extractChangedFiles,
|
extractChangedFiles,
|
||||||
isGitFile,
|
|
||||||
toRelativePath,
|
toRelativePath,
|
||||||
} from './changedFiles';
|
} from './changedFiles';
|
||||||
import { ChangedFilesList } from './ChangedFilesList';
|
import { ChangedFilesList } from './ChangedFilesList';
|
||||||
@@ -18,7 +16,6 @@ import { changedFilesPopoverClassName, changedFilesPopoverStyle } from './change
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import type { TurnActivityRecord } from './lib/turns/types';
|
import type { TurnActivityRecord } from './lib/turns/types';
|
||||||
import { toAbsoluteFilePath } from '@/lib/path-utils';
|
|
||||||
|
|
||||||
interface TurnChangedFilesDropdownProps {
|
interface TurnChangedFilesDropdownProps {
|
||||||
activityParts: TurnActivityRecord[] | undefined;
|
activityParts: TurnActivityRecord[] | undefined;
|
||||||
@@ -29,7 +26,6 @@ export const TurnChangedFilesDropdown: React.FC<TurnChangedFilesDropdownProps> =
|
|||||||
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
||||||
const triggerButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
const triggerButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||||
const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
|
const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
|
||||||
const runtime = React.useContext(RuntimeAPIContext);
|
|
||||||
const isGitRepo = useIsGitRepo(currentDirectory);
|
const isGitRepo = useIsGitRepo(currentDirectory);
|
||||||
|
|
||||||
const changedFiles = React.useMemo<ChangedFile[]>(() => {
|
const changedFiles = React.useMemo<ChangedFile[]>(() => {
|
||||||
@@ -56,24 +52,16 @@ export const TurnChangedFilesDropdown: React.FC<TurnChangedFilesDropdownProps> =
|
|||||||
|
|
||||||
const handleOpenFile = (file: ChangedFileEntry) => {
|
const handleOpenFile = (file: ChangedFileEntry) => {
|
||||||
if (!currentDirectory) return;
|
if (!currentDirectory) return;
|
||||||
if (isGitFile(file)) return;
|
|
||||||
|
|
||||||
const absolutePath = toAbsoluteFilePath(currentDirectory, file.path);
|
|
||||||
|
|
||||||
const editor = runtime?.editor;
|
|
||||||
if (editor) {
|
|
||||||
void editor.openFile(absolutePath);
|
|
||||||
setIsExpanded(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = useUIStore.getState();
|
const store = useUIStore.getState();
|
||||||
|
const relativePath = toRelativePath(file.path, currentDirectory);
|
||||||
if (!store.isMobile) {
|
if (!store.isMobile) {
|
||||||
store.openContextFile(currentDirectory, absolutePath);
|
store.openContextDiff(currentDirectory, relativePath, false, 'turn');
|
||||||
setIsExpanded(false);
|
setIsExpanded(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
store.navigateToDiff(toRelativePath(file.path, currentDirectory));
|
|
||||||
|
store.navigateToDiff(relativePath, false, 'turn');
|
||||||
store.setRightSidebarOpen(false);
|
store.setRightSidebarOpen(false);
|
||||||
setIsExpanded(false);
|
setIsExpanded(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export interface TurnGroupingContext {
|
|||||||
activityOwnerMessageId?: string;
|
activityOwnerMessageId?: string;
|
||||||
isFirstAssistantInTurn: boolean;
|
isFirstAssistantInTurn: boolean;
|
||||||
isLastAssistantInTurn: boolean;
|
isLastAssistantInTurn: boolean;
|
||||||
|
isLatestTurn: boolean;
|
||||||
summaryBody?: string;
|
summaryBody?: string;
|
||||||
activityParts?: TurnActivityRecord[];
|
activityParts?: TurnActivityRecord[];
|
||||||
activityGroupSegments?: TurnActivityGroup[];
|
activityGroupSegments?: TurnActivityGroup[];
|
||||||
|
|||||||
@@ -65,37 +65,88 @@ const getDisplayFileName = (file: string): string => {
|
|||||||
return segments.at(-1) ?? file;
|
return segments.at(-1) ?? file;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TurnChangedFilePills = React.memo(({ files }: { files?: TurnChangedFile[] }) => {
|
const TurnChangedFileChipContent = React.memo(({ file, interactive = false }: { file: TurnChangedFile; interactive?: boolean }) => (
|
||||||
if (!files || files.length === 0) {
|
<span
|
||||||
return null;
|
className={cn(
|
||||||
}
|
'inline-flex max-w-full items-center gap-1.5 rounded-lg border border-border/30 bg-muted/30 px-2 py-1 text-xs leading-[1.35] text-muted-foreground',
|
||||||
|
interactive && 'transition-colors hover:border-border/60 hover:bg-interactive-hover'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileTypeIcon filePath={file.file} className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
|
<span className="max-w-52 truncate text-foreground/80" title={file.file}>{getDisplayFileName(file.file)}</span>
|
||||||
|
<span className="flex-shrink-0 inline-flex items-center gap-0 typography-meta" style={{ fontSize: '0.8rem', lineHeight: '1' }}>
|
||||||
|
<span style={{ color: 'var(--status-success)' }}>+{file.additions}</span>
|
||||||
|
<span className="text-muted-foreground/70">/</span>
|
||||||
|
<span style={{ color: 'var(--status-error)' }}>-{file.deletions}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
|
||||||
|
const TurnChangedFilePillButton = React.memo(({
|
||||||
|
file,
|
||||||
|
onOpen,
|
||||||
|
}: {
|
||||||
|
file: TurnChangedFile;
|
||||||
|
onOpen: (file: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-8 max-w-full cursor-pointer items-center rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]"
|
||||||
|
aria-label={t('chat.changedFiles.actions.openFileTitle', { path: file.file })}
|
||||||
|
title={file.file}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onOpen(file.file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TurnChangedFileChipContent file={file} interactive />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const StaticTurnChangedFilePills = React.memo(({ files }: { files: TurnChangedFile[] }) => (
|
||||||
|
<>
|
||||||
|
{files.map((file) => (
|
||||||
|
<span key={file.file} className="inline-flex h-8 max-w-full items-center" title={file.file}>
|
||||||
|
<TurnChangedFileChipContent file={file} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
|
||||||
|
const InteractiveTurnChangedFilePills = React.memo(({ files }: { files: TurnChangedFile[] }) => {
|
||||||
|
const effectiveDirectory = useEffectiveDirectory();
|
||||||
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
const navigateToDiff = useUIStore((state) => state.navigateToDiff);
|
||||||
|
const openContextDiff = useUIStore((state) => state.openContextDiff);
|
||||||
|
|
||||||
|
const openLastTurnDiff = React.useCallback((file: string) => {
|
||||||
|
if (!isMobile && effectiveDirectory) {
|
||||||
|
openContextDiff(effectiveDirectory, file, false, 'turn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToDiff(file, false, 'turn');
|
||||||
|
}, [effectiveDirectory, isMobile, navigateToDiff, openContextDiff]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{files.map((file) => {
|
{files.map((file) => (
|
||||||
return (
|
<TurnChangedFilePillButton key={file.file} file={file} onOpen={openLastTurnDiff} />
|
||||||
<Tooltip key={file.file}>
|
))}
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span className="inline-flex h-8 max-w-full items-center">
|
|
||||||
<span className="inline-flex max-w-full items-center gap-1.5 rounded-lg border border-border/30 bg-muted/30 px-2 py-1 text-xs leading-[1.35] text-muted-foreground">
|
|
||||||
<FileTypeIcon filePath={file.file} className="h-3.5 w-3.5 flex-shrink-0" />
|
|
||||||
<span className="max-w-52 truncate text-foreground/80" title={file.file}>{getDisplayFileName(file.file)}</span>
|
|
||||||
<span className="flex-shrink-0 inline-flex items-center gap-0 typography-meta" style={{ fontSize: '0.8rem', lineHeight: '1' }}>
|
|
||||||
<span style={{ color: 'var(--status-success)' }}>+{file.additions}</span>
|
|
||||||
<span className="text-muted-foreground/70">/</span>
|
|
||||||
<span style={{ color: 'var(--status-error)' }}>-{file.deletions}</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{file.file}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const TurnChangedFilePills = React.memo(({ files, isInteractive }: { files?: TurnChangedFile[]; isInteractive: boolean }) => {
|
||||||
|
if (!files || files.length === 0) return null;
|
||||||
|
|
||||||
|
return isInteractive ? <InteractiveTurnChangedFilePills files={files} /> : <StaticTurnChangedFilePills files={files} />;
|
||||||
|
});
|
||||||
|
|
||||||
type SubtaskPartLike = Part & {
|
type SubtaskPartLike = Part & {
|
||||||
type: 'subtask';
|
type: 'subtask';
|
||||||
description?: unknown;
|
description?: unknown;
|
||||||
@@ -2079,7 +2130,10 @@ const AssistantMessageBody = React.memo(({
|
|||||||
<TurnChangedFilesDropdown activityParts={turnGroupingContext?.activityParts} />
|
<TurnChangedFilesDropdown activityParts={turnGroupingContext?.activityParts} />
|
||||||
) : null}
|
) : null}
|
||||||
{!isMiniChatSurface && isLastAssistantInTurn && hasStopFinish ? (
|
{!isMiniChatSurface && isLastAssistantInTurn && hasStopFinish ? (
|
||||||
<TurnChangedFilePills files={turnGroupingContext?.changedFiles} />
|
<TurnChangedFilePills
|
||||||
|
files={turnGroupingContext?.changedFiles}
|
||||||
|
isInteractive={turnGroupingContext?.isLatestTurn === true}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ export const areRelevantTurnGroupingContextsEqual = (
|
|||||||
if (left.turnId !== right.turnId) return false;
|
if (left.turnId !== right.turnId) return false;
|
||||||
if (left.isFirstAssistantInTurn !== right.isFirstAssistantInTurn) return false;
|
if (left.isFirstAssistantInTurn !== right.isFirstAssistantInTurn) return false;
|
||||||
if (left.isLastAssistantInTurn !== right.isLastAssistantInTurn) return false;
|
if (left.isLastAssistantInTurn !== right.isLastAssistantInTurn) return false;
|
||||||
|
if (left.isLatestTurn !== right.isLatestTurn) return false;
|
||||||
if (left.isWorking !== right.isWorking) return false;
|
if (left.isWorking !== right.isWorking) return false;
|
||||||
if (left.hasTools !== right.hasTools) return false;
|
if (left.hasTools !== right.hasTools) return false;
|
||||||
if (left.hasReasoning !== right.hasReasoning) return false;
|
if (left.hasReasoning !== right.hasReasoning) return false;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
||||||
import { useUIStore, type ContextPanelMode } from '@/stores/useUIStore';
|
import { useUIStore, type ContextPanelMode, type PendingDiffScope } from '@/stores/useUIStore';
|
||||||
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
|
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useInputStore } from '@/sync/input-store';
|
import { useInputStore } from '@/sync/input-store';
|
||||||
@@ -2298,7 +2298,7 @@ export const ContextPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [tabs]);
|
}, [tabs]);
|
||||||
|
|
||||||
const handleDiffScopeChange = React.useCallback((nextScope: 'working' | 'staged') => {
|
const handleDiffScopeChange = React.useCallback((nextScope: PendingDiffScope) => {
|
||||||
if (!directoryKey || activeTab?.mode !== 'diff') {
|
if (!directoryKey || activeTab?.mode !== 'diff') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2307,6 +2307,7 @@ export const ContextPanel: React.FC = () => {
|
|||||||
mode: 'diff',
|
mode: 'diff',
|
||||||
targetPath: activeTab.targetPath,
|
targetPath: activeTab.targetPath,
|
||||||
stagedDiff: nextScope === 'staged',
|
stagedDiff: nextScope === 'staged',
|
||||||
|
diffScope: nextScope,
|
||||||
});
|
});
|
||||||
}, [activeTab, directoryKey, openContextPanelTab]);
|
}, [activeTab, directoryKey, openContextPanelTab]);
|
||||||
|
|
||||||
@@ -2649,7 +2650,7 @@ export const ContextPanel: React.FC = () => {
|
|||||||
stackedDefaultCollapsedAll
|
stackedDefaultCollapsedAll
|
||||||
pinSelectedFileHeaderToTopOnNavigate
|
pinSelectedFileHeaderToTopOnNavigate
|
||||||
showOpenInEditorAction
|
showOpenInEditorAction
|
||||||
diffScope={tab.stagedDiff ? 'staged' : 'working'}
|
diffScope={tab.diffScope ?? (tab.stagedDiff ? 'staged' : 'working')}
|
||||||
onDiffScopeChange={handleDiffScopeChange}
|
onDiffScopeChange={handleDiffScopeChange}
|
||||||
targetFilePath={tab.targetPath}
|
targetFilePath={tab.targetPath}
|
||||||
flushContent
|
flushContent
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { fileDiffFromPatch } from '@/lib/diff/patchFileDiff';
|
|||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { startReviewFlow } from '@/lib/reviewFlow';
|
import { startReviewFlow } from '@/lib/reviewFlow';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
|
import { useSessionMessages } from '@/sync/sync-context';
|
||||||
import { getFirstChangedModifiedLineFromPatch } from './diffPatchUtils';
|
import { getFirstChangedModifiedLineFromPatch } from './diffPatchUtils';
|
||||||
import type { FileDiffMetadata } from '@pierre/diffs';
|
import type { FileDiffMetadata } from '@pierre/diffs';
|
||||||
|
|
||||||
@@ -74,7 +75,17 @@ type DiffData = {
|
|||||||
fileDiff?: FileDiffMetadata;
|
fileDiff?: FileDiffMetadata;
|
||||||
contextMode?: DiffContextMode;
|
contextMode?: DiffContextMode;
|
||||||
};
|
};
|
||||||
type DiffScope = 'all' | 'staged' | 'working';
|
type DiffScope = 'all' | 'staged' | 'working' | 'turn';
|
||||||
|
|
||||||
|
type TurnSnapshotDiff = {
|
||||||
|
file?: string;
|
||||||
|
status?: string;
|
||||||
|
before?: string;
|
||||||
|
after?: string;
|
||||||
|
patch?: string;
|
||||||
|
additions?: number;
|
||||||
|
deletions?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const BinaryDiffPlaceholder = React.memo(() => {
|
const BinaryDiffPlaceholder = React.memo(() => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -164,6 +175,20 @@ const getFirstChangedModifiedLine = (original: string, modified: string): number
|
|||||||
const isBinaryPatch = (patch: string): boolean =>
|
const isBinaryPatch = (patch: string): boolean =>
|
||||||
/^Binary files .+ differ$/m.test(patch) || /^GIT binary patch$/m.test(patch);
|
/^Binary files .+ differ$/m.test(patch) || /^GIT binary patch$/m.test(patch);
|
||||||
|
|
||||||
|
const listTurnDiffs = (value: unknown): TurnSnapshotDiff[] => {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value.filter((diff): diff is TurnSnapshotDiff => {
|
||||||
|
if (!diff || typeof diff !== 'object') return false;
|
||||||
|
return typeof (diff as TurnSnapshotDiff).file === 'string';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusToGitCode = (status?: string): string => {
|
||||||
|
if (status === 'added') return 'A';
|
||||||
|
if (status === 'deleted') return 'D';
|
||||||
|
return 'M';
|
||||||
|
};
|
||||||
|
|
||||||
const createTextDiffDataFromPatch = (filePath: string, patch: string, contextMode: DiffContextMode): DiffData => {
|
const createTextDiffDataFromPatch = (filePath: string, patch: string, contextMode: DiffContextMode): DiffData => {
|
||||||
if (isBinaryPatch(patch)) {
|
if (isBinaryPatch(patch)) {
|
||||||
return { original: '', modified: '', isBinary: true, patch, contextMode };
|
return { original: '', modified: '', isBinary: true, patch, contextMode };
|
||||||
@@ -201,22 +226,28 @@ const formatDiffTotals = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ChangeScopeSelectorProps {
|
interface ChangeScopeSelectorProps {
|
||||||
scope: Extract<DiffScope, 'working' | 'staged'>;
|
scope: Extract<DiffScope, 'working' | 'staged' | 'turn'>;
|
||||||
workingCount: number;
|
workingCount: number;
|
||||||
stagedCount: number;
|
stagedCount: number;
|
||||||
onScopeChange?: (scope: Extract<DiffScope, 'working' | 'staged'>) => void;
|
turnCount: number;
|
||||||
|
onScopeChange?: (scope: Extract<DiffScope, 'working' | 'staged' | 'turn'>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangeScopeSelector = React.memo<ChangeScopeSelectorProps>(({
|
const ChangeScopeSelector = React.memo<ChangeScopeSelectorProps>(({
|
||||||
scope,
|
scope,
|
||||||
workingCount,
|
workingCount,
|
||||||
stagedCount,
|
stagedCount,
|
||||||
|
turnCount,
|
||||||
onScopeChange,
|
onScopeChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const currentCount = scope === 'staged' ? stagedCount : workingCount;
|
const currentCount = scope === 'staged' ? stagedCount : scope === 'turn' ? turnCount : workingCount;
|
||||||
const currentLabel = scope === 'staged' ? t('diffView.scope.staged') : t('diffView.scope.changed');
|
const currentLabel = scope === 'staged'
|
||||||
|
? t('diffView.scope.staged')
|
||||||
|
: scope === 'turn'
|
||||||
|
? t('diffView.scope.lastTurn')
|
||||||
|
: t('diffView.scope.changed');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
@@ -236,7 +267,7 @@ const ChangeScopeSelector = React.memo<ChangeScopeSelectorProps>(({
|
|||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup
|
||||||
value={scope}
|
value={scope}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value === 'working' || value === 'staged') {
|
if (value === 'working' || value === 'staged' || value === 'turn') {
|
||||||
onScopeChange?.(value);
|
onScopeChange?.(value);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
@@ -254,6 +285,12 @@ const ChangeScopeSelector = React.memo<ChangeScopeSelectorProps>(({
|
|||||||
<span className="typography-meta text-muted-foreground">{stagedCount}</span>
|
<span className="typography-meta text-muted-foreground">{stagedCount}</span>
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem value="turn">
|
||||||
|
<span className="flex min-w-0 flex-1 items-center justify-between gap-3">
|
||||||
|
<span>{t('diffView.scope.lastTurn')}</span>
|
||||||
|
<span className="typography-meta text-muted-foreground">{turnCount}</span>
|
||||||
|
</span>
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -532,6 +569,7 @@ interface MultiFileDiffEntryProps {
|
|||||||
onOpenInEditor?: (filePath: string, diffData: DiffData | null) => void;
|
onOpenInEditor?: (filePath: string, diffData: DiffData | null) => void;
|
||||||
staged?: boolean;
|
staged?: boolean;
|
||||||
loadFullFiles?: boolean;
|
loadFullFiles?: boolean;
|
||||||
|
initialDiffData?: DiffData | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
||||||
@@ -550,6 +588,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
|||||||
onOpenInEditor,
|
onOpenInEditor,
|
||||||
staged = false,
|
staged = false,
|
||||||
loadFullFiles = false,
|
loadFullFiles = false,
|
||||||
|
initialDiffData = null,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { git } = useRuntimeAPIs();
|
const { git } = useRuntimeAPIs();
|
||||||
@@ -578,11 +617,12 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
|||||||
const fileStatusKey = `${file.index}:${file.working_dir}:${file.insertions}:${file.deletions}`;
|
const fileStatusKey = `${file.index}:${file.working_dir}:${file.insertions}:${file.deletions}`;
|
||||||
|
|
||||||
const diffData = React.useMemo<DiffData | null>(() => {
|
const diffData = React.useMemo<DiffData | null>(() => {
|
||||||
|
if (initialDiffData) return initialDiffData;
|
||||||
if (staged) return stagedDiffData;
|
if (staged) return stagedDiffData;
|
||||||
if (localDiffData) return localDiffData;
|
if (localDiffData) return localDiffData;
|
||||||
if (!cachedDiff) return null;
|
if (!cachedDiff) return null;
|
||||||
return { original: cachedDiff.original, modified: cachedDiff.modified, isBinary: cachedDiff.isBinary, contextMode: 'full' };
|
return { original: cachedDiff.original, modified: cachedDiff.modified, isBinary: cachedDiff.isBinary, contextMode: 'full' };
|
||||||
}, [cachedDiff, localDiffData, staged, stagedDiffData]);
|
}, [cachedDiff, initialDiffData, localDiffData, staged, stagedDiffData]);
|
||||||
|
|
||||||
const diffDataMatchesContextMode = diffData?.contextMode === desiredContextMode;
|
const diffDataMatchesContextMode = diffData?.contextMode === desiredContextMode;
|
||||||
|
|
||||||
@@ -612,7 +652,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isExpanded || !isMounted) return;
|
if (!isExpanded || !isMounted) return;
|
||||||
if (!directory || (diffData && diffDataMatchesContextMode)) {
|
if (!directory || initialDiffData || (diffData && diffDataMatchesContextMode)) {
|
||||||
lastDiffRequestRef.current = null;
|
lastDiffRequestRef.current = null;
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -675,7 +715,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
|||||||
lastDiffRequestRef.current = null;
|
lastDiffRequestRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [desiredContextMode, diffData, diffDataMatchesContextMode, diffRetryNonce, directory, file.path, fileStatusKey, git, isExpanded, isMounted, loadFullFiles, setDiff, staged]);
|
}, [desiredContextMode, diffData, diffDataMatchesContextMode, diffRetryNonce, directory, file.path, fileStatusKey, git, initialDiffData, isExpanded, isMounted, loadFullFiles, setDiff, staged]);
|
||||||
|
|
||||||
const handleToggle = React.useCallback(() => {
|
const handleToggle = React.useCallback(() => {
|
||||||
handleOpenChange(!isExpanded);
|
handleOpenChange(!isExpanded);
|
||||||
@@ -900,7 +940,7 @@ interface DiffViewProps {
|
|||||||
pinSelectedFileHeaderToTopOnNavigate?: boolean;
|
pinSelectedFileHeaderToTopOnNavigate?: boolean;
|
||||||
showOpenInEditorAction?: boolean;
|
showOpenInEditorAction?: boolean;
|
||||||
diffScope?: DiffScope;
|
diffScope?: DiffScope;
|
||||||
onDiffScopeChange?: (scope: Extract<DiffScope, 'working' | 'staged'>) => void;
|
onDiffScopeChange?: (scope: Extract<DiffScope, 'working' | 'staged' | 'turn'>) => void;
|
||||||
targetFilePath?: string | null;
|
targetFilePath?: string | null;
|
||||||
/** Render diff content flush with the container edges (no outer padding). */
|
/** Render diff content flush with the container edges (no outer padding). */
|
||||||
flushContent?: boolean;
|
flushContent?: boolean;
|
||||||
@@ -937,9 +977,15 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
const [scrollRequestNonce, setScrollRequestNonce] = React.useState(0);
|
const [scrollRequestNonce, setScrollRequestNonce] = React.useState(0);
|
||||||
const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false);
|
const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false);
|
||||||
const [reviewFlowSubmitting, setReviewFlowSubmitting] = React.useState(false);
|
const [reviewFlowSubmitting, setReviewFlowSubmitting] = React.useState(false);
|
||||||
|
const [activeDiffScope, setActiveDiffScope] = React.useState(diffScope);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setActiveDiffScope(diffScope);
|
||||||
|
}, [diffScope]);
|
||||||
|
|
||||||
const pendingDiffFile = useUIStore((state) => state.pendingDiffFile);
|
const pendingDiffFile = useUIStore((state) => state.pendingDiffFile);
|
||||||
const pendingDiffStaged = useUIStore((state) => state.pendingDiffStaged);
|
const pendingDiffStaged = useUIStore((state) => state.pendingDiffStaged);
|
||||||
|
const pendingDiffScope = useUIStore((state) => state.pendingDiffScope);
|
||||||
const setPendingDiffFile = useUIStore((state) => state.setPendingDiffFile);
|
const setPendingDiffFile = useUIStore((state) => state.setPendingDiffFile);
|
||||||
const diffLayoutPreference = useUIStore((state) => state.diffLayoutPreference);
|
const diffLayoutPreference = useUIStore((state) => state.diffLayoutPreference);
|
||||||
const diffFileLayout = useUIStore((state) => state.diffFileLayout);
|
const diffFileLayout = useUIStore((state) => state.diffFileLayout);
|
||||||
@@ -948,12 +994,13 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
const setDiffWrapLines = useUIStore((state) => state.setDiffWrapLines);
|
const setDiffWrapLines = useUIStore((state) => state.setDiffWrapLines);
|
||||||
const openContextFileAtLine = useUIStore((state) => state.openContextFileAtLine);
|
const openContextFileAtLine = useUIStore((state) => state.openContextFileAtLine);
|
||||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||||
|
const sessionMessages = useSessionMessages(currentSessionId ?? '', effectiveDirectory ?? undefined);
|
||||||
const diffWrapLines = diffWrapLinesStore;
|
const diffWrapLines = diffWrapLinesStore;
|
||||||
const forcedStaged = diffScope === 'staged' ? true : diffScope === 'working' ? false : null;
|
const forcedStaged = activeDiffScope === 'staged' ? true : activeDiffScope === 'working' ? false : null;
|
||||||
const activeDiffStaged = forcedStaged ?? displayFileStaged;
|
const activeDiffStaged = forcedStaged ?? displayFileStaged;
|
||||||
|
|
||||||
const isMobileLayout = isMobile || screenWidth <= 768;
|
const isMobileLayout = isMobile || screenWidth <= 768;
|
||||||
const showReviewAction = Boolean(currentSessionId) && !isMobileLayout && !isVSCodeRuntime();
|
const showReviewAction = Boolean(currentSessionId) && activeDiffScope !== 'turn' && !isMobileLayout && !isVSCodeRuntime();
|
||||||
const showFileSidebar = !hideStackedFileSidebar && !isMobileLayout && screenWidth >= 1024;
|
const showFileSidebar = !hideStackedFileSidebar && !isMobileLayout && screenWidth >= 1024;
|
||||||
const diffScrollRef = React.useRef<HTMLElement | null>(null);
|
const diffScrollRef = React.useRef<HTMLElement | null>(null);
|
||||||
const fileSectionRefs = React.useRef(new Map<string, HTMLDivElement | null>());
|
const fileSectionRefs = React.useRef(new Map<string, HTMLDivElement | null>());
|
||||||
@@ -984,12 +1031,52 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const lastTurnDiffs = React.useMemo(() => {
|
||||||
|
for (let index = sessionMessages.length - 1; index >= 0; index -= 1) {
|
||||||
|
const message = sessionMessages[index] as { role?: string; summary?: { diffs?: unknown } };
|
||||||
|
if (message.role !== 'user') continue;
|
||||||
|
return listTurnDiffs(message.summary?.diffs);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [sessionMessages]);
|
||||||
|
|
||||||
|
const lastTurnDiffData = React.useMemo(() => {
|
||||||
|
const map = new Map<string, DiffData>();
|
||||||
|
for (const diff of lastTurnDiffs) {
|
||||||
|
if (!diff.file) continue;
|
||||||
|
if (typeof diff.patch === 'string') {
|
||||||
|
map.set(diff.file, createTextDiffDataFromPatch(diff.file, diff.patch, 'patch'));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
map.set(diff.file, {
|
||||||
|
original: diff.before ?? '',
|
||||||
|
modified: diff.after ?? '',
|
||||||
|
contextMode: 'full',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [lastTurnDiffs]);
|
||||||
|
|
||||||
const changedFiles: FileEntry[] = React.useMemo(() => {
|
const changedFiles: FileEntry[] = React.useMemo(() => {
|
||||||
|
if (activeDiffScope === 'turn') {
|
||||||
|
return lastTurnDiffs
|
||||||
|
.map((diff) => ({
|
||||||
|
path: diff.file ?? '',
|
||||||
|
index: '',
|
||||||
|
working_dir: statusToGitCode(diff.status),
|
||||||
|
insertions: diff.additions ?? 0,
|
||||||
|
deletions: diff.deletions ?? 0,
|
||||||
|
isNew: diff.status === 'added',
|
||||||
|
}))
|
||||||
|
.filter((file) => file.path)
|
||||||
|
.sort((a, b) => a.path.localeCompare(b.path));
|
||||||
|
}
|
||||||
|
|
||||||
if (!status?.files) return [];
|
if (!status?.files) return [];
|
||||||
const diffStats = status.diffStats ?? {};
|
const diffStats = status.diffStats ?? {};
|
||||||
const includeFile = diffScope === 'staged'
|
const includeFile = activeDiffScope === 'staged'
|
||||||
? isStagedStatusFile
|
? isStagedStatusFile
|
||||||
: diffScope === 'working'
|
: activeDiffScope === 'working'
|
||||||
? isWorkingStatusFile
|
? isWorkingStatusFile
|
||||||
: () => true;
|
: () => true;
|
||||||
|
|
||||||
@@ -1002,7 +1089,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
isNew: isNewStatusFile(file),
|
isNew: isNewStatusFile(file),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.path.localeCompare(b.path));
|
.sort((a, b) => a.path.localeCompare(b.path));
|
||||||
}, [diffScope, status]);
|
}, [activeDiffScope, lastTurnDiffs, status]);
|
||||||
|
|
||||||
const workingFileCount = React.useMemo(() => {
|
const workingFileCount = React.useMemo(() => {
|
||||||
if (!status?.files) return 0;
|
if (!status?.files) return 0;
|
||||||
@@ -1014,6 +1101,8 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
return status.files.filter(isStagedStatusFile).length;
|
return status.files.filter(isStagedStatusFile).length;
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
|
const turnFileCount = lastTurnDiffs.length;
|
||||||
|
|
||||||
const changedFilePathsKey = React.useMemo(
|
const changedFilePathsKey = React.useMemo(
|
||||||
() => changedFiles.map((file) => file.path).join('\0'),
|
() => changedFiles.map((file) => file.path).join('\0'),
|
||||||
[changedFiles],
|
[changedFiles],
|
||||||
@@ -1022,7 +1111,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const paths = changedFilePathsKey ? changedFilePathsKey.split('\0') : [];
|
const paths = changedFilePathsKey ? changedFilePathsKey.split('\0') : [];
|
||||||
const pathSet = new Set(paths);
|
const pathSet = new Set(paths);
|
||||||
const scopeKey = `${effectiveDirectory ?? ''}:${diffScope}:${stackedDefaultCollapsedAll ? 'collapsed' : 'default'}`;
|
const scopeKey = `${effectiveDirectory ?? ''}:${activeDiffScope}:${stackedDefaultCollapsedAll ? 'collapsed' : 'default'}`;
|
||||||
const shouldInitialize = stackedStateScopeRef.current !== scopeKey;
|
const shouldInitialize = stackedStateScopeRef.current !== scopeKey;
|
||||||
stackedStateScopeRef.current = scopeKey;
|
stackedStateScopeRef.current = scopeKey;
|
||||||
|
|
||||||
@@ -1062,7 +1151,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
}
|
}
|
||||||
return changed ? next : previous;
|
return changed ? next : previous;
|
||||||
});
|
});
|
||||||
}, [changedFilePathsKey, diffScope, effectiveDirectory, stackedDefaultCollapsedAll]);
|
}, [activeDiffScope, changedFilePathsKey, effectiveDirectory, stackedDefaultCollapsedAll]);
|
||||||
|
|
||||||
const syncVisibleStackedFiles = React.useCallback(() => {
|
const syncVisibleStackedFiles = React.useCallback(() => {
|
||||||
visibleSyncFrameRef.current = null;
|
visibleSyncFrameRef.current = null;
|
||||||
@@ -1167,23 +1256,26 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
|
|
||||||
// Handle pending diff file from external navigation
|
// Handle pending diff file from external navigation
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (diffScope !== 'all') {
|
if (activeDiffScope !== 'all' && !pendingDiffScope) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingDiffFile) {
|
if (pendingDiffFile) {
|
||||||
|
if (pendingDiffScope) {
|
||||||
|
setActiveDiffScope(pendingDiffScope);
|
||||||
|
}
|
||||||
setDisplayFile(pendingDiffFile);
|
setDisplayFile(pendingDiffFile);
|
||||||
setDisplayFileStaged(pendingDiffStaged);
|
setDisplayFileStaged(pendingDiffScope === 'staged' || (!pendingDiffScope && pendingDiffStaged));
|
||||||
setPendingDiffFile(null);
|
setPendingDiffFile(null);
|
||||||
shouldPinAfterAlignRef.current = true;
|
shouldPinAfterAlignRef.current = true;
|
||||||
pendingScrollTargetRef.current = pendingDiffFile;
|
pendingScrollTargetRef.current = pendingDiffFile;
|
||||||
expandStackedFile(pendingDiffFile);
|
expandStackedFile(pendingDiffFile);
|
||||||
setScrollRequestNonce((value) => value + 1);
|
setScrollRequestNonce((value) => value + 1);
|
||||||
}
|
}
|
||||||
}, [diffScope, expandStackedFile, pendingDiffFile, pendingDiffStaged, setPendingDiffFile]);
|
}, [activeDiffScope, expandStackedFile, pendingDiffFile, pendingDiffScope, pendingDiffStaged, setPendingDiffFile]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (diffScope === 'all') {
|
if (activeDiffScope === 'all') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,13 +1285,13 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDisplayFile(normalizedTarget);
|
setDisplayFile(normalizedTarget);
|
||||||
setDisplayFileStaged(diffScope === 'staged');
|
setDisplayFileStaged(activeDiffScope === 'staged');
|
||||||
|
|
||||||
shouldPinAfterAlignRef.current = true;
|
shouldPinAfterAlignRef.current = true;
|
||||||
pendingScrollTargetRef.current = normalizedTarget;
|
pendingScrollTargetRef.current = normalizedTarget;
|
||||||
expandStackedFile(normalizedTarget);
|
expandStackedFile(normalizedTarget);
|
||||||
setScrollRequestNonce((value) => value + 1);
|
setScrollRequestNonce((value) => value + 1);
|
||||||
}, [diffScope, expandStackedFile, targetFilePath]);
|
}, [activeDiffScope, expandStackedFile, targetFilePath]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!displayFile) {
|
if (!displayFile) {
|
||||||
@@ -1508,13 +1600,14 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
onSelect={handleSelectFile}
|
onSelect={handleSelectFile}
|
||||||
onExpandedChange={handleStackedEntryExpandedChange}
|
onExpandedChange={handleStackedEntryExpandedChange}
|
||||||
registerSectionRef={registerSectionRef}
|
registerSectionRef={registerSectionRef}
|
||||||
showOpenInEditorAction={showOpenInEditorAction}
|
showOpenInEditorAction={showOpenInEditorAction && activeDiffScope !== 'turn'}
|
||||||
isOpeningInEditor={openingEditorFilePath === file.path}
|
isOpeningInEditor={openingEditorFilePath === file.path}
|
||||||
onOpenInEditor={(filePath, diffData) => {
|
onOpenInEditor={(filePath, diffData) => {
|
||||||
void openFileInEditorAtChange(filePath, diffData);
|
void openFileInEditorAtChange(filePath, diffData);
|
||||||
}}
|
}}
|
||||||
staged={getFileStaged(file.path)}
|
staged={getFileStaged(file.path)}
|
||||||
loadFullFiles={loadFullFiles}
|
loadFullFiles={loadFullFiles}
|
||||||
|
initialDiffData={activeDiffScope === 'turn' ? lastTurnDiffData.get(file.path) ?? null : null}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1534,7 +1627,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoadingStatus && !status) {
|
if (activeDiffScope !== 'turn' && isLoadingStatus && !status) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center justify-center gap-2 text-sm text-muted-foreground">
|
<div className="flex flex-1 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||||
<Icon name="loader-4" className="size-4 animate-spin" />
|
<Icon name="loader-4" className="size-4 animate-spin" />
|
||||||
@@ -1543,7 +1636,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGitRepo === false) {
|
if (activeDiffScope !== 'turn' && isGitRepo === false) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||||
{t('diffView.state.notGitRepository')}
|
{t('diffView.state.notGitRepository')}
|
||||||
@@ -1554,7 +1647,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
if (changedFiles.length === 0) {
|
if (changedFiles.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||||
{t('diffView.state.cleanWorkingTree')}
|
{activeDiffScope === 'turn' ? t('diffView.state.noLastTurnChanges') : t('diffView.state.cleanWorkingTree')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1566,12 +1659,16 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
|||||||
<div className="flex h-full flex-col overflow-hidden bg-background">
|
<div className="flex h-full flex-col overflow-hidden bg-background">
|
||||||
<div className="@container/diff-toolbar flex min-w-0 items-center gap-2 px-3 py-2 bg-background">
|
<div className="@container/diff-toolbar flex min-w-0 items-center gap-2 px-3 py-2 bg-background">
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
diffScope === 'working' || diffScope === 'staged' ? (
|
activeDiffScope === 'working' || activeDiffScope === 'staged' || activeDiffScope === 'turn' ? (
|
||||||
<ChangeScopeSelector
|
<ChangeScopeSelector
|
||||||
scope={diffScope}
|
scope={activeDiffScope}
|
||||||
workingCount={workingFileCount}
|
workingCount={workingFileCount}
|
||||||
stagedCount={stagedFileCount}
|
stagedCount={stagedFileCount}
|
||||||
onScopeChange={onDiffScopeChange}
|
turnCount={turnFileCount}
|
||||||
|
onScopeChange={(scope) => {
|
||||||
|
setActiveDiffScope(scope);
|
||||||
|
onDiffScopeChange?.(scope);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1 rounded-md px-2 py-1 text-muted-foreground shrink-0">
|
<div className="flex items-center gap-1 rounded-md px-2 py-1 text-muted-foreground shrink-0">
|
||||||
|
|||||||
@@ -1254,6 +1254,7 @@ export const dict = {
|
|||||||
'diffView.state.loadingRepositoryStatus': 'Loading repository status...',
|
'diffView.state.loadingRepositoryStatus': 'Loading repository status...',
|
||||||
'diffView.state.notGitRepository': 'Not a git repository. Use the Git tab to initialize or change directories.',
|
'diffView.state.notGitRepository': 'Not a git repository. Use the Git tab to initialize or change directories.',
|
||||||
'diffView.state.cleanWorkingTree': 'Working tree clean, no changes to display',
|
'diffView.state.cleanWorkingTree': 'Working tree clean, no changes to display',
|
||||||
|
'diffView.state.noLastTurnChanges': 'No last turn changes to display',
|
||||||
'diffView.state.failedToLoadDiff': 'Failed to load diff',
|
'diffView.state.failedToLoadDiff': 'Failed to load diff',
|
||||||
'diffView.state.loadingDiff': 'Loading diff...',
|
'diffView.state.loadingDiff': 'Loading diff...',
|
||||||
'diffView.state.loadingChanges': 'Loading changes...',
|
'diffView.state.loadingChanges': 'Loading changes...',
|
||||||
@@ -1263,6 +1264,7 @@ export const dict = {
|
|||||||
'diffView.summary.changedFilesPlural': '{count} files changed',
|
'diffView.summary.changedFilesPlural': '{count} files changed',
|
||||||
'diffView.scope.changed': 'Changed',
|
'diffView.scope.changed': 'Changed',
|
||||||
'diffView.scope.staged': 'Staged',
|
'diffView.scope.staged': 'Staged',
|
||||||
|
'diffView.scope.lastTurn': 'Last turn',
|
||||||
'diffView.scope.selectorAria': 'Select change mode',
|
'diffView.scope.selectorAria': 'Select change mode',
|
||||||
'diffView.actions.retry': 'Retry',
|
'diffView.actions.retry': 'Retry',
|
||||||
'diffView.actions.renderAnyway': 'Render anyway',
|
'diffView.actions.renderAnyway': 'Render anyway',
|
||||||
|
|||||||
@@ -1220,6 +1220,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"diffView.state.loadingRepositoryStatus": "Cargando estado del repositorio...",
|
"diffView.state.loadingRepositoryStatus": "Cargando estado del repositorio...",
|
||||||
"diffView.state.notGitRepository": "No es un repositorio Git. Usa la pestaña Git para inicializar o cambiar de directorio.",
|
"diffView.state.notGitRepository": "No es un repositorio Git. Usa la pestaña Git para inicializar o cambiar de directorio.",
|
||||||
"diffView.state.cleanWorkingTree": "Worktree limpio, no hay cambios para mostrar",
|
"diffView.state.cleanWorkingTree": "Worktree limpio, no hay cambios para mostrar",
|
||||||
|
"diffView.state.noLastTurnChanges": "No hay cambios del último turno para mostrar",
|
||||||
"diffView.state.failedToLoadDiff": "No se pudo cargar la diferencia",
|
"diffView.state.failedToLoadDiff": "No se pudo cargar la diferencia",
|
||||||
"diffView.state.loadingDiff": "Cargando diff...",
|
"diffView.state.loadingDiff": "Cargando diff...",
|
||||||
"diffView.state.loadingChanges": "Cargando cambios...",
|
"diffView.state.loadingChanges": "Cargando cambios...",
|
||||||
@@ -1229,6 +1230,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"diffView.summary.changedFilesPlural": "{count} archivos modificados",
|
"diffView.summary.changedFilesPlural": "{count} archivos modificados",
|
||||||
"diffView.scope.changed": "Cambiados",
|
"diffView.scope.changed": "Cambiados",
|
||||||
"diffView.scope.staged": "Staged",
|
"diffView.scope.staged": "Staged",
|
||||||
|
"diffView.scope.lastTurn": "Último turno",
|
||||||
"diffView.scope.selectorAria": "Seleccionar modo de cambios",
|
"diffView.scope.selectorAria": "Seleccionar modo de cambios",
|
||||||
"diffView.actions.retry": "Volver a intentar",
|
"diffView.actions.retry": "Volver a intentar",
|
||||||
"diffView.actions.renderAnyway": "Renderizar de todos modos",
|
"diffView.actions.renderAnyway": "Renderizar de todos modos",
|
||||||
|
|||||||
@@ -1088,6 +1088,7 @@ export const dict = {
|
|||||||
'diffView.state.loadingRepositoryStatus': 'Chargement de l\'état du dépôt...',
|
'diffView.state.loadingRepositoryStatus': 'Chargement de l\'état du dépôt...',
|
||||||
'diffView.state.notGitRepository': 'Pas un dépôt git. Utilisez l\'onglet Git pour initialiser ou modifier des répertoires.',
|
'diffView.state.notGitRepository': 'Pas un dépôt git. Utilisez l\'onglet Git pour initialiser ou modifier des répertoires.',
|
||||||
'diffView.state.cleanWorkingTree': 'Worktree sans modifications, rien à afficher',
|
'diffView.state.cleanWorkingTree': 'Worktree sans modifications, rien à afficher',
|
||||||
|
'diffView.state.noLastTurnChanges': 'Aucun changement du dernier tour à afficher',
|
||||||
'diffView.state.failedToLoadDiff': 'Échec du chargement du différentiel',
|
'diffView.state.failedToLoadDiff': 'Échec du chargement du différentiel',
|
||||||
'diffView.state.loadingDiff': 'Chargement du différentiel...',
|
'diffView.state.loadingDiff': 'Chargement du différentiel...',
|
||||||
'diffView.state.loadingChanges': 'Chargement des modifications...',
|
'diffView.state.loadingChanges': 'Chargement des modifications...',
|
||||||
@@ -1097,6 +1098,7 @@ export const dict = {
|
|||||||
'diffView.summary.changedFilesPlural': 'Fichiers {count} modifiés',
|
'diffView.summary.changedFilesPlural': 'Fichiers {count} modifiés',
|
||||||
"diffView.scope.changed": "Modifiés",
|
"diffView.scope.changed": "Modifiés",
|
||||||
"diffView.scope.staged": "Staged",
|
"diffView.scope.staged": "Staged",
|
||||||
|
"diffView.scope.lastTurn": "Dernier tour",
|
||||||
"diffView.scope.selectorAria": "Sélectionner le mode de changements",
|
"diffView.scope.selectorAria": "Sélectionner le mode de changements",
|
||||||
'diffView.actions.retry': 'Réessayer',
|
'diffView.actions.retry': 'Réessayer',
|
||||||
'diffView.actions.renderAnyway': 'Afficher quand même',
|
'diffView.actions.renderAnyway': 'Afficher quand même',
|
||||||
|
|||||||
@@ -1250,6 +1250,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.state.loadingRepositoryStatus': 'リポジトリステータスを読み込み中...',
|
'diffView.state.loadingRepositoryStatus': 'リポジトリステータスを読み込み中...',
|
||||||
'diffView.state.notGitRepository': 'Gitリポジトリではありません。Gitタブを使用して初期化するかディレクトリを変更してください。',
|
'diffView.state.notGitRepository': 'Gitリポジトリではありません。Gitタブを使用して初期化するかディレクトリを変更してください。',
|
||||||
'diffView.state.cleanWorkingTree': 'ワーキングツリーはクリーンで、表示する変更はありません',
|
'diffView.state.cleanWorkingTree': 'ワーキングツリーはクリーンで、表示する変更はありません',
|
||||||
|
'diffView.state.noLastTurnChanges': '表示する最後のターンの変更はありません',
|
||||||
'diffView.state.failedToLoadDiff': '差分の読み込みに失敗しました',
|
'diffView.state.failedToLoadDiff': '差分の読み込みに失敗しました',
|
||||||
'diffView.state.loadingDiff': '差分を読み込み中...',
|
'diffView.state.loadingDiff': '差分を読み込み中...',
|
||||||
'diffView.state.loadingChanges': '変更を読み込み中...',
|
'diffView.state.loadingChanges': '変更を読み込み中...',
|
||||||
@@ -1259,6 +1260,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.summary.changedFilesPlural': '{count}ファイルが変更されました',
|
'diffView.summary.changedFilesPlural': '{count}ファイルが変更されました',
|
||||||
'diffView.scope.changed': '変更済み',
|
'diffView.scope.changed': '変更済み',
|
||||||
'diffView.scope.staged': 'ステージ済み',
|
'diffView.scope.staged': 'ステージ済み',
|
||||||
|
'diffView.scope.lastTurn': '最後のターン',
|
||||||
'diffView.scope.selectorAria': '変更モードを選択',
|
'diffView.scope.selectorAria': '変更モードを選択',
|
||||||
'diffView.actions.retry': '再試行',
|
'diffView.actions.retry': '再試行',
|
||||||
'diffView.actions.renderAnyway': 'とにかくレンダリング',
|
'diffView.actions.renderAnyway': 'とにかくレンダリング',
|
||||||
|
|||||||
@@ -1257,6 +1257,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.state.loadingRepositoryStatus': '레포지토리 상태 로드 중…',
|
'diffView.state.loadingRepositoryStatus': '레포지토리 상태 로드 중…',
|
||||||
'diffView.state.notGitRepository': 'Git 레포지토리가 아닙니다. Git 탭에서 초기화하거나 디렉터리를 변경하세요.',
|
'diffView.state.notGitRepository': 'Git 레포지토리가 아닙니다. Git 탭에서 초기화하거나 디렉터리를 변경하세요.',
|
||||||
'diffView.state.cleanWorkingTree': '워킹 트리가 깨끗합니다. 표시할 변경 사항이 없습니다.',
|
'diffView.state.cleanWorkingTree': '워킹 트리가 깨끗합니다. 표시할 변경 사항이 없습니다.',
|
||||||
|
'diffView.state.noLastTurnChanges': '표시할 마지막 턴 변경 사항이 없습니다.',
|
||||||
'diffView.state.failedToLoadDiff': '변경사항을 불러오지 못했습니다',
|
'diffView.state.failedToLoadDiff': '변경사항을 불러오지 못했습니다',
|
||||||
'diffView.state.loadingDiff': '변경사항 불러오는 중…',
|
'diffView.state.loadingDiff': '변경사항 불러오는 중…',
|
||||||
'diffView.state.loadingChanges': '변경 사항 로드 중…',
|
'diffView.state.loadingChanges': '변경 사항 로드 중…',
|
||||||
@@ -1266,6 +1267,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.summary.changedFilesPlural': '파일 {count}개 변경됨',
|
'diffView.summary.changedFilesPlural': '파일 {count}개 변경됨',
|
||||||
"diffView.scope.changed": "Changed",
|
"diffView.scope.changed": "Changed",
|
||||||
"diffView.scope.staged": "Staged",
|
"diffView.scope.staged": "Staged",
|
||||||
|
"diffView.scope.lastTurn": "마지막 턴",
|
||||||
"diffView.scope.selectorAria": "변경 모드 선택",
|
"diffView.scope.selectorAria": "변경 모드 선택",
|
||||||
'diffView.actions.retry': '다시 시도',
|
'diffView.actions.retry': '다시 시도',
|
||||||
'diffView.actions.renderAnyway': '그래도 렌더링',
|
'diffView.actions.renderAnyway': '그래도 렌더링',
|
||||||
|
|||||||
@@ -1537,6 +1537,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.section.files': 'Pliki',
|
'diffView.section.files': 'Pliki',
|
||||||
'diffView.selector.selectFile': 'Wybierz plik',
|
'diffView.selector.selectFile': 'Wybierz plik',
|
||||||
'diffView.state.cleanWorkingTree': 'Drzewo robocze jest czyste, brak zmian do wyświetlenia',
|
'diffView.state.cleanWorkingTree': 'Drzewo robocze jest czyste, brak zmian do wyświetlenia',
|
||||||
|
'diffView.state.noLastTurnChanges': 'Brak zmian z ostatniej tury do wyświetlenia',
|
||||||
'diffView.state.failedToLoadDiff': 'Nie udało się wczytać diffu',
|
'diffView.state.failedToLoadDiff': 'Nie udało się wczytać diffu',
|
||||||
'diffView.state.largeDiff': 'Duży diff ({count} zmienionych linii)',
|
'diffView.state.largeDiff': 'Duży diff ({count} zmienionych linii)',
|
||||||
'diffView.state.largeDiffDescription': 'Renderowanie może być wolne. Nadal możesz wyświetlić diff przyciskiem poniżej.',
|
'diffView.state.largeDiffDescription': 'Renderowanie może być wolne. Nadal możesz wyświetlić diff przyciskiem poniżej.',
|
||||||
@@ -1548,6 +1549,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.summary.changedFilesPlural': 'Zmieniono {count} plików',
|
'diffView.summary.changedFilesPlural': 'Zmieniono {count} plików',
|
||||||
"diffView.scope.changed": "Zmienione",
|
"diffView.scope.changed": "Zmienione",
|
||||||
"diffView.scope.staged": "Staged",
|
"diffView.scope.staged": "Staged",
|
||||||
|
"diffView.scope.lastTurn": "Ostatnia tura",
|
||||||
"diffView.scope.selectorAria": "Wybierz tryb zmian",
|
"diffView.scope.selectorAria": "Wybierz tryb zmian",
|
||||||
'diffView.summary.changedFilesSingle': 'Zmieniono {count} plik',
|
'diffView.summary.changedFilesSingle': 'Zmieniono {count} plik',
|
||||||
'directoryExplorerDialog.actions.addProject': 'Dodaj projekt',
|
'directoryExplorerDialog.actions.addProject': 'Dodaj projekt',
|
||||||
|
|||||||
@@ -1220,6 +1220,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"diffView.state.loadingRepositoryStatus": "Carregando status do repositório...",
|
"diffView.state.loadingRepositoryStatus": "Carregando status do repositório...",
|
||||||
"diffView.state.notGitRepository": "Não é um repositório Git. Use a aba Git para inicializar ou mudar de diretório.",
|
"diffView.state.notGitRepository": "Não é um repositório Git. Use a aba Git para inicializar ou mudar de diretório.",
|
||||||
"diffView.state.cleanWorkingTree": "Worktree limpo, não há alterações para mostrar",
|
"diffView.state.cleanWorkingTree": "Worktree limpo, não há alterações para mostrar",
|
||||||
|
"diffView.state.noLastTurnChanges": "Não há alterações do último turno para exibir",
|
||||||
"diffView.state.failedToLoadDiff": "Não foi possível carregar a diferencia",
|
"diffView.state.failedToLoadDiff": "Não foi possível carregar a diferencia",
|
||||||
"diffView.state.loadingDiff": "Carregando diff...",
|
"diffView.state.loadingDiff": "Carregando diff...",
|
||||||
"diffView.state.loadingChanges": "Carregando alterações...",
|
"diffView.state.loadingChanges": "Carregando alterações...",
|
||||||
@@ -1229,6 +1230,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"diffView.summary.changedFilesPlural": "{count} arquivos modificados",
|
"diffView.summary.changedFilesPlural": "{count} arquivos modificados",
|
||||||
"diffView.scope.changed": "Alteradas",
|
"diffView.scope.changed": "Alteradas",
|
||||||
"diffView.scope.staged": "Staged",
|
"diffView.scope.staged": "Staged",
|
||||||
|
"diffView.scope.lastTurn": "Último turno",
|
||||||
"diffView.scope.selectorAria": "Selecionar modo de alterações",
|
"diffView.scope.selectorAria": "Selecionar modo de alterações",
|
||||||
"diffView.actions.retry": "Tentar novamente",
|
"diffView.actions.retry": "Tentar novamente",
|
||||||
"diffView.actions.renderAnyway": "Renderizar mesmo assim",
|
"diffView.actions.renderAnyway": "Renderizar mesmo assim",
|
||||||
|
|||||||
@@ -1220,6 +1220,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"diffView.state.loadingRepositoryStatus": "Завантаження статусу сховища...",
|
"diffView.state.loadingRepositoryStatus": "Завантаження статусу сховища...",
|
||||||
"diffView.state.notGitRepository": "Це не Git-репозиторій. Скористайтеся вкладкою Git, щоб ініціалізувати або змінити каталог.",
|
"diffView.state.notGitRepository": "Це не Git-репозиторій. Скористайтеся вкладкою Git, щоб ініціалізувати або змінити каталог.",
|
||||||
"diffView.state.cleanWorkingTree": "Worktree чистий, без змін для відображення",
|
"diffView.state.cleanWorkingTree": "Worktree чистий, без змін для відображення",
|
||||||
|
"diffView.state.noLastTurnChanges": "Немає змін останнього ходу для відображення",
|
||||||
"diffView.state.failedToLoadDiff": "Не вдалося завантажити diff",
|
"diffView.state.failedToLoadDiff": "Не вдалося завантажити diff",
|
||||||
"diffView.state.loadingDiff": "Завантаження diff...",
|
"diffView.state.loadingDiff": "Завантаження diff...",
|
||||||
"diffView.state.loadingChanges": "Завантаження змін...",
|
"diffView.state.loadingChanges": "Завантаження змін...",
|
||||||
@@ -1229,6 +1230,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
"diffView.summary.changedFilesPlural": "Змінено файлів: {count}",
|
"diffView.summary.changedFilesPlural": "Змінено файлів: {count}",
|
||||||
"diffView.scope.changed": "Змінені",
|
"diffView.scope.changed": "Змінені",
|
||||||
"diffView.scope.staged": "Індексовані",
|
"diffView.scope.staged": "Індексовані",
|
||||||
|
"diffView.scope.lastTurn": "Останній хід",
|
||||||
"diffView.scope.selectorAria": "Вибрати режим змін",
|
"diffView.scope.selectorAria": "Вибрати режим змін",
|
||||||
"diffView.actions.retry": "Повторити спробу",
|
"diffView.actions.retry": "Повторити спробу",
|
||||||
"diffView.actions.renderAnyway": "Все одно відрендерити",
|
"diffView.actions.renderAnyway": "Все одно відрендерити",
|
||||||
|
|||||||
@@ -1220,6 +1220,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.state.loadingRepositoryStatus': '正在加载仓库状态...',
|
'diffView.state.loadingRepositoryStatus': '正在加载仓库状态...',
|
||||||
'diffView.state.notGitRepository': '当前不是 Git 仓库。请在 Git 标签中初始化或切换目录。',
|
'diffView.state.notGitRepository': '当前不是 Git 仓库。请在 Git 标签中初始化或切换目录。',
|
||||||
'diffView.state.cleanWorkingTree': '工作区干净,没有可显示的改动',
|
'diffView.state.cleanWorkingTree': '工作区干净,没有可显示的改动',
|
||||||
|
'diffView.state.noLastTurnChanges': '没有可显示的上一轮更改',
|
||||||
'diffView.state.failedToLoadDiff': '加载差异失败',
|
'diffView.state.failedToLoadDiff': '加载差异失败',
|
||||||
'diffView.state.loadingDiff': '正在加载差异...',
|
'diffView.state.loadingDiff': '正在加载差异...',
|
||||||
'diffView.state.loadingChanges': '正在加载变更...',
|
'diffView.state.loadingChanges': '正在加载变更...',
|
||||||
@@ -1229,6 +1230,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.summary.changedFilesPlural': '{count} 个文件已变更',
|
'diffView.summary.changedFilesPlural': '{count} 个文件已变更',
|
||||||
"diffView.scope.changed": "已更改",
|
"diffView.scope.changed": "已更改",
|
||||||
"diffView.scope.staged": "已暂存",
|
"diffView.scope.staged": "已暂存",
|
||||||
|
"diffView.scope.lastTurn": "上一轮",
|
||||||
"diffView.scope.selectorAria": "选择更改模式",
|
"diffView.scope.selectorAria": "选择更改模式",
|
||||||
'diffView.actions.retry': '重试',
|
'diffView.actions.retry': '重试',
|
||||||
'diffView.actions.renderAnyway': '仍然渲染',
|
'diffView.actions.renderAnyway': '仍然渲染',
|
||||||
|
|||||||
@@ -1230,6 +1230,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.state.loadingRepositoryStatus': '正在載入儲存庫狀態...',
|
'diffView.state.loadingRepositoryStatus': '正在載入儲存庫狀態...',
|
||||||
'diffView.state.notGitRepository': '目前不是 Git 儲存庫。請在 Git 標籤中初始化或切換目錄。',
|
'diffView.state.notGitRepository': '目前不是 Git 儲存庫。請在 Git 標籤中初始化或切換目錄。',
|
||||||
'diffView.state.cleanWorkingTree': '工作區乾淨,沒有可顯示的改動',
|
'diffView.state.cleanWorkingTree': '工作區乾淨,沒有可顯示的改動',
|
||||||
|
'diffView.state.noLastTurnChanges': '沒有可顯示的上一輪變更',
|
||||||
'diffView.state.failedToLoadDiff': '載入差異失敗',
|
'diffView.state.failedToLoadDiff': '載入差異失敗',
|
||||||
'diffView.state.loadingDiff': '正在載入差異...',
|
'diffView.state.loadingDiff': '正在載入差異...',
|
||||||
'diffView.state.loadingChanges': '正在載入變更...',
|
'diffView.state.loadingChanges': '正在載入變更...',
|
||||||
@@ -1239,6 +1240,7 @@ export const dict: Record<I18nKey, string> = {
|
|||||||
'diffView.summary.changedFilesPlural': '{count} 個檔案已變更',
|
'diffView.summary.changedFilesPlural': '{count} 個檔案已變更',
|
||||||
"diffView.scope.changed": "已變更",
|
"diffView.scope.changed": "已變更",
|
||||||
"diffView.scope.staged": "已暫存",
|
"diffView.scope.staged": "已暫存",
|
||||||
|
"diffView.scope.lastTurn": "上一輪",
|
||||||
"diffView.scope.selectorAria": "選擇變更模式",
|
"diffView.scope.selectorAria": "選擇變更模式",
|
||||||
'diffView.actions.retry': '重試',
|
'diffView.actions.retry': '重試',
|
||||||
'diffView.actions.renderAnyway': '仍然渲染',
|
'diffView.actions.renderAnyway': '仍然渲染',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getStoredMobileKeyboardMode, type MobileKeyboardMode } from '@/lib/mobi
|
|||||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||||
|
|
||||||
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context' | 'diagram';
|
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files' | 'context' | 'diagram';
|
||||||
|
export type PendingDiffScope = 'working' | 'staged' | 'turn';
|
||||||
export type RightSidebarTab = 'git' | 'files' | 'context';
|
export type RightSidebarTab = 'git' | 'files' | 'context';
|
||||||
export type ContextPanelMode = 'diff' | 'file' | 'context' | 'plan' | 'chat' | 'preview' | 'browser';
|
export type ContextPanelMode = 'diff' | 'file' | 'context' | 'plan' | 'chat' | 'preview' | 'browser';
|
||||||
export type MermaidRenderingMode = 'svg' | 'ascii';
|
export type MermaidRenderingMode = 'svg' | 'ascii';
|
||||||
@@ -34,6 +35,7 @@ type ContextPanelTab = {
|
|||||||
sessionTitleFallback: string | null;
|
sessionTitleFallback: string | null;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
stagedDiff: boolean;
|
stagedDiff: boolean;
|
||||||
|
diffScope: PendingDiffScope | null;
|
||||||
touchedAt: number;
|
touchedAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@ type ContextPanelTabDescriptor = {
|
|||||||
sessionTitleFallback?: string | null;
|
sessionTitleFallback?: string | null;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
stagedDiff?: boolean;
|
stagedDiff?: boolean;
|
||||||
|
diffScope?: PendingDiffScope | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextPanelDirectoryState = {
|
type ContextPanelDirectoryState = {
|
||||||
@@ -177,6 +180,10 @@ const normalizeContextTabLabel = (value: string | null | undefined): string | nu
|
|||||||
: trimmed;
|
: trimmed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizePendingDiffScope = (value: unknown): PendingDiffScope | null => {
|
||||||
|
return value === 'working' || value === 'staged' || value === 'turn' ? value : null;
|
||||||
|
};
|
||||||
|
|
||||||
const buildDefaultContextPanelTabDedupeKey = (mode: ContextPanelMode, targetPath: string | null): string => {
|
const buildDefaultContextPanelTabDedupeKey = (mode: ContextPanelMode, targetPath: string | null): string => {
|
||||||
if (mode === 'file') {
|
if (mode === 'file') {
|
||||||
return targetPath || mode;
|
return targetPath || mode;
|
||||||
@@ -228,6 +235,7 @@ const createContextPanelTab = (descriptor: ContextPanelTabDescriptor): ContextPa
|
|||||||
sessionTitleFallback: normalizeContextTabLabel(descriptor.sessionTitleFallback),
|
sessionTitleFallback: normalizeContextTabLabel(descriptor.sessionTitleFallback),
|
||||||
readOnly: descriptor.readOnly === true,
|
readOnly: descriptor.readOnly === true,
|
||||||
stagedDiff: descriptor.stagedDiff === true,
|
stagedDiff: descriptor.stagedDiff === true,
|
||||||
|
diffScope: normalizePendingDiffScope(descriptor.diffScope) ?? (descriptor.stagedDiff === true ? 'staged' : 'working'),
|
||||||
touchedAt: Date.now(),
|
touchedAt: Date.now(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -269,6 +277,7 @@ const sanitizeContextPanelTabs = (tabs: unknown): ContextPanelTab[] => {
|
|||||||
sessionTitleFallback?: unknown;
|
sessionTitleFallback?: unknown;
|
||||||
readOnly?: unknown;
|
readOnly?: unknown;
|
||||||
stagedDiff?: unknown;
|
stagedDiff?: unknown;
|
||||||
|
diffScope?: unknown;
|
||||||
touchedAt?: unknown;
|
touchedAt?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -297,6 +306,7 @@ const sanitizeContextPanelTabs = (tabs: unknown): ContextPanelTab[] => {
|
|||||||
sessionTitleFallback: normalizeContextTabLabel(typeof candidate.sessionTitleFallback === 'string' ? candidate.sessionTitleFallback : null),
|
sessionTitleFallback: normalizeContextTabLabel(typeof candidate.sessionTitleFallback === 'string' ? candidate.sessionTitleFallback : null),
|
||||||
readOnly: candidate.readOnly === true,
|
readOnly: candidate.readOnly === true,
|
||||||
stagedDiff: candidate.stagedDiff === true,
|
stagedDiff: candidate.stagedDiff === true,
|
||||||
|
diffScope: normalizePendingDiffScope(candidate.diffScope) ?? (candidate.stagedDiff === true ? 'staged' : 'working'),
|
||||||
touchedAt: typeof candidate.touchedAt === 'number' && Number.isFinite(candidate.touchedAt)
|
touchedAt: typeof candidate.touchedAt === 'number' && Number.isFinite(candidate.touchedAt)
|
||||||
? candidate.touchedAt
|
? candidate.touchedAt
|
||||||
: Date.now(),
|
: Date.now(),
|
||||||
@@ -357,6 +367,7 @@ const upsertContextPanelTab = (
|
|||||||
label: nextTab.label,
|
label: nextTab.label,
|
||||||
sessionTitleFallback: nextTab.sessionTitleFallback || tab.sessionTitleFallback,
|
sessionTitleFallback: nextTab.sessionTitleFallback || tab.sessionTitleFallback,
|
||||||
stagedDiff: nextTab.stagedDiff,
|
stagedDiff: nextTab.stagedDiff,
|
||||||
|
diffScope: nextTab.diffScope,
|
||||||
readOnly: nextTab.readOnly,
|
readOnly: nextTab.readOnly,
|
||||||
touchedAt: Date.now(),
|
touchedAt: Date.now(),
|
||||||
}
|
}
|
||||||
@@ -536,6 +547,7 @@ interface UIStore {
|
|||||||
sidebarOpenBeforeFullscreenTab: boolean | null;
|
sidebarOpenBeforeFullscreenTab: boolean | null;
|
||||||
pendingDiffFile: string | null;
|
pendingDiffFile: string | null;
|
||||||
pendingDiffStaged: boolean;
|
pendingDiffStaged: boolean;
|
||||||
|
pendingDiffScope: PendingDiffScope | null;
|
||||||
pendingDiagramFile: string | null;
|
pendingDiagramFile: string | null;
|
||||||
pendingFileNavigation: PendingFileNavigation | null;
|
pendingFileNavigation: PendingFileNavigation | null;
|
||||||
pendingFileFocusPath: string | null;
|
pendingFileFocusPath: string | null;
|
||||||
@@ -656,7 +668,7 @@ interface UIStore {
|
|||||||
setRightSidebarWidth: (width: number) => void;
|
setRightSidebarWidth: (width: number) => void;
|
||||||
setRightSidebarTab: (tab: RightSidebarTab) => void;
|
setRightSidebarTab: (tab: RightSidebarTab) => void;
|
||||||
openContextPanelTab: (directory: string, tab: ContextPanelTabDescriptor) => void;
|
openContextPanelTab: (directory: string, tab: ContextPanelTabDescriptor) => void;
|
||||||
openContextDiff: (directory: string, filePath: string, staged?: boolean) => void;
|
openContextDiff: (directory: string, filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void;
|
||||||
openContextFile: (directory: string, filePath: string) => void;
|
openContextFile: (directory: string, filePath: string) => void;
|
||||||
openContextFileAtLine: (directory: string, filePath: string, line: number, column?: number) => void;
|
openContextFileAtLine: (directory: string, filePath: string, line: number, column?: number) => void;
|
||||||
openContextOverview: (directory: string) => void;
|
openContextOverview: (directory: string) => void;
|
||||||
@@ -682,11 +694,11 @@ interface UIStore {
|
|||||||
prepareForRuntimeSwitch: (runtimeKey?: string | null) => void;
|
prepareForRuntimeSwitch: (runtimeKey?: string | null) => void;
|
||||||
restoreForRuntimeSwitch: (runtimeKey?: string | null) => void;
|
restoreForRuntimeSwitch: (runtimeKey?: string | null) => void;
|
||||||
setMainTabGuard: (guard: MainTabGuard | null) => void;
|
setMainTabGuard: (guard: MainTabGuard | null) => void;
|
||||||
setPendingDiffFile: (filePath: string | null, staged?: boolean) => void;
|
setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void;
|
||||||
setPendingDiagramFile: (filePath: string | null) => void;
|
setPendingDiagramFile: (filePath: string | null) => void;
|
||||||
setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void;
|
setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void;
|
||||||
setPendingFileFocusPath: (path: string | null) => void;
|
setPendingFileFocusPath: (path: string | null) => void;
|
||||||
navigateToDiff: (filePath: string, staged?: boolean) => void;
|
navigateToDiff: (filePath: string, staged?: boolean, scope?: PendingDiffScope | null) => void;
|
||||||
consumePendingDiffFile: () => string | null;
|
consumePendingDiffFile: () => string | null;
|
||||||
navigateToDiagram: (filePath: string) => void;
|
navigateToDiagram: (filePath: string) => void;
|
||||||
consumePendingDiagramFile: () => string | null;
|
consumePendingDiagramFile: () => string | null;
|
||||||
@@ -833,6 +845,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
sidebarOpenBeforeFullscreenTab: null,
|
sidebarOpenBeforeFullscreenTab: null,
|
||||||
pendingDiffFile: null,
|
pendingDiffFile: null,
|
||||||
pendingDiffStaged: false,
|
pendingDiffStaged: false,
|
||||||
|
pendingDiffScope: null,
|
||||||
pendingDiagramFile: null,
|
pendingDiagramFile: null,
|
||||||
pendingFileNavigation: null,
|
pendingFileNavigation: null,
|
||||||
pendingFileFocusPath: null,
|
pendingFileFocusPath: null,
|
||||||
@@ -1042,17 +1055,20 @@ export const useUIStore = create<UIStore>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openContextDiff: (directory, filePath, staged = false) => {
|
openContextDiff: (directory, filePath, staged = false, scope = null) => {
|
||||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||||
const normalizedFilePath = (filePath || '').trim();
|
const normalizedFilePath = (filePath || '').trim();
|
||||||
if (!normalizedDirectory || !normalizedFilePath) {
|
if (!normalizedDirectory || !normalizedFilePath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const diffScope = normalizePendingDiffScope(scope) ?? (staged ? 'staged' : 'working');
|
||||||
|
|
||||||
get().openContextPanelTab(normalizedDirectory, {
|
get().openContextPanelTab(normalizedDirectory, {
|
||||||
mode: 'diff',
|
mode: 'diff',
|
||||||
targetPath: normalizedFilePath,
|
targetPath: normalizedFilePath,
|
||||||
stagedDiff: staged,
|
stagedDiff: diffScope === 'staged',
|
||||||
|
diffScope,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1414,8 +1430,12 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ activeMainTab: restored });
|
set({ activeMainTab: restored });
|
||||||
},
|
},
|
||||||
|
|
||||||
setPendingDiffFile: (filePath, staged = false) => {
|
setPendingDiffFile: (filePath, staged = false, scope = null) => {
|
||||||
set({ pendingDiffFile: filePath, pendingDiffStaged: filePath ? staged : false });
|
set({
|
||||||
|
pendingDiffFile: filePath,
|
||||||
|
pendingDiffStaged: filePath ? staged : false,
|
||||||
|
pendingDiffScope: filePath ? scope : null,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setPendingDiagramFile: (filePath) => {
|
setPendingDiagramFile: (filePath) => {
|
||||||
@@ -1430,18 +1450,18 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ pendingFileFocusPath: path });
|
set({ pendingFileFocusPath: path });
|
||||||
},
|
},
|
||||||
|
|
||||||
navigateToDiff: (filePath, staged = false) => {
|
navigateToDiff: (filePath, staged = false, scope = null) => {
|
||||||
const guard = get().mainTabGuard;
|
const guard = get().mainTabGuard;
|
||||||
if (guard && !guard('diff')) {
|
if (guard && !guard('diff')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, activeMainTab: 'diff' });
|
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeMainTab: 'diff' });
|
||||||
},
|
},
|
||||||
|
|
||||||
consumePendingDiffFile: () => {
|
consumePendingDiffFile: () => {
|
||||||
const { pendingDiffFile } = get();
|
const { pendingDiffFile } = get();
|
||||||
if (pendingDiffFile) {
|
if (pendingDiffFile) {
|
||||||
set({ pendingDiffFile: null, pendingDiffStaged: false });
|
set({ pendingDiffFile: null, pendingDiffStaged: false, pendingDiffScope: null });
|
||||||
}
|
}
|
||||||
return pendingDiffFile;
|
return pendingDiffFile;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user