Live Activity Content Density
Nucleic-Session: B3643133-163C-4C5D-BE20-1A6B7FB83DA9 Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -11,6 +11,10 @@ final class LiveActivityManager {
|
||||
static let shared = LiveActivityManager()
|
||||
private init() {}
|
||||
|
||||
/// How many sessions the detail rows show. The glance stays a glance — the counts and churn
|
||||
/// still summarize everything, this just bounds the per-session list.
|
||||
private static let maxLines = 3
|
||||
|
||||
private var activity: Activity<NucleicSessionAttributes>?
|
||||
|
||||
/// Reconcile the Activity with the current session set.
|
||||
@@ -25,14 +29,33 @@ final class LiveActivityManager {
|
||||
return
|
||||
}
|
||||
|
||||
// The most urgent session headlines: the top waiter, else the freshest runner.
|
||||
let top = needsYou.max { $0.updatedAt < $1.updatedAt }
|
||||
?? running.max { $0.updatedAt < $1.updatedAt }
|
||||
// Everything in flight or waiting on the user, attention-first (approvals, then waiting
|
||||
// input, then running), freshest within a rank. This is both the detail-row source and
|
||||
// the set the aggregate churn/approval totals sum over.
|
||||
let active = live
|
||||
.filter {
|
||||
$0.status == .running || $0.status == .provisioning
|
||||
|| $0.status.needsYou($0.disposition)
|
||||
}
|
||||
.sorted { lhs, rhs in
|
||||
let l = StatusStyle.sortRank(lhs), r = StatusStyle.sortRank(rhs)
|
||||
return l == r ? lhs.updatedAt > rhs.updatedAt : l < r
|
||||
}
|
||||
|
||||
let approvals = active.reduce(0) { $0 + $1.pendingApprovalCount }
|
||||
let files = active.reduce(0) { $0 + ($1.diffStat?.filesChanged ?? 0) }
|
||||
let added = active.reduce(0) { $0 + ($1.diffStat?.added ?? 0) }
|
||||
let removed = active.reduce(0) { $0 + ($1.diffStat?.removed ?? 0) }
|
||||
let lines = active.prefix(Self.maxLines).map(Self.line(for:))
|
||||
|
||||
let state = NucleicSessionAttributes.ContentState(
|
||||
runningCount: running.count,
|
||||
needsYouCount: needsYou.count,
|
||||
topTitle: top?.title,
|
||||
topNeedsYou: top.map { $0.status.needsYou($0.disposition) } ?? false)
|
||||
approvalCount: approvals,
|
||||
filesChanged: files,
|
||||
linesAdded: added,
|
||||
linesRemoved: removed,
|
||||
lines: Array(lines))
|
||||
|
||||
if let activity {
|
||||
Task { await activity.update(ActivityContent(state: state, staleDate: nil)) }
|
||||
@@ -59,4 +82,58 @@ final class LiveActivityManager {
|
||||
dismissalPolicy: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Projection
|
||||
|
||||
/// Project a wire summary into the widget's self-contained row model.
|
||||
private static func line(for s: WireSessionSummary) -> NucleicSessionAttributes.SessionLine {
|
||||
NucleicSessionAttributes.SessionLine(
|
||||
id: s.sessionID.rawValue,
|
||||
title: s.title.isEmpty ? s.projectName : s.title,
|
||||
project: s.projectName,
|
||||
backend: backend(s.backend),
|
||||
kind: kind(for: s),
|
||||
detail: detail(for: s))
|
||||
}
|
||||
|
||||
private static func kind(for s: WireSessionSummary) -> NucleicSessionAttributes.Kind {
|
||||
switch s.status {
|
||||
case .awaitingApproval: .approval
|
||||
case .running: .running
|
||||
case .provisioning: .provisioning
|
||||
case .awaitingInput: s.disposition == .completed ? .done : .needsInput
|
||||
case .idle: .idle
|
||||
case .finished: .done
|
||||
case .interrupted, .error: .error
|
||||
}
|
||||
}
|
||||
|
||||
private static func backend(_ id: BackendID) -> NucleicSessionAttributes.Backend {
|
||||
switch id {
|
||||
case .claudeCode: .claude
|
||||
case .codex, .codexExec: .codex
|
||||
case .grok: .grok
|
||||
}
|
||||
}
|
||||
|
||||
/// The compact right-aligned status for a row: the actionable ask wins (approvals, then
|
||||
/// "waiting on you"), else the worktree churn, else what the agent is up to.
|
||||
private static func detail(for s: WireSessionSummary) -> String {
|
||||
if s.status == .awaitingApproval || s.pendingApprovalCount > 0 {
|
||||
let n = max(s.pendingApprovalCount, 1)
|
||||
return "\(n) to approve"
|
||||
}
|
||||
if s.status == .awaitingInput, s.disposition != .completed {
|
||||
return "Waiting on you"
|
||||
}
|
||||
if let d = s.diffStat, d.filesChanged > 0 {
|
||||
return "\(d.filesChanged) file\(d.filesChanged == 1 ? "" : "s") +\(d.added) −\(d.removed)"
|
||||
}
|
||||
switch s.status {
|
||||
case .provisioning: return "Starting…"
|
||||
case .running: return "Working…"
|
||||
case .awaitingInput: return "Done"
|
||||
default: return StatusStyle.label(s.status, disposition: s.disposition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user