erosolar-cli 1.7.231 → 1.7.233
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 +148 -22
- package/dist/core/aiFlowOptimizer.d.ts +26 -0
- package/dist/core/aiFlowOptimizer.d.ts.map +1 -0
- package/dist/core/aiFlowOptimizer.js +31 -0
- package/dist/core/aiFlowOptimizer.js.map +1 -0
- package/dist/core/aiOptimizationEngine.d.ts +158 -0
- package/dist/core/aiOptimizationEngine.d.ts.map +1 -0
- package/dist/core/aiOptimizationEngine.js +428 -0
- package/dist/core/aiOptimizationEngine.js.map +1 -0
- package/dist/core/aiOptimizationIntegration.d.ts +93 -0
- package/dist/core/aiOptimizationIntegration.d.ts.map +1 -0
- package/dist/core/aiOptimizationIntegration.js +250 -0
- package/dist/core/aiOptimizationIntegration.js.map +1 -0
- package/dist/core/enhancedErrorRecovery.d.ts +100 -0
- package/dist/core/enhancedErrorRecovery.d.ts.map +1 -0
- package/dist/core/enhancedErrorRecovery.js +345 -0
- package/dist/core/enhancedErrorRecovery.js.map +1 -0
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +18 -9
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.d.ts +6 -0
- package/dist/plugins/tools/build/buildPlugin.d.ts.map +1 -1
- package/dist/plugins/tools/build/buildPlugin.js +10 -4
- package/dist/plugins/tools/build/buildPlugin.js.map +1 -1
- package/dist/shell/claudeCodeStreamHandler.d.ts +145 -0
- package/dist/shell/claudeCodeStreamHandler.d.ts.map +1 -0
- package/dist/shell/claudeCodeStreamHandler.js +322 -0
- package/dist/shell/claudeCodeStreamHandler.js.map +1 -0
- package/dist/shell/inputQueueManager.d.ts +144 -0
- package/dist/shell/inputQueueManager.d.ts.map +1 -0
- package/dist/shell/inputQueueManager.js +290 -0
- package/dist/shell/inputQueueManager.js.map +1 -0
- package/dist/shell/interactiveShell.d.ts +2 -2
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +14 -13
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/streamingOutputManager.d.ts +115 -0
- package/dist/shell/streamingOutputManager.d.ts.map +1 -0
- package/dist/shell/streamingOutputManager.js +225 -0
- package/dist/shell/streamingOutputManager.js.map +1 -0
- package/dist/shell/terminalInput.d.ts +68 -21
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +449 -210
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +14 -6
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +20 -6
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +50 -0
- package/dist/ui/persistentPrompt.d.ts.map +1 -0
- package/dist/ui/persistentPrompt.js +92 -0
- package/dist/ui/persistentPrompt.js.map +1 -0
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +8 -6
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +26 -13
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,14 +3,13 @@
|
|
|
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)
|
|
7
6
|
* - Native bracketed paste support (no heuristics)
|
|
8
7
|
* - Clean cursor model with render-time wrapping
|
|
9
8
|
* - State machine for different input modes
|
|
10
9
|
* - No readline dependency for display
|
|
11
10
|
*/
|
|
12
11
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import { isMultilinePaste } from '../core/multilinePasteHandler.js';
|
|
12
|
+
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
14
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
15
14
|
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
16
15
|
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
@@ -68,9 +67,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
68
67
|
statusMessage = null;
|
|
69
68
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
70
69
|
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
|
|
74
70
|
reservedLines = 2;
|
|
75
71
|
scrollRegionActive = false;
|
|
76
72
|
lastRenderContent = '';
|
|
@@ -78,6 +74,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
78
74
|
renderDirty = false;
|
|
79
75
|
isRendering = false;
|
|
80
76
|
pinnedTopRows = 0;
|
|
77
|
+
inlineAnchorRow = null;
|
|
78
|
+
inlineLayout = false;
|
|
79
|
+
anchorProvider = null;
|
|
80
|
+
// Flow mode: render input immediately after content instead of at absolute bottom
|
|
81
|
+
flowMode = true;
|
|
82
|
+
// Track how many lines we rendered in flow mode for proper re-rendering
|
|
83
|
+
flowModeRenderedLines = 0;
|
|
81
84
|
// Lifecycle
|
|
82
85
|
disposed = false;
|
|
83
86
|
enabled = true;
|
|
@@ -92,6 +95,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
92
95
|
// Streaming render throttle
|
|
93
96
|
lastStreamingRender = 0;
|
|
94
97
|
streamingRenderInterval = 250; // ms between renders during streaming
|
|
98
|
+
// Metrics tracking for status bar
|
|
99
|
+
streamingStartTime = null;
|
|
100
|
+
tokensUsed = 0;
|
|
101
|
+
thinkingEnabled = true;
|
|
95
102
|
constructor(writeStream = process.stdout, config = {}) {
|
|
96
103
|
super();
|
|
97
104
|
this.out = writeStream;
|
|
@@ -179,6 +186,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
179
186
|
if (handled)
|
|
180
187
|
return;
|
|
181
188
|
}
|
|
189
|
+
// Handle '?' for help hint (if buffer is empty)
|
|
190
|
+
if (str === '?' && this.buffer.length === 0) {
|
|
191
|
+
this.emit('showHelp');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
182
194
|
// Insert printable characters
|
|
183
195
|
if (str && !key?.ctrl && !key?.meta) {
|
|
184
196
|
this.insertText(str);
|
|
@@ -187,24 +199,75 @@ export class TerminalInput extends EventEmitter {
|
|
|
187
199
|
/**
|
|
188
200
|
* Set the input mode
|
|
189
201
|
*
|
|
190
|
-
* Streaming
|
|
191
|
-
*
|
|
202
|
+
* Streaming mode disables scroll region and lets content flow naturally.
|
|
203
|
+
* The input area will be re-rendered after streaming ends at wherever
|
|
204
|
+
* the cursor is (below the streamed content).
|
|
192
205
|
*/
|
|
193
206
|
setMode(mode) {
|
|
194
207
|
const prevMode = this.mode;
|
|
195
208
|
this.mode = mode;
|
|
196
209
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
197
|
-
//
|
|
198
|
-
this.
|
|
210
|
+
// Track streaming start time for elapsed display
|
|
211
|
+
this.streamingStartTime = Date.now();
|
|
212
|
+
// Disable scroll region - let content flow naturally from current position
|
|
213
|
+
// This means content appears right after where cursor currently is
|
|
214
|
+
// (which should be after the banner)
|
|
215
|
+
this.disableScrollRegion();
|
|
216
|
+
// Reset flow mode rendered lines - content will flow naturally during streaming
|
|
217
|
+
this.flowModeRenderedLines = 0;
|
|
218
|
+
// Hide cursor during streaming to avoid racing chars
|
|
219
|
+
this.write(ESC.HIDE);
|
|
199
220
|
this.renderDirty = true;
|
|
200
|
-
this.render();
|
|
201
221
|
}
|
|
202
222
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
203
|
-
//
|
|
204
|
-
this.
|
|
223
|
+
// Reset streaming time
|
|
224
|
+
this.streamingStartTime = null;
|
|
225
|
+
// Show cursor again
|
|
226
|
+
this.write(ESC.SHOW);
|
|
227
|
+
// Add a newline to separate content from input area
|
|
228
|
+
this.write('\n');
|
|
229
|
+
// Re-render the input area below the content
|
|
205
230
|
this.forceRender();
|
|
206
231
|
}
|
|
207
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Enable or disable flow mode.
|
|
235
|
+
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
236
|
+
* When disabled, input renders at the absolute bottom of terminal.
|
|
237
|
+
*/
|
|
238
|
+
setFlowMode(enabled) {
|
|
239
|
+
if (this.flowMode === enabled)
|
|
240
|
+
return;
|
|
241
|
+
this.flowMode = enabled;
|
|
242
|
+
this.renderDirty = true;
|
|
243
|
+
this.scheduleRender();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if flow mode is enabled.
|
|
247
|
+
*/
|
|
248
|
+
isFlowMode() {
|
|
249
|
+
return this.flowMode;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Update token count for metrics display
|
|
253
|
+
*/
|
|
254
|
+
setTokensUsed(tokens) {
|
|
255
|
+
this.tokensUsed = tokens;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Toggle thinking/reasoning mode
|
|
259
|
+
*/
|
|
260
|
+
toggleThinking() {
|
|
261
|
+
this.thinkingEnabled = !this.thinkingEnabled;
|
|
262
|
+
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
263
|
+
this.scheduleRender();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get thinking enabled state
|
|
267
|
+
*/
|
|
268
|
+
isThinkingEnabled() {
|
|
269
|
+
return this.thinkingEnabled;
|
|
270
|
+
}
|
|
208
271
|
/**
|
|
209
272
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
210
273
|
*/
|
|
@@ -217,6 +280,42 @@ export class TerminalInput extends EventEmitter {
|
|
|
217
280
|
}
|
|
218
281
|
}
|
|
219
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
285
|
+
* restore the default bottom-aligned layout.
|
|
286
|
+
*/
|
|
287
|
+
setInlineAnchor(row) {
|
|
288
|
+
if (row === null || row === undefined) {
|
|
289
|
+
this.inlineAnchorRow = null;
|
|
290
|
+
this.inlineLayout = false;
|
|
291
|
+
this.renderDirty = true;
|
|
292
|
+
this.render();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const { rows } = this.getSize();
|
|
296
|
+
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
297
|
+
this.inlineAnchorRow = clamped;
|
|
298
|
+
this.inlineLayout = true;
|
|
299
|
+
this.renderDirty = true;
|
|
300
|
+
this.render();
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
304
|
+
* output by re-evaluating the anchor before each render.
|
|
305
|
+
*/
|
|
306
|
+
setInlineAnchorProvider(provider) {
|
|
307
|
+
this.anchorProvider = provider;
|
|
308
|
+
if (!provider) {
|
|
309
|
+
this.inlineLayout = false;
|
|
310
|
+
this.inlineAnchorRow = null;
|
|
311
|
+
this.renderDirty = true;
|
|
312
|
+
this.render();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
this.inlineLayout = true;
|
|
316
|
+
this.renderDirty = true;
|
|
317
|
+
this.render();
|
|
318
|
+
}
|
|
220
319
|
/**
|
|
221
320
|
* Get current mode
|
|
222
321
|
*/
|
|
@@ -326,29 +425,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
326
425
|
this.streamingLabel = next;
|
|
327
426
|
this.scheduleRender();
|
|
328
427
|
}
|
|
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
|
-
}
|
|
352
428
|
/**
|
|
353
429
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
354
430
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -417,100 +493,18 @@ export class TerminalInput extends EventEmitter {
|
|
|
417
493
|
// Use write lock during render to prevent interleaved output
|
|
418
494
|
writeLock.lock('terminalInput.render');
|
|
419
495
|
try {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
425
|
-
// Wrap buffer into display lines
|
|
426
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
427
|
-
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
428
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
429
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
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));
|
|
433
|
-
// Calculate display window (keep cursor visible)
|
|
434
|
-
let startLine = 0;
|
|
435
|
-
if (lines.length > displayLines) {
|
|
436
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
437
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
496
|
+
// No scroll regions - we render the input area directly at the bottom
|
|
497
|
+
// Content flows naturally above it
|
|
498
|
+
if (this.scrollRegionActive) {
|
|
499
|
+
this.disableScrollRegion();
|
|
438
500
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
this.write(ESC.HIDE);
|
|
443
|
-
this.write(ESC.RESET);
|
|
444
|
-
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
445
|
-
let currentRow = startRow;
|
|
446
|
-
// Clear the reserved block to avoid stale meta/status lines
|
|
447
|
-
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
448
|
-
// Meta/status header (elapsed, tokens/context)
|
|
449
|
-
if (metaLine) {
|
|
450
|
-
this.write(ESC.TO(currentRow, 1));
|
|
451
|
-
this.write(ESC.CLEAR_LINE);
|
|
452
|
-
this.write(metaLine);
|
|
453
|
-
currentRow += 1;
|
|
501
|
+
// Use flow mode rendering (inline after content) for better UX
|
|
502
|
+
if (this.flowMode) {
|
|
503
|
+
this.renderFlowMode();
|
|
454
504
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
this.write(ESC.CLEAR_LINE);
|
|
458
|
-
const divider = renderDivider(cols - 2);
|
|
459
|
-
this.write(divider);
|
|
460
|
-
currentRow += 1;
|
|
461
|
-
// Render input lines
|
|
462
|
-
let finalRow = currentRow;
|
|
463
|
-
let finalCol = 3;
|
|
464
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
465
|
-
const rowNum = currentRow + i;
|
|
466
|
-
this.write(ESC.TO(rowNum, 1));
|
|
467
|
-
this.write(ESC.CLEAR_LINE);
|
|
468
|
-
const line = visibleLines[i] ?? '';
|
|
469
|
-
const absoluteLineIdx = startLine + i;
|
|
470
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
471
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
472
|
-
// Background
|
|
473
|
-
this.write(ESC.BG_DARK);
|
|
474
|
-
// Prompt prefix
|
|
475
|
-
this.write(ESC.DIM);
|
|
476
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
477
|
-
this.write(ESC.RESET);
|
|
478
|
-
this.write(ESC.BG_DARK);
|
|
479
|
-
if (isCursorLine) {
|
|
480
|
-
// Render with block cursor
|
|
481
|
-
const col = Math.min(cursorCol, line.length);
|
|
482
|
-
const before = line.slice(0, col);
|
|
483
|
-
const at = col < line.length ? line[col] : ' ';
|
|
484
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
485
|
-
this.write(before);
|
|
486
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
487
|
-
this.write(at);
|
|
488
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
489
|
-
this.write(after);
|
|
490
|
-
finalRow = rowNum;
|
|
491
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
this.write(line);
|
|
495
|
-
}
|
|
496
|
-
// Pad to edge for clean look
|
|
497
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
498
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
499
|
-
if (padding > 0)
|
|
500
|
-
this.write(' '.repeat(padding));
|
|
501
|
-
this.write(ESC.RESET);
|
|
505
|
+
else {
|
|
506
|
+
this.renderBottomPinned();
|
|
502
507
|
}
|
|
503
|
-
// Mode controls line (Claude Code style)
|
|
504
|
-
const controlRow = currentRow + visibleLines.length;
|
|
505
|
-
this.write(ESC.TO(controlRow, 1));
|
|
506
|
-
this.write(ESC.CLEAR_LINE);
|
|
507
|
-
this.write(this.buildModeControls(cols));
|
|
508
|
-
// Position cursor
|
|
509
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
510
|
-
this.write(ESC.SHOW);
|
|
511
|
-
// Update state
|
|
512
|
-
this.lastRenderContent = this.buffer;
|
|
513
|
-
this.lastRenderCursor = this.cursor;
|
|
514
508
|
}
|
|
515
509
|
finally {
|
|
516
510
|
writeLock.unlock();
|
|
@@ -518,83 +512,278 @@ export class TerminalInput extends EventEmitter {
|
|
|
518
512
|
}
|
|
519
513
|
}
|
|
520
514
|
/**
|
|
521
|
-
*
|
|
515
|
+
* Render in flow mode - input area renders inline at current cursor position
|
|
516
|
+
* This creates a unified look where input flows naturally with content.
|
|
517
|
+
*
|
|
518
|
+
* Uses terminal save/restore cursor position for more stable re-renders.
|
|
519
|
+
* Layout: [status] [divider] [input...] [divider] [controls]
|
|
522
520
|
*/
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
521
|
+
renderFlowMode() {
|
|
522
|
+
const { cols } = this.getSize();
|
|
523
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
524
|
+
// Wrap buffer into display lines
|
|
525
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
526
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, 10));
|
|
527
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
528
|
+
// Calculate display window (keep cursor visible)
|
|
529
|
+
let startLine = 0;
|
|
530
|
+
if (lines.length > displayLines) {
|
|
531
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
532
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
533
|
+
}
|
|
534
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
535
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
536
|
+
// Total lines: status(1) + topDiv(1) + input(N) + bottomDiv(1) + controls(1)
|
|
537
|
+
const totalLines = visibleLines.length + 4;
|
|
538
|
+
this.write(ESC.HIDE);
|
|
539
|
+
this.write(ESC.RESET);
|
|
540
|
+
// If we've previously rendered, go back to the start of our rendered area
|
|
541
|
+
if (this.flowModeRenderedLines > 0) {
|
|
542
|
+
// Move cursor up to where our content started
|
|
543
|
+
this.write(`\x1b[${this.flowModeRenderedLines}A`);
|
|
544
|
+
this.write('\r'); // Go to column 1
|
|
545
|
+
}
|
|
546
|
+
// Save this position as our anchor point
|
|
547
|
+
this.write('\x1b7'); // Save cursor position (DEC)
|
|
548
|
+
const divider = renderDivider(cols - 2);
|
|
549
|
+
// Status bar
|
|
550
|
+
this.write(ESC.CLEAR_LINE);
|
|
551
|
+
this.write(this.buildStatusBar(cols));
|
|
552
|
+
this.write('\r\n');
|
|
553
|
+
// Top divider
|
|
554
|
+
this.write(ESC.CLEAR_LINE);
|
|
555
|
+
this.write(divider);
|
|
556
|
+
this.write('\r\n');
|
|
557
|
+
// Input lines
|
|
558
|
+
let cursorLineOffset = 2; // After status and top divider
|
|
559
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
560
|
+
this.write(ESC.CLEAR_LINE);
|
|
561
|
+
const line = visibleLines[i] ?? '';
|
|
562
|
+
const absoluteLineIdx = startLine + i;
|
|
563
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
564
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
565
|
+
// Background
|
|
566
|
+
this.write(ESC.BG_DARK);
|
|
567
|
+
// Prompt prefix
|
|
568
|
+
this.write(ESC.DIM);
|
|
569
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
570
|
+
this.write(ESC.RESET);
|
|
571
|
+
this.write(ESC.BG_DARK);
|
|
572
|
+
if (isCursorLine) {
|
|
573
|
+
cursorLineOffset = 2 + i; // Position within our rendered block
|
|
574
|
+
}
|
|
575
|
+
this.write(line);
|
|
576
|
+
// Pad to edge for clean look
|
|
577
|
+
const lineLen = this.config.promptChar.length + line.length;
|
|
578
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
579
|
+
if (padding > 0)
|
|
580
|
+
this.write(' '.repeat(padding));
|
|
581
|
+
this.write(ESC.RESET);
|
|
582
|
+
this.write('\r\n');
|
|
583
|
+
}
|
|
584
|
+
// Bottom divider
|
|
585
|
+
this.write(ESC.CLEAR_LINE);
|
|
586
|
+
this.write(divider);
|
|
587
|
+
this.write('\r\n');
|
|
588
|
+
// Mode controls
|
|
589
|
+
this.write(ESC.CLEAR_LINE);
|
|
590
|
+
this.write(this.buildModeControls(cols));
|
|
591
|
+
// Clear any leftover lines from previous renders with more input lines
|
|
592
|
+
if (this.flowModeRenderedLines > totalLines) {
|
|
593
|
+
const extraLines = this.flowModeRenderedLines - totalLines;
|
|
594
|
+
for (let i = 0; i < extraLines; i++) {
|
|
595
|
+
this.write('\r\n');
|
|
596
|
+
this.write(ESC.CLEAR_LINE);
|
|
597
|
+
}
|
|
538
598
|
}
|
|
539
|
-
|
|
599
|
+
// Remember how many lines we rendered
|
|
600
|
+
this.flowModeRenderedLines = totalLines;
|
|
601
|
+
// Restore cursor to anchor point, then move to input position
|
|
602
|
+
this.write('\x1b8'); // Restore cursor position (DEC)
|
|
603
|
+
// Move down to the input line and to cursor column
|
|
604
|
+
if (cursorLineOffset > 0) {
|
|
605
|
+
this.write(`\x1b[${cursorLineOffset}B`); // Move down
|
|
606
|
+
}
|
|
607
|
+
const col = Math.min(cursorCol, (visibleLines[adjustedCursorLine] ?? '').length);
|
|
608
|
+
const cursorColPos = this.config.promptChar.length + col + 1;
|
|
609
|
+
this.write(ESC.TO_COL(Math.min(cursorColPos, cols)));
|
|
610
|
+
this.write(ESC.SHOW);
|
|
611
|
+
// Update state
|
|
612
|
+
this.lastRenderContent = this.buffer;
|
|
613
|
+
this.lastRenderCursor = this.cursor;
|
|
540
614
|
}
|
|
541
615
|
/**
|
|
542
|
-
*
|
|
616
|
+
* Render in bottom-pinned mode - input area renders at absolute bottom
|
|
617
|
+
* Used when explicit bottom positioning is needed
|
|
543
618
|
*/
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
619
|
+
renderBottomPinned() {
|
|
620
|
+
const { rows, cols } = this.getSize();
|
|
621
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
622
|
+
// Wrap buffer into display lines
|
|
623
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
624
|
+
const availableForContent = Math.max(1, rows - 3);
|
|
625
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
626
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
627
|
+
// Reserved lines: separator(1) + controls(1) + input lines
|
|
628
|
+
this.updateReservedLines(displayLines + 2);
|
|
629
|
+
// Calculate display window (keep cursor visible)
|
|
630
|
+
let startLine = 0;
|
|
631
|
+
if (lines.length > displayLines) {
|
|
632
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
633
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
634
|
+
}
|
|
635
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
636
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
637
|
+
this.write(ESC.HIDE);
|
|
638
|
+
this.write(ESC.RESET);
|
|
639
|
+
// Calculate positions from absolute bottom
|
|
640
|
+
const modeControlRow = rows;
|
|
641
|
+
const bottomSepRow = rows - 1;
|
|
642
|
+
const inputEndRow = rows - 2;
|
|
643
|
+
const inputStartRow = inputEndRow - visibleLines.length + 1;
|
|
644
|
+
const topSepRow = inputStartRow - 1;
|
|
645
|
+
const statusBarRow = topSepRow - 1;
|
|
646
|
+
// Reserved lines: status(1) + topSep(1) + input + bottomSep(1) + controls(1)
|
|
647
|
+
this.updateReservedLines(visibleLines.length + 4);
|
|
648
|
+
// Status bar
|
|
649
|
+
this.write(ESC.TO(statusBarRow, 1));
|
|
650
|
+
this.write(ESC.CLEAR_LINE);
|
|
651
|
+
this.write(this.buildStatusBar(cols));
|
|
652
|
+
// Top separator
|
|
653
|
+
this.write(ESC.TO(topSepRow, 1));
|
|
654
|
+
this.write(ESC.CLEAR_LINE);
|
|
655
|
+
const divider = renderDivider(cols - 2);
|
|
656
|
+
this.write(divider);
|
|
657
|
+
// Render input lines
|
|
658
|
+
let finalRow = inputStartRow;
|
|
659
|
+
let finalCol = 3;
|
|
660
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
661
|
+
const rowNum = inputStartRow + i;
|
|
662
|
+
this.write(ESC.TO(rowNum, 1));
|
|
663
|
+
this.write(ESC.CLEAR_LINE);
|
|
664
|
+
const line = visibleLines[i] ?? '';
|
|
665
|
+
const absoluteLineIdx = startLine + i;
|
|
666
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
667
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
668
|
+
// Background
|
|
669
|
+
this.write(ESC.BG_DARK);
|
|
670
|
+
// Prompt prefix
|
|
671
|
+
this.write(ESC.DIM);
|
|
672
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
673
|
+
this.write(ESC.RESET);
|
|
674
|
+
this.write(ESC.BG_DARK);
|
|
675
|
+
if (isCursorLine) {
|
|
676
|
+
const col = Math.min(cursorCol, line.length);
|
|
677
|
+
const before = line.slice(0, col);
|
|
678
|
+
const at = col < line.length ? line[col] : ' ';
|
|
679
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
680
|
+
this.write(before);
|
|
681
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
682
|
+
this.write(at);
|
|
683
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
684
|
+
this.write(after);
|
|
685
|
+
finalRow = rowNum;
|
|
686
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
this.write(line);
|
|
690
|
+
}
|
|
691
|
+
// Pad to edge
|
|
692
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
693
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
694
|
+
if (padding > 0)
|
|
695
|
+
this.write(' '.repeat(padding));
|
|
696
|
+
this.write(ESC.RESET);
|
|
550
697
|
}
|
|
698
|
+
// Bottom separator
|
|
699
|
+
this.write(ESC.TO(bottomSepRow, 1));
|
|
700
|
+
this.write(ESC.CLEAR_LINE);
|
|
701
|
+
this.write(divider);
|
|
702
|
+
// Mode controls
|
|
703
|
+
this.write(ESC.TO(modeControlRow, 1));
|
|
704
|
+
this.write(ESC.CLEAR_LINE);
|
|
705
|
+
this.write(this.buildModeControls(cols));
|
|
706
|
+
// Position cursor in input area
|
|
707
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
708
|
+
this.write(ESC.SHOW);
|
|
709
|
+
// Update state
|
|
710
|
+
this.lastRenderContent = this.buffer;
|
|
711
|
+
this.lastRenderCursor = this.cursor;
|
|
551
712
|
}
|
|
552
713
|
/**
|
|
553
|
-
* Build
|
|
554
|
-
*
|
|
714
|
+
* Build status bar showing streaming/ready status, elapsed time, and token count.
|
|
715
|
+
* This is the TOP line above the input area.
|
|
555
716
|
*/
|
|
556
|
-
|
|
717
|
+
buildStatusBar(cols) {
|
|
557
718
|
const parts = [];
|
|
558
|
-
|
|
559
|
-
|
|
719
|
+
// Streaming/Ready status with elapsed time
|
|
720
|
+
if (this.mode === 'streaming') {
|
|
721
|
+
let statusText = '● Streaming';
|
|
722
|
+
if (this.streamingStartTime) {
|
|
723
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
724
|
+
const mins = Math.floor(elapsed / 60);
|
|
725
|
+
const secs = elapsed % 60;
|
|
726
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
727
|
+
}
|
|
728
|
+
parts.push({ text: statusText, tone: 'success' });
|
|
560
729
|
}
|
|
561
|
-
|
|
562
|
-
parts.push({ text:
|
|
730
|
+
else {
|
|
731
|
+
parts.push({ text: '○ Ready', tone: 'muted' });
|
|
732
|
+
}
|
|
733
|
+
// Token count (context usage)
|
|
734
|
+
if (this.tokensUsed > 0) {
|
|
735
|
+
const tokenStr = this.tokensUsed >= 1000
|
|
736
|
+
? `${(this.tokensUsed / 1000).toFixed(1)}k`
|
|
737
|
+
: `${this.tokensUsed}`;
|
|
738
|
+
parts.push({ text: `${tokenStr} tokens`, tone: 'info' });
|
|
563
739
|
}
|
|
564
|
-
|
|
565
|
-
|
|
740
|
+
// Context window remaining
|
|
741
|
+
if (this.contextUsage !== null) {
|
|
742
|
+
const pct = Math.max(0, 100 - this.contextUsage);
|
|
743
|
+
parts.push({ text: `ctx ${pct}%`, tone: pct < 25 ? 'warn' : 'muted' });
|
|
744
|
+
}
|
|
745
|
+
// Paste indicator
|
|
746
|
+
if (this.pastePlaceholders.length > 0) {
|
|
747
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
748
|
+
parts.push({ text: `📋 ${totalLines}L`, tone: 'info' });
|
|
566
749
|
}
|
|
567
|
-
|
|
750
|
+
return renderStatusLine(parts, cols - 2);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Build mode controls line showing toggles and shortcuts.
|
|
754
|
+
* This is the BOTTOM line below the input area.
|
|
755
|
+
*/
|
|
756
|
+
buildModeControls(cols) {
|
|
757
|
+
const parts = [];
|
|
758
|
+
// Thinking mode toggle
|
|
759
|
+
parts.push({
|
|
760
|
+
text: this.thinkingEnabled ? '💭 on (tab)' : '💭 off (tab)',
|
|
761
|
+
tone: this.thinkingEnabled ? 'info' : 'muted',
|
|
762
|
+
});
|
|
763
|
+
// Verification toggle
|
|
568
764
|
parts.push({
|
|
569
|
-
text:
|
|
765
|
+
text: this.verificationEnabled ? `✓ verify (${this.verificationHotkey})` : `✗ verify (${this.verificationHotkey})`,
|
|
570
766
|
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
571
767
|
});
|
|
572
|
-
|
|
768
|
+
// Edit mode
|
|
573
769
|
parts.push({
|
|
574
|
-
text:
|
|
575
|
-
tone:
|
|
770
|
+
text: this.editMode === 'display-edits' ? 'auto-edit (⇧⇥)' : 'ask-first (⇧⇥)',
|
|
771
|
+
tone: 'muted',
|
|
576
772
|
});
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const pct = Math.max(0, 100 - this.contextUsage);
|
|
581
|
-
const tone = pct < 25 ? 'warn' : 'muted';
|
|
582
|
-
parts.push({ text: `context ${pct}%`, tone });
|
|
773
|
+
// Override/warning status
|
|
774
|
+
if (this.overrideStatusMessage) {
|
|
775
|
+
parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
583
776
|
}
|
|
584
|
-
|
|
585
|
-
|
|
777
|
+
// Queue indicator during streaming
|
|
778
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
779
|
+
parts.push({ text: `queued: ${this.queue.length}`, tone: 'info' });
|
|
586
780
|
}
|
|
781
|
+
// Multi-line indicator
|
|
587
782
|
if (this.buffer.includes('\n')) {
|
|
588
|
-
|
|
589
|
-
parts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
590
|
-
}
|
|
591
|
-
if (this.pastePlaceholders.length > 0) {
|
|
592
|
-
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
593
|
-
parts.push({
|
|
594
|
-
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
595
|
-
tone: 'info',
|
|
596
|
-
});
|
|
783
|
+
parts.push({ text: `${this.buffer.split('\n').length}L`, tone: 'muted' });
|
|
597
784
|
}
|
|
785
|
+
// Shortcuts hint (at the end)
|
|
786
|
+
parts.push({ text: '? · esc', tone: 'muted' });
|
|
598
787
|
return renderStatusLine(parts, cols - 2);
|
|
599
788
|
}
|
|
600
789
|
/**
|
|
@@ -630,6 +819,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
630
819
|
* Register with display's output interceptor to position cursor correctly.
|
|
631
820
|
* When scroll region is active, output needs to go to the scroll region,
|
|
632
821
|
* not the protected bottom area where the input is rendered.
|
|
822
|
+
*
|
|
823
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
824
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
633
825
|
*/
|
|
634
826
|
registerOutputInterceptor(display) {
|
|
635
827
|
if (this.outputInterceptorCleanup) {
|
|
@@ -637,20 +829,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
637
829
|
}
|
|
638
830
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
639
831
|
beforeWrite: () => {
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
if (this.scrollRegionActive) {
|
|
643
|
-
const { rows } = this.getSize();
|
|
644
|
-
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
645
|
-
this.write(ESC.SAVE);
|
|
646
|
-
this.write(ESC.TO(scrollBottom, 1));
|
|
647
|
-
}
|
|
832
|
+
// Scroll region handles content containment automatically
|
|
833
|
+
// No per-write cursor manipulation needed
|
|
648
834
|
},
|
|
649
835
|
afterWrite: () => {
|
|
650
|
-
//
|
|
651
|
-
if (this.scrollRegionActive) {
|
|
652
|
-
this.write(ESC.RESTORE);
|
|
653
|
-
}
|
|
836
|
+
// No cursor manipulation needed
|
|
654
837
|
},
|
|
655
838
|
});
|
|
656
839
|
}
|
|
@@ -772,7 +955,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
772
955
|
this.toggleEditMode();
|
|
773
956
|
return true;
|
|
774
957
|
}
|
|
775
|
-
|
|
958
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
959
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
960
|
+
this.togglePasteExpansion();
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
this.toggleThinking();
|
|
964
|
+
}
|
|
965
|
+
return true;
|
|
966
|
+
case 'escape':
|
|
967
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
968
|
+
if (this.mode === 'streaming') {
|
|
969
|
+
this.emit('interrupt');
|
|
970
|
+
}
|
|
971
|
+
else if (this.buffer.length > 0) {
|
|
972
|
+
this.clear();
|
|
973
|
+
}
|
|
776
974
|
return true;
|
|
777
975
|
}
|
|
778
976
|
return false;
|
|
@@ -1063,9 +1261,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1063
1261
|
if (available <= 0)
|
|
1064
1262
|
return;
|
|
1065
1263
|
const chunk = clean.slice(0, available);
|
|
1066
|
-
|
|
1067
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1068
|
-
if (isMultiline && !isShortMultiline) {
|
|
1264
|
+
if (isMultilinePaste(chunk)) {
|
|
1069
1265
|
this.insertPastePlaceholder(chunk);
|
|
1070
1266
|
}
|
|
1071
1267
|
else {
|
|
@@ -1085,7 +1281,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1085
1281
|
return;
|
|
1086
1282
|
this.applyScrollRegion();
|
|
1087
1283
|
this.scrollRegionActive = true;
|
|
1088
|
-
this.forceRender();
|
|
1089
1284
|
}
|
|
1090
1285
|
disableScrollRegion() {
|
|
1091
1286
|
if (!this.scrollRegionActive)
|
|
@@ -1236,19 +1431,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1236
1431
|
this.shiftPlaceholders(position, text.length);
|
|
1237
1432
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1238
1433
|
}
|
|
1239
|
-
shouldInlineMultiline(content) {
|
|
1240
|
-
const lines = content.split('\n').length;
|
|
1241
|
-
const maxInlineLines = 4;
|
|
1242
|
-
const maxInlineChars = 240;
|
|
1243
|
-
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1244
|
-
}
|
|
1245
1434
|
findPlaceholderAt(position) {
|
|
1246
1435
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1247
1436
|
}
|
|
1248
|
-
buildPlaceholder(
|
|
1437
|
+
buildPlaceholder(summary) {
|
|
1249
1438
|
const id = ++this.pasteCounter;
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1439
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1440
|
+
// Show first line preview (truncated)
|
|
1441
|
+
const preview = summary.preview.length > 30
|
|
1442
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1443
|
+
: summary.preview;
|
|
1444
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1252
1445
|
return { id, placeholder };
|
|
1253
1446
|
}
|
|
1254
1447
|
insertPastePlaceholder(content) {
|
|
@@ -1256,21 +1449,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1256
1449
|
if (available <= 0)
|
|
1257
1450
|
return;
|
|
1258
1451
|
const cleanContent = content.slice(0, available);
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1452
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1453
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1454
|
+
if (summary.lineCount < 5) {
|
|
1455
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1456
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1457
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1458
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1261
1462
|
const insertPos = this.cursor;
|
|
1262
1463
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1263
1464
|
this.pastePlaceholders.push({
|
|
1264
1465
|
id,
|
|
1265
1466
|
content: cleanContent,
|
|
1266
|
-
lineCount,
|
|
1467
|
+
lineCount: summary.lineCount,
|
|
1267
1468
|
placeholder,
|
|
1268
1469
|
start: insertPos,
|
|
1269
1470
|
end: insertPos + placeholder.length,
|
|
1471
|
+
summary,
|
|
1472
|
+
expanded: false,
|
|
1270
1473
|
});
|
|
1271
1474
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1272
1475
|
this.cursor = insertPos + placeholder.length;
|
|
1273
1476
|
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1479
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1480
|
+
*/
|
|
1481
|
+
togglePasteExpansion() {
|
|
1482
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1483
|
+
if (!placeholder)
|
|
1484
|
+
return false;
|
|
1485
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1486
|
+
// Update the placeholder text in buffer
|
|
1487
|
+
const newPlaceholder = placeholder.expanded
|
|
1488
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1489
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1490
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1491
|
+
// Update buffer
|
|
1492
|
+
this.buffer =
|
|
1493
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1494
|
+
newPlaceholder +
|
|
1495
|
+
this.buffer.slice(placeholder.end);
|
|
1496
|
+
// Update placeholder tracking
|
|
1497
|
+
placeholder.placeholder = newPlaceholder;
|
|
1498
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1499
|
+
// Shift other placeholders
|
|
1500
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1501
|
+
this.scheduleRender();
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
buildExpandedPlaceholder(ph) {
|
|
1505
|
+
const lines = ph.content.split('\n');
|
|
1506
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1507
|
+
const lastLines = lines.length > 5
|
|
1508
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1509
|
+
: '';
|
|
1510
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1511
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1512
|
+
}
|
|
1274
1513
|
deletePlaceholder(placeholder) {
|
|
1275
1514
|
const length = placeholder.end - placeholder.start;
|
|
1276
1515
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|