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:
Yaojia Wang
2026-06-30 17:42:18 +02:00
parent 4f1d3ebc6b
commit d6809c65c4
54 changed files with 13171 additions and 200 deletions

View File

@@ -22,9 +22,10 @@
* Tab \t complete / toggle
* ← → \x1b[D/C cursor
* / / slash-command launcher
* 🎤 (voice) push-to-talk mic (A2; only when SpeechRecognition supported)
*/
import type { MountKeybar } from '../src/types.js'
import { isSpeechSupported } from './voice.js'
/** Key name → byte string mapping (pure data, for testing). */
export const KEY_MAP = {
@@ -78,8 +79,32 @@ export const KEYBAR_BUTTONS: KeybarButton[] = [
{ label: '/', caption: '命令', keyName: 'slash', title: 'Slash — command launcher' },
]
/** Mount the key bar: create buttons in #keybar, bind touchstart → onSend(bytes) + preventDefault. */
export const mountKeybar: MountKeybar = (onSend) => {
/**
* Options for mountKeybar (A2 voice extension).
*
* All fields are optional so existing callers that pass only `onSend`
* continue to work unchanged.
*/
export interface KeybarOpts {
/**
* Push-to-talk callback. Called with 'start' on touchstart/mousedown and
* 'stop' on touchend/mouseup. The 🎤 button is only rendered when speech
* recognition is supported by the browser AND this callback is supplied.
*
* SEC-L2: Chrome Web Speech forwards audio to Google's servers — the button
* title discloses this so the user can make an informed choice.
*/
onVoiceTrigger?: (action: 'start' | 'stop') => void
}
/**
* Mount the key bar: create buttons in #keybar, bind touchstart → onSend(bytes) +
* preventDefault (keeps the soft keyboard hidden on mobile).
*
* A2: when SpeechRecognition is supported and opts.onVoiceTrigger is provided,
* appends a 🎤 push-to-talk button at the end of the bar.
*/
export function mountKeybar(onSend: (data: string) => void, opts?: KeybarOpts): void {
const keybarEl = document.getElementById('keybar')
if (!keybarEl) return
@@ -115,4 +140,50 @@ export const mountKeybar: MountKeybar = (onSend) => {
keybarEl.appendChild(btn)
}
// A2: voice mic button — only when speech is supported and caller provides a trigger.
if (isSpeechSupported() && opts?.onVoiceTrigger) {
keybarEl.appendChild(buildMicButton(opts.onVoiceTrigger))
}
}
/**
* Build the 🎤 push-to-talk button element.
* SEC-L2: title discloses that Chrome sends audio to Google.
*/
function buildMicButton(onVoiceTrigger: (action: 'start' | 'stop') => void): HTMLButtonElement {
const btn = document.createElement('button')
btn.classList.add('keybar-btn')
btn.dataset.key = 'voice'
btn.title = 'Hold to dictate — audio processed by browser speech engine (Chrome: Google)'
btn.setAttribute('aria-label', 'Push to talk')
const keyEl = document.createElement('span')
keyEl.className = 'kb-key'
keyEl.textContent = '🎤'
const capEl = document.createElement('span')
capEl.className = 'kb-cap'
capEl.textContent = '语音'
btn.append(keyEl, capEl)
// push-to-talk: start on press, stop on release — preventDefault keeps soft keyboard hidden.
btn.addEventListener('touchstart', (e) => {
e.preventDefault()
onVoiceTrigger('start')
})
btn.addEventListener('touchend', (e) => {
e.preventDefault()
onVoiceTrigger('stop')
})
// Desktop fallback: mousedown/mouseup for push-to-talk semantics.
btn.addEventListener('mousedown', (e) => {
e.preventDefault()
onVoiceTrigger('start')
})
btn.addEventListener('mouseup', (e) => {
e.preventDefault()
onVoiceTrigger('stop')
})
return btn
}