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:
@@ -22,7 +22,14 @@
|
||||
* Coding style: immutable-update preference, no console.log, errors explicit.
|
||||
*/
|
||||
|
||||
import type { Config, Dims, Session, SessionManager, WebSocketLike } from '../types.js';
|
||||
import type {
|
||||
ClaudeStatus,
|
||||
Config,
|
||||
Dims,
|
||||
Session,
|
||||
SessionManager,
|
||||
WebSocketLike,
|
||||
} from '../types.js';
|
||||
import { WS_OPEN } from '../types.js';
|
||||
import { serialize } from '../protocol.js';
|
||||
import { createSession, attachWs, kill } from './session.js';
|
||||
@@ -119,6 +126,20 @@ export function createSessionManager(cfg: Config): SessionManager {
|
||||
return sessions.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* H2: a Claude Code hook reported activity for `sessionId`. Record it and push
|
||||
* a `status` frame to the attached ws (no-op if the session is gone/detached).
|
||||
*/
|
||||
function handleHookEvent(sessionId: string, status: ClaudeStatus, detail?: string): void {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session === undefined) return;
|
||||
session.claudeStatus = status;
|
||||
sendIfOpen(
|
||||
session.attachedWs,
|
||||
detail !== undefined ? { type: 'status', status, detail } : { type: 'status', status },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reclaim detached sessions whose liveness window has expired (M3).
|
||||
*
|
||||
@@ -159,5 +180,5 @@ export function createSessionManager(cfg: Config): SessionManager {
|
||||
sessions = new Map();
|
||||
}
|
||||
|
||||
return { handleAttach, get, reapIdle, shutdown };
|
||||
return { handleAttach, get, handleHookEvent, reapIdle, shutdown };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user