pi-cc-extensions 0.8.50 → 0.8.52
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 +9 -12
- package/README.md +9 -12
- package/extensions/config/config.ts +10 -10
- package/extensions/config/panel.ts +113 -26
- package/extensions/feature/agent-autocomplete.ts +3 -1
- package/extensions/feature/agent-summary.ts +8 -8
- package/extensions/feature/compact-thinking.ts +116 -55
- package/extensions/feature/context.ts +1 -1
- package/extensions/feature/markdown-enhance.ts +4 -1
- package/extensions/feature/pi-startup-header.ts +3 -8
- package/extensions/feature/session-reference-core.ts +3 -2
- package/extensions/feature/session-reference.ts +49 -21
- package/extensions/index.ts +12 -0
- package/extensions/renderer/compact-mode.ts +949 -0
- package/extensions/renderer/index.ts +73 -17
- package/extensions/renderer/message-display.ts +17 -3
- package/extensions/renderer/mouse-hover.ts +130 -0
- package/extensions/renderer/mouse-interaction.ts +74 -532
- package/extensions/renderer/mouse-layout.ts +160 -0
- package/extensions/renderer/mouse-packets.ts +143 -0
- package/extensions/renderer/mouse-scroll.ts +184 -0
- package/extensions/renderer/show-more-hint.ts +1 -3
- package/extensions/renderer/tool-diff/diff-renderer.ts +59 -2
- package/extensions/renderer/tool-grouping.ts +9 -2
- package/extensions/renderer/tool-result.ts +24 -8
- package/extensions/utils/agent-summary.ts +16 -2
- package/package.json +3 -2
- package/themes/cc-dark.json +3 -2
- package/themes/cc-light.json +3 -3
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact mode:每条含 toolCall 的 assistant message 折叠为一条逐步累加的摘要行
|
|
3
|
+
* (`Thought for 8s, bash×2, read×2`),edit/write 独立单行(`✓ write <path> (+25 -0)`),
|
|
4
|
+
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)在单个工具卡中恢复
|
|
5
|
+
* compact-thinking/Pi 原生或专用 renderer。edit/write 折叠时显示统计,展开时显示 rich diff。
|
|
6
|
+
*
|
|
7
|
+
* 统计口径与 utils/agent-summary.ts 对齐:read 按非空路径去重、bash 按调用计数、
|
|
8
|
+
* 其他工具按首次出现顺序计数;edit/write 不进入摘要。思考时长复用
|
|
9
|
+
* compact-thinking 的 completedDurations/activeThinking 与持久化 entry(只读查询),
|
|
10
|
+
* 不建立第二套计时器。最终 agent summary 仍由 feature/agent-summary.ts 独占。
|
|
11
|
+
*
|
|
12
|
+
* 补丁生命周期遵循仓库既有模式:Symbol 所有权、dispose 仅恢复仍由本安装持有的
|
|
13
|
+
* 方法、重入守卫防止 /reload 后残留闭包递归。
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
AssistantMessageComponent,
|
|
17
|
+
ToolExecutionComponent,
|
|
18
|
+
type Theme,
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import * as PiTui from "@earendil-works/pi-tui";
|
|
21
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
22
|
+
import { config, getToolDisplayConfig } from "../config/config.ts";
|
|
23
|
+
import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
|
|
24
|
+
import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
|
|
25
|
+
import { getMessageDisplayTheme } from "./message-display.ts";
|
|
26
|
+
import { showMoreHintText } from "./show-more-hint.ts";
|
|
27
|
+
import {
|
|
28
|
+
countEditDiffStats,
|
|
29
|
+
countWriteDiffStats,
|
|
30
|
+
isRichDiffComponent,
|
|
31
|
+
} from "./tool-diff/diff-renderer.ts";
|
|
32
|
+
import { renderRichToolResult } from "./tool-diff/index.ts";
|
|
33
|
+
import type { WriteExecutionMetadataStore } from "./tool-diff/write-execution.ts";
|
|
34
|
+
import {
|
|
35
|
+
insetComponent,
|
|
36
|
+
oneLine,
|
|
37
|
+
renderExpandedToolResult,
|
|
38
|
+
scheduleAnimation,
|
|
39
|
+
} from "./tool-result.ts";
|
|
40
|
+
|
|
41
|
+
/** compact 渲染层对 compact-thinking 的只读查询面(不建第二套计时器)。 */
|
|
42
|
+
export type CompactThinkingQuery = {
|
|
43
|
+
getMessageThinkingDurationMs(messageTimestamp: number): number | undefined;
|
|
44
|
+
isMessageThinkingActive?(messageTimestamp: number): boolean;
|
|
45
|
+
getThinkingAnimationFrame?(): number;
|
|
46
|
+
setCompactSummaryActive?(active: boolean): void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const COMPACT_MODE_PATCH_KEY = Symbol.for("pi.ccstyle.compact-mode-patch");
|
|
50
|
+
const COMPACT_THINKING_PATCH_KEY = Symbol.for("pi.ccstyle.compact-thinking-update");
|
|
51
|
+
const PROTOTYPE_ORIGINAL_KEY = Symbol.for("pi.ccstyle.prototype-original");
|
|
52
|
+
const ASSISTANT_SET_EXPANDED_DESCRIPTION = "pi.ccstyle.compact-assistant-set-expanded";
|
|
53
|
+
const ASSISTANT_TOGGLE_ROUND_DESCRIPTION = "pi.ccstyle.compact-assistant-toggle-round";
|
|
54
|
+
const ASSISTANT_REENTRY_DESCRIPTION = "pi.ccstyle.compact-assistant-reentry";
|
|
55
|
+
const ASSISTANT_SET_EXPANDED_KEY = Symbol.for(ASSISTANT_SET_EXPANDED_DESCRIPTION);
|
|
56
|
+
const ASSISTANT_TOGGLE_ROUND_KEY = Symbol.for(ASSISTANT_TOGGLE_ROUND_DESCRIPTION);
|
|
57
|
+
const ASSISTANT_REENTRY_KEY = Symbol.for(ASSISTANT_REENTRY_DESCRIPTION);
|
|
58
|
+
|
|
59
|
+
const Box = (PiTui as any).Box;
|
|
60
|
+
const EDIT_WRITE_TOOLS = new Set(["edit", "write"]);
|
|
61
|
+
|
|
62
|
+
type CompactThinkingTheme = Pick<Theme, "fg" | "italic" | "bold">;
|
|
63
|
+
|
|
64
|
+
/** 与 compact-thinking 主渲染器共用的静态文字样式。 */
|
|
65
|
+
export function styleCompactThinkingText(
|
|
66
|
+
text: string,
|
|
67
|
+
theme: CompactThinkingTheme | undefined,
|
|
68
|
+
bold = false,
|
|
69
|
+
): string {
|
|
70
|
+
if (!theme) return text;
|
|
71
|
+
const colored = typeof theme.fg === "function" ? theme.fg("thinkingText", text) : text;
|
|
72
|
+
const weighted = bold && typeof theme.bold === "function" ? theme.bold(colored) : colored;
|
|
73
|
+
return typeof theme.italic === "function" ? theme.italic(weighted) : weighted;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 与 compact-thinking 主渲染器共用的活动思考扫光动画。 */
|
|
77
|
+
export function animateCompactThinkingText(
|
|
78
|
+
text: string,
|
|
79
|
+
theme: CompactThinkingTheme | undefined,
|
|
80
|
+
animationFrame: number,
|
|
81
|
+
boldBase = false,
|
|
82
|
+
): string {
|
|
83
|
+
if (!theme) return text;
|
|
84
|
+
const characters = Array.from(text);
|
|
85
|
+
if (characters.length === 0) return "";
|
|
86
|
+
const highlightWidth = Math.max(1, Math.min(5, Math.ceil(characters.length * 0.28)));
|
|
87
|
+
const start = (animationFrame % (characters.length + highlightWidth)) - highlightWidth;
|
|
88
|
+
const end = start + highlightWidth;
|
|
89
|
+
const before = characters.slice(0, Math.max(0, start)).join("");
|
|
90
|
+
const highlighted = characters
|
|
91
|
+
.slice(Math.max(0, start), Math.min(characters.length, end))
|
|
92
|
+
.join("");
|
|
93
|
+
const after = characters.slice(Math.max(0, end)).join("");
|
|
94
|
+
const highlightedColored =
|
|
95
|
+
highlighted && typeof theme.fg === "function" ? theme.fg("text", highlighted) : highlighted;
|
|
96
|
+
const highlightedWeighted =
|
|
97
|
+
highlightedColored && typeof theme.bold === "function"
|
|
98
|
+
? theme.bold(highlightedColored)
|
|
99
|
+
: highlightedColored;
|
|
100
|
+
const highlightedText =
|
|
101
|
+
highlightedWeighted && typeof theme.italic === "function"
|
|
102
|
+
? theme.italic(highlightedWeighted)
|
|
103
|
+
: highlightedWeighted;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
styleCompactThinkingText(before, theme, boldBase) +
|
|
107
|
+
highlightedText +
|
|
108
|
+
styleCompactThinkingText(after, theme, boldBase)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatThoughtDuration(durationMs: number) {
|
|
113
|
+
if (durationMs < 1_000) {
|
|
114
|
+
return `${Math.max(1, Math.round(durationMs))}ms`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const totalSeconds = Math.max(1, Math.round(durationMs / 1_000));
|
|
118
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
119
|
+
const seconds = totalSeconds % 60;
|
|
120
|
+
if (minutes === 0) return `${seconds}s`;
|
|
121
|
+
return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { formatThoughtDuration };
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 逐条 assistant message 的摘要文本(无工具计数时可为空串):
|
|
128
|
+
* 思考时长在前,工具按消息内首次出现顺序排列;read 按非空路径去重。
|
|
129
|
+
*/
|
|
130
|
+
function buildMessagesSummary(
|
|
131
|
+
messages: Iterable<any>,
|
|
132
|
+
query?: CompactThinkingQuery,
|
|
133
|
+
thinkingActiveOverride?: boolean,
|
|
134
|
+
): string {
|
|
135
|
+
const parts: string[] = [];
|
|
136
|
+
const counts = new Map<string, number>();
|
|
137
|
+
const readPaths = new Set<string>();
|
|
138
|
+
const durationTimestamps = new Set<number>();
|
|
139
|
+
let durationMs = 0;
|
|
140
|
+
let thinkingActive = false;
|
|
141
|
+
|
|
142
|
+
for (const message of messages) {
|
|
143
|
+
if (typeof message?.timestamp === "number" && !durationTimestamps.has(message.timestamp)) {
|
|
144
|
+
durationTimestamps.add(message.timestamp);
|
|
145
|
+
if (query?.isMessageThinkingActive?.(message.timestamp)) thinkingActive = true;
|
|
146
|
+
const value = query?.getMessageThinkingDurationMs(message.timestamp);
|
|
147
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) durationMs += value;
|
|
148
|
+
}
|
|
149
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
150
|
+
for (const item of content) {
|
|
151
|
+
if (item?.type !== "toolCall") continue;
|
|
152
|
+
const rawName = typeof item.name === "string" ? item.name : "tool";
|
|
153
|
+
if (EDIT_WRITE_TOOLS.has(rawName)) continue;
|
|
154
|
+
const name = sanitizeToolResultText(rawName);
|
|
155
|
+
if (rawName.split(".").pop() === "read") {
|
|
156
|
+
const args = item.arguments ?? item.args ?? {};
|
|
157
|
+
const path = args.path ?? args.file_path ?? args.file;
|
|
158
|
+
if (typeof path === "string" && path.length > 0) {
|
|
159
|
+
if (readPaths.has(path)) continue;
|
|
160
|
+
readPaths.add(path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
thinkingActive = thinkingActiveOverride ?? thinkingActive;
|
|
168
|
+
if (thinkingActive) {
|
|
169
|
+
parts.push(
|
|
170
|
+
durationMs > 0 ? `Thinking... · ${formatThoughtDuration(durationMs)}` : "Thinking...",
|
|
171
|
+
);
|
|
172
|
+
} else if (durationMs > 0) parts.push(`Thought for ${formatThoughtDuration(durationMs)}`);
|
|
173
|
+
for (const [name, count] of counts) parts.push(`${name}×${count}`);
|
|
174
|
+
return parts.join(", ");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function buildMessageSummary(message: any, query?: CompactThinkingQuery): string {
|
|
178
|
+
return buildMessagesSummary([message], query);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fallbackTheme(): any {
|
|
182
|
+
return {
|
|
183
|
+
fg: (_color: string, text: string) => text,
|
|
184
|
+
italic: (text: string) => text,
|
|
185
|
+
bold: (text: string) => text,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function themeOf(): any {
|
|
190
|
+
return getMessageDisplayTheme() ?? fallbackTheme();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function expandedToolCard(theme: any): any {
|
|
194
|
+
return new Box(
|
|
195
|
+
1,
|
|
196
|
+
1,
|
|
197
|
+
typeof theme.bg === "function" ? (text: string) => theme.bg("userMessageBg", text) : undefined,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isAssistantComponent(value: any): boolean {
|
|
202
|
+
return value instanceof AssistantMessageComponent;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isToolComponent(value: any): boolean {
|
|
206
|
+
return value instanceof ToolExecutionComponent;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function detachAssistantExpansion(component: any): void {
|
|
210
|
+
let owned =
|
|
211
|
+
typeof component?.[ASSISTANT_SET_EXPANDED_KEY] === "function" &&
|
|
212
|
+
component.setExpanded === component[ASSISTANT_SET_EXPANDED_KEY];
|
|
213
|
+
if (!owned && component && typeof component === "object") {
|
|
214
|
+
// 首次升级时清理旧模块使用 Symbol() 留下的实例方法。
|
|
215
|
+
owned = Object.getOwnPropertySymbols(component).some(
|
|
216
|
+
(symbol) =>
|
|
217
|
+
symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION && component[symbol] === true,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!owned) return;
|
|
221
|
+
|
|
222
|
+
delete component.setExpanded;
|
|
223
|
+
for (const symbol of Object.getOwnPropertySymbols(component)) {
|
|
224
|
+
if (
|
|
225
|
+
symbol.description === ASSISTANT_SET_EXPANDED_DESCRIPTION ||
|
|
226
|
+
symbol.description === ASSISTANT_TOGGLE_ROUND_DESCRIPTION ||
|
|
227
|
+
symbol.description === ASSISTANT_REENTRY_DESCRIPTION
|
|
228
|
+
) {
|
|
229
|
+
delete component[symbol];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 供 mouse-interaction 识别可点击的 compact assistant 行(仅 compact 模式下生效)。 */
|
|
235
|
+
export function isCompactAssistantComponent(value: unknown): boolean {
|
|
236
|
+
if (config.mode !== "compact" || !value || typeof value !== "object") return false;
|
|
237
|
+
const component = value as any;
|
|
238
|
+
return (
|
|
239
|
+
typeof component[ASSISTANT_SET_EXPANDED_KEY] === "function" &&
|
|
240
|
+
component.setExpanded === component[ASSISTANT_SET_EXPANDED_KEY]
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export type CompactModeHooks = {
|
|
245
|
+
/** 会话事件后同步:所有权、全局展开状态、已挂载组件。 */
|
|
246
|
+
sync(ctx: any): void;
|
|
247
|
+
/** 重绘所有被跟踪的 assistant/tool 组件(模式切换用)。 */
|
|
248
|
+
refresh(): void;
|
|
249
|
+
/** compact 模式下重新认领 assistant patch(位于 compact-thinking 之上)。 */
|
|
250
|
+
assertOwnership(): void;
|
|
251
|
+
/** 重新渲染包含指定 toolCallId 的 assistant 消息(思考收尾时刷新时长)。 */
|
|
252
|
+
refreshToolCallMessage(toolCallId: string | undefined): void;
|
|
253
|
+
shutdown(): void;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
type CompactModeInstallDeps = {
|
|
257
|
+
query?: CompactThinkingQuery;
|
|
258
|
+
writeMetadata: WriteExecutionMetadataStore;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
type CompactModePatch = {
|
|
262
|
+
active: boolean;
|
|
263
|
+
prototype: any;
|
|
264
|
+
assistantInstalled: (...args: any[]) => any;
|
|
265
|
+
assistantOriginal: (...args: any[]) => any;
|
|
266
|
+
assistantNative: (...args: any[]) => any;
|
|
267
|
+
toolInstalledRender: (width: number) => string[];
|
|
268
|
+
toolInstalledUpdateDisplay: () => void;
|
|
269
|
+
toolOriginalRender: (width: number) => string[];
|
|
270
|
+
toolOriginalUpdateDisplay: () => void;
|
|
271
|
+
assertAssistantOwnership: () => void;
|
|
272
|
+
dispose: () => void;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const trackedAssistantComponents = new Set<any>();
|
|
276
|
+
const trackedToolComponents = new Set<any>();
|
|
277
|
+
let hoveredAssistantComponent: any;
|
|
278
|
+
|
|
279
|
+
export function setHoveredCompactAssistant(component: any): boolean {
|
|
280
|
+
if (hoveredAssistantComponent === component) return false;
|
|
281
|
+
hoveredAssistantComponent = component;
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function compactEditWriteLine(
|
|
286
|
+
component: any,
|
|
287
|
+
width: number,
|
|
288
|
+
writeMetadata?: WriteExecutionMetadataStore,
|
|
289
|
+
): string[] {
|
|
290
|
+
const theme = themeOf();
|
|
291
|
+
const name = String(component.toolName ?? "tool");
|
|
292
|
+
const args = component.args ?? {};
|
|
293
|
+
const path = sanitizeToolResultText(
|
|
294
|
+
typeof args.path === "string" && args.path
|
|
295
|
+
? args.path
|
|
296
|
+
: typeof args.file_path === "string" && args.file_path
|
|
297
|
+
? args.file_path
|
|
298
|
+
: "",
|
|
299
|
+
);
|
|
300
|
+
const isError = component.result?.isError === true;
|
|
301
|
+
const isPending = !component.result || component.isPartial === true;
|
|
302
|
+
const icon = isError ? "✗" : isPending ? toolLoadingIcon() : "✓";
|
|
303
|
+
const iconColor = isError ? "error" : isPending ? "accent" : "success";
|
|
304
|
+
let statsText = "";
|
|
305
|
+
let statsStyled = "";
|
|
306
|
+
if (!isError && !isPending) {
|
|
307
|
+
const stats =
|
|
308
|
+
name === "edit"
|
|
309
|
+
? countEditDiffStats(component.result?.details)
|
|
310
|
+
: name === "write"
|
|
311
|
+
? countWriteDiffStats(
|
|
312
|
+
typeof args.content === "string" ? args.content : undefined,
|
|
313
|
+
writeMetadata?.get(component.toolCallId)?.previousContent,
|
|
314
|
+
writeMetadata?.get(component.toolCallId)?.fileExistedBeforeWrite,
|
|
315
|
+
)
|
|
316
|
+
: undefined;
|
|
317
|
+
if (stats) {
|
|
318
|
+
statsText = ` (+${stats.added} -${stats.removed})`;
|
|
319
|
+
statsStyled = ` ${theme.fg("dim", "(")}${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}${theme.fg("dim", ")")}`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const iconPart = ` ${theme.fg(iconColor, icon)} `;
|
|
323
|
+
const namePart = theme.fg("toolTitle", name);
|
|
324
|
+
const hintText = component.expanded ? "" : ` • ${showMoreHintText()}`;
|
|
325
|
+
const fixedWidth =
|
|
326
|
+
visibleWidth(iconPart) +
|
|
327
|
+
visibleWidth(namePart) +
|
|
328
|
+
visibleWidth(statsText) +
|
|
329
|
+
visibleWidth(hintText);
|
|
330
|
+
const pathWidth = Math.max(0, width - fixedWidth - (path ? 1 : 0));
|
|
331
|
+
const pathPart = pathWidth > 0 && path ? ` ${oneLine(path, pathWidth)}` : "";
|
|
332
|
+
const line = `${iconPart}${namePart}${theme.fg("toolTitle", pathPart)}${statsStyled}${hintText ? theme.fg("dim", hintText) : ""}`;
|
|
333
|
+
return ["", truncateToWidth(line, width, "")];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** compact edit/write 展开:复用 mode=on 的 rich diff;不可用时回退 Input/Output。 */
|
|
337
|
+
function compactEditWriteExpandedLines(
|
|
338
|
+
component: any,
|
|
339
|
+
width: number,
|
|
340
|
+
writeMetadata?: WriteExecutionMetadataStore,
|
|
341
|
+
): string[] {
|
|
342
|
+
const theme = themeOf();
|
|
343
|
+
const result = component.result;
|
|
344
|
+
const isError = result?.isError === true;
|
|
345
|
+
const candidate = writeMetadata
|
|
346
|
+
? renderRichToolResult(
|
|
347
|
+
String(component.toolName ?? ""),
|
|
348
|
+
result,
|
|
349
|
+
{
|
|
350
|
+
expanded: true,
|
|
351
|
+
isPartial: component.isPartial === true,
|
|
352
|
+
isError,
|
|
353
|
+
},
|
|
354
|
+
theme,
|
|
355
|
+
component,
|
|
356
|
+
writeMetadata,
|
|
357
|
+
getToolDisplayConfig,
|
|
358
|
+
)
|
|
359
|
+
: undefined;
|
|
360
|
+
let detail: any;
|
|
361
|
+
if (isRichDiffComponent(candidate)) {
|
|
362
|
+
component.resultRendererComponent = candidate;
|
|
363
|
+
detail = insetComponent(candidate as any);
|
|
364
|
+
} else {
|
|
365
|
+
const outputText = sanitizeToolResultText(
|
|
366
|
+
Array.isArray(result?.content)
|
|
367
|
+
? result.content
|
|
368
|
+
.filter((item: any) => item?.type === "text")
|
|
369
|
+
.map((item: any) => String(item.text ?? ""))
|
|
370
|
+
.join("\n")
|
|
371
|
+
: "",
|
|
372
|
+
);
|
|
373
|
+
const state = (component.state ??= {});
|
|
374
|
+
detail = renderExpandedToolResult(
|
|
375
|
+
outputText,
|
|
376
|
+
theme,
|
|
377
|
+
isError,
|
|
378
|
+
state.ccstyleIoView,
|
|
379
|
+
component.args,
|
|
380
|
+
component,
|
|
381
|
+
);
|
|
382
|
+
component.resultRendererComponent = detail;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const box = expandedToolCard(theme);
|
|
386
|
+
box.addChild({
|
|
387
|
+
render(innerWidth: number): string[] {
|
|
388
|
+
return compactEditWriteLine(component, innerWidth, writeMetadata).slice(1);
|
|
389
|
+
},
|
|
390
|
+
invalidate() {},
|
|
391
|
+
});
|
|
392
|
+
box.addChild(detail);
|
|
393
|
+
return box.render(width);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function compactAssistantLineComponent(
|
|
397
|
+
component: any,
|
|
398
|
+
summary: string,
|
|
399
|
+
query?: CompactThinkingQuery,
|
|
400
|
+
options: { hint?: boolean; leadingBlank?: boolean; pad?: number } = {},
|
|
401
|
+
): any {
|
|
402
|
+
const self = component as any;
|
|
403
|
+
return {
|
|
404
|
+
render(width: number): string[] {
|
|
405
|
+
const theme = themeOf();
|
|
406
|
+
const pad = Math.max(0, options.pad ?? (Number(self.outputPad) || 0));
|
|
407
|
+
const available = Math.max(0, width - pad);
|
|
408
|
+
const hintText = options.hint === false ? "" : ` • ${showMoreHintText()}`;
|
|
409
|
+
const summaryWidth = Math.max(0, available - visibleWidth(hintText));
|
|
410
|
+
const thinkingActive = summary.startsWith("Thinking...");
|
|
411
|
+
const displaySummary = thinkingActive
|
|
412
|
+
? `${self.hiddenThinkingLabel || "Thinking..."}${summary.slice("Thinking...".length)}`
|
|
413
|
+
: summary;
|
|
414
|
+
const plainText = truncateToWidth(displaySummary, summaryWidth, "…");
|
|
415
|
+
let text = theme.fg("muted", plainText);
|
|
416
|
+
if (thinkingActive || plainText.startsWith("Thought for ")) {
|
|
417
|
+
const separator = plainText.indexOf(", ");
|
|
418
|
+
const heading = separator < 0 ? plainText : plainText.slice(0, separator);
|
|
419
|
+
const tools = separator < 0 ? "" : plainText.slice(separator);
|
|
420
|
+
if (thinkingActive) {
|
|
421
|
+
const durationSeparator = heading.indexOf(" · ");
|
|
422
|
+
const label = durationSeparator < 0 ? heading : heading.slice(0, durationSeparator);
|
|
423
|
+
const duration = durationSeparator < 0 ? "" : heading.slice(durationSeparator);
|
|
424
|
+
text = `${animateCompactThinkingText(
|
|
425
|
+
label,
|
|
426
|
+
theme,
|
|
427
|
+
query?.getThinkingAnimationFrame?.() ?? 0,
|
|
428
|
+
)}${styleCompactThinkingText(duration, theme)}${theme.fg("muted", tools)}`;
|
|
429
|
+
} else {
|
|
430
|
+
text = `${styleCompactThinkingText(heading, theme)}${theme.fg("muted", tools)}`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const hintColor = hoveredAssistantComponent === component ? "text" : "dim";
|
|
434
|
+
const line = `${text}${hintText ? theme.fg(hintColor, hintText) : ""}`;
|
|
435
|
+
const rendered = `${" ".repeat(pad)}${truncateToWidth(line, available, "")}`;
|
|
436
|
+
return options.leadingBlank === false ? [rendered] : ["", rendered];
|
|
437
|
+
},
|
|
438
|
+
invalidate() {},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function compactAssistantLine(component: any, summary: string, query?: CompactThinkingQuery): void {
|
|
443
|
+
if (!summary) return;
|
|
444
|
+
component.contentContainer.addChild(compactAssistantLineComponent(component, summary, query));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function ensureAssistantSetExpanded(component: any): void {
|
|
448
|
+
if (
|
|
449
|
+
typeof component[ASSISTANT_SET_EXPANDED_KEY] === "function" &&
|
|
450
|
+
component.setExpanded === component[ASSISTANT_SET_EXPANDED_KEY]
|
|
451
|
+
) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (typeof component.setExpanded === "function") {
|
|
455
|
+
detachAssistantExpansion(component);
|
|
456
|
+
if (typeof component.setExpanded === "function") return;
|
|
457
|
+
}
|
|
458
|
+
const installed = function (this: any, expanded: boolean) {
|
|
459
|
+
if (config.mode !== "compact") {
|
|
460
|
+
detachAssistantExpansion(this);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const toggleRound = this[ASSISTANT_TOGGLE_ROUND_KEY];
|
|
464
|
+
if (typeof toggleRound === "function") {
|
|
465
|
+
toggleRound(expanded);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.expanded = expanded;
|
|
469
|
+
if (typeof this.updateContent === "function" && this.lastMessage) {
|
|
470
|
+
this.updateContent(this.lastMessage);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
component[ASSISTANT_SET_EXPANDED_KEY] = installed;
|
|
474
|
+
component.setExpanded = installed;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function collectMountedComponents(root: any): void {
|
|
478
|
+
if (!root || typeof root !== "object") return;
|
|
479
|
+
const seen = new Set<any>();
|
|
480
|
+
const assistants = new Set<any>();
|
|
481
|
+
const tools = new Set<any>();
|
|
482
|
+
const visit = (value: any): void => {
|
|
483
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
484
|
+
seen.add(value);
|
|
485
|
+
if (Array.isArray(value)) {
|
|
486
|
+
for (const child of value) visit(child);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (isAssistantComponent(value)) {
|
|
490
|
+
assistants.add(value);
|
|
491
|
+
// 仅在 compact 模式给实例装 setExpanded,避免 on/off 下 Ctrl+O/mouse 回归。
|
|
492
|
+
if (config.mode === "compact") ensureAssistantSetExpanded(value);
|
|
493
|
+
} else if (isToolComponent(value)) {
|
|
494
|
+
tools.add(value);
|
|
495
|
+
}
|
|
496
|
+
const children = value.children;
|
|
497
|
+
if (Array.isArray(children)) {
|
|
498
|
+
for (const child of children) visit(child);
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const mounted = value.getMountedRoots?.();
|
|
502
|
+
if (Array.isArray(mounted)) {
|
|
503
|
+
for (const root2 of mounted) visit(root2);
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
// renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
visit(root);
|
|
510
|
+
trackedAssistantComponents.clear();
|
|
511
|
+
trackedToolComponents.clear();
|
|
512
|
+
for (const component of assistants) trackedAssistantComponents.add(component);
|
|
513
|
+
for (const component of tools) trackedToolComponents.add(component);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHooks {
|
|
517
|
+
const host = globalThis as any;
|
|
518
|
+
const previous = host[COMPACT_MODE_PATCH_KEY] as CompactModePatch | undefined;
|
|
519
|
+
if (previous) previous.dispose();
|
|
520
|
+
|
|
521
|
+
const assistantPrototype = AssistantMessageComponent.prototype as any;
|
|
522
|
+
const toolPrototype = ToolExecutionComponent.prototype as any;
|
|
523
|
+
const patch: CompactModePatch = {
|
|
524
|
+
active: true,
|
|
525
|
+
prototype: assistantPrototype,
|
|
526
|
+
assistantInstalled: undefined as any,
|
|
527
|
+
assistantOriginal: assistantPrototype.updateContent,
|
|
528
|
+
assistantNative: assistantPrototype.updateContent,
|
|
529
|
+
toolInstalledRender: undefined as any,
|
|
530
|
+
toolInstalledUpdateDisplay: undefined as any,
|
|
531
|
+
toolOriginalRender: toolPrototype.render,
|
|
532
|
+
toolOriginalUpdateDisplay: toolPrototype.updateDisplay,
|
|
533
|
+
assertAssistantOwnership: () => {},
|
|
534
|
+
dispose: () => {},
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const passThroughAssistant = (component: any, message: any): any => {
|
|
538
|
+
const self = component as any;
|
|
539
|
+
if (self[ASSISTANT_REENTRY_KEY] === patch)
|
|
540
|
+
return patch.assistantNative.call(component, message);
|
|
541
|
+
self[ASSISTANT_REENTRY_KEY] = patch;
|
|
542
|
+
try {
|
|
543
|
+
return patch.assistantOriginal.call(component, message);
|
|
544
|
+
} finally {
|
|
545
|
+
delete self[ASSISTANT_REENTRY_KEY];
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
type CompactRound = {
|
|
550
|
+
anchor: any;
|
|
551
|
+
messages: Map<any, any>;
|
|
552
|
+
detachedMessages: any[];
|
|
553
|
+
active: boolean;
|
|
554
|
+
suppressedToolIds: Set<string>;
|
|
555
|
+
};
|
|
556
|
+
let activeRound: CompactRound | undefined;
|
|
557
|
+
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
558
|
+
const expandedRoundToolIds = new Set<string>();
|
|
559
|
+
|
|
560
|
+
const renderAssistantWithoutThinking = (component: any, message: any): any => {
|
|
561
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
562
|
+
const result = passThroughAssistant(component, {
|
|
563
|
+
...message,
|
|
564
|
+
content: content.filter((item: any) => item?.type !== "thinking"),
|
|
565
|
+
});
|
|
566
|
+
component.lastMessage = message;
|
|
567
|
+
return result;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const roundMessages = (round: CompactRound): any[] => [
|
|
571
|
+
...round.messages.values(),
|
|
572
|
+
...round.detachedMessages,
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
const roundToolCallIds = (round: CompactRound): Set<string> => {
|
|
576
|
+
const ids = new Set<string>();
|
|
577
|
+
for (const message of roundMessages(round)) {
|
|
578
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
579
|
+
if (item?.type === "toolCall" && typeof item.id === "string") ids.add(item.id);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return ids;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const renderRound = (round: CompactRound): void => {
|
|
586
|
+
const summary = buildMessagesSummary(roundMessages(round), deps.query, round.active);
|
|
587
|
+
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
588
|
+
round.suppressedToolIds.clear();
|
|
589
|
+
|
|
590
|
+
for (const [component, message] of round.messages) {
|
|
591
|
+
component.lastMessage = message;
|
|
592
|
+
ensureAssistantSetExpanded(component);
|
|
593
|
+
component[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) => {
|
|
594
|
+
for (const member of round.messages.keys()) member.expanded = expanded;
|
|
595
|
+
if (!expanded) {
|
|
596
|
+
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
597
|
+
round.suppressedToolIds.clear();
|
|
598
|
+
}
|
|
599
|
+
renderRound(round);
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (round.anchor.expanded === true) {
|
|
604
|
+
const assistantChildren: any[] = [];
|
|
605
|
+
for (const [component, message] of round.messages) {
|
|
606
|
+
passThroughAssistant(component, message);
|
|
607
|
+
if (Array.isArray(component.contentContainer?.children)) {
|
|
608
|
+
assistantChildren.push(...component.contentContainer.children);
|
|
609
|
+
}
|
|
610
|
+
component.contentContainer?.clear?.();
|
|
611
|
+
}
|
|
612
|
+
const ids = roundToolCallIds(round);
|
|
613
|
+
for (const id of ids) {
|
|
614
|
+
round.suppressedToolIds.add(id);
|
|
615
|
+
expandedRoundToolIds.add(id);
|
|
616
|
+
}
|
|
617
|
+
// Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕。
|
|
618
|
+
for (const tool of trackedToolComponents) {
|
|
619
|
+
if (
|
|
620
|
+
!ids.has(tool.toolCallId) ||
|
|
621
|
+
EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")) ||
|
|
622
|
+
tool.expanded !== true
|
|
623
|
+
) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (typeof tool.setExpanded === "function") tool.setExpanded(false);
|
|
627
|
+
else {
|
|
628
|
+
tool.expanded = false;
|
|
629
|
+
tool.updateDisplay?.();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
round.anchor.contentContainer.addChild({
|
|
633
|
+
render(width: number): string[] {
|
|
634
|
+
const theme = themeOf();
|
|
635
|
+
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
|
+
for (const child of assistantChildren) box.addChild(child);
|
|
644
|
+
const ids = roundToolCallIds(round);
|
|
645
|
+
for (const tool of trackedToolComponents) {
|
|
646
|
+
if (!ids.has(tool.toolCallId) || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? ""))) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
box.addChild({
|
|
650
|
+
render: (innerWidth: number) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
651
|
+
invalidate: () => tool.invalidate?.(),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return ["", ...box.render(width)];
|
|
655
|
+
},
|
|
656
|
+
invalidate() {
|
|
657
|
+
for (const child of assistantChildren) child.invalidate?.();
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (const [component, message] of round.messages) {
|
|
664
|
+
if (component === round.anchor) {
|
|
665
|
+
renderAssistantWithoutThinking(component, message);
|
|
666
|
+
compactAssistantLine(component, summary, deps.query);
|
|
667
|
+
} else {
|
|
668
|
+
component.contentContainer?.clear?.();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const activateRound = (round: CompactRound): void => {
|
|
674
|
+
round.active = true;
|
|
675
|
+
activeRound = round;
|
|
676
|
+
deps.query?.setCompactSummaryActive?.(true);
|
|
677
|
+
};
|
|
678
|
+
|
|
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
|
+
};
|
|
687
|
+
|
|
688
|
+
const resetRounds = (): void => {
|
|
689
|
+
activeRound = undefined;
|
|
690
|
+
roundByComponent = new WeakMap();
|
|
691
|
+
expandedRoundToolIds.clear();
|
|
692
|
+
deps.query?.setCompactSummaryActive?.(false);
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
patch.assistantInstalled = function (this: any, message: any) {
|
|
696
|
+
const self = this as any;
|
|
697
|
+
if (self[ASSISTANT_REENTRY_KEY] === patch) {
|
|
698
|
+
return patch.assistantNative.call(this, message);
|
|
699
|
+
}
|
|
700
|
+
self.lastMessage = message;
|
|
701
|
+
trackedAssistantComponents.add(this);
|
|
702
|
+
if (!patch.active || config.mode !== "compact") {
|
|
703
|
+
return passThroughAssistant(this, message);
|
|
704
|
+
}
|
|
705
|
+
if (!self.contentContainer || typeof self.contentContainer.clear !== "function") {
|
|
706
|
+
return passThroughAssistant(this, message);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
710
|
+
const hasToolCalls = content.some((item: any) => item?.type === "toolCall");
|
|
711
|
+
const hasText = content.some(
|
|
712
|
+
(item: any) => item?.type === "text" && typeof item.text === "string" && item.text.trim(),
|
|
713
|
+
);
|
|
714
|
+
self.hasToolCalls = hasToolCalls;
|
|
715
|
+
|
|
716
|
+
if (hasToolCalls) {
|
|
717
|
+
let round = roundByComponent.get(this);
|
|
718
|
+
if (hasText && (!round || round.anchor !== this)) {
|
|
719
|
+
if (round) round.messages.delete(this);
|
|
720
|
+
if (activeRound) finishRound(activeRound);
|
|
721
|
+
round = {
|
|
722
|
+
anchor: this,
|
|
723
|
+
messages: new Map(),
|
|
724
|
+
detachedMessages: [],
|
|
725
|
+
active: true,
|
|
726
|
+
suppressedToolIds: new Set(),
|
|
727
|
+
};
|
|
728
|
+
roundByComponent.set(this, round);
|
|
729
|
+
activateRound(round);
|
|
730
|
+
} else if (!round) {
|
|
731
|
+
round = activeRound ?? {
|
|
732
|
+
anchor: this,
|
|
733
|
+
messages: new Map(),
|
|
734
|
+
detachedMessages: [],
|
|
735
|
+
active: true,
|
|
736
|
+
suppressedToolIds: new Set(),
|
|
737
|
+
};
|
|
738
|
+
roundByComponent.set(this, round);
|
|
739
|
+
if (!activeRound) activateRound(round);
|
|
740
|
+
}
|
|
741
|
+
round.messages.set(this, message);
|
|
742
|
+
renderRound(round);
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (hasText) {
|
|
747
|
+
const round = roundByComponent.get(this);
|
|
748
|
+
const previousMessage = round?.messages.get(this);
|
|
749
|
+
if (round && previousMessage && (round.anchor !== this || round.messages.size > 1)) {
|
|
750
|
+
// 最终回答开始后,当前组件恢复原生文本;它已完成的 thinking
|
|
751
|
+
// 留在上一轮摘要中,避免再次生成独立 Thought 行。
|
|
752
|
+
round.messages.delete(this);
|
|
753
|
+
round.detachedMessages.push(previousMessage);
|
|
754
|
+
roundByComponent.delete(this);
|
|
755
|
+
finishRound(round);
|
|
756
|
+
return renderAssistantWithoutThinking(this, message);
|
|
757
|
+
}
|
|
758
|
+
if (round) {
|
|
759
|
+
round.active = false;
|
|
760
|
+
roundByComponent.delete(this);
|
|
761
|
+
if (activeRound === round) {
|
|
762
|
+
activeRound = undefined;
|
|
763
|
+
deps.query?.setCompactSummaryActive?.(false);
|
|
764
|
+
}
|
|
765
|
+
return passThroughAssistant(this, message);
|
|
766
|
+
}
|
|
767
|
+
if (activeRound) finishRound(activeRound);
|
|
768
|
+
return renderAssistantWithoutThinking(this, message);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const hasThinking = content.some((item: any) => item?.type === "thinking");
|
|
772
|
+
if (hasThinking) {
|
|
773
|
+
let round = roundByComponent.get(this);
|
|
774
|
+
if (!round) {
|
|
775
|
+
round = activeRound ?? {
|
|
776
|
+
anchor: this,
|
|
777
|
+
messages: new Map(),
|
|
778
|
+
detachedMessages: [],
|
|
779
|
+
active: true,
|
|
780
|
+
suppressedToolIds: new Set(),
|
|
781
|
+
};
|
|
782
|
+
roundByComponent.set(this, round);
|
|
783
|
+
if (!activeRound) activateRound(round);
|
|
784
|
+
}
|
|
785
|
+
round.messages.set(this, message);
|
|
786
|
+
renderRound(round);
|
|
787
|
+
return undefined;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
self.contentContainer.clear();
|
|
791
|
+
return undefined;
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
patch.toolInstalledRender = function (this: any, width: number) {
|
|
795
|
+
if (!patch.active || config.mode !== "compact") {
|
|
796
|
+
return patch.toolOriginalRender.call(this, width);
|
|
797
|
+
}
|
|
798
|
+
const name = String(this.toolName ?? "");
|
|
799
|
+
if (EDIT_WRITE_TOOLS.has(name)) {
|
|
800
|
+
if (!this.result || this.isPartial === true) scheduleAnimation(this);
|
|
801
|
+
const lines = compactEditWriteLine(this, width, deps.writeMetadata);
|
|
802
|
+
if (this.expanded !== true) return lines;
|
|
803
|
+
return compactEditWriteExpandedLines(this, width, deps.writeMetadata);
|
|
804
|
+
}
|
|
805
|
+
if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
806
|
+
// 普通工具折叠时不显示独立行(摘要行已统计),独立展开走原 renderer。
|
|
807
|
+
if (this.expanded === true) {
|
|
808
|
+
return patch.toolOriginalRender.call(this, width);
|
|
809
|
+
}
|
|
810
|
+
return [];
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
patch.toolInstalledUpdateDisplay = function (this: any) {
|
|
814
|
+
if (
|
|
815
|
+
patch.active &&
|
|
816
|
+
config.mode === "compact" &&
|
|
817
|
+
expandedRoundToolIds.has(String(this.toolCallId ?? ""))
|
|
818
|
+
) {
|
|
819
|
+
this.expanded = false;
|
|
820
|
+
}
|
|
821
|
+
const result = patch.toolOriginalUpdateDisplay.call(this);
|
|
822
|
+
if (!patch.active) return result;
|
|
823
|
+
trackedToolComponents.add(this);
|
|
824
|
+
return result;
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
patch.assertAssistantOwnership = () => {
|
|
828
|
+
// 仅从已标记的 compact-thinking 包装器重新认领。未知外部包装器必须保留所有权。
|
|
829
|
+
if (!patch.active || assistantPrototype.updateContent === patch.assistantInstalled) return;
|
|
830
|
+
const current = assistantPrototype.updateContent;
|
|
831
|
+
if ((current as any)[COMPACT_THINKING_PATCH_KEY] !== true) return;
|
|
832
|
+
patch.assistantOriginal = current;
|
|
833
|
+
assistantPrototype.updateContent = patch.assistantInstalled;
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
(patch.assistantInstalled as any)[PROTOTYPE_ORIGINAL_KEY] = patch.assistantNative;
|
|
837
|
+
|
|
838
|
+
patch.dispose = () => {
|
|
839
|
+
if (!patch.active) return;
|
|
840
|
+
patch.active = false;
|
|
841
|
+
if (assistantPrototype.updateContent === patch.assistantInstalled) {
|
|
842
|
+
assistantPrototype.updateContent = patch.assistantOriginal;
|
|
843
|
+
}
|
|
844
|
+
if (toolPrototype.render === patch.toolInstalledRender) {
|
|
845
|
+
toolPrototype.render = patch.toolOriginalRender;
|
|
846
|
+
}
|
|
847
|
+
if (toolPrototype.updateDisplay === patch.toolInstalledUpdateDisplay) {
|
|
848
|
+
toolPrototype.updateDisplay = patch.toolOriginalUpdateDisplay;
|
|
849
|
+
}
|
|
850
|
+
if (host[COMPACT_MODE_PATCH_KEY] === patch) delete host[COMPACT_MODE_PATCH_KEY];
|
|
851
|
+
for (const component of trackedAssistantComponents) detachAssistantExpansion(component);
|
|
852
|
+
trackedAssistantComponents.clear();
|
|
853
|
+
trackedToolComponents.clear();
|
|
854
|
+
hoveredAssistantComponent = undefined;
|
|
855
|
+
resetRounds();
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
assistantPrototype.updateContent = patch.assistantInstalled;
|
|
859
|
+
toolPrototype.render = patch.toolInstalledRender;
|
|
860
|
+
toolPrototype.updateDisplay = patch.toolInstalledUpdateDisplay;
|
|
861
|
+
host[COMPACT_MODE_PATCH_KEY] = patch;
|
|
862
|
+
|
|
863
|
+
const syncGlobalExpanded = (ctx: any): void => {
|
|
864
|
+
let globalExpanded = false;
|
|
865
|
+
try {
|
|
866
|
+
globalExpanded = ctx?.ui?.getToolsExpanded?.() === true;
|
|
867
|
+
} catch {
|
|
868
|
+
// 测试或无 UI 上下文时保持折叠。
|
|
869
|
+
}
|
|
870
|
+
for (const component of trackedAssistantComponents) component.expanded = globalExpanded;
|
|
871
|
+
for (const component of trackedToolComponents) component.expanded = globalExpanded;
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const releaseAssistantOwnership = (): void => {
|
|
875
|
+
if (assistantPrototype.updateContent === patch.assistantInstalled) {
|
|
876
|
+
assistantPrototype.updateContent = patch.assistantOriginal;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
sync(ctx: any) {
|
|
882
|
+
if (!patch.active) return;
|
|
883
|
+
resetRounds();
|
|
884
|
+
if (config.mode === "compact") {
|
|
885
|
+
patch.assertAssistantOwnership();
|
|
886
|
+
syncGlobalExpanded(ctx);
|
|
887
|
+
} else {
|
|
888
|
+
releaseAssistantOwnership();
|
|
889
|
+
}
|
|
890
|
+
refreshTrackedComponents();
|
|
891
|
+
},
|
|
892
|
+
refresh() {
|
|
893
|
+
if (!patch.active) return;
|
|
894
|
+
resetRounds();
|
|
895
|
+
if (config.mode !== "compact") {
|
|
896
|
+
releaseAssistantOwnership();
|
|
897
|
+
hoveredAssistantComponent = undefined;
|
|
898
|
+
}
|
|
899
|
+
refreshTrackedComponents();
|
|
900
|
+
},
|
|
901
|
+
assertOwnership() {
|
|
902
|
+
if (!patch.active) return;
|
|
903
|
+
patch.assertAssistantOwnership();
|
|
904
|
+
},
|
|
905
|
+
refreshToolCallMessage(toolCallId: string | undefined) {
|
|
906
|
+
if (!patch.active || typeof toolCallId !== "string" || !toolCallId) return;
|
|
907
|
+
for (const component of [...trackedAssistantComponents]) {
|
|
908
|
+
const message = component.lastMessage;
|
|
909
|
+
const contains =
|
|
910
|
+
Array.isArray(message?.content) &&
|
|
911
|
+
message.content.some((item: any) => item?.type === "toolCall" && item.id === toolCallId);
|
|
912
|
+
if (!contains) continue;
|
|
913
|
+
try {
|
|
914
|
+
component.updateContent?.(message);
|
|
915
|
+
} catch {
|
|
916
|
+
trackedAssistantComponents.delete(component);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
shutdown() {
|
|
921
|
+
patch.dispose();
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function refreshTrackedComponents(): void {
|
|
927
|
+
for (const component of [...trackedAssistantComponents]) {
|
|
928
|
+
try {
|
|
929
|
+
if (config.mode !== "compact") detachAssistantExpansion(component);
|
|
930
|
+
if (component.lastMessage) component.updateContent?.(component.lastMessage);
|
|
931
|
+
else component.invalidate?.();
|
|
932
|
+
} catch {
|
|
933
|
+
trackedAssistantComponents.delete(component);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
for (const component of [...trackedToolComponents]) {
|
|
937
|
+
try {
|
|
938
|
+
component.updateDisplay?.();
|
|
939
|
+
component.invalidate?.();
|
|
940
|
+
} catch {
|
|
941
|
+
trackedToolComponents.delete(component);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** 供 renderer/index.ts 在 session 事件后收集 /reload 重建的组件。 */
|
|
947
|
+
export function refreshCompactModeComponents(root: any): void {
|
|
948
|
+
collectMountedComponents(root);
|
|
949
|
+
}
|