@alexleekt/pi-ask-user-glimpse 0.3.2 → 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 +85 -0
- package/CONTRIBUTING.md +15 -6
- package/README.md +75 -46
- package/constants/abbreviations.ts +6 -0
- package/constants/stopwords.ts +42 -0
- package/dist/index.html +950 -391
- package/index.ts +374 -211
- package/package.json +60 -9
- package/shared/ask-user.ts +11 -0
- package/tool/ask-user.ts +76 -306
- package/tool/response-formatter.ts +3 -2
- package/fallback/terminal-prompt.ts +0 -191
package/index.ts
CHANGED
|
@@ -3,117 +3,175 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
type CustomJournalEntry,
|
|
7
6
|
isCustomEntry,
|
|
7
|
+
type CustomJournalEntry,
|
|
8
8
|
} from "@alexleekt/pi-shared/types";
|
|
9
|
+
|
|
9
10
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
10
11
|
import type {
|
|
11
|
-
BuildSystemPromptOptions,
|
|
12
12
|
ExtensionAPI,
|
|
13
|
+
ExtensionContext,
|
|
13
14
|
} from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const ASK_USER_MANDATE = `
|
|
37
|
-
## Tool Usage Mandate — Auto-injected by pi-ask-user-glimpse
|
|
38
|
-
|
|
39
|
-
When you need to ask the user a question, you MUST use the \`ask_user\`
|
|
40
|
-
tool. Do NOT write questions as free-form assistant text. Each question
|
|
41
|
-
should be a separate \`ask_user\` tool call.
|
|
16
|
+
import { PROTECTED_ABBREVIATIONS } from "./constants/abbreviations.js";
|
|
17
|
+
import type { AnimationLevel, ThemeMode } from "./shared/ask-user.js";
|
|
18
|
+
import {
|
|
19
|
+
type AskUserMetadata,
|
|
20
|
+
type AskUserParams,
|
|
21
|
+
askUserHandler,
|
|
22
|
+
} from "./tool/ask-user.js";
|
|
23
|
+
|
|
24
|
+
/* ── Module-level reference to ExtensionAPI for tool execute closure ── */
|
|
25
|
+
let _pi: ExtensionAPI | undefined;
|
|
26
|
+
|
|
27
|
+
/* ── Style constants ── */
|
|
28
|
+
|
|
29
|
+
const YOLO_MANDATE = `
|
|
30
|
+
## Tool Usage Mandate — Auto-injected by pi-ask-user-glimpse (YOLO Style)
|
|
31
|
+
|
|
32
|
+
You are in YOLO style. Do NOT ask the user for input or confirmation.
|
|
33
|
+
Go with your best recommendation and proceed immediately.
|
|
34
|
+
|
|
35
|
+
Only use \`ask_user\` if the action would cause irreversible harm,
|
|
36
|
+
data loss, security compromise, or violate explicit hard constraints.
|
|
42
37
|
|
|
43
38
|
Rules:
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
- Set \`allowFreeform: true\` when the user might want to answer in their own words.
|
|
48
|
-
- Wait for the tool result before continuing to the next question.
|
|
39
|
+
- Do NOT use \`ask_user\` for routine decisions or clarifications.
|
|
40
|
+
- Make the call and keep moving.
|
|
41
|
+
- If you must use \`ask_user\`, list options from most recommended to least recommended.
|
|
49
42
|
`;
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
systemPrompt: string,
|
|
53
|
-
options: BuildSystemPromptOptions,
|
|
54
|
-
): boolean {
|
|
55
|
-
const hasQuestionSkill = !!options.skills?.some((s) =>
|
|
56
|
-
QUESTION_SKILL_NAMES.has(s.name.toLowerCase()),
|
|
57
|
-
);
|
|
58
|
-
const hasQuestionLanguage = QUESTION_SESSION_PATTERNS.some((p) =>
|
|
59
|
-
p.test(systemPrompt),
|
|
60
|
-
);
|
|
61
|
-
return hasQuestionSkill || hasQuestionLanguage;
|
|
62
|
-
}
|
|
44
|
+
type StyleMode = "plain" | "yolo";
|
|
63
45
|
|
|
64
|
-
function
|
|
46
|
+
function findCustomData(
|
|
47
|
+
entries: unknown[],
|
|
48
|
+
customType: string,
|
|
49
|
+
): Record<string, unknown> | undefined {
|
|
65
50
|
const entry = entries.find(
|
|
66
51
|
(e): e is CustomJournalEntry =>
|
|
67
|
-
isCustomEntry(e) && e.customType ===
|
|
52
|
+
isCustomEntry(e) && e.customType === customType,
|
|
68
53
|
);
|
|
69
|
-
|
|
70
|
-
return typeof enabled === "boolean" ? enabled : null;
|
|
54
|
+
return entry?.data as Record<string, unknown> | undefined;
|
|
71
55
|
}
|
|
72
56
|
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
const
|
|
57
|
+
function getStyleMode(entries: unknown[]): StyleMode {
|
|
58
|
+
const data = findCustomData(entries, "ask-user-style");
|
|
59
|
+
const mode = data?.mode;
|
|
60
|
+
if (mode === "plain" || mode === "yolo") {
|
|
61
|
+
return mode;
|
|
62
|
+
}
|
|
63
|
+
return "plain";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getThemeSettings(entries: unknown[]): {
|
|
67
|
+
theme?: ThemeMode;
|
|
68
|
+
animationLevel?: AnimationLevel;
|
|
69
|
+
} {
|
|
70
|
+
const data = findCustomData(entries, "ask-user-theme");
|
|
71
|
+
const theme = typeof data?.theme === "string" ? data.theme : undefined;
|
|
72
|
+
const animationLevel =
|
|
73
|
+
typeof data?.animationLevel === "string"
|
|
74
|
+
? data.animationLevel
|
|
75
|
+
: undefined;
|
|
76
|
+
return {
|
|
77
|
+
theme:
|
|
78
|
+
theme === "light" || theme === "dark" || theme === "system"
|
|
79
|
+
? theme
|
|
80
|
+
: undefined,
|
|
81
|
+
animationLevel:
|
|
82
|
+
animationLevel === "none" ||
|
|
83
|
+
animationLevel === "minimal" ||
|
|
84
|
+
animationLevel === "all"
|
|
85
|
+
? animationLevel
|
|
86
|
+
: undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Extract text blocks from a content array (journal entry). */
|
|
91
|
+
function extractTextFromContent(content: unknown): string {
|
|
76
92
|
if (typeof content === "string") return content;
|
|
77
93
|
if (!Array.isArray(content)) return "";
|
|
78
94
|
return content
|
|
79
95
|
.filter(
|
|
80
96
|
(c): c is { type: string; text: string } =>
|
|
81
|
-
typeof
|
|
82
|
-
|
|
97
|
+
typeof c === "object" &&
|
|
98
|
+
c !== null &&
|
|
99
|
+
typeof (c as Record<string, unknown>).type === "string" &&
|
|
100
|
+
typeof (c as Record<string, unknown>).text === "string",
|
|
83
101
|
)
|
|
84
102
|
.map((c) => c.text)
|
|
85
103
|
.join("\n");
|
|
86
104
|
}
|
|
87
105
|
|
|
88
|
-
/* ──
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
/* ── Shared helpers for consistent ask_user UX across all entry points ── */
|
|
107
|
+
|
|
108
|
+
/** Enrich raw ask_user params with persisted theme/animation settings. */
|
|
109
|
+
function enrichWithThemeSettings(
|
|
110
|
+
params: AskUserParams,
|
|
111
|
+
entries: unknown[],
|
|
112
|
+
): AskUserParams {
|
|
113
|
+
const { theme, animationLevel } = getThemeSettings(entries);
|
|
114
|
+
return { ...params, theme, animationLevel };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Persist theme/animation changes back to the session journal. */
|
|
118
|
+
function saveThemeMetadata(metadata: AskUserMetadata) {
|
|
119
|
+
if ((metadata.theme || metadata.animationLevel) && _pi) {
|
|
120
|
+
_pi.appendEntry("ask-user-theme", {
|
|
121
|
+
theme: metadata.theme,
|
|
122
|
+
animationLevel: metadata.animationLevel,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Strip XML-style `<thinking>` blocks and markdown reasoning blocks from text. */
|
|
128
|
+
function stripThinkingBlocks(text: string): string {
|
|
129
|
+
return text
|
|
130
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/g, "")
|
|
131
|
+
.replace(/```\s*thinking\n[\s\S]*?```/g, "")
|
|
132
|
+
.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Execute ask_user with full enrichment + persistence, used by tool and commands alike. */
|
|
136
|
+
async function runAskUserWithTheme(
|
|
137
|
+
rawParams: AskUserParams,
|
|
138
|
+
signal: AbortSignal | undefined,
|
|
139
|
+
ctx: ExtensionContext,
|
|
140
|
+
): Promise<ReturnType<typeof askUserHandler>> {
|
|
141
|
+
const entries = ctx.sessionManager.getEntries();
|
|
142
|
+
const params = enrichWithThemeSettings(rawParams, entries);
|
|
143
|
+
let metadata: AskUserMetadata = {};
|
|
144
|
+
|
|
145
|
+
// Strip reasoning chains from explicitly-passed context
|
|
146
|
+
const cleanedParams: AskUserParams = params.context
|
|
147
|
+
? { ...params, context: stripThinkingBlocks(params.context) }
|
|
148
|
+
: params;
|
|
149
|
+
|
|
150
|
+
const result = await askUserHandler(cleanedParams, signal, ctx, (m) => {
|
|
151
|
+
metadata = m;
|
|
152
|
+
});
|
|
153
|
+
saveThemeMetadata(metadata);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Extract plain text from a Pi journal assistant entry. */
|
|
158
|
+
function extractTextFromAssistantEntry(entry: unknown): string {
|
|
159
|
+
if (!entry || typeof entry !== "object") return "";
|
|
160
|
+
const content = ((entry as Record<string, unknown>).message as Record<string, unknown> | undefined)?.content;
|
|
161
|
+
return extractTextFromContent(content);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Find the most recent assistant entry in the session journal. */
|
|
165
|
+
function findLastAssistantEntry(entries: unknown[]): unknown | undefined {
|
|
166
|
+
return [...entries].reverse().find((e) => {
|
|
167
|
+
if (!e || typeof e !== "object") return false;
|
|
168
|
+
const msg = (e as Record<string, unknown>).message;
|
|
169
|
+
if (!msg || typeof msg !== "object") return false;
|
|
170
|
+
return (msg as Record<string, unknown>).role === "assistant";
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ── /ask: extract explicit questions only ── */
|
|
117
175
|
|
|
118
176
|
function splitSentences(text: string): string[] {
|
|
119
177
|
const PLACEHOLDER = "\x00";
|
|
@@ -142,19 +200,6 @@ function splitSentences(text: string): string[] {
|
|
|
142
200
|
.filter((s) => s.length > 0);
|
|
143
201
|
}
|
|
144
202
|
|
|
145
|
-
const IMPLICIT_REQUEST_PATTERNS = [
|
|
146
|
-
/\b(let me know|let us know)\b/i,
|
|
147
|
-
/\b(tell me|tell us)\b/i,
|
|
148
|
-
/\b(share your|share any)\b/i,
|
|
149
|
-
/\b(what do you think|what are your thoughts)\b/i,
|
|
150
|
-
/\b(which\b.*\b(would you|do you|should we)\b)/i,
|
|
151
|
-
/\b(should we|can you confirm|please confirm|could you confirm)\b/i,
|
|
152
|
-
/\b(i need your (input|feedback|thoughts|opinion))\b/i,
|
|
153
|
-
/\b(your (thoughts|opinion|preference|feedback))\b/i,
|
|
154
|
-
/\b(please provide|could you provide|can you provide)\b/i,
|
|
155
|
-
/\b(would you like|do you want|do you prefer)\b/i,
|
|
156
|
-
];
|
|
157
|
-
|
|
158
203
|
function hasQuotedQuestion(sentence: string): boolean {
|
|
159
204
|
return /["'`].*\?.*["'`]/.test(sentence) && !sentence.endsWith("?");
|
|
160
205
|
}
|
|
@@ -163,25 +208,17 @@ function looksLikeTernary(sentence: string): boolean {
|
|
|
163
208
|
return /\?\s*[:;]/.test(sentence) || /=\s*\S+\s*\?/.test(sentence);
|
|
164
209
|
}
|
|
165
210
|
|
|
211
|
+
/** Extract only explicit questions (sentences ending in ?).
|
|
212
|
+
* Implicit requests like "let me know" are ignored — the freeform textarea
|
|
213
|
+
* already handles open-ended input without creating phantom questionnaire rows. */
|
|
166
214
|
function extractQuestions(text: string): string[] {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (sentence.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (sentence.length < 10) continue;
|
|
175
|
-
explicit.push(sentence);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (IMPLICIT_REQUEST_PATTERNS.some((p) => p.test(sentence))) {
|
|
180
|
-
implicit.push(sentence);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return [...explicit, ...implicit];
|
|
215
|
+
return splitSentences(text).filter((sentence) => {
|
|
216
|
+
if (!sentence.endsWith("?")) return false;
|
|
217
|
+
if (hasQuotedQuestion(sentence)) return false;
|
|
218
|
+
if (looksLikeTernary(sentence)) return false;
|
|
219
|
+
if (sentence.length < 3) return false;
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
function truncate(str: string, max: number): string {
|
|
@@ -192,23 +229,24 @@ function buildAskLastParams(
|
|
|
192
229
|
questions: string[],
|
|
193
230
|
fullText: string,
|
|
194
231
|
): AskUserParams {
|
|
232
|
+
const cleanContext = stripThinkingBlocks(fullText);
|
|
195
233
|
if (questions.length === 0) {
|
|
196
234
|
return {
|
|
197
235
|
question: "The assistant would like your input on the following:",
|
|
198
|
-
context:
|
|
236
|
+
context: cleanContext,
|
|
199
237
|
allowFreeform: true,
|
|
200
238
|
};
|
|
201
239
|
}
|
|
202
240
|
if (questions.length === 1) {
|
|
203
241
|
return {
|
|
204
242
|
question: questions[0],
|
|
205
|
-
context:
|
|
243
|
+
context: cleanContext,
|
|
206
244
|
allowFreeform: true,
|
|
207
245
|
};
|
|
208
246
|
}
|
|
209
247
|
return {
|
|
210
248
|
question: "The assistant asked multiple questions",
|
|
211
|
-
context:
|
|
249
|
+
context: cleanContext,
|
|
212
250
|
questions: questions.map((q) => ({
|
|
213
251
|
title: truncate(q, 60),
|
|
214
252
|
description: q,
|
|
@@ -218,14 +256,23 @@ function buildAskLastParams(
|
|
|
218
256
|
};
|
|
219
257
|
}
|
|
220
258
|
|
|
259
|
+
/** Build a user-facing prefix for an auto-caught or manual /ask answer. */
|
|
260
|
+
function answerPrefix(questionCount: number): string {
|
|
261
|
+
if (questionCount === 0) {
|
|
262
|
+
return "Responding to your last message:";
|
|
263
|
+
}
|
|
264
|
+
const plural = questionCount > 1 ? "s" : "";
|
|
265
|
+
return `Answering the question${plural} from your last message:`;
|
|
266
|
+
}
|
|
267
|
+
|
|
221
268
|
function buildDebugParams(mode: string): AskUserParams | null {
|
|
222
269
|
switch (mode) {
|
|
223
270
|
case "single-select":
|
|
224
271
|
return {
|
|
225
272
|
question: "Test: Single Select",
|
|
226
|
-
context: "Pick one option (with optional freeform and comment)",
|
|
273
|
+
context: "Pick one option (with optional freeform and comment). This tests radio-style selection, recommended badges, search filtering, and keyboard navigation.",
|
|
227
274
|
options: [
|
|
228
|
-
{ title: "Option A", description: "Description for A" },
|
|
275
|
+
{ title: "Option A", description: "Description for A", recommended: true },
|
|
229
276
|
{ title: "Option B", description: "Description for B" },
|
|
230
277
|
{ title: "Option C", description: "Description for C" },
|
|
231
278
|
],
|
|
@@ -236,9 +283,9 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
236
283
|
return {
|
|
237
284
|
question: "Test: Multi Select",
|
|
238
285
|
context:
|
|
239
|
-
"Pick multiple options (with optional freeform and comment)",
|
|
286
|
+
"Pick multiple options (with optional freeform and comment). This tests checkbox-style selection, select-all/none links, and submit-gating.",
|
|
240
287
|
options: [
|
|
241
|
-
{ title: "Feature X", description: "Enable feature X" },
|
|
288
|
+
{ title: "Feature X", description: "Enable feature X", recommended: true },
|
|
242
289
|
{ title: "Feature Y", description: "Enable feature Y" },
|
|
243
290
|
{ title: "Feature Z", description: "Enable feature Z" },
|
|
244
291
|
],
|
|
@@ -249,13 +296,13 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
249
296
|
case "freeform":
|
|
250
297
|
return {
|
|
251
298
|
question: "Test: Freeform",
|
|
252
|
-
context: "Type any answer you like",
|
|
299
|
+
context: "Type any answer you like. This tests the textarea, character counter, and platform-aware keyboard shortcuts.",
|
|
253
300
|
allowFreeform: true,
|
|
254
301
|
};
|
|
255
302
|
case "questionnaire":
|
|
256
303
|
return {
|
|
257
304
|
question: "Test: Questionnaire",
|
|
258
|
-
context: "Answer multiple structured questions",
|
|
305
|
+
context: "Answer multiple structured questions. This tests the card layout, progress bar, required-field badges, and per-question character counters.",
|
|
259
306
|
questions: [
|
|
260
307
|
{
|
|
261
308
|
title: "Database",
|
|
@@ -264,6 +311,7 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
264
311
|
{
|
|
265
312
|
title: "PostgreSQL",
|
|
266
313
|
description: "Relational, proven",
|
|
314
|
+
recommended: true,
|
|
267
315
|
},
|
|
268
316
|
{ title: "SQLite", description: "Zero-config" },
|
|
269
317
|
],
|
|
@@ -272,7 +320,7 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
272
320
|
title: "Architecture",
|
|
273
321
|
description: "Preferred style?",
|
|
274
322
|
options: [
|
|
275
|
-
{ title: "Monolith", description: "Simple" },
|
|
323
|
+
{ title: "Monolith", description: "Simple", recommended: true },
|
|
276
324
|
{ title: "Microservices", description: "Scalable" },
|
|
277
325
|
],
|
|
278
326
|
allowMultiple: true,
|
|
@@ -284,71 +332,165 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
284
332
|
],
|
|
285
333
|
allowComment: true,
|
|
286
334
|
};
|
|
287
|
-
case "
|
|
288
|
-
return {
|
|
289
|
-
question: "This is a very long question that exceeds one hundred and twenty characters so it should trigger the auto-split behavior. The first sentence becomes the dialog title, and the rest flows to the context panel.",
|
|
290
|
-
options: [
|
|
291
|
-
{ title: "Split worked", description: "Title is short, context has the rest" },
|
|
292
|
-
{ title: "Not split", description: "Everything is still in the title" },
|
|
293
|
-
],
|
|
294
|
-
allowComment: true,
|
|
295
|
-
};
|
|
296
|
-
case "mermaid":
|
|
335
|
+
case "kitchen-sink":
|
|
297
336
|
return {
|
|
298
|
-
question: "
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
337
|
+
question: "Kitchen Sink: Every Feature",
|
|
338
|
+
contextFormat: "html",
|
|
339
|
+
context: `<div style="font-family: ui-sans-serif, system-ui, sans-serif; overflow-wrap: break-word;">
|
|
340
|
+
<h2 style="color: hsl(var(--primary)); margin-bottom: 0.75rem; font-size: 1.25rem; font-weight: 600;">🧪 Debug Kitchen Sink</h2>
|
|
341
|
+
<p style="color: hsl(var(--muted-foreground)); margin-bottom: 1rem; line-height: 1.5;">
|
|
342
|
+
This dialog demonstrates every major feature — including the built-in <code style="background: hsl(var(--muted)); padding: 0.125rem 0.25rem; border-radius: 4px; font-size: 0.875em;">pi</code> charting helpers.
|
|
343
|
+
</p>
|
|
344
|
+
|
|
345
|
+
<div id="chart-bar" style="margin-bottom: 1rem;"></div>
|
|
346
|
+
<div id="chart-pie" style="margin-bottom: 1rem;"></div>
|
|
347
|
+
<div id="comparison-table" style="margin-bottom: 1rem;"></div>
|
|
348
|
+
<div id="pros-cons" style="margin-bottom: 1rem;"></div>
|
|
349
|
+
<div id="timeline" style="margin-bottom: 1rem;"></div>
|
|
350
|
+
<div id="metrics" style="margin-bottom: 1rem;"></div>
|
|
351
|
+
|
|
352
|
+
<script>
|
|
353
|
+
pi.barChart('#chart-bar', [
|
|
354
|
+
{label: 'Monolith', value: 95},
|
|
355
|
+
{label: 'Microservices', value: 70},
|
|
356
|
+
{label: 'Serverless', value: 55}
|
|
357
|
+
], {title: 'Deployment Velocity Score', highlightIndex: 0, showValues: true});
|
|
358
|
+
|
|
359
|
+
pi.pieChart('#chart-pie', [
|
|
360
|
+
{label: 'Auth', value: 30},
|
|
361
|
+
{label: 'Cache', value: 25},
|
|
362
|
+
{label: 'Rate Limit', value: 20},
|
|
363
|
+
{label: 'Observability', value: 25}
|
|
364
|
+
], {title: 'Feature Effort Distribution', donut: true, showLegend: true});
|
|
365
|
+
|
|
366
|
+
pi.table('#comparison-table',
|
|
367
|
+
['Feature', 'Monolith', 'Microservices', 'Serverless'],
|
|
368
|
+
[
|
|
369
|
+
['Complexity', 'Low', 'High', 'Medium'],
|
|
370
|
+
['Scalability', 'Vertical', 'Horizontal', 'Auto'],
|
|
371
|
+
['Cost', 'Fixed', 'Variable', 'Pay-per-use'],
|
|
372
|
+
['Team Size', 'Small', 'Large', 'Any']
|
|
373
|
+
],
|
|
374
|
+
{title: 'Architecture Comparison', highlightColumn: 1, striped: true, compact: true}
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
pi.prosCons('#pros-cons',
|
|
378
|
+
['Simple deployment', 'Single codebase', 'Easy debugging', 'Low infra cost'],
|
|
379
|
+
['Hard to scale', 'Tight coupling', 'Single point of failure', 'Slower CI/CD'],
|
|
380
|
+
{title: 'Monolith Trade-offs'}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
pi.timeline('#timeline', [
|
|
384
|
+
{date: 'Week 1', title: 'Scoping', status: 'complete'},
|
|
385
|
+
{date: 'Week 2', title: 'Design', status: 'complete'},
|
|
386
|
+
{date: 'Week 3', title: 'Build MVP', status: 'current'},
|
|
387
|
+
{date: 'Week 4', title: 'Launch', status: 'pending'}
|
|
388
|
+
], {title: 'Project Timeline'});
|
|
389
|
+
|
|
390
|
+
pi.metrics('#metrics', [
|
|
391
|
+
{label: 'Latency (p99)', value: '42ms', change: '-12%', trend: 'down'},
|
|
392
|
+
{label: 'Throughput', value: '12.4k rps', change: '+8%', trend: 'up'},
|
|
393
|
+
{label: 'Error Rate', value: '0.02%', change: '-0.01%', trend: 'down'},
|
|
394
|
+
{label: 'Uptime', value: '99.97%', change: '+0.02%', trend: 'up'}
|
|
395
|
+
], {title: 'System Metrics', columns: 2});
|
|
396
|
+
</script>
|
|
397
|
+
|
|
398
|
+
<p style="color: hsl(var(--muted-foreground)); font-size: 0.875rem; line-height: 1.5; margin-top: 1rem;">
|
|
399
|
+
Try keyboard shortcuts: <strong>1-9</strong> per question · <strong>0</strong> comments · <strong>Tab</strong> next · <strong>Esc</strong> cancel · <strong>⌘Enter</strong> submit · <strong>↑↓</strong> navigate · <strong>Space</strong> toggle · theme toggle (⚙️).
|
|
400
|
+
</p>
|
|
401
|
+
</div>`,
|
|
402
|
+
questions: [
|
|
403
|
+
{
|
|
404
|
+
title: "Architecture",
|
|
405
|
+
description: "Which architecture style should we use?",
|
|
406
|
+
options: [
|
|
407
|
+
{ title: "Monolith", description: "Simple, single deployable", recommended: true },
|
|
408
|
+
{ title: "Microservices", description: "Scalable, complex" },
|
|
409
|
+
{ title: "Serverless", description: "Event-driven, pay-per-use" }
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
title: "Features",
|
|
414
|
+
description: "Select all features to implement:",
|
|
415
|
+
options: [
|
|
416
|
+
{ title: "Authentication", description: "OAuth + JWT" },
|
|
417
|
+
{ title: "Caching", description: "Redis layer" },
|
|
418
|
+
{ title: "Rate Limiting", description: "Token bucket" },
|
|
419
|
+
{ title: "Observability", description: "Metrics + logs" }
|
|
420
|
+
],
|
|
421
|
+
allowMultiple: true
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
title: "Deployment Target",
|
|
425
|
+
description: "Where should we deploy?",
|
|
426
|
+
options: [
|
|
427
|
+
{ title: "Vercel", description: "Edge, serverless-first" },
|
|
428
|
+
{ title: "AWS", description: "Full control, scalable" },
|
|
429
|
+
{ title: "Self-hosted", description: "Own the infrastructure" }
|
|
430
|
+
]
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
title: "Notes",
|
|
434
|
+
description: "Any additional requirements or constraints?"
|
|
435
|
+
}
|
|
323
436
|
],
|
|
324
437
|
allowComment: true,
|
|
438
|
+
allowSkip: true
|
|
325
439
|
};
|
|
326
440
|
default:
|
|
327
441
|
return null;
|
|
328
442
|
}
|
|
329
443
|
}
|
|
330
444
|
|
|
445
|
+
const TOOL_DESCRIPTION = [
|
|
446
|
+
"Ask the user a question with optional multiple-choice answers.",
|
|
447
|
+
"Use this to gather information interactively. Ask exactly one focused question per call.",
|
|
448
|
+
"Before calling, gather context with tools (read/web/ref) and pass a short summary via the context field.",
|
|
449
|
+
"The context panel supports Mermaid diagrams (flowcharts, sequence diagrams, etc.).",
|
|
450
|
+
"For richer visualizations, use contextFormat: 'html' with the built-in pi charting helpers:",
|
|
451
|
+
" pi.table(['Feature','A','B'], [['Auth','OAuth','SAML']], {highlightColumn:1}) — comparison tables;",
|
|
452
|
+
" pi.barChart('#chart', [{label:'A',value:30},{label:'B',value:80}], {highlightIndex:1}) — bar charts;",
|
|
453
|
+
" pi.prosCons('#pc', ['Fast','Simple'], ['Expensive','Locked'], {}) — trade-offs;",
|
|
454
|
+
" pi.metrics('#m', [{label:'Uptime',value:'99.9%',change:'+0.1%',trend:'up'}]) — KPI cards;",
|
|
455
|
+
" pi.pieChart('#pie', [{label:'X',value:30},{label:'Y',value:70}], {donut:true}) — distributions;",
|
|
456
|
+
" pi.timeline('#t', [{date:'Q1',title:'Plan',status:'complete'},{date:'Q2',title:'Build',status:'current'}]) — roadmaps.",
|
|
457
|
+
"All helpers auto-theme to light/dark mode.",
|
|
458
|
+
].join(" ");
|
|
459
|
+
|
|
331
460
|
const askUserTool = defineTool({
|
|
332
461
|
name: "ask_user",
|
|
333
462
|
label: "Ask User",
|
|
334
|
-
description:
|
|
335
|
-
"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. The context panel supports Mermaid diagrams (flowcharts, sequence diagrams, etc.) — include them when visualizing architecture, flows, or relationships would aid understanding.",
|
|
463
|
+
description: TOOL_DESCRIPTION,
|
|
336
464
|
promptSnippet:
|
|
337
465
|
"Ask the user one focused question with optional multiple-choice answers to gather information interactively",
|
|
338
466
|
promptGuidelines: [
|
|
339
467
|
"Always use ask_user instead of guessing when user input would improve the answer.",
|
|
340
468
|
"Keep the question field short and focused (ideally one sentence). Put background, examples, or elaboration in the context field.",
|
|
341
469
|
"Include Mermaid diagrams in the context field when visualizing architecture, data flows, or decision trees would help the user understand the question.",
|
|
470
|
+
"Use contextFormat: 'html' for rich visualizations (comparison tables, bar charts, pros/cons lists, metric cards, timelines, and layouts) that help the user understand trade-offs and make faster decisions. The iframe inherits the wrapper's CSS variables for automatic theme consistency.",
|
|
471
|
+
"When comparing 3+ options, render a comparison table with pi.table(headers, rows, {highlightColumn: recommendedIndex}).",
|
|
472
|
+
"When showing quantitative data or performance metrics, use pi.barChart() or pi.metrics() to visualize the numbers.",
|
|
473
|
+
"When weighing trade-offs, use pi.prosCons() to show a side-by-side comparison.",
|
|
342
474
|
"Pass a concise question and, when applicable, a list of options with short titles and optional longer descriptions.",
|
|
475
|
+
"List options from most recommended to least recommended.",
|
|
343
476
|
"Set allowMultiple: true when more than one choice is valid.",
|
|
344
477
|
"Set allowFreeform: true (default) when the user might want to answer in their own words.",
|
|
345
478
|
],
|
|
346
479
|
parameters: Type.Object({
|
|
347
|
-
question: Type.String({
|
|
480
|
+
question: Type.String({
|
|
481
|
+
description:
|
|
482
|
+
"A short, focused question (ideally one sentence). Put background detail in context.",
|
|
483
|
+
}),
|
|
348
484
|
context: Type.Optional(
|
|
349
485
|
Type.String({
|
|
350
486
|
description:
|
|
351
|
-
"Background, examples, or elaboration that helps the user understand the question. Shown in a side panel, so keep the question itself concise. Supports Mermaid diagrams (flowcharts, sequence diagrams, etc.) — wrap them in ```mermaid code blocks.",
|
|
487
|
+
"Background, examples, or elaboration that helps the user understand the question. Shown in a side panel, so keep the question itself concise. Supports Mermaid diagrams (flowcharts, sequence diagrams, etc.) — wrap them in ```mermaid code blocks. Use contextFormat: 'html' for rich visualizations with the built-in pi helpers (pi.table, pi.barChart, pi.prosCons, pi.metrics, pi.pieChart, pi.timeline).",
|
|
488
|
+
}),
|
|
489
|
+
),
|
|
490
|
+
contextFormat: Type.Optional(
|
|
491
|
+
StringEnum(["markdown", "html"], {
|
|
492
|
+
description:
|
|
493
|
+
"Format of the context field. 'markdown' (default) renders as formatted text. 'html' renders in a sandboxed iframe with automatic light/dark theme consistency. Use HTML context for comparison tables, bar charts, pros/cons lists, metric cards, timelines, and interactive layouts that help the user understand trade-offs and decide faster.",
|
|
352
494
|
}),
|
|
353
495
|
),
|
|
354
496
|
options: Type.Optional(
|
|
@@ -365,6 +507,12 @@ const askUserTool = defineTool({
|
|
|
365
507
|
"Longer description explaining this option",
|
|
366
508
|
}),
|
|
367
509
|
),
|
|
510
|
+
recommended: Type.Optional(
|
|
511
|
+
Type.Boolean({
|
|
512
|
+
description:
|
|
513
|
+
"Mark this option as most recommended. Shows a badge in the dialog.",
|
|
514
|
+
}),
|
|
515
|
+
),
|
|
368
516
|
}),
|
|
369
517
|
]),
|
|
370
518
|
{ description: "List of options for the user to choose from" },
|
|
@@ -391,6 +539,12 @@ const askUserTool = defineTool({
|
|
|
391
539
|
description: "Option description",
|
|
392
540
|
}),
|
|
393
541
|
),
|
|
542
|
+
recommended: Type.Optional(
|
|
543
|
+
Type.Boolean({
|
|
544
|
+
description:
|
|
545
|
+
"Mark this option as most recommended. Shows a badge in the dialog.",
|
|
546
|
+
}),
|
|
547
|
+
),
|
|
394
548
|
}),
|
|
395
549
|
{
|
|
396
550
|
description:
|
|
@@ -449,79 +603,86 @@ const askUserTool = defineTool({
|
|
|
449
603
|
}),
|
|
450
604
|
|
|
451
605
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
452
|
-
return
|
|
606
|
+
return runAskUserWithTheme(params as AskUserParams, signal, ctx);
|
|
453
607
|
},
|
|
454
608
|
});
|
|
455
609
|
|
|
456
610
|
export default function (pi: ExtensionAPI) {
|
|
611
|
+
_pi = pi;
|
|
457
612
|
pi.registerTool(askUserTool);
|
|
458
613
|
|
|
459
|
-
|
|
614
|
+
/** Send a user answer back into the journal with consistent error handling. */
|
|
615
|
+
async function deliverAnswer(
|
|
616
|
+
prefix: string,
|
|
617
|
+
answer: string,
|
|
618
|
+
ctx: ExtensionContext,
|
|
619
|
+
) {
|
|
620
|
+
try {
|
|
621
|
+
await pi.sendUserMessage(`${prefix}\n\n${answer}`, {
|
|
622
|
+
deliverAs: "steer",
|
|
623
|
+
});
|
|
624
|
+
} catch (err) {
|
|
625
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
626
|
+
console.error(
|
|
627
|
+
`[pi-ask-user-glimpse] sendUserMessage failed: ${msg}`,
|
|
628
|
+
);
|
|
629
|
+
ctx.ui?.notify(`Failed to send answer: ${msg}`, "error");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── Inject mandate based on ask style ──
|
|
460
634
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
461
635
|
const hasAskUser =
|
|
462
636
|
event.systemPromptOptions.selectedTools?.includes("ask_user");
|
|
463
637
|
if (!hasAskUser) return;
|
|
464
638
|
|
|
639
|
+
// Don't force ask_user in headless environments — the tool can't render dialogs
|
|
640
|
+
if (!ctx.hasUI) return;
|
|
641
|
+
|
|
465
642
|
const styleMode = getStyleMode(ctx.sessionManager.getEntries());
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
isQuestionSession(
|
|
470
|
-
event.systemPrompt,
|
|
471
|
-
event.systemPromptOptions,
|
|
472
|
-
));
|
|
473
|
-
|
|
474
|
-
if (shouldInject) {
|
|
475
|
-
return { systemPrompt: event.systemPrompt + ASK_USER_MANDATE };
|
|
643
|
+
|
|
644
|
+
if (styleMode === "yolo") {
|
|
645
|
+
return { systemPrompt: event.systemPrompt + YOLO_MANDATE };
|
|
476
646
|
}
|
|
647
|
+
// "plain" → no injection
|
|
477
648
|
});
|
|
478
649
|
|
|
479
|
-
// ── Manual style toggle
|
|
650
|
+
// ── Manual style toggle ──
|
|
480
651
|
pi.registerCommand("ask-style", {
|
|
481
652
|
description:
|
|
482
|
-
"Cycle ask_user style:
|
|
653
|
+
"Cycle ask_user style: Plain Text → YOLO → Plain Text",
|
|
483
654
|
handler: async (_args, ctx) => {
|
|
484
655
|
const styleMode = getStyleMode(ctx.sessionManager.getEntries());
|
|
485
656
|
|
|
486
|
-
let nextMode:
|
|
657
|
+
let nextMode: StyleMode;
|
|
487
658
|
let label: string;
|
|
488
659
|
|
|
489
|
-
if (styleMode ===
|
|
490
|
-
nextMode =
|
|
491
|
-
label = "
|
|
492
|
-
} else if (styleMode === true) {
|
|
493
|
-
nextMode = false;
|
|
494
|
-
label = "Plain Text (no dialog injection)";
|
|
660
|
+
if (styleMode === "plain") {
|
|
661
|
+
nextMode = "yolo";
|
|
662
|
+
label = "YOLO — go with your recommendation";
|
|
495
663
|
} else {
|
|
496
|
-
nextMode =
|
|
497
|
-
label = "
|
|
664
|
+
nextMode = "plain";
|
|
665
|
+
label = "Plain Text (no dialog injection)";
|
|
498
666
|
}
|
|
499
667
|
|
|
500
|
-
pi.appendEntry("ask-user-style", {
|
|
668
|
+
await pi.appendEntry("ask-user-style", { mode: nextMode });
|
|
501
669
|
ctx.ui.notify(`ask_user style: ${label}`, "info");
|
|
502
670
|
},
|
|
503
671
|
});
|
|
504
672
|
|
|
505
|
-
pi.registerCommand("ask
|
|
673
|
+
pi.registerCommand("ask", {
|
|
506
674
|
description:
|
|
507
675
|
"Extract questions from the last assistant message and ask them via ask_user",
|
|
508
676
|
handler: async (_args, ctx) => {
|
|
509
677
|
if (!ctx.hasUI) {
|
|
510
678
|
console.warn(
|
|
511
|
-
"[pi-ask-user-glimpse] ask
|
|
679
|
+
"[pi-ask-user-glimpse] /ask requires interactive mode",
|
|
512
680
|
);
|
|
513
681
|
return;
|
|
514
682
|
}
|
|
515
683
|
|
|
516
684
|
const entries = ctx.sessionManager.getEntries();
|
|
517
|
-
const lastAssistant =
|
|
518
|
-
const msg = (e as unknown as Record<string, unknown>).message;
|
|
519
|
-
return (
|
|
520
|
-
typeof msg === "object" &&
|
|
521
|
-
msg !== null &&
|
|
522
|
-
(msg as Record<string, unknown>).role === "assistant"
|
|
523
|
-
);
|
|
524
|
-
});
|
|
685
|
+
const lastAssistant = findLastAssistantEntry(entries);
|
|
525
686
|
|
|
526
687
|
if (!lastAssistant) {
|
|
527
688
|
ctx.ui.notify(
|
|
@@ -542,7 +703,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
542
703
|
|
|
543
704
|
const questions = extractQuestions(fullText);
|
|
544
705
|
|
|
545
|
-
const result = await
|
|
706
|
+
const result = await runAskUserWithTheme(
|
|
546
707
|
buildAskLastParams(questions, fullText),
|
|
547
708
|
undefined,
|
|
548
709
|
ctx,
|
|
@@ -557,14 +718,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
557
718
|
const answer = textContent?.type === "text" ? textContent.text : "";
|
|
558
719
|
if (!answer) return;
|
|
559
720
|
|
|
560
|
-
|
|
561
|
-
if (questions.length === 0) {
|
|
562
|
-
prefix = "Responding to your last message:";
|
|
563
|
-
} else {
|
|
564
|
-
const plural = questions.length > 1 ? "s" : "";
|
|
565
|
-
prefix = `Answering the question${plural} from your last message:`;
|
|
566
|
-
}
|
|
567
|
-
pi.sendUserMessage(`${prefix}\n\n${answer}`);
|
|
721
|
+
await deliverAnswer(answerPrefix(questions.length), answer, ctx);
|
|
568
722
|
},
|
|
569
723
|
});
|
|
570
724
|
|
|
@@ -583,19 +737,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
583
737
|
"multi-select",
|
|
584
738
|
"freeform",
|
|
585
739
|
"questionnaire",
|
|
586
|
-
"
|
|
587
|
-
"mermaid",
|
|
740
|
+
"kitchen-sink",
|
|
588
741
|
]);
|
|
589
742
|
if (!mode) return;
|
|
590
743
|
|
|
591
744
|
const params = buildDebugParams(mode);
|
|
592
745
|
if (!params) return;
|
|
593
746
|
|
|
594
|
-
const result = await
|
|
747
|
+
const result = await runAskUserWithTheme(params, undefined, ctx);
|
|
595
748
|
const textContent = result.content[0];
|
|
596
749
|
const text =
|
|
597
750
|
textContent.type === "text" ? textContent.text : "No response";
|
|
598
|
-
|
|
751
|
+
|
|
752
|
+
// Render debug result in the conversation thread without triggering AI processing
|
|
753
|
+
_pi?.sendMessage(
|
|
754
|
+
{
|
|
755
|
+
customType: "ask-debug-result",
|
|
756
|
+
content: [{ type: "text", text: `[debug] ${mode} → ${text}` }],
|
|
757
|
+
display: true,
|
|
758
|
+
},
|
|
759
|
+
{ triggerTurn: false },
|
|
760
|
+
);
|
|
599
761
|
},
|
|
600
762
|
});
|
|
763
|
+
|
|
601
764
|
}
|