diff --git a/apps/decodex/src/agent/codex_accounts.rs b/apps/decodex/src/agent/codex_accounts.rs index 1eee1958..d2628f25 100644 --- a/apps/decodex/src/agent/codex_accounts.rs +++ b/apps/decodex/src/agent/codex_accounts.rs @@ -728,6 +728,8 @@ struct AccountUsageSnapshot { impl AccountUsageSnapshot { fn is_limited(&self) -> bool { self.rate_limit_reached_type.is_some() + || self.primary.as_ref().is_some_and(UsageWindow::is_depleted) + || self.secondary.as_ref().is_some_and(UsageWindow::is_depleted) } } @@ -737,6 +739,11 @@ struct UsageWindow { remaining_percent: i64, resets_at_unix_epoch: Option, } +impl UsageWindow { + const fn is_depleted(&self) -> bool { + self.remaining_percent <= 0 + } +} #[derive(Clone, Debug, Eq, PartialEq)] struct CreditsSnapshot { @@ -924,13 +931,20 @@ fn account_candidate_score(candidate: &CodexAccountLogin) -> i64 { let secondary = summary.secondary_remaining_percent.unwrap_or(primary); let mut score = primary.saturating_mul(1_000).saturating_add(secondary.saturating_mul(10)); - if summary.rate_limit_reached_type.is_some() { - score = score.saturating_sub(50_000); + if account_summary_is_limited(summary) { + score = score.saturating_sub(200_000); } score } +fn account_summary_is_limited(summary: &CodexAccountActivitySummary) -> bool { + summary.rate_limit_reached_type.is_some() + || summary.status.to_lowercase().contains("limit") + || summary.primary_remaining_percent == Some(0) + || summary.secondary_remaining_percent == Some(0) +} + fn account_summaries( selected: &CodexAccountLogin, candidates: &[CodexAccountLogin], @@ -957,8 +971,8 @@ const fn is_false(value: &bool) -> bool { #[cfg(test)] mod tests { use crate::agent::codex_accounts::{ - self, CodexAccountActivitySummary, CodexAccountLogin, CreditsSnapshot, Path, UsageWindow, - compare_account_candidates, + self, AccountPoolRecord, CodexAccountActivitySummary, CodexAccountLogin, CodexTokenData, + CreditsSnapshot, Path, UsageWindow, compare_account_candidates, }; #[test] @@ -1037,7 +1051,7 @@ mod tests { } #[test] - fn usage_limit_requires_explicit_rate_limit_signal() { + fn usage_limit_detects_depleted_windows_without_credit_heuristics() { let payload = serde_json::json!({ "plan_type": "pro", "rate_limit": { @@ -1047,7 +1061,7 @@ mod tests { "reset_at": 1_800_018_000 }, "secondary_window": { - "used_percent": 0, + "used_percent": 100, "limit_window_seconds": 604_800, "reset_at": 1_800_604_800 } @@ -1062,9 +1076,65 @@ mod tests { let summary = codex_accounts::usage_snapshot_from_payload(&payload, 1_800_000_000); assert_eq!(summary.primary.as_ref().map(|window| window.remaining_percent), Some(100)); - assert_eq!(summary.secondary.as_ref().map(|window| window.remaining_percent), Some(100)); + assert_eq!(summary.secondary.as_ref().map(|window| window.remaining_percent), Some(0)); assert_eq!(summary.credits.as_ref().map(|credits| credits.has_credits), Some(false)); - assert!(!summary.is_limited()); + assert!(summary.is_limited()); + + let record = AccountPoolRecord { + email: Some(String::from("limited@example.com")), + disabled: false, + cooldown_until_unix_epoch: None, + cooldown_until: None, + auth_mode: Some(String::from("chatgpt")), + openai_api_key: None, + tokens: Some(CodexTokenData { + email: None, + id_token: None, + access_token: String::from("access"), + refresh_token: String::from("refresh"), + account_id: Some(String::from("acct_limited")), + }), + last_refresh: None, + }; + let login = record + .login_from_usage(summary, "not_needed") + .expect("limited usage should still produce an account summary"); + + assert_eq!(login.summary().status, "usage_limited"); + + let available_payload = serde_json::json!({ + "plan_type": "pro", + "rate_limit": { + "primary_window": { + "used_percent": 40, + "limit_window_seconds": 18_000, + "reset_at": 1_800_018_000 + }, + "secondary_window": { + "used_percent": 72, + "limit_window_seconds": 604_800, + "reset_at": 1_800_604_800 + } + }, + "credits": { + "has_credits": false, + "unlimited": false, + "balance": "0" + }, + "rate_limit_reached_type": null + }); + let available_summary = + codex_accounts::usage_snapshot_from_payload(&available_payload, 1_800_000_000); + + assert_eq!( + available_summary.primary.as_ref().map(|window| window.remaining_percent), + Some(60) + ); + assert_eq!( + available_summary.secondary.as_ref().map(|window| window.remaining_percent), + Some(28) + ); + assert!(!available_summary.is_limited()); } #[test] @@ -1102,7 +1172,7 @@ mod tests { } #[test] - fn account_candidate_sort_does_not_penalize_missing_credits() { + fn account_candidate_sort_does_not_penalize_zero_credits_when_windows_available() { let mut candidates = [ CodexAccountLogin { access_token: String::from("a"), diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index 85fe45a0..3eb04353 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -32,6 +32,7 @@ --danger: #c05c5c; --info: #4a73ca; --tone-queue: #4569d4; + --tone-ready: #1d8968; --tone-run: #1489a8; --tone-review: #8a5bc2; --tone-land: #1d8968; @@ -48,10 +49,33 @@ --fast: 140ms; --medium: 220ms; --slow: 380ms; + --type-micro: 9px; + --type-caption: 11px; + --type-label: 12px; + --type-meta: 12px; + --type-body: 13px; + --type-row-title: 13px; + --type-card-title: 15px; + --type-display: 17px; + --weight-label: 500; + --weight-strong: 600; + --tracking-caps: 0; + --space-2xs: 4px; + --space-xs: 6px; + --space-sm: 8px; + --space-account-row-y: 8px; + --space-md: 12px; + --space-row-y: 12px; + --space-panel-head-y: 12px; + --space-row-indent: 18px; + --space-section-x: 20px; + --space-section-y: 16px; + --space-card-y: 16px; --mono: - "SFMono-Regular", "IBM Plex Mono", "Menlo", "Monaco", "Consolas", monospace; + "Menlo", "Monaco", "SFMono-Regular", "SF Mono", ui-monospace, "Cascadia Mono", + "Roboto Mono", "Consolas", monospace; --sans: - "SF Pro Display", "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif; + -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "Helvetica Neue", sans-serif; } :root[data-theme="dark"] { @@ -73,6 +97,7 @@ --danger: #ef8a8a; --info: #83a8ff; --tone-queue: #92a8ff; + --tone-ready: #5cc59f; --tone-run: #67d9ef; --tone-review: #c59aff; --tone-land: #5cc59f; @@ -131,7 +156,7 @@ max-width: 1180px; margin: 0 auto; display: grid; - gap: 16px; + gap: var(--space-panel-head-y); } .masthead { @@ -176,7 +201,7 @@ h1 { margin: 0; - font-size: 1.14rem; + font-size: var(--type-display); letter-spacing: 0; line-height: 1; } @@ -190,12 +215,11 @@ } .table-meta { - font-family: var(--mono); - font-size: 11px; - font-weight: 600; - letter-spacing: 0.06em; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1.35; - text-transform: uppercase; color: var(--muted); } @@ -211,6 +235,57 @@ color: var(--muted); } + .metric-text { + display: inline-flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0 6px; + min-width: 0; + } + + .metric-group { + display: inline-flex; + align-items: baseline; + gap: 4px; + min-width: 0; + white-space: nowrap; + } + + .metric-number { + color: var(--muted-strong); + font-family: var(--mono); + font-size: 0.94em; + font-weight: var(--weight-label); + font-variant-numeric: tabular-nums; + letter-spacing: 0; + line-height: 1; + } + + .metric-label { + color: var(--muted); + font-family: var(--sans); + font-weight: var(--weight-label); + letter-spacing: 0; + } + + .metric-separator { + color: var(--muted); + opacity: 0.62; + } + + .table-meta .metric-number { + color: var(--muted-strong); + } + + .table-meta[data-tone="attention"] .metric-number, + .table-meta[data-tone="attention"] .metric-label { + color: color-mix(in srgb, var(--warning) 82%, var(--text)); + } + + .table-meta[data-tone="active"] .metric-number { + color: var(--muted-strong); + } + .topbar-side { display: flex; gap: 18px; @@ -257,10 +332,10 @@ background: var(--surface-strong); backdrop-filter: blur(18px); list-style: none; - font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.08em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); color: var(--danger); cursor: pointer; box-shadow: var(--shadow); @@ -302,8 +377,8 @@ .notice-hint { color: var(--muted); - font-size: 10px; - letter-spacing: 0.1em; + font-size: var(--type-caption); + letter-spacing: var(--tracking-caps); } .notice-panel { @@ -336,17 +411,17 @@ .notice-item strong { display: block; margin-bottom: 3px; - font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.07em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-caption); + font-weight: var(--weight-strong); + letter-spacing: var(--tracking-caps); color: var(--text); } .notice-item p { margin: 0; color: var(--muted-strong); - font-size: 13px; + font-size: var(--type-body); line-height: 1.38; } @@ -373,10 +448,10 @@ display: inline-flex; align-items: center; justify-content: center; - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); } .theme-option { @@ -467,9 +542,10 @@ align-items: center; min-width: 0; gap: 6px; - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.04em; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1; color: var(--muted); white-space: nowrap; @@ -486,11 +562,18 @@ overflow: hidden; max-width: min(42vw, 320px); color: var(--muted-strong); - font-weight: 600; + font-weight: var(--weight-label); + text-transform: none; text-overflow: ellipsis; white-space: nowrap; } + .transport-meta[data-kind="endpoint"] strong { + font-family: var(--mono); + font-variant-ligatures: none; + letter-spacing: 0; + } + .transport-meta[data-tone="success"] strong { color: var(--success); } @@ -520,11 +603,11 @@ } .flow-stage span { - font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.11em; - text-transform: uppercase; - color: var(--muted); + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); + color: var(--muted-strong); } .flow-stage-labels { @@ -556,9 +639,10 @@ .flow-stage strong { color: var(--text); - font-family: var(--mono); - font-size: 0.78rem; - font-weight: 600; + font-family: var(--sans); + font-size: var(--type-meta); + font-weight: var(--weight-label); + font-variant-numeric: tabular-nums; letter-spacing: 0; min-width: 0; overflow: hidden; @@ -575,6 +659,19 @@ color: var(--accent); } + .flow-stage strong .metric-number { + color: var(--text); + } + + .flow-stage strong .metric-label { + color: var(--muted); + } + + .flow-stage.active strong .metric-number, + .flow-stage.active strong .metric-label { + color: var(--accent); + } + .flow-stage[data-flow-stage="queue"] { --accent: var(--tone-queue); } @@ -618,34 +715,33 @@ flex-wrap: wrap; align-items: center; justify-content: space-between; - gap: 8px 18px; - margin: 20px 20px 0; - padding: 10px 0 8px; + gap: var(--space-sm) var(--space-row-indent); + margin: var(--space-section-y) var(--space-section-x) 0; + padding: var(--space-sm) 0 var(--space-xs); border-top: 1px solid var(--line-strong); - font-family: var(--mono); + font-family: var(--sans); color: var(--muted); } .section-marker:first-child { margin-top: 0; - padding-top: 14px; + padding-top: var(--space-panel-head-y); } - .section-marker span { + .section-marker > span { display: inline-flex; align-items: center; gap: 10px; min-width: 0; - color: var(--text); - font-size: 10px; - font-weight: 650; - letter-spacing: 0.12em; + color: var(--muted-strong); + font-size: var(--type-meta); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1.25; - text-transform: uppercase; white-space: nowrap; } - .section-marker span::before { + .section-marker > span::before { content: ""; flex: 0 0 auto; width: 2px; @@ -653,24 +749,14 @@ background: var(--section-tone); } - .section-marker p { - margin: 0; - min-width: 0; - overflow: hidden; - color: var(--muted); - font-size: 10px; - font-weight: 600; - letter-spacing: 0.08em; - line-height: 1.25; - text-overflow: ellipsis; - text-transform: uppercase; - white-space: nowrap; - } - .section-marker-control { --section-tone: var(--tone-muted); } + .section-marker-projects { + --section-tone: var(--info); + } + .section-marker-execution { --section-tone: var(--tone-run); } @@ -682,7 +768,7 @@ .panel { display: grid; gap: 0; - padding: 0 20px; + padding: 0 var(--space-section-x); border: 0; border-radius: 0; background: transparent; @@ -695,16 +781,10 @@ } #account-pool-panel { - padding-bottom: 12px; + padding-bottom: var(--space-md); background: transparent; } - #account-pool-panel .panel-head { - align-items: center; - padding-top: 16px; - padding-bottom: 10px; - } - #account-pool-panel .panel-body { padding-bottom: 4px; } @@ -718,8 +798,8 @@ flex-wrap: wrap; align-items: end; justify-content: space-between; - gap: 10px; - padding: 20px 0 12px; + gap: var(--space-md); + padding: var(--space-panel-head-y) 0 var(--space-md); border-bottom: 0; transition: color var(--fast) var(--ease), @@ -733,11 +813,10 @@ .panel-head h2 { margin: 0; - font-size: 12px; - font-weight: 650; - letter-spacing: 0.14em; - text-transform: uppercase; - font-family: var(--mono); + font-size: var(--type-meta); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); + font-family: var(--sans); color: var(--muted-strong); transition: color var(--fast) var(--ease); } @@ -747,7 +826,7 @@ } #queue-panel .panel-head { - padding-top: 14px; + padding-top: var(--space-panel-head-y); } .panel-body { @@ -768,7 +847,7 @@ isolation: isolate; position: relative; margin: 0; - padding: 18px 0 18px 18px; + padding: var(--space-card-y) 0 var(--space-card-y) var(--space-row-indent); background: transparent; border: 0; border-bottom: 1px solid var(--line); @@ -793,16 +872,16 @@ } .card-list > :first-child { - padding-top: 18px; + padding-top: var(--space-card-y); } .card-list > :last-child { border-bottom: 0; - padding-bottom: 18px; + padding-bottom: var(--space-card-y); } #active-panel .row-title h3 { - font-size: 17px; + font-size: var(--type-card-title); } .action-card::before, @@ -847,7 +926,7 @@ .row-title h3, .row-title h4 { margin: 0; - font-size: 16px; + font-size: var(--type-card-title); letter-spacing: 0; line-height: 1.26; } @@ -855,12 +934,12 @@ .worktree-card .row-title h3, .worktree-card .row-title h4 { color: var(--muted-strong); - font-size: 14px; - font-weight: 650; + font-size: var(--type-row-title); + font-weight: var(--weight-label); } .worktree-card .row-summary { - font-size: 13px; + font-size: var(--type-body); line-height: 1.48; } @@ -869,13 +948,13 @@ } .worktree-card .field-label { - font-size: 10px; + font-size: var(--type-caption); } .worktree-card .field-value { color: var(--muted-strong); - font-size: 12px; - font-weight: 600; + font-size: var(--type-meta); + font-weight: var(--weight-label); } .kicker, @@ -883,10 +962,10 @@ display: flex; flex-wrap: wrap; gap: 8px; - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.06em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); color: var(--muted); } @@ -904,8 +983,8 @@ .run-title { margin: 0; color: var(--text); - font-size: 16px; - font-weight: 700; + font-size: var(--type-card-title); + font-weight: var(--weight-strong); line-height: 1.28; } @@ -913,10 +992,10 @@ display: flex; flex-wrap: wrap; gap: 8px 12px; - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.06em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); color: var(--muted); } @@ -924,7 +1003,7 @@ margin: 10px 0 0; max-width: 78ch; color: var(--muted-strong); - font-size: 14px; + font-size: var(--type-row-title); line-height: 1.52; } @@ -933,14 +1012,14 @@ flex-wrap: wrap; gap: 12px; margin-top: 12px; - font-family: var(--mono); - font-size: 12px; + font-family: var(--sans); + font-size: var(--type-meta); color: var(--muted); } .status-line strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .activity-line { @@ -948,8 +1027,8 @@ flex-wrap: wrap; gap: 5px 14px; margin-top: 8px; - font-family: var(--mono); - font-size: 11px; + font-family: var(--sans); + font-size: var(--type-label); line-height: 1.4; color: var(--muted); } @@ -960,19 +1039,77 @@ .activity-line strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .project-scope-list { display: grid; gap: 0; + min-width: 0; + overflow-x: auto; + padding-bottom: 2px; + } + + .project-scope-list.has-registered-projects { + gap: 0; + } + + .project-table { + --project-grid: + minmax(150px, 0.52fr) minmax(300px, 1.28fr) minmax(112px, 0.36fr) + minmax(116px, 0.34fr); + display: grid; + min-width: 640px; + background: transparent; + } + + .project-table-guide { + display: grid; + grid-template-columns: var(--project-grid); + align-items: end; + gap: var(--space-row-y); + min-width: 0; + padding: 0 0 var(--space-sm) var(--space-row-indent); + border-bottom: 1px solid color-mix(in srgb, var(--line) 58%, transparent); + color: var(--muted); + font-family: var(--sans); + font-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); + line-height: 1.15; + } + + .project-table-guide span { + min-width: 0; + text-align: center; + } + + .project-table-guide .project-location-head { + text-align: left; + } + + .project-column-head { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + min-width: 0; + } + + .project-column-head.project-location-head { + justify-content: flex-start; + } + + .project-table-list { + display: grid; + min-width: 0; } .project-scope-list.is-empty .empty-state { grid-template-columns: minmax(142px, max-content) minmax(0, 1fr); max-width: none; min-height: 34px; - padding: 6px 0 8px 14px; + padding: var(--space-xs) 0 var(--space-sm) var(--space-row-y); border-left: 2px solid var(--tone-muted); } @@ -999,8 +1136,8 @@ width: fit-content; max-width: 100%; color: var(--muted-strong); - font-family: var(--mono); - font-size: 11px; + font-family: var(--sans); + font-size: var(--type-label); line-height: 1.35; overflow-wrap: anywhere; } @@ -1010,18 +1147,20 @@ position: relative; isolation: isolate; display: grid; - grid-template-columns: minmax(0, 1fr) minmax(240px, 0.46fr); - gap: 14px 28px; + grid-template-areas: + "identity location activity work"; + grid-template-columns: var(--project-grid); + gap: 4px var(--space-row-y); align-items: center; min-width: 0; - padding: 18px 0 18px 18px; + padding: var(--space-row-y) 0 var(--space-row-y) var(--space-row-indent); border-bottom: 1px solid var(--line); background: transparent; color: var(--muted-strong); transition: background-color var(--fast) var(--ease); } - .project-scope-list > .project-entry:last-child { + .project-table-list > .project-entry:last-child { border-bottom: 0; } @@ -1035,8 +1174,8 @@ position: absolute; z-index: 2; left: 0; - top: 18px; - bottom: 18px; + top: var(--space-row-y); + bottom: var(--space-row-y); width: 3px; background: var(--project-accent); box-shadow: none; @@ -1093,6 +1232,10 @@ --project-accent: var(--tone-land); } + .project-entry.tone-ready { + --project-accent: var(--tone-ready); + } + .project-entry.tone-wait { --project-accent: var(--tone-wait); } @@ -1106,24 +1249,37 @@ } .project-entry-main { + grid-area: identity; display: grid; - gap: 7px; + justify-self: center; min-width: 0; + text-align: center; } .project-kicker { color: var(--project-accent); + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: 0; + line-height: 1.2; + } + + .project-activity { + grid-area: activity; + justify-self: center; + color: var(--muted); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); letter-spacing: 0; line-height: 1.2; - text-transform: uppercase; } .project-title-line { display: flex; flex-wrap: wrap; align-items: baseline; + justify-content: center; gap: 8px 12px; min-width: 0; } @@ -1131,82 +1287,80 @@ .project-title-line strong { min-width: 0; color: var(--text); - font-size: 17px; + font-family: var(--mono); + font-size: var(--type-label); + font-weight: var(--weight-label); line-height: 1.26; overflow-wrap: anywhere; } - .project-health { - color: var(--project-accent); - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0; - text-transform: uppercase; - } - .project-path { + grid-area: location; + justify-self: stretch; + display: flex; + align-items: baseline; min-width: 0; - color: var(--muted-strong); + color: var(--muted); font-family: var(--mono); - font-size: 12px; + font-size: var(--type-caption); letter-spacing: 0; - line-height: 1.45; - overflow-wrap: anywhere; + line-height: 1.25; + overflow: hidden; + text-align: left; + white-space: nowrap; } - .project-meta-line { - display: flex; - flex-wrap: wrap; - gap: 6px 16px; + .project-path-prefix { min-width: 0; - font-family: var(--mono); - font-size: 12px; - line-height: 1.45; - color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + } + + .project-path-tail { + flex: 0 0 auto; + color: var(--muted-strong); + font-size: var(--type-label); + font-weight: var(--weight-label); } .project-stat-line { - display: grid; - grid-template-columns: repeat(3, minmax(68px, max-content)); - gap: 7px 16px; - justify-content: end; - justify-items: end; + grid-area: work; + display: inline-flex; + justify-self: center; + align-items: baseline; + justify-content: center; + gap: 0; min-width: 0; color: var(--muted); - font-family: var(--mono); - text-align: right; - } - - .project-stat-line strong { - color: var(--text); - font-size: 16px; - font-weight: 700; - line-height: 1; + font-variant-numeric: tabular-nums; + text-align: center; } - .project-stat-line span { - display: grid; + .project-work-ratio { + display: inline-flex; + align-items: baseline; + justify-content: center; gap: 3px; min-width: 0; - font-size: 10px; + font-size: var(--type-label); letter-spacing: 0; line-height: 1.2; - text-transform: uppercase; + white-space: nowrap; } - .project-meta-line span { - min-width: 0; - white-space: nowrap; + .project-work-ratio strong { + color: var(--muted-strong); + font-family: var(--mono); + font-size: var(--type-label); + font-weight: var(--weight-label); + line-height: 1; } - .project-side { - display: grid; - gap: 9px; - justify-items: end; - min-width: 0; + .project-work-separator { + color: var(--muted); + opacity: 0.74; } - .project-action-line, .run-control-line { display: flex; flex-wrap: wrap; @@ -1231,12 +1385,11 @@ border-radius: 999px; background: var(--surface-muted); color: var(--muted-strong); - font-family: var(--mono); - font-size: 10px; - font-weight: 650; - letter-spacing: 0.06em; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1; - text-transform: uppercase; transition: background-color var(--fast) var(--ease), border-color var(--fast) var(--ease), @@ -1281,16 +1434,16 @@ .timing-label { font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.08em; - text-transform: uppercase; + font-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); color: var(--muted); } .timing-value { margin-top: 4px; font-family: var(--mono); - font-size: 12px; + font-size: var(--type-meta); color: var(--text); overflow: hidden; text-overflow: ellipsis; @@ -1304,21 +1457,9 @@ overflow: hidden; } - .account-pool-tools { - display: flex; - align-items: center; - justify-content: flex-end; - min-width: 0; - } - - .panel-head > .account-pool-title { - display: inline-flex; - align-items: center; - gap: 7px; - min-width: 0; - } - - .account-privacy-toggle { + .account-privacy-toggle, + .project-location-toggle, + .project-work-info { display: inline-flex; align-items: center; justify-content: center; @@ -1335,36 +1476,134 @@ color var(--fast) var(--ease); } - .account-privacy-toggle svg { + .project-work-info { + cursor: help; + } + + .project-work-info-wrap { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .project-work-tooltip { + position: absolute; + z-index: 24; + top: calc(100% + 7px); + right: 0; + width: max-content; + max-width: 220px; + padding: 5px 7px; + border: 1px solid var(--line-strong); + border-radius: var(--radius-sm); + background: var(--surface); + box-shadow: 0 8px 20px color-mix(in srgb, var(--shadow) 20%, transparent); + color: var(--text); + font-family: var(--sans); + font-size: var(--type-caption); + font-weight: var(--weight-label); + line-height: 1.25; + opacity: 0; + pointer-events: none; + text-align: left; + transform: translateY(-2px); + transition: + opacity var(--fast) var(--ease), + transform var(--fast) var(--ease); + } + + .project-work-info-wrap:hover .project-work-tooltip, + .project-work-info:focus-visible + .project-work-tooltip, + .project-work-info.is-open + .project-work-tooltip { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + } + + .account-privacy-toggle svg, + .project-location-toggle svg, + .project-work-info svg { display: block; width: 14px; height: 14px; stroke-width: 2; } - .account-privacy-toggle .account-eye-open { + .account-privacy-toggle .account-eye-open, + .project-location-toggle .account-eye-open { display: none; } - .account-privacy-toggle.is-on .account-eye-open { + .account-privacy-toggle.is-on .account-eye-open, + .project-location-toggle.is-on .account-eye-open { display: block; } - .account-privacy-toggle.is-on .account-eye-off { + .account-privacy-toggle.is-on .account-eye-off, + .project-location-toggle.is-on .account-eye-off { display: none; } - .account-privacy-toggle:hover { + .account-privacy-toggle:hover, + .project-location-toggle:hover, + .project-work-info:hover { + background: var(--surface-muted); + color: var(--text); + } + + .account-privacy-toggle:focus-visible, + .project-location-toggle:focus-visible, + .project-work-info:focus-visible { + color: var(--text); + outline: 2px solid var(--info); + outline-offset: 2px; + } + + .project-filter-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--muted-strong); + cursor: pointer; + transition: + background-color var(--fast) var(--ease), + color var(--fast) var(--ease); + } + + .project-filter-toggle svg { + display: block; + width: 13px; + height: 13px; + stroke-width: 2; + } + + .project-filter-toggle.is-on { + color: var(--info); + } + + .project-filter-toggle:hover { background: var(--surface-muted); color: var(--text); } - .account-privacy-toggle:focus-visible { + .project-filter-toggle:focus-visible { color: var(--text); outline: 2px solid var(--info); outline-offset: 2px; } + .project-filter-toggle[disabled] { + cursor: not-allowed; + opacity: 0.45; + } + .account-use-line { display: flex; flex-wrap: wrap; @@ -1374,14 +1613,21 @@ padding-top: 10px; border-top: 1px solid var(--line); min-width: 0; - font-family: var(--mono); - font-size: 11px; + font-family: var(--sans); + font-size: var(--type-label); color: var(--muted); } + .account-use-line .account-name.is-machine, + .account-use-line .machine-text { + font-family: var(--mono); + font-variant-ligatures: none; + letter-spacing: 0; + } + .account-use-line strong { color: var(--text); - font-weight: 650; + font-weight: var(--weight-label); } .account-use-line span { @@ -1399,7 +1645,7 @@ gap: 5px 10px; min-width: 0; font-family: var(--mono); - font-size: 10px; + font-size: var(--type-caption); color: var(--muted); } @@ -1418,7 +1664,7 @@ .account-pool-list { --account-grid: minmax(220px, 1.12fr) minmax(56px, 0.42fr) repeat(4, minmax(0, 1fr)); - --account-gap: 14px; + --account-gap: var(--space-row-y); display: grid; min-width: 760px; background: transparent; @@ -1430,15 +1676,14 @@ align-items: end; gap: var(--account-gap); min-width: 0; - padding: 0 0 7px 18px; + padding: 0 0 var(--space-sm) var(--space-row-indent); border-bottom: 1px solid color-mix(in srgb, var(--line) 58%, transparent); color: var(--muted); - font-family: var(--mono); - font-size: 10px; - font-weight: 650; - letter-spacing: 0.08em; + font-family: var(--sans); + font-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1.15; - text-transform: uppercase; } .account-pool-sort { @@ -1462,6 +1707,15 @@ transition: color var(--fast) var(--ease); } + .account-pool-heading { + justify-self: center; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + min-width: 0; + } + .account-pool-sort:hover, .account-pool-sort.is-active { color: var(--text); @@ -1509,7 +1763,7 @@ gap: 4px var(--account-gap); min-width: 0; min-height: 42px; - padding: 10px 0 10px 18px; + padding: var(--space-account-row-y) 0 var(--space-account-row-y) var(--space-row-indent); border-bottom: 1px solid var(--line); background: transparent; transition: @@ -1531,8 +1785,8 @@ position: absolute; z-index: 2; left: 0; - top: 9px; - bottom: 9px; + top: calc(var(--space-account-row-y) - 1px); + bottom: calc(var(--space-account-row-y) - 1px); width: 3px; background: var(--account-accent); box-shadow: none; @@ -1566,6 +1820,10 @@ --account-accent: var(--success); } + .account-row.is-ready { + --account-accent: var(--success); + } + .account-row.is-selected::before { box-shadow: 0 0 10px color-mix(in srgb, var(--account-accent) 22%, transparent); } @@ -1614,18 +1872,24 @@ justify-self: stretch; gap: 6px; min-width: 0; - font-family: var(--mono); + font-family: var(--sans); text-align: center; } + .account-row-id.is-machine { + font-family: var(--mono); + font-variant-ligatures: none; + letter-spacing: 0; + } + .account-row-id strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); - font-size: 13px; - font-weight: 650; + font-size: var(--type-label); + font-weight: var(--weight-label); } .account-name-reroll { @@ -1671,8 +1935,8 @@ overflow: hidden; text-overflow: ellipsis; color: var(--muted); - font-family: var(--mono); - font-size: 11px; + font-family: var(--sans); + font-size: var(--type-label); font-weight: 500; line-height: 1.2; text-align: center; @@ -1715,9 +1979,9 @@ .account-window-label { display: none; color: var(--muted); - font-size: 10px; - font-weight: 650; - letter-spacing: 0.03em; + font-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); } .account-window > strong { @@ -1725,8 +1989,8 @@ grid-column: 1; grid-row: 1; color: var(--text); - font-size: 13px; - font-weight: 650; + font-size: var(--type-label); + font-weight: var(--weight-label); line-height: 1.15; } @@ -1747,7 +2011,7 @@ text-overflow: ellipsis; color: var(--muted); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); line-height: 1.08; white-space: nowrap; } @@ -1760,7 +2024,7 @@ text-overflow: ellipsis; color: var(--muted); font-family: var(--mono); - font-size: 10px; + font-size: var(--type-caption); line-height: 1.12; white-space: nowrap; } @@ -1773,8 +2037,8 @@ min-width: 0; width: auto; text-align: center; - font-family: var(--mono); - font-size: 11px; + font-family: var(--sans); + font-size: var(--type-label); color: var(--muted); } @@ -1786,7 +2050,7 @@ justify-content: center; min-width: 0; font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); line-height: 1.2; text-align: center; white-space: nowrap; @@ -1799,7 +2063,7 @@ .account-row-credit strong { color: var(--text); - font-weight: 650; + font-weight: var(--weight-label); } .account-row-credit.is-danger strong { @@ -1812,7 +2076,7 @@ gap: 6px; min-width: 0; color: var(--muted-strong); - font-weight: 650; + font-weight: var(--weight-label); line-height: 1.25; white-space: nowrap; transition: @@ -1832,7 +2096,8 @@ transform var(--medium) var(--ease); } - .account-row.is-selected .account-status { + .account-row.is-selected .account-status, + .account-row.is-ready .account-status { color: var(--account-accent); } @@ -1859,7 +2124,7 @@ gap: 10px 16px; min-width: 0; font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); line-height: 1.4; color: var(--muted); } @@ -1877,7 +2142,7 @@ .child-activity-head strong, .child-context strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .child-share-list { @@ -1893,7 +2158,7 @@ gap: 10px; min-width: 0; font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); color: var(--muted); } @@ -1930,7 +2195,7 @@ .child-bucket-value { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); justify-self: end; width: var(--child-bucket-value-column); min-width: 0; @@ -1954,7 +2219,7 @@ padding-top: 8px; border-top: 1px solid var(--line); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); color: var(--muted); } @@ -1991,7 +2256,7 @@ .child-bucket-signal strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .child-bucket-bar { @@ -2013,10 +2278,10 @@ display: inline-flex; align-items: center; gap: 6px; - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); } .status-label::before { @@ -2047,6 +2312,11 @@ color: var(--tone-queue); } + .tone-ready { + --accent: var(--tone-ready); + color: var(--tone-ready); + } + .tone-run { --accent: var(--tone-run); color: var(--tone-run); @@ -2150,18 +2420,19 @@ .queue-group-header h4 { margin: 0; - font-size: 0.76rem; - font-family: var(--mono); - letter-spacing: 0.1em; - text-transform: uppercase; + font-size: var(--type-meta); + font-family: var(--sans); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); color: var(--muted); } .queue-group-count { - font-family: var(--mono); - font-size: 0.76rem; - letter-spacing: 0.08em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-meta); + font-weight: var(--weight-label); + font-variant-numeric: tabular-nums; + letter-spacing: var(--tracking-caps); color: var(--muted); } @@ -2172,17 +2443,17 @@ } .field-label { - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.09em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); color: var(--muted); } .field-value { font-family: var(--mono); - font-size: 13px; - font-weight: 600; + font-size: var(--type-body); + font-weight: var(--weight-label); font-variant-numeric: tabular-nums; line-height: 1.35; word-break: break-word; @@ -2191,7 +2462,7 @@ .field-value.is-time, .field-value.is-muted { - font-size: 12px; + font-size: var(--type-meta); font-weight: 500; color: var(--muted-strong); } @@ -2204,7 +2475,7 @@ padding-top: 10px; border-top: 1px solid var(--line); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); line-height: 1.45; color: var(--muted); } @@ -2216,7 +2487,7 @@ .attention-facts strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } details { @@ -2227,10 +2498,10 @@ summary { cursor: pointer; - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); color: var(--muted); user-select: none; transition: @@ -2313,7 +2584,7 @@ padding: 8px 10px; background: var(--surface-muted); color: var(--muted-strong); - font-size: 12px; + font-size: var(--type-meta); } .attempt-row span { @@ -2349,16 +2620,25 @@ display: none; } + .fold-panel > summary.panel-head:focus { + outline: none; + } + + .fold-panel > summary.panel-head:focus-visible { + outline: 2px solid var(--info); + outline-offset: 2px; + } + .fold-panel.is-empty > .panel-body { display: block; } .summary-tools { display: grid; - grid-template-columns: minmax(0, max-content) 12px; + grid-template-columns: minmax(0, max-content) 14px; align-items: center; justify-content: end; - gap: 12px; + gap: var(--space-md); text-align: right; } @@ -2370,43 +2650,24 @@ display: inline-flex; align-items: center; justify-content: center; - position: relative; width: 14px; height: 14px; color: var(--muted); + font-family: var(--mono); + font-size: var(--type-row-title); + font-weight: var(--weight-label); + letter-spacing: 0; + line-height: 1; transition: - color var(--fast) var(--ease), - transform var(--slow) var(--ease); - } - - .fold-indicator::before, - .fold-indicator::after { - content: ""; - position: absolute; - left: 50%; - top: 50%; - width: 12px; - height: 1.5px; - border-radius: 999px; - background: currentColor; - transform: translate(-50%, -50%); - transition: - opacity var(--slow) var(--ease), - transform var(--slow) var(--ease), - background-color var(--fast) var(--ease); - } - - .fold-indicator::after { - transform: translate(-50%, -50%) rotate(90deg); + color var(--fast) var(--ease); } - .fold-panel[data-detail-state="open"] .fold-indicator { - transform: rotate(180deg); + .fold-indicator::before { + content: "+"; } - .fold-panel[data-detail-state="open"] .fold-indicator::after { - opacity: 0; - transform: translate(-50%, -50%) rotate(0deg); + details[data-detail-state="open"] > summary .fold-indicator::before { + content: "-"; } .fold-panel > summary:hover .fold-indicator { @@ -2417,9 +2678,9 @@ display: grid; grid-template-columns: minmax(118px, max-content); align-items: center; - gap: 4px 12px; + gap: var(--space-2xs) var(--space-md); max-width: 640px; - padding: 4px 0 8px; + padding: var(--space-2xs) 0 var(--space-sm); border: 0; border-radius: 0; background: transparent; @@ -2427,26 +2688,25 @@ } .card-list > .empty-state:first-child:last-child { - padding-top: 4px; - padding-bottom: 8px; + padding-top: var(--space-2xs); + padding-bottom: var(--space-sm); border-bottom: 0; } .empty-state strong { display: block; color: var(--muted); - font-family: var(--mono); - font-size: 10px; - font-weight: 500; - letter-spacing: 0.07em; + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1.3; - text-transform: uppercase; } .empty-state .empty-copy { max-width: 48ch; color: var(--muted); - font-size: 11px; + font-size: var(--type-label); line-height: 1.35; } @@ -2456,6 +2716,8 @@ .mono { font-family: var(--mono); + font-variant-ligatures: none; + letter-spacing: 0; } @keyframes reveal-panel { @@ -2517,9 +2779,9 @@ .project-stat-line { grid-template-columns: repeat(2, minmax(72px, max-content)); - justify-content: start; - justify-items: start; - text-align: left; + justify-content: center; + justify-items: center; + text-align: center; } .timing-strip { @@ -2533,7 +2795,7 @@ .account-pool-guide { padding-left: 18px; - font-size: 9px; + font-size: var(--type-micro); } .account-row { @@ -2542,12 +2804,6 @@ "meta meta meta meta meta meta"; } - .account-pool-tools { - justify-content: end; - justify-self: end; - text-align: right; - } - .account-window { gap: 4px; } @@ -2693,6 +2949,13 @@ gap: 3px; padding: 6px 0 10px 12px; } + + .project-table { + --project-grid: + minmax(150px, 1fr) minmax(70px, 0.36fr) minmax(88px, 0.46fr) + minmax(168px, 0.72fr); + min-width: 520px; + } } @media (max-width: 520px) { @@ -2760,46 +3023,33 @@

Decodex

- Control Plane -

Accounts · Projects

+ Accounts
-
- - -
-
-
-
-

Projects

-
-

-
+
+ + Projects + + +
+
-
+
@@ -2809,7 +3059,6 @@

Projects

aria-label="Execution group" > Execution -

Running · Intake

@@ -2841,14 +3090,13 @@

Intake Queue

aria-label="Closeout group" > Closeout -

Review · Recovery · History

Review & Landing

-

Waiting for snapshot

+

Snapshot pending

@@ -2908,7 +3156,8 @@

Run History

const ACCOUNT_PRIVACY_STORAGE_KEY = "decodex.operator.accountPrivacy"; const ACCOUNT_NAME_OFFSET_STORAGE_KEY = "decodex.operator.accountNameOffsets"; const ACCOUNT_POOL_SORT_STORAGE_KEY = "decodex.operator.accountSort"; - const DASHBOARD_SUBSCRIPTION_STORAGE_KEY = "decodex.operator.dashboardSubscription"; + const PROJECT_FILTER_STORAGE_KEY = "decodex.operator.projectFilter"; + const PROJECT_LOCATION_PRIVACY_STORAGE_KEY = "decodex.operator.projectLocationPrivacy"; const ACCOUNT_POOL_SORT_COLUMNS = [ ["account", "Account"], ["plan", "Plan"], @@ -3003,14 +3252,13 @@

Run History

}, sectionMarkers: { control: document.getElementById("section-marker-control"), + projects: document.getElementById("section-marker-projects"), execution: document.getElementById("section-marker-execution"), aftercare: document.getElementById("section-marker-aftercare"), }, projectOverview: document.getElementById("project-overview"), - projectsMeta: document.getElementById("projects-meta"), + projectFilterToggle: document.getElementById("project-filter-toggle"), accountPool: document.getElementById("account-pool"), - accountPoolMeta: document.getElementById("account-pool-meta"), - accountPrivacyToggle: document.getElementById("account-privacy-toggle"), queuedCandidates: document.getElementById("queued-candidates"), queuedMeta: document.getElementById("queued-meta"), activeRuns: document.getElementById("active-runs"), @@ -3033,7 +3281,7 @@

Run History

error: false, lastEventAt: null, }; - let dashboardSubscription = loadDashboardSubscription(); + let dashboardSubscription = normalizeDashboardSubscription(); let dashboardControlEvents = []; let dashboardControlRequestCounter = 0; const themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); @@ -3042,6 +3290,9 @@

Run History

let accountEmailsHidden = loadAccountPrivacy(); let accountNameOffsets = loadAccountNameOffsets(); let accountPoolSort = loadAccountPoolSort(); + let projectFilterMode = loadProjectFilterMode(); + let projectLocationsHidden = loadProjectLocationPrivacy(); + let projectWorkInfoOpen = false; const detailDisclosureState = new Map(); const detailAnimationTimers = new WeakMap(); @@ -3049,7 +3300,8 @@

Run History

primary: ["accountPool", "projects", "active", "queue", "review", "worktrees", "recent"], }; const DASHBOARD_SECTION_GROUPS = [ - { marker: "control", panels: ["accountPool", "projects"] }, + { marker: "control", panels: ["accountPool"] }, + { marker: "projects", panels: ["projects"] }, { marker: "execution", panels: ["active", "queue"] }, { marker: "aftercare", panels: ["review", "worktrees", "recent"] }, ]; @@ -3060,7 +3312,7 @@

Run History

runningInlineMetaPlural: "already running", protocolEvent: "Protocol event", staleClosed: "closed labels", - waitingSnapshot: "Waiting for snapshot", + waitingSnapshot: "Snapshot pending", }; function escapeHtml(value) { @@ -3082,6 +3334,53 @@

Run History

}); } + function metricTokenParts(token) { + const match = String(token).trim().match(/^(-?\d[\d.,]*(?:[a-zA-Z%]+)?)(?:\s+(.+))?$/); + if (!match) { + return { label: token }; + } + + return { + number: match[1], + label: match[2] || "", + }; + } + + function renderMetricGroup(token) { + const parts = metricTokenParts(token); + if (parts.number == null) { + return `${escapeHtml(parts.label)}`; + } + + const label = parts.label + ? `${escapeHtml(parts.label)}` + : ""; + return `${escapeHtml(parts.number)}${label}`; + } + + function renderMetricText(text) { + const tokens = String(text) + .split(" · ") + .map((token) => token.trim()) + .filter(Boolean); + + if (!tokens.length) { + return ""; + } + + return `${tokens + .map((token, index) => { + const separator = + index === 0 ? "" : ' · '; + return `${separator}${renderMetricGroup(token)}`; + }) + .join("")}`; + } + + function setMetricText(node, text) { + node.innerHTML = renderMetricText(text); + } + function humanizeToken(value) { if (!value) { return "none"; @@ -3227,25 +3526,44 @@

Run History

if (snapshotError || readiness.tone === "danger") { return { - label: "unavailable", + label: "Unavailable", tone: "danger", title: snapshotError || readiness.copy, }; } return { - label: "pending", + label: "Pending", tone: readiness.tone === "warning" ? "warning" : "muted", title: readiness.copy, }; } + function topbarReadinessLabel(label) { + switch (label) { + case "Snapshot ready": + return "Ready"; + case "Snapshot stale": + return "Stale"; + case "State degraded": + return "Degraded"; + case "Tracker sync paused": + return "Sync paused"; + case "Listener down": + return "Listener down"; + case "No snapshot": + return "No snapshot"; + default: + return label; + } + } + function dashboardStreamMeta() { if (!dashboardStreamState.supported) { return { label: "polling", tone: "muted", - title: "Browser WebSocket is unavailable; polling remains active.", + title: "WebSocket unavailable; polling active.", }; } @@ -3255,7 +3573,7 @@

Run History

tone: "success", title: dashboardStreamState.lastEventAt ? `Last event ${formatRelativeTimestamp(dashboardStreamState.lastEventAt)}` - : "Dashboard WebSocket connected.", + : "WebSocket connected.", }; } @@ -3263,14 +3581,14 @@

Run History

return { label: "reconnecting", tone: "warning", - title: "Polling remains active while the WebSocket reconnects.", + title: "WebSocket reconnecting; polling active.", }; } return { label: "starting", tone: "muted", - title: "Dashboard WebSocket is connecting.", + title: "WebSocket connecting.", }; } @@ -3453,6 +3771,13 @@

Run History

); } + function runWaitReasonShowsExecutionProgress(run) { + return ( + run.phase === "executing" && + ["model_execution", "tool_execution", "protocol_activity"].includes(run.wait_reason) + ); + } + function toneForRun(run) { if ( runNeedsAttention(run) || @@ -3467,8 +3792,11 @@

Run History

if (runTelemetryMissing(run) || runProcessStoppedWhileActive(run)) { return "tone-wait"; } + if (runWaitReasonShowsExecutionProgress(run)) { + return "tone-run"; + } if ( - run.wait_reason || + (run.wait_reason && !runWaitReasonShowsExecutionProgress(run)) || run.phase === "retry_backoff" || run.phase === "waiting_continuation" ) { @@ -3532,7 +3860,7 @@

Run History

return "tone-run"; } if (candidate.classification === "ready") { - return "tone-queue"; + return "tone-ready"; } if (candidate.classification === "claimed") { return "tone-retained"; @@ -3581,7 +3909,7 @@

Run History

function summarizeQueuedCandidate(candidate) { if (candidate.display_classification === "owned_active") { - return `Already shown in ${COPY.runningLane}; not counted in the queue.`; + return `Shown in ${COPY.runningLane}; excluded from queue.`; } if (candidate.attention?.summary) { return candidate.attention.summary; @@ -3590,23 +3918,23 @@

Run History

case "eligible_for_dispatch": return "Ready to start."; case "shared_claim_present": - return "Claimed by automation, but no live lane is visible here."; + return "Claimed; no live local lane visible."; case "open_tracker_blockers": return candidate.blocker_identifiers.length ? `Blocked by ${candidate.blocker_identifiers.join(", ")}.` - : "Blocked by unresolved dependencies in tracker."; + : "Blocked by tracker dependencies."; case "missing_dispatch_briefing": return "Missing dispatch brief."; case "global_concurrency_exhausted": - return "Waiting for an agent slot; it can start after a lane finishes."; + return "Waiting for a free agent slot."; case "issue_opted_out": return "Automation is disabled for this issue."; case "issue_needs_attention": - return "Needs-attention label is set; operator action required."; + return "Needs-attention label set."; case "non_startable_state": return `${candidate.state} cannot start.`; case "terminal_state": - return "Closed in tracker but still has an automation label."; + return "Closed in tracker; automation label remains."; default: return humanizeToken(candidate.reason); } @@ -3617,7 +3945,7 @@

Run History

return COPY.runningInline; } if (candidate.reason === "global_concurrency_exhausted") { - return "Waiting for capacity"; + return "At capacity"; } return humanizeToken(candidate.classification); @@ -3666,7 +3994,7 @@

Run History

function queueClassificationGroups(items) { const groups = [ ["ready", "Ready"], - ["waiting", "Waiting for capacity"], + ["waiting", "Waiting"], ["claimed", "Claimed"], ["blocked", "Blocked"], ]; @@ -3797,34 +4125,71 @@

Run History

} } - function isAccountPoolSortKey(value) { - return ACCOUNT_POOL_SORT_COLUMNS.some(([key]) => key === value); - } - - function loadAccountPoolSort() { + function isAccountPoolSortKey(value) { + return ACCOUNT_POOL_SORT_COLUMNS.some(([key]) => key === value); + } + + function loadAccountPoolSort() { + try { + const stored = window.localStorage.getItem(ACCOUNT_POOL_SORT_STORAGE_KEY); + const parsed = stored ? JSON.parse(stored) : {}; + const key = String(parsed?.key || ""); + const direction = String(parsed?.direction || "asc"); + if ( + isAccountPoolSortKey(key) && + (direction === "asc" || direction === "desc") + ) { + return { key, direction }; + } + } catch (_error) { + /* Ignore invalid storage and use the stable default order. */ + } + + return { key: "", direction: "asc" }; + } + + function persistAccountPoolSort() { + try { + window.localStorage.setItem( + ACCOUNT_POOL_SORT_STORAGE_KEY, + JSON.stringify(accountPoolSort), + ); + } catch (_error) { + /* Ignore storage failures and continue with the in-memory choice. */ + } + } + + function loadProjectFilterMode() { + try { + return window.localStorage.getItem(PROJECT_FILTER_STORAGE_KEY) === "all" + ? "all" + : "active"; + } catch (_error) { + return "active"; + } + } + + function persistProjectFilterMode() { + try { + window.localStorage.setItem(PROJECT_FILTER_STORAGE_KEY, projectFilterMode); + } catch (_error) { + /* Ignore storage failures and continue with the in-memory choice. */ + } + } + + function loadProjectLocationPrivacy() { try { - const stored = window.localStorage.getItem(ACCOUNT_POOL_SORT_STORAGE_KEY); - const parsed = stored ? JSON.parse(stored) : {}; - const key = String(parsed?.key || ""); - const direction = String(parsed?.direction || "asc"); - if ( - isAccountPoolSortKey(key) && - (direction === "asc" || direction === "desc") - ) { - return { key, direction }; - } + return window.localStorage.getItem(PROJECT_LOCATION_PRIVACY_STORAGE_KEY) === "hidden"; } catch (_error) { - /* Ignore invalid storage and use the stable default order. */ + return false; } - - return { key: "", direction: "asc" }; } - function persistAccountPoolSort() { + function persistProjectLocationPrivacy(hidden) { try { window.localStorage.setItem( - ACCOUNT_POOL_SORT_STORAGE_KEY, - JSON.stringify(accountPoolSort), + PROJECT_LOCATION_PRIVACY_STORAGE_KEY, + hidden ? "hidden" : "visible", ); } catch (_error) { /* Ignore storage failures and continue with the in-memory choice. */ @@ -3844,42 +4209,74 @@

Run History

}; } - function loadDashboardSubscription() { - try { - const stored = window.localStorage.getItem(DASHBOARD_SUBSCRIPTION_STORAGE_KEY); - return normalizeDashboardSubscription(stored ? JSON.parse(stored) : {}); - } catch (_error) { - return normalizeDashboardSubscription(); + function eyeToggleIconMarkup() { + return ` + + + `; + } + + function accountPrivacyToggleMarkup() { + return ``; + } + + function projectLocationToggleMarkup() { + return ``; + } + + function renderAccountPrivacyToggle() { + const visible = !accountEmailsHidden; + for (const toggle of document.querySelectorAll("[data-account-privacy-toggle]")) { + toggle.classList.toggle("is-on", visible); + toggle.setAttribute("aria-checked", visible ? "true" : "false"); + toggle.setAttribute( + "aria-label", + visible ? "Hide account emails" : "Show account emails", + ); + toggle.title = visible ? "Hide account emails" : "Show account emails"; } } - function persistDashboardSubscription() { - try { - window.localStorage.setItem( - DASHBOARD_SUBSCRIPTION_STORAGE_KEY, - JSON.stringify(dashboardSubscription), + function renderProjectLocationToggle() { + const visible = !projectLocationsHidden; + for (const toggle of document.querySelectorAll("[data-project-location-toggle]")) { + toggle.classList.toggle("is-on", visible); + toggle.setAttribute("aria-checked", visible ? "true" : "false"); + toggle.setAttribute( + "aria-label", + visible ? "Hide project locations" : "Show project locations", ); - } catch (_error) { - /* Ignore storage failures and continue with the in-memory choice. */ + toggle.title = visible ? "Hide project locations" : "Show project locations"; } } - function renderAccountPrivacyToggle() { - const visible = !accountEmailsHidden; - nodes.accountPrivacyToggle.classList.toggle("is-on", visible); - nodes.accountPrivacyToggle.setAttribute( - "aria-checked", - visible ? "true" : "false", - ); - nodes.accountPrivacyToggle.setAttribute( - "aria-label", - visible ? "Hide account emails" : "Show account emails", - ); - nodes.accountPrivacyToggle.title = visible ? "Hide account emails" : "Show account emails"; + function renderProjectWorkInfoState() { + for (const button of document.querySelectorAll("[data-project-work-info]")) { + button.classList.toggle("is-open", projectWorkInfoOpen); + button.setAttribute("aria-expanded", projectWorkInfoOpen ? "true" : "false"); + } + } + + function renderProjectFilterToggle(projects = []) { + const showingAll = projectFilterMode === "all"; + const title = showingAll ? "Show active projects" : "Show all projects"; + nodes.projectFilterToggle.classList.toggle("is-on", showingAll); + nodes.projectFilterToggle.disabled = projects.length === 0; + nodes.projectFilterToggle.setAttribute("aria-checked", showingAll ? "true" : "false"); + nodes.projectFilterToggle.setAttribute("aria-label", title); + nodes.projectFilterToggle.title = title; } function setPanelMeta(node, text, tone = "") { - node.textContent = text; + setMetricText(node, text); if (tone) { node.dataset.tone = tone; return; @@ -4171,6 +4568,12 @@

Run History

`; } + function renderRoutineEmptyList(container, snapshot, waitingCopy = "") { + container.innerHTML = snapshot + ? "" + : renderEmptyState(COPY.waitingSnapshot, waitingCopy); + } + function pluralLabel(count, singular, plural = `${singular}s`) { return count === 1 ? singular : plural; } @@ -4245,7 +4648,7 @@

Run History

return lane?.ledger_outcome || { ledger_status: "not_loaded", final_outcome: "local_attempt_history", - summary: "Linear run history was not loaded for this local snapshot.", + summary: "Linear history not loaded for this snapshot.", record_count: 0, }; } @@ -4407,17 +4810,17 @@

Run History

function runExecutionLivenessSummary(run) { switch (run.execution_liveness) { case "process_alive": - return "Process is alive"; + return "Process alive"; case "thread_active": - return "App-server thread is active"; + return "Thread active"; case "protocol_observed": - return "Protocol activity observed"; + return "Protocol active"; case "process_stopped": return "Process stopped"; case "not_running": return "Not running"; default: - return "Liveness not captured"; + return "Liveness unknown"; } } @@ -4425,20 +4828,20 @@

Run History

const leaseState = run.queue_lease_state || (run.active_lease ? "held" : "not_held"); if (leaseState === "held") { - return "queue lease held"; + return "lease held"; } switch (run.execution_liveness) { case "process_alive": - return "queue lease not held; live process keeps lane visible"; + return "no lease; live process"; case "thread_active": - return "queue lease not held; active app-server thread keeps lane visible"; + return "no lease; active thread"; case "protocol_observed": - return "queue lease not held; protocol activity keeps lane visible"; + return "no lease; protocol active"; case "process_stopped": - return "queue lease not held; stopped process needs attention"; + return "no lease; stopped process"; default: - return "queue lease not held"; + return "no lease"; } } @@ -4661,7 +5064,7 @@

Run History

} return ` -
+
${facts .map( ([label, value]) => ` @@ -4996,14 +5399,6 @@

Run History

}; } - function codexAccountLowestRemaining(account) { - const values = ["primary", "secondary"] - .map((prefix) => codexAccountWindowData(account, prefix).remainingPercent) - .filter((value) => value != null); - - return values.length ? Math.min(...values) : null; - } - function codexAccountWindowTone(percent) { if (percent == null) { return ""; @@ -5023,6 +5418,23 @@

Run History

return reached && reached !== "none" ? reached : ""; } + function codexAccountUsageLimited(account) { + if (!account) { + return false; + } + + const status = String(account.status || "").toLowerCase(); + const primary = codexAccountWindowData(account, "primary"); + const secondary = codexAccountWindowData(account, "secondary"); + + return Boolean( + codexAccountReachedType(account) || + status.includes("limit") || + primary.remainingPercent === 0 || + secondary.remainingPercent === 0, + ); + } + function codexAccountStatusTone(account) { if (!account) { return ""; @@ -5030,28 +5442,22 @@

Run History

const status = String(account.status || "").toLowerCase(); const refresh = String(account.refresh_status || "").toLowerCase(); - const lowestRemaining = codexAccountLowestRemaining(account); + const refreshNeedsAttention = + refresh && !["not_needed", "refreshed", "succeeded", "none"].includes(refresh); if ( - codexAccountReachedType(account) || - status.includes("limit") || + codexAccountUsageLimited(account) || status.includes("failed") || status.includes("unusable") || refresh.includes("failed") ) { return "danger"; } - if ( - lowestRemaining != null && - lowestRemaining <= 20 - ) { + if (refreshNeedsAttention) { return "warn"; } - if ( - refresh && - !["not_needed", "refreshed", "succeeded", "none"].includes(refresh) - ) { - return "warn"; + if (status === "available") { + return "ready"; } return ""; @@ -5064,7 +5470,17 @@

Run History

const reached = codexAccountReachedType(account); const status = reached || account.status || "selected"; + const refresh = String(account.refresh_status || "").toLowerCase(); const normalizedStatus = String(status).toLowerCase(); + if (codexAccountUsageLimited(account)) { + return "Limited"; + } + if (refresh.includes("failed")) { + return "Refresh failed"; + } + if (refresh && !["not_needed", "refreshed", "succeeded", "none"].includes(refresh)) { + return codexAccountTokenValue(account.refresh_status); + } if (normalizedStatus === "selected") { return "Active"; } @@ -5123,13 +5539,13 @@

Run History

return number.toFixed(2); } - function codexAccountCreditsTone(account) { - if (!account) { - return ""; - } - if (account.credits_has_credits === false) { - return "danger"; - } + function codexAccountCreditsTone(account) { + if (!account) { + return ""; + } + if (codexAccountReachedType(account).includes("credit")) { + return "danger"; + } return ""; } @@ -5203,14 +5619,15 @@

Run History

const toneClass = statusTone ? ` is-${statusTone}` : ""; const selectedClass = String(account.status || "").toLowerCase() === "selected" ? " is-selected" : ""; - const metaFacts = codexAccountMetaFacts(account); - const credits = codexAccountCreditsSummary(account); - const creditTone = codexAccountCreditsTone(account); - const creditClass = creditTone ? ` is-${creditTone}` : ""; + const metaFacts = codexAccountMetaFacts(account); + const credits = codexAccountCreditsSummary(account); + const creditTone = codexAccountCreditsTone(account); + const creditClass = creditTone ? ` is-${creditTone}` : ""; + const identityClass = codexAccountShowsEmail(account) ? " is-machine" : ""; - return ` + return `