Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1961,6 +1961,16 @@ export default function ChatView(props: ChatViewProps) {
selectActiveRightPanel(state.byThreadKey, activeThreadRef),
);
const diffOpen = activeRightPanelKind === "diff";
const explicitDiffOpenRef = useRef<ScopedThreadRef | null>(null);
useLayoutEffect(() => {
const explicitThreadRef = explicitDiffOpenRef.current;
explicitDiffOpenRef.current = null;
// Generic openings always show the checkout, including tab fallbacks and thread changes.
// A timeline click instead opens the specific turn/file the user requested.
if (diffOpen && activeThreadRef && explicitThreadRef !== activeThreadRef) {
useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
}
}, [activeThreadRef, diffOpen]);
const rightPanelState = useRightPanelStore((state) =>
selectThreadRightPanelState(state.byThreadKey, activeThreadRef),
);
Expand Down Expand Up @@ -3712,9 +3722,6 @@ export default function ChatView(props: ChatViewProps) {
showEnvironmentIndicator: showComposerEnvironmentIndicator,
hostsRestingComposerControls: routeKind === "server" && restingComposerControlsVisible,
});
const initialDiffPanelGitScope =
gitStatusQuery.data?.hasWorkingTreeChanges === true ? "unstaged" : "branch";
const diffPanelGitStatusResolutionKey = gitStatusQuery.data ? "resolved" : "pending";
const terminalShortcutLabelOptions = useMemo(
() => ({
context: {
Expand Down Expand Up @@ -4501,6 +4508,7 @@ export default function ChatView(props: ChatViewProps) {
);
const addDiffSurface = useCallback(() => {
if (!activeThreadRef || !isServerThread || !isGitRepo) return;
useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
useRightPanelStore.getState().open(activeThreadRef, "diff");
onDiffPanelOpen?.();
}, [activeThreadRef, isGitRepo, isServerThread, onDiffPanelOpen]);
Expand Down Expand Up @@ -4769,7 +4777,7 @@ export default function ChatView(props: ChatViewProps) {
if (!panels.openProactive(activeThreadRef, { id: "diff", kind: "diff" }, userActionRevision)) {
return;
}
useDiffPanelStore.getState().selectTurn(activeThreadRef, newlyCompletedTurnId);
useDiffPanelStore.getState().selectGitScope(activeThreadRef, "unstaged");
onDiffPanelOpen?.();
}, [
activeThread?.checkpoints,
Expand Down Expand Up @@ -9061,11 +9069,12 @@ export default function ChatView(props: ChatViewProps) {
const onOpenTurnDiff = useCallback(
(turnId: TurnId, filePath?: string) => {
if (!isServerThread || !activeThreadRef) return;
explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef;
useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath);
useRightPanelStore.getState().open(activeThreadRef, "diff");
onDiffPanelOpen?.();
},
[activeThreadRef, isServerThread, onDiffPanelOpen],
[activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen],
);
// The revert handler is read from a ref at call-time so the callback
// reference is fully stable and never busts TimelineRowCtx identity.
Expand Down Expand Up @@ -9209,10 +9218,9 @@ export default function ChatView(props: ChatViewProps) {
) : renderedRightPanelSurface?.kind === "diff" ? (
<Suspense fallback={null}>
<DiffPanel
key={`${activeThreadKey}:${diffPanelGitStatusResolutionKey}`}
key={activeThreadKey}
mode="embedded"
composerDraftTarget={composerDraftTarget}
initialGitScope={initialDiffPanelGitScope}
workspaceMutationId={workspaceMutationId}
/>
</Suspense>
Expand Down
9 changes: 1 addition & 8 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,16 @@ const EMPTY_COLLAPSED_DIFF_FILE_KEYS: ReadonlySet<string> = new Set();
interface DiffPanelProps {
mode?: DiffPanelMode;
composerDraftTarget: ScopedThreadRef | DraftId;
initialGitScope: "branch" | "unstaged";
workspaceMutationId: string | null;
}

export default function DiffPanel({
mode = "inline",
composerDraftTarget,
initialGitScope: initialGitScopeProp,
workspaceMutationId,
}: DiffPanelProps) {
const { resolvedTheme } = useTheme();
const settings = useClientSettings();
const [initialGitScope] = useState(initialGitScopeProp);
const diffLayout = settings.diffLayout;
const updateClientSettings = useUpdateClientSettings();
const [wordWrap, setWordWrap] = useState(settings.wordWrap);
Expand Down Expand Up @@ -187,11 +184,7 @@ export default function DiffPanel({
: null,
);
const diffSelection = useDiffPanelStore((state) =>
selectThreadDiffPanelSelection(
state.byThreadKey,
routeThreadRef,
initialGitScope === "unstaged",
),
selectThreadDiffPanelSelection(state.byThreadKey, routeThreadRef),
);
const isGitRepo = gitStatusQuery.data?.isRepo ?? true;
const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2528,7 +2528,7 @@ export function GeneralSettingsPanel() {

<SettingsRow
{...searchableSetting("proactive-panels")}
description="Open linked pull requests first. Otherwise, open turn diffs for changes to at least 3 files or 50 lines."
description="Open linked pull requests first. Otherwise, open the working tree diff for changes to at least 3 files or 50 lines."
resetAction={
settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled ? (
<SettingResetButton
Expand Down
41 changes: 33 additions & 8 deletions apps/web/src/diffPanelStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ describe("diffPanelStore", () => {
}),
);

it("defaults each thread to branch changes when the working tree is clean", () => {
it("defaults each thread to working tree changes without requiring git status", () => {
expect(
selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
).toEqual({ kind: "branch", baseRef: null });
).toEqual({ kind: "unstaged" });
});

it("defaults each thread to working changes when the working tree is dirty", () => {
expect(
selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),
).toEqual({ kind: "unstaged" });
it("defaults to working tree changes before a thread is selected", () => {
expect(selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, null)).toEqual({
kind: "unstaged",
});
});

it("preserves an explicit scope selection when the working tree state changes", () => {
it("preserves an explicit branch selection", () => {
useDiffPanelStore.getState().selectGitScope(THREAD_REF, "branch");

expect(
selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF, true),
selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
).toEqual({ kind: "branch", baseRef: null });
});

Expand All @@ -49,6 +49,31 @@ describe("diffPanelStore", () => {
).toEqual({ kind: "branch", baseRef: "origin/main" });
});

it("clears a thread's turn and file when selecting working tree without changing another thread's branch base", () => {
const otherThreadRef = scopeThreadRef(
EnvironmentId.make("environment-1"),
ThreadId.make("thread-2"),
);
const store = useDiffPanelStore.getState();
store.selectBranchBaseRef(THREAD_REF, "origin/release");
store.selectTurn(THREAD_REF, TurnId.make("turn-1"), "src/app.ts");
store.selectBranchBaseRef(otherThreadRef, "origin/main");

store.selectGitScope(THREAD_REF, "unstaged");

const { byThreadKey } = useDiffPanelStore.getState();
expect(selectThreadDiffPanelSelection(byThreadKey, THREAD_REF)).toEqual({ kind: "unstaged" });
expect(selectThreadDiffPanelSelection(byThreadKey, otherThreadRef)).toEqual({
kind: "branch",
baseRef: "origin/main",
});

store.selectGitScope(THREAD_REF, "branch");
expect(
selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, THREAD_REF),
).toEqual({ kind: "branch", baseRef: "origin/release" });
});

it("increments the reveal request when opening the same turn file again", () => {
const turnId = TurnId.make("turn-1");
useDiffPanelStore.getState().selectTurn(THREAD_REF, turnId, "src/app.ts");
Expand Down
9 changes: 2 additions & 7 deletions apps/web/src/diffPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ export type DiffPanelSelection =
| { kind: "unstaged" }
| { kind: "turn"; turnId: TurnId; filePath: string | null; revealRequestId: number };

const DEFAULT_SELECTION: DiffPanelSelection = { kind: "branch", baseRef: null };
const DEFAULT_WORKING_TREE_SELECTION: DiffPanelSelection = { kind: "unstaged" };
const DEFAULT_SELECTION: DiffPanelSelection = { kind: "unstaged" };

interface DiffPanelStoreState {
byThreadKey: Record<string, DiffPanelSelection>;
Expand Down Expand Up @@ -134,11 +133,7 @@ export const useDiffPanelStore = create<DiffPanelStoreState>()(
export function selectThreadDiffPanelSelection(
byThreadKey: Record<string, DiffPanelSelection>,
ref: ScopedThreadRef | null | undefined,
hasWorkingTreeChanges = false,
): DiffPanelSelection {
if (!ref) return DEFAULT_SELECTION;
return (
byThreadKey[scopedThreadKey(ref)] ??
(hasWorkingTreeChanges ? DEFAULT_WORKING_TREE_SELECTION : DEFAULT_SELECTION)
);
return byThreadKey[scopedThreadKey(ref)] ?? DEFAULT_SELECTION;
}
Loading