@ryanfw/prompt-orchestration-pipeline 0.6.0 → 0.7.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.
Files changed (61) hide show
  1. package/README.md +1 -2
  2. package/package.json +1 -2
  3. package/src/api/validators/json.js +39 -0
  4. package/src/components/DAGGrid.jsx +392 -303
  5. package/src/components/JobCard.jsx +13 -11
  6. package/src/components/JobDetail.jsx +41 -71
  7. package/src/components/JobTable.jsx +32 -22
  8. package/src/components/Layout.jsx +0 -21
  9. package/src/components/LiveText.jsx +47 -0
  10. package/src/components/TaskDetailSidebar.jsx +216 -0
  11. package/src/components/TimerText.jsx +82 -0
  12. package/src/components/ui/RestartJobModal.jsx +140 -0
  13. package/src/components/ui/toast.jsx +138 -0
  14. package/src/config/models.js +322 -0
  15. package/src/config/statuses.js +119 -0
  16. package/src/core/config.js +2 -164
  17. package/src/core/file-io.js +1 -1
  18. package/src/core/module-loader.js +54 -40
  19. package/src/core/pipeline-runner.js +52 -20
  20. package/src/core/status-writer.js +147 -3
  21. package/src/core/symlink-bridge.js +57 -0
  22. package/src/core/symlink-utils.js +94 -0
  23. package/src/core/task-runner.js +267 -443
  24. package/src/llm/index.js +167 -52
  25. package/src/pages/Code.jsx +57 -3
  26. package/src/pages/PipelineDetail.jsx +92 -22
  27. package/src/pages/PromptPipelineDashboard.jsx +15 -36
  28. package/src/providers/anthropic.js +83 -69
  29. package/src/providers/base.js +52 -0
  30. package/src/providers/deepseek.js +17 -34
  31. package/src/providers/gemini.js +226 -0
  32. package/src/providers/openai.js +36 -106
  33. package/src/providers/zhipu.js +136 -0
  34. package/src/ui/client/adapters/job-adapter.js +16 -26
  35. package/src/ui/client/api.js +134 -0
  36. package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -178
  37. package/src/ui/client/index.css +9 -0
  38. package/src/ui/client/index.html +1 -0
  39. package/src/ui/client/main.jsx +18 -15
  40. package/src/ui/client/time-store.js +161 -0
  41. package/src/ui/config-bridge.js +15 -24
  42. package/src/ui/config-bridge.node.js +15 -24
  43. package/src/ui/dist/assets/{index-WgJUlSmE.js → index-DqkbzXZ1.js} +1408 -771
  44. package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
  45. package/src/ui/dist/index.html +3 -2
  46. package/src/ui/public/favicon.svg +12 -0
  47. package/src/ui/server.js +231 -33
  48. package/src/ui/transformers/status-transformer.js +18 -31
  49. package/src/utils/dag.js +8 -4
  50. package/src/utils/duration.js +13 -19
  51. package/src/utils/formatters.js +27 -0
  52. package/src/utils/geometry-equality.js +83 -0
  53. package/src/utils/pipelines.js +5 -1
  54. package/src/utils/time-utils.js +40 -0
  55. package/src/utils/token-cost-calculator.js +4 -7
  56. package/src/utils/ui.jsx +14 -16
  57. package/src/components/ui/select.jsx +0 -27
  58. package/src/lib/utils.js +0 -6
  59. package/src/ui/client/hooks/useTicker.js +0 -26
  60. package/src/ui/config-bridge.browser.js +0 -149
  61. package/src/ui/dist/assets/style-x0V-5m8e.css +0 -62
@@ -520,11 +520,11 @@ function requireReact_production() {
520
520
  react_production.useState = function(initialState) {
521
521
  return ReactSharedInternals.H.useState(initialState);
522
522
  };
523
- react_production.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) {
523
+ react_production.useSyncExternalStore = function(subscribe2, getSnapshot2, getServerSnapshot2) {
524
524
  return ReactSharedInternals.H.useSyncExternalStore(
525
- subscribe,
526
- getSnapshot,
527
- getServerSnapshot
525
+ subscribe2,
526
+ getSnapshot2,
527
+ getServerSnapshot2
528
528
  );
529
529
  };
530
530
  react_production.useTransition = function() {
@@ -3180,15 +3180,15 @@ function requireReactDomClient_production() {
3180
3180
  return value;
3181
3181
  }
3182
3182
  var AbortControllerLocal = "undefined" !== typeof AbortController ? AbortController : function() {
3183
- var listeners = [], signal = this.signal = {
3183
+ var listeners2 = [], signal = this.signal = {
3184
3184
  aborted: false,
3185
3185
  addEventListener: function(type, listener) {
3186
- listeners.push(listener);
3186
+ listeners2.push(listener);
3187
3187
  }
3188
3188
  };
3189
3189
  this.abort = function() {
3190
3190
  signal.aborted = true;
3191
- listeners.forEach(function(listener) {
3191
+ listeners2.forEach(function(listener) {
3192
3192
  return listener();
3193
3193
  });
3194
3194
  };
@@ -3234,33 +3234,33 @@ function requireReactDomClient_production() {
3234
3234
  function pingEngtangledActionScope() {
3235
3235
  if (0 === --currentEntangledPendingCount && null !== currentEntangledListeners) {
3236
3236
  null !== currentEntangledActionThenable && (currentEntangledActionThenable.status = "fulfilled");
3237
- var listeners = currentEntangledListeners;
3237
+ var listeners2 = currentEntangledListeners;
3238
3238
  currentEntangledListeners = null;
3239
3239
  currentEntangledLane = 0;
3240
3240
  currentEntangledActionThenable = null;
3241
- for (var i2 = 0; i2 < listeners.length; i2++) (0, listeners[i2])();
3241
+ for (var i2 = 0; i2 < listeners2.length; i2++) (0, listeners2[i2])();
3242
3242
  }
3243
3243
  }
3244
3244
  function chainThenableValue(thenable, result) {
3245
- var listeners = [], thenableWithOverride = {
3245
+ var listeners2 = [], thenableWithOverride = {
3246
3246
  status: "pending",
3247
3247
  value: null,
3248
3248
  reason: null,
3249
3249
  then: function(resolve) {
3250
- listeners.push(resolve);
3250
+ listeners2.push(resolve);
3251
3251
  }
3252
3252
  };
3253
3253
  thenable.then(
3254
3254
  function() {
3255
3255
  thenableWithOverride.status = "fulfilled";
3256
3256
  thenableWithOverride.value = result;
3257
- for (var i2 = 0; i2 < listeners.length; i2++) (0, listeners[i2])(result);
3257
+ for (var i2 = 0; i2 < listeners2.length; i2++) (0, listeners2[i2])(result);
3258
3258
  },
3259
3259
  function(error) {
3260
3260
  thenableWithOverride.status = "rejected";
3261
3261
  thenableWithOverride.reason = error;
3262
- for (error = 0; error < listeners.length; error++)
3263
- (0, listeners[error])(void 0);
3262
+ for (error = 0; error < listeners2.length; error++)
3263
+ (0, listeners2[error])(void 0);
3264
3264
  }
3265
3265
  );
3266
3266
  return thenableWithOverride;
@@ -4327,22 +4327,22 @@ function requireReactDomClient_production() {
4327
4327
  }
4328
4328
  return [newState, dispatch];
4329
4329
  }
4330
- function updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
4330
+ function updateSyncExternalStore(subscribe2, getSnapshot2, getServerSnapshot2) {
4331
4331
  var fiber = currentlyRenderingFiber, hook = updateWorkInProgressHook(), isHydrating$jscomp$0 = isHydrating;
4332
4332
  if (isHydrating$jscomp$0) {
4333
- if (void 0 === getServerSnapshot) throw Error(formatProdErrorMessage(407));
4334
- getServerSnapshot = getServerSnapshot();
4335
- } else getServerSnapshot = getSnapshot();
4333
+ if (void 0 === getServerSnapshot2) throw Error(formatProdErrorMessage(407));
4334
+ getServerSnapshot2 = getServerSnapshot2();
4335
+ } else getServerSnapshot2 = getSnapshot2();
4336
4336
  var snapshotChanged = !objectIs(
4337
4337
  (currentHook || hook).memoizedState,
4338
- getServerSnapshot
4338
+ getServerSnapshot2
4339
4339
  );
4340
- snapshotChanged && (hook.memoizedState = getServerSnapshot, didReceiveUpdate = true);
4340
+ snapshotChanged && (hook.memoizedState = getServerSnapshot2, didReceiveUpdate = true);
4341
4341
  hook = hook.queue;
4342
- updateEffect(subscribeToStore.bind(null, fiber, hook, subscribe), [
4343
- subscribe
4342
+ updateEffect(subscribeToStore.bind(null, fiber, hook, subscribe2), [
4343
+ subscribe2
4344
4344
  ]);
4345
- if (hook.getSnapshot !== getSnapshot || snapshotChanged || null !== workInProgressHook && workInProgressHook.memoizedState.tag & 1) {
4345
+ if (hook.getSnapshot !== getSnapshot2 || snapshotChanged || null !== workInProgressHook && workInProgressHook.memoizedState.tag & 1) {
4346
4346
  fiber.flags |= 2048;
4347
4347
  pushSimpleEffect(
4348
4348
  9,
@@ -4351,29 +4351,29 @@ function requireReactDomClient_production() {
4351
4351
  null,
4352
4352
  fiber,
4353
4353
  hook,
4354
- getServerSnapshot,
4355
- getSnapshot
4354
+ getServerSnapshot2,
4355
+ getSnapshot2
4356
4356
  ),
4357
4357
  null
4358
4358
  );
4359
4359
  if (null === workInProgressRoot) throw Error(formatProdErrorMessage(349));
4360
- isHydrating$jscomp$0 || 0 !== (renderLanes & 127) || pushStoreConsistencyCheck(fiber, getSnapshot, getServerSnapshot);
4360
+ isHydrating$jscomp$0 || 0 !== (renderLanes & 127) || pushStoreConsistencyCheck(fiber, getSnapshot2, getServerSnapshot2);
4361
4361
  }
4362
- return getServerSnapshot;
4362
+ return getServerSnapshot2;
4363
4363
  }
4364
- function pushStoreConsistencyCheck(fiber, getSnapshot, renderedSnapshot) {
4364
+ function pushStoreConsistencyCheck(fiber, getSnapshot2, renderedSnapshot) {
4365
4365
  fiber.flags |= 16384;
4366
- fiber = { getSnapshot, value: renderedSnapshot };
4367
- getSnapshot = currentlyRenderingFiber.updateQueue;
4368
- null === getSnapshot ? (getSnapshot = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = getSnapshot, getSnapshot.stores = [fiber]) : (renderedSnapshot = getSnapshot.stores, null === renderedSnapshot ? getSnapshot.stores = [fiber] : renderedSnapshot.push(fiber));
4366
+ fiber = { getSnapshot: getSnapshot2, value: renderedSnapshot };
4367
+ getSnapshot2 = currentlyRenderingFiber.updateQueue;
4368
+ null === getSnapshot2 ? (getSnapshot2 = createFunctionComponentUpdateQueue(), currentlyRenderingFiber.updateQueue = getSnapshot2, getSnapshot2.stores = [fiber]) : (renderedSnapshot = getSnapshot2.stores, null === renderedSnapshot ? getSnapshot2.stores = [fiber] : renderedSnapshot.push(fiber));
4369
4369
  }
4370
- function updateStoreInstance(fiber, inst, nextSnapshot, getSnapshot) {
4370
+ function updateStoreInstance(fiber, inst, nextSnapshot, getSnapshot2) {
4371
4371
  inst.value = nextSnapshot;
4372
- inst.getSnapshot = getSnapshot;
4372
+ inst.getSnapshot = getSnapshot2;
4373
4373
  checkIfSnapshotChanged(inst) && forceStoreRerender(fiber);
4374
4374
  }
4375
- function subscribeToStore(fiber, inst, subscribe) {
4376
- return subscribe(function() {
4375
+ function subscribeToStore(fiber, inst, subscribe2) {
4376
+ return subscribe2(function() {
4377
4377
  checkIfSnapshotChanged(inst) && forceStoreRerender(fiber);
4378
4378
  });
4379
4379
  }
@@ -5092,23 +5092,23 @@ function requireReactDomClient_production() {
5092
5092
  mountWorkInProgressHook().memoizedState = stateHook;
5093
5093
  return [false, stateHook];
5094
5094
  },
5095
- useSyncExternalStore: function(subscribe, getSnapshot, getServerSnapshot) {
5095
+ useSyncExternalStore: function(subscribe2, getSnapshot2, getServerSnapshot2) {
5096
5096
  var fiber = currentlyRenderingFiber, hook = mountWorkInProgressHook();
5097
5097
  if (isHydrating) {
5098
- if (void 0 === getServerSnapshot)
5098
+ if (void 0 === getServerSnapshot2)
5099
5099
  throw Error(formatProdErrorMessage(407));
5100
- getServerSnapshot = getServerSnapshot();
5100
+ getServerSnapshot2 = getServerSnapshot2();
5101
5101
  } else {
5102
- getServerSnapshot = getSnapshot();
5102
+ getServerSnapshot2 = getSnapshot2();
5103
5103
  if (null === workInProgressRoot)
5104
5104
  throw Error(formatProdErrorMessage(349));
5105
- 0 !== (workInProgressRootRenderLanes & 127) || pushStoreConsistencyCheck(fiber, getSnapshot, getServerSnapshot);
5105
+ 0 !== (workInProgressRootRenderLanes & 127) || pushStoreConsistencyCheck(fiber, getSnapshot2, getServerSnapshot2);
5106
5106
  }
5107
- hook.memoizedState = getServerSnapshot;
5108
- var inst = { value: getServerSnapshot, getSnapshot };
5107
+ hook.memoizedState = getServerSnapshot2;
5108
+ var inst = { value: getServerSnapshot2, getSnapshot: getSnapshot2 };
5109
5109
  hook.queue = inst;
5110
- mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
5111
- subscribe
5110
+ mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe2), [
5111
+ subscribe2
5112
5112
  ]);
5113
5113
  fiber.flags |= 2048;
5114
5114
  pushSimpleEffect(
@@ -5118,12 +5118,12 @@ function requireReactDomClient_production() {
5118
5118
  null,
5119
5119
  fiber,
5120
5120
  inst,
5121
- getServerSnapshot,
5122
- getSnapshot
5121
+ getServerSnapshot2,
5122
+ getSnapshot2
5123
5123
  ),
5124
5124
  null
5125
5125
  );
5126
- return getServerSnapshot;
5126
+ return getServerSnapshot2;
5127
5127
  },
5128
5128
  useId: function() {
5129
5129
  var hook = mountWorkInProgressHook(), identifierPrefix = workInProgressRoot.identifierPrefix;
@@ -8813,10 +8813,10 @@ function requireReactDomClient_production() {
8813
8813
  var tag = node.tag;
8814
8814
  if ((0 === tag || 11 === tag || 15 === tag) && node.flags & 16384 && (tag = node.updateQueue, null !== tag && (tag = tag.stores, null !== tag)))
8815
8815
  for (var i2 = 0; i2 < tag.length; i2++) {
8816
- var check = tag[i2], getSnapshot = check.getSnapshot;
8816
+ var check = tag[i2], getSnapshot2 = check.getSnapshot;
8817
8817
  check = check.value;
8818
8818
  try {
8819
- if (!objectIs(getSnapshot(), check)) return false;
8819
+ if (!objectIs(getSnapshot2(), check)) return false;
8820
8820
  } catch (error) {
8821
8821
  return false;
8822
8822
  }
@@ -10201,15 +10201,15 @@ function requireReactDomClient_production() {
10201
10201
  };
10202
10202
  }
10203
10203
  function accumulateTwoPhaseListeners(targetFiber, reactName) {
10204
- for (var captureName = reactName + "Capture", listeners = []; null !== targetFiber; ) {
10204
+ for (var captureName = reactName + "Capture", listeners2 = []; null !== targetFiber; ) {
10205
10205
  var _instance2 = targetFiber, stateNode = _instance2.stateNode;
10206
10206
  _instance2 = _instance2.tag;
10207
- 5 !== _instance2 && 26 !== _instance2 && 27 !== _instance2 || null === stateNode || (_instance2 = getListener(targetFiber, captureName), null != _instance2 && listeners.unshift(
10207
+ 5 !== _instance2 && 26 !== _instance2 && 27 !== _instance2 || null === stateNode || (_instance2 = getListener(targetFiber, captureName), null != _instance2 && listeners2.unshift(
10208
10208
  createDispatchListener(targetFiber, _instance2, stateNode)
10209
- ), _instance2 = getListener(targetFiber, reactName), null != _instance2 && listeners.push(
10209
+ ), _instance2 = getListener(targetFiber, reactName), null != _instance2 && listeners2.push(
10210
10210
  createDispatchListener(targetFiber, _instance2, stateNode)
10211
10211
  ));
10212
- if (3 === targetFiber.tag) return listeners;
10212
+ if (3 === targetFiber.tag) return listeners2;
10213
10213
  targetFiber = targetFiber.return;
10214
10214
  }
10215
10215
  return [];
@@ -10222,18 +10222,18 @@ function requireReactDomClient_production() {
10222
10222
  return inst ? inst : null;
10223
10223
  }
10224
10224
  function accumulateEnterLeaveListenersForEvent(dispatchQueue, event, target, common, inCapturePhase) {
10225
- for (var registrationName = event._reactName, listeners = []; null !== target && target !== common; ) {
10225
+ for (var registrationName = event._reactName, listeners2 = []; null !== target && target !== common; ) {
10226
10226
  var _instance3 = target, alternate = _instance3.alternate, stateNode = _instance3.stateNode;
10227
10227
  _instance3 = _instance3.tag;
10228
10228
  if (null !== alternate && alternate === common) break;
10229
- 5 !== _instance3 && 26 !== _instance3 && 27 !== _instance3 || null === stateNode || (alternate = stateNode, inCapturePhase ? (stateNode = getListener(target, registrationName), null != stateNode && listeners.unshift(
10229
+ 5 !== _instance3 && 26 !== _instance3 && 27 !== _instance3 || null === stateNode || (alternate = stateNode, inCapturePhase ? (stateNode = getListener(target, registrationName), null != stateNode && listeners2.unshift(
10230
10230
  createDispatchListener(target, stateNode, alternate)
10231
- )) : inCapturePhase || (stateNode = getListener(target, registrationName), null != stateNode && listeners.push(
10231
+ )) : inCapturePhase || (stateNode = getListener(target, registrationName), null != stateNode && listeners2.push(
10232
10232
  createDispatchListener(target, stateNode, alternate)
10233
10233
  )));
10234
10234
  target = target.return;
10235
10235
  }
10236
- 0 !== listeners.length && dispatchQueue.push({ event, listeners });
10236
+ 0 !== listeners2.length && dispatchQueue.push({ event, listeners: listeners2 });
10237
10237
  }
10238
10238
  var NORMALIZE_NEWLINES_REGEX = /\r\n?/g, NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
10239
10239
  function normalizeMarkupForTextOrAttribute(markup) {
@@ -12550,7 +12550,7 @@ function requireClient() {
12550
12550
  return client.exports;
12551
12551
  }
12552
12552
  var clientExports = requireClient();
12553
- const ReactDOM = /* @__PURE__ */ getDefaultExportFromCjs(clientExports);
12553
+ const ReactDOM$1 = /* @__PURE__ */ getDefaultExportFromCjs(clientExports);
12554
12554
  /**
12555
12555
  * react-router v7.9.4
12556
12556
  *
@@ -14637,6 +14637,7 @@ function useViewTransitionState(to, { relative } = {}) {
14637
14637
  return matchPath(path.pathname, nextPath) != null || matchPath(path.pathname, currentPath) != null;
14638
14638
  }
14639
14639
  var reactDomExports = requireReactDom();
14640
+ const ReactDOM = /* @__PURE__ */ getDefaultExportFromCjs(reactDomExports);
14640
14641
  function setRef(ref, value) {
14641
14642
  if (typeof ref === "function") {
14642
14643
  return ref(value);
@@ -15332,11 +15333,11 @@ function usePointerDownOutside(onPointerDownOutside, ownerDocument = globalThis?
15332
15333
  }
15333
15334
  isPointerInsideReactTreeRef.current = false;
15334
15335
  };
15335
- const timerId = window.setTimeout(() => {
15336
+ const timerId2 = window.setTimeout(() => {
15336
15337
  ownerDocument.addEventListener("pointerdown", handlePointerDown);
15337
15338
  }, 0);
15338
15339
  return () => {
15339
- window.clearTimeout(timerId);
15340
+ window.clearTimeout(timerId2);
15340
15341
  ownerDocument.removeEventListener("pointerdown", handlePointerDown);
15341
15342
  ownerDocument.removeEventListener("click", handleClickRef.current);
15342
15343
  };
@@ -15380,6 +15381,15 @@ function handleAndDispatchCustomEvent(name, handler, detail, { discrete }) {
15380
15381
  target.dispatchEvent(event);
15381
15382
  }
15382
15383
  }
15384
+ var PORTAL_NAME$1 = "Portal";
15385
+ var Portal$1 = reactExports.forwardRef((props, forwardedRef) => {
15386
+ const { container: containerProp, ...portalProps } = props;
15387
+ const [mounted, setMounted] = reactExports.useState(false);
15388
+ useLayoutEffect2(() => setMounted(true), []);
15389
+ const container = containerProp || mounted && globalThis?.document?.body;
15390
+ return container ? ReactDOM.createPortal(/* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { ...portalProps, ref: forwardedRef }), container) : null;
15391
+ });
15392
+ Portal$1.displayName = PORTAL_NAME$1;
15383
15393
  function useSize(element) {
15384
15394
  const [size2, setSize] = reactExports.useState(void 0);
15385
15395
  useLayoutEffect2(() => {
@@ -16007,7 +16017,7 @@ async function convertValueToCoords(state, options) {
16007
16017
  y: crossAxis * crossAxisMulti
16008
16018
  };
16009
16019
  }
16010
- const offset$2 = function(options) {
16020
+ const offset$3 = function(options) {
16011
16021
  if (options === void 0) {
16012
16022
  options = 0;
16013
16023
  }
@@ -16990,7 +17000,7 @@ function autoUpdate(reference, floating, update, options) {
16990
17000
  }
16991
17001
  };
16992
17002
  }
16993
- const offset$1 = offset$2;
17003
+ const offset$2 = offset$3;
16994
17004
  const shift$1 = shift$2;
16995
17005
  const flip$1 = flip$2;
16996
17006
  const size$1 = size$2;
@@ -17263,8 +17273,8 @@ const arrow$1 = (options) => {
17263
17273
  }
17264
17274
  };
17265
17275
  };
17266
- const offset = (options, deps) => ({
17267
- ...offset$1(options),
17276
+ const offset$1 = (options, deps) => ({
17277
+ ...offset$2(options),
17268
17278
  options: [options, deps]
17269
17279
  });
17270
17280
  const shift = (options, deps) => ({
@@ -17388,7 +17398,7 @@ var PopperContent = reactExports.forwardRef(
17388
17398
  reference: context.anchor
17389
17399
  },
17390
17400
  middleware: [
17391
- offset({ mainAxis: sideOffset + arrowHeight, alignmentAxis: alignOffset }),
17401
+ offset$1({ mainAxis: sideOffset + arrowHeight, alignmentAxis: alignOffset }),
17392
17402
  avoidCollisions && shift({
17393
17403
  mainAxis: true,
17394
17404
  crossAxis: false,
@@ -18867,6 +18877,12 @@ var PORTAL_NAME = "TooltipPortal";
18867
18877
  var [PortalProvider, usePortalContext] = createTooltipContext(PORTAL_NAME, {
18868
18878
  forceMount: void 0
18869
18879
  });
18880
+ var TooltipPortal = (props) => {
18881
+ const { __scopeTooltip, forceMount, children, container } = props;
18882
+ const context = useTooltipContext(PORTAL_NAME, __scopeTooltip);
18883
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(PortalProvider, { scope: __scopeTooltip, forceMount, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Presence, { present: forceMount || context.open, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Portal$1, { asChild: true, container, children }) }) });
18884
+ };
18885
+ TooltipPortal.displayName = PORTAL_NAME;
18870
18886
  var CONTENT_NAME = "TooltipContent";
18871
18887
  var TooltipContent = reactExports.forwardRef(
18872
18888
  (props, forwardedRef) => {
@@ -19135,7 +19151,9 @@ function getHullPresorted(points) {
19135
19151
  var Provider = TooltipProvider;
19136
19152
  var Root3 = Tooltip;
19137
19153
  var Trigger = TooltipTrigger;
19154
+ var Portal = TooltipPortal;
19138
19155
  var Content2 = TooltipContent;
19156
+ var Arrow2 = TooltipArrow;
19139
19157
  var classnames = { exports: {} };
19140
19158
  /*!
19141
19159
  Copyright (c) 2018 Jed Watson.
@@ -19610,8 +19628,9 @@ function derivePipelineMetadata(source = {}) {
19610
19628
  const pipelineLabel = source?.pipelineLabel ?? (typeof pipelineSlugFromSource === "string" ? humanizePipelineSlug(pipelineSlugFromSource) : null);
19611
19629
  const pipelineObject = pipelineValue && typeof pipelineValue === "object" && !Array.isArray(pipelineValue) ? pipelineValue : null;
19612
19630
  const pipeline = pipelineObject ?? (typeof pipelineSlugFromSource === "string" ? pipelineSlugFromSource : null);
19631
+ const stringPipeline = typeof pipelineValue === "string" ? pipelineValue : null;
19613
19632
  return {
19614
- pipeline,
19633
+ pipeline: pipeline || stringPipeline,
19615
19634
  pipelineSlug: typeof pipelineSlugFromSource === "string" ? pipelineSlugFromSource : null,
19616
19635
  pipelineLabel: pipelineLabel || null
19617
19636
  };
@@ -19909,13 +19928,70 @@ function useJobListWithUpdates() {
19909
19928
  connectionStatus
19910
19929
  };
19911
19930
  }
19912
- const ALLOWED_STATES = /* @__PURE__ */ new Set(["pending", "running", "done", "failed"]);
19913
- function normalizeTaskState(raw) {
19931
+ const TaskState = Object.freeze({
19932
+ PENDING: "pending",
19933
+ RUNNING: "running",
19934
+ DONE: "done",
19935
+ FAILED: "failed"
19936
+ });
19937
+ const JobStatus = Object.freeze({
19938
+ PENDING: "pending",
19939
+ RUNNING: "running",
19940
+ FAILED: "failed",
19941
+ COMPLETE: "complete"
19942
+ });
19943
+ const JobLocation = Object.freeze({
19944
+ PENDING: "pending",
19945
+ CURRENT: "current",
19946
+ COMPLETE: "complete",
19947
+ REJECTED: "rejected"
19948
+ });
19949
+ new Set(Object.values(TaskState));
19950
+ new Set(Object.values(JobStatus));
19951
+ new Set(Object.values(JobLocation));
19952
+ function normalizeTaskState(state) {
19953
+ if (typeof state !== "string") {
19954
+ return TaskState.PENDING;
19955
+ }
19956
+ const normalized = state.toLowerCase().trim();
19957
+ switch (normalized) {
19958
+ case "error":
19959
+ return TaskState.FAILED;
19960
+ case "succeeded":
19961
+ return TaskState.DONE;
19962
+ case TaskState.PENDING:
19963
+ case TaskState.RUNNING:
19964
+ case TaskState.DONE:
19965
+ case TaskState.FAILED:
19966
+ return normalized;
19967
+ default:
19968
+ return TaskState.PENDING;
19969
+ }
19970
+ }
19971
+ function deriveJobStatusFromTasks(tasks) {
19972
+ if (!Array.isArray(tasks) || tasks.length === 0) {
19973
+ return JobStatus.PENDING;
19974
+ }
19975
+ const normalizedStates = tasks.map((task) => normalizeTaskState(task.state));
19976
+ if (normalizedStates.some((state) => state === TaskState.FAILED)) {
19977
+ return JobStatus.FAILED;
19978
+ }
19979
+ if (normalizedStates.some((state) => state === TaskState.RUNNING)) {
19980
+ return JobStatus.RUNNING;
19981
+ }
19982
+ if (normalizedStates.every((state) => state === TaskState.DONE)) {
19983
+ return JobStatus.COMPLETE;
19984
+ }
19985
+ return JobStatus.PENDING;
19986
+ }
19987
+ function normalizeTaskStateWithWarning(raw) {
19914
19988
  if (!raw || typeof raw !== "string")
19915
19989
  return { state: "pending", warning: "missing_state" };
19916
- const s2 = raw.toLowerCase();
19917
- if (ALLOWED_STATES.has(s2)) return { state: s2 };
19918
- return { state: "pending", warning: `unknown_state:${raw}` };
19990
+ const normalizedState = normalizeTaskState(raw);
19991
+ if (raw !== normalizedState) {
19992
+ return { state: normalizedState, warning: `unknown_state:${raw}` };
19993
+ }
19994
+ return { state: normalizedState };
19919
19995
  }
19920
19996
  function normalizeTasks(rawTasks) {
19921
19997
  if (!rawTasks) return { tasks: {}, warnings: [] };
@@ -19923,7 +19999,7 @@ function normalizeTasks(rawTasks) {
19923
19999
  if (typeof rawTasks === "object" && !Array.isArray(rawTasks)) {
19924
20000
  const tasks = {};
19925
20001
  Object.entries(rawTasks).forEach(([name, t2]) => {
19926
- const ns = normalizeTaskState(t2 && t2.state);
20002
+ const ns = normalizeTaskStateWithWarning(t2 && t2.state);
19927
20003
  if (ns.warning) warnings.push(`${name}:${ns.warning}`);
19928
20004
  const taskObj = {
19929
20005
  name,
@@ -19957,7 +20033,7 @@ function normalizeTasks(rawTasks) {
19957
20033
  const tasks = {};
19958
20034
  rawTasks.forEach((t2, idx) => {
19959
20035
  const name = t2 && t2.name ? String(t2.name) : `task-${idx}`;
19960
- const ns = normalizeTaskState(t2 && t2.state);
20036
+ const ns = normalizeTaskStateWithWarning(t2 && t2.state);
19961
20037
  if (ns.warning) warnings.push(`${name}:${ns.warning}`);
19962
20038
  tasks[name] = {
19963
20039
  name,
@@ -19978,14 +20054,6 @@ function normalizeTasks(rawTasks) {
19978
20054
  }
19979
20055
  return { tasks: {}, warnings: ["invalid_tasks_shape"] };
19980
20056
  }
19981
- function deriveStatusFromTasks(tasks) {
19982
- const taskList = Object.values(tasks);
19983
- if (!Array.isArray(taskList) || taskList.length === 0) return "pending";
19984
- if (taskList.some((t2) => t2.state === "failed")) return "failed";
19985
- if (taskList.some((t2) => t2.state === "running")) return "running";
19986
- if (taskList.every((t2) => t2.state === "done")) return "complete";
19987
- return "pending";
19988
- }
19989
20057
  function computeJobSummaryStats(tasks) {
19990
20058
  const taskList = Object.values(tasks);
19991
20059
  const taskCount = taskList.length;
@@ -19993,7 +20061,7 @@ function computeJobSummaryStats(tasks) {
19993
20061
  (acc, t2) => acc + (t2.state === "done" ? 1 : 0),
19994
20062
  0
19995
20063
  );
19996
- const status = deriveStatusFromTasks(tasks);
20064
+ const status = deriveJobStatusFromTasks(Object.values(tasks));
19997
20065
  const progress = taskCount > 0 ? Math.round(doneCount / taskCount * 100) : 0;
19998
20066
  return { status, progress, doneCount, taskCount };
19999
20067
  }
@@ -20076,101 +20144,6 @@ function adaptJobDetail(apiDetail) {
20076
20144
  if (warnings.length > 0) detail.__warnings = warnings;
20077
20145
  return detail;
20078
20146
  }
20079
- function normalizeState(state) {
20080
- switch (state) {
20081
- case "done":
20082
- return "completed";
20083
- case "failed":
20084
- case "error":
20085
- return "error";
20086
- case "pending":
20087
- case "running":
20088
- case "current":
20089
- case "completed":
20090
- case "rejected":
20091
- return state;
20092
- default:
20093
- return state;
20094
- }
20095
- }
20096
- function taskDisplayDurationMs(task, now = Date.now()) {
20097
- const { state, startedAt, endedAt, executionTime, executionTimeMs } = task;
20098
- const normalizedState = normalizeState(state);
20099
- switch (normalizedState) {
20100
- case "pending":
20101
- return 0;
20102
- case "running":
20103
- case "current":
20104
- if (!startedAt) {
20105
- return 0;
20106
- }
20107
- const startTime = Date.parse(startedAt);
20108
- return Math.max(0, now - startTime);
20109
- case "completed":
20110
- const execTime = executionTimeMs != null ? executionTimeMs : executionTime;
20111
- if (typeof execTime === "number" && execTime >= 0) {
20112
- return execTime;
20113
- }
20114
- if (!startedAt) {
20115
- return 0;
20116
- }
20117
- const completedStartTime = Date.parse(startedAt);
20118
- const endTime = endedAt ? Date.parse(endedAt) : now;
20119
- return Math.max(0, endTime - completedStartTime);
20120
- case "rejected":
20121
- return 0;
20122
- default:
20123
- return 0;
20124
- }
20125
- }
20126
- function jobCumulativeDurationMs(job, now = Date.now()) {
20127
- const { tasks } = job;
20128
- if (!tasks) {
20129
- return 0;
20130
- }
20131
- let taskList;
20132
- if (Array.isArray(tasks)) {
20133
- taskList = tasks;
20134
- } else if (typeof tasks === "object") {
20135
- taskList = Object.values(tasks);
20136
- } else {
20137
- return 0;
20138
- }
20139
- return taskList.reduce((total, task) => {
20140
- return total + taskDisplayDurationMs(task, now);
20141
- }, 0);
20142
- }
20143
- function fmtDuration(ms) {
20144
- if (ms <= 0) return "0s";
20145
- const seconds = Math.floor(ms / 1e3);
20146
- const minutes = Math.floor(seconds / 60);
20147
- const hours = Math.floor(minutes / 60);
20148
- if (hours > 0) {
20149
- const remainingMinutes = minutes % 60;
20150
- const remainingSeconds = seconds % 60;
20151
- if (remainingSeconds > 0) {
20152
- return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
20153
- } else {
20154
- return `${hours}h ${remainingMinutes}m`;
20155
- }
20156
- } else if (minutes > 0) {
20157
- return `${minutes}m ${seconds % 60}s`;
20158
- } else {
20159
- return `${seconds}s`;
20160
- }
20161
- }
20162
- function useTicker(intervalMs = 1e3) {
20163
- const [now, setNow] = reactExports.useState(() => Date.now());
20164
- reactExports.useEffect(() => {
20165
- const intervalId = setInterval(() => {
20166
- setNow(Date.now());
20167
- }, intervalMs);
20168
- return () => {
20169
- clearInterval(intervalId);
20170
- };
20171
- }, [intervalMs]);
20172
- return now;
20173
- }
20174
20147
  /**
20175
20148
  * @license lucide-react v0.544.0 - ISC
20176
20149
  *
@@ -20270,17 +20243,6 @@ const createLucideIcon = (iconName, iconNode) => {
20270
20243
  Component.displayName = toPascalCase(iconName);
20271
20244
  return Component;
20272
20245
  };
20273
- /**
20274
- * @license lucide-react v0.544.0 - ISC
20275
- *
20276
- * This source code is licensed under the ISC license.
20277
- * See the LICENSE file in the root directory of this source tree.
20278
- */
20279
- const __iconNode$4 = [
20280
- ["path", { d: "m12 19-7-7 7-7", key: "1l729n" }],
20281
- ["path", { d: "M19 12H5", key: "x3x0zl" }]
20282
- ];
20283
- const ArrowLeft = createLucideIcon("arrow-left", __iconNode$4);
20284
20246
  /**
20285
20247
  * @license lucide-react v0.544.0 - ISC
20286
20248
  *
@@ -20326,6 +20288,78 @@ const __iconNode = [
20326
20288
  ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }]
20327
20289
  ];
20328
20290
  const Upload = createLucideIcon("upload", __iconNode);
20291
+ function normalizeState(state) {
20292
+ const canonicalState = normalizeTaskState(state);
20293
+ if (canonicalState === TaskState.DONE) {
20294
+ return "completed";
20295
+ }
20296
+ return canonicalState;
20297
+ }
20298
+ function taskDisplayDurationMs(task, now = Date.now()) {
20299
+ const { state, startedAt, endedAt, executionTime, executionTimeMs } = task;
20300
+ const normalizedState = normalizeState(state);
20301
+ switch (normalizedState) {
20302
+ case TaskState.PENDING:
20303
+ return 0;
20304
+ case TaskState.RUNNING:
20305
+ if (!startedAt) {
20306
+ return 0;
20307
+ }
20308
+ const startTime = Date.parse(startedAt);
20309
+ return Math.max(0, now - startTime);
20310
+ case "completed":
20311
+ const execTime = executionTimeMs != null ? executionTimeMs : executionTime;
20312
+ if (typeof execTime === "number" && execTime >= 0) {
20313
+ return execTime;
20314
+ }
20315
+ if (!startedAt) {
20316
+ return 0;
20317
+ }
20318
+ const completedStartTime = Date.parse(startedAt);
20319
+ const endTime = endedAt ? Date.parse(endedAt) : now;
20320
+ return Math.max(0, endTime - completedStartTime);
20321
+ case TaskState.FAILED:
20322
+ return 0;
20323
+ default:
20324
+ return 0;
20325
+ }
20326
+ }
20327
+ function jobCumulativeDurationMs(job, now = Date.now()) {
20328
+ const { tasks } = job;
20329
+ if (!tasks) {
20330
+ return 0;
20331
+ }
20332
+ let taskList;
20333
+ if (Array.isArray(tasks)) {
20334
+ taskList = tasks;
20335
+ } else if (typeof tasks === "object") {
20336
+ taskList = Object.values(tasks);
20337
+ } else {
20338
+ return 0;
20339
+ }
20340
+ return taskList.reduce((total, task) => {
20341
+ return total + taskDisplayDurationMs(task, now);
20342
+ }, 0);
20343
+ }
20344
+ function fmtDuration(ms) {
20345
+ if (ms <= 0) return "0s";
20346
+ const seconds = Math.floor(ms / 1e3);
20347
+ const minutes = Math.floor(seconds / 60);
20348
+ const hours = Math.floor(minutes / 60);
20349
+ if (hours > 0) {
20350
+ const remainingMinutes = minutes % 60;
20351
+ const remainingSeconds = seconds % 60;
20352
+ if (remainingSeconds > 0) {
20353
+ return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
20354
+ } else {
20355
+ return `${hours}h ${remainingMinutes}m`;
20356
+ }
20357
+ } else if (minutes > 0) {
20358
+ return `${minutes}m ${seconds % 60}s`;
20359
+ } else {
20360
+ return `${seconds}s`;
20361
+ }
20362
+ }
20329
20363
  const countCompleted = (job) => {
20330
20364
  const list = Array.isArray(job?.tasks) ? job.tasks : Object.values(job?.tasks || {});
20331
20365
  return list.filter((t2) => t2?.state === "done" || t2?.state === "completed").length;
@@ -20347,14 +20381,13 @@ function Badge({ children, intent = "gray", className = "", ...props }) {
20347
20381
  }
20348
20382
  const statusBadge = (status) => {
20349
20383
  switch (status) {
20350
- case "running":
20384
+ case TaskState.RUNNING:
20351
20385
  return /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { intent: "blue", "aria-label": "Running", children: "Running" });
20352
- case "failed":
20386
+ case TaskState.FAILED:
20353
20387
  return /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { intent: "red", "aria-label": "Failed", children: "Failed" });
20354
- case "completed":
20355
- case "complete":
20388
+ case TaskState.DONE:
20356
20389
  return /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { intent: "green", "aria-label": "Completed", children: "Completed" });
20357
- case "pending":
20390
+ case TaskState.PENDING:
20358
20391
  return /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { intent: "gray", "aria-label": "Pending", children: "Pending" });
20359
20392
  default:
20360
20393
  return null;
@@ -20362,16 +20395,176 @@ const statusBadge = (status) => {
20362
20395
  };
20363
20396
  const progressClasses = (status) => {
20364
20397
  switch (status) {
20365
- case "running":
20398
+ case TaskState.RUNNING:
20366
20399
  return "bg-info/20 [&>div]:bg-info";
20367
- case "failed":
20400
+ case TaskState.FAILED:
20368
20401
  return "bg-destructive/20 [&>div]:bg-destructive";
20369
- case "completed":
20402
+ case TaskState.DONE:
20370
20403
  return "bg-success/20 [&>div]:bg-success";
20371
20404
  default:
20372
20405
  return "bg-muted [&>div]:bg-muted-foreground";
20373
20406
  }
20374
20407
  };
20408
+ const offset = Date.now() - performance.now();
20409
+ let currentNow = Math.floor(performance.now() + offset);
20410
+ const listeners = /* @__PURE__ */ new Set();
20411
+ const cadenceHints = /* @__PURE__ */ new Map();
20412
+ let timerId = null;
20413
+ let activeIntervalMs = 1e3;
20414
+ let isBackground = false;
20415
+ function subscribe(listener) {
20416
+ listeners.add(listener);
20417
+ if (listeners.size === 1) {
20418
+ startTimer();
20419
+ }
20420
+ return () => {
20421
+ listeners.delete(listener);
20422
+ if (listeners.size === 0) {
20423
+ stopTimer();
20424
+ }
20425
+ };
20426
+ }
20427
+ function getSnapshot() {
20428
+ return currentNow;
20429
+ }
20430
+ function getServerSnapshot() {
20431
+ return Date.now();
20432
+ }
20433
+ function addCadenceHint(id, ms) {
20434
+ cadenceHints.set(id, ms);
20435
+ recalculateInterval();
20436
+ }
20437
+ function removeCadenceHint(id) {
20438
+ cadenceHints.delete(id);
20439
+ recalculateInterval();
20440
+ }
20441
+ function recalculateInterval() {
20442
+ const minCadence = Math.min(...cadenceHints.values(), 1e3);
20443
+ const newIntervalMs = isBackground ? Math.max(minCadence, 6e4) : minCadence;
20444
+ if (newIntervalMs !== activeIntervalMs) {
20445
+ activeIntervalMs = newIntervalMs;
20446
+ if (listeners.size > 0) {
20447
+ stopTimer();
20448
+ startTimer();
20449
+ }
20450
+ }
20451
+ }
20452
+ function startTimer() {
20453
+ if (timerId !== null) return;
20454
+ const tick = () => {
20455
+ currentNow = Math.floor(performance.now() + offset);
20456
+ listeners.forEach((listener) => {
20457
+ try {
20458
+ listener();
20459
+ } catch (error) {
20460
+ console.error("Error in time store listener:", error);
20461
+ }
20462
+ });
20463
+ };
20464
+ if (activeIntervalMs >= 6e4) {
20465
+ const now = Date.now();
20466
+ const nextMinuteBoundary = Math.ceil(now / 6e4) * 6e4;
20467
+ const initialDelay = nextMinuteBoundary - now;
20468
+ timerId = setTimeout(() => {
20469
+ tick();
20470
+ timerId = setInterval(tick, 6e4);
20471
+ }, initialDelay);
20472
+ } else {
20473
+ timerId = setInterval(tick, activeIntervalMs);
20474
+ }
20475
+ }
20476
+ function stopTimer() {
20477
+ if (timerId !== null) {
20478
+ clearTimeout(timerId);
20479
+ clearInterval(timerId);
20480
+ timerId = null;
20481
+ }
20482
+ }
20483
+ function handleVisibilityChange() {
20484
+ const wasBackground = isBackground;
20485
+ isBackground = document.visibilityState === "hidden";
20486
+ if (wasBackground !== isBackground) {
20487
+ recalculateInterval();
20488
+ }
20489
+ }
20490
+ if (typeof document !== "undefined") {
20491
+ document.addEventListener("visibilitychange", handleVisibilityChange);
20492
+ }
20493
+ function TimerText({
20494
+ startMs,
20495
+ endMs = null,
20496
+ granularity = "second",
20497
+ format = fmtDuration,
20498
+ className
20499
+ }) {
20500
+ const id = reactExports.useId();
20501
+ const now = reactExports.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
20502
+ const [displayText, setDisplayText] = reactExports.useState(() => {
20503
+ if (!startMs) return "—";
20504
+ const initialEnd = endMs ?? Date.now();
20505
+ const elapsed = Math.max(0, initialEnd - startMs);
20506
+ return format(elapsed);
20507
+ });
20508
+ reactExports.useEffect(() => {
20509
+ if (!startMs) return;
20510
+ const cadenceMs = granularity === "second" ? 1e3 : 6e4;
20511
+ addCadenceHint(id, cadenceMs);
20512
+ return () => {
20513
+ removeCadenceHint(id);
20514
+ };
20515
+ }, [id, granularity, startMs]);
20516
+ reactExports.useEffect(() => {
20517
+ if (!startMs) return;
20518
+ if (endMs !== null) return;
20519
+ const elapsed = Math.max(0, now - startMs);
20520
+ setDisplayText(format(elapsed));
20521
+ }, [now, startMs, endMs, format]);
20522
+ reactExports.useEffect(() => {
20523
+ if (!startMs || endMs === null) return;
20524
+ const elapsed = Math.max(0, endMs - startMs);
20525
+ setDisplayText(format(elapsed));
20526
+ }, [startMs, endMs, format]);
20527
+ if (!startMs) {
20528
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className, children: "—" });
20529
+ }
20530
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className, children: displayText });
20531
+ }
20532
+ function LiveText({ compute, cadenceMs = 1e4, className }) {
20533
+ const id = reactExports.useId();
20534
+ const now = reactExports.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
20535
+ const [displayText, setDisplayText] = reactExports.useState(() => {
20536
+ return compute(Date.now());
20537
+ });
20538
+ reactExports.useEffect(() => {
20539
+ addCadenceHint(id, cadenceMs);
20540
+ return () => {
20541
+ removeCadenceHint(id);
20542
+ };
20543
+ }, [id, cadenceMs]);
20544
+ reactExports.useEffect(() => {
20545
+ setDisplayText(compute(now));
20546
+ }, [now, compute]);
20547
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className, children: displayText });
20548
+ }
20549
+ function toMilliseconds(timestamp) {
20550
+ if (timestamp === null || timestamp === void 0) {
20551
+ return null;
20552
+ }
20553
+ if (typeof timestamp === "number") {
20554
+ return isNaN(timestamp) ? null : timestamp;
20555
+ }
20556
+ if (typeof timestamp === "string") {
20557
+ const parsed = Date.parse(timestamp);
20558
+ return isNaN(parsed) ? null : parsed;
20559
+ }
20560
+ return null;
20561
+ }
20562
+ function taskToTimerProps(task) {
20563
+ return {
20564
+ startMs: toMilliseconds(task?.startedAt),
20565
+ endMs: toMilliseconds(task?.endedAt)
20566
+ };
20567
+ }
20375
20568
  function formatCurrency4$1(x) {
20376
20569
  if (typeof x !== "number" || x === 0) return "$0.0000";
20377
20570
  const formatted = x.toFixed(4);
@@ -20386,13 +20579,7 @@ function formatTokensCompact$1(n2) {
20386
20579
  }
20387
20580
  return `${n2} tok`;
20388
20581
  }
20389
- function JobTable({
20390
- jobs,
20391
- pipeline,
20392
- onOpenJob,
20393
- overallElapsed,
20394
- now
20395
- }) {
20582
+ function JobTable({ jobs, pipeline, onOpenJob }) {
20396
20583
  if (jobs.length === 0) {
20397
20584
  return /* @__PURE__ */ jsxRuntimeExports.jsx(p$7, { className: "p-6", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "2", className: "text-slate-600", children: "No jobs to show here yet." }) });
20398
20585
  }
@@ -20417,11 +20604,9 @@ function JobTable({
20417
20604
  })
20418
20605
  ) : job.tasks || {};
20419
20606
  const currentTask = job.current ? taskById[job.current] : void 0;
20420
- const currentElapsedMs = currentTask ? taskDisplayDurationMs(currentTask, now) : 0;
20421
20607
  const totalCompleted = countCompleted(job);
20422
20608
  const totalTasks = pipeline?.tasks?.length ?? (Array.isArray(job.tasks) ? job.tasks.length : Object.keys(job.tasks || {}).length);
20423
20609
  const progress = Number.isFinite(job.progress) ? Math.round(job.progress) : 0;
20424
- const duration = overallElapsed(job);
20425
20610
  const currentTaskName = currentTask ? currentTask.name ?? currentTask.id ?? job.current : void 0;
20426
20611
  const currentTaskConfig = job.current && (currentTask?.config || pipeline?.taskConfig?.[job.current]) || {};
20427
20612
  const costsSummary = job.costsSummary || {};
@@ -20448,12 +20633,26 @@ function JobTable({
20448
20633
  ] }) }),
20449
20634
  /* @__PURE__ */ jsxRuntimeExports.jsx(T, { children: statusBadge(job.status) }),
20450
20635
  /* @__PURE__ */ jsxRuntimeExports.jsx(T, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { direction: "column", gap: "1", children: [
20451
- /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "2", className: "text-slate-700", children: currentTaskName ? currentTaskName : job.status === "completed" ? "—" : job.current ?? "—" }),
20636
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "2", className: "text-slate-700", children: currentTaskName ? currentTaskName : job.status === "done" ? "—" : job.current ?? "—" }),
20452
20637
  currentTask && /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "1", className: "text-slate-500", children: [
20453
20638
  currentTaskConfig?.model || currentTask?.model,
20454
20639
  currentTaskConfig?.temperature != null || currentTask?.temperature != null ? `temp ${currentTaskConfig?.temperature ?? currentTask?.temperature}` : null,
20455
- currentElapsedMs > 0 ? fmtDuration(currentElapsedMs) : null
20456
- ].filter(Boolean).join(" · ") })
20640
+ (() => {
20641
+ const { startMs, endMs } = taskToTimerProps(currentTask);
20642
+ return startMs ? /* @__PURE__ */ jsxRuntimeExports.jsx(
20643
+ TimerText,
20644
+ {
20645
+ startMs,
20646
+ endMs,
20647
+ granularity: "second",
20648
+ className: "text-slate-500"
20649
+ }
20650
+ ) : null;
20651
+ })()
20652
+ ].filter(Boolean).map((item, index2) => /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
20653
+ typeof item === "string" ? item : item,
20654
+ index2 < 2 && " · "
20655
+ ] }, index2)) })
20457
20656
  ] }) }),
20458
20657
  /* @__PURE__ */ jsxRuntimeExports.jsx(T, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { direction: "column", gap: "2", className: "w-32", children: [
20459
20658
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -20480,7 +20679,14 @@ function JobTable({
20480
20679
  ] }) }),
20481
20680
  /* @__PURE__ */ jsxRuntimeExports.jsx(T, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { align: "center", gap: "1", children: [
20482
20681
  /* @__PURE__ */ jsxRuntimeExports.jsx(TimerReset, { className: "h-3 w-3 text-slate-500" }),
20483
- /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "2", className: "text-slate-700", children: fmtDuration(duration) })
20682
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
20683
+ LiveText,
20684
+ {
20685
+ cadenceMs: 1e4,
20686
+ compute: (now) => fmtDuration(jobCumulativeDurationMs(job, now)),
20687
+ className: "text-slate-700"
20688
+ }
20689
+ )
20484
20690
  ] }) }),
20485
20691
  /* @__PURE__ */ jsxRuntimeExports.jsx(T, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
20486
20692
  o$2,
@@ -20747,7 +20953,6 @@ function Layout({
20747
20953
  pageTitle,
20748
20954
  breadcrumbs,
20749
20955
  actions,
20750
- showBackButton = false,
20751
20956
  backTo = "/",
20752
20957
  maxWidth = "max-w-7xl"
20753
20958
  }) {
@@ -20761,9 +20966,6 @@ function Layout({
20761
20966
  if (location.pathname.startsWith(path)) return true;
20762
20967
  return false;
20763
20968
  };
20764
- const handleBack = () => {
20765
- navigate(backTo);
20766
- };
20767
20969
  const toggleUploadPanel = () => {
20768
20970
  setIsUploadOpen(!isUploadOpen);
20769
20971
  };
@@ -20819,20 +21021,6 @@ function Layout({
20819
21021
  gap: "4",
20820
21022
  children: [
20821
21023
  /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { align: "center", className: "min-w-0 flex-1", children: [
20822
- showBackButton && /* @__PURE__ */ jsxRuntimeExports.jsxs(Root3, { delayDuration: 200, children: [
20823
- /* @__PURE__ */ jsxRuntimeExports.jsx(Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
20824
- Button,
20825
- {
20826
- variant: "ghost",
20827
- size: "sm",
20828
- onClick: handleBack,
20829
- className: "shrink-0",
20830
- "aria-label": "Go back",
20831
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { className: "h-4 w-4" })
20832
- }
20833
- ) }),
20834
- /* @__PURE__ */ jsxRuntimeExports.jsx(Content2, { side: "bottom", sideOffset: 5, children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "2", children: "Go back" }) })
20835
- ] }),
20836
21024
  /* @__PURE__ */ jsxRuntimeExports.jsx(
20837
21025
  p$7,
20838
21026
  {
@@ -20966,34 +21154,32 @@ function PromptPipelineDashboard({ isConnected }) {
20966
21154
  return src.map(adaptJobSummary);
20967
21155
  }, [apiJobs, error]);
20968
21156
  const [activeTab, setActiveTab] = reactExports.useState("current");
20969
- const now = useTicker(1e4);
20970
21157
  const errorCount = reactExports.useMemo(
20971
- () => jobs.filter((j) => j.status === "failed").length,
21158
+ () => jobs.filter((j) => j.status === TaskState.FAILED).length,
20972
21159
  [jobs]
20973
21160
  );
20974
21161
  const currentCount = reactExports.useMemo(
20975
- () => jobs.filter((j) => j.status === "running").length,
21162
+ () => jobs.filter((j) => j.status === TaskState.RUNNING).length,
20976
21163
  [jobs]
20977
21164
  );
20978
21165
  const completedCount = reactExports.useMemo(
20979
- () => jobs.filter((j) => j.status === "complete").length,
21166
+ () => jobs.filter((j) => j.status === JobStatus.COMPLETE).length,
20980
21167
  [jobs]
20981
21168
  );
20982
21169
  const filteredJobs = reactExports.useMemo(() => {
20983
21170
  switch (activeTab) {
20984
21171
  case "current":
20985
- return jobs.filter((j) => j.status === "running");
21172
+ return jobs.filter((j) => j.status === TaskState.RUNNING);
20986
21173
  case "errors":
20987
- return jobs.filter((j) => j.status === "failed");
21174
+ return jobs.filter((j) => j.status === TaskState.FAILED);
20988
21175
  case "complete":
20989
- return jobs.filter((j) => j.status === "complete");
21176
+ return jobs.filter((j) => j.status === JobStatus.COMPLETE);
20990
21177
  default:
20991
21178
  return [];
20992
21179
  }
20993
21180
  }, [jobs, activeTab]);
20994
- const overallElapsed = (job) => jobCumulativeDurationMs(job, now);
20995
21181
  const runningJobs = reactExports.useMemo(
20996
- () => jobs.filter((j) => j.status === "running"),
21182
+ () => jobs.filter((j) => j.status === TaskState.RUNNING),
20997
21183
  [jobs]
20998
21184
  );
20999
21185
  const aggregateProgress = reactExports.useMemo(() => {
@@ -21042,41 +21228,42 @@ function PromptPipelineDashboard({ isConnected }) {
21042
21228
  ")"
21043
21229
  ] })
21044
21230
  ] }),
21045
- /* @__PURE__ */ jsxRuntimeExports.jsx(f, { value: "current", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
21046
- JobTable,
21047
- {
21048
- jobs: filteredJobs,
21049
- pipeline: null,
21050
- onOpenJob: openJob,
21051
- overallElapsed,
21052
- now
21053
- }
21054
- ) }),
21055
- /* @__PURE__ */ jsxRuntimeExports.jsx(f, { value: "errors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
21056
- JobTable,
21057
- {
21058
- jobs: filteredJobs,
21059
- pipeline: null,
21060
- onOpenJob: openJob,
21061
- overallElapsed,
21062
- now
21063
- }
21064
- ) }),
21065
- /* @__PURE__ */ jsxRuntimeExports.jsx(f, { value: "complete", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
21066
- JobTable,
21067
- {
21068
- jobs: filteredJobs,
21069
- pipeline: null,
21070
- onOpenJob: openJob,
21071
- overallElapsed,
21072
- now
21073
- }
21074
- ) })
21231
+ /* @__PURE__ */ jsxRuntimeExports.jsx(f, { value: "current", children: /* @__PURE__ */ jsxRuntimeExports.jsx(JobTable, { jobs: filteredJobs, pipeline: null, onOpenJob: openJob }) }),
21232
+ /* @__PURE__ */ jsxRuntimeExports.jsx(f, { value: "errors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(JobTable, { jobs: filteredJobs, pipeline: null, onOpenJob: openJob }) }),
21233
+ /* @__PURE__ */ jsxRuntimeExports.jsx(f, { value: "complete", children: /* @__PURE__ */ jsxRuntimeExports.jsx(JobTable, { jobs: filteredJobs, pipeline: null, onOpenJob: openJob }) })
21075
21234
  ]
21076
21235
  }
21077
21236
  )
21078
21237
  ] });
21079
21238
  }
21239
+ function areGeometriesEqual(prev, next, epsilon = 0.5) {
21240
+ if (prev === next) return true;
21241
+ if (!prev || !next) return false;
21242
+ if (prev.itemsLength !== next.itemsLength) return false;
21243
+ if (prev.effectiveCols !== next.effectiveCols) return false;
21244
+ if (!areOverlayBoxesEqual(prev.overlayBox, next.overlayBox, epsilon)) {
21245
+ return false;
21246
+ }
21247
+ const prevBoxes = prev.boxes;
21248
+ const nextBoxes = next.boxes;
21249
+ if (prevBoxes.length !== nextBoxes.length) return false;
21250
+ for (let i2 = 0; i2 < prevBoxes.length; i2++) {
21251
+ if (!areBoxesEqual(prevBoxes[i2], nextBoxes[i2], epsilon)) {
21252
+ return false;
21253
+ }
21254
+ }
21255
+ return true;
21256
+ }
21257
+ function areOverlayBoxesEqual(a2, b2, epsilon) {
21258
+ return areNumbersClose(a2.left, b2.left, epsilon) && areNumbersClose(a2.top, b2.top, epsilon) && areNumbersClose(a2.width, b2.width, epsilon) && areNumbersClose(a2.height, b2.height, epsilon) && areNumbersClose(a2.right, b2.right, epsilon) && areNumbersClose(a2.bottom, b2.bottom, epsilon);
21259
+ }
21260
+ function areBoxesEqual(a2, b2, epsilon) {
21261
+ if (!a2 || !b2) return a2 === b2;
21262
+ return areNumbersClose(a2.left, b2.left, epsilon) && areNumbersClose(a2.top, b2.top, epsilon) && areNumbersClose(a2.width, b2.width, epsilon) && areNumbersClose(a2.height, b2.height, epsilon) && areNumbersClose(a2.right, b2.right, epsilon) && areNumbersClose(a2.bottom, b2.bottom, epsilon) && areNumbersClose(a2.headerMidY, b2.headerMidY, epsilon);
21263
+ }
21264
+ function areNumbersClose(a2, b2, epsilon) {
21265
+ return Math.abs(a2 - b2) <= epsilon;
21266
+ }
21080
21267
  function TaskFilePane({
21081
21268
  isOpen,
21082
21269
  jobId,
@@ -21512,11 +21699,357 @@ function TaskFilePane({
21512
21699
  ] })
21513
21700
  ] });
21514
21701
  }
21515
- const CATEGORY_KEYS = ["artifacts", "logs", "tmp"];
21516
- const LEGACY_KEY_SET = /* @__PURE__ */ new Set([
21517
- "input",
21518
- "inputs",
21519
- "output",
21702
+ function TaskDetailSidebar({
21703
+ open,
21704
+ title,
21705
+ status,
21706
+ jobId,
21707
+ taskId,
21708
+ taskBody,
21709
+ filesByTypeForItem = () => ({ artifacts: [], logs: [], tmp: [] }),
21710
+ task,
21711
+ onClose,
21712
+ taskIndex
21713
+ // Add taskIndex for ID compatibility
21714
+ }) {
21715
+ const [filePaneType, setFilePaneType] = reactExports.useState("artifacts");
21716
+ const [filePaneOpen, setFilePaneOpen] = reactExports.useState(false);
21717
+ const [filePaneFilename, setFilePaneFilename] = reactExports.useState(null);
21718
+ const closeButtonRef = reactExports.useRef(null);
21719
+ const getHeaderClasses2 = (status2) => {
21720
+ switch (status2) {
21721
+ case TaskState.DONE:
21722
+ return "bg-green-50 border-green-200 text-green-700";
21723
+ case TaskState.RUNNING:
21724
+ return "bg-amber-50 border-amber-200 text-amber-700";
21725
+ case TaskState.FAILED:
21726
+ return "bg-pink-50 border-pink-200 text-pink-700";
21727
+ default:
21728
+ return "bg-gray-100 border-gray-200 text-gray-700";
21729
+ }
21730
+ };
21731
+ reactExports.useEffect(() => {
21732
+ if (open && closeButtonRef.current) {
21733
+ closeButtonRef.current.focus();
21734
+ }
21735
+ }, [open]);
21736
+ reactExports.useEffect(() => {
21737
+ if (open) {
21738
+ setFilePaneType("artifacts");
21739
+ setFilePaneOpen(false);
21740
+ setFilePaneFilename(null);
21741
+ }
21742
+ }, [open]);
21743
+ reactExports.useEffect(() => {
21744
+ setFilePaneFilename(null);
21745
+ setFilePaneOpen(false);
21746
+ }, [filePaneType]);
21747
+ const handleFileClick = (filename) => {
21748
+ setFilePaneFilename(filename);
21749
+ setFilePaneOpen(true);
21750
+ };
21751
+ const handleFilePaneClose = () => {
21752
+ setFilePaneOpen(false);
21753
+ setFilePaneFilename(null);
21754
+ };
21755
+ if (!open) {
21756
+ return null;
21757
+ }
21758
+ const filesForStep = filesByTypeForItem(task);
21759
+ const filesForTab = filesForStep[filePaneType] ?? [];
21760
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
21761
+ "aside",
21762
+ {
21763
+ role: "dialog",
21764
+ "aria-modal": "true",
21765
+ "aria-labelledby": `slide-over-title-${taskIndex}`,
21766
+ "aria-hidden": false,
21767
+ className: `fixed inset-y-0 right-0 z-[2000] w-full max-w-4xl bg-white border-l border-gray-200 transform transition-transform duration-300 ease-out translate-x-0`,
21768
+ children: [
21769
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
21770
+ "div",
21771
+ {
21772
+ className: `px-6 py-4 border-b flex items-center justify-between ${getHeaderClasses2(status)}`,
21773
+ children: [
21774
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21775
+ "div",
21776
+ {
21777
+ id: `slide-over-title-${taskIndex}`,
21778
+ className: "text-lg font-semibold truncate",
21779
+ children: title
21780
+ }
21781
+ ),
21782
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21783
+ "button",
21784
+ {
21785
+ ref: closeButtonRef,
21786
+ type: "button",
21787
+ "aria-label": "Close details",
21788
+ onClick: onClose,
21789
+ className: "rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 text-base",
21790
+ children: "×"
21791
+ }
21792
+ )
21793
+ ]
21794
+ }
21795
+ ),
21796
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-8 overflow-y-auto h-full", children: [
21797
+ status === TaskState.FAILED && taskBody && /* @__PURE__ */ jsxRuntimeExports.jsx("section", { "aria-label": "Error", children: /* @__PURE__ */ jsxRuntimeExports.jsx(n$2, { role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsxRuntimeExports.jsx(u$1, { className: "whitespace-pre-wrap break-words", children: taskBody }) }) }),
21798
+ /* @__PURE__ */ jsxRuntimeExports.jsx("section", { className: "mt-6", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
21799
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-base font-semibold text-gray-900", children: "Files" }),
21800
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center space-x-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex rounded-lg border border-gray-200 bg-gray-50 p-1", children: [
21801
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21802
+ "button",
21803
+ {
21804
+ onClick: () => setFilePaneType("artifacts"),
21805
+ className: `px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${filePaneType === "artifacts" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
21806
+ children: "Artifacts"
21807
+ }
21808
+ ),
21809
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21810
+ "button",
21811
+ {
21812
+ onClick: () => setFilePaneType("logs"),
21813
+ className: `px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${filePaneType === "logs" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
21814
+ children: "Logs"
21815
+ }
21816
+ ),
21817
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21818
+ "button",
21819
+ {
21820
+ onClick: () => setFilePaneType("tmp"),
21821
+ className: `px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${filePaneType === "tmp" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
21822
+ children: "Temp"
21823
+ }
21824
+ )
21825
+ ] }) })
21826
+ ] }) }),
21827
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
21828
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm text-gray-600", children: [
21829
+ filePaneType.charAt(0).toUpperCase() + filePaneType.slice(1),
21830
+ " files for ",
21831
+ taskId
21832
+ ] }),
21833
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1", children: filesForTab.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm text-gray-500 italic py-4 text-center", children: [
21834
+ "No ",
21835
+ filePaneType,
21836
+ " files available for this task"
21837
+ ] }) : filesForTab.map((name) => /* @__PURE__ */ jsxRuntimeExports.jsx(
21838
+ "div",
21839
+ {
21840
+ className: "flex items-center justify-between p-2 rounded border border-gray-200 hover:border-gray-300 hover:bg-gray-50 cursor-pointer transition-colors",
21841
+ onClick: () => handleFileClick(name),
21842
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center space-x-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: name }) })
21843
+ },
21844
+ `${filePaneType}-${name}`
21845
+ )) })
21846
+ ] }),
21847
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21848
+ TaskFilePane,
21849
+ {
21850
+ isOpen: filePaneOpen,
21851
+ jobId,
21852
+ taskId,
21853
+ type: filePaneType,
21854
+ filename: filePaneFilename,
21855
+ onClose: handleFilePaneClose
21856
+ }
21857
+ )
21858
+ ] })
21859
+ ]
21860
+ }
21861
+ );
21862
+ }
21863
+ React.memo(TaskDetailSidebar);
21864
+ function RestartJobModal({
21865
+ open,
21866
+ onClose,
21867
+ onConfirm,
21868
+ jobId,
21869
+ taskId,
21870
+ isSubmitting = false
21871
+ }) {
21872
+ const modalRef = reactExports.useRef(null);
21873
+ reactExports.useEffect(() => {
21874
+ const handleKeyDown2 = (e2) => {
21875
+ if (e2.key === "Escape" && open) {
21876
+ e2.preventDefault();
21877
+ onClose();
21878
+ }
21879
+ };
21880
+ if (open) {
21881
+ document.addEventListener("keydown", handleKeyDown2);
21882
+ if (modalRef.current) {
21883
+ modalRef.current.focus();
21884
+ }
21885
+ return () => {
21886
+ document.removeEventListener("keydown", handleKeyDown2);
21887
+ };
21888
+ }
21889
+ }, [open, onClose]);
21890
+ const handleKeyDown = (e2) => {
21891
+ if (e2.key === "Enter" && !isSubmitting && open) {
21892
+ e2.preventDefault();
21893
+ onConfirm();
21894
+ }
21895
+ };
21896
+ if (!open) return null;
21897
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
21898
+ "div",
21899
+ {
21900
+ className: "fixed inset-0 z-50 flex items-center justify-center",
21901
+ "aria-hidden": !open,
21902
+ children: [
21903
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21904
+ "div",
21905
+ {
21906
+ className: "absolute inset-0 bg-black/50",
21907
+ onClick: onClose,
21908
+ "aria-hidden": "true"
21909
+ }
21910
+ ),
21911
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21912
+ "div",
21913
+ {
21914
+ ref: modalRef,
21915
+ role: "dialog",
21916
+ "aria-modal": "true",
21917
+ "aria-labelledby": "restart-modal-title",
21918
+ "aria-describedby": "restart-modal-description",
21919
+ className: "relative bg-white rounded-lg shadow-2xl border border-gray-200 max-w-lg w-full mx-4 outline-none",
21920
+ style: { minWidth: "320px", maxWidth: "560px" },
21921
+ tabIndex: -1,
21922
+ onKeyDown: handleKeyDown,
21923
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6", children: [
21924
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21925
+ r$8,
21926
+ {
21927
+ id: "restart-modal-title",
21928
+ as: "h2",
21929
+ size: "5",
21930
+ className: "mb-4 text-gray-900",
21931
+ children: taskId ? `Restart from ${taskId}` : "Restart job (reset progress)"
21932
+ }
21933
+ ),
21934
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(p$7, { id: "restart-modal-description", className: "mb-6", children: [
21935
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { as: "p", className: "text-gray-700 mb-4", children: taskId ? `This will restart the job from the "${taskId}" task. Tasks before ${taskId} will remain completed, while ${taskId} and all subsequent tasks will be reset to pending. Files and artifacts are preserved. A new background run will start automatically. This cannot be undone.` : "This will clear the job's progress and active stage and reset all tasks to pending. Files and artifacts are preserved. A new background run will start automatically. This cannot be undone." }),
21936
+ taskId && /* @__PURE__ */ jsxRuntimeExports.jsxs(p$b, { as: "p", className: "text-sm text-gray-600 mb-3", children: [
21937
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Triggered from task:" }),
21938
+ " ",
21939
+ taskId
21940
+ ] }),
21941
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { as: "p", className: "text-sm text-gray-500 italic", children: "Note: Job must be in current lifecycle and not running." })
21942
+ ] }),
21943
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { gap: "3", justify: "end", children: [
21944
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21945
+ Button,
21946
+ {
21947
+ variant: "outline",
21948
+ onClick: onClose,
21949
+ disabled: isSubmitting,
21950
+ className: "min-w-[80px]",
21951
+ children: "Cancel"
21952
+ }
21953
+ ),
21954
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
21955
+ Button,
21956
+ {
21957
+ variant: "destructive",
21958
+ onClick: onConfirm,
21959
+ disabled: isSubmitting,
21960
+ className: "min-w-[80px]",
21961
+ children: isSubmitting ? "Restarting..." : "Restart"
21962
+ }
21963
+ )
21964
+ ] })
21965
+ ] })
21966
+ }
21967
+ )
21968
+ ]
21969
+ }
21970
+ ) });
21971
+ }
21972
+ async function restartJob(jobId, opts = {}) {
21973
+ const options = {
21974
+ clearTokenUsage: true,
21975
+ ...opts.options
21976
+ };
21977
+ const requestBody = opts.fromTask ? { fromTask: opts.fromTask, options } : { mode: "clean-slate", options };
21978
+ try {
21979
+ const response = await fetch(
21980
+ `/api/jobs/${encodeURIComponent(jobId)}/restart`,
21981
+ {
21982
+ method: "POST",
21983
+ headers: {
21984
+ "Content-Type": "application/json"
21985
+ },
21986
+ body: JSON.stringify(requestBody)
21987
+ }
21988
+ );
21989
+ if (!response.ok) {
21990
+ let errorData;
21991
+ try {
21992
+ errorData = await response.json();
21993
+ } catch {
21994
+ errorData = { message: response.statusText };
21995
+ }
21996
+ throw {
21997
+ code: errorData.code || getErrorCodeFromStatus(response.status),
21998
+ message: getRestartErrorMessage(errorData, response.status),
21999
+ status: response.status
22000
+ };
22001
+ }
22002
+ return await response.json();
22003
+ } catch (error) {
22004
+ if (error.code && error.message) {
22005
+ throw error;
22006
+ }
22007
+ throw {
22008
+ code: "network_error",
22009
+ message: error.message || "Failed to connect to server"
22010
+ };
22011
+ }
22012
+ }
22013
+ function getErrorCodeFromStatus(status) {
22014
+ switch (status) {
22015
+ case 404:
22016
+ return "job_not_found";
22017
+ case 409:
22018
+ return "conflict";
22019
+ case 500:
22020
+ return "spawn_failed";
22021
+ default:
22022
+ return "unknown_error";
22023
+ }
22024
+ }
22025
+ function getRestartErrorMessage(errorData, status) {
22026
+ if (status === 409) {
22027
+ if (errorData.code === "job_running") {
22028
+ return "Job is currently running; restart is unavailable.";
22029
+ }
22030
+ if (errorData.code === "unsupported_lifecycle") {
22031
+ return "Job must be in current to restart.";
22032
+ }
22033
+ if (errorData.message?.includes("job_running")) {
22034
+ return "Job is currently running; restart is unavailable.";
22035
+ }
22036
+ if (errorData.message?.includes("unsupported_lifecycle")) {
22037
+ return "Job must be in current to restart.";
22038
+ }
22039
+ }
22040
+ if (status === 404) {
22041
+ return "Job not found.";
22042
+ }
22043
+ if (status === 500) {
22044
+ return "Failed to start restart. Try again.";
22045
+ }
22046
+ return errorData.message || "Failed to restart job.";
22047
+ }
22048
+ const CATEGORY_KEYS = ["artifacts", "logs", "tmp"];
22049
+ const LEGACY_KEY_SET = /* @__PURE__ */ new Set([
22050
+ "input",
22051
+ "inputs",
22052
+ "output",
21520
22053
  "outputs",
21521
22054
  "legacyInput",
21522
22055
  "legacyOutput",
@@ -21613,6 +22146,10 @@ function getTaskFilesForTask(job, taskId) {
21613
22146
  console.debug("[getTaskFilesForTask] Task files result:", { taskId, result });
21614
22147
  return result;
21615
22148
  }
22149
+ const prefersReducedMotion = () => {
22150
+ if (typeof window === "undefined") return false;
22151
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
22152
+ };
21616
22153
  function upperFirst(s2) {
21617
22154
  return typeof s2 === "string" && s2.length > 0 ? s2.charAt(0).toUpperCase() + s2.slice(1) : s2;
21618
22155
  }
@@ -21635,26 +22172,136 @@ function formatStepName(item, idx) {
21635
22172
  const raw = item.title ?? item.id ?? `Step ${idx + 1}`;
21636
22173
  return upperFirst(item.title ? item.title : raw);
21637
22174
  }
21638
- const createDAGGridLogger = (jobId) => {
21639
- const prefix = `[DAGGrid:${jobId || "unknown"}]`;
21640
- return {
21641
- log: (message, data = null) => {
21642
- console.log(`${prefix} ${message}`, data ? data : "");
21643
- },
21644
- warn: (message, data = null) => {
21645
- console.warn(`${prefix} ${message}`, data ? data : "");
21646
- },
21647
- error: (message, data = null) => {
21648
- console.error(`${prefix} ${message}`, data ? data : "");
21649
- },
21650
- group: (label) => console.group(`${prefix} ${label}`),
21651
- groupEnd: () => console.groupEnd(),
21652
- table: (data, title) => {
21653
- console.log(`${prefix} ${title}:`);
21654
- console.table(data);
21655
- }
21656
- };
22175
+ const getHeaderClasses = (status) => {
22176
+ switch (status) {
22177
+ case TaskState.DONE:
22178
+ return "bg-green-50 border-green-200 text-green-700";
22179
+ case TaskState.RUNNING:
22180
+ return "bg-amber-50 border-amber-200 text-amber-700";
22181
+ case TaskState.FAILED:
22182
+ return "bg-pink-50 border-pink-200 text-pink-700";
22183
+ default:
22184
+ return "bg-gray-100 border-gray-200 text-gray-700";
22185
+ }
21657
22186
  };
22187
+ const canShowRestart = (status) => {
22188
+ return status === TaskState.FAILED || status === TaskState.DONE;
22189
+ };
22190
+ const TaskCard = reactExports.memo(function TaskCard2({
22191
+ item,
22192
+ idx,
22193
+ nodeRef,
22194
+ status,
22195
+ isActive,
22196
+ canRestart,
22197
+ isSubmitting,
22198
+ getRestartDisabledReason,
22199
+ onClick,
22200
+ onKeyDown,
22201
+ handleRestartClick
22202
+ }) {
22203
+ const { startMs, endMs } = taskToTimerProps(item);
22204
+ const reducedMotion = prefersReducedMotion();
22205
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
22206
+ "div",
22207
+ {
22208
+ ref: nodeRef,
22209
+ role: "listitem",
22210
+ "aria-current": isActive ? "step" : void 0,
22211
+ tabIndex: 0,
22212
+ onClick,
22213
+ onKeyDown,
22214
+ className: `cursor-pointer rounded-lg border border-gray-400 ${status === TaskState.PENDING ? "bg-gray-50" : "bg-white"} overflow-hidden flex flex-col ${reducedMotion ? "" : "transition-all duration-200 ease-in-out"} outline outline-2 outline-transparent hover:outline-gray-400/70 focus-visible:outline-blue-500/60`,
22215
+ children: [
22216
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
22217
+ "div",
22218
+ {
22219
+ "data-role": "card-header",
22220
+ className: `rounded-t-lg px-4 py-2 border-b flex items-center justify-between gap-3 ${reducedMotion ? "" : "transition-opacity duration-300 ease-in-out"} ${getHeaderClasses(status)}`,
22221
+ children: [
22222
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium truncate", children: formatStepName(item, idx) }),
22223
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2", children: status === TaskState.RUNNING ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
22224
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-4 w-4", "aria-label": "Active", children: [
22225
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "sr-only", children: "Active" }),
22226
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute inset-0 rounded-full border-2 border-amber-200" }),
22227
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
22228
+ "span",
22229
+ {
22230
+ className: `absolute inset-0 rounded-full border-2 border-transparent border-t-amber-600 ${reducedMotion ? "" : "animate-spin"}`
22231
+ }
22232
+ )
22233
+ ] }),
22234
+ item.stage && /* @__PURE__ */ jsxRuntimeExports.jsx(
22235
+ "span",
22236
+ {
22237
+ className: "text-[11px] font-medium opacity-80 truncate uppercase tracking-wide",
22238
+ title: item.stage,
22239
+ children: formatStageLabel(item.stage)
22240
+ }
22241
+ ),
22242
+ startMs && /* @__PURE__ */ jsxRuntimeExports.jsx(
22243
+ TimerText,
22244
+ {
22245
+ startMs,
22246
+ granularity: "second",
22247
+ className: "text-[11px] opacity-80"
22248
+ }
22249
+ )
22250
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
22251
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
22252
+ "span",
22253
+ {
22254
+ className: `text-[11px] uppercase tracking-wide opacity-80${reducedMotion ? "" : " transition-opacity duration-200"}`,
22255
+ children: [
22256
+ status,
22257
+ status === TaskState.FAILED && item.stage && /* @__PURE__ */ jsxRuntimeExports.jsxs(
22258
+ "span",
22259
+ {
22260
+ className: "text-[11px] font-medium opacity-80 truncate ml-2 uppercase tracking-wide",
22261
+ title: item.stage,
22262
+ children: [
22263
+ "(",
22264
+ formatStageLabel(item.stage),
22265
+ ")"
22266
+ ]
22267
+ }
22268
+ )
22269
+ ]
22270
+ }
22271
+ ),
22272
+ status === TaskState.DONE && startMs && /* @__PURE__ */ jsxRuntimeExports.jsx(
22273
+ TimerText,
22274
+ {
22275
+ startMs,
22276
+ endMs: endMs || item.finishedAt,
22277
+ granularity: "minute",
22278
+ className: "text-[11px] opacity-80"
22279
+ }
22280
+ )
22281
+ ] }) })
22282
+ ]
22283
+ }
22284
+ ),
22285
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4", children: [
22286
+ item.subtitle && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-gray-600", children: item.subtitle }),
22287
+ item.body && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 text-sm text-gray-700", children: item.body }),
22288
+ canShowRestart(status) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 pt-3 border-t border-gray-100", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
22289
+ Button,
22290
+ {
22291
+ variant: "outline",
22292
+ size: "sm",
22293
+ onClick: (e2) => handleRestartClick(e2, item.id),
22294
+ disabled: !canRestart || isSubmitting,
22295
+ className: "text-xs cursor-pointer disabled:cursor-not-allowed",
22296
+ title: !canRestart ? getRestartDisabledReason() : `Restart job from ${item.id}`,
22297
+ children: "Restart"
22298
+ }
22299
+ ) })
22300
+ ] })
22301
+ ]
22302
+ }
22303
+ );
22304
+ });
21658
22305
  function DAGGrid({
21659
22306
  items,
21660
22307
  cols = 3,
@@ -21663,28 +22310,19 @@ function DAGGrid({
21663
22310
  jobId,
21664
22311
  filesByTypeForItem = () => createEmptyTaskFiles()
21665
22312
  }) {
21666
- const logger = React.useMemo(() => createDAGGridLogger(jobId), [jobId]);
21667
22313
  const overlayRef = reactExports.useRef(null);
21668
22314
  const gridRef = reactExports.useRef(null);
21669
22315
  const nodeRefs = reactExports.useRef([]);
21670
22316
  const [lines, setLines] = reactExports.useState([]);
21671
22317
  const [effectiveCols, setEffectiveCols] = reactExports.useState(cols);
21672
- const [openIdx, setOpenIdx] = reactExports.useState(null);
21673
- const [selectedFile, setSelectedFile] = reactExports.useState(null);
21674
- const [filePaneOpen, setFilePaneOpen] = reactExports.useState(false);
21675
- const [filePaneType, setFilePaneType] = reactExports.useState("artifacts");
21676
- const [filePaneFilename, setFilePaneFilename] = reactExports.useState(null);
21677
- React.useEffect(() => {
21678
- logger.group("Component Render");
21679
- logger.log("Props received:", {
21680
- itemCount: items?.length,
21681
- cols,
21682
- activeIndex,
21683
- jobId
21684
- });
21685
- logger.log("Items data:", items);
21686
- logger.groupEnd();
21687
- }, [items, cols, activeIndex, jobId, logger]);
22318
+ const [openIdx, setOpenIdx] = reactExports.useState(-1);
22319
+ const [restartModalOpen, setRestartModalOpen] = reactExports.useState(false);
22320
+ const [restartTaskId, setRestartTaskId] = reactExports.useState(null);
22321
+ const [isSubmitting, setIsSubmitting] = reactExports.useState(false);
22322
+ const [alertMessage, setAlertMessage] = reactExports.useState(null);
22323
+ const [alertType, setAlertType] = reactExports.useState("info");
22324
+ const prevGeometryRef = reactExports.useRef(null);
22325
+ const rafRef = reactExports.useRef(null);
21688
22326
  nodeRefs.current = reactExports.useMemo(
21689
22327
  () => items.map((_, i2) => nodeRefs.current[i2] ?? reactExports.createRef()),
21690
22328
  [items.length]
@@ -21710,9 +22348,10 @@ function DAGGrid({
21710
22348
  const end = Math.min(start + effectiveCols, items.length);
21711
22349
  const slice = Array.from({ length: end - start }, (_, k) => start + k);
21712
22350
  const rowLen = slice.length;
21713
- const pad = Math.max(0, effectiveCols - rowLen);
21714
- if (r2 % 2 === 1) {
22351
+ const isReversedRow = r2 % 2 === 1;
22352
+ if (isReversedRow) {
21715
22353
  const reversed = slice.reverse();
22354
+ const pad = effectiveCols - rowLen;
21716
22355
  order.push(...Array(pad).fill(-1), ...reversed);
21717
22356
  } else {
21718
22357
  order.push(...slice);
@@ -21724,11 +22363,11 @@ function DAGGrid({
21724
22363
  if (typeof window === "undefined" || !overlayRef.current || items.length === 0) {
21725
22364
  return;
21726
22365
  }
21727
- let isComputing = false;
21728
22366
  const compute = () => {
21729
- if (isComputing) return;
21730
- isComputing = true;
21731
- try {
22367
+ if (rafRef.current) {
22368
+ cancelAnimationFrame(rafRef.current);
22369
+ }
22370
+ rafRef.current = requestAnimationFrame(() => {
21732
22371
  if (!overlayRef.current) return;
21733
22372
  const overlayBox = overlayRef.current.getBoundingClientRect();
21734
22373
  const boxes = nodeRefs.current.map((r2) => {
@@ -21750,6 +22389,17 @@ function DAGGrid({
21750
22389
  headerMidY
21751
22390
  };
21752
22391
  });
22392
+ const currentGeometry = {
22393
+ overlayBox,
22394
+ boxes: boxes.filter(Boolean),
22395
+ effectiveCols,
22396
+ itemsLength: items.length
22397
+ };
22398
+ const geometryChanged = !prevGeometryRef.current || !areGeometriesEqual(prevGeometryRef.current, currentGeometry);
22399
+ if (!geometryChanged) {
22400
+ rafRef.current = null;
22401
+ return;
22402
+ }
21753
22403
  const newLines = [];
21754
22404
  for (let i2 = 0; i2 < items.length - 1; i2++) {
21755
22405
  const a2 = boxes[i2];
@@ -21784,10 +22434,10 @@ function DAGGrid({
21784
22434
  });
21785
22435
  }
21786
22436
  }
22437
+ prevGeometryRef.current = currentGeometry;
21787
22438
  setLines(newLines);
21788
- } finally {
21789
- isComputing = false;
21790
- }
22439
+ rafRef.current = null;
22440
+ });
21791
22441
  };
21792
22442
  compute();
21793
22443
  let ro = null;
@@ -21804,66 +22454,147 @@ function DAGGrid({
21804
22454
  if (ro) ro.disconnect();
21805
22455
  window.removeEventListener("resize", handleResize);
21806
22456
  window.removeEventListener("scroll", handleScroll, true);
22457
+ if (rafRef.current) {
22458
+ cancelAnimationFrame(rafRef.current);
22459
+ }
21807
22460
  };
21808
22461
  }, [items, effectiveCols, visualOrder]);
21809
22462
  const getStatus = (index2) => {
21810
22463
  const item = items[index2];
21811
22464
  const s2 = item?.status;
21812
- if (s2 === "failed") return "failed";
21813
- if (s2 === "done") return "done";
21814
- if (s2 === "running") return "running";
22465
+ if (s2 === TaskState.FAILED) return TaskState.FAILED;
22466
+ if (s2 === TaskState.DONE) return TaskState.DONE;
22467
+ if (s2 === TaskState.RUNNING) return TaskState.RUNNING;
21815
22468
  if (typeof activeIndex === "number") {
21816
- if (index2 < activeIndex) return "done";
21817
- if (index2 === activeIndex) return "running";
21818
- return "pending";
21819
- }
21820
- return "pending";
21821
- };
21822
- const getHeaderClasses = (status) => {
21823
- switch (status) {
21824
- case "done":
21825
- return "bg-green-50 border-green-200 text-green-700";
21826
- case "running":
21827
- return "bg-amber-50 border-amber-200 text-amber-700";
21828
- case "failed":
21829
- return "bg-pink-50 border-pink-200 text-pink-700";
21830
- default:
21831
- return "bg-gray-100 border-gray-200 text-gray-700";
22469
+ if (index2 < activeIndex) return TaskState.DONE;
22470
+ if (index2 === activeIndex) return TaskState.RUNNING;
22471
+ return TaskState.PENDING;
21832
22472
  }
22473
+ return TaskState.PENDING;
21833
22474
  };
21834
22475
  React.useEffect(() => {
21835
22476
  const handleKeyDown = (e2) => {
21836
- if (e2.key === "Escape" && openIdx !== null) {
21837
- setOpenIdx(null);
21838
- setSelectedFile(null);
22477
+ if (e2.key === "Escape" && openIdx !== -1) {
22478
+ setOpenIdx(-1);
21839
22479
  }
21840
22480
  };
21841
- if (openIdx !== null) {
22481
+ if (openIdx !== -1) {
21842
22482
  document.addEventListener("keydown", handleKeyDown);
21843
22483
  return () => document.removeEventListener("keydown", handleKeyDown);
21844
22484
  }
21845
22485
  }, [openIdx]);
21846
- const closeButtonRef = reactExports.useRef(null);
21847
- React.useEffect(() => {
21848
- if (openIdx !== null && closeButtonRef.current) {
21849
- closeButtonRef.current.focus();
22486
+ const handleRestartClick = (e2, taskId) => {
22487
+ e2.stopPropagation();
22488
+ setRestartTaskId(taskId);
22489
+ setRestartModalOpen(true);
22490
+ };
22491
+ const handleRestartConfirm = async () => {
22492
+ if (!jobId || isSubmitting) return;
22493
+ setIsSubmitting(true);
22494
+ setAlertMessage(null);
22495
+ try {
22496
+ const restartOptions = {};
22497
+ if (restartTaskId) {
22498
+ restartOptions.fromTask = restartTaskId;
22499
+ }
22500
+ await restartJob(jobId, restartOptions);
22501
+ const successMessage = restartTaskId ? `Restart requested from ${restartTaskId}. The job will start from that task in the background.` : "Restart requested. The job will reset to pending and start in the background.";
22502
+ setAlertMessage(successMessage);
22503
+ setAlertType("success");
22504
+ setRestartModalOpen(false);
22505
+ setRestartTaskId(null);
22506
+ } catch (error) {
22507
+ let message = "Failed to start restart. Try again.";
22508
+ let type = "error";
22509
+ switch (error.code) {
22510
+ case "job_running":
22511
+ message = "Job is currently running; restart is unavailable.";
22512
+ type = "warning";
22513
+ break;
22514
+ case "unsupported_lifecycle":
22515
+ message = "Job must be in current lifecycle to restart.";
22516
+ type = "warning";
22517
+ break;
22518
+ case "job_not_found":
22519
+ message = "Job not found.";
22520
+ type = "error";
22521
+ break;
22522
+ case "spawn_failed":
22523
+ message = "Failed to start restart. Try again.";
22524
+ type = "error";
22525
+ break;
22526
+ default:
22527
+ message = error.message || "An unexpected error occurred.";
22528
+ type = "error";
22529
+ }
22530
+ setAlertMessage(message);
22531
+ setAlertType(type);
22532
+ } finally {
22533
+ setIsSubmitting(false);
21850
22534
  }
21851
- }, [openIdx]);
21852
- React.useEffect(() => {
21853
- setFilePaneFilename(null);
21854
- setFilePaneOpen(false);
21855
- }, [filePaneType]);
22535
+ };
22536
+ const handleRestartCancel = () => {
22537
+ setRestartModalOpen(false);
22538
+ setRestartTaskId(null);
22539
+ };
21856
22540
  React.useEffect(() => {
21857
- if (openIdx === null) {
21858
- setFilePaneFilename(null);
21859
- setFilePaneOpen(false);
21860
- return;
21861
- }
21862
- setFilePaneType("artifacts");
21863
- setFilePaneFilename(null);
21864
- setFilePaneOpen(false);
21865
- }, [openIdx]);
22541
+ if (alertMessage) {
22542
+ const timer = setTimeout(() => {
22543
+ setAlertMessage(null);
22544
+ }, 5e3);
22545
+ return () => clearTimeout(timer);
22546
+ }
22547
+ }, [alertMessage]);
22548
+ const isRestartEnabled = React.useCallback(() => {
22549
+ const isJobRunning = items.some(
22550
+ (item) => item?.state === TaskState.RUNNING
22551
+ );
22552
+ const hasRunningTask = items.some(
22553
+ (item) => item?.status === TaskState.RUNNING
22554
+ );
22555
+ const jobLifecycle = items[0]?.lifecycle || "current";
22556
+ return jobLifecycle === "current" && !isJobRunning && !hasRunningTask;
22557
+ }, [items]);
22558
+ const getRestartDisabledReason = React.useCallback(() => {
22559
+ const isJobRunning = items.some(
22560
+ (item) => item?.state === TaskState.RUNNING
22561
+ );
22562
+ const hasRunningTask = items.some(
22563
+ (item) => item?.status === TaskState.RUNNING
22564
+ );
22565
+ const jobLifecycle = items[0]?.lifecycle || "current";
22566
+ if (isJobRunning || hasRunningTask) return "Job is currently running";
22567
+ if (jobLifecycle !== "current") return "Job must be in current lifecycle";
22568
+ return "";
22569
+ }, [items]);
21866
22570
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full", role: "list", children: [
22571
+ alertMessage && /* @__PURE__ */ jsxRuntimeExports.jsx(
22572
+ "div",
22573
+ {
22574
+ className: `fixed top-4 right-4 z-[3000] max-w-sm p-4 rounded-lg shadow-lg border ${alertType === "success" ? "bg-green-50 border-green-200 text-green-800" : alertType === "error" ? "bg-red-50 border-red-200 text-red-800" : alertType === "warning" ? "bg-yellow-50 border-yellow-200 text-yellow-800" : "bg-blue-50 border-blue-200 text-blue-800"}`,
22575
+ role: "alert",
22576
+ "aria-live": "polite",
22577
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start", children: [
22578
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm font-medium", children: alertMessage }) }),
22579
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
22580
+ "button",
22581
+ {
22582
+ onClick: () => setAlertMessage(null),
22583
+ className: "ml-3 flex-shrink-0 inline-flex text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
22584
+ "aria-label": "Dismiss notification",
22585
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "h-4 w-4", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
22586
+ "path",
22587
+ {
22588
+ fillRule: "evenodd",
22589
+ d: "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z",
22590
+ clipRule: "evenodd"
22591
+ }
22592
+ ) })
22593
+ }
22594
+ )
22595
+ ] })
22596
+ }
22597
+ ),
21867
22598
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
21868
22599
  "svg",
21869
22600
  {
@@ -21882,7 +22613,8 @@ function DAGGrid({
21882
22613
  markerHeight: "8",
21883
22614
  orient: "auto",
21884
22615
  markerUnits: "userSpaceOnUse",
21885
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "#9ca3af" })
22616
+ className: "text-gray-400",
22617
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M 0 0 L 10 5 L 0 10 z" })
21886
22618
  }
21887
22619
  ) }),
21888
22620
  lines.map((line, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("g", { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -21891,9 +22623,9 @@ function DAGGrid({
21891
22623
  d: line.d,
21892
22624
  fill: "none",
21893
22625
  stroke: "currentColor",
21894
- strokeWidth: "3",
21895
- strokeLinecap: "round",
21896
- className: "text-gray-300",
22626
+ strokeWidth: "1",
22627
+ strokeLinecap: "square",
22628
+ className: "text-gray-400",
21897
22629
  strokeLinejoin: "round",
21898
22630
  markerEnd: "url(#arrow)"
21899
22631
  }
@@ -21920,195 +22652,58 @@ function DAGGrid({
21920
22652
  const item = items[idx];
21921
22653
  const status = getStatus(idx);
21922
22654
  const isActive = idx === activeIndex;
21923
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
21924
- "div",
22655
+ const canRestart = isRestartEnabled();
22656
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
22657
+ TaskCard,
21925
22658
  {
21926
- ref: nodeRefs.current[idx],
21927
- role: "listitem",
21928
- "aria-current": isActive ? "step" : void 0,
21929
- tabIndex: 0,
22659
+ idx,
22660
+ nodeRef: nodeRefs.current[idx],
22661
+ status,
22662
+ isActive,
22663
+ canRestart,
22664
+ isSubmitting,
22665
+ getRestartDisabledReason,
21930
22666
  onClick: () => {
21931
22667
  setOpenIdx(idx);
21932
- setSelectedFile(null);
21933
22668
  },
21934
22669
  onKeyDown: (e2) => {
21935
22670
  if (e2.key === "Enter" || e2.key === " ") {
21936
22671
  e2.preventDefault();
21937
22672
  setOpenIdx(idx);
21938
- setSelectedFile(null);
21939
22673
  }
21940
22674
  },
21941
- className: `cursor-pointer rounded-lg border border-gray-400 bg-white overflow-hidden flex flex-col transition outline outline-2 outline-transparent hover:outline-gray-400/70 focus-visible:outline-blue-500/60 ${cardClass}`,
21942
- children: [
21943
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
21944
- "div",
21945
- {
21946
- "data-role": "card-header",
21947
- className: `rounded-t-lg px-4 py-2 border-b flex items-center justify-between gap-3 ${getHeaderClasses(status)}`,
21948
- children: [
21949
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-medium truncate", children: formatStepName(item, idx) }),
21950
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2", children: status === "running" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
21951
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-4 w-4", "aria-label": "Running", children: [
21952
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "sr-only", children: "Running" }),
21953
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute inset-0 rounded-full border-2 border-amber-200" }),
21954
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "absolute inset-0 rounded-full border-2 border-transparent border-t-amber-600 animate-spin" })
21955
- ] }),
21956
- item.stage && /* @__PURE__ */ jsxRuntimeExports.jsx(
21957
- "span",
21958
- {
21959
- className: "text-[11px] font-medium opacity-80 truncate",
21960
- title: item.stage,
21961
- children: formatStageLabel(item.stage)
21962
- }
21963
- )
21964
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[11px] uppercase tracking-wide opacity-80", children: [
21965
- status,
21966
- status === "failed" && item.stage && /* @__PURE__ */ jsxRuntimeExports.jsxs(
21967
- "span",
21968
- {
21969
- className: "text-[11px] font-medium opacity-80 truncate ml-2",
21970
- title: item.stage,
21971
- children: [
21972
- "(",
21973
- formatStageLabel(item.stage),
21974
- ")"
21975
- ]
21976
- }
21977
- )
21978
- ] }) })
21979
- ]
21980
- }
21981
- ),
21982
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4", children: [
21983
- item.subtitle && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-gray-600", children: item.subtitle }),
21984
- item.body && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-2 text-sm text-gray-700", children: item.body })
21985
- ] })
21986
- ]
22675
+ handleRestartClick,
22676
+ item
21987
22677
  },
21988
22678
  item.id ?? idx
21989
22679
  );
21990
22680
  })
21991
22681
  }
21992
22682
  ),
22683
+ openIdx !== -1 && /* @__PURE__ */ jsxRuntimeExports.jsx(
22684
+ TaskDetailSidebar,
22685
+ {
22686
+ open: openIdx !== -1,
22687
+ title: formatStepName(items[openIdx], openIdx),
22688
+ status: getStatus(openIdx),
22689
+ jobId,
22690
+ taskId: items[openIdx]?.id || `task-${openIdx}`,
22691
+ taskBody: items[openIdx]?.body || null,
22692
+ filesByTypeForItem,
22693
+ task: items[openIdx],
22694
+ taskIndex: openIdx,
22695
+ onClose: () => setOpenIdx(-1)
22696
+ }
22697
+ ),
21993
22698
  /* @__PURE__ */ jsxRuntimeExports.jsx(
21994
- "aside",
22699
+ RestartJobModal,
21995
22700
  {
21996
- role: "dialog",
21997
- "aria-modal": "true",
21998
- "aria-labelledby": `slide-over-title-${openIdx}`,
21999
- "aria-hidden": openIdx === null,
22000
- className: `fixed inset-y-0 right-0 z-[2000] w-full max-w-4xl bg-white border-l border-gray-200 transform transition-transform duration-300 ease-out ${openIdx !== null ? "translate-x-0" : "translate-x-full"}`,
22001
- children: openIdx !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
22002
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
22003
- "div",
22004
- {
22005
- className: `px-6 py-4 border-b flex items-center justify-between ${getHeaderClasses(getStatus(openIdx))}`,
22006
- children: [
22007
- /* @__PURE__ */ jsxRuntimeExports.jsx(
22008
- "div",
22009
- {
22010
- id: `slide-over-title-${openIdx}`,
22011
- className: "text-lg font-semibold truncate",
22012
- children: formatStepName(items[openIdx], openIdx)
22013
- }
22014
- ),
22015
- /* @__PURE__ */ jsxRuntimeExports.jsx(
22016
- "button",
22017
- {
22018
- ref: closeButtonRef,
22019
- type: "button",
22020
- "aria-label": "Close details",
22021
- onClick: () => {
22022
- setOpenIdx(null);
22023
- setSelectedFile(null);
22024
- },
22025
- className: "rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 px-3 py-1.5 text-base",
22026
- children: "×"
22027
- }
22028
- )
22029
- ]
22030
- }
22031
- ),
22032
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-8 overflow-y-auto h-full", children: [
22033
- items[openIdx]?.status === "failed" && items[openIdx]?.body && /* @__PURE__ */ jsxRuntimeExports.jsx("section", { "aria-label": "Error", children: /* @__PURE__ */ jsxRuntimeExports.jsx(n$2, { role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsxRuntimeExports.jsx(u$1, { className: "whitespace-pre-wrap break-words", children: items[openIdx].body }) }) }),
22034
- /* @__PURE__ */ jsxRuntimeExports.jsx("section", { className: "mt-6", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
22035
- /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-base font-semibold text-gray-900", children: "Files" }),
22036
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center space-x-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex rounded-lg border border-gray-200 bg-gray-50 p-1", children: [
22037
- /* @__PURE__ */ jsxRuntimeExports.jsx(
22038
- "button",
22039
- {
22040
- onClick: () => setFilePaneType("artifacts"),
22041
- className: `px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${filePaneType === "artifacts" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
22042
- children: "Artifacts"
22043
- }
22044
- ),
22045
- /* @__PURE__ */ jsxRuntimeExports.jsx(
22046
- "button",
22047
- {
22048
- onClick: () => setFilePaneType("logs"),
22049
- className: `px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${filePaneType === "logs" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
22050
- children: "Logs"
22051
- }
22052
- ),
22053
- /* @__PURE__ */ jsxRuntimeExports.jsx(
22054
- "button",
22055
- {
22056
- onClick: () => setFilePaneType("tmp"),
22057
- className: `px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${filePaneType === "tmp" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
22058
- children: "Temp"
22059
- }
22060
- )
22061
- ] }) })
22062
- ] }) }),
22063
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
22064
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm text-gray-600", children: [
22065
- filePaneType.charAt(0).toUpperCase() + filePaneType.slice(1),
22066
- " ",
22067
- "files for ",
22068
- items[openIdx]?.id || `Task ${openIdx + 1}`
22069
- ] }),
22070
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1", children: (() => {
22071
- const filesForStep = filesByTypeForItem(items[openIdx]);
22072
- const filesForTab = filesForStep[filePaneType] ?? [];
22073
- if (filesForTab.length === 0) {
22074
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm text-gray-500 italic py-4 text-center", children: [
22075
- "No ",
22076
- filePaneType,
22077
- " files available for this task"
22078
- ] });
22079
- }
22080
- return filesForTab.map((name) => {
22081
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
22082
- "div",
22083
- {
22084
- className: "flex items-center justify-between p-2 rounded border border-gray-200 hover:border-gray-300 hover:bg-gray-50 cursor-pointer transition-colors",
22085
- onClick: () => {
22086
- setFilePaneFilename(name);
22087
- setFilePaneOpen(true);
22088
- },
22089
- children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center space-x-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: name }) })
22090
- },
22091
- `${filePaneType}-${name}`
22092
- );
22093
- });
22094
- })() })
22095
- ] }),
22096
- /* @__PURE__ */ jsxRuntimeExports.jsx(
22097
- TaskFilePane,
22098
- {
22099
- isOpen: filePaneOpen,
22100
- jobId,
22101
- taskId: items[openIdx]?.id || `task-${openIdx}`,
22102
- type: filePaneType,
22103
- filename: filePaneFilename,
22104
- onClose: () => {
22105
- setFilePaneOpen(false);
22106
- setFilePaneFilename(null);
22107
- }
22108
- }
22109
- )
22110
- ] })
22111
- ] })
22701
+ open: restartModalOpen,
22702
+ onClose: handleRestartCancel,
22703
+ onConfirm: handleRestartConfirm,
22704
+ jobId,
22705
+ taskId: restartTaskId,
22706
+ isSubmitting
22112
22707
  }
22113
22708
  )
22114
22709
  ] });
@@ -22149,7 +22744,7 @@ function computeDagItems(job, pipeline) {
22149
22744
  const jobTask = jobTasks[taskId];
22150
22745
  return {
22151
22746
  id: taskId,
22152
- status: jobTask ? jobTask.state : "pending",
22747
+ status: jobTask ? jobTask.state : TaskState.PENDING,
22153
22748
  source: "pipeline",
22154
22749
  stage: computeTaskStage(job, taskId)
22155
22750
  };
@@ -22172,14 +22767,16 @@ function computeDagItems(job, pipeline) {
22172
22767
  function computeActiveIndex(items) {
22173
22768
  if (!items || items.length === 0) return 0;
22174
22769
  const firstRunningIndex = items.findIndex(
22175
- (item) => item.status === "running"
22770
+ (item) => item.status === TaskState.RUNNING
22176
22771
  );
22177
22772
  if (firstRunningIndex !== -1) return firstRunningIndex;
22178
- const firstFailedIndex = items.findIndex((item) => item.status === "failed");
22773
+ const firstFailedIndex = items.findIndex(
22774
+ (item) => item.status === TaskState.FAILED
22775
+ );
22179
22776
  if (firstFailedIndex !== -1) return firstFailedIndex;
22180
22777
  let lastDoneIndex = -1;
22181
22778
  items.forEach((item, index2) => {
22182
- if (item.status === "done") lastDoneIndex = index2;
22779
+ if (item.status === TaskState.DONE) lastDoneIndex = index2;
22183
22780
  });
22184
22781
  if (lastDoneIndex !== -1) return lastDoneIndex;
22185
22782
  return 0;
@@ -22198,11 +22795,7 @@ function formatTokensCompact(n2) {
22198
22795
  }
22199
22796
  return `${n2} tokens`;
22200
22797
  }
22201
- function JobDetail({ job, pipeline, onClose, onResume }) {
22202
- const now = useTicker(1e3);
22203
- const [resumeFrom, setResumeFrom] = reactExports.useState(
22204
- pipeline?.tasks?.[0] ? typeof pipeline.tasks[0] === "string" ? pipeline.tasks[0] : pipeline.tasks[0].id ?? pipeline.tasks[0].name ?? "" : ""
22205
- );
22798
+ function JobDetail({ job, pipeline }) {
22206
22799
  const taskById = React.useMemo(() => {
22207
22800
  const tasks = job?.tasks;
22208
22801
  let result;
@@ -22222,11 +22815,6 @@ function JobDetail({ job, pipeline, onClose, onResume }) {
22222
22815
  }
22223
22816
  return result;
22224
22817
  }, [job?.tasks]);
22225
- reactExports.useEffect(() => {
22226
- setResumeFrom(
22227
- pipeline?.tasks?.[0] ? typeof pipeline.tasks[0] === "string" ? pipeline.tasks[0] : pipeline.tasks[0].id ?? pipeline.tasks[0].name ?? "" : ""
22228
- );
22229
- }, [job.id, pipeline?.tasks?.length]);
22230
22818
  const computedPipeline = React.useMemo(() => {
22231
22819
  let result;
22232
22820
  if (pipeline?.tasks) {
@@ -22242,18 +22830,9 @@ function JobDetail({ job, pipeline, onClose, onResume }) {
22242
22830
  }
22243
22831
  return result;
22244
22832
  }, [pipeline, job?.tasks]);
22245
- const dagItems = React.useMemo(() => {
22833
+ const stableDagItems = React.useMemo(() => {
22246
22834
  const rawDagItems = computeDagItems(job, computedPipeline);
22247
- const processedItems = rawDagItems.map((item, index2) => {
22248
- {
22249
- console.debug("[JobDetail] computed DAG item", {
22250
- id: item.id,
22251
- status: item.status,
22252
- stage: item.stage,
22253
- jobHasTasks: !!job?.tasks,
22254
- taskKeys: job?.tasks ? Object.keys(job.tasks) : null
22255
- });
22256
- }
22835
+ return rawDagItems.map((item, index2) => {
22257
22836
  const task = taskById[item.id];
22258
22837
  const taskConfig = task?.config || {};
22259
22838
  const subtitleParts = [];
@@ -22263,18 +22842,9 @@ function JobDetail({ job, pipeline, onClose, onResume }) {
22263
22842
  if (taskConfig?.temperature != null) {
22264
22843
  subtitleParts.push(`temp ${taskConfig.temperature}`);
22265
22844
  }
22266
- if (task?.attempts != null) {
22267
- subtitleParts.push(`${task.attempts} attempts`);
22268
- }
22269
22845
  if (task?.refinementAttempts != null) {
22270
22846
  subtitleParts.push(`${task.refinementAttempts} refinements`);
22271
22847
  }
22272
- if (task?.startedAt) {
22273
- const durationMs = taskDisplayDurationMs(task, now);
22274
- if (durationMs > 0) {
22275
- subtitleParts.push(fmtDuration(durationMs));
22276
- }
22277
- }
22278
22848
  const taskBreakdown = job?.costs?.taskBreakdown?.[item.id]?.summary || {};
22279
22849
  if (taskBreakdown.totalTokens > 0) {
22280
22850
  subtitleParts.push(formatTokensCompact(taskBreakdown.totalTokens));
@@ -22284,16 +22854,29 @@ function JobDetail({ job, pipeline, onClose, onResume }) {
22284
22854
  }
22285
22855
  const errorMsg = task?.error?.message;
22286
22856
  const body = (item.status === "failed" || item.status === "error") && errorMsg ? errorMsg : null;
22287
- const resultItem = {
22857
+ return {
22288
22858
  ...item,
22289
22859
  title: typeof item.id === "string" ? item.id : item.id?.name || item.id?.id || `Task ${item.id}`,
22290
22860
  subtitle: subtitleParts.length > 0 ? subtitleParts.join(" · ") : null,
22291
- body
22861
+ body,
22862
+ startedAt: task?.startedAt,
22863
+ endedAt: task?.endedAt
22292
22864
  };
22293
- return resultItem;
22294
22865
  });
22295
- return processedItems;
22296
- }, [job, computedPipeline, taskById, now]);
22866
+ }, [job, computedPipeline, taskById]);
22867
+ const prevDagItemsRef = React.useRef([]);
22868
+ const dagItems = React.useMemo(() => {
22869
+ const prevItems = prevDagItemsRef.current;
22870
+ const newItems = stableDagItems.map((item, index2) => {
22871
+ const prevItem = prevItems[index2];
22872
+ if (prevItem && prevItem.id === item.id && prevItem.status === item.status && prevItem.stage === item.stage && prevItem.title === item.title && prevItem.subtitle === item.subtitle && prevItem.body === item.body) {
22873
+ return prevItem;
22874
+ }
22875
+ return item;
22876
+ });
22877
+ prevDagItemsRef.current = newItems;
22878
+ return newItems;
22879
+ }, [stableDagItems]);
22297
22880
  const activeIndex = React.useMemo(() => {
22298
22881
  const index2 = computeActiveIndex(dagItems);
22299
22882
  return index2;
@@ -22305,7 +22888,6 @@ function JobDetail({ job, pipeline, onClose, onResume }) {
22305
22888
  },
22306
22889
  [job]
22307
22890
  );
22308
- console.log("dagItems", dagItems);
22309
22891
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-full flex-col", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
22310
22892
  DAGGrid,
22311
22893
  {
@@ -22317,40 +22899,6 @@ function JobDetail({ job, pipeline, onClose, onResume }) {
22317
22899
  ) });
22318
22900
  }
22319
22901
  const REFRESH_DEBOUNCE_MS = 200;
22320
- const createHookLogger = (jobId) => {
22321
- const prefix = `[useJobDetailWithUpdates:${jobId || "unknown"}]`;
22322
- return {
22323
- log: (message, data = null) => {
22324
- console.log(`${prefix} ${message}`, data ? data : "");
22325
- },
22326
- warn: (message, data = null) => {
22327
- console.warn(`${prefix} ${message}`, data ? data : "");
22328
- },
22329
- error: (message, data = null) => {
22330
- console.error(`${prefix} ${message}`, data ? data : "");
22331
- },
22332
- group: (label) => console.group(`${prefix} ${label}`),
22333
- groupEnd: () => console.groupEnd(),
22334
- table: (data, title) => {
22335
- console.log(`${prefix} ${title}:`);
22336
- console.table(data);
22337
- },
22338
- sse: (eventType, eventData) => {
22339
- console.log(
22340
- `%c${prefix} SSE Event: ${eventType}`,
22341
- "color: #0066cc; font-weight: bold;",
22342
- eventData
22343
- );
22344
- },
22345
- state: (stateName, value) => {
22346
- console.log(
22347
- `%c${prefix} State Change: ${stateName}`,
22348
- "color: #006600; font-weight: bold;",
22349
- value
22350
- );
22351
- }
22352
- };
22353
- };
22354
22902
  async function fetchJobDetail(jobId, { signal } = {}) {
22355
22903
  const response = await fetch(`/api/jobs/${jobId}`, { signal });
22356
22904
  if (!response.ok) {
@@ -22435,89 +22983,63 @@ function matchesJobTasksStatusPath(path, jobId) {
22435
22983
  }
22436
22984
  }
22437
22985
  function useJobDetailWithUpdates(jobId) {
22438
- const logger = reactExports.useMemo(() => createHookLogger(jobId), [jobId]);
22439
22986
  const [data, setData] = reactExports.useState(null);
22440
22987
  const [loading, setLoading] = reactExports.useState(true);
22441
22988
  const [error, setError] = reactExports.useState(null);
22442
22989
  const [connectionStatus, setConnectionStatus] = reactExports.useState("disconnected");
22990
+ const [isRefreshing, setIsRefreshing] = reactExports.useState(false);
22991
+ const [isPending, startTransition] = reactExports.useTransition();
22992
+ const [isHydrated, setIsHydrated] = reactExports.useState(false);
22443
22993
  const esRef = reactExports.useRef(null);
22444
22994
  const reconnectTimer = reactExports.useRef(null);
22445
22995
  const hydratedRef = reactExports.useRef(false);
22446
22996
  const eventQueue = reactExports.useRef([]);
22447
22997
  const mountedRef = reactExports.useRef(true);
22448
22998
  const refetchTimerRef = reactExports.useRef(null);
22449
- reactExports.useEffect(() => {
22450
- logger.group("Hook Initialization");
22451
- logger.log("Job ID:", jobId);
22452
- logger.log("Initial state:", { data, loading, error, connectionStatus });
22453
- logger.groupEnd();
22454
- }, [jobId, logger]);
22455
- reactExports.useEffect(() => {
22456
- logger.state("data", data);
22457
- }, [data, logger]);
22458
- reactExports.useEffect(() => {
22459
- logger.state("loading", loading);
22460
- }, [loading, logger]);
22461
- reactExports.useEffect(() => {
22462
- logger.state("error", error);
22463
- }, [error, logger]);
22464
- reactExports.useEffect(() => {
22465
- logger.state("connectionStatus", connectionStatus);
22466
- }, [connectionStatus, logger]);
22467
22999
  const scheduleDebouncedRefetch = reactExports.useCallback(
22468
23000
  (context = {}) => {
22469
- logger.group("Debounced Refetch Request");
22470
- logger.log("Request context:", context);
22471
- logger.log("Scheduling debounced refetch");
22472
23001
  if (refetchTimerRef.current) {
22473
- logger.log("Clearing existing refetch timer");
22474
23002
  clearTimeout(refetchTimerRef.current);
22475
23003
  }
22476
23004
  refetchTimerRef.current = setTimeout(async () => {
22477
23005
  if (!mountedRef.current || !hydratedRef.current) {
22478
- logger.warn(
22479
- "Refetch aborted - component not mounted or not hydrated",
22480
- { mounted: mountedRef.current, hydrated: hydratedRef.current }
22481
- );
22482
- logger.groupEnd();
22483
23006
  return;
22484
23007
  }
22485
- logger.log("Executing debounced refetch");
22486
- logger.log("Refetch jobId:", jobId);
22487
23008
  const abortController = new AbortController();
22488
23009
  try {
23010
+ startTransition(() => {
23011
+ setIsRefreshing(true);
23012
+ });
22489
23013
  const jobData = await fetchJobDetail(jobId, {
22490
23014
  signal: abortController.signal
22491
23015
  });
22492
- logger.log("Refetch response received");
22493
- logger.log("Refetch job data preview:", {
22494
- status: jobData?.status,
22495
- hasTasks: !!jobData?.tasks,
22496
- taskKeys: jobData?.tasks ? Object.keys(jobData.tasks) : []
22497
- });
22498
23016
  if (mountedRef.current) {
22499
- logger.log("Refetch successful, updating data");
22500
- setData(jobData);
22501
- setError(null);
22502
- } else {
22503
- logger.warn("Refetch completed but component is unmounted");
23017
+ startTransition(() => {
23018
+ setData(jobData);
23019
+ setError(null);
23020
+ setIsRefreshing(false);
23021
+ });
22504
23022
  }
22505
23023
  } catch (err) {
22506
- logger.error("Failed to refetch job detail:", err);
22507
- if (mountedRef.current) setError(err.message);
23024
+ if (mountedRef.current) {
23025
+ startTransition(() => {
23026
+ setError(err.message);
23027
+ setIsRefreshing(false);
23028
+ });
23029
+ }
22508
23030
  } finally {
22509
23031
  refetchTimerRef.current = null;
22510
- logger.groupEnd();
22511
23032
  }
22512
23033
  }, REFRESH_DEBOUNCE_MS);
22513
23034
  },
22514
- [jobId, logger]
23035
+ [jobId]
22515
23036
  );
22516
23037
  reactExports.useEffect(() => {
22517
23038
  setData(null);
22518
23039
  setLoading(true);
22519
23040
  setError(null);
22520
23041
  setConnectionStatus("disconnected");
23042
+ setIsHydrated(false);
22521
23043
  hydratedRef.current = false;
22522
23044
  eventQueue.current = [];
22523
23045
  if (refetchTimerRef.current) {
@@ -22528,92 +23050,80 @@ function useJobDetailWithUpdates(jobId) {
22528
23050
  reactExports.useEffect(() => {
22529
23051
  if (!jobId || !mountedRef.current) return;
22530
23052
  const doFetch = async () => {
22531
- logger.group("Initial Data Fetch");
22532
23053
  try {
22533
- setLoading(true);
23054
+ if (!hydratedRef.current) {
23055
+ setLoading(true);
23056
+ }
22534
23057
  setError(null);
22535
- logger.log("Starting initial job data fetch");
22536
23058
  const jobData = await fetchJobDetail(jobId);
22537
- logger.log("Initial fetch successful", jobData);
22538
23059
  let finalData = jobData;
22539
23060
  let queuedNeedsRefetch = false;
22540
23061
  if (eventQueue.current.length > 0) {
22541
- logger.log(`Processing ${eventQueue.current.length} queued events`);
22542
23062
  for (const ev of eventQueue.current) {
22543
- logger.log("Processing queued event:", ev);
22544
23063
  if (ev.type === "state:change") {
22545
23064
  const d2 = ev.payload && (ev.payload.data || ev.payload) || {};
22546
23065
  if (typeof d2.path === "string" && matchesJobTasksStatusPath(d2.path, jobId)) {
22547
- logger.log(
22548
- "Queued state:change matches tasks-status path, scheduling refetch"
22549
- );
22550
23066
  queuedNeedsRefetch = true;
22551
23067
  continue;
22552
23068
  }
22553
23069
  }
22554
23070
  finalData = applyJobEvent(finalData, ev, jobId);
22555
- logger.log("Applied queued event, result:", finalData);
22556
23071
  }
22557
23072
  eventQueue.current = [];
22558
23073
  }
22559
23074
  if (mountedRef.current) {
22560
- logger.log("Updating state with final data");
22561
- setData(finalData);
22562
- setError(null);
22563
- hydratedRef.current = true;
22564
- logger.log("Component hydrated");
23075
+ startTransition(() => {
23076
+ setData(finalData);
23077
+ setError(null);
23078
+ const wasHydrated = hydratedRef.current;
23079
+ hydratedRef.current = true;
23080
+ if (!wasHydrated) {
23081
+ setIsHydrated(true);
23082
+ setLoading(false);
23083
+ }
23084
+ });
22565
23085
  if (queuedNeedsRefetch) {
22566
- logger.log("Scheduling refetch for queued path changes");
22567
23086
  scheduleDebouncedRefetch();
22568
23087
  }
22569
23088
  }
22570
23089
  } catch (err) {
22571
- logger.error("Failed to fetch job detail:", err);
22572
23090
  if (mountedRef.current) {
22573
- setError(err.message);
22574
- setData(null);
23091
+ startTransition(() => {
23092
+ setError(err.message);
23093
+ setData(null);
23094
+ if (!hydratedRef.current) {
23095
+ setLoading(false);
23096
+ }
23097
+ });
22575
23098
  }
22576
23099
  } finally {
22577
- if (mountedRef.current) {
23100
+ if (mountedRef.current && !hydratedRef.current) {
22578
23101
  setLoading(false);
22579
23102
  }
22580
- logger.groupEnd();
22581
23103
  }
22582
23104
  };
22583
23105
  doFetch();
22584
- }, [jobId, scheduleDebouncedRefetch, logger]);
23106
+ }, [jobId, scheduleDebouncedRefetch]);
22585
23107
  reactExports.useEffect(() => {
22586
23108
  if (!jobId) {
22587
- logger.log("SSE setup skipped - no jobId available", {
22588
- hasJobId: !!jobId,
22589
- hasExistingEs: !!esRef.current,
22590
- isMounted: mountedRef.current
22591
- });
22592
23109
  return void 0;
22593
23110
  }
22594
23111
  if (esRef.current) {
22595
- logger.log("Closing existing EventSource before reinitializing");
22596
23112
  try {
22597
23113
  esRef.current.close();
22598
23114
  } catch (err) {
22599
- logger.warn("Error closing existing EventSource during reinit", err);
22600
23115
  }
22601
23116
  esRef.current = null;
22602
23117
  }
22603
- logger.group("SSE Connection Setup");
22604
- logger.log("Setting up SSE connection for job:", jobId);
22605
23118
  const attachListeners = (es) => {
22606
23119
  const onOpen = () => {
22607
- logger.log("SSE connection opened");
22608
23120
  if (mountedRef.current) {
22609
23121
  setConnectionStatus("connected");
22610
23122
  }
22611
23123
  };
22612
23124
  const onError = () => {
22613
- logger.warn("SSE connection error");
22614
23125
  try {
22615
23126
  const rs = esRef.current?.readyState;
22616
- logger.log("SSE readyState:", rs);
22617
23127
  if (rs === 0) {
22618
23128
  if (mountedRef.current) setConnectionStatus("disconnected");
22619
23129
  } else if (rs === 1) {
@@ -22624,11 +23134,9 @@ function useJobDetailWithUpdates(jobId) {
22624
23134
  if (mountedRef.current) setConnectionStatus("disconnected");
22625
23135
  }
22626
23136
  } catch (err) {
22627
- logger.error("Error getting readyState:", err);
22628
23137
  if (mountedRef.current) setConnectionStatus("disconnected");
22629
23138
  }
22630
23139
  if (esRef.current && esRef.current.readyState === 2 && mountedRef.current) {
22631
- logger.log("Scheduling SSE reconnection");
22632
23140
  if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
22633
23141
  reconnectTimer.current = setTimeout(() => {
22634
23142
  if (!mountedRef.current) return;
@@ -22638,7 +23146,6 @@ function useJobDetailWithUpdates(jobId) {
22638
23146
  } catch (e2) {
22639
23147
  }
22640
23148
  const eventsUrl = jobId ? `/api/events?jobId=${encodeURIComponent(jobId)}` : "/api/events";
22641
- logger.log("Creating new EventSource for reconnection");
22642
23149
  const newEs = new EventSource(eventsUrl);
22643
23150
  newEs.addEventListener("open", onOpen);
22644
23151
  newEs.addEventListener("job:updated", onJobUpdated);
@@ -22649,7 +23156,7 @@ function useJobDetailWithUpdates(jobId) {
22649
23156
  newEs.addEventListener("error", onError);
22650
23157
  esRef.current = newEs;
22651
23158
  } catch (err) {
22652
- logger.error("Failed to reconnect SSE:", err);
23159
+ console.error("Failed to reconnect SSE:", err);
22653
23160
  }
22654
23161
  }, 2e3);
22655
23162
  }
@@ -22658,64 +23165,37 @@ function useJobDetailWithUpdates(jobId) {
22658
23165
  try {
22659
23166
  const payload = evt && evt.data ? JSON.parse(evt.data) : null;
22660
23167
  const eventObj = { type, payload };
22661
- logger.sse(type, payload);
22662
23168
  if (payload && payload.jobId && payload.jobId !== jobId) {
22663
- logger.log(
22664
- `Ignoring event for different job: ${payload.jobId} (current: ${jobId})`
22665
- );
22666
23169
  return;
22667
23170
  }
22668
23171
  if (!hydratedRef.current) {
22669
- logger.log(`Queueing event until hydration: ${type}`);
22670
23172
  eventQueue.current = (eventQueue.current || []).concat(eventObj);
22671
23173
  return;
22672
23174
  }
22673
23175
  if (type === "state:change") {
22674
23176
  const d2 = payload && (payload.data || payload) || {};
22675
- logger.log("Processing state:change event:", d2);
22676
23177
  if (typeof d2.path === "string" && matchesJobTasksStatusPath(d2.path, jobId)) {
22677
- logger.log(
22678
- `state:change matches tasks-status path: ${d2.path}, scheduling refetch`
22679
- );
22680
23178
  scheduleDebouncedRefetch({
22681
23179
  reason: "state:change",
22682
23180
  path: d2.path
22683
23181
  });
22684
23182
  return;
22685
- } else {
22686
- logger.log(
22687
- `state:change does not match tasks-status path: ${d2.path}`
22688
- );
22689
23183
  }
22690
23184
  }
22691
- setData((prev) => {
22692
- logger.group("Applying SSE event to state");
22693
- logger.log("Previous state snapshot:", {
22694
- hasTasks: !!prev?.tasks,
22695
- taskKeys: prev?.tasks ? Object.keys(prev.tasks) : [],
22696
- status: prev?.status
22697
- });
22698
- logger.log("Incoming event payload:", payload);
22699
- const next = applyJobEvent(prev, eventObj, jobId);
22700
- try {
22701
- if (JSON.stringify(prev) === JSON.stringify(next)) {
22702
- logger.log("Event application resulted in no state change");
22703
- logger.groupEnd();
22704
- return prev;
23185
+ startTransition(() => {
23186
+ setData((prev) => {
23187
+ const next = applyJobEvent(prev, eventObj, jobId);
23188
+ try {
23189
+ if (JSON.stringify(prev) === JSON.stringify(next)) {
23190
+ return prev;
23191
+ }
23192
+ } catch (e2) {
23193
+ console.error("Error comparing states:", e2);
22705
23194
  }
22706
- } catch (e2) {
22707
- logger.error("Error comparing states:", e2);
22708
- }
22709
- logger.log("Event applied, state updated", {
22710
- hasTasks: !!next?.tasks,
22711
- taskKeys: next?.tasks ? Object.keys(next.tasks) : [],
22712
- status: next?.status
23195
+ return next;
22713
23196
  });
22714
- logger.groupEnd();
22715
- return next;
22716
23197
  });
22717
23198
  } catch (err) {
22718
- logger.error("Failed to handle SSE event:", err);
22719
23199
  console.error("Failed to handle SSE event:", err);
22720
23200
  }
22721
23201
  };
@@ -22724,7 +23204,6 @@ function useJobDetailWithUpdates(jobId) {
22724
23204
  const onJobRemoved = (evt) => handleIncomingEvent("job:removed", evt);
22725
23205
  const onStatusChanged = (evt) => handleIncomingEvent("status:changed", evt);
22726
23206
  const onStateChange = (evt) => handleIncomingEvent("state:change", evt);
22727
- logger.log("Attaching SSE event listeners");
22728
23207
  es.addEventListener("open", onOpen);
22729
23208
  es.addEventListener("job:updated", onJobUpdated);
22730
23209
  es.addEventListener("job:created", onJobCreated);
@@ -22733,14 +23212,11 @@ function useJobDetailWithUpdates(jobId) {
22733
23212
  es.addEventListener("state:change", onStateChange);
22734
23213
  es.addEventListener("error", onError);
22735
23214
  if (es.readyState === 1 && mountedRef.current) {
22736
- logger.log("SSE already open, setting connected");
22737
23215
  setConnectionStatus("connected");
22738
23216
  } else if (es.readyState === 0 && mountedRef.current) {
22739
- logger.log("SSE connecting, setting disconnected");
22740
23217
  setConnectionStatus("disconnected");
22741
23218
  }
22742
23219
  return () => {
22743
- logger.log("Cleaning up SSE connection");
22744
23220
  try {
22745
23221
  es.removeEventListener("open", onOpen);
22746
23222
  es.removeEventListener("job:updated", onJobUpdated);
@@ -22750,9 +23226,8 @@ function useJobDetailWithUpdates(jobId) {
22750
23226
  es.removeEventListener("state:change", onStateChange);
22751
23227
  es.removeEventListener("error", onError);
22752
23228
  es.close();
22753
- logger.log("SSE connection closed");
22754
23229
  } catch (err) {
22755
- logger.error("Error during SSE cleanup:", err);
23230
+ console.error("Error during SSE cleanup:", err);
22756
23231
  }
22757
23232
  if (reconnectTimer.current) {
22758
23233
  clearTimeout(reconnectTimer.current);
@@ -22763,21 +23238,18 @@ function useJobDetailWithUpdates(jobId) {
22763
23238
  };
22764
23239
  try {
22765
23240
  const eventsUrl = jobId ? `/api/events?jobId=${encodeURIComponent(jobId)}` : "/api/events";
22766
- logger.log(`Creating EventSource with URL: ${eventsUrl}`);
22767
23241
  const es = new EventSource(eventsUrl);
22768
23242
  esRef.current = es;
22769
23243
  const cleanup = attachListeners(es);
22770
- logger.groupEnd();
22771
23244
  return cleanup;
22772
23245
  } catch (err) {
22773
- logger.error("Failed to create SSE connection:", err);
23246
+ console.error("Failed to create SSE connection:", err);
22774
23247
  if (mountedRef.current) {
22775
23248
  setConnectionStatus("error");
22776
23249
  }
22777
- logger.groupEnd();
22778
23250
  return void 0;
22779
23251
  }
22780
- }, [jobId, scheduleDebouncedRefetch, logger]);
23252
+ }, [jobId, scheduleDebouncedRefetch]);
22781
23253
  reactExports.useEffect(() => {
22782
23254
  mountedRef.current = true;
22783
23255
  return () => {
@@ -22803,7 +23275,10 @@ function useJobDetailWithUpdates(jobId) {
22803
23275
  data,
22804
23276
  loading,
22805
23277
  error,
22806
- connectionStatus
23278
+ connectionStatus,
23279
+ isRefreshing,
23280
+ isTransitioning: isPending,
23281
+ isHydrated
22807
23282
  };
22808
23283
  }
22809
23284
  function PipelineDetail() {
@@ -22813,26 +23288,30 @@ function PipelineDetail() {
22813
23288
  Layout,
22814
23289
  {
22815
23290
  pageTitle: "Pipeline Details",
22816
- breadcrumbs: [
22817
- { label: "Home", href: "/" },
22818
- { label: "Pipeline Details" }
22819
- ],
22820
- showBackButton: true,
23291
+ breadcrumbs: [{ label: "Home", href: "/" }],
22821
23292
  children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$4, { align: "center", justify: "center", className: "min-h-64", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$7, { className: "text-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "5", weight: "medium", color: "red", className: "mb-2", children: "No job ID provided" }) }) })
22822
23293
  }
22823
23294
  );
22824
23295
  }
22825
- const { data: job, loading, error } = useJobDetailWithUpdates(jobId);
22826
- if (loading) {
23296
+ const {
23297
+ data: job,
23298
+ loading,
23299
+ error,
23300
+ isRefreshing,
23301
+ isHydrated
23302
+ } = useJobDetailWithUpdates(jobId);
23303
+ const showLoadingScreen = loading && !isHydrated;
23304
+ if (showLoadingScreen) {
22827
23305
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
22828
23306
  Layout,
22829
23307
  {
22830
23308
  pageTitle: "Pipeline Details",
22831
23309
  breadcrumbs: [
22832
23310
  { label: "Home", href: "/" },
22833
- { label: "Pipeline Details" }
23311
+ {
23312
+ label: job && job?.pipelineLabel ? job.pipelineLabel : "Pipeline Details"
23313
+ }
22834
23314
  ],
22835
- showBackButton: true,
22836
23315
  children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$4, { align: "center", justify: "center", className: "min-h-64", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$7, { className: "text-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "5", weight: "medium", className: "mb-2", children: "Loading job details..." }) }) })
22837
23316
  }
22838
23317
  );
@@ -22844,9 +23323,10 @@ function PipelineDetail() {
22844
23323
  pageTitle: "Pipeline Details",
22845
23324
  breadcrumbs: [
22846
23325
  { label: "Home", href: "/" },
22847
- { label: "Pipeline Details" }
23326
+ {
23327
+ label: job && job?.pipelineLabel ? job.pipelineLabel : "Pipeline Details"
23328
+ }
22848
23329
  ],
22849
- showBackButton: true,
22850
23330
  children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$4, { align: "center", justify: "center", className: "min-h-64", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(p$7, { className: "text-center", children: [
22851
23331
  /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "5", weight: "medium", color: "red", className: "mb-2", children: "Failed to load job details" }),
22852
23332
  /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "2", color: "gray", className: "mt-2", children: error })
@@ -22861,9 +23341,8 @@ function PipelineDetail() {
22861
23341
  pageTitle: "Pipeline Details",
22862
23342
  breadcrumbs: [
22863
23343
  { label: "Home", href: "/" },
22864
- { label: "Pipeline Details" }
23344
+ { label: job.pipelineLabel || "Pipeline Details" }
22865
23345
  ],
22866
- showBackButton: true,
22867
23346
  children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$4, { align: "center", justify: "center", className: "min-h-64", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$7, { className: "text-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "5", weight: "medium", className: "mb-2", children: "Job not found" }) }) })
22868
23347
  }
22869
23348
  );
@@ -22881,28 +23360,64 @@ function PipelineDetail() {
22881
23360
  const pageTitle = job.name || "Pipeline Details";
22882
23361
  const breadcrumbs = [
22883
23362
  { label: "Home", href: "/" },
22884
- { label: "Pipeline Details" },
23363
+ {
23364
+ label: job && job?.pipelineLabel ? job.pipelineLabel : "Pipeline Details"
23365
+ },
22885
23366
  ...job.name ? [{ label: job.name }] : []
22886
23367
  ];
22887
- const subheaderRightContent = /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { align: "center", gap: "3", className: "shrink-0", children: [
23368
+ const totalCost = job?.totalCost || job?.costs?.summary?.totalCost || 0;
23369
+ const totalTokens = job?.totalTokens || job?.costs?.summary?.totalTokens || 0;
23370
+ const totalInputTokens = job?.costs?.summary?.totalInputTokens || 0;
23371
+ const totalOutputTokens = job?.costs?.summary?.totalOutputTokens || 0;
23372
+ const costIndicator = /* @__PURE__ */ jsxRuntimeExports.jsxs(p$b, { size: "2", color: "gray", children: [
23373
+ "Cost: ",
23374
+ totalCost > 0 ? formatCurrency4(totalCost) : "—"
23375
+ ] });
23376
+ const costIndicatorWithTooltip = totalCost > 0 && totalTokens > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx(Provider, { delayDuration: 100, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Root3, { children: [
23377
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
23378
+ p$7,
23379
+ {
23380
+ className: "cursor-help border-b border-dotted border-gray-400 hover:border-gray-600 transition-colors",
23381
+ "aria-label": `Total cost: ${formatCurrency4(totalCost)}, ${formatTokensCompact(totalTokens)}`,
23382
+ children: costIndicator
23383
+ }
23384
+ ) }),
23385
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Portal, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
23386
+ Content2,
23387
+ {
23388
+ className: "bg-gray-900 text-white px-2 py-1 rounded text-xs max-w-xs",
23389
+ sideOffset: 5,
23390
+ children: [
23391
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
23392
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold", children: formatTokensCompact(totalTokens) }),
23393
+ totalInputTokens > 0 && totalOutputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-gray-300", children: [
23394
+ "Input: ",
23395
+ formatTokensCompact(totalInputTokens),
23396
+ " • Output:",
23397
+ " ",
23398
+ formatTokensCompact(totalOutputTokens)
23399
+ ] })
23400
+ ] }),
23401
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Arrow2, { className: "fill-gray-900" })
23402
+ ]
23403
+ }
23404
+ ) })
23405
+ ] }) }) : costIndicator;
23406
+ const subheaderRightContent = /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { align: "center", gap: "3", className: "shrink-0 flex-wrap", children: [
22888
23407
  /* @__PURE__ */ jsxRuntimeExports.jsxs(p$b, { size: "2", color: "gray", children: [
22889
23408
  "ID: ",
22890
23409
  job.id || jobId
22891
23410
  ] }),
23411
+ costIndicatorWithTooltip,
22892
23412
  statusBadge(job.status)
22893
23413
  ] });
22894
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
22895
- Layout,
22896
- {
22897
- pageTitle,
22898
- breadcrumbs,
22899
- showBackButton: true,
22900
- children: [
22901
- /* @__PURE__ */ jsxRuntimeExports.jsx(PageSubheader, { breadcrumbs, maxWidth: "max-w-7xl", children: subheaderRightContent }),
22902
- /* @__PURE__ */ jsxRuntimeExports.jsx(JobDetail, { job, pipeline })
22903
- ]
22904
- }
22905
- );
23414
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Layout, { pageTitle, breadcrumbs, children: [
23415
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(PageSubheader, { breadcrumbs, maxWidth: "max-w-7xl", children: [
23416
+ subheaderRightContent,
23417
+ isRefreshing && /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { size: "2", color: "blue", className: "ml-3 animate-pulse", children: "Refreshing..." })
23418
+ ] }),
23419
+ /* @__PURE__ */ jsxRuntimeExports.jsx(JobDetail, { job, pipeline })
23420
+ ] });
22906
23421
  }
22907
23422
  const ioFunctions = [
22908
23423
  {
@@ -23052,8 +23567,6 @@ function CodePage() {
23052
23567
  topP?: number,
23053
23568
  frequencyPenalty?: number,
23054
23569
  presencePenalty?: number,
23055
- tools?: Array<{type: "function", function: object}>,
23056
- toolChoice?: "auto" | "required" | { type: "function", function: { name: string } },
23057
23570
  seed?: number,
23058
23571
  provider?: string,
23059
23572
  model?: string,
@@ -23075,6 +23588,43 @@ function CodePage() {
23075
23588
  ] }, fn.fullPath))
23076
23589
  ) })
23077
23590
  ] }) }),
23591
+ /* @__PURE__ */ jsxRuntimeExports.jsx(r$8, { size: "6", mt: "8", mb: "4", children: "Validation API" }),
23592
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { as: "p", mb: "3", size: "2", children: "Schema validation helper available to task stages via validators." }),
23593
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(p$7, { mb: "4", children: [
23594
+ /* @__PURE__ */ jsxRuntimeExports.jsx(r$8, { size: "4", mb: "2", children: "Function Signature" }),
23595
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: `validateWithSchema(schema: object, data: object | string): { valid: true } | { valid: false, errors: AjvError[] }` }),
23596
+ /* @__PURE__ */ jsxRuntimeExports.jsx(r$8, { size: "4", mb: "2", mt: "4", children: "Behavior" }),
23597
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "list-disc list-inside mb-4 space-y-1", children: [
23598
+ /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "text-sm text-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(p$b, { as: "span", children: [
23599
+ "Parses string data to JSON; on parse failure returns",
23600
+ " ",
23601
+ `{ valid:false, errors:[{ keyword:"type", message:"must be a valid JSON object (string parsing failed)"} ]`
23602
+ ] }) }),
23603
+ /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "text-sm text-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(p$b, { as: "span", children: [
23604
+ "Uses Ajv(",
23605
+ `{ allErrors: true, strict: false }`,
23606
+ "); compiles provided schema"
23607
+ ] }) }),
23608
+ /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "text-sm text-gray-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { as: "span", children: "Returns AJV errors array when invalid" }) })
23609
+ ] }),
23610
+ /* @__PURE__ */ jsxRuntimeExports.jsx(r$8, { size: "4", mb: "2", children: "Source" }),
23611
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: "src/api/validators/json.js" }),
23612
+ /* @__PURE__ */ jsxRuntimeExports.jsx(r$8, { size: "4", mb: "2", mt: "4", children: "Usage Example" }),
23613
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: `export const validateStructure = async ({
23614
+ io,
23615
+ flags,
23616
+ validators: { validateWithSchema },
23617
+ }) => {
23618
+ const content = await io.readArtifact("research-output.json");
23619
+ const result = validateWithSchema(researchJsonSchema, content);
23620
+
23621
+ if (!result.valid) {
23622
+ console.warn("[Research:validateStructure] Validation failed", result.errors);
23623
+ return { output: {}, flags: { ...flags, validationFailed: true } };
23624
+ }
23625
+ return { output: {}, flags };
23626
+ };` })
23627
+ ] }),
23078
23628
  /* @__PURE__ */ jsxRuntimeExports.jsx(r$8, { size: "6", mt: "8", mb: "4", children: "Environment Configuration" }),
23079
23629
  /* @__PURE__ */ jsxRuntimeExports.jsxs(p$7, { mb: "4", children: [
23080
23630
  /* @__PURE__ */ jsxRuntimeExports.jsx(r$8, { size: "4", mb: "2", children: "Example .env Configuration" }),
@@ -23085,15 +23635,102 @@ function CodePage() {
23085
23635
  /* @__PURE__ */ jsxRuntimeExports.jsx(P$1, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(R, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: "DEEPSEEK_API_KEY=" }) }) }),
23086
23636
  /* @__PURE__ */ jsxRuntimeExports.jsx(P$1, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(R, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: "GEMINI_API_KEY=" }) }) }),
23087
23637
  /* @__PURE__ */ jsxRuntimeExports.jsx(P$1, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(R, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: "ANTHROPIC_API_KEY=" }) }) }),
23088
- /* @__PURE__ */ jsxRuntimeExports.jsx(P$1, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(R, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: "Z_API_KEY=" }) }) })
23638
+ /* @__PURE__ */ jsxRuntimeExports.jsx(P$1, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(R, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(p$2, { size: "3", children: "ZHIPU_API_KEY=" }) }) })
23089
23639
  ] })
23090
23640
  ] }) })
23091
23641
  ] })
23092
23642
  ] })
23093
23643
  ] });
23094
23644
  }
23095
- ReactDOM.createRoot(document.getElementById("root")).render(
23096
- /* @__PURE__ */ jsxRuntimeExports.jsx(React.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
23645
+ const ToastContext = reactExports.createContext();
23646
+ function ToastProvider({ children }) {
23647
+ const [toasts, setToasts] = reactExports.useState([]);
23648
+ const addToast = reactExports.useCallback((message, options = {}) => {
23649
+ const toast = {
23650
+ id: Date.now() + Math.random(),
23651
+ message,
23652
+ type: options.type || "info",
23653
+ duration: options.duration || 5e3
23654
+ };
23655
+ setToasts((prev) => [...prev, toast]);
23656
+ if (toast.duration > 0) {
23657
+ setTimeout(() => {
23658
+ removeToast(toast.id);
23659
+ }, toast.duration);
23660
+ }
23661
+ return toast.id;
23662
+ }, []);
23663
+ const removeToast = reactExports.useCallback((id) => {
23664
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
23665
+ }, []);
23666
+ const value = {
23667
+ addToast,
23668
+ removeToast,
23669
+ success: (message, options) => addToast(message, { ...options, type: "success" }),
23670
+ error: (message, options) => addToast(message, { ...options, type: "error" }),
23671
+ warning: (message, options) => addToast(message, { ...options, type: "warning" }),
23672
+ info: (message, options) => addToast(message, { ...options, type: "info" })
23673
+ };
23674
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(ToastContext.Provider, { value, children: [
23675
+ children,
23676
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ToastContainer, { toasts, onRemove: removeToast })
23677
+ ] });
23678
+ }
23679
+ function ToastContainer({ toasts, onRemove }) {
23680
+ if (toasts.length === 0) return null;
23681
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed top-4 right-4 z-50 space-y-2", children: toasts.map((toast) => /* @__PURE__ */ jsxRuntimeExports.jsx(ToastItem, { toast, onRemove }, toast.id)) });
23682
+ }
23683
+ function ToastItem({ toast, onRemove }) {
23684
+ const getToastStyles = (type) => {
23685
+ switch (type) {
23686
+ case "success":
23687
+ return "bg-green-50 border-green-200 text-green-800";
23688
+ case "error":
23689
+ return "bg-red-50 border-red-200 text-red-800";
23690
+ case "warning":
23691
+ return "bg-yellow-50 border-yellow-200 text-yellow-800";
23692
+ default:
23693
+ return "bg-blue-50 border-blue-200 text-blue-800";
23694
+ }
23695
+ };
23696
+ const getIcon = (type) => {
23697
+ switch (type) {
23698
+ case "success":
23699
+ return "✓";
23700
+ case "error":
23701
+ return "✕";
23702
+ case "warning":
23703
+ return "⚠";
23704
+ default:
23705
+ return "ℹ";
23706
+ }
23707
+ };
23708
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
23709
+ p$7,
23710
+ {
23711
+ className: `relative flex items-start p-4 border rounded-lg shadow-lg max-w-sm ${getToastStyles(
23712
+ toast.type
23713
+ )}`,
23714
+ role: "alert",
23715
+ "aria-live": "polite",
23716
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(p$4, { gap: "3", align: "start", children: [
23717
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { className: "flex-shrink-0 text-lg font-semibold", children: getIcon(toast.type) }),
23718
+ /* @__PURE__ */ jsxRuntimeExports.jsx(p$b, { className: "text-sm font-medium flex-1", children: toast.message }),
23719
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
23720
+ "button",
23721
+ {
23722
+ onClick: () => onRemove(toast.id),
23723
+ className: "flex-shrink-0 ml-4 text-sm opacity-60 hover:opacity-100 focus:outline-none focus:opacity-100",
23724
+ "aria-label": "Dismiss notification",
23725
+ children: "✕"
23726
+ }
23727
+ )
23728
+ ] })
23729
+ }
23730
+ );
23731
+ }
23732
+ ReactDOM$1.createRoot(document.getElementById("root")).render(
23733
+ /* @__PURE__ */ jsxRuntimeExports.jsx(React.StrictMode, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(ToastProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
23097
23734
  R$1,
23098
23735
  {
23099
23736
  accentColor: "iris",
@@ -23107,5 +23744,5 @@ ReactDOM.createRoot(document.getElementById("root")).render(
23107
23744
  /* @__PURE__ */ jsxRuntimeExports.jsx(Route, { path: "/code", element: /* @__PURE__ */ jsxRuntimeExports.jsx(CodePage, {}) })
23108
23745
  ] }) })
23109
23746
  }
23110
- ) })
23747
+ ) }) })
23111
23748
  );