great-cto 3.52.1 → 3.54.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.54.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -13,6 +13,7 @@ import { sseClients, notifHistory } from './state.mjs';
|
|
|
13
13
|
import { autoRegisterProject, listProjects, resolveProjectCwd, resolveProjectInfo, getChangeTier, readProjectsRegistry, getRegistryDegradation } from './projects.mjs';
|
|
14
14
|
import { readVerdictsWithHealth } from './verdicts.mjs';
|
|
15
15
|
import { agentUsage, usageSnapshot } from '../../../scripts/lib/agent-usage.mjs';
|
|
16
|
+
import { usageIndexSnapshot, summarizeUsage, readCodexTitles } from '../../../scripts/lib/session-usage.mjs';
|
|
16
17
|
import { reviewerStatus } from '../../../scripts/lib/required-reviewers.mjs';
|
|
17
18
|
import { readSessionStatus } from '../../../scripts/lib/session-status.mjs';
|
|
18
19
|
import { readScores, summarizeScores } from '../../../scripts/lib/scores.mjs';
|
|
@@ -1492,6 +1493,31 @@ async function dispatch(req, res, url, cwd) {
|
|
|
1492
1493
|
return true;
|
|
1493
1494
|
}
|
|
1494
1495
|
|
|
1496
|
+
// What Claude Code and Codex consumed on this machine — tokens, models, the
|
|
1497
|
+
// heaviest conversations, tools, skills, agents, cache, Codex's plan window —
|
|
1498
|
+
// read from the hosts' own session logs (scripts/lib/session-usage.mjs).
|
|
1499
|
+
// Machine-wide like /api/agent-usage: the logs are one history. Served on the
|
|
1500
|
+
// board's own host only; nothing here is sent anywhere.
|
|
1501
|
+
if (pathname === '/api/usage') {
|
|
1502
|
+
const rawDays = parseInt(url.searchParams.get('days') || '30', 10);
|
|
1503
|
+
const days = Number.isFinite(rawDays) && rawDays > 0 ? Math.min(rawDays, 365) : 30;
|
|
1504
|
+
const snap = boardSessionUsage().get();
|
|
1505
|
+
let body;
|
|
1506
|
+
if (snap.state !== 'ready') {
|
|
1507
|
+
body = { state: snap.state, why: snap.why };
|
|
1508
|
+
} else {
|
|
1509
|
+
const sum = summarizeUsage(snap.index, { days, codexTitles: readCodexTitles() });
|
|
1510
|
+
const ours = new Set(boardAgentNames());
|
|
1511
|
+
for (const host of Object.keys(sum.lists)) {
|
|
1512
|
+
sum.lists[host].agents = sum.lists[host].agents.map((a) => ({ ...a, ours: ours.has(a.name) }));
|
|
1513
|
+
}
|
|
1514
|
+
body = { ...sum, seen: snap.seen, files: snap.files };
|
|
1515
|
+
}
|
|
1516
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
1517
|
+
res.end(JSON.stringify(body));
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1495
1521
|
// Domain reviewers this project's PROJECT.md requires (archetype, packs,
|
|
1496
1522
|
// compliance), each with whether a verdict exists — the list gate:ship refuses on.
|
|
1497
1523
|
// Sessions in this project that wait for a person — written by the
|
|
@@ -2141,14 +2167,24 @@ async function dispatch(req, res, url, cwd) {
|
|
|
2141
2167
|
|
|
2142
2168
|
|
|
2143
2169
|
let _usageSnap = null;
|
|
2170
|
+
function boardAgentNames() {
|
|
2171
|
+
const agentsDir = path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'agents');
|
|
2172
|
+
try { return fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3)); } catch { return []; }
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2144
2175
|
function boardUsage() {
|
|
2145
2176
|
if (!_usageSnap) {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
try { agents = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3)); } catch { /* none: every count is unavailable-by-omission */ }
|
|
2177
|
+
// No agents dir: every count is unavailable-by-omission.
|
|
2178
|
+
const agents = boardAgentNames();
|
|
2149
2179
|
_usageSnap = usageSnapshot({ compute: () => agentUsage({ agents }) });
|
|
2150
2180
|
}
|
|
2151
2181
|
return _usageSnap;
|
|
2152
2182
|
}
|
|
2153
2183
|
|
|
2184
|
+
let _sessionUsageSnap = null;
|
|
2185
|
+
function boardSessionUsage() {
|
|
2186
|
+
if (!_sessionUsageSnap) _sessionUsageSnap = usageIndexSnapshot();
|
|
2187
|
+
return _sessionUsageSnap;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2154
2190
|
export { dispatch, secondOpinionForTree };
|
|
@@ -21,12 +21,14 @@
|
|
|
21
21
|
import fs from 'fs';
|
|
22
22
|
import path from 'path';
|
|
23
23
|
|
|
24
|
-
// The
|
|
25
|
-
// Fleet, Harness, Settings, plus the demoted-to-deep-link
|
|
26
|
-
// else is rejected outright, before any filesystem write — an
|
|
27
|
-
// name written to this file would silently corrupt the K2/K3 counts
|
|
28
|
-
// exists to protect.
|
|
29
|
-
|
|
24
|
+
// The view names the board routes to (index.html `ROUTES`): Work, History,
|
|
25
|
+
// Decisions, Ledger, Fleet, Harness, Usage, Settings, plus the demoted-to-deep-link
|
|
26
|
+
// `kanban`. Anything else is rejected outright, before any filesystem write — an
|
|
27
|
+
// unknown view name written to this file would silently corrupt the K2/K3 counts
|
|
28
|
+
// it exists to protect. Work and History were routed and missing from this list,
|
|
29
|
+
// so opening either answered 400 into the console; the test now pins the two
|
|
30
|
+
// lists to each other.
|
|
31
|
+
const VALID_VIEWS = ['work', 'history', 'decisions', 'ledger', 'fleet', 'harness', 'usage', 'settings', 'kanban'];
|
|
30
32
|
|
|
31
33
|
function logFilePath(root) {
|
|
32
34
|
return path.join(root, '.great_cto', 'view-counter.log');
|
|
@@ -2244,6 +2244,11 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
2244
2244
|
font-size: var(--fs-body);
|
|
2245
2245
|
}
|
|
2246
2246
|
.judge-status[hidden] { display: none; }
|
|
2247
|
+
/* The "retry" link appears only when the live key check got no answer — a
|
|
2248
|
+
degraded state, so no screenshot ever carried it, and it rendered in the
|
|
2249
|
+
browser's default blue until the on-system check met it on 2026-10-06. */
|
|
2250
|
+
.judge-status a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; }
|
|
2251
|
+
.judge-status a:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
|
|
2247
2252
|
|
|
2248
2253
|
/* What the judge CONCLUDED, under whether it is connected.
|
|
2249
2254
|
The scores existed for weeks and no page read them: /api/scores had zero
|
|
@@ -2369,6 +2374,74 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
2369
2374
|
|
|
2370
2375
|
/* ── Metrics page ─────────────────────────────────────────────────────────── */
|
|
2371
2376
|
.metrics-page { padding: 24px 32px; }
|
|
2377
|
+
|
|
2378
|
+
/* Usage (great_cto-egkw) — two hosts side by side, every block in two columns
|
|
2379
|
+
that fold into one at phone width. */
|
|
2380
|
+
.usage-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
|
2381
|
+
.usage-chips {
|
|
2382
|
+
display: inline-flex; gap: 4px; padding: 3px;
|
|
2383
|
+
background: var(--bg-muted); border: 1px solid var(--border);
|
|
2384
|
+
}
|
|
2385
|
+
.usage-chip {
|
|
2386
|
+
background: transparent; border: none; color: var(--text2);
|
|
2387
|
+
padding: 5px 12px; min-height: 32px; cursor: pointer;
|
|
2388
|
+
font-family: var(--mono); font-size: var(--fs-caption); font-weight: 500; letter-spacing: 0.05em;
|
|
2389
|
+
}
|
|
2390
|
+
.usage-chip:hover { color: var(--text); }
|
|
2391
|
+
.usage-chip.active { background: var(--accent); color: var(--on-accent); }
|
|
2392
|
+
.usage-chip:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
|
|
2393
|
+
.usage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
2394
|
+
.usage-card { background: var(--bg-card); border: 1px solid var(--border); padding: 16px 18px; min-width: 0; }
|
|
2395
|
+
.usage-card h3 { font-size: var(--fs-title-s); font-weight: 600; margin: 0 0 10px; color: var(--text); display: flex; justify-content: space-between; gap: 8px; }
|
|
2396
|
+
.usage-card h3 .muted { font-weight: 400; font-size: var(--fs-caption); }
|
|
2397
|
+
.usage-big { font-family: var(--display); font-size: var(--fs-num-m); font-variant-numeric: tabular-nums; color: var(--text); line-height: 1.1; }
|
|
2398
|
+
.usage-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; margin-top: 12px; }
|
|
2399
|
+
.usage-stat { min-width: 0; }
|
|
2400
|
+
.usage-stat b { display: block; font-family: var(--mono); font-size: var(--fs-title-s); font-weight: 500; font-variant-numeric: tabular-nums; color: var(--text); }
|
|
2401
|
+
.usage-stat span { font-size: var(--fs-caption); color: var(--text3); }
|
|
2402
|
+
.usage-limit { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
|
|
2403
|
+
.usage-limit-head { display: flex; justify-content: space-between; gap: 8px; font-size: var(--fs-small); color: var(--text2); }
|
|
2404
|
+
.usage-limit-head b { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--text); font-weight: 500; }
|
|
2405
|
+
.usage-meter { height: 8px; background: var(--bg-muted); border: 1px solid var(--border); margin: 6px 0; }
|
|
2406
|
+
.usage-meter > span { display: block; height: 100%; background: var(--accent); }
|
|
2407
|
+
.usage-meter.warn > span { background: var(--status-progress); }
|
|
2408
|
+
.usage-meter.full > span { background: var(--status-blocked); }
|
|
2409
|
+
.usage-sub { font-size: var(--fs-caption); color: var(--text3); line-height: 1.5; }
|
|
2410
|
+
.usage-note { font-size: var(--fs-small); color: var(--text2); line-height: 1.55; }
|
|
2411
|
+
.usage-chart svg { display: block; width: 100%; height: 120px; }
|
|
2412
|
+
.usage-axis { display: flex; justify-content: space-between; gap: 8px; font-family: var(--mono); font-size: var(--fs-caption); color: var(--text3); margin-top: 4px; }
|
|
2413
|
+
.usage-axis span { white-space: nowrap; }
|
|
2414
|
+
.usage-axis span:nth-child(2) { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
2415
|
+
.usage-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: var(--fs-caption); color: var(--text2); margin-top: 8px; }
|
|
2416
|
+
.usage-legend i { display: inline-block; width: 10px; height: 10px; margin-right: 5px; vertical-align: -1px; }
|
|
2417
|
+
.usage-table-wrap { overflow-x: auto; }
|
|
2418
|
+
.usage-table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); }
|
|
2419
|
+
.usage-table th { text-align: left; font-weight: 500; color: var(--text3); font-size: var(--fs-caption); padding: 4px 6px; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
2420
|
+
.usage-table td { padding: 6px; border-bottom: 1px solid var(--border); color: var(--text); vertical-align: top; }
|
|
2421
|
+
.usage-table td.num, .usage-table th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
2422
|
+
.usage-title { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2423
|
+
.usage-tag { display: inline-block; font-family: var(--mono); font-size: var(--fs-eyebrow); letter-spacing: 0.04em; padding: 1px 5px; margin-left: 6px; border: 1px solid var(--border-strong); color: var(--text2); }
|
|
2424
|
+
.usage-share { display: inline-block; width: 48px; height: 6px; background: var(--bg-muted); margin-right: 6px; vertical-align: 1px; }
|
|
2425
|
+
.usage-share > span { display: block; height: 100%; background: var(--accent); }
|
|
2426
|
+
.usage-sec-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
2427
|
+
.usage-sec-head .mp-section-label { margin: 0; }
|
|
2428
|
+
.usage-lists { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
2429
|
+
.usage-list { list-style: none; margin: 0; padding: 0; }
|
|
2430
|
+
.usage-list li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; font-size: var(--fs-small); padding: 4px 0; position: relative; }
|
|
2431
|
+
.usage-list li .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
|
|
2432
|
+
.usage-list li .n { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--text2); }
|
|
2433
|
+
.usage-list li .bar { grid-column: 1 / -1; height: 3px; background: var(--bg-muted); }
|
|
2434
|
+
.usage-list li .bar > span { display: block; height: 100%; background: var(--accent); opacity: 0.7; }
|
|
2435
|
+
.usage-line svg { display: block; width: 100%; height: 110px; overflow: visible; }
|
|
2436
|
+
.usage-line .ln { fill: none; stroke: var(--accent); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
|
|
2437
|
+
.usage-line .ceil { stroke: var(--status-progress); stroke-width: 1; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }
|
|
2438
|
+
.usage-line .grid { stroke: var(--border-strong); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
|
2439
|
+
.usage-kinds { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: var(--fs-small); color: var(--text2); margin: 10px 0 4px; }
|
|
2440
|
+
.usage-kinds b { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--text); font-weight: 500; margin-right: 4px; }
|
|
2441
|
+
.usage-quote { font-size: var(--fs-small); color: var(--text2); border-left: 2px solid var(--border-strong); padding: 2px 0 2px 10px; margin: 10px 0 0; overflow-wrap: anywhere; }
|
|
2442
|
+
.usage-pace { font-size: var(--fs-small); color: var(--text); margin: 4px 0 8px; }
|
|
2443
|
+
.usage-pace.bad { color: var(--status-blocked-ink); }
|
|
2444
|
+
@media (max-width: 767px) { .usage-title { max-width: 160px; } .usage-chip { min-height: 44px; } }
|
|
2372
2445
|
.mp-section-label {
|
|
2373
2446
|
font-family: var(--mono); font-size: var(--fs-caption);
|
|
2374
2447
|
letter-spacing: 0.1em; text-transform: uppercase;
|
|
@@ -3355,6 +3428,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3355
3428
|
<span>Harness</span>
|
|
3356
3429
|
<span class="count" id="nav-harness-count"></span>
|
|
3357
3430
|
</div>
|
|
3431
|
+
<div class="nav-item" data-tab="usage" role="tab" tabindex="0" aria-label="Usage" title="Usage" onkeydown="navTabKeydown(event, this)" onclick="switchTab('usage', this)">
|
|
3432
|
+
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 20V10 M10 20V4 M16 20v-7 M22 20H2"/></svg>
|
|
3433
|
+
<span>Usage</span>
|
|
3434
|
+
</div>
|
|
3358
3435
|
</nav>
|
|
3359
3436
|
</details>
|
|
3360
3437
|
<div class="nav-divider"></div>
|
|
@@ -3603,6 +3680,23 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3603
3680
|
</div>
|
|
3604
3681
|
</div>
|
|
3605
3682
|
|
|
3683
|
+
<!-- Usage (great_cto-egkw). What Claude Code and Codex consumed on this
|
|
3684
|
+
machine, read from the hosts' own session logs by
|
|
3685
|
+
scripts/lib/session-usage.mjs. Machine-wide, not per project. -->
|
|
3686
|
+
<div class="panel" id="panel-usage" role="tabpanel">
|
|
3687
|
+
<div class="metrics-page">
|
|
3688
|
+
<div class="usage-head">
|
|
3689
|
+
<h1 class="page-title">Usage <span class="muted" style="font-weight:400;font-size:var(--fs-small)">what Claude Code and Codex consumed on this machine</span></h1>
|
|
3690
|
+
<div class="usage-chips" role="group" aria-label="Period">
|
|
3691
|
+
<button type="button" class="usage-chip" data-days="7" onclick="loadUsage(7)">7D</button>
|
|
3692
|
+
<button type="button" class="usage-chip active" data-days="30" onclick="loadUsage(30)">30D</button>
|
|
3693
|
+
<button type="button" class="usage-chip" data-days="90" onclick="loadUsage(90)">90D</button>
|
|
3694
|
+
</div>
|
|
3695
|
+
</div>
|
|
3696
|
+
<div id="usage-body" class="usage-note" role="status">Reading the session logs of Claude Code and Codex on this machine.</div>
|
|
3697
|
+
</div>
|
|
3698
|
+
</div>
|
|
3699
|
+
|
|
3606
3700
|
<div class="panel" id="panel-kanban">
|
|
3607
3701
|
<h1 class="page-title">Tasks</h1>
|
|
3608
3702
|
<div id="filter-bar" class="filter-bar"></div>
|
|
@@ -5877,6 +5971,303 @@ function renderAgentActivity(snap) {
|
|
|
5877
5971
|
// The Harnesses card. Four states for the second opinion, and the fourth is
|
|
5878
5972
|
// the reason the card exists: `unavailable` — declared codex, no codex here —
|
|
5879
5973
|
// must never look like `none` or like a switch someone turned off.
|
|
5974
|
+
// ── Usage (great_cto-egkw) ─────────────────────────────────────────────────
|
|
5975
|
+
// Both hosts, side by side, from /api/usage. Three honesty rules carried from
|
|
5976
|
+
// the reader: an unpriced model is n/a, never $0; Claude Code's plan use is not
|
|
5977
|
+
// written anywhere, so it is not drawn — only the refusals are; and a host with
|
|
5978
|
+
// no logs on this machine says so instead of showing zeros.
|
|
5979
|
+
let usageDays = 30;
|
|
5980
|
+
let usageHost = 'claude';
|
|
5981
|
+
let usageData = null;
|
|
5982
|
+
let _usageTimer = null;
|
|
5983
|
+
const USAGE_HOSTS = { claude: 'Claude Code', codex: 'Codex' };
|
|
5984
|
+
const USAGE_LOGS = { claude: '~/.claude/projects', codex: '~/.codex/sessions' };
|
|
5985
|
+
const USAGE_FN = { chat: 'Conversations', subagent: 'Subagents', automation: 'Automations', headless: 'Headless runs' };
|
|
5986
|
+
const USAGE_FN_COLOR = { chat: 'var(--accent)', subagent: 'var(--status-progress)', automation: 'var(--status-gate)', headless: 'var(--text3)' };
|
|
5987
|
+
|
|
5988
|
+
function fmtTok(n) {
|
|
5989
|
+
n = Number(n) || 0;
|
|
5990
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
5991
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
5992
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
5993
|
+
return String(Math.round(n));
|
|
5994
|
+
}
|
|
5995
|
+
function fmtUsdEq(n) {
|
|
5996
|
+
if (n == null) return null;
|
|
5997
|
+
return n >= 100 ? `$${Math.round(n).toLocaleString('en-US')}` : `$${n.toFixed(2)}`;
|
|
5998
|
+
}
|
|
5999
|
+
const fmtPct = (x) => (x == null ? null : `${Math.round(x * 100)}%`);
|
|
6000
|
+
|
|
6001
|
+
async function loadUsage(days) {
|
|
6002
|
+
if (days) usageDays = days;
|
|
6003
|
+
document.querySelectorAll('.usage-chip[data-days]').forEach((c) => c.classList.toggle('active', Number(c.dataset.days) === usageDays));
|
|
6004
|
+
clearTimeout(_usageTimer);
|
|
6005
|
+
const d = await api(`/api/usage?days=${usageDays}`);
|
|
6006
|
+
const body = document.getElementById('usage-body');
|
|
6007
|
+
if (!body) return;
|
|
6008
|
+
if (!d || d.error) {
|
|
6009
|
+
body.className = 'usage-note';
|
|
6010
|
+
body.textContent = `Usage could not be read: ${(d && d.error) || 'the board is not reachable — is the server still running?'}`;
|
|
6011
|
+
return;
|
|
6012
|
+
}
|
|
6013
|
+
if (d.state === 'computing') {
|
|
6014
|
+
body.className = 'usage-note';
|
|
6015
|
+
body.textContent = `Counting — ${d.why}`;
|
|
6016
|
+
_usageTimer = setTimeout(() => { if (currentTab === 'usage') loadUsage(); }, 2000);
|
|
6017
|
+
return;
|
|
6018
|
+
}
|
|
6019
|
+
if (d.state !== 'counted') {
|
|
6020
|
+
body.className = 'usage-note';
|
|
6021
|
+
body.textContent = `Usage is not available: ${d.why || d.state}`;
|
|
6022
|
+
return;
|
|
6023
|
+
}
|
|
6024
|
+
usageData = d;
|
|
6025
|
+
renderUsage(d);
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
function setUsageHost(host) {
|
|
6029
|
+
usageHost = host;
|
|
6030
|
+
if (usageData) renderUsage(usageData);
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
function usageLimitWindow(minutes) {
|
|
6034
|
+
if (minutes === 10080) return 'Weekly limit';
|
|
6035
|
+
if (minutes === 300) return '5-hour limit';
|
|
6036
|
+
if (minutes === 43200) return 'Monthly limit';
|
|
6037
|
+
return minutes ? `${minutes}-minute limit` : 'Limit';
|
|
6038
|
+
}
|
|
6039
|
+
|
|
6040
|
+
function usageHostCard(d, host) {
|
|
6041
|
+
const name = USAGE_HOSTS[host];
|
|
6042
|
+
if (!d.seen || !d.seen[host]) {
|
|
6043
|
+
return `<section class="usage-card" data-host="${host}"><h3>${name}</h3><p class="usage-note">No ${name} session logs on this machine (${USAGE_LOGS[host]}) — nothing to count, which is not the same as nothing used.</p></section>`;
|
|
6044
|
+
}
|
|
6045
|
+
const h = d.hosts[host];
|
|
6046
|
+
const unpricedHere = (d.models || []).filter((m) => m.host === host && m.usd == null).map((m) => m.model);
|
|
6047
|
+
const usd = h.usd == null
|
|
6048
|
+
? absent('uncomputable', `no price is set for ${unpricedHere.join(', ') || 'these models'} — add one to ~/.great_cto/model-prices.json`)
|
|
6049
|
+
: esc(fmtUsdEq(h.usd)) + (h.unpricedResponses ? ` <span class="usage-tag" title="${esc(`${h.unpricedResponses} responses from unpriced models are not in this figure`)}">partial</span>` : '');
|
|
6050
|
+
const stat = (v, label) => `<div class="usage-stat"><b>${v}</b><span>${label}</span></div>`;
|
|
6051
|
+
let limit;
|
|
6052
|
+
if (host === 'codex') {
|
|
6053
|
+
const L = d.limits && d.limits.codex;
|
|
6054
|
+
const win = (w) => {
|
|
6055
|
+
if (!w) return '';
|
|
6056
|
+
const used = Math.max(0, Math.min(100, Number(w.used) || 0));
|
|
6057
|
+
const cls = used >= 90 ? 'full' : used >= 70 ? 'warn' : '';
|
|
6058
|
+
const resets = w.resets ? fmtDT(new Date(w.resets * 1000).toISOString()) : '—';
|
|
6059
|
+
return `<div class="usage-limit-head"><span>${usageLimitWindow(w.minutes)}</span><b>${used}% used</b></div>
|
|
6060
|
+
<div class="usage-meter ${cls}" role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${used}" aria-label="${esc(usageLimitWindow(w.minutes))}"><span style="width:${used}%"></span></div>
|
|
6061
|
+
<div class="usage-sub">resets ${esc(resets)}</div>`;
|
|
6062
|
+
};
|
|
6063
|
+
limit = L && (L.primary || L.secondary)
|
|
6064
|
+
? `<div class="usage-limit">${win(L.primary)}${win(L.secondary)}<div class="usage-sub">plan ${esc(L.plan || '—')} · as Codex last reported it, ${esc(relTime(L.at))} ago</div></div>`
|
|
6065
|
+
: `<div class="usage-limit"><div class="usage-limit-head"><span>Plan limit</span>${absent('unloaded', 'no Codex session has reported its rate-limit window yet')}</div></div>`;
|
|
6066
|
+
} else {
|
|
6067
|
+
const hits = h.limitHits || { plan: 0, server: 0 };
|
|
6068
|
+
limit = `<div class="usage-limit"><div class="usage-limit-head"><span>Plan limit</span>${absent('uncomputable', 'Claude Code does not write how much of the plan is used — only when a request is refused')}</div>
|
|
6069
|
+
<div class="usage-sub">${hits.plan} refused at the plan limit · ${hits.server} server-side throttles (not your limit) in ${d.window.days} days</div></div>`;
|
|
6070
|
+
}
|
|
6071
|
+
return `<section class="usage-card" data-host="${host}">
|
|
6072
|
+
<h3><span>${name}</span><span class="muted">${esc(d.window.from)} → ${esc(d.window.to)}</span></h3>
|
|
6073
|
+
<div class="usage-big" title="${esc(`${(h.tokens || 0).toLocaleString('en-US')} tokens, cache reads included`)}">${fmtTok(h.tokens)} <span class="muted" style="font-size:var(--fs-small)">tokens</span></div>
|
|
6074
|
+
<div class="usage-stats">
|
|
6075
|
+
${stat(usd, 'at API list price')}
|
|
6076
|
+
${stat(esc((h.sessions || 0).toLocaleString('en-US')), 'conversations')}
|
|
6077
|
+
${stat(esc((h.responses || 0).toLocaleString('en-US')), 'model responses')}
|
|
6078
|
+
${stat(h.hitRate == null ? absent('none', 'no input in this window') : esc(fmtPct(h.hitRate)), 'input from cache')}
|
|
6079
|
+
${stat(esc(fmtTok(h.output)), 'output tokens')}
|
|
6080
|
+
</div>
|
|
6081
|
+
${limit}
|
|
6082
|
+
</section>`;
|
|
6083
|
+
}
|
|
6084
|
+
|
|
6085
|
+
function usageDayChart(d, host) {
|
|
6086
|
+
const name = USAGE_HOSTS[host];
|
|
6087
|
+
const head = `<h3><span>${name} by day</span><span class="muted">tokens</span></h3>`;
|
|
6088
|
+
if (!d.seen || !d.seen[host]) return `<section class="usage-card usage-chart">${head}<p class="usage-note">No logs to draw.</p></section>`;
|
|
6089
|
+
const days = d.daily || [];
|
|
6090
|
+
const max = Math.max(0, ...days.map((x) => x[host].tokens));
|
|
6091
|
+
if (!max) return `<section class="usage-card usage-chart">${head}<p class="usage-note">Nothing in this window.</p></section>`;
|
|
6092
|
+
const fns = Object.keys(USAGE_FN).filter((f) => days.some((x) => x[host].byFn[f]));
|
|
6093
|
+
const W = 10;
|
|
6094
|
+
let bars = '';
|
|
6095
|
+
days.forEach((x, i) => {
|
|
6096
|
+
let y = 100;
|
|
6097
|
+
const parts = fns.map((f) => `${USAGE_FN[f]} ${fmtTok(x[host].byFn[f] || 0)}`).join(' · ');
|
|
6098
|
+
const tip = `${x.date}: ${fmtTok(x[host].tokens)} tokens${x[host].usd ? ` · ${fmtUsdEq(x[host].usd)}` : ''}${parts ? ` (${parts})` : ''}`;
|
|
6099
|
+
bars += `<g><title>${esc(tip)}</title>`;
|
|
6100
|
+
for (const f of fns) {
|
|
6101
|
+
const v = x[host].byFn[f] || 0;
|
|
6102
|
+
if (!v) continue;
|
|
6103
|
+
const hgt = (v / max) * 96;
|
|
6104
|
+
y -= hgt;
|
|
6105
|
+
bars += `<rect x="${i * W + 1}" y="${y.toFixed(2)}" width="${W - 2}" height="${hgt.toFixed(2)}" fill="${USAGE_FN_COLOR[f]}"></rect>`;
|
|
6106
|
+
}
|
|
6107
|
+
bars += `<rect x="${i * W}" y="0" width="${W}" height="100" fill="transparent"></rect></g>`;
|
|
6108
|
+
});
|
|
6109
|
+
const legend = fns.map((f) => `<span><i style="background:${USAGE_FN_COLOR[f]}"></i>${USAGE_FN[f]} ${fmtTok(d.hosts[host].byFn[f] || 0)}</span>`).join('');
|
|
6110
|
+
return `<section class="usage-card usage-chart">${head}
|
|
6111
|
+
<svg viewBox="0 0 ${days.length * W} 100" preserveAspectRatio="none" role="img" aria-label="${esc(`${name} tokens per day, peak ${fmtTok(max)}`)}">${bars}</svg>
|
|
6112
|
+
<div class="usage-axis"><span>${esc(days[0].date)}</span><span>peak ${fmtTok(max)}</span><span>${esc(days[days.length - 1].date)}</span></div>
|
|
6113
|
+
<div class="usage-legend">${legend}</div>
|
|
6114
|
+
</section>`;
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6117
|
+
function usageTopTable(d, host) {
|
|
6118
|
+
const name = USAGE_HOSTS[host];
|
|
6119
|
+
const rows = (d.top && d.top[host]) || [];
|
|
6120
|
+
const head = `<h3><span>${name}: heaviest conversations</span><span class="muted">share of ${name} tokens</span></h3>`;
|
|
6121
|
+
if (!rows.length) return `<section class="usage-card">${head}<p class="usage-note">${d.seen && d.seen[host] ? 'Nothing in this window.' : 'No logs on this machine.'}</p></section>`;
|
|
6122
|
+
const money = host === 'claude';
|
|
6123
|
+
const body = rows.map((r) => {
|
|
6124
|
+
const tag = r.fn && r.fn !== 'chat' ? `<span class="usage-tag">${esc(USAGE_FN[r.fn] || r.fn)}</span>` : '';
|
|
6125
|
+
const sub = r.subagentTokens ? `<div class="usage-sub">${fmtPct(r.subagentTokens / r.tokens)} in subagents</div>` : '';
|
|
6126
|
+
const share = Math.max(1, Math.round(r.share * 100));
|
|
6127
|
+
return `<tr>
|
|
6128
|
+
<td><div class="usage-title" title="${esc(r.title)}">${esc(r.title)}</div>${sub}</td>
|
|
6129
|
+
<td>${esc(r.project || '—')}${tag}</td>
|
|
6130
|
+
<td class="num"><span class="usage-share" aria-hidden="true"><span style="width:${share}%"></span></span>${fmtPct(r.share)}</td>
|
|
6131
|
+
<td class="num">${fmtTok(r.tokens)}</td>
|
|
6132
|
+
${money ? `<td class="num">${r.usd == null ? absent('uncomputable', 'a model in this conversation has no price') : esc(fmtUsdEq(r.usd))}</td>` : ''}
|
|
6133
|
+
</tr>`;
|
|
6134
|
+
}).join('');
|
|
6135
|
+
return `<section class="usage-card">${head}<div class="usage-table-wrap"><table class="usage-table">
|
|
6136
|
+
<thead><tr><th>Conversation</th><th>Project</th><th class="num">Share</th><th class="num">Tokens</th>${money ? '<th class="num">$ list</th>' : ''}</tr></thead>
|
|
6137
|
+
<tbody>${body}</tbody></table></div></section>`;
|
|
6138
|
+
}
|
|
6139
|
+
|
|
6140
|
+
function usageModels(d) {
|
|
6141
|
+
const rows = d.models || [];
|
|
6142
|
+
if (!rows.length) return '<p class="usage-note">No model responses in this window.</p>';
|
|
6143
|
+
const body = rows.map((m) => {
|
|
6144
|
+
const fast = m.model.endsWith('@fast');
|
|
6145
|
+
const label = fast ? `${m.model.slice(0, -5)} (fast)` : m.model;
|
|
6146
|
+
return `<tr><td>${esc(USAGE_HOSTS[m.host])}</td><td>${esc(label)}</td><td class="num">${m.responses.toLocaleString('en-US')}</td><td class="num">${fmtTok(m.tokens)}</td><td class="num">${fmtTok(m.output)}</td>
|
|
6147
|
+
<td class="num">${m.usd == null ? absent('uncomputable', `no price is set for ${m.model} — add one to ~/.great_cto/model-prices.json`) : esc(fmtUsdEq(m.usd))}</td></tr>`;
|
|
6148
|
+
}).join('');
|
|
6149
|
+
return `<section class="usage-card" style="margin-bottom:20px"><div class="usage-table-wrap"><table class="usage-table">
|
|
6150
|
+
<thead><tr><th>Host</th><th>Model</th><th class="num">Responses</th><th class="num">Tokens</th><th class="num">Output</th><th class="num">$ list</th></tr></thead>
|
|
6151
|
+
<tbody>${body}</tbody></table></div></section>`;
|
|
6152
|
+
}
|
|
6153
|
+
|
|
6154
|
+
function usageList(title, rows, empty) {
|
|
6155
|
+
if (!rows || !rows.length) return `<section class="usage-card"><h3><span>${title}</span></h3><p class="usage-note">${empty}</p></section>`;
|
|
6156
|
+
const max = Math.max(...rows.map((r) => r.n));
|
|
6157
|
+
const items = rows.map((r) => `<li><span class="nm" title="${esc(r.name)}">${esc(r.name)}${r.ours ? '<span class="usage-tag">great_cto</span>' : ''}</span><span class="n">${r.n.toLocaleString('en-US')}</span><span class="bar" aria-hidden="true"><span style="width:${Math.max(2, Math.round((r.n / max) * 100))}%"></span></span></li>`).join('');
|
|
6158
|
+
return `<section class="usage-card"><h3><span>${title}</span></h3><ul class="usage-list">${items}</ul></section>`;
|
|
6159
|
+
}
|
|
6160
|
+
|
|
6161
|
+
// ── Limits (great_cto-egkw stage 2) ────────────────────────────────────────
|
|
6162
|
+
const USAGE_LIMIT_KIND = { session: '5-hour session', weekly: 'weekly', spend: 'monthly spend', model: 'per-model', credits: 'usage credits', other: 'other' };
|
|
6163
|
+
const USAGE_LIMIT_COLOR = { session: 'var(--status-blocked)', weekly: 'var(--status-progress)', spend: 'var(--status-gate)', model: 'var(--text2)', credits: 'var(--text2)', other: 'var(--text3)' };
|
|
6164
|
+
const fmtWhen = (ms) => (ms ? fmtDT(new Date(ms).toISOString()) : '—');
|
|
6165
|
+
function fmtHours(ms) {
|
|
6166
|
+
const h = ms / 3600000;
|
|
6167
|
+
return h >= 48 ? `${Math.round(h / 24)} days` : `${Math.max(1, Math.round(h))} h`;
|
|
6168
|
+
}
|
|
6169
|
+
|
|
6170
|
+
/** A line over the period: points [ms, value]; marks [{t, color, title}] drawn as ticks. */
|
|
6171
|
+
function usageLineSvg({ from, to, points, max, marks = [], ceiling = null, label }) {
|
|
6172
|
+
const W = 1000; const H = 100;
|
|
6173
|
+
const x = (t) => (((t - from) / Math.max(1, to - from)) * W).toFixed(1);
|
|
6174
|
+
const y = (v) => (H - (Math.min(v, max) / max) * (H - 4)).toFixed(1);
|
|
6175
|
+
const path = points.map((p, i) => `${i ? 'L' : 'M'}${x(p[0])},${y(p[1])}`).join('');
|
|
6176
|
+
const ticks = marks.map((m) => `<g><title>${esc(m.title)}</title><line x1="${x(m.t)}" x2="${x(m.t)}" y1="0" y2="${H}" stroke="${m.color}" stroke-width="2" vector-effect="non-scaling-stroke" opacity="0.8"></line></g>`).join('');
|
|
6177
|
+
const ceil = ceiling != null ? `<line class="ceil" x1="0" x2="${W}" y1="${y(ceiling)}" y2="${y(ceiling)}"></line>` : '';
|
|
6178
|
+
return `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" role="img" aria-label="${esc(label)}">
|
|
6179
|
+
<line class="grid" x1="0" x2="${W}" y1="${H}" y2="${H}"></line>${ceil}${ticks}<path class="ln" d="${path}"></path></svg>`;
|
|
6180
|
+
}
|
|
6181
|
+
|
|
6182
|
+
function usageClaudeLimits(d) {
|
|
6183
|
+
const head = `<h3><span>Claude Code limits</span><span class="muted">from refusals — no plan percentage is recorded</span></h3>`;
|
|
6184
|
+
if (!d.seen || !d.seen.claude) return `<section class="usage-card" data-limits="claude">${head}<p class="usage-note">No Claude Code logs on this machine.</p></section>`;
|
|
6185
|
+
const L = d.limits && d.limits.claude;
|
|
6186
|
+
if (!L) return `<section class="usage-card" data-limits="claude">${head}<p class="usage-note">Not computed.</p></section>`;
|
|
6187
|
+
const hits = (d.hosts.claude && d.hosts.claude.limitHits) || {};
|
|
6188
|
+
const stat = (v, label) => `<div class="usage-stat"><b>${v}</b><span>${label}</span></div>`;
|
|
6189
|
+
const ceilLine = (c, span) => c
|
|
6190
|
+
? `Refused at about <b>${esc(fmtUsdEq(c.median))}</b> in ${span} <span class="muted">(median of ${c.n} refused hour${c.n === 1 ? '' : 's'}, ${esc(fmtUsdEq(c.min))}–${esc(fmtUsdEq(c.max))})</span>`
|
|
6191
|
+
: null;
|
|
6192
|
+
const ceilings = [ceilLine(L.ceilings.session, '5 hours'), ceilLine(L.ceilings.weekly, '7 days')].filter(Boolean);
|
|
6193
|
+
const kinds = Object.keys(USAGE_LIMIT_KIND).filter((k) => hits[k]).map((k) => `<span><b>${hits[k]}</b>${USAGE_LIMIT_KIND[k]}</span>`).join('');
|
|
6194
|
+
const s = L.series || { usd5h: [] };
|
|
6195
|
+
const to = s.from + (s.usd5h.length - 1) * s.stepMs;
|
|
6196
|
+
const pts = s.usd5h.map((v, i) => [s.from + i * s.stepMs, v]);
|
|
6197
|
+
const max = Math.max(1, ...s.usd5h, L.ceilings.session ? L.ceilings.session.max : 0);
|
|
6198
|
+
const marks = (L.hits || []).filter((h) => h.kind !== 'server').map((h) => ({ t: h.t, color: USAGE_LIMIT_COLOR[h.kind] || 'var(--text3)', title: `${fmtWhen(h.t)} — refused by the ${USAGE_LIMIT_KIND[h.kind] || h.kind} limit ×${h.n}` }));
|
|
6199
|
+
const chart = pts.length > 1 ? `<div class="usage-line">${usageLineSvg({ from: s.from, to, points: pts, max, marks, ceiling: L.ceilings.session ? L.ceilings.session.median : null, label: 'Claude Code spend in the trailing 5 hours, with refusals marked' })}</div>
|
|
6200
|
+
<div class="usage-axis"><span>${esc(d.window.from)}</span><span>$ in the trailing 5 hours · peak ${esc(fmtUsdEq(max))}</span><span>${esc(d.window.to)}</span></div>` : '';
|
|
6201
|
+
const last = L.lastHit ? `<p class="usage-quote" title="${esc(fmtDate(L.lastHit.at))}">${esc(L.lastHit.text)} <span class="muted">— ${esc(relTime(L.lastHit.at))} ago</span></p>` : '';
|
|
6202
|
+
return `<section class="usage-card" data-limits="claude">${head}
|
|
6203
|
+
<div class="usage-stats">${stat(esc(fmtUsdEq(L.fiveHour.usd)), 'last 5 hours, $ list')}${stat(esc(fmtUsdEq(L.sevenDay.usd)), 'last 7 days, $ list')}${stat(esc(String(hits.plan || 0)), `refusals in ${d.window.days} days`)}</div>
|
|
6204
|
+
${kinds ? `<div class="usage-kinds">${kinds}</div>` : `<p class="usage-note">No request was refused by a limit in this period.</p>`}
|
|
6205
|
+
${ceilings.length ? `<p class="usage-note">${ceilings.join('<br>')}. <span class="muted">An estimate from what had been spent when Claude Code refused — not a published limit.</span></p>` : ''}
|
|
6206
|
+
${chart}
|
|
6207
|
+
${hits.server ? `<p class="usage-sub">${hits.server} server-side throttles (not your limit) are not marked.</p>` : ''}
|
|
6208
|
+
${last}
|
|
6209
|
+
</section>`;
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
function usageCodexLane(name, lane, d) {
|
|
6213
|
+
const title = usageLimitWindow(lane.minutes);
|
|
6214
|
+
const P = lane.projection;
|
|
6215
|
+
const lastRead = lane.series.length ? lane.series[lane.series.length - 1][0] : null;
|
|
6216
|
+
let pace = `<p class="usage-pace muted">No open window${lastRead ? ` — the last reading of it is from ${esc(fmtWhen(lastRead))}; the plan may no longer have it` : ''}.</p>`;
|
|
6217
|
+
if (P && P.state === 'full') pace = `<p class="usage-pace bad">Full — resets ${esc(fmtWhen(P.resets))}.</p>`;
|
|
6218
|
+
else if (P && P.state === 'will-fill') pace = `<p class="usage-pace bad">${P.used}% used. At this pace it is full by ${esc(fmtWhen(P.fullBy))} — ${esc(fmtHours(P.resets - P.fullBy))} before the reset.</p>`;
|
|
6219
|
+
else if (P && P.state === 'fits') pace = `<p class="usage-pace">${P.used}% used. At this pace about ${Math.round(P.atReset)}% by the reset, ${esc(fmtWhen(P.resets))}.</p>`;
|
|
6220
|
+
const from = Date.parse(`${d.window.from}T00:00:00`);
|
|
6221
|
+
const to = Date.now();
|
|
6222
|
+
const pts = [];
|
|
6223
|
+
for (const [t, v] of lane.series) { if (pts.length) pts.push([t, pts[pts.length - 1][1]]); pts.push([t, v]); }
|
|
6224
|
+
const marks = lane.windows.filter((w) => w.fullAt && w.fullAt >= from).map((w) => ({ t: w.fullAt, color: 'var(--status-blocked)', title: `filled ${fmtWhen(w.fullAt)}, ${fmtHours(w.resets - w.fullAt)} before its reset` }));
|
|
6225
|
+
const chart = pts.length > 1 ? `<div class="usage-line">${usageLineSvg({ from, to, points: pts, max: 100, marks, label: `Codex ${title} percent used over the period` })}</div>
|
|
6226
|
+
<div class="usage-axis"><span>${esc(d.window.from)}</span><span>% used · red = filled</span><span>${esc(d.window.to)}</span></div>` : '';
|
|
6227
|
+
const touched = lane.windows.filter((w) => w.peak > 0);
|
|
6228
|
+
const early = lane.windows.filter((w) => w.fullAt).map((w) => w.resets - w.fullAt);
|
|
6229
|
+
const summary = touched.length
|
|
6230
|
+
? `${lane.filled} of ${touched.length} used windows filled${early.length ? `, on median ${esc(fmtHours(early.sort((a, b) => a - b)[Math.floor(early.length / 2)]))} before the reset` : ''}.`
|
|
6231
|
+
: 'No window was used in this period.';
|
|
6232
|
+
return `<div class="usage-limit" data-lane="${esc(name)}"><div class="usage-limit-head"><span>${esc(title)}</span></div>${pace}${chart}<p class="usage-sub">${summary}</p></div>`;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
function usageCodexLimits(d) {
|
|
6236
|
+
const head = `<h3><span>Codex limits</span><span class="muted">as Codex reported them</span></h3>`;
|
|
6237
|
+
if (!d.seen || !d.seen.codex) return `<section class="usage-card" data-limits="codex">${head}<p class="usage-note">No Codex logs on this machine.</p></section>`;
|
|
6238
|
+
const D = d.limits && d.limits.codex && d.limits.codex.detail;
|
|
6239
|
+
if (!D) return `<section class="usage-card" data-limits="codex">${head}<p class="usage-note">No Codex session reported its plan window in this period.</p></section>`;
|
|
6240
|
+
const order = ['fiveHour', 'weekly', ...Object.keys(D).filter((k) => k !== 'fiveHour' && k !== 'weekly')];
|
|
6241
|
+
const lanes = order.filter((k) => D[k]).map((k) => usageCodexLane(k, D[k], d)).join('');
|
|
6242
|
+
return `<section class="usage-card" data-limits="codex">${head}${lanes}<p class="usage-sub">A window Codex reset early shows as its own window.</p></section>`;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6245
|
+
function renderUsage(d) {
|
|
6246
|
+
const body = document.getElementById('usage-body');
|
|
6247
|
+
if (!body) return;
|
|
6248
|
+
body.className = '';
|
|
6249
|
+
const l = (d.lists && d.lists[usageHost]) || {};
|
|
6250
|
+
const hostBtn = (h) => `<button type="button" class="usage-chip${usageHost === h ? ' active' : ''}" aria-pressed="${usageHost === h}" onclick="setUsageHost('${h}')">${USAGE_HOSTS[h]}</button>`;
|
|
6251
|
+
body.innerHTML = `
|
|
6252
|
+
<div class="usage-grid">${usageHostCard(d, 'claude')}${usageHostCard(d, 'codex')}</div>
|
|
6253
|
+
<h2 class="mp-section-label">Limits</h2>
|
|
6254
|
+
<div class="usage-grid">${usageClaudeLimits(d)}${usageCodexLimits(d)}</div>
|
|
6255
|
+
<h2 class="mp-section-label">By day, by kind of work</h2>
|
|
6256
|
+
<div class="usage-grid">${usageDayChart(d, 'claude')}${usageDayChart(d, 'codex')}</div>
|
|
6257
|
+
<h2 class="mp-section-label">Heaviest conversations</h2>
|
|
6258
|
+
<div class="usage-grid">${usageTopTable(d, 'claude')}${usageTopTable(d, 'codex')}</div>
|
|
6259
|
+
<h2 class="mp-section-label">Models</h2>
|
|
6260
|
+
${usageModels(d)}
|
|
6261
|
+
<div class="usage-sec-head"><h2 class="mp-section-label">What was called</h2><div class="usage-chips" role="group" aria-label="Host">${hostBtn('claude')}${hostBtn('codex')}</div></div>
|
|
6262
|
+
<div class="usage-lists">
|
|
6263
|
+
${usageList('Tools', l.tools, 'No tool calls in this window.')}
|
|
6264
|
+
${usageList('Skills', l.skills, 'No skills were loaded in this window.')}
|
|
6265
|
+
${usageList('Agents', l.agents, 'No subagents were dispatched in this window.')}
|
|
6266
|
+
${usageList('MCP servers', l.mcp, 'No MCP tools were called in this window.')}
|
|
6267
|
+
</div>
|
|
6268
|
+
<p class="usage-note">Read on this machine from ${USAGE_LOGS.claude} and ${USAGE_LOGS.codex}; nothing is sent anywhere. Tokens include cache reads. <b>$ list</b> is the API list-price equivalent from great_cto's price table, not a subscription bill — Claude Code's own estimate can differ for some models. A model without a price shows n/a, never $0.</p>`;
|
|
6269
|
+
}
|
|
6270
|
+
|
|
5880
6271
|
async function renderHarnesses() {
|
|
5881
6272
|
const body = document.getElementById('harnesses-body');
|
|
5882
6273
|
const status = document.getElementById('harnesses-status');
|
|
@@ -8340,6 +8731,7 @@ function cmdkActions() {
|
|
|
8340
8731
|
{ group: 'Go', label: 'Ledger — what it cost, what is running', run: () => switchTab('ledger') },
|
|
8341
8732
|
{ group: 'Go', label: 'Fleet — the agents and what they may do', run: () => switchTab('fleet') },
|
|
8342
8733
|
{ group: 'Go', label: 'Harness — host and second opinion', run: () => switchTab('harness') },
|
|
8734
|
+
{ group: 'Go', label: 'Usage — what Claude Code and Codex consumed', run: () => switchTab('usage') },
|
|
8343
8735
|
{ group: 'Go', label: 'Settings', run: () => switchTab('settings') },
|
|
8344
8736
|
{ group: 'Go', label: 'Tasks board (#/kanban, deep link)', run: () => switchTab('kanban') },
|
|
8345
8737
|
{ group: 'Do', label: 'New issue', run: () => openNewIssueModal() },
|
|
@@ -8532,7 +8924,7 @@ try {
|
|
|
8532
8924
|
// panels, not keys.
|
|
8533
8925
|
const DEST = { decisions: 'inbox', ledger: 'budgets', fleet: 'agents', dashboard: 'budgets', share: 'settings', notifications: 'settings' };
|
|
8534
8926
|
const NAV = { inbox: 'decisions', budgets: 'ledger', agents: 'fleet', dashboard: 'ledger', share: 'settings', notifications: 'settings' };
|
|
8535
|
-
const ROUTES = { work: 1, history: 1, decisions: 1, ledger: 1, fleet: 1, harness: 1, settings: 1, kanban: 1 };
|
|
8927
|
+
const ROUTES = { work: 1, history: 1, decisions: 1, ledger: 1, fleet: 1, harness: 1, usage: 1, settings: 1, kanban: 1 };
|
|
8536
8928
|
function switchTab(id, el) {
|
|
8537
8929
|
const lid = DEST[id] || id; // the panel's own name
|
|
8538
8930
|
const navKey = NAV[id] || id; // the destination to highlight
|
|
@@ -8541,7 +8933,7 @@ function switchTab(id, el) {
|
|
|
8541
8933
|
currentTab = lid;
|
|
8542
8934
|
document.getElementById('nav-views-label').hidden = lid !== 'agents';
|
|
8543
8935
|
document.querySelector('.nav-views').hidden = lid !== 'agents';
|
|
8544
|
-
if (['agents', 'budgets', 'harness'].includes(lid)) document.getElementById('tools-nav').open = true;
|
|
8936
|
+
if (['agents', 'budgets', 'harness', 'usage'].includes(lid)) document.getElementById('tools-nav').open = true;
|
|
8545
8937
|
if (lid === 'work' || lid === 'history') window.GctoWork.refresh();
|
|
8546
8938
|
if (ROUTES[id]) {
|
|
8547
8939
|
if (location.hash !== `#/${id}`) history.replaceState(null, '', `#/${id}`);
|
|
@@ -8562,7 +8954,7 @@ function switchTab(id, el) {
|
|
|
8562
8954
|
// crumb — `labels` used to be referenced here but is only ever a function-local in
|
|
8563
8955
|
// this file, so this threw ReferenceError on every tab switch and skipped the per-tab
|
|
8564
8956
|
// loaders below (e.g. logs stuck on "Loading…"). Use a local label map.
|
|
8565
|
-
const TAB_LABELS = { work: 'Work', history: 'History', decisions: 'Decisions', ledger: 'Ledger', fleet: 'Fleet', harness: 'Harness', settings: 'Settings', budgets: 'Budgets', docs: 'Docs', dashboard: 'Board', inbox: 'Inbox', tasks: 'Tasks', agents: 'Agents', logs: 'Activity', notifications: 'Notifications', metrics: 'Metrics', share: 'Share' };
|
|
8957
|
+
const TAB_LABELS = { work: 'Work', history: 'History', decisions: 'Decisions', ledger: 'Ledger', fleet: 'Fleet', harness: 'Harness', usage: 'Usage', settings: 'Settings', budgets: 'Budgets', docs: 'Docs', dashboard: 'Board', inbox: 'Inbox', tasks: 'Tasks', agents: 'Agents', logs: 'Activity', notifications: 'Notifications', metrics: 'Metrics', share: 'Share' };
|
|
8566
8958
|
const crumb = document.getElementById('crumb-here'); if (crumb) crumb.textContent = TAB_LABELS[id] || TAB_LABELS[lid] || 'Board';
|
|
8567
8959
|
// Budgets render on Metrics, so the heartbeat that feeds them has to be
|
|
8568
8960
|
// fetched when Metrics opens — not only from the agents drill-in it used to
|
|
@@ -8595,6 +8987,7 @@ function switchTab(id, el) {
|
|
|
8595
8987
|
if (lid === 'notifications' || lid === 'settings') loadNotifications();
|
|
8596
8988
|
if (lid === 'settings') loadSettingsPage();
|
|
8597
8989
|
if (lid === 'harness') renderHarnesses();
|
|
8990
|
+
if (lid === 'usage') loadUsage();
|
|
8598
8991
|
}
|
|
8599
8992
|
|
|
8600
8993
|
// Sidebar tabs: Enter/Space activate, arrows move (WAI-ARIA tablist).
|