@alexleekt/pi-ask-user-glimpse 0.4.1 → 0.5.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/CHANGELOG.md +24 -1
- package/CONTRIBUTING.md +2 -2
- package/README.md +58 -37
- package/dist/index.html +997 -438
- package/index.ts +322 -266
- package/package.json +59 -10
- package/shared/ask-user.ts +5 -0
- package/tool/ask-user.ts +52 -18
- package/tool/response-formatter.ts +3 -2
- package/fallback/terminal-prompt.ts +0 -191
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexleekt/pi-ask-user-glimpse",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Ask better questions. Get better answers. Rich native WebView dialogs for the Pi agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
|
+
"pi",
|
|
9
10
|
"glimpseui",
|
|
10
11
|
"ask_user",
|
|
11
12
|
"interactive"
|
|
@@ -32,7 +33,6 @@
|
|
|
32
33
|
"index.ts",
|
|
33
34
|
"constants",
|
|
34
35
|
"tool",
|
|
35
|
-
"fallback",
|
|
36
36
|
"shared",
|
|
37
37
|
"types",
|
|
38
38
|
"dist",
|
|
@@ -42,26 +42,73 @@
|
|
|
42
42
|
"LICENSE"
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
|
-
"build": "
|
|
46
|
-
"build:css": "
|
|
47
|
-
"build:webview": "
|
|
45
|
+
"build": "wireit",
|
|
46
|
+
"build:css": "wireit",
|
|
47
|
+
"build:webview": "wireit",
|
|
48
48
|
"typecheck": "tsc --noEmit",
|
|
49
49
|
"check": "npm pack --dry-run",
|
|
50
50
|
"prepack": "npm run build",
|
|
51
51
|
"dev:webview": "npx vite --config ./webview/vite.config.ts",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"test:
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"test:coverage": "vitest run --coverage",
|
|
55
|
+
"test:e2e": "playwright test",
|
|
56
|
+
"test:e2e:ui": "playwright test --ui",
|
|
57
|
+
"validate": "test -f dist/index.html && grep -q 'ASK_USER_PAYLOAD' dist/index.html && echo \"✓ dist/index.html ready\" || echo \"✗ Run npm run build first\"",
|
|
58
|
+
"validate:gui": "echo \"validate:gui requires manual testing with pi extension load\""
|
|
59
|
+
},
|
|
60
|
+
"wireit": {
|
|
61
|
+
"build": {
|
|
62
|
+
"dependencies": [
|
|
63
|
+
"build:css",
|
|
64
|
+
"build:webview"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"build:css": {
|
|
68
|
+
"command": "npx tailwindcss -c ./webview/tailwind.config.js -i ./webview/src/index.css -o ./webview/src/index.generated.css --content './webview/index.html,./webview/src/**/*.{js,ts,jsx,tsx}'",
|
|
69
|
+
"files": [
|
|
70
|
+
"./webview/src/**/*.css",
|
|
71
|
+
"./webview/src/**/*.{ts,tsx,js,jsx}",
|
|
72
|
+
"./webview/tailwind.config.js",
|
|
73
|
+
"./webview/postcss.config.js",
|
|
74
|
+
"./webview/index.html"
|
|
75
|
+
],
|
|
76
|
+
"output": [
|
|
77
|
+
"./webview/src/index.generated.css"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"build:webview": {
|
|
81
|
+
"command": "npx vite build --config ./webview/vite.config.ts",
|
|
82
|
+
"files": [
|
|
83
|
+
"./webview/src/**/*.{ts,tsx}",
|
|
84
|
+
"./webview/index.html",
|
|
85
|
+
"./webview/vite.config.ts",
|
|
86
|
+
"./webview/tsconfig.json",
|
|
87
|
+
"./webview/postcss.config.js",
|
|
88
|
+
"./webview/src/index.generated.css"
|
|
89
|
+
],
|
|
90
|
+
"output": [
|
|
91
|
+
"./dist/index.html"
|
|
92
|
+
],
|
|
93
|
+
"dependencies": [
|
|
94
|
+
"build:css"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
55
97
|
},
|
|
56
98
|
"dependencies": {
|
|
57
99
|
"@alexleekt/pi-shared": "^0.1.0",
|
|
58
100
|
"glimpseui": "^0.8.1"
|
|
59
101
|
},
|
|
60
102
|
"devDependencies": {
|
|
103
|
+
"@playwright/test": "^1.60.0",
|
|
104
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
105
|
+
"@testing-library/react": "^16.3.2",
|
|
61
106
|
"@types/react": "^18.3.0",
|
|
62
107
|
"@types/react-dom": "^18.3.0",
|
|
63
108
|
"@vitejs/plugin-react": "^4.3.0",
|
|
109
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
64
110
|
"autoprefixer": "^10.4.20",
|
|
111
|
+
"jsdom": "^29.1.1",
|
|
65
112
|
"marked": "^15.0.12",
|
|
66
113
|
"mermaid": "^11.15.0",
|
|
67
114
|
"playwright": "^1.60.0",
|
|
@@ -71,7 +118,9 @@
|
|
|
71
118
|
"tailwindcss": "^3.4.14",
|
|
72
119
|
"typescript": "^5.6.0",
|
|
73
120
|
"vite": "^5.4.0",
|
|
74
|
-
"vite-plugin-singlefile": "^2.0.0"
|
|
121
|
+
"vite-plugin-singlefile": "^2.0.0",
|
|
122
|
+
"vitest": "^4.1.7",
|
|
123
|
+
"wireit": "^0.14.12"
|
|
75
124
|
},
|
|
76
125
|
"peerDependencies": {
|
|
77
126
|
"@earendil-works/pi-ai": "*",
|
package/shared/ask-user.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export interface QuestionOption {
|
|
10
10
|
title: string;
|
|
11
11
|
description?: string;
|
|
12
|
+
recommended?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface Question {
|
|
@@ -25,6 +26,7 @@ export interface AskUserPayload {
|
|
|
25
26
|
type: "single-select" | "multi-select" | "questionnaire" | "freeform";
|
|
26
27
|
question: string;
|
|
27
28
|
context?: string;
|
|
29
|
+
contextFormat?: "markdown" | "html";
|
|
28
30
|
options: QuestionOption[];
|
|
29
31
|
questions?: Question[];
|
|
30
32
|
allowMultiple: boolean;
|
|
@@ -42,3 +44,6 @@ export interface QuestionnaireDetail {
|
|
|
42
44
|
kind: "selection" | "freeform";
|
|
43
45
|
comment?: string;
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
/** Sentinel title used for the freeform "not listed" option in select dialogs. */
|
|
49
|
+
export const FREEFORM_OPTION_TITLE = "My answer isn't listed above";
|
package/tool/ask-user.ts
CHANGED
|
@@ -4,8 +4,12 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { prompt } from "glimpseui";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import type {
|
|
8
|
+
AnimationLevel,
|
|
9
|
+
AskUserPayload,
|
|
10
|
+
Question,
|
|
11
|
+
ThemeMode,
|
|
12
|
+
} from "../shared/ask-user.js";
|
|
9
13
|
import { formatResponse } from "./response-formatter.js";
|
|
10
14
|
|
|
11
15
|
const _require = createRequire(import.meta.url);
|
|
@@ -13,6 +17,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
13
17
|
|
|
14
18
|
import { STOPWORDS } from "../constants/stopwords.js";
|
|
15
19
|
|
|
20
|
+
/** Warn once per process when Glimpse is unavailable. */
|
|
21
|
+
let _warnedGlimpseUnavailable = false;
|
|
22
|
+
|
|
16
23
|
/** Extract a short title from a question by removing stopwords.
|
|
17
24
|
* Falls back to first 5 words if nothing meaningful remains.
|
|
18
25
|
*/
|
|
@@ -39,12 +46,17 @@ function summarizeTitle(question: string, maxWords = 3): string {
|
|
|
39
46
|
|
|
40
47
|
function resolveWebviewHtml(): string {
|
|
41
48
|
const distPath = join(__dirname, "..", "dist", "index.html");
|
|
49
|
+
console.log(`[pi-ask-user-glimpse] Loading webview from: ${distPath}`);
|
|
42
50
|
try {
|
|
43
|
-
|
|
51
|
+
const html = readFileSync(distPath, "utf-8");
|
|
52
|
+
const hasNewCode = html.includes('items-center justify-between');
|
|
53
|
+
console.log(`[pi-ask-user-glimpse] Webview has new code: ${hasNewCode}`);
|
|
54
|
+
return html;
|
|
44
55
|
} catch {
|
|
45
56
|
// Fallback for development: resolve from package root
|
|
46
57
|
const pkgRoot = dirname(_require.resolve("../package.json"));
|
|
47
58
|
const fallbackPath = join(pkgRoot, "dist", "index.html");
|
|
59
|
+
console.log(`[pi-ask-user-glimpse] Fallback: ${fallbackPath}`);
|
|
48
60
|
try {
|
|
49
61
|
return readFileSync(fallbackPath, "utf-8");
|
|
50
62
|
} catch (err) {
|
|
@@ -62,7 +74,11 @@ function resolveWebviewHtml(): string {
|
|
|
62
74
|
export interface AskUserParams {
|
|
63
75
|
question: string;
|
|
64
76
|
context?: string;
|
|
65
|
-
|
|
77
|
+
contextFormat?: "markdown" | "html";
|
|
78
|
+
options?: (
|
|
79
|
+
| string
|
|
80
|
+
| { title: string; description?: string; recommended?: boolean }
|
|
81
|
+
)[];
|
|
66
82
|
questions?: Question[];
|
|
67
83
|
allowMultiple?: boolean;
|
|
68
84
|
allowFreeform?: boolean;
|
|
@@ -99,7 +115,11 @@ export async function askUserHandler(
|
|
|
99
115
|
|
|
100
116
|
const normalizedOptions = (params.options ?? []).map((opt) => {
|
|
101
117
|
if (typeof opt === "string") return { title: opt };
|
|
102
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
title: opt.title,
|
|
120
|
+
description: opt.description,
|
|
121
|
+
recommended: opt.recommended,
|
|
122
|
+
};
|
|
103
123
|
});
|
|
104
124
|
|
|
105
125
|
const hasOptions = normalizedOptions.length > 0;
|
|
@@ -135,6 +155,7 @@ export async function askUserHandler(
|
|
|
135
155
|
type: payloadType,
|
|
136
156
|
question,
|
|
137
157
|
context,
|
|
158
|
+
contextFormat: params.contextFormat,
|
|
138
159
|
options: normalizedOptions,
|
|
139
160
|
questions: params.questions,
|
|
140
161
|
allowMultiple,
|
|
@@ -166,17 +187,17 @@ export async function askUserHandler(
|
|
|
166
187
|
? `Pi · ${sessionName} · ${questionTitle}`
|
|
167
188
|
: `Pi · ${questionTitle}`;
|
|
168
189
|
|
|
169
|
-
const
|
|
190
|
+
const windowOptions: Record<string, unknown> = {
|
|
170
191
|
width: 1200,
|
|
171
192
|
height: 900,
|
|
172
193
|
title: title.length > 60 ? `${title.slice(0, 57)}…` : title,
|
|
173
194
|
};
|
|
174
195
|
|
|
175
196
|
if (params.followCursor) {
|
|
176
|
-
|
|
197
|
+
windowOptions.followCursor = true;
|
|
177
198
|
}
|
|
178
199
|
|
|
179
|
-
result = (await prompt(html,
|
|
200
|
+
result = (await prompt(html, windowOptions)) as Record<
|
|
180
201
|
string,
|
|
181
202
|
unknown
|
|
182
203
|
> | null;
|
|
@@ -190,17 +211,30 @@ export async function askUserHandler(
|
|
|
190
211
|
});
|
|
191
212
|
}
|
|
192
213
|
} catch (err) {
|
|
193
|
-
// Glimpse unavailable —
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
result = fallbackResult;
|
|
214
|
+
// Glimpse unavailable — fast-exit and warn once
|
|
215
|
+
if (!_warnedGlimpseUnavailable) {
|
|
216
|
+
_warnedGlimpseUnavailable = true;
|
|
217
|
+
console.warn(
|
|
218
|
+
"[pi-ask-user-glimpse] Glimpse unavailable — " +
|
|
219
|
+
"ask_user will return errors. " +
|
|
220
|
+
"Install glimpseui or run in a UI-enabled environment.",
|
|
221
|
+
);
|
|
202
222
|
}
|
|
203
|
-
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: "text" as const,
|
|
227
|
+
text: "No UI available for ask_user dialog. Please ask the user directly in free-form text.",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
details: {
|
|
231
|
+
question: params.question,
|
|
232
|
+
options: normalizedOptions.map((o) => o.title),
|
|
233
|
+
response: null,
|
|
234
|
+
cancelled: true,
|
|
235
|
+
error: "No UI available",
|
|
236
|
+
},
|
|
237
|
+
};
|
|
204
238
|
}
|
|
205
239
|
|
|
206
240
|
return formatResponse(
|
|
@@ -60,6 +60,7 @@ function buildResponse(
|
|
|
60
60
|
};
|
|
61
61
|
})
|
|
62
62
|
: [],
|
|
63
|
+
additionalComments: pickString(result.additionalComments),
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -79,14 +80,14 @@ function buildResponse(
|
|
|
79
80
|
|
|
80
81
|
function responseToText(response: AskResponse): string {
|
|
81
82
|
if (response.kind === "freeform") {
|
|
82
|
-
return response.text
|
|
83
|
+
return response.text?.trim() || "No response";
|
|
83
84
|
}
|
|
84
85
|
const lines: string[] = [];
|
|
85
86
|
const selections = response.selections ?? [];
|
|
86
87
|
if (selections.length > 0) lines.push(selections.join(", "));
|
|
87
88
|
if (response.comment) lines.push(`Comment: ${response.comment}`);
|
|
88
89
|
if (response.additionalComments) lines.push(`Additional Comments: ${response.additionalComments}`);
|
|
89
|
-
return lines.join("\n\n");
|
|
90
|
+
return lines.join("\n\n") || "No response";
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
export function formatResponse(
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AskUserPayload, Question } from "../shared/ask-user.js";
|
|
3
|
-
|
|
4
|
-
export async function terminalPrompt(
|
|
5
|
-
payload: AskUserPayload,
|
|
6
|
-
ui: ExtensionUIContext | undefined,
|
|
7
|
-
): Promise<Record<string, unknown> | null> {
|
|
8
|
-
if (!ui) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Questionnaire mode: structured questions with per-question options
|
|
13
|
-
if (payload.questions && payload.questions.length > 0) {
|
|
14
|
-
return questionnaireFallback(
|
|
15
|
-
payload.questions,
|
|
16
|
-
payload.allowComment,
|
|
17
|
-
ui,
|
|
18
|
-
payload.context,
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Legacy flat options mode
|
|
23
|
-
return flatOptionsFallback(payload, ui);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type QuestionnaireAnswer = {
|
|
27
|
-
question: string;
|
|
28
|
-
answer: string;
|
|
29
|
-
kind: "selection" | "freeform";
|
|
30
|
-
comment?: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
async function questionnaireFallback(
|
|
34
|
-
questions: Question[],
|
|
35
|
-
allowComment: boolean,
|
|
36
|
-
ui: ExtensionUIContext,
|
|
37
|
-
context?: string,
|
|
38
|
-
): Promise<Record<string, unknown> | null> {
|
|
39
|
-
const answers: QuestionnaireAnswer[] = [];
|
|
40
|
-
|
|
41
|
-
for (const q of questions) {
|
|
42
|
-
const prompt = context ? `${q.title}\n\nContext: ${context}` : q.title;
|
|
43
|
-
let answer: string | undefined;
|
|
44
|
-
|
|
45
|
-
if (q.options && q.options.length > 0) {
|
|
46
|
-
const labels = q.options.map((opt, i) => `${i + 1}. ${opt.title}`);
|
|
47
|
-
|
|
48
|
-
if (q.allowMultiple) {
|
|
49
|
-
const selections: string[] = [];
|
|
50
|
-
while (true) {
|
|
51
|
-
const remaining = labels.filter((_, i) => {
|
|
52
|
-
const title = q.options?.[i]?.title;
|
|
53
|
-
return title ? !selections.includes(title) : false;
|
|
54
|
-
});
|
|
55
|
-
if (remaining.length === 0) break;
|
|
56
|
-
|
|
57
|
-
const choice = await ui.select(
|
|
58
|
-
`${prompt}\nSelected: ${selections.join(", ") || "none"}\nChoose one (or cancel to finish)`,
|
|
59
|
-
remaining,
|
|
60
|
-
);
|
|
61
|
-
if (choice === undefined) break;
|
|
62
|
-
|
|
63
|
-
const idx = labels.indexOf(choice);
|
|
64
|
-
const title = q.options[idx]?.title;
|
|
65
|
-
if (title && !selections.includes(title)) {
|
|
66
|
-
selections.push(title);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
answer = selections.join(", ");
|
|
70
|
-
} else {
|
|
71
|
-
const choice = await ui.select(prompt, labels);
|
|
72
|
-
if (choice === undefined) return null;
|
|
73
|
-
const idx = labels.indexOf(choice);
|
|
74
|
-
answer = q.options[idx]?.title;
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
answer = await ui.input(
|
|
78
|
-
prompt + (q.description ? `\n${q.description}` : ""),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (answer === undefined) return null;
|
|
83
|
-
|
|
84
|
-
let comment: string | undefined;
|
|
85
|
-
if (allowComment) {
|
|
86
|
-
comment =
|
|
87
|
-
(await ui.input(
|
|
88
|
-
`Comment for "${q.title}" (press Enter to skip):`,
|
|
89
|
-
)) ?? undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
answers.push({
|
|
93
|
-
question: q.title,
|
|
94
|
-
answer,
|
|
95
|
-
kind: q.options && q.options.length > 0 ? "selection" : "freeform",
|
|
96
|
-
comment,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
kind: "questionnaire",
|
|
102
|
-
selections: answers.map((a) => `${a.question}: ${a.answer}`),
|
|
103
|
-
questionnaireDetails: answers,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function flatOptionsFallback(
|
|
108
|
-
payload: AskUserPayload,
|
|
109
|
-
ui: ExtensionUIContext,
|
|
110
|
-
): Promise<Record<string, unknown> | null> {
|
|
111
|
-
const {
|
|
112
|
-
question,
|
|
113
|
-
context,
|
|
114
|
-
options,
|
|
115
|
-
allowMultiple,
|
|
116
|
-
allowFreeform,
|
|
117
|
-
allowComment,
|
|
118
|
-
} = payload;
|
|
119
|
-
|
|
120
|
-
const prompt = context ? `${question}\n\nContext: ${context}` : question;
|
|
121
|
-
|
|
122
|
-
if (options.length === 0) {
|
|
123
|
-
const text = await ui.input(prompt);
|
|
124
|
-
if (text === undefined) return null;
|
|
125
|
-
return { kind: "freeform", text };
|
|
126
|
-
}
|
|
127
|
-
const optionLabels = options.map((opt, i) => `${i + 1}. ${opt.title}`);
|
|
128
|
-
if (allowFreeform) {
|
|
129
|
-
optionLabels.push("Other (freeform)");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (allowMultiple) {
|
|
133
|
-
const selections: string[] = [];
|
|
134
|
-
while (true) {
|
|
135
|
-
const remaining = optionLabels.filter(
|
|
136
|
-
(_, i) => !selections.includes(options[i]?.title ?? ""),
|
|
137
|
-
);
|
|
138
|
-
if (remaining.length === 0) break;
|
|
139
|
-
|
|
140
|
-
const choice = await ui.select(
|
|
141
|
-
`${prompt}\nSelected: ${selections.join(", ") || "none"}\nChoose one (or cancel to finish)`,
|
|
142
|
-
remaining,
|
|
143
|
-
);
|
|
144
|
-
if (choice === undefined) break;
|
|
145
|
-
|
|
146
|
-
const idx = optionLabels.indexOf(choice);
|
|
147
|
-
if (idx >= options.length) {
|
|
148
|
-
const text = await ui.input("Enter your answer:");
|
|
149
|
-
if (text?.trim()) {
|
|
150
|
-
selections.push(`Other: ${text.trim()}`);
|
|
151
|
-
}
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const title = options[idx]?.title;
|
|
155
|
-
if (title && !selections.includes(title)) {
|
|
156
|
-
selections.push(title);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let comment: string | undefined;
|
|
161
|
-
if (allowComment && selections.length > 0) {
|
|
162
|
-
comment =
|
|
163
|
-
(await ui.input("Optional comment (press Enter to skip):")) ??
|
|
164
|
-
undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return { kind: "selection", selections, comment };
|
|
168
|
-
} else {
|
|
169
|
-
const choice = await ui.select(prompt, optionLabels);
|
|
170
|
-
if (choice === undefined) return null;
|
|
171
|
-
|
|
172
|
-
const idx = optionLabels.indexOf(choice);
|
|
173
|
-
if (idx >= options.length) {
|
|
174
|
-
const text = await ui.input("Enter your answer:");
|
|
175
|
-
if (text === undefined) return null;
|
|
176
|
-
return { kind: "freeform", text };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const title = options[idx]?.title;
|
|
180
|
-
if (!title) return null;
|
|
181
|
-
|
|
182
|
-
let comment: string | undefined;
|
|
183
|
-
if (allowComment) {
|
|
184
|
-
comment =
|
|
185
|
-
(await ui.input("Optional comment (press Enter to skip):")) ??
|
|
186
|
-
undefined;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return { kind: "selection", selections: [title], comment };
|
|
190
|
-
}
|
|
191
|
-
}
|