From 2aee44c069beb5103cd4bb79a97446ce0cd47890 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Mon, 11 May 2026 11:43:05 +0800 Subject: [PATCH 1/4] {"schema":"decodex/commit/1","summary":"Refine operator dashboard accounts and projects","authority":"manual"} --- apps/decodex/src/agent/codex_accounts.rs | 88 ++- .../src/orchestrator/operator_dashboard.html | 523 +++++++++++------- .../decodex/src/orchestrator/operator_http.rs | 2 +- .../tests/operator/status/dashboard.rs | 61 +- .../tests/operator/status/http.rs | 29 +- docs/reference/operator-control-plane.md | 53 +- 6 files changed, 508 insertions(+), 248 deletions(-) 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..3cf1167c 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -671,6 +671,10 @@ --section-tone: var(--tone-muted); } + .section-marker-projects { + --section-tone: var(--info); + } + .section-marker-execution { --section-tone: var(--tone-run); } @@ -968,6 +972,51 @@ gap: 0; } + .project-scope-list.has-registered-projects { + gap: 12px; + } + + .project-subsection { + display: grid; + gap: 0; + min-width: 0; + } + + .project-subsection-head { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content 14px; + align-items: center; + gap: 12px; + padding: 13px 0; + border-bottom: 1px solid var(--line); + color: var(--muted-strong); + font-family: var(--mono); + font-size: 12px; + font-weight: 650; + letter-spacing: 0.14em; + line-height: 1.3; + text-transform: uppercase; + } + + .project-subsection-head strong { + color: var(--text); + font-size: 11px; + font-weight: 700; + letter-spacing: 0; + text-transform: none; + white-space: nowrap; + } + + .project-subsection-head::after { + content: ""; + width: 14px; + height: 14px; + } + + .project-active-list { + display: grid; + } + .project-scope-list.is-empty .empty-state { grid-template-columns: minmax(142px, max-content) minmax(0, 1fr); max-width: none; @@ -1021,7 +1070,8 @@ transition: background-color var(--fast) var(--ease); } - .project-scope-list > .project-entry:last-child { + .project-active-list > .project-entry:last-child, + .registered-project-list > .project-entry:last-child { border-bottom: 0; } @@ -1089,6 +1139,83 @@ transform: scaleX(1); } + .registered-projects { + border-top: 1px solid var(--line); + } + + .registered-projects > summary { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content 14px; + align-items: center; + gap: 12px; + padding: 13px 0; + color: var(--muted-strong); + font-family: var(--mono); + font-size: 12px; + font-weight: 650; + letter-spacing: 0.14em; + line-height: 1.3; + text-transform: uppercase; + cursor: pointer; + list-style: none; + } + + .registered-projects > summary::-webkit-details-marker { + display: none; + } + + .registered-projects > summary:focus { + outline: none; + } + + .registered-projects > summary:focus-visible { + outline: 2px solid var(--info); + outline-offset: 2px; + } + + .registered-projects > summary strong { + color: var(--text); + font-size: 11px; + font-weight: 700; + letter-spacing: 0; + text-transform: none; + white-space: nowrap; + } + + .registered-projects > summary::after { + content: "+"; + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + color: var(--muted); + font-size: 13px; + font-weight: 700; + letter-spacing: 0; + line-height: 1; + text-transform: none; + transition: color var(--fast) var(--ease); + } + + .registered-projects[data-detail-state="open"] > summary::after { + content: "-"; + } + + .registered-projects > summary:hover::after { + color: var(--text); + } + + .registered-project-list { + display: grid; + border-top: 1px solid var(--line); + } + + .registered-project-list .project-entry { + padding-top: 14px; + padding-bottom: 14px; + } + .project-entry.tone-land { --project-accent: var(--tone-land); } @@ -1120,6 +1247,14 @@ text-transform: uppercase; } + .project-activity { + color: var(--muted); + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0; + line-height: 1.2; + } + .project-title-line { display: flex; flex-wrap: wrap; @@ -1206,7 +1341,6 @@ min-width: 0; } - .project-action-line, .run-control-line { display: flex; flex-wrap: wrap; @@ -1566,6 +1700,10 @@ --account-accent: var(--success); } + .account-row.is-ready { + --account-accent: var(--info); + } + .account-row.is-selected::before { box-shadow: 0 0 10px color-mix(in srgb, var(--account-accent) 22%, transparent); } @@ -1832,7 +1970,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); } @@ -2281,7 +2420,8 @@ details.is-animating > .grid, details.is-animating > .attempt-list, - details.is-animating > .panel-body { + details.is-animating > .panel-body, + details.is-animating > .registered-project-list { overflow: hidden; transform-origin: top center; transition: @@ -2349,13 +2489,22 @@ 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; @@ -2370,43 +2519,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: 13px; + font-weight: 700; + 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); + .fold-panel[data-detail-state="open"] .fold-indicator::before { + content: "-"; } .fold-panel > summary:hover .fold-indicator { @@ -2763,7 +2893,7 @@

Decodex

aria-label="Control Plane group" > Control Plane -

Accounts · Projects

+

Accounts

@@ -2791,15 +2921,17 @@

Accounts

-
-
-
-

Projects

-
-

-
+
+ Projects +

All · Active

+
+
-
+
@@ -2908,7 +3040,6 @@

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 ACCOUNT_POOL_SORT_COLUMNS = [ ["account", "Account"], ["plan", "Plan"], @@ -3003,11 +3134,11 @@

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"), accountPool: document.getElementById("account-pool"), accountPoolMeta: document.getElementById("account-pool-meta"), accountPrivacyToggle: document.getElementById("account-privacy-toggle"), @@ -3033,7 +3164,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)"); @@ -3049,7 +3180,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"] }, ]; @@ -3844,26 +3976,6 @@

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 persistDashboardSubscription() { - try { - window.localStorage.setItem( - DASHBOARD_SUBSCRIPTION_STORAGE_KEY, - JSON.stringify(dashboardSubscription), - ); - } catch (_error) { - /* Ignore storage failures and continue with the in-memory choice. */ - } - } - function renderAccountPrivacyToggle() { const visible = !accountEmailsHidden; nodes.accountPrivacyToggle.classList.toggle("is-on", visible); @@ -4021,7 +4133,7 @@

Run History

} function detailContent(details) { - return details.querySelector(":scope > .panel-body, :scope > .grid, :scope > .attempt-list"); + return details.querySelector(":scope > .panel-body, :scope > .grid, :scope > .attempt-list, :scope > .registered-project-list"); } function rememberDetailOpenState(details, isOpen) { @@ -5023,6 +5135,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 ""; @@ -5033,8 +5162,7 @@

Run History

const lowestRemaining = codexAccountLowestRemaining(account); if ( - codexAccountReachedType(account) || - status.includes("limit") || + codexAccountUsageLimited(account) || status.includes("failed") || status.includes("unusable") || refresh.includes("failed") @@ -5047,15 +5175,18 @@

Run History

) { return "warn"; } - if ( - refresh && - !["not_needed", "refreshed", "succeeded", "none"].includes(refresh) - ) { - return "warn"; - } - - return ""; + if ( + refresh && + !["not_needed", "refreshed", "succeeded", "none"].includes(refresh) + ) { + return "warn"; } + if (status === "available") { + return "ready"; + } + + return ""; + } function codexAccountStatusLabel(account) { if (!account) { @@ -5065,6 +5196,9 @@

Run History

const reached = codexAccountReachedType(account); const status = reached || account.status || "selected"; const normalizedStatus = String(status).toLowerCase(); + if (codexAccountUsageLimited(account)) { + return "Limited"; + } if (normalizedStatus === "selected") { return "Active"; } @@ -5123,13 +5257,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 ""; } @@ -6643,25 +6777,31 @@

Run History

return null; } - function projectsMetaCopy(snapshot, projects) { - if (!projects.length) { - return pluralize(0, "project"); + function projectHasRecentActivity(project) { + const parsed = Date.parse(project.last_activity_at || ""); + if (!Number.isFinite(parsed)) { + return false; } - const enabledCount = projects.filter((project) => project.enabled).length; - const selectedId = selectedProjectId(snapshot, projects); + const ageSeconds = Math.floor(Date.now() / 1000) - Math.floor(parsed / 1000); + return ageSeconds >= 0 && ageSeconds <= 900; + } - if (!enabledCount) { - return `${pluralize(projects.length, "project")} · none enabled`; - } - if (projects.length === 1) { - return pluralize(1, "project"); - } - if (selectedId) { - return `${pluralize(projects.length, "project")} · ${enabledCount} enabled · ${selectedId} selected`; - } + function projectHasVisibleWork(project) { + const workCount = + (project.active_run_count ?? 0) + + (project.waiting_lane_count ?? 0) + + (project.attention_count ?? 0) + + (project.post_review_lane_count ?? 0) + + (project.retained_worktree_count ?? 0); + const connector = projectConnectorSummary(project); + const syncNeedsAttention = project.enabled && ["backoff", "degraded", "stale"].includes(connector); + + return workCount > 0 || (project.warning_count ?? 0) > 0 || syncNeedsAttention || projectHasRecentActivity(project); + } - return `${pluralize(projects.length, "project")} · ${enabledCount} enabled`; + function activeProjects(projects) { + return projects.filter(projectHasVisibleWork); } function projectHealth(project) { @@ -6771,28 +6911,6 @@

Run History

return project.repo_root || project.config_path || "path unavailable"; } - function dashboardProjectIsFocused(project) { - return ( - dashboardSubscription.projectId === project.project_id && - !dashboardSubscription.issueId && - !dashboardSubscription.runId - ); - } - - function renderProjectControls(project) { - const focused = dashboardProjectIsFocused(project); - const toggleAction = project.enabled ? "pauseProject" : "resumeProject"; - const toggleLabel = project.enabled ? "Pause" : "Resume"; - const toggleTone = project.enabled ? "warning" : "success"; - - return ` -
- - -
- `; - } - const projectRegistrationCommand = "decodex project add ~/.codex/decodex/projects/"; @@ -6812,20 +6930,76 @@

Run History

`; } + function renderProjectEntry(project, selectedId, projects) { + const health = projectHealth(project); + const isActive = selectedId === project.project_id; + const projectPath = projectPathSummary(project); + const kicker = projectScopeKicker(project, isActive, projects); + const attention = projectAttentionSummary(project); + const lastActivity = formatRelativeTimestamp(project.last_activity_at); + const connectorCopy = projectSyncMeta(project, health); + const attentionCopy = + attention === "ok" || attention.includes("retained") ? "" : attention; + const activityCopy = lastActivity === "none" ? "" : `active ${lastActivity}`; + const metaItems = [connectorCopy].filter(Boolean); + if (attentionCopy) { + metaItems.splice(1, 0, attentionCopy); + } + const ariaCurrent = isActive ? ' aria-current="true"' : ""; + + return ` +
+
+
+ ${escapeHtml(project.project_id)} + ${escapeHtml(health.label)} + ${activityCopy ? `${escapeHtml(activityCopy)}` : ""} + ${kicker ? `${escapeHtml(kicker)}` : ""} +
+
${escapeHtml(projectPath)}
+
+ ${metaItems.map((item) => `${escapeHtml(item)}`).join("")} +
+
+
+
+ ${renderProjectStats(project)} +
+
+
+ `; + } + + function renderRegisteredProjects(projects, activeProjectRows, selectedId) { + const activeIds = new Set(activeProjectRows.map((project) => project.project_id)); + const hiddenCount = projects.filter((project) => !activeIds.has(project.project_id)).length; + const summary = hiddenCount + ? `${hiddenCount} idle · ${projects.length} total` + : `${projects.length} total`; + + return ` +
+ All${escapeHtml(summary)} +
+ ${projects.map((project) => renderProjectEntry(project, selectedId, projects)).join("")} +
+
+ `; + } + function renderProjects(snapshot, derived) { if (!snapshot) { - nodes.projectsMeta.textContent = ""; nodes.projectOverview.classList.add("is-empty", "is-waiting"); nodes.projectOverview.innerHTML = renderEmptyState(COPY.waitingSnapshot); return; } const projects = derived.projects; - - nodes.projectsMeta.textContent = projectsMetaCopy(snapshot, projects); + const activeProjectRows = activeProjects(projects); if (!projects.length) { nodes.projectOverview.classList.add("is-empty"); + nodes.projectOverview.classList.remove("has-registered-projects"); nodes.projectOverview.classList.remove("is-waiting"); nodes.projectOverview.innerHTML = renderProjectEmptyState( "No registered projects", @@ -6835,51 +7009,32 @@

Run History

} const selectedId = selectedProjectId(snapshot, projects); + const activeSummary = activeProjectRows.length + ? pluralize(activeProjectRows.length, "active project") + : "none active"; + const activeMarkup = activeProjectRows.length + ? activeProjectRows.map((project) => renderProjectEntry(project, selectedId, projects)).join("") + : renderProjectEmptyState( + "No active project work", + `${pluralize(projects.length, "project")} idle. Open All when you need the full registry.`, + { showAction: false }, + ); nodes.projectOverview.classList.remove("is-empty", "is-waiting"); - nodes.projectOverview.innerHTML = projects - .map((project) => { - const health = projectHealth(project); - const isActive = selectedId === project.project_id; - const projectPath = projectPathSummary(project); - const kicker = projectScopeKicker(project, isActive, projects); - const connector = projectConnectorSummary(project); - const attention = projectAttentionSummary(project); - const lastActivity = formatRelativeTimestamp(project.last_activity_at); - const connectorCopy = projectSyncMeta(project, health); - const attentionCopy = - attention === "ok" || attention.includes("retained") ? "" : attention; - const activityCopy = - lastActivity === "none" ? "no activity yet" : `active ${lastActivity}`; - const metaItems = [connectorCopy, activityCopy].filter(Boolean); - if (attentionCopy) { - metaItems.splice(1, 0, attentionCopy); - } - const ariaCurrent = isActive ? ' aria-current="true"' : ""; - - return ` -
-
-
- ${escapeHtml(project.project_id)} - ${escapeHtml(health.label)} - ${kicker ? `${escapeHtml(kicker)}` : ""} -
-
${escapeHtml(projectPath)}
-
- ${metaItems.map((item) => `${escapeHtml(item)}`).join("")} -
-
-
-
- ${renderProjectStats(project)} -
- ${renderProjectControls(project)} -
-
- `; - }) - .join(""); + nodes.projectOverview.classList.toggle("is-empty", activeProjectRows.length === 0); + nodes.projectOverview.classList.add("has-registered-projects"); + nodes.projectOverview.innerHTML = ` + ${renderRegisteredProjects(projects, activeProjectRows, selectedId)} +
+
+ Active + ${escapeHtml(activeSummary)} +
+
+ ${activeMarkup} +
+
+ `; } function setFlowCounts(queue, run, review, land) { @@ -7103,13 +7258,6 @@

${escapeHtml(item.title)}

); } - function dashboardRunIsFocused(run) { - return ( - (run.run_id && dashboardSubscription.runId === run.run_id) || - (run.issue_id && dashboardSubscription.issueId === run.issue_id) - ); - } - function runRetryControlEnabled(run) { return Boolean( run.project_id && @@ -7123,12 +7271,10 @@

${escapeHtml(item.title)}

} function renderRunControlLine(run) { - const focused = dashboardRunIsFocused(run); const retryEnabled = runRetryControlEnabled(run); return `
-
`; @@ -7805,17 +7951,6 @@

${escapeHtml(worktree.branch_name)}

}); } - function setDashboardSubscription(subscription, shouldSend = true) { - dashboardSubscription = normalizeDashboardSubscription(subscription); - persistDashboardSubscription(); - if (shouldSend) { - syncDashboardSubscriptionToSocket(); - } - if (lastDashboardRender) { - renderDashboardState(lastDashboardRender); - } - } - function sendDashboardControl(action, payload = {}) { return sendDashboardSocketMessage({ type: "control", @@ -7841,10 +7976,6 @@

${escapeHtml(worktree.branch_name)}

} function applyDashboardControlAck(payload) { - if (payload?.subscription) { - dashboardSubscription = normalizeDashboardSubscription(payload.subscription); - persistDashboardSubscription(); - } if (payload?.action === "ack" || payload?.action === "subscribe") { return; } @@ -8011,14 +8142,6 @@

${escapeHtml(worktree.branch_name)}

const runId = button.dataset.runId || null; switch (control) { - case "focusProject": - setDashboardSubscription({ projectId }); - break; - case "focusRun": - setDashboardSubscription({ projectId, issueId, runId }); - break; - case "pauseProject": - case "resumeProject": case "retryRun": sendDashboardControl(control, { projectId, issueId, runId }); break; diff --git a/apps/decodex/src/orchestrator/operator_http.rs b/apps/decodex/src/orchestrator/operator_http.rs index 0f94a341..dceaa77e 100644 --- a/apps/decodex/src/orchestrator/operator_http.rs +++ b/apps/decodex/src/orchestrator/operator_http.rs @@ -710,7 +710,7 @@ fn dashboard_clear_focus_control_ack( request_id: message.request_id.as_deref(), action, accepted: true, - status: "focused", + status: "cleared", message: "Dashboard focus cleared for this WebSocket session.", project_id: None, issue_id: None, diff --git a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs index 1846be25..e51a9d11 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs @@ -169,14 +169,18 @@ fn operator_dashboard_renders_account_usage_controls() { assert!(!response.contains(".stack > .panel + .panel")); assert!(response.contains("panel section-control\" id=\"account-pool-panel\"")); assert!(response.contains("section-marker section-marker-control")); + assert!(response.contains("section-marker section-marker-projects")); assert!(response.contains("section-marker section-marker-execution")); assert!(response.contains("section-marker section-marker-aftercare")); assert!(response.contains("Control Plane")); + assert!(response.contains("Projects")); assert!(response.contains("Execution")); assert!(response.contains("Closeout")); - assert!(response.contains("Accounts · Projects")); + assert!(response.contains("

Accounts

")); + assert!(response.contains("All · Active")); assert!(response.contains("Running · Intake")); assert!(response.contains("Review · Recovery · History")); + assert!(!response.contains("data-fold-key=\"panel:projects\"")); assert!(response.contains("panel section-execution\" id=\"active-panel\"")); assert!(response.contains("panel section-aftercare\" id=\"review-panel\"")); assert!(!response.contains("section-group-start")); @@ -333,6 +337,7 @@ fn operator_dashboard_accounts_keeps_compact_table_layout() { assert!(response.contains("account-window-date")); assert!(!response.contains("Reset")); assert!(response.contains("is-selected")); + assert!(response.contains("is-ready")); assert!(response.contains("--account-accent: var(--tone-muted);")); assert!(response.contains("grid-template-areas:")); assert!(response.contains("\"id plan primary secondary credit state\"")); @@ -391,6 +396,7 @@ fn operator_dashboard_accounts_keeps_window_status_and_credit_copy_compact() { assert!(response.contains(".account-row:focus-within .account-window")); assert!(response.contains(".account-status::before")); assert!(response.contains(".account-row.is-selected .account-status")); + assert!(response.contains(".account-row.is-ready .account-status")); assert!(response.contains(".account-row.is-warn .account-status")); assert!(response.contains(".account-row.is-danger .account-status")); assert!(response.contains(".account-row:hover .account-status::before")); @@ -432,6 +438,10 @@ fn operator_dashboard_accounts_keeps_debug_credit_and_reset_copy_compact() { assert!(!response.contains(".replace(/\\.00$/, \"\")")); assert!(!response.contains(".replace(/(\\.\\d)0$/, \"$1\")")); assert!(response.contains("function codexAccountCreditsTone(account)")); + assert!(response.contains("function codexAccountUsageLimited(account)")); + assert!(response.contains("if (status === \"available\")")); + assert!(response.contains("return \"ready\";")); + assert!(response.contains("codexAccountReachedType(account).includes(\"credit\")")); assert!(response.contains("const credits = codexAccountCreditsSummary(account);")); assert!(response.contains("const creditTone = codexAccountCreditsTone(account);")); assert!(response.contains("credits")); @@ -453,6 +463,7 @@ fn operator_dashboard_accounts_keeps_debug_credit_and_reset_copy_compact() { assert!(!response.contains("depleted")); assert!(response.contains("rate_limit_reached_type")); assert!(response.contains("if (normalizedStatus === \"available\")")); + assert!(response.contains("if (codexAccountUsageLimited(account))")); assert!(response.contains("if (normalizedStatus.includes(\"limit\"))")); assert!(response.contains("return \"Limited\";")); assert!(response.contains("cooldown_until_unix_epoch")); @@ -497,6 +508,26 @@ fn operator_dashboard_accounts_keeps_debug_credit_and_reset_copy_compact() { assert!(!response.contains("[\"checked\"")); } +#[test] +fn operator_dashboard_omits_watch_and_project_pause_controls() { + let response = dashboard_response(); + + assert!(!response.contains("function dashboardSubscriptionMatches(subscription)")); + assert!(!response.contains("function clearDashboardSubscription(shouldSend = true)")); + assert!(!response.contains("function toggleDashboardSubscription(subscription)")); + assert!(!response.contains("toggleDashboardSubscription({ projectId })")); + assert!(!response.contains("toggleDashboardSubscription({ projectId, issueId, runId })")); + assert!(!response.contains("data-dashboard-control=\"focusProject\"")); + assert!(!response.contains("data-dashboard-control=\"focusRun\"")); + assert!(!response.contains("data-dashboard-control=\"pauseProject\"")); + assert!(!response.contains("data-dashboard-control=\"resumeProject\"")); + assert!(!response.contains(">Watch")); + assert!(!response.contains(">Watching")); + assert!(!response.contains(">Pause")); + assert!(!response.contains(">Resume")); + assert!(response.contains("data-dashboard-control=\"retryRun\"")); +} + #[test] fn operator_dashboard_projects_keep_status_summary_compact() { let response = String::from_utf8( @@ -515,14 +546,35 @@ fn operator_dashboard_projects_keep_status_summary_compact() { assert!(response.contains("function projectCapacitySummary(project)")); assert!(response.contains("function renderProjectStats(project)")); - assert!(response.contains("function projectsMetaCopy(snapshot, projects)")); + assert!(response.contains("function projectHasVisibleWork(project)")); + assert!(response.contains("function activeProjects(projects)")); + assert!(response.contains("function renderProjectEntry(project, selectedId, projects)")); + assert!(response.contains("function renderRegisteredProjects(projects, activeProjectRows, selectedId)")); assert!(response.contains("function renderEmptyState(title, copy = \"\")")); assert!(response.contains("nodes.projectOverview.innerHTML = renderEmptyState(COPY.waitingSnapshot);")); assert!(response.contains("snapshot ? \"No running lanes\" : COPY.waitingSnapshot")); - assert!(response.contains("return pluralize(1, \"project\");")); - assert!(response.contains("return pluralize(0, \"project\");")); assert!(response.contains("return projects.length === 1 ? \"Current\" : \"Selected\";")); assert!(response.contains("return \"\";")); + assert!(!response.contains("

Projects

")); + assert!(!response.contains("id=\"projects-meta\"")); + assert!(!response.contains("project-panel-head")); + assert!(!response.contains("nodes.projectsMeta")); + assert!(response.contains("role=\"group\" aria-label=\"Projects\"")); + assert!(response.contains("const activeProjectRows = activeProjects(projects);")); + assert!(response.contains("No active project work")); + assert!(response.contains("Open All when you need the full registry.")); + assert!(response.contains("class=\"project-subsection\" aria-label=\"Active projects\"")); + assert!(response.contains(".project-subsection-head::after")); + assert!(response.contains("grid-template-columns: minmax(0, 1fr) max-content 14px;")); + assert!(response.contains("class=\"project-active-list\" role=\"list\" aria-label=\"Active projects\"")); + assert!(response.contains("All${escapeHtml(summary)}")); + assert!(response.contains("role=\"list\" aria-label=\"All projects\"")); + assert!(response.contains(".project-active-list > .project-entry:last-child")); + assert!(response.contains(".registered-project-list > .project-entry:last-child")); + assert!(response.contains("class=\"project-activity\"")); + assert!(response.contains("const activityCopy = lastActivity === \"none\" ? \"\" : `active ${lastActivity}`;")); + assert!(response.contains("project.post_review_lane_count ?? 0")); + assert!(response.contains("project.retained_worktree_count ?? 0")); assert!(response.contains("return pluralize(project.warning_count, \"warning\");")); assert!(response.contains("return `${pluralize(project.retained_worktree_count, \"worktree\")} retained`;")); assert!(response.contains("return { label: \"needs attention\", tone: \"tone-blocked\"")); @@ -533,6 +585,7 @@ fn operator_dashboard_projects_keep_status_summary_compact() { assert!(response.contains("const connectorCopy = projectSyncMeta(project, health);")); assert!(response.contains("if (connector === \"ok\")")); assert!(response.contains("return copy === health.label ? \"\" : copy;")); + assert!(!response.contains("const prefix = `${activeCount} active · ${projects.length} all`;")); assert!(response.contains("return \"ok\";")); assert!(response.contains("${kicker ? `${escapeHtml(kicker)}` : \"\"}")); assert!(!response.contains("const connectorCopy = `connector ${connector}`;")); diff --git a/apps/decodex/src/orchestrator/tests/operator/status/http.rs b/apps/decodex/src/orchestrator/tests/operator/status/http.rs index c770bab6..82bdb4ae 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/http.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/http.rs @@ -178,8 +178,10 @@ fn operator_state_endpoint_serves_dashboard_html_from_root_and_dashboard_route() assert!(response.contains("flow-queue")); assert!(response.contains("Intake")); assert!(response.contains("Landing")); - assert!(response.contains("

Projects

")); - assert!(response.contains("Registered projects")); + assert!(response.contains("section-marker section-marker-projects")); + assert!(!response.contains("

Projects

")); + assert!(!response.contains("data-fold-key=\"panel:projects\"")); + assert!(response.contains("All")); assert!(response.contains("projectRegistrationCommand")); assert!( response.contains("decodex project add ~/.codex/decodex/projects/") @@ -228,6 +230,10 @@ fn operator_state_endpoint_serves_dashboard_html_from_root_and_dashboard_route() assert!(!response.contains("running laness")); assert!(!response.contains("active-echo")); assert!(response.contains("fold-panel")); + assert!(response.contains(".fold-indicator::before")); + assert!(response.contains("content: \"+\";")); + assert!(response.contains("content: \"-\";")); + assert!(!response.contains(".fold-indicator::after")); assert!(response.contains("data-fold-key=\"panel:worktrees\"")); assert!(response.contains("data-fold-key=\"panel:recent\"")); assert!(response.contains("cursor: pointer;")); @@ -243,13 +249,16 @@ fn operator_state_endpoint_serves_dashboard_html_from_root_and_dashboard_route() assert!(!response.contains("Landing Readiness")); assert!(response.contains("/state")); assert!(response.contains("/readyz")); - assert!(response.contains("/dashboard/control")); - assert!(response.contains("WebSocket")); - assert!(response.contains("applyDashboardRunActivity")); - assert!(response.contains("sendDashboardControl")); - assert!(response.contains("data-dashboard-control=\"focusProject\"")); - assert!(response.contains("data-dashboard-control=\"retryRun\"")); - assert!(response.contains("controlAck")); + assert!(response.contains("/dashboard/control")); + assert!(response.contains("WebSocket")); + assert!(response.contains("applyDashboardRunActivity")); + assert!(response.contains("sendDashboardControl")); + assert!(!response.contains("data-dashboard-control=\"focusProject\"")); + assert!(!response.contains("data-dashboard-control=\"focusRun\"")); + assert!(!response.contains("data-dashboard-control=\"pauseProject\"")); + assert!(!response.contains("data-dashboard-control=\"resumeProject\"")); + assert!(response.contains("data-dashboard-control=\"retryRun\"")); + assert!(response.contains("controlAck")); assert!(!response.contains("Last updated: none")); assert!(!response.contains("Auto-refresh")); assert!(!response.contains("

Project Scope

")); @@ -505,7 +514,7 @@ fn operator_dashboard_websocket_controls_focus_and_clear_subscription() { }); assert_eq!(clear_ack["payload"]["accepted"], true); - assert_eq!(clear_ack["payload"]["status"], "focused"); + assert_eq!(clear_ack["payload"]["status"], "cleared"); assert_eq!(clear_ack["payload"]["subscription"]["projectId"], Value::Null); assert_eq!(clear_ack["payload"]["subscription"]["issueId"], Value::Null); assert_eq!(clear_ack["payload"]["subscription"]["runId"], Value::Null); diff --git a/docs/reference/operator-control-plane.md b/docs/reference/operator-control-plane.md index b938dc01..65cb97fe 100644 --- a/docs/reference/operator-control-plane.md +++ b/docs/reference/operator-control-plane.md @@ -30,12 +30,14 @@ Decodex currently runs as a local, single-machine control plane: - The project-owned `WORKFLOW.md` remains the execution-policy contract for that registered repo. -Project registration is not service intake. `Projects` may show multiple enabled -projects at once, but a service is only eligible to intake Linear issues labeled with -its matching `decodex:queued:` label. For example, a Decodex-only run -intakes issues labeled `decodex:queued:decodex`; `rsnap` can stay enabled in -`Projects`, and issues labeled `decodex:queued:rsnap` remain rsnap intake rather than -Decodex intake. The pilot runbook owns enqueue and run steps. +Project registration is not service intake. The `Projects` dashboard section may show +multiple active enabled projects with visible work at once, and the full `All` list may +include additional enabled projects, but a service is only eligible to intake +Linear issues labeled with its matching `decodex:queued:` label. For +example, a Decodex-only run intakes issues labeled `decodex:queued:decodex`; `rsnap` +can stay enabled in `All`, and issues labeled `decodex:queued:rsnap` +remain rsnap intake rather than Decodex intake. The pilot runbook owns enqueue and +run steps. When `decodex run --dry-run` or the status output has no eligible intake candidate, the operator hint points to the short checklist: `Todo`, the service-scoped @@ -71,31 +73,34 @@ directory does not, by itself, mean an active lane is still running; the owning section says whether the path belongs to an active lease, retained review/landing lane, queued attention state, or cleanup/recovery inbox. -`Projects` is the registered-project overview for this local installation. Its rows -come from project registrations and per-project runtime snapshot state stored in -`~/.codex/decodex/runtime.sqlite3`; it is not a repository discovery scan, Codex -conversation-history scan, or repo-local config search. If the view is empty, -Decodex has no registered project rows to summarize yet. If it is idle or -capacity-waiting, the registered projects exist but currently have no local lane ready -to run or are waiting on local capacity. Neither state is, by itself, evidence that -the Linear tracker or GitHub connector failed; confirm the central project registry -and service queue label before treating it as a connector problem. +`Projects` is its own dashboard section. Its default-visible `Active` subsection is +the fleet overview for this local installation. Rows come from project registrations +and per-project runtime snapshot state stored in `~/.codex/decodex/runtime.sqlite3`; +the section is not a repository discovery scan, Codex conversation-history scan, or +repo-local config search. The collapsed `All` disclosure appears before `Active` and +contains the complete registered-project list when an operator needs the full +registry. By default, `Active` shows only projects with visible local work, recent +activity, warnings, retained worktrees, or connector attention. If the active view is +empty, the registered projects exist but currently have no visible local work. Neither +state is, by itself, evidence that the +Linear tracker or GitHub connector failed; confirm the central project registry and +service queue label before treating it as a connector problem. The browser dashboard reads the complete published state from `GET /state` and may also keep a local WebSocket open at `GET /dashboard/control`. `/state` remains the authoritative reconciliation snapshot; the WebSocket pushes Decodex-owned snapshot -and active-lane activity updates sooner than the polling interval, and accepts a -small local dashboard control protocol. Browser-originated `subscribe` / `focus` -messages scope live active-lane updates for that socket. `pauseProject` and -`resumeProject` use the existing local project registry enabled flag, and `retryRun` -starts the existing local `decodex run ` path for an explicit operator retry. -`ack` is dashboard-local acknowledgement only. The socket is not a browser connection -to Codex app-server, GitHub, or Linear, and it does not make high-frequency protocol -activity durable outside the local operator surface. +and active-lane activity updates sooner than the polling interval, and accepts the +local dashboard control protocol. The current browser UI keeps live updates unscoped +and exposes only explicit lane retry controls; project watch and pause/resume controls +are intentionally not shown. `retryRun` starts the existing local `decodex run +` path for an explicit operator retry. `ack` is dashboard-local acknowledgement +only. The socket is not a browser connection to Codex app-server, GitHub, or Linear, +and it does not make high-frequency protocol activity durable outside the local +operator surface. | Section | Meaning | | --- | --- | -| `Projects` | Fleet-level project rows. This is the multi-project overview: enabled state, health, connector state, capacity, attention count, retained local worktree count, and last activity. It should not duplicate per-lane details already shown below. | +| `Projects` | Fleet-level project section. `All` is a collapsed disclosure for the full registry. `Active` shows project rows for projects with visible work or recent activity: enabled state, health, connector state, capacity, attention count, retained local worktree count, and last activity. It should not duplicate per-lane details already shown below. | | `Running Lanes` | Active leased or live-executing issue lanes. A lane here is currently owned by this local control plane, or a live process/thread/protocol marker still explains active execution even when the queue lease is not held. It shows issue identity, phase, operation, attempt, queue lease state, execution liveness, thread/protocol status, child-agent activity when captured, timing, branch, and worktree. | | `Intake Queue` | Queued tracker issues before execution. Candidates are classified as `ready`, capacity-waiting, claimed without a matching local lane, blocked, or closed/stale. A blocked queued candidate can still show an attached `.worktrees/XY-*` path when the queue owns the attention state; if that worktree has tracked changes after retries, the candidate is partial retained progress and not just a generic retry-budget hold. Running lanes are not repeated as normal intake work. | | `Review & Landing` | Retained PR lanes after review handoff. This section owns post-review repair, wait-for-review, ready-to-land, closeout, cleanup, and blocked retained-lane visibility. | From d32135640b93219b8d6030127392ebc9ddbc12d2 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Mon, 11 May 2026 14:01:55 +0800 Subject: [PATCH 2/4] {"schema":"decodex/commit/1","summary":"Polish operator dashboard project state","authority":"manual"} --- .../src/orchestrator/operator_dashboard.html | 584 +++++++++--------- .../tests/operator/status/dashboard.rs | 142 ++++- .../tests/operator/status/http.rs | 5 +- 3 files changed, 401 insertions(+), 330 deletions(-) diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index 3cf1167c..6653c046 100644 --- a/apps/decodex/src/orchestrator/operator_dashboard.html +++ b/apps/decodex/src/orchestrator/operator_dashboard.html @@ -48,6 +48,28 @@ --fast: 140ms; --medium: 220ms; --slow: 380ms; + --type-micro: 9px; + --type-caption: 10px; + --type-label: 11px; + --type-meta: 12px; + --type-body: 13px; + --type-row-title: 14px; + --type-card-title: 16px; + --type-display: 18px; + --weight-label: 650; + --weight-strong: 650; + --tracking-caps: 0; + --space-2xs: 4px; + --space-xs: 6px; + --space-sm: 8px; + --space-account-row-y: 10px; + --space-md: 12px; + --space-row-y: 14px; + --space-panel-head-y: 16px; + --space-row-indent: 18px; + --space-section-x: 20px; + --space-section-y: 20px; + --space-card-y: 18px; --mono: "SFMono-Regular", "IBM Plex Mono", "Menlo", "Monaco", "Consolas", monospace; --sans: @@ -131,7 +153,7 @@ max-width: 1180px; margin: 0 auto; display: grid; - gap: 16px; + gap: var(--space-panel-head-y); } .masthead { @@ -176,7 +198,7 @@ h1 { margin: 0; - font-size: 1.14rem; + font-size: var(--type-display); letter-spacing: 0; line-height: 1; } @@ -191,9 +213,9 @@ .table-meta { font-family: var(--mono); - font-size: 11px; - font-weight: 600; - letter-spacing: 0.06em; + 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); @@ -258,8 +280,8 @@ backdrop-filter: blur(18px); list-style: none; font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.08em; + font-size: var(--type-caption); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--danger); cursor: pointer; @@ -302,8 +324,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 { @@ -337,8 +359,8 @@ display: block; margin-bottom: 3px; font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.07em; + font-size: var(--type-caption); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--text); } @@ -346,7 +368,7 @@ .notice-item p { margin: 0; color: var(--muted-strong); - font-size: 13px; + font-size: var(--type-body); line-height: 1.38; } @@ -374,8 +396,8 @@ align-items: center; justify-content: center; font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.08em; + font-size: var(--type-label); + letter-spacing: var(--tracking-caps); text-transform: uppercase; } @@ -468,8 +490,8 @@ min-width: 0; gap: 6px; font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.04em; + font-size: var(--type-label); + letter-spacing: var(--tracking-caps); line-height: 1; color: var(--muted); white-space: nowrap; @@ -486,7 +508,7 @@ overflow: hidden; max-width: min(42vw, 320px); color: var(--muted-strong); - font-weight: 600; + font-weight: var(--weight-label); text-overflow: ellipsis; white-space: nowrap; } @@ -521,8 +543,8 @@ .flow-stage span { font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.11em; + font-size: var(--type-caption); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--muted); } @@ -557,8 +579,8 @@ .flow-stage strong { color: var(--text); font-family: var(--mono); - font-size: 0.78rem; - font-weight: 600; + font-size: var(--type-meta); + font-weight: var(--weight-label); letter-spacing: 0; min-width: 0; overflow: hidden; @@ -618,17 +640,21 @@ 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-md) 0 var(--space-sm); border-top: 1px solid var(--line-strong); font-family: var(--mono); color: var(--muted); } + .section-marker > .table-meta { + white-space: nowrap; + } + .section-marker:first-child { margin-top: 0; - padding-top: 14px; + padding-top: var(--space-panel-head-y); } .section-marker span { @@ -637,9 +663,9 @@ gap: 10px; min-width: 0; color: var(--text); - font-size: 10px; - font-weight: 650; - letter-spacing: 0.12em; + 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; @@ -649,24 +675,10 @@ content: ""; flex: 0 0 auto; width: 2px; - height: 14px; + height: 16px; 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); } @@ -686,7 +698,7 @@ .panel { display: grid; gap: 0; - padding: 0 20px; + padding: 0 var(--space-section-x); border: 0; border-radius: 0; background: transparent; @@ -699,14 +711,14 @@ } #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; + padding-top: var(--space-panel-head-y); + padding-bottom: var(--space-md); } #account-pool-panel .panel-body { @@ -722,8 +734,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), @@ -737,9 +749,9 @@ .panel-head h2 { margin: 0; - font-size: 12px; - font-weight: 650; - letter-spacing: 0.14em; + font-size: var(--type-meta); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); text-transform: uppercase; font-family: var(--mono); color: var(--muted-strong); @@ -751,7 +763,7 @@ } #queue-panel .panel-head { - padding-top: 14px; + padding-top: var(--space-panel-head-y); } .panel-body { @@ -772,7 +784,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); @@ -797,16 +809,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, @@ -851,7 +863,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; } @@ -859,12 +871,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; } @@ -873,13 +885,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, @@ -888,8 +900,8 @@ flex-wrap: wrap; gap: 8px; font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.06em; + font-size: var(--type-label); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--muted); } @@ -908,7 +920,7 @@ .run-title { margin: 0; color: var(--text); - font-size: 16px; + font-size: var(--type-card-title); font-weight: 700; line-height: 1.28; } @@ -918,8 +930,8 @@ flex-wrap: wrap; gap: 8px 12px; font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.06em; + font-size: var(--type-label); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--muted); } @@ -928,7 +940,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; } @@ -938,13 +950,13 @@ gap: 12px; margin-top: 12px; font-family: var(--mono); - font-size: 12px; + font-size: var(--type-meta); color: var(--muted); } .status-line strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .activity-line { @@ -953,7 +965,7 @@ gap: 5px 14px; margin-top: 8px; font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); line-height: 1.4; color: var(--muted); } @@ -964,7 +976,7 @@ .activity-line strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .project-scope-list { @@ -973,7 +985,7 @@ } .project-scope-list.has-registered-projects { - gap: 12px; + gap: 0; } .project-subsection { @@ -983,34 +995,8 @@ } .project-subsection-head { - display: grid; - grid-template-columns: minmax(0, 1fr) max-content 14px; - align-items: center; - gap: 12px; - padding: 13px 0; - border-bottom: 1px solid var(--line); - color: var(--muted-strong); - font-family: var(--mono); - font-size: 12px; - font-weight: 650; - letter-spacing: 0.14em; - line-height: 1.3; - text-transform: uppercase; - } - - .project-subsection-head strong { - color: var(--text); - font-size: 11px; - font-weight: 700; - letter-spacing: 0; - text-transform: none; - white-space: nowrap; - } - - .project-subsection-head::after { - content: ""; - width: 14px; - height: 14px; + padding-top: var(--space-panel-head-y); + padding-bottom: var(--space-md); } .project-active-list { @@ -1021,7 +1007,7 @@ 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); } @@ -1049,7 +1035,7 @@ max-width: 100%; color: var(--muted-strong); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); line-height: 1.35; overflow-wrap: anywhere; } @@ -1063,7 +1049,7 @@ gap: 14px 28px; 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); @@ -1085,8 +1071,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; @@ -1144,20 +1130,8 @@ } .registered-projects > summary { - display: grid; - grid-template-columns: minmax(0, 1fr) max-content 14px; - align-items: center; - gap: 12px; - padding: 13px 0; - color: var(--muted-strong); - font-family: var(--mono); - font-size: 12px; - font-weight: 650; - letter-spacing: 0.14em; - line-height: 1.3; - text-transform: uppercase; - cursor: pointer; - list-style: none; + padding-top: var(--space-panel-head-y); + padding-bottom: var(--space-md); } .registered-projects > summary::-webkit-details-marker { @@ -1173,36 +1147,7 @@ outline-offset: 2px; } - .registered-projects > summary strong { - color: var(--text); - font-size: 11px; - font-weight: 700; - letter-spacing: 0; - text-transform: none; - white-space: nowrap; - } - - .registered-projects > summary::after { - content: "+"; - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - color: var(--muted); - font-size: 13px; - font-weight: 700; - letter-spacing: 0; - line-height: 1; - text-transform: none; - transition: color var(--fast) var(--ease); - } - - .registered-projects[data-detail-state="open"] > summary::after { - content: "-"; - } - - .registered-projects > summary:hover::after { + .registered-projects > summary:hover h2 { color: var(--text); } @@ -1212,8 +1157,8 @@ } .registered-project-list .project-entry { - padding-top: 14px; - padding-bottom: 14px; + padding-top: var(--space-md); + padding-bottom: var(--space-md); } .project-entry.tone-land { @@ -1241,7 +1186,7 @@ .project-kicker { color: var(--project-accent); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); letter-spacing: 0; line-height: 1.2; text-transform: uppercase; @@ -1250,7 +1195,7 @@ .project-activity { color: var(--muted); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); letter-spacing: 0; line-height: 1.2; } @@ -1266,7 +1211,7 @@ .project-title-line strong { min-width: 0; color: var(--text); - font-size: 17px; + font-size: var(--type-row-title); line-height: 1.26; overflow-wrap: anywhere; } @@ -1274,7 +1219,7 @@ .project-health { color: var(--project-accent); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); letter-spacing: 0; text-transform: uppercase; } @@ -1283,7 +1228,7 @@ min-width: 0; color: var(--muted-strong); font-family: var(--mono); - font-size: 12px; + font-size: var(--type-label); letter-spacing: 0; line-height: 1.45; overflow-wrap: anywhere; @@ -1295,7 +1240,7 @@ gap: 6px 16px; min-width: 0; font-family: var(--mono); - font-size: 12px; + font-size: var(--type-label); line-height: 1.45; color: var(--muted); } @@ -1314,7 +1259,7 @@ .project-stat-line strong { color: var(--text); - font-size: 16px; + font-size: var(--type-body); font-weight: 700; line-height: 1; } @@ -1323,7 +1268,7 @@ display: grid; gap: 3px; min-width: 0; - font-size: 10px; + font-size: var(--type-caption); letter-spacing: 0; line-height: 1.2; text-transform: uppercase; @@ -1366,9 +1311,9 @@ background: var(--surface-muted); color: var(--muted-strong); font-family: var(--mono); - font-size: 10px; - font-weight: 650; - letter-spacing: 0.06em; + font-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1; text-transform: uppercase; transition: @@ -1415,8 +1360,8 @@ .timing-label { font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.08em; + font-size: var(--type-caption); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--muted); } @@ -1424,7 +1369,7 @@ .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; @@ -1509,13 +1454,13 @@ border-top: 1px solid var(--line); min-width: 0; font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); color: var(--muted); } .account-use-line strong { color: var(--text); - font-weight: 650; + font-weight: var(--weight-label); } .account-use-line span { @@ -1533,7 +1478,7 @@ gap: 5px 10px; min-width: 0; font-family: var(--mono); - font-size: 10px; + font-size: var(--type-caption); color: var(--muted); } @@ -1552,7 +1497,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; @@ -1564,13 +1509,13 @@ 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-size: var(--type-caption); + font-weight: var(--weight-label); + letter-spacing: var(--tracking-caps); line-height: 1.15; text-transform: uppercase; } @@ -1643,7 +1588,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: @@ -1665,8 +1610,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; @@ -1762,8 +1707,8 @@ text-overflow: ellipsis; white-space: nowrap; color: var(--text); - font-size: 13px; - font-weight: 650; + font-size: var(--type-body); + font-weight: var(--weight-label); } .account-name-reroll { @@ -1810,7 +1755,7 @@ text-overflow: ellipsis; color: var(--muted); font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); font-weight: 500; line-height: 1.2; text-align: center; @@ -1853,9 +1798,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 { @@ -1863,8 +1808,8 @@ grid-column: 1; grid-row: 1; color: var(--text); - font-size: 13px; - font-weight: 650; + font-size: var(--type-body); + font-weight: var(--weight-label); line-height: 1.15; } @@ -1885,7 +1830,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; } @@ -1898,7 +1843,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; } @@ -1912,7 +1857,7 @@ width: auto; text-align: center; font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); color: var(--muted); } @@ -1924,7 +1869,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; @@ -1937,7 +1882,7 @@ .account-row-credit strong { color: var(--text); - font-weight: 650; + font-weight: var(--weight-label); } .account-row-credit.is-danger strong { @@ -1950,7 +1895,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: @@ -1998,7 +1943,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); } @@ -2016,7 +1961,7 @@ .child-activity-head strong, .child-context strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .child-share-list { @@ -2032,7 +1977,7 @@ gap: 10px; min-width: 0; font-family: var(--mono); - font-size: 11px; + font-size: var(--type-label); color: var(--muted); } @@ -2069,7 +2014,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; @@ -2093,7 +2038,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); } @@ -2130,7 +2075,7 @@ .child-bucket-signal strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } .child-bucket-bar { @@ -2153,8 +2098,8 @@ align-items: center; gap: 6px; font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.08em; + font-size: var(--type-label); + letter-spacing: var(--tracking-caps); text-transform: uppercase; } @@ -2289,17 +2234,17 @@ .queue-group-header h4 { margin: 0; - font-size: 0.76rem; + font-size: var(--type-meta); font-family: var(--mono); - letter-spacing: 0.1em; + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--muted); } .queue-group-count { font-family: var(--mono); - font-size: 0.76rem; - letter-spacing: 0.08em; + font-size: var(--type-meta); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--muted); } @@ -2312,16 +2257,16 @@ .field-label { font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.09em; + font-size: var(--type-label); + letter-spacing: var(--tracking-caps); text-transform: uppercase; 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; @@ -2330,7 +2275,7 @@ .field-value.is-time, .field-value.is-muted { - font-size: 12px; + font-size: var(--type-meta); font-weight: 500; color: var(--muted-strong); } @@ -2343,7 +2288,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); } @@ -2355,7 +2300,7 @@ .attention-facts strong { color: var(--text); - font-weight: 600; + font-weight: var(--weight-label); } details { @@ -2367,8 +2312,8 @@ summary { cursor: pointer; font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.08em; + font-size: var(--type-label); + letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--muted); user-select: none; @@ -2453,7 +2398,7 @@ padding: 8px 10px; background: var(--surface-muted); color: var(--muted-strong); - font-size: 12px; + font-size: var(--type-meta); } .attempt-row span { @@ -2507,7 +2452,7 @@ grid-template-columns: minmax(0, max-content) 14px; align-items: center; justify-content: end; - gap: 12px; + gap: var(--space-md); text-align: right; } @@ -2523,7 +2468,7 @@ height: 14px; color: var(--muted); font-family: var(--mono); - font-size: 13px; + font-size: var(--type-row-title); font-weight: 700; letter-spacing: 0; line-height: 1; @@ -2535,11 +2480,12 @@ content: "+"; } - .fold-panel[data-detail-state="open"] .fold-indicator::before { + details[data-detail-state="open"] > summary .fold-indicator::before { content: "-"; } - .fold-panel > summary:hover .fold-indicator { + .fold-panel > summary:hover .fold-indicator, + .registered-projects > summary:hover .fold-indicator { color: var(--text); } @@ -2547,9 +2493,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; @@ -2557,8 +2503,8 @@ } .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; } @@ -2566,9 +2512,9 @@ display: block; color: var(--muted); font-family: var(--mono); - font-size: 10px; + font-size: var(--type-caption); font-weight: 500; - letter-spacing: 0.07em; + letter-spacing: var(--tracking-caps); line-height: 1.3; text-transform: uppercase; } @@ -2576,7 +2522,7 @@ .empty-state .empty-copy { max-width: 48ch; color: var(--muted); - font-size: 11px; + font-size: var(--type-label); line-height: 1.35; } @@ -2663,7 +2609,7 @@ .account-pool-guide { padding-left: 18px; - font-size: 9px; + font-size: var(--type-micro); } .account-row { @@ -2893,7 +2839,6 @@

Decodex

aria-label="Control Plane group" > Control Plane -

Accounts

@@ -2927,7 +2872,7 @@

Accounts

aria-label="Projects group" > Projects -

All · Active

+

@@ -2941,7 +2886,6 @@

Accounts

aria-label="Execution group" > Execution -

Running · Intake

@@ -2973,7 +2917,6 @@

Intake Queue

aria-label="Closeout group" > Closeout -

Review · Recovery · History

@@ -3139,6 +3082,7 @@

Run History

aftercare: document.getElementById("section-marker-aftercare"), }, projectOverview: document.getElementById("project-overview"), + projectsMeta: document.getElementById("projects-meta"), accountPool: document.getElementById("account-pool"), accountPoolMeta: document.getElementById("account-pool-meta"), accountPrivacyToggle: document.getElementById("account-privacy-toggle"), @@ -3585,6 +3529,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) || @@ -3599,8 +3550,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" ) { @@ -4283,6 +4237,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; } @@ -6220,6 +6180,10 @@

Run History

if (run.continuation_pending) { return "The latest turn completed with a continuation pending."; } + if (runWaitReasonShowsExecutionProgress(run)) { + const focus = protocolActivityFocus(run) || humanizeToken(run.wait_reason).toLowerCase(); + return `Running through ${focus}.`; + } if (run.wait_reason) { return `Waiting on ${humanizeToken(run.wait_reason).toLowerCase()}.`; } @@ -6279,6 +6243,9 @@

Run History

if (run.continuation_pending) { return "Continuation pending"; } + if (runWaitReasonShowsExecutionProgress(run)) { + return humanizeToken(run.wait_reason); + } if (run.wait_reason) { return `Waiting on ${humanizeToken(run.wait_reason).toLowerCase()}`; } @@ -6588,7 +6555,12 @@

Run History

continue; } - if (run.phase === "retry_backoff" || run.phase === "waiting_continuation" || run.wait_reason) { + const runIsWaiting = + run.phase === "retry_backoff" || + run.phase === "waiting_continuation" || + (run.wait_reason && !runWaitReasonShowsExecutionProgress(run)); + + if (runIsWaiting) { waitingItems.push({ tone, scope: "Running", @@ -6777,40 +6749,36 @@

Run History

return null; } - function projectHasRecentActivity(project) { - const parsed = Date.parse(project.last_activity_at || ""); - if (!Number.isFinite(parsed)) { - return false; - } - - const ageSeconds = Math.floor(Date.now() / 1000) - Math.floor(parsed / 1000); - return ageSeconds >= 0 && ageSeconds <= 900; - } - - function projectHasVisibleWork(project) { + function projectHasActiveWork(project) { const workCount = (project.active_run_count ?? 0) + + (project.queued_candidate_count ?? 0) + (project.waiting_lane_count ?? 0) + (project.attention_count ?? 0) + - (project.post_review_lane_count ?? 0) + - (project.retained_worktree_count ?? 0); + (project.post_review_lane_count ?? 0); const connector = projectConnectorSummary(project); const syncNeedsAttention = project.enabled && ["backoff", "degraded", "stale"].includes(connector); - return workCount > 0 || (project.warning_count ?? 0) > 0 || syncNeedsAttention || projectHasRecentActivity(project); + return workCount > 0 || (project.warning_count ?? 0) > 0 || syncNeedsAttention; } function activeProjects(projects) { - return projects.filter(projectHasVisibleWork); + return projects.filter(projectHasActiveWork); } function projectHealth(project) { if (!project.enabled) { return { label: "disabled", tone: "tone-muted", title: "Disabled in registry" }; } + if ((project.active_run_count ?? 0) > 0) { + return { label: "running", tone: "tone-run", title: "Project has active running lanes" }; + } if ((project.attention_count ?? 0) > 0) { return { label: "needs attention", tone: "tone-blocked", title: "Operator attention required" }; } + if ((project.waiting_lane_count ?? 0) > 0) { + return { label: "waiting", tone: "tone-wait", title: "Project has lanes waiting to resume" }; + } if (project.connector_state === "backoff") { return { label: "sync backoff", @@ -6940,7 +6908,7 @@

Run History

const connectorCopy = projectSyncMeta(project, health); const attentionCopy = attention === "ok" || attention.includes("retained") ? "" : attention; - const activityCopy = lastActivity === "none" ? "" : `active ${lastActivity}`; + const activityCopy = lastActivity === "none" ? "" : `activity ${lastActivity}`; const metaItems = [connectorCopy].filter(Boolean); if (attentionCopy) { metaItems.splice(1, 0, attentionCopy); @@ -6978,8 +6946,16 @@

Run History

: `${projects.length} total`; return ` -
- All${escapeHtml(summary)} +
+ +
+

All

+
+
+

${escapeHtml(summary)}

+ +
+
${projects.map((project) => renderProjectEntry(project, selectedId, projects)).join("")}
@@ -6987,8 +6963,20 @@

Run History

`; } + function projectOverviewSummary(projects, activeProjectRows) { + const activeCount = activeProjectRows.length; + const idleCount = Math.max(projects.length - activeCount, 0); + + return `${activeCount} active · ${idleCount} idle · ${projects.length} total`; + } + + function activeProjectSummary(activeProjectRows) { + return `${activeProjectRows.length} active`; + } + function renderProjects(snapshot, derived) { if (!snapshot) { + nodes.projectsMeta.textContent = COPY.waitingSnapshot; nodes.projectOverview.classList.add("is-empty", "is-waiting"); nodes.projectOverview.innerHTML = renderEmptyState(COPY.waitingSnapshot); return; @@ -6998,6 +6986,7 @@

Run History

const activeProjectRows = activeProjects(projects); if (!projects.length) { + nodes.projectsMeta.textContent = "0 total"; nodes.projectOverview.classList.add("is-empty"); nodes.projectOverview.classList.remove("has-registered-projects"); nodes.projectOverview.classList.remove("is-waiting"); @@ -7009,31 +6998,29 @@

Run History

} const selectedId = selectedProjectId(snapshot, projects); - const activeSummary = activeProjectRows.length - ? pluralize(activeProjectRows.length, "active project") - : "none active"; const activeMarkup = activeProjectRows.length ? activeProjectRows.map((project) => renderProjectEntry(project, selectedId, projects)).join("") - : renderProjectEmptyState( - "No active project work", - `${pluralize(projects.length, "project")} idle. Open All when you need the full registry.`, - { showAction: false }, - ); + : ""; + nodes.projectsMeta.textContent = projectOverviewSummary(projects, activeProjectRows); nodes.projectOverview.classList.remove("is-empty", "is-waiting"); nodes.projectOverview.classList.toggle("is-empty", activeProjectRows.length === 0); nodes.projectOverview.classList.add("has-registered-projects"); nodes.projectOverview.innerHTML = ` - ${renderRegisteredProjects(projects, activeProjectRows, selectedId)}
-
- Active - ${escapeHtml(activeSummary)} +
+
+

Active

+
+
+

${escapeHtml(activeProjectSummary(activeProjectRows))}

+
${activeMarkup}
+ ${renderRegisteredProjects(projects, activeProjectRows, selectedId)} `; } @@ -7125,40 +7112,13 @@

Run History

return parts.join(" · "); } - function backlogEmptyState(snapshot, derived) { - if (!snapshot) { - return { - title: COPY.waitingSnapshot, - copy: "Queue data appears after /state publishes a snapshot.", - }; - } - if (derived.reviewOwnedQueueCount) { - return { - title: "No queued issues", - copy: "Issues already in Review & Landing are not counted here.", - }; - } - if (derived.queuedActiveOwned) { - return { - title: "No queued issues", - copy: `All locally claimed issues are already visible under ${COPY.runningLane}.`, - }; - } - if (derived.queuedClosed) { - return { - title: "No queued issues", - copy: "Closed automation labels are counted as stale, not queue work.", - }; - } - return { - title: "No queued issues", - copy: "Ready, capacity-limited, or blocked issues appear here before they start.", - }; - } - - function renderQueuedCandidates(container, items, emptyTitle, emptyCopy) { + function renderQueuedCandidates(container, items, snapshot) { if (!items.length) { - container.innerHTML = renderEmptyState(emptyTitle, emptyCopy); + renderRoutineEmptyList( + container, + snapshot, + "Queue data appears after /state publishes a snapshot.", + ); return; } @@ -7210,9 +7170,9 @@

${escapeHtml(candidate.title)}

.join(""); } - function renderActionCards(container, items, emptyTitle, emptyCopy) { + function renderActionCards(container, items, snapshot, waitingCopy = "") { if (!items.length) { - container.innerHTML = renderEmptyState(emptyTitle, emptyCopy); + renderRoutineEmptyList(container, snapshot, waitingCopy); return; } @@ -7289,9 +7249,10 @@

${escapeHtml(item.title)}

); if (!runs.length) { - nodes.activeRuns.innerHTML = renderEmptyState( - snapshot ? "No running lanes" : COPY.waitingSnapshot, - snapshot ? "Queued work appears here after dispatch." : "", + renderRoutineEmptyList( + nodes.activeRuns, + snapshot, + "Running lane data appears after /state publishes a snapshot.", ); return; } @@ -7307,7 +7268,7 @@

${escapeHtml(item.title)}

const account = codexAccount(run); const accountStatusBit = codexAccountStatusBit(account); - if (run.wait_reason) { + if (run.wait_reason && !runWaitReasonShowsExecutionProgress(run)) { statusBits.push(`wait ${escapeHtml(humanizeToken(run.wait_reason))}`); } if (!run.active_lease) { @@ -7652,6 +7613,12 @@

${escapeHtml(title)}

`; } + function recoveryWorktreeShouldDefaultOpen(renderedWorktree) { + const role = renderedWorktree.role; + + return role.tone === "tone-blocked" || role.label.startsWith("post-land"); + } + function renderWorktrees(snapshot) { const worktrees = snapshot?.worktrees ?? []; const renderedWorktrees = worktrees @@ -7669,7 +7636,10 @@

${escapeHtml(title)}

}); const retainedWorktrees = renderedWorktrees.filter(({ role }) => role.sortRank > 0); setFoldPanelEmpty(nodes.panels.worktrees, !retainedWorktrees.length); - syncDefaultDetailOpenState(nodes.panels.worktrees, retainedWorktrees.length > 0); + syncDefaultDetailOpenState( + nodes.panels.worktrees, + retainedWorktrees.some(recoveryWorktreeShouldDefaultOpen), + ); nodes.worktreesMeta.textContent = retainedWorktrees.length ? `${pluralize(retainedWorktrees.length, "worktree")} · retained or cleanup` @@ -8114,20 +8084,18 @@

${escapeHtml(worktree.branch_name)}

renderProjects(snapshot, derived); renderAccountPool(snapshot); renderActiveRuns(snapshot, derived); - const backlogEmpty = backlogEmptyState(snapshot, derived); renderQueuedCandidates( nodes.queuedCandidates, derived.queueBacklogCandidates, - backlogEmpty.title, - backlogEmpty.copy, + snapshot, ); nodes.queuedMeta.textContent = backlogMetaText(snapshot, derived); renderRecentRuns(snapshot); renderActionCards( nodes.reviewQueue, reviewItems, - "No PR lanes", - "PRs waiting for review, landing, or closeout appear here.", + snapshot, + "Review lane data appears after /state publishes a snapshot.", ); nodes.reviewLanesMeta.textContent = snapshot ? `${pluralize(derived.postReviewLanes.length, "PR")} · ${pluralize(derived.reviewBlockerCount, "needs attention", "need attention")} · ${derived.readyItems.length} ready · ${derived.reviewWaitingCount} waiting` diff --git a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs index e51a9d11..19f371be 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/dashboard.rs @@ -24,6 +24,58 @@ fn operator_dashboard_background_wash_stays_viewport_fixed() { ); } +#[test] +fn operator_dashboard_uses_shared_type_scale_for_operator_rows() { + let response = dashboard_response(); + let section_marker_title = response + .split(".section-marker span {") + .nth(1) + .expect("section marker title style should exist") + .split(".section-marker span::before") + .next() + .expect("section marker title style should end before marker rule"); + let panel_title = response + .split(".panel-head h2 {") + .nth(1) + .expect("panel title style should exist") + .split(".panel:hover .panel-head h2") + .next() + .expect("panel title style should end before hover rule"); + let section_marker_bar = response + .split(".section-marker span::before {") + .nth(1) + .expect("section marker bar style should exist") + .split(".section-marker-control") + .next() + .expect("section marker bar style should end before meta rule"); + + assert!(response.contains("--type-micro: 9px;")); + assert!(response.contains("--type-row-title: 14px;")); + assert!(response.contains("--type-card-title: 16px;")); + assert!(response.contains("--tracking-caps: 0;")); + assert!(response.contains("--space-panel-head-y: 16px;")); + assert!(response.contains("--space-row-y: 14px;")); + assert!(response.contains("--space-card-y: 18px;")); + assert!(response.contains("--space-row-indent: 18px;")); + assert!(section_marker_title.contains("font-size: var(--type-meta);")); + assert!(section_marker_bar.contains("height: 16px;")); + assert!(panel_title.contains("font-size: var(--type-meta);")); + assert!(response.contains("padding: var(--space-panel-head-y) 0 var(--space-md);")); + assert!(response.contains("padding: var(--space-row-y) 0 var(--space-row-y) var(--space-row-indent);")); + assert!(response.contains("padding: var(--space-card-y) 0 var(--space-card-y) var(--space-row-indent);")); + assert!(response.contains(".project-title-line strong")); + assert!(response.contains(".project-stat-line strong")); + assert!(response.contains(".account-row-id strong")); + assert!(response.contains(".run-title")); + assert!(response.contains("font-size: var(--type-row-title);")); + assert!(response.contains("font-size: var(--type-body);")); + assert!(response.contains("font-size: var(--type-card-title);")); + assert!(!response.contains("font-size: 17px;")); + assert!(!response.contains("letter-spacing: 0.14em;")); + assert!(!response.contains("padding: 18px 0 18px 18px;")); + assert!(!response.contains("padding: 20px 0 12px;")); +} + #[test] fn operator_dashboard_child_bucket_rows_split_time_bars_from_event_diagnostics() { let response = String::from_utf8( @@ -85,6 +137,10 @@ fn operator_dashboard_child_bucket_rows_split_time_bars_from_event_diagnostics() assert!(response.contains("width: var(--child-bucket-value-column);")); assert!(response.contains("runNeedsAttention")); assert!(response.contains("runCountsAsRunning")); + assert!(response.contains("runWaitReasonShowsExecutionProgress")); + assert!(response.contains("[\"model_execution\", \"tool_execution\", \"protocol_activity\"].includes(run.wait_reason)")); + assert!(response.contains("return `Running through ${focus}.`;")); + assert!(response.contains("run.wait_reason && !runWaitReasonShowsExecutionProgress(run)")); assert!(response.contains("runOperationRequiresLiveAgent")); assert!(response.contains("runProcessStoppedWithoutAttention")); assert!(response.contains("runStageLabel")); @@ -174,12 +230,14 @@ fn operator_dashboard_renders_account_usage_controls() { assert!(response.contains("section-marker section-marker-aftercare")); assert!(response.contains("Control Plane")); assert!(response.contains("Projects")); + assert!(response.contains("

")); + assert!(!response.contains("

All · Active

")); assert!(response.contains("Execution")); assert!(response.contains("Closeout")); - assert!(response.contains("

Accounts

")); - assert!(response.contains("All · Active")); - assert!(response.contains("Running · Intake")); - assert!(response.contains("Review · Recovery · History")); + assert!(!response.contains("

Accounts

")); + assert!(!response.contains("All · Active")); + assert!(!response.contains("Running · Intake")); + assert!(!response.contains("Review · Recovery · History")); assert!(!response.contains("data-fold-key=\"panel:projects\"")); assert!(response.contains("panel section-execution\" id=\"active-panel\"")); assert!(response.contains("panel section-aftercare\" id=\"review-panel\"")); @@ -248,8 +306,8 @@ fn operator_dashboard_renders_account_usage_controls() { assert!(response.contains("renderDashboardState(lastDashboardRender);")); assert!(response.contains(".table-meta[data-tone=\"active\"]")); assert!(response.contains(".table-meta[data-tone=\"attention\"]")); - assert!(response.contains("font-size: 11px;")); - assert!(response.contains("letter-spacing: 0.06em;")); + assert!(response.contains("font-size: var(--type-label);")); + assert!(response.contains("letter-spacing: var(--tracking-caps);")); assert!(response.contains("text-transform: uppercase;")); assert!(response.contains("setPanelMeta(nodes.accountPoolMeta, meta, activeCount > 0 ? \"active\" : \"\")")); assert!(response.contains("${pluralize(accounts.length, \"account\")} · ${activeCount} active")); @@ -374,7 +432,7 @@ fn operator_dashboard_accounts_keeps_window_status_and_credit_copy_compact() { assert!(!response.contains("max-width: 190px;")); assert!(!response.contains("max-width: 142px;")); assert!(response.contains("min-height: 42px;")); - assert!(response.contains("padding: 10px 0 10px 18px;")); + assert!(response.contains("padding: var(--space-account-row-y) 0 var(--space-account-row-y) var(--space-row-indent);")); assert!(response.contains("border-bottom: 1px solid var(--line);")); assert!(response.contains(".account-pool-list > .account-row:last-child")); assert!(response.contains("account-row-credit")); @@ -546,38 +604,80 @@ fn operator_dashboard_projects_keep_status_summary_compact() { assert!(response.contains("function projectCapacitySummary(project)")); assert!(response.contains("function renderProjectStats(project)")); - assert!(response.contains("function projectHasVisibleWork(project)")); + assert!(response.contains("function projectHasActiveWork(project)")); + assert!(!response.contains("function projectHasVisibleWork(project)")); assert!(response.contains("function activeProjects(projects)")); assert!(response.contains("function renderProjectEntry(project, selectedId, projects)")); assert!(response.contains("function renderRegisteredProjects(projects, activeProjectRows, selectedId)")); assert!(response.contains("function renderEmptyState(title, copy = \"\")")); + assert!(response.contains("function renderRoutineEmptyList(container, snapshot, waitingCopy = \"\")")); assert!(response.contains("nodes.projectOverview.innerHTML = renderEmptyState(COPY.waitingSnapshot);")); - assert!(response.contains("snapshot ? \"No running lanes\" : COPY.waitingSnapshot")); + assert!(response.contains("renderRoutineEmptyList(")); + assert!(response.contains("Running lane data appears after /state publishes a snapshot.")); + assert!(response.contains("Queue data appears after /state publishes a snapshot.")); + assert!(response.contains("Review lane data appears after /state publishes a snapshot.")); + assert!(response.contains("renderQueuedCandidates(")); + assert!(response.contains("renderActionCards(")); + assert!(!response.contains("No running lanes")); + assert!(!response.contains("No queued issues")); + assert!(!response.contains("No PR lanes")); assert!(response.contains("return projects.length === 1 ? \"Current\" : \"Selected\";")); assert!(response.contains("return \"\";")); assert!(!response.contains("

Projects

")); - assert!(!response.contains("id=\"projects-meta\"")); + assert!(response.contains("id=\"projects-meta\"")); assert!(!response.contains("project-panel-head")); - assert!(!response.contains("nodes.projectsMeta")); + assert!(response.contains("nodes.projectsMeta")); assert!(response.contains("role=\"group\" aria-label=\"Projects\"")); assert!(response.contains("const activeProjectRows = activeProjects(projects);")); - assert!(response.contains("No active project work")); - assert!(response.contains("Open All when you need the full registry.")); + assert!(response.contains(": \"\";")); + assert!(!response.contains("No active project work")); + assert!(!response.contains("Open All when you need the full registry.")); assert!(response.contains("class=\"project-subsection\" aria-label=\"Active projects\"")); - assert!(response.contains(".project-subsection-head::after")); - assert!(response.contains("grid-template-columns: minmax(0, 1fr) max-content 14px;")); + assert!(response.contains(".project-subsection-head {")); + assert!(response.contains("class=\"project-subsection-head panel-head\"")); + assert!(response.contains("

Active

")); + assert!(response.contains("function activeProjectSummary(activeProjectRows)")); + assert!(response.contains("

${escapeHtml(activeProjectSummary(activeProjectRows))}

")); + assert!(response.contains("${renderRegisteredProjects(projects, activeProjectRows, selectedId)}")); + assert!( + response + .find("class=\"project-subsection\" aria-label=\"Active projects\"") + .expect("active projects section should render") + < response + .find("${renderRegisteredProjects(projects, activeProjectRows, selectedId)}") + .expect("registered projects disclosure should render after active list") + ); + assert!(response.contains("function projectOverviewSummary(projects, activeProjectRows)")); + assert!(response.contains("nodes.projectsMeta.textContent = projectOverviewSummary(projects, activeProjectRows);")); + assert!(response.contains("nodes.projectsMeta.textContent = COPY.waitingSnapshot;")); + assert!(response.contains("nodes.projectsMeta.textContent = \"0 total\";")); + assert!(response.contains("justify-content: space-between;")); assert!(response.contains("class=\"project-active-list\" role=\"list\" aria-label=\"Active projects\"")); - assert!(response.contains("All${escapeHtml(summary)}")); + assert!(response.contains("class=\"registered-projects fold-panel\"")); + assert!(response.contains("")); + assert!(response.contains("

All

")); + assert!(response.contains("
")); + assert!(response.contains("

${escapeHtml(summary)}

")); + assert!(response.contains("")); assert!(response.contains("role=\"list\" aria-label=\"All projects\"")); assert!(response.contains(".project-active-list > .project-entry:last-child")); assert!(response.contains(".registered-project-list > .project-entry:last-child")); + assert!(response.contains("return projects.filter(projectHasActiveWork);")); + assert!(response.contains("project.queued_candidate_count ?? 0")); + assert!(response.contains("project.post_review_lane_count ?? 0")); + assert!(response.contains("return workCount > 0 || (project.warning_count ?? 0) > 0 || syncNeedsAttention;")); + assert!(!response.contains("project.retained_worktree_count ?? 0);")); + assert!(!response.contains("projectHasRecentActivity(project)")); assert!(response.contains("class=\"project-activity\"")); - assert!(response.contains("const activityCopy = lastActivity === \"none\" ? \"\" : `active ${lastActivity}`;")); + assert!(response.contains("const activityCopy = lastActivity === \"none\" ? \"\" : `activity ${lastActivity}`;")); + assert!(!response.contains("`active ${lastActivity}`")); assert!(response.contains("project.post_review_lane_count ?? 0")); assert!(response.contains("project.retained_worktree_count ?? 0")); assert!(response.contains("return pluralize(project.warning_count, \"warning\");")); assert!(response.contains("return `${pluralize(project.retained_worktree_count, \"worktree\")} retained`;")); + assert!(response.contains("return { label: \"running\", tone: \"tone-run\"")); assert!(response.contains("return { label: \"needs attention\", tone: \"tone-blocked\"")); + assert!(response.contains("return { label: \"waiting\", tone: \"tone-wait\"")); assert!(response.contains("label: \"sync backoff\"")); assert!(response.contains("label: \"sync degraded\"")); assert!(response.contains("return { label: \"ok\", tone: \"tone-land\"")); @@ -635,9 +735,11 @@ fn operator_dashboard_flow_counts_distinguish_intake_attention() { "${pluralize(derived.postReviewLanes.length, \"PR\")} · ${pluralize(derived.reviewBlockerCount, \"needs attention\", \"need attention\")}" )); assert!(response.contains("${pluralize(retainedWorktrees.length, \"worktree\")} · retained or cleanup")); - assert!( - response.contains("Ready, capacity-limited, or blocked issues appear here before they start.") - ); + assert!(response.contains("function recoveryWorktreeShouldDefaultOpen(renderedWorktree)")); + assert!(response.contains("role.tone === \"tone-blocked\" || role.label.startsWith(\"post-land\")")); + assert!(response.contains("retainedWorktrees.some(recoveryWorktreeShouldDefaultOpen)")); + assert!(!response.contains("syncDefaultDetailOpenState(nodes.panels.worktrees, retainedWorktrees.length > 0);")); + assert!(!response.contains("Ready, capacity-limited, or blocked issues appear here before they start.")); assert!(!response.contains("claimed without local lane")); assert!(!response.contains("const repairCount = attentionItems.length;")); } diff --git a/apps/decodex/src/orchestrator/tests/operator/status/http.rs b/apps/decodex/src/orchestrator/tests/operator/status/http.rs index 82bdb4ae..39e0971d 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/http.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/http.rs @@ -181,7 +181,8 @@ fn operator_state_endpoint_serves_dashboard_html_from_root_and_dashboard_route() assert!(response.contains("section-marker section-marker-projects")); assert!(!response.contains("

Projects

")); assert!(!response.contains("data-fold-key=\"panel:projects\"")); - assert!(response.contains("All")); + assert!(response.contains("

All

")); + assert!(response.contains("
")); assert!(response.contains("projectRegistrationCommand")); assert!( response.contains("decodex project add ~/.codex/decodex/projects/") @@ -240,7 +241,7 @@ fn operator_state_endpoint_serves_dashboard_html_from_root_and_dashboard_route() assert!(response.contains("animateDetail(details, !details.open)")); assert!(response.contains("width: min(380px, calc(100vw - 36px));")); assert!(response.contains(".notice-item p")); - assert!(response.contains("font-size: 13px;")); + assert!(response.contains("font-size: var(--type-body);")); assert!(!response.contains(".fold-panel.is-empty .fold-indicator")); assert!(!response.contains("details.classList.contains(\"is-empty\")")); assert!(!response.contains("Operator views")); From 3be1c4b2cef66705e80e93c0e6d17b027ded4f01 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Mon, 11 May 2026 18:42:37 +0800 Subject: [PATCH 3/4] {"schema":"decodex/commit/1","summary":"Polish operator dashboard presentation","authority":"manual"} --- .../src/orchestrator/operator_dashboard.html | 1418 +++++++++++------ .../tests/operator/status/dashboard.rs | 318 +++- .../tests/operator/status/http.rs | 13 +- docs/reference/operator-control-plane.md | 31 +- 4 files changed, 1158 insertions(+), 622 deletions(-) diff --git a/apps/decodex/src/orchestrator/operator_dashboard.html b/apps/decodex/src/orchestrator/operator_dashboard.html index 6653c046..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; @@ -49,31 +50,32 @@ --medium: 220ms; --slow: 380ms; --type-micro: 9px; - --type-caption: 10px; - --type-label: 11px; + --type-caption: 11px; + --type-label: 12px; --type-meta: 12px; --type-body: 13px; - --type-row-title: 14px; - --type-card-title: 16px; - --type-display: 18px; - --weight-label: 650; - --weight-strong: 650; + --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: 10px; + --space-account-row-y: 8px; --space-md: 12px; - --space-row-y: 14px; - --space-panel-head-y: 16px; + --space-row-y: 12px; + --space-panel-head-y: 12px; --space-row-indent: 18px; --space-section-x: 20px; - --space-section-y: 20px; - --space-card-y: 18px; + --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"] { @@ -95,6 +97,7 @@ --danger: #ef8a8a; --info: #83a8ff; --tone-queue: #92a8ff; + --tone-ready: #5cc59f; --tone-run: #67d9ef; --tone-review: #c59aff; --tone-land: #5cc59f; @@ -212,12 +215,11 @@ } .table-meta { - font-family: var(--mono); + 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); } @@ -233,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; @@ -279,10 +332,10 @@ background: var(--surface-strong); backdrop-filter: blur(18px); list-style: none; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-caption); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--danger); cursor: pointer; box-shadow: var(--shadow); @@ -358,10 +411,10 @@ .notice-item strong { display: block; margin-bottom: 3px; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-caption); + font-weight: var(--weight-strong); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--text); } @@ -395,10 +448,10 @@ display: inline-flex; align-items: center; justify-content: center; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; } .theme-option { @@ -489,8 +542,9 @@ align-items: center; min-width: 0; gap: 6px; - font-family: var(--mono); + 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); @@ -509,10 +563,17 @@ max-width: min(42vw, 320px); color: var(--muted-strong); 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); } @@ -542,11 +603,11 @@ } .flow-stage span { - font-family: var(--mono); - font-size: var(--type-caption); + font-family: var(--sans); + font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; - color: var(--muted); + color: var(--muted-strong); } .flow-stage-labels { @@ -578,9 +639,10 @@ .flow-stage strong { color: var(--text); - font-family: var(--mono); + 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; @@ -597,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); } @@ -642,40 +717,35 @@ justify-content: space-between; gap: var(--space-sm) var(--space-row-indent); margin: var(--space-section-y) var(--space-section-x) 0; - padding: var(--space-md) 0 var(--space-sm); + 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 > .table-meta { - white-space: nowrap; - } - .section-marker:first-child { margin-top: 0; 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); + 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; - height: 16px; + height: 14px; background: var(--section-tone); } @@ -715,12 +785,6 @@ background: transparent; } - #account-pool-panel .panel-head { - align-items: center; - padding-top: var(--space-panel-head-y); - padding-bottom: var(--space-md); - } - #account-pool-panel .panel-body { padding-bottom: 4px; } @@ -752,8 +816,7 @@ font-size: var(--type-meta); font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; - font-family: var(--mono); + font-family: var(--sans); color: var(--muted-strong); transition: color var(--fast) var(--ease); } @@ -899,10 +962,10 @@ display: flex; flex-wrap: wrap; gap: 8px; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--muted); } @@ -921,7 +984,7 @@ margin: 0; color: var(--text); font-size: var(--type-card-title); - font-weight: 700; + font-weight: var(--weight-strong); line-height: 1.28; } @@ -929,10 +992,10 @@ display: flex; flex-wrap: wrap; gap: 8px 12px; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--muted); } @@ -949,7 +1012,7 @@ flex-wrap: wrap; gap: 12px; margin-top: 12px; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-meta); color: var(--muted); } @@ -964,7 +1027,7 @@ flex-wrap: wrap; gap: 5px 14px; margin-top: 8px; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); line-height: 1.4; color: var(--muted); @@ -982,25 +1045,64 @@ .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-subsection { + .project-table { + --project-grid: + minmax(150px, 0.52fr) minmax(300px, 1.28fr) minmax(112px, 0.36fr) + minmax(116px, 0.34fr); display: grid; - gap: 0; + 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-subsection-head { - padding-top: var(--space-panel-head-y); - padding-bottom: var(--space-md); + .project-table-guide span { + min-width: 0; + text-align: center; } - .project-active-list { + .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 { @@ -1034,7 +1136,7 @@ width: fit-content; max-width: 100%; color: var(--muted-strong); - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); line-height: 1.35; overflow-wrap: anywhere; @@ -1045,8 +1147,10 @@ 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: var(--space-row-y) 0 var(--space-row-y) var(--space-row-indent); @@ -1056,8 +1160,7 @@ transition: background-color var(--fast) var(--ease); } - .project-active-list > .project-entry:last-child, - .registered-project-list > .project-entry:last-child { + .project-table-list > .project-entry:last-child { border-bottom: 0; } @@ -1125,46 +1228,14 @@ transform: scaleX(1); } - .registered-projects { - border-top: 1px solid var(--line); - } - - .registered-projects > summary { - padding-top: var(--space-panel-head-y); - padding-bottom: var(--space-md); - } - - .registered-projects > summary::-webkit-details-marker { - display: none; - } - - .registered-projects > summary:focus { - outline: none; - } - - .registered-projects > summary:focus-visible { - outline: 2px solid var(--info); - outline-offset: 2px; - } - - .registered-projects > summary:hover h2 { - color: var(--text); - } - - .registered-project-list { - display: grid; - border-top: 1px solid var(--line); - } - - .registered-project-list .project-entry { - padding-top: var(--space-md); - padding-bottom: var(--space-md); - } - .project-entry.tone-land { --project-accent: var(--tone-land); } + .project-entry.tone-ready { + --project-accent: var(--tone-ready); + } + .project-entry.tone-wait { --project-accent: var(--tone-wait); } @@ -1178,21 +1249,25 @@ } .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(--mono); + font-family: var(--sans); font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: 0; line-height: 1.2; - text-transform: uppercase; } .project-activity { + grid-area: activity; + justify-self: center; color: var(--muted); font-family: var(--mono); font-size: var(--type-label); @@ -1204,6 +1279,7 @@ display: flex; flex-wrap: wrap; align-items: baseline; + justify-content: center; gap: 8px 12px; min-width: 0; } @@ -1211,79 +1287,78 @@ .project-title-line strong { min-width: 0; color: var(--text); - font-size: var(--type-row-title); - line-height: 1.26; - overflow-wrap: anywhere; - } - - .project-health { - color: var(--project-accent); font-family: var(--mono); font-size: var(--type-label); - letter-spacing: 0; - text-transform: uppercase; + font-weight: var(--weight-label); + line-height: 1.26; + overflow-wrap: anywhere; } .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: var(--type-label); + 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); + overflow: hidden; + text-overflow: ellipsis; + } + + .project-path-tail { + flex: 0 0 auto; + color: var(--muted-strong); font-size: var(--type-label); - line-height: 1.45; - color: var(--muted); + 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: var(--type-body); - 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: var(--type-caption); + 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; } .run-control-line { @@ -1310,12 +1385,11 @@ border-radius: 999px; background: var(--surface-muted); color: var(--muted-strong); - font-family: var(--mono); - font-size: var(--type-caption); + 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), @@ -1361,8 +1435,8 @@ .timing-label { font-family: var(--mono); font-size: var(--type-caption); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--muted); } @@ -1383,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; @@ -1414,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 { + .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); + } + + .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; @@ -1453,11 +1613,18 @@ padding-top: 10px; border-top: 1px solid var(--line); min-width: 0; - font-family: var(--mono); + 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: var(--weight-label); @@ -1512,12 +1679,11 @@ 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-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 { @@ -1541,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); @@ -1646,7 +1821,7 @@ } .account-row.is-ready { - --account-accent: var(--info); + --account-accent: var(--success); } .account-row.is-selected::before { @@ -1697,17 +1872,23 @@ 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: var(--type-body); + font-size: var(--type-label); font-weight: var(--weight-label); } @@ -1754,7 +1935,7 @@ overflow: hidden; text-overflow: ellipsis; color: var(--muted); - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); font-weight: 500; line-height: 1.2; @@ -1808,7 +1989,7 @@ grid-column: 1; grid-row: 1; color: var(--text); - font-size: var(--type-body); + font-size: var(--type-label); font-weight: var(--weight-label); line-height: 1.15; } @@ -1856,7 +2037,7 @@ min-width: 0; width: auto; text-align: center; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); color: var(--muted); } @@ -2097,10 +2278,10 @@ display: inline-flex; align-items: center; gap: 6px; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; } .status-label::before { @@ -2131,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); @@ -2235,17 +2421,18 @@ .queue-group-header h4 { margin: 0; font-size: var(--type-meta); - font-family: var(--mono); + font-family: var(--sans); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--muted); } .queue-group-count { - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-meta); + font-weight: var(--weight-label); + font-variant-numeric: tabular-nums; letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--muted); } @@ -2256,10 +2443,10 @@ } .field-label { - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--muted); } @@ -2311,10 +2498,10 @@ summary { cursor: pointer; - font-family: var(--mono); + font-family: var(--sans); font-size: var(--type-label); + font-weight: var(--weight-label); letter-spacing: var(--tracking-caps); - text-transform: uppercase; color: var(--muted); user-select: none; transition: @@ -2365,8 +2552,7 @@ details.is-animating > .grid, details.is-animating > .attempt-list, - details.is-animating > .panel-body, - details.is-animating > .registered-project-list { + details.is-animating > .panel-body { overflow: hidden; transform-origin: top center; transition: @@ -2469,7 +2655,7 @@ color: var(--muted); font-family: var(--mono); font-size: var(--type-row-title); - font-weight: 700; + font-weight: var(--weight-label); letter-spacing: 0; line-height: 1; transition: @@ -2484,8 +2670,7 @@ content: "-"; } - .fold-panel > summary:hover .fold-indicator, - .registered-projects > summary:hover .fold-indicator { + .fold-panel > summary:hover .fold-indicator { color: var(--text); } @@ -2511,12 +2696,11 @@ .empty-state strong { display: block; color: var(--muted); - font-family: var(--mono); - font-size: var(--type-caption); - font-weight: 500; + 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 { @@ -2532,6 +2716,8 @@ .mono { font-family: var(--mono); + font-variant-ligatures: none; + letter-spacing: 0; } @keyframes reveal-panel { @@ -2593,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 { @@ -2618,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; } @@ -2769,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) { @@ -2836,31 +3023,11 @@

Decodex

- Control Plane + Accounts
-
- - -
@@ -2871,8 +3038,14 @@

Accounts

id="section-marker-projects" aria-label="Projects group" > - Projects -

+ + Projects + +
@@ -2923,7 +3096,7 @@

Intake Queue

Review & Landing

-

Waiting for snapshot

+

Snapshot pending

@@ -2983,6 +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 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"], @@ -3082,10 +3257,8 @@

Run History

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"), @@ -3117,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(); @@ -3136,7 +3312,7 @@

Run History

runningInlineMetaPlural: "already running", protocolEvent: "Protocol event", staleClosed: "closed labels", - waitingSnapshot: "Waiting for snapshot", + waitingSnapshot: "Snapshot pending", }; function escapeHtml(value) { @@ -3158,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"; @@ -3303,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.", }; } @@ -3331,7 +3573,7 @@

Run History

tone: "success", title: dashboardStreamState.lastEventAt ? `Last event ${formatRelativeTimestamp(dashboardStreamState.lastEventAt)}` - : "Dashboard WebSocket connected.", + : "WebSocket connected.", }; } @@ -3339,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.", }; } @@ -3618,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"; @@ -3667,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; @@ -3676,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); } @@ -3703,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); @@ -3752,7 +3994,7 @@

Run History

function queueClassificationGroups(items) { const groups = [ ["ready", "Ready"], - ["waiting", "Waiting for capacity"], + ["waiting", "Waiting"], ["claimed", "Claimed"], ["blocked", "Blocked"], ]; @@ -3917,6 +4159,43 @@

Run History

} } + 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 { + return window.localStorage.getItem(PROJECT_LOCATION_PRIVACY_STORAGE_KEY) === "hidden"; + } catch (_error) { + return false; + } + } + + function persistProjectLocationPrivacy(hidden) { + try { + window.localStorage.setItem( + PROJECT_LOCATION_PRIVACY_STORAGE_KEY, + hidden ? "hidden" : "visible", + ); + } catch (_error) { + /* Ignore storage failures and continue with the in-memory choice. */ + } + } + function normalizeDashboardSubscription(subscription = {}) { const clean = (value) => { const text = String(value || "").trim(); @@ -3930,22 +4209,74 @@

Run History

}; } + function eyeToggleIconMarkup() { + return ` + + + `; + } + + function accountPrivacyToggleMarkup() { + return ``; + } + + function projectLocationToggleMarkup() { + return ``; + } + 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"; + 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 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", + ); + toggle.title = visible ? "Hide project locations" : "Show project locations"; + } + } + + 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; @@ -4087,7 +4418,7 @@

Run History

} function detailContent(details) { - return details.querySelector(":scope > .panel-body, :scope > .grid, :scope > .attempt-list, :scope > .registered-project-list"); + return details.querySelector(":scope > .panel-body, :scope > .grid, :scope > .attempt-list"); } function rememberDetailOpenState(details, isOpen) { @@ -4317,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, }; } @@ -4479,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"; } } @@ -4497,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"; } } @@ -4733,7 +5064,7 @@

Run History

} return ` -
+
${facts .map( ([label, value]) => ` @@ -5068,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 ""; @@ -5119,7 +5442,8 @@

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 ( codexAccountUsageLimited(account) || @@ -5129,24 +5453,15 @@

Run History

) { 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"; - } + if (status === "available") { + return "ready"; + } - return ""; - } + return ""; + } function codexAccountStatusLabel(account) { if (!account) { @@ -5155,10 +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"; } @@ -5297,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 `