@fadhelhaidar/pi-ask-user 1.0.4 → 1.1.0
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/README.md +4 -2
- package/dist/chunks/{index-x7fb685n.js → index-n7wxhdmh.js} +77 -1
- package/dist/chunks/index-pen3t92h.js +7 -0
- package/dist/chunks/{questionnaire-session-j2wmt696.js → questionnaire-session-m053x4ka.js} +238 -190
- package/dist/events.d.ts +78 -0
- package/dist/events.js +6 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +570 -74
- package/dist/locales/de.json +26 -29
- package/dist/locales/en.json +31 -27
- package/dist/locales/es.json +26 -29
- package/dist/locales/fr.json +26 -29
- package/dist/locales/pt-BR.json +26 -29
- package/dist/locales/pt.json +26 -29
- package/dist/locales/ru.json +26 -29
- package/dist/locales/uk.json +26 -29
- package/dist/locales/zh.json +28 -31
- package/docs/configuration.md +14 -0
- package/docs/hosts.md +48 -4
- package/docs/keyboard.md +33 -13
- package/docs/localization.md +7 -2
- package/docs/tool-schema.md +38 -6
- package/package.json +1 -1
- package/dist/chunks/index-bx2tbz93.js +0 -5
|
@@ -1,22 +1,113 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_COLLAPSE_KEY,
|
|
3
3
|
COLLAPSE_KEY_OFF,
|
|
4
|
+
DEFAULT_NOTES_KEY,
|
|
5
|
+
KEY_PLACEHOLDER,
|
|
6
|
+
formatNotesKeyForDisplay,
|
|
4
7
|
formatKeySpecForDisplay,
|
|
5
8
|
ROW_INTENT_META,
|
|
6
9
|
t,
|
|
7
10
|
displayLabel,
|
|
8
|
-
formatAnswerScalar
|
|
9
|
-
|
|
11
|
+
formatAnswerScalar,
|
|
12
|
+
BORDER_VERTICAL_OVERHEAD,
|
|
13
|
+
BORDER_HORIZONTAL_OVERHEAD,
|
|
14
|
+
BORDER_INNER_PADDING_HORIZONTAL,
|
|
15
|
+
stripFenceMarkers,
|
|
16
|
+
renderBorderedBox,
|
|
17
|
+
computeBoxDimensions
|
|
18
|
+
} from "./index-n7wxhdmh.js";
|
|
10
19
|
|
|
11
20
|
// view/dialog-builder.ts
|
|
12
21
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { Container as Container2, Spacer as Spacer2 } from "@earendil-works/pi-tui";
|
|
22
|
+
import { Container as Container2, CURSOR_MARKER, Spacer as Spacer2 } from "@earendil-works/pi-tui";
|
|
23
|
+
|
|
24
|
+
// view/scroll-window.ts
|
|
25
|
+
var OVERFLOW_UP = "↑";
|
|
26
|
+
var OVERFLOW_DOWN = "↓";
|
|
27
|
+
var OVERFLOW_BOTH = "↕";
|
|
28
|
+
function computeScrollStart(bodyRange, rowsBeforeBody, availableMiddle, middleRows) {
|
|
29
|
+
if (!bodyRange)
|
|
30
|
+
return 0;
|
|
31
|
+
const focusedRowInMiddle = rowsBeforeBody + bodyRange[0];
|
|
32
|
+
const focusedHeight = bodyRange[1] - bodyRange[0];
|
|
33
|
+
const idealStart = focusedRowInMiddle - Math.floor(Math.max(0, availableMiddle - focusedHeight) / 2);
|
|
34
|
+
return Math.max(0, Math.min(idealStart, middleRows - availableMiddle));
|
|
35
|
+
}
|
|
36
|
+
function decorateOverflow(scrollableMiddle, hasUp, hasDown, theme, protectedRows) {
|
|
37
|
+
const drawable = (row) => !protectedRows || row < protectedRows[0] || row >= protectedRows[1];
|
|
38
|
+
if (hasUp && hasDown && scrollableMiddle.length === 1) {
|
|
39
|
+
if (drawable(0))
|
|
40
|
+
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_BOTH);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (hasUp && scrollableMiddle.length > 0 && drawable(0)) {
|
|
44
|
+
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_UP);
|
|
45
|
+
}
|
|
46
|
+
const last = scrollableMiddle.length - 1;
|
|
47
|
+
if (hasDown && last > 0 && drawable(last)) {
|
|
48
|
+
scrollableMiddle[last] = theme.fg("dim", OVERFLOW_DOWN);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
14
51
|
|
|
15
52
|
// view/tab-content-strategy.ts
|
|
16
53
|
import { Container, Spacer, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
54
|
+
|
|
55
|
+
// state/selectors/derivations.ts
|
|
56
|
+
function selectConfirmedIndicator(questions, currentTab, answers, items) {
|
|
57
|
+
const q = questions[currentTab];
|
|
58
|
+
if (!q || q.multiSelect === true)
|
|
59
|
+
return;
|
|
60
|
+
const prior = answers.get(currentTab);
|
|
61
|
+
if (!prior)
|
|
62
|
+
return;
|
|
63
|
+
if (prior.kind === "custom") {
|
|
64
|
+
const otherIndex = items.findIndex((it) => it.kind === "other");
|
|
65
|
+
if (otherIndex < 0)
|
|
66
|
+
return;
|
|
67
|
+
return { index: otherIndex, labelOverride: prior.answer ?? "" };
|
|
68
|
+
}
|
|
69
|
+
if (prior.kind !== "option" || typeof prior.answer !== "string")
|
|
70
|
+
return;
|
|
71
|
+
const index = items.findIndex((it) => it.kind === "option" && it.label === prior.answer);
|
|
72
|
+
if (index < 0)
|
|
73
|
+
return;
|
|
74
|
+
return { index };
|
|
75
|
+
}
|
|
76
|
+
function selectSavedNote(notesByTab, answers, tab) {
|
|
77
|
+
const note = notesByTab.get(tab) ?? answers.get(tab)?.notes;
|
|
78
|
+
return note && note.length > 0 ? note : undefined;
|
|
79
|
+
}
|
|
80
|
+
function selectActivePreviewPaneIndex(currentTab, totalQuestions) {
|
|
81
|
+
if (totalQuestions <= 0)
|
|
82
|
+
return 0;
|
|
83
|
+
return Math.min(currentTab, totalQuestions - 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// view/dialog-copy.ts
|
|
87
|
+
var HINT_PART_ENTER = "Enter to select";
|
|
88
|
+
var HINT_PART_NAV = "↑/↓ to navigate";
|
|
89
|
+
var HINT_PART_NEW_LINE = "Shift+Enter for newline";
|
|
90
|
+
var HINT_PART_CLEAR = "Ctrl+U to clear";
|
|
91
|
+
var HINT_PART_TOGGLE = "Enter to toggle";
|
|
92
|
+
var HINT_PART_TAB = "Tab to switch questions";
|
|
93
|
+
var HINT_PART_CANCEL = "Esc to cancel";
|
|
94
|
+
var HINT_PART_NOTES_TEMPLATE = `${KEY_PLACEHOLDER} to add notes`;
|
|
95
|
+
var HINT_PART_NOTES = HINT_PART_NOTES_TEMPLATE.replace(KEY_PLACEHOLDER, formatNotesKeyForDisplay(DEFAULT_NOTES_KEY));
|
|
96
|
+
var HINT_PART_COLLAPSE_TEMPLATE = `${KEY_PLACEHOLDER} to collapse`;
|
|
97
|
+
var HINT_PART_EXPAND_TEMPLATE = `${KEY_PLACEHOLDER} to expand`;
|
|
98
|
+
var HINT_PART_COLLAPSE = HINT_PART_COLLAPSE_TEMPLATE.replace(KEY_PLACEHOLDER, formatKeySpecForDisplay(DEFAULT_COLLAPSE_KEY));
|
|
99
|
+
var HINT_SINGLE = [HINT_PART_ENTER, HINT_PART_NAV, HINT_PART_NOTES, HINT_PART_CANCEL].join(" · ");
|
|
100
|
+
var HINT_MULTI = [HINT_PART_ENTER, HINT_PART_NAV, HINT_PART_NOTES, HINT_PART_TAB, HINT_PART_CANCEL].join(" · ");
|
|
101
|
+
var COLLAPSED_HINT_TEMPLATE = [HINT_PART_EXPAND_TEMPLATE, HINT_PART_CANCEL].join(" · ");
|
|
102
|
+
var REVIEW_HEADING = "Review your answers";
|
|
103
|
+
var READY_PROMPT = "Ready to submit your answers?";
|
|
104
|
+
var INCOMPLETE_WARNING_PREFIX = "⚠ Answer remaining questions before submitting:";
|
|
105
|
+
var SAVED_NOTE_LABEL = "Note for this answer:";
|
|
106
|
+
|
|
107
|
+
// view/tab-content-strategy.ts
|
|
17
108
|
var NOTES_HEADER = "Notes:";
|
|
18
109
|
var GLOBAL_NOTES_HEADER = "Global note:";
|
|
19
|
-
var
|
|
110
|
+
var REVIEW_GLOBAL_HINT_TEMPLATE = `${KEY_PLACEHOLDER} to add a note`;
|
|
20
111
|
var REVIEW_NOTE_LABEL = "Note";
|
|
21
112
|
|
|
22
113
|
class OneLineClippedText {
|
|
@@ -65,19 +156,26 @@ class QuestionTabStrategy {
|
|
|
65
156
|
return this.config.getCurrentBodyHeight(width);
|
|
66
157
|
}
|
|
67
158
|
midRows(state) {
|
|
68
|
-
if (
|
|
159
|
+
if (state.notesVisible) {
|
|
160
|
+
return [
|
|
161
|
+
new Text(this.config.theme.fg("muted", t("notes.header", NOTES_HEADER)), 1, 0),
|
|
162
|
+
this.config.notesInput,
|
|
163
|
+
new Spacer(1)
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
const note = selectSavedNote(state.notesByTab, state.answers, state.currentTab);
|
|
167
|
+
if (note === undefined)
|
|
69
168
|
return [];
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
];
|
|
169
|
+
const [firstLine = "", ...rest] = note.split(`
|
|
170
|
+
`);
|
|
171
|
+
const text = `${t("notes.saved_label", SAVED_NOTE_LABEL)} ${firstLine}${rest.length > 0 ? " …" : ""}`;
|
|
172
|
+
return [new OneLineClippedText(this.config.theme.fg("dim", text), 1)];
|
|
75
173
|
}
|
|
76
174
|
footerRows(state) {
|
|
77
175
|
const question = this.config.questions[state.currentTab];
|
|
78
176
|
return [
|
|
79
177
|
new Spacer(1),
|
|
80
|
-
new OneLineClippedText(this.config.theme.fg("dim", buildHintText(question, this.config.isMulti, state, this.config.collapseKey)), 1)
|
|
178
|
+
new OneLineClippedText(this.config.theme.fg("dim", buildHintText(question, this.config.isMulti, state, this.config.collapseKey, this.config.notesKey)), 1)
|
|
81
179
|
];
|
|
82
180
|
}
|
|
83
181
|
focusedItemRowRange(width, state) {
|
|
@@ -151,18 +249,19 @@ class SubmitTabStrategy {
|
|
|
151
249
|
out.push(new Spacer(1));
|
|
152
250
|
out.push(new Spacer(1));
|
|
153
251
|
}
|
|
154
|
-
out.push(new OneLineClippedText(this.config.theme.fg("dim", buildSubmitHintText(state)), 1));
|
|
252
|
+
out.push(new OneLineClippedText(this.config.theme.fg("dim", buildSubmitHintText(state, this.config.notesKey)), 1));
|
|
155
253
|
return out;
|
|
156
254
|
}
|
|
157
255
|
focusedItemRowRange(_width, _state) {
|
|
158
256
|
return;
|
|
159
257
|
}
|
|
160
258
|
}
|
|
161
|
-
function buildHintText(question, isMulti, state, collapseKey) {
|
|
259
|
+
function buildHintText(question, isMulti, state, collapseKey, notesKey) {
|
|
162
260
|
const enterHint = question?.multiSelect === true && !state.notesVisible ? t("hint.enter_toggle", HINT_PART_TOGGLE) : t("hint.enter", HINT_PART_ENTER);
|
|
163
261
|
const parts = [enterHint, t("hint.navigate", HINT_PART_NAV)];
|
|
164
|
-
if (question && !state.notesVisible && !state.inputMode)
|
|
165
|
-
parts.push(t("hint.notes",
|
|
262
|
+
if (question && !state.notesVisible && !state.inputMode) {
|
|
263
|
+
parts.push(t("hint.notes", HINT_PART_NOTES_TEMPLATE).replace(KEY_PLACEHOLDER, formatNotesKeyForDisplay(notesKey)));
|
|
264
|
+
}
|
|
166
265
|
if (isMulti)
|
|
167
266
|
parts.push(t("hint.tab", HINT_PART_TAB));
|
|
168
267
|
parts.push(t("hint.cancel", HINT_PART_CANCEL));
|
|
@@ -175,10 +274,11 @@ function buildHintText(question, isMulti, state, collapseKey) {
|
|
|
175
274
|
parts.push(t("hint.clear", HINT_PART_CLEAR));
|
|
176
275
|
return parts.join(" · ");
|
|
177
276
|
}
|
|
178
|
-
function buildSubmitHintText(state) {
|
|
277
|
+
function buildSubmitHintText(state, notesKey) {
|
|
179
278
|
const parts = [t("hint.enter", HINT_PART_ENTER), t("hint.navigate", HINT_PART_NAV)];
|
|
180
|
-
if (!state.notesVisible)
|
|
181
|
-
parts.push(t("review.global_hint",
|
|
279
|
+
if (!state.notesVisible) {
|
|
280
|
+
parts.push(t("review.global_hint", REVIEW_GLOBAL_HINT_TEMPLATE).replace(KEY_PLACEHOLDER, formatNotesKeyForDisplay(notesKey)));
|
|
281
|
+
}
|
|
182
282
|
parts.push(t("hint.cancel", HINT_PART_CANCEL));
|
|
183
283
|
if (state.notesVisible)
|
|
184
284
|
parts.push(t("hint.newline", HINT_PART_NEW_LINE));
|
|
@@ -186,27 +286,6 @@ function buildSubmitHintText(state) {
|
|
|
186
286
|
}
|
|
187
287
|
|
|
188
288
|
// view/dialog-builder.ts
|
|
189
|
-
var HINT_PART_ENTER = "Enter to select";
|
|
190
|
-
var HINT_PART_NAV = "↑/↓ to navigate";
|
|
191
|
-
var HINT_PART_NEW_LINE = "Shift+Enter for newline";
|
|
192
|
-
var HINT_PART_CLEAR = "Ctrl+U to clear";
|
|
193
|
-
var HINT_PART_TOGGLE = "Enter to toggle";
|
|
194
|
-
var HINT_PART_NOTES = "n to add notes";
|
|
195
|
-
var HINT_PART_TAB = "Tab to switch questions";
|
|
196
|
-
var HINT_PART_CANCEL = "Esc to cancel";
|
|
197
|
-
var KEY_PLACEHOLDER = "{key}";
|
|
198
|
-
var HINT_PART_COLLAPSE_TEMPLATE = `${KEY_PLACEHOLDER} to collapse`;
|
|
199
|
-
var HINT_PART_EXPAND_TEMPLATE = `${KEY_PLACEHOLDER} to expand`;
|
|
200
|
-
var HINT_PART_COLLAPSE = HINT_PART_COLLAPSE_TEMPLATE.replace(KEY_PLACEHOLDER, formatKeySpecForDisplay(DEFAULT_COLLAPSE_KEY));
|
|
201
|
-
var HINT_SINGLE = [HINT_PART_ENTER, HINT_PART_NAV, HINT_PART_NOTES, HINT_PART_CANCEL].join(" · ");
|
|
202
|
-
var HINT_MULTI = [HINT_PART_ENTER, HINT_PART_NAV, HINT_PART_NOTES, HINT_PART_TAB, HINT_PART_CANCEL].join(" · ");
|
|
203
|
-
var COLLAPSED_HINT_TEMPLATE = [HINT_PART_EXPAND_TEMPLATE, HINT_PART_CANCEL].join(" · ");
|
|
204
|
-
var REVIEW_HEADING = "Review your answers";
|
|
205
|
-
var READY_PROMPT = "Ready to submit your answers?";
|
|
206
|
-
var INCOMPLETE_WARNING_PREFIX = "⚠ Answer remaining questions before submitting:";
|
|
207
|
-
var OVERFLOW_UP = "↑";
|
|
208
|
-
var OVERFLOW_DOWN = "↓";
|
|
209
|
-
var OVERFLOW_BOTH = "↕";
|
|
210
289
|
function renderFitsTerminal(natural, spacerRows) {
|
|
211
290
|
return spacerRows > 0 ? [...natural, ...Array(spacerRows).fill("")] : natural;
|
|
212
291
|
}
|
|
@@ -214,26 +293,7 @@ function renderChromeOnly(natural, topFixed, bottomFixed, termRows) {
|
|
|
214
293
|
const chromeOnly = [...natural.slice(0, topFixed), ...natural.slice(natural.length - bottomFixed)];
|
|
215
294
|
return chromeOnly.length > termRows ? chromeOnly.slice(0, termRows) : chromeOnly;
|
|
216
295
|
}
|
|
217
|
-
|
|
218
|
-
if (!bodyRange)
|
|
219
|
-
return 0;
|
|
220
|
-
const focusedRowInMiddle = headingCount + bodyRange[0];
|
|
221
|
-
const focusedHeight = bodyRange[1] - bodyRange[0];
|
|
222
|
-
const idealStart = focusedRowInMiddle - Math.floor(Math.max(0, availableMiddle - focusedHeight) / 2);
|
|
223
|
-
return Math.max(0, Math.min(idealStart, middleRows - availableMiddle));
|
|
224
|
-
}
|
|
225
|
-
function decorateOverflow(scrollableMiddle, hasUp, hasDown, theme) {
|
|
226
|
-
if (hasUp && hasDown && scrollableMiddle.length === 1) {
|
|
227
|
-
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_BOTH);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (hasUp && scrollableMiddle.length > 0) {
|
|
231
|
-
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_UP);
|
|
232
|
-
}
|
|
233
|
-
if (hasDown && scrollableMiddle.length > 0) {
|
|
234
|
-
scrollableMiddle[scrollableMiddle.length - 1] = theme.fg("dim", OVERFLOW_DOWN);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
296
|
+
var MIN_SCROLL_ROWS_WITH_PINNED_HEADING = 3;
|
|
237
297
|
|
|
238
298
|
class DialogView {
|
|
239
299
|
liveProps;
|
|
@@ -252,13 +312,15 @@ class DialogView {
|
|
|
252
312
|
notesInput: config.notesInput,
|
|
253
313
|
isMulti: config.isMulti,
|
|
254
314
|
getCurrentBodyHeight: config.getCurrentBodyHeight,
|
|
255
|
-
collapseKey: config.collapseKey
|
|
315
|
+
collapseKey: config.collapseKey,
|
|
316
|
+
notesKey: config.notesKey
|
|
256
317
|
});
|
|
257
318
|
this.submitStrategy = config.isMulti ? new SubmitTabStrategy({
|
|
258
319
|
theme: config.theme,
|
|
259
320
|
questions: config.questions,
|
|
260
321
|
submitPicker: config.submitPicker,
|
|
261
|
-
notesInput: config.notesInput
|
|
322
|
+
notesInput: config.notesInput,
|
|
323
|
+
notesKey: config.notesKey
|
|
262
324
|
}) : undefined;
|
|
263
325
|
this.maxFooterRowCount = Math.max(this.questionStrategy.footerRowCount, this.submitStrategy?.footerRowCount ?? 0);
|
|
264
326
|
}
|
|
@@ -272,31 +334,46 @@ class DialogView {
|
|
|
272
334
|
const onSubmit = this.config.isMulti && state.currentTab === this.config.questions.length;
|
|
273
335
|
const strategy = onSubmit && this.submitStrategy ? this.submitStrategy : this.questionStrategy;
|
|
274
336
|
const headingRowCache = strategy.headingRows(state);
|
|
275
|
-
const
|
|
276
|
-
const
|
|
337
|
+
const headingHeight = headingRowCache.reduce((rows, c) => rows + c.render(width).length, 0);
|
|
338
|
+
const midRows = strategy.midRows(state);
|
|
339
|
+
const natural = this.buildContainerFromStrategy(strategy, headingRowCache, midRows).render(width);
|
|
277
340
|
const topFixed = 1 + (this.config.isMulti && this.config.tabBar ? 2 : 0) + 1;
|
|
278
341
|
const bottomFixed = 1 + strategy.footerRowCount;
|
|
279
|
-
const
|
|
280
|
-
const spacerRows = Math.max(0, this.config.getBodyHeight(width) + this.maxFooterRowCount - strategy.bodyHeight(width, state) - strategy.footerRowCount);
|
|
342
|
+
const spacerRows = Math.max(0, this.config.getBodyHeight(width) + this.maxFooterRowCount - strategy.bodyHeight(width, state) - strategy.footerRowCount - (state.notesVisible ? 0 : midRows.length));
|
|
281
343
|
const termRows = this.config.getTerminalRows();
|
|
282
|
-
if (natural.length
|
|
283
|
-
return renderFitsTerminal(natural, spacerRows);
|
|
344
|
+
if (natural.length <= termRows) {
|
|
345
|
+
return renderFitsTerminal(natural, Math.min(spacerRows, termRows - natural.length));
|
|
284
346
|
}
|
|
285
|
-
const
|
|
347
|
+
const pinHeading = termRows - topFixed - headingHeight - bottomFixed >= MIN_SCROLL_ROWS_WITH_PINNED_HEADING;
|
|
348
|
+
const top = pinHeading ? topFixed + headingHeight : topFixed;
|
|
349
|
+
const rowsBeforeBody = pinHeading ? 0 : headingHeight;
|
|
350
|
+
const middleRows = natural.length - top - bottomFixed;
|
|
351
|
+
const availableMiddle = Math.max(0, termRows - top - bottomFixed);
|
|
286
352
|
if (availableMiddle === 0) {
|
|
287
|
-
return renderChromeOnly(natural,
|
|
353
|
+
return renderChromeOnly(natural, top, bottomFixed, termRows);
|
|
354
|
+
}
|
|
355
|
+
const windowRows = Math.min(availableMiddle, middleRows);
|
|
356
|
+
const optionFocus = strategy.focusedItemRowRange(width, state);
|
|
357
|
+
const notesRows = midRows.reduce((rows, component) => rows + component.render(width).length, 0);
|
|
358
|
+
const notesStart = Math.max(0, middleRows - notesRows);
|
|
359
|
+
let focusInMiddle;
|
|
360
|
+
if (state.notesVisible && notesRows > 0) {
|
|
361
|
+
const cursorRow = notesRows <= windowRows ? -1 : natural.slice(top, top + middleRows).findIndex((line, i) => i >= notesStart && line.includes(CURSOR_MARKER));
|
|
362
|
+
focusInMiddle = cursorRow === -1 ? [notesStart, middleRows] : [cursorRow, cursorRow + 1];
|
|
363
|
+
} else if (optionFocus) {
|
|
364
|
+
focusInMiddle = [rowsBeforeBody + optionFocus[0], rowsBeforeBody + optionFocus[1]];
|
|
288
365
|
}
|
|
289
|
-
const scrollStart = computeScrollStart(
|
|
290
|
-
const scrollableMiddle = natural.slice(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
...scrollableMiddle,
|
|
295
|
-
...natural.slice(natural.length - bottomFixed)
|
|
366
|
+
const scrollStart = computeScrollStart(focusInMiddle, 0, windowRows, middleRows);
|
|
367
|
+
const scrollableMiddle = natural.slice(top + scrollStart, top + scrollStart + windowRows);
|
|
368
|
+
const focusInWindow = focusInMiddle && [
|
|
369
|
+
focusInMiddle[0] - scrollStart,
|
|
370
|
+
focusInMiddle[1] - scrollStart
|
|
296
371
|
];
|
|
372
|
+
decorateOverflow(scrollableMiddle, scrollStart > 0, scrollStart + windowRows < middleRows, this.config.theme, focusInWindow);
|
|
373
|
+
const result = [...natural.slice(0, top), ...scrollableMiddle, ...natural.slice(natural.length - bottomFixed)];
|
|
297
374
|
return result.length > termRows ? result.slice(0, termRows) : result;
|
|
298
375
|
}
|
|
299
|
-
buildContainerFromStrategy(strategy, headingRowCache) {
|
|
376
|
+
buildContainerFromStrategy(strategy, headingRowCache, midRows) {
|
|
300
377
|
const { theme, isMulti, tabBar } = this.config;
|
|
301
378
|
const state = this.liveProps.state;
|
|
302
379
|
const container = new Container2;
|
|
@@ -309,7 +386,7 @@ class DialogView {
|
|
|
309
386
|
container.addChild(c);
|
|
310
387
|
container.addChild(strategy.bodyComponent(state));
|
|
311
388
|
container.addChild(new Spacer2(1));
|
|
312
|
-
for (const c of
|
|
389
|
+
for (const c of midRows)
|
|
313
390
|
container.addChild(c);
|
|
314
391
|
container.addChild(border());
|
|
315
392
|
for (const c of strategy.footerRows(state))
|
|
@@ -343,7 +420,7 @@ function perTabBinding(spec) {
|
|
|
343
420
|
import { truncateToWidth as truncateToWidth2, visibleWidth, wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
|
|
344
421
|
|
|
345
422
|
// view/components/inline-input.ts
|
|
346
|
-
import { CURSOR_MARKER, renderFakeCursor, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
423
|
+
import { CURSOR_MARKER as CURSOR_MARKER2, renderFakeCursor, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
347
424
|
var graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
348
425
|
function resolveCursorOffset(buffer, requested) {
|
|
349
426
|
if (requested !== undefined && requested >= 0 && requested <= buffer.length)
|
|
@@ -358,7 +435,7 @@ function buildCursorRaw(buffer, offset) {
|
|
|
358
435
|
`;
|
|
359
436
|
const atCursor = rawAt === "" || rawAt === " " || cursorAtLineEnd ? " " : rawAt;
|
|
360
437
|
const after = buffer.slice(offset + (cursorAtLineEnd ? 0 : rawAt.length));
|
|
361
|
-
return `${before}${
|
|
438
|
+
return `${before}${CURSOR_MARKER2}${renderFakeCursor(atCursor)}${after}`;
|
|
362
439
|
}
|
|
363
440
|
function renderInlineInputRow(opts) {
|
|
364
441
|
const { buffer, cursorOffset, rowPrefix, continuationPrefix, contentWidth, selectedText } = opts;
|
|
@@ -676,57 +753,7 @@ class OptionListView {
|
|
|
676
753
|
}
|
|
677
754
|
|
|
678
755
|
// view/components/preview/markdown-content-cache.ts
|
|
679
|
-
import { Markdown, visibleWidth as
|
|
680
|
-
|
|
681
|
-
// view/components/preview/preview-box-renderer.ts
|
|
682
|
-
import { truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
683
|
-
var ANSI_SGR_RE = /\x1b\[[0-9;]*m/g;
|
|
684
|
-
var ANSI_OSC8_RE = /\x1b\]8;[^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
685
|
-
var FENCE_MARKER_RE = /^`{3}/;
|
|
686
|
-
var BORDER_VERTICAL_OVERHEAD = 2;
|
|
687
|
-
var BORDER_HORIZONTAL_OVERHEAD = 2;
|
|
688
|
-
var BORDER_INNER_PADDING_HORIZONTAL = 1;
|
|
689
|
-
var BOX_MIN_CONTENT_WIDTH = 40;
|
|
690
|
-
function stripFenceMarkers(lines) {
|
|
691
|
-
return lines.filter((line) => {
|
|
692
|
-
const clean = line.replace(ANSI_SGR_RE, "").replace(ANSI_OSC8_RE, "");
|
|
693
|
-
return !FENCE_MARKER_RE.test(clean);
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
function renderBorderedBox(lines, width, colorFn, hidden = 0) {
|
|
697
|
-
const dashSpan = Math.max(1, width - BORDER_HORIZONTAL_OVERHEAD);
|
|
698
|
-
const contentInner = Math.max(1, dashSpan - 2 * BORDER_INNER_PADDING_HORIZONTAL);
|
|
699
|
-
const pad = " ".repeat(BORDER_INNER_PADDING_HORIZONTAL);
|
|
700
|
-
const top = colorFn(`┌${"─".repeat(dashSpan)}┐`);
|
|
701
|
-
const out = [top];
|
|
702
|
-
for (const line of lines) {
|
|
703
|
-
const padded = truncateToWidth3(line, contentInner, "", true);
|
|
704
|
-
out.push(`${colorFn("│")}${pad}${padded}${pad}${colorFn("│")}`);
|
|
705
|
-
}
|
|
706
|
-
if (hidden > 0) {
|
|
707
|
-
const indicator = ` ✂ ── ${hidden} lines hidden ── `;
|
|
708
|
-
const space = dashSpan - indicator.length;
|
|
709
|
-
const leftFill = "─".repeat(Math.max(0, Math.floor(space / 2)));
|
|
710
|
-
const rightFill = "─".repeat(Math.max(0, dashSpan - leftFill.length - indicator.length));
|
|
711
|
-
out.push(colorFn(`└${leftFill}${indicator}${rightFill}┘`));
|
|
712
|
-
} else {
|
|
713
|
-
out.push(colorFn(`└${"─".repeat(dashSpan)}┘`));
|
|
714
|
-
}
|
|
715
|
-
return out;
|
|
716
|
-
}
|
|
717
|
-
function computeBoxDimensions(contentLines, maxInnerWidth) {
|
|
718
|
-
let widest = Math.min(BOX_MIN_CONTENT_WIDTH, maxInnerWidth);
|
|
719
|
-
for (const line of contentLines) {
|
|
720
|
-
const w = visibleWidth3(line.replace(/\s+$/, ""));
|
|
721
|
-
if (w > widest)
|
|
722
|
-
widest = w;
|
|
723
|
-
}
|
|
724
|
-
const innerWidth = Math.min(widest, maxInnerWidth);
|
|
725
|
-
const boxWidth = innerWidth + BORDER_HORIZONTAL_OVERHEAD + 2 * BORDER_INNER_PADDING_HORIZONTAL;
|
|
726
|
-
return { innerWidth, boxWidth };
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// view/components/preview/markdown-content-cache.ts
|
|
756
|
+
import { Markdown, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
730
757
|
var MAX_PREVIEW_HEIGHT_SIDE_BY_SIDE = 20;
|
|
731
758
|
var MAX_PREVIEW_HEIGHT_STACKED = 15;
|
|
732
759
|
var NO_PREVIEW_TEXT = "No preview available";
|
|
@@ -764,7 +791,7 @@ class MarkdownContentCache {
|
|
|
764
791
|
const text = this.previewTexts.get(optionIndex);
|
|
765
792
|
if (!text) {
|
|
766
793
|
const placeholder = this.theme.fg("dim", t("preview.no_preview", NO_PREVIEW_TEXT));
|
|
767
|
-
const pad = Math.max(0, innerWidth -
|
|
794
|
+
const pad = Math.max(0, innerWidth - visibleWidth3(placeholder));
|
|
768
795
|
return [placeholder + " ".repeat(pad)];
|
|
769
796
|
}
|
|
770
797
|
let md = this.markdownCache.get(optionIndex);
|
|
@@ -782,7 +809,8 @@ class MarkdownContentCache {
|
|
|
782
809
|
}
|
|
783
810
|
|
|
784
811
|
// view/components/preview/preview-block-renderer.ts
|
|
785
|
-
var NOTES_AFFORDANCE_TEXT =
|
|
812
|
+
var NOTES_AFFORDANCE_TEXT = `Notes: press ${formatNotesKeyForDisplay(DEFAULT_NOTES_KEY)} to add notes`;
|
|
813
|
+
var NOTES_EDIT_AFFORDANCE_TEXT = `Note saved: press ${formatNotesKeyForDisplay(DEFAULT_NOTES_KEY)} to edit`;
|
|
786
814
|
function contentBudgetFor(mode) {
|
|
787
815
|
const cap = mode === "side-by-side" ? MAX_PREVIEW_HEIGHT_SIDE_BY_SIDE : MAX_PREVIEW_HEIGHT_STACKED;
|
|
788
816
|
return Math.max(1, cap - BORDER_VERTICAL_OVERHEAD - NOTES_AFFORDANCE_OVERHEAD);
|
|
@@ -794,8 +822,10 @@ function innerWidthFor(width) {
|
|
|
794
822
|
class PreviewBlockRenderer {
|
|
795
823
|
theme;
|
|
796
824
|
cache;
|
|
825
|
+
notesKey;
|
|
797
826
|
constructor(config) {
|
|
798
827
|
this.theme = config.theme;
|
|
828
|
+
this.notesKey = config.notesKey;
|
|
799
829
|
this.cache = new MarkdownContentCache(config.question, config.theme, config.markdownTheme);
|
|
800
830
|
}
|
|
801
831
|
hasAnyPreview() {
|
|
@@ -814,7 +844,7 @@ class PreviewBlockRenderer {
|
|
|
814
844
|
const contentRows = Math.min(rawRows, contentBudget);
|
|
815
845
|
return BORDER_VERTICAL_OVERHEAD + contentRows + NOTES_AFFORDANCE_OVERHEAD;
|
|
816
846
|
}
|
|
817
|
-
renderBlock(width, optionIndex, mode, focused, notesVisible) {
|
|
847
|
+
renderBlock(width, optionIndex, mode, focused, notesVisible, hasNote = false) {
|
|
818
848
|
const contentBudget = contentBudgetFor(mode);
|
|
819
849
|
const maxInnerWidth = innerWidthFor(width);
|
|
820
850
|
const raw = this.cache.bodyFor(optionIndex, maxInnerWidth);
|
|
@@ -825,13 +855,14 @@ class PreviewBlockRenderer {
|
|
|
825
855
|
const colorFn = (s) => this.theme.fg("accent", s);
|
|
826
856
|
const boxedLines = renderBorderedBox(contentLines, boxWidth, colorFn, hidden);
|
|
827
857
|
const showAffordance = focused && !notesVisible && this.cache.has(optionIndex);
|
|
828
|
-
const
|
|
858
|
+
const [affordanceKey, affordanceFallback] = hasNote ? ["preview.notes_edit_affordance", NOTES_EDIT_AFFORDANCE_TEXT] : ["preview.notes_affordance", NOTES_AFFORDANCE_TEXT];
|
|
859
|
+
const affordance = showAffordance ? this.theme.fg("muted", t(affordanceKey, affordanceFallback).replace(KEY_PLACEHOLDER, formatNotesKeyForDisplay(this.notesKey))) : "";
|
|
829
860
|
return [...boxedLines, "", affordance];
|
|
830
861
|
}
|
|
831
862
|
}
|
|
832
863
|
|
|
833
864
|
// view/components/preview/preview-layout-decider.ts
|
|
834
|
-
import { visibleWidth as
|
|
865
|
+
import { visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
|
|
835
866
|
var PREVIEW_MIN_WIDTH = 100;
|
|
836
867
|
var PREVIEW_COLUMN_GAP = 2;
|
|
837
868
|
var PREVIEW_PADDING_LEFT = 1;
|
|
@@ -848,7 +879,7 @@ function adaptiveLeftWidth(items, totalForNumbering, paneWidth) {
|
|
|
848
879
|
const confirmedOverhead = CONFIRMED_OVERHEAD;
|
|
849
880
|
let maxLabel = 0;
|
|
850
881
|
for (const item of items) {
|
|
851
|
-
const w =
|
|
882
|
+
const w = visibleWidth4(item.label);
|
|
852
883
|
if (w > maxLabel)
|
|
853
884
|
maxLabel = w;
|
|
854
885
|
}
|
|
@@ -876,7 +907,7 @@ function previewSourceWidth(question) {
|
|
|
876
907
|
continue;
|
|
877
908
|
for (const line of text.split(`
|
|
878
909
|
`)) {
|
|
879
|
-
const w =
|
|
910
|
+
const w = visibleWidth4(line);
|
|
880
911
|
if (w > max)
|
|
881
912
|
max = w;
|
|
882
913
|
}
|
|
@@ -917,7 +948,7 @@ function bodyWidths(paneWidth, mode, adaptiveLeft) {
|
|
|
917
948
|
}
|
|
918
949
|
|
|
919
950
|
// view/components/preview/preview-pane.ts
|
|
920
|
-
import { truncateToWidth as
|
|
951
|
+
import { truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth5 } from "@earendil-works/pi-tui";
|
|
921
952
|
class PreviewPane {
|
|
922
953
|
question;
|
|
923
954
|
getTerminalWidth;
|
|
@@ -932,7 +963,7 @@ class PreviewPane {
|
|
|
932
963
|
this.getTerminalWidth = config.getTerminalWidth;
|
|
933
964
|
this.optionListView = config.optionListView;
|
|
934
965
|
this.previewBlock = config.previewBlock;
|
|
935
|
-
this.props = { notesVisible: false, selectedIndex: 0, focused: false, inputMode: false };
|
|
966
|
+
this.props = { notesVisible: false, hasNote: false, selectedIndex: 0, focused: false, inputMode: false };
|
|
936
967
|
}
|
|
937
968
|
setGlobalLeftWidth(getter) {
|
|
938
969
|
this.globalLeftWidth = getter;
|
|
@@ -961,7 +992,7 @@ class PreviewPane {
|
|
|
961
992
|
return [
|
|
962
993
|
...this.optionListView.render(width),
|
|
963
994
|
...Array(STACKED_GAP_ROWS).fill(""),
|
|
964
|
-
...this.previewBlock.renderBlock(width, this.props.selectedIndex, mode, this.props.focused, this.props.notesVisible)
|
|
995
|
+
...this.previewBlock.renderBlock(width, this.props.selectedIndex, mode, this.props.focused, this.props.notesVisible, this.props.hasNote)
|
|
965
996
|
];
|
|
966
997
|
}
|
|
967
998
|
focusedItemRowRange(width) {
|
|
@@ -1026,29 +1057,29 @@ class PreviewPane {
|
|
|
1026
1057
|
for (let i = 0;i < rows; i++) {
|
|
1027
1058
|
const leftRaw = leftLines[i] ?? "";
|
|
1028
1059
|
const rightRaw = rightLines[i] ?? "";
|
|
1029
|
-
const leftClamped =
|
|
1030
|
-
const leftPad = " ".repeat(Math.max(0, leftWidth -
|
|
1060
|
+
const leftClamped = truncateToWidth3(leftRaw, leftWidth, "");
|
|
1061
|
+
const leftPad = " ".repeat(Math.max(0, leftWidth - visibleWidth5(leftClamped)));
|
|
1031
1062
|
const joined = `${leftClamped}${leftPad}${gapStr}${rightRaw}`;
|
|
1032
|
-
out.push(
|
|
1063
|
+
out.push(truncateToWidth3(joined, width, ""));
|
|
1033
1064
|
}
|
|
1034
1065
|
return out;
|
|
1035
1066
|
}
|
|
1036
1067
|
renderPaddedPreviewLines(colWidth, mode) {
|
|
1037
1068
|
const inner = Math.max(1, colWidth - PREVIEW_PADDING_LEFT);
|
|
1038
|
-
const contentLines = this.previewBlock.renderBlock(inner, this.props.selectedIndex, mode, this.props.focused, this.props.notesVisible);
|
|
1039
|
-
const boxWidth = Math.max(1,
|
|
1069
|
+
const contentLines = this.previewBlock.renderBlock(inner, this.props.selectedIndex, mode, this.props.focused, this.props.notesVisible, this.props.hasNote);
|
|
1070
|
+
const boxWidth = Math.max(1, visibleWidth5(contentLines[0] ?? ""));
|
|
1040
1071
|
const boxAlignedPad = Math.max(PREVIEW_PADDING_LEFT, colWidth - boxWidth);
|
|
1041
1072
|
return contentLines.map((line) => {
|
|
1042
1073
|
if (line === "")
|
|
1043
1074
|
return "";
|
|
1044
|
-
const pad = Math.max(PREVIEW_PADDING_LEFT, Math.min(boxAlignedPad, colWidth -
|
|
1045
|
-
return `${" ".repeat(pad)}${
|
|
1075
|
+
const pad = Math.max(PREVIEW_PADDING_LEFT, Math.min(boxAlignedPad, colWidth - visibleWidth5(line)));
|
|
1076
|
+
return `${" ".repeat(pad)}${truncateToWidth3(line, colWidth - pad, "")}`;
|
|
1046
1077
|
});
|
|
1047
1078
|
}
|
|
1048
1079
|
}
|
|
1049
1080
|
|
|
1050
1081
|
// view/components/submit-picker.ts
|
|
1051
|
-
import { truncateToWidth as
|
|
1082
|
+
import { truncateToWidth as truncateToWidth4 } from "@earendil-works/pi-tui";
|
|
1052
1083
|
var ACTIVE_POINTER2 = "❯ ";
|
|
1053
1084
|
var INACTIVE_POINTER2 = " ";
|
|
1054
1085
|
var NUMBER_SEPARATOR2 = ". ";
|
|
@@ -1078,14 +1109,14 @@ class SubmitPicker {
|
|
|
1078
1109
|
const pointer = active ? ACTIVE_POINTER2 : INACTIVE_POINTER2;
|
|
1079
1110
|
const number = `${i + 1}${NUMBER_SEPARATOR2}`;
|
|
1080
1111
|
const label = active ? this.theme.fg("accent", this.theme.bold(text)) : this.theme.fg("text", text);
|
|
1081
|
-
lines.push(
|
|
1112
|
+
lines.push(truncateToWidth4(`${pointer}${number}${label}`, width, ""));
|
|
1082
1113
|
}
|
|
1083
1114
|
return lines;
|
|
1084
1115
|
}
|
|
1085
1116
|
}
|
|
1086
1117
|
|
|
1087
1118
|
// view/components/tab-bar.ts
|
|
1088
|
-
import { truncateToWidth as
|
|
1119
|
+
import { truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
|
|
1089
1120
|
|
|
1090
1121
|
class TabBar {
|
|
1091
1122
|
theme;
|
|
@@ -1112,38 +1143,11 @@ class TabBar {
|
|
|
1112
1143
|
const submitStyled = this.props.submit.active ? this.theme.bg("selectedBg", this.theme.fg("text", submitText)) : this.theme.fg(this.props.submit.allAnswered ? "success" : "dim", submitText);
|
|
1113
1144
|
pieces.push(submitStyled);
|
|
1114
1145
|
pieces.push(" →");
|
|
1115
|
-
const tabLine =
|
|
1146
|
+
const tabLine = truncateToWidth5(pieces.join(""), width, "");
|
|
1116
1147
|
return [tabLine, ""];
|
|
1117
1148
|
}
|
|
1118
1149
|
}
|
|
1119
1150
|
|
|
1120
|
-
// state/selectors/derivations.ts
|
|
1121
|
-
function selectConfirmedIndicator(questions, currentTab, answers, items) {
|
|
1122
|
-
const q = questions[currentTab];
|
|
1123
|
-
if (!q || q.multiSelect === true)
|
|
1124
|
-
return;
|
|
1125
|
-
const prior = answers.get(currentTab);
|
|
1126
|
-
if (!prior)
|
|
1127
|
-
return;
|
|
1128
|
-
if (prior.kind === "custom") {
|
|
1129
|
-
const otherIndex = items.findIndex((it) => it.kind === "other");
|
|
1130
|
-
if (otherIndex < 0)
|
|
1131
|
-
return;
|
|
1132
|
-
return { index: otherIndex, labelOverride: prior.answer ?? "" };
|
|
1133
|
-
}
|
|
1134
|
-
if (prior.kind !== "option" || typeof prior.answer !== "string")
|
|
1135
|
-
return;
|
|
1136
|
-
const index = items.findIndex((it) => it.kind === "option" && it.label === prior.answer);
|
|
1137
|
-
if (index < 0)
|
|
1138
|
-
return;
|
|
1139
|
-
return { index };
|
|
1140
|
-
}
|
|
1141
|
-
function selectActivePreviewPaneIndex(currentTab, totalQuestions) {
|
|
1142
|
-
if (totalQuestions <= 0)
|
|
1143
|
-
return 0;
|
|
1144
|
-
return Math.min(currentTab, totalQuestions - 1);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
1151
|
// state/selectors/focus.ts
|
|
1148
1152
|
function selectActiveView(state, totalQuestions) {
|
|
1149
1153
|
if (state.notesVisible)
|
|
@@ -1290,6 +1294,7 @@ var selectSubmitPickerProps = (state, ctx) => {
|
|
|
1290
1294
|
};
|
|
1291
1295
|
var selectPreviewPaneProps = (state, ctx) => ({
|
|
1292
1296
|
notesVisible: state.notesVisible,
|
|
1297
|
+
hasNote: selectSavedNote(state.notesByTab, state.answers, ctx.i) !== undefined,
|
|
1293
1298
|
selectedIndex: state.optionIndex,
|
|
1294
1299
|
focused: ctx.activeView === "options",
|
|
1295
1300
|
inputMode: state.inputMode
|
|
@@ -1352,12 +1357,13 @@ class QuestionnaireBuilder {
|
|
|
1352
1357
|
initialState;
|
|
1353
1358
|
getCurrentTab;
|
|
1354
1359
|
collapseKey;
|
|
1360
|
+
notesKey;
|
|
1355
1361
|
selectTheme;
|
|
1356
1362
|
markdownTheme = getMarkdownTheme();
|
|
1357
1363
|
notesInput;
|
|
1358
1364
|
inlineInput;
|
|
1359
1365
|
getTerminalWidth = () => this.tui.terminal.columns;
|
|
1360
|
-
|
|
1366
|
+
rowBudget;
|
|
1361
1367
|
constructor(config) {
|
|
1362
1368
|
this.tui = config.tui;
|
|
1363
1369
|
this.theme = config.theme;
|
|
@@ -1367,6 +1373,8 @@ class QuestionnaireBuilder {
|
|
|
1367
1373
|
this.initialState = config.initialState;
|
|
1368
1374
|
this.getCurrentTab = config.getCurrentTab;
|
|
1369
1375
|
this.collapseKey = config.collapseKey;
|
|
1376
|
+
this.notesKey = config.notesKey;
|
|
1377
|
+
this.rowBudget = config.rowBudget ?? ((terminalRows) => terminalRows);
|
|
1370
1378
|
this.selectTheme = this.makeSelectTheme();
|
|
1371
1379
|
const textEditorTheme = editorTheme(this.theme);
|
|
1372
1380
|
this.notesInput = new Editor(this.tui, textEditorTheme);
|
|
@@ -1405,7 +1413,8 @@ class QuestionnaireBuilder {
|
|
|
1405
1413
|
const previewBlock = new PreviewBlockRenderer({
|
|
1406
1414
|
question,
|
|
1407
1415
|
theme: this.theme,
|
|
1408
|
-
markdownTheme: this.markdownTheme
|
|
1416
|
+
markdownTheme: this.markdownTheme,
|
|
1417
|
+
notesKey: this.notesKey
|
|
1409
1418
|
});
|
|
1410
1419
|
const preview = new PreviewPane({
|
|
1411
1420
|
question,
|
|
@@ -1466,8 +1475,9 @@ class QuestionnaireBuilder {
|
|
|
1466
1475
|
submitPicker,
|
|
1467
1476
|
getBodyHeight: heights.global,
|
|
1468
1477
|
getCurrentBodyHeight: heights.current,
|
|
1469
|
-
getTerminalRows: this.
|
|
1470
|
-
collapseKey: this.collapseKey
|
|
1478
|
+
getTerminalRows: () => this.rowBudget(this.tui.terminal.rows),
|
|
1479
|
+
collapseKey: this.collapseKey,
|
|
1480
|
+
notesKey: this.notesKey
|
|
1471
1481
|
}, { state: this.initialState, activePreviewPane: this.pickInitialActivePreview(tabs) });
|
|
1472
1482
|
}
|
|
1473
1483
|
buildGlobalBindings(dialog, submitPicker, tabBar) {
|
|
@@ -1531,6 +1541,10 @@ var KEYBIND_EDITOR_DOWN = "tui.editor.cursorDown";
|
|
|
1531
1541
|
var KEYBIND_CLEAR = "tui.editor.deleteToLineStart";
|
|
1532
1542
|
var KEYBIND_EXTERNAL_EDITOR = "app.editor.external";
|
|
1533
1543
|
var NOTES_ACTIVATE_KEY = "n";
|
|
1544
|
+
function matchesNotesKey(data, notesKey) {
|
|
1545
|
+
const key = typeof notesKey === "string" ? notesKey : NOTES_ACTIVATE_KEY;
|
|
1546
|
+
return data === key || matchesKey(data, key);
|
|
1547
|
+
}
|
|
1534
1548
|
function isConfirm(kb, data) {
|
|
1535
1549
|
return kb.matches(data, KEYBIND_CONFIRM) || kb.matches(data, KEYBIND_SUBMIT);
|
|
1536
1550
|
}
|
|
@@ -1674,11 +1688,19 @@ function routeSubmitTab(kb, data, state, runtime) {
|
|
|
1674
1688
|
if (isConfirm(kb, data)) {
|
|
1675
1689
|
return state.submitChoiceIndex === 1 ? { kind: "cancel" } : { kind: "submit" };
|
|
1676
1690
|
}
|
|
1677
|
-
if (data
|
|
1691
|
+
if (matchesNotesKey(data, runtime.notesKey)) {
|
|
1678
1692
|
return { kind: "notes_enter" };
|
|
1679
1693
|
}
|
|
1680
1694
|
return { kind: "ignore" };
|
|
1681
1695
|
}
|
|
1696
|
+
var DIGIT_KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
1697
|
+
function digitRow(data, runtime) {
|
|
1698
|
+
const index = DIGIT_KEYS.findIndex((digit) => matchesKey(data, digit));
|
|
1699
|
+
const item = runtime.items[index];
|
|
1700
|
+
if (!item || !ROW_INTENT_META[item.kind].numbered)
|
|
1701
|
+
return;
|
|
1702
|
+
return { index, activatesInputMode: ROW_INTENT_META[item.kind].activatesInputMode };
|
|
1703
|
+
}
|
|
1682
1704
|
function routeMultiSelectTab(kb, data, state, runtime) {
|
|
1683
1705
|
const focusedKind = runtime.currentItem?.kind;
|
|
1684
1706
|
const focusedMeta = focusedKind ? ROW_INTENT_META[focusedKind] : undefined;
|
|
@@ -1696,7 +1718,12 @@ function routeMultiSelectTab(kb, data, state, runtime) {
|
|
|
1696
1718
|
}
|
|
1697
1719
|
if (kb.matches(data, KEYBIND_CANCEL))
|
|
1698
1720
|
return { kind: "cancel" };
|
|
1699
|
-
|
|
1721
|
+
const row = digitRow(data, runtime);
|
|
1722
|
+
if (!row)
|
|
1723
|
+
return { kind: "ignore" };
|
|
1724
|
+
if (row.activatesInputMode)
|
|
1725
|
+
return { kind: "nav", nextIndex: row.index, inputValue: runtime.inputBuffer };
|
|
1726
|
+
return { kind: "toggle", index: row.index };
|
|
1700
1727
|
}
|
|
1701
1728
|
function routeSingleSelectTab(kb, data, state, runtime) {
|
|
1702
1729
|
if (isConfirm(kb, data)) {
|
|
@@ -1707,7 +1734,15 @@ function routeSingleSelectTab(kb, data, state, runtime) {
|
|
|
1707
1734
|
}
|
|
1708
1735
|
if (kb.matches(data, KEYBIND_CANCEL))
|
|
1709
1736
|
return { kind: "cancel" };
|
|
1710
|
-
|
|
1737
|
+
const row = digitRow(data, runtime);
|
|
1738
|
+
if (!row)
|
|
1739
|
+
return { kind: "ignore" };
|
|
1740
|
+
if (row.activatesInputMode)
|
|
1741
|
+
return { kind: "nav", nextIndex: row.index, inputValue: runtime.inputBuffer };
|
|
1742
|
+
const answer = buildSingleSelectAnswer({ ...state, optionIndex: row.index }, { ...runtime, currentItem: runtime.items[row.index] });
|
|
1743
|
+
if (!answer)
|
|
1744
|
+
return { kind: "ignore" };
|
|
1745
|
+
return { kind: "confirm", answer, autoAdvanceTab: computeAutoAdvanceTab(state, runtime) };
|
|
1711
1746
|
}
|
|
1712
1747
|
function routeKey(data, state, runtime) {
|
|
1713
1748
|
const kb = runtime.keybindings;
|
|
@@ -1729,7 +1764,7 @@ function routeKey(data, state, runtime) {
|
|
|
1729
1764
|
const q = runtime.questions[state.currentTab];
|
|
1730
1765
|
if (!q)
|
|
1731
1766
|
return { kind: "ignore" };
|
|
1732
|
-
if (data
|
|
1767
|
+
if (matchesNotesKey(data, runtime.notesKey)) {
|
|
1733
1768
|
return { kind: "notes_enter" };
|
|
1734
1769
|
}
|
|
1735
1770
|
if (kb.matches(data, KEYBIND_UP)) {
|
|
@@ -1816,12 +1851,20 @@ function withoutCustomDraft(state, tab) {
|
|
|
1816
1851
|
drafts.delete(tab);
|
|
1817
1852
|
return drafts;
|
|
1818
1853
|
}
|
|
1854
|
+
function answeredRowIndex(answer, items) {
|
|
1855
|
+
let index = -1;
|
|
1856
|
+
if (answer?.kind === "option")
|
|
1857
|
+
index = items.findIndex((i) => i.kind === "option" && i.label === answer.answer);
|
|
1858
|
+
if (answer?.kind === "multi")
|
|
1859
|
+
index = items.findIndex((i) => i.kind === "next");
|
|
1860
|
+
return Math.max(0, index);
|
|
1861
|
+
}
|
|
1819
1862
|
function switchTabResult(state, nextTab, ctx) {
|
|
1820
1863
|
const notesValue = notesValueFor(state, nextTab);
|
|
1821
1864
|
const transitioned = {
|
|
1822
1865
|
...state,
|
|
1823
1866
|
currentTab: nextTab,
|
|
1824
|
-
optionIndex:
|
|
1867
|
+
optionIndex: answeredRowIndex(state.answers.get(nextTab), ctx.itemsByTab[nextTab] ?? []),
|
|
1825
1868
|
inputMode: false,
|
|
1826
1869
|
notesVisible: false,
|
|
1827
1870
|
submitChoiceIndex: 0,
|
|
@@ -2038,6 +2081,7 @@ class QuestionnaireSession {
|
|
|
2038
2081
|
keybindings;
|
|
2039
2082
|
editInput;
|
|
2040
2083
|
collapseKey;
|
|
2084
|
+
notesKey;
|
|
2041
2085
|
canReopenWhileHidden;
|
|
2042
2086
|
inputEditorOpen = false;
|
|
2043
2087
|
overlayHandle;
|
|
@@ -2053,6 +2097,7 @@ class QuestionnaireSession {
|
|
|
2053
2097
|
this.keybindings = config.keybindings;
|
|
2054
2098
|
this.editInput = config.editInput;
|
|
2055
2099
|
this.collapseKey = config.collapseKey;
|
|
2100
|
+
this.notesKey = config.notesKey;
|
|
2056
2101
|
this.canReopenWhileHidden = config.canReopenWhileHidden;
|
|
2057
2102
|
const built = buildQuestionnaire({
|
|
2058
2103
|
tui: this.tui,
|
|
@@ -2062,7 +2107,9 @@ class QuestionnaireSession {
|
|
|
2062
2107
|
isMulti: this.isMulti,
|
|
2063
2108
|
initialState: this.state,
|
|
2064
2109
|
getCurrentTab: () => this.state.currentTab,
|
|
2065
|
-
collapseKey: this.collapseKey
|
|
2110
|
+
collapseKey: this.collapseKey,
|
|
2111
|
+
notesKey: this.notesKey,
|
|
2112
|
+
rowBudget: config.rowBudget
|
|
2066
2113
|
});
|
|
2067
2114
|
this.notesInput = built.notesInput;
|
|
2068
2115
|
this.inlineInput = built.inlineInput;
|
|
@@ -2164,7 +2211,8 @@ class QuestionnaireSession {
|
|
|
2164
2211
|
isMulti: this.isMulti,
|
|
2165
2212
|
currentItem: this.currentItem(),
|
|
2166
2213
|
items: this.itemsByTab[this.state.currentTab] ?? [],
|
|
2167
|
-
collapseKey: this.collapseKey
|
|
2214
|
+
collapseKey: this.collapseKey,
|
|
2215
|
+
notesKey: this.notesKey
|
|
2168
2216
|
};
|
|
2169
2217
|
}
|
|
2170
2218
|
applyContext() {
|