diff --git a/e2e/v1427-coach-launch.spec.ts b/e2e/v1427-coach-launch.spec.ts index 0cacdaaa8..3ca8c9604 100644 --- a/e2e/v1427-coach-launch.spec.ts +++ b/e2e/v1427-coach-launch.spec.ts @@ -181,4 +181,43 @@ test.describe("v1.4.27 — Coach reachability on insights sub-pages", () => { await expect(drawer).toBeVisible({ timeout: 10_000 }); await expect(drawer).toHaveAttribute("data-variant", "side-sheet"); }); + + test("conversation history is collapsed behind a toggle that opens the tray on Desktop Chrome", async ({ + page, + }, testInfo) => { + // v1.12.0 — the history rail is no longer an always-on left column; + // it lives behind the "Conversations" toggle on every viewport and + // opens as a tray on demand so the thread keeps the full width. + test.skip( + testInfo.project.name !== "chromium-desktop", + "exercises the desktop drawer layout", + ); + + await page.goto("/insights/blutdruck", { waitUntil: "domcontentloaded" }); + + const icon = page.locator('[data-slot="coach-launch-icon"]').first(); + await expect(icon).toBeVisible({ timeout: 10_000 }); + await icon.click(); + + const drawer = page.locator('[data-slot="coach-drawer"]'); + await expect(drawer).toBeVisible({ timeout: 10_000 }); + + // No inline history column — only the toggle. + await expect( + page.locator('[data-slot="coach-drawer-history"]'), + ).toHaveCount(0); + const historyToggle = page.locator( + '[data-slot="coach-drawer-history-tray-trigger"]', + ); + await expect(historyToggle).toBeVisible({ timeout: 10_000 }); + + // The history rail surfaces only after the toggle is pressed. + await expect( + page.locator('[data-slot="coach-drawer-history-tray"]'), + ).toHaveCount(0); + await historyToggle.click(); + await expect( + page.locator('[data-slot="coach-drawer-history-tray"]'), + ).toBeVisible({ timeout: 10_000 }); + }); }); diff --git a/src/app/api/insights/chat/route.ts b/src/app/api/insights/chat/route.ts index bd612c595..4a944a0d7 100644 --- a/src/app/api/insights/chat/route.ts +++ b/src/app/api/insights/chat/route.ts @@ -100,6 +100,20 @@ function tokeniseForStreaming(content: string): string[] { return matches ?? [content]; } +/** + * v1.12.0 — yield control back to the event loop for one tick so the + * stream controller flushes the just-enqueued frame before the next one + * is produced. `setTimeout(0)` (rather than a bare `Promise.resolve()` + * microtask) hands the turn back to the platform's stream pump so each + * SSE frame lands in its own network chunk; a microtask would drain + * before the runtime gets a chance to flush. The delay is intentionally + * zero — we want incremental delivery, not an artificial typewriter + * pause. + */ +function flushTick(): Promise { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + interface CoachTurn { role: "user" | "assistant"; content: string; @@ -485,9 +499,20 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`; }); // ── Stream the body to the client ──────────────────────────── - const stream = createSseStream((controller) => { + // v1.12.0 — yield to the event loop between token frames so each one + // flushes as its own network chunk. The provider clients return the + // full reply in one shot; without the yield the whole tokenised body + // was enqueued synchronously inside `start()` and the runtime + // coalesced every frame into a single read, so the client painted the + // answer all at once despite the per-token render path in `use-coach`. + // A zero-delay yield is enough to land each frame on its own tick — + // the visible cadence reads ChatGPT/Claude-style without a contrived + // sleep. The refusal + error paths stay single-frame (nothing to + // pace). + const stream = createSseStream(async (controller) => { for (const tok of tokeniseForStreaming(replyText)) { controller.enqueue(encodeFrame({ type: "token", token: tok })); + await flushTick(); } controller.enqueue( encodeFrame({ diff --git a/src/components/insights/coach-panel/__tests__/coach-drawer-mobile-trays.test.tsx b/src/components/insights/coach-panel/__tests__/coach-drawer-mobile-trays.test.tsx index 186a12705..63831a031 100644 --- a/src/components/insights/coach-panel/__tests__/coach-drawer-mobile-trays.test.tsx +++ b/src/components/insights/coach-panel/__tests__/coach-drawer-mobile-trays.test.tsx @@ -7,15 +7,15 @@ import { CoachDrawerBody } from "../coach-drawer-body"; /** * v1.4.20 phase B4 — Coach drawer mobile rail trays. * - * The drawer hides the history + sources rails on `