@tea-agent/loop-agent 0.13.0 → 0.15.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.
Files changed (272) hide show
  1. package/AGENTS.md +157 -157
  2. package/CHANGELOG.md +116 -305
  3. package/README.md +357 -334
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/cursor-prompt.js +6 -6
  7. package/dist/commands/init.js +505 -505
  8. package/dist/commands/loop-benchmark.js +11 -11
  9. package/dist/commands/pi-reuse-benchmark.js +16 -16
  10. package/dist/executors/pi-event-serializer.js +33 -11
  11. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  12. package/dist/task/runtime.js +27 -27
  13. package/dist/worker/observe/spec-evidence.js +19 -10
  14. package/dist/worker/observe/static/api.js +46 -46
  15. package/dist/worker/observe/static/app.js +151 -150
  16. package/dist/worker/observe/static/constants.js +156 -148
  17. package/dist/worker/observe/static/copy.js +67 -67
  18. package/dist/worker/observe/static/dag-helpers.js +201 -172
  19. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  20. package/dist/worker/observe/static/dag-layout.js +83 -83
  21. package/dist/worker/observe/static/dag-model.js +72 -72
  22. package/dist/worker/observe/static/dom.js +122 -122
  23. package/dist/worker/observe/static/format-pool.d.ts +71 -0
  24. package/dist/worker/observe/static/format-pool.js +134 -67
  25. package/dist/worker/observe/static/format.js +317 -292
  26. package/dist/worker/observe/static/index.html +350 -308
  27. package/dist/worker/observe/static/kpi.js +100 -94
  28. package/dist/worker/observe/static/markdown-render.js +124 -0
  29. package/dist/worker/observe/static/relations.js +133 -133
  30. package/dist/worker/observe/static/router.js +93 -93
  31. package/dist/worker/observe/static/run-processing.js +148 -148
  32. package/dist/worker/observe/static/shell-chrome.js +74 -68
  33. package/dist/worker/observe/static/state.js +273 -267
  34. package/dist/worker/observe/static/styles.css +2504 -1902
  35. package/dist/worker/observe/static/views/batch.js +227 -227
  36. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  37. package/dist/worker/observe/static/views/dag-inspector.js +530 -627
  38. package/dist/worker/observe/static/views/dag.js +371 -371
  39. package/dist/worker/observe/static/views/dashboard.js +86 -100
  40. package/dist/worker/observe/static/views/failures.js +143 -143
  41. package/dist/worker/observe/static/views/feature.js +492 -492
  42. package/dist/worker/observe/static/views/pool.js +708 -350
  43. package/dist/worker/observe/static/views/run.js +453 -453
  44. package/dist/worker/observe/static/views/session-timeline.js +771 -219
  45. package/dist/worker/observe/static/views/shell.js +7 -7
  46. package/dist/worker/observe/static/views/task.js +314 -314
  47. package/dist/worker/observe/static/views/timeline.js +163 -163
  48. package/dist/workflows/dag/canvas-observer.js +275 -275
  49. package/dist/workflows/dag/init-hybrid.js +27 -11
  50. package/docs/README.md +106 -104
  51. package/docs/architecture/README.md +26 -26
  52. package/docs/architecture/dag-execution.md +140 -140
  53. package/docs/architecture/evolution.md +54 -54
  54. package/docs/architecture/facts-and-state.md +71 -71
  55. package/docs/architecture/runtime-boundaries.md +191 -191
  56. package/docs/architecture/system-overview.md +93 -93
  57. package/docs/architecture/worker-and-feature.md +85 -85
  58. package/docs/harness-methodology-debugging.md +153 -153
  59. package/docs/harness-methodology-tdd.md +130 -130
  60. package/docs/harness-methodology-verification.md +27 -27
  61. package/docs/init-surface.manifest.json +304 -307
  62. package/docs/skills/README.md +7 -7
  63. package/docs/skills/vetted-skill-registry.md +29 -29
  64. package/docs/templates/adr.md +60 -60
  65. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  66. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  67. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  68. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  69. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  70. package/docs/templates/agent-dag-report.schema.json +473 -473
  71. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  72. package/docs/templates/agent-dag.base.json +190 -190
  73. package/docs/templates/agent-dag.final-verification.json +185 -185
  74. package/docs/templates/agent-dag.schema.json +411 -411
  75. package/docs/templates/agent-dag.supervised-implementation.json +620 -620
  76. package/docs/templates/backend-test-analysis.schema.json +44 -44
  77. package/docs/templates/backend-test-case-manifest.schema.json +190 -190
  78. package/docs/templates/backend-test-dag.classify.prompt.md +75 -75
  79. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +204 -204
  80. package/docs/templates/backend-test-dag.json +559 -559
  81. package/docs/templates/backend-test-dag.retrospect.prompt.md +139 -139
  82. package/docs/templates/backend-test-dag.review-cases.prompt.md +83 -83
  83. package/docs/templates/backend-test-execution.schema.json +133 -133
  84. package/docs/templates/backend-test-result.schema.json +99 -99
  85. package/docs/templates/branch-merge-report.md +0 -1
  86. package/docs/templates/exec-plan.md +64 -64
  87. package/docs/templates/feature-spec.md +53 -53
  88. package/docs/templates/frontend-design-contract.md +42 -42
  89. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  90. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  91. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  92. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  93. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  94. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  95. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  96. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  97. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  98. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  99. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  100. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  101. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  102. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  103. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  104. package/docs/templates/frontend-eval/metrics.md +138 -138
  105. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  106. package/docs/templates/frontend-implementation-contract.schema.json +27 -27
  107. package/docs/templates/frontend-task-constraints.md +35 -35
  108. package/docs/templates/frontend-task-requirement.md +70 -70
  109. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -5
  110. package/docs/templates/frontend-test-dag.json +23 -23
  111. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -3
  112. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -3
  113. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -3
  114. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -3
  115. package/docs/templates/harness.schema.json +221 -221
  116. package/docs/templates/hybrid-dag.json +188 -188
  117. package/docs/templates/init-evolution-review.md +35 -35
  118. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  119. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  120. package/docs/templates/knowledge-sync-dag.json +178 -178
  121. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  122. package/docs/templates/product-line/AGENTS.md +8 -8
  123. package/docs/templates/product-line/README.md +9 -9
  124. package/docs/templates/product-line/acceptance.yaml +14 -14
  125. package/docs/templates/product-line/closeout.yaml +9 -9
  126. package/docs/templates/product-line/design.md +13 -13
  127. package/docs/templates/product-line/links.md +10 -10
  128. package/docs/templates/product-line/requirement.md +17 -17
  129. package/docs/templates/product-line/task-graph.yaml +15 -15
  130. package/docs/templates/product-line/task.yaml +64 -64
  131. package/docs/templates/product-line/test-plan.md +7 -7
  132. package/docs/templates/production-readiness-checklist.md +57 -57
  133. package/docs/templates/progress-log.md +17 -17
  134. package/docs/templates/project-start-checklist.md +9 -9
  135. package/docs/templates/qa-report.md +48 -48
  136. package/docs/templates/sprint-contract.md +29 -29
  137. package/docs/templates/worker-dogfood-evidence.md +80 -80
  138. package/docs/templates/worker-dogfood-setup.md +68 -68
  139. package/examples/decision-gate-agent-dag.json +173 -173
  140. package/examples/example-dag.json +46 -46
  141. package/examples/hybrid-loop-agent-dag.json +188 -188
  142. package/harness.json +66 -66
  143. package/package.json +78 -52
  144. package/scripts/kb-bootstrap-init-skeleton.sh +240 -240
  145. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  146. package/scripts/kb-graph-materialize.mjs +105 -105
  147. package/scripts/kb-graph-promote.mjs +164 -164
  148. package/scripts/kb-query.mjs +554 -554
  149. package/skills/agent-worker/SKILL.md +39 -39
  150. package/skills/agent-worker/references/agent-worker-operator.md +60 -60
  151. package/skills/ai-engineering-context/SKILL.md +48 -48
  152. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  153. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  154. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  155. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  156. package/skills/analyze-product-dependencies/references/example.md +76 -76
  157. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  158. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  159. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  160. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  161. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  162. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  163. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  164. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  165. package/skills/analyze-product-requirements/SKILL.md +90 -90
  166. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  167. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  168. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  169. package/skills/analyze-product-requirements/references/example.md +86 -86
  170. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  171. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  172. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  173. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  174. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  175. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  176. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  177. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  178. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  179. package/skills/browser-tools/SKILL.md +196 -196
  180. package/skills/browser-tools/browser-content.js +103 -103
  181. package/skills/browser-tools/browser-cookies.js +35 -35
  182. package/skills/browser-tools/browser-eval.js +53 -53
  183. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  184. package/skills/browser-tools/browser-nav.js +44 -44
  185. package/skills/browser-tools/browser-pick.js +162 -162
  186. package/skills/browser-tools/browser-screenshot.js +34 -34
  187. package/skills/browser-tools/browser-start.js +86 -86
  188. package/skills/browser-tools/package-lock.json +2556 -2556
  189. package/skills/browser-tools/package.json +19 -19
  190. package/skills/code-review-core/SKILL.md +20 -20
  191. package/skills/codebase-scout/SKILL.md +19 -19
  192. package/skills/frontend-design-review/SKILL.md +66 -66
  193. package/skills/frontend-design-review/references/review-checklist.md +40 -58
  194. package/skills/frontend-implementation/SKILL.md +49 -49
  195. package/skills/frontend-implementation/references/code-standards.md +32 -32
  196. package/skills/frontend-implementation/references/design-spec.md +46 -46
  197. package/skills/frontend-implementation/references/node-contracts.md +27 -27
  198. package/skills/frontend-review/SKILL.md +61 -59
  199. package/skills/frontend-review/references/review-findings.md +48 -47
  200. package/skills/frontend-verification/SKILL.md +55 -53
  201. package/skills/frontend-verification/references/verification-checklist.md +59 -68
  202. package/skills/grill-me/SKILL.md +10 -10
  203. package/skills/grill-with-docs/SKILL.md +88 -88
  204. package/skills/grill-with-docs/adr-format.md +47 -47
  205. package/skills/grill-with-docs/context-format.md +60 -60
  206. package/skills/init-capability-evolution/SKILL.md +70 -70
  207. package/skills/loop-agent/SKILL.md +151 -151
  208. package/skills/loop-agent/references/README.md +67 -67
  209. package/skills/loop-agent/references/command-reference.md +527 -527
  210. package/skills/loop-agent/references/docs-converge.md +126 -126
  211. package/skills/loop-agent/references/harness-policy.md +263 -263
  212. package/skills/loop-agent/references/hybrid-dag.md +243 -243
  213. package/skills/loop-agent/references/learned/README.md +21 -21
  214. package/skills/loop-agent/references/long-running-loop.md +57 -57
  215. package/skills/loop-agent/references/model-routing.md +36 -36
  216. package/skills/loop-agent/references/multi-worktree.md +54 -54
  217. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  218. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  219. package/skills/loop-agent/references/pi-prompt.md +23 -23
  220. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  221. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  222. package/skills/loop-agent/references/task-workflow.md +89 -89
  223. package/skills/loop-agent/references/verification-and-failure-handling.md +141 -141
  224. package/skills/playwright-cli/SKILL.md +420 -420
  225. package/skills/playwright-cli/references/element-attributes.md +23 -23
  226. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  227. package/skills/playwright-cli/references/request-mocking.md +87 -87
  228. package/skills/playwright-cli/references/running-code.md +241 -241
  229. package/skills/playwright-cli/references/session-management.md +225 -225
  230. package/skills/playwright-cli/references/storage-state.md +275 -275
  231. package/skills/playwright-cli/references/test-generation.md +433 -433
  232. package/skills/playwright-cli/references/tracing.md +139 -139
  233. package/skills/playwright-cli/references/video-recording.md +143 -143
  234. package/skills/playwright-cli-case-generator/SKILL.md +74 -74
  235. package/skills/requesting-code-review/SKILL.md +101 -101
  236. package/skills/requesting-code-review/code-reviewer.md +168 -168
  237. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  238. package/skills/systematic-debugging/SKILL.md +296 -296
  239. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  240. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  241. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  242. package/skills/systematic-debugging/find-polluter.sh +63 -63
  243. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  244. package/skills/systematic-debugging/test-academic.md +14 -14
  245. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  246. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  247. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  248. package/skills/test-driven-development/SKILL.md +20 -20
  249. package/skills/using-git-worktrees/SKILL.md +215 -215
  250. package/skills/verification-before-completion/SKILL.md +154 -154
  251. package/skills/webapp-testing/SKILL.md +19 -19
  252. package/docs/agent-dag-recovery-playbook.md +0 -195
  253. package/docs/agent-dag-runner.md +0 -67
  254. package/docs/cursor-prompt-sidecar.md +0 -36
  255. package/docs/decisions/README.md +0 -18
  256. package/docs/design/README.md +0 -167
  257. package/docs/development-principles.md +0 -73
  258. package/docs/exec-plans/README.md +0 -6
  259. package/docs/exec-plans/active/README.md +0 -12
  260. package/docs/exec-plans/completed/README.md +0 -107
  261. package/docs/feature-workflow.md +0 -414
  262. package/docs/loop-agent-harness.md +0 -142
  263. package/docs/production-readiness.md +0 -96
  264. package/docs/progress/README.md +0 -80
  265. package/docs/reports/README.md +0 -159
  266. package/docs/verification-matrix.md +0 -70
  267. package/scripts/check-product-line-docs.sh +0 -29
  268. package/scripts/check-task-pool-root.sh +0 -32
  269. package/scripts/kb-graph-incremental-prepare.sh +0 -5
  270. package/scripts/kb-graph-materialize.sh +0 -4
  271. package/scripts/kb-graph-promote.sh +0 -4
  272. package/scripts/kb-query.sh +0 -5
@@ -1,219 +1,771 @@
1
- /** Session event timeline helpers (Observe UI R5). */
2
- import { UI_TEXT } from "../constants.js";
3
- import { el } from "../dom.js";
4
- import { formatTs, truncateText } from "../format.js";
5
- import { fetchJson } from "../api.js";
6
- import {
7
- uiState,
8
- updateDagTimelineViewportState,
9
- computeFollowLatest,
10
- } from "../state.js";
11
-
12
- export function extractAssistantSummary(event) {
13
- if (!event || typeof event !== "object") return "—";
14
- const record = event;
15
- if (record.message && typeof record.message === "object") {
16
- const content = record.message.content;
17
- if (Array.isArray(content)) {
18
- return (
19
- content
20
- .filter(
21
- (part) => part?.type === "text" && typeof part.text === "string",
22
- )
23
- .map((part) => part.text)
24
- .join(" ") || "—"
25
- );
26
- }
27
- if (typeof record.message.content === "string") {
28
- return record.message.content;
29
- }
30
- }
31
- if (typeof record.text === "string") return record.text;
32
- if (typeof record.outputPreview === "string") return record.outputPreview;
33
- return "—";
34
- }
35
-
36
- export function formatSessionEventLabel(event) {
37
- if (!event || typeof event !== "object") return "未知类型";
38
- const type = typeof event.type === "string" ? event.type : "unknown";
39
- const toolName = event.toolName ?? event.name ?? "—";
40
- if (type === "agent_start") return "Agent 已启动";
41
- if (type === "agent_end") return "Agent 已结束";
42
- if (type === "agent_settled") return "Agent 已稳定";
43
- if (type === "turn_start") return "开始新一轮处理";
44
- if (type === "tool_execution_start" || type === "tool_start") {
45
- return `调用工具 · ${toolName}`;
46
- }
47
- if (type === "tool_execution_end" || type === "tool_end") {
48
- return `工具调用完成 · ${toolName}`;
49
- }
50
- if (
51
- type === "turn_end" ||
52
- type === "assistant" ||
53
- type === "assistant_message"
54
- ) {
55
- return `AI 回复摘要 · ${truncateText(extractAssistantSummary(event), 120)}`;
56
- }
57
- return type;
58
- }
59
-
60
- export function sessionEventTimestamp(event) {
61
- if (!event || typeof event !== "object") return "";
62
- const ts =
63
- (typeof event.timestamp === "string" && event.timestamp) ||
64
- (typeof event.at === "string" && event.at) ||
65
- (typeof event.recordedAt === "string" && event.recordedAt);
66
- return ts || "";
67
- }
68
-
69
- export function formatSessionEventTime(event) {
70
- const ts = sessionEventTimestamp(event);
71
- if (!ts) return "未记录时间";
72
- return formatTs(ts).replace(/\s.*/, "") || formatTs(ts);
73
- }
74
-
75
- export function sessionEventKind(event) {
76
- const type = typeof event?.type === "string" ? event.type : "unknown";
77
- if (type === "tool_execution_start" || type === "tool_start")
78
- return "tool-start";
79
- if (type === "tool_execution_end" || type === "tool_end") return "tool-end";
80
- if (
81
- type === "turn_end" ||
82
- type === "assistant" ||
83
- type === "assistant_message"
84
- ) {
85
- return "assistant";
86
- }
87
- if (type.startsWith("agent_") || type === "turn_start") return "lifecycle";
88
- return "protocol";
89
- }
90
-
91
- export function buildSessionTimelineItems(events) {
92
- const items = [];
93
- for (let index = 0; index < events.length; index += 1) {
94
- const event = events[index];
95
- const type = typeof event?.type === "string" ? event.type : "unknown";
96
- if (type === "message_start" || type === "message_end") {
97
- let count = 1;
98
- while (index + 1 < events.length) {
99
- const nextType = events[index + 1]?.type;
100
- if (nextType !== "message_start" && nextType !== "message_end") break;
101
- count += 1;
102
- index += 1;
103
- }
104
- items.push({ event, kind: "protocol", protocolCount: count });
105
- continue;
106
- }
107
- items.push({ event, kind: sessionEventKind(event) });
108
- }
109
- return items;
110
- }
111
-
112
- export async function mergeSessionEvents(dagRunId, nodeId) {
113
- if (!dagRunId || !nodeId) return;
114
- // Capture the current identity token before awaiting. The token encodes the
115
- // {dagRunId, nodeId} pair plus a per-switch sequence, and is bumped by
116
- // selectDagNode / renderDagDetail whenever the selection changes. After the
117
- // response resolves we re-check both the token and the live selection so a
118
- // stale response for a previous node can never mutate the current node's
119
- // event cache, even if the user switched nodes mid-flight.
120
- const identity = uiState.sessionEventIdentity;
121
- const page = await fetchJson(
122
- `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/session-events?after=${uiState.sessionEventOffset}`,
123
- );
124
- if (
125
- identity !== uiState.sessionEventIdentity ||
126
- uiState.currentDagRunId !== dagRunId ||
127
- uiState.selectedDagNodeId !== nodeId
128
- ) {
129
- return;
130
- }
131
- if (!page) return;
132
- if (Array.isArray(page.events) && page.events.length > 0) {
133
- uiState.sessionEvents = uiState.sessionEvents.concat(page.events);
134
- }
135
- if (typeof page.nextOffset === "number") {
136
- uiState.sessionEventOffset = page.nextOffset;
137
- }
138
- }
139
-
140
- export function renderSessionTimeline(content, nodeId) {
141
- content.dataset.dagRunId = uiState.currentDagRunId ?? "";
142
- content.dataset.nodeId = nodeId ?? "";
143
- content.classList.add("process-timeline-scroll");
144
- if (!nodeId || uiState.sessionEvents.length === 0) {
145
- content.appendChild(el("p", "empty", UI_TEXT.noSessionEvents));
146
- return;
147
- }
148
-
149
- const timelineItems = buildSessionTimelineItems(uiState.sessionEvents);
150
- const hasEventTimes = uiState.sessionEvents.some((event) =>
151
- Boolean(sessionEventTimestamp(event)),
152
- );
153
- if (!hasEventTimes) {
154
- content.appendChild(
155
- el(
156
- "p",
157
- "process-timeline-note",
158
- "事件未记录具体时间,以下按采集顺序展示。",
159
- ),
160
- );
161
- }
162
- const ul = el("ul", "process-timeline");
163
- for (let index = 0; index < timelineItems.length; index += 1) {
164
- const item = timelineItems[index];
165
- const li = el(
166
- "li",
167
- `process-timeline-item process-timeline-${item.kind}${hasEventTimes ? " has-time" : ""}`,
168
- );
169
- if (index === 0) li.classList.add("is-first");
170
- if (index === timelineItems.length - 1) li.classList.add("is-last");
171
- const rail = el("span", "process-timeline-rail");
172
- rail.appendChild(el("span", "process-timeline-dot"));
173
- li.appendChild(rail);
174
- if (hasEventTimes) {
175
- li.appendChild(
176
- el("span", "process-timeline-time", formatSessionEventTime(item.event)),
177
- );
178
- }
179
- const label =
180
- item.protocolCount > 1
181
- ? `消息收发 · 已合并 ${item.protocolCount} 条协议事件`
182
- : formatSessionEventLabel(item.event);
183
- li.appendChild(el("span", "process-timeline-label", label));
184
- ul.appendChild(li);
185
- }
186
- content.appendChild(ul);
187
-
188
- const timelineState =
189
- uiState.dagTimelineViewportState?.dagRunId === uiState.currentDagRunId &&
190
- uiState.dagTimelineViewportState.nodeId === nodeId
191
- ? uiState.dagTimelineViewportState
192
- : null;
193
- content.addEventListener(
194
- "scroll",
195
- () => {
196
- const followLatest = computeFollowLatest(
197
- content.scrollTop,
198
- content.scrollHeight,
199
- content.clientHeight,
200
- );
201
- uiState.dagTimelineViewportState = updateDagTimelineViewportState(
202
- uiState.dagTimelineViewportState,
203
- uiState.currentDagRunId,
204
- nodeId,
205
- content.scrollLeft,
206
- content.scrollTop,
207
- followLatest,
208
- );
209
- },
210
- { passive: true },
211
- );
212
- if (timelineState?.followLatest ?? true) {
213
- content.scrollLeft = timelineState?.scrollLeft ?? 0;
214
- content.scrollTop = content.scrollHeight;
215
- } else {
216
- content.scrollLeft = timelineState.scrollLeft;
217
- content.scrollTop = timelineState.scrollTop;
218
- }
219
- }
1
+ /** Session event timeline helpers (Observe UI R5 + rich process view). */
2
+ import { UI_TEXT } from "../constants.js";
3
+ import { el } from "../dom.js";
4
+ import { formatTs, truncateText } from "../format.js";
5
+ import { renderMarkdown } from "../markdown-render.js";
6
+ import { fetchJson } from "../api.js";
7
+ import {
8
+ uiState,
9
+ updateDagTimelineViewportState,
10
+ computeFollowLatest,
11
+ } from "../state.js";
12
+
13
+ /** Default visible lines for tool result preview before expand. */
14
+ export const TOOL_RESULT_PREVIEW_LINES = 20;
15
+
16
+ export function normalizeSessionEventFields(event) {
17
+ if (!event || typeof event !== "object") return {};
18
+ const args = event.args ?? event.input ?? event.toolInput;
19
+ const result = event.result ?? event.toolResult;
20
+ return {
21
+ ...event,
22
+ ...(args !== undefined ? { args, input: event.input ?? args } : {}),
23
+ ...(result !== undefined ? { result } : {}),
24
+ };
25
+ }
26
+
27
+ export function sessionEventArgs(event) {
28
+ const normalized = normalizeSessionEventFields(event);
29
+ const args = normalized.args;
30
+ return args && typeof args === "object" && !Array.isArray(args) ? args : {};
31
+ }
32
+
33
+ export function sessionEventResult(event) {
34
+ return normalizeSessionEventFields(event).result;
35
+ }
36
+
37
+ export function extractAssistantSummary(event) {
38
+ if (!event || typeof event !== "object") return "";
39
+ const record = event;
40
+ if (record.message && typeof record.message === "object") {
41
+ const content = record.message.content;
42
+ if (Array.isArray(content)) {
43
+ return (
44
+ content
45
+ .filter(
46
+ (part) => part?.type === "text" && typeof part.text === "string",
47
+ )
48
+ .map((part) => part.text)
49
+ .join(" ") || "—"
50
+ );
51
+ }
52
+ if (typeof record.message.content === "string") {
53
+ return record.message.content;
54
+ }
55
+ }
56
+ if (typeof record.text === "string") return record.text;
57
+ if (typeof record.outputPreview === "string") return record.outputPreview;
58
+ return "—";
59
+ }
60
+
61
+ export function extractThinkingBlocks(event) {
62
+ const content = event?.message?.content;
63
+ if (!Array.isArray(content)) return [];
64
+ return content
65
+ .filter(
66
+ (part) =>
67
+ part?.type === "thinking" && typeof part.thinking === "string",
68
+ )
69
+ .map((part) => part.thinking);
70
+ }
71
+
72
+ /** Tool calls declared on a turn_end / assistant message (name + args + id). */
73
+ export function extractToolCallsFromTurn(event) {
74
+ const content = event?.message?.content;
75
+ if (!Array.isArray(content)) return [];
76
+ const calls = [];
77
+ for (const part of content) {
78
+ if (!part || typeof part !== "object") continue;
79
+ if (part.type !== "toolCall" && part.type !== "tool_use") continue;
80
+ const name = part.name ?? part.toolName ?? "tool";
81
+ const args =
82
+ (part.arguments && typeof part.arguments === "object"
83
+ ? part.arguments
84
+ : null) ??
85
+ (part.args && typeof part.args === "object" ? part.args : null) ??
86
+ {};
87
+ calls.push({
88
+ id: typeof part.id === "string" ? part.id : "",
89
+ name: String(name),
90
+ args,
91
+ summary: formatToolSummary(String(name), args),
92
+ });
93
+ }
94
+ return calls;
95
+ }
96
+
97
+ export function formatToolSummary(toolName, args) {
98
+ const name = toolName || "tool";
99
+ const input = args && typeof args === "object" ? args : {};
100
+ if (name === "bash" || name === "bashExecution") {
101
+ const cmd = typeof input.command === "string" ? input.command : "";
102
+ return cmd ? `${name} · ${truncateText(cmd, 80)}` : name;
103
+ }
104
+ if (name === "read" || name === "write" || name === "edit") {
105
+ const path = typeof input.path === "string" ? input.path : "";
106
+ return path ? `${name} · ${truncateText(path, 80)}` : name;
107
+ }
108
+ if (name === "grep") {
109
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
110
+ const scope =
111
+ (typeof input.glob === "string" && input.glob) ||
112
+ (typeof input.path === "string" && input.path) ||
113
+ "";
114
+ if (pattern && scope) return `grep · ${truncateText(pattern, 48)} in ${truncateText(scope, 40)}`;
115
+ if (pattern) return `grep · ${truncateText(pattern, 80)}`;
116
+ return "grep";
117
+ }
118
+ if (name === "find" || name === "glob") {
119
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
120
+ return pattern ? `${name} · ${truncateText(pattern, 80)}` : name;
121
+ }
122
+ if (name === "ls") {
123
+ const path = typeof input.path === "string" ? input.path : "";
124
+ return path ? `ls · ${truncateText(path, 80)}` : "ls";
125
+ }
126
+ try {
127
+ const json = JSON.stringify(input);
128
+ if (json && json !== "{}") return `${name} · ${truncateText(json, 80)}`;
129
+ } catch {
130
+ /* ignore */
131
+ }
132
+ return name;
133
+ }
134
+
135
+ export function formatToolResultText(result) {
136
+ if (result == null) return "";
137
+ if (typeof result === "string") return result;
138
+ if (typeof result !== "object") return String(result);
139
+ if (Array.isArray(result.content)) {
140
+ return result.content
141
+ .map((part) => {
142
+ if (typeof part === "string") return part;
143
+ if (part?.type === "text" && typeof part.text === "string") return part.text;
144
+ return "";
145
+ })
146
+ .filter(Boolean)
147
+ .join("\n");
148
+ }
149
+ if (typeof result.content === "string") return result.content;
150
+ if (typeof result.text === "string") return result.text;
151
+ if (result.error != null) {
152
+ return typeof result.error === "string"
153
+ ? result.error
154
+ : JSON.stringify(result.error, null, 2);
155
+ }
156
+ try {
157
+ return JSON.stringify(result, null, 2);
158
+ } catch {
159
+ return String(result);
160
+ }
161
+ }
162
+
163
+ export function splitPreviewLines(text, maxLines = TOOL_RESULT_PREVIEW_LINES) {
164
+ const normalized = String(text ?? "");
165
+ const lines = normalized.split("\n");
166
+ if (lines.length <= maxLines) {
167
+ return { preview: normalized, truncated: false, totalLines: lines.length };
168
+ }
169
+ return {
170
+ preview: lines.slice(0, maxLines).join("\n"),
171
+ truncated: true,
172
+ totalLines: lines.length,
173
+ };
174
+ }
175
+
176
+ export function formatSessionEventLabel(event) {
177
+ if (!event || typeof event !== "object") return "未知类型";
178
+ const type = typeof event.type === "string" ? event.type : "unknown";
179
+ const toolName = event.toolName ?? event.name ?? "—";
180
+ if (type === "agent_start") return "Agent 已启动";
181
+ if (type === "agent_end") return "Agent 已结束";
182
+ if (type === "agent_settled") return "Agent 已稳定";
183
+ if (type === "turn_start") return "开始新一轮处理";
184
+ if (type === "tool_execution_start" || type === "tool_start") {
185
+ return `调用工具 · ${formatToolSummary(toolName, sessionEventArgs(event))}`;
186
+ }
187
+ if (type === "tool_execution_end" || type === "tool_end") {
188
+ return `工具调用完成 · ${toolName}`;
189
+ }
190
+ if (
191
+ type === "turn_end" ||
192
+ type === "assistant" ||
193
+ type === "assistant_message"
194
+ ) {
195
+ return `AI 回复摘要 · ${truncateText(extractAssistantSummary(event), 120)}`;
196
+ }
197
+ return type;
198
+ }
199
+
200
+ export function sessionEventTimestamp(event) {
201
+ if (!event || typeof event !== "object") return "";
202
+ const ts =
203
+ (typeof event.timestamp === "string" && event.timestamp) ||
204
+ (typeof event.at === "string" && event.at) ||
205
+ (typeof event.recordedAt === "string" && event.recordedAt);
206
+ return ts || "";
207
+ }
208
+
209
+ export function formatSessionEventTime(event) {
210
+ const ts = sessionEventTimestamp(event);
211
+ if (!ts) return "未记录时间";
212
+ return formatTs(ts).replace(/\s.*/, "") || formatTs(ts);
213
+ }
214
+
215
+ export function sessionEventKind(event) {
216
+ const type = typeof event?.type === "string" ? event.type : "unknown";
217
+ if (type === "tool_execution_start" || type === "tool_start")
218
+ return "tool-start";
219
+ if (type === "tool_execution_end" || type === "tool_end") return "tool-end";
220
+ if (
221
+ type === "turn_end" ||
222
+ type === "assistant" ||
223
+ type === "assistant_message"
224
+ ) {
225
+ return "assistant";
226
+ }
227
+ if (type.startsWith("agent_") || type === "turn_start") return "lifecycle";
228
+ return "protocol";
229
+ }
230
+
231
+ /** Legacy flat item builder (kept for label-only consumers / tests). */
232
+ export function buildSessionTimelineItems(events) {
233
+ const items = [];
234
+ for (let index = 0; index < events.length; index += 1) {
235
+ const event = events[index];
236
+ const type = typeof event?.type === "string" ? event.type : "unknown";
237
+ if (type === "message_start" || type === "message_end") {
238
+ let count = 1;
239
+ while (index + 1 < events.length) {
240
+ const nextType = events[index + 1]?.type;
241
+ if (nextType !== "message_start" && nextType !== "message_end") break;
242
+ count += 1;
243
+ index += 1;
244
+ }
245
+ items.push({ event, kind: "protocol", protocolCount: count });
246
+ continue;
247
+ }
248
+ items.push({ event, kind: sessionEventKind(event) });
249
+ }
250
+ return items;
251
+ }
252
+
253
+ /**
254
+ * Build rich timeline items: merge tool start/end by toolCallId, collapse
255
+ * protocol noise, surface turn thinking/text without dumping agent_end.messages.
256
+ */
257
+ export function buildRichTimelineItems(events, options = {}) {
258
+ const showProtocol = Boolean(options.showProtocol);
259
+ const items = [];
260
+ const pendingTools = new Map();
261
+
262
+ const flushTool = (toolCallId) => {
263
+ const pending = pendingTools.get(toolCallId);
264
+ if (!pending) return;
265
+ pendingTools.delete(toolCallId);
266
+ items.push(pending);
267
+ };
268
+
269
+ const flushAllPending = () => {
270
+ for (const id of [...pendingTools.keys()]) flushTool(id);
271
+ };
272
+
273
+ for (let index = 0; index < events.length; index += 1) {
274
+ const event = normalizeSessionEventFields(events[index]);
275
+ const type = typeof event?.type === "string" ? event.type : "unknown";
276
+
277
+ if (type === "message_start" || type === "message_end") {
278
+ let count = 1;
279
+ const protocolEvents = [event];
280
+ while (index + 1 < events.length) {
281
+ const nextType = events[index + 1]?.type;
282
+ if (nextType !== "message_start" && nextType !== "message_end") break;
283
+ count += 1;
284
+ index += 1;
285
+ protocolEvents.push(normalizeSessionEventFields(events[index]));
286
+ }
287
+ if (showProtocol) {
288
+ items.push({
289
+ kind: "protocol",
290
+ event,
291
+ protocolCount: count,
292
+ protocolEvents,
293
+ detailKey: `protocol-${items.length}`,
294
+ });
295
+ } else {
296
+ items.push({
297
+ kind: "protocol",
298
+ event,
299
+ protocolCount: count,
300
+ collapsed: true,
301
+ detailKey: `protocol-${items.length}`,
302
+ });
303
+ }
304
+ continue;
305
+ }
306
+
307
+ if (type === "tool_execution_start" || type === "tool_start") {
308
+ const toolCallId =
309
+ typeof event.toolCallId === "string" && event.toolCallId
310
+ ? event.toolCallId
311
+ : `anon-start-${index}`;
312
+ const toolName = event.toolName ?? event.name ?? "tool";
313
+ const args = sessionEventArgs(event);
314
+ pendingTools.set(toolCallId, {
315
+ kind: "tool",
316
+ toolCallId,
317
+ toolName,
318
+ args,
319
+ startEvent: event,
320
+ endEvent: null,
321
+ result: null,
322
+ isError: false,
323
+ pending: true,
324
+ detailKey: `tool-${toolCallId}`,
325
+ event,
326
+ });
327
+ continue;
328
+ }
329
+
330
+ // Streaming progress between start/end — do not flush pending tools.
331
+ if (
332
+ type === "tool_execution_update" ||
333
+ type === "tool_update" ||
334
+ type === "thinking_delta" ||
335
+ type === "message_update"
336
+ ) {
337
+ continue;
338
+ }
339
+
340
+ if (type === "tool_execution_end" || type === "tool_end") {
341
+ const toolCallId =
342
+ typeof event.toolCallId === "string" && event.toolCallId
343
+ ? event.toolCallId
344
+ : `anon-end-${index}`;
345
+ const toolName = event.toolName ?? event.name ?? "tool";
346
+ const result = sessionEventResult(event);
347
+ const isError = event.isError === true;
348
+ const existing = pendingTools.get(toolCallId);
349
+ if (existing) {
350
+ existing.endEvent = event;
351
+ existing.result = result;
352
+ existing.isError = isError;
353
+ existing.pending = false;
354
+ existing.toolName = existing.toolName || toolName;
355
+ existing.event = event;
356
+ flushTool(toolCallId);
357
+ } else {
358
+ items.push({
359
+ kind: "tool",
360
+ toolCallId,
361
+ toolName,
362
+ args: sessionEventArgs(event),
363
+ startEvent: null,
364
+ endEvent: event,
365
+ result,
366
+ isError,
367
+ pending: false,
368
+ detailKey: `tool-${toolCallId}`,
369
+ event,
370
+ });
371
+ }
372
+ continue;
373
+ }
374
+
375
+ if (
376
+ type === "turn_end" ||
377
+ type === "assistant" ||
378
+ type === "assistant_message"
379
+ ) {
380
+ // Do not flush pending tools here: tool_execution_update / out-of-order
381
+ // ends may still arrive after turn_end in some streams.
382
+ items.push({
383
+ kind: "turn",
384
+ event,
385
+ thinking: extractThinkingBlocks(event),
386
+ text: extractAssistantSummary(event),
387
+ toolCalls: extractToolCallsFromTurn(event),
388
+ detailKey: `turn-${items.length}`,
389
+ });
390
+ continue;
391
+ }
392
+
393
+ if (type === "agent_end") {
394
+ const messages = Array.isArray(event.messages) ? event.messages : [];
395
+ items.push({
396
+ kind: "lifecycle",
397
+ event,
398
+ agentEnd: true,
399
+ messageCount: messages.length,
400
+ willRetry: event.willRetry === true,
401
+ detailKey: `agent-end-${items.length}`,
402
+ });
403
+ continue;
404
+ }
405
+
406
+ // Lifecycle / unknown noise must not flush in-flight tool starts.
407
+ if (
408
+ type === "agent_start" ||
409
+ type === "turn_start" ||
410
+ type === "agent_settled" ||
411
+ type === "auto_retry_start" ||
412
+ type === "auto_retry_end" ||
413
+ type.startsWith("agent_")
414
+ ) {
415
+ items.push({
416
+ kind: sessionEventKind(event),
417
+ event,
418
+ detailKey: `evt-${items.length}-${type}`,
419
+ });
420
+ continue;
421
+ }
422
+
423
+ items.push({
424
+ kind: sessionEventKind(event),
425
+ event,
426
+ detailKey: `evt-${items.length}-${type}`,
427
+ });
428
+ }
429
+
430
+ // Only after the full event list: leftover starts with no end.
431
+ flushAllPending();
432
+ return items;
433
+ }
434
+
435
+ function ensureOpenDetailsSet() {
436
+ if (!(uiState.sessionTimelineOpenDetails instanceof Set)) {
437
+ uiState.sessionTimelineOpenDetails = new Set();
438
+ }
439
+ return uiState.sessionTimelineOpenDetails;
440
+ }
441
+
442
+ function bindDetailsToggle(details, detailKey) {
443
+ const openSet = ensureOpenDetailsSet();
444
+ if (openSet.has(detailKey)) details.open = true;
445
+ details.addEventListener("toggle", () => {
446
+ if (details.open) openSet.add(detailKey);
447
+ else openSet.delete(detailKey);
448
+ });
449
+ }
450
+
451
+ function appendPreBlock(parent, className, text) {
452
+ const pre = el("pre", className);
453
+ pre.textContent = text;
454
+ parent.appendChild(pre);
455
+ return pre;
456
+ }
457
+
458
+ function renderToolCard(item) {
459
+ const card = el(
460
+ "div",
461
+ `process-timeline-card process-timeline-tool-card${item.isError ? " is-error" : ""}${item.pending ? " is-pending" : ""}`,
462
+ );
463
+ const summary = formatToolSummary(item.toolName, item.args);
464
+ const status = item.pending ? "进行中" : item.isError ? "失败" : "完成";
465
+ const details = document.createElement("details");
466
+ details.className = "process-timeline-details";
467
+ details.dataset.detailKey = item.detailKey;
468
+ bindDetailsToggle(details, item.detailKey);
469
+
470
+ const summaryEl = document.createElement("summary");
471
+ summaryEl.className = "process-timeline-summary";
472
+ summaryEl.appendChild(
473
+ el("span", "process-timeline-summary-title", summary),
474
+ );
475
+ summaryEl.appendChild(
476
+ el(
477
+ "span",
478
+ `process-timeline-summary-status status-${item.pending ? "pending" : item.isError ? "failed" : "succeeded"}`,
479
+ status,
480
+ ),
481
+ );
482
+ details.appendChild(summaryEl);
483
+
484
+ const body = el("div", "process-timeline-card-body");
485
+ const argsJson = (() => {
486
+ try {
487
+ return JSON.stringify(item.args ?? {}, null, 2);
488
+ } catch {
489
+ return String(item.args ?? {});
490
+ }
491
+ })();
492
+ body.appendChild(el("div", "process-timeline-section-label", "参数"));
493
+ appendPreBlock(body, "process-timeline-code", argsJson);
494
+
495
+ const resultText = formatToolResultText(item.result);
496
+ if (resultText) {
497
+ body.appendChild(el("div", "process-timeline-section-label", "结果"));
498
+ const { preview, truncated, totalLines } = splitPreviewLines(resultText);
499
+ appendPreBlock(body, "process-timeline-code", preview);
500
+ if (truncated) {
501
+ const more = document.createElement("details");
502
+ more.className = "process-timeline-more";
503
+ more.dataset.detailKey = `${item.detailKey}-full`;
504
+ bindDetailsToggle(more, `${item.detailKey}-full`);
505
+ const moreSummary = document.createElement("summary");
506
+ moreSummary.textContent = `展开全部结果(${totalLines} 行)`;
507
+ more.appendChild(moreSummary);
508
+ appendPreBlock(more, "process-timeline-code", resultText);
509
+ body.appendChild(more);
510
+ }
511
+ } else if (!item.pending) {
512
+ body.appendChild(el("p", "muted", "无结果正文"));
513
+ }
514
+ details.appendChild(body);
515
+ card.appendChild(details);
516
+ return card;
517
+ }
518
+
519
+ function renderTurnCard(item) {
520
+ const card = el("div", "process-timeline-card process-timeline-turn-card");
521
+ const toolCalls = Array.isArray(item.toolCalls) ? item.toolCalls : [];
522
+ const hasText = Boolean(item.text && item.text !== "—");
523
+ const title = hasText
524
+ ? `AI 回复 · ${truncateText(item.text, 80)}`
525
+ : toolCalls.length > 0
526
+ ? `本回合仅调用工具 · ${toolCalls.length} 个`
527
+ : item.thinking.length > 0
528
+ ? "AI 回复 · 含思考过程"
529
+ : "AI 回复";
530
+
531
+ if (item.thinking.length > 0) {
532
+ for (const [i, thinking] of item.thinking.entries()) {
533
+ const details = document.createElement("details");
534
+ details.className = "process-timeline-details process-timeline-thinking";
535
+ details.dataset.detailKey = `${item.detailKey}-thinking-${i}`;
536
+ // Default open for first thinking block so anomalies are visible.
537
+ if (i === 0) details.open = true;
538
+ bindDetailsToggle(details, `${item.detailKey}-thinking-${i}`);
539
+ const summary = document.createElement("summary");
540
+ summary.className = "process-timeline-summary";
541
+ summary.textContent = "思考过程";
542
+ details.appendChild(summary);
543
+ appendPreBlock(details, "process-timeline-thinking-body", thinking);
544
+ card.appendChild(details);
545
+ }
546
+ }
547
+
548
+ const textBlock = el("div", "process-timeline-turn-text");
549
+ textBlock.appendChild(el("div", "process-timeline-section-label", title));
550
+ if (hasText) {
551
+ textBlock.appendChild(renderMarkdown(item.text));
552
+ } else if (toolCalls.length > 0) {
553
+ textBlock.appendChild(
554
+ el(
555
+ "p",
556
+ "muted process-timeline-tool-only-note",
557
+ "本回合没有文本回复,模型直接发起了下列工具调用(详情见上方工具卡):",
558
+ ),
559
+ );
560
+ const list = el("ul", "process-timeline-turn-tools");
561
+ for (const call of toolCalls) {
562
+ list.appendChild(el("li", null, call.summary || call.name));
563
+ }
564
+ textBlock.appendChild(list);
565
+ } else {
566
+ textBlock.appendChild(el("p", "muted", "无文本回复"));
567
+ }
568
+ card.appendChild(textBlock);
569
+ return card;
570
+ }
571
+
572
+ function renderLifecycleCard(item) {
573
+ if (item.agentEnd) {
574
+ const card = el("div", "process-timeline-card process-timeline-lifecycle-card");
575
+ const parts = ["Agent 已结束"];
576
+ if (item.messageCount > 0) parts.push(`${item.messageCount} 条消息`);
577
+ if (item.willRetry) parts.push("将重试");
578
+ card.appendChild(el("div", "process-timeline-label", parts.join(" · ")));
579
+ card.appendChild(
580
+ el(
581
+ "p",
582
+ "muted process-timeline-agent-end-note",
583
+ "完整会话 messages 体积可能很大,此处不展开;请查看上方工具与回合卡片。",
584
+ ),
585
+ );
586
+ return card;
587
+ }
588
+ return el("span", "process-timeline-label", formatSessionEventLabel(item.event));
589
+ }
590
+
591
+ function renderProtocolCard(item) {
592
+ if (item.collapsed) {
593
+ return el(
594
+ "span",
595
+ "process-timeline-label",
596
+ `消息收发 · 已合并 ${item.protocolCount} 条协议事件`,
597
+ );
598
+ }
599
+ const details = document.createElement("details");
600
+ details.className = "process-timeline-details";
601
+ details.dataset.detailKey = item.detailKey;
602
+ bindDetailsToggle(details, item.detailKey);
603
+ const summary = document.createElement("summary");
604
+ summary.className = "process-timeline-summary";
605
+ summary.textContent = `消息收发 · ${item.protocolCount} 条协议事件`;
606
+ details.appendChild(summary);
607
+ const body = el("div", "process-timeline-card-body");
608
+ for (const event of item.protocolEvents ?? [item.event]) {
609
+ const role = event?.message?.role ?? event?.type ?? "event";
610
+ body.appendChild(el("div", "process-timeline-section-label", String(role)));
611
+ try {
612
+ appendPreBlock(
613
+ body,
614
+ "process-timeline-code",
615
+ JSON.stringify(event, null, 2),
616
+ );
617
+ } catch {
618
+ appendPreBlock(body, "process-timeline-code", String(event));
619
+ }
620
+ }
621
+ details.appendChild(body);
622
+ return details;
623
+ }
624
+
625
+ export async function mergeSessionEvents(dagRunId, nodeId) {
626
+ if (!dagRunId || !nodeId) return;
627
+ const identity = uiState.sessionEventIdentity;
628
+ const page = await fetchJson(
629
+ `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/session-events?after=${uiState.sessionEventOffset}`,
630
+ );
631
+ if (
632
+ identity !== uiState.sessionEventIdentity ||
633
+ uiState.currentDagRunId !== dagRunId ||
634
+ uiState.selectedDagNodeId !== nodeId
635
+ ) {
636
+ return;
637
+ }
638
+ if (!page) return;
639
+ if (Array.isArray(page.events) && page.events.length > 0) {
640
+ uiState.sessionEvents = uiState.sessionEvents.concat(page.events);
641
+ }
642
+ if (typeof page.nextOffset === "number") {
643
+ uiState.sessionEventOffset = page.nextOffset;
644
+ }
645
+ }
646
+
647
+ export function renderSessionTimeline(content, nodeId) {
648
+ content.dataset.dagRunId = uiState.currentDagRunId ?? "";
649
+ content.dataset.nodeId = nodeId ?? "";
650
+ content.classList.add("process-timeline-scroll");
651
+ if (!nodeId || uiState.sessionEvents.length === 0) {
652
+ content.appendChild(el("p", "empty", UI_TEXT.noSessionEvents));
653
+ return;
654
+ }
655
+
656
+ const showProtocol = Boolean(uiState.sessionTimelineShowProtocol);
657
+ const toolbar = el("div", "process-timeline-toolbar");
658
+ const protocolToggle = document.createElement("button");
659
+ protocolToggle.type = "button";
660
+ protocolToggle.className = "process-timeline-toggle";
661
+ protocolToggle.setAttribute("aria-pressed", String(showProtocol));
662
+ protocolToggle.textContent = showProtocol
663
+ ? "隐藏原始协议事件"
664
+ : "展开原始协议事件";
665
+ protocolToggle.addEventListener("click", () => {
666
+ uiState.sessionTimelineShowProtocol = !showProtocol;
667
+ const parent = content;
668
+ const scrollTop = parent.scrollTop;
669
+ const scrollLeft = parent.scrollLeft;
670
+ while (parent.firstChild) parent.removeChild(parent.firstChild);
671
+ renderSessionTimeline(parent, nodeId);
672
+ parent.scrollTop = scrollTop;
673
+ parent.scrollLeft = scrollLeft;
674
+ });
675
+ toolbar.appendChild(protocolToggle);
676
+ content.appendChild(toolbar);
677
+
678
+ const timelineItems = buildRichTimelineItems(uiState.sessionEvents, {
679
+ showProtocol,
680
+ });
681
+ const hasEventTimes = uiState.sessionEvents.some((event) =>
682
+ Boolean(sessionEventTimestamp(event)),
683
+ );
684
+ if (!hasEventTimes) {
685
+ content.appendChild(
686
+ el(
687
+ "p",
688
+ "process-timeline-note",
689
+ "事件未记录具体时间,以下按采集顺序展示。",
690
+ ),
691
+ );
692
+ }
693
+
694
+ const ul = el("ul", "process-timeline process-timeline-rich");
695
+ for (let index = 0; index < timelineItems.length; index += 1) {
696
+ const item = timelineItems[index];
697
+ const kindClass =
698
+ item.kind === "tool"
699
+ ? item.isError
700
+ ? "tool-end is-error"
701
+ : item.pending
702
+ ? "tool-start"
703
+ : "tool-end"
704
+ : item.kind === "turn"
705
+ ? "assistant"
706
+ : item.kind;
707
+ const li = el(
708
+ "li",
709
+ `process-timeline-item process-timeline-${kindClass}${hasEventTimes ? " has-time" : ""}`,
710
+ );
711
+ if (index === 0) li.classList.add("is-first");
712
+ if (index === timelineItems.length - 1) li.classList.add("is-last");
713
+ const rail = el("span", "process-timeline-rail");
714
+ rail.appendChild(el("span", "process-timeline-dot"));
715
+ li.appendChild(rail);
716
+ if (hasEventTimes) {
717
+ li.appendChild(
718
+ el("span", "process-timeline-time", formatSessionEventTime(item.event)),
719
+ );
720
+ }
721
+ const body = el("div", "process-timeline-body");
722
+ if (item.kind === "tool") {
723
+ body.appendChild(renderToolCard(item));
724
+ } else if (item.kind === "turn") {
725
+ body.appendChild(renderTurnCard(item));
726
+ } else if (item.kind === "lifecycle" && item.agentEnd) {
727
+ body.appendChild(renderLifecycleCard(item));
728
+ } else if (item.kind === "protocol") {
729
+ body.appendChild(renderProtocolCard(item));
730
+ } else {
731
+ body.appendChild(
732
+ el("span", "process-timeline-label", formatSessionEventLabel(item.event)),
733
+ );
734
+ }
735
+ li.appendChild(body);
736
+ ul.appendChild(li);
737
+ }
738
+ content.appendChild(ul);
739
+
740
+ const timelineState =
741
+ uiState.dagTimelineViewportState?.dagRunId === uiState.currentDagRunId &&
742
+ uiState.dagTimelineViewportState.nodeId === nodeId
743
+ ? uiState.dagTimelineViewportState
744
+ : null;
745
+ content.addEventListener(
746
+ "scroll",
747
+ () => {
748
+ const followLatest = computeFollowLatest(
749
+ content.scrollTop,
750
+ content.scrollHeight,
751
+ content.clientHeight,
752
+ );
753
+ uiState.dagTimelineViewportState = updateDagTimelineViewportState(
754
+ uiState.dagTimelineViewportState,
755
+ uiState.currentDagRunId,
756
+ nodeId,
757
+ content.scrollLeft,
758
+ content.scrollTop,
759
+ followLatest,
760
+ );
761
+ },
762
+ { passive: true },
763
+ );
764
+ if (timelineState?.followLatest ?? true) {
765
+ content.scrollLeft = timelineState?.scrollLeft ?? 0;
766
+ content.scrollTop = content.scrollHeight;
767
+ } else {
768
+ content.scrollLeft = timelineState.scrollLeft;
769
+ content.scrollTop = timelineState.scrollTop;
770
+ }
771
+ }