@pi-unipi/ask-user 2.1.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ask-ui.ts +45 -31
- package/launcher-ui.ts +26 -22
- package/package.json +2 -2
- package/settings-tui.ts +22 -9
package/ask-ui.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, type TUI, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { Theme, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { adaptiveInnerWidth, contentWidth, normalizeWidth, safeRepeat, shouldRenderBorder, WidthKeyedCache } from "@pi-unipi/core";
|
|
10
11
|
import type { NormalizedOption, AskUserResponse } from "./types.js";
|
|
11
12
|
|
|
12
13
|
/** Result returned by the ask UI */
|
|
@@ -60,7 +61,8 @@ export function renderAskUI(params: {
|
|
|
60
61
|
let optionIndex = 0;
|
|
61
62
|
let editMode = false;
|
|
62
63
|
let editTarget: "freeform" | number = "freeform"; // which option is being edited
|
|
63
|
-
|
|
64
|
+
// Width-keyed so a terminal resize can never serve stale, over-wide lines.
|
|
65
|
+
const lineCache = new WidthKeyedCache();
|
|
64
66
|
const selected = new Set<string>();
|
|
65
67
|
let customText: string | null = null; // Store custom text (global freeform)
|
|
66
68
|
const optionCustomTexts = new Map<string, string>(); // Per-option custom text for allowCustom
|
|
@@ -157,7 +159,7 @@ export function renderAskUI(params: {
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
function refresh() {
|
|
160
|
-
|
|
162
|
+
lineCache.clear();
|
|
161
163
|
tui.requestRender();
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -363,29 +365,39 @@ export function renderAskUI(params: {
|
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
|
|
366
|
-
function render(
|
|
367
|
-
|
|
368
|
+
function render(rawWidth: number): string[] {
|
|
369
|
+
const width = normalizeWidth(rawWidth);
|
|
370
|
+
const cached = lineCache.get(width);
|
|
371
|
+
if (cached) return cached;
|
|
368
372
|
|
|
369
373
|
const lines: string[] = [];
|
|
370
|
-
|
|
374
|
+
// Never exceed the terminal width — pi-tui throws on over-wide lines.
|
|
375
|
+
// On very narrow terminals the box border is dropped so the little
|
|
376
|
+
// width available all goes to content.
|
|
377
|
+
const innerWidth = adaptiveInnerWidth(width);
|
|
378
|
+
const bordered = shouldRenderBorder(width);
|
|
371
379
|
const border = (s: string) => theme.fg("accent", s);
|
|
372
380
|
|
|
373
381
|
function padVisible(content: string, targetWidth: number): string {
|
|
374
382
|
const vw = visibleWidth(content);
|
|
375
|
-
|
|
376
|
-
return content + " ".repeat(pad);
|
|
383
|
+
return content + safeRepeat(" ", targetWidth - vw);
|
|
377
384
|
}
|
|
378
385
|
|
|
379
|
-
const
|
|
386
|
+
const frame = (content: string) => {
|
|
387
|
+
const body = padVisible(truncateToWidth(content, innerWidth), innerWidth);
|
|
388
|
+
return bordered ? border("│") + body + border("│") : body;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const add = (s: string) => lines.push(frame(s));
|
|
380
392
|
const addWrapped = (s: string) => {
|
|
381
393
|
for (const line of wrapTextWithAnsi(s, innerWidth)) {
|
|
382
|
-
lines.push(
|
|
394
|
+
lines.push(frame(line));
|
|
383
395
|
}
|
|
384
396
|
};
|
|
385
|
-
const addEmpty = () => lines.push(
|
|
397
|
+
const addEmpty = () => lines.push(frame(""));
|
|
386
398
|
|
|
387
399
|
// Top border
|
|
388
|
-
lines.push(border(`╭${"─"
|
|
400
|
+
if (bordered) lines.push(border(`╭${safeRepeat("─", innerWidth)}╮`));
|
|
389
401
|
|
|
390
402
|
// Context
|
|
391
403
|
if (context) {
|
|
@@ -438,10 +450,9 @@ export function renderAskUI(params: {
|
|
|
438
450
|
}
|
|
439
451
|
|
|
440
452
|
// Bottom border
|
|
441
|
-
lines.push(border(`╰${"─"
|
|
453
|
+
if (bordered) lines.push(border(`╰${safeRepeat("─", innerWidth)}╯`));
|
|
442
454
|
|
|
443
|
-
|
|
444
|
-
return lines;
|
|
455
|
+
return lineCache.set(width, lines);
|
|
445
456
|
}
|
|
446
457
|
|
|
447
458
|
function renderOptions(
|
|
@@ -452,16 +463,28 @@ export function renderAskUI(params: {
|
|
|
452
463
|
) {
|
|
453
464
|
const addWrappedOptionLine = (prefix: string, content: string) => {
|
|
454
465
|
const prefixWidth = visibleWidth(prefix);
|
|
455
|
-
const
|
|
456
|
-
const continuationPrefix = " "
|
|
457
|
-
const wrapped = wrapTextWithAnsi(content,
|
|
466
|
+
const wrapWidth = contentWidth(width, prefixWidth);
|
|
467
|
+
const continuationPrefix = safeRepeat(" ", prefixWidth);
|
|
468
|
+
const wrapped = wrapTextWithAnsi(content, wrapWidth);
|
|
458
469
|
for (let lineIndex = 0; lineIndex < wrapped.length; lineIndex++) {
|
|
459
470
|
add((lineIndex === 0 ? prefix : continuationPrefix) + wrapped[lineIndex]);
|
|
460
471
|
}
|
|
461
472
|
};
|
|
462
473
|
|
|
474
|
+
// Indent descriptions under the option label, but give up the indent
|
|
475
|
+
// entirely when the terminal is too narrow to afford it.
|
|
476
|
+
const descriptionIndent = safeRepeat(" ", width > 10 ? 5 : 0);
|
|
463
477
|
const addWrappedDescription = (description: string) => {
|
|
464
|
-
addWrappedOptionLine(
|
|
478
|
+
addWrappedOptionLine(descriptionIndent, theme.fg("muted", description));
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Inline editor, inset by 3 columns where there is room for it.
|
|
482
|
+
const editorIndent = safeRepeat(" ", width > 8 ? 3 : 0);
|
|
483
|
+
const addInlineEditor = () => {
|
|
484
|
+
add(`${editorIndent}${theme.fg("muted", "Type your response:")}`);
|
|
485
|
+
for (const line of editor.render(contentWidth(width, visibleWidth(editorIndent) + 1))) {
|
|
486
|
+
add(`${editorIndent}${line}`);
|
|
487
|
+
}
|
|
465
488
|
};
|
|
466
489
|
|
|
467
490
|
for (let i = 0; i < displayOptions.length; i++) {
|
|
@@ -488,10 +511,7 @@ export function renderAskUI(params: {
|
|
|
488
511
|
|
|
489
512
|
// Show edit indicator if in edit mode for this option
|
|
490
513
|
if (editMode && editTarget === "freeform" && isSelected) {
|
|
491
|
-
|
|
492
|
-
for (const line of editor.render(width - 4)) {
|
|
493
|
-
add(` ${line}`);
|
|
494
|
-
}
|
|
514
|
+
addInlineEditor();
|
|
495
515
|
}
|
|
496
516
|
} else if (allowMultiple) {
|
|
497
517
|
// Multi-select: show checkbox
|
|
@@ -512,10 +532,7 @@ export function renderAskUI(params: {
|
|
|
512
532
|
|
|
513
533
|
// Show edit indicator if in edit mode for this option
|
|
514
534
|
if (editMode && editTarget === i && isSelected) {
|
|
515
|
-
|
|
516
|
-
for (const line of editor.render(width - 4)) {
|
|
517
|
-
add(` ${line}`);
|
|
518
|
-
}
|
|
535
|
+
addInlineEditor();
|
|
519
536
|
}
|
|
520
537
|
} else {
|
|
521
538
|
// Single-select: option
|
|
@@ -544,10 +561,7 @@ export function renderAskUI(params: {
|
|
|
544
561
|
|
|
545
562
|
// Show edit indicator if in edit mode for this option
|
|
546
563
|
if (editMode && editTarget === i && isSelected) {
|
|
547
|
-
|
|
548
|
-
for (const line of editor.render(width - 4)) {
|
|
549
|
-
add(` ${line}`);
|
|
550
|
-
}
|
|
564
|
+
addInlineEditor();
|
|
551
565
|
}
|
|
552
566
|
}
|
|
553
567
|
|
|
@@ -561,7 +575,7 @@ export function renderAskUI(params: {
|
|
|
561
575
|
return {
|
|
562
576
|
render,
|
|
563
577
|
invalidate: () => {
|
|
564
|
-
|
|
578
|
+
lineCache.clear();
|
|
565
579
|
},
|
|
566
580
|
handleInput,
|
|
567
581
|
};
|
package/launcher-ui.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { Key, matchesKey, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { Theme, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { adaptiveInnerWidth, contentWidth, normalizeWidth, safeRepeat, shouldRenderBorder, WidthKeyedCache } from "@pi-unipi/core";
|
|
10
11
|
import type { SessionLauncherResult } from "./types.js";
|
|
11
12
|
|
|
12
13
|
/** Launcher option definition */
|
|
@@ -45,10 +46,11 @@ export function renderLauncherUI(params: {
|
|
|
45
46
|
|
|
46
47
|
// State
|
|
47
48
|
let optionIndex = 0;
|
|
48
|
-
|
|
49
|
+
// Width-keyed so a terminal resize can never serve stale, over-wide lines.
|
|
50
|
+
const lineCache = new WidthKeyedCache();
|
|
49
51
|
|
|
50
52
|
function refresh() {
|
|
51
|
-
|
|
53
|
+
lineCache.clear();
|
|
52
54
|
_tui.requestRender();
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -79,34 +81,37 @@ export function renderLauncherUI(params: {
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
function render(
|
|
83
|
-
|
|
84
|
+
function render(rawWidth: number): string[] {
|
|
85
|
+
const width = normalizeWidth(rawWidth);
|
|
86
|
+
const cached = lineCache.get(width);
|
|
87
|
+
if (cached) return cached;
|
|
84
88
|
|
|
85
89
|
const lines: string[] = [];
|
|
86
|
-
|
|
90
|
+
// Never exceed the terminal width — pi-tui throws on over-wide lines.
|
|
91
|
+
const innerWidth = adaptiveInnerWidth(width);
|
|
92
|
+
const bordered = shouldRenderBorder(width);
|
|
87
93
|
const border = (s: string) => theme.fg("accent", s);
|
|
88
94
|
|
|
89
95
|
function padVisible(content: string, targetWidth: number): string {
|
|
90
96
|
const vw = visibleWidth(content);
|
|
91
|
-
|
|
92
|
-
return content + " ".repeat(pad);
|
|
97
|
+
return content + safeRepeat(" ", targetWidth - vw);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const addEmpty = () =>
|
|
102
|
-
lines.push(border("│") + " ".repeat(innerWidth) + border("│"));
|
|
100
|
+
const frame = (content: string) => {
|
|
101
|
+
const body = padVisible(truncateToWidth(content, innerWidth), innerWidth);
|
|
102
|
+
return bordered ? border("│") + body + border("│") : body;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const add = (s: string) => lines.push(frame(s));
|
|
106
|
+
const addEmpty = () => lines.push(frame(""));
|
|
103
107
|
|
|
104
108
|
// Top border
|
|
105
|
-
lines.push(border(`╭${"─"
|
|
109
|
+
if (bordered) lines.push(border(`╭${safeRepeat("─", innerWidth)}╮`));
|
|
106
110
|
|
|
107
|
-
// Header: show prefill command (truncated)
|
|
111
|
+
// Header: show prefill command (truncated).
|
|
112
|
+
// visibleWidth, not .length — the prefix holds an astral emoji.
|
|
108
113
|
const headerPrefix = " 🚀 ";
|
|
109
|
-
const maxPrefillWidth = innerWidth
|
|
114
|
+
const maxPrefillWidth = contentWidth(innerWidth, visibleWidth(headerPrefix) + 1);
|
|
110
115
|
const truncatedPrefill = truncateToWidth(prefill || "(no command)", maxPrefillWidth);
|
|
111
116
|
add(theme.fg("accent", headerPrefix) + theme.fg("text", truncatedPrefill));
|
|
112
117
|
addEmpty();
|
|
@@ -126,16 +131,15 @@ export function renderLauncherUI(params: {
|
|
|
126
131
|
add(theme.fg("dim", " ↑↓ navigate • Enter select • Esc cancel"));
|
|
127
132
|
|
|
128
133
|
// Bottom border
|
|
129
|
-
lines.push(border(`╰${"─"
|
|
134
|
+
if (bordered) lines.push(border(`╰${safeRepeat("─", innerWidth)}╯`));
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
return lines;
|
|
136
|
+
return lineCache.set(width, lines);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
return {
|
|
136
140
|
render,
|
|
137
141
|
invalidate: () => {
|
|
138
|
-
|
|
142
|
+
lineCache.clear();
|
|
139
143
|
},
|
|
140
144
|
handleInput,
|
|
141
145
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@pi-unipi/core": "2.
|
|
43
|
+
"@pi-unipi/core": "2.3.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
package/settings-tui.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Component } from "@earendil-works/pi-tui";
|
|
9
9
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
import { adaptiveInnerWidth, normalizeWidth, safeRepeat, shouldRenderBorder } from "@pi-unipi/core";
|
|
10
11
|
import { getAskUserSettings, saveAskUserSettings, type AskUserSettings } from "./config.js";
|
|
11
12
|
|
|
12
13
|
/** ANSI escape codes */
|
|
@@ -140,27 +141,39 @@ export class AskUserSettingsOverlay implements Component {
|
|
|
140
141
|
/**
|
|
141
142
|
* Render the overlay.
|
|
142
143
|
*/
|
|
143
|
-
render(
|
|
144
|
+
render(rawWidth: number): string[] {
|
|
145
|
+
const width = normalizeWidth(rawWidth);
|
|
144
146
|
const lines: string[] = [];
|
|
145
|
-
|
|
147
|
+
// Never exceed the given width — pi-tui throws on over-wide lines.
|
|
148
|
+
const innerWidth = adaptiveInnerWidth(width);
|
|
149
|
+
const bordered = shouldRenderBorder(width);
|
|
146
150
|
|
|
147
151
|
function padVisible(content: string, targetWidth: number): string {
|
|
148
152
|
const vw = visibleWidth(content);
|
|
149
|
-
|
|
150
|
-
return content + " ".repeat(pad);
|
|
153
|
+
return content + safeRepeat(" ", targetWidth - vw);
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
const
|
|
154
|
-
|
|
156
|
+
const frame = (content: string) => {
|
|
157
|
+
const body = padVisible(truncateToWidth(content, innerWidth), innerWidth);
|
|
158
|
+
return bordered
|
|
159
|
+
? `${ansi.cyan}│${ansi.reset}` + body + `${ansi.cyan}│${ansi.reset}`
|
|
160
|
+
: body;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const add = (s: string) => lines.push(frame(s));
|
|
164
|
+
const addEmpty = () => lines.push(frame(""));
|
|
155
165
|
|
|
156
166
|
// Top border
|
|
157
|
-
lines.push(`${ansi.cyan}╭${"─"
|
|
167
|
+
if (bordered) lines.push(`${ansi.cyan}╭${safeRepeat("─", innerWidth)}╮${ansi.reset}`);
|
|
158
168
|
|
|
159
169
|
// Header
|
|
160
170
|
add(`${ansi.bold}${ansi.cyan}Ask User Settings${ansi.reset}`);
|
|
161
171
|
add(`${ansi.dim}Configure how the agent can ask you questions${ansi.reset}`);
|
|
162
172
|
addEmpty();
|
|
163
173
|
|
|
174
|
+
// Indent descriptions, unless the terminal is too narrow to afford it.
|
|
175
|
+
const descIndent = safeRepeat(" ", width > 10 ? 3 : 0);
|
|
176
|
+
|
|
164
177
|
// Settings list
|
|
165
178
|
for (let i = 0; i < SETTINGS.length; i++) {
|
|
166
179
|
const item = SETTINGS[i];
|
|
@@ -171,7 +184,7 @@ export class AskUserSettingsOverlay implements Component {
|
|
|
171
184
|
const descColor = ansi.gray;
|
|
172
185
|
|
|
173
186
|
add(`${isSelected ? ansi.cyan + "▸" + ansi.reset : " "} ${toggle} ${labelColor}${item.label}${ansi.reset}`);
|
|
174
|
-
add(
|
|
187
|
+
add(`${descIndent}${descColor}${item.description}${ansi.reset}`);
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
// Footer
|
|
@@ -179,7 +192,7 @@ export class AskUserSettingsOverlay implements Component {
|
|
|
179
192
|
add(`${ansi.dim}↑↓ navigate • Space toggle • Enter save • Esc cancel${ansi.reset}`);
|
|
180
193
|
|
|
181
194
|
// Bottom border
|
|
182
|
-
lines.push(`${ansi.cyan}╰${"─"
|
|
195
|
+
if (bordered) lines.push(`${ansi.cyan}╰${safeRepeat("─", innerWidth)}╯${ansi.reset}`);
|
|
183
196
|
|
|
184
197
|
return lines;
|
|
185
198
|
}
|