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
This commit is contained in:
@@ -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<DirtyWorkspace> {
|
||||
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<DirtyWorkspace> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.id));
|
||||
|
||||
@@ -340,6 +340,7 @@ function ChangedFilesPane({
|
||||
workspaceId={workspaceId}
|
||||
cwd={workspaceRoot}
|
||||
enabled={isOpen}
|
||||
modeScope="compact-explorer"
|
||||
onOpenFile={onOpenFile}
|
||||
onAddToChat={canAddToChat ? addFile : undefined}
|
||||
state={changesState}
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>{emptyMessage}</Text>
|
||||
{emptyAction ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
testID="changes-empty-switch-mode"
|
||||
onPress={emptyAction.onPress}
|
||||
>
|
||||
{emptyAction.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -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 = (
|
||||
<DiffBodyContent
|
||||
@@ -1343,6 +1387,7 @@ export function ChangesSurface({
|
||||
diffTooLarge={diffTooLarge}
|
||||
hasChanges={hasChanges}
|
||||
emptyMessage={emptyMessage}
|
||||
emptyAction={emptyAction}
|
||||
checkingRepositoryLabel={t("workspace.git.diff.checkingRepository")}
|
||||
notRepositoryLabel={t("workspace.git.diff.notRepository")}
|
||||
>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -874,6 +874,8 @@ export const ar: TranslationResources = {
|
||||
failedRefresh: "فشل تحديث حالة git.",
|
||||
emptyHiddenWhitespace: "لا توجد تغييرات مرئية بعد إخفاء المسافة البيضاء",
|
||||
emptyUncommitted: "لا توجد تغييرات غير ملتزم بها",
|
||||
seeUncommittedChanges: "عرض التغييرات غير الملتزم بها",
|
||||
seeCommittedChanges: "عرض التغييرات الملتزم بها",
|
||||
emptyAgainstBase: "لا توجد تغييرات مقابل{{baseRef}}",
|
||||
checkingRepository: "فحص المستودع...",
|
||||
notRepository: "ليس مستودع جيت",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -885,6 +885,8 @@ export const ja: TranslationResources = {
|
||||
failedRefresh: "gitの状態の更新に失敗しました。",
|
||||
emptyHiddenWhitespace: "空白を非表示にすると変更は表示されません",
|
||||
emptyUncommitted: "未コミットの変更なし",
|
||||
seeUncommittedChanges: "未コミットの変更を表示",
|
||||
seeCommittedChanges: "コミット済みの変更を表示",
|
||||
emptyAgainstBase: "{{baseRef}}との差分なし",
|
||||
checkingRepository: "リポジトリを確認中...",
|
||||
notRepository: "gitリポジトリではありません",
|
||||
|
||||
@@ -881,6 +881,8 @@ export const ko: TranslationResources = {
|
||||
failedRefresh: "Git 상태를 새로고침하지 못했습니다.",
|
||||
emptyHiddenWhitespace: "공백을 숨긴 후 표시할 변경 사항이 없습니다",
|
||||
emptyUncommitted: "커밋되지 않은 변경 사항이 없습니다",
|
||||
seeUncommittedChanges: "커밋되지 않은 변경 사항 보기",
|
||||
seeCommittedChanges: "커밋된 변경 사항 보기",
|
||||
emptyAgainstBase: "{{baseRef}} 대비 변경 사항이 없습니다",
|
||||
checkingRepository: "저장소 확인 중...",
|
||||
notRepository: "Git 저장소가 아닙니다",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -889,6 +889,8 @@ export const ru: TranslationResources = {
|
||||
failedRefresh: "Не удалось обновить состояние Git.",
|
||||
emptyHiddenWhitespace: "После скрытия пробельных изменений видимых изменений нет",
|
||||
emptyUncommitted: "Нет незафиксированных изменений",
|
||||
seeUncommittedChanges: "Показать незафиксированные изменения",
|
||||
seeCommittedChanges: "Показать зафиксированные изменения",
|
||||
emptyAgainstBase: "Нет изменений относительно {{baseRef}}",
|
||||
checkingRepository: "Проверяем репозиторий...",
|
||||
notRepository: "Это не репозиторий Git",
|
||||
|
||||
@@ -866,6 +866,8 @@ export const zhCN: TranslationResources = {
|
||||
failedRefresh: "刷新 git 状态失败。",
|
||||
emptyHiddenWhitespace: "隐藏空白差异后没有可见变更",
|
||||
emptyUncommitted: "没有未 commit 的变更",
|
||||
seeUncommittedChanges: "查看未 commit 的变更",
|
||||
seeCommittedChanges: "查看已 commit 的变更",
|
||||
emptyAgainstBase: "相对于 {{baseRef}} 没有变更",
|
||||
checkingRepository: "正在检查 repository...",
|
||||
notRepository: "不是 git repository",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<typeof changesStateSchema>;
|
||||
|
||||
export const defaultChangesState: ChangesState = {
|
||||
mode: "uncommitted",
|
||||
layout: "unified",
|
||||
wrapLines: false,
|
||||
hideWhitespace: false,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user