pi-cc-extensions 0.8.59 → 0.8.60
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.
|
@@ -13,10 +13,10 @@ import { padLine } from "../utils/format.ts";
|
|
|
13
13
|
export type ContextPart = {
|
|
14
14
|
label: string;
|
|
15
15
|
tokens: number;
|
|
16
|
-
color: "accent" | "success" | "warning" | "muted" | "dim";
|
|
16
|
+
color: "accent" | "success" | "warning" | "customMessageLabel" | "muted" | "dim";
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
type PreviewKey = "systemPrompt" | "tools" | "
|
|
19
|
+
type PreviewKey = "systemPrompt" | "tools" | "toolResults" | "contextFiles";
|
|
20
20
|
|
|
21
21
|
type ContextPreview = {
|
|
22
22
|
key: PreviewKey;
|
|
@@ -259,20 +259,30 @@ const tokenEstimate = (value: unknown): number => {
|
|
|
259
259
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
-
export function
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
262
|
+
export function capParts(parts: ContextPart[], target: number, fixedPrefix = 0): ContextPart[] {
|
|
263
|
+
const fixed = parts.slice(0, fixedPrefix);
|
|
264
|
+
const variable = parts.slice(fixedPrefix);
|
|
265
|
+
const fixedTokens = fixed.reduce((sum, part) => sum + part.tokens, 0);
|
|
266
|
+
const variableTarget = Math.max(0, target - fixedTokens);
|
|
267
|
+
const estimated = variable.reduce((sum, part) => sum + part.tokens, 0);
|
|
268
|
+
if (estimated <= variableTarget || estimated === 0) return parts;
|
|
269
|
+
if (variableTarget === 0) {
|
|
270
|
+
return [...fixed, ...variable.map((part) => ({ ...part, tokens: 0 }))];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let previous = 0;
|
|
274
|
+
let cumulative = 0;
|
|
275
|
+
const capped = variable.map((part, index) => {
|
|
276
|
+
cumulative += part.tokens;
|
|
277
|
+
const next =
|
|
278
|
+
index === variable.length - 1
|
|
279
|
+
? variableTarget
|
|
280
|
+
: Math.round((cumulative / estimated) * variableTarget);
|
|
281
|
+
const tokens = next - previous;
|
|
282
|
+
previous = next;
|
|
283
|
+
return { ...part, tokens };
|
|
284
|
+
});
|
|
285
|
+
return [...fixed, ...capped];
|
|
276
286
|
}
|
|
277
287
|
|
|
278
288
|
export function formatTokens(tokens: number): string {
|
|
@@ -281,72 +291,122 @@ export function formatTokens(tokens: number): string {
|
|
|
281
291
|
return `${Math.round(tokens / 1_000)}k`;
|
|
282
292
|
}
|
|
283
293
|
|
|
294
|
+
export function resolveUsedTokens(
|
|
295
|
+
usage: { tokens: number | null; percent: number | null } | undefined,
|
|
296
|
+
estimated: number,
|
|
297
|
+
contextWindow: number,
|
|
298
|
+
): number {
|
|
299
|
+
const reported = usage?.tokens;
|
|
300
|
+
const fromPercent =
|
|
301
|
+
usage?.percent !== null && usage?.percent !== undefined && contextWindow > 0
|
|
302
|
+
? Math.round((usage.percent / 100) * contextWindow)
|
|
303
|
+
: undefined;
|
|
304
|
+
let resolved = reported ?? fromPercent ?? estimated;
|
|
305
|
+
if (reported !== null && reported !== undefined && fromPercent !== undefined) {
|
|
306
|
+
// 某些 Provider 会返回异常 totalTokens;百分比与底部状态栏不一致时优先采用百分比。
|
|
307
|
+
const tolerance = Math.max(32, Math.round(contextWindow * 0.001));
|
|
308
|
+
if (Math.abs(reported - fromPercent) > tolerance) resolved = fromPercent;
|
|
309
|
+
}
|
|
310
|
+
// tokens 与 percent 可能同时源自异常 usage;数量级明显偏小时回退到实际内容估算。
|
|
311
|
+
if (estimated > 0 && resolved < estimated * 0.25) return estimated;
|
|
312
|
+
return resolved;
|
|
313
|
+
}
|
|
314
|
+
|
|
284
315
|
type ContextBreakdown = {
|
|
285
316
|
parts: ContextPart[];
|
|
286
|
-
|
|
287
|
-
systemPrompt: string;
|
|
317
|
+
previews: Record<PreviewKey, string>;
|
|
288
318
|
};
|
|
289
319
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
function previewValue(value: unknown): string {
|
|
321
|
+
if (typeof value === "string") return value;
|
|
322
|
+
return `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 按真实请求的 systemPrompt、tools、messages 三部分同步组装计数与预览。 */
|
|
326
|
+
export function collectContextBreakdown(
|
|
327
|
+
ctx: ExtensionCommandContext,
|
|
328
|
+
allTools: ToolInfo[],
|
|
329
|
+
): ContextBreakdown {
|
|
295
330
|
const options = (ctx.getSystemPromptOptions?.() ?? {}) as BuildSystemPromptOptions;
|
|
296
331
|
const systemPrompt = typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
let toolTokens = tokenEstimate(options.promptGuidelines);
|
|
315
|
-
for (const name of tools) {
|
|
316
|
-
toolTokens += tokenEstimate(name) + tokenEstimate(snippets?.[name]);
|
|
332
|
+
const selectedTools = new Set(options.selectedTools ?? ["read", "bash", "edit", "write"]);
|
|
333
|
+
const toolDefinitionPreview: string[] = [];
|
|
334
|
+
const toolResultPreview: string[] = [];
|
|
335
|
+
const contextPreview: string[] = [];
|
|
336
|
+
let toolDefinitionTokens = 0;
|
|
337
|
+
let toolResultTokens = 0;
|
|
338
|
+
let contextTokens = 0;
|
|
339
|
+
|
|
340
|
+
for (const tool of allTools) {
|
|
341
|
+
if (!selectedTools.has(tool.name)) continue;
|
|
342
|
+
const definition = {
|
|
343
|
+
name: tool.name,
|
|
344
|
+
description: tool.description,
|
|
345
|
+
parameters: tool.parameters,
|
|
346
|
+
};
|
|
347
|
+
toolDefinitionTokens += tokenEstimate(definition);
|
|
348
|
+
toolDefinitionPreview.push(`## Definition: ${tool.name}\n\n${previewValue(definition)}`);
|
|
317
349
|
}
|
|
318
350
|
|
|
319
|
-
let user = 0;
|
|
320
|
-
let assistant = 0;
|
|
321
|
-
let toolResults = 0;
|
|
322
|
-
let summaries = 0;
|
|
323
351
|
for (const entry of ctx.sessionManager.buildContextEntries()) {
|
|
324
352
|
if (entry.type === "message") {
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
353
|
+
const message = entry.message;
|
|
354
|
+
if (message.role === "assistant") {
|
|
355
|
+
for (const block of message.content) {
|
|
356
|
+
if (block.type === "toolCall") {
|
|
357
|
+
contextTokens += tokenEstimate(block.name) + tokenEstimate(block.arguments);
|
|
358
|
+
contextPreview.push(
|
|
359
|
+
`## Assistant tool call: ${block.name}\n\n${previewValue(block.arguments)}`,
|
|
360
|
+
);
|
|
361
|
+
} else if (block.type === "text") {
|
|
362
|
+
contextTokens += tokenEstimate(block.text);
|
|
363
|
+
contextPreview.push(`## Assistant\n\n${block.text}`);
|
|
364
|
+
} else if (block.type === "thinking") {
|
|
365
|
+
contextTokens += tokenEstimate(block.thinking);
|
|
366
|
+
contextPreview.push(`## Assistant thinking\n\n${block.thinking}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} else if (message.role === "toolResult") {
|
|
370
|
+
toolResultTokens += estimateTokens(message);
|
|
371
|
+
toolResultPreview.push(
|
|
372
|
+
`## Result: ${message.toolName}\n\n${previewValue(message.content)}`,
|
|
373
|
+
);
|
|
374
|
+
} else if (message.role === "bashExecution") {
|
|
375
|
+
toolResultTokens += estimateTokens(message);
|
|
376
|
+
toolResultPreview.push(
|
|
377
|
+
`## Bash\n\nCommand:\n\n${previewValue(message.command)}\n\nOutput:\n\n${previewValue(message.output)}`,
|
|
378
|
+
);
|
|
379
|
+
} else if (message.role === "branchSummary" || message.role === "compactionSummary") {
|
|
380
|
+
contextTokens += estimateTokens(message);
|
|
381
|
+
contextPreview.push(`## ${message.role}\n\n${message.summary}`);
|
|
382
|
+
} else {
|
|
383
|
+
contextTokens += estimateTokens(message);
|
|
384
|
+
contextPreview.push(`## ${message.role}\n\n${previewValue(message.content)}`);
|
|
385
|
+
}
|
|
329
386
|
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
330
|
-
|
|
387
|
+
contextTokens += tokenEstimate(entry.summary);
|
|
388
|
+
contextPreview.push(
|
|
389
|
+
`## ${entry.type === "compaction" ? "Compaction" : "Branch summary"}\n\n${entry.summary}`,
|
|
390
|
+
);
|
|
391
|
+
} else if (entry.type === "custom_message") {
|
|
392
|
+
contextTokens += tokenEstimate(entry.content);
|
|
393
|
+
contextPreview.push(`## Custom: ${entry.customType}\n\n${previewValue(entry.content)}`);
|
|
331
394
|
}
|
|
332
395
|
}
|
|
333
396
|
|
|
334
|
-
const systemTotal = tokenEstimate(systemPrompt);
|
|
335
|
-
const baseSystem = Math.max(0, systemTotal - contextFileTokens - skillTokens - toolTokens);
|
|
336
|
-
const parts: ContextPart[] = [
|
|
337
|
-
{ label: "System prompt", tokens: baseSystem, color: "accent" },
|
|
338
|
-
{ label: "Tools", tokens: toolTokens, color: "success" },
|
|
339
|
-
{ label: "Context files", tokens: contextFileTokens, color: "warning" },
|
|
340
|
-
{ label: "Skills", tokens: skillTokens, color: "warning" },
|
|
341
|
-
{ label: "User messages", tokens: user, color: "muted" },
|
|
342
|
-
{ label: "Assistant messages", tokens: assistant, color: "accent" },
|
|
343
|
-
{ label: "Tool results", tokens: toolResults, color: "dim" },
|
|
344
|
-
{ label: "Compaction summaries", tokens: summaries, color: "success" },
|
|
345
|
-
];
|
|
346
397
|
return {
|
|
347
|
-
parts:
|
|
348
|
-
|
|
349
|
-
|
|
398
|
+
parts: [
|
|
399
|
+
{ label: "System prompt", tokens: tokenEstimate(systemPrompt), color: "accent" },
|
|
400
|
+
{ label: "Tools", tokens: toolDefinitionTokens, color: "success" },
|
|
401
|
+
{ label: "Tool results", tokens: toolResultTokens, color: "customMessageLabel" },
|
|
402
|
+
{ label: "Context", tokens: contextTokens, color: "warning" },
|
|
403
|
+
] satisfies ContextPart[],
|
|
404
|
+
previews: {
|
|
405
|
+
systemPrompt: systemPrompt || "No system prompt.",
|
|
406
|
+
tools: toolDefinitionPreview.join("\n\n") || "No active tool definitions.",
|
|
407
|
+
toolResults: toolResultPreview.join("\n\n") || "No tool results in the current context.",
|
|
408
|
+
contextFiles: contextPreview.join("\n\n") || "No conversation context.",
|
|
409
|
+
},
|
|
350
410
|
};
|
|
351
411
|
}
|
|
352
412
|
|
|
@@ -356,11 +416,20 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
356
416
|
handler: async (_args, ctx) => {
|
|
357
417
|
const usage = ctx.getContextUsage();
|
|
358
418
|
const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const
|
|
419
|
+
const tools = pi.getAllTools();
|
|
420
|
+
const breakdown = collectContextBreakdown(ctx, tools);
|
|
421
|
+
const estimated = breakdown.parts.reduce((sum, part) => sum + part.tokens, 0);
|
|
422
|
+
const fixedTokens = breakdown.parts.slice(0, 2).reduce((sum, part) => sum + part.tokens, 0);
|
|
423
|
+
const used = Math.max(resolveUsedTokens(usage, estimated, contextWindow), fixedTokens);
|
|
424
|
+
const parts = capParts(breakdown.parts, used, 2);
|
|
425
|
+
const attributed = parts.reduce((sum, part) => sum + part.tokens, 0);
|
|
426
|
+
const other = Math.max(0, used - attributed);
|
|
362
427
|
const free = Math.max(0, contextWindow - used);
|
|
363
|
-
const allParts = [
|
|
428
|
+
const allParts = [
|
|
429
|
+
...parts,
|
|
430
|
+
{ label: "Other", tokens: other, color: "muted" as const },
|
|
431
|
+
{ label: "Free space", tokens: free, color: "dim" as const },
|
|
432
|
+
];
|
|
364
433
|
|
|
365
434
|
if (ctx.mode !== "tui") {
|
|
366
435
|
const lines = allParts.map((part) => `${part.label}: ${formatTokens(part.tokens)} tokens`);
|
|
@@ -368,64 +437,30 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
368
437
|
return;
|
|
369
438
|
}
|
|
370
439
|
|
|
371
|
-
const options = breakdown.options;
|
|
372
|
-
const toolByName = new Map<string, ToolInfo>(
|
|
373
|
-
pi.getAllTools().map((tool) => [tool.name, tool] as const),
|
|
374
|
-
);
|
|
375
|
-
const toolContent = (options.selectedTools ?? []).map((name) => {
|
|
376
|
-
const tool = toolByName.get(name);
|
|
377
|
-
const lines = [`## ${name}`];
|
|
378
|
-
if (tool?.description) lines.push(tool.description);
|
|
379
|
-
if (options.toolSnippets?.[name]) lines.push(`Prompt: ${options.toolSnippets[name]}`);
|
|
380
|
-
if (tool?.promptGuidelines?.length) {
|
|
381
|
-
lines.push("Guidelines:", ...tool.promptGuidelines.map((guideline) => `- ${guideline}`));
|
|
382
|
-
}
|
|
383
|
-
return lines.join("\n");
|
|
384
|
-
});
|
|
385
|
-
if (options.promptGuidelines?.length) {
|
|
386
|
-
toolContent.push(
|
|
387
|
-
`## Shared prompt guidelines\n${options.promptGuidelines.map((guideline) => `- ${guideline}`).join("\n")}`,
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
const contextFilesContent = (options.contextFiles ?? [])
|
|
391
|
-
.map((file) => `===== ${file.path} =====\n${file.content}`)
|
|
392
|
-
.join("\n\n");
|
|
393
|
-
const skillsContent = (options.skills ?? [])
|
|
394
|
-
.map((skill) =>
|
|
395
|
-
[
|
|
396
|
-
`## ${skill.name}`,
|
|
397
|
-
skill.description,
|
|
398
|
-
`Path: ${skill.filePath}`,
|
|
399
|
-
`Model invocation: ${skill.disableModelInvocation ? "disabled" : "enabled"}`,
|
|
400
|
-
]
|
|
401
|
-
.filter(Boolean)
|
|
402
|
-
.join("\n"),
|
|
403
|
-
)
|
|
404
|
-
.join("\n\n");
|
|
405
440
|
const rawPreviews: ContextPreview[] = [
|
|
406
441
|
{
|
|
407
442
|
key: "systemPrompt",
|
|
408
443
|
label: "System prompt",
|
|
409
444
|
title: "System Prompt",
|
|
410
|
-
content: breakdown.systemPrompt,
|
|
445
|
+
content: breakdown.previews.systemPrompt,
|
|
411
446
|
},
|
|
412
447
|
{
|
|
413
448
|
key: "tools",
|
|
414
449
|
label: "Tools",
|
|
415
450
|
title: "Tools",
|
|
416
|
-
content:
|
|
451
|
+
content: breakdown.previews.tools,
|
|
417
452
|
},
|
|
418
453
|
{
|
|
419
|
-
key: "
|
|
420
|
-
label: "
|
|
421
|
-
title: "
|
|
422
|
-
content:
|
|
454
|
+
key: "toolResults",
|
|
455
|
+
label: "Tool results",
|
|
456
|
+
title: "Tool Results",
|
|
457
|
+
content: breakdown.previews.toolResults,
|
|
423
458
|
},
|
|
424
459
|
{
|
|
425
|
-
key: "
|
|
426
|
-
label: "
|
|
427
|
-
title: "
|
|
428
|
-
content:
|
|
460
|
+
key: "contextFiles",
|
|
461
|
+
label: "Context",
|
|
462
|
+
title: "Context",
|
|
463
|
+
content: breakdown.previews.contextFiles,
|
|
429
464
|
},
|
|
430
465
|
];
|
|
431
466
|
const previews = rawPreviews.map((preview) => ({
|
|
@@ -149,6 +149,42 @@ function deCircled(markdown: string): string {
|
|
|
149
149
|
return markdown.replace(CIRCLED_RE, (ch) => `(${ch.codePointAt(0)! - 0x2460 + 1})`);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// 跨行链接规整
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
const MULTILINE_LINK_RE = /\[([^\]\n]*(?:\n[^\]\n]*)+)\](?=\()/g;
|
|
157
|
+
|
|
158
|
+
/** 防止 Markdown 跨行链接把行尾填充空格纳入 OSC 8 点击区。 */
|
|
159
|
+
function normalizeMultilineLinks(markdown: string): string {
|
|
160
|
+
const out: string[] = [];
|
|
161
|
+
let prose: string[] = [];
|
|
162
|
+
let inFence = false;
|
|
163
|
+
const flush = () => {
|
|
164
|
+
if (prose.length === 0) return;
|
|
165
|
+
out.push(
|
|
166
|
+
prose.join("\n").replace(MULTILINE_LINK_RE, (_match, label: string) => {
|
|
167
|
+
const normalized = label.replace(/[ \t]*\n[ \t]*/g, " ").trim();
|
|
168
|
+
return `[${normalized}]`;
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
prose = [];
|
|
172
|
+
};
|
|
173
|
+
for (const line of markdown.split("\n")) {
|
|
174
|
+
if (/^```/.test(line)) {
|
|
175
|
+
flush();
|
|
176
|
+
inFence = !inFence;
|
|
177
|
+
out.push(line);
|
|
178
|
+
} else if (inFence) {
|
|
179
|
+
out.push(line);
|
|
180
|
+
} else {
|
|
181
|
+
prose.push(line);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
flush();
|
|
185
|
+
return out.join("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
152
188
|
// ============================================================================
|
|
153
189
|
// 注册
|
|
154
190
|
// ============================================================================
|
|
@@ -158,8 +194,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
158
194
|
// 所以三个转换合并为一次注册,内部按序链式执行。
|
|
159
195
|
pi.registerMarkdownTransformer((markdown, context) => {
|
|
160
196
|
const { messageType, isStreaming = false } = context ?? {};
|
|
161
|
-
//
|
|
162
|
-
if (messageType === "assistant-thinking"
|
|
197
|
+
// thinking 保持原文;普通回复即使流式中也先关闭跨行链接的空白点击区。
|
|
198
|
+
if (messageType === "assistant-thinking") return markdown;
|
|
199
|
+
markdown = normalizeMultilineLinks(markdown);
|
|
200
|
+
if (isStreaming) return markdown;
|
|
163
201
|
// 0. 圈数字转半角括号(Nerd Font 补丁字形缺陷规避)
|
|
164
202
|
markdown = deCircled(markdown);
|
|
165
203
|
// 1. Mermaid 方言渲染
|