@tea-agent/loop-agent 0.13.0 → 0.14.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 (270) hide show
  1. package/AGENTS.md +157 -157
  2. package/CHANGELOG.md +73 -301
  3. package/README.md +338 -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/docs/README.md +105 -104
  50. package/docs/agent-dag-recovery-playbook.md +195 -195
  51. package/docs/agent-dag-runner.md +67 -67
  52. package/docs/architecture/README.md +26 -26
  53. package/docs/architecture/dag-execution.md +140 -140
  54. package/docs/architecture/evolution.md +54 -54
  55. package/docs/architecture/facts-and-state.md +71 -71
  56. package/docs/architecture/runtime-boundaries.md +191 -191
  57. package/docs/architecture/system-overview.md +93 -93
  58. package/docs/architecture/worker-and-feature.md +85 -85
  59. package/docs/cursor-prompt-sidecar.md +36 -36
  60. package/docs/decisions/README.md +18 -18
  61. package/docs/design/README.md +167 -167
  62. package/docs/development-principles.md +73 -73
  63. package/docs/exec-plans/README.md +6 -6
  64. package/docs/exec-plans/active/README.md +2 -1
  65. package/docs/exec-plans/completed/README.md +105 -104
  66. package/docs/feature-workflow.md +414 -414
  67. package/docs/harness-methodology-debugging.md +153 -153
  68. package/docs/harness-methodology-tdd.md +130 -130
  69. package/docs/harness-methodology-verification.md +27 -27
  70. package/docs/init-surface.manifest.json +307 -307
  71. package/docs/loop-agent-harness.md +142 -142
  72. package/docs/production-readiness.md +96 -96
  73. package/docs/progress/README.md +59 -58
  74. package/docs/reports/README.md +123 -119
  75. package/docs/skills/README.md +7 -7
  76. package/docs/skills/vetted-skill-registry.md +29 -29
  77. package/docs/templates/adr.md +60 -60
  78. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  79. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  80. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  81. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  82. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  83. package/docs/templates/agent-dag-report.schema.json +473 -473
  84. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  85. package/docs/templates/agent-dag.base.json +190 -190
  86. package/docs/templates/agent-dag.final-verification.json +185 -185
  87. package/docs/templates/agent-dag.schema.json +411 -411
  88. package/docs/templates/agent-dag.supervised-implementation.json +620 -620
  89. package/docs/templates/backend-test-analysis.schema.json +44 -44
  90. package/docs/templates/backend-test-case-manifest.schema.json +190 -190
  91. package/docs/templates/backend-test-dag.classify.prompt.md +75 -75
  92. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +204 -204
  93. package/docs/templates/backend-test-dag.json +559 -559
  94. package/docs/templates/backend-test-dag.retrospect.prompt.md +139 -139
  95. package/docs/templates/backend-test-dag.review-cases.prompt.md +83 -83
  96. package/docs/templates/backend-test-execution.schema.json +133 -133
  97. package/docs/templates/backend-test-result.schema.json +99 -99
  98. package/docs/templates/exec-plan.md +64 -64
  99. package/docs/templates/feature-spec.md +53 -53
  100. package/docs/templates/frontend-design-contract.md +42 -42
  101. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  102. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  103. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  104. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  105. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  106. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  107. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  108. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  109. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  110. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  111. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  112. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  113. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  114. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  115. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  116. package/docs/templates/frontend-eval/metrics.md +138 -138
  117. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  118. package/docs/templates/frontend-implementation-contract.schema.json +27 -27
  119. package/docs/templates/frontend-task-constraints.md +35 -35
  120. package/docs/templates/frontend-task-requirement.md +70 -70
  121. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -5
  122. package/docs/templates/frontend-test-dag.json +23 -23
  123. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -3
  124. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -3
  125. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -3
  126. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -3
  127. package/docs/templates/harness.schema.json +221 -221
  128. package/docs/templates/hybrid-dag.json +188 -188
  129. package/docs/templates/init-evolution-review.md +35 -35
  130. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  131. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  132. package/docs/templates/knowledge-sync-dag.json +178 -178
  133. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  134. package/docs/templates/product-line/AGENTS.md +8 -8
  135. package/docs/templates/product-line/README.md +9 -9
  136. package/docs/templates/product-line/acceptance.yaml +14 -14
  137. package/docs/templates/product-line/closeout.yaml +9 -9
  138. package/docs/templates/product-line/design.md +13 -13
  139. package/docs/templates/product-line/links.md +10 -10
  140. package/docs/templates/product-line/requirement.md +17 -17
  141. package/docs/templates/product-line/task-graph.yaml +15 -15
  142. package/docs/templates/product-line/task.yaml +64 -64
  143. package/docs/templates/product-line/test-plan.md +7 -7
  144. package/docs/templates/production-readiness-checklist.md +57 -57
  145. package/docs/templates/progress-log.md +17 -17
  146. package/docs/templates/project-start-checklist.md +9 -9
  147. package/docs/templates/qa-report.md +48 -48
  148. package/docs/templates/sprint-contract.md +29 -29
  149. package/docs/templates/worker-dogfood-evidence.md +80 -80
  150. package/docs/templates/worker-dogfood-setup.md +68 -68
  151. package/docs/verification-matrix.md +70 -70
  152. package/examples/decision-gate-agent-dag.json +173 -173
  153. package/examples/example-dag.json +46 -46
  154. package/examples/hybrid-loop-agent-dag.json +188 -188
  155. package/harness.json +66 -66
  156. package/package.json +88 -52
  157. package/scripts/check-product-line-docs.sh +29 -29
  158. package/scripts/check-task-pool-root.sh +32 -32
  159. package/scripts/kb-bootstrap-init-skeleton.sh +240 -240
  160. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  161. package/scripts/kb-graph-incremental-prepare.sh +5 -5
  162. package/scripts/kb-graph-materialize.mjs +105 -105
  163. package/scripts/kb-graph-materialize.sh +4 -4
  164. package/scripts/kb-graph-promote.mjs +164 -164
  165. package/scripts/kb-graph-promote.sh +4 -4
  166. package/scripts/kb-query.mjs +554 -554
  167. package/scripts/kb-query.sh +5 -5
  168. package/skills/agent-worker/SKILL.md +39 -39
  169. package/skills/agent-worker/references/agent-worker-operator.md +60 -60
  170. package/skills/ai-engineering-context/SKILL.md +48 -48
  171. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  172. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  173. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  174. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  175. package/skills/analyze-product-dependencies/references/example.md +76 -76
  176. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  177. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  178. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  179. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  180. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  181. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  182. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  183. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  184. package/skills/analyze-product-requirements/SKILL.md +90 -90
  185. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  186. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  187. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  188. package/skills/analyze-product-requirements/references/example.md +86 -86
  189. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  190. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  191. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  192. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  193. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  194. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  195. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  196. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  197. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  198. package/skills/browser-tools/SKILL.md +196 -196
  199. package/skills/browser-tools/browser-content.js +103 -103
  200. package/skills/browser-tools/browser-cookies.js +35 -35
  201. package/skills/browser-tools/browser-eval.js +53 -53
  202. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  203. package/skills/browser-tools/browser-nav.js +44 -44
  204. package/skills/browser-tools/browser-pick.js +162 -162
  205. package/skills/browser-tools/browser-screenshot.js +34 -34
  206. package/skills/browser-tools/browser-start.js +86 -86
  207. package/skills/browser-tools/package-lock.json +2556 -2556
  208. package/skills/browser-tools/package.json +19 -19
  209. package/skills/code-review-core/SKILL.md +20 -20
  210. package/skills/codebase-scout/SKILL.md +19 -19
  211. package/skills/frontend-design-review/SKILL.md +66 -66
  212. package/skills/frontend-design-review/references/review-checklist.md +58 -58
  213. package/skills/frontend-implementation/SKILL.md +49 -49
  214. package/skills/frontend-implementation/references/code-standards.md +32 -32
  215. package/skills/frontend-implementation/references/design-spec.md +46 -46
  216. package/skills/frontend-implementation/references/node-contracts.md +27 -27
  217. package/skills/frontend-review/SKILL.md +59 -59
  218. package/skills/frontend-review/references/review-findings.md +47 -47
  219. package/skills/frontend-verification/SKILL.md +53 -53
  220. package/skills/frontend-verification/references/verification-checklist.md +68 -68
  221. package/skills/grill-me/SKILL.md +10 -10
  222. package/skills/grill-with-docs/SKILL.md +88 -88
  223. package/skills/grill-with-docs/adr-format.md +47 -47
  224. package/skills/grill-with-docs/context-format.md +60 -60
  225. package/skills/init-capability-evolution/SKILL.md +70 -70
  226. package/skills/loop-agent/SKILL.md +151 -151
  227. package/skills/loop-agent/references/README.md +67 -67
  228. package/skills/loop-agent/references/command-reference.md +527 -527
  229. package/skills/loop-agent/references/docs-converge.md +126 -126
  230. package/skills/loop-agent/references/harness-policy.md +263 -263
  231. package/skills/loop-agent/references/hybrid-dag.md +243 -243
  232. package/skills/loop-agent/references/learned/README.md +21 -21
  233. package/skills/loop-agent/references/long-running-loop.md +57 -57
  234. package/skills/loop-agent/references/model-routing.md +36 -36
  235. package/skills/loop-agent/references/multi-worktree.md +54 -54
  236. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  237. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  238. package/skills/loop-agent/references/pi-prompt.md +23 -23
  239. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  240. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  241. package/skills/loop-agent/references/task-workflow.md +89 -89
  242. package/skills/loop-agent/references/verification-and-failure-handling.md +141 -141
  243. package/skills/playwright-cli/SKILL.md +420 -420
  244. package/skills/playwright-cli/references/element-attributes.md +23 -23
  245. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  246. package/skills/playwright-cli/references/request-mocking.md +87 -87
  247. package/skills/playwright-cli/references/running-code.md +241 -241
  248. package/skills/playwright-cli/references/session-management.md +225 -225
  249. package/skills/playwright-cli/references/storage-state.md +275 -275
  250. package/skills/playwright-cli/references/test-generation.md +433 -433
  251. package/skills/playwright-cli/references/tracing.md +139 -139
  252. package/skills/playwright-cli/references/video-recording.md +143 -143
  253. package/skills/playwright-cli-case-generator/SKILL.md +74 -74
  254. package/skills/requesting-code-review/SKILL.md +101 -101
  255. package/skills/requesting-code-review/code-reviewer.md +168 -168
  256. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  257. package/skills/systematic-debugging/SKILL.md +296 -296
  258. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  259. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  260. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  261. package/skills/systematic-debugging/find-polluter.sh +63 -63
  262. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  263. package/skills/systematic-debugging/test-academic.md +14 -14
  264. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  265. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  266. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  267. package/skills/test-driven-development/SKILL.md +20 -20
  268. package/skills/using-git-worktrees/SKILL.md +215 -215
  269. package/skills/verification-before-completion/SKILL.md +154 -154
  270. package/skills/webapp-testing/SKILL.md +19 -19
@@ -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
+ }