agentweaver 0.1.15 → 0.1.17
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/README.md +76 -19
- package/dist/artifact-manifest.js +219 -0
- package/dist/artifacts.js +88 -3
- package/dist/doctor/checks/env-diagnostics.js +25 -0
- package/dist/doctor/checks/executors.js +2 -2
- package/dist/doctor/checks/flow-readiness.js +15 -18
- package/dist/flow-state.js +212 -15
- package/dist/index.js +539 -209
- package/dist/interactive/blessed-session.js +361 -0
- package/dist/interactive/controller.js +1326 -0
- package/dist/interactive/create-interactive-session.js +5 -0
- package/dist/interactive/ink/index.js +597 -0
- package/dist/interactive/progress.js +245 -0
- package/dist/interactive/selectors.js +14 -0
- package/dist/interactive/session.js +1 -0
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/tree.js +155 -0
- package/dist/interactive/types.js +1 -0
- package/dist/interactive/view-model.js +1 -0
- package/dist/interactive-ui.js +159 -194
- package/dist/pipeline/auto-flow.js +9 -6
- package/dist/pipeline/context.js +7 -5
- package/dist/pipeline/declarative-flow-runner.js +212 -6
- package/dist/pipeline/declarative-flows.js +63 -17
- package/dist/pipeline/execution-routing-config.js +15 -0
- package/dist/pipeline/flow-catalog.js +50 -12
- package/dist/pipeline/flow-run-resume.js +29 -0
- package/dist/pipeline/flow-specs/auto-common.json +90 -360
- package/dist/pipeline/flow-specs/auto-golang.json +81 -360
- package/dist/pipeline/flow-specs/auto-simple.json +141 -0
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
- package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +316 -0
- package/dist/pipeline/flow-specs/design-review.json +10 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
- package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
- package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
- package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +13 -6
- package/dist/pipeline/flow-specs/instant-task.json +177 -0
- package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
- package/dist/pipeline/flow-specs/plan-revise.json +7 -1
- package/dist/pipeline/flow-specs/plan.json +51 -71
- package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
- package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
- package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
- package/dist/pipeline/flow-specs/review/review-project.json +12 -0
- package/dist/pipeline/flow-specs/review/review.json +37 -31
- package/dist/pipeline/flow-specs/task-describe.json +2 -0
- package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
- package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
- package/dist/pipeline/launch-profile-config.js +30 -18
- package/dist/pipeline/node-contract.js +1 -0
- package/dist/pipeline/node-registry.js +115 -6
- package/dist/pipeline/node-runner.js +3 -2
- package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
- package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
- package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
- package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
- package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
- package/dist/pipeline/nodes/flow-run-node.js +242 -8
- package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
- package/dist/pipeline/nodes/llm-prompt-node.js +38 -36
- package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
- package/dist/pipeline/nodes/review-verdict-node.js +86 -0
- package/dist/pipeline/nodes/select-files-form-node.js +8 -0
- package/dist/pipeline/nodes/structured-summary-node.js +24 -0
- package/dist/pipeline/nodes/user-input-node.js +38 -3
- package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
- package/dist/pipeline/plugin-loader.js +389 -0
- package/dist/pipeline/plugin-types.js +1 -0
- package/dist/pipeline/prompt-registry.js +3 -1
- package/dist/pipeline/prompt-runtime.js +4 -1
- package/dist/pipeline/registry.js +71 -4
- package/dist/pipeline/review-iteration.js +26 -0
- package/dist/pipeline/spec-compiler.js +3 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-types.js +3 -0
- package/dist/pipeline/spec-validator.js +20 -0
- package/dist/pipeline/value-resolver.js +76 -2
- package/dist/plugin-sdk.js +1 -0
- package/dist/prompts.js +36 -14
- package/dist/review-severity.js +45 -0
- package/dist/runtime/artifact-registry.js +405 -0
- package/dist/runtime/design-review-input-contract.js +17 -16
- package/dist/runtime/env-loader.js +3 -0
- package/dist/runtime/execution-routing-store.js +134 -0
- package/dist/runtime/execution-routing.js +233 -0
- package/dist/runtime/interactive-execution-routing.js +471 -0
- package/dist/runtime/plan-revise-input-contract.js +35 -32
- package/dist/runtime/planning-bundle.js +123 -0
- package/dist/runtime/ready-to-merge.js +22 -1
- package/dist/runtime/review-input-contract.js +100 -0
- package/dist/structured-artifact-schema-registry.js +9 -0
- package/dist/structured-artifact-schemas.json +140 -1
- package/dist/structured-artifacts.js +77 -6
- package/dist/user-input.js +70 -3
- package/docs/example/.flows/examples/claude-example.json +50 -0
- package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/examples/.flows/claude-example.json +50 -0
- package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/plugin-sdk.md +731 -0
- package/package.json +11 -4
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
export function displayPhaseId(phase) {
|
|
2
|
+
let result = phase.id;
|
|
3
|
+
const values = Object.entries(phase.repeatVars)
|
|
4
|
+
.filter(([key]) => !key.endsWith("_minus_one"))
|
|
5
|
+
.map(([, value]) => value);
|
|
6
|
+
for (const value of values) {
|
|
7
|
+
const suffix = `_${String(value)}`;
|
|
8
|
+
if (result.endsWith(suffix)) {
|
|
9
|
+
result = result.slice(0, -suffix.length);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
function repeatGroupKey(repeatVars) {
|
|
15
|
+
const entries = Object.entries(repeatVars).sort(([left], [right]) => left.localeCompare(right));
|
|
16
|
+
return JSON.stringify(entries);
|
|
17
|
+
}
|
|
18
|
+
function repeatSeriesKey(phases) {
|
|
19
|
+
const repeatVarNames = Object.keys(phases[0]?.repeatVars ?? {}).sort();
|
|
20
|
+
const phaseNames = phases.map((phase) => displayPhaseId(phase));
|
|
21
|
+
return JSON.stringify({
|
|
22
|
+
repeatVarNames,
|
|
23
|
+
phaseNames,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function repeatLabel(repeatVars) {
|
|
27
|
+
const entries = Object.entries(repeatVars).filter(([key]) => !key.endsWith("_minus_one"));
|
|
28
|
+
if (entries.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (entries.length === 1) {
|
|
32
|
+
const [key, value] = entries[0] ?? ["repeat", ""];
|
|
33
|
+
return `${key} ${value}`;
|
|
34
|
+
}
|
|
35
|
+
return entries.map(([key, value]) => `${key}=${value}`).join(", ");
|
|
36
|
+
}
|
|
37
|
+
export function isAfterTermination(flowState, flow, phase) {
|
|
38
|
+
const terminationReason = flowState.terminationReason ?? "";
|
|
39
|
+
const match = /^Stopped by ([^:]+):/.exec(terminationReason);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const stoppedPhaseId = match[1];
|
|
44
|
+
const stoppedIndex = flow.phases.findIndex((candidate) => candidate.id === stoppedPhaseId);
|
|
45
|
+
const currentIndex = flow.phases.findIndex((candidate) => candidate.id === phase.id);
|
|
46
|
+
if (stoppedIndex < 0 || currentIndex < 0) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return currentIndex > stoppedIndex;
|
|
50
|
+
}
|
|
51
|
+
export function displayStatusForPhase(flowState, flow, phase, actualStatus) {
|
|
52
|
+
if (actualStatus) {
|
|
53
|
+
return actualStatus;
|
|
54
|
+
}
|
|
55
|
+
if (!flowState?.terminated) {
|
|
56
|
+
return "pending";
|
|
57
|
+
}
|
|
58
|
+
return isAfterTermination(flowState, flow, phase) ? "skipped" : "pending";
|
|
59
|
+
}
|
|
60
|
+
export function displayStatusForStep(flowState, flow, phase, actualStatus) {
|
|
61
|
+
if (actualStatus) {
|
|
62
|
+
return actualStatus;
|
|
63
|
+
}
|
|
64
|
+
if (!flowState?.terminated) {
|
|
65
|
+
return "pending";
|
|
66
|
+
}
|
|
67
|
+
return isAfterTermination(flowState, flow, phase) ? "skipped" : "pending";
|
|
68
|
+
}
|
|
69
|
+
export function statusForGroup(flow, phases, flowState) {
|
|
70
|
+
const statuses = phases.map((phase) => displayStatusForPhase(flowState, flow, phase, flowState?.phases.find((candidate) => candidate.id === phase.id)?.status ?? null));
|
|
71
|
+
if (statuses.some((status) => status === "running")) {
|
|
72
|
+
return "running";
|
|
73
|
+
}
|
|
74
|
+
if (statuses.every((status) => status === "skipped")) {
|
|
75
|
+
return "skipped";
|
|
76
|
+
}
|
|
77
|
+
if (statuses.every((status) => status === "done" || status === "skipped")) {
|
|
78
|
+
return "done";
|
|
79
|
+
}
|
|
80
|
+
return "pending";
|
|
81
|
+
}
|
|
82
|
+
export function groupPhases(flow) {
|
|
83
|
+
const items = [];
|
|
84
|
+
let index = 0;
|
|
85
|
+
while (index < flow.phases.length) {
|
|
86
|
+
const phase = flow.phases[index];
|
|
87
|
+
if (!phase) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
const label = repeatLabel(phase.repeatVars);
|
|
91
|
+
if (!label) {
|
|
92
|
+
items.push({ kind: "phase", phase });
|
|
93
|
+
index += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const phases = [phase];
|
|
97
|
+
let nextIndex = index + 1;
|
|
98
|
+
while (nextIndex < flow.phases.length) {
|
|
99
|
+
const candidate = flow.phases[nextIndex];
|
|
100
|
+
if (!candidate || repeatGroupKey(candidate.repeatVars) !== repeatGroupKey(phase.repeatVars)) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
phases.push(candidate);
|
|
104
|
+
nextIndex += 1;
|
|
105
|
+
}
|
|
106
|
+
items.push({ kind: "group", label, phases, seriesKey: repeatSeriesKey(phases) });
|
|
107
|
+
index = nextIndex;
|
|
108
|
+
}
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
export function shouldDisplayPhase(flow, flowState, phase) {
|
|
112
|
+
const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id) ?? null;
|
|
113
|
+
if (!flowState) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (phaseState?.status === "skipped" && flowState.terminated && isAfterTermination(flowState, flow, phase)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
export function visiblePhaseItems(flow, flowState) {
|
|
122
|
+
const pendingSeries = new Set();
|
|
123
|
+
return groupPhases(flow).filter((item) => {
|
|
124
|
+
if (item.kind === "phase") {
|
|
125
|
+
return shouldDisplayPhase(flow, flowState, item.phase);
|
|
126
|
+
}
|
|
127
|
+
const visiblePhases = item.phases.filter((phase) => shouldDisplayPhase(flow, flowState, phase));
|
|
128
|
+
const hasState = visiblePhases.some((phase) => flowState?.phases.some((candidate) => candidate.id === phase.id));
|
|
129
|
+
if (visiblePhases.length === 0) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (hasState) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (pendingSeries.has(item.seriesKey)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
pendingSeries.add(item.seriesKey);
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export function buildProgressViewModel(flow, flowState) {
|
|
143
|
+
if (!flow) {
|
|
144
|
+
return {
|
|
145
|
+
flow: null,
|
|
146
|
+
items: [],
|
|
147
|
+
anchorIndex: null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const items = [];
|
|
151
|
+
let anchorIndex = null;
|
|
152
|
+
let sawExecutedItem = false;
|
|
153
|
+
const rememberAnchor = (status) => {
|
|
154
|
+
if (status === "running") {
|
|
155
|
+
anchorIndex = items.length;
|
|
156
|
+
sawExecutedItem = true;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (status === "done" || status === "skipped") {
|
|
160
|
+
sawExecutedItem = true;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (status === "pending" && sawExecutedItem && anchorIndex === null) {
|
|
164
|
+
anchorIndex = items.length;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
for (const item of visiblePhaseItems(flow, flowState)) {
|
|
168
|
+
if (item.kind === "group") {
|
|
169
|
+
const visiblePhases = item.phases.filter((phase) => shouldDisplayPhase(flow, flowState, phase));
|
|
170
|
+
if (visiblePhases.length === 0) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const groupStatus = statusForGroup(flow, visiblePhases, flowState);
|
|
174
|
+
rememberAnchor(groupStatus);
|
|
175
|
+
items.push({
|
|
176
|
+
kind: "group",
|
|
177
|
+
label: item.label,
|
|
178
|
+
depth: 0,
|
|
179
|
+
status: groupStatus,
|
|
180
|
+
});
|
|
181
|
+
for (const phase of visiblePhases) {
|
|
182
|
+
const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
|
|
183
|
+
const phaseStatus = displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
|
|
184
|
+
rememberAnchor(phaseStatus);
|
|
185
|
+
items.push({
|
|
186
|
+
kind: "phase",
|
|
187
|
+
label: displayPhaseId(phase),
|
|
188
|
+
depth: 1,
|
|
189
|
+
status: phaseStatus,
|
|
190
|
+
});
|
|
191
|
+
for (const step of phase.steps) {
|
|
192
|
+
const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
|
|
193
|
+
const stepStatus = displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
|
|
194
|
+
rememberAnchor(stepStatus);
|
|
195
|
+
items.push({
|
|
196
|
+
kind: "step",
|
|
197
|
+
label: step.id,
|
|
198
|
+
depth: 2,
|
|
199
|
+
status: stepStatus,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const phase = item.phase;
|
|
206
|
+
if (!shouldDisplayPhase(flow, flowState, phase)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
|
|
210
|
+
const phaseStatus = displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
|
|
211
|
+
rememberAnchor(phaseStatus);
|
|
212
|
+
items.push({
|
|
213
|
+
kind: "phase",
|
|
214
|
+
label: displayPhaseId(phase),
|
|
215
|
+
depth: 0,
|
|
216
|
+
status: phaseStatus,
|
|
217
|
+
});
|
|
218
|
+
for (const step of phase.steps) {
|
|
219
|
+
const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
|
|
220
|
+
const stepStatus = displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
|
|
221
|
+
rememberAnchor(stepStatus);
|
|
222
|
+
items.push({
|
|
223
|
+
kind: "step",
|
|
224
|
+
label: step.id,
|
|
225
|
+
depth: 1,
|
|
226
|
+
status: stepStatus,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (flowState?.terminated) {
|
|
231
|
+
const terminationOutcome = flowState.terminationOutcome ?? "success";
|
|
232
|
+
items.push({
|
|
233
|
+
kind: "termination",
|
|
234
|
+
label: terminationOutcome === "stopped" ? "Flow stopped before completion" : "Flow completed successfully",
|
|
235
|
+
detail: `Reason: ${flowState.terminationReason ?? "flow terminated"}`,
|
|
236
|
+
depth: 0,
|
|
237
|
+
status: terminationOutcome === "stopped" ? "running" : "done",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
flow,
|
|
242
|
+
items,
|
|
243
|
+
anchorIndex,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildProgressViewModel } from "./progress.js";
|
|
2
|
+
import { computeVisibleFlowItems } from "./tree.js";
|
|
3
|
+
export function selectVisibleFlowItems(flowTree, expandedFlowFolders) {
|
|
4
|
+
return computeVisibleFlowItems(flowTree, expandedFlowFolders);
|
|
5
|
+
}
|
|
6
|
+
export function selectHeaderLabel(selectedItem, fallbackFlowId) {
|
|
7
|
+
if (!selectedItem) {
|
|
8
|
+
return fallbackFlowId;
|
|
9
|
+
}
|
|
10
|
+
return selectedItem.kind === "folder" ? selectedItem.pathSegments.join("/") : selectedItem.flow.label;
|
|
11
|
+
}
|
|
12
|
+
export function selectProgressViewModel(flow, flowState) {
|
|
13
|
+
return buildProgressViewModel(flow, flowState);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { buildFlowTree, collectInitiallyExpandedFolderKeys, computeVisibleFlowItems, makeFlowKey } from "./tree.js";
|
|
2
|
+
export function createInitialInteractiveState(options) {
|
|
3
|
+
const flowTree = buildFlowTree(options.flows);
|
|
4
|
+
const expandedFlowFolders = new Set(collectInitiallyExpandedFolderKeys(flowTree));
|
|
5
|
+
const visibleFlowItems = computeVisibleFlowItems(flowTree, expandedFlowFolders);
|
|
6
|
+
const initiallySelectedItem = visibleFlowItems.find((item) => item.kind === "flow") ?? visibleFlowItems[0];
|
|
7
|
+
const selectedFlowId = initiallySelectedItem?.kind === "flow" ? initiallySelectedItem.flow.id : options.flows[0]?.id ?? "auto-golang";
|
|
8
|
+
return {
|
|
9
|
+
scopeKey: options.scopeKey,
|
|
10
|
+
jiraIssueKey: options.jiraIssueKey ?? null,
|
|
11
|
+
summaryText: options.summaryText.trim(),
|
|
12
|
+
version: options.version ?? "",
|
|
13
|
+
flowTreeKeys: flowTree.map((node) => node.key),
|
|
14
|
+
selectedFlowId,
|
|
15
|
+
selectedFlowItemKey: initiallySelectedItem?.key ?? makeFlowKey(selectedFlowId),
|
|
16
|
+
focusedPane: "flows",
|
|
17
|
+
summaryVisible: options.summaryText.trim().length > 0,
|
|
18
|
+
busy: false,
|
|
19
|
+
currentFlowId: null,
|
|
20
|
+
currentNode: null,
|
|
21
|
+
currentExecutor: null,
|
|
22
|
+
failedFlowId: null,
|
|
23
|
+
flowState: {
|
|
24
|
+
flowId: null,
|
|
25
|
+
executionState: null,
|
|
26
|
+
},
|
|
27
|
+
runningStartedAt: null,
|
|
28
|
+
spinnerFrame: 0,
|
|
29
|
+
progressScrollOffset: 0,
|
|
30
|
+
summaryScrollOffset: 0,
|
|
31
|
+
logScrollOffset: 0,
|
|
32
|
+
helpScrollOffset: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
function compareTreeNames(left, right) {
|
|
2
|
+
return left.localeCompare(right, "ru");
|
|
3
|
+
}
|
|
4
|
+
export function makeFolderKey(pathSegments) {
|
|
5
|
+
return `folder:${pathSegments.join("/")}`;
|
|
6
|
+
}
|
|
7
|
+
export function makeFlowKey(flowId) {
|
|
8
|
+
return `flow:${flowId}`;
|
|
9
|
+
}
|
|
10
|
+
export function buildFlowTree(flows) {
|
|
11
|
+
const roots = new Map();
|
|
12
|
+
const ensureFolder = (pathSegments) => {
|
|
13
|
+
const firstSegment = pathSegments[0];
|
|
14
|
+
if (!firstSegment) {
|
|
15
|
+
throw new Error("Flow tree folder path cannot be empty.");
|
|
16
|
+
}
|
|
17
|
+
const rootFolder = roots.get(firstSegment);
|
|
18
|
+
let currentFolder;
|
|
19
|
+
if (rootFolder) {
|
|
20
|
+
currentFolder = rootFolder;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
currentFolder = {
|
|
24
|
+
kind: "folder",
|
|
25
|
+
key: makeFolderKey([firstSegment]),
|
|
26
|
+
name: firstSegment,
|
|
27
|
+
pathSegments: [firstSegment],
|
|
28
|
+
children: [],
|
|
29
|
+
};
|
|
30
|
+
roots.set(firstSegment, currentFolder);
|
|
31
|
+
}
|
|
32
|
+
for (let index = 1; index < pathSegments.length; index += 1) {
|
|
33
|
+
const segment = pathSegments[index] ?? "";
|
|
34
|
+
const folderPath = pathSegments.slice(0, index + 1);
|
|
35
|
+
let nextFolder = currentFolder.children.find((child) => child.kind === "folder" && child.name === segment);
|
|
36
|
+
if (!nextFolder) {
|
|
37
|
+
nextFolder = {
|
|
38
|
+
kind: "folder",
|
|
39
|
+
key: makeFolderKey(folderPath),
|
|
40
|
+
name: segment,
|
|
41
|
+
pathSegments: folderPath,
|
|
42
|
+
children: [],
|
|
43
|
+
};
|
|
44
|
+
currentFolder.children.push(nextFolder);
|
|
45
|
+
}
|
|
46
|
+
currentFolder = nextFolder;
|
|
47
|
+
}
|
|
48
|
+
return currentFolder;
|
|
49
|
+
};
|
|
50
|
+
for (const flow of flows) {
|
|
51
|
+
if (flow.treePath.length === 0) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const folderPath = flow.treePath.slice(0, -1);
|
|
55
|
+
const leafName = flow.treePath[flow.treePath.length - 1] ?? flow.id;
|
|
56
|
+
const parent = ensureFolder(folderPath);
|
|
57
|
+
parent.children.push({
|
|
58
|
+
kind: "flow",
|
|
59
|
+
key: makeFlowKey(flow.id),
|
|
60
|
+
name: leafName,
|
|
61
|
+
pathSegments: [...flow.treePath],
|
|
62
|
+
flow,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const sortNodes = (nodes) => [...nodes]
|
|
66
|
+
.sort((left, right) => {
|
|
67
|
+
if (left.kind !== right.kind) {
|
|
68
|
+
return left.kind === "folder" ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
return compareTreeNames(left.name, right.name);
|
|
71
|
+
})
|
|
72
|
+
.map((node) => node.kind === "folder"
|
|
73
|
+
? {
|
|
74
|
+
...node,
|
|
75
|
+
children: sortNodes(node.children),
|
|
76
|
+
}
|
|
77
|
+
: node);
|
|
78
|
+
const orderedRootNames = ["global", "custom", "default"];
|
|
79
|
+
const sortedRoots = [...roots.values()].sort((left, right) => {
|
|
80
|
+
const leftIndex = orderedRootNames.indexOf(left.name);
|
|
81
|
+
const rightIndex = orderedRootNames.indexOf(right.name);
|
|
82
|
+
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
83
|
+
if (leftIndex === -1) {
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
if (rightIndex === -1) {
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
return leftIndex - rightIndex;
|
|
90
|
+
}
|
|
91
|
+
return compareTreeNames(left.name, right.name);
|
|
92
|
+
});
|
|
93
|
+
return sortNodes(sortedRoots);
|
|
94
|
+
}
|
|
95
|
+
export function computeVisibleFlowItems(flowTree, expandedFlowFolders) {
|
|
96
|
+
const items = [];
|
|
97
|
+
const walk = (nodes, depth) => {
|
|
98
|
+
for (const node of nodes) {
|
|
99
|
+
if (node.kind === "folder") {
|
|
100
|
+
items.push({
|
|
101
|
+
kind: "folder",
|
|
102
|
+
key: node.key,
|
|
103
|
+
name: node.name,
|
|
104
|
+
depth,
|
|
105
|
+
pathSegments: [...node.pathSegments],
|
|
106
|
+
});
|
|
107
|
+
if (expandedFlowFolders.has(node.key)) {
|
|
108
|
+
walk(node.children, depth + 1);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
items.push({
|
|
113
|
+
kind: "flow",
|
|
114
|
+
key: node.key,
|
|
115
|
+
name: node.name,
|
|
116
|
+
depth,
|
|
117
|
+
pathSegments: [...node.pathSegments],
|
|
118
|
+
flow: node.flow,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
walk(flowTree, 0);
|
|
123
|
+
return items;
|
|
124
|
+
}
|
|
125
|
+
export function collectFolderKeys(flowTree) {
|
|
126
|
+
const keys = [];
|
|
127
|
+
const walk = (nodes) => {
|
|
128
|
+
for (const node of nodes) {
|
|
129
|
+
if (node.kind !== "folder") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
keys.push(node.key);
|
|
133
|
+
walk(node.children);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
walk(flowTree);
|
|
137
|
+
return keys;
|
|
138
|
+
}
|
|
139
|
+
export function collectInitiallyExpandedFolderKeys(flowTree) {
|
|
140
|
+
const keys = [];
|
|
141
|
+
const walk = (nodes) => {
|
|
142
|
+
for (const node of nodes) {
|
|
143
|
+
if (node.kind !== "folder") {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const expandedByDefault = node.pathSegments.length === 1 && (node.name === "default" || node.name === "global");
|
|
147
|
+
if (expandedByDefault) {
|
|
148
|
+
keys.push(node.key);
|
|
149
|
+
}
|
|
150
|
+
walk(node.children);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
walk(flowTree);
|
|
154
|
+
return keys;
|
|
155
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|