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.
Files changed (50) hide show
  1. package/README.md +47 -7
  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 +450 -108
  11. package/dist/interactive/auto-flow.js +644 -0
  12. package/dist/interactive/controller.js +417 -9
  13. package/dist/interactive/progress.js +194 -1
  14. package/dist/interactive/state.js +25 -0
  15. package/dist/interactive/web/index.js +97 -12
  16. package/dist/interactive/web/protocol.js +216 -1
  17. package/dist/interactive/web/server.js +72 -14
  18. package/dist/interactive/web/static/app.js +1603 -49
  19. package/dist/interactive/web/static/index.html +76 -11
  20. package/dist/interactive/web/static/styles.css +1 -1
  21. package/dist/interactive/web/static/styles.input.css +901 -47
  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 +29 -5
  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);
@@ -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 run yet."
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 { flow } = selectedItem;
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
  }