erosolar-cli 1.7.225 → 1.7.227
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 +22 -148
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +9 -18
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +0 -6
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +4 -10
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +2 -2
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +12 -12
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +18 -46
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +106 -264
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +6 -8
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +6 -12
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +6 -8
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +13 -26
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
- package/dist/core/aiFlowOptimizer.d.ts +0 -26
- package/dist/core/aiFlowOptimizer.d.ts.map +0 -1
- package/dist/core/aiFlowOptimizer.js +0 -31
- package/dist/core/aiFlowOptimizer.js.map +0 -1
- package/dist/core/aiOptimizationEngine.d.ts +0 -158
- package/dist/core/aiOptimizationEngine.d.ts.map +0 -1
- package/dist/core/aiOptimizationEngine.js +0 -428
- package/dist/core/aiOptimizationEngine.js.map +0 -1
- package/dist/core/aiOptimizationIntegration.d.ts +0 -93
- package/dist/core/aiOptimizationIntegration.d.ts.map +0 -1
- package/dist/core/aiOptimizationIntegration.js +0 -250
- package/dist/core/aiOptimizationIntegration.js.map +0 -1
- package/dist/core/enhancedErrorRecovery.d.ts +0 -100
- package/dist/core/enhancedErrorRecovery.d.ts.map +0 -1
- package/dist/core/enhancedErrorRecovery.js +0 -345
- package/dist/core/enhancedErrorRecovery.js.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.d.ts +0 -145
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +0 -1
- package/dist/shell/claudeCodeStreamHandler.js +0 -322
- package/dist/shell/claudeCodeStreamHandler.js.map +0 -1
- package/dist/shell/inputQueueManager.d.ts +0 -144
- package/dist/shell/inputQueueManager.d.ts.map +0 -1
- package/dist/shell/inputQueueManager.js +0 -290
- package/dist/shell/inputQueueManager.js.map +0 -1
- package/dist/shell/streamingOutputManager.d.ts +0 -115
- package/dist/shell/streamingOutputManager.d.ts.map +0 -1
- package/dist/shell/streamingOutputManager.js +0 -225
- package/dist/shell/streamingOutputManager.js.map +0 -1
- package/dist/ui/persistentPrompt.d.ts +0 -50
- package/dist/ui/persistentPrompt.d.ts.map +0 -1
- package/dist/ui/persistentPrompt.js +0 -92
- package/dist/ui/persistentPrompt.js.map +0 -1
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Design principles:
|
|
5
5
|
* - Single source of truth for input state
|
|
6
|
+
* - One bottom-pinned chat box for the entire session (no inline anchors)
|
|
6
7
|
* - Native bracketed paste support (no heuristics)
|
|
7
8
|
* - Clean cursor model with render-time wrapping
|
|
8
9
|
* - State machine for different input modes
|
|
9
10
|
* - No readline dependency for display
|
|
10
11
|
*/
|
|
11
12
|
import { EventEmitter } from 'node:events';
|
|
12
|
-
import { isMultilinePaste
|
|
13
|
+
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
13
14
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
15
|
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
15
16
|
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
@@ -67,6 +68,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
67
68
|
statusMessage = null;
|
|
68
69
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
69
70
|
streamingLabel = null; // Streaming progress indicator
|
|
71
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
72
|
+
metaTokensUsed = null; // Optional token usage
|
|
73
|
+
metaTokenLimit = null; // Optional token window
|
|
70
74
|
reservedLines = 2;
|
|
71
75
|
scrollRegionActive = false;
|
|
72
76
|
lastRenderContent = '';
|
|
@@ -74,9 +78,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
74
78
|
renderDirty = false;
|
|
75
79
|
isRendering = false;
|
|
76
80
|
pinnedTopRows = 0;
|
|
77
|
-
inlineAnchorRow = null;
|
|
78
|
-
inlineLayout = false;
|
|
79
|
-
anchorProvider = null;
|
|
80
81
|
// Lifecycle
|
|
81
82
|
disposed = false;
|
|
82
83
|
enabled = true;
|
|
@@ -91,10 +92,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
91
92
|
// Streaming render throttle
|
|
92
93
|
lastStreamingRender = 0;
|
|
93
94
|
streamingRenderInterval = 250; // ms between renders during streaming
|
|
94
|
-
// Metrics tracking for status bar
|
|
95
|
-
streamingStartTime = null;
|
|
96
|
-
tokensUsed = 0;
|
|
97
|
-
thinkingEnabled = true;
|
|
98
95
|
constructor(writeStream = process.stdout, config = {}) {
|
|
99
96
|
super();
|
|
100
97
|
this.out = writeStream;
|
|
@@ -182,11 +179,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
182
179
|
if (handled)
|
|
183
180
|
return;
|
|
184
181
|
}
|
|
185
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
186
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
187
|
-
this.emit('showHelp');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
182
|
// Insert printable characters
|
|
191
183
|
if (str && !key?.ctrl && !key?.meta) {
|
|
192
184
|
this.insertText(str);
|
|
@@ -195,64 +187,24 @@ export class TerminalInput extends EventEmitter {
|
|
|
195
187
|
/**
|
|
196
188
|
* Set the input mode
|
|
197
189
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* - Position cursor at where input was so content flows from there
|
|
201
|
-
*
|
|
202
|
-
* When exiting streaming mode:
|
|
203
|
-
* - Re-render the input area below the new content
|
|
190
|
+
* Streaming keeps the scroll region active so the prompt/status stay pinned
|
|
191
|
+
* below the streaming output. When streaming ends, we refresh the input area.
|
|
204
192
|
*/
|
|
205
193
|
setMode(mode) {
|
|
206
194
|
const prevMode = this.mode;
|
|
207
195
|
this.mode = mode;
|
|
208
196
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
209
|
-
//
|
|
210
|
-
this.
|
|
211
|
-
// Disable scroll region - let content flow naturally
|
|
212
|
-
// The input area will be re-rendered after streaming ends
|
|
213
|
-
this.disableScrollRegion();
|
|
214
|
-
// Clear the input area at the bottom so streaming content can use it
|
|
215
|
-
const { rows, cols } = this.getSize();
|
|
216
|
-
this.write(ESC.HIDE);
|
|
217
|
-
// Clear from status bar row to bottom
|
|
218
|
-
const statusBarRow = Math.max(1, rows - this.reservedLines - 1);
|
|
219
|
-
for (let r = statusBarRow; r <= rows; r++) {
|
|
220
|
-
this.write(ESC.TO(r, 1) + ESC.CLEAR_LINE);
|
|
221
|
-
}
|
|
222
|
-
// Position cursor where streaming content should start (where status bar was)
|
|
223
|
-
this.write(ESC.TO(statusBarRow, 1));
|
|
224
|
-
this.write(ESC.SHOW);
|
|
197
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
198
|
+
this.enableScrollRegion();
|
|
225
199
|
this.renderDirty = true;
|
|
200
|
+
this.render();
|
|
226
201
|
}
|
|
227
202
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
228
|
-
//
|
|
229
|
-
this.
|
|
230
|
-
// Add newlines to ensure input area doesn't overlap last content
|
|
231
|
-
this.write('\n');
|
|
232
|
-
// Re-render the input area below the streaming content
|
|
203
|
+
// Streaming ended - render the input area
|
|
204
|
+
this.enableScrollRegion();
|
|
233
205
|
this.forceRender();
|
|
234
206
|
}
|
|
235
207
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Update token count for metrics display
|
|
238
|
-
*/
|
|
239
|
-
setTokensUsed(tokens) {
|
|
240
|
-
this.tokensUsed = tokens;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Toggle thinking/reasoning mode
|
|
244
|
-
*/
|
|
245
|
-
toggleThinking() {
|
|
246
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
247
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
248
|
-
this.scheduleRender();
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Get thinking enabled state
|
|
252
|
-
*/
|
|
253
|
-
isThinkingEnabled() {
|
|
254
|
-
return this.thinkingEnabled;
|
|
255
|
-
}
|
|
256
208
|
/**
|
|
257
209
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
258
210
|
*/
|
|
@@ -265,42 +217,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
265
217
|
}
|
|
266
218
|
}
|
|
267
219
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
270
|
-
* restore the default bottom-aligned layout.
|
|
271
|
-
*/
|
|
272
|
-
setInlineAnchor(row) {
|
|
273
|
-
if (row === null || row === undefined) {
|
|
274
|
-
this.inlineAnchorRow = null;
|
|
275
|
-
this.inlineLayout = false;
|
|
276
|
-
this.renderDirty = true;
|
|
277
|
-
this.render();
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const { rows } = this.getSize();
|
|
281
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
282
|
-
this.inlineAnchorRow = clamped;
|
|
283
|
-
this.inlineLayout = true;
|
|
284
|
-
this.renderDirty = true;
|
|
285
|
-
this.render();
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
289
|
-
* output by re-evaluating the anchor before each render.
|
|
290
|
-
*/
|
|
291
|
-
setInlineAnchorProvider(provider) {
|
|
292
|
-
this.anchorProvider = provider;
|
|
293
|
-
if (!provider) {
|
|
294
|
-
this.inlineLayout = false;
|
|
295
|
-
this.inlineAnchorRow = null;
|
|
296
|
-
this.renderDirty = true;
|
|
297
|
-
this.render();
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
this.inlineLayout = true;
|
|
301
|
-
this.renderDirty = true;
|
|
302
|
-
this.render();
|
|
303
|
-
}
|
|
304
220
|
/**
|
|
305
221
|
* Get current mode
|
|
306
222
|
*/
|
|
@@ -410,6 +326,29 @@ export class TerminalInput extends EventEmitter {
|
|
|
410
326
|
this.streamingLabel = next;
|
|
411
327
|
this.scheduleRender();
|
|
412
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
331
|
+
*/
|
|
332
|
+
setMetaStatus(meta) {
|
|
333
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
334
|
+
? Math.floor(meta.elapsedSeconds)
|
|
335
|
+
: null;
|
|
336
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
337
|
+
? Math.floor(meta.tokensUsed)
|
|
338
|
+
: null;
|
|
339
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
340
|
+
? Math.floor(meta.tokenLimit)
|
|
341
|
+
: null;
|
|
342
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
343
|
+
this.metaTokensUsed === nextTokens &&
|
|
344
|
+
this.metaTokenLimit === nextLimit) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
348
|
+
this.metaTokensUsed = nextTokens;
|
|
349
|
+
this.metaTokenLimit = nextLimit;
|
|
350
|
+
this.scheduleRender();
|
|
351
|
+
}
|
|
413
352
|
/**
|
|
414
353
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
415
354
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -488,9 +427,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
488
427
|
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
489
428
|
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
490
429
|
const displayLines = Math.min(lines.length, maxVisible);
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
this.updateReservedLines(displayLines + 2);
|
|
430
|
+
const metaLine = this.buildMetaLine(cols - 2);
|
|
431
|
+
// Reserved lines: optional meta(1) + separator(1) + input lines + controls(1)
|
|
432
|
+
this.updateReservedLines(displayLines + 2 + (metaLine ? 1 : 0));
|
|
494
433
|
// Calculate display window (keep cursor visible)
|
|
495
434
|
let startLine = 0;
|
|
496
435
|
if (lines.length > displayLines) {
|
|
@@ -502,35 +441,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
502
441
|
// Render
|
|
503
442
|
this.write(ESC.HIDE);
|
|
504
443
|
this.write(ESC.RESET);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const topSepRow = inputStartRow - 1;
|
|
517
|
-
const statusBarRow = topSepRow - 1;
|
|
518
|
-
// Reserved lines: status(1) + topSep(1) + input + bottomSep(1) + controls(1)
|
|
519
|
-
this.updateReservedLines(visibleLines.length + 4);
|
|
520
|
-
// Status bar (streaming status + metrics)
|
|
521
|
-
this.write(ESC.TO(statusBarRow, 1));
|
|
522
|
-
this.write(ESC.CLEAR_LINE);
|
|
523
|
-
this.write(this.buildStatusBar(cols));
|
|
524
|
-
// Top separator
|
|
525
|
-
this.write(ESC.TO(topSepRow, 1));
|
|
444
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
445
|
+
let currentRow = startRow;
|
|
446
|
+
// Meta/status header (elapsed, tokens/context)
|
|
447
|
+
if (metaLine) {
|
|
448
|
+
this.write(ESC.TO(currentRow, 1));
|
|
449
|
+
this.write(ESC.CLEAR_LINE);
|
|
450
|
+
this.write(metaLine);
|
|
451
|
+
currentRow += 1;
|
|
452
|
+
}
|
|
453
|
+
// Separator line
|
|
454
|
+
this.write(ESC.TO(currentRow, 1));
|
|
526
455
|
this.write(ESC.CLEAR_LINE);
|
|
527
456
|
const divider = renderDivider(cols - 2);
|
|
528
457
|
this.write(divider);
|
|
458
|
+
currentRow += 1;
|
|
529
459
|
// Render input lines
|
|
530
|
-
let finalRow =
|
|
460
|
+
let finalRow = currentRow;
|
|
531
461
|
let finalCol = 3;
|
|
532
462
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
533
|
-
const rowNum =
|
|
463
|
+
const rowNum = currentRow + i;
|
|
534
464
|
this.write(ESC.TO(rowNum, 1));
|
|
535
465
|
this.write(ESC.CLEAR_LINE);
|
|
536
466
|
const line = visibleLines[i] ?? '';
|
|
@@ -568,15 +498,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
568
498
|
this.write(' '.repeat(padding));
|
|
569
499
|
this.write(ESC.RESET);
|
|
570
500
|
}
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
this.write(ESC.
|
|
574
|
-
this.write(divider);
|
|
575
|
-
// Mode controls (shortcuts + thinking toggle)
|
|
576
|
-
this.write(ESC.TO(modeControlRow, 1));
|
|
501
|
+
// Mode controls line (Claude Code style)
|
|
502
|
+
const controlRow = currentRow + visibleLines.length;
|
|
503
|
+
this.write(ESC.TO(controlRow, 1));
|
|
577
504
|
this.write(ESC.CLEAR_LINE);
|
|
578
505
|
this.write(this.buildModeControls(cols));
|
|
579
|
-
// Position cursor
|
|
506
|
+
// Position cursor
|
|
580
507
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
581
508
|
this.write(ESC.SHOW);
|
|
582
509
|
// Update state
|
|
@@ -589,86 +516,69 @@ export class TerminalInput extends EventEmitter {
|
|
|
589
516
|
}
|
|
590
517
|
}
|
|
591
518
|
/**
|
|
592
|
-
* Build
|
|
519
|
+
* Build a compact meta line above the divider (elapsed, context usage, queue size).
|
|
593
520
|
*/
|
|
594
|
-
|
|
521
|
+
buildMetaLine(width) {
|
|
595
522
|
const parts = [];
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
parts.push({ text: '● Streaming', tone: 'success' });
|
|
599
|
-
// Elapsed time
|
|
600
|
-
if (this.streamingStartTime) {
|
|
601
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
602
|
-
const mins = Math.floor(elapsed / 60);
|
|
603
|
-
const secs = elapsed % 60;
|
|
604
|
-
const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
605
|
-
parts.push({ text: `⏱ ${timeStr}`, tone: 'muted' });
|
|
606
|
-
}
|
|
523
|
+
if (this.metaElapsedSeconds !== null) {
|
|
524
|
+
parts.push({ text: `elapsed ${this.metaElapsedSeconds}s`, tone: 'muted' });
|
|
607
525
|
}
|
|
608
|
-
|
|
609
|
-
|
|
526
|
+
if (this.metaTokensUsed !== null) {
|
|
527
|
+
const limitText = this.metaTokenLimit ? `/${this.metaTokenLimit}` : '';
|
|
528
|
+
parts.push({ text: `tokens ${this.metaTokensUsed}${limitText}`, tone: 'muted' });
|
|
610
529
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
? `${(this.tokensUsed / 1000).toFixed(1)}k`
|
|
615
|
-
: `${this.tokensUsed}`;
|
|
616
|
-
parts.push({ text: `◈ ${tokenStr} tokens`, tone: 'info' });
|
|
530
|
+
if (this.contextUsage !== null) {
|
|
531
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
532
|
+
parts.push({ text: `used ${this.contextUsage}%`, tone });
|
|
617
533
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
text: this.thinkingEnabled ? '💭 Thinking on' : '💭 Thinking off',
|
|
621
|
-
tone: this.thinkingEnabled ? 'info' : 'muted',
|
|
622
|
-
});
|
|
623
|
-
// Paste block count if any
|
|
624
|
-
if (this.pastePlaceholders.length > 0) {
|
|
625
|
-
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
626
|
-
parts.push({
|
|
627
|
-
text: `📋 ${this.pastePlaceholders.length} paste(s), ${totalLines} lines`,
|
|
628
|
-
tone: 'info',
|
|
629
|
-
});
|
|
534
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
535
|
+
parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
630
536
|
}
|
|
631
|
-
return renderStatusLine(parts,
|
|
537
|
+
return parts.length ? renderStatusLine(parts, width) : '';
|
|
632
538
|
}
|
|
633
539
|
/**
|
|
634
540
|
* Build Claude Code style mode controls line.
|
|
635
|
-
*
|
|
541
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
636
542
|
*/
|
|
637
543
|
buildModeControls(cols) {
|
|
638
544
|
const parts = [];
|
|
639
|
-
// Keyboard shortcuts hint
|
|
640
|
-
parts.push({ text: '? help', tone: 'muted' });
|
|
641
|
-
parts.push({ text: 'esc interrupt', tone: 'muted' });
|
|
642
|
-
// Streaming status messages
|
|
643
545
|
if (this.streamingLabel) {
|
|
644
546
|
parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
645
547
|
}
|
|
646
548
|
if (this.overrideStatusMessage) {
|
|
647
549
|
parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
648
550
|
}
|
|
649
|
-
|
|
650
|
-
|
|
551
|
+
if (this.statusMessage) {
|
|
552
|
+
parts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
553
|
+
}
|
|
554
|
+
const verifyLabel = this.verificationEnabled ? 'verify+build on' : 'verify+build off';
|
|
651
555
|
parts.push({
|
|
652
556
|
text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
|
|
653
557
|
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
654
558
|
});
|
|
559
|
+
const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
|
|
560
|
+
parts.push({
|
|
561
|
+
text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
|
|
562
|
+
tone: this.autoContinueEnabled ? 'info' : 'muted',
|
|
563
|
+
});
|
|
655
564
|
const editLabel = this.editMode === 'display-edits' ? 'auto-edit' : 'ask first';
|
|
656
565
|
parts.push({ text: `${editLabel} (⇧⇥)`, tone: 'muted' });
|
|
657
|
-
// Context usage
|
|
658
566
|
if (this.contextUsage !== null) {
|
|
659
567
|
const pct = Math.max(0, 100 - this.contextUsage);
|
|
660
568
|
const tone = pct < 25 ? 'warn' : 'muted';
|
|
661
|
-
parts.push({ text: `
|
|
662
|
-
}
|
|
663
|
-
// Queue during streaming
|
|
664
|
-
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
665
|
-
parts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
569
|
+
parts.push({ text: `context ${pct}%`, tone });
|
|
666
570
|
}
|
|
667
|
-
// Multi-line indicator
|
|
668
571
|
if (this.buffer.includes('\n')) {
|
|
669
572
|
const lineCount = this.buffer.split('\n').length;
|
|
670
573
|
parts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
671
574
|
}
|
|
575
|
+
if (this.pastePlaceholders.length > 0) {
|
|
576
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
577
|
+
parts.push({
|
|
578
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
579
|
+
tone: 'info',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
672
582
|
return renderStatusLine(parts, cols - 2);
|
|
673
583
|
}
|
|
674
584
|
/**
|
|
@@ -704,23 +614,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
704
614
|
* Register with display's output interceptor to position cursor correctly.
|
|
705
615
|
* When scroll region is active, output needs to go to the scroll region,
|
|
706
616
|
* not the protected bottom area where the input is rendered.
|
|
707
|
-
*
|
|
708
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
709
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
710
617
|
*/
|
|
711
618
|
registerOutputInterceptor(display) {
|
|
712
619
|
if (this.outputInterceptorCleanup) {
|
|
713
620
|
this.outputInterceptorCleanup();
|
|
714
621
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// Scroll region handles content containment automatically
|
|
718
|
-
// No per-write cursor manipulation needed
|
|
719
|
-
},
|
|
720
|
-
afterWrite: () => {
|
|
721
|
-
// No cursor manipulation needed
|
|
722
|
-
},
|
|
723
|
-
});
|
|
622
|
+
// Scroll region handles containment now; interceptor is a no-op placeholder to keep API stable.
|
|
623
|
+
this.outputInterceptorCleanup = display.registerOutputInterceptor({});
|
|
724
624
|
}
|
|
725
625
|
/**
|
|
726
626
|
* Dispose and clean up
|
|
@@ -840,22 +740,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
840
740
|
this.toggleEditMode();
|
|
841
741
|
return true;
|
|
842
742
|
}
|
|
843
|
-
|
|
844
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
845
|
-
this.togglePasteExpansion();
|
|
846
|
-
}
|
|
847
|
-
else {
|
|
848
|
-
this.toggleThinking();
|
|
849
|
-
}
|
|
850
|
-
return true;
|
|
851
|
-
case 'escape':
|
|
852
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
853
|
-
if (this.mode === 'streaming') {
|
|
854
|
-
this.emit('interrupt');
|
|
855
|
-
}
|
|
856
|
-
else if (this.buffer.length > 0) {
|
|
857
|
-
this.clear();
|
|
858
|
-
}
|
|
743
|
+
this.insertText(' ');
|
|
859
744
|
return true;
|
|
860
745
|
}
|
|
861
746
|
return false;
|
|
@@ -1146,7 +1031,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1146
1031
|
if (available <= 0)
|
|
1147
1032
|
return;
|
|
1148
1033
|
const chunk = clean.slice(0, available);
|
|
1149
|
-
|
|
1034
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1035
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1036
|
+
if (isMultiline && !isShortMultiline) {
|
|
1150
1037
|
this.insertPastePlaceholder(chunk);
|
|
1151
1038
|
}
|
|
1152
1039
|
else {
|
|
@@ -1166,8 +1053,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1166
1053
|
return;
|
|
1167
1054
|
this.applyScrollRegion();
|
|
1168
1055
|
this.scrollRegionActive = true;
|
|
1169
|
-
|
|
1170
|
-
// This prevents cursor repositioning issues when entering streaming mode
|
|
1056
|
+
this.forceRender();
|
|
1171
1057
|
}
|
|
1172
1058
|
disableScrollRegion() {
|
|
1173
1059
|
if (!this.scrollRegionActive)
|
|
@@ -1318,17 +1204,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1318
1204
|
this.shiftPlaceholders(position, text.length);
|
|
1319
1205
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1320
1206
|
}
|
|
1207
|
+
shouldInlineMultiline(content) {
|
|
1208
|
+
const lines = content.split('\n').length;
|
|
1209
|
+
const maxInlineLines = 4;
|
|
1210
|
+
const maxInlineChars = 240;
|
|
1211
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1212
|
+
}
|
|
1321
1213
|
findPlaceholderAt(position) {
|
|
1322
1214
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1323
1215
|
}
|
|
1324
|
-
buildPlaceholder(
|
|
1216
|
+
buildPlaceholder(lineCount) {
|
|
1325
1217
|
const id = ++this.pasteCounter;
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
const preview = summary.preview.length > 30
|
|
1329
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1330
|
-
: summary.preview;
|
|
1331
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1218
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1219
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1332
1220
|
return { id, placeholder };
|
|
1333
1221
|
}
|
|
1334
1222
|
insertPastePlaceholder(content) {
|
|
@@ -1336,67 +1224,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1336
1224
|
if (available <= 0)
|
|
1337
1225
|
return;
|
|
1338
1226
|
const cleanContent = content.slice(0, available);
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
if (summary.lineCount < 5) {
|
|
1342
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1343
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1344
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1345
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1227
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1228
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1349
1229
|
const insertPos = this.cursor;
|
|
1350
1230
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1351
1231
|
this.pastePlaceholders.push({
|
|
1352
1232
|
id,
|
|
1353
1233
|
content: cleanContent,
|
|
1354
|
-
lineCount
|
|
1234
|
+
lineCount,
|
|
1355
1235
|
placeholder,
|
|
1356
1236
|
start: insertPos,
|
|
1357
1237
|
end: insertPos + placeholder.length,
|
|
1358
|
-
summary,
|
|
1359
|
-
expanded: false,
|
|
1360
1238
|
});
|
|
1361
1239
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1362
1240
|
this.cursor = insertPos + placeholder.length;
|
|
1363
1241
|
}
|
|
1364
|
-
/**
|
|
1365
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1366
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1367
|
-
*/
|
|
1368
|
-
togglePasteExpansion() {
|
|
1369
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1370
|
-
if (!placeholder)
|
|
1371
|
-
return false;
|
|
1372
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1373
|
-
// Update the placeholder text in buffer
|
|
1374
|
-
const newPlaceholder = placeholder.expanded
|
|
1375
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1376
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1377
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1378
|
-
// Update buffer
|
|
1379
|
-
this.buffer =
|
|
1380
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1381
|
-
newPlaceholder +
|
|
1382
|
-
this.buffer.slice(placeholder.end);
|
|
1383
|
-
// Update placeholder tracking
|
|
1384
|
-
placeholder.placeholder = newPlaceholder;
|
|
1385
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1386
|
-
// Shift other placeholders
|
|
1387
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1388
|
-
this.scheduleRender();
|
|
1389
|
-
return true;
|
|
1390
|
-
}
|
|
1391
|
-
buildExpandedPlaceholder(ph) {
|
|
1392
|
-
const lines = ph.content.split('\n');
|
|
1393
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1394
|
-
const lastLines = lines.length > 5
|
|
1395
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1396
|
-
: '';
|
|
1397
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1398
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1399
|
-
}
|
|
1400
1242
|
deletePlaceholder(placeholder) {
|
|
1401
1243
|
const length = placeholder.end - placeholder.start;
|
|
1402
1244
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|