@danypops/pi-papyrus 0.43.0 → 0.43.3

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/extension/src/artifact-browser.ts +54 -31
  2. package/extension/src/artifact-detail-view.ts +52 -42
  3. package/extension/src/artifact-format.ts +11 -5
  4. package/extension/src/artifact-relationship-lines.ts +4 -5
  5. package/extension/src/beautiful-mermaid-renderer.ts +3 -5
  6. package/extension/src/context-budget.ts +8 -5
  7. package/extension/src/context-hub-contribution.ts +6 -2
  8. package/extension/src/context-injection-telemetry.ts +2 -2
  9. package/extension/src/discuss-ask-layout.ts +3 -1
  10. package/extension/src/discuss-ask-view.ts +437 -111
  11. package/extension/src/discuss.ts +64 -15
  12. package/extension/src/discussion-detail-view.ts +44 -22
  13. package/extension/src/docs.ts +3 -2
  14. package/extension/src/domain-tools.ts +79 -34
  15. package/extension/src/index.ts +170 -66
  16. package/extension/src/markdown.ts +3 -7
  17. package/extension/src/note-widget.ts +1 -1
  18. package/extension/src/notes.ts +3 -8
  19. package/extension/src/playbook-bridge.ts +17 -6
  20. package/extension/src/playbooks.ts +17 -7
  21. package/extension/src/rules.ts +5 -5
  22. package/extension/src/service-client.ts +23 -8
  23. package/extension/src/skill-catalog-footprint.ts +1 -1
  24. package/extension/src/task-detail-format.ts +9 -9
  25. package/extension/src/task-detail-view.ts +36 -29
  26. package/extension/src/task-focus-events.ts +3 -2
  27. package/extension/src/task-graph.ts +16 -12
  28. package/extension/src/task-presentation.ts +2 -6
  29. package/extension/src/task-widget.ts +12 -8
  30. package/extension/src/tasks.ts +148 -57
  31. package/extension/src/tool-rendering/artifact-card.ts +1 -4
  32. package/extension/src/tool-rendering/artifact-list.ts +23 -24
  33. package/extension/src/tool-rendering/index.ts +2 -6
  34. package/extension/src/tool-rendering/render-model.ts +69 -55
  35. package/extension/src/vehicle-artifact-renderers.ts +58 -0
  36. package/extension/src/vehicle-notes-client.ts +35 -7
  37. package/package.json +5 -5
@@ -13,8 +13,8 @@
13
13
  import type { AgentToolUpdateCallback, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
14
14
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
15
15
  import {
16
- Container,
17
16
  type Component,
17
+ Container,
18
18
  CURSOR_MARKER,
19
19
  decodeKittyPrintable,
20
20
  Editor,
@@ -33,7 +33,7 @@ import {
33
33
  truncateToWidth,
34
34
  wrapTextWithAnsi,
35
35
  } from "@earendil-works/pi-tui";
36
- import { renderSingleSelectRows, type AskOption } from "./discuss-ask-layout.ts";
36
+ import { type AskOption, renderSingleSelectRows } from "./discuss-ask-layout.ts";
37
37
 
38
38
  /** See pi-ask-user's identical safeMarkdownTheme() comment: a broken theme Proxy throws only on
39
39
  * property access, not construction, so a bare try/catch around getMarkdownTheme() alone would
@@ -87,9 +87,7 @@ export interface AskAnswer {
87
87
  selected?: string[];
88
88
  }
89
89
 
90
- type AskResponse =
91
- | { kind: "selection"; selections: string[]; comment?: string }
92
- | { kind: "freeform"; text: string };
90
+ type AskResponse = { kind: "selection"; selections: string[]; comment?: string } | { kind: "freeform"; text: string };
93
91
 
94
92
  function normalizeOptionalComment(text: string | null | undefined): string | undefined {
95
93
  const trimmed = text?.trim();
@@ -99,9 +97,18 @@ function normalizeOptionalComment(text: string | null | undefined): string | und
99
97
  function parseBooleanPreference(value: string | undefined): boolean | undefined {
100
98
  if (value === undefined) return undefined;
101
99
  switch (value.trim().toLowerCase()) {
102
- case "1": case "true": case "yes": case "on": return true;
103
- case "0": case "false": case "no": case "off": return false;
104
- default: return undefined;
100
+ case "1":
101
+ case "true":
102
+ case "yes":
103
+ case "on":
104
+ return true;
105
+ case "0":
106
+ case "false":
107
+ case "no":
108
+ case "off":
109
+ return false;
110
+ default:
111
+ return undefined;
105
112
  }
106
113
  }
107
114
 
@@ -114,7 +121,9 @@ function createSelectionResponse(selections: string[], comment?: string | null):
114
121
  const normalizedSelections = selections.map((selection) => selection.trim()).filter(Boolean);
115
122
  if (normalizedSelections.length === 0) return null;
116
123
  const normalizedComment = normalizeOptionalComment(comment);
117
- return normalizedComment ? { kind: "selection", selections: normalizedSelections, comment: normalizedComment } : { kind: "selection", selections: normalizedSelections };
124
+ return normalizedComment
125
+ ? { kind: "selection", selections: normalizedSelections, comment: normalizedComment }
126
+ : { kind: "selection", selections: normalizedSelections };
118
127
  }
119
128
 
120
129
  function toAskAnswer(response: AskResponse): AskAnswer {
@@ -133,7 +142,10 @@ function buildCommentPrompt(prompt: string, selections: string[]): string {
133
142
  }
134
143
 
135
144
  function parseDialogSelections(input: string): string[] {
136
- return input.split(",").map((selection) => selection.trim()).filter(Boolean);
145
+ return input
146
+ .split(",")
147
+ .map((selection) => selection.trim())
148
+ .filter(Boolean);
137
149
  }
138
150
 
139
151
  function isCancelledInput(value: unknown): value is null | undefined {
@@ -159,7 +171,11 @@ const BOX_BORDER_RIGHT = " │";
159
171
  const BOX_BORDER_OVERHEAD = BOX_BORDER_LEFT.length + BOX_BORDER_RIGHT.length;
160
172
 
161
173
  class BoxBorderTop implements Component {
162
- constructor(private color: (s: string) => string, private title?: string, private titleColor?: (s: string) => string) {}
174
+ constructor(
175
+ private color: (s: string) => string,
176
+ private title?: string,
177
+ private titleColor?: (s: string) => string,
178
+ ) {}
163
179
  invalidate(): void {}
164
180
  render(width: number): string[] {
165
181
  const inner = Math.max(0, width - 2);
@@ -192,7 +208,9 @@ function literalHint(theme: Theme, key: string, description: string): string {
192
208
  return `${theme.fg("dim", key)}${theme.fg("muted", ` ${description}`)}`;
193
209
  }
194
210
 
195
- type ResolvedShortcut = { disabled: false; spec: string; matches: (data: string) => boolean } | { disabled: true; spec: null; matches: (data: string) => false };
211
+ type ResolvedShortcut =
212
+ | { disabled: false; spec: string; matches: (data: string) => boolean }
213
+ | { disabled: true; spec: null; matches: (data: string) => false };
196
214
 
197
215
  const DISABLED_SHORTCUT: ResolvedShortcut = { disabled: true, spec: null, matches: (() => false) as (data: string) => false };
198
216
  const SHORTCUT_DISABLE_VALUES = new Set(["off", "none", "disabled", ""]);
@@ -285,18 +303,35 @@ class MultiSelectList implements Component {
285
303
  private commentToggle: ResolvedShortcut,
286
304
  ) {}
287
305
 
288
- public isCommentEnabled(): boolean { return this.commentEnabled; }
289
- invalidate(): void { this.cachedWidth = undefined; this.cachedLines = undefined; }
306
+ public isCommentEnabled(): boolean {
307
+ return this.commentEnabled;
308
+ }
309
+ invalidate(): void {
310
+ this.cachedWidth = undefined;
311
+ this.cachedLines = undefined;
312
+ }
290
313
 
291
- private getItemCount(): number { return this.options.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0); }
292
- private getCommentToggleIndex(): number | null { return this.allowComment ? this.options.length : null; }
293
- private getFreeformIndex(): number { return this.options.length + (this.allowComment ? 1 : 0); }
294
- private isCommentToggleRow(index: number): boolean { const i = this.getCommentToggleIndex(); return i !== null && index === i; }
295
- private isFreeformRow(index: number): boolean { return this.allowFreeform && index === this.getFreeformIndex(); }
314
+ private getItemCount(): number {
315
+ return this.options.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
316
+ }
317
+ private getCommentToggleIndex(): number | null {
318
+ return this.allowComment ? this.options.length : null;
319
+ }
320
+ private getFreeformIndex(): number {
321
+ return this.options.length + (this.allowComment ? 1 : 0);
322
+ }
323
+ private isCommentToggleRow(index: number): boolean {
324
+ const i = this.getCommentToggleIndex();
325
+ return i !== null && index === i;
326
+ }
327
+ private isFreeformRow(index: number): boolean {
328
+ return this.allowFreeform && index === this.getFreeformIndex();
329
+ }
296
330
 
297
331
  private toggle(index: number): void {
298
332
  if (index < 0 || index >= this.options.length) return;
299
- if (this.checked.has(index)) this.checked.delete(index); else this.checked.add(index);
333
+ if (this.checked.has(index)) this.checked.delete(index);
334
+ else this.checked.add(index);
300
335
  }
301
336
 
302
337
  private toggleComment(): void {
@@ -306,36 +341,73 @@ class MultiSelectList implements Component {
306
341
  }
307
342
 
308
343
  handleInput(data: string): void {
309
- if (this.keybindings.matches(data, "tui.select.cancel")) { this.onCancel?.(); return; }
344
+ if (this.keybindings.matches(data, "tui.select.cancel")) {
345
+ this.onCancel?.();
346
+ return;
347
+ }
310
348
  const count = this.getItemCount();
311
- if (count === 0) { this.onCancel?.(); return; }
312
- if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) { this.toggleComment(); return; }
349
+ if (count === 0) {
350
+ this.onCancel?.();
351
+ return;
352
+ }
353
+ if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
354
+ this.toggleComment();
355
+ return;
356
+ }
313
357
 
314
- if (matchesSelectUp(data, this.keybindings)) { this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1; this.invalidate(); return; }
315
- if (matchesSelectDown(data, this.keybindings)) { this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1; this.invalidate(); return; }
358
+ if (matchesSelectUp(data, this.keybindings)) {
359
+ this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
360
+ this.invalidate();
361
+ return;
362
+ }
363
+ if (matchesSelectDown(data, this.keybindings)) {
364
+ this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1;
365
+ this.invalidate();
366
+ return;
367
+ }
316
368
 
317
369
  const numMatch = data.match(/^[1-9]$/);
318
370
  if (numMatch) {
319
371
  const idx = Number.parseInt(numMatch[0], 10) - 1;
320
- if (idx >= 0 && idx < this.options.length) { this.toggle(idx); this.selectedIndex = Math.min(idx, count - 1); this.invalidate(); }
372
+ if (idx >= 0 && idx < this.options.length) {
373
+ this.toggle(idx);
374
+ this.selectedIndex = Math.min(idx, count - 1);
375
+ this.invalidate();
376
+ }
321
377
  return;
322
378
  }
323
379
 
324
380
  if (matchesKey(data, Key.space)) {
325
- if (this.isCommentToggleRow(this.selectedIndex)) { this.toggleComment(); return; }
326
- if (this.isFreeformRow(this.selectedIndex)) { this.onEnterFreeform?.(); return; }
381
+ if (this.isCommentToggleRow(this.selectedIndex)) {
382
+ this.toggleComment();
383
+ return;
384
+ }
385
+ if (this.isFreeformRow(this.selectedIndex)) {
386
+ this.onEnterFreeform?.();
387
+ return;
388
+ }
327
389
  this.toggle(this.selectedIndex);
328
390
  this.invalidate();
329
391
  return;
330
392
  }
331
393
 
332
394
  if (this.keybindings.matches(data, "tui.select.confirm")) {
333
- if (this.isCommentToggleRow(this.selectedIndex)) { this.toggleComment(); return; }
334
- if (this.isFreeformRow(this.selectedIndex)) { this.onEnterFreeform?.(); return; }
335
- const selectedTitles = [...this.checked].sort((a, b) => a - b).map((i) => this.options[i]?.title).filter((t): t is string => !!t);
395
+ if (this.isCommentToggleRow(this.selectedIndex)) {
396
+ this.toggleComment();
397
+ return;
398
+ }
399
+ if (this.isFreeformRow(this.selectedIndex)) {
400
+ this.onEnterFreeform?.();
401
+ return;
402
+ }
403
+ const selectedTitles = [...this.checked]
404
+ .sort((a, b) => a - b)
405
+ .map((i) => this.options[i]?.title)
406
+ .filter((t): t is string => !!t);
336
407
  const fallback = this.options[this.selectedIndex]?.title;
337
408
  const result = selectedTitles.length > 0 ? selectedTitles : fallback ? [fallback] : [];
338
- if (result.length > 0) this.onSubmit?.(result); else this.onCancel?.();
409
+ if (result.length > 0) this.onSubmit?.(result);
410
+ else this.onCancel?.();
339
411
  }
340
412
  }
341
413
 
@@ -344,7 +416,11 @@ class MultiSelectList implements Component {
344
416
  const theme = this.theme;
345
417
  const count = this.getItemCount();
346
418
  const maxVisible = Math.min(count, 10);
347
- if (count === 0) { this.cachedLines = [theme.fg("warning", "No options")]; this.cachedWidth = width; return this.cachedLines; }
419
+ if (count === 0) {
420
+ this.cachedLines = [theme.fg("warning", "No options")];
421
+ this.cachedWidth = width;
422
+ return this.cachedLines;
423
+ }
348
424
 
349
425
  const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), count - maxVisible));
350
426
  const endIndex = Math.min(startIndex + maxVisible, count);
@@ -356,7 +432,9 @@ class MultiSelectList implements Component {
356
432
 
357
433
  if (this.isCommentToggleRow(i)) {
358
434
  const checkbox = this.commentEnabled ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
359
- const label = isSelected ? theme.fg("accent", theme.bold(COMMENT_TOGGLE_LABEL)) : theme.fg("text", theme.bold(COMMENT_TOGGLE_LABEL));
435
+ const label = isSelected
436
+ ? theme.fg("accent", theme.bold(COMMENT_TOGGLE_LABEL))
437
+ : theme.fg("text", theme.bold(COMMENT_TOGGLE_LABEL));
360
438
  lines.push(truncateToWidth(`${prefix} ${checkbox} ${label}`, width, ""));
361
439
  continue;
362
440
  }
@@ -374,11 +452,13 @@ class MultiSelectList implements Component {
374
452
  lines.push(truncateToWidth(`${prefix} ${num} ${checkbox} ${title}`, width, ""));
375
453
  if (option.description) {
376
454
  const indent = " ";
377
- for (const w of wrapTextWithAnsi(option.description, Math.max(10, width - indent.length))) lines.push(truncateToWidth(indent + theme.fg("muted", w), width, ""));
455
+ for (const w of wrapTextWithAnsi(option.description, Math.max(10, width - indent.length)))
456
+ lines.push(truncateToWidth(indent + theme.fg("muted", w), width, ""));
378
457
  }
379
458
  }
380
459
 
381
- if (startIndex > 0 || endIndex < count) lines.push(theme.fg("dim", truncateToWidth(` (${this.selectedIndex + 1}/${count})`, width, "")));
460
+ if (startIndex > 0 || endIndex < count)
461
+ lines.push(theme.fg("dim", truncateToWidth(` (${this.selectedIndex + 1}/${count})`, width, "")));
382
462
  this.cachedWidth = width;
383
463
  this.cachedLines = lines;
384
464
  return lines;
@@ -406,26 +486,44 @@ class WrappedSingleSelectList implements Component {
406
486
  private commentToggle: ResolvedShortcut,
407
487
  ) {}
408
488
 
409
- public isCommentEnabled(): boolean { return this.commentEnabled; }
489
+ public isCommentEnabled(): boolean {
490
+ return this.commentEnabled;
491
+ }
410
492
  setMaxVisibleRows(rows: number): void {
411
493
  const next = Math.max(1, Math.floor(rows));
412
- if (next !== this.maxVisibleRows) { this.maxVisibleRows = next; this.invalidate(); }
494
+ if (next !== this.maxVisibleRows) {
495
+ this.maxVisibleRows = next;
496
+ this.invalidate();
497
+ }
498
+ }
499
+ invalidate(): void {
500
+ this.cachedWidth = undefined;
501
+ this.cachedLines = undefined;
413
502
  }
414
- invalidate(): void { this.cachedWidth = undefined; this.cachedLines = undefined; }
415
503
 
416
504
  private getFilteredOptions(): AskOption[] {
417
505
  return fuzzyFilter(this.options, this.searchQuery, (option) => `${option.title} ${option.description ?? ""}`);
418
506
  }
419
- private getItemCount(filteredOptions: AskOption[]): number { return filteredOptions.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0); }
420
- private isCommentToggleRow(index: number, filteredOptions: AskOption[]): boolean { return this.allowComment && index === filteredOptions.length; }
421
- private isFreeformRow(index: number, filteredOptions: AskOption[]): boolean { return this.allowFreeform && index === filteredOptions.length + (this.allowComment ? 1 : 0); }
507
+ private getItemCount(filteredOptions: AskOption[]): number {
508
+ return filteredOptions.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
509
+ }
510
+ private isCommentToggleRow(index: number, filteredOptions: AskOption[]): boolean {
511
+ return this.allowComment && index === filteredOptions.length;
512
+ }
513
+ private isFreeformRow(index: number, filteredOptions: AskOption[]): boolean {
514
+ return this.allowFreeform && index === filteredOptions.length + (this.allowComment ? 1 : 0);
515
+ }
422
516
 
423
517
  private toggleComment(): void {
424
518
  if (!this.allowComment) return;
425
519
  this.commentEnabled = !this.commentEnabled;
426
520
  this.invalidate();
427
521
  }
428
- private setSearchQuery(query: string): void { this.searchQuery = query; this.selectedIndex = 0; this.invalidate(); }
522
+ private setSearchQuery(query: string): void {
523
+ this.searchQuery = query;
524
+ this.selectedIndex = 0;
525
+ this.invalidate();
526
+ }
429
527
  private popSearchCharacter(): void {
430
528
  if (!this.searchQuery) return;
431
529
  const characters = [...this.searchQuery];
@@ -469,15 +567,22 @@ class WrappedSingleSelectList implements Component {
469
567
  const count = this.getItemCount(filteredOptions);
470
568
  const searchValue = this.searchQuery ? this.theme.fg("text", this.searchQuery) : this.theme.fg("dim", "type to filter");
471
569
  lines.push(truncateToWidth(`${this.theme.fg("accent", "Filter:")} ${searchValue}`, width, ""));
472
- if (this.searchQuery && filteredOptions.length === 0) lines.push(truncateToWidth(this.theme.fg("warning", "No matching options"), width, ""));
570
+ if (this.searchQuery && filteredOptions.length === 0)
571
+ lines.push(truncateToWidth(this.theme.fg("warning", "No matching options"), width, ""));
473
572
  if (count === 0) {
474
573
  if (!this.searchQuery) lines.push(truncateToWidth(this.theme.fg("warning", "No options"), width, ""));
475
574
  return lines.slice(0, this.maxVisibleRows);
476
575
  }
477
576
  const maxRows = Math.max(1, this.maxVisibleRows - lines.length);
478
577
  const optionRows = renderSingleSelectRows({
479
- options: filteredOptions, selectedIndex: this.selectedIndex, width, allowFreeform: this.allowFreeform,
480
- allowComment: this.allowComment, commentEnabled: this.commentEnabled, maxRows, hideDescriptions,
578
+ options: filteredOptions,
579
+ selectedIndex: this.selectedIndex,
580
+ width,
581
+ allowFreeform: this.allowFreeform,
582
+ allowComment: this.allowComment,
583
+ commentEnabled: this.commentEnabled,
584
+ maxRows,
585
+ hideDescriptions,
481
586
  });
482
587
  lines.push(...optionRows.map((row) => this.styleListLine(row.line, width, row.selected)));
483
588
  return lines.slice(0, this.maxVisibleRows);
@@ -521,33 +626,67 @@ class WrappedSingleSelectList implements Component {
521
626
  }
522
627
 
523
628
  handleInput(data: string): void {
524
- if (this.searchQuery && matchesKey(data, Key.escape)) { this.setSearchQuery(""); return; }
525
- if (this.keybindings.matches(data, "tui.select.cancel")) { this.onCancel?.(); return; }
526
- if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) { this.toggleComment(); return; }
629
+ if (this.searchQuery && matchesKey(data, Key.escape)) {
630
+ this.setSearchQuery("");
631
+ return;
632
+ }
633
+ if (this.keybindings.matches(data, "tui.select.cancel")) {
634
+ this.onCancel?.();
635
+ return;
636
+ }
637
+ if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
638
+ this.toggleComment();
639
+ return;
640
+ }
527
641
 
528
642
  const filteredOptions = this.getFilteredOptions();
529
643
  const count = this.getItemCount(filteredOptions);
530
644
 
531
- if (matchesSelectUp(data, this.keybindings) && count > 0) { this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1; this.invalidate(); return; }
532
- if (matchesSelectDown(data, this.keybindings) && count > 0) { this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1; this.invalidate(); return; }
645
+ if (matchesSelectUp(data, this.keybindings) && count > 0) {
646
+ this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
647
+ this.invalidate();
648
+ return;
649
+ }
650
+ if (matchesSelectDown(data, this.keybindings) && count > 0) {
651
+ this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1;
652
+ this.invalidate();
653
+ return;
654
+ }
533
655
 
534
656
  const numMatch = data.match(/^[1-9]$/);
535
657
  if (numMatch && filteredOptions.length > 0) {
536
658
  const idx = Number.parseInt(numMatch[0], 10) - 1;
537
- if (idx >= 0 && idx < filteredOptions.length) { this.selectedIndex = idx; this.invalidate(); return; }
659
+ if (idx >= 0 && idx < filteredOptions.length) {
660
+ this.selectedIndex = idx;
661
+ this.invalidate();
662
+ return;
663
+ }
538
664
  }
539
665
 
540
- if (matchesKey(data, Key.space) && count > 0 && this.isCommentToggleRow(this.selectedIndex, filteredOptions)) { this.toggleComment(); return; }
666
+ if (matchesKey(data, Key.space) && count > 0 && this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
667
+ this.toggleComment();
668
+ return;
669
+ }
541
670
 
542
671
  if (this.keybindings.matches(data, "tui.select.confirm") && count > 0) {
543
- if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) { this.toggleComment(); return; }
544
- if (this.isFreeformRow(this.selectedIndex, filteredOptions)) { this.onEnterFreeform?.(); return; }
672
+ if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
673
+ this.toggleComment();
674
+ return;
675
+ }
676
+ if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
677
+ this.onEnterFreeform?.();
678
+ return;
679
+ }
545
680
  const result = filteredOptions[this.selectedIndex]?.title;
546
- if (result) this.onSubmit?.(result); else this.onCancel?.();
681
+ if (result) this.onSubmit?.(result);
682
+ else this.onCancel?.();
547
683
  return;
548
684
  }
549
685
 
550
- if (this.keybindings.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, Key.backspace)) { this.popSearchCharacter(); return; }
686
+ if (this.keybindings.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, Key.backspace)) {
687
+ this.popSearchCharacter();
688
+ return;
689
+ }
551
690
 
552
691
  const printableInput = this.getPrintableInput(data);
553
692
  if (printableInput) this.setSearchQuery(this.searchQuery + printableInput);
@@ -568,7 +707,11 @@ class WrappedSingleSelectList implements Component {
568
707
  const previewLines = this.buildPreviewLines(splitPane.right, filteredOptions, this.maxVisibleRows);
569
708
  const rowCount = Math.min(this.maxVisibleRows, Math.max(listLines.length, previewLines.length));
570
709
  const separator = this.theme.fg("dim", SPLIT_PANE_SEPARATOR);
571
- lines = Array.from({ length: rowCount }, (_, index) => `${truncateToWidth(listLines[index] ?? "", splitPane.left, "", true)}${separator}${truncateToWidth(previewLines[index] ?? "", splitPane.right, "")}`);
710
+ lines = Array.from(
711
+ { length: rowCount },
712
+ (_, index) =>
713
+ `${truncateToWidth(listLines[index] ?? "", splitPane.left, "", true)}${separator}${truncateToWidth(previewLines[index] ?? "", splitPane.right, "")}`,
714
+ );
572
715
  }
573
716
  this.cachedWidth = width;
574
717
  this.cachedLines = lines;
@@ -601,7 +744,9 @@ class AskComponent extends Container {
601
744
  private editor?: Editor;
602
745
 
603
746
  private _focused = false;
604
- get focused(): boolean { return this._focused; }
747
+ get focused(): boolean {
748
+ return this._focused;
749
+ }
605
750
  set focused(value: boolean) {
606
751
  this._focused = value;
607
752
  if (this.editor && (this.mode === "freeform" || this.mode === "comment")) (this.editor as any).focused = value;
@@ -622,7 +767,13 @@ class AskComponent extends Container {
622
767
  private onDone: (result: AskResponse | null) => void,
623
768
  ) {
624
769
  super();
625
- this.addChild(new BoxBorderTop((s) => theme.fg("accent", s), "discuss", (s) => theme.fg("dim", theme.bold(s))));
770
+ this.addChild(
771
+ new BoxBorderTop(
772
+ (s) => theme.fg("accent", s),
773
+ "discuss",
774
+ (s) => theme.fg("dim", theme.bold(s)),
775
+ ),
776
+ );
626
777
  this.addChild(new Spacer(1));
627
778
  this.titleText = new Text("", 1, 0);
628
779
  this.addChild(this.titleText);
@@ -653,7 +804,11 @@ class AskComponent extends Container {
653
804
  else this.showSelectMode();
654
805
  }
655
806
 
656
- override invalidate(): void { super.invalidate(); this.updateStaticText(); this.updateHelpText(); }
807
+ override invalidate(): void {
808
+ super.invalidate();
809
+ this.updateStaticText();
810
+ this.updateHelpText();
811
+ }
657
812
 
658
813
  override render(width: number): string[] {
659
814
  const innerWidth = Math.max(1, width - BOX_BORDER_OVERHEAD);
@@ -702,7 +857,10 @@ class AskComponent extends Container {
702
857
  modeBudget = Math.min(this.getPreferredModeRows(), contentRows);
703
858
  modeBudget = Math.max(Math.min(this.getMinimumModeRows(), contentRows), modeBudget);
704
859
  promptBudget = Math.max(0, contentRows - modeBudget);
705
- if (promptBudget > 0 && modeBudget > 0) { separatorRows = 1; promptBudget = Math.max(0, promptBudget - separatorRows); }
860
+ if (promptBudget > 0 && modeBudget > 0) {
861
+ separatorRows = 1;
862
+ promptBudget = Math.max(0, promptBudget - separatorRows);
863
+ }
706
864
  }
707
865
 
708
866
  const modeLines = this.renderModeLines(innerWidth, modeBudget);
@@ -720,7 +878,11 @@ class AskComponent extends Container {
720
878
  }
721
879
 
722
880
  private buildPromptLines(width: number): string[] {
723
- return [...this.titleText.render(width), ...this.questionText.render(width), ...(this.contextComponent ? ["", ...this.contextComponent.render(width)] : [])];
881
+ return [
882
+ ...this.titleText.render(width),
883
+ ...this.questionText.render(width),
884
+ ...(this.contextComponent ? ["", ...this.contextComponent.render(width)] : []),
885
+ ];
724
886
  }
725
887
 
726
888
  private getHelpBudget(bodyCapacity: number, renderedHelpRows: number): number {
@@ -790,26 +952,46 @@ class AskComponent extends Container {
790
952
  const maxStart = Math.max(0, contentLines.length - contentBudget);
791
953
  const start = cursorLineIndex >= 0 ? Math.max(0, Math.min(cursorLineIndex - contentBudget + 1, maxStart)) : maxStart;
792
954
  const visibleContentLines = contentLines.slice(start, start + contentBudget);
793
- const markedContentLines = this.applyPromptOverflowMarkers(visibleContentLines, width, start > 0, start + contentBudget < contentLines.length);
955
+ const markedContentLines = this.applyPromptOverflowMarkers(
956
+ visibleContentLines,
957
+ width,
958
+ start > 0,
959
+ start + contentBudget < contentLines.length,
960
+ );
794
961
  return [topBorder, ...markedContentLines, bottomBorder];
795
962
  }
796
963
 
797
964
  private renderPromptPane(promptLines: string[], budget: number, width: number): string[] {
798
965
  const viewportRows = Math.max(0, Math.floor(budget));
799
966
  this.promptViewportRows = viewportRows;
800
- if (viewportRows <= 0 || promptLines.length === 0) { this.promptMaxScrollOffset = 0; this.promptScrollOffset = 0; return []; }
967
+ if (viewportRows <= 0 || promptLines.length === 0) {
968
+ this.promptMaxScrollOffset = 0;
969
+ this.promptScrollOffset = 0;
970
+ return [];
971
+ }
801
972
  this.promptMaxScrollOffset = Math.max(0, promptLines.length - viewportRows);
802
973
  this.promptScrollOffset = Math.max(0, Math.min(this.promptScrollOffset, this.promptMaxScrollOffset));
803
974
  const visibleLines = promptLines.slice(this.promptScrollOffset, this.promptScrollOffset + viewportRows);
804
- return this.applyPromptOverflowMarkers(visibleLines, width, this.promptScrollOffset > 0, this.promptScrollOffset + viewportRows < promptLines.length);
975
+ return this.applyPromptOverflowMarkers(
976
+ visibleLines,
977
+ width,
978
+ this.promptScrollOffset > 0,
979
+ this.promptScrollOffset + viewportRows < promptLines.length,
980
+ );
805
981
  }
806
982
 
807
983
  private applyPromptOverflowMarkers(lines: string[], width: number, hasHiddenAbove: boolean, hasHiddenBelow: boolean): string[] {
808
984
  if (lines.length === 0) return lines;
809
985
  const marked = [...lines];
810
- if (hasHiddenAbove && hasHiddenBelow && marked.length === 1) { marked[0] = this.addPromptOverflowMarker(marked[0] ?? "", "↕", width); return marked; }
986
+ if (hasHiddenAbove && hasHiddenBelow && marked.length === 1) {
987
+ marked[0] = this.addPromptOverflowMarker(marked[0] ?? "", "↕", width);
988
+ return marked;
989
+ }
811
990
  if (hasHiddenAbove) marked[0] = this.addPromptOverflowMarker(marked[0] ?? "", "↑", width);
812
- if (hasHiddenBelow) { const lastIndex = marked.length - 1; marked[lastIndex] = this.addPromptOverflowMarker(marked[lastIndex] ?? "", "↓", width); }
991
+ if (hasHiddenBelow) {
992
+ const lastIndex = marked.length - 1;
993
+ marked[lastIndex] = this.addPromptOverflowMarker(marked[lastIndex] ?? "", "↓", width);
994
+ }
813
995
  return marked;
814
996
  }
815
997
 
@@ -827,7 +1009,13 @@ class AskComponent extends Container {
827
1009
  }
828
1010
 
829
1011
  private renderTopBorder(width: number): string {
830
- return new BoxBorderTop((s) => this.theme.fg("accent", s), "discuss", (s) => this.theme.fg("dim", this.theme.bold(s))).render(width)[0] ?? "";
1012
+ return (
1013
+ new BoxBorderTop(
1014
+ (s) => this.theme.fg("accent", s),
1015
+ "discuss",
1016
+ (s) => this.theme.fg("dim", this.theme.bold(s)),
1017
+ ).render(width)[0] ?? ""
1018
+ );
831
1019
  }
832
1020
 
833
1021
  private renderBottomBorder(width: number): string {
@@ -838,7 +1026,9 @@ class AskComponent extends Container {
838
1026
  const borderColor = (s: string) => this.theme.fg("accent", s);
839
1027
  return [
840
1028
  this.renderTopBorder(width),
841
- ...bodyLines.map((line) => `${borderColor(BOX_BORDER_LEFT)}${truncateToWidth(line, innerWidth, "", true)}${borderColor(BOX_BORDER_RIGHT)}`),
1029
+ ...bodyLines.map(
1030
+ (line) => `${borderColor(BOX_BORDER_LEFT)}${truncateToWidth(line, innerWidth, "", true)}${borderColor(BOX_BORDER_RIGHT)}`,
1031
+ ),
842
1032
  this.renderBottomBorder(width),
843
1033
  ];
844
1034
  }
@@ -849,7 +1039,9 @@ class AskComponent extends Container {
849
1039
  // normally, or "Optional comment" while in comment mode. A generic "Question" header above the
850
1040
  // real question text added nothing beyond what the question itself already says, and read
851
1041
  // confusingly like the question text WAS the header.
852
- this.titleText.setText(this.mode === "comment" ? theme.fg("accent", theme.bold("Optional comment")) : this.subtitle ? theme.fg("dim", this.subtitle) : "");
1042
+ this.titleText.setText(
1043
+ this.mode === "comment" ? theme.fg("accent", theme.bold("Optional comment")) : this.subtitle ? theme.fg("dim", this.subtitle) : "",
1044
+ );
853
1045
  this.questionText.setText(theme.fg("text", theme.bold(this.question)));
854
1046
  if (this.contextComponent && this.context) {
855
1047
  if (this.contextComponent instanceof Markdown) (this.contextComponent as Markdown).setText(`**Context:**\n${this.context}`);
@@ -860,7 +1052,10 @@ class AskComponent extends Container {
860
1052
  private updateHelpText(): void {
861
1053
  const theme = this.theme;
862
1054
  const promptScrollHint = literalHint(theme, "PgUp/PgDn", "prompt");
863
- const commentHint = this.allowComment && !this.shortcuts.commentToggle.disabled ? literalHint(theme, this.shortcuts.commentToggle.spec, "toggle context") : null;
1055
+ const commentHint =
1056
+ this.allowComment && !this.shortcuts.commentToggle.disabled
1057
+ ? literalHint(theme, this.shortcuts.commentToggle.spec, "toggle context")
1058
+ : null;
864
1059
 
865
1060
  if (this.mode === "freeform" || this.mode === "comment") {
866
1061
  const alternateCancelKeys = this.keybindings.getKeys("tui.select.cancel").filter((key) => key !== "escape" && key !== "esc");
@@ -870,35 +1065,53 @@ class AskComponent extends Container {
870
1065
  keybindingHint(theme, this.keybindings, "tui.input.newLine", "newline"),
871
1066
  literalHint(theme, "esc", canGoBack ? "back" : "cancel"),
872
1067
  canGoBack && alternateCancelKeys.length > 0 ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel") : null,
873
- ].filter((hint): hint is string => !!hint).join(" • ");
1068
+ ]
1069
+ .filter((hint): hint is string => !!hint)
1070
+ .join(" • ");
874
1071
  this.helpText.setText(theme.fg("dim", hints));
875
1072
  return;
876
1073
  }
877
1074
 
878
1075
  if (this.allowMultiple) {
879
1076
  const hints = [
880
- literalHint(theme, "↑↓", "navigate"), literalHint(theme, "space", "toggle"), commentHint, promptScrollHint,
1077
+ literalHint(theme, "↑↓", "navigate"),
1078
+ literalHint(theme, "space", "toggle"),
1079
+ commentHint,
1080
+ promptScrollHint,
881
1081
  keybindingHint(theme, this.keybindings, "tui.select.confirm", "submit"),
882
1082
  keybindingHint(theme, this.keybindings, "tui.select.cancel", "cancel"),
883
- ].filter((hint): hint is string => !!hint).join(" • ");
1083
+ ]
1084
+ .filter((hint): hint is string => !!hint)
1085
+ .join(" • ");
884
1086
  this.helpText.setText(theme.fg("dim", hints));
885
1087
  } else {
886
1088
  const alternateCancelKeys = this.keybindings.getKeys("tui.select.cancel").filter((key) => key !== "escape" && key !== "esc");
887
1089
  const hints = [
888
- literalHint(theme, "type", "filter"), commentHint, promptScrollHint,
1090
+ literalHint(theme, "type", "filter"),
1091
+ commentHint,
1092
+ promptScrollHint,
889
1093
  keybindingHint(theme, this.keybindings, "tui.editor.deleteCharBackward", "erase"),
890
1094
  literalHint(theme, "↑↓", "navigate"),
891
1095
  keybindingHint(theme, this.keybindings, "tui.select.confirm", "select"),
892
1096
  literalHint(theme, "esc", "clear/cancel"),
893
1097
  alternateCancelKeys.length > 0 ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel") : null,
894
- ].filter((hint): hint is string => !!hint).join(" • ");
1098
+ ]
1099
+ .filter((hint): hint is string => !!hint)
1100
+ .join(" • ");
895
1101
  this.helpText.setText(theme.fg("dim", hints));
896
1102
  }
897
1103
  }
898
1104
 
899
1105
  private ensureSingleSelectList(): WrappedSingleSelectList {
900
1106
  if (this.singleSelectList) return this.singleSelectList;
901
- const list = new WrappedSingleSelectList(this.options, this.allowFreeform, this.allowComment, this.theme, this.keybindings, this.shortcuts.commentToggle);
1107
+ const list = new WrappedSingleSelectList(
1108
+ this.options,
1109
+ this.allowFreeform,
1110
+ this.allowComment,
1111
+ this.theme,
1112
+ this.keybindings,
1113
+ this.shortcuts.commentToggle,
1114
+ );
902
1115
  list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
903
1116
  list.onCancel = () => this.onDone(null);
904
1117
  list.onEnterFreeform = () => this.showFreeformMode();
@@ -908,7 +1121,14 @@ class AskComponent extends Container {
908
1121
 
909
1122
  private ensureMultiSelectList(): MultiSelectList {
910
1123
  if (this.multiSelectList) return this.multiSelectList;
911
- const list = new MultiSelectList(this.options, this.allowFreeform, this.allowComment, this.theme, this.keybindings, this.shortcuts.commentToggle);
1124
+ const list = new MultiSelectList(
1125
+ this.options,
1126
+ this.allowFreeform,
1127
+ this.allowComment,
1128
+ this.theme,
1129
+ this.keybindings,
1130
+ this.shortcuts.commentToggle,
1131
+ );
912
1132
  list.onCancel = () => this.onDone(null);
913
1133
  list.onSubmit = (result) => this.handleSelectionSubmit(result, list.isCommentEnabled());
914
1134
  list.onEnterFreeform = () => this.showFreeformMode();
@@ -941,13 +1161,24 @@ class AskComponent extends Container {
941
1161
  }
942
1162
 
943
1163
  private handleSelectionSubmit(selections: string[], wantsComment: boolean): void {
944
- if (this.allowComment && wantsComment) { this.pendingSelections = selections; this.commentDraft = ""; this.showCommentMode(); return; }
1164
+ if (this.allowComment && wantsComment) {
1165
+ this.pendingSelections = selections;
1166
+ this.commentDraft = "";
1167
+ this.showCommentMode();
1168
+ return;
1169
+ }
945
1170
  this.onDone(createSelectionResponse(selections));
946
1171
  }
947
1172
 
948
1173
  private handleEditorSubmit(text: string): void {
949
- if (this.mode === "freeform") { this.onDone(createFreeformResponse(text)); return; }
950
- if (this.mode === "comment") { this.commentDraft = text; this.onDone(createSelectionResponse(this.pendingSelections, text)); }
1174
+ if (this.mode === "freeform") {
1175
+ this.onDone(createFreeformResponse(text));
1176
+ return;
1177
+ }
1178
+ if (this.mode === "comment") {
1179
+ this.commentDraft = text;
1180
+ this.onDone(createSelectionResponse(this.pendingSelections, text));
1181
+ }
951
1182
  }
952
1183
 
953
1184
  private showSelectMode(): void {
@@ -1011,26 +1242,58 @@ class AskComponent extends Container {
1011
1242
  if (this.mode !== "select") return false;
1012
1243
  const pageRows = Math.max(1, this.promptViewportRows - 1);
1013
1244
  const halfPageRows = Math.max(1, Math.floor(this.promptViewportRows / 2));
1014
- if (matchesKey(data, PROMPT_SCROLL_PAGE_UP_KEY)) { this.setPromptScrollOffset(this.promptScrollOffset - pageRows); return true; }
1015
- if (matchesKey(data, PROMPT_SCROLL_PAGE_DOWN_KEY)) { this.setPromptScrollOffset(this.promptScrollOffset + pageRows); return true; }
1016
- if (matchesKey(data, PROMPT_SCROLL_HOME_KEY)) { this.setPromptScrollOffset(0); return true; }
1017
- if (matchesKey(data, PROMPT_SCROLL_END_KEY)) { this.setPromptScrollOffset(this.promptMaxScrollOffset); return true; }
1018
- if (matchesKey(data, PROMPT_SCROLL_HALF_PAGE_UP_KEY)) { this.setPromptScrollOffset(this.promptScrollOffset - halfPageRows); return true; }
1019
- if (matchesKey(data, PROMPT_SCROLL_HALF_PAGE_DOWN_KEY)) { this.setPromptScrollOffset(this.promptScrollOffset + halfPageRows); return true; }
1245
+ if (matchesKey(data, PROMPT_SCROLL_PAGE_UP_KEY)) {
1246
+ this.setPromptScrollOffset(this.promptScrollOffset - pageRows);
1247
+ return true;
1248
+ }
1249
+ if (matchesKey(data, PROMPT_SCROLL_PAGE_DOWN_KEY)) {
1250
+ this.setPromptScrollOffset(this.promptScrollOffset + pageRows);
1251
+ return true;
1252
+ }
1253
+ if (matchesKey(data, PROMPT_SCROLL_HOME_KEY)) {
1254
+ this.setPromptScrollOffset(0);
1255
+ return true;
1256
+ }
1257
+ if (matchesKey(data, PROMPT_SCROLL_END_KEY)) {
1258
+ this.setPromptScrollOffset(this.promptMaxScrollOffset);
1259
+ return true;
1260
+ }
1261
+ if (matchesKey(data, PROMPT_SCROLL_HALF_PAGE_UP_KEY)) {
1262
+ this.setPromptScrollOffset(this.promptScrollOffset - halfPageRows);
1263
+ return true;
1264
+ }
1265
+ if (matchesKey(data, PROMPT_SCROLL_HALF_PAGE_DOWN_KEY)) {
1266
+ this.setPromptScrollOffset(this.promptScrollOffset + halfPageRows);
1267
+ return true;
1268
+ }
1020
1269
  return false;
1021
1270
  }
1022
1271
 
1023
1272
  handleInput(data: string): void {
1024
- if (this.handlePromptScrollInput(data)) { this.tui.requestRender(); return; }
1273
+ if (this.handlePromptScrollInput(data)) {
1274
+ this.tui.requestRender();
1275
+ return;
1276
+ }
1025
1277
  if (this.mode === "freeform" || this.mode === "comment") {
1026
1278
  // A freeform-only ask has no select mode to go back to -- escape cancels outright.
1027
- if (matchesKey(data, Key.escape)) { if (this.options.length > 0) this.showSelectMode(); else this.onDone(null); return; }
1028
- if (this.keybindings.matches(data, "tui.select.cancel")) { this.onDone(null); return; }
1279
+ if (matchesKey(data, Key.escape)) {
1280
+ if (this.options.length > 0) this.showSelectMode();
1281
+ else this.onDone(null);
1282
+ return;
1283
+ }
1284
+ if (this.keybindings.matches(data, "tui.select.cancel")) {
1285
+ this.onDone(null);
1286
+ return;
1287
+ }
1029
1288
  this.ensureEditor().handleInput(data);
1030
1289
  this.tui.requestRender();
1031
1290
  return;
1032
1291
  }
1033
- if (this.allowMultiple) { this.ensureMultiSelectList().handleInput?.(data); this.tui.requestRender(); return; }
1292
+ if (this.allowMultiple) {
1293
+ this.ensureMultiSelectList().handleInput?.(data);
1294
+ this.tui.requestRender();
1295
+ return;
1296
+ }
1034
1297
  this.ensureSingleSelectList().handleInput?.(data);
1035
1298
  this.tui.requestRender();
1036
1299
  }
@@ -1038,7 +1301,7 @@ class AskComponent extends Container {
1038
1301
 
1039
1302
  /** Plain dialog fallback (select/input) for a UI mode without setEditorComponent support. */
1040
1303
  async function askViaDialogs(
1041
- ui: { select: Function; input: Function },
1304
+ ui: ExtensionContext["ui"],
1042
1305
  question: string,
1043
1306
  context: string | undefined,
1044
1307
  options: AskOption[],
@@ -1056,12 +1319,18 @@ async function askViaDialogs(
1056
1319
  }
1057
1320
 
1058
1321
  if (allowMultiple) {
1059
- const rawSelections = (await ui.input(`${prompt}\n\nOptions (select one or more):\n${formatOptionsForMessage(options)}`, "Type your selection(s)...", dialogOpts)) as string | undefined;
1322
+ const rawSelections = (await ui.input(
1323
+ `${prompt}\n\nOptions (select one or more):\n${formatOptionsForMessage(options)}`,
1324
+ "Type your selection(s)...",
1325
+ dialogOpts,
1326
+ )) as string | undefined;
1060
1327
  if (isCancelledInput(rawSelections)) return null;
1061
1328
  const selections = parseDialogSelections(rawSelections);
1062
1329
  if (selections.length === 0) return null;
1063
1330
  if (!allowComment) return createSelectionResponse(selections);
1064
- const comment = (await ui.input(buildCommentPrompt(prompt, selections), "Optional comment (press Enter to skip)...", dialogOpts)) as string | undefined;
1331
+ const comment = (await ui.input(buildCommentPrompt(prompt, selections), "Optional comment (press Enter to skip)...", dialogOpts)) as
1332
+ | string
1333
+ | undefined;
1065
1334
  return createSelectionResponse(selections, comment);
1066
1335
  }
1067
1336
 
@@ -1076,7 +1345,9 @@ async function askViaDialogs(
1076
1345
  }
1077
1346
 
1078
1347
  if (!allowComment) return createSelectionResponse([selected]);
1079
- const comment = (await ui.input(buildCommentPrompt(prompt, [selected]), "Optional comment (press Enter to skip)...", dialogOpts)) as string | undefined;
1348
+ const comment = (await ui.input(buildCommentPrompt(prompt, [selected]), "Optional comment (press Enter to skip)...", dialogOpts)) as
1349
+ | string
1350
+ | undefined;
1080
1351
  return createSelectionResponse([selected], comment);
1081
1352
  }
1082
1353
 
@@ -1108,7 +1379,12 @@ let typingCourtesyQuietFloorMs = DISCUSS_TYPING_COURTESY_DEFAULT_QUIET_FLOOR_MS;
1108
1379
  let typingCourtesyDecayHorizonMs = DISCUSS_TYPING_COURTESY_DEFAULT_DECAY_HORIZON_MS;
1109
1380
 
1110
1381
  /** Test-only: the real decay curve runs over seconds, too slow to exercise at its real scale in a unit test. */
1111
- export function setTypingCourtesyTimingForTests(overrides?: { pollMs?: number; initialQuietMs?: number; floorMs?: number; decayHorizonMs?: number }): void {
1382
+ export function setTypingCourtesyTimingForTests(overrides?: {
1383
+ pollMs?: number;
1384
+ initialQuietMs?: number;
1385
+ floorMs?: number;
1386
+ decayHorizonMs?: number;
1387
+ }): void {
1112
1388
  typingCourtesyPollMs = overrides?.pollMs ?? DISCUSS_TYPING_COURTESY_DEFAULT_POLL_MS;
1113
1389
  typingCourtesyInitialQuietMs = overrides?.initialQuietMs ?? DISCUSS_TYPING_COURTESY_DEFAULT_INITIAL_QUIET_MS;
1114
1390
  typingCourtesyQuietFloorMs = overrides?.floorMs ?? DISCUSS_TYPING_COURTESY_DEFAULT_QUIET_FLOOR_MS;
@@ -1116,14 +1392,24 @@ export function setTypingCourtesyTimingForTests(overrides?: { pollMs?: number; i
1116
1392
  }
1117
1393
 
1118
1394
  function isTypingCourtesyEnabled(): boolean {
1119
- return parseBooleanPreference(process.env["PAPYRUS_DISCUSS_TYPING_COURTESY"]) ?? true;
1395
+ return parseBooleanPreference(process.env.PAPYRUS_DISCUSS_TYPING_COURTESY) ?? true;
1120
1396
  }
1121
1397
 
1122
1398
  function sleep(ms: number, signal?: AbortSignal): Promise<void> {
1123
1399
  return new Promise((resolve) => {
1124
- if (signal?.aborted) { resolve(); return; }
1400
+ if (signal?.aborted) {
1401
+ resolve();
1402
+ return;
1403
+ }
1125
1404
  const timer = setTimeout(resolve, ms);
1126
- signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
1405
+ signal?.addEventListener(
1406
+ "abort",
1407
+ () => {
1408
+ clearTimeout(timer);
1409
+ resolve();
1410
+ },
1411
+ { once: true },
1412
+ );
1127
1413
  });
1128
1414
  }
1129
1415
 
@@ -1153,7 +1439,10 @@ let trackedUi: ExtensionContext["ui"] | undefined;
1153
1439
  export function ensureTypingCourtesyTracking(ui: ExtensionContext["ui"]): void {
1154
1440
  if (typeof ui.onTerminalInput !== "function" || trackedUi === ui) return;
1155
1441
  trackedUi = ui;
1156
- ui.onTerminalInput(() => { lastKeystrokeAt = Date.now(); return undefined; });
1442
+ ui.onTerminalInput(() => {
1443
+ lastKeystrokeAt = Date.now();
1444
+ return undefined;
1445
+ });
1157
1446
  }
1158
1447
 
1159
1448
  /** Test-only: clears the ambient keystroke clock so one test's simulated typing can't bleed into another's. */
@@ -1185,7 +1474,10 @@ export async function waitForTypingCourtesy(params: Pick<AskQuestionParams, "onU
1185
1474
  while (lastKeystrokeAt > 0 && !params.signal?.aborted) {
1186
1475
  const elapsed = Date.now() - startedAt;
1187
1476
  if (Date.now() - lastKeystrokeAt >= requiredQuietMsAt(elapsed)) return;
1188
- if (!announced) { announced = true; params.onUpdate?.({ content: [{ type: "text", text: "Waiting for you to finish typing before asking..." }], details: undefined }); }
1477
+ if (!announced) {
1478
+ announced = true;
1479
+ params.onUpdate?.({ content: [{ type: "text", text: "Waiting for you to finish typing before asking..." }], details: undefined });
1480
+ }
1189
1481
  await sleep(typingCourtesyPollMs, params.signal);
1190
1482
  }
1191
1483
  }
@@ -1205,7 +1497,7 @@ async function askQuestionUnguarded(ctx: ExtensionContext, params: AskQuestionPa
1205
1497
  const options = params.options ?? [];
1206
1498
  const allowMultiple = params.allowMultiple ?? false;
1207
1499
  const allowFreeform = params.allowFreeform ?? true;
1208
- const allowComment = params.allowComment ?? parseBooleanPreference(process.env["PAPYRUS_DISCUSS_ALLOW_COMMENT"]) ?? false;
1500
+ const allowComment = params.allowComment ?? parseBooleanPreference(process.env.PAPYRUS_DISCUSS_ALLOW_COMMENT) ?? false;
1209
1501
  const normalizedContext = params.context?.trim() || undefined;
1210
1502
 
1211
1503
  if (isTypingCourtesyEnabled()) ensureTypingCourtesyTracking(ctx.ui);
@@ -1238,11 +1530,19 @@ class DiscussEditorHost implements EditorComponent {
1238
1530
  private readonly ask: AskComponent,
1239
1531
  private readonly preservedText: string,
1240
1532
  ) {}
1241
- getText(): string { return this.preservedText; }
1533
+ getText(): string {
1534
+ return this.preservedText;
1535
+ }
1242
1536
  setText(_text: string): void {}
1243
- render(width: number): string[] { return this.ask.render(width); }
1244
- handleInput(data: string): void { this.ask.handleInput(data); }
1245
- invalidate(): void { this.ask.invalidate(); }
1537
+ render(width: number): string[] {
1538
+ return this.ask.render(width);
1539
+ }
1540
+ handleInput(data: string): void {
1541
+ this.ask.handleInput(data);
1542
+ }
1543
+ invalidate(): void {
1544
+ this.ask.invalidate();
1545
+ }
1246
1546
  }
1247
1547
 
1248
1548
  async function askViaEditorSwap(
@@ -1272,7 +1572,20 @@ async function askViaEditorSwap(
1272
1572
  if (params.signal) params.signal.addEventListener("abort", () => finish(null), { once: true });
1273
1573
  if (params.timeout && params.timeout > 0) setTimeout(() => finish(null), params.timeout);
1274
1574
  ctx.ui.setEditorComponent((tui: TUI, _editorTheme: EditorTheme, keybindings: KeybindingsManager) => {
1275
- const ask = new AskComponent(params.question, normalizedContext, params.subtitle, options, allowMultiple, allowFreeform, allowComment, tui, theme, keybindings, shortcuts, finish);
1575
+ const ask = new AskComponent(
1576
+ params.question,
1577
+ normalizedContext,
1578
+ params.subtitle,
1579
+ options,
1580
+ allowMultiple,
1581
+ allowFreeform,
1582
+ allowComment,
1583
+ tui,
1584
+ theme,
1585
+ keybindings,
1586
+ shortcuts,
1587
+ finish,
1588
+ );
1276
1589
  return new DiscussEditorHost(ask, preservedText);
1277
1590
  });
1278
1591
  });
@@ -1288,14 +1601,27 @@ async function askQuestionBlocking(
1288
1601
  normalizedContext: string | undefined,
1289
1602
  ): Promise<AskAnswer | undefined> {
1290
1603
  const shortcuts: ResolvedAskShortcuts = {
1291
- commentToggle: resolveShortcut(undefined, process.env["PAPYRUS_DISCUSS_COMMENT_TOGGLE_KEY"], DEFAULT_COMMENT_TOGGLE_KEY),
1604
+ commentToggle: resolveShortcut(undefined, process.env.PAPYRUS_DISCUSS_COMMENT_TOGGLE_KEY, DEFAULT_COMMENT_TOGGLE_KEY),
1292
1605
  };
1293
1606
 
1294
1607
  // Falls to the plain dialog fallback if setEditorComponent isn't available in this UI mode.
1295
- if (typeof ctx.ui.setEditorComponent === "function" && typeof ctx.ui.getEditorComponent === "function" && typeof ctx.ui.getEditorText === "function") {
1608
+ if (
1609
+ typeof ctx.ui.setEditorComponent === "function" &&
1610
+ typeof ctx.ui.getEditorComponent === "function" &&
1611
+ typeof ctx.ui.getEditorText === "function"
1612
+ ) {
1296
1613
  const response = await askViaEditorSwap(ctx, params, options, allowMultiple, allowFreeform, allowComment, normalizedContext, shortcuts);
1297
1614
  return response ? toAskAnswer(response) : undefined;
1298
1615
  }
1299
- const response = await askViaDialogs(ctx.ui, params.question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, params.timeout);
1616
+ const response = await askViaDialogs(
1617
+ ctx.ui,
1618
+ params.question,
1619
+ normalizedContext,
1620
+ options,
1621
+ allowMultiple,
1622
+ allowFreeform,
1623
+ allowComment,
1624
+ params.timeout,
1625
+ );
1300
1626
  return response ? toAskAnswer(response) : undefined;
1301
1627
  }