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.
- package/README.md +1 -1
- package/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +28 -0
- package/dist/app/commands/command-session-actions.js +29 -1
- package/dist/app/constants.d.ts +1 -1
- package/dist/app/constants.js +2 -2
- package/dist/app/icons.d.ts +4 -9
- package/dist/app/icons.js +12 -35
- package/dist/app/model/model-usage-status.d.ts +2 -1
- package/dist/app/model/model-usage-status.js +33 -25
- package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.js +12 -18
- package/dist/app/rendering/conversation-viewport.d.ts +4 -0
- package/dist/app/rendering/conversation-viewport.js +144 -13
- package/dist/app/rendering/dcp-stats.js +42 -16
- package/dist/app/rendering/render-controller.js +4 -0
- package/dist/app/rendering/status-line-renderer.d.ts +8 -1
- package/dist/app/rendering/status-line-renderer.js +36 -1
- package/dist/app/rendering/tab-line-renderer.js +2 -2
- package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
- package/dist/app/rendering/tool-block-renderer.js +37 -11
- package/dist/app/runtime.js +1 -1
- package/dist/app/screen/mouse-controller.d.ts +5 -1
- package/dist/app/screen/mouse-controller.js +16 -0
- package/dist/app/screen/scroll-controller.d.ts +20 -0
- package/dist/app/screen/scroll-controller.js +127 -10
- package/dist/app/session/lazy-session-manager.js +35 -5
- package/dist/app/session/pix-system-message.d.ts +1 -0
- package/dist/app/session/pix-system-message.js +14 -3
- package/dist/app/session/queued-message-controller.d.ts +11 -4
- package/dist/app/session/queued-message-controller.js +74 -59
- package/dist/app/session/queued-message-entries.d.ts +2 -1
- package/dist/app/session/queued-message-entries.js +12 -1
- package/dist/app/session/session-event-controller.d.ts +42 -1
- package/dist/app/session/session-event-controller.js +500 -31
- package/dist/app/session/session-history.js +23 -4
- package/dist/app/session/tabs-controller.d.ts +11 -1
- package/dist/app/session/tabs-controller.js +102 -21
- package/dist/app/types.d.ts +14 -1
- package/dist/bundled-extensions/question/contract.d.ts +25 -0
- package/dist/bundled-extensions/question/contract.js +94 -0
- package/dist/bundled-extensions/question/index.d.ts +7 -0
- package/dist/bundled-extensions/question/index.js +28 -0
- package/dist/bundled-extensions/question/render.d.ts +4 -0
- package/dist/bundled-extensions/question/render.js +27 -0
- package/dist/bundled-extensions/question/result.d.ts +6 -0
- package/dist/bundled-extensions/question/result.js +84 -0
- package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
- package/dist/bundled-extensions/question/tool-description.js +11 -0
- package/dist/bundled-extensions/question/tui.d.ts +2 -0
- package/dist/bundled-extensions/question/tui.js +577 -0
- package/dist/bundled-extensions/question/types.d.ts +103 -0
- package/dist/bundled-extensions/question/types.js +1 -0
- package/dist/bundled-extensions/session-title/config.d.ts +17 -0
- package/dist/bundled-extensions/session-title/config.js +150 -0
- package/dist/bundled-extensions/session-title/index.d.ts +5 -0
- package/dist/bundled-extensions/session-title/index.js +384 -0
- package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
- package/dist/bundled-extensions/session-title/title-generation.js +141 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
- package/dist/bundled-extensions/terminal-bell/index.js +491 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -1
- package/dist/default-pix-config.js +2 -1
- package/dist/icon-theme.d.ts +7 -0
- package/dist/icon-theme.js +36 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
- package/dist/schemas/pi-tools-suite-schema.js +5 -0
- package/dist/schemas/pix-schema.d.ts +1 -0
- package/dist/schemas/pix-schema.js +1 -0
- package/external/pi-tools-suite/README.md +7 -7
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
- package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
- package/external/pi-tools-suite/src/dcp/config.ts +14 -14
- package/external/pi-tools-suite/src/dcp/index.ts +31 -43
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
- package/external/pi-tools-suite/src/tool-descriptions.ts +34 -54
- package/package.json +3 -2
- package/schemas/pi-tools-suite.json +14 -0
- package/schemas/pix.json +7 -0
- package/extensions/question/contract.ts +0 -100
- package/extensions/question/index.ts +0 -34
- package/extensions/question/render.ts +0 -28
- package/extensions/question/result.ts +0 -86
- package/extensions/question/tool-description.ts +0 -11
- package/extensions/question/tui.ts +0 -629
- package/extensions/question/types.ts +0 -123
- package/extensions/session-title/config.ts +0 -164
- package/extensions/session-title/index.ts +0 -502
- 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;
|