erosolar-cli 1.7.231 → 1.7.232
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 +65 -21
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +460 -209
- 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,290 @@ 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
|
|
522
517
|
*/
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
518
|
+
renderFlowMode() {
|
|
519
|
+
const { cols } = this.getSize();
|
|
520
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
521
|
+
// Wrap buffer into display lines
|
|
522
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
523
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, 10));
|
|
524
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
525
|
+
// Calculate display window (keep cursor visible)
|
|
526
|
+
let startLine = 0;
|
|
527
|
+
if (lines.length > displayLines) {
|
|
528
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
529
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
530
|
+
}
|
|
531
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
532
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
533
|
+
this.write(ESC.HIDE);
|
|
534
|
+
this.write(ESC.RESET);
|
|
535
|
+
// If we've previously rendered in flow mode, clear those lines first
|
|
536
|
+
if (this.flowModeRenderedLines > 0) {
|
|
537
|
+
// Move up to the start of our rendered content
|
|
538
|
+
this.write(`\x1b[${this.flowModeRenderedLines}A`);
|
|
539
|
+
// Clear each line we previously rendered
|
|
540
|
+
for (let i = 0; i < this.flowModeRenderedLines; i++) {
|
|
541
|
+
this.write(ESC.CLEAR_LINE);
|
|
542
|
+
if (i < this.flowModeRenderedLines - 1) {
|
|
543
|
+
this.write('\x1b[1B'); // Move down
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Move back up to where we'll start rendering
|
|
547
|
+
if (this.flowModeRenderedLines > 1) {
|
|
548
|
+
this.write(`\x1b[${this.flowModeRenderedLines - 1}A`);
|
|
549
|
+
}
|
|
535
550
|
}
|
|
536
|
-
|
|
537
|
-
|
|
551
|
+
// Track total lines we'll render: status(1) + topDiv(1) + input(N) + bottomDiv(1) + controls(1)
|
|
552
|
+
const totalLinesToRender = visibleLines.length + 4;
|
|
553
|
+
// Flow layout: render inline from current position
|
|
554
|
+
// [status bar] [divider] [input...] [divider] [controls]
|
|
555
|
+
// Status bar
|
|
556
|
+
this.write(ESC.CLEAR_LINE);
|
|
557
|
+
this.write(this.buildStatusBar(cols));
|
|
558
|
+
// Top divider
|
|
559
|
+
this.write('\n');
|
|
560
|
+
this.write(ESC.CLEAR_LINE);
|
|
561
|
+
const divider = renderDivider(cols - 2);
|
|
562
|
+
this.write(divider);
|
|
563
|
+
// Input lines
|
|
564
|
+
let cursorRow = 0;
|
|
565
|
+
let cursorColPos = 3;
|
|
566
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
567
|
+
this.write('\n');
|
|
568
|
+
this.write(ESC.CLEAR_LINE);
|
|
569
|
+
const line = visibleLines[i] ?? '';
|
|
570
|
+
const absoluteLineIdx = startLine + i;
|
|
571
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
572
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
573
|
+
// Background
|
|
574
|
+
this.write(ESC.BG_DARK);
|
|
575
|
+
// Prompt prefix
|
|
576
|
+
this.write(ESC.DIM);
|
|
577
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
578
|
+
this.write(ESC.RESET);
|
|
579
|
+
this.write(ESC.BG_DARK);
|
|
580
|
+
if (isCursorLine) {
|
|
581
|
+
// Render with block cursor
|
|
582
|
+
const col = Math.min(cursorCol, line.length);
|
|
583
|
+
const before = line.slice(0, col);
|
|
584
|
+
const at = col < line.length ? line[col] : ' ';
|
|
585
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
586
|
+
this.write(before);
|
|
587
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
588
|
+
this.write(at);
|
|
589
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
590
|
+
this.write(after);
|
|
591
|
+
cursorRow = i;
|
|
592
|
+
cursorColPos = this.config.promptChar.length + col + 1;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
this.write(line);
|
|
596
|
+
}
|
|
597
|
+
// Pad to edge
|
|
598
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
599
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
600
|
+
if (padding > 0)
|
|
601
|
+
this.write(' '.repeat(padding));
|
|
602
|
+
this.write(ESC.RESET);
|
|
538
603
|
}
|
|
539
|
-
|
|
604
|
+
// Bottom divider
|
|
605
|
+
this.write('\n');
|
|
606
|
+
this.write(ESC.CLEAR_LINE);
|
|
607
|
+
this.write(divider);
|
|
608
|
+
// Mode controls
|
|
609
|
+
this.write('\n');
|
|
610
|
+
this.write(ESC.CLEAR_LINE);
|
|
611
|
+
this.write(this.buildModeControls(cols));
|
|
612
|
+
// Remember how many lines we rendered for next re-render
|
|
613
|
+
this.flowModeRenderedLines = totalLinesToRender;
|
|
614
|
+
// Move cursor back to input area
|
|
615
|
+
// We're now at the last line (controls), need to go up to the input line
|
|
616
|
+
// Input starts at line 2 (after status and top divider), cursor is at cursorRow within input
|
|
617
|
+
const linesToGoUp = (visibleLines.length - cursorRow - 1) + 2; // +2 for bottom divider and controls
|
|
618
|
+
if (linesToGoUp > 0) {
|
|
619
|
+
this.write(`\x1b[${linesToGoUp}A`); // Move up
|
|
620
|
+
}
|
|
621
|
+
this.write(ESC.TO_COL(Math.min(cursorColPos, cols)));
|
|
622
|
+
this.write(ESC.SHOW);
|
|
623
|
+
// Update state
|
|
624
|
+
this.lastRenderContent = this.buffer;
|
|
625
|
+
this.lastRenderCursor = this.cursor;
|
|
540
626
|
}
|
|
541
627
|
/**
|
|
542
|
-
*
|
|
628
|
+
* Render in bottom-pinned mode - input area renders at absolute bottom
|
|
629
|
+
* Used when explicit bottom positioning is needed
|
|
543
630
|
*/
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
631
|
+
renderBottomPinned() {
|
|
632
|
+
const { rows, cols } = this.getSize();
|
|
633
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
634
|
+
// Wrap buffer into display lines
|
|
635
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
636
|
+
const availableForContent = Math.max(1, rows - 3);
|
|
637
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
638
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
639
|
+
// Reserved lines: separator(1) + controls(1) + input lines
|
|
640
|
+
this.updateReservedLines(displayLines + 2);
|
|
641
|
+
// Calculate display window (keep cursor visible)
|
|
642
|
+
let startLine = 0;
|
|
643
|
+
if (lines.length > displayLines) {
|
|
644
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
645
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
646
|
+
}
|
|
647
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
648
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
649
|
+
this.write(ESC.HIDE);
|
|
650
|
+
this.write(ESC.RESET);
|
|
651
|
+
// Calculate positions from absolute bottom
|
|
652
|
+
const modeControlRow = rows;
|
|
653
|
+
const bottomSepRow = rows - 1;
|
|
654
|
+
const inputEndRow = rows - 2;
|
|
655
|
+
const inputStartRow = inputEndRow - visibleLines.length + 1;
|
|
656
|
+
const topSepRow = inputStartRow - 1;
|
|
657
|
+
const statusBarRow = topSepRow - 1;
|
|
658
|
+
// Reserved lines: status(1) + topSep(1) + input + bottomSep(1) + controls(1)
|
|
659
|
+
this.updateReservedLines(visibleLines.length + 4);
|
|
660
|
+
// Status bar
|
|
661
|
+
this.write(ESC.TO(statusBarRow, 1));
|
|
662
|
+
this.write(ESC.CLEAR_LINE);
|
|
663
|
+
this.write(this.buildStatusBar(cols));
|
|
664
|
+
// Top separator
|
|
665
|
+
this.write(ESC.TO(topSepRow, 1));
|
|
666
|
+
this.write(ESC.CLEAR_LINE);
|
|
667
|
+
const divider = renderDivider(cols - 2);
|
|
668
|
+
this.write(divider);
|
|
669
|
+
// Render input lines
|
|
670
|
+
let finalRow = inputStartRow;
|
|
671
|
+
let finalCol = 3;
|
|
672
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
673
|
+
const rowNum = inputStartRow + i;
|
|
674
|
+
this.write(ESC.TO(rowNum, 1));
|
|
675
|
+
this.write(ESC.CLEAR_LINE);
|
|
676
|
+
const line = visibleLines[i] ?? '';
|
|
677
|
+
const absoluteLineIdx = startLine + i;
|
|
678
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
679
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
680
|
+
// Background
|
|
681
|
+
this.write(ESC.BG_DARK);
|
|
682
|
+
// Prompt prefix
|
|
683
|
+
this.write(ESC.DIM);
|
|
684
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
685
|
+
this.write(ESC.RESET);
|
|
686
|
+
this.write(ESC.BG_DARK);
|
|
687
|
+
if (isCursorLine) {
|
|
688
|
+
const col = Math.min(cursorCol, line.length);
|
|
689
|
+
const before = line.slice(0, col);
|
|
690
|
+
const at = col < line.length ? line[col] : ' ';
|
|
691
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
692
|
+
this.write(before);
|
|
693
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
694
|
+
this.write(at);
|
|
695
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
696
|
+
this.write(after);
|
|
697
|
+
finalRow = rowNum;
|
|
698
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
this.write(line);
|
|
702
|
+
}
|
|
703
|
+
// Pad to edge
|
|
704
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
705
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
706
|
+
if (padding > 0)
|
|
707
|
+
this.write(' '.repeat(padding));
|
|
708
|
+
this.write(ESC.RESET);
|
|
550
709
|
}
|
|
710
|
+
// Bottom separator
|
|
711
|
+
this.write(ESC.TO(bottomSepRow, 1));
|
|
712
|
+
this.write(ESC.CLEAR_LINE);
|
|
713
|
+
this.write(divider);
|
|
714
|
+
// Mode controls
|
|
715
|
+
this.write(ESC.TO(modeControlRow, 1));
|
|
716
|
+
this.write(ESC.CLEAR_LINE);
|
|
717
|
+
this.write(this.buildModeControls(cols));
|
|
718
|
+
// Position cursor in input area
|
|
719
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
720
|
+
this.write(ESC.SHOW);
|
|
721
|
+
// Update state
|
|
722
|
+
this.lastRenderContent = this.buffer;
|
|
723
|
+
this.lastRenderCursor = this.cursor;
|
|
551
724
|
}
|
|
552
725
|
/**
|
|
553
|
-
* Build
|
|
554
|
-
*
|
|
726
|
+
* Build status bar showing streaming/ready status, elapsed time, and token count.
|
|
727
|
+
* This is the TOP line above the input area.
|
|
555
728
|
*/
|
|
556
|
-
|
|
729
|
+
buildStatusBar(cols) {
|
|
557
730
|
const parts = [];
|
|
558
|
-
|
|
559
|
-
|
|
731
|
+
// Streaming/Ready status with elapsed time
|
|
732
|
+
if (this.mode === 'streaming') {
|
|
733
|
+
let statusText = '● Streaming';
|
|
734
|
+
if (this.streamingStartTime) {
|
|
735
|
+
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
736
|
+
const mins = Math.floor(elapsed / 60);
|
|
737
|
+
const secs = elapsed % 60;
|
|
738
|
+
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
739
|
+
}
|
|
740
|
+
parts.push({ text: statusText, tone: 'success' });
|
|
560
741
|
}
|
|
561
|
-
|
|
562
|
-
parts.push({ text:
|
|
742
|
+
else {
|
|
743
|
+
parts.push({ text: '○ Ready', tone: 'muted' });
|
|
563
744
|
}
|
|
564
|
-
|
|
565
|
-
|
|
745
|
+
// Token count (context usage)
|
|
746
|
+
if (this.tokensUsed > 0) {
|
|
747
|
+
const tokenStr = this.tokensUsed >= 1000
|
|
748
|
+
? `${(this.tokensUsed / 1000).toFixed(1)}k`
|
|
749
|
+
: `${this.tokensUsed}`;
|
|
750
|
+
parts.push({ text: `${tokenStr} tokens`, tone: 'info' });
|
|
566
751
|
}
|
|
567
|
-
|
|
752
|
+
// Context window remaining
|
|
753
|
+
if (this.contextUsage !== null) {
|
|
754
|
+
const pct = Math.max(0, 100 - this.contextUsage);
|
|
755
|
+
parts.push({ text: `ctx ${pct}%`, tone: pct < 25 ? 'warn' : 'muted' });
|
|
756
|
+
}
|
|
757
|
+
// Paste indicator
|
|
758
|
+
if (this.pastePlaceholders.length > 0) {
|
|
759
|
+
const totalLines = this.pastePlaceholders.reduce((sum, p) => sum + p.lineCount, 0);
|
|
760
|
+
parts.push({ text: `📋 ${totalLines}L`, tone: 'info' });
|
|
761
|
+
}
|
|
762
|
+
return renderStatusLine(parts, cols - 2);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Build mode controls line showing toggles and shortcuts.
|
|
766
|
+
* This is the BOTTOM line below the input area.
|
|
767
|
+
*/
|
|
768
|
+
buildModeControls(cols) {
|
|
769
|
+
const parts = [];
|
|
770
|
+
// Thinking mode toggle
|
|
771
|
+
parts.push({
|
|
772
|
+
text: this.thinkingEnabled ? '💭 on (tab)' : '💭 off (tab)',
|
|
773
|
+
tone: this.thinkingEnabled ? 'info' : 'muted',
|
|
774
|
+
});
|
|
775
|
+
// Verification toggle
|
|
568
776
|
parts.push({
|
|
569
|
-
text:
|
|
777
|
+
text: this.verificationEnabled ? `✓ verify (${this.verificationHotkey})` : `✗ verify (${this.verificationHotkey})`,
|
|
570
778
|
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
571
779
|
});
|
|
572
|
-
|
|
780
|
+
// Edit mode
|
|
573
781
|
parts.push({
|
|
574
|
-
text:
|
|
575
|
-
tone:
|
|
782
|
+
text: this.editMode === 'display-edits' ? 'auto-edit (⇧⇥)' : 'ask-first (⇧⇥)',
|
|
783
|
+
tone: 'muted',
|
|
576
784
|
});
|
|
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 });
|
|
785
|
+
// Override/warning status
|
|
786
|
+
if (this.overrideStatusMessage) {
|
|
787
|
+
parts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
583
788
|
}
|
|
584
|
-
|
|
585
|
-
|
|
789
|
+
// Queue indicator during streaming
|
|
790
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
791
|
+
parts.push({ text: `queued: ${this.queue.length}`, tone: 'info' });
|
|
586
792
|
}
|
|
793
|
+
// Multi-line indicator
|
|
587
794
|
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
|
-
});
|
|
795
|
+
parts.push({ text: `${this.buffer.split('\n').length}L`, tone: 'muted' });
|
|
597
796
|
}
|
|
797
|
+
// Shortcuts hint (at the end)
|
|
798
|
+
parts.push({ text: '? · esc', tone: 'muted' });
|
|
598
799
|
return renderStatusLine(parts, cols - 2);
|
|
599
800
|
}
|
|
600
801
|
/**
|
|
@@ -630,6 +831,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
630
831
|
* Register with display's output interceptor to position cursor correctly.
|
|
631
832
|
* When scroll region is active, output needs to go to the scroll region,
|
|
632
833
|
* not the protected bottom area where the input is rendered.
|
|
834
|
+
*
|
|
835
|
+
* NOTE: With scroll region properly set, content naturally stays within
|
|
836
|
+
* the region boundaries - no cursor manipulation needed per-write.
|
|
633
837
|
*/
|
|
634
838
|
registerOutputInterceptor(display) {
|
|
635
839
|
if (this.outputInterceptorCleanup) {
|
|
@@ -637,20 +841,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
637
841
|
}
|
|
638
842
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
639
843
|
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
|
-
}
|
|
844
|
+
// Scroll region handles content containment automatically
|
|
845
|
+
// No per-write cursor manipulation needed
|
|
648
846
|
},
|
|
649
847
|
afterWrite: () => {
|
|
650
|
-
//
|
|
651
|
-
if (this.scrollRegionActive) {
|
|
652
|
-
this.write(ESC.RESTORE);
|
|
653
|
-
}
|
|
848
|
+
// No cursor manipulation needed
|
|
654
849
|
},
|
|
655
850
|
});
|
|
656
851
|
}
|
|
@@ -772,7 +967,22 @@ export class TerminalInput extends EventEmitter {
|
|
|
772
967
|
this.toggleEditMode();
|
|
773
968
|
return true;
|
|
774
969
|
}
|
|
775
|
-
|
|
970
|
+
// Tab: toggle paste expansion if in placeholder, otherwise toggle thinking
|
|
971
|
+
if (this.findPlaceholderAt(this.cursor)) {
|
|
972
|
+
this.togglePasteExpansion();
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
this.toggleThinking();
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
case 'escape':
|
|
979
|
+
// Esc: interrupt if streaming, otherwise clear buffer
|
|
980
|
+
if (this.mode === 'streaming') {
|
|
981
|
+
this.emit('interrupt');
|
|
982
|
+
}
|
|
983
|
+
else if (this.buffer.length > 0) {
|
|
984
|
+
this.clear();
|
|
985
|
+
}
|
|
776
986
|
return true;
|
|
777
987
|
}
|
|
778
988
|
return false;
|
|
@@ -1063,9 +1273,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1063
1273
|
if (available <= 0)
|
|
1064
1274
|
return;
|
|
1065
1275
|
const chunk = clean.slice(0, available);
|
|
1066
|
-
|
|
1067
|
-
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1068
|
-
if (isMultiline && !isShortMultiline) {
|
|
1276
|
+
if (isMultilinePaste(chunk)) {
|
|
1069
1277
|
this.insertPastePlaceholder(chunk);
|
|
1070
1278
|
}
|
|
1071
1279
|
else {
|
|
@@ -1085,7 +1293,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1085
1293
|
return;
|
|
1086
1294
|
this.applyScrollRegion();
|
|
1087
1295
|
this.scrollRegionActive = true;
|
|
1088
|
-
this.forceRender();
|
|
1089
1296
|
}
|
|
1090
1297
|
disableScrollRegion() {
|
|
1091
1298
|
if (!this.scrollRegionActive)
|
|
@@ -1236,19 +1443,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1236
1443
|
this.shiftPlaceholders(position, text.length);
|
|
1237
1444
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1238
1445
|
}
|
|
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
1446
|
findPlaceholderAt(position) {
|
|
1246
1447
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1247
1448
|
}
|
|
1248
|
-
buildPlaceholder(
|
|
1449
|
+
buildPlaceholder(summary) {
|
|
1249
1450
|
const id = ++this.pasteCounter;
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1451
|
+
const lang = summary.language ? ` ${summary.language.toUpperCase()}` : '';
|
|
1452
|
+
// Show first line preview (truncated)
|
|
1453
|
+
const preview = summary.preview.length > 30
|
|
1454
|
+
? `${summary.preview.slice(0, 30)}...`
|
|
1455
|
+
: summary.preview;
|
|
1456
|
+
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1252
1457
|
return { id, placeholder };
|
|
1253
1458
|
}
|
|
1254
1459
|
insertPastePlaceholder(content) {
|
|
@@ -1256,21 +1461,67 @@ export class TerminalInput extends EventEmitter {
|
|
|
1256
1461
|
if (available <= 0)
|
|
1257
1462
|
return;
|
|
1258
1463
|
const cleanContent = content.slice(0, available);
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1464
|
+
const summary = generatePasteSummary(cleanContent);
|
|
1465
|
+
// For short pastes (< 5 lines), show full content instead of placeholder
|
|
1466
|
+
if (summary.lineCount < 5) {
|
|
1467
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1468
|
+
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1469
|
+
this.insertPlainText(cleanContent, insertPos);
|
|
1470
|
+
this.cursor = insertPos + cleanContent.length;
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1261
1474
|
const insertPos = this.cursor;
|
|
1262
1475
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1263
1476
|
this.pastePlaceholders.push({
|
|
1264
1477
|
id,
|
|
1265
1478
|
content: cleanContent,
|
|
1266
|
-
lineCount,
|
|
1479
|
+
lineCount: summary.lineCount,
|
|
1267
1480
|
placeholder,
|
|
1268
1481
|
start: insertPos,
|
|
1269
1482
|
end: insertPos + placeholder.length,
|
|
1483
|
+
summary,
|
|
1484
|
+
expanded: false,
|
|
1270
1485
|
});
|
|
1271
1486
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1272
1487
|
this.cursor = insertPos + placeholder.length;
|
|
1273
1488
|
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1491
|
+
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1492
|
+
*/
|
|
1493
|
+
togglePasteExpansion() {
|
|
1494
|
+
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1495
|
+
if (!placeholder)
|
|
1496
|
+
return false;
|
|
1497
|
+
placeholder.expanded = !placeholder.expanded;
|
|
1498
|
+
// Update the placeholder text in buffer
|
|
1499
|
+
const newPlaceholder = placeholder.expanded
|
|
1500
|
+
? this.buildExpandedPlaceholder(placeholder)
|
|
1501
|
+
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1502
|
+
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1503
|
+
// Update buffer
|
|
1504
|
+
this.buffer =
|
|
1505
|
+
this.buffer.slice(0, placeholder.start) +
|
|
1506
|
+
newPlaceholder +
|
|
1507
|
+
this.buffer.slice(placeholder.end);
|
|
1508
|
+
// Update placeholder tracking
|
|
1509
|
+
placeholder.placeholder = newPlaceholder;
|
|
1510
|
+
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1511
|
+
// Shift other placeholders
|
|
1512
|
+
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1513
|
+
this.scheduleRender();
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
buildExpandedPlaceholder(ph) {
|
|
1517
|
+
const lines = ph.content.split('\n');
|
|
1518
|
+
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1519
|
+
const lastLines = lines.length > 5
|
|
1520
|
+
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1521
|
+
: '';
|
|
1522
|
+
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1523
|
+
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1524
|
+
}
|
|
1274
1525
|
deletePlaceholder(placeholder) {
|
|
1275
1526
|
const length = placeholder.end - placeholder.start;
|
|
1276
1527
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|