@mrclrchtr/supi-context 1.13.0 → 1.14.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/README.md +4 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +10 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +98 -17
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/package.json +2 -2
- package/src/format-helpers.ts +85 -0
- package/src/format-sections.ts +326 -0
- package/src/format-summary.ts +255 -0
- package/src/format.ts +19 -654
package/src/format.ts
CHANGED
|
@@ -1,667 +1,32 @@
|
|
|
1
|
-
// biome-ignore-all lint/style/noExcessiveLinesPerFile: format file is inherently large
|
|
2
|
-
|
|
3
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
2
|
+
import { clampReportWidth, formatReportTitle } from "@mrclrchtr/supi-core/report";
|
|
5
3
|
import type { ContextAnalysis } from "./analysis.ts";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"other",
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const CATEGORY_LABELS: Record<CategoryKey, string> = {
|
|
26
|
-
systemPrompt: "System prompt",
|
|
27
|
-
userMessages: "User messages",
|
|
28
|
-
assistantMessages: "Assistant messages",
|
|
29
|
-
toolCalls: "Tool calls",
|
|
30
|
-
toolResults: "Tool results",
|
|
31
|
-
other: "Other",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const CATEGORY_COLORS: Record<CategoryKey, Parameters<Theme["fg"]>["0"]> = {
|
|
35
|
-
systemPrompt: "accent",
|
|
36
|
-
userMessages: "success",
|
|
37
|
-
assistantMessages: "warning",
|
|
38
|
-
toolCalls: "error",
|
|
39
|
-
toolResults: "dim",
|
|
40
|
-
other: "muted",
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function pct(value: number, total: number): string {
|
|
44
|
-
if (total <= 0) return "0.0%";
|
|
45
|
-
return `${((value / total) * 100).toFixed(1)}%`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function padLeft(text: string, width: number): string {
|
|
49
|
-
return text.padStart(width, " ");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function padRight(text: string, width: number): string {
|
|
53
|
-
return text.padEnd(width, " ");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function sum(values: number[]): number {
|
|
57
|
-
return values.reduce((total, value) => total + value, 0);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function allocateBlocks(values: number[], totalBlocks: number): number[] {
|
|
61
|
-
const total = sum(values);
|
|
62
|
-
if (total <= 0 || totalBlocks <= 0) {
|
|
63
|
-
return values.map(() => 0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const exact = values.map((value) => (value / total) * totalBlocks);
|
|
67
|
-
const counts = exact.map((value) => Math.floor(value));
|
|
68
|
-
const remaining = totalBlocks - sum(counts);
|
|
69
|
-
|
|
70
|
-
const byRemainder = exact
|
|
71
|
-
.map((value, index) => ({ index, remainder: value - counts[index] }))
|
|
72
|
-
.sort((a, b) => b.remainder - a.remainder);
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < remaining; i += 1) {
|
|
75
|
-
counts[byRemainder[i]?.index ?? 0] += 1;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return counts;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function healthColor(analysis: ContextAnalysis): Parameters<Theme["fg"]>["0"] {
|
|
82
|
-
if (analysis.contextWindow <= 0) return "dim";
|
|
83
|
-
const reserved = analysis.totalTokens ?? 0;
|
|
84
|
-
const pressure =
|
|
85
|
-
((reserved + analysis.categories.autocompactBuffer) / analysis.contextWindow) * 100;
|
|
86
|
-
if (pressure >= 90) return "error";
|
|
87
|
-
if (pressure >= 70) return "warning";
|
|
88
|
-
return "success";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function sectionHeader(title: string, meta: string | null, theme: Theme, width: number): string {
|
|
92
|
-
const left = theme.fg("text", title);
|
|
93
|
-
const content = meta ? `${left}${theme.fg("dim", ` ${meta}`)}` : left;
|
|
94
|
-
return truncateToWidth(content, width);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function renderSummary(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
98
|
-
const used = analysis.totalTokens ?? 0;
|
|
99
|
-
const health = theme.fg(healthColor(analysis), "●");
|
|
100
|
-
const usage =
|
|
101
|
-
analysis.contextWindow > 0
|
|
102
|
-
? `${formatTokens(used)} / ${formatTokens(analysis.contextWindow)} tokens (${pct(used, analysis.contextWindow)})`
|
|
103
|
-
: `${formatTokens(used)} tokens`;
|
|
104
|
-
|
|
105
|
-
const lines = [
|
|
106
|
-
truncateToWidth(
|
|
107
|
-
`${health} ${theme.fg("text", analysis.modelName)}${theme.fg("dim", ` · ${usage}`)}`,
|
|
108
|
-
width,
|
|
109
|
-
),
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
if (analysis.approximationNote) {
|
|
113
|
-
lines.push(...wrapTextWithAnsi(theme.fg("warning", analysis.approximationNote), width));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return lines;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function renderUsageBar(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
120
|
-
if (analysis.contextWindow <= 0) {
|
|
121
|
-
return [theme.fg("dim", "No model selected — usage bar unavailable")];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const percentLabel = pct(analysis.totalTokens ?? 0, analysis.contextWindow);
|
|
125
|
-
const barWidth = Math.max(12, Math.min(48, width - visibleWidth(percentLabel) - 3));
|
|
126
|
-
const values = [
|
|
127
|
-
...CATEGORY_ORDER.map((key) => analysis.categories[key]),
|
|
128
|
-
analysis.categories.autocompactBuffer,
|
|
129
|
-
analysis.categories.freeSpace,
|
|
130
|
-
];
|
|
131
|
-
const counts = allocateBlocks(values, barWidth);
|
|
132
|
-
|
|
133
|
-
const segments = [
|
|
134
|
-
...CATEGORY_ORDER.map((key, index) => ({
|
|
135
|
-
color: CATEGORY_COLORS[key],
|
|
136
|
-
block: "█",
|
|
137
|
-
count: counts[index] ?? 0,
|
|
138
|
-
})),
|
|
139
|
-
{
|
|
140
|
-
color: "warning" as Parameters<Theme["fg"]>["0"],
|
|
141
|
-
block: "▒",
|
|
142
|
-
count: counts[CATEGORY_ORDER.length] ?? 0,
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
color: "dim" as Parameters<Theme["fg"]>["0"],
|
|
146
|
-
block: "░",
|
|
147
|
-
count: counts[CATEGORY_ORDER.length + 1] ?? 0,
|
|
148
|
-
},
|
|
149
|
-
];
|
|
150
|
-
|
|
151
|
-
const bar = segments
|
|
152
|
-
.map((segment) =>
|
|
153
|
-
segment.count > 0 ? theme.fg(segment.color, segment.block.repeat(segment.count)) : "",
|
|
154
|
-
)
|
|
155
|
-
.join("");
|
|
156
|
-
|
|
157
|
-
const barLine = truncateToWidth(
|
|
158
|
-
`${theme.fg("dim", "[")}${bar}${theme.fg("dim", "]")} ${theme.fg("text", percentLabel)}`,
|
|
159
|
-
width,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const legendParts: string[] = [];
|
|
163
|
-
for (const key of CATEGORY_ORDER) {
|
|
164
|
-
if (analysis.categories[key] <= 0) continue;
|
|
165
|
-
legendParts.push(`${theme.fg(CATEGORY_COLORS[key], "●")} ${CATEGORY_LABELS[key]}`);
|
|
166
|
-
}
|
|
167
|
-
if (analysis.categories.autocompactBuffer > 0) {
|
|
168
|
-
legendParts.push(`${theme.fg("warning", "▒")} Autocompact buffer`);
|
|
169
|
-
}
|
|
170
|
-
if (analysis.categories.freeSpace > 0) {
|
|
171
|
-
legendParts.push(`${theme.fg("dim", "░")} Free space`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return [barLine, ...wrapTextWithAnsi(legendParts.join(theme.fg("dim", " • ")), width)];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function renderCategoryBreakdown(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
178
|
-
const lines: string[] = [];
|
|
179
|
-
lines.push(sectionHeader("Usage by category", null, theme, width));
|
|
180
|
-
|
|
181
|
-
const rows: Array<{
|
|
182
|
-
label: string;
|
|
183
|
-
color: Parameters<Theme["fg"]>["0"];
|
|
184
|
-
tokens: number;
|
|
185
|
-
}> = [
|
|
186
|
-
...CATEGORY_ORDER.map((key) => ({
|
|
187
|
-
label: CATEGORY_LABELS[key],
|
|
188
|
-
color: CATEGORY_COLORS[key],
|
|
189
|
-
tokens: analysis.categories[key],
|
|
190
|
-
})),
|
|
191
|
-
{
|
|
192
|
-
label: "Autocompact buffer",
|
|
193
|
-
color: "warning" as Parameters<Theme["fg"]>["0"],
|
|
194
|
-
tokens: analysis.categories.autocompactBuffer,
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
label: "Free space",
|
|
198
|
-
color: "dim" as Parameters<Theme["fg"]>["0"],
|
|
199
|
-
tokens: analysis.categories.freeSpace,
|
|
200
|
-
},
|
|
201
|
-
];
|
|
202
|
-
|
|
203
|
-
const labelWidth = Math.max(18, Math.min(22, width - 22));
|
|
204
|
-
for (const row of rows) {
|
|
205
|
-
if (row.tokens <= 0 && row.label !== "Free space") continue;
|
|
206
|
-
const bullet = theme.fg(row.color, "●");
|
|
207
|
-
const label = padRight(row.label, labelWidth);
|
|
208
|
-
const tokens = padLeft(formatTokens(row.tokens), 8);
|
|
209
|
-
const percentage = padLeft(pct(row.tokens, analysis.contextWindow), 7);
|
|
210
|
-
lines.push(truncateToWidth(` ${bullet} ${label} ${tokens} ${percentage}`, width));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return lines;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function renderCompositionGuidelineSubRows(
|
|
217
|
-
sources: Array<{ source: string; tokens: number }>,
|
|
218
|
-
opts: { subLabelWidth: number; total: number; theme: Theme; width: number },
|
|
219
|
-
): string[] {
|
|
220
|
-
const lines: string[] = [];
|
|
221
|
-
for (const item of sources) {
|
|
222
|
-
const label =
|
|
223
|
-
item.source === "default" ? "default" : item.source === "other" ? "extensions" : item.source;
|
|
224
|
-
lines.push(
|
|
225
|
-
truncateToWidth(
|
|
226
|
-
` ${opts.theme.fg("dim", padRight(label, opts.subLabelWidth))} ${padLeft(formatTokens(item.tokens), 8)} ${padLeft(pct(item.tokens, opts.total), 7)}`,
|
|
227
|
-
opts.width,
|
|
228
|
-
),
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
return lines;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function renderCompositionSnippetSubRows(
|
|
235
|
-
details: Array<{ name: string; tokens: number }>,
|
|
236
|
-
subLabelWidth: number,
|
|
237
|
-
theme: Theme,
|
|
238
|
-
width: number,
|
|
239
|
-
): string[] {
|
|
240
|
-
const lines: string[] = [];
|
|
241
|
-
for (const item of details) {
|
|
242
|
-
lines.push(
|
|
243
|
-
truncateToWidth(
|
|
244
|
-
` ${theme.fg("dim", padRight(item.name, subLabelWidth))} ${padLeft(formatTokens(item.tokens), 8)}`,
|
|
245
|
-
width,
|
|
246
|
-
),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
return lines;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function renderSystemPromptComposition(
|
|
253
|
-
analysis: ContextAnalysis,
|
|
254
|
-
theme: Theme,
|
|
255
|
-
width: number,
|
|
256
|
-
): string[] {
|
|
257
|
-
const breakdown = analysis.systemPromptBreakdown;
|
|
258
|
-
const instructionFileTokens = sum(breakdown.instructionFiles.map((f) => f.tokens));
|
|
259
|
-
const contextFileTokens = sum(breakdown.contextFiles.map((f) => f.tokens));
|
|
260
|
-
const skillTokens = sum(breakdown.skills.map((s) => s.tokens));
|
|
261
|
-
|
|
262
|
-
// Use sum of all breakdown components as the denominator so percentages
|
|
263
|
-
// stay internally consistent even when the system prompt token count has
|
|
264
|
-
// been scaled to actual model usage.
|
|
265
|
-
const total =
|
|
266
|
-
breakdown.base +
|
|
267
|
-
instructionFileTokens +
|
|
268
|
-
contextFileTokens +
|
|
269
|
-
skillTokens +
|
|
270
|
-
breakdown.guidelines +
|
|
271
|
-
breakdown.toolSnippets +
|
|
272
|
-
breakdown.appendText;
|
|
273
|
-
|
|
274
|
-
const lines: string[] = [];
|
|
275
|
-
lines.push(
|
|
276
|
-
sectionHeader(
|
|
277
|
-
"System prompt composition",
|
|
278
|
-
total > 0 ? `${formatTokens(total)} tokens` : null,
|
|
279
|
-
theme,
|
|
280
|
-
width,
|
|
281
|
-
),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const labelWidth = Math.max(18, Math.min(22, width - 22));
|
|
285
|
-
const subLabelWidth = labelWidth;
|
|
286
|
-
|
|
287
|
-
const rows = [
|
|
288
|
-
{ label: "Base", color: "accent" as Parameters<Theme["fg"]>["0"], tokens: breakdown.base },
|
|
289
|
-
{
|
|
290
|
-
label: "Instruction files",
|
|
291
|
-
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
292
|
-
tokens: instructionFileTokens,
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
label: "Context files",
|
|
296
|
-
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
297
|
-
tokens: contextFileTokens,
|
|
298
|
-
},
|
|
299
|
-
{ label: "Skills", color: "text" as Parameters<Theme["fg"]>["0"], tokens: skillTokens },
|
|
300
|
-
{
|
|
301
|
-
label: "Guidelines",
|
|
302
|
-
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
303
|
-
tokens: breakdown.guidelines,
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
label: "Tool snippets",
|
|
307
|
-
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
308
|
-
tokens: breakdown.toolSnippets,
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
label: "Append text",
|
|
312
|
-
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
313
|
-
tokens: breakdown.appendText,
|
|
314
|
-
},
|
|
315
|
-
];
|
|
316
|
-
|
|
317
|
-
for (const row of rows) {
|
|
318
|
-
if (row.tokens <= 0) continue;
|
|
319
|
-
const bullet = theme.fg(row.color, "●");
|
|
320
|
-
const label = padRight(row.label, labelWidth);
|
|
321
|
-
const tokens = padLeft(formatTokens(row.tokens), 8);
|
|
322
|
-
const percentage = padLeft(pct(row.tokens, total), 7);
|
|
323
|
-
lines.push(truncateToWidth(` ${bullet} ${label} ${tokens} ${percentage}`, width));
|
|
324
|
-
|
|
325
|
-
if (row.label === "Guidelines" && breakdown.guidelineSources.length > 0) {
|
|
326
|
-
lines.push(
|
|
327
|
-
...renderCompositionGuidelineSubRows(breakdown.guidelineSources, {
|
|
328
|
-
subLabelWidth,
|
|
329
|
-
total,
|
|
330
|
-
theme,
|
|
331
|
-
width,
|
|
332
|
-
}),
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (row.label === "Tool snippets" && breakdown.toolSnippetDetails.length > 0) {
|
|
337
|
-
lines.push(
|
|
338
|
-
...renderCompositionSnippetSubRows(
|
|
339
|
-
breakdown.toolSnippetDetails,
|
|
340
|
-
subLabelWidth,
|
|
341
|
-
theme,
|
|
342
|
-
width,
|
|
343
|
-
),
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return lines;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
interface FileSectionOptions {
|
|
352
|
-
title: string;
|
|
353
|
-
subtitle: string;
|
|
354
|
-
files: Array<{ path: string; tokens: number; lines: number; extra?: string }>;
|
|
355
|
-
total: number;
|
|
356
|
-
theme: Theme;
|
|
357
|
-
width: number;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function renderFileSection(options: FileSectionOptions): string[] {
|
|
361
|
-
const { title, subtitle, files, total, theme, width } = options;
|
|
362
|
-
if (files.length === 0) return [];
|
|
363
|
-
|
|
364
|
-
const sorted = [...files].sort((a, b) => b.tokens - a.tokens);
|
|
365
|
-
const totalTokens = sum(sorted.map((file) => file.tokens));
|
|
366
|
-
const header = sectionHeader(
|
|
367
|
-
title,
|
|
368
|
-
`${pluralize(sorted.length, "file", "files")}, ${formatTokens(totalTokens)} tokens${subtitle ? ` · ${subtitle}` : ""}`,
|
|
369
|
-
theme,
|
|
370
|
-
width,
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
const tokenWidth = 8;
|
|
374
|
-
const lineWidth = 10;
|
|
375
|
-
const pctWidth = 7;
|
|
376
|
-
const extraWidth = sorted.some((file) => file.extra) ? 10 : 0;
|
|
377
|
-
const reserved =
|
|
378
|
-
2 + 2 + lineWidth + 2 + tokenWidth + 2 + pctWidth + (extraWidth ? 2 + extraWidth : 0);
|
|
379
|
-
const pathWidth = Math.max(16, width - reserved);
|
|
380
|
-
|
|
381
|
-
const lines = [header];
|
|
382
|
-
for (const file of sorted) {
|
|
383
|
-
const path = padRight(truncateToWidth(file.path, pathWidth), pathWidth);
|
|
384
|
-
const lineCol = padLeft(`${file.lines} lines`, lineWidth);
|
|
385
|
-
const tokenCol = padLeft(formatTokens(file.tokens), tokenWidth);
|
|
386
|
-
const pctCol = padLeft(pct(file.tokens, total), pctWidth);
|
|
387
|
-
const extra = extraWidth ? ` ${theme.fg("dim", padRight(file.extra ?? "", extraWidth))}` : "";
|
|
388
|
-
lines.push(
|
|
389
|
-
` ${theme.fg("text", path)} ${theme.fg("dim", lineCol)} ${theme.fg("dim", tokenCol)}${extra} ${theme.fg("dim", pctCol)}`,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return lines;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function renderInstructionFilesSection(
|
|
397
|
-
analysis: ContextAnalysis,
|
|
398
|
-
theme: Theme,
|
|
399
|
-
width: number,
|
|
400
|
-
): string[] {
|
|
401
|
-
return renderFileSection({
|
|
402
|
-
title: "Instruction Files (AGENTS.md / CLAUDE.md)",
|
|
403
|
-
subtitle: "share of system prompt",
|
|
404
|
-
files: analysis.systemPromptBreakdown.instructionFiles.map((file) => ({
|
|
405
|
-
path: file.path,
|
|
406
|
-
tokens: file.tokens,
|
|
407
|
-
lines: file.lines,
|
|
408
|
-
extra: file.origin,
|
|
409
|
-
})),
|
|
410
|
-
total: analysis.categories.systemPrompt,
|
|
411
|
-
theme,
|
|
412
|
-
width,
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function renderContextFilesSection(
|
|
417
|
-
analysis: ContextAnalysis,
|
|
418
|
-
theme: Theme,
|
|
419
|
-
width: number,
|
|
420
|
-
): string[] {
|
|
421
|
-
return renderFileSection({
|
|
422
|
-
title: "Context Files (system prompt)",
|
|
423
|
-
subtitle: "share of system prompt",
|
|
424
|
-
files: analysis.systemPromptBreakdown.contextFiles.map((file) => ({
|
|
425
|
-
path: file.path,
|
|
426
|
-
tokens: file.tokens,
|
|
427
|
-
lines: file.lines,
|
|
428
|
-
})),
|
|
429
|
-
total: analysis.categories.systemPrompt,
|
|
430
|
-
theme,
|
|
431
|
-
width,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function renderInjectedFilesSection(
|
|
436
|
-
analysis: ContextAnalysis,
|
|
437
|
-
theme: Theme,
|
|
438
|
-
width: number,
|
|
439
|
-
): string[] {
|
|
440
|
-
return renderFileSection({
|
|
441
|
-
title: "Context Files (injected · supi-claude-md)",
|
|
442
|
-
subtitle: "share of full context",
|
|
443
|
-
files: analysis.injectedFiles.map((file) => ({
|
|
444
|
-
path: file.file,
|
|
445
|
-
tokens: file.tokens,
|
|
446
|
-
lines: file.lines,
|
|
447
|
-
extra: `turn ${file.turn}`,
|
|
448
|
-
})),
|
|
449
|
-
total: analysis.totalTokens ?? 0,
|
|
450
|
-
theme,
|
|
451
|
-
width,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function renderSkillsSection(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
456
|
-
const lines: string[] = [];
|
|
457
|
-
const total = sum(analysis.skills.map((skill) => skill.tokens));
|
|
458
|
-
lines.push(
|
|
459
|
-
sectionHeader(
|
|
460
|
-
`Skills (${analysis.skills.length})`,
|
|
461
|
-
total > 0 ? `${formatTokens(total)} tokens` : null,
|
|
462
|
-
theme,
|
|
463
|
-
width,
|
|
464
|
-
),
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
if (analysis.skills.length === 0) {
|
|
468
|
-
lines.push(truncateToWidth(theme.fg("dim", " Send a message to see skill details"), width));
|
|
469
|
-
return lines;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const skillNameWidth =
|
|
473
|
-
analysis.skills.length > 0 ? Math.max(...analysis.skills.map((s) => s.name.length)) : 0;
|
|
474
|
-
for (const skill of analysis.skills) {
|
|
475
|
-
lines.push(
|
|
476
|
-
truncateToWidth(
|
|
477
|
-
` ${theme.fg("text", padRight(skill.name, skillNameWidth))} ${theme.fg("dim", padLeft(formatTokens(skill.tokens), 8))}`,
|
|
478
|
-
width,
|
|
479
|
-
),
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return lines;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function renderSourceSummaryBar(
|
|
487
|
-
gs: Array<{ source: string; bulletCount: number }>,
|
|
488
|
-
_theme: Theme,
|
|
489
|
-
_width: number,
|
|
490
|
-
): string | null {
|
|
491
|
-
if (gs.length === 0) return null;
|
|
492
|
-
const parts: string[] = [];
|
|
493
|
-
for (const s of gs) {
|
|
494
|
-
const label =
|
|
495
|
-
s.source === "default" ? "default" : s.source === "other" ? "extensions" : s.source;
|
|
496
|
-
parts.push(`${pluralize(s.bulletCount, "bullet", "bullets")} from ${label}`);
|
|
497
|
-
}
|
|
498
|
-
return parts.join(" · ");
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function renderBulletLines(
|
|
502
|
-
bullets: string[],
|
|
503
|
-
full: boolean,
|
|
504
|
-
theme: Theme,
|
|
505
|
-
width: number,
|
|
506
|
-
): string[] {
|
|
507
|
-
const previewLimit = full ? bullets.length : Math.min(6, bullets.length);
|
|
508
|
-
const lines: string[] = [];
|
|
509
|
-
for (let i = 0; i < previewLimit; i += 1) {
|
|
510
|
-
const rawText = bullets[i] ?? "";
|
|
511
|
-
const previewText = full || rawText.length <= 90 ? rawText : `${rawText.slice(0, 90)}…`;
|
|
512
|
-
const bullet = `${theme.fg("dim", "•")} ${theme.fg("text", previewText)}`;
|
|
513
|
-
if (full) {
|
|
514
|
-
lines.push(...wrapTextWithAnsi(bullet, Math.max(1, width - 2)).map((line) => ` ${line}`));
|
|
515
|
-
} else {
|
|
516
|
-
lines.push(truncateToWidth(` ${bullet}`, width));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (!full && bullets.length > previewLimit) {
|
|
520
|
-
lines.push(
|
|
521
|
-
truncateToWidth(
|
|
522
|
-
` ${theme.fg("dim", `… and ${bullets.length - previewLimit} more — run /supi-context full`)}`,
|
|
523
|
-
width,
|
|
524
|
-
),
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
return lines;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function renderGuidelinesSection(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
531
|
-
const sourceSummary = renderSourceSummaryBar(analysis.guidelineSources, theme, width);
|
|
532
|
-
|
|
533
|
-
const lines = [
|
|
534
|
-
sectionHeader(
|
|
535
|
-
`Guidelines (${pluralize(analysis.guidelineBullets.length, "bullet", "bullets")})`,
|
|
536
|
-
`${formatTokens(analysis.guidelines)} tokens`,
|
|
537
|
-
theme,
|
|
538
|
-
width,
|
|
539
|
-
),
|
|
540
|
-
];
|
|
541
|
-
|
|
542
|
-
if (sourceSummary) {
|
|
543
|
-
lines.push(truncateToWidth(` ${theme.fg("dim", sourceSummary)}`, width));
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const bullets = analysis.guidelineBullets;
|
|
547
|
-
if (bullets.length === 0) {
|
|
548
|
-
return lines;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
lines.push(...renderBulletLines(bullets, analysis.full, theme, width));
|
|
552
|
-
return lines;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function renderToolDefinitionsSection(
|
|
556
|
-
analysis: ContextAnalysis,
|
|
557
|
-
theme: Theme,
|
|
558
|
-
width: number,
|
|
559
|
-
): string[] {
|
|
560
|
-
const tools = [...analysis.toolDefinitions.tools].sort((a, b) => b.tokens - a.tokens);
|
|
561
|
-
if (tools.length === 0) return [];
|
|
562
|
-
|
|
563
|
-
const hasSnippetDetails = analysis.toolSnippetDetails.length > 0;
|
|
564
|
-
|
|
565
|
-
const lines: string[] = [];
|
|
566
|
-
lines.push(
|
|
567
|
-
sectionHeader(
|
|
568
|
-
`Tool Definitions (${tools.length} active)`,
|
|
569
|
-
`${formatTokens(analysis.toolDefinitions.tokens)} def tokens${hasSnippetDetails ? ` + ${formatTokens(sum(analysis.toolSnippetDetails.map((s) => s.tokens)))} snippet` : ""}`,
|
|
570
|
-
theme,
|
|
571
|
-
width,
|
|
572
|
-
),
|
|
573
|
-
);
|
|
574
|
-
|
|
575
|
-
const previewLimit = analysis.full ? tools.length : Math.min(5, tools.length);
|
|
576
|
-
const nameWidth = Math.max(12, Math.min(18, Math.max(...tools.map((tool) => tool.name.length))));
|
|
577
|
-
const defTokenWidth = 8;
|
|
578
|
-
const snippetTokenWidth = hasSnippetDetails ? 10 : 0;
|
|
579
|
-
const reserved =
|
|
580
|
-
2 + nameWidth + 2 + defTokenWidth + 2 + (snippetTokenWidth ? snippetTokenWidth + 2 : 0);
|
|
581
|
-
const descWidth = Math.max(12, width - reserved);
|
|
582
|
-
|
|
583
|
-
for (let i = 0; i < previewLimit; i += 1) {
|
|
584
|
-
const tool = tools[i];
|
|
585
|
-
const name = padRight(tool.name, nameWidth);
|
|
586
|
-
const previewDescription =
|
|
587
|
-
analysis.full || tool.description.length <= 50
|
|
588
|
-
? tool.description
|
|
589
|
-
: `${tool.description.slice(0, 50)}…`;
|
|
590
|
-
const description = truncateToWidth(previewDescription, descWidth);
|
|
591
|
-
const defTokens = padLeft(formatTokens(tool.tokens), defTokenWidth);
|
|
592
|
-
const snippet = analysis.toolSnippetDetails.find((s) => s.name === tool.name);
|
|
593
|
-
const snippetCol = snippet
|
|
594
|
-
? ` ${theme.fg("dim", padLeft(`+${formatTokens(snippet.tokens)}snip`, snippetTokenWidth))}`
|
|
595
|
-
: "";
|
|
596
|
-
lines.push(
|
|
597
|
-
truncateToWidth(
|
|
598
|
-
` ${theme.fg("text", name)} ${theme.fg("dim", description)} ${theme.fg("dim", defTokens)}${snippetCol}`,
|
|
599
|
-
width,
|
|
600
|
-
),
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (!analysis.full && tools.length > previewLimit) {
|
|
605
|
-
lines.push(
|
|
606
|
-
truncateToWidth(
|
|
607
|
-
` ${theme.fg("dim", `… and ${tools.length - previewLimit} more — run /supi-context full`)}`,
|
|
608
|
-
width,
|
|
609
|
-
),
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Add a legend row for snippet column if any tools had snippets
|
|
614
|
-
if (hasSnippetDetails) {
|
|
615
|
-
const snippetTotal = sum(analysis.toolSnippetDetails.map((s) => s.tokens));
|
|
616
|
-
lines.push(
|
|
617
|
-
truncateToWidth(
|
|
618
|
-
` ${theme.fg("dim", `→ total tool snippet tokens: ${formatTokens(snippetTotal)}`)}`,
|
|
619
|
-
width,
|
|
620
|
-
),
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return lines;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function renderCompactionNote(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
628
|
-
if (!analysis.compaction) return [];
|
|
629
|
-
return [
|
|
630
|
-
truncateToWidth(
|
|
631
|
-
theme.fg(
|
|
632
|
-
"dim",
|
|
633
|
-
`↳ ${pluralize(analysis.compaction.summarizedTurns, "older turn", "older turns")} summarized (compaction)`,
|
|
634
|
-
),
|
|
635
|
-
width,
|
|
636
|
-
),
|
|
637
|
-
];
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function renderProviderSections(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
641
|
-
if (analysis.providerSections.length === 0) return [];
|
|
642
|
-
|
|
643
|
-
const lines: string[] = [];
|
|
644
|
-
for (const section of analysis.providerSections) {
|
|
645
|
-
lines.push(sectionHeader(section.label, null, theme, width));
|
|
646
|
-
for (const [key, value] of Object.entries(section.data)) {
|
|
647
|
-
const label = theme.fg("text", key);
|
|
648
|
-
const content = theme.fg("dim", String(value));
|
|
649
|
-
lines.push(truncateToWidth(` ${label}: ${content}`, width));
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return lines;
|
|
654
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
renderCompactionNote,
|
|
6
|
+
renderContextFilesSection,
|
|
7
|
+
renderGuidelinesSection,
|
|
8
|
+
renderInjectedFilesSection,
|
|
9
|
+
renderInstructionFilesSection,
|
|
10
|
+
renderProviderSections,
|
|
11
|
+
renderSkillsSection,
|
|
12
|
+
renderToolDefinitionsSection,
|
|
13
|
+
} from "./format-sections.ts";
|
|
14
|
+
import {
|
|
15
|
+
renderCategoryBreakdown,
|
|
16
|
+
renderSummary,
|
|
17
|
+
renderSystemPromptComposition,
|
|
18
|
+
renderUsageBar,
|
|
19
|
+
} from "./format-summary.ts";
|
|
655
20
|
|
|
656
21
|
export function formatContextReport(
|
|
657
22
|
analysis: ContextAnalysis,
|
|
658
23
|
theme: Theme,
|
|
659
24
|
width = 200,
|
|
660
25
|
): string[] {
|
|
661
|
-
const safeWidth =
|
|
26
|
+
const safeWidth = clampReportWidth(width);
|
|
662
27
|
const lines: string[] = [];
|
|
663
28
|
|
|
664
|
-
lines.push(
|
|
29
|
+
lines.push(formatReportTitle("◆ Context Usage", theme, safeWidth));
|
|
665
30
|
lines.push("");
|
|
666
31
|
lines.push(...renderSummary(analysis, theme, safeWidth));
|
|
667
32
|
lines.push("");
|