pi-cc-extensions 0.9.5 → 0.9.7
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 +2 -2
- package/README.md +2 -2
- package/extensions/feature/compact-thinking.ts +1 -1
- package/extensions/feature/shell/working-message.ts +21 -2
- package/extensions/renderer/compact-mode.ts +493 -88
- package/extensions/renderer/default-mode.ts +23 -14
- package/extensions/renderer/index.ts +9 -1
- package/extensions/renderer/mouse/interaction.ts +180 -79
- package/extensions/renderer/mouse/layout.ts +14 -5
- package/extensions/renderer/mouse/scroll.ts +43 -4
- package/extensions/renderer/tool/grouping.ts +3 -2
- package/extensions/renderer/tool/show-more-hint.ts +10 -0
- package/extensions/utils/ansi-text.ts +29 -0
- package/package.json +2 -1
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compact mode:每条含 toolCall 的 assistant message 折叠为一条逐步累加的摘要行
|
|
3
3
|
* (`Ran for 8s, bash×2, read×2`),edit/write 独立标题行(`✓ write <path> (+25 -0)`),
|
|
4
|
-
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen
|
|
5
|
-
* compact-thinking/Pi 原生或专用
|
|
4
|
+
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)时助手文本按原生渲染,
|
|
5
|
+
* thinking 与工具卡装进 userMessageBg 面板(卡内恢复 compact-thinking/Pi 原生或专用
|
|
6
|
+
* renderer)。edit/write 复用 mode=on 的 rich diff
|
|
6
7
|
* 与 `editDiffCollapsedLines` / `writeDiffCollapsedLines` / `expandedPreviewMaxLines`。
|
|
7
8
|
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
8
9
|
* 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
|
|
9
10
|
*
|
|
11
|
+
* live 围观态(回合进行中 / 收尾静默期):正文与摘要行跟折叠态同形,只在摘要行下面多一张
|
|
12
|
+
* 单一槽位卡(`↳` + 最新的一个块:仍在增长的思考预览,或本回合最近的工具,运行中优先)。
|
|
13
|
+
* 思考原地增长不轮换,只有新的思考块或工具调用进来才换槽位内容。
|
|
14
|
+
* 收尾时摘要行原地从 Running... 翻成 Ran for、槽位卡消失,正文与摘要行都不换位置。
|
|
15
|
+
*
|
|
10
16
|
* 工具计数:read 按非空路径去重、其余按调用计数(首次出现顺序);edit/write 不进摘要。
|
|
11
17
|
* 时长 = 回合流逝挂钟;进行中 Running...,结束 Ran for。
|
|
12
18
|
* abort/error/length 状态行挂在摘要外层,避免被折叠吞掉。
|
|
@@ -35,10 +41,16 @@ import {
|
|
|
35
41
|
import { renderRichToolResult } from "./tool/diff/index.ts";
|
|
36
42
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
37
43
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
44
|
+
import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
38
45
|
import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
|
|
39
46
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
40
47
|
import { formatDisplayPath } from "./tool/names.ts";
|
|
41
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
hasVisibleText,
|
|
50
|
+
removeStyledRange,
|
|
51
|
+
stripBackgroundAnsi,
|
|
52
|
+
stripTerminalSequencesPreservingLayout,
|
|
53
|
+
} from "../utils/ansi-text.ts";
|
|
42
54
|
import { walkComponentTree } from "../utils/component-tree.ts";
|
|
43
55
|
import {
|
|
44
56
|
ASSISTANT_REENTRY_KEY,
|
|
@@ -50,6 +62,11 @@ import {
|
|
|
50
62
|
PROTOTYPE_ORIGINAL_KEY,
|
|
51
63
|
} from "../utils/patch-keys.ts";
|
|
52
64
|
|
|
65
|
+
/** Pi 的 native 组件与扩展可能各持一份 pi-tui,Spacer 不能只靠 instanceof 判。 */
|
|
66
|
+
function isSpacerComponent(value: any): boolean {
|
|
67
|
+
return value instanceof Spacer || value?.constructor?.name === "Spacer";
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
/** compact 渲染层对 compact-thinking 的只读查询面(不建第二套计时器)。 */
|
|
54
71
|
export type CompactThinkingQuery = {
|
|
55
72
|
getMessageThinkingDurationMs(messageTimestamp: number): number | undefined;
|
|
@@ -60,6 +77,12 @@ export type CompactThinkingQuery = {
|
|
|
60
77
|
|
|
61
78
|
const EDIT_WRITE_TOOLS = new Set(["edit", "write"]);
|
|
62
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 回合收尾后延迟收拢的静默期(ms)。正文 token 往往先于 toolCall 到达,
|
|
82
|
+
* 立刻收拢会得到「先收再开」两次转场;静默期内被新回合接替则一并收拢。
|
|
83
|
+
*/
|
|
84
|
+
const FOLD_SETTLE_MS = 250;
|
|
85
|
+
|
|
63
86
|
type CompactThinkingTheme = Pick<Theme, "fg" | "italic" | "bold">;
|
|
64
87
|
|
|
65
88
|
/** 与 compact-thinking 主渲染器共用的静态文字样式。 */
|
|
@@ -320,46 +343,66 @@ function editWriteExpandedCard(theme: any): any {
|
|
|
320
343
|
);
|
|
321
344
|
}
|
|
322
345
|
|
|
323
|
-
/** compact
|
|
324
|
-
*
|
|
325
|
-
*
|
|
346
|
+
/** compact 展开面板:外卡片保持 userMessageBg;展开的 thinking 另包一层更深的
|
|
347
|
+
* 内卡(自身 Box 背景与外卡同色,不区分会糊在一起),工具卡直接铺在外卡背景上。
|
|
348
|
+
* 每个子卡前面补 1 行分隔(上一条已经是空行时不再补),子卡自带的首尾空行会被裁掉,
|
|
349
|
+
* 避免与外卡/内卡 padding 叠出双空行。
|
|
350
|
+
* hits:子卡的行区间与命中目标(thinking 命中自身,工具命中 tool 组件),
|
|
351
|
+
* 供展开后点击 hint。未开启 toolHits 时不映射工具,点击仍归外层面板。
|
|
352
|
+
* live=true:live 槽位用,不铺底色也不留上下空行,首行紧跟 `↳`、其余行缩进对齐
|
|
353
|
+
* (Claude Code 的 ⎿ 口径)。行的产出仍是一行对一行,hits 行号无需平移。
|
|
354
|
+
*/
|
|
326
355
|
function layoutExpandedToolCard(
|
|
327
356
|
theme: any,
|
|
328
357
|
children: any[],
|
|
329
358
|
width: number,
|
|
330
359
|
paints?: string[][],
|
|
360
|
+
toolHits = false,
|
|
361
|
+
live = false,
|
|
331
362
|
): { lines: string[]; hits: Array<{ child: any; start: number; end: number }> } {
|
|
332
363
|
const slot = "userMessageBg";
|
|
333
364
|
const toolBgAnsi = darkenBgAnsi(theme, slot);
|
|
334
|
-
|
|
365
|
+
/** live 每行前面的标记宽度(首行 `↳ `,其余对齐空格)。 */
|
|
366
|
+
const markerWidth = live ? 2 : 0;
|
|
367
|
+
const innerWidth = Math.max(0, width - 2 - markerWidth);
|
|
335
368
|
const lines: string[] = [];
|
|
336
369
|
const hits: Array<{ child: any; start: number; end: number }> = [];
|
|
337
370
|
const isThinkingPreview = (child: any) => typeof child?.setHintHovered === "function";
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
let
|
|
371
|
+
let lastBlank = false;
|
|
372
|
+
/** live 首行被吃掉的前导缩进:续行同步左移同样格数,保持块内相对对齐。 */
|
|
373
|
+
let liveLead = 0;
|
|
374
|
+
const pushRow = (raw: string) => {
|
|
375
|
+
if (!live) {
|
|
376
|
+
lines.push(paddedBackgroundRow(theme, slot, raw, width));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// 槽位里不重复展开入口(见 SLOT_HINT_PATTERNS)。
|
|
380
|
+
const line = stripSlotHint(raw);
|
|
381
|
+
// 工具卡行首带 SGR:ANSI 之后才是缩进空格。
|
|
382
|
+
const head = line.match(/^((?:\x1b\[[0-9;]*m)*)( *)/);
|
|
383
|
+
const ansi = head?.[1] ?? "";
|
|
384
|
+
const spaces = head?.[2]?.length ?? 0;
|
|
385
|
+
const first = lines.length === 0;
|
|
386
|
+
if (first) liveLead = spaces;
|
|
387
|
+
// 首行挂 muted 的 `↳`(与 default-mode 工具结果行同色);续行按 liveLead 去缩进。
|
|
388
|
+
// 两侧都保留行首 SGR:丢掉它整行会掉回终端默认前景色。
|
|
389
|
+
const marker = first ? `${theme.fg("muted", "↳")} ` : " ";
|
|
390
|
+
const lead = first ? spaces : Math.min(spaces, liveLead);
|
|
391
|
+
lines.push(
|
|
392
|
+
`${marker}${ansi}${truncateToWidth(line.slice(ansi.length + lead), innerWidth, "")}`,
|
|
393
|
+
);
|
|
394
|
+
};
|
|
395
|
+
const pushBlank = () => {
|
|
396
|
+
if (live || lastBlank) return;
|
|
397
|
+
lines.push(paddedBackgroundRow(theme, slot, "", width));
|
|
398
|
+
lastBlank = true;
|
|
399
|
+
};
|
|
341
400
|
for (let childIndex = 0; childIndex < children.length; childIndex++) {
|
|
342
401
|
const child = children[childIndex];
|
|
343
402
|
const painted = paints?.[childIndex];
|
|
344
403
|
const childLines = Array.isArray(painted) ? painted : child.render(innerWidth);
|
|
345
|
-
//
|
|
346
|
-
const
|
|
347
|
-
if (!nest) {
|
|
348
|
-
let start = 0;
|
|
349
|
-
if (skipLeadingBlank) {
|
|
350
|
-
while (start < childLines.length && !hasVisibleText(childLines[start])) start++;
|
|
351
|
-
skipLeadingBlank = false;
|
|
352
|
-
}
|
|
353
|
-
const rangeStart = lines.length;
|
|
354
|
-
for (let i = start; i < childLines.length; i++) {
|
|
355
|
-
lines.push(paddedBackgroundRow(theme, slot, childLines[i], width));
|
|
356
|
-
}
|
|
357
|
-
if (lines.length > rangeStart) hits.push({ child, start: rangeStart, end: lines.length });
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
// 工具卡:前导 1 空行 + 首尾文本行之间的内容区(深色);不保留尾随,
|
|
361
|
-
// 相邻工具卡之间正好 1 空行,底部由卡片 padding 收尾。
|
|
362
|
-
skipLeadingBlank = false;
|
|
404
|
+
// live 不留空行、也不叠深色内卡:槽位没有边框。
|
|
405
|
+
const innerCard = !live && isThinkingPreview(child) && child.expanded === true;
|
|
363
406
|
let first = -1;
|
|
364
407
|
let last = -1;
|
|
365
408
|
for (let i = 0; i < childLines.length; i++) {
|
|
@@ -369,28 +412,41 @@ function layoutExpandedToolCard(
|
|
|
369
412
|
}
|
|
370
413
|
}
|
|
371
414
|
if (first < 0) {
|
|
415
|
+
if (live) continue;
|
|
372
416
|
for (const line of childLines) {
|
|
373
417
|
lines.push(paddedBackgroundRow(theme, slot, line, width));
|
|
418
|
+
lastBlank = !hasVisibleText(line);
|
|
374
419
|
}
|
|
375
420
|
continue;
|
|
376
421
|
}
|
|
377
|
-
|
|
422
|
+
if (!innerCard) pushBlank();
|
|
378
423
|
const rangeStart = lines.length;
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
424
|
+
// 展开的 thinking 走深色内卡(上下各 1 行 padding),工具卡直接铺在外卡上。
|
|
425
|
+
const paintRow = innerCard
|
|
426
|
+
? (line: string) => lines.push(toolCardBgRow(theme, slot, toolBgAnsi, line, width))
|
|
427
|
+
: pushRow;
|
|
428
|
+
if (innerCard) paintRow("");
|
|
429
|
+
for (let i = first; i <= last; i++) paintRow(childLines[i]);
|
|
430
|
+
if (innerCard) paintRow("");
|
|
431
|
+
// 内卡的下 padding 不能当分隔:后面的子卡要再留 1 行外卡空行。
|
|
432
|
+
lastBlank = false;
|
|
433
|
+
// thinking 命中自身;工具在展开卡内需要能二次展开时才映射回 tool 组件。
|
|
385
434
|
if (isThinkingPreview(child)) hits.push({ child, start: rangeStart, end: lines.length });
|
|
435
|
+
else if (toolHits && child.__ccTool) {
|
|
436
|
+
hits.push({ child: child.__ccTool, start: rangeStart, end: lines.length });
|
|
437
|
+
}
|
|
386
438
|
}
|
|
387
|
-
|
|
439
|
+
pushBlank();
|
|
388
440
|
return { lines, hits };
|
|
389
441
|
}
|
|
390
442
|
|
|
391
443
|
function compactRoundCard(
|
|
392
444
|
cardItems: Array<{ child?: any; tool?: any }>,
|
|
393
445
|
toolRender: (tool: any, width: number) => string[],
|
|
446
|
+
/** true 时卡内工具行归 tool 组件,可点击二次展开(整回合展开卡)。 */
|
|
447
|
+
toolHits = false,
|
|
448
|
+
/** true 时走 live 槽位排布:不留空行、首行紧跟 `↳`。 */
|
|
449
|
+
live = false,
|
|
394
450
|
): any {
|
|
395
451
|
const children: any[] = [];
|
|
396
452
|
for (const item of cardItems) {
|
|
@@ -399,11 +455,14 @@ function compactRoundCard(
|
|
|
399
455
|
const tool = item.tool;
|
|
400
456
|
children.push({
|
|
401
457
|
__ccToolCard: true,
|
|
458
|
+
__ccTool: tool,
|
|
402
459
|
render: (innerWidth: number) => toolRender(tool, innerWidth),
|
|
403
460
|
invalidate: () => tool.invalidate?.(),
|
|
404
461
|
});
|
|
405
462
|
}
|
|
406
463
|
}
|
|
464
|
+
/** 子块可用宽度:live 还要让出首行 `↳ ` 的标记位。 */
|
|
465
|
+
const contentWidth = (width: number) => Math.max(0, width - 2 - (live ? 2 : 0));
|
|
407
466
|
let paint:
|
|
408
467
|
| {
|
|
409
468
|
width: number;
|
|
@@ -415,7 +474,7 @@ function compactRoundCard(
|
|
|
415
474
|
| undefined;
|
|
416
475
|
const layout = (width: number) => {
|
|
417
476
|
const theme = themeOf();
|
|
418
|
-
const innerWidth =
|
|
477
|
+
const innerWidth = contentWidth(width);
|
|
419
478
|
const paints = children.map((child) => {
|
|
420
479
|
const lines = child.render?.(innerWidth);
|
|
421
480
|
return Array.isArray(lines) ? lines : [];
|
|
@@ -429,12 +488,12 @@ function compactRoundCard(
|
|
|
429
488
|
) {
|
|
430
489
|
return paint;
|
|
431
490
|
}
|
|
432
|
-
const laid = layoutExpandedToolCard(theme, children, width, paints);
|
|
491
|
+
const laid = layoutExpandedToolCard(theme, children, width, paints, toolHits, live);
|
|
433
492
|
paint = {
|
|
434
493
|
width,
|
|
435
494
|
theme,
|
|
436
495
|
paints,
|
|
437
|
-
lines:
|
|
496
|
+
lines: laid.lines,
|
|
438
497
|
hits: laid.hits,
|
|
439
498
|
};
|
|
440
499
|
return paint;
|
|
@@ -445,10 +504,8 @@ function compactRoundCard(
|
|
|
445
504
|
return layout(width).lines;
|
|
446
505
|
},
|
|
447
506
|
childAtRow(localRow: number, width: number) {
|
|
448
|
-
if (localRow < 1) return null;
|
|
449
|
-
const row = localRow - 1;
|
|
450
507
|
for (const hit of layout(width).hits) {
|
|
451
|
-
if (
|
|
508
|
+
if (localRow >= hit.start && localRow < hit.end) return hit.child;
|
|
452
509
|
}
|
|
453
510
|
return null;
|
|
454
511
|
},
|
|
@@ -525,12 +582,60 @@ const trackedAssistantComponents = new Set<any>();
|
|
|
525
582
|
const trackedToolComponents = new Set<any>();
|
|
526
583
|
let hoveredAssistantComponent: any;
|
|
527
584
|
|
|
585
|
+
/**
|
|
586
|
+
* 活动回合摘要 getter:常驻状态行(pi working row)镜像用。
|
|
587
|
+
* installCompactMode 注册、卸载时清空,直接读 activeRound,不持有回合闭包。
|
|
588
|
+
*/
|
|
589
|
+
let compactRunStatusGetter: (() => string | undefined) | undefined;
|
|
590
|
+
|
|
591
|
+
/** 活动回合的摘要文本(`Running... · 9s, bash×1`);无活动回合返回 undefined。 */
|
|
592
|
+
export function getCompactRunStatusText(): string | undefined {
|
|
593
|
+
return compactRunStatusGetter?.();
|
|
594
|
+
}
|
|
595
|
+
|
|
528
596
|
export function setHoveredCompactAssistant(component: any): boolean {
|
|
529
597
|
if (hoveredAssistantComponent === component) return false;
|
|
530
598
|
hoveredAssistantComponent = component;
|
|
531
599
|
return true;
|
|
532
600
|
}
|
|
533
601
|
|
|
602
|
+
/**
|
|
603
|
+
* 整回合展开卡内的工具被点击展开时调用:让 compact 的强制折叠放行这一个,
|
|
604
|
+
* 并把同一回合里其他展开的工具收回去(保持单开)。非 round 内工具是空操作。
|
|
605
|
+
*/
|
|
606
|
+
let roundToolExpansion: { markUserExpanded(tool: any): void } | undefined;
|
|
607
|
+
|
|
608
|
+
export function markCompactRoundToolExpanded(tool: any): void {
|
|
609
|
+
roundToolExpansion?.markUserExpanded(tool);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** 面板展开前的视口位置:收起时还原,避免面板撑高后收起导致视口错位。 */
|
|
613
|
+
const panelViewportBeforeExpand = new WeakMap<
|
|
614
|
+
object,
|
|
615
|
+
{ scrollTop: number; followingEnd: boolean }
|
|
616
|
+
>();
|
|
617
|
+
|
|
618
|
+
function capturePanelViewport(card: any): void {
|
|
619
|
+
if (!card || panelViewportBeforeExpand.has(card)) return;
|
|
620
|
+
const view = getToolMouseTui()?.getPrimaryScrollView?.();
|
|
621
|
+
if (!view || typeof view.scrollTop !== "number" || typeof view.scrollTo !== "function") return;
|
|
622
|
+
panelViewportBeforeExpand.set(card, {
|
|
623
|
+
scrollTop: view.scrollTop,
|
|
624
|
+
followingEnd: view.isFollowingEnd !== false,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function restorePanelViewport(card: any): void {
|
|
629
|
+
const snapshot = panelViewportBeforeExpand.get(card);
|
|
630
|
+
if (!snapshot) return;
|
|
631
|
+
panelViewportBeforeExpand.delete(card);
|
|
632
|
+
const view = getToolMouseTui()?.getPrimaryScrollView?.();
|
|
633
|
+
if (!view) return;
|
|
634
|
+
// 展开前就在底部:交回官方 follow;否则回到展开前的滚动偏移。
|
|
635
|
+
if (snapshot.followingEnd && typeof view.scrollToEnd === "function") view.scrollToEnd();
|
|
636
|
+
else if (typeof view.scrollTo === "function") view.scrollTo(snapshot.scrollTop);
|
|
637
|
+
}
|
|
638
|
+
|
|
534
639
|
function compactEditWriteLine(
|
|
535
640
|
component: any,
|
|
536
641
|
width: number,
|
|
@@ -797,7 +902,9 @@ function compactAssistantLineComponent(
|
|
|
797
902
|
return paint.lines;
|
|
798
903
|
}
|
|
799
904
|
const plainText = truncateToWidth(resolved, summaryWidth, "…");
|
|
800
|
-
|
|
905
|
+
// Dim thinking text 开启时整行统一 dim,否则工具计数保持 muted。
|
|
906
|
+
const plain = (value: string) => theme.fg(config.dimThinkingText ? "dim" : "muted", value);
|
|
907
|
+
let text = plain(plainText);
|
|
801
908
|
if (runningActive || plainText.startsWith("Ran for ")) {
|
|
802
909
|
const separator = plainText.indexOf(", ");
|
|
803
910
|
const heading = separator < 0 ? plainText : plainText.slice(0, separator);
|
|
@@ -806,13 +913,9 @@ function compactAssistantLineComponent(
|
|
|
806
913
|
const durationSeparator = heading.indexOf(" · ");
|
|
807
914
|
const label = durationSeparator < 0 ? heading : heading.slice(0, durationSeparator);
|
|
808
915
|
const duration = durationSeparator < 0 ? "" : heading.slice(durationSeparator);
|
|
809
|
-
text = `${animateCompactThinkingText(
|
|
810
|
-
label,
|
|
811
|
-
theme,
|
|
812
|
-
query?.getThinkingAnimationFrame?.() ?? 0,
|
|
813
|
-
)}${styleCompactThinkingText(duration, theme)}${theme.fg("muted", tools)}`;
|
|
916
|
+
text = `${animateCompactThinkingText(label, theme, query?.getThinkingAnimationFrame?.() ?? 0)}${styleCompactThinkingText(duration, theme)}${plain(tools)}`;
|
|
814
917
|
} else {
|
|
815
|
-
text = `${styleCompactThinkingText(heading, theme)}${
|
|
918
|
+
text = `${styleCompactThinkingText(heading, theme)}${plain(tools)}`;
|
|
816
919
|
}
|
|
817
920
|
}
|
|
818
921
|
const hintColor = hover ? "text" : "dim";
|
|
@@ -850,6 +953,51 @@ function compactStopStatusLine(status: string, pad = 0): any {
|
|
|
850
953
|
};
|
|
851
954
|
}
|
|
852
955
|
|
|
956
|
+
/**
|
|
957
|
+
* 槽位卡内的展开入口:卡里是只读围观,展开走摘要行自己的 hint,卡内不再重复提示。
|
|
958
|
+
* 提示有两处来源——我们的 showMoreHintText(`… +N more lines • click to show more`)与
|
|
959
|
+
* pi 原生工具卡(`… (N earlier lines, ctrl+o to expand)`),所以按渲染结果收尾裁剪。
|
|
960
|
+
* 只去掉动作词,`(N more lines)` 这类计数保留。
|
|
961
|
+
*/
|
|
962
|
+
/** 展开入口的动作词:我们的 `click to show more`、pi 的 `<key> to expand`(键名可能取不到)。 */
|
|
963
|
+
const EXPAND_ACTION = String.raw`(?:click to show more|(?:[\w/+]+\s+)?to (?:show more|expand))`;
|
|
964
|
+
|
|
965
|
+
const SLOT_HINT_PATTERNS: RegExp[] = [
|
|
966
|
+
// 卡在括号里:`(9 more lines, click to show more)` / `(195 earlier lines, ctrl+o to expand)`,
|
|
967
|
+
// 只去掉动作词,收尾括号与计数保留。
|
|
968
|
+
new RegExp(String.raw`,\s*[^()]*?${EXPAND_ACTION}(?=\))`),
|
|
969
|
+
// 直接挂在行尾:整段去掉。
|
|
970
|
+
new RegExp(String.raw`\s*(?:•\s*)?${EXPAND_ACTION}\s*$`),
|
|
971
|
+
];
|
|
972
|
+
|
|
973
|
+
function stripSlotHint(line: string): string {
|
|
974
|
+
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
975
|
+
for (const re of SLOT_HINT_PATTERNS) {
|
|
976
|
+
const match = re.exec(plain);
|
|
977
|
+
if (!match) continue;
|
|
978
|
+
// 删除区间而非截断后拼纯文本:收尾括号留在原样式里(dim),不会掉回默认前景色。
|
|
979
|
+
return removeStyledRange(line, match.index, match.index + match[0].length);
|
|
980
|
+
}
|
|
981
|
+
return line;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** live 槽位卡:按 pad 缩进;首行 `↳` 由卡自身的 live 排布加,行不增不减。 */
|
|
985
|
+
function compactLiveSlot(card: any, pad = 0): any {
|
|
986
|
+
const innerOf = (width: number) => Math.max(0, width - Math.max(0, pad));
|
|
987
|
+
return {
|
|
988
|
+
render(width: number): string[] {
|
|
989
|
+
const indent = " ".repeat(Math.max(0, pad));
|
|
990
|
+
return card.render(innerOf(width)).map((line: string) => `${indent}${line}`);
|
|
991
|
+
},
|
|
992
|
+
childAtRow(localRow: number, width: number) {
|
|
993
|
+
return card.childAtRow?.(localRow, innerOf(width)) ?? null;
|
|
994
|
+
},
|
|
995
|
+
invalidate() {
|
|
996
|
+
card.invalidate();
|
|
997
|
+
},
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
853
1001
|
function compactAssistantLine(
|
|
854
1002
|
component: any,
|
|
855
1003
|
summary: string | (() => string),
|
|
@@ -961,6 +1109,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
961
1109
|
detachedMessages: any[];
|
|
962
1110
|
active: boolean;
|
|
963
1111
|
suppressedToolIds: Set<string>;
|
|
1112
|
+
/** live 槽位内联渲染的工具 id;回合结束时随槽位一起交还外层。 */
|
|
1113
|
+
liveSlotToolIds: Set<string>;
|
|
1114
|
+
/** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
|
|
1115
|
+
settleUntil?: number;
|
|
1116
|
+
foldTimer?: ReturnType<typeof setTimeout>;
|
|
964
1117
|
/** 回合挂钟起点,保证 Running 时长连续递增。 */
|
|
965
1118
|
startedAt: number;
|
|
966
1119
|
endedAt?: number;
|
|
@@ -968,6 +1121,10 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
968
1121
|
let activeRound: CompactRound | undefined;
|
|
969
1122
|
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
970
1123
|
const expandedRoundToolIds = new Set<string>();
|
|
1124
|
+
/** 用户显式点开的 round 内工具;强制折叠不压回这些 id。 */
|
|
1125
|
+
const explicitRoundToolIds = new Set<string>();
|
|
1126
|
+
const liveSlotToolIds = new Set<string>();
|
|
1127
|
+
const pendingFoldRounds = new Set<CompactRound>();
|
|
971
1128
|
let uiRef: { requestRender?: (force?: boolean) => void } | undefined;
|
|
972
1129
|
let roundTickTimer: ReturnType<typeof setInterval> | undefined;
|
|
973
1130
|
|
|
@@ -1007,8 +1164,22 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1007
1164
|
}, 250);
|
|
1008
1165
|
};
|
|
1009
1166
|
|
|
1010
|
-
/**
|
|
1011
|
-
const
|
|
1167
|
+
/** 围观中:活动回合,或收尾静默期还没走完的回合(槽位卡仍挂在屏幕上)。 */
|
|
1168
|
+
const roundLive = (round: CompactRound): boolean =>
|
|
1169
|
+
round.active || (round.settleUntil !== undefined && Date.now() < round.settleUntil);
|
|
1170
|
+
|
|
1171
|
+
const clearFoldTimer = (round: CompactRound): void => {
|
|
1172
|
+
if (round.foldTimer !== undefined) {
|
|
1173
|
+
clearTimeout(round.foldTimer);
|
|
1174
|
+
round.foldTimer = undefined;
|
|
1175
|
+
}
|
|
1176
|
+
round.settleUntil = undefined;
|
|
1177
|
+
pendingFoldRounds.delete(round);
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
/** 结束回合活动态;不做任何重绘。 */
|
|
1181
|
+
const finalizeRound = (round: CompactRound): void => {
|
|
1182
|
+
clearFoldTimer(round);
|
|
1012
1183
|
round.active = false;
|
|
1013
1184
|
if (round.endedAt === undefined) round.endedAt = Date.now();
|
|
1014
1185
|
if (activeRound === round) {
|
|
@@ -1016,7 +1187,36 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1016
1187
|
deps.query?.setCompactSummaryActive?.(false);
|
|
1017
1188
|
stopRoundTick();
|
|
1018
1189
|
}
|
|
1019
|
-
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。
|
|
1194
|
+
* 分组语义不受影响:activeRound 立即让位,新消息不会被并进已收尾的回合。
|
|
1195
|
+
*/
|
|
1196
|
+
const endRound = (round: CompactRound, render = false): void => {
|
|
1197
|
+
if (!render || !round.active) {
|
|
1198
|
+
finalizeRound(round);
|
|
1199
|
+
if (render) renderRound(round);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
finalizeRound(round);
|
|
1203
|
+
if (round.foldTimer !== undefined) return;
|
|
1204
|
+
round.settleUntil = Date.now() + FOLD_SETTLE_MS;
|
|
1205
|
+
pendingFoldRounds.add(round);
|
|
1206
|
+
round.foldTimer = setTimeout(() => {
|
|
1207
|
+
round.foldTimer = undefined;
|
|
1208
|
+
round.settleUntil = undefined;
|
|
1209
|
+
pendingFoldRounds.delete(round);
|
|
1210
|
+
renderRound(round);
|
|
1211
|
+
}, FOLD_SETTLE_MS);
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
/** 新回合接替时立即收掉还在静默期的回合,把两次转场并成一次。 */
|
|
1215
|
+
const flushSettle = (): void => {
|
|
1216
|
+
for (const round of [...pendingFoldRounds]) {
|
|
1217
|
+
clearFoldTimer(round);
|
|
1218
|
+
renderRound(round);
|
|
1219
|
+
}
|
|
1020
1220
|
};
|
|
1021
1221
|
|
|
1022
1222
|
const renderAssistantWithoutThinking = (
|
|
@@ -1037,11 +1237,46 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1037
1237
|
return result;
|
|
1038
1238
|
};
|
|
1039
1239
|
|
|
1240
|
+
/** compact 只允许单开:收回一张工具卡的展开态。 */
|
|
1241
|
+
const collapseTool = (tool: any): void => {
|
|
1242
|
+
if (typeof tool?.setExpanded === "function") tool.setExpanded(false);
|
|
1243
|
+
else {
|
|
1244
|
+
tool.expanded = false;
|
|
1245
|
+
tool.updateDisplay?.();
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
/** 按条件收回已展开的工具卡。 */
|
|
1250
|
+
const collapseTools = (shouldCollapse: (tool: any) => boolean): void => {
|
|
1251
|
+
for (const tool of trackedToolComponents) {
|
|
1252
|
+
if (tool.expanded === true && shouldCollapse(tool)) collapseTool(tool);
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
const markRoundToolUserExpanded = (tool: any): void => {
|
|
1257
|
+
const id = String(tool?.toolCallId ?? "");
|
|
1258
|
+
if (!id || !expandedRoundToolIds.has(id)) return;
|
|
1259
|
+
for (const other of trackedToolComponents) {
|
|
1260
|
+
const otherId = String(other?.toolCallId ?? "");
|
|
1261
|
+
if (other === tool || !expandedRoundToolIds.has(otherId) || other.expanded !== true) continue;
|
|
1262
|
+
explicitRoundToolIds.delete(otherId);
|
|
1263
|
+
collapseTool(other);
|
|
1264
|
+
}
|
|
1265
|
+
explicitRoundToolIds.add(id);
|
|
1266
|
+
};
|
|
1267
|
+
roundToolExpansion = { markUserExpanded: markRoundToolUserExpanded };
|
|
1268
|
+
|
|
1040
1269
|
const roundMessages = (round: CompactRound): any[] => [
|
|
1041
1270
|
...round.messages.values(),
|
|
1042
1271
|
...round.detachedMessages,
|
|
1043
1272
|
];
|
|
1044
1273
|
|
|
1274
|
+
/** 镜像给常驻状态行的文案:只认当前活动回合(摘要行被顶出视口时由 working-message 取用)。 */
|
|
1275
|
+
compactRunStatusGetter = () => {
|
|
1276
|
+
const round = activeRound;
|
|
1277
|
+
return round?.active ? summarize(roundMessages(round), true, round) || undefined : undefined;
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1045
1280
|
const roundToolCallIds = (round: CompactRound): Set<string> => {
|
|
1046
1281
|
const ids = new Set<string>();
|
|
1047
1282
|
for (const message of roundMessages(round)) {
|
|
@@ -1052,26 +1287,146 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1052
1287
|
return ids;
|
|
1053
1288
|
};
|
|
1054
1289
|
|
|
1290
|
+
/**
|
|
1291
|
+
* 末块仍是 thinking = 思考还在长;后面跟了正文或工具调用就算结束。
|
|
1292
|
+
* 围观态只预览这一种,已完成的思考直接回收进摘要行,不在屏幕上留 Thought 行。
|
|
1293
|
+
*/
|
|
1294
|
+
const hasTrailingThinking = (message: any): boolean => {
|
|
1295
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
1296
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
1297
|
+
const item = content[i];
|
|
1298
|
+
if (item?.type === "thinking") return true;
|
|
1299
|
+
if (item?.type === "toolCall") return false;
|
|
1300
|
+
if (item?.type === "text" && String(item.text ?? "").trim()) return false;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
/** 运行中的工具:已开始且尚无最终结果。 */
|
|
1306
|
+
const isRunningTool = (tool: any): boolean =>
|
|
1307
|
+
tool?.executionStarted === true && (!tool.result || tool.isPartial === true);
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* live 围观态:正文 + 摘要行照折叠态渲染,只在摘要行下多一张槽位卡。
|
|
1311
|
+
* - 思考:只预览最后一条仍在思考的消息,它的块是当前最新内容;
|
|
1312
|
+
* - 工具:没有活动思考时才占槽位,运行中优先,否则取本回合最近的,完成也留着;
|
|
1313
|
+
* - 槽位只放一个块(思考或工具),新的思考块/工具调用进来才轮换;
|
|
1314
|
+
* - 本回合所有工具都由槽位卡接管,外层不再单独成行。
|
|
1315
|
+
* 可见性判断只在这里发生;回合结束(renderRound 非 active)只剩槽位卡消失。
|
|
1316
|
+
*/
|
|
1317
|
+
const renderRoundLive = (
|
|
1318
|
+
round: CompactRound,
|
|
1319
|
+
stopStatus: string | undefined,
|
|
1320
|
+
getSummary: () => string,
|
|
1321
|
+
): void => {
|
|
1322
|
+
// 仍在思考的消息 = 当前最新内容,优先占槽位。
|
|
1323
|
+
let previewComponent: any;
|
|
1324
|
+
for (const [component, message] of round.messages) {
|
|
1325
|
+
if (hasTrailingThinking(message)) previewComponent = component;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const thinkingKids: any[] = [];
|
|
1329
|
+
for (const [component, message] of round.messages) {
|
|
1330
|
+
if (component !== previewComponent) {
|
|
1331
|
+
component.contentContainer?.clear?.();
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
passThroughAssistant(component, message);
|
|
1335
|
+
const kids = Array.isArray(component.contentContainer?.children)
|
|
1336
|
+
? [...component.contentContainer.children]
|
|
1337
|
+
: [];
|
|
1338
|
+
component.contentContainer?.clear?.();
|
|
1339
|
+
// 只摘思考预览块进槽位;已收尾的思考回收进摘要行。
|
|
1340
|
+
for (const kid of kids) {
|
|
1341
|
+
if (typeof kid?.setHintHovered === "function") thinkingKids.push(kid);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
// 正文照折叠态渲染(不含 thinking):live 与收尾后同形,收尾只剩槽位卡消失。
|
|
1345
|
+
renderAssistantWithoutThinking(round.anchor, round.messages.get(round.anchor));
|
|
1346
|
+
|
|
1347
|
+
const toolsById = new Map<string, any>();
|
|
1348
|
+
for (const tool of trackedToolComponents) {
|
|
1349
|
+
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
1350
|
+
}
|
|
1351
|
+
let runningTool: any;
|
|
1352
|
+
let lastTool: any;
|
|
1353
|
+
for (const [, message] of round.messages) {
|
|
1354
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1355
|
+
if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
|
|
1356
|
+
const tool = toolsById.get(item.id);
|
|
1357
|
+
// edit/write 保留自己的 compact 行,不进槽位。
|
|
1358
|
+
if (EDIT_WRITE_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
|
|
1359
|
+
round.liveSlotToolIds.add(item.id);
|
|
1360
|
+
liveSlotToolIds.add(item.id);
|
|
1361
|
+
if (!tool) continue;
|
|
1362
|
+
lastTool = tool;
|
|
1363
|
+
if (isRunningTool(tool)) runningTool = tool;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
// 槽位单块:活动思考最新;否则本回合最近的工具(运行中原位增长,完成也留着)。
|
|
1367
|
+
const slotTool = thinkingKids.length > 0 ? undefined : (runningTool ?? lastTool);
|
|
1368
|
+
const cardItems: Array<{ child?: any; tool?: any }> = slotTool
|
|
1369
|
+
? [{ tool: slotTool }]
|
|
1370
|
+
: thinkingKids.map((child) => ({ child }));
|
|
1371
|
+
|
|
1372
|
+
const anchor = round.anchor;
|
|
1373
|
+
const pad = Number(anchor.outputPad) || 0;
|
|
1374
|
+
// 摘要行位置与折叠态一致(正文之后),收尾只有时态变化和槽位卡消失。
|
|
1375
|
+
compactAssistantLine(anchor, getSummary, deps.query);
|
|
1376
|
+
if (cardItems.length > 0) {
|
|
1377
|
+
anchor.contentContainer.addChild(
|
|
1378
|
+
compactLiveSlot(
|
|
1379
|
+
compactRoundCard(
|
|
1380
|
+
cardItems,
|
|
1381
|
+
(tool, innerWidth) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
1382
|
+
false,
|
|
1383
|
+
true,
|
|
1384
|
+
),
|
|
1385
|
+
pad,
|
|
1386
|
+
),
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
appendStopStatus(anchor, stopStatus);
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1055
1392
|
const renderRound = (round: CompactRound): void => {
|
|
1056
1393
|
const messages = roundMessages(round);
|
|
1057
1394
|
const stopStatus = roundStopStatus(messages);
|
|
1058
1395
|
if (stopStatus) endRound(round);
|
|
1059
1396
|
// Running 时每次 render 重算时长(含挂钟下限);结束后固定。
|
|
1397
|
+
// 静默期内回合已收尾,摘要行直接给 Ran for,只有槽位卡多留一拍。
|
|
1060
1398
|
const getSummary = () => summarize(roundMessages(round), round.active, round);
|
|
1061
1399
|
const summary = getSummary();
|
|
1062
1400
|
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
1063
1401
|
round.suppressedToolIds.clear();
|
|
1402
|
+
// 回合收起(或本回合不在展开态)时忘掉用户的单开选择,并把面板接管的工具一起收回:
|
|
1403
|
+
// 少了后一步,被点开的工具会带着自身的 expanded 落到 toolInstalledRender 的放行分支,
|
|
1404
|
+
// 面板一收就单独渲染成一张卡。
|
|
1405
|
+
if (round.anchor.expanded !== true) {
|
|
1406
|
+
const ids = roundToolCallIds(round);
|
|
1407
|
+
for (const id of ids) explicitRoundToolIds.delete(id);
|
|
1408
|
+
collapseTools(
|
|
1409
|
+
(tool) =>
|
|
1410
|
+
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1411
|
+
!EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")),
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
|
|
1415
|
+
round.liveSlotToolIds.clear();
|
|
1064
1416
|
|
|
1065
1417
|
for (const [component, message] of round.messages) {
|
|
1066
1418
|
component.lastMessage = message;
|
|
1067
1419
|
ensureAssistantSetExpanded(component);
|
|
1068
1420
|
component[ASSISTANT_TOGGLE_ROUND_KEY] = (expanded: boolean) => {
|
|
1421
|
+
const wasExpanded = round.anchor.expanded === true;
|
|
1422
|
+
if (expanded && !wasExpanded) capturePanelViewport(round.anchor);
|
|
1069
1423
|
for (const member of round.messages.keys()) member.expanded = expanded;
|
|
1070
1424
|
if (!expanded) {
|
|
1071
1425
|
for (const id of round.suppressedToolIds) expandedRoundToolIds.delete(id);
|
|
1072
1426
|
round.suppressedToolIds.clear();
|
|
1073
1427
|
}
|
|
1074
1428
|
renderRound(round);
|
|
1429
|
+
if (!expanded && wasExpanded) restorePanelViewport(round.anchor);
|
|
1075
1430
|
};
|
|
1076
1431
|
}
|
|
1077
1432
|
|
|
@@ -1080,14 +1435,39 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1080
1435
|
for (const tool of trackedToolComponents) {
|
|
1081
1436
|
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
1082
1437
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1438
|
+
// 助手文本按原生渲染、不进面板(面板只留 thinking 与工具卡)。
|
|
1439
|
+
// thinking/工具卡按连续段各包一张面板;先出文本再出面板,
|
|
1440
|
+
// 否则消息内容里的 thinking([thinking, text, toolCall])会盖到文本上面。
|
|
1441
|
+
const anchorChildren: any[] = [];
|
|
1442
|
+
let cardItems: Array<{ child?: any; tool?: any }> = [];
|
|
1443
|
+
const flushPanel = () => {
|
|
1444
|
+
if (cardItems.length === 0) return;
|
|
1445
|
+
if (cardItems.every((item) => !item.tool && isSpacerComponent(item.child))) {
|
|
1446
|
+
for (const item of cardItems) anchorChildren.push(item.child);
|
|
1447
|
+
} else {
|
|
1448
|
+
// 面板上沿留 1 行卡外空行:面板底色与 userMessage 同色,
|
|
1449
|
+
// 贴在一起会看成一个块;卡内顶部再加 1 行 padding 保持上下对称。
|
|
1450
|
+
anchorChildren.push(new Spacer(1));
|
|
1451
|
+
anchorChildren.push(
|
|
1452
|
+
compactRoundCard(
|
|
1453
|
+
cardItems,
|
|
1454
|
+
(tool, innerWidth) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
1455
|
+
// 整回合展开卡:卡内工具行归 tool 组件,可单击二次展开。
|
|
1456
|
+
true,
|
|
1457
|
+
),
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
cardItems = [];
|
|
1461
|
+
};
|
|
1462
|
+
const takeChild = (child: any, outside: boolean) => {
|
|
1463
|
+
if (!outside) {
|
|
1464
|
+
cardItems.push({ child });
|
|
1465
|
+
return;
|
|
1088
1466
|
}
|
|
1089
|
-
|
|
1467
|
+
flushPanel();
|
|
1468
|
+
anchorChildren.push(child);
|
|
1090
1469
|
};
|
|
1470
|
+
const placedToolIds = new Set<string>();
|
|
1091
1471
|
const placeTool = (id: string, fallbackName?: string) => {
|
|
1092
1472
|
const tool = toolsById.get(id);
|
|
1093
1473
|
if (!tool || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? fallbackName ?? ""))) return;
|
|
@@ -1095,6 +1475,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1095
1475
|
placedToolIds.add(id);
|
|
1096
1476
|
cardItems.push({ tool });
|
|
1097
1477
|
};
|
|
1478
|
+
type LocalEntry = { outside: boolean; child?: any; toolId?: string; toolName?: string };
|
|
1098
1479
|
for (const [component, message] of round.messages) {
|
|
1099
1480
|
passThroughAssistant(component, message);
|
|
1100
1481
|
const kids = Array.isArray(component.contentContainer?.children)
|
|
@@ -1103,25 +1484,39 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1103
1484
|
component.contentContainer?.clear?.();
|
|
1104
1485
|
const cursor = { i: 0 };
|
|
1105
1486
|
let inThinkingRun = false;
|
|
1487
|
+
// 本消息产出先收集再发射:文本(含其前导 Spacer)统一排在面板前面。
|
|
1488
|
+
const local: LocalEntry[] = [];
|
|
1489
|
+
const takeRun = (outside: boolean) => {
|
|
1490
|
+
while (cursor.i < kids.length && isSpacerComponent(kids[cursor.i])) {
|
|
1491
|
+
local.push({ outside, child: kids[cursor.i++] });
|
|
1492
|
+
}
|
|
1493
|
+
if (cursor.i < kids.length) local.push({ outside, child: kids[cursor.i++] });
|
|
1494
|
+
};
|
|
1106
1495
|
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1107
1496
|
if (item?.type === "thinking") {
|
|
1108
1497
|
if (!String(item.thinking ?? "").trim()) continue;
|
|
1109
1498
|
if (!inThinkingRun) {
|
|
1110
|
-
|
|
1499
|
+
takeRun(false);
|
|
1111
1500
|
inThinkingRun = true;
|
|
1112
1501
|
}
|
|
1113
1502
|
continue;
|
|
1114
1503
|
}
|
|
1115
1504
|
inThinkingRun = false;
|
|
1116
1505
|
if (item?.type === "text" && String(item.text ?? "").trim()) {
|
|
1117
|
-
|
|
1506
|
+
takeRun(true);
|
|
1118
1507
|
continue;
|
|
1119
1508
|
}
|
|
1120
1509
|
if (item?.type === "toolCall" && typeof item.id === "string") {
|
|
1121
|
-
|
|
1510
|
+
local.push({ outside: false, toolId: item.id, toolName: item.name });
|
|
1122
1511
|
}
|
|
1123
1512
|
}
|
|
1124
|
-
while (cursor.i < kids.length)
|
|
1513
|
+
while (cursor.i < kids.length) local.push({ outside: false, child: kids[cursor.i++] });
|
|
1514
|
+
for (const entry of local) if (entry.outside) takeChild(entry.child, true);
|
|
1515
|
+
for (const entry of local) {
|
|
1516
|
+
if (entry.outside) continue;
|
|
1517
|
+
if (entry.toolId) placeTool(entry.toolId, entry.toolName);
|
|
1518
|
+
else takeChild(entry.child, false);
|
|
1519
|
+
}
|
|
1125
1520
|
}
|
|
1126
1521
|
for (const message of round.detachedMessages) {
|
|
1127
1522
|
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
@@ -1136,31 +1531,27 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1136
1531
|
expandedRoundToolIds.add(id);
|
|
1137
1532
|
if (!placedToolIds.has(id)) placeTool(id);
|
|
1138
1533
|
}
|
|
1139
|
-
// Round
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
tool.
|
|
1145
|
-
|
|
1146
|
-
continue;
|
|
1147
|
-
}
|
|
1148
|
-
if (typeof tool.setExpanded === "function") tool.setExpanded(false);
|
|
1149
|
-
else {
|
|
1150
|
-
tool.expanded = false;
|
|
1151
|
-
tool.updateDisplay?.();
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
round.anchor.contentContainer.addChild(
|
|
1155
|
-
compactRoundCard(cardItems, (tool, innerWidth) =>
|
|
1156
|
-
patch.toolOriginalRender.call(tool, innerWidth),
|
|
1157
|
-
),
|
|
1534
|
+
// Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕;
|
|
1535
|
+
// 用户单独点开的(explicitRoundToolIds)不压回去。
|
|
1536
|
+
collapseTools(
|
|
1537
|
+
(tool) =>
|
|
1538
|
+
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1539
|
+
!EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")) &&
|
|
1540
|
+
!explicitRoundToolIds.has(String(tool.toolCallId ?? "")),
|
|
1158
1541
|
);
|
|
1542
|
+
flushPanel();
|
|
1543
|
+
for (const child of anchorChildren) round.anchor.contentContainer.addChild(child);
|
|
1159
1544
|
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
1160
1545
|
appendStopStatus(round.anchor, stopStatus);
|
|
1161
1546
|
return;
|
|
1162
1547
|
}
|
|
1163
1548
|
|
|
1549
|
+
if (roundLive(round)) {
|
|
1550
|
+
// 围观态:单一槽位卡(活动思考预览 + 当前工具),回合结束自动收回。
|
|
1551
|
+
renderRoundLive(round, stopStatus, getSummary);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1164
1555
|
for (const [component, message] of round.messages) {
|
|
1165
1556
|
if (component === round.anchor) {
|
|
1166
1557
|
renderAssistantWithoutThinking(component, message);
|
|
@@ -1175,6 +1566,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1175
1566
|
};
|
|
1176
1567
|
|
|
1177
1568
|
const activateRound = (round: CompactRound): void => {
|
|
1569
|
+
flushSettle();
|
|
1178
1570
|
round.active = true;
|
|
1179
1571
|
if (!round.startedAt) round.startedAt = Date.now();
|
|
1180
1572
|
delete round.endedAt;
|
|
@@ -1186,9 +1578,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1186
1578
|
const finishRound = (round: CompactRound): void => endRound(round, true);
|
|
1187
1579
|
|
|
1188
1580
|
const resetRounds = (): void => {
|
|
1581
|
+
for (const round of [...pendingFoldRounds]) clearFoldTimer(round);
|
|
1189
1582
|
activeRound = undefined;
|
|
1190
1583
|
roundByComponent = new WeakMap();
|
|
1191
1584
|
expandedRoundToolIds.clear();
|
|
1585
|
+
explicitRoundToolIds.clear();
|
|
1586
|
+
liveSlotToolIds.clear();
|
|
1192
1587
|
deps.query?.setCompactSummaryActive?.(false);
|
|
1193
1588
|
stopRoundTick();
|
|
1194
1589
|
};
|
|
@@ -1227,6 +1622,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1227
1622
|
detachedMessages: [],
|
|
1228
1623
|
active: true,
|
|
1229
1624
|
suppressedToolIds: new Set(),
|
|
1625
|
+
liveSlotToolIds: new Set(),
|
|
1230
1626
|
startedAt: Date.now(),
|
|
1231
1627
|
};
|
|
1232
1628
|
roundByComponent.set(this, round);
|
|
@@ -1238,6 +1634,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1238
1634
|
detachedMessages: [],
|
|
1239
1635
|
active: true,
|
|
1240
1636
|
suppressedToolIds: new Set(),
|
|
1637
|
+
liveSlotToolIds: new Set(),
|
|
1241
1638
|
startedAt: Date.now(),
|
|
1242
1639
|
};
|
|
1243
1640
|
roundByComponent.set(this, round);
|
|
@@ -1261,7 +1658,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1261
1658
|
return renderAssistantWithoutThinking(this, message);
|
|
1262
1659
|
}
|
|
1263
1660
|
if (round) {
|
|
1264
|
-
|
|
1661
|
+
// 围观态下 anchor 还挂着槽位卡,必须重绘才能收回。
|
|
1662
|
+
endRound(round, true);
|
|
1265
1663
|
roundByComponent.delete(this);
|
|
1266
1664
|
return passThroughAssistant(this, message);
|
|
1267
1665
|
}
|
|
@@ -1279,6 +1677,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1279
1677
|
detachedMessages: [],
|
|
1280
1678
|
active: true,
|
|
1281
1679
|
suppressedToolIds: new Set(),
|
|
1680
|
+
liveSlotToolIds: new Set(),
|
|
1282
1681
|
startedAt: Date.now(),
|
|
1283
1682
|
};
|
|
1284
1683
|
roundByComponent.set(this, round);
|
|
@@ -1314,6 +1713,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1314
1713
|
}
|
|
1315
1714
|
// Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
|
|
1316
1715
|
if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
1716
|
+
// live 围观态:本回合工具交由槽位卡内联渲染,外层不再单独成行,
|
|
1717
|
+
// 卡片因此不会随工具完成反复挂载/卸载。
|
|
1718
|
+
if (liveSlotToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
1317
1719
|
// 普通工具折叠时不显示独立行(摘要行已统计),独立展开走原 renderer。
|
|
1318
1720
|
if (this.expanded === true) {
|
|
1319
1721
|
return patch.toolOriginalRender.call(this, width);
|
|
@@ -1322,12 +1724,11 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1322
1724
|
};
|
|
1323
1725
|
|
|
1324
1726
|
patch.toolInstalledUpdateDisplay = function (this: any) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
this.expanded = false;
|
|
1727
|
+
const id = String(this.toolCallId ?? "");
|
|
1728
|
+
if (patch.active && config.mode === "compact" && expandedRoundToolIds.has(id)) {
|
|
1729
|
+
// 用户点开的保留展开;其余(含全局展开被收回的)继续强制折叠。
|
|
1730
|
+
if (this.expanded !== true) explicitRoundToolIds.delete(id);
|
|
1731
|
+
else if (!explicitRoundToolIds.has(id)) this.expanded = false;
|
|
1331
1732
|
}
|
|
1332
1733
|
const result = patch.toolOriginalUpdateDisplay.call(this);
|
|
1333
1734
|
if (!patch.active) return result;
|
|
@@ -1355,6 +1756,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1355
1756
|
patch.dispose = () => {
|
|
1356
1757
|
if (!patch.active) return;
|
|
1357
1758
|
patch.active = false;
|
|
1759
|
+
if (roundToolExpansion?.markUserExpanded === markRoundToolUserExpanded) {
|
|
1760
|
+
roundToolExpansion = undefined;
|
|
1761
|
+
}
|
|
1358
1762
|
if (assistantPrototype.updateContent === patch.assistantInstalled) {
|
|
1359
1763
|
assistantPrototype.updateContent = patch.assistantOriginal;
|
|
1360
1764
|
}
|
|
@@ -1370,6 +1774,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1370
1774
|
trackedToolComponents.clear();
|
|
1371
1775
|
hoveredAssistantComponent = undefined;
|
|
1372
1776
|
resetRounds();
|
|
1777
|
+
compactRunStatusGetter = undefined;
|
|
1373
1778
|
};
|
|
1374
1779
|
|
|
1375
1780
|
assistantPrototype.updateContent = patch.assistantInstalled;
|