agentweaver 0.1.19 → 0.1.20
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 +47 -7
- package/dist/artifacts.js +9 -0
- package/dist/executors/git-commit-executor.js +24 -6
- package/dist/flow-state.js +3 -8
- package/dist/git/git-diff-parser.js +223 -0
- package/dist/git/git-service.js +562 -0
- package/dist/git/git-stage-selection.js +24 -0
- package/dist/git/git-status-parser.js +171 -0
- package/dist/git/git-types.js +1 -0
- package/dist/index.js +450 -108
- package/dist/interactive/auto-flow.js +644 -0
- package/dist/interactive/controller.js +417 -9
- package/dist/interactive/progress.js +194 -1
- package/dist/interactive/state.js +25 -0
- package/dist/interactive/web/index.js +97 -12
- package/dist/interactive/web/protocol.js +216 -1
- package/dist/interactive/web/server.js +72 -14
- package/dist/interactive/web/static/app.js +1603 -49
- package/dist/interactive/web/static/index.html +76 -11
- package/dist/interactive/web/static/styles.css +1 -1
- package/dist/interactive/web/static/styles.input.css +901 -47
- package/dist/pipeline/auto-flow-blocks.js +307 -0
- package/dist/pipeline/auto-flow-config.js +273 -0
- package/dist/pipeline/auto-flow-identity.js +49 -0
- package/dist/pipeline/auto-flow-presets.js +52 -0
- package/dist/pipeline/auto-flow-resolver.js +830 -0
- package/dist/pipeline/auto-flow-types.js +17 -0
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/declarative-flows.js +27 -1
- package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
- package/dist/pipeline/flow-specs/auto-golang.json +12 -1
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
- package/dist/pipeline/flow-specs/review/review-project.json +19 -1
- package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
- package/dist/pipeline/node-registry.js +9 -0
- package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
- package/dist/pipeline/nodes/flow-run-node.js +5 -3
- package/dist/pipeline/nodes/git-status-node.js +2 -168
- package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
- package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
- package/dist/pipeline/nodes/plan-codex-node.js +8 -1
- package/dist/pipeline/spec-loader.js +14 -4
- package/dist/runtime/artifact-catalog.js +29 -5
- package/dist/runtime/settings.js +114 -0
- package/dist/scope.js +14 -4
- package/package.json +1 -1
- package/dist/pipeline/flow-specs/auto-common.json +0 -179
- package/dist/pipeline/flow-specs/auto-simple.json +0 -141
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { FlowInterruptedError, TaskRunnerError } from "../errors.js";
|
|
3
|
+
import { createGitService } from "../git/git-service.js";
|
|
4
|
+
import { needsGitFileStage } from "../git/git-stage-selection.js";
|
|
3
5
|
import { renderMarkdownToTerminal } from "../markdown.js";
|
|
6
|
+
import { loadAutoFlowConfigByName, saveAutoFlowConfig, } from "../pipeline/auto-flow-config.js";
|
|
7
|
+
import { runCommand } from "../runtime/process-runner.js";
|
|
4
8
|
import { setOutputAdapter, stripAnsi } from "../tui.js";
|
|
5
9
|
import { buildInitialUserInputValues, normalizeUserInputFieldValue, resolveFieldDefinition, validateUserInputValues, } from "../user-input.js";
|
|
10
|
+
import { buildAutoFlowEditorViewModel, createConfigAutoFlowDefinition, insertAutoFlowBlock, removeAutoFlowBlock, setAutoFlowBlockEnabled, updateAutoFlowBlockParameter, } from "./auto-flow.js";
|
|
6
11
|
import { buildProgressViewModel } from "./progress.js";
|
|
7
12
|
import { selectHeaderLabel } from "./selectors.js";
|
|
8
13
|
import { createInitialInteractiveState } from "./state.js";
|
|
@@ -28,6 +33,9 @@ const LOG_FLUSH_INTERVAL_MS = 120;
|
|
|
28
33
|
function clamp(value, min, max) {
|
|
29
34
|
return Math.min(max, Math.max(min, value));
|
|
30
35
|
}
|
|
36
|
+
function cloneSavedAutoFlowConfig(config) {
|
|
37
|
+
return JSON.parse(JSON.stringify(config));
|
|
38
|
+
}
|
|
31
39
|
function isPrintableCharacter(ch, key) {
|
|
32
40
|
return Boolean(ch) && !key.ctrl && !key.meta && !/^[\x00-\x1f\x7f]$/.test(ch);
|
|
33
41
|
}
|
|
@@ -93,12 +101,16 @@ function normalizeLogText(text) {
|
|
|
93
101
|
}
|
|
94
102
|
return normalized.split("\n");
|
|
95
103
|
}
|
|
104
|
+
function isGitFileStaged(file) {
|
|
105
|
+
return file.indexStatus !== " " && file.indexStatus !== "?";
|
|
106
|
+
}
|
|
96
107
|
export class InteractiveSessionController {
|
|
97
108
|
options;
|
|
98
109
|
listeners = new Set();
|
|
99
110
|
flowMap;
|
|
100
111
|
flowTree;
|
|
101
112
|
expandedFlowFolders = new Set();
|
|
113
|
+
autoFlowEditors = new Map();
|
|
102
114
|
visibleFlowItems;
|
|
103
115
|
state;
|
|
104
116
|
logLines = [];
|
|
@@ -110,6 +122,7 @@ export class InteractiveSessionController {
|
|
|
110
122
|
activeFormSession = null;
|
|
111
123
|
spinnerTimer = null;
|
|
112
124
|
mounted = false;
|
|
125
|
+
gitService;
|
|
113
126
|
constructor(options) {
|
|
114
127
|
this.options = options;
|
|
115
128
|
if (options.flows.length === 0) {
|
|
@@ -117,9 +130,24 @@ export class InteractiveSessionController {
|
|
|
117
130
|
}
|
|
118
131
|
this.state = createInitialInteractiveState(options);
|
|
119
132
|
this.flowMap = new Map(options.flows.map((flow) => [flow.id, flow]));
|
|
133
|
+
for (const flow of options.flows) {
|
|
134
|
+
if (!flow.autoFlow) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
this.autoFlowEditors.set(flow.id, {
|
|
138
|
+
definition: flow.autoFlow,
|
|
139
|
+
config: cloneSavedAutoFlowConfig(flow.autoFlow.config),
|
|
140
|
+
diagnostics: [],
|
|
141
|
+
saveTarget: "project",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
120
144
|
this.flowTree = buildFlowTree(options.flows);
|
|
121
145
|
collectInitiallyExpandedFolderKeys(this.flowTree).forEach((key) => this.expandedFlowFolders.add(key));
|
|
122
146
|
this.visibleFlowItems = computeVisibleFlowItems(this.flowTree, this.expandedFlowFolders);
|
|
147
|
+
this.gitService = options.gitService ?? createGitService({
|
|
148
|
+
cwd: options.cwd,
|
|
149
|
+
runCommand,
|
|
150
|
+
});
|
|
123
151
|
}
|
|
124
152
|
subscribe(listener) {
|
|
125
153
|
this.listeners.add(listener);
|
|
@@ -525,6 +553,161 @@ export class InteractiveSessionController {
|
|
|
525
553
|
getCurrentFlowExecutionState() {
|
|
526
554
|
return this.state.flowState.executionState;
|
|
527
555
|
}
|
|
556
|
+
getGitService() {
|
|
557
|
+
return this.gitService;
|
|
558
|
+
}
|
|
559
|
+
getGitWorkspaceSnapshot() {
|
|
560
|
+
return this.state.gitWorkspace;
|
|
561
|
+
}
|
|
562
|
+
getAutoFlowEditor(flowId = this.state.selectedFlowId) {
|
|
563
|
+
return this.autoFlowViewForFlow(flowId);
|
|
564
|
+
}
|
|
565
|
+
selectAutoFlowPreset(preset) {
|
|
566
|
+
const flowId = preset === "simple" ? "auto-simple" : "auto-common";
|
|
567
|
+
this.selectFlowId(flowId);
|
|
568
|
+
}
|
|
569
|
+
loadAutoFlowConfig(name, flowId = this.state.selectedFlowId) {
|
|
570
|
+
const loaded = loadAutoFlowConfigByName(name, this.options.cwd);
|
|
571
|
+
const source = {
|
|
572
|
+
type: loaded.source.type === "project" ? "project-config" : "user-config",
|
|
573
|
+
configName: loaded.config.name,
|
|
574
|
+
path: loaded.source.path,
|
|
575
|
+
...(loaded.source.shadowedUserPath ? { shadowedUserPath: loaded.source.shadowedUserPath } : {}),
|
|
576
|
+
};
|
|
577
|
+
const definition = createConfigAutoFlowDefinition({
|
|
578
|
+
config: loaded.config,
|
|
579
|
+
source,
|
|
580
|
+
});
|
|
581
|
+
const targetFlowId = this.flowMap.has(`auto-config:${loaded.config.name}`)
|
|
582
|
+
? `auto-config:${loaded.config.name}`
|
|
583
|
+
: flowId;
|
|
584
|
+
if (!this.autoFlowEditors.has(targetFlowId)) {
|
|
585
|
+
throw new Error(`Flow '${targetFlowId}' is not a configurable auto-flow entry.`);
|
|
586
|
+
}
|
|
587
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
588
|
+
definition,
|
|
589
|
+
config: cloneSavedAutoFlowConfig(loaded.config),
|
|
590
|
+
diagnostics: [],
|
|
591
|
+
saveTarget: loaded.source.type,
|
|
592
|
+
lastMessage: `Loaded auto-flow config '${loaded.config.name}'.`,
|
|
593
|
+
});
|
|
594
|
+
this.selectFlowId(targetFlowId);
|
|
595
|
+
this.emitChange();
|
|
596
|
+
}
|
|
597
|
+
toggleAutoFlowBlock(flowId, blockId, enabled, slotId) {
|
|
598
|
+
const targetFlowId = flowId ?? this.state.selectedFlowId;
|
|
599
|
+
const editor = this.requireAutoFlowEditor(targetFlowId);
|
|
600
|
+
const view = this.autoFlowViewForFlow(targetFlowId);
|
|
601
|
+
const currentBlock = view?.slots.flatMap((slot) => slot.blocks).find((block) => (block.blockId === blockId && (slotId === undefined || block.slotId === slotId)));
|
|
602
|
+
const nextEnabled = enabled ?? !(currentBlock?.enabled ?? true);
|
|
603
|
+
const result = setAutoFlowBlockEnabled(editor.config, blockId, nextEnabled, slotId);
|
|
604
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
605
|
+
...editor,
|
|
606
|
+
config: result.config,
|
|
607
|
+
diagnostics: result.diagnostics,
|
|
608
|
+
lastMessage: result.diagnostics.length > 0
|
|
609
|
+
? result.diagnostics[0]?.message ?? `Auto-flow block '${blockId}' could not be updated.`
|
|
610
|
+
: `${nextEnabled ? "Enabled" : "Disabled"} auto-flow block '${blockId}'${slotId ? ` in '${slotId}'` : ""}.`,
|
|
611
|
+
});
|
|
612
|
+
this.emitChange();
|
|
613
|
+
if (result.diagnostics.length > 0) {
|
|
614
|
+
throw new TaskRunnerError(result.diagnostics[0]?.message ?? `Auto-flow block '${blockId}' could not be updated.`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
updateAutoFlowParameter(flowId, blockId, paramName, value, slotId) {
|
|
618
|
+
const targetFlowId = flowId ?? this.state.selectedFlowId;
|
|
619
|
+
const editor = this.requireAutoFlowEditor(targetFlowId);
|
|
620
|
+
const result = updateAutoFlowBlockParameter(editor.config, blockId, paramName, value, slotId);
|
|
621
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
622
|
+
...editor,
|
|
623
|
+
config: result.config,
|
|
624
|
+
diagnostics: result.diagnostics,
|
|
625
|
+
lastMessage: result.diagnostics.length > 0
|
|
626
|
+
? result.diagnostics[0]?.message ?? `Auto-flow block '${blockId}' parameter '${paramName}' is invalid.`
|
|
627
|
+
: `Updated '${blockId}.${paramName}'${slotId ? ` in '${slotId}'` : ""} to ${value}.`,
|
|
628
|
+
});
|
|
629
|
+
this.emitChange();
|
|
630
|
+
}
|
|
631
|
+
insertAutoFlowBlock(flowId, slotId, blockId) {
|
|
632
|
+
const targetFlowId = flowId ?? this.state.selectedFlowId;
|
|
633
|
+
const editor = this.requireAutoFlowEditor(targetFlowId);
|
|
634
|
+
const result = insertAutoFlowBlock(editor.config, slotId, blockId);
|
|
635
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
636
|
+
...editor,
|
|
637
|
+
config: result.config,
|
|
638
|
+
diagnostics: result.diagnostics,
|
|
639
|
+
lastMessage: result.diagnostics.length > 0
|
|
640
|
+
? result.diagnostics[0]?.message ?? `Auto-flow block '${blockId}' could not be inserted into '${slotId}'.`
|
|
641
|
+
: `Inserted auto-flow block '${blockId}' into '${slotId}'.`,
|
|
642
|
+
});
|
|
643
|
+
this.emitChange();
|
|
644
|
+
if (!result.inserted || result.diagnostics.length > 0) {
|
|
645
|
+
throw new TaskRunnerError(result.diagnostics[0]?.message ?? `Auto-flow block '${blockId}' could not be inserted into '${slotId}'.`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
removeAutoFlowBlock(flowId, slotId, blockId) {
|
|
649
|
+
const targetFlowId = flowId ?? this.state.selectedFlowId;
|
|
650
|
+
const editor = this.requireAutoFlowEditor(targetFlowId);
|
|
651
|
+
const result = removeAutoFlowBlock(editor.config, slotId, blockId);
|
|
652
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
653
|
+
...editor,
|
|
654
|
+
config: result.config,
|
|
655
|
+
diagnostics: result.diagnostics,
|
|
656
|
+
lastMessage: result.diagnostics.length > 0
|
|
657
|
+
? result.diagnostics[0]?.message ?? `Auto-flow block '${blockId}' could not be removed from '${slotId}'.`
|
|
658
|
+
: `Removed auto-flow block '${blockId}' from '${slotId}'.`,
|
|
659
|
+
});
|
|
660
|
+
this.emitChange();
|
|
661
|
+
if (!result.removed || result.diagnostics.length > 0) {
|
|
662
|
+
throw new TaskRunnerError(result.diagnostics[0]?.message ?? `Auto-flow block '${blockId}' could not be removed from '${slotId}'.`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
resetAutoFlowConfig(flowId) {
|
|
666
|
+
const targetFlowId = flowId ?? this.state.selectedFlowId;
|
|
667
|
+
const editor = this.requireAutoFlowEditor(targetFlowId);
|
|
668
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
669
|
+
...editor,
|
|
670
|
+
config: cloneSavedAutoFlowConfig(editor.definition.config),
|
|
671
|
+
diagnostics: [],
|
|
672
|
+
lastMessage: "Reset auto-flow changes.",
|
|
673
|
+
});
|
|
674
|
+
this.emitChange();
|
|
675
|
+
}
|
|
676
|
+
saveAutoFlowConfig(flowId, name, location) {
|
|
677
|
+
const targetFlowId = flowId ?? this.state.selectedFlowId;
|
|
678
|
+
const editor = this.requireAutoFlowEditor(targetFlowId);
|
|
679
|
+
const view = this.autoFlowViewForFlow(targetFlowId);
|
|
680
|
+
if (view && !view.status.canSave) {
|
|
681
|
+
const message = view.diagnostics[0]?.message ?? "Auto-flow config has validation errors and cannot be saved.";
|
|
682
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
683
|
+
...editor,
|
|
684
|
+
lastMessage: message,
|
|
685
|
+
});
|
|
686
|
+
this.emitChange();
|
|
687
|
+
throw new TaskRunnerError(message);
|
|
688
|
+
}
|
|
689
|
+
const config = {
|
|
690
|
+
...editor.config,
|
|
691
|
+
...(name ? { name } : {}),
|
|
692
|
+
};
|
|
693
|
+
const result = saveAutoFlowConfig(config, {
|
|
694
|
+
cwd: this.options.cwd,
|
|
695
|
+
location: location ?? editor.saveTarget,
|
|
696
|
+
});
|
|
697
|
+
this.autoFlowEditors.set(targetFlowId, {
|
|
698
|
+
...editor,
|
|
699
|
+
definition: {
|
|
700
|
+
...editor.definition,
|
|
701
|
+
config: cloneSavedAutoFlowConfig(result.config),
|
|
702
|
+
},
|
|
703
|
+
config: result.config,
|
|
704
|
+
diagnostics: [],
|
|
705
|
+
saveTarget: result.source.type,
|
|
706
|
+
lastMessage: `Saved auto-flow config '${result.config.name}' to ${result.source.path}.`,
|
|
707
|
+
});
|
|
708
|
+
this.appendLog(`Saved auto-flow config '${result.config.name}' to ${result.source.path}.`);
|
|
709
|
+
this.emitChange();
|
|
710
|
+
}
|
|
528
711
|
hasActiveInput() {
|
|
529
712
|
return this.confirmSession !== null || this.activeFormSession !== null;
|
|
530
713
|
}
|
|
@@ -542,8 +725,8 @@ export class InteractiveSessionController {
|
|
|
542
725
|
? "The workflow failed, but artifacts are available for review."
|
|
543
726
|
: "The workflow failed. The explorer can check for any artifacts written before failure."
|
|
544
727
|
: hasCount && count === 0
|
|
545
|
-
? "The workflow completed, but no artifacts were found for this
|
|
546
|
-
: "The workflow completed and artifacts are available for review.");
|
|
728
|
+
? "The workflow completed, but no artifacts were found for this scope yet."
|
|
729
|
+
: "The workflow completed and scope artifacts are available for review.");
|
|
547
730
|
this.state.artifactExplorer = {
|
|
548
731
|
available: true,
|
|
549
732
|
open: Boolean(input.open) && !this.hasActiveInput(),
|
|
@@ -595,17 +778,115 @@ export class InteractiveSessionController {
|
|
|
595
778
|
};
|
|
596
779
|
this.emitChange();
|
|
597
780
|
}
|
|
781
|
+
async refreshGitWorkspace(operation) {
|
|
782
|
+
const previous = this.state.gitWorkspace;
|
|
783
|
+
const snapshot = await this.gitService.status();
|
|
784
|
+
const validPaths = new Set(snapshot.changedFiles.map((file) => file.path));
|
|
785
|
+
this.state.gitWorkspace = {
|
|
786
|
+
...snapshot,
|
|
787
|
+
selectedPaths: previous.selectedPaths.filter((filePath) => validPaths.has(filePath)),
|
|
788
|
+
commitMessage: previous.commitMessage,
|
|
789
|
+
operation: operation ?? previous.operation,
|
|
790
|
+
};
|
|
791
|
+
this.emitChange();
|
|
792
|
+
}
|
|
793
|
+
updateGitCommitMessage(message) {
|
|
794
|
+
this.state.gitWorkspace = {
|
|
795
|
+
...this.state.gitWorkspace,
|
|
796
|
+
commitMessage: message,
|
|
797
|
+
};
|
|
798
|
+
this.emitChange();
|
|
799
|
+
}
|
|
800
|
+
updateGitSelectedPaths(paths) {
|
|
801
|
+
const changed = new Set(this.state.gitWorkspace.changedFiles.map((file) => file.path));
|
|
802
|
+
const selectedPaths = paths.filter((filePath, index, allPaths) => changed.has(filePath) && allPaths.indexOf(filePath) === index);
|
|
803
|
+
this.state.gitWorkspace = {
|
|
804
|
+
...this.state.gitWorkspace,
|
|
805
|
+
selectedPaths,
|
|
806
|
+
};
|
|
807
|
+
this.emitChange();
|
|
808
|
+
}
|
|
809
|
+
async createGitBranch(branchName) {
|
|
810
|
+
await this.runGitOperation("create branch", () => this.gitService.createBranch(branchName));
|
|
811
|
+
}
|
|
812
|
+
async checkoutGitBranch(branchName) {
|
|
813
|
+
await this.runGitOperation("checkout", () => this.gitService.checkout(branchName));
|
|
814
|
+
}
|
|
815
|
+
async fetchGitWorkspace() {
|
|
816
|
+
await this.runGitOperation("fetch", () => this.gitService.fetch());
|
|
817
|
+
}
|
|
818
|
+
async pullGitWorkspaceFfOnly() {
|
|
819
|
+
await this.runGitOperation("pull --ff-only", () => this.gitService.pullFfOnly());
|
|
820
|
+
}
|
|
821
|
+
async stageGitPaths(paths) {
|
|
822
|
+
if (paths.length === 0) {
|
|
823
|
+
this.setGitOperationError("No files were selected for staging.");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const snapshot = this.state.gitWorkspace;
|
|
827
|
+
this.updateGitSelectedPaths(paths);
|
|
828
|
+
const stagePaths = this.filterGitPaths(paths, snapshot, needsGitFileStage);
|
|
829
|
+
if (stagePaths.length === 0) {
|
|
830
|
+
this.setGitOperationError("Selected files are already staged.");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
await this.runGitOperation("stage", () => this.gitService.stage(stagePaths, snapshot));
|
|
834
|
+
}
|
|
835
|
+
async unstageGitPaths(paths) {
|
|
836
|
+
if (paths.length === 0) {
|
|
837
|
+
this.setGitOperationError("No files were selected for unstaging.");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const snapshot = this.state.gitWorkspace;
|
|
841
|
+
this.updateGitSelectedPaths(paths);
|
|
842
|
+
const unstagePaths = this.filterGitPaths(paths, snapshot, isGitFileStaged);
|
|
843
|
+
if (unstagePaths.length === 0) {
|
|
844
|
+
this.setGitOperationError("Selected files are not staged.");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
await this.runGitOperation("unstage", () => this.gitService.unstage(unstagePaths, snapshot));
|
|
848
|
+
}
|
|
849
|
+
filterGitPaths(paths, snapshot, predicate) {
|
|
850
|
+
const selected = new Set(paths);
|
|
851
|
+
return snapshot.changedFiles
|
|
852
|
+
.filter((file) => selected.has(file.path) && predicate(file))
|
|
853
|
+
.map((file) => file.path);
|
|
854
|
+
}
|
|
855
|
+
async commitGitChanges(paths, message) {
|
|
856
|
+
if (message.trim().length === 0) {
|
|
857
|
+
this.setGitOperationError("Commit message must not be empty.");
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const snapshot = this.state.gitWorkspace;
|
|
861
|
+
const commitPaths = paths ?? snapshot.selectedPaths;
|
|
862
|
+
if (paths) {
|
|
863
|
+
this.updateGitSelectedPaths(paths);
|
|
864
|
+
}
|
|
865
|
+
this.updateGitCommitMessage(message);
|
|
866
|
+
await this.runGitOperation("commit", () => this.gitService.commit(commitPaths, message, snapshot));
|
|
867
|
+
}
|
|
868
|
+
async pushGitWorkspace() {
|
|
869
|
+
const snapshot = this.state.gitWorkspace;
|
|
870
|
+
if (!snapshot.canPush) {
|
|
871
|
+
this.setGitOperationError(snapshot.pushDisabledReason ?? "Push is not available.");
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
await this.runGitOperation("push", () => this.gitService.push(snapshot));
|
|
875
|
+
}
|
|
598
876
|
getViewModel(layout) {
|
|
599
877
|
const selectedItem = this.selectedFlowTreeItem();
|
|
600
878
|
const activeFlowId = this.activeFlowId();
|
|
601
|
-
const selectedFlow = selectedItem?.kind === "flow" ? selectedItem.flow : null;
|
|
602
|
-
const progressFlow = this.state.busy ? this.flowMap.get(activeFlowId) ?? null : selectedFlow;
|
|
879
|
+
const selectedFlow = this.flowWithAutoFlowEditor(selectedItem?.kind === "flow" ? selectedItem.flow : null);
|
|
880
|
+
const progressFlow = this.flowWithAutoFlowEditor(this.state.busy ? this.flowMap.get(activeFlowId) ?? null : selectedFlow);
|
|
603
881
|
const progressState = progressFlow && this.state.flowState.flowId === progressFlow.id
|
|
604
882
|
? this.state.flowState.executionState
|
|
605
883
|
: progressFlow && this.state.currentFlowId === progressFlow.id
|
|
606
884
|
? this.state.flowState.executionState
|
|
607
885
|
: null;
|
|
608
|
-
const progressViewModel = buildProgressViewModel(progressFlow, progressState
|
|
886
|
+
const progressViewModel = buildProgressViewModel(progressFlow, progressState, {
|
|
887
|
+
failedFlowId: this.state.failedFlowId,
|
|
888
|
+
waitingForUserInput: this.activeFormSession !== null,
|
|
889
|
+
});
|
|
609
890
|
const helpText = `${HELP_TEXT}\n\nAvailable flows:\n${this.options.flows.map((flow) => `- ${flow.treePath.join("/")}`).join("\n")}`;
|
|
610
891
|
return {
|
|
611
892
|
header: this.buildHeaderText(),
|
|
@@ -625,6 +906,8 @@ export class InteractiveSessionController {
|
|
|
625
906
|
})),
|
|
626
907
|
selectedFlowIndex: Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey)),
|
|
627
908
|
progressTitle: this.panelTitle("Current Flow", "progress"),
|
|
909
|
+
progress: progressViewModel,
|
|
910
|
+
autoFlow: selectedFlow?.autoFlow ? this.autoFlowViewForFlow(selectedFlow.id) : null,
|
|
628
911
|
progressText: this.renderProgress(progressViewModel),
|
|
629
912
|
progressScrollOffset: this.state.progressScrollOffset,
|
|
630
913
|
descriptionText: this.renderDescription(selectedItem),
|
|
@@ -640,13 +923,83 @@ export class InteractiveSessionController {
|
|
|
640
923
|
confirmation: this.renderConfirmationView(),
|
|
641
924
|
form: this.renderFormView(layout),
|
|
642
925
|
artifactExplorer: { ...this.state.artifactExplorer },
|
|
926
|
+
gitWorkspace: {
|
|
927
|
+
...this.state.gitWorkspace,
|
|
928
|
+
changedFiles: this.state.gitWorkspace.changedFiles.map((file) => ({ ...file })),
|
|
929
|
+
branches: this.state.gitWorkspace.branches.map((branch) => ({ ...branch })),
|
|
930
|
+
remotes: this.state.gitWorkspace.remotes.map((remote) => ({ ...remote })),
|
|
931
|
+
warnings: [...this.state.gitWorkspace.warnings],
|
|
932
|
+
selectedPaths: [...this.state.gitWorkspace.selectedPaths],
|
|
933
|
+
operation: { ...this.state.gitWorkspace.operation },
|
|
934
|
+
},
|
|
643
935
|
};
|
|
644
936
|
}
|
|
937
|
+
setGitOperationError(message) {
|
|
938
|
+
this.state.gitWorkspace = {
|
|
939
|
+
...this.state.gitWorkspace,
|
|
940
|
+
operation: { status: "error", message },
|
|
941
|
+
};
|
|
942
|
+
this.appendLog(`Git operation failed: ${message}`);
|
|
943
|
+
this.emitChange();
|
|
944
|
+
}
|
|
945
|
+
async runGitOperation(action, operation) {
|
|
946
|
+
this.state.gitWorkspace = {
|
|
947
|
+
...this.state.gitWorkspace,
|
|
948
|
+
operation: { status: "running", action, message: `Running git ${action}...` },
|
|
949
|
+
};
|
|
950
|
+
this.emitChange();
|
|
951
|
+
const result = await operation();
|
|
952
|
+
if (result.status === "success") {
|
|
953
|
+
this.appendLog(`Git ${action}: ${result.message ?? "completed."}`);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
this.appendLog(`Git ${action} failed: ${result.message ?? "Git operation failed."}`);
|
|
957
|
+
}
|
|
958
|
+
await this.refreshGitWorkspace({ ...result, action });
|
|
959
|
+
}
|
|
645
960
|
emitChange(event = { type: "render" }) {
|
|
646
961
|
for (const listener of this.listeners) {
|
|
647
962
|
listener(event);
|
|
648
963
|
}
|
|
649
964
|
}
|
|
965
|
+
requireAutoFlowEditor(flowId) {
|
|
966
|
+
const editor = this.autoFlowEditors.get(flowId);
|
|
967
|
+
if (!editor) {
|
|
968
|
+
throw new Error(`Flow '${flowId}' is not a configurable auto-flow entry.`);
|
|
969
|
+
}
|
|
970
|
+
return editor;
|
|
971
|
+
}
|
|
972
|
+
flowWithAutoFlowEditor(flow) {
|
|
973
|
+
if (!flow?.autoFlow) {
|
|
974
|
+
return flow;
|
|
975
|
+
}
|
|
976
|
+
const editor = this.autoFlowEditors.get(flow.id);
|
|
977
|
+
if (!editor) {
|
|
978
|
+
return flow;
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
...flow,
|
|
982
|
+
autoFlow: {
|
|
983
|
+
...editor.definition,
|
|
984
|
+
config: cloneSavedAutoFlowConfig(editor.config),
|
|
985
|
+
...(editor.diagnostics.length > 0 ? { diagnostics: editor.diagnostics } : {}),
|
|
986
|
+
...(editor.lastMessage ? { lastMessage: editor.lastMessage } : {}),
|
|
987
|
+
},
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
autoFlowViewForFlow(flowId) {
|
|
991
|
+
const flow = this.flowMap.get(flowId);
|
|
992
|
+
const editor = this.autoFlowEditors.get(flowId);
|
|
993
|
+
if (!flow?.autoFlow || !editor) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
return buildAutoFlowEditorViewModel(editor.definition, {
|
|
997
|
+
config: editor.config,
|
|
998
|
+
...(editor.diagnostics.length > 0 ? { diagnostics: editor.diagnostics } : {}),
|
|
999
|
+
saveTarget: editor.saveTarget,
|
|
1000
|
+
...(editor.lastMessage ? { lastMessage: editor.lastMessage } : {}),
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
650
1003
|
createAdapter() {
|
|
651
1004
|
return {
|
|
652
1005
|
writeStdout: (text) => {
|
|
@@ -726,8 +1079,34 @@ export class InteractiveSessionController {
|
|
|
726
1079
|
`State: ${this.expandedFlowFolders.has(selectedItem.key) ? "expanded" : "collapsed"}`,
|
|
727
1080
|
].join("\n");
|
|
728
1081
|
}
|
|
729
|
-
const
|
|
1082
|
+
const flow = this.flowWithAutoFlowEditor(selectedItem.flow) ?? selectedItem.flow;
|
|
730
1083
|
const description = flow.description?.trim() || "No description available for this flow.";
|
|
1084
|
+
if (flow.autoFlow) {
|
|
1085
|
+
const autoFlow = this.autoFlowViewForFlow(flow.id);
|
|
1086
|
+
const diagnostics = autoFlow?.diagnostics ?? [];
|
|
1087
|
+
const slotLines = autoFlow?.slots.map((slot) => {
|
|
1088
|
+
const blockText = slot.blocks.length === 0
|
|
1089
|
+
? "empty"
|
|
1090
|
+
: slot.blocks.map((block) => `${block.title} [${block.status}]`).join(", ");
|
|
1091
|
+
return `- ${slot.title}: ${blockText}`;
|
|
1092
|
+
}) ?? [];
|
|
1093
|
+
const diagnosticLines = diagnostics.length > 0
|
|
1094
|
+
? ["", "Validation:", ...diagnostics.map((diagnostic) => `- ${diagnostic.message}`)]
|
|
1095
|
+
: [];
|
|
1096
|
+
const statusLine = autoFlow
|
|
1097
|
+
? `Config status: ${autoFlow.status.valid ? "valid" : "invalid"} (${autoFlow.status.sourceLabel})`
|
|
1098
|
+
: "Config status: unavailable";
|
|
1099
|
+
return renderMarkdownToTerminal(stripAnsi([
|
|
1100
|
+
description,
|
|
1101
|
+
"",
|
|
1102
|
+
statusLine,
|
|
1103
|
+
autoFlow?.status.lastMessage ? `Last action: ${autoFlow.status.lastMessage}` : "",
|
|
1104
|
+
"",
|
|
1105
|
+
"Slots:",
|
|
1106
|
+
...slotLines,
|
|
1107
|
+
...diagnosticLines,
|
|
1108
|
+
].filter((line) => line.length > 0).join("\n")));
|
|
1109
|
+
}
|
|
731
1110
|
const details = [
|
|
732
1111
|
`Path: ${flow.treePath.join("/")}`,
|
|
733
1112
|
`Source: ${flow.source === "project-local" ? "project-local" : flow.source === "global" ? "global" : "built-in"}`,
|
|
@@ -744,13 +1123,16 @@ export class InteractiveSessionController {
|
|
|
744
1123
|
const lines = [progressViewModel.flow.label, ""];
|
|
745
1124
|
for (const item of progressViewModel.items) {
|
|
746
1125
|
if (item.kind === "termination") {
|
|
747
|
-
const symbol = item.status === "done" ? "✓" : "■";
|
|
1126
|
+
const symbol = item.status === "done" || item.status === "success" ? "✓" : item.status === "failed" ? "×" : "■";
|
|
748
1127
|
lines.push(`${symbol} ${item.label}`);
|
|
749
1128
|
lines.push(item.detail);
|
|
750
1129
|
continue;
|
|
751
1130
|
}
|
|
752
1131
|
const indent = " ".repeat(item.depth);
|
|
753
1132
|
lines.push(`${indent}${this.symbolForStatus(progressViewModel.flow.id, item.status)} ${item.label}`);
|
|
1133
|
+
if ((item.kind === "slot" || item.kind === "block") && item.detail && item.status !== "pending" && item.status !== "success") {
|
|
1134
|
+
lines.push(`${indent} ${item.detail}`);
|
|
1135
|
+
}
|
|
754
1136
|
}
|
|
755
1137
|
return lines.join("\n").trimEnd();
|
|
756
1138
|
}
|
|
@@ -1064,6 +1446,20 @@ export class InteractiveSessionController {
|
|
|
1064
1446
|
if (!selectedItem || selectedItem.kind !== "flow") {
|
|
1065
1447
|
return;
|
|
1066
1448
|
}
|
|
1449
|
+
const autoFlow = this.autoFlowViewForFlow(selectedItem.flow.id);
|
|
1450
|
+
if (autoFlow && !autoFlow.status.canRun) {
|
|
1451
|
+
const message = autoFlow.diagnostics[0]?.message ?? "Auto-flow config has validation errors and cannot be run.";
|
|
1452
|
+
const editor = this.autoFlowEditors.get(selectedItem.flow.id);
|
|
1453
|
+
if (editor) {
|
|
1454
|
+
this.autoFlowEditors.set(selectedItem.flow.id, {
|
|
1455
|
+
...editor,
|
|
1456
|
+
lastMessage: message,
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
this.appendLog(message);
|
|
1460
|
+
this.emitChange();
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1067
1463
|
const confirmation = await this.options.getRunConfirmation(selectedItem.flow.id);
|
|
1068
1464
|
if (this.state.busy || this.confirmSession) {
|
|
1069
1465
|
return;
|
|
@@ -1249,13 +1645,22 @@ export class InteractiveSessionController {
|
|
|
1249
1645
|
return this.state.currentFlowId ?? this.state.selectedFlowId;
|
|
1250
1646
|
}
|
|
1251
1647
|
symbolForStatus(flowId, status) {
|
|
1252
|
-
if (status === "done") {
|
|
1648
|
+
if (status === "done" || status === "success") {
|
|
1253
1649
|
return "✓";
|
|
1254
1650
|
}
|
|
1651
|
+
if (status === "failed" || status === "invalid") {
|
|
1652
|
+
return "×";
|
|
1653
|
+
}
|
|
1654
|
+
if (status === "stopped" || status === "blocked") {
|
|
1655
|
+
return "■";
|
|
1656
|
+
}
|
|
1657
|
+
if (status === "disabled" || status === "empty") {
|
|
1658
|
+
return "·";
|
|
1659
|
+
}
|
|
1255
1660
|
if (status === "skipped") {
|
|
1256
1661
|
return "·";
|
|
1257
1662
|
}
|
|
1258
|
-
if (status === "running") {
|
|
1663
|
+
if (status === "running" || status === "waiting-user") {
|
|
1259
1664
|
if (this.state.failedFlowId === flowId && !this.state.busy) {
|
|
1260
1665
|
return "×";
|
|
1261
1666
|
}
|
|
@@ -1287,6 +1692,9 @@ export class InteractiveSessionController {
|
|
|
1287
1692
|
}
|
|
1288
1693
|
applyScrollOffset(panel, value, maxOffset) {
|
|
1289
1694
|
const next = clamp(value, 0, maxOffset);
|
|
1695
|
+
if (this.scrollOffsetFor(panel) === next) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1290
1698
|
if (panel === "progress") {
|
|
1291
1699
|
this.state.progressScrollOffset = next;
|
|
1292
1700
|
}
|