From 654177cb525d9af4e50d5599841e0dd24c32d70c Mon Sep 17 00:00:00 2001 From: katalinawinemixer Date: Thu, 28 May 2026 21:07:49 -0700 Subject: [PATCH] Keep saved conversation opens in saved routes --- app/src/components/RunDetail.tsx | 9 +++- app/tests-e2e/workshop-actions.spec.ts | 68 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/app/src/components/RunDetail.tsx b/app/src/components/RunDetail.tsx index c912a4a..23ba52d 100644 --- a/app/src/components/RunDetail.tsx +++ b/app/src/components/RunDetail.tsx @@ -1103,6 +1103,13 @@ export function RunDetail({ runId, routeBase, initialData, isReplay, source, onF }, [navigate, routeBase, runId], ); + const openConversationTurn = useCallback((id: string) => { + if (routeBase === "/saved" && (id === runId || isEventSaved(id))) { + navigate(tracePath("/saved", id)); + return; + } + navigate(runPath(id)); + }, [navigate, routeBase, runId]); // When another surface fires a span deep-link, open it in the span tree route. useEffect(() => { @@ -1389,7 +1396,7 @@ export function RunDetail({ runId, routeBase, initialData, isReplay, source, onF {activeTab === "convo" && run.convo_id && ( source === "cloud" ? - : navigate(runPath(id))} /> + : )} diff --git a/app/tests-e2e/workshop-actions.spec.ts b/app/tests-e2e/workshop-actions.spec.ts index 1b8f3cc..81316a7 100644 --- a/app/tests-e2e/workshop-actions.spec.ts +++ b/app/tests-e2e/workshop-actions.spec.ts @@ -13,6 +13,7 @@ import { readWorkshopRun, readWorkshopSpans } from "./helpers"; // is the source of truth — if it changes, update these numbers in lock // step with that change. const FIXTURE_RUN_ID = "00000000000000000000000000000001"; +const FIXTURE_CONVO_SIBLING_RUN_ID = "00000000000000000000000000000002"; const FIXTURE_EVENT_NAME = "code-agent"; const FIXTURE_SPAN_COUNT = 6; @@ -30,6 +31,38 @@ async function clearWorkshop(workshopUrl: string) { expect(res.ok, `POST /api/clear -> ${res.status}`).toBe(true); } +async function saveRun(workshopUrl: string, runId: string) { + const detailRes = await fetch(`${workshopUrl}/api/runs/detail/${encodeURIComponent(runId)}`); + expect(detailRes.ok, `GET /api/runs/detail/${runId} -> ${detailRes.status}`).toBe(true); + const detail = await detailRes.json() as { + run: { + id: string; + event_name?: string | null; + name?: string | null; + user_id?: string | null; + convo_id?: string | null; + started_at: number; + }; + }; + const run = detail.run; + const saveRes = await fetch(`${workshopUrl}/api/saved-runs/events/${encodeURIComponent(runId)}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: run.id, + event_name: run.event_name ?? run.name ?? run.id.slice(0, 12), + user_id: run.user_id ?? null, + convo_id: run.convo_id ?? null, + timestamp: new Date(run.started_at).toISOString(), + user_input: "Fix the typo in README.md", + assistant_output: null, + saved_at: Date.now(), + source: "local", + }), + }); + expect(saveRes.ok, `PUT /api/saved-runs/events/${runId} -> ${saveRes.status}`).toBe(true); +} + // Each test gets a clean slate. Tests that need fixtures call seedFixtures // themselves so it's obvious in the spec what data each one depends on. test.beforeEach(async ({ workshop }) => { @@ -191,6 +224,41 @@ test("workshop UI: switching between runs preserves each run's span tree", async await expect(page.locator(`[data-span-row="${spansB[0].id}"]`)).toHaveCount(0); }); +test("workshop UI: saved conversation open preserves saved/unsaved route targets", async ({ page, workshop }) => { + await seedFixtures(workshop.url); + await saveRun(workshop.url, FIXTURE_RUN_ID); + + await page.goto(`${workshop.url}/saved/${FIXTURE_RUN_ID}/convo`); + await expect(page.getByRole("button", { name: /^convo$/i })).toBeVisible({ timeout: 10_000 }); + await expect(page.getByText(/^conversation$/i)).toBeVisible({ timeout: 10_000 }); + + const openButtons = page.getByRole("button", { name: /^open →$/i }); + await expect(openButtons).toHaveCount(2); + + const openSavedTurn = openButtons.first(); + await expect(openSavedTurn).toBeVisible({ timeout: 5_000 }); + await openSavedTurn.click(); + await expect(page).toHaveURL(new RegExp(`/saved/${FIXTURE_RUN_ID}(?:[/?#]|$)`)); + + await page.goto(`${workshop.url}/saved/${FIXTURE_RUN_ID}/convo`); + await expect(openButtons).toHaveCount(2); + await openButtons.nth(1).click(); + await expect(page).toHaveURL(new RegExp(`/runs/${FIXTURE_CONVO_SIBLING_RUN_ID}(?:[/?#]|$)`)); +}); + +test("workshop UI: saved conversation open keeps saved sibling turns in saved route", async ({ page, workshop }) => { + await seedFixtures(workshop.url); + await saveRun(workshop.url, FIXTURE_RUN_ID); + await saveRun(workshop.url, FIXTURE_CONVO_SIBLING_RUN_ID); + + await page.goto(`${workshop.url}/saved/${FIXTURE_RUN_ID}/convo`); + const openButtons = page.getByRole("button", { name: /^open →$/i }); + await expect(openButtons).toHaveCount(2); + + await openButtons.nth(1).click(); + await expect(page).toHaveURL(new RegExp(`/saved/${FIXTURE_CONVO_SIBLING_RUN_ID}(?:[/?#]|$)`)); +}); + test("workshop UI: deleting a run via API removes it from the sidebar", async ({ page, workshop }) => { await seedFixtures(workshop.url);