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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user