Skip to content

feat: rebuild web consumer as React 19 + Zustand + Tailwind v4 + Vite app#4

Merged
teng-lin merged 4 commits intomainfrom
feature/frontend-redesign
Feb 16, 2026
Merged

feat: rebuild web consumer as React 19 + Zustand + Tailwind v4 + Vite app#4
teng-lin merged 4 commits intomainfrom
feature/frontend-redesign

Conversation

@teng-lin
Copy link
Copy Markdown
Owner

Summary

Rebuilds the web consumer from a single vanilla HTML/CSS/JS file (~689 lines) into a modern React 19 + Zustand + Tailwind v4 + Vite app with the "Amber Beam" design aesthetic.

  • 41 files changed, 4,710 insertions, 83 deletions
  • Builds to a single HTML file (299 KB, ~94 KB gzip) via vite-plugin-singlefile
  • Adapter-agnostic design — surfaces BeamCode's multi-backend advantage (Claude Code, Codex, Gemini CLI, etc.)

Architecture

  • Zustand 5 store with Record-based session data (not Maps) and patchSession() helper
  • WebSocket connect-on-switch pattern — one WS per active session to avoid browser connection limits
  • Flattened consumer types (shared/consumer-types.ts) — zero imports from backend internals
  • HTTP routing extraction from monolithic beamcode.ts into src/http/ modules (server.ts, api-sessions.ts, consumer-html.ts, health.ts)

UI Components

  • 3-panel responsive layout — collapsible sidebar, main chat, task panel
  • 3-level message grouping — content-block (AssistantMessage), message (MessageFeed), subagent nesting
  • Streaming indicator with blinking cursor, elapsed time, and token count
  • Slash command menu with categorized typeahead (forwardRef + useImperativeHandle pattern)
  • Permission banner with tool-specific previews (Bash commands, Edit diffs, file paths)
  • Context gauge with color-coded usage bar (green → yellow → red)
  • Adapter badges per session in sidebar (color-coded per backend)

Security & Performance

  • Body size limit (1 MB) on API requests with double-rejection prevention
  • CWD path traversal validation on session creation
  • Gzip pre-compression for HTML serving with CSP/X-Frame-Options/X-Content-Type-Options headers
  • Message history capped at 2,000 per session
  • Race condition fix: session launch after WS server setup
  • Streaming state cleanup on disconnect
  • React.memo on heavy components, stable keys, optimized selectors

Build Pipeline

pnpm build:lib   → tsdown (library)
pnpm build:web   → vite build (single HTML)
pnpm build       → both + copy to dist/consumer/
pnpm dev:web     → vite dev server with proxy to :3456

Test plan

  • cd web && npx vite build produces single index.html < 300 KB
  • pnpm build (root) builds library + consumer successfully
  • Dev workflow: dev:web proxies to running beamcode server
  • Connect to session, send message, see streamed response
  • Switch sessions in sidebar (WS disconnect/reconnect)
  • Slash menu with categorized commands on / input
  • Permission approve/deny works
  • Task panel shows context gauge + cost
  • Mobile responsive at 375px width
  • Keyboard-only navigation through all interactive elements

Generated with Claude Code

… app

Replace the 689-line vanilla HTML/CSS/JS consumer with a modern React app
that builds to a single HTML file (299 KB, 94 KB gzip).

Frontend (web/):
- React 19 with Zustand 5 state management (Record-based, not Map)
- Tailwind CSS v4 with Amber Beam design system (@theme tokens)
- Vite 6 + vite-plugin-singlefile for single-file output
- marked + DOMPurify for markdown rendering (~100KB lighter than react-markdown)
- 19 components: 3-panel layout, message feed with 3-level grouping,
  streaming indicator, slash command typeahead, permission cards with
  tool-specific previews, session sidebar with adapter badges, context gauge
- WebSocket connect-on-switch pattern (avoids browser 6-connection limit)
- Accessibility: ARIA live regions, keyboard navigation, prefers-reduced-motion

Backend changes:
- Extract HTTP routing from beamcode.ts into src/http/ modules
- Add REST API: GET/POST/DELETE /api/sessions with body size limit and
  path traversal validation
- Pre-compressed gzip serving with security headers (CSP, X-Frame-Options)
- Fix startup race: launch session after WS server is ready
- Add SessionManager.setServer() for deferred wiring

Shared types:
- shared/consumer-types.ts with zero imports from backend internals

Build pipeline:
- build:lib (tsdown) + build:web (vite) + copy-consumer
- dev:web with proxy to beamcode server for HMR development
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @teng-lin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant overhaul of the web consumer, transforming it into a robust and modern React application. The primary goal is to enhance the user experience, improve maintainability, and provide a more scalable foundation for future development. This change impacts the entire frontend architecture, build process, and user interface, while also bolstering security and performance aspects of the application.

Highlights

  • Frontend Rebuild: The web consumer has been completely rebuilt from a vanilla HTML/CSS/JS file into a modern React 19 application, leveraging Zustand for state management, Tailwind CSS v4 for styling, and Vite for the build process.
  • Architectural Enhancements: Introduced a Zustand 5 store for session data, implemented a WebSocket connect-on-switch pattern for active sessions, flattened consumer types to decouple from backend internals, and extracted HTTP routing into modular components.
  • New UI Components: Added a responsive 3-panel layout (collapsible sidebar, main chat, task panel), 3-level message grouping, a streaming indicator with real-time stats, a categorized slash command menu, a permission banner with tool-specific previews, and a context gauge.
  • Build Pipeline Updates: The build process now includes separate scripts for library (build:lib) and web consumer (build:web), with the main build command orchestrating both. A new dev:web script enables a Vite development server with proxying.
  • Security and Performance: Implemented body size limits on API requests, CWD path traversal validation, Gzip pre-compression for HTML, CSP/X-Frame-Options/X-Content-Type-Options headers, message history capping, and race condition fixes.
Changelog
  • package.json
    • Updated build scripts to introduce build:lib, build:web, and dev:web commands, streamlining the build and development workflows for the new web consumer.
  • scripts/copy-consumer.js
    • Modified to prioritize copying the Vite-built index.html from web/dist/ and added a fallback mechanism for legacy paths, ensuring compatibility with the new build system.
  • shared/consumer-types.ts
    • Added a new file defining a comprehensive set of flattened consumer-facing TypeScript types, crucial for the new web frontend's data structures and communication with the backend.
  • src/bin/beamcode.ts
    • Refactored HTTP server initialization by removing inline request handling and delegating to new, modular HTTP service files.
    • Adjusted the SessionManager setup to allow deferred wiring of the WebSocket server, improving server startup flow.
  • src/core/session-manager.ts
    • Added a setServer method to enable the WebSocket server to be set after the HTTP server has been created, addressing a dependency order issue.
  • src/http/api-sessions.ts
    • Added a new module to handle API endpoints related to sessions, including operations for listing, creating, retrieving, and deleting sessions, with built-in request body validation and path traversal prevention.
  • src/http/consumer-html.ts
    • Added a new module responsible for loading and serving the consumer HTML, incorporating caching, gzip compression, and essential security headers.
  • src/http/health.ts
    • Added a new module providing a simple HTTP health check endpoint for monitoring application status.
  • src/http/server.ts
    • Added a new module that centralizes the creation and routing logic for the BeamCode HTTP server, integrating various handlers for API, consumer HTML, and health checks.
  • web/index.html
    • Added the main HTML entry point for the new React-based web application, including essential meta tags, font imports, and the root element for React rendering.
  • web/package-lock.json
    • Added the package-lock.json file to manage and lock the dependencies for the new web frontend.
  • web/package.json
    • Added the package.json file, defining the dependencies (React 19, Zustand, Marked, DOMPurify) and development dependencies (Vite, Tailwind CSS v4, TypeScript, Vite plugins) for the new web frontend.
  • web/src/App.tsx
    • Added the root React component, which establishes the main application layout, including the sidebar, chat view, and task panel, and incorporates an error boundary for robust UI handling.
  • web/src/api.ts
    • Added utility functions for making API calls to the backend, specifically for managing sessions (list, create, delete, get).
  • web/src/components/AssistantMessage.tsx
    • Added a React component responsible for rendering assistant messages, intelligently grouping and displaying various content blocks such as text, thinking processes, tool uses, and tool results.
  • web/src/components/ChatView.tsx
    • Added the primary chat interface component, integrating the message feed, message composer, connection banner, permission banner, and streaming indicator.
  • web/src/components/Composer.tsx
    • Added the message input component, featuring auto-resizing, handling of user messages and slash commands, and interrupt functionality for ongoing processes.
  • web/src/components/ConnectionBanner.tsx
    • Added a component to display a banner indicating the CLI's connection status, particularly when disconnected.
  • web/src/components/ContextGauge.tsx
    • Added a component that visually represents the context window usage with a color-coded progress bar and warning indicators.
  • web/src/components/EmptyState.tsx
    • Added a component to display a friendly empty state message when no chat messages are present or a session is not active.
  • web/src/components/MarkdownContent.tsx
    • Added a component for rendering markdown content, ensuring proper HTML sanitization using DOMPurify.
  • web/src/components/MessageBubble.tsx
    • Added a component to render individual chat messages, distinguishing between user messages, assistant responses, errors, and slash command results.
  • web/src/components/MessageFeed.tsx
    • Added the message display area component, which groups messages (including subagent conversations) and provides auto-scrolling functionality.
  • web/src/components/PermissionBanner.tsx
    • Added a banner component to display and manage pending tool permission requests, offering 'Allow' and 'Deny' actions with tool-specific previews.
  • web/src/components/ResultBanner.tsx
    • Added a banner component to summarize the outcome of a session, displaying metrics like cost, duration, and token usage.
  • web/src/components/Sidebar.tsx
    • Added the sidebar component for navigating between sessions, displaying session names, adapter types, and status indicators.
  • web/src/components/SlashMenu.tsx
    • Added a component for an interactive slash command menu, featuring typeahead search, categorization, and keyboard navigation.
  • web/src/components/StreamingIndicator.tsx
    • Added a component to show a 'Generating...' indicator during streaming responses, including elapsed time and token count.
  • web/src/components/TaskPanel.tsx
    • Added the task panel component, which displays detailed session information such as context window usage, total cost, number of turns, and model usage breakdown.
  • web/src/components/ThinkingBlock.tsx
    • Added a component to display 'Thinking...' messages from the assistant, with an expandable section to reveal the detailed thought process.
  • web/src/components/ToolBlock.tsx
    • Added a component to render individual tool use blocks, showing the tool name, a preview of its input, and an expandable section for full input details.
  • web/src/components/ToolGroupBlock.tsx
    • Added a component to group multiple tool use blocks under a single expandable entry, improving readability for complex tool sequences.
  • web/src/components/TopBar.tsx
    • Added the top bar component, displaying the connection status, current model, and a count of pending permission requests, along with toggle buttons for the sidebar and task panel.
  • web/src/index.css
    • Added the main CSS file, incorporating Tailwind CSS and defining custom theme variables for colors, typography, and animations, as well as base styles and markdown prose styling.
  • web/src/main.tsx
    • Added the entry point for the React application, responsible for rendering the root App component into the DOM.
  • web/src/store.ts
    • Added a Zustand store for managing the global application state, including session data, UI state, and a comprehensive set of actions for state manipulation.
  • web/src/utils/format.ts
    • Added utility functions for formatting numerical values such as tokens, currency, and time durations, and for extracting the basename from a CWD path.
  • web/src/utils/sanitize.ts
    • Added a utility function that uses DOMPurify to safely sanitize HTML content, preventing cross-site scripting (XSS) vulnerabilities.
  • web/src/ws.ts
    • Added the WebSocket client module, handling connection, message parsing, state updates via Zustand, reconnection logic, and message sending to the backend.
  • web/tsconfig.json
    • Added the TypeScript configuration file for the new web frontend, specifying compiler options and inclusion paths for source files.
  • web/vite.config.ts
    • Added the Vite configuration file for the web frontend, including React plugin integration, single-file build settings, and proxy configurations for WebSocket and API routes.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

The web/ directory has its own package.json with React, Vite, and
Tailwind dependencies. CI was only running `pnpm install` at the root
level, causing `build:web` to fail with missing @vitejs/plugin-react.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant rewrite of the web consumer using a modern React stack, bringing many thoughtful features and architectural improvements. However, a critical security vulnerability has been identified: the application lacks authentication, and its 'localhost' restriction can be bypassed, potentially leading to Remote Code Execution (RCE) due to a flawed path traversal check in session creation. Additionally, the Content-Security-Policy uses 'unsafe-inline' which violates a security rule for this repository, and improvements are needed for type safety in streaming events. Addressing these security and typing issues is crucial for a robust and secure deployment.

Comment thread src/http/server.ts Outdated
Comment on lines +28 to +37
const tunnelActive = typeof isTunnelActive === "function" ? isTunnelActive() : isTunnelActive;
if (tunnelActive && url.pathname.startsWith("/api/")) {
const remote = req.socket.remoteAddress;
const isLocal = remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
if (!isLocal) {
res.writeHead(403, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "API access restricted to localhost" }));
return;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-critical critical

The application attempts to restrict access to the /api/* endpoints to localhost when a tunnel is active. However, this check is fundamentally flawed because tunneling tools like cloudflared forward requests from the local machine, causing them to appear as if they originate from 127.0.0.1. Consequently, the remoteAddress check passes for any request coming through the tunnel.

Furthermore, when the tunnel is not active, no restriction is applied at all, and the server listens on all interfaces by default, exposing the unauthenticated API to the local network.

Since the API allows listing all sessions (including their secret IDs) and creating new sessions with arbitrary working directories, an attacker can gain full control over the host machine via Remote Code Execution (RCE).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in commit 39a29ac: Replaced the flawed IP-based localhost check with API key authentication. A random API key is generated at startup and required via Authorization: Bearer <key> for all /api/* endpoints. This works correctly regardless of whether requests come through cloudflared or directly.

Comment thread src/http/api-sessions.ts
Comment on lines +70 to +77
const cwd = opts.cwd as string | undefined;
if (cwd) {
const resolved = resolvePath(cwd);
if (resolved.includes("..") || !resolved.startsWith("/")) {
json(res, 400, { error: "Invalid cwd" });
return;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-high high

The POST /api/sessions endpoint is vulnerable to path traversal. The current validation for cwd attempts to prevent this by checking for .. after path.resolve(cwd). This is ineffective because path.resolve() normalizes .. segments, allowing an attacker to bypass the check and start a session in any directory on the host filesystem, potentially leading to Remote Code Execution (RCE). Additionally, the !resolved.startsWith("/") check is not cross-platform. A more robust solution would be to verify that the resolved path is strictly within a designated base directory (e.g., process.cwd()).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in commit 39a29ac: Replaced the ineffective .includes('..') check (which never triggers after path.resolve() normalization) with filesystem validation — the resolved path must exist and be a directory (existsSync + statSync.isDirectory).

Comment thread src/http/consumer-html.ts Outdated
Comment on lines +27 to +28
"Content-Security-Policy":
"default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self' ws: wss:;",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The Content-Security-Policy (CSP) header includes 'unsafe-inline' for both script-src and style-src. This significantly increases the risk of Cross-Site Scripting (XSS) attacks, even with DOMPurify in place, if a sanitization bypass is found. This also violates a security rule for this repository. Given the single-file nature of the build, it is strongly recommended to implement a nonce-based CSP or use hashes for legitimate inline blocks instead of allowing all inline scripts. Vite plugins can often help automate this.

References
  1. For security hardening, avoid using 'unsafe-inline' in Content-Security-Policy. Prefer using nonce-based CSP or moving inline scripts and styles to external files.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in commit 39a29ac: Replaced 'unsafe-inline' with SHA-256 hash-based CSP. At startup, loadConsumerHtml() computes SHA-256 hashes of all inline <script> and <style> blocks from the built HTML and constructs the CSP header dynamically. This eliminates the XSS risk from blanket inline allowance.

Comment thread shared/consumer-types.ts Outdated
| { type: "session_init"; session: ConsumerSessionState & Record<string, unknown> }
| { type: "session_update"; session: Partial<ConsumerSessionState> & Record<string, unknown> }
| { type: "assistant"; message: AssistantContent; parent_tool_use_id: string | null }
| { type: "stream_event"; event: unknown; parent_tool_use_id: string | null }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The event property in the stream_event type is defined as unknown. While this is better than any, you can provide more specific types here to improve type safety. Based on its usage in web/src/ws.ts, it appears to be a discriminated union. Defining this union type would make the message handling code more robust and self-documenting. For example:

type StreamEvent = 
  | { type: 'message_start', ... }
  | { type: 'content_block_delta', delta: { type: 'text_delta', text: string } }
  | { type: 'message_delta', usage: { output_tokens: number } };

Then you could use event: StreamEvent in the ConsumerMessage type.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in commit 39a29ac: Added a StreamEvent discriminated union type to shared/consumer-types.ts covering message_start, content_block_delta, content_block_start, content_block_stop, message_delta, and message_stop event types. Updated ws.ts to use the typed event directly instead of casting to Record<string, unknown>.

- Replace broken IP-based localhost check with API key authentication
  for /api/* endpoints (cloudflared forwards as 127.0.0.1, making IP
  checks unreliable)
- Fix CWD path traversal: validate that resolved path is an existing
  directory instead of checking for ".." after path.resolve() normalization
- Replace 'unsafe-inline' CSP with SHA-256 hash-based policy computed
  from the actual inline script/style content at startup
- Add typed StreamEvent discriminated union replacing `event: unknown`
  in stream_event messages for better type safety
@teng-lin teng-lin merged commit e9b2f35 into main Feb 16, 2026
4 checks passed
@teng-lin teng-lin deleted the feature/frontend-redesign branch February 16, 2026 05:07
teng-lin added a commit that referenced this pull request Feb 24, 2026
Fix architecture principle violations #1, #3, #4, #7:

- Fix #1: Remove dual lifecycle mutation by deleting applyLifecycleFromBackendMessage()
  which was redundant with reduceLifecycle()

- Fix #3: Make transitionLifecycle() private. Add SESSION_CLOSING system signal and
  route coordinator's direct calls through process()

- Fix #7: Route ConsumerGateway's backend:relaunch_needed emission through the
  runtime as BACKEND_RELAUNCH_NEEDED system signal instead of emitting directly

- Fix #4: Route BackendConnector's inline broadcasts and event emissions through
  the runtime via routeSystemSignal callback:
  - Part A: Connect/disconnect lifecycle (cli_connected, backend:connected, etc.)
  - Part B: Slash passthrough results/errors (slash_command_result, slash_command_error)

This enforces the core design principle that all state mutations flow through
SessionRuntime.process(), which dispatches to the pure reducer and executes effects.
All 2902 tests pass, typecheck clean.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
teng-lin added a commit that referenced this pull request Feb 25, 2026
…ONSUMER effect (Tier 1 #4 + P3)

Add SEND_TO_CONSUMER effect type so targeted per-consumer errors go
through the reducer-effect pipeline instead of being sent inline.

Tier 1 #4: MessageQueueHandler.sendTo() calls replaced with QUEUE_ERROR
signal dispatches; reducer produces SEND_TO_CONSUMER effects.

P3 fix: handleInboundCommand() post-reducer user_message and set_adapter
rejections now append SEND_TO_CONSUMER effects to the effect list instead
of calling broadcaster.sendTo() directly.
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