| author | |
| committer | |
| log | 394af73e02238e551db14f44d57f1469dd2942aa |
| tree | e4fa392e26c3c7ce2e41f20b529329571aec40ac |
| parent | ed123897943e9175d4a0e92a5a8a09d200573336 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 callback | 9079 | // 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 | <DiffPanel | 9220 | <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(); |
| 121 | interface DiffPanelProps { | 121 | interface 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 | } |
| 127 | 126 | ||
| 128 | export default function DiffPanel({ | 127 | export 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() { |
| 2528 | 2528 | ||
| 2529 | <SettingsRow | 2529 | <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 | <SettingResetButton | 2534 | <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 | ); |
| 16 | 16 | ||
| 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 | }); |
| 22 | 22 | ||
| 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 | }); |
| 28 | 28 | ||
| 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"); |
| 31 | 31 | ||
| 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 | }); |
| 36 | 36 | ||
| ... | @@ -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 | }); |
| 51 | 51 | ||
| 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 }; |
| 12 | 12 | ||
| 13 | const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null }; | 13 | const DEFAULT_SELECTION: DiffPanelSelection = { kind: "unstaged" }; |
| 14 | const DEFAULT_WORKING_TREE_SELECTION: DiffPanelSelection = { kind: "unstaged" }; | ||
| 15 | 14 | ||
| 16 | interface DiffPanelStoreState { | 15 | interface 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>()( |
| 134 | export function selectThreadDiffPanelSelection( | 133 | export 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 | } |