pi-cc-extensions 0.8.58 → 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.
- package/README.en.md +1 -4
- package/README.md +1 -4
- package/extensions/feature/context.ts +150 -115
- package/extensions/feature/reference/index.ts +13 -5
- package/extensions/renderer/markdown-enhance.ts +54 -7
- package/package.json +3 -2
package/README.en.md
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
<a href="https://pi.dev/packages?name=pi-cc-extensions"><img alt="Pi package catalog" src="https://img.shields.io/badge/Pi-package-58B7FF?style=flat-square"></a>
|
|
7
7
|
<a href="https://www.npmjs.com/package/pi-cc-extensions"><img alt="npm version" src="https://img.shields.io/npm/v/pi-cc-extensions?style=flat-square&color=66E3C4"></a>
|
|
8
8
|
<a href="#compatibility"><img alt="Node.js 22.19 or newer" src="https://img.shields.io/badge/Node.js-%E2%89%A522.19-66E3C4?style=flat-square"></a>
|
|
9
|
-
<a href="./extensions"><img alt="TypeScript extensions" src="https://img.shields.io/badge/TypeScript-extensions-3178C6?style=flat-square"></a>
|
|
10
9
|
</p>
|
|
11
10
|
|
|
12
11
|
<p align="center">
|
|
@@ -21,9 +20,7 @@
|
|
|
21
20
|
|
|
22
21
|
## Preview
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
<img width="100%" alt="demo_new" src="https://github.com/user-attachments/assets/d4f9bb51-a49a-4a34-aa60-006514d37b09" />
|
|
26
|
-
</table>
|
|
23
|
+
https://github.com/user-attachments/assets/6c858000-fdad-43f9-957f-4d0278648498
|
|
27
24
|
|
|
28
25
|
## Quick start
|
|
29
26
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
<a href="https://pi.dev/packages?name=pi-cc-extensions"><img alt="Pi package catalog" src="https://img.shields.io/badge/Pi-package-58B7FF?style=flat-square"></a>
|
|
7
7
|
<a href="https://www.npmjs.com/package/pi-cc-extensions"><img alt="npm version" src="https://img.shields.io/npm/v/pi-cc-extensions?style=flat-square&color=66E3C4"></a>
|
|
8
8
|
<a href="#兼容性"><img alt="Node.js 22.19 or newer" src="https://img.shields.io/badge/Node.js-%E2%89%A522.19-66E3C4?style=flat-square"></a>
|
|
9
|
-
<a href="./extensions"><img alt="TypeScript extensions" src="https://img.shields.io/badge/TypeScript-extensions-3178C6?style=flat-square"></a>
|
|
10
9
|
</p>
|
|
11
10
|
|
|
12
11
|
<p align="center">
|
|
@@ -21,9 +20,7 @@
|
|
|
21
20
|
|
|
22
21
|
## 界面预览
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
<img width="100%" alt="demo_new" src="https://github.com/user-attachments/assets/d4f9bb51-a49a-4a34-aa60-006514d37b09" />
|
|
26
|
-
</table>
|
|
23
|
+
https://github.com/user-attachments/assets/6c858000-fdad-43f9-957f-4d0278648498
|
|
27
24
|
|
|
28
25
|
## 快速开始
|
|
29
26
|
|
|
@@ -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) => ({
|
|
@@ -415,6 +415,10 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
415
415
|
|
|
416
416
|
pi.on("session_start", (_event, ctx) => {
|
|
417
417
|
const generation = ++sessionGeneration;
|
|
418
|
+
// 会话替换/reload 会让 ctx 的 getter 抛 stale 错误,await 之后不能再读;
|
|
419
|
+
// 这里在同步阶段一次性取出纯值。
|
|
420
|
+
const currentCwd = ctx.cwd;
|
|
421
|
+
const ui = ctx.ui;
|
|
418
422
|
subagentIds.clear();
|
|
419
423
|
clearLiveSubagentRecords();
|
|
420
424
|
let loadErrorShown = false;
|
|
@@ -434,7 +438,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
434
438
|
if (!loadErrorShown) {
|
|
435
439
|
loadErrorShown = true;
|
|
436
440
|
const reason = error instanceof Error ? error.message : String(error);
|
|
437
|
-
|
|
441
|
+
ui.notify(`session-reference: failed to load sessions: ${reason}`, "error");
|
|
438
442
|
}
|
|
439
443
|
return [];
|
|
440
444
|
});
|
|
@@ -447,6 +451,8 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
447
451
|
| undefined;
|
|
448
452
|
const getReferences = async (): Promise<SessionReference[]> => {
|
|
449
453
|
const sessions = await getSessions();
|
|
454
|
+
// 会话替换/reload 后丢弃旧 generation 的结果,避免对失效状态继续工作。
|
|
455
|
+
if (generation !== sessionGeneration) return [];
|
|
450
456
|
const subagentKey = [...subagentIds].join("\0");
|
|
451
457
|
if (
|
|
452
458
|
referencesCache &&
|
|
@@ -457,7 +463,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
457
463
|
}
|
|
458
464
|
const ordered = orderSessionReferences(
|
|
459
465
|
mergeReferences(sessions, liveSubagentReferences(subagentIds, currentSessionId)),
|
|
460
|
-
|
|
466
|
+
currentCwd,
|
|
461
467
|
);
|
|
462
468
|
referencesCache = { sessions, subagentKey, ordered };
|
|
463
469
|
return ordered;
|
|
@@ -465,13 +471,15 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
465
471
|
|
|
466
472
|
getAvailableReferences = getReferences;
|
|
467
473
|
if (ctx.mode === "tui") {
|
|
468
|
-
|
|
474
|
+
// 预取是 detached 的,必须兜住 rejection,否则 stale 错误会变成
|
|
475
|
+
// unhandled rejection 直接终止 Pi。
|
|
476
|
+
void getReferences().catch(() => {});
|
|
469
477
|
// Register after other session_start handlers. pi-fff claims every @
|
|
470
478
|
// prefix, so a provider installed before it would never see session mentions.
|
|
471
479
|
setTimeout(() => {
|
|
472
480
|
if (generation !== sessionGeneration) return;
|
|
473
|
-
|
|
474
|
-
createAutocompleteProvider(current, getReferences,
|
|
481
|
+
ui.addAutocompleteProvider((current) =>
|
|
482
|
+
createAutocompleteProvider(current, getReferences, currentCwd),
|
|
475
483
|
);
|
|
476
484
|
}, 0);
|
|
477
485
|
}
|
|
@@ -82,14 +82,23 @@ function renderAdmonition(lines: string[], i: number): { output: string[]; next:
|
|
|
82
82
|
// 裸 URL 转可点击超链接
|
|
83
83
|
// ============================================================================
|
|
84
84
|
|
|
85
|
-
const URL_RE = /(?<!<)(?<!\]\()https?:\/\/[^\s<>'"
|
|
86
|
-
const TRIM_URL_RE = /[.,;:!?】」』"'
|
|
85
|
+
const URL_RE = /(?<!<)(?<!\]\()https?:\/\/[^\s<>'"|,。;:!?、」』】()【】《》『』「」]+/g;
|
|
86
|
+
const TRIM_URL_RE = /[.,;:!?】」』"'》)}]+$/;
|
|
87
87
|
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* 去掉 URL 尾部标点;括号按平衡保留(如 Wikipedia 链接含括号、
|
|
90
|
+
* IPv6 地址含 [::1]),尾部不成对的 ] 与 ) 会截掉。
|
|
91
|
+
*/
|
|
89
92
|
function trimUrl(url: string): string {
|
|
90
93
|
let t = url.replace(TRIM_URL_RE, "");
|
|
91
|
-
|
|
92
|
-
t
|
|
94
|
+
for (;;) {
|
|
95
|
+
if (t.endsWith(")") && (t.match(/\(/g)?.length ?? 0) < (t.match(/\)/g)?.length ?? 0)) {
|
|
96
|
+
t = t.slice(0, -1);
|
|
97
|
+
} else if (t.endsWith("]") && (t.match(/\[/g)?.length ?? 0) < (t.match(/\]/g)?.length ?? 0)) {
|
|
98
|
+
t = t.slice(0, -1);
|
|
99
|
+
} else {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
return t;
|
|
95
104
|
}
|
|
@@ -140,6 +149,42 @@ function deCircled(markdown: string): string {
|
|
|
140
149
|
return markdown.replace(CIRCLED_RE, (ch) => `(${ch.codePointAt(0)! - 0x2460 + 1})`);
|
|
141
150
|
}
|
|
142
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
|
+
|
|
143
188
|
// ============================================================================
|
|
144
189
|
// 注册
|
|
145
190
|
// ============================================================================
|
|
@@ -149,8 +194,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
149
194
|
// 所以三个转换合并为一次注册,内部按序链式执行。
|
|
150
195
|
pi.registerMarkdownTransformer((markdown, context) => {
|
|
151
196
|
const { messageType, isStreaming = false } = context ?? {};
|
|
152
|
-
//
|
|
153
|
-
if (messageType === "assistant-thinking"
|
|
197
|
+
// thinking 保持原文;普通回复即使流式中也先关闭跨行链接的空白点击区。
|
|
198
|
+
if (messageType === "assistant-thinking") return markdown;
|
|
199
|
+
markdown = normalizeMultilineLinks(markdown);
|
|
200
|
+
if (isStreaming) return markdown;
|
|
154
201
|
// 0. 圈数字转半角括号(Nerd Font 补丁字形缺陷规避)
|
|
155
202
|
markdown = deCircled(markdown);
|
|
156
203
|
// 1. Mermaid 方言渲染
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.60",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"extensions",
|
|
23
23
|
"assets/readme",
|
|
24
|
+
"assets/video",
|
|
24
25
|
"themes",
|
|
25
26
|
"README.md",
|
|
26
27
|
"README.en.md"
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"./themes/cc-light.json"
|
|
35
36
|
],
|
|
36
37
|
"image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/welcome.webp",
|
|
37
|
-
"video": "https://github.com/user-attachments/assets/
|
|
38
|
+
"video": "https://github.com/user-attachments/assets/6c858000-fdad-43f9-957f-4d0278648498"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
41
|
"test": "node --test tests/**/*.test.ts",
|