# Ship Report — optimize-web-ui-mvp

**Date**: 2026-05-15
**Change**: `openspec/changes/optimize-web-ui-mvp/`
**Branch**: `feat/optimize-web-ui-mvp`
**Worktree**: `.worktrees/optimize-web-ui-mvp/`
**Status**: Code complete, awaiting browser-based verification + merge

---

## What shipped

A 7-day MVP refactor of the Web UI in 5 commits across 3 phases:

| Phase | Commit | Slice |
|---|---|---|
| 1 | `feat(web): phase 1 F-slice — zustand stores + useChatSocket hook` | F |
| 1 | `feat(web): phase 1 A-slice — virtualized message list + rAF auto-scroll` | A |
| 2 | `feat(web): phase 2 — execution trace panel + shiki + typed bubbles` | D + B partial |
| 2 | `feat(web): phase 2 B-slice — skeletons + empty state + error banner + retry` | B remainder |
| 3 | `feat(web): phase 3 — cmdK command palette + global shortcuts` | C (without CRUD) |

`Chat.tsx` is 328 lines (after Phase 3 added cmdK + shortcuts + retry handlers); the original was 387 lines but mostly inline WS dispatch + 7 useState hooks. Now state lives in 4 zustand slices, the WS dispatch is in `useChatSocket`, REST session loading is in `useSessionLoader`, and Chat.tsx is purely a state-orchestrator + render layer. (Phase 1 ended at 200 lines as planned; Phases 2-3 added handlers without re-introducing state-management logic.)

## Bundle delta

Measured via `cd web && npm run build`:

| Phase | main JS gzip | Δ from previous | Cumulative |
|---|---|---|---|
| Baseline (pre-change) | 118.04 KB | — | 0 |
| Phase 1 F (zustand + hooks) | 120.66 KB | +2.62 KB | +2.62 KB |
| Phase 1 A (react-virtual) | 126.19 KB | +5.53 KB | +8.15 KB |
| Phase 2 D + B partial | 128.44 KB | +2.25 KB | +10.40 KB |
| Phase 2 B remainder | 130.33 KB | +1.89 KB | +12.29 KB |
| Phase 3 C | 132.87 KB | +2.54 KB | **+14.83 KB** |

**Within design budget**: the spec set a 30 KB total cap; we used 14.83 KB.

Lazy chunks (loaded on first code-block render only — do not affect first-paint):

| Chunk | Raw | Gzip |
|---|---|---|
| shiki core | 86.67 KB | 27.50 KB |
| github-dark theme | 11.41 KB | 2.56 KB |
| github-light theme | 11.18 KB | 2.52 KB |
| oniguruma engine | 6.84 KB | 2.65 KB |
| oniguruma wasm | 622.34 KB | 231.16 KB |

(The wasm chunk only streams in once a code block is actually rendered; the chat itself paints with the existing 132.87 KB main bundle.)

## Acceptance criteria status

| Criterion | Status | Evidence |
|---|---|---|
| `Chat.tsx` no longer a god-component | ✅ | 328 lines (Phase 1 hit 200; Phases 2-3 added 128 lines of new feature handlers). All state moved to zustand; no inline WS dispatch. |
| `lsp_diagnostics` clean across `web/src/` | ✅ | 0 errors |
| `npm run build` exit 0 | ✅ | 1916 modules, 1.7s |
| Bundle delta < 30 KB gzip | ✅ | +14.83 KB |
| Stable bugfixes preserved | ✅ | `38cf0ee` slash classifier, `7f32016` SERVER_ERROR, `0ad16c0` /stop, `4bbd026` session switch — all paths preserved verbatim in useChatSocket |
| Dual-theme parity | ⏳ | Component code uses `--c-*` CSS vars only; visual screenshot verification deferred to manual smoke |
| Profiler: 200-msg session commit < 16ms | ⏳ | Requires real browser; deferred to manual smoke |
| Playwright critical paths | ⏳ | Requires real backend + browser; deferred to manual smoke |

## Backend touchpoints encountered

Per `protocol-audit.md` Phase 0 audit, the following downgrades were applied (no backend was modified):

| Concern | Backend reality | UI behavior |
|---|---|---|
| `chat.done` token usage | Provided ✅ | Renders in metadata footer |
| `chat.done` model name | Not provided ❌ | Field hidden in metadata footer |
| `chat.done` server duration | Not provided ❌ | Client wall-clock duration (first delta → done) |
| Memory hits (L1/L2/L3/L4) | Never sent ❌ | "Memory hits: protocol pending" placeholder |
| `chat.tool_end` ok/err marker | Not provided ❌ | Defaults to `ok`; refinement deferred |
| `chat.thinking` events | Not present ❌ | Section omitted |
| `PATCH /api/web/sessions/:id` | Cross-data-model change required | Rename UI dropped (option C) |
| `delete_session` is NO-OP | Existing bug | Delete UI dropped (option C) |

## Out of scope follow-ups (open as separate work)

These were explicitly deferred and should be tracked as new OpenSpec changes or issues:

1. **Backend WS protocol extensions** (single change, scoped to `src/pyclaw/channels/web/`):
   - Add `model` to `chat.done`
   - Add `duration_ms` (server-measured) to `chat.done`
   - Add `is_error: bool` to `chat.tool_end`
   - Emit structured `memory_hits` from `ContextEngine.assemble` and surface as a streaming event
   - Optional: emit `thinking` blocks when the underlying provider supports them
2. **Backend `delete_session` bug**: `routes.py:179` is a NO-OP and currently leaks. Independent fix.
3. **Backend session rename**: requires `SessionHeader.title` schema change + `SessionStore` persistence + `PATCH /api/web/sessions/:id`. Cross-data-model.
4. **Frontend cleanup**: `types.ts` (now 173 lines) split into `protocol.ts` / `domain.ts`.
5. **Frontend tests**: Vitest + RTL basebuild; intentionally skipped during MVP.
6. **Frontend mobile**: current `Responsive layout` requirement explicitly defers sub-768px. Should be a follow-up change once delete/rename land.

## Verification gates (require human/browser)

Before merging, the following manual checks should be done:

1. **Critical path**: login → create session → send message → see streaming → click ⌘K → toggle theme → log out
2. **Code highlighting**: paste a Python snippet in chat, confirm Shiki theme matches active theme; toggle theme and confirm it switches synchronously
3. **Execution trace**: trigger a tool call, confirm panel shows `▸ tool_call` / `◂ tool_result`; verify metadata footer shows duration + tokens
4. **Long session**: load (or create) a session with 200+ messages; verify scrolling stays smooth; React Profiler commit time < 16ms during streaming
5. **Empty state**: open a fresh session, confirm 3 suggestion cards appear; click one, confirm it populates the textarea (does NOT auto-send)
6. **Error retry**: simulate a server error (kill backend mid-stream), confirm a red `Error` bubble with `Retry` button appears; click `Retry` resends the previous user message
7. **Disconnect banner**: kill backend cleanly, watch wifi icon pulse for two reconnect cycles, confirm red banner appears on the third failure with a `Reconnect` button
8. **Command palette**: ⌘K opens, type to filter, ↑↓ navigate, Enter selects. Sessions navigate, Actions execute, slash commands prefill the input without sending
9. **Shortcuts**: ⌘\ toggles sidebar, ⌘N creates session, ⌘/ opens shortcuts modal, Esc closes any modal/palette
10. **Dual theme**: each of the above tests passes in both `data-theme="dark"` and `data-theme="light"`

## Files touched

```
web/package.json                  +3 deps  (zustand, react-virtual, shiki)
web/package-lock.json             updated
web/src/index.css                 + @keyframes shimmer
web/src/types.ts                  + MessageMetadata, role 'error', WSServerChatDone widened
web/src/pages/Chat.tsx            387 → 328 lines (rewritten over store + hooks; Phase 1 hit 200, +128 from new Phase 2/3 handlers)
web/src/components/ChatArea.tsx   virtualized + suggestions + skeletons + retry plumb
web/src/components/MessageBubble.tsx  dispatch by role; ExecutionTrace embedded
web/src/components/MarkdownRenderer.tsx  fenced blocks → HighlightedCodeBlock
web/src/components/SessionSidebar.tsx  + isCreatingSession + skeleton row
web/src/components/ToolCall.tsx   removed (folded into ExecutionTrace)

new — components: ExecutionTrace, HighlightedCodeBlock, ErrorBubble, SystemMessage,
                  Skeleton, EmptyStateSuggestions, ErrorBanner, CommandPalette,
                  ShortcutsModal
new — hooks:      useChatSocket, useSessionLoader, useGlobalKeyboard
new — stores:     chat, session, ui, approval, index
new — lib:        highlighter (shiki lazy loader), fuzzy
```

## Next steps

1. Run the 10 verification gates above (manual + Playwright if available)
2. Merge `feat/optimize-web-ui-mvp` into `main` (fast-forward, 5 commits)
3. Open the 6 follow-up items above as either issues or new OpenSpec changes
4. Run `openspec` archive workflow on this change (deltas merge into `openspec/specs/web-frontend/`)
5. Remove worktree: `git worktree remove .worktrees/optimize-web-ui-mvp`
