Files
hapi/web
KorenKritaandGitHub 2fbd98dfe4 fix(pi): offer model-accurate thinking levels in the create-session form (#1626)
* fix(web): filter Pi effort options by model thinkingLevelMap in new session form

The create-session EffortField called getPiThinkingLevelOptions without the
selected model's thinkingLevelMap, so the Pi effort select always showed the
static off..high list: levels the model marks unsupported stayed visible and
xhigh/max never appeared even for models that opt in. Pass the map through
piSelectedModel (mirrors HappyComposer), and extend the stale-effort reset
effect so a level unsupported by the newly selected model falls back to auto.

* feat(cli): probe machine Pi models over RPC to carry thinkingLevelMap

The machine-level Pi model probe parsed the `pi --list-models` text table,
which only exposes provider/model/thinking-yes-no — thinkingLevelMap (and
name/contextWindow) never reached the create-session form, so model-accurate
thinking levels could not render there (xhigh/max are map-opt-in and were
permanently hidden; see the companion web commit).

Replace the table probe with a short-lived `pi --mode rpc` child
(--no-session --no-extensions --no-skills --no-prompt-templates --no-tools)
that issues get_available_models and reuses the session path's parsePiModels
schema, so machine and session catalogs share one wire contract. The
lightweight probe also measures faster than the table probe (~0.8s vs
~1.6-2.4s) and drops the whitespace-table parsing entirely. Cache/inflight
dedupe/timeout structure is unchanged.

* fix: address review findings on the Pi model probe and effort reset

Three review findings on the RPC probe / effort-map change:

- (high) Probe teardown killed only the direct child PID: with shell:true on
  Windows that is the shell, orphaning the interactive pi RPC process on
  every successful probe and on timeout; finish() also resolved before the
  process was confirmed gone. Use killProcessByChildProcess (taskkill /T on
  Windows, Unix tree kill, SIGKILL escalation) and settle only after the
  tree teardown completes.
- (medium) An explicit get_available_models success:false response was
  discarded, so Pi's own error text was lost and the interactive child hung
  until the generic 15s timeout. parsePiModelsProbeLine now returns a
  three-way result (unrelated/models/error), also validating the response
  id, and the probe rejects immediately with Pi's error.
- (medium) Switching an xhigh/max-capable model back to Default left the
  now-hidden effort in state and submitted it while the select visually fell
  back to auto. The reset effect now also covers model === 'auto' (undefined
  map) while still not resetting mid-resolve for a concrete model.

Tests: probe-line failure/foreign-id cases; NewSession restore-to-Default
reset and map-opt-in retention (the latter guards the former against a
false green from the restore path).

* fix(web): type the Pi model test mock as PiModelSummary

The inline mock element type omitted thinkingLevelMap, so the new
capability-driven tests failed typecheck (TS2353) and the required test job
stopped before the unit tests ran. Use the shared PiModelSummary type so the
mock cannot drift from the wire contract again.

* fix(web): reconcile hidden Pi effort when model discovery fails

A restored explicit Pi model never resolves when the machine catalog request
fails, so piSelectedModel stays null for good. The reset effect required a
resolved model, so it skipped reconciliation, while EffortField rendered with
an undefined map and hid xhigh/max. Creation is only gated on the loading
state, not on the error, so handleCreate could still forward the stale hidden
level for Pi to reject or clamp.

Treat a failed catalog as a settled selection (alongside Default and a
resolved model) and reconcile against the undefined map; keep skipping the
reset while a concrete model is still resolving without an error, so a
restored xhigh/max survives until the map can prove it valid.

Test asserts the spawn payload carries no effort after a failed catalog with
a restored xhigh; verified it fails when the error branch is reverted.

* fix(cli): honor a failed probe process-tree teardown

killProcessByChildProcess reports survivors by resolving false, but the probe
discarded that result and settled anyway. The Windows graceful path is
taskkill /T without /F and escalates nothing on its own, so a probe child that
refuses the signal would be reported as cleaned up and the catalog cached,
letting interactive Pi processes accumulate across refreshes.

Escalate to the forced teardown when the graceful one reports survivors, and
reject (caching nothing, so the next call re-probes) when even that fails.
Skip the check when the child has no pid: nothing can leak, and replacing a
spawn ENOENT with a teardown error would only obscure the real failure.

Adds probe lifecycle tests (spawn + teardown helper mocked) for the escalation
path, the reject-and-do-not-cache path, the no-pid spawn-failure path, and the
timeout path; verified they fail when the escalation is reverted.

* fix(cli): keep extensions enabled for the Pi model probe

--no-extensions silently dropped providers contributed through
pi.registerProvider, which the old `pi --list-models` probe did list. Users
with such an extension would have lost those models in the create-session
form only.

Verified with a project-local .pi/extensions provider in the same cwd: the
probe with --no-extensions returned 29 models, while both the default run and
the old table probe returned 30 including the extension's model (and its
thinkingLevelMap). Dropping the flag restores parity; the extension provider
now comes back with its map intact.

Discovery that cannot contribute models stays disabled (--no-session,
--no-skills, --no-prompt-templates, --no-tools). Cost: the probe now measures
~1.4-2.0s instead of ~0.65s, still at or below the old table probe (~1.6-2.4s)
and fronted by the existing 60s cache.

* fix(cli): probe Pi models from the home directory, not the runner cwd

Under launchd/systemd the runner cwd is `/`, and starting Pi there is
pathological: project discovery plus extensions that scan from the working
directory walk the whole filesystem root. With extensions enabled (required so
pi.registerProvider models still surface) the RPC probe took 16.8s at cwd=/,
past the 15s timeout, so machine-level model discovery failed 100% of the time
and the create-session form showed only 'Pi model discovery timed out'.

Measured on macOS with 9 global extensions:
  cwd=/      extensions on   16.8s  -> timeout
  cwd=/      extensions off   0.6s  -> would lose extension providers
  cwd=$HOME  extensions on    1.4s  -> 29 models, complete

The catalog is machine-scoped and does not depend on cwd, so probing from the
home directory is both safe and representative. Verified from cwd=/ under the
runner's exact launchd environment: 1.3-1.7s, 29 models.

The old `pi --list-models` probe was immune because it never initialized a
session; this regression arrived with the RPC probe and was missed because
every earlier verification ran from a project directory.

Tests assert the spawn cwd is homedir() and that --no-extensions stays absent;
verified the cwd test fails when the option is removed.

* fix(cli): keep the probe in the runner cwd, fall back to home only at a root

Forcing every probe to the home directory fixed the launchd timeout but broke
project-local discovery: a runner started inside a project stopped seeing that
project's .pi/extensions providers, which the replaced --list-models probe did
surface. Verified with a project-local provider: present from the project dir
(30 models), absent from home (29).

Use the runner cwd normally and fall back to home only when cwd is a
filesystem root -- the launchd/systemd case where Pi startup walks the whole
tree (16.8s, past PROBE_TIMEOUT_MS) and where there is no project to lose
anyway. process.cwd() can also throw for a deleted directory, so that falls
back to home too.

Verified under the runner's launchd environment: from / -> home, 1.6s, 29
models; from a project dir -> that dir, 1.4s, 30 models including the
project-local provider.

Tests cover all three branches and were checked in both directions: pinning to
home fails the project-cwd test, pinning to cwd fails the root test.
2026-08-19 09:20:40 +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.
  • /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:

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.