@herbertgao/pi-extensions 2026.10.4 → 2026.10.6
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.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +72 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +262 -48
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-currency.ts +29 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +96 -59
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/git-stats.ts +162 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +204 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +51 -86
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +212 -74
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +50 -362
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +35 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +28 -26
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +450 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +173 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +5 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +36 -10
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +140 -51
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +44 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -803
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +9 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { inspect } from "node:util";
|
|
3
3
|
import { config } from "../../config/config.ts";
|
|
4
|
-
import { createCodeBlockHighlighter } from "./diff/diff-highlight.ts";
|
|
5
|
-
import { showMoreHintText } from "./show-more-hint.ts";
|
|
6
|
-
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
7
|
-
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
8
4
|
import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
|
|
5
|
+
import { oneLine } from "../../utils/format.ts";
|
|
9
6
|
|
|
10
7
|
const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
|
|
11
8
|
/** 宽屏右侧留白上限:比例留白超过这么多列时改用固定留白。 */
|
|
@@ -75,581 +72,45 @@ export function countLines(text: string): number {
|
|
|
75
72
|
.filter((line) => line.trim().length > 0).length;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
function hasExpandableResult(text: string): boolean {
|
|
79
|
-
return countLines(text) > 1;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const activeAnimationContexts = new Set<any>();
|
|
83
|
-
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
84
|
-
|
|
85
|
-
function clearAnimation(context: any) {
|
|
86
|
-
if (context?.state) context.state.ccstyleAnimationLight = false;
|
|
87
|
-
if (!context?.state?.ccstyleAnimationScheduled) return;
|
|
88
|
-
context.state.ccstyleAnimationScheduled = false;
|
|
89
|
-
activeAnimationContexts.delete(context);
|
|
90
|
-
if (activeAnimationContexts.size === 0 && sharedAnimationTimer) {
|
|
91
|
-
clearTimeout(sharedAnimationTimer);
|
|
92
|
-
sharedAnimationTimer = null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function clearAllAnimations() {
|
|
97
|
-
for (const ctx of activeAnimationContexts) {
|
|
98
|
-
ctx.state.ccstyleAnimationScheduled = false;
|
|
99
|
-
ctx.state.ccstyleAnimationLight = false;
|
|
100
|
-
}
|
|
101
|
-
activeAnimationContexts.clear();
|
|
102
|
-
if (sharedAnimationTimer) {
|
|
103
|
-
clearTimeout(sharedAnimationTimer);
|
|
104
|
-
sharedAnimationTimer = null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function scheduleAnimation(
|
|
109
|
-
context: any,
|
|
110
|
-
options: { light?: boolean; intervalMs?: number } = {},
|
|
111
|
-
) {
|
|
112
|
-
const state = (context.state ??= {});
|
|
113
|
-
// light:调用方自己在 render() 内重取 loading 帧,定时器只需请求重绘;
|
|
114
|
-
// 否则定时器走 context.invalidate()(compact 摘要等靠它重建)。
|
|
115
|
-
// 每次调用都按本次 options 写入,避免上次 light 残留后非 light 调用仍跳过 invalidate。
|
|
116
|
-
state.ccstyleAnimationLight = Boolean(options.light);
|
|
117
|
-
if (state.ccstyleAnimationScheduled) return;
|
|
118
|
-
state.ccstyleAnimationScheduled = true;
|
|
119
|
-
activeAnimationContexts.add(context);
|
|
120
|
-
if (!sharedAnimationTimer) {
|
|
121
|
-
sharedAnimationTimer = setTimeout(() => {
|
|
122
|
-
sharedAnimationTimer = null;
|
|
123
|
-
const contexts = Array.from(activeAnimationContexts);
|
|
124
|
-
activeAnimationContexts.clear();
|
|
125
|
-
const tui = getToolMouseTui();
|
|
126
|
-
const canRequestRender = typeof tui?.requestRender === "function";
|
|
127
|
-
let lightFrames = false;
|
|
128
|
-
for (const ctx of contexts) {
|
|
129
|
-
ctx.state.ccstyleAnimationScheduled = false;
|
|
130
|
-
if (ctx.state.ccstyleAnimationLight && canRequestRender) lightFrames = true;
|
|
131
|
-
else ctx.invalidate?.();
|
|
132
|
-
}
|
|
133
|
-
if (lightFrames) tui.requestRender();
|
|
134
|
-
}, options.intervalMs ?? TOOL_LOADING_INTERVAL_MS);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function pendingIcon(_name: string): string {
|
|
139
|
-
return toolLoadingIcon();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
type ToolVisualState = "pending" | "success" | "error";
|
|
143
|
-
|
|
144
|
-
export function settledIcon(name: string, state: ToolVisualState | undefined): string {
|
|
145
|
-
if (state === "success") return "✓";
|
|
146
|
-
if (state === "error") return "✗";
|
|
147
|
-
// toolIcon(name) 曾恒返回 "●",已内联;name 保留以稳定导出签名。
|
|
148
|
-
return "●";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function setToolVisualState(context: any, visualState: ToolVisualState) {
|
|
152
|
-
const state = (context.state ??= {});
|
|
153
|
-
if (visualState !== "pending") clearAnimation(context);
|
|
154
|
-
if (state.ccstyleToolVisualState === visualState) return;
|
|
155
|
-
state.ccstyleToolVisualState = visualState;
|
|
156
|
-
// Do not invalidate synchronously from renderResult. Pi is already rendering
|
|
157
|
-
// this tool row; recursively scheduling another render here can retain both
|
|
158
|
-
// the finalized result component and its previous secondary/partial component,
|
|
159
|
-
// which displays the result summary twice. The current render pass also
|
|
160
|
-
// refreshes renderCall, so the settled icon still updates immediately.
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function getToolVisualState(context: any): ToolVisualState | undefined {
|
|
164
|
-
return context?.state?.ccstyleToolVisualState as ToolVisualState | undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function resolveToolVisualState(context: any): ToolVisualState | undefined {
|
|
168
|
-
const visualState = getToolVisualState(context);
|
|
169
|
-
if (visualState || context?.isPartial !== false) return visualState;
|
|
170
|
-
const settledState: ToolVisualState = context?.isError ? "error" : "success";
|
|
171
|
-
setToolVisualState(context, settledState);
|
|
172
|
-
return settledState;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function toolIconColor(context: any): "accent" | "error" | "success" | "muted" {
|
|
176
|
-
const visualState = getToolVisualState(context);
|
|
177
|
-
if (context?.isError || visualState === "error") return "error";
|
|
178
|
-
if (visualState === "success") return "success";
|
|
179
|
-
if (context?.isPartial || context?.executionStarted || visualState === "pending") return "accent";
|
|
180
|
-
return "muted";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function isToolExpanded(options: any, context: any): boolean {
|
|
184
|
-
const local = context?.state?.ccstyleToolExpanded;
|
|
185
|
-
return typeof local === "boolean" ? local : Boolean(options?.expanded ?? context?.expanded);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Keep the guide aligned when long result lines wrap at the viewport edge. */
|
|
189
|
-
export class ExpandedToolResultText {
|
|
190
|
-
private text: string;
|
|
191
|
-
private prefix: string;
|
|
192
|
-
private normalizedText: string;
|
|
193
|
-
private cachedWidth: number | undefined;
|
|
194
|
-
private cachedLines: string[] | undefined;
|
|
195
|
-
|
|
196
|
-
constructor(text: string, prefix: string) {
|
|
197
|
-
this.text = text;
|
|
198
|
-
this.prefix = prefix;
|
|
199
|
-
this.normalizedText = text.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
setText(text: string): void {
|
|
203
|
-
if (this.text === text) return;
|
|
204
|
-
this.text = text;
|
|
205
|
-
this.normalizedText = text.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
206
|
-
this.invalidate();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
setPrefix(prefix: string): void {
|
|
210
|
-
if (this.prefix === prefix) return;
|
|
211
|
-
this.prefix = prefix;
|
|
212
|
-
this.invalidate();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
render(width: number): string[] {
|
|
216
|
-
if (this.cachedLines !== undefined && this.cachedWidth === width) return this.cachedLines;
|
|
217
|
-
|
|
218
|
-
const prefixWidth = visibleWidth(this.prefix);
|
|
219
|
-
const contentWidth = Math.max(1, width - prefixWidth);
|
|
220
|
-
const lines = wrapTextWithAnsi(this.normalizedText, contentWidth).map((line) =>
|
|
221
|
-
truncateToWidth(this.prefix + line, width, ""),
|
|
222
|
-
);
|
|
223
|
-
this.cachedWidth = width;
|
|
224
|
-
this.cachedLines = lines;
|
|
225
|
-
return lines;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
invalidate(): void {
|
|
229
|
-
this.cachedWidth = undefined;
|
|
230
|
-
this.cachedLines = undefined;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** 截断体末行 `… +N more lines` 旁的展开提示,点击打开全量预览。 */
|
|
235
|
-
export const SHOW_MORE_LABEL = "· click to show more";
|
|
236
|
-
|
|
237
|
-
export type ToolIoSection = "input" | "output";
|
|
238
|
-
|
|
239
|
-
// Module-local token: /reload creates a fresh token, so stale view instances are
|
|
240
|
-
// replaced instead of retaining their old render implementation.
|
|
241
|
-
const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view");
|
|
242
|
-
|
|
243
75
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* Visual frame:
|
|
247
|
-
* ├ Input click to show more
|
|
248
|
-
* │ path: src/a.ts
|
|
249
|
-
* │
|
|
250
|
-
* └ Output click to show more
|
|
251
|
-
* result line…
|
|
252
|
-
*
|
|
253
|
-
* flushLeft=true(仅 mode=on 展开卡):去掉树线前导空格,由外层 Box(1,1) 提供 1 格 padding。
|
|
254
|
-
*
|
|
255
|
-
* Reused across re-renders via context.lastComponent when possible.
|
|
76
|
+
* 折叠行的结果文案:`N lines returned`(read 说 loaded)、`Done`、失败时首行错误。
|
|
77
|
+
* 单卡折叠行与工具组的子行共用这一处口径;TaskList 的树形摘要不走这里,由调用方先判。
|
|
256
78
|
*/
|
|
257
|
-
export
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
/** Input 里的代码块(codemode 的 `code`):着色行覆盖默认 dim/muted 样式。 */
|
|
273
|
-
private inputCode?:
|
|
274
|
-
| {
|
|
275
|
-
startLine: number;
|
|
276
|
-
language: string;
|
|
277
|
-
standalone: boolean;
|
|
278
|
-
code: string;
|
|
279
|
-
theme: unknown;
|
|
280
|
-
rows: () => string[];
|
|
281
|
-
}
|
|
282
|
-
| undefined;
|
|
283
|
-
|
|
284
|
-
/** flushLeft:贴左渲染(mode=on 展开卡);默认 false 保留前导空格(compact 等共用路径)。 */
|
|
285
|
-
private flushLeft: boolean;
|
|
286
|
-
|
|
287
|
-
constructor(
|
|
288
|
-
theme: any,
|
|
289
|
-
inputBody: string,
|
|
290
|
-
outputBody: string,
|
|
291
|
-
isError: boolean,
|
|
292
|
-
maxOutputLines = config.expandedOutputMaxLines,
|
|
293
|
-
maxInputLines = config.expandedInputMaxLines,
|
|
294
|
-
flushLeft = false,
|
|
295
|
-
) {
|
|
296
|
-
this.theme = theme;
|
|
297
|
-
this.inputBody = inputBody;
|
|
298
|
-
this.outputBody = outputBody;
|
|
299
|
-
this.isError = isError;
|
|
300
|
-
this.maxOutputLines = Math.max(1, maxOutputLines);
|
|
301
|
-
this.maxInputLines = Math.max(1, maxInputLines);
|
|
302
|
-
this.flushLeft = flushLeft;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
setContent(
|
|
306
|
-
inputBody: string,
|
|
307
|
-
outputBody: string,
|
|
308
|
-
isError: boolean,
|
|
309
|
-
maxOutputLines?: number,
|
|
310
|
-
maxInputLines?: number,
|
|
311
|
-
flushLeft?: boolean,
|
|
312
|
-
): void {
|
|
313
|
-
const nextOut =
|
|
314
|
-
maxOutputLines !== undefined ? Math.max(1, maxOutputLines) : this.maxOutputLines;
|
|
315
|
-
const nextIn = maxInputLines !== undefined ? Math.max(1, maxInputLines) : this.maxInputLines;
|
|
316
|
-
const nextFlush = flushLeft !== undefined ? flushLeft : this.flushLeft;
|
|
317
|
-
if (
|
|
318
|
-
this.inputBody === inputBody &&
|
|
319
|
-
this.outputBody === outputBody &&
|
|
320
|
-
this.isError === isError &&
|
|
321
|
-
this.maxOutputLines === nextOut &&
|
|
322
|
-
this.maxInputLines === nextIn &&
|
|
323
|
-
this.flushLeft === nextFlush
|
|
324
|
-
) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
this.inputBody = inputBody;
|
|
328
|
-
this.outputBody = outputBody;
|
|
329
|
-
this.isError = isError;
|
|
330
|
-
this.maxOutputLines = nextOut;
|
|
331
|
-
this.maxInputLines = nextIn;
|
|
332
|
-
this.flushLeft = nextFlush;
|
|
333
|
-
this.invalidate();
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
getInputBody(): string {
|
|
337
|
-
return this.inputBody;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* 登记 Input 里的代码块:同步 cli-highlight 打底,shiki 异步升级,落地后
|
|
342
|
-
* 经 invalidate 重画。同一份代码与主题只建一次(视图按宽度缓存行)。
|
|
343
|
-
*/
|
|
344
|
-
setInputCode(block: InputCodeBlock | undefined): void {
|
|
345
|
-
if (!block) {
|
|
346
|
-
if (this.inputCode) {
|
|
347
|
-
this.inputCode = undefined;
|
|
348
|
-
this.invalidate();
|
|
349
|
-
}
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (this.inputCode?.code === block.code && this.inputCode.theme === this.theme) {
|
|
353
|
-
this.inputCode.startLine = block.startLine;
|
|
354
|
-
this.inputCode.standalone = block.standalone;
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
this.inputCode = {
|
|
358
|
-
startLine: block.startLine,
|
|
359
|
-
language: block.language,
|
|
360
|
-
standalone: block.standalone,
|
|
361
|
-
code: block.code,
|
|
362
|
-
theme: this.theme,
|
|
363
|
-
rows: createCodeBlockHighlighter(block.code, block.language, this.theme, () => {
|
|
364
|
-
// 视图自己的行缓存 + 外层工具卡(清 paint 缓存并 requestRender)
|
|
365
|
-
this.invalidate();
|
|
366
|
-
invalidateIoView(this);
|
|
367
|
-
}),
|
|
368
|
-
};
|
|
369
|
-
this.invalidate();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* 整段 Input 就是代码块时给出语言与代码,供全量预览包一层围栏(弹框按 Markdown
|
|
374
|
-
* 渲染,围栏代码块由 pi 的 highlightCode 着色)。混了别的字段时返回 undefined。
|
|
375
|
-
*/
|
|
376
|
-
getInputCodeFence(): { language: string; code: string } | undefined {
|
|
377
|
-
const block = this.inputCode;
|
|
378
|
-
return block?.standalone ? { language: block.language, code: block.code } : undefined;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** 代码块里某一源行的着色内容(已剥掉行首缩进);不在块内时为 undefined。 */
|
|
382
|
-
private inputCodeLine(sourceIndex: number): string | undefined {
|
|
383
|
-
const block = this.inputCode;
|
|
384
|
-
if (!block) return undefined;
|
|
385
|
-
const offset = sourceIndex - block.startLine;
|
|
386
|
-
if (offset < 0) return undefined;
|
|
387
|
-
const rows = block.rows();
|
|
388
|
-
return offset < rows.length ? rows[offset] : undefined;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
getOutputBody(): string {
|
|
392
|
-
return this.outputBody.trim() ? this.outputBody : "Done";
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
setHoveredSection(section: ToolIoSection | null): void {
|
|
396
|
-
if (this.hoveredSection === section) return;
|
|
397
|
-
this.hoveredSection = section;
|
|
398
|
-
this.invalidate();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
getHoveredSection(): ToolIoSection | null {
|
|
402
|
-
return this.hoveredSection;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
|
|
406
|
-
matchShowMoreLine(plainLine: string): ToolIoSection | null {
|
|
407
|
-
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
408
|
-
if (!line.includes(` · ${showMoreHintText()}`) || !/\+\d+ more lines/.test(line)) return null;
|
|
409
|
-
if (this.truncated.input && line.includes("│")) return "input";
|
|
410
|
-
if (this.truncated.output) return "output";
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/** Precise header rows marked for show-more hit testing (last render). */
|
|
415
|
-
showMoreHeaderLineIndexes(): ReadonlyArray<{ section: ToolIoSection; line: number }> {
|
|
416
|
-
const out: Array<{ section: ToolIoSection; line: number }> = [];
|
|
417
|
-
if (this.showMoreHeaderRows.input !== undefined) {
|
|
418
|
-
out.push({ section: "input", line: this.showMoreHeaderRows.input });
|
|
419
|
-
}
|
|
420
|
-
if (this.showMoreHeaderRows.output !== undefined) {
|
|
421
|
-
out.push({ section: "output", line: this.showMoreHeaderRows.output });
|
|
422
|
-
}
|
|
423
|
-
return out;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** Column range (1-based, visible cells) of show-more on a rendered header, if present. */
|
|
427
|
-
showMoreHitbox(plainLine: string): { startCol: number; endCol: number } | null {
|
|
428
|
-
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
429
|
-
const label = ` · ${showMoreHintText()}`;
|
|
430
|
-
const idx = line.lastIndexOf(label);
|
|
431
|
-
if (idx < 0) return null;
|
|
432
|
-
const before = line.slice(0, idx);
|
|
433
|
-
const startCol = visibleWidth(before) + 1;
|
|
434
|
-
const endCol = startCol + visibleWidth(label) - 1;
|
|
435
|
-
return { startCol, endCol };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
render(width: number): string[] {
|
|
439
|
-
if (this.cachedLines !== undefined && this.cachedWidth === width) {
|
|
440
|
-
return withIoViewMarkers(this, this.cachedLines);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const theme = this.theme;
|
|
444
|
-
const safeWidth = Math.max(1, Math.floor(width));
|
|
445
|
-
// flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1 格前导空格(compact 共用)
|
|
446
|
-
const lead = this.flushLeft ? "" : " ";
|
|
447
|
-
const rail = `${lead}│ `;
|
|
448
|
-
const railWidth = visibleWidth(rail);
|
|
449
|
-
const bodyWidth = toolViewportWidth(safeWidth);
|
|
450
|
-
const contentWidth = Math.max(1, bodyWidth - railWidth);
|
|
451
|
-
const bodyColor = this.isError ? "error" : "toolOutput";
|
|
452
|
-
const lines: string[] = [];
|
|
453
|
-
this.truncated = { input: false, output: false };
|
|
454
|
-
this.showMoreHeaderRows = {};
|
|
455
|
-
|
|
456
|
-
const pushHeader = (corner: "├" | "└", label: string) => {
|
|
457
|
-
const mark = theme.fg("dim", `${lead}${corner} `);
|
|
458
|
-
const title = theme.fg(
|
|
459
|
-
"accent",
|
|
460
|
-
typeof theme.bold === "function" ? theme.bold(label) : label,
|
|
461
|
-
);
|
|
462
|
-
lines.push(truncateToWidth(mark + title, safeWidth, ""));
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
const pushRailLine = (styledContent: string, continued = true) => {
|
|
466
|
-
// 续行 rail;Output 正文相对 └ 缩进 2 格(+ 可选 lead)
|
|
467
|
-
const prefix = continued ? rail : `${lead} `;
|
|
468
|
-
lines.push(truncateToWidth(theme.fg("dim", prefix) + styledContent, safeWidth, ""));
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const pushBlankRail = () => {
|
|
472
|
-
lines.push(truncateToWidth(theme.fg("dim", `${lead}│`), safeWidth, ""));
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
const pushBody = (
|
|
476
|
-
body: string,
|
|
477
|
-
opts: { input?: boolean; limit: number; continued?: boolean; section: ToolIoSection },
|
|
478
|
-
): boolean /* truncated */ => {
|
|
479
|
-
const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
480
|
-
if (!raw.trim()) {
|
|
481
|
-
pushRailLine(theme.fg("dim", "(empty)"), opts.continued);
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
const sourceLines = raw.split("\n");
|
|
485
|
-
const wrapped: string[] = [];
|
|
486
|
-
for (const [index, source] of sourceLines.entries()) {
|
|
487
|
-
if (opts.input) {
|
|
488
|
-
// 代码块的行用着色版本(已剥缩进),其余走默认 dim/muted
|
|
489
|
-
wrapped.push(...wrapInputLine(source, contentWidth, theme, this.inputCodeLine(index)));
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
const styled = theme.fg(bodyColor, source);
|
|
493
|
-
const parts = wrapTextWithAnsi(styled, contentWidth);
|
|
494
|
-
if (parts.length === 0) wrapped.push(styled);
|
|
495
|
-
else wrapped.push(...parts);
|
|
496
|
-
}
|
|
497
|
-
// Prefer source-line count so plain multi-line dumps always cap, even when
|
|
498
|
-
// theme/wrap measurements disagree slightly.
|
|
499
|
-
const truncated = wrapped.length > opts.limit || sourceLines.length > opts.limit;
|
|
500
|
-
const visible = truncated ? wrapped.slice(0, Math.min(opts.limit, wrapped.length)) : wrapped;
|
|
501
|
-
for (const line of visible) pushRailLine(line, opts.continued);
|
|
502
|
-
if (truncated) {
|
|
503
|
-
const hidden = Math.max(0, wrapped.length - visible.length);
|
|
504
|
-
if (hidden > 0) {
|
|
505
|
-
// hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
|
|
506
|
-
const more =
|
|
507
|
-
theme.fg("dim", " ·") +
|
|
508
|
-
theme.fg(
|
|
509
|
-
this.hoveredSection === opts.section ? "text" : "dim",
|
|
510
|
-
` ${showMoreHintText()}`,
|
|
511
|
-
);
|
|
512
|
-
pushRailLine(theme.fg("dim", `… +${hidden} more lines`) + more, opts.continued);
|
|
513
|
-
this.showMoreHeaderRows[opts.section] = lines.length - 1;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return truncated;
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
const hasInput = this.inputBody.trim().length > 0;
|
|
520
|
-
const outputText = this.getOutputBody();
|
|
521
|
-
|
|
522
|
-
// Decide show-more from the same truncation rules as pushBody.
|
|
523
|
-
const inputWouldTruncate =
|
|
524
|
-
hasInput &&
|
|
525
|
-
bodyExceedsLineLimit(
|
|
526
|
-
this.inputBody,
|
|
527
|
-
this.maxInputLines,
|
|
528
|
-
contentWidth,
|
|
529
|
-
true,
|
|
530
|
-
theme,
|
|
531
|
-
bodyColor,
|
|
532
|
-
(inputIndex: number) => this.inputCodeLine(inputIndex),
|
|
533
|
-
);
|
|
534
|
-
const outputWouldTruncate = bodyExceedsLineLimit(
|
|
535
|
-
outputText,
|
|
536
|
-
this.maxOutputLines,
|
|
537
|
-
contentWidth,
|
|
538
|
-
false,
|
|
539
|
-
theme,
|
|
540
|
-
bodyColor,
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
if (hasInput) {
|
|
544
|
-
this.truncated.input = inputWouldTruncate;
|
|
545
|
-
pushHeader("├", "Input");
|
|
546
|
-
pushBody(this.inputBody, {
|
|
547
|
-
input: true,
|
|
548
|
-
limit: this.maxInputLines,
|
|
549
|
-
continued: true,
|
|
550
|
-
section: "input",
|
|
551
|
-
});
|
|
552
|
-
pushBlankRail();
|
|
553
|
-
this.truncated.output = outputWouldTruncate;
|
|
554
|
-
pushHeader("└", "Output");
|
|
555
|
-
pushBody(outputText, {
|
|
556
|
-
limit: this.maxOutputLines,
|
|
557
|
-
continued: false,
|
|
558
|
-
section: "output",
|
|
559
|
-
});
|
|
560
|
-
} else {
|
|
561
|
-
this.truncated.output = outputWouldTruncate;
|
|
562
|
-
pushHeader("└", "Output");
|
|
563
|
-
pushBody(outputText, {
|
|
564
|
-
limit: this.maxOutputLines,
|
|
565
|
-
continued: false,
|
|
566
|
-
section: "output",
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
this.cachedWidth = width;
|
|
571
|
-
this.cachedLines = lines;
|
|
572
|
-
return withIoViewMarkers(this, lines);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
invalidate(): void {
|
|
576
|
-
this.cachedWidth = undefined;
|
|
577
|
-
this.cachedLines = undefined;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export function isExpandedToolIoView(value: unknown): value is ExpandedToolIoView {
|
|
582
|
-
return Boolean(
|
|
583
|
-
value &&
|
|
584
|
-
typeof value === "object" &&
|
|
585
|
-
(value as ExpandedToolIoView)[EXPANDED_TOOL_IO_VIEW_GENERATION] === true &&
|
|
586
|
-
typeof (value as ExpandedToolIoView).getInputBody === "function" &&
|
|
587
|
-
typeof (value as ExpandedToolIoView).getOutputBody === "function" &&
|
|
588
|
-
typeof (value as ExpandedToolIoView).setHoveredSection === "function" &&
|
|
589
|
-
typeof (value as ExpandedToolIoView).render === "function",
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/** Input 行着色:`key: value` 拆成 dim 键 + muted 值,其余整行 muted。 */
|
|
594
|
-
function styleInputSourceLine(line: string, theme: any): string {
|
|
595
|
-
const match = line.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
|
|
596
|
-
if (!match) return theme.fg("muted", line);
|
|
597
|
-
const [, key, sep, rest] = match;
|
|
598
|
-
return theme.fg("dim", key + sep) + theme.fg("muted", rest ?? "");
|
|
79
|
+
export function collapsedResultSummary(options: {
|
|
80
|
+
result: any;
|
|
81
|
+
toolName: string;
|
|
82
|
+
text: string;
|
|
83
|
+
isError: boolean;
|
|
84
|
+
}): string {
|
|
85
|
+
const { result, toolName, text, isError } = options;
|
|
86
|
+
if (isError) {
|
|
87
|
+
// 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
|
|
88
|
+
return text ? oneLine(text, 72) : "Failed";
|
|
89
|
+
}
|
|
90
|
+
const outputLines = outputLineCount(result) || countLines(text);
|
|
91
|
+
if (!outputLines) return "Done";
|
|
92
|
+
const action = toolName === "read" ? "loaded" : "returned";
|
|
93
|
+
return `${outputLines} ${outputLines === 1 ? "line" : "lines"} ${action}`;
|
|
599
94
|
}
|
|
600
95
|
|
|
601
96
|
/**
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* 会被读成新的一行,所以缩进先剥掉、折行宽度相应扣减,再给每一行加回去。
|
|
605
|
-
* 行数判断(bodyExceedsLineLimit)与实际渲染共用这一套,展开提示才对得上。
|
|
97
|
+
* 工具耗时:不到 1 秒报毫秒,分钟级起带上分/时。
|
|
98
|
+
* 只报到秒的话,几分钟的长命令会显示成 `300.0s`。
|
|
606
99
|
*/
|
|
607
|
-
function
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const leadWidth = visibleWidth(lead);
|
|
620
|
-
const parts = wrapTextWithAnsi(styledBody, Math.max(1, contentWidth - leadWidth));
|
|
621
|
-
const rows = parts.length ? parts : [styledBody];
|
|
622
|
-
if (!leadWidth) return rows;
|
|
623
|
-
const prefix = theme.fg("muted", lead);
|
|
624
|
-
return rows.map((row) => prefix + row);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/** True when body needs truncation at the given line limit (source lines or wrapped rows). */
|
|
628
|
-
function bodyExceedsLineLimit(
|
|
629
|
-
body: string,
|
|
630
|
-
limit: number,
|
|
631
|
-
contentWidth: number,
|
|
632
|
-
asInput: boolean,
|
|
633
|
-
theme: any,
|
|
634
|
-
bodyColor = "toolOutput",
|
|
635
|
-
/** 代码块的源行取着色内容,行数判断与实际渲染保持同一套规则。 */
|
|
636
|
-
codeLine?: (sourceIndex: number) => string | undefined,
|
|
637
|
-
): boolean {
|
|
638
|
-
const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
639
|
-
if (!raw.trim()) return false;
|
|
640
|
-
const sourceLines = raw.split("\n");
|
|
641
|
-
if (sourceLines.length > limit) return true;
|
|
642
|
-
let total = 0;
|
|
643
|
-
for (const [index, source] of sourceLines.entries()) {
|
|
644
|
-
const parts = asInput
|
|
645
|
-
? wrapInputLine(source, contentWidth, theme, codeLine?.(index))
|
|
646
|
-
: wrapTextWithAnsi(theme.fg(bodyColor, source), contentWidth);
|
|
647
|
-
total += Math.max(1, parts.length);
|
|
648
|
-
if (total > limit) return true;
|
|
649
|
-
}
|
|
650
|
-
return false;
|
|
651
|
-
}
|
|
100
|
+
export function formatToolDuration(ms: number): string {
|
|
101
|
+
const seconds = ms / 1000;
|
|
102
|
+
if (seconds < 1) return `${Math.round(ms)}ms`;
|
|
103
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
104
|
+
const totalSeconds = Math.floor(seconds);
|
|
105
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
106
|
+
const remainder = totalSeconds % 60;
|
|
107
|
+
if (minutes < 60) return `${minutes}m ${remainder}s`;
|
|
108
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainder}s`;
|
|
109
|
+
}
|
|
110
|
+
/** 截断体末行 `… N more lines` 旁的展开提示,点击打开全量预览。 */
|
|
111
|
+
export const SHOW_MORE_LABEL = "· click to show more";
|
|
652
112
|
|
|
113
|
+
/** 折叠结果行:` ↳ <body>`。 */
|
|
653
114
|
export function renderCollapsedToolResult(body: string, collapsedHint = ""): string {
|
|
654
115
|
return ` ↳ ${body}${collapsedHint}`;
|
|
655
116
|
}
|
|
@@ -699,233 +160,3 @@ export function middleTruncateToWidth(text: string, width: number): string {
|
|
|
699
160
|
}
|
|
700
161
|
return `${left}…${right}`;
|
|
701
162
|
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* 展开卡 Input 的代码块:源行起始下标(相对 text)+ 语言 + 剥掉行首缩进的代码。
|
|
705
|
-
* standalone = 整段 Input 就是这块代码(没有 `code:` 标签),全量预览可以整段包围栏。
|
|
706
|
-
*/
|
|
707
|
-
export type InputCodeBlock = {
|
|
708
|
-
startLine: number;
|
|
709
|
-
language: string;
|
|
710
|
-
code: string;
|
|
711
|
-
standalone: boolean;
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
export type ToolInputBody = { text: string; codeBlock?: InputCodeBlock };
|
|
715
|
-
|
|
716
|
-
/** 按字段名判定可着色的入参:codemode / mcpscript 的 `code` 都是 JS 脚本。 */
|
|
717
|
-
const INPUT_CODE_LANGUAGES: Record<string, string> = { code: "javascript" };
|
|
718
|
-
|
|
719
|
-
/** Pretty-print tool call args for the expanded Input section. */
|
|
720
|
-
export function formatToolInputBody(args: unknown, maxChars = 8_000): ToolInputBody {
|
|
721
|
-
if (args === undefined || args === null) return { text: "" };
|
|
722
|
-
if (typeof args !== "object") {
|
|
723
|
-
const text = sanitizeToolResultText(String(args));
|
|
724
|
-
return { text: text.length > maxChars ? `${text.slice(0, maxChars)}…` : text };
|
|
725
|
-
}
|
|
726
|
-
if (Array.isArray(args)) {
|
|
727
|
-
try {
|
|
728
|
-
const json = JSON.stringify(args, null, 2);
|
|
729
|
-
return { text: json.length > maxChars ? `${json.slice(0, maxChars)}…` : json };
|
|
730
|
-
} catch {
|
|
731
|
-
return { text: sanitizeToolResultText(String(args)) };
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
const entries = Object.entries(args as Record<string, unknown>).filter(
|
|
736
|
-
([, value]) => value !== undefined,
|
|
737
|
-
);
|
|
738
|
-
if (entries.length === 0) return { text: "" };
|
|
739
|
-
|
|
740
|
-
// Stable, human-first field order for common tools.
|
|
741
|
-
const preferred = [
|
|
742
|
-
"path",
|
|
743
|
-
"file_path",
|
|
744
|
-
"command",
|
|
745
|
-
"query",
|
|
746
|
-
"pattern",
|
|
747
|
-
"url",
|
|
748
|
-
"name",
|
|
749
|
-
"message",
|
|
750
|
-
"content",
|
|
751
|
-
"old_string",
|
|
752
|
-
"new_string",
|
|
753
|
-
];
|
|
754
|
-
entries.sort(([left], [right]) => {
|
|
755
|
-
const li = preferred.indexOf(left);
|
|
756
|
-
const ri = preferred.indexOf(right);
|
|
757
|
-
if (li === -1 && ri === -1) return left.localeCompare(right);
|
|
758
|
-
if (li === -1) return 1;
|
|
759
|
-
if (ri === -1) return -1;
|
|
760
|
-
return li - ri;
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
const lines: string[] = [];
|
|
764
|
-
let codeBlock: InputCodeBlock | undefined;
|
|
765
|
-
for (const [rawKey, value] of entries) {
|
|
766
|
-
const key = sanitizeToolResultText(rawKey);
|
|
767
|
-
if (typeof value === "string") {
|
|
768
|
-
const safeValue = sanitizeToolResultText(value);
|
|
769
|
-
if (safeValue.includes("\n")) {
|
|
770
|
-
const expanded = safeValue.replace(/\t/g, " ");
|
|
771
|
-
const language = INPUT_CODE_LANGUAGES[key];
|
|
772
|
-
// 只有一个 code 字段时(codemode 等)整块 Input 就是脚本,`code:` 标签是噪音
|
|
773
|
-
const standaloneCode = Boolean(language) && entries.length === 1;
|
|
774
|
-
if (!standaloneCode) lines.push(`${key}:`);
|
|
775
|
-
if (language) {
|
|
776
|
-
// 缩进由 wrapInputLine 负责,着色只喂剥掉行首缩进的正文
|
|
777
|
-
codeBlock = {
|
|
778
|
-
startLine: lines.length,
|
|
779
|
-
language,
|
|
780
|
-
code: expanded
|
|
781
|
-
.split("\n")
|
|
782
|
-
.map((line) => line.replace(/^[ \t]*/, ""))
|
|
783
|
-
.join("\n"),
|
|
784
|
-
standalone: standaloneCode,
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
for (const line of expanded.split("\n")) {
|
|
788
|
-
lines.push(` ${line}`);
|
|
789
|
-
}
|
|
790
|
-
} else {
|
|
791
|
-
lines.push(`${key}: ${safeValue}`);
|
|
792
|
-
}
|
|
793
|
-
continue;
|
|
794
|
-
}
|
|
795
|
-
if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
796
|
-
lines.push(`${key}: ${String(value)}`);
|
|
797
|
-
continue;
|
|
798
|
-
}
|
|
799
|
-
try {
|
|
800
|
-
const json = JSON.stringify(value, null, 2);
|
|
801
|
-
if (json.includes("\n")) {
|
|
802
|
-
lines.push(`${key}:`);
|
|
803
|
-
for (const line of json.split("\n")) lines.push(` ${line}`);
|
|
804
|
-
} else {
|
|
805
|
-
lines.push(`${key}: ${json}`);
|
|
806
|
-
}
|
|
807
|
-
} catch {
|
|
808
|
-
lines.push(`${key}: [unserializable]`);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
const text = lines.join("\n");
|
|
812
|
-
// 截断后的行号与着色行不再对齐,这时只给纯文本
|
|
813
|
-
if (text.length > maxChars) return { text: `${text.slice(0, maxChars)}…` };
|
|
814
|
-
return codeBlock ? { text, codeBlock } : { text };
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
818
|
-
return formatToolInputBody(args, maxChars).text;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
export function hasExpandableDetail(outputText: string, args: unknown): boolean {
|
|
822
|
-
if (hasExpandableResult(outputText)) return true;
|
|
823
|
-
return formatToolInputArgs(args).trim().length > 0;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
export function renderExpandedToolResult(
|
|
827
|
-
body: string,
|
|
828
|
-
theme: any,
|
|
829
|
-
isError: boolean,
|
|
830
|
-
lastComponent?: unknown,
|
|
831
|
-
args?: unknown,
|
|
832
|
-
context?: any,
|
|
833
|
-
/** mode=on 展开卡贴左;compact 等保持默认前导空格 */
|
|
834
|
-
flushLeft = false,
|
|
835
|
-
): ExpandedToolIoView | ExpandedToolResultText | Text {
|
|
836
|
-
const { text: inputBody, codeBlock } = formatToolInputBody(args);
|
|
837
|
-
const outputBody = body;
|
|
838
|
-
const maxOutputLines = config.expandedOutputMaxLines;
|
|
839
|
-
const maxInputLines = config.expandedInputMaxLines;
|
|
840
|
-
|
|
841
|
-
// Prefer structured Input/Output when we have args or non-empty output.
|
|
842
|
-
if (inputBody.trim() || outputBody.trim()) {
|
|
843
|
-
let view: ExpandedToolIoView;
|
|
844
|
-
if (isExpandedToolIoView(lastComponent)) {
|
|
845
|
-
lastComponent.setContent(
|
|
846
|
-
inputBody,
|
|
847
|
-
outputBody,
|
|
848
|
-
isError,
|
|
849
|
-
maxOutputLines,
|
|
850
|
-
maxInputLines,
|
|
851
|
-
flushLeft,
|
|
852
|
-
);
|
|
853
|
-
view = lastComponent;
|
|
854
|
-
} else {
|
|
855
|
-
view = new ExpandedToolIoView(
|
|
856
|
-
theme,
|
|
857
|
-
inputBody,
|
|
858
|
-
outputBody,
|
|
859
|
-
isError,
|
|
860
|
-
maxOutputLines,
|
|
861
|
-
maxInputLines,
|
|
862
|
-
flushLeft,
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
view.setInputCode(codeBlock);
|
|
866
|
-
if (context) rememberIoView(context, view);
|
|
867
|
-
return view;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
if (context?.state) context.state.ccstyleIoView = undefined;
|
|
871
|
-
const color = isError ? "error" : "muted";
|
|
872
|
-
return new Text(theme.fg(color, renderCollapsedToolResult("Done")), 0, 0);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const toolViewMarker = (id: number, section: ToolIoSection) =>
|
|
876
|
-
`_cc:v${id}:${section === "input" ? "i" : "o"}`;
|
|
877
|
-
|
|
878
|
-
/** Per-frame ExpandedToolIoView ids for unambiguous show-more hit testing. */
|
|
879
|
-
export type IoViewFrameState = {
|
|
880
|
-
viewIds: Map<ExpandedToolIoView, number>;
|
|
881
|
-
idToView: Map<number, ExpandedToolIoView>;
|
|
882
|
-
nextId: number;
|
|
883
|
-
};
|
|
884
|
-
let activeIoViewFrame: IoViewFrameState | null = null;
|
|
885
|
-
|
|
886
|
-
function frameViewId(view: ExpandedToolIoView): number | null {
|
|
887
|
-
if (!activeIoViewFrame) return null;
|
|
888
|
-
let id = activeIoViewFrame.viewIds.get(view);
|
|
889
|
-
if (id === undefined) {
|
|
890
|
-
id = activeIoViewFrame.nextId++;
|
|
891
|
-
activeIoViewFrame.viewIds.set(view, id);
|
|
892
|
-
activeIoViewFrame.idToView.set(id, view);
|
|
893
|
-
}
|
|
894
|
-
return id;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/** cachedLines stay clean; only the returned paint copy carries show-more view markers. */
|
|
898
|
-
function withIoViewMarkers(view: ExpandedToolIoView, lines: string[]): string[] {
|
|
899
|
-
const id = frameViewId(view);
|
|
900
|
-
if (id === null) return lines;
|
|
901
|
-
// Mark by exact header row from render — never scan body text for Input/Output labels.
|
|
902
|
-
const marked = lines.slice();
|
|
903
|
-
for (const { section, line } of view.showMoreHeaderLineIndexes()) {
|
|
904
|
-
if (line < 0 || line >= marked.length) continue;
|
|
905
|
-
marked[line] = `${marked[line]}${toolViewMarker(id, section)}`;
|
|
906
|
-
}
|
|
907
|
-
return marked;
|
|
908
|
-
}
|
|
909
|
-
const ioViewInvalidators = new WeakMap<ExpandedToolIoView, () => void>();
|
|
910
|
-
function rememberIoView(context: any, view: ExpandedToolIoView): void {
|
|
911
|
-
if (!context || typeof context !== "object") return;
|
|
912
|
-
if (typeof context.invalidate === "function") {
|
|
913
|
-
ioViewInvalidators.set(view, () => context.invalidate());
|
|
914
|
-
}
|
|
915
|
-
if (!context.state || typeof context.state !== "object") context.state = {};
|
|
916
|
-
context.state.ccstyleIoView = view;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/** hover 状态变更后刷新上一个/下一个视图(context.invalidate 缓存于 ioViewInvalidators)。 */
|
|
920
|
-
export function invalidateIoView(view: ExpandedToolIoView): void {
|
|
921
|
-
ioViewInvalidators.get(view)?.();
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/** 鼠标帧渲染期间读写当前 IoView 帧 id 表(mouse-interaction 的 doRender 包装使用)。 */
|
|
925
|
-
export function getActiveIoViewFrame(): IoViewFrameState | null {
|
|
926
|
-
return activeIoViewFrame;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
export function setActiveIoViewFrame(frame: IoViewFrameState | null): void {
|
|
930
|
-
activeIoViewFrame = frame;
|
|
931
|
-
}
|