pi-cc-extensions 0.9.9 → 0.9.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 +49 -50
- package/README.md +7 -8
- package/extensions/config/config.ts +48 -8
- package/extensions/config/panel.ts +158 -30
- package/extensions/feature/compact-thinking.ts +19 -16
- package/extensions/feature/shell/footer-currency.ts +29 -0
- package/extensions/feature/shell/footer.ts +52 -7
- package/extensions/feature/shell/mcp-chip.ts +136 -0
- package/extensions/renderer/compact-mode.ts +78 -14
- package/extensions/renderer/default-mode.ts +51 -13
- package/extensions/renderer/mouse/interaction.ts +32 -21
- package/extensions/renderer/tool/codemode.ts +306 -0
- package/extensions/renderer/tool/diff/diff-header.ts +1 -1
- package/extensions/renderer/tool/diff/diff-highlight.ts +36 -0
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/diff/diff-write-render.ts +2 -2
- package/extensions/renderer/tool/diff/line-width-safety.ts +6 -5
- package/extensions/renderer/tool/grouping.ts +31 -6
- package/extensions/renderer/tool/message-display.ts +3 -3
- package/extensions/renderer/tool/names.ts +3 -0
- package/extensions/renderer/tool/result.ts +173 -36
- package/extensions/utils/ansi-text.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置 MCP 的状态栏芯片。
|
|
3
|
+
*
|
|
4
|
+
* pi 的内置 MCP 不写 setStatus:连接状态只在 `/mcp` 菜单里,扩展读不到。这里用公开数据推算
|
|
5
|
+
* 「已连接/已配置」,并在别的扩展接管 `/mcp` 时让位(pi-mcp-adapter 这类扩展自己会写 "mcp" 状态)。
|
|
6
|
+
*
|
|
7
|
+
* - 已连接:`pi.getAllTools()` 里来源是 builtin:mcp、曝光非 hidden 的工具,按 namespace 去重。
|
|
8
|
+
* 断线后工具仍在、0 个工具的服务器连上也看不见,needs-auth / failed 与 connecting 也分不开,
|
|
9
|
+
* 所以这只是近似值。
|
|
10
|
+
* - 已配置:mcp.json 里 enabled !== false 的条目(项目级要已 trust)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, statSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
/** 与 pi-mcp-adapter 共用同一个 status key;它写了 key 时本扩展不画。 */
|
|
18
|
+
export const MCP_STATUS_KEY = "mcp";
|
|
19
|
+
/** 内置 MCP 扩展的资源路径。 */
|
|
20
|
+
export const BUILTIN_MCP_SOURCE = "builtin:mcp";
|
|
21
|
+
const MCP_NAMESPACE_PREFIX = "mcp__";
|
|
22
|
+
|
|
23
|
+
type McpConfigCacheEntry = { mtimeMs: number; names: string[] };
|
|
24
|
+
|
|
25
|
+
const configCache = new Map<string, McpConfigCacheEntry>();
|
|
26
|
+
|
|
27
|
+
export function mcpAgentDir(): string {
|
|
28
|
+
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** pi 把 `-` 与 `_` 视作同一个服务器名(`mcpNamespace` 只把 `-` 换成 `_`)。 */
|
|
32
|
+
export function normalizeMcpServerName(name: string): string {
|
|
33
|
+
return name.trim().replace(/-/g, "_");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `mcp__<server>` → `<server>`;不是 MCP 命名空间返回 undefined。 */
|
|
37
|
+
export function mcpNamespaceServer(namespace: string): string | undefined {
|
|
38
|
+
if (!namespace.startsWith(MCP_NAMESPACE_PREFIX)) return undefined;
|
|
39
|
+
const server = namespace.slice(MCP_NAMESPACE_PREFIX.length);
|
|
40
|
+
return server || undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 已连接的服务器名(按 namespace 去重,返回规范化名字)。 */
|
|
44
|
+
export function connectedMcpServers(tools: readonly any[]): string[] {
|
|
45
|
+
const fromBuiltin = new Set<string>();
|
|
46
|
+
const fromNamespace = new Set<string>();
|
|
47
|
+
for (const tool of tools ?? []) {
|
|
48
|
+
if (tool?.exposure === "hidden") continue;
|
|
49
|
+
const server = mcpNamespaceServer(String(tool?.namespace?.name ?? ""));
|
|
50
|
+
if (!server) continue;
|
|
51
|
+
const name = normalizeMcpServerName(server);
|
|
52
|
+
if (tool?.sourceInfo?.path === BUILTIN_MCP_SOURCE) fromBuiltin.add(name);
|
|
53
|
+
else fromNamespace.add(name);
|
|
54
|
+
}
|
|
55
|
+
// 版本漂移兜底:来源路径对不上时按 namespace 认(接管 /mcp 的扩展已在此之前排除)
|
|
56
|
+
return [...(fromBuiltin.size > 0 ? fromBuiltin : fromNamespace)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `MCP 2/3`:已连接/(已连接 ∪ 已配置)。两边都空时不显示。 */
|
|
60
|
+
export function formatMcpChip(
|
|
61
|
+
connected: readonly string[],
|
|
62
|
+
configured: readonly string[],
|
|
63
|
+
): string | undefined {
|
|
64
|
+
const connectedNames = new Set(connected.map(normalizeMcpServerName).filter(Boolean));
|
|
65
|
+
const configuredNames = new Set(configured.map(normalizeMcpServerName).filter(Boolean));
|
|
66
|
+
const total = new Set([...connectedNames, ...configuredNames]).size;
|
|
67
|
+
if (total === 0) return undefined;
|
|
68
|
+
return `MCP ${connectedNames.size}/${total}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 读一个 mcp.json 里启用中的服务器名;文件缺失或损坏返回 undefined。 */
|
|
72
|
+
function readMcpConfigNames(path: string): string[] | undefined {
|
|
73
|
+
try {
|
|
74
|
+
const { mtimeMs } = statSync(path);
|
|
75
|
+
const cached = configCache.get(path);
|
|
76
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.names;
|
|
77
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as { mcpServers?: unknown };
|
|
78
|
+
const servers = parsed?.mcpServers;
|
|
79
|
+
const names: string[] = [];
|
|
80
|
+
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
81
|
+
for (const [name, entry] of Object.entries(servers as Record<string, unknown>)) {
|
|
82
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
83
|
+
if ((entry as { enabled?: unknown }).enabled === false) continue;
|
|
84
|
+
names.push(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
configCache.set(path, { mtimeMs, names });
|
|
88
|
+
return names;
|
|
89
|
+
} catch {
|
|
90
|
+
configCache.delete(path);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** mcp.json 里启用中的服务器名:用户级,加上已信任时的项目级(同名以项目为准,计数时去重)。 */
|
|
96
|
+
export function configuredMcpServers(options: {
|
|
97
|
+
agentDir: string;
|
|
98
|
+
cwd: string;
|
|
99
|
+
projectTrusted: boolean;
|
|
100
|
+
}): string[] {
|
|
101
|
+
const names = [...(readMcpConfigNames(join(options.agentDir, "mcp.json")) ?? [])];
|
|
102
|
+
if (options.projectTrusted) {
|
|
103
|
+
names.push(...(readMcpConfigNames(join(options.cwd, ".pi", "mcp.json")) ?? []));
|
|
104
|
+
}
|
|
105
|
+
return names;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 注册了 `/mcp` 的都是内置 MCP;别的扩展接管时它自己会写状态芯片。 */
|
|
109
|
+
export function builtinMcpOwnsCommand(commands: readonly any[]): boolean {
|
|
110
|
+
const mcpCommands = (commands ?? []).filter((command) => command?.name === MCP_STATUS_KEY);
|
|
111
|
+
return (
|
|
112
|
+
mcpCommands.length > 0 &&
|
|
113
|
+
mcpCommands.every((command) => command?.sourceInfo?.path === BUILTIN_MCP_SOURCE)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** footer 渲染用:当前芯片文案(不含 nerd 图标),不适用时返回 undefined。 */
|
|
118
|
+
export function buildMcpChip(ctx: any, api: any): string | undefined {
|
|
119
|
+
try {
|
|
120
|
+
// 工具与命令列表在 pi API 上,ctx 只提供 cwd / trust
|
|
121
|
+
if (!builtinMcpOwnsCommand(api?.getCommands?.() ?? [])) return undefined;
|
|
122
|
+
const tools = api?.getAllTools?.();
|
|
123
|
+
if (!Array.isArray(tools)) return undefined;
|
|
124
|
+
return formatMcpChip(
|
|
125
|
+
connectedMcpServers(tools),
|
|
126
|
+
configuredMcpServers({
|
|
127
|
+
agentDir: mcpAgentDir(),
|
|
128
|
+
cwd: String(ctx?.cwd ?? process.cwd()),
|
|
129
|
+
projectTrusted: ctx?.isProjectTrusted?.() === true,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
} catch {
|
|
133
|
+
// footer 不能因为读配置或读工具列表失败而中断渲染
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -338,16 +338,18 @@ function toolCardBgRow(
|
|
|
338
338
|
const inset = (n: number) => (outerBg ? `${outerBg}${" ".repeat(n)}\x1b[49m` : " ".repeat(n));
|
|
339
339
|
return `${inset(leftInset)}${bgAnsi}${stable}\x1b[49m${inset(rightInset)}`;
|
|
340
340
|
}
|
|
341
|
-
/** edit/write
|
|
341
|
+
/** edit/write 展开卡:保持原样式(展开卡背景 Box),不应用工具卡深色/间距改动。 */
|
|
342
342
|
function editWriteExpandedCard(theme: any): any {
|
|
343
343
|
return new Box(
|
|
344
344
|
1,
|
|
345
345
|
1,
|
|
346
|
-
typeof theme.bg === "function"
|
|
346
|
+
typeof theme.bg === "function"
|
|
347
|
+
? (text: string) => theme.bg(config.expandedCardBackground, text)
|
|
348
|
+
: undefined,
|
|
347
349
|
);
|
|
348
350
|
}
|
|
349
351
|
|
|
350
|
-
/** compact
|
|
352
|
+
/** compact 展开面板:外卡片保持展开卡背景;展开的 thinking 另包一层更深的
|
|
351
353
|
* 内卡(自身 Box 背景与外卡同色,不区分会糊在一起),工具卡直接铺在外卡背景上。
|
|
352
354
|
* 每个子卡前面补 1 行分隔(上一条已经是空行时不再补),子卡自带的首尾空行会被裁掉,
|
|
353
355
|
* 避免与外卡/内卡 padding 叠出双空行。
|
|
@@ -364,7 +366,7 @@ function layoutExpandedToolCard(
|
|
|
364
366
|
toolHits = false,
|
|
365
367
|
live = false,
|
|
366
368
|
): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
|
|
367
|
-
const slot =
|
|
369
|
+
const slot = config.expandedCardBackground;
|
|
368
370
|
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
369
371
|
/** live 每行前面的标记宽度(首行 `↳ `,其余对齐空格)。 */
|
|
370
372
|
const markerWidth = live ? 2 : 0;
|
|
@@ -471,6 +473,8 @@ function compactRoundCard(
|
|
|
471
473
|
| {
|
|
472
474
|
width: number;
|
|
473
475
|
theme: unknown;
|
|
476
|
+
/** 背景槽位也是面板行的输入:改配置后不能复用旧底色的整卡。 */
|
|
477
|
+
bgSlot: string;
|
|
474
478
|
paints: unknown[];
|
|
475
479
|
lines: string[];
|
|
476
480
|
hits: Array<{ child: any; start: number; end: number }>;
|
|
@@ -478,6 +482,7 @@ function compactRoundCard(
|
|
|
478
482
|
| undefined;
|
|
479
483
|
const layout = (width: number) => {
|
|
480
484
|
const theme = themeOf();
|
|
485
|
+
const bgSlot = config.expandedCardBackground;
|
|
481
486
|
const innerWidth = contentWidth(width);
|
|
482
487
|
const paints = children.map((child) => {
|
|
483
488
|
const lines = child.render?.(innerWidth);
|
|
@@ -487,6 +492,7 @@ function compactRoundCard(
|
|
|
487
492
|
paint &&
|
|
488
493
|
paint.width === width &&
|
|
489
494
|
paint.theme === theme &&
|
|
495
|
+
paint.bgSlot === bgSlot &&
|
|
490
496
|
paint.paints.length === paints.length &&
|
|
491
497
|
paint.paints.every((item, index) => item === paints[index])
|
|
492
498
|
) {
|
|
@@ -496,6 +502,7 @@ function compactRoundCard(
|
|
|
496
502
|
paint = {
|
|
497
503
|
width,
|
|
498
504
|
theme,
|
|
505
|
+
bgSlot,
|
|
499
506
|
paints,
|
|
500
507
|
lines: laid.lines,
|
|
501
508
|
hits: laid.hits,
|
|
@@ -528,6 +535,12 @@ function isToolComponent(value: any): boolean {
|
|
|
528
535
|
return value instanceof ToolExecutionComponent;
|
|
529
536
|
}
|
|
530
537
|
|
|
538
|
+
/** 成员离开回合后不再算外层面板,避免点别的卡时被跳过收起。 */
|
|
539
|
+
function clearRoundAnchor(component: any): void {
|
|
540
|
+
if (!component || Object.getOwnPropertyDescriptor(component, "roundAnchor")?.get) return;
|
|
541
|
+
delete component.roundAnchor;
|
|
542
|
+
}
|
|
543
|
+
|
|
531
544
|
function detachAssistantExpansion(component: any): void {
|
|
532
545
|
if (
|
|
533
546
|
typeof component?.[ASSISTANT_SET_EXPANDED_KEY] !== "function" ||
|
|
@@ -539,6 +552,7 @@ function detachAssistantExpansion(component: any): void {
|
|
|
539
552
|
delete component[ASSISTANT_SET_EXPANDED_KEY];
|
|
540
553
|
delete component[ASSISTANT_TOGGLE_ROUND_KEY];
|
|
541
554
|
delete component[ASSISTANT_REENTRY_KEY];
|
|
555
|
+
clearRoundAnchor(component);
|
|
542
556
|
}
|
|
543
557
|
|
|
544
558
|
/** 供 mouse-interaction 识别可点击的 compact assistant 行(仅 compact 模式下生效)。 */
|
|
@@ -697,7 +711,7 @@ function compactEditWriteLine(
|
|
|
697
711
|
const iconPart = `${options.flushLeft ? "" : " "}${theme.fg(iconColor, icon)} `;
|
|
698
712
|
const namePart = theme.fg("toolTitle", name);
|
|
699
713
|
const hintText =
|
|
700
|
-
options.hint !== false && component.expanded !== true ? `
|
|
714
|
+
options.hint !== false && component.expanded !== true ? ` · ${showMoreHintText()}` : "";
|
|
701
715
|
const fixedWidth =
|
|
702
716
|
visibleWidth(iconPart) +
|
|
703
717
|
visibleWidth(namePart) +
|
|
@@ -719,6 +733,8 @@ const compactRichDiffCache = new WeakMap<
|
|
|
719
733
|
type CompactEditPaintHit = {
|
|
720
734
|
width: number;
|
|
721
735
|
theme: unknown;
|
|
736
|
+
/** 展开卡的背景槽位同样参与命中判断。 */
|
|
737
|
+
bgSlot: string;
|
|
722
738
|
expanded: boolean;
|
|
723
739
|
result: unknown;
|
|
724
740
|
isPartial: boolean;
|
|
@@ -756,6 +772,7 @@ function compactEditWriteLines(
|
|
|
756
772
|
hit &&
|
|
757
773
|
hit.width === width &&
|
|
758
774
|
hit.theme === theme &&
|
|
775
|
+
hit.bgSlot === config.expandedCardBackground &&
|
|
759
776
|
hit.expanded === expanded &&
|
|
760
777
|
hit.result === component.result &&
|
|
761
778
|
hit.isPartial === isPartial &&
|
|
@@ -771,6 +788,7 @@ function compactEditWriteLines(
|
|
|
771
788
|
compactEditPaintCache.set(component, {
|
|
772
789
|
width,
|
|
773
790
|
theme,
|
|
791
|
+
bgSlot: config.expandedCardBackground,
|
|
774
792
|
expanded,
|
|
775
793
|
result: component.result,
|
|
776
794
|
isPartial,
|
|
@@ -901,7 +919,7 @@ function compactAssistantLineComponent(
|
|
|
901
919
|
const pad = Math.max(0, options.pad ?? (Number(self.outputPad) || 0));
|
|
902
920
|
const available = Math.max(0, width - pad);
|
|
903
921
|
const hint = options.hint !== false;
|
|
904
|
-
const hintText = hint ? `
|
|
922
|
+
const hintText = hint ? ` · ${showMoreHintText()}` : "";
|
|
905
923
|
const summaryWidth = Math.max(0, available - visibleWidth(hintText));
|
|
906
924
|
const resolved = typeof summary === "function" ? summary() : summary;
|
|
907
925
|
const runningActive = resolved.startsWith("Running...");
|
|
@@ -938,8 +956,11 @@ function compactAssistantLineComponent(
|
|
|
938
956
|
text = `${styleCompactThinkingText(heading, theme)}${plain(tools)}`;
|
|
939
957
|
}
|
|
940
958
|
}
|
|
941
|
-
|
|
942
|
-
const
|
|
959
|
+
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
960
|
+
const hintPart = hint
|
|
961
|
+
? `${theme.fg("dim", " · ")}${theme.fg(hover ? "text" : "dim", showMoreHintText())}`
|
|
962
|
+
: "";
|
|
963
|
+
const line = `${text}${hintPart}`;
|
|
943
964
|
const rendered = `${" ".repeat(pad)}${truncateToWidth(line, available, "")}`;
|
|
944
965
|
const lines = leadingBlank ? ["", rendered] : [rendered];
|
|
945
966
|
paint = {
|
|
@@ -975,7 +996,7 @@ function compactStopStatusLine(status: string, pad = 0): any {
|
|
|
975
996
|
|
|
976
997
|
/**
|
|
977
998
|
* 槽位卡内的展开入口:卡里是只读围观,展开走摘要行自己的 hint,卡内不再重复提示。
|
|
978
|
-
* 提示有两处来源——我们的 showMoreHintText(`… +N more lines
|
|
999
|
+
* 提示有两处来源——我们的 showMoreHintText(`… +N more lines · click to show more`)与
|
|
979
1000
|
* pi 原生工具卡(`… (N earlier lines, ctrl+o to expand)`),所以按渲染结果收尾裁剪。
|
|
980
1001
|
* 只去掉动作词,`(N more lines)` 这类计数保留。
|
|
981
1002
|
*/
|
|
@@ -987,7 +1008,8 @@ const SLOT_HINT_PATTERNS: RegExp[] = [
|
|
|
987
1008
|
// 只去掉动作词,收尾括号与计数保留。
|
|
988
1009
|
new RegExp(String.raw`,\s*[^()]*?${EXPAND_ACTION}(?=\))`),
|
|
989
1010
|
// 直接挂在行尾:整段去掉。
|
|
990
|
-
|
|
1011
|
+
// 尾部分隔点:ccstyle 统一 `·`,同时兼容旧帧与 pi 原生的 `•`
|
|
1012
|
+
new RegExp(String.raw`\s*(?:[•·]\s*)?${EXPAND_ACTION}\s*$`),
|
|
991
1013
|
];
|
|
992
1014
|
|
|
993
1015
|
function stripSlotHint(line: string): string {
|
|
@@ -1357,7 +1379,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1357
1379
|
const tail: MountedRoundTail = { host: undefined, container: undefined, parts: [], round };
|
|
1358
1380
|
const host: any = {
|
|
1359
1381
|
outputPad: Number(round.anchor?.outputPad) || 0,
|
|
1382
|
+
children: [],
|
|
1360
1383
|
render: (width: number) => tail.parts.flatMap((part: any) => part?.render?.(width) ?? []),
|
|
1384
|
+
childAtRow(localRow: number, width: number) {
|
|
1385
|
+
let offset = 0;
|
|
1386
|
+
for (const part of tail.parts) {
|
|
1387
|
+
const lines = part?.render?.(width);
|
|
1388
|
+
const count = Array.isArray(lines) ? lines.length : 0;
|
|
1389
|
+
if (localRow < offset + count) {
|
|
1390
|
+
return part?.childAtRow?.(localRow - offset, width) ?? null;
|
|
1391
|
+
}
|
|
1392
|
+
offset += count;
|
|
1393
|
+
}
|
|
1394
|
+
return null;
|
|
1395
|
+
},
|
|
1361
1396
|
invalidate: () => {
|
|
1362
1397
|
for (const part of tail.parts) part?.invalidate?.();
|
|
1363
1398
|
},
|
|
@@ -1373,6 +1408,10 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1373
1408
|
// 点击/悬停归属与 anchor 同一展开入口;toggle 每次 renderRound 重建,按值转发。
|
|
1374
1409
|
// 经 tail.round 动态解引用:重建过户时 anchor 可能换成别的组件。
|
|
1375
1410
|
ensureAssistantSetExpanded(host);
|
|
1411
|
+
Object.defineProperty(host, "roundAnchor", {
|
|
1412
|
+
configurable: true,
|
|
1413
|
+
get: () => tail.round?.anchor,
|
|
1414
|
+
});
|
|
1376
1415
|
host[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) =>
|
|
1377
1416
|
tail.round?.anchor?.[ASSISTANT_TOGGLE_ROUND_KEY]?.(expanded);
|
|
1378
1417
|
tail.host = host;
|
|
@@ -1527,6 +1566,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1527
1566
|
}
|
|
1528
1567
|
const tail = ensureTailHost(round);
|
|
1529
1568
|
tail.parts = makeParts(tail.host);
|
|
1569
|
+
tail.host.children = tail.parts;
|
|
1530
1570
|
syncTailPosition(round);
|
|
1531
1571
|
};
|
|
1532
1572
|
|
|
@@ -1665,6 +1705,10 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1665
1705
|
for (const [component, message] of round.messages) {
|
|
1666
1706
|
component.lastMessage = message;
|
|
1667
1707
|
ensureAssistantSetExpanded(component);
|
|
1708
|
+
// 尾行用 getter 指向 anchor;成员用同名属性,点内部工具时能认出同一回合。
|
|
1709
|
+
if (!Object.getOwnPropertyDescriptor(component, "roundAnchor")?.get) {
|
|
1710
|
+
component.roundAnchor = round.anchor;
|
|
1711
|
+
}
|
|
1668
1712
|
component[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) => {
|
|
1669
1713
|
const wasExpanded = round.anchor.expanded === true;
|
|
1670
1714
|
if (expanded && !wasExpanded) capturePanelViewport(round.anchor);
|
|
@@ -1679,8 +1723,10 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1679
1723
|
}
|
|
1680
1724
|
|
|
1681
1725
|
if (round.anchor.expanded === true) {
|
|
1682
|
-
//
|
|
1683
|
-
|
|
1726
|
+
// 展开内容留在摘要行那个位置:回合最后一个成员(含 write/edit)之后。
|
|
1727
|
+
// 放进 anchor 内部会跳到回合起点,盖到中间的 write/edit 上面。
|
|
1728
|
+
// anchor 未挂进 transcript 时没有这个位置,退回 anchor 内部。
|
|
1729
|
+
const mounted = Boolean(tailContainerOf(round.anchor));
|
|
1684
1730
|
const toolsById = new Map<string, any>();
|
|
1685
1731
|
for (const tool of trackedToolComponents) {
|
|
1686
1732
|
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
@@ -1790,7 +1836,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1790
1836
|
!explicitRoundToolIds.has(String(tool.toolCallId ?? "")),
|
|
1791
1837
|
);
|
|
1792
1838
|
flushPanel();
|
|
1793
|
-
|
|
1839
|
+
if (mounted) {
|
|
1840
|
+
// 正文留在各自的 assistant 消息上,面板挂回摘要行原位。
|
|
1841
|
+
const outside: any[] = [];
|
|
1842
|
+
const panel: any[] = [];
|
|
1843
|
+
for (const child of anchorChildren) {
|
|
1844
|
+
(typeof child?.childAtRow === "function" ? panel : outside).push(child);
|
|
1845
|
+
}
|
|
1846
|
+
for (const child of outside) round.anchor.contentContainer.addChild(child);
|
|
1847
|
+
if (panel.length > 0) mountRoundTail(round, () => panel);
|
|
1848
|
+
else unmountRoundTail(round);
|
|
1849
|
+
} else {
|
|
1850
|
+
unmountRoundTail(round);
|
|
1851
|
+
for (const child of anchorChildren) round.anchor.contentContainer.addChild(child);
|
|
1852
|
+
}
|
|
1794
1853
|
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
1795
1854
|
appendStopStatus(round.anchor, stopStatus);
|
|
1796
1855
|
return;
|
|
@@ -1901,7 +1960,10 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1901
1960
|
if (hasToolCalls) {
|
|
1902
1961
|
let round = roundByComponent.get(this);
|
|
1903
1962
|
if (hasText && (!round || round.anchor !== this)) {
|
|
1904
|
-
if (round)
|
|
1963
|
+
if (round) {
|
|
1964
|
+
round.messages.delete(this);
|
|
1965
|
+
clearRoundAnchor(this);
|
|
1966
|
+
}
|
|
1905
1967
|
if (activeRound) finishRound(activeRound);
|
|
1906
1968
|
round = createRound(this, message);
|
|
1907
1969
|
roundByComponent.set(this, round);
|
|
@@ -1921,6 +1983,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1921
1983
|
// 最终回答开始后,当前组件恢复原生文本;它已完成的 thinking
|
|
1922
1984
|
// 留在上一轮摘要中,避免再次生成独立 Thought 行。
|
|
1923
1985
|
round.messages.delete(this);
|
|
1986
|
+
clearRoundAnchor(this);
|
|
1924
1987
|
round.detachedMessages.push(previousMessage);
|
|
1925
1988
|
roundByComponent.delete(this);
|
|
1926
1989
|
finishRound(round);
|
|
@@ -1930,6 +1993,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1930
1993
|
// 围观态下 anchor 还挂着槽位卡,必须重绘才能收回。
|
|
1931
1994
|
endRound(round, true);
|
|
1932
1995
|
roundByComponent.delete(this);
|
|
1996
|
+
clearRoundAnchor(this);
|
|
1933
1997
|
return passThroughAssistant(this, message);
|
|
1934
1998
|
}
|
|
1935
1999
|
if (activeRound) finishRound(activeRound);
|
|
@@ -33,6 +33,11 @@ import {
|
|
|
33
33
|
} from "./tool/result.ts";
|
|
34
34
|
import { oneLine } from "../utils/format.ts";
|
|
35
35
|
import { showMoreHintText } from "./tool/show-more-hint.ts";
|
|
36
|
+
import {
|
|
37
|
+
CODEMODE_TOOL_NAME,
|
|
38
|
+
codemodeExpandedBody,
|
|
39
|
+
createCodemodeResultComponent,
|
|
40
|
+
} from "./tool/codemode.ts";
|
|
36
41
|
import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
|
|
37
42
|
import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
|
|
38
43
|
import { getMessageDisplayTheme } from "./tool/message-display.ts";
|
|
@@ -80,8 +85,8 @@ type GlobalToolRenderPatch = {
|
|
|
80
85
|
prototype: any;
|
|
81
86
|
active: boolean;
|
|
82
87
|
mode: () => CompactStyleMode;
|
|
83
|
-
wrap: (tool: any) => any;
|
|
84
|
-
byDefinition: WeakMap<object, any
|
|
88
|
+
wrap: (tool: any, toolName: string) => any;
|
|
89
|
+
byDefinition: WeakMap<object, Map<string, any>>;
|
|
85
90
|
byName: Map<string, any>;
|
|
86
91
|
downstream: ToolRenderMethods;
|
|
87
92
|
installed: ToolRenderMethods;
|
|
@@ -157,7 +162,7 @@ function taskListSummary(tasks: ParsedTask[]): string {
|
|
|
157
162
|
counts.completed ? `${counts.completed} completed` : "",
|
|
158
163
|
]
|
|
159
164
|
.filter(Boolean)
|
|
160
|
-
.join("
|
|
165
|
+
.join(" · ");
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
function renderExpandedTaskResult(
|
|
@@ -212,8 +217,8 @@ function renderExpandedTaskResult(
|
|
|
212
217
|
function createCcstyleTool(
|
|
213
218
|
originalTool: any,
|
|
214
219
|
writeExecutionMetadata: WriteExecutionMetadataStore,
|
|
220
|
+
toolName: string,
|
|
215
221
|
): any {
|
|
216
|
-
const toolName = originalTool.name;
|
|
217
222
|
const label = originalTool.label || toolName;
|
|
218
223
|
const defaultTitle = label === toolName ? humanizeToolLabel(label) : label;
|
|
219
224
|
// MCP 工具直接用 adapter 暴露的真实工具名,不做人性化
|
|
@@ -305,13 +310,38 @@ function createCcstyleTool(
|
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
const expanded = isToolExpanded(options, context);
|
|
313
|
+
const isError = Boolean(options?.isError || context?.isError);
|
|
314
|
+
// codemode:折叠态自己画子调用树,展开态给全部子调用与去掉脚本头的输出
|
|
315
|
+
if (toolName === CODEMODE_TOOL_NAME) {
|
|
316
|
+
if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
|
|
317
|
+
if (expanded) {
|
|
318
|
+
return renderExpandedToolResult(
|
|
319
|
+
codemodeExpandedBody(result),
|
|
320
|
+
theme,
|
|
321
|
+
isError,
|
|
322
|
+
context?.lastComponent,
|
|
323
|
+
context?.args,
|
|
324
|
+
context,
|
|
325
|
+
true,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
// 折叠态不再复用展开视图,免得 hover/鼠标命中指向已隐藏的组件
|
|
329
|
+
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
330
|
+
const toolCallId = context?.toolCallId;
|
|
331
|
+
return createCodemodeResultComponent({
|
|
332
|
+
result,
|
|
333
|
+
theme,
|
|
334
|
+
running: Boolean(options?.isPartial),
|
|
335
|
+
isError,
|
|
336
|
+
isHovered: () => isToolCallHovered(toolCallId),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
308
339
|
if (options?.isPartial) {
|
|
309
340
|
// 展开态贴左(Box 提供 1 格 pad);折叠态保持 3 格对齐标题
|
|
310
341
|
const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
|
|
311
342
|
return new Text(theme.fg("muted", pending), 0, 0);
|
|
312
343
|
}
|
|
313
344
|
|
|
314
|
-
const isError = options?.isError || context?.isError;
|
|
315
345
|
setToolVisualState(context, isError ? "error" : "success");
|
|
316
346
|
const toolCallId = context?.toolCallId;
|
|
317
347
|
if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
|
|
@@ -356,7 +386,7 @@ function createCcstyleTool(
|
|
|
356
386
|
: "Done";
|
|
357
387
|
const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args));
|
|
358
388
|
const hintText = showMoreHintText();
|
|
359
|
-
const hintPrefix = expandable ? theme.fg("dim", "
|
|
389
|
+
const hintPrefix = expandable ? theme.fg("dim", " · ") : "";
|
|
360
390
|
const hint = expandable ? hintPrefix + theme.fg("dim", hintText) : "";
|
|
361
391
|
const hoveredHint = expandable ? hintPrefix + theme.fg("text", hintText) : "";
|
|
362
392
|
if (expanded) {
|
|
@@ -429,19 +459,24 @@ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch):
|
|
|
429
459
|
|
|
430
460
|
function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): any {
|
|
431
461
|
const definition = component.toolDefinition ?? component.builtInToolDefinition;
|
|
462
|
+
const name = String(component.toolName || definition?.name || "tool");
|
|
432
463
|
if (definition && typeof definition === "object") {
|
|
433
|
-
let
|
|
464
|
+
let byName = patch.byDefinition.get(definition);
|
|
465
|
+
if (!byName) {
|
|
466
|
+
byName = new Map();
|
|
467
|
+
patch.byDefinition.set(definition, byName);
|
|
468
|
+
}
|
|
469
|
+
let wrapped = byName.get(name);
|
|
434
470
|
if (!wrapped) {
|
|
435
|
-
wrapped = patch.wrap(definition);
|
|
436
|
-
|
|
471
|
+
wrapped = patch.wrap(definition, name);
|
|
472
|
+
byName.set(name, wrapped);
|
|
437
473
|
}
|
|
438
474
|
return wrapped;
|
|
439
475
|
}
|
|
440
476
|
|
|
441
|
-
const name = String(component.toolName || "tool");
|
|
442
477
|
let wrapped = patch.byName.get(name);
|
|
443
478
|
if (!wrapped) {
|
|
444
|
-
wrapped = patch.wrap({ name, label: name });
|
|
479
|
+
wrapped = patch.wrap({ name, label: name }, name);
|
|
445
480
|
patch.byName.set(name, wrapped);
|
|
446
481
|
}
|
|
447
482
|
return wrapped;
|
|
@@ -538,7 +573,8 @@ function installGlobalToolRendering(
|
|
|
538
573
|
prototype,
|
|
539
574
|
active: true,
|
|
540
575
|
mode: () => config.mode,
|
|
541
|
-
wrap: (tool: any) =>
|
|
576
|
+
wrap: (tool: any, toolName: string) =>
|
|
577
|
+
createCcstyleTool(tool, writeExecutionMetadata, toolName),
|
|
542
578
|
byDefinition: new WeakMap(),
|
|
543
579
|
byName: new Map(),
|
|
544
580
|
downstream,
|
|
@@ -669,7 +705,9 @@ export function installToolExpandedBackground(): () => void {
|
|
|
669
705
|
if (box) {
|
|
670
706
|
box.paddingX = 1;
|
|
671
707
|
box.paddingY = 1;
|
|
672
|
-
if (box.setBgFn)
|
|
708
|
+
if (box.setBgFn) {
|
|
709
|
+
box.setBgFn((text: string) => theme.bg(config.expandedCardBackground, text));
|
|
710
|
+
}
|
|
673
711
|
}
|
|
674
712
|
},
|
|
675
713
|
original,
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
setHoveredCompactAssistant,
|
|
14
14
|
} from "../compact-mode.ts";
|
|
15
15
|
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
16
|
-
import { config } from "../../config/config.ts";
|
|
17
16
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
18
17
|
import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/show-more-hint.ts";
|
|
19
18
|
import {
|
|
@@ -144,7 +143,10 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
|
|
|
144
143
|
}
|
|
145
144
|
const title = section === "input" ? "Tool Input" : "Tool Output";
|
|
146
145
|
const content = section === "input" ? ioView.getInputBody() : ioView.getOutputBody();
|
|
147
|
-
|
|
146
|
+
// 整段就是代码时包一层围栏:弹框按 Markdown 渲染,围栏代码块才会着色
|
|
147
|
+
const fence = section === "input" ? ioView.getInputCodeFence() : undefined;
|
|
148
|
+
const preview = fence ? `\`\`\`${fence.language}\n${fence.code}\n\`\`\`` : content;
|
|
149
|
+
void showTextPreview({ ui }, title, preview || "(empty)");
|
|
148
150
|
return true;
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -305,10 +307,6 @@ function ensureFullscreenToolMouseMotion(tui: any): void {
|
|
|
305
307
|
releaseFullscreenToolMouseMotion(tui);
|
|
306
308
|
return;
|
|
307
309
|
}
|
|
308
|
-
// 面板改 scrollStepLines 后,下一帧渲染即同步(restore 仍按 original 恢复)。
|
|
309
|
-
if (typeof tui.wheelScrollLines === "number" && tui.wheelScrollLines !== config.scrollStepLines) {
|
|
310
|
-
tui.wheelScrollLines = config.scrollStepLines;
|
|
311
|
-
}
|
|
312
310
|
if (
|
|
313
311
|
!toolMouseInteractionActive() ||
|
|
314
312
|
tui.mouseEnabled === false ||
|
|
@@ -345,7 +343,6 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
|
|
|
345
343
|
}
|
|
346
344
|
|
|
347
345
|
const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
|
|
348
|
-
const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
|
|
349
346
|
|
|
350
347
|
/**
|
|
351
348
|
* diff 结果组件自带 remainder 行:声明存在时只有那一行是展开入口(正文里的同名字样不算);
|
|
@@ -415,11 +412,11 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
415
412
|
if (!isTool && !isGroup && !isAssistant && !isThinking && !isMessage) return false;
|
|
416
413
|
if (!component.expanded) {
|
|
417
414
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
415
|
+
// thinking 例外:标题行(标题、隐藏行数与提示同一行)整行都是展开入口。
|
|
418
416
|
const hint = collapsedHintHitbox(line);
|
|
419
417
|
const onHint = Boolean(
|
|
420
418
|
hint &&
|
|
421
|
-
packet.col >= hint.startCol &&
|
|
422
|
-
packet.col <= hint.endCol &&
|
|
419
|
+
(isThinking || (packet.col >= hint.startCol && packet.col <= hint.endCol)) &&
|
|
423
420
|
isCollapsedHintRow(component, line),
|
|
424
421
|
);
|
|
425
422
|
if (!onHint) {
|
|
@@ -433,13 +430,17 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
433
430
|
for (const other of others) {
|
|
434
431
|
if (other !== component && other.expanded) {
|
|
435
432
|
// 展开 round 卡内 thinking/工具时,外层 compact 卡是它的容器,不能收起。
|
|
436
|
-
|
|
433
|
+
// 面板挂在摘要行时,anchor 与摘要行是同一张外层卡,点内部工具不能收起它。
|
|
434
|
+
// 同一回合的摘要行、anchor 和其他消息都是外层面板,收起任一个都会关掉整块。
|
|
435
|
+
if ((isThinking || isTool) && isSameExpandedRound(other, target.owner)) continue;
|
|
437
436
|
other.setExpanded(false);
|
|
438
437
|
other.invalidate?.();
|
|
439
438
|
}
|
|
440
439
|
}
|
|
441
440
|
// 展开 round 卡内工具时,让 compact 的强制折叠放行它(非 round 内工具为空操作)。
|
|
442
441
|
if (isTool) markCompactRoundToolExpanded(component);
|
|
442
|
+
// compact 摘要行自己记展开前的视口(收起时还原跟随),这里不动它。
|
|
443
|
+
if (!isAssistant) keepViewportOnExpand(tui);
|
|
443
444
|
component.setExpanded(true);
|
|
444
445
|
clearPendingCollapsePress();
|
|
445
446
|
} else {
|
|
@@ -480,6 +481,25 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
480
481
|
return true;
|
|
481
482
|
}
|
|
482
483
|
|
|
484
|
+
/** 点到的组件和外层卡是否属于同一张 compact 回合面板。 */
|
|
485
|
+
function isSameExpandedRound(other: any, owner: any): boolean {
|
|
486
|
+
if (!owner || !isCompactAssistantComponent(other)) return false;
|
|
487
|
+
const anchor = owner.roundAnchor ?? owner;
|
|
488
|
+
return other === owner || other === anchor || other.roundAnchor === anchor;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 原地展开时停在当前滚动位置:transcript 跟随底部时,内容变高会把视口拖到底,
|
|
493
|
+
* 点击的行和它上方的外层面板被顶出屏幕。展开前关掉跟随,点击行留在原位。
|
|
494
|
+
*/
|
|
495
|
+
function keepViewportOnExpand(tui: any): void {
|
|
496
|
+
const view = tui.getPrimaryScrollView?.();
|
|
497
|
+
if (typeof view?.scrollTo !== "function" || typeof view.scrollTop !== "number") return;
|
|
498
|
+
view.scrollTo(view.scrollTop, { disableFollow: true });
|
|
499
|
+
// 已脱离底部跟随:亮出回到底部按钮,新输出到来时仍有入口。
|
|
500
|
+
if (view.isFollowingEnd === false) setScrollButtonVisible(true);
|
|
501
|
+
}
|
|
502
|
+
|
|
483
503
|
/**
|
|
484
504
|
* fullscreen 鼠标悬停:collapsed 卡 [click to show more] hint、
|
|
485
505
|
* expanded 卡截断头 show-more、回到底部按钮。motion 不 consume,官方链照常。
|
|
@@ -519,7 +539,8 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
519
539
|
if (component instanceof ToolGroupComponent) {
|
|
520
540
|
if (overHint) target = { kind: "group", component };
|
|
521
541
|
} else if (component instanceof ThinkingPreviewBlock) {
|
|
522
|
-
|
|
542
|
+
// thinking 折叠标题行整行可点:整块 hover 都高亮动作词,不只高亮按钮文本。
|
|
543
|
+
if (component.expanded || Boolean(hintBox)) target = { kind: "thinking", component };
|
|
523
544
|
} else if (isToolExecutionComponent(component)) {
|
|
524
545
|
let view: ExpandedToolIoView | null = null;
|
|
525
546
|
let section: ToolIoSection | null = null;
|
|
@@ -562,11 +583,6 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
562
583
|
const proto = Object.getPrototypeOf(tui);
|
|
563
584
|
const original = proto?.handleViewportInput;
|
|
564
585
|
if (typeof original !== "function") return;
|
|
565
|
-
// 官方原生 routeWheel 已完整处理嵌套 ScrollView;只调整默认步进(config.scrollStepLines)。
|
|
566
|
-
if (typeof tui.wheelScrollLines === "number") {
|
|
567
|
-
tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = tui.wheelScrollLines;
|
|
568
|
-
tui.wheelScrollLines = config.scrollStepLines;
|
|
569
|
-
}
|
|
570
586
|
tui[FULLSCREEN_VIEWPORT_PATCH] = true;
|
|
571
587
|
tui.handleViewportInput = function (this: any, data: string) {
|
|
572
588
|
if (toolMouseInteractionActive() && tui.mode === "fullscreen") {
|
|
@@ -615,11 +631,6 @@ function restoreFullscreenViewportInput(tui: any): void {
|
|
|
615
631
|
if (typeof proto?.handleViewportInput === "function") {
|
|
616
632
|
tui.handleViewportInput = proto.handleViewportInput;
|
|
617
633
|
}
|
|
618
|
-
const originalWheelScrollLines = tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL];
|
|
619
|
-
if (typeof originalWheelScrollLines === "number") {
|
|
620
|
-
tui.wheelScrollLines = originalWheelScrollLines;
|
|
621
|
-
tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = undefined;
|
|
622
|
-
}
|
|
623
634
|
tui[FULLSCREEN_VIEWPORT_PATCH] = false;
|
|
624
635
|
}
|
|
625
636
|
|