feat(app): v0.1 UX surfaces — startup panels + InputMonitoring banner + PTT #156

Merged
KofTwentyTwo merged 3 commits from feat/app-ux-surfaces-v0.1 into develop 2026-05-13 11:32:16 +00:00
KofTwentyTwo commented 2026-05-12 22:10:31 +00:00 (Migrated from github.com)

Summary

Three v0.1 AppDelegate UX holes wired in one branch (sequential commits, each touches App/AppDelegate.swift + adjacent App-target files).

  • #89 — Status panel + DevOverlay open on app start (closed in dca0695)
    Debug builds default startupSurfacesMode = .diagnostic, which opens HUD + Status (top-right) + DevOverlay (bottom-right, Logs tab). Release builds default .minimal. Skipped during the first-launch wizard and under XCTest hosts. DevOverlayView gained an initialTab: parameter so the panel can open straight to Logs; DevOverlayWindow exposes setFrameOrigin(_:) + frameSize for AppDelegate positioning.

  • #88 — InputMonitoring TCC banner v1 hole (closed in 48fb8b9)
    BannerContent.inputMonitoringDenied is now nonDismissible: true — user must not be able to click past a denial that breaks global hotkeys (silent no-op is the failure mode the banner exists to prevent). New HUDBannerCoordinator.dismiss(id:) clears a banner programmatically without poisoning dismissedThisLaunch. AppDelegate.reprobeInputMonitoring fires on NSApplication.didBecomeActiveNotification and handles both transitions: was-denied → now-granted dismisses the banner and rebinds the global hotkey under fresh TCC; was-granted → now-denied re-enqueues. IOHIDCheckAccess is query-only so the foreground hook never prompts.

  • #87 — PTT (VOICE-13) wired (closed in 112f584)
    WizardState.pttHotkey is a new persisted shortcut under Jarvis.pttHotkey (independent of the summon-Jarvis hotkey). Ships unbound — CLAUDE.md hotkey hygiene forbids defaulting because Cmd+Shift+J and Option+Space collide with too many tools. AppDelegate.rebindPTTFromState is called from three sites: installVoice tail (after PushToTalk is constructed), the foreground TCC re-grant path, and the Settings Save/Clear handlers. Settings UI gains a "Push-to-talk hotkey" section with the same Save/Clear pattern as the summon hotkey, fed by a separate onPTTHotkeyChanged callback.

Audio-graph isolation (re: Voice CRIT lane)

PTT.bind installs NSEvent.addGlobalMonitorForEvents callbacks only — it does NOT touch the audio graph. The press path routes through VoiceController.pttDown, which uses the audio graph the wake-word DAG already owns. So this lane is safe to land in parallel with fix/voice-crit-v0.1 (wake-word fix). No feature flag needed.

Test plan

  • bash scripts/check-app-builds.sh — App target compiles cleanly (Debug, arm64)
  • All 17 grep-based boundary gates green (including check-no-modal-presentation.sh and check-single-writer-hudstate.sh)
  • swift test --package-path packages/DevOverlay — 14 tests pass (no regression from initialTab: addition)
  • swift test --package-path packages/Voice — 26 tests pass (no regression from PTT call-site addition)
  • New tests added:
    • 3 in HUDBannerCoordinatorTests — programmatic dismiss-by-id clears non-dismissible banners, doesn't poison the id, no-op on unknown id.
    • 3 in WizardStateTests — PTT hotkey defaults to nil, persists round-trip, independent of summon hotkey.
  • Manual cold-launch sanity (Debug archive): HUD + Status (top-right) + DevOverlay (bottom-right, Logs tab) all visible; toggle off InputMonitoring → banner appears non-dismissible; toggle on in System Settings → banner clears on foreground; bind PTT in Settings → hold-to-talk activates STT.

Notes

  • DO NOT merge until the user has manually verified the three flows above.
  • The PR branch was created from a fresh develop checkout — clean of the worktree's fix/voice-crit-v0.1 state.
  • No package-level API removals; DevOverlayView.init, DevOverlayWindow.init, SettingsView.init, and SettingsWindowController.open all extended with default-valued parameters so existing call sites stay valid.

🤖 Generated with Claude Code

## Summary Three v0.1 AppDelegate UX holes wired in one branch (sequential commits, each touches `App/AppDelegate.swift` + adjacent App-target files). - **#89 — Status panel + DevOverlay open on app start** (closed in dca0695) Debug builds default `startupSurfacesMode = .diagnostic`, which opens HUD + Status (top-right) + DevOverlay (bottom-right, Logs tab). Release builds default `.minimal`. Skipped during the first-launch wizard and under XCTest hosts. `DevOverlayView` gained an `initialTab:` parameter so the panel can open straight to Logs; `DevOverlayWindow` exposes `setFrameOrigin(_:)` + `frameSize` for AppDelegate positioning. - **#88 — InputMonitoring TCC banner v1 hole** (closed in 48fb8b9) `BannerContent.inputMonitoringDenied` is now `nonDismissible: true` — user must not be able to click past a denial that breaks global hotkeys (silent no-op is the failure mode the banner exists to prevent). New `HUDBannerCoordinator.dismiss(id:)` clears a banner programmatically without poisoning `dismissedThisLaunch`. `AppDelegate.reprobeInputMonitoring` fires on `NSApplication.didBecomeActiveNotification` and handles both transitions: was-denied → now-granted dismisses the banner and rebinds the global hotkey under fresh TCC; was-granted → now-denied re-enqueues. `IOHIDCheckAccess` is query-only so the foreground hook never prompts. - **#87 — PTT (VOICE-13) wired** (closed in 112f584) `WizardState.pttHotkey` is a new persisted shortcut under `Jarvis.pttHotkey` (independent of the summon-Jarvis hotkey). Ships unbound — CLAUDE.md hotkey hygiene forbids defaulting because Cmd+Shift+J and Option+Space collide with too many tools. `AppDelegate.rebindPTTFromState` is called from three sites: `installVoice` tail (after PushToTalk is constructed), the foreground TCC re-grant path, and the Settings Save/Clear handlers. Settings UI gains a "Push-to-talk hotkey" section with the same Save/Clear pattern as the summon hotkey, fed by a separate `onPTTHotkeyChanged` callback. ## Audio-graph isolation (re: Voice CRIT lane) PTT.bind installs `NSEvent.addGlobalMonitorForEvents` callbacks only — it does NOT touch the audio graph. The press path routes through `VoiceController.pttDown`, which uses the audio graph the wake-word DAG already owns. So this lane is safe to land in parallel with `fix/voice-crit-v0.1` (wake-word fix). No feature flag needed. ## Test plan - [x] `bash scripts/check-app-builds.sh` — App target compiles cleanly (Debug, arm64) - [x] All 17 grep-based boundary gates green (including `check-no-modal-presentation.sh` and `check-single-writer-hudstate.sh`) - [x] `swift test --package-path packages/DevOverlay` — 14 tests pass (no regression from `initialTab:` addition) - [x] `swift test --package-path packages/Voice` — 26 tests pass (no regression from PTT call-site addition) - [x] New tests added: - 3 in `HUDBannerCoordinatorTests` — programmatic dismiss-by-id clears non-dismissible banners, doesn't poison the id, no-op on unknown id. - 3 in `WizardStateTests` — PTT hotkey defaults to nil, persists round-trip, independent of summon hotkey. - [ ] Manual cold-launch sanity (Debug archive): HUD + Status (top-right) + DevOverlay (bottom-right, Logs tab) all visible; toggle off InputMonitoring → banner appears non-dismissible; toggle on in System Settings → banner clears on foreground; bind PTT in Settings → hold-to-talk activates STT. ## Notes - DO NOT merge until the user has manually verified the three flows above. - The PR branch was created from a fresh `develop` checkout — clean of the worktree's `fix/voice-crit-v0.1` state. - No package-level API removals; `DevOverlayView.init`, `DevOverlayWindow.init`, `SettingsView.init`, and `SettingsWindowController.open` all extended with default-valued parameters so existing call sites stay valid. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.