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:
@@ -10,6 +10,7 @@ import { getStoredMobileKeyboardMode, type MobileKeyboardMode } from '@/lib/mobi
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
|
||||
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 ContextPanelMode = 'diff' | 'file' | 'context' | 'plan' | 'chat' | 'preview' | 'browser';
|
||||
export type MermaidRenderingMode = 'svg' | 'ascii';
|
||||
@@ -34,6 +35,7 @@ type ContextPanelTab = {
|
||||
sessionTitleFallback: string | null;
|
||||
readOnly: boolean;
|
||||
stagedDiff: boolean;
|
||||
diffScope: PendingDiffScope | null;
|
||||
touchedAt: number;
|
||||
};
|
||||
|
||||
@@ -45,6 +47,7 @@ type ContextPanelTabDescriptor = {
|
||||
sessionTitleFallback?: string | null;
|
||||
readOnly?: boolean;
|
||||
stagedDiff?: boolean;
|
||||
diffScope?: PendingDiffScope | null;
|
||||
};
|
||||
|
||||
type ContextPanelDirectoryState = {
|
||||
@@ -177,6 +180,10 @@ const normalizeContextTabLabel = (value: string | null | undefined): string | nu
|
||||
: trimmed;
|
||||
};
|
||||
|
||||
const normalizePendingDiffScope = (value: unknown): PendingDiffScope | null => {
|
||||
return value === 'working' || value === 'staged' || value === 'turn' ? value : null;
|
||||
};
|
||||
|
||||
const buildDefaultContextPanelTabDedupeKey = (mode: ContextPanelMode, targetPath: string | null): string => {
|
||||
if (mode === 'file') {
|
||||
return targetPath || mode;
|
||||
@@ -228,6 +235,7 @@ const createContextPanelTab = (descriptor: ContextPanelTabDescriptor): ContextPa
|
||||
sessionTitleFallback: normalizeContextTabLabel(descriptor.sessionTitleFallback),
|
||||
readOnly: descriptor.readOnly === true,
|
||||
stagedDiff: descriptor.stagedDiff === true,
|
||||
diffScope: normalizePendingDiffScope(descriptor.diffScope) ?? (descriptor.stagedDiff === true ? 'staged' : 'working'),
|
||||
touchedAt: Date.now(),
|
||||
};
|
||||
};
|
||||
@@ -269,6 +277,7 @@ const sanitizeContextPanelTabs = (tabs: unknown): ContextPanelTab[] => {
|
||||
sessionTitleFallback?: unknown;
|
||||
readOnly?: unknown;
|
||||
stagedDiff?: unknown;
|
||||
diffScope?: unknown;
|
||||
touchedAt?: unknown;
|
||||
};
|
||||
|
||||
@@ -297,6 +306,7 @@ const sanitizeContextPanelTabs = (tabs: unknown): ContextPanelTab[] => {
|
||||
sessionTitleFallback: normalizeContextTabLabel(typeof candidate.sessionTitleFallback === 'string' ? candidate.sessionTitleFallback : null),
|
||||
readOnly: candidate.readOnly === true,
|
||||
stagedDiff: candidate.stagedDiff === true,
|
||||
diffScope: normalizePendingDiffScope(candidate.diffScope) ?? (candidate.stagedDiff === true ? 'staged' : 'working'),
|
||||
touchedAt: typeof candidate.touchedAt === 'number' && Number.isFinite(candidate.touchedAt)
|
||||
? candidate.touchedAt
|
||||
: Date.now(),
|
||||
@@ -357,6 +367,7 @@ const upsertContextPanelTab = (
|
||||
label: nextTab.label,
|
||||
sessionTitleFallback: nextTab.sessionTitleFallback || tab.sessionTitleFallback,
|
||||
stagedDiff: nextTab.stagedDiff,
|
||||
diffScope: nextTab.diffScope,
|
||||
readOnly: nextTab.readOnly,
|
||||
touchedAt: Date.now(),
|
||||
}
|
||||
@@ -536,6 +547,7 @@ interface UIStore {
|
||||
sidebarOpenBeforeFullscreenTab: boolean | null;
|
||||
pendingDiffFile: string | null;
|
||||
pendingDiffStaged: boolean;
|
||||
pendingDiffScope: PendingDiffScope | null;
|
||||
pendingDiagramFile: string | null;
|
||||
pendingFileNavigation: PendingFileNavigation | null;
|
||||
pendingFileFocusPath: string | null;
|
||||
@@ -656,7 +668,7 @@ interface UIStore {
|
||||
setRightSidebarWidth: (width: number) => void;
|
||||
setRightSidebarTab: (tab: RightSidebarTab) => 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;
|
||||
openContextFileAtLine: (directory: string, filePath: string, line: number, column?: number) => void;
|
||||
openContextOverview: (directory: string) => void;
|
||||
@@ -682,11 +694,11 @@ interface UIStore {
|
||||
prepareForRuntimeSwitch: (runtimeKey?: string | null) => void;
|
||||
restoreForRuntimeSwitch: (runtimeKey?: string | 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;
|
||||
setPendingFileNavigation: (navigation: PendingFileNavigation | 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;
|
||||
navigateToDiagram: (filePath: string) => void;
|
||||
consumePendingDiagramFile: () => string | null;
|
||||
@@ -833,6 +845,7 @@ export const useUIStore = create<UIStore>()(
|
||||
sidebarOpenBeforeFullscreenTab: null,
|
||||
pendingDiffFile: null,
|
||||
pendingDiffStaged: false,
|
||||
pendingDiffScope: null,
|
||||
pendingDiagramFile: null,
|
||||
pendingFileNavigation: 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 normalizedFilePath = (filePath || '').trim();
|
||||
if (!normalizedDirectory || !normalizedFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diffScope = normalizePendingDiffScope(scope) ?? (staged ? 'staged' : 'working');
|
||||
|
||||
get().openContextPanelTab(normalizedDirectory, {
|
||||
mode: 'diff',
|
||||
targetPath: normalizedFilePath,
|
||||
stagedDiff: staged,
|
||||
stagedDiff: diffScope === 'staged',
|
||||
diffScope,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1414,8 +1430,12 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ activeMainTab: restored });
|
||||
},
|
||||
|
||||
setPendingDiffFile: (filePath, staged = false) => {
|
||||
set({ pendingDiffFile: filePath, pendingDiffStaged: filePath ? staged : false });
|
||||
setPendingDiffFile: (filePath, staged = false, scope = null) => {
|
||||
set({
|
||||
pendingDiffFile: filePath,
|
||||
pendingDiffStaged: filePath ? staged : false,
|
||||
pendingDiffScope: filePath ? scope : null,
|
||||
});
|
||||
},
|
||||
|
||||
setPendingDiagramFile: (filePath) => {
|
||||
@@ -1430,18 +1450,18 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ pendingFileFocusPath: path });
|
||||
},
|
||||
|
||||
navigateToDiff: (filePath, staged = false) => {
|
||||
navigateToDiff: (filePath, staged = false, scope = null) => {
|
||||
const guard = get().mainTabGuard;
|
||||
if (guard && !guard('diff')) {
|
||||
return;
|
||||
}
|
||||
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, activeMainTab: 'diff' });
|
||||
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, pendingDiffScope: scope, activeMainTab: 'diff' });
|
||||
},
|
||||
|
||||
consumePendingDiffFile: () => {
|
||||
const { pendingDiffFile } = get();
|
||||
if (pendingDiffFile) {
|
||||
set({ pendingDiffFile: null, pendingDiffStaged: false });
|
||||
set({ pendingDiffFile: null, pendingDiffStaged: false, pendingDiffScope: null });
|
||||
}
|
||||
return pendingDiffFile;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user