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:
@@ -69,6 +69,8 @@ export class TerminalSession {
|
||||
private readonly onClaudeStatus: ((status: ClaudeStatus, detail?: string) => void) | undefined
|
||||
private statusValue: SessionStatus = 'connecting'
|
||||
private claudeStatusValue: ClaudeStatus = 'unknown'
|
||||
private pendingApprovalValue = false
|
||||
private pendingToolValue: string | undefined = undefined
|
||||
|
||||
private ws: WebSocket | null = null
|
||||
private sessionId: string | null
|
||||
@@ -128,6 +130,14 @@ export class TerminalSession {
|
||||
return this.claudeStatusValue
|
||||
}
|
||||
|
||||
/** Whether a tool approval is held server-side and can be resolved (H3). */
|
||||
get pendingApproval(): boolean {
|
||||
return this.pendingApprovalValue
|
||||
}
|
||||
get pendingTool(): string | undefined {
|
||||
return this.pendingToolValue
|
||||
}
|
||||
|
||||
private setStatus(s: SessionStatus): void {
|
||||
this.statusValue = s
|
||||
this.onStatus?.(s)
|
||||
@@ -218,6 +228,8 @@ export class TerminalSession {
|
||||
}
|
||||
case 'status': {
|
||||
this.claudeStatusValue = msg.status
|
||||
this.pendingApprovalValue = msg.pending === true
|
||||
this.pendingToolValue = msg.pending === true ? msg.detail : undefined
|
||||
this.onClaudeStatus?.(msg.status, msg.detail)
|
||||
break
|
||||
}
|
||||
@@ -259,10 +271,24 @@ export class TerminalSession {
|
||||
}, delay)
|
||||
}
|
||||
|
||||
private sendMsg(msg: ClientMessage): void {
|
||||
if (this.ws === null || this.ws.readyState !== WebSocket.OPEN) return
|
||||
this.ws.send(buildMessage(msg))
|
||||
}
|
||||
|
||||
/** Send raw bytes as input (used by term.onData and the key bar). */
|
||||
send(data: string): void {
|
||||
if (this.ws === null || this.ws.readyState !== WebSocket.OPEN) return
|
||||
this.ws.send(buildMessage({ type: 'input', data }))
|
||||
this.sendMsg({ type: 'input', data })
|
||||
}
|
||||
|
||||
/** Resolve a held PermissionRequest (H3). */
|
||||
approve(): void {
|
||||
this.pendingApprovalValue = false
|
||||
this.sendMsg({ type: 'approve' })
|
||||
}
|
||||
reject(): void {
|
||||
this.pendingApprovalValue = false
|
||||
this.sendMsg({ type: 'reject' })
|
||||
}
|
||||
|
||||
/** Scrollback search (M1). */
|
||||
|
||||
Reference in New Issue
Block a user