authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-16 18:47:07-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-16 18:47:07-03:00
log52ad70ec43b5dcfcdc72ac79c0964da8e4d163db
treeb0eeb56948c1b47b4451d2db551828b50764b050
parentaae361c4ef6478e4dcf970d574f3fd38d908e6b9
signature Signed by PGP key B5690EEEBB952194

fix(web): prioritize linked pull requests over automatic diffs (#12142)


6 files changed, 111 insertions(+), 62 deletions(-)

apps/web/src/components/ChatView.logic.test.ts+28-22
......@@ -387,29 +387,35 @@ describe("proactive panels", () => {
387387 ).toBe(false);
388388 });
389389
390 it("opens a completed turn diff only for changed files", () => {
391 const changedCheckpoint = {
392 status: "ready",
393 files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }],
394 } satisfies Pick<TurnDiffSummary, "status" | "files">;
395 const unchangedCheckpoint = {
396 status: "ready",
397 files: [],
398 } satisfies Pick<TurnDiffSummary, "status" | "files">;
390 it.each([
391 { files: 0, additions: 0, deletions: 0, action: "ignore" },
392 { files: 1, additions: 1, deletions: 0, action: "ignore" },
393 { files: 2, additions: 12, deletions: 12, action: "ignore" },
394 { files: 1, additions: 25, deletions: 24, action: "ignore" },
395 { files: 1, additions: 25, deletions: 25, action: "open" },
396 { files: 1, additions: 0, deletions: 50, action: "open" },
397 { files: 3, additions: 1, deletions: 0, action: "open" },
398 ])(
399 "uses change size for automatic diffs: $files files, +$additions/-$deletions",
400 ({ files, additions, deletions, action }) => {
401 const changedCheckpoint = {
402 status: "ready",
403 files: Array.from({ length: files }, (_, index) => ({
404 path: `src/app-${index}.ts`,
405 kind: "modified" as const,
406 additions,
407 deletions,
408 })),
409 } satisfies Pick<TurnDiffSummary, "status" | "files">;
399410
400 expect(
401 resolveProactiveTurnDiffAction({
402 checkpoint: changedCheckpoint,
403 isGitRepo: true,
404 }),
405 ).toBe("open");
406 expect(
407 resolveProactiveTurnDiffAction({
408 checkpoint: unchangedCheckpoint,
409 isGitRepo: true,
410 }),
411 ).toBe("ignore");
412 });
411 expect(
412 resolveProactiveTurnDiffAction({
413 checkpoint: changedCheckpoint,
414 isGitRepo: true,
415 }),
416 ).toBe(action);
417 },
418 );
413419
414420 it("waits for definitive checkpoint and repository state", () => {
415421 const missingCheckpoint = {
apps/web/src/components/ChatView.logic.ts+5-1
......@@ -189,7 +189,11 @@ export function resolveProactiveTurnDiffAction(input: {
189189 ) {
190190 return "ignore";
191191 }
192 return "open";
192 const changedLines = input.checkpoint.files.reduce(
193 (total, file) => total + file.additions + file.deletions,
194 0,
195 );
196 return input.checkpoint.files.length >= 3 || changedLines >= 50 ? "open" : "ignore";
193197}
194198
195199export function codexArtifactTemplatePromptToAppend(
apps/web/src/components/ChatView.tsx+58-28
......@@ -4514,9 +4514,10 @@ export default function ChatView(props: ChatViewProps) {
45144514 }, [activeThreadRef]);
45154515 const supportsThreadPullRequests =
45164516 serverConfig?.environment.capabilities.threadPullRequests === true;
4517 const visiblePullRequestCount = visibleThreadPullRequests(
4517 const visiblePullRequests = visibleThreadPullRequests(
45184518 (activeThreadShell ?? activeThread)?.pullRequests ?? [],
4519 ).length;
4519 );
4520 const visiblePullRequestCount = visiblePullRequests.length;
45204521 const pullRequestsSurfaceAvailable =
45214522 isServerThread && supportsThreadPullRequests && visiblePullRequestCount > 0;
45224523 const addPullRequestsSurface = useCallback(() => {
......@@ -4616,6 +4617,7 @@ export default function ChatView(props: ChatViewProps) {
46164617 );
46174618 // The shell carries server PR updates even while thread detail is still loading.
46184619 const activeThreadMetadata = activeThreadShell ?? activeThread;
4620 const hasLinkedPullRequestDetail = activeThreadMetadata?.linkedPullRequest != null;
46194621 const linkedThreadPullRequest =
46204622 activeThreadMetadata?.linkedPullRequest ?? activeThreadMetadata?.branchPullRequest ?? null;
46214623 const activeProjectRepository = activeProject?.repositoryIdentity?.displayName ?? null;
......@@ -4626,6 +4628,11 @@ export default function ChatView(props: ChatViewProps) {
46264628 linkedThreadPullRequest.number,
46274629 ])
46284630 : null;
4631 const proactivePullRequestsKey = pullRequestsSurfaceAvailable
4632 ? JSON.stringify(
4633 visiblePullRequests.map((link) => [link.host, link.repository, link.number]).sort(),
4634 )
4635 : linkedThreadPullRequestKey;
46294636 const observedThreadPullRequestRef = useRef<{
46304637 readonly threadKey: string;
46314638 readonly reference: ThreadLinkedPullRequest | null;
......@@ -4692,7 +4699,40 @@ export default function ChatView(props: ChatViewProps) {
46924699 userActionRevision,
46934700 );
46944701 }
4695 if (!clientSettingsHydrated || threadDetailLoading) return;
4702 if (!clientSettingsHydrated) return;
4703
4704 const proactivePanelsEnabled = settings.proactivePanelsEnabled && !shouldUseRightPanelSheet;
4705 const eligibleLink =
4706 proactivePanelsEnabled &&
4707 shouldOpenProactivePullRequest(previousTargetKey, proactivePullRequestsKey);
4708 const shouldDeferLink = eligibleLink && !pullRequestsCapabilityKnown;
4709 proactivePanelObservationRef.current = {
4710 ...observation,
4711 targetKey: shouldDeferLink ? (previousTargetKey ?? null) : proactivePullRequestsKey,
4712 };
4713 if (eligibleLink && pullRequestsCapabilityKnown) {
4714 if (
4715 pullRequestsSurfaceAvailable &&
4716 (visiblePullRequestCount > 1 || !hasLinkedPullRequestDetail || !supportsPullRequests)
4717 ) {
4718 panels.openProactive(
4719 activeThreadRef,
4720 { id: "pull-requests", kind: "pull-requests" },
4721 userActionRevision,
4722 );
4723 } else if (
4724 !followSelectedPullRequest &&
4725 supportsPullRequests &&
4726 linkedThreadPullRequest !== null
4727 ) {
4728 panels.openProactive(
4729 activeThreadRef,
4730 pullRequestSurface(linkedThreadPullRequest),
4731 userActionRevision,
4732 );
4733 }
4734 }
4735 if (threadDetailLoading) return;
46964736
46974737 const settledTurnId = latestTurnSettled ? (activeLatestTurn?.turnId ?? null) : null;
46984738 const newlyCompletedTurnId = shouldOpenProactiveTurnDiff({
......@@ -4703,8 +4743,13 @@ export default function ChatView(props: ChatViewProps) {
47034743 })
47044744 ? settledTurnId
47054745 : null;
4706 const proactivePanelsEnabled = settings.proactivePanelsEnabled && !shouldUseRightPanelSheet;
4707 const eligibleCompletion = proactivePanelsEnabled && newlyCompletedTurnId !== null;
4746 const eligibleCompletion =
4747 proactivePanelsEnabled &&
4748 newlyCompletedTurnId !== null &&
4749 !(
4750 proactivePullRequestsKey !== null &&
4751 (!pullRequestsCapabilityKnown || supportsPullRequests || pullRequestsSurfaceAvailable)
4752 );
47084753 const completedCheckpoint = eligibleCompletion
47094754 ? activeThread?.checkpoints.find((checkpoint) => checkpoint.turnId === newlyCompletedTurnId)
47104755 : undefined;
......@@ -4714,30 +4759,12 @@ export default function ChatView(props: ChatViewProps) {
47144759 isGitRepo: gitStatusQuery.data?.isRepo,
47154760 })
47164761 : "ignore";
4717 const eligibleLink =
4718 proactivePanelsEnabled &&
4719 shouldOpenProactivePullRequest(previousTargetKey, linkedThreadPullRequestKey);
4720 const shouldDeferLink = eligibleLink && !pullRequestsCapabilityKnown;
47214762 proactivePanelObservationRef.current = {
4722 ...observation,
4723 // Preserve first-entry eligibility while the checkpoint or repository is loading.
4724 runningTurnId: diffAction === "defer" ? previousRunningTurnId : activeRunningTurnId,
4725 targetKey: shouldDeferLink ? (previousTargetKey ?? null) : linkedThreadPullRequestKey,
4763 ...proactivePanelObservationRef.current,
4764 // Preserve first-entry eligibility while capabilities, checkpoint or repository load.
4765 runningTurnId:
4766 diffAction === "defer" || shouldDeferLink ? previousRunningTurnId : activeRunningTurnId,
47264767 };
4727
4728 if (
4729 !followSelectedPullRequest &&
4730 eligibleLink &&
4731 pullRequestsCapabilityKnown &&
4732 supportsPullRequests &&
4733 linkedThreadPullRequest !== null
4734 ) {
4735 panels.openProactive(
4736 activeThreadRef,
4737 pullRequestSurface(linkedThreadPullRequest),
4738 userActionRevision,
4739 );
4740 }
47414768 if (diffAction !== "open" || newlyCompletedTurnId === null) return;
47424769 if (!panels.openProactive(activeThreadRef, { id: "diff", kind: "diff" }, userActionRevision)) {
47434770 return;
......@@ -4756,9 +4783,12 @@ export default function ChatView(props: ChatViewProps) {
47564783 isServerThread,
47574784 latestTurnSettled,
47584785 linkedThreadPullRequest,
4759 linkedThreadPullRequestKey,
4786 proactivePullRequestsKey,
4787 hasLinkedPullRequestDetail,
47604788 onDiffPanelOpen,
47614789 pullRequestsCapabilityKnown,
4790 pullRequestsSurfaceAvailable,
4791 visiblePullRequestCount,
47624792 settings.proactivePanelsEnabled,
47634793 shouldUseRightPanelSheet,
47644794 supportsPullRequests,
apps/web/src/components/settings/SettingsPanels.tsx+1-1
......@@ -2528,7 +2528,7 @@ export function GeneralSettingsPanel() {
25282528
25292529 <SettingsRow
25302530 {...searchableSetting("proactive-panels")}
2531 description="Open linked pull requests when found and turn diffs when work changes files."
2531 description="Open linked pull requests first. Otherwise, open turn diffs for changes to at least 3 files or 50 lines."
25322532 resetAction={
25332533 settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled ? (
25342534 <SettingResetButton
apps/web/src/rightPanelStore.test.ts+16-8
......@@ -109,22 +109,27 @@ describe("rightPanelStore", () => {
109109 number: 42,
110110 });
111111
112 it.each(["diff-first", "pull-request-first"])(
113 "prioritizes the linked pull request over browser and diff with %s delivery",
114 (order) => {
112 it.each([
113 { order: "diff-first", surface: linkedPullRequest },
114 { order: "pull-request-first", surface: linkedPullRequest },
115 { order: "diff-first", surface: { id: "pull-requests", kind: "pull-requests" } as const },
116 {
117 order: "pull-request-first",
118 surface: { id: "pull-requests", kind: "pull-requests" } as const,
119 },
120 ])(
121 "prioritizes $surface.kind over browser and diff with $order delivery",
122 ({ order, surface }) => {
115123 const store = useRightPanelStore.getState();
116124 store.openBrowser(refA, "existing-browser");
117125 const revision = store.getUserActionRevision(refA);
118 const requests =
119 order === "diff-first"
120 ? [completedDiff, linkedPullRequest]
121 : [linkedPullRequest, completedDiff];
126 const requests = order === "diff-first" ? [completedDiff, surface] : [surface, completedDiff];
122127 for (const surface of requests) store.openProactive(refA, surface, revision);
123128 store.reconcileBrowserSurfaces(refA, ["existing-browser", "agent-browser"]);
124129
125130 expect(
126131 selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA),
127 ).toEqual(linkedPullRequest);
132 ).toEqual(surface);
128133
129134 store.open(refA, "diff");
130135 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff");
......@@ -167,6 +172,9 @@ describe("rightPanelStore", () => {
167172
168173 expect(store.openProactive(refA, completedDiff, revision)).toBe(false);
169174 expect(store.openProactive(refA, linkedPullRequest, revision)).toBe(false);
175 expect(
176 store.openProactive(refA, { id: "pull-requests", kind: "pull-requests" }, revision),
177 ).toBe(false);
170178 expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toBe(
171179 chosen,
172180 );
apps/web/src/rightPanelStore.ts+3-2
......@@ -124,7 +124,7 @@ interface RightPanelStoreState {
124124 */
125125 openProactive: (
126126 ref: ScopedThreadRef,
127 surface: Extract<RightPanelSurface, { kind: "diff" | "pull-request" }>,
127 surface: Extract<RightPanelSurface, { kind: "diff" | "pull-request" | "pull-requests" }>,
128128 expectedUserActionRevision: number,
129129 ) => boolean;
130130 open: (
......@@ -496,7 +496,8 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
496496 // always apply, and later user choices reject both proactive requests.
497497 if (
498498 surface.kind === "diff" &&
499 selectActiveRightPanel(state.byThreadKey, ref) === "pull-request"
499 (selectActiveRightPanel(state.byThreadKey, ref) === "pull-request" ||
500 selectActiveRightPanel(state.byThreadKey, ref) === "pull-requests")
500501 ) {
501502 return state;
502503 }