moderado 0.4.46 → 0.4.51
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/dist/commands/chat.d.ts +3 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +27 -5
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/chat_slash.d.ts +1 -1
- package/dist/commands/chat_slash.d.ts.map +1 -1
- package/dist/commands/chat_slash.js +1 -0
- package/dist/commands/chat_slash.js.map +1 -1
- package/dist/turn_summary.js +1 -1
- package/dist/turn_summary.js.map +1 -1
- package/dist/ui/composer.d.ts +2 -1
- package/dist/ui/composer.d.ts.map +1 -1
- package/dist/ui/composer.js +235 -48
- package/dist/ui/composer.js.map +1 -1
- package/dist/ui/help_content.d.ts +1 -1
- package/dist/ui/help_content.js +1 -1
- package/dist/ui/help_content.js.map +1 -1
- package/dist/ui/welcome.d.ts +69 -15
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +232 -67
- package/dist/ui/welcome.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/ui/composer.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/ui/composer.ts"],"names":[],"mappings":"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"}
|
package/dist/ui/composer.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import readline from 'node:readline';
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import { activeMentionToken, filterMentionCandidates } from './file_mentions.js';
|
|
10
|
-
import { MOUSE_REPORT_OFF, MOUSE_REPORT_ON, MOUSE_REPORT_PATTERN,
|
|
10
|
+
import { KEY_MODIFIER_REPORT_OFF, KEY_MODIFIER_REPORT_ON, MOUSE_REPORT_OFF, MOUSE_REPORT_ON, MOUSE_REPORT_PATTERN, composerCaretIndex, composerView, exitCleanly, getChatComposerRows, getChatLayout, getChatCardWidth, getComposerInputMaxRows, getMatchingCommands, getWelcomeBottomPadding, getWelcomeCardWidth, getWelcomeIndent, navigateQuestionHistory, renderCenteredWelcomeScreen, renderFullWelcomeScreen, renderHelpPopupBox, renderAboutPopupBox, renderWelcomePopupLayer, windowChatTranscriptAt, buildChatTranscriptRows, chatTranscriptOffsetForRow, getChatTranscriptWidth, } from './welcome.js';
|
|
11
11
|
async function animateChatWordmark(stdout) {
|
|
12
12
|
const wordmark = 'MODERADO';
|
|
13
13
|
const frames = 6;
|
|
@@ -19,7 +19,7 @@ async function animateChatWordmark(stdout) {
|
|
|
19
19
|
const endColumn = Math.max(1, columns - width + 1);
|
|
20
20
|
const progress = frame / frames;
|
|
21
21
|
const column = Math.round(startColumn + (endColumn - startColumn) * progress);
|
|
22
|
-
const startRow = Math.max(1, Math.floor(rows / 2) -
|
|
22
|
+
const startRow = Math.max(1, Math.floor(rows / 2) - 5);
|
|
23
23
|
const row = Math.round(startRow + (1 - startRow) * progress);
|
|
24
24
|
stdout.write(`\x1b[H\x1b[J\x1b[${row};${column}H\x1b[1;38;5;75m${wordmark.slice(0, width)}\x1b[0m`);
|
|
25
25
|
if (frame < frames)
|
|
@@ -44,6 +44,36 @@ function isMcpCommandInput(value) {
|
|
|
44
44
|
const command = value.trim();
|
|
45
45
|
return command === '/mcp' || command.startsWith('/mcp ');
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Modified keys the terminal reports as escape sequences once XTMODKEYS is on:
|
|
49
|
+
* `CSI 27;modifier;key~` (xterm family) or `CSI key;modifier u` (kitty
|
|
50
|
+
* protocol). readline decodes those byte by byte, so the raw chunk is matched
|
|
51
|
+
* here instead and the derived keypresses are dropped.
|
|
52
|
+
*/
|
|
53
|
+
const MODIFIED_KEY_PATTERN = /\x1b\[(?:27;(\d+);(\d+)~|(\d+);(\d+)u)/;
|
|
54
|
+
const WIN32_KEY_PATTERN = /\x1b\[(\d+);\d+;(\d+);([01]);(\d+);(\d+)_/g;
|
|
55
|
+
const WIN32_INPUT_ON = '\x1b[?9001h';
|
|
56
|
+
const WIN32_INPUT_OFF = '\x1b[?9001l';
|
|
57
|
+
const WIN32_KEY_NAMES = {
|
|
58
|
+
8: 'backspace', 9: 'tab', 13: 'return', 27: 'escape',
|
|
59
|
+
33: 'pageup', 34: 'pagedown', 37: 'left', 38: 'up',
|
|
60
|
+
39: 'right', 40: 'down', 46: 'delete',
|
|
61
|
+
};
|
|
62
|
+
function decodeModifiedKey(text) {
|
|
63
|
+
const match = MODIFIED_KEY_PATTERN.exec(text);
|
|
64
|
+
if (!match)
|
|
65
|
+
return undefined;
|
|
66
|
+
const modifier = Number(match[1] ?? match[4]);
|
|
67
|
+
const keyCode = Number(match[2] ?? match[3]);
|
|
68
|
+
const bits = modifier - 1; // 1 = Shift, 2 = Alt, 4 = Ctrl
|
|
69
|
+
if (keyCode === 13 && (bits & 1))
|
|
70
|
+
return 'newline';
|
|
71
|
+
if (keyCode === 9 && (bits & 1))
|
|
72
|
+
return 'toggle-auto-approve';
|
|
73
|
+
if (keyCode === 99 && (bits & 4))
|
|
74
|
+
return 'cancel';
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
47
77
|
export async function promptInteractiveTurn(options) {
|
|
48
78
|
const stdin = process.stdin;
|
|
49
79
|
const stdout = process.stdout;
|
|
@@ -55,6 +85,10 @@ export async function promptInteractiveTurn(options) {
|
|
|
55
85
|
let caret = 0;
|
|
56
86
|
/** True while keypress events derived from a mouse report are being dropped. */
|
|
57
87
|
let ignoreComposerKeypress = false;
|
|
88
|
+
const useWin32Input = process.platform === 'win32' && Boolean(process.env.WT_SESSION);
|
|
89
|
+
let win32InputBound = false;
|
|
90
|
+
let pendingWin32Key = '';
|
|
91
|
+
let handleWin32Keypress = () => { };
|
|
58
92
|
/** Terminal height, with the same fallback the renderer uses. */
|
|
59
93
|
const getRows = () => stdout.rows || 24;
|
|
60
94
|
let commandSelection = 0;
|
|
@@ -106,6 +140,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
106
140
|
commandSelection,
|
|
107
141
|
chatQuestion: options.chatQuestion,
|
|
108
142
|
chatAnswer: options.chatAnswer,
|
|
143
|
+
chatHistory: options.chatHistory,
|
|
109
144
|
chatAnswerOffset: answerOffset,
|
|
110
145
|
// Live activity for the running turn, so a composer-driven repaint shows
|
|
111
146
|
// the transcript the loop is painting rather than an empty answer area.
|
|
@@ -141,23 +176,26 @@ export async function promptInteractiveTurn(options) {
|
|
|
141
176
|
stdin.resume();
|
|
142
177
|
stdin.setRawMode(true);
|
|
143
178
|
const getExtraLines = () => mentionFiles.length > 0 ? mentionFiles.length + 2 : 0;
|
|
144
|
-
/**
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
/** Columns of the card the input is painted in. */
|
|
180
|
+
const getCardWidth = () => getOptions().chatQuestion !== undefined
|
|
181
|
+
? getChatCardWidth(stdout.columns)
|
|
182
|
+
: getWelcomeCardWidth(stdout.columns);
|
|
183
|
+
/** Painted rows of the input box plus where the caret sits in them. */
|
|
184
|
+
const getInputView = () => composerView(input, caret, getCardWidth(), getComposerInputMaxRows(getRows()));
|
|
185
|
+
/** Rows between the input's first row and the terminal's last row. */
|
|
186
|
+
const getComposerOffset = () => getWelcomeBottomPadding(getOptions(), getRows()) + 3 + getInputView().rows.length + getExtraLines();
|
|
187
|
+
/** Screen row and 1-based column of the caret's cell in the input box. */
|
|
188
|
+
const getComposerAnchor = (caretRow = getInputView().caretRow) => {
|
|
148
189
|
const options = getOptions();
|
|
149
|
-
const
|
|
150
|
-
? getChatCardWidth(stdout.columns)
|
|
151
|
-
: getWelcomeCardWidth(stdout.columns);
|
|
152
|
-
const column = getWelcomeIndent(cardWidth, stdout.columns) + 3;
|
|
190
|
+
const column = getWelcomeIndent(getCardWidth(), stdout.columns) + 3;
|
|
153
191
|
if (options.chatQuestion !== undefined) {
|
|
154
192
|
// Chat frames place the composer at a computed row. Measure the card that
|
|
155
|
-
// is actually painted — a queued-commands or
|
|
156
|
-
// so the caret never lands on the mode row or inside the box.
|
|
193
|
+
// is actually painted — a queued-commands box or a wrapped prompt makes it
|
|
194
|
+
// taller — so the caret never lands on the mode row or inside the box.
|
|
157
195
|
const layout = getChatLayout(stdout.columns, getRows(), options.chatQuestion, getChatComposerRows(options));
|
|
158
|
-
return { row: layout.composerTop + 1, column };
|
|
196
|
+
return { row: layout.composerTop + 1 + caretRow, column };
|
|
159
197
|
}
|
|
160
|
-
return { row: getRows() - getComposerOffset(), column };
|
|
198
|
+
return { row: getRows() - getComposerOffset() + caretRow, column };
|
|
161
199
|
};
|
|
162
200
|
/**
|
|
163
201
|
* Move cursor to the ❯ input line (Line 2 inside the centered card).
|
|
@@ -166,20 +204,18 @@ export async function promptInteractiveTurn(options) {
|
|
|
166
204
|
* pressed repeatedly, e.g. browsing history with Left/Right.
|
|
167
205
|
*/
|
|
168
206
|
const positionCursorOnInput = () => {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
: getWelcomeCardWidth(stdout.columns);
|
|
173
|
-
const { caretColumn } = composerInputWindow(input, caret, cardWidth);
|
|
174
|
-
stdout.write(`\x1b[1 q\x1b[?25h\x1b[${row};${column + caretColumn}H`);
|
|
207
|
+
const view = getInputView();
|
|
208
|
+
const { row, column } = getComposerAnchor(view.caretRow);
|
|
209
|
+
stdout.write(`\x1b[1 q\x1b[?25h\x1b[${row};${column + view.caretColumn}H`);
|
|
175
210
|
};
|
|
176
211
|
/** Full-screen redraw: clear everything, repaint welcome TUI, reposition cursor. */
|
|
177
212
|
const redrawFull = () => {
|
|
213
|
+
stdout.write('\x1b[?2026h');
|
|
178
214
|
stdout.write('\x1b[H\x1b[J');
|
|
179
215
|
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
180
216
|
positionCursorOnInput();
|
|
217
|
+
stdout.write('\x1b[?2026l');
|
|
181
218
|
};
|
|
182
|
-
/** Repaint the welcome view so it remains centered after every edit or resize. */
|
|
183
219
|
const redrawCard = () => {
|
|
184
220
|
redrawFull();
|
|
185
221
|
};
|
|
@@ -201,7 +237,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
201
237
|
const layout = currentChatLayout();
|
|
202
238
|
if (layout.answerRows <= 0)
|
|
203
239
|
return 0;
|
|
204
|
-
return windowChatTranscriptAt(buildChatTranscriptRows(activity, answer ?? '', layout.
|
|
240
|
+
return windowChatTranscriptAt(buildChatTranscriptRows(activity, answer ?? '', getChatTranscriptWidth(layout), options.chatHistory, options.chatQuestion).rows, layout.answerRows, 0).hiddenAbove;
|
|
205
241
|
};
|
|
206
242
|
/**
|
|
207
243
|
* Move the transcript view; positive scrolls up towards older lines.
|
|
@@ -222,15 +258,84 @@ export async function promptInteractiveTurn(options) {
|
|
|
222
258
|
options.onTranscriptScroll?.(next);
|
|
223
259
|
redrawFull();
|
|
224
260
|
};
|
|
261
|
+
const jumpAnswerToRow = (row) => {
|
|
262
|
+
const layout = currentChatLayout();
|
|
263
|
+
const rows = buildChatTranscriptRows(options.getChatActivity?.(), options.chatAnswer ?? '', getChatTranscriptWidth(layout), options.chatHistory, options.chatQuestion).rows;
|
|
264
|
+
if (rows.length <= layout.answerRows)
|
|
265
|
+
return;
|
|
266
|
+
const next = chatTranscriptOffsetForRow(rows.length, layout.answerRows, row);
|
|
267
|
+
if (next === answerOffset)
|
|
268
|
+
return;
|
|
269
|
+
answerOffset = next;
|
|
270
|
+
options.onTranscriptScroll?.(next);
|
|
271
|
+
redrawFull();
|
|
272
|
+
};
|
|
225
273
|
/** One page is one screenful of answer rows, minus the overflow note. */
|
|
226
274
|
const answerPage = () => Math.max(1, currentChatLayout().answerRows - 1);
|
|
275
|
+
/** Insert a line break at the caret — Shift+Enter and Ctrl+J land here. */
|
|
276
|
+
const insertLineFeed = () => {
|
|
277
|
+
input = input.slice(0, caret) + '\n' + input.slice(caret);
|
|
278
|
+
caret += 1;
|
|
279
|
+
commandSelection = 0;
|
|
280
|
+
questionHistoryIndex = questionHistory.length;
|
|
281
|
+
updateMentions();
|
|
282
|
+
redrawCard();
|
|
283
|
+
};
|
|
284
|
+
/** Toggle Plan/Execute; kept callable from a decoded escape sequence too. */
|
|
285
|
+
const toggleMode = () => {
|
|
286
|
+
currentMode = currentMode === 'Plan' ? 'Execute' : 'Plan';
|
|
287
|
+
redrawCard();
|
|
288
|
+
};
|
|
227
289
|
/**
|
|
228
|
-
*
|
|
290
|
+
* Cancel the session from a decoded escape sequence (Ctrl+C reported as a
|
|
291
|
+
* modified key). Assigned once the cleanup path exists.
|
|
292
|
+
*/
|
|
293
|
+
let cancelSession = () => { };
|
|
294
|
+
/**
|
|
295
|
+
* Place the caret where the user clicked inside the input box. Only a primary
|
|
229
296
|
* press is honoured; releases, drags, wheel and other buttons are ignored.
|
|
230
297
|
*/
|
|
231
|
-
const
|
|
298
|
+
const onComposerData = (chunk) => {
|
|
232
299
|
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
233
300
|
let reported = false;
|
|
301
|
+
// Windows Terminal's Win32 input mode reports every key as a record.
|
|
302
|
+
// Translate those records through the same composer handler as legacy keys.
|
|
303
|
+
if (win32InputBound) {
|
|
304
|
+
const keyData = pendingWin32Key + text;
|
|
305
|
+
const records = [...keyData.matchAll(WIN32_KEY_PATTERN)];
|
|
306
|
+
pendingWin32Key = /\x1b\[[0-9;]*$/.exec(keyData)?.[0] ?? '';
|
|
307
|
+
if (records.length || pendingWin32Key)
|
|
308
|
+
reported = true;
|
|
309
|
+
for (const record of records) {
|
|
310
|
+
if (record[3] !== '1')
|
|
311
|
+
continue;
|
|
312
|
+
const virtualKey = Number(record[1]);
|
|
313
|
+
const unicode = Number(record[2]);
|
|
314
|
+
const state = Number(record[4]);
|
|
315
|
+
const repeat = Number(record[5]);
|
|
316
|
+
if (unicode > 65535 || repeat < 1 || repeat > 256)
|
|
317
|
+
continue;
|
|
318
|
+
const char = unicode ? String.fromCharCode(unicode) : '';
|
|
319
|
+
const name = WIN32_KEY_NAMES[virtualKey] ?? (virtualKey >= 65 && virtualKey <= 90 ? String.fromCharCode(virtualKey).toLowerCase() : char.toLowerCase());
|
|
320
|
+
if (!name)
|
|
321
|
+
continue;
|
|
322
|
+
const altGr = Boolean((state & 1) && (state & 8) && unicode >= 32);
|
|
323
|
+
const key = { name, shift: Boolean(state & 16), ctrl: Boolean(state & 12) && !altGr, meta: Boolean(state & 3) && !altGr };
|
|
324
|
+
for (let index = 0; index < repeat; index++)
|
|
325
|
+
handleWin32Keypress(virtualKey === 13 ? '\r' : char, key);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Shift+Enter arrives as one of these sequences once the terminal reports
|
|
329
|
+
// modified keys; readline would otherwise type it out as literal garbage.
|
|
330
|
+
const modified = decodeModifiedKey(text);
|
|
331
|
+
if (modified === 'newline')
|
|
332
|
+
insertLineFeed();
|
|
333
|
+
else if (modified === 'toggle-auto-approve')
|
|
334
|
+
toggleMode();
|
|
335
|
+
else if (modified === 'cancel')
|
|
336
|
+
cancelSession();
|
|
337
|
+
if (modified)
|
|
338
|
+
reported = true;
|
|
234
339
|
for (const report of text.matchAll(MOUSE_REPORT_PATTERN)) {
|
|
235
340
|
reported = true;
|
|
236
341
|
// Wheel up (64) / down (65): three rows a notch, like every other pager.
|
|
@@ -240,10 +345,21 @@ export async function promptInteractiveTurn(options) {
|
|
|
240
345
|
}
|
|
241
346
|
if (report[4] !== 'M' || Number(report[1]) !== 0)
|
|
242
347
|
continue;
|
|
243
|
-
|
|
244
|
-
|
|
348
|
+
if (options.chatQuestion !== undefined) {
|
|
349
|
+
const layout = currentChatLayout();
|
|
350
|
+
const scrollbarColumn = layout.area.startColumn + getChatTranscriptWidth(layout);
|
|
351
|
+
const clickedRow = Number(report[3]) - layout.answerTop;
|
|
352
|
+
if (Number(report[2]) === scrollbarColumn && clickedRow >= 0 && clickedRow < layout.answerRows) {
|
|
353
|
+
jumpAnswerToRow(clickedRow);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const view = getInputView();
|
|
358
|
+
const { row, column } = getComposerAnchor(0);
|
|
359
|
+
const rowOffset = Number(report[3]) - row;
|
|
360
|
+
if (rowOffset < 0 || rowOffset >= view.rows.length)
|
|
245
361
|
continue;
|
|
246
|
-
caret =
|
|
362
|
+
caret = composerCaretIndex(input, view.rows, rowOffset, Number(report[2]) - column);
|
|
247
363
|
positionCursorOnInput();
|
|
248
364
|
}
|
|
249
365
|
if (reported) {
|
|
@@ -254,7 +370,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
254
370
|
};
|
|
255
371
|
// Registered before readline's decoder so a report is consumed before it is
|
|
256
372
|
// mistaken for typed text.
|
|
257
|
-
stdin.prependListener('data',
|
|
373
|
+
stdin.prependListener('data', onComposerData);
|
|
258
374
|
stdout.on('resize', onResize);
|
|
259
375
|
// Initial paint
|
|
260
376
|
if (options.isFirstTurn) {
|
|
@@ -270,9 +386,9 @@ export async function promptInteractiveTurn(options) {
|
|
|
270
386
|
return new Promise((resolve) => {
|
|
271
387
|
const cleanup = () => {
|
|
272
388
|
unbindComposerInput();
|
|
273
|
-
stdin.removeListener('data',
|
|
389
|
+
stdin.removeListener('data', onComposerData);
|
|
274
390
|
stdout.removeListener('resize', onResize);
|
|
275
|
-
stdout.write(`${MOUSE_REPORT_OFF}\x1b[0 q\x1b[?25h`);
|
|
391
|
+
stdout.write(`${KEY_MODIFIER_REPORT_OFF}${MOUSE_REPORT_OFF}\x1b[0 q\x1b[?25h`);
|
|
276
392
|
if (stdin.isTTY) {
|
|
277
393
|
try {
|
|
278
394
|
stdin.setRawMode(false);
|
|
@@ -284,6 +400,10 @@ export async function promptInteractiveTurn(options) {
|
|
|
284
400
|
}
|
|
285
401
|
catch { /* ignore */ }
|
|
286
402
|
};
|
|
403
|
+
cancelSession = () => {
|
|
404
|
+
cleanup();
|
|
405
|
+
void exitCleanly('\x1b[33mSession cancelled.\x1b[0m');
|
|
406
|
+
};
|
|
287
407
|
const onAbort = () => {
|
|
288
408
|
cleanup();
|
|
289
409
|
resolve({ text: '', mode: currentMode, autoApprove: currentAutoApprove });
|
|
@@ -295,11 +415,34 @@ export async function promptInteractiveTurn(options) {
|
|
|
295
415
|
// Using a void async IIFE so we can await async operations (model selector)
|
|
296
416
|
// inside a synchronous event listener. For any async branch we first
|
|
297
417
|
// remove ourselves from stdin so no duplicate events fire during the await.
|
|
298
|
-
const onKeypress = (str, key) => {
|
|
418
|
+
const onKeypress = (str, key, fromWin32 = false) => {
|
|
299
419
|
void (async () => {
|
|
300
420
|
// Keystrokes decoded from a mouse report were already handled above.
|
|
301
|
-
if (ignoreComposerKeypress)
|
|
421
|
+
if (!fromWin32 && ignoreComposerKeypress)
|
|
422
|
+
return;
|
|
423
|
+
// Intercept XTMODKEYS modified keys that readline cannot decode.
|
|
424
|
+
// The raw byte chunk `str` carries the escape sequence (e.g. CSI 27;1;13~
|
|
425
|
+
// for Shift+Enter); readline decodes those byte-by-byte and drops them,
|
|
426
|
+
// so we match the pattern directly and derive the keypress ourselves.
|
|
427
|
+
const modified = decodeModifiedKey(str);
|
|
428
|
+
if (modified === 'newline') {
|
|
429
|
+
insertLineFeed();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (modified === 'toggle-auto-approve') {
|
|
433
|
+
currentAutoApprove = !currentAutoApprove;
|
|
434
|
+
redrawCard();
|
|
302
435
|
return;
|
|
436
|
+
}
|
|
437
|
+
if (modified === 'cancel') {
|
|
438
|
+
cleanup();
|
|
439
|
+
return await exitCleanly('\\x1b[33mSession cancelled.\\x1b[0m');
|
|
440
|
+
}
|
|
441
|
+
// Enter submits; a line feed (Ctrl+J, or Shift+Enter where the terminal
|
|
442
|
+
// reports it) breaks the line instead.
|
|
443
|
+
const isEnter = Boolean(key) && (key.name === 'return' || key.name === 'enter');
|
|
444
|
+
const isLineFeed = isEnter && (str === '\n' || key.shift === true || key.meta === true || key.ctrl === true);
|
|
445
|
+
const isSubmit = isEnter && !isLineFeed;
|
|
303
446
|
if (options.signal?.aborted) {
|
|
304
447
|
cleanup();
|
|
305
448
|
resolve({ text: '', mode: currentMode, autoApprove: currentAutoApprove });
|
|
@@ -321,7 +464,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
321
464
|
// ── /help overlay popup ───────────────────────────────────────────────
|
|
322
465
|
// Triggered when Enter is pressed with "/help" already in the input box.
|
|
323
466
|
// We keep the main TUI as background and layer the help box on top.
|
|
324
|
-
if (
|
|
467
|
+
if (isSubmit && input.trim() === '/help') {
|
|
325
468
|
setInput('');
|
|
326
469
|
unbindComposerInput(); // pause main handler
|
|
327
470
|
// Paint: full welcome TUI (background) + help box on top
|
|
@@ -355,6 +498,37 @@ export async function promptInteractiveTurn(options) {
|
|
|
355
498
|
bindComposerInput(); // re-attach main handler
|
|
356
499
|
return;
|
|
357
500
|
}
|
|
501
|
+
// ── /about overlay popup ──────────────────────────────────────────────
|
|
502
|
+
// Triggered when Enter is pressed with "/about" already in the input box.
|
|
503
|
+
if (isSubmit && input.trim() === '/about') {
|
|
504
|
+
setInput('');
|
|
505
|
+
unbindComposerInput();
|
|
506
|
+
const aboutLines = renderAboutPopupBox(options.version ?? 'v0.1');
|
|
507
|
+
const paintAbout = () => {
|
|
508
|
+
stdout.write('\x1b[H\x1b[J');
|
|
509
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), aboutLines, stdout.columns, stdout.rows));
|
|
510
|
+
};
|
|
511
|
+
const onAboutResize = () => paintAbout();
|
|
512
|
+
stdout.on('resize', onAboutResize);
|
|
513
|
+
paintAbout();
|
|
514
|
+
await new Promise((dismissResolve) => {
|
|
515
|
+
const onDismiss = (s, k) => {
|
|
516
|
+
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
517
|
+
s === 'q' || s === 'Q' ||
|
|
518
|
+
(k && k.ctrl && k.name === 'c')) {
|
|
519
|
+
stdin.removeListener('keypress', onDismiss);
|
|
520
|
+
stdout.removeListener('resize', onAboutResize);
|
|
521
|
+
dismissResolve();
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
stdin.on('keypress', onDismiss);
|
|
525
|
+
});
|
|
526
|
+
stdout.write('\x1b[H\x1b[J');
|
|
527
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
528
|
+
positionCursorOnInput();
|
|
529
|
+
bindComposerInput();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
358
532
|
// ── /model overlay popup ──────────────────────────────────────────────
|
|
359
533
|
// Triggered when Enter is pressed with "/model" in the input box.
|
|
360
534
|
// We temporarily detach the main keypress listener and hand the model
|
|
@@ -363,7 +537,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
363
537
|
// layer on top of it (Cline/OpenCode style) — the popup never scrolls
|
|
364
538
|
// the background because every step redraws the whole frame.
|
|
365
539
|
// After it returns, we repaint the full welcome TUI and reposition cursor.
|
|
366
|
-
if (
|
|
540
|
+
if (isSubmit && input.trim() === '/model') {
|
|
367
541
|
setInput('');
|
|
368
542
|
unbindComposerInput(); // pause main handler
|
|
369
543
|
if (options.onModelSelect) {
|
|
@@ -393,7 +567,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
393
567
|
bindComposerInput(); // re-attach main handler
|
|
394
568
|
return;
|
|
395
569
|
}
|
|
396
|
-
if (
|
|
570
|
+
if (isSubmit && input.trim() === '/connect') {
|
|
397
571
|
setInput('');
|
|
398
572
|
unbindComposerInput();
|
|
399
573
|
const drawFrame = (popupLines) => {
|
|
@@ -414,7 +588,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
414
588
|
bindComposerInput();
|
|
415
589
|
return;
|
|
416
590
|
}
|
|
417
|
-
if (
|
|
591
|
+
if (isSubmit && input.trim() === '/transcript') {
|
|
418
592
|
setInput('');
|
|
419
593
|
unbindComposerInput();
|
|
420
594
|
stdout.write('\x1b[H\x1b[J');
|
|
@@ -447,7 +621,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
447
621
|
bindComposerInput();
|
|
448
622
|
return;
|
|
449
623
|
}
|
|
450
|
-
if (
|
|
624
|
+
if (isSubmit && input.trim().startsWith('/session')) {
|
|
451
625
|
const command = input.trim();
|
|
452
626
|
setInput('');
|
|
453
627
|
unbindComposerInput();
|
|
@@ -473,7 +647,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
473
647
|
bindComposerInput();
|
|
474
648
|
return;
|
|
475
649
|
}
|
|
476
|
-
if (
|
|
650
|
+
if (isSubmit && input.trim().startsWith('/queue')) {
|
|
477
651
|
const command = input.trim();
|
|
478
652
|
setInput('');
|
|
479
653
|
unbindComposerInput();
|
|
@@ -502,7 +676,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
502
676
|
bindComposerInput();
|
|
503
677
|
return;
|
|
504
678
|
}
|
|
505
|
-
if (
|
|
679
|
+
if (isSubmit && input.trim().startsWith('/btw')) {
|
|
506
680
|
const command = input.trim();
|
|
507
681
|
setInput('');
|
|
508
682
|
unbindComposerInput();
|
|
@@ -545,7 +719,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
545
719
|
bindComposerInput();
|
|
546
720
|
return;
|
|
547
721
|
}
|
|
548
|
-
if (
|
|
722
|
+
if (isSubmit && isMcpCommandInput(input)) {
|
|
549
723
|
const command = input.trim();
|
|
550
724
|
setInput('');
|
|
551
725
|
unbindComposerInput();
|
|
@@ -564,7 +738,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
564
738
|
bindComposerInput();
|
|
565
739
|
return;
|
|
566
740
|
}
|
|
567
|
-
if (
|
|
741
|
+
if (isSubmit && input.trim() === '/init') {
|
|
568
742
|
setInput('');
|
|
569
743
|
unbindComposerInput();
|
|
570
744
|
if (options.onInit) {
|
|
@@ -582,7 +756,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
582
756
|
bindComposerInput();
|
|
583
757
|
return;
|
|
584
758
|
}
|
|
585
|
-
if (
|
|
759
|
+
if (isSubmit && input.trim().startsWith('/workflow')) {
|
|
586
760
|
const command = input.trim();
|
|
587
761
|
setInput('');
|
|
588
762
|
unbindComposerInput();
|
|
@@ -600,14 +774,14 @@ export async function promptInteractiveTurn(options) {
|
|
|
600
774
|
return;
|
|
601
775
|
}
|
|
602
776
|
// ── /clear ────────────────────────────────────────────────────────────
|
|
603
|
-
if (
|
|
777
|
+
if (isSubmit && input.trim() === '/clear') {
|
|
604
778
|
setInput('');
|
|
605
779
|
options.onClear?.();
|
|
606
780
|
redrawFull();
|
|
607
781
|
return;
|
|
608
782
|
}
|
|
609
783
|
// ── /exit ─────────────────────────────────────────────────────────────
|
|
610
|
-
if (
|
|
784
|
+
if (isSubmit &&
|
|
611
785
|
(input.trim() === '/exit' || input.trim() === '/quit')) {
|
|
612
786
|
cleanup();
|
|
613
787
|
if (options.signal)
|
|
@@ -680,7 +854,11 @@ export async function promptInteractiveTurn(options) {
|
|
|
680
854
|
return;
|
|
681
855
|
}
|
|
682
856
|
// Enter — submit real user message
|
|
683
|
-
if (
|
|
857
|
+
if (isLineFeed) {
|
|
858
|
+
insertLineFeed();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (isSubmit) {
|
|
684
862
|
const matching = getMatchingCommands(input);
|
|
685
863
|
if (input.startsWith('/') && matching.length > 0 && !matching.some((command) => command.name === input.trim())) {
|
|
686
864
|
setInput(matching[Math.min(commandSelection, matching.length - 1)].name);
|
|
@@ -695,7 +873,10 @@ export async function promptInteractiveTurn(options) {
|
|
|
695
873
|
options.signal.removeEventListener('abort', onAbort);
|
|
696
874
|
if (animateFirstChat)
|
|
697
875
|
await animateChatWordmark(stdout);
|
|
698
|
-
|
|
876
|
+
// Move past the rows below the caret's row, so the frame that follows
|
|
877
|
+
// starts under the card however tall the input box grew.
|
|
878
|
+
const inputView = getInputView();
|
|
879
|
+
const moveDown = (inputView.rows.length - 1 - inputView.caretRow) + 4 + getExtraLines();
|
|
699
880
|
stdout.write(`\x1b[${moveDown}B\r\n\x1b[0 q\x1b[?25h`);
|
|
700
881
|
resolve({ text: trimmed, mode: currentMode, autoApprove: currentAutoApprove });
|
|
701
882
|
return;
|
|
@@ -742,6 +923,7 @@ export async function promptInteractiveTurn(options) {
|
|
|
742
923
|
}
|
|
743
924
|
})();
|
|
744
925
|
};
|
|
926
|
+
handleWin32Keypress = (str, key) => onKeypress(str, key, true);
|
|
745
927
|
/**
|
|
746
928
|
* Mouse reporting is only useful while the composer owns the keyboard: a
|
|
747
929
|
* click on the composer line places the caret. Popups and sub-prompts take
|
|
@@ -749,12 +931,17 @@ export async function promptInteractiveTurn(options) {
|
|
|
749
931
|
* normal text selection, and clicks never leak into their filter fields.
|
|
750
932
|
*/
|
|
751
933
|
const bindComposerInput = () => {
|
|
752
|
-
|
|
934
|
+
win32InputBound = useWin32Input;
|
|
935
|
+
stdout.write(MOUSE_REPORT_ON + KEY_MODIFIER_REPORT_ON + (useWin32Input ? WIN32_INPUT_ON : ''));
|
|
753
936
|
stdin.on('keypress', onKeypress);
|
|
754
937
|
};
|
|
755
938
|
const unbindComposerInput = () => {
|
|
939
|
+
win32InputBound = false;
|
|
940
|
+
pendingWin32Key = '';
|
|
756
941
|
stdin.removeListener('keypress', onKeypress);
|
|
757
|
-
|
|
942
|
+
// Key-modifier reporting is scoped to the composer: popups read plain
|
|
943
|
+
// legacy keys, and leaving XTMODKEYS on would change what they receive.
|
|
944
|
+
stdout.write((useWin32Input ? WIN32_INPUT_OFF : '') + KEY_MODIFIER_REPORT_OFF + MOUSE_REPORT_OFF);
|
|
758
945
|
};
|
|
759
946
|
bindComposerInput();
|
|
760
947
|
});
|