Files
hapi/web/README.md
T
a0621194bb feat(web): native/deep-link ingest for /share (GET url, text, title) (#1413)
* feat(web): ingest GET /share?url=&text=&title= deep links

Native companions that cannot POST via Web Share Target can open the
same session picker by synthesizing the IndexedDB transfer client-side.
When id is present, the existing SW path still wins.

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

* fix(web): preserve share deep-link whitespace; scrub content beside id

Keep GET content strings verbatim when non-empty (match POST form-data).
When id is present with leftover url/text/title, replace to ?id= only so
payload does not linger in the address bar. Query-param contract stays —
fragments would break shipped native companions.

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

* fix(web): ingest /share deep links via URL fragment, not query

Shared url/text/title must not appear on the HTTP request line — hub
Hono logger (and any access log) records path+query. Native companions
open /share#url=&text=&title=; the client reads the fragment, scrubs it,
and continues with the existing ?id= picker path. Query validateSearch
keeps only id/error (Web Share Target redirect).

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

* fix(web): keep /share hash ingest across StrictMode remount

Capture the fragment in useState and reuse a single putShareTransfer
promise so the first effect's scrub + cleanup cancel does not lose the
deep-link under React.StrictMode.

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

* feat(web): fetch companion fileUrl into /share transfer files

Native shares cannot put binaries in the hash fragment. Companions host a
one-shot CORS URL and pass fileUrl/fileName/fileType; the share page fetches
bytes into the same IndexedDB files[] as Web Share Target POST.

* fix(web): cap share fileUrl fetch at the composer upload ceiling

Stream fileUrl downloads with Content-Length and body size checks matching
MAX_UPLOAD_BYTES so a crafted deep link cannot buffer unbounded bytes into
IndexedDB. Align native deep-link docs on the fileUrl hand-off vs POST.

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

* fix(web): cast streamed fileUrl chunks to BlobPart for tsc

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 12:36:58 +01:00

197 lines
6.0 KiB
Markdown

# 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.
- `/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/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 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
- 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
- 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:
```bash
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:
```bash
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/`:
```bash
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
```bash
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:
```bash
bun run build:web -- --base /<repo>/
```
2. Deploy `web/dist` to your static host.
3. Set hub CORS to allow the static origin (`HAPI_PUBLIC_URL` or `CORS_ORIGINS`).
4. 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.