agentweaver 0.1.18 → 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 +54 -6
- 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 +454 -108
- package/dist/interactive/auto-flow.js +644 -0
- package/dist/interactive/controller.js +489 -7
- package/dist/interactive/progress.js +194 -1
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/web/index.js +237 -5
- package/dist/interactive/web/protocol.js +222 -1
- package/dist/interactive/web/server.js +497 -3
- package/dist/interactive/web/static/app.js +2462 -37
- package/dist/interactive/web/static/index.html +113 -11
- package/dist/interactive/web/static/styles.css +1 -1
- package/dist/interactive/web/static/styles.input.css +1383 -149
- 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 +403 -0
- 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);
|
|
@@ -522,17 +550,343 @@ export class InteractiveSessionController {
|
|
|
522
550
|
setScrollOffset(panel, offset) {
|
|
523
551
|
this.applyScrollOffset(panel, offset, this.panelMaxScroll(panel));
|
|
524
552
|
}
|
|
553
|
+
getCurrentFlowExecutionState() {
|
|
554
|
+
return this.state.flowState.executionState;
|
|
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
|
+
}
|
|
711
|
+
hasActiveInput() {
|
|
712
|
+
return this.confirmSession !== null || this.activeFormSession !== null;
|
|
713
|
+
}
|
|
714
|
+
setArtifactExplorerAvailability(input) {
|
|
715
|
+
const count = input.artifactCount;
|
|
716
|
+
const hasCount = typeof count === "number";
|
|
717
|
+
const failed = input.status === "failed";
|
|
718
|
+
const label = input.label
|
|
719
|
+
?? (failed
|
|
720
|
+
? hasCount && count > 0 ? "Run failed; artifacts available" : "Run failed"
|
|
721
|
+
: hasCount && count === 0 ? "Run completed; no artifacts found" : "Artifacts ready");
|
|
722
|
+
const message = input.message
|
|
723
|
+
?? (failed
|
|
724
|
+
? hasCount && count > 0
|
|
725
|
+
? "The workflow failed, but artifacts are available for review."
|
|
726
|
+
: "The workflow failed. The explorer can check for any artifacts written before failure."
|
|
727
|
+
: hasCount && count === 0
|
|
728
|
+
? "The workflow completed, but no artifacts were found for this scope yet."
|
|
729
|
+
: "The workflow completed and scope artifacts are available for review.");
|
|
730
|
+
this.state.artifactExplorer = {
|
|
731
|
+
available: true,
|
|
732
|
+
open: Boolean(input.open) && !this.hasActiveInput(),
|
|
733
|
+
scopeKey: input.scopeKey,
|
|
734
|
+
runId: input.runId ?? null,
|
|
735
|
+
...(input.runIds && input.runIds.length > 1 ? { runIds: input.runIds } : {}),
|
|
736
|
+
status: input.status,
|
|
737
|
+
label,
|
|
738
|
+
...(hasCount ? { artifactCount: count } : {}),
|
|
739
|
+
message,
|
|
740
|
+
};
|
|
741
|
+
this.emitChange();
|
|
742
|
+
}
|
|
743
|
+
setArtifactExplorerUnavailable(message = "Artifacts are available after a Web UI workflow run completes.") {
|
|
744
|
+
if (!this.state.artifactExplorer.available && !this.state.artifactExplorer.open) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
this.state.artifactExplorer = {
|
|
748
|
+
available: false,
|
|
749
|
+
open: false,
|
|
750
|
+
scopeKey: null,
|
|
751
|
+
runId: null,
|
|
752
|
+
status: "unavailable",
|
|
753
|
+
label: "Artifact Explorer",
|
|
754
|
+
message,
|
|
755
|
+
};
|
|
756
|
+
this.emitChange();
|
|
757
|
+
}
|
|
758
|
+
closeArtifactExplorer() {
|
|
759
|
+
if (!this.state.artifactExplorer.open) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
this.state.artifactExplorer = {
|
|
763
|
+
...this.state.artifactExplorer,
|
|
764
|
+
open: false,
|
|
765
|
+
};
|
|
766
|
+
this.emitChange();
|
|
767
|
+
}
|
|
768
|
+
openArtifactExplorer() {
|
|
769
|
+
if (!this.state.artifactExplorer.available || this.hasActiveInput()) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (this.state.artifactExplorer.open) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
this.state.artifactExplorer = {
|
|
776
|
+
...this.state.artifactExplorer,
|
|
777
|
+
open: true,
|
|
778
|
+
};
|
|
779
|
+
this.emitChange();
|
|
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
|
+
}
|
|
525
876
|
getViewModel(layout) {
|
|
526
877
|
const selectedItem = this.selectedFlowTreeItem();
|
|
527
878
|
const activeFlowId = this.activeFlowId();
|
|
528
|
-
const selectedFlow = selectedItem?.kind === "flow" ? selectedItem.flow : null;
|
|
529
|
-
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);
|
|
530
881
|
const progressState = progressFlow && this.state.flowState.flowId === progressFlow.id
|
|
531
882
|
? this.state.flowState.executionState
|
|
532
883
|
: progressFlow && this.state.currentFlowId === progressFlow.id
|
|
533
884
|
? this.state.flowState.executionState
|
|
534
885
|
: null;
|
|
535
|
-
const progressViewModel = buildProgressViewModel(progressFlow, progressState
|
|
886
|
+
const progressViewModel = buildProgressViewModel(progressFlow, progressState, {
|
|
887
|
+
failedFlowId: this.state.failedFlowId,
|
|
888
|
+
waitingForUserInput: this.activeFormSession !== null,
|
|
889
|
+
});
|
|
536
890
|
const helpText = `${HELP_TEXT}\n\nAvailable flows:\n${this.options.flows.map((flow) => `- ${flow.treePath.join("/")}`).join("\n")}`;
|
|
537
891
|
return {
|
|
538
892
|
header: this.buildHeaderText(),
|
|
@@ -552,6 +906,8 @@ export class InteractiveSessionController {
|
|
|
552
906
|
})),
|
|
553
907
|
selectedFlowIndex: Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey)),
|
|
554
908
|
progressTitle: this.panelTitle("Current Flow", "progress"),
|
|
909
|
+
progress: progressViewModel,
|
|
910
|
+
autoFlow: selectedFlow?.autoFlow ? this.autoFlowViewForFlow(selectedFlow.id) : null,
|
|
555
911
|
progressText: this.renderProgress(progressViewModel),
|
|
556
912
|
progressScrollOffset: this.state.progressScrollOffset,
|
|
557
913
|
descriptionText: this.renderDescription(selectedItem),
|
|
@@ -566,13 +922,84 @@ export class InteractiveSessionController {
|
|
|
566
922
|
confirmText: this.renderConfirmText(),
|
|
567
923
|
confirmation: this.renderConfirmationView(),
|
|
568
924
|
form: this.renderFormView(layout),
|
|
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
|
+
},
|
|
569
935
|
};
|
|
570
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
|
+
}
|
|
571
960
|
emitChange(event = { type: "render" }) {
|
|
572
961
|
for (const listener of this.listeners) {
|
|
573
962
|
listener(event);
|
|
574
963
|
}
|
|
575
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
|
+
}
|
|
576
1003
|
createAdapter() {
|
|
577
1004
|
return {
|
|
578
1005
|
writeStdout: (text) => {
|
|
@@ -652,8 +1079,34 @@ export class InteractiveSessionController {
|
|
|
652
1079
|
`State: ${this.expandedFlowFolders.has(selectedItem.key) ? "expanded" : "collapsed"}`,
|
|
653
1080
|
].join("\n");
|
|
654
1081
|
}
|
|
655
|
-
const
|
|
1082
|
+
const flow = this.flowWithAutoFlowEditor(selectedItem.flow) ?? selectedItem.flow;
|
|
656
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
|
+
}
|
|
657
1110
|
const details = [
|
|
658
1111
|
`Path: ${flow.treePath.join("/")}`,
|
|
659
1112
|
`Source: ${flow.source === "project-local" ? "project-local" : flow.source === "global" ? "global" : "built-in"}`,
|
|
@@ -670,13 +1123,16 @@ export class InteractiveSessionController {
|
|
|
670
1123
|
const lines = [progressViewModel.flow.label, ""];
|
|
671
1124
|
for (const item of progressViewModel.items) {
|
|
672
1125
|
if (item.kind === "termination") {
|
|
673
|
-
const symbol = item.status === "done" ? "✓" : "■";
|
|
1126
|
+
const symbol = item.status === "done" || item.status === "success" ? "✓" : item.status === "failed" ? "×" : "■";
|
|
674
1127
|
lines.push(`${symbol} ${item.label}`);
|
|
675
1128
|
lines.push(item.detail);
|
|
676
1129
|
continue;
|
|
677
1130
|
}
|
|
678
1131
|
const indent = " ".repeat(item.depth);
|
|
679
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
|
+
}
|
|
680
1136
|
}
|
|
681
1137
|
return lines.join("\n").trimEnd();
|
|
682
1138
|
}
|
|
@@ -990,6 +1446,20 @@ export class InteractiveSessionController {
|
|
|
990
1446
|
if (!selectedItem || selectedItem.kind !== "flow") {
|
|
991
1447
|
return;
|
|
992
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
|
+
}
|
|
993
1463
|
const confirmation = await this.options.getRunConfirmation(selectedItem.flow.id);
|
|
994
1464
|
if (this.state.busy || this.confirmSession) {
|
|
995
1465
|
return;
|
|
@@ -1175,13 +1645,22 @@ export class InteractiveSessionController {
|
|
|
1175
1645
|
return this.state.currentFlowId ?? this.state.selectedFlowId;
|
|
1176
1646
|
}
|
|
1177
1647
|
symbolForStatus(flowId, status) {
|
|
1178
|
-
if (status === "done") {
|
|
1648
|
+
if (status === "done" || status === "success") {
|
|
1179
1649
|
return "✓";
|
|
1180
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
|
+
}
|
|
1181
1660
|
if (status === "skipped") {
|
|
1182
1661
|
return "·";
|
|
1183
1662
|
}
|
|
1184
|
-
if (status === "running") {
|
|
1663
|
+
if (status === "running" || status === "waiting-user") {
|
|
1185
1664
|
if (this.state.failedFlowId === flowId && !this.state.busy) {
|
|
1186
1665
|
return "×";
|
|
1187
1666
|
}
|
|
@@ -1213,6 +1692,9 @@ export class InteractiveSessionController {
|
|
|
1213
1692
|
}
|
|
1214
1693
|
applyScrollOffset(panel, value, maxOffset) {
|
|
1215
1694
|
const next = clamp(value, 0, maxOffset);
|
|
1695
|
+
if (this.scrollOffsetFor(panel) === next) {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1216
1698
|
if (panel === "progress") {
|
|
1217
1699
|
this.state.progressScrollOffset = next;
|
|
1218
1700
|
}
|