pi-ask-tool-extension 0.2.4 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ask-tool-extension",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Ask tool extension for pi with tabbed questioning and inline note editing",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +1,4 @@
1
- import { wrapTextWithAnsi } from "@mariozechner/pi-tui";
1
+ import { CURSOR_MARKER, wrapTextWithAnsi } from "@mariozechner/pi-tui";
2
2
 
3
3
  const INLINE_NOTE_SEPARATOR = " — note: ";
4
4
  const INLINE_EDIT_CURSOR_INVERT_ON = "\u001b[7m";
@@ -17,14 +17,19 @@ function clampCursorIndex(index: number, rawTextLength: number): number {
17
17
  return Math.floor(index);
18
18
  }
19
19
 
20
- function buildEditingInlineNote(rawNote: string, editingCursorIndex?: number): string {
20
+ function buildEditingInlineNote(
21
+ rawNote: string,
22
+ editingCursorIndex?: number,
23
+ includeHardwareCursorMarker = false,
24
+ ): string {
21
25
  const cursorIndex = clampCursorIndex(editingCursorIndex ?? rawNote.length, rawNote.length);
22
26
  const beforeCursor = sanitizeNoteForInlineDisplay(rawNote.slice(0, cursorIndex));
23
27
  const rawCharAtCursor = rawNote.slice(cursorIndex, cursorIndex + 1);
24
28
  const charAtCursor = sanitizeNoteForInlineDisplay(rawCharAtCursor) || " ";
25
29
  const afterCursorStartIndex = rawCharAtCursor.length > 0 ? cursorIndex + 1 : cursorIndex;
26
30
  const afterCursor = sanitizeNoteForInlineDisplay(rawNote.slice(afterCursorStartIndex));
27
- const cursorCell = `${INLINE_EDIT_CURSOR_INVERT_ON}${charAtCursor}${INLINE_EDIT_CURSOR_INVERT_OFF}`;
31
+ const cursorMarker = includeHardwareCursorMarker ? CURSOR_MARKER : "";
32
+ const cursorCell = `${cursorMarker}${INLINE_EDIT_CURSOR_INVERT_ON}${charAtCursor}${INLINE_EDIT_CURSOR_INVERT_OFF}`;
28
33
  return `${beforeCursor}${cursorCell}${afterCursor}`;
29
34
  }
30
35
 
@@ -48,6 +53,7 @@ export function buildOptionLabelWithInlineNote(
48
53
  isEditingNote: boolean,
49
54
  maxInlineLabelLength?: number,
50
55
  editingCursorIndex?: number,
56
+ includeHardwareCursorMarker = false,
51
57
  ): string {
52
58
  const sanitizedNote = sanitizeNoteForInlineDisplay(rawNote);
53
59
  if (!isEditingNote && sanitizedNote.trim().length === 0) {
@@ -55,7 +61,9 @@ export function buildOptionLabelWithInlineNote(
55
61
  }
56
62
 
57
63
  const labelPrefix = `${baseOptionLabel}${INLINE_NOTE_SEPARATOR}`;
58
- const inlineNote = isEditingNote ? buildEditingInlineNote(rawNote, editingCursorIndex) : sanitizedNote.trim();
64
+ const inlineNote = isEditingNote
65
+ ? buildEditingInlineNote(rawNote, editingCursorIndex, includeHardwareCursorMarker)
66
+ : sanitizedNote.trim();
59
67
  const inlineLabel = `${labelPrefix}${inlineNote}`;
60
68
 
61
69
  if (maxInlineLabelLength == null) {
@@ -74,6 +82,7 @@ export function buildWrappedOptionLabelWithInlineNote(
74
82
  maxInlineLabelLength: number,
75
83
  wrapPadding = INLINE_NOTE_WRAP_PADDING,
76
84
  editingCursorIndex?: number,
85
+ includeHardwareCursorMarker = false,
77
86
  ): string[] {
78
87
  const inlineLabel = buildOptionLabelWithInlineNote(
79
88
  baseOptionLabel,
@@ -81,6 +90,7 @@ export function buildWrappedOptionLabelWithInlineNote(
81
90
  isEditingNote,
82
91
  undefined,
83
92
  editingCursorIndex,
93
+ includeHardwareCursorMarker,
84
94
  );
85
95
  const sanitizedWrapPadding = Number.isFinite(wrapPadding) ? Math.max(0, Math.floor(wrapPadding)) : 0;
86
96
  const sanitizedMaxInlineLabelLength = Number.isFinite(maxInlineLabelLength)
@@ -111,6 +111,12 @@ export async function askSingleQuestionWithInlineNote(
111
111
  noteEditor.setText(getRawNoteForOption(cursorOptionIndex));
112
112
  };
113
113
 
114
+ const openNoteEditorForCurrentOption = () => {
115
+ if (isNoteEditorOpen) return;
116
+ isNoteEditorOpen = true;
117
+ loadCurrentNoteIntoEditor();
118
+ };
119
+
114
120
  const saveCurrentNoteFromEditor = (value: string) => {
115
121
  noteByOptionIndex.set(cursorOptionIndex, value);
116
122
  };
@@ -181,6 +187,7 @@ export async function askSingleQuestionWithInlineNote(
181
187
  Math.max(1, width - prefixWidth),
182
188
  INLINE_NOTE_WRAP_PADDING,
183
189
  isEditingThisOption ? activeEditingCursorIndex : undefined,
190
+ isEditingThisOption,
184
191
  );
185
192
  const continuationPrefix = " ".repeat(prefixWidth);
186
193
  addLine(`${cursorPrefix}${theme.fg(optionColor, `${markerText}${wrappedInlineLabelLines[0] ?? ""}`)}`);
@@ -217,25 +224,38 @@ export async function askSingleQuestionWithInlineNote(
217
224
  requestUiRerender();
218
225
  return;
219
226
  }
220
- noteEditor.handleInput(data);
221
- requestUiRerender();
222
- return;
227
+
228
+ if (
229
+ (matchesKey(data, Key.up) || matchesKey(data, Key.down)) &&
230
+ getTrimmedNoteForOption(cursorOptionIndex).length === 0
231
+ ) {
232
+ isNoteEditorOpen = false;
233
+ } else {
234
+ noteEditor.handleInput(data);
235
+ requestUiRerender();
236
+ return;
237
+ }
223
238
  }
224
239
 
225
240
  if (matchesKey(data, Key.up)) {
226
241
  cursorOptionIndex = Math.max(0, cursorOptionIndex - 1);
242
+ if (selectableOptionLabels[cursorOptionIndex] === OTHER_OPTION) {
243
+ openNoteEditorForCurrentOption();
244
+ }
227
245
  requestUiRerender();
228
246
  return;
229
247
  }
230
248
  if (matchesKey(data, Key.down)) {
231
249
  cursorOptionIndex = Math.min(selectableOptionLabels.length - 1, cursorOptionIndex + 1);
250
+ if (selectableOptionLabels[cursorOptionIndex] === OTHER_OPTION) {
251
+ openNoteEditorForCurrentOption();
252
+ }
232
253
  requestUiRerender();
233
254
  return;
234
255
  }
235
256
 
236
257
  if (matchesKey(data, Key.tab)) {
237
- isNoteEditorOpen = true;
238
- loadCurrentNoteIntoEditor();
258
+ openNoteEditorForCurrentOption();
239
259
  requestUiRerender();
240
260
  return;
241
261
  }
@@ -257,10 +277,19 @@ export async function askSingleQuestionWithInlineNote(
257
277
 
258
278
  if (matchesKey(data, Key.escape)) {
259
279
  done({ cancelled: true });
280
+ return;
281
+ }
282
+
283
+ if (selectableOptionLabels[cursorOptionIndex] === OTHER_OPTION) {
284
+ openNoteEditorForCurrentOption();
285
+ noteEditor.handleInput(data);
286
+ requestUiRerender();
287
+ return;
260
288
  }
261
289
  };
262
290
 
263
291
  return {
292
+ focused: true,
264
293
  render,
265
294
  invalidate: () => {
266
295
  cachedRenderedLines = undefined;
@@ -388,6 +388,7 @@ export async function askQuestionsWithTabs(
388
388
  Math.max(1, width - prefixWidth),
389
389
  INLINE_NOTE_WRAP_PADDING,
390
390
  isEditingThisOption ? activeEditingCursorIndex : undefined,
391
+ isEditingThisOption,
391
392
  );
392
393
  const continuationPrefix = " ".repeat(prefixWidth);
393
394
  addLine(`${cursorPrefix}${theme.fg(optionColor, `${markerText}${wrappedInlineLabelLines[0] ?? ""}`)}`);
@@ -404,12 +405,12 @@ export async function askQuestionsWithTabs(
404
405
  addLine(
405
406
  theme.fg(
406
407
  "dim",
407
- " ↑↓ move • Enter toggle/select • Tab add note • ←/→ switch tabs • Esc cancel",
408
+ " ↑↓ move • Space toggle/select • Enter next • Tab add note • ←/→ switch tabs • Esc cancel",
408
409
  ),
409
410
  );
410
411
  } else {
411
412
  addLine(
412
- theme.fg("dim", " ↑↓ move • Enter select • Tab add note • ←/→ switch tabs • Esc cancel"),
413
+ theme.fg("dim", " ↑↓ move • Space/Enter select • Tab add note • ←/→ switch tabs • Esc cancel"),
413
414
  );
414
415
  }
415
416
  }
@@ -449,19 +450,44 @@ export async function askQuestionsWithTabs(
449
450
  requestUiRerender();
450
451
  return;
451
452
  }
452
- noteEditor.handleInput(data);
453
- requestUiRerender();
454
- return;
453
+
454
+ const questionIndex = getActiveQuestionIndex();
455
+ const cursorOptionIndex = questionIndex == null ? 0 : cursorOptionIndexByQuestion[questionIndex];
456
+ const noteIsEmpty = questionIndex == null || getTrimmedQuestionNote(questionIndex, cursorOptionIndex).length === 0;
457
+ if (
458
+ noteIsEmpty &&
459
+ (matchesKey(data, Key.up) || matchesKey(data, Key.down) || matchesKey(data, Key.left) || matchesKey(data, Key.right))
460
+ ) {
461
+ isNoteEditorOpen = false;
462
+ } else {
463
+ noteEditor.handleInput(data);
464
+ requestUiRerender();
465
+ return;
466
+ }
455
467
  }
456
468
 
457
469
  if (matchesKey(data, Key.left)) {
458
470
  activeTabIndex = (activeTabIndex - 1 + preparedQuestions.length + 1) % (preparedQuestions.length + 1);
471
+ if (getActiveQuestionIndex() != null) {
472
+ const questionIndex = getActiveQuestionIndex() as number;
473
+ if (preparedQuestions[questionIndex].options[cursorOptionIndexByQuestion[questionIndex]] === OTHER_OPTION) {
474
+ openNoteEditorForActiveOption();
475
+ return;
476
+ }
477
+ }
459
478
  requestUiRerender();
460
479
  return;
461
480
  }
462
481
 
463
482
  if (matchesKey(data, Key.right)) {
464
483
  activeTabIndex = (activeTabIndex + 1) % (preparedQuestions.length + 1);
484
+ if (getActiveQuestionIndex() != null) {
485
+ const questionIndex = getActiveQuestionIndex() as number;
486
+ if (preparedQuestions[questionIndex].options[cursorOptionIndexByQuestion[questionIndex]] === OTHER_OPTION) {
487
+ openNoteEditorForActiveOption();
488
+ return;
489
+ }
490
+ }
465
491
  requestUiRerender();
466
492
  return;
467
493
  }
@@ -482,6 +508,10 @@ export async function askQuestionsWithTabs(
482
508
 
483
509
  if (matchesKey(data, Key.up)) {
484
510
  cursorOptionIndexByQuestion[questionIndex] = Math.max(0, cursorOptionIndexByQuestion[questionIndex] - 1);
511
+ if (preparedQuestion.options[cursorOptionIndexByQuestion[questionIndex]] === OTHER_OPTION) {
512
+ openNoteEditorForActiveOption();
513
+ return;
514
+ }
485
515
  requestUiRerender();
486
516
  return;
487
517
  }
@@ -491,6 +521,10 @@ export async function askQuestionsWithTabs(
491
521
  preparedQuestion.options.length - 1,
492
522
  cursorOptionIndexByQuestion[questionIndex] + 1,
493
523
  );
524
+ if (preparedQuestion.options[cursorOptionIndexByQuestion[questionIndex]] === OTHER_OPTION) {
525
+ openNoteEditorForActiveOption();
526
+ return;
527
+ }
494
528
  requestUiRerender();
495
529
  return;
496
530
  }
@@ -500,7 +534,7 @@ export async function askQuestionsWithTabs(
500
534
  return;
501
535
  }
502
536
 
503
- if (matchesKey(data, Key.enter)) {
537
+ if (matchesKey(data, Key.space)) {
504
538
  const cursorOptionIndex = cursorOptionIndexByQuestion[questionIndex];
505
539
 
506
540
  if (preparedQuestion.multi) {
@@ -533,6 +567,37 @@ export async function askQuestionsWithTabs(
533
567
  return;
534
568
  }
535
569
 
570
+ requestUiRerender();
571
+ return;
572
+ }
573
+
574
+ if (matchesKey(data, Key.enter)) {
575
+ const cursorOptionIndex = cursorOptionIndexByQuestion[questionIndex];
576
+
577
+ if (preparedQuestion.multi) {
578
+ if (
579
+ cursorOptionIndex === preparedQuestion.otherOptionIndex &&
580
+ selectedOptionIndexesByQuestion[questionIndex].includes(cursorOptionIndex) &&
581
+ getTrimmedQuestionNote(questionIndex, cursorOptionIndex).length === 0
582
+ ) {
583
+ openNoteEditorForActiveOption();
584
+ return;
585
+ }
586
+
587
+ advanceToNextTabOrSubmit();
588
+ requestUiRerender();
589
+ return;
590
+ }
591
+
592
+ selectedOptionIndexesByQuestion[questionIndex] = [cursorOptionIndex];
593
+ if (
594
+ cursorOptionIndex === preparedQuestion.otherOptionIndex &&
595
+ getTrimmedQuestionNote(questionIndex, cursorOptionIndex).length === 0
596
+ ) {
597
+ openNoteEditorForActiveOption();
598
+ return;
599
+ }
600
+
536
601
  advanceToNextTabOrSubmit();
537
602
  requestUiRerender();
538
603
  return;
@@ -540,10 +605,19 @@ export async function askQuestionsWithTabs(
540
605
 
541
606
  if (matchesKey(data, Key.escape)) {
542
607
  done(createTabsUiStateSnapshot(true, selectedOptionIndexesByQuestion, noteByQuestionByOption));
608
+ return;
609
+ }
610
+
611
+ if (preparedQuestion.options[cursorOptionIndexByQuestion[questionIndex]] === OTHER_OPTION) {
612
+ openNoteEditorForActiveOption();
613
+ noteEditor.handleInput(data);
614
+ requestUiRerender();
615
+ return;
543
616
  }
544
617
  };
545
618
 
546
619
  return {
620
+ focused: true,
547
621
  render,
548
622
  invalidate: () => {
549
623
  cachedRenderedLines = undefined;