agentweaver 0.1.15 → 0.1.16

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 (94) hide show
  1. package/README.md +26 -9
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +15 -0
  4. package/dist/doctor/checks/env-diagnostics.js +25 -0
  5. package/dist/doctor/checks/flow-readiness.js +15 -18
  6. package/dist/flow-state.js +75 -15
  7. package/dist/index.js +391 -175
  8. package/dist/interactive/blessed-session.js +361 -0
  9. package/dist/interactive/controller.js +1293 -0
  10. package/dist/interactive/create-interactive-session.js +5 -0
  11. package/dist/interactive/ink/index.js +576 -0
  12. package/dist/interactive/progress.js +245 -0
  13. package/dist/interactive/selectors.js +14 -0
  14. package/dist/interactive/session.js +1 -0
  15. package/dist/interactive/state.js +34 -0
  16. package/dist/interactive/tree.js +155 -0
  17. package/dist/interactive/types.js +1 -0
  18. package/dist/interactive/view-model.js +1 -0
  19. package/dist/interactive-ui.js +159 -194
  20. package/dist/pipeline/context.js +1 -0
  21. package/dist/pipeline/declarative-flow-runner.js +212 -6
  22. package/dist/pipeline/declarative-flows.js +27 -0
  23. package/dist/pipeline/execution-routing-config.js +15 -0
  24. package/dist/pipeline/flow-catalog.js +19 -3
  25. package/dist/pipeline/flow-run-resume.js +29 -0
  26. package/dist/pipeline/flow-specs/auto-common.json +89 -360
  27. package/dist/pipeline/flow-specs/auto-golang.json +58 -363
  28. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  29. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  30. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  31. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
  32. package/dist/pipeline/flow-specs/design-review.json +10 -0
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  35. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  36. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  37. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  38. package/dist/pipeline/flow-specs/implement.json +13 -6
  39. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  40. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  41. package/dist/pipeline/flow-specs/plan-revise.json +7 -1
  42. package/dist/pipeline/flow-specs/plan.json +48 -70
  43. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  44. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  45. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  46. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  47. package/dist/pipeline/flow-specs/review/review.json +37 -31
  48. package/dist/pipeline/flow-specs/task-describe.json +2 -0
  49. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  50. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  51. package/dist/pipeline/node-registry.js +41 -1
  52. package/dist/pipeline/node-runner.js +3 -2
  53. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  54. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  55. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  56. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  57. package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
  58. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  59. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  60. package/dist/pipeline/nodes/flow-run-node.js +226 -7
  61. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  62. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  63. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  64. package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
  65. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  66. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  67. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  68. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  69. package/dist/pipeline/nodes/user-input-node.js +38 -3
  70. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  71. package/dist/pipeline/prompt-registry.js +3 -1
  72. package/dist/pipeline/prompt-runtime.js +4 -1
  73. package/dist/pipeline/review-iteration.js +26 -0
  74. package/dist/pipeline/spec-compiler.js +2 -0
  75. package/dist/pipeline/spec-types.js +3 -0
  76. package/dist/pipeline/spec-validator.js +14 -0
  77. package/dist/pipeline/value-resolver.js +74 -1
  78. package/dist/prompts.js +36 -14
  79. package/dist/review-severity.js +45 -0
  80. package/dist/runtime/artifact-registry.js +402 -0
  81. package/dist/runtime/design-review-input-contract.js +17 -16
  82. package/dist/runtime/env-loader.js +3 -0
  83. package/dist/runtime/execution-routing-store.js +134 -0
  84. package/dist/runtime/execution-routing.js +227 -0
  85. package/dist/runtime/interactive-execution-routing.js +462 -0
  86. package/dist/runtime/plan-revise-input-contract.js +35 -32
  87. package/dist/runtime/planning-bundle.js +123 -0
  88. package/dist/runtime/ready-to-merge.js +22 -1
  89. package/dist/runtime/review-input-contract.js +100 -0
  90. package/dist/structured-artifact-schema-registry.js +9 -0
  91. package/dist/structured-artifact-schemas.json +140 -1
  92. package/dist/structured-artifacts.js +77 -6
  93. package/dist/user-input.js +70 -3
  94. package/package.json +6 -3
@@ -0,0 +1,1293 @@
1
+ import path from "node:path";
2
+ import { FlowInterruptedError, TaskRunnerError } from "../errors.js";
3
+ import { renderMarkdownToTerminal } from "../markdown.js";
4
+ import { setOutputAdapter, stripAnsi } from "../tui.js";
5
+ import { buildInitialUserInputValues, normalizeUserInputFieldValue, resolveFieldDefinition, validateUserInputValues, } from "../user-input.js";
6
+ import { buildProgressViewModel } from "./progress.js";
7
+ import { selectHeaderLabel } from "./selectors.js";
8
+ import { createInitialInteractiveState } from "./state.js";
9
+ import { buildFlowTree, collectInitiallyExpandedFolderKeys, computeVisibleFlowItems, makeFlowKey, makeFolderKey } from "./tree.js";
10
+ const HELP_TEXT = renderMarkdownToTerminal([
11
+ "AgentWeaver interactive mode",
12
+ "",
13
+ "Keys:",
14
+ "Up / Down select folder or flow",
15
+ "Right expand folder",
16
+ "Left collapse folder or go to parent",
17
+ "Enter expand folder or launch flow",
18
+ "Enter confirm launch in modal",
19
+ "Esc close help/modal or interrupt running flow",
20
+ "F1 open or close help",
21
+ "Tab switch pane",
22
+ "Ctrl+L clear log",
23
+ "q / Ctrl+C exit",
24
+ ].join("\n"));
25
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
26
+ function clamp(value, min, max) {
27
+ return Math.min(max, Math.max(min, value));
28
+ }
29
+ function isPrintableCharacter(ch, key) {
30
+ return Boolean(ch) && !key.ctrl && !key.meta && !/^[\x00-\x1f\x7f]$/.test(ch);
31
+ }
32
+ function isReverseTabKey(key) {
33
+ return key.name === "backtab" || (key.name === "tab" && key.shift === true);
34
+ }
35
+ function textIndexToLineColumn(value, index) {
36
+ const boundedIndex = clamp(index, 0, value.length);
37
+ const beforeCursor = value.slice(0, boundedIndex);
38
+ const lines = beforeCursor.split("\n");
39
+ return {
40
+ line: Math.max(0, lines.length - 1),
41
+ column: (lines[lines.length - 1] ?? "").length,
42
+ };
43
+ }
44
+ function textLineColumnToIndex(value, line, column) {
45
+ const lines = value.split("\n");
46
+ const boundedLine = clamp(line, 0, Math.max(0, lines.length - 1));
47
+ let index = 0;
48
+ for (let currentLine = 0; currentLine < boundedLine; currentLine += 1) {
49
+ index += (lines[currentLine] ?? "").length + 1;
50
+ }
51
+ const targetLine = lines[boundedLine] ?? "";
52
+ return index + clamp(column, 0, targetLine.length);
53
+ }
54
+ function insertCursor(value, index) {
55
+ const boundedIndex = clamp(index, 0, value.length);
56
+ return `${value.slice(0, boundedIndex)}│${value.slice(boundedIndex)}`;
57
+ }
58
+ function wrapTextLine(line, width) {
59
+ if (line.length === 0) {
60
+ return [""];
61
+ }
62
+ const chunks = [];
63
+ for (let index = 0; index < line.length; index += width) {
64
+ chunks.push(line.slice(index, index + width));
65
+ }
66
+ return chunks;
67
+ }
68
+ function buildTextInputBox(value, cursorIndex, requestedWidth) {
69
+ const rendered = insertCursor(value, cursorIndex);
70
+ const naturalWidth = rendered.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
71
+ const width = requestedWidth !== undefined
72
+ ? Math.max(8, requestedWidth)
73
+ : clamp(naturalWidth, 18, 52);
74
+ const renderedLines = rendered
75
+ .split("\n")
76
+ .flatMap((line) => wrapTextLine(line, width));
77
+ return [
78
+ `┌${"─".repeat(width + 2)}┐`,
79
+ ...renderedLines.map((line) => `│ ${line.padEnd(width, " ")} │`),
80
+ `└${"─".repeat(width + 2)}┘`,
81
+ ];
82
+ }
83
+ function normalizeLogText(text) {
84
+ const normalized = text
85
+ .split("\n")
86
+ .map((line) => line.replace(/\t/g, " "))
87
+ .join("\n")
88
+ .trimEnd();
89
+ if (!normalized) {
90
+ return [""];
91
+ }
92
+ return normalized.split("\n");
93
+ }
94
+ export class InteractiveSessionController {
95
+ options;
96
+ listeners = new Set();
97
+ flowMap;
98
+ flowTree;
99
+ expandedFlowFolders = new Set();
100
+ visibleFlowItems;
101
+ state;
102
+ logLines = [];
103
+ pendingLogLines = [];
104
+ logText = "";
105
+ logFlushTimer = null;
106
+ helpVisible = false;
107
+ confirmSession = null;
108
+ activeFormSession = null;
109
+ spinnerTimer = null;
110
+ mounted = false;
111
+ constructor(options) {
112
+ this.options = options;
113
+ if (options.flows.length === 0) {
114
+ throw new Error("Interactive UI requires at least one flow.");
115
+ }
116
+ this.state = createInitialInteractiveState(options);
117
+ this.flowMap = new Map(options.flows.map((flow) => [flow.id, flow]));
118
+ this.flowTree = buildFlowTree(options.flows);
119
+ collectInitiallyExpandedFolderKeys(this.flowTree).forEach((key) => this.expandedFlowFolders.add(key));
120
+ this.visibleFlowItems = computeVisibleFlowItems(this.flowTree, this.expandedFlowFolders);
121
+ }
122
+ subscribe(listener) {
123
+ this.listeners.add(listener);
124
+ return () => {
125
+ this.listeners.delete(listener);
126
+ };
127
+ }
128
+ mount() {
129
+ if (this.mounted) {
130
+ return;
131
+ }
132
+ this.mounted = true;
133
+ setOutputAdapter(this.createAdapter());
134
+ this.focusPane("flows");
135
+ this.emitChange();
136
+ }
137
+ destroy() {
138
+ if (this.logFlushTimer) {
139
+ clearTimeout(this.logFlushTimer);
140
+ this.logFlushTimer = null;
141
+ }
142
+ if (this.spinnerTimer) {
143
+ clearInterval(this.spinnerTimer);
144
+ this.spinnerTimer = null;
145
+ }
146
+ if (this.activeFormSession) {
147
+ this.activeFormSession.reject(new TaskRunnerError(`User cancelled form '${this.activeFormSession.form.formId}'.`));
148
+ this.activeFormSession = null;
149
+ }
150
+ this.confirmSession = null;
151
+ this.helpVisible = false;
152
+ this.mounted = false;
153
+ setOutputAdapter(null);
154
+ this.emitChange();
155
+ }
156
+ requestUserInput(form) {
157
+ if (this.activeFormSession) {
158
+ return Promise.reject(new TaskRunnerError("Another user input form is already active."));
159
+ }
160
+ if (form.fields.length === 0) {
161
+ return Promise.resolve({
162
+ formId: form.formId,
163
+ submittedAt: new Date().toISOString(),
164
+ values: {},
165
+ });
166
+ }
167
+ return new Promise((resolve, reject) => {
168
+ const values = buildInitialUserInputValues(form.fields);
169
+ const firstField = form.fields[0];
170
+ const initialCursorIndex = firstField?.type === "text" ? String(values[firstField.id] ?? "").length : 0;
171
+ const initialOptionIndex = firstField?.type === "single-select"
172
+ ? Math.max(0, firstField.options.findIndex((option) => option.value === String(values[firstField.id] ?? "")))
173
+ : firstField?.type === "multi-select"
174
+ ? Math.max(0, firstField.options.findIndex((option) => Array.isArray(values[firstField.id]) && values[firstField.id].includes(option.value)))
175
+ : 0;
176
+ this.activeFormSession = {
177
+ form,
178
+ values,
179
+ currentFieldIndex: 0,
180
+ currentOptionIndex: initialOptionIndex,
181
+ currentTextCursorIndex: initialCursorIndex,
182
+ previewScrollOffset: 0,
183
+ resolve,
184
+ reject,
185
+ };
186
+ this.confirmSession = null;
187
+ this.helpVisible = false;
188
+ this.emitChange();
189
+ });
190
+ }
191
+ setSummary(markdown) {
192
+ this.state.summaryText = markdown.trim();
193
+ this.state.summaryVisible = this.state.summaryText.length > 0;
194
+ if (!this.state.summaryVisible && this.state.focusedPane === "summary") {
195
+ this.state.focusedPane = "log";
196
+ }
197
+ this.emitChange();
198
+ }
199
+ clearSummary() {
200
+ this.state.summaryText = "";
201
+ this.state.summaryVisible = false;
202
+ if (this.state.focusedPane === "summary") {
203
+ this.state.focusedPane = "log";
204
+ }
205
+ this.emitChange();
206
+ }
207
+ setScope(scopeKey, jiraIssueKey) {
208
+ this.state.scopeKey = scopeKey;
209
+ this.state.jiraIssueKey = jiraIssueKey ?? null;
210
+ this.emitChange();
211
+ }
212
+ appendLog(text) {
213
+ const lines = normalizeLogText(text);
214
+ this.logLines.push(...lines);
215
+ this.pendingLogLines.push(...lines);
216
+ this.logText = this.logText.length > 0 ? `${this.logText}\n${lines.join("\n")}` : lines.join("\n");
217
+ this.state.logScrollOffset = Math.max(0, this.logLines.length - 1);
218
+ this.scheduleLogFlush();
219
+ }
220
+ clearLog() {
221
+ this.logLines.length = 0;
222
+ this.pendingLogLines.length = 0;
223
+ this.logText = "";
224
+ if (this.logFlushTimer) {
225
+ clearTimeout(this.logFlushTimer);
226
+ this.logFlushTimer = null;
227
+ }
228
+ this.state.logScrollOffset = 0;
229
+ this.emitChange({ type: "render", syncLog: true });
230
+ this.appendLog("Log cleared.");
231
+ }
232
+ setFlowFailed(flowId) {
233
+ this.state.failedFlowId = flowId;
234
+ this.emitChange();
235
+ }
236
+ interruptActiveForm(message = "Flow interrupted by user.") {
237
+ if (!this.activeFormSession) {
238
+ return;
239
+ }
240
+ const session = this.activeFormSession;
241
+ this.activeFormSession = null;
242
+ session.reject(new FlowInterruptedError(message));
243
+ this.focusPane("flows");
244
+ this.emitChange();
245
+ }
246
+ async handleKeypress(ch, key) {
247
+ if (this.activeFormSession) {
248
+ this.handleActiveFormKey(ch, key);
249
+ return;
250
+ }
251
+ if (this.confirmSession) {
252
+ await this.handleConfirmKey(key);
253
+ return;
254
+ }
255
+ if (this.helpVisible) {
256
+ this.handleHelpKey(key);
257
+ return;
258
+ }
259
+ if (key.ctrl && key.name === "c") {
260
+ this.openExitConfirm();
261
+ return;
262
+ }
263
+ if (key.ctrl && key.name === "l") {
264
+ this.clearLog();
265
+ return;
266
+ }
267
+ if (key.name === "q") {
268
+ this.openExitConfirm();
269
+ return;
270
+ }
271
+ if (key.name === "f1" || key.name === "h" || key.name === "?") {
272
+ this.helpVisible = true;
273
+ this.emitChange();
274
+ return;
275
+ }
276
+ if (key.name === "escape") {
277
+ if (this.state.busy) {
278
+ this.openInterruptConfirm();
279
+ }
280
+ return;
281
+ }
282
+ if (isReverseTabKey(key)) {
283
+ this.cycleFocus(-1);
284
+ return;
285
+ }
286
+ if (key.name === "tab") {
287
+ this.cycleFocus(1);
288
+ return;
289
+ }
290
+ if (this.state.focusedPane === "flows") {
291
+ await this.handleFlowKey(key);
292
+ return;
293
+ }
294
+ if (this.state.focusedPane === "progress") {
295
+ this.handleScrollKey("progress", key);
296
+ return;
297
+ }
298
+ if (this.state.focusedPane === "summary") {
299
+ this.handleScrollKey("summary", key);
300
+ return;
301
+ }
302
+ this.handleScrollKey("log", key);
303
+ }
304
+ selectFlowIndex(index) {
305
+ const selectedItem = this.visibleFlowItems[index];
306
+ if (!selectedItem) {
307
+ return;
308
+ }
309
+ this.state.selectedFlowItemKey = selectedItem.key;
310
+ if (selectedItem.kind === "flow") {
311
+ this.state.selectedFlowId = selectedItem.flow.id;
312
+ }
313
+ this.emitChange();
314
+ }
315
+ getViewModel(layout) {
316
+ const selectedItem = this.selectedFlowTreeItem();
317
+ const activeFlowId = this.activeFlowId();
318
+ const selectedFlow = selectedItem?.kind === "flow" ? selectedItem.flow : null;
319
+ const progressFlow = this.state.busy ? this.flowMap.get(activeFlowId) ?? null : selectedFlow;
320
+ const progressState = progressFlow && this.state.flowState.flowId === progressFlow.id
321
+ ? this.state.flowState.executionState
322
+ : progressFlow && this.state.currentFlowId === progressFlow.id
323
+ ? this.state.flowState.executionState
324
+ : null;
325
+ const progressViewModel = buildProgressViewModel(progressFlow, progressState);
326
+ const helpText = `${HELP_TEXT}\n\nAvailable flows:\n${this.options.flows.map((flow) => `- ${flow.treePath.join("/")}`).join("\n")}`;
327
+ return {
328
+ header: this.buildHeaderText(),
329
+ title: `AgentWeaver ${this.state.scopeKey}`,
330
+ footer: this.buildFooterText(),
331
+ helpVisible: this.helpVisible,
332
+ helpText,
333
+ helpScrollOffset: this.state.helpScrollOffset,
334
+ flowListTitle: this.panelTitle("Flows", "flows"),
335
+ flowItems: this.visibleFlowItems.map((item) => ({
336
+ key: item.key,
337
+ label: this.renderFlowTreeLabel(item),
338
+ })),
339
+ selectedFlowIndex: Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey)),
340
+ progressTitle: this.panelTitle("Current Flow", "progress"),
341
+ progressText: this.renderProgress(progressViewModel),
342
+ progressScrollOffset: this.state.progressScrollOffset,
343
+ descriptionText: this.renderDescription(selectedItem),
344
+ statusText: this.renderStatusText(),
345
+ summaryVisible: this.state.summaryVisible,
346
+ summaryTitle: this.panelTitle("Task Summary", "summary"),
347
+ summaryText: renderMarkdownToTerminal(stripAnsi(this.state.summaryText || "Task summary is not available yet.")),
348
+ summaryScrollOffset: this.state.summaryScrollOffset,
349
+ logTitle: this.panelTitle("Activity", "log"),
350
+ logText: this.logText,
351
+ logScrollOffset: this.state.logScrollOffset,
352
+ confirmText: this.renderConfirmText(),
353
+ form: this.renderFormView(layout),
354
+ };
355
+ }
356
+ emitChange(event = { type: "render" }) {
357
+ for (const listener of this.listeners) {
358
+ listener(event);
359
+ }
360
+ }
361
+ createAdapter() {
362
+ return {
363
+ writeStdout: (text) => {
364
+ this.appendLog(stripAnsi(text).replace(/\r/g, ""));
365
+ },
366
+ writeStderr: (text) => {
367
+ this.appendLog(stripAnsi(text).replace(/\r/g, ""));
368
+ },
369
+ supportsTransientStatus: false,
370
+ supportsPassthrough: false,
371
+ renderAuxiliaryOutput: true,
372
+ renderPanelsAsPlainText: true,
373
+ setExecutionState: (next) => {
374
+ this.state.currentNode = next.node;
375
+ this.state.currentExecutor = next.executor;
376
+ this.ensureSpinnerState();
377
+ this.emitChange();
378
+ },
379
+ setFlowState: (next) => {
380
+ this.state.flowState = {
381
+ flowId: next.flowId,
382
+ executionState: next.executionState,
383
+ };
384
+ if (next.flowId) {
385
+ this.state.currentFlowId = next.flowId;
386
+ }
387
+ this.ensureSpinnerState();
388
+ this.emitChange();
389
+ },
390
+ };
391
+ }
392
+ buildHeaderText() {
393
+ const current = this.state.currentFlowId ?? selectHeaderLabel(this.selectedFlowTreeItem(), this.state.selectedFlowId);
394
+ const pathParts = this.options.cwd.split(path.sep).filter(Boolean);
395
+ const folderName = pathParts.slice(-3).join("/") || this.options.cwd;
396
+ const branchLabel = this.options.gitBranchName ? this.options.gitBranchName : "detached-head";
397
+ const runningSuffix = this.state.busy ? " [running]" : "";
398
+ const versionLabel = this.state.version ? ` | Version ${this.state.version}` : "";
399
+ const jiraLabel = this.state.jiraIssueKey ? ` | Jira ${this.state.jiraIssueKey}` : "";
400
+ return `AgentWeaver | Scope ${this.state.scopeKey}${versionLabel}${jiraLabel} | Flow ${current}${runningSuffix} | Location ${folderName} • ${branchLabel}`;
401
+ }
402
+ buildFooterText() {
403
+ if (this.activeFormSession) {
404
+ const formView = this.renderFormView();
405
+ return formView?.footer ?? "Form active";
406
+ }
407
+ if (this.confirmSession) {
408
+ return "Confirm: Left/Right or Tab choose | Enter confirm | Esc cancel";
409
+ }
410
+ if (this.helpVisible) {
411
+ return "Help: Esc close | Up/Down/PageUp/PageDown scroll";
412
+ }
413
+ return `Focus: ${this.state.focusedPane} | Up/Down select or scroll | Left/Right fold | Enter run | h help | Esc interrupt | Tab switch | q exit`;
414
+ }
415
+ panelTitle(title, pane) {
416
+ return this.state.focusedPane === pane ? `▶ ${title}` : title;
417
+ }
418
+ renderFlowTreeLabel(item) {
419
+ const indent = " ".repeat(item.depth);
420
+ if (item.kind === "folder") {
421
+ const expanded = this.expandedFlowFolders.has(item.key);
422
+ return `${indent}${expanded ? "▾" : "▸"} ${item.name}`;
423
+ }
424
+ return `${indent}• ${item.name}`;
425
+ }
426
+ renderDescription(selectedItem) {
427
+ if (!selectedItem) {
428
+ return "Flow structure is not available.";
429
+ }
430
+ if (selectedItem.kind === "folder") {
431
+ const kindLabel = selectedItem.pathSegments[0] === "custom" ? "project-local" : "built-in";
432
+ return [
433
+ `Flow folder '${selectedItem.pathSegments.join("/")}'.`,
434
+ "",
435
+ `Source: ${kindLabel}`,
436
+ `State: ${this.expandedFlowFolders.has(selectedItem.key) ? "expanded" : "collapsed"}`,
437
+ ].join("\n");
438
+ }
439
+ const { flow } = selectedItem;
440
+ const description = flow.description?.trim() || "No description available for this flow.";
441
+ const details = [
442
+ `Path: ${flow.treePath.join("/")}`,
443
+ `Source: ${flow.source === "project-local" ? "project-local" : "built-in"}`,
444
+ flow.source === "project-local" && flow.sourcePath ? `File: ${flow.sourcePath}` : "",
445
+ ]
446
+ .filter((line) => line.length > 0)
447
+ .join("\n");
448
+ return renderMarkdownToTerminal(stripAnsi(details ? `${description}\n\n${details}` : description));
449
+ }
450
+ renderProgress(progressViewModel) {
451
+ if (!progressViewModel.flow) {
452
+ return "Select a flow in the tree to see its progress.";
453
+ }
454
+ const lines = [progressViewModel.flow.label, ""];
455
+ for (const item of progressViewModel.items) {
456
+ if (item.kind === "termination") {
457
+ const symbol = item.status === "done" ? "✓" : "■";
458
+ lines.push(`${symbol} ${item.label}`);
459
+ lines.push(item.detail);
460
+ continue;
461
+ }
462
+ const indent = " ".repeat(item.depth);
463
+ lines.push(`${indent}${this.symbolForStatus(progressViewModel.flow.id, item.status)} ${item.label}`);
464
+ }
465
+ return lines.join("\n").trimEnd();
466
+ }
467
+ renderStatusText() {
468
+ const running = this.state.busy || this.state.currentNode !== null || this.state.currentExecutor !== null;
469
+ const spinner = running ? SPINNER_FRAMES[this.state.spinnerFrame] ?? "•" : "•";
470
+ const stateText = running ? `${spinner} running` : "idle";
471
+ return [
472
+ `State: ${stateText}`,
473
+ `Time: ${this.formatElapsed(running ? Date.now() : null)}`,
474
+ `Node: ${this.state.currentNode ?? "-"}`,
475
+ `Executor: ${this.state.currentExecutor ?? "-"}`,
476
+ ].join("\n");
477
+ }
478
+ renderConfirmText() {
479
+ const session = this.confirmSession;
480
+ if (!session) {
481
+ return null;
482
+ }
483
+ const flow = session.flowId ? this.flowMap.get(session.flowId) : null;
484
+ const actions = this.confirmActions();
485
+ const actionLabels = actions
486
+ .map((action) => {
487
+ const label = action === "stop"
488
+ ? "Stop"
489
+ : action === "resume"
490
+ ? "Resume"
491
+ : action === "restart"
492
+ ? "Restart"
493
+ : action === "ok"
494
+ ? "OK"
495
+ : "Cancel";
496
+ return session.selectedAction === action ? `[ ${label} ]` : ` ${label} `;
497
+ })
498
+ .join(" ");
499
+ const lines = [session.kind === "interrupt"
500
+ ? `Interrupt flow "${flow?.label ?? session.flowId ?? "-"}"?`
501
+ : session.kind === "exit"
502
+ ? "Exit AgentWeaver?"
503
+ : `Run flow "${flow?.label ?? session.flowId ?? "-"}"?`];
504
+ if (session.details?.trim()) {
505
+ lines.push("", session.details.trim());
506
+ }
507
+ lines.push("", actionLabels, "", "Left/Right or Tab: choose Enter: confirm Esc: cancel");
508
+ return lines.join("\n");
509
+ }
510
+ renderFormView(layout) {
511
+ const session = this.activeFormSession;
512
+ const field = this.currentFormField();
513
+ if (!session || !field) {
514
+ return null;
515
+ }
516
+ const isLastField = session.currentFieldIndex >= session.form.fields.length - 1;
517
+ const lines = [session.form.title];
518
+ if (session.form.description?.trim()) {
519
+ lines.push("", session.form.description.trim());
520
+ }
521
+ lines.push("", `Field ${session.currentFieldIndex + 1}/${session.form.fields.length}`, field.label);
522
+ if (field.help?.trim()) {
523
+ lines.push(field.help.trim());
524
+ }
525
+ let footer = `Form: Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel`;
526
+ if (field.type === "boolean") {
527
+ lines.push("", `${session.values[field.id] === true ? "[x]" : "[ ]"} ${field.label}`);
528
+ footer = `Form: Space toggle | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel`;
529
+ }
530
+ else if (field.type === "text") {
531
+ const current = String(session.values[field.id] ?? "");
532
+ lines.push("", "Text input:");
533
+ lines.push(...buildTextInputBox(current, session.currentTextCursorIndex, layout?.formContentWidth));
534
+ if (!current && field.placeholder?.trim()) {
535
+ lines.push(`Hint: ${field.placeholder.trim()}`);
536
+ }
537
+ footer = field.multiline
538
+ ? "Form: Enter newline | Tab switch | Ctrl+S submit | Esc cancel"
539
+ : `Form: Type text | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel`;
540
+ }
541
+ else {
542
+ const preview = session.form.preview?.trim() ?? "";
543
+ if (preview) {
544
+ const previewLines = preview.split("\n");
545
+ const previewHeight = 10;
546
+ const maxOffset = Math.max(0, previewLines.length - previewHeight);
547
+ session.previewScrollOffset = clamp(session.previewScrollOffset, 0, maxOffset);
548
+ const visibleLines = previewLines.slice(session.previewScrollOffset, session.previewScrollOffset + previewHeight);
549
+ lines.push("", "Preview:", ...visibleLines);
550
+ if (maxOffset > 0) {
551
+ lines.push(`Preview ${session.previewScrollOffset + 1}-${session.previewScrollOffset + visibleLines.length} of ${previewLines.length}`);
552
+ }
553
+ }
554
+ lines.push("", "Options:");
555
+ const selectedValues = field.type === "single-select"
556
+ ? [String(session.values[field.id] ?? "")]
557
+ : Array.isArray(session.values[field.id]) ? session.values[field.id] : [];
558
+ field.options.forEach((option, index) => {
559
+ const pointer = index === session.currentOptionIndex ? ">" : " ";
560
+ const marker = selectedValues.includes(option.value) ? "[x]" : "[ ]";
561
+ lines.push(`${pointer} ${marker} ${option.label}`);
562
+ });
563
+ footer = preview
564
+ ? `Form: Up/Down move | PageUp/PageDown preview | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel`
565
+ : `Form: Up/Down move | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel`;
566
+ }
567
+ return {
568
+ title: "User Input",
569
+ content: lines.join("\n"),
570
+ footer,
571
+ };
572
+ }
573
+ currentFormField() {
574
+ if (!this.activeFormSession) {
575
+ return null;
576
+ }
577
+ const field = this.activeFormSession.form.fields[this.activeFormSession.currentFieldIndex] ?? null;
578
+ if (!field) {
579
+ return null;
580
+ }
581
+ normalizeUserInputFieldValue(field, this.activeFormSession.values);
582
+ return resolveFieldDefinition(field, this.activeFormSession.values);
583
+ }
584
+ handleHelpKey(key) {
585
+ if (key.name === "escape" || key.name === "f1" || key.name === "h" || key.name === "?") {
586
+ this.helpVisible = false;
587
+ this.emitChange();
588
+ return;
589
+ }
590
+ this.handleScrollKey("help", key);
591
+ }
592
+ async handleConfirmKey(key) {
593
+ if (key.name === "escape") {
594
+ this.confirmSession = null;
595
+ this.emitChange();
596
+ return;
597
+ }
598
+ if (key.name === "left" || isReverseTabKey(key)) {
599
+ this.moveConfirmSelection(-1);
600
+ return;
601
+ }
602
+ if (key.name === "right" || key.name === "tab") {
603
+ this.moveConfirmSelection(1);
604
+ return;
605
+ }
606
+ if (key.name === "enter") {
607
+ await this.acceptConfirm();
608
+ }
609
+ }
610
+ async handleFlowKey(key) {
611
+ if (key.name === "up") {
612
+ this.moveSelectedFlow(-1);
613
+ return;
614
+ }
615
+ if (key.name === "down") {
616
+ this.moveSelectedFlow(1);
617
+ return;
618
+ }
619
+ if (key.name === "home") {
620
+ this.selectFlowIndex(0);
621
+ return;
622
+ }
623
+ if (key.name === "end") {
624
+ this.selectFlowIndex(Math.max(0, this.visibleFlowItems.length - 1));
625
+ return;
626
+ }
627
+ if (key.name === "pageup") {
628
+ this.moveSelectedFlow(-10);
629
+ return;
630
+ }
631
+ if (key.name === "pagedown") {
632
+ this.moveSelectedFlow(10);
633
+ return;
634
+ }
635
+ if (key.name === "right") {
636
+ this.expandSelectedFlowFolder();
637
+ return;
638
+ }
639
+ if (key.name === "left") {
640
+ this.collapseSelectedFlowFolderOrSelectParent();
641
+ return;
642
+ }
643
+ if (key.name === "enter") {
644
+ if (this.state.busy) {
645
+ return;
646
+ }
647
+ const selectedItem = this.selectedFlowTreeItem();
648
+ if (selectedItem?.kind === "folder") {
649
+ this.toggleFlowFolder(selectedItem.key);
650
+ return;
651
+ }
652
+ await this.openConfirm();
653
+ }
654
+ }
655
+ handleScrollKey(panel, key) {
656
+ const maxOffset = this.panelMaxScroll(panel);
657
+ const current = this.scrollOffsetFor(panel);
658
+ if (key.name === "up") {
659
+ this.setScrollOffset(panel, current - 1, maxOffset);
660
+ return;
661
+ }
662
+ if (key.name === "down") {
663
+ this.setScrollOffset(panel, current + 1, maxOffset);
664
+ return;
665
+ }
666
+ if (key.name === "pageup") {
667
+ this.setScrollOffset(panel, current - 10, maxOffset);
668
+ return;
669
+ }
670
+ if (key.name === "pagedown") {
671
+ this.setScrollOffset(panel, current + 10, maxOffset);
672
+ return;
673
+ }
674
+ if (key.name === "home") {
675
+ this.setScrollOffset(panel, 0, maxOffset);
676
+ return;
677
+ }
678
+ if (key.name === "end") {
679
+ this.setScrollOffset(panel, maxOffset, maxOffset);
680
+ }
681
+ }
682
+ moveSelectedFlow(delta) {
683
+ const currentIndex = Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey));
684
+ this.selectFlowIndex(clamp(currentIndex + delta, 0, Math.max(0, this.visibleFlowItems.length - 1)));
685
+ }
686
+ focusPane(pane) {
687
+ this.state.focusedPane = pane;
688
+ }
689
+ cycleFocus(direction) {
690
+ const panes = this.state.summaryVisible ? ["flows", "progress", "summary", "log"] : ["flows", "progress", "log"];
691
+ const currentIndex = panes.indexOf(this.state.focusedPane);
692
+ const nextIndex = (currentIndex + direction + panes.length) % panes.length;
693
+ this.focusPane(panes[nextIndex] ?? "flows");
694
+ this.emitChange();
695
+ }
696
+ toggleFlowFolder(folderKey) {
697
+ if (this.expandedFlowFolders.has(folderKey)) {
698
+ this.expandedFlowFolders.delete(folderKey);
699
+ }
700
+ else {
701
+ this.expandedFlowFolders.add(folderKey);
702
+ }
703
+ this.refreshVisibleFlowItems();
704
+ this.emitChange();
705
+ }
706
+ expandSelectedFlowFolder() {
707
+ const selectedItem = this.selectedFlowTreeItem();
708
+ if (!selectedItem || selectedItem.kind !== "folder" || this.expandedFlowFolders.has(selectedItem.key)) {
709
+ return;
710
+ }
711
+ this.toggleFlowFolder(selectedItem.key);
712
+ }
713
+ collapseSelectedFlowFolderOrSelectParent() {
714
+ const selectedItem = this.selectedFlowTreeItem();
715
+ if (!selectedItem) {
716
+ return;
717
+ }
718
+ if (selectedItem.kind === "folder" && this.expandedFlowFolders.has(selectedItem.key)) {
719
+ this.toggleFlowFolder(selectedItem.key);
720
+ return;
721
+ }
722
+ const parentPath = selectedItem.pathSegments.slice(0, -1);
723
+ if (parentPath.length === 0) {
724
+ return;
725
+ }
726
+ const parentKey = makeFolderKey(parentPath);
727
+ if (!this.visibleFlowItems.some((item) => item.key === parentKey)) {
728
+ return;
729
+ }
730
+ this.state.selectedFlowItemKey = parentKey;
731
+ this.refreshVisibleFlowItems();
732
+ this.emitChange();
733
+ }
734
+ refreshVisibleFlowItems() {
735
+ this.visibleFlowItems = computeVisibleFlowItems(this.flowTree, this.expandedFlowFolders);
736
+ if (!this.visibleFlowItems.some((item) => item.key === this.state.selectedFlowItemKey)) {
737
+ this.state.selectedFlowItemKey = this.visibleFlowItems[0]?.key ?? makeFlowKey(this.state.selectedFlowId);
738
+ }
739
+ const selectedItem = this.selectedFlowTreeItem();
740
+ if (selectedItem?.kind === "flow") {
741
+ this.state.selectedFlowId = selectedItem.flow.id;
742
+ }
743
+ }
744
+ selectedFlowTreeItem() {
745
+ return this.visibleFlowItems.find((item) => item.key === this.state.selectedFlowItemKey);
746
+ }
747
+ async openConfirm() {
748
+ if (this.state.busy || this.confirmSession) {
749
+ return;
750
+ }
751
+ const selectedItem = this.selectedFlowTreeItem();
752
+ if (!selectedItem || selectedItem.kind !== "flow") {
753
+ return;
754
+ }
755
+ const confirmation = await this.options.getRunConfirmation(selectedItem.flow.id);
756
+ if (this.state.busy || this.confirmSession) {
757
+ return;
758
+ }
759
+ this.confirmSession = {
760
+ kind: "run",
761
+ flowId: selectedItem.flow.id,
762
+ resumeAvailable: confirmation.resumeAvailable,
763
+ hasExistingState: confirmation.hasExistingState,
764
+ details: confirmation.details ?? null,
765
+ selectedAction: confirmation.resumeAvailable ? "resume" : confirmation.hasExistingState ? "restart" : "ok",
766
+ };
767
+ this.emitChange();
768
+ }
769
+ openInterruptConfirm() {
770
+ const flowId = this.state.currentFlowId;
771
+ if (!flowId || this.confirmSession) {
772
+ return;
773
+ }
774
+ this.confirmSession = {
775
+ kind: "interrupt",
776
+ flowId,
777
+ resumeAvailable: true,
778
+ hasExistingState: true,
779
+ details: "The current flow will be stopped. State will be saved and can be continued via Resume.",
780
+ selectedAction: "stop",
781
+ };
782
+ this.emitChange();
783
+ }
784
+ openExitConfirm() {
785
+ if (this.confirmSession) {
786
+ return;
787
+ }
788
+ const details = this.state.busy
789
+ ? "A flow is currently running. Exiting will close the interactive UI."
790
+ : "The interactive session will be closed.";
791
+ this.confirmSession = {
792
+ kind: "exit",
793
+ flowId: null,
794
+ resumeAvailable: false,
795
+ hasExistingState: false,
796
+ details,
797
+ selectedAction: "ok",
798
+ };
799
+ this.emitChange();
800
+ }
801
+ confirmActions() {
802
+ if (!this.confirmSession) {
803
+ return ["cancel"];
804
+ }
805
+ if (this.confirmSession.kind === "interrupt") {
806
+ return ["stop", "cancel"];
807
+ }
808
+ if (this.confirmSession.kind === "exit") {
809
+ return ["ok", "cancel"];
810
+ }
811
+ return this.confirmSession.resumeAvailable
812
+ ? ["resume", "restart", "cancel"]
813
+ : this.confirmSession.hasExistingState
814
+ ? ["restart", "cancel"]
815
+ : ["ok", "cancel"];
816
+ }
817
+ moveConfirmSelection(delta) {
818
+ if (!this.confirmSession) {
819
+ return;
820
+ }
821
+ const actions = this.confirmActions();
822
+ const currentIndex = actions.indexOf(this.confirmSession.selectedAction);
823
+ const nextIndex = (currentIndex + delta + actions.length) % actions.length;
824
+ this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
825
+ this.emitChange();
826
+ }
827
+ async acceptConfirm() {
828
+ const session = this.confirmSession;
829
+ if (!session) {
830
+ return;
831
+ }
832
+ if (session.selectedAction === "cancel") {
833
+ this.confirmSession = null;
834
+ this.emitChange();
835
+ return;
836
+ }
837
+ if (session.kind === "interrupt") {
838
+ const flowId = session.flowId;
839
+ this.confirmSession = null;
840
+ this.emitChange();
841
+ if (flowId) {
842
+ await this.options.onInterrupt(flowId);
843
+ }
844
+ return;
845
+ }
846
+ if (session.kind === "exit") {
847
+ this.confirmSession = null;
848
+ this.emitChange();
849
+ this.options.onExit();
850
+ return;
851
+ }
852
+ const flowId = session.flowId ?? this.state.selectedFlowId;
853
+ const launchMode = session.selectedAction === "resume" ? "resume" : "restart";
854
+ this.confirmSession = null;
855
+ this.setBusy(true, flowId);
856
+ this.clearFlowFailure(flowId);
857
+ this.state.flowState = {
858
+ flowId,
859
+ executionState: null,
860
+ };
861
+ this.emitChange();
862
+ try {
863
+ await this.options.onRun(flowId, launchMode);
864
+ }
865
+ finally {
866
+ this.setBusy(false);
867
+ this.focusPane("flows");
868
+ this.emitChange();
869
+ }
870
+ }
871
+ clearFlowFailure(flowId) {
872
+ if (this.state.failedFlowId === flowId) {
873
+ this.state.failedFlowId = null;
874
+ }
875
+ }
876
+ setBusy(busy, flowId) {
877
+ this.state.busy = busy;
878
+ if (flowId !== undefined) {
879
+ this.state.currentFlowId = flowId;
880
+ }
881
+ if (busy && this.state.runningStartedAt === null) {
882
+ this.state.runningStartedAt = Date.now();
883
+ }
884
+ if (!busy && this.state.currentNode === null && this.state.currentExecutor === null) {
885
+ this.state.runningStartedAt = null;
886
+ }
887
+ this.ensureSpinnerState();
888
+ }
889
+ ensureSpinnerState() {
890
+ const running = this.state.busy || this.state.currentNode !== null || this.state.currentExecutor !== null;
891
+ if (running && this.spinnerTimer === null) {
892
+ if (this.state.runningStartedAt === null) {
893
+ this.state.runningStartedAt = Date.now();
894
+ }
895
+ this.spinnerTimer = setInterval(() => {
896
+ this.state.spinnerFrame = (this.state.spinnerFrame + 1) % SPINNER_FRAMES.length;
897
+ this.emitChange();
898
+ }, 120);
899
+ return;
900
+ }
901
+ if (!running && this.spinnerTimer) {
902
+ clearInterval(this.spinnerTimer);
903
+ this.spinnerTimer = null;
904
+ this.state.spinnerFrame = 0;
905
+ this.state.runningStartedAt = null;
906
+ }
907
+ }
908
+ activeFlowId() {
909
+ return this.state.currentFlowId ?? this.state.selectedFlowId;
910
+ }
911
+ symbolForStatus(flowId, status) {
912
+ if (status === "done") {
913
+ return "✓";
914
+ }
915
+ if (status === "skipped") {
916
+ return "·";
917
+ }
918
+ if (status === "running") {
919
+ if (this.state.failedFlowId === flowId && !this.state.busy) {
920
+ return "×";
921
+ }
922
+ return SPINNER_FRAMES[this.state.spinnerFrame] ?? "▶";
923
+ }
924
+ return "○";
925
+ }
926
+ formatElapsed(now) {
927
+ if (this.state.runningStartedAt === null || now === null) {
928
+ return "00:00:00";
929
+ }
930
+ const totalSeconds = Math.max(0, Math.floor((now - this.state.runningStartedAt) / 1000));
931
+ const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
932
+ const minutes = String((totalSeconds % 3600) / 60 | 0).padStart(2, "0");
933
+ const seconds = String(totalSeconds % 60).padStart(2, "0");
934
+ return `${hours}:${minutes}:${seconds}`;
935
+ }
936
+ scrollOffsetFor(panel) {
937
+ if (panel === "progress") {
938
+ return this.state.progressScrollOffset;
939
+ }
940
+ if (panel === "summary") {
941
+ return this.state.summaryScrollOffset;
942
+ }
943
+ if (panel === "help") {
944
+ return this.state.helpScrollOffset;
945
+ }
946
+ return this.state.logScrollOffset;
947
+ }
948
+ setScrollOffset(panel, value, maxOffset) {
949
+ const next = clamp(value, 0, maxOffset);
950
+ if (panel === "progress") {
951
+ this.state.progressScrollOffset = next;
952
+ }
953
+ else if (panel === "summary") {
954
+ this.state.summaryScrollOffset = next;
955
+ }
956
+ else if (panel === "help") {
957
+ this.state.helpScrollOffset = next;
958
+ }
959
+ else {
960
+ this.state.logScrollOffset = next;
961
+ }
962
+ this.emitChange();
963
+ }
964
+ panelMaxScroll(panel) {
965
+ const content = panel === "progress"
966
+ ? this.getViewModel().progressText
967
+ : panel === "summary"
968
+ ? this.getViewModel().summaryText
969
+ : panel === "help"
970
+ ? this.getViewModel().helpText
971
+ : this.getViewModel().logText;
972
+ return Math.max(0, content.split("\n").length - 1);
973
+ }
974
+ moveActiveFormField(delta) {
975
+ const session = this.activeFormSession;
976
+ if (!session) {
977
+ return;
978
+ }
979
+ this.syncActiveSelectFieldValue();
980
+ const nextIndex = clamp(session.currentFieldIndex + delta, 0, Math.max(0, session.form.fields.length - 1));
981
+ session.currentFieldIndex = nextIndex;
982
+ const nextField = session.form.fields[nextIndex];
983
+ if (nextField?.type === "text") {
984
+ const current = String(session.values[nextField.id] ?? "");
985
+ session.currentTextCursorIndex = current.length;
986
+ session.currentOptionIndex = 0;
987
+ }
988
+ else if (nextField?.type === "single-select" || nextField?.type === "multi-select") {
989
+ session.currentTextCursorIndex = 0;
990
+ session.currentOptionIndex = this.selectedOptionIndexForField(nextField);
991
+ }
992
+ else {
993
+ session.currentTextCursorIndex = 0;
994
+ session.currentOptionIndex = 0;
995
+ }
996
+ session.previewScrollOffset = 0;
997
+ this.emitChange();
998
+ }
999
+ selectedOptionIndexForField(field) {
1000
+ const session = this.activeFormSession;
1001
+ if (!session || field.options.length === 0) {
1002
+ return 0;
1003
+ }
1004
+ if (field.type === "single-select") {
1005
+ const selectedValue = String(session.values[field.id] ?? "");
1006
+ const selectedIndex = field.options.findIndex((option) => option.value === selectedValue);
1007
+ return selectedIndex >= 0 ? selectedIndex : 0;
1008
+ }
1009
+ const selectedValues = Array.isArray(session.values[field.id]) ? session.values[field.id] : [];
1010
+ const selectedIndex = field.options.findIndex((option) => selectedValues.includes(option.value));
1011
+ return selectedIndex >= 0 ? selectedIndex : 0;
1012
+ }
1013
+ syncActiveSelectFieldValue() {
1014
+ const session = this.activeFormSession;
1015
+ const field = this.currentFormField();
1016
+ if (!session || !field || (field.type !== "single-select" && field.type !== "multi-select")) {
1017
+ return;
1018
+ }
1019
+ session.currentOptionIndex = clamp(session.currentOptionIndex, 0, Math.max(0, field.options.length - 1));
1020
+ }
1021
+ toggleActiveFormValue() {
1022
+ const session = this.activeFormSession;
1023
+ const field = this.currentFormField();
1024
+ if (!session || !field) {
1025
+ return;
1026
+ }
1027
+ if (field.type === "boolean") {
1028
+ session.values[field.id] = session.values[field.id] !== true;
1029
+ this.emitChange();
1030
+ return;
1031
+ }
1032
+ if (field.type !== "single-select" && field.type !== "multi-select") {
1033
+ return;
1034
+ }
1035
+ this.syncActiveSelectFieldValue();
1036
+ const option = field.options[session.currentOptionIndex];
1037
+ if (!option) {
1038
+ return;
1039
+ }
1040
+ if (field.type === "single-select") {
1041
+ session.values[field.id] = option.value;
1042
+ this.emitChange();
1043
+ return;
1044
+ }
1045
+ const current = Array.isArray(session.values[field.id]) ? [...session.values[field.id]] : [];
1046
+ session.values[field.id] = current.includes(option.value)
1047
+ ? current.filter((item) => item !== option.value)
1048
+ : [...current, option.value];
1049
+ this.emitChange();
1050
+ }
1051
+ confirmActiveFormField() {
1052
+ const session = this.activeFormSession;
1053
+ if (!session) {
1054
+ return;
1055
+ }
1056
+ this.syncActiveSelectFieldValue();
1057
+ if (session.currentFieldIndex >= session.form.fields.length - 1) {
1058
+ this.submitActiveForm();
1059
+ return;
1060
+ }
1061
+ this.moveActiveFormField(1);
1062
+ }
1063
+ submitActiveForm() {
1064
+ const session = this.activeFormSession;
1065
+ if (!session) {
1066
+ return;
1067
+ }
1068
+ this.syncActiveSelectFieldValue();
1069
+ try {
1070
+ validateUserInputValues(session.form, session.values);
1071
+ const result = {
1072
+ formId: session.form.formId,
1073
+ submittedAt: new Date().toISOString(),
1074
+ values: session.values,
1075
+ };
1076
+ this.activeFormSession = null;
1077
+ this.focusPane("flows");
1078
+ session.resolve(result);
1079
+ this.emitChange();
1080
+ }
1081
+ catch (error) {
1082
+ this.appendLog(error.message);
1083
+ }
1084
+ }
1085
+ cancelActiveForm() {
1086
+ const session = this.activeFormSession;
1087
+ if (!session) {
1088
+ return;
1089
+ }
1090
+ this.activeFormSession = null;
1091
+ this.focusPane("flows");
1092
+ session.reject(new TaskRunnerError(`User cancelled form '${session.form.formId}'.`));
1093
+ this.emitChange();
1094
+ }
1095
+ handleActiveFormKey(ch, key) {
1096
+ const field = this.currentFormField();
1097
+ if (!field) {
1098
+ return;
1099
+ }
1100
+ if (key.ctrl && key.name === "s") {
1101
+ this.submitActiveForm();
1102
+ return;
1103
+ }
1104
+ if (key.name === "escape") {
1105
+ this.cancelActiveForm();
1106
+ return;
1107
+ }
1108
+ if (isReverseTabKey(key)) {
1109
+ this.moveActiveFormField(-1);
1110
+ return;
1111
+ }
1112
+ if (key.name === "tab") {
1113
+ this.moveActiveFormField(1);
1114
+ return;
1115
+ }
1116
+ if (field.type === "text") {
1117
+ this.handleTextFormKey(field, ch, key);
1118
+ return;
1119
+ }
1120
+ if (field.type === "boolean") {
1121
+ if (key.name === "space" || key.name === "left" || key.name === "right") {
1122
+ this.toggleActiveFormValue();
1123
+ return;
1124
+ }
1125
+ if (key.name === "enter") {
1126
+ this.confirmActiveFormField();
1127
+ }
1128
+ return;
1129
+ }
1130
+ this.handleSelectFormKey(key);
1131
+ }
1132
+ handleTextFormKey(field, ch, key) {
1133
+ const session = this.activeFormSession;
1134
+ if (!session) {
1135
+ return;
1136
+ }
1137
+ const current = String(session.values[field.id] ?? "");
1138
+ if (!field.multiline) {
1139
+ if (key.name === "enter") {
1140
+ this.confirmActiveFormField();
1141
+ return;
1142
+ }
1143
+ if (key.name === "left") {
1144
+ session.currentTextCursorIndex = Math.max(0, session.currentTextCursorIndex - 1);
1145
+ this.emitChange();
1146
+ return;
1147
+ }
1148
+ if (key.name === "right") {
1149
+ session.currentTextCursorIndex = Math.min(current.length, session.currentTextCursorIndex + 1);
1150
+ this.emitChange();
1151
+ return;
1152
+ }
1153
+ if (key.name === "home") {
1154
+ session.currentTextCursorIndex = 0;
1155
+ this.emitChange();
1156
+ return;
1157
+ }
1158
+ if (key.name === "end") {
1159
+ session.currentTextCursorIndex = current.length;
1160
+ this.emitChange();
1161
+ return;
1162
+ }
1163
+ if (key.name === "backspace" && session.currentTextCursorIndex > 0) {
1164
+ session.values[field.id] = `${current.slice(0, session.currentTextCursorIndex - 1)}${current.slice(session.currentTextCursorIndex)}`;
1165
+ session.currentTextCursorIndex -= 1;
1166
+ this.emitChange();
1167
+ return;
1168
+ }
1169
+ if (key.name === "delete" && session.currentTextCursorIndex < current.length) {
1170
+ session.values[field.id] = `${current.slice(0, session.currentTextCursorIndex)}${current.slice(session.currentTextCursorIndex + 1)}`;
1171
+ this.emitChange();
1172
+ return;
1173
+ }
1174
+ if (isPrintableCharacter(ch, key)) {
1175
+ session.values[field.id] = `${current.slice(0, session.currentTextCursorIndex)}${ch}${current.slice(session.currentTextCursorIndex)}`;
1176
+ session.currentTextCursorIndex += ch.length;
1177
+ this.emitChange();
1178
+ }
1179
+ return;
1180
+ }
1181
+ if (key.name === "enter") {
1182
+ session.values[field.id] = `${current.slice(0, session.currentTextCursorIndex)}\n${current.slice(session.currentTextCursorIndex)}`;
1183
+ session.currentTextCursorIndex += 1;
1184
+ this.emitChange();
1185
+ return;
1186
+ }
1187
+ if (key.name === "left") {
1188
+ session.currentTextCursorIndex = Math.max(0, session.currentTextCursorIndex - 1);
1189
+ this.emitChange();
1190
+ return;
1191
+ }
1192
+ if (key.name === "right") {
1193
+ session.currentTextCursorIndex = Math.min(current.length, session.currentTextCursorIndex + 1);
1194
+ this.emitChange();
1195
+ return;
1196
+ }
1197
+ if (key.name === "home") {
1198
+ const { line } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1199
+ session.currentTextCursorIndex = textLineColumnToIndex(current, line, 0);
1200
+ this.emitChange();
1201
+ return;
1202
+ }
1203
+ if (key.name === "end") {
1204
+ const { line } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1205
+ const lineText = current.split("\n")[line] ?? "";
1206
+ session.currentTextCursorIndex = textLineColumnToIndex(current, line, lineText.length);
1207
+ this.emitChange();
1208
+ return;
1209
+ }
1210
+ if (key.name === "up") {
1211
+ const { line, column } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1212
+ session.currentTextCursorIndex = textLineColumnToIndex(current, Math.max(0, line - 1), column);
1213
+ this.emitChange();
1214
+ return;
1215
+ }
1216
+ if (key.name === "down") {
1217
+ const { line, column } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1218
+ session.currentTextCursorIndex = textLineColumnToIndex(current, line + 1, column);
1219
+ this.emitChange();
1220
+ return;
1221
+ }
1222
+ if (key.name === "backspace" && session.currentTextCursorIndex > 0) {
1223
+ session.values[field.id] = `${current.slice(0, session.currentTextCursorIndex - 1)}${current.slice(session.currentTextCursorIndex)}`;
1224
+ session.currentTextCursorIndex -= 1;
1225
+ this.emitChange();
1226
+ return;
1227
+ }
1228
+ if (key.name === "delete" && session.currentTextCursorIndex < current.length) {
1229
+ session.values[field.id] = `${current.slice(0, session.currentTextCursorIndex)}${current.slice(session.currentTextCursorIndex + 1)}`;
1230
+ this.emitChange();
1231
+ return;
1232
+ }
1233
+ if (isPrintableCharacter(ch, key)) {
1234
+ session.values[field.id] = `${current.slice(0, session.currentTextCursorIndex)}${ch}${current.slice(session.currentTextCursorIndex)}`;
1235
+ session.currentTextCursorIndex += ch.length;
1236
+ this.emitChange();
1237
+ }
1238
+ }
1239
+ handleSelectFormKey(key) {
1240
+ const session = this.activeFormSession;
1241
+ const field = this.currentFormField();
1242
+ if (!session || !field || (field.type !== "single-select" && field.type !== "multi-select")) {
1243
+ return;
1244
+ }
1245
+ if (key.name === "up") {
1246
+ session.currentOptionIndex = clamp(session.currentOptionIndex - 1, 0, Math.max(0, field.options.length - 1));
1247
+ this.emitChange();
1248
+ return;
1249
+ }
1250
+ if (key.name === "down") {
1251
+ session.currentOptionIndex = clamp(session.currentOptionIndex + 1, 0, Math.max(0, field.options.length - 1));
1252
+ this.emitChange();
1253
+ return;
1254
+ }
1255
+ if (key.name === "pageup" || key.name === "pagedown") {
1256
+ const previewLines = (session.form.preview?.trim() ?? "").split("\n").filter((line) => line.length > 0);
1257
+ if (previewLines.length > 10) {
1258
+ session.previewScrollOffset = clamp(session.previewScrollOffset + (key.name === "pageup" ? -10 : 10), 0, Math.max(0, previewLines.length - 10));
1259
+ this.emitChange();
1260
+ }
1261
+ return;
1262
+ }
1263
+ if (key.name === "space") {
1264
+ this.toggleActiveFormValue();
1265
+ return;
1266
+ }
1267
+ if (key.name === "enter") {
1268
+ if (field.type === "single-select" && field.options[session.currentOptionIndex]) {
1269
+ session.values[field.id] = field.options[session.currentOptionIndex]?.value ?? "";
1270
+ }
1271
+ this.confirmActiveFormField();
1272
+ }
1273
+ }
1274
+ scheduleLogFlush() {
1275
+ if (this.logFlushTimer) {
1276
+ return;
1277
+ }
1278
+ this.logFlushTimer = setTimeout(() => {
1279
+ this.logFlushTimer = null;
1280
+ this.flushPendingLogLines();
1281
+ }, 50);
1282
+ }
1283
+ flushPendingLogLines() {
1284
+ if (this.pendingLogLines.length === 0) {
1285
+ return;
1286
+ }
1287
+ const appendedLines = this.pendingLogLines.splice(0, this.pendingLogLines.length);
1288
+ this.emitChange({
1289
+ type: "log",
1290
+ appendedLines,
1291
+ });
1292
+ }
1293
+ }