pi-ask-user 0.7.0 → 0.9.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/README.md +35 -4
- package/index.ts +156 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ High-quality video: [ask-user-demo.mp4](./media/ask-user-demo.mp4)
|
|
|
17
17
|
- User-toggleable extra context on structured selections
|
|
18
18
|
- Context display support
|
|
19
19
|
- Configurable display mode: `overlay` (modal, default) or `inline` (rendered directly in the flow)
|
|
20
|
+
- Runtime overlay toggle: press the configured overlay-toggle key (`alt+o` by default, configurable per call or via env var) while the prompt is open to temporarily hide/show the popup so you can read prior agent output, then press it again to bring it back
|
|
20
21
|
- Pi-TUI-aligned keybinding and editor behavior
|
|
21
22
|
- Custom TUI rendering for tool calls and results
|
|
22
23
|
- System prompt integration via `promptSnippet` and `promptGuidelines`
|
|
@@ -65,6 +66,8 @@ The registered tool name is:
|
|
|
65
66
|
| `allowFreeform` | `boolean?` | `true` | Add a "Type something" freeform option |
|
|
66
67
|
| `allowComment` | `boolean?` | `false` | Expose a user-toggleable extra-context option in the custom UI (`ctrl+g` or the toggle row) and collect an optional comment in fallback dialogs |
|
|
67
68
|
| `displayMode` | `"overlay" \| "inline"?` | env var or `"overlay"` | Controls custom UI rendering: `overlay` shows the centered modal (current behavior), `inline` renders without overlay framing |
|
|
69
|
+
| `overlayToggleKey` | `string?` | env var or `"alt+o"` | Shortcut for hiding/showing the overlay popup (overlay mode only). Pi-TUI key spec, e.g. `"alt+o"`, `"ctrl+shift+h"`. Pass `"off"` to disable. |
|
|
70
|
+
| `commentToggleKey` | `string?` | env var or `"ctrl+g"` | Shortcut for toggling the optional comment/extra-context row when `allowComment: true`. Pass `"off"` to disable. |
|
|
68
71
|
| `timeout` | `number?` | — | Auto-dismiss after N ms and return `null` if the prompt times out |
|
|
69
72
|
|
|
70
73
|
## Example usage shape
|
|
@@ -86,22 +89,50 @@ The registered tool name is:
|
|
|
86
89
|
|
|
87
90
|
`displayMode: "inline"` uses the same interaction logic but skips overlay mode when calling `ctx.ui.custom(...)`. RPC/headless fallback behavior is unchanged.
|
|
88
91
|
|
|
89
|
-
## Personal
|
|
92
|
+
## Personal preferences via environment variables
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
Configure your defaults globally by setting these in your shell profile (`~/.zshrc`, `~/.bash_profile`, etc.):
|
|
92
95
|
|
|
93
96
|
```bash
|
|
94
97
|
export PI_ASK_USER_DISPLAY_MODE=inline
|
|
98
|
+
export PI_ASK_USER_OVERLAY_TOGGLE_KEY=alt+h
|
|
99
|
+
export PI_ASK_USER_COMMENT_TOGGLE_KEY=alt+c
|
|
95
100
|
```
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
### Display mode
|
|
103
|
+
|
|
104
|
+
Effective order:
|
|
98
105
|
|
|
99
106
|
1. Per-call `displayMode` parameter (if provided)
|
|
100
|
-
2. `PI_ASK_USER_DISPLAY_MODE`
|
|
107
|
+
2. `PI_ASK_USER_DISPLAY_MODE` (if set to `"overlay"` or `"inline"`)
|
|
101
108
|
3. Fallback default: `"overlay"`
|
|
102
109
|
|
|
103
110
|
Unrecognised values are silently ignored and fall back to `"overlay"`.
|
|
104
111
|
|
|
112
|
+
### Shortcuts
|
|
113
|
+
|
|
114
|
+
Effective order for both `overlayToggleKey` and `commentToggleKey`:
|
|
115
|
+
|
|
116
|
+
1. Per-call parameter (if provided)
|
|
117
|
+
2. Matching env var (`PI_ASK_USER_OVERLAY_TOGGLE_KEY` / `PI_ASK_USER_COMMENT_TOGGLE_KEY`)
|
|
118
|
+
3. Built-in defaults: `alt+o` and `ctrl+g`
|
|
119
|
+
|
|
120
|
+
Pass `"off"`, `"none"`, or `"disabled"` (at any level) to disable the shortcut entirely. Invalid specs are silently dropped and the next source is used. Specs follow the Pi-TUI [`KeyId`](https://github.com/badlogic/pi-mono/blob/main/packages/tui/src/keys.ts) format: `[mod+]...key` where modifiers are `ctrl`, `shift`, `alt`, `super`, in any order, joined by `+` (e.g. `ctrl+g`, `alt+shift+x`, `escape`, `tab`).
|
|
121
|
+
|
|
122
|
+
## Controls
|
|
123
|
+
|
|
124
|
+
While an `ask_user` prompt is open:
|
|
125
|
+
|
|
126
|
+
| Key | Action |
|
|
127
|
+
|-----|--------|
|
|
128
|
+
| `alt+o` (configurable via `overlayToggleKey`) | Hide/show the overlay popup so you can read the agent's prior output. Available in `overlay` mode only. The first time you hide it, a notification reminds you which key brings it back. |
|
|
129
|
+
| `ctrl+g` (configurable via `commentToggleKey`) | Toggle the optional comment/extra-context row (when `allowComment: true`). |
|
|
130
|
+
| `enter` | Confirm the focused option, submit a freeform response, or submit/skip an optional comment. |
|
|
131
|
+
| `esc` | Clear the search filter, exit freeform/comment mode, or cancel the prompt. |
|
|
132
|
+
| `↑` / `↓` | Navigate options. |
|
|
133
|
+
|
|
134
|
+
If you prefer never to see the overlay, set `displayMode: "inline"` per call or `PI_ASK_USER_DISPLAY_MODE=inline` globally.
|
|
135
|
+
|
|
105
136
|
## Result details
|
|
106
137
|
|
|
107
138
|
All tool results include a structured `details` object for rendering and session state reconstruction:
|
package/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
Markdown,
|
|
22
22
|
type MarkdownTheme,
|
|
23
23
|
matchesKey,
|
|
24
|
+
type OverlayHandle,
|
|
24
25
|
Spacer,
|
|
25
26
|
Text,
|
|
26
27
|
type TUI,
|
|
@@ -63,6 +64,8 @@ interface AskParams {
|
|
|
63
64
|
allowFreeform?: boolean;
|
|
64
65
|
allowComment?: boolean;
|
|
65
66
|
displayMode?: AskDisplayMode;
|
|
67
|
+
overlayToggleKey?: string | null;
|
|
68
|
+
commentToggleKey?: string | null;
|
|
66
69
|
timeout?: number;
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -244,8 +247,63 @@ function literalHint(theme: Theme, key: string, description: string): string {
|
|
|
244
247
|
return `${theme.fg("dim", key)}${theme.fg("muted", ` ${description}`)}`;
|
|
245
248
|
}
|
|
246
249
|
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
type ResolvedShortcut =
|
|
251
|
+
| { disabled: false; spec: string; matches: (data: string) => boolean }
|
|
252
|
+
| { disabled: true; spec: null; matches: (data: string) => false };
|
|
253
|
+
|
|
254
|
+
interface ResolvedAskShortcuts {
|
|
255
|
+
overlayToggle: ResolvedShortcut;
|
|
256
|
+
commentToggle: ResolvedShortcut;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const DISABLED_SHORTCUT: ResolvedShortcut = {
|
|
260
|
+
disabled: true,
|
|
261
|
+
spec: null,
|
|
262
|
+
matches: ((_data: string) => false) as (data: string) => false,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const SHORTCUT_DISABLE_VALUES = new Set(["off", "none", "disabled", ""]);
|
|
266
|
+
|
|
267
|
+
function normalizeShortcutSpec(value: string | null | undefined): string | null | undefined {
|
|
268
|
+
if (value === undefined) return undefined;
|
|
269
|
+
if (value === null) return null;
|
|
270
|
+
const trimmed = value.trim().toLowerCase();
|
|
271
|
+
if (SHORTCUT_DISABLE_VALUES.has(trimmed)) return null;
|
|
272
|
+
return trimmed;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isValidShortcutSpec(spec: string): boolean {
|
|
276
|
+
// KeyId is canonical lowercase: modifiers (`ctrl|shift|alt|super`) joined by `+`,
|
|
277
|
+
// plus a base key. We do a light syntactic sanity check; matchesKey() does the rest.
|
|
278
|
+
if (!spec) return false;
|
|
279
|
+
if (!/^[a-z0-9+_\-!@#$%^&*()|~`'":;,./<>?[\]{}=\\]+$/i.test(spec)) return false;
|
|
280
|
+
if (spec.startsWith("+") || spec.endsWith("+")) return false;
|
|
281
|
+
if (spec.includes("++")) return false;
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildShortcut(spec: string): ResolvedShortcut {
|
|
286
|
+
return {
|
|
287
|
+
disabled: false,
|
|
288
|
+
spec,
|
|
289
|
+
matches: (data: string) => matchesKey(data, spec as any),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveShortcut(
|
|
294
|
+
paramValue: string | null | undefined,
|
|
295
|
+
envValue: string | undefined,
|
|
296
|
+
defaultSpec: string,
|
|
297
|
+
): ResolvedShortcut {
|
|
298
|
+
const candidates: Array<string | null | undefined> = [paramValue, envValue, defaultSpec];
|
|
299
|
+
for (const raw of candidates) {
|
|
300
|
+
const normalized = normalizeShortcutSpec(raw);
|
|
301
|
+
if (normalized === undefined) continue; // not provided, fall through
|
|
302
|
+
if (normalized === null) return DISABLED_SHORTCUT; // explicit disable
|
|
303
|
+
if (isValidShortcutSpec(normalized)) return buildShortcut(normalized);
|
|
304
|
+
// Invalid spec: silently fall through to next candidate.
|
|
305
|
+
}
|
|
306
|
+
return DISABLED_SHORTCUT;
|
|
249
307
|
}
|
|
250
308
|
|
|
251
309
|
type AskMode = "select" | "freeform" | "comment";
|
|
@@ -259,8 +317,13 @@ const SINGLE_SELECT_SPLIT_PANE_RIGHT_MIN_WIDTH = 28;
|
|
|
259
317
|
const SINGLE_SELECT_SPLIT_PANE_SEPARATOR = " │ ";
|
|
260
318
|
const FREEFORM_SENTINEL = "\u270f\ufe0f Type custom response...";
|
|
261
319
|
const COMMENT_TOGGLE_LABEL = "Add extra context after selection";
|
|
320
|
+
const DEFAULT_OVERLAY_TOGGLE_KEY = "alt+o";
|
|
321
|
+
const DEFAULT_COMMENT_TOGGLE_KEY = "ctrl+g";
|
|
262
322
|
|
|
263
|
-
function buildCustomUIOptions(
|
|
323
|
+
function buildCustomUIOptions(
|
|
324
|
+
displayMode: AskDisplayMode,
|
|
325
|
+
onHandle?: (handle: OverlayHandle) => void,
|
|
326
|
+
) {
|
|
264
327
|
switch (displayMode) {
|
|
265
328
|
case "inline":
|
|
266
329
|
return undefined;
|
|
@@ -274,6 +337,7 @@ function buildCustomUIOptions(displayMode: AskDisplayMode) {
|
|
|
274
337
|
maxHeight: "85%",
|
|
275
338
|
margin: 1,
|
|
276
339
|
},
|
|
340
|
+
...(onHandle ? { onHandle } : {}),
|
|
277
341
|
};
|
|
278
342
|
default: {
|
|
279
343
|
const _exhaustive: never = displayMode;
|
|
@@ -287,6 +351,7 @@ function buildCustomUIOptions(displayMode: AskDisplayMode) {
|
|
|
287
351
|
maxHeight: "85%",
|
|
288
352
|
margin: 1,
|
|
289
353
|
},
|
|
354
|
+
...(onHandle ? { onHandle } : {}),
|
|
290
355
|
};
|
|
291
356
|
}
|
|
292
357
|
}
|
|
@@ -298,6 +363,7 @@ class MultiSelectList implements Component {
|
|
|
298
363
|
private allowComment: boolean;
|
|
299
364
|
private theme: Theme;
|
|
300
365
|
private keybindings: KeybindingsManager;
|
|
366
|
+
private commentToggle: ResolvedShortcut;
|
|
301
367
|
private selectedIndex = 0;
|
|
302
368
|
private checked = new Set<number>();
|
|
303
369
|
private commentEnabled = false;
|
|
@@ -314,12 +380,14 @@ class MultiSelectList implements Component {
|
|
|
314
380
|
allowComment: boolean,
|
|
315
381
|
theme: Theme,
|
|
316
382
|
keybindings: KeybindingsManager,
|
|
383
|
+
commentToggle: ResolvedShortcut,
|
|
317
384
|
) {
|
|
318
385
|
this.options = options;
|
|
319
386
|
this.allowFreeform = allowFreeform;
|
|
320
387
|
this.allowComment = allowComment;
|
|
321
388
|
this.theme = theme;
|
|
322
389
|
this.keybindings = keybindings;
|
|
390
|
+
this.commentToggle = commentToggle;
|
|
323
391
|
}
|
|
324
392
|
|
|
325
393
|
public isCommentEnabled(): boolean {
|
|
@@ -376,7 +444,7 @@ class MultiSelectList implements Component {
|
|
|
376
444
|
return;
|
|
377
445
|
}
|
|
378
446
|
|
|
379
|
-
if (this.allowComment &&
|
|
447
|
+
if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
|
|
380
448
|
this.toggleComment();
|
|
381
449
|
return;
|
|
382
450
|
}
|
|
@@ -520,6 +588,7 @@ class WrappedSingleSelectList implements Component {
|
|
|
520
588
|
private allowComment: boolean;
|
|
521
589
|
private theme: Theme;
|
|
522
590
|
private keybindings: KeybindingsManager;
|
|
591
|
+
private commentToggle: ResolvedShortcut;
|
|
523
592
|
private selectedIndex = 0;
|
|
524
593
|
private searchQuery = "";
|
|
525
594
|
private commentEnabled = false;
|
|
@@ -537,12 +606,14 @@ class WrappedSingleSelectList implements Component {
|
|
|
537
606
|
allowComment: boolean,
|
|
538
607
|
theme: Theme,
|
|
539
608
|
keybindings: KeybindingsManager,
|
|
609
|
+
commentToggle: ResolvedShortcut,
|
|
540
610
|
) {
|
|
541
611
|
this.options = options;
|
|
542
612
|
this.allowFreeform = allowFreeform;
|
|
543
613
|
this.allowComment = allowComment;
|
|
544
614
|
this.theme = theme;
|
|
545
615
|
this.keybindings = keybindings;
|
|
616
|
+
this.commentToggle = commentToggle;
|
|
546
617
|
}
|
|
547
618
|
|
|
548
619
|
public isCommentEnabled(): boolean {
|
|
@@ -763,7 +834,7 @@ class WrappedSingleSelectList implements Component {
|
|
|
763
834
|
return;
|
|
764
835
|
}
|
|
765
836
|
|
|
766
|
-
if (this.allowComment &&
|
|
837
|
+
if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
|
|
767
838
|
this.toggleComment();
|
|
768
839
|
return;
|
|
769
840
|
}
|
|
@@ -868,9 +939,11 @@ class AskComponent extends Container {
|
|
|
868
939
|
private allowMultiple: boolean;
|
|
869
940
|
private allowFreeform: boolean;
|
|
870
941
|
private allowComment: boolean;
|
|
942
|
+
private displayMode: AskDisplayMode;
|
|
871
943
|
private tui: TUI;
|
|
872
944
|
private theme: Theme;
|
|
873
945
|
private keybindings: KeybindingsManager;
|
|
946
|
+
private shortcuts: ResolvedAskShortcuts;
|
|
874
947
|
private onDone: (result: AskUIResult | null) => void;
|
|
875
948
|
|
|
876
949
|
private mode: AskMode = "select";
|
|
@@ -909,9 +982,11 @@ class AskComponent extends Container {
|
|
|
909
982
|
allowMultiple: boolean,
|
|
910
983
|
allowFreeform: boolean,
|
|
911
984
|
allowComment: boolean,
|
|
985
|
+
displayMode: AskDisplayMode,
|
|
912
986
|
tui: TUI,
|
|
913
987
|
theme: Theme,
|
|
914
988
|
keybindings: KeybindingsManager,
|
|
989
|
+
shortcuts: ResolvedAskShortcuts,
|
|
915
990
|
onDone: (result: AskUIResult | null) => void,
|
|
916
991
|
) {
|
|
917
992
|
super();
|
|
@@ -922,9 +997,11 @@ class AskComponent extends Container {
|
|
|
922
997
|
this.allowMultiple = allowMultiple;
|
|
923
998
|
this.allowFreeform = allowFreeform;
|
|
924
999
|
this.allowComment = allowComment;
|
|
1000
|
+
this.displayMode = displayMode;
|
|
925
1001
|
this.tui = tui;
|
|
926
1002
|
this.theme = theme;
|
|
927
1003
|
this.keybindings = keybindings;
|
|
1004
|
+
this.shortcuts = shortcuts;
|
|
928
1005
|
this.onDone = onDone;
|
|
929
1006
|
|
|
930
1007
|
// Layout skeleton
|
|
@@ -1044,6 +1121,12 @@ class AskComponent extends Container {
|
|
|
1044
1121
|
|
|
1045
1122
|
private updateHelpText(): void {
|
|
1046
1123
|
const theme = this.theme;
|
|
1124
|
+
const overlayHint = this.displayMode === "overlay" && !this.shortcuts.overlayToggle.disabled
|
|
1125
|
+
? literalHint(theme, this.shortcuts.overlayToggle.spec, "hide")
|
|
1126
|
+
: null;
|
|
1127
|
+
const commentHint = this.allowComment && !this.shortcuts.commentToggle.disabled
|
|
1128
|
+
? literalHint(theme, this.shortcuts.commentToggle.spec, "toggle context")
|
|
1129
|
+
: null;
|
|
1047
1130
|
if (this.mode === "freeform" || this.mode === "comment") {
|
|
1048
1131
|
const alternateCancelKeys = this.keybindings
|
|
1049
1132
|
.getKeys("tui.select.cancel")
|
|
@@ -1052,6 +1135,7 @@ class AskComponent extends Container {
|
|
|
1052
1135
|
keybindingHint(theme, this.keybindings, "tui.input.submit", this.mode === "comment" ? "submit/skip" : "submit"),
|
|
1053
1136
|
keybindingHint(theme, this.keybindings, "tui.input.newLine", "newline"),
|
|
1054
1137
|
literalHint(theme, "esc", "back"),
|
|
1138
|
+
overlayHint,
|
|
1055
1139
|
alternateCancelKeys.length > 0 ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel") : null,
|
|
1056
1140
|
]
|
|
1057
1141
|
.filter((hint): hint is string => !!hint)
|
|
@@ -1064,7 +1148,8 @@ class AskComponent extends Container {
|
|
|
1064
1148
|
const hints = [
|
|
1065
1149
|
literalHint(theme, "↑↓", "navigate"),
|
|
1066
1150
|
literalHint(theme, "space", "toggle"),
|
|
1067
|
-
|
|
1151
|
+
commentHint,
|
|
1152
|
+
overlayHint,
|
|
1068
1153
|
keybindingHint(theme, this.keybindings, "tui.select.confirm", "submit"),
|
|
1069
1154
|
keybindingHint(theme, this.keybindings, "tui.select.cancel", "cancel"),
|
|
1070
1155
|
]
|
|
@@ -1079,7 +1164,8 @@ class AskComponent extends Container {
|
|
|
1079
1164
|
literalHint(theme, "type", "filter"),
|
|
1080
1165
|
keybindingHint(theme, this.keybindings, "tui.editor.deleteCharBackward", "erase"),
|
|
1081
1166
|
literalHint(theme, "↑↓", "navigate"),
|
|
1082
|
-
|
|
1167
|
+
commentHint,
|
|
1168
|
+
overlayHint,
|
|
1083
1169
|
keybindingHint(theme, this.keybindings, "tui.select.confirm", "select"),
|
|
1084
1170
|
literalHint(theme, "esc", "clear/cancel"),
|
|
1085
1171
|
alternateCancelKeys.length > 0
|
|
@@ -1101,6 +1187,7 @@ class AskComponent extends Container {
|
|
|
1101
1187
|
this.allowComment,
|
|
1102
1188
|
this.theme,
|
|
1103
1189
|
this.keybindings,
|
|
1190
|
+
this.shortcuts.commentToggle,
|
|
1104
1191
|
);
|
|
1105
1192
|
list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
|
|
1106
1193
|
list.onCancel = () => this.onDone(null);
|
|
@@ -1119,6 +1206,7 @@ class AskComponent extends Container {
|
|
|
1119
1206
|
this.allowComment,
|
|
1120
1207
|
this.theme,
|
|
1121
1208
|
this.keybindings,
|
|
1209
|
+
this.shortcuts.commentToggle,
|
|
1122
1210
|
);
|
|
1123
1211
|
list.onCancel = () => this.onDone(null);
|
|
1124
1212
|
list.onSubmit = (result) => this.handleSelectionSubmit(result, list.isCommentEnabled());
|
|
@@ -1389,6 +1477,18 @@ export default function(pi: ExtensionAPI) {
|
|
|
1389
1477
|
description: "UI rendering mode. 'overlay' shows a centered modal, 'inline' renders in-place. Default: PI_ASK_USER_DISPLAY_MODE env var if set, otherwise 'overlay'. Omit to respect the user's configured preference.",
|
|
1390
1478
|
}),
|
|
1391
1479
|
),
|
|
1480
|
+
overlayToggleKey: Type.Optional(
|
|
1481
|
+
Type.String({
|
|
1482
|
+
description:
|
|
1483
|
+
"Shortcut for hiding/showing the overlay popup (overlay mode only), e.g. 'alt+o' or 'ctrl+shift+h'. Pass 'off' to disable. Default: PI_ASK_USER_OVERLAY_TOGGLE_KEY env var if set, otherwise 'alt+o'.",
|
|
1484
|
+
}),
|
|
1485
|
+
),
|
|
1486
|
+
commentToggleKey: Type.Optional(
|
|
1487
|
+
Type.String({
|
|
1488
|
+
description:
|
|
1489
|
+
"Shortcut for toggling the optional comment/extra-context row when allowComment is true, e.g. 'ctrl+g'. Pass 'off' to disable. Default: PI_ASK_USER_COMMENT_TOGGLE_KEY env var if set, otherwise 'ctrl+g'.",
|
|
1490
|
+
}),
|
|
1491
|
+
),
|
|
1392
1492
|
timeout: Type.Optional(
|
|
1393
1493
|
Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." }),
|
|
1394
1494
|
),
|
|
@@ -1410,12 +1510,26 @@ export default function(pi: ExtensionAPI) {
|
|
|
1410
1510
|
allowFreeform = true,
|
|
1411
1511
|
allowComment = false,
|
|
1412
1512
|
displayMode,
|
|
1513
|
+
overlayToggleKey,
|
|
1514
|
+
commentToggleKey,
|
|
1413
1515
|
timeout,
|
|
1414
1516
|
} = params as AskParams;
|
|
1415
1517
|
const envMode = process.env.PI_ASK_USER_DISPLAY_MODE;
|
|
1416
1518
|
const envDisplayMode: AskDisplayMode | undefined =
|
|
1417
1519
|
envMode === "overlay" || envMode === "inline" ? envMode : undefined;
|
|
1418
1520
|
const effectiveDisplayMode: AskDisplayMode = displayMode ?? envDisplayMode ?? "overlay";
|
|
1521
|
+
const shortcuts: ResolvedAskShortcuts = {
|
|
1522
|
+
overlayToggle: resolveShortcut(
|
|
1523
|
+
overlayToggleKey,
|
|
1524
|
+
process.env.PI_ASK_USER_OVERLAY_TOGGLE_KEY,
|
|
1525
|
+
DEFAULT_OVERLAY_TOGGLE_KEY,
|
|
1526
|
+
),
|
|
1527
|
+
commentToggle: resolveShortcut(
|
|
1528
|
+
commentToggleKey,
|
|
1529
|
+
process.env.PI_ASK_USER_COMMENT_TOGGLE_KEY,
|
|
1530
|
+
DEFAULT_COMMENT_TOGGLE_KEY,
|
|
1531
|
+
),
|
|
1532
|
+
};
|
|
1419
1533
|
const options = normalizeOptions(rawOptions);
|
|
1420
1534
|
const normalizedContext = context?.trim() || undefined;
|
|
1421
1535
|
|
|
@@ -1461,6 +1575,9 @@ export default function(pi: ExtensionAPI) {
|
|
|
1461
1575
|
});
|
|
1462
1576
|
|
|
1463
1577
|
let result: AskUIResult | null;
|
|
1578
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
1579
|
+
let removeOverlayInputListener: (() => void) | undefined;
|
|
1580
|
+
let hasAnnouncedHide = false;
|
|
1464
1581
|
try {
|
|
1465
1582
|
const customFactory = (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: AskUIResult | null) => void) => {
|
|
1466
1583
|
if (signal) {
|
|
@@ -1479,14 +1596,43 @@ export default function(pi: ExtensionAPI) {
|
|
|
1479
1596
|
allowMultiple,
|
|
1480
1597
|
allowFreeform,
|
|
1481
1598
|
allowComment,
|
|
1599
|
+
effectiveDisplayMode,
|
|
1482
1600
|
tui,
|
|
1483
1601
|
theme,
|
|
1484
1602
|
keybindings,
|
|
1603
|
+
shortcuts,
|
|
1485
1604
|
done,
|
|
1486
1605
|
);
|
|
1487
1606
|
};
|
|
1488
1607
|
|
|
1489
|
-
|
|
1608
|
+
// Register a raw terminal input listener for the overlay-toggle key so the
|
|
1609
|
+
// overlay can be toggled even while it is hidden (hidden overlays do not
|
|
1610
|
+
// receive input). Inline mode does not need this because the prompt is
|
|
1611
|
+
// already non-modal. Skipped entirely if the user disabled the shortcut.
|
|
1612
|
+
const overlayToggle = shortcuts.overlayToggle;
|
|
1613
|
+
if (
|
|
1614
|
+
effectiveDisplayMode === "overlay"
|
|
1615
|
+
&& !overlayToggle.disabled
|
|
1616
|
+
&& typeof ctx.ui.onTerminalInput === "function"
|
|
1617
|
+
) {
|
|
1618
|
+
removeOverlayInputListener = ctx.ui.onTerminalInput((data) => {
|
|
1619
|
+
if (!overlayToggle.matches(data) || !overlayHandle) return undefined;
|
|
1620
|
+
const nextHidden = !overlayHandle.isHidden();
|
|
1621
|
+
overlayHandle.setHidden(nextHidden);
|
|
1622
|
+
if (nextHidden && !hasAnnouncedHide) {
|
|
1623
|
+
hasAnnouncedHide = true;
|
|
1624
|
+
ctx.ui.notify?.(`ask_user hidden — press ${overlayToggle.spec} to reopen`, "info");
|
|
1625
|
+
}
|
|
1626
|
+
return { consume: true };
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const customResult = await ctx.ui.custom<AskUIResult | null>(
|
|
1631
|
+
customFactory,
|
|
1632
|
+
buildCustomUIOptions(effectiveDisplayMode, (handle) => {
|
|
1633
|
+
overlayHandle = handle;
|
|
1634
|
+
}),
|
|
1635
|
+
);
|
|
1490
1636
|
|
|
1491
1637
|
if (customResult !== undefined) {
|
|
1492
1638
|
result = customResult;
|
|
@@ -1502,6 +1648,8 @@ export default function(pi: ExtensionAPI) {
|
|
|
1502
1648
|
isError: true,
|
|
1503
1649
|
details: { error: message },
|
|
1504
1650
|
};
|
|
1651
|
+
} finally {
|
|
1652
|
+
removeOverlayInputListener?.();
|
|
1505
1653
|
}
|
|
1506
1654
|
|
|
1507
1655
|
if (result === null) {
|
package/package.json
CHANGED