@mrclrchtr/supi-context 2.6.1 → 2.8.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/README.md +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
- 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/analysis.ts
CHANGED
|
@@ -5,13 +5,18 @@ import {
|
|
|
5
5
|
buildSessionContext,
|
|
6
6
|
type ExtensionAPI,
|
|
7
7
|
type ExtensionContext,
|
|
8
|
-
|
|
8
|
+
estimateTokens,
|
|
9
9
|
formatSkillsForPrompt,
|
|
10
10
|
getLatestCompactionEntry,
|
|
11
11
|
SettingsManager,
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { getRegisteredContextProviders } from "@mrclrchtr/supi-core/context";
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
analyzeContextCapacity,
|
|
17
|
+
type ContextPressureSnapshot,
|
|
18
|
+
createContextPressureSnapshot,
|
|
19
|
+
} from "./capacity.ts";
|
|
15
20
|
import { deriveOptionsFromSystemPrompt, extractGuidelinesSection } from "./prompt-inference.ts";
|
|
16
21
|
|
|
17
22
|
type AgentMessage = Parameters<typeof estimateTokens>[0];
|
|
@@ -69,17 +74,9 @@ export interface ContextProviderSection {
|
|
|
69
74
|
data: Record<string, string | number>;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
export interface ContextAnalysis {
|
|
73
|
-
modelName: string;
|
|
74
|
-
contextWindow: number;
|
|
75
|
-
totalTokens: number | null;
|
|
77
|
+
export interface ContextAnalysis extends ContextPressureSnapshot {
|
|
76
78
|
scaled: boolean;
|
|
77
|
-
|
|
78
|
-
full: boolean;
|
|
79
|
-
categories: CategoryTokens & {
|
|
80
|
-
autocompactBuffer: number;
|
|
81
|
-
freeSpace: number;
|
|
82
|
-
};
|
|
79
|
+
categories: CategoryTokens;
|
|
83
80
|
systemPromptBreakdown: {
|
|
84
81
|
base: number;
|
|
85
82
|
instructionFiles: ContextFileInfo[];
|
|
@@ -98,7 +95,6 @@ export interface ContextAnalysis {
|
|
|
98
95
|
guidelineSources: GuidelineSourceInfo[];
|
|
99
96
|
toolSnippetDetails: ToolSnippetInfo[];
|
|
100
97
|
toolDefinitions: { count: number; tokens: number; tools: ToolInfo[] };
|
|
101
|
-
compaction: { summarizedTurns: number } | null;
|
|
102
98
|
providerSections: ContextProviderSection[];
|
|
103
99
|
}
|
|
104
100
|
|
|
@@ -106,39 +102,6 @@ export function estimateTextTokens(text: string): number {
|
|
|
106
102
|
return Math.ceil(text.length / 4);
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
function estimateGenericContent(content: unknown): number {
|
|
110
|
-
if (typeof content === "string") {
|
|
111
|
-
return estimateTextTokens(content);
|
|
112
|
-
}
|
|
113
|
-
if (Array.isArray(content)) {
|
|
114
|
-
let chars = 0;
|
|
115
|
-
for (const block of content as Array<{ type?: string; text?: string }>) {
|
|
116
|
-
if (block.type === "text" && block.text) {
|
|
117
|
-
chars += block.text.length;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return Math.ceil(chars / 4);
|
|
121
|
-
}
|
|
122
|
-
return 0;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function estimateUserMessage(msg: Extract<AgentMessage, { role: "user" }>): number {
|
|
126
|
-
const content = msg.content;
|
|
127
|
-
if (typeof content === "string") {
|
|
128
|
-
return estimateTextTokens(content);
|
|
129
|
-
}
|
|
130
|
-
if (Array.isArray(content)) {
|
|
131
|
-
let chars = 0;
|
|
132
|
-
for (const block of content) {
|
|
133
|
-
if (block.type === "text" && block.text) {
|
|
134
|
-
chars += block.text.length;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return Math.ceil(chars / 4);
|
|
138
|
-
}
|
|
139
|
-
return 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
105
|
function estimateAssistantMessage(msg: Extract<AgentMessage, { role: "assistant" }>): {
|
|
143
106
|
text: number;
|
|
144
107
|
toolCalls: number;
|
|
@@ -169,7 +132,7 @@ function estimateMessageByCategory(msg: AgentMessage): {
|
|
|
169
132
|
} {
|
|
170
133
|
if (msg.role === "user") {
|
|
171
134
|
return {
|
|
172
|
-
user:
|
|
135
|
+
user: estimateTokens(msg),
|
|
173
136
|
assistantText: 0,
|
|
174
137
|
toolCalls: 0,
|
|
175
138
|
toolResult: 0,
|
|
@@ -185,7 +148,7 @@ function estimateMessageByCategory(msg: AgentMessage): {
|
|
|
185
148
|
user: 0,
|
|
186
149
|
assistantText: 0,
|
|
187
150
|
toolCalls: 0,
|
|
188
|
-
toolResult:
|
|
151
|
+
toolResult: estimateTokens(msg),
|
|
189
152
|
other: 0,
|
|
190
153
|
};
|
|
191
154
|
}
|
|
@@ -194,10 +157,14 @@ function estimateMessageByCategory(msg: AgentMessage): {
|
|
|
194
157
|
assistantText: 0,
|
|
195
158
|
toolCalls: 0,
|
|
196
159
|
toolResult: 0,
|
|
197
|
-
other:
|
|
160
|
+
other: estimateTokens(msg),
|
|
198
161
|
};
|
|
199
162
|
}
|
|
200
163
|
|
|
164
|
+
function estimateMessageTokens(msg: AgentMessage): number {
|
|
165
|
+
return estimateTokens(msg);
|
|
166
|
+
}
|
|
167
|
+
|
|
201
168
|
function computeMessageCategories(messages: AgentMessage[]): CategoryTokens {
|
|
202
169
|
const categories: CategoryTokens = {
|
|
203
170
|
systemPrompt: 0,
|
|
@@ -220,6 +187,26 @@ function computeMessageCategories(messages: AgentMessage[]): CategoryTokens {
|
|
|
220
187
|
return categories;
|
|
221
188
|
}
|
|
222
189
|
|
|
190
|
+
interface ScalingResult {
|
|
191
|
+
categories: CategoryTokens;
|
|
192
|
+
scaled: boolean;
|
|
193
|
+
approximationNote: string | null;
|
|
194
|
+
usedTokens: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
type CurrentContextUsage = ReturnType<ExtensionContext["getContextUsage"]>;
|
|
198
|
+
|
|
199
|
+
function hasMeasuredTokens(tokens: number | null | undefined): tokens is number {
|
|
200
|
+
return typeof tokens === "number" && tokens > 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getApproximationNote(contextUsage: CurrentContextUsage): string | null {
|
|
204
|
+
if (contextUsage === undefined) return "Approximate (no usage data available)";
|
|
205
|
+
return hasMeasuredTokens(contextUsage.tokens)
|
|
206
|
+
? null
|
|
207
|
+
: "Token count pending — send a message to refresh";
|
|
208
|
+
}
|
|
209
|
+
|
|
223
210
|
function applyScaling(
|
|
224
211
|
categories: CategoryTokens,
|
|
225
212
|
actualTokens: number | null,
|
|
@@ -227,20 +214,13 @@ function applyScaling(
|
|
|
227
214
|
contextUsage:
|
|
228
215
|
| { tokens: number | null; contextWindow: number; percent: number | null }
|
|
229
216
|
| undefined,
|
|
230
|
-
): {
|
|
231
|
-
categories: CategoryTokens;
|
|
232
|
-
scaled: boolean;
|
|
233
|
-
approximationNote: string | null;
|
|
234
|
-
totalTokens: number;
|
|
235
|
-
} {
|
|
217
|
+
): ScalingResult {
|
|
236
218
|
let scaled = false;
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
const
|
|
219
|
+
const hasActualTotal = hasMeasuredTokens(actualTokens);
|
|
220
|
+
const usedTokens = hasActualTotal ? actualTokens : rawTotal;
|
|
221
|
+
const approximationNote = getApproximationNote(contextUsage);
|
|
240
222
|
|
|
241
|
-
if (
|
|
242
|
-
approximationNote = "Approximate (no usage data available)";
|
|
243
|
-
} else if (hasActualTotal && rawTotal > 0) {
|
|
223
|
+
if (hasActualTotal && rawTotal > 0) {
|
|
244
224
|
const scale = actualTokens / rawTotal;
|
|
245
225
|
categories.systemPrompt = Math.round(categories.systemPrompt * scale);
|
|
246
226
|
categories.userMessages = Math.round(categories.userMessages * scale);
|
|
@@ -249,11 +229,9 @@ function applyScaling(
|
|
|
249
229
|
categories.toolResults = Math.round(categories.toolResults * scale);
|
|
250
230
|
categories.other = Math.round(categories.other * scale);
|
|
251
231
|
scaled = true;
|
|
252
|
-
} else if (actualTokens === null || actualTokens === 0) {
|
|
253
|
-
approximationNote = "Token count pending — send a message to refresh";
|
|
254
232
|
}
|
|
255
233
|
|
|
256
|
-
return { categories, scaled, approximationNote,
|
|
234
|
+
return { categories, scaled, approximationNote, usedTokens };
|
|
257
235
|
}
|
|
258
236
|
|
|
259
237
|
/**
|
|
@@ -508,18 +486,10 @@ function computeToolDefinitions(pi: ExtensionAPI): {
|
|
|
508
486
|
};
|
|
509
487
|
}
|
|
510
488
|
|
|
511
|
-
function
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (!compactionEntry) return null;
|
|
516
|
-
|
|
517
|
-
const index = branch.findIndex((e) => e.id === compactionEntry.id);
|
|
518
|
-
const messagesBefore = branch
|
|
519
|
-
.slice(0, Math.max(0, index))
|
|
520
|
-
.filter((e) => e.type === "message").length;
|
|
521
|
-
const summarizedTurns = Math.floor(messagesBefore / 2);
|
|
522
|
-
return { summarizedTurns };
|
|
489
|
+
function hasCompactionOnActiveBranch(
|
|
490
|
+
branch: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>,
|
|
491
|
+
): boolean {
|
|
492
|
+
return getLatestCompactionEntry(branch) !== null;
|
|
523
493
|
}
|
|
524
494
|
|
|
525
495
|
export function extractInjectedContextFiles(messages: AgentMessage[]): InjectedFileInfo[] {
|
|
@@ -556,22 +526,93 @@ export function extractInjectedContextFiles(messages: AgentMessage[]): InjectedF
|
|
|
556
526
|
return Array.from(seen.values()).sort((a, b) => a.turn - b.turn || a.file.localeCompare(b.file));
|
|
557
527
|
}
|
|
558
528
|
|
|
559
|
-
|
|
529
|
+
interface ContextFallback {
|
|
530
|
+
messages: AgentMessage[];
|
|
531
|
+
systemPromptText: string;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
interface CapacityObservation {
|
|
535
|
+
branch: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>;
|
|
536
|
+
contextUsage: CurrentContextUsage;
|
|
537
|
+
snapshot: ContextPressureSnapshot;
|
|
538
|
+
fallback?: ContextFallback;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
interface ContextObservation extends ContextFallback {
|
|
542
|
+
scaling: ScalingResult;
|
|
543
|
+
snapshot: ContextPressureSnapshot;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function estimateContextTokens(fallback: ContextFallback): number {
|
|
547
|
+
return (
|
|
548
|
+
estimateTextTokens(fallback.systemPromptText) +
|
|
549
|
+
fallback.messages.reduce((total, message) => total + estimateMessageTokens(message), 0)
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function collectContextFallback(
|
|
560
554
|
ctx: ExtensionContext,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
555
|
+
branch: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>,
|
|
556
|
+
): ContextFallback {
|
|
557
|
+
return {
|
|
558
|
+
messages: buildSessionContext(branch).messages,
|
|
559
|
+
systemPromptText: ctx.getSystemPrompt(),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Observe the small shared capacity seam. It only walks messages when Pi has
|
|
565
|
+
* no measured usage total and an aggregate estimate is genuinely necessary.
|
|
566
|
+
*/
|
|
567
|
+
function observeCapacity(ctx: ExtensionContext): CapacityObservation {
|
|
565
568
|
const branch = ctx.sessionManager.getBranch();
|
|
566
|
-
const apiView = buildSessionContext(branch);
|
|
567
569
|
const contextUsage = ctx.getContextUsage();
|
|
568
|
-
|
|
569
|
-
|
|
570
|
+
let fallback: ContextFallback | undefined;
|
|
571
|
+
let usedTokens: number;
|
|
572
|
+
if (hasMeasuredTokens(contextUsage?.tokens)) {
|
|
573
|
+
usedTokens = contextUsage.tokens;
|
|
574
|
+
} else {
|
|
575
|
+
fallback = collectContextFallback(ctx, branch);
|
|
576
|
+
usedTokens = estimateContextTokens(fallback);
|
|
577
|
+
}
|
|
578
|
+
const settings = SettingsManager.create(ctx.cwd, undefined, {
|
|
579
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
580
|
+
});
|
|
581
|
+
const capacity = analyzeContextCapacity({
|
|
582
|
+
contextWindow: contextUsage?.contextWindow ?? null,
|
|
583
|
+
usedTokens,
|
|
584
|
+
compactionEnabled: settings.getCompactionEnabled(),
|
|
585
|
+
configuredReserveTokens: settings.getCompactionReserveTokens(),
|
|
586
|
+
compacted: hasCompactionOnActiveBranch(branch),
|
|
587
|
+
approximationNote: getApproximationNote(contextUsage),
|
|
588
|
+
});
|
|
570
589
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
590
|
+
return {
|
|
591
|
+
branch,
|
|
592
|
+
contextUsage,
|
|
593
|
+
fallback,
|
|
594
|
+
snapshot: createContextPressureSnapshot(
|
|
595
|
+
ctx.model?.name ?? ctx.model?.id ?? "No model selected",
|
|
596
|
+
capacity,
|
|
597
|
+
),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Return a constant-shape Context Pressure Snapshot without diagnostic attribution. */
|
|
602
|
+
export function analyzeContextPressure(ctx: ExtensionContext): ContextPressureSnapshot {
|
|
603
|
+
return observeCapacity(ctx).snapshot;
|
|
604
|
+
}
|
|
574
605
|
|
|
606
|
+
/** Compose a full Context Usage Report from shared capacity and diagnostic attribution. */
|
|
607
|
+
export function analyzeContext(
|
|
608
|
+
ctx: ExtensionContext,
|
|
609
|
+
pi: ExtensionAPI,
|
|
610
|
+
cachedOptions: BuildSystemPromptOptions | undefined,
|
|
611
|
+
): ContextAnalysis {
|
|
612
|
+
const capacity = observeCapacity(ctx);
|
|
613
|
+
const fallback = capacity.fallback ?? collectContextFallback(ctx, capacity.branch);
|
|
614
|
+
const categories = computeMessageCategories(fallback.messages);
|
|
615
|
+
categories.systemPrompt = estimateTextTokens(fallback.systemPromptText);
|
|
575
616
|
const rawTotal =
|
|
576
617
|
categories.systemPrompt +
|
|
577
618
|
categories.userMessages +
|
|
@@ -579,45 +620,33 @@ export function analyzeContext(
|
|
|
579
620
|
categories.toolCalls +
|
|
580
621
|
categories.toolResults +
|
|
581
622
|
categories.other;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
scaling.categories.toolResults +
|
|
593
|
-
scaling.categories.other;
|
|
594
|
-
const freeSpace = Math.max(0, contextWindow - used - autocompactBuffer);
|
|
595
|
-
|
|
623
|
+
const observation: ContextObservation = {
|
|
624
|
+
...fallback,
|
|
625
|
+
scaling: applyScaling(
|
|
626
|
+
categories,
|
|
627
|
+
capacity.contextUsage?.tokens ?? null,
|
|
628
|
+
rawTotal,
|
|
629
|
+
capacity.contextUsage,
|
|
630
|
+
),
|
|
631
|
+
snapshot: capacity.snapshot,
|
|
632
|
+
};
|
|
596
633
|
const promptOptions = deriveOptionsFromSystemPrompt(ctx, cachedOptions);
|
|
597
634
|
const breakdown = computeSystemPromptBreakdown(
|
|
598
635
|
promptOptions,
|
|
599
|
-
systemPromptText,
|
|
600
|
-
scaling.categories.systemPrompt,
|
|
636
|
+
observation.systemPromptText,
|
|
637
|
+
observation.scaling.categories.systemPrompt,
|
|
601
638
|
ctx.cwd,
|
|
602
639
|
);
|
|
603
|
-
const injectedFiles = extractInjectedContextFiles(
|
|
640
|
+
const injectedFiles = extractInjectedContextFiles(observation.messages);
|
|
604
641
|
const toolDefinitions = computeToolDefinitions(pi);
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
642
|
+
const guidelineBullets = extractGuidelineBullets(
|
|
643
|
+
extractGuidelinesSection(observation.systemPromptText),
|
|
644
|
+
);
|
|
608
645
|
|
|
609
646
|
return {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
scaled: scaling.scaled,
|
|
614
|
-
approximationNote: scaling.approximationNote,
|
|
615
|
-
full,
|
|
616
|
-
categories: {
|
|
617
|
-
...scaling.categories,
|
|
618
|
-
autocompactBuffer,
|
|
619
|
-
freeSpace,
|
|
620
|
-
},
|
|
647
|
+
...observation.snapshot,
|
|
648
|
+
scaled: observation.scaling.scaled,
|
|
649
|
+
categories: observation.scaling.categories,
|
|
621
650
|
systemPromptBreakdown: breakdown,
|
|
622
651
|
injectedFiles,
|
|
623
652
|
skills: breakdown.skills,
|
|
@@ -626,7 +655,6 @@ export function analyzeContext(
|
|
|
626
655
|
guidelineSources: breakdown.guidelineSources,
|
|
627
656
|
toolSnippetDetails: breakdown.toolSnippetDetails,
|
|
628
657
|
toolDefinitions,
|
|
629
|
-
compaction,
|
|
630
658
|
providerSections: collectProviderData(),
|
|
631
659
|
};
|
|
632
660
|
}
|
package/src/capacity.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, point-in-time capacity reading without diagnostic attribution.
|
|
3
|
+
*/
|
|
4
|
+
export interface ContextCapacity {
|
|
5
|
+
contextWindow: number | null;
|
|
6
|
+
usedTokens: number;
|
|
7
|
+
usagePercent: number | null;
|
|
8
|
+
compactionEnabled: boolean;
|
|
9
|
+
reserveTokens: number;
|
|
10
|
+
headroomTokens: number | null;
|
|
11
|
+
pressurePercent: number | null;
|
|
12
|
+
compacted: boolean;
|
|
13
|
+
approximationNote: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Agent-facing, constant-shape reading of current context pressure. */
|
|
17
|
+
export interface ContextPressureSnapshot extends ContextCapacity {
|
|
18
|
+
modelName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Inputs that the shared capacity analysis derives from session state. */
|
|
22
|
+
export interface ContextCapacityInput {
|
|
23
|
+
contextWindow: number | null;
|
|
24
|
+
usedTokens: number;
|
|
25
|
+
compactionEnabled: boolean;
|
|
26
|
+
configuredReserveTokens: number;
|
|
27
|
+
compacted: boolean;
|
|
28
|
+
approximationNote: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function roundPercentage(value: number): number {
|
|
32
|
+
return Math.round(value * 10) / 10;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Derive reserve-adjusted capacity without inspecting diagnostic inventories.
|
|
37
|
+
*
|
|
38
|
+
* The configured reserve affects capacity only while auto-compaction is enabled.
|
|
39
|
+
* This lets report and snapshot callers share exactly the same Active Context
|
|
40
|
+
* Limit, Headroom, and Pressure Percentage semantics.
|
|
41
|
+
*/
|
|
42
|
+
export function analyzeContextCapacity(input: ContextCapacityInput): ContextCapacity {
|
|
43
|
+
const contextWindow = input.contextWindow && input.contextWindow > 0 ? input.contextWindow : null;
|
|
44
|
+
const reserveTokens = input.compactionEnabled ? Math.max(0, input.configuredReserveTokens) : 0;
|
|
45
|
+
const activeLimit = contextWindow === null ? null : contextWindow - reserveTokens;
|
|
46
|
+
const hasUsableActiveLimit = activeLimit !== null && activeLimit > 0;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
contextWindow,
|
|
50
|
+
usedTokens: input.usedTokens,
|
|
51
|
+
usagePercent:
|
|
52
|
+
contextWindow === null ? null : roundPercentage((input.usedTokens / contextWindow) * 100),
|
|
53
|
+
compactionEnabled: input.compactionEnabled,
|
|
54
|
+
reserveTokens,
|
|
55
|
+
headroomTokens: activeLimit === null ? null : Math.max(0, activeLimit - input.usedTokens),
|
|
56
|
+
pressurePercent: hasUsableActiveLimit
|
|
57
|
+
? roundPercentage((input.usedTokens / activeLimit) * 100)
|
|
58
|
+
: null,
|
|
59
|
+
compacted: input.compacted,
|
|
60
|
+
approximationNote: input.approximationNote,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Create the exact agent-facing Context Pressure Snapshot shape. */
|
|
65
|
+
export function createContextPressureSnapshot(
|
|
66
|
+
modelName: string,
|
|
67
|
+
capacity: ContextCapacity,
|
|
68
|
+
): ContextPressureSnapshot {
|
|
69
|
+
return {
|
|
70
|
+
modelName,
|
|
71
|
+
contextWindow: capacity.contextWindow,
|
|
72
|
+
usedTokens: capacity.usedTokens,
|
|
73
|
+
usagePercent: capacity.usagePercent,
|
|
74
|
+
compactionEnabled: capacity.compactionEnabled,
|
|
75
|
+
reserveTokens: capacity.reserveTokens,
|
|
76
|
+
headroomTokens: capacity.headroomTokens,
|
|
77
|
+
pressurePercent: capacity.pressurePercent,
|
|
78
|
+
compacted: capacity.compacted,
|
|
79
|
+
approximationNote: capacity.approximationNote,
|
|
80
|
+
};
|
|
81
|
+
}
|
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(
|