authorgravatar for jakeleventhal@me.comJake Leventhal <jakeleventhal@me.com> 2026-07-18 07:17:30-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-18 13:17:30+02:00
log38e0bb07bb24894524f1502ac0487c5ea4ff85de
tree2ebd44463832745f9fb133fadf816b9bc1e01463
parent765e1b5fc044b899741a11dcba141bb2c49c6c37
signature Signed by PGP key B5690EEEBB952194

Default diffs to working changes (#3974)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

4 files changed, 47 insertions(+), 9 deletions(-)

apps/web/src/components/ChatView.tsx+12-3
...@@ -2252,12 +2252,13 @@ function ChatViewContent(props: ChatViewProps) {...@@ -2252,12 +2252,13 @@ function ChatViewContent(props: ChatViewProps) {
2252 worktreePath: activeThread?.worktreePath ?? null,2252 worktreePath: activeThread?.worktreePath ?? null,
2253 })2253 })
2254 : null;2254 : null;
2255 const gitStatusCwd = activeThread?.worktreePath ?? gitCwd;
2255 const gitStatusQuery = useEnvironmentQuery(2256 const gitStatusQuery = useEnvironmentQuery(
2256 gitCwd === null2257 gitStatusCwd === null
2257 ? null2258 ? null
2258 : vcsEnvironment.status({2259 : vcsEnvironment.status({
2259 environmentId,2260 environmentId,
2260 input: { cwd: gitCwd },2261 input: { cwd: gitStatusCwd },
2261 }),2262 }),
2262 );2263 );
2263 const keybindings = useAtomValue(primaryServerKeybindingsAtom);2264 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
...@@ -2291,6 +2292,9 @@ function ChatViewContent(props: ChatViewProps) {...@@ -2291,6 +2292,9 @@ function ChatViewContent(props: ChatViewProps) {
2291 terminalUiLaunchContext?.threadId === activeThreadId ? terminalUiLaunchContext : null;2292 terminalUiLaunchContext?.threadId === activeThreadId ? terminalUiLaunchContext : null;
2292 // Default true while loading to avoid toolbar flicker.2293 // Default true while loading to avoid toolbar flicker.
2293 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;2294 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;
2295 const initialDiffPanelGitScope =
2296 gitStatusQuery.data?.hasWorkingTreeChanges === true ? "unstaged" : "branch";
2297 const diffPanelGitStatusResolutionKey = gitStatusQuery.data ? "resolved" : "pending";
2294 const terminalShortcutLabelOptions = useMemo(2298 const terminalShortcutLabelOptions = useMemo(
2295 () => ({2299 () => ({
2296 context: {2300 context: {
...@@ -5136,7 +5140,12 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5136,7 +5140,12 @@ function ChatViewContent(props: ChatViewProps) {
5136 />5140 />
5137 ) : activeRightPanelSurface?.kind === "diff" ? (5141 ) : activeRightPanelSurface?.kind === "diff" ? (
5138 <Suspense fallback={null}>5142 <Suspense fallback={null}>
5139 <DiffPanel mode="embedded" composerDraftTarget={composerDraftTarget} />5143 <DiffPanel
5144 key={`${activeThreadKey}:${diffPanelGitStatusResolutionKey}`}
5145 mode="embedded"
5146 composerDraftTarget={composerDraftTarget}
5147 initialGitScope={initialDiffPanelGitScope}
5148 />
5140 </Suspense>5149 </Suspense>
5141 ) : activeRightPanelSurface?.kind === "plan" ? (5150 ) : activeRightPanelSurface?.kind === "plan" ? (
5142 <PlanSidebar5151 <PlanSidebar
apps/web/src/components/DiffPanel.tsx+14-4
...@@ -178,13 +178,19 @@ const DIFF_PANEL_UNSAFE_CSS = `...@@ -178,13 +178,19 @@ const DIFF_PANEL_UNSAFE_CSS = `
178interface DiffPanelProps {178interface DiffPanelProps {
179 mode?: DiffPanelMode;179 mode?: DiffPanelMode;
180 composerDraftTarget: ScopedThreadRef | DraftId;180 composerDraftTarget: ScopedThreadRef | DraftId;
181 initialGitScope: "branch" | "unstaged";
181}182}
182183
183export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";184export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";
184185
185export default function DiffPanel({ mode = "inline", composerDraftTarget }: DiffPanelProps) {186export default function DiffPanel({
187 mode = "inline",
188 composerDraftTarget,
189 initialGitScope: initialGitScopeProp,
190}: DiffPanelProps) {
186 const { resolvedTheme } = useTheme();191 const { resolvedTheme } = useTheme();
187 const settings = useClientSettings();192 const settings = useClientSettings();
193 const [initialGitScope] = useState(initialGitScopeProp);
188 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");194 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
189 const [wordWrap, setWordWrap] = useState(settings.wordWrap);195 const [wordWrap, setWordWrap] = useState(settings.wordWrap);
190 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);196 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);
...@@ -199,9 +205,6 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -199,9 +205,6 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
199 strict: false,205 strict: false,
200 select: (params) => resolveThreadRouteRef(params),206 select: (params) => resolveThreadRouteRef(params),
201 });207 });
202 const diffSelection = useDiffPanelStore((state) =>
203 selectThreadDiffPanelSelection(state.byThreadKey, routeThreadRef),
204 );
205 const activeThreadId = routeThreadRef?.threadId ?? null;208 const activeThreadId = routeThreadRef?.threadId ?? null;
206 const activeThread = useThread(routeThreadRef);209 const activeThread = useThread(routeThreadRef);
207 const activeProjectId = activeThread?.projectId ?? null;210 const activeProjectId = activeThread?.projectId ?? null;
...@@ -229,6 +232,13 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -229,6 +232,13 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
229 })232 })
230 : null,233 : null,
231 );234 );
235 const diffSelection = useDiffPanelStore((state) =>
236 selectThreadDiffPanelSelection(
237 state.byThreadKey,
238 routeThreadRef,
239 initialGitScope === "unstaged",
240 ),
241 );
232 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;242 const isGitRepo = gitStatusQuery.data?.isRepo ?? true;
233 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =243 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
234 useTurnDiffSummaries(activeThread);244 useTurnDiffSummaries(activeThread);
apps/web/src/diffPanelStore.test.ts+15-1
...@@ -9,12 +9,26 @@ const THREAD_REF = scopeThreadRef(EnvironmentId.make("environment-1"), ThreadId....@@ -9,12 +9,26 @@ const THREAD_REF = scopeThreadRef(EnvironmentId.make("environment-1"), ThreadId.
9describe("diffPanelStore", () => {9describe("diffPanelStore", () => {
10 beforeEach(() => useDiffPanelStore.setState({ byThreadKey: {}, branchBaseRefByThreadKey: {} }));10 beforeEach(() => useDiffPanelStore.setState({ byThreadKey: {}, branchBaseRefByThreadKey: {} }));
1111
12 it("defaults each thread to branch changes with automatic base selection", () => {12 it("defaults each thread to branch changes when the working tree is clean", () => {
13 expect(13 expect(
14 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),14 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
15 ).toEqual({ kind: "branch", baseRef: null });15 ).toEqual({ kind: "branch", baseRef: null });
16 });16 });
1717
18 it("defaults each thread to working changes when the working tree is dirty", () => {
19 expect(
20 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),
21 ).toEqual({ kind: "unstaged" });
22 });
23
24 it("preserves an explicit scope selection when the working tree state changes", () => {
25 useDiffPanelStore.getState().selectGitScope(THREAD_REF, "branch");
26
27 expect(
28 selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),
29 ).toEqual({ kind: "branch", baseRef: null });
30 });
31
18 it("clears incompatible selection fields when changing scopes", () => {32 it("clears incompatible selection fields when changing scopes", () => {
19 const store = useDiffPanelStore.getState();33 const store = useDiffPanelStore.getState();
20 store.selectTurn(THREAD_REF, TurnId.make("turn-1"), "src/app.ts");34 store.selectTurn(THREAD_REF, TurnId.make("turn-1"), "src/app.ts");
apps/web/src/diffPanelStore.ts+6-1
...@@ -11,6 +11,7 @@ export type DiffPanelSelection =...@@ -11,6 +11,7 @@ export type DiffPanelSelection =
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: "branch", baseRef: null };
14const DEFAULT_WORKING_TREE_SELECTION: DiffPanelSelection = { kind: "unstaged" };
1415
15interface DiffPanelStoreState {16interface DiffPanelStoreState {
16 byThreadKey: Record<string, DiffPanelSelection>;17 byThreadKey: Record<string, DiffPanelSelection>;
...@@ -133,7 +134,11 @@ export const useDiffPanelStore = create<DiffPanelStoreState>()(...@@ -133,7 +134,11 @@ export const useDiffPanelStore = create<DiffPanelStoreState>()(
133export function selectThreadDiffPanelSelection(134export function selectThreadDiffPanelSelection(
134 byThreadKey: Record<string, DiffPanelSelection>,135 byThreadKey: Record<string, DiffPanelSelection>,
135 ref: ScopedThreadRef | null | undefined,136 ref: ScopedThreadRef | null | undefined,
137 hasWorkingTreeChanges = false,
136): DiffPanelSelection {138): DiffPanelSelection {
137 if (!ref) return DEFAULT_SELECTION;139 if (!ref) return DEFAULT_SELECTION;
138 return byThreadKey[scopedThreadKey(ref)] ?? DEFAULT_SELECTION;140 return (
141 byThreadKey[scopedThreadKey(ref)] ??
142 (hasWorkingTreeChanges ? DEFAULT_WORKING_TREE_SELECTION : DEFAULT_SELECTION)
143 );
139}144}