oc2token 0.1.3 → 0.1.5
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/README.md +17 -17
- package/dist/src/application.js +91 -7
- package/dist/src/application.js.map +1 -1
- package/dist/src/cache/schema.js +8 -2
- package/dist/src/cache/schema.js.map +1 -1
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +220 -41
- package/dist/src/cli.js.map +1 -1
- package/dist/src/dashboard/render/ansi.d.ts +11 -0
- package/dist/src/dashboard/render/ansi.js +19 -0
- package/dist/src/dashboard/render/ansi.js.map +1 -1
- package/dist/src/dashboard/render/dashboard.d.ts +17 -0
- package/dist/src/dashboard/render/dashboard.js +268 -156
- package/dist/src/dashboard/render/dashboard.js.map +1 -1
- package/dist/src/dashboard/render/format.js +3 -3
- package/dist/src/dashboard/render/format.js.map +1 -1
- package/dist/src/dashboard/render/types.d.ts +4 -0
- package/dist/src/dashboard/render/types.js +12 -10
- package/dist/src/dashboard/render/types.js.map +1 -1
- package/dist/src/dashboard/settings/index.d.ts +4 -0
- package/dist/src/dashboard/settings/index.js +15 -2
- package/dist/src/dashboard/settings/index.js.map +1 -1
- package/dist/src/domain/contracts.d.ts +2 -0
- package/dist/src/domain/contracts.js.map +1 -1
- package/dist/src/domain/windows.d.ts +11 -5
- package/dist/src/domain/windows.js +24 -7
- package/dist/src/domain/windows.js.map +1 -1
- package/dist/src/opencode/stats.js +52 -7
- package/dist/src/opencode/stats.js.map +1 -1
- package/dist/src/output/json.d.ts +1 -1
- package/dist/src/output/json.js +1 -1
- package/dist/src/output/table.js +31 -15
- package/dist/src/output/table.js.map +1 -1
- package/dist/src/pricing/pricing.d.ts +2 -2
- package/dist/src/pricing/pricing.js +31 -5
- package/dist/src/pricing/pricing.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { ANSI, colorEnabled, emphasis, paint, renderInPlace, statusColor, themeCache, themeCacheUnderline, themeCost, themeCyan, themeIn, themeInUnderline, themeOrange, themeOut, themeOutUnderline, themePurple, } from "./ansi.js";
|
|
1
|
+
import { ANSI, colorEnabled, emphasis, paint, renderInPlace, statusColor, themeCache, themeCacheUnderline, themeCost, themeCyan, themeIn, themeInUnderline, themeOrange, themeOut, themeOutUnderline, themePurple, themeWhite, zebraRow, } from "./ansi.js";
|
|
2
2
|
import { coverageState, DASHBOARD_ROW_CAP, formatCost, formatStatusFooter, formatTokenBreakdown, formatTokenCount, safeIdentifier, safeLabel, stripAnsi, } from "./format.js";
|
|
3
3
|
import { allWindowKinds, normalizeDashboardSnapshot, } from "./types.js";
|
|
4
4
|
import { ALL_PROVIDER_KINDS } from "../settings/index.js";
|
|
5
5
|
const CARD_MIN_WIDTH = 27;
|
|
6
6
|
const WIDE_LAYOUT_MIN_WIDTH = 90;
|
|
7
|
+
export const APP_PADDING = 2;
|
|
8
|
+
export const GITHUB_URL = "https://github.com/kaanyaren/OC2Token";
|
|
9
|
+
// ANSI/OSC sequences are intentionally matched here so outer padding can
|
|
10
|
+
// truncate styled lines by visible width without removing their escapes.
|
|
11
|
+
// eslint-disable-next-line no-control-regex
|
|
12
|
+
const ANSI_SEQUENCE = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/y;
|
|
7
13
|
function lineWidth(value) {
|
|
8
14
|
return [...stripAnsi(value)].length;
|
|
9
15
|
}
|
|
@@ -23,51 +29,58 @@ function truncateExact(value, width) {
|
|
|
23
29
|
return value;
|
|
24
30
|
return [...value].slice(0, Math.max(0, width - 1)).join("") + "…";
|
|
25
31
|
}
|
|
32
|
+
/** Truncate visible terminal width without stripping ANSI styling. */
|
|
33
|
+
function truncateVisible(value, width) {
|
|
34
|
+
if (lineWidth(value) <= width)
|
|
35
|
+
return value;
|
|
36
|
+
const target = Math.max(0, width - 1);
|
|
37
|
+
let result = "";
|
|
38
|
+
let index = 0;
|
|
39
|
+
let visible = 0;
|
|
40
|
+
while (index < value.length && visible < target) {
|
|
41
|
+
if (value[index] === "\u001b") {
|
|
42
|
+
ANSI_SEQUENCE.lastIndex = index;
|
|
43
|
+
const escape = ANSI_SEQUENCE.exec(value);
|
|
44
|
+
if (escape?.index === index) {
|
|
45
|
+
result += escape[0];
|
|
46
|
+
index += escape[0].length;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const codePoint = value.codePointAt(index);
|
|
51
|
+
if (codePoint === undefined)
|
|
52
|
+
break;
|
|
53
|
+
const character = String.fromCodePoint(codePoint);
|
|
54
|
+
result += character;
|
|
55
|
+
index += character.length;
|
|
56
|
+
visible += 1;
|
|
57
|
+
}
|
|
58
|
+
return `${result}…${value.includes("\u001b") ? ANSI.reset : ""}`;
|
|
59
|
+
}
|
|
26
60
|
function colorizeBreakdown(value, enabled, underline = false) {
|
|
27
61
|
if (!enabled || value.length === 0)
|
|
28
62
|
return value;
|
|
29
|
-
// Breakdown arrives as
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
63
|
+
// Breakdown arrives as gap-separated tokens like "IN 100 OUT 20 CACHE 0".
|
|
64
|
+
// truncateExact can cut the string mid-token ("IN 100 OU…"), so color each
|
|
65
|
+
// segment from its label to the next multi-space gap (or end of string).
|
|
66
|
+
// Alternatives are longest-first with a trailing-letter guard so truncated
|
|
67
|
+
// fragments ("OU", "CAC") still match without false positives inside words.
|
|
68
|
+
// Legacy short forms ("I", "In", …) are kept as fallbacks.
|
|
69
|
+
const specs = [
|
|
70
|
+
{ labels: ["IN", "In", "I"], fn: underline ? themeInUnderline : themeIn },
|
|
71
|
+
{ labels: ["OUT", "Out", "OU", "O"], fn: underline ? themeOutUnderline : themeOut },
|
|
72
|
+
{ labels: ["CACHE", "Cache", "CACH", "CAC", "CA", "C"], fn: underline ? themeCacheUnderline : themeCache },
|
|
73
|
+
];
|
|
34
74
|
let result = value;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else {
|
|
45
|
-
result = result.replace(/\bOut\s+[^\s]+/, (match) => outFn(match, enabled));
|
|
46
|
-
}
|
|
47
|
-
const hasShortCache = /\bC\s+[^\s]+/.test(result);
|
|
48
|
-
const hasLongCache = /\bCache\s+[^\s]+/.test(result);
|
|
49
|
-
if (hasShortCache) {
|
|
50
|
-
result = result.replace(/\bC\s+[^\s]+/, (match) => cacheFn(match, enabled));
|
|
51
|
-
}
|
|
52
|
-
else if (hasLongCache) {
|
|
53
|
-
result = result.replace(/\bCache\s+[^\s]+/, (match) => cacheFn(match, enabled));
|
|
54
|
-
}
|
|
55
|
-
else if (result.includes("C")) {
|
|
56
|
-
// Truncated cache token (e.g. "I 100 O 20 C" or "Ca…")
|
|
57
|
-
const idxC = result.lastIndexOf(" C");
|
|
58
|
-
const idxCa = result.indexOf("Ca");
|
|
59
|
-
const index = idxC !== -1 ? idxC + 1 : idxCa;
|
|
60
|
-
if (index !== -1) {
|
|
61
|
-
const before = result.slice(0, index);
|
|
62
|
-
const tail = result.slice(index);
|
|
63
|
-
if (!tail.includes(ANSI.cyan) && !tail.includes(ANSI.orange) && !tail.includes(ANSI.green)) {
|
|
64
|
-
result = before + cacheFn(tail, enabled);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
else if (result.trimEnd().endsWith("C")) {
|
|
68
|
-
// Single trailing C without value (heavily truncated)
|
|
69
|
-
result = result.replace(/C\s*$/, (m) => cacheFn(m, enabled));
|
|
70
|
-
}
|
|
75
|
+
for (const spec of specs) {
|
|
76
|
+
const match = new RegExp(`\\b(?:${spec.labels.join("|")})(?![A-Za-z])`).exec(result);
|
|
77
|
+
if (!match || match.index === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
const start = match.index;
|
|
80
|
+
const tail = result.slice(start);
|
|
81
|
+
const gap = tail.search(/\s{2,}/);
|
|
82
|
+
const end = gap === -1 ? result.length : start + gap;
|
|
83
|
+
result = result.slice(0, start) + spec.fn(result.slice(start, end), enabled) + result.slice(end);
|
|
71
84
|
}
|
|
72
85
|
return result;
|
|
73
86
|
}
|
|
@@ -75,7 +88,7 @@ function border(width, left, fill, right) {
|
|
|
75
88
|
return left + fill.repeat(Math.max(0, width - 2)) + right;
|
|
76
89
|
}
|
|
77
90
|
function cardTitle(kind) {
|
|
78
|
-
return kind === "hour" ? "LAST 60 MINUTES" : kind === "day" ? "TODAY" : "THIS WEEK";
|
|
91
|
+
return kind === "hour" ? "LAST 60 MINUTES" : kind === "day" ? "TODAY" : kind === "week" ? "THIS WEEK" : "LAST MONTH";
|
|
79
92
|
}
|
|
80
93
|
function panelHeading(title, width, color, accent = "purple") {
|
|
81
94
|
const label = truncate(title, Math.max(4, width - 6));
|
|
@@ -96,42 +109,6 @@ function providerAccent(name, color, bright = false) {
|
|
|
96
109
|
return (value) => themeCyan(value, color, bright);
|
|
97
110
|
return (value) => themePurple(value, color, bright);
|
|
98
111
|
}
|
|
99
|
-
function renderProviderStack(providers, color, width) {
|
|
100
|
-
if (providers.length === 0)
|
|
101
|
-
return [];
|
|
102
|
-
const total = providers.reduce((sum, entry) => sum + entry.totals.recorded_total, 0);
|
|
103
|
-
const plainEntries = providers.slice(0, 3).map((entry) => {
|
|
104
|
-
const name = safeIdentifier(entry.name);
|
|
105
|
-
const count = formatTokenCount(entry.totals.recorded_total);
|
|
106
|
-
const pct = total > 0 ? Math.round((entry.totals.recorded_total / total) * 100) : 0;
|
|
107
|
-
return `${name} ${count} (${pct}%)`;
|
|
108
|
-
});
|
|
109
|
-
const coloredEntries = plainEntries.map((text, index) => {
|
|
110
|
-
const safe = truncate(text, Math.max(4, width - 4));
|
|
111
|
-
if (index === 0)
|
|
112
|
-
return themePurple(safe, color, true);
|
|
113
|
-
if (index === 1)
|
|
114
|
-
return themeOrange(safe, color, true);
|
|
115
|
-
return themeCyan(safe, color, true);
|
|
116
|
-
});
|
|
117
|
-
if (width < 78) {
|
|
118
|
-
const lines = [panelHeading("Providers", width, color, "orange")];
|
|
119
|
-
for (const part of coloredEntries)
|
|
120
|
-
lines.push(` ${part}`);
|
|
121
|
-
if (providers.length > 3)
|
|
122
|
-
lines.push(` +${providers.length - 3} more`);
|
|
123
|
-
return lines;
|
|
124
|
-
}
|
|
125
|
-
const heading = themePurple("Providers", color, true);
|
|
126
|
-
const joined = coloredEntries.join(themePurple(" · ", color));
|
|
127
|
-
const extra = providers.length > 3 ? themePurple(` · +${providers.length - 3} more`, color) : "";
|
|
128
|
-
const line = `${heading} · ${joined}${extra}`;
|
|
129
|
-
// Ensure the inline stack never exceeds terminal width
|
|
130
|
-
if ([...stripAnsi(line)].length > width) {
|
|
131
|
-
return [truncate(line, width)];
|
|
132
|
-
}
|
|
133
|
-
return [line];
|
|
134
|
-
}
|
|
135
112
|
function cardLines(kind, window, width, color, selected) {
|
|
136
113
|
const inner = Math.max(1, width - 2);
|
|
137
114
|
const borderColor = selected ? ANSI.orange : ANSI.purpleDeep;
|
|
@@ -156,13 +133,10 @@ function cardLines(kind, window, width, color, selected) {
|
|
|
156
133
|
const costLine = `${themePurple(costLabel, color, true)} ${themeCost(costValue, color)}`;
|
|
157
134
|
const lines = [
|
|
158
135
|
paint(border(width, "╭", "─", "╮"), borderColor, color),
|
|
159
|
-
frame(
|
|
160
|
-
? themeOrange(truncate(title, inner), color, true)
|
|
161
|
-
: themePurple(truncate(title, inner), color, true)),
|
|
136
|
+
frame(emphasis(themeWhite(truncate(title, inner), color, true), color)),
|
|
162
137
|
frame(first),
|
|
163
138
|
frame(colorizeBreakdown(truncateExact(displayComponents, inner), color, true)),
|
|
164
139
|
frame(pad(costLine, inner)),
|
|
165
|
-
frame(themeOrange(truncate(`Range ${safeLabel(window.window.label)}`, inner), color)),
|
|
166
140
|
paint(border(width, "╰", "─", "╯"), borderColor, color),
|
|
167
141
|
];
|
|
168
142
|
return lines;
|
|
@@ -172,29 +146,41 @@ function composeCards(cards, gap = 2) {
|
|
|
172
146
|
const widths = cards.map((card) => lineWidth(card[0] ?? ""));
|
|
173
147
|
return Array.from({ length: maxLines }, (_, row) => cards.map((card, index) => pad(card[row] ?? "", widths[index])).join(" ".repeat(gap)).trimEnd());
|
|
174
148
|
}
|
|
175
|
-
const HEADER_LINES =
|
|
176
|
-
const CARD_LINES =
|
|
149
|
+
const HEADER_LINES = 3;
|
|
150
|
+
const CARD_LINES = 6;
|
|
177
151
|
const CARD_GAP = 2;
|
|
152
|
+
const FOUR_CARD_LAYOUT_MIN_WIDTH = CARD_MIN_WIDTH * 4 + CARD_GAP * 3;
|
|
178
153
|
export function getCardHitRegions(width) {
|
|
179
154
|
const clamped = Math.max(20, width);
|
|
155
|
+
const contentWidth = Math.max(1, clamped - APP_PADDING * 2);
|
|
180
156
|
const headerHeight = HEADER_LINES;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const cardWidth = Math.max(CARD_MIN_WIDTH, Math.floor((clamped - 4) / 3));
|
|
157
|
+
if (contentWidth >= FOUR_CARD_LAYOUT_MIN_WIDTH) {
|
|
158
|
+
const cardWidth = Math.max(CARD_MIN_WIDTH, Math.floor((contentWidth - CARD_GAP * 3) / 4));
|
|
184
159
|
const y1 = headerHeight + 1;
|
|
185
160
|
const y2 = y1 + CARD_LINES - 1;
|
|
186
161
|
return allWindowKinds().map((kind, idx) => {
|
|
187
|
-
const x1 = idx * (cardWidth + CARD_GAP) + 1;
|
|
162
|
+
const x1 = APP_PADDING + idx * (cardWidth + CARD_GAP) + 1;
|
|
188
163
|
const x2 = x1 + cardWidth - 1;
|
|
189
164
|
return { kind, x1, y1, x2, y2 };
|
|
190
165
|
});
|
|
191
166
|
}
|
|
192
|
-
|
|
193
|
-
|
|
167
|
+
if (contentWidth >= WIDE_LAYOUT_MIN_WIDTH) {
|
|
168
|
+
const cardWidth = Math.max(CARD_MIN_WIDTH, Math.floor((contentWidth - CARD_GAP) / 2));
|
|
169
|
+
const yGap = 0; // stacked rows touch — no blank separator (see renderDashboard)
|
|
170
|
+
return allWindowKinds().map((kind, idx) => {
|
|
171
|
+
const column = idx % 2;
|
|
172
|
+
const row = Math.floor(idx / 2);
|
|
173
|
+
const x1 = APP_PADDING + column * (cardWidth + CARD_GAP) + 1;
|
|
174
|
+
const y1 = headerHeight + 1 + row * (CARD_LINES + yGap);
|
|
175
|
+
return { kind, x1, y1, x2: x1 + cardWidth - 1, y2: y1 + CARD_LINES - 1 };
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const cardWidth = Math.max(12, Math.min(CARD_MIN_WIDTH, contentWidth - 2));
|
|
179
|
+
const yGap = 0; // stacked cards touch — no blank separator (see renderDashboard)
|
|
194
180
|
return allWindowKinds().map((kind, idx) => {
|
|
195
181
|
const y1 = headerHeight + 1 + idx * (CARD_LINES + yGap);
|
|
196
182
|
const y2 = y1 + CARD_LINES - 1;
|
|
197
|
-
const x1 = 1;
|
|
183
|
+
const x1 = APP_PADDING + 1;
|
|
198
184
|
const x2 = x1 + cardWidth - 1;
|
|
199
185
|
return { kind, x1, y1, x2, y2 };
|
|
200
186
|
});
|
|
@@ -205,31 +191,43 @@ function renderHeader(color, width) {
|
|
|
205
191
|
? `${themeOrange("◈", color, true)} ${emphasis(themePurple("OC2", color, true), color)} ${themePurple("// TOKENS", color)}`
|
|
206
192
|
: `${themeOrange("◈", color, true)} ${emphasis(themePurple("OC2TOKEN", color, true), color)} ${themePurple("// USAGE CONSOLE", color)}`;
|
|
207
193
|
const title = compact ? "OC2Token Usage" : "OpenCode 2 Token Usage";
|
|
194
|
+
// No trailing blank line: the top cards start directly under the rule to
|
|
195
|
+
// keep vertical spacing tight (HEADER_LINES must match this length).
|
|
208
196
|
return [
|
|
209
197
|
identity,
|
|
210
198
|
emphasis(themePurple(truncate(title, width), color, true), color),
|
|
211
|
-
themePurple("─".repeat(
|
|
212
|
-
"",
|
|
199
|
+
themePurple("─".repeat(width), color),
|
|
213
200
|
];
|
|
214
201
|
}
|
|
215
202
|
/**
|
|
216
203
|
* Single canonical TTY status footer. Uses the shared formatStatusFooter
|
|
217
204
|
* helper (`Status: <STATE · sessions…>` once) — do not duplicate the coverage
|
|
218
|
-
* string as `Status: X · Coverage: X`. Colored by coverageState
|
|
219
|
-
*
|
|
205
|
+
* string as `Status: X · Coverage: X`. Colored by coverageState, centered
|
|
206
|
+
* horizontally inside a box. Truncated to preserve redraw geometry.
|
|
220
207
|
*/
|
|
221
|
-
function
|
|
208
|
+
function renderStatusBox(snapshot, color, width) {
|
|
222
209
|
const state = coverageState(snapshot.coverage, snapshot.stale);
|
|
223
|
-
|
|
210
|
+
const maxContent = Math.max(1, width - 4);
|
|
211
|
+
const plain = truncate(formatStatusFooter(snapshot.coverage, snapshot.stale), maxContent);
|
|
212
|
+
const contentLen = [...plain].length;
|
|
213
|
+
const boxWidth = Math.min(width, contentLen + 4);
|
|
214
|
+
const leftPad = Math.max(0, Math.floor((width - boxWidth) / 2));
|
|
215
|
+
const prefix = " ".repeat(leftPad);
|
|
216
|
+
const colored = statusColor(plain, state, color);
|
|
217
|
+
return [
|
|
218
|
+
prefix + paint(border(boxWidth, "╭", "─", "╮"), ANSI.purpleDeep, color),
|
|
219
|
+
prefix + paint("│", ANSI.purpleDeep, color) + ` ${colored} ` + paint("│", ANSI.purpleDeep, color),
|
|
220
|
+
prefix + paint(border(boxWidth, "╰", "─", "╯"), ANSI.purpleDeep, color),
|
|
221
|
+
];
|
|
224
222
|
}
|
|
225
223
|
function renderTrend(snapshot, selected, color, width) {
|
|
226
224
|
const trend = snapshot.windows[selected].trends;
|
|
227
225
|
const title = `Trend · ${cardTitle(selected).toLowerCase()}`;
|
|
228
226
|
if (trend.length === 0)
|
|
229
|
-
return [panelHeading(title, width, color), " No trend data recorded."];
|
|
227
|
+
return [panelHeading(title, width, color), truncate(" No trend data recorded.", width)];
|
|
230
228
|
const max = Math.max(...trend.map((bucket) => bucket.totals.recorded_total), 0);
|
|
231
229
|
if (max === 0)
|
|
232
|
-
return [panelHeading(title, width, color), " No trend data recorded."];
|
|
230
|
+
return [panelHeading(title, width, color), truncate(" No trend data recorded.", width)];
|
|
233
231
|
const graphWidth = Math.max(8, width - 4);
|
|
234
232
|
const points = trend.length <= graphWidth
|
|
235
233
|
? Array.from({ length: graphWidth }, (_, index) => {
|
|
@@ -297,7 +295,21 @@ function graphLabels(points, width) {
|
|
|
297
295
|
}
|
|
298
296
|
return characters.join("").trimEnd();
|
|
299
297
|
}
|
|
300
|
-
function
|
|
298
|
+
function breakdownColumnWidths(values) {
|
|
299
|
+
const recorded = values.map((item) => formatTokenCount(item.totals.recorded_total));
|
|
300
|
+
const input = values.map((item) => formatTokenCount(item.totals.input));
|
|
301
|
+
const output = values.map((item) => formatTokenCount(item.totals.output + item.totals.reasoning));
|
|
302
|
+
const cache = values.map((item) => formatTokenCount(item.totals.cacheRead + item.totals.cacheWrite));
|
|
303
|
+
const cost = values.map((item) => formatCost(item.cost));
|
|
304
|
+
return {
|
|
305
|
+
recorded: Math.max("RECORDED".length, ...recorded.map((value) => [...value].length)),
|
|
306
|
+
input: Math.max("In".length, ...input.map((value) => [...value].length)),
|
|
307
|
+
output: Math.max("Out".length, ...output.map((value) => [...value].length)),
|
|
308
|
+
cache: Math.max("Cache".length, ...cache.map((value) => [...value].length)),
|
|
309
|
+
cost: Math.max("Cost".length, ...cost.map((value) => [...value].length)),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function renderBreakdown(title, values, color, width, accent, columnWidths) {
|
|
301
313
|
// Token columns intentionally merge sub-components for TTY width:
|
|
302
314
|
// Out = output + reasoning, Cache = cacheRead + cacheWrite (same as
|
|
303
315
|
// formatTokenBreakdown and the table view). Raw splits remain available in
|
|
@@ -311,23 +323,22 @@ function renderBreakdown(title, values, color, width, accent) {
|
|
|
311
323
|
}
|
|
312
324
|
const visible = values.slice(0, DASHBOARD_ROW_CAP);
|
|
313
325
|
const isProvider = title.toLowerCase().includes("provider");
|
|
314
|
-
const nameWidth = Math.max(8, Math.min(42, Math.floor(width * 0.42)));
|
|
315
326
|
const showTokenDetails = width >= 70;
|
|
316
327
|
const showCost = width >= 32;
|
|
328
|
+
// Keep the complete numeric block against the right edge of the terminal.
|
|
329
|
+
// The name column gets the remaining space instead of using a fixed 42%
|
|
330
|
+
// width, which previously left all numeric columns beside the names.
|
|
331
|
+
const numericWidth = columnWidths.recorded
|
|
332
|
+
+ (showTokenDetails ? columnWidths.input + columnWidths.output + columnWidths.cache : 0)
|
|
333
|
+
+ (showCost ? columnWidths.cost : 0);
|
|
334
|
+
const separatorWidth = showTokenDetails ? 9 : showCost ? 5 : 3;
|
|
335
|
+
const nameWidth = Math.max(1, width - numericWidth - separatorWidth);
|
|
317
336
|
// Table-aligned In/Out/Cache/Cost columns: header already shows labels, rows show only numbers.
|
|
318
|
-
const inStrs = visible.map((item) => formatTokenCount(item.totals.input));
|
|
319
|
-
const outStrs = visible.map((item) => formatTokenCount(item.totals.output + item.totals.reasoning));
|
|
320
|
-
const cacheStrs = visible.map((item) => formatTokenCount(item.totals.cacheRead + item.totals.cacheWrite));
|
|
321
|
-
const costStrs = visible.map((item) => formatCost(item.cost));
|
|
322
|
-
const inWidth = Math.max("In".length, ...inStrs.map((s) => [...s].length));
|
|
323
|
-
const outWidth = Math.max("Out".length, ...outStrs.map((s) => [...s].length));
|
|
324
|
-
const cacheWidth = Math.max("Cache".length, ...cacheStrs.map((s) => [...s].length));
|
|
325
|
-
const costWidth = Math.max("Cost".length, ...costStrs.map((s) => [...s].length));
|
|
326
337
|
// Header uses same column widths so values line up vertically (right-aligned numbers).
|
|
327
|
-
const headerIn = themeIn("In".padStart(
|
|
328
|
-
const headerOut = themeOut("Out".padStart(
|
|
329
|
-
const headerCache = themeCache("Cache".padStart(
|
|
330
|
-
const headerCost = themeCost("Cost".padStart(
|
|
338
|
+
const headerIn = themeIn("In".padStart(columnWidths.input), color);
|
|
339
|
+
const headerOut = themeOut("Out".padStart(columnWidths.output), color);
|
|
340
|
+
const headerCache = themeCache("Cache".padStart(columnWidths.cache), color);
|
|
341
|
+
const headerCost = themeCost("Cost".padStart(columnWidths.cost), color);
|
|
331
342
|
const details = showTokenDetails
|
|
332
343
|
? showCost
|
|
333
344
|
? ` ${headerIn} ${headerOut} ${headerCache} ${headerCost}`
|
|
@@ -335,21 +346,21 @@ function renderBreakdown(title, values, color, width, accent) {
|
|
|
335
346
|
: showCost
|
|
336
347
|
? ` ${headerCost}`
|
|
337
348
|
: "";
|
|
338
|
-
lines.push(` ${themePurple("NAME".padEnd(nameWidth), color, true)} ${themeOrange("RECORDED".padStart(
|
|
339
|
-
for (const item of visible) {
|
|
349
|
+
lines.push(` ${themePurple("NAME".padEnd(nameWidth), color, true)} ${themeOrange("RECORDED".padStart(columnWidths.recorded), color, true)}${details}`);
|
|
350
|
+
for (const [rowIndex, item] of visible.entries()) {
|
|
340
351
|
const safeProv = item.provider ? safeIdentifier(item.provider) : "";
|
|
341
352
|
const safeNm = safeIdentifier(item.name);
|
|
342
353
|
const rawName = safeProv && safeProv !== safeNm ? `${safeProv}/${safeNm}` : safeNm;
|
|
343
354
|
const safeName = truncate(rawName, nameWidth);
|
|
344
|
-
const countText = formatTokenCount(item.totals.recorded_total).padStart(
|
|
355
|
+
const countText = formatTokenCount(item.totals.recorded_total).padStart(columnWidths.recorded);
|
|
345
356
|
const inPlain = formatTokenCount(item.totals.input);
|
|
346
357
|
const outPlain = formatTokenCount(item.totals.output + item.totals.reasoning);
|
|
347
358
|
const cachePlain = formatTokenCount(item.totals.cacheRead + item.totals.cacheWrite);
|
|
348
359
|
const costPlain = formatCost(item.cost);
|
|
349
|
-
const inCol = themeIn(inPlain.padStart(
|
|
350
|
-
const outCol = themeOut(outPlain.padStart(
|
|
351
|
-
const cacheCol = themeCache(cachePlain.padStart(
|
|
352
|
-
const costCol = themeCost(costPlain.padStart(
|
|
360
|
+
const inCol = themeIn(inPlain.padStart(columnWidths.input), color);
|
|
361
|
+
const outCol = themeOut(outPlain.padStart(columnWidths.output), color);
|
|
362
|
+
const cacheCol = themeCache(cachePlain.padStart(columnWidths.cache), color);
|
|
363
|
+
const costCol = themeCost(costPlain.padStart(columnWidths.cost), color);
|
|
353
364
|
const componentDetails = showTokenDetails
|
|
354
365
|
? showCost
|
|
355
366
|
? ` ${inCol} ${outCol} ${cacheCol} ${costCol}`
|
|
@@ -359,13 +370,13 @@ function renderBreakdown(title, values, color, width, accent) {
|
|
|
359
370
|
: "";
|
|
360
371
|
if (isProvider) {
|
|
361
372
|
const accentForRow = providerAccent(item.name, color, true);
|
|
362
|
-
const countColored = accentForRow(countText
|
|
373
|
+
const countColored = accentForRow(countText);
|
|
363
374
|
// Keep provider name in its accent but retain table alignment for token columns.
|
|
364
375
|
const nameColored = accentForRow(safeName.padEnd(nameWidth));
|
|
365
|
-
lines.push(` ${nameColored} ${countColored}${componentDetails}
|
|
376
|
+
lines.push(zebraRow(` ${nameColored} ${countColored}${componentDetails}`, rowIndex % 2 === 1, color));
|
|
366
377
|
}
|
|
367
378
|
else {
|
|
368
|
-
lines.push(` ${
|
|
379
|
+
lines.push(zebraRow(` ${themeWhite(safeName.padEnd(nameWidth), color)} ${themeOrange(countText, color)}${componentDetails}`, rowIndex % 2 === 1, color));
|
|
369
380
|
}
|
|
370
381
|
}
|
|
371
382
|
if (values.length > visible.length) {
|
|
@@ -395,8 +406,9 @@ function formatRefreshInterval(seconds) {
|
|
|
395
406
|
return `${s}s`;
|
|
396
407
|
return `${m}m ${s}s`;
|
|
397
408
|
}
|
|
409
|
+
/** Refresh interval presets (seconds). Shared by the settings panel and mouse clicks. */
|
|
410
|
+
export const REFRESH_PRESETS = [60, 120, 300, 600, 900, 1800, 3600, 7200, 14400];
|
|
398
411
|
function renderSettingsPanel(settings, width, color) {
|
|
399
|
-
const REFRESH_PRESETS = [60, 120, 300, 600, 900, 1800, 3600, 7200, 14400];
|
|
400
412
|
function presetIndex(value) {
|
|
401
413
|
const clamped = Math.max(60, Math.min(14400, Math.floor(value)));
|
|
402
414
|
let best = 0;
|
|
@@ -452,8 +464,30 @@ function renderSettingsPanel(settings, width, color) {
|
|
|
452
464
|
lines.push(padPrefix + `│${pad(full, inner)}│`);
|
|
453
465
|
}
|
|
454
466
|
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
467
|
+
// Breakdown tables heading (focused indices 3-4; interval moves to 5)
|
|
468
|
+
const tablesHeading = truncate("Tables — space to toggle", inner);
|
|
469
|
+
lines.push(padPrefix + `│${pad(themePurple(tablesHeading, color, true), inner)}│`);
|
|
470
|
+
const tableRows = [
|
|
471
|
+
{ label: "Providers table", shown: settings.showProvidersTable !== false, index: 3 },
|
|
472
|
+
{ label: "Projects table", shown: settings.showProjectsTable !== false, index: 4 },
|
|
473
|
+
];
|
|
474
|
+
for (const row of tableRows) {
|
|
475
|
+
const focused = settings.focusedIndex === row.index;
|
|
476
|
+
const checkbox = row.shown ? "◉" : "○";
|
|
477
|
+
const prefix = focused ? themeOrange("▶ ", color, true) : " ";
|
|
478
|
+
const checkColored = row.shown ? themeOrange(checkbox, color, true) : themePurple(checkbox, color);
|
|
479
|
+
const suffix = focused ? " ← toggle" : "";
|
|
480
|
+
const suffixLen = focused ? 10 : 0; // " ← toggle" visible length
|
|
481
|
+
const nameAvail = Math.max(4, inner - 4 - suffixLen);
|
|
482
|
+
const nameTrunc = truncate(row.label, nameAvail);
|
|
483
|
+
const nameColored = row.shown ? themeWhite(nameTrunc, color, true) : themePurple(nameTrunc, color);
|
|
484
|
+
const lineContent = `${prefix}${checkColored} ${nameColored}`;
|
|
485
|
+
const full = focused ? `${lineContent}${themeOrange(suffix, color)}` : lineContent;
|
|
486
|
+
lines.push(padPrefix + `│${pad(full, inner)}│`);
|
|
487
|
+
}
|
|
488
|
+
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
455
489
|
// Refresh interval heading with value
|
|
456
|
-
const intervalFocused = settings.focusedIndex ===
|
|
490
|
+
const intervalFocused = settings.focusedIndex === 5;
|
|
457
491
|
const intervalHeadingPlain = "Refresh interval";
|
|
458
492
|
const headingAvail = Math.max(6, inner - intervalStr.length - 5 - (intervalFocused ? 2 : 0));
|
|
459
493
|
const headingTrunc = truncate(intervalHeadingPlain, headingAvail);
|
|
@@ -538,7 +572,7 @@ function renderProjectsPanel(snapshot, selected, width, color) {
|
|
|
538
572
|
const inner = Math.max(1, panelWidth - 2);
|
|
539
573
|
const leftPad = Math.max(0, Math.floor((width - panelWidth) / 2));
|
|
540
574
|
const padPrefix = " ".repeat(leftPad);
|
|
541
|
-
const titleKind = selected === "hour" ? "Last 60 Minutes" : selected === "day" ? "Today" : "This Week";
|
|
575
|
+
const titleKind = selected === "hour" ? "Last 60 Minutes" : selected === "day" ? "Today" : selected === "week" ? "This Week" : "Last Month";
|
|
542
576
|
const totalForWindow = snapshot.windows[selected]?.totals.recorded_total ?? 0;
|
|
543
577
|
const lines = [];
|
|
544
578
|
lines.push(padPrefix + paint(border(panelWidth, "╭", "─", "╮"), ANSI.purpleDeep, color));
|
|
@@ -639,12 +673,12 @@ function renderProjectsPanel(snapshot, selected, width, color) {
|
|
|
639
673
|
lines.push(padPrefix + `│${pad(themePurple(truncate(totalLine, inner), color), inner)}│`);
|
|
640
674
|
}
|
|
641
675
|
lines.push(padPrefix + `│${" ".repeat(inner)}│`);
|
|
642
|
-
lines.push(padPrefix + `│${pad(themePurple(truncate("p / esc to close • 1/2/3 switch period", inner), color), inner)}│`);
|
|
676
|
+
lines.push(padPrefix + `│${pad(themePurple(truncate("p / esc to close • 1/2/3/4 switch period", inner), color), inner)}│`);
|
|
643
677
|
lines.push(padPrefix + paint(border(panelWidth, "╰", "─", "╯"), ANSI.purpleDeep, color));
|
|
644
678
|
return lines;
|
|
645
679
|
}
|
|
646
680
|
function renderCredit(width, color) {
|
|
647
|
-
const repo =
|
|
681
|
+
const repo = GITHUB_URL;
|
|
648
682
|
const full = `Kaan Yaren · ${repo}`;
|
|
649
683
|
const plain = width < 45 ? "Kaan Yaren" : full;
|
|
650
684
|
const truncated = truncate(plain, width);
|
|
@@ -667,7 +701,7 @@ function renderFooter(help, color, width) {
|
|
|
667
701
|
return [
|
|
668
702
|
"",
|
|
669
703
|
panelHeading("Help", width, color, "orange"),
|
|
670
|
-
` ${key("r")} refresh ${key("1/2/3")} periods ${key("p")} projects`,
|
|
704
|
+
` ${key("r")} refresh ${key("1/2/3/4")} periods ${key("p")} projects`,
|
|
671
705
|
` ${key("q")} quit ${key("?")} close`,
|
|
672
706
|
];
|
|
673
707
|
}
|
|
@@ -675,65 +709,140 @@ function renderFooter(help, color, width) {
|
|
|
675
709
|
return [
|
|
676
710
|
"",
|
|
677
711
|
panelHeading("Help", width, color, "orange"),
|
|
678
|
-
` ${key("r/R")} Refresh ${key("1/2/3")} Periods ${key("p")} Projects`,
|
|
712
|
+
` ${key("r/R")} Refresh ${key("1/2/3/4")} Periods ${key("p")} Projects`,
|
|
679
713
|
` ${key("q")} Quit ${key("?")} Toggle help ${key("s")} Settings`,
|
|
680
714
|
];
|
|
681
715
|
}
|
|
682
716
|
return [
|
|
683
717
|
"",
|
|
684
718
|
panelHeading("Help", width, color, "orange"),
|
|
685
|
-
` ${key("r/R")} Refresh now ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("Tab/Arrows")} Navigate`,
|
|
719
|
+
` ${key("r/R")} Refresh now ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("4")} Month ${key("Tab/Arrows")} Navigate`,
|
|
686
720
|
` ${key("p")} Projects ${key("s")} Settings ${key("q")} Quit ${key("?")} Toggle help`,
|
|
687
721
|
];
|
|
688
722
|
}
|
|
689
723
|
if (width < 50) {
|
|
690
|
-
return [` ${key("r")} ${key("1/2/3")} ${key("p")} ${key("s")} ${key("q")} ${key("?")}`];
|
|
724
|
+
return [` ${key("r")} ${key("1/2/3/4")} ${key("p")} ${key("s")} ${key("q")} ${key("?")}`];
|
|
691
725
|
}
|
|
692
726
|
if (width < 70) {
|
|
693
727
|
// Keep footer under 50 chars so width=50 still truncates correctly
|
|
694
728
|
if (width < 60) {
|
|
695
|
-
return [` ${key("r")} Refresh
|
|
729
|
+
return [` ${key("r")} Refresh ${key("1/2/3/4")} ${key("p")} Proj ${key("s")} ${key("q")} Quit ${key("?")} Help`];
|
|
696
730
|
}
|
|
697
|
-
return [` ${key("r")} Refresh
|
|
731
|
+
return [` ${key("r")} Refresh ${key("1/2/3/4")} Periods ${key("p")} Proj ${key("s")} ${key("q")} Quit ${key("?")} Help`];
|
|
732
|
+
}
|
|
733
|
+
if (width < 78) {
|
|
734
|
+
return [` ${key("r")} ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("4")} Month ${key("p")} ${key("s")} ${key("q")} ${key("?")}`];
|
|
735
|
+
}
|
|
736
|
+
if (width < 90) {
|
|
737
|
+
return [` ${key("r")} ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("4")} Month ${key("p")} Proj ${key("s")} Settings ${key("q")} Quit ${key("?")} Help`];
|
|
738
|
+
}
|
|
739
|
+
if (width < 120) {
|
|
740
|
+
return [
|
|
741
|
+
` ${key("r")} Refresh ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("4")} Month ${key("p")} Projects ${key("s")} Settings ${key("q")} Quit ${key("?")} Help`,
|
|
742
|
+
];
|
|
698
743
|
}
|
|
699
744
|
return [
|
|
700
745
|
"",
|
|
701
|
-
` ${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`,
|
|
746
|
+
` ${key("r")} Refresh ${key("1")} Hour ${key("2")} Today ${key("3")} Week ${key("4")} Month ${key("Tab/Arrows")} Navigate ${key("p")} Projects ${key("s")} Settings ${key("q")} Quit ${key("?")} Help`,
|
|
702
747
|
];
|
|
703
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* Map a footer token (whitespace-delimited word under the cursor) to its
|
|
751
|
+
* dashboard action. Returns undefined for decorative tokens ("Periods",
|
|
752
|
+
* "Navigate", "1/2/3/4", …). Pure helper for mouse click handling.
|
|
753
|
+
*/
|
|
754
|
+
export function footerClickAction(token) {
|
|
755
|
+
switch (token) {
|
|
756
|
+
case "r":
|
|
757
|
+
case "Refresh":
|
|
758
|
+
return "refresh";
|
|
759
|
+
case "1":
|
|
760
|
+
case "Hour":
|
|
761
|
+
return "hour";
|
|
762
|
+
case "2":
|
|
763
|
+
case "Today":
|
|
764
|
+
return "day";
|
|
765
|
+
case "3":
|
|
766
|
+
case "Week":
|
|
767
|
+
return "week";
|
|
768
|
+
case "4":
|
|
769
|
+
case "Month":
|
|
770
|
+
return "month";
|
|
771
|
+
case "p":
|
|
772
|
+
case "Proj":
|
|
773
|
+
case "Projects":
|
|
774
|
+
return "projects";
|
|
775
|
+
case "s":
|
|
776
|
+
case "Settings":
|
|
777
|
+
return "settings";
|
|
778
|
+
case "q":
|
|
779
|
+
case "Quit":
|
|
780
|
+
return "quit";
|
|
781
|
+
case "?":
|
|
782
|
+
case "Help":
|
|
783
|
+
return "help";
|
|
784
|
+
default:
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Find the whitespace-delimited token under 1-based column `cx` in an
|
|
790
|
+
* already ANSI-stripped line. Returns undefined when the column is blank.
|
|
791
|
+
*/
|
|
792
|
+
export function footerTokenAtX(line, cx) {
|
|
793
|
+
const re = /\S+/g;
|
|
794
|
+
let match;
|
|
795
|
+
while ((match = re.exec(line)) !== null) {
|
|
796
|
+
const start = match.index + 1;
|
|
797
|
+
if (cx >= start && cx < start + match[0].length)
|
|
798
|
+
return match[0];
|
|
799
|
+
}
|
|
800
|
+
return undefined;
|
|
801
|
+
}
|
|
704
802
|
export function renderDashboard(input, options = {}) {
|
|
705
803
|
const snapshot = normalizeDashboardSnapshot(input);
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
const
|
|
804
|
+
// Reserve two columns on each side for a consistent application gutter.
|
|
805
|
+
// Content is rendered at the inner width so borders, tables, and overlays
|
|
806
|
+
// all share the same outer padding without relying on terminal wrapping.
|
|
807
|
+
const terminalWidth = Math.max(20, options.width ?? 100);
|
|
808
|
+
const width = Math.max(1, terminalWidth - APP_PADDING * 2);
|
|
710
809
|
const color = colorEnabled(options);
|
|
711
810
|
const selected = options.selectedWindow ?? "day";
|
|
712
811
|
const lines = renderHeader(color, width);
|
|
713
|
-
const cardWidth = width >=
|
|
714
|
-
? Math.max(CARD_MIN_WIDTH, Math.floor((width -
|
|
715
|
-
:
|
|
812
|
+
const cardWidth = width >= FOUR_CARD_LAYOUT_MIN_WIDTH
|
|
813
|
+
? Math.max(CARD_MIN_WIDTH, Math.floor((width - CARD_GAP * 3) / 4))
|
|
814
|
+
: width >= WIDE_LAYOUT_MIN_WIDTH
|
|
815
|
+
? Math.max(CARD_MIN_WIDTH, Math.floor((width - CARD_GAP) / 2))
|
|
816
|
+
: Math.max(12, Math.min(CARD_MIN_WIDTH, width - 2));
|
|
716
817
|
const cards = allWindowKinds().map((kind) => cardLines(kind, snapshot.windows[kind], cardWidth, color, selected === kind));
|
|
717
|
-
if (width >=
|
|
818
|
+
if (width >= FOUR_CARD_LAYOUT_MIN_WIDTH) {
|
|
718
819
|
lines.push(...composeCards(cards));
|
|
719
820
|
}
|
|
821
|
+
else if (width >= WIDE_LAYOUT_MIN_WIDTH) {
|
|
822
|
+
for (let index = 0; index < cards.length; index += 2) {
|
|
823
|
+
lines.push(...composeCards(cards.slice(index, index + 2)));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
720
826
|
else {
|
|
721
827
|
for (const card of cards)
|
|
722
|
-
lines.push(...card
|
|
723
|
-
}
|
|
724
|
-
// Below cards render unified provider stack - cards are unified totals, stack shows per-provider split with 3 accents
|
|
725
|
-
const stackProviders = snapshot.windows[selected]?.providers ?? snapshot.providers;
|
|
726
|
-
if (stackProviders.length > 0) {
|
|
727
|
-
lines.push("");
|
|
728
|
-
lines.push(...renderProviderStack(stackProviders, color, width));
|
|
828
|
+
lines.push(...card);
|
|
729
829
|
}
|
|
730
830
|
lines.push("");
|
|
731
831
|
lines.push(...renderTrend(snapshot, selected, color, width));
|
|
732
832
|
lines.push("");
|
|
733
833
|
const selectedWindow = snapshot.windows[selected];
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
834
|
+
const breakdownWidths = breakdownColumnWidths([
|
|
835
|
+
...selectedWindow.models,
|
|
836
|
+
...selectedWindow.providers,
|
|
837
|
+
...selectedWindow.projects,
|
|
838
|
+
]);
|
|
839
|
+
lines.push(...renderBreakdown(`Models · ${cardTitle(selected)}`, selectedWindow.models, color, width, "purple", breakdownWidths));
|
|
840
|
+
if (options.settings?.showProvidersTable !== false) {
|
|
841
|
+
lines.push(...renderBreakdown(`Providers · ${cardTitle(selected)}`, selectedWindow.providers, color, width, "orange", breakdownWidths));
|
|
842
|
+
}
|
|
843
|
+
if (options.settings?.showProjectsTable !== false) {
|
|
844
|
+
lines.push(...renderBreakdown(`Projects · ${cardTitle(selected)}`, selectedWindow.projects, color, width, "cyan", breakdownWidths));
|
|
845
|
+
}
|
|
737
846
|
if (snapshot.coverage.errors.length > 0) {
|
|
738
847
|
lines.push("");
|
|
739
848
|
lines.push(emphasis("Coverage errors", color));
|
|
@@ -743,7 +852,7 @@ export function renderDashboard(input, options = {}) {
|
|
|
743
852
|
lines.push(` ${label}${error.sessionID ? ` (${safeIdentifier(error.sessionID)})` : ""}`);
|
|
744
853
|
}
|
|
745
854
|
}
|
|
746
|
-
lines.push(
|
|
855
|
+
lines.push(...renderStatusBox(snapshot, color, width));
|
|
747
856
|
lines.push(...renderFooter(options.help === true, color, width));
|
|
748
857
|
if (options.settings?.visible === true) {
|
|
749
858
|
lines.push("");
|
|
@@ -755,7 +864,10 @@ export function renderDashboard(input, options = {}) {
|
|
|
755
864
|
}
|
|
756
865
|
lines.push("");
|
|
757
866
|
lines.push(renderCredit(width, color));
|
|
758
|
-
const
|
|
867
|
+
const outerPadding = " ".repeat(APP_PADDING);
|
|
868
|
+
const content = lines
|
|
869
|
+
.map((line) => `${outerPadding}${pad(truncateVisible(line, width), width)}${outerPadding}`)
|
|
870
|
+
.join("\n");
|
|
759
871
|
return options.previousLineCount === undefined
|
|
760
872
|
? content
|
|
761
873
|
: renderInPlace(content, options.previousLineCount, options);
|