pi-cc-extensions 0.9.3 → 0.9.5
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 +3 -3
- package/README.md +3 -3
- package/extensions/config/config.ts +2 -3
- package/extensions/config/panel.ts +2 -3
- package/extensions/feature/reference/index.ts +23 -36
- package/extensions/feature/reference/session.ts +95 -1
- package/extensions/feature/reference/subagent.ts +60 -21
- package/extensions/renderer/default-mode.ts +13 -25
- package/extensions/renderer/index.ts +5 -3
- package/extensions/renderer/mouse/interaction.ts +71 -1
- package/extensions/renderer/tool/diff/diff-component.ts +19 -2
- package/extensions/renderer/tool/diff/diff-edit-render.ts +18 -9
- package/extensions/renderer/tool/diff/diff-limits.ts +22 -21
- package/extensions/renderer/tool/diff/diff-write-render.ts +28 -18
- package/extensions/renderer/tool/grouping.ts +18 -3
- package/extensions/renderer/tool/mcp-title.ts +39 -0
- package/extensions/renderer/tool/names.ts +103 -1
- package/extensions/renderer/tool/result.ts +6 -1
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -50,7 +50,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
50
50
|
|
|
51
51
|
// features
|
|
52
52
|
"enableSessionReference": true, // @ session references
|
|
53
|
-
"enableSubagentAutocomplete": true, // @
|
|
53
|
+
"enableSubagentAutocomplete": true, // @subagent:[name] completion and delegation hints
|
|
54
54
|
"enableContextCommand": true, // /context usage check
|
|
55
55
|
"enableAgentSummary": true, // per-turn tool summary
|
|
56
56
|
"enableWorkingMessage": true, // Working... bottom token/elapsed
|
|
@@ -59,7 +59,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
59
59
|
// ui
|
|
60
60
|
"expandedInputMaxLines": 5, // expanded tool Input lines; overflow shows a footer hint
|
|
61
61
|
"expandedOutputMaxLines": 10, // expanded tool Output lines; overflow shows a footer hint
|
|
62
|
-
"expandedPreviewMaxLines": 40, // max lines for expanded
|
|
62
|
+
"expandedPreviewMaxLines": 40, // max lines for expanded TaskList bodies (expanded diffs always show all)
|
|
63
63
|
"inputClip": 100, // tool summary path/command clip length
|
|
64
64
|
"showStartupHeader": true, // startup header (logo + tips) toggle
|
|
65
65
|
"scrollStepLines": 3, // fullscreen wheel scroll step
|
|
@@ -89,7 +89,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
> [!TIP]
|
|
92
|
-
> **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries.
|
|
92
|
+
> **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. An expanded diff always shows every line; when expanded Input/Output exceeds the line cap, the footer `… +N more lines • click to show more` opens a full preview. Double-click an expanded panel to collapse it.
|
|
93
93
|
|
|
94
94
|
> [!NOTE]
|
|
95
95
|
> **Mermaid rendering**: 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.
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
50
50
|
|
|
51
51
|
// features
|
|
52
52
|
"enableSessionReference": true, // @ session 引用
|
|
53
|
-
"enableSubagentAutocomplete": true, // @
|
|
53
|
+
"enableSubagentAutocomplete": true, // @subagent:[name] 补全与委派提示
|
|
54
54
|
"enableContextCommand": true, // /context 上下文检查
|
|
55
55
|
"enableAgentSummary": true, // 每回合工具摘要
|
|
56
56
|
"enableWorkingMessage": true, // Working... 底部 token/耗时
|
|
@@ -59,7 +59,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
59
59
|
// ui
|
|
60
60
|
"expandedInputMaxLines": 5, // 展开工具卡 Input 可见行数,超出在末行显示展开提示
|
|
61
61
|
"expandedOutputMaxLines": 10, // 展开工具卡 Output 可见行数,超出在末行显示展开提示
|
|
62
|
-
"expandedPreviewMaxLines": 40, // 展开
|
|
62
|
+
"expandedPreviewMaxLines": 40, // 展开 TaskList 正文最大行数(展开 diff 始终全量)
|
|
63
63
|
"inputClip": 100, // 工具摘要 path/command 折叠字符数
|
|
64
64
|
"showStartupHeader": true, // 启动头(logo + tips)开关
|
|
65
65
|
"scrollStepLines": 3, // fullscreen 滚轮步进
|
|
@@ -89,7 +89,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
> [!TIP]
|
|
92
|
-
> **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact
|
|
92
|
+
> **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;工具卡展开态 diff 全量显示,Input/Output 超行时末行 `… +N more lines • click to show more` 打开全量预览,双击展开面板收起。
|
|
93
93
|
|
|
94
94
|
> [!NOTE]
|
|
95
95
|
> **Mermaid 渲染**:建议把 `markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
|
|
@@ -37,9 +37,8 @@ export const DEFAULT_TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
|
|
|
37
37
|
writeDiffCollapsedLines: 0,
|
|
38
38
|
diffWordWrap: true,
|
|
39
39
|
/**
|
|
40
|
-
* Expanded
|
|
41
|
-
*
|
|
42
|
-
* Raise via /ccstyle → Diff → Expanded max lines when reviewing large dumps.
|
|
40
|
+
* Expanded TaskList body cap. Tool Input/Output and diffs use their own settings;
|
|
41
|
+
* an expanded diff always renders every line.
|
|
43
42
|
*/
|
|
44
43
|
expandedPreviewMaxLines: 40,
|
|
45
44
|
};
|
|
@@ -485,8 +485,7 @@ export async function showCcstylePanel(
|
|
|
485
485
|
const expandedMaxSetting = {
|
|
486
486
|
id: "expandedPreviewMaxLines",
|
|
487
487
|
label: "Expanded max lines",
|
|
488
|
-
description:
|
|
489
|
-
"Max diff/TaskList body lines when expanded. Tool Input/Output use the two settings above.",
|
|
488
|
+
description: "Max TaskList body lines when expanded. Expanded diffs show every line.",
|
|
490
489
|
currentValue: String(config.expandedPreviewMaxLines),
|
|
491
490
|
values: [...EXPANDED_PREVIEW_MAX_LINES_VALUES],
|
|
492
491
|
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
@@ -556,7 +555,7 @@ export async function showCcstylePanel(
|
|
|
556
555
|
const subagentAutocompleteToggle = featureToggleSetting(
|
|
557
556
|
"enableSubagentAutocomplete",
|
|
558
557
|
"Subagent autocomplete",
|
|
559
|
-
"@
|
|
558
|
+
"@subagent:[name] mentions suggest agents and inject delegation instructions. Next restart applies.",
|
|
560
559
|
"Subagent autocomplete disabled.",
|
|
561
560
|
config.enableSubagentAutocomplete,
|
|
562
561
|
);
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
SESSION_REFERENCE_CUSTOM_TYPE,
|
|
16
16
|
SESSION_REFERENCE_PREFIX,
|
|
17
|
+
assignReferenceTokens,
|
|
17
18
|
buildReferenceContentFromSections,
|
|
18
19
|
extractSessionReferenceIds,
|
|
19
20
|
formatReferenceSession,
|
|
@@ -123,7 +124,7 @@ function formatDate(date: Date): string {
|
|
|
123
124
|
const sessionSearchTextCache = new WeakMap<SessionReference, string>();
|
|
124
125
|
const sessionItemCache = new WeakMap<
|
|
125
126
|
SessionReference,
|
|
126
|
-
{ cwd: string;
|
|
127
|
+
{ cwd: string; token: string; item: AutocompleteItem }
|
|
127
128
|
>();
|
|
128
129
|
|
|
129
130
|
function sessionSearchText(reference: SessionReference): string {
|
|
@@ -138,30 +139,24 @@ function sessionSearchText(reference: SessionReference): string {
|
|
|
138
139
|
function sessionItem(
|
|
139
140
|
reference: SessionReference,
|
|
140
141
|
currentCwd: string,
|
|
141
|
-
|
|
142
|
+
token: string,
|
|
142
143
|
): AutocompleteItem {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (cached?.cwd === currentCwd && cached[variant]) return cached[variant];
|
|
144
|
+
const cached = sessionItemCache.get(reference);
|
|
145
|
+
if (cached?.cwd === currentCwd && cached.token === token) return cached.item;
|
|
146
146
|
|
|
147
147
|
const session = reference.info;
|
|
148
148
|
const workspace = samePath(session.cwd, currentCwd)
|
|
149
149
|
? "current workspace"
|
|
150
150
|
: session.cwd || "unknown workspace";
|
|
151
|
-
const
|
|
152
|
-
//
|
|
153
|
-
const
|
|
154
|
-
const referenceId = useStableId
|
|
155
|
-
? reference.referenceIds[0]
|
|
156
|
-
: (sessionName ?? reference.referenceIds[0]);
|
|
151
|
+
const kindLabel = reference.kind === "subagent" ? "[SubAgent]" : "[Session]";
|
|
152
|
+
// 消歧后的 token 会带上日期;回落到 ID 时下拉仍显示可读标题。
|
|
153
|
+
const title = token === (reference.referenceIds[0] ?? session.id) ? sessionTitle(session) : token;
|
|
157
154
|
const item: AutocompleteItem = {
|
|
158
|
-
value: `${SESSION_REFERENCE_PREFIX}[${
|
|
159
|
-
label: `${
|
|
155
|
+
value: `${SESSION_REFERENCE_PREFIX}[${token}]`,
|
|
156
|
+
label: `${kindLabel} ${title}`,
|
|
160
157
|
description: `${workspace} · ${session.messageCount} messages · ${formatDate(session.modified)}`,
|
|
161
158
|
};
|
|
162
|
-
|
|
163
|
-
cached[variant] = item;
|
|
164
|
-
sessionItemCache.set(reference, cached);
|
|
159
|
+
sessionItemCache.set(reference, { cwd: currentCwd, token, item });
|
|
165
160
|
return item;
|
|
166
161
|
}
|
|
167
162
|
|
|
@@ -197,11 +192,7 @@ function filterSessions(
|
|
|
197
192
|
if (query.startsWith("session:")) return [];
|
|
198
193
|
|
|
199
194
|
const ordered = orderSessionReferences(references, currentCwd);
|
|
200
|
-
const
|
|
201
|
-
for (const reference of ordered) {
|
|
202
|
-
const name = reference.info.name?.trim();
|
|
203
|
-
if (name) nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
|
|
204
|
-
}
|
|
195
|
+
const tokens = assignReferenceTokens(ordered);
|
|
205
196
|
const trimmed = query.trim();
|
|
206
197
|
// 模糊匹配只接受有 session name 的会话;subagent 按 agent name 匹配。
|
|
207
198
|
const searchable = ordered.filter(
|
|
@@ -211,10 +202,11 @@ function filterSessions(
|
|
|
211
202
|
const matches = trimmed
|
|
212
203
|
? fuzzyFilter(searchable, trimmed, sessionSearchText)
|
|
213
204
|
: ordered.slice(0, MAX_SESSION_SUGGESTIONS);
|
|
214
|
-
return matches
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
205
|
+
return matches
|
|
206
|
+
.slice(0, MAX_SESSION_SUGGESTIONS)
|
|
207
|
+
.map((reference) =>
|
|
208
|
+
sessionItem(reference, currentCwd, tokens.get(reference) ?? reference.info.id),
|
|
209
|
+
);
|
|
218
210
|
}
|
|
219
211
|
|
|
220
212
|
function isPathLikeQuery(query: string): boolean {
|
|
@@ -452,22 +444,17 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
452
444
|
),
|
|
453
445
|
));
|
|
454
446
|
const referencesById = new Map<string, SessionReference>();
|
|
455
|
-
const
|
|
456
|
-
const ambiguousNames = new Set<string>();
|
|
447
|
+
const referencesByToken = new Map<string, SessionReference>();
|
|
457
448
|
for (const reference of references) {
|
|
458
449
|
for (const id of reference.referenceIds) referencesById.set(id, reference);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
ambiguousNames.add(name);
|
|
464
|
-
} else {
|
|
465
|
-
referencesByName.set(name, reference);
|
|
466
|
-
}
|
|
450
|
+
}
|
|
451
|
+
// 同位重名的会话用带日期的 token 区分,解析端与补全端用同一套分配。
|
|
452
|
+
for (const [reference, token] of assignReferenceTokens(references)) {
|
|
453
|
+
referencesByToken.set(token, reference);
|
|
467
454
|
}
|
|
468
455
|
const seenReferences = new Set<SessionReference>();
|
|
469
456
|
const matchingReferences = referenceIds
|
|
470
|
-
.map((id) => referencesById.get(id) ??
|
|
457
|
+
.map((id) => referencesById.get(id) ?? referencesByToken.get(id))
|
|
471
458
|
.filter((reference): reference is SessionReference => {
|
|
472
459
|
if (!reference || reference.info.id === currentSessionId || seenReferences.has(reference))
|
|
473
460
|
return false;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { stripTerminalSequences } from "../../utils/ansi-text.ts";
|
|
3
|
+
|
|
1
4
|
export const SESSION_REFERENCE_CUSTOM_TYPE = "session-reference";
|
|
2
5
|
export const SESSION_REFERENCE_PREFIX = "@session:";
|
|
3
6
|
|
|
@@ -45,13 +48,104 @@ export function extractSessionReferenceIds(text: string): string[] {
|
|
|
45
48
|
return ids;
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
const EMPTY_SESSION_TITLE = "(no messages)";
|
|
52
|
+
|
|
48
53
|
export function sessionTitle(info: ReferenceSessionInfo, maxLength = 80): string {
|
|
49
|
-
const source = info.name?.trim() || info.firstMessage ||
|
|
54
|
+
const source = info.name?.trim() || info.firstMessage || EMPTY_SESSION_TITLE;
|
|
50
55
|
const normalized = source.replace(CONTROL_CHARACTERS, " ").replace(/\s+/g, " ").trim();
|
|
51
56
|
if (normalized.length <= maxLength) return normalized;
|
|
52
57
|
return `${normalized.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 编辑器里的 `@session:[…]` 是行内文本,token 按显示宽度收口,
|
|
62
|
+
* 不让首条消息整段挤进输入行。同名会话靠后缀消歧,不受它影响。
|
|
63
|
+
*/
|
|
64
|
+
export const REFERENCE_TOKEN_MAX_WIDTH = 32;
|
|
65
|
+
|
|
66
|
+
/** Editor `@session:[…]` text: the readable title, never a placeholder or brackets. */
|
|
67
|
+
export function sessionReferenceLabel(
|
|
68
|
+
info: ReferenceSessionInfo,
|
|
69
|
+
maxWidth = REFERENCE_TOKEN_MAX_WIDTH,
|
|
70
|
+
): string {
|
|
71
|
+
const title = sessionTitle(info).replace(/[[\]]/g, "").trim();
|
|
72
|
+
if (!title || title === EMPTY_SESSION_TITLE) return "";
|
|
73
|
+
// truncateToWidth 会在省略号前后补 ANSI reset,token 是明文,去掉。
|
|
74
|
+
return stripTerminalSequences(truncateToWidth(title, maxWidth, "…")).trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ReferenceTokenSource {
|
|
78
|
+
referenceIds: string[];
|
|
79
|
+
info: ReferenceSessionInfo;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** UTC keeps the disambiguator identical across timezones (same-day drift is harmless). */
|
|
83
|
+
function referenceTokenDate(date: Date): string {
|
|
84
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
85
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
86
|
+
return `${month}-${day}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function referenceTokenTime(date: Date): string {
|
|
90
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
91
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
92
|
+
return `${hours}:${minutes}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Title first; date then time widen the disambiguation before the ID fallback. */
|
|
96
|
+
function referenceTokenCandidates(reference: ReferenceTokenSource): string[] {
|
|
97
|
+
const title = sessionReferenceLabel(reference.info);
|
|
98
|
+
const stableId = reference.referenceIds[0] ?? reference.info.id;
|
|
99
|
+
if (!title) return [stableId];
|
|
100
|
+
const date = referenceTokenDate(reference.info.modified);
|
|
101
|
+
return [
|
|
102
|
+
title,
|
|
103
|
+
`${title} ${date}`,
|
|
104
|
+
`${title} ${date} ${referenceTokenTime(reference.info.modified)}`,
|
|
105
|
+
stableId,
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Assign each reference a unique editor token. Same-named sessions keep a readable
|
|
111
|
+
* title plus a date/time suffix; the stable ID is only the last resort.
|
|
112
|
+
*/
|
|
113
|
+
export function assignReferenceTokens<T extends ReferenceTokenSource>(
|
|
114
|
+
references: T[],
|
|
115
|
+
): Map<T, string> {
|
|
116
|
+
const candidates = new Map<T, string[]>();
|
|
117
|
+
let levels = 0;
|
|
118
|
+
for (const reference of references) {
|
|
119
|
+
const options = referenceTokenCandidates(reference);
|
|
120
|
+
levels = Math.max(levels, options.length);
|
|
121
|
+
candidates.set(reference, options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tokens = new Map<T, string>();
|
|
125
|
+
const taken = new Set<string>();
|
|
126
|
+
for (let level = 0; level < levels && tokens.size < references.length; level++) {
|
|
127
|
+
const counts = new Map<string, number>();
|
|
128
|
+
for (const [reference, options] of candidates) {
|
|
129
|
+
const candidate = tokens.has(reference) ? undefined : options[level];
|
|
130
|
+
if (candidate) counts.set(candidate, (counts.get(candidate) ?? 0) + 1);
|
|
131
|
+
}
|
|
132
|
+
for (const [reference, options] of candidates) {
|
|
133
|
+
if (tokens.has(reference)) continue;
|
|
134
|
+
const candidate = options[level];
|
|
135
|
+
if (candidate && counts.get(candidate) === 1 && !taken.has(candidate)) {
|
|
136
|
+
tokens.set(reference, candidate);
|
|
137
|
+
taken.add(candidate);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const reference of references) {
|
|
142
|
+
if (!tokens.has(reference)) {
|
|
143
|
+
tokens.set(reference, reference.referenceIds[0] ?? reference.info.id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return tokens;
|
|
147
|
+
}
|
|
148
|
+
|
|
55
149
|
function byteLength(text: string): number {
|
|
56
150
|
return Buffer.byteLength(text, "utf8");
|
|
57
151
|
}
|
|
@@ -18,9 +18,12 @@ type AgentInfo = {
|
|
|
18
18
|
filePath: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
export const SUBAGENT_DELEGATION_CUSTOM_TYPE = "subagent-delegation";
|
|
22
|
+
export const SUBAGENT_REFERENCE_PREFIX = "@subagent:";
|
|
23
|
+
|
|
21
24
|
const MAX_SUGGESTIONS = 2;
|
|
22
|
-
|
|
23
|
-
const
|
|
25
|
+
const MENTION_PATTERN = /(?:^|[\t ])@([^\s@]*)$/;
|
|
26
|
+
const SUBAGENT_REFERENCE_PATTERN = /(?:^|\s)@subagent:\[([^\]]+)](?![\w-])/g;
|
|
24
27
|
|
|
25
28
|
function getAgentDir(): string {
|
|
26
29
|
const override = process.env.PI_CODING_AGENT_DIR;
|
|
@@ -60,6 +63,36 @@ function loadAgents(): AgentInfo[] {
|
|
|
60
63
|
});
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
function extractMentionQuery(textBeforeCursor: string): string | undefined {
|
|
67
|
+
return textBeforeCursor.match(MENTION_PATTERN)?.[1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Filter text for agent names, or null when this provider should not claim the @ query. */
|
|
71
|
+
function agentSearchQuery(raw: string): string | null {
|
|
72
|
+
if (raw.startsWith("session:")) return null;
|
|
73
|
+
if (raw === "subagent") return "";
|
|
74
|
+
const prefixed = raw.match(/^subagent:\[?([^\]]*)$/);
|
|
75
|
+
if (prefixed) return prefixed[1] ?? "";
|
|
76
|
+
return raw;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function extractSubagentReferenceNames(text: string): string[] {
|
|
80
|
+
const names: string[] = [];
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
for (const match of text.matchAll(SUBAGENT_REFERENCE_PATTERN)) {
|
|
83
|
+
const name = match[1]?.trim();
|
|
84
|
+
if (name && !seen.has(name)) {
|
|
85
|
+
seen.add(name);
|
|
86
|
+
names.push(name);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function subagentReferenceValue(name: string): string {
|
|
93
|
+
return `${SUBAGENT_REFERENCE_PREFIX}[${name}]`;
|
|
94
|
+
}
|
|
95
|
+
|
|
63
96
|
export function createAgentAutocompleteProvider(
|
|
64
97
|
current: AutocompleteProvider,
|
|
65
98
|
getAgents: () => AgentInfo[],
|
|
@@ -74,13 +107,16 @@ export function createAgentAutocompleteProvider(
|
|
|
74
107
|
): Promise<AutocompleteSuggestions | null> {
|
|
75
108
|
const currentLine = lines[cursorLine] ?? "";
|
|
76
109
|
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
77
|
-
const
|
|
110
|
+
const rawQuery = extractMentionQuery(textBeforeCursor);
|
|
111
|
+
if (rawQuery === undefined) {
|
|
112
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
113
|
+
}
|
|
78
114
|
|
|
79
|
-
|
|
115
|
+
const search = agentSearchQuery(rawQuery);
|
|
116
|
+
if (search === null) {
|
|
80
117
|
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
81
118
|
}
|
|
82
119
|
|
|
83
|
-
const query = match[1]!;
|
|
84
120
|
const agents = getAgents();
|
|
85
121
|
|
|
86
122
|
if (options.signal.aborted || agents.length === 0) {
|
|
@@ -89,17 +125,20 @@ export function createAgentAutocompleteProvider(
|
|
|
89
125
|
|
|
90
126
|
const [baseSuggestions, matches] = await Promise.all([
|
|
91
127
|
current.getSuggestions(lines, cursorLine, cursorCol, options),
|
|
92
|
-
Promise.resolve(
|
|
128
|
+
Promise.resolve(search.trim() ? fuzzyFilter(agents, search, (a) => a.name) : agents),
|
|
93
129
|
]);
|
|
94
130
|
if (options.signal.aborted) return null;
|
|
95
131
|
|
|
96
132
|
const agentItems: AutocompleteItem[] = matches.slice(0, MAX_SUGGESTIONS).map((agent) => ({
|
|
97
|
-
value:
|
|
133
|
+
value: subagentReferenceValue(agent.name),
|
|
98
134
|
label: `[SubAgent] ${agent.displayName}`,
|
|
99
135
|
description: `${agent.model ?? "?"} · ${agent.thinking ?? "?"}`,
|
|
100
136
|
}));
|
|
101
|
-
const
|
|
102
|
-
const
|
|
137
|
+
const prefix = `@${rawQuery}`;
|
|
138
|
+
const hasCompatibleBaseSuggestions = baseSuggestions?.prefix === prefix;
|
|
139
|
+
const agentValues = new Set(
|
|
140
|
+
agents.flatMap((agent) => [`@${agent.name}`, subagentReferenceValue(agent.name)]),
|
|
141
|
+
);
|
|
103
142
|
const baseItems = hasCompatibleBaseSuggestions
|
|
104
143
|
? baseSuggestions.items.filter((item) => !agentValues.has(item.value))
|
|
105
144
|
: [];
|
|
@@ -112,7 +151,7 @@ export function createAgentAutocompleteProvider(
|
|
|
112
151
|
});
|
|
113
152
|
|
|
114
153
|
if (items.length === 0 && !hasCompatibleBaseSuggestions) return baseSuggestions;
|
|
115
|
-
return { items, prefix
|
|
154
|
+
return { items, prefix };
|
|
116
155
|
},
|
|
117
156
|
|
|
118
157
|
applyCompletion(
|
|
@@ -166,16 +205,13 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
|
|
|
166
205
|
sessionGeneration++;
|
|
167
206
|
});
|
|
168
207
|
|
|
169
|
-
// Inject
|
|
170
|
-
// Supports multiple different subagents in a single prompt.
|
|
171
|
-
const AGENT_PROMPT_PATTERN = /(?:^|[\s])@([^\s:@][^\s:]*)/g;
|
|
208
|
+
// Inject as a request message, not systemPrompt, so the cached prefix stays stable.
|
|
172
209
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
173
210
|
const agents = getAgents();
|
|
174
211
|
if (agents.length === 0) return;
|
|
175
212
|
|
|
176
213
|
const mentions: string[] = [];
|
|
177
|
-
for (const
|
|
178
|
-
const name = m[1]!;
|
|
214
|
+
for (const name of extractSubagentReferenceNames(event.prompt)) {
|
|
179
215
|
if (agents.some((a) => a.name === name) && !mentions.includes(name)) {
|
|
180
216
|
mentions.push(name);
|
|
181
217
|
}
|
|
@@ -186,12 +222,15 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
|
|
|
186
222
|
const agentList = mentions.map((n) => `"${n}" (${agentMap.get(n)!.displayName})`).join(", ");
|
|
187
223
|
|
|
188
224
|
return {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
225
|
+
message: {
|
|
226
|
+
customType: SUBAGENT_DELEGATION_CUSTOM_TYPE,
|
|
227
|
+
content:
|
|
228
|
+
`The user's prompt references these subagent types: ${agentList}. ` +
|
|
229
|
+
`You MUST use the Agent tool for EACH mentioned subagent to delegate the relevant parts of the request. ` +
|
|
230
|
+
`Handle different subagents separately — do NOT merge their tasks into a single Agent call. ` +
|
|
231
|
+
`For example, if the user mentions @subagent:[coder] and @subagent:[explore], make two separate Agent tool calls, one with subagent_type="coder" and another with subagent_type="explore".`,
|
|
232
|
+
display: false,
|
|
233
|
+
},
|
|
195
234
|
};
|
|
196
235
|
});
|
|
197
236
|
}
|
|
@@ -36,7 +36,13 @@ import { showMoreHintText } from "./tool/show-more-hint.ts";
|
|
|
36
36
|
import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
|
|
37
37
|
import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
|
|
38
38
|
import { getMessageDisplayTheme } from "./tool/message-display.ts";
|
|
39
|
-
import {
|
|
39
|
+
import { mcpToolTitle } from "./tool/mcp-title.ts";
|
|
40
|
+
import {
|
|
41
|
+
fitToolCallSummary,
|
|
42
|
+
humanizeToolLabel,
|
|
43
|
+
renderToolSummary,
|
|
44
|
+
toolCallSummary,
|
|
45
|
+
} from "./tool/names.ts";
|
|
40
46
|
|
|
41
47
|
// 成功勾:亮绿 truecolor(与 message-display 一致)
|
|
42
48
|
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
@@ -102,25 +108,6 @@ export function shouldRenderRichDiff(
|
|
|
102
108
|
return mode === "on" && !isError && (toolName === "edit" || toolName === "write");
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
export function isMcpToolDefinition(definition: any, toolName: string): boolean {
|
|
106
|
-
const label = typeof definition?.label === "string" ? definition.label.trim() : "";
|
|
107
|
-
if (/^MCP(?::|$)/i.test(label)) return true;
|
|
108
|
-
if (toolName === "mcp" || /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName)) return true;
|
|
109
|
-
if (label) return false;
|
|
110
|
-
const description = typeof definition?.description === "string" ? definition.description : "";
|
|
111
|
-
return /\bModel Context Protocol\b/i.test(description);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function humanizeMcpToolName(toolName: string): string {
|
|
115
|
-
const words = toolName
|
|
116
|
-
.replace(/^mcp(?:[_:-]+)+/i, "")
|
|
117
|
-
.split(/[_:-]+/)
|
|
118
|
-
.filter(Boolean);
|
|
119
|
-
return words.length
|
|
120
|
-
? words.map((word) => word[0]!.toUpperCase() + word.slice(1)).join(" ")
|
|
121
|
-
: "MCP";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
111
|
/** 排除名单内且自带 renderer 的工具保留原渲染。 */
|
|
125
112
|
export function preservesOriginalRenderer(
|
|
126
113
|
extensionDefinition: any,
|
|
@@ -225,9 +212,10 @@ function createCcstyleTool(
|
|
|
225
212
|
writeExecutionMetadata: WriteExecutionMetadataStore,
|
|
226
213
|
): any {
|
|
227
214
|
const toolName = originalTool.name;
|
|
228
|
-
const label =
|
|
229
|
-
|
|
230
|
-
|
|
215
|
+
const label = originalTool.label || toolName;
|
|
216
|
+
const defaultTitle = label === toolName ? humanizeToolLabel(label) : label;
|
|
217
|
+
// MCP 工具直接用 adapter 暴露的真实工具名,不做人性化
|
|
218
|
+
const title = mcpToolTitle({ toolName, definition: originalTool }) ?? defaultTitle;
|
|
231
219
|
|
|
232
220
|
return {
|
|
233
221
|
...originalTool,
|
|
@@ -252,7 +240,7 @@ function createCcstyleTool(
|
|
|
252
240
|
// 不再每次 tick 走 updateDisplay 把整张卡重建一遍。
|
|
253
241
|
const pendingIconStyled = () => theme.fg(toolIconColor(context), pendingIcon(toolName));
|
|
254
242
|
const summary = toolCallSummary(toolName, args, {
|
|
255
|
-
title
|
|
243
|
+
title,
|
|
256
244
|
variant: "default",
|
|
257
245
|
cwd: context?.cwd,
|
|
258
246
|
});
|
|
@@ -295,7 +283,7 @@ function createCcstyleTool(
|
|
|
295
283
|
cachedWidth = width;
|
|
296
284
|
cachedIcon = icon;
|
|
297
285
|
// 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
|
|
298
|
-
cachedLine = `${lead}${icon} ${
|
|
286
|
+
cachedLine = `${lead}${icon} ${renderToolSummary(summary, mainWidth, theme.fg.bind(theme))}${extraStyled}`;
|
|
299
287
|
return [truncateToWidth(cachedLine, viewportWidth, "")];
|
|
300
288
|
},
|
|
301
289
|
invalidate() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { ToolExecutionComponent, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { CompactThinkingController } from "../feature/compact-thinking.ts";
|
|
3
3
|
import { installToolGrouping, type ToolGroupingHooks } from "./tool/grouping.ts";
|
|
4
4
|
import {
|
|
@@ -266,11 +266,13 @@ export default function (
|
|
|
266
266
|
// ---- 对外导出:入口/测试实际消费的符号 ----
|
|
267
267
|
export { getCompactThinkingConfig } from "../config/config.ts";
|
|
268
268
|
export {
|
|
269
|
-
humanizeMcpToolName,
|
|
270
|
-
isMcpToolDefinition,
|
|
271
269
|
preservesOriginalRenderer,
|
|
272
270
|
shouldRenderRichDiff,
|
|
273
271
|
} from "./default-mode.ts";
|
|
272
|
+
export {
|
|
273
|
+
isMcpToolDefinition,
|
|
274
|
+
mcpToolTitle,
|
|
275
|
+
} from "./tool/mcp-title.ts";
|
|
274
276
|
export {
|
|
275
277
|
ExpandedToolIoView,
|
|
276
278
|
ExpandedToolResultText,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { hasActiveTextPreview, showTextPreview } from "../../feature/context.ts";
|
|
2
3
|
import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
3
4
|
import { patchRegistry, TOOL_MOUSE_OWNER_KEY } from "../../utils/patch-keys.ts";
|
|
@@ -311,6 +312,31 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
|
|
|
311
312
|
const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
|
|
312
313
|
const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
|
|
313
314
|
|
|
315
|
+
/**
|
|
316
|
+
* diff 结果组件自带 remainder 行:声明存在时只有那一行是展开入口(正文里的同名字样不算);
|
|
317
|
+
* 未声明的组件继续用文本规则。
|
|
318
|
+
*/
|
|
319
|
+
function isCollapsedHintRow(component: any, finalLine: string): boolean {
|
|
320
|
+
const result = component?.resultRendererComponent;
|
|
321
|
+
return typeof result?.isCollapsedHintLine === "function"
|
|
322
|
+
? result.isCollapsedHintLine(finalLine)
|
|
323
|
+
: true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* pi 0.87 给工具结果区套了 MouseRegion:整卡左键 click 都会 setExpanded。
|
|
328
|
+
* 命中该区域时官方会跳过文本选区,所以可以安全吞掉;单行摘要卡没有声明
|
|
329
|
+
* remainder 行,继续走官方整行行为。
|
|
330
|
+
*/
|
|
331
|
+
function blocksOfficialCardToggle(component: any, event: any): boolean {
|
|
332
|
+
if (event?.type !== "click" || event.button !== "left" || component?.expanded) return false;
|
|
333
|
+
const result = component?.resultRendererComponent;
|
|
334
|
+
if (typeof result?.isCollapsedHintLine !== "function") return false;
|
|
335
|
+
const lines = component.render?.(Math.max(1, Math.floor(Number(event.width) || 0)));
|
|
336
|
+
const line = Array.isArray(lines) ? lines[event.y] : undefined;
|
|
337
|
+
return typeof line === "string" && !result.isCollapsedHintLine(line);
|
|
338
|
+
}
|
|
339
|
+
|
|
314
340
|
/**
|
|
315
341
|
* 官方 fullscreen 工具卡点击:collapsed hint 单击展开
|
|
316
342
|
* (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡双击收起,
|
|
@@ -352,6 +378,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
352
378
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
353
379
|
const hint = collapsedHintHitbox(line);
|
|
354
380
|
if (!hint || packet.col < hint.startCol || packet.col > hint.endCol) return false;
|
|
381
|
+
if (!isCollapsedHintRow(component, line)) return false;
|
|
355
382
|
// single-expand:展开前收起其他已展开工具卡/group。
|
|
356
383
|
const others: any[] = [];
|
|
357
384
|
collectFullscreenToolCards(hit.box.component, others);
|
|
@@ -574,7 +601,11 @@ function buildInteractionFrame(
|
|
|
574
601
|
const line = placement.finalLine;
|
|
575
602
|
if (!component.expanded) {
|
|
576
603
|
const box = collapsedHintHitbox(line);
|
|
577
|
-
if (
|
|
604
|
+
if (
|
|
605
|
+
box &&
|
|
606
|
+
COLLAPSED_TOOL_SUMMARY.test(stripTerminalSequences(line)) &&
|
|
607
|
+
isCollapsedHintRow(component, line)
|
|
608
|
+
) {
|
|
578
609
|
regions.push({ kind: "collapsed-hint", row: finalRow, ...box, component });
|
|
579
610
|
}
|
|
580
611
|
continue;
|
|
@@ -867,6 +898,7 @@ export function teardownToolMouseInteraction(
|
|
|
867
898
|
setHoveredMessageDisplay(null);
|
|
868
899
|
setHoveredToolIo(null, null);
|
|
869
900
|
setHoveredCompactAssistant(null);
|
|
901
|
+
releaseToolCardMouseGuard();
|
|
870
902
|
clearExpandPanelDoubleClick();
|
|
871
903
|
try {
|
|
872
904
|
if (isLazyProxyTui(getToolMouseTui())) releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
@@ -901,6 +933,43 @@ export function resetToolHoverState(): void {
|
|
|
901
933
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
902
934
|
}
|
|
903
935
|
|
|
936
|
+
/**
|
|
937
|
+
* pi 0.87 起工具卡结果区自带 MouseRegion,整卡左键 click 都会 setExpanded。
|
|
938
|
+
* ccstyle 的多行折叠卡(rich diff)只允许声明的 remainder 行展开,
|
|
939
|
+
* 其余位置直接吞掉 click,避免官方把 diff 正文当展开入口。
|
|
940
|
+
* 0.84 及更早没有 handleMouse,安装时跳过。
|
|
941
|
+
*/
|
|
942
|
+
const TOOL_CARD_MOUSE_GUARD_KEY = Symbol.for("pi.ccstyle.tool-card-mouse-guard");
|
|
943
|
+
|
|
944
|
+
type ToolCardMouseGuard = {
|
|
945
|
+
prototype: any;
|
|
946
|
+
original: (event: any) => any;
|
|
947
|
+
wrapper: (event: any) => any;
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
function installToolCardMouseGuard(): void {
|
|
951
|
+
const prototype = (ToolExecutionComponent as any)?.prototype;
|
|
952
|
+
if (typeof prototype?.handleMouse !== "function") return;
|
|
953
|
+
const previous = (globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] as ToolCardMouseGuard | undefined;
|
|
954
|
+
if (previous && previous.prototype === prototype && prototype.handleMouse === previous.wrapper)
|
|
955
|
+
return;
|
|
956
|
+
// 每次取当前值作为下游,可能是别的扩展的包装。
|
|
957
|
+
const original = prototype.handleMouse;
|
|
958
|
+
const wrapper = function (this: any, event: any) {
|
|
959
|
+
if (blocksOfficialCardToggle(this, event)) return { handled: true };
|
|
960
|
+
return original.call(this, event);
|
|
961
|
+
};
|
|
962
|
+
prototype.handleMouse = wrapper;
|
|
963
|
+
(globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] = { prototype, original, wrapper };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function releaseToolCardMouseGuard(): void {
|
|
967
|
+
const guard = (globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] as ToolCardMouseGuard | undefined;
|
|
968
|
+
if (!guard) return;
|
|
969
|
+
if (guard.prototype?.handleMouse === guard.wrapper) guard.prototype.handleMouse = guard.original;
|
|
970
|
+
(globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] = undefined;
|
|
971
|
+
}
|
|
972
|
+
|
|
904
973
|
export function installToolMouseInteraction(
|
|
905
974
|
ctx: any,
|
|
906
975
|
owner: object = DEFAULT_TOOL_MOUSE_OWNER,
|
|
@@ -912,6 +981,7 @@ export function installToolMouseInteraction(
|
|
|
912
981
|
|
|
913
982
|
toolMouseInstallationOwner = owner;
|
|
914
983
|
patchRegistry.install(TOOL_MOUSE_OWNER_KEY, owner);
|
|
984
|
+
installToolCardMouseGuard();
|
|
915
985
|
setHoveredToolCallId(null);
|
|
916
986
|
toolMouseUi = ctx.ui;
|
|
917
987
|
// 0.84+ 的 tui 是惰性 Proxy:regular 保留原生 scrollback;fullscreen
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
type DiffIndicatorMode,
|
|
4
4
|
type ToolDisplayConfig,
|
|
5
5
|
} from "../../../config/config.ts";
|
|
6
|
-
import
|
|
6
|
+
import { stripTerminalSequencesPreservingLayout } from "../../../utils/ansi-text.ts";
|
|
7
7
|
import { RICH_DIFF_COMPONENT } from "../../../utils/patch-keys.ts";
|
|
8
|
+
import type { DiffPresentationMode } from "./diff-presentation.ts";
|
|
8
9
|
export { RICH_DIFF_COMPONENT };
|
|
9
10
|
|
|
10
11
|
/** Snapshot or live getter — panel changes must apply on the next paint. */
|
|
@@ -33,6 +34,15 @@ export function resolveLiveDisplayConfig(input: DisplayConfigInput): ToolDisplay
|
|
|
33
34
|
return typeof input === "function" ? input() : input;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 比对用归一化:ANSI、缩进前缀和右侧填充不影响判断。
|
|
39
|
+
* 折叠态 remainder 行里有任意正文,入口必须先由组件声明再比对文本。
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeCollapsedHintLine(line: string | undefined): string | undefined {
|
|
42
|
+
if (line === undefined) return undefined;
|
|
43
|
+
return stripTerminalSequencesPreservingLayout(line).replace(/\s+/g, " ").trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
/** Cache key fragment so indicator/wrap/limits invalidate without host recreate. */
|
|
37
47
|
export function displayConfigCacheKey(config: ToolDisplayConfig): string {
|
|
38
48
|
return [
|
|
@@ -42,7 +52,6 @@ export function displayConfigCacheKey(config: ToolDisplayConfig): string {
|
|
|
42
52
|
String(config.editDiffCollapsedLines),
|
|
43
53
|
String(config.writeDiffCollapsedLines),
|
|
44
54
|
config.diffWordWrap ? "1" : "0",
|
|
45
|
-
String(config.expandedPreviewMaxLines),
|
|
46
55
|
].join(":");
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -59,6 +68,8 @@ export function createDiffRenderCache() {
|
|
|
59
68
|
let cachedConfigKey: string | undefined;
|
|
60
69
|
let cachedHovered: boolean | undefined;
|
|
61
70
|
let cachedLines: string[] | undefined;
|
|
71
|
+
/** 与 cachedLines 同一次渲染的折叠态 remainder 行文本。 */
|
|
72
|
+
let cachedHintLine: string | undefined;
|
|
62
73
|
|
|
63
74
|
return {
|
|
64
75
|
get(
|
|
@@ -80,6 +91,9 @@ export function createDiffRenderCache() {
|
|
|
80
91
|
}
|
|
81
92
|
return undefined;
|
|
82
93
|
},
|
|
94
|
+
getHintLine(): string | undefined {
|
|
95
|
+
return cachedHintLine;
|
|
96
|
+
},
|
|
83
97
|
set(
|
|
84
98
|
width: number,
|
|
85
99
|
expanded: boolean,
|
|
@@ -87,6 +101,7 @@ export function createDiffRenderCache() {
|
|
|
87
101
|
configKey: string,
|
|
88
102
|
hovered: boolean,
|
|
89
103
|
lines: string[],
|
|
104
|
+
hintLine?: string,
|
|
90
105
|
): string[] {
|
|
91
106
|
cachedWidth = width;
|
|
92
107
|
cachedExpanded = expanded;
|
|
@@ -94,6 +109,7 @@ export function createDiffRenderCache() {
|
|
|
94
109
|
cachedConfigKey = configKey;
|
|
95
110
|
cachedHovered = hovered;
|
|
96
111
|
cachedLines = lines;
|
|
112
|
+
cachedHintLine = hintLine;
|
|
97
113
|
return lines;
|
|
98
114
|
},
|
|
99
115
|
invalidate(): void {
|
|
@@ -103,6 +119,7 @@ export function createDiffRenderCache() {
|
|
|
103
119
|
cachedConfigKey = undefined;
|
|
104
120
|
cachedHovered = undefined;
|
|
105
121
|
cachedLines = undefined;
|
|
122
|
+
cachedHintLine = undefined;
|
|
106
123
|
},
|
|
107
124
|
};
|
|
108
125
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
displayConfigCacheKey,
|
|
33
33
|
resolveDiffIndicatorMode,
|
|
34
34
|
resolveLiveDisplayConfig,
|
|
35
|
+
normalizeCollapsedHintLine,
|
|
35
36
|
type DiffRenderOptions,
|
|
36
37
|
type DisplayConfigInput,
|
|
37
38
|
} from "./diff-component.ts";
|
|
@@ -127,6 +128,11 @@ export function renderEditDiffResult(
|
|
|
127
128
|
|
|
128
129
|
return {
|
|
129
130
|
[RICH_DIFF_COMPONENT]: true,
|
|
131
|
+
/** 鼠标层只在真正的 remainder 行上提供展开入口。 */
|
|
132
|
+
isCollapsedHintLine(plainLine: string): boolean {
|
|
133
|
+
const hint = cache.getHintLine();
|
|
134
|
+
return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
|
|
135
|
+
},
|
|
130
136
|
render(width: number): string[] {
|
|
131
137
|
// Live config: panel can change indicator/wrap/limits after this component is created.
|
|
132
138
|
const live = resolveLiveDisplayConfig(config);
|
|
@@ -161,11 +167,7 @@ export function renderEditDiffResult(
|
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
const headerRows = renderHeaderRows(parsed.stats, mode, safeWidth, theme);
|
|
164
|
-
const displayLimit = resolveDiffDisplayLimit(
|
|
165
|
-
options.expanded,
|
|
166
|
-
live.editDiffCollapsedLines,
|
|
167
|
-
live.expandedPreviewMaxLines,
|
|
168
|
-
);
|
|
170
|
+
const displayLimit = resolveDiffDisplayLimit(options.expanded, live.editDiffCollapsedLines);
|
|
169
171
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
170
172
|
// Only highlight/render a prefix that can fill the display limit; full-diff
|
|
171
173
|
// LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
|
|
@@ -198,7 +200,6 @@ export function renderEditDiffResult(
|
|
|
198
200
|
safeWidth,
|
|
199
201
|
options.expanded,
|
|
200
202
|
live.editDiffCollapsedLines,
|
|
201
|
-
live.expandedPreviewMaxLines,
|
|
202
203
|
parsed.stats.hunks,
|
|
203
204
|
theme,
|
|
204
205
|
unprocessedLogicalRows,
|
|
@@ -207,11 +208,19 @@ export function renderEditDiffResult(
|
|
|
207
208
|
const frame = renderDiffFrameLine(safeWidth, theme);
|
|
208
209
|
const renderedLines =
|
|
209
210
|
mode === "unified"
|
|
210
|
-
? [...headerRows.map((row) => row.text), frame, ...bodyWithLimit, frame]
|
|
211
|
-
: [...headerRows.map((row) => row.text), ...bodyWithLimit];
|
|
211
|
+
? [...headerRows.map((row) => row.text), frame, ...bodyWithLimit.lines, frame]
|
|
212
|
+
: [...headerRows.map((row) => row.text), ...bodyWithLimit.lines];
|
|
212
213
|
|
|
213
214
|
const clampedLines = clampDiffLinesToWidth(renderedLines, safeWidth);
|
|
214
|
-
return cache.set(
|
|
215
|
+
return cache.set(
|
|
216
|
+
safeWidth,
|
|
217
|
+
options.expanded,
|
|
218
|
+
mode,
|
|
219
|
+
configKey,
|
|
220
|
+
hovered,
|
|
221
|
+
clampedLines,
|
|
222
|
+
normalizeCollapsedHintLine(bodyWithLimit.hintLine),
|
|
223
|
+
);
|
|
215
224
|
},
|
|
216
225
|
invalidate: cache.invalidate,
|
|
217
226
|
} as Component;
|
|
@@ -31,18 +31,16 @@ function renderDiffSpacerLine(width: number): string {
|
|
|
31
31
|
return safeWidth > 0 ? " ".repeat(safeWidth) : "";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
): number {
|
|
39
|
-
|
|
40
|
-
? maxExpandedLines
|
|
41
|
-
: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines;
|
|
34
|
+
/**
|
|
35
|
+
* Body lines to render before the remainder hint. Expanded diffs are never capped:
|
|
36
|
+
* clicking `click to show more` expands the card to the whole diff.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveDiffDisplayLimit(expanded: boolean, maxCollapsedLines: number): number {
|
|
39
|
+
if (expanded) return Number.POSITIVE_INFINITY;
|
|
42
40
|
const collapsedLimit = Number.isFinite(maxCollapsedLines)
|
|
43
41
|
? maxCollapsedLines
|
|
44
42
|
: DEFAULT_TOOL_DISPLAY_CONFIG.editDiffCollapsedLines;
|
|
45
|
-
return
|
|
43
|
+
return Math.max(1, collapsedLimit);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
export function resolveWriteCollapsedLimit(
|
|
@@ -99,7 +97,6 @@ export function applyLineLimit(
|
|
|
99
97
|
width: number,
|
|
100
98
|
expanded: boolean,
|
|
101
99
|
maxCollapsedLines: number,
|
|
102
|
-
maxExpandedLines: number,
|
|
103
100
|
totalHunks: number,
|
|
104
101
|
theme: DiffTheme,
|
|
105
102
|
/**
|
|
@@ -108,17 +105,17 @@ export function applyLineLimit(
|
|
|
108
105
|
*/
|
|
109
106
|
unprocessedLogicalRows = 0,
|
|
110
107
|
hovered = false,
|
|
111
|
-
): string[] {
|
|
112
|
-
const limit = resolveDiffDisplayLimit(expanded, maxCollapsedLines
|
|
108
|
+
): { lines: string[]; hintLine?: string } {
|
|
109
|
+
const limit = resolveDiffDisplayLimit(expanded, maxCollapsedLines);
|
|
113
110
|
const safeUnprocessed = Math.max(0, unprocessedLogicalRows);
|
|
114
111
|
if (limit === 0 || (rows.length <= limit && safeUnprocessed === 0)) {
|
|
115
|
-
return rows.map((row) => clampDiffLineToWidth(row.text, width));
|
|
112
|
+
return { lines: rows.map((row) => clampDiffLineToWidth(row.text, width)) };
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
const shown = rows.length <= limit ? rows : rows.slice(0, limit);
|
|
119
116
|
const remaining = Math.max(0, rows.length - shown.length) + safeUnprocessed;
|
|
120
117
|
if (remaining === 0) {
|
|
121
|
-
return shown.map((row) => clampDiffLineToWidth(row.text, width));
|
|
118
|
+
return { lines: shown.map((row) => clampDiffLineToWidth(row.text, width)) };
|
|
122
119
|
}
|
|
123
120
|
const visibleHunks = new Set(
|
|
124
121
|
shown
|
|
@@ -137,15 +134,19 @@ export function applyLineLimit(
|
|
|
137
134
|
|
|
138
135
|
const clickLabel = showMoreHintText();
|
|
139
136
|
const clickIndex = hintText.lastIndexOf(clickLabel);
|
|
137
|
+
// 展开态不会再截断,剩下的分支必是折叠态提示。
|
|
140
138
|
const styledHint =
|
|
141
|
-
hovered &&
|
|
139
|
+
hovered && clickIndex >= 0
|
|
142
140
|
? theme.fg("muted", hintText.slice(0, clickIndex)) +
|
|
143
141
|
theme.fg("text", clickLabel) +
|
|
144
142
|
theme.fg("muted", hintText.slice(clickIndex + clickLabel.length))
|
|
145
|
-
: theme.fg(
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
: theme.fg("muted", hintText);
|
|
144
|
+
return {
|
|
145
|
+
lines: [
|
|
146
|
+
...shown.map((row) => clampDiffLineToWidth(row.text, width)),
|
|
147
|
+
renderDiffSpacerLine(width),
|
|
148
|
+
clampDiffLineToWidth(styledHint, width),
|
|
149
|
+
],
|
|
150
|
+
hintLine: hintText,
|
|
151
|
+
};
|
|
151
152
|
}
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
displayConfigCacheKey,
|
|
47
47
|
resolveDiffIndicatorMode,
|
|
48
48
|
resolveLiveDisplayConfig,
|
|
49
|
+
normalizeCollapsedHintLine,
|
|
49
50
|
type DiffRenderOptions,
|
|
50
51
|
type DisplayConfigInput,
|
|
51
52
|
} from "./diff-component.ts";
|
|
@@ -407,6 +408,11 @@ export function renderWriteDiffResult(
|
|
|
407
408
|
|
|
408
409
|
return {
|
|
409
410
|
[RICH_DIFF_COMPONENT]: true,
|
|
411
|
+
/** 鼠标层只在真正的 remainder 行上提供展开入口。 */
|
|
412
|
+
isCollapsedHintLine(plainLine: string): boolean {
|
|
413
|
+
const hint = cache.getHintLine();
|
|
414
|
+
return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
|
|
415
|
+
},
|
|
410
416
|
render(width: number): string[] {
|
|
411
417
|
// Live config: panel can change indicator/wrap/limits after this component is created.
|
|
412
418
|
const live = resolveLiveDisplayConfig(config);
|
|
@@ -433,22 +439,21 @@ export function renderWriteDiffResult(
|
|
|
433
439
|
const writeCollapsedLimit = resolveWriteCollapsedLimit(live);
|
|
434
440
|
const statsOnlyCollapsed = !options.expanded && writeCollapsedLimit === 0;
|
|
435
441
|
if (statsOnlyCollapsed) {
|
|
442
|
+
const hintLines = renderWriteCollapsedHintLine(
|
|
443
|
+
options.fileExistedBeforeWrite === true,
|
|
444
|
+
safeWidth,
|
|
445
|
+
theme,
|
|
446
|
+
hovered,
|
|
447
|
+
options.headerLabel,
|
|
448
|
+
);
|
|
436
449
|
return cache.set(
|
|
437
450
|
safeWidth,
|
|
438
451
|
options.expanded,
|
|
439
452
|
mode,
|
|
440
453
|
configKey,
|
|
441
454
|
hovered,
|
|
442
|
-
clampDiffLinesToWidth(
|
|
443
|
-
|
|
444
|
-
options.fileExistedBeforeWrite === true,
|
|
445
|
-
safeWidth,
|
|
446
|
-
theme,
|
|
447
|
-
hovered,
|
|
448
|
-
options.headerLabel,
|
|
449
|
-
),
|
|
450
|
-
safeWidth,
|
|
451
|
-
),
|
|
455
|
+
clampDiffLinesToWidth(hintLines, safeWidth),
|
|
456
|
+
normalizeCollapsedHintLine(hintLines[0]),
|
|
452
457
|
);
|
|
453
458
|
}
|
|
454
459
|
|
|
@@ -500,11 +505,7 @@ export function renderWriteDiffResult(
|
|
|
500
505
|
}
|
|
501
506
|
|
|
502
507
|
const data = getDetailedData();
|
|
503
|
-
const displayLimit = resolveDiffDisplayLimit(
|
|
504
|
-
options.expanded,
|
|
505
|
-
writeCollapsedLimit,
|
|
506
|
-
live.expandedPreviewMaxLines,
|
|
507
|
-
);
|
|
508
|
+
const displayLimit = resolveDiffDisplayLimit(options.expanded, writeCollapsedLimit);
|
|
508
509
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
509
510
|
const entryBudget = takeEntriesForLineBudget(data.entries, processBudget);
|
|
510
511
|
const splitBudget = takeSplitRowsForBudget(data.splitRows, processBudget);
|
|
@@ -541,7 +542,6 @@ export function renderWriteDiffResult(
|
|
|
541
542
|
safeWidth,
|
|
542
543
|
options.expanded,
|
|
543
544
|
writeCollapsedLimit,
|
|
544
|
-
live.expandedPreviewMaxLines,
|
|
545
545
|
data.hunkCount,
|
|
546
546
|
theme,
|
|
547
547
|
unprocessedLogicalRows,
|
|
@@ -549,9 +549,19 @@ export function renderWriteDiffResult(
|
|
|
549
549
|
);
|
|
550
550
|
const frame = renderDiffFrameLine(safeWidth, theme);
|
|
551
551
|
const renderedLines =
|
|
552
|
-
mode === "unified"
|
|
552
|
+
mode === "unified"
|
|
553
|
+
? [header, frame, ...bodyWithLimit.lines, frame]
|
|
554
|
+
: [header, ...bodyWithLimit.lines];
|
|
553
555
|
const finalLines = clampDiffLinesToWidth(renderedLines, safeWidth);
|
|
554
|
-
return cache.set(
|
|
556
|
+
return cache.set(
|
|
557
|
+
safeWidth,
|
|
558
|
+
options.expanded,
|
|
559
|
+
mode,
|
|
560
|
+
configKey,
|
|
561
|
+
hovered,
|
|
562
|
+
finalLines,
|
|
563
|
+
normalizeCollapsedHintLine(bodyWithLimit.hintLine),
|
|
564
|
+
);
|
|
555
565
|
},
|
|
556
566
|
invalidate: cache.invalidate,
|
|
557
567
|
} as Component;
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
10
10
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
11
|
+
import { mcpToolTitle } from "./mcp-title.ts";
|
|
11
12
|
import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
|
|
12
13
|
import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
|
|
13
14
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
14
15
|
import {
|
|
15
16
|
fitToolCallSummary,
|
|
16
17
|
humanizeToolLabel,
|
|
18
|
+
renderToolSummary,
|
|
17
19
|
toolCallSummary,
|
|
18
20
|
type ToolCallSummary,
|
|
19
21
|
} from "./names.ts";
|
|
@@ -44,6 +46,19 @@ function toolName(tool: any): string {
|
|
|
44
46
|
return String(tool?.toolName ?? tool?.toolDefinition?.name ?? "tool");
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 标题:MCP 工具用 adapter 暴露的真实工具名,其余回退工具名人性化。
|
|
51
|
+
*/
|
|
52
|
+
function toolTitle(tool: any): string {
|
|
53
|
+
const name = toolName(tool);
|
|
54
|
+
return (
|
|
55
|
+
mcpToolTitle({
|
|
56
|
+
toolName: name,
|
|
57
|
+
definition: tool?.toolDefinition ?? tool?.builtInToolDefinition,
|
|
58
|
+
}) ?? humanizeToolLabel(name)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
function isGroupable(value: unknown): boolean {
|
|
48
63
|
return value instanceof ToolExecutionComponent && !NON_GROUPABLE.has(toolName(value));
|
|
49
64
|
}
|
|
@@ -170,6 +185,7 @@ export function paddedBackgroundRow(
|
|
|
170
185
|
|
|
171
186
|
function toolSummary(tool: any): ToolCallSummary {
|
|
172
187
|
return toolCallSummary(toolName(tool), tool?.args ?? {}, {
|
|
188
|
+
title: toolTitle(tool),
|
|
173
189
|
variant: "grouping",
|
|
174
190
|
cwd: tool?.cwd,
|
|
175
191
|
});
|
|
@@ -347,8 +363,7 @@ export class ToolGroupComponent extends Container {
|
|
|
347
363
|
})
|
|
348
364
|
.join(` ${fg("dim", "•")} `);
|
|
349
365
|
const names = new Set(this.children.map(toolName));
|
|
350
|
-
const label =
|
|
351
|
-
names.size === 1 ? humanizeToolLabel(toolName(this.children[0])) : "Multiple Tools";
|
|
366
|
+
const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
|
|
352
367
|
const overall: ToolStatus = counts.error ? "error" : counts.pending ? "pending" : "success";
|
|
353
368
|
if (
|
|
354
369
|
(this.children as any[]).some((tool) => tool?.executionStarted && status(tool) === "pending")
|
|
@@ -398,7 +413,7 @@ export class ToolGroupComponent extends Container {
|
|
|
398
413
|
const mainWidth = Math.max(0, width - visibleWidth(prefix) - visibleWidth(detail));
|
|
399
414
|
lines.push(
|
|
400
415
|
truncateToWidth(
|
|
401
|
-
`${prefix}${
|
|
416
|
+
`${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}`,
|
|
402
417
|
width,
|
|
403
418
|
"",
|
|
404
419
|
),
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { humanizeToolLabel } from "./names.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP 工具的识别与标题,单卡(default-mode)与分组行/分组头(grouping)共用。
|
|
5
|
+
*
|
|
6
|
+
* - 判定:工具名带 mcp 片段(`mcp` 网关、`mcpScript`、`mcp__<server>`),或 adapter 注册时
|
|
7
|
+
* 给的 label 以 `MCP:` 开头。默认/none/short 前缀模式下直连工具的名字里没有 mcp 片段
|
|
8
|
+
* (`github_search_code`、`search_code`),只能靠 label 判定。
|
|
9
|
+
* - 标题:具体工具用 adapter 暴露的真实工具名,不做二次格式化;只有 `mcp` / `mcpScript`
|
|
10
|
+
* 这两个入口没有具体工具名,跟普通工具一样人性化(MCP / MCP Script)。
|
|
11
|
+
* - 参数摘要与其它工具共用 toolCallSummary 的字段链,网关额外把 `args.tool` 提到标题后。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 单次 MCP 调用的识别信息。 */
|
|
15
|
+
export type McpToolCall = {
|
|
16
|
+
toolName: string;
|
|
17
|
+
/** 工具定义;默认/none/short 前缀模式要靠 label 判定,会话重放时可能缺失。 */
|
|
18
|
+
definition?: any;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const GATEWAY_TOOL_NAME = "mcp";
|
|
22
|
+
const SCRIPT_TOOL_NAME = "mcpScript";
|
|
23
|
+
|
|
24
|
+
export function isMcpToolDefinition(definition: any, toolName: string): boolean {
|
|
25
|
+
const label = typeof definition?.label === "string" ? definition.label.trim() : "";
|
|
26
|
+
if (/^MCP(?::|$)/i.test(label)) return true;
|
|
27
|
+
if (toolName === GATEWAY_TOOL_NAME || toolName === SCRIPT_TOOL_NAME) return true;
|
|
28
|
+
return /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 标题:MCP 工具返回标题(入口走人性化,其余用真实工具名);非 MCP 返回 undefined。 */
|
|
32
|
+
export function mcpToolTitle(call: McpToolCall): string | undefined {
|
|
33
|
+
const { toolName } = call;
|
|
34
|
+
if (!isMcpToolDefinition(call.definition, toolName)) return undefined;
|
|
35
|
+
if (toolName === GATEWAY_TOOL_NAME || toolName === SCRIPT_TOOL_NAME) {
|
|
36
|
+
return humanizeToolLabel(toolName);
|
|
37
|
+
}
|
|
38
|
+
return toolName;
|
|
39
|
+
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { posix, win32 } from "node:path";
|
|
2
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
4
|
import { config } from "../../config/config.ts";
|
|
4
5
|
import { oneLine } from "../../utils/format.ts";
|
|
6
|
+
import { headTruncateToWidth } from "./result.ts";
|
|
5
7
|
|
|
6
8
|
function clip(value: unknown): string {
|
|
7
9
|
return oneLine(value, config.inputClip);
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
/** 载荷(入参 JSON / 脚本代码)至少留出这么多宽度才显示,否则整段省略(避免只剩一个孤零零的 " {…")。 */
|
|
13
|
+
const MIN_PAYLOAD_WIDTH = 10;
|
|
14
|
+
|
|
10
15
|
/** 品牌大小写固定写法;humanize 推导不出来的工具名写在这里。 */
|
|
11
16
|
const TOOL_LABEL_OVERRIDES: Readonly<Record<string, string>> = {
|
|
12
17
|
powershell: "PowerShell",
|
|
18
|
+
// MCP 入口两个没有具体工具名,跟普通工具一样人性化;缩写固定大写
|
|
19
|
+
mcp: "MCP",
|
|
20
|
+
mcpscript: "MCP Script",
|
|
13
21
|
};
|
|
14
22
|
|
|
15
23
|
/**
|
|
@@ -49,6 +57,8 @@ export type ToolCallSummary = {
|
|
|
49
57
|
detail: string;
|
|
50
58
|
/** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
|
|
51
59
|
path?: { prefix: string; value: string };
|
|
60
|
+
/** 标题之外的原始载荷(完整入参 JSON、脚本代码…),已压成单行,渲染时用 dim 接在标题后。 */
|
|
61
|
+
payload?: string;
|
|
52
62
|
};
|
|
53
63
|
|
|
54
64
|
function pathApi(value: string) {
|
|
@@ -229,6 +239,18 @@ export function toolCallSummary(
|
|
|
229
239
|
const variant = opts.variant ?? "default";
|
|
230
240
|
if (!args || typeof args !== "object") return { main: title, detail: "" };
|
|
231
241
|
const name = toolName.toLowerCase();
|
|
242
|
+
|
|
243
|
+
// MCP 入口沿用 mcp-adapter 自己的标题风格(adapter 的 formatMcpProxyToolCallLines):
|
|
244
|
+
// `MCP <动作> <目标>` / `MCP Script <代码>`,比把整包参数摊成 JSON 好读
|
|
245
|
+
if (name === "mcp") {
|
|
246
|
+
const gateway = mcpGatewaySummary(title, args);
|
|
247
|
+
if (gateway) return gateway;
|
|
248
|
+
}
|
|
249
|
+
if (name === "mcpscript") {
|
|
250
|
+
const code = typeof args.code === "string" && args.code ? clip(args.code) : "";
|
|
251
|
+
return code ? { main: title, detail: "", payload: code } : { main: title, detail: "" };
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
const value = (fallback: string, ...keys: string[]) => {
|
|
233
255
|
const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
|
|
234
256
|
return `${title} ${clip(found || fallback)}`;
|
|
@@ -315,5 +337,85 @@ export function toolCallSummary(
|
|
|
315
337
|
if (typeof preferredPath === "string" && preferredPath) {
|
|
316
338
|
return pathSummary(title, preferredPath, opts.cwd);
|
|
317
339
|
}
|
|
318
|
-
|
|
340
|
+
|
|
341
|
+
// 字段链认不出的参数(命名空间代理的 tool+args、第三方工具的自定义键…)展开完整入参 JSON,
|
|
342
|
+
// 而不是只剩一个标题;网关与脚本的专属形状在上面处理
|
|
343
|
+
const payload = jsonArgs(args);
|
|
344
|
+
return payload ? { main: title, detail: "", payload } : { main: title, detail: "" };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* MCP 网关注解 `mcp <动作> <目标>`:动作与目标照搬 mcp-adapter 的 formatMcpProxyToolCallLines,
|
|
349
|
+
* 人眼读起来比参数 JSON 快;`instructions` 是 adapter 自己漏掉的一档(它那里会落到 mcp status)。
|
|
350
|
+
* 认不出的入参返回 undefined,交给通用 JSON 回退,不猜成 status。
|
|
351
|
+
*/
|
|
352
|
+
function mcpGatewaySummary(title: string, args: any): ToolCallSummary | undefined {
|
|
353
|
+
const text = (value: unknown) => (typeof value === "string" && value ? value : "");
|
|
354
|
+
const tool = text(args.tool);
|
|
355
|
+
const connect = text(args.connect);
|
|
356
|
+
const describe = text(args.describe);
|
|
357
|
+
const instructions = text(args.instructions);
|
|
358
|
+
const search = text(args.search);
|
|
359
|
+
const server = text(args.server);
|
|
360
|
+
const action = text(args.action);
|
|
361
|
+
const scoped = (target: string) => (server ? `${target} @ ${server}` : target);
|
|
362
|
+
|
|
363
|
+
if (action === "ui-messages") return { main: `${title} ${action}`, detail: "" };
|
|
364
|
+
if (tool) {
|
|
365
|
+
const main = `${title} call ${scoped(tool)}`;
|
|
366
|
+
const payload = innerArgsPayload(args.args);
|
|
367
|
+
return payload ? { main, detail: "", payload } : { main, detail: "" };
|
|
368
|
+
}
|
|
369
|
+
if (connect) return { main: `${title} connect ${connect}`, detail: "" };
|
|
370
|
+
if (describe) return { main: `${title} describe ${scoped(describe)}`, detail: "" };
|
|
371
|
+
if (instructions) return { main: `${title} instructions ${instructions}`, detail: "" };
|
|
372
|
+
if (search) {
|
|
373
|
+
const flags = [
|
|
374
|
+
args.regex === true ? "regex" : "",
|
|
375
|
+
args.includeSchemas === false ? "schemas hidden" : "",
|
|
376
|
+
].filter(Boolean);
|
|
377
|
+
return {
|
|
378
|
+
main: `${title} search ${scoped(search)}`,
|
|
379
|
+
detail: flags.length ? ` (${flags.join(", ")})` : "",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (server) return { main: `${title} list ${server}`, detail: "" };
|
|
383
|
+
if (action) return { main: `${title} ${action}`, detail: "" };
|
|
384
|
+
if (Object.keys(args).length === 0) return { main: `${title} status`, detail: "" };
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 网关 call 的内层工具入参:对象转单行 JSON;网关允许传 JSON 字符串,就原样取用不再转义。 */
|
|
389
|
+
function innerArgsPayload(value: unknown): string {
|
|
390
|
+
if (typeof value === "string") return value ? clip(value) : "";
|
|
391
|
+
if (!value || typeof value !== "object") return "";
|
|
392
|
+
return jsonArgs(value);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** 入参序列化成单行 JSON;空对象或不可序列化时返回 ""。 */
|
|
396
|
+
function jsonArgs(args: any): string {
|
|
397
|
+
try {
|
|
398
|
+
const json = JSON.stringify(args);
|
|
399
|
+
return json && json !== "{}" ? clip(json) : "";
|
|
400
|
+
} catch {
|
|
401
|
+
return "";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* 标题行内容:标题用调用方的 toolTitle 色,原始载荷(入参 JSON / 脚本代码)单独用 dim。
|
|
407
|
+
* 标题优先占宽(它是身份),剩下的宽度给载荷;载荷超宽时尾部省略,连
|
|
408
|
+
* MIN_PAYLOAD_WIDTH 都放不下时整段省略。
|
|
409
|
+
*/
|
|
410
|
+
export function renderToolSummary(
|
|
411
|
+
summary: ToolCallSummary,
|
|
412
|
+
width: number,
|
|
413
|
+
fg: (color: ThemeColor, text: string) => string,
|
|
414
|
+
): string {
|
|
415
|
+
const title = fitToolCallSummary(summary, width);
|
|
416
|
+
if (!summary.payload) return fg("toolTitle", title);
|
|
417
|
+
const room = width - visibleWidth(title);
|
|
418
|
+
return room >= MIN_PAYLOAD_WIDTH
|
|
419
|
+
? `${fg("toolTitle", title)}${fg("dim", headTruncateToWidth(` ${summary.payload}`, room))}`
|
|
420
|
+
: fg("toolTitle", title);
|
|
319
421
|
}
|
|
@@ -14,7 +14,7 @@ export function toolViewportWidth(width: number): number {
|
|
|
14
14
|
|
|
15
15
|
/** 与默认工具结果相同的一级缩进包装。子组件只扣始终加上的 1 列;↳ 行多出的 2 列由 truncateToWidth 吃掉。 */
|
|
16
16
|
export function insetComponent(component: any): any {
|
|
17
|
-
|
|
17
|
+
const wrapped: Record<string, unknown> = {
|
|
18
18
|
render: (width: number) =>
|
|
19
19
|
component.render(Math.max(1, width - 1)).map((line: string) => {
|
|
20
20
|
const nestedMarker = line.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)↳/, "$1 ↳");
|
|
@@ -22,6 +22,11 @@ export function insetComponent(component: any): any {
|
|
|
22
22
|
}),
|
|
23
23
|
invalidate: () => component.invalidate?.(),
|
|
24
24
|
};
|
|
25
|
+
// 内层 diff 声明的 remainder 行:鼠标层只能看到这个包装组件。
|
|
26
|
+
if (typeof component.isCollapsedHintLine === "function") {
|
|
27
|
+
wrapped.isCollapsedHintLine = (line: string) => component.isCollapsedHintLine(line);
|
|
28
|
+
}
|
|
29
|
+
return wrapped;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
function rawTextFromResult(result: any): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Claude Code-style TUI output with some personal touches and handy utilities.",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"./themes/cc-light.json"
|
|
35
35
|
],
|
|
36
36
|
"image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/preview.webp",
|
|
37
|
-
"video": "https://github.com/
|
|
37
|
+
"video": "https://github.com/minuque/pi-cc-extensions/releases/download/v0.9.4/demo.mp4"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"test": "node --test tests/**/*.test.ts",
|