langchain_agentx_stream_ui 0.2.0 → 0.2.3
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/{chunk-DP7V33X7.js → chunk-6Z4ZF36Z.js} +1 -1
- package/dist/chunk-6Z4ZF36Z.js.map +1 -0
- package/dist/{chunk-2T26L3VT.js → chunk-DGC43A5L.js} +5 -5
- package/dist/chunk-DGC43A5L.js.map +1 -0
- package/dist/{chunk-4RIOBLGB.js → chunk-GBY5DJ7L.js} +1 -1
- package/dist/chunk-GBY5DJ7L.js.map +1 -0
- package/dist/{chunk-ZNXAQ4ZZ.js → chunk-OFXC2GZI.js} +144 -64
- package/dist/chunk-OFXC2GZI.js.map +1 -0
- package/dist/{chunk-6DYISADG.js → chunk-T626YBJX.js} +4 -4
- package/dist/chunk-T626YBJX.js.map +1 -0
- package/dist/{chunk-7CLAU74Y.js → chunk-YUZXTGJJ.js} +2 -2
- package/dist/chunk-YUZXTGJJ.js.map +1 -0
- package/dist/index.d.ts +18 -10
- package/dist/index.js +568 -327
- package/dist/index.js.map +1 -1
- package/dist/multi-session.js +6 -6
- package/dist/tools-presentation.js +2 -2
- package/dist/tools.js +4 -4
- package/dist/virtual.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-2T26L3VT.js.map +0 -1
- package/dist/chunk-4RIOBLGB.js.map +0 -1
- package/dist/chunk-6DYISADG.js.map +0 -1
- package/dist/chunk-7CLAU74Y.js.map +0 -1
- package/dist/chunk-DP7V33X7.js.map +0 -1
- package/dist/chunk-ZNXAQ4ZZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
fixDiagramSvg,
|
|
24
24
|
markEventIdSeen,
|
|
25
25
|
shouldSkipDuplicateEvent
|
|
26
|
-
} from "./chunk-
|
|
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-
|
|
36
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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;
|
|
@@ -1659,6 +1837,9 @@ function resolveParallelItemLoopSessionId(eventType, eventSessionId, derivedLoop
|
|
|
1659
1837
|
if (itemKey && parsed?.taskKey === itemKey) {
|
|
1660
1838
|
return eventSessionId;
|
|
1661
1839
|
}
|
|
1840
|
+
if (itemKey && eventSessionId.endsWith(`-${itemKey}`)) {
|
|
1841
|
+
return eventSessionId;
|
|
1842
|
+
}
|
|
1662
1843
|
return null;
|
|
1663
1844
|
}
|
|
1664
1845
|
function resolveStructureOpenLoopSessionId(eventType, eventSessionId, derivedLoopSessionId, data) {
|
|
@@ -1727,6 +1908,9 @@ function settleCompletedStageSubtree(tree, stageContainerId) {
|
|
|
1727
1908
|
}
|
|
1728
1909
|
if (node.container_id === stageContainerId) continue;
|
|
1729
1910
|
if (node.status === "completed" || node.status === "failed") continue;
|
|
1911
|
+
if (node.status === "running" || node.status === "retrying" || node.status === "pending" || node.status === "blocked") {
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1730
1914
|
next = upsertRuntimeNode(next, {
|
|
1731
1915
|
...node,
|
|
1732
1916
|
status: "completed",
|
|
@@ -1908,17 +2092,26 @@ function findContainerByLoopSession(tree, loopSessionId) {
|
|
|
1908
2092
|
return findContainerIdByLoopSession(tree, loopSessionId);
|
|
1909
2093
|
}
|
|
1910
2094
|
var WORKFLOW_DEFAULT_LOOP_SESSION_ID = "workflow-loop-default";
|
|
1911
|
-
function resolveAgentLoopSessionId(event, activeLoopSessionId) {
|
|
2095
|
+
function resolveAgentLoopSessionId(event, activeLoopSessionId, containerTree) {
|
|
1912
2096
|
if (event.session_id) return event.session_id;
|
|
1913
2097
|
if (isWorkflowStructureEventType(event.event_type)) return null;
|
|
1914
2098
|
if (event.event_type === "task-list-snapshot") return null;
|
|
1915
2099
|
const derived = deriveLoopSessionIdFromAgentEvent(event);
|
|
1916
2100
|
if (derived) return derived;
|
|
2101
|
+
const byLoop = containersByLoopSessionId(containerTree);
|
|
2102
|
+
if (activeLoopSessionId) {
|
|
2103
|
+
const activeContainer = byLoop.get(activeLoopSessionId);
|
|
2104
|
+
if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
|
|
2105
|
+
return activeLoopSessionId;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const runningLoopIds = [...byLoop.entries()].filter(([, node]) => node.status === "running" || node.status === "retrying").map(([loopSessionId]) => loopSessionId);
|
|
2109
|
+
if (runningLoopIds.length === 1) return runningLoopIds[0];
|
|
1917
2110
|
if (activeLoopSessionId) return activeLoopSessionId;
|
|
1918
2111
|
return WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
1919
2112
|
}
|
|
1920
2113
|
function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
|
|
1921
|
-
const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId);
|
|
2114
|
+
const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId, tree);
|
|
1922
2115
|
if (!sessionId) return tree;
|
|
1923
2116
|
let nextTree = ensureEmbeddedSubworkflowContainers(tree, sessionId);
|
|
1924
2117
|
if (findContainerByLoopSession(nextTree, sessionId)) return nextTree;
|
|
@@ -2157,7 +2350,7 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
2157
2350
|
if (isWorkflowStructureEventType(event.event_type)) {
|
|
2158
2351
|
return applyStructureEvent(state, event);
|
|
2159
2352
|
}
|
|
2160
|
-
const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
2353
|
+
const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId, state.containerTree) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
2161
2354
|
let containerTree = bindLoopSessionToContainerTree(
|
|
2162
2355
|
state.containerTree,
|
|
2163
2356
|
event,
|
|
@@ -2176,6 +2369,11 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
2176
2369
|
next.containerTree,
|
|
2177
2370
|
next.workflowProgress
|
|
2178
2371
|
);
|
|
2372
|
+
let status = next.status;
|
|
2373
|
+
if (workflowProgress.status === "done") status = "done";
|
|
2374
|
+
else if (workflowProgress.status === "error") status = "error";
|
|
2375
|
+
else if (reducedLoop.status === "error") status = "error";
|
|
2376
|
+
else if (status === "connecting") status = "running";
|
|
2179
2377
|
return {
|
|
2180
2378
|
...next,
|
|
2181
2379
|
workflowProgress,
|
|
@@ -2184,7 +2382,85 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
2184
2382
|
[loopSessionId]: reducedLoop
|
|
2185
2383
|
},
|
|
2186
2384
|
activeLoopSessionId: loopSessionId,
|
|
2187
|
-
status
|
|
2385
|
+
status
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
function finalizeWorkflowSessionOnStreamComplete(state) {
|
|
2389
|
+
if (state.status === "done" || state.status === "error") {
|
|
2390
|
+
return { ...state, activeLoopSessionId: null };
|
|
2391
|
+
}
|
|
2392
|
+
const tree = state.containerTree;
|
|
2393
|
+
const rootId = tree.rootContainerIds[0];
|
|
2394
|
+
if (!rootId) {
|
|
2395
|
+
return { ...state, status: "done", activeLoopSessionId: null };
|
|
2396
|
+
}
|
|
2397
|
+
const root = tree.containersById[rootId];
|
|
2398
|
+
if (!root) {
|
|
2399
|
+
return { ...state, status: "done", activeLoopSessionId: null };
|
|
2400
|
+
}
|
|
2401
|
+
const topLevelStages = Object.values(tree.containersById).filter(
|
|
2402
|
+
(node) => node.scope === "stage" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
|
|
2403
|
+
);
|
|
2404
|
+
const progressTotal = root.display?.progress_total ?? (topLevelStages.reduce(
|
|
2405
|
+
(max, s) => Math.max(max, s.display?.progress_total ?? 0),
|
|
2406
|
+
0
|
|
2407
|
+
) || topLevelStages.length);
|
|
2408
|
+
const settledStageCount = topLevelStages.filter(
|
|
2409
|
+
(s) => s.status === "completed" || s.status === "failed" || s.status === "skipped"
|
|
2410
|
+
).length;
|
|
2411
|
+
const progressCurrent = Math.max(
|
|
2412
|
+
root.display?.progress_current ?? 0,
|
|
2413
|
+
settledStageCount
|
|
2414
|
+
);
|
|
2415
|
+
const rootParallelItems = Object.values(tree.containersById).filter(
|
|
2416
|
+
(node) => node.scope === "item" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
|
|
2417
|
+
);
|
|
2418
|
+
const rootParallelSettled = topLevelStages.length === 0 && rootParallelItems.length > 0 && rootParallelItems.every(
|
|
2419
|
+
(item) => item.status === "completed" || item.status === "failed" || item.status === "skipped"
|
|
2420
|
+
) && !Object.values(tree.containersById).some(
|
|
2421
|
+
(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")
|
|
2422
|
+
);
|
|
2423
|
+
const workflowVisuallyComplete = root.status === "completed" || root.status === "failed" || progressTotal > 0 && progressCurrent >= progressTotal || rootParallelSettled;
|
|
2424
|
+
if (!workflowVisuallyComplete) {
|
|
2425
|
+
return state;
|
|
2426
|
+
}
|
|
2427
|
+
let containersById = { ...tree.containersById };
|
|
2428
|
+
for (const [id, node] of Object.entries(containersById)) {
|
|
2429
|
+
if (node.status === "running" || node.status === "pending") {
|
|
2430
|
+
containersById[id] = { ...node, status: "completed", is_open: false };
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
const settledRoot = containersById[rootId] ?? root;
|
|
2434
|
+
if (settledRoot.status !== "completed" && settledRoot.status !== "failed") {
|
|
2435
|
+
containersById = {
|
|
2436
|
+
...containersById,
|
|
2437
|
+
[rootId]: { ...settledRoot, status: "completed", is_open: false }
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
const loopTreesBySessionId = { ...state.loopTreesBySessionId };
|
|
2441
|
+
for (const [loopSessionId, loopTree] of Object.entries(loopTreesBySessionId)) {
|
|
2442
|
+
if (loopTree.status === "running" || loopTree.status === "connecting") {
|
|
2443
|
+
loopTreesBySessionId[loopSessionId] = finalizeWorkflowLoopTree(loopTree, "done");
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
const containerTree = {
|
|
2447
|
+
...tree,
|
|
2448
|
+
containersById,
|
|
2449
|
+
activeContainerIds: tree.activeContainerIds.filter(
|
|
2450
|
+
(id) => containersById[id]?.is_open
|
|
2451
|
+
)
|
|
2452
|
+
};
|
|
2453
|
+
const workflowProgress = syncWorkflowProgressFromContainerTree(
|
|
2454
|
+
containerTree,
|
|
2455
|
+
state.workflowProgress
|
|
2456
|
+
);
|
|
2457
|
+
return {
|
|
2458
|
+
...state,
|
|
2459
|
+
status: workflowProgress.status === "error" ? "error" : "done",
|
|
2460
|
+
containerTree,
|
|
2461
|
+
workflowProgress,
|
|
2462
|
+
loopTreesBySessionId,
|
|
2463
|
+
activeLoopSessionId: null
|
|
2188
2464
|
};
|
|
2189
2465
|
}
|
|
2190
2466
|
function reduceWorkflowEvents(events, options) {
|
|
@@ -2194,180 +2470,37 @@ function reduceWorkflowEvents(events, options) {
|
|
|
2194
2470
|
);
|
|
2195
2471
|
}
|
|
2196
2472
|
|
|
2197
|
-
// src/core/
|
|
2198
|
-
|
|
2199
|
-
|
|
2473
|
+
// src/core/workflowSessionStore.ts
|
|
2474
|
+
var DEFAULT_HYDRATE_CHUNK_SIZE = 50;
|
|
2475
|
+
function nextAnimationFrame() {
|
|
2476
|
+
return new Promise((resolve) => {
|
|
2477
|
+
requestAnimationFrame(() => resolve());
|
|
2478
|
+
});
|
|
2200
2479
|
}
|
|
2201
|
-
function
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2480
|
+
function withoutHydrating(ids, loopSessionId) {
|
|
2481
|
+
if (!ids[loopSessionId]) return ids;
|
|
2482
|
+
const next = { ...ids };
|
|
2483
|
+
delete next[loopSessionId];
|
|
2484
|
+
return next;
|
|
2485
|
+
}
|
|
2486
|
+
function safeReduceWorkflow(state, event, eventIndex, options) {
|
|
2487
|
+
try {
|
|
2488
|
+
return { state: reduceWorkflowSession(state, event, eventIndex, options) };
|
|
2489
|
+
} catch (err) {
|
|
2490
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2491
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
2492
|
+
if (import.meta.env?.DEV) {
|
|
2493
|
+
console.error("[langchain_agentx_stream_ui] workflow reducer error:", err);
|
|
2211
2494
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
}
|
|
2221
|
-
function buildStageDoneSummary(tree) {
|
|
2222
|
-
if (tree.status !== "done" && tree.status !== "error") return null;
|
|
2223
|
-
const toolUses = countToolUses(tree);
|
|
2224
|
-
const duration = computeDurationSeconds(tree);
|
|
2225
|
-
const durationSuffix = duration != null ? ` \xB7 ${duration}s` : "";
|
|
2226
|
-
const label = toolUses > 0 ? `Done (${toolUses} tool use${toolUses === 1 ? "" : "s"}${durationSuffix})` : `Done (1 agent turn${durationSuffix})`;
|
|
2227
|
-
const fullText = lastTextContent(tree);
|
|
2228
|
-
const lines = fullText.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2229
|
-
const teaser = lines[0] ?? "";
|
|
2230
|
-
const extraLines = Math.max(0, lines.length - 1);
|
|
2231
|
-
return { label, teaser, extraLines };
|
|
2232
|
-
}
|
|
2233
|
-
function formatTeaserExpandHint(extraLines, verbose) {
|
|
2234
|
-
if (extraLines <= 0) return null;
|
|
2235
|
-
const shown = verbose ? Math.min(extraLines, 2) : 0;
|
|
2236
|
-
const hidden = extraLines - shown;
|
|
2237
|
-
if (hidden <= 0) return null;
|
|
2238
|
-
return `\u2026 +${hidden} lines (ctrl+o to expand)`;
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
// src/core/workflow/workflowLoopEviction.ts
|
|
2242
|
-
function containersByLoopSessionId(tree) {
|
|
2243
|
-
const map = /* @__PURE__ */ new Map();
|
|
2244
|
-
for (const node of Object.values(tree.containersById)) {
|
|
2245
|
-
if (node.loopSessionId) map.set(node.loopSessionId, node);
|
|
2246
|
-
}
|
|
2247
|
-
return map;
|
|
2248
|
-
}
|
|
2249
|
-
function collectProtectedLoopSessionIds(state, pinnedLoopSessionIds) {
|
|
2250
|
-
const protectedIds = new Set(Object.keys(pinnedLoopSessionIds));
|
|
2251
|
-
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
2252
|
-
if (state.activeLoopSessionId) {
|
|
2253
|
-
const activeContainer = byLoop.get(state.activeLoopSessionId);
|
|
2254
|
-
if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
|
|
2255
|
-
protectedIds.add(state.activeLoopSessionId);
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
for (const containerId of state.containerTree.activeContainerIds) {
|
|
2259
|
-
const node = state.containerTree.containersById[containerId];
|
|
2260
|
-
if (!node?.loopSessionId) continue;
|
|
2261
|
-
if (node.status === "running" || node.status === "retrying") {
|
|
2262
|
-
protectedIds.add(node.loopSessionId);
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
for (const node of Object.values(state.containerTree.containersById)) {
|
|
2266
|
-
if (!node.loopSessionId) continue;
|
|
2267
|
-
if (node.status === "running" || node.status === "retrying") {
|
|
2268
|
-
protectedIds.add(node.loopSessionId);
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
return protectedIds;
|
|
2272
|
-
}
|
|
2273
|
-
function evictInactiveLoopTrees(state, pinnedLoopSessionIds) {
|
|
2274
|
-
const protectedIds = collectProtectedLoopSessionIds(state, pinnedLoopSessionIds);
|
|
2275
|
-
for (const id of state.hotCompletedLoopSessionIds) {
|
|
2276
|
-
protectedIds.add(id);
|
|
2277
|
-
}
|
|
2278
|
-
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
2279
|
-
const nextLoops = { ...state.loopTreesBySessionId };
|
|
2280
|
-
const nextSummaries = {
|
|
2281
|
-
...state.stageDoneSummariesByContainerId
|
|
2282
|
-
};
|
|
2283
|
-
for (const [loopSessionId, tree] of Object.entries(state.loopTreesBySessionId)) {
|
|
2284
|
-
if (protectedIds.has(loopSessionId)) continue;
|
|
2285
|
-
const container = byLoop.get(loopSessionId);
|
|
2286
|
-
if (!container) continue;
|
|
2287
|
-
if (container.status !== "completed" && container.status !== "failed") continue;
|
|
2288
|
-
const summary = buildStageDoneSummary(tree);
|
|
2289
|
-
if (summary) {
|
|
2290
|
-
nextSummaries[container.container_id] = summary;
|
|
2291
|
-
}
|
|
2292
|
-
delete nextLoops[loopSessionId];
|
|
2293
|
-
}
|
|
2294
|
-
return {
|
|
2295
|
-
...state,
|
|
2296
|
-
loopTreesBySessionId: nextLoops,
|
|
2297
|
-
stageDoneSummariesByContainerId: nextSummaries
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
function touchCompletedLoopSession(state, loopSessionId) {
|
|
2301
|
-
const ids = state.hotCompletedLoopSessionIds.filter((id) => id !== loopSessionId);
|
|
2302
|
-
ids.push(loopSessionId);
|
|
2303
|
-
return {
|
|
2304
|
-
...state,
|
|
2305
|
-
hotCompletedLoopSessionIds: ids
|
|
2306
|
-
};
|
|
2307
|
-
}
|
|
2308
|
-
function evictCompletedLoopOverflow(state, maxHydratedCompletedLoops) {
|
|
2309
|
-
if (state.hotCompletedLoopSessionIds.length <= maxHydratedCompletedLoops) {
|
|
2310
|
-
return state;
|
|
2311
|
-
}
|
|
2312
|
-
const overflow = state.hotCompletedLoopSessionIds.length - maxHydratedCompletedLoops;
|
|
2313
|
-
const evictedIds = new Set(state.hotCompletedLoopSessionIds.slice(0, overflow));
|
|
2314
|
-
const trimmedIds = state.hotCompletedLoopSessionIds.slice(overflow);
|
|
2315
|
-
const byLoop = containersByLoopSessionId(state.containerTree);
|
|
2316
|
-
const nextLoops = { ...state.loopTreesBySessionId };
|
|
2317
|
-
const nextSummaries = {
|
|
2318
|
-
...state.stageDoneSummariesByContainerId
|
|
2319
|
-
};
|
|
2320
|
-
for (const id of evictedIds) {
|
|
2321
|
-
const tree = nextLoops[id];
|
|
2322
|
-
if (!tree) continue;
|
|
2323
|
-
const container = byLoop.get(id);
|
|
2324
|
-
if (container) {
|
|
2325
|
-
const summary = buildStageDoneSummary(tree);
|
|
2326
|
-
if (summary) {
|
|
2327
|
-
nextSummaries[container.container_id] = summary;
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
delete nextLoops[id];
|
|
2331
|
-
}
|
|
2332
|
-
return {
|
|
2333
|
-
...state,
|
|
2334
|
-
loopTreesBySessionId: nextLoops,
|
|
2335
|
-
stageDoneSummariesByContainerId: nextSummaries,
|
|
2336
|
-
hotCompletedLoopSessionIds: trimmedIds
|
|
2337
|
-
};
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
// src/core/workflowSessionStore.ts
|
|
2341
|
-
var DEFAULT_HYDRATE_CHUNK_SIZE = 50;
|
|
2342
|
-
function nextAnimationFrame() {
|
|
2343
|
-
return new Promise((resolve) => {
|
|
2344
|
-
requestAnimationFrame(() => resolve());
|
|
2345
|
-
});
|
|
2346
|
-
}
|
|
2347
|
-
function withoutHydrating(ids, loopSessionId) {
|
|
2348
|
-
if (!ids[loopSessionId]) return ids;
|
|
2349
|
-
const next = { ...ids };
|
|
2350
|
-
delete next[loopSessionId];
|
|
2351
|
-
return next;
|
|
2352
|
-
}
|
|
2353
|
-
function safeReduceWorkflow(state, event, eventIndex, options) {
|
|
2354
|
-
try {
|
|
2355
|
-
return { state: reduceWorkflowSession(state, event, eventIndex, options) };
|
|
2356
|
-
} catch (err) {
|
|
2357
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2358
|
-
const stack = err instanceof Error ? err.stack : void 0;
|
|
2359
|
-
if (import.meta.env?.DEV) {
|
|
2360
|
-
console.error("[langchain_agentx_stream_ui] workflow reducer error:", err);
|
|
2361
|
-
}
|
|
2362
|
-
return {
|
|
2363
|
-
state: {
|
|
2364
|
-
...state,
|
|
2365
|
-
internalErrors: [
|
|
2366
|
-
...state.internalErrors,
|
|
2367
|
-
{ eventIndex, eventType: event.event_type, message, stack }
|
|
2368
|
-
]
|
|
2369
|
-
}
|
|
2370
|
-
};
|
|
2495
|
+
return {
|
|
2496
|
+
state: {
|
|
2497
|
+
...state,
|
|
2498
|
+
internalErrors: [
|
|
2499
|
+
...state.internalErrors,
|
|
2500
|
+
{ eventIndex, eventType: event.event_type, message, stack }
|
|
2501
|
+
]
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2371
2504
|
}
|
|
2372
2505
|
}
|
|
2373
2506
|
function finalizeWorkflowSessionOnStreamError(state, message, eventIndex, stack) {
|
|
@@ -2462,6 +2595,13 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
|
|
|
2462
2595
|
)
|
|
2463
2596
|
});
|
|
2464
2597
|
},
|
|
2598
|
+
markStreamComplete() {
|
|
2599
|
+
set((current) => ({
|
|
2600
|
+
state: finalizeState(
|
|
2601
|
+
finalizeWorkflowSessionOnStreamComplete(current.state)
|
|
2602
|
+
)
|
|
2603
|
+
}));
|
|
2604
|
+
},
|
|
2465
2605
|
/**
|
|
2466
2606
|
* P2:将 completed loop 加入 LRU 热缓存并淘汰溢出。
|
|
2467
2607
|
* running 保护由 collectProtectedLoopSessionIds 按状态自动覆盖,不再依赖永久 pin。
|
|
@@ -2716,6 +2856,76 @@ function WorkflowMarkdownPreview({
|
|
|
2716
2856
|
return /* @__PURE__ */ jsx7("div", { className, "data-testid": testId, children: /* @__PURE__ */ jsx7(RichMarkdown, { content, deferDiagrams: true }) });
|
|
2717
2857
|
}
|
|
2718
2858
|
|
|
2859
|
+
// src/view/workflow/workflowStageListUtils.ts
|
|
2860
|
+
var ACTIVE_CONTAINER_STATUSES = /* @__PURE__ */ new Set([
|
|
2861
|
+
"running",
|
|
2862
|
+
"retrying",
|
|
2863
|
+
"pending",
|
|
2864
|
+
"blocked"
|
|
2865
|
+
]);
|
|
2866
|
+
function isDescendantContainer(containersById, ancestorId, nodeId) {
|
|
2867
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2868
|
+
let current = containersById[nodeId]?.parent_container_id ?? null;
|
|
2869
|
+
while (current) {
|
|
2870
|
+
if (current === ancestorId) return true;
|
|
2871
|
+
if (seen.has(current)) return false;
|
|
2872
|
+
seen.add(current);
|
|
2873
|
+
current = containersById[current]?.parent_container_id ?? null;
|
|
2874
|
+
}
|
|
2875
|
+
return false;
|
|
2876
|
+
}
|
|
2877
|
+
function subtreeHasActiveWork(containersById, rootContainerId) {
|
|
2878
|
+
for (const node of Object.values(containersById)) {
|
|
2879
|
+
if (node.container_id === rootContainerId) continue;
|
|
2880
|
+
if (!isDescendantContainer(containersById, rootContainerId, node.container_id)) continue;
|
|
2881
|
+
if (ACTIVE_CONTAINER_STATUSES.has(node.status)) return true;
|
|
2882
|
+
}
|
|
2883
|
+
return false;
|
|
2884
|
+
}
|
|
2885
|
+
function stageSortKey(node) {
|
|
2886
|
+
return node.display?.progress_current ?? 0;
|
|
2887
|
+
}
|
|
2888
|
+
function partitionWorkflowStages(stages, maxVisibleDoneCount, containersById) {
|
|
2889
|
+
const sorted = [...stages].sort(
|
|
2890
|
+
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2891
|
+
);
|
|
2892
|
+
const always = [];
|
|
2893
|
+
const done = [];
|
|
2894
|
+
for (const stage of sorted) {
|
|
2895
|
+
if (containersById && subtreeHasActiveWork(containersById, stage.container_id)) {
|
|
2896
|
+
always.push(stage);
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
if (stage.status === "running" || stage.status === "retrying" || stage.status === "failed" || stage.status === "pending" || stage.status === "blocked") {
|
|
2900
|
+
always.push(stage);
|
|
2901
|
+
} else if (stage.status === "completed" || stage.status === "skipped") {
|
|
2902
|
+
done.push(stage);
|
|
2903
|
+
} else {
|
|
2904
|
+
always.push(stage);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
const visibleDone = done.slice(Math.max(0, done.length - maxVisibleDoneCount));
|
|
2908
|
+
const visible = [...always, ...visibleDone].sort(
|
|
2909
|
+
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2910
|
+
);
|
|
2911
|
+
const hiddenCount = stages.length - visible.length;
|
|
2912
|
+
return {
|
|
2913
|
+
visible,
|
|
2914
|
+
hiddenCount,
|
|
2915
|
+
total: stages.length,
|
|
2916
|
+
visibleDoneCount: visibleDone.length,
|
|
2917
|
+
totalDoneCount: done.length
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
|
|
2921
|
+
if (activeContainerIds.includes(node.container_id)) return true;
|
|
2922
|
+
if (node.status === "completed" || node.status === "failed" || node.status === "skipped") {
|
|
2923
|
+
return false;
|
|
2924
|
+
}
|
|
2925
|
+
if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
|
|
2926
|
+
return false;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2719
2929
|
// src/view/workflow/workflowContainerLineUtils.ts
|
|
2720
2930
|
function formatContainerScopeMetaLabel(scope) {
|
|
2721
2931
|
return scope;
|
|
@@ -2785,6 +2995,27 @@ function buildWorkflowRootMeta(node) {
|
|
|
2785
2995
|
}
|
|
2786
2996
|
return withScopeMeta(node, meta);
|
|
2787
2997
|
}
|
|
2998
|
+
function buildEffectiveWorkflowRootMeta(node, containersById) {
|
|
2999
|
+
const meta = [];
|
|
3000
|
+
if (node.pattern) meta.push(node.pattern);
|
|
3001
|
+
const topStages = Object.values(containersById).filter(
|
|
3002
|
+
(n) => n.scope === "stage" && n.parent_container_id === node.container_id
|
|
3003
|
+
);
|
|
3004
|
+
const total = node.display?.progress_total ?? Math.max(topStages.length, 1);
|
|
3005
|
+
let settled = 0;
|
|
3006
|
+
for (const stage of topStages) {
|
|
3007
|
+
if (subtreeHasActiveWork(containersById, stage.container_id)) continue;
|
|
3008
|
+
if (stage.status === "completed" || stage.status === "failed" || stage.status === "skipped") {
|
|
3009
|
+
settled += 1;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (total > 0) meta.push(`${settled}/${total}`);
|
|
3013
|
+
return withScopeMeta(node, meta);
|
|
3014
|
+
}
|
|
3015
|
+
function isWorkflowRootVisuallyActive(node, containersById) {
|
|
3016
|
+
if (node.status === "running" || node.status === "retrying") return true;
|
|
3017
|
+
return subtreeHasActiveWork(containersById, node.container_id);
|
|
3018
|
+
}
|
|
2788
3019
|
function buildStageLineMeta(node) {
|
|
2789
3020
|
const progress = buildStageProgressMeta(node);
|
|
2790
3021
|
return withScopeMeta(node, progress ? [progress] : []);
|
|
@@ -3008,9 +3239,41 @@ var WorkflowAggregateContainer = memo(
|
|
|
3008
3239
|
|
|
3009
3240
|
// src/view/workflow/WorkflowParallelGroup.tsx
|
|
3010
3241
|
import { useCallback as useCallback3, useEffect as useEffect5, useMemo as useMemo4, useState as useState3, memo as memo3 } from "react";
|
|
3011
|
-
import { useStore as useStore3 } from "zustand";
|
|
3012
3242
|
|
|
3013
3243
|
// src/core/workflow/workflowParallelGroupMetrics.ts
|
|
3244
|
+
var ACTIVE_ITEM_STATUSES = /* @__PURE__ */ new Set([
|
|
3245
|
+
"running",
|
|
3246
|
+
"retrying",
|
|
3247
|
+
"pending",
|
|
3248
|
+
"blocked"
|
|
3249
|
+
]);
|
|
3250
|
+
function countMetricsFromContainerStatus(items) {
|
|
3251
|
+
let running = 0;
|
|
3252
|
+
let completed = 0;
|
|
3253
|
+
let failed = 0;
|
|
3254
|
+
for (const item of items) {
|
|
3255
|
+
if (ACTIVE_ITEM_STATUSES.has(item.status)) {
|
|
3256
|
+
running += 1;
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
switch (item.status) {
|
|
3260
|
+
case "completed":
|
|
3261
|
+
completed += 1;
|
|
3262
|
+
break;
|
|
3263
|
+
case "failed":
|
|
3264
|
+
failed += 1;
|
|
3265
|
+
break;
|
|
3266
|
+
default:
|
|
3267
|
+
break;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return {
|
|
3271
|
+
total: items.length,
|
|
3272
|
+
running,
|
|
3273
|
+
completed,
|
|
3274
|
+
failed
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3014
3277
|
function readDisplayMetrics(items) {
|
|
3015
3278
|
let hasMetrics = false;
|
|
3016
3279
|
let running = 0;
|
|
@@ -3031,34 +3294,29 @@ function readDisplayMetrics(items) {
|
|
|
3031
3294
|
if (total <= 0) total = items.length;
|
|
3032
3295
|
return { total, running, completed, failed };
|
|
3033
3296
|
}
|
|
3297
|
+
function mergeParallelGroupMetrics(fromDisplay, fromStatus, itemCount) {
|
|
3298
|
+
return {
|
|
3299
|
+
total: Math.max(fromDisplay.total, fromStatus.total, itemCount),
|
|
3300
|
+
running: Math.max(fromDisplay.running, fromStatus.running),
|
|
3301
|
+
completed: Math.max(fromDisplay.completed, fromStatus.completed),
|
|
3302
|
+
failed: Math.max(fromDisplay.failed, fromStatus.failed)
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3034
3305
|
function computeParallelGroupMetrics(items) {
|
|
3306
|
+
const fromStatus = countMetricsFromContainerStatus(items);
|
|
3035
3307
|
const fromDisplay = readDisplayMetrics(items);
|
|
3036
|
-
if (fromDisplay) return
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3308
|
+
if (!fromDisplay) return fromStatus;
|
|
3309
|
+
return mergeParallelGroupMetrics(fromDisplay, fromStatus, items.length);
|
|
3310
|
+
}
|
|
3311
|
+
function parallelGroupHasActiveWork(items, loopTreesBySessionId = {}) {
|
|
3312
|
+
const metrics = computeParallelGroupMetrics(items);
|
|
3313
|
+
if (metrics.running > 0) return true;
|
|
3040
3314
|
for (const item of items) {
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
running += 1;
|
|
3045
|
-
break;
|
|
3046
|
-
case "completed":
|
|
3047
|
-
completed += 1;
|
|
3048
|
-
break;
|
|
3049
|
-
case "failed":
|
|
3050
|
-
failed += 1;
|
|
3051
|
-
break;
|
|
3052
|
-
default:
|
|
3053
|
-
break;
|
|
3054
|
-
}
|
|
3315
|
+
const loopSessionId = item.loopSessionId;
|
|
3316
|
+
if (!loopSessionId) continue;
|
|
3317
|
+
if (isWorkflowLoopTreeActive(loopTreesBySessionId[loopSessionId])) return true;
|
|
3055
3318
|
}
|
|
3056
|
-
return
|
|
3057
|
-
total: items.length,
|
|
3058
|
-
running,
|
|
3059
|
-
completed,
|
|
3060
|
-
failed
|
|
3061
|
-
};
|
|
3319
|
+
return false;
|
|
3062
3320
|
}
|
|
3063
3321
|
function formatParallelGroupSummary(metrics) {
|
|
3064
3322
|
if (metrics.running > 0) {
|
|
@@ -3115,47 +3373,6 @@ function WorkflowStageDoneBody({
|
|
|
3115
3373
|
);
|
|
3116
3374
|
}
|
|
3117
3375
|
|
|
3118
|
-
// src/view/workflow/workflowStageListUtils.ts
|
|
3119
|
-
function stageSortKey(node) {
|
|
3120
|
-
return node.display?.progress_current ?? 0;
|
|
3121
|
-
}
|
|
3122
|
-
function partitionWorkflowStages(stages, maxVisibleDoneCount) {
|
|
3123
|
-
const sorted = [...stages].sort(
|
|
3124
|
-
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
3125
|
-
);
|
|
3126
|
-
const always = [];
|
|
3127
|
-
const done = [];
|
|
3128
|
-
for (const stage of sorted) {
|
|
3129
|
-
if (stage.status === "running" || stage.status === "retrying" || stage.status === "failed" || stage.status === "pending" || stage.status === "blocked") {
|
|
3130
|
-
always.push(stage);
|
|
3131
|
-
} else if (stage.status === "completed" || stage.status === "skipped") {
|
|
3132
|
-
done.push(stage);
|
|
3133
|
-
} else {
|
|
3134
|
-
always.push(stage);
|
|
3135
|
-
}
|
|
3136
|
-
}
|
|
3137
|
-
const visibleDone = done.slice(Math.max(0, done.length - maxVisibleDoneCount));
|
|
3138
|
-
const visible = [...always, ...visibleDone].sort(
|
|
3139
|
-
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
3140
|
-
);
|
|
3141
|
-
const hiddenCount = stages.length - visible.length;
|
|
3142
|
-
return {
|
|
3143
|
-
visible,
|
|
3144
|
-
hiddenCount,
|
|
3145
|
-
total: stages.length,
|
|
3146
|
-
visibleDoneCount: visibleDone.length,
|
|
3147
|
-
totalDoneCount: done.length
|
|
3148
|
-
};
|
|
3149
|
-
}
|
|
3150
|
-
function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
|
|
3151
|
-
if (activeContainerIds.includes(node.container_id)) return true;
|
|
3152
|
-
if (node.status === "completed" || node.status === "failed" || node.status === "skipped") {
|
|
3153
|
-
return false;
|
|
3154
|
-
}
|
|
3155
|
-
if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
|
|
3156
|
-
return false;
|
|
3157
|
-
}
|
|
3158
|
-
|
|
3159
3376
|
// src/view/workflow/WorkflowStageContainer.tsx
|
|
3160
3377
|
import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3161
3378
|
function containerTestId(node) {
|
|
@@ -3174,10 +3391,10 @@ function WorkflowStageContainerInner({
|
|
|
3174
3391
|
virtualized,
|
|
3175
3392
|
virtualizeThreshold,
|
|
3176
3393
|
groupParallelTools,
|
|
3177
|
-
suppressHostScopedLoop = false
|
|
3394
|
+
suppressHostScopedLoop = false,
|
|
3395
|
+
subtreeHasActiveWork: subtreeHasActiveWork2 = false
|
|
3178
3396
|
}) {
|
|
3179
3397
|
const { verbose } = useSessionViewOptions();
|
|
3180
|
-
const scale = useWorkflowScaleOptions();
|
|
3181
3398
|
const store = useWorkflowSessionStoreApi();
|
|
3182
3399
|
const replayHandler = useWorkflowLoopReplayHandler();
|
|
3183
3400
|
const activeLoopSessionId = useStore2(store, (s) => s.state.activeLoopSessionId);
|
|
@@ -3187,37 +3404,37 @@ function WorkflowStageContainerInner({
|
|
|
3187
3404
|
(s) => s.state.stageDoneSummariesByContainerId[node.container_id]
|
|
3188
3405
|
);
|
|
3189
3406
|
const pinLoopSession = useStore2(store, (s) => s.pinLoopSession);
|
|
3190
|
-
const uiStatus = mapContainerUiStatus(node.status);
|
|
3191
3407
|
const isRunning = node.status === "running" || node.status === "retrying";
|
|
3192
3408
|
const isDone = node.status === "completed" || node.status === "failed";
|
|
3193
3409
|
const isSkipped = node.status === "skipped";
|
|
3410
|
+
const loopStillActive = isWorkflowLoopTreeActive(loopTree);
|
|
3411
|
+
const effectivelyRunning = isRunning || loopStillActive || subtreeHasActiveWork2 && isDone;
|
|
3412
|
+
const effectivelyDone = isDone && !subtreeHasActiveWork2 && !loopStillActive;
|
|
3413
|
+
const uiStatus = mapContainerUiStatus(effectivelyRunning ? "running" : node.status);
|
|
3194
3414
|
const isActive = isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds);
|
|
3195
3415
|
const doneSummaryFromTree = useMemo3(
|
|
3196
3416
|
() => loopTree && isDone ? buildStageDoneSummary(loopTree) : null,
|
|
3197
3417
|
[loopTree, isDone]
|
|
3198
3418
|
);
|
|
3199
3419
|
const doneSummary = doneSummaryFromTree ?? (isDone ? cachedSummary : null);
|
|
3200
|
-
const shouldAutoExpandRunning =
|
|
3420
|
+
const shouldAutoExpandRunning = effectivelyRunning && !isSkipped;
|
|
3201
3421
|
const [expanded, setExpanded] = useState2(shouldAutoExpandRunning);
|
|
3202
3422
|
useEffect4(() => {
|
|
3203
3423
|
if (shouldAutoExpandRunning) {
|
|
3204
3424
|
setExpanded(true);
|
|
3205
3425
|
return;
|
|
3206
3426
|
}
|
|
3207
|
-
if (
|
|
3427
|
+
if (effectivelyDone || isSkipped) {
|
|
3208
3428
|
setExpanded(false);
|
|
3209
|
-
return;
|
|
3210
3429
|
}
|
|
3211
|
-
|
|
3212
|
-
setExpanded(false);
|
|
3213
|
-
}
|
|
3214
|
-
}, [shouldAutoExpandRunning, isRunning, isDone, isSkipped, scale.onlyExpandActiveRunning]);
|
|
3430
|
+
}, [shouldAutoExpandRunning, effectivelyDone, isSkipped]);
|
|
3215
3431
|
const requestLoopHydration = useCallback2(() => {
|
|
3432
|
+
if (suppressHostScopedLoop) return;
|
|
3216
3433
|
const loopSessionId = node.loopSessionId;
|
|
3217
3434
|
if (!loopSessionId || loopTree) return;
|
|
3218
3435
|
pinLoopSession(loopSessionId);
|
|
3219
3436
|
void replayHandler?.(loopSessionId);
|
|
3220
|
-
}, [node.loopSessionId, loopTree, pinLoopSession, replayHandler]);
|
|
3437
|
+
}, [suppressHostScopedLoop, node.loopSessionId, loopTree, pinLoopSession, replayHandler]);
|
|
3221
3438
|
const toggleExpanded = useCallback2(() => {
|
|
3222
3439
|
setExpanded((prev) => {
|
|
3223
3440
|
const next = !prev;
|
|
@@ -3236,17 +3453,18 @@ function WorkflowStageContainerInner({
|
|
|
3236
3453
|
window.addEventListener("keydown", onKeyDown);
|
|
3237
3454
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
3238
3455
|
}, [requestLoopHydration]);
|
|
3456
|
+
const testId = containerTestId(node);
|
|
3457
|
+
const isItem = node.scope === "item";
|
|
3239
3458
|
const displayTitle = node.title;
|
|
3240
3459
|
const usesScopedLoop = Boolean(node.loopSessionId) && !suppressHostScopedLoop;
|
|
3241
|
-
const runningLoopTree = loopTree ?? (usesScopedLoop &&
|
|
3460
|
+
const runningLoopTree = loopTree ?? (usesScopedLoop && effectivelyRunning ? createWorkflowRunningLoopTree() : void 0);
|
|
3461
|
+
const itemLoopVirtualized = isItem ? false : virtualized;
|
|
3242
3462
|
const staticContentTeaser = !usesScopedLoop && !loopTree && node.content_blocks.length > 0 ? node.content_blocks[node.content_blocks.length - 1].preview : "";
|
|
3243
3463
|
const teaserText = doneSummary?.teaser || staticContentTeaser;
|
|
3244
3464
|
const expandHint = doneSummary ? formatTeaserExpandHint(doneSummary.extraLines, verbose) : null;
|
|
3245
3465
|
const doneLabel = doneSummary?.label ?? "done";
|
|
3246
|
-
const statusLabel =
|
|
3247
|
-
const
|
|
3248
|
-
const isItem = node.scope === "item";
|
|
3249
|
-
const canToggleShell = isRunning && !isSkipped || isDone;
|
|
3466
|
+
const statusLabel = effectivelyDone ? doneLabel : suppressHostScopedLoop && effectivelyRunning ? "running" : mapContainerStatusLabel(effectivelyRunning ? "running" : node.status);
|
|
3467
|
+
const canToggleShell = effectivelyRunning && !isSkipped || effectivelyDone;
|
|
3250
3468
|
const shellToggle = canToggleShell ? toggleExpanded : void 0;
|
|
3251
3469
|
return /* @__PURE__ */ jsxs5(
|
|
3252
3470
|
"div",
|
|
@@ -3272,13 +3490,13 @@ function WorkflowStageContainerInner({
|
|
|
3272
3490
|
onToggle: shellToggle,
|
|
3273
3491
|
interactive: canToggleShell,
|
|
3274
3492
|
expanded,
|
|
3275
|
-
headerTestId:
|
|
3493
|
+
headerTestId: effectivelyDone ? `${testId}-done` : void 0,
|
|
3276
3494
|
titleTestId: `${testId}-title`,
|
|
3277
3495
|
descriptionTestId: `${testId}-desc`,
|
|
3278
3496
|
className: isItem ? "lax-workflow-parallel-item__header" : void 0
|
|
3279
3497
|
}
|
|
3280
3498
|
),
|
|
3281
|
-
|
|
3499
|
+
effectivelyDone && !expanded && (teaserText || expandHint) ? /* @__PURE__ */ jsxs5("div", { className: "lax-workflow-stage-container__done-teaser", children: [
|
|
3282
3500
|
teaserText ? /* @__PURE__ */ jsx10(
|
|
3283
3501
|
"span",
|
|
3284
3502
|
{
|
|
@@ -3304,7 +3522,7 @@ function WorkflowStageContainerInner({
|
|
|
3304
3522
|
))
|
|
3305
3523
|
}
|
|
3306
3524
|
) : null,
|
|
3307
|
-
expanded &&
|
|
3525
|
+
expanded && effectivelyRunning && runningLoopTree ? /* @__PURE__ */ jsx10(
|
|
3308
3526
|
"div",
|
|
3309
3527
|
{
|
|
3310
3528
|
className: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body",
|
|
@@ -3314,25 +3532,25 @@ function WorkflowStageContainerInner({
|
|
|
3314
3532
|
{
|
|
3315
3533
|
tree: runningLoopTree,
|
|
3316
3534
|
showTaskListFooter: false,
|
|
3317
|
-
virtualized,
|
|
3535
|
+
virtualized: itemLoopVirtualized,
|
|
3318
3536
|
virtualizeThreshold,
|
|
3319
3537
|
groupParallelTools
|
|
3320
3538
|
}
|
|
3321
3539
|
)
|
|
3322
3540
|
}
|
|
3323
3541
|
) : null,
|
|
3324
|
-
|
|
3542
|
+
effectivelyDone && expanded ? loopTree ? /* @__PURE__ */ jsx10(
|
|
3325
3543
|
WorkflowStageDoneBody,
|
|
3326
3544
|
{
|
|
3327
3545
|
node,
|
|
3328
3546
|
loopTree,
|
|
3329
3547
|
testId: bodyTestId(node),
|
|
3330
|
-
virtualized,
|
|
3548
|
+
virtualized: itemLoopVirtualized,
|
|
3331
3549
|
virtualizeThreshold,
|
|
3332
3550
|
groupParallelTools,
|
|
3333
3551
|
bodyClassName: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body"
|
|
3334
3552
|
}
|
|
3335
|
-
) : /* @__PURE__ */ jsxs5(
|
|
3553
|
+
) : suppressHostScopedLoop ? null : /* @__PURE__ */ jsxs5(
|
|
3336
3554
|
"div",
|
|
3337
3555
|
{
|
|
3338
3556
|
className: "lax-workflow-stage-container__body lax-workflow-loop-replay-pending",
|
|
@@ -3349,6 +3567,7 @@ function WorkflowStageContainerInner({
|
|
|
3349
3567
|
}
|
|
3350
3568
|
function workflowStageContainerPropsEqual(prev, next) {
|
|
3351
3569
|
if (prev.suppressHostScopedLoop !== next.suppressHostScopedLoop) return false;
|
|
3570
|
+
if (prev.subtreeHasActiveWork !== next.subtreeHasActiveWork) return false;
|
|
3352
3571
|
if (prev.loopTree !== next.loopTree) return false;
|
|
3353
3572
|
if (!workflowContainerViewPropsEqual(prev, next)) return false;
|
|
3354
3573
|
return workflowContainerNodeVisualEqual(prev.node, next.node);
|
|
@@ -3370,34 +3589,23 @@ function WorkflowParallelGroupInner({
|
|
|
3370
3589
|
virtualizeThreshold,
|
|
3371
3590
|
groupParallelTools
|
|
3372
3591
|
}) {
|
|
3373
|
-
const scale = useWorkflowScaleOptions();
|
|
3374
|
-
const store = useWorkflowSessionStoreApi();
|
|
3375
|
-
const activeLoopSessionId = useStore3(store, (s) => s.state.activeLoopSessionId);
|
|
3376
|
-
const activeContainerIds = useStore3(store, (s) => s.state.containerTree.activeContainerIds);
|
|
3377
3592
|
const metrics = useMemo4(() => computeParallelGroupMetrics(items), [items]);
|
|
3378
3593
|
const summary = formatParallelGroupSummary(metrics);
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
() => items.some((item) => isActiveWorkflowContainer(item, activeLoopSessionId, activeContainerIds)),
|
|
3383
|
-
[items, activeLoopSessionId, activeContainerIds]
|
|
3594
|
+
const hasActiveWork = useMemo4(
|
|
3595
|
+
() => parallelGroupHasActiveWork(items, loopTreesBySessionId),
|
|
3596
|
+
[items, loopTreesBySessionId]
|
|
3384
3597
|
);
|
|
3385
3598
|
const allSettled = metrics.total > 0 && metrics.running === 0 && metrics.completed + metrics.failed >= metrics.total;
|
|
3386
|
-
const
|
|
3387
|
-
const [expanded, setExpanded] = useState3(shouldAutoExpandGroup);
|
|
3599
|
+
const [expanded, setExpanded] = useState3(hasActiveWork);
|
|
3388
3600
|
useEffect5(() => {
|
|
3389
|
-
if (
|
|
3601
|
+
if (hasActiveWork) {
|
|
3390
3602
|
setExpanded(true);
|
|
3391
3603
|
return;
|
|
3392
3604
|
}
|
|
3393
|
-
if (hasRunning && scale.onlyExpandActiveRunning) {
|
|
3394
|
-
setExpanded(false);
|
|
3395
|
-
return;
|
|
3396
|
-
}
|
|
3397
3605
|
if (allSettled) {
|
|
3398
3606
|
setExpanded(false);
|
|
3399
3607
|
}
|
|
3400
|
-
}, [
|
|
3608
|
+
}, [hasActiveWork, allSettled]);
|
|
3401
3609
|
const toggleExpanded = useCallback3(() => {
|
|
3402
3610
|
setExpanded((prev) => !prev);
|
|
3403
3611
|
}, []);
|
|
@@ -3434,7 +3642,7 @@ function WorkflowParallelGroupInner({
|
|
|
3434
3642
|
WorkflowExpandTrigger,
|
|
3435
3643
|
{
|
|
3436
3644
|
summary,
|
|
3437
|
-
expandHint: !expanded &&
|
|
3645
|
+
expandHint: !expanded && hasActiveWork ? "(click to expand agents)" : !expanded && !hasActiveWork ? "(ctrl+o to expand)" : null,
|
|
3438
3646
|
onToggle: toggleExpanded,
|
|
3439
3647
|
expanded,
|
|
3440
3648
|
testId: `lax-workflow-parallel-group-summary-${groupKey}`,
|
|
@@ -3469,11 +3677,14 @@ var WorkflowParallelGroup = memo3(WorkflowParallelGroupInner, workflowParallelGr
|
|
|
3469
3677
|
import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3470
3678
|
function WorkflowRootContainer({
|
|
3471
3679
|
node,
|
|
3472
|
-
children
|
|
3680
|
+
children,
|
|
3681
|
+
containersById
|
|
3473
3682
|
}) {
|
|
3474
3683
|
const { verbose } = useSessionViewOptions();
|
|
3475
|
-
const
|
|
3476
|
-
const
|
|
3684
|
+
const visuallyActive = containersById ? isWorkflowRootVisuallyActive(node, containersById) : node.status === "running" || node.status === "retrying";
|
|
3685
|
+
const uiStatus = mapContainerUiStatus(visuallyActive ? "running" : node.status);
|
|
3686
|
+
const statusLabel = mapWorkflowRootStatusLabel(visuallyActive ? "running" : node.status);
|
|
3687
|
+
const meta = containersById ? buildEffectiveWorkflowRootMeta(node, containersById) : buildWorkflowRootMeta(node);
|
|
3477
3688
|
return /* @__PURE__ */ jsxs7("div", { className: "lax-workflow-root", "data-testid": "lax-workflow-root", children: [
|
|
3478
3689
|
/* @__PURE__ */ jsx12(
|
|
3479
3690
|
WorkflowContainerLine,
|
|
@@ -3482,7 +3693,7 @@ function WorkflowRootContainer({
|
|
|
3482
3693
|
title: node.title,
|
|
3483
3694
|
description: node.subtitle || void 0,
|
|
3484
3695
|
uiStatus,
|
|
3485
|
-
meta
|
|
3696
|
+
meta,
|
|
3486
3697
|
statusLabel,
|
|
3487
3698
|
titleTestId: "lax-workflow-root-title",
|
|
3488
3699
|
descriptionTestId: "lax-workflow-root-desc",
|
|
@@ -3610,31 +3821,36 @@ function buildDoneSummary(node) {
|
|
|
3610
3821
|
function WorkflowSubworkflowContainer({
|
|
3611
3822
|
node,
|
|
3612
3823
|
depth = 1,
|
|
3613
|
-
children
|
|
3824
|
+
children,
|
|
3825
|
+
subtreeHasActiveWork: subtreeHasActiveWork2 = false
|
|
3614
3826
|
}) {
|
|
3615
3827
|
const { verbose } = useSessionViewOptions();
|
|
3616
|
-
const uiStatus = mapContainerUiStatus(node.status);
|
|
3617
3828
|
const isRunning = node.status === "running" || node.status === "retrying";
|
|
3618
3829
|
const isDone = node.status === "completed" || node.status === "failed";
|
|
3830
|
+
const effectivelyRunning = isRunning || subtreeHasActiveWork2;
|
|
3831
|
+
const effectivelyDone = isDone && !subtreeHasActiveWork2;
|
|
3832
|
+
const uiStatus = mapContainerUiStatus(
|
|
3833
|
+
effectivelyRunning ? "running" : node.status
|
|
3834
|
+
);
|
|
3619
3835
|
const testId = `lax-workflow-subworkflow-${node.scope_key}`;
|
|
3620
3836
|
const teaser = useMemo6(() => {
|
|
3621
3837
|
if (node.content_blocks.length === 0) return "";
|
|
3622
3838
|
return node.content_blocks[node.content_blocks.length - 1]?.preview ?? "";
|
|
3623
3839
|
}, [node.content_blocks]);
|
|
3624
|
-
const [expanded, setExpanded] = useState5(
|
|
3840
|
+
const [expanded, setExpanded] = useState5(effectivelyRunning);
|
|
3625
3841
|
useEffect7(() => {
|
|
3626
|
-
if (
|
|
3627
|
-
if (
|
|
3628
|
-
}, [
|
|
3842
|
+
if (effectivelyRunning) setExpanded(true);
|
|
3843
|
+
else if (effectivelyDone) setExpanded(false);
|
|
3844
|
+
}, [effectivelyRunning, effectivelyDone]);
|
|
3629
3845
|
const toggleExpanded = useCallback5(() => {
|
|
3630
3846
|
setExpanded((prev) => !prev);
|
|
3631
3847
|
}, []);
|
|
3632
|
-
const shellToggle =
|
|
3848
|
+
const shellToggle = effectivelyRunning ? toggleExpanded : effectivelyDone && expanded ? toggleExpanded : void 0;
|
|
3633
3849
|
const depthStyle = {
|
|
3634
3850
|
"--lax-workflow-depth": depth,
|
|
3635
3851
|
"--lax-workflow-indent-unit": workflowIndentUnit(depth)
|
|
3636
3852
|
};
|
|
3637
|
-
const showShellLine = !(
|
|
3853
|
+
const showShellLine = !(effectivelyDone && !expanded);
|
|
3638
3854
|
return /* @__PURE__ */ jsxs9(
|
|
3639
3855
|
"div",
|
|
3640
3856
|
{
|
|
@@ -3651,9 +3867,11 @@ function WorkflowSubworkflowContainer({
|
|
|
3651
3867
|
description: node.subtitle || void 0,
|
|
3652
3868
|
uiStatus,
|
|
3653
3869
|
meta: buildWorkflowRootMeta(node),
|
|
3654
|
-
statusLabel: mapWorkflowRootStatusLabel(
|
|
3870
|
+
statusLabel: mapWorkflowRootStatusLabel(
|
|
3871
|
+
effectivelyRunning ? "running" : node.status
|
|
3872
|
+
),
|
|
3655
3873
|
onToggle: shellToggle,
|
|
3656
|
-
interactive:
|
|
3874
|
+
interactive: effectivelyRunning,
|
|
3657
3875
|
expanded,
|
|
3658
3876
|
titleTestId: `${testId}-title`,
|
|
3659
3877
|
descriptionTestId: `${testId}-desc`,
|
|
@@ -3673,13 +3891,13 @@ function WorkflowSubworkflowContainer({
|
|
|
3673
3891
|
!expanded && children ? /* @__PURE__ */ jsx14(
|
|
3674
3892
|
WorkflowExpandTrigger,
|
|
3675
3893
|
{
|
|
3676
|
-
summary: `${node.title} \xB7 ${
|
|
3894
|
+
summary: `${node.title} \xB7 ${effectivelyDone ? buildDoneSummary(node) : "running"}`,
|
|
3677
3895
|
summaryTestId: `${testId}-title`,
|
|
3678
3896
|
teaser: teaser || void 0,
|
|
3679
|
-
expandHint:
|
|
3897
|
+
expandHint: effectivelyRunning ? "(click to expand subworkflow)" : void 0,
|
|
3680
3898
|
onToggle: () => setExpanded(true),
|
|
3681
3899
|
expanded: false,
|
|
3682
|
-
testId:
|
|
3900
|
+
testId: effectivelyDone ? `${testId}-done` : `${testId}-running-collapsed`,
|
|
3683
3901
|
teaserTestId: teaser ? `${testId}-teaser` : void 0
|
|
3684
3902
|
}
|
|
3685
3903
|
) : null
|
|
@@ -3705,6 +3923,7 @@ function collectSiblingItems(node, containersById) {
|
|
|
3705
3923
|
}
|
|
3706
3924
|
function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps, options) {
|
|
3707
3925
|
const loopTree = resolveLoopTree2(node, loopTreesBySessionId);
|
|
3926
|
+
const subtreeActive = options?.subtreeHasActiveWork ?? subtreeHasActiveWork(containerTree.containersById, node.container_id);
|
|
3708
3927
|
switch (node.scope) {
|
|
3709
3928
|
case "aggregate":
|
|
3710
3929
|
return /* @__PURE__ */ jsx15(
|
|
@@ -3737,6 +3956,7 @@ function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, v
|
|
|
3737
3956
|
loopTree,
|
|
3738
3957
|
depth,
|
|
3739
3958
|
suppressHostScopedLoop: options?.suppressHostScopedLoop,
|
|
3959
|
+
subtreeHasActiveWork: subtreeActive,
|
|
3740
3960
|
...viewProps
|
|
3741
3961
|
},
|
|
3742
3962
|
node.container_id
|
|
@@ -3790,6 +4010,10 @@ function renderContainerNode(node, containerTree, loopTreesBySessionId, viewProp
|
|
|
3790
4010
|
{
|
|
3791
4011
|
node,
|
|
3792
4012
|
depth,
|
|
4013
|
+
subtreeHasActiveWork: subtreeHasActiveWork(
|
|
4014
|
+
containerTree.containersById,
|
|
4015
|
+
node.container_id
|
|
4016
|
+
),
|
|
3793
4017
|
children: hasNested ? nested : null
|
|
3794
4018
|
},
|
|
3795
4019
|
node.container_id
|
|
@@ -3851,8 +4075,8 @@ function WorkflowStageListPanel({
|
|
|
3851
4075
|
const [extraCompletedPages, setExtraCompletedPages] = useState6(0);
|
|
3852
4076
|
const maxVisibleDoneCount = scale.completedStagePageSize + extraCompletedPages * scale.completedStagePageSize;
|
|
3853
4077
|
const partition = useMemo7(
|
|
3854
|
-
() => partitionWorkflowStages(stages, maxVisibleDoneCount),
|
|
3855
|
-
[stages, maxVisibleDoneCount]
|
|
4078
|
+
() => partitionWorkflowStages(stages, maxVisibleDoneCount, containerTree.containersById),
|
|
4079
|
+
[stages, maxVisibleDoneCount, containerTree.containersById]
|
|
3856
4080
|
);
|
|
3857
4081
|
if (partition.visible.length === 0) return null;
|
|
3858
4082
|
const showMoreCount = Math.min(
|
|
@@ -3950,17 +4174,25 @@ function WorkflowChrome({
|
|
|
3950
4174
|
}
|
|
3951
4175
|
) });
|
|
3952
4176
|
}
|
|
3953
|
-
return /* @__PURE__ */ jsx15("div", { className: "lax-workflow-chrome", "data-testid": "lax-workflow-chrome", children: rootNodes.map((workflowNode) => /* @__PURE__ */ jsx15(
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
4177
|
+
return /* @__PURE__ */ jsx15("div", { className: "lax-workflow-chrome", "data-testid": "lax-workflow-chrome", children: rootNodes.map((workflowNode) => /* @__PURE__ */ jsx15(
|
|
4178
|
+
WorkflowRootContainer,
|
|
4179
|
+
{
|
|
4180
|
+
node: workflowNode,
|
|
4181
|
+
containersById: containerTree.containersById,
|
|
4182
|
+
children: renderWorkflowChildren(
|
|
4183
|
+
workflowNode,
|
|
4184
|
+
containerTree,
|
|
4185
|
+
loopTreesBySessionId,
|
|
4186
|
+
viewProps
|
|
4187
|
+
)
|
|
4188
|
+
},
|
|
4189
|
+
workflowNode.container_id
|
|
4190
|
+
)) });
|
|
3959
4191
|
}
|
|
3960
4192
|
|
|
3961
4193
|
// src/view/workflow/WorkflowTaskListFooter.tsx
|
|
3962
4194
|
import { useMemo as useMemo8, useState as useState7 } from "react";
|
|
3963
|
-
import { useStore as
|
|
4195
|
+
import { useStore as useStore3 } from "zustand";
|
|
3964
4196
|
|
|
3965
4197
|
// src/view/workflow/workflowTaskListMerge.ts
|
|
3966
4198
|
var STATUS_ORDER = {
|
|
@@ -4040,7 +4272,7 @@ function MessageResponse({ children }) {
|
|
|
4040
4272
|
}
|
|
4041
4273
|
function WorkflowTaskListFooter() {
|
|
4042
4274
|
const store = useWorkflowSessionStoreApi();
|
|
4043
|
-
const state =
|
|
4275
|
+
const state = useStore3(store, (s) => s.state);
|
|
4044
4276
|
const [expanded, setExpanded] = useState7(false);
|
|
4045
4277
|
const footerState = useMemo8(() => deriveWorkflowTaskFooterState(state), [state]);
|
|
4046
4278
|
const visible = shouldShowWorkflowTaskListFooter(state);
|
|
@@ -4080,7 +4312,7 @@ function WorkflowTaskListFooter() {
|
|
|
4080
4312
|
}
|
|
4081
4313
|
|
|
4082
4314
|
// src/view/workflow/WorkflowSession.tsx
|
|
4083
|
-
import { useStore as
|
|
4315
|
+
import { useStore as useStore4 } from "zustand";
|
|
4084
4316
|
import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4085
4317
|
function WorkflowSession({
|
|
4086
4318
|
source,
|
|
@@ -4103,6 +4335,7 @@ function WorkflowSession({
|
|
|
4103
4335
|
groupParallelTools = false,
|
|
4104
4336
|
workflowScale,
|
|
4105
4337
|
onRequestLoopReplay,
|
|
4338
|
+
onStreamComplete,
|
|
4106
4339
|
children
|
|
4107
4340
|
}) {
|
|
4108
4341
|
const storeRef = useRef3(null);
|
|
@@ -4152,39 +4385,46 @@ function WorkflowSession({
|
|
|
4152
4385
|
workspaceRoot
|
|
4153
4386
|
]
|
|
4154
4387
|
);
|
|
4155
|
-
const containerTree =
|
|
4388
|
+
const containerTree = useStore4(
|
|
4156
4389
|
storeRef.current,
|
|
4157
4390
|
(s) => s.state.containerTree
|
|
4158
4391
|
);
|
|
4159
|
-
const loopTreesBySessionId =
|
|
4392
|
+
const loopTreesBySessionId = useStore4(
|
|
4160
4393
|
storeRef.current,
|
|
4161
4394
|
(s) => s.state.loopTreesBySessionId
|
|
4162
4395
|
);
|
|
4163
|
-
const sessionStatus =
|
|
4396
|
+
const sessionStatus = useStore4(
|
|
4164
4397
|
storeRef.current,
|
|
4165
4398
|
(s) => s.state.status
|
|
4166
4399
|
);
|
|
4167
|
-
const internalErrors =
|
|
4400
|
+
const internalErrors = useStore4(
|
|
4168
4401
|
storeRef.current,
|
|
4169
4402
|
(s) => s.state.internalErrors
|
|
4170
4403
|
);
|
|
4404
|
+
const onErrorRef = useRef3(onError);
|
|
4405
|
+
const onStreamCompleteRef = useRef3(onStreamComplete);
|
|
4406
|
+
onErrorRef.current = onError;
|
|
4407
|
+
onStreamCompleteRef.current = onStreamComplete;
|
|
4171
4408
|
useEffect8(() => {
|
|
4172
4409
|
const store = storeRef.current;
|
|
4173
4410
|
const controller = new AbortController();
|
|
4174
4411
|
void source.start((event, ctx) => {
|
|
4175
4412
|
store.getState().applyEvent(event, ctx);
|
|
4176
|
-
}, controller.signal).
|
|
4413
|
+
}, controller.signal).then(() => {
|
|
4414
|
+
store.getState().markStreamComplete();
|
|
4415
|
+
onStreamCompleteRef.current?.();
|
|
4416
|
+
}).catch((err) => {
|
|
4177
4417
|
if (err instanceof SseTransportTerminalError) {
|
|
4178
4418
|
store.getState().markAsError(err);
|
|
4179
|
-
|
|
4419
|
+
onErrorRef.current?.(err);
|
|
4180
4420
|
} else if (err instanceof Error) {
|
|
4181
|
-
|
|
4421
|
+
onErrorRef.current?.(err);
|
|
4182
4422
|
}
|
|
4183
4423
|
});
|
|
4184
4424
|
return () => {
|
|
4185
4425
|
controller.abort();
|
|
4186
4426
|
};
|
|
4187
|
-
}, [source
|
|
4427
|
+
}, [source]);
|
|
4188
4428
|
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(
|
|
4189
4429
|
"div",
|
|
4190
4430
|
{
|
|
@@ -4407,6 +4647,7 @@ export {
|
|
|
4407
4647
|
createReplaySource,
|
|
4408
4648
|
createSessionStore,
|
|
4409
4649
|
createWorkflowSessionStore,
|
|
4650
|
+
deriveLoopSessionIdFromAgentEvent,
|
|
4410
4651
|
displayPath,
|
|
4411
4652
|
entryKey,
|
|
4412
4653
|
filterAgentLoopReplayEvents,
|