@mrclrchtr/supi-context 1.13.0 → 1.14.1

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/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 { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { clampReportWidth, formatReportTitle } from "@mrclrchtr/supi-core/report";
5
3
  import type { ContextAnalysis } from "./analysis.ts";
6
- import { formatTokens, pluralize } from "./utils.ts";
7
-
8
- type CategoryKey =
9
- | "systemPrompt"
10
- | "userMessages"
11
- | "assistantMessages"
12
- | "toolCalls"
13
- | "toolResults"
14
- | "other";
15
-
16
- const CATEGORY_ORDER: CategoryKey[] = [
17
- "systemPrompt",
18
- "userMessages",
19
- "assistantMessages",
20
- "toolCalls",
21
- "toolResults",
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 = Math.max(24, width);
26
+ const safeWidth = clampReportWidth(width);
662
27
  const lines: string[] = [];
663
28
 
664
- lines.push(truncateToWidth(theme.fg("accent", "◆ Context Usage"), safeWidth));
29
+ lines.push(formatReportTitle("◆ Context Usage", theme, safeWidth));
665
30
  lines.push("");
666
31
  lines.push(...renderSummary(analysis, theme, safeWidth));
667
32
  lines.push("");