pi-cc-extensions 0.9.11 → 1.0.0
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 +5 -3
- package/README.md +5 -3
- package/extensions/config/config.ts +28 -10
- package/extensions/config/panel.ts +60 -20
- package/extensions/feature/shell/footer.ts +35 -49
- package/extensions/feature/shell/git-stats.ts +162 -0
- package/extensions/renderer/compact-mode.ts +121 -28
- package/extensions/renderer/default-mode.ts +152 -24
- package/extensions/renderer/index.ts +30 -1
- package/extensions/renderer/mouse/interaction.ts +22 -4
- package/extensions/renderer/mouse/sticky-prompt.ts +398 -0
- package/extensions/renderer/tool/ask-user-question.ts +173 -0
- package/extensions/renderer/tool/codemode.ts +4 -8
- package/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
- package/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
- package/extensions/renderer/tool/diff/diff-write-render.ts +35 -9
- package/extensions/renderer/tool/grouping.ts +115 -24
- package/extensions/renderer/tool/names.ts +3 -3
- package/extensions/renderer/tool/result.ts +106 -15
- package/extensions/utils/patch-keys.ts +5 -0
- package/package.json +1 -4
- package/themes/cc-dark.json +1 -1
- package/themes/cc-light.json +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { FooterGitStatsMode } from "../../config/config.ts";
|
|
4
|
+
|
|
5
|
+
export type GitStats = { add: number; del: number; status?: string };
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const BRANCH_BASE_REFS = ["refs/remotes/origin/HEAD", "refs/heads/main", "refs/heads/master"];
|
|
9
|
+
|
|
10
|
+
export function parseGitStats(stdout: string): GitStats {
|
|
11
|
+
let add = 0;
|
|
12
|
+
let del = 0;
|
|
13
|
+
for (const line of stdout.split("\n")) {
|
|
14
|
+
const match = line.match(/^(\d+)\s+(\d+)/);
|
|
15
|
+
if (match) {
|
|
16
|
+
add += Number(match[1]);
|
|
17
|
+
del += Number(match[2]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { add, del };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CONFLICT_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* git status --porcelain --branch 的第一行带分支头(## branch...origin/branch [ahead N, behind M]),
|
|
27
|
+
* 其余行是 XY 文件状态。返回 Starship 风格的状态符号串(= ! ? ⇡⇣),无变化时 undefined。
|
|
28
|
+
*/
|
|
29
|
+
export function parseGitStatus(stdout: string): string | undefined {
|
|
30
|
+
let conflicted = false;
|
|
31
|
+
let modified = false;
|
|
32
|
+
let untracked = false;
|
|
33
|
+
let ahead: number | undefined;
|
|
34
|
+
let behind: number | undefined;
|
|
35
|
+
for (const line of stdout.split("\n")) {
|
|
36
|
+
if (line.startsWith("## ")) {
|
|
37
|
+
ahead = Number(line.match(/ahead (\d+)/)?.[1]) || undefined;
|
|
38
|
+
behind = Number(line.match(/behind (\d+)/)?.[1]) || undefined;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const code = line.slice(0, 2);
|
|
42
|
+
if (code === "??") untracked = true;
|
|
43
|
+
else if (CONFLICT_CODES.has(code)) {
|
|
44
|
+
// 冲突文件必然也有未提交内容,! 与 = 同时出现
|
|
45
|
+
conflicted = true;
|
|
46
|
+
modified = true;
|
|
47
|
+
} else if (line.trim()) modified = true;
|
|
48
|
+
}
|
|
49
|
+
let status = "";
|
|
50
|
+
if (conflicted) status += "=";
|
|
51
|
+
if (modified) status += "!";
|
|
52
|
+
if (untracked) status += "?";
|
|
53
|
+
if (ahead) status += `⇡${ahead}`;
|
|
54
|
+
if (behind) status += `⇣${behind}`;
|
|
55
|
+
return status || undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Read only local refs and tracked files; never fetch or change the index. */
|
|
59
|
+
export async function readGitStats(
|
|
60
|
+
cwd: string,
|
|
61
|
+
mode: FooterGitStatsMode,
|
|
62
|
+
signal?: AbortSignal,
|
|
63
|
+
): Promise<GitStats | undefined> {
|
|
64
|
+
const git = async (args: string[]) => {
|
|
65
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
66
|
+
cwd,
|
|
67
|
+
timeout: 2000,
|
|
68
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
69
|
+
signal,
|
|
70
|
+
});
|
|
71
|
+
return stdout.trim();
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
let base = "HEAD";
|
|
75
|
+
if (mode === "branch") {
|
|
76
|
+
let baseCommit: string | undefined;
|
|
77
|
+
for (const ref of BRANCH_BASE_REFS) {
|
|
78
|
+
try {
|
|
79
|
+
baseCommit = await git(["rev-parse", "--verify", `${ref}^{commit}`]);
|
|
80
|
+
break;
|
|
81
|
+
} catch {
|
|
82
|
+
if (signal?.aborted) return undefined;
|
|
83
|
+
// Missing/dangling remote HEAD: try the local default branches.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!baseCommit) return undefined;
|
|
87
|
+
base = await git(["merge-base", "HEAD", baseCommit]);
|
|
88
|
+
}
|
|
89
|
+
// One comparison includes committed + staged + unstaged net changes,
|
|
90
|
+
// without double-counting edits that are later reverted. Untracked files are excluded.
|
|
91
|
+
const [numstat, statusOut] = await Promise.all([
|
|
92
|
+
git(["diff", "--numstat", base, "--"]),
|
|
93
|
+
git(["status", "--porcelain", "--branch"]).catch(() => ""),
|
|
94
|
+
]);
|
|
95
|
+
if (signal?.aborted) return undefined;
|
|
96
|
+
const stats = parseGitStats(numstat);
|
|
97
|
+
const status = parseGitStatus(statusOut);
|
|
98
|
+
return status ? { ...stats, status } : stats;
|
|
99
|
+
} catch {
|
|
100
|
+
// Non-repository, unborn HEAD, unrelated histories, timeout, etc.
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Coalesce overlapping refreshes and reject stale results after a mode/branch change. */
|
|
106
|
+
export function createGitStatsRefresher(options: {
|
|
107
|
+
getMode: () => FooterGitStatsMode;
|
|
108
|
+
query: (mode: FooterGitStatsMode, signal: AbortSignal) => Promise<GitStats | undefined>;
|
|
109
|
+
onChange: (stats: GitStats | undefined) => void;
|
|
110
|
+
}): { refresh: (reset?: boolean) => void; dispose: () => void } {
|
|
111
|
+
let stats: GitStats | undefined;
|
|
112
|
+
let mode = options.getMode();
|
|
113
|
+
let generation = 0;
|
|
114
|
+
let running = false;
|
|
115
|
+
let pending = false;
|
|
116
|
+
let disposed = false;
|
|
117
|
+
let controller: AbortController | undefined;
|
|
118
|
+
const publish = (next: GitStats | undefined) => {
|
|
119
|
+
if (stats?.add === next?.add && stats?.del === next?.del && stats?.status === next?.status)
|
|
120
|
+
return;
|
|
121
|
+
stats = next;
|
|
122
|
+
options.onChange(next);
|
|
123
|
+
};
|
|
124
|
+
const refresh = (reset = false) => {
|
|
125
|
+
if (disposed) return;
|
|
126
|
+
const nextMode = options.getMode();
|
|
127
|
+
if (reset || mode !== nextMode) publish(undefined);
|
|
128
|
+
mode = nextMode;
|
|
129
|
+
const request = ++generation;
|
|
130
|
+
if (running) {
|
|
131
|
+
pending = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
running = true;
|
|
135
|
+
const queryController = new AbortController();
|
|
136
|
+
controller = queryController;
|
|
137
|
+
void (async () => {
|
|
138
|
+
let next: GitStats | undefined;
|
|
139
|
+
try {
|
|
140
|
+
next = await options.query(nextMode, queryController.signal);
|
|
141
|
+
} catch {
|
|
142
|
+
// Failed queries clear the old numbers rather than leaving a stale chip.
|
|
143
|
+
} finally {
|
|
144
|
+
if (!disposed && request === generation && nextMode === options.getMode()) publish(next);
|
|
145
|
+
running = false;
|
|
146
|
+
controller = undefined;
|
|
147
|
+
if (pending && !disposed) {
|
|
148
|
+
pending = false;
|
|
149
|
+
refresh();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
refresh,
|
|
156
|
+
dispose: () => {
|
|
157
|
+
disposed = true;
|
|
158
|
+
generation++;
|
|
159
|
+
controller?.abort();
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -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,11 +30,14 @@
|
|
|
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";
|
|
@@ -42,10 +49,16 @@ import {
|
|
|
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 {
|
|
56
|
+
import {
|
|
57
|
+
insetComponent,
|
|
58
|
+
isLiveToolCall,
|
|
59
|
+
renderExpandedToolResult,
|
|
60
|
+
scheduleAnimation,
|
|
61
|
+
} from "./tool/result.ts";
|
|
49
62
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
50
63
|
import { formatDisplayPath } from "./tool/names.ts";
|
|
51
64
|
import {
|
|
@@ -81,6 +94,13 @@ export type CompactThinkingQuery = {
|
|
|
81
94
|
|
|
82
95
|
const EDIT_WRITE_TOOLS = new Set(["edit", "write"]);
|
|
83
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 留在外层的工具:不进摘要计数,折叠态也单独成行。
|
|
99
|
+
* edit/write 走 compact 自己的 rich diff;ask_user_question 走 default 模式的专用渲染
|
|
100
|
+
*(标题 (+N) 加折叠问答树)。
|
|
101
|
+
*/
|
|
102
|
+
const OUTER_TOOLS = new Set([...EDIT_WRITE_TOOLS, ASK_USER_QUESTION_TOOL_NAME]);
|
|
103
|
+
|
|
84
104
|
/**
|
|
85
105
|
* 回合收尾后延迟收拢的静默期(ms)。正文 token 往往先于 toolCall 到达,
|
|
86
106
|
* 立刻收拢会得到「先收再开」两次转场;静默期内被新回合接替则一并收拢。
|
|
@@ -205,7 +225,7 @@ function buildMessagesSummary(
|
|
|
205
225
|
for (const item of content) {
|
|
206
226
|
if (item?.type !== "toolCall") continue;
|
|
207
227
|
const rawName = typeof item.name === "string" ? item.name : "tool";
|
|
208
|
-
if (
|
|
228
|
+
if (OUTER_TOOLS.has(rawName)) continue;
|
|
209
229
|
const name = sanitizeToolResultText(rawName);
|
|
210
230
|
if (rawName.split(".").pop() === "read") {
|
|
211
231
|
const args = item.arguments ?? item.args ?? {};
|
|
@@ -674,7 +694,7 @@ function compactEditWriteLine(
|
|
|
674
694
|
component: any,
|
|
675
695
|
width: number,
|
|
676
696
|
writeMetadata?: WriteExecutionMetadataStore,
|
|
677
|
-
options: { hint?: boolean; flushLeft?: boolean } = {},
|
|
697
|
+
options: { hint?: boolean; flushLeft?: boolean; showStats?: boolean } = {},
|
|
678
698
|
): string[] {
|
|
679
699
|
const theme = themeOf();
|
|
680
700
|
const name = String(component.toolName ?? "tool");
|
|
@@ -691,7 +711,8 @@ function compactEditWriteLine(
|
|
|
691
711
|
const iconColor = isError ? "error" : isPending ? "accent" : "success";
|
|
692
712
|
let statsText = "";
|
|
693
713
|
let statsStyled = "";
|
|
694
|
-
|
|
714
|
+
// 统计只显示一处:正文自带 +A -D 时标题不再重复(showStats=false)。
|
|
715
|
+
if (!isError && !isPending && options.showStats !== false) {
|
|
695
716
|
const stats =
|
|
696
717
|
name === "edit"
|
|
697
718
|
? countEditDiffStats(component.result?.details)
|
|
@@ -710,8 +731,9 @@ function compactEditWriteLine(
|
|
|
710
731
|
// 展开卡 Box(1,1) 已 pad;折叠行自己留 1 格前导空格
|
|
711
732
|
const iconPart = `${options.flushLeft ? "" : " "}${theme.fg(iconColor, icon)} `;
|
|
712
733
|
const namePart = theme.fg("toolTitle", name);
|
|
713
|
-
const
|
|
714
|
-
|
|
734
|
+
const clickable = options.hint !== false && component.expanded !== true;
|
|
735
|
+
const clickText = clickable ? showMoreHintText() : "";
|
|
736
|
+
const hintText = clickable ? ` · ${clickText}` : "";
|
|
715
737
|
const fixedWidth =
|
|
716
738
|
visibleWidth(iconPart) +
|
|
717
739
|
visibleWidth(namePart) +
|
|
@@ -720,10 +742,20 @@ function compactEditWriteLine(
|
|
|
720
742
|
const pathWidth = Math.max(0, width - fixedWidth - (path ? 1 : 0));
|
|
721
743
|
const pathPart =
|
|
722
744
|
pathWidth > 0 && path ? ` ${formatDisplayPath(path, component.cwd, pathWidth)}` : "";
|
|
723
|
-
|
|
745
|
+
// 圆点保持 dim;hover 只高亮可点击文字,与 compact 摘要行一致
|
|
746
|
+
const hintPart = clickable
|
|
747
|
+
? `${theme.fg("dim", " · ")}${theme.fg(isToolCallHovered(component.toolCallId) ? "text" : "dim", clickText)}`
|
|
748
|
+
: "";
|
|
749
|
+
const line = `${iconPart}${namePart}${theme.fg("toolTitle", pathPart)}${statsStyled}${hintPart}`;
|
|
724
750
|
return ["", truncateToWidth(line, width, "")];
|
|
725
751
|
}
|
|
726
752
|
|
|
753
|
+
/** 正文自带 +A -D 的声明:diff 组件自报,折叠行数/宽度这些细节由它自己判断。 */
|
|
754
|
+
function richDiffBodyShowsStats(component: unknown): boolean {
|
|
755
|
+
const probe = (component as { bodyShowsStats?: () => boolean } | undefined)?.bodyShowsStats;
|
|
756
|
+
return typeof probe === "function" && probe.call(component) === true;
|
|
757
|
+
}
|
|
758
|
+
|
|
727
759
|
/** expanded 写进 rich diff 闭包,折叠/展开分槽缓存,避免每帧 parseDiff。 */
|
|
728
760
|
const compactRichDiffCache = new WeakMap<
|
|
729
761
|
object,
|
|
@@ -735,6 +767,7 @@ type CompactEditPaintHit = {
|
|
|
735
767
|
theme: unknown;
|
|
736
768
|
/** 展开卡的背景槽位同样参与命中判断。 */
|
|
737
769
|
bgSlot: string;
|
|
770
|
+
diffConfigKey: string;
|
|
738
771
|
expanded: boolean;
|
|
739
772
|
result: unknown;
|
|
740
773
|
isPartial: boolean;
|
|
@@ -766,6 +799,7 @@ function compactEditWriteLines(
|
|
|
766
799
|
const hover = isToolCallHovered(component.toolCallId);
|
|
767
800
|
const ioHover = compactEditIoHover(component);
|
|
768
801
|
const theme = themeOf();
|
|
802
|
+
const diffConfigKey = displayConfigCacheKey(getToolDisplayConfig());
|
|
769
803
|
if (!pending) {
|
|
770
804
|
const hit = compactEditPaintCache.get(component);
|
|
771
805
|
if (
|
|
@@ -773,6 +807,7 @@ function compactEditWriteLines(
|
|
|
773
807
|
hit.width === width &&
|
|
774
808
|
hit.theme === theme &&
|
|
775
809
|
hit.bgSlot === config.expandedCardBackground &&
|
|
810
|
+
hit.diffConfigKey === diffConfigKey &&
|
|
776
811
|
hit.expanded === expanded &&
|
|
777
812
|
hit.result === component.result &&
|
|
778
813
|
hit.isPartial === isPartial &&
|
|
@@ -789,6 +824,7 @@ function compactEditWriteLines(
|
|
|
789
824
|
width,
|
|
790
825
|
theme,
|
|
791
826
|
bgSlot: config.expandedCardBackground,
|
|
827
|
+
diffConfigKey,
|
|
792
828
|
expanded,
|
|
793
829
|
result: component.result,
|
|
794
830
|
isPartial,
|
|
@@ -843,7 +879,12 @@ function paintCompactEditWrite(
|
|
|
843
879
|
component.resultRendererComponent = candidate;
|
|
844
880
|
}
|
|
845
881
|
|
|
846
|
-
|
|
882
|
+
// 统计只显示一处:正文里已经有 +A -D(diff 头行/0 行摘要行)时标题不再重复。
|
|
883
|
+
const showStats = !(hasRich && richDiffBodyShowsStats(candidate));
|
|
884
|
+
const title = compactEditWriteLine(component, width, writeMetadata, {
|
|
885
|
+
hint: !hasRich,
|
|
886
|
+
showStats,
|
|
887
|
+
});
|
|
847
888
|
if (!hasRich && !expanded) {
|
|
848
889
|
return title;
|
|
849
890
|
}
|
|
@@ -883,6 +924,7 @@ function paintCompactEditWrite(
|
|
|
883
924
|
return compactEditWriteLine(component, innerWidth, writeMetadata, {
|
|
884
925
|
hint: false,
|
|
885
926
|
flushLeft: true,
|
|
927
|
+
showStats,
|
|
886
928
|
}).slice(1);
|
|
887
929
|
},
|
|
888
930
|
invalidate() {},
|
|
@@ -996,7 +1038,7 @@ function compactStopStatusLine(status: string, pad = 0): any {
|
|
|
996
1038
|
|
|
997
1039
|
/**
|
|
998
1040
|
* 槽位卡内的展开入口:卡里是只读围观,展开走摘要行自己的 hint,卡内不再重复提示。
|
|
999
|
-
* 提示有两处来源——我们的 showMoreHintText(`…
|
|
1041
|
+
* 提示有两处来源——我们的 showMoreHintText(`… N more lines · click to show more`)与
|
|
1000
1042
|
* pi 原生工具卡(`… (N earlier lines, ctrl+o to expand)`),所以按渲染结果收尾裁剪。
|
|
1001
1043
|
* 只去掉动作词,`(N more lines)` 这类计数保留。
|
|
1002
1044
|
*/
|
|
@@ -1357,6 +1399,54 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1357
1399
|
return found;
|
|
1358
1400
|
};
|
|
1359
1401
|
|
|
1402
|
+
/** 回合成员判定:本回合的 assistant 组件,或归属本回合的工具(含分组容器)。 */
|
|
1403
|
+
const roundMemberMatcher = (round: CompactRound): ((value: any) => boolean) => {
|
|
1404
|
+
const ids = roundToolCallIds(round);
|
|
1405
|
+
const matchesTool = (value: any): boolean =>
|
|
1406
|
+
typeof value?.toolCallId === "string" && ids.has(value.toolCallId);
|
|
1407
|
+
return (value: any): boolean =>
|
|
1408
|
+
round.messages.has(value) ||
|
|
1409
|
+
matchesTool(value) ||
|
|
1410
|
+
(Array.isArray(value?.children) && value.children.some(matchesTool));
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
/** 用户消息(含 skill 调用块)是回合的硬边界。 */
|
|
1414
|
+
const isUserMessageComponent = (value: any): boolean =>
|
|
1415
|
+
value instanceof UserMessageComponent || value instanceof SkillInvocationMessageComponent;
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* 新组件与回合已有成员之间是否隔着一条用户消息。隔着就说明这是下一回合的内容:
|
|
1419
|
+
* steer 消息插进了回合中间,或上一回合没有正文就收尾、用户又问了新问题。
|
|
1420
|
+
* 组件还没挂进容器时判断不了,按不隔开处理(会话重放的 refresh 会再求值一次)。
|
|
1421
|
+
*/
|
|
1422
|
+
const crossesUserMessage = (round: CompactRound, component: any): boolean => {
|
|
1423
|
+
const container = tailContainerOf(round.anchor);
|
|
1424
|
+
const children = container?.children;
|
|
1425
|
+
if (!Array.isArray(children)) return false;
|
|
1426
|
+
const index = children.indexOf(component);
|
|
1427
|
+
if (index < 0) return false;
|
|
1428
|
+
const isMember = roundMemberMatcher(round);
|
|
1429
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
1430
|
+
if (isMember(children[i])) return false;
|
|
1431
|
+
if (isUserMessageComponent(children[i])) return true;
|
|
1432
|
+
}
|
|
1433
|
+
return false;
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* 可以并入的活动回合。被用户消息隔开时返回 undefined,调用方收掉旧回合另起一个,
|
|
1438
|
+
* 保证回合不跨用户消息——否则区间里的用户消息会被当成外来行挪到回合末尾之后。
|
|
1439
|
+
*/
|
|
1440
|
+
const joinableActiveRound = (component: any): CompactRound | undefined =>
|
|
1441
|
+
activeRound && !crossesUserMessage(activeRound, component) ? activeRound : undefined;
|
|
1442
|
+
|
|
1443
|
+
/** 取并入目标;旧回合被用户消息隔开时先收尾(静默期一转场,新回合立即接手)。 */
|
|
1444
|
+
const joinOrCreateRound = (component: any, message: any): CompactRound => {
|
|
1445
|
+
const joinable = joinableActiveRound(component);
|
|
1446
|
+
if (!joinable && activeRound) finishRound(activeRound);
|
|
1447
|
+
return joinable ?? createRound(component, message);
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1360
1450
|
/** 尾行实际所在容器;缓存的 container 过期时按树找回来。 */
|
|
1361
1451
|
const tailHostContainer = (tail: MountedRoundTail): any =>
|
|
1362
1452
|
containerOfHost(tail.host) ??
|
|
@@ -1439,13 +1529,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1439
1529
|
const children = container.children as any[];
|
|
1440
1530
|
const current = children.indexOf(tail.host);
|
|
1441
1531
|
if (current >= 0) children.splice(current, 1);
|
|
1442
|
-
const
|
|
1443
|
-
const matchesTool = (value: any): boolean =>
|
|
1444
|
-
typeof value?.toolCallId === "string" && ids.has(value.toolCallId);
|
|
1445
|
-
const isMember = (value: any): boolean =>
|
|
1446
|
-
round.messages.has(value) ||
|
|
1447
|
-
matchesTool(value) ||
|
|
1448
|
-
(Array.isArray(value?.children) && value.children.some(matchesTool));
|
|
1532
|
+
const isMember = roundMemberMatcher(round);
|
|
1449
1533
|
let first = -1;
|
|
1450
1534
|
let last = -1;
|
|
1451
1535
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -1456,7 +1540,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1456
1540
|
const lastMember = last >= 0 ? children[last] : undefined;
|
|
1457
1541
|
const intruders: any[] = [];
|
|
1458
1542
|
for (let i = last - 1; i > first; i--) {
|
|
1459
|
-
|
|
1543
|
+
// 用户消息永远留在原地:挪走会把别人的消息挪到用户消息下面去。
|
|
1544
|
+
if (isMember(children[i]) || isUserMessageComponent(children[i])) continue;
|
|
1545
|
+
intruders.unshift(children.splice(i, 1)[0]);
|
|
1460
1546
|
}
|
|
1461
1547
|
if (lastMember) last = children.indexOf(lastMember);
|
|
1462
1548
|
const insertAt = last < 0 ? children.length : last + 1;
|
|
@@ -1638,7 +1724,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1638
1724
|
if (item?.type !== "toolCall" || typeof item.id !== "string") continue;
|
|
1639
1725
|
const tool = toolsById.get(item.id);
|
|
1640
1726
|
// edit/write 保留自己的 compact 行,不进槽位。
|
|
1641
|
-
if (
|
|
1727
|
+
if (OUTER_TOOLS.has(String(tool?.toolName ?? item.name ?? ""))) continue;
|
|
1642
1728
|
round.liveSlotToolIds.add(item.id);
|
|
1643
1729
|
liveSlotToolIds.add(item.id);
|
|
1644
1730
|
if (!tool) continue;
|
|
@@ -1695,8 +1781,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1695
1781
|
for (const id of ids) explicitRoundToolIds.delete(id);
|
|
1696
1782
|
collapseTools(
|
|
1697
1783
|
(tool) =>
|
|
1698
|
-
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1699
|
-
!EDIT_WRITE_TOOLS.has(String(tool.toolName ?? "")),
|
|
1784
|
+
ids.has(String(tool.toolCallId ?? "")) && !OUTER_TOOLS.has(String(tool.toolName ?? "")),
|
|
1700
1785
|
);
|
|
1701
1786
|
}
|
|
1702
1787
|
for (const id of round.liveSlotToolIds) liveSlotToolIds.delete(id);
|
|
@@ -1766,7 +1851,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1766
1851
|
const placedToolIds = new Set<string>();
|
|
1767
1852
|
const placeTool = (id: string, fallbackName?: string) => {
|
|
1768
1853
|
const tool = toolsById.get(id);
|
|
1769
|
-
if (!tool ||
|
|
1854
|
+
if (!tool || OUTER_TOOLS.has(String(tool.toolName ?? fallbackName ?? ""))) return;
|
|
1770
1855
|
if (placedToolIds.has(id)) return;
|
|
1771
1856
|
placedToolIds.add(id);
|
|
1772
1857
|
cardItems.push({ tool });
|
|
@@ -1832,7 +1917,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1832
1917
|
collapseTools(
|
|
1833
1918
|
(tool) =>
|
|
1834
1919
|
ids.has(String(tool.toolCallId ?? "")) &&
|
|
1835
|
-
!
|
|
1920
|
+
!OUTER_TOOLS.has(String(tool.toolName ?? "")) &&
|
|
1836
1921
|
!explicitRoundToolIds.has(String(tool.toolCallId ?? "")),
|
|
1837
1922
|
);
|
|
1838
1923
|
flushPanel();
|
|
@@ -1968,7 +2053,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1968
2053
|
round = createRound(this, message);
|
|
1969
2054
|
roundByComponent.set(this, round);
|
|
1970
2055
|
} else if (!round) {
|
|
1971
|
-
|
|
2056
|
+
// 中间隔着用户消息就另起一回合,不并进上一个回合。
|
|
2057
|
+
round = joinOrCreateRound(this, message);
|
|
1972
2058
|
roundByComponent.set(this, round);
|
|
1973
2059
|
}
|
|
1974
2060
|
round.messages.set(this, message);
|
|
@@ -2004,7 +2090,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2004
2090
|
if (hasThinking) {
|
|
2005
2091
|
let round = roundByComponent.get(this);
|
|
2006
2092
|
if (!round) {
|
|
2007
|
-
round =
|
|
2093
|
+
round = joinOrCreateRound(this, message);
|
|
2008
2094
|
roundByComponent.set(this, round);
|
|
2009
2095
|
}
|
|
2010
2096
|
round.messages.set(this, message);
|
|
@@ -2030,10 +2116,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2030
2116
|
return patch.toolOriginalRender.call(this, width);
|
|
2031
2117
|
}
|
|
2032
2118
|
const name = String(this.toolName ?? "");
|
|
2033
|
-
if (
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2119
|
+
if (OUTER_TOOLS.has(name)) {
|
|
2120
|
+
// edit/write 走 compact 自己的 rich diff;ask_user_question 走 default 模式的专用渲染。
|
|
2121
|
+
// 两者都留在外层,不进摘要、不进展开面板。
|
|
2122
|
+
if (EDIT_WRITE_TOOLS.has(name)) {
|
|
2123
|
+
const pending = !this.result || this.isPartial === true;
|
|
2124
|
+
// 等待窗口(声明流式中或等待派发)也要转,否则这一行冻在第一帧。
|
|
2125
|
+
if (pending && (this.executionStarted || isLiveToolCall(this.toolCallId)))
|
|
2126
|
+
scheduleAnimation(this);
|
|
2127
|
+
return compactEditWriteLines(this, width, deps.writeMetadata);
|
|
2128
|
+
}
|
|
2129
|
+
return patch.toolOriginalRender.call(this, width);
|
|
2037
2130
|
}
|
|
2038
2131
|
// Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
|
|
2039
2132
|
if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
|