langchain_agentx_stream_ui 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{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 +562 -325
- 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;
|
|
@@ -1727,6 +1905,9 @@ function settleCompletedStageSubtree(tree, stageContainerId) {
|
|
|
1727
1905
|
}
|
|
1728
1906
|
if (node.container_id === stageContainerId) continue;
|
|
1729
1907
|
if (node.status === "completed" || node.status === "failed") continue;
|
|
1908
|
+
if (node.status === "running" || node.status === "retrying" || node.status === "pending" || node.status === "blocked") {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1730
1911
|
next = upsertRuntimeNode(next, {
|
|
1731
1912
|
...node,
|
|
1732
1913
|
status: "completed",
|
|
@@ -1908,17 +2089,26 @@ function findContainerByLoopSession(tree, loopSessionId) {
|
|
|
1908
2089
|
return findContainerIdByLoopSession(tree, loopSessionId);
|
|
1909
2090
|
}
|
|
1910
2091
|
var WORKFLOW_DEFAULT_LOOP_SESSION_ID = "workflow-loop-default";
|
|
1911
|
-
function resolveAgentLoopSessionId(event, activeLoopSessionId) {
|
|
2092
|
+
function resolveAgentLoopSessionId(event, activeLoopSessionId, containerTree) {
|
|
1912
2093
|
if (event.session_id) return event.session_id;
|
|
1913
2094
|
if (isWorkflowStructureEventType(event.event_type)) return null;
|
|
1914
2095
|
if (event.event_type === "task-list-snapshot") return null;
|
|
1915
2096
|
const derived = deriveLoopSessionIdFromAgentEvent(event);
|
|
1916
2097
|
if (derived) return derived;
|
|
2098
|
+
const byLoop = containersByLoopSessionId(containerTree);
|
|
2099
|
+
if (activeLoopSessionId) {
|
|
2100
|
+
const activeContainer = byLoop.get(activeLoopSessionId);
|
|
2101
|
+
if (activeContainer && (activeContainer.status === "running" || activeContainer.status === "retrying")) {
|
|
2102
|
+
return activeLoopSessionId;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
const runningLoopIds = [...byLoop.entries()].filter(([, node]) => node.status === "running" || node.status === "retrying").map(([loopSessionId]) => loopSessionId);
|
|
2106
|
+
if (runningLoopIds.length === 1) return runningLoopIds[0];
|
|
1917
2107
|
if (activeLoopSessionId) return activeLoopSessionId;
|
|
1918
2108
|
return WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
1919
2109
|
}
|
|
1920
2110
|
function bindLoopSessionToContainerTree(tree, event, activeLoopSessionId) {
|
|
1921
|
-
const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId);
|
|
2111
|
+
const sessionId = resolveAgentLoopSessionId(event, activeLoopSessionId, tree);
|
|
1922
2112
|
if (!sessionId) return tree;
|
|
1923
2113
|
let nextTree = ensureEmbeddedSubworkflowContainers(tree, sessionId);
|
|
1924
2114
|
if (findContainerByLoopSession(nextTree, sessionId)) return nextTree;
|
|
@@ -2157,7 +2347,7 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
2157
2347
|
if (isWorkflowStructureEventType(event.event_type)) {
|
|
2158
2348
|
return applyStructureEvent(state, event);
|
|
2159
2349
|
}
|
|
2160
|
-
const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
2350
|
+
const loopSessionId = resolveAgentLoopSessionId(event, state.activeLoopSessionId, state.containerTree) ?? WORKFLOW_DEFAULT_LOOP_SESSION_ID;
|
|
2161
2351
|
let containerTree = bindLoopSessionToContainerTree(
|
|
2162
2352
|
state.containerTree,
|
|
2163
2353
|
event,
|
|
@@ -2176,6 +2366,11 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
2176
2366
|
next.containerTree,
|
|
2177
2367
|
next.workflowProgress
|
|
2178
2368
|
);
|
|
2369
|
+
let status = next.status;
|
|
2370
|
+
if (workflowProgress.status === "done") status = "done";
|
|
2371
|
+
else if (workflowProgress.status === "error") status = "error";
|
|
2372
|
+
else if (reducedLoop.status === "error") status = "error";
|
|
2373
|
+
else if (status === "connecting") status = "running";
|
|
2179
2374
|
return {
|
|
2180
2375
|
...next,
|
|
2181
2376
|
workflowProgress,
|
|
@@ -2184,7 +2379,85 @@ function reduceWorkflowSession(state, event, eventIndex, options) {
|
|
|
2184
2379
|
[loopSessionId]: reducedLoop
|
|
2185
2380
|
},
|
|
2186
2381
|
activeLoopSessionId: loopSessionId,
|
|
2187
|
-
status
|
|
2382
|
+
status
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
function finalizeWorkflowSessionOnStreamComplete(state) {
|
|
2386
|
+
if (state.status === "done" || state.status === "error") {
|
|
2387
|
+
return { ...state, activeLoopSessionId: null };
|
|
2388
|
+
}
|
|
2389
|
+
const tree = state.containerTree;
|
|
2390
|
+
const rootId = tree.rootContainerIds[0];
|
|
2391
|
+
if (!rootId) {
|
|
2392
|
+
return { ...state, status: "done", activeLoopSessionId: null };
|
|
2393
|
+
}
|
|
2394
|
+
const root = tree.containersById[rootId];
|
|
2395
|
+
if (!root) {
|
|
2396
|
+
return { ...state, status: "done", activeLoopSessionId: null };
|
|
2397
|
+
}
|
|
2398
|
+
const topLevelStages = Object.values(tree.containersById).filter(
|
|
2399
|
+
(node) => node.scope === "stage" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
|
|
2400
|
+
);
|
|
2401
|
+
const progressTotal = root.display?.progress_total ?? (topLevelStages.reduce(
|
|
2402
|
+
(max, s) => Math.max(max, s.display?.progress_total ?? 0),
|
|
2403
|
+
0
|
|
2404
|
+
) || topLevelStages.length);
|
|
2405
|
+
const settledStageCount = topLevelStages.filter(
|
|
2406
|
+
(s) => s.status === "completed" || s.status === "failed" || s.status === "skipped"
|
|
2407
|
+
).length;
|
|
2408
|
+
const progressCurrent = Math.max(
|
|
2409
|
+
root.display?.progress_current ?? 0,
|
|
2410
|
+
settledStageCount
|
|
2411
|
+
);
|
|
2412
|
+
const rootParallelItems = Object.values(tree.containersById).filter(
|
|
2413
|
+
(node) => node.scope === "item" && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth
|
|
2414
|
+
);
|
|
2415
|
+
const rootParallelSettled = topLevelStages.length === 0 && rootParallelItems.length > 0 && rootParallelItems.every(
|
|
2416
|
+
(item) => item.status === "completed" || item.status === "failed" || item.status === "skipped"
|
|
2417
|
+
) && !Object.values(tree.containersById).some(
|
|
2418
|
+
(node) => node.container_id !== rootId && node.workflow_path === root.workflow_path && node.workflow_depth === root.workflow_depth && (node.status === "running" || node.status === "retrying" || node.status === "pending")
|
|
2419
|
+
);
|
|
2420
|
+
const workflowVisuallyComplete = root.status === "completed" || root.status === "failed" || progressTotal > 0 && progressCurrent >= progressTotal || rootParallelSettled;
|
|
2421
|
+
if (!workflowVisuallyComplete) {
|
|
2422
|
+
return state;
|
|
2423
|
+
}
|
|
2424
|
+
let containersById = { ...tree.containersById };
|
|
2425
|
+
for (const [id, node] of Object.entries(containersById)) {
|
|
2426
|
+
if (node.status === "running" || node.status === "pending") {
|
|
2427
|
+
containersById[id] = { ...node, status: "completed", is_open: false };
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const settledRoot = containersById[rootId] ?? root;
|
|
2431
|
+
if (settledRoot.status !== "completed" && settledRoot.status !== "failed") {
|
|
2432
|
+
containersById = {
|
|
2433
|
+
...containersById,
|
|
2434
|
+
[rootId]: { ...settledRoot, status: "completed", is_open: false }
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
const loopTreesBySessionId = { ...state.loopTreesBySessionId };
|
|
2438
|
+
for (const [loopSessionId, loopTree] of Object.entries(loopTreesBySessionId)) {
|
|
2439
|
+
if (loopTree.status === "running" || loopTree.status === "connecting") {
|
|
2440
|
+
loopTreesBySessionId[loopSessionId] = finalizeWorkflowLoopTree(loopTree, "done");
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const containerTree = {
|
|
2444
|
+
...tree,
|
|
2445
|
+
containersById,
|
|
2446
|
+
activeContainerIds: tree.activeContainerIds.filter(
|
|
2447
|
+
(id) => containersById[id]?.is_open
|
|
2448
|
+
)
|
|
2449
|
+
};
|
|
2450
|
+
const workflowProgress = syncWorkflowProgressFromContainerTree(
|
|
2451
|
+
containerTree,
|
|
2452
|
+
state.workflowProgress
|
|
2453
|
+
);
|
|
2454
|
+
return {
|
|
2455
|
+
...state,
|
|
2456
|
+
status: workflowProgress.status === "error" ? "error" : "done",
|
|
2457
|
+
containerTree,
|
|
2458
|
+
workflowProgress,
|
|
2459
|
+
loopTreesBySessionId,
|
|
2460
|
+
activeLoopSessionId: null
|
|
2188
2461
|
};
|
|
2189
2462
|
}
|
|
2190
2463
|
function reduceWorkflowEvents(events, options) {
|
|
@@ -2194,180 +2467,37 @@ function reduceWorkflowEvents(events, options) {
|
|
|
2194
2467
|
);
|
|
2195
2468
|
}
|
|
2196
2469
|
|
|
2197
|
-
// src/core/
|
|
2198
|
-
|
|
2199
|
-
|
|
2470
|
+
// src/core/workflowSessionStore.ts
|
|
2471
|
+
var DEFAULT_HYDRATE_CHUNK_SIZE = 50;
|
|
2472
|
+
function nextAnimationFrame() {
|
|
2473
|
+
return new Promise((resolve) => {
|
|
2474
|
+
requestAnimationFrame(() => resolve());
|
|
2475
|
+
});
|
|
2200
2476
|
}
|
|
2201
|
-
function
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2477
|
+
function withoutHydrating(ids, loopSessionId) {
|
|
2478
|
+
if (!ids[loopSessionId]) return ids;
|
|
2479
|
+
const next = { ...ids };
|
|
2480
|
+
delete next[loopSessionId];
|
|
2481
|
+
return next;
|
|
2482
|
+
}
|
|
2483
|
+
function safeReduceWorkflow(state, event, eventIndex, options) {
|
|
2484
|
+
try {
|
|
2485
|
+
return { state: reduceWorkflowSession(state, event, eventIndex, options) };
|
|
2486
|
+
} catch (err) {
|
|
2487
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2488
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
2489
|
+
if (import.meta.env?.DEV) {
|
|
2490
|
+
console.error("[langchain_agentx_stream_ui] workflow reducer error:", err);
|
|
2211
2491
|
}
|
|
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
|
-
};
|
|
2492
|
+
return {
|
|
2493
|
+
state: {
|
|
2494
|
+
...state,
|
|
2495
|
+
internalErrors: [
|
|
2496
|
+
...state.internalErrors,
|
|
2497
|
+
{ eventIndex, eventType: event.event_type, message, stack }
|
|
2498
|
+
]
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2371
2501
|
}
|
|
2372
2502
|
}
|
|
2373
2503
|
function finalizeWorkflowSessionOnStreamError(state, message, eventIndex, stack) {
|
|
@@ -2462,6 +2592,13 @@ function createWorkflowSessionStore(initialState = createEmptyWorkflowSessionSta
|
|
|
2462
2592
|
)
|
|
2463
2593
|
});
|
|
2464
2594
|
},
|
|
2595
|
+
markStreamComplete() {
|
|
2596
|
+
set((current) => ({
|
|
2597
|
+
state: finalizeState(
|
|
2598
|
+
finalizeWorkflowSessionOnStreamComplete(current.state)
|
|
2599
|
+
)
|
|
2600
|
+
}));
|
|
2601
|
+
},
|
|
2465
2602
|
/**
|
|
2466
2603
|
* P2:将 completed loop 加入 LRU 热缓存并淘汰溢出。
|
|
2467
2604
|
* running 保护由 collectProtectedLoopSessionIds 按状态自动覆盖,不再依赖永久 pin。
|
|
@@ -2716,6 +2853,76 @@ function WorkflowMarkdownPreview({
|
|
|
2716
2853
|
return /* @__PURE__ */ jsx7("div", { className, "data-testid": testId, children: /* @__PURE__ */ jsx7(RichMarkdown, { content, deferDiagrams: true }) });
|
|
2717
2854
|
}
|
|
2718
2855
|
|
|
2856
|
+
// src/view/workflow/workflowStageListUtils.ts
|
|
2857
|
+
var ACTIVE_CONTAINER_STATUSES = /* @__PURE__ */ new Set([
|
|
2858
|
+
"running",
|
|
2859
|
+
"retrying",
|
|
2860
|
+
"pending",
|
|
2861
|
+
"blocked"
|
|
2862
|
+
]);
|
|
2863
|
+
function isDescendantContainer(containersById, ancestorId, nodeId) {
|
|
2864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2865
|
+
let current = containersById[nodeId]?.parent_container_id ?? null;
|
|
2866
|
+
while (current) {
|
|
2867
|
+
if (current === ancestorId) return true;
|
|
2868
|
+
if (seen.has(current)) return false;
|
|
2869
|
+
seen.add(current);
|
|
2870
|
+
current = containersById[current]?.parent_container_id ?? null;
|
|
2871
|
+
}
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
function subtreeHasActiveWork(containersById, rootContainerId) {
|
|
2875
|
+
for (const node of Object.values(containersById)) {
|
|
2876
|
+
if (node.container_id === rootContainerId) continue;
|
|
2877
|
+
if (!isDescendantContainer(containersById, rootContainerId, node.container_id)) continue;
|
|
2878
|
+
if (ACTIVE_CONTAINER_STATUSES.has(node.status)) return true;
|
|
2879
|
+
}
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
function stageSortKey(node) {
|
|
2883
|
+
return node.display?.progress_current ?? 0;
|
|
2884
|
+
}
|
|
2885
|
+
function partitionWorkflowStages(stages, maxVisibleDoneCount, containersById) {
|
|
2886
|
+
const sorted = [...stages].sort(
|
|
2887
|
+
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2888
|
+
);
|
|
2889
|
+
const always = [];
|
|
2890
|
+
const done = [];
|
|
2891
|
+
for (const stage of sorted) {
|
|
2892
|
+
if (containersById && subtreeHasActiveWork(containersById, stage.container_id)) {
|
|
2893
|
+
always.push(stage);
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
if (stage.status === "running" || stage.status === "retrying" || stage.status === "failed" || stage.status === "pending" || stage.status === "blocked") {
|
|
2897
|
+
always.push(stage);
|
|
2898
|
+
} else if (stage.status === "completed" || stage.status === "skipped") {
|
|
2899
|
+
done.push(stage);
|
|
2900
|
+
} else {
|
|
2901
|
+
always.push(stage);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
const visibleDone = done.slice(Math.max(0, done.length - maxVisibleDoneCount));
|
|
2905
|
+
const visible = [...always, ...visibleDone].sort(
|
|
2906
|
+
(a, b) => stageSortKey(a) - stageSortKey(b) || a.scope_key.localeCompare(b.scope_key)
|
|
2907
|
+
);
|
|
2908
|
+
const hiddenCount = stages.length - visible.length;
|
|
2909
|
+
return {
|
|
2910
|
+
visible,
|
|
2911
|
+
hiddenCount,
|
|
2912
|
+
total: stages.length,
|
|
2913
|
+
visibleDoneCount: visibleDone.length,
|
|
2914
|
+
totalDoneCount: done.length
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
function isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds) {
|
|
2918
|
+
if (activeContainerIds.includes(node.container_id)) return true;
|
|
2919
|
+
if (node.status === "completed" || node.status === "failed" || node.status === "skipped") {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
if (node.loopSessionId && node.loopSessionId === activeLoopSessionId) return true;
|
|
2923
|
+
return false;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2719
2926
|
// src/view/workflow/workflowContainerLineUtils.ts
|
|
2720
2927
|
function formatContainerScopeMetaLabel(scope) {
|
|
2721
2928
|
return scope;
|
|
@@ -2785,6 +2992,27 @@ function buildWorkflowRootMeta(node) {
|
|
|
2785
2992
|
}
|
|
2786
2993
|
return withScopeMeta(node, meta);
|
|
2787
2994
|
}
|
|
2995
|
+
function buildEffectiveWorkflowRootMeta(node, containersById) {
|
|
2996
|
+
const meta = [];
|
|
2997
|
+
if (node.pattern) meta.push(node.pattern);
|
|
2998
|
+
const topStages = Object.values(containersById).filter(
|
|
2999
|
+
(n) => n.scope === "stage" && n.parent_container_id === node.container_id
|
|
3000
|
+
);
|
|
3001
|
+
const total = node.display?.progress_total ?? Math.max(topStages.length, 1);
|
|
3002
|
+
let settled = 0;
|
|
3003
|
+
for (const stage of topStages) {
|
|
3004
|
+
if (subtreeHasActiveWork(containersById, stage.container_id)) continue;
|
|
3005
|
+
if (stage.status === "completed" || stage.status === "failed" || stage.status === "skipped") {
|
|
3006
|
+
settled += 1;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
if (total > 0) meta.push(`${settled}/${total}`);
|
|
3010
|
+
return withScopeMeta(node, meta);
|
|
3011
|
+
}
|
|
3012
|
+
function isWorkflowRootVisuallyActive(node, containersById) {
|
|
3013
|
+
if (node.status === "running" || node.status === "retrying") return true;
|
|
3014
|
+
return subtreeHasActiveWork(containersById, node.container_id);
|
|
3015
|
+
}
|
|
2788
3016
|
function buildStageLineMeta(node) {
|
|
2789
3017
|
const progress = buildStageProgressMeta(node);
|
|
2790
3018
|
return withScopeMeta(node, progress ? [progress] : []);
|
|
@@ -3008,9 +3236,41 @@ var WorkflowAggregateContainer = memo(
|
|
|
3008
3236
|
|
|
3009
3237
|
// src/view/workflow/WorkflowParallelGroup.tsx
|
|
3010
3238
|
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
3239
|
|
|
3013
3240
|
// src/core/workflow/workflowParallelGroupMetrics.ts
|
|
3241
|
+
var ACTIVE_ITEM_STATUSES = /* @__PURE__ */ new Set([
|
|
3242
|
+
"running",
|
|
3243
|
+
"retrying",
|
|
3244
|
+
"pending",
|
|
3245
|
+
"blocked"
|
|
3246
|
+
]);
|
|
3247
|
+
function countMetricsFromContainerStatus(items) {
|
|
3248
|
+
let running = 0;
|
|
3249
|
+
let completed = 0;
|
|
3250
|
+
let failed = 0;
|
|
3251
|
+
for (const item of items) {
|
|
3252
|
+
if (ACTIVE_ITEM_STATUSES.has(item.status)) {
|
|
3253
|
+
running += 1;
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
switch (item.status) {
|
|
3257
|
+
case "completed":
|
|
3258
|
+
completed += 1;
|
|
3259
|
+
break;
|
|
3260
|
+
case "failed":
|
|
3261
|
+
failed += 1;
|
|
3262
|
+
break;
|
|
3263
|
+
default:
|
|
3264
|
+
break;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
return {
|
|
3268
|
+
total: items.length,
|
|
3269
|
+
running,
|
|
3270
|
+
completed,
|
|
3271
|
+
failed
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3014
3274
|
function readDisplayMetrics(items) {
|
|
3015
3275
|
let hasMetrics = false;
|
|
3016
3276
|
let running = 0;
|
|
@@ -3031,34 +3291,29 @@ function readDisplayMetrics(items) {
|
|
|
3031
3291
|
if (total <= 0) total = items.length;
|
|
3032
3292
|
return { total, running, completed, failed };
|
|
3033
3293
|
}
|
|
3294
|
+
function mergeParallelGroupMetrics(fromDisplay, fromStatus, itemCount) {
|
|
3295
|
+
return {
|
|
3296
|
+
total: Math.max(fromDisplay.total, fromStatus.total, itemCount),
|
|
3297
|
+
running: Math.max(fromDisplay.running, fromStatus.running),
|
|
3298
|
+
completed: Math.max(fromDisplay.completed, fromStatus.completed),
|
|
3299
|
+
failed: Math.max(fromDisplay.failed, fromStatus.failed)
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3034
3302
|
function computeParallelGroupMetrics(items) {
|
|
3303
|
+
const fromStatus = countMetricsFromContainerStatus(items);
|
|
3035
3304
|
const fromDisplay = readDisplayMetrics(items);
|
|
3036
|
-
if (fromDisplay) return
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3305
|
+
if (!fromDisplay) return fromStatus;
|
|
3306
|
+
return mergeParallelGroupMetrics(fromDisplay, fromStatus, items.length);
|
|
3307
|
+
}
|
|
3308
|
+
function parallelGroupHasActiveWork(items, loopTreesBySessionId = {}) {
|
|
3309
|
+
const metrics = computeParallelGroupMetrics(items);
|
|
3310
|
+
if (metrics.running > 0) return true;
|
|
3040
3311
|
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
|
-
}
|
|
3312
|
+
const loopSessionId = item.loopSessionId;
|
|
3313
|
+
if (!loopSessionId) continue;
|
|
3314
|
+
if (isWorkflowLoopTreeActive(loopTreesBySessionId[loopSessionId])) return true;
|
|
3055
3315
|
}
|
|
3056
|
-
return
|
|
3057
|
-
total: items.length,
|
|
3058
|
-
running,
|
|
3059
|
-
completed,
|
|
3060
|
-
failed
|
|
3061
|
-
};
|
|
3316
|
+
return false;
|
|
3062
3317
|
}
|
|
3063
3318
|
function formatParallelGroupSummary(metrics) {
|
|
3064
3319
|
if (metrics.running > 0) {
|
|
@@ -3115,47 +3370,6 @@ function WorkflowStageDoneBody({
|
|
|
3115
3370
|
);
|
|
3116
3371
|
}
|
|
3117
3372
|
|
|
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
3373
|
// src/view/workflow/WorkflowStageContainer.tsx
|
|
3160
3374
|
import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3161
3375
|
function containerTestId(node) {
|
|
@@ -3174,10 +3388,10 @@ function WorkflowStageContainerInner({
|
|
|
3174
3388
|
virtualized,
|
|
3175
3389
|
virtualizeThreshold,
|
|
3176
3390
|
groupParallelTools,
|
|
3177
|
-
suppressHostScopedLoop = false
|
|
3391
|
+
suppressHostScopedLoop = false,
|
|
3392
|
+
subtreeHasActiveWork: subtreeHasActiveWork2 = false
|
|
3178
3393
|
}) {
|
|
3179
3394
|
const { verbose } = useSessionViewOptions();
|
|
3180
|
-
const scale = useWorkflowScaleOptions();
|
|
3181
3395
|
const store = useWorkflowSessionStoreApi();
|
|
3182
3396
|
const replayHandler = useWorkflowLoopReplayHandler();
|
|
3183
3397
|
const activeLoopSessionId = useStore2(store, (s) => s.state.activeLoopSessionId);
|
|
@@ -3187,31 +3401,30 @@ function WorkflowStageContainerInner({
|
|
|
3187
3401
|
(s) => s.state.stageDoneSummariesByContainerId[node.container_id]
|
|
3188
3402
|
);
|
|
3189
3403
|
const pinLoopSession = useStore2(store, (s) => s.pinLoopSession);
|
|
3190
|
-
const uiStatus = mapContainerUiStatus(node.status);
|
|
3191
3404
|
const isRunning = node.status === "running" || node.status === "retrying";
|
|
3192
3405
|
const isDone = node.status === "completed" || node.status === "failed";
|
|
3193
3406
|
const isSkipped = node.status === "skipped";
|
|
3407
|
+
const loopStillActive = isWorkflowLoopTreeActive(loopTree);
|
|
3408
|
+
const effectivelyRunning = isRunning || loopStillActive || subtreeHasActiveWork2 && isDone;
|
|
3409
|
+
const effectivelyDone = isDone && !subtreeHasActiveWork2 && !loopStillActive;
|
|
3410
|
+
const uiStatus = mapContainerUiStatus(effectivelyRunning ? "running" : node.status);
|
|
3194
3411
|
const isActive = isActiveWorkflowContainer(node, activeLoopSessionId, activeContainerIds);
|
|
3195
3412
|
const doneSummaryFromTree = useMemo3(
|
|
3196
3413
|
() => loopTree && isDone ? buildStageDoneSummary(loopTree) : null,
|
|
3197
3414
|
[loopTree, isDone]
|
|
3198
3415
|
);
|
|
3199
3416
|
const doneSummary = doneSummaryFromTree ?? (isDone ? cachedSummary : null);
|
|
3200
|
-
const shouldAutoExpandRunning =
|
|
3417
|
+
const shouldAutoExpandRunning = effectivelyRunning && !isSkipped;
|
|
3201
3418
|
const [expanded, setExpanded] = useState2(shouldAutoExpandRunning);
|
|
3202
3419
|
useEffect4(() => {
|
|
3203
3420
|
if (shouldAutoExpandRunning) {
|
|
3204
3421
|
setExpanded(true);
|
|
3205
3422
|
return;
|
|
3206
3423
|
}
|
|
3207
|
-
if (
|
|
3424
|
+
if (effectivelyDone || isSkipped) {
|
|
3208
3425
|
setExpanded(false);
|
|
3209
|
-
return;
|
|
3210
3426
|
}
|
|
3211
|
-
|
|
3212
|
-
setExpanded(false);
|
|
3213
|
-
}
|
|
3214
|
-
}, [shouldAutoExpandRunning, isRunning, isDone, isSkipped, scale.onlyExpandActiveRunning]);
|
|
3427
|
+
}, [shouldAutoExpandRunning, effectivelyDone, isSkipped]);
|
|
3215
3428
|
const requestLoopHydration = useCallback2(() => {
|
|
3216
3429
|
const loopSessionId = node.loopSessionId;
|
|
3217
3430
|
if (!loopSessionId || loopTree) return;
|
|
@@ -3236,17 +3449,18 @@ function WorkflowStageContainerInner({
|
|
|
3236
3449
|
window.addEventListener("keydown", onKeyDown);
|
|
3237
3450
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
3238
3451
|
}, [requestLoopHydration]);
|
|
3452
|
+
const testId = containerTestId(node);
|
|
3453
|
+
const isItem = node.scope === "item";
|
|
3239
3454
|
const displayTitle = node.title;
|
|
3240
3455
|
const usesScopedLoop = Boolean(node.loopSessionId) && !suppressHostScopedLoop;
|
|
3241
|
-
const runningLoopTree = loopTree ?? (usesScopedLoop &&
|
|
3456
|
+
const runningLoopTree = loopTree ?? (usesScopedLoop && effectivelyRunning ? createWorkflowRunningLoopTree() : void 0);
|
|
3457
|
+
const itemLoopVirtualized = isItem ? false : virtualized;
|
|
3242
3458
|
const staticContentTeaser = !usesScopedLoop && !loopTree && node.content_blocks.length > 0 ? node.content_blocks[node.content_blocks.length - 1].preview : "";
|
|
3243
3459
|
const teaserText = doneSummary?.teaser || staticContentTeaser;
|
|
3244
3460
|
const expandHint = doneSummary ? formatTeaserExpandHint(doneSummary.extraLines, verbose) : null;
|
|
3245
3461
|
const doneLabel = doneSummary?.label ?? "done";
|
|
3246
|
-
const statusLabel =
|
|
3247
|
-
const
|
|
3248
|
-
const isItem = node.scope === "item";
|
|
3249
|
-
const canToggleShell = isRunning && !isSkipped || isDone;
|
|
3462
|
+
const statusLabel = effectivelyDone ? doneLabel : suppressHostScopedLoop && effectivelyRunning ? "running" : mapContainerStatusLabel(effectivelyRunning ? "running" : node.status);
|
|
3463
|
+
const canToggleShell = effectivelyRunning && !isSkipped || effectivelyDone;
|
|
3250
3464
|
const shellToggle = canToggleShell ? toggleExpanded : void 0;
|
|
3251
3465
|
return /* @__PURE__ */ jsxs5(
|
|
3252
3466
|
"div",
|
|
@@ -3272,13 +3486,13 @@ function WorkflowStageContainerInner({
|
|
|
3272
3486
|
onToggle: shellToggle,
|
|
3273
3487
|
interactive: canToggleShell,
|
|
3274
3488
|
expanded,
|
|
3275
|
-
headerTestId:
|
|
3489
|
+
headerTestId: effectivelyDone ? `${testId}-done` : void 0,
|
|
3276
3490
|
titleTestId: `${testId}-title`,
|
|
3277
3491
|
descriptionTestId: `${testId}-desc`,
|
|
3278
3492
|
className: isItem ? "lax-workflow-parallel-item__header" : void 0
|
|
3279
3493
|
}
|
|
3280
3494
|
),
|
|
3281
|
-
|
|
3495
|
+
effectivelyDone && !expanded && (teaserText || expandHint) ? /* @__PURE__ */ jsxs5("div", { className: "lax-workflow-stage-container__done-teaser", children: [
|
|
3282
3496
|
teaserText ? /* @__PURE__ */ jsx10(
|
|
3283
3497
|
"span",
|
|
3284
3498
|
{
|
|
@@ -3304,7 +3518,7 @@ function WorkflowStageContainerInner({
|
|
|
3304
3518
|
))
|
|
3305
3519
|
}
|
|
3306
3520
|
) : null,
|
|
3307
|
-
expanded &&
|
|
3521
|
+
expanded && effectivelyRunning && runningLoopTree ? /* @__PURE__ */ jsx10(
|
|
3308
3522
|
"div",
|
|
3309
3523
|
{
|
|
3310
3524
|
className: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body",
|
|
@@ -3314,20 +3528,20 @@ function WorkflowStageContainerInner({
|
|
|
3314
3528
|
{
|
|
3315
3529
|
tree: runningLoopTree,
|
|
3316
3530
|
showTaskListFooter: false,
|
|
3317
|
-
virtualized,
|
|
3531
|
+
virtualized: itemLoopVirtualized,
|
|
3318
3532
|
virtualizeThreshold,
|
|
3319
3533
|
groupParallelTools
|
|
3320
3534
|
}
|
|
3321
3535
|
)
|
|
3322
3536
|
}
|
|
3323
3537
|
) : null,
|
|
3324
|
-
|
|
3538
|
+
effectivelyDone && expanded ? loopTree ? /* @__PURE__ */ jsx10(
|
|
3325
3539
|
WorkflowStageDoneBody,
|
|
3326
3540
|
{
|
|
3327
3541
|
node,
|
|
3328
3542
|
loopTree,
|
|
3329
3543
|
testId: bodyTestId(node),
|
|
3330
|
-
virtualized,
|
|
3544
|
+
virtualized: itemLoopVirtualized,
|
|
3331
3545
|
virtualizeThreshold,
|
|
3332
3546
|
groupParallelTools,
|
|
3333
3547
|
bodyClassName: isItem ? "lax-workflow-parallel-item__body lax-workflow-stage-container__body" : "lax-workflow-stage-container__body"
|
|
@@ -3349,6 +3563,7 @@ function WorkflowStageContainerInner({
|
|
|
3349
3563
|
}
|
|
3350
3564
|
function workflowStageContainerPropsEqual(prev, next) {
|
|
3351
3565
|
if (prev.suppressHostScopedLoop !== next.suppressHostScopedLoop) return false;
|
|
3566
|
+
if (prev.subtreeHasActiveWork !== next.subtreeHasActiveWork) return false;
|
|
3352
3567
|
if (prev.loopTree !== next.loopTree) return false;
|
|
3353
3568
|
if (!workflowContainerViewPropsEqual(prev, next)) return false;
|
|
3354
3569
|
return workflowContainerNodeVisualEqual(prev.node, next.node);
|
|
@@ -3370,34 +3585,23 @@ function WorkflowParallelGroupInner({
|
|
|
3370
3585
|
virtualizeThreshold,
|
|
3371
3586
|
groupParallelTools
|
|
3372
3587
|
}) {
|
|
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
3588
|
const metrics = useMemo4(() => computeParallelGroupMetrics(items), [items]);
|
|
3378
3589
|
const summary = formatParallelGroupSummary(metrics);
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
() => items.some((item) => isActiveWorkflowContainer(item, activeLoopSessionId, activeContainerIds)),
|
|
3383
|
-
[items, activeLoopSessionId, activeContainerIds]
|
|
3590
|
+
const hasActiveWork = useMemo4(
|
|
3591
|
+
() => parallelGroupHasActiveWork(items, loopTreesBySessionId),
|
|
3592
|
+
[items, loopTreesBySessionId]
|
|
3384
3593
|
);
|
|
3385
3594
|
const allSettled = metrics.total > 0 && metrics.running === 0 && metrics.completed + metrics.failed >= metrics.total;
|
|
3386
|
-
const
|
|
3387
|
-
const [expanded, setExpanded] = useState3(shouldAutoExpandGroup);
|
|
3595
|
+
const [expanded, setExpanded] = useState3(hasActiveWork);
|
|
3388
3596
|
useEffect5(() => {
|
|
3389
|
-
if (
|
|
3597
|
+
if (hasActiveWork) {
|
|
3390
3598
|
setExpanded(true);
|
|
3391
3599
|
return;
|
|
3392
3600
|
}
|
|
3393
|
-
if (hasRunning && scale.onlyExpandActiveRunning) {
|
|
3394
|
-
setExpanded(false);
|
|
3395
|
-
return;
|
|
3396
|
-
}
|
|
3397
3601
|
if (allSettled) {
|
|
3398
3602
|
setExpanded(false);
|
|
3399
3603
|
}
|
|
3400
|
-
}, [
|
|
3604
|
+
}, [hasActiveWork, allSettled]);
|
|
3401
3605
|
const toggleExpanded = useCallback3(() => {
|
|
3402
3606
|
setExpanded((prev) => !prev);
|
|
3403
3607
|
}, []);
|
|
@@ -3434,7 +3638,7 @@ function WorkflowParallelGroupInner({
|
|
|
3434
3638
|
WorkflowExpandTrigger,
|
|
3435
3639
|
{
|
|
3436
3640
|
summary,
|
|
3437
|
-
expandHint: !expanded &&
|
|
3641
|
+
expandHint: !expanded && hasActiveWork ? "(click to expand agents)" : !expanded && !hasActiveWork ? "(ctrl+o to expand)" : null,
|
|
3438
3642
|
onToggle: toggleExpanded,
|
|
3439
3643
|
expanded,
|
|
3440
3644
|
testId: `lax-workflow-parallel-group-summary-${groupKey}`,
|
|
@@ -3469,11 +3673,14 @@ var WorkflowParallelGroup = memo3(WorkflowParallelGroupInner, workflowParallelGr
|
|
|
3469
3673
|
import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3470
3674
|
function WorkflowRootContainer({
|
|
3471
3675
|
node,
|
|
3472
|
-
children
|
|
3676
|
+
children,
|
|
3677
|
+
containersById
|
|
3473
3678
|
}) {
|
|
3474
3679
|
const { verbose } = useSessionViewOptions();
|
|
3475
|
-
const
|
|
3476
|
-
const
|
|
3680
|
+
const visuallyActive = containersById ? isWorkflowRootVisuallyActive(node, containersById) : node.status === "running" || node.status === "retrying";
|
|
3681
|
+
const uiStatus = mapContainerUiStatus(visuallyActive ? "running" : node.status);
|
|
3682
|
+
const statusLabel = mapWorkflowRootStatusLabel(visuallyActive ? "running" : node.status);
|
|
3683
|
+
const meta = containersById ? buildEffectiveWorkflowRootMeta(node, containersById) : buildWorkflowRootMeta(node);
|
|
3477
3684
|
return /* @__PURE__ */ jsxs7("div", { className: "lax-workflow-root", "data-testid": "lax-workflow-root", children: [
|
|
3478
3685
|
/* @__PURE__ */ jsx12(
|
|
3479
3686
|
WorkflowContainerLine,
|
|
@@ -3482,7 +3689,7 @@ function WorkflowRootContainer({
|
|
|
3482
3689
|
title: node.title,
|
|
3483
3690
|
description: node.subtitle || void 0,
|
|
3484
3691
|
uiStatus,
|
|
3485
|
-
meta
|
|
3692
|
+
meta,
|
|
3486
3693
|
statusLabel,
|
|
3487
3694
|
titleTestId: "lax-workflow-root-title",
|
|
3488
3695
|
descriptionTestId: "lax-workflow-root-desc",
|
|
@@ -3610,31 +3817,36 @@ function buildDoneSummary(node) {
|
|
|
3610
3817
|
function WorkflowSubworkflowContainer({
|
|
3611
3818
|
node,
|
|
3612
3819
|
depth = 1,
|
|
3613
|
-
children
|
|
3820
|
+
children,
|
|
3821
|
+
subtreeHasActiveWork: subtreeHasActiveWork2 = false
|
|
3614
3822
|
}) {
|
|
3615
3823
|
const { verbose } = useSessionViewOptions();
|
|
3616
|
-
const uiStatus = mapContainerUiStatus(node.status);
|
|
3617
3824
|
const isRunning = node.status === "running" || node.status === "retrying";
|
|
3618
3825
|
const isDone = node.status === "completed" || node.status === "failed";
|
|
3826
|
+
const effectivelyRunning = isRunning || subtreeHasActiveWork2;
|
|
3827
|
+
const effectivelyDone = isDone && !subtreeHasActiveWork2;
|
|
3828
|
+
const uiStatus = mapContainerUiStatus(
|
|
3829
|
+
effectivelyRunning ? "running" : node.status
|
|
3830
|
+
);
|
|
3619
3831
|
const testId = `lax-workflow-subworkflow-${node.scope_key}`;
|
|
3620
3832
|
const teaser = useMemo6(() => {
|
|
3621
3833
|
if (node.content_blocks.length === 0) return "";
|
|
3622
3834
|
return node.content_blocks[node.content_blocks.length - 1]?.preview ?? "";
|
|
3623
3835
|
}, [node.content_blocks]);
|
|
3624
|
-
const [expanded, setExpanded] = useState5(
|
|
3836
|
+
const [expanded, setExpanded] = useState5(effectivelyRunning);
|
|
3625
3837
|
useEffect7(() => {
|
|
3626
|
-
if (
|
|
3627
|
-
if (
|
|
3628
|
-
}, [
|
|
3838
|
+
if (effectivelyRunning) setExpanded(true);
|
|
3839
|
+
else if (effectivelyDone) setExpanded(false);
|
|
3840
|
+
}, [effectivelyRunning, effectivelyDone]);
|
|
3629
3841
|
const toggleExpanded = useCallback5(() => {
|
|
3630
3842
|
setExpanded((prev) => !prev);
|
|
3631
3843
|
}, []);
|
|
3632
|
-
const shellToggle =
|
|
3844
|
+
const shellToggle = effectivelyRunning ? toggleExpanded : effectivelyDone && expanded ? toggleExpanded : void 0;
|
|
3633
3845
|
const depthStyle = {
|
|
3634
3846
|
"--lax-workflow-depth": depth,
|
|
3635
3847
|
"--lax-workflow-indent-unit": workflowIndentUnit(depth)
|
|
3636
3848
|
};
|
|
3637
|
-
const showShellLine = !(
|
|
3849
|
+
const showShellLine = !(effectivelyDone && !expanded);
|
|
3638
3850
|
return /* @__PURE__ */ jsxs9(
|
|
3639
3851
|
"div",
|
|
3640
3852
|
{
|
|
@@ -3651,9 +3863,11 @@ function WorkflowSubworkflowContainer({
|
|
|
3651
3863
|
description: node.subtitle || void 0,
|
|
3652
3864
|
uiStatus,
|
|
3653
3865
|
meta: buildWorkflowRootMeta(node),
|
|
3654
|
-
statusLabel: mapWorkflowRootStatusLabel(
|
|
3866
|
+
statusLabel: mapWorkflowRootStatusLabel(
|
|
3867
|
+
effectivelyRunning ? "running" : node.status
|
|
3868
|
+
),
|
|
3655
3869
|
onToggle: shellToggle,
|
|
3656
|
-
interactive:
|
|
3870
|
+
interactive: effectivelyRunning,
|
|
3657
3871
|
expanded,
|
|
3658
3872
|
titleTestId: `${testId}-title`,
|
|
3659
3873
|
descriptionTestId: `${testId}-desc`,
|
|
@@ -3673,13 +3887,13 @@ function WorkflowSubworkflowContainer({
|
|
|
3673
3887
|
!expanded && children ? /* @__PURE__ */ jsx14(
|
|
3674
3888
|
WorkflowExpandTrigger,
|
|
3675
3889
|
{
|
|
3676
|
-
summary: `${node.title} \xB7 ${
|
|
3890
|
+
summary: `${node.title} \xB7 ${effectivelyDone ? buildDoneSummary(node) : "running"}`,
|
|
3677
3891
|
summaryTestId: `${testId}-title`,
|
|
3678
3892
|
teaser: teaser || void 0,
|
|
3679
|
-
expandHint:
|
|
3893
|
+
expandHint: effectivelyRunning ? "(click to expand subworkflow)" : void 0,
|
|
3680
3894
|
onToggle: () => setExpanded(true),
|
|
3681
3895
|
expanded: false,
|
|
3682
|
-
testId:
|
|
3896
|
+
testId: effectivelyDone ? `${testId}-done` : `${testId}-running-collapsed`,
|
|
3683
3897
|
teaserTestId: teaser ? `${testId}-teaser` : void 0
|
|
3684
3898
|
}
|
|
3685
3899
|
) : null
|
|
@@ -3705,6 +3919,7 @@ function collectSiblingItems(node, containersById) {
|
|
|
3705
3919
|
}
|
|
3706
3920
|
function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, viewProps, options) {
|
|
3707
3921
|
const loopTree = resolveLoopTree2(node, loopTreesBySessionId);
|
|
3922
|
+
const subtreeActive = options?.subtreeHasActiveWork ?? subtreeHasActiveWork(containerTree.containersById, node.container_id);
|
|
3708
3923
|
switch (node.scope) {
|
|
3709
3924
|
case "aggregate":
|
|
3710
3925
|
return /* @__PURE__ */ jsx15(
|
|
@@ -3737,6 +3952,7 @@ function renderLeafContainer(node, depth, containerTree, loopTreesBySessionId, v
|
|
|
3737
3952
|
loopTree,
|
|
3738
3953
|
depth,
|
|
3739
3954
|
suppressHostScopedLoop: options?.suppressHostScopedLoop,
|
|
3955
|
+
subtreeHasActiveWork: subtreeActive,
|
|
3740
3956
|
...viewProps
|
|
3741
3957
|
},
|
|
3742
3958
|
node.container_id
|
|
@@ -3790,6 +4006,10 @@ function renderContainerNode(node, containerTree, loopTreesBySessionId, viewProp
|
|
|
3790
4006
|
{
|
|
3791
4007
|
node,
|
|
3792
4008
|
depth,
|
|
4009
|
+
subtreeHasActiveWork: subtreeHasActiveWork(
|
|
4010
|
+
containerTree.containersById,
|
|
4011
|
+
node.container_id
|
|
4012
|
+
),
|
|
3793
4013
|
children: hasNested ? nested : null
|
|
3794
4014
|
},
|
|
3795
4015
|
node.container_id
|
|
@@ -3851,8 +4071,8 @@ function WorkflowStageListPanel({
|
|
|
3851
4071
|
const [extraCompletedPages, setExtraCompletedPages] = useState6(0);
|
|
3852
4072
|
const maxVisibleDoneCount = scale.completedStagePageSize + extraCompletedPages * scale.completedStagePageSize;
|
|
3853
4073
|
const partition = useMemo7(
|
|
3854
|
-
() => partitionWorkflowStages(stages, maxVisibleDoneCount),
|
|
3855
|
-
[stages, maxVisibleDoneCount]
|
|
4074
|
+
() => partitionWorkflowStages(stages, maxVisibleDoneCount, containerTree.containersById),
|
|
4075
|
+
[stages, maxVisibleDoneCount, containerTree.containersById]
|
|
3856
4076
|
);
|
|
3857
4077
|
if (partition.visible.length === 0) return null;
|
|
3858
4078
|
const showMoreCount = Math.min(
|
|
@@ -3950,17 +4170,25 @@ function WorkflowChrome({
|
|
|
3950
4170
|
}
|
|
3951
4171
|
) });
|
|
3952
4172
|
}
|
|
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
|
-
|
|
4173
|
+
return /* @__PURE__ */ jsx15("div", { className: "lax-workflow-chrome", "data-testid": "lax-workflow-chrome", children: rootNodes.map((workflowNode) => /* @__PURE__ */ jsx15(
|
|
4174
|
+
WorkflowRootContainer,
|
|
4175
|
+
{
|
|
4176
|
+
node: workflowNode,
|
|
4177
|
+
containersById: containerTree.containersById,
|
|
4178
|
+
children: renderWorkflowChildren(
|
|
4179
|
+
workflowNode,
|
|
4180
|
+
containerTree,
|
|
4181
|
+
loopTreesBySessionId,
|
|
4182
|
+
viewProps
|
|
4183
|
+
)
|
|
4184
|
+
},
|
|
4185
|
+
workflowNode.container_id
|
|
4186
|
+
)) });
|
|
3959
4187
|
}
|
|
3960
4188
|
|
|
3961
4189
|
// src/view/workflow/WorkflowTaskListFooter.tsx
|
|
3962
4190
|
import { useMemo as useMemo8, useState as useState7 } from "react";
|
|
3963
|
-
import { useStore as
|
|
4191
|
+
import { useStore as useStore3 } from "zustand";
|
|
3964
4192
|
|
|
3965
4193
|
// src/view/workflow/workflowTaskListMerge.ts
|
|
3966
4194
|
var STATUS_ORDER = {
|
|
@@ -4040,7 +4268,7 @@ function MessageResponse({ children }) {
|
|
|
4040
4268
|
}
|
|
4041
4269
|
function WorkflowTaskListFooter() {
|
|
4042
4270
|
const store = useWorkflowSessionStoreApi();
|
|
4043
|
-
const state =
|
|
4271
|
+
const state = useStore3(store, (s) => s.state);
|
|
4044
4272
|
const [expanded, setExpanded] = useState7(false);
|
|
4045
4273
|
const footerState = useMemo8(() => deriveWorkflowTaskFooterState(state), [state]);
|
|
4046
4274
|
const visible = shouldShowWorkflowTaskListFooter(state);
|
|
@@ -4080,7 +4308,7 @@ function WorkflowTaskListFooter() {
|
|
|
4080
4308
|
}
|
|
4081
4309
|
|
|
4082
4310
|
// src/view/workflow/WorkflowSession.tsx
|
|
4083
|
-
import { useStore as
|
|
4311
|
+
import { useStore as useStore4 } from "zustand";
|
|
4084
4312
|
import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4085
4313
|
function WorkflowSession({
|
|
4086
4314
|
source,
|
|
@@ -4103,6 +4331,7 @@ function WorkflowSession({
|
|
|
4103
4331
|
groupParallelTools = false,
|
|
4104
4332
|
workflowScale,
|
|
4105
4333
|
onRequestLoopReplay,
|
|
4334
|
+
onStreamComplete,
|
|
4106
4335
|
children
|
|
4107
4336
|
}) {
|
|
4108
4337
|
const storeRef = useRef3(null);
|
|
@@ -4152,39 +4381,46 @@ function WorkflowSession({
|
|
|
4152
4381
|
workspaceRoot
|
|
4153
4382
|
]
|
|
4154
4383
|
);
|
|
4155
|
-
const containerTree =
|
|
4384
|
+
const containerTree = useStore4(
|
|
4156
4385
|
storeRef.current,
|
|
4157
4386
|
(s) => s.state.containerTree
|
|
4158
4387
|
);
|
|
4159
|
-
const loopTreesBySessionId =
|
|
4388
|
+
const loopTreesBySessionId = useStore4(
|
|
4160
4389
|
storeRef.current,
|
|
4161
4390
|
(s) => s.state.loopTreesBySessionId
|
|
4162
4391
|
);
|
|
4163
|
-
const sessionStatus =
|
|
4392
|
+
const sessionStatus = useStore4(
|
|
4164
4393
|
storeRef.current,
|
|
4165
4394
|
(s) => s.state.status
|
|
4166
4395
|
);
|
|
4167
|
-
const internalErrors =
|
|
4396
|
+
const internalErrors = useStore4(
|
|
4168
4397
|
storeRef.current,
|
|
4169
4398
|
(s) => s.state.internalErrors
|
|
4170
4399
|
);
|
|
4400
|
+
const onErrorRef = useRef3(onError);
|
|
4401
|
+
const onStreamCompleteRef = useRef3(onStreamComplete);
|
|
4402
|
+
onErrorRef.current = onError;
|
|
4403
|
+
onStreamCompleteRef.current = onStreamComplete;
|
|
4171
4404
|
useEffect8(() => {
|
|
4172
4405
|
const store = storeRef.current;
|
|
4173
4406
|
const controller = new AbortController();
|
|
4174
4407
|
void source.start((event, ctx) => {
|
|
4175
4408
|
store.getState().applyEvent(event, ctx);
|
|
4176
|
-
}, controller.signal).
|
|
4409
|
+
}, controller.signal).then(() => {
|
|
4410
|
+
store.getState().markStreamComplete();
|
|
4411
|
+
onStreamCompleteRef.current?.();
|
|
4412
|
+
}).catch((err) => {
|
|
4177
4413
|
if (err instanceof SseTransportTerminalError) {
|
|
4178
4414
|
store.getState().markAsError(err);
|
|
4179
|
-
|
|
4415
|
+
onErrorRef.current?.(err);
|
|
4180
4416
|
} else if (err instanceof Error) {
|
|
4181
|
-
|
|
4417
|
+
onErrorRef.current?.(err);
|
|
4182
4418
|
}
|
|
4183
4419
|
});
|
|
4184
4420
|
return () => {
|
|
4185
4421
|
controller.abort();
|
|
4186
4422
|
};
|
|
4187
|
-
}, [source
|
|
4423
|
+
}, [source]);
|
|
4188
4424
|
return /* @__PURE__ */ jsx17(WorkflowSessionStoreContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx17(WorkflowScaleContext.Provider, { value: scaleOptions, children: /* @__PURE__ */ jsx17(WorkflowLoopReplayProvider, { onRequestLoopReplay, children: /* @__PURE__ */ jsx17(InteractionBusContext.Provider, { value: busRef, children: /* @__PURE__ */ jsx17(NodeRegistryContext.Provider, { value: registryRef, children: /* @__PURE__ */ jsx17(MarkdownRendererContext.Provider, { value: markdownRenderer, children: /* @__PURE__ */ jsx17(ToolDisplayOptionsContext.Provider, { value: toolDisplayRef, children: /* @__PURE__ */ jsx17(SessionViewOptionsContext.Provider, { value: sessionViewRef, children: /* @__PURE__ */ jsxs12(
|
|
4189
4425
|
"div",
|
|
4190
4426
|
{
|
|
@@ -4407,6 +4643,7 @@ export {
|
|
|
4407
4643
|
createReplaySource,
|
|
4408
4644
|
createSessionStore,
|
|
4409
4645
|
createWorkflowSessionStore,
|
|
4646
|
+
deriveLoopSessionIdFromAgentEvent,
|
|
4410
4647
|
displayPath,
|
|
4411
4648
|
entryKey,
|
|
4412
4649
|
filterAgentLoopReplayEvents,
|