From ad17b88ddd99c74a1ed354d40d21f23ceea591a6 Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 1 Oct 2026 12:20:18 +0000 Subject: [PATCH] fix(web): keep proactive PR panels visible when opening links --- .github/fork-features.yml | 2 +- apps/web/src/rightPanelStore.test.ts | 47 ++++++++++++++++++++++++++++ apps/web/src/rightPanelStore.ts | 26 +++++++++++---- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/.github/fork-features.yml b/.github/fork-features.yml index edf79ea7affc..bbd4d34e491c 100644 --- a/.github/fork-features.yml +++ b/.github/fork-features.yml @@ -1904,7 +1904,7 @@ features: status: maintained prs: [46, 166] invariants: - - Sidebar links and proactive panels reuse the same pull request tab when the linked reference supplies its host through the URL; reopening consolidates existing duplicates without merging different hosts or environments. + - Sidebar links and proactive panels reuse the same pull request tab when the linked reference supplies its host through the URL; reopening consolidates existing duplicates without merging different hosts or environments. URL-derived hosts are resolved before rendering and on saved-panel migration, so clicking the same PR link preserves the component and cache identity. - The command palette and keybinding expose provider-appropriate pull request navigation only when the current thread has an associated change request, then open it in the in-app panel. - Pull request result toasts include the created pull request number and open the corresponding in-app panel when available, retaining the external URL as a fallback. implementation_paths: diff --git a/apps/web/src/rightPanelStore.test.ts b/apps/web/src/rightPanelStore.test.ts index eb6485e95b4e..eed6eb52134b 100644 --- a/apps/web/src/rightPanelStore.test.ts +++ b/apps/web/src/rightPanelStore.test.ts @@ -738,6 +738,53 @@ describe("rightPanelStore", () => { }, ); + it("keeps the rendered PR reference stable when its chat link is opened", () => { + const linked = { + projectId: "project-a", + repository: "acme/api", + number: 5, + url: "https://github.com/acme/api/pull/5", + }; + const store = useRightPanelStore.getState(); + store.openProactive(refA, pullRequestSurface(linked), store.getUserActionRevision(refA)); + const before = selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA); + expect(before).toMatchObject({ host: "github.com" }); + store.openPullRequest(refA, { ...linked, host: "github.com" }); + expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA)).toEqual( + before, + ); + }); + + it("resolves a saved URL-only PR host without changing the active tab", () => { + const legacy = { + id: "pull-request:project-a:acme%2Fapi:5", + kind: "pull-request", + projectId: "project-a", + repository: "acme/api", + number: 5, + url: "https://github.com/acme/api/pull/5", + }; + const migrated = migratePersistedRightPanelState({ + byThreadKey: { + "env-1:thread-A": { + isOpen: true, + activeSurfaceId: legacy.id, + surfaces: [completedDiff, legacy], + }, + }, + }); + useRightPanelStore.setState(migrated); + expect(selectActiveRightPanelSurface(migrated.byThreadKey, refA)).toEqual({ + ...legacy, + host: "github.com", + }); + useRightPanelStore.getState().openPullRequest(refA, { ...legacy, host: "github.com" }); + expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ + ...legacy, + host: "github.com", + }); + }); + it("consolidates previously duplicated PR tabs when reopened", () => { const linked = pullRequestSurface({ projectId: "project-a", diff --git a/apps/web/src/rightPanelStore.ts b/apps/web/src/rightPanelStore.ts index 6731d1dea773..2aa1b2ee8a38 100644 --- a/apps/web/src/rightPanelStore.ts +++ b/apps/web/src/rightPanelStore.ts @@ -93,7 +93,8 @@ const RIGHT_PANEL_STORAGE_KEY = "styal:right-panel-state:v2"; // v10 keys pull-request surfaces by reference instead of a singleton tab. // v11 stops persisting the pull-request list's shared panel, so a restart opens the page fresh. // v12 adds the device surface. -const RIGHT_PANEL_STORAGE_VERSION = 13; +// v14 resolves URL-only pull request hosts before the panel mounts. +const RIGHT_PANEL_STORAGE_VERSION = 14; /** A fixed workspace-level ref: each PR surface carries its own real environment. */ export const PULL_REQUESTS_PANEL_REF = scopeThreadRef( @@ -256,12 +257,18 @@ export function pullRequestSurface(target: { number: number; url?: string; }): PullRequestSurface { + // Linked references carry the host in their URL. Resolve it before rendering so an explicit + // link to the same PR cannot change the panel's React key or its query/snapshot cache keys. + const host = + target.host?.toLowerCase() ?? + (typeof target.url === "string" ? parseChangeRequestUrl(target.url)?.host : undefined); return { + // Keep historical tab IDs: upsertSurface already matches explicit and URL-derived hosts. id: pullRequestSurfaceId(target), kind: "pull-request", ...(target.environmentId === undefined ? {} : { environmentId: target.environmentId }), projectId: target.projectId, - ...(typeof target.host === "string" ? { host: target.host.toLowerCase() } : {}), + ...(host === undefined ? {} : { host }), repository: target.repository, number: target.number, ...(typeof target.url === "string" ? { url: target.url } : {}), @@ -429,10 +436,17 @@ export function migratePersistedRightPanelState(persistedState: unknown): { const { environmentId, ...rest } = surface; // Anything else stored under that name is not an environment. return [ - pullRequestSurface({ - ...rest, - ...(typeof environmentId === "string" ? { environmentId } : {}), - }), + { + ...pullRequestSurface({ + ...rest, + ...(typeof environmentId === "string" ? { environmentId } : {}), + }), + // Resolving the host must not change the selected tab on reload. + ...(typeof surface.id === "string" && + surface.id.startsWith("pull-request:") + ? { id: surface.id } + : {}), + }, ]; } if (surface.kind !== "terminal") return [surface];