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
917 changes: 867 additions & 50 deletions apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift

Large diffs are not rendered by default.

9 changes: 0 additions & 9 deletions apps/decodex-app/Sources/DecodexApp/LoginSheetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 107 additions & 4 deletions apps/decodex-app/Sources/DecodexApp/OperatorSnapshotModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct OperatorSnapshotResponse: Decodable, Sendable {
}

var shouldDisplayInPanel: Bool {
hasVisibleSignal && !isAPIOnlySnapshot
hasVisibleSignal && (activeRunCount > 0 || !isAPIOnlySnapshot)
}

var warningSummary: String? {
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -306,20 +343,86 @@ 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 {
let container = try decoder.container(keyedBy: CodingKeys.self)

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
}
}

Expand Down
58 changes: 58 additions & 0 deletions apps/decodex-app/Sources/DecodexApp/PanelTypography.swift
Original file line number Diff line number Diff line change
@@ -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)
}
52 changes: 48 additions & 4 deletions apps/decodex/src/orchestrator/entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,17 +464,61 @@ fn run_control_plane_tick(
fn run_control_plane_api_only_tick(state_store: &StateStore) -> Result<OperatorStatusSnapshot> {
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");
}

add_operator_snapshot_warning(&mut snapshot, "automation_disabled");

snapshot.projects = registered_projects
.iter()
.map(operator_project_status_from_api_only_registration)
.collect();
for registration in &registered_projects {
let mut project_status = operator_project_status_from_api_only_registration(registration);

if registration.enabled() {
match ServiceConfig::from_path(registration.config_path()).and_then(|project| {
build_operator_status_snapshot(
&project,
state_store,
DEFAULT_OPERATOR_DASHBOARD_RUN_LIMIT,
)
}) {
Ok(project_snapshot) => {
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)
Expand Down
Loading