@bolloon/bolloon-agent 0.2.0 → 0.2.2
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/dist/agents/judgment-protocol.js +479 -0
- package/dist/agents/pi-sdk.js +269 -1
- package/dist/electron/config.js +9 -14
- package/dist/electron/dialogs.js +16 -53
- package/dist/electron/first-run.js +24 -65
- package/dist/electron/ipc.js +10 -14
- package/dist/electron/logger.js +7 -44
- package/dist/electron/main.js +38 -41
- package/dist/electron/menu.js +13 -18
- package/dist/electron/paths.js +12 -54
- package/dist/electron/server.js +18 -56
- package/dist/electron/tray.js +15 -53
- package/dist/electron/window.js +22 -61
- package/dist/security/tool-gate.js +4 -0
- package/dist/web/client.js +3767 -2873
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/ui/message-renderer.js +442 -326
- package/dist/web/ui/step-timeline.js +351 -255
- package/ios/App/App/AppDelegate.swift +49 -0
- package/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png +0 -0
- package/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/ios/App/App/Assets.xcassets/Contents.json +6 -0
- package/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json +23 -0
- package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png +0 -0
- package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png +0 -0
- package/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png +0 -0
- package/ios/App/App/Base.lproj/LaunchScreen.storyboard +32 -0
- package/ios/App/App/Base.lproj/Main.storyboard +19 -0
- package/ios/App/App/Info.plist +88 -0
- package/ios/App/App.xcodeproj/project.pbxproj +376 -0
- package/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/App/CapApp-SPM/Package.swift +25 -0
- package/ios/App/CapApp-SPM/README.md +5 -0
- package/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift +1 -0
- package/ios/debug.xcconfig +1 -0
- package/package.json +14 -3
- package/scripts/build-app-bundle.cjs +170 -0
- package/.comm/README.md +0 -21
- package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +0 -7
|
@@ -1,279 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* step-timeline.ts — 气泡内 4 状态步骤条 (2026-06-15)
|
|
3
|
+
*
|
|
4
|
+
* 职责 (单一):
|
|
5
|
+
* - 给每条 AI 消息挂一个可折叠/展开的步骤条
|
|
6
|
+
* - 状态机: idle ○ / active ● / done 🟢 / error 🔴
|
|
7
|
+
* - 摘要条 + 详情区共用同一份 Step[] 数据
|
|
8
|
+
* - 流式期间在 streaming 元素内, finalize 时搬到正式 message
|
|
9
|
+
* - 折叠状态按 channel 持久化 (localStorage)
|
|
10
|
+
*
|
|
11
|
+
* 状态 (模块私有, 每个 message 一个实例):
|
|
12
|
+
* - steps: Step[]
|
|
13
|
+
* - currentIndex: number
|
|
14
|
+
* - expanded: boolean (受 localStorage 影响)
|
|
15
|
+
*
|
|
16
|
+
* 输入 (从 message-renderer 调用):
|
|
17
|
+
* - createEmptyStepTimeline(channelId) -> HTMLElement
|
|
18
|
+
* - mountStepTimeline(messageEl, channelId) // 已有 message 上挂 timeline
|
|
19
|
+
* - pushStepToTimeline(timelineEl, type, data) // 订阅 step_start/done/error
|
|
20
|
+
* - migrateStepTimeline(fromEl, toEl) // finalize 时搬家
|
|
21
|
+
*
|
|
22
|
+
* 输出 (DOM):
|
|
23
|
+
* - .step-timeline / .step-timeline-summary / .step-timeline-body
|
|
24
|
+
* - .step-dot (摘要条圆点) / .step-timeline-node (详情区节点)
|
|
25
|
+
*/
|
|
1
26
|
const MAX_VISIBLE_NODES = 8;
|
|
2
|
-
const STORAGE_PREFIX =
|
|
3
|
-
const stateMap =
|
|
27
|
+
const STORAGE_PREFIX = 'bolloon.stepTimeline.expanded.';
|
|
28
|
+
const stateMap = new WeakMap();
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// 工具: 持久化
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
4
32
|
function readExpanded(channelId) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
33
|
+
if (!channelId)
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
return localStorage.getItem(STORAGE_PREFIX + channelId) === '1';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
11
41
|
}
|
|
12
42
|
function writeExpanded(channelId, expanded) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
if (!channelId)
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
localStorage.setItem(STORAGE_PREFIX + channelId, expanded ? '1' : '0');
|
|
47
|
+
}
|
|
48
|
+
catch { /* localStorage 写失败 (隐私模式), 静默 */ }
|
|
18
49
|
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// 工具: 摘要条 title
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
19
53
|
function computeTitle(steps) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
if (steps.length === 0)
|
|
55
|
+
return '执行步骤';
|
|
56
|
+
// 优先 active, 其次最后一个 error
|
|
57
|
+
const activeIdx = steps.findIndex(s => s.status === 'active');
|
|
58
|
+
if (activeIdx >= 0)
|
|
59
|
+
return `● 执行中 · ${steps[activeIdx].name}`;
|
|
60
|
+
const lastErrorIdx = (() => {
|
|
61
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
62
|
+
if (steps[i].status === 'error')
|
|
63
|
+
return i;
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
})();
|
|
67
|
+
if (lastErrorIdx >= 0)
|
|
68
|
+
return `✗ 失败 · ${steps[lastErrorIdx].name}`;
|
|
69
|
+
return `✓ 已完成 · ${steps.length} 步`;
|
|
31
70
|
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// 核心: 渲染 (摘要条 + 详情区, 共用 steps 状态)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
32
74
|
function render(timelineEl) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const hiddenCount = Math.max(0, steps.length - MAX_VISIBLE_NODES);
|
|
60
|
-
const visibleSteps = showAll ? steps : steps.slice(-MAX_VISIBLE_NODES);
|
|
61
|
-
const existing = Array.from(listEl.children);
|
|
62
|
-
let htmlIdx = 0;
|
|
63
|
-
if (!showAll && hiddenCount > 0) {
|
|
64
|
-
let moreBtn = existing[htmlIdx];
|
|
65
|
-
if (!moreBtn || !moreBtn.classList.contains("step-timeline-more")) {
|
|
66
|
-
moreBtn = document.createElement("li");
|
|
67
|
-
moreBtn.className = "step-timeline-more";
|
|
68
|
-
moreBtn.setAttribute("role", "button");
|
|
69
|
-
moreBtn.textContent = `+ ${hiddenCount} \u66F4\u591A`;
|
|
70
|
-
moreBtn.onclick = () => {
|
|
71
|
-
state.showAll = true;
|
|
72
|
-
render(timelineEl);
|
|
73
|
-
};
|
|
74
|
-
if (existing[htmlIdx] && existing[htmlIdx] !== moreBtn) {
|
|
75
|
-
listEl.replaceChild(moreBtn, existing[htmlIdx]);
|
|
76
|
-
} else {
|
|
77
|
-
listEl.appendChild(moreBtn);
|
|
75
|
+
const state = stateMap.get(timelineEl);
|
|
76
|
+
if (!state)
|
|
77
|
+
return;
|
|
78
|
+
const { steps, expanded, showAll } = state;
|
|
79
|
+
const titleEl = timelineEl.querySelector('[data-current-tool]');
|
|
80
|
+
if (titleEl)
|
|
81
|
+
titleEl.textContent = computeTitle(steps);
|
|
82
|
+
// 摘要条 dots
|
|
83
|
+
const dotsEl = timelineEl.querySelector('[data-dots]');
|
|
84
|
+
if (dotsEl) {
|
|
85
|
+
// diff: 复用现有 dot, 多余 remove, 缺少 append
|
|
86
|
+
const existing = Array.from(dotsEl.children);
|
|
87
|
+
for (let i = 0; i < steps.length; i++) {
|
|
88
|
+
const step = steps[i];
|
|
89
|
+
let dot = existing[i];
|
|
90
|
+
if (!dot) {
|
|
91
|
+
dot = document.createElement('span');
|
|
92
|
+
dot.className = 'step-dot';
|
|
93
|
+
dot.setAttribute('data-index', String(i));
|
|
94
|
+
dotsEl.appendChild(dot);
|
|
95
|
+
}
|
|
96
|
+
dot.setAttribute('data-status', step.status);
|
|
97
|
+
dot.setAttribute('title', `${step.name} · ${step.status}`);
|
|
98
|
+
}
|
|
99
|
+
while (dotsEl.children.length > steps.length) {
|
|
100
|
+
dotsEl.removeChild(dotsEl.lastChild);
|
|
78
101
|
}
|
|
79
|
-
} else {
|
|
80
|
-
moreBtn.textContent = `+ ${hiddenCount} \u66F4\u591A`;
|
|
81
|
-
}
|
|
82
|
-
htmlIdx++;
|
|
83
102
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// 详情区 list
|
|
104
|
+
const listEl = timelineEl.querySelector('[data-list]');
|
|
105
|
+
if (listEl) {
|
|
106
|
+
const hiddenCount = Math.max(0, steps.length - MAX_VISIBLE_NODES);
|
|
107
|
+
const visibleSteps = showAll ? steps : steps.slice(-MAX_VISIBLE_NODES);
|
|
108
|
+
const existing = Array.from(listEl.children);
|
|
109
|
+
let htmlIdx = 0;
|
|
110
|
+
// "+N 更多" 折叠行 (仅在 showAll=false 且有 hidden 时)
|
|
111
|
+
if (!showAll && hiddenCount > 0) {
|
|
112
|
+
let moreBtn = existing[htmlIdx];
|
|
113
|
+
if (!moreBtn || !moreBtn.classList.contains('step-timeline-more')) {
|
|
114
|
+
moreBtn = document.createElement('li');
|
|
115
|
+
moreBtn.className = 'step-timeline-more';
|
|
116
|
+
moreBtn.setAttribute('role', 'button');
|
|
117
|
+
moreBtn.textContent = `+ ${hiddenCount} 更多`;
|
|
118
|
+
moreBtn.onclick = () => {
|
|
119
|
+
state.showAll = true;
|
|
120
|
+
render(timelineEl);
|
|
121
|
+
};
|
|
122
|
+
if (existing[htmlIdx] && existing[htmlIdx] !== moreBtn) {
|
|
123
|
+
listEl.replaceChild(moreBtn, existing[htmlIdx]);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
listEl.appendChild(moreBtn);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
moreBtn.textContent = `+ ${hiddenCount} 更多`;
|
|
131
|
+
}
|
|
132
|
+
htmlIdx++;
|
|
133
|
+
}
|
|
134
|
+
for (let i = 0; i < visibleSteps.length; i++) {
|
|
135
|
+
const step = visibleSteps[i];
|
|
136
|
+
const realIndex = showAll ? i : (steps.length - visibleSteps.length + i);
|
|
137
|
+
let node = existing[htmlIdx];
|
|
138
|
+
if (!node || !node.classList.contains('step-timeline-node')) {
|
|
139
|
+
node = document.createElement('li');
|
|
140
|
+
node.className = 'step-timeline-node';
|
|
141
|
+
node.setAttribute('data-index', String(realIndex));
|
|
142
|
+
const marker = document.createElement('span');
|
|
143
|
+
marker.className = 'step-timeline-marker';
|
|
144
|
+
const label = document.createElement('span');
|
|
145
|
+
label.className = 'step-timeline-label';
|
|
146
|
+
const args = document.createElement('span');
|
|
147
|
+
args.className = 'step-timeline-args';
|
|
148
|
+
node.appendChild(marker);
|
|
149
|
+
node.appendChild(label);
|
|
150
|
+
node.appendChild(args);
|
|
151
|
+
if (existing[htmlIdx] && existing[htmlIdx] !== node) {
|
|
152
|
+
listEl.replaceChild(node, existing[htmlIdx]);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
listEl.appendChild(node);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
node.setAttribute('data-index', String(realIndex));
|
|
160
|
+
}
|
|
161
|
+
node.setAttribute('data-status', step.status);
|
|
162
|
+
const label = node.querySelector('.step-timeline-label');
|
|
163
|
+
if (label)
|
|
164
|
+
label.textContent = step.name;
|
|
165
|
+
const argsEl = node.querySelector('.step-timeline-args');
|
|
166
|
+
if (argsEl) {
|
|
167
|
+
const argStr = step.args && Object.keys(step.args).length > 0
|
|
168
|
+
? JSON.stringify(step.args).slice(0, 60)
|
|
169
|
+
: '';
|
|
170
|
+
argsEl.textContent = argStr;
|
|
171
|
+
argsEl.style.display = argStr ? '' : 'none';
|
|
172
|
+
}
|
|
173
|
+
htmlIdx++;
|
|
105
174
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const argStr = step.args && Object.keys(step.args).length > 0 ? JSON.stringify(step.args).slice(0, 60) : "";
|
|
115
|
-
argsEl.textContent = argStr;
|
|
116
|
-
argsEl.style.display = argStr ? "" : "none";
|
|
117
|
-
}
|
|
118
|
-
htmlIdx++;
|
|
175
|
+
// 清理多余 li
|
|
176
|
+
while (listEl.children.length > htmlIdx) {
|
|
177
|
+
listEl.removeChild(listEl.lastChild);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 折叠态: 空状态显示占位, 有节点就保持
|
|
181
|
+
if (steps.length === 0) {
|
|
182
|
+
timelineEl.setAttribute('data-empty', 'true');
|
|
119
183
|
}
|
|
120
|
-
|
|
121
|
-
|
|
184
|
+
else {
|
|
185
|
+
timelineEl.removeAttribute('data-empty');
|
|
186
|
+
}
|
|
187
|
+
// 折叠 class
|
|
188
|
+
const body = timelineEl.querySelector('[data-body]');
|
|
189
|
+
if (body) {
|
|
190
|
+
if (expanded) {
|
|
191
|
+
body.style.maxHeight = body.scrollHeight + 'px';
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
// 动画结束后清掉, 让内容自适应 (新 step 进来时能撑开)
|
|
194
|
+
if (state.expanded)
|
|
195
|
+
body.style.maxHeight = '';
|
|
196
|
+
}, 300);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
body.style.maxHeight = '0';
|
|
200
|
+
}
|
|
122
201
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const body = timelineEl.querySelector("[data-body]");
|
|
130
|
-
if (body) {
|
|
131
|
-
if (expanded) {
|
|
132
|
-
body.style.maxHeight = body.scrollHeight + "px";
|
|
133
|
-
setTimeout(() => {
|
|
134
|
-
if (state.expanded) body.style.maxHeight = "";
|
|
135
|
-
}, 300);
|
|
136
|
-
} else {
|
|
137
|
-
body.style.maxHeight = "0";
|
|
202
|
+
const arrow = timelineEl.querySelector('.step-timeline-arrow');
|
|
203
|
+
if (arrow)
|
|
204
|
+
arrow.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
205
|
+
// 自动滚到底 (新节点进来时, 详情区跟到底)
|
|
206
|
+
if (expanded && listEl) {
|
|
207
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
138
208
|
}
|
|
139
|
-
}
|
|
140
|
-
const arrow = timelineEl.querySelector(".step-timeline-arrow");
|
|
141
|
-
if (arrow) arrow.style.transform = expanded ? "rotate(180deg)" : "rotate(0deg)";
|
|
142
|
-
if (expanded && listEl) {
|
|
143
|
-
listEl.scrollTop = listEl.scrollHeight;
|
|
144
|
-
}
|
|
145
209
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// 公共: 创建一个空 timeline (AI 消息创建时挂的占位)
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
export function createEmptyStepTimeline(channelId = null) {
|
|
214
|
+
const root = document.createElement('div');
|
|
215
|
+
root.className = 'step-timeline';
|
|
216
|
+
root.setAttribute('data-step-timeline', '');
|
|
217
|
+
root.setAttribute('data-empty', 'true');
|
|
218
|
+
// 摘要条
|
|
219
|
+
const summary = document.createElement('div');
|
|
220
|
+
summary.className = 'step-timeline-summary';
|
|
221
|
+
summary.setAttribute('data-summary', '');
|
|
222
|
+
const titleSpan = document.createElement('span');
|
|
223
|
+
titleSpan.className = 'step-timeline-title';
|
|
224
|
+
const titleContent = document.createElement('span');
|
|
225
|
+
titleContent.setAttribute('data-current-tool', '');
|
|
226
|
+
titleContent.textContent = '执行步骤';
|
|
227
|
+
titleSpan.appendChild(titleContent);
|
|
228
|
+
const dots = document.createElement('div');
|
|
229
|
+
dots.className = 'step-timeline-dots';
|
|
230
|
+
dots.setAttribute('data-dots', '');
|
|
231
|
+
const arrow = document.createElement('span');
|
|
232
|
+
arrow.className = 'step-timeline-arrow';
|
|
233
|
+
arrow.textContent = '▾';
|
|
234
|
+
summary.appendChild(titleSpan);
|
|
235
|
+
summary.appendChild(dots);
|
|
236
|
+
summary.appendChild(arrow);
|
|
237
|
+
summary.onclick = () => {
|
|
238
|
+
const state = stateMap.get(root);
|
|
239
|
+
if (!state)
|
|
240
|
+
return;
|
|
241
|
+
state.expanded = !state.expanded;
|
|
242
|
+
writeExpanded(state.channelId, state.expanded);
|
|
243
|
+
render(root);
|
|
244
|
+
};
|
|
245
|
+
// 详情区
|
|
246
|
+
const body = document.createElement('div');
|
|
247
|
+
body.className = 'step-timeline-body';
|
|
248
|
+
body.setAttribute('data-body', '');
|
|
249
|
+
const list = document.createElement('ul');
|
|
250
|
+
list.className = 'step-timeline-list';
|
|
251
|
+
list.setAttribute('data-list', '');
|
|
252
|
+
body.appendChild(list);
|
|
253
|
+
root.appendChild(summary);
|
|
254
|
+
root.appendChild(body);
|
|
255
|
+
// 初始折叠态
|
|
256
|
+
const initialExpanded = readExpanded(channelId);
|
|
257
|
+
stateMap.set(root, {
|
|
258
|
+
steps: [],
|
|
259
|
+
currentIndex: -1,
|
|
260
|
+
expanded: initialExpanded,
|
|
261
|
+
channelId: channelId || null,
|
|
262
|
+
showAll: false,
|
|
263
|
+
});
|
|
174
264
|
render(root);
|
|
175
|
-
|
|
176
|
-
const body = document.createElement("div");
|
|
177
|
-
body.className = "step-timeline-body";
|
|
178
|
-
body.setAttribute("data-body", "");
|
|
179
|
-
const list = document.createElement("ul");
|
|
180
|
-
list.className = "step-timeline-list";
|
|
181
|
-
list.setAttribute("data-list", "");
|
|
182
|
-
body.appendChild(list);
|
|
183
|
-
root.appendChild(summary);
|
|
184
|
-
root.appendChild(body);
|
|
185
|
-
const initialExpanded = readExpanded(channelId);
|
|
186
|
-
stateMap.set(root, {
|
|
187
|
-
steps: [],
|
|
188
|
-
currentIndex: -1,
|
|
189
|
-
expanded: initialExpanded,
|
|
190
|
-
channelId: channelId || null,
|
|
191
|
-
showAll: false
|
|
192
|
-
});
|
|
193
|
-
render(root);
|
|
194
|
-
return root;
|
|
265
|
+
return root;
|
|
195
266
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
bubble.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// 公共: 把 timeline 挂到 message 上 (初始化或补挂)
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
export function mountStepTimeline(messageEl, channelId) {
|
|
271
|
+
// 已经挂过 → 复用
|
|
272
|
+
const existing = messageEl.querySelector('[data-step-timeline]');
|
|
273
|
+
if (existing) {
|
|
274
|
+
const state = stateMap.get(existing);
|
|
275
|
+
if (state && channelId && !state.channelId)
|
|
276
|
+
state.channelId = channelId;
|
|
277
|
+
return existing;
|
|
278
|
+
}
|
|
279
|
+
// 找插入点: 在 .bubble 之后, .message-actions 之前
|
|
280
|
+
const bubble = messageEl.querySelector('.bubble');
|
|
281
|
+
const actions = messageEl.querySelector('.message-actions');
|
|
282
|
+
const timeline = createEmptyStepTimeline(channelId);
|
|
283
|
+
if (actions && actions.parentNode === messageEl) {
|
|
284
|
+
messageEl.insertBefore(timeline, actions);
|
|
285
|
+
}
|
|
286
|
+
else if (bubble && bubble.parentNode === messageEl) {
|
|
287
|
+
bubble.parentNode.insertBefore(timeline, bubble.nextSibling);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
messageEl.appendChild(timeline);
|
|
291
|
+
}
|
|
292
|
+
return timeline;
|
|
214
293
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
state
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// 公共: 推一个 step 事件 (start/done/error)
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
export function pushStepToTimeline(timelineEl, eventType, data) {
|
|
298
|
+
const state = stateMap.get(timelineEl);
|
|
299
|
+
if (!state)
|
|
300
|
+
return;
|
|
301
|
+
const { steps } = state;
|
|
302
|
+
if (eventType === 'step_start') {
|
|
303
|
+
state.currentIndex = steps.length;
|
|
304
|
+
steps.push({
|
|
305
|
+
name: data.tool,
|
|
306
|
+
status: 'active',
|
|
307
|
+
args: data.args,
|
|
308
|
+
eventType,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// step_done / step_error — 找当前 active 节点关闭
|
|
313
|
+
// 优先匹配 currentIndex, 其次回退到最后一个 active
|
|
314
|
+
let targetIdx = state.currentIndex;
|
|
315
|
+
if (targetIdx < 0 || !steps[targetIdx] || steps[targetIdx].name !== data.tool) {
|
|
316
|
+
// 兜底: 找最后一个同名 active
|
|
317
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
318
|
+
if (steps[i].name === data.tool && steps[i].status === 'active') {
|
|
319
|
+
targetIdx = i;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
234
323
|
}
|
|
235
|
-
|
|
324
|
+
if (targetIdx < 0) {
|
|
325
|
+
// 没有 active 节点匹配, 当作新 error 节点
|
|
326
|
+
targetIdx = steps.length;
|
|
327
|
+
steps.push({ name: data.tool, status: 'error', eventType });
|
|
328
|
+
}
|
|
329
|
+
if (eventType === 'step_done') {
|
|
330
|
+
steps[targetIdx].status = data.success === false ? 'error' : 'done';
|
|
331
|
+
if (data.output)
|
|
332
|
+
steps[targetIdx].output = data.output;
|
|
333
|
+
if (data.error)
|
|
334
|
+
steps[targetIdx].error = data.error;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
steps[targetIdx].status = 'error';
|
|
338
|
+
if (data.error)
|
|
339
|
+
steps[targetIdx].error = data.error;
|
|
340
|
+
}
|
|
341
|
+
steps[targetIdx].eventType = eventType;
|
|
342
|
+
state.currentIndex = -1;
|
|
236
343
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
344
|
+
render(timelineEl);
|
|
345
|
+
}
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// 公共: finalize 时把 timeline 元素从 from 搬到 to (避免重渲)
|
|
348
|
+
// 接受 timelineEl 本身或 fromEl 内部的 timeline
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
export function migrateStepTimeline(fromEl, toEl) {
|
|
351
|
+
const fromTimeline = fromEl.querySelector('[data-step-timeline]');
|
|
352
|
+
const toTimeline = toEl.querySelector('[data-step-timeline]');
|
|
353
|
+
if (!fromTimeline)
|
|
354
|
+
return;
|
|
355
|
+
// 移除 toEl 的占位, 搬 fromEl 的过来
|
|
356
|
+
if (toTimeline && toTimeline !== fromTimeline) {
|
|
357
|
+
toTimeline.remove();
|
|
240
358
|
}
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
359
|
+
if (toTimeline !== fromTimeline) {
|
|
360
|
+
toEl.appendChild(fromTimeline);
|
|
361
|
+
}
|
|
362
|
+
// 重渲一次 (channelId 注入)
|
|
363
|
+
const state = stateMap.get(fromTimeline);
|
|
364
|
+
if (state) {
|
|
365
|
+
render(fromTimeline);
|
|
248
366
|
}
|
|
249
|
-
steps[targetIdx].eventType = eventType;
|
|
250
|
-
state.currentIndex = -1;
|
|
251
|
-
}
|
|
252
|
-
render(timelineEl);
|
|
253
|
-
}
|
|
254
|
-
function migrateStepTimeline(fromEl, toEl) {
|
|
255
|
-
const fromTimeline = fromEl.querySelector("[data-step-timeline]");
|
|
256
|
-
const toTimeline = toEl.querySelector("[data-step-timeline]");
|
|
257
|
-
if (!fromTimeline) return;
|
|
258
|
-
if (toTimeline && toTimeline !== fromTimeline) {
|
|
259
|
-
toTimeline.remove();
|
|
260
|
-
}
|
|
261
|
-
if (toTimeline !== fromTimeline) {
|
|
262
|
-
toEl.appendChild(fromTimeline);
|
|
263
|
-
}
|
|
264
|
-
const state = stateMap.get(fromTimeline);
|
|
265
|
-
if (state) {
|
|
266
|
-
render(fromTimeline);
|
|
267
|
-
}
|
|
268
367
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// 公共: 拿到一个 message 内的 timeline (无则返回 null)
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
export function getStepTimeline(messageEl) {
|
|
372
|
+
if (!messageEl)
|
|
373
|
+
return null;
|
|
374
|
+
return messageEl.querySelector('[data-step-timeline]');
|
|
272
375
|
}
|
|
273
|
-
export {
|
|
274
|
-
createEmptyStepTimeline,
|
|
275
|
-
getStepTimeline,
|
|
276
|
-
migrateStepTimeline,
|
|
277
|
-
mountStepTimeline,
|
|
278
|
-
pushStepToTimeline
|
|
279
|
-
};
|