feat(documents): open the doc-scoped Coach in the side drawer, not a full page

"Ask the Coach" on a document used to navigate to /coach?doc=<id> — a full-page
jump away from the vault. It now opens the real, doc-scoped conversation in the
shared side drawer, and the drawer's maximize control expands it to the full
/coach page while preserving scope.

- Thread an optional documentId through the launch context: askCoach gains a
  fourth argument, the provider tracks it alongside prefill/scope and clears it
  on close, and LayoutCoachMount forwards it to the drawer as initialDocumentId.
  The open-state precedence moves into a pure resolveLaunchState helper.
- The vault detail sheet closes itself and calls askCoach(..., doc.id) instead
  of routing away, so the drawer owns the surface.
- Maximize preserves continuity: the drawer reads the live conversation id via
  a new imperative getter out of the conversation surface (parallel to the
  reset registration) and hands off to /coach?c=<id> once a thread exists,
  falling back to /coach?doc=<id> for a chat maximized before its first turn.

The send path is untouched: every document turn still routes through the
hardened fenced endpoint keyed on documentId — only where the conversation
renders (drawer vs page) changed. The documents-chat e2e now asserts the
drawer opens in place, doc-scoped, rather than a URL change.
This commit is contained in:
Marc-André Bombeck committed 2026-07-16 22:34:17 +02:00
1 parent 7c96abc8ec
commit d72f078764
9 files changed
+390 -51

No files matched your search

+26 -18
View File
@@ -1,19 +1,21 @@
/**
* Document vault — the "chat about a document" entry point (v1.28.51, R3).
* Document vault — the "chat about a document" entry point (v1.28.52, R3).
*
* The bespoke in-sheet chat drawer is gone (Documents R3, Design A). Asking the
* Coach about a document now hands off to the REAL Coach, scoped to that
* document: the detail sheet's Coach affordance navigates to `/coach?doc=<id>`,
* where the conversation runs on the same hardened fenced endpoint but inside
* the Coach's own surface and history.
* The bespoke in-sheet chat drawer is gone (Documents R3). Asking the Coach
* about a document now opens the REAL Coach conversation, scoped to that
* document, in the shared SIDE DRAWER: the detail sheet's Coach affordance
* closes the sheet and opens the coach drawer pre-scoped to the document (its
* maximize control expands to the full `/coach` page). The conversation runs on
* the same hardened fenced endpoint — only WHERE it renders changed.
*
* This suite pins the documents-side contract: the affordance is offered only
* when a provider is available, and it navigates to the doc-scoped Coach; with
* NO provider the AI area collapses to the settings pointer and no Coach entry
* renders at all. The Coach-side rendering (the "Chatting about" scope banner +
* the not-indexed hint) is covered by the coach-conversation component test; the
* fenced-send guarantee (a document turn never reaches the tool route) by the
* coach-send-target + persistence-scope unit tests.
* when a provider is available, and it OPENS THE DOC-SCOPED DRAWER in place (no
* full-page nav); with NO provider the AI area collapses to the settings
* pointer and no Coach entry renders at all. The Coach-side rendering (the
* "Chatting about" scope banner + the not-indexed hint) is covered by the
* coach-conversation component test; the fenced-send guarantee (a document turn
* never reaches the tool route) by the coach-send-target + persistence-scope
* unit tests.
*
* v1.27.31 lesson kept: the sheet's AI area depends on the capability probe +
* usage endpoints, so both are mocked — otherwise the AI area never renders and
@@ -94,9 +96,9 @@ test.describe("document vault — chat about a document", () => {
test.slow();
});
// ── (a) Provider available: the Coach entry hands off to the doc-scoped Coach ──
// ── (a) Provider available: the Coach entry opens the doc-scoped drawer ──
test("offers the Coach entry and hands off to the document-scoped Coach", async ({
test("offers the Coach entry and opens the document-scoped Coach drawer in place", async ({
page,
}) => {
await mockAiEnabled(page);
@@ -106,14 +108,20 @@ test.describe("document vault — chat about a document", () => {
await expect(sheet).toBeVisible();
// The neutral Coach affordance lives in the detail-sheet AI area; tapping it
// navigates to the real Coach seeded with this document.
// closes the sheet and opens the REAL coach drawer scoped to this document.
const open = sheet.locator('[data-slot="document-chat-open"]');
await expect(open).toBeVisible();
await open.click();
// The documents-side contract: the URL hands off to the doc-scoped Coach.
await page.waitForURL(new RegExp(`/coach\\?doc=${CONTENT_DOC_ID}`));
expect(new URL(page.url()).searchParams.get("doc")).toBe(CONTENT_DOC_ID);
// The documents-side contract: the coach drawer opens IN PLACE (no
// full-page nav) scoped to this document — its doc-scope banner renders.
const drawer = page.locator('[data-slot="coach-drawer"]');
await expect(drawer).toBeVisible({ timeout: 10_000 });
await expect(drawer.locator('[data-slot="coach-doc-scope"]')).toBeVisible({
timeout: 10_000,
});
// Still on /documents — the drawer expands to the page only via maximize.
expect(new URL(page.url()).pathname).not.toContain("/coach");
});
// ── (b) No provider: the calm settings pointer, no Coach entry at all ──