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.
This commit is contained in:
Yaojia Wang
2026-06-17 18:44:07 +02:00
parent 8c2a6825cc
commit a411c89ee9
7 changed files with 218 additions and 6 deletions

View File

@@ -58,17 +58,26 @@ export function createSession(
now: number,
onExit: (session: Session) => void,
): Session {
// Generate the id first so it can be injected into the shell env: Claude Code
// hooks running inside this shell read $WEBTERM_SESSION to tell us which tab
// an event belongs to, and POST to $WEBTERM_HOOK_URL (H2).
const id = randomUUID();
// M4: let a spawn failure (e.g. missing shell) propagate synchronously.
const pty: IPty = spawn(cfg.shellPath, [], {
name: 'xterm-256color',
cols: dims.cols,
rows: dims.rows,
cwd: cfg.homeDir,
env: process.env,
env: {
...process.env,
WEBTERM_SESSION: id,
WEBTERM_HOOK_URL: `http://127.0.0.1:${cfg.port}/hook`,
},
});
const meta: SessionMeta = {
id: randomUUID(),
id,
createdAt: now,
shellPath: cfg.shellPath,
};
@@ -81,6 +90,7 @@ export function createSession(
lastOutputAt: now,
exitedAt: null,
exitCode: null,
claudeStatus: 'unknown',
pty,
};