diff --git a/NucleicRemote/Shared/SessionActivityAttributes.swift b/NucleicRemote/Shared/SessionActivityAttributes.swift index 467eed3..0d29361 100644 --- a/NucleicRemote/Shared/SessionActivityAttributes.swift +++ b/NucleicRemote/Shared/SessionActivityAttributes.swift @@ -2,19 +2,74 @@ import ActivityKit import Foundation /// The one aggregate Live Activity (UX_IOS §5.3, aggregate per §11.4 — a single Activity -/// respects iOS's budget better than one per session): "N running, M waiting" plus the most -/// urgent session's title. Shared between the app (which starts/updates it) and the widget -/// extension (which renders it). +/// respects iOS's budget better than one per session). It answers "what is Nucleic doing right +/// now?" at a glance: the headline counts (running / waiting / approvals), the total churn in +/// flight, and a few of the most urgent sessions with their state and diff. Shared between the +/// app (which starts/updates it) and the widget extension (which renders it). +/// +/// Deliberately free of `NucleicProtocol` types — the widget extension links only this file, so +/// the state carries plain value types and its own small `Backend`/`Kind` enums rather than the +/// wire `BackendID`/`SessionStatus`. struct NucleicSessionAttributes: ActivityAttributes { + /// The agent behind a session — drives a row's provider tag/tint. + enum Backend: String, Codable, Hashable { + case claude, codex, grok, other + + var label: String { + switch self { + case .claude: "Claude" + case .codex: "Codex" + case .grok: "Grok" + case .other: "Agent" + } + } + } + + /// A session's live state, collapsed to what the glance needs — drives each row's glyph/tint. + enum Kind: String, Codable, Hashable { + case running // agent actively working a turn + case provisioning // worktree/setup spinning up + case approval // blocked on a human approval decision (loudest) + case needsInput // turn ended asking the user something + case done // turn finished its work; nothing required + case error // errored / interrupted + case idle // created, not yet working + } + + /// One agent session surfaced in the activity's detail rows. + struct SessionLine: Codable, Hashable, Identifiable { + var id: String + /// The session's title (falls back to the project name upstream). + var title: String + /// Project the session belongs to. + var project: String + var backend: Backend + var kind: Kind + /// A compact right-aligned status: its diff ("3 files +42 −7"), "2 to approve", + /// "Waiting on you", "Working…", etc. + var detail: String + } + struct ContentState: Codable, Hashable { /// Sessions actively working a turn. var runningCount: Int /// Sessions blocked on the user (approval or next prompt) — the loud number. var needsYouCount: Int - /// The most urgent session's title: the top waiting one, else the busiest running. - var topTitle: String? - /// Whether `topTitle` is waiting on the user (amber) vs just running (blue). - var topNeedsYou: Bool + /// Total individual approval decisions waiting across all sessions — the "act now" count. + var approvalCount: Int + /// Aggregate worktree churn across the active sessions. + var filesChanged: Int + var linesAdded: Int + var linesRemoved: Int + /// The most urgent sessions, attention-first (capped, typically ≤3): the detail rows. + var lines: [SessionLine] + + /// The single most urgent session (drives the compact/minimal regions). + var top: SessionLine? { lines.first } + /// Whether anything needs the user (waiting session or a pending approval). + var needsAttention: Bool { needsYouCount > 0 || approvalCount > 0 } + /// Whether any worktree has uncommitted changes to summarize. + var hasChurn: Bool { filesChanged > 0 } } /// The paired Mac's name (static for the Activity's lifetime).