* feat(cli): list local OpenCode sessions over machine RPC * feat(hub): import OpenCode sessions into HAPI * feat(web): import OpenCode history from new-session picker * fix(cli): derive OpenCode imported message ids from part ids Random ids made every scan differ, so delta classification flagged unchanged transcripts as diverged on re-import. * fix(cli): surface unreadable OpenCode db as rpc error A locked or unopenable opencode.db now fails the listing instead of reporting an empty success; summaries normalize timestamps and the web import toast counts only created or actually-appended sessions. * fix(hub): fail closed on opencode re-import past live HAPI continuation Imported entries must remain a strict prefix of the native transcript. Insertions ahead of the imported boundary or new native messages after the session continued in HAPI now fail closed instead of appending duplicated or reordered history. * fix(web,hub): apply selected launch config when importing OpenCode history Pass the model, reasoning effort, and permission mode chosen in the new-session picker through the import request and persist them on the imported HAPI session via applySessionConfig, so reopen launches with the visible selection instead of stale or default values. * fix(hub,cli): preserve explicit launch resets and skip streaming parts on import - Launch config keys are applied by presence, so explicit null/default selections from the picker must reach applySessionConfig to clear stale model, effort, or permission values on re-import. Requests that omit the fields entirely still leave persisted config untouched. - Assistant text parts are mutable while a response streams. Mirror the live scanner gate and import only finished (time.end set), synthetic, or user text so an in-progress row can never be stored and later finalized as transcript_diverged. * test(hub): fix launch-config reset assertions to satisfy strict types * fix(cli): fail opencode transcript reads on query errors instead of partial snapshots Swallowed SQLite errors could return a truncated message list that the hub later rejects as transcript_diverged with no repair path. Full transcript reads now propagate query failures; summary lastUserMessage extraction stays resilient per message. * test(cli): cover unreadable-part-table transcript read stays non-partial * fix(hub): scope launch-config failures to the per-session import result A thrown applySessionConfig call used to escape the bulk handler as an unstructured 500 even though transcripts were already persisted. Catch it per session and report it as a config_failed result so remaining sessions in a bulk request still import. Also cover the mid-transcript part-query failure with an injected opener that rejects instead of returning a holey transcript. * fix(cli): read opencode transcripts inside a deferred snapshot transaction Messages and their parts were read in separate statements, so a concurrent OpenCode write could leave a hole in the imported sequence that positional re-import checks then reject forever. Wrap each full transcript read in BEGIN DEFERRED/COMMIT with rollback on failure. * fix(cli): follow machine path policy for OpenCode imports * fix(web): use unified permission mode for OpenCode imports * fix(opencode): harden session import consistency * fix(hub): preserve metadata on rejected OpenCode imports * fix(hub): refresh OpenCode import state under lock * fix(hub): resolve OpenCode imports after locking * fix(opencode): bind imported resume to source machine
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.
- Codex
request_user_inputchoices honorisOther: None of the above focuses optional notes, supports empty notes, and preserves the canonical wire value across languages. Recorded other answers and notes remain visible in live and historical cards; Pi/MCP forms without the flag are unchanged. - 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. See the session list behavior guide for search and picker rules.
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.
- Metadata search with relevance ranking, wildcard matching, and date/unread/machine filters.
- Machine/project grouping, pinned and in-progress sections, and configurable collapsed previews.
Chat interface (src/components/SessionChat.tsx)
- Message thread with infinite scroll.
- Composer for sending messages.
- Permission mode and model selection for supported agents.
Cursor Auto uses CLI Auto for new/resumed sessions configured with Auto. Switching a live session to Auto when ACP does not advertise a literal Auto option relaunches the Cursor ACP process in place under
--model autowhile preserving the hub session id. - Session abort and handoff controls.
- Codex Continue planning hides the current proposal's action menu locally and focuses the composer without sending a message or replacing its draft. The plan document remains readable; recycled cards stay dismissed and new proposals get fresh action menus.
- 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.