diff --git a/surfaces/gui/e2e/artifacts.spec.ts b/surfaces/gui/e2e/artifacts.spec.ts index 0bcddcfe..ebd18224 100644 --- a/surfaces/gui/e2e/artifacts.spec.ts +++ b/surfaces/gui/e2e/artifacts.spec.ts @@ -36,3 +36,18 @@ test("HTML artifact offers Open in browser as the unsandboxed escape hatch", asy await openReport(page); await expect(page.getByTestId("artifact-open-browser")).toBeVisible(); }); + +test("a transcript chip opens the viewer on the FIRST click even with the rail hidden", async ({ + page, +}) => { + // Owner-hit 2026-08-15: the chip fires one event; the rail's select-listener was only + // registered while the rail was visible, so click #1 unhid an empty rail and the + // selection was lost — the viewer appeared only on a later click. + await page.goto("/"); + await page.getByPlaceholder(/Ask the coworker/).fill("show the report"); + await page.getByRole("button", { name: "Send" }).click(); + await page.getByRole("button", { name: "Hide side panel" }).click(); + + await page.getByTestId("artifact-chip").click(); + await expect(page.getByTestId("artifact-frame")).toBeVisible(); +}); diff --git a/surfaces/gui/e2e/fixtures.ts b/surfaces/gui/e2e/fixtures.ts index 0675eaa7..2350b2aa 100644 --- a/surfaces/gui/e2e/fixtures.ts +++ b/surfaces/gui/e2e/fixtures.ts @@ -610,6 +610,14 @@ export async function mockApi(page: import("@playwright/test").Page) { }); return; // suspended on the approval } + // A deliverable turn ending in an artifact chip (§34) — for the chip-open flow. + if (/show the report/i.test(msg.text)) { + send("assistant_message", { + text: "Done — [Security review](artifact:reports/security-review.html)", + }); + send("turn_done"); + return; + } // The pre-fix payload shape (owner-hit 2026-08-14): no installable/version/summary // — an older sidecar, or any surface that forgets the field. Must render NOT // installable, never a guessed Install offer. diff --git a/surfaces/gui/src/components/RightRail.tsx b/surfaces/gui/src/components/RightRail.tsx index ab6302bf..651ee917 100644 --- a/surfaces/gui/src/components/RightRail.tsx +++ b/surfaces/gui/src/components/RightRail.tsx @@ -118,8 +118,11 @@ export function RightRail({ // §34 (UX-016): [Title](artifact:path) chips in the transcript open the viewer directly. // Resolve against the loaded list first; on a miss, refresh once (the file may be // seconds old), then fall back to a minimal record — readArtifact validates the path. + // Registered even while the rail is HIDDEN (owner-hit 2026-08-15): the chip fires ONE + // event, and App's unhide listener and this one race it — gating this on `active` + // dropped the selection, so the first click only opened an empty rail. useEffect(() => { - if (!active) return; + if (!sessionId) return; const minimal = (path: string): ArtifactInfo => ({ path, name: path.split("/").pop() || path, @@ -146,7 +149,7 @@ export function RightRail({ }; window.addEventListener(OPEN_ARTIFACT_EVENT, onOpen); return () => window.removeEventListener(OPEN_ARTIFACT_EVENT, onOpen); - }, [active, sessionId, artifacts]); + }, [sessionId, artifacts]); if (!active) return null;