* feat(web): persist composer draft across session switches Switching between sessions now preserves the text typed in the composer. Drafts are stored per-session in sessionStorage and restored when the user navigates back. - Add composer-drafts utility (sessionStorage, in-memory cache) - Restore draft on HappyComposer mount, save on unmount - Clear draft on message send - Evict oldest drafts when exceeding 50 entries - Add unit tests for composer-drafts Fixes #231 * fix(web): add key prop to HappyComposer for explicit remount on session switch * fix(web): move clearDraft to SessionChat after send validation Prevents draft loss when Codex rejects an unsupported slash command. * fix(web): remove explicit clearDraft, rely on unmount save Successful sends clear the composer text, so the unmount save naturally persists an empty string which deletes the draft entry. This avoids clearing the draft when the send is blocked or fails. * fix(web): clear draft on successful send via onSuccess callback Move draft clearing to the send-success path so drafts are only removed after the message is actually accepted by the server. * fix(web): pass session ID to onSuccess to clear correct draft The previous version used the current route's sessionId, which could clear the wrong draft if the user switched sessions before the send completed. * test(web): add useSendMessage onSuccess callback tests Verify that onSuccess receives the correct session ID (including resolved IDs), and is not called on send failure or block. * refactor(web): extract useComposerDraft hook with unit tests Extract the draft save/restore logic from HappyComposer into a dedicated useComposerDraft hook. Adds 6 unit tests covering: - mount: restores saved draft via rAF - mount: skips restore if composer already has text - mount: skips restore if no saved draft - unmount: saves current text after rAF has fired - unmount: skips save before rAF (draftReady guard) - no-op when sessionId is undefined * fix(web): clear both route and resolved session drafts after send When resolveSessionId swaps the session (e.g. inactive → resumed), the sent ID differs from the route's session ID. Extract clearDraftsAfterSend so both are cleared and unit-testable. * fix(web): refresh eviction order when updating an existing draft Delete the key before re-inserting so Object.keys() reflects the most recent write, preventing a recently edited draft from being evicted first.
hapi-web
React Mini App / PWA for monitoring and controlling hapi sessions.
What it does
- Session list with status, pending approvals, todos, and summaries.
- Chat view with streaming updates and message sending.
- Permission approval and denial workflows.
- Permission mode and model selection.
- Machine list and remote session spawn.
- File browser and git status/diff views.
- PWA install prompt and offline banner.
Runtime behavior
- When opened inside Telegram, auth uses Telegram WebApp init data.
- When opened in a normal browser, you can log in with
CLI_API_TOKEN:<namespace>(orCLI_API_TOKENfor the default namespace). - The login screen includes a top-right hub picker; if unset, the app uses the same origin it was loaded from.
- Live updates come from the hub via SSE.
Routes
See src/router.tsx for route definitions.
/- Redirect to /sessions./sessions- Session list./sessions/$sessionId- Chat interface./sessions/new- Create new session./sessions/$sessionId/files- File browser with git status./sessions/$sessionId/file- File viewer with diff support./sessions/$sessionId/terminal- Terminal interface./settings- Application settings.
Features
Session list (src/components/SessionList.tsx)
- Active/inactive status indicator.
- Session title from name, summary, or path.
- Todo progress display.
- Pending permission request count.
- Agent flavor label (claude/codex/gemini).
- Model mode display.
Chat interface (src/components/SessionChat.tsx)
- Message thread with infinite scroll.
- Composer for sending messages.
- Permission mode toggle (default/acceptEdits/bypassPermissions/plan).
- Model selection (default/sonnet/sonnet[1m]/opus/opus[1m]).
- Session abort and mode switch controls.
- Context size display.
File browser (src/routes/sessions/files.tsx)
- Git status view (staged/unstaged files).
- File search with ripgrep.
- Navigate to file viewer.
File viewer (src/routes/sessions/file.tsx)
- File content display with syntax highlighting.
- Staged/unstaged diff view.
Terminal (src/routes/sessions/terminal.tsx)
- Remote terminal via xterm.js
- Real-time via Socket.IO
- Resize handling
Voice assistant
- ElevenLabs integration (@elevenlabs/react)
- Real-time voice control
New session (src/components/NewSession/)
Modular session creation:
- Machine selector
- Directory input with recent paths
- Agent type selector
- Model selector
- Permission mode toggle (YOLO mode)
Authentication
See src/hooks/useAuth.ts and src/hooks/useAuthSource.ts.
- Telegram Mini App: Uses initData from WebApp SDK.
- Browser: Uses CLI_API_TOKEN from login prompt.
- JWT tokens with auto-refresh.
Data fetching
See src/hooks/queries/ for query hooks and src/hooks/mutations/ for mutations.
- Sessions, messages, machines via TanStack Query.
- Git status and file operations.
- Optimistic updates for message sending.
Real-time updates
See src/hooks/useSSE.ts.
- SSE connection to
/api/events. - Session/message/machine update events.
- Automatic cache invalidation on events.
Stack
React 19 + Vite + TanStack Router/Query + Tailwind + @assistant-ui/react + xterm.js + @elevenlabs/react + socket.io-client + workbox + shiki.
Source structure
src/router.tsx- Route definitions.src/components/- UI components.src/hooks/- Data fetching and state hooks.src/api/client.ts- API client.src/types/api.ts- Type definitions.
Development
From the repo root:
bun install
bun run dev:web
If testing in Telegram, set:
HAPI_PUBLIC_URLto the public HTTPS URL of the dev server.CORS_ORIGINSto include the dev server origin.
Build
bun run build:web
The built assets land in web/dist and are served by hapi-hub. The single executable can embed these assets.
Standalone hosting
You can host web/dist on a static host (GitHub Pages, Cloudflare Pages) and point it at any hapi hub:
- Build the web app. If your static host uses a subpath, set the Vite base:
bun run build:web -- --base /<repo>/
- Deploy
web/distto your static host. - Set hub CORS to allow the static origin (
HAPI_PUBLIC_URLorCORS_ORIGINS). - Open the static site, click the top-right Hub button on the login screen, and enter the hapi hub origin.
Clear the hub override in the same dialog to return to same-origin behavior.