diff --git a/src/core/services/budgetService.ts b/src/core/services/budgetService.ts
index 8c2c6d5..44482bf 100644
--- a/src/core/services/budgetService.ts
+++ b/src/core/services/budgetService.ts
@@ -11,8 +11,9 @@ import { getRoster, inferKind, type RosterEntry } from './rosterService.js';
* 1. REAL — `doneTokens` recorded on a task (via `task done --tokens`).
* 2. EST. — an estimate derived from time-on-task: an active agent burns
* roughly TOKENS_PER_ACTIVE_MIN while a task sits in_progress /
- * review, so the estimate grows live as work happens. Done tasks
- * without a recorded count fall back to their total active span.
+ * review, capped aggressively so stale status does not dominate.
+ * Done tasks without a recorded count fall back to their total
+ * active span with a separate cap.
*
* Every estimated figure is flagged `estimated:true` so the UI can mark it
* with a "~" — no silent fabrication.
@@ -22,7 +23,7 @@ import { getRoster, inferKind, type RosterEntry } from './rosterService.js';
export const TOKENS_PER_ACTIVE_MIN = 3000;
/**
- * Hard cap on ESTIMATED active minutes per task. Wall-clock time-in-state is a
+ * Hard cap on ESTIMATED done-task minutes. Wall-clock time-in-state is a
* bad proxy for actual compute: a task can sit "in review" or "done" for days
* while the real work was minutes. Without this cap a task open for 12 days
* would estimate ~50M tokens. A single coding task rarely exceeds ~45 min of
@@ -31,6 +32,13 @@ export const TOKENS_PER_ACTIVE_MIN = 3000;
*/
export const MAX_ACTIVE_MIN_PER_TASK = 45;
+/**
+ * Active in_progress/review tasks are the least reliable source: external
+ * agents can stay in a state for hours/days while doing no compute. Keep the
+ * live estimate useful without letting one stale task flatten the distribution.
+ */
+export const MAX_LIVE_ESTIMATE_MIN_PER_TASK = 12;
+
/** USD → EUR display rate (rough, labelled approximate in the UI). */
const USD_TO_EUR = 0.92;
@@ -87,6 +95,8 @@ function tokensForTask(cwd: string, id: string, status: string, createdAt: strin
}
} else if (status === 'in_progress' || status === 'review') {
ms = now - (isNaN(updated) ? now : updated); // since it was claimed / sent to review
+ const minutes = Math.min(Math.max(0, ms) / 60_000, MAX_LIVE_ESTIMATE_MIN_PER_TASK);
+ return { tokens: Math.round(minutes * TOKENS_PER_ACTIVE_MIN), estimated: true };
} else {
return { tokens: 0, estimated: true }; // open / cancelled — no work yet
}
@@ -191,7 +201,7 @@ export function computeBudget(cwd: string): BudgetReport {
totals: { tokens: totalTokens, costEur: totalCost, estimated: anyEstimated },
assumptions: {
tokensPerActiveMin: TOKENS_PER_ACTIVE_MIN,
- note: `External CLIs do not report tokens; figures marked ~ are estimated from active time-on-task (capped at ${MAX_ACTIVE_MIN_PER_TASK} min/task). Cost is a directional estimate at blended model rates, not billing.`,
+ note: `External CLIs do not report tokens; figures marked ~ are estimated from active time-on-task (live states capped at ${MAX_LIVE_ESTIMATE_MIN_PER_TASK} min/task, done fallbacks at ${MAX_ACTIVE_MIN_PER_TASK} min/task). Cost is a directional estimate at blended model rates, not billing.`,
},
generatedAt: new Date().toISOString(),
};
diff --git a/src/server/board.ts b/src/server/board.ts
index 36eb93a..cb7582c 100644
--- a/src/server/board.ts
+++ b/src/server/board.ts
@@ -249,7 +249,7 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(280px, 1fr);
gap: 14px;
- align-items: start;
+ align-items: stretch;
}
.board-area {
min-width: 0;
@@ -261,8 +261,9 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
- align-items: start;
+ align-items: stretch;
min-width: 0;
+ height: clamp(420px, calc(100vh - 210px), 760px);
}
.column {
min-width: 0;
@@ -270,7 +271,9 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px;
- min-height: 88px;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
box-shadow: 0 14px 36px rgba(2, 6, 18, .18);
}
.col-head {
@@ -305,6 +308,10 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
gap: 8px;
min-width: 0;
+ min-height: 0;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding-right: 2px;
align-items: start;
}
.card {
@@ -316,8 +323,7 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
border-left: 3px solid var(--accent);
border-radius: 8px;
padding: 9px 10px;
- min-height: 164px;
- aspect-ratio: 1;
+ min-height: 142px;
display: flex;
flex-direction: column;
user-select: none;
@@ -426,32 +432,6 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
}
.empty { color: var(--muted); font-size: 12px; padding: 4px 2px; }
- .panels {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
- margin-top: 20px;
- }
- .panel {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 12px 14px;
- }
- .panel h2 { font-size: 13px; margin: 0 0 10px; font-weight: 700; }
- .row {
- display: flex;
- gap: 8px;
- align-items: center;
- padding: 7px 0;
- border-top: 1px solid var(--border);
- }
- .row:first-of-type { border-top: 0; }
- .row .what { flex: 1; min-width: 0; overflow-wrap: anywhere; }
- .row .who-cell { display: flex; align-items: center; gap: 4px; font-size: 12px; white-space: nowrap; }
- .row .who { display: flex; align-items: center; gap: 4px; color: var(--accent); font-size: 11px; white-space: nowrap; }
- .row .when { color: var(--muted); font: 11px/1.4 var(--mono); white-space: nowrap; }
-
@media (max-width: 1180px) {
.dashboard { grid-template-columns: 1fr; }
}
@@ -460,7 +440,6 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
.app-header { padding-left: 14px; padding-right: 14px; }
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.board { grid-template-columns: repeat(2, 1fr); }
- .panels { grid-template-columns: 1fr; }
.project-name { max-width: 50vw; }
}
@media (max-width: 560px) {
@@ -589,7 +568,6 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
/* ── In-card live agent console ─────────────────────────────────────── */
.card-console-wrap { margin-top: 9px; }
.card:has(.card-console:not([hidden])) {
- aspect-ratio: auto;
min-height: 230px;
}
.card-console-toggle {
@@ -655,6 +633,36 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
color: var(--text);
}
.budget-head .total small { color: var(--muted); font-weight: 400; }
+ .insight-actions {
+ flex-basis: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ }
+ .seg {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ padding: 3px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: rgba(15, 23, 42, .46);
+ }
+ .seg button, .reset-btn {
+ min-height: 28px;
+ border: 0;
+ border-radius: 6px;
+ color: var(--muted);
+ background: transparent;
+ font: 700 11px/1 var(--mono);
+ padding: 0 9px;
+ cursor: pointer;
+ transition: color 160ms ease, background 160ms ease;
+ }
+ .seg button.active { color: var(--text); background: var(--raised); }
+ .reset-btn { border: 1px solid rgba(148, 163, 184, .22); }
+ .reset-btn:hover, .seg button:hover { color: var(--text); background: rgba(148, 163, 184, .12); }
.donut-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
.donut-card {
min-width: 0;
@@ -664,6 +672,17 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
background: rgba(15, 23, 42, .44);
}
.donut-title { margin: 0 0 6px; color: var(--muted); font: 700 11px/1.2 var(--mono); text-transform: uppercase; letter-spacing: .05em; }
+ .donut-tabs { display: flex; justify-content: flex-end; gap: 3px; margin: -2px 0 4px; }
+ .donut-tabs button {
+ min-height: 26px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: transparent;
+ color: var(--muted);
+ font: 700 10.5px/1 var(--mono);
+ padding: 0 8px;
+ }
+ .donut-tabs button.active { background: var(--raised); color: var(--text); }
.half-donut { position: relative; min-height: 132px; display: grid; place-items: center; }
.half-donut svg { width: min(220px, 100%); height: auto; overflow: visible; }
.donut-segment {
@@ -690,7 +709,7 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
.legend-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.agent-bars { margin-top: 14px; }
.agent-bars h3 { margin: 0 0 10px; font: 700 12px/1.2 var(--mono); text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
- .agent-bar-row { display: grid; grid-template-columns: minmax(96px, 1fr) 1.35fr; gap: 10px; align-items: center; padding: 8px 0; border-top: 1px solid var(--border); }
+ .agent-bar-row { display: grid; grid-template-columns: minmax(96px, 1fr) 1.35fr; gap: 10px; align-items: center; padding: 8px 0; border-top: 1px solid var(--border); transition: transform 260ms cubic-bezier(.2,.7,.2,1); }
.agent-bar-row:first-of-type { border-top: 0; }
.budget-agent { display: flex; align-items: center; gap: 8px; min-width: 0; }
.budget-agent .ba-core {
@@ -748,7 +767,10 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
}
@media (max-width: 920px) {
.cards { grid-template-columns: 1fr; }
- .card { aspect-ratio: auto; min-height: 142px; }
+ .board { height: auto; }
+ .column { max-height: 64vh; }
+ .cards { grid-template-columns: 1fr; }
+ .card { min-height: 142px; }
}
@media (prefers-reduced-motion: reduce) {
@@ -807,17 +829,6 @@ export function renderBoardHtml(projectName = 'AgentHub Project'): string {
Handoffs
- Decisions
-