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>
This commit is contained in:
1 parent
ba42b6918a
commit
dca0695c61
3 files changed
+125
-18
No files matched your search
+99
-14
@@ -297,6 +297,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
/// so the SwiftUI model state survives the panel cycling.
|
||||
private var statusPanel: StatusPanel?
|
||||
|
||||
/// Issue #89 — startup-surfaces mode. `.diagnostic` opens Status panel +
|
||||
/// DevOverlay alongside the HUD on app start; `.minimal` opens only the
|
||||
/// HUD. Defaults to `.diagnostic` for Debug builds (developer's
|
||||
/// observability surface is one launch away, not three menu clicks) and
|
||||
/// `.minimal` for Release builds. Settable by injection in tests; tests
|
||||
/// also use the `XCTestConfigurationFilePath` short-circuit below to
|
||||
/// keep test runs from popping panels.
|
||||
enum StartupSurfaces { case minimal, diagnostic }
|
||||
var startupSurfacesMode: StartupSurfaces = {
|
||||
#if DEBUG
|
||||
return .diagnostic
|
||||
#else
|
||||
return .minimal
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Set to `true` when `applicationWillFinishLaunching` decided to open
|
||||
/// the onboarding wizard. The `bootHealthTask` tail reads this to skip
|
||||
/// auto-opening Status + DevOverlay over the top of the wizard.
|
||||
private var startupWizardWillOpen: Bool = false
|
||||
|
||||
/// Drains `orchToReplayChannel` into `replayLog`. Pre-CR-02 this Task
|
||||
/// `for await _ in channel`'d into `/dev/null` and the channel had no
|
||||
/// producer either — ME-04's contract was structurally unverifiable
|
||||
@@ -724,6 +745,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
wizardController = OnboardingWizardController(state: state)
|
||||
settingsWindowController = SettingsWindowController(state: state)
|
||||
let willOpenWizard = !apiKeyStored
|
||||
startupWizardWillOpen = willOpenWizard
|
||||
if willOpenWizard {
|
||||
openWizard(firstLaunch: true)
|
||||
}
|
||||
@@ -899,6 +921,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
bootHealthTask = Task { @MainActor [weak self] in
|
||||
await self?.selfKnowledgeInstallTask?.value
|
||||
await self?.runBootHealth()
|
||||
// 17. Issue #89 — open Status panel + DevOverlay alongside
|
||||
// the HUD on app start when the startup-surfaces mode is
|
||||
// `.diagnostic`. Runs after `runBootHealth` so the first
|
||||
// probe sweep has populated state before either panel
|
||||
// reads it. Skipped during the first-launch wizard so the
|
||||
// two panels don't stack on top of the wizard window.
|
||||
self?.openStartupSurfacesIfDiagnostic()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2910,23 +2939,79 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var devOverlayWindow: DevOverlayWindow?
|
||||
|
||||
private func toggleDevOverlay() {
|
||||
if devOverlayWindow == nil {
|
||||
let hudCoordinator = self.hudStateCoordinator
|
||||
// HudState lives in the App target; the overlay package only
|
||||
// gets a string. Closure-captured weak reference so the
|
||||
// overlay never extends the coordinator's lifetime.
|
||||
let reader: @MainActor () -> String = { [weak hudCoordinator] in
|
||||
hudCoordinator?.currentStateForTests.rawValue ?? "—"
|
||||
}
|
||||
devOverlayWindow = DevOverlayWindow(
|
||||
emitter: devSnapshotEmitter,
|
||||
bootHealthOrchestrator: bootHealthOrchestrator,
|
||||
hudStateReader: reader
|
||||
)
|
||||
}
|
||||
ensureDevOverlayWindow(initialTab: 0)
|
||||
devOverlayWindow?.toggle()
|
||||
}
|
||||
|
||||
/// Idempotent constructor for the lazy `DevOverlayWindow`. Returns the
|
||||
/// existing instance on subsequent calls; `initialTab` is honored only
|
||||
/// on the first call (later calls are ignored — SwiftUI's TabView
|
||||
/// `selection` is `@State`-driven after first render).
|
||||
private func ensureDevOverlayWindow(initialTab: Int) {
|
||||
guard devOverlayWindow == nil else { return }
|
||||
let hudCoordinator = self.hudStateCoordinator
|
||||
// HudState lives in the App target; the overlay package only
|
||||
// gets a string. Closure-captured weak reference so the
|
||||
// overlay never extends the coordinator's lifetime.
|
||||
let reader: @MainActor () -> String = { [weak hudCoordinator] in
|
||||
hudCoordinator?.currentStateForTests.rawValue ?? "—"
|
||||
}
|
||||
devOverlayWindow = DevOverlayWindow(
|
||||
emitter: devSnapshotEmitter,
|
||||
bootHealthOrchestrator: bootHealthOrchestrator,
|
||||
hudStateReader: reader,
|
||||
initialTab: initialTab
|
||||
)
|
||||
}
|
||||
|
||||
/// Issue #89 — open Status panel + DevOverlay alongside the HUD on app
|
||||
/// start when `startupSurfacesMode == .diagnostic`. Called from the
|
||||
/// `bootHealthTask` tail so both panels read settled state. Skipped:
|
||||
/// - When `.minimal` (Release-build default).
|
||||
/// - When the onboarding wizard is open — the wizard is a blocking
|
||||
/// surface and stacking two panels behind it would be noisy.
|
||||
/// - When running under XCTest — panels would pop during unit-test
|
||||
/// hosts, breaking timing-sensitive tests.
|
||||
///
|
||||
/// Positioning: HUD already centers itself; Status panel goes to the
|
||||
/// top-right of the main screen's visible frame and DevOverlay goes
|
||||
/// bottom-right so the three surfaces don't overlap on a 1440×900
|
||||
/// display. The DevOverlay opens to the Logs tab (index 3) so the
|
||||
/// operator sees activity stream immediately.
|
||||
@MainActor
|
||||
private func openStartupSurfacesIfDiagnostic() {
|
||||
guard startupSurfacesMode == .diagnostic else { return }
|
||||
if startupWizardWillOpen { return }
|
||||
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { return }
|
||||
|
||||
// Status panel — top-right.
|
||||
openStatusPanel()
|
||||
if let screenFrame = NSScreen.main?.visibleFrame, let panel = statusPanel {
|
||||
let inset: CGFloat = 16
|
||||
let size = panel.frame.size
|
||||
let origin = NSPoint(
|
||||
x: screenFrame.maxX - size.width - inset,
|
||||
y: screenFrame.maxY - size.height - inset
|
||||
)
|
||||
panel.setFrameOrigin(origin)
|
||||
}
|
||||
|
||||
// DevOverlay — bottom-right, opened to Logs tab.
|
||||
ensureDevOverlayWindow(initialTab: 3)
|
||||
devOverlayWindow?.show()
|
||||
if let screenFrame = NSScreen.main?.visibleFrame, let window = devOverlayWindow {
|
||||
let inset: CGFloat = 16
|
||||
let size = window.frameSize
|
||||
let origin = NSPoint(
|
||||
x: screenFrame.maxX - size.width - inset,
|
||||
y: screenFrame.minY + inset
|
||||
)
|
||||
window.setFrameOrigin(origin)
|
||||
}
|
||||
|
||||
systemLogger?.info("openStartupSurfacesIfDiagnostic: opened Status + DevOverlay (mode=diagnostic)")
|
||||
}
|
||||
|
||||
/// Copies a debug state snapshot to the system clipboard. Writes ONLY
|
||||
/// boolean presence indicators (`apiKeyStored`, `inputMonitoringGranted`,
|
||||
/// `hotkeyBound`). The API key VALUE is never fetched into a local
|
||||
|
||||
@@ -40,12 +40,19 @@ public struct DevOverlayView: View {
|
||||
// pattern for read-only observation of an injected model.
|
||||
public let viewModel: DevOverlayViewModel
|
||||
|
||||
public init(viewModel: DevOverlayViewModel) {
|
||||
/// Initial selected tab. Tag indices match the `.tag(_:)` calls below:
|
||||
/// 0 = Agent, 1 = Tools, 2 = Health, 3 = Logs. Issue #89 / v0.1 startup
|
||||
/// flow opens straight to Logs so the operator sees activity stream on
|
||||
/// first launch instead of a blank Agent tab.
|
||||
@State private var selection: Int
|
||||
|
||||
public init(viewModel: DevOverlayViewModel, initialTab: Int = 0) {
|
||||
self.viewModel = viewModel
|
||||
_selection = State(initialValue: initialTab)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
TabView {
|
||||
TabView(selection: $selection) {
|
||||
AgentPane(viewModel: viewModel)
|
||||
.tabItem { Label("Agent", systemImage: "brain") }
|
||||
.tag(0)
|
||||
|
||||
@@ -71,7 +71,8 @@ public final class DevOverlayWindow {
|
||||
emitter: DevSnapshotEmitter?,
|
||||
bootHealthOrchestrator: BootHealthOrchestrator? = nil,
|
||||
hudStateReader: (@MainActor () -> String)? = nil,
|
||||
viewModel: DevOverlayViewModel = DevOverlayViewModel()
|
||||
viewModel: DevOverlayViewModel = DevOverlayViewModel(),
|
||||
initialTab: Int = 0
|
||||
) {
|
||||
self.viewModel = viewModel
|
||||
self.bridge = DevOverlayBridge(viewModel: viewModel)
|
||||
@@ -98,7 +99,9 @@ public final class DevOverlayWindow {
|
||||
p.hidesOnDeactivate = false
|
||||
p.title = "Jarvis DevOverlay"
|
||||
p.isReleasedWhenClosed = false
|
||||
p.contentView = NSHostingView(rootView: DevOverlayView(viewModel: viewModel))
|
||||
p.contentView = NSHostingView(
|
||||
rootView: DevOverlayView(viewModel: viewModel, initialTab: initialTab)
|
||||
)
|
||||
p.orderOut(nil)
|
||||
self.panel = p
|
||||
|
||||
@@ -133,6 +136,18 @@ public final class DevOverlayWindow {
|
||||
if isVisible { hide() } else { show() }
|
||||
}
|
||||
|
||||
/// Issue #89: AppDelegate positions Status + DevOverlay non-overlapping
|
||||
/// on launch. Exposes the underlying `NSPanel.setFrameOrigin` without
|
||||
/// leaking the panel itself (KVC on the private `panel` field would
|
||||
/// work but couples callers to the property name).
|
||||
public func setFrameOrigin(_ origin: NSPoint) {
|
||||
panel.setFrameOrigin(origin)
|
||||
}
|
||||
|
||||
/// Read-only access to the panel frame size so callers can compute an
|
||||
/// inset-from-screen-edge origin without poking at private state.
|
||||
public var frameSize: NSSize { panel.frame.size }
|
||||
|
||||
// MARK: - Polling (BootHealth + HudState)
|
||||
|
||||
/// 1 Hz read loop. BootHealth is queried via `lastSnapshot()` (no
|
||||
|
||||
Reference in new issue
Block a user