Files
hapi/web
4c203f17cb feat(web,hub): scratchlist v2.2 hub attachment storage (#921) (#1205)
* feat(hub,shared): scratchlist v2.2 hub attachment storage foundation (#921)

Hub stores scratchlist attachment bytes on filesystem; SQLite holds
AttachmentMetadata[] JSON via session_scratchlist.attachments (v11→v12).
Upstream ladder: v10→v11 text-only scratchlist table (#896), v11→v12
attachments column. Configurable limits via HAPI_SCRATCHLIST_* env vars.
Upload, serve, and limits REST routes; delete entry cleans hub files.

Web promote/rehydrate still TODO. Soup renumber branch follows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): scratchlist v2.2 attachment UX (#921)

Route scratchlist-mode composer submits with attachments to hub storage,
show image thumbnails in the drawer, and rehydrate attachments on promote
to composer or queue (hub fetch → CLI upload for send).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): scratchlist attach submit, float thumbs, copy tooltip (#921)

Hub upload adapter now sets path on ready attachments so the composer send
button unlocks in scratchlist mode; routing label matches attachments too.
Entry thumbnails float left with text wrap; copy tooltip clarifies text-only.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): adapt scratchlist update tests to patch API (#921)

update() now takes { text?, attachments? }; v12 CRUD tests still passed a string.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub,web): harden scratchlist attachment ownership and orphan cleanup

Resolve claimed hub paths against the current session before persist,
count on-disk session bytes for upload caps, delete blobs dropped on
entry update, and DELETE pending uploads when composer remove runs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: drop accidental .cursor files from attachment PR

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): exit scratchlist mode before rehydrate; delete raced uploads

Promote-to-composer flushes mode exit so attachments use the chat adapter.
Cancel-during-upload deletes the hub blob once upload returns.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub,web): exact UUID delete match; stage hub paths on chat send

Reject partial attachment ids on disk delete, and restage scratchlist hub
attachments through uploadFile when sending after leaving scratchlist mode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): skip text-only PUT resolve; cleanup session attachment dirs

Text-only edits keep existing attachment metadata after session-id transfer.
Require full UUID on resolve. Delete scratchlist attachment files when a
session is deleted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub,web): scratchlist attach route, PUT bytes, orphan deletes

Park only hub-resident attachments; subtract removed blobs from the PUT
session cap; delete attachment files only when no other entry still
references them.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): canonicalize scratchlist attachment filenames

Resolve stores the on-disk sanitized name (not claimed.filename) and
hardens Content-Disposition against CR/LF/quote injection.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(hub): cover toxic filename canonicalize on resolve

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub,web): serialize scratchlist uploads; drop hub blobs after chat stage

Per-session upload lock keeps disk byte caps honest under concurrency.
After a successful toggle-off chat send, delete the staged hub copies so
they no longer count against the session attachment budget.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(shared,web): allow clearing scratchlist attachments; cleanup staged uploads

PUT may send attachments:[] without a text change. Staging to chat rolls
back partial normal-upload copies on failure.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): re-key scratchlist attachment files on session merge

Move hub blobs when scratchlist rows transfer between session ids so
quota and path ownership stay correct. Reject PUT that would leave an
empty textless entry after clearing attachments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): reuse restored scratchlist hub attachments without re-upload

Composer draft remount was re-uploading blobs that already had a
hapi-hub:scratchlist path, orphaning the originals against session quota.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 10:05:24 +08:00
..
2026-03-24 12:32:37 +08:00

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> (or CLI_API_TOKEN for 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 - 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/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 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/auto/bypassPermissions/plan).
  • Model selection (default/sonnet/sonnet[1m]/opus/opus[1m]).
  • Session abort and mode switch 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).
    • Persists across reloads via localStorage keyed per session.
    • 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
  • 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_URL to the public HTTPS URL of the dev server.
  • CORS_ORIGINS to 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:

  1. Build the web app. If your static host uses a subpath, set the Vite base:
bun run build:web -- --base /<repo>/
  1. Deploy web/dist to your static host.
  2. Set hub CORS to allow the static origin (HAPI_PUBLIC_URL or CORS_ORIGINS).
  3. 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.