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:
@@ -43,13 +43,47 @@ export class TabApp {
|
||||
private notifyAsked = false
|
||||
private readonly paneHost: HTMLElement
|
||||
private readonly tabBar: HTMLElement
|
||||
private readonly approvalBar: HTMLDivElement
|
||||
|
||||
constructor(paneHost: HTMLElement, tabBar: HTMLElement) {
|
||||
this.paneHost = paneHost
|
||||
this.tabBar = tabBar
|
||||
this.approvalBar = document.createElement('div')
|
||||
this.approvalBar.id = 'approvalbar'
|
||||
this.approvalBar.style.display = 'none'
|
||||
document.body.appendChild(this.approvalBar)
|
||||
this.restore()
|
||||
}
|
||||
|
||||
/** Show/hide the approve/reject banner for the active tab's held request (H3). */
|
||||
private updateApprovalBar(): void {
|
||||
const session = this.tabs[this.activeIndex]?.session
|
||||
if (!session || !session.pendingApproval) {
|
||||
this.approvalBar.style.display = 'none'
|
||||
return
|
||||
}
|
||||
this.approvalBar.replaceChildren()
|
||||
const label = document.createElement('span')
|
||||
label.className = 'approval-label'
|
||||
label.textContent = `Claude wants to use ${session.pendingTool ?? 'a tool'}`
|
||||
const approve = document.createElement('button')
|
||||
approve.className = 'approval-yes'
|
||||
approve.textContent = '✓ Approve'
|
||||
approve.addEventListener('click', () => {
|
||||
session.approve()
|
||||
this.updateApprovalBar()
|
||||
})
|
||||
const reject = document.createElement('button')
|
||||
reject.className = 'approval-no'
|
||||
reject.textContent = '✗ Reject'
|
||||
reject.addEventListener('click', () => {
|
||||
session.reject()
|
||||
this.updateApprovalBar()
|
||||
})
|
||||
this.approvalBar.append(label, approve, reject)
|
||||
this.approvalBar.style.display = 'flex'
|
||||
}
|
||||
|
||||
private displayTitle(entry: TabEntry, idx: number): string {
|
||||
return entry.customTitle ?? entry.autoTitle ?? `Term ${idx + 1}`
|
||||
}
|
||||
@@ -138,6 +172,7 @@ export class TabApp {
|
||||
onStatus: () => this.refreshTab(entry),
|
||||
onClaudeStatus: (status) => {
|
||||
this.refreshTab(entry)
|
||||
this.updateApprovalBar()
|
||||
// Notify when a background tab needs approval (H2/H4).
|
||||
if (status === 'waiting' && this.tabs.indexOf(entry) !== this.activeIndex) {
|
||||
this.notify(entry)
|
||||
@@ -165,6 +200,7 @@ export class TabApp {
|
||||
if (entry) entry.hasActivity = false // viewing clears the unread dot
|
||||
this.tabs.forEach((t, idx) => (idx === i ? t.session.show() : t.session.hide()))
|
||||
this.tabs.forEach((t) => this.refreshTab(t)) // in-place class/text update, no rebuild
|
||||
this.updateApprovalBar()
|
||||
this.persist()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user