agentweaver 0.1.11 → 0.1.12

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.
@@ -15,6 +15,28 @@ function makeFolderKey(pathSegments) {
15
15
  function makeFlowKey(flowId) {
16
16
  return `flow:${flowId}`;
17
17
  }
18
+ function escapeBlessedTags(text) {
19
+ return text.replace(/[{}]/g, (ch) => (ch === "{" ? "{open}" : "{close}"));
20
+ }
21
+ function textIndexToLineColumn(value, index) {
22
+ const boundedIndex = Math.max(0, Math.min(value.length, index));
23
+ const beforeCursor = value.slice(0, boundedIndex);
24
+ const lines = beforeCursor.split("\n");
25
+ return {
26
+ line: Math.max(0, lines.length - 1),
27
+ column: (lines[lines.length - 1] ?? "").length,
28
+ };
29
+ }
30
+ function textLineColumnToIndex(value, line, column) {
31
+ const lines = value.split("\n");
32
+ const boundedLine = Math.max(0, Math.min(lines.length - 1, line));
33
+ let index = 0;
34
+ for (let currentLine = 0; currentLine < boundedLine; currentLine += 1) {
35
+ index += (lines[currentLine] ?? "").length + 1;
36
+ }
37
+ const targetLine = lines[boundedLine] ?? "";
38
+ return index + Math.max(0, Math.min(targetLine.length, column));
39
+ }
18
40
  function buildFlowTree(flows) {
19
41
  const roots = new Map();
20
42
  const ensureFolder = (pathSegments) => {
@@ -114,6 +136,11 @@ export class InteractiveUi {
114
136
  help;
115
137
  confirm;
116
138
  formModal;
139
+ formLineInput;
140
+ formTextInput;
141
+ formBooleanInput;
142
+ formSelectInput;
143
+ formHint;
117
144
  flowMap;
118
145
  flowTree;
119
146
  expandedFlowFolders = new Set();
@@ -283,6 +310,86 @@ export class InteractiveUi {
283
310
  fg: "white",
284
311
  },
285
312
  });
313
+ this.formLineInput = blessed.box({
314
+ parent: this.formModal,
315
+ top: 0,
316
+ left: 0,
317
+ width: "100%-2",
318
+ height: 3,
319
+ hidden: true,
320
+ tags: true,
321
+ border: "line",
322
+ style: {
323
+ border: { fg: "cyan" },
324
+ fg: "white",
325
+ },
326
+ });
327
+ this.formTextInput = blessed.box({
328
+ parent: this.formModal,
329
+ top: 0,
330
+ left: 0,
331
+ width: "100%-2",
332
+ height: 3,
333
+ hidden: true,
334
+ tags: true,
335
+ mouse: true,
336
+ border: "line",
337
+ style: {
338
+ border: { fg: "cyan" },
339
+ fg: "white",
340
+ },
341
+ });
342
+ this.formBooleanInput = blessed.checkbox({
343
+ parent: this.formModal,
344
+ top: 0,
345
+ left: 0,
346
+ width: "100%-2",
347
+ height: 3,
348
+ hidden: true,
349
+ mouse: true,
350
+ keys: true,
351
+ vi: true,
352
+ border: "line",
353
+ style: {
354
+ border: { fg: "cyan" },
355
+ fg: "white",
356
+ },
357
+ });
358
+ this.formSelectInput = blessed.list({
359
+ parent: this.formModal,
360
+ top: 0,
361
+ left: 0,
362
+ width: "100%-2",
363
+ height: 8,
364
+ hidden: true,
365
+ tags: true,
366
+ mouse: true,
367
+ keys: true,
368
+ vi: true,
369
+ border: "line",
370
+ scrollable: true,
371
+ alwaysScroll: true,
372
+ style: {
373
+ border: { fg: "cyan" },
374
+ fg: "white",
375
+ selected: {
376
+ fg: "black",
377
+ bg: "green",
378
+ },
379
+ },
380
+ });
381
+ this.formHint = blessed.box({
382
+ parent: this.formModal,
383
+ top: 0,
384
+ left: 1,
385
+ width: "100%-4",
386
+ height: 2,
387
+ hidden: true,
388
+ tags: true,
389
+ style: {
390
+ fg: "gray",
391
+ },
392
+ });
286
393
  this.description = blessed.box({
287
394
  parent: this.screen,
288
395
  bottom: 6,
@@ -749,20 +856,6 @@ export class InteractiveUi {
749
856
  }
750
857
  return this.activeFormSession.form.fields[this.activeFormSession.currentFieldIndex] ?? null;
751
858
  }
752
- renderTextInputValue(value, placeholder, rows = 1) {
753
- const rawLines = (value || placeholder || "Введите текст").split("\n");
754
- const visibleLines = rawLines.slice(0, Math.max(1, rows));
755
- const contentWidth = visibleLines.reduce((max, line) => Math.max(max, line.length), 0);
756
- const frameWidth = Math.max(36, contentWidth + 6);
757
- const innerWidth = Math.max(32, frameWidth - 4);
758
- const renderedRows = Array.from({ length: Math.max(1, rows) }, (_, index) => {
759
- const rawLine = visibleLines[index] ?? "";
760
- const visibleText = rawLine.length > innerWidth - 2 ? `${rawLine.slice(0, innerWidth - 5)}...` : rawLine;
761
- const color = value ? "white-fg" : "gray-fg";
762
- return `{cyan-fg}│{/cyan-fg}{black-bg} ${index === 0 ? "{green-fg}>{/green-fg}" : " "} {${color}}${visibleText.padEnd(innerWidth - 2, " ")}{/${color}} {/black-bg}{cyan-fg}│{/cyan-fg}`;
763
- });
764
- return [`{cyan-fg}┌${"─".repeat(frameWidth - 2)}┐{/cyan-fg}`, ...renderedRows, `{cyan-fg}└${"─".repeat(frameWidth - 2)}┘{/cyan-fg}`];
765
- }
766
859
  formModalInnerHeight() {
767
860
  const rawHeight = typeof this.formModal.height === "number" ? this.formModal.height : this.formModal?.lpos?.yi
768
861
  ? this.formModal.lpos.yl - this.formModal.lpos.yi + 1
@@ -779,91 +872,14 @@ export class InteractiveUi {
779
872
  const paddingRight = Number(this.formModal.padding?.right ?? 0);
780
873
  return Math.max(24, rawWidth - 2 - paddingLeft - paddingRight);
781
874
  }
782
- wrapFormText(text, width) {
783
- const normalized = text.trim();
784
- if (!normalized) {
785
- return [""];
786
- }
787
- const wrapped = [];
788
- for (const paragraph of normalized.split("\n")) {
789
- if (!paragraph.trim()) {
790
- wrapped.push("");
791
- continue;
792
- }
793
- let remaining = paragraph.trim();
794
- while (remaining.length > width) {
795
- let splitAt = remaining.lastIndexOf(" ", width);
796
- if (splitAt <= 0) {
797
- splitAt = width;
798
- }
799
- wrapped.push(remaining.slice(0, splitAt).trimEnd());
800
- remaining = remaining.slice(splitAt).trimStart();
801
- }
802
- wrapped.push(remaining);
803
- }
804
- return wrapped.length > 0 ? wrapped : [""];
805
- }
806
- renderSelectableFieldWindow(field, value, currentOptionIndex, availableLines) {
807
- const contentWidth = this.formModalInnerWidth();
808
- const firstLineWidth = Math.max(12, contentWidth - 6);
809
- const continuationWidth = Math.max(8, contentWidth - 6);
810
- const descriptionWidth = Math.max(8, contentWidth - 4);
811
- const renderedOptions = field.options.map((option, index) => {
812
- const isCursor = index === currentOptionIndex;
813
- const isSelected = field.type === "single-select"
814
- ? value === option.value
815
- : Array.isArray(value) && value.includes(option.value);
816
- const cursor = isCursor ? "{cyan-fg}>{/cyan-fg}" : " ";
817
- const marker = isSelected ? "[x]" : "[ ]";
818
- const labelLines = this.wrapFormText(option.label, firstLineWidth);
819
- const itemLines = [`${cursor} ${marker} ${labelLines[0] ?? ""}`];
820
- for (const continuation of labelLines.slice(1)) {
821
- itemLines.push(` ${continuation.slice(0, continuationWidth)}`);
822
- }
823
- if (option.description?.trim()) {
824
- for (const descriptionLine of this.wrapFormText(option.description, descriptionWidth)) {
825
- itemLines.push(` {gray-fg}${descriptionLine}{/gray-fg}`);
826
- }
827
- }
828
- return itemLines;
829
- });
830
- if (renderedOptions.length === 0) {
831
- return ["{gray-fg}Нет доступных вариантов.{/gray-fg}"];
832
- }
833
- let startIndex = 0;
834
- let selectedStartLine = 0;
835
- for (let index = 0; index < currentOptionIndex; index += 1) {
836
- selectedStartLine += renderedOptions[index]?.length ?? 0;
837
- }
838
- let visibleLines = 0;
839
- for (let index = 0; index < renderedOptions.length; index += 1) {
840
- const itemHeight = renderedOptions[index]?.length ?? 0;
841
- if (index < currentOptionIndex && selectedStartLine + itemHeight > availableLines) {
842
- startIndex = index + 1;
843
- selectedStartLine -= itemHeight;
844
- }
845
- }
846
- const output = [];
847
- for (let index = startIndex; index < renderedOptions.length; index += 1) {
848
- const itemLines = renderedOptions[index] ?? [];
849
- if (output.length > 0 && output.length + itemLines.length > availableLines) {
850
- break;
851
- }
852
- if (output.length === 0 && itemLines.length > availableLines) {
853
- output.push(...itemLines.slice(0, availableLines));
854
- break;
855
- }
856
- output.push(...itemLines);
857
- visibleLines += itemLines.length;
858
- if (visibleLines >= availableLines) {
859
- break;
860
- }
861
- }
862
- return output;
863
- }
864
875
  renderActiveForm() {
865
876
  if (!this.activeFormSession) {
866
877
  this.formModal.hide();
878
+ this.hideFormLineInput();
879
+ this.hideFormTextInput();
880
+ this.hideFormBooleanInput();
881
+ this.hideFormSelectInput();
882
+ this.hideFormHint();
867
883
  this.footer.setContent(" Up/Down: select | Left/Right: fold | Enter: toggle/run | Esc: close/interrupt | h: help | Tab: switch pane | q: exit ");
868
884
  this.requestRender();
869
885
  return;
@@ -873,6 +889,7 @@ export class InteractiveUi {
873
889
  if (!field) {
874
890
  return;
875
891
  }
892
+ const isLastField = session.currentFieldIndex >= session.form.fields.length - 1;
876
893
  const headerLines = [`{bold}${session.form.title}{/bold}`];
877
894
  if (session.form.description?.trim()) {
878
895
  headerLines.push("");
@@ -885,58 +902,321 @@ export class InteractiveUi {
885
902
  headerLines.push(field.help.trim());
886
903
  }
887
904
  headerLines.push("");
888
- const footerLines = ["", "{green-fg}Ctrl+S{/green-fg}: submit", "{magenta-fg}Shift+Tab{/magenta-fg}: previous field", "{red-fg}Esc{/red-fg}: cancel"];
905
+ const helperLines = [];
889
906
  const lines = [...headerLines];
907
+ let footerHint = ` Form: Enter ${isLastField ? "submit" : "confirm"} | Tab next | Shift+Tab prev | Esc cancel `;
908
+ let hintLines = [];
890
909
  if (field.type === "boolean") {
910
+ this.hideFormLineInput();
911
+ this.hideFormTextInput();
912
+ this.hideFormSelectInput();
913
+ helperLines.push("{cyan-fg}Use the checkbox below.{/cyan-fg}");
914
+ this.ensureFormBooleanInputVisible(headerLines.length + helperLines.length, 0);
891
915
  const current = session.values[field.id] === true;
892
- lines.push(`${current ? "[x]" : "[ ]"} ${field.label}`);
893
- lines.push("");
894
- lines.push("Space: toggle");
895
- lines.push("Enter/Tab: next field");
916
+ this.formBooleanInput.setText(field.label);
917
+ if (current) {
918
+ this.formBooleanInput.check();
919
+ }
920
+ else {
921
+ this.formBooleanInput.uncheck();
922
+ }
923
+ hintLines = [
924
+ `Space: toggle | Enter: ${isLastField ? "submit" : "confirm and next"}`,
925
+ "Tab/Shift+Tab: switch field | Esc: cancel",
926
+ ];
927
+ footerHint = ` Form: Space toggle | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel `;
896
928
  }
897
929
  else if (field.type === "text") {
930
+ this.hideFormBooleanInput();
931
+ this.hideFormSelectInput();
932
+ helperLines.push("{cyan-fg}Use the standard editor below.{/cyan-fg}");
898
933
  const current = String(session.values[field.id] ?? "");
899
- lines.push(...this.renderTextInputValue(current, field.placeholder, field.multiline ? Math.max(1, field.rows ?? 3) : 1));
900
- lines.push("");
901
- lines.push(field.multiline ? "Type text, Enter: new line, Backspace: delete" : "Type text, Backspace: delete");
902
- lines.push("Tab: next field");
934
+ if (field.multiline) {
935
+ this.hideFormLineInput();
936
+ this.ensureFormTextInputVisible(field, headerLines.length + helperLines.length, 0);
937
+ session.currentTextCursorIndex = Math.max(0, Math.min(current.length, session.currentTextCursorIndex));
938
+ this.renderFormMultilineInput(current || field.placeholder || "", current.length === 0 && Boolean(field.placeholder));
939
+ hintLines = [
940
+ "Enter: newline | Tab/Shift+Tab: switch field",
941
+ `Ctrl+S: submit | Esc: cancel`,
942
+ ];
943
+ footerHint = " Form: Enter newline | Tab switch | Ctrl+S submit | Esc cancel ";
944
+ }
945
+ else {
946
+ this.hideFormTextInput();
947
+ this.ensureFormLineInputVisible(headerLines.length + helperLines.length, 0);
948
+ session.currentTextCursorIndex = Math.max(0, Math.min(current.length, session.currentTextCursorIndex));
949
+ this.renderFormLineInput(current);
950
+ hintLines = [
951
+ `Left/Right/Home/End: move | Enter: ${isLastField ? "submit" : "confirm and next"}`,
952
+ "Tab/Shift+Tab: switch field | Esc: cancel",
953
+ ];
954
+ footerHint = ` Form: Type text | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel `;
955
+ }
903
956
  }
904
957
  else {
905
- const currentOptionIndex = Math.min(session.currentOptionIndex, Math.max(0, field.options.length - 1));
958
+ this.hideFormLineInput();
959
+ this.hideFormTextInput();
960
+ this.hideFormBooleanInput();
961
+ helperLines.push("{cyan-fg}Use the list below.{/cyan-fg}");
962
+ this.ensureFormSelectInputVisible(headerLines.length + helperLines.length, 0);
963
+ const preferredOptionIndex = field.type === "single-select"
964
+ ? this.selectedOptionIndexForField(field)
965
+ : session.currentOptionIndex;
966
+ const currentOptionIndex = Math.min(preferredOptionIndex, Math.max(0, field.options.length - 1));
906
967
  session.currentOptionIndex = currentOptionIndex;
907
- const availableLines = Math.max(3, this.formModalInnerHeight() - headerLines.length - footerLines.length - 3);
908
- lines.push(...this.renderSelectableFieldWindow(field, session.values[field.id], currentOptionIndex, availableLines));
909
- lines.push("");
910
- lines.push("Up/Down: move");
911
- lines.push("Space: select/toggle");
912
- lines.push("Enter/Tab: next field");
913
- }
914
- lines.push(...footerLines);
968
+ const selectedValues = field.type === "single-select"
969
+ ? [String(session.values[field.id] ?? "")]
970
+ : Array.isArray(session.values[field.id]) ? session.values[field.id] : [];
971
+ const items = field.options.map((option) => {
972
+ const isSelected = selectedValues.includes(option.value);
973
+ const marker = isSelected ? "[x]" : "[ ]";
974
+ return `${marker} ${option.label}`;
975
+ });
976
+ this.formSelectInput.setItems(items);
977
+ this.formSelectInput.select(currentOptionIndex);
978
+ hintLines = [
979
+ `Up/Down: move | Space: ${field.type === "single-select" ? "pick" : "toggle"} | Enter: ${isLastField ? "submit" : "confirm and next"}`,
980
+ "Tab/Shift+Tab: switch field | Esc: cancel",
981
+ ];
982
+ footerHint = ` Form: Up/Down move | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel `;
983
+ }
984
+ lines.push(...helperLines);
915
985
  this.formModal.setContent(lines.join("\n"));
916
986
  this.formModal.setScroll(0);
917
987
  this.formModal.show();
918
988
  this.formModal.setFront();
919
- this.formModal.focus();
920
- this.footer.setContent(" Form: Space select | Tab next | Shift+Tab prev | Ctrl+S submit | Esc cancel ");
989
+ if (field.type === "text") {
990
+ if (field.multiline) {
991
+ this.formTextInput.focus();
992
+ }
993
+ else {
994
+ this.formLineInput.focus();
995
+ }
996
+ }
997
+ else if (field.type === "boolean") {
998
+ this.formBooleanInput.focus();
999
+ }
1000
+ else {
1001
+ this.formSelectInput.focus();
1002
+ }
1003
+ this.showFormHint(hintLines);
1004
+ this.footer.setContent(footerHint);
921
1005
  this.requestRender();
922
1006
  }
923
1007
  moveActiveFormField(delta) {
924
1008
  if (!this.activeFormSession) {
925
1009
  return;
926
1010
  }
1011
+ this.syncActiveTextFieldValue();
1012
+ this.syncActiveBooleanFieldValue();
1013
+ this.syncActiveSelectFieldValue();
927
1014
  const nextIndex = Math.min(this.activeFormSession.form.fields.length - 1, Math.max(0, this.activeFormSession.currentFieldIndex + delta));
928
1015
  this.activeFormSession.currentFieldIndex = nextIndex;
929
- this.activeFormSession.currentOptionIndex = 0;
1016
+ const nextField = this.activeFormSession.form.fields[nextIndex];
1017
+ if (nextField?.type === "text") {
1018
+ const current = String(this.activeFormSession.values[nextField.id] ?? "");
1019
+ this.activeFormSession.currentTextCursorIndex = current.length;
1020
+ this.activeFormSession.currentOptionIndex = 0;
1021
+ }
1022
+ else if (nextField?.type === "single-select" || nextField?.type === "multi-select") {
1023
+ this.activeFormSession.currentTextCursorIndex = 0;
1024
+ this.activeFormSession.currentOptionIndex = this.selectedOptionIndexForField(nextField);
1025
+ }
1026
+ else {
1027
+ this.activeFormSession.currentTextCursorIndex = 0;
1028
+ this.activeFormSession.currentOptionIndex = 0;
1029
+ }
930
1030
  this.renderActiveForm();
931
1031
  }
932
- moveActiveFormOption(delta) {
1032
+ selectedOptionIndexForField(field) {
1033
+ const session = this.activeFormSession;
1034
+ if (!session || field.options.length === 0) {
1035
+ return 0;
1036
+ }
1037
+ if (field.type === "single-select") {
1038
+ const selectedValue = String(session.values[field.id] ?? "");
1039
+ const selectedIndex = field.options.findIndex((option) => option.value === selectedValue);
1040
+ return selectedIndex >= 0 ? selectedIndex : 0;
1041
+ }
1042
+ const selectedValues = Array.isArray(session.values[field.id]) ? session.values[field.id] : [];
1043
+ const selectedIndex = field.options.findIndex((option) => selectedValues.includes(option.value));
1044
+ return selectedIndex >= 0 ? selectedIndex : 0;
1045
+ }
1046
+ confirmActiveFormField() {
1047
+ const session = this.activeFormSession;
1048
+ if (!session) {
1049
+ return;
1050
+ }
1051
+ this.syncActiveTextFieldValue();
1052
+ this.syncActiveBooleanFieldValue();
1053
+ this.syncActiveSelectFieldValue();
1054
+ if (session.currentFieldIndex >= session.form.fields.length - 1) {
1055
+ this.submitActiveForm();
1056
+ return;
1057
+ }
1058
+ this.moveActiveFormField(1);
1059
+ }
1060
+ ensureFormLineInputVisible(headerLineCount, footerLineCount) {
1061
+ const reservedTop = Math.max(1, headerLineCount + 2);
1062
+ const availableHeight = Math.max(5, this.formModalInnerHeight() - reservedTop - footerLineCount - 2);
1063
+ this.formLineInput.top = reservedTop;
1064
+ this.formLineInput.left = 1;
1065
+ this.formLineInput.width = Math.max(24, this.formModalInnerWidth() - 4);
1066
+ this.formLineInput.height = Math.min(3, availableHeight);
1067
+ this.formLineInput.show();
1068
+ this.formLineInput.setFront();
1069
+ this.formLineInput.focus();
1070
+ this.positionFormHint(this.formLineInput.top + this.formLineInput.height);
1071
+ }
1072
+ ensureFormTextInputVisible(field, headerLineCount, footerLineCount) {
1073
+ const reservedTop = Math.max(1, headerLineCount + 2);
1074
+ const availableHeight = Math.max(5, this.formModalInnerHeight() - reservedTop - footerLineCount - 2);
1075
+ const desiredRows = field.multiline ? Math.max(3, field.rows ?? 3) : 1;
1076
+ const inputHeight = Math.min(Math.max(3, availableHeight - 2), desiredRows + 2);
1077
+ this.formTextInput.top = reservedTop;
1078
+ this.formTextInput.left = 1;
1079
+ this.formTextInput.width = Math.max(24, this.formModalInnerWidth() - 4);
1080
+ this.formTextInput.height = inputHeight;
1081
+ this.formTextInput.show();
1082
+ this.formTextInput.setFront();
1083
+ this.formTextInput.focus();
1084
+ this.positionFormHint(this.formTextInput.top + this.formTextInput.height);
1085
+ }
1086
+ renderFormLineInput(value) {
1087
+ const session = this.activeFormSession;
1088
+ if (!session) {
1089
+ return;
1090
+ }
1091
+ const totalWidth = typeof this.formLineInput.width === "number" ? this.formLineInput.width : this.formModalInnerWidth();
1092
+ const innerWidth = Math.max(1, totalWidth - 2);
1093
+ const cursorIndex = Math.max(0, Math.min(value.length, session.currentTextCursorIndex));
1094
+ session.currentTextCursorIndex = cursorIndex;
1095
+ const visibleStart = Math.max(0, cursorIndex - innerWidth + 1);
1096
+ const visibleValue = value.slice(visibleStart, visibleStart + innerWidth);
1097
+ const cursorOffset = cursorIndex - visibleStart;
1098
+ const beforeCursor = escapeBlessedTags(visibleValue.slice(0, Math.max(0, cursorOffset)));
1099
+ const cursorChar = visibleValue[cursorOffset] ?? " ";
1100
+ const afterCursor = escapeBlessedTags(visibleValue.slice(Math.min(visibleValue.length, cursorOffset + 1)));
1101
+ this.formLineInput.setContent(`${beforeCursor}{inverse}${escapeBlessedTags(cursorChar)}{/inverse}${afterCursor}`);
1102
+ }
1103
+ renderFormMultilineInput(value, dimmed = false) {
1104
+ const session = this.activeFormSession;
1105
+ if (!session) {
1106
+ return;
1107
+ }
1108
+ const totalWidth = typeof this.formTextInput.width === "number" ? this.formTextInput.width : this.formModalInnerWidth();
1109
+ const totalHeight = typeof this.formTextInput.height === "number" ? this.formTextInput.height : 3;
1110
+ const innerWidth = Math.max(1, totalWidth - 2);
1111
+ const innerHeight = Math.max(1, totalHeight - 2);
1112
+ const cursorIndex = Math.max(0, Math.min(value.length, session.currentTextCursorIndex));
1113
+ session.currentTextCursorIndex = cursorIndex;
1114
+ const cursor = textIndexToLineColumn(value, cursorIndex);
1115
+ const sourceLines = value.split("\n");
1116
+ const visibleStartLine = Math.max(0, cursor.line - innerHeight + 1);
1117
+ const visibleLines = sourceLines.slice(visibleStartLine, visibleStartLine + innerHeight);
1118
+ const renderedLines = visibleLines.map((lineText, visibleLineIndex) => {
1119
+ const absoluteLineIndex = visibleStartLine + visibleLineIndex;
1120
+ const isCursorLine = absoluteLineIndex === cursor.line;
1121
+ const startColumn = isCursorLine && cursor.column >= innerWidth ? cursor.column - innerWidth + 1 : 0;
1122
+ const visibleText = lineText.slice(startColumn, startColumn + innerWidth);
1123
+ if (!isCursorLine) {
1124
+ return escapeBlessedTags(visibleText);
1125
+ }
1126
+ const cursorOffset = cursor.column - startColumn;
1127
+ const beforeCursor = escapeBlessedTags(visibleText.slice(0, Math.max(0, cursorOffset)));
1128
+ const cursorChar = visibleText[cursorOffset] ?? " ";
1129
+ const afterCursor = escapeBlessedTags(visibleText.slice(Math.min(visibleText.length, cursorOffset + 1)));
1130
+ return `${beforeCursor}{inverse}${escapeBlessedTags(cursorChar)}{/inverse}${afterCursor}`;
1131
+ });
1132
+ const rendered = renderedLines.join("\n");
1133
+ this.formTextInput.setContent(dimmed ? `{gray-fg}${rendered}{/gray-fg}` : rendered);
1134
+ }
1135
+ ensureFormBooleanInputVisible(headerLineCount, footerLineCount) {
1136
+ const reservedTop = Math.max(1, headerLineCount + 2);
1137
+ const availableHeight = Math.max(5, this.formModalInnerHeight() - reservedTop - footerLineCount - 2);
1138
+ this.formBooleanInput.top = reservedTop;
1139
+ this.formBooleanInput.left = 1;
1140
+ this.formBooleanInput.width = Math.max(24, this.formModalInnerWidth() - 4);
1141
+ this.formBooleanInput.height = Math.min(4, availableHeight);
1142
+ this.formBooleanInput.show();
1143
+ this.formBooleanInput.setFront();
1144
+ this.formBooleanInput.focus();
1145
+ this.positionFormHint(this.formBooleanInput.top + this.formBooleanInput.height);
1146
+ }
1147
+ ensureFormSelectInputVisible(headerLineCount, footerLineCount) {
1148
+ const reservedTop = Math.max(1, headerLineCount + 2);
1149
+ const availableHeight = Math.max(6, this.formModalInnerHeight() - reservedTop - footerLineCount - 2);
1150
+ this.formSelectInput.top = reservedTop;
1151
+ this.formSelectInput.left = 1;
1152
+ this.formSelectInput.width = Math.max(24, this.formModalInnerWidth() - 4);
1153
+ this.formSelectInput.height = Math.max(4, availableHeight - 2);
1154
+ this.formSelectInput.show();
1155
+ this.formSelectInput.setFront();
1156
+ this.formSelectInput.focus();
1157
+ this.positionFormHint(this.formSelectInput.top + this.formSelectInput.height);
1158
+ }
1159
+ positionFormHint(top) {
1160
+ this.formHint.top = top;
1161
+ this.formHint.left = 1;
1162
+ this.formHint.width = Math.max(24, this.formModalInnerWidth() - 4);
1163
+ this.formHint.height = 2;
1164
+ }
1165
+ showFormHint(lines) {
1166
+ this.formHint.setContent(lines.join("\n"));
1167
+ this.formHint.show();
1168
+ this.formHint.setFront();
1169
+ }
1170
+ hideFormLineInput() {
1171
+ this.formLineInput.hide();
1172
+ if (typeof this.formLineInput.blur === "function") {
1173
+ this.formLineInput.blur();
1174
+ }
1175
+ }
1176
+ hideFormTextInput() {
1177
+ this.formTextInput.hide();
1178
+ if (typeof this.formTextInput.blur === "function") {
1179
+ this.formTextInput.blur();
1180
+ }
1181
+ }
1182
+ hideFormBooleanInput() {
1183
+ this.formBooleanInput.hide();
1184
+ if (typeof this.formBooleanInput.blur === "function") {
1185
+ this.formBooleanInput.blur();
1186
+ }
1187
+ }
1188
+ hideFormSelectInput() {
1189
+ this.formSelectInput.hide();
1190
+ if (typeof this.formSelectInput.blur === "function") {
1191
+ this.formSelectInput.blur();
1192
+ }
1193
+ }
1194
+ hideFormHint() {
1195
+ this.formHint.hide();
1196
+ }
1197
+ syncActiveTextFieldValue() {
1198
+ const session = this.activeFormSession;
933
1199
  const field = this.currentFormField();
934
- if (!this.activeFormSession || !field || (field.type !== "single-select" && field.type !== "multi-select")) {
1200
+ if (!session || !field || field.type !== "text") {
935
1201
  return;
936
1202
  }
937
- const nextIndex = Math.min(field.options.length - 1, Math.max(0, this.activeFormSession.currentOptionIndex + delta));
938
- this.activeFormSession.currentOptionIndex = nextIndex;
939
- this.renderActiveForm();
1203
+ }
1204
+ syncActiveBooleanFieldValue() {
1205
+ const session = this.activeFormSession;
1206
+ const field = this.currentFormField();
1207
+ if (!session || !field || field.type !== "boolean") {
1208
+ return;
1209
+ }
1210
+ session.values[field.id] = this.formBooleanInput.checked === true;
1211
+ }
1212
+ syncActiveSelectFieldValue() {
1213
+ const session = this.activeFormSession;
1214
+ const field = this.currentFormField();
1215
+ if (!session || !field || (field.type !== "single-select" && field.type !== "multi-select")) {
1216
+ return;
1217
+ }
1218
+ const selectedIndex = this.formSelectInput.selected ?? session.currentOptionIndex;
1219
+ session.currentOptionIndex = Math.max(0, Math.min(field.options.length - 1, selectedIndex));
940
1220
  }
941
1221
  toggleActiveFormValue() {
942
1222
  const session = this.activeFormSession;
@@ -949,6 +1229,7 @@ export class InteractiveUi {
949
1229
  this.renderActiveForm();
950
1230
  return;
951
1231
  }
1232
+ this.syncActiveSelectFieldValue();
952
1233
  if (field.type === "single-select") {
953
1234
  const option = field.options[session.currentOptionIndex];
954
1235
  if (!option) {
@@ -971,33 +1252,13 @@ export class InteractiveUi {
971
1252
  this.renderActiveForm();
972
1253
  }
973
1254
  }
974
- appendActiveFormText(ch, key, appendNewline = false) {
975
- const session = this.activeFormSession;
976
- const field = this.currentFormField();
977
- if (!session || !field || field.type !== "text") {
978
- return;
979
- }
980
- const current = String(session.values[field.id] ?? "");
981
- if (key.name === "backspace") {
982
- session.values[field.id] = current.slice(0, -1);
983
- this.renderActiveForm();
984
- return;
985
- }
986
- if (appendNewline) {
987
- session.values[field.id] = `${current}\n`;
988
- this.renderActiveForm();
989
- return;
990
- }
991
- if (key.ctrl || key.meta || !ch || ch === "\r" || ch === "\n" || ch === "\t") {
992
- return;
993
- }
994
- session.values[field.id] = `${current}${ch}`;
995
- this.renderActiveForm();
996
- }
997
1255
  submitActiveForm() {
998
1256
  if (!this.activeFormSession) {
999
1257
  return;
1000
1258
  }
1259
+ this.syncActiveTextFieldValue();
1260
+ this.syncActiveBooleanFieldValue();
1261
+ this.syncActiveSelectFieldValue();
1001
1262
  const session = this.activeFormSession;
1002
1263
  try {
1003
1264
  validateUserInputValues(session.form, session.values);
@@ -1008,6 +1269,10 @@ export class InteractiveUi {
1008
1269
  };
1009
1270
  this.activeFormSession = null;
1010
1271
  this.formModal.hide();
1272
+ this.hideFormLineInput();
1273
+ this.hideFormTextInput();
1274
+ this.hideFormBooleanInput();
1275
+ this.hideFormSelectInput();
1011
1276
  this.focusPane("flows");
1012
1277
  session.resolve(result);
1013
1278
  this.renderActiveForm();
@@ -1024,6 +1289,10 @@ export class InteractiveUi {
1024
1289
  const session = this.activeFormSession;
1025
1290
  this.activeFormSession = null;
1026
1291
  this.formModal.hide();
1292
+ this.hideFormLineInput();
1293
+ this.hideFormTextInput();
1294
+ this.hideFormBooleanInput();
1295
+ this.hideFormSelectInput();
1027
1296
  this.focusPane("flows");
1028
1297
  session.reject(new TaskRunnerError(`User cancelled form '${session.form.formId}'.`));
1029
1298
  this.renderActiveForm();
@@ -1035,6 +1304,10 @@ export class InteractiveUi {
1035
1304
  const session = this.activeFormSession;
1036
1305
  this.activeFormSession = null;
1037
1306
  this.formModal.hide();
1307
+ this.hideFormLineInput();
1308
+ this.hideFormTextInput();
1309
+ this.hideFormBooleanInput();
1310
+ this.hideFormSelectInput();
1038
1311
  this.focusPane("flows");
1039
1312
  session.reject(new FlowInterruptedError(message));
1040
1313
  this.renderActiveForm();
@@ -1061,16 +1334,153 @@ export class InteractiveUi {
1061
1334
  return;
1062
1335
  }
1063
1336
  if (field.type === "text") {
1337
+ if (!field.multiline) {
1338
+ const session = this.activeFormSession;
1339
+ if (!session) {
1340
+ return;
1341
+ }
1342
+ const current = String(session.values[field.id] ?? "");
1343
+ if (key.name === "enter") {
1344
+ this.confirmActiveFormField();
1345
+ return;
1346
+ }
1347
+ if (key.name === "left") {
1348
+ session.currentTextCursorIndex = Math.max(0, session.currentTextCursorIndex - 1);
1349
+ this.renderFormLineInput(current);
1350
+ this.requestRender();
1351
+ return;
1352
+ }
1353
+ if (key.name === "right") {
1354
+ session.currentTextCursorIndex = Math.min(current.length, session.currentTextCursorIndex + 1);
1355
+ this.renderFormLineInput(current);
1356
+ this.requestRender();
1357
+ return;
1358
+ }
1359
+ if (key.name === "home") {
1360
+ session.currentTextCursorIndex = 0;
1361
+ this.renderFormLineInput(current);
1362
+ this.requestRender();
1363
+ return;
1364
+ }
1365
+ if (key.name === "end") {
1366
+ session.currentTextCursorIndex = current.length;
1367
+ this.renderFormLineInput(current);
1368
+ this.requestRender();
1369
+ return;
1370
+ }
1371
+ if (key.name === "backspace") {
1372
+ if (session.currentTextCursorIndex > 0) {
1373
+ const nextValue = `${current.slice(0, session.currentTextCursorIndex - 1)}${current.slice(session.currentTextCursorIndex)}`;
1374
+ session.currentTextCursorIndex -= 1;
1375
+ session.values[field.id] = nextValue;
1376
+ this.renderFormLineInput(nextValue);
1377
+ this.requestRender();
1378
+ }
1379
+ return;
1380
+ }
1381
+ if (key.name === "delete") {
1382
+ if (session.currentTextCursorIndex < current.length) {
1383
+ const nextValue = `${current.slice(0, session.currentTextCursorIndex)}${current.slice(session.currentTextCursorIndex + 1)}`;
1384
+ session.values[field.id] = nextValue;
1385
+ this.renderFormLineInput(nextValue);
1386
+ this.requestRender();
1387
+ }
1388
+ return;
1389
+ }
1390
+ if (ch && !key.ctrl && !key.meta && !/^[\x00-\x1f\x7f]$/.test(ch)) {
1391
+ const nextValue = `${current.slice(0, session.currentTextCursorIndex)}${ch}${current.slice(session.currentTextCursorIndex)}`;
1392
+ session.currentTextCursorIndex += ch.length;
1393
+ session.values[field.id] = nextValue;
1394
+ this.renderFormLineInput(nextValue);
1395
+ this.requestRender();
1396
+ }
1397
+ return;
1398
+ }
1064
1399
  if (key.name === "enter") {
1065
- if (field.multiline) {
1066
- this.appendActiveFormText(ch, key, true);
1400
+ const session = this.activeFormSession;
1401
+ if (!session) {
1402
+ return;
1403
+ }
1404
+ const current = String(session.values[field.id] ?? "");
1405
+ const nextValue = `${current.slice(0, session.currentTextCursorIndex)}\n${current.slice(session.currentTextCursorIndex)}`;
1406
+ session.currentTextCursorIndex += 1;
1407
+ session.values[field.id] = nextValue;
1408
+ this.renderFormMultilineInput(nextValue);
1409
+ this.requestRender();
1410
+ return;
1411
+ }
1412
+ const session = this.activeFormSession;
1413
+ if (!session) {
1414
+ return;
1415
+ }
1416
+ const current = String(session.values[field.id] ?? "");
1417
+ if (key.name === "left") {
1418
+ session.currentTextCursorIndex = Math.max(0, session.currentTextCursorIndex - 1);
1419
+ this.renderFormMultilineInput(current);
1420
+ this.requestRender();
1421
+ return;
1422
+ }
1423
+ if (key.name === "right") {
1424
+ session.currentTextCursorIndex = Math.min(current.length, session.currentTextCursorIndex + 1);
1425
+ this.renderFormMultilineInput(current);
1426
+ this.requestRender();
1427
+ return;
1428
+ }
1429
+ if (key.name === "home") {
1430
+ const { line } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1431
+ session.currentTextCursorIndex = textLineColumnToIndex(current, line, 0);
1432
+ this.renderFormMultilineInput(current);
1433
+ this.requestRender();
1434
+ return;
1435
+ }
1436
+ if (key.name === "end") {
1437
+ const { line } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1438
+ const lineText = current.split("\n")[line] ?? "";
1439
+ session.currentTextCursorIndex = textLineColumnToIndex(current, line, lineText.length);
1440
+ this.renderFormMultilineInput(current);
1441
+ this.requestRender();
1442
+ return;
1443
+ }
1444
+ if (key.name === "up") {
1445
+ const { line, column } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1446
+ session.currentTextCursorIndex = textLineColumnToIndex(current, Math.max(0, line - 1), column);
1447
+ this.renderFormMultilineInput(current);
1448
+ this.requestRender();
1449
+ return;
1450
+ }
1451
+ if (key.name === "down") {
1452
+ const { line, column } = textIndexToLineColumn(current, session.currentTextCursorIndex);
1453
+ session.currentTextCursorIndex = textLineColumnToIndex(current, line + 1, column);
1454
+ this.renderFormMultilineInput(current);
1455
+ this.requestRender();
1456
+ return;
1457
+ }
1458
+ if (key.name === "backspace") {
1459
+ if (session.currentTextCursorIndex > 0) {
1460
+ const nextValue = `${current.slice(0, session.currentTextCursorIndex - 1)}${current.slice(session.currentTextCursorIndex)}`;
1461
+ session.currentTextCursorIndex -= 1;
1462
+ session.values[field.id] = nextValue;
1463
+ this.renderFormMultilineInput(nextValue);
1464
+ this.requestRender();
1067
1465
  }
1068
- else {
1069
- this.moveActiveFormField(1);
1466
+ return;
1467
+ }
1468
+ if (key.name === "delete") {
1469
+ if (session.currentTextCursorIndex < current.length) {
1470
+ const nextValue = `${current.slice(0, session.currentTextCursorIndex)}${current.slice(session.currentTextCursorIndex + 1)}`;
1471
+ session.values[field.id] = nextValue;
1472
+ this.renderFormMultilineInput(nextValue);
1473
+ this.requestRender();
1070
1474
  }
1071
1475
  return;
1072
1476
  }
1073
- this.appendActiveFormText(ch, key);
1477
+ if (ch && !key.ctrl && !key.meta && !/^[\x00-\x1f\x7f]$/.test(ch)) {
1478
+ const nextValue = `${current.slice(0, session.currentTextCursorIndex)}${ch}${current.slice(session.currentTextCursorIndex)}`;
1479
+ session.currentTextCursorIndex += ch.length;
1480
+ session.values[field.id] = nextValue;
1481
+ this.renderFormMultilineInput(nextValue);
1482
+ this.requestRender();
1483
+ }
1074
1484
  return;
1075
1485
  }
1076
1486
  if (field.type === "boolean") {
@@ -1079,27 +1489,28 @@ export class InteractiveUi {
1079
1489
  return;
1080
1490
  }
1081
1491
  if (key.name === "enter") {
1082
- this.moveActiveFormField(1);
1492
+ this.confirmActiveFormField();
1083
1493
  }
1084
1494
  return;
1085
1495
  }
1086
1496
  if (key.name === "up") {
1087
- this.moveActiveFormOption(-1);
1497
+ this.syncActiveSelectFieldValue();
1498
+ this.requestRender();
1088
1499
  return;
1089
1500
  }
1090
1501
  if (key.name === "down") {
1091
- this.moveActiveFormOption(1);
1502
+ this.syncActiveSelectFieldValue();
1503
+ this.requestRender();
1092
1504
  return;
1093
1505
  }
1094
1506
  if (key.name === "space") {
1507
+ this.syncActiveSelectFieldValue();
1095
1508
  this.toggleActiveFormValue();
1096
1509
  return;
1097
1510
  }
1098
1511
  if (key.name === "enter") {
1099
- if (field.type === "single-select") {
1100
- this.toggleActiveFormValue();
1101
- }
1102
- this.moveActiveFormField(1);
1512
+ this.syncActiveSelectFieldValue();
1513
+ this.confirmActiveFormField();
1103
1514
  }
1104
1515
  }
1105
1516
  renderDescription() {
@@ -1544,11 +1955,20 @@ export class InteractiveUi {
1544
1955
  });
1545
1956
  }
1546
1957
  return new Promise((resolve, reject) => {
1958
+ const values = buildInitialUserInputValues(form.fields);
1959
+ const firstField = form.fields[0];
1960
+ const initialCursorIndex = firstField?.type === "text" ? String(values[firstField.id] ?? "").length : 0;
1961
+ const initialOptionIndex = firstField?.type === "single-select"
1962
+ ? Math.max(0, firstField.options.findIndex((option) => option.value === String(values[firstField.id] ?? "")))
1963
+ : firstField?.type === "multi-select"
1964
+ ? Math.max(0, firstField.options.findIndex((option) => Array.isArray(values[firstField.id]) && values[firstField.id].includes(option.value)))
1965
+ : 0;
1547
1966
  this.activeFormSession = {
1548
1967
  form,
1549
- values: buildInitialUserInputValues(form.fields),
1968
+ values,
1550
1969
  currentFieldIndex: 0,
1551
- currentOptionIndex: 0,
1970
+ currentOptionIndex: initialOptionIndex,
1971
+ currentTextCursorIndex: initialCursorIndex,
1552
1972
  resolve,
1553
1973
  reject,
1554
1974
  };