From e8aa96baa13eb7358b563e7403cf96b0177de247 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Fri, 22 May 2026 15:19:33 +0800 Subject: [PATCH 1/2] {"schema":"decodex/commit/1","summary":"Improve app lane activity popovers","authority":"manual"} --- .../Sources/DecodexApp/AccountPanelView.swift | 917 +++++++++++++++++- .../Sources/DecodexApp/LoginSheetView.swift | 9 - .../DecodexApp/OperatorSnapshotModels.swift | 111 ++- .../Sources/DecodexApp/PanelTypography.swift | 58 ++ apps/decodex/src/orchestrator/entrypoints.rs | 50 +- .../src/orchestrator/operator_dashboard.html | 270 +++++- .../tests/operator/status/control_plane.rs | 39 + .../tests/operator/status/dashboard.rs | 7 + dev/operator-dashboard-mock.mjs | 75 ++ 9 files changed, 1467 insertions(+), 69 deletions(-) create mode 100644 apps/decodex-app/Sources/DecodexApp/PanelTypography.swift diff --git a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift index 31559e16..93cb732d 100644 --- a/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift +++ b/apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift @@ -2,33 +2,6 @@ import AppKit import Foundation import SwiftUI -enum PanelFont { - private static func text( - _ size: CGFloat, - weight: Font.Weight, - design: Font.Design = .default - ) -> Font { - .system(size: size, weight: weight, design: design) - } - - static let headerIcon = text(14.1, weight: .semibold) - static let headerTitle = text(14.8, weight: .semibold) - static let headerSubtitle = text(11.1, weight: .medium) - static let emptyIcon = text(16.8, weight: .medium) - static let emptyTitle = text(12.2, weight: .semibold) - static let emptyBody = text(10.9, weight: .regular) - static let notice = text(10.6, weight: .regular) - static let summaryIcon = text(10.4, weight: .medium) - static let metricLabel = text(10.4, weight: .medium) - static let metricValue = text(11.9, weight: .semibold) - static let accountName = text(13.1, weight: .semibold) - static let accountDetail = text(10.9, weight: .medium) - static let usageLabel = text(10.4, weight: .medium) - static let usageValue = text(10.7, weight: .semibold) - static let tertiary = text(9.7, weight: .medium) - static let iconButton = text(11.2, weight: .semibold) -} - enum PanelPalette { static func primaryText(_ colorScheme: ColorScheme) -> Color { colorScheme == .dark @@ -841,17 +814,10 @@ struct AccountRunSummaryView: View { var body: some View { HStack(spacing: 5) { - ForEach(Array(runs.prefix(2))) { run in - AccountRunChipView(run: run) - } + AccountRunChipView(run: runs[0]) - if runs.count > 2 { - Text("+\(runs.count - 2)") - .font(PanelFont.metricLabel) - .foregroundStyle(PanelPalette.secondaryText(colorScheme)) - .frame(height: 19) - .padding(.horizontal, 6) - .modernGlassSurface(cornerRadius: 9, depth: .control) + if runs.count > 1 { + AccountRunOverflowView(runs: runs) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -863,29 +829,137 @@ struct AccountRunChipView: View { @Environment(\.colorScheme) private var colorScheme var body: some View { - HStack(spacing: 4) { + HStack(spacing: 5) { Image(systemName: symbol) .font(PanelFont.summaryIcon) - .foregroundStyle(tint.opacity(colorScheme == .dark ? 0.9 : 0.78)) - .frame(width: 10) + .foregroundStyle(tint.opacity(colorScheme == .dark ? 0.88 : 0.76)) + .frame(width: 11) Text(run.compactTitle) - .font(PanelFont.metricLabel) + .font(PanelFont.metricValue) .foregroundStyle(PanelPalette.primaryText(colorScheme).opacity(0.92)) .lineLimit(1) .truncationMode(.middle) + } + .frame(height: 21) + .padding(.horizontal, 8) + .modernGlassSurface(cornerRadius: 10.5, depth: .control) + .fixedSize(horizontal: true, vertical: false) + } - Text(run.compactDetail) - .font(PanelFont.metricLabel) + private var symbol: String { + if run.hasAttentionTone { + return "exclamationmark.triangle.fill" + } + if run.isWaiting { + return "clock" + } + + return "play.fill" + } + + private var tint: Color { + if run.hasAttentionTone { + return PanelPalette.warning(colorScheme) + } + if run.isWaiting { + return PanelPalette.secondaryText(colorScheme) + } + + return PanelPalette.routeAccent(colorScheme) + } +} + +struct AccountRunOverflowView: View { + let runs: [OperatorRunStatus] + @Environment(\.colorScheme) private var colorScheme + @State private var isHovered = false + @State private var showsPopover = false + + var body: some View { + Text("+\(max(0, runs.count - 1))") + .font(PanelFont.metricLabel) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .frame(height: 21) + .padding(.horizontal, 7) + .background { + RoundedRectangle(cornerRadius: 10.5, style: .continuous) + .fill(isHovered ? PanelPalette.routeAccent(colorScheme).opacity(0.08) : Color.clear) + } + .modernGlassSurface(cornerRadius: 10.5, depth: .control) + .fixedSize(horizontal: true, vertical: false) + .contentShape(RoundedRectangle(cornerRadius: 10.5, style: .continuous)) + .onHover { hovering in + withAnimation(PanelMotion.hover) { + isHovered = hovering + } + showsPopover = hovering + } + .popover(isPresented: $showsPopover, arrowEdge: .trailing) { + AccountRunOverflowPopoverView(runs: runs) + .frame(width: 240) + .padding(10) + } + } +} + +struct AccountRunOverflowPopoverView: View { + let runs: [OperatorRunStatus] + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 7) { + Image(systemName: "arrow.triangle.branch") + .font(PanelFont.summaryIcon) + .foregroundStyle(PanelPalette.routeAccent(colorScheme).opacity(0.86)) + .frame(width: 12) + + Text("\(runs.count) running") + .font(PanelFont.lanePopoverTitle) + .foregroundStyle(PanelPalette.primaryText(colorScheme)) + .lineLimit(1) + } + + VStack(spacing: 0) { + ForEach(runs) { run in + AccountRunOverflowRowView(run: run) + } + } + } + .padding(10) + .modernGlassSurface(cornerRadius: 12, depth: .section) + .accessibilityLabel("\(runs.count) account lanes") + } +} + +struct AccountRunOverflowRowView: View { + let run: OperatorRunStatus + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + HStack(spacing: 7) { + Image(systemName: symbol) + .font(PanelFont.summaryIcon) + .foregroundStyle(tint.opacity(colorScheme == .dark ? 0.9 : 0.76)) + .frame(width: 12) + + Text(run.compactTitle) + .font(PanelFont.laneTitle) + .foregroundStyle(PanelPalette.primaryText(colorScheme)) + .lineLimit(1) + .truncationMode(.middle) + + Spacer(minLength: 6) + + Text(statusLabel) + .font(PanelFont.tertiary) .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .monospacedDigit() .lineLimit(1) - .truncationMode(.tail) } - .frame(height: 19) - .frame(maxWidth: 132, alignment: .leading) - .padding(.horizontal, 6) - .modernGlassSurface(cornerRadius: 9, depth: .control) - .layoutPriority(1) + .frame(height: 25) + .padding(.horizontal, 2) } private var symbol: String { @@ -909,6 +983,17 @@ struct AccountRunChipView: View { return PanelPalette.routeAccent(colorScheme) } + + private var statusLabel: String { + if run.hasAttentionTone { + return "attention" + } + if run.isWaiting { + return "waiting" + } + + return humanizedPanelToken(run.phase ?? run.status ?? "running") + } } struct AccountPoolUsageEstimateView: View { @@ -1448,9 +1533,10 @@ struct OperatorStatusStripView: View { let updatedAt: Date? let refreshIntervalSeconds: Int @Environment(\.colorScheme) private var colorScheme + @State private var showsAllLanes = false var body: some View { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 5) { if !metrics.isEmpty { HStack(spacing: 0) { ForEach(Array(metrics.enumerated()), id: \.element.id) { index, metric in @@ -1464,6 +1550,11 @@ struct OperatorStatusStripView: View { .frame(height: 32) } + if !snapshot.activeRuns.isEmpty { + OperatorLaneListView(runs: snapshot.activeRuns, showsAllLanes: $showsAllLanes) + .padding(.top, metrics.isEmpty ? 0 : 1) + } + if let warning = snapshot.warningSummary { HStack(spacing: 5) { Image(systemName: "exclamationmark.circle") @@ -1549,6 +1640,578 @@ struct OperatorStatusStripView: View { } } +struct OperatorLaneListView: View { + let runs: [OperatorRunStatus] + @Binding var showsAllLanes: Bool + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + VStack(spacing: 0) { + ForEach(visibleRuns) { run in + OperatorLaneRowView(run: run) + } + + if hasOverflow { + Button { + withAnimation(PanelMotion.state) { + showsAllLanes.toggle() + } + } label: { + HStack(spacing: 5) { + Text(toggleLabel) + .font(PanelFont.tertiary) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .lineLimit(1) + Spacer(minLength: 4) + Image(systemName: showsAllLanes ? "chevron.up" : "chevron.down") + .font(PanelFont.summaryIcon) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .frame(width: 12) + } + .frame(height: 20) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help(showsAllLanes ? "Collapse running lanes" : "Show all running lanes") + } + } + .overlay(alignment: .top) { + Rectangle() + .fill(PanelPalette.separator(colorScheme).opacity(colorScheme == .dark ? 0.58 : 0.78)) + .frame(height: 0.5) + .allowsHitTesting(false) + } + .overlay(alignment: .bottom) { + Rectangle() + .fill(PanelPalette.separator(colorScheme).opacity(colorScheme == .dark ? 0.36 : 0.5)) + .frame(height: 0.5) + .allowsHitTesting(false) + } + .accessibilityLabel("\(runs.count) running lanes") + } + + private var visibleRuns: [OperatorRunStatus] { + showsAllLanes ? runs : Array(runs.prefix(3)) + } + + private var hasOverflow: Bool { + runs.count > 3 + } + + private var toggleLabel: String { + if showsAllLanes { + return "Show 3 lanes" + } + + return "\(runs.count - 3) more lane\(runs.count - 3 == 1 ? "" : "s")" + } +} + +struct OperatorLaneRowView: View { + let run: OperatorRunStatus + @Environment(\.colorScheme) private var colorScheme + @State private var isHovered = false + @State private var showsPopover = false + + var body: some View { + HStack(spacing: 7) { + Image(systemName: symbol) + .font(PanelFont.summaryIcon) + .foregroundStyle(tint.opacity(colorScheme == .dark ? 0.96 : 0.82)) + .frame(width: 12) + + Text(run.compactTitle) + .font(PanelFont.laneTitle) + .foregroundStyle(PanelPalette.primaryText(colorScheme)) + .lineLimit(1) + .truncationMode(.middle) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(statusLabel) + .font(PanelFont.laneStatus) + .foregroundStyle(tint.opacity(colorScheme == .dark ? 0.9 : 0.78)) + .monospacedDigit() + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) + } + .frame(height: 25) + .padding(.horizontal, 2) + .background { + RoundedRectangle(cornerRadius: 7, style: .continuous) + .fill(isHovered ? tint.opacity(colorScheme == .dark ? 0.08 : 0.07) : Color.clear) + } + .contentShape(RoundedRectangle(cornerRadius: 7, style: .continuous)) + .onHover { hovering in + withAnimation(PanelMotion.hover) { + isHovered = hovering + } + showsPopover = hovering + } + .popover(isPresented: $showsPopover, arrowEdge: .trailing) { + OperatorLanePopoverView(run: run) + .frame(width: 360) + .padding(8) + } + } + + private var symbol: String { + if run.hasAttentionTone { + return "exclamationmark.triangle.fill" + } + if run.isWaiting { + return "clock" + } + if run.processAlive == false { + return "checkmark.circle" + } + + return "play.fill" + } + + private var statusLabel: String { + if run.hasAttentionTone { + return "attention" + } + if run.isWaiting { + return "waiting" + } + if run.processAlive == false { + return "done" + } + + return humanizedPanelToken(run.phase ?? run.status ?? "running") + } + + private var tint: Color { + if run.hasAttentionTone { + return PanelPalette.warning(colorScheme) + } + if run.isWaiting { + return PanelPalette.secondaryText(colorScheme) + } + if run.processAlive == false { + return PanelPalette.landingAccent(colorScheme) + } + + return PanelPalette.routeAccent(colorScheme) + } +} + +struct OperatorLanePopoverView: View { + let run: OperatorRunStatus + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + VStack(alignment: .leading, spacing: 7) { + header + + if let modelBucket { + OperatorLaneProgressReadoutRow( + title: "Model", + percent: bucketPercent(modelBucket), + elapsed: formatActivityDuration(modelBucket.wallSeconds) ?? "0s", + total: formatActivityDuration(totalWallSeconds) ?? "0s", + barShare: bucketShare(modelBucket) + ) + OperatorLaneReadoutDivider() + } + + ForEach(detailBuckets) { bucket in + OperatorLaneReadoutRow(title: humanizedPanelToken(bucket.name), items: bucketReadoutItems(bucket)) + } + + if !contextReadoutItems.isEmpty { + OperatorLaneReadoutDivider() + OperatorLaneReadoutRow(title: "Context", items: contextReadoutItems) + } + } + .padding(9) + .modernGlassSurface(cornerRadius: 12, depth: .section) + .accessibilityLabel("Lane activity for \(run.compactTitle)") + } + + private var tint: Color { + if run.hasAttentionTone { + return PanelPalette.warning(colorScheme) + } + if run.isWaiting { + return PanelPalette.secondaryText(colorScheme) + } + + return PanelPalette.routeAccent(colorScheme) + } + + private var activity: OperatorChildAgentActivity? { + run.childAgentActivity + } + + private var currentSummary: String { + guard let activity else { + return "Waiting for child activity" + } + + let label = panelTrimmed(activity.currentDetail) + ?? panelTrimmed(activity.currentBucket).map(humanizedPanelToken) + ?? "Active" + if let elapsed = formatActivityDuration(activity.currentElapsedSeconds) { + return "\(humanizedPanelToken(label)) · \(elapsed)" + } + + return humanizedPanelToken(label) + } + + private var header: some View { + OperatorLaneReadoutRow(title: "Activity", items: [ + OperatorLaneReadoutItem(label: nil, value: currentSummary, tone: .primary), + ], trailing: run.compactTitle) + } + + private var modelBucket: OperatorChildAgentBucket? { + orderedBuckets.first { bucket in + bucket.name.caseInsensitiveCompare("Model") == .orderedSame + } + } + + private var detailBuckets: [OperatorChildAgentBucket] { + orderedBuckets.filter { bucket in + bucket.name.caseInsensitiveCompare("Model") != .orderedSame + && !bucketReadoutItems(bucket).isEmpty + } + } + + private var orderedBuckets: [OperatorChildAgentBucket] { + bucketRows.sorted { left, right in + let leftPriority = bucketPriority(left.name) + let rightPriority = bucketPriority(right.name) + if leftPriority != rightPriority { + return leftPriority < rightPriority + } + if left.wallSeconds != right.wallSeconds { + return left.wallSeconds > right.wallSeconds + } + if left.eventCount != right.eventCount { + return left.eventCount > right.eventCount + } + + return left.name < right.name + } + } + + private var contextReadoutItems: [OperatorLaneReadoutItem] { + guard let activity else { + return [] + } + + var items = [OperatorLaneReadoutItem]() + if let current = activity.inputTokensCurrent { + items.append(OperatorLaneReadoutItem(label: "current", value: "\(formatCompactCount(current)) tok")) + } + if let peak = activity.inputTokensMax, peak != activity.inputTokensCurrent { + items.append(OperatorLaneReadoutItem(label: "peak", value: "\(formatCompactCount(peak)) tok")) + } + if activity.inputTokensCumulative > 0 { + items.append(OperatorLaneReadoutItem(label: "input", value: "\(formatCompactCount(activity.inputTokensCumulative)) tok")) + } + if activity.toolCallCount > 0 { + items.append(OperatorLaneReadoutItem(label: "tool_calls", value: formatCompactCount(activity.toolCallCount))) + } + if let largestOutput = activity.largestToolOutputBytes, largestOutput > 0 { + items.append(OperatorLaneReadoutItem(label: "largest output", value: formatCompactBytes(largestOutput))) + } + + return items + } + + private var bucketRows: [OperatorChildAgentBucket] { + activity?.buckets ?? [] + } + + private var totalWallSeconds: Int { + max( + 1, + activity?.wallSeconds ?? 0, + bucketRows.reduce(0) { $0 + max(0, $1.wallSeconds) } + ) + } + + private func bucketReadoutItems(_ bucket: OperatorChildAgentBucket) -> [OperatorLaneReadoutItem] { + let normalizedName = bucket.name.lowercased() + var items = [OperatorLaneReadoutItem]() + + if normalizedName.contains("tracker"), bucket.wallSeconds > 0 { + items.append(OperatorLaneReadoutItem(label: "wall", value: formatActivityDuration(bucket.wallSeconds) ?? "0s")) + } + if bucket.eventCount > 0 { + items.append(OperatorLaneReadoutItem(label: "events", value: formatCompactCount(bucket.eventCount))) + } + if normalizedName.contains("protocol") { + if bucket.inputTokens > 0 { + items.append(OperatorLaneReadoutItem(label: "input", value: "\(formatCompactCount(bucket.inputTokens)) tok")) + } + if bucket.outputTokens > 0 { + items.append(OperatorLaneReadoutItem(label: "output", value: "\(formatCompactCount(bucket.outputTokens)) tok")) + } + } else { + if bucket.toolCallCount > 0 { + items.append(OperatorLaneReadoutItem(label: "tool_calls", value: formatCompactCount(bucket.toolCallCount))) + } + if bucket.outputBytes > 0 { + items.append(OperatorLaneReadoutItem(label: "output bytes", value: formatCompactBytes(bucket.outputBytes))) + } + if !normalizedName.contains("tracker") { + if bucket.inputTokens > 0 { + items.append(OperatorLaneReadoutItem(label: "input", value: "\(formatCompactCount(bucket.inputTokens)) tok")) + } + if bucket.outputTokens > 0 { + items.append(OperatorLaneReadoutItem(label: "output", value: "\(formatCompactCount(bucket.outputTokens)) tok")) + } + } + } + + return items + } + + private func bucketPriority(_ name: String) -> Int { + let normalizedName = name.lowercased() + if normalizedName.contains("model") { + return 0 + } + if normalizedName.contains("protocol") { + return 1 + } + if normalizedName.contains("tracker") { + return 2 + } + + return 10 + } + + private func bucketShare(_ bucket: OperatorChildAgentBucket) -> CGFloat { + guard bucket.wallSeconds > 0 else { + return 0 + } + + return min(1, max(0.02, CGFloat(bucket.wallSeconds) / CGFloat(max(1, totalWallSeconds)))) + } + + private func bucketPercent(_ bucket: OperatorChildAgentBucket) -> Int { + Int((Double(bucket.wallSeconds) / Double(max(1, totalWallSeconds)) * 100).rounded()) + } +} + +struct OperatorLaneReadoutItem: Identifiable { + enum Tone { + case primary + case secondary + } + + let label: String? + let value: String + let tone: Tone + + init(label: String?, value: String, tone: Tone = .secondary) { + self.label = label + self.value = value + self.tone = tone + } + + var id: String { + "\(label ?? "value")-\(value)" + } +} + +struct OperatorLaneReadoutRow: View { + let title: String + let items: [OperatorLaneReadoutItem] + let trailing: String? + @Environment(\.colorScheme) private var colorScheme + + init(title: String, items: [OperatorLaneReadoutItem], trailing: String? = nil) { + self.title = title + self.items = items + self.trailing = trailing + } + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(title) + .font(PanelFont.lanePopoverLabel) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .lineLimit(1) + .frame(width: 62, alignment: .leading) + + OperatorLaneReadoutFlowLayout(spacing: 8, rowSpacing: 4) { + ForEach(items) { item in + OperatorLaneReadoutItemView(item: item) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + + if let trailing = panelTrimmed(trailing) { + Text(trailing) + .font(PanelFont.lanePopoverMeta) + .foregroundStyle(PanelPalette.secondaryText(colorScheme).opacity(0.76)) + .lineLimit(1) + .truncationMode(.middle) + .frame(maxWidth: 78, alignment: .trailing) + } + } + } +} + +struct OperatorLaneProgressReadoutRow: View { + let title: String + let percent: Int + let elapsed: String + let total: String + let barShare: CGFloat + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + HStack(alignment: .center, spacing: 8) { + Text(title) + .font(PanelFont.lanePopoverLabel) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .lineLimit(1) + .frame(width: 62, alignment: .leading) + + ZStack(alignment: .leading) { + Capsule() + .fill(PanelPalette.separator(colorScheme).opacity(colorScheme == .dark ? 0.42 : 0.56)) + Capsule() + .fill(PanelPalette.routeAccent(colorScheme).opacity(colorScheme == .dark ? 0.74 : 0.62)) + .frame(maxWidth: .infinity) + .scaleEffect(x: barShare, y: 1, anchor: .leading) + } + .frame(height: 5) + .frame(maxWidth: .infinity) + + Text("\(percent)% · \(elapsed) / \(total)") + .font(PanelFont.lanePopoverMeta) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .monospacedDigit() + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) + } + .frame(height: 22) + } +} + +struct OperatorLaneReadoutItemView: View { + let item: OperatorLaneReadoutItem + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 3) { + if let label = item.label { + Text(label) + .font(PanelFont.lanePopoverMeta) + .foregroundStyle(PanelPalette.secondaryText(colorScheme)) + .lineLimit(1) + } + + Text(item.value) + .font(PanelFont.lanePopoverValue) + .foregroundStyle(valueColor) + .monospacedDigit() + .lineLimit(1) + .minimumScaleFactor(0.78) + } + .fixedSize(horizontal: true, vertical: false) + } + + private var valueColor: Color { + switch item.tone { + case .primary: + return PanelPalette.primaryText(colorScheme) + case .secondary: + return PanelPalette.primaryText(colorScheme).opacity(colorScheme == .dark ? 0.9 : 0.84) + } + } +} + +struct OperatorLaneReadoutDivider: View { + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Rectangle() + .fill(PanelPalette.separator(colorScheme)) + .frame(height: 0.5) + .padding(.vertical, 1) + } +} + +struct OperatorLaneReadoutFlowLayout: Layout { + let spacing: CGFloat + let rowSpacing: CGFloat + + init(spacing: CGFloat = 8, rowSpacing: CGFloat = 4) { + self.spacing = spacing + self.rowSpacing = rowSpacing + } + + func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout Void + ) -> CGSize { + let maxWidth = max(0, proposal.width ?? subviews.map { $0.sizeThatFits(.unspecified).width }.reduce(0, +)) + var currentX: CGFloat = 0 + var currentY: CGFloat = 0 + var rowHeight: CGFloat = 0 + var measuredWidth: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if currentX > 0, currentX + spacing + size.width > maxWidth { + currentY += rowHeight + rowSpacing + currentX = 0 + rowHeight = 0 + } + + if currentX > 0 { + currentX += spacing + } + currentX += size.width + rowHeight = max(rowHeight, size.height) + measuredWidth = max(measuredWidth, currentX) + } + + return CGSize(width: proposal.width ?? measuredWidth, height: currentY + rowHeight) + } + + func placeSubviews( + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout Void + ) { + let maxWidth = bounds.width + var currentX: CGFloat = bounds.minX + var currentY: CGFloat = bounds.minY + var rowHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if currentX > bounds.minX, currentX + spacing + size.width > bounds.minX + maxWidth { + currentY += rowHeight + rowSpacing + currentX = bounds.minX + rowHeight = 0 + } + + if currentX > bounds.minX { + currentX += spacing + } + subview.place( + at: CGPoint(x: currentX, y: currentY), + proposal: ProposedViewSize(size) + ) + currentX += size.width + rowHeight = max(rowHeight, size.height) + } + } +} + struct OperatorFlowMetric: Identifiable { let title: String let value: Int @@ -1928,6 +2591,160 @@ private func compactUsageDate(_ value: String) -> String { return formatter.string(from: date) } +private func panelTrimmed(_ value: String?) -> String? { + value?.trimmingCharacters(in: .whitespacesAndNewlines) +} + +private func humanizedPanelToken(_ value: String) -> String { + let words = value + .replacingOccurrences(of: "-", with: " ") + .replacingOccurrences(of: "_", with: " ") + .split(separator: " ") + .map { word in + let text = String(word) + guard let first = text.first else { + return text + } + + return first.uppercased() + String(text.dropFirst()) + } + + return words.joined(separator: " ") +} + +private func formatLaneDuration(_ seconds: Int?) -> String? { + guard let seconds else { + return nil + } + + let value = max(0, seconds) + if value < 60 { + return "<1m" + } + + let days = value / 86_400 + let hours = (value % 86_400) / 3_600 + let minutes = (value % 3_600) / 60 + if days > 0 { + return hours > 0 ? "\(days)d \(hours)h" : "\(days)d" + } + if hours > 0 { + return minutes > 0 ? "\(hours)h \(minutes)m" : "\(hours)h" + } + + return "\(minutes)m" +} + +private func formatActivityDuration(_ seconds: Int?) -> String? { + guard let seconds else { + return nil + } + + let value = max(0, seconds) + if value < 60 { + return "\(value)s" + } + + let hours = value / 3_600 + let minutes = (value % 3_600) / 60 + let remainderSeconds = value % 60 + if hours > 0 { + return minutes > 0 ? "\(hours)h \(minutes)m" : "\(hours)h" + } + if minutes > 0 { + return remainderSeconds > 0 ? "\(minutes)m \(remainderSeconds)s" : "\(minutes)m" + } + + return "\(remainderSeconds)s" +} + +private func formatCompactCount(_ value: Int) -> String { + let absoluteValue = abs(Double(value)) + let sign = value < 0 ? "-" : "" + + if absoluteValue >= 1_000_000_000 { + return "\(sign)\(formatCompactDecimal(absoluteValue / 1_000_000_000))B" + } + if absoluteValue >= 1_000_000 { + return "\(sign)\(formatCompactDecimal(absoluteValue / 1_000_000))M" + } + if absoluteValue >= 1_000 { + return "\(sign)\(formatCompactDecimal(absoluteValue / 1_000))K" + } + + return "\(value)" +} + +private func formatCompactBytes(_ value: Int) -> String { + let absoluteValue = max(0, Double(value)) + if absoluteValue >= 1_073_741_824 { + return "\(formatCompactDecimal(absoluteValue / 1_073_741_824))GB" + } + if absoluteValue >= 1_048_576 { + return "\(formatCompactDecimal(absoluteValue / 1_048_576))MB" + } + if absoluteValue >= 1_024 { + return "\(formatCompactDecimal(absoluteValue / 1_024))KB" + } + + return "\(max(0, value))B" +} + +private func formatCompactDecimal(_ value: Double) -> String { + let rounded = (value * 10).rounded() / 10 + if rounded >= 10 || rounded.rounded() == rounded { + return String(format: "%.0f", rounded) + } + + return String(format: "%.1f", rounded) +} + +private func formatPanelTimestamp(_ value: String?) -> String? { + guard let value = panelTrimmed(value) else { + return nil + } + + let date = parsePanelTimestamp(value) + guard let date else { + return value + } + + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + let calendar = Calendar(identifier: .gregorian) + formatter.dateFormat = calendar.component(.year, from: date) == calendar.component(.year, from: Date()) + ? "MMM d HH:mm" + : "MMM d yyyy HH:mm" + return formatter.string(from: date) +} + +private func parsePanelTimestamp(_ value: String) -> Date? { + let fractionalFormatter = ISO8601DateFormatter() + fractionalFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = fractionalFormatter.date(from: value) { + return date + } + + return ISO8601DateFormatter().date(from: value) +} + +private func compactLanePath(_ value: String?) -> String? { + guard var text = panelTrimmed(value), !text.isEmpty else { + return nil + } + + if let home = ProcessInfo.processInfo.environment["HOME"], !home.isEmpty { + text = text.replacingOccurrences(of: home, with: "~") + } + if text.count <= 42 { + return text + } + + let prefix = text.prefix(18) + let suffix = text.suffix(20) + return "\(prefix)...\(suffix)" +} + enum GlassSurfaceDepth { case panel case section diff --git a/apps/decodex-app/Sources/DecodexApp/LoginSheetView.swift b/apps/decodex-app/Sources/DecodexApp/LoginSheetView.swift index 461483a2..b4ab6c40 100644 --- a/apps/decodex-app/Sources/DecodexApp/LoginSheetView.swift +++ b/apps/decodex-app/Sources/DecodexApp/LoginSheetView.swift @@ -452,15 +452,6 @@ private struct LoginTextButtonStyle: ButtonStyle { } -private enum LoginFont { - static let title = Font.system(size: 14.6, weight: .semibold) - static let caption = Font.system(size: 10.6, weight: .medium) - static let destination = Font.system(size: 10.8, weight: .semibold) - static let button = Font.system(size: 10.8, weight: .semibold) - static let icon = Font.system(size: 11.4, weight: .semibold) - static let code = Font.system(size: 16.2, weight: .semibold, design: .monospaced) -} - private enum LoginPalette { static func primaryText(_ colorScheme: ColorScheme) -> Color { colorScheme == .dark diff --git a/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift b/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift index 06b3560b..88763e4f 100644 --- a/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift +++ b/apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift @@ -54,7 +54,7 @@ struct OperatorSnapshotResponse: Decodable, Sendable { } var shouldDisplayInPanel: Bool { - hasVisibleSignal && !isAPIOnlySnapshot + hasVisibleSignal && (activeRunCount > 0 || !isAPIOnlySnapshot) } var warningSummary: String? { @@ -188,16 +188,29 @@ struct OperatorPostReviewLaneStatus: Decodable, Sendable { } struct OperatorRunStatus: Decodable, Identifiable, Sendable { + let projectID: String? let runID: String + let issueID: String? let issueIdentifier: String? let title: String? let status: String? let attemptStatus: String? + let attemptNumber: Int? let phase: String? let waitReason: String? let currentOperation: String? let threadStatus: String? let idleForSeconds: Int? + let protocolIdleForSeconds: Int? + let updatedAt: String? + let lastProgressAt: String? + let nextRetryAt: String? + let lastEventType: String? + let eventCount: Int? + let processAlive: Bool? + let activeLease: Bool? + let branchName: String? + let worktreePath: String? let suspectedStall: Bool let childAgentActivity: OperatorChildAgentActivity? let account: OperatorRunAccountSummary? @@ -258,22 +271,33 @@ struct OperatorRunStatus: Decodable, Identifiable, Sendable { } func isAssigned(to account: CodexAccount) -> Bool { - let runAccounts = ([self.account].compactMap { $0 }) + accounts - - return runAccounts.contains { $0.matches(account) } + self.account?.matches(account) == true } enum CodingKeys: String, CodingKey { + case projectID = "project_id" case runID = "run_id" + case issueID = "issue_id" case issueIdentifier = "issue_identifier" case title case status case attemptStatus = "attempt_status" + case attemptNumber = "attempt_number" case phase case waitReason = "wait_reason" case currentOperation = "current_operation" case threadStatus = "thread_status" case idleForSeconds = "idle_for_seconds" + case protocolIdleForSeconds = "protocol_idle_for_seconds" + case updatedAt = "updated_at" + case lastProgressAt = "last_progress_at" + case nextRetryAt = "next_retry_at" + case lastEventType = "last_event_type" + case eventCount = "event_count" + case processAlive = "process_alive" + case activeLease = "active_lease" + case branchName = "branch_name" + case worktreePath = "worktree_path" case suspectedStall = "suspected_stall" case childAgentActivity = "child_agent_activity" case account @@ -283,16 +307,29 @@ struct OperatorRunStatus: Decodable, Identifiable, Sendable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + projectID = try container.decodeIfPresent(String.self, forKey: .projectID) runID = try container.decodeIfPresent(String.self, forKey: .runID) ?? UUID().uuidString + issueID = try container.decodeIfPresent(String.self, forKey: .issueID) issueIdentifier = try container.decodeIfPresent(String.self, forKey: .issueIdentifier) title = try container.decodeIfPresent(String.self, forKey: .title) status = try container.decodeIfPresent(String.self, forKey: .status) attemptStatus = try container.decodeIfPresent(String.self, forKey: .attemptStatus) + attemptNumber = try container.decodeIfPresent(Int.self, forKey: .attemptNumber) phase = try container.decodeIfPresent(String.self, forKey: .phase) waitReason = try container.decodeIfPresent(String.self, forKey: .waitReason) currentOperation = try container.decodeIfPresent(String.self, forKey: .currentOperation) threadStatus = try container.decodeIfPresent(String.self, forKey: .threadStatus) idleForSeconds = try container.decodeIfPresent(Int.self, forKey: .idleForSeconds) + protocolIdleForSeconds = try container.decodeIfPresent(Int.self, forKey: .protocolIdleForSeconds) + updatedAt = try container.decodeIfPresent(String.self, forKey: .updatedAt) + lastProgressAt = try container.decodeIfPresent(String.self, forKey: .lastProgressAt) + nextRetryAt = try container.decodeIfPresent(String.self, forKey: .nextRetryAt) + lastEventType = try container.decodeIfPresent(String.self, forKey: .lastEventType) + eventCount = try container.decodeIfPresent(Int.self, forKey: .eventCount) + processAlive = try container.decodeIfPresent(Bool.self, forKey: .processAlive) + activeLease = try container.decodeIfPresent(Bool.self, forKey: .activeLease) + branchName = try container.decodeIfPresent(String.self, forKey: .branchName) + worktreePath = try container.decodeIfPresent(String.self, forKey: .worktreePath) suspectedStall = try container.decodeIfPresent(Bool.self, forKey: .suspectedStall) ?? false childAgentActivity = try container.decodeIfPresent( OperatorChildAgentActivity.self, @@ -306,12 +343,32 @@ struct OperatorRunStatus: Decodable, Identifiable, Sendable { struct OperatorChildAgentActivity: Decodable, Sendable { let currentBucket: String? let currentDetail: String? + let currentElapsedSeconds: Int? + let eventCount: Int + let inputTokensCumulative: Int + let inputTokensCurrent: Int? + let inputTokensMax: Int? + let largestToolOutputBytes: Int? + let largestToolOutputTool: String? + let outputTokensCumulative: Int let toolCallCount: Int + let wallSeconds: Int + let buckets: [OperatorChildAgentBucket] enum CodingKeys: String, CodingKey { case currentBucket = "current_bucket" case currentDetail = "current_detail" + case currentElapsedSeconds = "current_elapsed_seconds" + case eventCount = "event_count" + case inputTokensCumulative = "input_tokens_cumulative" + case inputTokensCurrent = "input_tokens_current" + case inputTokensMax = "input_tokens_max" + case largestToolOutputBytes = "largest_tool_output_bytes" + case largestToolOutputTool = "largest_tool_output_tool" + case outputTokensCumulative = "output_tokens_cumulative" case toolCallCount = "tool_call_count" + case wallSeconds = "wall_seconds" + case buckets } init(from decoder: Decoder) throws { @@ -319,7 +376,53 @@ struct OperatorChildAgentActivity: Decodable, Sendable { currentBucket = try container.decodeIfPresent(String.self, forKey: .currentBucket) currentDetail = try container.decodeIfPresent(String.self, forKey: .currentDetail) + currentElapsedSeconds = try container.decodeIfPresent(Int.self, forKey: .currentElapsedSeconds) + eventCount = try container.decodeIfPresent(Int.self, forKey: .eventCount) ?? 0 + inputTokensCumulative = try container.decodeIfPresent(Int.self, forKey: .inputTokensCumulative) ?? 0 + inputTokensCurrent = try container.decodeIfPresent(Int.self, forKey: .inputTokensCurrent) + inputTokensMax = try container.decodeIfPresent(Int.self, forKey: .inputTokensMax) + largestToolOutputBytes = try container.decodeIfPresent(Int.self, forKey: .largestToolOutputBytes) + largestToolOutputTool = try container.decodeIfPresent(String.self, forKey: .largestToolOutputTool) + outputTokensCumulative = try container.decodeIfPresent(Int.self, forKey: .outputTokensCumulative) ?? 0 + toolCallCount = try container.decodeIfPresent(Int.self, forKey: .toolCallCount) ?? 0 + wallSeconds = try container.decodeIfPresent(Int.self, forKey: .wallSeconds) ?? 0 + buckets = try container.decodeIfPresent([OperatorChildAgentBucket].self, forKey: .buckets) ?? [] + } +} + +struct OperatorChildAgentBucket: Decodable, Identifiable, Sendable { + let name: String + let eventCount: Int + let inputTokens: Int + let outputBytes: Int + let outputTokens: Int + let toolCallCount: Int + let wallSeconds: Int + + var id: String { + name + } + + enum CodingKeys: String, CodingKey { + case name + case eventCount = "event_count" + case inputTokens = "input_tokens" + case outputBytes = "output_bytes" + case outputTokens = "output_tokens" + case toolCallCount = "tool_call_count" + case wallSeconds = "wall_seconds" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Activity" + eventCount = try container.decodeIfPresent(Int.self, forKey: .eventCount) ?? 0 + inputTokens = try container.decodeIfPresent(Int.self, forKey: .inputTokens) ?? 0 + outputBytes = try container.decodeIfPresent(Int.self, forKey: .outputBytes) ?? 0 + outputTokens = try container.decodeIfPresent(Int.self, forKey: .outputTokens) ?? 0 toolCallCount = try container.decodeIfPresent(Int.self, forKey: .toolCallCount) ?? 0 + wallSeconds = try container.decodeIfPresent(Int.self, forKey: .wallSeconds) ?? 0 } } diff --git a/apps/decodex-app/Sources/DecodexApp/PanelTypography.swift b/apps/decodex-app/Sources/DecodexApp/PanelTypography.swift new file mode 100644 index 00000000..986315a1 --- /dev/null +++ b/apps/decodex-app/Sources/DecodexApp/PanelTypography.swift @@ -0,0 +1,58 @@ +import SwiftUI + +enum PanelFont { + private static func text( + _ size: CGFloat, + weight: Font.Weight, + design: Font.Design = .default + ) -> Font { + .system(size: size, weight: weight, design: design) + } + + static let headerIcon = text(14.1, weight: .semibold) + static let headerTitle = text(14.8, weight: .semibold) + static let headerSubtitle = text(11.1, weight: .medium) + + static let emptyIcon = text(16.8, weight: .medium) + static let emptyTitle = text(12.2, weight: .semibold) + static let emptyBody = text(10.9, weight: .regular) + static let notice = text(10.6, weight: .regular) + + static let accountName = text(13.1, weight: .semibold) + static let accountDetail = text(10.9, weight: .medium) + + static let summaryIcon = text(10.4, weight: .medium) + static let metricLabel = text(10.4, weight: .medium) + static let metricValue = text(11.9, weight: .semibold) + static let usageLabel = text(10.4, weight: .medium) + static let usageValue = text(10.7, weight: .semibold) + static let tertiary = text(9.7, weight: .medium) + + static let laneTitle = text(11.6, weight: .semibold) + static let laneDetail = text(10.8, weight: .medium) + static let laneStatus = text(10.6, weight: .medium) + static let lanePopoverTitle = text(12.8, weight: .semibold) + static let lanePopoverLabel = text(10.8, weight: .semibold) + static let lanePopoverValue = text(11.0, weight: .semibold) + static let lanePopoverMeta = text(10.6, weight: .medium) + static let lanePopoverChip = text(10.5, weight: .semibold) + + static let iconButton = text(11.2, weight: .semibold) +} + +enum LoginFont { + private static func text( + _ size: CGFloat, + weight: Font.Weight, + design: Font.Design = .default + ) -> Font { + .system(size: size, weight: weight, design: design) + } + + static let title = text(14.6, weight: .semibold) + static let caption = text(10.6, weight: .medium) + static let destination = text(10.8, weight: .semibold) + static let button = text(10.8, weight: .semibold) + static let icon = text(11.4, weight: .semibold) + static let code = text(16.2, weight: .semibold, design: .monospaced) +} diff --git a/apps/decodex/src/orchestrator/entrypoints.rs b/apps/decodex/src/orchestrator/entrypoints.rs index e3cd3b76..4a666175 100644 --- a/apps/decodex/src/orchestrator/entrypoints.rs +++ b/apps/decodex/src/orchestrator/entrypoints.rs @@ -464,6 +464,7 @@ fn run_control_plane_tick( fn run_control_plane_api_only_tick(state_store: &StateStore) -> Result { let registered_projects = state_store.list_projects()?; let mut snapshot = empty_control_plane_snapshot(DEFAULT_OPERATOR_DASHBOARD_RUN_LIMIT); + let mut project_statuses = Vec::new(); if !registered_projects.iter().any(ProjectRegistration::enabled) { add_operator_snapshot_warning(&mut snapshot, "no_enabled_projects"); @@ -471,10 +472,51 @@ fn run_control_plane_api_only_tick(state_store: &StateStore) -> Result { + if let Some(local_status) = project_snapshot.projects.first() { + project_status.active_run_count = local_status.active_run_count; + project_status.retained_worktree_count = local_status.retained_worktree_count; + project_status.waiting_lane_count = local_status.waiting_lane_count; + project_status.attention_count = local_status.attention_count; + project_status.cleanup_blocked_count = local_status.cleanup_blocked_count; + project_status.cleanup_pending_count = local_status.cleanup_pending_count; + project_status.last_activity_at = local_status.last_activity_at.clone(); + project_status.warning_count = + project_status.warning_count.saturating_add(local_status.warning_count); + } else { + project_status.active_run_count = project_snapshot.active_runs.len(); + } + + append_control_plane_project_snapshot(&mut snapshot, project_snapshot); + }, + Err(error) => { + let _ = error; + + project_status.warning_count = project_status.warning_count.saturating_add(1); + add_operator_snapshot_warning(&mut snapshot, "operator_snapshot_build_failed"); + tracing::warn!( + project_id = registration.service_id(), + "API-only operator snapshot local run hydration failed; sensitive runtime details were withheld." + ); + }, + } + } + + project_statuses.push(project_status); + } + + snapshot.projects = project_statuses; snapshot.account_control = global_codex_account_control_status(); Ok(snapshot) diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index f6d10035..9e277c2b 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -1798,12 +1798,85 @@ .account-pool { display: grid; - gap: 0; + gap: 10px; min-width: 0; overflow-x: auto; padding-bottom: 2px; } + .account-pool-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0; + min-width: 0; + padding: 8px 0 8px var(--space-row-indent); + border-bottom: 1px solid color-mix(in srgb, var(--line) 58%, transparent); + color: var(--muted); + } + + .account-pool-metric { + display: grid; + gap: 3px; + min-width: 0; + padding: 0 14px; + border-left: 1px solid var(--line); + font-family: var(--mono); + font-variant-numeric: tabular-nums; + } + + .account-pool-metric:first-child { + padding-left: 0; + border-left: 0; + } + + .account-pool-metric-label { + overflow: hidden; + color: var(--muted); + font-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; + } + + .account-pool-metric-value { + overflow: hidden; + color: var(--muted-strong); + font-size: var(--type-row-title); + font-weight: var(--weight-label); + letter-spacing: 0; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; + } + + .account-pool-metric-value[data-tone="run"] { + color: var(--tone-run); + } + + .account-pool-metric-value[data-tone="warning"] { + color: var(--warning); + } + + .account-pool-metric-value[data-tone="danger"] { + color: var(--danger); + } + + .account-pool-metric-value[data-tone="muted"] { + color: var(--muted); + } + + .account-pool-summary-note { + grid-column: 1 / -1; + margin-top: 2px; + color: var(--muted); + font-family: var(--mono); + font-size: var(--type-caption); + font-weight: var(--weight-label); + line-height: 1.2; + } + .account-pool-list { --account-grid: minmax(220px, 1.12fr) minmax(56px, 0.42fr) repeat(4, minmax(0, 1fr)); --account-gap: var(--space-row-y); @@ -6231,6 +6304,148 @@

Run History

return Math.max(0, Math.min(100, Math.round(number))); } + function formatUsagePercent(value) { + if (value == null || value === "") { + return "-"; + } + const number = Number(value); + if (!Number.isFinite(number)) { + return "-"; + } + + const rounded = Math.round(number); + if (Math.abs(number - rounded) < 0.05) { + return `${rounded}%`; + } + + return `${number.toFixed(1)}%`; + } + + function formatDailyUsageRate(value) { + const percent = formatUsagePercent(value); + return percent === "-" ? "-" : `${percent}/d`; + } + + function formatPercentagePointDelta(value) { + if (value == null || value === "") { + return "-"; + } + const number = Number(value); + if (!Number.isFinite(number)) { + return "-"; + } + + const absValue = Math.abs(number); + const sign = number > 0.05 ? "+" : number < -0.05 ? "-" : ""; + const rounded = Math.round(absValue); + if (Math.abs(absValue - rounded) < 0.05) { + return `${sign}${rounded}pp`; + } + + return `${sign}${absValue.toFixed(1)}pp`; + } + + function codexAccountUsageRecords(account) { + return Array.isArray(account?.usage_records) + ? account.usage_records.filter((record) => record?.date) + : []; + } + + function previousUsageDate(value) { + const match = String(value || "").match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!match) { + return ""; + } + + const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))); + if (Number.isNaN(date.getTime())) { + return ""; + } + date.setUTCDate(date.getUTCDate() - 1); + + return date.toISOString().slice(0, 10); + } + + function usageRecordForDate(account, date) { + return codexAccountUsageRecords(account) + .filter((record) => record.date === date) + .sort( + (left, right) => + Number(right.checked_at_unix_epoch || 0) - + Number(left.checked_at_unix_epoch || 0), + )[0] || null; + } + + function accountPoolUsageEstimate(snapshot) { + return accountApiSnapshot?.usage_estimate || snapshot?.usage_estimate || null; + } + + function accountPoolDayDeltaPercentagePoints(accounts, estimate) { + const measuredAccounts = accounts.filter( + (account) => codexAccountNumber(account?.seven_day_used_percent) != null, + ); + const totalCapacity = codexAccountNumber(estimate?.total_capacity_percent); + const currentPoolUsed = codexAccountNumber(estimate?.total_used_of_capacity_percent); + if (!measuredAccounts.length || !totalCapacity || currentPoolUsed == null) { + return null; + } + + const latestDate = measuredAccounts + .flatMap(codexAccountUsageRecords) + .map((record) => record.date) + .sort() + .at(-1); + if (!latestDate) { + return currentPoolUsed; + } + + const previousDate = previousUsageDate(latestDate); + if (!previousDate) { + return currentPoolUsed; + } + + const previousUsedPercent = measuredAccounts.reduce((total, account) => { + const record = usageRecordForDate(account, previousDate); + return total + (codexAccountNumber(record?.used_percent) || 0); + }, 0); + const previousPoolPercent = (previousUsedPercent / totalCapacity) * 100; + + return currentPoolUsed - previousPoolPercent; + } + + function accountPoolUsageTone(used) { + if (used == null || used === "") { + return "muted"; + } + const value = Number(used); + if (!Number.isFinite(value)) { + return "muted"; + } + if (value >= 90) { + return "danger"; + } + if (value >= 75) { + return "warning"; + } + + return "run"; + } + + function accountPoolDayDeltaTone(delta, used) { + if (delta == null || delta === "") { + return "muted"; + } + const value = Number(delta); + if (!Number.isFinite(value) || Math.abs(value) <= 0.05) { + return "muted"; + } + if (value < -0.05) { + return "muted"; + } + + return accountPoolUsageTone(used); + } + function codexAccountUnixTimestamp(value) { const seconds = codexAccountNumber(value); if (seconds == null || seconds <= 0) { @@ -6647,13 +6862,64 @@

Run History

nodes.accountModeMeta.title = title; } + function renderCodexAccountPoolUsageSummary(accounts, snapshot) { + const estimate = accountPoolUsageEstimate(snapshot); + if (!estimate) { + return ""; + } + + const used = codexAccountNumber(estimate.total_used_of_capacity_percent); + const delta = accountPoolDayDeltaPercentagePoints(accounts, estimate); + const metrics = [ + { + label: "Pool used", + value: formatUsagePercent(used), + tone: accountPoolUsageTone(used), + }, + { + label: "Day Δ", + value: delta == null ? "-" : formatPercentagePointDelta(delta), + tone: accountPoolDayDeltaTone(delta, used), + }, + { + label: "Daily avg", + value: formatDailyUsageRate(estimate.average_daily_pool_percent), + tone: "muted", + }, + ]; + const measured = Number(estimate.account_estimate_count || 0); + const accountCount = Number(estimate.account_count || 0); + const note = + accountCount > 0 && measured > 0 && measured < accountCount + ? `` + : ""; + const label = `Pool usage: ${metrics.map((metric) => `${metric.label} ${metric.value}`).join(", ")}`; + + return ` + + `; + } + function renderCodexAccountPool(accounts, snapshot) { if (!accounts.length) { return ""; } return ` -