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.
Files changed (50) hide show
  1. package/README.md +54 -6
  2. package/dist/artifacts.js +9 -0
  3. package/dist/executors/git-commit-executor.js +24 -6
  4. package/dist/flow-state.js +3 -8
  5. package/dist/git/git-diff-parser.js +223 -0
  6. package/dist/git/git-service.js +562 -0
  7. package/dist/git/git-stage-selection.js +24 -0
  8. package/dist/git/git-status-parser.js +171 -0
  9. package/dist/git/git-types.js +1 -0
  10. package/dist/index.js +454 -108
  11. package/dist/interactive/auto-flow.js +644 -0
  12. package/dist/interactive/controller.js +489 -7
  13. package/dist/interactive/progress.js +194 -1
  14. package/dist/interactive/state.js +34 -0
  15. package/dist/interactive/web/index.js +237 -5
  16. package/dist/interactive/web/protocol.js +222 -1
  17. package/dist/interactive/web/server.js +497 -3
  18. package/dist/interactive/web/static/app.js +2462 -37
  19. package/dist/interactive/web/static/index.html +113 -11
  20. package/dist/interactive/web/static/styles.css +1 -1
  21. package/dist/interactive/web/static/styles.input.css +1383 -149
  22. package/dist/pipeline/auto-flow-blocks.js +307 -0
  23. package/dist/pipeline/auto-flow-config.js +273 -0
  24. package/dist/pipeline/auto-flow-identity.js +49 -0
  25. package/dist/pipeline/auto-flow-presets.js +52 -0
  26. package/dist/pipeline/auto-flow-resolver.js +830 -0
  27. package/dist/pipeline/auto-flow-types.js +17 -0
  28. package/dist/pipeline/context.js +1 -0
  29. package/dist/pipeline/declarative-flows.js +27 -1
  30. package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
  31. package/dist/pipeline/flow-specs/auto-golang.json +12 -1
  32. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
  35. package/dist/pipeline/flow-specs/review/review-project.json +19 -1
  36. package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
  37. package/dist/pipeline/node-registry.js +9 -0
  38. package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
  39. package/dist/pipeline/nodes/flow-run-node.js +5 -3
  40. package/dist/pipeline/nodes/git-status-node.js +2 -168
  41. package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
  42. package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
  43. package/dist/pipeline/nodes/plan-codex-node.js +8 -1
  44. package/dist/pipeline/spec-loader.js +14 -4
  45. package/dist/runtime/artifact-catalog.js +403 -0
  46. package/dist/runtime/settings.js +114 -0
  47. package/dist/scope.js +14 -4
  48. package/package.json +1 -1
  49. package/dist/pipeline/flow-specs/auto-common.json +0 -179
  50. 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 { flow } = selectedItem;
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
  }