@openlines/opl-pi-sht 0.1.14

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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/configs/opl-browser.json.sample +8 -0
  4. package/configs/opl-footer.json +48 -0
  5. package/configs/opl-footer.json.sample +48 -0
  6. package/configs/opl-input.json +22 -0
  7. package/configs/opl-input.json.sample +22 -0
  8. package/configs/opl-modes.json +291 -0
  9. package/configs/opl-modes.json.sample +285 -0
  10. package/configs/opl-simplebench.json +8 -0
  11. package/configs/opl-simplebench.json.sample +8 -0
  12. package/configs/opl-todo.json +12 -0
  13. package/configs/opl-todo.json.sample +12 -0
  14. package/configs/opl-webaccess.json +31 -0
  15. package/configs/opl-webaccess.json.sample +31 -0
  16. package/extensions/opl-browser/README.md +83 -0
  17. package/extensions/opl-browser/browser.ts +185 -0
  18. package/extensions/opl-browser/config.ts +37 -0
  19. package/extensions/opl-browser/index.ts +89 -0
  20. package/extensions/opl-browser/package-lock.json +60 -0
  21. package/extensions/opl-browser/package.json +10 -0
  22. package/extensions/opl-ctxtrim/README.md +78 -0
  23. package/extensions/opl-ctxtrim/index.ts +151 -0
  24. package/extensions/opl-footer/README.md +193 -0
  25. package/extensions/opl-footer/config.ts +91 -0
  26. package/extensions/opl-footer/git-status.ts +170 -0
  27. package/extensions/opl-footer/icons.ts +49 -0
  28. package/extensions/opl-footer/index.ts +368 -0
  29. package/extensions/opl-footer/segments/caveman.ts +25 -0
  30. package/extensions/opl-footer/segments/chat-mode.ts +24 -0
  31. package/extensions/opl-footer/segments/context.ts +92 -0
  32. package/extensions/opl-footer/segments/cost.ts +25 -0
  33. package/extensions/opl-footer/segments/git.ts +47 -0
  34. package/extensions/opl-footer/segments/helpers.ts +18 -0
  35. package/extensions/opl-footer/segments/index.ts +51 -0
  36. package/extensions/opl-footer/segments/mode-switcher.ts +24 -0
  37. package/extensions/opl-footer/segments/model.ts +22 -0
  38. package/extensions/opl-footer/segments/path.ts +34 -0
  39. package/extensions/opl-footer/segments/pi.ts +11 -0
  40. package/extensions/opl-footer/segments/plan-mode.ts +24 -0
  41. package/extensions/opl-footer/segments/separator.ts +9 -0
  42. package/extensions/opl-footer/segments/session-stats.ts +82 -0
  43. package/extensions/opl-footer/segments/thinking.ts +51 -0
  44. package/extensions/opl-footer/segments/tokens.ts +66 -0
  45. package/extensions/opl-footer/theme.ts +113 -0
  46. package/extensions/opl-footer/types.ts +172 -0
  47. package/extensions/opl-init/README.md +23 -0
  48. package/extensions/opl-init/index.ts +451 -0
  49. package/extensions/opl-input/README.md +83 -0
  50. package/extensions/opl-input/config.ts +137 -0
  51. package/extensions/opl-input/index.ts +284 -0
  52. package/extensions/opl-input/mode-style.ts +38 -0
  53. package/extensions/opl-input/utils.ts +378 -0
  54. package/extensions/opl-modes/README.md +132 -0
  55. package/extensions/opl-modes/config.ts +473 -0
  56. package/extensions/opl-modes/index.ts +1081 -0
  57. package/extensions/opl-modes/menus.ts +60 -0
  58. package/extensions/opl-modes/state.ts +152 -0
  59. package/extensions/opl-modes/types.ts +164 -0
  60. package/extensions/opl-modes/utils.ts +138 -0
  61. package/extensions/opl-questionnaire/README.md +26 -0
  62. package/extensions/opl-questionnaire/index.ts +445 -0
  63. package/extensions/opl-questionnaire/types.ts +55 -0
  64. package/extensions/opl-simplebench/README.md +242 -0
  65. package/extensions/opl-simplebench/artifact.ts +41 -0
  66. package/extensions/opl-simplebench/benchmark.ts +873 -0
  67. package/extensions/opl-simplebench/coding.ts +282 -0
  68. package/extensions/opl-simplebench/index.ts +203 -0
  69. package/extensions/opl-simplebench/llama-server.ts +290 -0
  70. package/extensions/opl-simplebench/metrics.ts +73 -0
  71. package/extensions/opl-simplebench/report.ts +46 -0
  72. package/extensions/opl-simplebench/research.ts +200 -0
  73. package/extensions/opl-simplebench/scoring.ts +48 -0
  74. package/extensions/opl-simplebench/tests.ts +39 -0
  75. package/extensions/opl-simplebench/types.ts +6 -0
  76. package/extensions/opl-simplebench/util/config.ts +170 -0
  77. package/extensions/opl-simplebench/util/debug.ts +32 -0
  78. package/extensions/opl-simplebench/util/format.ts +402 -0
  79. package/extensions/opl-simplebench/util/providers.ts +841 -0
  80. package/extensions/opl-todo/README.md +43 -0
  81. package/extensions/opl-todo/config.ts +71 -0
  82. package/extensions/opl-todo/index.ts +415 -0
  83. package/extensions/opl-webaccess/README.md +47 -0
  84. package/extensions/opl-webaccess/config.ts +55 -0
  85. package/extensions/opl-webaccess/extract.ts +83 -0
  86. package/extensions/opl-webaccess/index.ts +232 -0
  87. package/extensions/opl-webaccess/package-lock.json +345 -0
  88. package/extensions/opl-webaccess/package.json +18 -0
  89. package/extensions/opl-webaccess/pdf.ts +8 -0
  90. package/extensions/opl-webaccess/providers/ddgs.ts +72 -0
  91. package/extensions/opl-webaccess/providers/exa.ts +74 -0
  92. package/extensions/opl-webaccess/providers/gemini.ts +60 -0
  93. package/extensions/opl-webaccess/providers/searxng.ts +86 -0
  94. package/extensions/opl-webaccess/providers/tavily.ts +60 -0
  95. package/extensions/opl-webaccess/search.ts +36 -0
  96. package/extensions/opl-webaccess/storage.ts +51 -0
  97. package/extensions/opl-webaccess/turndown.d.ts +1 -0
  98. package/extensions/opl-webaccess/types.ts +26 -0
  99. package/extensions/opl-webaccess/utils.ts +28 -0
  100. package/package.json +55 -0
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Questionnaire Tool - Unified tool for asking single or multiple questions
3
+ *
4
+ * Single question: simple options list
5
+ * Multiple questions: tab bar navigation between questions
6
+ *
7
+ * Entirely model-invoked: the LLM calls this tool instead of listing questions
8
+ * in plain text. There is no slash command or keybinding.
9
+ */
10
+
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import {
13
+ Editor,
14
+ type EditorTheme,
15
+ Key,
16
+ matchesKey,
17
+ Text,
18
+ visibleWidth,
19
+ wrapTextWithAnsi,
20
+ } from "@earendil-works/pi-tui";
21
+
22
+ import {
23
+ type Answer,
24
+ type Question,
25
+ QuestionnaireParams,
26
+ type QuestionnaireResult,
27
+ type RenderOption,
28
+ } from "./types.js";
29
+
30
+ function errorResult(
31
+ message: string,
32
+ questions: Question[] = [],
33
+ ): { content: { type: "text"; text: string }[]; details: QuestionnaireResult } {
34
+ return {
35
+ content: [{ type: "text", text: message }],
36
+ details: { questions, answers: [], cancelled: true },
37
+ };
38
+ }
39
+
40
+ export default function questionnaire(pi: ExtensionAPI) {
41
+ pi.registerTool({
42
+ name: "questionnaire",
43
+ label: "Questionnaire",
44
+ description:
45
+ "Ask the user one or more questions. Use for clarifying requirements, getting preferences, or confirming decisions. For single questions, shows a simple option list. For multiple questions, shows a tab-based interface. If this tool errors (e.g. non-interactive session), fall back to asking the questions in plain text.",
46
+ promptSnippet: "Ask the user clarifying questions with selectable options",
47
+ promptGuidelines: [
48
+ "Use questionnaire when you need to ask the user one or more clarifying questions with selectable options, instead of listing questions in plain text and waiting for the user to answer by number.",
49
+ "Prefer questionnaire whenever a decision has a small set of discrete choices (scope, approach, yes/no confirmation); it lets the user pick options directly and returns structured answers.",
50
+ ],
51
+ parameters: QuestionnaireParams,
52
+
53
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
54
+ if (ctx.mode !== "tui") {
55
+ return errorResult("Error: UI not available (running in non-interactive mode)");
56
+ }
57
+ if (params.questions.length === 0) {
58
+ return errorResult("Error: No questions provided");
59
+ }
60
+
61
+ // Reject duplicate ids — answers are keyed by id and would silently collide.
62
+ const seenIds = new Set<string>();
63
+ for (const q of params.questions) {
64
+ if (seenIds.has(q.id)) {
65
+ return errorResult(`Error: Duplicate question id "${q.id}" — question ids must be unique`);
66
+ }
67
+ seenIds.add(q.id);
68
+ }
69
+
70
+ // Reject unselectable questions (no options and no free-text fallback).
71
+ for (const q of params.questions) {
72
+ const allowOther = q.allowOther !== false;
73
+ if (q.options.length === 0 && !allowOther) {
74
+ return errorResult(
75
+ `Error: Question "${q.id}" has no options and allowOther is false — nothing is selectable`,
76
+ );
77
+ }
78
+ }
79
+
80
+ // Normalize questions with defaults
81
+ const questions: Question[] = params.questions.map((q, i) => ({
82
+ ...q,
83
+ label: q.label || `Q${i + 1}`,
84
+ allowOther: q.allowOther !== false,
85
+ }));
86
+
87
+ const isMulti = questions.length > 1;
88
+ const totalTabs = questions.length + 1; // questions + Submit
89
+
90
+ const result = await ctx.ui.custom<QuestionnaireResult>((tui, theme, _kb, done) => {
91
+ // State
92
+ let currentTab = 0;
93
+ let optionIndex = 0;
94
+ let inputMode = false;
95
+ let inputQuestionId: string | null = null;
96
+ let cachedLines: string[] | undefined;
97
+ const answers = new Map<string, Answer>();
98
+
99
+ // Editor for "Type something" option
100
+ const editorTheme: EditorTheme = {
101
+ borderColor: (s) => theme.fg("accent", s),
102
+ selectList: {
103
+ selectedPrefix: (t) => theme.fg("accent", t),
104
+ selectedText: (t) => theme.fg("accent", t),
105
+ description: (t) => theme.fg("muted", t),
106
+ scrollInfo: (t) => theme.fg("dim", t),
107
+ noMatch: (t) => theme.fg("warning", t),
108
+ },
109
+ };
110
+ const editor = new Editor(tui, editorTheme);
111
+
112
+ // Helpers
113
+ function refresh() {
114
+ cachedLines = undefined;
115
+ tui.requestRender();
116
+ }
117
+
118
+ function submit(cancelled: boolean) {
119
+ done({ questions, answers: Array.from(answers.values()), cancelled });
120
+ }
121
+
122
+ function currentQuestion(): Question | undefined {
123
+ return questions[currentTab];
124
+ }
125
+
126
+ function currentOptions(): RenderOption[] {
127
+ const q = currentQuestion();
128
+ if (!q) return [];
129
+ const opts: RenderOption[] = [...q.options];
130
+ if (q.allowOther) {
131
+ opts.push({ value: "__other__", label: "Type something.", isOther: true });
132
+ }
133
+ return opts;
134
+ }
135
+
136
+ function allAnswered(): boolean {
137
+ return questions.every((q) => answers.has(q.id));
138
+ }
139
+
140
+ function advanceAfterAnswer() {
141
+ if (!isMulti) {
142
+ submit(false);
143
+ return;
144
+ }
145
+ if (currentTab < questions.length - 1) {
146
+ currentTab++;
147
+ } else {
148
+ currentTab = questions.length; // Submit tab
149
+ }
150
+ optionIndex = 0;
151
+ refresh();
152
+ }
153
+
154
+ function saveAnswer(questionId: string, value: string, label: string, wasCustom: boolean, index?: number) {
155
+ answers.set(questionId, { id: questionId, value, label, wasCustom, index });
156
+ }
157
+
158
+ /** Select the option at the given index — enters input mode for "other", else saves + advances. */
159
+ function selectOption(q: Question, opts: RenderOption[], index: number) {
160
+ const opt = opts[index];
161
+ if (!opt) return;
162
+ if (opt.isOther) {
163
+ inputMode = true;
164
+ inputQuestionId = q.id;
165
+ editor.setText("");
166
+ refresh();
167
+ return;
168
+ }
169
+ saveAnswer(q.id, opt.value, opt.label, false, index + 1);
170
+ advanceAfterAnswer();
171
+ }
172
+
173
+ // Editor submit callback
174
+ editor.onSubmit = (value) => {
175
+ if (!inputQuestionId) return;
176
+ const trimmed = value.trim() || "(no response)";
177
+ saveAnswer(inputQuestionId, trimmed, trimmed, true);
178
+ inputMode = false;
179
+ inputQuestionId = null;
180
+ editor.setText("");
181
+ advanceAfterAnswer();
182
+ };
183
+
184
+ function handleInput(data: string) {
185
+ // Input mode: route to editor
186
+ if (inputMode) {
187
+ if (matchesKey(data, Key.escape)) {
188
+ inputMode = false;
189
+ inputQuestionId = null;
190
+ editor.setText("");
191
+ refresh();
192
+ return;
193
+ }
194
+ editor.handleInput(data);
195
+ refresh();
196
+ return;
197
+ }
198
+
199
+ const q = currentQuestion();
200
+ const opts = currentOptions();
201
+
202
+ // Tab navigation (multi-question only)
203
+ if (isMulti) {
204
+ if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
205
+ currentTab = (currentTab + 1) % totalTabs;
206
+ optionIndex = 0;
207
+ refresh();
208
+ return;
209
+ }
210
+ if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
211
+ currentTab = (currentTab - 1 + totalTabs) % totalTabs;
212
+ optionIndex = 0;
213
+ refresh();
214
+ return;
215
+ }
216
+ }
217
+
218
+ // Submit tab
219
+ if (currentTab === questions.length) {
220
+ if (matchesKey(data, Key.enter) && allAnswered()) {
221
+ submit(false);
222
+ } else if (matchesKey(data, Key.escape)) {
223
+ submit(true);
224
+ }
225
+ return;
226
+ }
227
+
228
+ // Digit-key direct selection: 1-9 select that numbered option, 0 selects the 10th.
229
+ if (q && data.length === 1 && data >= "0" && data <= "9") {
230
+ const n = data === "0" ? 10 : Number(data);
231
+ const target = n - 1;
232
+ if (target < opts.length) {
233
+ optionIndex = target;
234
+ selectOption(q, opts, target);
235
+ }
236
+ return;
237
+ }
238
+
239
+ // Option navigation
240
+ if (matchesKey(data, Key.up)) {
241
+ optionIndex = Math.max(0, optionIndex - 1);
242
+ refresh();
243
+ return;
244
+ }
245
+ if (matchesKey(data, Key.down)) {
246
+ optionIndex = Math.min(opts.length - 1, optionIndex + 1);
247
+ refresh();
248
+ return;
249
+ }
250
+
251
+ // Select option
252
+ if (matchesKey(data, Key.enter) && q) {
253
+ selectOption(q, opts, optionIndex);
254
+ return;
255
+ }
256
+
257
+ // Cancel
258
+ if (matchesKey(data, Key.escape)) {
259
+ submit(true);
260
+ }
261
+ }
262
+
263
+ function render(width: number): string[] {
264
+ if (cachedLines) return cachedLines;
265
+
266
+ const lines: string[] = [];
267
+ const renderWidth = Math.max(1, width);
268
+ const q = currentQuestion();
269
+ const opts = currentOptions();
270
+
271
+ function addWrapped(text: string) {
272
+ lines.push(...wrapTextWithAnsi(text, renderWidth));
273
+ }
274
+
275
+ function addWrappedWithPrefix(prefix: string, text: string) {
276
+ const prefixWidth = visibleWidth(prefix);
277
+ if (prefixWidth >= renderWidth) {
278
+ addWrapped(prefix + text);
279
+ return;
280
+ }
281
+ const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth);
282
+ const continuationPrefix = " ".repeat(prefixWidth);
283
+ for (let i = 0; i < wrapped.length; i++) {
284
+ lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`);
285
+ }
286
+ }
287
+
288
+ lines.push(theme.fg("accent", "─".repeat(renderWidth)));
289
+
290
+ // Tab bar (multi-question only)
291
+ if (isMulti) {
292
+ const tabs: string[] = ["← "];
293
+ for (let i = 0; i < questions.length; i++) {
294
+ const isActive = i === currentTab;
295
+ const isAnswered = answers.has(questions[i].id);
296
+ const lbl = questions[i].label;
297
+ const box = isAnswered ? "■" : "□";
298
+ const color = isAnswered ? "success" : "muted";
299
+ const text = ` ${box} ${lbl} `;
300
+ const styled = isActive ? theme.bg("selectedBg", theme.fg("text", text)) : theme.fg(color, text);
301
+ tabs.push(`${styled} `);
302
+ }
303
+ const canSubmit = allAnswered();
304
+ const isSubmitTab = currentTab === questions.length;
305
+ const submitText = " ✓ Submit ";
306
+ const submitStyled = isSubmitTab
307
+ ? theme.bg("selectedBg", theme.fg("text", submitText))
308
+ : theme.fg(canSubmit ? "success" : "dim", submitText);
309
+ tabs.push(`${submitStyled} →`);
310
+ addWrappedWithPrefix(" ", tabs.join(""));
311
+ lines.push("");
312
+ }
313
+
314
+ // Helper to render options list
315
+ function renderOptions() {
316
+ for (let i = 0; i < opts.length; i++) {
317
+ const opt = opts[i];
318
+ const selected = i === optionIndex;
319
+ const isOther = opt.isOther === true;
320
+ const prefix = selected ? theme.fg("accent", "> ") : " ";
321
+ const label = `${i + 1}. ${opt.label}${isOther && inputMode ? " ✎" : ""}`;
322
+ const color = selected || (isOther && inputMode) ? "accent" : "text";
323
+
324
+ addWrappedWithPrefix(prefix, theme.fg(color, label));
325
+ if (opt.description) {
326
+ addWrappedWithPrefix(" ", theme.fg("muted", opt.description));
327
+ }
328
+ }
329
+ }
330
+
331
+ // Content
332
+ if (inputMode && q) {
333
+ addWrappedWithPrefix(" ", theme.fg("text", q.prompt));
334
+ lines.push("");
335
+ // Show options for reference
336
+ renderOptions();
337
+ lines.push("");
338
+ addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:"));
339
+ for (const line of editor.render(Math.max(1, renderWidth - 2))) {
340
+ lines.push(` ${line}`);
341
+ }
342
+ lines.push("");
343
+ addWrappedWithPrefix(" ", theme.fg("dim", "Enter to submit • Esc to cancel"));
344
+ } else if (currentTab === questions.length) {
345
+ addWrappedWithPrefix(" ", theme.fg("accent", theme.bold("Ready to submit")));
346
+ lines.push("");
347
+ for (const question of questions) {
348
+ const answer = answers.get(question.id);
349
+ if (answer) {
350
+ const prefix = answer.wasCustom ? "(wrote) " : "";
351
+ const summary = `${theme.fg("muted", `${question.label}: `)}${theme.fg("text", prefix + answer.label)}`;
352
+ addWrappedWithPrefix(" ", summary);
353
+ }
354
+ }
355
+ lines.push("");
356
+ if (allAnswered()) {
357
+ addWrappedWithPrefix(" ", theme.fg("success", "Press Enter to submit"));
358
+ } else {
359
+ const missing = questions
360
+ .filter((q) => !answers.has(q.id))
361
+ .map((q) => q.label)
362
+ .join(", ");
363
+ addWrappedWithPrefix(" ", theme.fg("warning", `Unanswered: ${missing}`));
364
+ }
365
+ } else if (q) {
366
+ addWrappedWithPrefix(" ", theme.fg("text", q.prompt));
367
+ lines.push("");
368
+ renderOptions();
369
+ }
370
+
371
+ lines.push("");
372
+ if (!inputMode) {
373
+ const help = isMulti
374
+ ? "Tab/←→ navigate • ↑↓ or 1-9 select • Enter confirm • Esc cancel"
375
+ : "↑↓ or 1-9 select • Enter confirm • Esc cancel";
376
+ addWrappedWithPrefix(" ", theme.fg("dim", help));
377
+ }
378
+ lines.push(theme.fg("accent", "─".repeat(renderWidth)));
379
+
380
+ cachedLines = lines;
381
+ return lines;
382
+ }
383
+
384
+ return {
385
+ render,
386
+ invalidate: () => {
387
+ cachedLines = undefined;
388
+ },
389
+ handleInput,
390
+ };
391
+ });
392
+
393
+ if (result.cancelled) {
394
+ return {
395
+ content: [{ type: "text", text: "User cancelled the questionnaire" }],
396
+ details: result,
397
+ };
398
+ }
399
+
400
+ const answerLines = result.answers.map((a) => {
401
+ const qLabel = questions.find((q) => q.id === a.id)?.label || a.id;
402
+ if (a.wasCustom) {
403
+ return `${qLabel}: user wrote: ${a.label}`;
404
+ }
405
+ return `${qLabel}: user selected: ${a.index}. ${a.label}`;
406
+ });
407
+
408
+ return {
409
+ content: [{ type: "text", text: answerLines.join("\n") }],
410
+ details: result,
411
+ };
412
+ },
413
+
414
+ renderCall(args, theme, _context) {
415
+ const qs = (args.questions as Question[]) || [];
416
+ const count = qs.length;
417
+ const labels = qs.map((q) => q.label || q.id).join(", ");
418
+ let text = theme.fg("toolTitle", theme.bold("questionnaire "));
419
+ text += theme.fg("muted", `${count} question${count !== 1 ? "s" : ""}`);
420
+ if (labels) {
421
+ text += theme.fg("dim", ` (${labels})`);
422
+ }
423
+ return new Text(text, 0, 0);
424
+ },
425
+
426
+ renderResult(result, _options, theme, _context) {
427
+ const details = result.details as QuestionnaireResult | undefined;
428
+ if (!details) {
429
+ const text = result.content[0];
430
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
431
+ }
432
+ if (details.cancelled) {
433
+ return new Text(theme.fg("warning", "Cancelled"), 0, 0);
434
+ }
435
+ const lines = details.answers.map((a) => {
436
+ if (a.wasCustom) {
437
+ return `${theme.fg("success", "✓ ")}${theme.fg("accent", a.id)}: ${theme.fg("muted", "(wrote) ")}${a.label}`;
438
+ }
439
+ const display = a.index ? `${a.index}. ${a.label}` : a.label;
440
+ return `${theme.fg("success", "✓ ")}${theme.fg("accent", a.id)}: ${display}`;
441
+ });
442
+ return new Text(lines.join("\n"), 0, 0);
443
+ },
444
+ });
445
+ }
@@ -0,0 +1,55 @@
1
+ /** Shared types and schemas for the questionnaire tool. */
2
+
3
+ import { Type } from "typebox";
4
+
5
+ export interface QuestionOption {
6
+ value: string;
7
+ label: string;
8
+ description?: string;
9
+ }
10
+
11
+ export type RenderOption = QuestionOption & { isOther?: boolean };
12
+
13
+ export interface Question {
14
+ id: string;
15
+ label: string;
16
+ prompt: string;
17
+ options: QuestionOption[];
18
+ allowOther: boolean;
19
+ }
20
+
21
+ export interface Answer {
22
+ id: string;
23
+ value: string;
24
+ label: string;
25
+ wasCustom: boolean;
26
+ index?: number;
27
+ }
28
+
29
+ export interface QuestionnaireResult {
30
+ questions: Question[];
31
+ answers: Answer[];
32
+ cancelled: boolean;
33
+ }
34
+
35
+ export const QuestionOptionSchema = Type.Object({
36
+ value: Type.String({ description: "The value returned when selected" }),
37
+ label: Type.String({ description: "Display label for the option" }),
38
+ description: Type.Optional(Type.String({ description: "Optional description shown below label" })),
39
+ });
40
+
41
+ export const QuestionSchema = Type.Object({
42
+ id: Type.String({ description: "Unique identifier for this question" }),
43
+ label: Type.Optional(
44
+ Type.String({
45
+ description: "Short contextual label for tab bar, e.g. 'Scope', 'Priority' (defaults to Q1, Q2)",
46
+ }),
47
+ ),
48
+ prompt: Type.String({ description: "The full question text to display" }),
49
+ options: Type.Array(QuestionOptionSchema, { description: "Available options to choose from" }),
50
+ allowOther: Type.Optional(Type.Boolean({ description: "Allow 'Type something' option (default: true)" })),
51
+ });
52
+
53
+ export const QuestionnaireParams = Type.Object({
54
+ questions: Type.Array(QuestionSchema, { description: "Questions to ask the user" }),
55
+ });