pi-cc-extensions 0.8.52 → 0.8.54
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 +6 -8
- package/README.md +5 -6
- package/assets/readme/hero.en.svg +101 -0
- package/extensions/config/config.ts +1 -17
- package/extensions/config/panel.ts +5 -3
- package/extensions/{utils/agent-summary.ts → feature/agent-summary/core.ts} +37 -35
- package/extensions/feature/{agent-summary.ts → agent-summary/index.ts} +2 -6
- package/extensions/feature/compact-thinking.ts +13 -17
- package/extensions/feature/{session-reference.ts → reference/index.ts} +1 -1
- package/extensions/feature/{pi-startup-header.ts → shell/startup-header.ts} +1 -1
- package/extensions/index.ts +22 -33
- package/extensions/renderer/compact-mode.ts +375 -87
- package/extensions/renderer/default-mode.ts +726 -0
- package/extensions/renderer/index.ts +57 -773
- package/extensions/{feature → renderer}/markdown-enhance.ts +14 -0
- package/extensions/renderer/{mouse-hover.ts → mouse/hover.ts} +5 -5
- package/extensions/renderer/{mouse-interaction.ts → mouse/interaction.ts} +37 -35
- package/extensions/renderer/{mouse-layout.ts → mouse/layout.ts} +5 -21
- package/extensions/renderer/{mouse-packets.ts → mouse/packets.ts} +1 -1
- package/extensions/renderer/{mouse-scroll.ts → mouse/scroll.ts} +46 -33
- package/extensions/renderer/{tool-diff → tool/diff}/diff-renderer.ts +2 -2
- package/extensions/renderer/{tool-diff → tool/diff}/line-width-safety.ts +1 -1
- package/extensions/renderer/tool/diff/write-display-utils.ts +12 -0
- package/extensions/renderer/{tool-grouping.ts → tool/grouping.ts} +18 -8
- package/extensions/renderer/{tool-result.ts → tool/result.ts} +4 -13
- package/extensions/renderer/transcript-refresh.ts +91 -0
- package/extensions/utils/fullscreen-detect.ts +0 -27
- package/package.json +3 -2
- package/extensions/renderer/tool-diff/write-display-utils.ts +0 -29
- /package/extensions/feature/{session-reference-core.ts → reference/session.ts} +0 -0
- /package/extensions/feature/{agent-autocomplete.ts → reference/subagent.ts} +0 -0
- /package/extensions/feature/{pi-aliases.ts → shell/aliases.ts} +0 -0
- /package/extensions/feature/{working-message.ts → shell/working-message.ts} +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/ATTRIBUTION.md +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/ansi-utils.ts +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/diff-presentation.ts +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/index.ts +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/render-utils.ts +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/shiki-highlight.ts +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/types.ts +0 -0
- /package/extensions/renderer/{tool-diff → tool/diff}/write-execution.ts +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compact mode:每条含 toolCall 的 assistant message 折叠为一条逐步累加的摘要行
|
|
3
|
-
* (`
|
|
3
|
+
* (`Ran for 8s, bash×2, read×2`),edit/write 独立单行(`✓ write <path> (+25 -0)`),
|
|
4
4
|
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)在单个工具卡中恢复
|
|
5
5
|
* compact-thinking/Pi 原生或专用 renderer。edit/write 折叠时显示统计,展开时显示 rich diff。
|
|
6
|
+
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
7
|
+
* 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* 工具计数:read 按非空路径去重、其余按调用计数(首次出现顺序);edit/write 不进摘要。
|
|
10
|
+
* 时长 = 回合流逝挂钟;进行中 Running...,结束 Ran for。
|
|
11
|
+
* abort/error/length 状态行挂在摘要外层,避免被折叠吞掉。
|
|
12
|
+
* 最终 agent 回合摘要由 feature/agent-summary 独占(bash/read/edit/write/other)。
|
|
11
13
|
*
|
|
12
14
|
* 补丁生命周期遵循仓库既有模式:Symbol 所有权、dispose 仅恢复仍由本安装持有的
|
|
13
15
|
* 方法、重入守卫防止 /reload 后残留闭包递归。
|
|
@@ -17,26 +19,27 @@ import {
|
|
|
17
19
|
ToolExecutionComponent,
|
|
18
20
|
type Theme,
|
|
19
21
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import
|
|
21
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
22
|
+
import { truncateToWidth, visibleWidth, Box } from "@earendil-works/pi-tui";
|
|
22
23
|
import { config, getToolDisplayConfig } from "../config/config.ts";
|
|
23
24
|
import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
|
|
24
25
|
import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
|
|
26
|
+
import { refreshTranscriptComponent } from "./transcript-refresh.ts";
|
|
25
27
|
import { getMessageDisplayTheme } from "./message-display.ts";
|
|
26
28
|
import { showMoreHintText } from "./show-more-hint.ts";
|
|
27
29
|
import {
|
|
28
30
|
countEditDiffStats,
|
|
29
31
|
countWriteDiffStats,
|
|
30
32
|
isRichDiffComponent,
|
|
31
|
-
} from "./tool
|
|
32
|
-
import { renderRichToolResult } from "./tool
|
|
33
|
-
import type { WriteExecutionMetadataStore } from "./tool
|
|
33
|
+
} from "./tool/diff/diff-renderer.ts";
|
|
34
|
+
import { renderRichToolResult } from "./tool/diff/index.ts";
|
|
35
|
+
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
34
36
|
import {
|
|
35
37
|
insetComponent,
|
|
36
38
|
oneLine,
|
|
37
39
|
renderExpandedToolResult,
|
|
38
40
|
scheduleAnimation,
|
|
39
|
-
} from "./tool
|
|
41
|
+
} from "./tool/result.ts";
|
|
42
|
+
import { paddedBackgroundRow, stripBackgroundAnsi } from "./tool/grouping.ts";
|
|
40
43
|
|
|
41
44
|
/** compact 渲染层对 compact-thinking 的只读查询面(不建第二套计时器)。 */
|
|
42
45
|
export type CompactThinkingQuery = {
|
|
@@ -56,7 +59,6 @@ const ASSISTANT_SET_EXPANDED_KEY = Symbol.for(ASSISTANT_SET_EXPANDED_DESCRIPTION
|
|
|
56
59
|
const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for(ASSISTANT_TOGGLE_ROUND_DESCRIPTION);
|
|
57
60
|
const ASSISTANT_REENTRY_KEY = Symbol.for(ASSISTANT_REENTRY_DESCRIPTION);
|
|
58
61
|
|
|
59
|
-
const Box = (PiTui as any).Box;
|
|
60
62
|
const EDIT_WRITE_TOOLS = new Set(["edit", "write"]);
|
|
61
63
|
|
|
62
64
|
type CompactThinkingTheme = Pick<Theme, "fg" | "italic" | "bold">;
|
|
@@ -123,26 +125,52 @@ function formatThoughtDuration(durationMs: number) {
|
|
|
123
125
|
|
|
124
126
|
export { formatThoughtDuration };
|
|
125
127
|
|
|
128
|
+
/** assistant stopReason → 外层状态文案(与 Pi 原生口径对齐)。 */
|
|
129
|
+
export function messageStopStatus(message: any): string | undefined {
|
|
130
|
+
const reason = message?.stopReason;
|
|
131
|
+
if (reason === "aborted") {
|
|
132
|
+
return message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
133
|
+
? String(message.errorMessage)
|
|
134
|
+
: "Operation aborted";
|
|
135
|
+
}
|
|
136
|
+
if (reason === "error") {
|
|
137
|
+
return `Error: ${message.errorMessage || "Unknown error"}`;
|
|
138
|
+
}
|
|
139
|
+
if (reason === "length") {
|
|
140
|
+
return "Response was truncated before completion.";
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function roundStopStatus(messages: Iterable<any>): string | undefined {
|
|
146
|
+
for (const message of messages) {
|
|
147
|
+
const status = messageStopStatus(message);
|
|
148
|
+
if (status) return status;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
/**
|
|
127
154
|
* 逐条 assistant message 的摘要文本(无工具计数时可为空串):
|
|
128
|
-
*
|
|
155
|
+
* 时长 = max(thinking query, durationFloorMs 挂钟);工具首次出现顺序;read 路径去重。
|
|
129
156
|
*/
|
|
130
157
|
function buildMessagesSummary(
|
|
131
158
|
messages: Iterable<any>,
|
|
132
159
|
query?: CompactThinkingQuery,
|
|
133
|
-
|
|
160
|
+
runningActiveOverride?: boolean,
|
|
161
|
+
durationFloorMs?: number,
|
|
134
162
|
): string {
|
|
135
163
|
const parts: string[] = [];
|
|
136
164
|
const counts = new Map<string, number>();
|
|
137
165
|
const readPaths = new Set<string>();
|
|
138
166
|
const durationTimestamps = new Set<number>();
|
|
139
167
|
let durationMs = 0;
|
|
140
|
-
let
|
|
168
|
+
let runningActive = false;
|
|
141
169
|
|
|
142
170
|
for (const message of messages) {
|
|
143
171
|
if (typeof message?.timestamp === "number" && !durationTimestamps.has(message.timestamp)) {
|
|
144
172
|
durationTimestamps.add(message.timestamp);
|
|
145
|
-
if (query?.isMessageThinkingActive?.(message.timestamp))
|
|
173
|
+
if (query?.isMessageThinkingActive?.(message.timestamp)) runningActive = true;
|
|
146
174
|
const value = query?.getMessageThinkingDurationMs(message.timestamp);
|
|
147
175
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) durationMs += value;
|
|
148
176
|
}
|
|
@@ -164,18 +192,28 @@ function buildMessagesSummary(
|
|
|
164
192
|
}
|
|
165
193
|
}
|
|
166
194
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
if (
|
|
196
|
+
typeof durationFloorMs === "number" &&
|
|
197
|
+
Number.isFinite(durationFloorMs) &&
|
|
198
|
+
durationFloorMs > durationMs
|
|
199
|
+
) {
|
|
200
|
+
durationMs = durationFloorMs;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
runningActive = runningActiveOverride ?? runningActive;
|
|
204
|
+
if (runningActive) {
|
|
205
|
+
parts.push(durationMs > 0 ? `Running... · ${formatThoughtDuration(durationMs)}` : "Running...");
|
|
206
|
+
} else if (durationMs > 0) parts.push(`Ran for ${formatThoughtDuration(durationMs)}`);
|
|
173
207
|
for (const [name, count] of counts) parts.push(`${name}×${count}`);
|
|
174
208
|
return parts.join(", ");
|
|
175
209
|
}
|
|
176
210
|
|
|
177
|
-
export function buildMessageSummary(
|
|
178
|
-
|
|
211
|
+
export function buildMessageSummary(
|
|
212
|
+
message: any,
|
|
213
|
+
query?: CompactThinkingQuery,
|
|
214
|
+
durationFloorMs?: number,
|
|
215
|
+
): string {
|
|
216
|
+
return buildMessagesSummary([message], query, undefined, durationFloorMs);
|
|
179
217
|
}
|
|
180
218
|
|
|
181
219
|
function fallbackTheme(): any {
|
|
@@ -190,7 +228,101 @@ function themeOf(): any {
|
|
|
190
228
|
return getMessageDisplayTheme() ?? fallbackTheme();
|
|
191
229
|
}
|
|
192
230
|
|
|
193
|
-
|
|
231
|
+
/** RGB → HSL(h∈[0,1), l/s∈[0,1])。 */
|
|
232
|
+
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
|
233
|
+
r /= 255;
|
|
234
|
+
g /= 255;
|
|
235
|
+
b /= 255;
|
|
236
|
+
const max = Math.max(r, g, b);
|
|
237
|
+
const min = Math.min(r, g, b);
|
|
238
|
+
const l = (max + min) / 2;
|
|
239
|
+
if (max === min) return [0, 0, l];
|
|
240
|
+
const d = max - min;
|
|
241
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
242
|
+
let h: number;
|
|
243
|
+
switch (max) {
|
|
244
|
+
case r:
|
|
245
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
246
|
+
break;
|
|
247
|
+
case g:
|
|
248
|
+
h = (b - r) / d + 2;
|
|
249
|
+
break;
|
|
250
|
+
default:
|
|
251
|
+
h = (r - g) / d + 4;
|
|
252
|
+
}
|
|
253
|
+
return [h / 6, l, s];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** HSL → RGB(h∈[0,1),l/s∈[0,1])。 */
|
|
257
|
+
function hslToRgb(h: number, l: number, s: number): [number, number, number] {
|
|
258
|
+
if (s === 0) {
|
|
259
|
+
const v = Math.round(l * 255);
|
|
260
|
+
return [v, v, v];
|
|
261
|
+
}
|
|
262
|
+
const hue2rgb = (p: number, q: number, t: number): number => {
|
|
263
|
+
if (t < 0) t += 1;
|
|
264
|
+
if (t > 1) t -= 1;
|
|
265
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
266
|
+
if (t < 1 / 2) return q;
|
|
267
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
268
|
+
return p;
|
|
269
|
+
};
|
|
270
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
271
|
+
const p = 2 * l - q;
|
|
272
|
+
return [
|
|
273
|
+
Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
|
|
274
|
+
Math.round(hue2rgb(p, q, h) * 255),
|
|
275
|
+
Math.round(hue2rgb(p, q, h - 1 / 3) * 255),
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 内部 tool call card 背景:在 userMessageBg 基础上按 HSL 变暗(l×0.7),
|
|
280
|
+
* 与外卡片形成嵌套层次;非 RGB 色或已足够暗时原样返回。 */
|
|
281
|
+
function darkenBgAnsi(theme: any, slot: string): string {
|
|
282
|
+
const prefix = typeof theme?.getBgAnsi === "function" ? String(theme.getBgAnsi(slot)) : "";
|
|
283
|
+
const m = prefix.match(/48;2;(\d+);(\d+);(\d+)/);
|
|
284
|
+
if (!m) return prefix;
|
|
285
|
+
const [h, l, s] = rgbToHsl(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
286
|
+
if (l <= 0.05) return prefix; // 已足够暗,不再变暗
|
|
287
|
+
const [r, g, b] = hslToRgb(h, l * 0.7, s);
|
|
288
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** 去掉行内所有 CSI/OSC 序列后是否有可见文本(判断工具卡首尾内容行)。 */
|
|
292
|
+
function hasVisibleText(line: string): boolean {
|
|
293
|
+
return (
|
|
294
|
+
line
|
|
295
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
296
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
297
|
+
.trim().length > 0
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 工具卡深色行:左右内缩(左 2 右 3 格,含工具卡自身 padding),背景只到内容区,
|
|
302
|
+
* 修复行尾 reset 截断。 */
|
|
303
|
+
function toolCardBgRow(
|
|
304
|
+
theme: any,
|
|
305
|
+
slot: string,
|
|
306
|
+
bgAnsi: string,
|
|
307
|
+
line: string,
|
|
308
|
+
width: number,
|
|
309
|
+
): string {
|
|
310
|
+
const leftInset = 2;
|
|
311
|
+
const rightInset = 3;
|
|
312
|
+
const contentWidth = Math.max(0, width - leftInset - rightInset);
|
|
313
|
+
// 深色块内左右各 1 格内部 padding;去掉行首原有空格后重新对齐
|
|
314
|
+
const text = stripBackgroundAnsi(line).replace(/^ +/, "");
|
|
315
|
+
const innerPad = 2;
|
|
316
|
+
const clipped = truncateToWidth(text, Math.max(0, contentWidth - innerPad), "");
|
|
317
|
+
const pad = Math.max(0, contentWidth - innerPad - visibleWidth(clipped));
|
|
318
|
+
const body = ` ${clipped}${" ".repeat(pad)} `;
|
|
319
|
+
const stable = body.replace(/\x1b\[(?:0)?m/g, (reset) => reset + bgAnsi);
|
|
320
|
+
const outerBg = typeof theme?.getBgAnsi === "function" ? String(theme.getBgAnsi(slot)) : "";
|
|
321
|
+
const inset = (n) => (outerBg ? `${outerBg}${" ".repeat(n)}\x1b[49m` : " ".repeat(n));
|
|
322
|
+
return `${inset(leftInset)}${bgAnsi}${stable}\x1b[49m${inset(rightInset)}`;
|
|
323
|
+
}
|
|
324
|
+
/** edit/write 展开卡:保持原样式(userMessageBg Box),不应用工具卡深色/间距改动。 */
|
|
325
|
+
function editWriteExpandedCard(theme: any): any {
|
|
194
326
|
return new Box(
|
|
195
327
|
1,
|
|
196
328
|
1,
|
|
@@ -198,6 +330,73 @@ function expandedToolCard(theme: any): any {
|
|
|
198
330
|
);
|
|
199
331
|
}
|
|
200
332
|
|
|
333
|
+
/** compact 展开卡:外卡片保持 userMessageBg 原色;内部 tool call card(addToolChild)
|
|
334
|
+
* 背景更深一层且只覆盖内容区(左右内缩、上下限首尾文本行),形成嵌套层次。
|
|
335
|
+
* 逐行重建背景同时修复嵌套 Box 的右侧截断。 */
|
|
336
|
+
function expandedToolCard(theme: any): any {
|
|
337
|
+
const slot = "userMessageBg";
|
|
338
|
+
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
339
|
+
const children: any[] = [];
|
|
340
|
+
return {
|
|
341
|
+
children,
|
|
342
|
+
addChild(child: any) {
|
|
343
|
+
children.push(child);
|
|
344
|
+
},
|
|
345
|
+
addToolChild(child: any) {
|
|
346
|
+
child.__ccToolCard = true;
|
|
347
|
+
children.push(child);
|
|
348
|
+
},
|
|
349
|
+
render(width: number): string[] {
|
|
350
|
+
const innerWidth = Math.max(0, width - 2);
|
|
351
|
+
const lines: string[] = [];
|
|
352
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
353
|
+
// 展开不再显示摘要行:跳过第一个 child 的首行空白,避免白占一行
|
|
354
|
+
let skipLeadingBlank = true;
|
|
355
|
+
for (const child of children) {
|
|
356
|
+
const childLines = child.render(innerWidth);
|
|
357
|
+
if (!child.__ccToolCard) {
|
|
358
|
+
let start = 0;
|
|
359
|
+
if (skipLeadingBlank) {
|
|
360
|
+
while (start < childLines.length && !hasVisibleText(childLines[start])) start++;
|
|
361
|
+
skipLeadingBlank = false;
|
|
362
|
+
}
|
|
363
|
+
for (let i = start; i < childLines.length; i++) {
|
|
364
|
+
lines.push(paddedBackgroundRow(theme, slot, childLines[i], width));
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
// 工具卡:前导 1 空行 + 首尾文本行之间的内容区(深色);不保留尾随,
|
|
369
|
+
// 相邻工具卡之间正好 1 空行,底部由卡片 padding 收尾。
|
|
370
|
+
skipLeadingBlank = false;
|
|
371
|
+
let first = -1;
|
|
372
|
+
let last = -1;
|
|
373
|
+
for (let i = 0; i < childLines.length; i++) {
|
|
374
|
+
if (hasVisibleText(childLines[i])) {
|
|
375
|
+
if (first < 0) first = i;
|
|
376
|
+
last = i;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (first < 0) {
|
|
380
|
+
for (const line of childLines) {
|
|
381
|
+
lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
386
|
+
// 子卡片内部上下各 1 行深色 padding
|
|
387
|
+
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
|
|
388
|
+
for (let i = first; i <= last; i++) {
|
|
389
|
+
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, childLines[i], width));
|
|
390
|
+
}
|
|
391
|
+
lines.push(toolCardBgRow(theme, slot, toolBgAnsi, "", width));
|
|
392
|
+
}
|
|
393
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
394
|
+
return lines;
|
|
395
|
+
},
|
|
396
|
+
invalidate() {},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
201
400
|
function isAssistantComponent(value: any): boolean {
|
|
202
401
|
return value instanceof AssistantMessageComponent;
|
|
203
402
|
}
|
|
@@ -248,7 +447,7 @@ export type CompactModeHooks = {
|
|
|
248
447
|
refresh(): void;
|
|
249
448
|
/** compact 模式下重新认领 assistant patch(位于 compact-thinking 之上)。 */
|
|
250
449
|
assertOwnership(): void;
|
|
251
|
-
/** 重新渲染包含指定 toolCallId 的 assistant
|
|
450
|
+
/** 重新渲染包含指定 toolCallId 的 assistant 消息(思考收尾时刷新)。 */
|
|
252
451
|
refreshToolCallMessage(toolCallId: string | undefined): void;
|
|
253
452
|
shutdown(): void;
|
|
254
453
|
};
|
|
@@ -382,7 +581,7 @@ function compactEditWriteExpandedLines(
|
|
|
382
581
|
component.resultRendererComponent = detail;
|
|
383
582
|
}
|
|
384
583
|
|
|
385
|
-
const box =
|
|
584
|
+
const box = editWriteExpandedCard(theme);
|
|
386
585
|
box.addChild({
|
|
387
586
|
render(innerWidth: number): string[] {
|
|
388
587
|
return compactEditWriteLine(component, innerWidth, writeMetadata).slice(1);
|
|
@@ -395,7 +594,8 @@ function compactEditWriteExpandedLines(
|
|
|
395
594
|
|
|
396
595
|
function compactAssistantLineComponent(
|
|
397
596
|
component: any,
|
|
398
|
-
|
|
597
|
+
/** 静态串或每次 render 重算(Running 时长需逐步跳动)。 */
|
|
598
|
+
summary: string | (() => string),
|
|
399
599
|
query?: CompactThinkingQuery,
|
|
400
600
|
options: { hint?: boolean; leadingBlank?: boolean; pad?: number } = {},
|
|
401
601
|
): any {
|
|
@@ -407,17 +607,15 @@ function compactAssistantLineComponent(
|
|
|
407
607
|
const available = Math.max(0, width - pad);
|
|
408
608
|
const hintText = options.hint === false ? "" : ` • ${showMoreHintText()}`;
|
|
409
609
|
const summaryWidth = Math.max(0, available - visibleWidth(hintText));
|
|
410
|
-
const
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
: summary;
|
|
414
|
-
const plainText = truncateToWidth(displaySummary, summaryWidth, "…");
|
|
610
|
+
const resolved = typeof summary === "function" ? summary() : summary;
|
|
611
|
+
const runningActive = resolved.startsWith("Running...");
|
|
612
|
+
const plainText = truncateToWidth(resolved, summaryWidth, "…");
|
|
415
613
|
let text = theme.fg("muted", plainText);
|
|
416
|
-
if (
|
|
614
|
+
if (runningActive || plainText.startsWith("Ran for ")) {
|
|
417
615
|
const separator = plainText.indexOf(", ");
|
|
418
616
|
const heading = separator < 0 ? plainText : plainText.slice(0, separator);
|
|
419
617
|
const tools = separator < 0 ? "" : plainText.slice(separator);
|
|
420
|
-
if (
|
|
618
|
+
if (runningActive) {
|
|
421
619
|
const durationSeparator = heading.indexOf(" · ");
|
|
422
620
|
const label = durationSeparator < 0 ? heading : heading.slice(0, durationSeparator);
|
|
423
621
|
const duration = durationSeparator < 0 ? "" : heading.slice(durationSeparator);
|
|
@@ -439,11 +637,35 @@ function compactAssistantLineComponent(
|
|
|
439
637
|
};
|
|
440
638
|
}
|
|
441
639
|
|
|
442
|
-
function
|
|
443
|
-
|
|
640
|
+
function compactStopStatusLine(status: string, pad = 0): any {
|
|
641
|
+
return {
|
|
642
|
+
render(width: number): string[] {
|
|
643
|
+
const theme = themeOf();
|
|
644
|
+
const prefix = " ".repeat(Math.max(0, pad));
|
|
645
|
+
const line = `${prefix}${theme.fg("error", status)}`;
|
|
646
|
+
return ["", truncateToWidth(line, width, "")];
|
|
647
|
+
},
|
|
648
|
+
invalidate() {},
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function compactAssistantLine(
|
|
653
|
+
component: any,
|
|
654
|
+
summary: string | (() => string),
|
|
655
|
+
query?: CompactThinkingQuery,
|
|
656
|
+
): void {
|
|
657
|
+
// 空静态串跳过;getter 留给 render 判断(Running 可能稍后才有时长)。
|
|
658
|
+
if (typeof summary === "string" && !summary) return;
|
|
444
659
|
component.contentContainer.addChild(compactAssistantLineComponent(component, summary, query));
|
|
445
660
|
}
|
|
446
661
|
|
|
662
|
+
function appendStopStatus(component: any, status: string | undefined): void {
|
|
663
|
+
if (!status || !component?.contentContainer?.addChild) return;
|
|
664
|
+
component.contentContainer.addChild(
|
|
665
|
+
compactStopStatusLine(status, Number(component.outputPad) || 0),
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
447
669
|
function ensureAssistantSetExpanded(component: any): void {
|
|
448
670
|
if (
|
|
449
671
|
typeof component[ASSISTANT_SET_EXPANDED_KEY] === "function" &&
|
|
@@ -507,10 +729,17 @@ function collectMountedComponents(root: any): void {
|
|
|
507
729
|
}
|
|
508
730
|
};
|
|
509
731
|
visit(root);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
732
|
+
// 扫到组件才替换跟踪表。面板/custom UI 打开时 root 往往扫不到 transcript,
|
|
733
|
+
// 若此时清空会丢掉 live updateContent/updateDisplay 已登记的实例,
|
|
734
|
+
// /ccstyle 切换就只能靠 /reload 重建。
|
|
735
|
+
if (assistants.size > 0 || tools.size > 0) {
|
|
736
|
+
trackedAssistantComponents.clear();
|
|
737
|
+
trackedToolComponents.clear();
|
|
738
|
+
for (const component of assistants) trackedAssistantComponents.add(component);
|
|
739
|
+
for (const component of tools) trackedToolComponents.add(component);
|
|
740
|
+
} else if (config.mode === "compact") {
|
|
741
|
+
for (const component of trackedAssistantComponents) ensureAssistantSetExpanded(component);
|
|
742
|
+
}
|
|
514
743
|
}
|
|
515
744
|
|
|
516
745
|
export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHooks {
|
|
@@ -552,10 +781,62 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
552
781
|
detachedMessages: any[];
|
|
553
782
|
active: boolean;
|
|
554
783
|
suppressedToolIds: Set<string>;
|
|
784
|
+
/** 回合挂钟起点,保证 Running 时长连续递增。 */
|
|
785
|
+
startedAt: number;
|
|
786
|
+
endedAt?: number;
|
|
555
787
|
};
|
|
556
788
|
let activeRound: CompactRound | undefined;
|
|
557
789
|
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
558
790
|
const expandedRoundToolIds = new Set<string>();
|
|
791
|
+
let uiRef: { requestRender?: (force?: boolean) => void } | undefined;
|
|
792
|
+
let roundTickTimer: ReturnType<typeof setInterval> | undefined;
|
|
793
|
+
|
|
794
|
+
const roundWallMs = (round: CompactRound): number => {
|
|
795
|
+
const end = round.active ? Date.now() : (round.endedAt ?? Date.now());
|
|
796
|
+
return Math.max(1, end - round.startedAt);
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const summarize = (messages: Iterable<any>, runningActive?: boolean, round?: CompactRound) =>
|
|
800
|
+
buildMessagesSummary(
|
|
801
|
+
messages,
|
|
802
|
+
deps.query,
|
|
803
|
+
runningActive,
|
|
804
|
+
round ? roundWallMs(round) : undefined,
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
const stopRoundTick = (): void => {
|
|
808
|
+
if (!roundTickTimer) return;
|
|
809
|
+
clearInterval(roundTickTimer);
|
|
810
|
+
roundTickTimer = undefined;
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// 自备 tick:getter 在 render 重算挂钟;兼驱动 Running 扫光(setCompactSummaryActive)。
|
|
814
|
+
const ensureRoundTick = (): void => {
|
|
815
|
+
if (roundTickTimer) return;
|
|
816
|
+
roundTickTimer = setInterval(() => {
|
|
817
|
+
if (!patch.active || !activeRound?.active) {
|
|
818
|
+
stopRoundTick();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
uiRef?.requestRender?.(true);
|
|
823
|
+
} catch {
|
|
824
|
+
/* 无 UI */
|
|
825
|
+
}
|
|
826
|
+
}, 250);
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
/** 结束回合活动态;可选立即重绘。 */
|
|
830
|
+
const endRound = (round: CompactRound, render = false): void => {
|
|
831
|
+
round.active = false;
|
|
832
|
+
if (round.endedAt === undefined) round.endedAt = Date.now();
|
|
833
|
+
if (activeRound === round) {
|
|
834
|
+
activeRound = undefined;
|
|
835
|
+
deps.query?.setCompactSummaryActive?.(false);
|
|
836
|
+
stopRoundTick();
|
|
837
|
+
}
|
|
838
|
+
if (render) renderRound(round);
|
|
839
|
+
};
|
|
559
840
|
|
|
560
841
|
const renderAssistantWithoutThinking = (component: any, message: any): any => {
|
|
561
842
|
const content = Array.isArray(message?.content) ? message.content : [];
|
|
@@ -583,7 +864,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
583
864
|
};
|
|
584
865
|
|
|
585
866
|
const renderRound = (round: CompactRound): void => {
|
|
586
|
-
const
|
|
867
|
+
const messages = roundMessages(round);
|
|
868
|
+
const stopStatus = roundStopStatus(messages);
|
|
869
|
+
if (stopStatus) endRound(round);
|
|
870
|
+
// Running 时每次 render 重算时长(含挂钟下限);结束后固定。
|
|
871
|
+
const getSummary = () => summarize(roundMessages(round), round.active, round);
|
|
872
|
+
const summary = getSummary();
|
|
587
873
|
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
588
874
|
round.suppressedToolIds.clear();
|
|
589
875
|
|
|
@@ -633,20 +919,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
633
919
|
render(width: number): string[] {
|
|
634
920
|
const theme = themeOf();
|
|
635
921
|
const box = expandedToolCard(theme);
|
|
636
|
-
box.addChild(
|
|
637
|
-
compactAssistantLineComponent(round.anchor, summary, deps.query, {
|
|
638
|
-
hint: false,
|
|
639
|
-
leadingBlank: false,
|
|
640
|
-
pad: 0,
|
|
641
|
-
}),
|
|
642
|
-
);
|
|
643
922
|
for (const child of assistantChildren) box.addChild(child);
|
|
644
923
|
const ids = roundToolCallIds(round);
|
|
645
924
|
for (const tool of trackedToolComponents) {
|
|
646
925
|
if (!ids.has(tool.toolCallId) || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? ""))) {
|
|
647
926
|
continue;
|
|
648
927
|
}
|
|
649
|
-
box.
|
|
928
|
+
box.addToolChild({
|
|
650
929
|
render: (innerWidth: number) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
651
930
|
invalidate: () => tool.invalidate?.(),
|
|
652
931
|
});
|
|
@@ -657,13 +936,18 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
657
936
|
for (const child of assistantChildren) child.invalidate?.();
|
|
658
937
|
},
|
|
659
938
|
});
|
|
939
|
+
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
940
|
+
appendStopStatus(round.anchor, stopStatus);
|
|
660
941
|
return;
|
|
661
942
|
}
|
|
662
943
|
|
|
663
944
|
for (const [component, message] of round.messages) {
|
|
664
945
|
if (component === round.anchor) {
|
|
665
946
|
renderAssistantWithoutThinking(component, message);
|
|
666
|
-
|
|
947
|
+
// 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)
|
|
948
|
+
if (summary || round.active) compactAssistantLine(component, getSummary, deps.query);
|
|
949
|
+
// 折叠时工具行被隐藏:abort/error/length 必须挂在摘要外层。
|
|
950
|
+
appendStopStatus(component, stopStatus);
|
|
667
951
|
} else {
|
|
668
952
|
component.contentContainer?.clear?.();
|
|
669
953
|
}
|
|
@@ -672,24 +956,21 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
672
956
|
|
|
673
957
|
const activateRound = (round: CompactRound): void => {
|
|
674
958
|
round.active = true;
|
|
959
|
+
if (!round.startedAt) round.startedAt = Date.now();
|
|
960
|
+
delete round.endedAt;
|
|
675
961
|
activeRound = round;
|
|
676
962
|
deps.query?.setCompactSummaryActive?.(true);
|
|
963
|
+
ensureRoundTick();
|
|
677
964
|
};
|
|
678
965
|
|
|
679
|
-
const finishRound = (round: CompactRound): void =>
|
|
680
|
-
round.active = false;
|
|
681
|
-
renderRound(round);
|
|
682
|
-
if (activeRound === round) {
|
|
683
|
-
activeRound = undefined;
|
|
684
|
-
deps.query?.setCompactSummaryActive?.(false);
|
|
685
|
-
}
|
|
686
|
-
};
|
|
966
|
+
const finishRound = (round: CompactRound): void => endRound(round, true);
|
|
687
967
|
|
|
688
968
|
const resetRounds = (): void => {
|
|
689
969
|
activeRound = undefined;
|
|
690
970
|
roundByComponent = new WeakMap();
|
|
691
971
|
expandedRoundToolIds.clear();
|
|
692
972
|
deps.query?.setCompactSummaryActive?.(false);
|
|
973
|
+
stopRoundTick();
|
|
693
974
|
};
|
|
694
975
|
|
|
695
976
|
patch.assistantInstalled = function (this: any, message: any) {
|
|
@@ -724,6 +1005,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
724
1005
|
detachedMessages: [],
|
|
725
1006
|
active: true,
|
|
726
1007
|
suppressedToolIds: new Set(),
|
|
1008
|
+
startedAt: Date.now(),
|
|
727
1009
|
};
|
|
728
1010
|
roundByComponent.set(this, round);
|
|
729
1011
|
activateRound(round);
|
|
@@ -734,6 +1016,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
734
1016
|
detachedMessages: [],
|
|
735
1017
|
active: true,
|
|
736
1018
|
suppressedToolIds: new Set(),
|
|
1019
|
+
startedAt: Date.now(),
|
|
737
1020
|
};
|
|
738
1021
|
roundByComponent.set(this, round);
|
|
739
1022
|
if (!activeRound) activateRound(round);
|
|
@@ -756,12 +1039,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
756
1039
|
return renderAssistantWithoutThinking(this, message);
|
|
757
1040
|
}
|
|
758
1041
|
if (round) {
|
|
759
|
-
round
|
|
1042
|
+
endRound(round);
|
|
760
1043
|
roundByComponent.delete(this);
|
|
761
|
-
if (activeRound === round) {
|
|
762
|
-
activeRound = undefined;
|
|
763
|
-
deps.query?.setCompactSummaryActive?.(false);
|
|
764
|
-
}
|
|
765
1044
|
return passThroughAssistant(this, message);
|
|
766
1045
|
}
|
|
767
1046
|
if (activeRound) finishRound(activeRound);
|
|
@@ -778,6 +1057,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
778
1057
|
detachedMessages: [],
|
|
779
1058
|
active: true,
|
|
780
1059
|
suppressedToolIds: new Set(),
|
|
1060
|
+
startedAt: Date.now(),
|
|
781
1061
|
};
|
|
782
1062
|
roundByComponent.set(this, round);
|
|
783
1063
|
if (!activeRound) activateRound(round);
|
|
@@ -787,6 +1067,15 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
787
1067
|
return undefined;
|
|
788
1068
|
}
|
|
789
1069
|
|
|
1070
|
+
// 无可见内容的 abort/error/length:直接外层状态行,并结束进行中的回合。
|
|
1071
|
+
const loneStatus = messageStopStatus(message);
|
|
1072
|
+
if (loneStatus) {
|
|
1073
|
+
if (activeRound) finishRound(activeRound);
|
|
1074
|
+
self.contentContainer.clear();
|
|
1075
|
+
appendStopStatus(this, loneStatus);
|
|
1076
|
+
return undefined;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
790
1079
|
self.contentContainer.clear();
|
|
791
1080
|
return undefined;
|
|
792
1081
|
};
|
|
@@ -802,6 +1091,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
802
1091
|
if (this.expanded !== true) return lines;
|
|
803
1092
|
return compactEditWriteExpandedLines(this, width, deps.writeMetadata);
|
|
804
1093
|
}
|
|
1094
|
+
// Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
|
|
805
1095
|
if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
806
1096
|
// 普通工具折叠时不显示独立行(摘要行已统计),独立展开走原 renderer。
|
|
807
1097
|
if (this.expanded === true) {
|
|
@@ -825,12 +1115,18 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
825
1115
|
};
|
|
826
1116
|
|
|
827
1117
|
patch.assertAssistantOwnership = () => {
|
|
828
|
-
//
|
|
1118
|
+
// compact 必须在外层:从 compact-thinking 包装器认领,或收回 mode=on/off
|
|
1119
|
+
// 时主动释放给 original/native 的所有权。未知外部包装器不抢,避免递归。
|
|
829
1120
|
if (!patch.active || assistantPrototype.updateContent === patch.assistantInstalled) return;
|
|
830
1121
|
const current = assistantPrototype.updateContent;
|
|
831
|
-
if ((current as any)[COMPACT_THINKING_PATCH_KEY]
|
|
832
|
-
|
|
833
|
-
|
|
1122
|
+
if ((current as any)[COMPACT_THINKING_PATCH_KEY] === true) {
|
|
1123
|
+
patch.assistantOriginal = current;
|
|
1124
|
+
assistantPrototype.updateContent = patch.assistantInstalled;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (current === patch.assistantOriginal || current === patch.assistantNative) {
|
|
1128
|
+
assistantPrototype.updateContent = patch.assistantInstalled;
|
|
1129
|
+
}
|
|
834
1130
|
};
|
|
835
1131
|
|
|
836
1132
|
(patch.assistantInstalled as any)[PROTOTYPE_ORIGINAL_KEY] = patch.assistantNative;
|
|
@@ -871,31 +1167,23 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
871
1167
|
for (const component of trackedToolComponents) component.expanded = globalExpanded;
|
|
872
1168
|
};
|
|
873
1169
|
|
|
874
|
-
const releaseAssistantOwnership = (): void => {
|
|
875
|
-
if (assistantPrototype.updateContent === patch.assistantInstalled) {
|
|
876
|
-
assistantPrototype.updateContent = patch.assistantOriginal;
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
|
|
880
1170
|
return {
|
|
881
1171
|
sync(ctx: any) {
|
|
882
1172
|
if (!patch.active) return;
|
|
1173
|
+
uiRef = ctx?.ui;
|
|
883
1174
|
resetRounds();
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
1175
|
+
// 始终保持补丁在链上:mode=on/off 走 passThrough,仍写入 lastMessage
|
|
1176
|
+
// 与 tracked 集合,这样 /ccstyle 切回 compact 时不必 /reload。
|
|
1177
|
+
patch.assertAssistantOwnership();
|
|
1178
|
+
if (config.mode === "compact") syncGlobalExpanded(ctx);
|
|
1179
|
+
else hoveredAssistantComponent = undefined;
|
|
890
1180
|
refreshTrackedComponents();
|
|
891
1181
|
},
|
|
892
1182
|
refresh() {
|
|
893
1183
|
if (!patch.active) return;
|
|
894
1184
|
resetRounds();
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
hoveredAssistantComponent = undefined;
|
|
898
|
-
}
|
|
1185
|
+
patch.assertAssistantOwnership();
|
|
1186
|
+
if (config.mode !== "compact") hoveredAssistantComponent = undefined;
|
|
899
1187
|
refreshTrackedComponents();
|
|
900
1188
|
},
|
|
901
1189
|
assertOwnership() {
|
|
@@ -927,15 +1215,15 @@ function refreshTrackedComponents(): void {
|
|
|
927
1215
|
for (const component of [...trackedAssistantComponents]) {
|
|
928
1216
|
try {
|
|
929
1217
|
if (config.mode !== "compact") detachAssistantExpansion(component);
|
|
930
|
-
|
|
931
|
-
else component.invalidate?.();
|
|
1218
|
+
refreshTranscriptComponent(component);
|
|
932
1219
|
} catch {
|
|
933
1220
|
trackedAssistantComponents.delete(component);
|
|
934
1221
|
}
|
|
935
1222
|
}
|
|
936
1223
|
for (const component of [...trackedToolComponents]) {
|
|
937
1224
|
try {
|
|
938
|
-
|
|
1225
|
+
// 共享实现负责 updateDisplay;invalidate 属于 compact-mode 的跟踪语义。
|
|
1226
|
+
refreshTranscriptComponent(component);
|
|
939
1227
|
component.invalidate?.();
|
|
940
1228
|
} catch {
|
|
941
1229
|
trackedToolComponents.delete(component);
|