@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/index.ts
CHANGED
|
@@ -3,108 +3,106 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
isCustomEntry,
|
|
6
7
|
type CustomJournalEntry,
|
|
7
|
-
isCustomEntry as _isCustomEntry,
|
|
8
8
|
} from "@alexleekt/pi-shared/types";
|
|
9
9
|
|
|
10
|
-
/* ── Defensive: isCustomEntry may resolve to undefined in some jiti cache states ── */
|
|
11
|
-
const isCustomEntry: typeof _isCustomEntry =
|
|
12
|
-
typeof _isCustomEntry === "function"
|
|
13
|
-
? _isCustomEntry
|
|
14
|
-
: (e: unknown): e is CustomJournalEntry => {
|
|
15
|
-
if (!e || typeof e !== "object") return false;
|
|
16
|
-
const entry = e as Record<string, unknown>;
|
|
17
|
-
return (
|
|
18
|
-
entry.type === "custom" &&
|
|
19
|
-
typeof entry.customType === "string" &&
|
|
20
|
-
typeof entry.data === "object" &&
|
|
21
|
-
entry.data !== null
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
10
|
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
25
11
|
import type {
|
|
26
|
-
BuildSystemPromptOptions,
|
|
27
12
|
ExtensionAPI,
|
|
28
13
|
ExtensionContext,
|
|
29
14
|
} from "@earendil-works/pi-coding-agent";
|
|
30
15
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
31
|
-
import { type AskUserParams, askUserHandler } from "./tool/ask-user.js";
|
|
32
|
-
import type { AnimationLevel, ThemeMode } from "./shared/ask-user.js";
|
|
33
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";
|
|
34
23
|
|
|
35
24
|
/* ── Module-level reference to ExtensionAPI for tool execute closure ── */
|
|
36
25
|
let _pi: ExtensionAPI | undefined;
|
|
37
26
|
|
|
38
|
-
/* ──
|
|
39
|
-
|
|
40
|
-
const QUESTION_SESSION_PATTERNS = [
|
|
41
|
-
/ask the questions? one at a time/i,
|
|
42
|
-
/interview me/i,
|
|
43
|
-
/grilling session/i,
|
|
44
|
-
/ask questions? (one at a time|sequentially|individually)/i,
|
|
45
|
-
/wait for feedback/i,
|
|
46
|
-
/questionnaire mode/i,
|
|
47
|
-
/one question per call/i,
|
|
48
|
-
];
|
|
27
|
+
/* ── Style constants ── */
|
|
49
28
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
"questionnaire",
|
|
53
|
-
"interview",
|
|
54
|
-
"grill",
|
|
55
|
-
]);
|
|
29
|
+
const YOLO_MANDATE = `
|
|
30
|
+
## Tool Usage Mandate — Auto-injected by pi-ask-user-glimpse (YOLO Style)
|
|
56
31
|
|
|
57
|
-
|
|
58
|
-
|
|
32
|
+
You are in YOLO style. Do NOT ask the user for input or confirmation.
|
|
33
|
+
Go with your best recommendation and proceed immediately.
|
|
59
34
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
should be a separate \`ask_user\` tool call.
|
|
35
|
+
Only use \`ask_user\` if the action would cause irreversible harm,
|
|
36
|
+
data loss, security compromise, or violate explicit hard constraints.
|
|
63
37
|
|
|
64
38
|
Rules:
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
- Set \`allowFreeform: true\` when the user might want to answer in their own words.
|
|
69
|
-
- 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.
|
|
70
42
|
`;
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
systemPrompt: string,
|
|
74
|
-
options: BuildSystemPromptOptions,
|
|
75
|
-
): boolean {
|
|
76
|
-
const hasQuestionSkill = !!options.skills?.some((s) =>
|
|
77
|
-
QUESTION_SKILL_NAMES.has(s.name.toLowerCase()),
|
|
78
|
-
);
|
|
79
|
-
const hasQuestionLanguage = QUESTION_SESSION_PATTERNS.some((p) =>
|
|
80
|
-
p.test(systemPrompt),
|
|
81
|
-
);
|
|
82
|
-
return hasQuestionSkill || hasQuestionLanguage;
|
|
83
|
-
}
|
|
44
|
+
type StyleMode = "plain" | "yolo";
|
|
84
45
|
|
|
85
|
-
function
|
|
46
|
+
function findCustomData(
|
|
47
|
+
entries: unknown[],
|
|
48
|
+
customType: string,
|
|
49
|
+
): Record<string, unknown> | undefined {
|
|
86
50
|
const entry = entries.find(
|
|
87
51
|
(e): e is CustomJournalEntry =>
|
|
88
|
-
isCustomEntry(e) && e.customType ===
|
|
52
|
+
isCustomEntry(e) && e.customType === customType,
|
|
89
53
|
);
|
|
90
|
-
|
|
91
|
-
return typeof enabled === "boolean" ? enabled : null;
|
|
54
|
+
return entry?.data as Record<string, unknown> | undefined;
|
|
92
55
|
}
|
|
93
56
|
|
|
94
|
-
function
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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");
|
|
100
71
|
const theme = typeof data?.theme === "string" ? data.theme : undefined;
|
|
101
|
-
const animationLevel =
|
|
72
|
+
const animationLevel =
|
|
73
|
+
typeof data?.animationLevel === "string"
|
|
74
|
+
? data.animationLevel
|
|
75
|
+
: undefined;
|
|
102
76
|
return {
|
|
103
|
-
theme:
|
|
104
|
-
|
|
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,
|
|
105
87
|
};
|
|
106
88
|
}
|
|
107
89
|
|
|
90
|
+
/** Extract text blocks from a content array (journal entry). */
|
|
91
|
+
function extractTextFromContent(content: unknown): string {
|
|
92
|
+
if (typeof content === "string") return content;
|
|
93
|
+
if (!Array.isArray(content)) return "";
|
|
94
|
+
return content
|
|
95
|
+
.filter(
|
|
96
|
+
(c): c is { type: string; text: string } =>
|
|
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",
|
|
101
|
+
)
|
|
102
|
+
.map((c) => c.text)
|
|
103
|
+
.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
108
106
|
/* ── Shared helpers for consistent ask_user UX across all entry points ── */
|
|
109
107
|
|
|
110
108
|
/** Enrich raw ask_user params with persisted theme/animation settings. */
|
|
@@ -116,16 +114,22 @@ function enrichWithThemeSettings(
|
|
|
116
114
|
return { ...params, theme, animationLevel };
|
|
117
115
|
}
|
|
118
116
|
|
|
119
|
-
/**
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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();
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
/** Execute ask_user with full enrichment + persistence, used by tool and commands alike. */
|
|
@@ -136,97 +140,38 @@ async function runAskUserWithTheme(
|
|
|
136
140
|
): Promise<ReturnType<typeof askUserHandler>> {
|
|
137
141
|
const entries = ctx.sessionManager.getEntries();
|
|
138
142
|
const params = enrichWithThemeSettings(rawParams, entries);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const enrichedParams: AskUserParams = preamble
|
|
145
|
-
? {
|
|
146
|
-
...params,
|
|
147
|
-
context: params.context
|
|
148
|
-
? `${preamble}\n\n---\n\n${params.context}`
|
|
149
|
-
: preamble,
|
|
150
|
-
}
|
|
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) }
|
|
151
148
|
: params;
|
|
152
149
|
|
|
153
|
-
const result = await askUserHandler(
|
|
150
|
+
const result = await askUserHandler(cleanedParams, signal, ctx, (m) => {
|
|
154
151
|
metadata = m;
|
|
155
152
|
});
|
|
156
|
-
|
|
153
|
+
saveThemeMetadata(metadata);
|
|
157
154
|
return result;
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/** Extract plain text from a Pi journal assistant entry. */
|
|
161
158
|
function extractTextFromAssistantEntry(entry: unknown): string {
|
|
162
159
|
if (!entry || typeof entry !== "object") return "";
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
if (!message || typeof message !== "object") return "";
|
|
166
|
-
const msg = message as Record<string, unknown>;
|
|
167
|
-
|
|
168
|
-
const content = msg.content;
|
|
169
|
-
if (typeof content === "string") return content;
|
|
170
|
-
if (!Array.isArray(content)) return "";
|
|
171
|
-
|
|
172
|
-
return content
|
|
173
|
-
.filter(
|
|
174
|
-
(c): c is { type: string; text: string } =>
|
|
175
|
-
typeof c === "object" &&
|
|
176
|
-
c !== null &&
|
|
177
|
-
typeof (c as Record<string, unknown>).type === "string" &&
|
|
178
|
-
typeof (c as Record<string, unknown>).text === "string",
|
|
179
|
-
)
|
|
180
|
-
.map((c) => c.text)
|
|
181
|
-
.join("\n");
|
|
160
|
+
const content = ((entry as Record<string, unknown>).message as Record<string, unknown> | undefined)?.content;
|
|
161
|
+
return extractTextFromContent(content);
|
|
182
162
|
}
|
|
183
163
|
|
|
184
164
|
/** Find the most recent assistant entry in the session journal. */
|
|
185
165
|
function findLastAssistantEntry(entries: unknown[]): unknown | undefined {
|
|
186
166
|
return [...entries].reverse().find((e) => {
|
|
187
167
|
if (!e || typeof e !== "object") return false;
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
if (!msg || typeof msg !== "object" || msg === null) return false;
|
|
168
|
+
const msg = (e as Record<string, unknown>).message;
|
|
169
|
+
if (!msg || typeof msg !== "object") return false;
|
|
191
170
|
return (msg as Record<string, unknown>).role === "assistant";
|
|
192
171
|
});
|
|
193
172
|
}
|
|
194
173
|
|
|
195
|
-
|
|
196
|
-
* Extract the agent's introductory text from the most recent assistant
|
|
197
|
-
* message, excluding text that is already duplicated in the question or
|
|
198
|
-
* context fields.
|
|
199
|
-
*/
|
|
200
|
-
function buildAgentPreamble(
|
|
201
|
-
params: AskUserParams,
|
|
202
|
-
entries: unknown[],
|
|
203
|
-
): string | undefined {
|
|
204
|
-
const lastAssistant = findLastAssistantEntry(entries);
|
|
205
|
-
if (!lastAssistant) return undefined;
|
|
206
|
-
|
|
207
|
-
const text = extractTextFromAssistantEntry(lastAssistant).trim();
|
|
208
|
-
if (!text) return undefined;
|
|
209
|
-
|
|
210
|
-
const question = params.question.trim();
|
|
211
|
-
|
|
212
|
-
// Skip if the assistant text is just the question itself
|
|
213
|
-
if (text === question) return undefined;
|
|
214
|
-
|
|
215
|
-
// If the text ends with the question, take only the prefix
|
|
216
|
-
if (text.endsWith(question)) {
|
|
217
|
-
const prefix = text.slice(0, text.length - question.length).trim();
|
|
218
|
-
return prefix || undefined;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Skip if the assistant text is already fully contained in the context
|
|
222
|
-
if (params.context?.trim().includes(text)) return undefined;
|
|
223
|
-
|
|
224
|
-
return text;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/* ── /ask: extract questions & implicit requests ── */
|
|
228
|
-
|
|
229
|
-
|
|
174
|
+
/* ── /ask: extract explicit questions only ── */
|
|
230
175
|
|
|
231
176
|
function splitSentences(text: string): string[] {
|
|
232
177
|
const PLACEHOLDER = "\x00";
|
|
@@ -255,19 +200,6 @@ function splitSentences(text: string): string[] {
|
|
|
255
200
|
.filter((s) => s.length > 0);
|
|
256
201
|
}
|
|
257
202
|
|
|
258
|
-
const IMPLICIT_REQUEST_PATTERNS = [
|
|
259
|
-
/\b(let me know|let us know)\b/i,
|
|
260
|
-
/\b(tell me|tell us)\b/i,
|
|
261
|
-
/\b(share your|share any)\b/i,
|
|
262
|
-
/\b(what do you think|what are your thoughts)\b/i,
|
|
263
|
-
/\b(which\b.*\b(would you|do you|should we)\b)/i,
|
|
264
|
-
/\b(should we|can you confirm|please confirm|could you confirm)\b/i,
|
|
265
|
-
/\b(i need your (input|feedback|thoughts|opinion))\b/i,
|
|
266
|
-
/\b(your (thoughts|opinion|preference|feedback))\b/i,
|
|
267
|
-
/\b(please provide|could you provide|can you provide)\b/i,
|
|
268
|
-
/\b(would you like|do you want|do you prefer)\b/i,
|
|
269
|
-
];
|
|
270
|
-
|
|
271
203
|
function hasQuotedQuestion(sentence: string): boolean {
|
|
272
204
|
return /["'`].*\?.*["'`]/.test(sentence) && !sentence.endsWith("?");
|
|
273
205
|
}
|
|
@@ -276,25 +208,17 @@ function looksLikeTernary(sentence: string): boolean {
|
|
|
276
208
|
return /\?\s*[:;]/.test(sentence) || /=\s*\S+\s*\?/.test(sentence);
|
|
277
209
|
}
|
|
278
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. */
|
|
279
214
|
function extractQuestions(text: string): string[] {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (sentence.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (sentence.length < 3) continue;
|
|
288
|
-
explicit.push(sentence);
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (IMPLICIT_REQUEST_PATTERNS.some((p) => p.test(sentence))) {
|
|
293
|
-
implicit.push(sentence);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
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
|
+
});
|
|
298
222
|
}
|
|
299
223
|
|
|
300
224
|
function truncate(str: string, max: number): string {
|
|
@@ -305,23 +229,24 @@ function buildAskLastParams(
|
|
|
305
229
|
questions: string[],
|
|
306
230
|
fullText: string,
|
|
307
231
|
): AskUserParams {
|
|
232
|
+
const cleanContext = stripThinkingBlocks(fullText);
|
|
308
233
|
if (questions.length === 0) {
|
|
309
234
|
return {
|
|
310
235
|
question: "The assistant would like your input on the following:",
|
|
311
|
-
context:
|
|
236
|
+
context: cleanContext,
|
|
312
237
|
allowFreeform: true,
|
|
313
238
|
};
|
|
314
239
|
}
|
|
315
240
|
if (questions.length === 1) {
|
|
316
241
|
return {
|
|
317
242
|
question: questions[0],
|
|
318
|
-
context:
|
|
243
|
+
context: cleanContext,
|
|
319
244
|
allowFreeform: true,
|
|
320
245
|
};
|
|
321
246
|
}
|
|
322
247
|
return {
|
|
323
248
|
question: "The assistant asked multiple questions",
|
|
324
|
-
context:
|
|
249
|
+
context: cleanContext,
|
|
325
250
|
questions: questions.map((q) => ({
|
|
326
251
|
title: truncate(q, 60),
|
|
327
252
|
description: q,
|
|
@@ -331,14 +256,23 @@ function buildAskLastParams(
|
|
|
331
256
|
};
|
|
332
257
|
}
|
|
333
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
|
+
|
|
334
268
|
function buildDebugParams(mode: string): AskUserParams | null {
|
|
335
269
|
switch (mode) {
|
|
336
270
|
case "single-select":
|
|
337
271
|
return {
|
|
338
272
|
question: "Test: Single Select",
|
|
339
|
-
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.",
|
|
340
274
|
options: [
|
|
341
|
-
{ title: "Option A", description: "Description for A" },
|
|
275
|
+
{ title: "Option A", description: "Description for A", recommended: true },
|
|
342
276
|
{ title: "Option B", description: "Description for B" },
|
|
343
277
|
{ title: "Option C", description: "Description for C" },
|
|
344
278
|
],
|
|
@@ -349,9 +283,9 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
349
283
|
return {
|
|
350
284
|
question: "Test: Multi Select",
|
|
351
285
|
context:
|
|
352
|
-
"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.",
|
|
353
287
|
options: [
|
|
354
|
-
{ title: "Feature X", description: "Enable feature X" },
|
|
288
|
+
{ title: "Feature X", description: "Enable feature X", recommended: true },
|
|
355
289
|
{ title: "Feature Y", description: "Enable feature Y" },
|
|
356
290
|
{ title: "Feature Z", description: "Enable feature Z" },
|
|
357
291
|
],
|
|
@@ -362,13 +296,13 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
362
296
|
case "freeform":
|
|
363
297
|
return {
|
|
364
298
|
question: "Test: Freeform",
|
|
365
|
-
context: "Type any answer you like",
|
|
299
|
+
context: "Type any answer you like. This tests the textarea, character counter, and platform-aware keyboard shortcuts.",
|
|
366
300
|
allowFreeform: true,
|
|
367
301
|
};
|
|
368
302
|
case "questionnaire":
|
|
369
303
|
return {
|
|
370
304
|
question: "Test: Questionnaire",
|
|
371
|
-
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.",
|
|
372
306
|
questions: [
|
|
373
307
|
{
|
|
374
308
|
title: "Database",
|
|
@@ -377,6 +311,7 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
377
311
|
{
|
|
378
312
|
title: "PostgreSQL",
|
|
379
313
|
description: "Relational, proven",
|
|
314
|
+
recommended: true,
|
|
380
315
|
},
|
|
381
316
|
{ title: "SQLite", description: "Zero-config" },
|
|
382
317
|
],
|
|
@@ -385,7 +320,7 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
385
320
|
title: "Architecture",
|
|
386
321
|
description: "Preferred style?",
|
|
387
322
|
options: [
|
|
388
|
-
{ title: "Monolith", description: "Simple" },
|
|
323
|
+
{ title: "Monolith", description: "Simple", recommended: true },
|
|
389
324
|
{ title: "Microservices", description: "Scalable" },
|
|
390
325
|
],
|
|
391
326
|
allowMultiple: true,
|
|
@@ -397,71 +332,165 @@ function buildDebugParams(mode: string): AskUserParams | null {
|
|
|
397
332
|
],
|
|
398
333
|
allowComment: true,
|
|
399
334
|
};
|
|
400
|
-
case "
|
|
335
|
+
case "kitchen-sink":
|
|
401
336
|
return {
|
|
402
|
-
question: "
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
}
|
|
436
436
|
],
|
|
437
437
|
allowComment: true,
|
|
438
|
+
allowSkip: true
|
|
438
439
|
};
|
|
439
440
|
default:
|
|
440
441
|
return null;
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
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
|
+
|
|
444
460
|
const askUserTool = defineTool({
|
|
445
461
|
name: "ask_user",
|
|
446
462
|
label: "Ask User",
|
|
447
|
-
description:
|
|
448
|
-
"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,
|
|
449
464
|
promptSnippet:
|
|
450
465
|
"Ask the user one focused question with optional multiple-choice answers to gather information interactively",
|
|
451
466
|
promptGuidelines: [
|
|
452
467
|
"Always use ask_user instead of guessing when user input would improve the answer.",
|
|
453
468
|
"Keep the question field short and focused (ideally one sentence). Put background, examples, or elaboration in the context field.",
|
|
454
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.",
|
|
455
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.",
|
|
456
476
|
"Set allowMultiple: true when more than one choice is valid.",
|
|
457
477
|
"Set allowFreeform: true (default) when the user might want to answer in their own words.",
|
|
458
478
|
],
|
|
459
479
|
parameters: Type.Object({
|
|
460
|
-
question: Type.String({
|
|
480
|
+
question: Type.String({
|
|
481
|
+
description:
|
|
482
|
+
"A short, focused question (ideally one sentence). Put background detail in context.",
|
|
483
|
+
}),
|
|
461
484
|
context: Type.Optional(
|
|
462
485
|
Type.String({
|
|
463
486
|
description:
|
|
464
|
-
"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.",
|
|
465
494
|
}),
|
|
466
495
|
),
|
|
467
496
|
options: Type.Optional(
|
|
@@ -478,6 +507,12 @@ const askUserTool = defineTool({
|
|
|
478
507
|
"Longer description explaining this option",
|
|
479
508
|
}),
|
|
480
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
|
+
),
|
|
481
516
|
}),
|
|
482
517
|
]),
|
|
483
518
|
{ description: "List of options for the user to choose from" },
|
|
@@ -504,6 +539,12 @@ const askUserTool = defineTool({
|
|
|
504
539
|
description: "Option description",
|
|
505
540
|
}),
|
|
506
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
|
+
),
|
|
507
548
|
}),
|
|
508
549
|
{
|
|
509
550
|
description:
|
|
@@ -562,7 +603,7 @@ const askUserTool = defineTool({
|
|
|
562
603
|
}),
|
|
563
604
|
|
|
564
605
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
565
|
-
return runAskUserWithTheme(params, signal, ctx);
|
|
606
|
+
return runAskUserWithTheme(params as AskUserParams, signal, ctx);
|
|
566
607
|
},
|
|
567
608
|
});
|
|
568
609
|
|
|
@@ -570,48 +611,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
570
611
|
_pi = pi;
|
|
571
612
|
pi.registerTool(askUserTool);
|
|
572
613
|
|
|
573
|
-
|
|
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 ──
|
|
574
634
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
575
635
|
const hasAskUser =
|
|
576
636
|
event.systemPromptOptions.selectedTools?.includes("ask_user");
|
|
577
637
|
if (!hasAskUser) return;
|
|
578
638
|
|
|
639
|
+
// Don't force ask_user in headless environments — the tool can't render dialogs
|
|
640
|
+
if (!ctx.hasUI) return;
|
|
641
|
+
|
|
579
642
|
const styleMode = getStyleMode(ctx.sessionManager.getEntries());
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
isQuestionSession(
|
|
584
|
-
event.systemPrompt,
|
|
585
|
-
event.systemPromptOptions,
|
|
586
|
-
));
|
|
587
|
-
|
|
588
|
-
if (shouldInject) {
|
|
589
|
-
return { systemPrompt: event.systemPrompt + ASK_USER_MANDATE };
|
|
643
|
+
|
|
644
|
+
if (styleMode === "yolo") {
|
|
645
|
+
return { systemPrompt: event.systemPrompt + YOLO_MANDATE };
|
|
590
646
|
}
|
|
647
|
+
// "plain" → no injection
|
|
591
648
|
});
|
|
592
649
|
|
|
593
|
-
// ── Manual style toggle
|
|
650
|
+
// ── Manual style toggle ──
|
|
594
651
|
pi.registerCommand("ask-style", {
|
|
595
652
|
description:
|
|
596
|
-
"Cycle ask_user style:
|
|
653
|
+
"Cycle ask_user style: Plain Text → YOLO → Plain Text",
|
|
597
654
|
handler: async (_args, ctx) => {
|
|
598
655
|
const styleMode = getStyleMode(ctx.sessionManager.getEntries());
|
|
599
656
|
|
|
600
|
-
let nextMode:
|
|
657
|
+
let nextMode: StyleMode;
|
|
601
658
|
let label: string;
|
|
602
659
|
|
|
603
|
-
if (styleMode ===
|
|
604
|
-
nextMode =
|
|
605
|
-
label = "
|
|
606
|
-
} else if (styleMode === true) {
|
|
607
|
-
nextMode = false;
|
|
608
|
-
label = "Plain Text (no dialog injection)";
|
|
660
|
+
if (styleMode === "plain") {
|
|
661
|
+
nextMode = "yolo";
|
|
662
|
+
label = "YOLO — go with your recommendation";
|
|
609
663
|
} else {
|
|
610
|
-
nextMode =
|
|
611
|
-
label = "
|
|
664
|
+
nextMode = "plain";
|
|
665
|
+
label = "Plain Text (no dialog injection)";
|
|
612
666
|
}
|
|
613
667
|
|
|
614
|
-
pi.appendEntry("ask-user-style", {
|
|
668
|
+
await pi.appendEntry("ask-user-style", { mode: nextMode });
|
|
615
669
|
ctx.ui.notify(`ask_user style: ${label}`, "info");
|
|
616
670
|
},
|
|
617
671
|
});
|
|
@@ -664,14 +718,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
664
718
|
const answer = textContent?.type === "text" ? textContent.text : "";
|
|
665
719
|
if (!answer) return;
|
|
666
720
|
|
|
667
|
-
|
|
668
|
-
if (questions.length === 0) {
|
|
669
|
-
prefix = "Responding to your last message:";
|
|
670
|
-
} else {
|
|
671
|
-
const plural = questions.length > 1 ? "s" : "";
|
|
672
|
-
prefix = `Answering the question${plural} from your last message:`;
|
|
673
|
-
}
|
|
674
|
-
pi.sendUserMessage(`${prefix}\n\n${answer}`);
|
|
721
|
+
await deliverAnswer(answerPrefix(questions.length), answer, ctx);
|
|
675
722
|
},
|
|
676
723
|
});
|
|
677
724
|
|
|
@@ -690,8 +737,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
690
737
|
"multi-select",
|
|
691
738
|
"freeform",
|
|
692
739
|
"questionnaire",
|
|
693
|
-
"
|
|
694
|
-
"mermaid",
|
|
740
|
+
"kitchen-sink",
|
|
695
741
|
]);
|
|
696
742
|
if (!mode) return;
|
|
697
743
|
|
|
@@ -702,7 +748,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
702
748
|
const textContent = result.content[0];
|
|
703
749
|
const text =
|
|
704
750
|
textContent.type === "text" ? textContent.text : "No response";
|
|
705
|
-
|
|
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
|
+
);
|
|
706
761
|
},
|
|
707
762
|
});
|
|
763
|
+
|
|
708
764
|
}
|