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.
@@ -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 { hasVisibleText, stripBackgroundAnsi } from "../utils/ansi-text.ts";
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
- const innerWidth = Math.max(0, width - 2);
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
- // 展开的 thinking 带深色内卡(自带上下 padding,不再补外卡分隔行)。
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
- : (line: string) => lines.push(paddedBackgroundRow(theme, slot, line, width));
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 = Math.max(0, width - 2);
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
- let text = theme.fg("muted", plainText);
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)}${theme.fg("muted", tools)}`;
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
- function compactAssistantLine(
898
- component: any,
899
- summary: string | (() => string),
900
- query?: CompactThinkingQuery,
901
- ): void {
902
- // 空静态串跳过;getter 留给 render 判断(Running 可能稍后才有时长)。
903
- if (typeof summary === "string" && !summary) return;
904
- component.contentContainer.addChild(compactAssistantLineComponent(component, summary, query));
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 endRound = (round: CompactRound, render = false): void => {
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
- if (render) renderRound(round);
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
- if (typeof other.setExpanded === "function") other.setExpanded(false);
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
- for (const id of roundToolCallIds(round)) explicitRoundToolIds.delete(id);
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
- for (const tool of trackedToolComponents) {
1252
- if (
1253
- !ids.has(tool.toolCallId) ||
1254
- EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")) ||
1255
- tool.expanded !== true ||
1256
- explicitRoundToolIds.has(String(tool.toolCallId ?? ""))
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) compactAssistantLine(component, getSummary, deps.query);
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
- endRound(round);
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;