pi-cc-extensions 0.8.63 → 0.8.65
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 +29 -15
- package/README.md +31 -23
- package/extensions/feature/compact-thinking.ts +56 -11
- package/extensions/feature/context.ts +209 -158
- package/extensions/index.ts +0 -2
- package/extensions/renderer/mouse/hover.ts +13 -0
- package/extensions/renderer/mouse/interaction.ts +16 -5
- package/extensions/renderer/tool/message-display.ts +18 -1
- package/package.json +1 -1
- package/extensions/feature/shell/model-status.ts +0 -19
package/README.en.md
CHANGED
|
@@ -51,24 +51,38 @@ Run `/reload` after installation.
|
|
|
51
51
|
|
|
52
52
|
```js
|
|
53
53
|
{
|
|
54
|
-
"mode": "on", // on
|
|
55
|
-
"excludeRenderers": [], //
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
54
|
+
"mode": "on", // on / compact / off
|
|
55
|
+
"excludeRenderers": [], // tools keeping the native renderer; Agent always keeps its dedicated renderer
|
|
56
|
+
|
|
57
|
+
// diff
|
|
58
|
+
"diffViewMode": "auto", // layout: auto / split / unified
|
|
59
|
+
"diffIndicatorMode": "bars", // change indicators: bars / classic / none
|
|
60
|
+
"diffSplitMinWidth": 120, // min terminal width for side-by-side columns
|
|
61
|
+
"editDiffCollapsedLines": 24, // Edit collapse lines; beyond that shows the expand hint
|
|
62
|
+
"writeDiffCollapsedLines": 0, // write collapse lines; 0 = creation summary only
|
|
63
|
+
"diffWordWrap": true, // wrap long diff lines
|
|
64
|
+
"expandedPreviewMaxLines": 40, // max lines for expanded bodies
|
|
65
|
+
|
|
66
|
+
// thinking
|
|
67
|
+
"useSummaryTitlesAsThinkingTitle": true, // use latest summary as thinking title
|
|
68
|
+
"previewLines": 3, // preview lines; 0 hides
|
|
69
|
+
"animationIntervalMs": 90, // title animation interval (ms)
|
|
70
|
+
|
|
71
|
+
// ui
|
|
72
|
+
"showStartupHeader": true, // startup header (logo + tips) toggle
|
|
73
|
+
"scrollStepLines": 3, // fullscreen wheel scroll step
|
|
74
|
+
|
|
75
|
+
// features
|
|
76
|
+
"enableSessionReference": true, // @ session references
|
|
77
|
+
"enableSubagentAutocomplete": true, // @ subagent completion and delegation hints
|
|
78
|
+
"enableContextCommand": true, // /context usage check
|
|
79
|
+
"enableAgentSummary": true, // per-turn tool summary
|
|
80
|
+
"enableWorkingMessage": true, // Working... bottom token/elapsed
|
|
81
|
+
"enableAliases": true // /clear, /exit aliases
|
|
68
82
|
}
|
|
69
83
|
```
|
|
70
84
|
|
|
71
|
-
> **Fullscreen
|
|
85
|
+
> **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. Double-click an expanded panel to collapse it.
|
|
72
86
|
>
|
|
73
87
|
> **Tip**: set `markdown.mermaid` to `final` via `~/.pi/agent/settings.json` or the Mermaid diagrams option in `/settings`. Default `streaming` redraws per frame; `final` renders once at completion.
|
|
74
88
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="./assets/readme/hero.svg" width="100%"
|
|
2
|
+
<img src="./assets/readme/hero.svg" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -51,30 +51,38 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
51
51
|
|
|
52
52
|
```js
|
|
53
53
|
{
|
|
54
|
-
"mode": "on", // on
|
|
55
|
-
"excludeRenderers": [], //
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
"
|
|
54
|
+
"mode": "on", // on / compact / off
|
|
55
|
+
"excludeRenderers": [], // 走原生渲染的工具名;Agent 始终保留专用渲染器
|
|
56
|
+
|
|
57
|
+
// diff
|
|
58
|
+
"diffViewMode": "auto", // 布局:auto / split / unified
|
|
59
|
+
"diffIndicatorMode": "bars", // 变更指示:bars / classic / none
|
|
60
|
+
"diffSplitMinWidth": 120, // 左右分栏的最小终端宽度
|
|
61
|
+
"editDiffCollapsedLines": 24, // Edit 折叠行数,超出显示展开提示
|
|
62
|
+
"writeDiffCollapsedLines": 0, // write 折叠行数,0 仅显示创建摘要
|
|
63
|
+
"diffWordWrap": true, // 长 diff 行换行
|
|
64
|
+
"expandedPreviewMaxLines": 40, // 展开正文最大行数
|
|
65
|
+
|
|
66
|
+
// thinking
|
|
67
|
+
"useSummaryTitlesAsThinkingTitle": true, // 用最新摘要作思考标题
|
|
68
|
+
"previewLines": 3, // 预览行数,0 隐藏
|
|
69
|
+
"animationIntervalMs": 90, // 标题动画间隔(毫秒)
|
|
70
|
+
|
|
71
|
+
// ui
|
|
72
|
+
"showStartupHeader": true, // 启动头(logo + tips)开关
|
|
73
|
+
"scrollStepLines": 3, // fullscreen 滚轮步进
|
|
74
|
+
|
|
75
|
+
// features
|
|
76
|
+
"enableSessionReference": true, // @ session 引用
|
|
77
|
+
"enableSubagentAutocomplete": true, // @ subagent 补全与委派提示
|
|
78
|
+
"enableContextCommand": true, // /context 上下文检查
|
|
79
|
+
"enableAgentSummary": true, // 每回合工具摘要
|
|
80
|
+
"enableWorkingMessage": true, // Working... 底部 token/耗时
|
|
81
|
+
"enableAliases": true // /clear、/exit 别名
|
|
74
82
|
}
|
|
75
83
|
```
|
|
76
84
|
|
|
77
|
-
> **Fullscreen
|
|
85
|
+
> **Fullscreen**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要,双击展开面板收起。
|
|
78
86
|
>
|
|
79
87
|
> **建议**:`markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
|
|
80
88
|
|
|
@@ -88,7 +96,7 @@ npm run typecheck
|
|
|
88
96
|
|
|
89
97
|
## 兼容性
|
|
90
98
|
|
|
91
|
-
- Node.js `>=22.19.0`,Pi `^0.84.0
|
|
99
|
+
- Node.js `>=22.19.0`,Pi `^0.84.0`
|
|
92
100
|
|
|
93
101
|
## 推荐搭配
|
|
94
102
|
|
|
@@ -200,12 +200,58 @@ function cachePreview(key: string, entry: PreviewCacheEntry): void {
|
|
|
200
200
|
|
|
201
201
|
export function clearThinkingPreviewCache(): void {
|
|
202
202
|
previewCache.clear();
|
|
203
|
+
expandedWrapCache.clear();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type ExpandedWrapEntry = { text: string; lines: string[] };
|
|
207
|
+
const expandedWrapCache = new Map<string, ExpandedWrapEntry>();
|
|
208
|
+
export const THINKING_EXPANDED_WRAP_CACHE_MAX = 64;
|
|
209
|
+
|
|
210
|
+
function expandedWrapKey(messageTimestamp: number, runStartIndex: number, width: number): string {
|
|
211
|
+
return `${messageTimestamp}:${runStartIndex}:${width}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function wrapExpandedThinking(
|
|
215
|
+
messageTimestamp: number,
|
|
216
|
+
runStartIndex: number,
|
|
217
|
+
text: string,
|
|
218
|
+
width: number,
|
|
219
|
+
): string[] {
|
|
220
|
+
const key = expandedWrapKey(messageTimestamp, runStartIndex, width);
|
|
221
|
+
const hit = expandedWrapCache.get(key);
|
|
222
|
+
if (hit && hit.text === text) {
|
|
223
|
+
expandedWrapCache.delete(key);
|
|
224
|
+
expandedWrapCache.set(key, hit);
|
|
225
|
+
return hit.lines;
|
|
226
|
+
}
|
|
227
|
+
const lines = wrapTextWithAnsi(text.replace(/\t/g, " "), Math.max(1, width));
|
|
228
|
+
expandedWrapCache.delete(key);
|
|
229
|
+
while (expandedWrapCache.size >= THINKING_EXPANDED_WRAP_CACHE_MAX) {
|
|
230
|
+
const oldest = expandedWrapCache.keys().next().value;
|
|
231
|
+
if (oldest === undefined) break;
|
|
232
|
+
expandedWrapCache.delete(oldest);
|
|
233
|
+
}
|
|
234
|
+
expandedWrapCache.set(key, { text, lines });
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function evictExpandedWraps(messageTimestamp: number, runStartIndex: number): void {
|
|
239
|
+
for (const key of expandedWrapCache.keys()) {
|
|
240
|
+
const [ts, run] = key.split(":");
|
|
241
|
+
if (Number(ts) === messageTimestamp && Number(run) === runStartIndex) {
|
|
242
|
+
expandedWrapCache.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
203
245
|
}
|
|
204
246
|
|
|
205
247
|
export function thinkingPreviewCacheSize(): number {
|
|
206
248
|
return previewCache.size;
|
|
207
249
|
}
|
|
208
250
|
|
|
251
|
+
export function thinkingExpandedWrapCacheSize(): number {
|
|
252
|
+
return expandedWrapCache.size;
|
|
253
|
+
}
|
|
254
|
+
|
|
209
255
|
/** 按可见宽度估算折行数,不走 Text 全量 wrap。无换行长段也能计到隐藏行。 */
|
|
210
256
|
function countWrappedLines(text: string, contentWidth: number): number {
|
|
211
257
|
if (!text) return 0;
|
|
@@ -285,12 +331,12 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
285
331
|
private heading: string;
|
|
286
332
|
private text: string;
|
|
287
333
|
private padding: number;
|
|
288
|
-
|
|
334
|
+
readonly messageTimestamp: number;
|
|
335
|
+
readonly runStartIndex: number;
|
|
289
336
|
private style: (text: string) => string;
|
|
290
337
|
private theme: Theme | undefined;
|
|
291
338
|
private _expanded: boolean;
|
|
292
339
|
private hintHovered = false;
|
|
293
|
-
private expandedBody: { width: number; lines: string[] } | undefined;
|
|
294
340
|
private collapsedMemo:
|
|
295
341
|
| {
|
|
296
342
|
width: number;
|
|
@@ -308,11 +354,13 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
308
354
|
messageTimestamp: number,
|
|
309
355
|
style: (text: string) => string,
|
|
310
356
|
theme?: Theme,
|
|
357
|
+
runStartIndex = 0,
|
|
311
358
|
) {
|
|
312
359
|
this.heading = heading;
|
|
313
360
|
this.text = text;
|
|
314
361
|
this.padding = padding;
|
|
315
362
|
this.messageTimestamp = messageTimestamp;
|
|
363
|
+
this.runStartIndex = runStartIndex;
|
|
316
364
|
this.style = style;
|
|
317
365
|
this.theme = theme;
|
|
318
366
|
this._expanded = expandedThinking.has(messageTimestamp);
|
|
@@ -334,7 +382,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
334
382
|
if (expanded) expandedThinking.add(this.messageTimestamp);
|
|
335
383
|
else {
|
|
336
384
|
expandedThinking.delete(this.messageTimestamp);
|
|
337
|
-
this.
|
|
385
|
+
evictExpandedWraps(this.messageTimestamp, this.runStartIndex);
|
|
338
386
|
}
|
|
339
387
|
}
|
|
340
388
|
|
|
@@ -368,13 +416,10 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
368
416
|
return { lines: [], hiddenLines: 0 };
|
|
369
417
|
}
|
|
370
418
|
if (this._expanded) {
|
|
371
|
-
|
|
372
|
-
this.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
return { lines: this.expandedBody.lines, hiddenLines: 0 };
|
|
419
|
+
return {
|
|
420
|
+
lines: wrapExpandedThinking(this.messageTimestamp, this.runStartIndex, this.text, width),
|
|
421
|
+
hiddenLines: 0,
|
|
422
|
+
};
|
|
378
423
|
}
|
|
379
424
|
const preview = layoutThinkingPreview(this.text, width, padding);
|
|
380
425
|
return { lines: preview.visible, hiddenLines: preview.hiddenLines };
|
|
@@ -430,7 +475,6 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
430
475
|
|
|
431
476
|
invalidate() {
|
|
432
477
|
this.collapsedMemo = undefined;
|
|
433
|
-
this.expandedBody = undefined;
|
|
434
478
|
}
|
|
435
479
|
}
|
|
436
480
|
|
|
@@ -715,6 +759,7 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
715
759
|
message.timestamp,
|
|
716
760
|
thinkingStyle,
|
|
717
761
|
activeTheme,
|
|
762
|
+
runStartIndex,
|
|
718
763
|
),
|
|
719
764
|
);
|
|
720
765
|
|
|
@@ -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" | "customMessageLabel" | "muted" | "dim";
|
|
16
|
+
color: "accent" | "success" | "warning" | "customMessageLabel" | "muted" | "dim" | "error";
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
type PreviewKey = "systemPrompt" | "tools" | "toolResults" | "contextFiles";
|
|
19
|
+
type PreviewKey = "systemPrompt" | "memoryFiles" | "tools" | "toolResults" | "contextFiles";
|
|
20
20
|
|
|
21
21
|
type ContextPreview = {
|
|
22
22
|
key: PreviewKey;
|
|
@@ -47,11 +47,16 @@ export function escCloseHitbox(bounds: DialogBounds): {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
let
|
|
50
|
+
let activeContextOverlays = 0;
|
|
51
51
|
|
|
52
|
-
/** fullscreen
|
|
52
|
+
/** fullscreen 输入包装用于把鼠标事件继续传给当前 context 主弹框或文本预览 overlay。 */
|
|
53
53
|
export function hasActiveTextPreview(): boolean {
|
|
54
|
-
return
|
|
54
|
+
return activeContextOverlays > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 官方 fullscreen 打开 overlay 时会退回 1002;重新启用 1003 才能收到无按键 hover。 */
|
|
58
|
+
function ensureFullscreenMouseMotion(tui: any): void {
|
|
59
|
+
if (tui.mode === "fullscreen") tui.terminal?.write?.("\x1b[?1003h\x1b[?1006h");
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export async function showTextPreview(
|
|
@@ -60,10 +65,11 @@ export async function showTextPreview(
|
|
|
60
65
|
rawContent: string,
|
|
61
66
|
): Promise<void> {
|
|
62
67
|
const content = normalizePreviewText(rawContent);
|
|
63
|
-
|
|
68
|
+
activeContextOverlays++;
|
|
64
69
|
try {
|
|
65
70
|
await ctx.ui.custom(
|
|
66
71
|
(tui, theme, _keybindings, done) => {
|
|
72
|
+
ensureFullscreenMouseMotion(tui);
|
|
67
73
|
let scrollOffset = 0;
|
|
68
74
|
let pageSize = 1;
|
|
69
75
|
let totalLines = 1;
|
|
@@ -249,7 +255,7 @@ export async function showTextPreview(
|
|
|
249
255
|
},
|
|
250
256
|
);
|
|
251
257
|
} finally {
|
|
252
|
-
|
|
258
|
+
activeContextOverlays--;
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
@@ -337,6 +343,13 @@ export function collectContextBreakdown(
|
|
|
337
343
|
let toolResultTokens = 0;
|
|
338
344
|
let contextTokens = 0;
|
|
339
345
|
|
|
346
|
+
const memoryPreview: string[] = [];
|
|
347
|
+
let memoryTokens = 0;
|
|
348
|
+
for (const file of options.contextFiles ?? []) {
|
|
349
|
+
memoryTokens += tokenEstimate(file.content);
|
|
350
|
+
memoryPreview.push(`## ${file.path}\n\n${previewValue(file.content)}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
340
353
|
for (const tool of allTools) {
|
|
341
354
|
if (!selectedTools.has(tool.name)) continue;
|
|
342
355
|
const definition = {
|
|
@@ -397,12 +410,14 @@ export function collectContextBreakdown(
|
|
|
397
410
|
return {
|
|
398
411
|
parts: [
|
|
399
412
|
{ label: "System prompt", tokens: tokenEstimate(systemPrompt), color: "accent" },
|
|
413
|
+
{ label: "Memory", tokens: memoryTokens, color: "error" },
|
|
400
414
|
{ label: "Tools", tokens: toolDefinitionTokens, color: "success" },
|
|
401
415
|
{ label: "Tool results", tokens: toolResultTokens, color: "customMessageLabel" },
|
|
402
416
|
{ label: "Context", tokens: contextTokens, color: "warning" },
|
|
403
417
|
] satisfies ContextPart[],
|
|
404
418
|
previews: {
|
|
405
419
|
systemPrompt: systemPrompt || "No system prompt.",
|
|
420
|
+
memoryFiles: memoryPreview.join("\n\n") || "No memory files in context.",
|
|
406
421
|
tools: toolDefinitionPreview.join("\n\n") || "No active tool definitions.",
|
|
407
422
|
toolResults: toolResultPreview.join("\n\n") || "No tool results in the current context.",
|
|
408
423
|
contextFiles: contextPreview.join("\n\n") || "No conversation context.",
|
|
@@ -419,9 +434,10 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
419
434
|
const tools = pi.getAllTools();
|
|
420
435
|
const breakdown = collectContextBreakdown(ctx, tools);
|
|
421
436
|
const estimated = breakdown.parts.reduce((sum, part) => sum + part.tokens, 0);
|
|
422
|
-
|
|
437
|
+
// System prompt、Memory、Tools 为固定项,capParts 时保持原值不压缩。
|
|
438
|
+
const fixedTokens = breakdown.parts.slice(0, 3).reduce((sum, part) => sum + part.tokens, 0);
|
|
423
439
|
const used = Math.max(resolveUsedTokens(usage, estimated, contextWindow), fixedTokens);
|
|
424
|
-
const parts = capParts(breakdown.parts, used,
|
|
440
|
+
const parts = capParts(breakdown.parts, used, 3);
|
|
425
441
|
const attributed = parts.reduce((sum, part) => sum + part.tokens, 0);
|
|
426
442
|
const other = Math.max(0, used - attributed);
|
|
427
443
|
const free = Math.max(0, contextWindow - used);
|
|
@@ -444,6 +460,12 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
444
460
|
title: "System Prompt",
|
|
445
461
|
content: breakdown.previews.systemPrompt,
|
|
446
462
|
},
|
|
463
|
+
{
|
|
464
|
+
key: "memoryFiles",
|
|
465
|
+
label: "Memory",
|
|
466
|
+
title: "Memory Files",
|
|
467
|
+
content: breakdown.previews.memoryFiles,
|
|
468
|
+
},
|
|
447
469
|
{
|
|
448
470
|
key: "tools",
|
|
449
471
|
label: "Tools",
|
|
@@ -474,158 +496,187 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
474
496
|
let selectedPreviewIndex = 0;
|
|
475
497
|
|
|
476
498
|
while (true) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
mouse.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
499
|
+
// 主弹框也计入活动 overlay,fullscreen 下官方输入链才会把鼠标包放行给行点击。
|
|
500
|
+
activeContextOverlays++;
|
|
501
|
+
let action;
|
|
502
|
+
try {
|
|
503
|
+
action = await ctx.ui.custom(
|
|
504
|
+
(tui, theme, _keybindings, done) => {
|
|
505
|
+
ensureFullscreenMouseMotion(tui);
|
|
506
|
+
let previewHitboxes: Array<{
|
|
507
|
+
key: PreviewKey;
|
|
508
|
+
row: number;
|
|
509
|
+
startCol: number;
|
|
510
|
+
endCol: number;
|
|
511
|
+
}> = [];
|
|
512
|
+
let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
513
|
+
let escHovered = false;
|
|
514
|
+
let hoveredKey: PreviewKey | undefined;
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
invalidate() {},
|
|
518
|
+
handleInput(data: string) {
|
|
519
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
520
|
+
done(undefined);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (matchesKey(data, Key.up) && visiblePreviews.length > 0) {
|
|
524
|
+
selectedPreviewIndex =
|
|
525
|
+
(selectedPreviewIndex - 1 + visiblePreviews.length) % visiblePreviews.length;
|
|
526
|
+
tui.requestRender();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (matchesKey(data, Key.down) && visiblePreviews.length > 0) {
|
|
530
|
+
selectedPreviewIndex = (selectedPreviewIndex + 1) % visiblePreviews.length;
|
|
531
|
+
tui.requestRender();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (matchesKey(data, Key.enter)) {
|
|
535
|
+
done(visiblePreviews[selectedPreviewIndex]?.key);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const mouse = parseSgrMousePacket(data);
|
|
540
|
+
if (!mouse || mouse.final !== "M") return;
|
|
541
|
+
if ((mouse.code & 32) !== 0) {
|
|
542
|
+
// SGR 1003 的无按键 hover code 为 35,不能按左键事件过滤。
|
|
543
|
+
const overEsc = Boolean(
|
|
544
|
+
escHitbox &&
|
|
545
|
+
mouse.row === escHitbox.row &&
|
|
546
|
+
mouse.col >= escHitbox.startCol &&
|
|
547
|
+
mouse.col <= escHitbox.endCol,
|
|
548
|
+
);
|
|
549
|
+
const hovered = previewHitboxes.find(
|
|
550
|
+
(candidate) =>
|
|
551
|
+
mouse.row === candidate.row &&
|
|
552
|
+
mouse.col >= candidate.startCol &&
|
|
553
|
+
mouse.col <= candidate.endCol,
|
|
554
|
+
);
|
|
555
|
+
if (overEsc !== escHovered || hovered?.key !== hoveredKey) {
|
|
556
|
+
escHovered = overEsc;
|
|
557
|
+
hoveredKey = hovered?.key;
|
|
558
|
+
tui.requestRender();
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (mouseBaseButton(mouse.code) !== 0) return;
|
|
563
|
+
if (
|
|
564
|
+
escHitbox &&
|
|
565
|
+
mouse.row === escHitbox.row &&
|
|
566
|
+
mouse.col >= escHitbox.startCol &&
|
|
567
|
+
mouse.col <= escHitbox.endCol
|
|
568
|
+
) {
|
|
569
|
+
done(undefined);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const hitbox = previewHitboxes.find(
|
|
573
|
+
(candidate) =>
|
|
574
|
+
mouse.row === candidate.row &&
|
|
575
|
+
mouse.col >= candidate.startCol &&
|
|
576
|
+
mouse.col <= candidate.endCol,
|
|
536
577
|
);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
578
|
+
if (hitbox) {
|
|
579
|
+
selectedPreviewIndex = Math.max(
|
|
580
|
+
0,
|
|
581
|
+
visiblePreviews.findIndex((preview) => preview.key === hitbox.key),
|
|
582
|
+
);
|
|
583
|
+
done(hitbox.key);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
render(width: number) {
|
|
587
|
+
const inner = Math.max(1, width - 2);
|
|
588
|
+
const escWidth = visibleWidth("[esc]");
|
|
589
|
+
const percent = contextWindow > 0 ? (used / contextWindow) * 100 : 0;
|
|
590
|
+
const title = theme.bold(theme.fg("accent", "Context Usage"));
|
|
591
|
+
const subtitle = `${formatTokens(used)} / ${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`;
|
|
592
|
+
const barWidth = Math.max(1, Math.min(60, inner - 2));
|
|
593
|
+
let remaining = barWidth;
|
|
594
|
+
const segments = allParts
|
|
595
|
+
.map((part, index) => {
|
|
596
|
+
const cells =
|
|
597
|
+
index === allParts.length - 1
|
|
598
|
+
? remaining
|
|
599
|
+
: Math.min(
|
|
600
|
+
remaining,
|
|
601
|
+
Math.round((part.tokens / Math.max(1, contextWindow)) * barWidth),
|
|
602
|
+
);
|
|
603
|
+
remaining -= cells;
|
|
604
|
+
return theme.fg(part.color, "█".repeat(Math.max(0, cells)));
|
|
605
|
+
})
|
|
606
|
+
.join("");
|
|
607
|
+
const labelWidth = Math.min(
|
|
608
|
+
24,
|
|
609
|
+
Math.max(...allParts.map((part) => part.label.length)),
|
|
610
|
+
);
|
|
611
|
+
const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
|
|
612
|
+
const partRows = allParts.map((part) => {
|
|
613
|
+
const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
|
|
614
|
+
const swatch = theme.fg(part.color, "■");
|
|
615
|
+
const label = part.label.padEnd(labelWidth);
|
|
616
|
+
const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
|
|
617
|
+
const selected = part.label === selectedLabel;
|
|
618
|
+
const hoverLabel = visiblePreviews.find((p) => p.key === hoveredKey)?.label;
|
|
619
|
+
const prefix = selected ? "› " : " ";
|
|
620
|
+
const row = padLine(`${prefix}${swatch} ${label} ${amount}`, inner);
|
|
621
|
+
if (selected) return theme.bg("selectedBg", row);
|
|
622
|
+
return part.label === hoverLabel ? theme.bg("customMessageBg", row) : row;
|
|
623
|
+
});
|
|
624
|
+
const border = (text: string) => theme.fg("border", text);
|
|
625
|
+
const lines = [
|
|
626
|
+
border(`╭${"─".repeat(inner)}╮`),
|
|
627
|
+
`${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
|
|
628
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
629
|
+
`${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
|
|
630
|
+
`${border("│")}${" ".repeat(inner)}${border("│")}`,
|
|
631
|
+
...partRows.map((row) => `${border("│")}${row}${border("│")}`),
|
|
632
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
633
|
+
`${border("│")}${padLine(theme.fg("dim", " ↑↓ select · Click / Enter to preview · [esc] close"), inner)}${border("│")}`,
|
|
634
|
+
border(`╰${"─".repeat(inner)}╯`),
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
638
|
+
const maxHeight = Math.min(
|
|
639
|
+
Math.max(1, Math.floor(terminalHeight * 0.9)),
|
|
640
|
+
Math.max(1, terminalHeight - 2),
|
|
641
|
+
);
|
|
642
|
+
const visibleHeight = Math.min(lines.length, maxHeight);
|
|
643
|
+
const overlayTop =
|
|
644
|
+
1 + Math.floor((Math.max(1, terminalHeight - 2) - visibleHeight) / 2);
|
|
645
|
+
const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
|
|
646
|
+
escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
|
|
647
|
+
previewHitboxes = visiblePreviews.flatMap((preview) => {
|
|
648
|
+
const partIndex = allParts.findIndex((part) => part.label === preview.label);
|
|
649
|
+
const line = 5 + partIndex;
|
|
650
|
+
return partIndex >= 0 && line < visibleHeight
|
|
651
|
+
? [
|
|
652
|
+
{
|
|
653
|
+
key: preview.key,
|
|
654
|
+
row: overlayTop + line + 1,
|
|
655
|
+
startCol: overlayLeft + 1,
|
|
656
|
+
endCol: overlayLeft + width,
|
|
657
|
+
},
|
|
658
|
+
]
|
|
659
|
+
: [];
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
return lines;
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
overlay: true,
|
|
668
|
+
overlayOptions: {
|
|
669
|
+
anchor: "center",
|
|
670
|
+
width: 64,
|
|
671
|
+
minWidth: 44,
|
|
672
|
+
maxHeight: "90%",
|
|
673
|
+
margin: 1,
|
|
615
674
|
},
|
|
616
|
-
};
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
overlay: true,
|
|
620
|
-
overlayOptions: {
|
|
621
|
-
anchor: "center",
|
|
622
|
-
width: 64,
|
|
623
|
-
minWidth: 44,
|
|
624
|
-
maxHeight: "90%",
|
|
625
|
-
margin: 1,
|
|
626
675
|
},
|
|
627
|
-
|
|
628
|
-
|
|
676
|
+
);
|
|
677
|
+
} finally {
|
|
678
|
+
activeContextOverlays--;
|
|
679
|
+
}
|
|
629
680
|
|
|
630
681
|
if (!action) break;
|
|
631
682
|
const preview = previewByKey.get(action as PreviewKey);
|
package/extensions/index.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { config } from "./config/config.ts";
|
|
|
5
5
|
import piAliases from "./feature/shell/aliases.ts";
|
|
6
6
|
import piStartupHeader from "./feature/shell/startup-header.ts";
|
|
7
7
|
import workingMessage from "./feature/shell/working-message.ts";
|
|
8
|
-
import modelStatus from "./feature/shell/model-status.ts";
|
|
9
8
|
|
|
10
9
|
// feature
|
|
11
10
|
import agentAutocomplete from "./feature/reference/subagent.ts";
|
|
@@ -23,7 +22,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
23
22
|
if (config.enableAliases) piAliases(pi);
|
|
24
23
|
piStartupHeader(pi);
|
|
25
24
|
if (config.enableWorkingMessage) workingMessage(pi);
|
|
26
|
-
modelStatus(pi);
|
|
27
25
|
|
|
28
26
|
// render stack:thinking controller 直接交给 style 作 query
|
|
29
27
|
markdownEnhance(pi);
|
|
@@ -54,6 +54,16 @@ export function setHoveredThinking(block: ThinkingPreviewBlock | null): boolean
|
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
let hoveredMessageDisplay: any = null;
|
|
58
|
+
|
|
59
|
+
export function setHoveredMessageDisplay(component: any): boolean {
|
|
60
|
+
if (component === hoveredMessageDisplay) return false;
|
|
61
|
+
hoveredMessageDisplay?.setHintHovered?.(false);
|
|
62
|
+
hoveredMessageDisplay = component;
|
|
63
|
+
component?.setHintHovered?.(true);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
let hoveredToolIoView: ExpandedToolIoView | null = null;
|
|
58
68
|
let hoveredToolIoSection: ToolIoSection | null = null;
|
|
59
69
|
|
|
@@ -110,6 +120,7 @@ export type FullscreenHoverTarget =
|
|
|
110
120
|
| { kind: "button" }
|
|
111
121
|
| { kind: "group"; component: ToolGroupComponent }
|
|
112
122
|
| { kind: "thinking"; component: ThinkingPreviewBlock }
|
|
123
|
+
| { kind: "message"; component: any }
|
|
113
124
|
| { kind: "assistant"; component: any }
|
|
114
125
|
| {
|
|
115
126
|
kind: "tool";
|
|
@@ -133,6 +144,8 @@ export function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | n
|
|
|
133
144
|
if (setHoveredToolGroup(nextGroup)) changed = true;
|
|
134
145
|
const nextThinking = target?.kind === "thinking" ? target.component : null;
|
|
135
146
|
if (setHoveredThinking(nextThinking)) changed = true;
|
|
147
|
+
const nextMessage = target?.kind === "message" ? target.component : null;
|
|
148
|
+
if (setHoveredMessageDisplay(nextMessage)) changed = true;
|
|
136
149
|
const nextAssistant = target?.kind === "assistant" ? target.component : null;
|
|
137
150
|
if (setHoveredCompactAssistant(nextAssistant)) changed = true;
|
|
138
151
|
const nextView = target?.kind === "tool" ? target.view : null;
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
setHoveredToolCallId,
|
|
60
60
|
setHoveredToolGroup,
|
|
61
61
|
setHoveredThinking,
|
|
62
|
+
setHoveredMessageDisplay,
|
|
62
63
|
setHoveredToolIo,
|
|
63
64
|
type FullscreenHoverTarget,
|
|
64
65
|
} from "./hover.ts";
|
|
@@ -155,13 +156,20 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
const EXPAND_PANEL_DOUBLE_CLICK_MS = 400;
|
|
158
|
-
let lastExpandPanelClick: {
|
|
159
|
+
let lastExpandPanelClick: { id: unknown; at: number } | null = null;
|
|
160
|
+
|
|
161
|
+
function expandPanelIdentity(card: any): unknown {
|
|
162
|
+
return card instanceof ThinkingPreviewBlock
|
|
163
|
+
? `${card.messageTimestamp}:${card.runStartIndex}`
|
|
164
|
+
: card;
|
|
165
|
+
}
|
|
159
166
|
|
|
160
167
|
function isExpandPanelDoubleClick(card: any): boolean {
|
|
161
168
|
const now = Date.now();
|
|
169
|
+
const id = expandPanelIdentity(card);
|
|
162
170
|
const prev = lastExpandPanelClick;
|
|
163
|
-
lastExpandPanelClick = {
|
|
164
|
-
return Boolean(prev && prev.
|
|
171
|
+
lastExpandPanelClick = { id, at: now };
|
|
172
|
+
return Boolean(prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
function clearExpandPanelDoubleClick(): void {
|
|
@@ -174,6 +182,7 @@ function collapseExpandedCard(tui: any, card: any): boolean {
|
|
|
174
182
|
setHoveredToolCallId(null);
|
|
175
183
|
setHoveredToolGroup(null);
|
|
176
184
|
setHoveredThinking(null);
|
|
185
|
+
setHoveredMessageDisplay(null);
|
|
177
186
|
setHoveredToolIo(null, null);
|
|
178
187
|
setHoveredCompactAssistant(null);
|
|
179
188
|
card.invalidate?.();
|
|
@@ -345,6 +354,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
345
354
|
setHoveredToolCallId(null);
|
|
346
355
|
setHoveredToolGroup(null);
|
|
347
356
|
setHoveredThinking(null);
|
|
357
|
+
setHoveredMessageDisplay(null);
|
|
348
358
|
setHoveredToolIo(null, null);
|
|
349
359
|
setHoveredCompactAssistant(null);
|
|
350
360
|
card.invalidate?.();
|
|
@@ -416,8 +426,7 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
416
426
|
// compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
|
|
417
427
|
if (component.expanded || overHint) target = { kind: "assistant", component };
|
|
418
428
|
} else if (isMessageDisplayComponent(component)) {
|
|
419
|
-
|
|
420
|
-
if (component.expanded || overHint) target = { kind: "assistant", component };
|
|
429
|
+
if (overHint) target = { kind: "message", component };
|
|
421
430
|
}
|
|
422
431
|
}
|
|
423
432
|
}
|
|
@@ -814,6 +823,7 @@ export function teardownToolMouseInteraction(
|
|
|
814
823
|
setHoveredToolCallId(null);
|
|
815
824
|
setHoveredToolGroup(null);
|
|
816
825
|
setHoveredThinking(null);
|
|
826
|
+
setHoveredMessageDisplay(null);
|
|
817
827
|
setHoveredToolIo(null, null);
|
|
818
828
|
setHoveredCompactAssistant(null);
|
|
819
829
|
clearExpandPanelDoubleClick();
|
|
@@ -841,6 +851,7 @@ export function teardownToolMouseInteraction(
|
|
|
841
851
|
export function resetToolHoverState(): void {
|
|
842
852
|
setHoveredToolCallId(null);
|
|
843
853
|
setHoveredThinking(null);
|
|
854
|
+
setHoveredMessageDisplay(null);
|
|
844
855
|
setHoveredCompactAssistant(null);
|
|
845
856
|
setScrollButtonVisible(false);
|
|
846
857
|
setScrollButtonHovered(false);
|
|
@@ -55,6 +55,15 @@ const BRANCH_KIND: DisplayKind = {
|
|
|
55
55
|
body: (component) => String(component.message?.summary ?? ""),
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
function ensureHintHover(component: any): void {
|
|
59
|
+
if (typeof component.setHintHovered === "function") return;
|
|
60
|
+
component.setHintHovered = function (this: any, hovered: boolean) {
|
|
61
|
+
if (this.hintHovered === hovered) return;
|
|
62
|
+
this.hintHovered = hovered;
|
|
63
|
+
this.invalidate?.();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
59
68
|
const theme = displayTheme;
|
|
60
69
|
if (!theme) return; // 主题未就绪时保留原生渲染
|
|
@@ -64,13 +73,21 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
|
64
73
|
component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
|
|
65
74
|
}
|
|
66
75
|
component.clear();
|
|
76
|
+
ensureHintHover(component);
|
|
67
77
|
const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
|
|
68
78
|
const title = theme.fg("toolTitle", kind.title(component));
|
|
69
79
|
if (!component.expanded) {
|
|
70
|
-
const
|
|
80
|
+
const hovered = component.hintHovered === true;
|
|
81
|
+
const hint = `${theme.fg("dim", " • ")}${theme.fg(hovered ? "text" : "dim", showMoreHintText())}`;
|
|
71
82
|
component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
|
|
72
83
|
return;
|
|
73
84
|
}
|
|
85
|
+
// 展开卡与 tool 一致:userMessageBg + 上下左右 1 格
|
|
86
|
+
component.paddingX = 1;
|
|
87
|
+
component.paddingY = 1;
|
|
88
|
+
if (typeof theme.bg === "function" && typeof component.setBgFn === "function") {
|
|
89
|
+
component.setBgFn((text: string) => theme.bg("userMessageBg", text));
|
|
90
|
+
}
|
|
74
91
|
component.addChild(new Text(`${icon} ${title}`, 0, 0));
|
|
75
92
|
component.addChild(new Spacer(1));
|
|
76
93
|
component.addChild(
|
package/package.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
function modelLabel(model: any): string | undefined {
|
|
4
|
-
if (!model) return undefined;
|
|
5
|
-
const name = model.name || model.id;
|
|
6
|
-
if (!name) return undefined;
|
|
7
|
-
return model.provider ? `${model.provider} · ${name}` : name;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function apply(ctx: any): void {
|
|
11
|
-
if (!ctx?.hasUI) return;
|
|
12
|
-
ctx.ui?.setStatus?.("model", modelLabel(ctx.model));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** 用 setStatus 挂模型名,不 setFooter,不接管原生 footer。 */
|
|
16
|
-
export default function (pi: ExtensionAPI): void {
|
|
17
|
-
pi.on("session_start", async (_event, ctx) => apply(ctx));
|
|
18
|
-
pi.on("model_select", async (_event, ctx) => apply(ctx));
|
|
19
|
-
}
|