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:
Yaojia Wang
2026-06-17 19:00:34 +02:00
parent a411c89ee9
commit c81cebdc47
5 changed files with 143 additions and 2 deletions

View File

@@ -12,7 +12,7 @@ import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { SearchAddon } from '@xterm/addon-search'
import { WebLinksAddon } from '@xterm/addon-web-links'
import type { ClientMessage, ServerMessage } from '../src/types.js'
import type { ClaudeStatus, ClientMessage, ServerMessage } from '../src/types.js'
import { folderFromTitle } from './title-util.js'
const RESET = '\x1b[0m'
@@ -51,6 +51,8 @@ export interface TerminalSessionOpts {
onTitle?: (title: string) => void
/** Optional: fired when connection status changes (for the tab status dot). */
onStatus?: (status: SessionStatus) => void
/** Optional: fired when Claude Code activity changes (from a hook, H2). */
onClaudeStatus?: (status: ClaudeStatus, detail?: string) => void
}
export class TerminalSession {
@@ -64,7 +66,9 @@ export class TerminalSession {
private readonly onActivity: (() => void) | undefined
private readonly onTitle: ((title: string) => void) | undefined
private readonly onStatus: ((status: SessionStatus) => void) | undefined
private readonly onClaudeStatus: ((status: ClaudeStatus, detail?: string) => void) | undefined
private statusValue: SessionStatus = 'connecting'
private claudeStatusValue: ClaudeStatus = 'unknown'
private ws: WebSocket | null = null
private sessionId: string | null
@@ -83,6 +87,7 @@ export class TerminalSession {
this.onActivity = opts.onActivity
this.onTitle = opts.onTitle
this.onStatus = opts.onStatus
this.onClaudeStatus = opts.onClaudeStatus
this.el = document.createElement('div')
this.el.className = 'term-pane'
@@ -118,6 +123,11 @@ export class TerminalSession {
return this.statusValue
}
/** Current Claude Code activity (from hooks, H2). */
get claudeStatus(): ClaudeStatus {
return this.claudeStatusValue
}
private setStatus(s: SessionStatus): void {
this.statusValue = s
this.onStatus?.(s)
@@ -206,6 +216,11 @@ export class TerminalSession {
if (this.el.style.display === 'none') this.onActivity?.()
break
}
case 'status': {
this.claudeStatusValue = msg.status
this.onClaudeStatus?.(msg.status, msg.detail)
break
}
case 'exit': {
this.setStatus('exited')
const reason = msg.reason ? ` (${msg.reason})` : ''