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
29 changes: 22 additions & 7 deletions apps/agentacct/Sources/agentacct/DashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,11 @@ enum DashboardUsageSeries: String, CaseIterable, Identifiable {
}
}

func subtitle(dayCount: Int) -> String {
func subtitle(rangeDays: Int, periodPresentation: UsagePeriodPresentation) -> String {
let range = periodPresentation.historyRangeDescription(days: rangeDays)
switch self {
case .tokens: return "Fresh tokens · last \(dayCount) days · client reported"
case .cost: return "Estimated cost · last \(dayCount) days · pricing-table basis"
case .tokens: return "Fresh tokens · \(range) · client reported"
case .cost: return "Estimated cost · \(range) · pricing-table basis"
}
}
}
Expand Down Expand Up @@ -306,8 +307,15 @@ struct DashboardPane: View {
)
}

if let periods = dashboard.usage?.byPeriod, periods.count > 1 {
DashboardUsageChart(periods: periods)
if let usage = dashboard.usage,
let periods = usage.byPeriod,
periods.count > 1
{
DashboardUsageChart(
periods: periods,
rangeDays: dashboard.usageDays,
periodPresentation: UsagePeriodPresentation(usage: usage)
)
}
}
.padding(Space.gutter)
Expand Down Expand Up @@ -931,6 +939,8 @@ private struct AgentPlanRowView: View {

private struct DashboardUsageChart: View {
let periods: [PeriodBucket]
let rangeDays: Int
let periodPresentation: UsagePeriodPresentation

@State private var series: DashboardUsageSeries = .tokens
@State private var hoveredIndex: Int?
Expand All @@ -949,7 +959,12 @@ private struct DashboardUsageChart: View {
Text("Usage history")
.font(Type.titleCard)
.foregroundStyle(Theme.muted)
Text(series.subtitle(dayCount: periods.count))
Text(
series.subtitle(
rangeDays: rangeDays,
periodPresentation: periodPresentation
)
)
.font(Type.caption)
.foregroundStyle(Theme.muted)
}
Expand Down Expand Up @@ -1053,7 +1068,7 @@ private struct DashboardUsageChart: View {
.accessibilityLabel(
"\(period.period ?? period.shortLabel), \(series.valueText(for: period))"
)
.accessibilityHint("Pins or clears this day's value")
.accessibilityHint(periodPresentation.pinAccessibilityHint)
.accessibilityAddTraits(pinnedIndex == index ? .isSelected : [])
.accessibilityIdentifier("dashboard.usage.day.\(index)")

Expand Down
32 changes: 25 additions & 7 deletions apps/agentacct/Sources/agentacct/DashboardSnapshotHarness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ struct WorkSnapshotFixture: Decodable {
}
}

enum SnapshotRecordedUsageState {
case sevenDays
case ninetyDays

func storeState(for fixture: DashboardSnapshotFixture) -> SnapshotUsageStoreState {
switch self {
case .sevenDays:
return SnapshotUsageStoreState(days: 7, summary: fixture.usage)
case .ninetyDays:
return SnapshotUsageStoreState(days: 90, summary: fixture.usage90Days)
}
}
}

enum SnapshotError: LocalizedError {
case unsupportedSchema(payload: String, actual: String, expected: String)
case missingFixtureDate
Expand Down Expand Up @@ -141,22 +155,25 @@ struct DashboardSnapshotConfiguration {
let height: CGFloat
let colorScheme: ColorScheme
let workState: SnapshotWorkStoreState
let recordedUsageState: SnapshotRecordedUsageState

var filename: String {
let appearance = colorScheme == .dark ? "dark" : "light"
return "dashboard-\(viewport)-\(appearance).png"
}

static let reviewConfigurations: [Self] = [
Self(viewport: "minimum", width: 960, height: 560, colorScheme: .light, workState: .populated),
Self(viewport: "minimum", width: 960, height: 560, colorScheme: .dark, workState: .populated),
Self(viewport: "minimum", width: 960, height: 560, colorScheme: .light, workState: .populated, recordedUsageState: .sevenDays),
Self(viewport: "minimum", width: 960, height: 560, colorScheme: .dark, workState: .populated, recordedUsageState: .sevenDays),
// The reference viewport must show the complete dashboard, including
// chart labels. The shorter minimum pair intentionally verifies the
// real top-of-scroll experience instead.
Self(viewport: "reference", width: 1120, height: 800, colorScheme: .light, workState: .populated),
Self(viewport: "reference", width: 1120, height: 800, colorScheme: .dark, workState: .populated),
Self(viewport: "attention-unavailable", width: 1120, height: 800, colorScheme: .light, workState: .listErrorWithRetainedData),
Self(viewport: "attention-unavailable", width: 1120, height: 800, colorScheme: .dark, workState: .listErrorWithRetainedData),
Self(viewport: "reference", width: 1120, height: 800, colorScheme: .light, workState: .populated, recordedUsageState: .sevenDays),
Self(viewport: "reference", width: 1120, height: 800, colorScheme: .dark, workState: .populated, recordedUsageState: .sevenDays),
Self(viewport: "weekly-reference", width: 1120, height: 900, colorScheme: .light, workState: .populated, recordedUsageState: .ninetyDays),
Self(viewport: "weekly-reference", width: 1120, height: 900, colorScheme: .dark, workState: .populated, recordedUsageState: .ninetyDays),
Self(viewport: "attention-unavailable", width: 1120, height: 800, colorScheme: .light, workState: .listErrorWithRetainedData, recordedUsageState: .sevenDays),
Self(viewport: "attention-unavailable", width: 1120, height: 800, colorScheme: .dark, workState: .listErrorWithRetainedData, recordedUsageState: .sevenDays),
]
}

Expand Down Expand Up @@ -202,7 +219,8 @@ enum DashboardSnapshotRenderer {
let glance = GlanceState(preloaded: fixture.glanceSnapshot)
let dashboard = DashboardStore(
preloaded: fixture,
workState: configuration.workState
workState: configuration.workState,
usageState: configuration.recordedUsageState.storeState(for: fixture)
)
let selection = AppSelection()
selection.pane = .dashboard
Expand Down
2 changes: 1 addition & 1 deletion apps/agentacct/Sources/agentacct/DashboardStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ final class DashboardStore {
}

/// Switch the pane range and refetch BOTH the plan lane and the cost cube
/// so the plan breakdown, the daily bars, and the $ view stay on one window.
/// so the plan breakdown, the period bars, and the $ view stay on one window.
/// The range label only flips once both payloads have landed, and only the
/// newest in-flight switch is allowed to write.
func setUsageDays(_ days: Int) async {
Expand Down
10 changes: 10 additions & 0 deletions apps/agentacct/Sources/agentacct/UsagePane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,19 +371,23 @@ struct UsagePeriodPresentation {
let value: String?
let absent: String
private let unit: String
private let bucketDescription: String?

init(usage: UsageSummary) {
switch usage.filtersEcho?.granularity {
case "daily":
unit = "day"
bucketDescription = nil
label = "Active days"
absent = "no daily series"
case "weekly":
unit = "week"
bucketDescription = "weekly buckets"
label = "Active weeks"
absent = "no weekly series"
default:
unit = "period"
bucketDescription = "period buckets"
label = "Active periods"
absent = "no period series"
}
Expand All @@ -405,6 +409,12 @@ struct UsagePeriodPresentation {
var previousAccessibilityLabel: String { "Previous usage \(unit)" }
var nextAccessibilityLabel: String { "Next usage \(unit)" }
var selectionAccessibilityHint: String { "Selects this \(unit)'s value" }
var pinAccessibilityHint: String { "Pins or clears this \(unit)'s value" }

func historyRangeDescription(days: Int) -> String {
let range = "last \(days) days"
return bucketDescription.map { "\(range) · \($0)" } ?? range
}
}

private struct UsagePlanClientDetail: View {
Expand Down
16 changes: 1 addition & 15 deletions apps/agentacct/Sources/agentacct/UsageSnapshotHarness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,12 @@ struct UsageSnapshotConfiguration {
case disconnected
}

enum RecordedUsageState {
case sevenDays
case ninetyDays

func storeState(for fixture: DashboardSnapshotFixture) -> SnapshotUsageStoreState {
switch self {
case .sevenDays:
return SnapshotUsageStoreState(days: 7, summary: fixture.usage)
case .ninetyDays:
return SnapshotUsageStoreState(days: 90, summary: fixture.usage90Days)
}
}
}

let viewport: String
let width: CGFloat
let height: CGFloat
let colorScheme: ColorScheme
let capacityState: CapacityState
let recordedUsageState: RecordedUsageState
let recordedUsageState: SnapshotRecordedUsageState

var filename: String {
let appearance = colorScheme == .dark ? "dark" : "light"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@ final class DashboardInteractionTests: XCTestCase {
XCTAssertEqual(DashboardUsageSeries.cost.totalText(for: [periods[2]]), "—")
}

func testUsageSeriesDescribesTheSelectedRangeAndEffectiveGranularity() throws {
let daily = try usagePeriodPresentation(granularity: "daily")
let weekly = try usagePeriodPresentation(granularity: "weekly")
let unknown = try usagePeriodPresentation(granularity: nil)

XCTAssertEqual(
DashboardUsageSeries.tokens.subtitle(
rangeDays: 7,
periodPresentation: daily
),
"Fresh tokens · last 7 days · client reported"
)
XCTAssertEqual(
DashboardUsageSeries.tokens.subtitle(
rangeDays: 90,
periodPresentation: weekly
),
"Fresh tokens · last 90 days · weekly buckets · client reported"
)
XCTAssertEqual(
DashboardUsageSeries.cost.subtitle(
rangeDays: 90,
periodPresentation: weekly
),
"Estimated cost · last 90 days · weekly buckets · pricing-table basis"
)
XCTAssertEqual(
DashboardUsageSeries.tokens.subtitle(
rangeDays: 30,
periodPresentation: unknown
),
"Fresh tokens · last 30 days · period buckets · client reported"
)
XCTAssertEqual(weekly.pinAccessibilityHint, "Pins or clears this week's value")
}

func testActiveWorkIncludesOnlyRunningStates() {
let cases: [(status: String?, isActive: Bool)] = [
("started", true),
Expand Down Expand Up @@ -1042,4 +1078,15 @@ final class DashboardInteractionTests: XCTestCase {
private func decode<Value: Decodable>(_ type: Value.Type, from json: String) throws -> Value {
try JSONDecoder().decode(type, from: Data(json.utf8))
}

private func usagePeriodPresentation(granularity: String?) throws -> UsagePeriodPresentation {
let filtersEcho = granularity.map {
",\"filters_echo\":{\"granularity\":\"\($0)\"}"
} ?? ""
let usage = try decode(
UsageSummary.self,
from: "{\"by_client\":[],\"by_model\":[]\(filtersEcho)}"
)
return UsagePeriodPresentation(usage: usage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ final class DashboardSnapshotHarnessTests: XCTestCase {
ExpectedArtifact(filename: "dashboard-minimum-dark.png", pixelsWide: 1920, pixelsHigh: 1120),
ExpectedArtifact(filename: "dashboard-reference-light.png", pixelsWide: 2240, pixelsHigh: 1600),
ExpectedArtifact(filename: "dashboard-reference-dark.png", pixelsWide: 2240, pixelsHigh: 1600),
ExpectedArtifact(filename: "dashboard-weekly-reference-light.png", pixelsWide: 2240, pixelsHigh: 1800),
ExpectedArtifact(filename: "dashboard-weekly-reference-dark.png", pixelsWide: 2240, pixelsHigh: 1800),
ExpectedArtifact(filename: "dashboard-attention-unavailable-light.png", pixelsWide: 2240, pixelsHigh: 1600),
ExpectedArtifact(filename: "dashboard-attention-unavailable-dark.png", pixelsWide: 2240, pixelsHigh: 1600),
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ final class DashboardVisualRegressionTests: XCTestCase {
"dashboard-minimum-dark.png",
"dashboard-reference-light.png",
"dashboard-reference-dark.png",
"dashboard-weekly-reference-light.png",
"dashboard-weekly-reference-dark.png",
"dashboard-attention-unavailable-light.png",
"dashboard-attention-unavailable-dark.png",
]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class UsagePeriodPresentationTests: XCTestCase {
XCTAssertEqual(presentation.tokenChartTitle(group: nil), "Fresh tokens per day")
XCTAssertEqual(presentation.previousAccessibilityLabel, "Previous usage day")
XCTAssertEqual(presentation.selectionAccessibilityHint, "Selects this day's value")
XCTAssertEqual(presentation.pinAccessibilityHint, "Pins or clears this day's value")
}

func testUsesEffectiveWeeklyGranularity() throws {
Expand All @@ -31,6 +32,7 @@ final class UsagePeriodPresentationTests: XCTestCase {
)
XCTAssertEqual(presentation.nextAccessibilityLabel, "Next usage week")
XCTAssertEqual(presentation.selectionAccessibilityHint, "Selects this week's value")
XCTAssertEqual(presentation.pinAccessibilityHint, "Pins or clears this week's value")
}

func testUnknownOrMissingGranularityUsesTruthfulGenericCopy() throws {
Expand All @@ -44,6 +46,7 @@ final class UsagePeriodPresentationTests: XCTestCase {
XCTAssertEqual(presentation.absent, "no period series")
XCTAssertEqual(presentation.costChartTitle, "Cost per period")
XCTAssertEqual(presentation.previousAccessibilityLabel, "Previous usage period")
XCTAssertEqual(presentation.pinAccessibilityHint, "Pins or clears this period's value")
}
}

Expand Down
Loading