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.
Files changed (37) hide show
  1. package/README.md +95 -32
  2. package/docs/configuration.md +93 -0
  3. package/docs/hosts.md +71 -0
  4. package/docs/keyboard.md +65 -0
  5. package/docs/tool-schema.md +124 -0
  6. package/package.json +12 -2
  7. package/preview/popup-submit.webp +0 -0
  8. package/preview/popup-with-notes.webp +0 -0
  9. package/preview/popup-with-tab.webp +0 -0
  10. package/src/ask-user-question.ts +54 -21
  11. package/src/config.ts +118 -24
  12. package/src/rpc-fallback.ts +85 -28
  13. package/src/state/build-questionnaire.ts +45 -8
  14. package/src/state/external-editor.ts +24 -12
  15. package/src/state/key-router.ts +152 -41
  16. package/src/state/questionnaire-session.ts +79 -14
  17. package/src/state/row-intent.ts +6 -2
  18. package/src/state/selectors/derivations.ts +18 -6
  19. package/src/state/selectors/focus.ts +6 -2
  20. package/src/state/selectors/projections.ts +9 -1
  21. package/src/state/state-reducer.ts +110 -38
  22. package/src/tool/response-envelope.ts +71 -22
  23. package/src/tool/types.ts +28 -4
  24. package/src/view/component-binding.ts +3 -1
  25. package/src/view/components/inline-input.ts +3 -1
  26. package/src/view/components/multi-select-view.ts +32 -7
  27. package/src/view/components/option-list-view.ts +5 -0
  28. package/src/view/components/preview/markdown-content-cache.ts +75 -23
  29. package/src/view/components/preview/preview-block-renderer.ts +8 -1
  30. package/src/view/components/preview/preview-box-renderer.ts +4 -5
  31. package/src/view/components/preview/preview-layout-decider.ts +52 -15
  32. package/src/view/components/preview/preview-pane.ts +72 -28
  33. package/src/view/components/tab-bar.ts +26 -8
  34. package/src/view/components/wrapping-select.ts +110 -37
  35. package/src/view/dialog-builder.ts +44 -10
  36. package/src/view/props-adapter.ts +29 -7
  37. 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) return 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) return this.restingNoteRows(state);
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) return [];
198
+ if (this.restingNoteRowCount(state) === 0) {
199
+ return [];
200
+ }
194
201
  const note = noteForTab(state, state.currentTab);
195
- if (note.length === 0) return [new Spacer(1)];
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) return 0;
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) return 1;
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) continue;
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) continue;
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) return [];
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)) missing.push(tabLabel(q.header, 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) return undefined;
404
+ if (state.timerCancelled) {
405
+ return undefined;
406
+ }
384
407
  const ms = state.remainingMs;
385
- if (ms === undefined) return 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 parts: string[] = [HINT_PART_ENTER, HINT_PART_NAV];
411
- if (question?.multiSelect === true) parts.push(HINT_PART_TOGGLE);
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) parts.push(HINT_PART_TAB);
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) parts.push(HINT_PART_NEW_LINE);
427
- if (state.inputMode) parts.push(HINT_PART_CLEAR);
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) parts.push(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) parts.push(REVIEW_GLOBAL_HINT);
483
+ if (!state.notesVisible) {
484
+ parts.push(REVIEW_GLOBAL_HINT);
485
+ }
442
486
  parts.push(HINT_PART_CANCEL);
443
- if (state.notesVisible) parts.push(HINT_PART_NEW_LINE);
487
+ if (state.notesVisible) {
488
+ parts.push(HINT_PART_NEW_LINE);
489
+ }
444
490
  const rem = remainingLabel(state);
445
- if (rem) parts.push(rem);
491
+ if (rem) {
492
+ parts.push(rem);
493
+ }
446
494
  return parts.join(" · ");
447
495
  }