fix: render the seeded first-message on the web UI (v0.19.9)

The #347 auto-seed worked (the greeting was in the session ledger at seq-0),
but the web UI never showed it: there was no GET /api/sessions/{id}/messages
route and startSession() went straight from create to persona/tools/admin
hydration, so the transcript only filled from the live turn stream + user
echoes — a seeded turn-0 was invisible.

- server: new proxy route GET /api/sessions/{id}/messages -> get_session_messages
  (mirrors the tools/bifrost proxies; status-preserving envelope).
- SPA: loadTranscript(sessionId) fetches it on open and renders existing turns
  (assistant -> .response .md-body via markdownSafe escape-first; user ->
  .prompt-echo via textContent), called after the workspace opens. Best-effort.

web_debug_surface contract amended (endpoint + loadTranscript). 2 web route
tests, suite 617 green. Playwright DOM check proved the render end-to-end
(drive the real UI -> Sindra's greeting bubble appears).
This commit is contained in:
2026-07-06 15:19:56 -07:00
parent be171304f5
commit 263ec2917b
7 changed files with 105 additions and 2 deletions
@@ -35,6 +35,17 @@ functions:
steps: "Open client_factory() client; await get_session_tools(client, session_id); return 200. Except SessionApiFailed -> status-preserving envelope."
flexibility: "prescriptive"
- name: "_session_messages_endpoint"
signature: "async _session_messages_endpoint(request: Request) -> JSONResponse"
description: "GET /api/sessions/{session_id}/messages — proxy the session's message history so the SPA renders existing turns on open (notably a #347 authored first-message seeded at create-time; without it a seeded session's transcript is blank until the user speaks)."
preconditions:
- "session_id in path_params."
postconditions:
- "POST-001: 200 with the upstream {session_id, items, next_cursor} dict verbatim on success."
- "POST-002: on SessionApiFailed(status) -> JSONResponse({error_code:'session_messages_unavailable', status}, status_code=status) — status-preserving."
steps: "Open client_factory() client; await get_session_messages(client, session_id); return 200. Except SessionApiFailed -> status-preserving envelope."
flexibility: "prescriptive"
- name: "_session_bifrost_endpoint"
signature: "async _session_bifrost_endpoint(request: Request) -> JSONResponse"
description: "GET /api/sessions/{session_id}/bifrost — proxy admin-scoped Bifrost dispatch state."
@@ -95,6 +106,14 @@ functions:
- "POST-002: a scheduled poll no-ops if a NEW turn has started (state.turnId truthy) or already settled — no refresh of a stale agent, no unbounded polling."
flexibility: "open"
- name: "loadTranscript (index.html)"
signature: "async loadTranscript(sessionId) -> void"
description: "On session open, GET /api/sessions/{id}/messages and render each EXISTING turn into #transcript — notably a #347 authored first-message seeded at create-time (which lives in the ledger, not the live turn stream, so without this the transcript is blank until the user speaks)."
postconditions:
- "POST-001: assistant items render as a .response .md-body bubble via markdownSafe(content) (escape-first whitelist, same path as appendResponse); user items render as a .prompt-echo via textContent — no upstream content reaches innerHTML unescaped (INV-004)."
- "POST-002: any non-200, fetch error, or parse error is swallowed (best-effort) — a blank transcript is acceptable; opening the workspace is never blocked."
flexibility: "prescriptive"
- name: "web pane renderers (index.html: renderToolsInventory / renderBifrostState / openAdminEvents)"
signature: "renderToolsInventory(inv) ; renderBifrostState(b) ; openAdminEvents(sessionId)"
description: "Render the three new surfaces; all content escaped (INV-004)."