feat(v0.3): H3 — remote approve/reject (no typing)
- types/protocol: ClientMessage approve/reject; ServerMessage status.pending
- server: POST /hook/permission HELD until the client decides; approve/reject
ws messages resolve it with {hookSpecificOutput.decision.behavior allow|deny};
5-min timeout + release-on-disconnect fall back to Claude's own prompt
- manager.handleHookEvent threads pending flag
- setup-hooks: PermissionRequest uses the held curl (writes decision to stdout);
status events stay fire-and-forget
- FE: terminal-session approve()/reject() + pending state; tabs approval banner
(Approve/Reject) for the active tab's held request
- Tests: protocol approve/reject; integration ⑧ held→approve→allow. 207 green.
- Browser-verified: banner 'Claude wants to use Bash' → Approve → resolves allow.
This commit is contained in:
20
src/types.ts
20
src/types.ts
@@ -38,23 +38,26 @@ export type EnvLike = Readonly<Record<string, string | undefined>>;
|
||||
|
||||
/* ─────────────────────── protocol (§3.2) ─────────────────────── */
|
||||
|
||||
/** client → server */
|
||||
/** client → server. approve/reject (H3) resolve a held PermissionRequest. */
|
||||
export type ClientMessage =
|
||||
| { type: 'attach'; sessionId: string | null }
|
||||
| { type: 'input'; data: string }
|
||||
| { type: 'resize'; cols: number; rows: number };
|
||||
| { type: 'resize'; cols: number; rows: number }
|
||||
| { type: 'approve' }
|
||||
| { type: 'reject' };
|
||||
|
||||
/** Claude Code activity, derived from hooks (H2). 'unknown' = no hook signal yet. */
|
||||
export type ClaudeStatus = 'working' | 'waiting' | 'idle' | 'unknown';
|
||||
|
||||
/** server → client. exit.code = shell code; -1 when spawn never succeeded (M4).
|
||||
* exit.reason optional normally, REQUIRED on spawn failure / abnormal exit.
|
||||
* status (H2) = Claude Code activity pushed from a hook side-channel. */
|
||||
* status (H2/H3) = Claude Code activity; `pending` true when a tool approval
|
||||
* is held server-side and the client can approve/reject it. */
|
||||
export type ServerMessage =
|
||||
| { type: 'attached'; sessionId: string }
|
||||
| { type: 'output'; data: string }
|
||||
| { type: 'exit'; code: number; reason?: string }
|
||||
| { type: 'status'; status: ClaudeStatus; detail?: string };
|
||||
| { type: 'status'; status: ClaudeStatus; detail?: string; pending?: boolean };
|
||||
|
||||
/** parseClientMessage result — never throws; errors flow here (§5.3). */
|
||||
export type ParseResult =
|
||||
@@ -163,8 +166,13 @@ export interface Session {
|
||||
export interface SessionManager {
|
||||
handleAttach(ws: WebSocketLike, sessionId: string | null, dims: Dims, now: number): Session;
|
||||
get(id: string): Session | undefined;
|
||||
/** Set a session's Claude status (from a hook) and push it to the attached ws (H2). */
|
||||
handleHookEvent(sessionId: string, status: ClaudeStatus, detail?: string): void;
|
||||
/** Set a session's Claude status (from a hook) and push it to the attached ws (H2/H3). */
|
||||
handleHookEvent(
|
||||
sessionId: string,
|
||||
status: ClaudeStatus,
|
||||
detail?: string,
|
||||
pending?: boolean,
|
||||
): void;
|
||||
/** reclaim when now - max(detachedAt, lastOutputAt) > idleTtlMs (M3). Returns count. */
|
||||
reapIdle(now: number): number;
|
||||
shutdown(): void;
|
||||
|
||||
Reference in New Issue
Block a user