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.
- package/README.md +7 -17
- package/agents/erosolar-code.rules.json +1 -111
- package/agents/general.rules.json +0 -6
- package/dist/bin/erosolar.js +0 -22
- package/dist/bin/erosolar.js.map +1 -1
- package/dist/bin/selfTest.js +2 -190
- package/dist/bin/selfTest.js.map +1 -1
- package/dist/core/agent.d.ts +0 -12
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +12 -75
- package/dist/core/agent.js.map +1 -1
- package/dist/core/contextManager.d.ts +1 -2
- package/dist/core/contextManager.d.ts.map +1 -1
- package/dist/core/contextManager.js +2 -11
- package/dist/core/contextManager.js.map +1 -1
- package/dist/core/multilinePasteHandler.d.ts +4 -8
- package/dist/core/multilinePasteHandler.d.ts.map +1 -1
- package/dist/core/multilinePasteHandler.js +17 -67
- package/dist/core/multilinePasteHandler.js.map +1 -1
- package/dist/core/preferences.js +2 -8
- package/dist/core/preferences.js.map +1 -1
- package/dist/core/resultVerification.d.ts +1 -1
- package/dist/core/resultVerification.d.ts.map +1 -1
- package/dist/core/resultVerification.js +10 -11
- package/dist/core/resultVerification.js.map +1 -1
- package/dist/core/schemaValidator.d.ts.map +1 -1
- package/dist/core/schemaValidator.js +1 -36
- package/dist/core/schemaValidator.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +0 -61
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +1 -303
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/unified/schema.d.ts.map +1 -1
- package/dist/core/unified/schema.js +1 -34
- package/dist/core/unified/schema.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +0 -3
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/providers/anthropicProvider.d.ts.map +1 -1
- package/dist/providers/anthropicProvider.js +1 -4
- package/dist/providers/anthropicProvider.js.map +1 -1
- package/dist/shell/bracketedPasteManager.d.ts.map +1 -1
- package/dist/shell/bracketedPasteManager.js +3 -5
- package/dist/shell/bracketedPasteManager.js.map +1 -1
- package/dist/shell/composableMessage.js +2 -2
- package/dist/shell/composableMessage.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +1 -6
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +95 -421
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +0 -7
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.js +5 -5
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/tools/bashTools.d.ts +0 -8
- package/dist/tools/bashTools.d.ts.map +1 -1
- package/dist/tools/bashTools.js +5 -80
- package/dist/tools/bashTools.js.map +1 -1
- package/dist/tools/diffUtils.d.ts.map +1 -1
- package/dist/tools/diffUtils.js +8 -12
- package/dist/tools/diffUtils.js.map +1 -1
- package/dist/tools/editTools.d.ts.map +1 -1
- package/dist/tools/editTools.js +36 -386
- package/dist/tools/editTools.js.map +1 -1
- package/dist/tools/fileTools.d.ts.map +1 -1
- package/dist/tools/fileTools.js +130 -25
- package/dist/tools/fileTools.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +0 -5
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +0 -21
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +10 -83
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +117 -522
- package/dist/ui/persistentPrompt.js.map +1 -1
- package/dist/ui/richText.js +4 -4
- package/dist/ui/richText.js.map +1 -1
- package/dist/ui/shortcutsHelp.d.ts.map +1 -1
- package/dist/ui/shortcutsHelp.js +13 -31
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/package.json +1 -4
- package/dist/ui/EnhancedPinnedChatBox.d.ts +0 -93
- package/dist/ui/EnhancedPinnedChatBox.d.ts.map +0 -1
- package/dist/ui/EnhancedPinnedChatBox.js +0 -309
- package/dist/ui/EnhancedPinnedChatBox.js.map +0 -1
- package/dist/ui/PinnedChatBoxEnhancer.d.ts +0 -88
- package/dist/ui/PinnedChatBoxEnhancer.d.ts.map +0 -1
- package/dist/ui/PinnedChatBoxEnhancer.js +0 -205
- 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
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
//
|
|
337
|
-
this.
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
//
|
|
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
|
|
497
|
-
|
|
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 (
|
|
501
|
-
statusText =
|
|
344
|
+
else if (this.state.statusMessage) {
|
|
345
|
+
statusText = this.state.statusMessage;
|
|
502
346
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
//
|
|
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
|
|
512
|
-
if
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
|
896
|
-
if (char.includes('\n')
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
|
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
|
|
979
|
-
|
|
980
|
-
let
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|