@agnishc/edb-context-viewer 0.10.9 → 0.12.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/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/src/index.ts +43 -9
- package/src/stats-tab-content.ts +176 -83
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import {
|
|
18
18
|
buildSessionContext,
|
|
19
19
|
type ContextUsage,
|
|
20
|
+
DEFAULT_COMPACTION_SETTINGS,
|
|
20
21
|
type ExtensionAPI,
|
|
21
22
|
type ExtensionCommandContext,
|
|
22
23
|
type SessionContext,
|
|
@@ -197,6 +198,16 @@ function buildToolsText(activeToolDefs: { name: string; description?: string; pa
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
/** Build the token breakdown, scaling raw char-based estimates to match actual token count. */
|
|
201
|
+
function isSkillPath(path: unknown): boolean {
|
|
202
|
+
if (typeof path !== "string") return false;
|
|
203
|
+
return /(^|\/)\.agents\/skills\/|(^|\/)\.pi\/agent\/.*\/skills\/|(^|\/)skills\/[^/]+\/SKILL\.md$/i.test(path);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isSkillReadToolCall(block: any): boolean {
|
|
207
|
+
if (block?.name !== "read") return false;
|
|
208
|
+
return isSkillPath(block.arguments?.path ?? block.input?.path ?? block.args?.path);
|
|
209
|
+
}
|
|
210
|
+
|
|
200
211
|
function buildTokenBreakdown(
|
|
201
212
|
systemPrompt: string,
|
|
202
213
|
activeToolDefs: unknown[],
|
|
@@ -206,6 +217,7 @@ function buildTokenBreakdown(
|
|
|
206
217
|
if (!usage?.tokens || !usage.contextWindow) return null;
|
|
207
218
|
|
|
208
219
|
const estimateTokens = (text: string) => Math.ceil(text.length / 4);
|
|
220
|
+
const reserveTokens = Math.min(DEFAULT_COMPACTION_SETTINGS.reserveTokens, usage.contextWindow);
|
|
209
221
|
|
|
210
222
|
const systemRaw = estimateTokens(systemPrompt);
|
|
211
223
|
const toolDefsRaw = estimateTokens(JSON.stringify(activeToolDefs));
|
|
@@ -213,6 +225,8 @@ function buildTokenBreakdown(
|
|
|
213
225
|
let msgTokensRaw = 0;
|
|
214
226
|
let toolCallTokensRaw = 0;
|
|
215
227
|
let toolResultTokensRaw = 0;
|
|
228
|
+
let skillsRaw = 0;
|
|
229
|
+
const skillToolCallIds = new Set<string>();
|
|
216
230
|
|
|
217
231
|
for (const entry of branch) {
|
|
218
232
|
if (entry.type === "message" && entry.message) {
|
|
@@ -230,13 +244,24 @@ function buildTokenBreakdown(
|
|
|
230
244
|
else if (Array.isArray(m.content)) {
|
|
231
245
|
for (const p of m.content) {
|
|
232
246
|
if (p?.type === "text") msgTokensRaw += estimateTokens(p.text ?? "");
|
|
233
|
-
else if (p?.type === "toolCall")
|
|
247
|
+
else if (p?.type === "toolCall") {
|
|
248
|
+
if (isSkillReadToolCall(p)) {
|
|
249
|
+
skillsRaw += estimateTokens(JSON.stringify(p));
|
|
250
|
+
if (p.id) skillToolCallIds.add(p.id);
|
|
251
|
+
} else {
|
|
252
|
+
toolCallTokensRaw += estimateTokens(JSON.stringify(p));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
234
255
|
}
|
|
235
256
|
}
|
|
236
257
|
} else if (m.role === "toolResult") {
|
|
258
|
+
const isSkillResult = skillToolCallIds.has(m.toolCallId);
|
|
237
259
|
if (Array.isArray(m.content)) {
|
|
238
260
|
for (const p of m.content) {
|
|
239
|
-
if (p?.type === "text")
|
|
261
|
+
if (p?.type === "text") {
|
|
262
|
+
if (isSkillResult) skillsRaw += estimateTokens(p.text ?? "");
|
|
263
|
+
else toolResultTokensRaw += estimateTokens(p.text ?? "");
|
|
264
|
+
}
|
|
240
265
|
}
|
|
241
266
|
}
|
|
242
267
|
} else if (m.role === "bashExecution") {
|
|
@@ -248,22 +273,26 @@ function buildTokenBreakdown(
|
|
|
248
273
|
}
|
|
249
274
|
}
|
|
250
275
|
|
|
251
|
-
const totalRaw = systemRaw + toolDefsRaw + msgTokensRaw + toolCallTokensRaw + toolResultTokensRaw;
|
|
276
|
+
const totalRaw = systemRaw + skillsRaw + toolDefsRaw + msgTokensRaw + toolCallTokensRaw + toolResultTokensRaw;
|
|
252
277
|
const ratio = totalRaw > 0 ? usage.tokens / totalRaw : 1;
|
|
253
278
|
|
|
254
279
|
const sys = Math.round(systemRaw * ratio);
|
|
255
|
-
const
|
|
280
|
+
const skills = Math.round(skillsRaw * ratio);
|
|
281
|
+
const systemTools = Math.round(toolDefsRaw * ratio);
|
|
256
282
|
const msgs = Math.round(msgTokensRaw * ratio);
|
|
257
|
-
const
|
|
258
|
-
const accounted = sys +
|
|
283
|
+
const tools = Math.round((toolCallTokensRaw + toolResultTokensRaw) * ratio);
|
|
284
|
+
const accounted = sys + skills + systemTools + msgs + tools;
|
|
259
285
|
|
|
260
286
|
return {
|
|
261
287
|
total: usage.tokens,
|
|
262
288
|
contextWindow: usage.contextWindow,
|
|
263
289
|
percent: usage.percent ?? (usage.tokens / usage.contextWindow) * 100,
|
|
290
|
+
reserveTokens,
|
|
291
|
+
safeAvailable: Math.max(0, usage.contextWindow - reserveTokens - usage.tokens),
|
|
264
292
|
systemPrompt: sys,
|
|
265
|
-
systemTools
|
|
266
|
-
|
|
293
|
+
systemTools,
|
|
294
|
+
tools,
|
|
295
|
+
skills,
|
|
267
296
|
messages: msgs,
|
|
268
297
|
other: Math.max(0, usage.tokens - accounted),
|
|
269
298
|
};
|
|
@@ -322,8 +351,13 @@ export default function contextViewerExtension(pi: ExtensionAPI): void {
|
|
|
322
351
|
}
|
|
323
352
|
const messagesText = messagesLines.join("\n");
|
|
324
353
|
|
|
354
|
+
const modelName = ctx.model?.id ?? (ctx.model as any)?.modelId ?? "unknown model";
|
|
355
|
+
|
|
325
356
|
const tabs = [
|
|
326
|
-
new StatsTabContent(breakdown, theme
|
|
357
|
+
new StatsTabContent(breakdown, theme, {
|
|
358
|
+
name: modelName,
|
|
359
|
+
contextWindow: ctx.model?.contextWindow ?? usage?.contextWindow,
|
|
360
|
+
}),
|
|
327
361
|
new ScrollableTabContent(
|
|
328
362
|
{ rawText: systemPrompt, displayLines: buildNumberedLines(systemPrompt, theme), theme },
|
|
329
363
|
"System",
|
package/src/stats-tab-content.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* StatsTabContent — token distribution grid + category breakdown table.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Shows a compact model/context summary, then a colored 10×5 grid beside an
|
|
5
|
+
* estimated per-category usage breakdown. The final grid segment is reserved for
|
|
6
|
+
* Pi's auto-compaction response buffer.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -15,23 +15,106 @@ const GRID_WIDTH = 10;
|
|
|
15
15
|
const GRID_HEIGHT = 5;
|
|
16
16
|
const TOTAL_BLOCKS = GRID_WIDTH * GRID_HEIGHT; // 50 blocks = 2% each
|
|
17
17
|
|
|
18
|
+
const ANSI_RESET = "\x1b[0m";
|
|
19
|
+
const ANSI_BOLD = "\x1b[1m";
|
|
20
|
+
|
|
21
|
+
function fg(hex: string, text: string): string {
|
|
22
|
+
const normalized = hex.replace("#", "");
|
|
23
|
+
const r = Number.parseInt(normalized.slice(0, 2), 16);
|
|
24
|
+
const g = Number.parseInt(normalized.slice(2, 4), 16);
|
|
25
|
+
const b = Number.parseInt(normalized.slice(4, 6), 16);
|
|
26
|
+
return `\x1b[38;2;${r};${g};${b}m${text}${ANSI_RESET}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function bold(text: string): string {
|
|
30
|
+
return `${ANSI_BOLD}${text}${ANSI_RESET}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
export interface ContextTokenBreakdown {
|
|
19
34
|
total: number;
|
|
20
35
|
contextWindow: number;
|
|
21
36
|
percent: number;
|
|
37
|
+
reserveTokens: number;
|
|
38
|
+
safeAvailable: number;
|
|
22
39
|
systemPrompt: number;
|
|
23
40
|
systemTools: number;
|
|
24
|
-
|
|
41
|
+
tools: number;
|
|
42
|
+
skills: number;
|
|
25
43
|
messages: number;
|
|
26
44
|
other: number;
|
|
27
45
|
}
|
|
28
46
|
|
|
47
|
+
export interface StatsModelInfo {
|
|
48
|
+
name: string;
|
|
49
|
+
contextWindow?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
interface Category {
|
|
53
|
+
key: keyof Pick<
|
|
54
|
+
ContextTokenBreakdown,
|
|
55
|
+
"systemPrompt" | "systemTools" | "tools" | "skills" | "messages" | "safeAvailable" | "reserveTokens"
|
|
56
|
+
>;
|
|
30
57
|
label: string;
|
|
58
|
+
icon: string;
|
|
59
|
+
fallbackIcon: string;
|
|
31
60
|
value: number;
|
|
32
|
-
|
|
61
|
+
hex: string;
|
|
62
|
+
block: string;
|
|
63
|
+
includeInGrid: boolean;
|
|
33
64
|
}
|
|
34
65
|
|
|
66
|
+
const CATEGORY_META = {
|
|
67
|
+
systemPrompt: {
|
|
68
|
+
label: "System prompt",
|
|
69
|
+
icon: "",
|
|
70
|
+
fallbackIcon: "●",
|
|
71
|
+
hex: "#A78BFA",
|
|
72
|
+
block: "",
|
|
73
|
+
},
|
|
74
|
+
systemTools: {
|
|
75
|
+
label: "System tools",
|
|
76
|
+
icon: "",
|
|
77
|
+
fallbackIcon: "◆",
|
|
78
|
+
hex: "#22D3EE",
|
|
79
|
+
block: "",
|
|
80
|
+
},
|
|
81
|
+
tools: {
|
|
82
|
+
label: "Tools",
|
|
83
|
+
icon: "",
|
|
84
|
+
fallbackIcon: "▲",
|
|
85
|
+
hex: "#34D399",
|
|
86
|
+
block: "",
|
|
87
|
+
},
|
|
88
|
+
skills: {
|
|
89
|
+
label: "Skills",
|
|
90
|
+
icon: "",
|
|
91
|
+
fallbackIcon: "✦",
|
|
92
|
+
hex: "#FBBF24",
|
|
93
|
+
block: "",
|
|
94
|
+
},
|
|
95
|
+
messages: {
|
|
96
|
+
label: "Messages",
|
|
97
|
+
icon: "",
|
|
98
|
+
fallbackIcon: "■",
|
|
99
|
+
hex: "#60A5FA",
|
|
100
|
+
block: "",
|
|
101
|
+
},
|
|
102
|
+
safeAvailable: {
|
|
103
|
+
label: "Available",
|
|
104
|
+
icon: "",
|
|
105
|
+
fallbackIcon: "□",
|
|
106
|
+
hex: "#6B7280",
|
|
107
|
+
block: "",
|
|
108
|
+
},
|
|
109
|
+
reserveTokens: {
|
|
110
|
+
label: "Auto-compact buffer",
|
|
111
|
+
icon: "",
|
|
112
|
+
fallbackIcon: "▨",
|
|
113
|
+
hex: "#FB923C",
|
|
114
|
+
block: "",
|
|
115
|
+
},
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
35
118
|
export class StatsTabContent implements TabContent {
|
|
36
119
|
readonly name = "Stats";
|
|
37
120
|
readonly footerHints = "";
|
|
@@ -39,6 +122,7 @@ export class StatsTabContent implements TabContent {
|
|
|
39
122
|
constructor(
|
|
40
123
|
private breakdown: ContextTokenBreakdown | null,
|
|
41
124
|
private theme: Theme,
|
|
125
|
+
private modelInfo?: StatsModelInfo,
|
|
42
126
|
) {}
|
|
43
127
|
|
|
44
128
|
/** Stats view has no interactive search bar — always use border separator. */
|
|
@@ -48,8 +132,8 @@ export class StatsTabContent implements TabContent {
|
|
|
48
132
|
|
|
49
133
|
getFooterLeft(): string {
|
|
50
134
|
if (!this.breakdown) return "";
|
|
51
|
-
const { total, contextWindow, percent } = this.breakdown;
|
|
52
|
-
return `${formatTokens(total)} / ${formatTokens(contextWindow)} (${percent.toFixed(1)}%)`;
|
|
135
|
+
const { total, contextWindow, percent, safeAvailable } = this.breakdown;
|
|
136
|
+
return `${formatTokens(total)} / ${formatTokens(contextWindow)} (${percent.toFixed(1)}%) · ${formatTokens(safeAvailable)} safe left`;
|
|
53
137
|
}
|
|
54
138
|
|
|
55
139
|
/** Stats view has no keyboard interactions. */
|
|
@@ -72,103 +156,112 @@ export class StatsTabContent implements TabContent {
|
|
|
72
156
|
return lines;
|
|
73
157
|
}
|
|
74
158
|
|
|
75
|
-
const { total, contextWindow, percent,
|
|
159
|
+
const { total, contextWindow, percent, reserveTokens, safeAvailable } = this.breakdown;
|
|
160
|
+
const modelName = this.modelInfo?.name ?? "unknown model";
|
|
161
|
+
const safeLeftText =
|
|
162
|
+
safeAvailable > 0 ? `${formatTokens(safeAvailable)} safe left` : "auto-compact threshold reached";
|
|
163
|
+
|
|
164
|
+
const categories = this.getCategories();
|
|
165
|
+
const gridLines = this.renderGrid(categories);
|
|
166
|
+
const breakdownLines = this.renderBreakdown(categories);
|
|
76
167
|
|
|
77
|
-
const
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
168
|
+
const lines: string[] = [
|
|
169
|
+
` ${bold(`${modelName} · ${formatTokens(total)}/${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`)} ${th.fg("dim", `· ${safeLeftText}`)}`,
|
|
170
|
+
"",
|
|
171
|
+
"",
|
|
172
|
+
` ${th.fg("dim", "Estimated usage by category")}`,
|
|
173
|
+
"",
|
|
82
174
|
];
|
|
83
175
|
|
|
84
|
-
|
|
85
|
-
|
|
176
|
+
const GRID_VIS_W = GRID_WIDTH * 2 - 1;
|
|
177
|
+
const maxRows = Math.max(gridLines.length, breakdownLines.length);
|
|
178
|
+
for (let i = 0; i < maxRows; i++) {
|
|
179
|
+
const leftRaw = gridLines[i] ?? "";
|
|
180
|
+
const leftVisW = visibleWidth(leftRaw);
|
|
181
|
+
const pad = " ".repeat(Math.max(0, GRID_VIS_W - leftVisW));
|
|
182
|
+
const right = breakdownLines[i] ?? "";
|
|
183
|
+
lines.push(` ${leftRaw}${pad} ${right}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (safeAvailable <= 0) {
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push(` ${fg(CATEGORY_META.reserveTokens.hex, "Auto-compact buffer is being used")}`);
|
|
189
|
+
} else {
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push(
|
|
192
|
+
` ${th.fg("dim", `Auto-compact starts after ${formatTokens(contextWindow - reserveTokens)} tokens`)}`,
|
|
193
|
+
);
|
|
86
194
|
}
|
|
87
195
|
|
|
88
|
-
|
|
196
|
+
while (lines.length < height) lines.push("");
|
|
197
|
+
return lines.slice(0, height);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private getCategories(): Category[] {
|
|
201
|
+
const b = this.breakdown!;
|
|
202
|
+
return [
|
|
203
|
+
this.category("systemPrompt", b.systemPrompt, true),
|
|
204
|
+
this.category("systemTools", b.systemTools, true),
|
|
205
|
+
this.category("tools", b.tools, true),
|
|
206
|
+
this.category("skills", b.skills, true),
|
|
207
|
+
this.category("messages", b.messages + b.other, true),
|
|
208
|
+
this.category("safeAvailable", b.safeAvailable, true),
|
|
209
|
+
this.category("reserveTokens", b.reserveTokens, true),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private category(key: Category["key"], value: number, includeInGrid: boolean): Category {
|
|
214
|
+
const meta = CATEGORY_META[key];
|
|
215
|
+
return { key, value, includeInGrid, ...meta };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private renderGrid(categories: Category[]): string[] {
|
|
219
|
+
const blocks: string[] = [];
|
|
220
|
+
const { contextWindow, reserveTokens } = this.breakdown!;
|
|
221
|
+
const reserveBlockCount = Math.max(1, Math.round((reserveTokens / contextWindow) * TOTAL_BLOCKS));
|
|
222
|
+
const safeBlockCount = Math.max(0, TOTAL_BLOCKS - reserveBlockCount);
|
|
223
|
+
const safeWindow = Math.max(1, contextWindow - reserveTokens);
|
|
224
|
+
const safeCategories = categories.filter((cat) => cat.key !== "reserveTokens");
|
|
89
225
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
for (const cat of categories) {
|
|
93
|
-
let count = Math.round((cat.value / contextWindow) * TOTAL_BLOCKS);
|
|
226
|
+
for (const cat of safeCategories) {
|
|
227
|
+
let count = Math.round((cat.value / safeWindow) * safeBlockCount);
|
|
94
228
|
if (count === 0 && cat.value > 0) count = 1;
|
|
95
|
-
for (let
|
|
96
|
-
blocks.push(
|
|
229
|
+
for (let j = 0; j < count && blocks.length < safeBlockCount; j++) {
|
|
230
|
+
blocks.push(fg(cat.hex, cat.block));
|
|
97
231
|
}
|
|
98
232
|
}
|
|
99
|
-
|
|
100
|
-
|
|
233
|
+
|
|
234
|
+
while (blocks.length < safeBlockCount) {
|
|
235
|
+
blocks.push(fg(CATEGORY_META.safeAvailable.hex, CATEGORY_META.safeAvailable.block));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < reserveBlockCount && blocks.length < TOTAL_BLOCKS; i++) {
|
|
239
|
+
blocks.push(fg(CATEGORY_META.reserveTokens.hex, CATEGORY_META.reserveTokens.block));
|
|
101
240
|
}
|
|
102
241
|
|
|
103
|
-
// ── Render grid rows ─────────────────────────────────────────────────────
|
|
104
242
|
const gridLines: string[] = [];
|
|
105
243
|
for (let r = 0; r < GRID_HEIGHT; r++) {
|
|
106
244
|
let row = "";
|
|
107
245
|
for (let c = 0; c < GRID_WIDTH; c++) {
|
|
108
|
-
|
|
109
|
-
row += th.fg(b.color as Parameters<typeof th.fg>[0], b.filled ? "■" : "□");
|
|
246
|
+
row += blocks[r * GRID_WIDTH + c] ?? fg(CATEGORY_META.safeAvailable.hex, CATEGORY_META.safeAvailable.block);
|
|
110
247
|
if (c < GRID_WIDTH - 1) row += " ";
|
|
111
248
|
}
|
|
112
249
|
gridLines.push(row);
|
|
113
250
|
}
|
|
251
|
+
return gridLines;
|
|
252
|
+
}
|
|
114
253
|
|
|
115
|
-
|
|
116
|
-
const LABEL_W =
|
|
254
|
+
private renderBreakdown(categories: Category[]): string[] {
|
|
255
|
+
const LABEL_W = 21;
|
|
117
256
|
const TOKEN_W = 7;
|
|
118
257
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
127
|
-
legendLines.push(""); // blank separator before categories
|
|
128
|
-
|
|
129
|
-
// Per-category lines
|
|
130
|
-
for (const cat of categories) {
|
|
131
|
-
const pct = ((cat.value / contextWindow) * 100).toFixed(1);
|
|
132
|
-
legendLines.push(
|
|
133
|
-
`${th.fg(cat.color as Parameters<typeof th.fg>[0], "■")} ` +
|
|
134
|
-
`${th.fg("text", cat.label.padEnd(LABEL_W))} ` +
|
|
135
|
-
`${th.fg("accent", formatTokens(cat.value).padStart(TOKEN_W))} ` +
|
|
136
|
-
`${th.fg("dim", `(${pct.padStart(5)}%)`)}`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Available line
|
|
141
|
-
const availPct = ((available / contextWindow) * 100).toFixed(1);
|
|
142
|
-
legendLines.push(
|
|
143
|
-
`${th.fg("borderMuted" as Parameters<typeof th.fg>[0], "□")} ` +
|
|
144
|
-
`${th.fg("dim", "Available".padEnd(LABEL_W))} ` +
|
|
145
|
-
`${th.fg("dim", formatTokens(available).padStart(TOKEN_W))} ` +
|
|
146
|
-
`${th.fg("dim", `(${availPct.padStart(5)}%)`)}`,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// ── Combine grid + legend side by side ───────────────────────────────────
|
|
150
|
-
// Grid visible width: GRID_WIDTH * 2 - 1 (each "■ " or "□ " = 2, minus trailing space)
|
|
151
|
-
const GRID_VIS_W = GRID_WIDTH * 2 - 1;
|
|
152
|
-
const maxRows = Math.max(gridLines.length, legendLines.length);
|
|
153
|
-
|
|
154
|
-
const combined: string[] = [];
|
|
155
|
-
combined.push(""); // top padding
|
|
156
|
-
|
|
157
|
-
for (let i = 0; i < maxRows; i++) {
|
|
158
|
-
const leftRaw = gridLines[i] ?? "";
|
|
159
|
-
const leftVisW = visibleWidth(leftRaw);
|
|
160
|
-
const pad = " ".repeat(Math.max(0, GRID_VIS_W - leftVisW));
|
|
161
|
-
const right = legendLines[i] ?? "";
|
|
162
|
-
combined.push(` ${leftRaw}${pad} ${right}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
combined.push(""); // bottom padding
|
|
166
|
-
|
|
167
|
-
// Fill or truncate to content height
|
|
168
|
-
const result: string[] = [];
|
|
169
|
-
for (let i = 0; i < height; i++) {
|
|
170
|
-
result.push(combined[i] ?? "");
|
|
171
|
-
}
|
|
172
|
-
return result;
|
|
258
|
+
return categories.map((cat) => {
|
|
259
|
+
const pct = this.breakdown!.contextWindow > 0 ? (cat.value / this.breakdown!.contextWindow) * 100 : 0;
|
|
260
|
+
const icon = fg(cat.hex, cat.icon);
|
|
261
|
+
const label = fg(cat.hex, cat.label.padEnd(LABEL_W));
|
|
262
|
+
const tokens = fg(cat.hex, formatTokens(cat.value).padStart(TOKEN_W));
|
|
263
|
+
const percent = this.theme.fg("dim", `(${pct.toFixed(1).padStart(5)}%)`);
|
|
264
|
+
return `${icon} ${label} ${tokens} ${percent}`;
|
|
265
|
+
});
|
|
173
266
|
}
|
|
174
267
|
}
|