langchain_agentx_stream_ui 0.1.9 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  fixDiagramSvg,
24
24
  markEventIdSeen,
25
25
  shouldSkipDuplicateEvent
26
- } from "./chunk-2T26L3VT.js";
26
+ } from "./chunk-DGC43A5L.js";
27
27
  import {
28
28
  DefaultToolBody,
29
29
  InteractionBusContext,
@@ -32,8 +32,8 @@ import {
32
32
  getNoopInteractionBus,
33
33
  resolveToolBody,
34
34
  useInteractionBus
35
- } from "./chunk-6DYISADG.js";
36
- import "./chunk-DP7V33X7.js";
35
+ } from "./chunk-T626YBJX.js";
36
+ import "./chunk-6Z4ZF36Z.js";
37
37
  import {
38
38
  CollapseKind,
39
39
  CollapsedExploreNode,
@@ -111,7 +111,7 @@ import {
111
111
  useSessionStatus,
112
112
  useSessionViewOptions,
113
113
  useTimeline
114
- } from "./chunk-ZNXAQ4ZZ.js";
114
+ } from "./chunk-OFXC2GZI.js";
115
115
  import {
116
116
  Agent,
117
117
  AgentToolBody,
@@ -138,7 +138,7 @@ import {
138
138
  WriteToolBody,
139
139
  createDefaultToolRegistry,
140
140
  formatAgentTitle
141
- } from "./chunk-7CLAU74Y.js";
141
+ } from "./chunk-YUZXTGJJ.js";
142
142
  import {
143
143
  BodyBlockList,
144
144
  DiffView,
@@ -154,7 +154,7 @@ import {
154
154
  formatTimeoutFooter,
155
155
  parseDiffText,
156
156
  truncateCommand
157
- } from "./chunk-4RIOBLGB.js";
157
+ } from "./chunk-GBY5DJ7L.js";
158
158
 
159
159
  // src/view/AgentSession.tsx
160
160
  import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "react";
@@ -275,6 +275,14 @@ function transportLog(message, level = "warn") {
275
275
  console.warn(line);
276
276
  }
277
277
  }
278
+ function isSseStreamTerminalEvent(agentEvent, expectedApplicationKind) {
279
+ const eventType = agentEvent.event_type;
280
+ if (eventType === "error") return true;
281
+ if (expectedApplicationKind === "workflow") {
282
+ return eventType === "workflow-end" || eventType === "workflow-failed";
283
+ }
284
+ return eventType === "finish";
285
+ }
278
286
  function buildResumeStreamUrl(baseUrl, lastEventId, queryParam = "last_event_id") {
279
287
  if (!lastEventId) return baseUrl;
280
288
  const sep = baseUrl.includes("?") ? "&" : "?";
@@ -420,7 +428,7 @@ function createAgentxSseSource(options) {
420
428
  } else {
421
429
  flushBufferedDelta();
422
430
  handler(agentEvent, ctx);
423
- if (agentEvent.event_type === "finish" || agentEvent.event_type === "error") {
431
+ if (isSseStreamTerminalEvent(agentEvent, expectedApplicationKind)) {
424
432
  streamTerminal = true;
425
433
  }
426
434
  }
@@ -626,6 +634,8 @@ function AgentSession({
626
634
  workspaceRoot
627
635
  ]
628
636
  );
637
+ const onErrorRef = useRef2(onError);
638
+ onErrorRef.current = onError;
629
639
  useEffect2(() => {
630
640
  const store = storeRef.current;
631
641
  const controller = new AbortController();
@@ -637,12 +647,15 @@ function AgentSession({
637
647
  if (tree.status === "running" || tree.status === "connecting") {
638
648
  store.setState({ tree: { ...tree, status: "error" } });
639
649
  }
650
+ onErrorRef.current?.(err);
651
+ } else if (err instanceof Error) {
652
+ onErrorRef.current?.(err);
640
653
  }
641
654
  });
642
655
  return () => {
643
656
  controller.abort();
644
657
  };
645
- }, [source, onError]);
658
+ }, [source]);
646
659
  return /* @__PURE__ */ jsx3(SessionStoreContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx3(InteractionBusContext.Provider, { value: busRef, children: /* @__PURE__ */ jsx3(NodeRegistryContext.Provider, { value: registryRef, children: /* @__PURE__ */ jsx3(MarkdownRendererContext.Provider, { value: markdownRenderer, children: /* @__PURE__ */ jsx3(ToolDisplayOptionsContext.Provider, { value: toolDisplayRef, children: /* @__PURE__ */ jsx3(SessionViewOptionsContext.Provider, { value: sessionViewRef, children: /* @__PURE__ */ jsxs("div", { className: "lax-agent-session", "data-testid": "lax-agent-session", children: [
647
660
  children ?? /* @__PURE__ */ jsx3(
648
661
  AgentLoopView,
@@ -762,6 +775,9 @@ function createEmptyWorkflowSessionState() {
762
775
  }
763
776
 
764
777
  // src/core/workflow/workflowLoopTreeUtils.ts
778
+ function isWorkflowLoopTreeActive(tree) {
779
+ return tree?.status === "running" || tree?.status === "connecting";
780
+ }
765
781
  function createWorkflowRunningLoopTree(seedMs = Date.now()) {
766
782
  const tree = createEmptyTree();
767
783
  tree.status = "running";
@@ -793,6 +809,168 @@ function finalizeWorkflowLoopTree(tree, terminalStatus = "done") {
793
809
  };
794
810
  }
795
811
 
812
+ // src/core/workflow/stageDoneSummary.ts
813
+ function countToolUses(tree) {
814
+ return Object.values(tree.byId).filter((node) => node.kind === "tool_call").length;
815
+ }
816
+ function computeDurationSeconds(tree) {
817
+ const started = tree.meta.startedAt;
818
+ if (started == null) return null;
819
+ let maxTs = started;
820
+ for (const node of Object.values(tree.byId)) {
821
+ if ("endedAt" in node && typeof node.endedAt === "number") {
822
+ maxTs = Math.max(maxTs, node.endedAt);
823
+ }
824
+ if ("startedAt" in node && typeof node.startedAt === "number") {
825
+ maxTs = Math.max(maxTs, node.startedAt);
826
+ }
827
+ }
828
+ const seconds = Math.round((maxTs - started) / 1e3);
829
+ return seconds > 0 ? seconds : null;
830
+ }
831
+ function lastTextContent(tree) {
832
+ const textNodes = Object.values(tree.byId).filter((node) => node.kind === "text");
833
+ if (textNodes.length === 0) return "";
834
+ return textNodes[textNodes.length - 1].accumulated.trim();
835
+ }
836
+ function buildStageDoneSummary(tree) {
837
+ if (tree.status !== "done" && tree.status !== "error") return null;
838
+ const toolUses = countToolUses(tree);
839
+ const duration = computeDurationSeconds(tree);
840
+ const durationSuffix = duration != null ? ` \xB7 ${duration}s` : "";
841
+ const label = toolUses > 0 ? `Done (${toolUses} tool use${toolUses === 1 ? "" : "s"}${durationSuffix})` : `Done (1 agent turn${durationSuffix})`;
842
+ const fullText = lastTextContent(tree);
843
+ const lines = fullText.split("\n").map((line) => line.trim()).filter(Boolean);
844
+ const teaser = lines[0] ?? "";
845
+ const extraLines = Math.max(0, lines.length - 1);
846
+ return { label, teaser, extraLines };
847
+ }
848
+ function formatTeaserExpandHint(extraLines, verbose) {
849
+ if (extraLines <= 0) return null;
850
+ const shown = verbose ? Math.min(extraLines, 2) : 0;
851
+ const hidden = extraLines - shown;
852
+ if (hidden <= 0) return null;
853
+ return `\u2026 +${hidden} lines (ctrl+o to expand)`;
854
+ }
855
+
856
+ // src/core/workflow/workflowLoopEviction.ts
857
+ function containersByLoopSessionId(tree) {
858
+ const map = /* @__PURE__ */ new Map();
859
+ for (const node of Object.values(tree.containersById)) {
860
+ if (node.loopSessionId) map.set(node.loopSessionId, node);
861
+ }
862
+ return map;
863
+ }
864
+ function hasRunningParallelSibling(node, tree) {
865
+ if (node.scope !== "item" || !node.parent_container_id) return false;
866
+ for (const sibling of Object.values(tree.containersById)) {
867
+ if (sibling.container_id === node.container_id) continue;
868
+ if (sibling.scope !== "item") continue;
869
+ if (sibling.parent_container_id !== node.parent_container_id) continue;
870
+ if (sibling.status === "running" || sibling.status === "retrying") return true;
871
+ }
872
+ return false;
873
+ }
874
+ function collectProtectedLoopSessionIds(state, pinnedLoopSessionIds) {
875
+ const protectedIds = new Set(Object.keys(pinnedLoopSessionIds));
876
+ const byLoop = containersByLoopSessionId(state.containerTree);
877
+ if (state.activeLoopSessionId) {
878
+ const activeContainer = byLoop.get(state.activeLoopSessionId);
879
+ if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
880
+ protectedIds.add(state.activeLoopSessionId);
881
+ }
882
+ }
883
+ for (const containerId of state.containerTree.activeContainerIds) {
884
+ const node = state.containerTree.containersById[containerId];
885
+ if (!node?.loopSessionId) continue;
886
+ if (node.status === "running" || node.status === "retrying") {
887
+ protectedIds.add(node.loopSessionId);
888
+ }
889
+ }
890
+ for (const node of Object.values(state.containerTree.containersById)) {
891
+ if (!node.loopSessionId) continue;
892
+ if (node.status === "running" || node.status === "retrying") {
893
+ protectedIds.add(node.loopSessionId);
894
+ }
895
+ if ((node.status === "completed" || node.status === "failed") && hasRunningParallelSibling(node, state.containerTree)) {
896
+ protectedIds.add(node.loopSessionId);
897
+ }
898
+ }
899
+ return protectedIds;
900
+ }
901
+ function evictInactiveLoopTrees(state, pinnedLoopSessionIds) {
902
+ const protectedIds = collectProtectedLoopSessionIds(state, pinnedLoopSessionIds);
903
+ for (const id of state.hotCompletedLoopSessionIds) {
904
+ protectedIds.add(id);
905
+ }
906
+ const byLoop = containersByLoopSessionId(state.containerTree);
907
+ const nextLoops = { ...state.loopTreesBySessionId };
908
+ const nextSummaries = {
909
+ ...state.stageDoneSummariesByContainerId
910
+ };
911
+ for (const [loopSessionId, tree] of Object.entries(state.loopTreesBySessionId)) {
912
+ if (protectedIds.has(loopSessionId)) continue;
913
+ const container = byLoop.get(loopSessionId);
914
+ if (!container) continue;
915
+ if (container.status !== "completed" && container.status !== "failed") continue;
916
+ const summary = buildStageDoneSummary(tree);
917
+ if (summary) {
918
+ nextSummaries[container.container_id] = summary;
919
+ }
920
+ delete nextLoops[loopSessionId];
921
+ }
922
+ return {
923
+ ...state,
924
+ loopTreesBySessionId: nextLoops,
925
+ stageDoneSummariesByContainerId: nextSummaries
926
+ };
927
+ }
928
+ function touchCompletedLoopSession(state, loopSessionId) {
929
+ const ids = state.hotCompletedLoopSessionIds.filter((id) => id !== loopSessionId);
930
+ ids.push(loopSessionId);
931
+ return {
932
+ ...state,
933
+ hotCompletedLoopSessionIds: ids
934
+ };
935
+ }
936
+ function evictCompletedLoopOverflow(state, maxHydratedCompletedLoops) {
937
+ if (state.hotCompletedLoopSessionIds.length <= maxHydratedCompletedLoops) {
938
+ return state;
939
+ }
940
+ const overflow = state.hotCompletedLoopSessionIds.length - maxHydratedCompletedLoops;
941
+ const candidateEvictIds = state.hotCompletedLoopSessionIds.slice(0, overflow);
942
+ let trimmedIds = state.hotCompletedLoopSessionIds.slice(overflow);
943
+ const byLoop = containersByLoopSessionId(state.containerTree);
944
+ const nextLoops = { ...state.loopTreesBySessionId };
945
+ const nextSummaries = {
946
+ ...state.stageDoneSummariesByContainerId
947
+ };
948
+ for (const id of candidateEvictIds) {
949
+ const tree = nextLoops[id];
950
+ if (!tree) continue;
951
+ const container = byLoop.get(id);
952
+ if (container && hasRunningParallelSibling(container, state.containerTree)) {
953
+ if (!trimmedIds.includes(id)) {
954
+ trimmedIds = [id, ...trimmedIds];
955
+ }
956
+ continue;
957
+ }
958
+ if (container) {
959
+ const summary = buildStageDoneSummary(tree);
960
+ if (summary) {
961
+ nextSummaries[container.container_id] = summary;
962
+ }
963
+ }
964
+ delete nextLoops[id];
965
+ }
966
+ return {
967
+ ...state,
968
+ loopTreesBySessionId: nextLoops,
969
+ stageDoneSummariesByContainerId: nextSummaries,
970
+ hotCompletedLoopSessionIds: trimmedIds
971
+ };
972
+ }
973
+
796
974
  // src/types/workflowDisplay.ts
797
975
  function readDisplayPayload(data) {
798
976
  const display = data.display;
@@ -1169,17 +1347,24 @@ var WorkflowContainerStateMachine = class {
1169
1347
  (node) => workflowPath === node.workflow_path || workflowPath.startsWith(`${node.workflow_path}>`)
1170
1348
  );
1171
1349
  if (prefixCandidates.length > 0) {
1172
- const parent = prefixCandidates.reduce(
1173
- (best, node) => this.active.compareContainerRank(node.container_id, best.container_id) > 0 ? node : best
1174
- );
1175
- if (parent.workflow_path === workflowPath && parent.scope === "workflow") {
1176
- return parent.container_id;
1177
- }
1178
- if (workflowPath.startsWith(`${parent.workflow_path}>`)) {
1179
- return parent.container_id;
1180
- }
1181
- if (parent.workflow_path === workflowPath && parent.scope !== scope) {
1182
- return parent.container_id;
1350
+ const parentCandidates = prefixCandidates.filter((node) => {
1351
+ if (scope === "item") return node.scope !== "item";
1352
+ if (scope === "aggregate") return node.scope !== "item" && node.scope !== "aggregate";
1353
+ return node.scope !== scope;
1354
+ });
1355
+ if (parentCandidates.length > 0) {
1356
+ const parent = parentCandidates.reduce(
1357
+ (best, node) => this.active.compareContainerRank(node.container_id, best.container_id) > 0 ? node : best
1358
+ );
1359
+ if (parent.workflow_path === workflowPath && parent.scope === "workflow") {
1360
+ return parent.container_id;
1361
+ }
1362
+ if (workflowPath.startsWith(`${parent.workflow_path}>`)) {
1363
+ return parent.container_id;
1364
+ }
1365
+ if (parent.workflow_path === workflowPath && parent.scope !== scope) {
1366
+ return parent.container_id;
1367
+ }
1183
1368
  }
1184
1369
  }
1185
1370
  const workflowCandidates = openNodes.filter((node) => node.scope === "workflow");
@@ -1277,6 +1462,167 @@ function loopTaskScopeForStructureOpen(eventType) {
1277
1462
  return LOOP_TASK_OPEN_SCOPES[eventType];
1278
1463
  }
1279
1464
 
1465
+ // src/core/workflowStructureEvents.ts
1466
+ var WORKFLOW_STRUCTURE_EVENT_PREFIXES = [
1467
+ "workflow-",
1468
+ "stage-",
1469
+ "parallel-",
1470
+ "route-",
1471
+ "subworkflow-"
1472
+ ];
1473
+ var WORKFLOW_STRUCTURE_EVENT_TYPES = /* @__PURE__ */ new Set([
1474
+ "workflow-start",
1475
+ "workflow-end",
1476
+ "workflow-failed",
1477
+ "subworkflow-start",
1478
+ "subworkflow-end",
1479
+ "stage-start",
1480
+ "stage-done",
1481
+ "stage-failed",
1482
+ "parallel-item-start",
1483
+ "parallel-item-done",
1484
+ "parallel-item-failed",
1485
+ "parallel-aggregate-start",
1486
+ "parallel-aggregate-done",
1487
+ "parallel-aggregate-failed",
1488
+ "route-branch-start",
1489
+ "route-branch-done",
1490
+ "route-branch-failed"
1491
+ ]);
1492
+ var AGENT_LOOP_REPLAY_EVENT_TYPES = new Set(
1493
+ Object.keys(DEFAULT_EVENT_TIERS)
1494
+ );
1495
+ function isWorkflowStructureEventType(eventType) {
1496
+ if (WORKFLOW_STRUCTURE_EVENT_TYPES.has(eventType)) return true;
1497
+ return WORKFLOW_STRUCTURE_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
1498
+ }
1499
+ function isAgentLoopReplayEventType(eventType) {
1500
+ return AGENT_LOOP_REPLAY_EVENT_TYPES.has(eventType);
1501
+ }
1502
+ function filterAgentLoopReplayEvents(events) {
1503
+ return events.filter(
1504
+ (event) => isAgentLoopReplayEventType(event.event_type) && !isWorkflowStructureEventType(event.event_type)
1505
+ );
1506
+ }
1507
+
1508
+ // src/core/workflow/workflowPathUtils.ts
1509
+ function splitWorkflowPathSegments(workflowPath) {
1510
+ return workflowPath.split(">").filter(Boolean);
1511
+ }
1512
+ function parentWorkflowPath(workflowPath) {
1513
+ const segments = splitWorkflowPathSegments(workflowPath);
1514
+ if (segments.length <= 1) return null;
1515
+ return segments.slice(0, -1).join(">");
1516
+ }
1517
+ function lastWorkflowPathSegment(workflowPath) {
1518
+ const segments = splitWorkflowPathSegments(workflowPath);
1519
+ return segments.length > 0 ? segments[segments.length - 1] : null;
1520
+ }
1521
+ function isWorkflowPathUnderAncestor(workflowPath, ancestorPath) {
1522
+ if (workflowPath === ancestorPath) return false;
1523
+ const prefix = `${ancestorPath}>`;
1524
+ return workflowPath.startsWith(prefix);
1525
+ }
1526
+ function isWorkflowPathEqualOrUnder(workflowPath, ancestorPath) {
1527
+ return workflowPath === ancestorPath || isWorkflowPathUnderAncestor(workflowPath, ancestorPath);
1528
+ }
1529
+
1530
+ // src/core/workflow/nativeNestedUtils.ts
1531
+ function readString3(data, key) {
1532
+ const value = data[key];
1533
+ return typeof value === "string" ? value : void 0;
1534
+ }
1535
+ function readNumber(data, key) {
1536
+ const value = data[key];
1537
+ return typeof value === "number" ? value : void 0;
1538
+ }
1539
+ function isNativeNestedStructureEvent(event) {
1540
+ if (!isWorkflowStructureEventType(event.event_type)) return false;
1541
+ const data = event.data ?? {};
1542
+ const path = readString3(data, "workflow_path") ?? "";
1543
+ const depth = readNumber(data, "workflow_depth") ?? 0;
1544
+ return depth >= 1 && path.includes(">");
1545
+ }
1546
+ function resolveHostStageContainerId(tree, workflowPath) {
1547
+ const parentPath = parentWorkflowPath(workflowPath);
1548
+ const childSegment = lastWorkflowPathSegment(workflowPath);
1549
+ if (!parentPath || !childSegment) return null;
1550
+ const stageId = buildContainerId(parentPath, "stage", childSegment);
1551
+ return tree.containersById[stageId] != null ? stageId : null;
1552
+ }
1553
+ function isHostStageNestedChildPath(tree, workflowPath) {
1554
+ return resolveHostStageContainerId(tree, workflowPath) != null;
1555
+ }
1556
+ function hasNativeNestedStructureInTree(tree) {
1557
+ for (const node of Object.values(tree.containersById)) {
1558
+ if (node.workflow_depth < 1) continue;
1559
+ if (!node.workflow_path.includes(">")) continue;
1560
+ if (isHostStageNestedChildPath(tree, node.workflow_path)) {
1561
+ return true;
1562
+ }
1563
+ }
1564
+ return false;
1565
+ }
1566
+ function clearHostStageLoopSession(tree, hostStageId) {
1567
+ if (!hostStageId) return tree;
1568
+ const hostStage = tree.containersById[hostStageId];
1569
+ if (!hostStage || hostStage.scope !== "stage" || !hostStage.loopSessionId) return tree;
1570
+ return {
1571
+ ...tree,
1572
+ containersById: {
1573
+ ...tree.containersById,
1574
+ [hostStageId]: { ...hostStage, loopSessionId: null }
1575
+ }
1576
+ };
1577
+ }
1578
+ function isNestedHostStageShell(tree, stageNode) {
1579
+ if (stageNode.scope !== "stage") return false;
1580
+ return Object.values(tree.containersById).some(
1581
+ (node) => node.scope === "subworkflow" && node.parent_container_id === stageNode.container_id
1582
+ );
1583
+ }
1584
+ function clearHostStageLoopOnNestedChildOpen(tree, event) {
1585
+ const eventType = event.event_type;
1586
+ if (eventType !== "subworkflow-start" && eventType !== "parallel-item-start") return tree;
1587
+ const data = event.data ?? {};
1588
+ const workflowPath = readString3(data, "workflow_path");
1589
+ if (!workflowPath) return tree;
1590
+ if (eventType === "subworkflow-start") {
1591
+ const childWorkflowId = readString3(data, "child_workflow_id");
1592
+ if (!childWorkflowId) return tree;
1593
+ const subId = buildContainerId(workflowPath, "subworkflow", childWorkflowId);
1594
+ const sub = tree.containersById[subId];
1595
+ const hostStageId = sub?.parent_container_id ?? resolveHostStageContainerId(tree, workflowPath);
1596
+ return clearHostStageLoopSession(tree, hostStageId);
1597
+ }
1598
+ return clearHostStageLoopSession(tree, resolveHostStageContainerId(tree, workflowPath));
1599
+ }
1600
+ function dropStaleEmbeddedPathsOnNativeSubworkflowStart(tree, event) {
1601
+ if (event.event_type !== "subworkflow-start" || !isNativeNestedStructureEvent(event)) {
1602
+ return tree;
1603
+ }
1604
+ const data = event.data ?? {};
1605
+ const nativeChildPath = readString3(data, "workflow_path");
1606
+ if (!nativeChildPath) return tree;
1607
+ const rootAnchor = splitWorkflowPathSegments(nativeChildPath)[0];
1608
+ if (!rootAnchor) return tree;
1609
+ let changed = false;
1610
+ const containersById = { ...tree.containersById };
1611
+ let activeContainerIds = [...tree.activeContainerIds];
1612
+ for (const [id, node] of Object.entries(tree.containersById)) {
1613
+ if (node.workflow_depth < 1) continue;
1614
+ if (!node.workflow_path.startsWith(`${rootAnchor}>`)) continue;
1615
+ if (node.workflow_path === nativeChildPath || isWorkflowPathEqualOrUnder(node.workflow_path, nativeChildPath)) {
1616
+ continue;
1617
+ }
1618
+ if (isHostStageNestedChildPath(tree, node.workflow_path)) continue;
1619
+ delete containersById[id];
1620
+ activeContainerIds = activeContainerIds.filter((cid) => cid !== id);
1621
+ changed = true;
1622
+ }
1623
+ return changed ? { ...tree, containersById, activeContainerIds } : tree;
1624
+ }
1625
+
1280
1626
  // src/core/workflow/embeddedSubworkflowUtils.ts
1281
1627
  function parseLoopSessionParts(sessionId) {
1282
1628
  const trimmed = sessionId.trim();
@@ -1378,7 +1724,20 @@ function completeOpenItemsUnderSubworkflow(tree, subworkflowId) {
1378
1724
  function resolveLeafScope(taskKey) {
1379
1725
  return taskKey === "aggregate" ? "aggregate" : "item";
1380
1726
  }
1727
+ function shouldUseEmbeddedSynthesis(treeOrState, pendingEvent) {
1728
+ const tree = "containerTree" in treeOrState ? treeOrState.containerTree : treeOrState;
1729
+ if (pendingEvent && isNativeNestedStructureEvent(pendingEvent)) {
1730
+ return false;
1731
+ }
1732
+ if (hasNativeNestedStructureInTree(tree)) {
1733
+ return false;
1734
+ }
1735
+ return true;
1736
+ }
1381
1737
  function ensureEmbeddedSubworkflowContainers(tree, loopSessionId) {
1738
+ if (!shouldUseEmbeddedSynthesis(tree)) {
1739
+ return tree;
1740
+ }
1382
1741
  const root = resolveRootWorkflow(tree);
1383
1742
  const parts = parseLoopSessionParts(loopSessionId);
1384
1743
  if (!root || !parts || !isEmbeddedChildLoopSession(loopSessionId, root.workflowId)) {
@@ -1461,11 +1820,39 @@ function findContainerIdByLoopSession(tree, loopSessionId) {
1461
1820
  }
1462
1821
  return null;
1463
1822
  }
1823
+ var PARALLEL_ITEM_STRUCTURE_EVENTS = /* @__PURE__ */ new Set([
1824
+ "parallel-item-start",
1825
+ "parallel-item-done",
1826
+ "parallel-item-failed"
1827
+ ]);
1828
+ function readItemKey(data) {
1829
+ const value = data.item_key;
1830
+ return typeof value === "string" ? value : void 0;
1831
+ }
1832
+ function resolveParallelItemLoopSessionId(eventType, eventSessionId, derivedLoopSessionId, data) {
1833
+ if (!PARALLEL_ITEM_STRUCTURE_EVENTS.has(eventType) || !eventSessionId) return null;
1834
+ if (eventSessionId === derivedLoopSessionId) return null;
1835
+ const itemKey = readItemKey(data);
1836
+ const parsed = parseLoopSessionParts(eventSessionId);
1837
+ if (itemKey && parsed?.taskKey === itemKey) {
1838
+ return eventSessionId;
1839
+ }
1840
+ return null;
1841
+ }
1464
1842
  function resolveStructureOpenLoopSessionId(eventType, eventSessionId, derivedLoopSessionId, data) {
1465
1843
  const rootWorkflowId = readWorkflowId(data) ?? "";
1466
1844
  if ((eventType === "parallel-aggregate-start" || eventType === "parallel-aggregate-done" || eventType === "parallel-aggregate-failed") && eventSessionId && isEmbeddedChildLoopSession(eventSessionId, rootWorkflowId)) {
1467
1845
  return eventSessionId;
1468
1846
  }
1847
+ const parallelItemSessionId = resolveParallelItemLoopSessionId(
1848
+ eventType,
1849
+ eventSessionId,
1850
+ derivedLoopSessionId,
1851
+ data
1852
+ );
1853
+ if (parallelItemSessionId) {
1854
+ return parallelItemSessionId;
1855
+ }
1469
1856
  return derivedLoopSessionId;
1470
1857
  }
1471
1858
  var EMBEDDED_AGGREGATE_STRUCTURE_EVENTS = /* @__PURE__ */ new Set([
@@ -1483,7 +1870,7 @@ function patchEmbeddedStructureEvent(event, rootWorkflowId) {
1483
1870
  const parts = parseLoopSessionParts(event.session_id);
1484
1871
  if (!parts) return event;
1485
1872
  const data = event.data ?? {};
1486
- const rootPath = readString3(data, "workflow_path") ?? rootWorkflowId;
1873
+ const rootPath = readString4(data, "workflow_path") ?? rootWorkflowId;
1487
1874
  const childPath = `${rootPath}>${parts.workflowId}`;
1488
1875
  const workflowDepth = typeof data.workflow_depth === "number" ? data.workflow_depth + 1 : 1;
1489
1876
  return {
@@ -1495,14 +1882,17 @@ function patchEmbeddedStructureEvent(event, rootWorkflowId) {
1495
1882
  }
1496
1883
  };
1497
1884
  }
1498
- function readString3(data, key) {
1885
+ function readString4(data, key) {
1499
1886
  const value = data[key];
1500
1887
  return typeof value === "string" ? value : void 0;
1501
1888
  }
1502
1889
  function isDescendantOf(containersById, ancestorId, nodeId) {
1890
+ const seen = /* @__PURE__ */ new Set();
1503
1891
  let current = containersById[nodeId]?.parent_container_id ?? null;
1504
1892
  while (current) {
1505
1893
  if (current === ancestorId) return true;
1894
+ if (seen.has(current)) return false;
1895
+ seen.add(current);
1506
1896
  current = containersById[current]?.parent_container_id ?? null;
1507
1897
  }
1508
1898
  return false;
@@ -1515,6 +1905,9 @@ function settleCompletedStageSubtree(tree, stageContainerId) {
1515
1905
  }
1516
1906
  if (node.container_id === stageContainerId) continue;
1517
1907
  if (node.status === "completed" || node.status === "failed") continue;
1908
+ if (node.status === "running" || node.status === "retrying" || node.status === "pending" || node.status === "blocked") {
1909
+ continue;
1910
+ }
1518
1911
  next = upsertRuntimeNode(next, {
1519
1912
  ...node,
1520
1913
  status: "completed",
@@ -1530,51 +1923,8 @@ function readWorkflowId(data) {
1530
1923
  return readWorkflowIdFromData(data);
1531
1924
  }
1532
1925
 
1533
- // src/core/workflowStructureEvents.ts
1534
- var WORKFLOW_STRUCTURE_EVENT_PREFIXES = [
1535
- "workflow-",
1536
- "stage-",
1537
- "parallel-",
1538
- "route-",
1539
- "subworkflow-"
1540
- ];
1541
- var WORKFLOW_STRUCTURE_EVENT_TYPES = /* @__PURE__ */ new Set([
1542
- "workflow-start",
1543
- "workflow-end",
1544
- "workflow-failed",
1545
- "subworkflow-start",
1546
- "subworkflow-end",
1547
- "stage-start",
1548
- "stage-done",
1549
- "stage-failed",
1550
- "parallel-item-start",
1551
- "parallel-item-done",
1552
- "parallel-item-failed",
1553
- "parallel-aggregate-start",
1554
- "parallel-aggregate-done",
1555
- "parallel-aggregate-failed",
1556
- "route-branch-start",
1557
- "route-branch-done",
1558
- "route-branch-failed"
1559
- ]);
1560
- var AGENT_LOOP_REPLAY_EVENT_TYPES = new Set(
1561
- Object.keys(DEFAULT_EVENT_TIERS)
1562
- );
1563
- function isWorkflowStructureEventType(eventType) {
1564
- if (WORKFLOW_STRUCTURE_EVENT_TYPES.has(eventType)) return true;
1565
- return WORKFLOW_STRUCTURE_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
1566
- }
1567
- function isAgentLoopReplayEventType(eventType) {
1568
- return AGENT_LOOP_REPLAY_EVENT_TYPES.has(eventType);
1569
- }
1570
- function filterAgentLoopReplayEvents(events) {
1571
- return events.filter(
1572
- (event) => isAgentLoopReplayEventType(event.event_type) && !isWorkflowStructureEventType(event.event_type)
1573
- );
1574
- }
1575
-
1576
1926
  // src/core/workflowReducer.ts
1577
- function readString4(data, key) {
1927
+ function readString5(data, key) {
1578
1928
  const value = data[key];
1579
1929
  return typeof value === "string" ? value : void 0;
1580
1930
  }
@@ -1673,7 +2023,7 @@ function prebindLoopSessionFromStructureOpen(tree, event) {
1673
2023
  loopSessionId = derivedLoopSessionId;
1674
2024
  }
1675
2025
  let nextTree = event.event_type === "parallel-aggregate-start" ? ensureEmbeddedSubworkflowContainers(tree, loopSessionId) : tree;
1676
- const workflowPath = readString4(data, "workflow_path") ?? "";
2026
+ const workflowPath = readString5(data, "workflow_path") ?? "";
1677
2027
  const scopeKey = resolveScopeKey(event.event_type, data);
1678
2028
  const containerId = buildContainerId(workflowPath, scope, scopeKey);
1679
2029
  let targetId = containerId;
@@ -1739,22 +2089,31 @@ function findContainerByLoopSession(tree, loopSessionId) {
1739
2089
  return findContainerIdByLoopSession(tree, loopSessionId);
1740
2090
  }
1741
2091
  var WORKFLOW_DEFAULT_LOOP_SESSION_ID = "workflow-loop-default";
1742
- function resolveAgentLoopSessionId(event, activeLoopSessionId) {
2092
+ function resolveAgentLoopSessionId(event, activeLoopSessionId, containerTree) {
1743
2093
  if (event.session_id) return event.session_id;
1744
2094
  if (isWorkflowStructureEventType(event.event_type)) return null;
1745
2095
  if (event.event_type === "task-list-snapshot") return null;
1746
2096
  const derived = deriveLoopSessionIdFromAgentEvent(event);
1747
2097
  if (derived) return derived;
2098
+ const byLoop = containersByLoopSessionId(containerTree);
2099
+ if (activeLoopSessionId) {
2100
+ const activeContainer = byLoop.get(activeLoopSessionId);
2101
+ if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
2102
+ return activeLoopSessionId;
2103
+ }
2104
+ }
2105
+ const runningLoopIds = [...byLoop.entries()].filter(([, node]) => node.status === "running" || node.status === "retrying").map(([loopSessionId]) => loopSessionId);
2106
+ if (runningLoopIds.length === 1) return runningLoopIds[0];
1748
2107
  if (activeLoopSessionId) return activeLoopSessionId;
1749
2108
  return WORKFLOW_DEFAULT_LOOP_SESSION_ID;
1750
2109
  }
1751
2110
  function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
1752
- const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId);
2111
+ const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId, tree);
1753
2112
  if (!sessionId) return tree;
1754
2113
  let nextTree = ensureEmbeddedSubworkflowContainers(tree, sessionId);
1755
2114
  if (findContainerByLoopSession(nextTree, sessionId)) return nextTree;
1756
2115
  const data = event.data ?? {};
1757
- const workflowPath = readString4(data, "workflow_path") ?? null;
2116
+ const workflowPath = readString5(data, "workflow_path") ?? null;
1758
2117
  const scopeKey = resolveContentScopeKey(data);
1759
2118
  let targetId = findContainerByLoopSession(nextTree, sessionId);
1760
2119
  if (!targetId && scopeKey) {
@@ -1765,12 +2124,17 @@ function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
1765
2124
  );
1766
2125
  }
1767
2126
  if (!targetId) {
1768
- for (const containerId of nextTree.activeContainerIds) {
1769
- const node2 = nextTree.containersById[containerId];
1770
- if ((node2?.scope === "item" || node2?.scope === "stage" || node2?.scope === "aggregate") && !node2.loopSessionId) {
2127
+ const fallbackScopes = ["item", "aggregate", "stage"];
2128
+ for (const scope of fallbackScopes) {
2129
+ for (const containerId of nextTree.activeContainerIds) {
2130
+ const node2 = nextTree.containersById[containerId];
2131
+ if (!node2 || node2.loopSessionId) continue;
2132
+ if (node2.scope !== scope) continue;
2133
+ if (node2.scope === "stage" && isNestedHostStageShell(nextTree, node2)) continue;
1771
2134
  targetId = containerId;
1772
2135
  break;
1773
2136
  }
2137
+ if (targetId) break;
1774
2138
  }
1775
2139
  }
1776
2140
  if (!targetId) {
@@ -1783,6 +2147,9 @@ function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
1783
2147
  if (!targetId) return nextTree;
1784
2148
  const node = nextTree.containersById[targetId];
1785
2149
  if (!node || node.loopSessionId) return nextTree;
2150
+ if (node.scope === "stage" && isNestedHostStageShell(nextTree, node)) {
2151
+ return nextTree;
2152
+ }
1786
2153
  if (node.scope !== "stage" && node.scope !== "item" && node.scope !== "branch" && node.scope !== "aggregate") {
1787
2154
  return nextTree;
1788
2155
  }
@@ -1882,8 +2249,15 @@ function finalizeLoopTreeOnStructureClose(state, event) {
1882
2249
  }
1883
2250
  function applyStructureEvent(state, event) {
1884
2251
  const rootWorkflowId = resolveRootWorkflowId(state.containerTree);
1885
- const projectedEvent = rootWorkflowId != null ? patchEmbeddedStructureEvent(event, rootWorkflowId) : event;
2252
+ const useEmbeddedSynthesis = shouldUseEmbeddedSynthesis(state, event);
2253
+ const projectedEvent = rootWorkflowId != null && useEmbeddedSynthesis ? patchEmbeddedStructureEvent(event, rootWorkflowId) : event;
1886
2254
  let containerTree = applyStructureToContainerTree(state.containerTree, projectedEvent);
2255
+ if (projectedEvent.event_type === "subworkflow-start" || projectedEvent.event_type === "parallel-item-start") {
2256
+ containerTree = clearHostStageLoopOnNestedChildOpen(containerTree, projectedEvent);
2257
+ }
2258
+ if (projectedEvent.event_type === "subworkflow-start") {
2259
+ containerTree = dropStaleEmbeddedPathsOnNativeSubworkflowStart(containerTree, projectedEvent);
2260
+ }
1887
2261
  const derivedLoopSessionId = deriveLoopSessionIdFromStructureOpen(projectedEvent);
1888
2262
  const structureData = projectedEvent.data ?? {};
1889
2263
  const structureLoopSessionId = derivedLoopSessionId ? resolveStructureOpenLoopSessionId(
@@ -1892,13 +2266,13 @@ function applyStructureEvent(state, event) {
1892
2266
  derivedLoopSessionId,
1893
2267
  structureData
1894
2268
  ) : null;
1895
- if (structureLoopSessionId) {
2269
+ if (structureLoopSessionId && useEmbeddedSynthesis) {
1896
2270
  containerTree = ensureEmbeddedSubworkflowContainers(containerTree, structureLoopSessionId);
1897
2271
  }
1898
2272
  containerTree = prebindLoopSessionFromStructureOpen(containerTree, projectedEvent);
1899
2273
  if (projectedEvent.event_type === "stage-done" || projectedEvent.event_type === "stage-failed") {
1900
- const stageKey = readString4(structureData, "stage_key");
1901
- const workflowPath = readString4(structureData, "workflow_path") ?? "";
2274
+ const stageKey = readString5(structureData, "stage_key");
2275
+ const workflowPath = readString5(structureData, "workflow_path") ?? "";
1902
2276
  if (stageKey) {
1903
2277
  const stageId = buildContainerId(workflowPath, "stage", stageKey);
1904
2278
  if (containerTree.containersById[stageId]) {
@@ -1973,7 +2347,7 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
1973
2347
  if (isWorkflowStructureEventType(event.event_type)) {
1974
2348
  return applyStructureEvent(state, event);
1975
2349
  }
1976
- const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
2350
+ const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId, state.containerTree) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
1977
2351
  let containerTree = bindLoopSessionToContainerTree(
1978
2352
  state.containerTree,
1979
2353
  event,
@@ -1992,6 +2366,11 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
1992
2366
  next.containerTree,
1993
2367
  next.workflowProgress
1994
2368
  );
2369
+ let status = next.status;
2370
+ if (workflowProgress.status === "done") status = "done";
2371
+ else if (workflowProgress.status === "error") status = "error";
2372
+ else if (reducedLoop.status === "error") status = "error";
2373
+ else if (status === "connecting") status = "running";
1995
2374
  return {
1996
2375
  ...next,
1997
2376
  workflowProgress,
@@ -2000,160 +2379,107 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
2000
2379
  [loopSessionId]: reducedLoop
2001
2380
  },
2002
2381
  activeLoopSessionId: loopSessionId,
2003
- status: reducedLoop.status === "error" ? "error" : next.status === "connecting" ? "running" : next.status
2382
+ status
2004
2383
  };
2005
2384
  }
2006
- function reduceWorkflowEvents(events, options) {
2007
- return events.reduce(
2008
- (state, event, index) => reduceWorkflowSession(state, event, index, options),
2009
- createEmptyWorkflowSessionState()
2010
- );
2011
- }
2012
-
2013
- // src/core/workflow/stageDoneSummary.ts
2014
- function countToolUses(tree) {
2015
- return Object.values(tree.byId).filter((node) => node.kind === "tool_call").length;
2016
- }
2017
- function computeDurationSeconds(tree) {
2018
- const started = tree.meta.startedAt;
2019
- if (started == null) return null;
2020
- let maxTs = started;
2021
- for (const node of Object.values(tree.byId)) {
2022
- if ("endedAt" in node && typeof node.endedAt === "number") {
2023
- maxTs = Math.max(maxTs, node.endedAt);
2024
- }
2025
- if ("startedAt" in node && typeof node.startedAt === "number") {
2026
- maxTs = Math.max(maxTs, node.startedAt);
2027
- }
2385
+ function finalizeWorkflowSessionOnStreamComplete(state) {
2386
+ if (state.status === "done" || state.status === "error") {
2387
+ return { ...state, activeLoopSessionId: null };
2028
2388
  }
2029
- const seconds = Math.round((maxTs - started) / 1e3);
2030
- return seconds > 0 ? seconds : null;
2031
- }
2032
- function lastTextContent(tree) {
2033
- const textNodes = Object.values(tree.byId).filter((node) => node.kind === "text");
2034
- if (textNodes.length === 0) return "";
2035
- return textNodes[textNodes.length - 1].accumulated.trim();
2036
- }
2037
- function buildStageDoneSummary(tree) {
2038
- if (tree.status !== "done" && tree.status !== "error") return null;
2039
- const toolUses = countToolUses(tree);
2040
- const duration = computeDurationSeconds(tree);
2041
- const durationSuffix = duration != null ? ` \xB7 ${duration}s` : "";
2042
- const label = toolUses > 0 ? `Done (${toolUses} tool use${toolUses === 1 ? "" : "s"}${durationSuffix})` : `Done (1 agent turn${durationSuffix})`;
2043
- const fullText = lastTextContent(tree);
2044
- const lines = fullText.split("\n").map((line) => line.trim()).filter(Boolean);
2045
- const teaser = lines[0] ?? "";
2046
- const extraLines = Math.max(0, lines.length - 1);
2047
- return { label, teaser, extraLines };
2048
- }
2049
- function formatTeaserExpandHint(extraLines, verbose) {
2050
- if (extraLines <= 0) return null;
2051
- const shown = verbose ? Math.min(extraLines, 2) : 0;
2052
- const hidden = extraLines - shown;
2053
- if (hidden <= 0) return null;
2054
- return `\u2026 +${hidden} lines (ctrl+o to expand)`;
2055
- }
2056
-
2057
- // src/core/workflow/workflowLoopEviction.ts
2058
- function containersByLoopSessionId(tree) {
2059
- const map = /* @__PURE__ */ new Map();
2060
- for (const node of Object.values(tree.containersById)) {
2061
- if (node.loopSessionId) map.set(node.loopSessionId, node);
2389
+ const tree = state.containerTree;
2390
+ const rootId = tree.rootContainerIds[0];
2391
+ if (!rootId) {
2392
+ return { ...state, status: "done", activeLoopSessionId: null };
2062
2393
  }
2063
- return map;
2064
- }
2065
- function collectProtectedLoopSessionIds(state, pinnedLoopSessionIds) {
2066
- const protectedIds = new Set(Object.keys(pinnedLoopSessionIds));
2067
- const byLoop = containersByLoopSessionId(state.containerTree);
2068
- if (state.activeLoopSessionId) {
2069
- const activeContainer = byLoop.get(state.activeLoopSessionId);
2070
- if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
2071
- protectedIds.add(state.activeLoopSessionId);
2072
- }
2394
+ const root = tree.containersById[rootId];
2395
+ if (!root) {
2396
+ return { ...state, status: "done", activeLoopSessionId: null };
2073
2397
  }
2074
- for (const containerId of state.containerTree.activeContainerIds) {
2075
- const node = state.containerTree.containersById[containerId];
2076
- if (!node?.loopSessionId) continue;
2077
- if (node.status === "running" || node.status === "retrying") {
2078
- protectedIds.add(node.loopSessionId);
2079
- }
2398
+ const topLevelStages = Object.values(tree.containersById).filter(
2399
+ (node) => node.scope === "stage" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
2400
+ );
2401
+ const progressTotal = root.display?.progress_total ?? (topLevelStages.reduce(
2402
+ (max, s) => Math.max(max, s.display?.progress_total ?? 0),
2403
+ 0
2404
+ ) || topLevelStages.length);
2405
+ const settledStageCount = topLevelStages.filter(
2406
+ (s) => s.status === "completed" || s.status === "failed" || s.status === "skipped"
2407
+ ).length;
2408
+ const progressCurrent = Math.max(
2409
+ root.display?.progress_current ?? 0,
2410
+ settledStageCount
2411
+ );
2412
+ const rootParallelItems = Object.values(tree.containersById).filter(
2413
+ (node) => node.scope === "item" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
2414
+ );
2415
+ const rootParallelSettled = topLevelStages.length === 0 && rootParallelItems.length > 0 && rootParallelItems.every(
2416
+ (item) => item.status === "completed" || item.status === "failed" || item.status === "skipped"
2417
+ ) && !Object.values(tree.containersById).some(
2418
+ (node) => node.container_id !== rootId && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth && (node.status === "running" || node.status === "retrying" || node.status === "pending")
2419
+ );
2420
+ const workflowVisuallyComplete = root.status === "completed" || root.status === "failed" || progressTotal > 0 && progressCurrent >= progressTotal || rootParallelSettled;
2421
+ if (!workflowVisuallyComplete) {
2422
+ return state;
2080
2423
  }
2081
- for (const node of Object.values(state.containerTree.containersById)) {
2082
- if (!node.loopSessionId) continue;
2083
- if (node.status === "running" || node.status === "retrying") {
2084
- protectedIds.add(node.loopSessionId);
2424
+ let containersById = { ...tree.containersById };
2425
+ for (const [id, node] of Object.entries(containersById)) {
2426
+ if (node.status === "running" || node.status === "pending") {
2427
+ containersById[id] = { ...node, status: "completed", is_open: false };
2085
2428
  }
2086
2429
  }
2087
- return protectedIds;
2088
- }
2089
- function evictInactiveLoopTrees(state, pinnedLoopSessionIds) {
2090
- const protectedIds = collectProtectedLoopSessionIds(state, pinnedLoopSessionIds);
2091
- for (const id of state.hotCompletedLoopSessionIds) {
2092
- protectedIds.add(id);
2430
+ const settledRoot = containersById[rootId] ?? root;
2431
+ if (settledRoot.status !== "completed" && settledRoot.status !== "failed") {
2432
+ containersById = {
2433
+ ...containersById,
2434
+ [rootId]: { ...settledRoot, status: "completed", is_open: false }
2435
+ };
2093
2436
  }
2094
- const byLoop = containersByLoopSessionId(state.containerTree);
2095
- const nextLoops = { ...state.loopTreesBySessionId };
2096
- const nextSummaries = {
2097
- ...state.stageDoneSummariesByContainerId
2098
- };
2099
- for (const [loopSessionId, tree] of Object.entries(state.loopTreesBySessionId)) {
2100
- if (protectedIds.has(loopSessionId)) continue;
2101
- const container = byLoop.get(loopSessionId);
2102
- if (!container) continue;
2103
- if (container.status !== "completed" && container.status !== "failed") continue;
2104
- const summary = buildStageDoneSummary(tree);
2105
- if (summary) {
2106
- nextSummaries[container.container_id] = summary;
2437
+ const loopTreesBySessionId = { ...state.loopTreesBySessionId };
2438
+ for (const [loopSessionId, loopTree] of Object.entries(loopTreesBySessionId)) {
2439
+ if (loopTree.status === "running" || loopTree.status === "connecting") {
2440
+ loopTreesBySessionId[loopSessionId] = finalizeWorkflowLoopTree(loopTree, "done");
2107
2441
  }
2108
- delete nextLoops[loopSessionId];
2109
2442
  }
2110
- return {
2111
- ...state,
2112
- loopTreesBySessionId: nextLoops,
2113
- stageDoneSummariesByContainerId: nextSummaries
2443
+ const containerTree = {
2444
+ ...tree,
2445
+ containersById,
2446
+ activeContainerIds: tree.activeContainerIds.filter(
2447
+ (id) => containersById[id]?.is_open
2448
+ )
2114
2449
  };
2115
- }
2116
- function touchCompletedLoopSession(state, loopSessionId) {
2117
- const ids = state.hotCompletedLoopSessionIds.filter((id) => id !== loopSessionId);
2118
- ids.push(loopSessionId);
2450
+ const workflowProgress = syncWorkflowProgressFromContainerTree(
2451
+ containerTree,
2452
+ state.workflowProgress
2453
+ );
2119
2454
  return {
2120
2455
  ...state,
2121
- hotCompletedLoopSessionIds: ids
2456
+ status: workflowProgress.status === "error" ? "error" : "done",
2457
+ containerTree,
2458
+ workflowProgress,
2459
+ loopTreesBySessionId,
2460
+ activeLoopSessionId: null
2122
2461
  };
2123
2462
  }
2124
- function evictCompletedLoopOverflow(state, maxHydratedCompletedLoops) {
2125
- if (state.hotCompletedLoopSessionIds.length <= maxHydratedCompletedLoops) {
2126
- return state;
2127
- }
2128
- const overflow = state.hotCompletedLoopSessionIds.length - maxHydratedCompletedLoops;
2129
- const evictedIds = new Set(state.hotCompletedLoopSessionIds.slice(0, overflow));
2130
- const trimmedIds = state.hotCompletedLoopSessionIds.slice(overflow);
2131
- const byLoop = containersByLoopSessionId(state.containerTree);
2132
- const nextLoops = { ...state.loopTreesBySessionId };
2133
- const nextSummaries = {
2134
- ...state.stageDoneSummariesByContainerId
2135
- };
2136
- for (const id of evictedIds) {
2137
- const tree = nextLoops[id];
2138
- if (!tree) continue;
2139
- const container = byLoop.get(id);
2140
- if (container) {
2141
- const summary = buildStageDoneSummary(tree);
2142
- if (summary) {
2143
- nextSummaries[container.container_id] = summary;
2144
- }
2145
- }
2146
- delete nextLoops[id];
2147
- }
2148
- return {
2149
- ...state,
2150
- loopTreesBySessionId: nextLoops,
2151
- stageDoneSummariesByContainerId: nextSummaries,
2152
- hotCompletedLoopSessionIds: trimmedIds
2153
- };
2463
+ function reduceWorkflowEvents(events, options) {
2464
+ return events.reduce(
2465
+ (state, event, index) => reduceWorkflowSession(state, event, index, options),
2466
+ createEmptyWorkflowSessionState()
2467
+ );
2154
2468
  }
2155
2469
 
2156
2470
  // src/core/workflowSessionStore.ts
2471
+ var DEFAULT_HYDRATE_CHUNK_SIZE = 50;
2472
+ function nextAnimationFrame() {
2473
+ return new Promise((resolve) => {
2474
+ requestAnimationFrame(() => resolve());
2475
+ });
2476
+ }
2477
+ function withoutHydrating(ids, loopSessionId) {
2478
+ if (!ids[loopSessionId]) return ids;
2479
+ const next = { ...ids };
2480
+ delete next[loopSessionId];
2481
+ return next;
2482
+ }
2157
2483
  function safeReduceWorkflow(state, event, eventIndex, options) {
2158
2484
  try {
2159
2485
  return { state: reduceWorkflowSession(state, event, eventIndex, options) };
@@ -2174,6 +2500,39 @@ function safeReduceWorkflow(state, event, eventIndex, options) {
2174
2500
  };
2175
2501
  }
2176
2502
  }
2503
+ function finalizeWorkflowSessionOnStreamError(state, message, eventIndex, stack) {
2504
+ const containersById = {
2505
+ ...state.containerTree.containersById
2506
+ };
2507
+ for (const [id, node] of Object.entries(containersById)) {
2508
+ if (node.status === "running" || node.status === "pending") {
2509
+ containersById[id] = { ...node, status: "failed", is_open: false };
2510
+ }
2511
+ }
2512
+ const loopTreesBySessionId = { ...state.loopTreesBySessionId };
2513
+ for (const [loopSessionId, tree] of Object.entries(loopTreesBySessionId)) {
2514
+ if (tree.status === "running" || tree.status === "connecting") {
2515
+ loopTreesBySessionId[loopSessionId] = finalizeWorkflowLoopTree(tree, "error");
2516
+ }
2517
+ }
2518
+ return {
2519
+ ...state,
2520
+ status: "error",
2521
+ containerTree: {
2522
+ ...state.containerTree,
2523
+ containersById,
2524
+ activeContainerIds: state.containerTree.activeContainerIds.filter(
2525
+ (id) => containersById[id]?.is_open
2526
+ )
2527
+ },
2528
+ loopTreesBySessionId,
2529
+ activeLoopSessionId: null,
2530
+ internalErrors: [
2531
+ ...state.internalErrors,
2532
+ { eventIndex, eventType: "stream-error", message, stack }
2533
+ ]
2534
+ };
2535
+ }
2177
2536
  function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionState(), storeOptions) {
2178
2537
  const reduceOpts = {
2179
2538
  tierOverrides: storeOptions?.tierOverrides
@@ -2190,6 +2549,7 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2190
2549
  return createStore2((set, get) => ({
2191
2550
  state: initialState,
2192
2551
  eventCount: initialEventCount,
2552
+ hydratingLoopSessionIds: {},
2193
2553
  applyEvent(event, ctx) {
2194
2554
  const sseEventId = ctx?.sseEventId;
2195
2555
  if (shouldSkipDuplicateEvent(seenEventIds, sseEventId)) return;
@@ -2216,28 +2576,29 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2216
2576
  seenEventIds.clear();
2217
2577
  set({
2218
2578
  state: createEmptyWorkflowSessionState(),
2219
- eventCount: 0
2579
+ eventCount: 0,
2580
+ hydratingLoopSessionIds: {}
2220
2581
  });
2221
2582
  },
2222
2583
  markAsError(error) {
2223
- const { state } = get();
2584
+ const { state, eventCount } = get();
2224
2585
  const message = error?.message ?? "Workflow stream error";
2225
2586
  set({
2226
- state: {
2227
- ...state,
2228
- status: "error",
2229
- internalErrors: [
2230
- ...state.internalErrors,
2231
- {
2232
- eventIndex: get().eventCount,
2233
- eventType: "stream-error",
2234
- message,
2235
- stack: error?.stack
2236
- }
2237
- ]
2238
- }
2587
+ state: finalizeWorkflowSessionOnStreamError(
2588
+ state,
2589
+ message,
2590
+ eventCount,
2591
+ error?.stack
2592
+ )
2239
2593
  });
2240
2594
  },
2595
+ markStreamComplete() {
2596
+ set((current) => ({
2597
+ state: finalizeState(
2598
+ finalizeWorkflowSessionOnStreamComplete(current.state)
2599
+ )
2600
+ }));
2601
+ },
2241
2602
  /**
2242
2603
  * P2:将 completed loop 加入 LRU 热缓存并淘汰溢出。
2243
2604
  * running 保护由 collectProtectedLoopSessionIds 按状态自动覆盖,不再依赖永久 pin。
@@ -2275,6 +2636,71 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2275
2636
  next = finalizeState(next);
2276
2637
  return { state: next };
2277
2638
  });
2639
+ },
2640
+ async hydrateLoopSessionAsync(loopSessionId, events, options) {
2641
+ const { hydratingLoopSessionIds } = get();
2642
+ if (hydratingLoopSessionIds[loopSessionId]) return;
2643
+ const filtered = filterAgentLoopReplayEvents(events);
2644
+ if (filtered.length === 0) return;
2645
+ const chunkSize = options?.chunkSize ?? DEFAULT_HYDRATE_CHUNK_SIZE;
2646
+ const signal = options?.signal;
2647
+ set((current) => ({
2648
+ hydratingLoopSessionIds: {
2649
+ ...current.hydratingLoopSessionIds,
2650
+ [loopSessionId]: true
2651
+ }
2652
+ }));
2653
+ try {
2654
+ let tree = get().state.loopTreesBySessionId[loopSessionId] ?? createEmptyTree();
2655
+ let idx = get().eventCount;
2656
+ for (let offset = 0; offset < filtered.length; offset += chunkSize) {
2657
+ if (signal?.aborted) {
2658
+ throw new DOMException("Hydrate aborted", "AbortError");
2659
+ }
2660
+ const chunk = filtered.slice(offset, offset + chunkSize);
2661
+ for (const event of chunk) {
2662
+ tree = reduceTree(tree, event, idx, reduceOpts);
2663
+ idx += 1;
2664
+ }
2665
+ const isLastChunk = offset + chunkSize >= filtered.length;
2666
+ set((current) => {
2667
+ let next = {
2668
+ ...current.state,
2669
+ loopTreesBySessionId: {
2670
+ ...current.state.loopTreesBySessionId,
2671
+ [loopSessionId]: tree
2672
+ }
2673
+ };
2674
+ if (isLastChunk) {
2675
+ next = touchCompletedLoopSession(next, loopSessionId);
2676
+ next = evictCompletedLoopOverflow(next, maxHydratedCompletedLoops);
2677
+ }
2678
+ next = finalizeState(next);
2679
+ return { state: next };
2680
+ });
2681
+ if (!isLastChunk) {
2682
+ await nextAnimationFrame();
2683
+ }
2684
+ }
2685
+ } catch {
2686
+ set((current) => {
2687
+ const { [loopSessionId]: _removed, ...restLoops } = current.state.loopTreesBySessionId;
2688
+ return {
2689
+ state: { ...current.state, loopTreesBySessionId: restLoops },
2690
+ hydratingLoopSessionIds: withoutHydrating(
2691
+ current.hydratingLoopSessionIds,
2692
+ loopSessionId
2693
+ )
2694
+ };
2695
+ });
2696
+ return;
2697
+ }
2698
+ set((current) => ({
2699
+ hydratingLoopSessionIds: withoutHydrating(
2700
+ current.hydratingLoopSessionIds,
2701
+ loopSessionId
2702
+ )
2703
+ }));
2278
2704
  }
2279
2705
  }));
2280
2706
  }
@@ -2283,7 +2709,7 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2283
2709
  import { useMemo as useMemo7, useState as useState6 } from "react";
2284
2710
 
2285
2711
  // src/view/workflow/WorkflowAggregateContainer.tsx
2286
- import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState } from "react";
2712
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState, memo } from "react";
2287
2713
 
2288
2714
  // src/view/workflow/WorkflowContainerLine.tsx
2289
2715
  import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -2427,6 +2853,76 @@ function WorkflowMarkdownPreview({
2427
2853
  return /* @__PURE__ */ jsx7("div", { className, "data-testid": testId, children: /* @__PURE__ */ jsx7(RichMarkdown, { content, deferDiagrams: true }) });
2428
2854
  }
2429
2855
 
2856
+ // src/view/workflow/workflowStageListUtils.ts
2857
+ var ACTIVE_CONTAINER_STATUSES = /* @__PURE__ */ new Set([
2858
+ "running",
2859
+ "retrying",
2860
+ "pending",
2861
+ "blocked"
2862
+ ]);
2863
+ function isDescendantContainer(containersById, ancestorId, nodeId) {
2864
+ const seen = /* @__PURE__ */ new Set();
2865
+ let current = containersById[nodeId]?.parent_container_id ?? null;
2866
+ while (current) {
2867
+ if (current === ancestorId) return true;
2868
+ if (seen.has(current)) return false;
2869
+ seen.add(current);
2870
+ current = containersById[current]?.parent_container_id ?? null;
2871
+ }
2872
+ return false;
2873
+ }
2874
+ function subtreeHasActiveWork(containersById, rootContainerId) {
2875
+ for (const node of Object.values(containersById)) {
2876
+ if (node.container_id === rootContainerId) continue;
2877
+ if (!isDescendantContainer(containersById, rootContainerId, node.container_id)) continue;
2878
+ if (ACTIVE_CONTAINER_STATUSES.has(node.status)) return true;
2879
+ }
2880
+ return false;
2881
+ }
2882
+ function stageSortKey(node) {
2883
+ return node.display?.progress_current ?? 0;
2884
+ }
2885
+ function partitionWorkflowStages(stages, maxVisibleDoneCount, containersById) {
2886
+ const sorted = [...stages].sort(
2887
+ (a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
2888
+ );
2889
+ const always = [];
2890
+ const done = [];
2891
+ for (const stage of sorted) {
2892
+ if (containersById && subtreeHasActiveWork(containersById, stage.container_id)) {
2893
+ always.push(stage);
2894
+ continue;
2895
+ }
2896
+ if (stage.status === "running" || stage.status === "retrying" || stage.status === "failed" || stage.status === "pending" || stage.status === "blocked") {
2897
+ always.push(stage);
2898
+ } else if (stage.status === "completed" || stage.status === "skipped") {
2899
+ done.push(stage);
2900
+ } else {
2901
+ always.push(stage);
2902
+ }
2903
+ }
2904
+ const visibleDone = done.slice(Math.max(0, done.length - maxVisibleDoneCount));
2905
+ const visible = [...always, ...visibleDone].sort(
2906
+ (a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
2907
+ );
2908
+ const hiddenCount = stages.length - visible.length;
2909
+ return {
2910
+ visible,
2911
+ hiddenCount,
2912
+ total: stages.length,
2913
+ visibleDoneCount: visibleDone.length,
2914
+ totalDoneCount: done.length
2915
+ };
2916
+ }
2917
+ function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
2918
+ if (activeContainerIds.includes(node.container_id)) return true;
2919
+ if (node.status === "completed" || node.status === "failed" || node.status === "skipped") {
2920
+ return false;
2921
+ }
2922
+ if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
2923
+ return false;
2924
+ }
2925
+
2430
2926
  // src/view/workflow/workflowContainerLineUtils.ts
2431
2927
  function formatContainerScopeMetaLabel(scope) {
2432
2928
  return scope;
@@ -2496,6 +2992,27 @@ function buildWorkflowRootMeta(node) {
2496
2992
  }
2497
2993
  return withScopeMeta(node, meta);
2498
2994
  }
2995
+ function buildEffectiveWorkflowRootMeta(node, containersById) {
2996
+ const meta = [];
2997
+ if (node.pattern) meta.push(node.pattern);
2998
+ const topStages = Object.values(containersById).filter(
2999
+ (n) => n.scope === "stage" && n.parent_container_id === node.container_id
3000
+ );
3001
+ const total = node.display?.progress_total ?? Math.max(topStages.length, 1);
3002
+ let settled = 0;
3003
+ for (const stage of topStages) {
3004
+ if (subtreeHasActiveWork(containersById, stage.container_id)) continue;
3005
+ if (stage.status === "completed" || stage.status === "failed" || stage.status === "skipped") {
3006
+ settled += 1;
3007
+ }
3008
+ }
3009
+ if (total > 0) meta.push(`${settled}/${total}`);
3010
+ return withScopeMeta(node, meta);
3011
+ }
3012
+ function isWorkflowRootVisuallyActive(node, containersById) {
3013
+ if (node.status === "running" || node.status === "retrying") return true;
3014
+ return subtreeHasActiveWork(containersById, node.container_id);
3015
+ }
2499
3016
  function buildStageLineMeta(node) {
2500
3017
  const progress = buildStageProgressMeta(node);
2501
3018
  return withScopeMeta(node, progress ? [progress] : []);
@@ -2551,6 +3068,29 @@ function mapAggregateUiStatus(node) {
2551
3068
  return mapContainerUiStatus(node.status);
2552
3069
  }
2553
3070
 
3071
+ // src/view/workflow/workflowContainerMemo.ts
3072
+ function displayVisualEqual(a, b) {
3073
+ if (a === b) return true;
3074
+ if (a === null || b === null) return false;
3075
+ if (a.progress_current !== b.progress_current) return false;
3076
+ if (a.status !== b.status) return false;
3077
+ return true;
3078
+ }
3079
+ function workflowContainerNodeVisualEqual(a, b) {
3080
+ if (a.container_id !== b.container_id) return false;
3081
+ if (a.status !== b.status) return false;
3082
+ if (a.is_open !== b.is_open) return false;
3083
+ if (a.loopSessionId !== b.loopSessionId) return false;
3084
+ if (a.title !== b.title) return false;
3085
+ if (a.subtitle !== b.subtitle) return false;
3086
+ if (a.content_blocks.length !== b.content_blocks.length) return false;
3087
+ if (!displayVisualEqual(a.display, b.display)) return false;
3088
+ return true;
3089
+ }
3090
+ function workflowContainerViewPropsEqual(prev, next) {
3091
+ return prev.depth === next.depth && prev.virtualized === next.virtualized && prev.virtualizeThreshold === next.virtualizeThreshold && prev.groupParallelTools === next.groupParallelTools;
3092
+ }
3093
+
2554
3094
  // src/view/workflow/WorkflowAggregateContainer.tsx
2555
3095
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
2556
3096
  function mergeContentPreview(node) {
@@ -2558,7 +3098,7 @@ function mergeContentPreview(node) {
2558
3098
  if (blocks.length === 0) return "";
2559
3099
  return blocks.map((block) => block.preview?.trim() ?? "").filter(Boolean).join("\n\n");
2560
3100
  }
2561
- function WorkflowAggregateContainer({
3101
+ function WorkflowAggregateContainerInner({
2562
3102
  node,
2563
3103
  loopTree,
2564
3104
  siblingItems = [],
@@ -2678,12 +3218,59 @@ function WorkflowAggregateContainer({
2678
3218
  }
2679
3219
  );
2680
3220
  }
3221
+ function workflowAggregateContainerPropsEqual(prev, next) {
3222
+ if (prev.loopTree !== next.loopTree) return false;
3223
+ if (!workflowContainerViewPropsEqual(prev, next)) return false;
3224
+ if (prev.siblingItems.length !== next.siblingItems.length) return false;
3225
+ for (let i = 0; i < prev.siblingItems.length; i += 1) {
3226
+ if (!workflowContainerNodeVisualEqual(prev.siblingItems[i], next.siblingItems[i])) {
3227
+ return false;
3228
+ }
3229
+ }
3230
+ return workflowContainerNodeVisualEqual(prev.node, next.node);
3231
+ }
3232
+ var WorkflowAggregateContainer = memo(
3233
+ WorkflowAggregateContainerInner,
3234
+ workflowAggregateContainerPropsEqual
3235
+ );
2681
3236
 
2682
3237
  // src/view/workflow/WorkflowParallelGroup.tsx
2683
- import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3 } from "react";
2684
- import { useStore as useStore3 } from "zustand";
3238
+ import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3, memo as memo3 } from "react";
2685
3239
 
2686
3240
  // src/core/workflow/workflowParallelGroupMetrics.ts
3241
+ var ACTIVE_ITEM_STATUSES = /* @__PURE__ */ new Set([
3242
+ "running",
3243
+ "retrying",
3244
+ "pending",
3245
+ "blocked"
3246
+ ]);
3247
+ function countMetricsFromContainerStatus(items) {
3248
+ let running = 0;
3249
+ let completed = 0;
3250
+ let failed = 0;
3251
+ for (const item of items) {
3252
+ if (ACTIVE_ITEM_STATUSES.has(item.status)) {
3253
+ running += 1;
3254
+ continue;
3255
+ }
3256
+ switch (item.status) {
3257
+ case "completed":
3258
+ completed += 1;
3259
+ break;
3260
+ case "failed":
3261
+ failed += 1;
3262
+ break;
3263
+ default:
3264
+ break;
3265
+ }
3266
+ }
3267
+ return {
3268
+ total: items.length,
3269
+ running,
3270
+ completed,
3271
+ failed
3272
+ };
3273
+ }
2687
3274
  function readDisplayMetrics(items) {
2688
3275
  let hasMetrics = false;
2689
3276
  let running = 0;
@@ -2704,34 +3291,29 @@ function readDisplayMetrics(items) {
2704
3291
  if (total <= 0) total = items.length;
2705
3292
  return { total, running, completed, failed };
2706
3293
  }
3294
+ function mergeParallelGroupMetrics(fromDisplay, fromStatus, itemCount) {
3295
+ return {
3296
+ total: Math.max(fromDisplay.total, fromStatus.total, itemCount),
3297
+ running: Math.max(fromDisplay.running, fromStatus.running),
3298
+ completed: Math.max(fromDisplay.completed, fromStatus.completed),
3299
+ failed: Math.max(fromDisplay.failed, fromStatus.failed)
3300
+ };
3301
+ }
2707
3302
  function computeParallelGroupMetrics(items) {
3303
+ const fromStatus = countMetricsFromContainerStatus(items);
2708
3304
  const fromDisplay = readDisplayMetrics(items);
2709
- if (fromDisplay) return fromDisplay;
2710
- let running = 0;
2711
- let completed = 0;
2712
- let failed = 0;
3305
+ if (!fromDisplay) return fromStatus;
3306
+ return mergeParallelGroupMetrics(fromDisplay, fromStatus, items.length);
3307
+ }
3308
+ function parallelGroupHasActiveWork(items, loopTreesBySessionId = {}) {
3309
+ const metrics = computeParallelGroupMetrics(items);
3310
+ if (metrics.running > 0) return true;
2713
3311
  for (const item of items) {
2714
- switch (item.status) {
2715
- case "running":
2716
- case "retrying":
2717
- running += 1;
2718
- break;
2719
- case "completed":
2720
- completed += 1;
2721
- break;
2722
- case "failed":
2723
- failed += 1;
2724
- break;
2725
- default:
2726
- break;
2727
- }
3312
+ const loopSessionId = item.loopSessionId;
3313
+ if (!loopSessionId) continue;
3314
+ if (isWorkflowLoopTreeActive(loopTreesBySessionId[loopSessionId])) return true;
2728
3315
  }
2729
- return {
2730
- total: items.length,
2731
- running,
2732
- completed,
2733
- failed
2734
- };
3316
+ return false;
2735
3317
  }
2736
3318
  function formatParallelGroupSummary(metrics) {
2737
3319
  if (metrics.running > 0) {
@@ -2747,7 +3329,7 @@ function formatParallelGroupSummary(metrics) {
2747
3329
  }
2748
3330
 
2749
3331
  // src/view/workflow/WorkflowStageContainer.tsx
2750
- import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useState as useState2 } from "react";
3332
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useState as useState2, memo as memo2 } from "react";
2751
3333
  import { useStore as useStore2 } from "zustand";
2752
3334
 
2753
3335
  // src/core/workflow/loopTreeUtils.ts
@@ -2788,44 +3370,6 @@ function WorkflowStageDoneBody({
2788
3370
  );
2789
3371
  }
2790
3372
 
2791
- // src/view/workflow/workflowStageListUtils.ts
2792
- function stageSortKey(node) {
2793
- return node.display?.progress_current ?? 0;
2794
- }
2795
- function partitionWorkflowStages(stages, maxVisibleDoneCount) {
2796
- const sorted = [...stages].sort(
2797
- (a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
2798
- );
2799
- const always = [];
2800
- const done = [];
2801
- for (const stage of sorted) {
2802
- if (stage.status === "running" || stage.status === "retrying" || stage.status === "failed" || stage.status === "pending" || stage.status === "blocked") {
2803
- always.push(stage);
2804
- } else if (stage.status === "completed" || stage.status === "skipped") {
2805
- done.push(stage);
2806
- } else {
2807
- always.push(stage);
2808
- }
2809
- }
2810
- const visibleDone = done.slice(Math.max(0, done.length - maxVisibleDoneCount));
2811
- const visible = [...always, ...visibleDone].sort(
2812
- (a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
2813
- );
2814
- const hiddenCount = stages.length - visible.length;
2815
- return {
2816
- visible,
2817
- hiddenCount,
2818
- total: stages.length,
2819
- visibleDoneCount: visibleDone.length,
2820
- totalDoneCount: done.length
2821
- };
2822
- }
2823
- function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
2824
- if (activeContainerIds.includes(node.container_id)) return true;
2825
- if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
2826
- return false;
2827
- }
2828
-
2829
3373
  // src/view/workflow/WorkflowStageContainer.tsx
2830
3374
  import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
2831
3375
  function containerTestId(node) {
@@ -2837,16 +3381,17 @@ function bodyTestId(node) {
2837
3381
  if (node.scope === "item") return `lax-workflow-parallel-body-${node.scope_key}`;
2838
3382
  return `lax-workflow-stage-body-${node.scope_key}`;
2839
3383
  }
2840
- function WorkflowStageContainer({
3384
+ function WorkflowStageContainerInner({
2841
3385
  node,
2842
3386
  loopTree,
2843
3387
  depth = 0,
2844
3388
  virtualized,
2845
3389
  virtualizeThreshold,
2846
- groupParallelTools
3390
+ groupParallelTools,
3391
+ suppressHostScopedLoop = false,
3392
+ subtreeHasActiveWork: subtreeHasActiveWork2 = false
2847
3393
  }) {
2848
3394
  const { verbose } = useSessionViewOptions();
2849
- const scale = useWorkflowScaleOptions();
2850
3395
  const store = useWorkflowSessionStoreApi();
2851
3396
  const replayHandler = useWorkflowLoopReplayHandler();
2852
3397
  const activeLoopSessionId = useStore2(store, (s) => s.state.activeLoopSessionId);
@@ -2856,31 +3401,30 @@ function WorkflowStageContainer({
2856
3401
  (s) => s.state.stageDoneSummariesByContainerId[node.container_id]
2857
3402
  );
2858
3403
  const pinLoopSession = useStore2(store, (s) => s.pinLoopSession);
2859
- const uiStatus = mapContainerUiStatus(node.status);
2860
3404
  const isRunning = node.status === "running" || node.status === "retrying";
2861
3405
  const isDone = node.status === "completed" || node.status === "failed";
2862
3406
  const isSkipped = node.status === "skipped";
3407
+ const loopStillActive = isWorkflowLoopTreeActive(loopTree);
3408
+ const effectivelyRunning = isRunning || loopStillActive || subtreeHasActiveWork2 && isDone;
3409
+ const effectivelyDone = isDone && !subtreeHasActiveWork2 && !loopStillActive;
3410
+ const uiStatus = mapContainerUiStatus(effectivelyRunning ? "running" : node.status);
2863
3411
  const isActive = isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds);
2864
3412
  const doneSummaryFromTree = useMemo3(
2865
3413
  () => loopTree && isDone ? buildStageDoneSummary(loopTree) : null,
2866
3414
  [loopTree, isDone]
2867
3415
  );
2868
3416
  const doneSummary = doneSummaryFromTree ?? (isDone ? cachedSummary : null);
2869
- const shouldAutoExpandRunning = isRunning && !isSkipped && (!scale.onlyExpandActiveRunning || isActive);
3417
+ const shouldAutoExpandRunning = effectivelyRunning && !isSkipped;
2870
3418
  const [expanded, setExpanded] = useState2(shouldAutoExpandRunning);
2871
3419
  useEffect4(() => {
2872
3420
  if (shouldAutoExpandRunning) {
2873
3421
  setExpanded(true);
2874
3422
  return;
2875
3423
  }
2876
- if (isRunning && !isSkipped && scale.onlyExpandActiveRunning) {
2877
- setExpanded(false);
2878
- return;
2879
- }
2880
- if (isDone || isSkipped) {
3424
+ if (effectivelyDone || isSkipped) {
2881
3425
  setExpanded(false);
2882
3426
  }
2883
- }, [shouldAutoExpandRunning, isRunning, isDone, isSkipped, scale.onlyExpandActiveRunning]);
3427
+ }, [shouldAutoExpandRunning, effectivelyDone, isSkipped]);
2884
3428
  const requestLoopHydration = useCallback2(() => {
2885
3429
  const loopSessionId = node.loopSessionId;
2886
3430
  if (!loopSessionId || loopTree) return;
@@ -2905,17 +3449,18 @@ function WorkflowStageContainer({
2905
3449
  window.addEventListener("keydown", onKeyDown);
2906
3450
  return () => window.removeEventListener("keydown", onKeyDown);
2907
3451
  }, [requestLoopHydration]);
3452
+ const testId = containerTestId(node);
3453
+ const isItem = node.scope === "item";
2908
3454
  const displayTitle = node.title;
2909
- const usesScopedLoop = Boolean(node.loopSessionId);
2910
- const runningLoopTree = loopTree ?? (usesScopedLoop && isRunning && isActive ? createWorkflowRunningLoopTree() : void 0);
3455
+ const usesScopedLoop = Boolean(node.loopSessionId) && !suppressHostScopedLoop;
3456
+ const runningLoopTree = loopTree ?? (usesScopedLoop && effectivelyRunning ? createWorkflowRunningLoopTree() : void 0);
3457
+ const itemLoopVirtualized = isItem ? false : virtualized;
2911
3458
  const staticContentTeaser = !usesScopedLoop && !loopTree && node.content_blocks.length > 0 ? node.content_blocks[node.content_blocks.length - 1].preview : "";
2912
3459
  const teaserText = doneSummary?.teaser || staticContentTeaser;
2913
3460
  const expandHint = doneSummary ? formatTeaserExpandHint(doneSummary.extraLines, verbose) : null;
2914
3461
  const doneLabel = doneSummary?.label ?? "done";
2915
- const statusLabel = isDone ? doneLabel : mapContainerStatusLabel(node.status);
2916
- const testId = containerTestId(node);
2917
- const isItem = node.scope === "item";
2918
- const canToggleShell = isRunning && !isSkipped || isDone;
3462
+ const statusLabel = effectivelyDone ? doneLabel : suppressHostScopedLoop && effectivelyRunning ? "running" : mapContainerStatusLabel(effectivelyRunning ? "running" : node.status);
3463
+ const canToggleShell = effectivelyRunning && !isSkipped || effectivelyDone;
2919
3464
  const shellToggle = canToggleShell ? toggleExpanded : void 0;
2920
3465
  return /* @__PURE__ */ jsxs5(
2921
3466
  "div",
@@ -2941,13 +3486,13 @@ function WorkflowStageContainer({
2941
3486
  onToggle: shellToggle,
2942
3487
  interactive: canToggleShell,
2943
3488
  expanded,
2944
- headerTestId: isDone ? `${testId}-done` : void 0,
3489
+ headerTestId: effectivelyDone ? `${testId}-done` : void 0,
2945
3490
  titleTestId: `${testId}-title`,
2946
3491
  descriptionTestId: `${testId}-desc`,
2947
3492
  className: isItem ? "lax-workflow-parallel-item__header" : void 0
2948
3493
  }
2949
3494
  ),
2950
- isDone && !expanded && (teaserText || expandHint) ? /* @__PURE__ */ jsxs5("div", { className: "lax-workflow-stage-container__done-teaser", children: [
3495
+ effectivelyDone && !expanded && (teaserText || expandHint) ? /* @__PURE__ */ jsxs5("div", { className: "lax-workflow-stage-container__done-teaser", children: [
2951
3496
  teaserText ? /* @__PURE__ */ jsx10(
2952
3497
  "span",
2953
3498
  {
@@ -2973,7 +3518,7 @@ function WorkflowStageContainer({
2973
3518
  ))
2974
3519
  }
2975
3520
  ) : null,
2976
- expanded && isRunning && runningLoopTree ? /* @__PURE__ */ jsx10(
3521
+ expanded && effectivelyRunning && runningLoopTree ? /* @__PURE__ */ jsx10(
2977
3522
  "div",
2978
3523
  {
2979
3524
  className: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body",
@@ -2983,20 +3528,20 @@ function WorkflowStageContainer({
2983
3528
  {
2984
3529
  tree: runningLoopTree,
2985
3530
  showTaskListFooter: false,
2986
- virtualized,
3531
+ virtualized: itemLoopVirtualized,
2987
3532
  virtualizeThreshold,
2988
3533
  groupParallelTools
2989
3534
  }
2990
3535
  )
2991
3536
  }
2992
3537
  ) : null,
2993
- isDone && expanded ? loopTree ? /* @__PURE__ */ jsx10(
3538
+ effectivelyDone && expanded ? loopTree ? /* @__PURE__ */ jsx10(
2994
3539
  WorkflowStageDoneBody,
2995
3540
  {
2996
3541
  node,
2997
3542
  loopTree,
2998
3543
  testId: bodyTestId(node),
2999
- virtualized,
3544
+ virtualized: itemLoopVirtualized,
3000
3545
  virtualizeThreshold,
3001
3546
  groupParallelTools,
3002
3547
  bodyClassName: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body"
@@ -3016,6 +3561,14 @@ function WorkflowStageContainer({
3016
3561
  }
3017
3562
  );
3018
3563
  }
3564
+ function workflowStageContainerPropsEqual(prev, next) {
3565
+ if (prev.suppressHostScopedLoop !== next.suppressHostScopedLoop) return false;
3566
+ if (prev.subtreeHasActiveWork !== next.subtreeHasActiveWork) return false;
3567
+ if (prev.loopTree !== next.loopTree) return false;
3568
+ if (!workflowContainerViewPropsEqual(prev, next)) return false;
3569
+ return workflowContainerNodeVisualEqual(prev.node, next.node);
3570
+ }
3571
+ var WorkflowStageContainer = memo2(WorkflowStageContainerInner, workflowStageContainerPropsEqual);
3019
3572
 
3020
3573
  // src/view/workflow/WorkflowParallelGroup.tsx
3021
3574
  import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -3023,7 +3576,7 @@ function resolveLoopTree(node, loopTreesBySessionId) {
3023
3576
  if (!node.loopSessionId) return void 0;
3024
3577
  return loopTreesBySessionId[node.loopSessionId];
3025
3578
  }
3026
- function WorkflowParallelGroup({
3579
+ function WorkflowParallelGroupInner({
3027
3580
  groupKey,
3028
3581
  items,
3029
3582
  loopTreesBySessionId,
@@ -3032,33 +3585,23 @@ function WorkflowParallelGroup({
3032
3585
  virtualizeThreshold,
3033
3586
  groupParallelTools
3034
3587
  }) {
3035
- const scale = useWorkflowScaleOptions();
3036
- const store = useWorkflowSessionStoreApi();
3037
- const activeLoopSessionId = useStore3(store, (s) => s.state.activeLoopSessionId);
3038
- const activeContainerIds = useStore3(store, (s) => s.state.containerTree.activeContainerIds);
3039
3588
  const metrics = useMemo4(() => computeParallelGroupMetrics(items), [items]);
3040
3589
  const summary = formatParallelGroupSummary(metrics);
3041
- const hasRunning = metrics.running > 0;
3042
- const hasActiveItem = useMemo4(
3043
- () => items.some((item) => isActiveWorkflowContainer(item, activeLoopSessionId, activeContainerIds)),
3044
- [items, activeLoopSessionId, activeContainerIds]
3590
+ const hasActiveWork = useMemo4(
3591
+ () => parallelGroupHasActiveWork(items, loopTreesBySessionId),
3592
+ [items, loopTreesBySessionId]
3045
3593
  );
3046
3594
  const allSettled = metrics.total > 0 && metrics.running === 0 && metrics.completed + metrics.failed >= metrics.total;
3047
- const shouldAutoExpandGroup = hasRunning && (!scale.onlyExpandActiveRunning || hasActiveItem);
3048
- const [expanded, setExpanded] = useState3(shouldAutoExpandGroup);
3595
+ const [expanded, setExpanded] = useState3(hasActiveWork);
3049
3596
  useEffect5(() => {
3050
- if (shouldAutoExpandGroup) {
3597
+ if (hasActiveWork) {
3051
3598
  setExpanded(true);
3052
3599
  return;
3053
3600
  }
3054
- if (hasRunning && scale.onlyExpandActiveRunning) {
3055
- setExpanded(false);
3056
- return;
3057
- }
3058
3601
  if (allSettled) {
3059
3602
  setExpanded(false);
3060
3603
  }
3061
- }, [shouldAutoExpandGroup, hasRunning, allSettled, scale.onlyExpandActiveRunning]);
3604
+ }, [hasActiveWork, allSettled]);
3062
3605
  const toggleExpanded = useCallback3(() => {
3063
3606
  setExpanded((prev) => !prev);
3064
3607
  }, []);
@@ -3095,7 +3638,7 @@ function WorkflowParallelGroup({
3095
3638
  WorkflowExpandTrigger,
3096
3639
  {
3097
3640
  summary,
3098
- expandHint: !expanded && !hasRunning ? "(ctrl+o to expand)" : null,
3641
+ expandHint: !expanded && hasActiveWork ? "(click to expand agents)" : !expanded && !hasActiveWork ? "(ctrl+o to expand)" : null,
3099
3642
  onToggle: toggleExpanded,
3100
3643
  expanded,
3101
3644
  testId: `lax-workflow-parallel-group-summary-${groupKey}`,
@@ -3107,16 +3650,37 @@ function WorkflowParallelGroup({
3107
3650
  }
3108
3651
  );
3109
3652
  }
3653
+ function workflowParallelGroupPropsEqual(prev, next) {
3654
+ if (prev.groupKey !== next.groupKey) return false;
3655
+ if (!workflowContainerViewPropsEqual(prev, next)) return false;
3656
+ if (prev.items.length !== next.items.length) return false;
3657
+ for (let i = 0; i < prev.items.length; i += 1) {
3658
+ const prevItem = prev.items[i];
3659
+ const nextItem = next.items[i];
3660
+ if (!workflowContainerNodeVisualEqual(prevItem, nextItem)) return false;
3661
+ const loopSessionId = prevItem.loopSessionId;
3662
+ if (loopSessionId) {
3663
+ if (prev.loopTreesBySessionId[loopSessionId] !== next.loopTreesBySessionId[loopSessionId]) {
3664
+ return false;
3665
+ }
3666
+ }
3667
+ }
3668
+ return true;
3669
+ }
3670
+ var WorkflowParallelGroup = memo3(WorkflowParallelGroupInner, workflowParallelGroupPropsEqual);
3110
3671
 
3111
3672
  // src/view/workflow/WorkflowRootContainer.tsx
3112
3673
  import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
3113
3674
  function WorkflowRootContainer({
3114
3675
  node,
3115
- children
3676
+ children,
3677
+ containersById
3116
3678
  }) {
3117
3679
  const { verbose } = useSessionViewOptions();
3118
- const uiStatus = mapContainerUiStatus(node.status);
3119
- const statusLabel = mapWorkflowRootStatusLabel(node.status);
3680
+ const visuallyActive = containersById ? isWorkflowRootVisuallyActive(node, containersById) : node.status === "running" || node.status === "retrying";
3681
+ const uiStatus = mapContainerUiStatus(visuallyActive ? "running" : node.status);
3682
+ const statusLabel = mapWorkflowRootStatusLabel(visuallyActive ? "running" : node.status);
3683
+ const meta = containersById ? buildEffectiveWorkflowRootMeta(node, containersById) : buildWorkflowRootMeta(node);
3120
3684
  return /* @__PURE__ */ jsxs7("div", { className: "lax-workflow-root", "data-testid": "lax-workflow-root", children: [
3121
3685
  /* @__PURE__ */ jsx12(
3122
3686
  WorkflowContainerLine,
@@ -3125,7 +3689,7 @@ function WorkflowRootContainer({
3125
3689
  title: node.title,
3126
3690
  description: node.subtitle || void 0,
3127
3691
  uiStatus,
3128
- meta: buildWorkflowRootMeta(node),
3692
+ meta,
3129
3693
  statusLabel,
3130
3694
  titleTestId: "lax-workflow-root-title",
3131
3695
  descriptionTestId: "lax-workflow-root-desc",
@@ -3253,31 +3817,36 @@ function buildDoneSummary(node) {
3253
3817
  function WorkflowSubworkflowContainer({
3254
3818
  node,
3255
3819
  depth = 1,
3256
- children
3820
+ children,
3821
+ subtreeHasActiveWork: subtreeHasActiveWork2 = false
3257
3822
  }) {
3258
3823
  const { verbose } = useSessionViewOptions();
3259
- const uiStatus = mapContainerUiStatus(node.status);
3260
3824
  const isRunning = node.status === "running" || node.status === "retrying";
3261
3825
  const isDone = node.status === "completed" || node.status === "failed";
3826
+ const effectivelyRunning = isRunning || subtreeHasActiveWork2;
3827
+ const effectivelyDone = isDone && !subtreeHasActiveWork2;
3828
+ const uiStatus = mapContainerUiStatus(
3829
+ effectivelyRunning ? "running" : node.status
3830
+ );
3262
3831
  const testId = `lax-workflow-subworkflow-${node.scope_key}`;
3263
3832
  const teaser = useMemo6(() => {
3264
3833
  if (node.content_blocks.length === 0) return "";
3265
3834
  return node.content_blocks[node.content_blocks.length - 1]?.preview ?? "";
3266
3835
  }, [node.content_blocks]);
3267
- const [expanded, setExpanded] = useState5(isRunning);
3836
+ const [expanded, setExpanded] = useState5(effectivelyRunning);
3268
3837
  useEffect7(() => {
3269
- if (isRunning) setExpanded(true);
3270
- if (isDone) setExpanded(false);
3271
- }, [isRunning, isDone]);
3838
+ if (effectivelyRunning) setExpanded(true);
3839
+ else if (effectivelyDone) setExpanded(false);
3840
+ }, [effectivelyRunning, effectivelyDone]);
3272
3841
  const toggleExpanded = useCallback5(() => {
3273
3842
  setExpanded((prev) => !prev);
3274
3843
  }, []);
3275
- const shellToggle = isRunning ? toggleExpanded : isDone && expanded ? toggleExpanded : void 0;
3844
+ const shellToggle = effectivelyRunning ? toggleExpanded : effectivelyDone && expanded ? toggleExpanded : void 0;
3276
3845
  const depthStyle = {
3277
3846
  "--lax-workflow-depth": depth,
3278
3847
  "--lax-workflow-indent-unit": workflowIndentUnit(depth)
3279
3848
  };
3280
- const showShellLine = !(isDone && !expanded);
3849
+ const showShellLine = !(effectivelyDone && !expanded);
3281
3850
  return /* @__PURE__ */ jsxs9(
3282
3851
  "div",
3283
3852
  {
@@ -3294,9 +3863,11 @@ function WorkflowSubworkflowContainer({
3294
3863
  description: node.subtitle || void 0,
3295
3864
  uiStatus,
3296
3865
  meta: buildWorkflowRootMeta(node),
3297
- statusLabel: mapWorkflowRootStatusLabel(node.status),
3866
+ statusLabel: mapWorkflowRootStatusLabel(
3867
+ effectivelyRunning ? "running" : node.status
3868
+ ),
3298
3869
  onToggle: shellToggle,
3299
- interactive: isRunning,
3870
+ interactive: effectivelyRunning,
3300
3871
  expanded,
3301
3872
  titleTestId: `${testId}-title`,
3302
3873
  descriptionTestId: `${testId}-desc`,
@@ -3313,15 +3884,16 @@ function WorkflowSubworkflowContainer({
3313
3884
  children
3314
3885
  }
3315
3886
  ) : null,
3316
- isDone && !expanded ? /* @__PURE__ */ jsx14(
3887
+ !expanded && children ? /* @__PURE__ */ jsx14(
3317
3888
  WorkflowExpandTrigger,
3318
3889
  {
3319
- summary: `${node.title} \xB7 ${buildDoneSummary(node)}`,
3890
+ summary: `${node.title} \xB7 ${effectivelyDone ? buildDoneSummary(node) : "running"}`,
3320
3891
  summaryTestId: `${testId}-title`,
3321
3892
  teaser: teaser || void 0,
3893
+ expandHint: effectivelyRunning ? "(click to expand subworkflow)" : void 0,
3322
3894
  onToggle: () => setExpanded(true),
3323
3895
  expanded: false,
3324
- testId: `${testId}-done`,
3896
+ testId: effectivelyDone ? `${testId}-done` : `${testId}-running-collapsed`,
3325
3897
  teaserTestId: teaser ? `${testId}-teaser` : void 0
3326
3898
  }
3327
3899
  ) : null
@@ -3345,8 +3917,9 @@ function collectSiblingItems(node, containersById) {
3345
3917
  (child) => child.scope === "item"
3346
3918
  );
3347
3919
  }
3348
- function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps) {
3920
+ function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps, options) {
3349
3921
  const loopTree = resolveLoopTree2(node, loopTreesBySessionId);
3922
+ const subtreeActive = options?.subtreeHasActiveWork ?? subtreeHasActiveWork(containerTree.containersById, node.container_id);
3350
3923
  switch (node.scope) {
3351
3924
  case "aggregate":
3352
3925
  return /* @__PURE__ */ jsx15(
@@ -3378,6 +3951,8 @@ function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, v
3378
3951
  node,
3379
3952
  loopTree,
3380
3953
  depth,
3954
+ suppressHostScopedLoop: options?.suppressHostScopedLoop,
3955
+ subtreeHasActiveWork: subtreeActive,
3381
3956
  ...viewProps
3382
3957
  },
3383
3958
  node.container_id
@@ -3402,6 +3977,11 @@ function renderItemSiblings(items, groupKey, depth, containerTree, loopTreesBySe
3402
3977
  }
3403
3978
  return null;
3404
3979
  }
3980
+ function stageHasSubworkflowChild(stageId, containerTree) {
3981
+ return collectChildContainers(containerTree.containersById, stageId).some(
3982
+ (node) => node.scope === "subworkflow"
3983
+ );
3984
+ }
3405
3985
  function renderContainerChildren(parentId, containerTree, loopTreesBySessionId, viewProps, depth) {
3406
3986
  const children = collectChildContainers(containerTree.containersById, parentId);
3407
3987
  const items = children.filter((node) => node.scope === "item");
@@ -3426,19 +4006,31 @@ function renderContainerNode(node, containerTree, loopTreesBySessionId, viewProp
3426
4006
  {
3427
4007
  node,
3428
4008
  depth,
4009
+ subtreeHasActiveWork: subtreeHasActiveWork(
4010
+ containerTree.containersById,
4011
+ node.container_id
4012
+ ),
3429
4013
  children: hasNested ? nested : null
3430
4014
  },
3431
4015
  node.container_id
3432
4016
  );
3433
4017
  }
3434
4018
  if (node.scope === "stage") {
4019
+ const suppressHostScopedLoop = stageHasSubworkflowChild(node.container_id, containerTree);
3435
4020
  return /* @__PURE__ */ jsxs10(
3436
4021
  "div",
3437
4022
  {
3438
4023
  className: "lax-workflow-nested-block",
3439
4024
  "data-testid": `lax-workflow-nested-${node.scope}-${node.scope_key}`,
3440
4025
  children: [
3441
- renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps),
4026
+ renderLeafContainer(
4027
+ node,
4028
+ depth,
4029
+ containerTree,
4030
+ loopTreesBySessionId,
4031
+ viewProps,
4032
+ { suppressHostScopedLoop }
4033
+ ),
3442
4034
  hasNested ? /* @__PURE__ */ jsx15("div", { className: "lax-workflow-container-children", children: nested }) : null
3443
4035
  ]
3444
4036
  },
@@ -3479,8 +4071,8 @@ function WorkflowStageListPanel({
3479
4071
  const [extraCompletedPages, setExtraCompletedPages] = useState6(0);
3480
4072
  const maxVisibleDoneCount = scale.completedStagePageSize + extraCompletedPages * scale.completedStagePageSize;
3481
4073
  const partition = useMemo7(
3482
- () => partitionWorkflowStages(stages, maxVisibleDoneCount),
3483
- [stages, maxVisibleDoneCount]
4074
+ () => partitionWorkflowStages(stages, maxVisibleDoneCount, containerTree.containersById),
4075
+ [stages, maxVisibleDoneCount, containerTree.containersById]
3484
4076
  );
3485
4077
  if (partition.visible.length === 0) return null;
3486
4078
  const showMoreCount = Math.min(
@@ -3578,17 +4170,25 @@ function WorkflowChrome({
3578
4170
  }
3579
4171
  ) });
3580
4172
  }
3581
- return /* @__PURE__ */ jsx15("div", { className: "lax-workflow-chrome", "data-testid": "lax-workflow-chrome", children: rootNodes.map((workflowNode) => /* @__PURE__ */ jsx15(WorkflowRootContainer, { node: workflowNode, children: renderWorkflowChildren(
3582
- workflowNode,
3583
- containerTree,
3584
- loopTreesBySessionId,
3585
- viewProps
3586
- ) }, workflowNode.container_id)) });
4173
+ return /* @__PURE__ */ jsx15("div", { className: "lax-workflow-chrome", "data-testid": "lax-workflow-chrome", children: rootNodes.map((workflowNode) => /* @__PURE__ */ jsx15(
4174
+ WorkflowRootContainer,
4175
+ {
4176
+ node: workflowNode,
4177
+ containersById: containerTree.containersById,
4178
+ children: renderWorkflowChildren(
4179
+ workflowNode,
4180
+ containerTree,
4181
+ loopTreesBySessionId,
4182
+ viewProps
4183
+ )
4184
+ },
4185
+ workflowNode.container_id
4186
+ )) });
3587
4187
  }
3588
4188
 
3589
4189
  // src/view/workflow/WorkflowTaskListFooter.tsx
3590
4190
  import { useMemo as useMemo8, useState as useState7 } from "react";
3591
- import { useStore as useStore4 } from "zustand";
4191
+ import { useStore as useStore3 } from "zustand";
3592
4192
 
3593
4193
  // src/view/workflow/workflowTaskListMerge.ts
3594
4194
  var STATUS_ORDER = {
@@ -3668,7 +4268,7 @@ function MessageResponse({ children }) {
3668
4268
  }
3669
4269
  function WorkflowTaskListFooter() {
3670
4270
  const store = useWorkflowSessionStoreApi();
3671
- const state = useStore4(store, (s) => s.state);
4271
+ const state = useStore3(store, (s) => s.state);
3672
4272
  const [expanded, setExpanded] = useState7(false);
3673
4273
  const footerState = useMemo8(() => deriveWorkflowTaskFooterState(state), [state]);
3674
4274
  const visible = shouldShowWorkflowTaskListFooter(state);
@@ -3708,7 +4308,7 @@ function WorkflowTaskListFooter() {
3708
4308
  }
3709
4309
 
3710
4310
  // src/view/workflow/WorkflowSession.tsx
3711
- import { useStore as useStore5 } from "zustand";
4311
+ import { useStore as useStore4 } from "zustand";
3712
4312
  import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
3713
4313
  function WorkflowSession({
3714
4314
  source,
@@ -3731,6 +4331,7 @@ function WorkflowSession({
3731
4331
  groupParallelTools = false,
3732
4332
  workflowScale,
3733
4333
  onRequestLoopReplay,
4334
+ onStreamComplete,
3734
4335
  children
3735
4336
  }) {
3736
4337
  const storeRef = useRef3(null);
@@ -3780,37 +4381,61 @@ function WorkflowSession({
3780
4381
  workspaceRoot
3781
4382
  ]
3782
4383
  );
3783
- const containerTree = useStore5(
4384
+ const containerTree = useStore4(
3784
4385
  storeRef.current,
3785
4386
  (s) => s.state.containerTree
3786
4387
  );
3787
- const loopTreesBySessionId = useStore5(
4388
+ const loopTreesBySessionId = useStore4(
3788
4389
  storeRef.current,
3789
4390
  (s) => s.state.loopTreesBySessionId
3790
4391
  );
4392
+ const sessionStatus = useStore4(
4393
+ storeRef.current,
4394
+ (s) => s.state.status
4395
+ );
4396
+ const internalErrors = useStore4(
4397
+ storeRef.current,
4398
+ (s) => s.state.internalErrors
4399
+ );
4400
+ const onErrorRef = useRef3(onError);
4401
+ const onStreamCompleteRef = useRef3(onStreamComplete);
4402
+ onErrorRef.current = onError;
4403
+ onStreamCompleteRef.current = onStreamComplete;
3791
4404
  useEffect8(() => {
3792
4405
  const store = storeRef.current;
3793
4406
  const controller = new AbortController();
3794
4407
  void source.start((event, ctx) => {
3795
4408
  store.getState().applyEvent(event, ctx);
3796
- }, controller.signal).catch((err) => {
4409
+ }, controller.signal).then(() => {
4410
+ store.getState().markStreamComplete();
4411
+ onStreamCompleteRef.current?.();
4412
+ }).catch((err) => {
3797
4413
  if (err instanceof SseTransportTerminalError) {
3798
- const { state } = store.getState();
3799
- if (state.status === "running") {
3800
- store.setState({ state: { ...state, status: "error" } });
3801
- }
4414
+ store.getState().markAsError(err);
4415
+ onErrorRef.current?.(err);
4416
+ } else if (err instanceof Error) {
4417
+ onErrorRef.current?.(err);
3802
4418
  }
3803
4419
  });
3804
4420
  return () => {
3805
4421
  controller.abort();
3806
4422
  };
3807
- }, [source, onError]);
4423
+ }, [source]);
3808
4424
  return /* @__PURE__ */ jsx17(WorkflowSessionStoreContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx17(WorkflowScaleContext.Provider, { value: scaleOptions, children: /* @__PURE__ */ jsx17(WorkflowLoopReplayProvider, { onRequestLoopReplay, children: /* @__PURE__ */ jsx17(InteractionBusContext.Provider, { value: busRef, children: /* @__PURE__ */ jsx17(NodeRegistryContext.Provider, { value: registryRef, children: /* @__PURE__ */ jsx17(MarkdownRendererContext.Provider, { value: markdownRenderer, children: /* @__PURE__ */ jsx17(ToolDisplayOptionsContext.Provider, { value: toolDisplayRef, children: /* @__PURE__ */ jsx17(SessionViewOptionsContext.Provider, { value: sessionViewRef, children: /* @__PURE__ */ jsxs12(
3809
4425
  "div",
3810
4426
  {
3811
4427
  className: "lax-agent-session lax-workflow-session",
3812
4428
  "data-testid": "lax-workflow-session",
3813
4429
  children: [
4430
+ sessionStatus === "error" || internalErrors.length > 0 ? /* @__PURE__ */ jsx17(
4431
+ "div",
4432
+ {
4433
+ className: "lax-workflow-error",
4434
+ "data-testid": "lax-workflow-error",
4435
+ role: "alert",
4436
+ children: internalErrors[internalErrors.length - 1]?.message ?? "Workflow stream error"
4437
+ }
4438
+ ) : null,
3814
4439
  /* @__PURE__ */ jsx17(
3815
4440
  WorkflowChrome,
3816
4441
  {
@@ -4018,6 +4643,7 @@ export {
4018
4643
  createReplaySource,
4019
4644
  createSessionStore,
4020
4645
  createWorkflowSessionStore,
4646
+ deriveLoopSessionIdFromAgentEvent,
4021
4647
  displayPath,
4022
4648
  entryKey,
4023
4649
  filterAgentLoopReplayEvents,