agentweaver 0.1.17 → 0.1.18

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 (47) hide show
  1. package/README.md +104 -23
  2. package/dist/artifacts.js +41 -0
  3. package/dist/index.js +252 -27
  4. package/dist/interactive/controller.js +249 -13
  5. package/dist/interactive/ink/index.js +2 -2
  6. package/dist/interactive/state.js +1 -0
  7. package/dist/interactive/web/index.js +179 -0
  8. package/dist/interactive/web/protocol.js +154 -0
  9. package/dist/interactive/web/server.js +575 -0
  10. package/dist/interactive/web/static/app.js +709 -0
  11. package/dist/interactive/web/static/index.html +77 -0
  12. package/dist/interactive/web/static/styles.css +2 -0
  13. package/dist/interactive/web/static/styles.input.css +469 -0
  14. package/dist/pipeline/flow-catalog.js +4 -0
  15. package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
  16. package/dist/pipeline/flow-specs/auto-common.json +3 -1
  17. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +2 -0
  18. package/dist/pipeline/flow-specs/design-review.json +2 -0
  19. package/dist/pipeline/flow-specs/implement.json +3 -1
  20. package/dist/pipeline/flow-specs/plan.json +4 -0
  21. package/dist/pipeline/flow-specs/playbook-init.json +199 -0
  22. package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
  23. package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
  24. package/dist/pipeline/flow-specs/review/review.json +2 -0
  25. package/dist/pipeline/node-registry.js +45 -0
  26. package/dist/pipeline/nodes/flow-run-node.js +13 -1
  27. package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
  28. package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
  29. package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
  30. package/dist/pipeline/nodes/playbook-write-node.js +243 -0
  31. package/dist/pipeline/nodes/project-guidance-node.js +69 -0
  32. package/dist/pipeline/prompt-registry.js +4 -1
  33. package/dist/pipeline/prompt-runtime.js +6 -2
  34. package/dist/pipeline/spec-types.js +19 -0
  35. package/dist/pipeline/value-resolver.js +39 -1
  36. package/dist/playbook/practice-candidates.js +12 -0
  37. package/dist/playbook/repo-inventory.js +208 -0
  38. package/dist/prompts.js +31 -0
  39. package/dist/runtime/playbook.js +485 -0
  40. package/dist/runtime/project-guidance.js +339 -0
  41. package/dist/structured-artifact-schema-registry.js +8 -0
  42. package/dist/structured-artifact-schemas.json +235 -0
  43. package/dist/structured-artifacts.js +7 -1
  44. package/docs/declarative-workflows.md +565 -0
  45. package/docs/features.md +77 -0
  46. package/docs/playbook.md +327 -0
  47. package/package.json +8 -3
@@ -182,6 +182,7 @@ export class InteractiveSessionController {
182
182
  currentOptionIndex: initialOptionIndex,
183
183
  currentTextCursorIndex: initialCursorIndex,
184
184
  previewScrollOffset: 0,
185
+ validationError: null,
185
186
  resolve,
186
187
  reject,
187
188
  };
@@ -206,9 +207,12 @@ export class InteractiveSessionController {
206
207
  }
207
208
  this.emitChange();
208
209
  }
209
- setScope(scopeKey, jiraIssueKey) {
210
+ setScope(scopeKey, jiraIssueKey, gitBranchName) {
210
211
  this.state.scopeKey = scopeKey;
211
212
  this.state.jiraIssueKey = jiraIssueKey ?? null;
213
+ if (gitBranchName !== undefined) {
214
+ this.state.gitBranchName = gitBranchName;
215
+ }
212
216
  this.emitChange();
213
217
  }
214
218
  appendLog(text) {
@@ -306,7 +310,7 @@ export class InteractiveSessionController {
306
310
  selectFlowIndex(index) {
307
311
  const selectedItem = this.visibleFlowItems[index];
308
312
  if (!selectedItem) {
309
- return;
313
+ throw new Error(`Invalid flow index: ${index}`);
310
314
  }
311
315
  this.state.selectedFlowItemKey = selectedItem.key;
312
316
  if (selectedItem.kind === "flow") {
@@ -314,6 +318,210 @@ export class InteractiveSessionController {
314
318
  }
315
319
  this.emitChange();
316
320
  }
321
+ selectFlowKey(key) {
322
+ const selectedItem = this.visibleFlowItems.find((item) => item.key === key);
323
+ if (!selectedItem) {
324
+ throw new Error(`Unknown visible flow item key: ${key}`);
325
+ }
326
+ this.state.selectedFlowItemKey = selectedItem.key;
327
+ if (selectedItem.kind === "flow") {
328
+ this.state.selectedFlowId = selectedItem.flow.id;
329
+ }
330
+ this.emitChange();
331
+ }
332
+ selectFlowId(flowId) {
333
+ const selectedItem = this.visibleFlowItems.find((item) => item.kind === "flow" && item.flow.id === flowId);
334
+ if (!selectedItem) {
335
+ throw new Error(`Unknown visible flow: ${flowId}`);
336
+ }
337
+ this.state.selectedFlowItemKey = selectedItem.key;
338
+ this.state.selectedFlowId = selectedItem.flow.id;
339
+ this.emitChange();
340
+ }
341
+ toggleFolderKey(key) {
342
+ const item = this.visibleFlowItems.find((candidate) => candidate.key === key);
343
+ if (!item || item.kind !== "folder") {
344
+ throw new Error(`Unknown visible folder key: ${key}`);
345
+ }
346
+ this.toggleFlowFolder(key);
347
+ }
348
+ toggleFolder(key) {
349
+ this.toggleFolderKey(key);
350
+ }
351
+ async openRunConfirm(flowId, key) {
352
+ if (flowId) {
353
+ if (!this.visibleFlowItems.some((item) => item.kind === "flow" && item.flow.id === flowId)) {
354
+ throw new Error(`Unknown visible flow: ${flowId}`);
355
+ }
356
+ this.selectFlowId(flowId);
357
+ }
358
+ else if (key) {
359
+ const keyedItem = this.visibleFlowItems.find((item) => item.key === key);
360
+ if (!keyedItem || keyedItem.kind !== "flow") {
361
+ throw new Error(`Unknown visible flow item key: ${key}`);
362
+ }
363
+ this.selectFlowKey(key);
364
+ }
365
+ const selectedItem = this.selectedFlowTreeItem();
366
+ if (!selectedItem || selectedItem.kind !== "flow") {
367
+ throw new Error("A flow must be selected before opening run confirmation.");
368
+ }
369
+ await this.openConfirm();
370
+ }
371
+ selectConfirmAction(action) {
372
+ if (!this.confirmSession) {
373
+ throw new Error("No confirmation is active.");
374
+ }
375
+ const actions = this.confirmActions();
376
+ if (!actions.includes(action)) {
377
+ throw new Error(`Invalid confirmation action: ${action}`);
378
+ }
379
+ this.confirmSession.selectedAction = action;
380
+ this.emitChange();
381
+ }
382
+ async acceptConfirmation() {
383
+ await this.acceptConfirm();
384
+ }
385
+ async acceptConfirm() {
386
+ if (!this.confirmSession) {
387
+ throw new Error("No confirmation is active.");
388
+ }
389
+ await this.acceptActiveConfirm();
390
+ }
391
+ cancelConfirmation() {
392
+ this.cancelConfirm();
393
+ }
394
+ cancelConfirm() {
395
+ if (!this.confirmSession) {
396
+ throw new Error("No confirmation is active.");
397
+ }
398
+ this.confirmSession = null;
399
+ this.emitChange();
400
+ }
401
+ updateActiveFormValues(values) {
402
+ const session = this.activeFormSession;
403
+ if (!session) {
404
+ throw new Error("No form is active.");
405
+ }
406
+ const fieldIds = new Set(session.form.fields.map((field) => field.id));
407
+ const nextValues = { ...session.values };
408
+ let changed = false;
409
+ for (const [fieldId, value] of Object.entries(values)) {
410
+ if (!fieldIds.has(fieldId)) {
411
+ continue;
412
+ }
413
+ nextValues[fieldId] = value;
414
+ changed = true;
415
+ }
416
+ if (!changed) {
417
+ return;
418
+ }
419
+ session.values = nextValues;
420
+ for (const field of session.form.fields) {
421
+ normalizeUserInputFieldValue(field, session.values);
422
+ }
423
+ session.validationError = null;
424
+ const field = this.currentFormField();
425
+ if (field?.type === "text") {
426
+ session.currentTextCursorIndex = String(session.values[field.id] ?? "").length;
427
+ }
428
+ else if (field?.type === "single-select" || field?.type === "multi-select") {
429
+ session.currentOptionIndex = this.selectedOptionIndexForField(field);
430
+ }
431
+ this.emitChange();
432
+ }
433
+ updateFormField(fieldId, value) {
434
+ const session = this.activeFormSession;
435
+ if (!session) {
436
+ throw new Error("No form is active.");
437
+ }
438
+ const fieldIndex = session.form.fields.findIndex((candidate) => candidate.id === fieldId);
439
+ const baseField = session.form.fields[fieldIndex];
440
+ if (!baseField) {
441
+ throw new Error(`Unknown form field: ${fieldId}`);
442
+ }
443
+ const field = resolveFieldDefinition(baseField, session.values);
444
+ if (field.type === "boolean" && typeof value !== "boolean") {
445
+ throw new Error(`Field '${field.label}' must be a boolean.`);
446
+ }
447
+ if (field.type === "text" && typeof value !== "string") {
448
+ throw new Error(`Field '${field.label}' must be a string.`);
449
+ }
450
+ if (field.type === "single-select" && typeof value !== "string") {
451
+ throw new Error(`Field '${field.label}' must be a string.`);
452
+ }
453
+ if (field.type === "multi-select"
454
+ && (!Array.isArray(value) || value.some((item) => typeof item !== "string"))) {
455
+ throw new Error(`Field '${field.label}' must be a string array.`);
456
+ }
457
+ session.currentFieldIndex = fieldIndex;
458
+ this.updateActiveFormValues({ [fieldId]: value });
459
+ }
460
+ submitActiveFormValues(values) {
461
+ if (!this.activeFormSession) {
462
+ throw new Error("No form is active.");
463
+ }
464
+ if (values) {
465
+ this.updateActiveFormValues(values);
466
+ }
467
+ this.submitActiveForm();
468
+ if (this.activeFormSession) {
469
+ throw new Error("Form validation failed. See session log for details.");
470
+ }
471
+ }
472
+ submitForm(values) {
473
+ this.submitActiveFormValues(values);
474
+ }
475
+ cancelForm() {
476
+ if (!this.activeFormSession) {
477
+ throw new Error("No form is active.");
478
+ }
479
+ this.cancelActiveForm();
480
+ }
481
+ async interruptFlow(flowId) {
482
+ const hadActiveForm = this.activeFormSession !== null;
483
+ if (this.activeFormSession) {
484
+ this.interruptActiveForm();
485
+ }
486
+ const targetFlowId = flowId ?? this.state.currentFlowId;
487
+ if (!targetFlowId && hadActiveForm) {
488
+ return;
489
+ }
490
+ if (!targetFlowId) {
491
+ throw new Error("No running flow is available to interrupt.");
492
+ }
493
+ await this.options.onInterrupt(targetFlowId);
494
+ }
495
+ async interruptCurrentFlow(flowId) {
496
+ await this.interruptFlow(flowId);
497
+ }
498
+ toggleHelp(visible) {
499
+ this.helpVisible = visible ?? !this.helpVisible;
500
+ this.emitChange();
501
+ }
502
+ showHelp(visible) {
503
+ this.toggleHelp(visible);
504
+ }
505
+ scrollPane(panel, options) {
506
+ if (panel === "flows") {
507
+ if (options.delta !== undefined) {
508
+ this.moveSelectedFlow(options.delta);
509
+ return;
510
+ }
511
+ if (options.offset !== undefined) {
512
+ this.selectFlowIndex(options.offset);
513
+ return;
514
+ }
515
+ throw new Error("Flow scroll requires delta or offset.");
516
+ }
517
+ const scrollPanel = panel;
518
+ const maxOffset = this.panelMaxScroll(scrollPanel);
519
+ const current = this.scrollOffsetFor(scrollPanel);
520
+ this.applyScrollOffset(scrollPanel, options.offset ?? current + (options.delta ?? 0), maxOffset);
521
+ }
522
+ setScrollOffset(panel, offset) {
523
+ this.applyScrollOffset(panel, offset, this.panelMaxScroll(panel));
524
+ }
317
525
  getViewModel(layout) {
318
526
  const selectedItem = this.selectedFlowTreeItem();
319
527
  const activeFlowId = this.activeFlowId();
@@ -337,6 +545,10 @@ export class InteractiveSessionController {
337
545
  flowItems: this.visibleFlowItems.map((item) => ({
338
546
  key: item.key,
339
547
  label: this.renderFlowTreeLabel(item),
548
+ kind: item.kind,
549
+ name: item.name,
550
+ depth: item.depth,
551
+ ...(item.kind === "folder" ? { expanded: this.expandedFlowFolders.has(item.key) } : {}),
340
552
  })),
341
553
  selectedFlowIndex: Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey)),
342
554
  progressTitle: this.panelTitle("Current Flow", "progress"),
@@ -352,6 +564,7 @@ export class InteractiveSessionController {
352
564
  logText: this.logText,
353
565
  logScrollOffset: this.state.logScrollOffset,
354
566
  confirmText: this.renderConfirmText(),
567
+ confirmation: this.renderConfirmationView(),
355
568
  form: this.renderFormView(layout),
356
569
  };
357
570
  }
@@ -395,7 +608,7 @@ export class InteractiveSessionController {
395
608
  const current = this.state.currentFlowId ?? selectHeaderLabel(this.selectedFlowTreeItem(), this.state.selectedFlowId);
396
609
  const pathParts = this.options.cwd.split(path.sep).filter(Boolean);
397
610
  const folderName = pathParts.slice(-3).join("/") || this.options.cwd;
398
- const branchLabel = this.options.gitBranchName ? this.options.gitBranchName : "detached-head";
611
+ const branchLabel = this.state.gitBranchName ? this.state.gitBranchName : "detached-head";
399
612
  const runningSuffix = this.state.busy ? " [running]" : "";
400
613
  const versionLabel = this.state.version ? ` | Version ${this.state.version}` : "";
401
614
  const jiraLabel = this.state.jiraIssueKey ? ` | Jira ${this.state.jiraIssueKey}` : "";
@@ -512,6 +725,20 @@ export class InteractiveSessionController {
512
725
  lines.push("", actionLabels, "", "Left/Right or Tab: choose Enter: confirm Esc: cancel");
513
726
  return lines.join("\n");
514
727
  }
728
+ renderConfirmationView() {
729
+ const session = this.confirmSession;
730
+ const text = this.renderConfirmText();
731
+ if (!session || !text) {
732
+ return null;
733
+ }
734
+ return {
735
+ kind: session.kind,
736
+ flowId: session.flowId,
737
+ text,
738
+ actions: this.confirmActions(),
739
+ selectedAction: session.selectedAction,
740
+ };
741
+ }
515
742
  renderFormView(layout) {
516
743
  const session = this.activeFormSession;
517
744
  const field = this.currentFormField();
@@ -573,6 +800,12 @@ export class InteractiveSessionController {
573
800
  title: "User Input",
574
801
  content: lines.join("\n"),
575
802
  footer,
803
+ formId: session.form.formId,
804
+ definition: session.form,
805
+ values: { ...session.values },
806
+ fields: session.form.fields.map((candidate) => resolveFieldDefinition(candidate, session.values)),
807
+ currentFieldId: field.id,
808
+ error: session.validationError,
576
809
  };
577
810
  }
578
811
  currentFormField() {
@@ -609,7 +842,7 @@ export class InteractiveSessionController {
609
842
  return;
610
843
  }
611
844
  if (key.name === "enter") {
612
- await this.acceptConfirm();
845
+ await this.acceptActiveConfirm();
613
846
  }
614
847
  }
615
848
  async handleFlowKey(key) {
@@ -661,27 +894,27 @@ export class InteractiveSessionController {
661
894
  const maxOffset = this.panelMaxScroll(panel);
662
895
  const current = this.scrollOffsetFor(panel);
663
896
  if (key.name === "up") {
664
- this.setScrollOffset(panel, current - 1, maxOffset);
897
+ this.applyScrollOffset(panel, current - 1, maxOffset);
665
898
  return;
666
899
  }
667
900
  if (key.name === "down") {
668
- this.setScrollOffset(panel, current + 1, maxOffset);
901
+ this.applyScrollOffset(panel, current + 1, maxOffset);
669
902
  return;
670
903
  }
671
904
  if (key.name === "pageup") {
672
- this.setScrollOffset(panel, current - 10, maxOffset);
905
+ this.applyScrollOffset(panel, current - 10, maxOffset);
673
906
  return;
674
907
  }
675
908
  if (key.name === "pagedown") {
676
- this.setScrollOffset(panel, current + 10, maxOffset);
909
+ this.applyScrollOffset(panel, current + 10, maxOffset);
677
910
  return;
678
911
  }
679
912
  if (key.name === "home") {
680
- this.setScrollOffset(panel, 0, maxOffset);
913
+ this.applyScrollOffset(panel, 0, maxOffset);
681
914
  return;
682
915
  }
683
916
  if (key.name === "end") {
684
- this.setScrollOffset(panel, maxOffset, maxOffset);
917
+ this.applyScrollOffset(panel, maxOffset, maxOffset);
685
918
  }
686
919
  }
687
920
  moveSelectedFlow(delta) {
@@ -853,7 +1086,7 @@ export class InteractiveSessionController {
853
1086
  this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
854
1087
  this.emitChange();
855
1088
  }
856
- async acceptConfirm() {
1089
+ async acceptActiveConfirm() {
857
1090
  const session = this.confirmSession;
858
1091
  if (!session) {
859
1092
  return;
@@ -978,7 +1211,7 @@ export class InteractiveSessionController {
978
1211
  }
979
1212
  return this.state.logScrollOffset;
980
1213
  }
981
- setScrollOffset(panel, value, maxOffset) {
1214
+ applyScrollOffset(panel, value, maxOffset) {
982
1215
  const next = clamp(value, 0, maxOffset);
983
1216
  if (panel === "progress") {
984
1217
  this.state.progressScrollOffset = next;
@@ -1100,6 +1333,7 @@ export class InteractiveSessionController {
1100
1333
  }
1101
1334
  this.syncActiveSelectFieldValue();
1102
1335
  try {
1336
+ session.validationError = null;
1103
1337
  validateUserInputValues(session.form, session.values);
1104
1338
  const result = {
1105
1339
  formId: session.form.formId,
@@ -1112,7 +1346,9 @@ export class InteractiveSessionController {
1112
1346
  this.emitChange();
1113
1347
  }
1114
1348
  catch (error) {
1115
- this.appendLog(error.message);
1349
+ session.validationError = error.message;
1350
+ this.appendLog(session.validationError);
1351
+ this.emitChange();
1116
1352
  }
1117
1353
  }
1118
1354
  cancelActiveForm() {
@@ -562,8 +562,8 @@ class InkInteractiveSession {
562
562
  clearSummary() {
563
563
  this.controller.clearSummary();
564
564
  }
565
- setScope(scopeKey, jiraIssueKey) {
566
- this.controller.setScope(scopeKey, jiraIssueKey);
565
+ setScope(scopeKey, jiraIssueKey, gitBranchName) {
566
+ this.controller.setScope(scopeKey, jiraIssueKey, gitBranchName);
567
567
  }
568
568
  appendLog(text) {
569
569
  this.controller.appendLog(text);
@@ -8,6 +8,7 @@ export function createInitialInteractiveState(options) {
8
8
  return {
9
9
  scopeKey: options.scopeKey,
10
10
  jiraIssueKey: options.jiraIssueKey ?? null,
11
+ gitBranchName: options.gitBranchName,
11
12
  summaryText: options.summaryText.trim(),
12
13
  version: options.version ?? "",
13
14
  flowTreeKeys: flowTree.map((node) => node.key),
@@ -0,0 +1,179 @@
1
+ import process from "node:process";
2
+ import { writeSync } from "node:fs";
3
+ import { FlowInterruptedError } from "../../errors.js";
4
+ import { InteractiveSessionController } from "../controller.js";
5
+ import { startWebServer } from "./server.js";
6
+ function actionId(action) {
7
+ return "actionId" in action ? action.actionId : undefined;
8
+ }
9
+ export function createWebInteractiveSession(options, webOptions = {}) {
10
+ const controller = new InteractiveSessionController(options);
11
+ let server = null;
12
+ let unsubscribe = null;
13
+ let mounted = false;
14
+ let shuttingDown = false;
15
+ function snapshot() {
16
+ return { type: "snapshot", viewModel: controller.getViewModel() };
17
+ }
18
+ function sendError(client, message, id) {
19
+ const event = { type: "error", message, ...(id ? { actionId: id } : {}) };
20
+ if (client) {
21
+ client.send(event);
22
+ }
23
+ else {
24
+ server?.broadcast(event);
25
+ }
26
+ }
27
+ async function dispatch(action, client) {
28
+ try {
29
+ if (action.type === "flow.select") {
30
+ if (action.key) {
31
+ controller.selectFlowKey(action.key);
32
+ }
33
+ else {
34
+ controller.selectFlowIndex(action.index ?? 0);
35
+ }
36
+ return;
37
+ }
38
+ if (action.type === "folder.toggle") {
39
+ controller.toggleFolderKey(action.key);
40
+ return;
41
+ }
42
+ if (action.type === "run.openConfirm") {
43
+ await controller.openRunConfirm(action.flowId, action.key);
44
+ return;
45
+ }
46
+ if (action.type === "confirm.select") {
47
+ controller.selectConfirmAction(action.action);
48
+ return;
49
+ }
50
+ if (action.type === "confirm.accept") {
51
+ if (action.action) {
52
+ controller.selectConfirmAction(action.action);
53
+ }
54
+ await controller.acceptConfirmation();
55
+ return;
56
+ }
57
+ if (action.type === "confirm.cancel") {
58
+ controller.cancelConfirmation();
59
+ return;
60
+ }
61
+ if (action.type === "form.update") {
62
+ controller.updateActiveFormValues(action.values);
63
+ return;
64
+ }
65
+ if (action.type === "form.fieldUpdate") {
66
+ controller.updateFormField(action.fieldId, action.value);
67
+ return;
68
+ }
69
+ if (action.type === "form.submit") {
70
+ controller.submitForm(action.values);
71
+ return;
72
+ }
73
+ if (action.type === "form.cancel") {
74
+ controller.cancelForm();
75
+ return;
76
+ }
77
+ if (action.type === "flow.interrupt") {
78
+ await controller.interruptCurrentFlow(action.flowId);
79
+ return;
80
+ }
81
+ if (action.type === "interrupt.openConfirm") {
82
+ controller.openInterruptConfirm();
83
+ return;
84
+ }
85
+ if (action.type === "log.clear") {
86
+ controller.clearLog();
87
+ return;
88
+ }
89
+ if (action.type === "help.toggle") {
90
+ controller.showHelp(action.visible ?? !controller.getViewModel().helpVisible);
91
+ return;
92
+ }
93
+ controller.scrollPane(action.pane, { ...(action.delta !== undefined ? { delta: action.delta } : {}), ...(action.offset !== undefined ? { offset: action.offset } : {}) });
94
+ }
95
+ catch (error) {
96
+ const message = error.message;
97
+ controller.appendLog(`Web action failed: ${message}`);
98
+ sendError(client, message, actionId(action));
99
+ }
100
+ }
101
+ return {
102
+ mount() {
103
+ if (mounted) {
104
+ return;
105
+ }
106
+ mounted = true;
107
+ controller.mount();
108
+ unsubscribe = controller.subscribe((event) => {
109
+ if (event.type === "log") {
110
+ server?.broadcast({ type: "log.append", appendedLines: event.appendedLines });
111
+ return;
112
+ }
113
+ server?.broadcast(snapshot());
114
+ });
115
+ void startWebServer({
116
+ ...(webOptions.noOpen !== undefined ? { noOpen: webOptions.noOpen } : {}),
117
+ ...(webOptions.host !== undefined ? { host: webOptions.host } : {}),
118
+ ...(webOptions.auth !== undefined ? { auth: webOptions.auth } : {}),
119
+ printInfo: (message) => {
120
+ webOptions.printInfo?.(message);
121
+ controller.appendLog(message);
122
+ },
123
+ ...(webOptions.openBrowser ? { openBrowser: webOptions.openBrowser } : {}),
124
+ onClientAction: (action, client) => {
125
+ void dispatch(action, client);
126
+ },
127
+ onClientConnected: (client) => {
128
+ client.send(snapshot());
129
+ },
130
+ onExitRequested: () => {
131
+ options.onExit();
132
+ },
133
+ }).then((started) => {
134
+ if (shuttingDown) {
135
+ void started.close().catch((error) => {
136
+ process.stderr.write(`Failed to close Web UI server: ${error.message}\n`);
137
+ });
138
+ return;
139
+ }
140
+ server = started;
141
+ webOptions.onServerReady?.(started);
142
+ }).catch((error) => {
143
+ const message = `Web UI startup failed: ${error.message}`;
144
+ controller.appendLog(message);
145
+ writeSync(process.stderr.fd, `${message}\n`);
146
+ options.onExit();
147
+ });
148
+ },
149
+ destroy() {
150
+ if (shuttingDown) {
151
+ return;
152
+ }
153
+ shuttingDown = true;
154
+ controller.interruptActiveForm("Web UI session closed.");
155
+ unsubscribe?.();
156
+ unsubscribe = null;
157
+ const closePromise = server?.close();
158
+ server = null;
159
+ if (closePromise) {
160
+ void closePromise.catch((error) => {
161
+ process.stderr.write(`Failed to close Web UI server: ${error.message}\n`);
162
+ });
163
+ }
164
+ controller.destroy();
165
+ },
166
+ requestUserInput: (form) => controller.requestUserInput(form),
167
+ setSummary: (markdown) => controller.setSummary(markdown),
168
+ clearSummary: () => controller.clearSummary(),
169
+ setScope: (scopeKey, jiraIssueKey, gitBranchName) => controller.setScope(scopeKey, jiraIssueKey, gitBranchName),
170
+ appendLog: (text) => controller.appendLog(text),
171
+ setFlowFailed: (flowId) => controller.setFlowFailed(flowId),
172
+ interruptActiveForm: (message = "Flow interrupted by user.") => {
173
+ controller.interruptActiveForm(message);
174
+ if (message) {
175
+ controller.appendLog(new FlowInterruptedError(message).message);
176
+ }
177
+ },
178
+ };
179
+ }