moderado 0.4.18 → 0.4.28
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 +3 -4
- package/dist/args.js +1 -1
- package/dist/commands/chat.d.ts +1 -3
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +117 -45
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/skills.js +2 -2
- package/dist/commands/skills.js.map +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +16 -8
- package/dist/skills.js.map +1 -1
- package/dist/ui/help_content.d.ts +17 -0
- package/dist/ui/help_content.d.ts.map +1 -0
- package/dist/ui/help_content.js +13 -0
- package/dist/ui/help_content.js.map +1 -0
- package/dist/ui/help_modal.d.ts.map +1 -1
- package/dist/ui/help_modal.js +5 -5
- package/dist/ui/help_modal.js.map +1 -1
- package/dist/ui/provider_connect.js +2 -2
- package/dist/ui/provider_connect.js.map +1 -1
- package/dist/ui/renderer.d.ts +6 -0
- package/dist/ui/renderer.d.ts.map +1 -1
- package/dist/ui/renderer.js +23 -4
- package/dist/ui/renderer.js.map +1 -1
- package/dist/ui/welcome.d.ts +32 -7
- package/dist/ui/welcome.d.ts.map +1 -1
- package/dist/ui/welcome.js +176 -74
- package/dist/ui/welcome.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/welcome.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline';
|
|
2
2
|
import { overlayCentered, dimLines, shadowUnder } from './popup.js';
|
|
3
3
|
import { activeMentionToken, filterMentionCandidates } from './file_mentions.js';
|
|
4
|
+
import { getHelpCommandGroups } from './help_content.js';
|
|
4
5
|
export const MODERADO_ASCII_LOGO = [
|
|
5
6
|
'\x1b[38;5;75m __ _______ ____ __________ ___ ____ ____ \x1b[0m',
|
|
6
7
|
'\x1b[38;5;75m / |/ / __ \\/ __ \\/ ____/ __ \\/ | / __ \\/ __ \\\x1b[0m',
|
|
@@ -16,6 +17,11 @@ export function stripAnsi(str) {
|
|
|
16
17
|
function visibleLen(str) {
|
|
17
18
|
return stripAnsi(str).length;
|
|
18
19
|
}
|
|
20
|
+
function composerInputWindow(input, caret, width) {
|
|
21
|
+
const capacity = Math.max(1, width - 3);
|
|
22
|
+
const start = Math.max(0, Math.min(caret, input.length - capacity));
|
|
23
|
+
return { text: input.slice(start, start + capacity), caretColumn: caret - start };
|
|
24
|
+
}
|
|
19
25
|
/** Card width: nearly full terminal width, like OpenCode/Cline. */
|
|
20
26
|
export function getWelcomeCardWidth(width) {
|
|
21
27
|
const terminalWidth = width ?? (process.stdout.columns || 80);
|
|
@@ -26,13 +32,28 @@ export function getWelcomeIndent(blockWidth, width) {
|
|
|
26
32
|
const terminalWidth = width ?? (process.stdout.columns || 80);
|
|
27
33
|
return Math.max(0, Math.floor((terminalWidth - blockWidth) / 2));
|
|
28
34
|
}
|
|
29
|
-
/** Center each line of a block
|
|
35
|
+
/** Center each line of a block inside the input card's columns (used for the ASCII logo / hints). */
|
|
30
36
|
function centerBlock(lines, width) {
|
|
37
|
+
const terminalWidth = width ?? (process.stdout.columns || 80);
|
|
38
|
+
const cardWidth = getWelcomeCardWidth(terminalWidth);
|
|
39
|
+
const cardIndent = getWelcomeIndent(cardWidth, terminalWidth);
|
|
31
40
|
return lines.map((line) => {
|
|
32
|
-
const pad =
|
|
33
|
-
return ' '.repeat(pad) + line;
|
|
41
|
+
const pad = Math.max(0, Math.floor((cardWidth - visibleLen(line)) / 2));
|
|
42
|
+
return ' '.repeat(cardIndent + pad) + line;
|
|
34
43
|
});
|
|
35
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Geometry of the answer area: it occupies the same columns as the composer
|
|
47
|
+
* card, so the question row, the streamed answer, and the input box all share
|
|
48
|
+
* one width instead of stretching across a wide terminal.
|
|
49
|
+
*/
|
|
50
|
+
export function getChatAnswerArea(width) {
|
|
51
|
+
const terminalWidth = width ?? (process.stdout.columns || 80);
|
|
52
|
+
const cardWidth = getWelcomeCardWidth(terminalWidth);
|
|
53
|
+
const indent = getWelcomeIndent(cardWidth, terminalWidth);
|
|
54
|
+
const textWidth = Math.max(20, cardWidth - 2);
|
|
55
|
+
return { indent, cardWidth, textWidth, startColumn: indent + 1, wrapWidth: indent + textWidth };
|
|
56
|
+
}
|
|
36
57
|
export function renderQueuedCommandsBox(commands, width) {
|
|
37
58
|
if (!commands || commands.length === 0)
|
|
38
59
|
return [];
|
|
@@ -63,7 +84,7 @@ export function renderWelcomeCard(options) {
|
|
|
63
84
|
const width = getWelcomeCardWidth(options.width);
|
|
64
85
|
const indent = ' '.repeat(getWelcomeIndent(width, options.width));
|
|
65
86
|
const displayInput = options.input && options.input.length > 0
|
|
66
|
-
? options.input
|
|
87
|
+
? composerInputWindow(options.input, options.inputCaret ?? options.input.length, width).text
|
|
67
88
|
: '\x1b[38;5;242mAsk anything, I am all ears...\x1b[0m';
|
|
68
89
|
const promptMarker = '\x1b[1;38;5;75m' + String.fromCodePoint(0x276F) + '\x1b[0;48;5;236m';
|
|
69
90
|
const textBox = `${promptMarker} ${displayInput}`;
|
|
@@ -93,18 +114,21 @@ export function renderWelcomeCard(options) {
|
|
|
93
114
|
const spaces4Count = Math.max(1, width - visibleLen(left4) - right4Raw.length);
|
|
94
115
|
const line4 = left4 + ' '.repeat(spaces4Count) + right4;
|
|
95
116
|
// Line 5: Working directory (left) ... model and Auto-approve (right)
|
|
96
|
-
const
|
|
97
|
-
? '
|
|
117
|
+
const approvalLabelRaw = options.autoApprove
|
|
118
|
+
? 'Auto-approve enabled (Shift+Tab)'
|
|
119
|
+
: 'Auto-approve off (Shift+Tab)';
|
|
120
|
+
const modelSpace = Math.max(8, width - approvalLabelRaw.length - 10);
|
|
121
|
+
const shortModel = options.model.length > modelSpace ? '...' + options.model.slice(-(modelSpace - 3)) : options.model;
|
|
122
|
+
const workspaceSpace = Math.max(3, width - approvalLabelRaw.length - shortModel.length - 2);
|
|
123
|
+
const shortWs = options.workspace.length > workspaceSpace
|
|
124
|
+
? '...' + options.workspace.slice(-(workspaceSpace - 3))
|
|
98
125
|
: options.workspace;
|
|
99
126
|
const left5 = `\x1b[38;5;245m${shortWs}\x1b[0m`;
|
|
100
127
|
const approvalLabel = options.autoApprove
|
|
101
128
|
? '\x1b[38;5;114mAuto-approve enabled (Shift+Tab)\x1b[0m'
|
|
102
129
|
: '\x1b[38;5;242mAuto-approve off (Shift+Tab)\x1b[0m';
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
: 'Auto-approve off (Shift+Tab)';
|
|
106
|
-
const modelLabel = `\x1b[38;5;180m${options.model}\x1b[0m`;
|
|
107
|
-
const spaces5Count = Math.max(1, width - shortWs.length - options.model.length - approvalLabelRaw.length - 2);
|
|
130
|
+
const modelLabel = `\x1b[38;5;180m${shortModel}\x1b[0m`;
|
|
131
|
+
const spaces5Count = Math.max(1, width - shortWs.length - shortModel.length - approvalLabelRaw.length - 1);
|
|
108
132
|
const line5 = left5 + ' '.repeat(spaces5Count) + modelLabel + ' ' + approvalLabel;
|
|
109
133
|
const cardLines = [];
|
|
110
134
|
const queuedBox = renderQueuedCommandsBox(options.queuedCommands ?? [], width);
|
|
@@ -115,11 +139,11 @@ export function renderWelcomeCard(options) {
|
|
|
115
139
|
if (!options.mentionFiles?.length && options.input?.startsWith('/')) {
|
|
116
140
|
const matching = getMatchingCommands(options.input);
|
|
117
141
|
if (matching.length)
|
|
118
|
-
cardLines.push(...renderSuggestionsBox(matching, width).map(line => indent + line));
|
|
142
|
+
cardLines.push(...renderSuggestionsBox(matching, width, options.commandSelection ?? 0).map(line => indent + line));
|
|
119
143
|
}
|
|
120
144
|
cardLines.push(indent + surfaceLine(), indent + surfaceLine(textBox), indent + surfaceLine(), indent + line4, indent + line5);
|
|
121
145
|
if (options.mentionFiles && options.mentionFiles.length > 0) {
|
|
122
|
-
cardLines.push(...renderMentionSuggestionsBox(options.mentionFiles, options.mentionSelection ?? 0).map((line) => indent + line));
|
|
146
|
+
cardLines.push(...renderMentionSuggestionsBox(options.mentionFiles, options.mentionSelection ?? 0, width).map((line) => indent + line));
|
|
123
147
|
}
|
|
124
148
|
return cardLines.join('\n');
|
|
125
149
|
}
|
|
@@ -153,60 +177,88 @@ export function selectCommandCandidate(input, currentIndex, direction) {
|
|
|
153
177
|
const index = ((currentIndex + direction) % commands.length + commands.length) % commands.length;
|
|
154
178
|
return commands[index];
|
|
155
179
|
}
|
|
156
|
-
export function renderSuggestionsBox(commands, width) {
|
|
180
|
+
export function renderSuggestionsBox(commands, width, selectedIndex = 0) {
|
|
157
181
|
if (commands.length === 0)
|
|
158
182
|
return [];
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const boxWidth =
|
|
183
|
+
// Exactly the card width so the box shares the composer's columns instead of
|
|
184
|
+
// forcing its own (previously a 78–82 fixed width that escaped narrow cards).
|
|
185
|
+
const boxWidth = width ?? getWelcomeCardWidth(process.stdout?.columns || 80);
|
|
162
186
|
const innerW = boxWidth - 4;
|
|
163
|
-
const title = 'Commands (
|
|
187
|
+
const title = 'Commands (↑↓ browse, Tab to insert)';
|
|
164
188
|
const remainingDashes = Math.max(0, boxWidth - title.length - 5);
|
|
165
189
|
const nameColWidth = 10;
|
|
166
190
|
const availDescWidth = Math.max(20, innerW - nameColWidth - 2);
|
|
167
191
|
const lines = [];
|
|
168
192
|
lines.push('\x1b[38;5;240m╭─ \x1b[1;38;5;75m' + title + '\x1b[0;38;5;240m ' + '─'.repeat(remainingDashes) + '╮\x1b[0m');
|
|
169
|
-
for (const cmd of commands) {
|
|
170
|
-
const
|
|
193
|
+
for (const [index, cmd] of commands.entries()) {
|
|
194
|
+
const isSelected = index === selectedIndex;
|
|
171
195
|
let desc = cmd.desc;
|
|
172
196
|
if (desc.length > availDescWidth) {
|
|
173
197
|
desc = desc.slice(0, availDescWidth - 1) + '…';
|
|
174
198
|
}
|
|
175
|
-
const
|
|
176
|
-
|
|
199
|
+
const nameCell = cmd.name.padEnd(nameColWidth);
|
|
200
|
+
const descCell = desc.padEnd(availDescWidth);
|
|
201
|
+
if (isSelected) {
|
|
202
|
+
// Highlight bar matches the app's list popups: a ❯ cursor plus a dark
|
|
203
|
+
// background across the whole row, so the arrow-key selection is visible.
|
|
204
|
+
lines.push('\x1b[38;5;240m│\x1b[0m' +
|
|
205
|
+
'\x1b[48;5;236m\x1b[1;38;5;75m❯ ' + nameCell +
|
|
206
|
+
'\x1b[0;48;5;236;38;5;253m ' + descCell + '\x1b[0m' +
|
|
207
|
+
'\x1b[38;5;240m│\x1b[0m');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
lines.push('\x1b[38;5;240m│\x1b[0m' +
|
|
211
|
+
' ' + `\x1b[1;38;5;75m${nameCell}\x1b[0m` + ' ' +
|
|
212
|
+
`\x1b[38;5;244m${descCell}\x1b[0m` +
|
|
213
|
+
'\x1b[38;5;240m│\x1b[0m');
|
|
214
|
+
}
|
|
177
215
|
}
|
|
178
216
|
lines.push('\x1b[38;5;240m╰' + '─'.repeat(boxWidth - 2) + '╯\x1b[0m');
|
|
179
217
|
return lines;
|
|
180
218
|
}
|
|
181
|
-
export function renderMentionSuggestionsBox(files, selectedIndex = 0) {
|
|
219
|
+
export function renderMentionSuggestionsBox(files, selectedIndex = 0, width) {
|
|
182
220
|
if (files.length === 0)
|
|
183
221
|
return [];
|
|
222
|
+
// Sized to the composer card (like the queued/commands boxes) so it never
|
|
223
|
+
// breaks the card's left/right boundary.
|
|
224
|
+
const boxWidth = width ?? getWelcomeCardWidth(process.stdout?.columns || 80);
|
|
225
|
+
const title = 'Files (@ to mention, Tab to insert)';
|
|
184
226
|
const lines = [];
|
|
185
|
-
|
|
227
|
+
const remainingDashes = Math.max(0, boxWidth - title.length - 5);
|
|
228
|
+
lines.push('\x1b[38;5;240m╭─ \x1b[1;38;5;75m' + title + '\x1b[0;38;5;240m ' + '─'.repeat(remainingDashes) + '╮\x1b[0m');
|
|
186
229
|
for (let i = 0; i < files.length; i++) {
|
|
187
230
|
const file = files[i];
|
|
188
231
|
const isSelected = i === selectedIndex;
|
|
189
232
|
const marker = isSelected ? '\x1b[1;38;5;75m❯ \x1b[0m' : ' ';
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
233
|
+
// Visible budget: │ + space + marker(2) + label + pad + │ = boxWidth.
|
|
234
|
+
const labelCapacity = Math.max(4, boxWidth - 5);
|
|
235
|
+
let label = `@${file}`;
|
|
236
|
+
if (label.length > labelCapacity)
|
|
237
|
+
label = `@${file.slice(0, Math.max(1, labelCapacity - 4))}...`;
|
|
238
|
+
const nameStr = isSelected ? `\x1b[1;38;5;75m${label}\x1b[0m` : `\x1b[38;5;250m${label}\x1b[0m`;
|
|
239
|
+
const pad = Math.max(0, boxWidth - 5 - label.length);
|
|
240
|
+
lines.push(`\x1b[38;5;240m│\x1b[0m ${marker}${nameStr}${' '.repeat(pad)}\x1b[38;5;240m│\x1b[0m`);
|
|
195
241
|
}
|
|
196
|
-
lines.push('\x1b[38;5;240m
|
|
242
|
+
lines.push('\x1b[38;5;240m╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯\x1b[0m');
|
|
197
243
|
return lines;
|
|
198
244
|
}
|
|
199
|
-
export function renderModeradoHeader() {
|
|
245
|
+
export function renderModeradoHeader(width) {
|
|
200
246
|
return [
|
|
201
247
|
'',
|
|
202
|
-
...centerBlock(MODERADO_ASCII_LOGO),
|
|
248
|
+
...centerBlock(MODERADO_ASCII_LOGO, width),
|
|
203
249
|
'',
|
|
204
|
-
...centerBlock([COMMAND_HINT]),
|
|
250
|
+
...centerBlock([COMMAND_HINT], width),
|
|
205
251
|
].join('\n');
|
|
206
252
|
}
|
|
207
|
-
/** Word-wrap plain text to `maxWidth` visible columns. */
|
|
253
|
+
/** Word-wrap plain text to `maxWidth` visible columns, hard-breaking long words. */
|
|
208
254
|
export function wrapText(text, maxWidth) {
|
|
209
255
|
const out = [];
|
|
256
|
+
const pushWrapped = (word) => {
|
|
257
|
+
// A single word longer than the column budget is split so no line overflows.
|
|
258
|
+
for (let start = 0; start < word.length; start += maxWidth) {
|
|
259
|
+
out.push(word.slice(start, start + maxWidth));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
210
262
|
for (const rawLine of text.split('\n')) {
|
|
211
263
|
if (rawLine.length <= maxWidth) {
|
|
212
264
|
out.push(rawLine);
|
|
@@ -214,6 +266,14 @@ export function wrapText(text, maxWidth) {
|
|
|
214
266
|
}
|
|
215
267
|
let current = '';
|
|
216
268
|
for (const word of rawLine.split(' ')) {
|
|
269
|
+
if (word.length > maxWidth) {
|
|
270
|
+
if (current.length > 0) {
|
|
271
|
+
out.push(current);
|
|
272
|
+
current = '';
|
|
273
|
+
}
|
|
274
|
+
pushWrapped(word);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
217
277
|
if (current.length === 0) {
|
|
218
278
|
current = word;
|
|
219
279
|
}
|
|
@@ -238,27 +298,35 @@ export function wrapText(text, maxWidth) {
|
|
|
238
298
|
export function renderChatScreen(options, height) {
|
|
239
299
|
const terminalWidth = options.width ?? (process.stdout.columns || 80);
|
|
240
300
|
const terminalHeight = height ?? process.stdout.rows ?? 24;
|
|
241
|
-
const
|
|
242
|
-
const
|
|
301
|
+
const area = getChatAnswerArea(terminalWidth);
|
|
302
|
+
const indent = ' '.repeat(area.indent);
|
|
243
303
|
const lines = [];
|
|
244
|
-
lines.push(...renderModeradoHeader().split('\n'));
|
|
304
|
+
lines.push(...renderModeradoHeader(terminalWidth).split('\n'));
|
|
245
305
|
lines.push('');
|
|
246
|
-
// Row 1: the user's question
|
|
306
|
+
// Row 1: the user's question, wrapped onto the same columns as the composer card.
|
|
247
307
|
const questionMarker = '\x1b[1;38;5;75m' + String.fromCodePoint(0x276F) + '\x1b[0;48;5;236m';
|
|
248
|
-
const
|
|
249
|
-
|
|
308
|
+
for (const [index, questionLine] of wrapText(options.chatQuestion ?? '', area.textWidth - 2).entries()) {
|
|
309
|
+
const questionText = `${index === 0 ? questionMarker : ' '} ${questionLine}`;
|
|
310
|
+
lines.push(indent + String.fromCharCode(27) + '[48;5;236m' + questionText + ' '.repeat(Math.max(0, area.cardWidth - visibleLen(questionText))) + String.fromCharCode(27) + '[0m');
|
|
311
|
+
}
|
|
250
312
|
const thoughtTime = options.chatThoughtTime ?? 0;
|
|
251
313
|
const thoughtTimeLabel = thoughtTime > 0 && thoughtTime < 1 ? '<1s' : `${Math.round(thoughtTime)}s`;
|
|
252
|
-
const chatStatus = !options.
|
|
253
|
-
|
|
314
|
+
const chatStatus = !options.chatAnswer?.trim()
|
|
315
|
+
? ''
|
|
316
|
+
: options.chatTaskDone
|
|
317
|
+
? `✔ Done · Thought for ${thoughtTimeLabel}`
|
|
318
|
+
: options.tokenUsage
|
|
319
|
+
? ''
|
|
320
|
+
: `Thought for ${thoughtTimeLabel}`;
|
|
321
|
+
lines.push(chatStatus ? indent + `\x1b[38;5;244m${chatStatus.slice(0, Math.max(0, area.cardWidth - 1))}\x1b[0m` : '');
|
|
254
322
|
// Answer section: multi-line model answer
|
|
255
323
|
if (options.chatAnswer && options.chatAnswer.trim().length > 0) {
|
|
256
324
|
lines.push('');
|
|
257
|
-
for (const answerLine of wrapText(options.chatAnswer.trim(),
|
|
325
|
+
for (const answerLine of wrapText(options.chatAnswer.trim(), area.textWidth)) {
|
|
258
326
|
const heading = answerLine.match(/^\s*(?:#{1,6}\s+([^#]+?)|\*\*(.+?)\*\*)\s*$/);
|
|
259
|
-
lines.push(heading
|
|
327
|
+
lines.push(indent + (heading
|
|
260
328
|
? `\x1b[1;38;5;25m${(heading[1] ?? heading[2]).trim()}\x1b[0m`
|
|
261
|
-
: `\x1b[38;5;253m${answerLine}\x1b[0m`);
|
|
329
|
+
: `\x1b[38;5;253m${answerLine}\x1b[0m`));
|
|
262
330
|
}
|
|
263
331
|
}
|
|
264
332
|
const composer = renderWelcomeCard(options).split('\n');
|
|
@@ -268,7 +336,8 @@ export function renderChatScreen(options, height) {
|
|
|
268
336
|
}
|
|
269
337
|
/** Place the terminal cursor on the first row of the bottom composer. */
|
|
270
338
|
export function renderChatComposerCursor(options, inputLength) {
|
|
271
|
-
const
|
|
339
|
+
const width = getWelcomeCardWidth(options.width);
|
|
340
|
+
const column = getWelcomeIndent(width, options.width) + 2 + Math.min(inputLength, width - 3);
|
|
272
341
|
return `\x1b[1 q\x1b[?25h\x1b[4A\r\x1b[${column}C`;
|
|
273
342
|
}
|
|
274
343
|
export function navigateQuestionHistory(history, direction, index, draft) {
|
|
@@ -276,34 +345,57 @@ export function navigateQuestionHistory(history, direction, index, draft) {
|
|
|
276
345
|
return { input: nextIndex === history.length ? draft : history[nextIndex], index: nextIndex, draft };
|
|
277
346
|
}
|
|
278
347
|
/** Update the fixed chat elapsed-time row without repainting the terminal. */
|
|
279
|
-
export function renderChatThoughtTimeUpdate(thoughtTime) {
|
|
348
|
+
export function renderChatThoughtTimeUpdate(thoughtTime, taskStep, width) {
|
|
349
|
+
const area = getChatAnswerArea(width);
|
|
280
350
|
const label = thoughtTime > 0 && thoughtTime < 1 ? '<1s' : `${Math.round(thoughtTime)}s`;
|
|
281
|
-
|
|
351
|
+
const status = (taskStep && taskStep.maxSteps > 0 && taskStep.step >= 0
|
|
352
|
+
? `Processing task… Step ${taskStep.step}/${taskStep.maxSteps} · ${label}`
|
|
353
|
+
: `Thought for ${label}`).slice(0, area.cardWidth);
|
|
354
|
+
return `\x1b7\x1b[13;${area.startColumn}H\x1b[K\x1b[38;5;244m${status}\x1b[0m\x1b8`;
|
|
282
355
|
}
|
|
283
|
-
/**
|
|
284
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Write one streamed fragment into the answer area without repainting the chat
|
|
358
|
+
* frame. Wrapped lines return to `startColumn` so the answer keeps the same
|
|
359
|
+
* columns as the composer card instead of stretching across the terminal.
|
|
360
|
+
*/
|
|
361
|
+
export function renderChatAnswerDelta(delta, start, width, startColumn = 1) {
|
|
285
362
|
let row = start.row;
|
|
286
363
|
let column = start.column;
|
|
364
|
+
let writeColumn = start.column;
|
|
365
|
+
let sequence = '';
|
|
366
|
+
let line = '';
|
|
367
|
+
const flush = () => {
|
|
368
|
+
if (line.length === 0)
|
|
369
|
+
return;
|
|
370
|
+
sequence += `\x1b[${row};${writeColumn}H\x1b[38;5;253m${line}\x1b[0m`;
|
|
371
|
+
line = '';
|
|
372
|
+
};
|
|
287
373
|
for (const character of delta) {
|
|
288
374
|
if (character === '\n') {
|
|
375
|
+
flush();
|
|
289
376
|
row++;
|
|
290
|
-
column =
|
|
377
|
+
column = startColumn;
|
|
378
|
+
writeColumn = startColumn;
|
|
291
379
|
continue;
|
|
292
380
|
}
|
|
381
|
+
line += character;
|
|
293
382
|
column++;
|
|
294
383
|
if (column > width) {
|
|
384
|
+
flush();
|
|
295
385
|
row++;
|
|
296
|
-
column =
|
|
386
|
+
column = startColumn;
|
|
387
|
+
writeColumn = startColumn;
|
|
297
388
|
}
|
|
298
389
|
}
|
|
299
|
-
|
|
390
|
+
flush();
|
|
391
|
+
return { sequence, row, column };
|
|
300
392
|
}
|
|
301
393
|
export function renderFullWelcomeScreen(options, height) {
|
|
302
394
|
if (options.chatQuestion !== undefined) {
|
|
303
395
|
return renderChatScreen(options, height);
|
|
304
396
|
}
|
|
305
397
|
return [
|
|
306
|
-
renderModeradoHeader(),
|
|
398
|
+
renderModeradoHeader(options.width),
|
|
307
399
|
'',
|
|
308
400
|
renderWelcomeCard(options),
|
|
309
401
|
].join('\n') + '\n';
|
|
@@ -346,21 +438,10 @@ export function renderHelpPopupBox(version, workspace, width) {
|
|
|
346
438
|
const remainingDashes = Math.max(0, boxWidth - titleStr.length - 5);
|
|
347
439
|
const content = [
|
|
348
440
|
'\x1b[1;38;5;75mSlash Commands:\x1b[0m',
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
'\x1b[1m/mcp\x1b[0m Manage local MCP servers',
|
|
354
|
-
'\x1b[1m/session\x1b[0m Create, resume, undo, redo, share, export, or compact sessions',
|
|
355
|
-
'\x1b[1m/queue\x1b[0m Add, inspect, or clear queued follow-up commands',
|
|
356
|
-
'\x1b[1m/workflow\x1b[0m Inspect Git, build plans, or undo agent changes',
|
|
357
|
-
'\x1b[1m/review\x1b[0m Review current workspace changes for bugs',
|
|
358
|
-
'\x1b[1m/map\x1b[0m Show a compact workspace file map',
|
|
359
|
-
'\x1b[1m/budget\x1b[0m Set a per-task token limit',
|
|
360
|
-
'\x1b[1m/skills\x1b[0m List skills; enable or disable user skills',
|
|
361
|
-
'\x1b[1m/clear\x1b[0m Reset conversation memory and context history',
|
|
362
|
-
'\x1b[1m/help\x1b[0m Display this commands, shortcuts & version guide',
|
|
363
|
-
'\x1b[1m/exit\x1b[0m Exit Moderado session cleanly',
|
|
441
|
+
...getHelpCommandGroups().flatMap(({ name, commands }) => [
|
|
442
|
+
`\x1b[1;38;5;180m${name}:\x1b[0m`,
|
|
443
|
+
...commands.map(([command, description]) => `\x1b[1m${command.padEnd(11)}\x1b[0m${description}`),
|
|
444
|
+
]),
|
|
364
445
|
'',
|
|
365
446
|
'\x1b[1;38;5;114mKeyboard Shortcuts:\x1b[0m',
|
|
366
447
|
'\x1b[1mTab\x1b[0m Toggle between [Plan] and [Execute] mode',
|
|
@@ -368,6 +449,7 @@ export function renderHelpPopupBox(version, workspace, width) {
|
|
|
368
449
|
'\x1b[1mCtrl+C\x1b[0m Cancel active inference or exit session',
|
|
369
450
|
'\x1b[1mEsc\x1b[0m Stop the active task',
|
|
370
451
|
'\x1b[1mLeft/Right\x1b[0m Move the caret inside the bottom input line',
|
|
452
|
+
'\x1b[1mUp/Down\x1b[0m Browse slash commands after "/", else recall history',
|
|
371
453
|
'\x1b[1mMouse click\x1b[0m Place the caret on the input line',
|
|
372
454
|
'',
|
|
373
455
|
`\x1b[38;5;245mWorkspace:\x1b[0m \x1b[38;5;253m${shortWs}\x1b[0m`,
|
|
@@ -485,9 +567,12 @@ export async function promptInteractiveTurn(options) {
|
|
|
485
567
|
mode: currentMode,
|
|
486
568
|
autoApprove: currentAutoApprove,
|
|
487
569
|
input,
|
|
570
|
+
inputCaret: caret,
|
|
571
|
+
commandSelection,
|
|
488
572
|
chatQuestion: options.chatQuestion,
|
|
489
573
|
chatAnswer: options.chatAnswer,
|
|
490
574
|
chatThoughtTime: options.chatThoughtTime,
|
|
575
|
+
chatTaskDone: options.chatTaskDone,
|
|
491
576
|
outputTokenRate: options.outputTokenRate,
|
|
492
577
|
tokenUsage: options.tokenUsage,
|
|
493
578
|
queuedCommands: options.queuedCommands,
|
|
@@ -527,7 +612,8 @@ export async function promptInteractiveTurn(options) {
|
|
|
527
612
|
*/
|
|
528
613
|
const positionCursorOnInput = () => {
|
|
529
614
|
const { row, column } = getComposerAnchor();
|
|
530
|
-
|
|
615
|
+
const { caretColumn } = composerInputWindow(input, caret, getWelcomeCardWidth());
|
|
616
|
+
stdout.write(`\x1b[1 q\x1b[?25h\x1b[${row};${column + caretColumn}H`);
|
|
531
617
|
};
|
|
532
618
|
/** Full-screen redraw: clear everything, repaint welcome TUI, reposition cursor. */
|
|
533
619
|
const redrawFull = () => {
|
|
@@ -670,6 +756,13 @@ export async function promptInteractiveTurn(options) {
|
|
|
670
756
|
if (newId && newId !== currentModel) {
|
|
671
757
|
currentModel = newId;
|
|
672
758
|
}
|
|
759
|
+
const autoContinuePrompt = options.takeAutoContinuePrompt?.();
|
|
760
|
+
if (autoContinuePrompt) {
|
|
761
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
762
|
+
cleanup();
|
|
763
|
+
resolve({ text: autoContinuePrompt, mode: currentMode, autoApprove: currentAutoApprove });
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
673
766
|
}
|
|
674
767
|
// Selection flow left stdin in non-raw mode — restore it for our TUI
|
|
675
768
|
readline.emitKeypressEvents(stdin);
|
|
@@ -712,6 +805,13 @@ export async function promptInteractiveTurn(options) {
|
|
|
712
805
|
stdout.write(renderWelcomePopupLayer(getOptions(), popupLines, stdout.columns, stdout.rows));
|
|
713
806
|
});
|
|
714
807
|
}
|
|
808
|
+
const autoContinuePrompt = options.takeAutoContinuePrompt?.();
|
|
809
|
+
if (autoContinuePrompt) {
|
|
810
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
811
|
+
cleanup();
|
|
812
|
+
resolve({ text: autoContinuePrompt, mode: currentMode, autoApprove: currentAutoApprove });
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
715
815
|
readline.emitKeypressEvents(stdin);
|
|
716
816
|
stdin.resume();
|
|
717
817
|
stdin.setRawMode(true);
|
|
@@ -1014,11 +1114,12 @@ export function formatTokenUsage(event) {
|
|
|
1014
1114
|
}
|
|
1015
1115
|
/** Update only the chat status row, preserving the answer/composer cursor. */
|
|
1016
1116
|
export function renderChatUsageUpdate(event, width = process.stdout.columns || 80, budgetStatus, usageDetails, terminalHeight = process.stdout.rows || 24, mode) {
|
|
1117
|
+
const area = getChatAnswerArea(width);
|
|
1017
1118
|
const usageText = formatTokenUsage(event);
|
|
1018
1119
|
const suffix = `${usageDetails ? ` | ${usageDetails}` : ''}${budgetStatus ? ` | ${budgetStatus}` : ''}`;
|
|
1019
1120
|
const modeLabel = mode === 'Plan' ? '\x1b[1;38;5;75m[Plan]\x1b[0m / Execute (Tab)' : 'Plan / \x1b[1;38;5;75m[Execute]\x1b[0m (Tab)';
|
|
1020
1121
|
const modeText = mode === 'Plan' ? '[Plan] / Execute (Tab)' : 'Plan / [Execute] (Tab)';
|
|
1021
|
-
const rowWidth =
|
|
1122
|
+
const rowWidth = area.cardWidth;
|
|
1022
1123
|
const textLimit = Math.max(0, rowWidth - (mode ? modeText.length + 1 : 0));
|
|
1023
1124
|
const usageLimit = Math.max(0, textLimit - suffix.length);
|
|
1024
1125
|
const clippedUsage = mode && usageText.length > usageLimit
|
|
@@ -1026,14 +1127,15 @@ export function renderChatUsageUpdate(event, width = process.stdout.columns || 8
|
|
|
1026
1127
|
: usageText;
|
|
1027
1128
|
const text = `${clippedUsage}${suffix}`.slice(0, textLimit);
|
|
1028
1129
|
const row = Math.max(1, terminalHeight - 2);
|
|
1029
|
-
return `\x1b7\x1b[${row}
|
|
1130
|
+
return `\x1b7\x1b[${row};${area.startColumn}H\x1b[K\x1b[38;5;244m${text}\x1b[0m${mode ? `${' '.repeat(Math.max(1, rowWidth - text.length - modeText.length))}${modeLabel}` : ''}\x1b8`;
|
|
1030
1131
|
}
|
|
1031
1132
|
/** Show trusted, schema-validated Gateway route changes without provider supplied prose. */
|
|
1032
1133
|
export function renderGatewayFallbackUpdate(event, width = process.stdout.columns || 80) {
|
|
1134
|
+
const area = getChatAnswerArea(width);
|
|
1033
1135
|
const destination = event.toProvider ? `${event.toProvider}/${event.toModel}` : event.toModel;
|
|
1034
1136
|
const reason = event.reason === 'rate_limited_or_unavailable' ? 'after an upstream route was rate limited or unavailable' : 'after a fallback';
|
|
1035
|
-
const text = `Gateway route changed to ${destination} ${reason}`.slice(0,
|
|
1137
|
+
const text = `Gateway route changed to ${destination} ${reason}`.slice(0, area.cardWidth);
|
|
1036
1138
|
const row = renderModeradoHeader().split('\n').length + 3;
|
|
1037
|
-
return `\x1b7\x1b[${row}
|
|
1139
|
+
return `\x1b7\x1b[${row};${area.startColumn}H\x1b[K\x1b[38;5;244m${text}\x1b[0m\x1b8`;
|
|
1038
1140
|
}
|
|
1039
1141
|
//# sourceMappingURL=welcome.js.map
|