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:
Yaojia Wang
2026-06-17 19:13:07 +02:00
parent 04355f05e9
commit 9a6150c355
10 changed files with 279 additions and 23 deletions

View File

@@ -22,11 +22,19 @@ import path from 'node:path'
const SETTINGS = path.join(homedir(), '.claude', 'settings.json')
const MARKER = 'WEBTERM_HOOK_URL' // identifies our hook entries
const COMMAND =
// Fire-and-forget status events: POST and discard the response.
const FF_COMMAND =
'curl -s -X POST "$WEBTERM_HOOK_URL" -H "X-Webterm-Session: $WEBTERM_SESSION"' +
' -H "Content-Type: application/json" --data-binary @- >/dev/null 2>&1 || true'
// Events we care about (status derives from the event/notification_type server-side).
const EVENTS = ['UserPromptSubmit', 'PreToolUse', 'Notification', 'PermissionRequest', 'Stop', 'SessionEnd']
const FF_EVENTS = ['UserPromptSubmit', 'PreToolUse', 'Notification', 'Stop', 'SessionEnd']
// PermissionRequest is HELD: curl waits for the server's response (the decision
// JSON, produced when you tap Approve/Reject) and writes it to stdout for Claude.
// Empty/failed (outside web-terminal, or timeout) → Claude shows its own prompt.
const PERM_COMMAND =
'curl -s --max-time 350 -X POST "${WEBTERM_HOOK_URL}/permission"' +
' -H "X-Webterm-Session: $WEBTERM_SESSION" -H "Content-Type: application/json" --data-binary @-'
const remove = process.argv.includes('--remove')
@@ -59,10 +67,12 @@ for (const ev of Object.keys(settings.hooks)) {
}
if (!remove) {
for (const ev of EVENTS) {
for (const ev of FF_EVENTS) {
if (!Array.isArray(settings.hooks[ev])) settings.hooks[ev] = []
settings.hooks[ev].push({ hooks: [{ type: 'command', command: COMMAND }] })
settings.hooks[ev].push({ hooks: [{ type: 'command', command: FF_COMMAND }] })
}
if (!Array.isArray(settings.hooks['PermissionRequest'])) settings.hooks['PermissionRequest'] = []
settings.hooks['PermissionRequest'].push({ hooks: [{ type: 'command', command: PERM_COMMAND }] })
}
mkdirSync(path.dirname(SETTINGS), { recursive: true })
@@ -71,7 +81,7 @@ writeFileSync(SETTINGS, `${JSON.stringify(settings, null, 2)}\n`)
console.log(
remove
? `Removed web-terminal hooks from ${SETTINGS}`
: `Installed web-terminal hooks into ${SETTINGS} (events: ${EVENTS.join(', ')})`,
: `Installed web-terminal hooks into ${SETTINGS} (events: ${[...FF_EVENTS, 'PermissionRequest'].join(', ')})`,
)
if (existsSync(`${SETTINGS}.webterm-bak`)) console.log(`Backup: ${SETTINGS}.webterm-bak`)
console.log('Restart any running `claude` sessions for the hooks to take effect.')