#89: new AppDelegate.startupSurfacesMode enum (.minimal/.diagnostic). Debug defaults .diagnostic; Release defaults .minimal. openStartupSurfacesIfDiagnostic() runs at tail of bootHealthTask (skipped under XCTest or when wizard is open). Status panel top-right + Dev Overlay bottom-right of NSScreen.main.visibleFrame. DevOverlay opens to Logs tab. #88: BannerContent.inputMonitoringDenied is now nonDismissible: true. New HUDBannerCoordinator.dismiss(id:) clears a banner programmatically without writing dismissedThisLaunch. AppDelegate.reprobeInputMonitoring fires on NSApplication.didBecomeActiveNotification; rebinds the global hotkey on re-grant. Uses IOHIDCheckAccess (query-only, no prompt) per CLAUDE.md guidance. 3 new coordinator tests. #87: WizardState.pttHotkey persisted under Jarvis.pttHotkey (independent of summon hotkey). Ships unbound per CLAUDE.md hotkey hygiene. AppDelegate.rebindPTTFromState() called from installVoice, foreground TCC re-grant, Settings Save/Clear. SettingsView extended with PTT hotkey section. Audio-graph isolation: NSEvent monitors only, no audio-graph surgery — safe alongside fix/voice-crit-v0.1. 3 new WizardState tests. DevOverlay 14 + Voice 26 tests pass; all 17 grep boundary gates green (incl. single-writer-hudstate + no-modal-presentation).
73 lines
2.5 KiB
Swift
73 lines
2.5 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import Keychain
|
|
import Shell
|
|
|
|
/// Hosts the `SettingsView` in an `NSWindow`. Distinct from the first-run
|
|
/// wizard:
|
|
///
|
|
/// Setup… → `OnboardingWizardController` (sequential first-run flow)
|
|
/// Settings… → `SettingsWindowController` (this — freeform settings panel)
|
|
///
|
|
/// Both share the same `WizardState` because the canonical truth lives there
|
|
/// (Keychain for the API key, IOHIDCheckAccess for input monitoring,
|
|
/// UserDefaults `Jarvis.hotkey` for the global shortcut). Edits in either
|
|
/// surface write through to those stores so the other surface sees current
|
|
/// values on next open.
|
|
///
|
|
/// Window discipline mirrors `OnboardingWizardController`: a single reusable
|
|
/// window, brought forward and refreshed on each `open(...)`. No stacking.
|
|
@MainActor
|
|
public final class SettingsWindowController {
|
|
public let state: WizardState
|
|
private var window: NSWindow?
|
|
|
|
public init(state: WizardState) {
|
|
self.state = state
|
|
}
|
|
|
|
public func open(
|
|
keychain: any KeychainStore,
|
|
validator: AnthropicKeyValidator,
|
|
onGrantInputMonitoring: @escaping () -> Bool,
|
|
onHotkeyChanged: @escaping (Shell.KeyboardShortcut?) -> Void,
|
|
onPTTHotkeyChanged: @escaping (Shell.KeyboardShortcut?) -> Void = { _ in }
|
|
) {
|
|
// Refresh truth before view creation: TCC grants and Keychain entries
|
|
// can change between sessions, same reasoning as the wizard's
|
|
// refresh-on-open in `OnboardingWizardController`.
|
|
state.refresh()
|
|
|
|
if let existing = window, existing.isVisible {
|
|
existing.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
return
|
|
}
|
|
|
|
let rootView = SettingsView(
|
|
state: state,
|
|
keychain: keychain,
|
|
validator: validator,
|
|
onGrantInputMonitoring: onGrantInputMonitoring,
|
|
onHotkeyChanged: onHotkeyChanged,
|
|
onPTTHotkeyChanged: onPTTHotkeyChanged,
|
|
onClose: { [weak self] in self?.close() }
|
|
)
|
|
let hosting = NSHostingController(rootView: rootView)
|
|
let window = NSWindow(contentViewController: hosting)
|
|
window.styleMask = [.titled, .closable, .miniaturizable]
|
|
window.title = "Settings — Jarvis"
|
|
window.isReleasedWhenClosed = false
|
|
self.window = window
|
|
|
|
window.center()
|
|
window.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
}
|
|
|
|
public func close() {
|
|
window?.close()
|
|
window = nil
|
|
}
|
|
}
|