Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Expected behaviors and their verification status. This is the project's oracle
| Status | Agent detail shows opencode session ID in TUI meta pane | Manual: TUI agent detail view | manual | 2026-02-10 |
| Control Center | Manual daemon targeting defaults to the single global endpoint without project hashing | `cd macos/ControlCenter && swift test --filter ShellBootstrapTests` | covered | 2026-03-14 |
| Control Center | App-triggered daemon start uses manual mode with an aligned listen address | `cd macos/ControlCenter && swift test --filter DaemonControlTests` | covered | 2026-03-14 |
| Control Center | Monitoring reconnect forces an authoritative detail/event reload before resuming cursor-based event fetches | `cd macos/ControlCenter && swift test --filter MonitoringStoreTests/testReconnectForcesAuthoritativeEventReloadBeforeCursorResumes` | covered | 2026-03-15 |
| Control Center | Selected session detail stays visible with retained output after the workload leaves live status | `cd macos/ControlCenter && swift test --filter MonitoringStoreTests/testSelectedDetailIsRetainedWhenWorkloadLeavesLiveStatus` | covered | 2026-03-15 |
| Control Center | Menu bar session shortcuts deep-link the app to the correct session detail and surface handoff-unavailable copy | `cd macos/ControlCenter && swift test --filter MonitoringStoreTests` | covered | 2026-03-15 |
| Agent Control | af kill <agent> sends SIGTERM and validates agent state | `go test ./internal/daemon/... -run TestHandleAgentKill` | covered | 2026-02-10 |
| Agent Control | Kill rejects invalid PIDs (0 or negative) | `go test ./internal/daemon/... -run TestHandleAgentKillInvalidPID` | covered | 2026-02-10 |
| Agent Control | Kill rejects non-running agents | `go test ./internal/daemon/... -run TestHandleAgentKillNonRunningAgent` | covered | 2026-02-10 |
67 changes: 67 additions & 0 deletions docs/solutions/handoffs/macos-monitoring-diagnostics-20260315.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# macOS Monitoring Diagnostics

**Date**: 2026-03-15
**Task**: ts-9fb41d

## Context

This ticket was to make the reduced macOS Control Center trustworthy under daemon churn by adding diagnostics and regression coverage for connect, reconnect, start/stop, deep-link, handoff, and daemon-targeting behavior. When resumed after the manual-daemon follow-up work, the branch had already picked up reconnect handling, retained terminal detail, lifecycle diagnostics, and daemon-target mismatch coverage from `main`, but the required menu bar deep-link and explicit handoff-unavailable surfaces were still missing.

## What Was Done

1. Added monitoring-model surfaces in `macos/ControlCenter/Sources/AetherflowControlCenter/MonitoringStore.swift`
- `menuBarSessionShortcuts` exposes the leading workloads in the same order the sessions lane uses
- `handoffUnavailableCopy` turns `attachable=false` into explicit operator-facing copy
- `activateMenuBarSessionDeepLink(...)` centralizes the session-lane selection behavior

2. Updated the UI in `macos/ControlCenter/Sources/AetherflowControlCenter/ControlCenterRootView.swift`
- menu bar now renders up to three session shortcuts
- deep-link actions select the workload, switch to the sessions lane, and force an immediate refresh
- session detail now renders an `Opencode handoff unavailable` card when the daemon marks the session unattached

3. Added regression coverage in `macos/ControlCenter/Tests/AetherflowControlCenterTests/MonitoringStoreTests.swift`
- menu bar shortcuts expose the leading ordered workloads
- menu bar deep-link selects the correct session detail
- handoff-unavailable copy is surfaced when `attachable` is false

4. Updated `MATRIX.md`
- reconnect reload behavior
- retained terminal detail behavior
- menu bar deep-link and handoff-unavailable behavior

## What Was Tried That Didn't Work

The first patch attempt failed because the selection-detail type was mislocated in memory: `MonitoringSelectionDetail` lives in `MonitoringStore.swift`, not `ShellModels.swift`. Splitting the patch by file fixed that quickly.

The original assumption that menu bar deep links already existed was wrong relative to current `main`. Verifying the actual view tree showed only a generic `Focus Sessions` button, so the missing work had to be implemented rather than just checked off.

During live smoke verification, an isolated daemon on `127.0.0.1:7199` exposed a separate CLI endpoint-resolution inconsistency: explicit daemon URL overrides did not resolve consistently across `start`, `status`, and `stop`. That was split into `ts-3db986` so it would not block closing this app-focused task.

## Key Decisions

- Keep the deep-link logic in the monitoring model/store layer so menu bar behavior stays aligned with the sessions lane selection model.
- Treat handoff unavailability as a first-class diagnostic message, not just an implied boolean in a fact card.
- Trigger an immediate refresh on menu bar deep-link to avoid a blank detail pane while waiting for the next poll interval.

## Files Modified

- `macos/ControlCenter/Sources/AetherflowControlCenter/MonitoringStore.swift`
- `macos/ControlCenter/Sources/AetherflowControlCenter/ControlCenterRootView.swift`
- `macos/ControlCenter/Tests/AetherflowControlCenterTests/MonitoringStoreTests.swift`
- `MATRIX.md`
- `docs/solutions/logic-errors/macos-monitoring-diagnostics-regression-20260315.md`
- `docs/solutions/learnings.md`
- `docs/solutions/handoffs/macos-monitoring-diagnostics-20260315.md`

## Verification

- `cd macos/ControlCenter && swift test`
- `go test ./...`
- `go build ./...`
- `golangci-lint run`
- `git diff --check`
- Live smoke:
- `go build -o /tmp/aetherflow-af ./cmd/af`
- `/tmp/aetherflow-af daemon start --spawn-policy manual --listen-addr 127.0.0.1:7199 --detach`
- `AETHERFLOW_DAEMON_URL=http://127.0.0.1:7199 /tmp/aetherflow-af status`
- `cd macos/ControlCenter && AETHERFLOW_CLI_PATH=/tmp/aetherflow-af AETHERFLOW_DAEMON_URL=http://127.0.0.1:7199 swift run AetherflowControlCenter`
4 changes: 4 additions & 0 deletions docs/solutions/learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ Manual mode can be global by default without making `--project` inert. In this C
### macos-control-center: align daemon start with the resolved probe target

Changing the app’s default daemon URL is not enough on its own. If the app can also start the daemon, the start command must carry the same resolved endpoint or the app can still boot one daemon and probe another. Passing the normalized listen address through the start path keeps manual-mode bootstrap coherent.

### monitoring-ui: menu bar deep links need an immediate refresh

Selecting a workload from the menu bar is not enough if the monitoring store only polls on an interval. The selection changes immediately, but the detail pane can stay blank until the next refresh. When a menu bar action is supposed to open a specific session, trigger an explicit refresh after updating selection so the operator lands on populated detail instead of a transient empty state.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
module: macos-control-center
date: 2026-03-15
problem_type: logic_error
component: monitoring
symptoms:
- "the menu bar could only focus the sessions lane, not the specific session the operator intended to inspect"
- "a selected session with attachable=false only showed `Attach: Pending`, leaving handoff failures implicit"
- "the ticket claimed menu bar deep links existed, but current main only exposed generic session focus controls"
root_cause: the monitoring model exposed workload ordering and retained detail state, but not first-class menu bar shortcuts or explicit handoff-unavailable messaging
resolution_type: code_fix
severity: medium
tags:
- macos
- control-center
- monitoring
- diagnostics
- menu-bar
- handoff
---

# macOS Monitoring Diagnostics Regression Coverage

## Problem

`ts-9fb41d` was meant to make daemon lifecycle failures diagnosable in the reduced macOS Control Center. When resumed on current `main`, the core reconnect and retained-detail work was already present, but two required operator paths were still missing:

1. The menu bar could only open the sessions lane generically, not deep-link to the intended workload.
2. Session detail did not explicitly explain when opencode handoff was unavailable.

That meant the task's required deep-link and handoff diagnostics were not actually complete.

## What Didn't Work

The first pass assumed those behaviors already existed because earlier work notes said they did. Verifying against the current tree showed the opposite: the menu bar only rendered `Focus Sessions`, and the session detail pane only exposed `Attach: Pending`. The missing state had to be added in the monitoring model before the view layer could render it coherently.

## Solution

1. Add `menuBarSessionShortcuts` to the monitoring snapshot so the menu bar can render the leading workloads directly from the same ordered model the sessions lane uses.
2. Add `handoffUnavailableCopy` to `MonitoringSelectionDetail` so attachability failure becomes explicit operator-facing copy instead of an implied fact-card state.
3. Route menu bar deep links through a single helper that selects the sessions lane and workload together, then trigger an immediate refresh so the detail pane does not sit empty until the next poll.
4. Add regression tests for shortcut ordering, deep-link selection, reconnect reload, retained terminal detail, and handoff-unavailable copy.

## Prevention

- Treat ticket claims as untrusted until verified against the current branch state.
- Keep menu bar actions backed by monitoring-model state rather than hand-built view logic so selection order stays consistent.
- When a daemon contract field like `attachable` affects operator actionability, surface explicit copy instead of forcing the user to infer meaning from a boolean-shaped label.
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ struct MenuBarControlCenterView: View {
@EnvironmentObject private var transportStore: TransportStore
@EnvironmentObject private var lifecycleStore: DaemonLifecycleStore
@EnvironmentObject private var navigationStore: NavigationStore
@EnvironmentObject private var monitoringStore: MonitoringStore

let windowID: String

Expand Down Expand Up @@ -690,6 +691,40 @@ struct MenuBarControlCenterView: View {
}
.buttonStyle(.bordered)

if !monitoringStore.snapshot.menuBarSessionShortcuts.isEmpty {
Divider()

VStack(alignment: .leading, spacing: 8) {
Text("Jump To Session")
.font(.system(size: 11, weight: .bold, design: .monospaced))
.foregroundStyle(ShellPalette.mutedInk)

ForEach(monitoringStore.snapshot.menuBarSessionShortcuts) { shortcut in
Button {
activateMenuBarSessionDeepLink(
workloadID: shortcut.id,
navigationStore: navigationStore,
monitoringStore: monitoringStore
)
Task {
await monitoringStore.refresh()
}
NSApplication.shared.activate(ignoringOtherApps: true)
openWindow(id: windowID)
} label: {
VStack(alignment: .leading, spacing: 3) {
Text(shortcut.title)
Text("\(shortcut.sessionID) • \(shortcut.subtitle)")
.font(.system(size: 11, weight: .medium, design: .monospaced))
.foregroundStyle(ShellPalette.mutedInk)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.buttonStyle(.bordered)
}
}
}

HStack(spacing: 10) {
Button("Start") {
lifecycleStore.requestStart()
Expand Down Expand Up @@ -1163,6 +1198,10 @@ private struct SessionsDetailPanel: View {
SessionFactCard(label: "Updated", value: monitoringTimestampLabel(detail.session.updatedAt))
}

if let handoffUnavailableCopy = detail.handoffUnavailableCopy {
HandoffUnavailableCard(copy: handoffUnavailableCopy)
}

if let lastLog = detail.agent.lastLog.nonEmptyValue {
VStack(alignment: .leading, spacing: 8) {
Text("Latest daemon note")
Expand Down Expand Up @@ -1224,6 +1263,31 @@ private struct SessionsDetailPanel: View {
}
}

private struct HandoffUnavailableCard: View {
let copy: String

var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Opencode handoff unavailable")
.font(.system(size: 13, weight: .bold, design: .rounded))
.foregroundStyle(ShellPalette.ink)
Text(copy)
.font(.system(size: 12, weight: .medium, design: .rounded))
.foregroundStyle(ShellPalette.mutedInk)
.fixedSize(horizontal: false, vertical: true)
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: 18)
.fill(Color.white.opacity(0.28))
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(ShellPalette.ember.opacity(0.45), lineWidth: 1)
)
)
}
}

private struct SessionFactCard: View {
let label: String
let value: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ struct MonitoringWorkloadSummary: Identifiable, Equatable, Sendable {
let lastActivityAt: Date?
}

struct MenuBarSessionShortcut: Identifiable, Equatable, Sendable {
let id: String
let title: String
let subtitle: String
let sessionID: String
}

struct MonitoringQueueItem: Identifiable, Equatable, Sendable {
let id: String
let priority: Int
Expand Down Expand Up @@ -64,6 +71,13 @@ struct MonitoringSelectionDetail: Equatable, Sendable {
)
}

var handoffUnavailableCopy: String? {
guard !session.attachable else {
return nil
}
return "Opencode handoff is unavailable for this session. The daemon has not exposed an attachable route yet."
}

static func isTerminalLifecycle(_ value: String) -> Bool {
switch value.lowercased() {
case "complete", "completed", "crashed", "error", "errored", "exited", "failed", "idle":
Expand Down Expand Up @@ -100,6 +114,17 @@ struct MonitoringSnapshot: Equatable, Sendable {
let lastError: String?
let updatedAt: Date

var menuBarSessionShortcuts: [MenuBarSessionShortcut] {
workloads.prefix(3).map { workload in
MenuBarSessionShortcut(
id: workload.id,
title: workload.title,
subtitle: workload.workRef,
sessionID: workload.sessionID.nonEmptyValue ?? "claiming"
)
}
}

static func initial(context: ShellBootstrapContext) -> Self {
Self(
phase: .connecting,
Expand All @@ -117,6 +142,16 @@ struct MonitoringSnapshot: Equatable, Sendable {
}
}

@MainActor
func activateMenuBarSessionDeepLink(
workloadID: String,
navigationStore: NavigationStore,
monitoringStore: MonitoringStore
) {
navigationStore.select(section: .sessions)
monitoringStore.selectWorkload(id: workloadID)
}

@MainActor
final class MonitoringStore: ObservableObject {
@Published private(set) var snapshot: MonitoringSnapshot
Expand Down
Loading
Loading