Files
hapi/web
73fa846df3 fix(web): drop "No response requested." assistant messages (#402)
* fix(web): drop "No response requested." assistant messages

When Claude Code injects system messages (task notifications, system
reminders) as user turns, Claude responds with "No response requested."
In the HAPI web UI this appears as a reply to the user's message,
making it look like Claude is ignoring their input.

Filter these out in isSkippableAgentContent() (catches the fallback
path in normalize.ts) and in normalizeAssistantOutput() (catches the
primary path). Both checks verify the assistant message contains only
the text "No response requested." with no tool calls.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): filter text block instead of dropping entire message

Address bot review: dropping the whole normalized record breaks
sidechain UUID threading (parentUUID chain orphans).

Instead of returning null, suppress only the "No response requested."
text block during content extraction. The message record (uuid,
parentUUID, usage) is preserved so the tracer's sidechain grouping
continues to work.

Also remove the isSkippableAgentContent check since we no longer
need to drop the message at that layer.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): move "No response requested." filter to reducer layer

Address bot review: filtering in normalizeAssistantOutput() produced
empty content arrays, breaking traceMessages() which reads uuid and
parentUUID from content[0]. Sidechain child messages whose parentUUID
pointed to the filtered message became orphaned.

Fix: revert the normalizer to always emit the text block (preserving
the UUID chain for the tracer), and filter the sentinel text in
reducerTimeline.ts where text blocks become visible AgentTextBlocks.
At this point tracing is already complete.

Also adds reducer-level tests for the filter and updates the
normalize test to verify the text block is preserved.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): scope sentinel filter to single-block assistant messages only

Address bot review: the previous filter suppressed any text block
matching "No response requested.", which could hide legitimate replies.

Now the filter only triggers when the message has exactly one content
block (msg.content.length === 1) — i.e., the assistant response is
purely the sentinel text with no tool calls or reasoning blocks.
This prevents false positives while still catching the system-injection
auto-reply case.

Add test for the multi-block case (text + tool call) to verify the
sentinel text is preserved when other content exists.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): add parentUUID structural check to sentinel filter

Address bot review: raw text match alone could theoretically suppress
a legitimate reply.  Add c.parentUUID !== null as a structural guard:

- Sentinel auto-replies always follow a prior assistant turn, so their
  parentUUID is set (pointing to the previous message in the chain).
- A first message in a conversation has parentUUID: null and will
  never be filtered.

Combined conditions: msg.content.length === 1 (sole block, no tool
calls) AND c.parentUUID !== null (not the first reply) AND exact text
match.

Add tests for the parentUUID=null escape hatch.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): use injected-turn UUID tracking for sentinel filter

Address review: use structural markers instead of broad text matching.

1. Pre-scan collects UUIDs from sidechain content blocks (system-
   injected user turns). The sentinel filter now only triggers when
   parentUUID points to one of these known injected turns.

2. Move task-notification event extraction from normalizer to reducer.
   Previously, task-notifications with summary were normalized as
   role:'event', losing their uuid. Now they stay as sidechain (uuid
   preserved for pre-scan), and the reducer extracts the summary as
   an agent-event block.

3. Remove redundant 'uuid' in c guard (always present on sidechain type).

False positive analysis: a legitimate reply is only suppressed when ALL
of: (a) sole content block, (b) parentUUID matches a sidechain-injected
turn, (c) exact sentinel text. This combination cannot occur for real
user-facing content.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): add parentUUID to sidechain content type for tracer linkage

The sidechain content block was missing parentUUID, so traceMessages()
could not chain system-injected user turns (task notifications, system
reminders) inside a Task sidechain back to their parent. This caused
later sidechain messages pointing to the injected turn's UUID to become
orphaned and disappear from the Task card.

Add parentUUID to the sidechain type definition and propagate it from
normalizeUserOutput() in both the isSidechain and non-sidechain paths.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): handle array-content sidechain user messages to prevent prompt leak

Sidechain user messages can arrive with either string content or array
content ([{type:'text', text:'...'}]) depending on how Claude Code
serialises them. The previous fix only handled the string case, causing
intermittent prompt leaks when array format was used.

Now normalizeUserOutput extracts text from array-content sidechain
messages and emits them as sidechain blocks, so the tracer can match
them to their parent Task tool call.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* test(web): verify parentUUID propagation from assistant output data

Add integration tests confirming normalizeAssistantOutput correctly
maps data.parentUuid to text block parentUUID (used by the reducer's
sentinel detection). Tests cover both present and absent parentUuid.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: HAPI <noreply@hapi.run>
2026-04-06 20:42:40 +08:00
..
2026-03-24 12:32:37 +08:00
2025-12-19 15:26:33 +08:00
2026-01-29 16:25:47 +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 - Application settings.

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/bypassPermissions/plan).
  • Model selection (default/sonnet/sonnet[1m]/opus/opus[1m]).
  • Session abort and mode switch controls.
  • Context size display.

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.

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.