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.
@@ -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 independently (used for the ASCII logo / hints). */
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 = getWelcomeIndent(visibleLen(line), width);
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 shortWs = options.workspace.length > 36
97
- ? '...' + options.workspace.slice(-33)
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 approvalLabelRaw = options.autoApprove
104
- ? 'Auto-approve enabled (Shift+Tab)'
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
- const terminalWidth = width ?? (process.stdout?.columns || 80);
160
- const targetWidth = 82;
161
- const boxWidth = Math.max(78, Math.min(terminalWidth, targetWidth));
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 (Press Tab to autocomplete)';
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 nameStr = `\x1b[1;38;5;75m${cmd.name.padEnd(nameColWidth)}\x1b[0m`;
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 descStr = `\x1b[38;5;244m${desc.padEnd(availDescWidth)}\x1b[0m`;
176
- lines.push(`\x1b[38;5;240m│\x1b[0m ${nameStr} ${descStr}\x1b[38;5;240m│\x1b[0m`);
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
- lines.push('\x1b[38;5;240m ╭─ Files (@ to mention, Tab to insert) ──────────────╮\x1b[0m');
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
- const nameStr = isSelected ? `\x1b[1;38;5;75m@${file}\x1b[0m` : `\x1b[38;5;250m@${file}\x1b[0m`;
191
- const lineContent = `${marker}${nameStr}`;
192
- const plainLen = visibleLen(lineContent);
193
- const pad = Math.max(0, 52 - plainLen);
194
- lines.push(`\x1b[38;5;240m │\x1b[0m ${lineContent}${' '.repeat(pad)}\x1b[38;5;240m│\x1b[0m`);
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 ╰─────────────────────────────────────────────────────╯\x1b[0m');
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 maxWidth = Math.max(40, terminalWidth - 4);
242
- const width = Math.max(40, terminalWidth - 2);
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 questionText = `${questionMarker} ${options.chatQuestion ?? ''}`;
249
- lines.push(String.fromCharCode(27) + '[48;5;236m' + questionText + ' '.repeat(Math.max(0, width - visibleLen(questionText))) + String.fromCharCode(27) + '[0m');
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.tokenUsage && options.chatAnswer?.trim() ? `Thought for ${thoughtTimeLabel}` : '';
253
- lines.push(chatStatus ? `\x1b[38;5;244m${chatStatus.slice(0, Math.max(0, terminalWidth - 1))}\x1b[0m` : '');
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(), maxWidth)) {
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 column = getWelcomeIndent(getWelcomeCardWidth(options.width), options.width) + 2 + inputLength;
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
- return `\x1b7\x1b[13;1H\r\x1b[K\x1b[38;5;244mThought for ${label}\x1b[0m\x1b8`;
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
- /** Write one streamed fragment into the answer area without repainting the chat frame. */
284
- export function renderChatAnswerDelta(delta, start, width) {
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 = 1;
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 = 1;
386
+ column = startColumn;
387
+ writeColumn = startColumn;
297
388
  }
298
389
  }
299
- return { sequence: `\x1b[${start.row};${start.column}H\x1b[38;5;253m${delta}\x1b[0m`, row, column };
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
- '\x1b[1m/init\x1b[0m Scaffold AGENTS.md from workspace scan',
350
- '\x1b[1m/model\x1b[0m Switch active AI model (Free, Paid, or Custom)',
351
- '\x1b[1m/connect\x1b[0m Choose a Gateway, hosted, or local provider',
352
- '\x1b[1m/btw\x1b[0m Ask an ephemeral side question (no session pollution)',
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
- stdout.write(`\x1b[1 q\x1b[?25h\x1b[${row};${column + caret}H`);
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 = Math.max(0, width - 1);
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};1H\r\x1b[K\x1b[38;5;244m${text}\x1b[0m${mode ? `${' '.repeat(Math.max(1, rowWidth - text.length - modeText.length))}${modeLabel}` : ''}\x1b8`;
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, Math.max(0, width - 1));
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};1H\r\x1b[K\x1b[38;5;244m${text}\x1b[0m\x1b8`;
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