feat(v0.3): H4 — live Claude status badge + notifications + setup-hooks
- terminal-session: handle 'status' frame → claudeStatus + onClaudeStatus - tabs: per-tab Claude glyph (⚙ working / ⏳ waiting / ✓ idle); inactive 'waiting' tab gets an amber highlight + a browser notification (permission requested on first tab switch) - scripts/setup-hooks.mjs + 'npm run setup-hooks': idempotently install/remove command hooks in ~/.claude/settings.json (inline curl to $WEBTERM_HOOK_URL with $WEBTERM_SESSION; no-op outside web-terminal); backs up settings.json - Browser-verified end-to-end: PermissionRequest→⏳, PreToolUse→⚙, Stop→✓; inactive waiting tab shows amber + glyph. 205 tests green.
This commit is contained in:
@@ -40,6 +40,7 @@ export class TabApp {
|
||||
private activeIndex = -1
|
||||
private editingIndex = -1
|
||||
private dragIndex = -1
|
||||
private notifyAsked = false
|
||||
private readonly paneHost: HTMLElement
|
||||
private readonly tabBar: HTMLElement
|
||||
|
||||
@@ -135,6 +136,13 @@ export class TabApp {
|
||||
this.refreshTab(entry)
|
||||
},
|
||||
onStatus: () => this.refreshTab(entry),
|
||||
onClaudeStatus: (status) => {
|
||||
this.refreshTab(entry)
|
||||
// Notify when a background tab needs approval (H2/H4).
|
||||
if (status === 'waiting' && this.tabs.indexOf(entry) !== this.activeIndex) {
|
||||
this.notify(entry)
|
||||
}
|
||||
},
|
||||
})
|
||||
this.paneHost.appendChild(entry.session.el)
|
||||
this.tabs.push(entry)
|
||||
@@ -151,6 +159,7 @@ export class TabApp {
|
||||
|
||||
activate(i: number): void {
|
||||
if (i < 0 || i >= this.tabs.length) return
|
||||
this.maybeAskNotify() // first switch is a user gesture — request notif permission
|
||||
this.activeIndex = i
|
||||
const entry = this.tabs[i]
|
||||
if (entry) entry.hasActivity = false // viewing clears the unread dot
|
||||
@@ -218,6 +227,22 @@ export class TabApp {
|
||||
this.tabs[this.activeIndex]?.session.clearSearch()
|
||||
}
|
||||
|
||||
/** Ask for notification permission once, on a user gesture (H2/H4). */
|
||||
private maybeAskNotify(): void {
|
||||
if (this.notifyAsked) return
|
||||
this.notifyAsked = true
|
||||
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
||||
void Notification.requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
/** Browser notification for a background tab needing approval. */
|
||||
private notify(entry: TabEntry): void {
|
||||
if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return
|
||||
const title = this.displayTitle(entry, this.tabs.indexOf(entry))
|
||||
new Notification(`Claude needs you — ${title}`, { body: 'Waiting for approval' })
|
||||
}
|
||||
|
||||
/* ── rendering ───────────────────────────────────────────────── */
|
||||
|
||||
/** In-place update of one tab's classes/label/dot — never destroys the DOM. */
|
||||
@@ -227,12 +252,19 @@ export class TabApp {
|
||||
const idx = this.tabs.indexOf(entry)
|
||||
el.classList.toggle('active', idx === this.activeIndex)
|
||||
el.classList.toggle('unread', entry.hasActivity && idx !== this.activeIndex)
|
||||
const cs = entry.session.claudeStatus
|
||||
el.classList.toggle('claude-waiting', cs === 'waiting' && idx !== this.activeIndex)
|
||||
const title = this.displayTitle(entry, idx)
|
||||
el.title = title
|
||||
const dot = el.querySelector('.tab-dot')
|
||||
if (dot) dot.className = `tab-dot dot-${entry.session.status}`
|
||||
const label = el.querySelector('.tab-label')
|
||||
if (label) label.textContent = title
|
||||
const claude = el.querySelector('.tab-claude')
|
||||
if (claude) {
|
||||
claude.textContent =
|
||||
cs === 'working' ? '⚙' : cs === 'waiting' ? '⏳' : cs === 'idle' ? '✓' : ''
|
||||
}
|
||||
}
|
||||
|
||||
/** Full rebuild — ONLY for structural changes (add/close/reorder/rename). */
|
||||
@@ -311,6 +343,10 @@ export class TabApp {
|
||||
})
|
||||
tabEl.appendChild(label)
|
||||
|
||||
const claude = document.createElement('span')
|
||||
claude.className = 'tab-claude'
|
||||
tabEl.appendChild(claude)
|
||||
|
||||
const close = document.createElement('button')
|
||||
close.className = 'tab-close'
|
||||
close.textContent = '×'
|
||||
|
||||
Reference in New Issue
Block a user