authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-16 20:14:05-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-16 20:14:05-03:00
log394af73e02238e551db14f44d57f1469dd2942aa
treee4fa392e26c3c7ce2e41f20b529329571aec40ac
parented123897943e9175d4a0e92a5a8a09d200573336
signature Signed by PGP key B5690EEEBB952194

fix(web): default diff panel to working tree (#12139)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

5 files changed, 52 insertions(+), 31 deletions(-)

apps/web/src/components/ChatView.tsx+15-7
...@@ -1961,6 +1961,16 @@ export default function ChatView(props: ChatViewProps) {...@@ -1961,6 +1961,16 @@ export default function ChatView(props: ChatViewProps) {
1961 selectActiveRightPanel(state.byThreadKey, activeThreadRef),1961 selectActiveRightPanel(state.byThreadKey, activeThreadRef),
1962 );1962 );
1963 const diffOpen = activeRightPanelKind === "diff";1963 const diffOpen = activeRightPanelKind === "diff";
1964 const explicitDiffOpenRef = useRef<ScopedThreadRef | null>(null);
1965 useLayoutEffect(() => {
1966 const explicitThreadRef = explicitDiffOpenRef.current;
1967 explicitDiffOpenRef.current = null;
1968 // Generic openings always show the checkout, including tab fallbacks and thread changes.
1969 // A timeline click instead opens the specific turn/file the user requested.
1970 if (diffOpen && activeThreadRef && explicitThreadRef !== activeThreadRef) {
1971 useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
1972 }
1973 }, [activeThreadRef, diffOpen]);
1964 const rightPanelState = useRightPanelStore((state) =>1974 const rightPanelState = useRightPanelStore((state) =>
1965 selectThreadRightPanelState(state.byThreadKey, activeThreadRef),1975 selectThreadRightPanelState(state.byThreadKey, activeThreadRef),
1966 );1976 );
...@@ -3712,9 +3722,6 @@ export default function ChatView(props: ChatViewProps) {...@@ -3712,9 +3722,6 @@ export default function ChatView(props: ChatViewProps) {
3712 showEnvironmentIndicator: showComposerEnvironmentIndicator,3722 showEnvironmentIndicator: showComposerEnvironmentIndicator,
3713 hostsRestingComposerControls: routeKind === "server" && restingComposerControlsVisible,3723 hostsRestingComposerControls: routeKind === "server" && restingComposerControlsVisible,
3714 });3724 });
3715 const initialDiffPanelGitScope =
3716 gitStatusQuery.data?.hasWorkingTreeChanges === true ? "unstaged" : "branch";
3717 const diffPanelGitStatusResolutionKey = gitStatusQuery.data ? "resolved" : "pending";
3718 const terminalShortcutLabelOptions = useMemo(3725 const terminalShortcutLabelOptions = useMemo(
3719 () => ({3726 () => ({
3720 context: {3727 context: {
...@@ -4501,6 +4508,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -4501,6 +4508,7 @@ export default function ChatView(props: ChatViewProps) {
4501 );4508 );
4502 const addDiffSurface = useCallback(() => {4509 const addDiffSurface = useCallback(() => {
4503 if (!activeThreadRef || !isServerThread || !isGitRepo) return;4510 if (!activeThreadRef || !isServerThread || !isGitRepo) return;
4511 useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
4504 useRightPanelStore.getState().open(activeThreadRef, "diff");4512 useRightPanelStore.getState().open(activeThreadRef, "diff");
4505 onDiffPanelOpen?.();4513 onDiffPanelOpen?.();
4506 }, [activeThreadRef, isGitRepo, isServerThread, onDiffPanelOpen]);4514 }, [activeThreadRef, isGitRepo, isServerThread, onDiffPanelOpen]);
...@@ -4769,7 +4777,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -4769,7 +4777,7 @@ export default function ChatView(props: ChatViewProps) {
4769 if (!panels.openProactive(activeThreadRef, { id: "diff", kind: "diff" }, userActionRevision)) {4777 if (!panels.openProactive(activeThreadRef, { id: "diff", kind: "diff" }, userActionRevision)) {
4770 return;4778 return;
4771 }4779 }
4772 useDiffPanelStore.getState().selectTurn(activeThreadRef, newlyCompletedTurnId);4780 useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
4773 onDiffPanelOpen?.();4781 onDiffPanelOpen?.();
4774 }, [4782 }, [
4775 activeThread?.checkpoints,4783 activeThread?.checkpoints,
...@@ -9061,11 +9069,12 @@ export default function ChatView(props: ChatViewProps) {...@@ -9061,11 +9069,12 @@ export default function ChatView(props: ChatViewProps) {
9061 const onOpenTurnDiff = useCallback(9069 const onOpenTurnDiff = useCallback(
9062 (turnId: TurnId, filePath?: string) => {9070 (turnId: TurnId, filePath?: string) => {
9063 if (!isServerThread || !activeThreadRef) return;9071 if (!isServerThread || !activeThreadRef) return;
9072 explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef;
9064 useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath);9073 useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath);
9065 useRightPanelStore.getState().open(activeThreadRef, "diff");9074 useRightPanelStore.getState().open(activeThreadRef, "diff");
9066 onDiffPanelOpen?.();9075 onDiffPanelOpen?.();
9067 },9076 },
9068 [activeThreadRef, isServerThread, onDiffPanelOpen],9077 [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen],
9069 );9078 );
9070 // The revert handler is read from a ref at call-time so the callback9079 // The revert handler is read from a ref at call-time so the callback
9071 // reference is fully stable and never busts TimelineRowCtx identity.9080 // reference is fully stable and never busts TimelineRowCtx identity.
...@@ -9209,10 +9218,9 @@ export default function ChatView(props: ChatViewProps) {...@@ -9209,10 +9218,9 @@ export default function ChatView(props: ChatViewProps) {
9209 ) : renderedRightPanelSurface?.kind === "diff" ? (9218 ) : renderedRightPanelSurface?.kind === "diff" ? (
9210 <Suspense fallback={null}>9219 <Suspense fallback={null}>
9211 <DiffPanel9220 <DiffPanel
9212 key={`${activeThreadKey}:${diffPanelGitStatusResolutionKey}`}9221 key={activeThreadKey}
9213 mode="embedded"9222 mode="embedded"
9214 composerDraftTarget={composerDraftTarget}9223 composerDraftTarget={composerDraftTarget}
9215 initialGitScope={initialDiffPanelGitScope}
9216 workspaceMutationId={workspaceMutationId}9224 workspaceMutationId={workspaceMutationId}
9217 />9225 />
9218 </Suspense>9226 </Suspense>
apps/web/src/components/DiffPanel.tsx+1-8
...@@ -121,19 +121,16 @@ const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set();...@@ -121,19 +121,16 @@ const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set();
121interface DiffPanelProps {121interface DiffPanelProps {
122 mode?: DiffPanelMode;122 mode?: DiffPanelMode;
123 composerDraftTarget: ScopedThreadRef | DraftId;123 composerDraftTarget: ScopedThreadRef | DraftId;
124 initialGitScope: "branch" | "unstaged";
125 workspaceMutationId: string | null;124 workspaceMutationId: string | null;
126}125}
127126
128export default function DiffPanel({127export default function DiffPanel({
129 mode = "inline",128 mode = "inline",
130 composerDraftTarget,129 composerDraftTarget,
131 initialGitScope: initialGitScopeProp,
132 workspaceMutationId,130 workspaceMutationId,
133}: DiffPanelProps) {131}: DiffPanelProps) {
134 const { resolvedTheme } = useTheme();132 const { resolvedTheme } = useTheme();
135 const settings = useClientSettings();133 const settings = useClientSettings();
136 const [initialGitScope] = useState(initialGitScopeProp);
137 const diffLayout = settings.diffLayout;134 const diffLayout = settings.diffLayout;
138 const updateClientSettings = useUpdateClientSettings();135 const updateClientSettings = useUpdateClientSettings();
139 const [wordWrap, setWordWrap] = useState(settings.wordWrap);136 const [wordWrap, setWordWrap] = useState(settings.wordWrap);
...@@ -187,11 +184,7 @@ export default function DiffPanel({...@@ -187,11 +184,7 @@ export default function DiffPanel({
187 : null,184 : null,
188 );185 );
189 const diffSelection = useDiffPanelStore((state) =>186 const diffSelection = useDiffPanelStore((state) =>
190 selectThreadDiffPanelSelection(187 selectThreadDiffPanelSelection(state.byThreadKey, routeThreadRef),
191 state.byThreadKey,
192 routeThreadRef,
193 initialGitScope === "unstaged",
194 ),
195 );188 );
196 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;189 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;
197 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =190 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
apps/web/src/components/settings/SettingsPanels.tsx+1-1
...@@ -2528,7 +2528,7 @@ export function GeneralSettingsPanel() {...@@ -2528,7 +2528,7 @@ export function GeneralSettingsPanel() {
25282528
2529 <SettingsRow2529 <SettingsRow
2530 {...searchableSetting("proactive-panels")}2530 {...searchableSetting("proactive-panels")}
2531 description="Open linked pull requests first. Otherwise, open turn diffs for changes to at least 3 files or 50 lines."2531 description="Open linked pull requests first. Otherwise, open the working tree diff for changes to at least 3 files or 50 lines."
2532 resetAction={2532 resetAction={
2533 settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled ? (2533 settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled ? (
2534 <SettingResetButton2534 <SettingResetButton
apps/web/src/diffPanelStore.test.ts+33-8
...@@ -14,23 +14,23 @@ describe("diffPanelStore", () => {...@@ -14,23 +14,23 @@ describe("diffPanelStore", () => {
14 }),14 }),
15 );15 );
1616
17 it("defaults each thread to branch changes when the working tree is clean", () => {17 it("defaults each thread to working tree changes without requiring git status", () => {
18 expect(18 expect(
19 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),19 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
20 ).toEqual({ kind: "branch", baseRef: null });20 ).toEqual({ kind: "unstaged" });
21 });21 });
2222
23 it("defaults each thread to working changes when the working tree is dirty", () => {23 it("defaults to working tree changes before a thread is selected", () => {
24 expect(24 expect(selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, null)).toEqual({
25 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),25 kind: "unstaged",
26 ).toEqual({ kind: "unstaged" });26 });
27 });27 });
2828
29 it("preserves an explicit scope selection when the working tree state changes", () => {29 it("preserves an explicit branch selection", () => {
30 useDiffPanelStore.getState().selectGitScope(THREAD_REF, "branch");30 useDiffPanelStore.getState().selectGitScope(THREAD_REF, "branch");
3131
32 expect(32 expect(
33 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),33 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
34 ).toEqual({ kind: "branch", baseRef: null });34 ).toEqual({ kind: "branch", baseRef: null });
35 });35 });
3636
...@@ -49,6 +49,31 @@ describe("diffPanelStore", () => {...@@ -49,6 +49,31 @@ describe("diffPanelStore", () => {
49 ).toEqual({ kind: "branch", baseRef: "origin/main" });49 ).toEqual({ kind: "branch", baseRef: "origin/main" });
50 });50 });
5151
52 it("clears a thread's turn and file when selecting working tree without changing another thread's branch base", () => {
53 const otherThreadRef = scopeThreadRef(
54 EnvironmentId.make("environment-1"),
55 ThreadId.make("thread-2"),
56 );
57 const store = useDiffPanelStore.getState();
58 store.selectBranchBaseRef(THREAD_REF, "origin/release");
59 store.selectTurn(THREAD_REF, TurnId.make("turn-1"), "src/app.ts");
60 store.selectBranchBaseRef(otherThreadRef, "origin/main");
61
62 store.selectGitScope(THREAD_REF, "unstaged");
63
64 const { byThreadKey } = useDiffPanelStore.getState();
65 expect(selectThreadDiffPanelSelection(byThreadKey, THREAD_REF)).toEqual({ kind: "unstaged" });
66 expect(selectThreadDiffPanelSelection(byThreadKey, otherThreadRef)).toEqual({
67 kind: "branch",
68 baseRef: "origin/main",
69 });
70
71 store.selectGitScope(THREAD_REF, "branch");
72 expect(
73 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
74 ).toEqual({ kind: "branch", baseRef: "origin/release" });
75 });
76
52 it("increments the reveal request when opening the same turn file again", () => {77 it("increments the reveal request when opening the same turn file again", () => {
53 const turnId = TurnId.make("turn-1");78 const turnId = TurnId.make("turn-1");
54 useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts");79 useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts");
apps/web/src/diffPanelStore.ts+2-7
...@@ -10,8 +10,7 @@ export type DiffPanelSelection =...@@ -10,8 +10,7 @@ export type DiffPanelSelection =
10 | { kind: "unstaged" }10 | { kind: "unstaged" }
11 | { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number };11 | { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number };
1212
13const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null };13const DEFAULT_SELECTION: DiffPanelSelection = { kind: "unstaged" };
14const DEFAULT_WORKING_TREE_SELECTION: DiffPanelSelection = { kind: "unstaged" };
1514
16interface DiffPanelStoreState {15interface DiffPanelStoreState {
17 byThreadKey: Record<string, DiffPanelSelection>;16 byThreadKey: Record<string, DiffPanelSelection>;
...@@ -134,11 +133,7 @@ export const useDiffPanelStore = create<DiffPanelStoreState>()(...@@ -134,11 +133,7 @@ export const useDiffPanelStore = create<DiffPanelStoreState>()(
134export function selectThreadDiffPanelSelection(133export function selectThreadDiffPanelSelection(
135 byThreadKey: Record<string, DiffPanelSelection>,134 byThreadKey: Record<string, DiffPanelSelection>,
136 ref: ScopedThreadRef | null | undefined,135 ref: ScopedThreadRef | null | undefined,
137 hasWorkingTreeChanges = false,
138): DiffPanelSelection {136): DiffPanelSelection {
139 if (!ref) return DEFAULT_SELECTION;137 if (!ref) return DEFAULT_SELECTION;
140 return (138 return byThreadKey[scopedThreadKey(ref)] ?? DEFAULT_SELECTION;
141 byThreadKey[scopedThreadKey(ref)] ??
142 (hasWorkingTreeChanges ? DEFAULT_WORKING_TREE_SELECTION : DEFAULT_SELECTION)
143 );
144}139}