@herbertgao/pi-extensions 2026.10.4 → 2026.10.6
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 +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +72 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +262 -48
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-currency.ts +29 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +96 -59
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/git-stats.ts +162 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +204 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +51 -86
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +212 -74
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +50 -362
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +35 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +28 -26
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +450 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +173 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +5 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +36 -10
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +140 -51
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +44 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -803
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +9 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
type ExtensionCommandContext,
|
|
5
5
|
type ToolInfo,
|
|
6
|
+
copyToClipboard,
|
|
6
7
|
estimateTokens,
|
|
7
8
|
formatSkillsForPrompt,
|
|
8
9
|
getMarkdownTheme,
|
|
@@ -54,6 +55,30 @@ export function escCloseHitbox(bounds: DialogBounds): {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
const COPY_LABEL = "[copy]";
|
|
59
|
+
const ESC_LABEL = "[esc]";
|
|
60
|
+
|
|
61
|
+
/** 测试可替换;生产使用 pi 的剪贴板实现。 */
|
|
62
|
+
let copyTextImpl: (text: string) => Promise<void> = (text) => copyToClipboard(text);
|
|
63
|
+
|
|
64
|
+
export function setPreviewCopyImpl(impl: ((text: string) => Promise<void>) | null): void {
|
|
65
|
+
copyTextImpl = impl ?? ((text) => copyToClipboard(text));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 1-based terminal hitbox of the [copy] button, one space left of [esc] on the same title row. */
|
|
69
|
+
export function copyButtonHitbox(bounds: DialogBounds): {
|
|
70
|
+
row: number;
|
|
71
|
+
startCol: number;
|
|
72
|
+
endCol: number;
|
|
73
|
+
} {
|
|
74
|
+
const esc = escCloseHitbox(bounds);
|
|
75
|
+
return {
|
|
76
|
+
row: esc.row,
|
|
77
|
+
startCol: esc.startCol - visibleWidth(COPY_LABEL) - 1,
|
|
78
|
+
endCol: esc.startCol - 2,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
let activeContextOverlays = 0;
|
|
58
83
|
|
|
59
84
|
/** fullscreen 输入包装用于把鼠标事件继续传给当前 context 主弹框或文本预览 overlay。 */
|
|
@@ -81,7 +106,9 @@ export async function showTextPreview(
|
|
|
81
106
|
let pageSize = 1;
|
|
82
107
|
let totalLines = 1;
|
|
83
108
|
let escHovered = false;
|
|
109
|
+
let copyHovered = false;
|
|
84
110
|
let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
111
|
+
let copyHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
85
112
|
let scrollbarHitbox:
|
|
86
113
|
| {
|
|
87
114
|
col: number;
|
|
@@ -108,6 +135,24 @@ export async function showTextPreview(
|
|
|
108
135
|
tui.requestRender();
|
|
109
136
|
};
|
|
110
137
|
|
|
138
|
+
const setCopyHovered = (hovered: boolean): void => {
|
|
139
|
+
if (hovered === copyHovered) return;
|
|
140
|
+
copyHovered = hovered;
|
|
141
|
+
tui.requestRender();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const copyPreview = (): void => {
|
|
145
|
+
void copyTextImpl(content)
|
|
146
|
+
.then(() => {
|
|
147
|
+
const flash = (tui as { flash?: (message: string) => void }).flash;
|
|
148
|
+
if (typeof flash === "function") flash("Copied!");
|
|
149
|
+
else ctx.ui.notify("Copied preview to clipboard", "info");
|
|
150
|
+
})
|
|
151
|
+
.catch((error: unknown) => {
|
|
152
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Copy failed", "error");
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
111
156
|
const dragScrollbarTo = (mouseRow: number): void => {
|
|
112
157
|
if (!scrollbarHitbox || scrollbarDragOffset === null) return;
|
|
113
158
|
const trackSize = scrollbarHitbox.endRow - scrollbarHitbox.startRow + 1;
|
|
@@ -151,7 +196,14 @@ export async function showTextPreview(
|
|
|
151
196
|
mouse.col >= escHitbox.startCol &&
|
|
152
197
|
mouse.col <= escHitbox.endCol,
|
|
153
198
|
);
|
|
199
|
+
const overCopy = Boolean(
|
|
200
|
+
copyHitbox &&
|
|
201
|
+
mouse.row === copyHitbox.row &&
|
|
202
|
+
mouse.col >= copyHitbox.startCol &&
|
|
203
|
+
mouse.col <= copyHitbox.endCol,
|
|
204
|
+
);
|
|
154
205
|
setEscHovered(overEsc);
|
|
206
|
+
setCopyHovered(overCopy);
|
|
155
207
|
const button = mouseBaseButton(mouse.code);
|
|
156
208
|
const motion = (mouse.code & 32) !== 0;
|
|
157
209
|
if (button === 0 && !motion) {
|
|
@@ -159,6 +211,10 @@ export async function showTextPreview(
|
|
|
159
211
|
done(undefined);
|
|
160
212
|
return;
|
|
161
213
|
}
|
|
214
|
+
if (overCopy) {
|
|
215
|
+
copyPreview();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
162
218
|
if (
|
|
163
219
|
scrollbarHitbox &&
|
|
164
220
|
mouse.col === scrollbarHitbox.col &&
|
|
@@ -187,7 +243,7 @@ export async function showTextPreview(
|
|
|
187
243
|
|
|
188
244
|
render(width: number) {
|
|
189
245
|
const inner = Math.max(1, width - 2);
|
|
190
|
-
const
|
|
246
|
+
const actionsWidth = visibleWidth(`${COPY_LABEL} ${ESC_LABEL}`);
|
|
191
247
|
const bodyInner = Math.max(1, inner - 1);
|
|
192
248
|
const bodyWidth = Math.max(1, bodyInner - 1);
|
|
193
249
|
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
@@ -205,6 +261,7 @@ export async function showTextPreview(
|
|
|
205
261
|
const overlayTop = 2 + Math.floor((availableHeight - viewportHeight) / 2);
|
|
206
262
|
const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
|
|
207
263
|
escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
|
|
264
|
+
copyHitbox = copyButtonHitbox({ left: overlayLeft, top: overlayTop, width });
|
|
208
265
|
const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
|
|
209
266
|
const border = (text: string) => theme.fg("border", text);
|
|
210
267
|
const scrollable = totalLines > pageSize;
|
|
@@ -237,11 +294,11 @@ export async function showTextPreview(
|
|
|
237
294
|
});
|
|
238
295
|
const start = totalLines === 0 ? 0 : scrollOffset + 1;
|
|
239
296
|
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
240
|
-
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [esc] close`;
|
|
297
|
+
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [copy] · [esc] close`;
|
|
241
298
|
|
|
242
299
|
return [
|
|
243
300
|
border(`╭${"─".repeat(inner)}╮`),
|
|
244
|
-
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner -
|
|
301
|
+
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - actionsWidth)}${theme.fg(copyHovered ? "text" : "muted", COPY_LABEL)} ${theme.fg(escHovered ? "text" : "muted", ESC_LABEL)}${border("│")}`,
|
|
245
302
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
246
303
|
...bodyRows,
|
|
247
304
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
@@ -273,6 +330,27 @@ const tokenEstimate = (value: unknown): number => {
|
|
|
273
330
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
274
331
|
};
|
|
275
332
|
|
|
333
|
+
/** system 消息对请求的贡献:正文 + 各 section 文本 + 声明的工具 schema,与 pi 的 estimateTokens 同口径。 */
|
|
334
|
+
function systemMessageTokens(message: {
|
|
335
|
+
content?: unknown;
|
|
336
|
+
sections?: Record<string, string | null> | null;
|
|
337
|
+
toolsAdded?: unknown;
|
|
338
|
+
toolsRemoved?: unknown;
|
|
339
|
+
}): number {
|
|
340
|
+
let chars = typeof message.content === "string" ? message.content.length : 0;
|
|
341
|
+
for (const section of Object.values(message.sections ?? {})) {
|
|
342
|
+
if (typeof section === "string") chars += section.length;
|
|
343
|
+
}
|
|
344
|
+
if (message.toolsAdded) chars += JSON.stringify(message.toolsAdded).length;
|
|
345
|
+
if (message.toolsRemoved) chars += JSON.stringify(message.toolsRemoved).length;
|
|
346
|
+
return Math.ceil(chars / 4);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** prompt 组装会把 CRLF 归一成 LF,匹配前对齐行尾。 */
|
|
350
|
+
function normalizeLineEndings(text: string): string {
|
|
351
|
+
return text.replace(/\r\n?/g, "\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
276
354
|
/** 只统计确实嵌进 system prompt 的片段,避免源文件预览把占用加两遍。 */
|
|
277
355
|
function embeddedTokens(prompt: string, chunk: string): number {
|
|
278
356
|
if (!chunk || !prompt.includes(chunk)) return 0;
|
|
@@ -348,24 +426,24 @@ export function collectContextBreakdown(
|
|
|
348
426
|
allTools: ToolInfo[],
|
|
349
427
|
): ContextBreakdown {
|
|
350
428
|
const options = (ctx.getSystemPromptOptions?.() ?? {}) as BuildSystemPromptOptions;
|
|
351
|
-
const systemPrompt = typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
|
|
352
429
|
const selectedTools = new Set(options.selectedTools ?? ["read", "bash", "edit", "write"]);
|
|
353
430
|
const toolDefinitionPreview: string[] = [];
|
|
354
431
|
const toolResultPreview: string[] = [];
|
|
355
432
|
const contextPreview: string[] = [];
|
|
433
|
+
const systemPromptPreview: string[] = [];
|
|
356
434
|
let toolDefinitionTokens = 0;
|
|
357
435
|
let toolResultTokens = 0;
|
|
358
436
|
let contextTokens = 0;
|
|
437
|
+
let systemTokens = 0;
|
|
359
438
|
|
|
360
439
|
const memoryPreview: string[] = [];
|
|
361
|
-
|
|
362
|
-
for (const file of
|
|
363
|
-
memoryTokens += embeddedTokens(systemPrompt, file.content);
|
|
440
|
+
const contextFiles = options.contextFiles ?? [];
|
|
441
|
+
for (const file of contextFiles) {
|
|
364
442
|
memoryPreview.push(`## ${file.path}\n\n${previewValue(file.content)}`);
|
|
365
443
|
}
|
|
366
444
|
|
|
367
|
-
const
|
|
368
|
-
const
|
|
445
|
+
const skills = options.skills ?? [];
|
|
446
|
+
const skillsText = formatSkillsForPrompt(skills).trim();
|
|
369
447
|
|
|
370
448
|
for (const tool of allTools) {
|
|
371
449
|
if (!selectedTools.has(tool.name)) continue;
|
|
@@ -378,21 +456,51 @@ export function collectContextBreakdown(
|
|
|
378
456
|
toolDefinitionPreview.push(`## Definition: ${tool.name}\n\n${previewValue(definition)}`);
|
|
379
457
|
}
|
|
380
458
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
459
|
+
let memoryTokens = 0;
|
|
460
|
+
let skillsTokens = 0;
|
|
461
|
+
for (const projected of ctx.sessionManager.buildSessionProjection().entries) {
|
|
462
|
+
const source = projected.sourceEntry;
|
|
463
|
+
for (const message of projected.messages) {
|
|
464
|
+
if (message.role === "system") {
|
|
465
|
+
systemTokens += systemMessageTokens(message);
|
|
466
|
+
// Memory / skills 按投影中的实际 section 文本切分,与 system prompt 增量互补、不重不漏。
|
|
467
|
+
const sections = (message.sections ?? {}) as Record<string, string | null>;
|
|
468
|
+
const projectContext =
|
|
469
|
+
typeof sections.project_context === "string"
|
|
470
|
+
? normalizeLineEndings(sections.project_context)
|
|
471
|
+
: "";
|
|
472
|
+
for (const file of contextFiles) {
|
|
473
|
+
const chunk = `<project_instructions path="${file.path}">\n${normalizeLineEndings(file.content)}\n</project_instructions>`;
|
|
474
|
+
memoryTokens += embeddedTokens(projectContext, chunk);
|
|
475
|
+
systemTokens -= embeddedTokens(projectContext, chunk);
|
|
476
|
+
}
|
|
477
|
+
const skillsSection =
|
|
478
|
+
typeof sections.skills === "string" ? normalizeLineEndings(sections.skills) : "";
|
|
479
|
+
skillsTokens += embeddedTokens(skillsSection, skillsText);
|
|
480
|
+
systemTokens -= embeddedTokens(skillsSection, skillsText);
|
|
481
|
+
systemPromptPreview.push(
|
|
482
|
+
(source.type === "compaction"
|
|
483
|
+
? "## System prompt snapshot at compaction\n\n"
|
|
484
|
+
: "## System prompt update\n\n") +
|
|
485
|
+
previewValue({
|
|
486
|
+
content: message.content,
|
|
487
|
+
sections: message.sections,
|
|
488
|
+
toolsAdded: (message.toolsAdded ?? []).map((tool: { name?: string }) => tool?.name),
|
|
489
|
+
toolsRemoved: (message.toolsRemoved ?? []).map(
|
|
490
|
+
(tool: { name?: string }) => tool?.name,
|
|
491
|
+
),
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
} else if (message.role === "assistant") {
|
|
495
|
+
contextTokens += estimateTokens(message);
|
|
385
496
|
for (const block of message.content) {
|
|
386
497
|
if (block.type === "toolCall") {
|
|
387
|
-
contextTokens += tokenEstimate(block.name) + tokenEstimate(block.arguments);
|
|
388
498
|
contextPreview.push(
|
|
389
499
|
`## Assistant tool call: ${block.name}\n\n${previewValue(block.arguments)}`,
|
|
390
500
|
);
|
|
391
501
|
} else if (block.type === "text") {
|
|
392
|
-
contextTokens += tokenEstimate(block.text);
|
|
393
502
|
contextPreview.push(`## Assistant\n\n${block.text}`);
|
|
394
503
|
} else if (block.type === "thinking") {
|
|
395
|
-
contextTokens += tokenEstimate(block.thinking);
|
|
396
504
|
contextPreview.push(`## Assistant thinking\n\n${block.thinking}`);
|
|
397
505
|
}
|
|
398
506
|
}
|
|
@@ -402,8 +510,8 @@ export function collectContextBreakdown(
|
|
|
402
510
|
`## Result: ${message.toolName}\n\n${previewValue(message.content)}`,
|
|
403
511
|
);
|
|
404
512
|
} else if (message.role === "bashExecution") {
|
|
405
|
-
|
|
406
|
-
|
|
513
|
+
contextTokens += estimateTokens(message);
|
|
514
|
+
contextPreview.push(
|
|
407
515
|
`## Bash\n\nCommand:\n\n${previewValue(message.command)}\n\nOutput:\n\n${previewValue(message.output)}`,
|
|
408
516
|
);
|
|
409
517
|
} else if (message.role === "branchSummary" || message.role === "compactionSummary") {
|
|
@@ -413,18 +521,10 @@ export function collectContextBreakdown(
|
|
|
413
521
|
contextTokens += estimateTokens(message);
|
|
414
522
|
contextPreview.push(`## ${message.role}\n\n${previewValue(message.content)}`);
|
|
415
523
|
}
|
|
416
|
-
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
417
|
-
contextTokens += tokenEstimate(entry.summary);
|
|
418
|
-
contextPreview.push(
|
|
419
|
-
`## ${entry.type === "compaction" ? "Compaction" : "Branch summary"}\n\n${entry.summary}`,
|
|
420
|
-
);
|
|
421
|
-
} else if (entry.type === "custom_message") {
|
|
422
|
-
contextTokens += tokenEstimate(entry.content);
|
|
423
|
-
contextPreview.push(`## Custom: ${entry.customType}\n\n${previewValue(entry.content)}`);
|
|
424
524
|
}
|
|
425
525
|
}
|
|
426
526
|
|
|
427
|
-
|
|
527
|
+
systemTokens = Math.max(0, systemTokens);
|
|
428
528
|
return {
|
|
429
529
|
parts: [
|
|
430
530
|
{ label: "System prompt", tokens: systemTokens, color: "accent" },
|
|
@@ -435,7 +535,10 @@ export function collectContextBreakdown(
|
|
|
435
535
|
{ label: "Context", tokens: contextTokens, color: "warning" },
|
|
436
536
|
] satisfies ContextPart[],
|
|
437
537
|
previews: {
|
|
438
|
-
systemPrompt:
|
|
538
|
+
systemPrompt:
|
|
539
|
+
systemPromptPreview.join("\n\n") ||
|
|
540
|
+
(typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "") ||
|
|
541
|
+
"No system prompt.",
|
|
439
542
|
memoryFiles: memoryPreview.join("\n\n") || "No memory files in context.",
|
|
440
543
|
skills: skillsText || "No skills in context.",
|
|
441
544
|
tools: toolDefinitionPreview.join("\n\n") || "No active tool definitions.",
|
|
@@ -439,6 +439,8 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
439
439
|
});
|
|
440
440
|
|
|
441
441
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
442
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
443
|
+
if (!("prompt" in event)) return;
|
|
442
444
|
const referenceIds = extractSessionReferenceIds(event.prompt);
|
|
443
445
|
if (referenceIds.length === 0) return;
|
|
444
446
|
|
|
@@ -26,6 +26,7 @@ const MENTION_PATTERN = /(?:^|[\t ])@([^\s@]*)$/;
|
|
|
26
26
|
const SUBAGENT_REFERENCE_PATTERN = /(?:^|\s)@subagent:\[([^\]]+)](?![\w-])/g;
|
|
27
27
|
|
|
28
28
|
function getAgentDir(): string {
|
|
29
|
+
// PI_CODING_AGENT_DIR 本身就是 agent 目录(默认 ~/.pi/agent),不是家目录。
|
|
29
30
|
const override = process.env.PI_CODING_AGENT_DIR;
|
|
30
31
|
return join(override ? override : join(homedir(), ".pi", "agent"), "agents");
|
|
31
32
|
}
|
|
@@ -214,6 +215,8 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
|
|
|
214
215
|
|
|
215
216
|
// Inject as a request message, not systemPrompt, so the cached prefix stays stable.
|
|
216
217
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
218
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
219
|
+
if (!("prompt" in event)) return;
|
|
217
220
|
const agents = getAgents();
|
|
218
221
|
if (agents.length === 0) return;
|
|
219
222
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const KNOWN_CURRENCIES = new Set(Intl.supportedValuesOf("currency"));
|
|
2
|
+
|
|
3
|
+
/** 1 美元费用对应的显示币种数量。面板切换币种时写入。 */
|
|
4
|
+
export const FOOTER_CURRENCY_RATES = {
|
|
5
|
+
CNY: 7.12,
|
|
6
|
+
EUR: 0.92,
|
|
7
|
+
JPY: 150,
|
|
8
|
+
GBP: 0.76,
|
|
9
|
+
INR: 96.25,
|
|
10
|
+
HKD: 7.78,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export const FOOTER_CURRENCY_PRESETS = ["USD", ...Object.keys(FOOTER_CURRENCY_RATES)] as const;
|
|
14
|
+
|
|
15
|
+
/** 不在表里的币种,包括 USD,返回 null。 */
|
|
16
|
+
export function presetFooterCurrencyRate(currency: string): number | null {
|
|
17
|
+
const code = currency.trim().toUpperCase();
|
|
18
|
+
return FOOTER_CURRENCY_RATES[code as keyof typeof FOOTER_CURRENCY_RATES] ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 底栏费用文本。USD 或不支持的币种保持美元写法。没有倍率时只换符号。 */
|
|
22
|
+
export function formatFooterCost(cost: number, currency: string, rate: number | null): string {
|
|
23
|
+
const usd = `$${cost.toFixed(2)}`;
|
|
24
|
+
if (currency === "USD" || !KNOWN_CURRENCIES.has(currency)) return usd;
|
|
25
|
+
const formatted = new Intl.NumberFormat("en", { style: "currency", currency }).format(
|
|
26
|
+
rate === null ? cost : cost * rate,
|
|
27
|
+
);
|
|
28
|
+
return rate === null ? formatted : `≈${formatted}`;
|
|
29
|
+
}
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
17
|
-
import { execFile } from "node:child_process";
|
|
18
17
|
import { existsSync } from "node:fs";
|
|
19
18
|
import { homedir } from "node:os";
|
|
20
19
|
import { join } from "node:path";
|
|
21
20
|
import { pathToFileURL } from "node:url";
|
|
22
21
|
import { config } from "../../config/config.ts";
|
|
23
22
|
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
23
|
+
import { formatFooterCost } from "./footer-currency.ts";
|
|
24
|
+
import { formatTokenCount } from "../../utils/format.ts";
|
|
24
25
|
import {
|
|
25
26
|
PI_USAGE_KEY,
|
|
26
27
|
isSkippedFooterStatusKey,
|
|
@@ -28,7 +29,11 @@ import {
|
|
|
28
29
|
visibleFooterPluginTexts,
|
|
29
30
|
type FooterChipLayout,
|
|
30
31
|
} from "./footer-layout.ts";
|
|
32
|
+
import { createGitStatsRefresher, readGitStats, type GitStats } from "./git-stats.ts";
|
|
31
33
|
import { MCP_STATUS_KEY, buildMcpChip } from "./mcp-chip.ts";
|
|
34
|
+
import { currentTtftMs, displayTokenRate, formatTokenRate } from "./token-rate.ts";
|
|
35
|
+
|
|
36
|
+
export { parseGitStats } from "./git-stats.ts";
|
|
32
37
|
|
|
33
38
|
const GIT_REFRESH_INTERVAL_MS = 10_000;
|
|
34
39
|
const USAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -48,7 +53,48 @@ export function footerGlyphs(nerdIcons: boolean): { git: string; cache: string;
|
|
|
48
53
|
: { git: "", cache: "", mcp: "" };
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
/**
|
|
57
|
+
* 思考等级的填充圆标:等级越高填得越满。
|
|
58
|
+
* 全部取自同一圆径的几何圆,避开占位虚线圆和偏小的半圆。
|
|
59
|
+
*/
|
|
60
|
+
const EFFORT_SYMBOL: Record<string, string> = {
|
|
61
|
+
minimal: "\u25cb",
|
|
62
|
+
low: "\u25ce",
|
|
63
|
+
medium: "\u25d4",
|
|
64
|
+
high: "\u25d5",
|
|
65
|
+
xhigh: "\u25c9",
|
|
66
|
+
max: "\u25cf",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** 模型名后直接接圆标和等级,中间不用分隔点。 */
|
|
70
|
+
export function modelChip(
|
|
71
|
+
theme: { fg: (color: string, text: string) => string },
|
|
72
|
+
modelLabel: string,
|
|
73
|
+
level: string,
|
|
74
|
+
thinkingColor: (text: string) => string,
|
|
75
|
+
): string {
|
|
76
|
+
const symbol = EFFORT_SYMBOL[level];
|
|
77
|
+
if (!symbol) return theme.fg("accent", modelLabel);
|
|
78
|
+
return `${theme.fg("accent", modelLabel)} ${thinkingColor(`${symbol} ${level}`)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** git 状态符号按职能分色:冲突/落后 error,修改 warning,未跟踪 muted,领先 success。 */
|
|
82
|
+
export function colorGitStatus(
|
|
83
|
+
theme: { fg: (color: string, text: string) => string },
|
|
84
|
+
status: string,
|
|
85
|
+
): string {
|
|
86
|
+
let out = "";
|
|
87
|
+
let color = "dim";
|
|
88
|
+
for (const ch of status) {
|
|
89
|
+
if (ch === "=" || ch === "\u21e3") color = "error";
|
|
90
|
+
else if (ch === "!") color = "warning";
|
|
91
|
+
else if (ch === "?") color = "muted";
|
|
92
|
+
else if (ch === "\u21e1") color = "success";
|
|
93
|
+
else if (!/[0-9]/.test(ch)) color = "dim";
|
|
94
|
+
out += theme.fg(color, ch);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
52
98
|
|
|
53
99
|
type XaiFooterReport = {
|
|
54
100
|
buckets?: Array<{
|
|
@@ -61,19 +107,6 @@ type XaiFooterReport = {
|
|
|
61
107
|
metrics?: Array<{ id?: string; value?: unknown }>;
|
|
62
108
|
};
|
|
63
109
|
|
|
64
|
-
export function parseGitStats(stdout: string): GitStats {
|
|
65
|
-
let add = 0;
|
|
66
|
-
let del = 0;
|
|
67
|
-
for (const line of stdout.split("\n")) {
|
|
68
|
-
const match = line.match(/^(\d+)\s+(\d+)/);
|
|
69
|
-
if (match) {
|
|
70
|
-
add += Number(match[1]);
|
|
71
|
-
del += Number(match[2]);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return { add, del };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
110
|
export function formatXaiFooterChip(report: XaiFooterReport): string | undefined {
|
|
78
111
|
const included = report.buckets?.find((b) => b.id === "included-allowance");
|
|
79
112
|
if (included?.unit === "percent" && typeof included.used === "number") {
|
|
@@ -180,7 +213,12 @@ function colorUsageChip(theme: any, text: string): string {
|
|
|
180
213
|
}
|
|
181
214
|
|
|
182
215
|
let currentTui: any = undefined;
|
|
183
|
-
let refreshCurrentGitStats: (() => void) | undefined;
|
|
216
|
+
let refreshCurrentGitStats: ((reset?: boolean) => void) | undefined;
|
|
217
|
+
|
|
218
|
+
/** Apply a /ccstyle Git mode change immediately, without recreating the footer. */
|
|
219
|
+
export function refreshFooterGitStats(): void {
|
|
220
|
+
refreshCurrentGitStats?.(true);
|
|
221
|
+
}
|
|
184
222
|
let refreshCurrentUsage: (() => void) | undefined;
|
|
185
223
|
let refreshCurrentMcpChip: (() => void) | undefined;
|
|
186
224
|
/** 工具/命令列表在 pi API 上,而 footer 工厂只拿得到 ctx,加载时记下来。 */
|
|
@@ -192,33 +230,23 @@ const createCustomFooterFactory =
|
|
|
192
230
|
const sep = theme.fg("muted", " · ");
|
|
193
231
|
const joinChips = (parts: string[]) => parts.filter(Boolean).join(sep);
|
|
194
232
|
let gitStats: GitStats | undefined;
|
|
195
|
-
let gitRefreshRunning = false;
|
|
196
233
|
let localUsageChip = "";
|
|
197
234
|
let usageGeneration = 0;
|
|
198
235
|
let usageAbort: AbortController | undefined;
|
|
199
236
|
let disposed = false;
|
|
200
237
|
|
|
201
|
-
// 查询在 render
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (gitStats?.add === next.add && gitStats.del === next.del) return;
|
|
214
|
-
gitStats = next;
|
|
215
|
-
tui.requestRender();
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
};
|
|
219
|
-
refreshCurrentGitStats = refreshGitStats;
|
|
220
|
-
refreshGitStats();
|
|
221
|
-
const gitRefreshTimer = setInterval(refreshGitStats, GIT_REFRESH_INTERVAL_MS);
|
|
238
|
+
// 查询在 render 外执行;模式/分支变化清旧值,普通轮询只在统计变化时重绘。
|
|
239
|
+
const gitRefresher = createGitStatsRefresher({
|
|
240
|
+
getMode: () => config.footerGitStatsMode,
|
|
241
|
+
query: (mode, signal) => readGitStats(ctx.cwd, mode, signal),
|
|
242
|
+
onChange: (next) => {
|
|
243
|
+
gitStats = next;
|
|
244
|
+
tui.requestRender();
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
refreshCurrentGitStats = gitRefresher.refresh;
|
|
248
|
+
gitRefresher.refresh();
|
|
249
|
+
const gitRefreshTimer = setInterval(() => gitRefresher.refresh(), GIT_REFRESH_INTERVAL_MS);
|
|
222
250
|
gitRefreshTimer.unref?.();
|
|
223
251
|
|
|
224
252
|
const refreshUsage = () => {
|
|
@@ -314,19 +342,13 @@ const createCustomFooterFactory =
|
|
|
314
342
|
const mcpRefreshTimer = setInterval(refreshMcp, MCP_REFRESH_INTERVAL_MS);
|
|
315
343
|
mcpRefreshTimer.unref?.();
|
|
316
344
|
|
|
317
|
-
//
|
|
345
|
+
// 分支变化时重新选择基准,并拒绝旧分支的异步查询结果。
|
|
318
346
|
const unsubBranch = footerData.onBranchChange(() => {
|
|
319
|
-
|
|
347
|
+
gitRefresher.refresh(true);
|
|
320
348
|
tui.requestRender();
|
|
321
349
|
});
|
|
322
350
|
|
|
323
|
-
const fmt =
|
|
324
|
-
if (n < 1000) return `${n}`;
|
|
325
|
-
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
326
|
-
if (n < 1000000) return `${Math.round(n / 1000)}k`;
|
|
327
|
-
if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
328
|
-
return `${Math.round(n / 1000000)}M`;
|
|
329
|
-
};
|
|
351
|
+
const fmt = formatTokenCount;
|
|
330
352
|
|
|
331
353
|
// ---- pi-cc-status 风格:上下文条形图 ----
|
|
332
354
|
const BAR_WIDTH = 10;
|
|
@@ -417,16 +439,27 @@ const createCustomFooterFactory =
|
|
|
417
439
|
const glyphs = footerGlyphs(config.footerNerdIcons);
|
|
418
440
|
const cacheLabel =
|
|
419
441
|
cachePct > 0 ? `${glyphs.cache ? `${glyphs.cache} ` : ""}${Math.floor(cachePct)}%` : "";
|
|
442
|
+
const rateLabel = config.footerTokenRate
|
|
443
|
+
? formatTokenRate(
|
|
444
|
+
displayTokenRate(config.footerTokenRateMode),
|
|
445
|
+
currentTtftMs(),
|
|
446
|
+
config.footerTokenRateMode,
|
|
447
|
+
config.footerTokenRateTtft,
|
|
448
|
+
)
|
|
449
|
+
: "";
|
|
450
|
+
const rateChip = rateLabel ? theme.fg("dim", rateLabel) : "";
|
|
420
451
|
const costChip =
|
|
421
452
|
cost || usingSubscription
|
|
422
|
-
? theme.fg(
|
|
423
|
-
|
|
453
|
+
? theme.fg(
|
|
454
|
+
"dim",
|
|
455
|
+
formatFooterCost(cost, config.footerCurrency, config.footerCurrencyRate),
|
|
456
|
+
) + (usingSubscription ? theme.fg("warning", " sub") : "")
|
|
424
457
|
: "";
|
|
425
458
|
const line1 = joinChips([
|
|
426
|
-
theme
|
|
427
|
-
model?.reasoning ? thinkingColor(thinkingLevelStr) : "",
|
|
459
|
+
modelChip(theme, modelLabel, model?.reasoning ? thinkingLevelStr : "", thinkingColor),
|
|
428
460
|
barGauge(percent ?? 0) + theme.fg("dim", ` ${pctLabel}/${fmt(contextWindow)}`),
|
|
429
461
|
cacheLabel ? theme.fg("dim", cacheLabel) : "",
|
|
462
|
+
rateChip,
|
|
430
463
|
costChip,
|
|
431
464
|
...line1Plugins,
|
|
432
465
|
]);
|
|
@@ -439,14 +472,17 @@ const createCustomFooterFactory =
|
|
|
439
472
|
}
|
|
440
473
|
const branch = footerData.getGitBranch();
|
|
441
474
|
if (branch) {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
475
|
+
const s = gitStats;
|
|
476
|
+
let stats = "";
|
|
477
|
+
if (s && s.status)
|
|
478
|
+
stats = theme.fg("dim", " [") + colorGitStatus(theme, s.status) + theme.fg("dim", "]");
|
|
479
|
+
if (s && (s.add || s.del))
|
|
480
|
+
stats +=
|
|
481
|
+
theme.fg("dim", "(") +
|
|
482
|
+
theme.fg("success", `+${s.add}`) +
|
|
483
|
+
" " +
|
|
484
|
+
theme.fg("error", `−${s.del}`) +
|
|
485
|
+
theme.fg("dim", ")");
|
|
450
486
|
const gitLabel = glyphs.git ? `${glyphs.git} ${branch}` : branch;
|
|
451
487
|
place += theme.fg("muted", " on ") + gitColor(gitLabel) + stats;
|
|
452
488
|
}
|
|
@@ -467,6 +503,7 @@ const createCustomFooterFactory =
|
|
|
467
503
|
},
|
|
468
504
|
dispose(): void {
|
|
469
505
|
disposed = true;
|
|
506
|
+
gitRefresher.dispose();
|
|
470
507
|
usageAbort?.abort();
|
|
471
508
|
usageAbort = undefined;
|
|
472
509
|
clearInterval(gitRefreshTimer);
|