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:
Mohamed Boudra
2026-08-21 11:23:43 +02:00
committed by GitHub
parent 04624cb419
commit a7f3bf7abb
17 changed files with 228 additions and 45 deletions
+54 -8
View File
@@ -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,
+33 -16
View File
@@ -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,