Skip to content
Open
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
167 changes: 112 additions & 55 deletions apps/agentacct/Sources/agentacct/DashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftUI
struct DashboardWorkItem: Identifiable {
let id: String
let title: String
let project: String?
let client: String
let lastActivityAt: Double?
let outcome: String
Expand All @@ -21,12 +22,9 @@ struct DashboardWorkItem: Identifiable {

init(task: ReceiptSummary) {
id = task.taskId
if let taskTitle = task.title, !taskTitle.isEmpty {
title = taskTitle
} else {
title = task.taskId
}
client = task.primaryRoot?.client ?? "Unknown agent"
title = recordedTaskDisplayTitle(task.title, taskId: task.taskId)
project = Self.nonempty(task.project)
client = Self.nonempty(task.primaryRoot?.client) ?? "Unknown agent"
lastActivityAt = task.lastActivityAt
outcomeKey = task.decisionStatus.key
if let label = task.decisionStatus.label, !label.isEmpty {
Expand All @@ -44,6 +42,12 @@ struct DashboardWorkItem: Identifiable {
agoText(lastActivityAt)
}

var contextComponents: [String] {
[project, client, recency].compactMap { $0 }
}

var visibleCost: String { cost == "—" ? "unpriced" : cost }

/// Strongest evidence tier present (drives the row's tier pip).
var strongestTier: String? {
gradeable ? (strongestTierKey ?? "unchecked") : nil
Expand All @@ -61,6 +65,15 @@ struct DashboardWorkItem: Identifiable {
}
}

private static func nonempty(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty
else {
return nil
}
return trimmed
}

private static func compactCost(_ cost: ReceiptCost) -> String {
guard let value = cost.estimatedCostUsd else { return "—" }
let prefix: String
Expand Down Expand Up @@ -93,19 +106,15 @@ struct DashboardAttentionItem: Identifiable, Equatable {
init?(task: ReceiptSummary) {
guard let reason = task.attention else { return nil }
id = task.taskId
if let taskTitle = task.title, !taskTitle.isEmpty {
title = taskTitle
} else {
title = task.taskId
}
project = task.project
client = task.primaryRoot?.client
title = recordedTaskDisplayTitle(task.title, taskId: task.taskId)
project = Self.nonempty(task.project)
client = Self.nonempty(task.primaryRoot?.client)
reasonKind = reason.kind
summary = reason.summary
nextStep = reason.nextStep
summary = reason.summary.trimmingCharacters(in: .whitespacesAndNewlines)
nextStep = Self.nonempty(reason.nextStep)
observedAt = reason.observedAt
handedOff = task.handedOff
switch reason.source {
switch Self.nonempty(reason.source) {
case "mcp": sourceLabel = "MCP record"
case "client_log": sourceLabel = "Local client log"
case "machine": sourceLabel = "Machine check"
Expand All @@ -130,6 +139,15 @@ struct DashboardAttentionItem: Identifiable, Equatable {
}
}

private static func nonempty(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
else {
return nil
}
return value
}

var recency: String? { agoText(observedAt) }
}

Expand Down Expand Up @@ -236,7 +254,7 @@ enum DashboardAttentionPresentation: Equatable {
self = .loading
return
}
guard Self.hasConsistentEnvelope(payload) else {
guard hasConsistentAttentionHeadEnvelope(payload) else {
self = .inconsistent(total: max(0, payload.total))
return
}
Expand All @@ -251,36 +269,6 @@ enum DashboardAttentionPresentation: Equatable {
}
}

private static func hasConsistentEnvelope(_ payload: V1AttentionPayload) -> Bool {
let taskIDs = payload.items.map {
$0.taskId.trimmingCharacters(in: .whitespacesAndNewlines)
}
guard payload.schema == "agentacct.v1-attention.v1",
payload.total >= 0,
payload.offset == 0,
(1 ... 50).contains(payload.limit),
payload.counts.failedCheck >= 0,
payload.counts.failedStep >= 0,
payload.counts.blocker >= 0,
payload.items.count <= payload.limit,
payload.items.count <= payload.total,
taskIDs.allSatisfy({ !$0.isEmpty }),
Set(taskIDs).count == payload.items.count,
payload.items.allSatisfy({ $0.attention != nil }),
payload.total == 0 || !payload.items.isEmpty,
payload.truncated == (payload.total > payload.items.count)
else {
return false
}

let first = payload.counts.failedCheck.addingReportingOverflow(
payload.counts.failedStep
)
guard !first.overflow else { return false }
let total = first.partialValue.addingReportingOverflow(payload.counts.blocker)
return !total.overflow && total.partialValue == payload.total
}

var dashboardHeadline: String {
switch self {
case .loading: return "Checking recorded work"
Expand All @@ -294,7 +282,7 @@ enum DashboardAttentionPresentation: Equatable {
var dashboardStatus: String {
switch self {
case .loading: return "Loading review projection"
case .unavailable: return "Refresh to retry"
case .unavailable: return "Unavailable"
case .clear: return "0 review items"
case .focus(_, let total), .inconsistent(let total):
return "\(total) review item\(total == 1 ? "" : "s")"
Expand Down Expand Up @@ -800,7 +788,9 @@ struct DashboardPane: View {

RecentWorkCard(
items: recentWork,
totalCount: dashboard.totalReceiptTasks ?? dashboard.receiptTasks.count
totalCount: dashboard.totalReceiptTasks,
hasLoaded: dashboard.hasLoadedReceiptTasks,
error: dashboard.receiptListError
) { destination in
selection.open(destination)
}
Expand Down Expand Up @@ -1467,11 +1457,43 @@ private extension DashboardCardHeader where Action == EmptyView {
}
}

enum DashboardRecentWorkPresentation: Equatable {
case loading
case unavailable(String)
case empty
case populated

init(items: [DashboardWorkItem], total: Int?, hasLoaded: Bool, error: String?) {
if !items.isEmpty {
self = .populated
} else if let error {
self = .unavailable(error)
} else if !hasLoaded {
self = .loading
} else if let total, total > 0 {
self = .unavailable("The receipt count loaded, but no recent rows were returned.")
} else {
self = .empty
}
}
}

private struct RecentWorkCard: View {
let items: [DashboardWorkItem]
let totalCount: Int
let totalCount: Int?
let hasLoaded: Bool
let error: String?
let open: (DashboardDestination) -> Void

private var presentation: DashboardRecentWorkPresentation {
DashboardRecentWorkPresentation(
items: items,
total: totalCount,
hasLoaded: hasLoaded,
error: error
)
}

var body: some View {
Card(padding: 0, fillsHeight: true) {
VStack(spacing: 0) {
Expand All @@ -1485,14 +1507,43 @@ private struct RecentWorkCard: View {
}
Divider().overlay(Theme.hairline)

if items.isEmpty {
switch presentation {
case .loading:
HStack(spacing: Space.m) {
ProgressView().controlSize(.small).tint(Theme.muted)
Text("Loading recent work…")
.font(Type.body)
.foregroundStyle(Theme.muted)
}
.padding(Space.xl)
.frame(maxWidth: .infinity, minHeight: 222, alignment: .leading)
case .unavailable(let message):
DashboardEmptyState(
icon: "exclamationmark.triangle",
title: "Recent work unavailable",
message: message
)
.frame(minHeight: 222)
case .empty:
DashboardEmptyState(
icon: "checklist",
title: "No recorded work yet",
message: "Set up recording to see task outcomes and evidence here."
)
.frame(minHeight: 222)
} else {
case .populated:
if let error {
Label(
"Showing last loaded work · \(error)",
systemImage: "exclamationmark.triangle.fill"
)
.font(Type.dataSmall)
.foregroundStyle(Theme.amber)
.padding(.horizontal, Space.l)
.padding(.vertical, Space.s)
.frame(maxWidth: .infinity, alignment: .leading)
Divider().overlay(Theme.hairline)
}
workColumnLabels
Divider().overlay(Theme.hairline)
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
Expand Down Expand Up @@ -1533,7 +1584,7 @@ private struct RecentWorkRow: View {
.font(Type.rowLabel)
.foregroundStyle(Theme.ink)
.lineLimit(2)
Text([item.client, item.recency].compactMap { $0 }.joined(separator: " · "))
Text(item.contextComponents.joined(separator: " · "))
.font(Type.caption)
.foregroundStyle(Theme.muted)
.lineLimit(1)
Expand All @@ -1560,7 +1611,7 @@ private struct RecentWorkRow: View {
}
.frame(width: 118, alignment: .leading)

Text(item.cost == "—" ? "unpriced" : item.cost)
Text(item.visibleCost)
.font(Type.dataSmall)
.foregroundStyle(Theme.muted)
.frame(width: 68, alignment: .trailing)
Expand All @@ -1573,7 +1624,13 @@ private struct RecentWorkRow: View {
}
.buttonStyle(DashboardRowButtonStyle())
.accessibilityLabel(
"\(item.title), \(item.outcome), \(item.evidence), \(item.cost)"
[
item.title,
item.contextComponents.joined(separator: ", "),
item.outcome,
item.evidence,
item.visibleCost,
].joined(separator: ", ")
)
.accessibilityHint("Opens this task in Work")
.accessibilityIdentifier("dashboard.recent-work.task.\(item.id)")
Expand Down
60 changes: 59 additions & 1 deletion apps/agentacct/Sources/agentacct/DashboardSnapshotHarness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ struct DashboardSnapshotConfiguration {
let height: CGFloat
let colorScheme: ColorScheme
let workState: SnapshotWorkStoreState
let glanceState: DashboardSnapshotGlanceState

init(
viewport: String,
width: CGFloat,
height: CGFloat,
colorScheme: ColorScheme,
workState: SnapshotWorkStoreState,
glanceState: DashboardSnapshotGlanceState = .fixture
) {
self.viewport = viewport
self.width = width
self.height = height
self.colorScheme = colorScheme
self.workState = workState
self.glanceState = glanceState
}

var filename: String {
let appearance = colorScheme == .dark ? "dark" : "light"
Expand All @@ -151,9 +168,48 @@ struct DashboardSnapshotConfiguration {
Self(viewport: "reference", width: 1120, height: 800, colorScheme: .dark, workState: .populated),
Self(viewport: "trust-unavailable", width: 1120, height: 800, colorScheme: .light, workState: .shiftBriefUnavailable),
Self(viewport: "trust-unavailable", width: 1120, height: 800, colorScheme: .dark, workState: .shiftBriefUnavailable),
Self(viewport: "old-daemon-statusless", width: 1120, height: 800, colorScheme: .light, workState: .oldDaemonUnavailable, glanceState: .statuslessUsage),
Self(viewport: "old-daemon-statusless", width: 1120, height: 800, colorScheme: .dark, workState: .oldDaemonUnavailable, glanceState: .statuslessUsage),
]
}

enum DashboardSnapshotGlanceState {
case fixture
case statuslessUsage

func snapshot(from fixture: DashboardSnapshotFixture) -> GlanceSnapshot {
guard self == .statuslessUsage else { return fixture.glanceSnapshot }
let glance = fixture.glance
let generatedAt = glance.generatedAt ?? 0
let ids = [
"01a046ac", "01a046bd", "01a046ce", "01a046df",
"01a046e0", "01a046f1", "01a04702", "01a04713",
]
let sessions = ids.enumerated().map { index, id in
RecentSession(
client: "codex",
sessionId: "\(id)-statusless",
title: nil,
status: nil,
lastActivityAt: generatedAt - Double(51 + index * 22),
planPct: nil
)
}
return GlanceSnapshot(
glance: Glance(
schema: glance.schema,
generatedAt: glance.generatedAt,
daemon: glance.daemon,
usage: glance.usage,
limits: glance.limits,
plan: glance.plan,
recentSessions: sessions
),
daemonVersion: fixture.daemonVersion
)
}
}

enum DashboardSnapshotRenderer {
private static let snapshotLocale = Locale(identifier: "en_US_POSIX")
private static let snapshotTimeZone = TimeZone(secondsFromGMT: 0)!
Expand Down Expand Up @@ -193,7 +249,9 @@ enum DashboardSnapshotRenderer {

return try configurations.map { configuration in
SnapshotScheme.override = configuration.colorScheme
let glance = GlanceState(preloaded: fixture.glanceSnapshot)
let glance = GlanceState(
preloaded: configuration.glanceState.snapshot(from: fixture)
)
let dashboard = DashboardStore(
preloaded: fixture,
workState: configuration.workState
Expand Down
Loading
Loading