@alexleekt/pi-ask-user-glimpse 0.2.1

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.
@@ -0,0 +1,169 @@
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(payload.questions, payload.allowComment, ui, payload.context);
15
+ }
16
+
17
+ // Legacy flat options mode
18
+ return flatOptionsFallback(payload, ui);
19
+ }
20
+
21
+ type QuestionnaireAnswer = {
22
+ question: string;
23
+ answer: string;
24
+ kind: "selection" | "freeform";
25
+ comment?: string;
26
+ };
27
+
28
+ async function questionnaireFallback(
29
+ questions: Question[],
30
+ allowComment: boolean,
31
+ ui: ExtensionUIContext,
32
+ context?: string,
33
+ ): Promise<Record<string, unknown> | null> {
34
+ const answers: QuestionnaireAnswer[] = [];
35
+
36
+ for (const q of questions) {
37
+ const prompt = context
38
+ ? `${q.title}\n\nContext: ${context}`
39
+ : q.title;
40
+ let answer: string | undefined;
41
+
42
+ if (q.options && q.options.length > 0) {
43
+ const labels = q.options.map((opt, i) => `${i + 1}. ${opt.title}`);
44
+
45
+ if (q.allowMultiple) {
46
+ const selections: string[] = [];
47
+ while (true) {
48
+ const remaining = labels.filter((_, i) => !selections.includes(q.options![i].title));
49
+ if (remaining.length === 0) break;
50
+
51
+ const choice = await ui.select(
52
+ `${prompt}\nSelected: ${selections.join(", ") || "none"}\nChoose one (or cancel to finish)`,
53
+ remaining,
54
+ );
55
+ if (choice === undefined) break;
56
+
57
+ const idx = labels.indexOf(choice);
58
+ const title = q.options[idx]?.title;
59
+ if (title && !selections.includes(title)) {
60
+ selections.push(title);
61
+ }
62
+ }
63
+ answer = selections.join(", ");
64
+ } else {
65
+ const choice = await ui.select(prompt, labels);
66
+ if (choice === undefined) return null;
67
+ const idx = labels.indexOf(choice);
68
+ answer = q.options[idx]?.title;
69
+ }
70
+ } else {
71
+ answer = await ui.input(prompt + (q.description ? `\n${q.description}` : ""));
72
+ }
73
+
74
+ if (answer === undefined) return null;
75
+
76
+ let comment: string | undefined;
77
+ if (allowComment) {
78
+ comment = (await ui.input(`Comment for "${q.title}" (press Enter to skip):`)) ?? undefined;
79
+ }
80
+
81
+ answers.push({
82
+ question: q.title,
83
+ answer,
84
+ kind: (q.options && q.options.length > 0 ? "selection" : "freeform"),
85
+ comment,
86
+ });
87
+ }
88
+
89
+ return {
90
+ kind: "questionnaire",
91
+ selections: answers.map((a) => `${a.question}: ${a.answer}`),
92
+ questionnaireDetails: answers,
93
+ };
94
+ }
95
+
96
+ async function flatOptionsFallback(
97
+ payload: AskUserPayload,
98
+ ui: ExtensionUIContext,
99
+ ): Promise<Record<string, unknown> | null> {
100
+ const { question, context, options, allowMultiple, allowFreeform, allowComment } = payload;
101
+
102
+ const prompt = context ? `${question}\n\nContext: ${context}` : question;
103
+
104
+ if (options.length === 0) {
105
+ const text = await ui.input(prompt);
106
+ if (text === undefined) return null;
107
+ return { kind: "freeform", text };
108
+ }
109
+ const optionLabels = options.map((opt, i) => `${i + 1}. ${opt.title}`);
110
+ if (allowFreeform) {
111
+ optionLabels.push("Other (freeform)");
112
+ }
113
+
114
+ if (allowMultiple) {
115
+ const selections: string[] = [];
116
+ while (true) {
117
+ const remaining = optionLabels.filter(
118
+ (_, i) => !selections.includes(options[i]?.title ?? ""),
119
+ );
120
+ if (remaining.length === 0) break;
121
+
122
+ const choice = await ui.select(
123
+ `${prompt}\nSelected: ${selections.join(", ") || "none"}\nChoose one (or cancel to finish)`,
124
+ remaining,
125
+ );
126
+ if (choice === undefined) break;
127
+
128
+ const idx = optionLabels.indexOf(choice);
129
+ if (idx >= options.length) {
130
+ const text = await ui.input("Enter your answer:");
131
+ if (text !== undefined && text.trim()) {
132
+ selections.push(`Other: ${text.trim()}`);
133
+ }
134
+ continue;
135
+ }
136
+ const title = options[idx]?.title;
137
+ if (title && !selections.includes(title)) {
138
+ selections.push(title);
139
+ }
140
+ }
141
+
142
+ let comment: string | undefined;
143
+ if (allowComment && selections.length > 0) {
144
+ comment = (await ui.input("Optional comment (press Enter to skip):")) ?? undefined;
145
+ }
146
+
147
+ return { kind: "selection", selections, comment };
148
+ } else {
149
+ const choice = await ui.select(prompt, optionLabels);
150
+ if (choice === undefined) return null;
151
+
152
+ const idx = optionLabels.indexOf(choice);
153
+ if (idx >= options.length) {
154
+ const text = await ui.input("Enter your answer:");
155
+ if (text === undefined) return null;
156
+ return { kind: "freeform", text };
157
+ }
158
+
159
+ const title = options[idx]?.title;
160
+ if (!title) return null;
161
+
162
+ let comment: string | undefined;
163
+ if (allowComment) {
164
+ comment = (await ui.input("Optional comment (press Enter to skip):")) ?? undefined;
165
+ }
166
+
167
+ return { kind: "selection", selections: [title], comment };
168
+ }
169
+ }
package/index.ts ADDED
@@ -0,0 +1,443 @@
1
+ /**
2
+ * pi-ask-user-glimpse — Pi extension that replaces ask_user with native WebView dialogs via glimpseui.
3
+ */
4
+
5
+ import type { BuildSystemPromptOptions, ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { defineTool } from "@earendil-works/pi-coding-agent";
7
+ import { Type, StringEnum } from "@earendil-works/pi-ai";
8
+ import { askUserHandler, type AskUserParams } from "./tool/ask-user.js";
9
+
10
+ /* ── Generic question-session detection ── */
11
+
12
+ const QUESTION_SESSION_PATTERNS = [
13
+ /ask the questions? one at a time/i,
14
+ /interview me/i,
15
+ /grilling session/i,
16
+ /ask questions? (one at a time|sequentially|individually)/i,
17
+ /wait for feedback/i,
18
+ /questionnaire mode/i,
19
+ /one question per call/i,
20
+ ];
21
+
22
+ const QUESTION_SKILL_NAMES = new Set(["grill-with-docs", "questionnaire", "interview", "grill"]);
23
+
24
+ const ASK_USER_MANDATE = `
25
+ ## Tool Usage Mandate — Auto-injected by pi-ask-user-glimpse
26
+
27
+ When you need to ask the user a question, you MUST use the \`ask_user\`
28
+ tool. Do NOT write questions as free-form assistant text. Each question
29
+ should be a separate \`ask_user\` tool call.
30
+
31
+ Rules:
32
+ - One question per \`ask_user\` call.
33
+ - Provide concise options when the question has discrete choices.
34
+ - Set \`allowMultiple: true\` when more than one choice is valid.
35
+ - Set \`allowFreeform: true\` when the user might want to answer in their own words.
36
+ - Wait for the tool result before continuing to the next question.
37
+ `;
38
+
39
+ function isQuestionSession(systemPrompt: string, options: BuildSystemPromptOptions): boolean {
40
+ const hasQuestionSkill = !!options.skills?.some((s) =>
41
+ QUESTION_SKILL_NAMES.has(s.name.toLowerCase()),
42
+ );
43
+ const hasQuestionLanguage = QUESTION_SESSION_PATTERNS.some((p) => p.test(systemPrompt));
44
+ return hasQuestionSkill || hasQuestionLanguage;
45
+ }
46
+
47
+ function getStyleMode(entries: any[]): boolean | null {
48
+ const entry = entries.find((e) => e.type === "custom" && e.customType === "ask-user-style");
49
+ return (entry as any)?.data?.enabled ?? null;
50
+ }
51
+
52
+ function extractTextFromAssistantEntry(entry: any): string {
53
+ const content = entry.message?.content;
54
+ if (typeof content === "string") return content;
55
+ if (!Array.isArray(content)) return "";
56
+ return content
57
+ .filter((c) => c.type === "text")
58
+ .map((c) => c.text)
59
+ .join("\n");
60
+ }
61
+
62
+ /* ── /ask-last: extract questions & implicit requests ── */
63
+
64
+ const PROTECTED_ABBREVIATIONS = new Set([
65
+ "etc", "vs", "fig", "dr", "mr", "mrs", "ms",
66
+ "prof", "jr", "sr", "inc", "ltd", "corp", "co", "llc", "al",
67
+ "et", "vol", "vols", "pg", "pp", "ch", "chap", "sec", "secs",
68
+ ]);
69
+
70
+ function splitSentences(text: string): string[] {
71
+ const PLACEHOLDER = "\x00";
72
+
73
+ let buffer = text.replace(
74
+ /\b(e\.g\.|i\.e\.)\b/gi,
75
+ (m) => m.replace(/\./g, PLACEHOLDER),
76
+ );
77
+
78
+ buffer = buffer.replace(
79
+ /\b([a-zA-Z]{1,4})\./g,
80
+ (match, abbr) => (PROTECTED_ABBREVIATIONS.has(abbr.toLowerCase()) ? match.replace(".", PLACEHOLDER) : match),
81
+ );
82
+
83
+ buffer = buffer.replace(/\d+\.\d+/g, (m) => m.replace(/\./g, PLACEHOLDER));
84
+ buffer = buffer.replace(/https?:\/\/\S+/g, (m) => m.replace(/\./g, PLACEHOLDER));
85
+ buffer = buffer.replace(/\b\w\.\w\./g, (m) => m.replace(/\./g, PLACEHOLDER));
86
+
87
+ return buffer
88
+ .split(/(?<=[.!?])\s+/)
89
+ .map((s) => s.trim().replace(new RegExp(PLACEHOLDER, "g"), "."))
90
+ .filter((s) => s.length > 0);
91
+ }
92
+
93
+ const IMPLICIT_REQUEST_PATTERNS = [
94
+ /\b(let me know|let us know)\b/i,
95
+ /\b(tell me|tell us)\b/i,
96
+ /\b(share your|share any)\b/i,
97
+ /\b(what do you think|what are your thoughts)\b/i,
98
+ /\b(which\b.*\b(would you|do you|should we)\b)/i,
99
+ /\b(should we|can you confirm|please confirm|could you confirm)\b/i,
100
+ /\b(i need your (input|feedback|thoughts|opinion))\b/i,
101
+ /\b(your (thoughts|opinion|preference|feedback))\b/i,
102
+ /\b(please provide|could you provide|can you provide)\b/i,
103
+ /\b(would you like|do you want|do you prefer)\b/i,
104
+ ];
105
+
106
+ function hasQuotedQuestion(sentence: string): boolean {
107
+ return /["'`].*\?.*["'`]/.test(sentence) && !sentence.endsWith("?");
108
+ }
109
+
110
+ function looksLikeTernary(sentence: string): boolean {
111
+ return /\?\s*[:;]/.test(sentence) || /=\s*\S+\s*\?/.test(sentence);
112
+ }
113
+
114
+ function extractQuestions(text: string): string[] {
115
+ const explicit: string[] = [];
116
+ const implicit: string[] = [];
117
+
118
+ for (const sentence of splitSentences(text)) {
119
+ if (sentence.endsWith("?")) {
120
+ if (hasQuotedQuestion(sentence)) continue;
121
+ if (looksLikeTernary(sentence)) continue;
122
+ if (sentence.length < 10) continue;
123
+ explicit.push(sentence);
124
+ continue;
125
+ }
126
+
127
+ if (IMPLICIT_REQUEST_PATTERNS.some((p) => p.test(sentence))) {
128
+ implicit.push(sentence);
129
+ }
130
+ }
131
+
132
+ return [...explicit, ...implicit];
133
+ }
134
+
135
+ function truncate(str: string, max: number): string {
136
+ return str.length > max ? str.slice(0, max - 3) + "..." : str;
137
+ }
138
+
139
+ function buildAskLastParams(questions: string[], fullText: string): AskUserParams {
140
+ if (questions.length === 0) {
141
+ return {
142
+ question: "The assistant would like your input on the following:",
143
+ context: fullText,
144
+ allowFreeform: true,
145
+ };
146
+ }
147
+ if (questions.length === 1) {
148
+ return {
149
+ question: questions[0],
150
+ context: fullText,
151
+ allowFreeform: true,
152
+ };
153
+ }
154
+ return {
155
+ question: "The assistant asked multiple questions",
156
+ context: fullText,
157
+ questions: questions.map((q) => ({
158
+ title: truncate(q, 60),
159
+ description: q,
160
+ })),
161
+ allowComment: true,
162
+ allowSkip: true,
163
+ };
164
+ }
165
+
166
+ function buildDebugParams(mode: string): AskUserParams | null {
167
+ switch (mode) {
168
+ case "single-select":
169
+ return {
170
+ question: "Test: Single Select",
171
+ context: "Pick one option (with optional freeform and comment)",
172
+ options: [
173
+ { title: "Option A", description: "Description for A" },
174
+ { title: "Option B", description: "Description for B" },
175
+ { title: "Option C", description: "Description for C" },
176
+ ],
177
+ allowFreeform: true,
178
+ allowComment: true,
179
+ };
180
+ case "multi-select":
181
+ return {
182
+ question: "Test: Multi Select",
183
+ context: "Pick multiple options (with optional freeform and comment)",
184
+ options: [
185
+ { title: "Feature X", description: "Enable feature X" },
186
+ { title: "Feature Y", description: "Enable feature Y" },
187
+ { title: "Feature Z", description: "Enable feature Z" },
188
+ ],
189
+ allowMultiple: true,
190
+ allowFreeform: true,
191
+ allowComment: true,
192
+ };
193
+ case "freeform":
194
+ return {
195
+ question: "Test: Freeform",
196
+ context: "Type any answer you like",
197
+ allowFreeform: true,
198
+ };
199
+ case "questionnaire":
200
+ return {
201
+ question: "Test: Questionnaire",
202
+ context: "Answer multiple structured questions",
203
+ questions: [
204
+ {
205
+ title: "Database",
206
+ description: "Which database should we use?",
207
+ options: [
208
+ { title: "PostgreSQL", description: "Relational, proven" },
209
+ { title: "SQLite", description: "Zero-config" },
210
+ ],
211
+ },
212
+ {
213
+ title: "Architecture",
214
+ description: "Preferred style?",
215
+ options: [
216
+ { title: "Monolith", description: "Simple" },
217
+ { title: "Microservices", description: "Scalable" },
218
+ ],
219
+ allowMultiple: true,
220
+ },
221
+ {
222
+ title: "Notes",
223
+ description: "Any additional thoughts?",
224
+ },
225
+ ],
226
+ allowComment: true,
227
+ };
228
+ default:
229
+ return null;
230
+ }
231
+ }
232
+
233
+ const askUserTool = defineTool({
234
+ name: "ask_user",
235
+ label: "Ask User",
236
+ description:
237
+ "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call. Before calling, gather context with tools (read/web/ref) and pass a short summary via the context field.",
238
+ promptSnippet:
239
+ "Ask the user one focused question with optional multiple-choice answers to gather information interactively",
240
+ promptGuidelines: [
241
+ "Always use ask_user instead of guessing when user input would improve the answer.",
242
+ "Pass a concise question and, when applicable, a list of options with short titles and optional longer descriptions.",
243
+ "Set allowMultiple: true when more than one choice is valid.",
244
+ "Set allowFreeform: true (default) when the user might want to answer in their own words.",
245
+ ],
246
+ parameters: Type.Object({
247
+ question: Type.String({ description: "The question to ask the user" }),
248
+ context: Type.Optional(
249
+ Type.String({
250
+ description: "Additional context to help the user understand the question",
251
+ }),
252
+ ),
253
+ options: Type.Optional(
254
+ Type.Array(
255
+ Type.Union([
256
+ Type.String({ description: "Short option label" }),
257
+ Type.Object({
258
+ title: Type.String({ description: "Short title for this option" }),
259
+ description: Type.Optional(
260
+ Type.String({
261
+ description: "Longer description explaining this option",
262
+ }),
263
+ ),
264
+ }),
265
+ ]),
266
+ { description: "List of options for the user to choose from" },
267
+ ),
268
+ ),
269
+ questions: Type.Optional(
270
+ Type.Array(
271
+ Type.Object({
272
+ title: Type.String({ description: "Question title" }),
273
+ description: Type.Optional(Type.String({ description: "Question description" })),
274
+ options: Type.Optional(
275
+ Type.Array(
276
+ Type.Object({
277
+ title: Type.String({ description: "Option title" }),
278
+ description: Type.Optional(Type.String({ description: "Option description" })),
279
+ }),
280
+ { description: "Options for this question (omit for freeform text)" },
281
+ ),
282
+ ),
283
+ allowMultiple: Type.Optional(
284
+ Type.Boolean({ description: "Allow multiple selections for this question. Default: false" }),
285
+ ),
286
+ },
287
+ { description: "For questionnaire mode: structured questions with optional multiple-choice options per question" },
288
+ ),
289
+ ),
290
+ ),
291
+ allowMultiple: Type.Optional(
292
+ Type.Boolean({
293
+ description: "Allow selecting multiple options. Default: false",
294
+ }),
295
+ ),
296
+ allowFreeform: Type.Optional(
297
+ Type.Boolean({
298
+ description: "Add a freeform text option. Default: true",
299
+ }),
300
+ ),
301
+ allowComment: Type.Optional(
302
+ Type.Boolean({
303
+ description:
304
+ "Collect an optional comment after selecting one or more options. Default: false",
305
+ }),
306
+ ),
307
+ displayMode: Type.Optional(
308
+ StringEnum(["overlay", "inline"], {
309
+ description: "Legacy option; ignored by Glimpse (always opens a centered dialog)",
310
+ }),
311
+ ),
312
+ allowSkip: Type.Optional(
313
+ Type.Boolean({
314
+ description: "Allow submitting a questionnaire without answering all questions. Default: false",
315
+ }),
316
+ ),
317
+ followCursor: Type.Optional(
318
+ Type.Boolean({
319
+ description: "Make the dialog follow the terminal cursor. Default: false",
320
+ }),
321
+ ),
322
+ }),
323
+
324
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
325
+ return askUserHandler(params, signal, ctx);
326
+ },
327
+ });
328
+
329
+ export default function (pi: ExtensionAPI) {
330
+ pi.registerTool(askUserTool);
331
+
332
+ // ── Auto-detect question sessions and force ask_user usage ──
333
+ pi.on("before_agent_start", async (event, ctx) => {
334
+ const hasAskUser = event.systemPromptOptions.selectedTools?.includes("ask_user");
335
+ if (!hasAskUser) return;
336
+
337
+ const styleMode = getStyleMode(ctx.sessionManager.getEntries());
338
+ const shouldInject =
339
+ styleMode === true ||
340
+ (styleMode === null && isQuestionSession(event.systemPrompt, event.systemPromptOptions));
341
+
342
+ if (shouldInject) {
343
+ return { systemPrompt: event.systemPrompt + ASK_USER_MANDATE };
344
+ }
345
+ });
346
+
347
+ // ── Manual style toggle for ask_user behavior ──
348
+ pi.registerCommand("ask-style", {
349
+ description: "Cycle ask_user style: Auto → Always Dialog → Plain Text → Auto",
350
+ handler: async (_args, ctx) => {
351
+ const styleMode = getStyleMode(ctx.sessionManager.getEntries());
352
+
353
+ let nextMode: boolean | null;
354
+ let label: string;
355
+
356
+ if (styleMode === null) {
357
+ nextMode = true;
358
+ label = "Always Dialog (auto-detection overridden)";
359
+ } else if (styleMode === true) {
360
+ nextMode = false;
361
+ label = "Plain Text (no dialog injection)";
362
+ } else {
363
+ nextMode = null;
364
+ label = "Auto (skill + pattern detection)";
365
+ }
366
+
367
+ pi.appendEntry("ask-user-style", { enabled: nextMode });
368
+ ctx.ui.notify(`ask_user style: ${label}`, "info");
369
+ },
370
+ });
371
+
372
+ pi.registerCommand("ask-last", {
373
+ description: "Extract questions from the last assistant message and ask them via ask_user",
374
+ handler: async (_args, ctx) => {
375
+ if (!ctx.hasUI) {
376
+ console.warn("[pi-ask-user-glimpse] ask-last requires interactive mode");
377
+ return;
378
+ }
379
+
380
+ const entries = ctx.sessionManager.getEntries();
381
+ const lastAssistant = [...entries]
382
+ .reverse()
383
+ .find((e) => e.type === "message" && (e as any).message?.role === "assistant");
384
+
385
+ if (!lastAssistant) {
386
+ ctx.ui.notify("No assistant messages found in this session", "warning");
387
+ return;
388
+ }
389
+
390
+ const fullText = extractTextFromAssistantEntry(lastAssistant);
391
+ if (!fullText.trim()) {
392
+ ctx.ui.notify("Last assistant message has no text content", "warning");
393
+ return;
394
+ }
395
+
396
+ const questions = extractQuestions(fullText);
397
+
398
+ const result = await askUserHandler(buildAskLastParams(questions, fullText), undefined, ctx);
399
+
400
+ if ((result.details as any)?.cancelled) {
401
+ ctx.ui.notify("Cancelled — no answer sent", "info");
402
+ return;
403
+ }
404
+
405
+ const textContent = result.content[0];
406
+ const answer = textContent?.type === "text" ? textContent.text : "";
407
+ if (!answer) return;
408
+
409
+ let prefix: string;
410
+ if (questions.length === 0) {
411
+ prefix = "Responding to your last message:";
412
+ } else {
413
+ const plural = questions.length > 1 ? "s" : "";
414
+ prefix = `Answering the question${plural} from your last message:`;
415
+ }
416
+ pi.sendUserMessage(`${prefix}\n\n${answer}`);
417
+ },
418
+ });
419
+
420
+ pi.registerCommand("ask-debug", {
421
+ description: "Open a debug prompt to test each ask_user dialog type",
422
+ handler: async (_args, ctx) => {
423
+ if (!ctx.hasUI) {
424
+ console.warn("[pi-ask-user-glimpse] ask-debug requires interactive mode");
425
+ return;
426
+ }
427
+
428
+ const mode = await ctx.ui.select(
429
+ "Choose a prompt type to test:",
430
+ ["single-select", "multi-select", "freeform", "questionnaire"],
431
+ );
432
+ if (!mode) return;
433
+
434
+ const params = buildDebugParams(mode);
435
+ if (!params) return;
436
+
437
+ const result = await askUserHandler(params, undefined, ctx);
438
+ const textContent = result.content[0];
439
+ const text = textContent.type === "text" ? textContent.text : "No response";
440
+ ctx.ui.notify(`Result: ${text}`, "info");
441
+ },
442
+ });
443
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@alexleekt/pi-ask-user-glimpse",
3
+ "version": "0.2.1",
4
+ "description": "Pi extension that replaces ask_user with rich native WebView dialogs via glimpseui and shadcn/ui",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "glimpseui",
10
+ "ask_user",
11
+ "interactive"
12
+ ],
13
+ "author": "Alex Lee <alex@alexleekt.com>",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/alexleekt/pi-ask-user-glimpse.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/alexleekt/pi-ask-user-glimpse/issues"
21
+ },
22
+ "homepage": "https://github.com/alexleekt/pi-ask-user-glimpse#readme",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "pi": {
27
+ "extensions": [
28
+ "./index.ts"
29
+ ]
30
+ },
31
+ "files": [
32
+ "index.ts",
33
+ "tool",
34
+ "fallback",
35
+ "shared",
36
+ "types",
37
+ "dist",
38
+ "README.md",
39
+ "CHANGELOG.md",
40
+ "CONTRIBUTING.md",
41
+ "LICENSE"
42
+ ],
43
+ "scripts": {
44
+ "build": "npm run build:css && npm run build:webview",
45
+ "build:css": "cd webview && npx tailwindcss -i ./src/index.css -o ./src/index.generated.css --content './index.html,./src/**/*.{js,ts,jsx,tsx}'",
46
+ "build:webview": "cd webview && npx vite build --config ./vite.config.ts",
47
+ "check": "npm pack --dry-run",
48
+ "prepack": "npm run build",
49
+ "dev:webview": "cd webview && npx vite --config ./vite.config.ts",
50
+ "validate": "npx tsx scripts/validate.ts",
51
+ "validate:gui": "npx tsx scripts/validate.ts --gui"
52
+ },
53
+ "dependencies": {
54
+ "glimpseui": "^0.8.1"
55
+ },
56
+ "devDependencies": {
57
+ "@types/react": "^18.3.0",
58
+ "@types/react-dom": "^18.3.0",
59
+ "@vitejs/plugin-react": "^4.3.0",
60
+ "autoprefixer": "^10.4.20",
61
+ "marked": "^15.0.12",
62
+ "postcss": "^8.4.47",
63
+ "react": "^18.3.1",
64
+ "react-dom": "^18.3.1",
65
+ "tailwindcss": "^3.4.14",
66
+ "typescript": "^5.6.0",
67
+ "vite": "^5.4.0",
68
+ "vite-plugin-singlefile": "^2.0.0"
69
+ },
70
+ "peerDependencies": {
71
+ "@earendil-works/pi-coding-agent": "*",
72
+ "@earendil-works/pi-ai": "*"
73
+ },
74
+ "engines": {
75
+ "node": ">=18.0.0"
76
+ }
77
+ }