@bahulam/code 2.6.14 → 2.6.16

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,5 +1,7 @@
1
1
  // Present-progressive verbs — read more conversationally than "Read file":
2
2
  // "Reading auth.py — 47 lines" reads like the agent narrating, not a log.
3
+ import { isSensitiveConfigPath } from '../core/safety.mjs';
4
+
3
5
  const TOOL_LABELS = Object.freeze({
4
6
  shell: 'Running',
5
7
  read_file: 'Reading',
@@ -129,6 +131,7 @@ export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
129
131
  .join(', ') || 'Project files';
130
132
  case 'edit_file': {
131
133
  const filePath = shortPath(args.file_path || args.path, cwd);
134
+ if (isSensitiveConfigPath(filePath)) return `${filePath} · match [redacted]`;
132
135
  const search = String(args.search || '').trim();
133
136
  return search ? `${filePath} · match "${search.slice(0, 40)}${search.length > 40 ? '...' : ''}"` : filePath;
134
137
  }
@@ -229,8 +232,12 @@ export function shellCommandProfile(command, {
229
232
  || commandLineCount >= compactLines
230
233
  || commandByteCount > compactChars;
231
234
  const kind = script?.kind || (lineCount > 1 ? 'shell script' : 'shell command');
235
+ const preview = script?.body ? scriptBodyPreview(script.body) : '';
232
236
  const summary = compact
233
- ? `${kind} · ${lineCount} line${lineCount === 1 ? '' : 's'} · ${formatBytes(byteCount)}`
237
+ ? [
238
+ `${kind} · ${lineCount} line${lineCount === 1 ? '' : 's'} · ${formatBytes(byteCount)}`,
239
+ preview ? `preview: ${preview}` : '',
240
+ ].filter(Boolean).join(' · ')
234
241
  : body;
235
242
 
236
243
  return {
@@ -244,6 +251,7 @@ export function shellCommandProfile(command, {
244
251
  compact,
245
252
  kind,
246
253
  summary,
254
+ preview,
247
255
  script,
248
256
  detailHint: compact ? 'details: F2 or /last' : '',
249
257
  };
@@ -297,6 +305,17 @@ function interpreterKind(value) {
297
305
  return 'shell script';
298
306
  }
299
307
 
308
+ function scriptBodyPreview(body, maxChars = 20) {
309
+ const line = String(body || '')
310
+ .replace(/\r\n?/g, '\n')
311
+ .split('\n')
312
+ .map(value => value.trim())
313
+ .find(Boolean) || '';
314
+ const compact = line.replace(/\s+/g, ' ');
315
+ if (compact.length <= maxChars) return compact;
316
+ return `${compact.slice(0, Math.max(0, maxChars - 1))}…`;
317
+ }
318
+
300
319
  function byteLength(value) {
301
320
  try {
302
321
  return Buffer.byteLength(String(value || ''), 'utf8');
@@ -19,6 +19,7 @@ import { paint, width as visibleWidth } from './palette.mjs';
19
19
  import { icon } from './icons.mjs';
20
20
  import { shellCommandDisplay, shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
21
21
  import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
22
+ import { isSensitiveConfigPath } from '../core/safety.mjs';
22
23
 
23
24
  /**
24
25
  * Default option set per tier. Caller can override via `opts.options`.
@@ -100,7 +101,14 @@ export function renderApprovalDockPrompt({
100
101
  } = {}) {
101
102
  const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
102
103
  const opts = options || defaultOptions(tier, { tool, args });
103
- const subject = approvalDockSubject(tool, args, cols, showDetails);
104
+ // Multi-line shell/python scripts auto-expand: the user cannot approve
105
+ // what they cannot see. "shell script · 5 lines · 309 B" as the only
106
+ // subject made blind approval the default. After execution the script
107
+ // collapses back to the one-line tool card (details stay on /last).
108
+ const isScriptCommand = tool === 'shell'
109
+ && /\n/.test(String(args.command || args.cmd || ''));
110
+ const detailView = showDetails || isScriptCommand;
111
+ const subject = approvalDockSubject(tool, args, cols, detailView);
104
112
  const risks = riskTerms(tool, args, tier);
105
113
  const reason = compactReason(tool, args, why);
106
114
  const lines = [
@@ -111,14 +119,22 @@ export function renderApprovalDockPrompt({
111
119
  ...opts.map((option, index) => optionToken(option, index === selected, explicitAccent(tier))),
112
120
  ];
113
121
 
122
+ // Show the WHOLE script when it fits — a partial script (…lines cut at
123
+ // the top) makes blind approval the default. Cap at half the terminal
124
+ // so the dock never eats the whole screen. Non-detail approvals keep
125
+ // the tight 8-row cap.
126
+ const termRows = Math.max(12, Number(process.stderr.rows) || 24);
127
+ const detailCap = Math.max(12, Math.floor(termRows / 2));
128
+ const maxRows = detailView ? Math.min(detailCap, Math.max(12, lines.length + 1)) : 8;
129
+
114
130
  return {
115
131
  prefix: '? approve › ',
116
- value: truncateForDock(subject, showDetails ? 1200 : 220),
132
+ value: truncateForDock(subject, detailView ? 1200 : 220),
117
133
  context: `${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
118
134
  meta: '',
119
- tips: approvalFooter(tool, showDetails),
135
+ tips: approvalFooter(tool, detailView),
120
136
  lines,
121
- maxRows: showDetails ? 12 : 8,
137
+ maxRows,
122
138
  };
123
139
  }
124
140
 
@@ -158,6 +174,7 @@ export { TIERS };
158
174
  function tierTitle(tier) {
159
175
  switch (tier) {
160
176
  case TIERS.SENSITIVE_READ: return 'SENSITIVE';
177
+ case TIERS.PROTECTED_EDIT: return 'PROTECTED';
161
178
  case TIERS.SHELL_DANGEROUS: return 'DANGEROUS';
162
179
  case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
163
180
  case TIERS.SHELL_MEDIUM: return 'MEDIUM';
@@ -172,6 +189,7 @@ function tierTitle(tier) {
172
189
  function approvalTitle(tier) {
173
190
  switch (tier) {
174
191
  case TIERS.SENSITIVE_READ:
192
+ case TIERS.PROTECTED_EDIT:
175
193
  case TIERS.SHELL_DANGEROUS:
176
194
  case TIERS.DESTRUCTIVE:
177
195
  return tierTitle(tier);
@@ -286,6 +304,7 @@ function riskTerms(tool, args = {}, tier) {
286
304
  }
287
305
  }
288
306
  if (tier === TIERS.SENSITIVE_READ) terms.push('sensitive read');
307
+ if (tier === TIERS.PROTECTED_EDIT) terms.push('protected edit');
289
308
  if (tier === TIERS.DESTRUCTIVE) terms.push('destructive');
290
309
  return [...new Set(terms)].slice(0, 3);
291
310
  }
@@ -315,11 +334,18 @@ function subjectDetails(tool, args = {}, summary = '', available = 72) {
315
334
  }
316
335
  if (tool === 'write_file') {
317
336
  const file = args.file_path || args.path || summary || '';
337
+ if (isSensitiveConfigPath(file)) return [`${file} · content redacted`];
318
338
  const lineCount = typeof args.content === 'string' ? args.content.split('\n').length : null;
319
339
  return [`${file}${lineCount ? ` · ${lineCount} lines` : ''}`];
320
340
  }
321
341
  if (tool === 'edit_file') {
322
342
  const file = args.file_path || args.path || '';
343
+ if (isSensitiveConfigPath(file)) {
344
+ const details = [`${file || summary}`];
345
+ if (args.search || args.old_string) details.push('match: [redacted]');
346
+ if (args.replace || args.new_string) details.push('replace: [redacted]');
347
+ return details;
348
+ }
323
349
  const search = String(args.search || args.old_string || '').trim();
324
350
  const replacement = String(args.replace || args.new_string || '').trim();
325
351
  const details = [`${file || summary}`];
@@ -397,9 +423,11 @@ function approvalDockSubject(tool, args = {}, cols = 96, showDetails = false) {
397
423
  function approvalDockSubjectRows(subject) {
398
424
  const lines = String(subject || '').split('\n');
399
425
  const first = lines.shift() || '';
426
+ // 12 continuation rows matches approvalDockDetails' script cap — a
427
+ // 12-line script renders fully in the approval prompt.
400
428
  return [
401
429
  `${paint.text.dim('? approve ›')} ${paint.text.primary(truncate(first, 160))}`,
402
- ...lines.slice(0, 6).map(line => `${paint.text.dim(' ')}${paint.text.primary(truncate(line, 160))}`),
430
+ ...lines.slice(0, 12).map(line => `${paint.text.dim(' ')}${paint.text.primary(truncate(line, 160))}`),
403
431
  ];
404
432
  }
405
433
 
@@ -37,6 +37,7 @@
37
37
  import { paint, width as visibleWidth } from './palette.mjs';
38
38
  import { term, onResize } from './term.mjs';
39
39
  import { wrapToLines, tailWithEllipsis, cursorPositionInLines } from './text-layout.mjs';
40
+ import * as queue from './render-queue.mjs';
40
41
 
41
42
  const ESC = '\x1b[';
42
43
  const OUT = process.stderr;
@@ -52,6 +53,7 @@ const INPUT_RIGHT_PAD = 2;
52
53
  const META_INDENT = 4;
53
54
 
54
55
  const DEFAULT_MAX_INPUT_ROWS = 6;
56
+ const DEFAULT_OVERLAY_MAX_ROWS = 8;
55
57
  const MIN_INPUT_ROWS = 1;
56
58
  const MAX_INPUT_ROWS_CAP = 12;
57
59
 
@@ -74,10 +76,40 @@ let unsubResize = null;
74
76
  let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null, overlayLines: null };
75
77
  let resetting = false;
76
78
  let lastGeometry = null;
77
-
78
- function write(s) { try { OUT.write(s); } catch {} }
79
- function setScrollRegion(top, bottom) { write(`${ESC}${top};${bottom}r`); }
80
- function clearScrollRegion() { write(`${ESC}r`); }
79
+ let contentCursorRow = 1;
80
+ let contentCursorCol = 1;
81
+ let contentTrackingActive = false;
82
+ let suppressWriteTracking = 0;
83
+ let originalStdoutWrite = null;
84
+ let originalStderrWrite = null;
85
+
86
+ function write(s) {
87
+ // All dock frame bytes flow through the render queue's serialized raw
88
+ // channel when it is active — bypassing the content redirect so frame
89
+ // paints never land in the transcript. Legacy fallback writes straight
90
+ // to stderr with the old suppress-tracking guard.
91
+ if (queue.isActive()) {
92
+ queue.raw(s);
93
+ return;
94
+ }
95
+ try {
96
+ suppressWriteTracking++;
97
+ OUT.write(s);
98
+ } catch {
99
+ } finally {
100
+ suppressWriteTracking = Math.max(0, suppressWriteTracking - 1);
101
+ }
102
+ }
103
+ function setScrollRegion(top, bottom) {
104
+ // Keep the queue's notion of the content region in sync — its content
105
+ // cursor clamps to this bottom.
106
+ if (queue.isActive()) { queue.setRegion(top, bottom); return; }
107
+ write(`${ESC}${top};${bottom}r`);
108
+ }
109
+ function clearScrollRegion() {
110
+ if (queue.isActive()) { queue.clearRegion(); return; }
111
+ write(`${ESC}r`);
112
+ }
81
113
  function saveCursor() { write(`${ESC}s`); }
82
114
  function restoreCursor() { write(`${ESC}u`); }
83
115
  function moveTo(row, col) { write(`${ESC}${row};${col}H`); }
@@ -102,6 +134,69 @@ function contentBottomRow() {
102
134
  return Math.max(1, rows() - reservedRows);
103
135
  }
104
136
 
137
+ function clampContentCursor() {
138
+ const bottom = contentBottomRow();
139
+ contentCursorRow = Math.max(1, Math.min(bottom, contentCursorRow || 1));
140
+ contentCursorCol = Math.max(1, Math.min(cols(), contentCursorCol || 1));
141
+ }
142
+
143
+ function resetContentCursor(row = 1, col = 1) {
144
+ contentCursorRow = Math.max(1, Math.min(contentBottomRow(), Math.floor(row || 1)));
145
+ contentCursorCol = Math.max(1, Math.min(cols(), Math.floor(col || 1)));
146
+ }
147
+
148
+ function trackContentWrite(chunk) {
149
+ if (!mounted || !contentTrackingActive || suppressWriteTracking > 0) return;
150
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
151
+ if (!text) return;
152
+ const clean = text
153
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
154
+ .replace(/\x1b[()][A-Za-z0-9]/g, '');
155
+ const bottom = contentBottomRow();
156
+ const width = Math.max(1, drawableColumns());
157
+ for (const ch of clean) {
158
+ if (ch === '\r') {
159
+ contentCursorCol = 1;
160
+ continue;
161
+ }
162
+ if (ch === '\n') {
163
+ contentCursorRow = Math.min(bottom, contentCursorRow + 1);
164
+ contentCursorCol = 1;
165
+ continue;
166
+ }
167
+ contentCursorCol++;
168
+ if (contentCursorCol > width) {
169
+ contentCursorRow = Math.min(bottom, contentCursorRow + 1);
170
+ contentCursorCol = 1;
171
+ }
172
+ }
173
+ }
174
+
175
+ function patchOutputTracking() {
176
+ if (originalStdoutWrite || originalStderrWrite) return;
177
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
178
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
179
+ process.stdout.write = function trackedStdoutWrite(chunk, ...args) {
180
+ trackContentWrite(chunk);
181
+ return originalStdoutWrite(chunk, ...args);
182
+ };
183
+ process.stderr.write = function trackedStderrWrite(chunk, ...args) {
184
+ trackContentWrite(chunk);
185
+ return originalStderrWrite(chunk, ...args);
186
+ };
187
+ }
188
+
189
+ function unpatchOutputTracking() {
190
+ if (originalStdoutWrite) {
191
+ process.stdout.write = originalStdoutWrite;
192
+ originalStdoutWrite = null;
193
+ }
194
+ if (originalStderrWrite) {
195
+ process.stderr.write = originalStderrWrite;
196
+ originalStderrWrite = null;
197
+ }
198
+ }
199
+
105
200
  // Row map (top → bottom of the reserved region).
106
201
  function topRuleRow() { return contentBottomRow() + 1; }
107
202
  function spacerAboveRow() { return topRuleRow() + 1; }
@@ -125,6 +220,23 @@ function resolveMaxInputRows(requested) {
125
220
  return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
126
221
  }
127
222
 
223
+ function resolveOverlayRowCap(requested = DEFAULT_OVERLAY_MAX_ROWS) {
224
+ const n = Number.parseInt(String(requested), 10);
225
+ if (!Number.isFinite(n)) return DEFAULT_OVERLAY_MAX_ROWS;
226
+ // Overlays (approval scripts) may need more rows than the typing cap —
227
+ // the user cannot approve what they cannot see. Allow up to half the
228
+ // terminal so the transcript stays visible; typing input keeps the
229
+ // tight MAX_INPUT_ROWS_CAP via normalizeInputRows above.
230
+ const dynamicCap = Math.max(MAX_INPUT_ROWS_CAP, Math.floor((rows() || 24) / 2));
231
+ return Math.max(MIN_INPUT_ROWS, Math.min(dynamicCap, n));
232
+ }
233
+
234
+ function overlayRowsForWrapped(wrappedLength, requestedMaxRows = DEFAULT_OVERLAY_MAX_ROWS) {
235
+ const rowCap = resolveOverlayRowCap(requestedMaxRows);
236
+ const wanted = Math.max(MIN_INPUT_ROWS, Math.floor(Number(wrappedLength) || 0));
237
+ return Math.min(rowCap, wanted);
238
+ }
239
+
128
240
  // How many input rows does this (prefix + value) buffer need? Wrapped line
129
241
  // count clamped to [1, inputRowsMax]. Beyond the cap, tail-with-ellipsis
130
242
  // takes over inside drawInputLines so at most inputRowsMax rows render.
@@ -236,6 +348,10 @@ function parkCursorAtInput() {
236
348
  const prefix = lastFrame.prefix || '';
237
349
  const value = lastFrame.value || '';
238
350
  if (!prefix && !value) {
351
+ if (queue.isActive()) {
352
+ queue.park(inputRowStart(), INPUT_INDENT + 1);
353
+ return;
354
+ }
239
355
  moveTo(inputRowStart(), INPUT_INDENT + 1);
240
356
  return;
241
357
  }
@@ -250,6 +366,7 @@ function applyLayout({ clearPrevious = false } = {}) {
250
366
  const bottom = contentBottomRow();
251
367
  setScrollRegion(1, bottom);
252
368
  renderFrame(lastFrame);
369
+ clampContentCursor();
253
370
  // On (re)mount and resize, park at input if we have one; otherwise sit at
254
371
  // the bottom of the content region so any pending content writes flush
255
372
  // above the dock rather than into a stale mid-frame position.
@@ -385,7 +502,11 @@ export function isInputDockMounted() {
385
502
  return mounted;
386
503
  }
387
504
 
388
- export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
505
+ export function mountInputDock({
506
+ inputRowsMax: requestedMax,
507
+ initialContentRow = 1,
508
+ initialContentCol = 1,
509
+ } = {}) {
389
510
  const t = term();
390
511
  if (!t.isTTY || t.plain) return false;
391
512
  if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
@@ -395,10 +516,29 @@ export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
395
516
  inputRowsMax = resolveMaxInputRows(requestedMax);
396
517
  inputRows = MIN_INPUT_ROWS;
397
518
  reservedRows = FIXED_ROWS + inputRows;
519
+ resetContentCursor(initialContentRow, initialContentCol);
520
+ contentTrackingActive = false;
398
521
  mounted = true;
522
+ // Render queue becomes the sole writer + exact cursor tracker. The
523
+ // legacy simulate-by-parsing patch only engages if activation is
524
+ // refused (shouldn't happen — mount gating matches activate gating).
525
+ const queued = queue.activate({
526
+ initialRow: contentCursorRow,
527
+ initialCol: contentCursorCol,
528
+ bottom: contentBottomRow(),
529
+ });
530
+ if (!queued) patchOutputTracking();
399
531
  applyLayout();
400
532
 
401
- unsubResize = onResize(() => applyLayout({ clearPrevious: true }));
533
+ unsubResize = onResize(() => {
534
+ if (queue.isActive()) {
535
+ // Terminal reflow makes any tracked position fiction — hard
536
+ // re-anchor to the new content-region bottom before repainting.
537
+ queue.reanchor({ row: contentBottomRow(), col: 1, bottom: contentBottomRow() });
538
+ resetContentCursor(contentBottomRow(), 1);
539
+ }
540
+ applyLayout({ clearPrevious: true });
541
+ });
402
542
  process.once('exit', safeUnmount);
403
543
  process.once('SIGTERM', () => { safeUnmount(); process.exit(143); });
404
544
  return true;
@@ -415,6 +555,9 @@ export function unmountInputDock() {
415
555
  if (unsubResize) { unsubResize(); unsubResize = null; }
416
556
  } finally {
417
557
  mounted = false;
558
+ contentTrackingActive = false;
559
+ queue.deactivate();
560
+ unpatchOutputTracking();
418
561
  resetting = false;
419
562
  lastGeometry = null;
420
563
  }
@@ -424,21 +567,32 @@ function safeUnmount() { try { unmountInputDock(); } catch {} }
424
567
 
425
568
  export function moveToContent() {
426
569
  if (!mounted) return false;
427
- moveTo(contentBottomRow(), 1);
570
+ if (queue.isActive()) {
571
+ // Content self-positions through queue.content(); nothing to do.
572
+ return true;
573
+ }
574
+ clampContentCursor();
575
+ contentTrackingActive = true;
576
+ moveTo(contentCursorRow, contentCursorCol);
428
577
  return true;
429
578
  }
430
579
 
431
- // One row above the bottom of the scroll region. Writes here won't trigger
432
- // the scroll-on-LF that happens when writing '\n' at the region's bottom row
433
- // exactly what the persistent explore/spinner line needs to overwrite in place
434
- // without piling copies of itself into scrollback.
580
+ // The next transcript row. Spinner/status overlays live next to the latest
581
+ // content instead of near the scroll-region bottom; otherwise sparse agent
582
+ // events leave large blank holes between visible lines.
435
583
  export function pinnedStatusRow() {
436
584
  if (!mounted) return null;
437
- return Math.max(1, contentBottomRow() - 1);
585
+ clampContentCursor();
586
+ return Math.max(1, Math.min(contentBottomRow(), contentCursorRow));
438
587
  }
439
588
 
440
589
  export function drawPinnedStatus(line) {
441
590
  if (!mounted) return false;
591
+ if (queue.isActive()) {
592
+ // Coalesced, serialized, no VT100 save-slot involvement.
593
+ queue.status(String(line || ''));
594
+ return true;
595
+ }
442
596
  const row = pinnedStatusRow();
443
597
  if (row == null) return false;
444
598
  saveCursor();
@@ -451,6 +605,10 @@ export function drawPinnedStatus(line) {
451
605
 
452
606
  export function clearPinnedStatus() {
453
607
  if (!mounted) return false;
608
+ if (queue.isActive()) {
609
+ queue.clearStatus();
610
+ return true;
611
+ }
454
612
  const row = pinnedStatusRow();
455
613
  if (row == null) return false;
456
614
  saveCursor();
@@ -466,6 +624,7 @@ export function clearPinnedStatus() {
466
624
  // clearing their overlay.
467
625
  export function redrawDockFrame() {
468
626
  if (!mounted) return false;
627
+ contentTrackingActive = false;
469
628
  renderFrame(lastFrame);
470
629
  parkCursorAtInput();
471
630
  return true;
@@ -473,6 +632,7 @@ export function redrawDockFrame() {
473
632
 
474
633
  export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {}) {
475
634
  if (!mounted) return false;
635
+ contentTrackingActive = false;
476
636
  setInputRowsTo(MIN_INPUT_ROWS);
477
637
  clearInputRows();
478
638
  renderFrame({ context, tips, meta, prefix: '', value: '', overlayLines: null });
@@ -482,9 +642,11 @@ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {})
482
642
 
483
643
  export function clearInputPrompt() {
484
644
  if (!mounted) return false;
485
- clearInputRows();
645
+ contentTrackingActive = false;
486
646
  lastFrame.value = '';
487
647
  lastFrame.overlayLines = null;
648
+ setInputRowsTo(MIN_INPUT_ROWS);
649
+ clearInputRows();
488
650
  renderFrame(lastFrame);
489
651
  parkCursorAtInput();
490
652
  return true;
@@ -492,6 +654,7 @@ export function clearInputPrompt() {
492
654
 
493
655
  export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
494
656
  if (!mounted) return false;
657
+ contentTrackingActive = false;
495
658
  setInputRowsTo(computeInputRowsForBuffer(prefix, value));
496
659
  renderFrame({ context, tips, meta, prefix, value, cursor, overlayLines: null });
497
660
  const layout = layoutInput(prefix, value);
@@ -505,13 +668,14 @@ export function renderDockOverlay({
505
668
  lines = [],
506
669
  meta = '',
507
670
  tips = '',
508
- maxRows = 8,
671
+ maxRows = DEFAULT_OVERLAY_MAX_ROWS,
509
672
  } = {}) {
510
673
  if (!mounted) return false;
674
+ contentTrackingActive = false;
511
675
  const sourceLines = Array.isArray(lines) ? lines : String(lines || '').split('\n');
512
676
  const wrapped = layoutOverlayLines(sourceLines);
513
- const rowCap = Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, Math.max(inputRowsMax, maxRows)));
514
- setInputRowsTo(Math.min(rowCap, Math.max(MIN_INPUT_ROWS, wrapped.length)), { maxRows: rowCap });
677
+ const rowCap = resolveOverlayRowCap(maxRows);
678
+ setInputRowsTo(overlayRowsForWrapped(wrapped.length, rowCap), { maxRows: rowCap });
515
679
  const tail = tailWithEllipsis(wrapped, inputRows);
516
680
  renderFrame({
517
681
  context,
@@ -537,6 +701,7 @@ export function renderDockOverlay({
537
701
  */
538
702
  export function focusDockInput(prefix, value = '', cursorInValue = null) {
539
703
  if (!mounted) return false;
704
+ contentTrackingActive = false;
540
705
  const layout = layoutInput(prefix, value);
541
706
  const valueStr = String(value || '');
542
707
  const rawCursor = cursorInValue == null
@@ -551,6 +716,12 @@ export function focusDockInput(prefix, value = '', cursorInValue = null) {
551
716
  );
552
717
  const row = inputRowStart() + visibleRowIdx;
553
718
  const col = Math.min(cols(), INPUT_INDENT + 1 + Math.max(0, pos.col));
719
+ if (queue.isActive()) {
720
+ // Record the park position — every queue op re-parks here so readline
721
+ // echoes always land in the input row, even mid-stream.
722
+ queue.park(row, col);
723
+ return true;
724
+ }
554
725
  moveTo(row, col);
555
726
  return true;
556
727
  }
@@ -570,7 +741,12 @@ export function _internals() {
570
741
  bottomRuleLine,
571
742
  padLine,
572
743
  drawableColumns,
744
+ resetContentCursor,
745
+ contentCursor: () => ({ row: contentCursorRow, col: contentCursorCol, active: contentTrackingActive }),
746
+ overlayRowsForWrapped,
573
747
  FIXED_ROWS,
748
+ MAX_INPUT_ROWS_CAP,
749
+ DEFAULT_OVERLAY_MAX_ROWS,
574
750
  BRAND_LABEL,
575
751
  };
576
752
  }