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 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**: Plain string single/multi options can reveal a separate inline `Optional clarification...` field when selected, letting users attach a short note to a choice without using `Ask`
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 (!questionCanClarifyOption(question) || !isSelected) return null;
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 = questionCanClarifyOption(question) ? getChoiceNote(id, selected.value) : "";
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 = questionCanClarifyOption(question) ? getChoiceNote(id, input.value) : "";
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 (questionCanClarifyOption(question) && choiceValue.note) {
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 (questionCanClarifyOption(question) && choiceValue.note) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-interview",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Interactive interview form extension for pi coding agent",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
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
- return {
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
- return { label: o.label };
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}"`);