pi-interview 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/form/script.js +5 -17
- package/package.json +1 -1
- package/schema.ts +62 -2
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Restart pi to load the extension.
|
|
|
40
40
|
- **Path Normalization**: Handles shell-escaped paths (`\ `) and macOS screenshot filenames (narrow no-break space before AM/PM)
|
|
41
41
|
- **Generate & Review Options**: Single/multi-select questions, including rich-option questions with inline content blocks, show "✦ Generate more" (appends new choices) and "↻ Review options" (reviews options and rewrites the question for clarity) buttons powered by an LLM
|
|
42
42
|
- **Ask About an Option**: Single/multi options, including rich options with inline content blocks, can open an inline assistant panel with prompt chips, freeform follow-up questions, provider/model overrides under Advanced, and actions like pinning analysis or applying a suggested rewrite
|
|
43
|
-
- **Option Clarifications**:
|
|
43
|
+
- **Option Clarifications**: Single/multi options, including rich options with inline content blocks, can reveal a separate inline `Optional clarification...` field when selected, letting users attach a short note to a choice without using `Ask`
|
|
44
44
|
- **Tool Discoverability (pi v0.59+)**: Registers a `promptSnippet` so `interview` remains eligible for inclusion in pi's default `Available tools` prompt section
|
|
45
45
|
- **Themes**: Built-in default + optional light/dark + custom theme CSS
|
|
46
46
|
|
package/form/script.js
CHANGED
|
@@ -556,13 +556,6 @@
|
|
|
556
556
|
return resolved;
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
function questionCanClarifyOption(question) {
|
|
560
|
-
return (question.type === "single" || question.type === "multi")
|
|
561
|
-
&& Array.isArray(question.options)
|
|
562
|
-
&& question.options.length > 0
|
|
563
|
-
&& question.options.every((option) => typeof option === "string");
|
|
564
|
-
}
|
|
565
|
-
|
|
566
559
|
function isChoiceResponseValue(value) {
|
|
567
560
|
return value && typeof value === "object" && !Array.isArray(value) && typeof value.option === "string";
|
|
568
561
|
}
|
|
@@ -647,10 +640,6 @@
|
|
|
647
640
|
.filter((value) => value && value !== "__other__");
|
|
648
641
|
}
|
|
649
642
|
|
|
650
|
-
function syncChoiceNotesWithSelection(question) {
|
|
651
|
-
if (!questionCanClarifyOption(question)) return;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
643
|
function isRichOption(option) {
|
|
655
644
|
return typeof option === "object" && option !== null && "label" in option;
|
|
656
645
|
}
|
|
@@ -1436,7 +1425,7 @@
|
|
|
1436
1425
|
}
|
|
1437
1426
|
|
|
1438
1427
|
function createOptionNoteInput(question, optionLabel, isSelected) {
|
|
1439
|
-
if (!
|
|
1428
|
+
if (!questionSupportsOptionInsights(question) || !isSelected) return null;
|
|
1440
1429
|
|
|
1441
1430
|
const wrap = document.createElement("div");
|
|
1442
1431
|
wrap.className = "option-note-wrap";
|
|
@@ -1491,7 +1480,6 @@
|
|
|
1491
1480
|
input.id = `q-${question.id}-${optionIndex}`;
|
|
1492
1481
|
|
|
1493
1482
|
input.addEventListener("change", () => {
|
|
1494
|
-
syncChoiceNotesWithSelection(question);
|
|
1495
1483
|
debounceSave();
|
|
1496
1484
|
if (question.type === "multi") {
|
|
1497
1485
|
updateDoneState(question.id);
|
|
@@ -3348,7 +3336,7 @@
|
|
|
3348
3336
|
const otherValue = getOtherValue(id).trim();
|
|
3349
3337
|
return otherValue ? { option: otherValue } : "";
|
|
3350
3338
|
}
|
|
3351
|
-
const note =
|
|
3339
|
+
const note = questionSupportsOptionInsights(question) ? getChoiceNote(id, selected.value) : "";
|
|
3352
3340
|
return note ? { option: selected.value, note } : { option: selected.value };
|
|
3353
3341
|
}
|
|
3354
3342
|
if (question.type === "multi") {
|
|
@@ -3359,7 +3347,7 @@
|
|
|
3359
3347
|
const otherValue = getOtherValue(id).trim();
|
|
3360
3348
|
return otherValue ? { option: otherValue } : null;
|
|
3361
3349
|
}
|
|
3362
|
-
const note =
|
|
3350
|
+
const note = questionSupportsOptionInsights(question) ? getChoiceNote(id, input.value) : "";
|
|
3363
3351
|
return note ? { option: input.value, note } : { option: input.value };
|
|
3364
3352
|
}).filter((value) => value && value.option);
|
|
3365
3353
|
}
|
|
@@ -3432,7 +3420,7 @@
|
|
|
3432
3420
|
);
|
|
3433
3421
|
if (input) {
|
|
3434
3422
|
input.checked = true;
|
|
3435
|
-
if (
|
|
3423
|
+
if (questionSupportsOptionInsights(question) && choiceValue.note) {
|
|
3436
3424
|
setChoiceNote(question.id, choiceValue.option, choiceValue.note);
|
|
3437
3425
|
}
|
|
3438
3426
|
} else {
|
|
@@ -3469,7 +3457,7 @@
|
|
|
3469
3457
|
);
|
|
3470
3458
|
if (input) {
|
|
3471
3459
|
input.checked = true;
|
|
3472
|
-
if (
|
|
3460
|
+
if (questionSupportsOptionInsights(question) && choiceValue.note) {
|
|
3473
3461
|
setChoiceNote(question.id, choiceValue.option, choiceValue.note);
|
|
3474
3462
|
}
|
|
3475
3463
|
} else if (choiceValue.option) {
|
package/package.json
CHANGED
package/schema.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type ContentBlock = MarkdownContentBlock | CodeContentBlock;
|
|
|
23
23
|
export interface RichOption {
|
|
24
24
|
label: string;
|
|
25
25
|
content?: ContentBlock;
|
|
26
|
+
recommended?: boolean;
|
|
27
|
+
conviction?: "strong" | "slight";
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export type OptionValue = string | RichOption;
|
|
@@ -75,6 +77,57 @@ export function isRichOption(option: OptionValue): option is RichOption {
|
|
|
75
77
|
return typeof option === "object" && option !== null && "label" in option;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
function normalizeOptionLevelRecommendations(question: Question): void {
|
|
81
|
+
if (!question.options) return;
|
|
82
|
+
|
|
83
|
+
const recommendedOptions: RichOption[] = [];
|
|
84
|
+
const convictions = new Set<"strong" | "slight">();
|
|
85
|
+
const richOptions: RichOption[] = [];
|
|
86
|
+
|
|
87
|
+
for (const option of question.options) {
|
|
88
|
+
if (!isRichOption(option)) continue;
|
|
89
|
+
richOptions.push(option);
|
|
90
|
+
if (option.conviction !== undefined && option.recommended !== true) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Question "${question.id}" option "${option.label}": conviction requires recommended`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (option.recommended !== true) continue;
|
|
96
|
+
recommendedOptions.push(option);
|
|
97
|
+
if (option.conviction) convictions.add(option.conviction);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (recommendedOptions.length === 0) return;
|
|
101
|
+
|
|
102
|
+
if (question.recommended !== undefined || question.conviction !== undefined) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Question "${question.id}": use either question-level recommended/conviction or option-level recommended flags, not both`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (question.type === "single" && recommendedOptions.length !== 1) {
|
|
109
|
+
throw new Error(`Question "${question.id}": exactly one option must be recommended for single-select`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (convictions.size > 1) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Question "${question.id}": recommended options must use the same conviction`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const option of richOptions) {
|
|
119
|
+
delete option.recommended;
|
|
120
|
+
delete option.conviction;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const recommendedLabels = recommendedOptions.map((option) => option.label);
|
|
124
|
+
question.recommended = question.type === "single" ? recommendedLabels[0] : recommendedLabels;
|
|
125
|
+
const conviction = convictions.values().next().value;
|
|
126
|
+
if (conviction !== undefined) {
|
|
127
|
+
question.conviction = conviction;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
function validateMediaBlock(block: unknown, context: string): MediaBlock {
|
|
79
132
|
if (!block || typeof block !== "object") {
|
|
80
133
|
throw new Error(`${context}: media must be an object`);
|
|
@@ -224,12 +277,18 @@ function validateOption(option: unknown, questionId: string, index: number): Opt
|
|
|
224
277
|
);
|
|
225
278
|
}
|
|
226
279
|
if (o.content !== undefined) {
|
|
227
|
-
|
|
280
|
+
const result: RichOption = {
|
|
228
281
|
label: o.label,
|
|
229
282
|
content: validateContentBlock(o.content, `Question "${questionId}" option "${o.label}"`),
|
|
230
283
|
};
|
|
284
|
+
if (o.recommended === true) result.recommended = true;
|
|
285
|
+
if (o.conviction === "strong" || o.conviction === "slight") result.conviction = o.conviction;
|
|
286
|
+
return result;
|
|
231
287
|
}
|
|
232
|
-
|
|
288
|
+
const result: RichOption = { label: o.label };
|
|
289
|
+
if (o.recommended === true) result.recommended = true;
|
|
290
|
+
if (o.conviction === "strong" || o.conviction === "slight") result.conviction = o.conviction;
|
|
291
|
+
return result;
|
|
233
292
|
}
|
|
234
293
|
throw new Error(
|
|
235
294
|
`Question "${questionId}": option at index ${index} must be a string or object with label`
|
|
@@ -354,6 +413,7 @@ export function validateQuestions(data: unknown): QuestionsFile {
|
|
|
354
413
|
if (!q.options || q.options.length === 0) {
|
|
355
414
|
throw new Error(`Question "${q.id}": options required for type "${q.type}"`);
|
|
356
415
|
}
|
|
416
|
+
normalizeOptionLevelRecommendations(q);
|
|
357
417
|
} else if (q.type === "text" || q.type === "image" || q.type === "info") {
|
|
358
418
|
if (q.options) {
|
|
359
419
|
throw new Error(`Question "${q.id}": options not allowed for type "${q.type}"`);
|