@tea-agent/loop-agent 0.26.0 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +1056 -1020
  2. package/README.md +8 -3
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/application/dag/generate-task-dag.js +33 -0
  5. package/dist/cli/command-definitions.js +25 -10
  6. package/dist/cli/help.js +4 -3
  7. package/dist/cli/program.js +43 -17
  8. package/dist/commands/cursor-prompt.js +6 -6
  9. package/dist/commands/import-prd.js +7 -2
  10. package/dist/commands/init.js +7 -5
  11. package/dist/commands/loop-benchmark.js +11 -11
  12. package/dist/commands/pi-reuse-benchmark.js +16 -16
  13. package/dist/commands/task-source-prepare.js +474 -0
  14. package/dist/executors/dag-pi-executor.js +40 -5
  15. package/dist/executors/shell-executor.js +111 -0
  16. package/dist/executors/shell-presets.js +12 -4
  17. package/dist/executors/shell-write-guard.js +161 -25
  18. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  19. package/dist/task/config-types.js +6 -0
  20. package/dist/task/contract/constants.js +1 -0
  21. package/dist/task/contract/project.js +8 -0
  22. package/dist/task/contract/schema.js +1 -0
  23. package/dist/task/frontend-preflight.js +131 -0
  24. package/dist/task/runtime.js +2 -4
  25. package/dist/task/source-prepare/build-draft.js +224 -0
  26. package/dist/task/source-prepare/completeness.js +195 -0
  27. package/dist/task/source-prepare/index.js +7 -0
  28. package/dist/task/source-prepare/parse-intent.js +373 -0
  29. package/dist/task/source-prepare/path-policy.js +197 -0
  30. package/dist/task/source-prepare/prepare.js +506 -0
  31. package/dist/task/source-prepare/reference-integrity.js +274 -0
  32. package/dist/task/source-prepare/types.js +7 -0
  33. package/dist/worker/observability/read-model.js +134 -0
  34. package/dist/worker/observe/static/copy.js +67 -67
  35. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  36. package/dist/worker/observe/static/dag-layout.js +83 -83
  37. package/dist/worker/observe/static/dom.js +220 -220
  38. package/dist/worker/observe/static/relations.js +133 -133
  39. package/dist/worker/observe/static/router.js +93 -93
  40. package/dist/worker/observe/static/run-processing.js +148 -148
  41. package/dist/worker/observe/static/state.js +61 -0
  42. package/dist/worker/observe/static/styles.css +8 -0
  43. package/dist/worker/observe/static/views/batch.js +227 -227
  44. package/dist/worker/observe/static/views/dag-graph.js +248 -172
  45. package/dist/worker/observe/static/views/dag-inspector.js +374 -157
  46. package/dist/worker/observe/static/views/dag.js +4 -11
  47. package/dist/worker/observe/static/views/failures.js +143 -143
  48. package/dist/worker/observe/static/views/feature.js +492 -492
  49. package/dist/worker/observe/static/views/run.js +453 -453
  50. package/dist/worker/observe/static/views/shell.js +7 -7
  51. package/dist/worker/observe/static/views/timeline.js +163 -163
  52. package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
  53. package/dist/workflows/dag/canvas-observer.js +275 -275
  54. package/dist/workflows/dag/convergence/controller.js +110 -21
  55. package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
  56. package/dist/workflows/dag/frontend-review-context.js +7 -1
  57. package/dist/workflows/dag/frontend-verification-trace.js +14 -3
  58. package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
  59. package/dist/workflows/dag/init-hybrid.js +96 -34
  60. package/dist/workflows/dag/output-protocol.js +180 -7
  61. package/dist/workflows/dag/runner.js +141 -52
  62. package/dist/workflows/dag/types.js +4 -0
  63. package/dist/workflows/dag/validate.js +3 -2
  64. package/docs/skills/README.md +7 -7
  65. package/docs/templates/adr.md +60 -60
  66. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  67. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  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/backend-test-dag.json +100 -8
  73. package/docs/templates/backend-test-result.schema.json +99 -99
  74. package/docs/templates/feature-spec.md +53 -53
  75. package/docs/templates/frontend-design-contract.md +42 -42
  76. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  77. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  78. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  79. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  80. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  81. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  82. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  83. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  84. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  85. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  86. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  87. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  88. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  89. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  90. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  91. package/docs/templates/frontend-eval/metrics.md +138 -138
  92. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  93. package/docs/templates/frontend-task-constraints.md +35 -35
  94. package/docs/templates/frontend-task-requirement.md +70 -70
  95. package/docs/templates/init-evolution-review.md +35 -35
  96. package/docs/templates/init-managed-agents.md +10 -5
  97. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  98. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  99. package/docs/templates/knowledge-sync-dag.json +178 -178
  100. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  101. package/docs/templates/product-line/closeout.yaml +9 -9
  102. package/docs/templates/product-line/design.md +13 -13
  103. package/docs/templates/product-line/links.md +10 -10
  104. package/docs/templates/product-line/requirement.md +17 -17
  105. package/docs/templates/product-line/test-plan.md +7 -7
  106. package/docs/templates/production-readiness-checklist.md +57 -57
  107. package/docs/templates/project-start-checklist.md +9 -9
  108. package/docs/templates/qa-report.md +48 -48
  109. package/docs/templates/sprint-contract.md +29 -29
  110. package/docs/templates/worker-dogfood-evidence.md +80 -80
  111. package/docs/templates/worker-dogfood-setup.md +68 -68
  112. package/package.json +1 -1
  113. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  114. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  115. package/scripts/kb-graph-materialize.mjs +105 -105
  116. package/scripts/kb-graph-promote.mjs +164 -164
  117. package/scripts/kb-query.mjs +554 -554
  118. package/skills/ai-engineering-context/SKILL.md +48 -48
  119. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  120. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  121. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  122. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  123. package/skills/analyze-product-dependencies/references/example.md +76 -76
  124. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  125. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  126. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  127. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  128. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  129. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  130. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  131. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  132. package/skills/analyze-product-requirements/SKILL.md +90 -90
  133. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  134. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  135. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  136. package/skills/analyze-product-requirements/references/example.md +86 -86
  137. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  138. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  139. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  140. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  141. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  142. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  143. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  144. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  145. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  146. package/skills/browser-tools/browser-content.js +103 -103
  147. package/skills/browser-tools/browser-cookies.js +35 -35
  148. package/skills/browser-tools/browser-eval.js +53 -53
  149. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  150. package/skills/browser-tools/browser-nav.js +44 -44
  151. package/skills/browser-tools/browser-pick.js +162 -162
  152. package/skills/browser-tools/browser-screenshot.js +34 -34
  153. package/skills/browser-tools/browser-start.js +86 -86
  154. package/skills/browser-tools/package-lock.json +2556 -2556
  155. package/skills/browser-tools/package.json +19 -19
  156. package/skills/code-review-core/SKILL.md +20 -20
  157. package/skills/codebase-scout/SKILL.md +19 -19
  158. package/skills/grill-me/SKILL.md +10 -10
  159. package/skills/loop-agent/SKILL.md +5 -2
  160. package/skills/loop-agent/references/README.md +67 -67
  161. package/skills/loop-agent/references/command-reference.md +17 -15
  162. package/skills/loop-agent/references/docs-converge.md +126 -126
  163. package/skills/loop-agent/references/harness-policy.md +3 -4
  164. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  165. package/skills/loop-agent/references/learned/README.md +21 -21
  166. package/skills/loop-agent/references/long-running-loop.md +57 -57
  167. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  168. package/skills/loop-agent/references/pi-prompt.md +23 -23
  169. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  170. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  171. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  172. package/skills/loop-agent/references/task-workflow.md +7 -5
  173. package/skills/playwright-cli/SKILL.md +420 -420
  174. package/skills/playwright-cli/references/element-attributes.md +23 -23
  175. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  176. package/skills/playwright-cli/references/request-mocking.md +87 -87
  177. package/skills/playwright-cli/references/running-code.md +241 -241
  178. package/skills/playwright-cli/references/session-management.md +225 -225
  179. package/skills/playwright-cli/references/storage-state.md +275 -275
  180. package/skills/playwright-cli/references/test-generation.md +433 -433
  181. package/skills/playwright-cli/references/tracing.md +139 -139
  182. package/skills/playwright-cli/references/video-recording.md +143 -143
  183. package/skills/requesting-code-review/SKILL.md +101 -101
  184. package/skills/requesting-code-review/code-reviewer.md +168 -168
  185. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  186. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  187. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  188. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  189. package/skills/systematic-debugging/find-polluter.sh +63 -63
  190. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  191. package/skills/systematic-debugging/test-academic.md +14 -14
  192. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  193. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  194. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  195. package/skills/using-git-worktrees/SKILL.md +215 -215
  196. package/skills/verification-before-completion/SKILL.md +154 -154
  197. package/skills/webapp-testing/SKILL.md +19 -19
@@ -23,6 +23,9 @@ import {
23
23
  updateDagOutputViewportState,
24
24
  computeFollowLatest,
25
25
  isInteractiveGestureActive,
26
+ inspectorViewportRegionKey,
27
+ saveDagInspectorViewportState,
28
+ getDagInspectorViewportState,
26
29
  } from "../state.js";
27
30
  import {
28
31
  formatSessionEventTime,
@@ -37,7 +40,11 @@ const inspectorSelection = { dagRunId: null, node: null };
37
40
 
38
41
  function renderDagInspectorForCurrent() {
39
42
  if (!inspectorSelection.dagRunId || !inspectorSelection.node) return;
40
- renderDagInspector(inspectorSelection.dagRunId, inspectorSelection.node, false);
43
+ renderDagInspector(
44
+ inspectorSelection.dagRunId,
45
+ inspectorSelection.node,
46
+ false,
47
+ );
41
48
  }
42
49
 
43
50
  export function selectDagNode(dagRunId, nodeId) {
@@ -132,9 +139,7 @@ export function createDagInspectorResizeHandle(panel) {
132
139
  const updateRange = (requestedWidth) => {
133
140
  const range = setDagInspectorWidth(
134
141
  panel,
135
- requestedWidth ??
136
- uiState.dagInspectorWidth ??
137
- dagInspectorDefaultWidth(),
142
+ requestedWidth ?? uiState.dagInspectorWidth ?? dagInspectorDefaultWidth(),
138
143
  );
139
144
  handle.setAttribute("aria-valuemin", String(range.minimum));
140
145
  handle.setAttribute("aria-valuemax", String(range.maximum));
@@ -200,16 +205,27 @@ export function captureDagTimelineViewportState(panel) {
200
205
  }
201
206
 
202
207
  async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
203
- const evidence = cachedEvidence ?? await fetchJson(
204
- `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/spec-evidence`,
205
- );
206
- if (!content.isConnected || content.dataset.dagNodeId !== String(nodeId)) return;
208
+ const evidence =
209
+ cachedEvidence ??
210
+ (await fetchJson(
211
+ `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/spec-evidence`,
212
+ ));
213
+ if (
214
+ !content.isConnected ||
215
+ content.dataset.dagNodeId !== String(nodeId) ||
216
+ content.dataset.dagRunId !== String(dagRunId) ||
217
+ uiState.dagInspectorTab !== "spec-evidence"
218
+ ) {
219
+ return;
220
+ }
207
221
  if (uiState.specEvidenceDetail) {
208
222
  renderSpecEvidenceDetail(content, dagRunId, nodeId);
223
+ restoreInspectorContentScroll(content, dagRunId, nodeId, "spec-evidence");
209
224
  return;
210
225
  }
211
226
  if (!evidence) {
212
227
  content.appendChild(el("p", "empty", "无法加载规范证据。"));
228
+ restoreInspectorContentScroll(content, dagRunId, nodeId, "spec-evidence");
213
229
  return;
214
230
  }
215
231
  uiState.specEvidenceListEvidence = evidence;
@@ -238,7 +254,12 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
238
254
  }
239
255
  content.appendChild(statusSection);
240
256
 
241
- const appendListSection = (title, entries, renderEntry, emptyMessage = "") => {
257
+ const appendListSection = (
258
+ title,
259
+ entries,
260
+ renderEntry,
261
+ emptyMessage = "",
262
+ ) => {
242
263
  if (!entries?.length && !emptyMessage) return;
243
264
  const section = el("div", "spec-evidence-section");
244
265
  section.appendChild(el("h4", null, title));
@@ -253,36 +274,55 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
253
274
  content.appendChild(section);
254
275
  };
255
276
 
256
- appendListSection("注入的 Skill", evidence.skillInjection?.skills, (skill) => {
257
- const item = el("li", null);
258
- const icon = el("i", "ri-bookmark-line");
259
- icon.setAttribute("aria-hidden", "true");
260
- item.append(icon, document.createTextNode(` ${skill}`));
261
- return item;
262
- });
263
- appendListSection("已绑定任务源", evidence.sourceBinding?.sources, (source) => {
264
- const item = el("li", null);
265
- const button = document.createElement("button");
266
- button.type = "button";
267
- button.className = "spec-evidence-file-btn";
268
- button.setAttribute("data-source", "binding");
269
- button.setAttribute("data-path", source.path);
270
- button.id = `spec-evidence-btn-binding-${source.path}`;
271
- const icon = el("i", "ri-links-line");
272
- icon.setAttribute("aria-hidden", "true");
273
- button.append(
274
- icon,
275
- document.createTextNode(` ${source.kind}: `),
276
- el("code", null, source.path),
277
- document.createTextNode(` · sha256 ${source.sha256.slice(0, 12)}…`),
278
- );
279
- button.addEventListener("click", () =>
280
- onSpecEvidenceFileClick(content, dagRunId, nodeId, "binding", source.path, button),
281
- );
282
- item.appendChild(button);
283
- return item;
284
- });
285
- appendListSection("显式需求编号", evidence.sourceBinding?.requirementIds, (id) => el("li", null, id));
277
+ appendListSection(
278
+ "注入的 Skill",
279
+ evidence.skillInjection?.skills,
280
+ (skill) => {
281
+ const item = el("li", null);
282
+ const icon = el("i", "ri-bookmark-line");
283
+ icon.setAttribute("aria-hidden", "true");
284
+ item.append(icon, document.createTextNode(` ${skill}`));
285
+ return item;
286
+ },
287
+ );
288
+ appendListSection(
289
+ "已绑定任务源",
290
+ evidence.sourceBinding?.sources,
291
+ (source) => {
292
+ const item = el("li", null);
293
+ const button = document.createElement("button");
294
+ button.type = "button";
295
+ button.className = "spec-evidence-file-btn";
296
+ button.setAttribute("data-source", "binding");
297
+ button.setAttribute("data-path", source.path);
298
+ button.id = `spec-evidence-btn-binding-${source.path}`;
299
+ const icon = el("i", "ri-links-line");
300
+ icon.setAttribute("aria-hidden", "true");
301
+ button.append(
302
+ icon,
303
+ document.createTextNode(` ${source.kind}: `),
304
+ el("code", null, source.path),
305
+ document.createTextNode(` · sha256 ${source.sha256.slice(0, 12)}…`),
306
+ );
307
+ button.addEventListener("click", () =>
308
+ onSpecEvidenceFileClick(
309
+ content,
310
+ dagRunId,
311
+ nodeId,
312
+ "binding",
313
+ source.path,
314
+ button,
315
+ ),
316
+ );
317
+ item.appendChild(button);
318
+ return item;
319
+ },
320
+ );
321
+ appendListSection(
322
+ "显式需求编号",
323
+ evidence.sourceBinding?.requirementIds,
324
+ (id) => el("li", null, id),
325
+ );
286
326
  const openspecReadPaths = new Set(
287
327
  (evidence.openspecReads ?? []).map((entry) => entry.path),
288
328
  );
@@ -316,7 +356,14 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
316
356
  );
317
357
  }
318
358
  button.addEventListener("click", () =>
319
- onSpecEvidenceFileClick(content, dagRunId, nodeId, "read", read.path, button),
359
+ onSpecEvidenceFileClick(
360
+ content,
361
+ dagRunId,
362
+ nodeId,
363
+ "read",
364
+ read.path,
365
+ button,
366
+ ),
320
367
  );
321
368
  item.appendChild(button);
322
369
  return item;
@@ -329,7 +376,10 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
329
376
  const item = el("li", null);
330
377
  const icon = el("i", "ri-search-line");
331
378
  icon.setAttribute("aria-hidden", "true");
332
- item.append(icon, document.createTextNode(` ${search.tool}: ${search.query}`));
379
+ item.append(
380
+ icon,
381
+ document.createTextNode(` ${search.tool}: ${search.query}`),
382
+ );
333
383
  return item;
334
384
  },
335
385
  );
@@ -374,7 +424,14 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
374
424
  );
375
425
  }
376
426
  button.addEventListener("click", () =>
377
- onSpecEvidenceFileClick(content, dagRunId, nodeId, "read", read.path, button),
427
+ onSpecEvidenceFileClick(
428
+ content,
429
+ dagRunId,
430
+ nodeId,
431
+ "read",
432
+ read.path,
433
+ button,
434
+ ),
378
435
  );
379
436
  item.appendChild(button);
380
437
  return item;
@@ -391,13 +448,20 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
391
448
  const item = el("li", null);
392
449
  const icon = el("i", "ri-search-line");
393
450
  icon.setAttribute("aria-hidden", "true");
394
- item.append(icon, document.createTextNode(` ${search.tool}: ${search.query}`));
451
+ item.append(
452
+ icon,
453
+ document.createTextNode(` ${search.tool}: ${search.query}`),
454
+ );
395
455
  return item;
396
456
  },
397
457
  "unavailable:未观察到 openspec 检索。",
398
458
  );
399
459
 
400
- if (evidence.status === "source-bound" || evidence.status === "spec-injected" || evidence.status === "no-evidence") {
460
+ if (
461
+ evidence.status === "source-bound" ||
462
+ evidence.status === "spec-injected" ||
463
+ evidence.status === "no-evidence"
464
+ ) {
401
465
  const warning = el("div", "spec-evidence-warning");
402
466
  const icon = el("i", "ri-alert-line");
403
467
  icon.setAttribute("aria-hidden", "true");
@@ -407,12 +471,13 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
407
471
  evidence.status === "source-bound"
408
472
  ? "DAG 已绑定权威任务源及内容哈希,但尚未观察到成功的 read 工具调用;绑定证明上下文来源,read 事件才证明节点读取了完整文件。"
409
473
  : evidence.status === "no-evidence"
410
- ? "未观察到规范读取。只有 session event 中成功配对的 read 工具调用才算已读取,模型总结或文件名命中不能作为证明。"
411
- : "规范 skill 已注入,但未观察到规范读取或知识库查询;grep/find/ls 只能证明检索,不能替代 read。",
474
+ ? "未观察到规范读取。只有 session event 中成功配对的 read 工具调用才算已读取,模型总结或文件名命中不能作为证明。"
475
+ : "规范 skill 已注入,但未观察到规范读取或知识库查询;grep/find/ls 只能证明检索,不能替代 read。",
412
476
  ),
413
477
  );
414
478
  content.appendChild(warning);
415
479
  }
480
+ restoreInspectorContentScroll(content, dagRunId, nodeId, "spec-evidence");
416
481
  }
417
482
 
418
483
  /**
@@ -420,14 +485,26 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
420
485
  * view in the same spec-evidence panel (no modal), records the trigger button
421
486
  * so the back action can restore keyboard focus (AC-005).
422
487
  */
423
- function onSpecEvidenceFileClick(content, dagRunId, nodeId, source, relPath, button) {
488
+ function onSpecEvidenceFileClick(
489
+ content,
490
+ dagRunId,
491
+ nodeId,
492
+ source,
493
+ relPath,
494
+ button,
495
+ ) {
424
496
  openSpecEvidenceDetail(source, relPath, button);
425
497
  clearNode(content);
426
498
  const detail = uiState.specEvidenceDetail;
427
499
  renderSpecEvidenceDetail(content, dagRunId, nodeId);
428
500
  void (async () => {
429
501
  try {
430
- const data = await fetchSpecEvidenceFile(dagRunId, nodeId, source, relPath);
502
+ const data = await fetchSpecEvidenceFile(
503
+ dagRunId,
504
+ nodeId,
505
+ source,
506
+ relPath,
507
+ );
431
508
  if (
432
509
  !content.isConnected ||
433
510
  content.dataset.dagNodeId !== String(nodeId) ||
@@ -464,7 +541,12 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
464
541
  const detail = uiState.specEvidenceDetail;
465
542
  clearNode(content);
466
543
  if (!detail) {
467
- renderSpecEvidence(content, dagRunId, nodeId, uiState.specEvidenceListEvidence);
544
+ renderSpecEvidence(
545
+ content,
546
+ dagRunId,
547
+ nodeId,
548
+ uiState.specEvidenceListEvidence,
549
+ );
468
550
  return;
469
551
  }
470
552
 
@@ -481,7 +563,12 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
481
563
  clearNode(content);
482
564
  // Restore the list synchronously from the cached evidence so the user
483
565
  // sees no flicker and the trigger button is immediately focusable.
484
- renderSpecEvidence(content, dagRunId, nodeId, uiState.specEvidenceListEvidence);
566
+ renderSpecEvidence(
567
+ content,
568
+ dagRunId,
569
+ nodeId,
570
+ uiState.specEvidenceListEvidence,
571
+ );
485
572
  if (closed?.triggerId) {
486
573
  const trigger = document.getElementById(closed.triggerId);
487
574
  trigger?.focus();
@@ -516,7 +603,11 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
516
603
  loading.setAttribute("aria-live", "polite");
517
604
  wrapper.appendChild(loading);
518
605
  } else if (detail.error) {
519
- const error = el("p", "spec-evidence-detail-error", `加载失败:${detail.error}`);
606
+ const error = el(
607
+ "p",
608
+ "spec-evidence-detail-error",
609
+ `加载失败:${detail.error}`,
610
+ );
520
611
  error.setAttribute("role", "alert");
521
612
  wrapper.appendChild(error);
522
613
  } else if (detail.data) {
@@ -533,6 +624,7 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
533
624
  }
534
625
  }
535
626
  content.appendChild(wrapper);
627
+ restoreInspectorContentScroll(content, dagRunId, nodeId, "spec-evidence");
536
628
  }
537
629
 
538
630
  function sourceLabel(source) {
@@ -573,9 +665,7 @@ function appendCodeList(parent, title, values) {
573
665
 
574
666
  function renderDagNodeInputDetail(content, data) {
575
667
  const wrap = el("div", "node-input-root");
576
- wrap.appendChild(
577
- el("p", "node-input-semantics", UI_TEXT.inputFrozenLabel),
578
- );
668
+ wrap.appendChild(el("p", "node-input-semantics", UI_TEXT.inputFrozenLabel));
579
669
 
580
670
  if (data.summary) {
581
671
  const section = el("section", "node-input-section");
@@ -747,17 +837,10 @@ function renderDagNodeInputDetail(content, data) {
747
837
  data.promptFingerprint.sha256Prefix ?? "(parse failed)",
748
838
  null,
749
839
  );
750
- appendMetaRow(
751
- dl,
752
- "lengthChars",
753
- data.promptFingerprint.lengthChars,
754
- null,
755
- );
840
+ appendMetaRow(dl, "lengthChars", data.promptFingerprint.lengthChars, null);
756
841
  appendMetaRow(dl, "artifact", data.promptFingerprint.artifact, null);
757
842
  section.appendChild(dl);
758
- section.appendChild(
759
- el("p", "muted", UI_TEXT.inputFingerprintNote),
760
- );
843
+ section.appendChild(el("p", "muted", UI_TEXT.inputFingerprintNote));
761
844
  wrap.appendChild(section);
762
845
  }
763
846
 
@@ -793,6 +876,10 @@ async function renderDagNodeInput(content, dagRunId, node) {
793
876
  uiState.dagNodeInputLoading = true;
794
877
  uiState.dagNodeInputError = null;
795
878
 
879
+ // Preserve prior intent scroll across the short loading shell (AC-004).
880
+ saveInspectorContentScroll(content, dagRunId, nodeId, "input", {
881
+ preserveIntent: true,
882
+ });
796
883
  clearNode(content);
797
884
  const loading = el("p", "muted", UI_TEXT.inputLoading);
798
885
  loading.setAttribute("role", "status");
@@ -812,8 +899,11 @@ async function renderDagNodeInput(content, dagRunId, node) {
812
899
  return;
813
900
  }
814
901
 
815
- uiState.dagNodeInputLoading = false;
816
902
  clearNode(content);
903
+ const finishInputRender = () => {
904
+ restoreInspectorContentScroll(content, dagRunId, nodeId, "input");
905
+ uiState.dagNodeInputLoading = false;
906
+ };
817
907
 
818
908
  if (!result.ok) {
819
909
  const message =
@@ -826,6 +916,7 @@ async function renderDagNodeInput(content, dagRunId, node) {
826
916
  const error = el("p", "node-input-error", message);
827
917
  error.setAttribute("role", "alert");
828
918
  content.appendChild(error);
919
+ finishInputRender();
829
920
  return;
830
921
  }
831
922
 
@@ -845,57 +936,198 @@ async function renderDagNodeInput(content, dagRunId, node) {
845
936
  content.appendChild(el("p", "node-input-warning", String(warning)));
846
937
  }
847
938
  }
939
+ finishInputRender();
848
940
  return;
849
941
  }
850
942
 
851
943
  renderDagNodeInputDetail(content, data);
944
+ finishInputRender();
852
945
  }
853
946
 
854
- function fillDagInspectorContent(content, dagRunId, node) {
855
- const previousRenderedNode = content.dataset.dagNodeId ?? "";
856
- const previousRenderedRun = content.dataset.dagRunId ?? "";
857
- const nodeChanged =
858
- previousRenderedNode !== String(node.nodeId ?? "") ||
859
- previousRenderedRun !== String(dagRunId ?? "");
860
- if (nodeChanged) {
861
- // Drop any open spec-evidence preview + cached list so a stale A detail
862
- // cannot survive into node B (AC-007 regression guard).
863
- uiState.specEvidenceDetail = null;
864
- uiState.specEvidenceListEvidence = null;
865
- uiState.dagNodeInputData = null;
866
- uiState.dagNodeInputError = null;
947
+ function activeInspectorTab() {
948
+ return ["input", "timeline", "spec-evidence"].includes(
949
+ uiState.dagInspectorTab,
950
+ )
951
+ ? uiState.dagInspectorTab
952
+ : "output";
953
+ }
954
+
955
+ function saveInspectorContentScroll(
956
+ content,
957
+ dagRunId,
958
+ nodeId,
959
+ tab,
960
+ options = {},
961
+ ) {
962
+ if (!content) return null;
963
+ const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab);
964
+ const saved = saveDagInspectorViewportState(
965
+ regionKey,
966
+ content.scrollLeft,
967
+ content.scrollTop,
968
+ options,
969
+ );
970
+ if (tab === "input") {
971
+ uiState.dagNodeInputViewportState = updateDagOutputViewportState(
972
+ uiState.dagNodeInputViewportState,
973
+ regionKey,
974
+ saved?.scrollLeft ?? 0,
975
+ saved?.scrollTop ?? 0,
976
+ false,
977
+ );
978
+ } else if (tab === "output") {
979
+ const followLatest = computeFollowLatest(
980
+ content.scrollTop,
981
+ content.scrollHeight,
982
+ content.clientHeight,
983
+ );
984
+ uiState.dagNodeOutputViewportState = updateDagOutputViewportState(
985
+ uiState.dagNodeOutputViewportState,
986
+ regionKey,
987
+ content.scrollLeft,
988
+ content.scrollTop,
989
+ followLatest,
990
+ );
991
+ } else if (tab === "spec-evidence") {
992
+ uiState.dagNodeSpecEvidenceViewportState = saved;
993
+ }
994
+ return saved;
995
+ }
996
+
997
+ function restoreInspectorContentScroll(content, dagRunId, nodeId, tab) {
998
+ if (!content) return;
999
+ const regionKey = inspectorViewportRegionKey(dagRunId, nodeId, tab);
1000
+ const apply = () => {
1001
+ if (
1002
+ !content.isConnected ||
1003
+ content.dataset.dagNodeId !== String(nodeId ?? "") ||
1004
+ content.dataset.dagRunId !== String(dagRunId ?? "") ||
1005
+ activeInspectorTab() !== tab
1006
+ ) {
1007
+ return;
867
1008
  }
868
- const activeTab = ["input", "timeline", "spec-evidence"].includes(
869
- uiState.dagInspectorTab,
870
- )
871
- ? uiState.dagInspectorTab
872
- : "output";
873
- content.id = `dag-inspector-panel-${activeTab}`;
874
- content.setAttribute("role", "tabpanel");
875
- content.setAttribute("aria-labelledby", `dag-inspector-tab-${activeTab}`);
876
- content.dataset.dagNodeId = String(node.nodeId ?? "");
877
- content.dataset.dagRunId = String(dagRunId ?? "");
878
- // Drop timeline-only classes when switching tabs so output layout stays clean.
879
- content.className = "dag-inspector-content";
880
- if (activeTab === "spec-evidence") {
881
- void renderSpecEvidence(content, dagRunId, node.nodeId);
882
- } else if (activeTab === "timeline") {
883
- renderSessionTimeline(content, node.nodeId);
884
- } else if (activeTab === "input") {
885
- void renderDagNodeInput(content, dagRunId, node);
886
- } else if (!node.outputPreview && !node.errorPreview) {
887
- content.appendChild(el("p", "empty", UI_TEXT.noOutput));
888
- } else {
889
- if (node.outputPreview)
890
- content.appendChild(renderMarkdown(node.outputPreview));
891
- if (node.errorPreview) {
892
- const error = el("section", "markdown-error");
893
- error.appendChild(el("p", "markdown-error-title", "错误输出"));
894
- error.appendChild(renderMarkdown(node.errorPreview));
895
- content.appendChild(error);
1009
+ if (tab === "output") {
1010
+ const saved = uiState.dagNodeOutputViewportState;
1011
+ if (saved?.regionKey === regionKey && saved.followLatest) {
1012
+ content.scrollTop = content.scrollHeight;
1013
+ content.scrollLeft = saved.scrollLeft ?? 0;
1014
+ return;
896
1015
  }
897
1016
  }
1017
+ const saved =
1018
+ getDagInspectorViewportState(regionKey) ??
1019
+ (tab === "input" &&
1020
+ uiState.dagNodeInputViewportState?.regionKey === regionKey
1021
+ ? uiState.dagNodeInputViewportState
1022
+ : null) ??
1023
+ (tab === "output" &&
1024
+ uiState.dagNodeOutputViewportState?.regionKey === regionKey
1025
+ ? uiState.dagNodeOutputViewportState
1026
+ : null) ??
1027
+ (tab === "spec-evidence" &&
1028
+ uiState.dagNodeSpecEvidenceViewportState?.regionKey === regionKey
1029
+ ? uiState.dagNodeSpecEvidenceViewportState
1030
+ : null);
1031
+ content.scrollLeft = saved?.scrollLeft ?? 0;
1032
+ content.scrollTop = saved?.scrollTop ?? 0;
1033
+ };
1034
+ apply();
1035
+ if (typeof requestAnimationFrame === "function") {
1036
+ requestAnimationFrame(apply);
1037
+ }
1038
+ }
1039
+
1040
+ function ensureInspectorContentScrollCapture(content, dagRunId, nodeId, tab) {
1041
+ if (!content || content.dataset.scrollCaptureBound === "1") return;
1042
+ content.dataset.scrollCaptureBound = "1";
1043
+ content.addEventListener(
1044
+ "scroll",
1045
+ () => {
1046
+ const runId = content.dataset.dagRunId;
1047
+ const nId = content.dataset.dagNodeId;
1048
+ const active = activeInspectorTab();
1049
+ if (!runId || !nId || !active) return;
1050
+ saveInspectorContentScroll(content, runId, nId, active, {
1051
+ // Clearing a tall input pane for its loading shell can emit a native
1052
+ // scroll-to-zero event. Preserve the user's prior intent until the
1053
+ // asynchronous content has been restored.
1054
+ preserveIntent:
1055
+ active === "input" && Boolean(uiState.dagNodeInputLoading),
1056
+ });
1057
+ },
1058
+ { passive: true },
1059
+ );
1060
+ // Touch current context so callers always have a key even before first scroll.
1061
+ saveInspectorContentScroll(content, dagRunId, nodeId, tab, {
1062
+ preserveIntent: true,
1063
+ });
1064
+ }
1065
+
1066
+ function fillDagInspectorContent(content, dagRunId, node) {
1067
+ const previousRenderedNode = content.dataset.dagNodeId ?? "";
1068
+ const previousRenderedRun = content.dataset.dagRunId ?? "";
1069
+ const previousTab = content.dataset.inspectorTab ?? "";
1070
+ const nodeChanged =
1071
+ previousRenderedNode !== String(node.nodeId ?? "") ||
1072
+ previousRenderedRun !== String(dagRunId ?? "");
1073
+ if (nodeChanged) {
1074
+ // Drop any open spec-evidence preview + cached list so a stale A detail
1075
+ // cannot survive into node B (AC-007 regression guard).
1076
+ uiState.specEvidenceDetail = null;
1077
+ uiState.specEvidenceListEvidence = null;
1078
+ uiState.dagNodeInputData = null;
1079
+ uiState.dagNodeInputError = null;
1080
+ }
1081
+ const activeTab = activeInspectorTab();
1082
+ // Capture outgoing context scroll before datasets flip (AC-005 isolation).
1083
+ if (
1084
+ previousRenderedNode &&
1085
+ previousRenderedRun &&
1086
+ (previousTab || activeTab)
1087
+ ) {
1088
+ saveInspectorContentScroll(
1089
+ content,
1090
+ previousRenderedRun,
1091
+ previousRenderedNode,
1092
+ previousTab || activeTab,
1093
+ { preserveIntent: true },
1094
+ );
1095
+ }
1096
+ content.id = `dag-inspector-panel-${activeTab}`;
1097
+ content.setAttribute("role", "tabpanel");
1098
+ content.setAttribute("aria-labelledby", `dag-inspector-tab-${activeTab}`);
1099
+ content.dataset.dagNodeId = String(node.nodeId ?? "");
1100
+ content.dataset.dagRunId = String(dagRunId ?? "");
1101
+ content.dataset.inspectorTab = activeTab;
1102
+ // Drop timeline-only classes when switching tabs so output layout stays clean.
1103
+ content.className = "dag-inspector-content";
1104
+ ensureInspectorContentScrollCapture(
1105
+ content,
1106
+ dagRunId,
1107
+ node.nodeId,
1108
+ activeTab,
1109
+ );
1110
+ if (activeTab === "spec-evidence") {
1111
+ void renderSpecEvidence(content, dagRunId, node.nodeId);
1112
+ } else if (activeTab === "timeline") {
1113
+ renderSessionTimeline(content, node.nodeId);
1114
+ } else if (activeTab === "input") {
1115
+ void renderDagNodeInput(content, dagRunId, node);
1116
+ } else if (!node.outputPreview && !node.errorPreview) {
1117
+ content.appendChild(el("p", "empty", UI_TEXT.noOutput));
1118
+ restoreInspectorContentScroll(content, dagRunId, node.nodeId, "output");
1119
+ } else {
1120
+ if (node.outputPreview)
1121
+ content.appendChild(renderMarkdown(node.outputPreview));
1122
+ if (node.errorPreview) {
1123
+ const error = el("section", "markdown-error");
1124
+ error.appendChild(el("p", "markdown-error-title", "错误输出"));
1125
+ error.appendChild(renderMarkdown(node.errorPreview));
1126
+ content.appendChild(error);
1127
+ }
1128
+ restoreInspectorContentScroll(content, dagRunId, node.nodeId, "output");
898
1129
  }
1130
+ }
899
1131
 
900
1132
  function buildDagInspectorHeader(dagRunId, node) {
901
1133
  const header = el("div", "dag-inspector-header");
@@ -990,25 +1222,18 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
990
1222
  }
991
1223
 
992
1224
  function dagInspectorSignature(dagRunId, node) {
993
- const inputRevision =
994
- uiState.dagInspectorTab === "input"
995
- ? [
996
- uiState.dagNodeInputIdentity,
997
- uiState.dagNodeInputLoading,
998
- uiState.dagNodeInputError ?? "",
999
- uiState.dagNodeInputData?.availabilityReason ?? "",
1000
- uiState.dagNodeInputData?.promptFingerprint?.sha256Prefix ?? "",
1001
- node.status ?? "",
1002
- ]
1003
- : null;
1225
+ const tab = activeInspectorTab();
1226
+ const contentRevision =
1227
+ tab === "output"
1228
+ ? [node.outputPreview ?? "", node.errorPreview ?? ""]
1229
+ : tab === "timeline"
1230
+ ? [uiState.sessionEventOffset]
1231
+ : [node.status ?? ""];
1004
1232
  return JSON.stringify([
1005
1233
  dagRunId,
1006
1234
  node.nodeId,
1007
- uiState.dagInspectorTab,
1008
- node.outputPreview ?? "",
1009
- node.errorPreview ?? "",
1010
- uiState.dagInspectorTab === "timeline" ? uiState.sessionEventOffset : null,
1011
- inputRevision,
1235
+ tab,
1236
+ contentRevision,
1012
1237
  ]);
1013
1238
  }
1014
1239
 
@@ -1042,36 +1267,23 @@ export function renderDagInspector(dagRunId, node, initial = true) {
1042
1267
  panel.classList.add("is-open");
1043
1268
  panel.setAttribute("aria-hidden", "false");
1044
1269
  syncDagInspectorHeader(panel, dagRunId, node);
1270
+ // AC-003: signature unchanged → keep content DOM; header already synced.
1045
1271
  if (panel.dataset.contentSignature === nextContentSignature) return;
1046
1272
  const previousNode = existingContent.dataset.dagNodeId ?? "";
1047
1273
  const previousRun = existingContent.dataset.dagRunId ?? "";
1274
+ const previousTab =
1275
+ existingContent.dataset.inspectorTab ?? activeInspectorTab();
1048
1276
  const nodeChanged =
1049
1277
  previousNode !== String(node.nodeId ?? "") ||
1050
1278
  previousRun !== String(dagRunId ?? "");
1051
- const activeTab = ["input", "timeline", "spec-evidence"].includes(
1052
- uiState.dagInspectorTab,
1053
- )
1054
- ? uiState.dagInspectorTab
1055
- : "output";
1056
- const isInputTab = activeTab === "input";
1057
- const followLatest = isInputTab
1058
- ? false
1059
- : nodeChanged ||
1060
- computeFollowLatest(
1061
- existingContent.scrollTop,
1062
- existingContent.scrollHeight,
1063
- existingContent.clientHeight,
1064
- );
1065
- const regionKey = `dag:${dagRunId}:${node.nodeId}:${isInputTab ? "input" : "output"}`;
1066
- const viewportUpdater = isInputTab
1067
- ? "dagNodeInputViewportState"
1068
- : "dagNodeOutputViewportState";
1069
- uiState[viewportUpdater] = updateDagOutputViewportState(
1070
- uiState[viewportUpdater],
1071
- regionKey,
1072
- existingContent.scrollLeft,
1073
- existingContent.scrollTop,
1074
- followLatest,
1279
+ const activeTab = activeInspectorTab();
1280
+ // Capture scroll for the outgoing context before rebuild.
1281
+ saveInspectorContentScroll(
1282
+ existingContent,
1283
+ previousRun || dagRunId,
1284
+ previousNode || node.nodeId,
1285
+ previousTab,
1286
+ { preserveIntent: true },
1075
1287
  );
1076
1288
  const nestedViewportState = [
1077
1289
  ...existingContent.querySelectorAll("pre"),
@@ -1081,19 +1293,24 @@ export function renderDagInspector(dagRunId, node, initial = true) {
1081
1293
  }));
1082
1294
  clearNode(existingContent);
1083
1295
  fillDagInspectorContent(existingContent, dagRunId, node);
1084
- if (followLatest) {
1085
- existingContent.scrollTop = existingContent.scrollHeight;
1086
- } else {
1087
- const saved = uiState[viewportUpdater];
1088
- existingContent.scrollTop = nodeChanged ? 0 : (saved?.scrollTop ?? 0);
1296
+ // Async tabs restore themselves after fetch; sync tabs restore here.
1297
+ if (activeTab === "output" || activeTab === "timeline") {
1298
+ restoreInspectorContentScroll(
1299
+ existingContent,
1300
+ dagRunId,
1301
+ node.nodeId,
1302
+ activeTab,
1303
+ );
1089
1304
  }
1090
- for (const [index, pre] of [
1091
- ...existingContent.querySelectorAll("pre"),
1092
- ].entries()) {
1093
- const previous = nestedViewportState[index];
1094
- if (!previous) continue;
1095
- pre.scrollLeft = previous.scrollLeft;
1096
- pre.scrollTop = previous.scrollTop;
1305
+ if (!nodeChanged && previousTab === activeTab) {
1306
+ for (const [index, pre] of [
1307
+ ...existingContent.querySelectorAll("pre"),
1308
+ ].entries()) {
1309
+ const previous = nestedViewportState[index];
1310
+ if (!previous) continue;
1311
+ pre.scrollLeft = previous.scrollLeft;
1312
+ pre.scrollTop = previous.scrollTop;
1313
+ }
1097
1314
  }
1098
1315
  panel.dataset.contentSignature = nextContentSignature;
1099
1316
  return;