@herbertgao/pi-extensions 2026.10.4 → 2026.10.6
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.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +72 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +262 -48
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-currency.ts +29 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +96 -59
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/git-stats.ts +162 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +204 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +51 -86
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +212 -74
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +50 -362
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +35 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +28 -26
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +450 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +173 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +5 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +36 -10
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +140 -51
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +44 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -803
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +9 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* renderer)。edit/write 复用 mode=on 的 rich diff
|
|
7
7
|
* 与 `editDiffCollapsedLines` / `writeDiffCollapsedLines` / `expandedPreviewMaxLines`。
|
|
8
8
|
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
9
|
+
* edit/write 与 ask_user_question 留在外层:不进摘要计数,折叠态也单独成行。
|
|
10
|
+
* 用户消息是回合的硬边界:steer 消息插进回合中间、或上一回合没正文就收尾时,回合
|
|
11
|
+
* 就地收口、后续消息另起一回合,区间里的用户消息不会被当成外来行挪走(摘要行、
|
|
12
|
+
* 展开面板因此留在自己回合的位置上)。
|
|
9
13
|
* 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
|
|
10
14
|
*
|
|
11
15
|
* live 围观态(回合进行中 / 收尾静默期):正文跟折叠态同形;摘要行与单一槽位卡
|
|
@@ -26,27 +30,34 @@
|
|
|
26
30
|
*/
|
|
27
31
|
import {
|
|
28
32
|
AssistantMessageComponent,
|
|
33
|
+
SkillInvocationMessageComponent,
|
|
29
34
|
ToolExecutionComponent,
|
|
35
|
+
UserMessageComponent,
|
|
30
36
|
type Theme,
|
|
31
37
|
} from "@earendil-works/pi-coding-agent";
|
|
32
38
|
import { truncateToWidth, visibleWidth, Box, Spacer } from "@earendil-works/pi-tui";
|
|
33
39
|
import { config, getToolDisplayConfig } from "../config/config.ts";
|
|
40
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "./tool/ask-user-question.ts";
|
|
34
41
|
import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
|
|
35
42
|
import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
|
|
36
43
|
import { refreshTranscriptComponent } from "./transcript-refresh.ts";
|
|
37
44
|
import { getMessageDisplayTheme } from "./tool/message-display.ts";
|
|
38
|
-
import { showMoreHintText } from "./tool/
|
|
45
|
+
import { showMoreHintText } from "./tool/names.ts";
|
|
39
46
|
import {
|
|
40
47
|
countEditDiffStats,
|
|
41
48
|
countWriteDiffStats,
|
|
42
49
|
isRichDiffComponent,
|
|
43
50
|
} from "./tool/diff/diff-renderer.ts";
|
|
44
51
|
import { renderRichToolResult } from "./tool/diff/index.ts";
|
|
52
|
+
import { displayConfigCacheKey } from "./tool/diff/diff-component.ts";
|
|
45
53
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
46
54
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
47
55
|
import { getToolMouseTui } from "./mouse/scroll.ts";
|
|
48
|
-
import { insetComponent
|
|
56
|
+
import { insetComponent } from "./tool/result.ts";
|
|
57
|
+
import { isLiveToolCall, scheduleAnimation } from "./tool/tool-state.ts";
|
|
58
|
+
import { renderExpandedToolResult } from "./tool/tool-io.ts";
|
|
49
59
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
60
|
+
import { styleChangeCounts } from "./tool/ccstyle-tool.ts";
|
|
50
61
|
import { formatDisplayPath } from "./tool/names.ts";
|
|
51
62
|
import {
|
|
52
63
|
hasVisibleText,
|
|
@@ -81,6 +92,13 @@ export type CompactThinkingQuery = {
|
|
|
81
92
|
|
|
82
93
|
const EDIT_WRITE_TOOLS = new Set(["edit", "write"]);
|
|
83
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 留在外层的工具:不进摘要计数,折叠态也单独成行。
|
|
97
|
+
* edit/write 走 compact 自己的 rich diff;ask_user_question 走 default 模式的专用渲染
|
|
98
|
+
*(标题 (+N) 加折叠问答树)。
|
|
99
|
+
*/
|
|
100
|
+
const OUTER_TOOLS = new Set([...EDIT_WRITE_TOOLS, ASK_USER_QUESTION_TOOL_NAME]);
|
|
101
|
+
|
|
84
102
|
/**
|
|
85
103
|
* 回合收尾后延迟收拢的静默期(ms)。正文 token 往往先于 toolCall 到达,
|
|
86
104
|
* 立刻收拢会得到「先收再开」两次转场;静默期内被新回合接替则一并收拢。
|
|
@@ -205,7 +223,7 @@ function buildMessagesSummary(
|
|
|
205
223
|
for (const item of content) {
|
|
206
224
|
if (item?.type !== "toolCall") continue;
|
|
207
225
|
const rawName = typeof item.name === "string" ? item.name : "tool";
|
|
208
|
-
if (
|
|
226
|
+
if (OUTER_TOOLS.has(rawName)) continue;
|
|
209
227
|
const name = sanitizeToolResultText(rawName);
|
|
210
228
|
if (rawName.split(".").pop() === "read") {
|
|
211
229
|
const args = item.arguments ?? item.args ?? {};
|
|
@@ -574,6 +592,11 @@ export type CompactModeHooks = {
|
|
|
574
592
|
assertOwnership(): void;
|
|
575
593
|
/** 重新渲染包含指定 toolCallId 的 assistant 消息(思考收尾时刷新)。 */
|
|
576
594
|
refreshToolCallMessage(toolCallId: string | undefined): void;
|
|
595
|
+
/**
|
|
596
|
+
* 收尾 resume 重放留下的活动回合。重放是同步的,回合之间靠下一条消息收口,
|
|
597
|
+
* 只有最后一条没有后续消息,会停在 Running。
|
|
598
|
+
*/
|
|
599
|
+
settleReplayedRound(): void;
|
|
577
600
|
shutdown(): void;
|
|
578
601
|
};
|
|
579
602
|
|
|
@@ -600,17 +623,6 @@ const trackedAssistantComponents = new Set<any>();
|
|
|
600
623
|
const trackedToolComponents = new Set<any>();
|
|
601
624
|
let hoveredAssistantComponent: any;
|
|
602
625
|
|
|
603
|
-
/**
|
|
604
|
-
* 活动回合摘要 getter:常驻状态行(pi working row)镜像用。
|
|
605
|
-
* installCompactMode 注册、卸载时清空,直接读 activeRound,不持有回合闭包。
|
|
606
|
-
*/
|
|
607
|
-
let compactRunStatusGetter: (() => string | undefined) | undefined;
|
|
608
|
-
|
|
609
|
-
/** 活动回合的摘要文本(`Running... · 9s, bash×1`);无活动回合返回 undefined。 */
|
|
610
|
-
export function getCompactRunStatusText(): string | undefined {
|
|
611
|
-
return compactRunStatusGetter?.();
|
|
612
|
-
}
|
|
613
|
-
|
|
614
626
|
/**
|
|
615
627
|
* 回合尾行记录:摘要行(live 时连槽位卡)挂成 transcript 容器里回合末尾的
|
|
616
628
|
* 兄弟组件,而不是 anchor 内部。anchor 内部的可变内容一旦被工具/diff 顶出视口
|
|
@@ -643,38 +655,44 @@ export function markCompactRoundToolExpanded(tool: any): void {
|
|
|
643
655
|
roundToolExpansion?.markUserExpanded(tool);
|
|
644
656
|
}
|
|
645
657
|
|
|
646
|
-
/**
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
658
|
+
/**
|
|
659
|
+
* 展开面板前的分隔空行与面板绑成一体:挂在同一尾行上,才不会被拆到
|
|
660
|
+
* anchor 正文之后(面板挂回合尾行,空行却留在消息里)。
|
|
661
|
+
* 带 childAtRow 返回 null:拆分判据认它为面板成员,行命中落空时归外层面板。
|
|
662
|
+
*/
|
|
663
|
+
function panelBoundSpacer(): any {
|
|
664
|
+
return {
|
|
665
|
+
render: () => [""],
|
|
666
|
+
childAtRow: () => null,
|
|
667
|
+
invalidate() {},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** 面板展开前的视口标记:收起时需要重钉视口的卡。 */
|
|
672
|
+
const panelViewportBeforeExpand = new WeakSet<object>();
|
|
651
673
|
|
|
652
674
|
function capturePanelViewport(card: any): void {
|
|
653
|
-
if (!card
|
|
654
|
-
|
|
655
|
-
if (!view || typeof view.scrollTop !== "number" || typeof view.scrollTo !== "function") return;
|
|
656
|
-
panelViewportBeforeExpand.set(card, {
|
|
657
|
-
scrollTop: view.scrollTop,
|
|
658
|
-
followingEnd: view.isFollowingEnd !== false,
|
|
659
|
-
});
|
|
675
|
+
if (!card) return;
|
|
676
|
+
panelViewportBeforeExpand.add(card);
|
|
660
677
|
}
|
|
661
678
|
|
|
662
679
|
function restorePanelViewport(card: any): void {
|
|
663
|
-
|
|
664
|
-
if (!snapshot) return;
|
|
665
|
-
panelViewportBeforeExpand.delete(card);
|
|
680
|
+
if (!panelViewportBeforeExpand.delete(card)) return;
|
|
666
681
|
const view = getToolMouseTui()?.getPrimaryScrollView?.();
|
|
667
682
|
if (!view) return;
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
683
|
+
// 不用展开前的快照:展开期间用户可能滚进面板里,回跳旧位置才是"跳"。
|
|
684
|
+
// 面板在摘要行之下,收起只删它以下的行,保持当前 scrollTop 摘要行原地不动;
|
|
685
|
+
// clamp 后落在新底时 scrollTo 会自动恢复 followingEnd(并清 followSuppressedAtEnd)。
|
|
686
|
+
if (typeof view.scrollTo === "function" && typeof view.scrollTop === "number") {
|
|
687
|
+
view.scrollTo(view.scrollTop);
|
|
688
|
+
}
|
|
671
689
|
}
|
|
672
690
|
|
|
673
691
|
function compactEditWriteLine(
|
|
674
692
|
component: any,
|
|
675
693
|
width: number,
|
|
676
694
|
writeMetadata?: WriteExecutionMetadataStore,
|
|
677
|
-
options: { hint?: boolean; flushLeft?: boolean } = {},
|
|
695
|
+
options: { hint?: boolean; flushLeft?: boolean; showStats?: boolean } = {},
|
|
678
696
|
): string[] {
|
|
679
697
|
const theme = themeOf();
|
|
680
698
|
const name = String(component.toolName ?? "tool");
|
|
@@ -691,7 +709,8 @@ function compactEditWriteLine(
|
|
|
691
709
|
const iconColor = isError ? "error" : isPending ? "accent" : "success";
|
|
692
710
|
let statsText = "";
|
|
693
711
|
let statsStyled = "";
|
|
694
|
-
|
|
712
|
+
// 统计只显示一处:正文自带 +A -D 时标题不再重复(showStats=false)。
|
|
713
|
+
if (!isError && !isPending && options.showStats !== false) {
|
|
695
714
|
const stats =
|
|
696
715
|
name === "edit"
|
|
697
716
|
? countEditDiffStats(component.result?.details)
|
|
@@ -703,8 +722,13 @@ function compactEditWriteLine(
|
|
|
703
722
|
)
|
|
704
723
|
: undefined;
|
|
705
724
|
if (stats) {
|
|
706
|
-
|
|
707
|
-
|
|
725
|
+
const counts = styleChangeCounts(
|
|
726
|
+
(color, text) => theme.fg(color, text),
|
|
727
|
+
stats.added,
|
|
728
|
+
stats.removed,
|
|
729
|
+
);
|
|
730
|
+
statsText = counts.text;
|
|
731
|
+
statsStyled = counts.styled;
|
|
708
732
|
}
|
|
709
733
|
}
|
|
710
734
|
// 展开卡 Box(1,1) 已 pad;折叠行自己留 1 格前导空格
|
|
@@ -729,6 +753,12 @@ function compactEditWriteLine(
|
|
|
729
753
|
return ["", truncateToWidth(line, width, "")];
|
|
730
754
|
}
|
|
731
755
|
|
|
756
|
+
/** 正文自带 +A -D 的声明:diff 组件自报,折叠行数/宽度这些细节由它自己判断。 */
|
|
757
|
+
function richDiffBodyShowsStats(component: unknown): boolean {
|
|
758
|
+
const probe = (component as { bodyShowsStats?: () => boolean } | undefined)?.bodyShowsStats;
|
|
759
|
+
return typeof probe === "function" && probe.call(component) === true;
|
|
760
|
+
}
|
|
761
|
+
|
|
732
762
|
/** expanded 写进 rich diff 闭包,折叠/展开分槽缓存,避免每帧 parseDiff。 */
|
|
733
763
|
const compactRichDiffCache = new WeakMap<
|
|
734
764
|
object,
|
|
@@ -740,6 +770,7 @@ type CompactEditPaintHit = {
|
|
|
740
770
|
theme: unknown;
|
|
741
771
|
/** 展开卡的背景槽位同样参与命中判断。 */
|
|
742
772
|
bgSlot: string;
|
|
773
|
+
diffConfigKey: string;
|
|
743
774
|
expanded: boolean;
|
|
744
775
|
result: unknown;
|
|
745
776
|
isPartial: boolean;
|
|
@@ -771,6 +802,7 @@ function compactEditWriteLines(
|
|
|
771
802
|
const hover = isToolCallHovered(component.toolCallId);
|
|
772
803
|
const ioHover = compactEditIoHover(component);
|
|
773
804
|
const theme = themeOf();
|
|
805
|
+
const diffConfigKey = displayConfigCacheKey(getToolDisplayConfig());
|
|
774
806
|
if (!pending) {
|
|
775
807
|
const hit = compactEditPaintCache.get(component);
|
|
776
808
|
if (
|
|
@@ -778,6 +810,7 @@ function compactEditWriteLines(
|
|
|
778
810
|
hit.width === width &&
|
|
779
811
|
hit.theme === theme &&
|
|
780
812
|
hit.bgSlot === config.expandedCardBackground &&
|
|
813
|
+
hit.diffConfigKey === diffConfigKey &&
|
|
781
814
|
hit.expanded === expanded &&
|
|
782
815
|
hit.result === component.result &&
|
|
783
816
|
hit.isPartial === isPartial &&
|
|
@@ -794,6 +827,7 @@ function compactEditWriteLines(
|
|
|
794
827
|
width,
|
|
795
828
|
theme,
|
|
796
829
|
bgSlot: config.expandedCardBackground,
|
|
830
|
+
diffConfigKey,
|
|
797
831
|
expanded,
|
|
798
832
|
result: component.result,
|
|
799
833
|
isPartial,
|
|
@@ -848,7 +882,12 @@ function paintCompactEditWrite(
|
|
|
848
882
|
component.resultRendererComponent = candidate;
|
|
849
883
|
}
|
|
850
884
|
|
|
851
|
-
|
|
885
|
+
// 统计只显示一处:正文里已经有 +A -D(diff 头行/0 行摘要行)时标题不再重复。
|
|
886
|
+
const showStats = !(hasRich && richDiffBodyShowsStats(candidate));
|
|
887
|
+
const title = compactEditWriteLine(component, width, writeMetadata, {
|
|
888
|
+
hint: !hasRich,
|
|
889
|
+
showStats,
|
|
890
|
+
});
|
|
852
891
|
if (!hasRich && !expanded) {
|
|
853
892
|
return title;
|
|
854
893
|
}
|
|
@@ -888,6 +927,7 @@ function paintCompactEditWrite(
|
|
|
888
927
|
return compactEditWriteLine(component, innerWidth, writeMetadata, {
|
|
889
928
|
hint: false,
|
|
890
929
|
flushLeft: true,
|
|
930
|
+
showStats,
|
|
891
931
|
}).slice(1);
|
|
892
932
|
},
|
|
893
933
|
invalidate() {},
|
|
@@ -1001,7 +1041,7 @@ function compactStopStatusLine(status: string, pad = 0): any {
|
|
|
1001
1041
|
|
|
1002
1042
|
/**
|
|
1003
1043
|
* 槽位卡内的展开入口:卡里是只读围观,展开走摘要行自己的 hint,卡内不再重复提示。
|
|
1004
|
-
* 提示有两处来源——我们的 showMoreHintText(`…
|
|
1044
|
+
* 提示有两处来源——我们的 showMoreHintText(`… N more lines · click to show more`)与
|
|
1005
1045
|
* pi 原生工具卡(`… (N earlier lines, ctrl+o to expand)`),所以按渲染结果收尾裁剪。
|
|
1006
1046
|
* 只去掉动作词,`(N more lines)` 这类计数保留。
|
|
1007
1047
|
*/
|
|
@@ -1150,6 +1190,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1150
1190
|
liveSlotToolIds: Set<string>;
|
|
1151
1191
|
/** 收尾后的静默期截止时间戳;期间仍按围观态渲染,到点才收拢。 */
|
|
1152
1192
|
settleUntil?: number;
|
|
1193
|
+
/** 消息已有 toolCall、工具组件还没到:等它一 updateDisplay 就重画槽位。 */
|
|
1194
|
+
pendingLiveTool?: boolean;
|
|
1153
1195
|
foldTimer?: ReturnType<typeof setTimeout>;
|
|
1154
1196
|
/** 回合挂钟起点,保证 Running 时长连续递增。 */
|
|
1155
1197
|
startedAt: number;
|
|
@@ -1311,12 +1353,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1311
1353
|
...round.detachedMessages,
|
|
1312
1354
|
];
|
|
1313
1355
|
|
|
1314
|
-
/** 镜像给常驻状态行的文案:只认当前活动回合(摘要行被顶出视口时由 working-message 取用)。 */
|
|
1315
|
-
compactRunStatusGetter = () => {
|
|
1316
|
-
const round = activeRound;
|
|
1317
|
-
return round?.active ? summarize(roundMessages(round), true, round) || undefined : undefined;
|
|
1318
|
-
};
|
|
1319
|
-
|
|
1320
1356
|
const roundToolCallIds = (round: CompactRound): Set<string> => {
|
|
1321
1357
|
const ids = new Set<string>();
|
|
1322
1358
|
for (const message of roundMessages(round)) {
|
|
@@ -1327,6 +1363,26 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1327
1363
|
return ids;
|
|
1328
1364
|
};
|
|
1329
1365
|
|
|
1366
|
+
/**
|
|
1367
|
+
* 摘要行有东西可汇总才挂:普通工具(edit/write/ask 之外)或非空思考。
|
|
1368
|
+
* 只剩外层卡的回合不挂——那些卡各自能展开,空摘要点开只会跳位。
|
|
1369
|
+
*/
|
|
1370
|
+
const roundHasSummaryContent = (round: CompactRound): boolean => {
|
|
1371
|
+
for (const message of roundMessages(round)) {
|
|
1372
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1373
|
+
if (item?.type === "thinking" && String(item.thinking ?? "").trim()) return true;
|
|
1374
|
+
if (
|
|
1375
|
+
item?.type === "toolCall" &&
|
|
1376
|
+
typeof item.name === "string" &&
|
|
1377
|
+
!OUTER_TOOLS.has(item.name)
|
|
1378
|
+
) {
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return false;
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1330
1386
|
/**
|
|
1331
1387
|
* anchor 所在的 transcript 容器(pi chatContainer)。
|
|
1332
1388
|
* 优先走分组补丁在 addChild 时挂的父指针;没有则沿 tui 组件树找直属父容器。
|
|
@@ -1362,6 +1418,54 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1362
1418
|
return found;
|
|
1363
1419
|
};
|
|
1364
1420
|
|
|
1421
|
+
/** 回合成员判定:本回合的 assistant 组件,或归属本回合的工具(含分组容器)。 */
|
|
1422
|
+
const roundMemberMatcher = (round: CompactRound): ((value: any) => boolean) => {
|
|
1423
|
+
const ids = roundToolCallIds(round);
|
|
1424
|
+
const matchesTool = (value: any): boolean =>
|
|
1425
|
+
typeof value?.toolCallId === "string" && ids.has(value.toolCallId);
|
|
1426
|
+
return (value: any): boolean =>
|
|
1427
|
+
round.messages.has(value) ||
|
|
1428
|
+
matchesTool(value) ||
|
|
1429
|
+
(Array.isArray(value?.children) && value.children.some(matchesTool));
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
/** 用户消息(含 skill 调用块)是回合的硬边界。 */
|
|
1433
|
+
const isUserMessageComponent = (value: any): boolean =>
|
|
1434
|
+
value instanceof UserMessageComponent || value instanceof SkillInvocationMessageComponent;
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* 新组件与回合已有成员之间是否隔着一条用户消息。隔着就说明这是下一回合的内容:
|
|
1438
|
+
* steer 消息插进了回合中间,或上一回合没有正文就收尾、用户又问了新问题。
|
|
1439
|
+
* 组件还没挂进容器时判断不了,按不隔开处理(会话重放的 refresh 会再求值一次)。
|
|
1440
|
+
*/
|
|
1441
|
+
const crossesUserMessage = (round: CompactRound, component: any): boolean => {
|
|
1442
|
+
const container = tailContainerOf(round.anchor);
|
|
1443
|
+
const children = container?.children;
|
|
1444
|
+
if (!Array.isArray(children)) return false;
|
|
1445
|
+
const index = children.indexOf(component);
|
|
1446
|
+
if (index < 0) return false;
|
|
1447
|
+
const isMember = roundMemberMatcher(round);
|
|
1448
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
1449
|
+
if (isMember(children[i])) return false;
|
|
1450
|
+
if (isUserMessageComponent(children[i])) return true;
|
|
1451
|
+
}
|
|
1452
|
+
return false;
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* 可以并入的活动回合。被用户消息隔开时返回 undefined,调用方收掉旧回合另起一个,
|
|
1457
|
+
* 保证回合不跨用户消息——否则区间里的用户消息会被当成外来行挪到回合末尾之后。
|
|
1458
|
+
*/
|
|
1459
|
+
const joinableActiveRound = (component: any): CompactRound | undefined =>
|
|
1460
|
+
activeRound && !crossesUserMessage(activeRound, component) ? activeRound : undefined;
|
|
1461
|
+
|
|
1462
|
+
/** 取并入目标;旧回合被用户消息隔开时先收尾(静默期一转场,新回合立即接手)。 */
|
|
1463
|
+
const joinOrCreateRound = (component: any, message: any): CompactRound => {
|
|
1464
|
+
const joinable = joinableActiveRound(component);
|
|
1465
|
+
if (!joinable && activeRound) finishRound(activeRound);
|
|
1466
|
+
return joinable ?? createRound(component, message);
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1365
1469
|
/** 尾行实际所在容器;缓存的 container 过期时按树找回来。 */
|
|
1366
1470
|
const tailHostContainer = (tail: MountedRoundTail): any =>
|
|
1367
1471
|
containerOfHost(tail.host) ??
|
|
@@ -1444,13 +1548,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1444
1548
|
const children = container.children as any[];
|
|
1445
1549
|
const current = children.indexOf(tail.host);
|
|
1446
1550
|
if (current >= 0) children.splice(current, 1);
|
|
1447
|
-
const
|
|
1448
|
-
const matchesTool = (value: any): boolean =>
|
|
1449
|
-
typeof value?.toolCallId === "string" && ids.has(value.toolCallId);
|
|
1450
|
-
const isMember = (value: any): boolean =>
|
|
1451
|
-
round.messages.has(value) ||
|
|
1452
|
-
matchesTool(value) ||
|
|
1453
|
-
(Array.isArray(value?.children) && value.children.some(matchesTool));
|
|
1551
|
+
const isMember = roundMemberMatcher(round);
|
|
1454
1552
|
let first = -1;
|
|
1455
1553
|
let last = -1;
|
|
1456
1554
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -1461,7 +1559,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1461
1559
|
const lastMember = last >= 0 ? children[last] : undefined;
|
|
1462
1560
|
const intruders: any[] = [];
|
|
1463
1561
|
for (let i = last - 1; i > first; i--) {
|
|
1464
|
-
|
|
1562
|
+
// 用户消息永远留在原地:挪走会把别人的消息挪到用户消息下面去。
|
|
1563
|
+
if (isMember(children[i]) || isUserMessageComponent(children[i])) continue;
|
|
1564
|
+
intruders.unshift(children.splice(i, 1)[0]);
|
|
1465
1565
|
}
|
|
1466
1566
|
if (lastMember) last = children.indexOf(lastMember);
|
|
1467
1567
|
const insertAt = last < 0 ? children.length : last + 1;
|
|
@@ -1491,10 +1591,12 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1491
1591
|
const syncTailForTool = (toolCallId: string): void => {
|
|
1492
1592
|
if (!toolCallId) return;
|
|
1493
1593
|
for (const round of [activeRound, ...pendingFoldRounds]) {
|
|
1494
|
-
if (round
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1594
|
+
if (!round || !roundToolCallIds(round).has(toolCallId)) continue;
|
|
1595
|
+
// 槽位是在消息更新时画的,当时工具组件还不存在:组件到了就补上。
|
|
1596
|
+
// 参数流式更新也走这里,槽位卡跟着换内容。
|
|
1597
|
+
if (round.pendingLiveTool || roundLive(round)) renderRound(round);
|
|
1598
|
+
else if (round.tail) syncTailPosition(round);
|
|
1599
|
+
return;
|
|
1498
1600
|
}
|
|
1499
1601
|
};
|
|
1500
1602
|
|
|
@@ -1638,19 +1740,26 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1638
1740
|
}
|
|
1639
1741
|
let runningTool: any;
|
|
1640
1742
|
let lastTool: any;
|
|
1743
|
+
let missingTool = false;
|
|
1641
1744
|
for (const [, message] of round.messages) {
|
|
1642
1745
|
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
1643
1746
|
if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
|
|
1644
1747
|
const tool = toolsById.get(item.id);
|
|
1645
1748
|
// edit/write 保留自己的 compact 行,不进槽位。
|
|
1646
|
-
if (
|
|
1749
|
+
if (OUTER_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
|
|
1647
1750
|
round.liveSlotToolIds.add(item.id);
|
|
1648
1751
|
liveSlotToolIds.add(item.id);
|
|
1649
|
-
|
|
1752
|
+
// pi 的 message_update 先 updateContent 再建工具组件:这一帧计数已经算上,
|
|
1753
|
+
// 组件要等 updateDisplay 才能进槽位。记下来,组件一到就补画。
|
|
1754
|
+
if (!tool) {
|
|
1755
|
+
missingTool = true;
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1650
1758
|
lastTool = tool;
|
|
1651
1759
|
if (isRunningTool(tool)) runningTool = tool;
|
|
1652
1760
|
}
|
|
1653
1761
|
}
|
|
1762
|
+
round.pendingLiveTool = missingTool;
|
|
1654
1763
|
// 槽位单块:活动思考最新;否则本回合最近的工具(运行中原位增长,完成也留着)。
|
|
1655
1764
|
const slotTool = thinkingKids.length > 0 ? undefined : (runningTool ?? lastTool);
|
|
1656
1765
|
const cardItems: Array<{ child?: any; tool?: any }> = slotTool
|
|
@@ -1700,12 +1809,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1700
1809
|
for (const id of ids) explicitRoundToolIds.delete(id);
|
|
1701
1810
|
collapseTools(
|
|
1702
1811
|
(tool) =>
|
|
1703
|
-
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1704
|
-
!EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")),
|
|
1812
|
+
ids.has(String(tool.toolCallId ?? "")) && !OUTER_TOOLS.has(String(tool.toolName ?? "")),
|
|
1705
1813
|
);
|
|
1706
1814
|
}
|
|
1707
1815
|
for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
|
|
1708
1816
|
round.liveSlotToolIds.clear();
|
|
1817
|
+
// pendingLiveTool 留给 renderRoundLive 重算:这里清掉的话,组件刚到的那次
|
|
1818
|
+
// 补画会把自己的触发条件抹掉,槽位仍是空的。
|
|
1709
1819
|
|
|
1710
1820
|
for (const [component, message] of round.messages) {
|
|
1711
1821
|
component.lastMessage = message;
|
|
@@ -1727,6 +1837,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1727
1837
|
};
|
|
1728
1838
|
}
|
|
1729
1839
|
|
|
1840
|
+
if (round.anchor.expanded === true) {
|
|
1841
|
+
// 没有可汇总内容的回合不进展开态:摘要行不存在,展开只会把正文搬回回合开头。
|
|
1842
|
+
if (!roundHasSummaryContent(round)) {
|
|
1843
|
+
for (const member of round.messages.keys()) member.expanded = false;
|
|
1844
|
+
round.anchor.expanded = false;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1730
1847
|
if (round.anchor.expanded === true) {
|
|
1731
1848
|
// 展开内容留在摘要行那个位置:回合最后一个成员(含 write/edit)之后。
|
|
1732
1849
|
// 放进 anchor 内部会跳到回合起点,盖到中间的 write/edit 上面。
|
|
@@ -1748,7 +1865,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1748
1865
|
} else {
|
|
1749
1866
|
// 面板上沿留 1 行卡外空行:面板底色与 userMessage 同色,
|
|
1750
1867
|
// 贴在一起会看成一个块;卡内顶部再加 1 行 padding 保持上下对称。
|
|
1751
|
-
|
|
1868
|
+
// 空行必须与面板同进退(childAtRow 参与面板拆分),用 panelBoundSpacer。
|
|
1869
|
+
anchorChildren.push(panelBoundSpacer());
|
|
1752
1870
|
anchorChildren.push(
|
|
1753
1871
|
compactRoundCard(
|
|
1754
1872
|
cardItems,
|
|
@@ -1771,7 +1889,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1771
1889
|
const placedToolIds = new Set<string>();
|
|
1772
1890
|
const placeTool = (id: string, fallbackName?: string) => {
|
|
1773
1891
|
const tool = toolsById.get(id);
|
|
1774
|
-
if (!tool ||
|
|
1892
|
+
if (!tool || OUTER_TOOLS.has(String(tool.toolName ?? fallbackName ?? ""))) return;
|
|
1775
1893
|
if (placedToolIds.has(id)) return;
|
|
1776
1894
|
placedToolIds.add(id);
|
|
1777
1895
|
cardItems.push({ tool });
|
|
@@ -1837,7 +1955,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1837
1955
|
collapseTools(
|
|
1838
1956
|
(tool) =>
|
|
1839
1957
|
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1840
|
-
!
|
|
1958
|
+
!OUTER_TOOLS.has(String(tool.toolName ?? "")) &&
|
|
1841
1959
|
!explicitRoundToolIds.has(String(tool.toolCallId ?? "")),
|
|
1842
1960
|
);
|
|
1843
1961
|
flushPanel();
|
|
@@ -1869,8 +1987,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1869
1987
|
for (const [component, message] of round.messages) {
|
|
1870
1988
|
if (component === round.anchor) {
|
|
1871
1989
|
renderAssistantWithoutThinking(component, message);
|
|
1872
|
-
// 空摘要不挂行(getter 在 Running
|
|
1873
|
-
|
|
1990
|
+
// 空摘要不挂行(getter 在 Running 启动瞬间也可能短暂为空)。
|
|
1991
|
+
// 只剩外层卡的回合同样不挂:摘要行没有可汇总的内容。
|
|
1992
|
+
if ((summary || round.active) && (round.active || roundHasSummaryContent(round))) {
|
|
1874
1993
|
mountRoundTail(round, (identity) => [
|
|
1875
1994
|
compactAssistantLineComponent(identity, getSummary, deps.query),
|
|
1876
1995
|
]);
|
|
@@ -1973,7 +2092,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1973
2092
|
round = createRound(this, message);
|
|
1974
2093
|
roundByComponent.set(this, round);
|
|
1975
2094
|
} else if (!round) {
|
|
1976
|
-
|
|
2095
|
+
// 中间隔着用户消息就另起一回合,不并进上一个回合。
|
|
2096
|
+
round = joinOrCreateRound(this, message);
|
|
1977
2097
|
roundByComponent.set(this, round);
|
|
1978
2098
|
}
|
|
1979
2099
|
round.messages.set(this, message);
|
|
@@ -2009,7 +2129,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2009
2129
|
if (hasThinking) {
|
|
2010
2130
|
let round = roundByComponent.get(this);
|
|
2011
2131
|
if (!round) {
|
|
2012
|
-
round =
|
|
2132
|
+
round = joinOrCreateRound(this, message);
|
|
2013
2133
|
roundByComponent.set(this, round);
|
|
2014
2134
|
}
|
|
2015
2135
|
round.messages.set(this, message);
|
|
@@ -2035,10 +2155,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2035
2155
|
return patch.toolOriginalRender.call(this, width);
|
|
2036
2156
|
}
|
|
2037
2157
|
const name = String(this.toolName ?? "");
|
|
2038
|
-
if (
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2158
|
+
if (OUTER_TOOLS.has(name)) {
|
|
2159
|
+
// edit/write 走 compact 自己的 rich diff;ask_user_question 走 default 模式的专用渲染。
|
|
2160
|
+
// 两者都留在外层,不进摘要、不进展开面板。
|
|
2161
|
+
if (EDIT_WRITE_TOOLS.has(name)) {
|
|
2162
|
+
const pending = !this.result || this.isPartial === true;
|
|
2163
|
+
// 等待窗口(声明流式中或等待派发)也要转,否则这一行冻在第一帧。
|
|
2164
|
+
if (pending && (this.executionStarted || isLiveToolCall(this.toolCallId)))
|
|
2165
|
+
scheduleAnimation(this);
|
|
2166
|
+
return compactEditWriteLines(this, width, deps.writeMetadata);
|
|
2167
|
+
}
|
|
2168
|
+
return patch.toolOriginalRender.call(this, width);
|
|
2042
2169
|
}
|
|
2043
2170
|
// Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
|
|
2044
2171
|
if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
@@ -2054,8 +2181,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2054
2181
|
|
|
2055
2182
|
patch.toolInstalledUpdateDisplay = function (this: any) {
|
|
2056
2183
|
const id = String(this.toolCallId ?? "");
|
|
2057
|
-
if (
|
|
2058
|
-
|
|
2184
|
+
if (
|
|
2185
|
+
patch.active &&
|
|
2186
|
+
config.mode === "compact" &&
|
|
2187
|
+
(expandedRoundToolIds.has(id) || liveSlotToolIds.has(id))
|
|
2188
|
+
) {
|
|
2189
|
+
// 用户点开的保留展开;其余(含全局展开、live 槽位接管的)继续强制折叠。
|
|
2190
|
+
// 槽位卡在消息更新时就接管了 id,组件后到时 setExpanded 会把它翻开,外层多出一张卡。
|
|
2059
2191
|
if (this.expanded !== true) explicitRoundToolIds.delete(id);
|
|
2060
2192
|
else if (!explicitRoundToolIds.has(id)) this.expanded = false;
|
|
2061
2193
|
}
|
|
@@ -2104,7 +2236,6 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2104
2236
|
trackedToolComponents.clear();
|
|
2105
2237
|
hoveredAssistantComponent = undefined;
|
|
2106
2238
|
resetRounds();
|
|
2107
|
-
compactRunStatusGetter = undefined;
|
|
2108
2239
|
};
|
|
2109
2240
|
|
|
2110
2241
|
assistantPrototype.updateContent = patch.assistantInstalled;
|
|
@@ -2161,6 +2292,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2161
2292
|
}
|
|
2162
2293
|
}
|
|
2163
2294
|
},
|
|
2295
|
+
settleReplayedRound() {
|
|
2296
|
+
if (!patch.active || config.mode !== "compact") return;
|
|
2297
|
+
const round = activeRound;
|
|
2298
|
+
if (!round) return;
|
|
2299
|
+
finalizeRound(round); // clears activeRound — capture it first
|
|
2300
|
+
renderRound(round);
|
|
2301
|
+
},
|
|
2164
2302
|
shutdown() {
|
|
2165
2303
|
patch.dispose();
|
|
2166
2304
|
},
|