pi-cc-extensions 0.9.6 → 0.9.8
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/extensions/feature/shell/working-message.ts +21 -2
- package/extensions/renderer/compact-mode.ts +588 -78
- package/extensions/renderer/default-mode.ts +2 -0
- package/extensions/renderer/index.ts +24 -6
- package/extensions/renderer/mouse/scroll.ts +43 -4
- package/extensions/renderer/tool/diff/index.ts +45 -8
- package/extensions/renderer/tool/names.ts +1 -1
- package/extensions/utils/ansi-text.ts +29 -0
- package/extensions/utils/fullscreen-detect.ts +51 -0
- package/extensions/utils/patch-keys.ts +5 -0
- package/package.json +1 -1
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
9
9
|
* 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
|
|
10
10
|
*
|
|
11
|
+
* live 围观态(回合进行中 / 收尾静默期):正文跟折叠态同形;摘要行与单一槽位卡
|
|
12
|
+
* (`↳` + 最新的一个块:仍在增长的思考预览,或本回合最近的工具,运行中优先)挂在
|
|
13
|
+
* transcript 容器的回合末尾(尾行组件,anchor 与工具卡之后、下一消息之前)。
|
|
14
|
+
* 思考原地增长不轮换,只有新的思考块或工具调用进来才换槽位内容。
|
|
15
|
+
* 摘要行挂末尾是刻意的:运行中它恒在可写视口底缘,收尾时原地从 Running... 翻成
|
|
16
|
+
* Ran for 真实落进 scrollback;放在 anchor 内部则会被工具/diff 顶出视口后定格。
|
|
17
|
+
* (anchor 未挂载进容器的独立渲染场景退回组件内旧行为。)
|
|
18
|
+
*
|
|
11
19
|
* 工具计数:read 按非空路径去重、其余按调用计数(首次出现顺序);edit/write 不进摘要。
|
|
12
20
|
* 时长 = 回合流逝挂钟;进行中 Running...,结束 Ran for。
|
|
13
21
|
* abort/error/length 状态行挂在摘要外层,避免被折叠吞掉。
|
|
@@ -40,7 +48,12 @@ import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
|
40
48
|
import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
|
|
41
49
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
42
50
|
import { formatDisplayPath } from "./tool/names.ts";
|
|
43
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
hasVisibleText,
|
|
53
|
+
removeStyledRange,
|
|
54
|
+
stripBackgroundAnsi,
|
|
55
|
+
stripTerminalSequencesPreservingLayout,
|
|
56
|
+
} from "../utils/ansi-text.ts";
|
|
44
57
|
import { walkComponentTree } from "../utils/component-tree.ts";
|
|
45
58
|
import {
|
|
46
59
|
ASSISTANT_REENTRY_KEY,
|
|
@@ -50,6 +63,7 @@ import {
|
|
|
50
63
|
COMPACT_THINKING_PATCH_KEY,
|
|
51
64
|
patchRegistry,
|
|
52
65
|
PROTOTYPE_ORIGINAL_KEY,
|
|
66
|
+
TOOL_GROUPING_PARENT_KEY,
|
|
53
67
|
} from "../utils/patch-keys.ts";
|
|
54
68
|
|
|
55
69
|
/** Pi 的 native 组件与扩展可能各持一份 pi-tui,Spacer 不能只靠 instanceof 判。 */
|
|
@@ -67,6 +81,12 @@ export type CompactThinkingQuery = {
|
|
|
67
81
|
|
|
68
82
|
const EDIT_WRITE_TOOLS = new Set(["edit", "write"]);
|
|
69
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 回合收尾后延迟收拢的静默期(ms)。正文 token 往往先于 toolCall 到达,
|
|
86
|
+
* 立刻收拢会得到「先收再开」两次转场;静默期内被新回合接替则一并收拢。
|
|
87
|
+
*/
|
|
88
|
+
const FOLD_SETTLE_MS = 250;
|
|
89
|
+
|
|
70
90
|
type CompactThinkingTheme = Pick<Theme, "fg" | "italic" | "bold">;
|
|
71
91
|
|
|
72
92
|
/** 与 compact-thinking 主渲染器共用的静态文字样式。 */
|
|
@@ -332,23 +352,52 @@ function editWriteExpandedCard(theme: any): any {
|
|
|
332
352
|
* 每个子卡前面补 1 行分隔(上一条已经是空行时不再补),子卡自带的首尾空行会被裁掉,
|
|
333
353
|
* 避免与外卡/内卡 padding 叠出双空行。
|
|
334
354
|
* hits:子卡的行区间与命中目标(thinking 命中自身,工具命中 tool 组件),
|
|
335
|
-
* 供展开后点击 hint。未开启 toolHits 时不映射工具,点击仍归外层面板。
|
|
355
|
+
* 供展开后点击 hint。未开启 toolHits 时不映射工具,点击仍归外层面板。
|
|
356
|
+
* live=true:live 槽位用,不铺底色也不留上下空行,首行紧跟 `↳`、其余行缩进对齐
|
|
357
|
+
* (Claude Code 的 ⎿ 口径)。行的产出仍是一行对一行,hits 行号无需平移。
|
|
358
|
+
*/
|
|
336
359
|
function layoutExpandedToolCard(
|
|
337
360
|
theme: any,
|
|
338
361
|
children: any[],
|
|
339
362
|
width: number,
|
|
340
363
|
paints?: string[][],
|
|
341
364
|
toolHits = false,
|
|
365
|
+
live = false,
|
|
342
366
|
): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
|
|
343
367
|
const slot = "userMessageBg";
|
|
344
368
|
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
345
|
-
|
|
369
|
+
/** live 每行前面的标记宽度(首行 `↳ `,其余对齐空格)。 */
|
|
370
|
+
const markerWidth = live ? 2 : 0;
|
|
371
|
+
const innerWidth = Math.max(0, width - 2 - markerWidth);
|
|
346
372
|
const lines: string[] = [];
|
|
347
373
|
const hits: Array<{ child: any; start: number; end: number }> = [];
|
|
348
374
|
const isThinkingPreview = (child: any) => typeof child?.setHintHovered === "function";
|
|
349
375
|
let lastBlank = false;
|
|
376
|
+
/** live 首行被吃掉的前导缩进:续行同步左移同样格数,保持块内相对对齐。 */
|
|
377
|
+
let liveLead = 0;
|
|
378
|
+
const pushRow = (raw: string) => {
|
|
379
|
+
if (!live) {
|
|
380
|
+
lines.push(paddedBackgroundRow(theme, slot, raw, width));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// 槽位里不重复展开入口(见 SLOT_HINT_PATTERNS)。
|
|
384
|
+
const line = stripSlotHint(raw);
|
|
385
|
+
// 工具卡行首带 SGR:ANSI 之后才是缩进空格。
|
|
386
|
+
const head = line.match(/^((?:\x1b\[[0-9;]*m)*)( *)/);
|
|
387
|
+
const ansi = head?.[1] ?? "";
|
|
388
|
+
const spaces = head?.[2]?.length ?? 0;
|
|
389
|
+
const first = lines.length === 0;
|
|
390
|
+
if (first) liveLead = spaces;
|
|
391
|
+
// 首行挂 muted 的 `↳`(与 default-mode 工具结果行同色);续行按 liveLead 去缩进。
|
|
392
|
+
// 两侧都保留行首 SGR:丢掉它整行会掉回终端默认前景色。
|
|
393
|
+
const marker = first ? `${theme.fg("muted", "↳")} ` : " ";
|
|
394
|
+
const lead = first ? spaces : Math.min(spaces, liveLead);
|
|
395
|
+
lines.push(
|
|
396
|
+
`${marker}${ansi}${truncateToWidth(line.slice(ansi.length + lead), innerWidth, "")}`,
|
|
397
|
+
);
|
|
398
|
+
};
|
|
350
399
|
const pushBlank = () => {
|
|
351
|
-
if (lastBlank) return;
|
|
400
|
+
if (live || lastBlank) return;
|
|
352
401
|
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
353
402
|
lastBlank = true;
|
|
354
403
|
};
|
|
@@ -356,8 +405,8 @@ function layoutExpandedToolCard(
|
|
|
356
405
|
const child = children[childIndex];
|
|
357
406
|
const painted = paints?.[childIndex];
|
|
358
407
|
const childLines = Array.isArray(painted) ? painted : child.render(innerWidth);
|
|
359
|
-
//
|
|
360
|
-
const innerCard = isThinkingPreview(child) && child.expanded === true;
|
|
408
|
+
// live 不留空行、也不叠深色内卡:槽位没有边框。
|
|
409
|
+
const innerCard = !live && isThinkingPreview(child) && child.expanded === true;
|
|
361
410
|
let first = -1;
|
|
362
411
|
let last = -1;
|
|
363
412
|
for (let i = 0; i < childLines.length; i++) {
|
|
@@ -367,6 +416,7 @@ function layoutExpandedToolCard(
|
|
|
367
416
|
}
|
|
368
417
|
}
|
|
369
418
|
if (first < 0) {
|
|
419
|
+
if (live) continue;
|
|
370
420
|
for (const line of childLines) {
|
|
371
421
|
lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
372
422
|
lastBlank = !hasVisibleText(line);
|
|
@@ -378,7 +428,7 @@ function layoutExpandedToolCard(
|
|
|
378
428
|
// 展开的 thinking 走深色内卡(上下各 1 行 padding),工具卡直接铺在外卡上。
|
|
379
429
|
const paintRow = innerCard
|
|
380
430
|
? (line: string) => lines.push(toolCardBgRow(theme, slot, toolBgAnsi, line, width))
|
|
381
|
-
:
|
|
431
|
+
: pushRow;
|
|
382
432
|
if (innerCard) paintRow("");
|
|
383
433
|
for (let i = first; i <= last; i++) paintRow(childLines[i]);
|
|
384
434
|
if (innerCard) paintRow("");
|
|
@@ -399,6 +449,8 @@ function compactRoundCard(
|
|
|
399
449
|
toolRender: (tool: any, width: number) => string[],
|
|
400
450
|
/** true 时卡内工具行归 tool 组件,可点击二次展开(整回合展开卡)。 */
|
|
401
451
|
toolHits = false,
|
|
452
|
+
/** true 时走 live 槽位排布:不留空行、首行紧跟 `↳`。 */
|
|
453
|
+
live = false,
|
|
402
454
|
): any {
|
|
403
455
|
const children: any[] = [];
|
|
404
456
|
for (const item of cardItems) {
|
|
@@ -413,6 +465,8 @@ function compactRoundCard(
|
|
|
413
465
|
});
|
|
414
466
|
}
|
|
415
467
|
}
|
|
468
|
+
/** 子块可用宽度:live 还要让出首行 `↳ ` 的标记位。 */
|
|
469
|
+
const contentWidth = (width: number) => Math.max(0, width - 2 - (live ? 2 : 0));
|
|
416
470
|
let paint:
|
|
417
471
|
| {
|
|
418
472
|
width: number;
|
|
@@ -424,7 +478,7 @@ function compactRoundCard(
|
|
|
424
478
|
| undefined;
|
|
425
479
|
const layout = (width: number) => {
|
|
426
480
|
const theme = themeOf();
|
|
427
|
-
const innerWidth =
|
|
481
|
+
const innerWidth = contentWidth(width);
|
|
428
482
|
const paints = children.map((child) => {
|
|
429
483
|
const lines = child.render?.(innerWidth);
|
|
430
484
|
return Array.isArray(lines) ? lines : [];
|
|
@@ -438,7 +492,7 @@ function compactRoundCard(
|
|
|
438
492
|
) {
|
|
439
493
|
return paint;
|
|
440
494
|
}
|
|
441
|
-
const laid = layoutExpandedToolCard(theme, children, width, paints, toolHits);
|
|
495
|
+
const laid = layoutExpandedToolCard(theme, children, width, paints, toolHits, live);
|
|
442
496
|
paint = {
|
|
443
497
|
width,
|
|
444
498
|
theme,
|
|
@@ -532,6 +586,33 @@ const trackedAssistantComponents = new Set<any>();
|
|
|
532
586
|
const trackedToolComponents = new Set<any>();
|
|
533
587
|
let hoveredAssistantComponent: any;
|
|
534
588
|
|
|
589
|
+
/**
|
|
590
|
+
* 活动回合摘要 getter:常驻状态行(pi working row)镜像用。
|
|
591
|
+
* installCompactMode 注册、卸载时清空,直接读 activeRound,不持有回合闭包。
|
|
592
|
+
*/
|
|
593
|
+
let compactRunStatusGetter: (() => string | undefined) | undefined;
|
|
594
|
+
|
|
595
|
+
/** 活动回合的摘要文本(`Running... · 9s, bash×1`);无活动回合返回 undefined。 */
|
|
596
|
+
export function getCompactRunStatusText(): string | undefined {
|
|
597
|
+
return compactRunStatusGetter?.();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* 回合尾行记录:摘要行(live 时连槽位卡)挂成 transcript 容器里回合末尾的
|
|
602
|
+
* 兄弟组件,而不是 anchor 内部。anchor 内部的可变内容一旦被工具/diff 顶出视口
|
|
603
|
+
* 就再也写不进 scrollback(regular 主屏只能差分可视区),运行态 Running 会永久
|
|
604
|
+
* 定格在挤出前最后一帧;尾行恒在可写底缘,收尾的 Ran for 才真实落进历史。
|
|
605
|
+
* 模块级持有(与 trackedAssistantComponents 同理):跨 /reload 去重与清理。
|
|
606
|
+
*/
|
|
607
|
+
type MountedRoundTail = {
|
|
608
|
+
host: any;
|
|
609
|
+
container: any;
|
|
610
|
+
parts: any[];
|
|
611
|
+
round: any;
|
|
612
|
+
};
|
|
613
|
+
const tailByAnchor = new WeakMap<object, MountedRoundTail>();
|
|
614
|
+
const mountedTails = new Set<MountedRoundTail>();
|
|
615
|
+
|
|
535
616
|
export function setHoveredCompactAssistant(component: any): boolean {
|
|
536
617
|
if (hoveredAssistantComponent === component) return false;
|
|
537
618
|
hoveredAssistantComponent = component;
|
|
@@ -841,7 +922,9 @@ function compactAssistantLineComponent(
|
|
|
841
922
|
return paint.lines;
|
|
842
923
|
}
|
|
843
924
|
const plainText = truncateToWidth(resolved, summaryWidth, "…");
|
|
844
|
-
|
|
925
|
+
// Dim thinking text 开启时整行统一 dim,否则工具计数保持 muted。
|
|
926
|
+
const plain = (value: string) => theme.fg(config.dimThinkingText ? "dim" : "muted", value);
|
|
927
|
+
let text = plain(plainText);
|
|
845
928
|
if (runningActive || plainText.startsWith("Ran for ")) {
|
|
846
929
|
const separator = plainText.indexOf(", ");
|
|
847
930
|
const heading = separator < 0 ? plainText : plainText.slice(0, separator);
|
|
@@ -850,13 +933,9 @@ function compactAssistantLineComponent(
|
|
|
850
933
|
const durationSeparator = heading.indexOf(" · ");
|
|
851
934
|
const label = durationSeparator < 0 ? heading : heading.slice(0, durationSeparator);
|
|
852
935
|
const duration = durationSeparator < 0 ? "" : heading.slice(durationSeparator);
|
|
853
|
-
text = `${animateCompactThinkingText(
|
|
854
|
-
label,
|
|
855
|
-
theme,
|
|
856
|
-
query?.getThinkingAnimationFrame?.() ?? 0,
|
|
857
|
-
)}${styleCompactThinkingText(duration, theme)}${theme.fg("muted", tools)}`;
|
|
936
|
+
text = `${animateCompactThinkingText(label, theme, query?.getThinkingAnimationFrame?.() ?? 0)}${styleCompactThinkingText(duration, theme)}${plain(tools)}`;
|
|
858
937
|
} else {
|
|
859
|
-
text = `${styleCompactThinkingText(heading, theme)}${
|
|
938
|
+
text = `${styleCompactThinkingText(heading, theme)}${plain(tools)}`;
|
|
860
939
|
}
|
|
861
940
|
}
|
|
862
941
|
const hintColor = hover ? "text" : "dim";
|
|
@@ -894,14 +973,49 @@ function compactStopStatusLine(status: string, pad = 0): any {
|
|
|
894
973
|
};
|
|
895
974
|
}
|
|
896
975
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
976
|
+
/**
|
|
977
|
+
* 槽位卡内的展开入口:卡里是只读围观,展开走摘要行自己的 hint,卡内不再重复提示。
|
|
978
|
+
* 提示有两处来源——我们的 showMoreHintText(`… +N more lines • click to show more`)与
|
|
979
|
+
* pi 原生工具卡(`… (N earlier lines, ctrl+o to expand)`),所以按渲染结果收尾裁剪。
|
|
980
|
+
* 只去掉动作词,`(N more lines)` 这类计数保留。
|
|
981
|
+
*/
|
|
982
|
+
/** 展开入口的动作词:我们的 `click to show more`、pi 的 `<key> to expand`(键名可能取不到)。 */
|
|
983
|
+
const EXPAND_ACTION = String.raw`(?:click to show more|(?:[\w/+]+\s+)?to (?:show more|expand))`;
|
|
984
|
+
|
|
985
|
+
const SLOT_HINT_PATTERNS: RegExp[] = [
|
|
986
|
+
// 卡在括号里:`(9 more lines, click to show more)` / `(195 earlier lines, ctrl+o to expand)`,
|
|
987
|
+
// 只去掉动作词,收尾括号与计数保留。
|
|
988
|
+
new RegExp(String.raw`,\s*[^()]*?${EXPAND_ACTION}(?=\))`),
|
|
989
|
+
// 直接挂在行尾:整段去掉。
|
|
990
|
+
new RegExp(String.raw`\s*(?:•\s*)?${EXPAND_ACTION}\s*$`),
|
|
991
|
+
];
|
|
992
|
+
|
|
993
|
+
function stripSlotHint(line: string): string {
|
|
994
|
+
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
995
|
+
for (const re of SLOT_HINT_PATTERNS) {
|
|
996
|
+
const match = re.exec(plain);
|
|
997
|
+
if (!match) continue;
|
|
998
|
+
// 删除区间而非截断后拼纯文本:收尾括号留在原样式里(dim),不会掉回默认前景色。
|
|
999
|
+
return removeStyledRange(line, match.index, match.index + match[0].length);
|
|
1000
|
+
}
|
|
1001
|
+
return line;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** live 槽位卡:按 pad 缩进;首行 `↳` 由卡自身的 live 排布加,行不增不减。 */
|
|
1005
|
+
function compactLiveSlot(card: any, pad = 0): any {
|
|
1006
|
+
const innerOf = (width: number) => Math.max(0, width - Math.max(0, pad));
|
|
1007
|
+
return {
|
|
1008
|
+
render(width: number): string[] {
|
|
1009
|
+
const indent = " ".repeat(Math.max(0, pad));
|
|
1010
|
+
return card.render(innerOf(width)).map((line: string) => `${indent}${line}`);
|
|
1011
|
+
},
|
|
1012
|
+
childAtRow(localRow: number, width: number) {
|
|
1013
|
+
return card.childAtRow?.(localRow, innerOf(width)) ?? null;
|
|
1014
|
+
},
|
|
1015
|
+
invalidate() {
|
|
1016
|
+
card.invalidate();
|
|
1017
|
+
},
|
|
1018
|
+
};
|
|
905
1019
|
}
|
|
906
1020
|
|
|
907
1021
|
function appendStopStatus(component: any, status: string | undefined): void {
|
|
@@ -1005,15 +1119,24 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1005
1119
|
detachedMessages: any[];
|
|
1006
1120
|
active: boolean;
|
|
1007
1121
|
suppressedToolIds: Set<string>;
|
|
1122
|
+
/** live 槽位内联渲染的工具 id;回合结束时随槽位一起交还外层。 */
|
|
1123
|
+
liveSlotToolIds: Set<string>;
|
|
1124
|
+
/** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
|
|
1125
|
+
settleUntil?: number;
|
|
1126
|
+
foldTimer?: ReturnType<typeof setTimeout>;
|
|
1008
1127
|
/** 回合挂钟起点,保证 Running 时长连续递增。 */
|
|
1009
1128
|
startedAt: number;
|
|
1010
1129
|
endedAt?: number;
|
|
1130
|
+
/** 回合尾行(摘要 + live 槽位),挂在 transcript 容器的回合末尾。 */
|
|
1131
|
+
tail?: MountedRoundTail;
|
|
1011
1132
|
};
|
|
1012
1133
|
let activeRound: CompactRound | undefined;
|
|
1013
1134
|
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
1014
1135
|
const expandedRoundToolIds = new Set<string>();
|
|
1015
1136
|
/** 用户显式点开的 round 内工具;强制折叠不压回这些 id。 */
|
|
1016
1137
|
const explicitRoundToolIds = new Set<string>();
|
|
1138
|
+
const liveSlotToolIds = new Set<string>();
|
|
1139
|
+
const pendingFoldRounds = new Set<CompactRound>();
|
|
1017
1140
|
let uiRef: { requestRender?: (force?: boolean) => void } | undefined;
|
|
1018
1141
|
let roundTickTimer: ReturnType<typeof setInterval> | undefined;
|
|
1019
1142
|
|
|
@@ -1053,8 +1176,22 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1053
1176
|
}, 250);
|
|
1054
1177
|
};
|
|
1055
1178
|
|
|
1056
|
-
/**
|
|
1057
|
-
const
|
|
1179
|
+
/** 围观中:活动回合,或收尾静默期还没走完的回合(槽位卡仍挂在屏幕上)。 */
|
|
1180
|
+
const roundLive = (round: CompactRound): boolean =>
|
|
1181
|
+
round.active || (round.settleUntil !== undefined && Date.now() < round.settleUntil);
|
|
1182
|
+
|
|
1183
|
+
const clearFoldTimer = (round: CompactRound): void => {
|
|
1184
|
+
if (round.foldTimer !== undefined) {
|
|
1185
|
+
clearTimeout(round.foldTimer);
|
|
1186
|
+
round.foldTimer = undefined;
|
|
1187
|
+
}
|
|
1188
|
+
round.settleUntil = undefined;
|
|
1189
|
+
pendingFoldRounds.delete(round);
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
/** 结束回合活动态;不做任何重绘。 */
|
|
1193
|
+
const finalizeRound = (round: CompactRound): void => {
|
|
1194
|
+
clearFoldTimer(round);
|
|
1058
1195
|
round.active = false;
|
|
1059
1196
|
if (round.endedAt === undefined) round.endedAt = Date.now();
|
|
1060
1197
|
if (activeRound === round) {
|
|
@@ -1062,7 +1199,36 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1062
1199
|
deps.query?.setCompactSummaryActive?.(false);
|
|
1063
1200
|
stopRoundTick();
|
|
1064
1201
|
}
|
|
1065
|
-
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。
|
|
1206
|
+
* 分组语义不受影响:activeRound 立即让位,新消息不会被并进已收尾的回合。
|
|
1207
|
+
*/
|
|
1208
|
+
const endRound = (round: CompactRound, render = false): void => {
|
|
1209
|
+
if (!render || !round.active) {
|
|
1210
|
+
finalizeRound(round);
|
|
1211
|
+
if (render) renderRound(round);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
finalizeRound(round);
|
|
1215
|
+
if (round.foldTimer !== undefined) return;
|
|
1216
|
+
round.settleUntil = Date.now() + FOLD_SETTLE_MS;
|
|
1217
|
+
pendingFoldRounds.add(round);
|
|
1218
|
+
round.foldTimer = setTimeout(() => {
|
|
1219
|
+
round.foldTimer = undefined;
|
|
1220
|
+
round.settleUntil = undefined;
|
|
1221
|
+
pendingFoldRounds.delete(round);
|
|
1222
|
+
renderRound(round);
|
|
1223
|
+
}, FOLD_SETTLE_MS);
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
/** 新回合接替时立即收掉还在静默期的回合,把两次转场并成一次。 */
|
|
1227
|
+
const flushSettle = (): void => {
|
|
1228
|
+
for (const round of [...pendingFoldRounds]) {
|
|
1229
|
+
clearFoldTimer(round);
|
|
1230
|
+
renderRound(round);
|
|
1231
|
+
}
|
|
1066
1232
|
};
|
|
1067
1233
|
|
|
1068
1234
|
const renderAssistantWithoutThinking = (
|
|
@@ -1083,6 +1249,22 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1083
1249
|
return result;
|
|
1084
1250
|
};
|
|
1085
1251
|
|
|
1252
|
+
/** compact 只允许单开:收回一张工具卡的展开态。 */
|
|
1253
|
+
const collapseTool = (tool: any): void => {
|
|
1254
|
+
if (typeof tool?.setExpanded === "function") tool.setExpanded(false);
|
|
1255
|
+
else {
|
|
1256
|
+
tool.expanded = false;
|
|
1257
|
+
tool.updateDisplay?.();
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
/** 按条件收回已展开的工具卡。 */
|
|
1262
|
+
const collapseTools = (shouldCollapse: (tool: any) => boolean): void => {
|
|
1263
|
+
for (const tool of trackedToolComponents) {
|
|
1264
|
+
if (tool.expanded === true && shouldCollapse(tool)) collapseTool(tool);
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1086
1268
|
const markRoundToolUserExpanded = (tool: any): void => {
|
|
1087
1269
|
const id = String(tool?.toolCallId ?? "");
|
|
1088
1270
|
if (!id || !expandedRoundToolIds.has(id)) return;
|
|
@@ -1090,11 +1272,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1090
1272
|
const otherId = String(other?.toolCallId ?? "");
|
|
1091
1273
|
if (other === tool || !expandedRoundToolIds.has(otherId) || other.expanded !== true) continue;
|
|
1092
1274
|
explicitRoundToolIds.delete(otherId);
|
|
1093
|
-
|
|
1094
|
-
else {
|
|
1095
|
-
other.expanded = false;
|
|
1096
|
-
other.updateDisplay?.();
|
|
1097
|
-
}
|
|
1275
|
+
collapseTool(other);
|
|
1098
1276
|
}
|
|
1099
1277
|
explicitRoundToolIds.add(id);
|
|
1100
1278
|
};
|
|
@@ -1105,6 +1283,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1105
1283
|
...round.detachedMessages,
|
|
1106
1284
|
];
|
|
1107
1285
|
|
|
1286
|
+
/** 镜像给常驻状态行的文案:只认当前活动回合(摘要行被顶出视口时由 working-message 取用)。 */
|
|
1287
|
+
compactRunStatusGetter = () => {
|
|
1288
|
+
const round = activeRound;
|
|
1289
|
+
return round?.active ? summarize(roundMessages(round), true, round) || undefined : undefined;
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1108
1292
|
const roundToolCallIds = (round: CompactRound): Set<string> => {
|
|
1109
1293
|
const ids = new Set<string>();
|
|
1110
1294
|
for (const message of roundMessages(round)) {
|
|
@@ -1115,19 +1299,339 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1115
1299
|
return ids;
|
|
1116
1300
|
};
|
|
1117
1301
|
|
|
1302
|
+
/**
|
|
1303
|
+
* anchor 所在的 transcript 容器(pi chatContainer)。
|
|
1304
|
+
* 优先走分组补丁在 addChild 时挂的父指针;没有则沿 tui 组件树找直属父容器。
|
|
1305
|
+
*/
|
|
1306
|
+
const tailContainerOf = (anchor: any): any => {
|
|
1307
|
+
const keyed = anchor?.[TOOL_GROUPING_PARENT_KEY];
|
|
1308
|
+
if (keyed && Array.isArray(keyed.children) && keyed.children.includes(anchor)) {
|
|
1309
|
+
return keyed;
|
|
1310
|
+
}
|
|
1311
|
+
let found: any;
|
|
1312
|
+
walkComponentTree(getToolMouseTui(), (value: any) => {
|
|
1313
|
+
if (found) return false;
|
|
1314
|
+
if (Array.isArray(value?.children) && value.children.includes(anchor)) {
|
|
1315
|
+
found = value;
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
return undefined;
|
|
1319
|
+
});
|
|
1320
|
+
return found;
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const ensureTailHost = (round: CompactRound): MountedRoundTail => {
|
|
1324
|
+
if (round.tail) return round.tail;
|
|
1325
|
+
// 同一 anchor 上残留的旧回合尾行(reset/重组后建新回合)先摘除,避免双摘要。
|
|
1326
|
+
const stale = tailByAnchor.get(round.anchor);
|
|
1327
|
+
if (stale) {
|
|
1328
|
+
stale.container?.removeChild?.(stale.host);
|
|
1329
|
+
mountedTails.delete(stale);
|
|
1330
|
+
tailByAnchor.delete(round.anchor);
|
|
1331
|
+
}
|
|
1332
|
+
const tail: MountedRoundTail = { host: undefined, container: undefined, parts: [], round };
|
|
1333
|
+
const host: any = {
|
|
1334
|
+
outputPad: Number(round.anchor?.outputPad) || 0,
|
|
1335
|
+
render: (width: number) => tail.parts.flatMap((part: any) => part?.render?.(width) ?? []),
|
|
1336
|
+
invalidate: () => {
|
|
1337
|
+
for (const part of tail.parts) part?.invalidate?.();
|
|
1338
|
+
},
|
|
1339
|
+
};
|
|
1340
|
+
Object.defineProperty(host, "expanded", {
|
|
1341
|
+
configurable: true,
|
|
1342
|
+
get: () => tail.round?.anchor?.expanded === true,
|
|
1343
|
+
set: (value: boolean) => {
|
|
1344
|
+
const anchor = tail.round?.anchor;
|
|
1345
|
+
if (anchor) anchor.expanded = value;
|
|
1346
|
+
},
|
|
1347
|
+
});
|
|
1348
|
+
// 点击/悬停归属与 anchor 同一展开入口;toggle 每次 renderRound 重建,按值转发。
|
|
1349
|
+
// 经 tail.round 动态解引用:重建过户时 anchor 可能换成别的组件。
|
|
1350
|
+
ensureAssistantSetExpanded(host);
|
|
1351
|
+
host[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) =>
|
|
1352
|
+
tail.round?.anchor?.[ASSISTANT_TOGGLE_ROUND_KEY]?.(expanded);
|
|
1353
|
+
tail.host = host;
|
|
1354
|
+
tailByAnchor.set(round.anchor, tail);
|
|
1355
|
+
mountedTails.add(tail);
|
|
1356
|
+
round.tail = tail;
|
|
1357
|
+
return tail;
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* 尾行归位。运行中(含收尾静默期)尾行恒为容器最末一行:计数行钉在
|
|
1362
|
+
* transcript 底缘,运行中到达的提示行/custom 条目只能落在它上方,计数
|
|
1363
|
+
* 不会被顶离底部截成两行。回合收尾后摘要行回落到回合最后一个成员之后,
|
|
1364
|
+
* 外来行统一换到摘要行之下,通知行永远落在回合边界外。
|
|
1365
|
+
*/
|
|
1366
|
+
const syncTailPosition = (round: CompactRound): void => {
|
|
1367
|
+
const tail = round.tail;
|
|
1368
|
+
if (!tail?.host) return;
|
|
1369
|
+
const container =
|
|
1370
|
+
tail.container?.children?.includes?.(round.anchor) === true
|
|
1371
|
+
? tail.container
|
|
1372
|
+
: tailContainerOf(round.anchor);
|
|
1373
|
+
if (!container || !Array.isArray(container.children)) return;
|
|
1374
|
+
tail.container = container;
|
|
1375
|
+
const children = container.children as any[];
|
|
1376
|
+
const current = children.indexOf(tail.host);
|
|
1377
|
+
if (current >= 0) children.splice(current, 1);
|
|
1378
|
+
const ids = roundToolCallIds(round);
|
|
1379
|
+
const matchesTool = (value: any): boolean =>
|
|
1380
|
+
typeof value?.toolCallId === "string" && ids.has(value.toolCallId);
|
|
1381
|
+
const isMember = (value: any): boolean =>
|
|
1382
|
+
round.messages.has(value) ||
|
|
1383
|
+
matchesTool(value) ||
|
|
1384
|
+
(Array.isArray(value?.children) && value.children.some(matchesTool));
|
|
1385
|
+
let first = -1;
|
|
1386
|
+
let last = -1;
|
|
1387
|
+
for (let i = 0; i < children.length; i++) {
|
|
1388
|
+
if (!isMember(children[i])) continue;
|
|
1389
|
+
if (first < 0) first = i;
|
|
1390
|
+
last = i;
|
|
1391
|
+
}
|
|
1392
|
+
const lastMember = last >= 0 ? children[last] : undefined;
|
|
1393
|
+
const intruders: any[] = [];
|
|
1394
|
+
for (let i = last - 1; i > first; i--) {
|
|
1395
|
+
if (!isMember(children[i])) intruders.unshift(children.splice(i, 1)[0]);
|
|
1396
|
+
}
|
|
1397
|
+
if (lastMember) last = children.indexOf(lastMember);
|
|
1398
|
+
const insertAt = last < 0 ? children.length : last + 1;
|
|
1399
|
+
if (roundLive(round)) {
|
|
1400
|
+
// 活回合:外来行压到最后成员之后,尾行占容器最末。
|
|
1401
|
+
children.splice(insertAt, 0, ...intruders);
|
|
1402
|
+
children.push(tail.host);
|
|
1403
|
+
} else {
|
|
1404
|
+
children.splice(insertAt, 0, tail.host, ...intruders);
|
|
1405
|
+
}
|
|
1406
|
+
tail.host[TOOL_GROUPING_PARENT_KEY] = container;
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const unmountRoundTail = (round: CompactRound): void => {
|
|
1410
|
+
const tail = round.tail;
|
|
1411
|
+
if (!tail) return;
|
|
1412
|
+
round.tail = undefined;
|
|
1413
|
+
tail.container?.removeChild?.(tail.host);
|
|
1414
|
+
mountedTails.delete(tail);
|
|
1415
|
+
if (tailByAnchor.get(round.anchor) === tail) tailByAnchor.delete(round.anchor);
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* pi 把新工具卡 append 在容器末尾(尾行之后):updateDisplay 时把所属回合的
|
|
1420
|
+
* 尾行压回末尾。只有 live/收尾静默期的回合还会长工具。
|
|
1421
|
+
*/
|
|
1422
|
+
const syncTailForTool = (toolCallId: string): void => {
|
|
1423
|
+
if (!toolCallId) return;
|
|
1424
|
+
for (const round of [activeRound, ...pendingFoldRounds]) {
|
|
1425
|
+
if (round?.tail && roundToolCallIds(round).has(toolCallId)) {
|
|
1426
|
+
syncTailPosition(round);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* refresh 重建时继承旧回合:resetRounds 只清索引不摘 compact 尾行,
|
|
1434
|
+
* 残留尾行带着旧 round。同一组件重放同一消息(lastMessage 回放)
|
|
1435
|
+
* 才算重建——保留挂钟起点、已脱离的工具历史与尾行组件本身;
|
|
1436
|
+
* 旧回合已收尾的直接落成折叠态(不回摆 Running、不重演静默期),
|
|
1437
|
+
* 仍活动的只接回挂钟,Running 时长继续走。
|
|
1438
|
+
*/
|
|
1439
|
+
const adoptPriorRound = (round: CompactRound, message: any): void => {
|
|
1440
|
+
let prior: CompactRound | undefined;
|
|
1441
|
+
for (const tail of mountedTails) {
|
|
1442
|
+
const stale = tail.round as CompactRound | undefined;
|
|
1443
|
+
if (
|
|
1444
|
+
stale &&
|
|
1445
|
+
(stale.messages.get(round.anchor) === message || stale.detachedMessages.includes(message))
|
|
1446
|
+
) {
|
|
1447
|
+
prior = stale;
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (!prior) return;
|
|
1452
|
+
round.startedAt = prior.startedAt;
|
|
1453
|
+
if (prior.detachedMessages.length) round.detachedMessages.push(...prior.detachedMessages);
|
|
1454
|
+
if (!prior.active) {
|
|
1455
|
+
round.active = false;
|
|
1456
|
+
round.endedAt = prior.endedAt ?? round.startedAt;
|
|
1457
|
+
}
|
|
1458
|
+
const tail = prior.tail;
|
|
1459
|
+
if (tail) {
|
|
1460
|
+
prior.tail = undefined;
|
|
1461
|
+
if (tailByAnchor.get(prior.anchor) === tail) tailByAnchor.delete(prior.anchor);
|
|
1462
|
+
tail.round = round;
|
|
1463
|
+
tailByAnchor.set(round.anchor, tail);
|
|
1464
|
+
round.tail = tail;
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
/** 新建活回合:literal + 激活 + refresh 重建时的旧回合收编。 */
|
|
1469
|
+
const createRound = (anchor: any, message: any): CompactRound => {
|
|
1470
|
+
const round: CompactRound = {
|
|
1471
|
+
anchor,
|
|
1472
|
+
messages: new Map(),
|
|
1473
|
+
detachedMessages: [],
|
|
1474
|
+
active: true,
|
|
1475
|
+
suppressedToolIds: new Set(),
|
|
1476
|
+
liveSlotToolIds: new Set(),
|
|
1477
|
+
startedAt: Date.now(),
|
|
1478
|
+
};
|
|
1479
|
+
activateRound(round);
|
|
1480
|
+
adoptPriorRound(round, message);
|
|
1481
|
+
return round;
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* 尾行挂载入口:makeParts 按渲染态产出内容(折叠态 [摘要行],live 态
|
|
1486
|
+
* [摘要行, 槽位卡]),identity 决定行内 hover/点击归属组件(尾行模式下是
|
|
1487
|
+
* host,hit-test 直达它)。anchor 未挂进任何容器时(独立渲染)退回
|
|
1488
|
+
* contentContainer——与旧行为一致。
|
|
1489
|
+
*/
|
|
1490
|
+
const mountRoundTail = (round: CompactRound, makeParts: (identity: any) => any[]): void => {
|
|
1491
|
+
if (!tailContainerOf(round.anchor)) {
|
|
1492
|
+
// anchor 不在任何容器里(独立渲染/未挂载):退回 anchor 内部,与旧行为一致。
|
|
1493
|
+
unmountRoundTail(round);
|
|
1494
|
+
for (const part of makeParts(round.anchor)) {
|
|
1495
|
+
round.anchor?.contentContainer?.addChild?.(part);
|
|
1496
|
+
}
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const tail = ensureTailHost(round);
|
|
1500
|
+
tail.parts = makeParts(tail.host);
|
|
1501
|
+
syncTailPosition(round);
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* 末块仍是 thinking = 思考还在长;后面跟了正文或工具调用就算结束。
|
|
1506
|
+
* 围观态只预览这一种,已完成的思考直接回收进摘要行,不在屏幕上留 Thought 行。
|
|
1507
|
+
*/
|
|
1508
|
+
const hasTrailingThinking = (message: any): boolean => {
|
|
1509
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
1510
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
1511
|
+
const item = content[i];
|
|
1512
|
+
if (item?.type === "thinking") return true;
|
|
1513
|
+
if (item?.type === "toolCall") return false;
|
|
1514
|
+
if (item?.type === "text" && String(item.text ?? "").trim()) return false;
|
|
1515
|
+
}
|
|
1516
|
+
return false;
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
/** 运行中的工具:已开始且尚无最终结果。 */
|
|
1520
|
+
const isRunningTool = (tool: any): boolean =>
|
|
1521
|
+
tool?.executionStarted === true && (!tool.result || tool.isPartial === true);
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* live 围观态:正文 + 摘要行照折叠态渲染,只在摘要行下多一张槽位卡。
|
|
1525
|
+
* - 思考:只预览最后一条仍在思考的消息,它的块是当前最新内容;
|
|
1526
|
+
* - 工具:没有活动思考时才占槽位,运行中优先,否则取本回合最近的,完成也留着;
|
|
1527
|
+
* - 槽位只放一个块(思考或工具),新的思考块/工具调用进来才轮换;
|
|
1528
|
+
* - 本回合所有工具都由槽位卡接管,外层不再单独成行。
|
|
1529
|
+
* 可见性判断只在这里发生;回合结束(renderRound 非 active)只剩槽位卡消失。
|
|
1530
|
+
*/
|
|
1531
|
+
const renderRoundLive = (
|
|
1532
|
+
round: CompactRound,
|
|
1533
|
+
stopStatus: string | undefined,
|
|
1534
|
+
getSummary: () => string,
|
|
1535
|
+
): void => {
|
|
1536
|
+
// 仍在思考的消息 = 当前最新内容,优先占槽位。
|
|
1537
|
+
let previewComponent: any;
|
|
1538
|
+
for (const [component, message] of round.messages) {
|
|
1539
|
+
if (hasTrailingThinking(message)) previewComponent = component;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const thinkingKids: any[] = [];
|
|
1543
|
+
for (const [component, message] of round.messages) {
|
|
1544
|
+
if (component !== previewComponent) {
|
|
1545
|
+
component.contentContainer?.clear?.();
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
passThroughAssistant(component, message);
|
|
1549
|
+
const kids = Array.isArray(component.contentContainer?.children)
|
|
1550
|
+
? [...component.contentContainer.children]
|
|
1551
|
+
: [];
|
|
1552
|
+
component.contentContainer?.clear?.();
|
|
1553
|
+
// 只摘思考预览块进槽位;已收尾的思考回收进摘要行。
|
|
1554
|
+
for (const kid of kids) {
|
|
1555
|
+
if (typeof kid?.setHintHovered === "function") thinkingKids.push(kid);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
// 正文照折叠态渲染(不含 thinking):live 与收尾后同形,收尾只剩槽位卡消失。
|
|
1559
|
+
renderAssistantWithoutThinking(round.anchor, round.messages.get(round.anchor));
|
|
1560
|
+
|
|
1561
|
+
const toolsById = new Map<string, any>();
|
|
1562
|
+
for (const tool of trackedToolComponents) {
|
|
1563
|
+
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
1564
|
+
}
|
|
1565
|
+
let runningTool: any;
|
|
1566
|
+
let lastTool: any;
|
|
1567
|
+
for (const [, message] of round.messages) {
|
|
1568
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1569
|
+
if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
|
|
1570
|
+
const tool = toolsById.get(item.id);
|
|
1571
|
+
// edit/write 保留自己的 compact 行,不进槽位。
|
|
1572
|
+
if (EDIT_WRITE_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
|
|
1573
|
+
round.liveSlotToolIds.add(item.id);
|
|
1574
|
+
liveSlotToolIds.add(item.id);
|
|
1575
|
+
if (!tool) continue;
|
|
1576
|
+
lastTool = tool;
|
|
1577
|
+
if (isRunningTool(tool)) runningTool = tool;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
// 槽位单块:活动思考最新;否则本回合最近的工具(运行中原位增长,完成也留着)。
|
|
1581
|
+
const slotTool = thinkingKids.length > 0 ? undefined : (runningTool ?? lastTool);
|
|
1582
|
+
const cardItems: Array<{ child?: any; tool?: any }> = slotTool
|
|
1583
|
+
? [{ tool: slotTool }]
|
|
1584
|
+
: thinkingKids.map((child) => ({ child }));
|
|
1585
|
+
|
|
1586
|
+
const anchor = round.anchor;
|
|
1587
|
+
const pad = Number(anchor.outputPad) || 0;
|
|
1588
|
+
// 摘要行与槽位卡挂回合尾行(transcript 容器内回合末尾,而非 anchor 内部):
|
|
1589
|
+
// 运行中恒在可写视口底缘,收尾的 Ran for 原地落进 scrollback,不再留下
|
|
1590
|
+
// 被挤出视口后定格的 Running 帧。
|
|
1591
|
+
mountRoundTail(round, (identity) => {
|
|
1592
|
+
const parts: any[] = [compactAssistantLineComponent(identity, getSummary, deps.query)];
|
|
1593
|
+
if (cardItems.length > 0) {
|
|
1594
|
+
parts.push(
|
|
1595
|
+
compactLiveSlot(
|
|
1596
|
+
compactRoundCard(
|
|
1597
|
+
cardItems,
|
|
1598
|
+
(tool, innerWidth) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
1599
|
+
false,
|
|
1600
|
+
true,
|
|
1601
|
+
),
|
|
1602
|
+
pad,
|
|
1603
|
+
),
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
return parts;
|
|
1607
|
+
});
|
|
1608
|
+
appendStopStatus(anchor, stopStatus);
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1118
1611
|
const renderRound = (round: CompactRound): void => {
|
|
1119
1612
|
const messages = roundMessages(round);
|
|
1120
1613
|
const stopStatus = roundStopStatus(messages);
|
|
1121
1614
|
if (stopStatus) endRound(round);
|
|
1122
1615
|
// Running 时每次 render 重算时长(含挂钟下限);结束后固定。
|
|
1616
|
+
// 静默期内回合已收尾,摘要行直接给 Ran for,只有槽位卡多留一拍。
|
|
1123
1617
|
const getSummary = () => summarize(roundMessages(round), round.active, round);
|
|
1124
1618
|
const summary = getSummary();
|
|
1125
1619
|
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
1126
1620
|
round.suppressedToolIds.clear();
|
|
1127
|
-
//
|
|
1621
|
+
// 回合收起(或本回合不在展开态)时忘掉用户的单开选择,并把面板接管的工具一起收回:
|
|
1622
|
+
// 少了后一步,被点开的工具会带着自身的 expanded 落到 toolInstalledRender 的放行分支,
|
|
1623
|
+
// 面板一收就单独渲染成一张卡。
|
|
1128
1624
|
if (round.anchor.expanded !== true) {
|
|
1129
|
-
|
|
1625
|
+
const ids = roundToolCallIds(round);
|
|
1626
|
+
for (const id of ids) explicitRoundToolIds.delete(id);
|
|
1627
|
+
collapseTools(
|
|
1628
|
+
(tool) =>
|
|
1629
|
+
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1630
|
+
!EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")),
|
|
1631
|
+
);
|
|
1130
1632
|
}
|
|
1633
|
+
for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
|
|
1634
|
+
round.liveSlotToolIds.clear();
|
|
1131
1635
|
|
|
1132
1636
|
for (const [component, message] of round.messages) {
|
|
1133
1637
|
component.lastMessage = message;
|
|
@@ -1146,6 +1650,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1146
1650
|
}
|
|
1147
1651
|
|
|
1148
1652
|
if (round.anchor.expanded === true) {
|
|
1653
|
+
// 展开卡自带全部内容,尾行摘下(收起时在折叠分支重新挂载)。
|
|
1654
|
+
unmountRoundTail(round);
|
|
1149
1655
|
const toolsById = new Map<string, any>();
|
|
1150
1656
|
for (const tool of trackedToolComponents) {
|
|
1151
1657
|
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
@@ -1248,21 +1754,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1248
1754
|
}
|
|
1249
1755
|
// Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕;
|
|
1250
1756
|
// 用户单独点开的(explicitRoundToolIds)不压回去。
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
EDIT_WRITE_TOOLS.has(String(tool.toolName ?? ""))
|
|
1255
|
-
tool.
|
|
1256
|
-
|
|
1257
|
-
) {
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
if (typeof tool.setExpanded === "function") tool.setExpanded(false);
|
|
1261
|
-
else {
|
|
1262
|
-
tool.expanded = false;
|
|
1263
|
-
tool.updateDisplay?.();
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1757
|
+
collapseTools(
|
|
1758
|
+
(tool) =>
|
|
1759
|
+
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1760
|
+
!EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")) &&
|
|
1761
|
+
!explicitRoundToolIds.has(String(tool.toolCallId ?? "")),
|
|
1762
|
+
);
|
|
1266
1763
|
flushPanel();
|
|
1267
1764
|
for (const child of anchorChildren) round.anchor.contentContainer.addChild(child);
|
|
1268
1765
|
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
@@ -1270,11 +1767,23 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1270
1767
|
return;
|
|
1271
1768
|
}
|
|
1272
1769
|
|
|
1770
|
+
if (roundLive(round)) {
|
|
1771
|
+
// 围观态:单一槽位卡(活动思考预览 + 当前工具),回合结束自动收回。
|
|
1772
|
+
renderRoundLive(round, stopStatus, getSummary);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1273
1776
|
for (const [component, message] of round.messages) {
|
|
1274
1777
|
if (component === round.anchor) {
|
|
1275
1778
|
renderAssistantWithoutThinking(component, message);
|
|
1276
1779
|
// 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)
|
|
1277
|
-
if (summary || round.active)
|
|
1780
|
+
if (summary || round.active) {
|
|
1781
|
+
mountRoundTail(round, (identity) => [
|
|
1782
|
+
compactAssistantLineComponent(identity, getSummary, deps.query),
|
|
1783
|
+
]);
|
|
1784
|
+
} else {
|
|
1785
|
+
unmountRoundTail(round);
|
|
1786
|
+
}
|
|
1278
1787
|
// 折叠时工具行被隐藏:abort/error/length 必须挂在摘要外层。
|
|
1279
1788
|
appendStopStatus(component, stopStatus);
|
|
1280
1789
|
} else {
|
|
@@ -1284,6 +1793,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1284
1793
|
};
|
|
1285
1794
|
|
|
1286
1795
|
const activateRound = (round: CompactRound): void => {
|
|
1796
|
+
flushSettle();
|
|
1287
1797
|
round.active = true;
|
|
1288
1798
|
if (!round.startedAt) round.startedAt = Date.now();
|
|
1289
1799
|
delete round.endedAt;
|
|
@@ -1295,10 +1805,28 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1295
1805
|
const finishRound = (round: CompactRound): void => endRound(round, true);
|
|
1296
1806
|
|
|
1297
1807
|
const resetRounds = (): void => {
|
|
1808
|
+
for (const round of [...pendingFoldRounds]) clearFoldTimer(round);
|
|
1298
1809
|
activeRound = undefined;
|
|
1299
1810
|
roundByComponent = new WeakMap();
|
|
1300
1811
|
expandedRoundToolIds.clear();
|
|
1301
1812
|
explicitRoundToolIds.clear();
|
|
1813
|
+
liveSlotToolIds.clear();
|
|
1814
|
+
// 尾行是 transcript 兄弟组件:compact 下保留为静态行(与原 anchor 内摘要
|
|
1815
|
+
// 一致;同 anchor 重建回合时 ensureTailHost 去重摘除),非 compact 摘掉,
|
|
1816
|
+
// 别把紧凑摘要带进 default 渲染。
|
|
1817
|
+
for (const tail of [...mountedTails]) {
|
|
1818
|
+
if (tail.container?.children?.includes?.(tail.host) !== true) {
|
|
1819
|
+
mountedTails.delete(tail);
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
if (config.mode === "compact") continue;
|
|
1823
|
+
tail.container.removeChild(tail.host);
|
|
1824
|
+
mountedTails.delete(tail);
|
|
1825
|
+
if (tail.round?.tail === tail) tail.round.tail = undefined;
|
|
1826
|
+
if (tail.round?.anchor && tailByAnchor.get(tail.round.anchor) === tail) {
|
|
1827
|
+
tailByAnchor.delete(tail.round.anchor);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1302
1830
|
deps.query?.setCompactSummaryActive?.(false);
|
|
1303
1831
|
stopRoundTick();
|
|
1304
1832
|
};
|
|
@@ -1331,27 +1859,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1331
1859
|
if (hasText && (!round || round.anchor !== this)) {
|
|
1332
1860
|
if (round) round.messages.delete(this);
|
|
1333
1861
|
if (activeRound) finishRound(activeRound);
|
|
1334
|
-
round =
|
|
1335
|
-
anchor: this,
|
|
1336
|
-
messages: new Map(),
|
|
1337
|
-
detachedMessages: [],
|
|
1338
|
-
active: true,
|
|
1339
|
-
suppressedToolIds: new Set(),
|
|
1340
|
-
startedAt: Date.now(),
|
|
1341
|
-
};
|
|
1862
|
+
round = createRound(this, message);
|
|
1342
1863
|
roundByComponent.set(this, round);
|
|
1343
|
-
activateRound(round);
|
|
1344
1864
|
} else if (!round) {
|
|
1345
|
-
round = activeRound ??
|
|
1346
|
-
anchor: this,
|
|
1347
|
-
messages: new Map(),
|
|
1348
|
-
detachedMessages: [],
|
|
1349
|
-
active: true,
|
|
1350
|
-
suppressedToolIds: new Set(),
|
|
1351
|
-
startedAt: Date.now(),
|
|
1352
|
-
};
|
|
1865
|
+
round = activeRound ?? createRound(this, message);
|
|
1353
1866
|
roundByComponent.set(this, round);
|
|
1354
|
-
if (!activeRound) activateRound(round);
|
|
1355
1867
|
}
|
|
1356
1868
|
round.messages.set(this, message);
|
|
1357
1869
|
renderRound(round);
|
|
@@ -1371,7 +1883,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1371
1883
|
return renderAssistantWithoutThinking(this, message);
|
|
1372
1884
|
}
|
|
1373
1885
|
if (round) {
|
|
1374
|
-
|
|
1886
|
+
// 围观态下 anchor 还挂着槽位卡,必须重绘才能收回。
|
|
1887
|
+
endRound(round, true);
|
|
1375
1888
|
roundByComponent.delete(this);
|
|
1376
1889
|
return passThroughAssistant(this, message);
|
|
1377
1890
|
}
|
|
@@ -1383,16 +1896,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1383
1896
|
if (hasThinking) {
|
|
1384
1897
|
let round = roundByComponent.get(this);
|
|
1385
1898
|
if (!round) {
|
|
1386
|
-
round = activeRound ??
|
|
1387
|
-
anchor: this,
|
|
1388
|
-
messages: new Map(),
|
|
1389
|
-
detachedMessages: [],
|
|
1390
|
-
active: true,
|
|
1391
|
-
suppressedToolIds: new Set(),
|
|
1392
|
-
startedAt: Date.now(),
|
|
1393
|
-
};
|
|
1899
|
+
round = activeRound ?? createRound(this, message);
|
|
1394
1900
|
roundByComponent.set(this, round);
|
|
1395
|
-
if (!activeRound) activateRound(round);
|
|
1396
1901
|
}
|
|
1397
1902
|
round.messages.set(this, message);
|
|
1398
1903
|
renderRound(round);
|
|
@@ -1424,6 +1929,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1424
1929
|
}
|
|
1425
1930
|
// Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
|
|
1426
1931
|
if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
1932
|
+
// live 围观态:本回合工具交由槽位卡内联渲染,外层不再单独成行,
|
|
1933
|
+
// 卡片因此不会随工具完成反复挂载/卸载。
|
|
1934
|
+
if (liveSlotToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
1427
1935
|
// 普通工具折叠时不显示独立行(摘要行已统计),独立展开走原 renderer。
|
|
1428
1936
|
if (this.expanded === true) {
|
|
1429
1937
|
return patch.toolOriginalRender.call(this, width);
|
|
@@ -1441,6 +1949,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1441
1949
|
const result = patch.toolOriginalUpdateDisplay.call(this);
|
|
1442
1950
|
if (!patch.active) return result;
|
|
1443
1951
|
trackedToolComponents.add(this);
|
|
1952
|
+
if (config.mode === "compact") syncTailForTool(id);
|
|
1444
1953
|
return result;
|
|
1445
1954
|
};
|
|
1446
1955
|
|
|
@@ -1482,6 +1991,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1482
1991
|
trackedToolComponents.clear();
|
|
1483
1992
|
hoveredAssistantComponent = undefined;
|
|
1484
1993
|
resetRounds();
|
|
1994
|
+
compactRunStatusGetter = undefined;
|
|
1485
1995
|
};
|
|
1486
1996
|
|
|
1487
1997
|
assistantPrototype.updateContent = patch.assistantInstalled;
|