feat(scratchpad): voice + mobile-first + Mac satellite quick-capture#28
Open
ovdmar wants to merge 15 commits into
Open
feat(scratchpad): voice + mobile-first + Mac satellite quick-capture#28ovdmar wants to merge 15 commits into
ovdmar wants to merge 15 commits into
Conversation
…satellite Spec updates landing first per the implementation plan at .agents/plans/scratchpad-capture-voice-mobile.md: - B.2: scratchpad voice mic affordance; mobile-default route to /scratchpad on narrow viewports; mobile-tightened layout (composer safe-area pinning, ≥36px tap targets, history sidebar behind toggle). - B.7: rename "per-workspace scratchpad.md" to the actual global scratchpad at config.dataDir/scratchpad.md; document the new GET /quick-capture daemon-served HTML page that posts to the existing block endpoint; add a Security/trust-model subsection naming the daemon's localhost-trusted, CORS-open posture. - B.1: document /?modal=new-workspace deeplink for external launchers. - C: note that scripts/mac-satellite/ is macOS-only and outside make check.
apps/daemon/src/app.ts was at 804 lines (file-size gate cap is 800; scripts/checks/file-size.ts reported it as 805). Extracting the SPA shell + static-asset fallback into a sibling module preserves behavior verbatim (including the /api/* and /events fall-through guard) and drops app.ts to 797 lines, restoring margin for upcoming work. The new helper also owns the default web/dist path resolution so callers don't have to repeat the import.meta.url dance — fileURLToPath is now unused in app.ts and the import is removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a microphone toggle next to the cockpit scratchpad composer and the per-block editor. Tapping dictates into the textarea; another tap, blur, or 10s of silence stops recognition. Logic lives in a non-React controller (speech-recognition-controller.ts) so the silence-timer behavior and the iOS-Safari synchronous-throw path are unit-testable without RTL. useSpeechRecognition is a thin React adapter, VoiceCaptureButton is a stateless lucide-icon toggle that returns null when neither SpeechRecognition nor webkitSpeechRecognition is exposed — older browsers and locked-down environments get no UI at all rather than a non-functional button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ratchpad On viewports matching (max-width: 820px), a bare landing at the cockpit root now redirects to /scratchpad so opening Citadel on iPhone goes straight to the capture surface. Deeplinks with any search or hash (including /?modal=new-workspace) are unaffected — the redirect mirrors the isBareRootLanding semantics in last-route.ts. The pre-router gate (applyBootstrapNavigationFromWindow) intentionally runs the mobile rule BEFORE bootstrapLastRoute so a mobile user with a persisted desktop route (e.g. /settings) still lands on scratchpad. On desktop the mobile rule no-ops and bootstrap restores the saved route unchanged. Mobile-tightened scratchpad layout: composer pinned with safe-area padding (viewport-fit=cover now in index.html so env() is non-zero on iOS), history sidebar collapsed behind a History toggle in the page header, mic/history controls sized to ≥36×36 tap targets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…page A self-contained HTML page (inline CSS+JS, no bundler) designed to be opened in a chromeless popup by external shortcuts. It posts to the existing POST /api/scratchpad/blocks — no new write surface — and on success attempts window.close(). In Chrome --app= mode close succeeds; in Safari and regular tabs the close call no-ops and the page swaps itself for a "Captured. Press ⌘W to close." confirmation ~60ms later. Includes the same Web Speech API mic toggle as the cockpit composer (graceful absence when neither SpeechRecognition nor webkitSpeechRecognition is exposed). Registered before registerSpaFallback so the SPA wildcard doesn't swallow the path; route tests guard against accidental sub-path matching and method confusion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…modal External launchers (the Mac satellite new-workspace shortcut) jump straight into workspace creation by opening the cockpit at this URL. The Cockpit component checks shouldOpenNewWorkspaceModal on mount, opens the existing modal, and consumeNewWorkspaceDeeplink strips the param via history.replaceState — preserving other params and the hash — so a page refresh doesn't re-open it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two thin POSIX-shell helpers that wrap the daemon's web surfaces into global-shortcut targets on macOS: - quick-capture.sh opens GET /quick-capture in a Chrome --app= popup (Safari fallback) sized like Spotlight. Liveness-probes the daemon first; surfaces a macOS notification if it's not reachable. - new-workspace.sh opens /?modal=new-workspace in the default browser, which the cockpit recognises as a deeplink to the Create Workspace modal. Both default to the systemd long-term daemon at 127.0.0.1:4010, overridable via CITADEL_HOST/CITADEL_PORT. Worktree daemons (4110+) are deliberately NOT auto-discovered — a global shortcut can't infer which worktree is active. README documents Hammerspoon (recommended, silent) and Shortcuts.app (fallback, banner) bindings plus the trust model (daemon is single-user localhost-trusted; cross-LAN exposure is operator-responsibility). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new Playwright specs: - scratchpad-mobile.spec.ts — on narrow viewports (≤820px), bare root redirects to /scratchpad; History toggle is in the header; the version history sidebar is hidden by default and visible after toggling. - scratchpad-mobile-deeplink.spec.ts — BLOCKER-2 regression guard: /?modal=new-workspace on a narrow viewport stays at / (no mobile redirect) and auto-opens the Create Workspace modal. - quick-capture.spec.ts — hits the daemon-served /quick-capture page, fills the textarea, ⌘+Enter, and asserts via the API that a new scratchpad block surfaces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-fixes from biome check --write across the files this branch touched; also picks up a stray pre-existing format nit in apps/daemon/src/terminal-routes.ts that surfaced once biome ran across the full surface for make check. Switches the speech-recognition test cleanup from `delete` to undefined-assignment to satisfy lint/performance/noDelete (the controller's resolveCtor uses ??, so undefined and missing are equivalent for the support probe). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gets Addresses the surviving findings from /review-pr: - VoiceCaptureButton: move onError invocation from render into a useEffect keyed on speech.error so a stateful onError can't cause a render loop. Expose an imperative `stop()` via controlRef so hosts can satisfy spec B.2 #9 ("blurring stops voice capture"). - Scratchpad composer + per-block editor: wire the new mic stop() handle into their onBlur paths so dictation stops on textarea blur. - Extract `appendTranscript()` (composer/editor used to copy-paste the trim-and-join one-liner). The daemon-served quick-capture page keeps its inlined copy because it's bundler-free. - speech-recognition-controller.dispose(): null the onresult/onerror/ onend handlers BEFORE abort() so the async onend the browser fires during teardown cannot reach stopAndReport on a disposed controller. New test simulates the late callback. - scratchpad.css mobile breakpoint: enlarge .scratchpad-block-delete to 36×36 to meet spec B.2 #11 tap-target requirement. - specs/B.2-ade-cockpit.md line 108: change "per-workspace" → "global" to match the B.7 storage description. - quick-capture-route.ts: template the silence timeout from the new QUICK_CAPTURE_SILENCE_TIMEOUT_MS constant instead of a magic 10000. - New integration test (quick-capture-route.test.ts) registers both the quick-capture route AND the SPA fallback in both orders and asserts the inline HTML wins only when registered first. Catches any future swap that would let the SPA wildcard swallow the page. - New bootstrap-redirect test for the primary AC2 case: narrow + bare + no saved route → /scratchpad. - E2E scratchpad-mobile: assert composer focus after redirect and mic boundingBox ≥ 36×36 (skipped when SpeechRecognition not exposed in headless Chromium). - E2E scratchpad-mobile-deeplink: tighter assertion that the URL stays bare-root (not /scratchpad) and that consumeNewWorkspaceDeeplink strips the modal param after mount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…odal mount
CI surfaced two interactions with the mobile-first scratchpad redirect
that didn't fail locally:
- e2e/operator-cockpit.spec.ts navigates to "/" on the mobile project
and expects the cockpit shell. The new redirect now sends bare-root
mobile loads to /scratchpad, so the test never sees the shell. Swap
both gotos to "/?from=cockpit-test" — any non-bare URL skips the
redirect (isBareRootLanding requires empty search). The cockpit
shell is what the test exercises; the redirect's bare-URL behavior
is covered by scratchpad-mobile.spec.ts.
- The Create Workspace modal is rendered as a child of the Navigator
column. On mobile the column has `display: none` by default
(mobileView starts at 'stage'), so the modal mounts into the DOM
but is invisible — the deeplink test asserted toBeVisible() on the
modal heading and failed. Cockpit's deeplink useEffect now also
calls setMobileView("navigator") when ?modal=new-workspace fires;
desktop is unaffected (mobileView has no visual effect there).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pture
Phase 2 of the mac-satellite work, originally deferred but folded into
this PR per user request. The native shell now ships alongside the
existing scripts/mac-satellite/ POSIX-shell fallbacks.
apps/mac-satellite/ — Electron app that registers two global shortcuts:
⌘⇧S → Spotlight-shaped frameless BrowserWindow loading the daemon's
existing /quick-capture page. Toggle-like: a second press while
the popup is focused closes it. Esc closes too.
⌘⇧N → opens the cockpit at /?modal=new-workspace in the user's
default browser; the cockpit's deeplink auto-opens the
Create Workspace modal.
Runs as a background utility (app.dock.hide()), single-instance,
re-resolves the daemon target on every press so env-var overrides
take effect after a restart. Liveness probe (2s, fetch with
AbortController) surfaces a native notification if the daemon
isn't reachable rather than silently failing.
No new daemon endpoints — all traffic goes through the surfaces this
PR's earlier commits added (GET /quick-capture, POST /api/scratchpad/
blocks, /?modal=new-workspace deeplink). The shell-script helpers under
scripts/mac-satellite/ remain as the no-Electron path.
Electron over Tauri: keeps CI on its existing pnpm/TS toolchain (no
Rust), the satellite is ~200 LOC, and globalShortcut does exactly what
we need. Pinned electron@33.2.1 in apps/mac-satellite/devDependencies;
added to pnpm-workspace.yaml allowBuilds so the postinstall binary
download runs. apps/mac-satellite/ added to the root tsconfig
project-references list so check:size, typecheck, lint, and test all
sweep it like any other workspace.
Pure helpers (config.ts: daemon URL resolution, env-var precedence,
Spotlight popup geometry with multi-monitor offsets) are unit-tested
in src/config.test.ts. The Electron main process typechecks but is
exercised manually with `pnpm --filter @citadel/mac-satellite dev`
because Electron's runtime is not headless-testable in CI.
apps/mac-satellite/README.md documents the dev flow, configuration,
trust model, and the Electron-vs-Tauri choice. scripts/mac-satellite/
README updated to point at the new app as the recommended path. The
C-technical-stack spec describes both deliveries (Electron app +
shell-script fallback) and which CI gates apply to each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ure-voice-mobile-dt70fe
After merging origin/main, biome flagged a single-line expression that status-monitor-wiring.ts's status-monitor-wiring brought in from the new 2e31415 status-monitor work. One-line format fix; no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ure-voice-mobile-dt70fe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/scratchpad; layout tightened (composer safe-area, History sidebar behind a toggle, ≥36×36 tap targets).GET /quick-captureSpotlight-style HTML page +scripts/mac-satellite/shell helpers wiringcmd+shift+s(quick-capture) and a sibling shortcut (new-workspace via cockpit deeplink?modal=new-workspace).Plan
.agents/plans/scratchpad-capture-voice-mobile.md
Out of scope (deferred to a follow-up): a native Tauri/Electron Mac
.appthat wraps the same/quick-capturepage and registers the global shortcut itself. This PR's surface area is forward-compatible — the follow-up reuses the same URL and the same deeplink.Test plan
make checkpasses locally (typecheck, biome, vitest, coverage, deps, build)pnpm e2epasses locally — three new specs added (mobile redirect, mobile deeplink regression guard, quick-capture happy path)pnpm smoke— daemon HTTP surface gainedGET /quick-capture; recommend a quick smoke runTest surface added
append-transcript,bootstrap-redirect,mobile-redirect,new-workspace-deeplink,speech-recognition-controller(+quick-capture-route,spa-fallback-routeon the daemon side). 35+ new assertions including a regression test for the daemon route-registration order that catches future swaps which would let the SPA wildcard swallow/quick-capture.scratchpad-mobile,scratchpad-mobile-deeplink(BLOCKER-2 regression guard),quick-capture.How to QA
Then:
/scratchpad. Composer is focused, mic button visible (in Chrome), History sidebar hidden by default with a toggle in the header.For the Mac satellite shortcuts (macOS only):
For E2E coverage:
🤖 Generated with Claude Code