@tea-agent/loop-agent 0.32.1 → 0.33.1

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 (40) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/executors/model-routing.js +14 -4
  3. package/dist/governance/manifest-types.js +34 -7
  4. package/dist/worker/console/chat/model-resolver.js +114 -34
  5. package/dist/worker/console/chat/workspace-landing.js +58 -22
  6. package/dist/worker/console/doctor.js +1 -0
  7. package/dist/worker/console/night-aux-ticker.js +5 -0
  8. package/dist/worker/console/operator-surface-health.js +1 -0
  9. package/dist/worker/console/pi-readiness.js +26 -17
  10. package/dist/worker/console/server.js +2 -0
  11. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  12. package/dist/worker/console/static/assets/index-PzYzcuFG.js +29 -0
  13. package/dist/worker/console/static/index.html +3 -2
  14. package/dist/worker/console/static-src/app/useRecoveryConsole.js +3 -2
  15. package/dist/worker/console/static-src/night/useNightBoard.js +0 -31
  16. package/dist/worker/observability/read-model.js +106 -0
  17. package/dist/worker/observe/routes.js +22 -9
  18. package/dist/worker/observe/static/api.js +42 -3
  19. package/dist/worker/observe/static/app.js +15 -0
  20. package/dist/worker/observe/static/constants.js +10 -0
  21. package/dist/worker/observe/static/custom-select.js +567 -0
  22. package/dist/worker/observe/static/index.html +56 -47
  23. package/dist/worker/observe/static/kpi.js +2 -24
  24. package/dist/worker/observe/static/operator-chrome.css +480 -0
  25. package/dist/worker/observe/static/operator-chrome.d.ts +82 -0
  26. package/dist/worker/observe/static/operator-chrome.js +554 -0
  27. package/dist/worker/observe/static/router.js +54 -1
  28. package/dist/worker/observe/static/shell-chrome.js +1 -11
  29. package/dist/worker/observe/static/state.js +20 -0
  30. package/dist/worker/observe/static/styles.css +680 -299
  31. package/dist/worker/observe/static/views/dag-inspector.js +20 -17
  32. package/dist/worker/observe/static/views/dag.js +136 -59
  33. package/dist/worker/observe/static/views/dags.js +877 -0
  34. package/dist/worker/observe/static/views/dashboard.js +67 -8
  35. package/dist/workflows/dag/init-hybrid.js +58 -29
  36. package/docs/templates/harness.schema.json +5 -0
  37. package/harness.json +4 -4
  38. package/package.json +1 -1
  39. package/dist/worker/console/static/assets/index-Bpa2qrc-.js +0 -29
  40. package/dist/worker/console/static/assets/index-BqfFDdnG.css +0 -1
@@ -0,0 +1,567 @@
1
+ /** Observe UI — shared custom select (trigger + listbox).
2
+ *
3
+ * UI-only component: holds ephemeral open/active state, never writes state.js
4
+ * or persistence, and never fetches. Callers own business callbacks.
5
+ */
6
+
7
+ const MENU_GAP_PX = 7;
8
+ const VIEWPORT_PAD_PX = 8;
9
+ const MAX_VISIBLE_OPTIONS = 6;
10
+ const OPTION_HEIGHT_PX = 38;
11
+
12
+ /** @type {null | { close: (opts?: { restoreFocus?: boolean }) => void }} */
13
+ let openInstance = null;
14
+
15
+ let idSeq = 0;
16
+ function nextId(prefix) {
17
+ idSeq += 1;
18
+ return `${prefix}-${idSeq}`;
19
+ }
20
+
21
+ function safeAddListener(target, type, handler, options) {
22
+ if (!target || typeof target.addEventListener !== "function") return () => {};
23
+ target.addEventListener(type, handler, options);
24
+ return () => {
25
+ if (typeof target.removeEventListener === "function") {
26
+ target.removeEventListener(type, handler, options);
27
+ }
28
+ };
29
+ }
30
+
31
+ function optionLabel(options, value) {
32
+ const hit = options.find((o) => String(o.value) === String(value));
33
+ return hit ? String(hit.label) : value == null ? "" : String(value);
34
+ }
35
+
36
+ function clamp(n, min, max) {
37
+ return Math.min(max, Math.max(min, n));
38
+ }
39
+
40
+ /**
41
+ * @param {{
42
+ * ariaLabel: string,
43
+ * value: string,
44
+ * options: Array<{ value: string, label: string }>,
45
+ * onChange: (value: string) => void,
46
+ * className?: string,
47
+ * disabled?: boolean,
48
+ * id?: string,
49
+ * }} config
50
+ * @returns {HTMLElement}
51
+ */
52
+ export function createCustomSelect(config) {
53
+ const {
54
+ ariaLabel,
55
+ options: rawOptions = [],
56
+ onChange,
57
+ className = "",
58
+ disabled = false,
59
+ id,
60
+ } = config;
61
+ const options = Array.isArray(rawOptions) ? rawOptions.slice() : [];
62
+ let value =
63
+ config.value != null && config.value !== ""
64
+ ? String(config.value)
65
+ : options[0]
66
+ ? String(options[0].value)
67
+ : "";
68
+
69
+ const root = document.createElement("div");
70
+ root.className = ["custom-select", className].filter(Boolean).join(" ");
71
+ root.dataset.customSelect = "1";
72
+ if (id) root.id = id;
73
+
74
+ const listboxId = nextId("custom-select-listbox");
75
+ const trigger = document.createElement("button");
76
+ trigger.type = "button";
77
+ trigger.className = "custom-select-trigger";
78
+ trigger.setAttribute("aria-haspopup", "listbox");
79
+ trigger.setAttribute("aria-expanded", "false");
80
+ trigger.setAttribute("aria-controls", listboxId);
81
+ if (ariaLabel) trigger.setAttribute("aria-label", ariaLabel);
82
+ trigger.disabled = Boolean(disabled);
83
+
84
+ const valueEl = document.createElement("span");
85
+ valueEl.className = "custom-select-value";
86
+ valueEl.textContent = optionLabel(options, value);
87
+
88
+ const chevron = document.createElement("i");
89
+ chevron.className = "ri-arrow-down-s-line custom-select-chevron";
90
+ chevron.setAttribute("aria-hidden", "true");
91
+
92
+ trigger.appendChild(valueEl);
93
+ trigger.appendChild(chevron);
94
+ root.appendChild(trigger);
95
+
96
+ const menu = document.createElement("div");
97
+ menu.className = "custom-select-menu";
98
+ menu.id = listboxId;
99
+ menu.setAttribute("role", "listbox");
100
+ if (ariaLabel) menu.setAttribute("aria-label", ariaLabel);
101
+ menu.hidden = true;
102
+ menu.tabIndex = -1;
103
+ root.appendChild(menu);
104
+
105
+ /** @type {HTMLElement[]} */
106
+ let optionNodes = [];
107
+ /** @type {number} */
108
+ let activeIndex = 0;
109
+ /** @type {Array<() => void>} */
110
+ let detachGlobal = [];
111
+ let isOpen = false;
112
+
113
+ function rebuildOptions() {
114
+ while (menu.firstChild) menu.removeChild(menu.firstChild);
115
+ optionNodes = [];
116
+ for (let i = 0; i < options.length; i++) {
117
+ const opt = options[i];
118
+ const optionEl = document.createElement("div");
119
+ optionEl.className = "custom-select-option";
120
+ optionEl.setAttribute("role", "option");
121
+ optionEl.dataset.value = String(opt.value);
122
+ // Mirror raw value for integration stubs that still read `.value`.
123
+ optionEl.value = String(opt.value);
124
+ optionEl.setAttribute("data-value", String(opt.value));
125
+ optionEl.id = `${listboxId}-opt-${i}`;
126
+ const selected = String(opt.value) === String(value);
127
+ optionEl.setAttribute("aria-selected", selected ? "true" : "false");
128
+ if (selected) optionEl.classList.add("is-selected");
129
+
130
+ const label = document.createElement("span");
131
+ label.className = "custom-select-option-label";
132
+ label.textContent = String(opt.label);
133
+ optionEl.appendChild(label);
134
+
135
+ const check = document.createElement("i");
136
+ check.className = "ri-check-line custom-select-option-check";
137
+ check.setAttribute("aria-hidden", "true");
138
+ optionEl.appendChild(check);
139
+
140
+ optionEl.addEventListener("click", (event) => {
141
+ if (event && typeof event.preventDefault === "function") {
142
+ event.preventDefault();
143
+ }
144
+ if (event && typeof event.stopPropagation === "function") {
145
+ event.stopPropagation();
146
+ }
147
+ commitValue(String(opt.value));
148
+ });
149
+ optionEl.addEventListener("mouseenter", () => {
150
+ setActiveIndex(i, { scroll: false });
151
+ });
152
+
153
+ menu.appendChild(optionEl);
154
+ optionNodes.push(optionEl);
155
+ }
156
+ // Keep a hidden native-like options mirror for limited test stubs that
157
+ // still inspect OPTION children; not used for interaction.
158
+ // (Not a focusable <select>; no native menu.)
159
+ }
160
+
161
+ function syncSelectedClasses() {
162
+ for (const node of optionNodes) {
163
+ const selected = node.dataset.value === String(value);
164
+ node.setAttribute("aria-selected", selected ? "true" : "false");
165
+ if (selected) node.classList.add("is-selected");
166
+ else node.classList.remove("is-selected");
167
+ }
168
+ valueEl.textContent = optionLabel(options, value);
169
+ }
170
+
171
+ function setActiveIndex(index, { scroll = true } = {}) {
172
+ if (optionNodes.length === 0) {
173
+ activeIndex = 0;
174
+ return;
175
+ }
176
+ activeIndex = clamp(index, 0, optionNodes.length - 1);
177
+ for (let i = 0; i < optionNodes.length; i++) {
178
+ const node = optionNodes[i];
179
+ if (i === activeIndex) {
180
+ node.classList.add("is-active");
181
+ if (scroll && typeof node.scrollIntoView === "function") {
182
+ try {
183
+ node.scrollIntoView({ block: "nearest" });
184
+ } catch {
185
+ node.scrollIntoView();
186
+ }
187
+ }
188
+ } else {
189
+ node.classList.remove("is-active");
190
+ }
191
+ }
192
+ const active = optionNodes[activeIndex];
193
+ if (active) {
194
+ menu.setAttribute("aria-activedescendant", active.id);
195
+ }
196
+ }
197
+
198
+ function indexOfValue(v) {
199
+ const idx = options.findIndex((o) => String(o.value) === String(v));
200
+ return idx >= 0 ? idx : 0;
201
+ }
202
+
203
+ function positionMenu() {
204
+ // Prefer absolute positioning within the root so sticky bars keep
205
+ // correct stacking without needing a body portal.
206
+ menu.style.position = "absolute";
207
+ menu.style.left = "0";
208
+ menu.style.right = "auto";
209
+ menu.style.minWidth = "100%";
210
+ menu.style.width = "max-content";
211
+ menu.style.maxWidth = "";
212
+ menu.style.top = "";
213
+ menu.style.bottom = "";
214
+ menu.classList.remove("is-open-up");
215
+ menu.classList.add("is-open-down");
216
+
217
+ const triggerRect =
218
+ typeof trigger.getBoundingClientRect === "function"
219
+ ? trigger.getBoundingClientRect()
220
+ : null;
221
+ const viewportW =
222
+ typeof window !== "undefined" && window.innerWidth
223
+ ? window.innerWidth
224
+ : 1024;
225
+ const viewportH =
226
+ typeof window !== "undefined" && window.innerHeight
227
+ ? window.innerHeight
228
+ : 768;
229
+
230
+ const maxMenuHeight = MAX_VISIBLE_OPTIONS * OPTION_HEIGHT_PX + 8;
231
+ menu.style.maxHeight = `${maxMenuHeight}px`;
232
+
233
+ if (!triggerRect) {
234
+ menu.style.top = `calc(100% + ${MENU_GAP_PX}px)`;
235
+ return;
236
+ }
237
+
238
+ const spaceBelow = viewportH - triggerRect.bottom - VIEWPORT_PAD_PX;
239
+ const spaceAbove = triggerRect.top - VIEWPORT_PAD_PX;
240
+ const openUp =
241
+ spaceBelow < Math.min(maxMenuHeight, OPTION_HEIGHT_PX * 3) &&
242
+ spaceAbove > spaceBelow;
243
+
244
+ if (openUp) {
245
+ menu.classList.remove("is-open-down");
246
+ menu.classList.add("is-open-up");
247
+ menu.style.top = "auto";
248
+ menu.style.bottom = `calc(100% + ${MENU_GAP_PX}px)`;
249
+ menu.style.maxHeight = `${Math.min(maxMenuHeight, Math.max(OPTION_HEIGHT_PX, spaceAbove - MENU_GAP_PX))}px`;
250
+ } else {
251
+ menu.style.top = `calc(100% + ${MENU_GAP_PX}px)`;
252
+ menu.style.bottom = "auto";
253
+ menu.style.maxHeight = `${Math.min(maxMenuHeight, Math.max(OPTION_HEIGHT_PX, spaceBelow - MENU_GAP_PX))}px`;
254
+ }
255
+
256
+ // Clamp width to viewport so narrow screens do not overflow.
257
+ const maxWidth = Math.max(80, viewportW - VIEWPORT_PAD_PX * 2);
258
+ menu.style.maxWidth = `${maxWidth}px`;
259
+ // If the root is near the right edge, pin the menu to the trigger's
260
+ // right side so it grows leftward within the viewport.
261
+ const overflowRight = triggerRect.left + triggerRect.width > viewportW - VIEWPORT_PAD_PX;
262
+ if (overflowRight || triggerRect.left < VIEWPORT_PAD_PX) {
263
+ const left = clamp(
264
+ triggerRect.left,
265
+ VIEWPORT_PAD_PX,
266
+ viewportW - VIEWPORT_PAD_PX - Math.min(triggerRect.width, maxWidth),
267
+ );
268
+ // Switch to fixed coordinates only when we need viewport clamping
269
+ // beyond the local anchor box.
270
+ if (left !== triggerRect.left || triggerRect.width > maxWidth) {
271
+ menu.style.position = "fixed";
272
+ menu.style.left = `${left}px`;
273
+ menu.style.minWidth = `${Math.min(triggerRect.width, maxWidth)}px`;
274
+ menu.style.width = "auto";
275
+ if (openUp) {
276
+ menu.style.top = "auto";
277
+ menu.style.bottom = `${viewportH - triggerRect.top + MENU_GAP_PX}px`;
278
+ } else {
279
+ menu.style.top = `${triggerRect.bottom + MENU_GAP_PX}px`;
280
+ menu.style.bottom = "auto";
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ function clearGlobalListeners() {
287
+ for (const off of detachGlobal) {
288
+ try {
289
+ off();
290
+ } catch {
291
+ // ignore stub cleanup failures
292
+ }
293
+ }
294
+ detachGlobal = [];
295
+ }
296
+
297
+ function closeMenu({ restoreFocus = false } = {}) {
298
+ if (!isOpen) {
299
+ if (restoreFocus && typeof trigger.focus === "function") {
300
+ try {
301
+ trigger.focus({ preventScroll: true });
302
+ } catch {
303
+ trigger.focus();
304
+ }
305
+ }
306
+ return;
307
+ }
308
+ isOpen = false;
309
+ menu.hidden = true;
310
+ trigger.setAttribute("aria-expanded", "false");
311
+ root.classList.remove("is-open");
312
+ menu.classList.remove("is-open-up", "is-open-down");
313
+ menu.removeAttribute("aria-activedescendant");
314
+ clearGlobalListeners();
315
+ if (openInstance && openInstance.root === root) {
316
+ openInstance = null;
317
+ }
318
+ if (restoreFocus && typeof trigger.focus === "function") {
319
+ try {
320
+ trigger.focus({ preventScroll: true });
321
+ } catch {
322
+ trigger.focus();
323
+ }
324
+ }
325
+ }
326
+
327
+ function openMenu() {
328
+ if (trigger.disabled || options.length === 0) return;
329
+ if (openInstance && openInstance.root !== root) {
330
+ openInstance.close({ restoreFocus: false });
331
+ }
332
+ isOpen = true;
333
+ menu.hidden = false;
334
+ trigger.setAttribute("aria-expanded", "true");
335
+ root.classList.add("is-open");
336
+ setActiveIndex(indexOfValue(value), { scroll: true });
337
+ positionMenu();
338
+
339
+ const onPointerDown = (event) => {
340
+ const target = event?.target;
341
+ if (!target) return;
342
+ const inRoot =
343
+ typeof root.contains === "function"
344
+ ? root.contains(target)
345
+ : target === root || target === trigger || target === menu;
346
+ if (inRoot) return;
347
+ // closest walk for limited stubs
348
+ if (typeof target.closest === "function") {
349
+ if (target.closest("[data-custom-select]")) {
350
+ // Another custom-select trigger: let that instance open,
351
+ // which will close us via openInstance single-open.
352
+ const other = target.closest("[data-custom-select]");
353
+ if (other === root) return;
354
+ }
355
+ }
356
+ closeMenu({ restoreFocus: false });
357
+ };
358
+ const onResize = () => {
359
+ if (isOpen) positionMenu();
360
+ };
361
+ const onScroll = (event) => {
362
+ if (!isOpen) return;
363
+ const t = event?.target;
364
+ // Ignore scrolls inside the menu itself.
365
+ if (t && (t === menu || (typeof menu.contains === "function" && menu.contains(t)))) {
366
+ return;
367
+ }
368
+ positionMenu();
369
+ };
370
+ // Sole open-state keyboard owner: document capture. Trigger only opens
371
+ // while closed so capture→target never double-steps or re-opens.
372
+ const onKeyDown = (event) => {
373
+ if (!isOpen) return;
374
+ const key = event?.key;
375
+ if (!key) return;
376
+ const stop = () => {
377
+ if (typeof event.preventDefault === "function") event.preventDefault();
378
+ if (typeof event.stopPropagation === "function") event.stopPropagation();
379
+ };
380
+ if (key === "Escape") {
381
+ stop();
382
+ closeMenu({ restoreFocus: true });
383
+ return;
384
+ }
385
+ if (key === "Tab") {
386
+ // Close without trapping focus; browser moves focus naturally.
387
+ closeMenu({ restoreFocus: false });
388
+ return;
389
+ }
390
+ if (key === "ArrowDown") {
391
+ stop();
392
+ setActiveIndex(activeIndex + 1);
393
+ return;
394
+ }
395
+ if (key === "ArrowUp") {
396
+ stop();
397
+ setActiveIndex(activeIndex - 1);
398
+ return;
399
+ }
400
+ if (key === "Home") {
401
+ stop();
402
+ setActiveIndex(0);
403
+ return;
404
+ }
405
+ if (key === "End") {
406
+ stop();
407
+ setActiveIndex(optionNodes.length - 1);
408
+ return;
409
+ }
410
+ if (key === "Enter" || key === " ") {
411
+ stop();
412
+ const opt = options[activeIndex];
413
+ if (opt) commitValue(String(opt.value));
414
+ }
415
+ };
416
+
417
+ detachGlobal.push(safeAddListener(document, "pointerdown", onPointerDown, true));
418
+ // Fallback for environments without pointer events.
419
+ detachGlobal.push(safeAddListener(document, "mousedown", onPointerDown, true));
420
+ detachGlobal.push(safeAddListener(window, "resize", onResize));
421
+ detachGlobal.push(safeAddListener(document, "scroll", onScroll, true));
422
+ detachGlobal.push(safeAddListener(document, "keydown", onKeyDown, true));
423
+
424
+ openInstance = {
425
+ root,
426
+ close: closeMenu,
427
+ };
428
+
429
+ if (typeof menu.focus === "function") {
430
+ try {
431
+ menu.focus({ preventScroll: true });
432
+ } catch {
433
+ menu.focus();
434
+ }
435
+ }
436
+ }
437
+
438
+ function commitValue(next) {
439
+ const nextValue = String(next);
440
+ const prev = value;
441
+ value = nextValue;
442
+ syncSelectedClasses();
443
+ closeMenu({ restoreFocus: true });
444
+ if (nextValue !== String(prev) && typeof onChange === "function") {
445
+ onChange(nextValue);
446
+ } else if (nextValue === String(prev) && typeof onChange === "function") {
447
+ // Still notify when user explicitly re-selects the same option so
448
+ // callers that rely on change semantics stay consistent; history
449
+ // page clamps hash and no-ops identical query.
450
+ onChange(nextValue);
451
+ }
452
+ }
453
+
454
+ function toggleMenu() {
455
+ if (isOpen) closeMenu({ restoreFocus: true });
456
+ else openMenu();
457
+ }
458
+
459
+ trigger.addEventListener("click", (event) => {
460
+ if (event && typeof event.preventDefault === "function") event.preventDefault();
461
+ if (event && typeof event.stopPropagation === "function") event.stopPropagation();
462
+ if (trigger.disabled) return;
463
+ toggleMenu();
464
+ });
465
+
466
+ trigger.addEventListener("keydown", (event) => {
467
+ if (trigger.disabled) return;
468
+ // Open-state keys are owned exclusively by the document capture listener.
469
+ if (isOpen) return;
470
+ const key = event?.key;
471
+ if (key === "Enter" || key === " ") {
472
+ if (typeof event.preventDefault === "function") event.preventDefault();
473
+ openMenu();
474
+ return;
475
+ }
476
+ if (key === "ArrowDown" || key === "ArrowUp") {
477
+ if (typeof event.preventDefault === "function") event.preventDefault();
478
+ openMenu();
479
+ }
480
+ });
481
+
482
+ // Support limited test stubs that set `.value` then fire a `change` handler
483
+ // (legacy native <select> contract used by inspector unit tests).
484
+ Object.defineProperty(root, "value", {
485
+ configurable: true,
486
+ enumerable: true,
487
+ get() {
488
+ return value;
489
+ },
490
+ set(next) {
491
+ value = next == null ? "" : String(next);
492
+ syncSelectedClasses();
493
+ },
494
+ });
495
+ Object.defineProperty(root, "disabled", {
496
+ configurable: true,
497
+ enumerable: true,
498
+ get() {
499
+ return Boolean(trigger.disabled);
500
+ },
501
+ set(next) {
502
+ trigger.disabled = Boolean(next);
503
+ if (trigger.disabled) {
504
+ root.classList.add("is-disabled");
505
+ closeMenu({ restoreFocus: false });
506
+ } else {
507
+ root.classList.remove("is-disabled");
508
+ }
509
+ // Mirror disabled onto root for query paths that check el.disabled.
510
+ root.setAttribute("aria-disabled", trigger.disabled ? "true" : "false");
511
+ if (trigger.disabled) root.dataset.disabled = "true";
512
+ else delete root.dataset.disabled;
513
+ },
514
+ });
515
+
516
+ // Also mirror disabled/value onto the trigger for attribute-based queries.
517
+ Object.defineProperty(trigger, "value", {
518
+ configurable: true,
519
+ enumerable: true,
520
+ get() {
521
+ return value;
522
+ },
523
+ set(next) {
524
+ root.value = next;
525
+ },
526
+ });
527
+
528
+ root.addEventListener("change", () => {
529
+ // Invoked by stubs via handlers.change() after assigning .value.
530
+ if (typeof onChange === "function") onChange(String(value));
531
+ });
532
+
533
+ // Public control surface for callers that re-render with new props.
534
+ root.setCustomSelectDisabled = (next) => {
535
+ root.disabled = Boolean(next);
536
+ };
537
+ root.setCustomSelectValue = (next) => {
538
+ root.value = next;
539
+ };
540
+ root.getCustomSelectTrigger = () => trigger;
541
+ root.closeCustomSelect = () => closeMenu({ restoreFocus: false });
542
+
543
+ // Forward data-* attributes helpers: allow callers to stamp business hooks
544
+ // on the trigger (e.g. data-dags-page-size) after creation.
545
+ root.getCustomSelectRoot = () => root;
546
+
547
+ if (disabled) root.disabled = true;
548
+ rebuildOptions();
549
+ syncSelectedClasses();
550
+ setActiveIndex(indexOfValue(value), { scroll: false });
551
+
552
+ // Compatibility surface for integration stubs that previously walked native
553
+ // <option> children under a <select>. Not used for real DOM interaction.
554
+ root.__customSelectOptions = () =>
555
+ options.map((opt) => ({
556
+ tagName: "OPTION",
557
+ value: String(opt.value),
558
+ textContent: String(opt.label),
559
+ }));
560
+
561
+ return root;
562
+ }
563
+
564
+ /** Close any open custom select (e.g. before view teardown). */
565
+ export function closeOpenCustomSelect() {
566
+ if (openInstance) openInstance.close({ restoreFocus: false });
567
+ }
@@ -11,47 +11,14 @@
11
11
  href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css"
12
12
  />
13
13
  <link rel="stylesheet" href="styles.css" />
14
+ <link rel="stylesheet" href="operator-chrome.css" />
14
15
  </head>
15
16
  <body data-shell="observe-console">
16
17
  <a class="skip-link" href="#main-content">跳到主内容</a>
17
- <nav class="embedded-operator-nav" aria-label="Operator 导航">
18
- <a href="/">返回操作台</a>
19
- <span aria-current="page">观测</span>
20
- </nav>
18
+ <!-- 统一 Operator Chrome(操作/观测同壳):由 operator-chrome.js 挂载 -->
19
+ <header id="operator-chrome"></header>
21
20
  <div id="app">
22
- <header class="site-header">
23
- <a href="#/" class="logo" aria-label="Loop Agent 运行看板首页">
24
- <i class="ri-radar-line" aria-hidden="true"></i>
25
- <span>Loop Agent</span>
26
- </a>
27
- <nav class="top-nav" aria-label="主导航">
28
- <a href="#/" class="nav-link"
29
- ><i class="ri-dashboard-3-line" aria-hidden="true"></i
30
- ><span>总览</span></a
31
- >
32
- <a href="#/pool" class="nav-link"
33
- ><i class="ri-database-2-line" aria-hidden="true"></i
34
- ><span>资源池</span></a
35
- >
36
- <a href="#/failures" class="nav-link"
37
- ><i class="ri-alarm-warning-line" aria-hidden="true"></i
38
- ><span>异常 Task</span></a
39
- >
40
- <a href="#/night" class="nav-link"
41
- ><i class="ri-moon-clear-line" aria-hidden="true"></i
42
- ><span>夜间任务</span></a
43
- >
44
- </nav>
45
- <div class="header-meta">
46
- <i class="ri-live-line" aria-hidden="true"></i>
47
- <span
48
- id="header-refresh"
49
- class="header-refresh"
50
- aria-live="polite"
51
- ></span>
52
- </div>
53
- <nav id="breadcrumb" class="breadcrumb" aria-label="面包屑"></nav>
54
- </header>
21
+ <nav id="breadcrumb" class="breadcrumb" aria-label="面包屑"></nav>
55
22
 
56
23
  <main id="main-content" class="dashboard-main" tabindex="-1">
57
24
  <section
@@ -63,12 +30,13 @@
63
30
  <div class="view-heading page-header-heading">
64
31
  <h2>运行总览</h2>
65
32
  </div>
66
- <div
67
- id="dashboard-repo"
68
- class="repo-banner repo-banner-inline"
69
- aria-label="当前仓库"
70
- ></div>
71
33
  </header>
34
+ <section
35
+ id="dashboard-night"
36
+ class="panel night-strip"
37
+ aria-label="昨夜班次"
38
+ hidden
39
+ ></section>
72
40
  <section
73
41
  id="dashboard-kpi"
74
42
  class="panel panel-kpi"
@@ -185,23 +153,64 @@
185
153
  ></aside>
186
154
  </section>
187
155
 
156
+ <section data-view="dags" id="view-dags" class="view view-dags" hidden>
157
+ <header class="page-header dags-page-header">
158
+ <div class="view-heading page-header-heading">
159
+ <p class="view-kicker">DAG History</p>
160
+ <h2 id="dags-page-title" tabindex="-1">DAG 运行历史</h2>
161
+ <p>全部正式 DAG 执行记录(不含 dry-run / init-only)。</p>
162
+ </div>
163
+ </header>
164
+ <div
165
+ id="dags-degraded"
166
+ class="panel dags-degraded-banner"
167
+ hidden
168
+ ></div>
169
+ <section
170
+ id="dags-status"
171
+ class="panel"
172
+ aria-label="历史列表状态"
173
+ ></section>
174
+ <section
175
+ id="dags-table"
176
+ class="panel panel-table dags-table-panel"
177
+ aria-label="DAG 运行历史表格"
178
+ ></section>
179
+ <section
180
+ id="dags-meta"
181
+ class="panel dags-meta-panel"
182
+ aria-label="分页与刷新"
183
+ ></section>
184
+ <div
185
+ id="dags-copy-announce"
186
+ class="sr-only"
187
+ aria-live="polite"
188
+ aria-atomic="true"
189
+ ></div>
190
+ <div
191
+ id="dags-copy-toast"
192
+ class="dags-copy-toast"
193
+ role="status"
194
+ aria-live="polite"
195
+ hidden
196
+ ></div>
197
+ </section>
198
+
188
199
  <section data-view="night" id="view-night" class="view" hidden>
189
200
  <header class="page-header">
190
201
  <div class="view-heading page-header-heading">
191
202
  <p class="view-kicker">Night Scheduler</p>
192
203
  <h2>夜间任务</h2>
193
204
  </div>
194
- <div class="cta-row">
205
+ <div class="page-header-actions" aria-label="夜间页操作">
195
206
  <button
196
207
  type="button"
197
208
  id="night-refresh-btn"
198
- class="btn btn-secondary"
209
+ class="page-action-btn"
199
210
  >
200
211
  刷新
201
212
  </button>
202
- <a class="btn btn-secondary" href="/?workspace=night"
203
- >打开 Operate 面板</a
204
- >
213
+ <a class="page-action-btn" href="/#/night">在操作台管理 →</a>
205
214
  </div>
206
215
  </header>
207
216
  <section