codecartographer-pi 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/dashboard.js +165 -15
- package/package.json +1 -1
package/dist/core/dashboard.js
CHANGED
|
@@ -14,6 +14,7 @@ export function renderDashboard(inputs) {
|
|
|
14
14
|
`<div class="cc-main">`,
|
|
15
15
|
renderHeader(inputs),
|
|
16
16
|
renderStalenessWarning(inputs),
|
|
17
|
+
renderHealthPanel(inputs),
|
|
17
18
|
inputs.narration ? renderNarration(inputs.narration, completedPhaseCount(inputs.status)) : "",
|
|
18
19
|
renderKeyResults(inputs),
|
|
19
20
|
renderProgressBar(inputs.pipeline, inputs.status),
|
|
@@ -84,7 +85,7 @@ function renderHeader(inputs) {
|
|
|
84
85
|
const { status, pipeline, packageVersion, generatedAt } = inputs;
|
|
85
86
|
const projectName = status.project_name || "(unnamed project)";
|
|
86
87
|
const pipelineLabel = pipeline.workflow_name || status.pipeline;
|
|
87
|
-
const currentPhase = status
|
|
88
|
+
const currentPhase = displayCurrentPhase(status);
|
|
88
89
|
const totals = computeTotals(inputs.usage);
|
|
89
90
|
const completed = completedPhaseCount(status);
|
|
90
91
|
const total = pipeline.phase_order.length;
|
|
@@ -100,7 +101,7 @@ function renderHeader(inputs) {
|
|
|
100
101
|
renderStat("Pipeline", pipelineLabel),
|
|
101
102
|
renderStat("Current phase", currentPhase),
|
|
102
103
|
renderStat("Progress", `${completed}/${total} phases`),
|
|
103
|
-
renderStat("Recorded tokens",
|
|
104
|
+
renderStat("Recorded tokens", formatTokensForDashboard(inputs.usage)),
|
|
104
105
|
renderStat("Tool uses", String(totals.tool_uses)),
|
|
105
106
|
renderStat("Package", `v${packageVersion}`),
|
|
106
107
|
`</div>`,
|
|
@@ -125,6 +126,66 @@ function renderStalenessWarning(inputs) {
|
|
|
125
126
|
return "";
|
|
126
127
|
return `<section class="cc-warning" data-section><strong>Dashboard may be stale.</strong> Status was updated at ${escapeHtml(inputs.status.last_updated)} after this dashboard was generated at ${escapeHtml(inputs.generatedAt)}. Regenerate with <code>/codecarto-dashboard</code>.</section>`;
|
|
127
128
|
}
|
|
129
|
+
function renderHealthPanel(inputs) {
|
|
130
|
+
const { status, pipeline, usage } = inputs;
|
|
131
|
+
const completed = completedPhaseCount(status);
|
|
132
|
+
const total = pipeline.phase_order.length;
|
|
133
|
+
const totals = computeTotals(usage);
|
|
134
|
+
const issues = collectDashboardIssues(inputs);
|
|
135
|
+
const openQuestionCount = countOpenQuestions(status);
|
|
136
|
+
const carryForwardCount = countCarryForward(status);
|
|
137
|
+
const health = issues.some((i) => i.severity === "blocker") ? "attention required" : issues.length ? "review recommended" : completed === total ? "complete" : "on track";
|
|
138
|
+
const healthClass = issues.some((i) => i.severity === "blocker") ? "bad" : issues.length ? "warn" : "ok";
|
|
139
|
+
const tokenText = usageHasTokenAccounting(usage) ? formatTokenCount(totals.tokens.input + totals.tokens.output) : usage.runs.length ? "unavailable" : "0";
|
|
140
|
+
const issueMarkup = issues.length
|
|
141
|
+
? `<div class="cc-health-issues">${issues.map(renderDashboardIssue).join("\n")}</div>`
|
|
142
|
+
: `<p class="cc-health-ok">No blocking artifact gaps detected.</p>`;
|
|
143
|
+
return [
|
|
144
|
+
`<section class="cc-card cc-health cc-health-${healthClass}" aria-label="Dashboard health" data-section data-search-text="health status blockers missing artifacts open questions">`,
|
|
145
|
+
`<div class="cc-health-hero">`,
|
|
146
|
+
`<div><div class="cc-eyebrow">Pipeline health</div><h2>${escapeHtml(health)}</h2><p>${escapeHtml(status.current_phase && status.current_phase !== "complete" ? `Current phase: ${status.current_phase}` : "Pipeline complete — all phases have finished.")}</p></div>`,
|
|
147
|
+
`<div class="cc-health-ring" aria-label="${completed} of ${total} phases complete"><strong>${completed}/${total}</strong><span>phases</span></div>`,
|
|
148
|
+
`</div>`,
|
|
149
|
+
`<div class="cc-health-grid">`,
|
|
150
|
+
renderHealthMetric("Artifacts needing attention", String(issues.length), issues.length ? "bad" : "ok"),
|
|
151
|
+
renderHealthMetric("Open questions", String(openQuestionCount), openQuestionCount ? "warn" : "ok"),
|
|
152
|
+
renderHealthMetric("Carry-forward items", String(carryForwardCount), carryForwardCount ? "warn" : "ok"),
|
|
153
|
+
renderHealthMetric("Tool uses", String(totals.tool_uses), "neutral"),
|
|
154
|
+
renderHealthMetric("Runtime", formatMillis(totals.duration_ms), "neutral"),
|
|
155
|
+
renderHealthMetric("Tokens", tokenText, tokenText === "unavailable" ? "warn" : "neutral"),
|
|
156
|
+
`</div>`,
|
|
157
|
+
issueMarkup,
|
|
158
|
+
`</section>`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
function renderHealthMetric(label, value, tone) {
|
|
162
|
+
return `<div class="cc-health-metric cc-tone-${tone}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
163
|
+
}
|
|
164
|
+
function renderDashboardIssue(issue) {
|
|
165
|
+
const path = issue.path ? `<code>${escapeHtml(issue.path)}</code>` : "";
|
|
166
|
+
return `<article class="cc-health-issue cc-issue-${issue.severity}"><div><span class="cc-pill ${issue.severity === "blocker" ? "cc-pill-bad" : ""}">${escapeHtml(issue.severity)}</span><a href="#${phaseAnchor(issue.phaseId)}">${escapeHtml(issue.phaseId)}</a></div><strong>${escapeHtml(issue.title)}</strong><p>${escapeHtml(issue.detail)} ${path}</p></article>`;
|
|
167
|
+
}
|
|
168
|
+
function collectDashboardIssues(inputs) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const phaseId of inputs.pipeline.phase_order) {
|
|
171
|
+
const phase = getPhase(inputs.pipeline, phaseId);
|
|
172
|
+
if (!phase?.primary_output)
|
|
173
|
+
continue;
|
|
174
|
+
const state = phaseRenderState(inputs.status, phaseId);
|
|
175
|
+
const primary = inputs.outputsPresent.get(phaseId)?.primary;
|
|
176
|
+
const shouldExist = state === "complete" || state === "current" || state === "running";
|
|
177
|
+
if (shouldExist && primary?.exists === false) {
|
|
178
|
+
out.push({
|
|
179
|
+
severity: state === "complete" || state === "current" ? "blocker" : "warning",
|
|
180
|
+
phaseId,
|
|
181
|
+
title: "Required primary output is missing",
|
|
182
|
+
detail: state === "complete" ? "Phase is marked complete but the dashboard cannot find its primary artifact at" : "This phase is active or ready, but its required artifact is not present at",
|
|
183
|
+
path: phase.primary_output,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
128
189
|
function renderNarration(narration, currentCompletedCount) {
|
|
129
190
|
const runsSince = Math.max(0, currentCompletedCount - narration.phaseCountAtGeneration);
|
|
130
191
|
const staleness = runsSince === 0 ? "current" : `${runsSince} run${runsSince === 1 ? "" : "s"} since`;
|
|
@@ -305,38 +366,71 @@ function renderPhaseOwnerNotes(phaseState) {
|
|
|
305
366
|
function renderPhaseLastRun(run) {
|
|
306
367
|
if (!run)
|
|
307
368
|
return `<div class="cc-phase-section"><h3>Last run</h3><p class="cc-muted">No usage record for this phase.</p></div>`;
|
|
308
|
-
const tokensTotal = (run
|
|
369
|
+
const tokensTotal = formatRunTokens(run);
|
|
309
370
|
const sessionLink = run.session_file ? renderSafeLink(run.session_file, "transcript") : undefined;
|
|
310
371
|
const session = sessionLink ? `<dt>Session</dt><dd>${sessionLink}</dd>` : "";
|
|
311
|
-
return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(
|
|
372
|
+
return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(tokensTotal)}</dd>`, `<dt>Duration</dt><dd>${escapeHtml(formatMillis(run.duration_ms))}</dd>`, session, `</dl>`, `</div>`].join("");
|
|
312
373
|
}
|
|
313
374
|
function renderUsagePanel(inputs) {
|
|
314
375
|
const { usage, pipeline, status } = inputs;
|
|
315
376
|
const totals = computeTotals(usage);
|
|
316
377
|
const perPhase = computePerPhaseTotals(usage);
|
|
378
|
+
const tokenAccounting = usageHasTokenAccounting(usage);
|
|
379
|
+
const maxTools = Math.max(1, ...[...perPhase.values()].map((t) => t.tool_uses));
|
|
380
|
+
const maxDuration = Math.max(1, ...[...perPhase.values()].map((t) => t.duration_ms));
|
|
317
381
|
const rows = pipeline.phase_order.map((phaseId) => {
|
|
318
382
|
const t = perPhase.get(phaseId);
|
|
319
383
|
if (!t) {
|
|
320
384
|
const complete = status.phases[phaseId]?.status === "complete";
|
|
321
|
-
return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
|
|
385
|
+
return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
|
|
322
386
|
}
|
|
323
387
|
const tokensTotal = t.tokens.input + t.tokens.output;
|
|
324
|
-
return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(formatTokenCount(tokensTotal))}</td><td>${t.tool_uses}</td><td>${
|
|
388
|
+
return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(tokenAccounting ? formatTokenCount(tokensTotal) : "unavailable")}</td><td>${renderUsageBar(t.tool_uses, maxTools, String(t.tool_uses))}</td><td>${renderUsageBar(t.duration_ms, maxDuration, formatMillis(t.duration_ms))}</td><td>${usagePhaseNote(phaseId, status)}</td></tr>`;
|
|
325
389
|
}).join("");
|
|
326
390
|
return [
|
|
327
391
|
`<section class="cc-card cc-usage" id="usage" aria-label="Token usage" data-section data-search-text="usage tokens tool duration">`,
|
|
328
392
|
`<div class="cc-section-head"><h2>Usage</h2><span>${totals.runs} runs</span></div>`,
|
|
329
|
-
usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals)}</dl>`,
|
|
393
|
+
usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals, tokenAccounting)}</dl>`,
|
|
394
|
+
renderUsageInsights(perPhase, status),
|
|
330
395
|
`<table class="cc-usage-table">`,
|
|
331
|
-
`<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th></tr></thead>`,
|
|
396
|
+
`<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th><th>State</th></tr></thead>`,
|
|
332
397
|
`<tbody>${rows}</tbody>`,
|
|
333
398
|
`</table>`,
|
|
334
399
|
`</section>`,
|
|
335
400
|
].join("\n");
|
|
336
401
|
}
|
|
337
|
-
function renderUsageTotalsList(totals) {
|
|
402
|
+
function renderUsageTotalsList(totals, tokenAccounting) {
|
|
338
403
|
const tokensTotal = totals.tokens.input + totals.tokens.output;
|
|
339
|
-
|
|
404
|
+
const tokenDetail = tokenAccounting
|
|
405
|
+
? `${escapeHtml(formatTokenCount(totals.tokens.input))} / ${escapeHtml(formatTokenCount(totals.tokens.output))} / ${escapeHtml(formatTokenCount(totals.tokens.cache_write))}`
|
|
406
|
+
: `<span class="cc-muted">unavailable — host did not report token counts</span>`;
|
|
407
|
+
const tokenTotal = tokenAccounting ? escapeHtml(formatTokenCount(tokensTotal)) : `<span class="cc-muted">unavailable</span>`;
|
|
408
|
+
return [`<dt>Total runs</dt><dd>${totals.runs}</dd>`, `<dt>Tokens (in / out / cache)</dt><dd>${tokenDetail}</dd>`, `<dt>Total tokens</dt><dd>${tokenTotal}</dd>`, `<dt>Tool uses</dt><dd>${totals.tool_uses}</dd>`, `<dt>Total duration</dt><dd>${escapeHtml(formatMillis(totals.duration_ms))}</dd>`].join("");
|
|
409
|
+
}
|
|
410
|
+
function renderUsageInsights(perPhase, status) {
|
|
411
|
+
if (perPhase.size === 0)
|
|
412
|
+
return "";
|
|
413
|
+
const entries = [...perPhase.entries()];
|
|
414
|
+
const longest = entries.reduce((best, entry) => entry[1].duration_ms > best[1].duration_ms ? entry : best, entries[0]);
|
|
415
|
+
const mostTools = entries.reduce((best, entry) => entry[1].tool_uses > best[1].tool_uses ? entry : best, entries[0]);
|
|
416
|
+
const current = status.current_phase ? perPhase.get(status.current_phase) : undefined;
|
|
417
|
+
return [
|
|
418
|
+
`<div class="cc-usage-insights">`,
|
|
419
|
+
renderInsight("Longest phase", longest[0], formatMillis(longest[1].duration_ms)),
|
|
420
|
+
renderInsight("Most tool-heavy", mostTools[0], `${mostTools[1].tool_uses} tools`),
|
|
421
|
+
current && status.current_phase ? renderInsight("Current phase usage", status.current_phase, `${current.runs} run${current.runs === 1 ? "" : "s"} · ${formatMillis(current.duration_ms)}`) : "",
|
|
422
|
+
`</div>`,
|
|
423
|
+
].filter(Boolean).join("\n");
|
|
424
|
+
}
|
|
425
|
+
function renderInsight(label, phaseId, value) {
|
|
426
|
+
return `<article class="cc-insight"><span>${escapeHtml(label)}</span><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a><strong>${escapeHtml(value)}</strong></article>`;
|
|
427
|
+
}
|
|
428
|
+
function renderUsageBar(value, max, label) {
|
|
429
|
+
const pct = Math.max(3, Math.min(100, Math.round((value / max) * 100)));
|
|
430
|
+
return `<span class="cc-bar-cell"><span class="cc-bar" style="--cc-bar:${pct}%"></span><span>${escapeHtml(label)}</span></span>`;
|
|
431
|
+
}
|
|
432
|
+
function usagePhaseNote(phaseId, status) {
|
|
433
|
+
return escapeHtml(phaseRenderState(status, phaseId));
|
|
340
434
|
}
|
|
341
435
|
function renderActivityTimeline(runs) {
|
|
342
436
|
if (runs.length === 0)
|
|
@@ -344,17 +438,19 @@ function renderActivityTimeline(runs) {
|
|
|
344
438
|
const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
|
|
345
439
|
const visible = sorted.slice(0, TIMELINE_VISIBLE_COUNT);
|
|
346
440
|
const overflow = sorted.slice(TIMELINE_VISIBLE_COUNT);
|
|
441
|
+
const hasSessionLinks = sorted.some((run) => Boolean(run.session_file && safeRelativeHref(run.session_file)));
|
|
347
442
|
const rowOf = (run) => {
|
|
348
|
-
const tokensTotal = (run
|
|
443
|
+
const tokensTotal = formatRunTokens(run);
|
|
349
444
|
const sessionCell = run.session_file ? (renderSafeLink(run.session_file, "session") ?? "—") : "—";
|
|
350
|
-
return `<tr><td>${escapeHtml(run.timestamp)}</td><td><a href="#${phaseAnchor(run.phase)}">${escapeHtml(run.phase)}</a></td><td>${escapeHtml(run.status)}</td><td>${run.turn_count}</td><td>${run.tool_uses}</td><td>${escapeHtml(
|
|
445
|
+
return `<tr><td>${escapeHtml(run.timestamp)}</td><td><a href="#${phaseAnchor(run.phase)}">${escapeHtml(run.phase)}</a></td><td>${escapeHtml(run.status)}</td><td>${run.turn_count}</td><td>${run.tool_uses}</td><td>${escapeHtml(tokensTotal)}</td><td>${escapeHtml(formatMillis(run.duration_ms))}</td>${hasSessionLinks ? `<td>${sessionCell}</td>` : ""}</tr>`;
|
|
351
446
|
};
|
|
352
447
|
const overflowBlock = overflow.length === 0 ? "" : [`<details class="cc-timeline-older">`, `<summary>Older runs (${overflow.length})</summary>`, `<table class="cc-timeline-table">`, `<tbody>${overflow.map(rowOf).join("")}</tbody>`, `</table>`, `</details>`].join("\n");
|
|
353
|
-
return [`<section class="cc-card cc-timeline" aria-label="Activity timeline" data-section data-search-text="activity timeline sessions">`, `<div class="cc-section-head"><h2>Activity timeline</h2><span>newest first</span></div>`, `<table class="cc-timeline-table">`, `<thead><tr><th>When</th><th>Phase</th><th>Status</th><th
|
|
448
|
+
return [`<section class="cc-card cc-timeline" aria-label="Activity timeline" data-section data-search-text="activity timeline sessions">`, `<div class="cc-section-head"><h2>Activity timeline</h2><span>newest first</span></div>`, `<table class="cc-timeline-table">`, `<thead><tr><th>When</th><th>Phase</th><th>Status</th><th>Turns</th><th>Tools</th><th>Tokens</th><th>Duration</th>${hasSessionLinks ? "<th>Session</th>" : ""}</tr></thead>`, `<tbody>${visible.map(rowOf).join("")}</tbody>`, `</table>`, overflowBlock, `</section>`].join("\n");
|
|
354
449
|
}
|
|
355
450
|
function renderOpenQuestionsRollup(status) {
|
|
356
451
|
const buckets = [];
|
|
357
452
|
const seen = new Set();
|
|
453
|
+
const byKind = new Map();
|
|
358
454
|
let total = 0;
|
|
359
455
|
for (const [phaseId, phaseState] of Object.entries(status.phases)) {
|
|
360
456
|
const questions = [];
|
|
@@ -363,6 +459,8 @@ function renderOpenQuestionsRollup(status) {
|
|
|
363
459
|
if (seen.has(key))
|
|
364
460
|
continue;
|
|
365
461
|
seen.add(key);
|
|
462
|
+
const kind = String(q.kind ?? "unspecified");
|
|
463
|
+
byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
|
|
366
464
|
questions.push(renderOpenQuestion(q));
|
|
367
465
|
}
|
|
368
466
|
if (questions.length === 0)
|
|
@@ -372,7 +470,8 @@ function renderOpenQuestionsRollup(status) {
|
|
|
372
470
|
}
|
|
373
471
|
if (total === 0)
|
|
374
472
|
return "";
|
|
375
|
-
|
|
473
|
+
const kindSummary = [...byKind.entries()].sort((a, b) => b[1] - a[1]).map(([kind, count]) => `<span class="cc-kind-chip"><strong>${count}</strong>${escapeHtml(kind)}</span>`).join("");
|
|
474
|
+
return [`<section class="cc-card cc-rollup" aria-label="Open questions roll-up" data-section data-search-text="open questions">`, `<div class="cc-section-head"><h2>Open questions</h2><span>${total} unique</span></div>`, `<div class="cc-kind-summary">${kindSummary}</div>`, buckets.join("\n"), `</section>`].join("\n");
|
|
376
475
|
}
|
|
377
476
|
function renderCloseoutsList(inputs) {
|
|
378
477
|
const closeouts = inputs.closeouts;
|
|
@@ -478,9 +577,32 @@ function phaseRenderState(status, phaseId) {
|
|
|
478
577
|
return "current";
|
|
479
578
|
return "pending";
|
|
480
579
|
}
|
|
580
|
+
function displayCurrentPhase(status) {
|
|
581
|
+
return status.current_phase === "complete" ? "Pipeline complete" : status.current_phase || "—";
|
|
582
|
+
}
|
|
481
583
|
function completedPhaseCount(status) {
|
|
482
584
|
return Object.values(status.phases).filter((p) => p.status === "complete").length;
|
|
483
585
|
}
|
|
586
|
+
function countOpenQuestions(status) {
|
|
587
|
+
return Object.values(status.phases).reduce((sum, p) => sum + (p.open_questions?.length ?? 0), 0);
|
|
588
|
+
}
|
|
589
|
+
function countCarryForward(status) {
|
|
590
|
+
return Object.values(status.phases).reduce((sum, p) => sum + (p.carry_forward?.length ?? 0), 0);
|
|
591
|
+
}
|
|
592
|
+
function usageHasTokenAccounting(usage) {
|
|
593
|
+
return usage.runs.some((run) => (run.tokens?.input ?? 0) > 0 || (run.tokens?.output ?? 0) > 0 || (run.tokens?.cache_write ?? 0) > 0);
|
|
594
|
+
}
|
|
595
|
+
function formatTokensForDashboard(usage) {
|
|
596
|
+
const totals = computeTotals(usage);
|
|
597
|
+
if (usage.runs.length > 0 && !usageHasTokenAccounting(usage))
|
|
598
|
+
return "unavailable";
|
|
599
|
+
return formatTokenCount(totals.tokens.input + totals.tokens.output);
|
|
600
|
+
}
|
|
601
|
+
function formatRunTokens(run) {
|
|
602
|
+
const total = (run.tokens?.input ?? 0) + (run.tokens?.output ?? 0);
|
|
603
|
+
const hasAccounting = total > 0 || (run.tokens?.cache_write ?? 0) > 0;
|
|
604
|
+
return hasAccounting ? formatTokenCount(total) : "unavailable";
|
|
605
|
+
}
|
|
484
606
|
function lastRunPerPhase(runs) {
|
|
485
607
|
const out = new Map();
|
|
486
608
|
for (const r of runs) {
|
|
@@ -586,6 +708,24 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
|
|
|
586
708
|
.cc-meta-details { margin-top: var(--s-3); } .cc-header-meta, .cc-run-meta, .cc-usage-totals { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-1) var(--s-3); margin: var(--s-2) 0; } dt { color: var(--fg-dim); } dd { margin: 0; }
|
|
587
709
|
.cc-card, .cc-warning { padding: var(--s-3); margin-bottom: var(--s-4); }
|
|
588
710
|
.cc-warning { border-color: var(--status-running); background: color-mix(in srgb, var(--status-running) 9%, var(--bg-card)); }
|
|
711
|
+
.cc-health { border-width: 1px; position: relative; overflow: hidden; }
|
|
712
|
+
.cc-health::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 4px; background: var(--accent-2); }
|
|
713
|
+
.cc-health-bad::before { background: var(--status-error); } .cc-health-warn::before { background: var(--status-running); } .cc-health-ok::before { background: var(--status-complete); }
|
|
714
|
+
.cc-health-hero { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); margin-bottom: var(--s-3); }
|
|
715
|
+
.cc-health-hero h2 { font-size: clamp(24px, 3vw, 38px); text-transform: capitalize; margin: 2px 0; }
|
|
716
|
+
.cc-health-hero p { color: var(--fg-dim); margin: 0; }
|
|
717
|
+
.cc-health-ring { width: 112px; height: 112px; border-radius: 999px; display: grid; place-items: center; align-content: center; background: radial-gradient(circle at center, var(--bg-card) 58%, transparent 59%), conic-gradient(var(--accent-3), var(--accent-2)); border: 1px solid var(--border); flex: 0 0 auto; }
|
|
718
|
+
.cc-health-ring strong { font-size: 24px; line-height: 1; } .cc-health-ring span { color: var(--fg-dim); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
|
|
719
|
+
.cc-health-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--s-2); margin-bottom: var(--s-3); }
|
|
720
|
+
.cc-health-metric { padding: var(--s-2); border: 1px solid var(--border); border-radius: 14px; background: var(--bg); min-width: 0; }
|
|
721
|
+
.cc-health-metric span { display: block; color: var(--fg-dim); font-size: 12px; } .cc-health-metric strong { display: block; margin-top: 2px; font-size: 18px; overflow-wrap: anywhere; }
|
|
722
|
+
.cc-tone-ok strong { color: var(--status-complete); } .cc-tone-warn strong { color: var(--status-running); } .cc-tone-bad strong { color: var(--status-error); }
|
|
723
|
+
.cc-health-issues { display: grid; gap: var(--s-2); }
|
|
724
|
+
.cc-health-issue { padding: var(--s-2); border: 1px solid var(--border); border-radius: 14px; background: var(--bg); }
|
|
725
|
+
.cc-health-issue div { display: flex; flex-wrap: wrap; gap: var(--s-1); align-items: center; margin-bottom: 2px; }
|
|
726
|
+
.cc-health-issue strong { display: block; font-size: 15px; } .cc-health-issue p { color: var(--fg-dim); margin: 2px 0 0; }
|
|
727
|
+
.cc-issue-blocker { border-color: color-mix(in srgb, var(--status-error) 55%, var(--border)); background: color-mix(in srgb, var(--status-error) 8%, var(--bg-card)); }
|
|
728
|
+
.cc-health-ok { color: var(--status-complete); margin: 0; }
|
|
589
729
|
.cc-section-head { display: flex; justify-content: space-between; gap: var(--s-2); align-items: baseline; padding-bottom: var(--s-2); margin-bottom: var(--s-2); border-bottom: 1px solid var(--border); }
|
|
590
730
|
.cc-section-head span, .cc-muted, .cc-empty { color: var(--fg-dim); }
|
|
591
731
|
.cc-narration pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: inherit; }
|
|
@@ -616,6 +756,15 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
|
|
|
616
756
|
.cc-usage-table th, .cc-usage-table td, .cc-timeline-table th, .cc-timeline-table td { padding: var(--s-1) var(--s-2); text-align: left; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
|
|
617
757
|
.cc-usage-table th, .cc-timeline-table th { color: var(--fg-dim); font-weight: 700; text-transform: uppercase; font-size: 11px; letter-spacing: .04em; }
|
|
618
758
|
.cc-usage-missing td:last-child { color: var(--status-running); }
|
|
759
|
+
.cc-usage-insights { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2); margin: var(--s-3) 0; }
|
|
760
|
+
.cc-insight { padding: var(--s-2); border: 1px solid var(--border); border-radius: 12px; background: var(--bg); display: grid; gap: 2px; }
|
|
761
|
+
.cc-insight span { color: var(--fg-dim); font-size: 12px; } .cc-insight a { font-weight: 800; } .cc-insight strong { font-size: 15px; }
|
|
762
|
+
.cc-bar-cell { min-width: 120px; display: grid; grid-template-columns: minmax(42px, 1fr) auto; align-items: center; gap: var(--s-2); }
|
|
763
|
+
.cc-bar-cell > span:last-child { min-width: 48px; text-align: right; }
|
|
764
|
+
.cc-bar { height: 8px; border-radius: 999px; background: linear-gradient(90deg, var(--accent-2) var(--cc-bar), var(--bg-soft) var(--cc-bar)); border: 1px solid var(--border); }
|
|
765
|
+
.cc-kind-summary { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 var(--s-3); }
|
|
766
|
+
.cc-kind-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px; border: 1px solid var(--border); border-radius: 999px; background: var(--bg); color: var(--fg-dim); font-size: 12px; }
|
|
767
|
+
.cc-kind-chip strong { color: var(--fg); }
|
|
619
768
|
.cc-timeline-older { margin-top: var(--s-2); }
|
|
620
769
|
.cc-footer { margin-top: var(--s-5); padding-top: var(--s-3); border-top: 1px solid var(--border); color: var(--fg-dim); font-size: 13px; }
|
|
621
770
|
@media (max-width: 900px) {
|
|
@@ -624,7 +773,8 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
|
|
|
624
773
|
body.cc-sidebar-open .cc-sidebar { transform: translateX(0); }
|
|
625
774
|
.cc-menu-button { display: block; }
|
|
626
775
|
.cc-main { padding: var(--s-3); padding-top: var(--s-5); }
|
|
627
|
-
.cc-stat-grid, .cc-artifact-row, .cc-closeout-row { grid-template-columns: 1fr; }
|
|
776
|
+
.cc-stat-grid, .cc-artifact-row, .cc-closeout-row, .cc-health-grid, .cc-usage-insights { grid-template-columns: 1fr; }
|
|
777
|
+
.cc-health-hero { align-items: flex-start; } .cc-health-ring { width: 88px; height: 88px; }
|
|
628
778
|
.cc-header-meta, .cc-run-meta, .cc-usage-totals { grid-template-columns: 1fr; }
|
|
629
779
|
table { display: block; overflow-x: auto; white-space: nowrap; }
|
|
630
780
|
}
|