agentweaver 0.1.16 → 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 (74) hide show
  1. package/README.md +148 -27
  2. package/dist/artifacts.js +114 -3
  3. package/dist/doctor/checks/executors.js +2 -2
  4. package/dist/flow-state.js +138 -1
  5. package/dist/index.js +421 -82
  6. package/dist/interactive/controller.js +305 -36
  7. package/dist/interactive/ink/index.js +24 -3
  8. package/dist/interactive/state.js +1 -0
  9. package/dist/interactive/tree.js +2 -2
  10. package/dist/interactive/web/index.js +179 -0
  11. package/dist/interactive/web/protocol.js +154 -0
  12. package/dist/interactive/web/server.js +575 -0
  13. package/dist/interactive/web/static/app.js +709 -0
  14. package/dist/interactive/web/static/index.html +77 -0
  15. package/dist/interactive/web/static/styles.css +2 -0
  16. package/dist/interactive/web/static/styles.input.css +469 -0
  17. package/dist/pipeline/auto-flow.js +9 -6
  18. package/dist/pipeline/context.js +6 -5
  19. package/dist/pipeline/declarative-flows.js +39 -20
  20. package/dist/pipeline/flow-catalog.js +40 -14
  21. package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
  22. package/dist/pipeline/flow-specs/auto-common.json +4 -1
  23. package/dist/pipeline/flow-specs/auto-golang.json +27 -1
  24. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +15 -1
  25. package/dist/pipeline/flow-specs/design-review.json +2 -0
  26. package/dist/pipeline/flow-specs/implement.json +3 -1
  27. package/dist/pipeline/flow-specs/plan.json +8 -2
  28. package/dist/pipeline/flow-specs/playbook-init.json +199 -0
  29. package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
  30. package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
  31. package/dist/pipeline/flow-specs/review/review.json +2 -0
  32. package/dist/pipeline/launch-profile-config.js +30 -18
  33. package/dist/pipeline/node-contract.js +1 -0
  34. package/dist/pipeline/node-registry.js +119 -5
  35. package/dist/pipeline/nodes/flow-run-node.js +200 -173
  36. package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
  37. package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
  38. package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
  39. package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
  40. package/dist/pipeline/nodes/playbook-write-node.js +243 -0
  41. package/dist/pipeline/nodes/project-guidance-node.js +69 -0
  42. package/dist/pipeline/plugin-loader.js +389 -0
  43. package/dist/pipeline/plugin-types.js +1 -0
  44. package/dist/pipeline/prompt-registry.js +4 -1
  45. package/dist/pipeline/prompt-runtime.js +6 -2
  46. package/dist/pipeline/registry.js +71 -4
  47. package/dist/pipeline/spec-compiler.js +1 -0
  48. package/dist/pipeline/spec-loader.js +14 -0
  49. package/dist/pipeline/spec-types.js +19 -0
  50. package/dist/pipeline/spec-validator.js +6 -0
  51. package/dist/pipeline/value-resolver.js +41 -2
  52. package/dist/playbook/practice-candidates.js +12 -0
  53. package/dist/playbook/repo-inventory.js +208 -0
  54. package/dist/plugin-sdk.js +1 -0
  55. package/dist/prompts.js +31 -0
  56. package/dist/runtime/artifact-registry.js +3 -0
  57. package/dist/runtime/execution-routing.js +25 -19
  58. package/dist/runtime/interactive-execution-routing.js +66 -57
  59. package/dist/runtime/playbook.js +485 -0
  60. package/dist/runtime/project-guidance.js +339 -0
  61. package/dist/structured-artifact-schema-registry.js +8 -0
  62. package/dist/structured-artifact-schemas.json +235 -0
  63. package/dist/structured-artifacts.js +7 -1
  64. package/docs/declarative-workflows.md +565 -0
  65. package/docs/example/.flows/examples/claude-example.json +50 -0
  66. package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
  67. package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
  68. package/docs/examples/.flows/claude-example.json +50 -0
  69. package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
  70. package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
  71. package/docs/features.md +77 -0
  72. package/docs/playbook.md +327 -0
  73. package/docs/plugin-sdk.md +731 -0
  74. package/package.json +13 -4
@@ -23,6 +23,8 @@ const HELP_TEXT = renderMarkdownToTerminal([
23
23
  "q / Ctrl+C exit",
24
24
  ].join("\n"));
25
25
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
26
+ const SPINNER_INTERVAL_MS = 200;
27
+ const LOG_FLUSH_INTERVAL_MS = 120;
26
28
  function clamp(value, min, max) {
27
29
  return Math.min(max, Math.max(min, value));
28
30
  }
@@ -180,6 +182,7 @@ export class InteractiveSessionController {
180
182
  currentOptionIndex: initialOptionIndex,
181
183
  currentTextCursorIndex: initialCursorIndex,
182
184
  previewScrollOffset: 0,
185
+ validationError: null,
183
186
  resolve,
184
187
  reject,
185
188
  };
@@ -204,9 +207,12 @@ export class InteractiveSessionController {
204
207
  }
205
208
  this.emitChange();
206
209
  }
207
- setScope(scopeKey, jiraIssueKey) {
210
+ setScope(scopeKey, jiraIssueKey, gitBranchName) {
208
211
  this.state.scopeKey = scopeKey;
209
212
  this.state.jiraIssueKey = jiraIssueKey ?? null;
213
+ if (gitBranchName !== undefined) {
214
+ this.state.gitBranchName = gitBranchName;
215
+ }
210
216
  this.emitChange();
211
217
  }
212
218
  appendLog(text) {
@@ -304,7 +310,18 @@ export class InteractiveSessionController {
304
310
  selectFlowIndex(index) {
305
311
  const selectedItem = this.visibleFlowItems[index];
306
312
  if (!selectedItem) {
307
- return;
313
+ throw new Error(`Invalid flow index: ${index}`);
314
+ }
315
+ this.state.selectedFlowItemKey = selectedItem.key;
316
+ if (selectedItem.kind === "flow") {
317
+ this.state.selectedFlowId = selectedItem.flow.id;
318
+ }
319
+ this.emitChange();
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}`);
308
325
  }
309
326
  this.state.selectedFlowItemKey = selectedItem.key;
310
327
  if (selectedItem.kind === "flow") {
@@ -312,6 +329,199 @@ export class InteractiveSessionController {
312
329
  }
313
330
  this.emitChange();
314
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
+ }
315
525
  getViewModel(layout) {
316
526
  const selectedItem = this.selectedFlowTreeItem();
317
527
  const activeFlowId = this.activeFlowId();
@@ -335,6 +545,10 @@ export class InteractiveSessionController {
335
545
  flowItems: this.visibleFlowItems.map((item) => ({
336
546
  key: item.key,
337
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) } : {}),
338
552
  })),
339
553
  selectedFlowIndex: Math.max(0, this.visibleFlowItems.findIndex((item) => item.key === this.state.selectedFlowItemKey)),
340
554
  progressTitle: this.panelTitle("Current Flow", "progress"),
@@ -350,6 +564,7 @@ export class InteractiveSessionController {
350
564
  logText: this.logText,
351
565
  logScrollOffset: this.state.logScrollOffset,
352
566
  confirmText: this.renderConfirmText(),
567
+ confirmation: this.renderConfirmationView(),
353
568
  form: this.renderFormView(layout),
354
569
  };
355
570
  }
@@ -393,7 +608,7 @@ export class InteractiveSessionController {
393
608
  const current = this.state.currentFlowId ?? selectHeaderLabel(this.selectedFlowTreeItem(), this.state.selectedFlowId);
394
609
  const pathParts = this.options.cwd.split(path.sep).filter(Boolean);
395
610
  const folderName = pathParts.slice(-3).join("/") || this.options.cwd;
396
- const branchLabel = this.options.gitBranchName ? this.options.gitBranchName : "detached-head";
611
+ const branchLabel = this.state.gitBranchName ? this.state.gitBranchName : "detached-head";
397
612
  const runningSuffix = this.state.busy ? " [running]" : "";
398
613
  const versionLabel = this.state.version ? ` | Version ${this.state.version}` : "";
399
614
  const jiraLabel = this.state.jiraIssueKey ? ` | Jira ${this.state.jiraIssueKey}` : "";
@@ -428,7 +643,8 @@ export class InteractiveSessionController {
428
643
  return "Flow structure is not available.";
429
644
  }
430
645
  if (selectedItem.kind === "folder") {
431
- const kindLabel = selectedItem.pathSegments[0] === "custom" ? "project-local" : "built-in";
646
+ const rootName = selectedItem.pathSegments[0];
647
+ const kindLabel = rootName === "custom" ? "project-local" : rootName === "global" ? "global" : "built-in";
432
648
  return [
433
649
  `Flow folder '${selectedItem.pathSegments.join("/")}'.`,
434
650
  "",
@@ -440,8 +656,8 @@ export class InteractiveSessionController {
440
656
  const description = flow.description?.trim() || "No description available for this flow.";
441
657
  const details = [
442
658
  `Path: ${flow.treePath.join("/")}`,
443
- `Source: ${flow.source === "project-local" ? "project-local" : "built-in"}`,
444
- flow.source === "project-local" && flow.sourcePath ? `File: ${flow.sourcePath}` : "",
659
+ `Source: ${flow.source === "project-local" ? "project-local" : flow.source === "global" ? "global" : "built-in"}`,
660
+ flow.source !== "built-in" && flow.sourcePath ? `File: ${flow.sourcePath}` : "",
445
661
  ]
446
662
  .filter((line) => line.length > 0)
447
663
  .join("\n");
@@ -488,11 +704,13 @@ export class InteractiveSessionController {
488
704
  ? "Stop"
489
705
  : action === "resume"
490
706
  ? "Resume"
491
- : action === "restart"
492
- ? "Restart"
493
- : action === "ok"
494
- ? "OK"
495
- : "Cancel";
707
+ : action === "continue"
708
+ ? "Continue"
709
+ : action === "restart"
710
+ ? "Restart"
711
+ : action === "ok"
712
+ ? "OK"
713
+ : "Cancel";
496
714
  return session.selectedAction === action ? `[ ${label} ]` : ` ${label} `;
497
715
  })
498
716
  .join(" ");
@@ -507,6 +725,20 @@ export class InteractiveSessionController {
507
725
  lines.push("", actionLabels, "", "Left/Right or Tab: choose Enter: confirm Esc: cancel");
508
726
  return lines.join("\n");
509
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
+ }
510
742
  renderFormView(layout) {
511
743
  const session = this.activeFormSession;
512
744
  const field = this.currentFormField();
@@ -568,6 +800,12 @@ export class InteractiveSessionController {
568
800
  title: "User Input",
569
801
  content: lines.join("\n"),
570
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,
571
809
  };
572
810
  }
573
811
  currentFormField() {
@@ -604,7 +842,7 @@ export class InteractiveSessionController {
604
842
  return;
605
843
  }
606
844
  if (key.name === "enter") {
607
- await this.acceptConfirm();
845
+ await this.acceptActiveConfirm();
608
846
  }
609
847
  }
610
848
  async handleFlowKey(key) {
@@ -656,27 +894,27 @@ export class InteractiveSessionController {
656
894
  const maxOffset = this.panelMaxScroll(panel);
657
895
  const current = this.scrollOffsetFor(panel);
658
896
  if (key.name === "up") {
659
- this.setScrollOffset(panel, current - 1, maxOffset);
897
+ this.applyScrollOffset(panel, current - 1, maxOffset);
660
898
  return;
661
899
  }
662
900
  if (key.name === "down") {
663
- this.setScrollOffset(panel, current + 1, maxOffset);
901
+ this.applyScrollOffset(panel, current + 1, maxOffset);
664
902
  return;
665
903
  }
666
904
  if (key.name === "pageup") {
667
- this.setScrollOffset(panel, current - 10, maxOffset);
905
+ this.applyScrollOffset(panel, current - 10, maxOffset);
668
906
  return;
669
907
  }
670
908
  if (key.name === "pagedown") {
671
- this.setScrollOffset(panel, current + 10, maxOffset);
909
+ this.applyScrollOffset(panel, current + 10, maxOffset);
672
910
  return;
673
911
  }
674
912
  if (key.name === "home") {
675
- this.setScrollOffset(panel, 0, maxOffset);
913
+ this.applyScrollOffset(panel, 0, maxOffset);
676
914
  return;
677
915
  }
678
916
  if (key.name === "end") {
679
- this.setScrollOffset(panel, maxOffset, maxOffset);
917
+ this.applyScrollOffset(panel, maxOffset, maxOffset);
680
918
  }
681
919
  }
682
920
  moveSelectedFlow(delta) {
@@ -759,10 +997,20 @@ export class InteractiveSessionController {
759
997
  this.confirmSession = {
760
998
  kind: "run",
761
999
  flowId: selectedItem.flow.id,
762
- resumeAvailable: confirmation.resumeAvailable,
763
- hasExistingState: confirmation.hasExistingState,
1000
+ availability: {
1001
+ hasExistingState: confirmation.hasExistingState,
1002
+ resume: confirmation.resume.available,
1003
+ continue: confirmation.continue.available,
1004
+ restart: confirmation.restart.available,
1005
+ },
764
1006
  details: confirmation.details ?? null,
765
- selectedAction: confirmation.resumeAvailable ? "resume" : confirmation.hasExistingState ? "restart" : "ok",
1007
+ selectedAction: confirmation.resume.available
1008
+ ? "resume"
1009
+ : confirmation.continue.available
1010
+ ? "continue"
1011
+ : confirmation.restart.available
1012
+ ? "restart"
1013
+ : "ok",
766
1014
  };
767
1015
  this.emitChange();
768
1016
  }
@@ -774,8 +1022,12 @@ export class InteractiveSessionController {
774
1022
  this.confirmSession = {
775
1023
  kind: "interrupt",
776
1024
  flowId,
777
- resumeAvailable: true,
778
- hasExistingState: true,
1025
+ availability: {
1026
+ hasExistingState: true,
1027
+ resume: true,
1028
+ continue: false,
1029
+ restart: false,
1030
+ },
779
1031
  details: "The current flow will be stopped. State will be saved and can be continued via Resume.",
780
1032
  selectedAction: "stop",
781
1033
  };
@@ -791,8 +1043,12 @@ export class InteractiveSessionController {
791
1043
  this.confirmSession = {
792
1044
  kind: "exit",
793
1045
  flowId: null,
794
- resumeAvailable: false,
795
- hasExistingState: false,
1046
+ availability: {
1047
+ hasExistingState: false,
1048
+ resume: false,
1049
+ continue: false,
1050
+ restart: false,
1051
+ },
796
1052
  details,
797
1053
  selectedAction: "ok",
798
1054
  };
@@ -808,11 +1064,17 @@ export class InteractiveSessionController {
808
1064
  if (this.confirmSession.kind === "exit") {
809
1065
  return ["ok", "cancel"];
810
1066
  }
811
- return this.confirmSession.resumeAvailable
812
- ? ["resume", "restart", "cancel"]
813
- : this.confirmSession.hasExistingState
814
- ? ["restart", "cancel"]
815
- : ["ok", "cancel"];
1067
+ const actions = [];
1068
+ if (this.confirmSession.availability.resume) {
1069
+ actions.push("resume");
1070
+ }
1071
+ if (this.confirmSession.availability.continue) {
1072
+ actions.push("continue");
1073
+ }
1074
+ if (this.confirmSession.availability.restart) {
1075
+ actions.push("restart");
1076
+ }
1077
+ return actions.length > 0 ? [...actions, "cancel"] : ["ok", "cancel"];
816
1078
  }
817
1079
  moveConfirmSelection(delta) {
818
1080
  if (!this.confirmSession) {
@@ -824,7 +1086,7 @@ export class InteractiveSessionController {
824
1086
  this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
825
1087
  this.emitChange();
826
1088
  }
827
- async acceptConfirm() {
1089
+ async acceptActiveConfirm() {
828
1090
  const session = this.confirmSession;
829
1091
  if (!session) {
830
1092
  return;
@@ -850,7 +1112,11 @@ export class InteractiveSessionController {
850
1112
  return;
851
1113
  }
852
1114
  const flowId = session.flowId ?? this.state.selectedFlowId;
853
- const launchMode = session.selectedAction === "resume" ? "resume" : "restart";
1115
+ const launchMode = session.selectedAction === "resume"
1116
+ ? "resume"
1117
+ : session.selectedAction === "continue"
1118
+ ? "continue"
1119
+ : "restart";
854
1120
  this.confirmSession = null;
855
1121
  this.setBusy(true, flowId);
856
1122
  this.clearFlowFailure(flowId);
@@ -895,7 +1161,7 @@ export class InteractiveSessionController {
895
1161
  this.spinnerTimer = setInterval(() => {
896
1162
  this.state.spinnerFrame = (this.state.spinnerFrame + 1) % SPINNER_FRAMES.length;
897
1163
  this.emitChange();
898
- }, 120);
1164
+ }, SPINNER_INTERVAL_MS);
899
1165
  return;
900
1166
  }
901
1167
  if (!running && this.spinnerTimer) {
@@ -945,7 +1211,7 @@ export class InteractiveSessionController {
945
1211
  }
946
1212
  return this.state.logScrollOffset;
947
1213
  }
948
- setScrollOffset(panel, value, maxOffset) {
1214
+ applyScrollOffset(panel, value, maxOffset) {
949
1215
  const next = clamp(value, 0, maxOffset);
950
1216
  if (panel === "progress") {
951
1217
  this.state.progressScrollOffset = next;
@@ -1067,6 +1333,7 @@ export class InteractiveSessionController {
1067
1333
  }
1068
1334
  this.syncActiveSelectFieldValue();
1069
1335
  try {
1336
+ session.validationError = null;
1070
1337
  validateUserInputValues(session.form, session.values);
1071
1338
  const result = {
1072
1339
  formId: session.form.formId,
@@ -1079,7 +1346,9 @@ export class InteractiveSessionController {
1079
1346
  this.emitChange();
1080
1347
  }
1081
1348
  catch (error) {
1082
- this.appendLog(error.message);
1349
+ session.validationError = error.message;
1350
+ this.appendLog(session.validationError);
1351
+ this.emitChange();
1083
1352
  }
1084
1353
  }
1085
1354
  cancelActiveForm() {
@@ -1278,7 +1547,7 @@ export class InteractiveSessionController {
1278
1547
  this.logFlushTimer = setTimeout(() => {
1279
1548
  this.logFlushTimer = null;
1280
1549
  this.flushPendingLogLines();
1281
- }, 50);
1550
+ }, LOG_FLUSH_INTERVAL_MS);
1282
1551
  }
1283
1552
  flushPendingLogLines() {
1284
1553
  if (this.pendingLogLines.length === 0) {
@@ -370,14 +370,35 @@ function createInkApp(react, ink, controller) {
370
370
  const { Fragment, createElement, useEffect, useState } = react;
371
371
  const { Box, Text, useInput, useStdout } = ink;
372
372
  const Panel = createPanelComponent(react, ink);
373
+ const LOG_REPAINT_DEBOUNCE_MS = 100;
373
374
  const App = () => {
374
375
  const [, setVersion] = useState(0);
375
376
  useEffect(() => {
376
- const unsubscribe = controller.subscribe(() => {
377
+ let logRepaintTimer = null;
378
+ const flushRepaint = () => {
379
+ if (logRepaintTimer) {
380
+ clearTimeout(logRepaintTimer);
381
+ logRepaintTimer = null;
382
+ }
377
383
  setVersion((previous) => previous + 1);
384
+ };
385
+ const unsubscribe = controller.subscribe((event) => {
386
+ if (event.type === "log") {
387
+ if (logRepaintTimer) {
388
+ return;
389
+ }
390
+ logRepaintTimer = setTimeout(() => {
391
+ flushRepaint();
392
+ }, LOG_REPAINT_DEBOUNCE_MS);
393
+ return;
394
+ }
395
+ flushRepaint();
378
396
  });
379
397
  controller.mount();
380
398
  return () => {
399
+ if (logRepaintTimer) {
400
+ clearTimeout(logRepaintTimer);
401
+ }
381
402
  unsubscribe();
382
403
  controller.destroy();
383
404
  };
@@ -541,8 +562,8 @@ class InkInteractiveSession {
541
562
  clearSummary() {
542
563
  this.controller.clearSummary();
543
564
  }
544
- setScope(scopeKey, jiraIssueKey) {
545
- this.controller.setScope(scopeKey, jiraIssueKey);
565
+ setScope(scopeKey, jiraIssueKey, gitBranchName) {
566
+ this.controller.setScope(scopeKey, jiraIssueKey, gitBranchName);
546
567
  }
547
568
  appendLog(text) {
548
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),
@@ -75,7 +75,7 @@ export function buildFlowTree(flows) {
75
75
  children: sortNodes(node.children),
76
76
  }
77
77
  : node);
78
- const orderedRootNames = ["custom", "default"];
78
+ const orderedRootNames = ["global", "custom", "default"];
79
79
  const sortedRoots = [...roots.values()].sort((left, right) => {
80
80
  const leftIndex = orderedRootNames.indexOf(left.name);
81
81
  const rightIndex = orderedRootNames.indexOf(right.name);
@@ -143,7 +143,7 @@ export function collectInitiallyExpandedFolderKeys(flowTree) {
143
143
  if (node.kind !== "folder") {
144
144
  continue;
145
145
  }
146
- const expandedByDefault = node.pathSegments.length === 1 && node.name === "default";
146
+ const expandedByDefault = node.pathSegments.length === 1 && (node.name === "default" || node.name === "global");
147
147
  if (expandedByDefault) {
148
148
  keys.push(node.key);
149
149
  }