feat(v0.7): Walk-away Workbench (Band A + B) — multi-agent parallel build
Implements docs/PLAN_WALKAWAY_WORKBENCH.md (27 tasks, waves R0→W0→W1×14→W2→W3→W4)
via module-builder agents. 23 tasks built, 0 blocked.
Band A (finish the walk-away loop): A1 Web Push + lock-screen approve/deny
(web-push dep), A2 voice dictation, A3 quick-reply chips + saved-prompt palette,
A4 activity timeline, A5 stuck/idle alert.
Band B (workbench above the terminal): B1 read-only git diff viewer, B2 statusLine
telemetry → per-tab cost/context/PR gauges, B3 create git worktrees from the UI,
B4 plan-mode / permission-mode relay.
New: src/push/* (subscription store + VAPID push), src/http/{diff,statusline}.ts,
src/session/timeline.ts, public/{diff,timeline,quickreply,push-ui,...}.ts, sw-push,
statusLine script; extends hook intake, manager, server routes (Origin/CSRF guards
+ per-IP rate limits on state-changing ones; loopback-only ingest), terminal-session,
tabs, projects detail, service worker, setup-hooks (statusLine + ntfy bridge).
Orchestrator reconciled a W0 contract gap: added the 21 v0.7 Config fields to the
Config interface in types.ts (T-types had left them only in config.ts's return).
Verified: both tsc clean, full vitest + coverage 91.4/84.1/92.2/93.4 (≥80×4),
build:web OK. W4 review: no CRITICAL/HIGH; all security checks pass. Follow-ups
(non-blocking): move approve.mode validation into parseClientMessage, drop CSP
ws:/wss: wildcard, validate worktree base ref, +2 targeted tests.
This commit is contained in:
@@ -13,7 +13,14 @@ import type { ITheme } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { SearchAddon } from '@xterm/addon-search'
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import type { ClaudeStatus, ClientMessage, ServerMessage } from '../src/types.js'
|
||||
import type {
|
||||
ClaudeStatus,
|
||||
ClientMessage,
|
||||
PermissionGate,
|
||||
PermissionMode,
|
||||
ServerMessage,
|
||||
StatusTelemetry,
|
||||
} from '../src/types.js'
|
||||
import { folderFromTitle, cwdFromOsc7 } from './title-util.js'
|
||||
|
||||
// Delay after the shell is ready before typing a session's initial command
|
||||
@@ -56,6 +63,9 @@ export interface TerminalSessionOpts {
|
||||
onStatus?: (status: SessionStatus) => void
|
||||
/** Optional: fired when Claude Code activity changes (from a hook, H2). */
|
||||
onClaudeStatus?: (status: ClaudeStatus, detail?: string) => void
|
||||
/** Optional: fired when new statusLine telemetry arrives (B2). Single source of
|
||||
* truth — T-tabs reads session.telemetry via the getter, not its own copy. */
|
||||
onTelemetry?: (telemetry: StatusTelemetry) => void
|
||||
/** Optional: spawn a NEW session in this directory ("new tab here", M6). */
|
||||
cwd?: string
|
||||
/** Optional: type this once the new session's shell is ready (O2 resume). */
|
||||
@@ -74,6 +84,7 @@ export class TerminalSession {
|
||||
private readonly onTitle: ((title: string) => void) | undefined
|
||||
private readonly onStatus: ((status: SessionStatus) => void) | undefined
|
||||
private readonly onClaudeStatus: ((status: ClaudeStatus, detail?: string) => void) | undefined
|
||||
private readonly onTelemetry: ((telemetry: StatusTelemetry) => void) | undefined
|
||||
private readonly spawnCwd: string | undefined
|
||||
private readonly initialInput: string | undefined
|
||||
private initialSent = false
|
||||
@@ -82,6 +93,8 @@ export class TerminalSession {
|
||||
private cwdValue: string | null = null
|
||||
private pendingApprovalValue = false
|
||||
private pendingToolValue: string | undefined = undefined
|
||||
private telemetryValue: StatusTelemetry | null = null
|
||||
private pendingGateValue: PermissionGate | null = null
|
||||
|
||||
private ws: WebSocket | null = null
|
||||
private sessionId: string | null
|
||||
@@ -102,6 +115,7 @@ export class TerminalSession {
|
||||
this.onTitle = opts.onTitle
|
||||
this.onStatus = opts.onStatus
|
||||
this.onClaudeStatus = opts.onClaudeStatus
|
||||
this.onTelemetry = opts.onTelemetry
|
||||
this.spawnCwd = opts.cwd
|
||||
this.initialInput = opts.initialInput
|
||||
|
||||
@@ -163,6 +177,18 @@ export class TerminalSession {
|
||||
return this.pendingToolValue
|
||||
}
|
||||
|
||||
/** Latest statusLine telemetry (B2). Single source of truth (review #15):
|
||||
* T-tabs reads this getter; TabEntry does NOT cache its own copy. */
|
||||
get telemetry(): StatusTelemetry | null {
|
||||
return this.telemetryValue
|
||||
}
|
||||
|
||||
/** The gate kind from the last pending status frame (B4): 'plan' or 'tool'.
|
||||
* Null when no approval is held or the last status had no gate. */
|
||||
get pendingGate(): PermissionGate | null {
|
||||
return this.pendingGateValue
|
||||
}
|
||||
|
||||
private setStatus(s: SessionStatus): void {
|
||||
this.statusValue = s
|
||||
this.onStatus?.(s)
|
||||
@@ -270,9 +296,15 @@ export class TerminalSession {
|
||||
this.claudeStatusValue = msg.status
|
||||
this.pendingApprovalValue = msg.pending === true
|
||||
this.pendingToolValue = msg.pending === true ? msg.detail : undefined
|
||||
this.pendingGateValue = msg.gate ?? null
|
||||
this.onClaudeStatus?.(msg.status, msg.detail)
|
||||
break
|
||||
}
|
||||
case 'telemetry': {
|
||||
this.telemetryValue = msg.telemetry
|
||||
this.onTelemetry?.(msg.telemetry)
|
||||
break
|
||||
}
|
||||
case 'exit': {
|
||||
this.setStatus('exited')
|
||||
const reason = msg.reason ? ` (${msg.reason})` : ''
|
||||
@@ -294,6 +326,14 @@ export class TerminalSession {
|
||||
})
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// Compile-time exhaustiveness: TypeScript narrows msg to 'never' here
|
||||
// once all ServerMessage variants are handled. Silently ignored at runtime
|
||||
// (future server additions won't crash older clients).
|
||||
const _exhaustive: never = msg
|
||||
void _exhaustive
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,10 +358,12 @@ export class TerminalSession {
|
||||
this.sendMsg({ type: 'input', data })
|
||||
}
|
||||
|
||||
/** Resolve a held PermissionRequest (H3). */
|
||||
approve(): void {
|
||||
/** Resolve a held PermissionRequest (H3). Optionally relay a permission-mode
|
||||
* change (B4): mode is included only when explicitly provided so the server
|
||||
* can distinguish "approve with default" from "approve, keep existing mode". */
|
||||
approve(mode?: PermissionMode): void {
|
||||
this.pendingApprovalValue = false
|
||||
this.sendMsg({ type: 'approve' })
|
||||
this.sendMsg({ type: 'approve', ...(mode !== undefined ? { mode } : {}) })
|
||||
}
|
||||
reject(): void {
|
||||
this.pendingApprovalValue = false
|
||||
|
||||
Reference in New Issue
Block a user