moderado 0.4.33 → 0.4.46
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 +14 -9
- package/dist/commands/chat.d.ts +7 -70
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +305 -456
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/chat_meta.d.ts +36 -0
- package/dist/commands/chat_meta.d.ts.map +1 -0
- package/dist/commands/chat_meta.js +102 -0
- package/dist/commands/chat_meta.js.map +1 -0
- package/dist/commands/chat_outcome.d.ts +31 -0
- package/dist/commands/chat_outcome.d.ts.map +1 -0
- package/dist/commands/chat_outcome.js +70 -0
- package/dist/commands/chat_outcome.js.map +1 -0
- package/dist/commands/chat_search.d.ts +24 -0
- package/dist/commands/chat_search.d.ts.map +1 -0
- package/dist/commands/chat_search.js +61 -0
- package/dist/commands/chat_search.js.map +1 -0
- package/dist/commands/chat_slash.d.ts +17 -0
- package/dist/commands/chat_slash.d.ts.map +1 -0
- package/dist/commands/chat_slash.js +108 -0
- package/dist/commands/chat_slash.js.map +1 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +25 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -2
- package/dist/config.js.map +1 -1
- package/dist/diagnostic_log.d.ts.map +1 -1
- package/dist/diagnostic_log.js +1 -0
- package/dist/diagnostic_log.js.map +1 -1
- package/dist/http_shutdown.d.ts +22 -0
- package/dist/http_shutdown.d.ts.map +1 -0
- package/dist/http_shutdown.js +36 -0
- package/dist/http_shutdown.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/self_update.d.ts +6 -0
- package/dist/self_update.d.ts.map +1 -1
- package/dist/self_update.js +17 -1
- package/dist/self_update.js.map +1 -1
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +5 -1
- package/dist/sessions.js.map +1 -1
- package/dist/transcript.d.ts +19 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +76 -0
- package/dist/transcript.js.map +1 -0
- package/dist/turn_summary.d.ts +76 -0
- package/dist/turn_summary.d.ts.map +1 -0
- package/dist/turn_summary.js +305 -0
- package/dist/turn_summary.js.map +1 -0
- package/dist/ui/box.d.ts +19 -0
- package/dist/ui/box.d.ts.map +1 -1
- package/dist/ui/box.js +119 -12
- package/dist/ui/box.js.map +1 -1
- package/dist/ui/chart.d.ts +2 -0
- package/dist/ui/chart.d.ts.map +1 -0
- package/dist/ui/chart.js +97 -0
- package/dist/ui/chart.js.map +1 -0
- package/dist/ui/composer.d.ts +80 -0
- package/dist/ui/composer.d.ts.map +1 -0
- package/dist/ui/composer.js +762 -0
- package/dist/ui/composer.js.map +1 -0
- package/dist/ui/help_content.d.ts +1 -1
- package/dist/ui/help_content.d.ts.map +1 -1
- package/dist/ui/help_content.js +4 -2
- package/dist/ui/help_content.js.map +1 -1
- package/dist/ui/model_selector.d.ts +1 -0
- package/dist/ui/model_selector.d.ts.map +1 -1
- package/dist/ui/model_selector.js +61 -18
- package/dist/ui/model_selector.js.map +1 -1
- package/dist/ui/popup.d.ts.map +1 -1
- package/dist/ui/popup.js +13 -0
- package/dist/ui/popup.js.map +1 -1
- package/dist/ui/provider_connect.d.ts +8 -0
- package/dist/ui/provider_connect.d.ts.map +1 -1
- package/dist/ui/provider_connect.js +81 -23
- package/dist/ui/provider_connect.js.map +1 -1
- package/dist/ui/renderer.d.ts +49 -4
- package/dist/ui/renderer.d.ts.map +1 -1
- package/dist/ui/renderer.js +167 -31
- package/dist/ui/renderer.js.map +1 -1
- package/dist/ui/terminal_approval.d.ts +21 -0
- package/dist/ui/terminal_approval.d.ts.map +1 -1
- package/dist/ui/terminal_approval.js +145 -47
- package/dist/ui/terminal_approval.js.map +1 -1
- package/dist/ui/welcome.d.ts +234 -69
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +588 -693
- package/dist/ui/welcome.js.map +1 -1
- package/dist/vendor/providers/presets.d.ts +16 -0
- package/dist/vendor/providers/presets.d.ts.map +1 -1
- package/dist/vendor/providers/presets.js +13 -0
- package/dist/vendor/providers/presets.js.map +1 -1
- package/dist/vendor/tools/checkpoints.js +5 -5
- package/dist/vendor/tools/checkpoints.js.map +1 -1
- package/dist/vendor/tools/jail.d.ts +8 -0
- package/dist/vendor/tools/jail.d.ts.map +1 -1
- package/dist/vendor/tools/jail.js +13 -2
- package/dist/vendor/tools/jail.js.map +1 -1
- package/dist/vendor/tools/tools/apply_patch.d.ts.map +1 -1
- package/dist/vendor/tools/tools/apply_patch.js +6 -3
- package/dist/vendor/tools/tools/apply_patch.js.map +1 -1
- package/dist/vendor/tools/tools/git_diff.d.ts.map +1 -1
- package/dist/vendor/tools/tools/git_diff.js +5 -3
- package/dist/vendor/tools/tools/git_diff.js.map +1 -1
- package/package.json +24 -7
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interactive composer: raw-mode keypress handling, the prompt line, the
|
|
3
|
+
* suggestion and mention boxes, popup delegation, and history navigation.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `ui/welcome.ts`, which now owns only layout and painting.
|
|
6
|
+
*/
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { activeMentionToken, filterMentionCandidates } from './file_mentions.js';
|
|
10
|
+
import { MOUSE_REPORT_OFF, MOUSE_REPORT_ON, MOUSE_REPORT_PATTERN, composerInputWindow, exitCleanly, getChatComposerRows, getChatLayout, getChatCardWidth, getMatchingCommands, getWelcomeBottomPadding, getWelcomeCardWidth, getWelcomeIndent, navigateQuestionHistory, renderCenteredWelcomeScreen, renderFullWelcomeScreen, renderHelpPopupBox, renderWelcomePopupLayer, windowChatTranscriptAt, buildChatTranscriptRows, } from './welcome.js';
|
|
11
|
+
async function animateChatWordmark(stdout) {
|
|
12
|
+
const wordmark = 'MODERADO';
|
|
13
|
+
const frames = 6;
|
|
14
|
+
for (let frame = 0; frame <= frames; frame += 1) {
|
|
15
|
+
const columns = stdout.columns || 80;
|
|
16
|
+
const rows = stdout.rows || 24;
|
|
17
|
+
const width = Math.min(columns, wordmark.length);
|
|
18
|
+
const startColumn = Math.max(1, Math.floor((columns - width) / 2) + 1);
|
|
19
|
+
const endColumn = Math.max(1, columns - width + 1);
|
|
20
|
+
const progress = frame / frames;
|
|
21
|
+
const column = Math.round(startColumn + (endColumn - startColumn) * progress);
|
|
22
|
+
const startRow = Math.max(1, Math.floor(rows / 2) - 4);
|
|
23
|
+
const row = Math.round(startRow + (1 - startRow) * progress);
|
|
24
|
+
stdout.write(`\x1b[H\x1b[J\x1b[${row};${column}H\x1b[1;38;5;75m${wordmark.slice(0, width)}\x1b[0m`);
|
|
25
|
+
if (frame < frames)
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Open a file in the user's pager. Returns false when no pager could be run, so
|
|
31
|
+
* the caller can print the path instead of pretending something was shown.
|
|
32
|
+
*/
|
|
33
|
+
function openInPager(file) {
|
|
34
|
+
const configured = process.env.PAGER?.trim();
|
|
35
|
+
const command = configured
|
|
36
|
+
? configured.split(/\s+/).filter(Boolean)
|
|
37
|
+
: process.platform === 'win32' ? [] : ['less', '-R'];
|
|
38
|
+
if (command.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
const result = spawnSync(command[0], [...command.slice(1), file], { stdio: 'inherit' });
|
|
41
|
+
return !result.error;
|
|
42
|
+
}
|
|
43
|
+
function isMcpCommandInput(value) {
|
|
44
|
+
const command = value.trim();
|
|
45
|
+
return command === '/mcp' || command.startsWith('/mcp ');
|
|
46
|
+
}
|
|
47
|
+
export async function promptInteractiveTurn(options) {
|
|
48
|
+
const stdin = process.stdin;
|
|
49
|
+
const stdout = process.stdout;
|
|
50
|
+
let currentModel = options.model;
|
|
51
|
+
let currentMode = options.initialMode ?? 'Execute';
|
|
52
|
+
let currentAutoApprove = options.initialAutoApprove ?? true;
|
|
53
|
+
let input = '';
|
|
54
|
+
/** Caret index inside `input` (0 .. input.length) - the composer is editable. */
|
|
55
|
+
let caret = 0;
|
|
56
|
+
/** True while keypress events derived from a mouse report are being dropped. */
|
|
57
|
+
let ignoreComposerKeypress = false;
|
|
58
|
+
/** Terminal height, with the same fallback the renderer uses. */
|
|
59
|
+
const getRows = () => stdout.rows || 24;
|
|
60
|
+
let commandSelection = 0;
|
|
61
|
+
const questionHistory = options.questionHistory ?? [];
|
|
62
|
+
let questionHistoryIndex = questionHistory.length;
|
|
63
|
+
let questionHistoryDraft = '';
|
|
64
|
+
let cachedFiles = [];
|
|
65
|
+
if (options.onMentionComplete) {
|
|
66
|
+
Promise.resolve(options.onMentionComplete('')).then((files) => {
|
|
67
|
+
cachedFiles = Array.isArray(files) ? files : [];
|
|
68
|
+
updateMentions();
|
|
69
|
+
}).catch(() => { cachedFiles = []; });
|
|
70
|
+
}
|
|
71
|
+
let mentionFiles = [];
|
|
72
|
+
let mentionSelection = 0;
|
|
73
|
+
/**
|
|
74
|
+
* Rows the answer is scrolled up from its tail. 0 follows the newest line;
|
|
75
|
+
* PgUp/PgDn and the mouse wheel move it, and every new turn starts at 0.
|
|
76
|
+
*/
|
|
77
|
+
let answerOffset = 0;
|
|
78
|
+
const updateMentions = () => {
|
|
79
|
+
const mention = activeMentionToken(input, caret);
|
|
80
|
+
if (mention && cachedFiles.length > 0) {
|
|
81
|
+
mentionFiles = filterMentionCandidates(cachedFiles, mention.token, 5);
|
|
82
|
+
mentionSelection = Math.min(mentionSelection, Math.max(0, mentionFiles.length - 1));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
mentionFiles = [];
|
|
86
|
+
mentionSelection = 0;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/** Replace the composer text, leaving the caret at the end of the new value. */
|
|
90
|
+
const setInput = (value) => {
|
|
91
|
+
input = value;
|
|
92
|
+
caret = value.length;
|
|
93
|
+
};
|
|
94
|
+
const getOptions = () => ({
|
|
95
|
+
model: currentModel,
|
|
96
|
+
provider: options.getProviderName?.(),
|
|
97
|
+
tokens: options.tokens,
|
|
98
|
+
cost: options.cost,
|
|
99
|
+
usageAvailable: options.usageAvailable,
|
|
100
|
+
usageEstimated: options.usageEstimated,
|
|
101
|
+
workspace: options.workspace,
|
|
102
|
+
mode: currentMode,
|
|
103
|
+
autoApprove: currentAutoApprove,
|
|
104
|
+
input,
|
|
105
|
+
inputCaret: caret,
|
|
106
|
+
commandSelection,
|
|
107
|
+
chatQuestion: options.chatQuestion,
|
|
108
|
+
chatAnswer: options.chatAnswer,
|
|
109
|
+
chatAnswerOffset: answerOffset,
|
|
110
|
+
// Live activity for the running turn, so a composer-driven repaint shows
|
|
111
|
+
// the transcript the loop is painting rather than an empty answer area.
|
|
112
|
+
chatActivity: options.getChatActivity?.(),
|
|
113
|
+
chatThoughtTime: options.chatThoughtTime,
|
|
114
|
+
chatTaskDone: options.chatTaskDone,
|
|
115
|
+
outputTokenRate: options.outputTokenRate,
|
|
116
|
+
tokenUsage: options.tokenUsage,
|
|
117
|
+
queuedCommands: options.queuedCommands,
|
|
118
|
+
// The @ completion list lives in the card, so the composer must be told
|
|
119
|
+
// about it — without these the keypress handler maintained a selection that
|
|
120
|
+
// was never painted.
|
|
121
|
+
mentionFiles,
|
|
122
|
+
mentionSelection,
|
|
123
|
+
isTurnSettled: true,
|
|
124
|
+
});
|
|
125
|
+
// ── Non-TTY fallback ──────────────────────────────────────────────────────
|
|
126
|
+
if (!stdin.isTTY) {
|
|
127
|
+
stdout.write('\x1b]0;Moderado\x07');
|
|
128
|
+
stdout.write(renderFullWelcomeScreen(getOptions()));
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
131
|
+
rl.question('\x1b[1;38;5;75m❯\x1b[0m ', (answer) => {
|
|
132
|
+
rl.close();
|
|
133
|
+
resolve({ text: answer.trim(), mode: currentMode, autoApprove: currentAutoApprove });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// ── TTY Interactive Loop ───────────────────────────────────────────────────
|
|
138
|
+
readline.emitKeypressEvents(stdin);
|
|
139
|
+
// A previous turn's cleanup() pauses stdin — resume it so keystrokes keep
|
|
140
|
+
// flowing after an answer (the main app must never appear dead post-answer).
|
|
141
|
+
stdin.resume();
|
|
142
|
+
stdin.setRawMode(true);
|
|
143
|
+
const getExtraLines = () => mentionFiles.length > 0 ? mentionFiles.length + 2 : 0;
|
|
144
|
+
/** Rows between the painted composer line and the terminal's last row. */
|
|
145
|
+
const getComposerOffset = () => getWelcomeBottomPadding(getOptions(), getRows()) + 4 + getExtraLines();
|
|
146
|
+
/** Screen row and 1-based column of the composer's first text cell. */
|
|
147
|
+
const getComposerAnchor = () => {
|
|
148
|
+
const options = getOptions();
|
|
149
|
+
const cardWidth = options.chatQuestion !== undefined
|
|
150
|
+
? getChatCardWidth(stdout.columns)
|
|
151
|
+
: getWelcomeCardWidth(stdout.columns);
|
|
152
|
+
const column = getWelcomeIndent(cardWidth, stdout.columns) + 3;
|
|
153
|
+
if (options.chatQuestion !== undefined) {
|
|
154
|
+
// Chat frames place the composer at a computed row. Measure the card that
|
|
155
|
+
// is actually painted — a queued-commands or mention box makes it taller —
|
|
156
|
+
// so the caret never lands on the mode row or inside the box.
|
|
157
|
+
const layout = getChatLayout(stdout.columns, getRows(), options.chatQuestion, getChatComposerRows(options));
|
|
158
|
+
return { row: layout.composerTop + 1, column };
|
|
159
|
+
}
|
|
160
|
+
return { row: getRows() - getComposerOffset(), column };
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Move cursor to the ❯ input line (Line 2 inside the centered card).
|
|
164
|
+
* Absolute addressing (`row;column H`) works no matter where the cursor
|
|
165
|
+
* currently sits — relative "move up N" escapes the input line when
|
|
166
|
+
* pressed repeatedly, e.g. browsing history with Left/Right.
|
|
167
|
+
*/
|
|
168
|
+
const positionCursorOnInput = () => {
|
|
169
|
+
const { row, column } = getComposerAnchor();
|
|
170
|
+
const cardWidth = getOptions().chatQuestion !== undefined
|
|
171
|
+
? getChatCardWidth(stdout.columns)
|
|
172
|
+
: getWelcomeCardWidth(stdout.columns);
|
|
173
|
+
const { caretColumn } = composerInputWindow(input, caret, cardWidth);
|
|
174
|
+
stdout.write(`\x1b[1 q\x1b[?25h\x1b[${row};${column + caretColumn}H`);
|
|
175
|
+
};
|
|
176
|
+
/** Full-screen redraw: clear everything, repaint welcome TUI, reposition cursor. */
|
|
177
|
+
const redrawFull = () => {
|
|
178
|
+
stdout.write('\x1b[H\x1b[J');
|
|
179
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
180
|
+
positionCursorOnInput();
|
|
181
|
+
};
|
|
182
|
+
/** Repaint the welcome view so it remains centered after every edit or resize. */
|
|
183
|
+
const redrawCard = () => {
|
|
184
|
+
redrawFull();
|
|
185
|
+
};
|
|
186
|
+
const onResize = () => redrawFull();
|
|
187
|
+
/** Layout of the chat frame as it is painted right now. */
|
|
188
|
+
const currentChatLayout = () => getChatLayout(stdout.columns, getRows(), options.chatQuestion ?? '', getChatComposerRows(getOptions()));
|
|
189
|
+
/**
|
|
190
|
+
* How many rows of transcript sit above the tail view — the scroll range.
|
|
191
|
+
*
|
|
192
|
+
* The range covers the whole transcript, not just the answer: the activity log
|
|
193
|
+
* and the answer are one list, so scrolling up reaches what the agent did as
|
|
194
|
+
* well as what it said.
|
|
195
|
+
*/
|
|
196
|
+
const answerScrollLimit = () => {
|
|
197
|
+
const answer = options.chatAnswer;
|
|
198
|
+
const activity = options.getChatActivity?.();
|
|
199
|
+
if ((!answer || answer.trim().length === 0) && !activity)
|
|
200
|
+
return 0;
|
|
201
|
+
const layout = currentChatLayout();
|
|
202
|
+
if (layout.answerRows <= 0)
|
|
203
|
+
return 0;
|
|
204
|
+
return windowChatTranscriptAt(buildChatTranscriptRows(activity, answer ?? '', layout.area.textWidth).rows, layout.answerRows, 0).hiddenAbove;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Move the transcript view; positive scrolls up towards older lines.
|
|
208
|
+
*
|
|
209
|
+
* A long turn is taller than the transcript area, so the tail view hides its
|
|
210
|
+
* opening rows. This is the way back: the whole turn stays in the buffer, the
|
|
211
|
+
* status row reports the position, and the turn loop is told so its next
|
|
212
|
+
* incremental paint does not yank the reader back to the bottom.
|
|
213
|
+
*/
|
|
214
|
+
const scrollAnswer = (delta) => {
|
|
215
|
+
const limit = answerScrollLimit();
|
|
216
|
+
if (limit <= 0)
|
|
217
|
+
return;
|
|
218
|
+
const next = Math.max(0, Math.min(limit, answerOffset + delta));
|
|
219
|
+
if (next === answerOffset)
|
|
220
|
+
return;
|
|
221
|
+
answerOffset = next;
|
|
222
|
+
options.onTranscriptScroll?.(next);
|
|
223
|
+
redrawFull();
|
|
224
|
+
};
|
|
225
|
+
/** One page is one screenful of answer rows, minus the overflow note. */
|
|
226
|
+
const answerPage = () => Math.max(1, currentChatLayout().answerRows - 1);
|
|
227
|
+
/**
|
|
228
|
+
* Place the caret where the user clicked on the composer line. Only a primary
|
|
229
|
+
* press is honoured; releases, drags, wheel and other buttons are ignored.
|
|
230
|
+
*/
|
|
231
|
+
const onMouseData = (chunk) => {
|
|
232
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
233
|
+
let reported = false;
|
|
234
|
+
for (const report of text.matchAll(MOUSE_REPORT_PATTERN)) {
|
|
235
|
+
reported = true;
|
|
236
|
+
// Wheel up (64) / down (65): three rows a notch, like every other pager.
|
|
237
|
+
if (report[4] === 'M' && (Number(report[1]) === 64 || Number(report[1]) === 65)) {
|
|
238
|
+
scrollAnswer(Number(report[1]) === 64 ? 3 : -3);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (report[4] !== 'M' || Number(report[1]) !== 0)
|
|
242
|
+
continue;
|
|
243
|
+
const { row, column } = getComposerAnchor();
|
|
244
|
+
if (Number(report[3]) !== row)
|
|
245
|
+
continue;
|
|
246
|
+
caret = Math.max(0, Math.min(input.length, Number(report[2]) - column));
|
|
247
|
+
positionCursorOnInput();
|
|
248
|
+
}
|
|
249
|
+
if (reported) {
|
|
250
|
+
// readline turns the same bytes into keypress events ("0", ";", "5", "M").
|
|
251
|
+
ignoreComposerKeypress = true;
|
|
252
|
+
setImmediate(() => { ignoreComposerKeypress = false; });
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
// Registered before readline's decoder so a report is consumed before it is
|
|
256
|
+
// mistaken for typed text.
|
|
257
|
+
stdin.prependListener('data', onMouseData);
|
|
258
|
+
stdout.on('resize', onResize);
|
|
259
|
+
// Initial paint
|
|
260
|
+
if (options.isFirstTurn) {
|
|
261
|
+
stdout.write('\x1b]0;Moderado\x07');
|
|
262
|
+
stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// After an AI response the screen has content below — clear and repaint the full TUI.
|
|
266
|
+
stdout.write('\x1b[H\x1b[J');
|
|
267
|
+
}
|
|
268
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
269
|
+
positionCursorOnInput();
|
|
270
|
+
return new Promise((resolve) => {
|
|
271
|
+
const cleanup = () => {
|
|
272
|
+
unbindComposerInput();
|
|
273
|
+
stdin.removeListener('data', onMouseData);
|
|
274
|
+
stdout.removeListener('resize', onResize);
|
|
275
|
+
stdout.write(`${MOUSE_REPORT_OFF}\x1b[0 q\x1b[?25h`);
|
|
276
|
+
if (stdin.isTTY) {
|
|
277
|
+
try {
|
|
278
|
+
stdin.setRawMode(false);
|
|
279
|
+
}
|
|
280
|
+
catch { /* ignore */ }
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
stdin.pause();
|
|
284
|
+
}
|
|
285
|
+
catch { /* ignore */ }
|
|
286
|
+
};
|
|
287
|
+
const onAbort = () => {
|
|
288
|
+
cleanup();
|
|
289
|
+
resolve({ text: '', mode: currentMode, autoApprove: currentAutoApprove });
|
|
290
|
+
};
|
|
291
|
+
if (options.signal) {
|
|
292
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
293
|
+
}
|
|
294
|
+
// ── Async keypress handler ────────────────────────────────────────────────
|
|
295
|
+
// Using a void async IIFE so we can await async operations (model selector)
|
|
296
|
+
// inside a synchronous event listener. For any async branch we first
|
|
297
|
+
// remove ourselves from stdin so no duplicate events fire during the await.
|
|
298
|
+
const onKeypress = (str, key) => {
|
|
299
|
+
void (async () => {
|
|
300
|
+
// Keystrokes decoded from a mouse report were already handled above.
|
|
301
|
+
if (ignoreComposerKeypress)
|
|
302
|
+
return;
|
|
303
|
+
if (options.signal?.aborted) {
|
|
304
|
+
cleanup();
|
|
305
|
+
resolve({ text: '', mode: currentMode, autoApprove: currentAutoApprove });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// ── Ctrl+C ────────────────────────────────────────────────────────────
|
|
309
|
+
if (key && key.ctrl && key.name === 'c') {
|
|
310
|
+
cleanup();
|
|
311
|
+
return await exitCleanly('\x1b[33mSession cancelled.\x1b[0m');
|
|
312
|
+
}
|
|
313
|
+
// ── Answer scrollback ─────────────────────────────────────────────────
|
|
314
|
+
// PgUp/PgDn browse the answer without leaving the composer. The turn
|
|
315
|
+
// summary is often taller than the answer area, and the terminal's own
|
|
316
|
+
// scrollback is not available to a full-screen TUI.
|
|
317
|
+
if (key && (key.name === 'pageup' || key.name === 'pagedown')) {
|
|
318
|
+
scrollAnswer(key.name === 'pageup' ? answerPage() : -answerPage());
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// ── /help overlay popup ───────────────────────────────────────────────
|
|
322
|
+
// Triggered when Enter is pressed with "/help" already in the input box.
|
|
323
|
+
// We keep the main TUI as background and layer the help box on top.
|
|
324
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/help') {
|
|
325
|
+
setInput('');
|
|
326
|
+
unbindComposerInput(); // pause main handler
|
|
327
|
+
// Paint: full welcome TUI (background) + help box on top
|
|
328
|
+
const helpLines = renderHelpPopupBox(options.version ?? 'v0.1', options.workspace);
|
|
329
|
+
const paintHelp = () => {
|
|
330
|
+
stdout.write('\x1b[H\x1b[J');
|
|
331
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), helpLines, stdout.columns, stdout.rows));
|
|
332
|
+
};
|
|
333
|
+
// The composer's own resize repaint clears the box off screen, so the
|
|
334
|
+
// overlay has to repaint itself at the new geometry.
|
|
335
|
+
const onHelpResize = () => paintHelp();
|
|
336
|
+
stdout.on('resize', onHelpResize);
|
|
337
|
+
paintHelp();
|
|
338
|
+
// Wait for Esc / Enter / q to dismiss
|
|
339
|
+
await new Promise((dismissResolve) => {
|
|
340
|
+
const onDismiss = (s, k) => {
|
|
341
|
+
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
342
|
+
s === 'q' || s === 'Q' ||
|
|
343
|
+
(k && k.ctrl && k.name === 'c')) {
|
|
344
|
+
stdin.removeListener('keypress', onDismiss);
|
|
345
|
+
stdout.removeListener('resize', onHelpResize);
|
|
346
|
+
dismissResolve();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
stdin.on('keypress', onDismiss);
|
|
350
|
+
});
|
|
351
|
+
// Restore: repaint the full welcome TUI, show cursor, position on input line
|
|
352
|
+
stdout.write('\x1b[H\x1b[J');
|
|
353
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
354
|
+
positionCursorOnInput();
|
|
355
|
+
bindComposerInput(); // re-attach main handler
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// ── /model overlay popup ──────────────────────────────────────────────
|
|
359
|
+
// Triggered when Enter is pressed with "/model" in the input box.
|
|
360
|
+
// We temporarily detach the main keypress listener and hand the model
|
|
361
|
+
// selector a drawFrame callback. drawFrame repaints the background TUI
|
|
362
|
+
// exactly as-is and composites the popup content as a centered floating
|
|
363
|
+
// layer on top of it (Cline/OpenCode style) — the popup never scrolls
|
|
364
|
+
// the background because every step redraws the whole frame.
|
|
365
|
+
// After it returns, we repaint the full welcome TUI and reposition cursor.
|
|
366
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/model') {
|
|
367
|
+
setInput('');
|
|
368
|
+
unbindComposerInput(); // pause main handler
|
|
369
|
+
if (options.onModelSelect) {
|
|
370
|
+
const drawFrame = (popupLines) => {
|
|
371
|
+
stdout.write('\x1b[H\x1b[J');
|
|
372
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
373
|
+
};
|
|
374
|
+
const newId = await options.onModelSelect(drawFrame);
|
|
375
|
+
if (newId && newId !== currentModel) {
|
|
376
|
+
currentModel = newId;
|
|
377
|
+
}
|
|
378
|
+
const autoContinuePrompt = options.takeAutoContinuePrompt?.();
|
|
379
|
+
if (autoContinuePrompt) {
|
|
380
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
381
|
+
cleanup();
|
|
382
|
+
resolve({ text: autoContinuePrompt, mode: currentMode, autoApprove: currentAutoApprove });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Selection flow left stdin in non-raw mode — restore it for our TUI
|
|
387
|
+
readline.emitKeypressEvents(stdin);
|
|
388
|
+
stdin.resume();
|
|
389
|
+
stdin.setRawMode(true);
|
|
390
|
+
stdout.write('\x1b[H\x1b[J');
|
|
391
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
392
|
+
positionCursorOnInput();
|
|
393
|
+
bindComposerInput(); // re-attach main handler
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/connect') {
|
|
397
|
+
setInput('');
|
|
398
|
+
unbindComposerInput();
|
|
399
|
+
const drawFrame = (popupLines) => {
|
|
400
|
+
stdout.write('\x1b[H\x1b[J');
|
|
401
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
402
|
+
};
|
|
403
|
+
if (options.onConnect) {
|
|
404
|
+
const newModel = await options.onConnect(drawFrame);
|
|
405
|
+
if (newModel)
|
|
406
|
+
currentModel = newModel;
|
|
407
|
+
}
|
|
408
|
+
readline.emitKeypressEvents(stdin);
|
|
409
|
+
stdin.resume();
|
|
410
|
+
stdin.setRawMode(true);
|
|
411
|
+
stdout.write('\x1b[H\x1b[J');
|
|
412
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
413
|
+
positionCursorOnInput();
|
|
414
|
+
bindComposerInput();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/transcript') {
|
|
418
|
+
setInput('');
|
|
419
|
+
unbindComposerInput();
|
|
420
|
+
stdout.write('\x1b[H\x1b[J');
|
|
421
|
+
const file = await options.onTranscript?.();
|
|
422
|
+
if (file) {
|
|
423
|
+
// The pager needs the terminal in cooked mode and no mouse reporting;
|
|
424
|
+
// both are process state, so they are restored before repainting.
|
|
425
|
+
try {
|
|
426
|
+
stdin.setRawMode(false);
|
|
427
|
+
}
|
|
428
|
+
catch { /* ignore */ }
|
|
429
|
+
stdout.write(MOUSE_REPORT_OFF);
|
|
430
|
+
const opened = openInPager(file);
|
|
431
|
+
stdout.write(MOUSE_REPORT_ON);
|
|
432
|
+
try {
|
|
433
|
+
stdin.setRawMode(true);
|
|
434
|
+
}
|
|
435
|
+
catch { /* ignore */ }
|
|
436
|
+
if (!opened)
|
|
437
|
+
stdout.write(`\x1b[38;5;244mTranscript written to ${file}\x1b[0m\n`);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
stdout.write('\x1b[38;5;244mNo transcript to show yet — finish a turn first.\x1b[0m\n');
|
|
441
|
+
}
|
|
442
|
+
readline.emitKeypressEvents(stdin);
|
|
443
|
+
stdin.resume();
|
|
444
|
+
stdout.write('\x1b[H\x1b[J');
|
|
445
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
446
|
+
positionCursorOnInput();
|
|
447
|
+
bindComposerInput();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/session')) {
|
|
451
|
+
const command = input.trim();
|
|
452
|
+
setInput('');
|
|
453
|
+
unbindComposerInput();
|
|
454
|
+
if (options.onSession) {
|
|
455
|
+
await options.onSession(command, (popupLines) => {
|
|
456
|
+
stdout.write('\x1b[H\x1b[J');
|
|
457
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const autoContinuePrompt = options.takeAutoContinuePrompt?.();
|
|
461
|
+
if (autoContinuePrompt) {
|
|
462
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
463
|
+
cleanup();
|
|
464
|
+
resolve({ text: autoContinuePrompt, mode: currentMode, autoApprove: currentAutoApprove });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
readline.emitKeypressEvents(stdin);
|
|
468
|
+
stdin.resume();
|
|
469
|
+
stdin.setRawMode(true);
|
|
470
|
+
stdout.write('\x1b[H\x1b[J');
|
|
471
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
472
|
+
positionCursorOnInput();
|
|
473
|
+
bindComposerInput();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/queue')) {
|
|
477
|
+
const command = input.trim();
|
|
478
|
+
setInput('');
|
|
479
|
+
unbindComposerInput();
|
|
480
|
+
if (options.onQueue) {
|
|
481
|
+
await options.onQueue(command, async (popupLines) => {
|
|
482
|
+
stdout.write('\x1b[H\x1b[J');
|
|
483
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
484
|
+
await new Promise((dismissResolve) => {
|
|
485
|
+
const onDismiss = (s, k) => {
|
|
486
|
+
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
487
|
+
s === 'q' || s === 'Q' || s === '\x1b') {
|
|
488
|
+
stdin.removeListener('keypress', onDismiss);
|
|
489
|
+
dismissResolve();
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
stdin.on('keypress', onDismiss);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
readline.emitKeypressEvents(stdin);
|
|
497
|
+
stdin.resume();
|
|
498
|
+
stdin.setRawMode(true);
|
|
499
|
+
stdout.write('\x1b[H\x1b[J');
|
|
500
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
501
|
+
positionCursorOnInput();
|
|
502
|
+
bindComposerInput();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/btw')) {
|
|
506
|
+
const command = input.trim();
|
|
507
|
+
setInput('');
|
|
508
|
+
unbindComposerInput();
|
|
509
|
+
if (options.onBtw) {
|
|
510
|
+
const ac = new AbortController();
|
|
511
|
+
const onBtwKey = (s, k) => {
|
|
512
|
+
if ((k && k.name === 'escape') || s === '\x1b') {
|
|
513
|
+
ac.abort();
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
stdin.on('keypress', onBtwKey);
|
|
517
|
+
try {
|
|
518
|
+
await options.onBtw(command, async (popupLines, frameOpts) => {
|
|
519
|
+
stdout.write('\x1b[H\x1b[J');
|
|
520
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
521
|
+
if (frameOpts?.waitDismiss !== false && !ac.signal.aborted) {
|
|
522
|
+
await new Promise((dismissResolve) => {
|
|
523
|
+
const onDismiss = (s, k) => {
|
|
524
|
+
if ((k && (k.name === 'escape' || k.name === 'return' || k.name === 'enter')) ||
|
|
525
|
+
s === 'q' || s === 'Q' || s === '\x1b') {
|
|
526
|
+
stdin.removeListener('keypress', onDismiss);
|
|
527
|
+
dismissResolve();
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
stdin.on('keypress', onDismiss);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}, ac.signal);
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
stdin.removeListener('keypress', onBtwKey);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
readline.emitKeypressEvents(stdin);
|
|
540
|
+
stdin.resume();
|
|
541
|
+
stdin.setRawMode(true);
|
|
542
|
+
stdout.write('\x1b[H\x1b[J');
|
|
543
|
+
stdout.write(renderCenteredWelcomeScreen(getOptions(), stdout.rows));
|
|
544
|
+
positionCursorOnInput();
|
|
545
|
+
bindComposerInput();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (key && (key.name === 'return' || key.name === 'enter') && isMcpCommandInput(input)) {
|
|
549
|
+
const command = input.trim();
|
|
550
|
+
setInput('');
|
|
551
|
+
unbindComposerInput();
|
|
552
|
+
if (options.onMcp) {
|
|
553
|
+
await options.onMcp(command, (popupLines) => {
|
|
554
|
+
stdout.write('\x1b[H\x1b[J');
|
|
555
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
if (options.signal?.aborted)
|
|
559
|
+
return;
|
|
560
|
+
readline.emitKeypressEvents(stdin);
|
|
561
|
+
stdin.resume();
|
|
562
|
+
stdin.setRawMode(true);
|
|
563
|
+
redrawFull();
|
|
564
|
+
bindComposerInput();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/init') {
|
|
568
|
+
setInput('');
|
|
569
|
+
unbindComposerInput();
|
|
570
|
+
if (options.onInit) {
|
|
571
|
+
await options.onInit((popupLines) => {
|
|
572
|
+
stdout.write('\x1b[H\x1b[J');
|
|
573
|
+
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (options.signal?.aborted)
|
|
577
|
+
return;
|
|
578
|
+
readline.emitKeypressEvents(stdin);
|
|
579
|
+
stdin.resume();
|
|
580
|
+
stdin.setRawMode(true);
|
|
581
|
+
redrawFull();
|
|
582
|
+
bindComposerInput();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim().startsWith('/workflow')) {
|
|
586
|
+
const command = input.trim();
|
|
587
|
+
setInput('');
|
|
588
|
+
unbindComposerInput();
|
|
589
|
+
const action = options.onWorkflow ? await options.onWorkflow(command, (popupLines) => { stdout.write('\x1b[H\x1b[J'); stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows)); }) : undefined;
|
|
590
|
+
if (action === 'build') {
|
|
591
|
+
cleanup();
|
|
592
|
+
resolve({ text: '', mode: 'Execute', autoApprove: currentAutoApprove, workflowAction: 'build' });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
readline.emitKeypressEvents(stdin);
|
|
596
|
+
stdin.resume();
|
|
597
|
+
stdin.setRawMode(true);
|
|
598
|
+
redrawFull();
|
|
599
|
+
bindComposerInput();
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
// ── /clear ────────────────────────────────────────────────────────────
|
|
603
|
+
if (key && (key.name === 'return' || key.name === 'enter') && input.trim() === '/clear') {
|
|
604
|
+
setInput('');
|
|
605
|
+
options.onClear?.();
|
|
606
|
+
redrawFull();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// ── /exit ─────────────────────────────────────────────────────────────
|
|
610
|
+
if (key && (key.name === 'return' || key.name === 'enter') &&
|
|
611
|
+
(input.trim() === '/exit' || input.trim() === '/quit')) {
|
|
612
|
+
cleanup();
|
|
613
|
+
if (options.signal)
|
|
614
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
615
|
+
return await exitCleanly(options.onExit?.() ?? '\x1b[32mGoodbye! Stay Tuned with Moderado!\x1b[0m');
|
|
616
|
+
}
|
|
617
|
+
// ── Normal keys ───────────────────────────────────────────────────────
|
|
618
|
+
if (!key) {
|
|
619
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
620
|
+
input = input.slice(0, caret) + str + input.slice(caret);
|
|
621
|
+
caret += 1;
|
|
622
|
+
updateMentions();
|
|
623
|
+
redrawCard();
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// Shift+Tab — toggle auto-approve
|
|
628
|
+
if ((key.name === 'tab' && key.shift) || key.sequence === '\x1b[Z') {
|
|
629
|
+
currentAutoApprove = !currentAutoApprove;
|
|
630
|
+
redrawCard();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// Tab — autocomplete mention, autocomplete slash command or toggle mode
|
|
634
|
+
if (key.name === 'tab' && !key.shift) {
|
|
635
|
+
const mention = activeMentionToken(input, caret);
|
|
636
|
+
if (mention && mentionFiles.length > 0) {
|
|
637
|
+
const chosen = mentionFiles[Math.min(mentionSelection, mentionFiles.length - 1)];
|
|
638
|
+
const before = input.slice(0, mention.start);
|
|
639
|
+
const after = input.slice(caret);
|
|
640
|
+
const insert = `@${chosen} `;
|
|
641
|
+
input = before + insert + after;
|
|
642
|
+
caret = before.length + insert.length;
|
|
643
|
+
updateMentions();
|
|
644
|
+
redrawCard();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (input.startsWith('/')) {
|
|
648
|
+
const matching = getMatchingCommands(input);
|
|
649
|
+
if (matching.length > 0) {
|
|
650
|
+
setInput(matching[Math.min(commandSelection, matching.length - 1)].name);
|
|
651
|
+
commandSelection = 0;
|
|
652
|
+
redrawCard();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
currentMode = currentMode === 'Plan' ? 'Execute' : 'Plan';
|
|
657
|
+
redrawCard();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (mentionFiles.length > 0 && (key.name === 'up' || key.name === 'down')) {
|
|
661
|
+
mentionSelection = ((mentionSelection + (key.name === 'up' ? -1 : 1)) % mentionFiles.length + mentionFiles.length) % mentionFiles.length;
|
|
662
|
+
redrawCard();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (input.startsWith('/') && (key.name === 'up' || key.name === 'down')) {
|
|
666
|
+
const matching = getMatchingCommands(input);
|
|
667
|
+
if (matching.length > 0) {
|
|
668
|
+
commandSelection = ((commandSelection + (key.name === 'up' ? -1 : 1)) % matching.length + matching.length) % matching.length;
|
|
669
|
+
redrawCard();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!input.startsWith('/') && (key.name === 'up' || key.name === 'down') && questionHistory.length > 0) {
|
|
674
|
+
if (questionHistoryIndex === questionHistory.length)
|
|
675
|
+
questionHistoryDraft = input;
|
|
676
|
+
const recalled = navigateQuestionHistory(questionHistory, key.name === 'up' ? -1 : 1, questionHistoryIndex, questionHistoryDraft);
|
|
677
|
+
setInput(recalled.input);
|
|
678
|
+
questionHistoryIndex = recalled.index;
|
|
679
|
+
redrawCard();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Enter — submit real user message
|
|
683
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
684
|
+
const matching = getMatchingCommands(input);
|
|
685
|
+
if (input.startsWith('/') && matching.length > 0 && !matching.some((command) => command.name === input.trim())) {
|
|
686
|
+
setInput(matching[Math.min(commandSelection, matching.length - 1)].name);
|
|
687
|
+
commandSelection = 0;
|
|
688
|
+
redrawCard();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const trimmed = input.trim();
|
|
692
|
+
const animateFirstChat = options.chatQuestion === undefined && trimmed.length > 0;
|
|
693
|
+
cleanup();
|
|
694
|
+
if (options.signal)
|
|
695
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
696
|
+
if (animateFirstChat)
|
|
697
|
+
await animateChatWordmark(stdout);
|
|
698
|
+
const moveDown = 4 + getExtraLines();
|
|
699
|
+
stdout.write(`\x1b[${moveDown}B\r\n\x1b[0 q\x1b[?25h`);
|
|
700
|
+
resolve({ text: trimmed, mode: currentMode, autoApprove: currentAutoApprove });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
// Caret movement (cursor-only hop) and forward delete (repaint: the
|
|
704
|
+
// line shrank, so the old trailing character must be cleared).
|
|
705
|
+
if (key.name === 'left' || key.name === 'right' || key.name === 'delete') {
|
|
706
|
+
if (key.name === 'left') {
|
|
707
|
+
caret = Math.max(0, caret - 1);
|
|
708
|
+
updateMentions();
|
|
709
|
+
redrawCard();
|
|
710
|
+
}
|
|
711
|
+
else if (key.name === 'right') {
|
|
712
|
+
caret = Math.min(input.length, caret + 1);
|
|
713
|
+
updateMentions();
|
|
714
|
+
redrawCard();
|
|
715
|
+
}
|
|
716
|
+
else if (caret < input.length) {
|
|
717
|
+
input = input.slice(0, caret) + input.slice(caret + 1);
|
|
718
|
+
updateMentions();
|
|
719
|
+
redrawCard();
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
// Backspace
|
|
724
|
+
if (key.name === 'backspace') {
|
|
725
|
+
if (caret > 0) {
|
|
726
|
+
input = input.slice(0, caret - 1) + input.slice(caret);
|
|
727
|
+
caret = Math.max(0, caret - 1);
|
|
728
|
+
commandSelection = 0;
|
|
729
|
+
updateMentions();
|
|
730
|
+
redrawCard();
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
// Printable character
|
|
735
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32 && !key.ctrl && !key.meta) {
|
|
736
|
+
input = input.slice(0, caret) + str + input.slice(caret);
|
|
737
|
+
caret += 1;
|
|
738
|
+
commandSelection = 0;
|
|
739
|
+
questionHistoryIndex = questionHistory.length;
|
|
740
|
+
updateMentions();
|
|
741
|
+
redrawCard();
|
|
742
|
+
}
|
|
743
|
+
})();
|
|
744
|
+
};
|
|
745
|
+
/**
|
|
746
|
+
* Mouse reporting is only useful while the composer owns the keyboard: a
|
|
747
|
+
* click on the composer line places the caret. Popups and sub-prompts take
|
|
748
|
+
* the keyboard over with mouse reporting off, so they keep the terminal's
|
|
749
|
+
* normal text selection, and clicks never leak into their filter fields.
|
|
750
|
+
*/
|
|
751
|
+
const bindComposerInput = () => {
|
|
752
|
+
stdout.write(MOUSE_REPORT_ON);
|
|
753
|
+
stdin.on('keypress', onKeypress);
|
|
754
|
+
};
|
|
755
|
+
const unbindComposerInput = () => {
|
|
756
|
+
stdin.removeListener('keypress', onKeypress);
|
|
757
|
+
stdout.write(MOUSE_REPORT_OFF);
|
|
758
|
+
};
|
|
759
|
+
bindComposerInput();
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
//# sourceMappingURL=composer.js.map
|