@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.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 (78) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/application/evaluation/budget.js +19 -1
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/executors/dag-pi-executor.js +762 -252
  5. package/dist/executors/pi-executor.js +20 -1
  6. package/dist/executors/pi-sdk-executor.js +64 -1
  7. package/dist/executors/shell-executor.js +5 -2
  8. package/dist/shared/frontend-execution-policy.js +22 -0
  9. package/dist/task/config-types.js +4 -0
  10. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  11. package/dist/task/source-prepare/ledger-review.js +6 -9
  12. package/dist/task/source-prepare/semantic-intake.js +16 -26
  13. package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
  14. package/dist/worker/observe/node-transparency.js +81 -72
  15. package/dist/worker/observe/routes.js +20 -1
  16. package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
  17. package/dist/worker/observe/static/dom.js +20 -1
  18. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  19. package/dist/worker/observe/static/format-pool.js +6 -0
  20. package/dist/worker/observe/static/format.js +7 -0
  21. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  22. package/dist/worker/observe/static/inspector-submission.js +32 -0
  23. package/dist/worker/observe/static/kpi.js +1 -0
  24. package/dist/worker/observe/static/relations.js +2 -0
  25. package/dist/worker/observe/static/router.js +13 -0
  26. package/dist/worker/observe/static/run-processing.js +2 -0
  27. package/dist/worker/observe/static/shell-chrome.js +36 -3
  28. package/dist/worker/observe/static/state.js +35 -2
  29. package/dist/worker/observe/static/styles.css +260 -39
  30. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  31. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  32. package/dist/worker/observe/static/task-history.js +12 -0
  33. package/dist/worker/observe/static/views/batch.js +6 -13
  34. package/dist/worker/observe/static/views/dag-graph.js +50 -3
  35. package/dist/worker/observe/static/views/dag-inspector.js +746 -265
  36. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  37. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  38. package/dist/worker/observe/static/views/dag.js +48 -10
  39. package/dist/worker/observe/static/views/dags.js +2 -0
  40. package/dist/worker/observe/static/views/dashboard.js +21 -12
  41. package/dist/worker/observe/static/views/failures.js +21 -11
  42. package/dist/worker/observe/static/views/feature.js +11 -29
  43. package/dist/worker/observe/static/views/pool.js +37 -28
  44. package/dist/worker/observe/static/views/run.js +48 -5
  45. package/dist/worker/observe/static/views/session-timeline.js +189 -240
  46. package/dist/worker/observe/static/views/task.js +81 -62
  47. package/dist/workflows/dag/budget-enforcement.js +53 -3
  48. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  49. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  50. package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
  51. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  52. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  53. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  54. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  55. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  56. package/dist/workflows/dag/frontend-review-scopes.js +117 -0
  57. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  58. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  59. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  60. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  61. package/dist/workflows/dag/init-hybrid.js +16 -10
  62. package/dist/workflows/dag/node-execution.js +32 -155
  63. package/dist/workflows/dag/prompt.js +4 -0
  64. package/dist/workflows/dag/rerun-plan.js +7 -1
  65. package/dist/workflows/dag/runner.js +26 -1
  66. package/dist/workflows/dag/types.js +6 -0
  67. package/docs/operations/README.md +1 -0
  68. package/docs/templates/frontend-design-contract.md +4 -4
  69. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  70. package/docs/templates/frontend-implementation-dag.json +5 -5
  71. package/package.json +1 -1
  72. package/skills/frontend-contract/SKILL.md +2 -1
  73. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  74. package/skills/frontend-design-review/SKILL.md +12 -11
  75. package/skills/frontend-plan/SKILL.md +2 -2
  76. package/skills/frontend-plan/references/decision-contract.md +18 -5
  77. package/skills/frontend-review/SKILL.md +10 -11
  78. package/skills/frontend-scout/references/scout-evidence.md +4 -0
@@ -183,6 +183,13 @@ body[data-shell="observe-console"] .process-timeline-time,
183
183
  color: var(--muted);
184
184
  font-size: 12px;
185
185
  }
186
+ /* 末级(当前页)窄屏截断,避免长标题/裸 ID 撑换行。 */
187
+ .breadcrumb .breadcrumb-current {
188
+ overflow: hidden;
189
+ min-width: 0;
190
+ text-overflow: ellipsis;
191
+ white-space: nowrap;
192
+ }
186
193
 
187
194
  .breadcrumb[hidden] {
188
195
  display: none;
@@ -274,9 +281,34 @@ body[data-shell="observe-console"] .process-timeline-time,
274
281
 
275
282
  .view-heading {
276
283
  display: grid;
277
- gap: 4px;
284
+ grid-template-columns: auto minmax(0, 1fr) auto;
285
+ align-items: center;
286
+ gap: 4px 10px;
278
287
  margin-bottom: var(--space-6);
279
288
  }
289
+ /* 标题行:kicker + 标题 + 刷新按钮同行(按钮靠右);说明文字独占下一行。
290
+ 用显式 grid 而非 flex-wrap:flex-basis 百分比在部分嵌套下不触发换行。 */
291
+ .view-heading .view-kicker {
292
+ grid-row: 1;
293
+ grid-column: 1;
294
+ }
295
+ /* 无 kicker 的页面标题从第一列开始,避免空列 + 列间距造成缩进。 */
296
+ .view-heading:not(:has(.view-kicker)) > h2 {
297
+ grid-column: 1 / 3;
298
+ }
299
+ .view-heading > h2 {
300
+ grid-row: 1;
301
+ grid-column: 2;
302
+ margin: 0;
303
+ }
304
+ .view-heading .view-manual-refresh {
305
+ grid-row: 1;
306
+ grid-column: 3;
307
+ }
308
+ .view-heading > p:last-of-type {
309
+ grid-row: 2;
310
+ grid-column: 1 / -1;
311
+ }
280
312
 
281
313
  .view-kicker {
282
314
  display: inline-flex;
@@ -301,7 +333,7 @@ body[data-shell="observe-console"] .process-timeline-time,
301
333
  letter-spacing: -0.02em;
302
334
  }
303
335
 
304
- .view-heading > p:last-child {
336
+ .view-heading > p:last-of-type {
305
337
  max-width: 66ch;
306
338
  margin: 0;
307
339
  color: var(--body);
@@ -1321,6 +1353,26 @@ body.is-resizing-dag-graph {
1321
1353
  overscroll-behavior-x: contain;
1322
1354
  scrollbar-width: thin;
1323
1355
  }
1356
+ /* 溢出渐隐:边缘阴影提示还有更多 Tab(类名由 overflow 测量驱动)。 */
1357
+ .dag-inspector-tabs.is-overflow-start,
1358
+ .dag-inspector-tabs.is-overflow-end {
1359
+ background-repeat: no-repeat;
1360
+ background-size: 22px 100%;
1361
+ }
1362
+ .dag-inspector-tabs.is-overflow-start {
1363
+ background-image: linear-gradient(90deg, rgb(38 37 30 / 12%), rgb(38 37 30 / 0));
1364
+ background-position: left center;
1365
+ }
1366
+ .dag-inspector-tabs.is-overflow-end {
1367
+ background-image: linear-gradient(270deg, rgb(38 37 30 / 12%), rgb(38 37 30 / 0));
1368
+ background-position: right center;
1369
+ }
1370
+ .dag-inspector-tabs.is-overflow-start.is-overflow-end {
1371
+ background-image:
1372
+ linear-gradient(90deg, rgb(38 37 30 / 12%), rgb(38 37 30 / 0)),
1373
+ linear-gradient(270deg, rgb(38 37 30 / 12%), rgb(38 37 30 / 0));
1374
+ background-position: left center, right center;
1375
+ }
1324
1376
  .dag-inspector-tab {
1325
1377
  flex: 0 0 auto;
1326
1378
  min-height: 30px;
@@ -1406,6 +1458,25 @@ body.is-resizing-dag-graph {
1406
1458
  font-weight: 800;
1407
1459
  letter-spacing: 0.12em;
1408
1460
  }
1461
+ .inspector-page-eyebrow-row {
1462
+ display: flex;
1463
+ align-items: center;
1464
+ justify-content: space-between;
1465
+ gap: 8px;
1466
+ }
1467
+ .inspector-page-eyebrow-row .observe-icon-button {
1468
+ flex: 0 0 24px;
1469
+ width: 24px;
1470
+ min-width: 24px;
1471
+ height: 24px;
1472
+ min-height: 24px;
1473
+ font-size: 14px;
1474
+ }
1475
+ /* 收起态只保留 eyebrow 行,点击可再展开(会话级记忆)。 */
1476
+ .inspector-page-lead.is-collapsed h4,
1477
+ .inspector-page-lead.is-collapsed p {
1478
+ display: none;
1479
+ }
1409
1480
  .dag-inspector-attempt-bar {
1410
1481
  position: sticky;
1411
1482
  top: calc(-1 * var(--space-4));
@@ -2029,13 +2100,16 @@ body.is-resizing-dag-graph {
2029
2100
  margin: 0 2px;
2030
2101
  pointer-events: none;
2031
2102
  }
2103
+ /* display:flex 会盖过 UA 的 [hidden],贴底/无溢出隐藏 dock 需显式声明。 */
2104
+ .process-timeline-autoscroll-dock[hidden] {
2105
+ display: none;
2106
+ }
2032
2107
  .process-timeline-autoscroll {
2033
2108
  position: relative;
2034
2109
  pointer-events: auto;
2035
2110
  display: inline-flex;
2036
2111
  align-items: center;
2037
2112
  justify-content: center;
2038
- gap: 7px;
2039
2113
  width: 36px;
2040
2114
  height: 36px;
2041
2115
  padding: 0;
@@ -2044,7 +2118,9 @@ body.is-resizing-dag-graph {
2044
2118
  background: color-mix(in srgb, var(--surface) 94%, var(--canvas));
2045
2119
  color: var(--body);
2046
2120
  box-shadow: 0 8px 24px rgb(38 37 30 / 14%);
2047
- font-size: 14px;
2121
+ font-size: 11px;
2122
+ font-weight: 650;
2123
+ white-space: nowrap;
2048
2124
  cursor: pointer;
2049
2125
  backdrop-filter: blur(10px);
2050
2126
  }
@@ -2080,25 +2156,22 @@ body.is-resizing-dag-graph {
2080
2156
  outline: none;
2081
2157
  box-shadow: 0 8px 24px rgb(38 37 30 / 16%), 0 0 0 3px var(--orange-soft);
2082
2158
  }
2083
- .process-timeline-autoscroll.is-active {
2084
- border-color: var(--ink);
2085
- background: var(--ink);
2086
- color: #fff;
2087
- }
2088
- .process-timeline-autoscroll.is-active:hover,
2089
- .process-timeline-autoscroll.is-active:focus-visible {
2159
+ .process-timeline-autoscroll.has-unread {
2090
2160
  border-color: var(--orange);
2091
- background: var(--ink);
2092
- color: #fff;
2161
+ color: var(--orange-active);
2093
2162
  }
2094
2163
  .process-timeline-autoscroll-signal {
2164
+ display: none;
2165
+ position: absolute;
2166
+ top: 5px;
2167
+ right: 5px;
2095
2168
  width: 5px;
2096
2169
  height: 5px;
2097
2170
  border-radius: 50%;
2098
- background: var(--muted-soft);
2099
- }
2100
- .process-timeline-autoscroll.is-active .process-timeline-autoscroll-signal {
2101
2171
  background: var(--orange);
2172
+ }
2173
+ .process-timeline-autoscroll.has-unread .process-timeline-autoscroll-signal {
2174
+ display: block;
2102
2175
  box-shadow: 0 0 0 4px rgb(245 78 0 / 16%);
2103
2176
  animation: process-timeline-live-pulse 1.8s ease-out infinite;
2104
2177
  }
@@ -2107,7 +2180,7 @@ body.is-resizing-dag-graph {
2107
2180
  75% { box-shadow: 0 0 0 7px rgb(245 78 0 / 0%); }
2108
2181
  }
2109
2182
  @media (prefers-reduced-motion: reduce) {
2110
- .process-timeline-autoscroll.is-active .process-timeline-autoscroll-signal {
2183
+ .process-timeline-autoscroll.has-unread .process-timeline-autoscroll-signal {
2111
2184
  animation: none;
2112
2185
  }
2113
2186
  .process-timeline-autoscroll::after {
@@ -2119,28 +2192,6 @@ body.is-resizing-dag-graph {
2119
2192
  flex-wrap: wrap;
2120
2193
  gap: 4px;
2121
2194
  }
2122
- .process-timeline-toggle {
2123
- min-height: 28px;
2124
- padding: 0 10px;
2125
- border: 1px solid var(--hairline-strong);
2126
- border-radius: var(--radius-sm);
2127
- background: var(--surface);
2128
- color: var(--body);
2129
- font-size: 11px;
2130
- font-weight: 600;
2131
- cursor: pointer;
2132
- }
2133
- .process-timeline-toggle:hover,
2134
- .process-timeline-toggle:focus-visible {
2135
- border-color: var(--orange);
2136
- color: var(--orange-active);
2137
- outline: none;
2138
- }
2139
- .process-timeline-toggle[aria-pressed="true"] {
2140
- border-color: color-mix(in srgb, var(--orange) 55%, var(--hairline));
2141
- background: #fff5ed;
2142
- color: var(--orange-active);
2143
- }
2144
2195
  .process-timeline-body {
2145
2196
  min-width: 0;
2146
2197
  padding: 4px 0 8px var(--space-2);
@@ -2995,6 +3046,19 @@ body.is-resizing-dag-graph {
2995
3046
  white-space: pre-wrap;
2996
3047
  overflow-wrap: anywhere;
2997
3048
  }
3049
+ .run-output-shell {
3050
+ position: relative;
3051
+ }
3052
+ .run-output-follow-dock {
3053
+ position: absolute;
3054
+ right: 10px;
3055
+ bottom: 10px;
3056
+ z-index: 3;
3057
+ pointer-events: none;
3058
+ }
3059
+ .run-output-follow-dock[hidden] {
3060
+ display: none;
3061
+ }
2998
3062
 
2999
3063
  .artifact-list {
3000
3064
  display: grid;
@@ -5199,3 +5263,160 @@ button.copy-command:focus-visible {
5199
5263
  .prompt-restart-status.prompt-restart-error {
5200
5264
  color: var(--red);
5201
5265
  }
5266
+
5267
+ /* Reader controls stay within reach while the body scrolls. */
5268
+ .dag-inspector.is-expanded { width: calc(100vw - 48px); }
5269
+ .inspect-workspace-root .dag-inspector.is-expanded { width: calc(100% - 32px); }
5270
+ .dag-inspector-header .dag-inspector-attempt-bar { position: relative; top: auto; grid-column: 1 / -1; margin: 0; padding: 8px 0 0; border-bottom: 0; border-top: 1px solid var(--hairline); }
5271
+ .process-timeline-toolbar { position: sticky; top: -16px; z-index: 5; padding: 8px 0; background: var(--surface); border-bottom: 1px solid var(--hairline); }
5272
+ .process-timeline-search { flex: 1 1 160px; min-width: 100px; padding: 6px 8px; border: 1px solid var(--hairline-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); }
5273
+ .process-timeline-item.is-search-match { background: color-mix(in srgb, var(--amber) 12%, var(--surface)); border-radius: var(--radius-sm); }
5274
+ .dag-inspector.is-expanded .inspector-page-lead { max-width: none; }
5275
+ .dag-inspector-content .process-timeline-code, .dag-inspector-content .process-timeline-thinking-body { max-height: none; overflow-y: visible; }
5276
+ @media (max-width: 700px) {
5277
+ .dag-inspector.is-expanded { width: calc(100vw - 24px); }
5278
+ .dag-inspector-controls { flex-wrap: wrap; }
5279
+ .dag-inspector-header { padding: 12px; }
5280
+ .dag-inspector-purpose, .dag-inspector-attempt-label { display: none; }
5281
+ .dag-inspector-attempt-preview { font-size: 11px; }
5282
+ }
5283
+
5284
+ .dag-action-intents, .node-relation-links { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
5285
+ .node-relation-links h5 { width:100%; margin:8px 0 0; }
5286
+ .dag-action-intents [aria-pressed="true"] { border-color:var(--orange); color:var(--orange-active); }
5287
+ .dag-action-card[hidden] { display:none; }
5288
+ .context-component { padding:10px 0; border-top:1px solid var(--hairline); }
5289
+ .context-component > summary { cursor:pointer; overflow-wrap:anywhere; }
5290
+ .context-component-body { margin-top:12px; }
5291
+ .context-record-heading { margin:0; color:var(--ink); font-weight:650; }
5292
+ .context-record-meta { margin:0; color:var(--muted); font-size:12px; line-height:1.6; overflow-wrap:anywhere; }
5293
+ .context-component > summary { display:list-item; color:var(--ink); }
5294
+ .context-component-name { font-weight:600; }
5295
+ .context-component-meta { display:block; margin:4px 0 0 16px; color:var(--muted); font-size:11px; font-weight:400; }
5296
+ .context-source-details { margin-top:12px; color:var(--muted); font-size:12px; }
5297
+ .context-source-details > summary { cursor:pointer; }
5298
+ .context-source-details .node-transparency-summary { margin-top:12px; }
5299
+ .inspector-operation { padding:8px 12px; border:1px solid var(--hairline); border-radius:var(--radius-sm); margin-bottom:8px; overflow-wrap:anywhere; }
5300
+ .dag-graph-toolbar { flex-wrap:wrap; gap:8px; }
5301
+ .dag-graph-search, .relation-task-picker { padding:6px 8px; border:1px solid var(--hairline); border-radius:var(--radius-sm); background:var(--surface); max-width:100%; }
5302
+ #view-dashboard { display:flex; flex-direction:column; }
5303
+ #view-dashboard[hidden] { display:none; }
5304
+ #view-dashboard .dashboard-workspace { order:-2; }
5305
+ #dashboard-kpi { padding-top:8px; padding-bottom:8px; }
5306
+ #dashboard-kpi .kpi-card { padding-top:12px; padding-bottom:12px; }
5307
+ #dashboard-kpi .kpi-value { font-size:24px; }
5308
+
5309
+ .attempt-comparison-toolbar { display:flex; gap:12px; padding-bottom:12px; }
5310
+ .attempt-comparison-columns { display:grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap:16px; }
5311
+ .attempt-comparison-columns > p { grid-column:1/-1; }
5312
+ .attempt-comparison-column { min-width:0; overflow-wrap:anywhere; }
5313
+ @media (max-width:700px) { .attempt-comparison-columns { grid-template-columns:minmax(0,1fr); } }
5314
+
5315
+ /* Compact controls share the existing palette and keep names available on focus. */
5316
+ .observe-icon-button,
5317
+ .dag-graph-toolbar .observe-icon-button {
5318
+ display: inline-grid;
5319
+ place-items: center;
5320
+ flex: 0 0 32px;
5321
+ width: 32px;
5322
+ min-width: 32px;
5323
+ height: 32px;
5324
+ min-height: 32px;
5325
+ padding: 0;
5326
+ border: 1px solid transparent;
5327
+ border-radius: 7px;
5328
+ background: transparent;
5329
+ color: var(--body);
5330
+ font-size: 17px;
5331
+ line-height: 1;
5332
+ box-shadow: none;
5333
+ cursor: pointer;
5334
+ }
5335
+ .observe-icon-button:hover:not(:disabled),
5336
+ .observe-icon-button[aria-pressed="true"] {
5337
+ background: var(--surface-muted);
5338
+ color: var(--ink);
5339
+ border-color: var(--hairline);
5340
+ transform: none;
5341
+ }
5342
+ .observe-icon-button:focus-visible,
5343
+ .observe-select:focus-visible {
5344
+ outline: 2px solid var(--orange);
5345
+ outline-offset: 2px;
5346
+ }
5347
+ .observe-icon-button:disabled { opacity: .35; cursor: default; }
5348
+ .observe-select {
5349
+ min-height: 32px;
5350
+ max-width: 100%;
5351
+ padding: 4px 24px 4px 10px;
5352
+ border: 1px solid var(--hairline);
5353
+ border-radius: 7px;
5354
+ background: var(--surface);
5355
+ color: var(--ink);
5356
+ font: inherit;
5357
+ font-size: 12px;
5358
+ cursor: pointer;
5359
+ }
5360
+ .dag-inspector-header { gap: 10px; padding: 12px 16px; }
5361
+ .dag-inspector-title { gap: 3px; }
5362
+ .dag-inspector-purpose { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5363
+ .dag-inspector-controls { gap: 3px; }
5364
+ .dag-inspector-header .dag-inspector-attempt-bar { gap: 4px; padding-top: 8px; }
5365
+ .dag-inspector-attempt-label { display: none; }
5366
+ .dag-inspector-attempt-preview { flex: 1; gap: 8px; min-width: 0; }
5367
+ .dag-inspector-attempt-category { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
5368
+ .process-timeline-toolbar { gap: 4px; align-items: center; flex-shrink: 0; }
5369
+ .process-timeline-filter-group { flex: 0 0 auto; max-width: 135px; }
5370
+ .process-timeline-search { margin: 0 4px; height: 32px; flex: 1 1 120px; }
5371
+ .process-timeline-toolbar > [role="status"] { font-size: 11px; white-space: nowrap; }
5372
+ .dag-graph-toolbar { gap: 4px; }
5373
+ .dag-graph-toolbar-left { margin-right: auto; }
5374
+ .dag-graph-search { height: 32px; width: 160px; margin-left: 8px; }
5375
+ @media (max-width: 700px) {
5376
+ .dag-inspector-header { padding: 10px 12px; }
5377
+ .dag-inspector-controls { flex-wrap: nowrap; }
5378
+ .dag-inspector-status { padding: 2px 6px; }
5379
+ .dag-inspector-attempt-preview { flex-basis: 100%; padding-left: 36px; }
5380
+ .process-timeline-search { flex-basis: 96px; min-width: 76px; }
5381
+ .dag-graph-toolbar-left { flex: 1 1 100%; }
5382
+ .dag-graph-search { flex: 1; min-width: 96px; }
5383
+ }
5384
+ .observe-icon-button { position: relative; }
5385
+ .observe-icon-button::after {
5386
+ content: attr(data-tooltip);
5387
+ position: absolute;
5388
+ right: 0;
5389
+ top: calc(100% + 6px);
5390
+ z-index: 30;
5391
+ padding: 6px 8px;
5392
+ border-radius: 5px;
5393
+ background: var(--ink);
5394
+ color: var(--surface);
5395
+ font: 11px/1.4 system-ui, sans-serif;
5396
+ white-space: nowrap;
5397
+ opacity: 0;
5398
+ pointer-events: none;
5399
+ }
5400
+ .observe-icon-button:focus-visible::after { opacity: 1; }
5401
+ .risk-dag-items { display: grid; gap: 6px; padding-top: 8px; }
5402
+ /* The hosted Console also has .panel styles; keep the reader shell flush. */
5403
+ #dag-inspector { padding: 0; gap: 0; }
5404
+ .dag-inspector-header .dag-inspector-attempt-bar { flex-wrap: nowrap; background: transparent; }
5405
+ .dag-inspector-header .dag-inspector-attempt-select { min-width: 0; flex: 0 1 126px; }
5406
+ .dag-inspector-header .dag-inspector-attempt-preview { flex: 1 1 auto; flex-wrap: nowrap; padding: 0; }
5407
+ .dag-inspector-header .dag-inspector-attempt-category,
5408
+ .dag-inspector-header .dag-inspector-attempt-duration { display: none; }
5409
+ @container (max-width: 560px) {
5410
+ .process-timeline-toolbar { display: grid; grid-template-columns: minmax(80px, 1fr) 32px 32px 32px; gap: 4px; }
5411
+ .process-timeline-filter-group { grid-row: 1; grid-column: 1 / 3; width: fit-content; }
5412
+ .process-timeline-errors { grid-row: 1; grid-column: 3; }
5413
+ .process-timeline-protocol-toggle { grid-row: 1; grid-column: 4; }
5414
+ .process-timeline-search { grid-row: 2; grid-column: 1 / 3; width: 100%; margin: 0; }
5415
+ .process-search-previous { grid-row: 2; grid-column: 3; }
5416
+ .process-search-next { grid-row: 2; grid-column: 4; }
5417
+ .process-timeline-toolbar > [role="status"] { grid-row: 3; grid-column: 1 / -1; }
5418
+ .process-timeline-toolbar > [role="status"]:empty { display: none; }
5419
+ }
5420
+
5421
+ .process-search-highlight { background: #ffe082; color: #382800; border-radius: 2px; }
5422
+ .process-timeline-item.is-search-current { outline: 2px solid var(--amber); outline-offset: -2px; }
@@ -0,0 +1,4 @@
1
+ export const TASK_FAILURE_UNKNOWN_LABEL: string;
2
+
3
+ export function taskFailureCategoryLabel(category: unknown): string;
4
+ export function followUpLabel(action: unknown): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 异常 Task 页业务文案映射(Observe UI,2026-09-06-observe-reading-and-polish)。
3
+ *
4
+ * 表驱动映射:Task 级 failureCategory(ProductLineFailureCategory)与
5
+ * 建议跟进(FOLLOW_UP_BY_CATEGORY / DAG_RECOVERY_ACTIONS)仅保留 raw token
6
+ * 于 title 等诊断属性,主文案一律中文业务术语。未知或缺失分类 fail-closed
7
+ * 原文透传(异常页是排障入口,未收录 token 比误译更有诊断价值)。
8
+ */
9
+
10
+ /** 表一:Task 失败分类(src/worker/pool/types.ts ProductLineFailureCategory)→ 中文。 */
11
+ const TASK_FAILURE_CATEGORY_LABELS = {
12
+ SpecUnclear: "需求不明确",
13
+ ContractMismatch: "契约不一致",
14
+ ProductBug: "产品缺陷",
15
+ TestBug: "测试缺陷",
16
+ EnvFailure: "环境问题",
17
+ FlakyTest: "测试不稳定",
18
+ RiskyChange: "变更风险",
19
+ DependencyFailure: "依赖阻塞",
20
+ NeedsHuman: "需要人工",
21
+ Unknown: "未分类",
22
+ // run 级补充(read-model:state.json controller-interrupted)。
23
+ "controller-interrupted": "控制器中断",
24
+ };
25
+
26
+ /**
27
+ * 表二:建议跟进/恢复动作 → 中文。并集覆盖
28
+ * src/worker/pool/failure-routing.ts FOLLOW_UP_BY_CATEGORY 与
29
+ * src/workflows/dag/recovery-recommendation.ts DAG_RECOVERY_ACTIONS。
30
+ */
31
+ const FOLLOW_UP_LABELS = {
32
+ "spec-clarification": "澄清需求",
33
+ "architecture-contract-fix": "修订契约",
34
+ "dev-fix": "开发修复",
35
+ "qa-fix-test": "修复测试",
36
+ "env-fix": "修复环境",
37
+ "flaky-test-analysis": "分析不稳定测试",
38
+ "human-review": "人工审查",
39
+ "manual-review": "人工审查",
40
+ "unblock-dependency": "解除依赖阻塞",
41
+ "human-triage": "人工分诊",
42
+ none: "无需处理",
43
+ monitor: "持续观察",
44
+ "retry-node": "重试节点",
45
+ "rerun-after-fix": "修复后重跑",
46
+ "resume-or-reject": "恢复或拒绝",
47
+ "inspect-upstream": "检查上游",
48
+ };
49
+
50
+ export const TASK_FAILURE_UNKNOWN_LABEL = "未分类";
51
+
52
+ /**
53
+ * Task 失败分类 → 中文;未知英文 token 原样透传(title 仍可标注 raw),
54
+ * 空值返回空串由调用方决定占位。
55
+ */
56
+ export function taskFailureCategoryLabel(category) {
57
+ const raw = String(category ?? "").trim();
58
+ if (!raw) return "";
59
+ return TASK_FAILURE_CATEGORY_LABELS[raw] ?? raw;
60
+ }
61
+
62
+ /** 建议跟进/恢复动作 → 中文;未知 token 原样透传,空值返回空串。 */
63
+ export function followUpLabel(action) {
64
+ const raw = String(action ?? "").trim();
65
+ if (!raw) return "";
66
+ return FOLLOW_UP_LABELS[raw] ?? raw;
67
+ }
@@ -0,0 +1,12 @@
1
+ /** Merge poll/older pages without dropping the pages the reader loaded. */
2
+ export function mergeTaskHistoryPage(state, page, older = false) {
3
+ const existing = new Map(state.runs.map((run) => [run.workerRunId, run]));
4
+ for (const run of page.runs ?? []) existing.set(run.workerRunId, run);
5
+ const order = older ? [...state.runs, ...(page.runs ?? [])] : [...(page.runs ?? []), ...state.runs];
6
+ const seen = new Set();
7
+ state.runs = order.filter((run) => { if (seen.has(run.workerRunId)) return false; seen.add(run.workerRunId); return true; }).map((run) => existing.get(run.workerRunId));
8
+ if (older || !state.loadedOlder) state.nextBefore = page.nextBefore ?? null;
9
+ if (older) state.loadedOlder = true;
10
+ state.error = null;
11
+ return state;
12
+ }
@@ -1,3 +1,4 @@
1
+ import { bindManualViewRefresh, mountTaskRelationPicker } from "../shell-chrome.js";
1
2
  /** Observe UI view module (R5). */
2
3
  import {
3
4
  UI_TEXT,
@@ -19,6 +20,7 @@ import {
19
20
  import { clearNode, el, metaGrid, buildTable } from "../dom.js";
20
21
  import {
21
22
  badge,
23
+ batchStatusBadge,
22
24
  badgeClass,
23
25
  statusLabel,
24
26
  formatTs,
@@ -71,6 +73,7 @@ import { mountBoundedEventTimeline } from "./timeline.js";
71
73
 
72
74
  export async function renderBatch(batchRunId) {
73
75
  showView("batch");
76
+ bindManualViewRefresh("batch", () => renderBatch(batchRunId));
74
77
  setBreadcrumb([
75
78
  { label: UI_TEXT.dashboard, href: "#/" },
76
79
  { label: batchRunId },
@@ -88,7 +91,7 @@ export async function renderBatch(batchRunId) {
88
91
  ]);
89
92
  if (snapshot) {
90
93
  uiState.lastSnapshot = snapshot;
91
- updateHeaderRefresh(snapshot.generatedAt);
94
+ updateHeaderRefresh(snapshot.generatedAt, { mode: "manual" });
92
95
  }
93
96
  if (!batch) {
94
97
  clearNode(metaEl);
@@ -100,17 +103,7 @@ export async function renderBatch(batchRunId) {
100
103
  return;
101
104
  }
102
105
 
103
- mountRelationBar(
104
- "batch-relations",
105
- {
106
- featureId: batch.featureId,
107
- batchRunId: batch.batchRunId,
108
- taskId: batch.tasks?.[0]?.taskId,
109
- workerRunId: batch.tasks?.[0]?.workerRunId,
110
- dagRunId: batch.tasks?.[0]?.dagRunId,
111
- },
112
- snapshot,
113
- );
106
+ mountTaskRelationPicker("batch-relations", { featureId: batch.featureId, batchRunId: batch.batchRunId }, (batch.tasks ?? []).map((task) => ({ ...task, featureId: task.featureId ?? batch.featureId })), snapshot);
114
107
 
115
108
  const totalDerived = derivedDurationMs(batch.startedAt, batch.finishedAt);
116
109
  clearNode(metaEl);
@@ -123,7 +116,7 @@ export async function renderBatch(batchRunId) {
123
116
  ? objectRouteLink("feature", batch.featureId, snapshot)
124
117
  : el("span", "muted", "—"),
125
118
  ],
126
- ["状态", badge(batch.status)],
119
+ ["状态", batchStatusBadge(batch)],
127
120
  ["开始时间", formatTs(batch.startedAt)],
128
121
  ["结束时间", formatTs(batch.finishedAt)],
129
122
  ["总耗时", formatDuration(totalDerived)],
@@ -6,7 +6,7 @@ import {
6
6
  edgeCollidesNonEndpointNodes,
7
7
  routeControlEdge,
8
8
  } from "../dag-edge-routing.js";
9
- import { el } from "../dom.js";
9
+ import { el, iconButton } from "../dom.js";
10
10
  import {
11
11
  statusLabel,
12
12
  statusSymbol,
@@ -23,7 +23,7 @@ import {
23
23
  currentDagGraphViewMode,
24
24
  } from "../state.js";
25
25
  import { describeDagNodePurpose } from "../dag-node-purpose.js";
26
- import { selectDagNode } from "./dag-inspector.js";
26
+ import { selectDagNode, selectDagExecution } from "./dag-inspector.js";
27
27
  import { renderTrajectoryStoryboard } from "./dag-trajectory.js";
28
28
 
29
29
  export function svgEl(tag, attrs = {}) {
@@ -138,6 +138,10 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
138
138
  controlLaneCount,
139
139
  });
140
140
  const wrap = existingWrap ?? el("div", "dag-graph-wrap");
141
+ const active = document.activeElement;
142
+ const focusedNodeId = existingWrap?.contains?.(active) ? active?.getAttribute?.("data-node-id") : null;
143
+ const focusedAction = existingWrap?.contains?.(active) ? active?.dataset?.graphAction : null;
144
+ const previousSearch = existingWrap?.querySelector(".dag-graph-search")?.value ?? "";
141
145
  // Preserve .dag-graph-viewport DOM identity across poll refreshes so the
142
146
  // user's active scroll/pointer target is not destroyed mid-gesture.
143
147
  let viewport =
@@ -169,7 +173,7 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
169
173
  }
170
174
  toolbarLeft.appendChild(el("span", "dag-graph-summary", summaryText));
171
175
  toolbar.appendChild(toolbarLeft);
172
- const reset = el("button", "dag-graph-reset", "重置视图");
176
+ const reset = iconButton("重置视图", "ri-restart-line", "dag-graph-reset");
173
177
  toolbar.appendChild(reset);
174
178
  const appendBeforeViewport = (child) => {
175
179
  if (viewport?.parentNode === wrap) wrap.insertBefore(child, viewport);
@@ -339,6 +343,7 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
339
343
  const purpose = describeDagNodePurpose(node.nodeId, node);
340
344
  const group = svgEl("g", {
341
345
  transform: `translate(${position.x} ${position.y})`,
346
+ "data-node-id": node.nodeId,
342
347
  class: `dag-graph-node status-${status}${node.nodeId === uiState.selectedDagNodeId ? " selected" : ""}${uiState.selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`,
343
348
  tabindex: 0,
344
349
  role: "button",
@@ -402,6 +407,9 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
402
407
  });
403
408
  svg.appendChild(group);
404
409
  }
410
+ const scale = Number(viewport.dataset.scale) || 1;
411
+ svg.style.width = `${Number(svg.getAttribute("width")) * scale}px`;
412
+ svg.style.height = `${Number(svg.getAttribute("height")) * scale}px`;
405
413
  viewport.appendChild(svg);
406
414
  }
407
415
  if (viewport.parentNode !== wrap) wrap.appendChild(viewport);
@@ -476,9 +484,48 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
476
484
  }
477
485
  }
478
486
  }
487
+ const findNode = (id) => {
488
+ const matches = [...viewport.querySelectorAll("[data-node-id]")].filter((element) => element.getAttribute("data-node-id") === id);
489
+ const key = uiState.dagInspectorAttempt?.nodeId === id ? uiState.dagInspectorAttempt.key : null;
490
+ return matches.find((element) => key && element.getAttribute("data-execution-key") === key) ?? matches.at(-1);
491
+ };
492
+ const center = (id, executionId = null) => {
493
+ const target = executionId ? [...viewport.querySelectorAll("[data-execution-id]")].find((item) => item.getAttribute("data-execution-id") === executionId) : findNode(id);
494
+ if (!target) return;
495
+ const rect = target.getBoundingClientRect(), container = viewport.getBoundingClientRect();
496
+ viewport.scrollTo({ left: viewport.scrollLeft + rect.left - container.left - (container.width - rect.width) / 2, top: viewport.scrollTop + rect.top - container.top - (container.height - rect.height) / 2, behavior: "smooth" });
497
+ };
498
+ const addAction = (id, label, action) => { const icons = { selected: "ri-focus-3-line", running: "ri-play-circle-line", failed: "ri-error-warning-line", fit: "ri-fullscreen-line" }; const button = iconButton(label, icons[id], "dag-graph-reset"); button.type = "button"; button.dataset.graphAction = id; button.addEventListener("click", action); toolbar.appendChild(button); return button; };
499
+ addAction("selected", "定位选中", () => center(uiState.selectedDagNodeId)).disabled = !findNode(uiState.selectedDagNodeId);
500
+ addAction("running", "定位运行中", () => { const node = nodes.find((item) => String(item.status).toLowerCase() === "running"); if (node) center(node.nodeId); }).disabled = !nodes.some((item) => String(item.status).toLowerCase() === "running");
501
+ const failedNodes = (mode === "trajectory" ? dag.executionTrajectory?.occurrences ?? [] : nodes).filter((item) => ["error", "failed"].includes(String(item.status).toLowerCase()));
502
+ const failed = addAction("failed", `下个失败 (${failedNodes.length})`, () => {
503
+ if (!failedNodes.length) return;
504
+ const index = failedNodes.findIndex((item) => item.nodeId === uiState.selectedDagNodeId && (mode !== "trajectory" || (item.pass ? `pass-${item.pass}` : null) === (uiState.dagInspectorAttempt?.key ?? null)));
505
+ const node = failedNodes[(index + 1) % failedNodes.length];
506
+ if (mode === "trajectory") selectDagExecution(dagRunId, node.nodeId, node.pass ? `pass-${node.pass}` : null);
507
+ else selectDagNode(dagRunId, node.nodeId);
508
+ requestAnimationFrame(() => center(node.nodeId, node.executionId));
509
+ });
510
+ failed.disabled = !failedNodes.some((node) => findNode(node.nodeId));
511
+ const fit = addAction("fit", "适应画布", () => {
512
+ const svg = viewport.querySelector("svg.dag-graph"); if (!svg) return;
513
+ const width = Number(svg.getAttribute("width")), height = Number(svg.getAttribute("height"));
514
+ const scale = Math.min(1, (viewport.clientWidth - 24) / width, (viewport.clientHeight - 24) / height);
515
+ if (!(scale > 0)) return;
516
+ viewport.dataset.scale = String(scale); svg.style.width = `${width * scale}px`; svg.style.height = `${height * scale}px`; viewport.scrollTo({ left: 0, top: 0 });
517
+ });
518
+ fit.disabled = mode !== "topology";
519
+ const search = el("input", "dag-graph-search"); search.type = "search"; search.placeholder = "查找节点…"; search.value = previousSearch; search.setAttribute("aria-label", "查找流程节点"); search.dataset.graphAction = "search";
520
+ search.addEventListener("keydown", (event) => { if (event.key !== "Enter" || event.isComposing) return; event.preventDefault(); const matches = nodes.filter((item) => `${item.nodeId} ${describeDagNodePurpose(item.nodeId, item)}`.toLocaleLowerCase().includes(search.value.trim().toLocaleLowerCase())); if (!matches.length) return; const index = matches.findIndex((item) => item.nodeId === uiState.selectedDagNodeId); const node = matches[(index + 1) % matches.length]; center(node.nodeId); selectDagNode(dagRunId, node.nodeId); }); toolbar.appendChild(search);
521
+ if (focusedNodeId) findNode(focusedNodeId)?.focus?.({ preventScroll: true });
522
+ else if (focusedAction) [...toolbar.querySelectorAll("[data-graph-action]")].find((item) => item.dataset.graphAction === focusedAction)?.focus?.({ preventScroll: true });
479
523
  // Toolbar is rebuilt each paint; bind reset to the current viewport/dagRunId.
480
524
  // 重置视图只重置当前模式的 viewport:不切换模式、不重置另一模式、不关闭 Inspector。
481
525
  reset.addEventListener("click", () => {
526
+ viewport.dataset.scale = "1";
527
+ const svg = viewport.querySelector("svg.dag-graph");
528
+ if (svg) { svg.style.width = `${svg.getAttribute("width")}px`; svg.style.height = `${svg.getAttribute("height")}px`; }
482
529
  if (mode === "trajectory") {
483
530
  uiState.dagTrajectoryViewportState = updateDagTrajectoryViewportState(
484
531
  uiState.dagTrajectoryViewportState,