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
3 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 112f584f02 feat(app): wire push-to-talk hotkey (VOICE-13) — closes #87
The push-to-talk path was fully implemented but completely unwired:
`VoiceController.pttDown()` / `pttUp()` and `PushToTalk.bind(...)` all
existed as dead code because no production caller ever bound a hotkey.

Three changes plug the gap end-to-end:

  - `WizardState.pttHotkey` — persisted PTT shortcut under a separate
    UserDefaults key (`Jarvis.pttHotkey`) from the summon-Jarvis hotkey.
    Ships unbound (nil): CLAUDE.md hotkey-hygiene forbids pre-binding
    because Cmd+Shift+J and Option+Space collide with too many tools.
  - `AppDelegate.rebindPTTFromState` — pulls the persisted shortcut and
    `inputMonitoringGranted` flag, then calls `PushToTalk.bind(...)`.
    Called from three sites: installVoice tail (after `pushToTalk` is
    constructed), the foreground TCC re-grant path (so a global monitor
    is acquired under fresh TCC), and the Settings Save/Clear handlers.
  - `SettingsView.pttHotkeySection` — same Save/Clear pattern as the
    summon hotkey, fed by a separate `onPTTHotkeyChanged` callback to
    avoid coupling the two paths.

Audio-graph isolation: PTT.bind installs `NSEvent.addGlobalMonitorFor
Events` callbacks only; 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 the Voice
CRIT branch (`fix/voice-crit-v0.1`) — no audio-graph surgery.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 17:07:12 -05:00
james.maesandClaude Opus 4.7 48fb8b9bab feat(app): non-dismissible Input Monitoring banner + foreground re-probe — closes #88
SHELL-06 v1 requirement: surface a HUD banner when global key monitoring
silently no-ops on TCC denial, and clear it when the user grants access.
Settings already exposed the state; the HUD path was missing.

Three changes:
  - `BannerContent.inputMonitoringDenied` is now `nonDismissible: true`.
    The user must not be able to click past a denial that breaks global
    hotkeys — the silent-no-op failure mode is exactly what this banner
    exists to prevent.
  - `HUDBannerCoordinator.dismiss(id:)` clears a banner by id, bypassing
    the `nonDismissible` flag, without poisoning `dismissedThisLaunch`.
    The condition can recur (user revokes again), so we need to be able
    to re-enqueue after a programmatic clear.
  - `AppDelegate.reprobeInputMonitoring` runs on
    `NSApplication.didBecomeActiveNotification`. Was-denied → now-granted
    dismisses the banner and rebinds the global hotkey under fresh TCC
    (the original bind silently fell back to local-only). Was-granted →
    now-denied re-enqueues the banner.

`IOHIDCheckAccess` reads the cached TCC decision without prompting, so
this is safe to call on every foreground hop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 16:41:38 -05:00
james.maesandClaude Opus 4.7 dca0695c61 feat(app): open Status panel + DevOverlay alongside HUD on app start — closes #89
When Jarvis launches in Debug builds, the HUD, Status panel, and DevOverlay
all now open visible by default — eliminating the three-menu-click discovery
gap to the operator's primary observability surface.

Behavior controlled by `AppDelegate.startupSurfacesMode`:
  - `.diagnostic` (Debug default) — HUD + Status + DevOverlay all visible.
  - `.minimal`    (Release default) — HUD only.

Positioning: HUD stays centered; Status panel goes top-right, DevOverlay
goes bottom-right of `NSScreen.main.visibleFrame`. DevOverlay opens to the
Logs tab (index 3) so the operator sees activity stream immediately
instead of a blank Agent tab.

Skip conditions: wizard open (don't stack panels behind the wizard) and
XCTest hosts (don't pop panels during unit-test runs).

Plumbing:
  - `DevOverlayView.init` gains `initialTab: Int = 0` parameter; selection
    becomes `@State` driven so the first tab can be picked at construction.
  - `DevOverlayWindow.init` forwards `initialTab` and exposes
    `setFrameOrigin(_:)` + `frameSize` so AppDelegate can position the
    panel without KVC-poking at the private `panel` field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 16:23:09 -05:00