@mrclrchtr/supi-context 0.1.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 +49 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +42 -0
- package/src/analysis.ts +430 -0
- package/src/context.ts +33 -0
- package/src/format.ts +280 -0
- package/src/index.ts +1 -0
- package/src/prompt-inference.ts +131 -0
- package/src/renderer.ts +26 -0
- package/src/utils.ts +15 -0
package/src/analysis.ts
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// biome-ignore lint/nursery/noExcessiveLinesPerFile: analysis file is inherently large
|
|
2
|
+
import {
|
|
3
|
+
type BuildSystemPromptOptions,
|
|
4
|
+
buildSessionContext,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionCommandContext,
|
|
7
|
+
type estimateTokens,
|
|
8
|
+
formatSkillsForPrompt,
|
|
9
|
+
getLatestCompactionEntry,
|
|
10
|
+
SettingsManager,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { getRegisteredContextProviders } from "@mrclrchtr/supi-core";
|
|
13
|
+
import { deriveOptionsFromSystemPrompt, extractGuidelinesSection } from "./prompt-inference.ts";
|
|
14
|
+
|
|
15
|
+
type AgentMessage = Parameters<typeof estimateTokens>[0];
|
|
16
|
+
|
|
17
|
+
export interface CategoryTokens {
|
|
18
|
+
systemPrompt: number;
|
|
19
|
+
userMessages: number;
|
|
20
|
+
assistantMessages: number;
|
|
21
|
+
toolCalls: number;
|
|
22
|
+
toolResults: number;
|
|
23
|
+
other: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ContextFileInfo {
|
|
27
|
+
path: string;
|
|
28
|
+
tokens: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InjectedFileInfo {
|
|
32
|
+
file: string;
|
|
33
|
+
turn: number;
|
|
34
|
+
tokens: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SkillInfo {
|
|
38
|
+
name: string;
|
|
39
|
+
tokens: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ContextProviderSection {
|
|
43
|
+
id: string;
|
|
44
|
+
label: string;
|
|
45
|
+
data: Record<string, string | number>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ContextAnalysis {
|
|
49
|
+
modelName: string;
|
|
50
|
+
contextWindow: number;
|
|
51
|
+
totalTokens: number | null;
|
|
52
|
+
scaled: boolean;
|
|
53
|
+
approximationNote: string | null;
|
|
54
|
+
categories: CategoryTokens & {
|
|
55
|
+
autocompactBuffer: number;
|
|
56
|
+
freeSpace: number;
|
|
57
|
+
};
|
|
58
|
+
systemPromptBreakdown: {
|
|
59
|
+
base: number;
|
|
60
|
+
contextFiles: ContextFileInfo[];
|
|
61
|
+
skills: SkillInfo[];
|
|
62
|
+
guidelines: number;
|
|
63
|
+
toolSnippets: number;
|
|
64
|
+
appendText: number;
|
|
65
|
+
};
|
|
66
|
+
injectedFiles: InjectedFileInfo[];
|
|
67
|
+
skills: SkillInfo[];
|
|
68
|
+
guidelines: number;
|
|
69
|
+
toolDefinitions: { count: number; tokens: number };
|
|
70
|
+
compaction: { summarizedTurns: number } | null;
|
|
71
|
+
providerSections: ContextProviderSection[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function estimateTextTokens(text: string): number {
|
|
75
|
+
return Math.ceil(text.length / 4);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function estimateGenericContent(content: unknown): number {
|
|
79
|
+
if (typeof content === "string") {
|
|
80
|
+
return estimateTextTokens(content);
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(content)) {
|
|
83
|
+
let chars = 0;
|
|
84
|
+
for (const block of content as Array<{ type?: string; text?: string }>) {
|
|
85
|
+
if (block.type === "text" && block.text) {
|
|
86
|
+
chars += block.text.length;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return Math.ceil(chars / 4);
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function estimateUserMessage(msg: Extract<AgentMessage, { role: "user" }>): number {
|
|
95
|
+
const content = msg.content;
|
|
96
|
+
if (typeof content === "string") {
|
|
97
|
+
return estimateTextTokens(content);
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(content)) {
|
|
100
|
+
let chars = 0;
|
|
101
|
+
for (const block of content) {
|
|
102
|
+
if (block.type === "text" && block.text) {
|
|
103
|
+
chars += block.text.length;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return Math.ceil(chars / 4);
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function estimateAssistantMessage(msg: Extract<AgentMessage, { role: "assistant" }>): {
|
|
112
|
+
text: number;
|
|
113
|
+
toolCalls: number;
|
|
114
|
+
} {
|
|
115
|
+
if (!Array.isArray(msg.content)) {
|
|
116
|
+
return { text: 0, toolCalls: 0 };
|
|
117
|
+
}
|
|
118
|
+
let textChars = 0;
|
|
119
|
+
let toolChars = 0;
|
|
120
|
+
for (const block of msg.content) {
|
|
121
|
+
if (block.type === "text") {
|
|
122
|
+
textChars += block.text.length;
|
|
123
|
+
} else if (block.type === "thinking") {
|
|
124
|
+
textChars += block.thinking.length;
|
|
125
|
+
} else if (block.type === "toolCall") {
|
|
126
|
+
toolChars += block.name.length + JSON.stringify(block.arguments).length;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { text: Math.ceil(textChars / 4), toolCalls: Math.ceil(toolChars / 4) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function estimateMessageByCategory(msg: AgentMessage): {
|
|
133
|
+
user: number;
|
|
134
|
+
assistantText: number;
|
|
135
|
+
toolCalls: number;
|
|
136
|
+
toolResult: number;
|
|
137
|
+
other: number;
|
|
138
|
+
} {
|
|
139
|
+
if (msg.role === "user") {
|
|
140
|
+
return {
|
|
141
|
+
user: estimateUserMessage(msg),
|
|
142
|
+
assistantText: 0,
|
|
143
|
+
toolCalls: 0,
|
|
144
|
+
toolResult: 0,
|
|
145
|
+
other: 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (msg.role === "assistant") {
|
|
149
|
+
const est = estimateAssistantMessage(msg);
|
|
150
|
+
return { user: 0, assistantText: est.text, toolCalls: est.toolCalls, toolResult: 0, other: 0 };
|
|
151
|
+
}
|
|
152
|
+
if (msg.role === "toolResult") {
|
|
153
|
+
return {
|
|
154
|
+
user: 0,
|
|
155
|
+
assistantText: 0,
|
|
156
|
+
toolCalls: 0,
|
|
157
|
+
toolResult: estimateGenericContent(msg.content),
|
|
158
|
+
other: 0,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
user: 0,
|
|
163
|
+
assistantText: 0,
|
|
164
|
+
toolCalls: 0,
|
|
165
|
+
toolResult: 0,
|
|
166
|
+
other: estimateGenericContent((msg as unknown as Record<string, unknown>).content),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function computeMessageCategories(messages: AgentMessage[]): CategoryTokens {
|
|
171
|
+
const categories: CategoryTokens = {
|
|
172
|
+
systemPrompt: 0,
|
|
173
|
+
userMessages: 0,
|
|
174
|
+
assistantMessages: 0,
|
|
175
|
+
toolCalls: 0,
|
|
176
|
+
toolResults: 0,
|
|
177
|
+
other: 0,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
for (const msg of messages) {
|
|
181
|
+
const est = estimateMessageByCategory(msg);
|
|
182
|
+
categories.userMessages += est.user;
|
|
183
|
+
categories.assistantMessages += est.assistantText;
|
|
184
|
+
categories.toolCalls += est.toolCalls;
|
|
185
|
+
categories.toolResults += est.toolResult;
|
|
186
|
+
categories.other += est.other;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return categories;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function applyScaling(
|
|
193
|
+
categories: CategoryTokens,
|
|
194
|
+
actualTokens: number | null,
|
|
195
|
+
rawTotal: number,
|
|
196
|
+
contextUsage:
|
|
197
|
+
| { tokens: number | null; contextWindow: number; percent: number | null }
|
|
198
|
+
| undefined,
|
|
199
|
+
): {
|
|
200
|
+
categories: CategoryTokens;
|
|
201
|
+
scaled: boolean;
|
|
202
|
+
approximationNote: string | null;
|
|
203
|
+
totalTokens: number;
|
|
204
|
+
} {
|
|
205
|
+
let scaled = false;
|
|
206
|
+
let approximationNote: string | null = null;
|
|
207
|
+
const hasActualTotal = actualTokens !== null && actualTokens > 0;
|
|
208
|
+
const totalTokens = hasActualTotal ? actualTokens : rawTotal;
|
|
209
|
+
|
|
210
|
+
if (contextUsage === undefined) {
|
|
211
|
+
approximationNote = "Approximate (no usage data available)";
|
|
212
|
+
} else if (hasActualTotal && rawTotal > 0) {
|
|
213
|
+
const scale = actualTokens / rawTotal;
|
|
214
|
+
categories.systemPrompt = Math.round(categories.systemPrompt * scale);
|
|
215
|
+
categories.userMessages = Math.round(categories.userMessages * scale);
|
|
216
|
+
categories.assistantMessages = Math.round(categories.assistantMessages * scale);
|
|
217
|
+
categories.toolCalls = Math.round(categories.toolCalls * scale);
|
|
218
|
+
categories.toolResults = Math.round(categories.toolResults * scale);
|
|
219
|
+
categories.other = Math.round(categories.other * scale);
|
|
220
|
+
scaled = true;
|
|
221
|
+
} else if (actualTokens === null || actualTokens === 0) {
|
|
222
|
+
approximationNote = "Token count pending — send a message to refresh";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { categories, scaled, approximationNote, totalTokens };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Collect data from registered context providers.
|
|
230
|
+
*/
|
|
231
|
+
function collectProviderData(): ContextProviderSection[] {
|
|
232
|
+
const sections: ContextProviderSection[] = [];
|
|
233
|
+
for (const provider of getRegisteredContextProviders()) {
|
|
234
|
+
const data = provider.getData();
|
|
235
|
+
if (data) {
|
|
236
|
+
sections.push({ id: provider.id, label: provider.label, data });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return sections;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function computeContextFiles(
|
|
243
|
+
promptOptions: BuildSystemPromptOptions | undefined,
|
|
244
|
+
): ContextFileInfo[] {
|
|
245
|
+
const files: ContextFileInfo[] = [];
|
|
246
|
+
if (promptOptions?.contextFiles) {
|
|
247
|
+
for (const cf of promptOptions.contextFiles) {
|
|
248
|
+
files.push({ path: cf.path, tokens: estimateTextTokens(cf.content) });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return files;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function computeSkills(promptOptions: BuildSystemPromptOptions | undefined): SkillInfo[] {
|
|
255
|
+
const skills: SkillInfo[] = [];
|
|
256
|
+
if (promptOptions?.skills) {
|
|
257
|
+
for (const skill of promptOptions.skills) {
|
|
258
|
+
const skillText = formatSkillsForPrompt([skill]);
|
|
259
|
+
skills.push({ name: skill.name, tokens: estimateTextTokens(skillText) });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return skills;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function computeSystemPromptBreakdown(
|
|
266
|
+
promptOptions: BuildSystemPromptOptions | undefined,
|
|
267
|
+
systemPromptText: string,
|
|
268
|
+
systemPromptTokens: number,
|
|
269
|
+
): ContextAnalysis["systemPromptBreakdown"] {
|
|
270
|
+
const contextFiles = computeContextFiles(promptOptions);
|
|
271
|
+
const skills = computeSkills(promptOptions);
|
|
272
|
+
|
|
273
|
+
const skillsTotal = skills.reduce((s, c) => s + c.tokens, 0);
|
|
274
|
+
const inferredGuidelines = extractGuidelinesSection(systemPromptText);
|
|
275
|
+
const guidelines = inferredGuidelines
|
|
276
|
+
? estimateTextTokens(inferredGuidelines)
|
|
277
|
+
: promptOptions?.promptGuidelines
|
|
278
|
+
? estimateTextTokens(promptOptions.promptGuidelines.join("\n"))
|
|
279
|
+
: 0;
|
|
280
|
+
const toolSnippets = promptOptions?.toolSnippets
|
|
281
|
+
? estimateTextTokens(Object.values(promptOptions.toolSnippets).join("\n"))
|
|
282
|
+
: 0;
|
|
283
|
+
const appendText = promptOptions?.appendSystemPrompt
|
|
284
|
+
? estimateTextTokens(promptOptions.appendSystemPrompt)
|
|
285
|
+
: 0;
|
|
286
|
+
const customTokens = promptOptions?.customPrompt
|
|
287
|
+
? estimateTextTokens(promptOptions.customPrompt)
|
|
288
|
+
: 0;
|
|
289
|
+
|
|
290
|
+
const knownSubtotal =
|
|
291
|
+
contextFiles.reduce((s, c) => s + c.tokens, 0) +
|
|
292
|
+
skillsTotal +
|
|
293
|
+
guidelines +
|
|
294
|
+
toolSnippets +
|
|
295
|
+
appendText +
|
|
296
|
+
customTokens;
|
|
297
|
+
|
|
298
|
+
const base = Math.max(0, systemPromptTokens - knownSubtotal);
|
|
299
|
+
|
|
300
|
+
return { base, contextFiles, skills, guidelines, toolSnippets, appendText };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function computeToolDefinitions(pi: ExtensionAPI): { count: number; tokens: number } {
|
|
304
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
305
|
+
const allTools = pi.getAllTools();
|
|
306
|
+
const activeTools = allTools.filter((t) => activeToolNames.has(t.name));
|
|
307
|
+
return {
|
|
308
|
+
count: activeTools.length,
|
|
309
|
+
tokens: activeTools.reduce(
|
|
310
|
+
(sum, t) =>
|
|
311
|
+
sum +
|
|
312
|
+
estimateTextTokens(
|
|
313
|
+
JSON.stringify({ name: t.name, description: t.description, parameters: t.parameters }),
|
|
314
|
+
),
|
|
315
|
+
0,
|
|
316
|
+
),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function detectCompaction(
|
|
321
|
+
branch: ReturnType<ExtensionCommandContext["sessionManager"]["getBranch"]>,
|
|
322
|
+
): {
|
|
323
|
+
summarizedTurns: number;
|
|
324
|
+
} | null {
|
|
325
|
+
const compactionEntry = getLatestCompactionEntry(branch);
|
|
326
|
+
if (!compactionEntry) return null;
|
|
327
|
+
|
|
328
|
+
const index = branch.findIndex((e) => e.id === compactionEntry.id);
|
|
329
|
+
const messagesBefore = branch
|
|
330
|
+
.slice(0, Math.max(0, index))
|
|
331
|
+
.filter((e) => e.type === "message").length;
|
|
332
|
+
const summarizedTurns = Math.floor(messagesBefore / 2);
|
|
333
|
+
return { summarizedTurns };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function extractInjectedContextFiles(messages: AgentMessage[]): InjectedFileInfo[] {
|
|
337
|
+
const regex =
|
|
338
|
+
/<extension-context source="supi-claude-md" file="([^"]+)" turn="(\d+)">([\s\S]*?)<\/extension-context>/g;
|
|
339
|
+
const seen = new Map<string, InjectedFileInfo>();
|
|
340
|
+
|
|
341
|
+
for (const msg of messages) {
|
|
342
|
+
if (msg.role !== "toolResult") continue;
|
|
343
|
+
const content =
|
|
344
|
+
typeof msg.content === "string"
|
|
345
|
+
? msg.content
|
|
346
|
+
: msg.content
|
|
347
|
+
.map((b: { type: string; text?: string }) => (b.type === "text" ? b.text : ""))
|
|
348
|
+
.join("");
|
|
349
|
+
let match = regex.exec(content);
|
|
350
|
+
while (match !== null) {
|
|
351
|
+
const file = match[1];
|
|
352
|
+
const turn = Number.parseInt(match[2], 10);
|
|
353
|
+
const innerContent = match[3];
|
|
354
|
+
const key = `${file}::${turn}`;
|
|
355
|
+
if (!seen.has(key)) {
|
|
356
|
+
seen.set(key, { file, turn, tokens: estimateTextTokens(innerContent) });
|
|
357
|
+
}
|
|
358
|
+
match = regex.exec(content);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return Array.from(seen.values()).sort((a, b) => a.turn - b.turn || a.file.localeCompare(b.file));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function analyzeContext(
|
|
366
|
+
ctx: ExtensionCommandContext,
|
|
367
|
+
pi: ExtensionAPI,
|
|
368
|
+
cachedOptions: BuildSystemPromptOptions | undefined,
|
|
369
|
+
): ContextAnalysis {
|
|
370
|
+
const branch = ctx.sessionManager.getBranch();
|
|
371
|
+
const apiView = buildSessionContext(branch);
|
|
372
|
+
const contextUsage = ctx.getContextUsage();
|
|
373
|
+
const contextWindow = contextUsage?.contextWindow ?? 0;
|
|
374
|
+
const actualTokens = contextUsage?.tokens ?? null;
|
|
375
|
+
|
|
376
|
+
const systemPromptText = ctx.getSystemPrompt();
|
|
377
|
+
const categories = computeMessageCategories(apiView.messages);
|
|
378
|
+
categories.systemPrompt = estimateTextTokens(systemPromptText);
|
|
379
|
+
|
|
380
|
+
const rawTotal =
|
|
381
|
+
categories.systemPrompt +
|
|
382
|
+
categories.userMessages +
|
|
383
|
+
categories.assistantMessages +
|
|
384
|
+
categories.toolCalls +
|
|
385
|
+
categories.toolResults +
|
|
386
|
+
categories.other;
|
|
387
|
+
|
|
388
|
+
const scaling = applyScaling(categories, actualTokens, rawTotal, contextUsage);
|
|
389
|
+
|
|
390
|
+
const autocompactBuffer =
|
|
391
|
+
contextWindow > 0 ? SettingsManager.create(ctx.cwd).getCompactionReserveTokens() : 0;
|
|
392
|
+
const used =
|
|
393
|
+
scaling.categories.systemPrompt +
|
|
394
|
+
scaling.categories.userMessages +
|
|
395
|
+
scaling.categories.assistantMessages +
|
|
396
|
+
scaling.categories.toolCalls +
|
|
397
|
+
scaling.categories.toolResults +
|
|
398
|
+
scaling.categories.other;
|
|
399
|
+
const freeSpace = Math.max(0, contextWindow - used - autocompactBuffer);
|
|
400
|
+
|
|
401
|
+
const promptOptions = deriveOptionsFromSystemPrompt(ctx, cachedOptions);
|
|
402
|
+
const breakdown = computeSystemPromptBreakdown(
|
|
403
|
+
promptOptions,
|
|
404
|
+
systemPromptText,
|
|
405
|
+
scaling.categories.systemPrompt,
|
|
406
|
+
);
|
|
407
|
+
const injectedFiles = extractInjectedContextFiles(apiView.messages);
|
|
408
|
+
const toolDefinitions = computeToolDefinitions(pi);
|
|
409
|
+
const compaction = detectCompaction(branch);
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
modelName: ctx.model?.name ?? ctx.model?.id ?? "No model selected",
|
|
413
|
+
contextWindow,
|
|
414
|
+
totalTokens: scaling.totalTokens,
|
|
415
|
+
scaled: scaling.scaled,
|
|
416
|
+
approximationNote: scaling.approximationNote,
|
|
417
|
+
categories: {
|
|
418
|
+
...scaling.categories,
|
|
419
|
+
autocompactBuffer,
|
|
420
|
+
freeSpace,
|
|
421
|
+
},
|
|
422
|
+
systemPromptBreakdown: breakdown,
|
|
423
|
+
injectedFiles,
|
|
424
|
+
skills: breakdown.skills,
|
|
425
|
+
guidelines: breakdown.guidelines,
|
|
426
|
+
toolDefinitions,
|
|
427
|
+
compaction,
|
|
428
|
+
providerSections: collectProviderData(),
|
|
429
|
+
};
|
|
430
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BuildSystemPromptOptions, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { analyzeContext } from "./analysis.ts";
|
|
3
|
+
import { registerContextRenderer } from "./renderer.ts";
|
|
4
|
+
import { formatTokens } from "./utils.ts";
|
|
5
|
+
|
|
6
|
+
export default function contextExtension(pi: ExtensionAPI) {
|
|
7
|
+
let cachedOptions: BuildSystemPromptOptions | undefined;
|
|
8
|
+
|
|
9
|
+
pi.on("before_agent_start", async (event) => {
|
|
10
|
+
cachedOptions = event.systemPromptOptions;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
pi.on("session_start", async () => {
|
|
14
|
+
cachedOptions = undefined;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
pi.registerCommand("supi-context", {
|
|
18
|
+
description: "Show detailed context usage",
|
|
19
|
+
handler: async (_args, ctx) => {
|
|
20
|
+
const analysis = analyzeContext(ctx, pi, cachedOptions);
|
|
21
|
+
const shortContent = `${formatTokens(analysis.totalTokens ?? 0)} / ${formatTokens(analysis.contextWindow)} tokens`;
|
|
22
|
+
|
|
23
|
+
pi.sendMessage({
|
|
24
|
+
customType: "supi-context",
|
|
25
|
+
content: shortContent,
|
|
26
|
+
display: true,
|
|
27
|
+
details: { analysis },
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
registerContextRenderer(pi);
|
|
33
|
+
}
|