| 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 | 1961 | selectActiveRightPanel(state.byThreadKey, activeThreadRef), |
| 1962 | 1962 | ); |
| 1963 | 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 | 1974 | const rightPanelState = useRightPanelStore((state) => |
| 1965 | 1975 | selectThreadRightPanelState(state.byThreadKey, activeThreadRef), |
| 1966 | 1976 | ); |
| ... | ... | @@ -3712,9 +3722,6 @@ export default function ChatView(props: ChatViewProps) { |
| 3712 | 3722 | showEnvironmentIndicator: showComposerEnvironmentIndicator, |
| 3713 | 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 | 3725 | const terminalShortcutLabelOptions = useMemo( |
| 3719 | 3726 | () => ({ |
| 3720 | 3727 | context: { |
| ... | ... | @@ -4501,6 +4508,7 @@ export default function ChatView(props: ChatViewProps) { |
| 4501 | 4508 | ); |
| 4502 | 4509 | const addDiffSurface = useCallback(() => { |
| 4503 | 4510 | if (!activeThreadRef || !isServerThread || !isGitRepo) return; |
| 4511 | useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged"); | |
| 4504 | 4512 | useRightPanelStore.getState().open(activeThreadRef, "diff"); |
| 4505 | 4513 | onDiffPanelOpen?.(); |
| 4506 | 4514 | }, [activeThreadRef, isGitRepo, isServerThread, onDiffPanelOpen]); |
| ... | ... | @@ -4769,7 +4777,7 @@ export default function ChatView(props: ChatViewProps) { |
| 4769 | 4777 | if (!panels.openProactive(activeThreadRef, { id: "diff", kind: "diff" }, userActionRevision)) { |
| 4770 | 4778 | return; |
| 4771 | 4779 | } |
| 4772 | useDiffPanelStore.getState().selectTurn(activeThreadRef, newlyCompletedTurnId); | |
| 4780 | useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged"); | |
| 4773 | 4781 | onDiffPanelOpen?.(); |
| 4774 | 4782 | }, [ |
| 4775 | 4783 | activeThread?.checkpoints, |
| ... | ... | @@ -9061,11 +9069,12 @@ export default function ChatView(props: ChatViewProps) { |
| 9061 | 9069 | const onOpenTurnDiff = useCallback( |
| 9062 | 9070 | (turnId: TurnId, filePath?: string) => { |
| 9063 | 9071 | if (!isServerThread || !activeThreadRef) return; |
| 9072 | explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef; | |
| 9064 | 9073 | useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath); |
| 9065 | 9074 | useRightPanelStore.getState().open(activeThreadRef, "diff"); |
| 9066 | 9075 | onDiffPanelOpen?.(); |
| 9067 | 9076 | }, |
| 9068 | [activeThreadRef, isServerThread, onDiffPanelOpen], | |
| 9077 | [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen], | |
| 9069 | 9078 | ); |
| 9070 | 9079 | // The revert handler is read from a ref at call-time so the callback |
| 9071 | 9080 | // reference is fully stable and never busts TimelineRowCtx identity. |
| ... | ... | @@ -9209,10 +9218,9 @@ export default function ChatView(props: ChatViewProps) { |
| 9209 | 9218 | ) : renderedRightPanelSurface?.kind === "diff" ? ( |
| 9210 | 9219 | <Suspense fallback={null}> |
| 9211 | 9220 | <DiffPanel |
| 9212 | key={`${activeThreadKey}:${diffPanelGitStatusResolutionKey}`} | |
| 9221 | key={activeThreadKey} | |
| 9213 | 9222 | mode="embedded" |
| 9214 | 9223 | composerDraftTarget={composerDraftTarget} |
| 9215 | initialGitScope={initialDiffPanelGitScope} | |
| 9216 | 9224 | workspaceMutationId={workspaceMutationId} |
| 9217 | 9225 | /> |
| 9218 | 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 | 121 | interface DiffPanelProps { |
| 122 | 122 | mode?: DiffPanelMode; |
| 123 | 123 | composerDraftTarget: ScopedThreadRef | DraftId; |
| 124 | initialGitScope: "branch" | "unstaged"; | |
| 125 | 124 | workspaceMutationId: string | null; |
| 126 | 125 | } |
| 127 | 126 | |
| 128 | 127 | export default function DiffPanel({ |
| 129 | 128 | mode = "inline", |
| 130 | 129 | composerDraftTarget, |
| 131 | initialGitScope: initialGitScopeProp, | |
| 132 | 130 | workspaceMutationId, |
| 133 | 131 | }: DiffPanelProps) { |
| 134 | 132 | const { resolvedTheme } = useTheme(); |
| 135 | 133 | const settings = useClientSettings(); |
| 136 | const [initialGitScope] = useState(initialGitScopeProp); | |
| 137 | 134 | const diffLayout = settings.diffLayout; |
| 138 | 135 | const updateClientSettings = useUpdateClientSettings(); |
| 139 | 136 | const [wordWrap, setWordWrap] = useState(settings.wordWrap); |
| ... | ... | @@ -187,11 +184,7 @@ export default function DiffPanel({ |
| 187 | 184 | : null, |
| 188 | 185 | ); |
| 189 | 186 | const diffSelection = useDiffPanelStore((state) => |
| 190 | selectThreadDiffPanelSelection( | |
| 191 | state.byThreadKey, | |
| 192 | routeThreadRef, | |
| 193 | initialGitScope === "unstaged", | |
| 194 | ), | |
| 187 | selectThreadDiffPanelSelection(state.byThreadKey, routeThreadRef), | |
| 195 | 188 | ); |
| 196 | 189 | const isGitRepo = gitStatusQuery.data?.isRepo ?? true; |
| 197 | 190 | const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = |
apps/web/src/components/settings/SettingsPanels.tsx+1-1| ... | ... | @@ -2528,7 +2528,7 @@ export function GeneralSettingsPanel() { |
| 2528 | 2528 | |
| 2529 | 2529 | <SettingsRow |
| 2530 | 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 | 2532 | resetAction={ |
| 2533 | 2533 | settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled ? ( |
| 2534 | 2534 | <SettingResetButton |
apps/web/src/diffPanelStore.test.ts+33-8| ... | ... | @@ -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 | 18 | expect( |
| 19 | 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", () => { | |
| 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 | }); | |
| 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 | 30 | useDiffPanelStore.getState().selectGitScope(THREAD_REF, "branch"); |
| 31 | 31 | |
| 32 | 32 | expect( |
| 33 | selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true), | |
| 33 | selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF), | |
| 34 | 34 | ).toEqual({ kind: "branch", baseRef: null }); |
| 35 | 35 | }); |
| 36 | 36 | |
| ... | ... | @@ -49,6 +49,31 @@ describe("diffPanelStore", () => { |
| 49 | 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 | 77 | it("increments the reveal request when opening the same turn file again", () => { |
| 53 | 78 | const turnId = TurnId.make("turn-1"); |
| 54 | 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 | 10 | | { kind: "unstaged" } |
| 11 | 11 | | { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number }; |
| 12 | 12 | |
| 13 | const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null }; | |
| 14 | const DEFAULT_WORKING_TREE_SELECTION: DiffPanelSelection = { kind: "unstaged" }; | |
| 13 | const DEFAULT_SELECTION: DiffPanelSelection = { kind: "unstaged" }; | |
| 15 | 14 | |
| 16 | 15 | interface DiffPanelStoreState { |
| 17 | 16 | byThreadKey: Record<string, DiffPanelSelection>; |
| ... | ... | @@ -134,11 +133,7 @@ export const useDiffPanelStore = create<DiffPanelStoreState>()( |
| 134 | 133 | export function selectThreadDiffPanelSelection( |
| 135 | 134 | byThreadKey: Record<string, DiffPanelSelection>, |
| 136 | 135 | ref: ScopedThreadRef | null | undefined, |
| 137 | hasWorkingTreeChanges = false, | |
| 138 | 136 | ): DiffPanelSelection { |
| 139 | 137 | 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; | |
| 144 | 139 | } |