@danypops/pi-papyrus 0.43.1 → 0.43.4
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/extension/src/artifact-browser.ts +54 -31
- package/extension/src/artifact-detail-view.ts +52 -42
- package/extension/src/artifact-format.ts +11 -5
- package/extension/src/artifact-relationship-lines.ts +4 -5
- package/extension/src/beautiful-mermaid-renderer.ts +3 -5
- package/extension/src/context-budget.ts +8 -5
- package/extension/src/context-hub-contribution.ts +6 -2
- package/extension/src/context-injection-telemetry.ts +2 -2
- package/extension/src/discuss-ask-layout.ts +3 -1
- package/extension/src/discuss-ask-view.ts +437 -111
- package/extension/src/discuss.ts +64 -15
- package/extension/src/discussion-detail-view.ts +44 -22
- package/extension/src/docs.ts +3 -2
- package/extension/src/domain-tools.ts +79 -34
- package/extension/src/index.ts +170 -66
- package/extension/src/markdown.ts +3 -7
- package/extension/src/note-widget.ts +1 -1
- package/extension/src/notes.ts +3 -8
- package/extension/src/playbook-bridge.ts +17 -6
- package/extension/src/playbooks.ts +17 -7
- package/extension/src/rules.ts +5 -5
- package/extension/src/service-client.ts +23 -8
- package/extension/src/skill-catalog-footprint.ts +1 -1
- package/extension/src/task-detail-format.ts +9 -9
- package/extension/src/task-detail-view.ts +36 -29
- package/extension/src/task-focus-events.ts +3 -2
- package/extension/src/task-graph.ts +16 -12
- package/extension/src/task-presentation.ts +2 -6
- package/extension/src/task-widget.ts +12 -8
- package/extension/src/tasks.ts +148 -57
- package/extension/src/tool-rendering/artifact-card.ts +16 -4
- package/extension/src/tool-rendering/artifact-list.ts +23 -24
- package/extension/src/tool-rendering/index.ts +2 -6
- package/extension/src/tool-rendering/render-model.ts +96 -56
- package/extension/src/vehicle-artifact-renderers.ts +102 -0
- package/extension/src/vehicle-notes-client.ts +35 -7
- 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 {
|
|
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":
|
|
103
|
-
case "
|
|
104
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 {
|
|
289
|
-
|
|
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 {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
private
|
|
295
|
-
|
|
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);
|
|
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")) {
|
|
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) {
|
|
312
|
-
|
|
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)) {
|
|
315
|
-
|
|
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) {
|
|
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)) {
|
|
326
|
-
|
|
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)) {
|
|
334
|
-
|
|
335
|
-
|
|
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);
|
|
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) {
|
|
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
|
|
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)))
|
|
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)
|
|
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 {
|
|
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) {
|
|
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 {
|
|
420
|
-
|
|
421
|
-
|
|
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 {
|
|
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)
|
|
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,
|
|
480
|
-
|
|
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)) {
|
|
525
|
-
|
|
526
|
-
|
|
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) {
|
|
532
|
-
|
|
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) {
|
|
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)) {
|
|
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)) {
|
|
544
|
-
|
|
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);
|
|
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)) {
|
|
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(
|
|
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 {
|
|
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(
|
|
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 {
|
|
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) {
|
|
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 [
|
|
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(
|
|
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) {
|
|
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(
|
|
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) {
|
|
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) {
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
]
|
|
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"),
|
|
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
|
-
]
|
|
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"),
|
|
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
|
-
]
|
|
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(
|
|
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(
|
|
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) {
|
|
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") {
|
|
950
|
-
|
|
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)) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (matchesKey(data,
|
|
1019
|
-
|
|
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)) {
|
|
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)) {
|
|
1028
|
-
|
|
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) {
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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?: {
|
|
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
|
|
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) {
|
|
1400
|
+
if (signal?.aborted) {
|
|
1401
|
+
resolve();
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1125
1404
|
const timer = setTimeout(resolve, ms);
|
|
1126
|
-
signal?.addEventListener(
|
|
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(() => {
|
|
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) {
|
|
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
|
|
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 {
|
|
1533
|
+
getText(): string {
|
|
1534
|
+
return this.preservedText;
|
|
1535
|
+
}
|
|
1242
1536
|
setText(_text: string): void {}
|
|
1243
|
-
render(width: number): string[] {
|
|
1244
|
-
|
|
1245
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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(
|
|
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
|
}
|