diff --git a/ui/src/components/task-side-panel/TaskSidePanel.test.tsx b/ui/src/components/task-side-panel/TaskSidePanel.test.tsx index 3980ce51992..ca17885d0e1 100644 --- a/ui/src/components/task-side-panel/TaskSidePanel.test.tsx +++ b/ui/src/components/task-side-panel/TaskSidePanel.test.tsx @@ -236,6 +236,28 @@ describe("TaskSidePanel", () => { expect(routeFixture.navigate).not.toHaveBeenCalled(); }); + it("preserves an explicitly linked document when artifacts arrive", async () => { + fixture.documents = [issueDocument("agents", "AGENTS.md")]; + const documentDeepLink = { requestId: 1, documentKey: "agents" }; + await render(panel({ documentDeepLink, artifactsOpenRequestId: 1 })); + expect(container.querySelector('[role="tab"][aria-selected="true"]')?.textContent).toContain("AGENTS.md"); + expect(container.querySelectorAll('[data-side-panel-tab-target="artifacts"]')).toHaveLength(1); + }); + + it("handles each document link once without overriding later manual selection", async () => { + fixture.documents = [issueDocument("agents", "AGENTS.md")]; + const documentDeepLink = { requestId: 1, documentKey: "agents" }; + await render(panel({ documentDeepLink })); + await act(async () => container.querySelector("#side-panel-tab-properties")?.click()); + fixture.documents = [...fixture.documents, issueDocument("skill", "SKILL.md")]; + await render(panel({ documentDeepLink: { ...documentDeepLink }, artifactsOpenRequestId: 1 })); + expect(container.querySelector('[role="tab"][aria-selected="true"]')?.textContent).toContain("Properties"); + expect(container.querySelectorAll('[data-side-panel-tab-target="artifacts"]')).toHaveLength(1); + + await render(panel({ documentDeepLink: { ...documentDeepLink, requestId: 2 } })); + expect(container.querySelector('[role="tab"][aria-selected="true"]')?.textContent).toContain("AGENTS.md"); + }); + it("keeps a document selected when an artifact arrives", async () => { fixture.documents = [issueDocument("report", "Report")]; const documentDeepLink = { documentKey: "report", requestId: 1 }; diff --git a/ui/src/components/task-side-panel/TaskSidePanel.tsx b/ui/src/components/task-side-panel/TaskSidePanel.tsx index 5ffff70deb7..f5c11e0e291 100644 --- a/ui/src/components/task-side-panel/TaskSidePanel.tsx +++ b/ui/src/components/task-side-panel/TaskSidePanel.tsx @@ -270,6 +270,7 @@ export function TaskSidePanel({ const userInteractedRef = useRef(restoredRef.current?.userInteracted ?? false); const autoPlanHandledRef = useRef(restoredRef.current?.autoPlanHandled ?? false); const handledArtifactsRequestRef = useRef(undefined); + const handledDocumentRequestRef = useRef(undefined); const initialState = useMemo(() => { const restored = restoredRef.current?.state; let tabs = restored?.tabs ?? (issue.conversationAgentId ? [taskPanelArtifactsTab()] : [taskPanelPropertiesTab()]); @@ -356,13 +357,15 @@ export function TaskSidePanel({ }, [controller.openTab, planDocument]); useEffect(() => { - if (!documentDeepLink) return; + if (!documentDeepLink || handledDocumentRequestRef.current === documentDeepLink.requestId) return; if ( documentDeepLink.documentKey === "plan" && planDocument === null ) return; const document = documents.find((candidate) => candidate.key === documentDeepLink.documentKey); const label = document ? documentDisplayTitle(document) : documentDeepLink.documentKey === "plan" ? "Plan" : documentDeepLink.documentKey; + // A refresh must not replay a link after the user selects another tab. + handledDocumentRequestRef.current = documentDeepLink.requestId; controller.openTab(taskPanelDocumentTab(documentDeepLink.documentKey, label)); }, [controller.openTab, documentDeepLink, documents, planDocument]); diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx index ba812bbc0a4..30294009db8 100644 --- a/ui/src/pages/IssueDetail.test.tsx +++ b/ui/src/pages/IssueDetail.test.tsx @@ -2162,6 +2162,29 @@ describe("IssueDetail", () => { }); }); + it("opens Artifacts for existing output without discarding a document deep link", async () => { + mockLocation.hash = "#document-agents"; + mockIssuesApi.get.mockResolvedValue(createIssue({ + documentSummaries: [{ + id: "agents-doc", companyId: "company-1", issueId: "issue-1", + key: "agents", title: "AGENTS.md", format: "markdown", + latestRevisionId: "revision-1", latestRevisionNumber: 1, + createdByAgentId: "agent-1", createdByUserId: null, + updatedByAgentId: "agent-1", updatedByUserId: null, + lockedAt: null, lockedByAgentId: null, lockedByUserId: null, + createdAt: new Date(), updatedAt: new Date(), + }], + })); + await act(async () => { + root.render(); + }); + await waitForAssertion(() => { + const props = mockOpenPanel.mock.calls.at(-1)?.[0]?.props.children?.props; + expect(props?.artifactsOpenRequestId).toBe(1); + expect(props?.documentDeepLink?.documentKey).toBe("agents"); + }); + }); + it.each([false, true])("registers new artifacts without opening a closed panel (mobile: %s)", async (isMobile) => { mockSidebarState.isMobile = isMobile; mockLocation.state = createIssueDetailLocationState("Inbox", "/inbox/mine", "inbox");