@tea-agent/loop-agent 0.39.1 → 0.39.3

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.
@@ -21,6 +21,7 @@ import {
21
21
  } from "../api.js";
22
22
  import { badge, formatLedgerSummary, formatMs, prettyJsonBlock } from "../format.js";
23
23
  import { renderMarkdown, renderInspectorDocument } from "../markdown-render.js";
24
+ import { describeDagNodePurpose } from "../dag-node-purpose.js";
24
25
  import { buildPromptRestartCandidateOptions } from "../prompt-restart-candidates.js";
25
26
  import { copyText } from "../copy.js";
26
27
  import {
@@ -133,6 +134,175 @@ function createPromptRestartCopyButton(readCandidate) {
133
134
  return button;
134
135
  }
135
136
 
137
+ function formatPromptRestartMeta(text) {
138
+ const value = String(text ?? "");
139
+ const chars = [...value].length;
140
+ const lines = value.length === 0 ? 0 : value.split("\n").length;
141
+ return `${lines} 行 · ${chars.toLocaleString("zh-CN")} 字`;
142
+ }
143
+
144
+ function createPromptRestartBanner({ tone, kicker, title, body }) {
145
+ const banner = el("div", `prompt-restart-banner is-${tone}`);
146
+ banner.setAttribute("role", "status");
147
+ banner.appendChild(el("span", "prompt-restart-banner-kicker", kicker));
148
+ banner.appendChild(el("p", "prompt-restart-banner-title", title));
149
+ banner.appendChild(el("p", "prompt-restart-banner-body", body));
150
+ return banner;
151
+ }
152
+
153
+ function createPromptRestartCandidatePicker({
154
+ options,
155
+ value,
156
+ onChange,
157
+ label,
158
+ hint,
159
+ }) {
160
+ const field = el("div", "prompt-restart-field");
161
+ field.appendChild(el("span", "prompt-restart-label", label));
162
+ const select = createCustomSelect({
163
+ ariaLabel: label,
164
+ className: "prompt-restart-candidate-select",
165
+ value,
166
+ disabled: options.length < 2,
167
+ options: options.map((option) => ({
168
+ value: option.key,
169
+ label: option.label,
170
+ })),
171
+ onChange: (nextKey) => {
172
+ onChange(String(nextKey));
173
+ },
174
+ });
175
+ select.setAttribute("data-testid", "prompt-restart-candidate");
176
+ field.appendChild(select);
177
+ const hintEl = el("p", "prompt-restart-hint", hint ?? "");
178
+ if (!hint) hintEl.hidden = true;
179
+ field.appendChild(hintEl);
180
+ return { field, select, hintEl };
181
+ }
182
+
183
+ function createPromptRestartModeSwitch({ value = "append", onChange }) {
184
+ const field = el("div", "prompt-restart-field prompt-restart-mode-field");
185
+ field.appendChild(el("span", "prompt-restart-label", "编辑模式"));
186
+ const group = el("div", "prompt-restart-mode-switch");
187
+ group.setAttribute("role", "radiogroup");
188
+ group.setAttribute("aria-label", "编辑模式");
189
+ group.setAttribute("data-testid", "prompt-restart-mode");
190
+ let current = value === "replace" ? "replace" : "append";
191
+ const buttons = [];
192
+ const sync = () => {
193
+ for (const btn of buttons) {
194
+ const selected = btn.dataset.mode === current;
195
+ btn.setAttribute("aria-checked", selected ? "true" : "false");
196
+ btn.classList.toggle("is-selected", selected);
197
+ }
198
+ };
199
+ for (const mode of [
200
+ { value: "append", label: "追加指令" },
201
+ { value: "replace", label: "全文替换" },
202
+ ]) {
203
+ const btn = document.createElement("button");
204
+ btn.type = "button";
205
+ btn.className = "prompt-restart-mode-option";
206
+ btn.dataset.mode = mode.value;
207
+ btn.setAttribute("role", "radio");
208
+ btn.setAttribute("aria-label", mode.label);
209
+ btn.textContent = mode.label;
210
+ btn.addEventListener("click", () => {
211
+ if (current === mode.value) return;
212
+ current = mode.value;
213
+ sync();
214
+ onChange?.(current);
215
+ });
216
+ buttons.push(btn);
217
+ group.appendChild(btn);
218
+ }
219
+ Object.defineProperty(group, "value", {
220
+ configurable: true,
221
+ enumerable: true,
222
+ get() {
223
+ return current;
224
+ },
225
+ set(next) {
226
+ current = next === "replace" ? "replace" : "append";
227
+ sync();
228
+ },
229
+ });
230
+ sync();
231
+ field.appendChild(group);
232
+ const hint = el(
233
+ "p",
234
+ "prompt-restart-hint",
235
+ "追加会保留原提示词;全文替换会改写整份底稿。",
236
+ );
237
+ field.appendChild(hint);
238
+ return { field, group, hint };
239
+ }
240
+
241
+ function createPromptRestartPromptCard({
242
+ title,
243
+ badge,
244
+ readCandidate,
245
+ preTestId,
246
+ collapsible = false,
247
+ startCollapsed = false,
248
+ }) {
249
+ const card = el("div", "prompt-restart-prompt-card");
250
+ const head = el("div", "prompt-restart-prompt-head");
251
+ const titles = el("div", "prompt-restart-prompt-titles");
252
+ const titleEl = el("span", "prompt-restart-prompt-title", title);
253
+ titles.appendChild(titleEl);
254
+ if (badge) {
255
+ titles.appendChild(el("span", "prompt-restart-prompt-badge", badge));
256
+ }
257
+ const metaEl = el("span", "prompt-restart-prompt-meta", "");
258
+ titles.appendChild(metaEl);
259
+ head.appendChild(titles);
260
+ const headActions = el("div", "prompt-restart-prompt-actions");
261
+ let fold = null;
262
+ let collapsed = Boolean(collapsible && startCollapsed);
263
+ const syncFold = () => {
264
+ if (!fold) return;
265
+ card.classList.toggle("is-collapsed", collapsed);
266
+ fold.setAttribute("aria-expanded", collapsed ? "false" : "true");
267
+ fold.textContent = collapsed ? "展开" : "收起";
268
+ };
269
+ if (collapsible) {
270
+ card.classList.add("is-collapsible");
271
+ fold = document.createElement("button");
272
+ fold.type = "button";
273
+ fold.className = "prompt-restart-prompt-fold";
274
+ fold.setAttribute("aria-controls", preTestId);
275
+ fold.addEventListener("click", (event) => {
276
+ event.preventDefault();
277
+ event.stopPropagation();
278
+ collapsed = !collapsed;
279
+ syncFold();
280
+ });
281
+ headActions.appendChild(fold);
282
+ }
283
+ headActions.appendChild(createPromptRestartCopyButton(readCandidate));
284
+ head.appendChild(headActions);
285
+ card.appendChild(head);
286
+ const pre = document.createElement("pre");
287
+ pre.className = "prompt-restart-readonly";
288
+ pre.setAttribute("data-testid", preTestId);
289
+ if (preTestId) pre.id = preTestId;
290
+ card.appendChild(pre);
291
+ const sync = (candidate) => {
292
+ const next = candidate ?? readCandidate?.();
293
+ titleEl.textContent =
294
+ next?.key === "frozen"
295
+ ? "冻结原始提示词"
296
+ : (next?.label ?? title);
297
+ pre.textContent = next?.prompt ?? "";
298
+ metaEl.textContent = formatPromptRestartMeta(next?.prompt ?? "");
299
+ if (next?.truncated) metaEl.textContent += " · 已截断";
300
+ };
301
+ sync(readCandidate?.());
302
+ syncFold();
303
+ return { card, pre, titleEl, metaEl, sync };
304
+ }
305
+
136
306
  function renderDagInspectorForCurrent() {
137
307
  if (!inspectorSelection.dagRunId || !inspectorSelection.node) return;
138
308
  renderDagInspector(
@@ -1234,57 +1404,41 @@ async function renderPromptRestart(content, dagRunId, node) {
1234
1404
 
1235
1405
  if (node.status !== "ERROR") {
1236
1406
  // Non-failed nodes still show their frozen prompt — read-only.
1237
- const hint = el(
1238
- "p",
1239
- "muted",
1240
- `当前节点状态为 ${node.status ?? "unknown"};仅失败(ERROR)节点支持编辑提示词并重启。以下可只读查看冻结提示词与历史失败候选。`,
1407
+ const panel = el("div", "prompt-restart-panel is-readonly");
1408
+ panel.appendChild(
1409
+ createPromptRestartBanner({
1410
+ tone: "readonly",
1411
+ kicker: "只读",
1412
+ title: `当前节点 ${node.status ?? "unknown"}`,
1413
+ body: "仅失败(ERROR)节点支持编辑提示词并重启。以下可只读查看冻结提示词与历史失败候选。",
1414
+ }),
1241
1415
  );
1242
- hint.setAttribute("role", "status");
1243
- content.appendChild(hint);
1244
- const picker = el("label", "prompt-restart-field");
1245
- picker.appendChild(el("span", "muted", "提示词版本(只读)"));
1246
- const select = document.createElement("select");
1247
- select.className = "prompt-restart-mode-select";
1248
- select.setAttribute("data-testid", "prompt-restart-candidate");
1249
- for (const option of promptCandidateOptions) {
1250
- const element = document.createElement("option");
1251
- element.value = option.key;
1252
- element.textContent = option.label;
1253
- select.appendChild(element);
1254
- }
1255
- picker.appendChild(select);
1256
- if (promptCandidateOptions.length === 1) {
1257
- picker.appendChild(
1258
- el(
1259
- "span",
1260
- "muted",
1261
- historicalCandidatesMissing
1416
+ const promptCard = createPromptRestartPromptCard({
1417
+ title: "冻结原始提示词",
1418
+ badge: "只读",
1419
+ readCandidate: () => selectedCandidate,
1420
+ preTestId: "prompt-restart-readonly",
1421
+ });
1422
+ const picker = createPromptRestartCandidatePicker({
1423
+ options: promptCandidateOptions,
1424
+ value: selectedCandidate.key,
1425
+ label: "提示词版本(只读)",
1426
+ hint:
1427
+ promptCandidateOptions.length === 1
1428
+ ? historicalCandidatesMissing
1262
1429
  ? "该历史运行未保存失败 attempt 的实际提示词;新运行会在此列出。"
1263
- : "该节点没有已保存的失败 attempt。",
1264
- ),
1265
- );
1266
- }
1267
- content.appendChild(picker);
1268
-
1269
- const promptCard = el("div", "prompt-restart-prompt-card");
1270
- const promptHead = el("div", "prompt-restart-prompt-head");
1271
- promptHead.appendChild(el("span", "muted", "当前提示词"));
1272
- promptHead.appendChild(
1273
- createPromptRestartCopyButton(() => selectedCandidate),
1274
- );
1275
- promptCard.appendChild(promptHead);
1276
- const pre = document.createElement("pre");
1277
- pre.className = "prompt-restart-readonly";
1278
- pre.setAttribute("data-testid", "prompt-restart-readonly");
1279
- pre.textContent = selectedCandidate.prompt;
1280
- promptCard.appendChild(pre);
1281
- content.appendChild(promptCard);
1282
- select.addEventListener("change", () => {
1283
- selectedCandidate =
1284
- promptCandidateOptions.find((option) => option.key === select.value) ??
1285
- promptCandidateOptions[0];
1286
- pre.textContent = selectedCandidate.prompt;
1430
+ : "该节点没有已保存的失败 attempt。"
1431
+ : "可切换查看冻结底稿与历史失败候选。",
1432
+ onChange: (nextKey) => {
1433
+ selectedCandidate =
1434
+ promptCandidateOptions.find((option) => option.key === nextKey) ??
1435
+ promptCandidateOptions[0];
1436
+ promptCard.sync(selectedCandidate);
1437
+ },
1287
1438
  });
1439
+ panel.appendChild(picker.field);
1440
+ panel.appendChild(promptCard.card);
1441
+ content.appendChild(panel);
1288
1442
  return;
1289
1443
  }
1290
1444
 
@@ -1299,151 +1453,129 @@ async function renderPromptRestart(content, dagRunId, node) {
1299
1453
  return;
1300
1454
  }
1301
1455
 
1302
- const box = el("div", "prompt-restart-box");
1456
+ const box = el("div", "prompt-restart-box prompt-restart-panel is-edit");
1303
1457
  box.appendChild(
1304
- el(
1305
- "p",
1306
- "muted",
1307
- "编辑该失败节点的提示词并从它重启:父 run spec 保持冻结,将创建带修改提示词的续跑 run(实验能力,仅 ERROR 节点)。",
1308
- ),
1309
- );
1310
- const candidateRow = el("label", "prompt-restart-field");
1311
- candidateRow.appendChild(el("span", "muted", "提示词版本"));
1312
- const candidateSelect = document.createElement("select");
1313
- candidateSelect.className = "prompt-restart-mode-select";
1314
- candidateSelect.setAttribute("data-testid", "prompt-restart-candidate");
1315
- for (const option of promptCandidateOptions) {
1316
- const element = document.createElement("option");
1317
- element.value = option.key;
1318
- element.textContent = option.label;
1319
- candidateSelect.appendChild(element);
1320
- }
1321
- candidateRow.appendChild(candidateSelect);
1322
- const candidateHint = el(
1323
- "span",
1324
- "muted",
1325
- promptCandidateOptions.length > 1
1326
- ? "选择失败候选会载入全文替换;冻结原始提示词仍可使用追加模式。"
1327
- : historicalCandidatesMissing
1328
- ? "该历史运行未保存失败 attempt 的实际提示词;新运行会在此列出。"
1329
- : "该节点没有已保存的失败 attempt;当前仅有冻结原始提示词。",
1330
- );
1331
- candidateRow.appendChild(candidateHint);
1332
- box.appendChild(candidateRow);
1333
-
1334
- const modeRow = el("div", "prompt-restart-mode");
1335
- modeRow.appendChild(el("span", "muted", "编辑模式"));
1336
- const modeSelect = document.createElement("select");
1337
- modeSelect.className = "prompt-restart-mode-select";
1338
- modeSelect.setAttribute("data-testid", "prompt-restart-mode");
1339
- const appendOption = document.createElement("option");
1340
- appendOption.value = "append";
1341
- appendOption.textContent =
1342
- "追加指令(推荐):保留原提示词,在末尾追加 operator_addendum 块";
1343
- const replaceOption = document.createElement("option");
1344
- replaceOption.value = "replace";
1345
- replaceOption.textContent = "全文替换(高级):替换整个提示词";
1346
- modeSelect.appendChild(appendOption);
1347
- modeSelect.appendChild(replaceOption);
1348
- modeSelect.value = "append";
1349
- modeRow.appendChild(modeSelect);
1350
- box.appendChild(modeRow);
1351
-
1352
- const baseLabel = el(
1353
- "div",
1354
- "prompt-restart-field prompt-restart-prompt-card",
1355
- );
1356
- const baseHead = el("div", "prompt-restart-prompt-head");
1357
- baseHead.appendChild(
1358
- el("span", "muted", `冻结提示词(只读底稿 · ${nodeId})`),
1458
+ createPromptRestartBanner({
1459
+ tone: "edit",
1460
+ kicker: "可编辑",
1461
+ title: "从失败节点编辑提示词并重启",
1462
+ body: "父 run spec 保持冻结,将创建带修改提示词的续跑 run。",
1463
+ }),
1359
1464
  );
1360
- baseHead.appendChild(createPromptRestartCopyButton(() => selectedCandidate));
1361
- baseLabel.appendChild(baseHead);
1362
- const baseBlock = document.createElement("pre");
1363
- baseBlock.className = "prompt-restart-readonly";
1364
- baseBlock.setAttribute("data-testid", "prompt-restart-base");
1365
- baseBlock.textContent = selectedCandidate.prompt;
1366
- baseLabel.appendChild(baseBlock);
1367
- box.appendChild(baseLabel);
1465
+ const promptCard = createPromptRestartPromptCard({
1466
+ title: `冻结提示词(只读底稿 · ${nodeId})`,
1467
+ badge: "底稿",
1468
+ readCandidate: () => selectedCandidate,
1469
+ preTestId: "prompt-restart-base",
1470
+ collapsible: true,
1471
+ startCollapsed: true,
1472
+ });
1473
+ const picker = createPromptRestartCandidatePicker({
1474
+ options: promptCandidateOptions,
1475
+ value: selectedCandidate.key,
1476
+ label: "提示词版本",
1477
+ hint:
1478
+ promptCandidateOptions.length > 1
1479
+ ? "选择失败候选会载入全文替换;冻结原始提示词仍可使用追加模式。"
1480
+ : historicalCandidatesMissing
1481
+ ? "该历史运行未保存失败 attempt 的实际提示词;新运行会在此列出。"
1482
+ : "该节点没有已保存的失败 attempt;当前仅有冻结原始提示词。",
1483
+ onChange: (nextKey) => {
1484
+ selectedCandidate =
1485
+ promptCandidateOptions.find((option) => option.key === nextKey) ??
1486
+ promptCandidateOptions[0];
1487
+ promptCard.sync(selectedCandidate);
1488
+ replaceArea.value = selectedCandidate.prompt;
1489
+ if (selectedCandidate.key !== "frozen") {
1490
+ modeSelect.value = "replace";
1491
+ }
1492
+ picker.hintEl.hidden = false;
1493
+ picker.hintEl.textContent = selectedCandidate.truncated
1494
+ ? "该候选已截断,仅供查看,不能直接提交重启。"
1495
+ : selectedCandidate.key === "frozen"
1496
+ ? "冻结原始提示词可使用追加或全文替换。"
1497
+ : "已载入该失败候选;可在全文替换编辑器中修订后重启。";
1498
+ applyModeVisibility();
1499
+ },
1500
+ });
1501
+ const modeSwitch = createPromptRestartModeSwitch({
1502
+ value: "append",
1503
+ onChange: () => applyModeVisibility(),
1504
+ });
1505
+ const modeSelect = modeSwitch.group;
1506
+ const toolbar = el("div", "prompt-restart-toolbar");
1507
+ toolbar.appendChild(picker.field);
1508
+ toolbar.appendChild(modeSwitch.field);
1509
+ box.appendChild(toolbar);
1368
1510
 
1369
- const addendumLabel = el("label", "prompt-restart-field");
1511
+ const addendumLabel = el("label", "prompt-restart-field prompt-restart-work");
1512
+ addendumLabel.appendChild(
1513
+ el("span", "prompt-restart-label", "追加指令"),
1514
+ );
1370
1515
  addendumLabel.appendChild(
1371
1516
  el(
1372
- "span",
1373
- "muted",
1374
- "追加指令(将包裹为 <operator_addendum> 块附加到原提示词末尾)",
1517
+ "p",
1518
+ "prompt-restart-hint",
1519
+ "将包裹为 <operator_addendum> 块附加到原提示词末尾",
1375
1520
  ),
1376
1521
  );
1377
1522
  const addendumArea = document.createElement("textarea");
1378
- addendumArea.className = "prompt-restart-textarea";
1379
- addendumArea.rows = 6;
1523
+ addendumArea.className = "prompt-restart-textarea prompt-restart-textarea-work";
1524
+ addendumArea.rows = 8;
1380
1525
  addendumArea.placeholder = "例如:重点检查 X 边界;输出前先自检 Y 约束…";
1381
1526
  addendumArea.setAttribute("data-testid", "prompt-restart-addendum");
1382
1527
  addendumLabel.appendChild(addendumArea);
1383
1528
  box.appendChild(addendumLabel);
1384
1529
 
1385
- const replaceLabel = el("label", "prompt-restart-field");
1386
- replaceLabel.appendChild(el("span", "muted", `完整提示词(${nodeId})`));
1530
+ const replaceLabel = el("label", "prompt-restart-field prompt-restart-work");
1531
+ replaceLabel.appendChild(
1532
+ el("span", "prompt-restart-label", `完整提示词(${nodeId})`),
1533
+ );
1387
1534
  const replaceArea = document.createElement("textarea");
1388
- replaceArea.className = "prompt-restart-textarea";
1389
- replaceArea.rows = 16;
1535
+ replaceArea.className = "prompt-restart-textarea prompt-restart-textarea-work";
1536
+ replaceArea.rows = 12;
1390
1537
  replaceArea.value = selectedCandidate.prompt;
1391
1538
  replaceArea.setAttribute("data-testid", "prompt-restart-textarea");
1392
1539
  replaceLabel.appendChild(replaceArea);
1393
1540
  box.appendChild(replaceLabel);
1394
1541
 
1542
+ const baseLabel = promptCard.card;
1543
+ box.appendChild(baseLabel);
1544
+
1395
1545
  const applyModeVisibility = () => {
1396
1546
  const append = modeSelect.value === "append";
1397
- baseLabel.style.display = append ? "" : "none";
1398
- addendumLabel.style.display = append ? "" : "none";
1399
- replaceLabel.style.display = append ? "none" : "";
1547
+ baseLabel.classList.toggle("is-hidden", !append);
1548
+ addendumLabel.classList.toggle("is-hidden", !append);
1549
+ replaceLabel.classList.toggle("is-hidden", append);
1400
1550
  };
1401
- modeSelect.addEventListener("change", applyModeVisibility);
1402
- candidateSelect.addEventListener("change", () => {
1403
- selectedCandidate =
1404
- promptCandidateOptions.find(
1405
- (option) => option.key === candidateSelect.value,
1406
- ) ?? promptCandidateOptions[0];
1407
- baseBlock.textContent = selectedCandidate.prompt;
1408
- replaceArea.value = selectedCandidate.prompt;
1409
- if (selectedCandidate.key !== "frozen") {
1410
- modeSelect.value = "replace";
1411
- }
1412
- if (candidateHint) {
1413
- candidateHint.textContent = selectedCandidate.truncated
1414
- ? "该候选已截断,仅供查看,不能直接提交重启。"
1415
- : selectedCandidate.key === "frozen"
1416
- ? "冻结原始提示词可使用追加或全文替换。"
1417
- : "已载入该失败候选;可在全文替换编辑器中修订后重启。";
1418
- }
1419
- applyModeVisibility();
1420
- });
1421
1551
  applyModeVisibility();
1422
1552
 
1423
1553
  const reasonLabel = el("label", "prompt-restart-field");
1424
- reasonLabel.appendChild(el("span", "muted", "重启原因(结构化说明)"));
1554
+ reasonLabel.appendChild(el("span", "prompt-restart-label", "重启原因"));
1555
+ reasonLabel.appendChild(
1556
+ el("p", "prompt-restart-hint", "结构化说明,会写入续跑 provenance"),
1557
+ );
1425
1558
  const reasonArea = document.createElement("textarea");
1426
- reasonArea.className = "prompt-restart-textarea";
1559
+ reasonArea.className = "prompt-restart-textarea prompt-restart-textarea-compact";
1427
1560
  reasonArea.rows = 3;
1428
1561
  reasonArea.value = `operator-prompt-override / run=${dagRunId} / node=${nodeId} / 手工编辑失败节点提示词后重启`;
1429
1562
  reasonArea.setAttribute("data-testid", "prompt-restart-reason");
1430
1563
  reasonLabel.appendChild(reasonArea);
1431
1564
  box.appendChild(reasonLabel);
1432
1565
 
1433
- const row = el("div", "prompt-restart-actions");
1566
+ const row = el("div", "prompt-restart-actions prompt-restart-footer");
1567
+ const status = el("p", "prompt-restart-status", "");
1568
+ status.setAttribute("role", "status");
1569
+ status.setAttribute("aria-live", "polite");
1434
1570
  const submit = document.createElement("button");
1435
1571
  submit.type = "button";
1436
1572
  submit.className = "prompt-restart-submit";
1437
1573
  submit.textContent = "资格预检并重启";
1438
1574
  submit.setAttribute("data-testid", "prompt-restart-submit");
1575
+ row.appendChild(status);
1439
1576
  row.appendChild(submit);
1440
1577
  box.appendChild(row);
1441
1578
 
1442
- const status = el("p", "prompt-restart-status", "");
1443
- status.setAttribute("role", "status");
1444
- status.setAttribute("aria-live", "polite");
1445
- box.appendChild(status);
1446
-
1447
1579
  content.appendChild(box);
1448
1580
 
1449
1581
  const setStatus = (text, isError = false) => {
@@ -1535,7 +1667,7 @@ async function renderPromptRestart(content, dagRunId, node) {
1535
1667
  setStatus(`重启完成,新 run:${newRunId}`);
1536
1668
  const open = document.createElement("button");
1537
1669
  open.type = "button";
1538
- open.className = "prompt-restart-submit";
1670
+ open.className = "prompt-restart-submit is-secondary";
1539
1671
  open.textContent = "打开新 run";
1540
1672
  open.addEventListener("click", () => {
1541
1673
  window.location.hash = `#/dag/${encodeURIComponent(String(newRunId))}`;
@@ -2378,6 +2510,16 @@ function fillDagInspectorContent(content, dagRunId, node) {
2378
2510
  }
2379
2511
  }
2380
2512
 
2513
+ function inspectorNodePurpose(node) {
2514
+ return describeDagNodePurpose(node?.nodeId, node);
2515
+ }
2516
+
2517
+ function appendInspectorPurpose(title, node) {
2518
+ const purpose = inspectorNodePurpose(node);
2519
+ if (!purpose) return;
2520
+ title.appendChild(el("p", "dag-inspector-purpose", purpose));
2521
+ }
2522
+
2381
2523
  function buildDagInspectorHeader(dagRunId, node) {
2382
2524
  const header = el("div", "dag-inspector-header");
2383
2525
  header.dataset.dagRunId = String(dagRunId ?? "");
@@ -2385,6 +2527,7 @@ function buildDagInspectorHeader(dagRunId, node) {
2385
2527
  const title = el("div", "dag-inspector-title");
2386
2528
  title.appendChild(el("h3", null, "节点检查器"));
2387
2529
  title.appendChild(el("div", "dag-inspector-node", node.nodeId));
2530
+ appendInspectorPurpose(title, node);
2388
2531
  header.appendChild(title);
2389
2532
  const controls = el("div", "dag-inspector-controls");
2390
2533
  const status = badge(node.status ?? "unknown");
@@ -2464,6 +2607,16 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
2464
2607
  header.dataset.dagNodeId = String(node.nodeId ?? "");
2465
2608
  const nodeLabel = header.querySelector(".dag-inspector-node");
2466
2609
  if (nodeLabel) nodeLabel.textContent = node.nodeId;
2610
+ const title = header.querySelector(".dag-inspector-title");
2611
+ const purposeLine = header.querySelector(".dag-inspector-purpose");
2612
+ const purpose = inspectorNodePurpose(node);
2613
+ if (purposeLine && purpose) {
2614
+ purposeLine.textContent = purpose;
2615
+ } else if (purposeLine && !purpose) {
2616
+ purposeLine.remove();
2617
+ } else if (!purposeLine && purpose && title) {
2618
+ title.appendChild(el("p", "dag-inspector-purpose", purpose));
2619
+ }
2467
2620
  const status = header.querySelector(".dag-inspector-status");
2468
2621
  if (status) {
2469
2622
  const next = badge(node.status ?? "unknown");
@@ -0,0 +1,13 @@
1
+ export interface DagNodeRemark {
2
+ failed: boolean;
3
+ text: string;
4
+ isErrorSummary: boolean;
5
+ }
6
+
7
+ export function resolveNodeRemark(node: {
8
+ status?: string | null;
9
+ errorPreview?: string | null;
10
+ label?: string | null;
11
+ nodeId: string;
12
+ rank?: string | number | null;
13
+ } | null | undefined): DagNodeRemark;
@@ -70,6 +70,7 @@ import {
70
70
  dashboardVisibleFeatures,
71
71
  findDagForRun,
72
72
  } from "../dag-model.js";
73
+ import { describeDagNodePurpose } from "../dag-node-purpose.js";
73
74
  import { layoutDag } from "../dag-layout.js";
74
75
  import {
75
76
  showView,
@@ -136,7 +137,7 @@ export function extractFirstErrorSummary(errorPreview) {
136
137
  return effective ?? "";
137
138
  }
138
139
 
139
- /** Resolve 备注 cell content for a DAG node (failed → summary; else label). */
140
+ /** Resolve 备注 cell content for a DAG node (failed → summary; else purpose). */
140
141
  export function resolveNodeRemark(node) {
141
142
  const failed = isNodeFailed(node?.status);
142
143
  if (failed) {
@@ -147,10 +148,10 @@ export function resolveNodeRemark(node) {
147
148
  isErrorSummary: Boolean(summary),
148
149
  };
149
150
  }
150
- const label = typeof node?.label === "string" ? node.label.trim() : "";
151
+ const purpose = describeDagNodePurpose(node?.nodeId, node);
151
152
  return {
152
153
  failed: false,
153
- text: label || "—",
154
+ text: purpose || "—",
154
155
  isErrorSummary: false,
155
156
  };
156
157
  }