P3 — Typed SPA chat (Phase P webUI) #1151

Merged
Mos merged 6 commits from feat/webui-p3-chat into next 2026-08-10 22:57:07 +00:00
Contributor

P3 — Typed SPA chat (Phase P webUI)

Brings the typed SPA chat surface to parity for the Vite SPA app, speaking origin-relative /chat (same-origin io('/chat')). Delivered as increments P3-1…P3-6 with independent integrator-verify + code + security review at each gate (author ≠ reviewer).

Commits

  • feat(web): add typed SPA chat + lifecycle hardening + re-review remediations
  • d46a2d67 sanitize command errors, harden turn-lock & resource caps (re-review#3)
  • 4aaf41dd sanitize all executor catches; lock pre-start turn boundary (re-review#4)
  • 7d84e4ee sanitize raw exceptions in /mcp status + /reload sources (re-review#5 blocker)

Security posture (verified)

  • All gateway command-executor catches + the /mcp status and /reload source services log raw exceptions server-side only; client-facing fields carry generic strings. Non-tautological specs assert marker-absent-from-client / marker-present-in-logger.
  • Turn-lock at the achievable client-only boundary; full wire-turnId correlation deferred to P5 (payloads carry no turn identity — documented residual).

Deferred follow-ups (out of P3 scope, tracked)

  • Wire-turnId correlation on agent:end/error (packages contract change) — P5.
  • GW-chat-hardening WS/Origin handshake — P5 precondition.
  • Tool-result error hygiene ([minor], indirect, pre-existing): built-in agent tools (incl. MCP tool-call failure, mcp-client.service.ts:262) return raw exception text into model context; not a direct DOM sink. Recommend a dedicated genericization pass across the tool suite.

Gates (independently re-derived on 7d84e4ee)

gateway typecheck / lint / test (673 passed, 0 failed) / build = green · frozen-lock clean · format:check clean · scope confined per increment. Clean merge against next (no conflicts).

## P3 — Typed SPA chat (Phase P webUI) Brings the typed SPA chat surface to parity for the Vite SPA app, speaking origin-relative `/chat` (same-origin `io('/chat')`). Delivered as increments P3-1…P3-6 with independent integrator-verify + code + security review at each gate (author ≠ reviewer). ### Commits - `feat(web): add typed SPA chat` + lifecycle hardening + re-review remediations - `d46a2d67` sanitize command errors, harden turn-lock & resource caps (re-review#3) - `4aaf41dd` sanitize all executor catches; lock pre-start turn boundary (re-review#4) - `7d84e4ee` sanitize raw exceptions in `/mcp status` + `/reload` sources (re-review#5 blocker) ### Security posture (verified) - All gateway command-executor catches + the `/mcp status` and `/reload` source services log raw exceptions **server-side only**; client-facing fields carry generic strings. Non-tautological specs assert marker-absent-from-client / marker-present-in-logger. - Turn-lock at the achievable client-only boundary; full wire-`turnId` correlation deferred to P5 (payloads carry no turn identity — documented residual). ### Deferred follow-ups (out of P3 scope, tracked) - **Wire-`turnId`** correlation on `agent:end`/`error` (packages contract change) — P5. - **GW-chat-hardening** WS/Origin handshake — P5 precondition. - **Tool-result error hygiene** (`[minor]`, indirect, pre-existing): built-in agent tools (incl. MCP tool-call failure, mcp-client.service.ts:262) return raw exception text into model context; not a direct DOM sink. Recommend a dedicated genericization pass across the tool suite. ### Gates (independently re-derived on 7d84e4ee) gateway typecheck / lint / test (673 passed, 0 failed) / build = green · frozen-lock clean · format:check clean · scope confined per increment. Clean merge against `next` (no conflicts).
Mos added 6 commits 2026-08-10 22:31:05 +00:00
Bring the chat experience into the Vite/React-Router SPA on the exact typed
Socket.IO /chat contract from @mosaicstack/types, replacing the /chat
placeholder behind AuthGuard. Surfaces message:ack (with an accessible
status), agent:start, streamed agent:text/agent:thinking, tool start/end
status, agent:end with usage, session:info (thinking controls + routing
decision), commands:manifest, command:result, command:approval (with a
one-time approved-run affordance), system:reload (refreshing the rendered
manifest), and error, and emits message/abort/set:thinking/command:execute/
command:approve with exact payloads.

The gateway does not guarantee message:ack is the first event for a new
conversation (session:info, and error on auth/session-creation failure, can
both arrive first) — conversation-scoped events now adopt the conversation
from whichever scoped event names it first while a send is pending, then
filter everything else against that established conversation. A typed error
stops streaming instead of leaving Stop stuck active; agent:end no longer
appends an empty assistant turn when there is no text or thinking; and a
second message can no longer be sent while a turn is streaming.

Command approval is now integrity-checked end to end: only one
command:approve request may be outstanding at a time (a concurrent request
is ignored rather than overwriting the pending command/args), a stale or
mismatched command:approval response cannot replace active approval state,
and running an approved command clears its approval state immediately (via
a ref, before React re-renders) so a double-click cannot replay
command:execute.

The `/chat` socket is now typed at a single boundary: apps/web/src/lib/
socket.ts narrows socket.io-client's untyped `io()` return value to
`ChatSocket` (Socket<ServerToClientEvents, ClientToServerEvents>) once, at
creation, via the one assertion the library's types force; every consumer
(use-chat-connection.ts) then gets fully checked `on`/`emit` calls with no
further casts. The shared contract types live in the new
apps/web/src/lib/chat-contract.ts (replacing the old spa/chat/types.ts
shim), which re-exports them via type-only imports resolved directly
against packages/types/src (apps/web has no @mosaicstack/types package
dependency, so this stays source-only and is erased at compile time —
no package manifest or lockfile is touched). The two recorded-event test
suites now drive a shared, typed fake socket
(spa/chat/test-support/fake-chat-socket.ts) instead of an untyped
`(event: string, payload: unknown)` harness, so a wrong event name or
malformed payload fails to compile.
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
Mos merged commit e00cc475a2 into next 2026-08-10 22:57:07 +00:00
Sign in to join this conversation.