Files
web-terminal/src/http/hook.ts
Yaojia Wang a411c89ee9 feat(v0.3): H2 server — Claude Code hooks → live status side-channel
- types: ClaudeStatus + ServerMessage 'status'; Session.claudeStatus;
  SessionManager.handleHookEvent
- session: inject WEBTERM_SESSION + WEBTERM_HOOK_URL into the spawned shell env
  so hooks know which tab they belong to
- http/hook.ts: pure event→status mapper (PreToolUse→working, PermissionRequest/
  Notification:permission_prompt→waiting, Stop/idle_prompt→idle); 6 unit tests
- server: POST /hook (loopback-only, express.json 64kb) → manager pushes 'status'
- fix: closeIdleConnections() on shutdown so an idle hook keep-alive socket
  doesn't hang close()/SIGTERM
- integration ⑦ (real PTY): attach → POST /hook → ws receives status. 205 tests green.
2026-06-17 18:44:07 +02:00

58 lines
1.9 KiB
TypeScript

/**
* src/http/hook.ts (H2) — map a Claude Code hook POST to a session status update.
*
* Pure + never throws. The web-terminal sessionId arrives in the
* `X-Webterm-Session` header (injected as $WEBTERM_SESSION on spawn); the hook
* payload arrives as JSON in the body. We translate the event into a coarse
* ClaudeStatus the UI can show. The server is still a byte-shuttle for the
* terminal stream — this is an out-of-band side-channel.
*/
import type { ClaudeStatus } from '../types.js';
export interface HookEvent {
sessionId: string;
status: ClaudeStatus;
detail?: string;
}
/**
* @param sessionId value of the `X-Webterm-Session` header
* @param body parsed JSON body of the hook POST (untrusted)
* @returns a status update, or null if the payload is unusable
*/
export function parseHookEvent(sessionId: string | undefined, body: unknown): HookEvent | null {
if (typeof sessionId !== 'string' || sessionId.length === 0) return null;
if (body === null || typeof body !== 'object') return null;
const b = body as Record<string, unknown>;
const event = typeof b['hook_event_name'] === 'string' ? b['hook_event_name'] : '';
const notif = typeof b['notification_type'] === 'string' ? b['notification_type'] : '';
const tool = typeof b['tool_name'] === 'string' ? b['tool_name'] : undefined;
let status: ClaudeStatus;
switch (event) {
case 'PreToolUse':
case 'PostToolUse':
case 'UserPromptSubmit':
status = 'working';
break;
case 'PermissionRequest':
status = 'waiting';
break;
case 'Stop':
case 'SessionEnd':
status = 'idle';
break;
case 'Notification':
status = notif === 'permission_prompt' ? 'waiting' : 'idle';
break;
default:
return null;
}
return status === 'waiting' && tool !== undefined
? { sessionId, status, detail: tool }
: { sessionId, status };
}