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) {
19611961 selectActiveRightPanel(state.byThreadKey, activeThreadRef),
19621962 );
19631963 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]);
19641974 const rightPanelState = useRightPanelStore((state) =>
19651975 selectThreadRightPanelState(state.byThreadKey, activeThreadRef),
19661976 );
......@@ -3712,9 +3722,6 @@ export default function ChatView(props: ChatViewProps) {
37123722 showEnvironmentIndicator: showComposerEnvironmentIndicator,
37133723 hostsRestingComposerControls: routeKind === "server" && restingComposerControlsVisible,
37143724 });
3715 const initialDiffPanelGitScope =
3716 gitStatusQuery.data?.hasWorkingTreeChanges === true ? "unstaged" : "branch";
3717 const diffPanelGitStatusResolutionKey = gitStatusQuery.data ? "resolved" : "pending";
37183725 const terminalShortcutLabelOptions = useMemo(
37193726 () => ({
37203727 context: {
......@@ -4501,6 +4508,7 @@ export default function ChatView(props: ChatViewProps) {
45014508 );
45024509 const addDiffSurface = useCallback(() => {
45034510 if (!activeThreadRef || !isServerThread || !isGitRepo) return;
4511 useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
45044512 useRightPanelStore.getState().open(activeThreadRef, "diff");
45054513 onDiffPanelOpen?.();
45064514 }, [activeThreadRef, isGitRepo, isServerThread, onDiffPanelOpen]);
......@@ -4769,7 +4777,7 @@ export default function ChatView(props: ChatViewProps) {
47694777 if (!panels.openProactive(activeThreadRef, { id: "diff", kind: "diff" }, userActionRevision)) {
47704778 return;
47714779 }
4772 useDiffPanelStore.getState().selectTurn(activeThreadRef, newlyCompletedTurnId);
4780 useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
47734781 onDiffPanelOpen?.();
47744782 }, [
47754783 activeThread?.checkpoints,
......@@ -9061,11 +9069,12 @@ export default function ChatView(props: ChatViewProps) {
90619069 const onOpenTurnDiff = useCallback(
90629070 (turnId: TurnId, filePath?: string) => {
90639071 if (!isServerThread || !activeThreadRef) return;
9072 explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef;
90649073 useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath);
90659074 useRightPanelStore.getState().open(activeThreadRef, "diff");
90669075 onDiffPanelOpen?.();
90679076 },
9068 [activeThreadRef, isServerThread, onDiffPanelOpen],
9077 [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen],
90699078 );
90709079 // The revert handler is read from a ref at call-time so the callback
90719080 // reference is fully stable and never busts TimelineRowCtx identity.
......@@ -9209,10 +9218,9 @@ export default function ChatView(props: ChatViewProps) {
92099218 ) : renderedRightPanelSurface?.kind === "diff" ? (
92109219 <Suspense fallback={null}>
92119220 <DiffPanel
9212 key={`${activeThreadKey}:${diffPanelGitStatusResolutionKey}`}
9221 key={activeThreadKey}
92139222 mode="embedded"
92149223 composerDraftTarget={composerDraftTarget}
9215 initialGitScope={initialDiffPanelGitScope}
92169224 workspaceMutationId={workspaceMutationId}
92179225 />
92189226 </Suspense>
apps/web/src/components/DiffPanel.tsx+1-8
......@@ -121,19 +121,16 @@ const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set();
121121interface DiffPanelProps {
122122 mode?: DiffPanelMode;
123123 composerDraftTarget: ScopedThreadRef | DraftId;
124 initialGitScope: "branch" | "unstaged";
125124 workspaceMutationId: string | null;
126125}
127126
128127export default function DiffPanel({
129128 mode = "inline",
130129 composerDraftTarget,
131 initialGitScope: initialGitScopeProp,
132130 workspaceMutationId,
133131}: DiffPanelProps) {
134132 const { resolvedTheme } = useTheme();
135133 const settings = useClientSettings();
136 const [initialGitScope] = useState(initialGitScopeProp);
137134 const diffLayout = settings.diffLayout;
138135 const updateClientSettings = useUpdateClientSettings();
139136 const [wordWrap, setWordWrap] = useState(settings.wordWrap);
......@@ -187,11 +184,7 @@ export default function DiffPanel({
187184 : null,
188185 );
189186 const diffSelection = useDiffPanelStore((state) =>
190 selectThreadDiffPanelSelection(
191 state.byThreadKey,
192 routeThreadRef,
193 initialGitScope === "unstaged",
194 ),
187 selectThreadDiffPanelSelection(state.byThreadKey, routeThreadRef),
195188 );
196189 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;
197190 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
apps/web/src/components/settings/SettingsPanels.tsx+1-1
......@@ -2528,7 +2528,7 @@ export function GeneralSettingsPanel() {
25282528
25292529 <SettingsRow
25302530 {...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."
25322532 resetAction={
25332533 settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled ? (
25342534 <SettingResetButton
apps/web/src/diffPanelStore.test.ts+33-8
......@@ -14,23 +14,23 @@ describe("diffPanelStore", () => {
1414 }),
1515 );
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", () => {
1818 expect(
1919 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
20 ).toEqual({ kind: "branch", baseRef: null });
20 ).toEqual({ kind: "unstaged" });
2121 });
2222
23 it("defaults each thread to working changes when the working tree is dirty", () => {
24 expect(
25 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),
26 ).toEqual({ kind: "unstaged" });
23 it("defaults to working tree changes before a thread is selected", () => {
24 expect(selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, null)).toEqual({
25 kind: "unstaged",
26 });
2727 });
2828
29 it("preserves an explicit scope selection when the working tree state changes", () => {
29 it("preserves an explicit branch selection", () => {
3030 useDiffPanelStore.getState().selectGitScope(THREAD_REF, "branch");
3131
3232 expect(
33 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),
33 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
3434 ).toEqual({ kind: "branch", baseRef: null });
3535 });
3636
......@@ -49,6 +49,31 @@ describe("diffPanelStore", () => {
4949 ).toEqual({ kind: "branch", baseRef: "origin/main" });
5050 });
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
5277 it("increments the reveal request when opening the same turn file again", () => {
5378 const turnId = TurnId.make("turn-1");
5479 useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts");
apps/web/src/diffPanelStore.ts+2-7
......@@ -10,8 +10,7 @@ export type DiffPanelSelection =
1010 | { kind: "unstaged" }
1111 | { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number };
1212
13const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null };
14const DEFAULT_WORKING_TREE_SELECTION: DiffPanelSelection = { kind: "unstaged" };
13const DEFAULT_SELECTION: DiffPanelSelection = { kind: "unstaged" };
1514
1615interface DiffPanelStoreState {
1716 byThreadKey: Record<string, DiffPanelSelection>;
......@@ -134,11 +133,7 @@ export const useDiffPanelStore = create<DiffPanelStoreState>()(
134133export function selectThreadDiffPanelSelection(
135134 byThreadKey: Record<string, DiffPanelSelection>,
136135 ref: ScopedThreadRef | null | undefined,
137 hasWorkingTreeChanges = false,
138136): DiffPanelSelection {
139137 if (!ref) return DEFAULT_SELECTION;
140 return (
141 byThreadKey[scopedThreadKey(ref)] ??
142 (hasWorkingTreeChanges ? DEFAULT_WORKING_TREE_SELECTION : DEFAULT_SELECTION)
143 );
138 return byThreadKey[scopedThreadKey(ref)] ?? DEFAULT_SELECTION;
144139}