pi-cc-extensions 1.0.9 → 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/config/config.ts +1 -0
- package/extensions/config/panel.ts +3 -6
- package/extensions/feature/context.ts +22 -5
- package/extensions/renderer/compact-mode.ts +44 -14
- package/extensions/renderer/default-mode.ts +1 -5
- package/extensions/renderer/index.ts +20 -5
- package/extensions/renderer/markdown-enhance.ts +4 -1
- package/extensions/renderer/mouse/interaction.ts +30 -18
- package/extensions/renderer/mouse/layout.ts +12 -3
- package/extensions/renderer/tool/ccstyle-tool.ts +109 -0
- package/extensions/renderer/tool/grouping.ts +217 -113
- package/extensions/renderer/tool/names.ts +12 -5
- package/extensions/renderer/tool/subagent-notification.ts +556 -0
- package/extensions/utils/ansi-text.ts +50 -0
- package/extensions/utils/format.ts +11 -0
- package/extensions/utils/patch-keys.ts +4 -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
|
```
|
|
@@ -122,6 +122,7 @@ export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
|
|
|
122
122
|
export const THINKING_ANIMATION_INTERVAL_VALUES = ["40", "60", "90", "120", "180"];
|
|
123
123
|
/** Tools commonly toggled in excludeRenderers via the settings panel. */
|
|
124
124
|
export const EXCLUDE_RENDERER_CANDIDATES = [
|
|
125
|
+
"agent",
|
|
125
126
|
"bash",
|
|
126
127
|
"powershell",
|
|
127
128
|
"read",
|
|
@@ -82,8 +82,8 @@ function modeSettingDescription(mode: CompactStyleMode): string {
|
|
|
82
82
|
|
|
83
83
|
function excludeRenderersDescription(names: readonly string[]): string {
|
|
84
84
|
return names.length === 0
|
|
85
|
-
? "No tools excluded.
|
|
86
|
-
: `Native renderer for: ${names.join(", ")}.
|
|
85
|
+
? "No tools excluded. Enter to toggle common tools."
|
|
86
|
+
: `Native renderer for: ${names.join(", ")}. Enter to toggle.`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
function customFooterDescription(enabled: boolean): string {
|
|
@@ -274,10 +274,7 @@ function buildExcludeRenderersSubmenu(
|
|
|
274
274
|
const items = candidates.map((name) => ({
|
|
275
275
|
id: name,
|
|
276
276
|
label: name,
|
|
277
|
-
description:
|
|
278
|
-
name === "Agent"
|
|
279
|
-
? "Agent always uses its dedicated renderer and cannot be forced through ccstyle."
|
|
280
|
-
: `Use Pi native renderer for ${name} instead of Claude Code styling.`,
|
|
277
|
+
description: `Use Pi native renderer for ${name} instead of Claude Code styling.`,
|
|
281
278
|
currentValue: config.excludeRenderers.includes(name) ? "exclude" : "style",
|
|
282
279
|
values: ["style", "exclude"],
|
|
283
280
|
}));
|
|
@@ -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() {
|
|
@@ -20,10 +20,7 @@ import { createCcstyleTool } from "./tool/ccstyle-tool.ts";
|
|
|
20
20
|
import type { WriteExecutionMetadataStore } from "./tool/diff/index.ts";
|
|
21
21
|
export { shouldRenderRichDiff } from "./tool/ccstyle-tool.ts";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]);
|
|
25
|
-
|
|
26
|
-
/** 排除名单内且自带 renderer 的工具保留原渲染。 */
|
|
23
|
+
/** 排除名单内且自带 renderer 的工具保留原渲染;excludeRenderers 里的工具也交给原生渲染器。 */
|
|
27
24
|
export function preservesOriginalRenderer(
|
|
28
25
|
extensionDefinition: any,
|
|
29
26
|
toolName: string,
|
|
@@ -117,7 +114,6 @@ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch):
|
|
|
117
114
|
// compact 也复用同一套 ccstyle call/result,避免折叠态工具卡回落到 Pi 原生样式。
|
|
118
115
|
const useCcstyle =
|
|
119
116
|
patch.mode() !== "off" &&
|
|
120
|
-
!DEDICATED_RENDERER_TOOLS.has(toolName) &&
|
|
121
117
|
!preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
|
|
122
118
|
component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
|
|
123
119
|
return useCcstyle;
|
|
@@ -49,13 +49,17 @@ import {
|
|
|
49
49
|
refreshMessageDisplays,
|
|
50
50
|
setMessageDisplayTheme,
|
|
51
51
|
} from "./tool/message-display.ts";
|
|
52
|
+
import {
|
|
53
|
+
installSubagentNotificationRenderer,
|
|
54
|
+
refreshSubagentNotifications,
|
|
55
|
+
} from "./tool/subagent-notification.ts";
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
58
|
* Claude Code Style for pi — 装配入口。
|
|
55
59
|
*
|
|
56
60
|
* mode=on → default-mode(工具卡样式 + 展开背景)
|
|
57
61
|
* mode=compact → compact-mode(消息折叠摘要)
|
|
58
|
-
* 共用 → tool-grouping / message-display / mouse / write override
|
|
62
|
+
* 共用 → tool-grouping / message-display / subagent-notification / mouse / write override
|
|
59
63
|
*/
|
|
60
64
|
|
|
61
65
|
let compactModeHooks: CompactModeHooks | undefined;
|
|
@@ -64,6 +68,7 @@ function refreshCurrentTranscript(ctx?: any, toolGrouping?: ToolGroupingHooks):
|
|
|
64
68
|
const tui = getToolMouseTui();
|
|
65
69
|
toolGrouping?.refresh(tui);
|
|
66
70
|
refreshMessageDisplays(tui);
|
|
71
|
+
refreshSubagentNotifications(tui);
|
|
67
72
|
refreshCompactModeComponents(tui);
|
|
68
73
|
compactModeHooks?.refresh();
|
|
69
74
|
tui?.requestRender?.(true);
|
|
@@ -117,6 +122,7 @@ export default function (
|
|
|
117
122
|
toolGrouping: ToolGroupingHooks;
|
|
118
123
|
compactMode: CompactModeHooks;
|
|
119
124
|
disposeMessageDisplay: () => void;
|
|
125
|
+
disposeSubagentNotificationRenderer: () => void;
|
|
120
126
|
disposeToolExpandedBackground: () => void;
|
|
121
127
|
}
|
|
122
128
|
| undefined;
|
|
@@ -138,6 +144,8 @@ export default function (
|
|
|
138
144
|
});
|
|
139
145
|
compactModeHooks = compactMode;
|
|
140
146
|
const disposeMessageDisplay = installMessageDisplayRendering();
|
|
147
|
+
// pi-subagents 的结束通知:换查找结果而不注册,加载先后都归本插件,off 还给原生。
|
|
148
|
+
const disposeSubagentNotificationRenderer = installSubagentNotificationRenderer();
|
|
141
149
|
// 展开背景必须在 compact-mode 之后装,shutdown 时先于 compact 释放。
|
|
142
150
|
const disposeToolExpandedBackground = installToolExpandedBackground();
|
|
143
151
|
installation = {
|
|
@@ -145,6 +153,7 @@ export default function (
|
|
|
145
153
|
toolGrouping,
|
|
146
154
|
compactMode,
|
|
147
155
|
disposeMessageDisplay,
|
|
156
|
+
disposeSubagentNotificationRenderer,
|
|
148
157
|
disposeToolExpandedBackground,
|
|
149
158
|
};
|
|
150
159
|
return installation;
|
|
@@ -249,11 +258,14 @@ export default function (
|
|
|
249
258
|
syncCompactMode(ctx);
|
|
250
259
|
// compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
|
|
251
260
|
// 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
|
|
261
|
+
// 历史回放也在这之后跑完:sync 末尾按 ctx.isIdle() 收尾末尾回合,
|
|
262
|
+
// 它没有后续消息,不收就会停在 Running。
|
|
252
263
|
setTimeout(() => syncCompactMode(ctx), 0);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
scheduleSessionRender(() => {
|
|
265
|
+
hooks.toolGrouping.refresh(getToolMouseTui());
|
|
266
|
+
// /reload、/resume 在 session_start 之前就重建了 transcript:已挂载的通知卡换上本插件的渲染器。
|
|
267
|
+
refreshSubagentNotifications(getToolMouseTui());
|
|
268
|
+
});
|
|
257
269
|
});
|
|
258
270
|
|
|
259
271
|
pi.on("session_compact", async (event, ctx) => {
|
|
@@ -290,6 +302,8 @@ export default function (
|
|
|
290
302
|
|
|
291
303
|
pi.on("agent_end", async () => {
|
|
292
304
|
clearLiveToolCalls();
|
|
305
|
+
// 这轮跑完:末尾回合没有正文收口(纯工具回合)就一直挂着 Running。
|
|
306
|
+
compactModeHooks?.settleFinishedRound();
|
|
293
307
|
});
|
|
294
308
|
|
|
295
309
|
pi.on("session_shutdown", async () => {
|
|
@@ -307,6 +321,7 @@ export default function (
|
|
|
307
321
|
current.compactMode.shutdown();
|
|
308
322
|
compactModeHooks = undefined;
|
|
309
323
|
current.disposeMessageDisplay();
|
|
324
|
+
current.disposeSubagentNotificationRenderer();
|
|
310
325
|
disposeStickyPromptPatches();
|
|
311
326
|
clearAllAnimations();
|
|
312
327
|
installation = undefined;
|
|
@@ -102,7 +102,10 @@ function deCircled(markdown: string): string {
|
|
|
102
102
|
// 跨行链接规整
|
|
103
103
|
// ============================================================================
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
// \x1b 排除:Pi 的带颜色 Mermaid 输出把每行包成含 ANSI SGR(\x1b[...m)的 code span,
|
|
106
|
+
// 转义序列里的 `[` 不能当成链接起点,否则匹配会从第一个转义跨行直到后面真正的 `](`,
|
|
107
|
+
// 把图和之后的段落全压成一行(#84)。
|
|
108
|
+
const MULTILINE_LINK_RE = /(?<!\x1b)\[([^\]\n\x1b]*(?:\n[^\]\n\x1b]*)+)\](?=\()/g;
|
|
106
109
|
|
|
107
110
|
/** 防止 Markdown 跨行链接把行尾填充空格纳入 OSC 8 点击区。 */
|
|
108
111
|
function normalizeMultilineLinks(markdown: string): string {
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
markCompactRoundToolExpanded,
|
|
13
13
|
setHoveredCompactAssistant,
|
|
14
14
|
} from "../compact-mode.ts";
|
|
15
|
-
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
16
15
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
17
16
|
import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/names.ts";
|
|
18
17
|
import {
|
|
@@ -43,6 +42,7 @@ import {
|
|
|
43
42
|
collapsedHintHitbox,
|
|
44
43
|
fullscreenContentWidth,
|
|
45
44
|
fullscreenLeafAt,
|
|
45
|
+
isMessageCard,
|
|
46
46
|
isScrollbarColumnAt,
|
|
47
47
|
} from "./layout.ts";
|
|
48
48
|
import {
|
|
@@ -122,7 +122,7 @@ let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
122
122
|
let latestInteractionFrame: InteractionFrame = { regions: [] };
|
|
123
123
|
|
|
124
124
|
/** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
|
|
125
|
-
const COLLAPSED_TOOL_SUMMARY = /^\s*(
|
|
125
|
+
const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…|\.\.\.)/;
|
|
126
126
|
|
|
127
127
|
function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
128
128
|
const matches = latestInteractionFrame.regions.filter(
|
|
@@ -347,28 +347,40 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
|
|
|
347
347
|
const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
|
|
348
348
|
|
|
349
349
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
350
|
+
* 折叠卡上哪些行声明为展开入口。resultRendererComponent 自带 isCollapsedHintLine
|
|
351
|
+
* 的(diff/ask/codemode)按它声明的判,行号原样转发;不带的原生卡(grep/find/ls/
|
|
352
|
+
* read/bash/fallback)按原生展开提示的收尾特征判——pi 给结果区套整卡 MouseRegion,
|
|
353
|
+
* 正文行(含路径文本)也会被官方 click 展开,这里把入口收到提示那几行。
|
|
354
|
+
* localRow 是组件 render 内的行号(frame marker / 布局 localRow),供声明按行判。
|
|
352
355
|
*/
|
|
353
|
-
function isCollapsedHintRow(component: any, finalLine: string): boolean {
|
|
356
|
+
function isCollapsedHintRow(component: any, finalLine: string, localRow?: number): boolean {
|
|
354
357
|
const result = component?.resultRendererComponent;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
+
if (typeof result?.isCollapsedHintLine === "function") {
|
|
359
|
+
return result.isCollapsedHintLine(finalLine, localRow);
|
|
360
|
+
}
|
|
361
|
+
return isNativeExpandHintLine(finalLine);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** 原生折叠卡展开提示的收尾特征:`(... N more lines, ctrl+o to expand)` 或行尾的 `to show more`。 */
|
|
365
|
+
function isNativeExpandHintLine(line: string): boolean {
|
|
366
|
+
return /(\([^()]*\/(?: click| to expand)\)|(?:[\w/+]+\s+)?to (?:expand|show more)\)?)\s*$/.test(
|
|
367
|
+
stripTerminalSequencesPreservingLayout(line),
|
|
368
|
+
);
|
|
358
369
|
}
|
|
359
370
|
|
|
360
371
|
/**
|
|
361
372
|
* pi 0.87 给工具结果区套了 MouseRegion:整卡左键 click 都会 setExpanded。
|
|
362
|
-
*
|
|
363
|
-
*
|
|
373
|
+
* 声明了展开入口的组件(diff remainder 等)与原生卡(提示在预览末行)只放行入口
|
|
374
|
+
* 行,其余位置直接吞掉 click,避免官方把正文当展开入口;无提示的单行摘要卡保持
|
|
375
|
+
* 官方整行行为。
|
|
364
376
|
*/
|
|
365
377
|
function blocksOfficialCardToggle(component: any, event: any): boolean {
|
|
366
378
|
if (event?.type !== "click" || event.button !== "left" || component?.expanded) return false;
|
|
367
|
-
const result = component?.resultRendererComponent;
|
|
368
|
-
if (typeof result?.isCollapsedHintLine !== "function") return false;
|
|
369
379
|
const lines = component.render?.(Math.max(1, Math.floor(Number(event.width) || 0)));
|
|
370
|
-
const
|
|
371
|
-
|
|
380
|
+
const row = event.y;
|
|
381
|
+
const line = Array.isArray(lines) ? lines[row] : undefined;
|
|
382
|
+
if (typeof line !== "string") return false;
|
|
383
|
+
return !isCollapsedHintRow(component, line, row);
|
|
372
384
|
}
|
|
373
385
|
|
|
374
386
|
/**
|
|
@@ -410,7 +422,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
410
422
|
const isGroup = component instanceof ToolGroupComponent;
|
|
411
423
|
const isAssistant = isCompactAssistantComponent(component);
|
|
412
424
|
const isThinking = component instanceof ThinkingPreviewBlock;
|
|
413
|
-
const isMessage =
|
|
425
|
+
const isMessage = isMessageCard(component);
|
|
414
426
|
if (!isTool && !isGroup && !isAssistant && !isThinking && !isMessage) return false;
|
|
415
427
|
if (!component.expanded) {
|
|
416
428
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
@@ -419,7 +431,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
419
431
|
const onHint = Boolean(
|
|
420
432
|
hint &&
|
|
421
433
|
(isThinking || (packet.col >= hint.startCol && packet.col <= hint.endCol)) &&
|
|
422
|
-
isCollapsedHintRow(component, line),
|
|
434
|
+
isCollapsedHintRow(component, line, hit.localRow),
|
|
423
435
|
);
|
|
424
436
|
if (!onHint) {
|
|
425
437
|
// 面板内非提示区(工具卡标题/摘要行、thinking 预览正文):单击收起整块面板。
|
|
@@ -573,7 +585,7 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
573
585
|
} else if (isCompactAssistantComponent(component)) {
|
|
574
586
|
// compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
|
|
575
587
|
if (component.expanded || overHint) target = { kind: "assistant", component };
|
|
576
|
-
} else if (
|
|
588
|
+
} else if (isMessageCard(component)) {
|
|
577
589
|
if (overHint) target = { kind: "message", component };
|
|
578
590
|
}
|
|
579
591
|
}
|
|
@@ -758,7 +770,7 @@ function buildInteractionFrame(
|
|
|
758
770
|
if (
|
|
759
771
|
box &&
|
|
760
772
|
COLLAPSED_TOOL_SUMMARY.test(stripTerminalSequences(line)) &&
|
|
761
|
-
isCollapsedHintRow(component, line)
|
|
773
|
+
isCollapsedHintRow(component, line, placement.componentRow)
|
|
762
774
|
) {
|
|
763
775
|
regions.push({ kind: "collapsed-hint", row: finalRow, ...box, component });
|
|
764
776
|
}
|
|
@@ -3,6 +3,7 @@ import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
|
3
3
|
import { isCompactAssistantComponent } from "../compact-mode.ts";
|
|
4
4
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
5
5
|
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
6
|
+
import { isSubagentNotificationView } from "../tool/subagent-notification.ts";
|
|
6
7
|
import { isToolExecutionComponent, stripTerminalSequencesPreservingLayout } from "./packets.ts";
|
|
7
8
|
import { getScrollButtonWidget } from "./scroll.ts";
|
|
8
9
|
|
|
@@ -15,11 +16,19 @@ export type ComponentRowHit = {
|
|
|
15
16
|
owner?: any;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 消息类卡片:skill/压缩/分支摘要三种消息组件,以及单独出现的 subagent 通知行。
|
|
21
|
+
* 点击、悬停与 single-expand 规则相同。
|
|
22
|
+
*/
|
|
23
|
+
export function isMessageCard(component: any): boolean {
|
|
24
|
+
return isMessageDisplayComponent(component) || isSubagentNotificationView(component);
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/** 行内 [click to show more] / [↑ Collapse] 提示的命中列区间(1-based,含两端)。 */
|
|
19
28
|
export function collapsedHintHitbox(line: string): { startCol: number; endCol: number } | null {
|
|
20
29
|
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
21
30
|
const match =
|
|
22
|
-
/(\([^()\n]* \/ click\)|click to show more|to show more|↑ Collapse|to collapse)(?=\)?\s*$)/.exec(
|
|
31
|
+
/(\([^()\n]*(?: \/ click|, (?:[\w/+]+\s+)?to (?:expand|show more))\)|click to show more|to show more|↑ Collapse|to collapse)(?=\)?\s*$)/.exec(
|
|
23
32
|
plain,
|
|
24
33
|
);
|
|
25
34
|
if (!match?.[1]) return null;
|
|
@@ -140,7 +149,7 @@ export function componentAtLocalRow(
|
|
|
140
149
|
if (component instanceof ThinkingPreviewBlock) {
|
|
141
150
|
return { component, row: localRow };
|
|
142
151
|
}
|
|
143
|
-
if (
|
|
152
|
+
if (isMessageCard(component)) {
|
|
144
153
|
return { component, row: localRow };
|
|
145
154
|
}
|
|
146
155
|
if (component === getScrollButtonWidget()) {
|
|
@@ -182,7 +191,7 @@ export function collectFullscreenToolCards(
|
|
|
182
191
|
component instanceof ToolGroupComponent ||
|
|
183
192
|
isCompactAssistantComponent(component) ||
|
|
184
193
|
component instanceof ThinkingPreviewBlock ||
|
|
185
|
-
|
|
194
|
+
isMessageCard(component)
|
|
186
195
|
) {
|
|
187
196
|
out.push(component);
|
|
188
197
|
return;
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
expandedBodyComponent,
|
|
31
31
|
formatToolDuration,
|
|
32
32
|
insetComponent,
|
|
33
|
+
renderCollapsedToolResult,
|
|
33
34
|
renderCollapsedToolResultToWidth,
|
|
34
35
|
textFromResult,
|
|
35
36
|
toolViewportWidth,
|
|
@@ -46,6 +47,12 @@ import {
|
|
|
46
47
|
toolIconColor,
|
|
47
48
|
} from "./tool-state.ts";
|
|
48
49
|
import { showMoreHintText } from "./names.ts";
|
|
50
|
+
import { oneLine } from "../../utils/format.ts";
|
|
51
|
+
import {
|
|
52
|
+
isFailedSubagentStatus,
|
|
53
|
+
measuredStats,
|
|
54
|
+
agentStatusLabel,
|
|
55
|
+
} from "./subagent-notification.ts";
|
|
49
56
|
|
|
50
57
|
/** 成功勾:亮绿 truecolor(与 message-display 一致)。 */
|
|
51
58
|
const BRIGHT_GREEN = "[38;2;80;220;100m";
|
|
@@ -82,6 +89,8 @@ function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any
|
|
|
82
89
|
return new Text(fallback, 0, 0);
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
export const AGENT_TOOL_NAME = "Agent";
|
|
93
|
+
|
|
85
94
|
type ParsedTask = { id: string; status: string; subject: string };
|
|
86
95
|
|
|
87
96
|
function parseTaskList(text: string): ParsedTask[] {
|
|
@@ -185,6 +194,92 @@ function shellDurationText(toolName: string, context: any, isPartial: boolean):
|
|
|
185
194
|
return elapsed === undefined ? "" : formatToolDuration(elapsed);
|
|
186
195
|
}
|
|
187
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Agent 的 renderResult:与 pi-subagents 保持同样的分支,但收敛成 ccstyle 的 ↳ 行。
|
|
199
|
+
* running/partial 转交原渲染器(只有它有 spinner frame 和 activity 文案);
|
|
200
|
+
* background/completed/stopped/error 画单行摘要,展开态结果正文交给通用展开视图。
|
|
201
|
+
*/
|
|
202
|
+
function renderAgentResult(
|
|
203
|
+
originalTool: any,
|
|
204
|
+
result: any,
|
|
205
|
+
options: any,
|
|
206
|
+
theme: any,
|
|
207
|
+
context: any,
|
|
208
|
+
expanded: boolean,
|
|
209
|
+
isError: boolean,
|
|
210
|
+
): any | undefined {
|
|
211
|
+
const details = result?.details;
|
|
212
|
+
const text = textFromResult(result);
|
|
213
|
+
// 没拿到子代理状态(pi-subagents 原始路径的兜底)就沿用通用渲染。
|
|
214
|
+
if (!details?.status) return undefined;
|
|
215
|
+
|
|
216
|
+
const isPartial = Boolean(options?.isPartial);
|
|
217
|
+
const status = String(details.status);
|
|
218
|
+
|
|
219
|
+
// running:pi-subagents 的 spinner/activity 组件才能实时动,原样转交。
|
|
220
|
+
if (isPartial || status === "running") {
|
|
221
|
+
return renderDefault(originalTool, "renderResult", [result, options, theme, context], text);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const stats = measuredStats(details).join(" · ");
|
|
225
|
+
|
|
226
|
+
const agentId = details.agentId ? `ID: ${details.agentId}` : "";
|
|
227
|
+
const tail = (body: string) => (stats ? `${body} · ${stats}` : body);
|
|
228
|
+
|
|
229
|
+
if (status === "background") {
|
|
230
|
+
if (!expanded) {
|
|
231
|
+
const body = tail(`Running in background${agentId ? ` · ${agentId}` : ""}`);
|
|
232
|
+
return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
|
|
233
|
+
}
|
|
234
|
+
const view = renderExpandedToolResult(
|
|
235
|
+
`Running in background${agentId ? ` · ${agentId}` : ""}`,
|
|
236
|
+
theme,
|
|
237
|
+
false,
|
|
238
|
+
context?.lastComponent,
|
|
239
|
+
context?.args,
|
|
240
|
+
context,
|
|
241
|
+
true,
|
|
242
|
+
);
|
|
243
|
+
return view;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (status === "completed" || status === "steered") {
|
|
247
|
+
if (!expanded) {
|
|
248
|
+
const label = status === "steered" ? "Wrapped up (turn limit)" : "Done";
|
|
249
|
+
const body = [label, stats].filter(Boolean).join(" · ");
|
|
250
|
+
return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
|
|
251
|
+
}
|
|
252
|
+
const view = renderExpandedToolResult(
|
|
253
|
+
text || "Done",
|
|
254
|
+
theme,
|
|
255
|
+
Boolean(isError),
|
|
256
|
+
context?.lastComponent,
|
|
257
|
+
context?.args,
|
|
258
|
+
context,
|
|
259
|
+
true,
|
|
260
|
+
);
|
|
261
|
+
return view;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (status === "stopped") {
|
|
265
|
+
const body = tail("Stopped");
|
|
266
|
+
return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// error / aborted:首行错误文案 + 统计,与通用失败行口径一致。
|
|
270
|
+
const label = agentStatusLabel(status);
|
|
271
|
+
const firstLine = text.split("\n").find((line) => line.trim()) ?? label;
|
|
272
|
+
const body = tail(oneLine(firstLine, 72));
|
|
273
|
+
return new Text(
|
|
274
|
+
theme.fg(
|
|
275
|
+
isFailedSubagentStatus(status) ? "error" : "muted",
|
|
276
|
+
renderCollapsedToolResult(body, ""),
|
|
277
|
+
),
|
|
278
|
+
0,
|
|
279
|
+
0,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
188
283
|
/** 用 ccstyle call/result 包装任意工具定义。 */
|
|
189
284
|
export function createCcstyleTool(
|
|
190
285
|
originalTool: any,
|
|
@@ -315,6 +410,20 @@ export function createCcstyleTool(
|
|
|
315
410
|
|
|
316
411
|
const expanded = isToolExpanded(options, context);
|
|
317
412
|
const isError = Boolean(options?.isError || context?.isError);
|
|
413
|
+
// Agent:running 转交原渲染器(spinner/activity 只有它有);其余画 ↳ 摘要行。
|
|
414
|
+
// toolName 大小写宽松匹配:上游改名也不静默失效。
|
|
415
|
+
if (toolName.toLowerCase() === AGENT_TOOL_NAME.toLowerCase()) {
|
|
416
|
+
const agentResult = renderAgentResult(
|
|
417
|
+
originalTool,
|
|
418
|
+
result,
|
|
419
|
+
options,
|
|
420
|
+
theme,
|
|
421
|
+
context,
|
|
422
|
+
expanded,
|
|
423
|
+
isError,
|
|
424
|
+
);
|
|
425
|
+
if (agentResult) return agentResult;
|
|
426
|
+
}
|
|
318
427
|
// codemode:折叠态自己画子调用树,展开态给全部子调用与去掉脚本头的输出
|
|
319
428
|
if (toolName === CODEMODE_TOOL_NAME) {
|
|
320
429
|
if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
|