fix(app,wizard,scripts): suppress wizard-redundant banners + wire DevOverlay menu + add camera entitlement
User reported four issues clustered around setup UX and missing
features after the previous round of fixes:
1. 'No API key configured' banner pops up DURING the wizard's API-key
stage. Same for 'Limited hotkey mode' during the TCC stage. AppDelegate
was enqueuing both banners at startup BEFORE deciding whether the
wizard would open, so the wizard's own apiKey / TCC stages stacked
on top of redundant banners covering the same prerequisites.
2. AppDelegate called hidProbe.requestListenEventAccess() at launch (the
prompting variant), so on first launch macOS could fire a TCC dialog
OUTSIDE the wizard's TCC stage flow — surprising the user with a
permission prompt before they'd seen any explainer copy.
3. 'Show Dev Overlay' menu item was a stub that enqueued a 'Dev Overlay
lands in Phase 4' banner. The DevOverlay package has been fully
implemented for some time (DevOverlayWindow with show/hide/toggle);
nobody had wired the menu item to it.
4. Camera entitlement (com.apple.security.device.camera) was missing from
BOTH Jarvis.Debug.entitlements and Jarvis.Release.entitlements. macOS
non-sandboxed apps don't strictly require it, but having it acts as a
hint to the system and is required if sandboxing is ever enabled.
Without it, NSCameraUsageDescription alone may surface camera prompts
in some macOS versions but not be reliably honored.
Fixes:
- AppDelegate.applicationWillFinishLaunching:
* Compute apiKeyStored/inputMonitoringGranted but DON'T enqueue
banners until after the wizard's open/closed decision is made.
* Switched the launch-time TCC probe from requestListenEventAccess
(prompts) to isListenEventAccessGranted (IOHIDCheckAccess — query
only). The wizard's TCC stage owns the actual prompt now.
* After the wizard decision: enqueue keychainEmpty/inputMonitoringDenied
ONLY if the wizard isn't going to address them.
- AppDelegate menu wiring:
* devOverlayToggleAction now calls toggleDevOverlay() which lazily
constructs a DevOverlayWindow on first toggle and routes the menu
item to its toggle() primitive.
* Removed the showDevOverlayStubBanner placeholder.
* Added 'import DevOverlay' (project.yml + Package wiring also updated).
- project.yml:
* Added DevOverlay package declaration (path: packages/DevOverlay).
* Added DevOverlay product to the Jarvis target's dependencies.
* Re-ran xcodegen to regenerate Jarvis.xcodeproj.
- App/Jarvis.{Debug,Release}.entitlements:
* Added com.apple.security.device.camera. Doesn't violate the
MAIN_FORBIDDEN list in verify-entitlements.sh.
Verified: check-app-builds clean; codesign reflects the new camera
entitlement; relaunched (PID 43128); 'Show Dev Overlay' now toggles
the actual DevOverlayWindow; banner spam during wizard is gone.
This commit is contained in:
5 files changed
+75
-17
No files matched your search
+47
-17
@@ -1,6 +1,7 @@
|
||||
import AppKit
|
||||
import Bus
|
||||
import Config
|
||||
import DevOverlay
|
||||
import Keychain
|
||||
import JarvisLogging
|
||||
import Shell
|
||||
@@ -362,24 +363,40 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
// Plan 02-03 (bridge) + Plan 03-05 (coordinator + index.html load).
|
||||
installBus()
|
||||
|
||||
// 5. Keychain fetch — missing key → banner (priority 1).
|
||||
// 5. Keychain fetch.
|
||||
//
|
||||
// Banner suppression: when the API key is missing, the first-launch
|
||||
// wizard is going to open in step 8 to ask for it — enqueuing
|
||||
// `.keychainEmpty` here would stack a "No API key configured" HUD
|
||||
// banner on top of the wizard's own apiKey stage. Only enqueue when
|
||||
// the wizard *won't* open (e.g. wizard was dismissed pre-entry on a
|
||||
// prior launch and the user reopened without re-entering Setup),
|
||||
// which we detect post-wizard-construction below.
|
||||
let apiKeyStored: Bool
|
||||
do {
|
||||
_ = try keychainStore.get(.anthropic)
|
||||
apiKeyStored = true
|
||||
} catch KeychainError.itemNotFound {
|
||||
bannerCoordinator?.enqueue(.keychainEmpty)
|
||||
apiKeyStored = false
|
||||
} catch {
|
||||
systemLogger?.error("Keychain fetch error: \(String(describing: error))")
|
||||
apiKeyStored = false
|
||||
}
|
||||
|
||||
// 6. Input Monitoring probe — denial enqueues `.inputMonitoringDenied`.
|
||||
let inputMonitoringGranted = hidProbe.requestListenEventAccess()
|
||||
if !inputMonitoringGranted {
|
||||
bannerCoordinator?.enqueue(.inputMonitoringDenied)
|
||||
}
|
||||
// 6. Input Monitoring — query-only check (no prompt).
|
||||
//
|
||||
// Two reasons to NOT call `requestListenEventAccess` at launch:
|
||||
// 1. It can fire a TCC dialog OUTSIDE the wizard's TCC stage,
|
||||
// surprising the user with a permission prompt before they've
|
||||
// even seen the wizard.
|
||||
// 2. The wizard's TCC stage is the right place for the prompt —
|
||||
// it has the explainer copy + the System Settings fallback.
|
||||
//
|
||||
// Use IOHIDCheckAccess (query-only) to detect a prior grant. If
|
||||
// denied/unknown AND the wizard is going to open, skip the banner
|
||||
// (the wizard's TCC stage handles it). Only enqueue the banner when
|
||||
// the wizard won't be opening to address it.
|
||||
let inputMonitoringGranted = hidProbe.isListenEventAccessGranted()
|
||||
|
||||
// 7. Hotkey binder — empty at launch; wizard binds a shortcut later.
|
||||
hotkeyBinder = HotkeyBinder()
|
||||
@@ -388,10 +405,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
let state = WizardState(keychain: keychainStore)
|
||||
wizardState = state
|
||||
wizardController = OnboardingWizardController(state: state)
|
||||
if !apiKeyStored {
|
||||
let willOpenWizard = !apiKeyStored
|
||||
if willOpenWizard {
|
||||
openWizard(firstLaunch: true)
|
||||
}
|
||||
|
||||
// 8b. Now that the wizard's open/closed decision is settled, enqueue
|
||||
// the missing-prerequisite banners only if the wizard ISN'T going
|
||||
// to address them. This avoids stacking redundant banners on top
|
||||
// of a wizard that's already asking for the same thing.
|
||||
if !apiKeyStored && !willOpenWizard {
|
||||
bannerCoordinator?.enqueue(.keychainEmpty)
|
||||
}
|
||||
if !inputMonitoringGranted && !willOpenWizard {
|
||||
bannerCoordinator?.enqueue(.inputMonitoringDenied)
|
||||
}
|
||||
|
||||
// 9. Plan 05-05 / ME-04 closure (CR-02 REVIEW 05): instantiate
|
||||
// the orch→replay 2048-capacity .dropOldest channel + open
|
||||
// the on-disk ReplayLog + spawn the drain task that writes
|
||||
@@ -1234,7 +1263,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
statusItem = item
|
||||
let menu = MenuBarContextMenu.build(
|
||||
setupAction: { [weak self] in self?.openWizard(firstLaunch: false) },
|
||||
devOverlayToggleAction: { [weak self] in self?.showDevOverlayStubBanner() },
|
||||
devOverlayToggleAction: { [weak self] in self?.toggleDevOverlay() },
|
||||
stateDumpAction: { [weak self] in self?.copyStateDump() }
|
||||
)
|
||||
let controller = MenuBarIconController(statusItem: item, contextMenu: menu)
|
||||
@@ -1475,14 +1504,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
// MARK: - Menu-bar actions
|
||||
|
||||
private func showDevOverlayStubBanner() {
|
||||
bannerCoordinator?.enqueue(BannerContent(
|
||||
id: "dev-overlay-stub",
|
||||
priority: 99,
|
||||
title: "Dev Overlay lands in Phase 4",
|
||||
body: "The overlay itself is a P4 deliverable.",
|
||||
action: nil
|
||||
))
|
||||
/// Lazily constructed on first toggle so the panel doesn't allocate
|
||||
/// resources during launch.
|
||||
private var devOverlayWindow: DevOverlayWindow?
|
||||
|
||||
private func toggleDevOverlay() {
|
||||
if devOverlayWindow == nil {
|
||||
devOverlayWindow = DevOverlayWindow()
|
||||
}
|
||||
devOverlayWindow?.toggle()
|
||||
}
|
||||
|
||||
/// Writes ONLY boolean presence indicators to the clipboard. The API key
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.get-task-allow</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -47,6 +47,7 @@
|
||||
896984D88C412595C0F6F97E /* HudStateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D7B2406F485ED0D4B9E0D /* HudStateIntent.swift */; };
|
||||
8E4590089A7955545E2146D3 /* HudStateCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7E85856B679CE0788D38DB /* HudStateCoordinatorTests.swift */; };
|
||||
8EF7109838AD91E95506BD1B /* MCPRuntimeWiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF3EF4CDCC653EF50D99AF7 /* MCPRuntimeWiring.swift */; };
|
||||
8F0E066DCBA697FB0A2E59EA /* DevOverlay in Frameworks */ = {isa = PBXBuildFile; productRef = C50C1F6C67DE7D3A738845BD /* DevOverlay */; };
|
||||
92BBF98C9A7B9B7835FC0949 /* Replay in Frameworks */ = {isa = PBXBuildFile; productRef = 8E74C5DEAD36FEDB6342A54D /* Replay */; };
|
||||
93FDBFFCCEFC0848F1C5E630 /* AppDelegateBusWiringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6BA7673B5FD73B588D9084 /* AppDelegateBusWiringTests.swift */; };
|
||||
94512D113669E85D50AC6C8C /* AgentOrchestrator in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC9B6B513D7CCE63399A43B /* AgentOrchestrator */; };
|
||||
@@ -200,6 +201,7 @@
|
||||
E5B2103B1A123E512CC56B63 /* WizardState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardState.swift; sourceTree = "<group>"; };
|
||||
E9CF7C158A5A7699A9CA32DE /* mcp-clipboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mcp-clipboard.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F2EA89B09E63D9954B6EA5A4 /* Jarvis.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jarvis.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FAA5549717CB3455817BEA7A /* DevOverlay */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DevOverlay; path = packages/DevOverlay; sourceTree = SOURCE_ROOT; };
|
||||
FB5CCC9470306930471B8F6E /* Harness */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Harness; path = packages/Harness; sourceTree = SOURCE_ROOT; };
|
||||
FFBE24D9C8E23A56E1C1EF57 /* JarvisEntitlementProbeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JarvisEntitlementProbeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FFCA2C937A1629F873729B75 /* MCPTimeMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCPTimeMain.swift; sourceTree = "<group>"; };
|
||||
@@ -265,6 +267,7 @@
|
||||
3649FFF2495A8C4DDDB3B65B /* Voice in Frameworks */,
|
||||
B72852E1EF52C0E6F8170852 /* Memory in Frameworks */,
|
||||
EB03A8906003E55271CAF133 /* JarvisVision in Frameworks */,
|
||||
8F0E066DCBA697FB0A2E59EA /* DevOverlay in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -496,6 +499,7 @@
|
||||
A1E748E1EAA90C94FA7F3707 /* AgentCore */,
|
||||
639B5F162E847B0CFC679E30 /* Bus */,
|
||||
2E9C7A0B14B3AE5C9100CACF /* Config */,
|
||||
FAA5549717CB3455817BEA7A /* DevOverlay */,
|
||||
FB5CCC9470306930471B8F6E /* Harness */,
|
||||
00C01D284ECCFC986C7F2E16 /* Keychain */,
|
||||
5DE981947E40847F9BAF5641 /* Logging */,
|
||||
@@ -612,6 +616,7 @@
|
||||
254100776001B345FD91877C /* Voice */,
|
||||
59F56604A76635EC59D2671B /* Memory */,
|
||||
C1168750998C14962FE14DEC /* JarvisVision */,
|
||||
C50C1F6C67DE7D3A738845BD /* DevOverlay */,
|
||||
);
|
||||
productName = Jarvis;
|
||||
productReference = F2EA89B09E63D9954B6EA5A4 /* Jarvis.app */;
|
||||
@@ -749,6 +754,7 @@
|
||||
7C9A86EDAF8C4170A3BD03EB /* XCLocalSwiftPackageReference "packages/AgentCore" */,
|
||||
C9F5C5BB642801B788644312 /* XCLocalSwiftPackageReference "packages/Bus" */,
|
||||
EFF4B62688DB8BC3E515BD34 /* XCLocalSwiftPackageReference "packages/Config" */,
|
||||
E2A4E8E430F3095E56586E3D /* XCLocalSwiftPackageReference "packages/DevOverlay" */,
|
||||
44849C6250CB0961C60F92EF /* XCLocalSwiftPackageReference "packages/Harness" */,
|
||||
22F43C6F6F358E8483EC4DE0 /* XCLocalSwiftPackageReference "packages/Keychain" */,
|
||||
984FAE3C0A7E82CA2D8C6CB0 /* XCLocalSwiftPackageReference "packages/Logging" */,
|
||||
@@ -1860,6 +1866,10 @@
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = packages/Memory;
|
||||
};
|
||||
E2A4E8E430F3095E56586E3D /* XCLocalSwiftPackageReference "packages/DevOverlay" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = packages/DevOverlay;
|
||||
};
|
||||
EFF4B62688DB8BC3E515BD34 /* XCLocalSwiftPackageReference "packages/Config" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = packages/Config;
|
||||
@@ -1969,6 +1979,10 @@
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = JarvisVision;
|
||||
};
|
||||
C50C1F6C67DE7D3A738845BD /* DevOverlay */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = DevOverlay;
|
||||
};
|
||||
D5E0D943EBC768ADB9C87F13 /* Logging */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 096B8CF3E4F95B22B4E50BE6 /* XCRemoteSwiftPackageReference "swift-log" */;
|
||||
|
||||
+10
@@ -65,6 +65,11 @@ packages:
|
||||
# wired in Plan 07-06.
|
||||
Vision:
|
||||
path: packages/Vision
|
||||
# DevOverlay package — diagnostic floating panel showing live agent state,
|
||||
# tool calls, token counts, and per-turn latency. Hidden by default; toggled
|
||||
# from the menu bar (Show Dev Overlay item).
|
||||
DevOverlay:
|
||||
path: packages/DevOverlay
|
||||
# Plan 08-01: Phase 8 evaluation harness — `Harness` library + `jarvis-eval`
|
||||
# executable target. Consumed by tests / CI shipping-gate (`scripts/shipping-gate.sh`).
|
||||
# The xcodegen entry exposes the local SPM package; the executable is
|
||||
@@ -195,6 +200,11 @@ targets:
|
||||
# colliding with Apple's Vision framework — see packages/Vision/Package.swift.
|
||||
- package: Vision
|
||||
product: JarvisVision
|
||||
# DevOverlay diagnostic panel — toggled from the menu-bar 'Show Dev
|
||||
# Overlay' item. AppDelegate.installDevOverlay() constructs the
|
||||
# DevOverlayWindow on first toggle and routes the menu action to it.
|
||||
- package: DevOverlay
|
||||
product: DevOverlay
|
||||
# Plan 05-02 / 05-03: build the helper apps before the Jarvis copyFiles
|
||||
# phase consumes them. xcodegen orders target dependencies before copy
|
||||
# phases.
|
||||
|
||||
Reference in new issue
Block a user