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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@
<key>fileRef</key>
<string>B00000000000000000000007</string>
</dict>
<key>A00000000000000000000007</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>B00000000000000000000008</string>
</dict>
<key>B00000000000000000000001</key>
<dict>
<key>isa</key>
Expand Down Expand Up @@ -118,6 +125,17 @@
<key>sourceTree</key>
<string>&lt;group&gt;</string>
</dict>
<key>B00000000000000000000008</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>lastKnownFileType</key>
<string>sourcecode.swift</string>
<key>path</key>
<string>MonitoringStore.swift</string>
<key>sourceTree</key>
<string>&lt;group&gt;</string>
</dict>
<key>B00000000000000000000006</key>
<dict>
<key>isa</key>
Expand Down Expand Up @@ -156,6 +174,7 @@
<string>A00000000000000000000004</string>
<string>A00000000000000000000005</string>
<string>A00000000000000000000006</string>
<string>A00000000000000000000007</string>
</array>
<key>runOnlyForDeploymentPostprocessing</key>
<string>0</string>
Expand Down Expand Up @@ -221,6 +240,7 @@
<string>B00000000000000000000004</string>
<string>B00000000000000000000005</string>
<string>B00000000000000000000007</string>
<string>B00000000000000000000008</string>
</array>
<key>path</key>
<string>AetherflowControlCenter</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,8 @@ private struct DetailColumn: View {
private var headerTitle: String {
switch selectedSection {
case .sessions:
return monitoring.selectedDetail?.session.workRef.nonEmptyValue
return monitoring.selectedDetail?.agent.taskTitle.nonEmptyValue
?? monitoring.selectedDetail?.session.workRef.nonEmptyValue
?? monitoring.selectedDetail?.workloadID
?? "Live session monitor"
case .queue:
Expand All @@ -466,9 +467,12 @@ private struct DetailColumn: View {
private var headerSummary: String {
switch selectedSection {
case .sessions:
return monitoring.selectedDetail?.session.sessionID.nonEmptyValue.map {
"Showing live status, tool activity, and recent events for \($0)."
} ?? monitoring.note
if let detail = monitoring.selectedDetail {
let sessionRoute = detail.session.sessionID.nonEmptyValue ?? "pending session"
let lifecycle = detail.agent.lifecycleState.nonEmptyValue ?? detail.session.status.nonEmptyValue ?? "unknown"
return "Showing live status, tool activity, and recent events for \(sessionRoute) in \(lifecycle) state."
}
return monitoring.note
case .queue:
return monitoring.queue.isEmpty ? monitoring.note : "The queue lane mirrors the daemon's waiting work without leaving the monitoring shell."
case .overview, .diagnostics:
Expand Down Expand Up @@ -519,17 +523,19 @@ private struct DetailHighlightsPanel: View {
switch section {
case .sessions:
if let detail = monitoring.selectedDetail {
let pidLabel = detail.agent.pid > 0 ? String(detail.agent.pid) : "pending"
return [
"Selected workload: \(detail.workloadID).",
"Selected workload: \(detail.workloadID) on pid \(pidLabel).",
"Session route: \(detail.session.serverRef.nonEmptyValue ?? "pending server") / \(detail.session.sessionID.nonEmptyValue ?? "pending session").",
"Task lane: \(detail.agent.taskID.nonEmptyValue ?? detail.session.workRef.nonEmptyValue ?? "manual spawn"). Lifecycle: \(detail.agent.lifecycleState.nonEmptyValue ?? detail.agent.state.nonEmptyValue ?? "unknown").",
"Recent tool calls: \(detail.toolCalls.count). Event lines cached: \(detail.eventLines.count).",
detail.errors.isEmpty ? "No daemon-reported errors on the selected workload." : "Daemon reported \(detail.errors.count) error\(detail.errors.count == 1 ? "" : "s") for this workload."
]
}
return [
"Monitoring phase: \(monitoring.phase.rawValue).",
"Visible workloads: \(monitoring.workloads.count).",
"Selection will load session metadata, tool calls, and recent daemon events in place."
"Selection follows the same session when the daemon rekeys a workload after reconnect."
]
case .queue:
if monitoring.queue.isEmpty {
Expand Down Expand Up @@ -926,6 +932,12 @@ private struct MonitoringSummaryRow: View {
var body: some View {
HStack(spacing: 12) {
MiniChip(text: snapshot.phase.rawValue.capitalized, tone: phaseTone)
if let poolMode = snapshot.poolMode.nonEmptyValue {
MiniChip(text: "Pool \(poolMode)", tone: ShellPalette.brass)
}
if let spawnPolicy = snapshot.spawnPolicy.nonEmptyValue {
MiniChip(text: "Policy \(spawnPolicy)", tone: ShellPalette.moss)
}
MiniChip(text: "Workloads \(snapshot.workloads.count)", tone: ShellPalette.ember)
MiniChip(text: "Queue \(snapshot.queue.count)", tone: ShellPalette.moss)
Spacer(minLength: 0)
Expand Down Expand Up @@ -967,6 +979,9 @@ private struct MonitoringWorkloadRow: View {
Text(workload.kind == .poolAgent ? "POOL" : "SPAWN")
.font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundStyle(workload.kind == .poolAgent ? ShellPalette.ember : ShellPalette.moss)
Text(workload.workRef)
.font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundStyle(ShellPalette.mutedInk)
if workload.attentionNeeded {
Text("ATTN")
.font(.system(size: 10, weight: .bold, design: .monospaced))
Expand All @@ -990,7 +1005,8 @@ private struct MonitoringWorkloadRow: View {

HStack(spacing: 10) {
DetailPill(label: "role", value: workload.role)
DetailPill(label: "session", value: workload.sessionID.nonEmptyValue ?? "pending")
DetailPill(label: "session", value: workload.sessionID.nonEmptyValue ?? "claiming")
DetailPill(label: "pid", value: workload.pid > 0 ? String(workload.pid) : "pending")
DetailPill(label: "activity", value: workload.lastActivityAt.map(Self.relativeTimestamp) ?? "waiting")
}
}
Expand Down Expand Up @@ -1077,10 +1093,26 @@ private struct SessionsDetailPanel: View {
if let detail = snapshot.selectedDetail {
VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 12) {
DetailPill(label: "work", value: detail.session.workRef.nonEmptyValue ?? detail.workloadID)
DetailPill(label: "work", value: detail.agent.taskID.nonEmptyValue ?? detail.session.workRef.nonEmptyValue ?? detail.workloadID)
DetailPill(label: "session", value: detail.session.sessionID.nonEmptyValue ?? "pending")
DetailPill(label: "origin", value: detail.session.originType.nonEmptyValue ?? "unknown")
DetailPill(label: "status", value: detail.session.status.nonEmptyValue ?? "unknown")
DetailPill(label: "status", value: detail.agent.lifecycleState.nonEmptyValue ?? detail.session.status.nonEmptyValue ?? "unknown")
}

HStack(spacing: 12) {
DetailPill(label: "agent", value: detail.workloadID)
DetailPill(label: "pid", value: detail.agent.pid > 0 ? String(detail.agent.pid) : "pending")
DetailPill(label: "server", value: detail.session.serverRef.nonEmptyValue ?? "pending")
DetailPill(label: "attach", value: detail.session.attachable ? "ready" : "pending")
}

if let lastLog = detail.agent.lastLog.nonEmptyValue {
VStack(alignment: .leading, spacing: 8) {
Text("Latest daemon note")
.font(.system(size: 12, weight: .bold, design: .monospaced))
.foregroundStyle(ShellPalette.mutedInk)
EventLineRow(line: lastLog, tone: ShellPalette.brass)
}
}

if !detail.toolCalls.isEmpty {
Expand Down Expand Up @@ -1122,8 +1154,10 @@ private struct SessionsDetailPanel: View {
}
} else {
MonitoringEmptyRow(
title: snapshot.workloads.isEmpty ? "No session selected" : "Select a workload",
detail: snapshot.note
title: snapshot.workloads.isEmpty ? "No session selected" : "Select a session lane",
detail: snapshot.workloads.isEmpty
? snapshot.note
: "The detail pane will hold session route, tool activity, and recent daemon events without changing selection."
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ struct MonitoringWorkloadSummary: Identifiable, Equatable, Sendable {
let id: String
let kind: MonitoringWorkloadKind
let role: String
let workRef: String
let title: String
let subtitle: String
let sessionID: String
let lifecycleState: String
let pid: Int
let attentionNeeded: Bool
let spawnedAt: Date
let lastActivityAt: Date?
}

Expand All @@ -34,6 +37,7 @@ struct MonitoringQueueItem: Identifiable, Equatable, Sendable {
struct MonitoringSelectionDetail: Equatable, Sendable {
let workloadID: String
let session: DaemonSessionMetadataPayload
let agent: DaemonAgentStatusPayload
let toolCalls: [DaemonToolCallPayload]
let eventLines: [String]
let lastEventTimestamp: Int64
Expand Down Expand Up @@ -74,6 +78,11 @@ struct MonitoringSnapshot: Equatable, Sendable {
final class MonitoringStore: ObservableObject {
@Published private(set) var snapshot: MonitoringSnapshot

private struct SelectionAnchor: Equatable {
let workloadID: String
let sessionID: String?
}

private let context: ShellBootstrapContext
private let controller: DaemonControlling
private let isDaemonAbsent: (Error) -> Bool
Expand All @@ -82,6 +91,7 @@ final class MonitoringStore: ObservableObject {
private var monitorTask: Task<Void, Never>?
private var hasConnected = false
private var needsAuthoritativeReload = true
private var selectionAnchor: SelectionAnchor?

init(
context: ShellBootstrapContext,
Expand Down Expand Up @@ -132,6 +142,12 @@ final class MonitoringStore: ObservableObject {
return
}
needsAuthoritativeReload = true
selectionAnchor = id.flatMap { workloadID in
guard let workload = snapshot.workloads.first(where: { $0.id == workloadID }) else {
return SelectionAnchor(workloadID: workloadID, sessionID: nil)
}
return SelectionAnchor(workloadID: workloadID, sessionID: workload.sessionID.nonEmptyValue)
}
snapshot = MonitoringSnapshot(
phase: snapshot.phase,
project: snapshot.project,
Expand All @@ -150,8 +166,8 @@ final class MonitoringStore: ObservableObject {
func refresh() async {
do {
let status = try await controller.fetchStatus(daemonURL: context.daemonURL)
let workloads = buildWorkloads(status: status)
let selectedWorkloadID = resolvedSelectionID(workloads: workloads, preferredID: snapshot.selectedWorkloadID)
let workloads = orderedWorkloads(incoming: buildWorkloads(status: status), previous: snapshot.workloads)
let selectedWorkloadID = resolvedSelectionID(workloads: workloads)
var selectedDetail = snapshot.selectedDetail

let reconnected = needsAuthoritativeReload || snapshot.phase != .connected
Expand All @@ -177,6 +193,12 @@ final class MonitoringStore: ObservableObject {
lastError: status.errors.last?.nonEmptyValue,
updatedAt: .now
)
selectionAnchor = selectedWorkloadID.flatMap { workloadID in
guard let workload = workloads.first(where: { $0.id == workloadID }) else {
return SelectionAnchor(workloadID: workloadID, sessionID: selectedDetail?.session.sessionID.nonEmptyValue)
}
return SelectionAnchor(workloadID: workloadID, sessionID: workload.sessionID.nonEmptyValue)
}
hasConnected = true
needsAuthoritativeReload = false
} catch {
Expand All @@ -201,19 +223,13 @@ final class MonitoringStore: ObservableObject {
afterTimestamp: afterTimestamp
)

let lines: [String]
let lastEventTimestamp: Int64
if previous == nil {
lines = events.lines
lastEventTimestamp = events.lastTS
} else {
lines = previous!.eventLines + events.lines
lastEventTimestamp = max(previous!.lastEventTimestamp, events.lastTS)
}
let lines = (previous?.eventLines ?? []) + events.lines
let lastEventTimestamp = max(previous?.lastEventTimestamp ?? 0, events.lastTS)

return MonitoringSelectionDetail(
workloadID: workloadID,
session: events.session.sessionID.nonEmptyValue == nil ? detail.session : events.session,
agent: detail.agent,
toolCalls: detail.toolCalls,
eventLines: lines,
lastEventTimestamp: lastEventTimestamp,
Expand Down Expand Up @@ -247,11 +263,14 @@ final class MonitoringStore: ObservableObject {
id: $0.id,
kind: .poolAgent,
role: $0.role,
workRef: $0.taskID.nonEmptyValue ?? $0.id,
title: $0.taskTitle.nonEmptyValue ?? $0.id,
subtitle: $0.lastLog.nonEmptyValue ?? $0.taskID.nonEmptyValue ?? "Pool agent",
sessionID: $0.sessionID,
lifecycleState: $0.lifecycleState.nonEmptyValue ?? $0.state,
pid: $0.pid,
attentionNeeded: $0.attentionNeeded,
spawnedAt: $0.spawnTime,
lastActivityAt: $0.lastActivityAt
)
}
Expand All @@ -260,25 +279,62 @@ final class MonitoringStore: ObservableObject {
id: $0.spawnID,
kind: .spawn,
role: "spawn",
workRef: $0.spawnID,
title: $0.prompt.nonEmptyValue ?? $0.spawnID,
subtitle: $0.state.nonEmptyValue ?? "Spawned session",
subtitle: $0.sessionID.nonEmptyValue ?? ($0.state.nonEmptyValue ?? "Spawned session"),
sessionID: $0.sessionID,
lifecycleState: $0.lifecycleState.nonEmptyValue ?? $0.state,
pid: $0.pid,
attentionNeeded: $0.attentionNeeded,
spawnedAt: $0.spawnTime,
lastActivityAt: $0.lastActivityAt ?? $0.exitedAt
)
}
return agents + spawns
}

private func resolvedSelectionID(
workloads: [MonitoringWorkloadSummary],
preferredID: String?
) -> String? {
if let preferredID, workloads.contains(where: { $0.id == preferredID }) {
return preferredID
private func orderedWorkloads(
incoming: [MonitoringWorkloadSummary],
previous: [MonitoringWorkloadSummary]
) -> [MonitoringWorkloadSummary] {
let incomingByID = Dictionary(uniqueKeysWithValues: incoming.map { ($0.id, $0) })
let retained = previous.compactMap { incomingByID[$0.id] }
let retainedIDs = Set(retained.map(\.id))
let appended = incoming
.filter { !retainedIDs.contains($0.id) }
.sorted(by: compareNewWorkloads)
return retained + appended
}

private func compareNewWorkloads(
_ lhs: MonitoringWorkloadSummary,
_ rhs: MonitoringWorkloadSummary
) -> Bool {
if lhs.attentionNeeded != rhs.attentionNeeded {
return lhs.attentionNeeded && !rhs.attentionNeeded
}
if lhs.kind != rhs.kind {
return lhs.kind == .poolAgent
}
if lhs.spawnedAt != rhs.spawnedAt {
return lhs.spawnedAt > rhs.spawnedAt
}
return lhs.id.localizedStandardCompare(rhs.id) == .orderedAscending
}

private func resolvedSelectionID(workloads: [MonitoringWorkloadSummary]) -> String? {
if let sessionID = selectionAnchor?.sessionID,
let match = workloads.first(where: { $0.sessionID == sessionID }) {
return match.id
}
if let workloadID = selectionAnchor?.workloadID,
workloads.contains(where: { $0.id == workloadID }) {
return workloadID
}
guard let preferred = workloads.first else {
return nil
}
return workloads.first?.id
return preferred.id
}

private func monitoringNote(for status: DaemonStatusPayload, workloadCount: Int) -> String {
Expand Down
Loading
Loading