@mrclrchtr/supi-context 2.6.1 → 2.7.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 +51 -57
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/package.json +8 -5
- package/src/analysis.ts +146 -118
- package/src/capacity.ts +81 -0
- package/src/context.ts +39 -27
- package/src/entry-renderer.ts +23 -0
- package/src/format-helpers.ts +3 -5
- package/src/format-sections.ts +9 -13
- package/src/format-summary.ts +37 -31
- package/src/format.ts +6 -2
- package/src/report-component.ts +4 -3
- package/src/snapshot-component.ts +75 -0
- package/src/tool/guidance.ts +5 -2
- package/src/tool/output.ts +51 -0
- package/src/tool/render.ts +42 -42
- package/src/renderer.ts +0 -15
package/src/context.ts
CHANGED
|
@@ -1,49 +1,53 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
1
2
|
import type { BuildSystemPromptOptions, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "typebox";
|
|
3
|
-
import { analyzeContext } from "./analysis.ts";
|
|
4
|
+
import { analyzeContext, analyzeContextPressure } from "./analysis.ts";
|
|
4
5
|
import { loadContextConfig } from "./config.ts";
|
|
5
|
-
import {
|
|
6
|
+
import { type ContextReportEntryData, registerContextEntryRenderer } from "./entry-renderer.ts";
|
|
6
7
|
import { registerContextSettings } from "./settings-registration.ts";
|
|
7
8
|
import { promptGuidelines, promptSnippet, toolDescription } from "./tool/guidance.ts";
|
|
9
|
+
import { serializeFullContextAnalysis } from "./tool/output.ts";
|
|
8
10
|
import {
|
|
9
11
|
type ContextToolDetails,
|
|
10
12
|
renderContextToolCall,
|
|
11
13
|
renderContextToolResult,
|
|
12
14
|
} from "./tool/render.ts";
|
|
13
|
-
|
|
15
|
+
|
|
16
|
+
const contextToolParameters = Type.Object({
|
|
17
|
+
mode: Type.Optional(
|
|
18
|
+
StringEnum(["concise", "full"] as const, {
|
|
19
|
+
description: "Omit for concise capacity data, or use full for the diagnostic report.",
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
});
|
|
14
23
|
|
|
15
24
|
export default function contextExtension(pi: ExtensionAPI) {
|
|
16
25
|
let cachedOptions: BuildSystemPromptOptions | undefined;
|
|
26
|
+
let commandRegistered = false;
|
|
17
27
|
|
|
18
|
-
// Register settings synchronously during factory
|
|
28
|
+
// Register settings synchronously during factory.
|
|
19
29
|
registerContextSettings(pi);
|
|
30
|
+
registerContextEntryRenderer(pi);
|
|
20
31
|
|
|
21
32
|
pi.on("before_agent_start", async (event) => {
|
|
22
33
|
cachedOptions = event.systemPromptOptions;
|
|
23
34
|
});
|
|
24
35
|
|
|
25
|
-
pi.on("session_start", async () => {
|
|
36
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
26
37
|
cachedOptions = undefined;
|
|
27
|
-
|
|
38
|
+
if (ctx.mode !== "tui" || commandRegistered) return;
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
content: shortContent,
|
|
39
|
-
display: true,
|
|
40
|
-
details: { analysis },
|
|
41
|
-
});
|
|
42
|
-
},
|
|
40
|
+
commandRegistered = true;
|
|
41
|
+
pi.registerCommand("supi-context", {
|
|
42
|
+
description: "Show detailed context usage. Pass 'full' to show all guideline bullets.",
|
|
43
|
+
handler: async (args, commandCtx) => {
|
|
44
|
+
const mode = args.trim() === "full" ? "full" : "preview";
|
|
45
|
+
const analysis = analyzeContext(commandCtx, pi, cachedOptions);
|
|
46
|
+
pi.appendEntry<ContextReportEntryData>("supi-context", { mode, analysis });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
43
49
|
});
|
|
44
50
|
|
|
45
|
-
registerContextRenderer(pi);
|
|
46
|
-
|
|
47
51
|
// ── supi_context agent tool (gated on config) ────────────
|
|
48
52
|
|
|
49
53
|
if (loadContextConfig(process.cwd()).agentToolEnabled) {
|
|
@@ -52,16 +56,24 @@ export default function contextExtension(pi: ExtensionAPI) {
|
|
|
52
56
|
label: "Context Usage",
|
|
53
57
|
description: toolDescription,
|
|
54
58
|
promptSnippet,
|
|
55
|
-
parameters:
|
|
59
|
+
parameters: contextToolParameters,
|
|
56
60
|
promptGuidelines,
|
|
57
61
|
renderCall: renderContextToolCall,
|
|
58
62
|
renderResult: renderContextToolResult,
|
|
59
63
|
// biome-ignore lint/complexity/useMaxParams: pi tool execute signature
|
|
60
|
-
async execute(_toolCallId,
|
|
61
|
-
|
|
64
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
65
|
+
if (params.mode !== "full") {
|
|
66
|
+
const snapshot = analyzeContextPressure(ctx);
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: JSON.stringify(snapshot) }],
|
|
69
|
+
details: { mode: "concise", snapshot } satisfies ContextToolDetails,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const analysis = analyzeContext(ctx, pi, cachedOptions);
|
|
62
74
|
return {
|
|
63
|
-
content: [{ type: "text", text:
|
|
64
|
-
details: { analysis } satisfies ContextToolDetails,
|
|
75
|
+
content: [{ type: "text", text: await serializeFullContextAnalysis(analysis) }],
|
|
76
|
+
details: { mode: "full", analysis } satisfies ContextToolDetails,
|
|
65
77
|
};
|
|
66
78
|
},
|
|
67
79
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { ContextAnalysis } from "./analysis.ts";
|
|
4
|
+
import type { ContextReportMode } from "./format.ts";
|
|
5
|
+
import { ContextReportComponent } from "./report-component.ts";
|
|
6
|
+
|
|
7
|
+
/** Durable, TUI-only payload appended by the `/supi-context` command. */
|
|
8
|
+
export interface ContextReportEntryData {
|
|
9
|
+
analysis: ContextAnalysis;
|
|
10
|
+
mode: ContextReportMode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Register the TUI renderer for new Context Usage Report custom entries. */
|
|
14
|
+
export function registerContextEntryRenderer(pi: ExtensionAPI): void {
|
|
15
|
+
pi.registerEntryRenderer<ContextReportEntryData>("supi-context", (entry, _options, theme) => {
|
|
16
|
+
const data = entry.data;
|
|
17
|
+
if (!data) {
|
|
18
|
+
return new Text(theme.fg("dim", "No context analysis data"), 1, 0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return new ContextReportComponent(data.analysis, theme, data.mode);
|
|
22
|
+
});
|
|
23
|
+
}
|
package/src/format-helpers.ts
CHANGED
|
@@ -74,11 +74,9 @@ export function allocateBlocks(values: number[], totalBlocks: number): number[]
|
|
|
74
74
|
return counts;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export function healthColor(analysis: ContextAnalysis): ReportColor {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const pressure =
|
|
81
|
-
((reserved + analysis.categories.autocompactBuffer) / analysis.contextWindow) * 100;
|
|
77
|
+
export function healthColor(analysis: Pick<ContextAnalysis, "pressurePercent">): ReportColor {
|
|
78
|
+
const pressure = analysis.pressurePercent;
|
|
79
|
+
if (pressure === null) return "dim";
|
|
82
80
|
if (pressure >= 90) return "error";
|
|
83
81
|
if (pressure >= 70) return "warning";
|
|
84
82
|
return "success";
|
package/src/format-sections.ts
CHANGED
|
@@ -112,7 +112,7 @@ export function renderInjectedFilesSection(
|
|
|
112
112
|
lines: file.lines,
|
|
113
113
|
extra: `turn ${file.turn}`,
|
|
114
114
|
})),
|
|
115
|
-
total: analysis.
|
|
115
|
+
total: analysis.usedTokens,
|
|
116
116
|
theme,
|
|
117
117
|
width,
|
|
118
118
|
});
|
|
@@ -196,6 +196,7 @@ export function renderGuidelinesSection(
|
|
|
196
196
|
analysis: ContextAnalysis,
|
|
197
197
|
theme: Theme,
|
|
198
198
|
width: number,
|
|
199
|
+
full: boolean,
|
|
199
200
|
): string[] {
|
|
200
201
|
const sourceSummary = renderSourceSummaryBar(analysis.guidelineSources);
|
|
201
202
|
|
|
@@ -217,7 +218,7 @@ export function renderGuidelinesSection(
|
|
|
217
218
|
return lines;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
lines.push(...renderBulletLines(bullets,
|
|
221
|
+
lines.push(...renderBulletLines(bullets, full, theme, width));
|
|
221
222
|
return lines;
|
|
222
223
|
}
|
|
223
224
|
|
|
@@ -225,6 +226,7 @@ export function renderToolDefinitionsSection(
|
|
|
225
226
|
analysis: ContextAnalysis,
|
|
226
227
|
theme: Theme,
|
|
227
228
|
width: number,
|
|
229
|
+
full: boolean,
|
|
228
230
|
): string[] {
|
|
229
231
|
const tools = [...analysis.toolDefinitions.tools].sort((a, b) => b.tokens - a.tokens);
|
|
230
232
|
if (tools.length === 0) return [];
|
|
@@ -241,7 +243,7 @@ export function renderToolDefinitionsSection(
|
|
|
241
243
|
),
|
|
242
244
|
);
|
|
243
245
|
|
|
244
|
-
const previewLimit =
|
|
246
|
+
const previewLimit = full ? tools.length : Math.min(5, tools.length);
|
|
245
247
|
const nameWidth = Math.max(12, Math.min(18, Math.max(...tools.map((tool) => tool.name.length))));
|
|
246
248
|
const defTokenWidth = 8;
|
|
247
249
|
const snippetTokenWidth = hasSnippetDetails ? 10 : 0;
|
|
@@ -253,7 +255,7 @@ export function renderToolDefinitionsSection(
|
|
|
253
255
|
const tool = tools[i];
|
|
254
256
|
const name = padRight(tool.name, nameWidth);
|
|
255
257
|
const previewDescription =
|
|
256
|
-
|
|
258
|
+
full || tool.description.length <= 50
|
|
257
259
|
? tool.description
|
|
258
260
|
: `${tool.description.slice(0, 50)}…`;
|
|
259
261
|
const description = truncateToWidth(previewDescription, descWidth);
|
|
@@ -270,7 +272,7 @@ export function renderToolDefinitionsSection(
|
|
|
270
272
|
);
|
|
271
273
|
}
|
|
272
274
|
|
|
273
|
-
if (!
|
|
275
|
+
if (!full && tools.length > previewLimit) {
|
|
274
276
|
lines.push(
|
|
275
277
|
formatOverflowHint(tools.length - previewLimit, theme, width, {
|
|
276
278
|
hint: "run /supi-context full",
|
|
@@ -293,14 +295,8 @@ export function renderCompactionNote(
|
|
|
293
295
|
theme: Theme,
|
|
294
296
|
width: number,
|
|
295
297
|
): string[] {
|
|
296
|
-
if (!analysis.
|
|
297
|
-
return [
|
|
298
|
-
formatDimLine(
|
|
299
|
-
`↳ ${pluralize(analysis.compaction.summarizedTurns, "older turn", "older turns")} summarized (compaction)`,
|
|
300
|
-
theme,
|
|
301
|
-
width,
|
|
302
|
-
),
|
|
303
|
-
];
|
|
298
|
+
if (!analysis.compacted) return [];
|
|
299
|
+
return [formatDimLine("↳ Compaction present on the active branch", theme, width)];
|
|
304
300
|
}
|
|
305
301
|
|
|
306
302
|
export function renderProviderSections(
|
package/src/format-summary.ts
CHANGED
|
@@ -15,13 +15,27 @@ import {
|
|
|
15
15
|
} from "./format-helpers.ts";
|
|
16
16
|
import { formatTokens } from "./utils.ts";
|
|
17
17
|
|
|
18
|
+
function formatPercentage(value: number | null): string {
|
|
19
|
+
return value === null ? "?" : `${value.toFixed(1)}%`;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
export function renderSummary(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
19
|
-
const used = analysis.totalTokens ?? 0;
|
|
20
23
|
const health = theme.fg(healthColor(analysis), "●");
|
|
21
24
|
const usage =
|
|
22
|
-
analysis.contextWindow
|
|
23
|
-
? `${formatTokens(
|
|
24
|
-
: `${formatTokens(
|
|
25
|
+
analysis.contextWindow !== null
|
|
26
|
+
? `${formatTokens(analysis.usedTokens)} / ${formatTokens(analysis.contextWindow)} tokens (${formatPercentage(analysis.usagePercent)} usage)`
|
|
27
|
+
: `${formatTokens(analysis.usedTokens)} tokens`;
|
|
28
|
+
const capacityParts: string[] = [];
|
|
29
|
+
|
|
30
|
+
if (analysis.headroomTokens !== null) {
|
|
31
|
+
capacityParts.push(`Headroom ${formatTokens(analysis.headroomTokens)}`);
|
|
32
|
+
}
|
|
33
|
+
if (analysis.compactionEnabled) {
|
|
34
|
+
capacityParts.push(`Compaction reserve ${formatTokens(analysis.reserveTokens)}`);
|
|
35
|
+
}
|
|
36
|
+
if (analysis.pressurePercent !== null) {
|
|
37
|
+
capacityParts.push(`Pressure ${formatPercentage(analysis.pressurePercent)}`);
|
|
38
|
+
}
|
|
25
39
|
|
|
26
40
|
const lines = [
|
|
27
41
|
truncateToWidth(
|
|
@@ -30,6 +44,9 @@ export function renderSummary(analysis: ContextAnalysis, theme: Theme, width: nu
|
|
|
30
44
|
),
|
|
31
45
|
];
|
|
32
46
|
|
|
47
|
+
if (capacityParts.length > 0) {
|
|
48
|
+
lines.push(...wrapReportText(theme.fg("dim", capacityParts.join(" · ")), width));
|
|
49
|
+
}
|
|
33
50
|
if (analysis.approximationNote) {
|
|
34
51
|
lines.push(...wrapReportText(theme.fg("warning", analysis.approximationNote), width));
|
|
35
52
|
}
|
|
@@ -38,16 +55,16 @@ export function renderSummary(analysis: ContextAnalysis, theme: Theme, width: nu
|
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
export function renderUsageBar(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
41
|
-
if (analysis.contextWindow
|
|
58
|
+
if (analysis.contextWindow === null) {
|
|
42
59
|
return [theme.fg("dim", "No model selected — usage bar unavailable")];
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
const percentLabel =
|
|
62
|
+
const percentLabel = formatPercentage(analysis.usagePercent);
|
|
46
63
|
const barWidth = Math.max(12, Math.min(48, width - visibleWidth(percentLabel) - 3));
|
|
47
64
|
const values = [
|
|
48
65
|
...CATEGORY_ORDER.map((key) => analysis.categories[key]),
|
|
49
|
-
analysis.
|
|
50
|
-
analysis.
|
|
66
|
+
analysis.reserveTokens,
|
|
67
|
+
analysis.headroomTokens ?? 0,
|
|
51
68
|
];
|
|
52
69
|
const counts = allocateBlocks(values, barWidth);
|
|
53
70
|
|
|
@@ -85,11 +102,11 @@ export function renderUsageBar(analysis: ContextAnalysis, theme: Theme, width: n
|
|
|
85
102
|
if (analysis.categories[key] <= 0) continue;
|
|
86
103
|
legendParts.push(`${theme.fg(CATEGORY_COLORS[key], "●")} ${CATEGORY_LABELS[key]}`);
|
|
87
104
|
}
|
|
88
|
-
if (analysis.
|
|
89
|
-
legendParts.push(`${theme.fg("warning", "▒")}
|
|
105
|
+
if (analysis.reserveTokens > 0) {
|
|
106
|
+
legendParts.push(`${theme.fg("warning", "▒")} Compaction reserve`);
|
|
90
107
|
}
|
|
91
|
-
if (analysis.
|
|
92
|
-
legendParts.push(`${theme.fg("dim", "░")}
|
|
108
|
+
if (analysis.headroomTokens !== null && analysis.headroomTokens > 0) {
|
|
109
|
+
legendParts.push(`${theme.fg("dim", "░")} Headroom`);
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
return [barLine, ...wrapReportText(legendParts.join(theme.fg("dim", " • ")), width)];
|
|
@@ -107,31 +124,20 @@ export function renderCategoryBreakdown(
|
|
|
107
124
|
label: string;
|
|
108
125
|
color: Parameters<Theme["fg"]>[0];
|
|
109
126
|
tokens: number;
|
|
110
|
-
}> =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
})),
|
|
116
|
-
{
|
|
117
|
-
label: "Autocompact buffer",
|
|
118
|
-
color: "warning",
|
|
119
|
-
tokens: analysis.categories.autocompactBuffer,
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
label: "Free space",
|
|
123
|
-
color: "dim",
|
|
124
|
-
tokens: analysis.categories.freeSpace,
|
|
125
|
-
},
|
|
126
|
-
];
|
|
127
|
+
}> = CATEGORY_ORDER.map((key) => ({
|
|
128
|
+
label: CATEGORY_LABELS[key],
|
|
129
|
+
color: CATEGORY_COLORS[key],
|
|
130
|
+
tokens: analysis.categories[key],
|
|
131
|
+
}));
|
|
127
132
|
|
|
128
133
|
const labelWidth = Math.max(18, Math.min(22, width - 22));
|
|
134
|
+
const total = analysis.contextWindow ?? 0;
|
|
129
135
|
for (const row of rows) {
|
|
130
|
-
if (row.tokens <= 0
|
|
136
|
+
if (row.tokens <= 0) continue;
|
|
131
137
|
const bullet = theme.fg(row.color, "●");
|
|
132
138
|
const label = padRight(row.label, labelWidth);
|
|
133
139
|
const tokens = padLeft(formatTokens(row.tokens), 8);
|
|
134
|
-
const percentage = padLeft(pct(row.tokens,
|
|
140
|
+
const percentage = padLeft(pct(row.tokens, total), 7);
|
|
135
141
|
lines.push(truncateToWidth(` ${bullet} ${label} ${tokens} ${percentage}`, width));
|
|
136
142
|
}
|
|
137
143
|
|
package/src/format.ts
CHANGED
|
@@ -18,10 +18,14 @@ import {
|
|
|
18
18
|
renderUsageBar,
|
|
19
19
|
} from "./format-summary.ts";
|
|
20
20
|
|
|
21
|
+
/** Presentation mode for a Context Usage Report. */
|
|
22
|
+
export type ContextReportMode = "preview" | "full";
|
|
23
|
+
|
|
21
24
|
export function formatContextReport(
|
|
22
25
|
analysis: ContextAnalysis,
|
|
23
26
|
theme: Theme,
|
|
24
27
|
width = 200,
|
|
28
|
+
mode: ContextReportMode = "preview",
|
|
25
29
|
): string[] {
|
|
26
30
|
const safeWidth = clampReportWidth(width);
|
|
27
31
|
const lines: string[] = [];
|
|
@@ -41,8 +45,8 @@ export function formatContextReport(
|
|
|
41
45
|
renderContextFilesSection(analysis, theme, safeWidth),
|
|
42
46
|
renderInjectedFilesSection(analysis, theme, safeWidth),
|
|
43
47
|
renderSkillsSection(analysis, theme, safeWidth),
|
|
44
|
-
renderGuidelinesSection(analysis, theme, safeWidth),
|
|
45
|
-
renderToolDefinitionsSection(analysis, theme, safeWidth),
|
|
48
|
+
renderGuidelinesSection(analysis, theme, safeWidth, mode === "full"),
|
|
49
|
+
renderToolDefinitionsSection(analysis, theme, safeWidth, mode === "full"),
|
|
46
50
|
renderCompactionNote(analysis, theme, safeWidth),
|
|
47
51
|
renderProviderSections(analysis, theme, safeWidth),
|
|
48
52
|
].filter((section) => section.length > 0);
|
package/src/report-component.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ContextAnalysis } from "./analysis.ts";
|
|
3
|
-
import { formatContextReport } from "./format.ts";
|
|
3
|
+
import { type ContextReportMode, formatContextReport } from "./format.ts";
|
|
4
4
|
|
|
5
|
-
/** Width-aware
|
|
5
|
+
/** Width-aware Context Usage Report shared by custom-entry and tool renderers. */
|
|
6
6
|
export class ContextReportComponent {
|
|
7
7
|
private cachedWidth?: number;
|
|
8
8
|
private cachedLines?: string[];
|
|
@@ -10,6 +10,7 @@ export class ContextReportComponent {
|
|
|
10
10
|
constructor(
|
|
11
11
|
private readonly analysis: ContextAnalysis,
|
|
12
12
|
private readonly theme: Theme,
|
|
13
|
+
private readonly mode: ContextReportMode = "preview",
|
|
13
14
|
) {}
|
|
14
15
|
|
|
15
16
|
render(width: number): string[] {
|
|
@@ -17,7 +18,7 @@ export class ContextReportComponent {
|
|
|
17
18
|
return this.cachedLines;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const lines = formatContextReport(this.analysis, this.theme, width);
|
|
21
|
+
const lines = formatContextReport(this.analysis, this.theme, width, this.mode);
|
|
21
22
|
this.cachedWidth = width;
|
|
22
23
|
this.cachedLines = lines;
|
|
23
24
|
return lines;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { ContextPressureSnapshot } from "./capacity.ts";
|
|
4
|
+
import { healthColor } from "./format-helpers.ts";
|
|
5
|
+
import { formatTokens } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
function formatPercentage(value: number | null): string {
|
|
8
|
+
return value === null ? "unavailable" : `${value.toFixed(1)}%`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatTokenValue(value: number | null): string {
|
|
12
|
+
return value === null ? "unavailable" : formatTokens(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Width-safe TUI renderer for the small Context Pressure Snapshot.
|
|
17
|
+
*
|
|
18
|
+
* The collapsed view is intentionally one dense line. Expansion exposes every
|
|
19
|
+
* snapshot field without falling back to the raw JSON returned to the agent.
|
|
20
|
+
*/
|
|
21
|
+
export class ContextPressureComponent implements Component {
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly snapshot: ContextPressureSnapshot,
|
|
24
|
+
private readonly theme: Theme,
|
|
25
|
+
private readonly expanded: boolean,
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
render(width: number): string[] {
|
|
29
|
+
return this.expanded ? this.renderExpanded(width) : [this.renderCollapsed(width)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
invalidate(): void {}
|
|
33
|
+
|
|
34
|
+
private renderCollapsed(width: number): string {
|
|
35
|
+
const usage =
|
|
36
|
+
this.snapshot.contextWindow === null
|
|
37
|
+
? `${formatTokens(this.snapshot.usedTokens)} used`
|
|
38
|
+
: `${formatTokens(this.snapshot.usedTokens)}/${formatTokens(this.snapshot.contextWindow)} used`;
|
|
39
|
+
const parts = [
|
|
40
|
+
`${this.theme.fg(healthColor(this.snapshot), "●")} ${this.theme.fg("text", usage)}`,
|
|
41
|
+
`${this.theme.fg("dim", "headroom")} ${this.theme.fg("muted", formatTokenValue(this.snapshot.headroomTokens))}`,
|
|
42
|
+
`${this.theme.fg("dim", "pressure")} ${this.theme.fg("muted", formatPercentage(this.snapshot.pressurePercent))}`,
|
|
43
|
+
this.theme.fg("muted", this.snapshot.modelName),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
return truncateToWidth(parts.join(` ${this.theme.fg("dim", "·")} `), width);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private renderExpanded(width: number): string[] {
|
|
50
|
+
const values: Array<[string, string]> = [
|
|
51
|
+
["Model", this.snapshot.modelName],
|
|
52
|
+
["Context window", formatTokenValue(this.snapshot.contextWindow)],
|
|
53
|
+
["Used", formatTokens(this.snapshot.usedTokens)],
|
|
54
|
+
["Usage", formatPercentage(this.snapshot.usagePercent)],
|
|
55
|
+
["Auto-compaction", this.snapshot.compactionEnabled ? "enabled" : "disabled"],
|
|
56
|
+
["Compaction reserve", formatTokens(this.snapshot.reserveTokens)],
|
|
57
|
+
["Headroom", formatTokenValue(this.snapshot.headroomTokens)],
|
|
58
|
+
["Pressure", formatPercentage(this.snapshot.pressurePercent)],
|
|
59
|
+
["Compacted", this.snapshot.compacted ? "yes" : "no"],
|
|
60
|
+
];
|
|
61
|
+
const labelWidth = Math.max(8, Math.min(18, width - 12));
|
|
62
|
+
const lines = values.map(([label, value]) =>
|
|
63
|
+
truncateToWidth(
|
|
64
|
+
`${this.theme.fg("dim", `${label.padEnd(labelWidth)} `)}${this.theme.fg("text", value)}`,
|
|
65
|
+
width,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (this.snapshot.approximationNote) {
|
|
70
|
+
lines.push(truncateToWidth(this.theme.fg("warning", this.snapshot.approximationNote), width));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/tool/guidance.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
1
3
|
// Prompt guidance and tool description for the supi_context agent tool.
|
|
2
4
|
|
|
3
5
|
export const toolDescription =
|
|
4
|
-
"Report current
|
|
6
|
+
"Report current context capacity. Omit mode for a concise, constant-shape pressure snapshot; use mode: full only when diagnostic attribution is needed. Full output is compact JSON and is replaced with a temporary-file envelope above " +
|
|
7
|
+
`${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}.`;
|
|
5
8
|
|
|
6
9
|
export const promptSnippet =
|
|
7
|
-
"supi_context — context
|
|
10
|
+
"supi_context — concise context-pressure snapshot (mode: full for diagnostics)";
|
|
8
11
|
|
|
9
12
|
export const promptGuidelines = [
|
|
10
13
|
"Use supi_context before large operations or when context usage is near the limit.",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_MAX_BYTES,
|
|
6
|
+
DEFAULT_MAX_LINES,
|
|
7
|
+
truncateHead,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { ContextAnalysis } from "../analysis.ts";
|
|
10
|
+
|
|
11
|
+
interface TruncatedOutputEnvelope {
|
|
12
|
+
truncated: true;
|
|
13
|
+
fullOutputPath: string;
|
|
14
|
+
totalLines: number;
|
|
15
|
+
totalBytes: number;
|
|
16
|
+
maxLines: number;
|
|
17
|
+
maxBytes: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serialize a diagnostic report without ever returning invalid partial JSON.
|
|
22
|
+
*
|
|
23
|
+
* When Pi's normal tool-output limits would truncate the report, the complete
|
|
24
|
+
* compact JSON is stored in a temporary file and the agent receives a small,
|
|
25
|
+
* valid JSON envelope pointing to it instead.
|
|
26
|
+
*/
|
|
27
|
+
export async function serializeFullContextAnalysis(analysis: ContextAnalysis): Promise<string> {
|
|
28
|
+
const fullJson = JSON.stringify(analysis);
|
|
29
|
+
const truncation = truncateHead(fullJson, {
|
|
30
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
31
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!truncation.truncated) {
|
|
35
|
+
return fullJson;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const outputDir = await mkdtemp(join(tmpdir(), "supi-context-"));
|
|
39
|
+
const fullOutputPath = join(outputDir, "context-usage-report.json");
|
|
40
|
+
await writeFile(fullOutputPath, fullJson, "utf8");
|
|
41
|
+
|
|
42
|
+
const envelope: TruncatedOutputEnvelope = {
|
|
43
|
+
truncated: true,
|
|
44
|
+
fullOutputPath,
|
|
45
|
+
totalLines: truncation.totalLines,
|
|
46
|
+
totalBytes: truncation.totalBytes,
|
|
47
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
48
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
49
|
+
};
|
|
50
|
+
return JSON.stringify(envelope);
|
|
51
|
+
}
|
package/src/tool/render.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { ContextAnalysis } from "../analysis.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { type ContextPressureSnapshot, createContextPressureSnapshot } from "../capacity.ts";
|
|
5
5
|
import { ContextReportComponent } from "../report-component.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { ContextPressureComponent } from "../snapshot-component.ts";
|
|
7
7
|
|
|
8
|
-
export interface
|
|
8
|
+
export interface ContextToolConciseDetails {
|
|
9
|
+
mode: "concise";
|
|
10
|
+
snapshot: ContextPressureSnapshot;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ContextToolFullDetails {
|
|
14
|
+
mode: "full";
|
|
9
15
|
analysis: ContextAnalysis;
|
|
10
16
|
}
|
|
11
17
|
|
|
18
|
+
/** TUI data mirrors the mode-specific data returned to the agent. */
|
|
19
|
+
export type ContextToolDetails = ContextToolConciseDetails | ContextToolFullDetails;
|
|
20
|
+
|
|
12
21
|
interface ContextToolResult {
|
|
13
22
|
content: Array<{ type: string; text?: string }>;
|
|
14
23
|
details?: unknown;
|
|
@@ -20,8 +29,15 @@ interface ResultOptions {
|
|
|
20
29
|
isPartial: boolean;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
export function renderContextToolCall(
|
|
24
|
-
const
|
|
32
|
+
export function renderContextToolCall(args: unknown, theme: Theme): Text {
|
|
33
|
+
const mode =
|
|
34
|
+
args &&
|
|
35
|
+
typeof args === "object" &&
|
|
36
|
+
"mode" in args &&
|
|
37
|
+
(args as { mode?: string }).mode === "full"
|
|
38
|
+
? "full"
|
|
39
|
+
: "concise";
|
|
40
|
+
const content = `${theme.fg("toolTitle", "supi_context")} ${theme.fg("muted", mode)}`;
|
|
25
41
|
return new Text(content, 0, 0);
|
|
26
42
|
}
|
|
27
43
|
|
|
@@ -29,7 +45,7 @@ export function renderContextToolResult(
|
|
|
29
45
|
result: ContextToolResult | undefined,
|
|
30
46
|
options: ResultOptions,
|
|
31
47
|
theme: Theme,
|
|
32
|
-
): Text | ContextReportComponent {
|
|
48
|
+
): Text | ContextPressureComponent | ContextReportComponent {
|
|
33
49
|
if (options.isPartial) {
|
|
34
50
|
return new Text(theme.fg("warning", "Analyzing context…"), 0, 0);
|
|
35
51
|
}
|
|
@@ -38,53 +54,37 @@ export function renderContextToolResult(
|
|
|
38
54
|
return new Text(theme.fg("error", "supi_context failed"), 0, 0);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
57
|
+
const details = extractDetails(result?.details);
|
|
58
|
+
if (!details) {
|
|
43
59
|
return new Text(theme.fg("dim", "No context analysis data"), 0, 0);
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
if (details.mode === "concise") {
|
|
63
|
+
return new ContextPressureComponent(details.snapshot, theme, options.expanded);
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
if (options.expanded) {
|
|
47
|
-
return new ContextReportComponent(analysis, theme);
|
|
67
|
+
return new ContextReportComponent(details.analysis, theme, "full");
|
|
48
68
|
}
|
|
49
69
|
|
|
50
|
-
return new
|
|
70
|
+
return new ContextPressureComponent(
|
|
71
|
+
createContextPressureSnapshot(details.analysis.modelName, details.analysis),
|
|
72
|
+
theme,
|
|
73
|
+
false,
|
|
74
|
+
);
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
function
|
|
54
|
-
if (!details || typeof details !== "object" || !("
|
|
77
|
+
function extractDetails(details: unknown): ContextToolDetails | undefined {
|
|
78
|
+
if (!details || typeof details !== "object" || !("mode" in details)) {
|
|
55
79
|
return undefined;
|
|
56
80
|
}
|
|
57
|
-
return (details as ContextToolDetails).analysis;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function formatCollapsedSummary(analysis: ContextAnalysis, theme: Theme): string {
|
|
61
|
-
const used = analysis.totalTokens ?? 0;
|
|
62
|
-
const usage =
|
|
63
|
-
analysis.contextWindow > 0
|
|
64
|
-
? `${formatTokens(used)} / ${formatTokens(analysis.contextWindow)} (${pct(used, analysis.contextWindow)})`
|
|
65
|
-
: `${formatTokens(used)} tokens`;
|
|
66
|
-
const dot = theme.fg("dim", "·");
|
|
67
|
-
const parts = [
|
|
68
|
-
`${theme.fg(healthColor(analysis), "●")} ${theme.fg("dim", "usage")} ${theme.fg("text", theme.bold(usage))}`,
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
if (analysis.contextWindow > 0) {
|
|
72
|
-
parts.push(
|
|
73
|
-
`${theme.fg("dim", "free")} ${theme.fg("muted", formatTokens(analysis.categories.freeSpace))}`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
);
|
|
82
|
+
const mode = (details as { mode?: unknown }).mode;
|
|
83
|
+
if (mode === "concise" && "snapshot" in details) {
|
|
84
|
+
return details as ContextToolConciseDetails;
|
|
81
85
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!analysis.approximationNote) {
|
|
86
|
-
return parts.join(` ${dot} `);
|
|
86
|
+
if (mode === "full" && "analysis" in details) {
|
|
87
|
+
return details as ContextToolFullDetails;
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
return `${parts.join(` ${dot} `)}\n${theme.fg("warning", analysis.approximationNote)}`;
|
|
89
|
+
return undefined;
|
|
90
90
|
}
|