pi-ask-popup 0.1.0 → 0.2.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 +95 -32
- package/docs/configuration.md +93 -0
- package/docs/hosts.md +71 -0
- package/docs/keyboard.md +65 -0
- package/docs/tool-schema.md +124 -0
- package/package.json +12 -2
- package/preview/popup-submit.webp +0 -0
- package/preview/popup-with-notes.webp +0 -0
- package/preview/popup-with-tab.webp +0 -0
- package/src/ask-user-question.ts +54 -21
- package/src/config.ts +118 -24
- package/src/rpc-fallback.ts +85 -28
- package/src/state/build-questionnaire.ts +45 -8
- package/src/state/external-editor.ts +24 -12
- package/src/state/key-router.ts +152 -41
- package/src/state/questionnaire-session.ts +79 -14
- package/src/state/row-intent.ts +6 -2
- package/src/state/selectors/derivations.ts +18 -6
- package/src/state/selectors/focus.ts +6 -2
- package/src/state/selectors/projections.ts +9 -1
- package/src/state/state-reducer.ts +110 -38
- package/src/tool/response-envelope.ts +71 -22
- package/src/tool/types.ts +28 -4
- package/src/view/component-binding.ts +3 -1
- package/src/view/components/inline-input.ts +3 -1
- package/src/view/components/multi-select-view.ts +32 -7
- package/src/view/components/option-list-view.ts +5 -0
- package/src/view/components/preview/markdown-content-cache.ts +75 -23
- package/src/view/components/preview/preview-block-renderer.ts +8 -1
- package/src/view/components/preview/preview-box-renderer.ts +4 -5
- package/src/view/components/preview/preview-layout-decider.ts +52 -15
- package/src/view/components/preview/preview-pane.ts +72 -28
- package/src/view/components/tab-bar.ts +26 -8
- package/src/view/components/wrapping-select.ts +110 -37
- package/src/view/dialog-builder.ts +44 -10
- package/src/view/props-adapter.ts +29 -7
- package/src/view/tab-content-strategy.ts +69 -21
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
HINT_PART_NOTES,
|
|
24
24
|
HINT_PART_NOTES_EDIT,
|
|
25
25
|
HINT_PART_TAB,
|
|
26
|
+
HINT_PART_ENTER_CONFIRM,
|
|
26
27
|
HINT_PART_TOGGLE,
|
|
27
28
|
INCOMPLETE_WARNING_PREFIX,
|
|
28
29
|
KEY_PLACEHOLDER,
|
|
@@ -152,7 +153,9 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
152
153
|
bodyComponent(state: DialogState): Component {
|
|
153
154
|
const question = this.config.questions[state.currentTab];
|
|
154
155
|
const multiSelect = this.config.tabsByIndex[state.currentTab]?.multiSelect;
|
|
155
|
-
if (question?.multiSelect === true && multiSelect)
|
|
156
|
+
if (question?.multiSelect === true && multiSelect) {
|
|
157
|
+
return multiSelect;
|
|
158
|
+
}
|
|
156
159
|
return this.config.getPreviewPane();
|
|
157
160
|
}
|
|
158
161
|
|
|
@@ -161,7 +164,9 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
midRows(state: DialogState): Component[] {
|
|
164
|
-
if (!state.notesVisible)
|
|
167
|
+
if (!state.notesVisible) {
|
|
168
|
+
return this.restingNoteRows(state);
|
|
169
|
+
}
|
|
165
170
|
return [
|
|
166
171
|
new Text(this.config.theme.fg("muted", NOTES_HEADER), 1, 0),
|
|
167
172
|
this.config.notesInput,
|
|
@@ -190,9 +195,13 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
190
195
|
* put noise on every un-noted tab of every questionnaire.
|
|
191
196
|
*/
|
|
192
197
|
restingNoteRows(state: DialogState): Component[] {
|
|
193
|
-
if (this.restingNoteRowCount(state) === 0)
|
|
198
|
+
if (this.restingNoteRowCount(state) === 0) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
194
201
|
const note = noteForTab(state, state.currentTab);
|
|
195
|
-
if (note.length === 0)
|
|
202
|
+
if (note.length === 0) {
|
|
203
|
+
return [new Spacer(1)];
|
|
204
|
+
}
|
|
196
205
|
return [
|
|
197
206
|
new OneLineClippedText(
|
|
198
207
|
this.config.theme.fg("dim", `${NOTES_LABEL} ${collapseToOneLine(note)}`),
|
|
@@ -213,9 +222,13 @@ export class QuestionTabStrategy implements TabContentStrategy {
|
|
|
213
222
|
* then, and its own height is the intentional expansion.
|
|
214
223
|
*/
|
|
215
224
|
restingNoteRowCount(state: DialogState): number {
|
|
216
|
-
if (state.notesVisible)
|
|
225
|
+
if (state.notesVisible) {
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
217
228
|
for (let i = 0; i < this.config.questions.length; i++) {
|
|
218
|
-
if (noteForTab(state, i).length > 0)
|
|
229
|
+
if (noteForTab(state, i).length > 0) {
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
219
232
|
}
|
|
220
233
|
return 0;
|
|
221
234
|
}
|
|
@@ -273,13 +286,17 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
273
286
|
const c = new Container();
|
|
274
287
|
for (let i = 0; i < this.config.questions.length; i++) {
|
|
275
288
|
const q = this.config.questions[i];
|
|
276
|
-
if (!q)
|
|
289
|
+
if (!q) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
277
292
|
const a = state.answers.get(i);
|
|
278
293
|
const note = noteForTab(state, i);
|
|
279
294
|
// A note with no answer still gets an entry. Skipping it, which is what
|
|
280
295
|
// this loop used to do, dropped the note from the review and from the
|
|
281
296
|
// result: the user wrote something and was never told it went nowhere.
|
|
282
|
-
if (!a && note.length === 0)
|
|
297
|
+
if (!a && note.length === 0) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
283
300
|
c.addChild(new Text(this.config.theme.fg("muted", ` ● ${tabLabel(q.header, i)}`), 1, 0));
|
|
284
301
|
// No arrow row when there is no answer. The absent row is the signal, and
|
|
285
302
|
// the footer already names the question in its incomplete warning, so
|
|
@@ -326,7 +343,9 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
326
343
|
midRows(state: DialogState): Component[] {
|
|
327
344
|
// The question tabs' notes editor, mirrored for the global note. The draft
|
|
328
345
|
// itself is reducer-owned; this only renders it.
|
|
329
|
-
if (!state.notesVisible)
|
|
346
|
+
if (!state.notesVisible) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
330
349
|
return [
|
|
331
350
|
new Text(this.config.theme.fg("muted", GLOBAL_NOTES_HEADER), 1, 0),
|
|
332
351
|
this.config.notesInput,
|
|
@@ -338,7 +357,9 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
338
357
|
const missing: string[] = [];
|
|
339
358
|
for (let i = 0; i < this.config.questions.length; i++) {
|
|
340
359
|
const q = this.config.questions[i];
|
|
341
|
-
if (q && !state.answers.has(i))
|
|
360
|
+
if (q && !state.answers.has(i)) {
|
|
361
|
+
missing.push(tabLabel(q.header, i));
|
|
362
|
+
}
|
|
342
363
|
}
|
|
343
364
|
const promptText =
|
|
344
365
|
missing.length === 0
|
|
@@ -380,9 +401,13 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
380
401
|
}
|
|
381
402
|
|
|
382
403
|
function remainingLabel(state: DialogState): string | undefined {
|
|
383
|
-
if (state.timerCancelled)
|
|
404
|
+
if (state.timerCancelled) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
384
407
|
const ms = state.remainingMs;
|
|
385
|
-
if (ms === undefined)
|
|
408
|
+
if (ms === undefined) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
386
411
|
const secs = Math.max(0, Math.ceil(ms / 1000));
|
|
387
412
|
return `${secs}s left`;
|
|
388
413
|
}
|
|
@@ -407,8 +432,17 @@ export function buildHintText(
|
|
|
407
432
|
state: DialogState,
|
|
408
433
|
collapseKey: string,
|
|
409
434
|
): string {
|
|
410
|
-
const
|
|
411
|
-
|
|
435
|
+
const typingOnMulti = question?.multiSelect === true && state.inputMode;
|
|
436
|
+
// Typing on the multi-select free-text row: Enter ticks that row rather than
|
|
437
|
+
// choosing an answer, and Space belongs to the draft, so neither resting part
|
|
438
|
+
// is true here.
|
|
439
|
+
const parts: string[] = [
|
|
440
|
+
typingOnMulti ? HINT_PART_ENTER_CONFIRM : HINT_PART_ENTER,
|
|
441
|
+
HINT_PART_NAV,
|
|
442
|
+
];
|
|
443
|
+
if (question?.multiSelect === true && !typingOnMulti) {
|
|
444
|
+
parts.push(HINT_PART_TOGGLE);
|
|
445
|
+
}
|
|
412
446
|
if (question && !state.notesVisible && !state.inputMode) {
|
|
413
447
|
// "add" is wrong once one exists, and the hint is the only affordance
|
|
414
448
|
// telling a keyboard user the resting row can be reopened at all.
|
|
@@ -416,17 +450,25 @@ export function buildHintText(
|
|
|
416
450
|
noteForTab(state, state.currentTab).length > 0 ? HINT_PART_NOTES_EDIT : HINT_PART_NOTES,
|
|
417
451
|
);
|
|
418
452
|
}
|
|
419
|
-
if (isMulti)
|
|
453
|
+
if (isMulti) {
|
|
454
|
+
parts.push(HINT_PART_TAB);
|
|
455
|
+
}
|
|
420
456
|
parts.push(HINT_PART_CANCEL);
|
|
421
457
|
if (collapseKey !== COLLAPSE_KEY_OFF) {
|
|
422
458
|
parts.push(
|
|
423
459
|
HINT_PART_COLLAPSE_TEMPLATE.replace(KEY_PLACEHOLDER, formatKeySpecForDisplay(collapseKey)),
|
|
424
460
|
);
|
|
425
461
|
}
|
|
426
|
-
if (state.notesVisible || state.inputMode)
|
|
427
|
-
|
|
462
|
+
if (state.notesVisible || state.inputMode) {
|
|
463
|
+
parts.push(HINT_PART_NEW_LINE);
|
|
464
|
+
}
|
|
465
|
+
if (state.inputMode) {
|
|
466
|
+
parts.push(HINT_PART_CLEAR);
|
|
467
|
+
}
|
|
428
468
|
const rem = remainingLabel(state);
|
|
429
|
-
if (rem)
|
|
469
|
+
if (rem) {
|
|
470
|
+
parts.push(rem);
|
|
471
|
+
}
|
|
430
472
|
return parts.join(" · ");
|
|
431
473
|
}
|
|
432
474
|
|
|
@@ -438,10 +480,16 @@ export function buildHintText(
|
|
|
438
480
|
*/
|
|
439
481
|
export function buildSubmitHintText(state: DialogState): string {
|
|
440
482
|
const parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
|
|
441
|
-
if (!state.notesVisible)
|
|
483
|
+
if (!state.notesVisible) {
|
|
484
|
+
parts.push(REVIEW_GLOBAL_HINT);
|
|
485
|
+
}
|
|
442
486
|
parts.push(HINT_PART_CANCEL);
|
|
443
|
-
if (state.notesVisible)
|
|
487
|
+
if (state.notesVisible) {
|
|
488
|
+
parts.push(HINT_PART_NEW_LINE);
|
|
489
|
+
}
|
|
444
490
|
const rem = remainingLabel(state);
|
|
445
|
-
if (rem)
|
|
491
|
+
if (rem) {
|
|
492
|
+
parts.push(rem);
|
|
493
|
+
}
|
|
446
494
|
return parts.join(" · ");
|
|
447
495
|
}
|