erosolar-cli 1.7.155 → 1.7.156

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.
Files changed (90) hide show
  1. package/README.md +7 -17
  2. package/agents/erosolar-code.rules.json +1 -111
  3. package/agents/general.rules.json +0 -6
  4. package/dist/bin/erosolar.js +0 -22
  5. package/dist/bin/erosolar.js.map +1 -1
  6. package/dist/bin/selfTest.js +2 -190
  7. package/dist/bin/selfTest.js.map +1 -1
  8. package/dist/core/agent.d.ts +0 -12
  9. package/dist/core/agent.d.ts.map +1 -1
  10. package/dist/core/agent.js +12 -75
  11. package/dist/core/agent.js.map +1 -1
  12. package/dist/core/contextManager.d.ts +1 -2
  13. package/dist/core/contextManager.d.ts.map +1 -1
  14. package/dist/core/contextManager.js +2 -11
  15. package/dist/core/contextManager.js.map +1 -1
  16. package/dist/core/multilinePasteHandler.d.ts +4 -8
  17. package/dist/core/multilinePasteHandler.d.ts.map +1 -1
  18. package/dist/core/multilinePasteHandler.js +17 -67
  19. package/dist/core/multilinePasteHandler.js.map +1 -1
  20. package/dist/core/preferences.js +2 -8
  21. package/dist/core/preferences.js.map +1 -1
  22. package/dist/core/resultVerification.d.ts +1 -1
  23. package/dist/core/resultVerification.d.ts.map +1 -1
  24. package/dist/core/resultVerification.js +10 -11
  25. package/dist/core/resultVerification.js.map +1 -1
  26. package/dist/core/schemaValidator.d.ts.map +1 -1
  27. package/dist/core/schemaValidator.js +1 -36
  28. package/dist/core/schemaValidator.js.map +1 -1
  29. package/dist/core/toolRuntime.d.ts +0 -61
  30. package/dist/core/toolRuntime.d.ts.map +1 -1
  31. package/dist/core/toolRuntime.js +1 -303
  32. package/dist/core/toolRuntime.js.map +1 -1
  33. package/dist/core/unified/schema.d.ts.map +1 -1
  34. package/dist/core/unified/schema.js +1 -34
  35. package/dist/core/unified/schema.js.map +1 -1
  36. package/dist/headless/headlessApp.d.ts.map +1 -1
  37. package/dist/headless/headlessApp.js +0 -3
  38. package/dist/headless/headlessApp.js.map +1 -1
  39. package/dist/providers/anthropicProvider.d.ts.map +1 -1
  40. package/dist/providers/anthropicProvider.js +1 -4
  41. package/dist/providers/anthropicProvider.js.map +1 -1
  42. package/dist/shell/bracketedPasteManager.d.ts.map +1 -1
  43. package/dist/shell/bracketedPasteManager.js +3 -5
  44. package/dist/shell/bracketedPasteManager.js.map +1 -1
  45. package/dist/shell/composableMessage.js +2 -2
  46. package/dist/shell/composableMessage.js.map +1 -1
  47. package/dist/shell/interactiveShell.d.ts +1 -6
  48. package/dist/shell/interactiveShell.d.ts.map +1 -1
  49. package/dist/shell/interactiveShell.js +95 -421
  50. package/dist/shell/interactiveShell.js.map +1 -1
  51. package/dist/shell/shellApp.d.ts.map +1 -1
  52. package/dist/shell/shellApp.js +0 -7
  53. package/dist/shell/shellApp.js.map +1 -1
  54. package/dist/shell/systemPrompt.js +5 -5
  55. package/dist/shell/systemPrompt.js.map +1 -1
  56. package/dist/tools/bashTools.d.ts +0 -8
  57. package/dist/tools/bashTools.d.ts.map +1 -1
  58. package/dist/tools/bashTools.js +5 -80
  59. package/dist/tools/bashTools.js.map +1 -1
  60. package/dist/tools/diffUtils.d.ts.map +1 -1
  61. package/dist/tools/diffUtils.js +8 -12
  62. package/dist/tools/diffUtils.js.map +1 -1
  63. package/dist/tools/editTools.d.ts.map +1 -1
  64. package/dist/tools/editTools.js +36 -386
  65. package/dist/tools/editTools.js.map +1 -1
  66. package/dist/tools/fileTools.d.ts.map +1 -1
  67. package/dist/tools/fileTools.js +130 -25
  68. package/dist/tools/fileTools.js.map +1 -1
  69. package/dist/ui/ShellUIAdapter.d.ts +0 -5
  70. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  71. package/dist/ui/ShellUIAdapter.js +0 -21
  72. package/dist/ui/ShellUIAdapter.js.map +1 -1
  73. package/dist/ui/persistentPrompt.d.ts +10 -83
  74. package/dist/ui/persistentPrompt.d.ts.map +1 -1
  75. package/dist/ui/persistentPrompt.js +117 -522
  76. package/dist/ui/persistentPrompt.js.map +1 -1
  77. package/dist/ui/richText.js +4 -4
  78. package/dist/ui/richText.js.map +1 -1
  79. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  80. package/dist/ui/shortcutsHelp.js +13 -31
  81. package/dist/ui/shortcutsHelp.js.map +1 -1
  82. package/package.json +1 -4
  83. package/dist/ui/EnhancedPinnedChatBox.d.ts +0 -93
  84. package/dist/ui/EnhancedPinnedChatBox.d.ts.map +0 -1
  85. package/dist/ui/EnhancedPinnedChatBox.js +0 -309
  86. package/dist/ui/EnhancedPinnedChatBox.js.map +0 -1
  87. package/dist/ui/PinnedChatBoxEnhancer.d.ts +0 -88
  88. package/dist/ui/PinnedChatBoxEnhancer.d.ts.map +0 -1
  89. package/dist/ui/PinnedChatBoxEnhancer.js +0 -205
  90. package/dist/ui/PinnedChatBoxEnhancer.js.map +0 -1
@@ -215,8 +215,8 @@ const ANSI = {
215
215
  export class PinnedChatBox {
216
216
  writeStream;
217
217
  state;
218
+ reservedLines = 2; // Lines reserved at bottom for status bar (readline handles input)
218
219
  _lastRenderedHeight = 0;
219
- maxInputLines = 5; // Max lines to show before truncating
220
220
  inputBuffer = '';
221
221
  cursorPosition = 0;
222
222
  commandIdCounter = 0;
@@ -234,8 +234,6 @@ export class PinnedChatBox {
234
234
  maxStatusMessageLength = 200;
235
235
  // Scroll region management for persistent bottom input
236
236
  scrollRegionActive = false;
237
- // Track the row position we should return to after rendering status (bottom of scroll region)
238
- scrollRegionBottomRow = 0;
239
237
  // Input history for up/down navigation
240
238
  inputHistory = [];
241
239
  historyIndex = -1;
@@ -246,13 +244,6 @@ export class PinnedChatBox {
246
244
  pastedFullContent = '';
247
245
  /** Cleanup function for output interceptor registration */
248
246
  outputInterceptorCleanup;
249
- // Performance: Track if we're in the middle of a rapid input sequence
250
- inputSequenceTimer = null;
251
- // Visual state: Track if cursor should blink
252
- showCursor = true;
253
- cursorBlinkTimer = null;
254
- // Session tracking for elapsed time display
255
- sessionStartTime = Date.now();
256
247
  constructor(writeStream, _promptText = '> ', // Unused - readline handles input display
257
248
  options = {}) {
258
249
  this.writeStream = writeStream;
@@ -262,7 +253,6 @@ export class PinnedChatBox {
262
253
  this.maxQueueSize = options.maxQueueSize ?? 100;
263
254
  this.state = {
264
255
  isProcessing: false,
265
- isReasoningModel: false,
266
256
  queuedCommands: [],
267
257
  currentInput: '',
268
258
  contextUsage: 0,
@@ -276,11 +266,6 @@ export class PinnedChatBox {
276
266
  * Register with the display's output interceptor system.
277
267
  * With scroll regions, output flows in the scrollable area while
278
268
  * the input stays pinned at the bottom.
279
- *
280
- * NOTE: We no longer re-render the persistent input on every afterWrite
281
- * because this caused the input to appear multiple times in the scroll
282
- * output (the chat box was being "logged" to the terminal history).
283
- * Instead, we only render when state actually changes (via scheduleRender).
284
269
  */
285
270
  registerOutputInterceptor(display) {
286
271
  if (this.outputInterceptorCleanup) {
@@ -292,333 +277,106 @@ export class PinnedChatBox {
292
277
  // No need to clear - terminal handles it
293
278
  },
294
279
  afterWrite: () => {
295
- // NOTE: We intentionally do NOT re-render here anymore.
296
- // The scroll region keeps the input pinned at the bottom, and
297
- // re-rendering on every write was causing the chat box to appear
298
- // multiple times in the terminal output history.
299
- // The chat box is only re-rendered when input state changes
300
- // (typing, cursor blink, etc.) via scheduleRender().
280
+ // Debounced re-render of persistent input after streaming output
281
+ if (this.scrollRegionActive && this.state.isProcessing) {
282
+ if (this.pendingAfterWriteRender) {
283
+ clearTimeout(this.pendingAfterWriteRender);
284
+ }
285
+ // Debounce to ~30fps to avoid excessive rendering during fast streaming
286
+ this.pendingAfterWriteRender = setTimeout(() => {
287
+ this.pendingAfterWriteRender = null;
288
+ if (!this.isDisposed && this.scrollRegionActive) {
289
+ this.renderPersistentInput();
290
+ }
291
+ }, 33);
292
+ }
301
293
  },
302
294
  });
303
295
  }
304
296
  /**
305
297
  * Enable scroll region to keep bottom lines reserved for input.
306
298
  * Content scrolls in the upper region, input stays pinned at bottom.
307
- *
308
- * Optimized for stability:
309
- * - Validates terminal dimensions before setting region
310
- * - Uses atomic cursor save/restore operations
311
- * - Ensures clean state transition
312
299
  */
313
300
  enableScrollRegion() {
314
301
  if (!this.supportsRendering() || this.scrollRegionActive)
315
302
  return;
316
303
  const rows = this.writeStream.rows || 24;
317
- // Dynamically size the reserved area based on current input height
318
- const reservedHeight = this.getReservedLines();
319
- // Sanity check: need at least 6 rows for scroll region to work properly
320
- if (rows < reservedHeight + 4)
321
- return;
322
- // Track the bottom row of the scroll region for reference
323
- this.scrollRegionBottomRow = rows - reservedHeight;
324
- // Hide cursor during setup
325
- this.safeWrite(ANSI.HIDE_CURSOR);
326
- // First, scroll terminal content up to make room for the chat box
327
- // This ensures prompt and content are visible above the reserved area
328
- // Move cursor to where chat box will be, then scroll up
329
- this.safeWrite(`\u001b[${rows};1H`); // Move to bottom
330
- for (let i = 0; i < reservedHeight; i++) {
331
- this.safeWrite('\n'); // Scroll content up
332
- }
333
- // Set scroll region to exclude bottom lines (1 to scrollRegionBottomRow)
334
- this.safeWrite(ANSI.SET_SCROLL_REGION(1, this.scrollRegionBottomRow));
304
+ const reservedHeight = this.reservedLines;
305
+ // Set scroll region to exclude bottom lines
306
+ this.safeWrite(ANSI.SET_SCROLL_REGION(1, rows - reservedHeight));
307
+ // Move cursor to end of scroll region
308
+ this.safeWrite(`\u001b[${rows - reservedHeight};1H`);
335
309
  this.scrollRegionActive = true;
336
- // Force immediate render to ensure persistent chat box is visible
337
- this.forceRender();
338
- // Render the persistent input area at the bottom (outside scroll region)
339
- this.renderStatusBarOnly();
340
- // Position cursor at the bottom of the scroll region
341
- // New streaming content will appear here and scroll up naturally
342
- this.safeWrite(`\u001b[${this.scrollRegionBottomRow};1H`);
343
- // Keep terminal cursor hidden during streaming - we show a visual cursor in the chat box instead
344
- // This prevents the cursor from appearing in the stream output area
345
- // The visual cursor (inverted block) in renderPersistentInput() provides user feedback
346
- // Start cursor blink for visual feedback that input is active
347
- this.startCursorBlink();
348
- }
349
- /**
350
- * Render only the status bar at bottom without affecting cursor position.
351
- * Used during initial scroll region setup.
352
- */
353
- renderStatusBarOnly() {
354
- if (!this.supportsRendering())
355
- return;
356
- const rows = this.writeStream.rows || 24;
357
- const cols = Math.max(this.writeStream.columns || 80, 40);
358
- const borderWidth = cols - 2;
359
- // Calculate starting row (4 lines: top border + input + bottom border + status)
360
- let currentRow = rows - 3;
361
- // Build status line
362
- const queueCount = this.state.queuedCommands.length;
363
- let statusText = '';
364
- if (this.state.isProcessing) {
365
- const icon = this.state.isReasoningModel ? '🧠' : '⏳';
366
- const queuePart = queueCount > 0 ? ` +${queueCount}` : '';
367
- statusText = `${icon}${queuePart} [Enter: queue]`;
368
- }
369
- // Use absolute row positioning to prevent scroll history leakage
370
- // Top border with status on right
371
- const statusSuffix = statusText ? ` ${statusText}` : '';
372
- const topBorderWidth = Math.max(10, borderWidth - statusSuffix.length);
373
- this.safeWrite(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}`);
374
- this.safeWrite(theme.ui.border('─'.repeat(topBorderWidth)) + theme.ui.muted(statusSuffix));
375
- currentRow++;
376
- // Input line with prompt
377
- this.safeWrite(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}`);
378
- this.safeWrite(theme.ui.muted('> '));
379
- currentRow++;
380
- // Bottom border
381
- this.safeWrite(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}`);
382
- this.safeWrite(theme.ui.border('─'.repeat(borderWidth)));
383
- currentRow++;
384
- // Status line (context + time)
385
- const contextTimeLine = this.buildContextTimeStatus(cols);
386
- if (contextTimeLine) {
387
- this.safeWrite(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}`);
388
- this.safeWrite(contextTimeLine);
389
- }
310
+ // Render the persistent input area at the bottom
311
+ this.renderPersistentInput();
390
312
  }
391
313
  /**
392
314
  * Disable scroll region and restore normal terminal behavior.
393
- * Ensures clean state restoration.
394
315
  */
395
316
  disableScrollRegion() {
396
317
  if (!this.supportsRendering() || !this.scrollRegionActive)
397
318
  return;
398
- // Stop cursor blink
399
- this.stopCursorBlink();
400
- // Atomic operation: hide cursor, reset region, show cursor
401
- this.safeWrite(ANSI.HIDE_CURSOR);
402
319
  // Reset scroll region to full terminal
403
320
  this.safeWrite(ANSI.RESET_SCROLL_REGION);
404
321
  this.scrollRegionActive = false;
405
- this.safeWrite(ANSI.SHOW_CURSOR);
406
- }
407
- /**
408
- * Start cursor blink animation for visual feedback during typing
409
- */
410
- startCursorBlink() {
411
- if (this.cursorBlinkTimer)
412
- return;
413
- this.showCursor = true;
414
- this.cursorBlinkTimer = setInterval(() => {
415
- if (!this.scrollRegionActive || this.isDisposed) {
416
- this.stopCursorBlink();
417
- return;
418
- }
419
- this.showCursor = !this.showCursor;
420
- // Only re-render if we're actually showing the input line
421
- if (this.state.isProcessing && this.inputBuffer.length > 0) {
422
- this.renderPersistentInput();
423
- }
424
- }, 530); // Standard cursor blink rate
425
- }
426
- /**
427
- * Stop cursor blink animation
428
- */
429
- stopCursorBlink() {
430
- if (this.cursorBlinkTimer) {
431
- clearInterval(this.cursorBlinkTimer);
432
- this.cursorBlinkTimer = null;
433
- }
434
- this.showCursor = true;
435
- }
436
- /**
437
- * Calculate how many lines the current input needs for display
438
- */
439
- calculateInputLines(cols) {
440
- if (!this.inputBuffer)
441
- return 1;
442
- const contentWidth = cols - 4; // Account for "> " prompt and padding
443
- const lines = this.inputBuffer.split('\n');
444
- let totalLines = 0;
445
- for (const line of lines) {
446
- // Each line wraps based on terminal width
447
- totalLines += Math.max(1, Math.ceil(line.length / contentWidth));
448
- }
449
- return Math.min(totalLines, this.maxInputLines);
450
- }
451
- /**
452
- * Get the current reserved line count based on input content
453
- */
454
- getReservedLines() {
455
- const cols = this.writeStream.columns || 80;
456
- const inputLines = this.calculateInputLines(cols);
457
- return inputLines + 3; // top border + input lines + bottom border + status line
458
322
  }
459
323
  /**
460
324
  * Render the persistent input area at the bottom of the terminal.
461
325
  * Called when scroll region is active to keep input visible during streaming.
462
- *
463
- * Layout:
464
- * ─────────────────────────────────────────────────── ⏳ +2 [Enter: queue]
465
- * > input text here
466
- * continued on next line if needed
467
- * ───────────────────────────────────────────────────────────────────────
326
+ * Shows actual input line so users can see what they're typing.
468
327
  */
469
328
  renderPersistentInput() {
470
329
  if (!this.supportsRendering())
471
330
  return;
472
- // IMPORTANT: Only render when scroll region is active
473
- // This prevents the chat box from leaking into the scroll history
474
- if (!this.scrollRegionActive)
475
- return;
476
331
  const rows = this.writeStream.rows || 24;
477
332
  const cols = Math.max(this.writeStream.columns || 80, 40);
478
- const borderWidth = cols - 2;
479
- // Calculate dynamic height based on input content
480
- const inputLines = this.calculateInputLines(cols);
481
- const totalHeight = inputLines + 3; // top border + input + bottom border + status line
482
- // Calculate starting row for the chat box (reserved area at bottom)
483
- const startRow = rows - totalHeight + 1;
484
- let currentRow = startRow;
485
- // Build the entire output as a single string for atomic write
486
- const output = [];
487
- // CRITICAL FIX FOR UI LEAK:
488
- // 1. Save cursor position (so streaming can continue from where it was)
489
- // 2. Use absolute row positioning (no newlines that add to scroll buffer)
490
- // 3. Restore cursor position (streaming continues seamlessly)
491
- output.push('\u001b7'); // DECSC - save cursor position
492
- // Build status indicators for top border
333
+ // Save cursor position
334
+ this.safeWrite(ANSI.SAVE_CURSOR);
335
+ // Move to the reserved bottom area (outside scroll region)
336
+ this.safeWrite(ANSI.CURSOR_TO_BOTTOM(rows - 1));
337
+ // Build status line
493
338
  const queueCount = this.state.queuedCommands.length;
494
339
  let statusText = '';
495
340
  if (this.state.isProcessing) {
496
- const icon = this.state.isReasoningModel ? '🧠' : '';
497
- const queuePart = queueCount > 0 ? ` +${queueCount}` : '';
498
- statusText = `${icon}${queuePart} [Enter: queue]`;
341
+ const queuePart = queueCount > 0 ? ` (${queueCount} queued)` : '';
342
+ statusText = `⏳ Processing...${queuePart} [Esc: cancel · Enter: queue]`;
499
343
  }
500
- else if (queueCount > 0) {
501
- statusText = `📋 ${queueCount} queued`;
344
+ else if (this.state.statusMessage) {
345
+ statusText = this.state.statusMessage;
502
346
  }
503
- // Top border with status on right
504
- const statusSuffix = statusText ? ` ${statusText}` : '';
505
- const topBorderWidth = Math.max(10, borderWidth - statusSuffix.length);
506
- const topBorder = theme.ui.border('─'.repeat(topBorderWidth)) + theme.ui.muted(statusSuffix);
507
- output.push(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}${topBorder}`);
508
- currentRow++;
509
- // Input lines
347
+ else if (queueCount > 0) {
348
+ statusText = `${queueCount} follow-up${queueCount === 1 ? '' : 's'} queued`;
349
+ }
350
+ // Render separator and status on bottom lines
351
+ const separatorWidth = Math.min(cols - 2, 72);
352
+ const separator = theme.ui.border('─'.repeat(separatorWidth));
353
+ // Line rows-1: separator
354
+ this.safeWrite(ANSI.CLEAR_LINE);
355
+ this.safeWrite(separator);
356
+ // Line rows: input line with prompt or status
357
+ this.safeWrite(`\n${ANSI.CLEAR_LINE}`);
358
+ // Show actual input line during processing so user can see what they're typing
359
+ const promptPrefix = theme.ui.muted('> ');
510
360
  const currentInput = this.inputBuffer;
511
- const contentWidth = cols - 4;
512
- if (!currentInput) {
513
- // Empty input - show just the prompt
514
- output.push(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}${theme.ui.muted('> ')}`);
515
- // Visual cursor at prompt position
516
- if (this.showCursor) {
517
- output.push('\u001b[7m \u001b[27m'); // Inverted space for cursor
518
- }
519
- currentRow++;
520
- }
521
- else {
522
- // Wrap input text into display lines
523
- const displayLines = this.wrapInputForDisplay(currentInput, contentWidth, inputLines);
524
- for (let i = 0; i < displayLines.length; i++) {
525
- output.push(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}`);
526
- const lineContent = displayLines[i] ?? '';
527
- const isFirstLine = i === 0;
528
- const prompt = isFirstLine ? theme.ui.muted('> ') : ' ';
529
- output.push(prompt);
530
- // Check if cursor is on this line
531
- const cursorOnThisLine = this.isCursorOnDisplayLine(i, displayLines, contentWidth);
532
- if (cursorOnThisLine !== false && this.showCursor) {
533
- // Render with cursor
534
- const cursorCol = cursorOnThisLine;
535
- const before = lineContent.slice(0, cursorCol);
536
- const at = lineContent[cursorCol] || ' ';
537
- const after = lineContent.slice(cursorCol + 1);
538
- output.push(before);
539
- output.push(`\u001b[7m${at}\u001b[27m`); // Inverted for cursor
540
- output.push(after);
541
- }
542
- else {
543
- output.push(lineContent);
544
- }
545
- currentRow++;
546
- }
547
- // Show truncation indicator if content exceeds max lines
548
- if (this.inputBuffer.split('\n').length > this.maxInputLines ||
549
- this.inputBuffer.length > contentWidth * this.maxInputLines) {
550
- const truncatedLines = this.inputBuffer.split('\n').length - this.maxInputLines;
551
- if (truncatedLines > 0) {
552
- output.push(theme.ui.muted(` (+${truncatedLines} more lines)`));
553
- }
554
- }
555
- }
556
- // Bottom border (full width)
557
- output.push(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}${theme.ui.border('─'.repeat(borderWidth))}`);
558
- currentRow++;
559
- // Status line below: Context usage and session elapsed time
560
- const statusLine = this.buildContextTimeStatus(cols);
561
- if (statusLine) {
562
- output.push(`\u001b[${currentRow};1H${ANSI.CLEAR_LINE}${statusLine}`);
563
- }
564
- // Restore cursor position so streaming continues from where it left off
565
- // This is critical - without it, streaming would jump to a fixed position
566
- output.push('\u001b8'); // DECRC - restore cursor position
567
- // Ensure terminal cursor stays hidden in the stream area
568
- // We show a visual cursor (inverted block) in the chat box instead
569
- output.push(ANSI.HIDE_CURSOR);
570
- // Single atomic write for smoothness
571
- this.safeWrite(output.join(''));
572
- this._lastRenderedHeight = totalHeight;
573
- }
574
- /**
575
- * Wrap input text into display lines for rendering
576
- */
577
- wrapInputForDisplay(input, maxWidth, maxLines) {
578
- const lines = [];
579
- const inputLines = input.split('\n');
580
- for (const line of inputLines) {
581
- if (lines.length >= maxLines)
582
- break;
583
- if (line.length <= maxWidth) {
584
- lines.push(line);
585
- }
586
- else {
587
- // Wrap long lines
588
- for (let i = 0; i < line.length && lines.length < maxLines; i += maxWidth) {
589
- lines.push(line.slice(i, i + maxWidth));
590
- }
361
+ const maxInputDisplay = cols - 4; // Reserve space for prompt and cursor
362
+ // Truncate input if too long, showing end of input
363
+ let displayInput = currentInput;
364
+ if (displayInput.length > maxInputDisplay) {
365
+ displayInput = '…' + displayInput.slice(-(maxInputDisplay - 1));
366
+ }
367
+ // Render prompt + input
368
+ this.safeWrite(promptPrefix);
369
+ this.safeWrite(displayInput);
370
+ // Show status hint on the right if there's room
371
+ if (statusText && currentInput.length < maxInputDisplay - statusText.length - 5) {
372
+ const padding = cols - 3 - currentInput.length - statusText.length - 3;
373
+ if (padding > 3) {
374
+ this.safeWrite(' '.repeat(padding));
375
+ this.safeWrite(theme.ui.muted(statusText.slice(0, cols - currentInput.length - 6)));
591
376
  }
592
377
  }
593
- return lines;
594
- }
595
- /**
596
- * Determine if cursor should appear on a given display line
597
- * Returns the column position if yes, false if no
598
- */
599
- isCursorOnDisplayLine(lineIndex, displayLines, _maxWidth) {
600
- // Calculate which display line the cursor is on
601
- let charCount = 0;
602
- for (let i = 0; i < displayLines.length; i++) {
603
- const line = displayLines[i] ?? '';
604
- const lineLength = line.length;
605
- const lineEnd = charCount + lineLength;
606
- // Account for newlines in original input
607
- if (i > 0)
608
- charCount++; // +1 for the newline character
609
- if (this.cursorPosition >= charCount && this.cursorPosition <= lineEnd) {
610
- if (i === lineIndex) {
611
- return this.cursorPosition - charCount;
612
- }
613
- }
614
- charCount = lineEnd;
615
- }
616
- // Cursor at end of last line
617
- if (lineIndex === displayLines.length - 1 && this.cursorPosition >= charCount) {
618
- const lastLine = displayLines[lineIndex] ?? '';
619
- return lastLine.length;
620
- }
621
- return false;
378
+ // Restore cursor to scroll region
379
+ this.safeWrite(ANSI.RESTORE_CURSOR);
622
380
  }
623
381
  /**
624
382
  * Update the persistent input during streaming.
@@ -639,53 +397,6 @@ export class PinnedChatBox {
639
397
  typeof this.writeStream.write === 'function' &&
640
398
  this.writeStream.writable !== false);
641
399
  }
642
- /**
643
- * Build status line showing context usage and session elapsed time
644
- */
645
- buildContextTimeStatus(cols) {
646
- const parts = [];
647
- // Context usage with visual bar
648
- const contextPct = this.state.contextUsage;
649
- if (contextPct > 0) {
650
- const barWidth = 10;
651
- const filled = Math.round((contextPct / 100) * barWidth);
652
- const empty = barWidth - filled;
653
- const bar = `${'█'.repeat(filled)}${'░'.repeat(empty)}`;
654
- // Color based on usage level
655
- let contextColor = theme.success;
656
- if (contextPct >= 80)
657
- contextColor = theme.error;
658
- else if (contextPct >= 60)
659
- contextColor = theme.warning;
660
- parts.push(`${theme.ui.muted('Context')} ${contextColor(bar)} ${contextColor(`${contextPct}%`)}`);
661
- }
662
- // Session elapsed time
663
- const elapsedMs = Date.now() - this.sessionStartTime;
664
- const elapsedSecs = Math.floor(elapsedMs / 1000);
665
- const elapsedStr = this.formatElapsedTime(elapsedSecs);
666
- parts.push(`${theme.ui.muted('Session')} ${theme.info(elapsedStr)}`);
667
- if (parts.length === 0)
668
- return '';
669
- // Center the status line
670
- const statusText = parts.join(theme.ui.muted(' │ '));
671
- const visibleLen = this.stripAnsi(statusText).length;
672
- const padding = Math.max(0, Math.floor((cols - visibleLen) / 2));
673
- return ' '.repeat(padding) + statusText;
674
- }
675
- /**
676
- * Format elapsed seconds as human-readable time
677
- */
678
- formatElapsedTime(seconds) {
679
- if (seconds < 60)
680
- return `${seconds}s`;
681
- const mins = Math.floor(seconds / 60);
682
- const secs = seconds % 60;
683
- if (mins < 60)
684
- return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
685
- const hours = Math.floor(mins / 60);
686
- const remainMins = mins % 60;
687
- return `${hours}h ${remainMins}m`;
688
- }
689
400
  /**
690
401
  * Strip ANSI escape codes
691
402
  */
@@ -751,15 +462,6 @@ export class PinnedChatBox {
751
462
  this.scheduleRender();
752
463
  }
753
464
  }
754
- /**
755
- * Set whether current model is a reasoning model (o1, deepseek-reasoner).
756
- * Reasoning models don't stream text - they think internally then respond.
757
- */
758
- setReasoningModel(isReasoning) {
759
- if (this.isDisposed)
760
- return;
761
- this.state.isReasoningModel = isReasoning;
762
- }
763
465
  /**
764
466
  * Set processing state
765
467
  * Enables scroll region during processing to keep input visible while streaming
@@ -881,19 +583,14 @@ export class PinnedChatBox {
881
583
  /**
882
584
  * Handle character input with validation
883
585
  * Detects multiline paste and stores full content while showing summary
884
- *
885
- * Optimized for fast typing:
886
- * - Batches rapid keystrokes
887
- * - Immediate visual feedback
888
- * - Efficient state updates
889
586
  */
890
587
  handleInput(char) {
891
588
  if (!this.isEnabled || this.isDisposed)
892
589
  return;
893
590
  if (typeof char !== 'string')
894
591
  return;
895
- // Detect multiline paste (content with newlines or very long single paste)
896
- if (char.includes('\n') || char.length > 100) {
592
+ // Detect multiline paste (content with newlines)
593
+ if (char.includes('\n')) {
897
594
  this.handleMultilinePaste(char);
898
595
  return;
899
596
  }
@@ -914,122 +611,63 @@ export class PinnedChatBox {
914
611
  this.inputBuffer.slice(this.cursorPosition);
915
612
  this.cursorPosition = Math.min(this.cursorPosition + chunk.length, this.inputBuffer.length);
916
613
  this.state.currentInput = this.inputBuffer;
917
- // Reset cursor blink on input for immediate visual feedback
918
- this.showCursor = true;
919
- // Fast render for single keystrokes during streaming
920
- if (this.scrollRegionActive && this.state.isProcessing) {
921
- // During streaming: immediate render for responsiveness
922
- this.renderPersistentInput();
923
- }
924
- else {
925
- // Normal mode: use standard scheduling
926
- this.scheduleRender();
927
- }
614
+ this.scheduleRender();
928
615
  }
929
616
  /**
930
- * Handle multiline paste - show full content for small/medium pastes, summarized block for large ones
931
- * Claude Code style: show all lines unless very long, then show first/last lines with ellipsis
932
- * CRITICAL: Must work correctly in all circumstances:
933
- * - During streaming (scroll region active)
934
- * - During idle (normal readline mode)
935
- * - Small/medium pastes (≤15 lines) - show full content
936
- * - Large pastes (>15 lines) - show block with first/last lines
617
+ * Handle multiline paste - store full content but display summary
937
618
  */
938
619
  handleMultilinePaste(content) {
939
620
  const lines = content.split('\n');
940
621
  const lineCount = lines.length;
941
- // Show full content for small/medium pastes (up to 15 lines)
942
- // For larger pastes, show a summarized block with first/last lines
943
- const isSmallPaste = lineCount <= 15;
944
- if (isSmallPaste) {
945
- // Show full content for small/medium pastes
946
- this.pastedFullContent = '';
947
- this.isPastedBlock = false;
948
- this.inputBuffer = content;
949
- this.cursorPosition = content.length;
950
- this.state.currentInput = content;
951
- }
952
- else {
953
- // Show summarized block for large pastes
954
- this.pastedFullContent = content;
955
- this.isPastedBlock = true;
956
- const summary = this.generatePasteSummary(content, lineCount);
957
- this.inputBuffer = summary;
958
- this.cursorPosition = summary.length;
959
- this.state.currentInput = summary;
960
- }
961
- // Use consistent rendering approach as handleInput:
962
- // During streaming with scroll region active, render immediately
963
- // Otherwise use scheduled render
964
- if (this.scrollRegionActive && this.state.isProcessing) {
965
- this.renderPersistentInput();
966
- }
967
- else {
968
- this.scheduleRender();
969
- }
622
+ this.pastedFullContent = content;
623
+ this.isPastedBlock = true;
624
+ // Generate a summary for display
625
+ const summary = this.generatePasteSummary(content, lineCount);
626
+ this.inputBuffer = summary;
627
+ this.cursorPosition = summary.length;
628
+ this.state.currentInput = summary;
629
+ this.scheduleRender();
970
630
  }
971
631
  /**
972
- * Generate a block summary of pasted content for display (Claude Code style)
973
- * Shows first few lines, ellipsis with count, and last line
632
+ * Generate a short summary of pasted content for display
974
633
  */
975
634
  generatePasteSummary(content, lineCount) {
976
635
  const lines = content.split('\n');
636
+ const firstLine = lines[0]?.trim() || '';
977
637
  const charCount = content.length;
978
- // Detect content type for header
979
- const firstNonEmpty = lines.find(l => l.trim()) || '';
980
- let typeLabel = 'text';
981
- const codePatterns = [
982
- [/^(import|export|from\s+['"]|require\s*\()/, 'code'],
983
- [/^(function|const|let|var|class|interface|type|enum)\s+\w/, 'code'],
984
- [/^(def |class |import |from |async def |@\w+)/, 'python'],
985
- [/^(package |func |type |import |var |const )\w/, 'go'],
986
- [/^(fn |use |mod |struct |impl |pub )/, 'rust'],
987
- [/^\s*[{[]/, 'json'],
988
- [/^<[!?]?[a-zA-Z]/, 'markup'],
989
- [/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\s/i, 'sql'],
990
- [/^#!\//, 'script'],
991
- ];
992
- for (const [pattern, label] of codePatterns) {
993
- if (pattern.test(firstNonEmpty)) {
994
- typeLabel = label;
995
- break;
996
- }
997
- }
998
- // Format size info
999
- const sizeStr = charCount >= 1000
1000
- ? `${(charCount / 1000).toFixed(1)}k chars`
1001
- : `${charCount} chars`;
1002
- // Build block display: show first 4 lines, ellipsis, last 2 lines
1003
- const showFirstLines = 4;
1004
- const showLastLines = 2;
1005
- const hiddenLines = lineCount - showFirstLines - showLastLines;
1006
- const displayLines = [];
1007
- // Header line
1008
- displayLines.push(`📋 Pasted ${lineCount} lines (${typeLabel}, ${sizeStr}):`);
1009
- // First N lines (truncate long lines)
1010
- const maxLineWidth = 60;
1011
- for (let i = 0; i < Math.min(showFirstLines, lines.length); i++) {
1012
- let line = lines[i] || '';
1013
- if (line.length > maxLineWidth) {
1014
- line = line.slice(0, maxLineWidth - 1) + '…';
1015
- }
1016
- displayLines.push(` ${line}`);
1017
- }
1018
- // Ellipsis with hidden count (if there are hidden lines)
1019
- if (hiddenLines > 0) {
1020
- displayLines.push(` ... (${hiddenLines} more lines)`);
1021
- // Last N lines
1022
- for (let i = lineCount - showLastLines; i < lineCount; i++) {
1023
- if (i >= showFirstLines) { // Avoid duplicates
1024
- let line = lines[i] || '';
1025
- if (line.length > maxLineWidth) {
1026
- line = line.slice(0, maxLineWidth - 1) + '…';
1027
- }
1028
- displayLines.push(` ${line}`);
1029
- }
1030
- }
1031
- }
1032
- return displayLines.join('\n');
638
+ // Detect content type and get appropriate icon/label
639
+ let typeLabel = 'Pasted';
640
+ let typeIcon = '📋';
641
+ if (firstLine.match(/^(import|export|const|let|var|function|class|def |async |from |interface |type )/)) {
642
+ typeIcon = '📝';
643
+ typeLabel = 'Code';
644
+ }
645
+ else if (firstLine.match(/^[{[\]]/)) {
646
+ typeIcon = '📊';
647
+ typeLabel = 'JSON';
648
+ }
649
+ else if (firstLine.match(/^<[!?]?[a-zA-Z]/)) {
650
+ typeIcon = '📄';
651
+ typeLabel = 'XML/HTML';
652
+ }
653
+ else if (firstLine.match(/^#|^\/\/|^\/\*|^\*|^--/)) {
654
+ typeIcon = '💬';
655
+ typeLabel = 'Text';
656
+ }
657
+ else if (firstLine.match(/^```|^~~~|^\s{4}/)) {
658
+ typeIcon = '📖';
659
+ typeLabel = 'Markdown';
660
+ }
661
+ // Create a compact preview
662
+ const maxPreviewLen = 25;
663
+ const preview = firstLine.length > maxPreviewLen
664
+ ? firstLine.slice(0, maxPreviewLen - 1) + '…'
665
+ : firstLine || '(empty)';
666
+ // Format size info compactly
667
+ const sizeInfo = charCount > 1000
668
+ ? `${(charCount / 1000).toFixed(1)}k`
669
+ : `${charCount}`;
670
+ return `${typeIcon} [${typeLabel}: ${lineCount}L/${sizeInfo}c] ${preview}`;
1033
671
  }
1034
672
  /**
1035
673
  * Check if current input is a pasted block
@@ -1060,7 +698,7 @@ export class PinnedChatBox {
1060
698
  this.state.currentInput = '';
1061
699
  }
1062
700
  /**
1063
- * Handle backspace - deletes paste chips as a single unit
701
+ * Handle backspace
1064
702
  */
1065
703
  handleBackspace() {
1066
704
  if (!this.isEnabled || this.isDisposed)
@@ -1068,28 +706,6 @@ export class PinnedChatBox {
1068
706
  this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
1069
707
  if (this.cursorPosition === 0)
1070
708
  return;
1071
- // Check if we're at the end of a paste chip (character before cursor is ']')
1072
- const charBefore = this.inputBuffer[this.cursorPosition - 1];
1073
- if (charBefore === ']') {
1074
- // Find the matching '[' to get the full chip
1075
- const beforeCursor = this.inputBuffer.slice(0, this.cursorPosition);
1076
- const chipStart = beforeCursor.lastIndexOf('[');
1077
- if (chipStart !== -1) {
1078
- // Verify this looks like a paste chip (contains emoji)
1079
- const potentialChip = beforeCursor.slice(chipStart);
1080
- if (potentialChip.match(/^\[📋|^\[📝|^\[📊|^\[📄/)) {
1081
- // Delete the entire chip
1082
- this.inputBuffer =
1083
- this.inputBuffer.slice(0, chipStart) +
1084
- this.inputBuffer.slice(this.cursorPosition);
1085
- this.cursorPosition = chipStart;
1086
- this.state.currentInput = this.inputBuffer;
1087
- this.scheduleRender();
1088
- return;
1089
- }
1090
- }
1091
- }
1092
- // Normal single character delete
1093
709
  this.inputBuffer =
1094
710
  this.inputBuffer.slice(0, this.cursorPosition - 1) +
1095
711
  this.inputBuffer.slice(this.cursorPosition);
@@ -1098,7 +714,7 @@ export class PinnedChatBox {
1098
714
  this.scheduleRender();
1099
715
  }
1100
716
  /**
1101
- * Handle delete key - deletes paste chips as a single unit
717
+ * Handle delete key
1102
718
  */
1103
719
  handleDelete() {
1104
720
  if (!this.isEnabled || this.isDisposed)
@@ -1106,23 +722,6 @@ export class PinnedChatBox {
1106
722
  this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
1107
723
  if (this.cursorPosition >= this.inputBuffer.length)
1108
724
  return;
1109
- // Check if we're at the start of a paste chip (character at cursor is '[')
1110
- const charAtCursor = this.inputBuffer[this.cursorPosition];
1111
- if (charAtCursor === '[') {
1112
- // Check if this looks like a paste chip
1113
- const afterCursor = this.inputBuffer.slice(this.cursorPosition);
1114
- const chipMatch = afterCursor.match(/^\[(?:📋|📝|📊|📄)[^\]]*\]/);
1115
- if (chipMatch) {
1116
- // Delete the entire chip
1117
- this.inputBuffer =
1118
- this.inputBuffer.slice(0, this.cursorPosition) +
1119
- this.inputBuffer.slice(this.cursorPosition + chipMatch[0].length);
1120
- this.state.currentInput = this.inputBuffer;
1121
- this.scheduleRender();
1122
- return;
1123
- }
1124
- }
1125
- // Normal single character delete
1126
725
  this.inputBuffer =
1127
726
  this.inputBuffer.slice(0, this.cursorPosition) +
1128
727
  this.inputBuffer.slice(this.cursorPosition + 1);
@@ -1524,6 +1123,12 @@ export class PinnedChatBox {
1524
1123
  handleResize() {
1525
1124
  this.scheduleRender();
1526
1125
  }
1126
+ /**
1127
+ * Get number of reserved lines at bottom
1128
+ */
1129
+ getReservedLines() {
1130
+ return this.reservedLines;
1131
+ }
1527
1132
  /**
1528
1133
  * Get last rendered height (for layout calculations)
1529
1134
  */
@@ -1536,25 +1141,16 @@ export class PinnedChatBox {
1536
1141
  dispose() {
1537
1142
  if (this.isDisposed)
1538
1143
  return;
1539
- // Clean up all timers
1540
- this.stopCursorBlink();
1144
+ // Clean up pending render timeout
1541
1145
  if (this.pendingAfterWriteRender) {
1542
1146
  clearTimeout(this.pendingAfterWriteRender);
1543
1147
  this.pendingAfterWriteRender = null;
1544
1148
  }
1545
- if (this.inputSequenceTimer) {
1546
- clearTimeout(this.inputSequenceTimer);
1547
- this.inputSequenceTimer = null;
1548
- }
1549
1149
  // Clean up output interceptor registration
1550
1150
  if (this.outputInterceptorCleanup) {
1551
1151
  this.outputInterceptorCleanup();
1552
1152
  this.outputInterceptorCleanup = undefined;
1553
1153
  }
1554
- // Disable scroll region before clearing
1555
- if (this.scrollRegionActive) {
1556
- this.disableScrollRegion();
1557
- }
1558
1154
  try {
1559
1155
  this.clear();
1560
1156
  }
@@ -1594,7 +1190,6 @@ export class PinnedChatBox {
1594
1190
  this.cursorPosition = 0;
1595
1191
  this.state = {
1596
1192
  isProcessing: false,
1597
- isReasoningModel: false,
1598
1193
  queuedCommands: [],
1599
1194
  currentInput: '',
1600
1195
  contextUsage: 0,