@bolloon/bolloon-agent 0.1.35 → 0.1.36

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.
Files changed (194) hide show
  1. package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
  2. package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
  3. package/Bolloon.md +45 -4
  4. package/Dive-into/CITATION.cff +17 -0
  5. package/Dive-into/LICENSE +25 -0
  6. package/Dive-into/README.md +598 -0
  7. package/Dive-into/README_zh.md +599 -0
  8. package/Dive-into/assets/context.png +0 -0
  9. package/Dive-into/assets/extensibility.png +0 -0
  10. package/Dive-into/assets/iteration.png +0 -0
  11. package/Dive-into/assets/layered_architecture.png +0 -0
  12. package/Dive-into/assets/main_structure.png +0 -0
  13. package/Dive-into/assets/permission.png +0 -0
  14. package/Dive-into/assets/session_compact.png +0 -0
  15. package/Dive-into/assets/subagent.png +0 -0
  16. package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
  17. package/README.md +1 -1
  18. package/dist/agents/p2p-chat-tools.js +6 -6
  19. package/dist/agents/permission-mode.js +115 -0
  20. package/dist/agents/pi-sdk.js +435 -25
  21. package/dist/agents/pre-tool-validator.js +194 -0
  22. package/dist/agents/workflow-pivot-loop.js +113 -12
  23. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
  24. package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
  25. package/dist/bollharness/src/scripts/context_router.js +1 -1
  26. package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
  27. package/dist/bollharness/src/scripts/guard_router.js +1 -1
  28. package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
  29. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
  30. package/dist/bollharness-integration/context-router.js +2 -2
  31. package/dist/bollharness-integration/guard-checker.js +1 -1
  32. package/dist/bootstrap/bootstrap.js +3 -0
  33. package/dist/bootstrap/context-collector.js +17 -1
  34. package/dist/bootstrap/context-hierarchy.js +218 -0
  35. package/dist/bootstrap/lifecycle-hooks.js +86 -24
  36. package/dist/bootstrap/project-context.js +10 -3
  37. package/dist/context-compaction/auto-compact.js +144 -0
  38. package/dist/context-compaction/budget-gate.js +28 -0
  39. package/dist/context-compaction/budget-reduce.js +35 -0
  40. package/dist/context-compaction/context-collapse.js +66 -0
  41. package/dist/context-compaction/index.js +21 -0
  42. package/dist/context-compaction/microcompact.js +51 -0
  43. package/dist/context-compaction/pipeline.js +123 -0
  44. package/dist/context-compaction/snip.js +45 -0
  45. package/dist/context-compaction/token-estimator.js +35 -0
  46. package/dist/context-compaction/types.js +19 -0
  47. package/dist/heartbeat/HealthMonitor.js +3 -2
  48. package/dist/index.js +38 -2
  49. package/dist/llm/llm-judgment-client.js +32 -30
  50. package/dist/llm/pi-ai.js +103 -16
  51. package/dist/llm/system-prompt/health.js +129 -0
  52. package/dist/llm/system-prompt/registry.js +246 -0
  53. package/dist/llm/system-prompt/strip-hibsml.js +51 -0
  54. package/dist/llm/tool-manifest/ask_user_input.js +35 -0
  55. package/dist/llm/tool-manifest/bash.js +23 -0
  56. package/dist/llm/tool-manifest/create_file.js +24 -0
  57. package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
  58. package/dist/llm/tool-manifest/image_search.js +24 -0
  59. package/dist/llm/tool-manifest/index.js +69 -0
  60. package/dist/llm/tool-manifest/mcp.js +43 -0
  61. package/dist/llm/tool-manifest/message_compose.js +31 -0
  62. package/dist/llm/tool-manifest/places.js +83 -0
  63. package/dist/llm/tool-manifest/present_files.js +21 -0
  64. package/dist/llm/tool-manifest/recipe.js +40 -0
  65. package/dist/llm/tool-manifest/recommend_apps.js +23 -0
  66. package/dist/llm/tool-manifest/str_replace.js +27 -0
  67. package/dist/llm/tool-manifest/types.js +7 -0
  68. package/dist/llm/tool-manifest/view.js +24 -0
  69. package/dist/llm/tool-manifest/weather.js +27 -0
  70. package/dist/llm/tool-manifest/web.js +51 -0
  71. package/dist/network/p2p-direct.js +23 -0
  72. package/dist/network/source-intent-broadcaster.js +203 -0
  73. package/dist/network/source-intent.js +100 -0
  74. package/dist/pi-ecosystem-judgment/adaptive-scan.js +48 -0
  75. package/dist/pi-ecosystem-judgment/injection-gate.js +54 -12
  76. package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
  77. package/dist/security/context-router-tool.js +1 -1
  78. package/dist/security/input-scanner.js +223 -0
  79. package/dist/web/client.js +3677 -4161
  80. package/dist/web/components/p2p/P2PModal.js +188 -0
  81. package/dist/web/components/p2p/index.js +264 -226
  82. package/dist/web/components/p2p/p2p-modal.js +657 -0
  83. package/dist/web/components/p2p/p2p-tools.js +248 -0
  84. package/dist/web/index.html +58 -67
  85. package/dist/web/server.js +299 -117
  86. package/dist/web/style.css +531 -249
  87. package/dist/web/ui/message-renderer.js +463 -0
  88. package/dist/web/ui/step-timeline.js +375 -0
  89. package/lefthook.yml +4 -0
  90. package/package.json +1 -1
  91. package/scripts/auto-evolve-loop.ts +105 -0
  92. package/scripts/build-web.ts +35 -1
  93. package/scripts/validate-system-prompt.ts +142 -0
  94. package/src/agents/p2p-chat-tools.ts +6 -6
  95. package/src/agents/permission-mode.ts +127 -0
  96. package/src/agents/pi-sdk.ts +461 -28
  97. package/src/agents/pre-tool-validator.ts +213 -0
  98. package/src/agents/workflow-pivot-loop.ts +110 -19
  99. package/src/bollharness/CLAUDE.md +1 -1
  100. package/src/bollharness/README.md +2 -2
  101. package/src/bollharness/README.zh-CN.md +2 -2
  102. package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
  103. package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
  104. package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
  105. package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
  106. package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
  107. package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
  108. package/src/bollharness/src/scripts/context_router.ts +1 -1
  109. package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
  110. package/src/bollharness/src/scripts/guard_router.ts +1 -1
  111. package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
  112. package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
  113. package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
  114. package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
  115. package/src/bollharness-integration/context-router.ts +2 -2
  116. package/src/bollharness-integration/guard-checker.ts +1 -1
  117. package/src/bootstrap/bootstrap.ts +3 -0
  118. package/src/bootstrap/context-collector.ts +30 -1
  119. package/src/bootstrap/context-hierarchy.ts +283 -0
  120. package/src/bootstrap/lifecycle-hooks.ts +139 -26
  121. package/src/bootstrap/project-context.ts +11 -3
  122. package/src/context-compaction/auto-compact.ts +153 -0
  123. package/src/context-compaction/budget-gate.ts +32 -0
  124. package/src/context-compaction/budget-reduce.ts +37 -0
  125. package/src/context-compaction/context-collapse.ts +72 -0
  126. package/src/context-compaction/index.ts +24 -0
  127. package/src/context-compaction/microcompact.ts +54 -0
  128. package/src/context-compaction/pipeline.ts +133 -0
  129. package/src/context-compaction/snip.ts +51 -0
  130. package/src/context-compaction/token-estimator.ts +36 -0
  131. package/src/context-compaction/types.ts +99 -0
  132. package/src/heartbeat/HealthMonitor.ts +3 -2
  133. package/src/index.ts +36 -2
  134. package/src/llm/llm-judgment-client.ts +36 -35
  135. package/src/llm/pi-ai.ts +111 -16
  136. package/src/llm/system-prompt/health.ts +159 -0
  137. package/src/llm/system-prompt/layers/channel/local.md +14 -0
  138. package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
  139. package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
  140. package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
  141. package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
  142. package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
  143. package/src/llm/system-prompt/layers/core/identity.md +37 -0
  144. package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
  145. package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
  146. package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
  147. package/src/llm/system-prompt/layers/core/refusal.md +37 -0
  148. package/src/llm/system-prompt/layers/core/tone.md +31 -0
  149. package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
  150. package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
  151. package/src/llm/system-prompt/layers/role/architect.md +20 -0
  152. package/src/llm/system-prompt/layers/role/expert.md +19 -0
  153. package/src/llm/system-prompt/layers/role/implementer.md +15 -0
  154. package/src/llm/system-prompt/layers/role/security.md +15 -0
  155. package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
  156. package/src/llm/system-prompt/layers/tool/bash.md +25 -0
  157. package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
  158. package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
  159. package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
  160. package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
  161. package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
  162. package/src/llm/system-prompt/registry.ts +325 -0
  163. package/src/llm/system-prompt/strip-hibsml.ts +52 -0
  164. package/src/llm/tool-manifest/ask_user_input.ts +37 -0
  165. package/src/llm/tool-manifest/bash.ts +25 -0
  166. package/src/llm/tool-manifest/create_file.ts +26 -0
  167. package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
  168. package/src/llm/tool-manifest/image_search.ts +26 -0
  169. package/src/llm/tool-manifest/index.ts +88 -0
  170. package/src/llm/tool-manifest/mcp.ts +46 -0
  171. package/src/llm/tool-manifest/message_compose.ts +33 -0
  172. package/src/llm/tool-manifest/places.ts +86 -0
  173. package/src/llm/tool-manifest/present_files.ts +23 -0
  174. package/src/llm/tool-manifest/recipe.ts +42 -0
  175. package/src/llm/tool-manifest/recommend_apps.ts +25 -0
  176. package/src/llm/tool-manifest/str_replace.ts +29 -0
  177. package/src/llm/tool-manifest/types.ts +52 -0
  178. package/src/llm/tool-manifest/view.ts +26 -0
  179. package/src/llm/tool-manifest/weather.ts +29 -0
  180. package/src/llm/tool-manifest/web.ts +54 -0
  181. package/src/network/p2p-direct.ts +22 -0
  182. package/src/network/source-intent-broadcaster.ts +242 -0
  183. package/src/network/source-intent.ts +167 -0
  184. package/src/security/context-router-tool.ts +1 -1
  185. package/src/security/input-scanner.ts +287 -0
  186. package/src/web/client.js +271 -2817
  187. package/src/web/client.ts +4326 -0
  188. package/src/web/index.html +58 -67
  189. package/src/web/server.ts +301 -141
  190. package/src/web/style.css +531 -249
  191. package/src/web/ui/message-renderer.ts +540 -0
  192. package/src/web/ui/step-timeline.ts +394 -0
  193. package/test-results/.last-run.json +6 -0
  194. package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
@@ -0,0 +1,394 @@
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
+ */
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // 类型
29
+ // ---------------------------------------------------------------------------
30
+ export type StepStatus = 'idle' | 'active' | 'done' | 'error';
31
+ export interface Step {
32
+ name: string;
33
+ status: StepStatus;
34
+ args?: Record<string, unknown>;
35
+ output?: string;
36
+ error?: string;
37
+ /** 该 step 对应的事件 type (供调试/扩展) */
38
+ eventType?: 'step_start' | 'step_done' | 'step_error';
39
+ }
40
+
41
+ const MAX_VISIBLE_NODES = 8;
42
+ const STORAGE_PREFIX = 'bolloon.stepTimeline.expanded.';
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // 模块私有状态 (按 timeline 元素实例化)
46
+ // 用 WeakMap 避免循环引用泄漏
47
+ // ---------------------------------------------------------------------------
48
+ interface TimelineState {
49
+ steps: Step[];
50
+ currentIndex: number;
51
+ expanded: boolean;
52
+ channelId: string | null;
53
+ showAll: boolean; // "+N 更多" 展开
54
+ }
55
+ const stateMap = new WeakMap<HTMLElement, TimelineState>();
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // 工具: 持久化
59
+ // ---------------------------------------------------------------------------
60
+ function readExpanded(channelId: string | null): boolean {
61
+ if (!channelId) return false;
62
+ try {
63
+ return localStorage.getItem(STORAGE_PREFIX + channelId) === '1';
64
+ } catch { return false; }
65
+ }
66
+ function writeExpanded(channelId: string | null, expanded: boolean): void {
67
+ if (!channelId) return;
68
+ try {
69
+ localStorage.setItem(STORAGE_PREFIX + channelId, expanded ? '1' : '0');
70
+ } catch { /* localStorage 写失败 (隐私模式), 静默 */ }
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // 工具: 摘要条 title
75
+ // ---------------------------------------------------------------------------
76
+ function computeTitle(steps: Step[]): string {
77
+ if (steps.length === 0) return '执行步骤';
78
+ // 优先 active, 其次最后一个 error
79
+ const activeIdx = steps.findIndex(s => s.status === 'active');
80
+ if (activeIdx >= 0) return `● 执行中 · ${steps[activeIdx].name}`;
81
+ const lastErrorIdx = (() => {
82
+ for (let i = steps.length - 1; i >= 0; i--) {
83
+ if (steps[i].status === 'error') return i;
84
+ }
85
+ return -1;
86
+ })();
87
+ if (lastErrorIdx >= 0) return `✗ 失败 · ${steps[lastErrorIdx].name}`;
88
+ return `✓ 已完成 · ${steps.length} 步`;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // 核心: 渲染 (摘要条 + 详情区, 共用 steps 状态)
93
+ // ---------------------------------------------------------------------------
94
+ function render(timelineEl: HTMLElement): void {
95
+ const state = stateMap.get(timelineEl);
96
+ if (!state) return;
97
+ const { steps, expanded, showAll } = state;
98
+
99
+ const titleEl = timelineEl.querySelector('[data-current-tool]') as HTMLElement | null;
100
+ if (titleEl) titleEl.textContent = computeTitle(steps);
101
+
102
+ // 摘要条 dots
103
+ const dotsEl = timelineEl.querySelector('[data-dots]') as HTMLElement | null;
104
+ if (dotsEl) {
105
+ // diff: 复用现有 dot, 多余 remove, 缺少 append
106
+ const existing = Array.from(dotsEl.children) as HTMLElement[];
107
+ for (let i = 0; i < steps.length; i++) {
108
+ const step = steps[i];
109
+ let dot = existing[i];
110
+ if (!dot) {
111
+ dot = document.createElement('span');
112
+ dot.className = 'step-dot';
113
+ dot.setAttribute('data-index', String(i));
114
+ dotsEl.appendChild(dot);
115
+ }
116
+ dot.setAttribute('data-status', step.status);
117
+ dot.setAttribute('title', `${step.name} · ${step.status}`);
118
+ }
119
+ while (dotsEl.children.length > steps.length) {
120
+ dotsEl.removeChild(dotsEl.lastChild!);
121
+ }
122
+ }
123
+
124
+ // 详情区 list
125
+ const listEl = timelineEl.querySelector('[data-list]') as HTMLElement | null;
126
+ if (listEl) {
127
+ const hiddenCount = Math.max(0, steps.length - MAX_VISIBLE_NODES);
128
+ const visibleSteps = showAll ? steps : steps.slice(-MAX_VISIBLE_NODES);
129
+ const existing = Array.from(listEl.children) as HTMLElement[];
130
+
131
+ let htmlIdx = 0;
132
+ // "+N 更多" 折叠行 (仅在 showAll=false 且有 hidden 时)
133
+ if (!showAll && hiddenCount > 0) {
134
+ let moreBtn = existing[htmlIdx] as HTMLElement | undefined;
135
+ if (!moreBtn || !moreBtn.classList.contains('step-timeline-more')) {
136
+ moreBtn = document.createElement('li');
137
+ moreBtn.className = 'step-timeline-more';
138
+ moreBtn.setAttribute('role', 'button');
139
+ moreBtn.textContent = `+ ${hiddenCount} 更多`;
140
+ moreBtn.onclick = () => {
141
+ state.showAll = true;
142
+ render(timelineEl);
143
+ };
144
+ if (existing[htmlIdx] && existing[htmlIdx] !== moreBtn) {
145
+ listEl.replaceChild(moreBtn, existing[htmlIdx]);
146
+ } else {
147
+ listEl.appendChild(moreBtn);
148
+ }
149
+ } else {
150
+ moreBtn.textContent = `+ ${hiddenCount} 更多`;
151
+ }
152
+ htmlIdx++;
153
+ }
154
+
155
+ for (let i = 0; i < visibleSteps.length; i++) {
156
+ const step = visibleSteps[i];
157
+ const realIndex = showAll ? i : (steps.length - visibleSteps.length + i);
158
+ let node = existing[htmlIdx];
159
+ if (!node || !node.classList.contains('step-timeline-node')) {
160
+ node = document.createElement('li');
161
+ node.className = 'step-timeline-node';
162
+ node.setAttribute('data-index', String(realIndex));
163
+ const marker = document.createElement('span');
164
+ marker.className = 'step-timeline-marker';
165
+ const label = document.createElement('span');
166
+ label.className = 'step-timeline-label';
167
+ const args = document.createElement('span');
168
+ args.className = 'step-timeline-args';
169
+ node.appendChild(marker);
170
+ node.appendChild(label);
171
+ node.appendChild(args);
172
+ if (existing[htmlIdx] && existing[htmlIdx] !== node) {
173
+ listEl.replaceChild(node, existing[htmlIdx]);
174
+ } else {
175
+ listEl.appendChild(node);
176
+ }
177
+ } else {
178
+ node.setAttribute('data-index', String(realIndex));
179
+ }
180
+ node.setAttribute('data-status', step.status);
181
+ const label = node.querySelector('.step-timeline-label') as HTMLElement;
182
+ if (label) label.textContent = step.name;
183
+ const argsEl = node.querySelector('.step-timeline-args') as HTMLElement;
184
+ if (argsEl) {
185
+ const argStr = step.args && Object.keys(step.args).length > 0
186
+ ? JSON.stringify(step.args).slice(0, 60)
187
+ : '';
188
+ argsEl.textContent = argStr;
189
+ argsEl.style.display = argStr ? '' : 'none';
190
+ }
191
+ htmlIdx++;
192
+ }
193
+
194
+ // 清理多余 li
195
+ while (listEl.children.length > htmlIdx) {
196
+ listEl.removeChild(listEl.lastChild!);
197
+ }
198
+ }
199
+
200
+ // 折叠态: 空状态显示占位, 有节点就保持
201
+ if (steps.length === 0) {
202
+ timelineEl.setAttribute('data-empty', 'true');
203
+ } else {
204
+ timelineEl.removeAttribute('data-empty');
205
+ }
206
+ // 折叠 class
207
+ const body = timelineEl.querySelector('[data-body]') as HTMLElement | null;
208
+ if (body) {
209
+ if (expanded) {
210
+ body.style.maxHeight = body.scrollHeight + 'px';
211
+ setTimeout(() => {
212
+ // 动画结束后清掉, 让内容自适应 (新 step 进来时能撑开)
213
+ if (state.expanded) body.style.maxHeight = '';
214
+ }, 300);
215
+ } else {
216
+ body.style.maxHeight = '0';
217
+ }
218
+ }
219
+ const arrow = timelineEl.querySelector('.step-timeline-arrow') as HTMLElement | null;
220
+ if (arrow) arrow.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0deg)';
221
+
222
+ // 自动滚到底 (新节点进来时, 详情区跟到底)
223
+ if (expanded && listEl) {
224
+ listEl.scrollTop = listEl.scrollHeight;
225
+ }
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // 公共: 创建一个空 timeline (AI 消息创建时挂的占位)
230
+ // ---------------------------------------------------------------------------
231
+ export function createEmptyStepTimeline(channelId: string | null = null): HTMLElement {
232
+ const root = document.createElement('div');
233
+ root.className = 'step-timeline';
234
+ root.setAttribute('data-step-timeline', '');
235
+ root.setAttribute('data-empty', 'true');
236
+
237
+ // 摘要条
238
+ const summary = document.createElement('div');
239
+ summary.className = 'step-timeline-summary';
240
+ summary.setAttribute('data-summary', '');
241
+ const titleSpan = document.createElement('span');
242
+ titleSpan.className = 'step-timeline-title';
243
+ const titleContent = document.createElement('span');
244
+ titleContent.setAttribute('data-current-tool', '');
245
+ titleContent.textContent = '执行步骤';
246
+ titleSpan.appendChild(titleContent);
247
+ const dots = document.createElement('div');
248
+ dots.className = 'step-timeline-dots';
249
+ dots.setAttribute('data-dots', '');
250
+ const arrow = document.createElement('span');
251
+ arrow.className = 'step-timeline-arrow';
252
+ arrow.textContent = '▾';
253
+ summary.appendChild(titleSpan);
254
+ summary.appendChild(dots);
255
+ summary.appendChild(arrow);
256
+ summary.onclick = () => {
257
+ const state = stateMap.get(root);
258
+ if (!state) return;
259
+ state.expanded = !state.expanded;
260
+ writeExpanded(state.channelId, state.expanded);
261
+ render(root);
262
+ };
263
+
264
+ // 详情区
265
+ const body = document.createElement('div');
266
+ body.className = 'step-timeline-body';
267
+ body.setAttribute('data-body', '');
268
+ const list = document.createElement('ul');
269
+ list.className = 'step-timeline-list';
270
+ list.setAttribute('data-list', '');
271
+ body.appendChild(list);
272
+
273
+ root.appendChild(summary);
274
+ root.appendChild(body);
275
+
276
+ // 初始折叠态
277
+ const initialExpanded = readExpanded(channelId);
278
+ stateMap.set(root, {
279
+ steps: [],
280
+ currentIndex: -1,
281
+ expanded: initialExpanded,
282
+ channelId: channelId || null,
283
+ showAll: false,
284
+ });
285
+ render(root);
286
+ return root;
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // 公共: 把 timeline 挂到 message 上 (初始化或补挂)
291
+ // ---------------------------------------------------------------------------
292
+ export function mountStepTimeline(messageEl: HTMLElement, channelId: string | null): HTMLElement {
293
+ // 已经挂过 → 复用
294
+ const existing = messageEl.querySelector('[data-step-timeline]') as HTMLElement | null;
295
+ if (existing) {
296
+ const state = stateMap.get(existing);
297
+ if (state && channelId && !state.channelId) state.channelId = channelId;
298
+ return existing;
299
+ }
300
+ // 找插入点: 在 .bubble 之后, .message-actions 之前
301
+ const bubble = messageEl.querySelector('.bubble');
302
+ const actions = messageEl.querySelector('.message-actions');
303
+ const timeline = createEmptyStepTimeline(channelId);
304
+ if (actions && actions.parentNode === messageEl) {
305
+ messageEl.insertBefore(timeline, actions);
306
+ } else if (bubble && bubble.parentNode === messageEl) {
307
+ bubble.parentNode.insertBefore(timeline, bubble.nextSibling);
308
+ } else {
309
+ messageEl.appendChild(timeline);
310
+ }
311
+ return timeline;
312
+ }
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // 公共: 推一个 step 事件 (start/done/error)
316
+ // ---------------------------------------------------------------------------
317
+ export function pushStepToTimeline(
318
+ timelineEl: HTMLElement,
319
+ eventType: 'step_start' | 'step_done' | 'step_error',
320
+ data: { tool: string; args?: Record<string, unknown>; success?: boolean; output?: string; error?: string }
321
+ ): void {
322
+ const state = stateMap.get(timelineEl);
323
+ if (!state) return;
324
+ const { steps } = state;
325
+
326
+ if (eventType === 'step_start') {
327
+ state.currentIndex = steps.length;
328
+ steps.push({
329
+ name: data.tool,
330
+ status: 'active',
331
+ args: data.args,
332
+ eventType,
333
+ });
334
+ } else {
335
+ // step_done / step_error — 找当前 active 节点关闭
336
+ // 优先匹配 currentIndex, 其次回退到最后一个 active
337
+ let targetIdx = state.currentIndex;
338
+ if (targetIdx < 0 || !steps[targetIdx] || steps[targetIdx].name !== data.tool) {
339
+ // 兜底: 找最后一个同名 active
340
+ for (let i = steps.length - 1; i >= 0; i--) {
341
+ if (steps[i].name === data.tool && steps[i].status === 'active') {
342
+ targetIdx = i;
343
+ break;
344
+ }
345
+ }
346
+ }
347
+ if (targetIdx < 0) {
348
+ // 没有 active 节点匹配, 当作新 error 节点
349
+ targetIdx = steps.length;
350
+ steps.push({ name: data.tool, status: 'error', eventType });
351
+ }
352
+ if (eventType === 'step_done') {
353
+ steps[targetIdx].status = data.success === false ? 'error' : 'done';
354
+ if (data.output) steps[targetIdx].output = data.output;
355
+ if (data.error) steps[targetIdx].error = data.error;
356
+ } else {
357
+ steps[targetIdx].status = 'error';
358
+ if (data.error) steps[targetIdx].error = data.error;
359
+ }
360
+ steps[targetIdx].eventType = eventType;
361
+ state.currentIndex = -1;
362
+ }
363
+ render(timelineEl);
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // 公共: finalize 时把 timeline 元素从 from 搬到 to (避免重渲)
368
+ // 接受 timelineEl 本身或 fromEl 内部的 timeline
369
+ // ---------------------------------------------------------------------------
370
+ export function migrateStepTimeline(fromEl: HTMLElement, toEl: HTMLElement): void {
371
+ const fromTimeline = fromEl.querySelector('[data-step-timeline]') as HTMLElement | null;
372
+ const toTimeline = toEl.querySelector('[data-step-timeline]') as HTMLElement | null;
373
+ if (!fromTimeline) return;
374
+ // 移除 toEl 的占位, 搬 fromEl 的过来
375
+ if (toTimeline && toTimeline !== fromTimeline) {
376
+ toTimeline.remove();
377
+ }
378
+ if (toTimeline !== fromTimeline) {
379
+ toEl.appendChild(fromTimeline);
380
+ }
381
+ // 重渲一次 (channelId 注入)
382
+ const state = stateMap.get(fromTimeline);
383
+ if (state) {
384
+ render(fromTimeline);
385
+ }
386
+ }
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // 公共: 拿到一个 message 内的 timeline (无则返回 null)
390
+ // ---------------------------------------------------------------------------
391
+ export function getStepTimeline(messageEl: HTMLElement | null): HTMLElement | null {
392
+ if (!messageEl) return null;
393
+ return messageEl.querySelector('[data-step-timeline]') as HTMLElement | null;
394
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "status": "failed",
3
+ "failedTests": [
4
+ "5a05f24c2683c4708372-34e7e529a0f74bad57a0"
5
+ ]
6
+ }
@@ -0,0 +1,285 @@
1
+ # Instructions
2
+
3
+ - Following Playwright test failed.
4
+ - Explain why, be concise, respect Playwright best practices.
5
+ - Provide a snippet of code with the fix, if possible.
6
+
7
+ # Test info
8
+
9
+ - Name: src/test/web-loop-status-bar.spec.ts >> loop-status-bar 三态机 >> 1. loading 态: status bar 显示 spinner, 不显示循环计数
10
+ - Location: src/test/web-loop-status-bar.spec.ts:149:3
11
+
12
+ # Error details
13
+
14
+ ```
15
+ Error: expect(locator).toBeVisible() failed
16
+
17
+ Locator: locator('#loop-status-bar')
18
+ Expected: visible
19
+ Received: hidden
20
+ Timeout: 5000ms
21
+
22
+ Call log:
23
+ - Expect "toBeVisible" with timeout 5000ms
24
+ - waiting for locator('#loop-status-bar')
25
+ 14 × locator resolved to <div hidden="" id="loop-status-bar" data-state="loading" class="loop-status-bar">…</div>
26
+ - unexpected value "hidden"
27
+
28
+ ```
29
+
30
+ ```yaml
31
+ - complementary:
32
+ - button "收起侧边栏":
33
+ - img
34
+ - text: ◈ Bolloon 智能体
35
+ - button "新建智能体":
36
+ - img
37
+ - text: 新建智能体
38
+ - list:
39
+ - listitem:
40
+ - img
41
+ - text: 💬 Loop Bar Channel
42
+ - button "配置智能体 (钱包 / 工具)":
43
+ - img
44
+ - button "×"
45
+ - list
46
+ - text: ▼ P2P 好友
47
+ - button "⊞ 展开"
48
+ - button "我的 ID"
49
+ - button "+ 好友"
50
+ - list:
51
+ - listitem: (暂无好友, 点 + 添加)
52
+ - text: 已连接
53
+ - main:
54
+ - heading "Loop Bar Channel" [level=1]
55
+ - button "切换主题":
56
+ - img
57
+ - button "API 配置":
58
+ - img
59
+ - button "钱包管理":
60
+ - img
61
+ - button "我的判断":
62
+ - img
63
+ - text: 你好!我是 Bolloon Agent。有什么我可以帮你的吗? 执行步骤 ▾
64
+ - list
65
+ - button "复制":
66
+ - img
67
+ - text: 复制
68
+ - button "蒸馏为判断":
69
+ - img
70
+ - text: 蒸馏为判断
71
+ - button "重新回答":
72
+ - img
73
+ - text: 重新回答
74
+ - text: 16:53
75
+ - textbox "输入消息..."
76
+ - button "发送 (Enter)":
77
+ - img
78
+ ```
79
+
80
+ # Test source
81
+
82
+ ```ts
83
+ 69 | const safePath = pathname === '/' ? '/index.html' : pathname;
84
+ 70 | const filePath = path.join(WEB_ROOT, safePath);
85
+ 71 | if (!filePath.startsWith(WEB_ROOT)) {
86
+ 72 | res.writeHead(403);
87
+ 73 | res.end('Forbidden');
88
+ 74 | return;
89
+ 75 | }
90
+ 76 | try {
91
+ 77 | const body = await readFile(filePath);
92
+ 78 | const ext = path.extname(filePath);
93
+ 79 | res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'application/octet-stream' });
94
+ 80 | res.end(body);
95
+ 81 | } catch {
96
+ 82 | res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
97
+ 83 | res.end('Not found');
98
+ 84 | }
99
+ 85 | }
100
+ 86 |
101
+ 87 | type MockServer = { server: ReturnType<typeof createServer>; clients: Set<SseClient>; port: number };
102
+ 88 |
103
+ 89 | function startMockServer(extraSetup?: (clients: Set<SseClient>) => void): Promise<MockServer> {
104
+ 90 | const clients = new Set<SseClient>();
105
+ 91 | const server = createServer(async (req, res) => {
106
+ 92 | const url = new URL(req.url || '/', `http://localhost`);
107
+ 93 | const pathname = url.pathname;
108
+ 94 |
109
+ 95 | if (pathname === '/events' || pathname.startsWith('/events')) {
110
+ 96 | const channelId = url.searchParams.get('channelId') || CHANNEL_ID;
111
+ 97 | sse(res);
112
+ 98 | const client: SseClient = { channelId, res };
113
+ 99 | clients.add(client);
114
+ 100 | req.on('close', () => clients.delete(client));
115
+ 101 | setTimeout(() => writeEvent(client, { type: 'ping' }), 100);
116
+ 102 | return;
117
+ 103 | }
118
+ 104 |
119
+ 105 | if (pathname === '/channels') return json(res, [defaultChannel]);
120
+ 106 | if (pathname === `/channels/${CHANNEL_ID}/sessions`) {
121
+ 107 | return json(res, { session: { id: 'sess-1' }, currentSessionId: 'sess-1' });
122
+ 108 | }
123
+ 109 | if (pathname === '/theme') return json(res, { theme: 'dark', agentId: 'agent_loop' });
124
+ 110 | if (pathname === '/api/remote-channels') return json(res, { peers: [] });
125
+ 111 | if (pathname.startsWith('/sessions/')) {
126
+ 112 | res.writeHead(200, { 'Content-Type': 'application/json' });
127
+ 113 | return res.end(JSON.stringify({ ok: true }));
128
+ 114 | }
129
+ 115 | if (pathname === '/api/loop/inspect') {
130
+ 116 | return json(res, {
131
+ 117 | summary: 'loop 完成 (mock): 跑了 3 个步骤',
132
+ 118 | steps: [
133
+ 119 | { name: 'shell_exec: echo 1', status: 'ok', durationMs: 320, output: '1' },
134
+ 120 | { name: 'shell_exec: echo 2', status: 'ok', durationMs: 280, output: '2' },
135
+ 121 | { name: 'shell_exec: echo 3', status: 'ok', durationMs: 250, output: '3' },
136
+ 122 | ],
137
+ 123 | finalReply: '已完成 3 步',
138
+ 124 | tokens: { input: 1200, output: 80 },
139
+ 125 | });
140
+ 126 | }
141
+ 127 | if (pathname === '/message' && req.method === 'POST') {
142
+ 128 | let body = '';
143
+ 129 | req.on('data', (chunk) => { body += chunk; });
144
+ 130 | req.on('end', () => json(res, { ok: true, reply: 'mock' }));
145
+ 131 | return;
146
+ 132 | }
147
+ 133 |
148
+ 134 | return serveStatic(res, pathname);
149
+ 135 | });
150
+ 136 |
151
+ 137 | if (extraSetup) extraSetup(clients);
152
+ 138 |
153
+ 139 | return new Promise<MockServer>((resolve) => {
154
+ 140 | server.listen(0, '127.0.0.1', () => {
155
+ 141 | const addr = server.address();
156
+ 142 | const port = typeof addr === 'object' && addr ? addr.port : 0;
157
+ 143 | resolve({ server, clients, port });
158
+ 144 | });
159
+ 145 | });
160
+ 146 | }
161
+ 147 |
162
+ 148 | test.describe.serial('loop-status-bar 三态机', () => {
163
+ 149 | test('1. loading 态: status bar 显示 spinner, 不显示循环计数', async () => {
164
+ 150 | const { server, port } = await startMockServer((clients) => {
165
+ 151 | setTimeout(() => {
166
+ 152 | const ch = [...clients].find((c) => c.channelId === CHANNEL_ID);
167
+ 153 | if (!ch) return;
168
+ 154 | writeEvent(ch, { channelId: CHANNEL_ID, type: 'status', tool: 'system', content: '🔄 开始 ReAct 循环...' });
169
+ 155 | }, 500);
170
+ 156 | });
171
+ 157 | try {
172
+ 158 | const { chromium } = await import('@playwright/test');
173
+ 159 | const ctx = await chromium.launch();
174
+ 160 | const page = await ctx.newPage();
175
+ 161 | page.on('console', (msg) => {
176
+ 162 | if (msg.type() === 'error') console.log('[test1 console err]', msg.text());
177
+ 163 | });
178
+ 164 | await page.goto(`http://127.0.0.1:${port}/`);
179
+ 165 | await page.waitForSelector('#input', { timeout: 5000 }).catch(() => {});
180
+ 166 | await page.waitForTimeout(2500);
181
+ 167 |
182
+ 168 | const bar = page.locator('#loop-status-bar');
183
+ > 169 | await expect(bar).toBeVisible();
184
+ | ^ Error: expect(locator).toBeVisible() failed
185
+ 170 | await expect(bar).toHaveAttribute('data-state', 'loading');
186
+ 171 | const text = await bar.textContent();
187
+ 172 | expect(text).not.toMatch(/\d+\/\d+/);
188
+ 173 | await ctx.close();
189
+ 174 | } finally {
190
+ 175 | server.close();
191
+ 176 | }
192
+ 177 | });
193
+ 178 |
194
+ 179 | test('2. retrying 态: 自动重试徽章出现, 无重试按钮', async () => {
195
+ 180 | const { server, port } = await startMockServer((clients) => {
196
+ 181 | setTimeout(() => {
197
+ 182 | const ch = [...clients].find((c) => c.channelId === CHANNEL_ID);
198
+ 183 | if (!ch) return;
199
+ 184 | writeEvent(ch, { channelId: CHANNEL_ID, type: 'status', tool: 'system', content: '↻ 自动重试 loop 1/3 (1s 后)' });
200
+ 185 | }, 800);
201
+ 186 | });
202
+ 187 | try {
203
+ 188 | const { chromium } = await import('@playwright/test');
204
+ 189 | const ctx = await chromium.launch();
205
+ 190 | const page = await ctx.newPage();
206
+ 191 | page.on('console', (msg) => {
207
+ 192 | if (msg.type() === 'error') console.log('[test2 console err]', msg.text());
208
+ 193 | });
209
+ 194 | await page.goto(`http://127.0.0.1:${port}/`);
210
+ 195 | await page.waitForSelector('#input', { timeout: 5000 }).catch(() => {});
211
+ 196 | await page.waitForTimeout(2500);
212
+ 197 |
213
+ 198 | const bar = page.locator('#loop-status-bar');
214
+ 199 | await expect(bar).toHaveAttribute('data-state', 'retrying');
215
+ 200 |
216
+ 201 | const retry = page.locator('#loop-status-retry');
217
+ 202 | await expect(retry).toBeVisible();
218
+ 203 | const retryText = await retry.textContent();
219
+ 204 | expect(retryText).toMatch(/自动重试\s+1\/3/);
220
+ 205 |
221
+ 206 | const tagName = await retry.evaluate((el) => el.tagName.toLowerCase());
222
+ 207 | expect(tagName).toBe('span');
223
+ 208 | await ctx.close();
224
+ 209 | } finally {
225
+ 210 | server.close();
226
+ 211 | }
227
+ 212 | });
228
+ 213 |
229
+ 214 | test('3. done 态: 检查按钮出现, 重试徽章隐藏', async () => {
230
+ 215 | const { server, port } = await startMockServer((clients) => {
231
+ 216 | setTimeout(() => {
232
+ 217 | const ch = [...clients].find((c) => c.channelId === CHANNEL_ID);
233
+ 218 | if (!ch) return;
234
+ 219 | writeEvent(ch, { channelId: CHANNEL_ID, type: 'status', tool: 'system', content: '🔄 循环中' });
235
+ 220 | setTimeout(() => {
236
+ 221 | writeEvent(ch, { channelId: CHANNEL_ID, type: 'done', content: '' });
237
+ 222 | }, 600);
238
+ 223 | }, 800);
239
+ 224 | });
240
+ 225 | try {
241
+ 226 | const { chromium } = await import('@playwright/test');
242
+ 227 | const ctx = await chromium.launch();
243
+ 228 | const page = await ctx.newPage();
244
+ 229 | page.on('console', (msg) => {
245
+ 230 | if (msg.type() === 'error') console.log('[test3 console err]', msg.text());
246
+ 231 | });
247
+ 232 | await page.goto(`http://127.0.0.1:${port}/`);
248
+ 233 | await page.waitForSelector('#input', { timeout: 5000 }).catch(() => {});
249
+ 234 | await page.waitForTimeout(2500);
250
+ 235 |
251
+ 236 | const bar = page.locator('#loop-status-bar');
252
+ 237 | await expect(bar).toHaveAttribute('data-state', 'done');
253
+ 238 |
254
+ 239 | const checkBtn = page.locator('#loop-status-check');
255
+ 240 | await expect(checkBtn).toBeVisible();
256
+ 241 | await expect(checkBtn).toHaveText(/检查/);
257
+ 242 |
258
+ 243 | const retry = page.locator('#loop-status-retry');
259
+ 244 | await expect(retry).toBeHidden();
260
+ 245 | await ctx.close();
261
+ 246 | } finally {
262
+ 247 | server.close();
263
+ 248 | }
264
+ 249 | });
265
+ 250 |
266
+ 251 | test('4. 点检查按钮 → 弹 modal 列出 step 输出', async () => {
267
+ 252 | const { server, port } = await startMockServer((clients) => {
268
+ 253 | setTimeout(() => {
269
+ 254 | const ch = [...clients].find((c) => c.channelId === CHANNEL_ID);
270
+ 255 | if (!ch) return;
271
+ 256 | writeEvent(ch, { channelId: CHANNEL_ID, type: 'status', tool: 'system', content: '🔄 循环中' });
272
+ 257 | setTimeout(() => {
273
+ 258 | writeEvent(ch, { channelId: CHANNEL_ID, type: 'done', content: '' });
274
+ 259 | }, 600);
275
+ 260 | }, 800);
276
+ 261 | });
277
+ 262 | try {
278
+ 263 | const { chromium } = await import('@playwright/test');
279
+ 264 | const ctx = await chromium.launch();
280
+ 265 | const page = await ctx.newPage();
281
+ 266 | page.on('console', (msg) => {
282
+ 267 | if (msg.type() === 'error') console.log('[test4 console err]', msg.text());
283
+ 268 | });
284
+ 269 | await page.goto(`http://127.0.0.1:${port}/`);
285
+ ```