@tea-agent/loop-agent 0.10.0 → 0.11.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.
- package/AGENTS.md +10 -2
- package/CHANGELOG.md +67 -25
- package/README.md +82 -11
- package/dist/application/dag/args.js +1 -12
- package/dist/application/dag/generate-task-dag.js +23 -2
- package/dist/application/dag/run-dag.js +1 -27
- package/dist/application/dag/validate-dag.js +2 -2
- package/dist/application/loop/run-action.js +0 -4
- package/dist/cli/command-definitions.js +44 -16
- package/dist/cli/program.js +40 -23
- package/dist/cli/update/notifier.js +117 -0
- package/dist/cli/update/npm-client.js +151 -0
- package/dist/cli/update/policy.js +58 -0
- package/dist/cli/update/state.js +68 -0
- package/dist/cli.js +33 -0
- package/dist/commands/cursor-prompt.js +42 -82
- package/dist/commands/dag-approve.js +36 -0
- package/dist/commands/delegate.js +75 -77
- package/dist/commands/doctor.js +0 -18
- package/dist/commands/init.js +476 -91
- package/dist/commands/instructions.js +7 -10
- package/dist/commands/loop.js +4 -20
- package/dist/commands/plan.js +50 -0
- package/dist/executors/config-core.js +0 -51
- package/dist/executors/dag-pi-executor.js +1 -1
- package/dist/executors/dag.js +0 -1
- package/dist/executors/index.js +0 -2
- package/dist/executors/model-routing.js +9 -9
- package/dist/executors/shell-executor.js +1 -1
- package/dist/governance/checks.js +6 -3
- package/dist/governance/exec-plans.js +545 -0
- package/dist/governance/manifest-types.js +24 -2
- package/dist/infrastructure/harness/loop-action-store.js +0 -3
- package/dist/records/harvest.js +2 -23
- package/dist/records/one-shot-runs.js +1 -1
- package/dist/shared/artifacts-core.js +24 -5
- package/dist/shared/output-truncation.js +37 -0
- package/dist/shared/package-metadata.js +353 -0
- package/dist/{executors/cursor-executor.js → sidecars/cursor-prompt/executor.js} +2 -42
- package/dist/sidecars/cursor-prompt/index.js +3 -0
- package/dist/sidecars/cursor-prompt/stream.js +121 -0
- package/dist/task/config-types.js +28 -12
- package/dist/task/delegate.js +9 -21
- package/dist/task/runtime.js +1 -2
- package/dist/worker/cli.js +29 -2
- package/dist/worker/delivery/final-verification.js +47 -11
- package/dist/worker/delivery/package.js +63 -10
- package/dist/worker/feature/run.js +60 -8
- package/dist/worker/loop-agent/loop-agent-client.js +329 -126
- package/dist/worker/observability/read-model.js +27 -1
- package/dist/worker/observe/static/app.js +326 -45
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/styles.css +5 -4
- package/dist/worker/preflight.js +49 -1
- package/dist/worker/run-task/run-task.js +22 -12
- package/dist/worker/runner/run-ready.js +76 -12
- package/dist/worker/task-spec/schema.js +0 -1
- package/dist/workflows/dag/convergence/controller.js +1 -1
- package/dist/workflows/dag/executor-registry.js +0 -2
- package/dist/workflows/dag/init-hybrid.js +402 -25
- package/dist/workflows/dag/node-execution.js +61 -7
- package/dist/workflows/dag/runner.js +45 -17
- package/dist/workflows/dag/scheduler.js +7 -2
- package/dist/workflows/dag/sdd-embedded.js +128 -0
- package/dist/workflows/dag/skill-instructions.js +5 -4
- package/dist/workflows/dag/skill-snapshot.js +527 -0
- package/dist/workflows/dag/types.js +42 -9
- package/dist/workflows/dag/validate.js +5 -8
- package/dist/workflows/loop/actions/dag-action.js +0 -2
- package/dist/workflows/loop/actions/shared.js +1 -1
- package/dist/workflows/loop/actions.js +14 -31
- package/dist/workflows/loop/benchmark.js +1 -1
- package/dist/workflows/loop/index.js +1 -1
- package/dist/workflows/loop/policy/auto-policy.js +22 -14
- package/dist/workflows/loop/policy/path-patterns.js +13 -0
- package/docs/README.md +36 -33
- package/docs/agent-dag-recovery-playbook.md +1 -1
- package/docs/agent-dag-runner.md +2 -2
- package/docs/architecture/README.md +26 -0
- package/docs/architecture/dag-execution.md +134 -0
- package/docs/architecture/evolution.md +52 -0
- package/docs/architecture/facts-and-state.md +58 -0
- package/docs/architecture/runtime-boundaries.md +45 -17
- package/docs/architecture/system-overview.md +93 -0
- package/docs/architecture/worker-and-feature.md +81 -0
- package/docs/cursor-prompt-sidecar.md +36 -0
- package/docs/decisions/README.md +13 -1
- package/docs/design/README.md +42 -21
- package/docs/development-principles.md +2 -2
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/feature-workflow.md +50 -4
- package/docs/harness-methodology-debugging.md +1 -1
- package/docs/harness-methodology-tdd.md +3 -3
- package/docs/init-surface.manifest.json +60 -25
- package/docs/loop-agent-harness.md +28 -4
- package/docs/progress/README.md +32 -1
- package/docs/reports/README.md +84 -18
- package/docs/skills/README.md +2 -1
- package/docs/skills/vetted-skill-registry.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +6 -6
- package/docs/templates/agent-dag.base.json +0 -5
- package/docs/templates/agent-dag.final-verification.json +0 -5
- package/docs/templates/agent-dag.schema.json +1 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -6
- package/docs/templates/frontend-design-contract.md +33 -0
- package/docs/templates/frontend-task-constraints.md +25 -0
- package/docs/templates/frontend-task-requirement.md +61 -0
- package/docs/templates/harness.schema.json +10 -12
- package/docs/templates/hybrid-dag.json +1 -6
- package/docs/templates/interactive-ui-round2-experiment.md +1 -1
- package/docs/templates/product-line/task.yaml +0 -1
- package/docs/templates/project-start-checklist.md +2 -2
- package/docs/templates/worker-dogfood-evidence.md +28 -0
- package/docs/templates/worker-dogfood-setup.md +20 -0
- package/docs/verification-matrix.md +10 -0
- package/examples/decision-gate-agent-dag.json +87 -33
- package/examples/example-dag.json +0 -5
- package/examples/hybrid-loop-agent-dag.json +0 -5
- package/harness.json +7 -15
- package/package.json +22 -46
- package/scripts/check-product-line-docs.sh +10 -7
- package/skills/agent-worker/SKILL.md +37 -0
- package/skills/agent-worker/references/agent-worker-operator.md +43 -0
- package/skills/frontend-design-review/SKILL.md +59 -0
- package/skills/frontend-design-review/references/review-checklist.md +37 -0
- package/skills/frontend-implementation/SKILL.md +51 -0
- package/skills/frontend-implementation/references/code-standards.md +34 -0
- package/skills/frontend-implementation/references/design-spec.md +46 -0
- package/skills/frontend-implementation/references/node-contracts.md +32 -0
- package/skills/frontend-review/SKILL.md +53 -0
- package/skills/frontend-review/references/review-findings.md +42 -0
- package/skills/frontend-verification/SKILL.md +40 -0
- package/skills/frontend-verification/references/verification-checklist.md +56 -0
- package/skills/grill-me/SKILL.md +10 -0
- package/skills/grill-with-docs/SKILL.md +88 -0
- package/skills/grill-with-docs/adr-format.md +47 -0
- package/skills/grill-with-docs/context-format.md +60 -0
- package/skills/loop-agent/SKILL.md +11 -9
- package/skills/loop-agent/references/command-reference.md +13 -15
- package/skills/loop-agent/references/docs-converge.md +126 -0
- package/skills/loop-agent/references/harness-policy.md +7 -7
- package/skills/loop-agent/references/hybrid-dag.md +15 -18
- package/skills/loop-agent/references/long-running-loop.md +4 -6
- package/skills/loop-agent/references/multi-worktree.md +6 -6
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +14 -11
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/using-git-worktrees/SKILL.md +215 -0
- package/dist/commands/cursor-worker.js +0 -43
- package/dist/cursor-worker-entry.js +0 -8
- package/dist/executors/cursor-artifacts.js +0 -33
- package/dist/executors/cursor-execution-log.js +0 -81
- package/dist/executors/cursor-executor-artifacts.js +0 -134
- package/dist/executors/cursor-run.js +0 -115
- package/dist/executors/cursor-tool.js +0 -94
- package/dist/executors/cursor-worker-client.js +0 -223
- package/dist/executors/cursor-worker-protocol.js +0 -18
- package/dist/executors/cursor-worker-server.js +0 -54
- package/dist/executors/cursor-worker.js +0 -3
- package/dist/executors/cursor.js +0 -6
- package/dist/executors/dag-cursor-executor.js +0 -87
- package/dist/workflows/loop/actions/cursor-fix.js +0 -191
- package/dist/workflows/loop/policy/cursor-fix-policy.js +0 -31
- package/docs/cursor-executor-usage.md +0 -25
- package/docs/dynamic-workflow-dag-engine-roadmap.md +0 -1749
|
@@ -9,6 +9,7 @@ const DAG_INSPECTOR_MAX_WIDTH = 680;
|
|
|
9
9
|
const DAG_INSPECTOR_RIGHT_OFFSET = 24;
|
|
10
10
|
const DAG_INSPECTOR_VIEWPORT_MARGIN = 48;
|
|
11
11
|
const DAG_INSPECTOR_KEYBOARD_STEP = 24;
|
|
12
|
+
const OUTPUT_FOLLOW_LATEST_PX = 48;
|
|
12
13
|
|
|
13
14
|
const UI_TEXT = {
|
|
14
15
|
dashboard: "总览",
|
|
@@ -163,6 +164,9 @@ let dagInspectorWidth = null;
|
|
|
163
164
|
let dagGraphViewportState = null;
|
|
164
165
|
const dagGraphViewportStates = new Map();
|
|
165
166
|
let dagTimelineViewportState = null;
|
|
167
|
+
let dagNodeOutputViewportState = null;
|
|
168
|
+
let runOutputViewportState = null;
|
|
169
|
+
let dagInspectorContentSignature = null;
|
|
166
170
|
let pollingGeneration = 0;
|
|
167
171
|
|
|
168
172
|
function isPageVisible() {
|
|
@@ -325,6 +329,66 @@ export function updateDagTimelineViewportState(
|
|
|
325
329
|
};
|
|
326
330
|
}
|
|
327
331
|
|
|
332
|
+
// Unified follow-latest helper for DAG node output and Worker output scroll
|
|
333
|
+
// regions. Returns true only when the user is parked within `threshold` px of
|
|
334
|
+
// the bottom, mirroring the existing process-timeline behaviour.
|
|
335
|
+
export function computeFollowLatest(
|
|
336
|
+
scrollTop,
|
|
337
|
+
scrollHeight,
|
|
338
|
+
clientHeight,
|
|
339
|
+
threshold = OUTPUT_FOLLOW_LATEST_PX,
|
|
340
|
+
) {
|
|
341
|
+
const top = Number(scrollTop);
|
|
342
|
+
const height = Number(scrollHeight);
|
|
343
|
+
const client = Number(clientHeight);
|
|
344
|
+
if (!Number.isFinite(top) || !Number.isFinite(height) || !Number.isFinite(client)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
if (height <= 0 || client <= 0) return false;
|
|
348
|
+
return height - top - client < Math.max(0, Number(threshold) || 0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Generalised viewport-state record keyed by `regionKey`, so the same helper
|
|
352
|
+
// serves the DAG node inspector output ("dag:<runId>:<nodeId>:output") and the
|
|
353
|
+
// Worker run output ("run:<runId>:output").
|
|
354
|
+
export function updateDagOutputViewportState(
|
|
355
|
+
_previous,
|
|
356
|
+
regionKey,
|
|
357
|
+
scrollLeft,
|
|
358
|
+
scrollTop,
|
|
359
|
+
followLatest,
|
|
360
|
+
) {
|
|
361
|
+
return {
|
|
362
|
+
regionKey,
|
|
363
|
+
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
364
|
+
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
365
|
+
followLatest: Boolean(followLatest),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Poll-render decision helpers. Initial paints always rebuild the shell; a
|
|
370
|
+
// non-initial refresh reuses the existing viewport/inspector DOM identity so
|
|
371
|
+
// active pointer-capture targets (scrollbar / resize handle) survive.
|
|
372
|
+
export function shouldReuseDagDetailShell(initial, hasExistingViewport) {
|
|
373
|
+
return !Boolean(initial) && Boolean(hasExistingViewport);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function shouldPreserveDiagnosticsOpen(initial, existingOpen) {
|
|
377
|
+
if (initial) return false;
|
|
378
|
+
return Boolean(existingOpen);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Detects an in-progress resize/scroll gesture so the poll renderer can defer
|
|
382
|
+
// tearing down the interactive shell until the gesture releases.
|
|
383
|
+
export function isInteractiveGestureActive(bodyClassList) {
|
|
384
|
+
const list = bodyClassList;
|
|
385
|
+
if (!list || typeof list.contains !== "function") return false;
|
|
386
|
+
return (
|
|
387
|
+
list.contains("is-resizing-dag-inspector") ||
|
|
388
|
+
list.contains("is-resizing-dag-graph")
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
328
392
|
export function parseHashRoute(hashValue) {
|
|
329
393
|
const hash = hashValue.replace(/^#/, "") || "/";
|
|
330
394
|
const batchMatch = /^\/batch\/([^/]+)$/.exec(hash);
|
|
@@ -882,8 +946,11 @@ function createDagInspectorResizeHandle(panel) {
|
|
|
882
946
|
function captureDagTimelineViewportState(panel) {
|
|
883
947
|
const scrollEl = panel.querySelector(".process-timeline-scroll");
|
|
884
948
|
if (scrollEl?.dataset.dagRunId && scrollEl.dataset.nodeId) {
|
|
885
|
-
const followLatest =
|
|
886
|
-
scrollEl.
|
|
949
|
+
const followLatest = computeFollowLatest(
|
|
950
|
+
scrollEl.scrollTop,
|
|
951
|
+
scrollEl.scrollHeight,
|
|
952
|
+
scrollEl.clientHeight,
|
|
953
|
+
);
|
|
887
954
|
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
888
955
|
dagTimelineViewportState,
|
|
889
956
|
scrollEl.dataset.dagRunId,
|
|
@@ -943,8 +1010,11 @@ function renderSessionTimeline(content, nodeId) {
|
|
|
943
1010
|
content.addEventListener(
|
|
944
1011
|
"scroll",
|
|
945
1012
|
() => {
|
|
946
|
-
const followLatest =
|
|
947
|
-
content.
|
|
1013
|
+
const followLatest = computeFollowLatest(
|
|
1014
|
+
content.scrollTop,
|
|
1015
|
+
content.scrollHeight,
|
|
1016
|
+
content.clientHeight,
|
|
1017
|
+
);
|
|
948
1018
|
dagTimelineViewportState = updateDagTimelineViewportState(
|
|
949
1019
|
dagTimelineViewportState,
|
|
950
1020
|
currentDagRunId,
|
|
@@ -1197,19 +1267,42 @@ function svgEl(tag, attrs = {}) {
|
|
|
1197
1267
|
return node;
|
|
1198
1268
|
}
|
|
1199
1269
|
|
|
1200
|
-
function renderDagGraph(dag, dagRunId) {
|
|
1270
|
+
function renderDagGraph(dag, dagRunId, existingWrap) {
|
|
1201
1271
|
const nodes = dag.nodes ?? [];
|
|
1202
1272
|
const edges = dag.edges ?? [];
|
|
1203
1273
|
const layout = layoutDag(nodes, edges, dag.ranks ?? []);
|
|
1204
|
-
const
|
|
1274
|
+
const existingViewport = existingWrap?.querySelector?.(":scope > .dag-graph-viewport") ?? null;
|
|
1275
|
+
const reuseViewport =
|
|
1276
|
+
Boolean(existingWrap?.isConnected) &&
|
|
1277
|
+
Boolean(existingViewport?.isConnected) &&
|
|
1278
|
+
existingViewport.classList?.contains("dag-graph-viewport") &&
|
|
1279
|
+
existingViewport.dataset?.dagRunId === dagRunId;
|
|
1280
|
+
// reuse-dag-graph-wrap: the live viewport remains attached to the document
|
|
1281
|
+
// during polling. Only its toolbar and SVG contents are patched in place.
|
|
1282
|
+
const wrap = reuseViewport ? existingWrap : el("div", "dag-graph-wrap");
|
|
1205
1283
|
const toolbar = el("div", "dag-graph-toolbar");
|
|
1206
1284
|
toolbar.appendChild(el("span", "dag-graph-summary", `${nodes.length} 节点 · ${layout.edges.length} 条依赖`));
|
|
1207
1285
|
const reset = el("button", "dag-graph-reset", "重置视图");
|
|
1208
1286
|
toolbar.appendChild(reset);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1287
|
+
const previousToolbar = reuseViewport
|
|
1288
|
+
? wrap.querySelector(":scope > .dag-graph-toolbar")
|
|
1289
|
+
: null;
|
|
1290
|
+
if (previousToolbar) wrap.replaceChild(toolbar, previousToolbar);
|
|
1291
|
+
else wrap.prepend(toolbar);
|
|
1292
|
+
const previousFallback = reuseViewport
|
|
1293
|
+
? wrap.querySelector(":scope > .dag-graph-fallback")
|
|
1294
|
+
: null;
|
|
1295
|
+
if (edges.length === 0) {
|
|
1296
|
+
const fallback = previousFallback ?? el("p", "dag-graph-fallback");
|
|
1297
|
+
fallback.textContent = "该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。";
|
|
1298
|
+
if (!previousFallback) wrap.insertBefore(fallback, existingViewport ?? null);
|
|
1299
|
+
} else {
|
|
1300
|
+
previousFallback?.remove();
|
|
1301
|
+
}
|
|
1302
|
+
const viewport =
|
|
1303
|
+
reuseViewport && existingViewport ? existingViewport : el("div", "dag-graph-viewport");
|
|
1212
1304
|
viewport.dataset.dagRunId = dagRunId;
|
|
1305
|
+
clearNode(viewport);
|
|
1213
1306
|
const svg = svgEl("svg", { class: "dag-graph", viewBox: `0 0 ${layout.width} ${layout.height}`, width: layout.width, height: layout.height, role: "img", "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点` });
|
|
1214
1307
|
const defs = svgEl("defs");
|
|
1215
1308
|
const marker = svgEl("marker", { id: "dag-arrow", viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: "auto-start-reverse" });
|
|
@@ -1236,26 +1329,39 @@ function renderDagGraph(dag, dagRunId) {
|
|
|
1236
1329
|
group.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(); } });
|
|
1237
1330
|
svg.appendChild(group);
|
|
1238
1331
|
}
|
|
1239
|
-
viewport.appendChild(svg);
|
|
1332
|
+
viewport.appendChild(svg);
|
|
1333
|
+
if (!reuseViewport) wrap.appendChild(viewport);
|
|
1240
1334
|
const graphViewportState =
|
|
1241
1335
|
dagGraphViewportStates.get(dagRunId) ??
|
|
1242
1336
|
(dagGraphViewportState?.dagRunId === dagRunId
|
|
1243
1337
|
? dagGraphViewportState
|
|
1244
1338
|
: null);
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
dagGraphViewportState
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1339
|
+
if (!reuseViewport) {
|
|
1340
|
+
viewport.addEventListener(
|
|
1341
|
+
"scroll",
|
|
1342
|
+
() => {
|
|
1343
|
+
dagGraphViewportState = updateDagGraphViewportState(
|
|
1344
|
+
dagGraphViewportState,
|
|
1345
|
+
dagRunId,
|
|
1346
|
+
viewport.scrollLeft,
|
|
1347
|
+
viewport.scrollTop,
|
|
1348
|
+
);
|
|
1349
|
+
dagGraphViewportStates.set(dagRunId, dagGraphViewportState);
|
|
1350
|
+
},
|
|
1351
|
+
{ passive: true },
|
|
1352
|
+
);
|
|
1353
|
+
viewport.addEventListener("pointerdown", () => {
|
|
1354
|
+
document.body.classList.add("is-resizing-dag-graph");
|
|
1355
|
+
const finish = () => {
|
|
1356
|
+
document.body.classList.remove("is-resizing-dag-graph");
|
|
1357
|
+
window.removeEventListener("pointerup", finish, true);
|
|
1358
|
+
window.removeEventListener("pointercancel", finish, true);
|
|
1359
|
+
};
|
|
1360
|
+
window.addEventListener("pointerup", finish, true);
|
|
1361
|
+
window.addEventListener("pointercancel", finish, true);
|
|
1362
|
+
}, { passive: true });
|
|
1363
|
+
}
|
|
1364
|
+
if (!reuseViewport && graphViewportState) {
|
|
1259
1365
|
const restoreViewport = () => {
|
|
1260
1366
|
viewport.scrollLeft = graphViewportState.scrollLeft;
|
|
1261
1367
|
viewport.scrollTop = graphViewportState.scrollTop;
|
|
@@ -1389,7 +1495,7 @@ function renderMarkdown(content) {
|
|
|
1389
1495
|
return article;
|
|
1390
1496
|
}
|
|
1391
1497
|
|
|
1392
|
-
function renderDagInspector(dagRunId, node) {
|
|
1498
|
+
function renderDagInspector(dagRunId, node, initial = true) {
|
|
1393
1499
|
const panel = document.getElementById("dag-inspector");
|
|
1394
1500
|
if (!panel) return;
|
|
1395
1501
|
captureDagTimelineViewportState(panel);
|
|
@@ -1399,17 +1505,97 @@ function renderDagInspector(dagRunId, node) {
|
|
|
1399
1505
|
return;
|
|
1400
1506
|
}
|
|
1401
1507
|
|
|
1508
|
+
// Defer tearing down the inspector shell while the user is actively
|
|
1509
|
+
// dragging the resize handle (or the graph) so the pointer-capture target
|
|
1510
|
+
// is not destroyed mid-gesture; the next poll refreshes the content.
|
|
1511
|
+
if (isInteractiveGestureActive(document.body.classList)) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const existingContent = panel.querySelector(".dag-inspector-content") ?? null;
|
|
1516
|
+
const reuseShell = !initial && Boolean(existingContent);
|
|
1517
|
+
const nextContentSignature = dagInspectorSignature(dagRunId, node);
|
|
1518
|
+
|
|
1519
|
+
if (reuseShell && existingContent) {
|
|
1520
|
+
panel.classList.add("is-open");
|
|
1521
|
+
panel.setAttribute("aria-hidden", "false");
|
|
1522
|
+
syncDagInspectorHeader(panel, dagRunId, node);
|
|
1523
|
+
if (dagInspectorContentSignature === nextContentSignature) return;
|
|
1524
|
+
// Capture the reader's scroll position before repainting the content so we
|
|
1525
|
+
// can follow latest output when parked at the bottom, or stay put otherwise.
|
|
1526
|
+
const previousNode = existingContent.dataset.dagNodeId ?? "";
|
|
1527
|
+
const nodeChanged = previousNode !== String(node.nodeId ?? "");
|
|
1528
|
+
const followLatest = nodeChanged || computeFollowLatest(
|
|
1529
|
+
existingContent.scrollTop,
|
|
1530
|
+
existingContent.scrollHeight,
|
|
1531
|
+
existingContent.clientHeight,
|
|
1532
|
+
);
|
|
1533
|
+
const regionKey = `dag:${dagRunId}:${node.nodeId}:output`;
|
|
1534
|
+
dagNodeOutputViewportState = updateDagOutputViewportState(
|
|
1535
|
+
dagNodeOutputViewportState,
|
|
1536
|
+
regionKey,
|
|
1537
|
+
existingContent.scrollLeft,
|
|
1538
|
+
existingContent.scrollTop,
|
|
1539
|
+
followLatest,
|
|
1540
|
+
);
|
|
1541
|
+
const nestedViewportState = [...existingContent.querySelectorAll("pre")].map((pre) => ({
|
|
1542
|
+
scrollLeft: pre.scrollLeft,
|
|
1543
|
+
scrollTop: pre.scrollTop,
|
|
1544
|
+
}));
|
|
1545
|
+
clearNode(existingContent);
|
|
1546
|
+
fillDagInspectorContent(existingContent, dagRunId, node);
|
|
1547
|
+
if (followLatest) {
|
|
1548
|
+
existingContent.scrollTop = existingContent.scrollHeight;
|
|
1549
|
+
} else {
|
|
1550
|
+
existingContent.scrollTop = dagNodeOutputViewportState.scrollTop;
|
|
1551
|
+
}
|
|
1552
|
+
for (const [index, pre] of [...existingContent.querySelectorAll("pre")].entries()) {
|
|
1553
|
+
const previous = nestedViewportState[index];
|
|
1554
|
+
if (!previous) continue;
|
|
1555
|
+
pre.scrollLeft = previous.scrollLeft;
|
|
1556
|
+
pre.scrollTop = previous.scrollTop;
|
|
1557
|
+
}
|
|
1558
|
+
dagInspectorContentSignature = nextContentSignature;
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1402
1562
|
clearNode(panel);
|
|
1403
1563
|
panel.classList.add("is-open");
|
|
1404
1564
|
panel.setAttribute("aria-hidden", "false");
|
|
1405
1565
|
panel.appendChild(createDagInspectorResizeHandle(panel));
|
|
1566
|
+
const header = buildDagInspectorHeader(dagRunId, node);
|
|
1567
|
+
panel.appendChild(header);
|
|
1568
|
+
|
|
1569
|
+
const content = el("div", "dag-inspector-content");
|
|
1570
|
+
content.setAttribute("role", "tabpanel");
|
|
1571
|
+
fillDagInspectorContent(content, dagRunId, node);
|
|
1572
|
+
panel.appendChild(content);
|
|
1573
|
+
dagInspectorContentSignature = nextContentSignature;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function dagInspectorSignature(dagRunId, node) {
|
|
1577
|
+
return JSON.stringify([
|
|
1578
|
+
dagRunId,
|
|
1579
|
+
node.nodeId,
|
|
1580
|
+
dagInspectorTab,
|
|
1581
|
+
node.outputPreview ?? "",
|
|
1582
|
+
node.errorPreview ?? "",
|
|
1583
|
+
dagInspectorTab === "timeline" ? sessionEventOffset : null,
|
|
1584
|
+
]);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function buildDagInspectorHeader(dagRunId, node) {
|
|
1406
1588
|
const header = el("div", "dag-inspector-header");
|
|
1589
|
+
header.dataset.dagRunId = String(dagRunId ?? "");
|
|
1590
|
+
header.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
1407
1591
|
const title = el("div", "dag-inspector-title");
|
|
1408
1592
|
title.appendChild(el("h3", null, "节点检查器"));
|
|
1409
1593
|
title.appendChild(el("div", "dag-inspector-node", node.nodeId));
|
|
1410
1594
|
header.appendChild(title);
|
|
1411
1595
|
const controls = el("div", "dag-inspector-controls");
|
|
1412
|
-
|
|
1596
|
+
const status = badge(node.status ?? "unknown");
|
|
1597
|
+
status.classList.add("dag-inspector-status");
|
|
1598
|
+
controls.appendChild(status);
|
|
1413
1599
|
const close = document.createElement("button");
|
|
1414
1600
|
close.type = "button";
|
|
1415
1601
|
close.className = "dag-inspector-close";
|
|
@@ -1427,6 +1613,7 @@ function renderDagInspector(dagRunId, node) {
|
|
|
1427
1613
|
const button = document.createElement("button");
|
|
1428
1614
|
button.type = "button";
|
|
1429
1615
|
button.className = "dag-inspector-tab";
|
|
1616
|
+
button.dataset.inspectorTab = tab;
|
|
1430
1617
|
button.setAttribute("role", "tab");
|
|
1431
1618
|
button.setAttribute("aria-selected", String(dagInspectorTab === tab));
|
|
1432
1619
|
const tabIcon = el("i", icon);
|
|
@@ -1435,15 +1622,40 @@ function renderDagInspector(dagRunId, node) {
|
|
|
1435
1622
|
button.appendChild(document.createTextNode(` ${label}`));
|
|
1436
1623
|
button.addEventListener("click", () => {
|
|
1437
1624
|
dagInspectorTab = tab;
|
|
1438
|
-
renderDagInspector(dagRunId, node);
|
|
1625
|
+
renderDagInspector(dagRunId, node, false);
|
|
1439
1626
|
});
|
|
1440
1627
|
tabs.appendChild(button);
|
|
1441
1628
|
}
|
|
1442
1629
|
header.appendChild(tabs);
|
|
1443
|
-
|
|
1630
|
+
return header;
|
|
1631
|
+
}
|
|
1444
1632
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1633
|
+
function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
1634
|
+
const existingHeader = panel.querySelector(":scope > .dag-inspector-header");
|
|
1635
|
+
if (!existingHeader) return;
|
|
1636
|
+
if (
|
|
1637
|
+
existingHeader.dataset.dagRunId !== String(dagRunId ?? "") ||
|
|
1638
|
+
existingHeader.dataset.dagNodeId !== String(node.nodeId ?? "")
|
|
1639
|
+
) {
|
|
1640
|
+
panel.replaceChild(buildDagInspectorHeader(dagRunId, node), existingHeader);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const nodeLabel = existingHeader.querySelector(".dag-inspector-node");
|
|
1644
|
+
if (nodeLabel) nodeLabel.textContent = node.nodeId;
|
|
1645
|
+
const previousStatus = existingHeader.querySelector(".dag-inspector-status");
|
|
1646
|
+
if (previousStatus?.parentNode) {
|
|
1647
|
+
const nextStatus = badge(node.status ?? "unknown");
|
|
1648
|
+
nextStatus.classList.add("dag-inspector-status");
|
|
1649
|
+
previousStatus.parentNode.replaceChild(nextStatus, previousStatus);
|
|
1650
|
+
}
|
|
1651
|
+
for (const tab of existingHeader.querySelectorAll(".dag-inspector-tab")) {
|
|
1652
|
+
tab.setAttribute("aria-selected", String(tab.dataset.inspectorTab === dagInspectorTab));
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function fillDagInspectorContent(content, dagRunId, node) {
|
|
1657
|
+
content.dataset.dagNodeId = String(node.nodeId ?? "");
|
|
1658
|
+
content.dataset.dagRunId = String(dagRunId ?? "");
|
|
1447
1659
|
if (dagInspectorTab === "timeline") {
|
|
1448
1660
|
renderSessionTimeline(content, node.nodeId);
|
|
1449
1661
|
} else if (!node.outputPreview && !node.errorPreview) {
|
|
@@ -1457,7 +1669,6 @@ function renderDagInspector(dagRunId, node) {
|
|
|
1457
1669
|
content.appendChild(error);
|
|
1458
1670
|
}
|
|
1459
1671
|
}
|
|
1460
|
-
panel.appendChild(content);
|
|
1461
1672
|
}
|
|
1462
1673
|
|
|
1463
1674
|
async function renderFailures() {
|
|
@@ -2038,10 +2249,43 @@ function renderRunPanels(task, events, snapshot) {
|
|
|
2038
2249
|
|
|
2039
2250
|
const outputLabel = cmd?.label;
|
|
2040
2251
|
const tail = getOutputTail(events, outputLabel);
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2252
|
+
const existingPre = outputEl.querySelector("pre.output-block");
|
|
2253
|
+
let followRunOutput = true;
|
|
2254
|
+
let runOutputScrollLeft = 0;
|
|
2255
|
+
let runOutputScrollTop = 0;
|
|
2256
|
+
if (existingPre) {
|
|
2257
|
+
followRunOutput = computeFollowLatest(
|
|
2258
|
+
existingPre.scrollTop,
|
|
2259
|
+
existingPre.scrollHeight,
|
|
2260
|
+
existingPre.clientHeight,
|
|
2261
|
+
);
|
|
2262
|
+
runOutputScrollLeft = existingPre.scrollLeft;
|
|
2263
|
+
runOutputScrollTop = existingPre.scrollTop;
|
|
2264
|
+
}
|
|
2265
|
+
const nextOutput = tail || "暂无输出";
|
|
2266
|
+
let pre = existingPre;
|
|
2267
|
+
if (!pre) {
|
|
2268
|
+
clearNode(outputEl);
|
|
2269
|
+
outputEl.appendChild(el("h3", null, "最新输出"));
|
|
2270
|
+
pre = el("pre", "output-block", nextOutput);
|
|
2271
|
+
outputEl.appendChild(pre);
|
|
2272
|
+
} else if (pre.textContent !== nextOutput) {
|
|
2273
|
+
pre.textContent = nextOutput;
|
|
2274
|
+
}
|
|
2275
|
+
if (followRunOutput) {
|
|
2276
|
+
pre.scrollTop = pre.scrollHeight;
|
|
2277
|
+
} else {
|
|
2278
|
+
pre.scrollTop = runOutputScrollTop;
|
|
2279
|
+
}
|
|
2280
|
+
pre.scrollLeft = runOutputScrollLeft;
|
|
2281
|
+
const runRegionKey = `run:${task.workerRunId ?? task.taskId}:output`;
|
|
2282
|
+
runOutputViewportState = updateDagOutputViewportState(
|
|
2283
|
+
runOutputViewportState,
|
|
2284
|
+
runRegionKey,
|
|
2285
|
+
pre.scrollLeft,
|
|
2286
|
+
pre.scrollTop,
|
|
2287
|
+
followRunOutput,
|
|
2288
|
+
);
|
|
2045
2289
|
|
|
2046
2290
|
const artifacts = collectArtifacts(task, events);
|
|
2047
2291
|
clearNode(artifactsEl);
|
|
@@ -2142,6 +2386,7 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
2142
2386
|
dagInspectorTab = "output";
|
|
2143
2387
|
dagGraphViewportState = null;
|
|
2144
2388
|
dagTimelineViewportState = null;
|
|
2389
|
+
dagInspectorContentSignature = null;
|
|
2145
2390
|
}
|
|
2146
2391
|
|
|
2147
2392
|
const metaEl = document.getElementById("dag-meta");
|
|
@@ -2182,7 +2427,19 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
2182
2427
|
const succeededCount = nodes.filter((n) => isNodeFinished(n.status) && !isNodeFailed(n.status))
|
|
2183
2428
|
.length;
|
|
2184
2429
|
|
|
2185
|
-
|
|
2430
|
+
// reuse-dag-detail-shell: on a poll refresh (non-initial) we keep the
|
|
2431
|
+
// interactive DOM identity stable so active scroll/resize gestures and the
|
|
2432
|
+
// diagnostics <details> open state survive the redraw. Initial paints and
|
|
2433
|
+
// DAG-run switches rebuild everything from scratch as before.
|
|
2434
|
+
const existingGraphWrap = ranksEl.querySelector(":scope > .dag-graph-wrap") ?? null;
|
|
2435
|
+
const existingViewport = existingGraphWrap?.querySelector(":scope > .dag-graph-viewport") ?? null;
|
|
2436
|
+
const existingDetails = metaEl.querySelector("details.meta-diagnostics") ?? null;
|
|
2437
|
+
const reuseShell = shouldReuseDagDetailShell(initial, Boolean(existingViewport));
|
|
2438
|
+
const preserveDetailsOpen = shouldPreserveDiagnosticsOpen(
|
|
2439
|
+
initial,
|
|
2440
|
+
Boolean(existingDetails?.open),
|
|
2441
|
+
);
|
|
2442
|
+
|
|
2186
2443
|
const visibility = dagMetaVisibility(dag);
|
|
2187
2444
|
const livenessValue = LIVENESS_LABELS[dag.liveness] ?? dag.liveness ?? LIVENESS_LABELS.unknown;
|
|
2188
2445
|
const primaryEntries = [
|
|
@@ -2202,10 +2459,8 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
2202
2459
|
["成功/失败/跳过", `${succeededCount}/${failedCount}/${skippedCount}`],
|
|
2203
2460
|
["DAG 路径", dag.dagPath ?? "—"],
|
|
2204
2461
|
);
|
|
2205
|
-
|
|
2206
|
-
const
|
|
2207
|
-
details.appendChild(el("summary", null, "诊断信息"));
|
|
2208
|
-
details.appendChild(metaGrid([
|
|
2462
|
+
const primaryGrid = metaGrid(primaryEntries);
|
|
2463
|
+
const diagnosticGrid = metaGrid([
|
|
2209
2464
|
["当前判定", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
2210
2465
|
["原始记录状态", badge(dag.status)],
|
|
2211
2466
|
["执行器状态", livenessValue],
|
|
@@ -2213,17 +2468,41 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
2213
2468
|
["状态一致性", visibility.showConsistencyWarning ? "不一致,需要处理" : "一致"],
|
|
2214
2469
|
["允许恢复", visibility.showResume ? "是" : "否"],
|
|
2215
2470
|
["允许收口", visibility.showReconcile ? "是" : "否"],
|
|
2216
|
-
])
|
|
2217
|
-
metaEl
|
|
2471
|
+
]);
|
|
2472
|
+
if (!initial && existingDetails?.parentNode === metaEl) {
|
|
2473
|
+
const existingPrimary = metaEl.querySelector(":scope > .meta-grid");
|
|
2474
|
+
if (existingPrimary) metaEl.replaceChild(primaryGrid, existingPrimary);
|
|
2475
|
+
else metaEl.insertBefore(primaryGrid, existingDetails);
|
|
2476
|
+
const existingDiagnosticGrid = existingDetails.querySelector(":scope > .meta-grid");
|
|
2477
|
+
if (existingDiagnosticGrid) {
|
|
2478
|
+
existingDetails.replaceChild(diagnosticGrid, existingDiagnosticGrid);
|
|
2479
|
+
} else {
|
|
2480
|
+
existingDetails.appendChild(diagnosticGrid);
|
|
2481
|
+
}
|
|
2482
|
+
existingDetails.open = preserveDetailsOpen;
|
|
2483
|
+
} else {
|
|
2484
|
+
clearNode(metaEl);
|
|
2485
|
+
metaEl.appendChild(primaryGrid);
|
|
2486
|
+
const details = el("details", "meta-diagnostics");
|
|
2487
|
+
details.appendChild(el("summary", null, "诊断信息"));
|
|
2488
|
+
details.appendChild(diagnosticGrid);
|
|
2489
|
+
metaEl.appendChild(details);
|
|
2490
|
+
}
|
|
2218
2491
|
|
|
2219
|
-
|
|
2220
|
-
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
2492
|
+
const graphGestureActive = document.body.classList.contains("is-resizing-dag-graph");
|
|
2221
2493
|
if (nodes.length === 0) {
|
|
2494
|
+
clearNode(ranksEl);
|
|
2495
|
+
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
2222
2496
|
ranksEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
|
|
2497
|
+
} else if (reuseShell && existingGraphWrap?.parentNode === ranksEl) {
|
|
2498
|
+
if (!graphGestureActive) renderDagGraph(dag, dagRunId, existingGraphWrap);
|
|
2223
2499
|
} else {
|
|
2224
|
-
|
|
2500
|
+
clearNode(ranksEl);
|
|
2501
|
+
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
2502
|
+
ranksEl.appendChild(renderDagGraph(dag, dagRunId, null));
|
|
2225
2503
|
}
|
|
2226
2504
|
|
|
2505
|
+
const nodesScrollLeft = nodesEl.scrollLeft;
|
|
2227
2506
|
clearNode(nodesEl);
|
|
2228
2507
|
const nodeHeading = el("div", "dag-section-heading");
|
|
2229
2508
|
nodeHeading.appendChild(el("h3", null, "节点"));
|
|
@@ -2277,6 +2556,7 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
2277
2556
|
}
|
|
2278
2557
|
nodesEl.appendChild(nodeTable);
|
|
2279
2558
|
}
|
|
2559
|
+
nodesEl.scrollLeft = nodesScrollLeft;
|
|
2280
2560
|
|
|
2281
2561
|
if (dagInspectorOpen && selectedDagNodeId) {
|
|
2282
2562
|
await mergeSessionEvents(dagRunId, selectedDagNodeId);
|
|
@@ -2284,6 +2564,7 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
2284
2564
|
renderDagInspector(
|
|
2285
2565
|
dagRunId,
|
|
2286
2566
|
nodes.find((node) => node.nodeId === selectedDagNodeId),
|
|
2567
|
+
initial,
|
|
2287
2568
|
);
|
|
2288
2569
|
return isDagRunActive(dag);
|
|
2289
2570
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</div>
|
|
38
38
|
<div id="dashboard-repo" class="repo-banner" aria-label="当前仓库"></div>
|
|
39
39
|
<section id="dashboard-features" class="panel panel-features" aria-label="Feature 决策"></section>
|
|
40
|
-
<section id="dashboard-kpi" class="panel panel-kpi" aria-label="运行摘要"></section>
|
|
40
|
+
<section id="dashboard-kpi" class="panel panel-kpi" aria-label="运行摘要"></section>
|
|
41
41
|
<div class="dashboard-workspace">
|
|
42
42
|
<section id="dashboard-active-dags" class="panel panel-primary"></section>
|
|
43
43
|
<aside id="dashboard-risk" class="panel panel-risk" aria-label="运行风险"></aside>
|
|
@@ -250,10 +250,10 @@ td:first-child,
|
|
|
250
250
|
.kpi-group + .kpi-group { margin-top: var(--space-3); }
|
|
251
251
|
.kpi-group-legend { padding: 0; color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
|
|
252
252
|
|
|
253
|
-
.kpi-grid {
|
|
253
|
+
.kpi-grid {
|
|
254
254
|
display: grid;
|
|
255
255
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
256
|
-
overflow: visible;
|
|
256
|
+
overflow: visible;
|
|
257
257
|
border: 1px solid var(--hairline-strong);
|
|
258
258
|
border-radius: var(--radius-lg);
|
|
259
259
|
background: var(--surface);
|
|
@@ -275,8 +275,8 @@ td:first-child,
|
|
|
275
275
|
.kpi-help { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--muted-soft); cursor: help; font-size: 13px; line-height: 1; }
|
|
276
276
|
.kpi-help:hover, .kpi-help:focus-visible { color: var(--ink); background: var(--surface-muted); outline: none; }
|
|
277
277
|
.kpi-help:focus-visible { box-shadow: 0 0 0 2px var(--hairline-strong); }
|
|
278
|
-
.kpi-tip { position: absolute; left: 12px; right: 12px; top: calc(100% - 8px); z-index: 10; max-width: 280px; padding: 8px 10px; border: 1px solid var(--hairline-strong); border-radius: var(--radius-md); background: var(--surface-ink); color: var(--canvas); font-size: 11px; font-weight: 400; line-height: 1.5; box-shadow: var(--shadow-panel); opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--transition-fast); }
|
|
279
|
-
.kpi-help:hover + .kpi-tip, .kpi-help:focus-visible + .kpi-tip { opacity: 1; visibility: visible; }
|
|
278
|
+
.kpi-tip { position: absolute; left: 12px; right: 12px; top: calc(100% - 8px); z-index: 10; max-width: 280px; padding: 8px 10px; border: 1px solid var(--hairline-strong); border-radius: var(--radius-md); background: var(--surface-ink); color: var(--canvas); font-size: 11px; font-weight: 400; line-height: 1.5; box-shadow: var(--shadow-panel); opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--transition-fast); }
|
|
279
|
+
.kpi-help:hover + .kpi-tip, .kpi-help:focus-visible + .kpi-tip { opacity: 1; visibility: visible; }
|
|
280
280
|
|
|
281
281
|
.dashboard-workspace,
|
|
282
282
|
.dashboard-secondary {
|
|
@@ -451,6 +451,7 @@ tr.clickable:focus-within { outline: 2px solid var(--orange); outline-offset: -2
|
|
|
451
451
|
.dag-inspector-resize-handle:focus-visible::before { background: var(--orange); opacity: 1; }
|
|
452
452
|
.dag-inspector-resize-handle:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
|
|
453
453
|
body.is-resizing-dag-inspector { cursor: col-resize; user-select: none; }
|
|
454
|
+
body.is-resizing-dag-graph { cursor: grabbing; user-select: none; }
|
|
454
455
|
.dag-inspector-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-3); align-items: start; padding: var(--space-4) var(--space-4) var(--space-3); border-bottom: 1px solid var(--hairline); background: var(--canvas-soft); }
|
|
455
456
|
.dag-inspector-title { display: grid; gap: 6px; min-width: 0; }
|
|
456
457
|
.dag-inspector-title h3 { margin: 0; font-size: 14px; }
|