agent-mp 0.5.22 → 0.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/input.js CHANGED
@@ -1,347 +1,409 @@
1
1
  import chalk from 'chalk';
2
- // ─── Midas brand colors ──────────────────────────────────────────────────────
3
- const T = (s) => chalk.rgb(0, 185, 180)(s); // teal — borders
4
- const B = (s) => chalk.rgb(30, 110, 185)(s); // blue — prompt arrow
5
- const PREFIX = T('│') + B(' > ');
6
- const PREFIX_CONT = T('') + B(' '); // continuation lines
7
- const PREFIX_COLS = 4; // visual width of "│ > " and "│ "
8
- // Maximum content rows the input box can grow to (Shift+Enter / word-wrap).
9
- const MAX_CONTENT_ROWS = 4;
10
- const ACTIVITY_LINES = 5;
11
- // Reserved rows at the bottom:
12
- // Idle: 7 = 1 status row + up to 4 content + 2 borders
13
- // Active: 10 = 7 (activity box) + 3 (input box: 1 content + 2 borders)
14
- // The scroll region is updated whenever activity mode toggles.
15
- const IDLE_RESERVED = MAX_CONTENT_ROWS + 3; // 7
16
- const ACTIVE_RESERVED = ACTIVITY_LINES + 2 + 3; // 10 = activity(7) + input(3)
17
- // ─── FixedInput ──────────────────────────────────────────────────────────────
2
+ const DIM = chalk.dim;
3
+ const B = (s) => chalk.rgb(30, 110, 185)(s);
4
+ const PROMPT = B('> ');
5
+ const PROMPT_W = 2;
6
+ const INDENT = ' ';
7
+ const PLACEHOLDER = 'Type your message… (Shift+Enter / \\ para nueva línea)';
8
+ /**
9
+ * Inline prompt, styled like Gemini/Qwen CLI.
10
+ * Draws at the current cursor line; when output arrives we erase the area
11
+ * (line-by-line, \x1b[2K) and redraw below. No absolute positioning.
12
+ */
18
13
  export class FixedInput {
19
- buf = '';
20
14
  history = [];
21
- histIdx = -1;
22
15
  origLog;
23
- _pasting = false;
24
- _pasteAccum = '';
25
- _drawPending = false;
26
- // ── Activity box state (null = input mode, string = activity mode) ──────────
27
16
  _activityHeader = null;
28
17
  _activityLines = [];
29
- get rows() { return process.stdout.rows || 24; }
18
+ _inputBuffer = [];
19
+ _cursorPos = 0;
20
+ _pasting = false;
21
+ _pasteAccum = '';
22
+ _resolveInput;
23
+ _inputActive = false;
24
+ // Geometry after the last draw.
25
+ _areaRows = 0; // total rows the area occupies
26
+ _cursorRow = 0; // row (0-indexed from top of area) where terminal cursor sits
27
+ _onResize;
30
28
  get cols() { return process.stdout.columns || 80; }
31
- get _reservedRows() { return this._activityHeader !== null ? ACTIVE_RESERVED : IDLE_RESERVED; }
32
- get scrollBottom() { return this.rows - this._reservedRows; }
33
- _contentRows() {
34
- // During activity mode only 1 content row fits below the activity box
35
- if (this._activityHeader !== null)
36
- return 1;
37
- const w = this.cols - PREFIX_COLS - 2;
38
- if (w <= 0)
39
- return 1;
40
- if (!this.buf)
41
- return 1;
42
- let n = 0;
43
- for (const seg of this.buf.split('\n'))
44
- n += Math.max(1, Math.ceil((seg.length || 1) / w));
45
- return Math.min(n, MAX_CONTENT_ROWS);
46
- }
47
- // ── Lifecycle ──────────────────────────────────────────────────────────────
48
29
  setup() {
49
30
  this.origLog = console.log;
50
31
  console.log = (...args) => {
51
- const text = args.map(a => (typeof a === 'string' ? a : String(a))).join(' ');
32
+ const text = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
52
33
  this.println(text);
53
34
  };
54
- this._setScrollRegion();
55
- process.stdout.write(`\x1b[${this.scrollBottom};1H`);
56
- this._clearReserved();
57
- this._drawBox();
58
35
  process.stdout.write('\x1b[?2004h');
59
- process.stdout.on('resize', () => {
60
- this._setScrollRegion();
61
- this._clearReserved();
62
- this._drawBox();
63
- });
36
+ this._onResize = () => this._redraw();
37
+ process.stdout.on('resize', this._onResize);
64
38
  }
65
39
  teardown() {
66
- this._activityHeader = null;
67
- this._activityLines = [];
68
40
  console.log = this.origLog;
69
41
  process.stdout.write('\x1b[?2004l');
70
- process.stdout.write('\x1b[r');
71
- process.stdout.write('\x1b[?25h');
72
- process.stdout.write(`\x1b[${this.rows};1H\n`);
42
+ if (this._onResize)
43
+ process.stdout.off('resize', this._onResize);
73
44
  }
74
- redrawBox() { this._drawBox(); }
75
45
  suspend() {
46
+ this._clearArea();
76
47
  console.log = this.origLog;
77
48
  process.stdout.write('\x1b[?2004l');
78
- process.stdout.write('\x1b[r');
79
- this._clearReserved();
80
- process.stdout.write(`\x1b[${this.scrollBottom};1H`);
81
49
  return () => {
82
50
  console.log = (...args) => {
83
- const text = args.map(a => (typeof a === 'string' ? a : String(a))).join(' ');
51
+ const text = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
84
52
  this.println(text);
85
53
  };
86
- this._setScrollRegion();
87
- this._clearReserved();
88
- this._drawBox();
89
54
  process.stdout.write('\x1b[?2004h');
55
+ this._redraw();
90
56
  };
91
57
  }
92
- // ── Activity box API ───────────────────────────────────────────────────────
93
- /** Enter activity mode: show the 5-line log box instead of the input box. */
94
58
  startActivity(header) {
95
59
  this._activityHeader = header;
96
60
  this._activityLines = [];
97
- this._setScrollRegion();
98
- this._drawBox();
61
+ this._redraw();
99
62
  }
100
- /** Update the header line (spinner frame + elapsed time) without clearing lines. */
101
63
  updateActivityHeader(header) {
102
64
  this._activityHeader = header;
103
- this._drawBox();
65
+ this._redraw();
104
66
  }
105
- /**
106
- * Append a line to the activity log (keeps last ACTIVITY_LINES lines).
107
- * Strips ANSI codes and skips blank or pure-JSON lines.
108
- */
109
67
  pushActivity(rawLine) {
110
68
  if (this._activityHeader === null)
111
69
  return;
112
- // Strip ANSI escape sequences
113
- const clean = rawLine
114
- .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
115
- .replace(/[^\x20-\x7e\u00a0-\uffff]/g, '')
116
- .trim();
70
+ const clean = rawLine.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').trim();
117
71
  if (!clean)
118
72
  return;
119
73
  this._activityLines.push(clean);
120
- if (this._activityLines.length > ACTIVITY_LINES)
74
+ if (this._activityLines.length > 5)
121
75
  this._activityLines.shift();
122
- this._scheduleDraw();
76
+ this._redraw();
77
+ }
78
+ /** Replace all content lines at once (for streaming preview). */
79
+ setActivityLines(lines) {
80
+ if (this._activityHeader === null)
81
+ return;
82
+ this._activityLines = lines.map(l => l.slice(0, this.cols - 4));
83
+ this._redraw();
123
84
  }
124
- /** Leave activity mode and restore the normal input box. */
125
85
  stopActivity() {
126
- // Explicitly clear the full ACTIVE reserved zone before shrinking
127
- // the scroll region — otherwise activity box rows bleed into scroll history.
128
- const activeSB = this.rows - ACTIVE_RESERVED;
129
- for (let r = activeSB + 1; r <= this.rows; r++)
130
- process.stdout.write(`\x1b[${r};1H\x1b[2K`);
131
86
  this._activityHeader = null;
132
87
  this._activityLines = [];
133
- this._setScrollRegion();
134
- this._drawBox();
88
+ this._redraw();
89
+ }
90
+ println(text) {
91
+ this._clearArea();
92
+ process.stdout.write(text + '\n');
93
+ this._redraw();
94
+ }
95
+ printSeparator() {
96
+ this._clearArea();
97
+ process.stdout.write(DIM('─'.repeat(this.cols - 1)) + '\n');
98
+ this._redraw();
99
+ }
100
+ redrawBox() {
101
+ this._redraw();
135
102
  }
136
- // ── Input ──────────────────────────────────────────────────────────────────
137
103
  readLine() {
138
- this.buf = '';
139
- this.histIdx = -1;
140
- this._drawBox();
141
- return new Promise((resolve) => {
104
+ this._inputBuffer = [];
105
+ this._cursorPos = 0;
106
+ this._inputActive = true;
107
+ this._redraw();
108
+ if (process.stdin.isTTY)
142
109
  process.stdin.setRawMode(true);
143
- process.stdin.resume();
144
- const done = (line) => {
110
+ process.stdin.resume();
111
+ return new Promise((resolve) => {
112
+ this._resolveInput = resolve;
113
+ const finish = (value) => {
114
+ this._inputActive = false;
115
+ this._clearArea();
145
116
  process.stdin.removeListener('data', onData);
146
117
  if (process.stdin.isTTY)
147
118
  process.stdin.setRawMode(false);
148
- this.buf = '';
149
- this._drawBox();
150
- resolve(line);
119
+ process.stdin.pause();
120
+ const r = this._resolveInput;
121
+ this._resolveInput = undefined;
122
+ r?.(value);
151
123
  };
152
124
  const onData = (data) => {
153
- const hex = data.toString('hex');
154
125
  const key = data.toString();
155
- // ── Bracketed paste: start ────────────────────────────────────
156
- if (key.includes('\x1b[200~')) {
126
+ const hex = data.toString('hex');
127
+ // Bracketed paste start (may include end marker in same chunk).
128
+ if (!this._pasting && key.includes('\x1b[200~')) {
157
129
  this._pasting = true;
158
130
  this._pasteAccum = '';
159
- const after = key.slice(key.indexOf('\x1b[200~') + 6);
160
- if (after)
161
- this._pasteAccum += after;
131
+ const afterStart = key.slice(key.indexOf('\x1b[200~') + 6);
132
+ const endIdx = afterStart.indexOf('\x1b[201~');
133
+ if (endIdx !== -1) {
134
+ this._pasteAccum += afterStart.slice(0, endIdx);
135
+ this._commitPaste();
136
+ }
137
+ else {
138
+ this._pasteAccum += afterStart;
139
+ }
162
140
  return;
163
141
  }
164
- // ── Bracketed paste: accumulate ───────────────────────────────
165
142
  if (this._pasting) {
166
- if (key.includes('\x1b[201~')) {
167
- const before = key.slice(0, key.indexOf('\x1b[201~'));
168
- this._pasteAccum += before;
169
- this.buf += this._pasteAccum;
170
- this._pasting = false;
171
- this._pasteAccum = '';
172
- this._scheduleDraw();
143
+ const endIdx = key.indexOf('\x1b[201~');
144
+ if (endIdx !== -1) {
145
+ this._pasteAccum += key.slice(0, endIdx);
146
+ this._commitPaste();
173
147
  }
174
148
  else {
175
149
  this._pasteAccum += key;
176
150
  }
177
151
  return;
178
152
  }
179
- // ── Shift+Enter newline ────────────────────────────────────
180
- if (hex === '5c0d' ||
181
- key === '\x0a' ||
182
- hex === '1b5b31333b327e' ||
183
- hex === '1b5b31333b3275' ||
184
- hex === '1b4f4d') {
185
- this.buf += '\n';
186
- this._scheduleDraw();
187
- // ── Enter → submit ───────────────────────────────────────────
188
- }
189
- else if (key === '\r') {
190
- const line = this.buf;
153
+ // Enter submit
154
+ if (key === '\r') {
155
+ const line = this._inputBuffer.join('');
191
156
  if (line.trim()) {
192
157
  this.history.unshift(line);
193
158
  if (this.history.length > 200)
194
159
  this.history.pop();
195
160
  }
196
- done(line);
161
+ finish(line);
162
+ return;
163
+ }
164
+ // Ctrl+J (LF) — insert newline
165
+ if (key === '\n') {
166
+ this._inputBuffer.splice(this._cursorPos, 0, '\n');
167
+ this._cursorPos++;
168
+ this._redraw();
169
+ return;
170
+ }
171
+ // Backspace
172
+ if (key === '\x7f' || key === '\x08') {
173
+ if (this._cursorPos > 0) {
174
+ this._inputBuffer.splice(this._cursorPos - 1, 1);
175
+ this._cursorPos--;
176
+ this._redraw();
177
+ }
178
+ return;
197
179
  }
198
- else if (key === '\x7f' || key === '\x08') {
199
- if (this.buf.length > 0) {
200
- this.buf = this.buf.slice(0, -1);
201
- this._scheduleDraw();
180
+ // Delete
181
+ if (hex === '1b5b337e') {
182
+ if (this._cursorPos < this._inputBuffer.length) {
183
+ this._inputBuffer.splice(this._cursorPos, 1);
184
+ this._redraw();
202
185
  }
186
+ return;
203
187
  }
204
- else if (key === '\x03') {
188
+ // Ctrl+C
189
+ if (key === '\x03') {
190
+ this._clearArea();
191
+ if (process.stdin.isTTY)
192
+ process.stdin.setRawMode(false);
193
+ process.stdin.pause();
205
194
  this.teardown();
206
195
  process.exit(0);
207
196
  }
208
- else if (key === '\x04') {
209
- done('/exit');
197
+ // Ctrl+D
198
+ if (key === '\x04') {
199
+ finish('/exit');
200
+ return;
201
+ }
202
+ // Ctrl+U — clear line
203
+ if (key === '\x15') {
204
+ this._inputBuffer = [];
205
+ this._cursorPos = 0;
206
+ this._redraw();
207
+ return;
208
+ }
209
+ // Ctrl+W / Alt+Backspace — delete prev word
210
+ if (key === '\x17' || hex === '1b7f') {
211
+ const before = this._inputBuffer.slice(0, this._cursorPos).join('').trimEnd();
212
+ const lastSpace = before.lastIndexOf(' ');
213
+ const newPos = lastSpace === -1 ? 0 : lastSpace + 1;
214
+ this._inputBuffer = [
215
+ ...this._inputBuffer.slice(0, newPos),
216
+ ...this._inputBuffer.slice(this._cursorPos),
217
+ ];
218
+ this._cursorPos = newPos;
219
+ this._redraw();
220
+ return;
210
221
  }
211
- else if (key === '\x15') {
212
- this.buf = '';
213
- this._scheduleDraw();
222
+ // Ctrl+A / Home
223
+ if (key === '\x01' || hex === '1b5b48' || hex === '1b4f48') {
224
+ this._cursorPos = 0;
225
+ this._redraw();
226
+ return;
227
+ }
228
+ // Ctrl+E / End
229
+ if (key === '\x05' || hex === '1b5b46' || hex === '1b4f46') {
230
+ this._cursorPos = this._inputBuffer.length;
231
+ this._redraw();
232
+ return;
214
233
  }
215
- else if (hex === '1b5b41') { // Arrow
216
- if (this.histIdx + 1 < this.history.length) {
217
- this.histIdx++;
218
- this.buf = this.history[this.histIdx];
219
- this._scheduleDraw();
234
+ // Arrow Left
235
+ if (hex === '1b5b44') {
236
+ if (this._cursorPos > 0) {
237
+ this._cursorPos--;
238
+ this._redraw();
220
239
  }
240
+ return;
221
241
  }
222
- else if (hex === '1b5b42') { // Arrow
223
- if (this.histIdx > 0) {
224
- this.histIdx--;
225
- this.buf = this.history[this.histIdx];
242
+ // Arrow Right
243
+ if (hex === '1b5b43') {
244
+ if (this._cursorPos < this._inputBuffer.length) {
245
+ this._cursorPos++;
246
+ this._redraw();
226
247
  }
227
- else {
228
- this.histIdx = -1;
229
- this.buf = '';
248
+ return;
249
+ }
250
+ // Arrow Up — last history
251
+ if (hex === '1b5b41') {
252
+ if (this.history.length > 0) {
253
+ this._inputBuffer = this.history[0].split('');
254
+ this._cursorPos = this._inputBuffer.length;
255
+ this._redraw();
230
256
  }
231
- this._scheduleDraw();
257
+ return;
232
258
  }
233
- else if (key.length >= 1 && key.charCodeAt(0) >= 32 && !key.startsWith('\x1b')) {
234
- this.buf += key;
235
- this._scheduleDraw();
259
+ // Arrow Down clear
260
+ if (hex === '1b5b42') {
261
+ if (this._inputBuffer.length > 0) {
262
+ this._inputBuffer = [];
263
+ this._cursorPos = 0;
264
+ this._redraw();
265
+ }
266
+ return;
267
+ }
268
+ // Shift/Alt+Enter — insert newline (various terminals)
269
+ if (hex === '1b0d' || hex === '1b0a' ||
270
+ hex === '1b5b31333b327e' || hex === '1b5b31333b3275' ||
271
+ hex === '1b4f4d') {
272
+ this._inputBuffer.splice(this._cursorPos, 0, '\n');
273
+ this._cursorPos++;
274
+ this._redraw();
275
+ return;
276
+ }
277
+ // Regular typed characters.
278
+ // Shift+Enter fallback: gnome-terminal sends '\' + LF (or sometimes just '\').
279
+ // Collapse a '\' — with an optional trailing LF/CR — into a single newline.
280
+ // Literal '\' still works through paste (Ctrl+Shift+V) since that path is
281
+ // handled by the bracketed-paste branch.
282
+ if (key.length >= 1 && !key.startsWith('\x1b')) {
283
+ const raw = [...key];
284
+ const chars = [];
285
+ for (let i = 0; i < raw.length; i++) {
286
+ const c = raw[i];
287
+ if (c === '\\') {
288
+ chars.push('\n');
289
+ if (raw[i + 1] === '\n' || raw[i + 1] === '\r')
290
+ i++;
291
+ continue;
292
+ }
293
+ if (c === '\n' || c.charCodeAt(0) >= 32)
294
+ chars.push(c);
295
+ }
296
+ if (chars.length) {
297
+ this._inputBuffer.splice(this._cursorPos, 0, ...chars);
298
+ this._cursorPos += chars.length;
299
+ this._redraw();
300
+ }
236
301
  }
237
302
  };
238
303
  process.stdin.on('data', onData);
239
304
  });
240
305
  }
241
- // ── Output helpers ─────────────────────────────────────────────────────────
242
- println(text) {
243
- process.stdout.write(`\x1b[${this.scrollBottom};1H`);
244
- process.stdout.write(text + '\n');
245
- this._drawBox();
306
+ _commitPaste() {
307
+ const text = this._pasteAccum.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
308
+ const chars = [...text];
309
+ this._inputBuffer.splice(this._cursorPos, 0, ...chars);
310
+ this._cursorPos += chars.length;
311
+ this._pasting = false;
312
+ this._pasteAccum = '';
313
+ this._redraw();
246
314
  }
247
- printSeparator() {
248
- this.println(chalk.rgb(0, 120, 116)('─'.repeat(this.cols - 1)));
249
- }
250
- // ── Private drawing ────────────────────────────────────────────────────────
251
- _scheduleDraw() {
252
- if (this._drawPending)
253
- return;
254
- this._drawPending = true;
255
- setImmediate(() => { this._drawPending = false; this._drawBox(); });
256
- }
257
- _setScrollRegion() {
258
- const sb = this.scrollBottom;
259
- if (sb >= 1)
260
- process.stdout.write(`\x1b[1;${sb}r`);
261
- }
262
- _clearReserved() {
263
- for (let r = this.scrollBottom + 1; r <= this.rows; r++)
264
- process.stdout.write(`\x1b[${r};1H\x1b[2K`);
265
- }
266
- _drawBox() {
267
- process.stdout.write('\x1b[?25l');
268
- this._clearReserved();
269
- if (this._activityHeader !== null) {
270
- this._drawActivityBox();
315
+ /** Build a string that erases the currently-drawn area and leaves the cursor at col 0 of the top row. */
316
+ _buildClear() {
317
+ if (this._areaRows === 0)
318
+ return '';
319
+ let s = '\r';
320
+ // Move up to first row of area.
321
+ if (this._cursorRow > 0)
322
+ s += `\x1b[${this._cursorRow}A`;
323
+ // Clear each row (line-by-line most portable).
324
+ for (let i = 0; i < this._areaRows; i++) {
325
+ s += '\x1b[2K';
326
+ if (i < this._areaRows - 1)
327
+ s += '\x1b[1B';
271
328
  }
272
- this._drawInputBox();
273
- process.stdout.write('\x1b[?25h');
329
+ // Back to first row, col 0.
330
+ if (this._areaRows > 1)
331
+ s += `\x1b[${this._areaRows - 1}A`;
332
+ s += '\r';
333
+ return s;
274
334
  }
275
- // ── Activity box (shown while a subagent is running) ───────────────────────
276
- _drawActivityBox() {
277
- const cols = this.cols;
278
- const inner = cols - 4; // │ + space + content + space + │
279
- const topRow = this.scrollBottom + 1;
280
- const header = (this._activityHeader || '').slice(0, cols - 4);
281
- const dashFill = Math.max(0, cols - 3 - header.length);
282
- // Top border with header text
283
- process.stdout.write(`\x1b[${topRow};1H`);
284
- process.stdout.write(T('╭─') + chalk.bold.white(header) + T('─'.repeat(dashFill)) + T('╮'));
285
- // Content rows (last ACTIVITY_LINES lines, or blank)
286
- for (let i = 0; i < ACTIVITY_LINES; i++) {
287
- const row = topRow + 1 + i;
288
- const line = (this._activityLines[i] ?? '').slice(0, inner);
289
- const pad = inner - line.length;
290
- process.stdout.write(`\x1b[${row};1H`);
291
- process.stdout.write(T('│') + ' ' + chalk.rgb(180, 210, 210)(line) + ' '.repeat(pad) + ' ' + T('│'));
292
- }
293
- // Bottom border
294
- process.stdout.write(`\x1b[${topRow + ACTIVITY_LINES + 1};1H`);
295
- process.stdout.write(T('╰') + T('─'.repeat(cols - 2)) + T('╯'));
335
+ _clearArea() {
336
+ const s = this._buildClear();
337
+ if (s)
338
+ process.stdout.write(s);
339
+ this._areaRows = 0;
340
+ this._cursorRow = 0;
296
341
  }
297
- // ── Normal input box ───────────────────────────────────────────────────────
298
- _drawInputBox() {
342
+ _redraw() {
343
+ const clear = this._buildClear();
344
+ if (!this._inputActive && this._activityHeader === null) {
345
+ if (clear)
346
+ process.stdout.write(clear);
347
+ this._areaRows = 0;
348
+ this._cursorRow = 0;
349
+ return;
350
+ }
299
351
  const cols = this.cols;
300
- const cRows = this._contentRows();
301
- const cWidth = cols - PREFIX_COLS - 2;
302
- const topBorder = this.rows - cRows - 1;
303
- // ── Top border ───────────────────────────────────────────────
304
- process.stdout.write(`\x1b[${topBorder};1H`);
305
- process.stdout.write(T('╭') + T('─'.repeat(cols - 2)));
306
- process.stdout.write(`\x1b[${cols}G` + T('╮'));
307
- // ── Content rows ─────────────────────────────────────────────
308
- const wrapped = this._wrapText(this.buf, cWidth);
309
- const showStart = Math.max(0, wrapped.length - cRows);
310
- const visible = wrapped.slice(showStart);
311
- for (let i = 0; i < cRows; i++) {
312
- const row = topBorder + 1 + i;
313
- let line = visible[i] ?? '';
314
- if (i === 0 && showStart > 0)
315
- line = '… ' + line.slice(0, Math.max(0, cWidth - 2));
316
- else
317
- line = line.slice(0, cWidth);
318
- const pfx = (i === 0) ? PREFIX : PREFIX_CONT;
319
- process.stdout.write(`\x1b[${row};1H`);
320
- process.stdout.write(pfx + line);
321
- process.stdout.write(`\x1b[${cols}G` + T('│'));
352
+ let body = '';
353
+ let row = 0;
354
+ let cursorRow = 0;
355
+ let cursorCol = 0;
356
+ // Activity box
357
+ if (this._activityHeader !== null) {
358
+ const header = (this._activityHeader || '').slice(0, cols - 4);
359
+ const topPad = Math.max(0, cols - 3 - header.length);
360
+ body += DIM('┌') + chalk.bold.white(header) + DIM('─'.repeat(topPad) + '┐') + '\n';
361
+ row++;
362
+ for (let i = 0; i < 5; i++) {
363
+ const line = (this._activityLines[i] ?? '').slice(0, cols - 4);
364
+ const pad = Math.max(0, cols - 4 - line.length);
365
+ body += DIM('│') + ' ' + chalk.rgb(180, 210, 210)(line) + ' '.repeat(pad) + ' ' + DIM('│') + '\n';
366
+ row++;
367
+ }
368
+ body += DIM('└') + DIM('─'.repeat(cols - 2)) + DIM('┘') + '\n';
369
+ row++;
322
370
  }
323
- // ── Bottom border ────────────────────────────────────────────
324
- process.stdout.write(`\x1b[${this.rows};1H`);
325
- process.stdout.write(T('╰') + T('─'.repeat(cols - 2)));
326
- process.stdout.write(`\x1b[${cols}G` + T(''));
327
- // ── Position cursor ──────────────────────────────────────────
328
- const lastLine = visible[visible.length - 1] ?? '';
329
- const cursorRow = topBorder + cRows;
330
- const cursorCol = PREFIX_COLS + 1 + lastLine.length;
331
- process.stdout.write(`\x1b[${cursorRow};${cursorCol}H`);
332
- }
333
- _wrapText(text, maxWidth) {
334
- if (!text || maxWidth <= 0)
335
- return [''];
336
- const result = [];
337
- for (const seg of text.split('\n')) {
338
- if (seg.length === 0) {
339
- result.push('');
340
- continue;
371
+ let trailing = '';
372
+ let lastRow = row;
373
+ if (this._inputActive) {
374
+ const text = this._inputBuffer.join('');
375
+ const beforeCursor = this._inputBuffer.slice(0, this._cursorPos).join('');
376
+ const cursorLineIdx = beforeCursor.split('\n').length - 1;
377
+ const cursorColInLine = (beforeCursor.split('\n').pop() ?? '').length;
378
+ const logicalLines = text.length === 0 ? [''] : text.split('\n');
379
+ const displayLines = logicalLines.map((l, i) => (i === 0 ? PROMPT : INDENT) + (text === '' ? DIM(PLACEHOLDER) : l.slice(0, Math.max(0, cols - 3))));
380
+ for (let i = 0; i < displayLines.length; i++) {
381
+ const isLast = i === displayLines.length - 1;
382
+ body += displayLines[i] + (isLast ? '' : '\n');
383
+ if (i === cursorLineIdx) {
384
+ cursorRow = row;
385
+ cursorCol = PROMPT_W + Math.min(cursorColInLine, Math.max(0, cols - 3));
386
+ }
387
+ if (!isLast)
388
+ row++;
341
389
  }
342
- for (let i = 0; i < seg.length; i += maxWidth)
343
- result.push(seg.slice(i, i + maxWidth));
390
+ lastRow = row;
391
+ // After writing the body, terminal cursor is at end of last drawn line. Reposition to (cursorRow, cursorCol).
392
+ trailing = '\r';
393
+ const upBy = lastRow - cursorRow;
394
+ if (upBy > 0)
395
+ trailing += `\x1b[${upBy}A`;
396
+ if (cursorCol > 0)
397
+ trailing += `\x1b[${cursorCol}C`;
398
+ this._areaRows = lastRow + 1;
399
+ this._cursorRow = cursorRow;
400
+ }
401
+ else {
402
+ // Activity-only — leave cursor on the blank line just below the box.
403
+ this._areaRows = row;
404
+ this._cursorRow = row;
344
405
  }
345
- return result;
406
+ // Single atomic write: clear + body + reposition.
407
+ process.stdout.write(clear + body + trailing);
346
408
  }
347
409
  }