* fix(web): force-dismiss indeterminate queued rows on cancel/edit When cancel returns busy for an already-indeterminate QueuedMessagesBar row, keep the optimistic removal instead of re-sticking the row so Edit/X cannot leave the UI dead. Upgrade busy to invoked when queued-state reconcile shows the steer already landed, so Edit toasts instead of prefilling a duplicate. Fixes #1839 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): never prefill Edit when queued cancel returns busy Indeterminate rows can still be a live dispatch. Prefilling on busy risked a duplicate send if the original steer later landed. Keep force-dismiss for Cancel; toast on Edit+busy without restoring the draft. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep force-dismissed indeterminate rows for late consume Hide abandoned outcome-unknown rows from the queued bar via queueDismissed so Cancel/Edit still clear the stuck chip, while leaving the message in the window for a later messages-consumed SSE. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): close queueDismissed races on cancel busy and requeue Restore the hidden hold before getQueuedState so concurrent consume SSE can land, skip overwriting it in onSuccess, and clear dismissal when markMessagesRequeued confirms FIFO again. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): undismiss force-held row when cancel busy finds FIFO If getQueuedState reports the localId is queued again after a failed steer, clear queueDismissed so Edit/Cancel return without prefilling. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): preserve queueDismissed across refresh and merges Force-dismiss of indeterminate rows survived Cancel, but latest-page refetch and same-ID merges dropped the client-only flag. Carry it while the authoritative row remains uninvoked and indeterminate. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): clear queueDismissed on reconnect FIFO reconcile Reconnect queued-state reconciliation now markMessagesRequeued for confirmed queuedLocalIds so force-dismissed rows regain Edit/Cancel after a missed messages-requeued SSE. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
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.
- Optional unread-session count on the Windows taskbar when installed as an Edge/Chrome PWA (toggleable in Display settings; off by default).
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.
- Session
@suggestions require conversation content, including untitled conversations. Names and directory labels affect display/search, not eligibility; empty stubs stay excluded.
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./browse- Workspace browser, enabled by the runner's configured workspace roots./share- Share-target landing (Web Share Target POST →?id=, or native/share#url=&text=&title=)./settings- Settings category hub (mobile) and responsive master-detail shell./settings/general- Language preferences./settings/display- Appearance, typography, colors, and session list preferences./settings/chat- Message input, tool card, and conversation color preferences./settings/voice- Everyday voice assistant preferences./settings/voice/voices- Full-page voice picker./settings/voice/advanced- Voice persona, tuning, and diagnostics./settings/machines- Machine management and runner status./settings/storage- SQLite storage sizes for the hub owner./settings/usage- Cache-aware token usage dashboard for the hub owner./settings/about- Application links and version information.
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 name and model display.
Chat interface (src/components/SessionChat.tsx)
- Message thread with infinite scroll.
- Composer for sending messages.
- Permission mode and model selection for supported agents.
- Session abort and handoff controls.
- Context size display.
- Per-session scratchlist (
src/components/AssistantChat/ScratchlistPanel.tsx)- Workbench panel for held notes/drafts; distinct from the queue.
- Add/delete/reorder entries; promote to composer (copy) or queue (send).
- Entries and attachments saved on the hub and synced across devices.
- Reordering affects only the current view and resets when entries refresh.
- Keyboard shortcut: Ctrl/Cmd+Shift+S to focus the add-input.
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
/terminal - Resize handling
Voice assistant
- ElevenLabs (@elevenlabs/react), Gemini Live, and Qwen Realtime backends
- Real-time voice control
- Standard and realtime composer dictation with provider capability selection
New session (src/components/NewSession/)
Modular session creation:
- Machine selector
- Directory input with recent paths
- Agent type selector
- Model selector
- Per-agent permission, effort, and collaboration controls when supported
First-User-Experience (FUE)
For a new, non-essential feature whose affordance would otherwise be hard to discover, consider the existing FUE primitive rather than a permanent UI block. Optional, not a requirement for every feature or a reason to expand a bug fix.
src/lib/use-fue.ts:useFue(featureId)returns{ status, engage, dismiss }; acknowledgement is isolated per feature inhapi.fue.v1.<featureId>localStorage keys.src/components/Fue.tsx:FueDotmarks the affordance;FueCalloutexplains it whilestatus === 'engaging'.- Dismissal requires an affirmative user action ("Got it"), never an auto-timeout.
- The FUE dot and feature-specific badges/counters are mutually exclusive; onboarding wins until acknowledged.
- Opt in per feature; skip the wrapper if an upstream component already supplies onboarding.
- Working example:
ScratchlistToggleButtoninsrc/components/AssistantChat/ComposerButtons.tsx. Use the source rather than maintaining a copied example here.
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.
Tests
Unit tests run under vitest + jsdom:
bun run test:web
End-to-end browser tests for the scratchlist component (real Chromium, real
inert focus blocking, real localStorage round-trips) live at the repo root
under e2e/:
bun run test:e2e # headless
bun run test:e2e:ui # Playwright UI mode (debug)
The spec drives a Vite-served fixture page (web/e2e-fixtures/scratchlist-fixture.html)
that mounts the production ScratchlistPanel in isolation, so no hub /
auth / socket setup is required.
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.