From f9d710de5312d12269441f8272a06b49efbb1f28 Mon Sep 17 00:00:00 2001 From: xgooz3x <79577106+xgooz3x@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:25:55 +1100 Subject: [PATCH] fix(web): reuse open issue tabs and keep the tracker filter reachable Opening an issue from a link reuses the tab the issues list already opened, and the tracker filter menu stays visible while a tracker is selected so "All trackers" can be chosen again. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/rightPanelStore.test.ts | 16 ++++++++++++++++ apps/web/src/rightPanelStore.ts | 14 ++++++++++++++ apps/web/src/routes/_chat.issues.tsx | 4 +++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/web/src/rightPanelStore.test.ts b/apps/web/src/rightPanelStore.test.ts index 2f4d8d6d70ed..8d883dbf97ff 100644 --- a/apps/web/src/rightPanelStore.test.ts +++ b/apps/web/src/rightPanelStore.test.ts @@ -39,6 +39,22 @@ async function reloadRightPanelStore() { } describe("rightPanelStore", () => { + it("reuses an open issue tab when the same issue is reopened without its host", () => { + const store = useRightPanelStore.getState(); + const issue = { + environmentId: "env-1", + projectId: "project-a", + tracker: "github", + scope: "xgooz3x/tolarian-academy", + key: "53", + }; + store.openIssue(refA, { ...issue, host: "github.com", url: "https://github.com/x/53" }); + store.openIssue(refA, issue); + const state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); + expect(state.surfaces.filter((surface) => surface.kind === "issue")).toHaveLength(1); + expect(state.activeSurfaceId).toBe(state.surfaces[0]!.id); + }); + it("records single and bulk tab closes, newest first", () => { const store = useRightPanelStore.getState(); const pr = pullRequestSurface({ diff --git a/apps/web/src/rightPanelStore.ts b/apps/web/src/rightPanelStore.ts index 7323e03c10c5..7b9f520305ed 100644 --- a/apps/web/src/rightPanelStore.ts +++ b/apps/web/src/rightPanelStore.ts @@ -805,6 +805,20 @@ export const useRightPanelStore = create()( openIssue: (ref, target) => set((state) => userAction(state, scopedThreadKey(ref), (current) => { + // A link (the page URL) names an issue without its host; reuse the tab a row opened. + const existing = + target.host === undefined + ? current.surfaces.find( + (entry) => + entry.kind === "issue" && + entry.environmentId === target.environmentId && + entry.projectId === target.projectId && + entry.tracker === target.tracker && + entry.scope.toLowerCase() === target.scope.toLowerCase() && + entry.key === target.key, + ) + : undefined; + if (existing) return { ...current, isOpen: true, activeSurfaceId: existing.id }; const surface = issueSurface(target); const next = upsertSurface(current, surface); return target.url diff --git a/apps/web/src/routes/_chat.issues.tsx b/apps/web/src/routes/_chat.issues.tsx index 8bbfe8b0b6b4..b85134996ff4 100644 --- a/apps/web/src/routes/_chat.issues.tsx +++ b/apps/web/src/routes/_chat.issues.tsx @@ -703,7 +703,9 @@ function IssuesRoute() { options={INVOLVEMENT_TABS} onChange={(who) => updateSearch({ who })} /> - {hostOptions.length > 2 ? ( + {/* The server filters by host, so a filtered page only knows its own tracker; keep + the menu while filtered so "All trackers" stays reachable. */} + {hostOptions.length > 2 || search.host ? (