pi-ui-extend 0.1.32 → 0.1.34

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 (95) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +2 -0
  3. package/dist/app/app.js +28 -0
  4. package/dist/app/commands/command-session-actions.js +29 -1
  5. package/dist/app/constants.d.ts +1 -1
  6. package/dist/app/constants.js +2 -2
  7. package/dist/app/icons.d.ts +4 -9
  8. package/dist/app/icons.js +12 -35
  9. package/dist/app/model/model-usage-status.d.ts +2 -1
  10. package/dist/app/model/model-usage-status.js +33 -25
  11. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  12. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  13. package/dist/app/rendering/conversation-tool-renderer.js +12 -18
  14. package/dist/app/rendering/conversation-viewport.d.ts +4 -0
  15. package/dist/app/rendering/conversation-viewport.js +144 -13
  16. package/dist/app/rendering/dcp-stats.js +42 -16
  17. package/dist/app/rendering/render-controller.js +4 -0
  18. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  19. package/dist/app/rendering/status-line-renderer.js +36 -1
  20. package/dist/app/rendering/tab-line-renderer.js +2 -2
  21. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  22. package/dist/app/rendering/tool-block-renderer.js +37 -11
  23. package/dist/app/runtime.js +1 -1
  24. package/dist/app/screen/mouse-controller.d.ts +5 -1
  25. package/dist/app/screen/mouse-controller.js +16 -0
  26. package/dist/app/screen/scroll-controller.d.ts +20 -0
  27. package/dist/app/screen/scroll-controller.js +127 -10
  28. package/dist/app/session/lazy-session-manager.js +35 -5
  29. package/dist/app/session/pix-system-message.d.ts +1 -0
  30. package/dist/app/session/pix-system-message.js +14 -3
  31. package/dist/app/session/queued-message-controller.d.ts +11 -4
  32. package/dist/app/session/queued-message-controller.js +74 -59
  33. package/dist/app/session/queued-message-entries.d.ts +2 -1
  34. package/dist/app/session/queued-message-entries.js +12 -1
  35. package/dist/app/session/session-event-controller.d.ts +42 -1
  36. package/dist/app/session/session-event-controller.js +500 -31
  37. package/dist/app/session/session-history.js +23 -4
  38. package/dist/app/session/tabs-controller.d.ts +11 -1
  39. package/dist/app/session/tabs-controller.js +102 -21
  40. package/dist/app/types.d.ts +14 -1
  41. package/dist/bundled-extensions/question/contract.d.ts +25 -0
  42. package/dist/bundled-extensions/question/contract.js +94 -0
  43. package/dist/bundled-extensions/question/index.d.ts +7 -0
  44. package/dist/bundled-extensions/question/index.js +28 -0
  45. package/dist/bundled-extensions/question/render.d.ts +4 -0
  46. package/dist/bundled-extensions/question/render.js +27 -0
  47. package/dist/bundled-extensions/question/result.d.ts +6 -0
  48. package/dist/bundled-extensions/question/result.js +84 -0
  49. package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
  50. package/dist/bundled-extensions/question/tool-description.js +11 -0
  51. package/dist/bundled-extensions/question/tui.d.ts +2 -0
  52. package/dist/bundled-extensions/question/tui.js +577 -0
  53. package/dist/bundled-extensions/question/types.d.ts +103 -0
  54. package/dist/bundled-extensions/question/types.js +1 -0
  55. package/dist/bundled-extensions/session-title/config.d.ts +17 -0
  56. package/dist/bundled-extensions/session-title/config.js +150 -0
  57. package/dist/bundled-extensions/session-title/index.d.ts +5 -0
  58. package/dist/bundled-extensions/session-title/index.js +384 -0
  59. package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
  60. package/dist/bundled-extensions/session-title/title-generation.js +141 -0
  61. package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
  62. package/dist/bundled-extensions/terminal-bell/index.js +491 -0
  63. package/dist/config.d.ts +1 -1
  64. package/dist/config.js +2 -1
  65. package/dist/default-pix-config.js +2 -1
  66. package/dist/icon-theme.d.ts +7 -0
  67. package/dist/icon-theme.js +36 -0
  68. package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
  69. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  70. package/dist/schemas/pix-schema.d.ts +1 -0
  71. package/dist/schemas/pix-schema.js +1 -0
  72. package/external/pi-tools-suite/README.md +7 -7
  73. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
  74. package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
  75. package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
  76. package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
  77. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
  78. package/external/pi-tools-suite/src/dcp/config.ts +14 -14
  79. package/external/pi-tools-suite/src/dcp/index.ts +31 -43
  80. package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
  81. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
  82. package/external/pi-tools-suite/src/tool-descriptions.ts +34 -54
  83. package/package.json +3 -2
  84. package/schemas/pi-tools-suite.json +14 -0
  85. package/schemas/pix.json +7 -0
  86. package/extensions/question/contract.ts +0 -100
  87. package/extensions/question/index.ts +0 -34
  88. package/extensions/question/render.ts +0 -28
  89. package/extensions/question/result.ts +0 -86
  90. package/extensions/question/tool-description.ts +0 -11
  91. package/extensions/question/tui.ts +0 -629
  92. package/extensions/question/types.ts +0 -123
  93. package/extensions/session-title/config.ts +0 -164
  94. package/extensions/session-title/index.ts +0 -502
  95. package/extensions/terminal-bell/index.ts +0 -345
@@ -0,0 +1,577 @@
1
+ import * as PiTui from "@earendil-works/pi-tui";
2
+ import { CUSTOM_ANSWER_LABEL } from "./contract.js";
3
+ function isKey(data, key) {
4
+ const tui = PiTui;
5
+ const keyValue = key === "shift+enter" && typeof tui.Key?.shift === "function" ? tui.Key.shift("enter") : tui.Key?.[key];
6
+ if (typeof keyValue === "string" && tui.matchesKey?.(data, keyValue))
7
+ return true;
8
+ if (data === key)
9
+ return true;
10
+ const aliases = {
11
+ up: ["\u001b[A"],
12
+ down: ["\u001b[B"],
13
+ right: ["\u001b[C"],
14
+ left: ["\u001b[D"],
15
+ enter: ["\r", "\n"],
16
+ escape: ["\u001b"],
17
+ backspace: ["\u007f", "\b"],
18
+ tab: ["\t"],
19
+ "shift+tab": ["\u001b[Z"],
20
+ "shift+enter": ["\u001b[13;2u", "\u001b[13;2~", "\u001b[27;2;13~", "\u001b\r", "\u001b\n"],
21
+ };
22
+ return aliases[key]?.includes(data) ?? false;
23
+ }
24
+ function truncateLine(line, width, suffix = "…") {
25
+ const truncateToWidth = PiTui.truncateToWidth;
26
+ if (truncateToWidth)
27
+ return truncateToWidth(line, width, suffix);
28
+ if (line.length <= width)
29
+ return line;
30
+ return `${line.slice(0, Math.max(0, width - suffix.length))}${suffix}`;
31
+ }
32
+ function wrapLine(line, width) {
33
+ const wrapTextWithAnsi = PiTui.wrapTextWithAnsi;
34
+ if (wrapTextWithAnsi)
35
+ return wrapTextWithAnsi(line, width);
36
+ if (width <= 0)
37
+ return [""];
38
+ if (line.length <= width)
39
+ return [line];
40
+ const words = line.split(/(\s+)/);
41
+ const lines = [];
42
+ let current = "";
43
+ for (const word of words) {
44
+ if (current.length + word.length <= width) {
45
+ current += word;
46
+ continue;
47
+ }
48
+ if (current.trimEnd())
49
+ lines.push(current.trimEnd());
50
+ if (word.length > width) {
51
+ for (let index = 0; index < word.length; index += width)
52
+ lines.push(word.slice(index, index + width));
53
+ current = "";
54
+ }
55
+ else {
56
+ current = word.trimStart();
57
+ }
58
+ }
59
+ if (current.trimEnd())
60
+ lines.push(current.trimEnd());
61
+ return lines.length > 0 ? lines : [""];
62
+ }
63
+ function stripAnsi(text) {
64
+ return text.replace(/\u001b\[[0-9;]*m/g, "");
65
+ }
66
+ function visibleLength(text) {
67
+ return stripAnsi(text).length;
68
+ }
69
+ function styleText(theme, text, options) {
70
+ if (theme.style)
71
+ return theme.style(text, options);
72
+ let styled = text;
73
+ if (options.foreground)
74
+ styled = theme.fg(options.foreground, styled);
75
+ if (options.background && theme.bg)
76
+ styled = theme.bg(options.background, styled);
77
+ if (options.bold && theme.bold)
78
+ styled = theme.bold(styled);
79
+ return styled;
80
+ }
81
+ function padVisible(text, width) {
82
+ return `${text}${" ".repeat(Math.max(0, width - visibleLength(text)))}`;
83
+ }
84
+ function paintLine(theme, text, width, options = {}) {
85
+ return styleText(theme, padVisible(truncateLine(text, width), width), options);
86
+ }
87
+ function formatHeader(title, width) {
88
+ const titleText = title.replace(/\s+/g, " ").trim() || "Question";
89
+ return truncateLine(titleText, width);
90
+ }
91
+ function clampIndex(index, length) {
92
+ return Math.max(0, Math.min(Math.max(0, length - 1), index));
93
+ }
94
+ export async function runQuestionnaire(questions, ctx) {
95
+ return ctx.ui.custom((tui, theme, _keybindings, done) => {
96
+ const selections = new Map();
97
+ const customDrafts = new Map();
98
+ const reviewSubmitIndex = questions.length;
99
+ const pixCapabilities = tui.pix;
100
+ const usesSharedEditor = Boolean(pixCapabilities?.delegatedEditorInput && ctx.ui.setEditorText && ctx.ui.getEditorText);
101
+ let questionIndex = 0;
102
+ let selectedChoiceIndex = 0;
103
+ let selectedReviewIndex = reviewSubmitIndex;
104
+ let mode = "choices";
105
+ let customError;
106
+ let cachedWidth;
107
+ let cachedLines;
108
+ let clickZones = [];
109
+ function currentQuestion() {
110
+ return questions[questionIndex];
111
+ }
112
+ function invalidateCache() {
113
+ cachedWidth = undefined;
114
+ cachedLines = undefined;
115
+ }
116
+ function refresh() {
117
+ invalidateCache();
118
+ tui.requestRender();
119
+ }
120
+ function customAnswerIndex(question = currentQuestion()) {
121
+ return question.choices.length;
122
+ }
123
+ function sharedEditorText() {
124
+ if (usesSharedEditor)
125
+ return ctx.ui.getEditorText?.() ?? "";
126
+ return customDrafts.get(currentQuestion().id) ?? "";
127
+ }
128
+ function setSharedEditorText(text) {
129
+ customDrafts.set(currentQuestion().id, text);
130
+ if (usesSharedEditor)
131
+ ctx.ui.setEditorText?.(text);
132
+ }
133
+ function clearSharedEditorText() {
134
+ if (usesSharedEditor)
135
+ ctx.ui.setEditorText?.("");
136
+ }
137
+ function captureCustomDraft() {
138
+ if (mode !== "custom")
139
+ return;
140
+ customDrafts.set(currentQuestion().id, sharedEditorText());
141
+ }
142
+ function getCompleteSelections() {
143
+ const orderedSelections = [];
144
+ for (const question of questions) {
145
+ const selection = selections.get(question.id);
146
+ if (!selection)
147
+ return undefined;
148
+ orderedSelections.push(selection);
149
+ }
150
+ return orderedSelections;
151
+ }
152
+ function firstUnansweredIndex() {
153
+ return questions.findIndex((question) => !selections.has(question.id));
154
+ }
155
+ function submitCompleteSelections() {
156
+ const completeSelections = getCompleteSelections();
157
+ if (completeSelections)
158
+ done(completeSelections);
159
+ }
160
+ function submitOrAnswerRemaining() {
161
+ const firstUnanswered = firstUnansweredIndex();
162
+ if (firstUnanswered !== -1) {
163
+ moveToQuestion(firstUnanswered);
164
+ return;
165
+ }
166
+ submitCompleteSelections();
167
+ }
168
+ function formatReviewAnswerLabel(question, selection) {
169
+ if ("customText" in selection)
170
+ return `${CUSTOM_ANSWER_LABEL}: ${selection.customText}`;
171
+ return question.choices.find((choice) => choice.value === selection.choiceValue)?.label ?? "Unknown";
172
+ }
173
+ function selectionIndexForQuestion(question) {
174
+ const selection = selections.get(question.id);
175
+ if (!selection)
176
+ return 0;
177
+ if ("customText" in selection)
178
+ return customAnswerIndex(question);
179
+ const choiceIndex = question.choices.findIndex((choice) => choice.value === selection.choiceValue);
180
+ return choiceIndex === -1 ? 0 : choiceIndex;
181
+ }
182
+ function syncChoiceSelection() {
183
+ selectedChoiceIndex = selectionIndexForQuestion(currentQuestion());
184
+ }
185
+ function renderSelectableLine(add, selected, text, zone, width, foreground = "text") {
186
+ const marker = selected ? "›" : " ";
187
+ const line = ` ${marker} ${text}`;
188
+ const row = add(selected
189
+ ? paintLine(theme, line, width, { foreground: "selectedText", background: "selectedBg", bold: true })
190
+ : paintLine(theme, line, width, { foreground }));
191
+ clickZones.push({ ...zone, row, startColumn: 1, endColumn: width + 1 });
192
+ }
193
+ function renderMutedLine(add, text, width) {
194
+ add(paintLine(theme, text, width, { foreground: "muted" }));
195
+ }
196
+ function moveToQuestion(index) {
197
+ captureCustomDraft();
198
+ clearSharedEditorText();
199
+ questionIndex = clampIndex(index, questions.length);
200
+ mode = "choices";
201
+ customError = undefined;
202
+ syncChoiceSelection();
203
+ refresh();
204
+ }
205
+ function goBack() {
206
+ if (mode === "custom") {
207
+ captureCustomDraft();
208
+ clearSharedEditorText();
209
+ mode = "choices";
210
+ customError = undefined;
211
+ syncChoiceSelection();
212
+ refresh();
213
+ return;
214
+ }
215
+ if (mode === "review") {
216
+ moveToQuestion(questions.length - 1);
217
+ return;
218
+ }
219
+ if (questionIndex > 0)
220
+ moveToQuestion(questionIndex - 1);
221
+ else
222
+ done(null);
223
+ }
224
+ function showReview() {
225
+ captureCustomDraft();
226
+ clearSharedEditorText();
227
+ mode = "review";
228
+ const firstUnanswered = firstUnansweredIndex();
229
+ selectedReviewIndex = firstUnanswered === -1 ? reviewSubmitIndex : firstUnanswered;
230
+ refresh();
231
+ }
232
+ function advanceAfterAnswer() {
233
+ if (questions.length === 1) {
234
+ submitCompleteSelections();
235
+ return;
236
+ }
237
+ if (questionIndex < questions.length - 1) {
238
+ moveToQuestion(questionIndex + 1);
239
+ return;
240
+ }
241
+ showReview();
242
+ }
243
+ function enterCustomMode() {
244
+ const question = currentQuestion();
245
+ const existing = selections.get(question.id);
246
+ const prefill = existing && "customText" in existing ? existing.customText : customDrafts.get(question.id) ?? "";
247
+ mode = "custom";
248
+ selectedChoiceIndex = customAnswerIndex(question);
249
+ customError = undefined;
250
+ setSharedEditorText(prefill);
251
+ refresh();
252
+ }
253
+ function selectChoice(index) {
254
+ const question = currentQuestion();
255
+ const choice = question.choices[index];
256
+ if (choice) {
257
+ selections.set(question.id, { id: question.id, choiceValue: choice.value });
258
+ customDrafts.delete(question.id);
259
+ clearSharedEditorText();
260
+ advanceAfterAnswer();
261
+ return;
262
+ }
263
+ if (index === question.choices.length)
264
+ enterCustomMode();
265
+ }
266
+ function submitCustomAnswer() {
267
+ const text = sharedEditorText();
268
+ const trimmed = text.trim();
269
+ if (!trimmed) {
270
+ customError = "Custom Answer cannot be empty.";
271
+ refresh();
272
+ return;
273
+ }
274
+ const question = currentQuestion();
275
+ selections.set(question.id, { id: question.id, customText: trimmed });
276
+ customDrafts.delete(question.id);
277
+ clearSharedEditorText();
278
+ advanceAfterAnswer();
279
+ }
280
+ function updateChoiceSelection(index) {
281
+ const maxIndex = customAnswerIndex();
282
+ const nextIndex = Math.max(0, Math.min(maxIndex, index));
283
+ if (nextIndex === selectedChoiceIndex)
284
+ return;
285
+ selectedChoiceIndex = nextIndex;
286
+ refresh();
287
+ }
288
+ function updateReviewSelection(index) {
289
+ const nextIndex = Math.max(0, Math.min(reviewSubmitIndex, index));
290
+ if (nextIndex === selectedReviewIndex)
291
+ return;
292
+ selectedReviewIndex = nextIndex;
293
+ refresh();
294
+ }
295
+ function activeTab() {
296
+ return mode === "review" ? "review" : questionIndex;
297
+ }
298
+ function activateTab(target) {
299
+ if (target === "review")
300
+ showReview();
301
+ else
302
+ moveToQuestion(target);
303
+ }
304
+ function moveTab(delta) {
305
+ const targets = [...questions.map((_, index) => index), "review"];
306
+ const current = activeTab();
307
+ const currentIndex = Math.max(0, targets.findIndex((target) => target === current));
308
+ const nextIndex = (currentIndex + delta + targets.length) % targets.length;
309
+ activateTab(targets[nextIndex]);
310
+ }
311
+ function handleTabNavigation(data) {
312
+ if (isKey(data, "tab")) {
313
+ moveTab(1);
314
+ return true;
315
+ }
316
+ if (isKey(data, "shift+tab")) {
317
+ moveTab(-1);
318
+ return true;
319
+ }
320
+ return false;
321
+ }
322
+ function handleCustomInput(data) {
323
+ if (handleTabNavigation(data))
324
+ return;
325
+ if (isKey(data, "escape")) {
326
+ goBack();
327
+ return;
328
+ }
329
+ if (isKey(data, "shift+enter")) {
330
+ if (usesSharedEditor)
331
+ return { consume: false };
332
+ setSharedEditorText(`${sharedEditorText()}\n`);
333
+ customError = undefined;
334
+ refresh();
335
+ return;
336
+ }
337
+ if (isKey(data, "enter")) {
338
+ submitCustomAnswer();
339
+ return;
340
+ }
341
+ if (usesSharedEditor)
342
+ return { consume: false };
343
+ if (isKey(data, "backspace")) {
344
+ setSharedEditorText(sharedEditorText().slice(0, -1));
345
+ customError = undefined;
346
+ refresh();
347
+ return;
348
+ }
349
+ if (data >= " ") {
350
+ setSharedEditorText(sharedEditorText() + data);
351
+ customError = undefined;
352
+ refresh();
353
+ }
354
+ }
355
+ function handleReviewInput(data) {
356
+ if (handleTabNavigation(data))
357
+ return;
358
+ if (isKey(data, "left")) {
359
+ moveTab(-1);
360
+ return;
361
+ }
362
+ if (isKey(data, "right")) {
363
+ moveTab(1);
364
+ return;
365
+ }
366
+ if (isKey(data, "up")) {
367
+ updateReviewSelection(selectedReviewIndex - 1);
368
+ return;
369
+ }
370
+ if (isKey(data, "down")) {
371
+ updateReviewSelection(selectedReviewIndex + 1);
372
+ return;
373
+ }
374
+ if (data === "s") {
375
+ submitOrAnswerRemaining();
376
+ return;
377
+ }
378
+ if (data === "b" || isKey(data, "backspace")) {
379
+ goBack();
380
+ return;
381
+ }
382
+ if (isKey(data, "enter")) {
383
+ if (selectedReviewIndex === reviewSubmitIndex) {
384
+ submitOrAnswerRemaining();
385
+ return;
386
+ }
387
+ moveToQuestion(selectedReviewIndex);
388
+ return;
389
+ }
390
+ if (isKey(data, "escape"))
391
+ done(null);
392
+ }
393
+ function handleChoiceInput(data) {
394
+ if (handleTabNavigation(data))
395
+ return;
396
+ if (isKey(data, "left")) {
397
+ moveTab(-1);
398
+ return;
399
+ }
400
+ if (isKey(data, "right")) {
401
+ moveTab(1);
402
+ return;
403
+ }
404
+ if (isKey(data, "up")) {
405
+ updateChoiceSelection(selectedChoiceIndex - 1);
406
+ return;
407
+ }
408
+ if (isKey(data, "down")) {
409
+ updateChoiceSelection(selectedChoiceIndex + 1);
410
+ return;
411
+ }
412
+ if (data === "b" || isKey(data, "backspace")) {
413
+ goBack();
414
+ return;
415
+ }
416
+ if (isKey(data, "enter")) {
417
+ selectChoice(selectedChoiceIndex);
418
+ return;
419
+ }
420
+ if (isKey(data, "escape")) {
421
+ done(null);
422
+ return;
423
+ }
424
+ if (/^[1-9]$/.test(data)) {
425
+ const index = Number(data) - 1;
426
+ if (index <= customAnswerIndex())
427
+ selectChoice(index);
428
+ }
429
+ }
430
+ function renderHeader(add, title, width) {
431
+ add(paintLine(theme, formatHeader(title, width), width, { foreground: "accent", background: "headerBg", bold: true }));
432
+ }
433
+ function renderTabs(add, width) {
434
+ let plain = " ";
435
+ const styledParts = [" "];
436
+ const zones = [];
437
+ const active = activeTab();
438
+ for (let index = 0; index < questions.length; index += 1) {
439
+ const answered = selections.has(questions[index].id);
440
+ const label = ` ${index + 1}${answered ? "✓" : "·"} `;
441
+ const startColumn = visibleLength(plain) + 1;
442
+ plain += label;
443
+ const endColumn = visibleLength(plain) + 1;
444
+ styledParts.push(active === index
445
+ ? styleText(theme, label, { foreground: "selectedText", background: "selectedBg", bold: true })
446
+ : theme.fg(answered ? "success" : "muted", label));
447
+ zones.push({ kind: "tab", target: index, startColumn, endColumn });
448
+ plain += " ";
449
+ styledParts.push(" ");
450
+ }
451
+ const reviewComplete = Boolean(getCompleteSelections());
452
+ const reviewLabel = " Review ";
453
+ const reviewStart = visibleLength(plain) + 1;
454
+ plain += reviewLabel;
455
+ const reviewEnd = visibleLength(plain) + 1;
456
+ styledParts.push(active === "review"
457
+ ? styleText(theme, reviewLabel, { foreground: "selectedText", background: "selectedBg", bold: true })
458
+ : theme.fg(reviewComplete ? "success" : "muted", reviewLabel));
459
+ zones.push({ kind: "tab", target: "review", startColumn: reviewStart, endColumn: reviewEnd });
460
+ const renderedRow = add(padVisible(styledParts.join(""), width));
461
+ for (const zone of zones) {
462
+ if (zone.startColumn <= width)
463
+ clickZones.push({ ...zone, row: renderedRow, endColumn: Math.min(zone.endColumn, width + 1) });
464
+ }
465
+ }
466
+ function renderSeparator(add, width) {
467
+ add(paintLine(theme, "─".repeat(width), width, { foreground: "muted" }));
468
+ }
469
+ function renderReview(add, addWrapped, width) {
470
+ renderHeader(add, "Review answers", width);
471
+ renderTabs(add, width);
472
+ renderSeparator(add, width);
473
+ questions.forEach((question, index) => {
474
+ const answer = selections.get(question.id);
475
+ const label = answer ? formatReviewAnswerLabel(question, answer) : "Unanswered";
476
+ const status = answer ? "✓" : "·";
477
+ renderSelectableLine(add, index === selectedReviewIndex, `${status} ${index + 1}. ${question.label}: ${label}`, { kind: "review", index }, width, answer ? "success" : "warning");
478
+ });
479
+ const isComplete = Boolean(getCompleteSelections());
480
+ renderSeparator(add, width);
481
+ renderSelectableLine(add, selectedReviewIndex === reviewSubmitIndex, isComplete ? "Submit answers" : "Answer remaining questions", { kind: "submit" }, width, isComplete ? "success" : "warning");
482
+ }
483
+ function renderQuestion(add, addWrapped, width) {
484
+ const question = currentQuestion();
485
+ renderHeader(add, `${questionIndex + 1}/${questions.length} ${question.label}`, width);
486
+ renderTabs(add, width);
487
+ renderSeparator(add, width);
488
+ addWrapped(theme.fg("info", ` ${question.prompt}`));
489
+ question.choices.forEach((choice, index) => {
490
+ renderSelectableLine(add, mode === "choices" && index === selectedChoiceIndex, `${index + 1}. ${choice.label}`, { kind: "choice", index }, width, "warning");
491
+ if (choice.description)
492
+ renderMutedLine(add, ` ${choice.description}`, width);
493
+ });
494
+ renderSelectableLine(add, mode === "choices" && selectedChoiceIndex === customAnswerIndex(), `${question.choices.length + 1}. ${CUSTOM_ANSWER_LABEL}`, { kind: "custom" }, width, "warning");
495
+ if (mode === "custom") {
496
+ if (!usesSharedEditor) {
497
+ (sharedEditorText() || " ").split("\n").forEach((line) => addWrapped(theme.fg("text", ` ${line}`)));
498
+ }
499
+ if (customError && !sharedEditorText().trim())
500
+ addWrapped(theme.fg("warning", ` ${customError}`));
501
+ }
502
+ }
503
+ function handleMouse(event) {
504
+ if (!event.released)
505
+ return false;
506
+ const zone = clickZones.find((candidate) => (candidate.row === event.localRow
507
+ && event.localColumn >= candidate.startColumn
508
+ && event.localColumn < candidate.endColumn));
509
+ if (!zone)
510
+ return false;
511
+ switch (zone.kind) {
512
+ case "tab":
513
+ activateTab(zone.target);
514
+ return true;
515
+ case "choice":
516
+ selectedChoiceIndex = zone.index;
517
+ selectChoice(zone.index);
518
+ return true;
519
+ case "custom":
520
+ enterCustomMode();
521
+ return true;
522
+ case "review":
523
+ moveToQuestion(zone.index);
524
+ return true;
525
+ case "submit":
526
+ submitOrAnswerRemaining();
527
+ return true;
528
+ }
529
+ }
530
+ return {
531
+ handleInput(data) {
532
+ switch (mode) {
533
+ case "custom":
534
+ return handleCustomInput(data);
535
+ case "review":
536
+ handleReviewInput(data);
537
+ return;
538
+ case "choices":
539
+ handleChoiceInput(data);
540
+ }
541
+ },
542
+ handleMouse,
543
+ usesEditor() {
544
+ return mode === "custom" && usesSharedEditor;
545
+ },
546
+ invalidate() {
547
+ invalidateCache();
548
+ },
549
+ render(width) {
550
+ if (cachedLines && cachedWidth === width)
551
+ return cachedLines;
552
+ const safeWidth = Math.max(1, width);
553
+ const lines = [];
554
+ clickZones = [];
555
+ const add = (text) => {
556
+ const row = lines.length;
557
+ lines.push(text);
558
+ return row;
559
+ };
560
+ const addWrapped = (text) => {
561
+ const row = lines.length;
562
+ const wrapped = wrapLine(text, safeWidth);
563
+ for (const line of wrapped.length > 0 ? wrapped : [""])
564
+ lines.push(truncateLine(line, safeWidth));
565
+ return row;
566
+ };
567
+ if (mode === "review")
568
+ renderReview(add, addWrapped, safeWidth);
569
+ else
570
+ renderQuestion(add, addWrapped, safeWidth);
571
+ cachedWidth = width;
572
+ cachedLines = lines;
573
+ return lines;
574
+ },
575
+ };
576
+ });
577
+ }
@@ -0,0 +1,103 @@
1
+ export interface QuestionChoiceInput {
2
+ value: string;
3
+ label: string;
4
+ description?: string;
5
+ }
6
+ export interface QuestionInput {
7
+ id: string;
8
+ label: string;
9
+ prompt: string;
10
+ choices: QuestionChoiceInput[];
11
+ }
12
+ export interface QuestionToolInput {
13
+ questions: QuestionInput[];
14
+ }
15
+ export interface NormalizedQuestionChoice {
16
+ value: string;
17
+ label: string;
18
+ description?: string;
19
+ }
20
+ export interface NormalizedQuestion {
21
+ id: string;
22
+ label: string;
23
+ prompt: string;
24
+ choices: NormalizedQuestionChoice[];
25
+ }
26
+ export interface PredefinedQuestionSelection {
27
+ id: string;
28
+ choiceValue: string;
29
+ }
30
+ export interface CustomQuestionSelection {
31
+ id: string;
32
+ customText: string;
33
+ }
34
+ export type QuestionSelection = PredefinedQuestionSelection | CustomQuestionSelection;
35
+ export interface QuestionAnswer {
36
+ id: string;
37
+ value: string;
38
+ label: string;
39
+ wasCustom: boolean;
40
+ index?: number;
41
+ }
42
+ export interface SuccessfulQuestionResult {
43
+ answers: QuestionAnswer[];
44
+ canceled: false;
45
+ }
46
+ export interface CanceledQuestionResult {
47
+ answers: [];
48
+ canceled: true;
49
+ reason: "ui_unavailable" | "user_canceled";
50
+ fallbackPrompt?: string;
51
+ }
52
+ export type QuestionResultDetails = SuccessfulQuestionResult | CanceledQuestionResult;
53
+ export interface TextContent {
54
+ type: "text";
55
+ text: string;
56
+ }
57
+ export interface QuestionToolResult {
58
+ content: TextContent[];
59
+ details: QuestionResultDetails;
60
+ }
61
+ export interface QuestionUiContext {
62
+ hasUI?: boolean;
63
+ ui: {
64
+ custom<T>(factory: (tui: QuestionTui, theme: QuestionTheme, keybindings: unknown, done: (value: T) => void) => QuestionComponent): Promise<T>;
65
+ setEditorText?(text: string): void;
66
+ getEditorText?(): string;
67
+ notify?(message: string, level: "info" | "warning" | "error"): void;
68
+ };
69
+ }
70
+ export interface QuestionTui {
71
+ requestRender(): void;
72
+ }
73
+ export interface QuestionThemeStyleOptions {
74
+ foreground?: string;
75
+ background?: string;
76
+ bold?: boolean;
77
+ }
78
+ export interface QuestionTheme {
79
+ fg(color: string, text: string): string;
80
+ bg?(color: string, text: string): string;
81
+ bold?(text: string): string;
82
+ style?(text: string, options: QuestionThemeStyleOptions): string;
83
+ }
84
+ export interface QuestionMouseEvent {
85
+ button: number;
86
+ x: number;
87
+ y: number;
88
+ released: boolean;
89
+ localRow: number;
90
+ localColumn: number;
91
+ width: number;
92
+ }
93
+ export interface QuestionInputHandlingResult {
94
+ consume?: boolean;
95
+ data?: string;
96
+ }
97
+ export interface QuestionComponent {
98
+ handleInput(data: string): void | QuestionInputHandlingResult;
99
+ handleMouse?(event: QuestionMouseEvent): boolean | void;
100
+ usesEditor?(): boolean;
101
+ invalidate(): void;
102
+ render(width: number): string[];
103
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ export interface SessionTitleConfig {
2
+ enabled: boolean;
3
+ model: string;
4
+ fallbackModels: string[];
5
+ maxInputChars: number;
6
+ maxTitleChars: number;
7
+ maxTokens: number;
8
+ maxRetries: number;
9
+ generationAttempts: number;
10
+ retryDelayMs: number;
11
+ timeoutMs: number;
12
+ terminalTitle: boolean;
13
+ terminalTitlePrefix: string;
14
+ notify: boolean;
15
+ debug: boolean;
16
+ }
17
+ export declare function loadSessionTitleConfig(projectDir: string): SessionTitleConfig;