diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index c8afd10d37252055c8efb42f9e5084c512d81cf8..c67ea3f3d5c3bd0259be0ed7b5269f5bf182ae82 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -41,6 +41,7 @@ import { resolveComposerInteractionMode, resolveComposerProviderSelection, resolveDraftPromotionNavigationTarget, + resolveProactiveTurnDiffAction, resolveThreadMetadataUpdateForNextTurn, resolveSendEnvMode, resolveDraftHeroState, @@ -159,6 +160,80 @@ describe("proactive panels", () => { }), ).toBe(false); }); + + it("opens a completed turn diff only for changed files", () => { + const changedCheckpoint = { + status: "ready", + files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }], + } satisfies Pick; + const unchangedCheckpoint = { + status: "ready", + files: [], + } satisfies Pick; + + expect( + resolveProactiveTurnDiffAction({ + checkpoint: changedCheckpoint, + isGitRepo: true, + activeSurfaceKind: null, + }), + ).toBe("open"); + expect( + resolveProactiveTurnDiffAction({ + checkpoint: unchangedCheckpoint, + isGitRepo: true, + activeSurfaceKind: null, + }), + ).toBe("ignore"); + }); + + it("waits for definitive checkpoint and repository state", () => { + const missingCheckpoint = { + status: "missing", + files: [], + } satisfies Pick; + const changedCheckpoint = { + status: "ready", + files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }], + } satisfies Pick; + + expect( + resolveProactiveTurnDiffAction({ + checkpoint: undefined, + isGitRepo: true, + activeSurfaceKind: null, + }), + ).toBe("defer"); + expect( + resolveProactiveTurnDiffAction({ + checkpoint: missingCheckpoint, + isGitRepo: true, + activeSurfaceKind: null, + }), + ).toBe("defer"); + expect( + resolveProactiveTurnDiffAction({ + checkpoint: changedCheckpoint, + isGitRepo: undefined, + activeSurfaceKind: null, + }), + ).toBe("defer"); + }); + + it("keeps an active pull request above a completed turn diff", () => { + const changedCheckpoint = { + status: "ready", + files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }], + } satisfies Pick; + + expect( + resolveProactiveTurnDiffAction({ + checkpoint: changedCheckpoint, + isGitRepo: true, + activeSurfaceKind: "pull-request", + }), + ).toBe("ignore"); + }); }); describe("toolGroupConsumesUpwardNavigation", () => { diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 4a0b9f5761037fc56d385240ce472424f48d1586..10c2fd4710ee145cb65f3a7183fc258ff9d0bad7 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -121,6 +121,24 @@ export function shouldOpenProactiveTurnDiff(input: { ); } +export function resolveProactiveTurnDiffAction(input: { + checkpoint: Pick | undefined; + isGitRepo: boolean | undefined; + activeSurfaceKind: RightPanelSurface["kind"] | null; +}): "defer" | "ignore" | "open" { + if (input.activeSurfaceKind === "pull-request") return "ignore"; + if (input.checkpoint === undefined || input.checkpoint.status === "missing") return "defer"; + if (input.isGitRepo === undefined) return "defer"; + if ( + !input.isGitRepo || + input.checkpoint.status !== "ready" || + input.checkpoint.files.length === 0 + ) { + return "ignore"; + } + return "open"; +} + export function codexArtifactTemplatePromptToAppend( currentDraft: string, template: CodexArtifactTemplate, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ccd970e6dda3897175aacb5f16eaf6947720fcc9..cfbeb3aa7f72f4eead29eaf20b29be25e68ce588 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -385,6 +385,7 @@ import { resolveComposerInteractionMode, resolveComposerProviderSelection, resolveDraftHeroState, + resolveProactiveTurnDiffAction, resolveThreadMetadataUpdateForNextTurn, resolveSendEnvMode, revokeBlobPreviewUrl, @@ -3922,18 +3923,21 @@ export default function ChatView(props: ChatViewProps) { : null; const eligibleCompletion = settings.proactivePanelsEnabled && !shouldUseRightPanelSheet && newlyCompletedTurnId !== null; - const checkpointReady = - eligibleCompletion && - activeThread?.checkpoints.some((checkpoint) => checkpoint.turnId === newlyCompletedTurnId) === - true; - const shouldOpenTurn = checkpointReady && gitStatusQuery.data?.isRepo === true; - const shouldDeferCompletion = - eligibleCompletion && !shouldOpenTurn && gitStatusQuery.data?.isRepo !== false; + const completedCheckpoint = eligibleCompletion + ? activeThread?.checkpoints.find((checkpoint) => checkpoint.turnId === newlyCompletedTurnId) + : undefined; + const diffAction = eligibleCompletion + ? resolveProactiveTurnDiffAction({ + checkpoint: completedCheckpoint, + isGitRepo: gitStatusQuery.data?.isRepo, + activeSurfaceKind: activeRightPanelSurface?.kind ?? null, + }) + : "ignore"; proactiveTurnObservationRef.current = { threadKey: activeThreadKey, - runningTurnId: shouldDeferCompletion ? (previousRunningTurnId ?? null) : activeRunningTurnId, + runningTurnId: diffAction === "defer" ? (previousRunningTurnId ?? null) : activeRunningTurnId, }; - if (!shouldOpenTurn || newlyCompletedTurnId === null) return; + if (diffAction !== "open" || newlyCompletedTurnId === null) return; useDiffPanelStore.getState().selectTurn(activeThreadRef, newlyCompletedTurnId); useRightPanelStore.getState().open(activeThreadRef, "diff"); @@ -3945,6 +3949,7 @@ export default function ChatView(props: ChatViewProps) { activeRunningTurnId, activeThreadKey, activeThreadRef, + activeRightPanelSurface?.kind, clientSettingsHydrated, gitStatusQuery.data?.isRepo, isServerThread, diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index d78828da5cfe89c2ffd32bdf22ad7be2b999691b..b0344b6403930f435d75affb79d19652083ee80a 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -2308,7 +2308,7 @@ export function GeneralSettingsPanel() {