agentweaver 0.1.11 → 0.1.13
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.
- package/LICENSE +21 -0
- package/README.md +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/doctor/checks/agentweaver-home.js +57 -0
- package/dist/doctor/checks/category.js +9 -0
- package/dist/doctor/checks/cwd-context.js +69 -0
- package/dist/doctor/checks/env-diagnostics.js +171 -0
- package/dist/doctor/checks/executors.js +106 -0
- package/dist/doctor/checks/flow-readiness.js +305 -0
- package/dist/doctor/checks/node-version.js +79 -0
- package/dist/doctor/checks/register.js +18 -0
- package/dist/doctor/checks/system.js +91 -0
- package/dist/doctor/index.js +4 -0
- package/dist/doctor/orchestrator.js +78 -0
- package/dist/doctor/registry.js +50 -0
- package/dist/doctor/runner.js +89 -0
- package/dist/doctor/types.js +12 -0
- package/dist/executors/codex-executor.js +2 -1
- package/dist/executors/jira-fetch-executor.js +1 -0
- package/dist/executors/opencode-executor.js +22 -11
- package/dist/executors/process-executor.js +3 -0
- package/dist/index.js +59 -35
- package/dist/interactive-ui.js +579 -159
- package/dist/jira.js +57 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +105 -21
- package/dist/pipeline/flow-specs/review/review-fix.json +1 -0
- package/dist/pipeline/flow-specs/review/review-loop.json +2 -0
- package/dist/pipeline/flow-specs/task-describe.json +64 -3
- package/dist/pipeline/nodes/jira-fetch-node.js +3 -0
- package/dist/pipeline/nodes/review-findings-form-node.js +33 -3
- package/dist/pipeline/nodes/user-input-node.js +18 -4
- package/dist/pipeline/prompt-registry.js +2 -1
- package/dist/pipeline/spec-types.js +2 -0
- package/dist/pipeline/value-resolver.js +11 -1
- package/dist/prompts.js +17 -2
- package/dist/runtime/process-runner.js +9 -3
- package/dist/structured-artifact-schema-registry.js +1 -0
- package/dist/structured-artifact-schemas.json +22 -0
- package/dist/user-input.js +8 -1
- package/package.json +4 -2
- package/dist/pipeline/flow-model-settings.js +0 -77
package/dist/interactive-ui.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
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
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
920
|
-
|
|
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.
|
|
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
|
-
|
|
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 (!
|
|
1200
|
+
if (!session || !field || field.type !== "text") {
|
|
935
1201
|
return;
|
|
936
1202
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
this.
|
|
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
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
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.
|
|
1492
|
+
this.confirmActiveFormField();
|
|
1083
1493
|
}
|
|
1084
1494
|
return;
|
|
1085
1495
|
}
|
|
1086
1496
|
if (key.name === "up") {
|
|
1087
|
-
this.
|
|
1497
|
+
this.syncActiveSelectFieldValue();
|
|
1498
|
+
this.requestRender();
|
|
1088
1499
|
return;
|
|
1089
1500
|
}
|
|
1090
1501
|
if (key.name === "down") {
|
|
1091
|
-
this.
|
|
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
|
-
|
|
1100
|
-
|
|
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
|
|
1968
|
+
values,
|
|
1550
1969
|
currentFieldIndex: 0,
|
|
1551
|
-
currentOptionIndex:
|
|
1970
|
+
currentOptionIndex: initialOptionIndex,
|
|
1971
|
+
currentTextCursorIndex: initialCursorIndex,
|
|
1552
1972
|
resolve,
|
|
1553
1973
|
reject,
|
|
1554
1974
|
};
|