Skip to content

feat(chat): web chat client with streaming UI#66

Merged
mcheemaa merged 3 commits intomainfrom
project-4-chat-client
Apr 15, 2026
Merged

feat(chat): web chat client with streaming UI#66
mcheemaa merged 3 commits intomainfrom
project-4-chat-client

Conversation

@mcheemaa
Copy link
Copy Markdown
Member

Summary

Full chat client SPA for Phantom, connecting to the PR1 backend (#65, merged). Users can chat with Phantom via the browser with real-time streaming, tool activity cards, thinking blocks, and session management.

  • 62 source files, 5,200 LOC in chat-ui/ at repo root
  • 159KB gzipped (budget: 500KB) with manual chunk splitting
  • Vite + React 19 + TypeScript + shadcn/ui (new-york-v4) + Tailwind v4
  • Dockerfile chat-ui-builder stage for production builds

Architecture

  • useSyncExternalStore-backed chat store dispatching all 24 SSE wire events
  • SSE frame parser in use-chat hook with streaming state management
  • Tool card state machine: 8 states (pending, input_streaming, input_complete, running, result, error, aborted, blocked) with 10 per-tool renderers
  • Thinking blocks with auto-open during streaming, auto-close on response
  • Session sidebar with date grouping (Today/Yesterday/Last 7/Last 30/Older), pinned sessions
  • Warm cream token system: #faf9f5 light, #1a1917 dark, #4850c4 indigo accent
  • Keyboard shortcuts: Cmd+Shift+O (new), Cmd+. (stop), Cmd+Shift+D (theme), Cmd+K (palette)

Known gaps (fix pass)

  • Font files are HTML placeholders, not real WOFF2 binaries (system fallbacks work)
  • Markdown renderer is hand-rolled regex, not streamdown (React 19 compat gap) - needs proper library
  • Both will be addressed in the fix pass after review

Test plan

  • bun run --cwd chat-ui typecheck passes
  • bun run --cwd chat-ui build passes (159KB gzipped)
  • bun test server tests pass (1,485 tests, 0 failures, no regressions)
  • Visual testing: open /chat in browser, verify streaming, tool cards, sidebar
  • PR3 (auth, push) and PR4 (polish, attachments) follow on this branch

Full chat client SPA in chat-ui/ at repo root:
Vite + React 19 + TypeScript + shadcn/ui (new-york-v4) + Tailwind v4.

Core architecture:
- useSyncExternalStore-backed chat store dispatching all 24 SSE events
- SSE parser in use-chat hook with frame-by-frame state updates
- Tool card state machine (8 states, 10 per-tool renderers)
- Thinking blocks with auto-open/close on streaming transitions
- Session sidebar with date grouping and pinned sessions
- Auto-scroll with jump-to-bottom pill
- Warm cream/dark token system (#faf9f5 light, #1a1917 dark, #4850c4 accent)

Stack: 62 source files, 5,200 LOC, 159KB gzipped (budget: 500KB)
- 21 shadcn primitives in src/ui/
- 21 composites in src/components/
- 6 hooks, 6 lib modules, 4 routes
- Dockerfile chat-ui-builder stage

Known gaps (fix pass):
- Font files are HTML placeholders, not real WOFF2 (system fallbacks work)
- Markdown uses hand-rolled renderer, not streamdown (React 19 compat)
Inter variable (230KB), JetBrains Mono variable (72KB), Instrument
Serif (19KB), all Latin subset from Google Fonts gstatic CDN.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ceca7084ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread chat-ui/src/hooks/use-chat.ts Outdated
Comment on lines +34 to +35
let currentEvent = "";
let currentData = "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist SSE parser state across reader chunks

currentEvent and currentData are reinitialized on every reader.read() iteration, so SSE frames get dropped whenever event:, data:, and the terminating blank line arrive in different network chunks (a normal condition on streamed responses). In that case dispatchFrame is never called for those frames, which causes missing/delayed message text and tool/thinking updates during streaming.

Useful? React with 👍 / 👎.

Comment on lines +111 to +114
.then((detail: SessionDetail) => {
const msgs = detail.messages.map(messageRowToChatMessage);
store.update((s) => ({ ...s, messages: msgs, sessionId: id }));
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard session hydration from clobbering newer chat state

This callback unconditionally replaces messages with the snapshot returned by getSession, even if newer updates have already been applied to the store (for example when SessionRoute sends an initial message immediately after navigation, or when users switch sessions quickly). A late getSession response can therefore overwrite in-flight streamed frames and show stale/incorrect conversation content.

Useful? React with 👍 / 👎.

Comment thread chat-ui/src/components/markdown.tsx Outdated
Comment on lines +64 to +65
/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer" class="text-primary underline underline-offset-2 hover:text-primary/80">$1</a>',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Sanitize rendered markdown link URLs before HTML injection

The markdown renderer injects $2 directly into href and then renders via dangerouslySetInnerHTML; because quotes and URL schemes are not validated/escaped here, model-generated content can produce malicious links (e.g. javascript: URLs or attribute-breaking payloads). This creates an XSS vector in assistant-rendered messages.

Useful? React with 👍 / 👎.

P0: thinking blocks wired through useChat -> SessionRoute -> MessageList
(were tracked in store but never rendered)

P1 fixes:
- SSE parser state persists across network chunks (was re-initialized)
- Generation counter guards against double-stream corruption on rapid sends
- Reader explicitly cancelled on abort (was continuing after signal)
- Session hydration checks staleness before overwriting messages
- Text blocks preserve interleaving with tool calls (was merging all into one)
- Multi-line SSE data fields accumulated per spec (was overwriting)
- react-markdown + remark-gfm + rehype-sanitize replaces hand-rolled regex
  renderer (eliminates XSS via attribute injection and javascript: URLs)

P2 fixes:
- session.error/aborted update last message status (was stuck in streaming)
- initialMessage skips loadSession for new sessions (was overwriting)
- ToolCallCard auto-expands on transition to error/blocked state
- MutationObserver scoped to childList with RAF debounce (was scroll storm)
- Command palette searches by title not UUID
- StrictMode guard prevents double session creation
- Tool calls and thinking blocks cleared on session terminal events

Bundle: 210KB gzipped (budget: 500KB). Server tests: 1,485 pass, 0 fail.
@mcheemaa mcheemaa merged commit c0eae11 into main Apr 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant