oc2token 0.1.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/LICENSE +21 -0
- package/README.md +399 -0
- package/dist/src/accounting/index.d.ts +1 -0
- package/dist/src/accounting/index.js +2 -0
- package/dist/src/accounting/index.js.map +1 -0
- package/dist/src/accounting/reducer.d.ts +21 -0
- package/dist/src/accounting/reducer.js +105 -0
- package/dist/src/accounting/reducer.js.map +1 -0
- package/dist/src/antigravity/discovery.d.ts +18 -0
- package/dist/src/antigravity/discovery.js +95 -0
- package/dist/src/antigravity/discovery.js.map +1 -0
- package/dist/src/antigravity/doctor.d.ts +6 -0
- package/dist/src/antigravity/doctor.js +78 -0
- package/dist/src/antigravity/doctor.js.map +1 -0
- package/dist/src/antigravity/index.d.ts +3 -0
- package/dist/src/antigravity/index.js +4 -0
- package/dist/src/antigravity/index.js.map +1 -0
- package/dist/src/antigravity/proto.d.ts +73 -0
- package/dist/src/antigravity/proto.js +259 -0
- package/dist/src/antigravity/proto.js.map +1 -0
- package/dist/src/antigravity/scanner.d.ts +8 -0
- package/dist/src/antigravity/scanner.js +552 -0
- package/dist/src/antigravity/scanner.js.map +1 -0
- package/dist/src/application.d.ts +73 -0
- package/dist/src/application.js +657 -0
- package/dist/src/application.js.map +1 -0
- package/dist/src/cache/filesystem.d.ts +8 -0
- package/dist/src/cache/filesystem.js +70 -0
- package/dist/src/cache/filesystem.js.map +1 -0
- package/dist/src/cache/index.d.ts +5 -0
- package/dist/src/cache/index.js +6 -0
- package/dist/src/cache/index.js.map +1 -0
- package/dist/src/cache/lock.d.ts +40 -0
- package/dist/src/cache/lock.js +227 -0
- package/dist/src/cache/lock.js.map +1 -0
- package/dist/src/cache/schema.d.ts +45 -0
- package/dist/src/cache/schema.js +308 -0
- package/dist/src/cache/schema.js.map +1 -0
- package/dist/src/cache/store.d.ts +46 -0
- package/dist/src/cache/store.js +396 -0
- package/dist/src/cache/store.js.map +1 -0
- package/dist/src/cache/types.d.ts +130 -0
- package/dist/src/cache/types.js +6 -0
- package/dist/src/cache/types.js.map +1 -0
- package/dist/src/cli.d.ts +8 -0
- package/dist/src/cli.js +874 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/codex/discovery.d.ts +6 -0
- package/dist/src/codex/discovery.js +88 -0
- package/dist/src/codex/discovery.js.map +1 -0
- package/dist/src/codex/doctor.d.ts +6 -0
- package/dist/src/codex/doctor.js +52 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/index.d.ts +3 -0
- package/dist/src/codex/index.js +3 -0
- package/dist/src/codex/index.js.map +1 -0
- package/dist/src/codex/scanner.d.ts +6 -0
- package/dist/src/codex/scanner.js +487 -0
- package/dist/src/codex/scanner.js.map +1 -0
- package/dist/src/codex/types.d.ts +31 -0
- package/dist/src/codex/types.js +5 -0
- package/dist/src/codex/types.js.map +1 -0
- package/dist/src/collector/collector.d.ts +15 -0
- package/dist/src/collector/collector.js +597 -0
- package/dist/src/collector/collector.js.map +1 -0
- package/dist/src/collector/index.d.ts +3 -0
- package/dist/src/collector/index.js +3 -0
- package/dist/src/collector/index.js.map +1 -0
- package/dist/src/collector/types.d.ts +55 -0
- package/dist/src/collector/types.js +18 -0
- package/dist/src/collector/types.js.map +1 -0
- package/dist/src/dashboard/render/ansi.d.ts +53 -0
- package/dist/src/dashboard/render/ansi.js +106 -0
- package/dist/src/dashboard/render/ansi.js.map +1 -0
- package/dist/src/dashboard/render/dashboard.d.ts +13 -0
- package/dist/src/dashboard/render/dashboard.js +750 -0
- package/dist/src/dashboard/render/dashboard.js.map +1 -0
- package/dist/src/dashboard/render/format.d.ts +23 -0
- package/dist/src/dashboard/render/format.js +169 -0
- package/dist/src/dashboard/render/format.js.map +1 -0
- package/dist/src/dashboard/render/redraw.d.ts +13 -0
- package/dist/src/dashboard/render/redraw.js +24 -0
- package/dist/src/dashboard/render/redraw.js.map +1 -0
- package/dist/src/dashboard/render/types.d.ts +83 -0
- package/dist/src/dashboard/render/types.js +528 -0
- package/dist/src/dashboard/render/types.js.map +1 -0
- package/dist/src/dashboard/scheduler/index.d.ts +1 -0
- package/dist/src/dashboard/scheduler/index.js +2 -0
- package/dist/src/dashboard/scheduler/index.js.map +1 -0
- package/dist/src/dashboard/scheduler/refresh-scheduler.d.ts +46 -0
- package/dist/src/dashboard/scheduler/refresh-scheduler.js +137 -0
- package/dist/src/dashboard/scheduler/refresh-scheduler.js.map +1 -0
- package/dist/src/dashboard/settings/index.d.ts +25 -0
- package/dist/src/dashboard/settings/index.js +162 -0
- package/dist/src/dashboard/settings/index.js.map +1 -0
- package/dist/src/dashboard/state/index.d.ts +2 -0
- package/dist/src/dashboard/state/index.js +3 -0
- package/dist/src/dashboard/state/index.js.map +1 -0
- package/dist/src/dashboard/state/refresh-coordinator.d.ts +72 -0
- package/dist/src/dashboard/state/refresh-coordinator.js +346 -0
- package/dist/src/dashboard/state/refresh-coordinator.js.map +1 -0
- package/dist/src/dashboard/state/types.d.ts +68 -0
- package/dist/src/dashboard/state/types.js +2 -0
- package/dist/src/dashboard/state/types.js.map +1 -0
- package/dist/src/domain/contracts.d.ts +219 -0
- package/dist/src/domain/contracts.js +2 -0
- package/dist/src/domain/contracts.js.map +1 -0
- package/dist/src/domain/errors.d.ts +25 -0
- package/dist/src/domain/errors.js +65 -0
- package/dist/src/domain/errors.js.map +1 -0
- package/dist/src/domain/index.d.ts +5 -0
- package/dist/src/domain/index.js +6 -0
- package/dist/src/domain/index.js.map +1 -0
- package/dist/src/domain/records.d.ts +59 -0
- package/dist/src/domain/records.js +141 -0
- package/dist/src/domain/records.js.map +1 -0
- package/dist/src/domain/tokens.d.ts +51 -0
- package/dist/src/domain/tokens.js +93 -0
- package/dist/src/domain/tokens.js.map +1 -0
- package/dist/src/domain/windows.d.ts +59 -0
- package/dist/src/domain/windows.js +272 -0
- package/dist/src/domain/windows.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/opencode/adapter.d.ts +47 -0
- package/dist/src/opencode/adapter.js +118 -0
- package/dist/src/opencode/adapter.js.map +1 -0
- package/dist/src/opencode/client.d.ts +64 -0
- package/dist/src/opencode/client.js +118 -0
- package/dist/src/opencode/client.js.map +1 -0
- package/dist/src/opencode/doctor.d.ts +28 -0
- package/dist/src/opencode/doctor.js +82 -0
- package/dist/src/opencode/doctor.js.map +1 -0
- package/dist/src/opencode/index.d.ts +5 -0
- package/dist/src/opencode/index.js +6 -0
- package/dist/src/opencode/index.js.map +1 -0
- package/dist/src/opencode/stats.d.ts +8 -0
- package/dist/src/opencode/stats.js +174 -0
- package/dist/src/opencode/stats.js.map +1 -0
- package/dist/src/opencode/transport.d.ts +54 -0
- package/dist/src/opencode/transport.js +621 -0
- package/dist/src/opencode/transport.js.map +1 -0
- package/dist/src/output/index.d.ts +20 -0
- package/dist/src/output/index.js +46 -0
- package/dist/src/output/index.js.map +1 -0
- package/dist/src/output/json.d.ts +72 -0
- package/dist/src/output/json.js +133 -0
- package/dist/src/output/json.js.map +1 -0
- package/dist/src/output/table.d.ts +4 -0
- package/dist/src/output/table.js +81 -0
- package/dist/src/output/table.js.map +1 -0
- package/dist/src/pricing/index.d.ts +1 -0
- package/dist/src/pricing/index.js +2 -0
- package/dist/src/pricing/index.js.map +1 -0
- package/dist/src/pricing/pricing.d.ts +19 -0
- package/dist/src/pricing/pricing.js +441 -0
- package/dist/src/pricing/pricing.js.map +1 -0
- package/package.json +27 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import { ANSI, colorEnabled, emphasis, paint, renderInPlace, statusColor, themeCache, themeCacheUnderline, themeCost, themeCyan, themeIn, themeInUnderline, themeOrange, themeOut, themeOutUnderline, themePurple, } from "./ansi.js";
|
|
2
|
+
import { coverageState, formatCoverage, formatCost, formatTokenBreakdown, formatTokenCount, safeIdentifier, safeLabel, stripAnsi, } from "./format.js";
|
|
3
|
+
import { allWindowKinds, normalizeDashboardSnapshot, } from "./types.js";
|
|
4
|
+
const CARD_MIN_WIDTH = 27;
|
|
5
|
+
const WIDE_LAYOUT_MIN_WIDTH = 90;
|
|
6
|
+
function lineWidth(value) {
|
|
7
|
+
return [...stripAnsi(value)].length;
|
|
8
|
+
}
|
|
9
|
+
function pad(value, width) {
|
|
10
|
+
const gap = Math.max(0, width - lineWidth(value));
|
|
11
|
+
return value + " ".repeat(gap);
|
|
12
|
+
}
|
|
13
|
+
function truncate(value, width) {
|
|
14
|
+
const safe = safeLabel(value, width);
|
|
15
|
+
return lineWidth(safe) <= width ? safe : [...safe].slice(0, Math.max(0, width - 1)).join("") + "…";
|
|
16
|
+
}
|
|
17
|
+
function truncateExact(value, width) {
|
|
18
|
+
// Like truncate but preserves multiple spaces — for trusted breakdown strings
|
|
19
|
+
// where " " spacing must not be collapsed to " ".
|
|
20
|
+
const len = [...value].length;
|
|
21
|
+
if (len <= width)
|
|
22
|
+
return value;
|
|
23
|
+
return [...value].slice(0, Math.max(0, width - 1)).join("") + "…";
|
|
24
|
+
}
|
|
25
|
+
function colorizeBreakdown(value, enabled, underline = false) {
|
|
26
|
+
if (!enabled || value.length === 0)
|
|
27
|
+
return value;
|
|
28
|
+
// Breakdown arrives as single-space separated tokens like "I 100 O 20 C 0"
|
|
29
|
+
// (legacy "In 100 Out 23 Cache 9" is also handled for back-compat)
|
|
30
|
+
const inFn = underline ? themeInUnderline : themeIn;
|
|
31
|
+
const outFn = underline ? themeOutUnderline : themeOut;
|
|
32
|
+
const cacheFn = underline ? themeCacheUnderline : themeCache;
|
|
33
|
+
let result = value;
|
|
34
|
+
if (/\bI\s+[^\s]+/.test(result)) {
|
|
35
|
+
result = result.replace(/\bI\s+[^\s]+/, (match) => inFn(match, enabled));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
result = result.replace(/\bIn\s+[^\s]+/, (match) => inFn(match, enabled));
|
|
39
|
+
}
|
|
40
|
+
if (/\bO\s+[^\s]+/.test(result)) {
|
|
41
|
+
result = result.replace(/\bO\s+[^\s]+/, (match) => outFn(match, enabled));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
result = result.replace(/\bOut\s+[^\s]+/, (match) => outFn(match, enabled));
|
|
45
|
+
}
|
|
46
|
+
const hasShortCache = /\bC\s+[^\s]+/.test(result);
|
|
47
|
+
const hasLongCache = /\bCache\s+[^\s]+/.test(result);
|
|
48
|
+
if (hasShortCache) {
|
|
49
|
+
result = result.replace(/\bC\s+[^\s]+/, (match) => cacheFn(match, enabled));
|
|
50
|
+
}
|
|
51
|
+
else if (hasLongCache) {
|
|
52
|
+
result = result.replace(/\bCache\s+[^\s]+/, (match) => cacheFn(match, enabled));
|
|
53
|
+
}
|
|
54
|
+
else if (result.includes("C")) {
|
|
55
|
+
// Truncated cache token (e.g. "I 100 O 20 C" or "Ca…")
|
|
56
|
+
const idxC = result.lastIndexOf(" C");
|
|
57
|
+
const idxCa = result.indexOf("Ca");
|
|
58
|
+
const index = idxC !== -1 ? idxC + 1 : idxCa;
|
|
59
|
+
if (index !== -1) {
|
|
60
|
+
const before = result.slice(0, index);
|
|
61
|
+
const tail = result.slice(index);
|
|
62
|
+
if (!tail.includes(ANSI.cyan) && !tail.includes(ANSI.orange) && !tail.includes(ANSI.green)) {
|
|
63
|
+
result = before + cacheFn(tail, enabled);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (result.trimEnd().endsWith("C")) {
|
|
67
|
+
// Single trailing C without value (heavily truncated)
|
|
68
|
+
result = result.replace(/C\s*$/, (m) => cacheFn(m, enabled));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function border(width, left, fill, right) {
|
|
74
|
+
return left + fill.repeat(Math.max(0, width - 2)) + right;
|
|
75
|
+
}
|
|
76
|
+
function cardTitle(kind) {
|
|
77
|
+
return kind === "hour" ? "LAST 60 MINUTES" : kind === "day" ? "TODAY" : "THIS WEEK";
|
|
78
|
+
}
|
|
79
|
+
function panelHeading(title, width, color, accent = "purple") {
|
|
80
|
+
const label = truncate(title, Math.max(4, width - 6));
|
|
81
|
+
const plainPrefix = `◆ ${label}`;
|
|
82
|
+
const prefix = accent === "orange"
|
|
83
|
+
? themeOrange(plainPrefix, color, true)
|
|
84
|
+
: accent === "cyan"
|
|
85
|
+
? themeCyan(plainPrefix, color, true)
|
|
86
|
+
: themePurple(plainPrefix, color, true);
|
|
87
|
+
const ruleLength = Math.max(2, width - lineWidth(plainPrefix) - 1);
|
|
88
|
+
return `${prefix} ${themePurple("─".repeat(ruleLength), color)}`;
|
|
89
|
+
}
|
|
90
|
+
function providerAccent(name, color, bright = false) {
|
|
91
|
+
const key = String(name).toLowerCase();
|
|
92
|
+
if (key === "codex")
|
|
93
|
+
return (value) => themeOrange(value, color, bright);
|
|
94
|
+
if (key === "antigravity")
|
|
95
|
+
return (value) => themeCyan(value, color, bright);
|
|
96
|
+
return (value) => themePurple(value, color, bright);
|
|
97
|
+
}
|
|
98
|
+
function renderProviderStack(providers, color, width) {
|
|
99
|
+
if (providers.length === 0)
|
|
100
|
+
return [];
|
|
101
|
+
const total = providers.reduce((sum, entry) => sum + entry.totals.recorded_total, 0);
|
|
102
|
+
const plainEntries = providers.slice(0, 3).map((entry) => {
|
|
103
|
+
const name = safeIdentifier(entry.name);
|
|
104
|
+
const count = formatTokenCount(entry.totals.recorded_total);
|
|
105
|
+
const pct = total > 0 ? Math.round((entry.totals.recorded_total / total) * 100) : 0;
|
|
106
|
+
return `${name} ${count} (${pct}%)`;
|
|
107
|
+
});
|
|
108
|
+
const coloredEntries = plainEntries.map((text, index) => {
|
|
109
|
+
const safe = truncate(text, Math.max(4, width - 4));
|
|
110
|
+
if (index === 0)
|
|
111
|
+
return themePurple(safe, color, true);
|
|
112
|
+
if (index === 1)
|
|
113
|
+
return themeOrange(safe, color, true);
|
|
114
|
+
return themeCyan(safe, color, true);
|
|
115
|
+
});
|
|
116
|
+
if (width < 78) {
|
|
117
|
+
const lines = [panelHeading("Providers", width, color, "orange")];
|
|
118
|
+
for (const part of coloredEntries)
|
|
119
|
+
lines.push(` ${part}`);
|
|
120
|
+
if (providers.length > 3)
|
|
121
|
+
lines.push(` +${providers.length - 3} more`);
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
const heading = themePurple("Providers", color, true);
|
|
125
|
+
const joined = coloredEntries.join(themePurple(" · ", color));
|
|
126
|
+
const extra = providers.length > 3 ? themePurple(` · +${providers.length - 3} more`, color) : "";
|
|
127
|
+
const line = `${heading} · ${joined}${extra}`;
|
|
128
|
+
// Ensure the inline stack never exceeds terminal width
|
|
129
|
+
if ([...stripAnsi(line)].length > width) {
|
|
130
|
+
return [truncate(line, width)];
|
|
131
|
+
}
|
|
132
|
+
return [line];
|
|
133
|
+
}
|
|
134
|
+
function cardLines(kind, window, width, color, selected) {
|
|
135
|
+
const inner = Math.max(1, width - 2);
|
|
136
|
+
const borderColor = selected ? ANSI.orange : ANSI.purpleDeep;
|
|
137
|
+
const totalLabel = inner < 15 ? "Total" : "Recorded";
|
|
138
|
+
const total = `${themePurple(totalLabel, color, true)} ${themeOrange(formatTokenCount(window.totals.recorded_total), color, true)}`;
|
|
139
|
+
const first = pad(total, inner);
|
|
140
|
+
const rawComponents = formatTokenBreakdown(window.totals);
|
|
141
|
+
// Preserve the 4-space gaps (" ") — truncate() would collapse them via safeLabel.
|
|
142
|
+
// Adaptively fall back to 2 spaces when the 4-space layout doesn't fit.
|
|
143
|
+
let displayComponents = rawComponents;
|
|
144
|
+
if (lineWidth(rawComponents) > inner) {
|
|
145
|
+
const fallback = rawComponents.replaceAll(" ", " ");
|
|
146
|
+
if (lineWidth(fallback) <= inner)
|
|
147
|
+
displayComponents = fallback;
|
|
148
|
+
else
|
|
149
|
+
displayComponents = truncateExact(rawComponents, inner);
|
|
150
|
+
}
|
|
151
|
+
const title = `${selected ? "▶" : "·"} ${cardTitle(kind)}`;
|
|
152
|
+
const frame = (value) => `│${pad(value, inner)}│`;
|
|
153
|
+
const costLabel = inner < 15 ? "Cost" : "Est. cost";
|
|
154
|
+
const costValue = formatCost(window.cost);
|
|
155
|
+
const costLine = `${themePurple(costLabel, color, true)} ${themeCost(costValue, color)}`;
|
|
156
|
+
const lines = [
|
|
157
|
+
paint(border(width, "╭", "─", "╮"), borderColor, color),
|
|
158
|
+
frame(selected
|
|
159
|
+
? themeOrange(truncate(title, inner), color, true)
|
|
160
|
+
: themePurple(truncate(title, inner), color, true)),
|
|
161
|
+
frame(first),
|
|
162
|
+
frame(colorizeBreakdown(truncateExact(displayComponents, inner), color, true)),
|
|
163
|
+
frame(pad(costLine, inner)),
|
|
164
|
+
frame(themeOrange(truncate(`Range ${safeLabel(window.window.label)}`, inner), color)),
|
|
165
|
+
paint(border(width, "╰", "─", "╯"), borderColor, color),
|
|
166
|
+
];
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
function composeCards(cards, gap = 2) {
|
|
170
|
+
const maxLines = Math.max(...cards.map((card) => card.length), 0);
|
|
171
|
+
const widths = cards.map((card) => lineWidth(card[0] ?? ""));
|
|
172
|
+
return Array.from({ length: maxLines }, (_, row) => cards.map((card, index) => pad(card[row] ?? "", widths[index])).join(" ".repeat(gap)).trimEnd());
|
|
173
|
+
}
|
|
174
|
+
const HEADER_LINES = 4;
|
|
175
|
+
const CARD_LINES = 7;
|
|
176
|
+
const CARD_GAP = 2;
|
|
177
|
+
export function getCardHitRegions(width) {
|
|
178
|
+
const clamped = Math.max(20, width);
|
|
179
|
+
const headerHeight = HEADER_LINES;
|
|
180
|
+
const isWide = clamped >= WIDE_LAYOUT_MIN_WIDTH;
|
|
181
|
+
if (isWide) {
|
|
182
|
+
const cardWidth = Math.max(CARD_MIN_WIDTH, Math.floor((clamped - 4) / 3));
|
|
183
|
+
const y1 = headerHeight + 1;
|
|
184
|
+
const y2 = y1 + CARD_LINES - 1;
|
|
185
|
+
return allWindowKinds().map((kind, idx) => {
|
|
186
|
+
const x1 = idx * (cardWidth + CARD_GAP) + 1;
|
|
187
|
+
const x2 = x1 + cardWidth - 1;
|
|
188
|
+
return { kind, x1, y1, x2, y2 };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const cardWidth = Math.max(12, Math.min(CARD_MIN_WIDTH, clamped - 2));
|
|
192
|
+
const yGap = 1;
|
|
193
|
+
return allWindowKinds().map((kind, idx) => {
|
|
194
|
+
const y1 = headerHeight + 1 + idx * (CARD_LINES + yGap);
|
|
195
|
+
const y2 = y1 + CARD_LINES - 1;
|
|
196
|
+
const x1 = 1;
|
|
197
|
+
const x2 = x1 + cardWidth - 1;
|
|
198
|
+
return { kind, x1, y1, x2, y2 };
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function renderHeader(color, width) {
|
|
202
|
+
const compact = width < 70;
|
|
203
|
+
const identity = width < 28
|
|
204
|
+
? `${themeOrange("◈", color, true)} ${emphasis(themePurple("OC2", color, true), color)} ${themePurple("// TOKENS", color)}`
|
|
205
|
+
: `${themeOrange("◈", color, true)} ${emphasis(themePurple("OC2TOKEN", color, true), color)} ${themePurple("// USAGE CONSOLE", color)}`;
|
|
206
|
+
const title = compact ? "OC2Token Usage" : "OpenCode 2 Token Usage";
|
|
207
|
+
return [
|
|
208
|
+
identity,
|
|
209
|
+
emphasis(themePurple(truncate(title, width), color, true), color),
|
|
210
|
+
themePurple("─".repeat(Math.max(20, width)), color),
|
|
211
|
+
"",
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
function renderStatusFooter(snapshot, color, width) {
|
|
215
|
+
const state = coverageState(snapshot.coverage, snapshot.stale);
|
|
216
|
+
const coverage = formatCoverage(snapshot.coverage, snapshot.stale);
|
|
217
|
+
return statusColor(truncate(`Status: ${coverage} · Coverage: ${coverage}`, width), state, color);
|
|
218
|
+
}
|
|
219
|
+
function renderTrend(snapshot, selected, color, width) {
|
|
220
|
+
const trend = snapshot.windows[selected].trends;
|
|
221
|
+
const title = `Trend · ${cardTitle(selected).toLowerCase()}`;
|
|
222
|
+
if (trend.length === 0)
|
|
223
|
+
return [panelHeading(title, width, color), " No trend data recorded."];
|
|
224
|
+
const max = Math.max(...trend.map((bucket) => bucket.totals.recorded_total), 0);
|
|
225
|
+
if (max === 0)
|
|
226
|
+
return [panelHeading(title, width, color), " No trend data recorded."];
|
|
227
|
+
const graphWidth = Math.max(8, width - 4);
|
|
228
|
+
const points = trend.length <= graphWidth
|
|
229
|
+
? Array.from({ length: graphWidth }, (_, index) => {
|
|
230
|
+
const sourceIndex = Math.floor(index * trend.length / graphWidth);
|
|
231
|
+
const bucket = trend[sourceIndex];
|
|
232
|
+
return { label: bucket.label, totals: bucket.totals };
|
|
233
|
+
})
|
|
234
|
+
: Array.from({ length: graphWidth }, (_, index) => {
|
|
235
|
+
const start = Math.floor(index * trend.length / graphWidth);
|
|
236
|
+
const end = Math.max(start + 1, Math.floor((index + 1) * trend.length / graphWidth));
|
|
237
|
+
return {
|
|
238
|
+
label: trend[start]?.label ?? "",
|
|
239
|
+
totals: trend.slice(start, end).reduce(sumTrendTotals, zeroTotals()),
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
const levels = 5;
|
|
243
|
+
return [
|
|
244
|
+
panelHeading(title, width, color),
|
|
245
|
+
...Array.from({ length: levels }, (_, row) => {
|
|
246
|
+
const level = levels - row;
|
|
247
|
+
const cells = points.map((point) => {
|
|
248
|
+
const filled = point.totals.recorded_total > 0 && point.totals.recorded_total / max >= level / levels;
|
|
249
|
+
return filled ? themeOrange("█", color, true) : themePurple("·", color);
|
|
250
|
+
}).join("");
|
|
251
|
+
return ` ${cells}`;
|
|
252
|
+
}),
|
|
253
|
+
` ${themePurple("─".repeat(graphWidth), color)}`,
|
|
254
|
+
` ${themePurple(graphLabels(points, graphWidth), color)}`,
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
function zeroTotals() {
|
|
258
|
+
return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, recorded_total: 0 };
|
|
259
|
+
}
|
|
260
|
+
function sumTrendTotals(left, right) {
|
|
261
|
+
return {
|
|
262
|
+
input: left.input + right.totals.input,
|
|
263
|
+
output: left.output + right.totals.output,
|
|
264
|
+
reasoning: left.reasoning + right.totals.reasoning,
|
|
265
|
+
cacheRead: left.cacheRead + right.totals.cacheRead,
|
|
266
|
+
cacheWrite: left.cacheWrite + right.totals.cacheWrite,
|
|
267
|
+
recorded_total: left.recorded_total + right.totals.recorded_total,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function graphLabels(points, width) {
|
|
271
|
+
if (points.length === 0)
|
|
272
|
+
return "";
|
|
273
|
+
const labelWidth = Math.max(3, Math.min(7, Math.floor(width / 3)));
|
|
274
|
+
const labels = points.length < 12
|
|
275
|
+
? [{ index: 0, value: points[0]?.label ?? "" }, { index: points.length - 1, value: points.at(-1)?.label ?? "" }]
|
|
276
|
+
: [
|
|
277
|
+
{ index: 0, value: points[0]?.label ?? "" },
|
|
278
|
+
{ index: Math.floor(points.length / 2), value: points[Math.floor(points.length / 2)]?.label ?? "" },
|
|
279
|
+
{ index: points.length - 1, value: points.at(-1)?.label ?? "" },
|
|
280
|
+
];
|
|
281
|
+
const characters = Array.from({ length: width }, () => " ");
|
|
282
|
+
for (const entry of labels) {
|
|
283
|
+
const value = truncate(entry.value, labelWidth);
|
|
284
|
+
const position = entry.index === points.length - 1
|
|
285
|
+
? Math.max(0, width - value.length)
|
|
286
|
+
: Math.min(width - value.length, Math.round(entry.index / Math.max(1, points.length - 1) * (width - value.length)));
|
|
287
|
+
for (const [offset, character] of [...value].entries()) {
|
|
288
|
+
if (position + offset < characters.length)
|
|
289
|
+
characters[position + offset] = character;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return characters.join("").trimEnd();
|
|
293
|
+
}
|
|
294
|
+
function renderBreakdown(title, values, color, width, accent) {
|
|
295
|
+
const lines = [panelHeading(title, width, color, accent)];
|
|
296
|
+
if (values.length === 0) {
|
|
297
|
+
return [...lines, width < 32 ? " (none)" : " No breakdown data recorded."];
|
|
298
|
+
}
|
|
299
|
+
const visible = values.slice(0, 12);
|
|
300
|
+
const isProvider = title.toLowerCase().includes("provider");
|
|
301
|
+
const nameWidth = Math.max(8, Math.min(42, Math.floor(width * 0.42)));
|
|
302
|
+
const showTokenDetails = width >= 70;
|
|
303
|
+
const showCost = width >= 32;
|
|
304
|
+
// Table-aligned In/Out/Cache/Cost columns: header already shows labels, rows show only numbers.
|
|
305
|
+
const inStrs = visible.map((item) => formatTokenCount(item.totals.input));
|
|
306
|
+
const outStrs = visible.map((item) => formatTokenCount(item.totals.output + item.totals.reasoning));
|
|
307
|
+
const cacheStrs = visible.map((item) => formatTokenCount(item.totals.cacheRead + item.totals.cacheWrite));
|
|
308
|
+
const costStrs = visible.map((item) => formatCost(item.cost));
|
|
309
|
+
const inWidth = Math.max("In".length, ...inStrs.map((s) => [...s].length));
|
|
310
|
+
const outWidth = Math.max("Out".length, ...outStrs.map((s) => [...s].length));
|
|
311
|
+
const cacheWidth = Math.max("Cache".length, ...cacheStrs.map((s) => [...s].length));
|
|
312
|
+
const costWidth = Math.max("Cost".length, ...costStrs.map((s) => [...s].length));
|
|
313
|
+
// Header uses same column widths so values line up vertically (right-aligned numbers).
|
|
314
|
+
const headerIn = themeIn("In".padStart(inWidth), color);
|
|
315
|
+
const headerOut = themeOut("Out".padStart(outWidth), color);
|
|
316
|
+
const headerCache = themeCache("Cache".padStart(cacheWidth), color);
|
|
317
|
+
const headerCost = themeCost("Cost".padStart(costWidth), color);
|
|
318
|
+
const details = showTokenDetails
|
|
319
|
+
? showCost
|
|
320
|
+
? ` ${headerIn} ${headerOut} ${headerCache} ${headerCost}`
|
|
321
|
+
: ` ${headerIn} ${headerOut} ${headerCache}`
|
|
322
|
+
: showCost
|
|
323
|
+
? ` ${headerCost}`
|
|
324
|
+
: "";
|
|
325
|
+
lines.push(` ${themePurple("NAME".padEnd(nameWidth), color, true)} ${themeOrange("RECORDED".padStart(8), color, true)}${details}`);
|
|
326
|
+
for (const item of visible) {
|
|
327
|
+
const rawName = item.provider ? `${safeIdentifier(item.provider)}/${safeIdentifier(item.name)}` : safeIdentifier(item.name);
|
|
328
|
+
const safeName = truncate(rawName, nameWidth);
|
|
329
|
+
const countText = formatTokenCount(item.totals.recorded_total).padStart(8);
|
|
330
|
+
const inPlain = formatTokenCount(item.totals.input);
|
|
331
|
+
const outPlain = formatTokenCount(item.totals.output + item.totals.reasoning);
|
|
332
|
+
const cachePlain = formatTokenCount(item.totals.cacheRead + item.totals.cacheWrite);
|
|
333
|
+
const costPlain = formatCost(item.cost);
|
|
334
|
+
const inCol = themeIn(inPlain.padStart(inWidth), color);
|
|
335
|
+
const outCol = themeOut(outPlain.padStart(outWidth), color);
|
|
336
|
+
const cacheCol = themeCache(cachePlain.padStart(cacheWidth), color);
|
|
337
|
+
const costCol = themeCost(costPlain.padStart(costWidth), color);
|
|
338
|
+
const componentDetails = showTokenDetails
|
|
339
|
+
? showCost
|
|
340
|
+
? ` ${inCol} ${outCol} ${cacheCol} ${costCol}`
|
|
341
|
+
: ` ${inCol} ${outCol} ${cacheCol}`
|
|
342
|
+
: showCost
|
|
343
|
+
? ` ${costCol}`
|
|
344
|
+
: "";
|
|
345
|
+
if (isProvider) {
|
|
346
|
+
const accentForRow = providerAccent(item.name, color, true);
|
|
347
|
+
const countColored = accentForRow(countText.trimStart().padStart(8));
|
|
348
|
+
// Keep provider name in its accent but retain table alignment for token columns.
|
|
349
|
+
const nameColored = accentForRow(safeName.padEnd(nameWidth));
|
|
350
|
+
lines.push(` ${nameColored} ${countColored}${componentDetails}`);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
lines.push(` ${themePurple(safeName.padEnd(nameWidth), color)} ${themeOrange(countText, color)}${componentDetails}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (values.length > visible.length) {
|
|
357
|
+
lines.push(` +${values.length - visible.length} more`);
|
|
358
|
+
}
|
|
359
|
+
return lines;
|
|
360
|
+
}
|
|
361
|
+
function formatRefreshInterval(seconds) {
|
|
362
|
+
const clamped = Math.max(60, Math.min(14400, Math.floor(seconds)));
|
|
363
|
+
if (clamped % 3600 === 0) {
|
|
364
|
+
const h = clamped / 3600;
|
|
365
|
+
return `${h}h`;
|
|
366
|
+
}
|
|
367
|
+
if (clamped >= 3600) {
|
|
368
|
+
const h = Math.floor(clamped / 3600);
|
|
369
|
+
const m = Math.floor((clamped % 3600) / 60);
|
|
370
|
+
if (m === 0)
|
|
371
|
+
return `${h}h`;
|
|
372
|
+
return `${h}h ${m}m`;
|
|
373
|
+
}
|
|
374
|
+
if (clamped % 60 === 0) {
|
|
375
|
+
return `${clamped / 60}m`;
|
|
376
|
+
}
|
|
377
|
+
const m = Math.floor(clamped / 60);
|
|
378
|
+
const s = clamped % 60;
|
|
379
|
+
if (m === 0)
|
|
380
|
+
return `${s}s`;
|
|
381
|
+
return `${m}m ${s}s`;
|
|
382
|
+
}
|
|
383
|
+
function renderSettingsPanel(settings, width, color) {
|
|
384
|
+
const REFRESH_PRESETS = [60, 120, 300, 600, 900, 1800, 3600, 7200, 14400];
|
|
385
|
+
function presetIndex(value) {
|
|
386
|
+
const clamped = Math.max(60, Math.min(14400, Math.floor(value)));
|
|
387
|
+
let best = 0;
|
|
388
|
+
let bestDiff = Infinity;
|
|
389
|
+
for (let i = 0; i < REFRESH_PRESETS.length; i += 1) {
|
|
390
|
+
const diff = Math.abs(REFRESH_PRESETS[i] - clamped);
|
|
391
|
+
if (diff < bestDiff) {
|
|
392
|
+
bestDiff = diff;
|
|
393
|
+
best = i;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return best;
|
|
397
|
+
}
|
|
398
|
+
const panelWidth = Math.min(width, Math.min(Math.max(38, width - 4), 58));
|
|
399
|
+
const inner = Math.max(1, panelWidth - 2);
|
|
400
|
+
const leftPad = Math.max(0, Math.floor((width - panelWidth) / 2));
|
|
401
|
+
const padPrefix = " ".repeat(leftPad);
|
|
402
|
+
const providers = ["opencode", "codex", "antigravity"];
|
|
403
|
+
const intervalStr = formatRefreshInterval(settings.refreshIntervalSeconds);
|
|
404
|
+
const presetIdx = presetIndex(settings.refreshIntervalSeconds);
|
|
405
|
+
const pct = presetIdx / (REFRESH_PRESETS.length - 1);
|
|
406
|
+
const lines = [];
|
|
407
|
+
lines.push(padPrefix + paint(border(panelWidth, "╭", "─", "╮"), ANSI.purpleDeep, color));
|
|
408
|
+
// Title
|
|
409
|
+
const titlePlain = "◆ Settings";
|
|
410
|
+
const hintPlain = "· s / esc to close";
|
|
411
|
+
const titleAvail = Math.max(6, inner);
|
|
412
|
+
// Truncate plain parts separately to keep colors
|
|
413
|
+
const hintAvail = Math.max(1, inner - titlePlain.length - 2);
|
|
414
|
+
const titleTrunc = truncate(titlePlain, Math.min(titlePlain.length, inner));
|
|
415
|
+
const hintTrunc = truncate(hintPlain, hintAvail);
|
|
416
|
+
const titleLine = `${themeOrange(titleTrunc, color, true)} ${themePurple(hintTrunc, color)}`;
|
|
417
|
+
lines.push(padPrefix + `│${pad(titleLine, inner)}│`);
|
|
418
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
419
|
+
// Providers heading
|
|
420
|
+
const providersHeading = truncate("Providers — space to toggle", inner);
|
|
421
|
+
lines.push(padPrefix + `│${pad(themePurple(providersHeading, color, true), inner)}│`);
|
|
422
|
+
for (let i = 0; i < providers.length; i += 1) {
|
|
423
|
+
const name = providers[i];
|
|
424
|
+
const enabled = settings.enabledProviders.includes(name);
|
|
425
|
+
const focused = settings.focusedIndex === i;
|
|
426
|
+
const checkbox = enabled ? "◉" : "○";
|
|
427
|
+
const prefix = focused ? themeOrange("▶ ", color, true) : " ";
|
|
428
|
+
const checkColored = enabled ? themeOrange(checkbox, color, true) : themePurple(checkbox, color);
|
|
429
|
+
const accent = providerAccent(name, color, true);
|
|
430
|
+
const suffix = focused ? " ← toggle" : "";
|
|
431
|
+
const suffixLen = focused ? 10 : 0; // " ← toggle" visible length
|
|
432
|
+
const nameAvail = Math.max(4, inner - 4 - suffixLen);
|
|
433
|
+
const nameTrunc = truncate(name, nameAvail);
|
|
434
|
+
const nameColored = enabled ? accent(nameTrunc) : themePurple(nameTrunc, color);
|
|
435
|
+
const lineContent = `${prefix}${checkColored} ${nameColored}`;
|
|
436
|
+
const full = focused ? `${lineContent}${themeOrange(suffix, color)}` : lineContent;
|
|
437
|
+
lines.push(padPrefix + `│${pad(full, inner)}│`);
|
|
438
|
+
}
|
|
439
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
440
|
+
// Refresh interval heading with value
|
|
441
|
+
const intervalFocused = settings.focusedIndex === 3;
|
|
442
|
+
const intervalHeadingPlain = "Refresh interval";
|
|
443
|
+
const headingAvail = Math.max(6, inner - intervalStr.length - 5 - (intervalFocused ? 2 : 0));
|
|
444
|
+
const headingTrunc = truncate(intervalHeadingPlain, headingAvail);
|
|
445
|
+
const headingColored = intervalFocused
|
|
446
|
+
? themeOrange(`▶ ${headingTrunc}`, color, true)
|
|
447
|
+
: themePurple(headingTrunc, color, true);
|
|
448
|
+
const headingWithValue = `${headingColored} ${themePurple("·", color)} ${themeOrange(intervalStr, color, true)}`;
|
|
449
|
+
lines.push(padPrefix + `│${pad(headingWithValue, inner)}│`);
|
|
450
|
+
// Slider track
|
|
451
|
+
const leftLabel = "1m";
|
|
452
|
+
const rightLabel = "4h";
|
|
453
|
+
const trackAvail = Math.max(8, inner - leftLabel.length - rightLabel.length - 6);
|
|
454
|
+
const trackLen = Math.max(8, Math.min(26, trackAvail));
|
|
455
|
+
const handlePos = Math.round(pct * (trackLen - 1));
|
|
456
|
+
const ticks = new Set();
|
|
457
|
+
for (let i = 0; i < REFRESH_PRESETS.length; i += 1) {
|
|
458
|
+
ticks.add(Math.round((i / (REFRESH_PRESETS.length - 1)) * (trackLen - 1)));
|
|
459
|
+
}
|
|
460
|
+
const trackChars = Array.from({ length: trackLen }, (_, idx) => {
|
|
461
|
+
if (idx === handlePos)
|
|
462
|
+
return "●";
|
|
463
|
+
if (ticks.has(idx))
|
|
464
|
+
return "·";
|
|
465
|
+
return "─";
|
|
466
|
+
});
|
|
467
|
+
const trackStr = trackChars
|
|
468
|
+
.map((ch, idx) => {
|
|
469
|
+
if (idx === handlePos)
|
|
470
|
+
return intervalFocused ? themeOrange(ch, color, true) : themePurple(ch, color, true);
|
|
471
|
+
if (ch === "·")
|
|
472
|
+
return themePurple(ch, color);
|
|
473
|
+
if (idx < handlePos)
|
|
474
|
+
return themeOrange(ch, color);
|
|
475
|
+
return themePurple(ch, color);
|
|
476
|
+
})
|
|
477
|
+
.join("");
|
|
478
|
+
const sliderCore = `${themePurple(leftLabel, color)} ${trackStr} ${themePurple(rightLabel, color)}`;
|
|
479
|
+
// Center slider within inner
|
|
480
|
+
const sliderVisibleLen = leftLabel.length + 1 + trackLen + 1 + rightLabel.length;
|
|
481
|
+
const sliderLeftPad = Math.max(1, Math.floor((inner - sliderVisibleLen) / 2));
|
|
482
|
+
const sliderLine = `${" ".repeat(sliderLeftPad)}${sliderCore}`;
|
|
483
|
+
lines.push(padPrefix + `│${pad(sliderLine, inner)}│`);
|
|
484
|
+
// Preset scale / hint
|
|
485
|
+
if (inner >= 44) {
|
|
486
|
+
const presetLabels = REFRESH_PRESETS.map((v) => formatRefreshInterval(v));
|
|
487
|
+
const scale = presetLabels
|
|
488
|
+
.map((label, idx) => (idx === presetIdx ? themeOrange(label, color, true) : themePurple(label, color)))
|
|
489
|
+
.join(themePurple(" · ", color));
|
|
490
|
+
const scaleTrunc = truncate(scale, inner);
|
|
491
|
+
// If truncated, scale may have stripped colors; re-apply? Keep simple: truncate plain then recolor? But scale already colored; truncate will strip.
|
|
492
|
+
// Instead build plain scale and compare: if scale visible length > inner, fall back to compact hint.
|
|
493
|
+
const plainScaleLen = presetLabels.join(" · ").length;
|
|
494
|
+
if (plainScaleLen + 2 <= inner) {
|
|
495
|
+
lines.push(padPrefix + `│${pad(` ${scale}`, inner)}│`);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
// Fallback compact: show current value centered
|
|
499
|
+
const valLine = themeOrange(intervalStr, color, true);
|
|
500
|
+
const padVal = Math.max(0, Math.floor((inner - intervalStr.length) / 2));
|
|
501
|
+
lines.push(padPrefix + `│${pad(`${" ".repeat(padVal)}${valLine}`, inner)}│`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
const compact = intervalFocused ? "← → to adjust" : "← 1m · 4h →";
|
|
506
|
+
const hint = intervalFocused ? themeOrange(compact, color, true) : themePurple(compact, color);
|
|
507
|
+
const hintPad = Math.max(0, Math.floor((inner - stripAnsi(compact).length) / 2));
|
|
508
|
+
lines.push(padPrefix + `│${pad(`${" ".repeat(hintPad)}${hint}`, inner)}│`);
|
|
509
|
+
}
|
|
510
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
511
|
+
const navHint = "↑↓ navigate • space toggle • ←→ adjust";
|
|
512
|
+
const closeHint = "s / esc close • r refresh";
|
|
513
|
+
lines.push(padPrefix + `│${pad(themePurple(truncate(navHint, inner), color), inner)}│`);
|
|
514
|
+
lines.push(padPrefix + `│${pad(themePurple(truncate(closeHint, inner), color), inner)}│`);
|
|
515
|
+
lines.push(padPrefix + paint(border(panelWidth, "╰", "─", "╯"), ANSI.purpleDeep, color));
|
|
516
|
+
return lines;
|
|
517
|
+
}
|
|
518
|
+
function renderProjectsPanel(snapshot, selected, width, color) {
|
|
519
|
+
const windowProjects = snapshot.windows[selected]?.projects ?? [];
|
|
520
|
+
const globalProjects = snapshot.projects;
|
|
521
|
+
const projects = windowProjects.length > 0 ? windowProjects : globalProjects;
|
|
522
|
+
const panelWidth = Math.min(width, Math.min(Math.max(42, width - 6), 76));
|
|
523
|
+
const inner = Math.max(1, panelWidth - 2);
|
|
524
|
+
const leftPad = Math.max(0, Math.floor((width - panelWidth) / 2));
|
|
525
|
+
const padPrefix = " ".repeat(leftPad);
|
|
526
|
+
const titleKind = selected === "hour" ? "Last 60 Minutes" : selected === "day" ? "Today" : "This Week";
|
|
527
|
+
const totalForWindow = snapshot.windows[selected]?.totals.recorded_total ?? 0;
|
|
528
|
+
const lines = [];
|
|
529
|
+
lines.push(padPrefix + paint(border(panelWidth, "╭", "─", "╮"), ANSI.purpleDeep, color));
|
|
530
|
+
const titlePlain = `◆ Projects · ${titleKind}`;
|
|
531
|
+
const hintPlain = "· p / esc to close";
|
|
532
|
+
const titleAvail = Math.max(8, inner - hintPlain.length - 1);
|
|
533
|
+
const titleTrunc = truncate(titlePlain, titleAvail);
|
|
534
|
+
const hintTrunc = truncate(hintPlain, Math.max(1, inner - titleTrunc.length - 1));
|
|
535
|
+
const titleLine = `${themeOrange(titleTrunc, color, true)} ${themePurple(hintTrunc, color)}`;
|
|
536
|
+
lines.push(padPrefix + `│${pad(titleLine, inner)}│`);
|
|
537
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
538
|
+
if (projects.length === 0) {
|
|
539
|
+
const emptyMsg = truncate("No project data recorded for this period.", inner - 2);
|
|
540
|
+
lines.push(padPrefix + `│${pad(` ${themePurple(emptyMsg, color)}`, inner)}│`);
|
|
541
|
+
lines.push(padPrefix + `│${pad(themePurple(truncate("Projects are derived from session directories.", inner - 2), color), inner)}│`);
|
|
542
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
543
|
+
lines.push(padPrefix + `│${pad(themePurple(truncate("Tip: run a session in a project folder and refresh.", inner), color), inner)}│`);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
const visible = projects.slice(0, 10);
|
|
547
|
+
const showCost = inner >= 50;
|
|
548
|
+
let costWidth = 0;
|
|
549
|
+
let headerCost = "";
|
|
550
|
+
if (showCost) {
|
|
551
|
+
const costStrs = visible.map((entry) => formatCost(entry.cost));
|
|
552
|
+
const winCostStr = formatCost(snapshot.windows[selected]?.cost);
|
|
553
|
+
const allCostStrs = [...costStrs, winCostStr];
|
|
554
|
+
costWidth = Math.max("Cost".length, ...allCostStrs.map((s) => [...s].length));
|
|
555
|
+
headerCost = themeCost("Cost".padStart(costWidth), color);
|
|
556
|
+
}
|
|
557
|
+
// Header row
|
|
558
|
+
const nameWidth = Math.max(12, Math.min(36, Math.floor(inner * 0.52)));
|
|
559
|
+
const headerName = themePurple("PROJECT".padEnd(nameWidth), color, true);
|
|
560
|
+
const headerRecorded = themeOrange("RECORDED".padStart(9), color, true);
|
|
561
|
+
const headerPct = themePurple("%".padStart(4), color);
|
|
562
|
+
const header = showCost
|
|
563
|
+
? ` ${headerName} ${headerRecorded} ${headerPct} ${headerCost}`
|
|
564
|
+
: ` ${headerName} ${headerRecorded} ${headerPct}`;
|
|
565
|
+
lines.push(padPrefix + `│${pad(header, inner)}│`);
|
|
566
|
+
const separator = showCost
|
|
567
|
+
? ` ${themePurple("─".repeat(nameWidth), color)} ${themePurple("─".repeat(9), color)} ${themePurple("─".repeat(4), color)} ${themePurple("─".repeat(costWidth), color)}`
|
|
568
|
+
: ` ${themePurple("─".repeat(nameWidth), color)} ${themePurple("─".repeat(9), color)} ${themePurple("─".repeat(4), color)}`;
|
|
569
|
+
lines.push(padPrefix + `│${pad(separator, inner)}│`);
|
|
570
|
+
for (const entry of visible) {
|
|
571
|
+
const pct = totalForWindow > 0 ? Math.round((entry.totals.recorded_total / totalForWindow) * 100) : 0;
|
|
572
|
+
const count = formatTokenCount(entry.totals.recorded_total).padStart(9);
|
|
573
|
+
const pctStr = `${pct}%`.padStart(4);
|
|
574
|
+
// Derive short name: basename if absolute path, otherwise full
|
|
575
|
+
let display = entry.name;
|
|
576
|
+
if (display.includes("/") && display.length > nameWidth) {
|
|
577
|
+
const parts = display.split("/").filter((p) => p.length > 0);
|
|
578
|
+
const base = parts.at(-1) ?? display;
|
|
579
|
+
const oneUp = parts.length >= 2 ? `${parts.at(-2)}/${base}` : base;
|
|
580
|
+
if (oneUp.length <= nameWidth)
|
|
581
|
+
display = oneUp;
|
|
582
|
+
else
|
|
583
|
+
display = base;
|
|
584
|
+
}
|
|
585
|
+
const safe = truncate(display, nameWidth);
|
|
586
|
+
const nameColored = themeCyan(safe.padEnd(nameWidth), color, true);
|
|
587
|
+
const countColored = themeOrange(count, color, true);
|
|
588
|
+
const pctColored = themePurple(pctStr, color);
|
|
589
|
+
if (showCost) {
|
|
590
|
+
const costColored = themeCost(formatCost(entry.cost).padStart(costWidth), color);
|
|
591
|
+
lines.push(padPrefix + `│${pad(` ${nameColored} ${countColored} ${pctColored} ${costColored}`, inner)}│`);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
lines.push(padPrefix + `│${pad(` ${nameColored} ${countColored} ${pctColored}`, inner)}│`);
|
|
595
|
+
}
|
|
596
|
+
// Second line with breakdown In/Out/Cache for that project under narrow? Only if inner wide
|
|
597
|
+
if (inner >= 50) {
|
|
598
|
+
const breakdown = formatTokenBreakdown(entry.totals);
|
|
599
|
+
if (showCost) {
|
|
600
|
+
const costStr = formatCost(entry.cost);
|
|
601
|
+
const availForBreakdown = Math.max(1, inner - 4 - 2 - [...costStr].length);
|
|
602
|
+
const breakdownTrunc = truncate(breakdown, availForBreakdown);
|
|
603
|
+
const coloredBreakdown = colorizeBreakdown(breakdownTrunc, color);
|
|
604
|
+
const coloredCost = themeCost(costStr, color);
|
|
605
|
+
const combined = `${coloredBreakdown} ${coloredCost}`;
|
|
606
|
+
lines.push(padPrefix + `│${pad(` ${combined}`, inner)}│`);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const breakdownTrunc = truncate(breakdown, inner - 4);
|
|
610
|
+
const coloredBreakdown = colorizeBreakdown(breakdownTrunc, color);
|
|
611
|
+
lines.push(padPrefix + `│${pad(` ${themePurple(coloredBreakdown, color)}`, inner)}│`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (projects.length > visible.length) {
|
|
616
|
+
const more = truncate(`+${projects.length - visible.length} more projects`, inner - 2);
|
|
617
|
+
lines.push(padPrefix + `│${pad(` ${themePurple(more, color)}`, inner)}│`);
|
|
618
|
+
}
|
|
619
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
620
|
+
const windowCostStr = formatCost(snapshot.windows[selected]?.cost);
|
|
621
|
+
const totalLine = showCost
|
|
622
|
+
? `Total projects: ${projects.length} · Period total: ${formatTokenCount(totalForWindow)} · Period cost: ${windowCostStr}`
|
|
623
|
+
: `Total projects: ${projects.length} · Period total: ${formatTokenCount(totalForWindow)}`;
|
|
624
|
+
lines.push(padPrefix + `│${pad(themePurple(truncate(totalLine, inner), color), inner)}│`);
|
|
625
|
+
}
|
|
626
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
627
|
+
lines.push(padPrefix + `│${pad(themePurple(truncate("p / esc to close • 1/2/3 switch period", inner), color), inner)}│`);
|
|
628
|
+
lines.push(padPrefix + paint(border(panelWidth, "╰", "─", "╯"), ANSI.purpleDeep, color));
|
|
629
|
+
return lines;
|
|
630
|
+
}
|
|
631
|
+
function renderCredit(width, color) {
|
|
632
|
+
const repo = "github.com/kaanyaren/OC2Token";
|
|
633
|
+
const full = `Kaan Yaren · ${repo}`;
|
|
634
|
+
const plain = width < 45 ? "Kaan Yaren" : full;
|
|
635
|
+
const truncated = truncate(plain, width);
|
|
636
|
+
// Use hyperlink OSC 8 when color/ansi enabled — still counts as plain text for width checks (stripAnsi removes it)
|
|
637
|
+
const link = colorEnabled({ isTTY: true, ansi: true, color })
|
|
638
|
+
? `\u001b]8;;https://github.com/kaanyaren/OC2Token\u0007${truncated}\u001b]8;;\u0007`
|
|
639
|
+
: truncated;
|
|
640
|
+
const colored = paint(link, ANSI.purpleDim, color);
|
|
641
|
+
// Center within width
|
|
642
|
+
const visibleLen = lineWidth(truncated);
|
|
643
|
+
const left = Math.max(0, Math.floor((width - visibleLen) / 2));
|
|
644
|
+
// stripAnsi for final length check: left pad + truncated length <= width (when color false)
|
|
645
|
+
// When color true, link still has OSC 8 but stripAnsi removes it, so same.
|
|
646
|
+
return `${" ".repeat(left)}${colored}`;
|
|
647
|
+
}
|
|
648
|
+
function renderFooter(help, color, width) {
|
|
649
|
+
const key = (value) => themeOrange(value, color, true);
|
|
650
|
+
if (help) {
|
|
651
|
+
if (width < 50) {
|
|
652
|
+
return [
|
|
653
|
+
"",
|
|
654
|
+
panelHeading("Help", width, color, "orange"),
|
|
655
|
+
` ${key("r")} refresh ${key("1/2/3")} periods ${key("p")} projects`,
|
|
656
|
+
` ${key("q")} quit ${key("?")} close`,
|
|
657
|
+
];
|
|
658
|
+
}
|
|
659
|
+
if (width < 70) {
|
|
660
|
+
return [
|
|
661
|
+
"",
|
|
662
|
+
panelHeading("Help", width, color, "orange"),
|
|
663
|
+
` ${key("r/R")} Refresh ${key("1/2/3")} Periods ${key("p")} Projects`,
|
|
664
|
+
` ${key("q")} Quit ${key("?")} Toggle help ${key("s")} Settings`,
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
return [
|
|
668
|
+
"",
|
|
669
|
+
panelHeading("Help", width, color, "orange"),
|
|
670
|
+
` ${key("r/R")} Refresh now ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("Tab/Arrows")} Navigate`,
|
|
671
|
+
` ${key("p")} Projects ${key("s")} Settings ${key("q")} Quit ${key("?")} Toggle help`,
|
|
672
|
+
];
|
|
673
|
+
}
|
|
674
|
+
if (width < 50) {
|
|
675
|
+
return [` ${key("r")} ${key("1/2/3")} ${key("p")} ${key("s")} ${key("q")} ${key("?")}`];
|
|
676
|
+
}
|
|
677
|
+
if (width < 70) {
|
|
678
|
+
// Keep footer under 50 chars so width=50 still truncates correctly
|
|
679
|
+
if (width < 60) {
|
|
680
|
+
return [` ${key("r")} Refresh ${key("1/2/3")} ${key("p")} Proj ${key("s")} Settings ${key("q")} Quit ${key("?")} Help`];
|
|
681
|
+
}
|
|
682
|
+
return [` ${key("r")} Refresh ${key("1/2/3")} Periods ${key("p")} Projects ${key("s")} Settings ${key("q")} Quit ${key("?")} Help`];
|
|
683
|
+
}
|
|
684
|
+
return [
|
|
685
|
+
"",
|
|
686
|
+
` ${key("r")} Refresh ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("Tab/Arrows")} Navigate ${key("p")} Projects ${key("s")} Settings ${key("q")} Quit ${key("?")} Help`,
|
|
687
|
+
];
|
|
688
|
+
}
|
|
689
|
+
export function renderDashboard(input, options = {}) {
|
|
690
|
+
const snapshot = normalizeDashboardSnapshot(input);
|
|
691
|
+
// Keep a small floor for borders, but never force a 40-column frame on a
|
|
692
|
+
// narrower terminal. Content is truncated/stacked rather than relying on
|
|
693
|
+
// terminal wrapping to preserve redraw geometry.
|
|
694
|
+
const width = Math.max(20, options.width ?? 100);
|
|
695
|
+
const color = colorEnabled(options);
|
|
696
|
+
const selected = options.selectedWindow ?? "day";
|
|
697
|
+
const lines = renderHeader(color, width);
|
|
698
|
+
const cardWidth = width >= WIDE_LAYOUT_MIN_WIDTH
|
|
699
|
+
? Math.max(CARD_MIN_WIDTH, Math.floor((width - 4) / 3))
|
|
700
|
+
: Math.max(12, Math.min(CARD_MIN_WIDTH, width - 2));
|
|
701
|
+
const cards = allWindowKinds().map((kind) => cardLines(kind, snapshot.windows[kind], cardWidth, color, selected === kind));
|
|
702
|
+
if (width >= WIDE_LAYOUT_MIN_WIDTH && cards.length === 3) {
|
|
703
|
+
lines.push(...composeCards(cards));
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
for (const card of cards)
|
|
707
|
+
lines.push(...card, "");
|
|
708
|
+
}
|
|
709
|
+
// Below cards render unified provider stack - cards are unified totals, stack shows per-provider split with 3 accents
|
|
710
|
+
const stackProviders = snapshot.windows[selected]?.providers ?? snapshot.providers;
|
|
711
|
+
if (stackProviders.length > 0) {
|
|
712
|
+
lines.push("");
|
|
713
|
+
lines.push(...renderProviderStack(stackProviders, color, width));
|
|
714
|
+
}
|
|
715
|
+
lines.push("");
|
|
716
|
+
lines.push(...renderTrend(snapshot, selected, color, width));
|
|
717
|
+
lines.push("");
|
|
718
|
+
const selectedWindow = snapshot.windows[selected];
|
|
719
|
+
lines.push(...renderBreakdown(`Models · ${cardTitle(selected)}`, selectedWindow.models, color, width, "purple"));
|
|
720
|
+
lines.push(...renderBreakdown(`Providers · ${cardTitle(selected)}`, selectedWindow.providers, color, width, "orange"));
|
|
721
|
+
lines.push(...renderBreakdown(`Projects · ${cardTitle(selected)}`, selectedWindow.projects, color, width, "cyan"));
|
|
722
|
+
if (snapshot.coverage.errors.length > 0) {
|
|
723
|
+
lines.push("");
|
|
724
|
+
lines.push(emphasis("Coverage errors", color));
|
|
725
|
+
for (const error of snapshot.coverage.errors.slice(0, 5)) {
|
|
726
|
+
const message = error.message;
|
|
727
|
+
const label = message ? safeIdentifier(message) : safeIdentifier(error.code);
|
|
728
|
+
lines.push(` ${label}${error.sessionID ? ` (${safeIdentifier(error.sessionID)})` : ""}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
lines.push(renderStatusFooter(snapshot, color, width));
|
|
732
|
+
lines.push(...renderFooter(options.help === true, color, width));
|
|
733
|
+
if (options.settings?.visible === true) {
|
|
734
|
+
lines.push("");
|
|
735
|
+
lines.push(...renderSettingsPanel(options.settings, width, color));
|
|
736
|
+
}
|
|
737
|
+
if (options.projects?.visible === true) {
|
|
738
|
+
lines.push("");
|
|
739
|
+
lines.push(...renderProjectsPanel(snapshot, selected, width, color));
|
|
740
|
+
}
|
|
741
|
+
lines.push("");
|
|
742
|
+
lines.push(renderCredit(width, color));
|
|
743
|
+
const content = lines.join("\n");
|
|
744
|
+
return options.previousLineCount === undefined
|
|
745
|
+
? content
|
|
746
|
+
: renderInPlace(content, options.previousLineCount, options);
|
|
747
|
+
}
|
|
748
|
+
export const renderTUI = renderDashboard;
|
|
749
|
+
export const renderTerminalDashboard = renderDashboard;
|
|
750
|
+
//# sourceMappingURL=dashboard.js.map
|