langchain_agentx_stream_ui 0.2.7 → 0.3.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-CUKNUCGU.js";
26
+ } from "./chunk-ISQ7JWGD.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-TCKOXX7O.js";
36
+ import "./chunk-6Z4ZF36Z.js";
37
37
  import {
38
38
  CollapseKind,
39
39
  CollapsedExploreNode,
@@ -72,7 +72,6 @@ import {
72
72
  createEmptyTree,
73
73
  createProjectionContext,
74
74
  entryKey,
75
- formatExploreSummaryText,
76
75
  formatHookFinishedSummary,
77
76
  formatShellProgressSuffix,
78
77
  formatTaskFooterLabel,
@@ -111,7 +110,7 @@ import {
111
110
  useSessionStatus,
112
111
  useSessionViewOptions,
113
112
  useTimeline
114
- } from "./chunk-SUEMZC4U.js";
113
+ } from "./chunk-6ORA5RMV.js";
115
114
  import {
116
115
  Agent,
117
116
  AgentToolBody,
@@ -137,8 +136,9 @@ import {
137
136
  Write,
138
137
  WriteToolBody,
139
138
  createDefaultToolRegistry,
140
- formatAgentTitle
141
- } from "./chunk-7CLAU74Y.js";
139
+ formatAgentTitle,
140
+ formatExploreSummaryText
141
+ } from "./chunk-FL2G7SV3.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";
@@ -184,7 +184,8 @@ function safeReduce(tree, event, eventIndex, options) {
184
184
  function createSessionStore(initialTree = createEmptyTree(), storeOptions) {
185
185
  const reduceOpts = {
186
186
  tierOverrides: storeOptions?.tierOverrides,
187
- collectDebug: storeOptions?.debug === true
187
+ collectDebug: storeOptions?.debug === true,
188
+ subagentLeakGate: storeOptions?.subagentLeakGate === true
188
189
  };
189
190
  const initialEventCount = storeOptions?.initialEventCount ?? 0;
190
191
  const seenEventIds = new Set(storeOptions?.initialSeenEventIds);
@@ -317,6 +318,15 @@ var PROFILE_PRESETS = {
317
318
  ...BOUNDED_DEFAULTS
318
319
  },
319
320
  terminal: WORKFLOW_TERMINAL_WITH_FALLBACK
321
+ },
322
+ "workflow-tail": {
323
+ profile: "workflow-tail",
324
+ replay: "none",
325
+ reconnect: {
326
+ mode: "bounded",
327
+ ...BOUNDED_DEFAULTS
328
+ },
329
+ terminal: WORKFLOW_TERMINAL_WITH_FALLBACK
320
330
  }
321
331
  };
322
332
  function legacyTerminal(expectedApplicationKind) {
@@ -778,19 +788,22 @@ function AgentSession({
778
788
  exploreFullscreenBash = true,
779
789
  memoryDir = null,
780
790
  workspaceRoot = null,
791
+ subagentLeakGate = false,
781
792
  children
782
793
  }) {
783
794
  const storeRef = useRef2(null);
784
795
  if (storeRef.current === null) {
785
796
  const replayOpts = {
786
797
  tierOverrides: _tierOverrides,
787
- collectDebug: debug
798
+ collectDebug: debug,
799
+ subagentLeakGate
788
800
  };
789
801
  const initialTree = initialEvents && initialEvents.length > 0 ? replayEvents(initialEvents, replayOpts) : void 0;
790
802
  storeRef.current = createSessionStore(initialTree, {
791
803
  tierOverrides: _tierOverrides,
792
804
  debug,
793
- initialEventCount: initialEvents?.length ?? 0
805
+ initialEventCount: initialEvents?.length ?? 0,
806
+ subagentLeakGate
794
807
  });
795
808
  }
796
809
  const registryRef = useMemo(() => registry ?? createDefaultRegistry(), [registry]);
@@ -815,7 +828,8 @@ function AgentSession({
815
828
  verbose,
816
829
  exploreFullscreenBash,
817
830
  memoryDir,
818
- workspaceRoot
831
+ workspaceRoot,
832
+ subagentLeakGate
819
833
  }),
820
834
  [
821
835
  groupParallelTools,
@@ -825,7 +839,8 @@ function AgentSession({
825
839
  verbose,
826
840
  exploreFullscreenBash,
827
841
  memoryDir,
828
- workspaceRoot
842
+ workspaceRoot,
843
+ subagentLeakGate
829
844
  ]
830
845
  );
831
846
  const onErrorRef = useRef2(onError);
@@ -864,11 +879,85 @@ function AgentSession({
864
879
  }
865
880
 
866
881
  // src/view/workflow/WorkflowSession.tsx
867
- import { useEffect as useEffect8, useMemo as useMemo9, useRef as useRef3 } from "react";
882
+ import { useEffect as useEffect10, useMemo as useMemo9, useRef as useRef4 } from "react";
868
883
 
869
884
  // src/core/context/WorkflowSessionContext.tsx
870
885
  import { createContext, useContext } from "react";
871
886
  import { useStore } from "zustand";
887
+
888
+ // src/core/workflow/workflowAuthoritySelectors.ts
889
+ var AUTHORITY_TERMINAL = /* @__PURE__ */ new Set([
890
+ "done",
891
+ "error",
892
+ "canceled",
893
+ "partial_success"
894
+ ]);
895
+ function isAuthorityTerminalStatus(status) {
896
+ return status != null && AUTHORITY_TERMINAL.has(status);
897
+ }
898
+ function isAuthorityTerminal(state) {
899
+ return isAuthorityTerminalStatus(state.authority?.status);
900
+ }
901
+ function shouldShowGlobalSpinner(state) {
902
+ const auth = state.authority?.status;
903
+ if (auth === "canceled") return false;
904
+ if (isAuthorityTerminalStatus(auth)) return false;
905
+ if (state.display.awaitingAuthority) return false;
906
+ if (auth == null || auth === "pending" || auth === "running") {
907
+ return true;
908
+ }
909
+ const proj = state.display.projectionStatus;
910
+ return proj === "running" || proj === "connecting";
911
+ }
912
+ function resolveWorkflowBanner(state) {
913
+ if (state.display.authoritySubscribeFailed) {
914
+ return { kind: "sync_failed", message: "\u72B6\u6001\u540C\u6B65\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5" };
915
+ }
916
+ if (state.display.authoritySyncFailed) {
917
+ return { kind: "sync_failed", message: "\u540C\u6B65\u72B6\u6001\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0" };
918
+ }
919
+ const auth = state.authority?.status;
920
+ if (auth === "error") {
921
+ return { kind: "authority_error", message: "Workflow \u6267\u884C\u5931\u8D25" };
922
+ }
923
+ if (state.snapshotHydrateFailed) {
924
+ return {
925
+ kind: "snapshot_unavailable",
926
+ message: "\u65E0\u6CD5\u52A0\u8F7D projection snapshot\uFF0C\u5C55\u793A\u53EF\u80FD\u4E0D\u5B8C\u6574"
927
+ };
928
+ }
929
+ if (state.display.awaitingAuthority) {
930
+ return { kind: "awaiting_authority", message: "\u6536\u5C3E\u4E2D\u2026" };
931
+ }
932
+ if (auth === "partial_success") {
933
+ const forced = state.display.terminalKind === "forced_closeout";
934
+ return {
935
+ kind: "partial_success",
936
+ message: forced ? "\u90E8\u5206\u5B8C\u6210\uFF08\u90E8\u5206\u5BB9\u5668\u672A\u6B63\u5E38\u7ED3\u675F\uFF09" : "\u90E8\u5206\u5B8C\u6210\uFF08\u6240\u6709\u5BB9\u5668\u6B63\u5E38\u7ED3\u675F\uFF09"
937
+ };
938
+ }
939
+ const meta = state.authority?.metadata;
940
+ if (auth === "done" && meta?.projection_status === "degraded") {
941
+ const dropped = meta.projection_dropped_count;
942
+ const suffix = dropped != null && dropped > 0 ? `\uFF08\u7EA6 ${dropped} \u6761\u4E8B\u4EF6\u672A\u5B8C\u6574\u5C55\u793A\uFF09` : "";
943
+ return {
944
+ kind: "projection_degraded",
945
+ message: `\u5C55\u793A\u53EF\u80FD\u4E0D\u5B8C\u6574${suffix}`
946
+ };
947
+ }
948
+ if (state.display.terminalKind === "forced_closeout" && auth === "done") {
949
+ return { kind: "forced_closeout", message: "Workflow \u5DF2\u7ED3\u675F\uFF08\u90E8\u5206\u5BB9\u5668\u672A\u6B63\u5E38\u5173\u95ED\uFF09" };
950
+ }
951
+ return { kind: "none", message: "" };
952
+ }
953
+ function getDisplayProjectionStatus(state) {
954
+ return state.display.projectionStatus;
955
+ }
956
+ function applyAuthoritySnapshotToState(state, snapshot) {
957
+ return { ...state, authority: snapshot };
958
+ }
959
+
960
+ // src/core/context/WorkflowSessionContext.tsx
872
961
  var WorkflowSessionStoreContext = createContext(null);
873
962
  function useWorkflowSessionStoreApi() {
874
963
  const store = useContext(WorkflowSessionStoreContext);
@@ -879,7 +968,7 @@ function useWorkflowSessionStoreApi() {
879
968
  }
880
969
  function useWorkflowSessionStatus() {
881
970
  const store = useWorkflowSessionStoreApi();
882
- return useStore(store, (s) => s.state.status);
971
+ return useStore(store, (s) => getDisplayProjectionStatus(s.state));
883
972
  }
884
973
 
885
974
  // src/core/context/WorkflowLoopReplayContext.tsx
@@ -944,6 +1033,17 @@ function containerTreeToSnapshot(tree) {
944
1033
  }
945
1034
 
946
1035
  // src/types/workflowSession.ts
1036
+ function createEmptyWorkflowDisplayProjection() {
1037
+ return {
1038
+ streamEnded: false,
1039
+ projectionStatus: "connecting",
1040
+ terminalKind: null,
1041
+ openChildIdentities: [],
1042
+ awaitingAuthority: false,
1043
+ authoritySyncFailed: false,
1044
+ authoritySubscribeFailed: false
1045
+ };
1046
+ }
947
1047
  function createEmptyWorkflowProgress() {
948
1048
  return {
949
1049
  workflowId: null,
@@ -955,9 +1055,8 @@ function createEmptyWorkflowProgress() {
955
1055
  }
956
1056
  function createEmptyWorkflowSessionState() {
957
1057
  return {
958
- status: "connecting",
959
- terminalKind: null,
960
- openChildIdentities: [],
1058
+ authority: null,
1059
+ display: createEmptyWorkflowDisplayProjection(),
961
1060
  meta: { workflowRunId: null, startedAt: null, lastEventId: null },
962
1061
  containerTree: createEmptyWorkflowContainerTreeState(),
963
1062
  workflowProgress: createEmptyWorkflowProgress(),
@@ -965,6 +1064,10 @@ function createEmptyWorkflowSessionState() {
965
1064
  stageDoneSummariesByContainerId: {},
966
1065
  hotCompletedLoopSessionIds: [],
967
1066
  activeLoopSessionId: null,
1067
+ projectionCursor: null,
1068
+ snapshotVersion: null,
1069
+ reconnectSuppressedReason: null,
1070
+ snapshotHydrateFailed: false,
968
1071
  openScopeStack: [],
969
1072
  internalErrors: []
970
1073
  };
@@ -2150,7 +2253,10 @@ function readOpenChildIdentities(data) {
2150
2253
  if (!Array.isArray(raw)) return [];
2151
2254
  return raw.filter((item) => typeof item === "string");
2152
2255
  }
2153
- function isWorkflowSessionTerminal(status) {
2256
+ function projectionStatusFromTerminalKind(kind, isWorkflowFailedEvent) {
2257
+ return sessionStatusFromTerminalKind(kind, isWorkflowFailedEvent);
2258
+ }
2259
+ function isDisplayProjectionTerminal(status) {
2154
2260
  return status === "done" || status === "done_with_warning" || status === "error";
2155
2261
  }
2156
2262
 
@@ -2523,18 +2629,16 @@ function applyStructureEvent(state, event) {
2523
2629
  containerTree,
2524
2630
  state.workflowProgress
2525
2631
  );
2526
- let status = nextState.status;
2527
- if (workflowProgress.status === "done") status = "done";
2528
- else if (workflowProgress.status === "error") status = "error";
2529
- else if (status === "connecting") status = "running";
2530
- let terminalKind = state.terminalKind;
2531
- let openChildIdentities = state.openChildIdentities;
2632
+ let projectionStatus = nextState.display.projectionStatus;
2633
+ if (projectionStatus === "connecting") projectionStatus = "running";
2634
+ let terminalKind = state.display.terminalKind;
2635
+ let openChildIdentities = state.display.openChildIdentities;
2532
2636
  if (projectedEvent.event_type === "workflow-end" || projectedEvent.event_type === "workflow-failed") {
2533
2637
  const resolved = resolveWorkflowTerminalKind(
2534
2638
  structureData,
2535
2639
  projectedEvent.event_type
2536
2640
  );
2537
- status = sessionStatusFromTerminalKind(
2641
+ projectionStatus = projectionStatusFromTerminalKind(
2538
2642
  resolved,
2539
2643
  projectedEvent.event_type === "workflow-failed"
2540
2644
  );
@@ -2543,9 +2647,12 @@ function applyStructureEvent(state, event) {
2543
2647
  }
2544
2648
  const base = {
2545
2649
  ...nextState,
2546
- status,
2547
- terminalKind,
2548
- openChildIdentities,
2650
+ display: {
2651
+ ...nextState.display,
2652
+ projectionStatus,
2653
+ terminalKind,
2654
+ openChildIdentities
2655
+ },
2549
2656
  containerTree,
2550
2657
  workflowProgress
2551
2658
  };
@@ -2613,11 +2720,9 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
2613
2720
  next.containerTree,
2614
2721
  next.workflowProgress
2615
2722
  );
2616
- let status = next.status;
2617
- if (workflowProgress.status === "done") status = "done";
2618
- else if (workflowProgress.status === "error") status = "error";
2619
- else if (reducedLoop.status === "error") status = "error";
2620
- else if (status === "connecting") status = "running";
2723
+ let projectionStatus = next.display.projectionStatus;
2724
+ if (reducedLoop.status === "error") projectionStatus = "error";
2725
+ else if (projectionStatus === "connecting") projectionStatus = "running";
2621
2726
  return {
2622
2727
  ...next,
2623
2728
  workflowProgress,
@@ -2626,61 +2731,38 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
2626
2731
  [loopSessionId]: reducedLoop
2627
2732
  },
2628
2733
  activeLoopSessionId: loopSessionId,
2629
- status
2734
+ display: {
2735
+ ...next.display,
2736
+ projectionStatus
2737
+ }
2630
2738
  };
2631
2739
  }
2632
- function finalizeWorkflowSessionOnStreamComplete(state) {
2633
- if (isWorkflowSessionTerminal(state.status)) {
2634
- return { ...state, activeLoopSessionId: null };
2635
- }
2636
- const tree = state.containerTree;
2637
- const rootId = tree.rootContainerIds[0];
2638
- if (!rootId) {
2639
- return { ...state, status: "done", activeLoopSessionId: null };
2640
- }
2641
- const root = tree.containersById[rootId];
2642
- if (!root) {
2643
- return { ...state, status: "done", activeLoopSessionId: null };
2740
+ function finalizeWorkflowDisplayOnStreamEnded(state) {
2741
+ if (isDisplayProjectionTerminal(state.display.projectionStatus)) {
2742
+ return {
2743
+ ...state,
2744
+ display: { ...state.display, streamEnded: true },
2745
+ activeLoopSessionId: null
2746
+ };
2644
2747
  }
2645
- const topLevelStages = Object.values(tree.containersById).filter(
2646
- (node) => node.scope === "stage" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
2647
- );
2648
- const progressTotal = root.display?.progress_total ?? (topLevelStages.reduce(
2649
- (max, s) => Math.max(max, s.display?.progress_total ?? 0),
2650
- 0
2651
- ) || topLevelStages.length);
2652
- const settledStageCount = topLevelStages.filter(
2653
- (s) => s.status === "completed" || s.status === "failed" || s.status === "skipped"
2654
- ).length;
2655
- const progressCurrent = Math.max(
2656
- root.display?.progress_current ?? 0,
2657
- settledStageCount
2658
- );
2659
- const rootParallelItems = Object.values(tree.containersById).filter(
2660
- (node) => node.scope === "item" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
2661
- );
2662
- const rootParallelSettled = topLevelStages.length === 0 && rootParallelItems.length > 0 && rootParallelItems.every(
2663
- (item) => item.status === "completed" || item.status === "failed" || item.status === "skipped"
2664
- ) && !Object.values(tree.containersById).some(
2665
- (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")
2666
- );
2667
- const workflowVisuallyComplete = root.status === "completed" || root.status === "failed" || progressTotal > 0 && progressCurrent >= progressTotal || rootParallelSettled;
2668
- if (!workflowVisuallyComplete) {
2669
- return state;
2748
+ const withStreamEnded = {
2749
+ ...state,
2750
+ display: { ...state.display, streamEnded: true },
2751
+ activeLoopSessionId: null
2752
+ };
2753
+ if (isAuthorityTerminalStatus(state.authority?.status)) {
2754
+ return forceSettleRunningContainersForDisplay(withStreamEnded);
2670
2755
  }
2756
+ return withStreamEnded;
2757
+ }
2758
+ function forceSettleRunningContainersForDisplay(state) {
2759
+ const tree = state.containerTree;
2671
2760
  let containersById = { ...tree.containersById };
2672
2761
  for (const [id, node] of Object.entries(containersById)) {
2673
2762
  if (node.status === "running" || node.status === "pending") {
2674
2763
  containersById[id] = { ...node, status: "completed", is_open: false };
2675
2764
  }
2676
2765
  }
2677
- const settledRoot = containersById[rootId] ?? root;
2678
- if (settledRoot.status !== "completed" && settledRoot.status !== "failed") {
2679
- containersById = {
2680
- ...containersById,
2681
- [rootId]: { ...settledRoot, status: "completed", is_open: false }
2682
- };
2683
- }
2684
2766
  const loopTreesBySessionId = { ...state.loopTreesBySessionId };
2685
2767
  for (const [loopSessionId, loopTree] of Object.entries(loopTreesBySessionId)) {
2686
2768
  if (loopTree.status === "running" || loopTree.status === "connecting") {
@@ -2694,15 +2776,9 @@ function finalizeWorkflowSessionOnStreamComplete(state) {
2694
2776
  (id) => containersById[id]?.is_open
2695
2777
  )
2696
2778
  };
2697
- const workflowProgress = syncWorkflowProgressFromContainerTree(
2698
- containerTree,
2699
- state.workflowProgress
2700
- );
2701
2779
  return {
2702
2780
  ...state,
2703
- status: workflowProgress.status === "error" ? "error" : "done",
2704
2781
  containerTree,
2705
- workflowProgress,
2706
2782
  loopTreesBySessionId,
2707
2783
  activeLoopSessionId: null
2708
2784
  };
@@ -2714,6 +2790,41 @@ function reduceWorkflowEvents(events, options) {
2714
2790
  );
2715
2791
  }
2716
2792
 
2793
+ // src/core/workflow/hydrateWorkflowSnapshot.ts
2794
+ function isAtOrBeforeProjectionCursor(projectionCursor, eventId) {
2795
+ if (!projectionCursor || !eventId) return false;
2796
+ const cursorNum = Number.parseInt(projectionCursor, 10);
2797
+ const eventNum = Number.parseInt(eventId, 10);
2798
+ if (!Number.isNaN(cursorNum) && !Number.isNaN(eventNum)) {
2799
+ return eventNum <= cursorNum;
2800
+ }
2801
+ return eventId <= projectionCursor;
2802
+ }
2803
+ function hydrateWorkflowFromSnapshot(state, snapshot, options = {}) {
2804
+ const authority = options.preserveAuthority ? state.authority : null;
2805
+ return {
2806
+ ...createEmptyWorkflowSessionState(),
2807
+ authority,
2808
+ display: {
2809
+ ...snapshot.display,
2810
+ streamEnded: false
2811
+ },
2812
+ containerTree: structuredClone(snapshot.containerTree),
2813
+ projectionCursor: snapshot.cursor,
2814
+ snapshotVersion: snapshot.snapshotVersion,
2815
+ meta: {
2816
+ ...state.meta,
2817
+ workflowRunId: snapshot.taskId,
2818
+ lastEventId: snapshot.cursor,
2819
+ startedAt: state.meta.startedAt ?? snapshot.capturedAt
2820
+ },
2821
+ loopTreesBySessionId: {},
2822
+ internalErrors: [],
2823
+ reconnectSuppressedReason: null,
2824
+ snapshotHydrateFailed: false
2825
+ };
2826
+ }
2827
+
2717
2828
  // src/core/workflowSessionStore.ts
2718
2829
  var DEFAULT_HYDRATE_CHUNK_SIZE = 50;
2719
2830
  function nextAnimationFrame() {
@@ -2764,7 +2875,10 @@ function finalizeWorkflowSessionOnStreamError(state, message, eventIndex, stack)
2764
2875
  }
2765
2876
  return {
2766
2877
  ...state,
2767
- status: "error",
2878
+ display: {
2879
+ ...state.display,
2880
+ projectionStatus: "error"
2881
+ },
2768
2882
  containerTree: {
2769
2883
  ...state.containerTree,
2770
2884
  containersById,
@@ -2799,8 +2913,12 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2799
2913
  hydratingLoopSessionIds: {},
2800
2914
  applyEvent(event, ctx) {
2801
2915
  const sseEventId = ctx?.sseEventId;
2802
- if (shouldSkipDuplicateEvent(seenEventIds, sseEventId)) return;
2803
2916
  const { state, eventCount } = get();
2917
+ if (isAtOrBeforeProjectionCursor(state.projectionCursor, sseEventId)) {
2918
+ markEventIdSeen(seenEventIds, sseEventId);
2919
+ return;
2920
+ }
2921
+ if (shouldSkipDuplicateEvent(seenEventIds, sseEventId)) return;
2804
2922
  const { state: reduced } = safeReduceWorkflow(state, event, eventCount, reduceOpts);
2805
2923
  const evicted = finalizeState(reduced);
2806
2924
  const nextMeta = sseEventId ? { ...evicted.meta, lastEventId: sseEventId } : evicted.meta;
@@ -2809,7 +2927,10 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2809
2927
  state: {
2810
2928
  ...evicted,
2811
2929
  meta: nextMeta,
2812
- status: evicted.status === "connecting" ? "running" : evicted.status
2930
+ display: {
2931
+ ...evicted.display,
2932
+ projectionStatus: evicted.display.projectionStatus === "connecting" ? "running" : evicted.display.projectionStatus
2933
+ }
2813
2934
  },
2814
2935
  eventCount: eventCount + 1
2815
2936
  });
@@ -2839,13 +2960,59 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2839
2960
  )
2840
2961
  });
2841
2962
  },
2842
- markStreamComplete() {
2963
+ markStreamEnded() {
2843
2964
  set((current) => ({
2844
2965
  state: finalizeState(
2845
- finalizeWorkflowSessionOnStreamComplete(current.state)
2966
+ finalizeWorkflowDisplayOnStreamEnded(current.state)
2846
2967
  )
2847
2968
  }));
2848
2969
  },
2970
+ applyAuthoritySnapshot(snapshot) {
2971
+ set((current) => ({
2972
+ state: {
2973
+ ...current.state,
2974
+ authority: snapshot,
2975
+ display: {
2976
+ ...current.state.display,
2977
+ awaitingAuthority: false,
2978
+ authoritySyncFailed: false
2979
+ }
2980
+ }
2981
+ }));
2982
+ },
2983
+ setAwaitingAuthority(value) {
2984
+ set((current) => ({
2985
+ state: {
2986
+ ...current.state,
2987
+ display: {
2988
+ ...current.state.display,
2989
+ awaitingAuthority: value
2990
+ }
2991
+ }
2992
+ }));
2993
+ },
2994
+ setAuthoritySyncFailed(value) {
2995
+ set((current) => ({
2996
+ state: {
2997
+ ...current.state,
2998
+ display: {
2999
+ ...current.state.display,
3000
+ authoritySyncFailed: value
3001
+ }
3002
+ }
3003
+ }));
3004
+ },
3005
+ setAuthoritySubscribeFailed(value) {
3006
+ set((current) => ({
3007
+ state: {
3008
+ ...current.state,
3009
+ display: {
3010
+ ...current.state.display,
3011
+ authoritySubscribeFailed: value
3012
+ }
3013
+ }
3014
+ }));
3015
+ },
2849
3016
  /**
2850
3017
  * P2:将 completed loop 加入 LRU 热缓存并淘汰溢出。
2851
3018
  * running 保护由 collectProtectedLoopSessionIds 按状态自动覆盖,不再依赖永久 pin。
@@ -2948,15 +3115,114 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
2948
3115
  loopSessionId
2949
3116
  )
2950
3117
  }));
3118
+ },
3119
+ hydrateFromSnapshot(snapshot, options) {
3120
+ const { state } = get();
3121
+ const next = finalizeState(
3122
+ hydrateWorkflowFromSnapshot(state, snapshot, options)
3123
+ );
3124
+ markEventIdSeen(seenEventIds, snapshot.cursor);
3125
+ set({ state: next });
3126
+ },
3127
+ markSnapshotHydrateFailed() {
3128
+ set((current) => ({
3129
+ state: {
3130
+ ...current.state,
3131
+ snapshotHydrateFailed: true,
3132
+ reconnectSuppressedReason: "snapshot_unavailable"
3133
+ }
3134
+ }));
2951
3135
  }
2952
3136
  }));
2953
3137
  }
2954
3138
 
3139
+ // src/core/workflow/useWorkflowAuthorityBinding.ts
3140
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
3141
+
3142
+ // src/core/workflow/normalizeAuthoritySnapshot.ts
3143
+ var KNOWN = /* @__PURE__ */ new Set([
3144
+ "pending",
3145
+ "running",
3146
+ "done",
3147
+ "error",
3148
+ "canceled",
3149
+ "partial_success"
3150
+ ]);
3151
+ function normalizeAuthoritySnapshot(snapshot) {
3152
+ if (KNOWN.has(snapshot.status)) {
3153
+ return snapshot;
3154
+ }
3155
+ if (import.meta.env?.DEV) {
3156
+ console.warn(
3157
+ `[langchain_agentx_stream_ui] unknown authority.status "${String(snapshot.status)}"; defaulting to running`
3158
+ );
3159
+ }
3160
+ return { ...snapshot, status: "running" };
3161
+ }
3162
+
3163
+ // src/core/workflow/useWorkflowAuthorityBinding.ts
3164
+ function useWorkflowAuthorityBinding(store, authoritySource, callbacks) {
3165
+ const onAuthorityTerminalRef = useRef3(callbacks.onAuthorityTerminal);
3166
+ onAuthorityTerminalRef.current = callbacks.onAuthorityTerminal;
3167
+ const terminalFiredRef = useRef3(false);
3168
+ useEffect3(() => {
3169
+ terminalFiredRef.current = false;
3170
+ const apply = (raw) => {
3171
+ const snapshot = normalizeAuthoritySnapshot(raw);
3172
+ store.getState().applyAuthoritySnapshot(snapshot);
3173
+ if (!terminalFiredRef.current && isAuthorityTerminalStatus(snapshot.status)) {
3174
+ terminalFiredRef.current = true;
3175
+ onAuthorityTerminalRef.current?.(snapshot);
3176
+ }
3177
+ };
3178
+ const initial = authoritySource.getSnapshot();
3179
+ if (initial) {
3180
+ apply(initial);
3181
+ }
3182
+ let unsubscribe;
3183
+ try {
3184
+ unsubscribe = authoritySource.subscribe(apply);
3185
+ } catch (err) {
3186
+ if (import.meta.env?.DEV) {
3187
+ console.error("[langchain_agentx_stream_ui] authoritySource.subscribe failed:", err);
3188
+ }
3189
+ store.getState().setAuthoritySubscribeFailed(true);
3190
+ }
3191
+ return () => {
3192
+ unsubscribe?.();
3193
+ };
3194
+ }, [store, authoritySource]);
3195
+ }
3196
+
3197
+ // src/core/workflow/useWorkflowAwaitingAuthorityTimers.ts
3198
+ import { useEffect as useEffect4 } from "react";
3199
+ var AWAITING_MS = 3e4;
3200
+ var SYNC_FAILED_MS = 12e4;
3201
+ function useWorkflowAwaitingAuthorityTimers(store, streamEnded, authorityStatus) {
3202
+ useEffect4(() => {
3203
+ if (!streamEnded || authorityStatus !== "running") {
3204
+ store.getState().setAwaitingAuthority(false);
3205
+ store.getState().setAuthoritySyncFailed(false);
3206
+ return;
3207
+ }
3208
+ const tAwait = window.setTimeout(() => {
3209
+ store.getState().setAwaitingAuthority(true);
3210
+ }, AWAITING_MS);
3211
+ const tFail = window.setTimeout(() => {
3212
+ store.getState().setAuthoritySyncFailed(true);
3213
+ }, SYNC_FAILED_MS);
3214
+ return () => {
3215
+ window.clearTimeout(tAwait);
3216
+ window.clearTimeout(tFail);
3217
+ };
3218
+ }, [store, streamEnded, authorityStatus]);
3219
+ }
3220
+
2955
3221
  // src/view/workflow/WorkflowChrome.tsx
2956
3222
  import { useMemo as useMemo7, useState as useState6 } from "react";
2957
3223
 
2958
3224
  // src/view/workflow/WorkflowAggregateContainer.tsx
2959
- import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState, memo } from "react";
3225
+ import { useCallback, useEffect as useEffect5, useMemo as useMemo2, useState, memo } from "react";
2960
3226
 
2961
3227
  // src/view/workflow/WorkflowContainerLine.tsx
2962
3228
  import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -3366,7 +3632,7 @@ function WorkflowAggregateContainerInner({
3366
3632
  const uiStatus = mapAggregateUiStatus(node);
3367
3633
  const statusLabel = buildAggregateStatusLabel(node, siblingItems);
3368
3634
  const [expanded, setExpanded] = useState(isRunning);
3369
- useEffect3(() => {
3635
+ useEffect5(() => {
3370
3636
  if (isRunning) setExpanded(true);
3371
3637
  if (isDone) setExpanded(false);
3372
3638
  }, [isRunning, isDone]);
@@ -3485,7 +3751,7 @@ var WorkflowAggregateContainer = memo(
3485
3751
  );
3486
3752
 
3487
3753
  // src/view/workflow/WorkflowParallelGroup.tsx
3488
- import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3, memo as memo3 } from "react";
3754
+ import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo4, useState as useState3, memo as memo3 } from "react";
3489
3755
 
3490
3756
  // src/core/workflow/workflowParallelGroupMetrics.ts
3491
3757
  var ACTIVE_ITEM_STATUSES = /* @__PURE__ */ new Set([
@@ -3579,7 +3845,7 @@ function formatParallelGroupSummary(metrics) {
3579
3845
  }
3580
3846
 
3581
3847
  // src/view/workflow/WorkflowStageContainer.tsx
3582
- import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useState as useState2, memo as memo2 } from "react";
3848
+ import { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMemo3, useState as useState2, memo as memo2 } from "react";
3583
3849
  import { useStore as useStore2 } from "zustand";
3584
3850
 
3585
3851
  // src/core/workflow/loopTreeUtils.ts
@@ -3666,7 +3932,7 @@ function WorkflowStageContainerInner({
3666
3932
  const doneSummary = doneSummaryFromTree ?? (isDone ? cachedSummary : null);
3667
3933
  const shouldAutoExpandRunning = effectivelyRunning && !isSkipped;
3668
3934
  const [expanded, setExpanded] = useState2(shouldAutoExpandRunning);
3669
- useEffect4(() => {
3935
+ useEffect6(() => {
3670
3936
  if (shouldAutoExpandRunning) {
3671
3937
  setExpanded(true);
3672
3938
  return;
@@ -3691,7 +3957,7 @@ function WorkflowStageContainerInner({
3691
3957
  return next;
3692
3958
  });
3693
3959
  }, [isDone, node.loopSessionId, loopTree, requestLoopHydration]);
3694
- useEffect4(() => {
3960
+ useEffect6(() => {
3695
3961
  const onKeyDown = (event) => {
3696
3962
  if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "o") return;
3697
3963
  setExpanded(true);
@@ -3844,7 +4110,7 @@ function WorkflowParallelGroupInner({
3844
4110
  );
3845
4111
  const allSettled = metrics.total > 0 && metrics.running === 0 && metrics.completed + metrics.failed >= metrics.total;
3846
4112
  const [expanded, setExpanded] = useState3(hasActiveWork);
3847
- useEffect5(() => {
4113
+ useEffect7(() => {
3848
4114
  if (hasActiveWork) {
3849
4115
  setExpanded(true);
3850
4116
  return;
@@ -3856,7 +4122,7 @@ function WorkflowParallelGroupInner({
3856
4122
  const toggleExpanded = useCallback3(() => {
3857
4123
  setExpanded((prev) => !prev);
3858
4124
  }, []);
3859
- useEffect5(() => {
4125
+ useEffect7(() => {
3860
4126
  const onKeyDown = (event) => {
3861
4127
  if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "o") return;
3862
4128
  setExpanded(true);
@@ -3956,7 +4222,7 @@ function WorkflowRootContainer({
3956
4222
  }
3957
4223
 
3958
4224
  // src/view/workflow/WorkflowRouteContainer.tsx
3959
- import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo5, useState as useState4 } from "react";
4225
+ import { useCallback as useCallback4, useEffect as useEffect8, useMemo as useMemo5, useState as useState4 } from "react";
3960
4226
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
3961
4227
  function WorkflowRouteContainer({
3962
4228
  node,
@@ -3981,7 +4247,7 @@ function WorkflowRouteContainer({
3981
4247
  const expandHint = doneSummary ? formatTeaserExpandHint(doneSummary.extraLines, verbose) : null;
3982
4248
  const doneLabel = doneSummary?.label ?? "done";
3983
4249
  const [expanded, setExpanded] = useState4(isRunning);
3984
- useEffect6(() => {
4250
+ useEffect8(() => {
3985
4251
  if (isSkipped) return;
3986
4252
  if (isRunning) setExpanded(true);
3987
4253
  if (isDone) setExpanded(false);
@@ -4057,7 +4323,7 @@ function WorkflowRouteContainer({
4057
4323
  }
4058
4324
 
4059
4325
  // src/view/workflow/WorkflowSubworkflowContainer.tsx
4060
- import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo6, useState as useState5 } from "react";
4326
+ import { useCallback as useCallback5, useEffect as useEffect9, useMemo as useMemo6, useState as useState5 } from "react";
4061
4327
  import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
4062
4328
  function buildDoneSummary(node) {
4063
4329
  const display = node.display;
@@ -4088,7 +4354,7 @@ function WorkflowSubworkflowContainer({
4088
4354
  return node.content_blocks[node.content_blocks.length - 1]?.preview ?? "";
4089
4355
  }, [node.content_blocks]);
4090
4356
  const [expanded, setExpanded] = useState5(effectivelyRunning);
4091
- useEffect7(() => {
4357
+ useEffect9(() => {
4092
4358
  if (effectivelyRunning) setExpanded(true);
4093
4359
  else if (effectivelyDone) setExpanded(false);
4094
4360
  }, [effectivelyRunning, effectivelyDone]);
@@ -4440,9 +4706,122 @@ function WorkflowChrome({
4440
4706
  )) });
4441
4707
  }
4442
4708
 
4709
+ // src/view/workflow/WorkflowGlobalSpinner.tsx
4710
+ import { useStore as useStore3 } from "zustand";
4711
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
4712
+ function WorkflowGlobalSpinner() {
4713
+ const store = useWorkflowSessionStoreApi();
4714
+ const show = useStore3(store, (s) => shouldShowGlobalSpinner(s.state));
4715
+ if (!show) return null;
4716
+ return /* @__PURE__ */ jsx16(
4717
+ "div",
4718
+ {
4719
+ className: "lax-workflow-global-spinner lax-spinner-container",
4720
+ "data-testid": "lax-workflow-global-spinner",
4721
+ role: "status",
4722
+ children: /* @__PURE__ */ jsxs11("span", { className: "lax-spinner lax-workflow-global-spinner__inner", children: [
4723
+ /* @__PURE__ */ jsx16("span", { className: "lax-spinner-frame", children: "\u280B" }),
4724
+ /* @__PURE__ */ jsx16("span", { className: "lax-spinner-verb lax-spinner-verb--shimmer", children: "Workflow" })
4725
+ ] })
4726
+ }
4727
+ );
4728
+ }
4729
+
4730
+ // src/view/workflow/WorkflowSessionBanners.tsx
4731
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
4732
+ function WorkflowSessionBanners({ state }) {
4733
+ const banner = resolveWorkflowBanner(state);
4734
+ const { display } = state;
4735
+ if (banner.kind === "none" && display.projectionStatus !== "error" && display.projectionStatus !== "done_with_warning" && state.internalErrors.length === 0) {
4736
+ return null;
4737
+ }
4738
+ const showTransportError = (display.projectionStatus === "error" || state.internalErrors.length > 0) && banner.kind !== "projection_degraded" && banner.kind !== "authority_error";
4739
+ return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4740
+ banner.kind === "authority_error" ? /* @__PURE__ */ jsx17(
4741
+ "div",
4742
+ {
4743
+ className: "lax-workflow-banner lax-workflow-banner--authority-error",
4744
+ "data-testid": "lax-workflow-authority-error-banner",
4745
+ role: "alert",
4746
+ children: banner.message
4747
+ }
4748
+ ) : null,
4749
+ banner.kind === "projection_degraded" ? /* @__PURE__ */ jsx17(
4750
+ "div",
4751
+ {
4752
+ className: "lax-workflow-banner lax-workflow-banner--projection-degraded",
4753
+ "data-testid": "lax-workflow-projection-degraded-banner",
4754
+ role: "status",
4755
+ children: banner.message
4756
+ }
4757
+ ) : null,
4758
+ banner.kind === "snapshot_unavailable" ? /* @__PURE__ */ jsx17(
4759
+ "div",
4760
+ {
4761
+ className: "lax-workflow-banner lax-workflow-banner--snapshot-unavailable",
4762
+ "data-testid": "lax-workflow-snapshot-unavailable-banner",
4763
+ role: "status",
4764
+ children: banner.message
4765
+ }
4766
+ ) : null,
4767
+ banner.kind === "partial_success" ? /* @__PURE__ */ jsx17(
4768
+ "div",
4769
+ {
4770
+ className: "lax-workflow-banner lax-workflow-banner--partial-success",
4771
+ "data-testid": "lax-workflow-partial-success-banner",
4772
+ role: "status",
4773
+ children: banner.message
4774
+ }
4775
+ ) : null,
4776
+ banner.kind === "forced_closeout" || display.projectionStatus === "done_with_warning" ? /* @__PURE__ */ jsxs12(
4777
+ "div",
4778
+ {
4779
+ className: "lax-workflow-closeout-warning",
4780
+ "data-testid": "lax-workflow-closeout-warning",
4781
+ role: "status",
4782
+ children: [
4783
+ /* @__PURE__ */ jsx17("strong", { children: "Workflow forced closeout" }),
4784
+ /* @__PURE__ */ jsxs12("span", { children: [
4785
+ " ",
4786
+ "\u2014 stream ended with unresolved child scopes; not a healthy completion."
4787
+ ] }),
4788
+ display.openChildIdentities.length > 0 ? /* @__PURE__ */ jsx17("ul", { className: "lax-workflow-closeout-warning__scopes", children: display.openChildIdentities.map((identity) => /* @__PURE__ */ jsx17("li", { children: identity }, identity)) }) : null
4789
+ ]
4790
+ }
4791
+ ) : null,
4792
+ banner.kind === "awaiting_authority" ? /* @__PURE__ */ jsx17(
4793
+ "div",
4794
+ {
4795
+ className: "lax-workflow-banner lax-workflow-banner--awaiting",
4796
+ "data-testid": "lax-workflow-awaiting-authority-banner",
4797
+ role: "status",
4798
+ children: banner.message
4799
+ }
4800
+ ) : null,
4801
+ banner.kind === "sync_failed" ? /* @__PURE__ */ jsx17(
4802
+ "div",
4803
+ {
4804
+ className: "lax-workflow-banner lax-workflow-banner--sync-failed",
4805
+ "data-testid": "lax-workflow-authority-sync-failed-banner",
4806
+ role: "alert",
4807
+ children: banner.message
4808
+ }
4809
+ ) : null,
4810
+ showTransportError ? /* @__PURE__ */ jsx17(
4811
+ "div",
4812
+ {
4813
+ className: "lax-workflow-error",
4814
+ "data-testid": "lax-workflow-error",
4815
+ role: "alert",
4816
+ children: state.internalErrors[state.internalErrors.length - 1]?.message ?? "Workflow stream error"
4817
+ }
4818
+ ) : null
4819
+ ] });
4820
+ }
4821
+
4443
4822
  // src/view/workflow/WorkflowTaskListFooter.tsx
4444
4823
  import { useMemo as useMemo8, useState as useState7 } from "react";
4445
- import { useStore as useStore3 } from "zustand";
4824
+ import { useStore as useStore4 } from "zustand";
4446
4825
 
4447
4826
  // src/view/workflow/workflowTaskListMerge.ts
4448
4827
  var STATUS_ORDER = {
@@ -4483,7 +4862,7 @@ function deriveWorkflowTaskFooterState(state) {
4483
4862
  tasks: mergeWorkflowLoopTasks(state.loopTreesBySessionId),
4484
4863
  taskPhase: deriveWorkflowTaskPhase(trees),
4485
4864
  hasEverShownTaskList: trees.some((tree) => tree.hasEverShownTaskList),
4486
- sessionStatus: state.status
4865
+ sessionStatus: getDisplayProjectionStatus(state)
4487
4866
  };
4488
4867
  }
4489
4868
  function shouldShowWorkflowTaskListFooter(state) {
@@ -4513,16 +4892,16 @@ function loopSessionDisplayLabel(loopSessionId) {
4513
4892
  }
4514
4893
 
4515
4894
  // src/view/workflow/WorkflowTaskListFooter.tsx
4516
- import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
4895
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
4517
4896
  function MessageResponse({ children }) {
4518
- return /* @__PURE__ */ jsxs11("div", { className: "lax-message-response", children: [
4519
- /* @__PURE__ */ jsx16("span", { className: "lax-message-response__marker", children: "\u23BF " }),
4520
- /* @__PURE__ */ jsx16("span", { className: "lax-message-response__content", children })
4897
+ return /* @__PURE__ */ jsxs13("div", { className: "lax-message-response", children: [
4898
+ /* @__PURE__ */ jsx18("span", { className: "lax-message-response__marker", children: "\u23BF " }),
4899
+ /* @__PURE__ */ jsx18("span", { className: "lax-message-response__content", children })
4521
4900
  ] });
4522
4901
  }
4523
4902
  function WorkflowTaskListFooter() {
4524
4903
  const store = useWorkflowSessionStoreApi();
4525
- const state = useStore3(store, (s) => s.state);
4904
+ const state = useStore4(store, (s) => s.state);
4526
4905
  const [expanded, setExpanded] = useState7(false);
4527
4906
  const footerState = useMemo8(() => deriveWorkflowTaskFooterState(state), [state]);
4528
4907
  const visible = shouldShowWorkflowTaskListFooter(state);
@@ -4531,8 +4910,8 @@ function WorkflowTaskListFooter() {
4531
4910
  }
4532
4911
  const label = formatWorkflowTaskFooterLabel(state);
4533
4912
  const groups = groupWorkflowTasksByLoop(footerState.tasks);
4534
- return /* @__PURE__ */ jsxs11("div", { className: "lax-workflow-task-list-footer", "data-testid": "lax-workflow-task-list-footer", children: [
4535
- /* @__PURE__ */ jsxs11(
4913
+ return /* @__PURE__ */ jsxs13("div", { className: "lax-workflow-task-list-footer", "data-testid": "lax-workflow-task-list-footer", children: [
4914
+ /* @__PURE__ */ jsxs13(
4536
4915
  "button",
4537
4916
  {
4538
4917
  type: "button",
@@ -4542,18 +4921,18 @@ function WorkflowTaskListFooter() {
4542
4921
  onClick: () => setExpanded((v) => !v),
4543
4922
  children: [
4544
4923
  label,
4545
- /* @__PURE__ */ jsx16("span", { className: "lax-task-list-footer__hint", children: expanded ? " \xB7 \u2191 to hide" : " \xB7 \u2193 to view" })
4924
+ /* @__PURE__ */ jsx18("span", { className: "lax-task-list-footer__hint", children: expanded ? " \xB7 \u2191 to hide" : " \xB7 \u2193 to view" })
4546
4925
  ]
4547
4926
  }
4548
4927
  ),
4549
- expanded ? /* @__PURE__ */ jsx16(MessageResponse, { children: /* @__PURE__ */ jsx16("div", { className: "lax-workflow-task-list-groups", children: groups.map((group) => /* @__PURE__ */ jsxs11(
4928
+ expanded ? /* @__PURE__ */ jsx18(MessageResponse, { children: /* @__PURE__ */ jsx18("div", { className: "lax-workflow-task-list-groups", children: groups.map((group) => /* @__PURE__ */ jsxs13(
4550
4929
  "div",
4551
4930
  {
4552
4931
  className: "lax-workflow-task-list-group",
4553
4932
  "data-testid": `lax-workflow-task-group-${group.loopSessionId}`,
4554
4933
  children: [
4555
- /* @__PURE__ */ jsx16("div", { className: "lax-workflow-task-list-group-title", children: loopSessionDisplayLabel(group.loopSessionId) }),
4556
- /* @__PURE__ */ jsx16(TaskList, { tasks: group.tasks })
4934
+ /* @__PURE__ */ jsx18("div", { className: "lax-workflow-task-list-group-title", children: loopSessionDisplayLabel(group.loopSessionId) }),
4935
+ /* @__PURE__ */ jsx18(TaskList, { tasks: group.tasks })
4557
4936
  ]
4558
4937
  },
4559
4938
  group.loopSessionId
@@ -4562,10 +4941,13 @@ function WorkflowTaskListFooter() {
4562
4941
  }
4563
4942
 
4564
4943
  // src/view/workflow/WorkflowSession.tsx
4565
- import { useStore as useStore4 } from "zustand";
4566
- import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
4944
+ import { useStore as useStore5 } from "zustand";
4945
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
4567
4946
  function WorkflowSession({
4568
4947
  source,
4948
+ authoritySource,
4949
+ snapshotBootstrap,
4950
+ snapshotHydrateFailed = false,
4569
4951
  initialEvents,
4570
4952
  registry,
4571
4953
  tierOverrides,
@@ -4585,10 +4967,11 @@ function WorkflowSession({
4585
4967
  groupParallelTools = false,
4586
4968
  workflowScale,
4587
4969
  onRequestLoopReplay,
4588
- onStreamComplete,
4970
+ onAuthorityTerminal,
4971
+ onDisplayStreamEnded,
4589
4972
  children
4590
4973
  }) {
4591
- const storeRef = useRef3(null);
4974
+ const storeRef = useRef4(null);
4592
4975
  if (storeRef.current === null) {
4593
4976
  const initialState = initialEvents && initialEvents.length > 0 ? reduceWorkflowEvents(initialEvents, { tierOverrides }) : void 0;
4594
4977
  storeRef.current = createWorkflowSessionStore(initialState, {
@@ -4598,6 +4981,21 @@ function WorkflowSession({
4598
4981
  maxHydratedCompletedLoops: workflowScale?.maxHydratedCompletedLoops ?? DEFAULT_WORKFLOW_SCALE_OPTIONS.maxHydratedCompletedLoops
4599
4982
  });
4600
4983
  }
4984
+ const store = storeRef.current;
4985
+ useWorkflowAuthorityBinding(store, authoritySource, { onAuthorityTerminal });
4986
+ const streamEnded = useStore5(store, (s) => s.state.display.streamEnded);
4987
+ const authorityStatus = useStore5(store, (s) => s.state.authority?.status);
4988
+ useWorkflowAwaitingAuthorityTimers(store, streamEnded, authorityStatus);
4989
+ useEffect10(() => {
4990
+ if (snapshotBootstrap) {
4991
+ store.getState().hydrateFromSnapshot(snapshotBootstrap, { preserveAuthority: true });
4992
+ }
4993
+ }, [snapshotBootstrap, store]);
4994
+ useEffect10(() => {
4995
+ if (snapshotHydrateFailed) {
4996
+ store.getState().markSnapshotHydrateFailed();
4997
+ }
4998
+ }, [snapshotHydrateFailed, store]);
4601
4999
  const scaleOptions = useMemo9(
4602
5000
  () => ({ ...DEFAULT_WORKFLOW_SCALE_OPTIONS, ...workflowScale }),
4603
5001
  [workflowScale]
@@ -4635,38 +5033,23 @@ function WorkflowSession({
4635
5033
  workspaceRoot
4636
5034
  ]
4637
5035
  );
4638
- const containerTree = useStore4(
4639
- storeRef.current,
4640
- (s) => s.state.containerTree
4641
- );
4642
- const loopTreesBySessionId = useStore4(
4643
- storeRef.current,
4644
- (s) => s.state.loopTreesBySessionId
4645
- );
4646
- const openChildIdentities = useStore4(
4647
- storeRef.current,
4648
- (s) => s.state.openChildIdentities
4649
- );
4650
- const sessionStatus = useStore4(
4651
- storeRef.current,
4652
- (s) => s.state.status
4653
- );
4654
- const internalErrors = useStore4(
4655
- storeRef.current,
4656
- (s) => s.state.internalErrors
4657
- );
4658
- const onErrorRef = useRef3(onError);
4659
- const onStreamCompleteRef = useRef3(onStreamComplete);
5036
+ const sessionState = useStore5(store, (s) => s.state);
5037
+ const containerTree = sessionState.containerTree;
5038
+ const loopTreesBySessionId = sessionState.loopTreesBySessionId;
5039
+ const onErrorRef = useRef4(onError);
5040
+ const onDisplayStreamEndedRef = useRef4(onDisplayStreamEnded);
5041
+ const streamAbortRef = useRef4(null);
4660
5042
  onErrorRef.current = onError;
4661
- onStreamCompleteRef.current = onStreamComplete;
4662
- useEffect8(() => {
4663
- const store = storeRef.current;
5043
+ onDisplayStreamEndedRef.current = onDisplayStreamEnded;
5044
+ useEffect10(() => {
5045
+ if (!source) return void 0;
4664
5046
  const controller = new AbortController();
5047
+ streamAbortRef.current = controller;
4665
5048
  void source.start((event, ctx) => {
4666
5049
  store.getState().applyEvent(event, ctx);
4667
5050
  }, controller.signal).then(() => {
4668
- store.getState().markStreamComplete();
4669
- onStreamCompleteRef.current?.();
5051
+ store.getState().markStreamEnded();
5052
+ onDisplayStreamEndedRef.current?.();
4670
5053
  }).catch((err) => {
4671
5054
  if (err instanceof SseTransportTerminalError) {
4672
5055
  store.getState().markAsError(err);
@@ -4677,40 +5060,23 @@ function WorkflowSession({
4677
5060
  });
4678
5061
  return () => {
4679
5062
  controller.abort();
5063
+ streamAbortRef.current = null;
4680
5064
  };
4681
- }, [source]);
4682
- 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(
5065
+ }, [source, store]);
5066
+ useEffect10(() => {
5067
+ if (isAuthorityTerminalStatus(authorityStatus)) {
5068
+ streamAbortRef.current?.abort();
5069
+ }
5070
+ }, [authorityStatus]);
5071
+ return /* @__PURE__ */ jsx19(WorkflowSessionStoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx19(WorkflowScaleContext.Provider, { value: scaleOptions, children: /* @__PURE__ */ jsx19(WorkflowLoopReplayProvider, { onRequestLoopReplay, children: /* @__PURE__ */ jsx19(InteractionBusContext.Provider, { value: busRef, children: /* @__PURE__ */ jsx19(NodeRegistryContext.Provider, { value: registryRef, children: /* @__PURE__ */ jsx19(MarkdownRendererContext.Provider, { value: markdownRenderer, children: /* @__PURE__ */ jsx19(ToolDisplayOptionsContext.Provider, { value: toolDisplayRef, children: /* @__PURE__ */ jsx19(SessionViewOptionsContext.Provider, { value: sessionViewRef, children: /* @__PURE__ */ jsxs14(
4683
5072
  "div",
4684
5073
  {
4685
5074
  className: "lax-agent-session lax-workflow-session",
4686
5075
  "data-testid": "lax-workflow-session",
4687
5076
  children: [
4688
- sessionStatus === "done_with_warning" ? /* @__PURE__ */ jsxs12(
4689
- "div",
4690
- {
4691
- className: "lax-workflow-closeout-warning",
4692
- "data-testid": "lax-workflow-closeout-warning",
4693
- role: "status",
4694
- children: [
4695
- /* @__PURE__ */ jsx17("strong", { children: "Workflow forced closeout" }),
4696
- /* @__PURE__ */ jsxs12("span", { children: [
4697
- " ",
4698
- "\u2014 stream ended with unresolved child scopes; not a healthy completion."
4699
- ] }),
4700
- openChildIdentities.length > 0 ? /* @__PURE__ */ jsx17("ul", { className: "lax-workflow-closeout-warning__scopes", children: openChildIdentities.map((identity) => /* @__PURE__ */ jsx17("li", { children: identity }, identity)) }) : null
4701
- ]
4702
- }
4703
- ) : null,
4704
- sessionStatus === "error" || internalErrors.length > 0 ? /* @__PURE__ */ jsx17(
4705
- "div",
4706
- {
4707
- className: "lax-workflow-error",
4708
- "data-testid": "lax-workflow-error",
4709
- role: "alert",
4710
- children: internalErrors[internalErrors.length - 1]?.message ?? "Workflow stream error"
4711
- }
4712
- ) : null,
4713
- /* @__PURE__ */ jsx17(
5077
+ /* @__PURE__ */ jsx19(WorkflowSessionBanners, { state: sessionState }),
5078
+ /* @__PURE__ */ jsx19(WorkflowGlobalSpinner, {}),
5079
+ /* @__PURE__ */ jsx19(
4714
5080
  WorkflowChrome,
4715
5081
  {
4716
5082
  containerTree,
@@ -4720,13 +5086,158 @@ function WorkflowSession({
4720
5086
  groupParallelTools
4721
5087
  }
4722
5088
  ),
4723
- /* @__PURE__ */ jsx17(WorkflowTaskListFooter, {}),
5089
+ /* @__PURE__ */ jsx19(WorkflowTaskListFooter, {}),
4724
5090
  children
4725
5091
  ]
4726
5092
  }
4727
5093
  ) }) }) }) }) }) }) }) });
4728
5094
  }
4729
5095
 
5096
+ // src/core/workflow/createMockAuthoritySource.ts
5097
+ function defaultDoneSnapshot() {
5098
+ return {
5099
+ taskId: "mock-task",
5100
+ status: "done",
5101
+ updatedAt: Date.now()
5102
+ };
5103
+ }
5104
+ function defaultRunningSnapshot() {
5105
+ return {
5106
+ taskId: "mock-task",
5107
+ status: "running",
5108
+ updatedAt: Date.now()
5109
+ };
5110
+ }
5111
+ function createMockAuthoritySource(snapshotOrOptions) {
5112
+ let snapshot = null;
5113
+ let pendingUpdates = [];
5114
+ if (snapshotOrOptions == null) {
5115
+ snapshot = defaultRunningSnapshot();
5116
+ } else if (typeof snapshotOrOptions === "object" && ("pendingUpdates" in snapshotOrOptions || "snapshot" in snapshotOrOptions) && !("status" in snapshotOrOptions)) {
5117
+ const opts = snapshotOrOptions;
5118
+ snapshot = opts.snapshot ?? defaultDoneSnapshot();
5119
+ pendingUpdates = opts.pendingUpdates ?? [];
5120
+ } else if (snapshotOrOptions === null) {
5121
+ snapshot = null;
5122
+ } else {
5123
+ snapshot = snapshotOrOptions;
5124
+ }
5125
+ return {
5126
+ getSnapshot: () => snapshot,
5127
+ subscribe(cb) {
5128
+ if (snapshot) {
5129
+ cb(snapshot);
5130
+ }
5131
+ for (const update of pendingUpdates) {
5132
+ snapshot = update;
5133
+ cb(update);
5134
+ }
5135
+ return () => {
5136
+ };
5137
+ }
5138
+ };
5139
+ }
5140
+ function createDoneAuthoritySource(taskId = "mock-task") {
5141
+ return createMockAuthoritySource({
5142
+ taskId,
5143
+ status: "done",
5144
+ updatedAt: Date.now()
5145
+ });
5146
+ }
5147
+ function createRunningAuthoritySource(taskId = "mock-task") {
5148
+ return createMockAuthoritySource({
5149
+ taskId,
5150
+ status: "running",
5151
+ updatedAt: Date.now()
5152
+ });
5153
+ }
5154
+ function createControllableAuthoritySource(initial) {
5155
+ let snapshot = initial;
5156
+ const listeners = /* @__PURE__ */ new Set();
5157
+ return {
5158
+ getSnapshot: () => snapshot,
5159
+ subscribe(cb) {
5160
+ listeners.add(cb);
5161
+ cb(snapshot);
5162
+ return () => {
5163
+ listeners.delete(cb);
5164
+ };
5165
+ },
5166
+ push(next) {
5167
+ snapshot = next;
5168
+ for (const listener of listeners) {
5169
+ listener(next);
5170
+ }
5171
+ }
5172
+ };
5173
+ }
5174
+
5175
+ // src/core/workflow/createPollingAuthoritySource.ts
5176
+ var DEFAULT_INTERVAL_MS = 2e3;
5177
+ function createPollingAuthoritySource(taskId, fetchFn, options) {
5178
+ let cached = options?.initialSnapshot ?? null;
5179
+ let intervalId;
5180
+ let pollInFlight = false;
5181
+ const listeners = /* @__PURE__ */ new Set();
5182
+ const notify = (snap) => {
5183
+ cached = snap;
5184
+ for (const listener of listeners) {
5185
+ listener(snap);
5186
+ }
5187
+ };
5188
+ const stopPoll = () => {
5189
+ if (intervalId != null) {
5190
+ clearInterval(intervalId);
5191
+ intervalId = void 0;
5192
+ }
5193
+ };
5194
+ const pollOnce = async () => {
5195
+ if (pollInFlight || options?.signal?.aborted) return;
5196
+ pollInFlight = true;
5197
+ try {
5198
+ const snap = await fetchFn(taskId);
5199
+ const prev = cached;
5200
+ if (prev == null || prev.status !== snap.status || prev.updatedAt !== snap.updatedAt) {
5201
+ notify(snap);
5202
+ }
5203
+ if (isAuthorityTerminalStatus(snap.status)) {
5204
+ stopPoll();
5205
+ }
5206
+ } finally {
5207
+ pollInFlight = false;
5208
+ }
5209
+ };
5210
+ const startPoll = () => {
5211
+ if (intervalId != null || options?.signal?.aborted) return;
5212
+ if (cached != null && isAuthorityTerminalStatus(cached.status)) {
5213
+ return;
5214
+ }
5215
+ void pollOnce();
5216
+ intervalId = setInterval(() => {
5217
+ void pollOnce();
5218
+ }, options?.intervalMs ?? DEFAULT_INTERVAL_MS);
5219
+ };
5220
+ options?.signal?.addEventListener("abort", stopPoll, { once: true });
5221
+ return {
5222
+ getSnapshot: () => cached,
5223
+ subscribe(cb) {
5224
+ listeners.add(cb);
5225
+ if (cached != null) {
5226
+ cb(cached);
5227
+ }
5228
+ if (cached == null || !isAuthorityTerminalStatus(cached.status)) {
5229
+ startPoll();
5230
+ }
5231
+ return () => {
5232
+ listeners.delete(cb);
5233
+ if (listeners.size === 0) {
5234
+ stopPoll();
5235
+ }
5236
+ };
5237
+ }
5238
+ };
5239
+ }
5240
+
4730
5241
  // src/view/tools/groupParallelTools.ts
4731
5242
  function buildTimelineEntries(rootIds, byId, options = {}) {
4732
5243
  const filtered = options.hideStandalonePermissions ? rootIds.filter((id) => byId[id]?.kind !== "permission") : [...rootIds];
@@ -4764,18 +5275,18 @@ function buildTimelineEntries(rootIds, byId, options = {}) {
4764
5275
  }
4765
5276
 
4766
5277
  // src/view/nodes/SubAgentNode.tsx
4767
- import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
5278
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
4768
5279
  function SubAgentBlock({ nodeId }) {
4769
5280
  const node = useNodeTyped(nodeId, "subagent");
4770
5281
  const childIds = useChildren(nodeId);
4771
5282
  const registry = useNodeRegistry();
4772
5283
  if (!node) return null;
4773
- return /* @__PURE__ */ jsxs13("div", { className: "lax-subagent-block", "data-status": node.status, children: [
4774
- /* @__PURE__ */ jsxs13("div", { className: "lax-subagent-block__header", children: [
5284
+ return /* @__PURE__ */ jsxs15("div", { className: "lax-subagent-block", "data-status": node.status, children: [
5285
+ /* @__PURE__ */ jsxs15("div", { className: "lax-subagent-block__header", children: [
4775
5286
  "SubAgent: ",
4776
5287
  node.subagentId
4777
5288
  ] }),
4778
- /* @__PURE__ */ jsx18("div", { className: "lax-subagent-block__children", children: childIds.map((childId) => /* @__PURE__ */ jsx18(TimelineItem, { nodeId: childId, registry }, childId)) })
5289
+ /* @__PURE__ */ jsx20("div", { className: "lax-subagent-block__children", children: childIds.map((childId) => /* @__PURE__ */ jsx20(TimelineItem, { nodeId: childId, registry }, childId)) })
4779
5290
  ] });
4780
5291
  }
4781
5292
 
@@ -4894,6 +5405,7 @@ export {
4894
5405
  WorkflowTaskListFooter,
4895
5406
  Write,
4896
5407
  WriteToolBody,
5408
+ applyAuthoritySnapshotToState,
4897
5409
  buildBashBodyBlocks,
4898
5410
  buildCanonicalFromTree,
4899
5411
  buildProjectionContext,
@@ -4909,21 +5421,27 @@ export {
4909
5421
  configurePlantumlServer,
4910
5422
  createActiveSessionBridge,
4911
5423
  createAgentxSseSource,
5424
+ createControllableAuthoritySource,
4912
5425
  createDefaultRegistry,
4913
5426
  createDefaultToolRegistry,
5427
+ createDoneAuthoritySource,
4914
5428
  createEmptyTree,
4915
5429
  createEmptyWorkflowSessionState,
4916
5430
  createInteractionBus,
5431
+ createMockAuthoritySource,
4917
5432
  createMockSource,
4918
5433
  createMultiSessionStore,
5434
+ createPollingAuthoritySource,
4919
5435
  createProjectionContext,
4920
5436
  createReplaySource,
5437
+ createRunningAuthoritySource,
4921
5438
  createSessionStore,
4922
5439
  createWorkflowSessionStore,
4923
5440
  deriveLoopSessionIdFromAgentEvent,
4924
5441
  displayPath,
4925
5442
  entryKey,
4926
5443
  filterAgentLoopReplayEvents,
5444
+ finalizeWorkflowDisplayOnStreamEnded,
4927
5445
  fixDiagramSvg,
4928
5446
  formatAgentTitle,
4929
5447
  formatDiffFromStrings,
@@ -4936,9 +5454,14 @@ export {
4936
5454
  formatTimeoutFooter,
4937
5455
  formatToolTitle,
4938
5456
  getChildIds,
5457
+ getDisplayProjectionStatus,
4939
5458
  getMainStageIds,
4940
5459
  getNoopInteractionBus,
5460
+ hydrateWorkflowFromSnapshot,
4941
5461
  isAgentLoopReplayEventType,
5462
+ isAtOrBeforeProjectionCursor,
5463
+ isAuthorityTerminal,
5464
+ isAuthorityTerminalStatus,
4942
5465
  isGitOperationCommand,
4943
5466
  isGroupableToolName,
4944
5467
  isMcpToolName,
@@ -4962,8 +5485,10 @@ export {
4962
5485
  resolveShellProgressFromPayload,
4963
5486
  resolveSseTransportPolicy,
4964
5487
  resolveToolBody,
5488
+ resolveWorkflowBanner,
4965
5489
  shouldApplyGrouping,
4966
5490
  shouldShowExploreDetail,
5491
+ shouldShowGlobalSpinner,
4967
5492
  shouldShowTaskListFooter,
4968
5493
  streamChunk,
4969
5494
  truncateCommand,