pi-cc-extensions 1.0.10 → 1.0.11
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 -1
- package/README.md +1 -1
- package/extensions/feature/context.ts +22 -5
- package/extensions/renderer/compact-mode.ts +44 -14
- package/extensions/renderer/index.ts +4 -3
- package/extensions/renderer/tool/grouping.ts +25 -6
- package/extensions/utils/ansi-text.ts +50 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -91,7 +91,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
91
91
|
"footerGitStatsMode": "working", // working (default): uncommitted; branch: cumulative net changes
|
|
92
92
|
"footerHiddenKeys": [], // hidden chip keys
|
|
93
93
|
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "pi-usage"], // chip order per line; builtins and plugins share one layout
|
|
94
|
-
"footerLine2Keys": ["cwd", "git"],
|
|
94
|
+
"footerLine2Keys": ["cwd", "git"], // cwd: directory and session; git: branch and changes. Either can move to any line
|
|
95
95
|
"footerLine3Keys": [], // painted only when a chip is visible
|
|
96
96
|
}
|
|
97
97
|
```
|
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
91
91
|
"footerGitStatsMode": "working", // working(默认):未提交改动;branch:分支累计净改动
|
|
92
92
|
"footerHiddenKeys": [], // 隐藏的芯片 key
|
|
93
93
|
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "pi-usage"], // 每行芯片顺序;内置项与插件同一套排布
|
|
94
|
-
"footerLine2Keys": ["cwd", "git"],
|
|
94
|
+
"footerLine2Keys": ["cwd", "git"], // cwd:目录和会话名;git:分支和改动。可换到任意行
|
|
95
95
|
"footerLine3Keys": [] // 有可见芯片才占行
|
|
96
96
|
}
|
|
97
97
|
```
|
|
@@ -887,27 +887,44 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
887
887
|
return theme.style("█".repeat(Math.max(0, cells)), { fg: part.color });
|
|
888
888
|
})
|
|
889
889
|
.join("");
|
|
890
|
+
const amountCells = allParts.map((part) => {
|
|
891
|
+
const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
|
|
892
|
+
const tokens = formatTokens(part.tokens).padStart(7);
|
|
893
|
+
const percent = `${pct.toFixed(1).padStart(5)}%`;
|
|
894
|
+
return { tokens, percent, plain: `${tokens} ${percent}` };
|
|
895
|
+
});
|
|
896
|
+
// 数字列整体贴右:label 最多占到「行宽 - 数字列宽 - 前缀 - 边距」为止。
|
|
897
|
+
const amountWidth = Math.max(
|
|
898
|
+
...amountCells.map((cell) => visibleWidth(cell.plain)),
|
|
899
|
+
);
|
|
890
900
|
const labelWidth = Math.min(
|
|
891
901
|
24,
|
|
892
902
|
Math.max(...allParts.map((part) => part.label.length)),
|
|
903
|
+
Math.max(1, inner - amountWidth - 5),
|
|
893
904
|
);
|
|
894
905
|
const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
|
|
895
|
-
const partRows = allParts.map((part) => {
|
|
896
|
-
const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
|
|
906
|
+
const partRows = allParts.map((part, index) => {
|
|
897
907
|
const swatch = theme.style("■", { fg: part.color });
|
|
898
908
|
const label = part.label.padEnd(labelWidth);
|
|
899
|
-
const amount =
|
|
909
|
+
const amount = amountCells[index];
|
|
900
910
|
const selected = part.label === selectedLabel;
|
|
901
911
|
const hoverLabel = visiblePreviews.find((p) => p.key === hoveredKey)?.label;
|
|
902
912
|
const prefix = selected ? "› " : " ";
|
|
903
|
-
const
|
|
913
|
+
const left = `${prefix}${swatch} ${label}`;
|
|
914
|
+
const gap = Math.max(
|
|
915
|
+
1,
|
|
916
|
+
inner - 1 - visibleWidth(left) - visibleWidth(amount?.plain ?? ""),
|
|
917
|
+
);
|
|
918
|
+
// token 数字用次级色,百分比保持正文色以保留可读的主指标。
|
|
919
|
+
const right = `${theme.fg("dim", amount?.tokens ?? "")} ${amount?.percent ?? ""}`;
|
|
920
|
+
const row = padLine(`${left}${" ".repeat(gap)}${right}`, inner);
|
|
904
921
|
if (selected) return theme.bg("selectedBg", row);
|
|
905
922
|
return part.label === hoverLabel ? theme.bg("customMessageBg", row) : row;
|
|
906
923
|
});
|
|
907
924
|
const border = (text: string) => theme.fg("border", text);
|
|
908
925
|
const lines = [
|
|
909
926
|
border(`╭${"─".repeat(inner)}╮`),
|
|
910
|
-
`${border("│")}${padLine(` ${title} ${theme.fg("
|
|
927
|
+
`${border("│")}${padLine(` ${title} ${theme.fg("dim", subtitle)}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
|
|
911
928
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
912
929
|
`${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
|
|
913
930
|
`${border("│")}${" ".repeat(inner)}${border("│")}`,
|
|
@@ -64,6 +64,7 @@ import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
|
64
64
|
import { styleChangeCounts } from "./tool/ccstyle-tool.ts";
|
|
65
65
|
import { formatDisplayPath } from "./tool/names.ts";
|
|
66
66
|
import {
|
|
67
|
+
contentRowRange,
|
|
67
68
|
hasVisibleText,
|
|
68
69
|
removeStyledRange,
|
|
69
70
|
stripBackgroundAnsi,
|
|
@@ -372,7 +373,7 @@ function editWriteExpandedCard(theme: any, component: any): any {
|
|
|
372
373
|
/** compact 展开面板:外卡片保持展开卡背景;展开的 thinking 另包一层更深的
|
|
373
374
|
* 内卡(自身 Box 背景与外卡同色,不区分会糊在一起),工具卡直接铺在外卡背景上。
|
|
374
375
|
* 每个子卡前面补 1 行分隔(上一条已经是空行时不再补),子卡自带的首尾空行会被裁掉,
|
|
375
|
-
* 避免与外卡/内卡 padding
|
|
376
|
+
* 避免与外卡/内卡 padding 叠出双空行(内联图片的占位行不裁,见 contentRowRange)。
|
|
376
377
|
* hits:子卡的行区间与命中目标(thinking 命中自身,工具命中 tool 组件),
|
|
377
378
|
* 供展开后点击 hint。未开启 toolHits 时不映射工具,点击仍归外层面板。
|
|
378
379
|
* live=true:live 槽位用,不铺底色也不留上下空行,首行紧跟 `↳`、其余行缩进对齐
|
|
@@ -434,14 +435,8 @@ function layoutExpandedToolCard(
|
|
|
434
435
|
const painted = paints?.[childIndex];
|
|
435
436
|
const childLines = Array.isArray(painted) ? painted : child.render(innerWidth);
|
|
436
437
|
const innerCard = !live && isThinkingPreview(child) && child.expanded === true;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
for (let i = 0; i < childLines.length; i++) {
|
|
440
|
-
if (hasVisibleText(childLines[i])) {
|
|
441
|
-
if (first < 0) first = i;
|
|
442
|
-
last = i;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
438
|
+
// 内联图片按 placement 把范围撑开:锚点前后的空行是图片占的位置,不能当首尾空行裁。
|
|
439
|
+
const { first, last } = contentRowRange(childLines);
|
|
445
440
|
if (first < 0) {
|
|
446
441
|
if (live) continue;
|
|
447
442
|
for (const line of childLines) {
|
|
@@ -603,16 +598,20 @@ export type CompactModeHooks = {
|
|
|
603
598
|
/** 重新渲染包含指定 toolCallId 的 assistant 消息(思考收尾时刷新)。 */
|
|
604
599
|
refreshToolCallMessage(toolCallId: string | undefined): void;
|
|
605
600
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
601
|
+
* 会话已空闲时收尾末尾回合。回放/切换样式/重载重建之后,末尾那条消息
|
|
602
|
+
* 后面没有消息来收口;活性只问会话,不从消息形状推断。
|
|
608
603
|
*/
|
|
609
|
-
|
|
604
|
+
settleIdleRound(): void;
|
|
605
|
+
/** agent 这轮跑完:收尾仍在 live 的末尾回合,别让它一直挂着 Running。 */
|
|
606
|
+
settleFinishedRound(): void;
|
|
610
607
|
shutdown(): void;
|
|
611
608
|
};
|
|
612
609
|
|
|
613
610
|
type CompactModeInstallDeps = {
|
|
614
611
|
query?: CompactThinkingQuery;
|
|
615
612
|
writeMetadata: WriteExecutionMetadataStore;
|
|
613
|
+
/** 会话是否空闲;测试注入用,正式路径取 ctx.isIdle。 */
|
|
614
|
+
isIdle?: () => boolean;
|
|
616
615
|
};
|
|
617
616
|
|
|
618
617
|
type CompactModePatch = {
|
|
@@ -1233,8 +1232,18 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1233
1232
|
const liveSlotToolIds = new Set<string>();
|
|
1234
1233
|
const pendingFoldRounds = new Set<CompactRound>();
|
|
1235
1234
|
let uiRef: { requestRender?: (force?: boolean) => void } | undefined;
|
|
1235
|
+
let idleRef: (() => boolean) | undefined = deps.isIdle;
|
|
1236
1236
|
let roundTickTimer: ReturnType<typeof setInterval> | undefined;
|
|
1237
1237
|
|
|
1238
|
+
/** 会话是否空闲:回合活性的事实来源,取不到就当没空闲(宁可保留 Running)。 */
|
|
1239
|
+
const sessionIdle = (): boolean => {
|
|
1240
|
+
try {
|
|
1241
|
+
return idleRef?.() === true;
|
|
1242
|
+
} catch {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1238
1247
|
const roundWallMs = (round: CompactRound): number => {
|
|
1239
1248
|
const end = round.active ? Date.now() : (round.endedAt ?? Date.now());
|
|
1240
1249
|
return Math.max(1, end - round.startedAt);
|
|
@@ -2172,6 +2181,19 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2172
2181
|
}
|
|
2173
2182
|
};
|
|
2174
2183
|
|
|
2184
|
+
/**
|
|
2185
|
+
* 会话已空闲时收尾末尾回合。重建 pass 内仍按顺序并回合(同一 turn 的多条
|
|
2186
|
+
* 消息要合成一条摘要),只在 pass 结束时对账:会话没在跑,末尾那条就不会
|
|
2187
|
+
* 再长出内容,停在 Running 是假的。
|
|
2188
|
+
*/
|
|
2189
|
+
const settleTrailingRound = (): void => {
|
|
2190
|
+
if (!sessionIdle()) return;
|
|
2191
|
+
const round = activeRound;
|
|
2192
|
+
if (!round) return;
|
|
2193
|
+
finalizeRound(round);
|
|
2194
|
+
renderRound(round);
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2175
2197
|
patch.assistantInstalled = function (this: any, message: any, isStreaming?: boolean) {
|
|
2176
2198
|
const self = this as any;
|
|
2177
2199
|
// 同 compact-thinking:isStreaming 丢失 → mermaid 流式误渲染来回闪。
|
|
@@ -2423,6 +2445,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2423
2445
|
sync(ctx: any) {
|
|
2424
2446
|
if (!patch.active) return;
|
|
2425
2447
|
uiRef = ctx?.ui;
|
|
2448
|
+
if (typeof ctx?.isIdle === "function") idleRef = () => ctx.isIdle() === true;
|
|
2426
2449
|
resetRounds();
|
|
2427
2450
|
// 始终保持补丁在链上:mode=on/off 走 passThrough,仍写入 lastMessage
|
|
2428
2451
|
// 与 tracked 集合,这样 /ccstyle 切回 compact 时不必 /reload。
|
|
@@ -2431,6 +2454,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2431
2454
|
else hoveredAssistantComponent = undefined;
|
|
2432
2455
|
refreshTrackedComponents();
|
|
2433
2456
|
sweepOrphanTails();
|
|
2457
|
+
settleTrailingRound();
|
|
2434
2458
|
},
|
|
2435
2459
|
refresh() {
|
|
2436
2460
|
if (!patch.active) return;
|
|
@@ -2439,6 +2463,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2439
2463
|
if (config.mode !== "compact") hoveredAssistantComponent = undefined;
|
|
2440
2464
|
refreshTrackedComponents();
|
|
2441
2465
|
sweepOrphanTails();
|
|
2466
|
+
settleTrailingRound();
|
|
2442
2467
|
},
|
|
2443
2468
|
assertOwnership() {
|
|
2444
2469
|
if (!patch.active) return;
|
|
@@ -2459,11 +2484,16 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2459
2484
|
}
|
|
2460
2485
|
}
|
|
2461
2486
|
},
|
|
2462
|
-
|
|
2487
|
+
settleIdleRound() {
|
|
2488
|
+
if (!patch.active || config.mode !== "compact") return;
|
|
2489
|
+
settleTrailingRound();
|
|
2490
|
+
},
|
|
2491
|
+
settleFinishedRound() {
|
|
2463
2492
|
if (!patch.active || config.mode !== "compact") return;
|
|
2464
2493
|
const round = activeRound;
|
|
2465
2494
|
if (!round) return;
|
|
2466
|
-
|
|
2495
|
+
// 走静默期:摘要行当场翻成 Ran for,槽位卡多留一拍再收拢。
|
|
2496
|
+
endRound(round, true);
|
|
2467
2497
|
renderRound(round);
|
|
2468
2498
|
},
|
|
2469
2499
|
shutdown() {
|
|
@@ -258,10 +258,9 @@ export default function (
|
|
|
258
258
|
syncCompactMode(ctx);
|
|
259
259
|
// compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
|
|
260
260
|
// 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
|
|
261
|
+
// 历史回放也在这之后跑完:sync 末尾按 ctx.isIdle() 收尾末尾回合,
|
|
262
|
+
// 它没有后续消息,不收就会停在 Running。
|
|
261
263
|
setTimeout(() => syncCompactMode(ctx), 0);
|
|
262
|
-
// 历史回放发生在 session_start 之后,延迟到回放结束再收尾最后一个回合,
|
|
263
|
-
// 否则它没有后续消息,摘要行会停在 Running。
|
|
264
|
-
setTimeout(() => compactModeHooks?.settleReplayedRound(), 0);
|
|
265
264
|
scheduleSessionRender(() => {
|
|
266
265
|
hooks.toolGrouping.refresh(getToolMouseTui());
|
|
267
266
|
// /reload、/resume 在 session_start 之前就重建了 transcript:已挂载的通知卡换上本插件的渲染器。
|
|
@@ -303,6 +302,8 @@ export default function (
|
|
|
303
302
|
|
|
304
303
|
pi.on("agent_end", async () => {
|
|
305
304
|
clearLiveToolCalls();
|
|
305
|
+
// 这轮跑完:末尾回合没有正文收口(纯工具回合)就一直挂着 Running。
|
|
306
|
+
compactModeHooks?.settleFinishedRound();
|
|
306
307
|
});
|
|
307
308
|
|
|
308
309
|
pi.on("session_shutdown", async () => {
|
|
@@ -24,7 +24,13 @@ import {
|
|
|
24
24
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
25
25
|
import { mcpToolTitle } from "./mcp-title.ts";
|
|
26
26
|
import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./names.ts";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
contentRowRange,
|
|
29
|
+
imagePlacement,
|
|
30
|
+
stripAnsi,
|
|
31
|
+
stripBackgroundAnsi,
|
|
32
|
+
stripLeadingStatusIcon,
|
|
33
|
+
} from "../../utils/ansi-text.ts";
|
|
28
34
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
29
35
|
import {
|
|
30
36
|
fitToolCallSummary,
|
|
@@ -193,8 +199,22 @@ function requestAnimationRender(patch: Patch): void {
|
|
|
193
199
|
getToolMouseTui()?.requestRender?.();
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
/** 丢掉空白行;内联图片锚点行与它占用的空行要留着,否则终端里图片盖住后续行。 */
|
|
196
203
|
function visibleLines(lines: string[]): string[] {
|
|
197
|
-
|
|
204
|
+
const keep = new Set<number>();
|
|
205
|
+
for (let index = 0; index < lines.length; index++) {
|
|
206
|
+
const placement = imagePlacement(lines[index]);
|
|
207
|
+
if (!placement) {
|
|
208
|
+
if (stripAnsi(lines[index]).trim()) keep.add(index);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const start = placement.grows === "up" ? index - placement.rows + 1 : index;
|
|
212
|
+
const end = placement.grows === "down" ? index + placement.rows - 1 : index;
|
|
213
|
+
for (let row = Math.max(0, start); row <= Math.min(lines.length - 1, end); row++) {
|
|
214
|
+
keep.add(row);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return lines.filter((_line, index) => keep.has(index));
|
|
198
218
|
}
|
|
199
219
|
|
|
200
220
|
function stripLeadingSpaces(line: string, count: number): string {
|
|
@@ -388,12 +408,11 @@ function collapsedGroupLine(
|
|
|
388
408
|
return truncateToWidth(`${prefix}${item.summary(room, theme)}`, rowWidth, "");
|
|
389
409
|
}
|
|
390
410
|
|
|
391
|
-
/**
|
|
411
|
+
/** 末项结尾的尾随空行不计入卡高(内联图片的占位行除外,见 contentRowRange)。 */
|
|
392
412
|
function panelRows(rows: string[], last: boolean): string[] {
|
|
393
413
|
if (!last) return rows;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return end === rows.length ? rows : rows.slice(0, end);
|
|
414
|
+
const end = Math.max(1, contentRowRange(rows).last + 1);
|
|
415
|
+
return end >= rows.length ? rows : rows.slice(0, end);
|
|
397
416
|
}
|
|
398
417
|
|
|
399
418
|
type GroupHeaderCounts = { pending: number; success: number; error: number };
|
|
@@ -33,6 +33,56 @@ export function hasVisibleText(line: string): boolean {
|
|
|
33
33
|
return line.replace(CSI_SEQUENCE_RE, "").replace(OSC_SEQUENCE_STRICT_RE, "").trim().length > 0;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const KITTY_IMAGE_PREFIX = "\x1b_G";
|
|
37
|
+
const ITERM2_IMAGE_PREFIX = "\x1b]1337;File=";
|
|
38
|
+
|
|
39
|
+
/** 行内是否带内联图片序列(kitty APC 或 iTerm2 OSC 1337)。 */
|
|
40
|
+
export function isImageLine(line: string): boolean {
|
|
41
|
+
return line.includes(KITTY_IMAGE_PREFIX) || line.includes(ITERM2_IMAGE_PREFIX);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 内联图片在终端里占用的行数,以及它相对锚点行的铺开方向。
|
|
46
|
+
* kitty 的锚点在首行、向下铺(`r=N`);iTerm2 的锚点在末行、先上移再画(`\x1b[NA`)。
|
|
47
|
+
*/
|
|
48
|
+
export function imagePlacement(line: string): { rows: number; grows: "down" | "up" } | undefined {
|
|
49
|
+
const kittyControls = /\x1b_G([^;]*);/.exec(line)?.[1];
|
|
50
|
+
if (kittyControls !== undefined) {
|
|
51
|
+
const rows = Number(/(?:^|,)r=(\d+)/.exec(kittyControls)?.[1]);
|
|
52
|
+
return { rows: rows >= 1 ? rows : 1, grows: "down" };
|
|
53
|
+
}
|
|
54
|
+
if (line.includes(ITERM2_IMAGE_PREFIX)) {
|
|
55
|
+
const up = Number(/\x1b\[(\d+)A/.exec(line)?.[1]);
|
|
56
|
+
return { rows: up >= 1 ? up + 1 : 1, grows: "up" };
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 行区里必须保留的范围(首行/末行下标;没有内容时 first 为 -1)。
|
|
63
|
+
* 内联图片的行要按 placement 整体算进来:锚点前后那些空行是图片自己占的位置,
|
|
64
|
+
* 当首尾空行裁掉后终端里图片照样铺开,后面画的内容会被盖住。
|
|
65
|
+
*/
|
|
66
|
+
export function contentRowRange(rows: string[]): { first: number; last: number } {
|
|
67
|
+
let first = -1;
|
|
68
|
+
let last = -1;
|
|
69
|
+
const keep = (start: number, end: number) => {
|
|
70
|
+
const boundedStart = Math.max(0, start);
|
|
71
|
+
first = first < 0 ? boundedStart : Math.min(first, boundedStart);
|
|
72
|
+
last = Math.max(last, Math.min(rows.length - 1, end));
|
|
73
|
+
};
|
|
74
|
+
for (let index = 0; index < rows.length; index++) {
|
|
75
|
+
const placement = imagePlacement(rows[index]);
|
|
76
|
+
if (placement) {
|
|
77
|
+
if (placement.grows === "down") keep(index, index + placement.rows - 1);
|
|
78
|
+
else keep(index - placement.rows + 1, index);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (hasVisibleText(rows[index])) keep(index, index);
|
|
82
|
+
}
|
|
83
|
+
return { first, last };
|
|
84
|
+
}
|
|
85
|
+
|
|
36
86
|
/** 剥离背景色 ANSI(用于重新铺背景行)。 */
|
|
37
87
|
export function stripBackgroundAnsi(line: string): string {
|
|
38
88
|
return line.replace(/\x1b\[(?:4[0-9]|10[0-7]|48(?:(?:;|:)[0-9]+)+|49)m/g, "");
|