@pi9/context 0.1.0 → 0.2.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 +2 -2
- package/media/context-report.png +0 -0
- package/package.json +4 -4
- package/src/builders.ts +9 -49
- package/src/component.ts +95 -80
- package/src/types.ts +1 -21
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pi9/context
|
|
2
2
|
|
|
3
|
-
Pi extension that registers `/context` — a scrollable breakdown of how your current context window is being used (model,
|
|
3
|
+
Pi extension that registers `/context` — a scrollable breakdown of how your current context window is being used (model, responsive usage graph, compaction reserve, conversation stats, memory files, tools, and skills).
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -37,4 +37,4 @@ To verify the TUI end-to-end:
|
|
|
37
37
|
|
|
38
38
|
1. Start pi with this extension in a terminal at least 80 columns wide.
|
|
39
39
|
2. Run `/context` on a session with several messages so the report exceeds the viewport.
|
|
40
|
-
3. Confirm keyboard scrolling, help text, the
|
|
40
|
+
3. Confirm keyboard scrolling, help text, the responsive graph and displayed per-block token scale, section token totals, and **Esc** closes the inline view without injecting report text into the chat.
|
package/media/context-report.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi9/context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pi extension to show a breakdown of your current context usage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chase Cummings <chaseecummings@gmail.com>",
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"types": "src/index.ts",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/Chase-C/
|
|
12
|
+
"url": "git+https://github.com/Chase-C/pi9.git",
|
|
13
13
|
"directory": "packages/context"
|
|
14
14
|
},
|
|
15
15
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/Chase-C/
|
|
16
|
+
"url": "https://github.com/Chase-C/pi9/issues"
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://github.com/Chase-C/
|
|
18
|
+
"homepage": "https://github.com/Chase-C/pi9/tree/main/packages/context#readme",
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
21
|
"media",
|
package/src/builders.ts
CHANGED
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
ContextReport,
|
|
14
14
|
ConversationDetails,
|
|
15
15
|
ConversationStats,
|
|
16
|
-
ConversationTurn,
|
|
17
16
|
MemoryDetails,
|
|
18
17
|
ModelDetails,
|
|
19
18
|
SkillDetails,
|
|
@@ -248,91 +247,52 @@ export function collectConversationDetails(ctx: ExtensionCommandContext): Conver
|
|
|
248
247
|
userMessages: 0,
|
|
249
248
|
assistantMessages: 0,
|
|
250
249
|
toolResults: 0,
|
|
251
|
-
toolCalls: 0,
|
|
252
250
|
thinkingBlocks: 0,
|
|
253
251
|
imageBlocks: 0,
|
|
254
252
|
compactions: branch.filter((entry) => entry.type === "compaction").length,
|
|
255
253
|
};
|
|
256
|
-
const
|
|
254
|
+
const toolCallCounts = new Map<string, number>();
|
|
257
255
|
let tokens = 0;
|
|
258
256
|
|
|
259
257
|
for (const message of messages) {
|
|
260
|
-
|
|
261
|
-
tokens += messageTokens;
|
|
258
|
+
tokens += estimateMessageTokens(message);
|
|
262
259
|
|
|
263
260
|
switch (message.role) {
|
|
264
261
|
case "user":
|
|
265
262
|
stats.userMessages += 1;
|
|
266
263
|
stats.imageBlocks += countImageBlocks(message.content);
|
|
267
|
-
history.push({ kind: "user", tokens: messageTokens });
|
|
268
264
|
break;
|
|
269
265
|
case "assistant":
|
|
270
266
|
stats.assistantMessages += 1;
|
|
271
|
-
|
|
267
|
+
collectAssistantStats(message, stats, toolCallCounts);
|
|
272
268
|
break;
|
|
273
269
|
case "toolResult":
|
|
274
270
|
stats.toolResults += 1;
|
|
275
271
|
stats.imageBlocks += countImageBlocks(message.content);
|
|
276
|
-
history.push({
|
|
277
|
-
kind: "tool-result",
|
|
278
|
-
tool: message.toolName ?? "unknown",
|
|
279
|
-
tokens: messageTokens,
|
|
280
|
-
callId: message.toolCallId,
|
|
281
|
-
isError: message.isError,
|
|
282
|
-
});
|
|
283
272
|
break;
|
|
284
273
|
case "custom":
|
|
285
274
|
stats.imageBlocks += countImageBlocks(message.content);
|
|
286
|
-
history.push({ kind: "custom", tokens: messageTokens });
|
|
287
|
-
break;
|
|
288
|
-
default:
|
|
289
|
-
history.push({ kind: "custom", tokens: messageTokens });
|
|
290
275
|
break;
|
|
291
276
|
}
|
|
292
277
|
}
|
|
293
278
|
|
|
294
|
-
return { stats,
|
|
279
|
+
return { stats, toolCallCounts, tokens };
|
|
295
280
|
}
|
|
296
281
|
|
|
297
|
-
function
|
|
282
|
+
function collectAssistantStats(
|
|
298
283
|
message: AssistantMessage,
|
|
299
|
-
history: ConversationTurn[],
|
|
300
284
|
stats: ConversationStats,
|
|
301
|
-
|
|
285
|
+
toolCallCounts: Map<string, number>,
|
|
302
286
|
): void {
|
|
303
|
-
if (!Array.isArray(message.content))
|
|
304
|
-
history.push({ kind: "assistant", tokens: fallbackTokens });
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
287
|
+
if (!Array.isArray(message.content)) return;
|
|
307
288
|
|
|
308
|
-
let emitted = false;
|
|
309
289
|
for (const block of message.content) {
|
|
310
|
-
if (
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (block.type === "text") {
|
|
315
|
-
history.push({ kind: "assistant", tokens: estimateTokens("text" in block ? block.text : "") });
|
|
316
|
-
emitted = true;
|
|
317
|
-
} else if (block.type === "thinking") {
|
|
290
|
+
if (block.type === "thinking") {
|
|
318
291
|
stats.thinkingBlocks += 1;
|
|
319
|
-
history.push({ kind: "thinking", tokens: estimateTokens("thinking" in block ? block.thinking : "") });
|
|
320
|
-
emitted = true;
|
|
321
292
|
} else if (block.type === "toolCall") {
|
|
322
|
-
|
|
323
|
-
history.push({
|
|
324
|
-
kind: "tool-call",
|
|
325
|
-
tool: "name" in block && typeof block.name === "string" ? block.name : "unknown",
|
|
326
|
-
tokens: estimateTokens(block),
|
|
327
|
-
callId: "id" in block && typeof block.id === "string" ? block.id : undefined,
|
|
328
|
-
});
|
|
329
|
-
emitted = true;
|
|
293
|
+
toolCallCounts.set(block.name, (toolCallCounts.get(block.name) ?? 0) + 1);
|
|
330
294
|
}
|
|
331
295
|
}
|
|
332
|
-
|
|
333
|
-
if (!emitted) {
|
|
334
|
-
history.push({ kind: "assistant", tokens: fallbackTokens });
|
|
335
|
-
}
|
|
336
296
|
}
|
|
337
297
|
|
|
338
298
|
function escapeXml(value: string): string {
|
package/src/component.ts
CHANGED
|
@@ -5,22 +5,21 @@ import type { ContextReport, ToolSource } from "./types.js";
|
|
|
5
5
|
|
|
6
6
|
export const CONTEXT_REPORT_HELP = "↑↓/jk scroll · PgUp/PgDn or u/d page · Home/End · q/Esc close";
|
|
7
7
|
|
|
8
|
-
const CHROME_LINES =
|
|
8
|
+
const CHROME_LINES = 3;
|
|
9
9
|
const VIEW_HEIGHT_FRACTION = 0.9;
|
|
10
|
-
const GRAPH_CELL_TOKENS = 1_000;
|
|
11
10
|
const GRAPH_CELL_GAP = " ";
|
|
12
11
|
const GRAPH_GAP = 4;
|
|
13
|
-
const
|
|
12
|
+
const STACKED_GRAPH_ROWS = 7;
|
|
14
13
|
const GRAPH_STYLE = {
|
|
15
|
-
prompt: { glyph: "●", color: "
|
|
16
|
-
tools: { glyph: "
|
|
17
|
-
memory: { glyph: "
|
|
18
|
-
skills: { glyph: "
|
|
14
|
+
prompt: { glyph: "●", color: "text" },
|
|
15
|
+
tools: { glyph: "●", color: "warning" },
|
|
16
|
+
memory: { glyph: "●", color: "error" },
|
|
17
|
+
skills: { glyph: "●", color: "success" },
|
|
19
18
|
conversation: { glyph: "◉", color: "accent" },
|
|
20
|
-
other: { glyph: "
|
|
19
|
+
other: { glyph: "●", color: "muted" },
|
|
21
20
|
free: { glyph: "○", color: "borderMuted" },
|
|
22
|
-
compaction: { glyph: "
|
|
23
|
-
unknown: { glyph: "
|
|
21
|
+
compaction: { glyph: "●", color: "warning" },
|
|
22
|
+
unknown: { glyph: "●", color: "dim" },
|
|
24
23
|
} as const satisfies Record<string, { glyph: string; color: ThemeColor }>;
|
|
25
24
|
|
|
26
25
|
type TreeBranch = "├" | "└";
|
|
@@ -119,12 +118,18 @@ export function createContextReportComponent(
|
|
|
119
118
|
|
|
120
119
|
const innerWidth = width - 2;
|
|
121
120
|
const border = (text: string) => theme.fg("border", text);
|
|
122
|
-
const padLine = (text: string) =>
|
|
121
|
+
const padLine = (text: string) => {
|
|
122
|
+
const clipped = truncateToWidth(text, innerWidth, "...", true);
|
|
123
|
+
return `${clipped}${" ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)))}`;
|
|
124
|
+
};
|
|
123
125
|
const title = truncateToWidth(" Context Report ", innerWidth);
|
|
124
|
-
const
|
|
126
|
+
const titleWidth = visibleWidth(title);
|
|
127
|
+
const separator = "─".repeat(Math.min(4, Math.max(0, innerWidth - titleWidth)));
|
|
128
|
+
const helpWidth = Math.max(0, innerWidth - titleWidth - separator.length);
|
|
129
|
+
const help = truncateToWidth(` ${CONTEXT_REPORT_HELP} `, helpWidth, "");
|
|
130
|
+
const titlePad = Math.max(0, helpWidth - visibleWidth(help));
|
|
125
131
|
const result = [
|
|
126
|
-
border("╭") + theme.fg("accent", title) + border(`${"─".repeat(titlePad)}╮`),
|
|
127
|
-
border("│") + padLine(` ${theme.fg("dim", CONTEXT_REPORT_HELP)}`) + border("│"),
|
|
132
|
+
border("╭") + theme.fg("accent", theme.bold(title)) + border(separator) + theme.fg("dim", help) + border(`${"─".repeat(titlePad)}╮`),
|
|
128
133
|
];
|
|
129
134
|
|
|
130
135
|
const visible = displayLines.slice(scrollOffset, scrollOffset + viewport);
|
|
@@ -137,7 +142,7 @@ export function createContextReportComponent(
|
|
|
137
142
|
|
|
138
143
|
const maxOffset = maxScrollOffset(displayLines.length, viewport);
|
|
139
144
|
const scrollHint = maxOffset > 0
|
|
140
|
-
? theme.fg("
|
|
145
|
+
? theme.fg("muted", ` ${scrollOffset + 1}-${scrollOffset + visible.length} of ${displayLines.length}`)
|
|
141
146
|
: "";
|
|
142
147
|
result.push(border("│") + padLine(scrollHint) + border("│"));
|
|
143
148
|
result.push(border(`╰${"─".repeat(innerWidth)}╯`));
|
|
@@ -167,11 +172,7 @@ export function clampScrollOffset(offset: number, lineCount: number, viewportLin
|
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
export function formatContextReportLines(report: ContextReport, theme: Theme, width = 80): string[] {
|
|
170
|
-
const lines = [
|
|
171
|
-
theme.fg("customMessageLabel", theme.bold("Context Usage")),
|
|
172
|
-
"",
|
|
173
|
-
...formatUsageOverview(report, theme, width),
|
|
174
|
-
];
|
|
175
|
+
const lines = ["", ...formatUsageOverview(report, theme, width)];
|
|
175
176
|
|
|
176
177
|
if (report.kind === "conversation") {
|
|
177
178
|
pushSection(lines, formatConversationSection(report, theme));
|
|
@@ -221,11 +222,28 @@ function formatUsageOverview(report: ContextReport, theme: Theme, width: number)
|
|
|
221
222
|
];
|
|
222
223
|
const totalGraphTokens = contextWindow > 0
|
|
223
224
|
? contextWindow
|
|
224
|
-
: Math.max(
|
|
225
|
+
: Math.max(1, sum(graphCategories.map((category) => category.tokens)));
|
|
225
226
|
const visibleGraphCategories = graphCategories.length > 0
|
|
226
227
|
? graphCategories
|
|
227
228
|
: [{ label: "Free space", tokens: totalGraphTokens, ...GRAPH_STYLE.free }];
|
|
228
|
-
const
|
|
229
|
+
const summaryProbe = formatUsageSummary(
|
|
230
|
+
report,
|
|
231
|
+
usedCategories,
|
|
232
|
+
freeTokens,
|
|
233
|
+
unknownTokens,
|
|
234
|
+
configuredReserve,
|
|
235
|
+
compactionTokens,
|
|
236
|
+
totalGraphTokens,
|
|
237
|
+
theme,
|
|
238
|
+
);
|
|
239
|
+
const summaryWidth = Math.max(...summaryProbe.map(visibleWidth));
|
|
240
|
+
const columns = graphColumnCount(width, summaryWidth);
|
|
241
|
+
const sideBySide = shouldRenderSideBySide(width, columns, summaryWidth);
|
|
242
|
+
const targetRows = sideBySide ? summaryProbe.length : STACKED_GRAPH_ROWS;
|
|
243
|
+
const graphRows = Math.max(targetRows, Math.ceil(visibleGraphCategories.length / columns));
|
|
244
|
+
const cellCount = columns * graphRows;
|
|
245
|
+
const tokensPerCell = totalGraphTokens / cellCount;
|
|
246
|
+
const graphLines = formatGraphGrid(visibleGraphCategories, cellCount, columns, theme);
|
|
229
247
|
const summaryLines = formatUsageSummary(
|
|
230
248
|
report,
|
|
231
249
|
usedCategories,
|
|
@@ -233,10 +251,11 @@ function formatUsageOverview(report: ContextReport, theme: Theme, width: number)
|
|
|
233
251
|
unknownTokens,
|
|
234
252
|
configuredReserve,
|
|
235
253
|
compactionTokens,
|
|
254
|
+
tokensPerCell,
|
|
236
255
|
theme,
|
|
237
256
|
);
|
|
238
257
|
|
|
239
|
-
if (!
|
|
258
|
+
if (!sideBySide) {
|
|
240
259
|
return [
|
|
241
260
|
...graphLines,
|
|
242
261
|
"",
|
|
@@ -260,6 +279,7 @@ function formatUsageSummary(
|
|
|
260
279
|
unknownTokens: number,
|
|
261
280
|
configuredReserve: number,
|
|
262
281
|
compactionTokens: number,
|
|
282
|
+
tokensPerCell: number,
|
|
263
283
|
theme: Theme,
|
|
264
284
|
): string[] {
|
|
265
285
|
const contextWindow = knownTokenValue(report.usage.contextWindow) ?? 0;
|
|
@@ -272,25 +292,25 @@ function formatUsageSummary(
|
|
|
272
292
|
`${theme.fg("text", theme.bold(modelLabel))}${contextWindow > 0 ? theme.fg("muted", ` (${formatTokens(contextWindow)} context)`) : ""}`,
|
|
273
293
|
theme.fg("muted", modelParts.join(" · ")),
|
|
274
294
|
`${theme.fg("accent", formatTokens(currentTotal))}${contextWindow > 0 ? `/${formatTokens(contextWindow)}` : ""} tokens (${formatPercent(report.usage.percent)})`,
|
|
275
|
-
theme.fg("muted", `1
|
|
295
|
+
theme.fg("muted", `1 block ≈ ${formatTokens(tokensPerCell)} tokens`),
|
|
276
296
|
"",
|
|
277
297
|
heading(theme, "Estimated breakdown", currentTotal),
|
|
278
298
|
];
|
|
279
299
|
|
|
280
300
|
for (const category of categories) {
|
|
281
|
-
lines.push(`${theme.fg(category.color, category.glyph)} ${category.label}: ${
|
|
301
|
+
lines.push(`${theme.fg(category.color, category.glyph)} ${category.label}: ${formatBreakdownValue(category.tokens, contextWindow, theme)}`);
|
|
282
302
|
}
|
|
283
303
|
if (freeTokens > 0) {
|
|
284
|
-
lines.push(`${theme.fg(GRAPH_STYLE.free.color, GRAPH_STYLE.free.glyph)} Free space: ${
|
|
304
|
+
lines.push(`${theme.fg(GRAPH_STYLE.free.color, GRAPH_STYLE.free.glyph)} Free space: ${formatBreakdownValue(freeTokens, contextWindow, theme)}`);
|
|
285
305
|
}
|
|
286
306
|
if (unknownTokens > 0) {
|
|
287
|
-
lines.push(`${theme.fg(GRAPH_STYLE.unknown.color, GRAPH_STYLE.unknown.glyph)} Unknown capacity: ${
|
|
307
|
+
lines.push(`${theme.fg(GRAPH_STYLE.unknown.color, GRAPH_STYLE.unknown.glyph)} Unknown capacity: ${formatBreakdownValue(unknownTokens, contextWindow, theme)}`);
|
|
288
308
|
}
|
|
289
309
|
if (configuredReserve > 0) {
|
|
290
310
|
const remaining = compactionTokens < configuredReserve
|
|
291
|
-
? theme.fg("
|
|
311
|
+
? theme.fg("muted", ` · ${formatTokens(compactionTokens)} unoccupied`)
|
|
292
312
|
: "";
|
|
293
|
-
lines.push(`${theme.fg(GRAPH_STYLE.compaction.color, GRAPH_STYLE.compaction.glyph)} Compaction reserve: ${
|
|
313
|
+
lines.push(`${theme.fg(GRAPH_STYLE.compaction.color, GRAPH_STYLE.compaction.glyph)} Compaction reserve: ${formatBreakdownValue(configuredReserve, contextWindow, theme)}${remaining}`);
|
|
294
314
|
}
|
|
295
315
|
|
|
296
316
|
return lines;
|
|
@@ -352,15 +372,14 @@ function scaleCategories(categories: GraphCategory[], targetTokens: number): Gra
|
|
|
352
372
|
|
|
353
373
|
function formatGraphGrid(
|
|
354
374
|
categories: GraphCategory[],
|
|
355
|
-
|
|
375
|
+
cellCount: number,
|
|
356
376
|
columns: number,
|
|
357
377
|
theme: Theme,
|
|
358
378
|
): string[] {
|
|
359
|
-
const cellCount = Math.max(1, Math.ceil(totalTokens / GRAPH_CELL_TOKENS));
|
|
360
379
|
const cells = allocateGraphCells(categories, cellCount);
|
|
361
380
|
const lines: string[] = [];
|
|
362
381
|
|
|
363
|
-
for (let start = 0; start <
|
|
382
|
+
for (let start = 0; start < cells.length; start += columns) {
|
|
364
383
|
lines.push(cells
|
|
365
384
|
.slice(start, start + columns)
|
|
366
385
|
.map((category) => theme.fg(category.color, category.glyph))
|
|
@@ -371,39 +390,41 @@ function formatGraphGrid(
|
|
|
371
390
|
}
|
|
372
391
|
|
|
373
392
|
function allocateGraphCells(categories: GraphCategory[], cellCount: number): GraphCategory[] {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
393
|
+
const visibleCategories = categories.filter((category) => category.tokens > 0);
|
|
394
|
+
if (visibleCategories.length === 0) return [];
|
|
395
|
+
|
|
396
|
+
const targetCellCount = Math.max(cellCount, visibleCategories.length);
|
|
397
|
+
const totalTokens = sum(visibleCategories.map((category) => category.tokens));
|
|
398
|
+
const allocations = visibleCategories.map((category) => {
|
|
399
|
+
const ideal = (category.tokens / totalTokens) * targetCellCount;
|
|
400
|
+
return { category, ideal, count: Math.max(1, Math.floor(ideal)) };
|
|
401
|
+
});
|
|
402
|
+
let allocated = sum(allocations.map((allocation) => allocation.count));
|
|
403
|
+
|
|
404
|
+
while (allocated > targetCellCount) {
|
|
405
|
+
const donor = allocations
|
|
406
|
+
.filter((allocation) => allocation.count > 1)
|
|
407
|
+
.sort((a, b) => (b.count - b.ideal) - (a.count - a.ideal))[0];
|
|
408
|
+
if (!donor) break;
|
|
409
|
+
donor.count -= 1;
|
|
410
|
+
allocated -= 1;
|
|
381
411
|
}
|
|
382
412
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
let best = fallback;
|
|
389
|
-
let bestOverlap = 0;
|
|
390
|
-
|
|
391
|
-
for (const range of ranges) {
|
|
392
|
-
const overlap = Math.max(0, Math.min(end, range.end) - Math.max(start, range.start));
|
|
393
|
-
if (overlap > bestOverlap) {
|
|
394
|
-
best = range;
|
|
395
|
-
bestOverlap = overlap;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
cells.push(best);
|
|
413
|
+
while (allocated < targetCellCount) {
|
|
414
|
+
const recipient = [...allocations]
|
|
415
|
+
.sort((a, b) => (b.ideal - b.count) - (a.ideal - a.count))[0]!;
|
|
416
|
+
recipient.count += 1;
|
|
417
|
+
allocated += 1;
|
|
400
418
|
}
|
|
401
|
-
|
|
419
|
+
|
|
420
|
+
return allocations.flatMap(({ category, count }) =>
|
|
421
|
+
Array.from({ length: count }, () => category),
|
|
422
|
+
);
|
|
402
423
|
}
|
|
403
424
|
|
|
404
|
-
function graphColumnCount(width: number): number {
|
|
425
|
+
function graphColumnCount(width: number, summaryWidth: number): number {
|
|
405
426
|
const target = width >= 112 ? 36 : width >= 88 ? 32 : width >= 68 ? 24 : 20;
|
|
406
|
-
const sideBySideWidth = width - GRAPH_GAP -
|
|
427
|
+
const sideBySideWidth = width - GRAPH_GAP - summaryWidth;
|
|
407
428
|
const maxWidth = sideBySideWidth >= 10 ? sideBySideWidth : width;
|
|
408
429
|
return Math.max(1, Math.min(target, maxGraphColumnsForWidth(maxWidth)));
|
|
409
430
|
}
|
|
@@ -413,9 +434,9 @@ function maxGraphColumnsForWidth(width: number): number {
|
|
|
413
434
|
return Math.max(1, Math.floor((Math.max(1, width) + gapWidth) / (1 + gapWidth)));
|
|
414
435
|
}
|
|
415
436
|
|
|
416
|
-
function shouldRenderSideBySide(width: number,
|
|
417
|
-
const graphWidth =
|
|
418
|
-
return width - graphWidth - GRAPH_GAP >=
|
|
437
|
+
function shouldRenderSideBySide(width: number, columns: number, summaryWidth: number): boolean {
|
|
438
|
+
const graphWidth = columns + (columns - 1) * visibleWidth(GRAPH_CELL_GAP);
|
|
439
|
+
return width - graphWidth - GRAPH_GAP >= summaryWidth;
|
|
419
440
|
}
|
|
420
441
|
|
|
421
442
|
function padAnsi(text: string, width: number): string {
|
|
@@ -432,7 +453,7 @@ function formatConversationSection(report: Extract<ContextReport, { kind: "conve
|
|
|
432
453
|
return [
|
|
433
454
|
heading(theme, "Conversation (estimated)", report.conversation.tokens),
|
|
434
455
|
`${branch(theme, "├")}messages: user ${stats.userMessages} · assistant ${stats.assistantMessages} · tool results ${stats.toolResults}`,
|
|
435
|
-
`${branch(theme, "├")}blocks: tool calls ${
|
|
456
|
+
`${branch(theme, "├")}blocks: tool calls ${sum([...report.conversation.toolCallCounts.values()])} · thinking ${stats.thinkingBlocks} · images ${stats.imageBlocks}`,
|
|
436
457
|
`${branch(theme, "├")}compactions: ${stats.compactions}`,
|
|
437
458
|
`${branch(theme, "└")}message tokens: ${theme.fg("accent", formatTokens(report.conversation.tokens))}`,
|
|
438
459
|
];
|
|
@@ -442,7 +463,7 @@ function formatToolsSection(report: ContextReport, theme: Theme): string[] {
|
|
|
442
463
|
if (report.tools.length === 0) return [];
|
|
443
464
|
|
|
444
465
|
const callCounts = report.kind === "conversation"
|
|
445
|
-
?
|
|
466
|
+
? report.conversation.toolCallCounts
|
|
446
467
|
: new Map<string, number>();
|
|
447
468
|
const groups: Array<{ title: string; kind: ToolSource["kind"] }> = [
|
|
448
469
|
{ title: "Built-in tools", kind: "builtin" },
|
|
@@ -457,11 +478,11 @@ function formatToolsSection(report: ContextReport, theme: Theme): string[] {
|
|
|
457
478
|
if (tools.length === 0) continue;
|
|
458
479
|
|
|
459
480
|
const groupTokens = sum(tools.filter((tool) => tool.active).map((tool) => tool.tokens));
|
|
460
|
-
lines.push(`${theme.fg("muted", " ")}${theme.fg("text", theme.bold(group.title))}${theme.fg("
|
|
481
|
+
lines.push(`${theme.fg("muted", " ")}${theme.fg("text", theme.bold(group.title))}${theme.fg("muted", ` · ${formatTokens(groupTokens)} tokens`)}`);
|
|
461
482
|
tools.forEach((tool, index) => {
|
|
462
483
|
const tree = index === tools.length - 1 ? "└" : "├";
|
|
463
484
|
const meta = `${tool.active ? "active" : "inactive"} · ${formatToolSource(tool.source)} · ${formatCallCount(callCounts.get(tool.name) ?? 0)}`;
|
|
464
|
-
lines.push(`${theme.fg("muted", ` ${tree}─ `)}${theme.fg("text", tool.name)}: ${theme.fg("accent", formatTokens(tool.tokens))} tokens${theme.fg("
|
|
485
|
+
lines.push(`${theme.fg("muted", ` ${tree}─ `)}${theme.fg("text", tool.name)}: ${theme.fg("accent", formatTokens(tool.tokens))} tokens${theme.fg("muted", ` · ${meta}`)}`);
|
|
465
486
|
});
|
|
466
487
|
}
|
|
467
488
|
|
|
@@ -477,7 +498,7 @@ function formatDetailSection(
|
|
|
477
498
|
|
|
478
499
|
const lines = [heading(theme, title, sum(items.map((item) => item.tokens)))];
|
|
479
500
|
items.forEach((item, index) => {
|
|
480
|
-
const meta = item.meta ? theme.fg("
|
|
501
|
+
const meta = item.meta ? theme.fg("muted", ` · ${item.meta}`) : "";
|
|
481
502
|
lines.push(`${branch(theme, index === items.length - 1 ? "└" : "├")}${theme.fg("text", item.label)}: ${theme.fg("accent", formatTokens(item.tokens))} tokens${meta}`);
|
|
482
503
|
});
|
|
483
504
|
|
|
@@ -493,16 +514,6 @@ function formatCallCount(count: number): string {
|
|
|
493
514
|
return count === 1 ? "1 call" : `${count.toLocaleString()} calls`;
|
|
494
515
|
}
|
|
495
516
|
|
|
496
|
-
function collectToolCallCounts(history: Extract<ContextReport, { kind: "conversation" }>["conversation"]["history"]): Map<string, number> {
|
|
497
|
-
const counts = new Map<string, number>();
|
|
498
|
-
for (const turn of history) {
|
|
499
|
-
if (turn.kind === "tool-call") {
|
|
500
|
-
counts.set(turn.tool, (counts.get(turn.tool) ?? 0) + 1);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
return counts;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
517
|
function pushSection(lines: string[], section: string[]): void {
|
|
507
518
|
if (section.length === 0) return;
|
|
508
519
|
if (lines.length > 0) lines.push("");
|
|
@@ -510,7 +521,7 @@ function pushSection(lines: string[], section: string[]): void {
|
|
|
510
521
|
}
|
|
511
522
|
|
|
512
523
|
function heading(theme: Theme, text: string, tokens?: number | null): string {
|
|
513
|
-
const total = tokens === undefined ? "" : theme.fg("
|
|
524
|
+
const total = tokens === undefined ? "" : theme.fg("muted", ` · ${formatTokens(tokens)} tokens`);
|
|
514
525
|
return theme.fg("customMessageLabel", theme.bold(text)) + total;
|
|
515
526
|
}
|
|
516
527
|
|
|
@@ -532,8 +543,12 @@ function formatPercent(value: number | null | undefined): string {
|
|
|
532
543
|
return value === null || value === undefined ? "unknown" : `${value.toFixed(1)}%`;
|
|
533
544
|
}
|
|
534
545
|
|
|
535
|
-
function
|
|
536
|
-
|
|
546
|
+
function formatBreakdownValue(tokens: number, contextWindow: number, theme: Theme): string {
|
|
547
|
+
const tokenValue = `${theme.fg("accent", formatTokens(tokens))}${theme.fg("text", " tokens")}`;
|
|
548
|
+
const percent = contextWindow > 0
|
|
549
|
+
? theme.fg("muted", ` · ${((tokens / contextWindow) * 100).toFixed(1)}%`)
|
|
550
|
+
: "";
|
|
551
|
+
return `${tokenValue}${percent}`;
|
|
537
552
|
}
|
|
538
553
|
|
|
539
554
|
function sum(values: readonly number[]): number {
|
package/src/types.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface SnapshotDetails {
|
|
|
71
71
|
|
|
72
72
|
export interface ConversationDetails {
|
|
73
73
|
stats: ConversationStats;
|
|
74
|
-
|
|
74
|
+
toolCallCounts: Map<string, number>;
|
|
75
75
|
tokens: number;
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -79,27 +79,7 @@ export interface ConversationStats {
|
|
|
79
79
|
userMessages: number;
|
|
80
80
|
assistantMessages: number;
|
|
81
81
|
toolResults: number;
|
|
82
|
-
toolCalls: number;
|
|
83
82
|
thinkingBlocks: number;
|
|
84
83
|
imageBlocks: number;
|
|
85
84
|
compactions: number;
|
|
86
85
|
}
|
|
87
|
-
|
|
88
|
-
export type ConversationTurn =
|
|
89
|
-
| {
|
|
90
|
-
kind: "user" | "assistant" | "thinking" | "bash" | "custom" | "compact" | "branch";
|
|
91
|
-
tokens: number;
|
|
92
|
-
}
|
|
93
|
-
| {
|
|
94
|
-
kind: "tool-call";
|
|
95
|
-
tool: string;
|
|
96
|
-
tokens: number;
|
|
97
|
-
callId?: string;
|
|
98
|
-
}
|
|
99
|
-
| {
|
|
100
|
-
kind: "tool-result";
|
|
101
|
-
tool: string;
|
|
102
|
-
tokens: number;
|
|
103
|
-
callId?: string;
|
|
104
|
-
isError?: boolean;
|
|
105
|
-
};
|