From a7f3bf7abb61a4cf5bf2d5dcd623be4103dc2620 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 21 Aug 2026 11:23:43 +0200 Subject: [PATCH] fix(app): open Changes on the populated comparison (#3636) * fix(app): restore smart Changes comparison selection The tab migration persisted the selected comparison, pinning Changes to an empty view after checkout state changed. Keep comparison overrides ephemeral per surface and expire them when dirtiness changes, while preserving only presentation state in tabs. * chore(app): date Changes compatibility cleanup --- packages/app/e2e/browser/changes-pane.spec.ts | 84 +++++++++++++++++++ .../components/compact-explorer-sidebar.tsx | 1 + packages/app/src/git/diff-pane.tsx | 62 ++++++++++++-- packages/app/src/git/use-working-diff.ts | 49 +++++++---- packages/app/src/i18n/resources/ar.ts | 2 + packages/app/src/i18n/resources/en.ts | 2 + packages/app/src/i18n/resources/es.ts | 2 + packages/app/src/i18n/resources/fr.ts | 2 + packages/app/src/i18n/resources/ja.ts | 2 + packages/app/src/i18n/resources/ko.ts | 2 + packages/app/src/i18n/resources/pt-BR.ts | 2 + packages/app/src/i18n/resources/ru.ts | 2 + packages/app/src/i18n/resources/zh-CN.ts | 2 + packages/app/src/panels/changes/state.test.ts | 20 +++++ packages/app/src/panels/changes/state.ts | 28 ++++--- packages/app/src/panels/diff-panel.tsx | 3 +- .../src/stores/workspace-layout-store.test.ts | 8 +- 17 files changed, 228 insertions(+), 45 deletions(-) create mode 100644 packages/app/src/panels/changes/state.test.ts diff --git a/packages/app/e2e/browser/changes-pane.spec.ts b/packages/app/e2e/browser/changes-pane.spec.ts index c77ab6b52..4550f043a 100644 --- a/packages/app/e2e/browser/changes-pane.spec.ts +++ b/packages/app/e2e/browser/changes-pane.spec.ts @@ -258,6 +258,61 @@ test.afterEach(async () => { } }); +test("Changes opens the populated committed comparison for a clean checkout", async ({ page }) => { + const workspace = await createWorkspaceWithCommittedDiff(); + + await openWorkspaceChangesSurface(page, workspace); + + const panel = page.getByTestId("working-diff-panel").filter({ visible: true }); + await expect(panel.getByTestId("changes-diff-status-trigger")).toContainText("Committed"); + await expect(panel.getByText("committed-only.ts", { exact: true })).toBeVisible(); +}); + +test("Changes expires a manual comparison when checkout dirtiness changes", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff(); + await openWorkspaceChanges(page, workspace); + + const panel = page.getByTestId("working-diff-panel").filter({ visible: true }); + const mode = panel.getByTestId("changes-diff-status-trigger"); + await expect(mode).toContainText("Uncommitted"); + + await mode.click(); + await page.getByTestId("changes-diff-mode-committed").click(); + await expect(mode).toContainText("Committed"); + await expect(panel.getByRole("button", { name: "See uncommitted changes" })).toBeVisible(); + + execFileSync("git", ["add", "--all"], { cwd: workspace.repoPath }); + execFileSync("git", ["commit", "-m", "Commit working changes"], { cwd: workspace.repoPath }); + await expect(mode).toContainText("Committed"); + await expect(panel.getByRole("button", { name: "See uncommitted changes" })).toHaveCount(0, { + timeout: 30_000, + }); + + await writeFile(path.join(workspace.repoPath, "new-working-change.txt"), "uncommitted\n"); + await expect(mode).toContainText("Uncommitted", { timeout: 30_000 }); +}); + +test("an empty Changes comparison links to the populated comparison", async ({ page }) => { + const workspace = await createWorkspaceWithCommittedDiff(); + await openWorkspaceChangesSurface(page, workspace); + + const panel = page.getByTestId("working-diff-panel").filter({ visible: true }); + const mode = panel.getByTestId("changes-diff-status-trigger"); + await mode.click(); + await page.getByTestId("changes-diff-mode-committed").click(); + await expect(panel.getByText("committed-only.ts", { exact: true })).toBeVisible(); + + await mode.click(); + await page.getByTestId("changes-diff-mode-uncommitted").click(); + + await expect(panel.getByText("No uncommitted changes", { exact: true })).toBeVisible(); + const seeCommitted = panel.getByRole("button", { name: "See committed changes" }); + await expect(seeCommitted).toBeVisible(); + await seeCommitted.click(); + await expect(mode).toContainText("Committed"); + await expect(panel.getByText("committed-only.ts", { exact: true })).toBeVisible(); +}); + test("changes file actions open below the right-click without a reserved kebab", async ({ page, }) => { @@ -1365,6 +1420,28 @@ async function createWorkspaceWithMountedTabDiff( return { id: createdWorkspace.workspace.id, repoPath: repo.path }; } +async function createWorkspaceWithCommittedDiff(): Promise { + const repo = await createTempGitRepo("changes-committed-", { + files: [{ path: "tracked.ts", content: "export const tracked = 1;\n" }], + }); + const client = await connectSeedClient(); + cleanupTasks.push({ + run: async () => { + await client.close().catch(() => undefined); + await repo.cleanup().catch(() => undefined); + }, + }); + + execFileSync("git", ["checkout", "-b", "feature"], { cwd: repo.path }); + await writeFile(path.join(repo.path, "committed-only.ts"), "export const committed = true;\n"); + execFileSync("git", ["add", "committed-only.ts"], { cwd: repo.path }); + execFileSync("git", ["commit", "-m", "Add committed-only file"], { cwd: repo.path }); + + const created = await client.createWorkspace({ source: { kind: "directory", path: repo.path } }); + if (!created.workspace) throw new Error(created.error ?? "Failed to create committed workspace"); + return { id: created.workspace.id, repoPath: repo.path }; +} + async function createWorkspaceWithExactSelectionDiff(content: string): Promise { const repo = await createTempGitRepo("changes-canvas-selection-", { files: [{ path: "src/selection.ts", content: "" }], @@ -1390,6 +1467,13 @@ async function openWorkspaceChanges(page: Page, workspace: DirtyWorkspace): Prom await expectExpandedMountedTabDiff(page); } +async function openWorkspaceChangesSurface(page: Page, workspace: DirtyWorkspace): Promise { + await page.setViewportSize({ width: 1400, height: 900 }); + await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.id)); + await waitForWorkspaceTabsVisible(page); + await openChangesPanel(page); +} + async function openSelectionWorkspaceChanges(page: Page, workspace: DirtyWorkspace): Promise { await page.setViewportSize({ width: 1400, height: 900 }); await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.id)); diff --git a/packages/app/src/components/compact-explorer-sidebar.tsx b/packages/app/src/components/compact-explorer-sidebar.tsx index 23af1f161..788a379b8 100644 --- a/packages/app/src/components/compact-explorer-sidebar.tsx +++ b/packages/app/src/components/compact-explorer-sidebar.tsx @@ -340,6 +340,7 @@ function ChangedFilesPane({ workspaceId={workspaceId} cwd={workspaceRoot} enabled={isOpen} + modeScope="compact-explorer" onOpenFile={onOpenFile} onAddToChat={canAddToChat ? addFile : undefined} state={changesState} diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx index 55f024972..0e557516e 100644 --- a/packages/app/src/git/diff-pane.tsx +++ b/packages/app/src/git/diff-pane.tsx @@ -69,6 +69,7 @@ import { useToast } from "@/contexts/toast-context"; import { useSessionStore } from "@/stores/session-store"; import { confirmDialog } from "@/utils/confirm-dialog"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { Button } from "@/components/ui/button"; import { PaneContentToolbar, paneContentToolbarIconSize, @@ -80,6 +81,7 @@ import type { WorkspaceTabTarget } from "@/workspace-tabs/model"; import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model"; import { isWeb } from "@/constants/platform"; import { usePublishWorkingDiffAttachment, useWorkingDiff } from "@/git/use-working-diff"; +import type { CheckoutStatusPayload } from "@/git/use-status-query"; import { DiffTooLargeState } from "@/git/diff-too-large-state"; import { openDesktopTarget, useDesktopOpenTargets } from "@/workspace/desktop-open-targets"; @@ -165,6 +167,7 @@ interface ChangesSurfaceProps { cwd: string; enabled?: boolean; host: "explorer" | "panel"; + modeScope: string; focusPath?: string; focusRequestId?: number; onOpenFile?: (path: string) => void; @@ -607,6 +610,7 @@ interface DiffBodyContentProps { diffTooLarge: boolean; hasChanges: boolean; emptyMessage: string; + emptyAction: ChangesEmptyAction | null; children: ReactElement; checkingRepositoryLabel: string; notRepositoryLabel: string; @@ -621,6 +625,7 @@ function DiffBodyContent({ diffTooLarge, hasChanges, emptyMessage, + emptyAction, children, checkingRepositoryLabel, notRepositoryLabel, @@ -668,6 +673,16 @@ function DiffBodyContent({ return ( {emptyMessage} + {emptyAction ? ( + + ) : null} ); } @@ -690,6 +705,32 @@ function computeCommittedDiffDescription( return branchLabel === baseRefLabel ? undefined : `${branchLabel} -> ${baseRefLabel}`; } +interface ChangesEmptyAction { + label: string; + onPress: () => void; +} + +function computeChangesEmptyAction(input: { + hideWhitespace: boolean; + diffMode: "uncommitted" | "base"; + status: CheckoutStatusPayload | null; + seeUncommittedLabel: string; + seeCommittedLabel: string; + selectUncommitted: () => void; + selectBase: () => void; +}): ChangesEmptyAction | null { + if (input.hideWhitespace || !input.status?.isGit) { + return null; + } + if (input.diffMode === "base" && input.status.isDirty) { + return { label: input.seeUncommittedLabel, onPress: input.selectUncommitted }; + } + if (input.diffMode === "uncommitted" && (input.status.aheadBehind?.ahead ?? 0) > 0) { + return { label: input.seeCommittedLabel, onPress: input.selectBase }; + } + return null; +} + function computePrErrorMessage( githubFeaturesEnabled: boolean, prPayloadError: { message?: string } | null | undefined, @@ -1012,6 +1053,7 @@ export function ChangesSurface({ cwd, enabled, host, + modeScope, focusPath, focusRequestId, onOpenFile, @@ -1059,11 +1101,6 @@ export function ChangesSurface({ layout: instanceState.layout === "unified" ? "split" : "unified", }); }, [instanceState, updateState]); - const handleDiffModeChange = useCallback( - (mode: "uncommitted" | "base") => updateState({ ...instanceState, mode }), - [instanceState, updateState], - ); - const codeFontSize = appSettings.codeFontSize; const overflowToggleStyle = useMemo(() => buildOverflowButtonStyle(isMobile), [isMobile]); @@ -1127,9 +1164,7 @@ export function ChangesSurface({ cwd, ignoreWhitespace: instanceState.hideWhitespace, enabled: enabled !== false, - mode: instanceState.mode, - baseRef: instanceState.baseRef, - onModeChange: handleDiffModeChange, + modeScope, }); usePublishWorkingDiffAttachment({ serverId, @@ -1332,6 +1367,15 @@ export function ChangesSurface({ uncommitted: t("workspace.git.diff.emptyUncommitted"), againstBase: (label) => t("workspace.git.diff.emptyAgainstBase", { baseRef: label }), }); + const emptyAction = computeChangesEmptyAction({ + hideWhitespace: instanceState.hideWhitespace, + diffMode, + status, + seeUncommittedLabel: t("workspace.git.diff.seeUncommittedChanges"), + seeCommittedLabel: t("workspace.git.diff.seeCommittedChanges"), + selectUncommitted: handleSelectUncommitted, + selectBase: handleSelectBase, + }); const diffContent: ReactElement = ( @@ -1549,6 +1594,7 @@ const styles = StyleSheet.create((theme) => ({ alignItems: "center", justifyContent: "center", paddingTop: theme.spacing[16], + gap: theme.spacing[2], }, emptyText: { fontSize: theme.fontSize.base, diff --git a/packages/app/src/git/use-working-diff.ts b/packages/app/src/git/use-working-diff.ts index 4e735a892..ed150aa81 100644 --- a/packages/app/src/git/use-working-diff.ts +++ b/packages/app/src/git/use-working-diff.ts @@ -5,8 +5,11 @@ import { } from "@/attachments/workspace-attachments-store"; import { buildReviewDraftKey, + buildReviewDraftScopeKey, useInlineReviewController, + useResolvedDiffMode, useReviewAttachmentSnapshot, + useSetDiffModeOverride, } from "@/review"; import { useCheckoutDiffQuery } from "@/git/use-diff-query"; import { useCheckoutStatusQuery } from "@/git/use-status-query"; @@ -18,9 +21,7 @@ interface UseWorkingDiffOptions { ignoreWhitespace: boolean; enabled: boolean; queryScope?: string; - mode?: "uncommitted" | "base"; - baseRef?: string; - onModeChange?: (mode: "uncommitted" | "base") => void; + modeScope: string; } export function useWorkingDiff({ @@ -30,9 +31,7 @@ export function useWorkingDiff({ ignoreWhitespace, enabled, queryScope, - mode, - baseRef: selectedBaseRef, - onModeChange, + modeScope, }: UseWorkingDiffOptions) { const { status, @@ -46,26 +45,44 @@ export function useWorkingDiff({ const statusErrorMessage = status?.error?.message ?? (isStatusError && statusError instanceof Error ? statusError.message : null); - const baseRef = selectedBaseRef ?? gitStatus?.baseRef ?? undefined; + const baseRef = gitStatus?.baseRef ?? undefined; const hasUncommittedChanges = Boolean(gitStatus?.isDirty); const currentBranchName = gitStatus?.currentBranch && gitStatus.currentBranch !== "HEAD" ? gitStatus.currentBranch : null; + const reviewDraftScopeKey = useMemo( + () => + buildReviewDraftScopeKey({ + serverId, + workspaceId, + cwd, + baseRef, + ignoreWhitespace, + }), + [baseRef, cwd, ignoreWhitespace, serverId, workspaceId], + ); + const modeScopeKey = `${reviewDraftScopeKey}:surface=${encodeURIComponent(modeScope)}`; + const diffMode = useResolvedDiffMode({ + scopeKey: modeScopeKey, + hasUncommittedChanges, + }); + const setDiffModeOverride = useSetDiffModeOverride(); const selectDiffMode = useCallback( (nextMode: "uncommitted" | "base") => { - if (onModeChange) { - onModeChange(nextMode); - return; - } - // A Changes surface always supplies its tab-owned presentation. Keep this - // fallback deterministic for legacy non-tab consumers without mutating - // workspace-wide review preferences. + setDiffModeOverride({ + scopeKey: modeScopeKey, + override: { + serverId, + cwd, + mode: nextMode, + isDirtyAtSelection: hasUncommittedChanges, + }, + }); }, - [onModeChange], + [cwd, hasUncommittedChanges, modeScopeKey, serverId, setDiffModeOverride], ); const selectUncommitted = useCallback(() => selectDiffMode("uncommitted"), [selectDiffMode]); const selectBase = useCallback(() => selectDiffMode("base"), [selectDiffMode]); - const diffMode = mode ?? (hasUncommittedChanges ? "uncommitted" : "base"); const { files, diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index dabdbbadd..2cb217efa 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -874,6 +874,8 @@ export const ar: TranslationResources = { failedRefresh: "فشل تحديث حالة git.", emptyHiddenWhitespace: "لا توجد تغييرات مرئية بعد إخفاء المسافة البيضاء", emptyUncommitted: "لا توجد تغييرات غير ملتزم بها", + seeUncommittedChanges: "عرض التغييرات غير الملتزم بها", + seeCommittedChanges: "عرض التغييرات الملتزم بها", emptyAgainstBase: "لا توجد تغييرات مقابل{{baseRef}}", checkingRepository: "فحص المستودع...", notRepository: "ليس مستودع جيت", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 46ba08dfb..ee1cc173d 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -883,6 +883,8 @@ export const en = { failedRefresh: "Failed to refresh git state.", emptyHiddenWhitespace: "No visible changes after hiding whitespace", emptyUncommitted: "No uncommitted changes", + seeUncommittedChanges: "See uncommitted changes", + seeCommittedChanges: "See committed changes", emptyAgainstBase: "No changes vs {{baseRef}}", checkingRepository: "Checking repository...", notRepository: "Not a git repository", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index c0a6bd090..4f861a484 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -905,6 +905,8 @@ export const es: TranslationResources = { failedRefresh: "No se pudo actualizar el estado de git.", emptyHiddenWhitespace: "No hay cambios visibles después de ocultar espacios en blanco", emptyUncommitted: "Sin cambios no confirmados", + seeUncommittedChanges: "Ver cambios no confirmados", + seeCommittedChanges: "Ver cambios confirmados", emptyAgainstBase: "Sin cambios frente a{{baseRef}}", checkingRepository: "Comprobando repositorio...", notRepository: "No es un repositorio de git", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 4ab55862b..4a9a4bd79 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -904,6 +904,8 @@ export const fr: TranslationResources = { failedRefresh: "Échec de l'actualisation de l'état git.", emptyHiddenWhitespace: "Aucun changement visible après avoir masqué les espaces", emptyUncommitted: "Aucune modification non validée", + seeUncommittedChanges: "Voir les modifications non validées", + seeCommittedChanges: "Voir les modifications validées", emptyAgainstBase: "Aucun changement par rapport à{{baseRef}}", checkingRepository: "Vérification du référentiel...", notRepository: "Pas un dépôt git", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index f11fb67ae..77f08e65c 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -885,6 +885,8 @@ export const ja: TranslationResources = { failedRefresh: "gitの状態の更新に失敗しました。", emptyHiddenWhitespace: "空白を非表示にすると変更は表示されません", emptyUncommitted: "未コミットの変更なし", + seeUncommittedChanges: "未コミットの変更を表示", + seeCommittedChanges: "コミット済みの変更を表示", emptyAgainstBase: "{{baseRef}}との差分なし", checkingRepository: "リポジトリを確認中...", notRepository: "gitリポジトリではありません", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index a9195eae4..2355bc997 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -881,6 +881,8 @@ export const ko: TranslationResources = { failedRefresh: "Git 상태를 새로고침하지 못했습니다.", emptyHiddenWhitespace: "공백을 숨긴 후 표시할 변경 사항이 없습니다", emptyUncommitted: "커밋되지 않은 변경 사항이 없습니다", + seeUncommittedChanges: "커밋되지 않은 변경 사항 보기", + seeCommittedChanges: "커밋된 변경 사항 보기", emptyAgainstBase: "{{baseRef}} 대비 변경 사항이 없습니다", checkingRepository: "저장소 확인 중...", notRepository: "Git 저장소가 아닙니다", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 68d586389..6f8fce141 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -896,6 +896,8 @@ export const ptBR: TranslationResources = { failedRefresh: "Falha ao atualizar estado do git.", emptyHiddenWhitespace: "Nenhuma alteração visível após ocultar espaços em branco", emptyUncommitted: "Nenhuma alteração sem commit", + seeUncommittedChanges: "Ver alterações sem commit", + seeCommittedChanges: "Ver alterações com commit", emptyAgainstBase: "Nenhuma alteração vs {{baseRef}}", checkingRepository: "Verificando repositório...", notRepository: "Não é um repositório git", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 191f83ff9..943650123 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -889,6 +889,8 @@ export const ru: TranslationResources = { failedRefresh: "Не удалось обновить состояние Git.", emptyHiddenWhitespace: "После скрытия пробельных изменений видимых изменений нет", emptyUncommitted: "Нет незафиксированных изменений", + seeUncommittedChanges: "Показать незафиксированные изменения", + seeCommittedChanges: "Показать зафиксированные изменения", emptyAgainstBase: "Нет изменений относительно {{baseRef}}", checkingRepository: "Проверяем репозиторий...", notRepository: "Это не репозиторий Git", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 40848d589..8ead2dd2b 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -866,6 +866,8 @@ export const zhCN: TranslationResources = { failedRefresh: "刷新 git 状态失败。", emptyHiddenWhitespace: "隐藏空白差异后没有可见变更", emptyUncommitted: "没有未 commit 的变更", + seeUncommittedChanges: "查看未 commit 的变更", + seeCommittedChanges: "查看已 commit 的变更", emptyAgainstBase: "相对于 {{baseRef}} 没有变更", checkingRepository: "正在检查 repository...", notRepository: "不是 git repository", diff --git a/packages/app/src/panels/changes/state.test.ts b/packages/app/src/panels/changes/state.test.ts new file mode 100644 index 000000000..0e9c52a83 --- /dev/null +++ b/packages/app/src/panels/changes/state.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { changesStateSchema, defaultChangesState } from "./state"; + +describe("Changes presentation state", () => { + it("drops the persisted mode without resetting the remaining presentation", () => { + expect( + changesStateSchema.parse({ + ...defaultChangesState, + mode: "base", + baseRef: "origin/main", + layout: "split", + wrapLines: true, + }), + ).toEqual({ + ...defaultChangesState, + layout: "split", + wrapLines: true, + }); + }); +}); diff --git a/packages/app/src/panels/changes/state.ts b/packages/app/src/panels/changes/state.ts index 5f715ed97..1b36b5dcb 100644 --- a/packages/app/src/panels/changes/state.ts +++ b/packages/app/src/panels/changes/state.ts @@ -1,22 +1,24 @@ import { z } from "zod"; -export const changesStateSchema = z.strictObject({ - mode: z.enum(["uncommitted", "base"]), - baseRef: z.string().optional(), - layout: z.enum(["unified", "split"]), - wrapLines: z.boolean(), - hideWhitespace: z.boolean(), - treeVisible: z.boolean(), - treeWidth: z.number().optional(), - collapsedFilePaths: z.array(z.string()), - collapsedFolderPaths: z.array(z.string()), - commitsCollapsed: z.boolean(), -}); +export const changesStateSchema = z + .strictObject({ + // COMPAT(changesModeState): fields persisted by v0.5.0-beta.3; remove after 2026-11-21. Mode selection is ephemeral. + mode: z.enum(["uncommitted", "base"]).optional(), + baseRef: z.string().optional(), + layout: z.enum(["unified", "split"]), + wrapLines: z.boolean(), + hideWhitespace: z.boolean(), + treeVisible: z.boolean(), + treeWidth: z.number().optional(), + collapsedFilePaths: z.array(z.string()), + collapsedFolderPaths: z.array(z.string()), + commitsCollapsed: z.boolean(), + }) + .transform(({ mode: _mode, baseRef: _baseRef, ...presentation }) => presentation); export type ChangesState = z.infer; export const defaultChangesState: ChangesState = { - mode: "uncommitted", layout: "unified", wrapLines: false, hideWhitespace: false, diff --git a/packages/app/src/panels/diff-panel.tsx b/packages/app/src/panels/diff-panel.tsx index 7014e99f8..763777c2c 100644 --- a/packages/app/src/panels/diff-panel.tsx +++ b/packages/app/src/panels/diff-panel.tsx @@ -77,7 +77,7 @@ function PanelState({ function WorkingDiffPanel() { const { t } = useTranslation(); - const { serverId, workspaceId, target, openFileInWorkspace } = usePaneContext(); + const { serverId, workspaceId, tabId, target, openFileInWorkspace } = usePaneContext(); const [changesState, setChangesState] = usePanelState(changesStateSchema, defaultChangesState); const cwd = useWorkspaceDirectory(serverId, workspaceId); const isActive = useRetainedPanelActive(); @@ -101,6 +101,7 @@ function WorkingDiffPanel() { cwd={cwd} enabled={isActive} host="panel" + modeScope={tabId} focusPath={target.focusPath} focusRequestId={target.focusRequestId} onOpenFile={handleOpenFile} diff --git a/packages/app/src/stores/workspace-layout-store.test.ts b/packages/app/src/stores/workspace-layout-store.test.ts index cba566fd3..1a806d96c 100644 --- a/packages/app/src/stores/workspace-layout-store.test.ts +++ b/packages/app/src/stores/workspace-layout-store.test.ts @@ -484,7 +484,7 @@ describe("workspace-layout-store actions", () => { ).toEqual(override); }); - it("keeps every Changes state field independent across explicit instances", () => { + it("keeps every Changes presentation field independent across explicit instances", () => { const workspaceKey = createWorkspaceKey(); const store = workspaceLayoutStore.getState(); const first = store.openTab({ @@ -498,8 +498,6 @@ describe("workspace-layout-store actions", () => { intent: "new", })!; const changed: ChangesState = { - mode: "base", - baseRef: "origin/main", layout: "split" as const, wrapLines: true, hideWhitespace: true, @@ -811,8 +809,6 @@ describe("workspace-layout-store actions", () => { .getState() .openTab({ workspaceKey: workspaceKey, target: { kind: "working_diff" }, intent: "new" })!; const firstState: ChangesState = { - mode: "base" as const, - baseRef: "origin/main", layout: "split" as const, wrapLines: true, hideWhitespace: true, @@ -823,8 +819,6 @@ describe("workspace-layout-store actions", () => { commitsCollapsed: false, }; const secondState: ChangesState = { - mode: "base" as const, - baseRef: "release", layout: "unified" as const, wrapLines: false, hideWhitespace: false,