erosolar-cli 1.7.243 → 1.7.245
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/core/customCommands.d.ts +1 -0
- package/dist/core/customCommands.d.ts.map +1 -1
- package/dist/core/customCommands.js +3 -0
- package/dist/core/customCommands.js.map +1 -1
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +0 -14
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +0 -5
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts.map +1 -1
- package/dist/core/toolValidation.js +14 -3
- package/dist/core/toolValidation.js.map +1 -1
- 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 +9 -2
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +133 -17
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +46 -117
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +312 -541
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +12 -15
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +8 -22
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +19 -0
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +108 -0
- package/dist/ui/display.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 +1 -0
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +52 -25
- 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,16 +3,18 @@
|
|
|
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
|
-
import { renderDivider } from '../ui/unified/layout.js';
|
|
15
|
+
import { renderDivider, renderStatusLine } from '../ui/unified/layout.js';
|
|
15
16
|
import { isStreamingMode } from '../ui/globalWriteLock.js';
|
|
17
|
+
import { formatThinking } from '../ui/toolDisplay.js';
|
|
16
18
|
// ANSI escape codes
|
|
17
19
|
const ESC = {
|
|
18
20
|
// Cursor control
|
|
@@ -55,8 +57,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
55
57
|
isPasting = false;
|
|
56
58
|
pastePlaceholders = [];
|
|
57
59
|
pasteCounter = 0;
|
|
58
|
-
// Streaming render timer for periodic status updates
|
|
59
|
-
streamingRenderTimer = null;
|
|
60
60
|
// History
|
|
61
61
|
history = [];
|
|
62
62
|
historyIndex = -1;
|
|
@@ -69,6 +69,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
69
69
|
statusMessage = null;
|
|
70
70
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
71
71
|
streamingLabel = null; // Streaming progress indicator
|
|
72
|
+
metaElapsedSeconds = null; // Optional elapsed time for header line
|
|
73
|
+
metaTokensUsed = null; // Optional token usage
|
|
74
|
+
metaTokenLimit = null; // Optional token window
|
|
75
|
+
metaThinkingMs = null; // Optional thinking duration
|
|
76
|
+
metaThinkingHasContent = false; // Whether collapsed thinking content exists
|
|
72
77
|
reservedLines = 2;
|
|
73
78
|
scrollRegionActive = false;
|
|
74
79
|
lastRenderContent = '';
|
|
@@ -76,22 +81,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
76
81
|
renderDirty = false;
|
|
77
82
|
isRendering = false;
|
|
78
83
|
pinnedTopRows = 0;
|
|
79
|
-
inlineAnchorRow = null;
|
|
80
|
-
inlineLayout = false;
|
|
81
|
-
anchorProvider = null;
|
|
82
|
-
// Flow mode: when true, renders inline after content (no absolute positioning)
|
|
83
|
-
flowMode = true;
|
|
84
|
-
flowModeRenderedLines = 0; // Track lines rendered for clearing
|
|
85
|
-
// Command suggestions (Claude Code style auto-complete)
|
|
86
|
-
commandSuggestions = [];
|
|
87
|
-
filteredSuggestions = [];
|
|
88
|
-
selectedSuggestionIndex = 0;
|
|
89
|
-
showSuggestions = false;
|
|
90
|
-
maxVisibleSuggestions = 10;
|
|
91
84
|
// Lifecycle
|
|
92
85
|
disposed = false;
|
|
93
86
|
enabled = true;
|
|
94
87
|
contextUsage = null;
|
|
88
|
+
contextAutoCompactThreshold = 90;
|
|
89
|
+
thinkingModeLabel = null;
|
|
95
90
|
editMode = 'display-edits';
|
|
96
91
|
verificationEnabled = true;
|
|
97
92
|
autoContinueEnabled = false;
|
|
@@ -102,10 +97,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
102
97
|
// Streaming render throttle
|
|
103
98
|
lastStreamingRender = 0;
|
|
104
99
|
streamingRenderInterval = 250; // ms between renders during streaming
|
|
105
|
-
// Metrics tracking for status bar
|
|
106
|
-
streamingStartTime = null;
|
|
107
|
-
tokensUsed = 0;
|
|
108
|
-
thinkingEnabled = true;
|
|
109
100
|
constructor(writeStream = process.stdout, config = {}) {
|
|
110
101
|
super();
|
|
111
102
|
this.out = writeStream;
|
|
@@ -193,11 +184,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
193
184
|
if (handled)
|
|
194
185
|
return;
|
|
195
186
|
}
|
|
196
|
-
// Handle '?' for help hint (if buffer is empty)
|
|
197
|
-
if (str === '?' && this.buffer.length === 0) {
|
|
198
|
-
this.emit('showHelp');
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
187
|
// Insert printable characters
|
|
202
188
|
if (str && !key?.ctrl && !key?.meta) {
|
|
203
189
|
this.insertText(str);
|
|
@@ -206,158 +192,24 @@ export class TerminalInput extends EventEmitter {
|
|
|
206
192
|
/**
|
|
207
193
|
* Set the input mode
|
|
208
194
|
*
|
|
209
|
-
* Streaming
|
|
210
|
-
*
|
|
211
|
-
* the cursor is (below the streamed content).
|
|
195
|
+
* Streaming keeps the scroll region active so the prompt/status stay pinned
|
|
196
|
+
* below the streaming output. When streaming ends, we refresh the input area.
|
|
212
197
|
*/
|
|
213
198
|
setMode(mode) {
|
|
214
199
|
const prevMode = this.mode;
|
|
215
200
|
this.mode = mode;
|
|
216
201
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
217
|
-
//
|
|
218
|
-
this.streamingStartTime = Date.now();
|
|
219
|
-
// Enable scroll region so content scrolls while input stays fixed
|
|
202
|
+
// Keep scroll region active so status/prompt stay pinned while streaming
|
|
220
203
|
this.enableScrollRegion();
|
|
221
|
-
// Position cursor in scroll region for content
|
|
222
|
-
const { rows } = this.getSize();
|
|
223
|
-
const scrollBottom = Math.max(1, rows - this.reservedLines);
|
|
224
|
-
this.write(ESC.TO(scrollBottom, 1));
|
|
225
|
-
// Mark dirty for continuous updates
|
|
226
204
|
this.renderDirty = true;
|
|
227
|
-
|
|
228
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
229
|
-
if (this.mode === 'streaming') {
|
|
230
|
-
this.forceRender();
|
|
231
|
-
}
|
|
232
|
-
}, 1000);
|
|
233
|
-
// Render the input area (it's outside scroll region, stays fixed)
|
|
234
|
-
this.forceRender();
|
|
205
|
+
this.render();
|
|
235
206
|
}
|
|
236
207
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
clearInterval(this.streamingRenderTimer);
|
|
240
|
-
this.streamingRenderTimer = null;
|
|
241
|
-
}
|
|
242
|
-
// Reset streaming time
|
|
243
|
-
this.streamingStartTime = null;
|
|
244
|
-
// Keep scroll region active - consistent UI
|
|
245
|
-
// Re-render the input area
|
|
208
|
+
// Streaming ended - render the input area
|
|
209
|
+
this.enableScrollRegion();
|
|
246
210
|
this.forceRender();
|
|
247
211
|
}
|
|
248
212
|
}
|
|
249
|
-
/**
|
|
250
|
-
* Enable or disable flow mode.
|
|
251
|
-
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
252
|
-
* When disabled, input renders at the absolute bottom of terminal.
|
|
253
|
-
*/
|
|
254
|
-
setFlowMode(enabled) {
|
|
255
|
-
if (this.flowMode === enabled)
|
|
256
|
-
return;
|
|
257
|
-
this.flowMode = enabled;
|
|
258
|
-
this.renderDirty = true;
|
|
259
|
-
this.scheduleRender();
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Check if flow mode is enabled.
|
|
263
|
-
*/
|
|
264
|
-
isFlowMode() {
|
|
265
|
-
return this.flowMode;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Set available slash commands for auto-complete suggestions.
|
|
269
|
-
*/
|
|
270
|
-
setCommands(commands) {
|
|
271
|
-
this.commandSuggestions = commands;
|
|
272
|
-
this.updateSuggestions();
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Update filtered suggestions based on current input.
|
|
276
|
-
*/
|
|
277
|
-
updateSuggestions() {
|
|
278
|
-
const input = this.buffer.trim();
|
|
279
|
-
// Only show suggestions when input starts with "/"
|
|
280
|
-
if (!input.startsWith('/')) {
|
|
281
|
-
this.showSuggestions = false;
|
|
282
|
-
this.filteredSuggestions = [];
|
|
283
|
-
this.selectedSuggestionIndex = 0;
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const query = input.toLowerCase();
|
|
287
|
-
this.filteredSuggestions = this.commandSuggestions.filter(cmd => cmd.command.toLowerCase().startsWith(query) ||
|
|
288
|
-
cmd.command.toLowerCase().includes(query.slice(1)));
|
|
289
|
-
// Show suggestions if we have matches
|
|
290
|
-
this.showSuggestions = this.filteredSuggestions.length > 0;
|
|
291
|
-
// Keep selection in bounds
|
|
292
|
-
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
293
|
-
this.selectedSuggestionIndex = Math.max(0, this.filteredSuggestions.length - 1);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Select next suggestion (arrow down / tab).
|
|
298
|
-
*/
|
|
299
|
-
selectNextSuggestion() {
|
|
300
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
301
|
-
return;
|
|
302
|
-
this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % this.filteredSuggestions.length;
|
|
303
|
-
this.renderDirty = true;
|
|
304
|
-
this.scheduleRender();
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Select previous suggestion (arrow up / shift+tab).
|
|
308
|
-
*/
|
|
309
|
-
selectPrevSuggestion() {
|
|
310
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
311
|
-
return;
|
|
312
|
-
this.selectedSuggestionIndex = this.selectedSuggestionIndex === 0
|
|
313
|
-
? this.filteredSuggestions.length - 1
|
|
314
|
-
: this.selectedSuggestionIndex - 1;
|
|
315
|
-
this.renderDirty = true;
|
|
316
|
-
this.scheduleRender();
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Accept current suggestion and insert into buffer.
|
|
320
|
-
*/
|
|
321
|
-
acceptSuggestion() {
|
|
322
|
-
if (!this.showSuggestions || this.filteredSuggestions.length === 0)
|
|
323
|
-
return false;
|
|
324
|
-
const selected = this.filteredSuggestions[this.selectedSuggestionIndex];
|
|
325
|
-
if (!selected)
|
|
326
|
-
return false;
|
|
327
|
-
// Replace buffer with selected command
|
|
328
|
-
this.buffer = selected.command + ' ';
|
|
329
|
-
this.cursor = this.buffer.length;
|
|
330
|
-
this.showSuggestions = false;
|
|
331
|
-
this.renderDirty = true;
|
|
332
|
-
this.scheduleRender();
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Check if suggestions are visible.
|
|
337
|
-
*/
|
|
338
|
-
areSuggestionsVisible() {
|
|
339
|
-
return this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Update token count for metrics display
|
|
343
|
-
*/
|
|
344
|
-
setTokensUsed(tokens) {
|
|
345
|
-
this.tokensUsed = tokens;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Toggle thinking/reasoning mode
|
|
349
|
-
*/
|
|
350
|
-
toggleThinking() {
|
|
351
|
-
this.thinkingEnabled = !this.thinkingEnabled;
|
|
352
|
-
this.emit('thinkingToggle', this.thinkingEnabled);
|
|
353
|
-
this.scheduleRender();
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Get thinking enabled state
|
|
357
|
-
*/
|
|
358
|
-
isThinkingEnabled() {
|
|
359
|
-
return this.thinkingEnabled;
|
|
360
|
-
}
|
|
361
213
|
/**
|
|
362
214
|
* Keep the top N rows pinned outside the scroll region (used for the launch banner).
|
|
363
215
|
*/
|
|
@@ -370,42 +222,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
370
222
|
}
|
|
371
223
|
}
|
|
372
224
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
375
|
-
* restore the default bottom-aligned layout.
|
|
376
|
-
*/
|
|
377
|
-
setInlineAnchor(row) {
|
|
378
|
-
if (row === null || row === undefined) {
|
|
379
|
-
this.inlineAnchorRow = null;
|
|
380
|
-
this.inlineLayout = false;
|
|
381
|
-
this.renderDirty = true;
|
|
382
|
-
this.render();
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const { rows } = this.getSize();
|
|
386
|
-
const clamped = Math.max(1, Math.min(Math.floor(row), rows));
|
|
387
|
-
this.inlineAnchorRow = clamped;
|
|
388
|
-
this.inlineLayout = true;
|
|
389
|
-
this.renderDirty = true;
|
|
390
|
-
this.render();
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Provide a dynamic anchor callback. When set, the prompt will follow the
|
|
394
|
-
* output by re-evaluating the anchor before each render.
|
|
395
|
-
*/
|
|
396
|
-
setInlineAnchorProvider(provider) {
|
|
397
|
-
this.anchorProvider = provider;
|
|
398
|
-
if (!provider) {
|
|
399
|
-
this.inlineLayout = false;
|
|
400
|
-
this.inlineAnchorRow = null;
|
|
401
|
-
this.renderDirty = true;
|
|
402
|
-
this.render();
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
this.inlineLayout = true;
|
|
406
|
-
this.renderDirty = true;
|
|
407
|
-
this.render();
|
|
408
|
-
}
|
|
409
225
|
/**
|
|
410
226
|
* Get current mode
|
|
411
227
|
*/
|
|
@@ -515,6 +331,37 @@ export class TerminalInput extends EventEmitter {
|
|
|
515
331
|
this.streamingLabel = next;
|
|
516
332
|
this.scheduleRender();
|
|
517
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Surface meta status just above the divider (e.g., elapsed time or token usage).
|
|
336
|
+
*/
|
|
337
|
+
setMetaStatus(meta) {
|
|
338
|
+
const nextElapsed = typeof meta.elapsedSeconds === 'number' && Number.isFinite(meta.elapsedSeconds) && meta.elapsedSeconds >= 0
|
|
339
|
+
? Math.floor(meta.elapsedSeconds)
|
|
340
|
+
: null;
|
|
341
|
+
const nextTokens = typeof meta.tokensUsed === 'number' && Number.isFinite(meta.tokensUsed) && meta.tokensUsed >= 0
|
|
342
|
+
? Math.floor(meta.tokensUsed)
|
|
343
|
+
: null;
|
|
344
|
+
const nextLimit = typeof meta.tokenLimit === 'number' && Number.isFinite(meta.tokenLimit) && meta.tokenLimit > 0
|
|
345
|
+
? Math.floor(meta.tokenLimit)
|
|
346
|
+
: null;
|
|
347
|
+
const nextThinking = typeof meta.thinkingMs === 'number' && Number.isFinite(meta.thinkingMs) && meta.thinkingMs >= 0
|
|
348
|
+
? Math.floor(meta.thinkingMs)
|
|
349
|
+
: null;
|
|
350
|
+
const nextThinkingHasContent = !!meta.thinkingHasContent;
|
|
351
|
+
if (this.metaElapsedSeconds === nextElapsed &&
|
|
352
|
+
this.metaTokensUsed === nextTokens &&
|
|
353
|
+
this.metaTokenLimit === nextLimit &&
|
|
354
|
+
this.metaThinkingMs === nextThinking &&
|
|
355
|
+
this.metaThinkingHasContent === nextThinkingHasContent) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.metaElapsedSeconds = nextElapsed;
|
|
359
|
+
this.metaTokensUsed = nextTokens;
|
|
360
|
+
this.metaTokenLimit = nextLimit;
|
|
361
|
+
this.metaThinkingMs = nextThinking;
|
|
362
|
+
this.metaThinkingHasContent = nextThinkingHasContent;
|
|
363
|
+
this.scheduleRender();
|
|
364
|
+
}
|
|
518
365
|
/**
|
|
519
366
|
* Keep mode toggles (verification/auto-continue) visible in the control bar.
|
|
520
367
|
* Hotkey labels remain stable so the bar looks the same before/during streaming.
|
|
@@ -524,16 +371,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
524
371
|
const nextAutoContinue = !!options.autoContinueEnabled;
|
|
525
372
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
526
373
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
374
|
+
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
527
375
|
if (this.verificationEnabled === nextVerification &&
|
|
528
376
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
529
377
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
530
|
-
this.autoContinueHotkey === nextAutoHotkey
|
|
378
|
+
this.autoContinueHotkey === nextAutoHotkey &&
|
|
379
|
+
this.thinkingModeLabel === nextThinkingLabel) {
|
|
531
380
|
return;
|
|
532
381
|
}
|
|
533
382
|
this.verificationEnabled = nextVerification;
|
|
534
383
|
this.autoContinueEnabled = nextAutoContinue;
|
|
535
384
|
this.verificationHotkey = nextVerifyHotkey;
|
|
536
385
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
386
|
+
this.thinkingModeLabel = nextThinkingLabel;
|
|
537
387
|
this.scheduleRender();
|
|
538
388
|
}
|
|
539
389
|
/**
|
|
@@ -548,192 +398,88 @@ export class TerminalInput extends EventEmitter {
|
|
|
548
398
|
/**
|
|
549
399
|
* Render the input area - Claude Code style with mode controls
|
|
550
400
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
401
|
+
* IMPORTANT: During streaming mode, we skip the full render to avoid
|
|
402
|
+
* interfering with streaming content. Streaming content writes naturally
|
|
403
|
+
* to stdout, and cursor positioning during streaming would cause garbled output.
|
|
404
|
+
* A full render is done when streaming ends via forceRender().
|
|
553
405
|
*/
|
|
554
406
|
render() {
|
|
555
407
|
if (!this.canRender())
|
|
556
408
|
return;
|
|
557
409
|
if (this.isRendering)
|
|
558
410
|
return;
|
|
559
|
-
|
|
411
|
+
// CRITICAL: During streaming, skip full render to avoid interfering
|
|
412
|
+
// with streaming content. The streaming chunks need to flow naturally
|
|
413
|
+
// without cursor repositioning breaking them up.
|
|
414
|
+
// Check both local mode and global streaming mode flag
|
|
415
|
+
if (this.mode === 'streaming' || isStreamingMode()) {
|
|
416
|
+
this.renderDirty = true; // Mark dirty so we render after streaming ends
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
560
419
|
const shouldSkip = !this.renderDirty &&
|
|
561
420
|
this.buffer === this.lastRenderContent &&
|
|
562
421
|
this.cursor === this.lastRenderCursor;
|
|
563
422
|
this.renderDirty = false;
|
|
564
|
-
// Skip if nothing changed
|
|
565
|
-
if (shouldSkip
|
|
423
|
+
// Skip if nothing changed and no explicit refresh requested
|
|
424
|
+
if (shouldSkip) {
|
|
566
425
|
return;
|
|
567
426
|
}
|
|
568
|
-
// If write lock is held, defer render
|
|
427
|
+
// If write lock is held, defer render to avoid race conditions
|
|
569
428
|
if (writeLock.isLocked()) {
|
|
570
429
|
writeLock.safeWrite(() => this.render());
|
|
571
430
|
return;
|
|
572
431
|
}
|
|
573
432
|
this.isRendering = true;
|
|
433
|
+
// Use write lock during render to prevent interleaved output
|
|
574
434
|
writeLock.lock('terminalInput.render');
|
|
575
435
|
try {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
this.write('\x1b7'); // Save cursor (DECSC)
|
|
436
|
+
if (!this.scrollRegionActive) {
|
|
437
|
+
this.enableScrollRegion();
|
|
579
438
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
439
|
+
const { rows, cols } = this.getSize();
|
|
440
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
441
|
+
// Wrap buffer into display lines
|
|
442
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
443
|
+
const availableForContent = Math.max(1, rows - 3); // room for separator + input + controls
|
|
444
|
+
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
445
|
+
const displayLines = Math.min(lines.length, maxVisible);
|
|
446
|
+
const metaLines = this.buildMetaLines(cols - 2);
|
|
447
|
+
// Reserved lines: optional meta lines + separator(1) + input lines + controls(1)
|
|
448
|
+
this.updateReservedLines(displayLines + 2 + metaLines.length);
|
|
449
|
+
// Calculate display window (keep cursor visible)
|
|
450
|
+
let startLine = 0;
|
|
451
|
+
if (lines.length > displayLines) {
|
|
452
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
453
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
585
454
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
this.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
*/
|
|
598
|
-
renderFlowMode() {
|
|
599
|
-
// Use stable bottom-pinned approach
|
|
600
|
-
this.renderBottomPinned();
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
604
|
-
*
|
|
605
|
-
* Layout when suggestions visible:
|
|
606
|
-
* - Top divider
|
|
607
|
-
* - Input line(s)
|
|
608
|
-
* - Bottom divider
|
|
609
|
-
* - Suggestions (command list)
|
|
610
|
-
*
|
|
611
|
-
* Layout when suggestions hidden:
|
|
612
|
-
* - Status bar (Ready/Streaming)
|
|
613
|
-
* - Top divider
|
|
614
|
-
* - Input line(s)
|
|
615
|
-
* - Bottom divider
|
|
616
|
-
* - Mode controls
|
|
617
|
-
*/
|
|
618
|
-
renderBottomPinned() {
|
|
619
|
-
const { rows, cols } = this.getSize();
|
|
620
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
621
|
-
// Wrap buffer into display lines
|
|
622
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
623
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
624
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
625
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
626
|
-
// Calculate display window (keep cursor visible)
|
|
627
|
-
let startLine = 0;
|
|
628
|
-
if (lines.length > displayLines) {
|
|
629
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
630
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
631
|
-
}
|
|
632
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
633
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
634
|
-
// Calculate suggestion display
|
|
635
|
-
const suggestionsToShow = this.showSuggestions
|
|
636
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
637
|
-
: [];
|
|
638
|
-
const suggestionLines = suggestionsToShow.length;
|
|
639
|
-
this.write(ESC.HIDE);
|
|
640
|
-
this.write(ESC.RESET);
|
|
641
|
-
const divider = renderDivider(cols - 2);
|
|
642
|
-
// Calculate positions from absolute bottom
|
|
643
|
-
let currentRow;
|
|
644
|
-
if (suggestionLines > 0) {
|
|
645
|
-
// With suggestions: input area + dividers + suggestions
|
|
646
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
647
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
648
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
649
|
-
this.updateReservedLines(totalHeight);
|
|
650
|
-
// Top divider
|
|
651
|
-
this.write(ESC.TO(currentRow, 1));
|
|
652
|
-
this.write(ESC.CLEAR_LINE);
|
|
653
|
-
this.write(divider);
|
|
654
|
-
currentRow++;
|
|
655
|
-
// Input lines
|
|
656
|
-
let finalRow = currentRow;
|
|
657
|
-
let finalCol = 3;
|
|
658
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
455
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
456
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
457
|
+
// Render
|
|
458
|
+
this.write(ESC.HIDE);
|
|
459
|
+
this.write(ESC.RESET);
|
|
460
|
+
const startRow = Math.max(1, rows - this.reservedLines + 1);
|
|
461
|
+
let currentRow = startRow;
|
|
462
|
+
// Clear the reserved block to avoid stale meta/status lines
|
|
463
|
+
this.clearReservedArea(startRow, this.reservedLines, cols);
|
|
464
|
+
// Meta/status header (elapsed, tokens/context)
|
|
465
|
+
for (const metaLine of metaLines) {
|
|
659
466
|
this.write(ESC.TO(currentRow, 1));
|
|
660
467
|
this.write(ESC.CLEAR_LINE);
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
664
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
665
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
666
|
-
if (isCursorLine) {
|
|
667
|
-
const col = Math.min(cursorCol, line.length);
|
|
668
|
-
this.write(line.slice(0, col));
|
|
669
|
-
this.write(ESC.REVERSE);
|
|
670
|
-
this.write(col < line.length ? line[col] : ' ');
|
|
671
|
-
this.write(ESC.RESET);
|
|
672
|
-
this.write(line.slice(col + 1));
|
|
673
|
-
finalRow = currentRow;
|
|
674
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
this.write(line);
|
|
678
|
-
}
|
|
679
|
-
currentRow++;
|
|
680
|
-
}
|
|
681
|
-
// Bottom divider
|
|
682
|
-
this.write(ESC.TO(currentRow, 1));
|
|
683
|
-
this.write(ESC.CLEAR_LINE);
|
|
684
|
-
this.write(divider);
|
|
685
|
-
currentRow++;
|
|
686
|
-
// Suggestions (Claude Code style)
|
|
687
|
-
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
688
|
-
this.write(ESC.TO(currentRow, 1));
|
|
689
|
-
this.write(ESC.CLEAR_LINE);
|
|
690
|
-
const suggestion = suggestionsToShow[i];
|
|
691
|
-
const isSelected = i === this.selectedSuggestionIndex;
|
|
692
|
-
// Indent and highlight selected
|
|
693
|
-
this.write(' ');
|
|
694
|
-
if (isSelected) {
|
|
695
|
-
this.write(ESC.REVERSE);
|
|
696
|
-
this.write(ESC.BOLD);
|
|
697
|
-
}
|
|
698
|
-
this.write(suggestion.command);
|
|
699
|
-
if (isSelected) {
|
|
700
|
-
this.write(ESC.RESET);
|
|
701
|
-
}
|
|
702
|
-
// Description (dimmed)
|
|
703
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
704
|
-
if (descSpace > 10 && suggestion.description) {
|
|
705
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
706
|
-
this.write(ESC.RESET);
|
|
707
|
-
this.write(ESC.DIM);
|
|
708
|
-
this.write(' ');
|
|
709
|
-
this.write(desc);
|
|
710
|
-
this.write(ESC.RESET);
|
|
711
|
-
}
|
|
712
|
-
currentRow++;
|
|
468
|
+
this.write(metaLine);
|
|
469
|
+
currentRow += 1;
|
|
713
470
|
}
|
|
714
|
-
//
|
|
715
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
716
|
-
}
|
|
717
|
-
else {
|
|
718
|
-
// Without suggestions: normal layout with status bar and controls
|
|
719
|
-
const totalHeight = visibleLines.length + 4; // status + topDiv + input + bottomDiv + controls
|
|
720
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
721
|
-
this.updateReservedLines(totalHeight);
|
|
722
|
-
// Status bar
|
|
723
|
-
this.write(ESC.TO(currentRow, 1));
|
|
724
|
-
this.write(ESC.CLEAR_LINE);
|
|
725
|
-
this.write(this.buildStatusBar(cols));
|
|
726
|
-
currentRow++;
|
|
727
|
-
// Top divider
|
|
471
|
+
// Separator line
|
|
728
472
|
this.write(ESC.TO(currentRow, 1));
|
|
729
473
|
this.write(ESC.CLEAR_LINE);
|
|
474
|
+
const divider = renderDivider(cols - 2);
|
|
730
475
|
this.write(divider);
|
|
731
|
-
currentRow
|
|
732
|
-
//
|
|
476
|
+
currentRow += 1;
|
|
477
|
+
// Render input lines
|
|
733
478
|
let finalRow = currentRow;
|
|
734
479
|
let finalCol = 3;
|
|
735
480
|
for (let i = 0; i < visibleLines.length; i++) {
|
|
736
|
-
|
|
481
|
+
const rowNum = currentRow + i;
|
|
482
|
+
this.write(ESC.TO(rowNum, 1));
|
|
737
483
|
this.write(ESC.CLEAR_LINE);
|
|
738
484
|
const line = visibleLines[i] ?? '';
|
|
739
485
|
const absoluteLineIdx = startLine + i;
|
|
@@ -747,6 +493,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
747
493
|
this.write(ESC.RESET);
|
|
748
494
|
this.write(ESC.BG_DARK);
|
|
749
495
|
if (isCursorLine) {
|
|
496
|
+
// Render with block cursor
|
|
750
497
|
const col = Math.min(cursorCol, line.length);
|
|
751
498
|
const before = line.slice(0, col);
|
|
752
499
|
const at = col < line.length ? line[col] : ' ';
|
|
@@ -756,137 +503,214 @@ export class TerminalInput extends EventEmitter {
|
|
|
756
503
|
this.write(at);
|
|
757
504
|
this.write(ESC.RESET + ESC.BG_DARK);
|
|
758
505
|
this.write(after);
|
|
759
|
-
finalRow =
|
|
506
|
+
finalRow = rowNum;
|
|
760
507
|
finalCol = this.config.promptChar.length + col + 1;
|
|
761
508
|
}
|
|
762
509
|
else {
|
|
763
510
|
this.write(line);
|
|
764
511
|
}
|
|
765
|
-
// Pad to edge
|
|
512
|
+
// Pad to edge for clean look
|
|
766
513
|
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
767
514
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
768
515
|
if (padding > 0)
|
|
769
516
|
this.write(' '.repeat(padding));
|
|
770
517
|
this.write(ESC.RESET);
|
|
771
|
-
currentRow++;
|
|
772
518
|
}
|
|
773
|
-
//
|
|
774
|
-
|
|
775
|
-
this.write(ESC.
|
|
776
|
-
this.write(divider);
|
|
777
|
-
currentRow++;
|
|
778
|
-
// Mode controls
|
|
779
|
-
this.write(ESC.TO(currentRow, 1));
|
|
519
|
+
// Mode controls line (Claude Code style)
|
|
520
|
+
const controlRow = currentRow + visibleLines.length;
|
|
521
|
+
this.write(ESC.TO(controlRow, 1));
|
|
780
522
|
this.write(ESC.CLEAR_LINE);
|
|
781
523
|
this.write(this.buildModeControls(cols));
|
|
782
|
-
// Position cursor
|
|
524
|
+
// Position cursor
|
|
783
525
|
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
526
|
+
this.write(ESC.SHOW);
|
|
527
|
+
// Update state
|
|
528
|
+
this.lastRenderContent = this.buffer;
|
|
529
|
+
this.lastRenderCursor = this.cursor;
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
writeLock.unlock();
|
|
533
|
+
this.isRendering = false;
|
|
784
534
|
}
|
|
785
|
-
this.write(ESC.SHOW);
|
|
786
|
-
// Update state
|
|
787
|
-
this.lastRenderContent = this.buffer;
|
|
788
|
-
this.lastRenderCursor = this.cursor;
|
|
789
535
|
}
|
|
790
536
|
/**
|
|
791
|
-
* Build
|
|
792
|
-
* This is the TOP line above the input area - minimal Claude Code style.
|
|
537
|
+
* Build one or more compact meta lines above the divider (thinking, status, usage).
|
|
793
538
|
*/
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
let statusText = '● Streaming';
|
|
800
|
-
if (this.streamingStartTime) {
|
|
801
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
802
|
-
const mins = Math.floor(elapsed / 60);
|
|
803
|
-
const secs = elapsed % 60;
|
|
804
|
-
statusText += mins > 0 ? ` ${mins}m ${secs}s` : ` ${secs}s`;
|
|
805
|
-
}
|
|
806
|
-
parts.push(`\x1b[32m${statusText}\x1b[0m`); // Green
|
|
539
|
+
buildMetaLines(width) {
|
|
540
|
+
const lines = [];
|
|
541
|
+
if (this.metaThinkingMs !== null) {
|
|
542
|
+
const thinkingText = formatThinking(this.metaThinkingMs, this.metaThinkingHasContent);
|
|
543
|
+
lines.push(renderStatusLine([{ text: thinkingText, tone: 'info' }], width));
|
|
807
544
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
545
|
+
const statusParts = [];
|
|
546
|
+
const statusLabel = this.statusMessage ?? this.streamingLabel;
|
|
547
|
+
if (statusLabel) {
|
|
548
|
+
statusParts.push({ text: statusLabel, tone: 'info' });
|
|
811
549
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
550
|
+
if (this.mode === 'streaming' || isStreamingMode()) {
|
|
551
|
+
statusParts.push({ text: 'esc to interrupt', tone: 'warn' });
|
|
552
|
+
}
|
|
553
|
+
if (this.metaElapsedSeconds !== null) {
|
|
554
|
+
statusParts.push({ text: this.formatElapsedLabel(this.metaElapsedSeconds), tone: 'muted' });
|
|
555
|
+
}
|
|
556
|
+
const tokensRemaining = this.computeTokensRemaining();
|
|
557
|
+
if (tokensRemaining !== null) {
|
|
558
|
+
statusParts.push({ text: `↓ ${tokensRemaining}`, tone: 'muted' });
|
|
559
|
+
}
|
|
560
|
+
if (statusParts.length) {
|
|
561
|
+
lines.push(renderStatusLine(statusParts, width));
|
|
562
|
+
}
|
|
563
|
+
const usageParts = [];
|
|
564
|
+
if (this.metaTokensUsed !== null) {
|
|
565
|
+
const formattedUsed = this.formatTokenCount(this.metaTokensUsed);
|
|
566
|
+
const formattedLimit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
|
|
567
|
+
usageParts.push({ text: `tokens ${formattedUsed}${formattedLimit}`, tone: 'muted' });
|
|
568
|
+
}
|
|
569
|
+
if (this.contextUsage !== null) {
|
|
570
|
+
const tone = this.contextUsage >= 75 ? 'warn' : 'muted';
|
|
571
|
+
const left = Math.max(0, 100 - this.contextUsage);
|
|
572
|
+
usageParts.push({ text: `context used ${this.contextUsage}% (${left}% left)`, tone });
|
|
573
|
+
}
|
|
574
|
+
if (this.queue.length > 0) {
|
|
575
|
+
usageParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
576
|
+
}
|
|
577
|
+
if (usageParts.length) {
|
|
578
|
+
lines.push(renderStatusLine(usageParts, width));
|
|
579
|
+
}
|
|
580
|
+
return lines;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Clear the reserved bottom block (meta + divider + input + controls) before repainting.
|
|
584
|
+
*/
|
|
585
|
+
clearReservedArea(startRow, reservedLines, cols) {
|
|
586
|
+
const width = Math.max(1, cols);
|
|
587
|
+
for (let i = 0; i < reservedLines; i++) {
|
|
588
|
+
const row = startRow + i;
|
|
589
|
+
this.write(ESC.TO(row, 1));
|
|
590
|
+
this.write(' '.repeat(width));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Build Claude Code style mode controls line.
|
|
595
|
+
* Combines streaming label + override status + main status for simultaneous display.
|
|
596
|
+
*/
|
|
597
|
+
buildModeControls(cols) {
|
|
598
|
+
const width = Math.max(8, cols - 2);
|
|
599
|
+
const leftParts = [];
|
|
600
|
+
const rightParts = [];
|
|
601
|
+
if (this.streamingLabel) {
|
|
602
|
+
leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
816
603
|
}
|
|
817
|
-
// Override/warning status
|
|
818
604
|
if (this.overrideStatusMessage) {
|
|
819
|
-
|
|
605
|
+
leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
|
|
820
606
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
607
|
+
if (this.statusMessage) {
|
|
608
|
+
leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
|
|
609
|
+
}
|
|
610
|
+
const editLabel = this.editMode === 'display-edits' ? 'accept edits on' : 'ask before edits';
|
|
611
|
+
const editIcon = this.editMode === 'display-edits' ? '⏵⏵' : '🛡';
|
|
612
|
+
leftParts.push({
|
|
613
|
+
text: `${editIcon} ${editLabel} (shift+tab to cycle)`,
|
|
614
|
+
tone: this.editMode === 'display-edits' ? 'success' : 'muted',
|
|
615
|
+
});
|
|
616
|
+
const verifyLabel = this.verificationEnabled ? 'verify+build on' : 'verify+build off';
|
|
617
|
+
leftParts.push({
|
|
618
|
+
text: `${verifyLabel} (${this.verificationHotkey.toLowerCase()})`,
|
|
619
|
+
tone: this.verificationEnabled ? 'success' : 'muted',
|
|
620
|
+
});
|
|
621
|
+
const continueLabel = this.autoContinueEnabled ? 'auto-continue on' : 'auto-continue off';
|
|
622
|
+
leftParts.push({
|
|
623
|
+
text: `${continueLabel} (${this.autoContinueHotkey.toLowerCase()})`,
|
|
624
|
+
tone: this.autoContinueEnabled ? 'info' : 'muted',
|
|
625
|
+
});
|
|
626
|
+
if (this.queue.length > 0 && this.mode !== 'streaming') {
|
|
627
|
+
leftParts.push({ text: `queued ${this.queue.length}`, tone: 'info' });
|
|
824
628
|
}
|
|
825
|
-
// Multi-line indicator
|
|
826
629
|
if (this.buffer.includes('\n')) {
|
|
827
|
-
|
|
630
|
+
const lineCount = this.buffer.split('\n').length;
|
|
631
|
+
leftParts.push({ text: `${lineCount} lines`, tone: 'muted' });
|
|
632
|
+
}
|
|
633
|
+
if (this.pastePlaceholders.length > 0) {
|
|
634
|
+
const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
|
|
635
|
+
leftParts.push({
|
|
636
|
+
text: `paste #${latest.id} +${latest.lineCount} lines (⌫ to drop)`,
|
|
637
|
+
tone: 'info',
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
const contextRemaining = this.computeContextRemaining();
|
|
641
|
+
if (this.thinkingModeLabel) {
|
|
642
|
+
rightParts.push({ text: `thinking ${this.thinkingModeLabel} (/thinking)`, tone: 'info' });
|
|
643
|
+
}
|
|
644
|
+
if (contextRemaining !== null) {
|
|
645
|
+
const tone = contextRemaining <= 10 ? 'warn' : 'muted';
|
|
646
|
+
const label = contextRemaining === 0 && this.contextUsage !== null
|
|
647
|
+
? 'Context auto-compact imminent'
|
|
648
|
+
: `Context left until auto-compact: ${contextRemaining}%`;
|
|
649
|
+
rightParts.push({ text: label, tone });
|
|
650
|
+
}
|
|
651
|
+
if (!rightParts.length || width < 60) {
|
|
652
|
+
const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
|
|
653
|
+
return renderStatusLine(merged, width);
|
|
654
|
+
}
|
|
655
|
+
const leftWidth = Math.max(12, Math.floor(width * 0.6));
|
|
656
|
+
const rightWidth = Math.max(14, width - leftWidth - 1);
|
|
657
|
+
const leftText = renderStatusLine(leftParts, leftWidth);
|
|
658
|
+
const rightText = renderStatusLine(rightParts, rightWidth);
|
|
659
|
+
const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
|
|
660
|
+
return `${leftText}${' '.repeat(spacing)}${rightText}`;
|
|
661
|
+
}
|
|
662
|
+
computeContextRemaining() {
|
|
663
|
+
if (this.contextUsage === null) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
return Math.max(0, this.contextAutoCompactThreshold - this.contextUsage);
|
|
667
|
+
}
|
|
668
|
+
computeTokensRemaining() {
|
|
669
|
+
if (this.metaTokensUsed === null || this.metaTokenLimit === null) {
|
|
670
|
+
return null;
|
|
828
671
|
}
|
|
829
|
-
|
|
830
|
-
|
|
672
|
+
const remaining = Math.max(0, this.metaTokenLimit - this.metaTokensUsed);
|
|
673
|
+
return this.formatTokenCount(remaining);
|
|
674
|
+
}
|
|
675
|
+
formatElapsedLabel(seconds) {
|
|
676
|
+
if (seconds < 60) {
|
|
677
|
+
return `${seconds}s`;
|
|
831
678
|
}
|
|
832
|
-
const
|
|
833
|
-
|
|
679
|
+
const mins = Math.floor(seconds / 60);
|
|
680
|
+
const secs = seconds % 60;
|
|
681
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
682
|
+
}
|
|
683
|
+
formatTokenCount(value) {
|
|
684
|
+
if (!Number.isFinite(value)) {
|
|
685
|
+
return `${value}`;
|
|
686
|
+
}
|
|
687
|
+
if (value >= 1_000_000) {
|
|
688
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
689
|
+
}
|
|
690
|
+
if (value >= 1_000) {
|
|
691
|
+
return `${(value / 1_000).toFixed(1)}k`;
|
|
692
|
+
}
|
|
693
|
+
return `${Math.round(value)}`;
|
|
694
|
+
}
|
|
695
|
+
visibleLength(value) {
|
|
696
|
+
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
697
|
+
return value.replace(ansiPattern, '').length;
|
|
834
698
|
}
|
|
835
699
|
/**
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
* Layout: [toggles on left] ... [context info on right]
|
|
700
|
+
* Debug-only snapshot used by tests to assert rendered strings without
|
|
701
|
+
* needing a TTY. Not used by production code.
|
|
840
702
|
*/
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const editLabel = this.editMode === 'display-edits' ? 'auto-edit' : 'ask-first';
|
|
848
|
-
toggles.push(`${editIcon} ${editLabel}`);
|
|
849
|
-
// Thinking mode toggle
|
|
850
|
-
const thinkIcon = this.thinkingEnabled ? '💭' : '○';
|
|
851
|
-
toggles.push(`${thinkIcon} think`);
|
|
852
|
-
// Verification toggle
|
|
853
|
-
const verifyIcon = this.verificationEnabled ? '✓' : '○';
|
|
854
|
-
toggles.push(`${verifyIcon} verify`);
|
|
855
|
-
// Auto-continue toggle
|
|
856
|
-
const autoIcon = this.autoContinueEnabled ? '↻' : '○';
|
|
857
|
-
toggles.push(`${autoIcon} auto`);
|
|
858
|
-
const leftPart = toggles.join(' · ') + ' (⇧⇥)';
|
|
859
|
-
// Right side: Context usage information
|
|
860
|
-
let rightPart = '';
|
|
861
|
-
if (this.contextUsage !== null) {
|
|
862
|
-
const remaining = Math.max(0, 100 - this.contextUsage);
|
|
863
|
-
const urgency = remaining < 10 ? '⚠ ' : remaining < 25 ? '! ' : '';
|
|
864
|
-
rightPart = `${urgency}ctx: ${remaining}%`;
|
|
865
|
-
}
|
|
866
|
-
// Calculate spacing
|
|
867
|
-
const leftLen = leftPart.length;
|
|
868
|
-
const rightLen = rightPart.length;
|
|
869
|
-
const totalLen = leftLen + rightLen;
|
|
870
|
-
// If both fit with spacing, align right part to right edge
|
|
871
|
-
if (totalLen < maxWidth - 4) {
|
|
872
|
-
const spacing = maxWidth - totalLen;
|
|
873
|
-
return `${ESC.DIM}${leftPart}${' '.repeat(spacing)}${rightPart}${ESC.RESET}`;
|
|
874
|
-
}
|
|
875
|
-
// If they don't fit, prioritize left and truncate right
|
|
876
|
-
if (rightLen > 0 && leftLen + 8 < maxWidth) {
|
|
877
|
-
const availableForRight = maxWidth - leftLen - 4;
|
|
878
|
-
const truncatedRight = rightPart.slice(0, availableForRight);
|
|
879
|
-
return `${ESC.DIM}${leftPart} ${truncatedRight}${ESC.RESET}`;
|
|
880
|
-
}
|
|
881
|
-
// Just show left part
|
|
882
|
-
return `${ESC.DIM}${leftPart.slice(0, maxWidth)}${ESC.RESET}`;
|
|
703
|
+
getDebugUiSnapshot(width) {
|
|
704
|
+
const cols = Math.max(8, width ?? this.getSize().cols);
|
|
705
|
+
return {
|
|
706
|
+
meta: this.buildMetaLines(cols - 2),
|
|
707
|
+
controls: this.buildModeControls(cols),
|
|
708
|
+
};
|
|
883
709
|
}
|
|
884
710
|
/**
|
|
885
711
|
* Force a re-render
|
|
886
712
|
*/
|
|
887
713
|
forceRender() {
|
|
888
|
-
// Ensure scroll region is enabled for consistent UI
|
|
889
|
-
this.enableScrollRegion();
|
|
890
714
|
this.lastRenderContent = '';
|
|
891
715
|
this.lastRenderCursor = -1;
|
|
892
716
|
this.renderDirty = true;
|
|
@@ -916,9 +740,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
916
740
|
* Register with display's output interceptor to position cursor correctly.
|
|
917
741
|
* When scroll region is active, output needs to go to the scroll region,
|
|
918
742
|
* not the protected bottom area where the input is rendered.
|
|
919
|
-
*
|
|
920
|
-
* NOTE: With scroll region properly set, content naturally stays within
|
|
921
|
-
* the region boundaries - no cursor manipulation needed per-write.
|
|
922
743
|
*/
|
|
923
744
|
registerOutputInterceptor(display) {
|
|
924
745
|
if (this.outputInterceptorCleanup) {
|
|
@@ -926,11 +747,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
926
747
|
}
|
|
927
748
|
this.outputInterceptorCleanup = display.registerOutputInterceptor({
|
|
928
749
|
beforeWrite: () => {
|
|
929
|
-
//
|
|
930
|
-
//
|
|
750
|
+
// When the scroll region is active, temporarily move the cursor into
|
|
751
|
+
// the scrollable area so streamed output lands above the pinned prompt.
|
|
752
|
+
if (this.scrollRegionActive) {
|
|
753
|
+
const { rows } = this.getSize();
|
|
754
|
+
const scrollBottom = Math.max(this.pinnedTopRows + 1, rows - this.reservedLines);
|
|
755
|
+
this.write(ESC.SAVE);
|
|
756
|
+
this.write(ESC.TO(scrollBottom, 1));
|
|
757
|
+
}
|
|
931
758
|
},
|
|
932
759
|
afterWrite: () => {
|
|
933
|
-
//
|
|
760
|
+
// Restore cursor back to the pinned prompt after output completes.
|
|
761
|
+
if (this.scrollRegionActive) {
|
|
762
|
+
this.write(ESC.RESTORE);
|
|
763
|
+
}
|
|
934
764
|
},
|
|
935
765
|
});
|
|
936
766
|
}
|
|
@@ -940,11 +770,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
940
770
|
dispose() {
|
|
941
771
|
if (this.disposed)
|
|
942
772
|
return;
|
|
943
|
-
// Clean up streaming render timer
|
|
944
|
-
if (this.streamingRenderTimer) {
|
|
945
|
-
clearInterval(this.streamingRenderTimer);
|
|
946
|
-
this.streamingRenderTimer = null;
|
|
947
|
-
}
|
|
948
773
|
// Clean up output interceptor
|
|
949
774
|
if (this.outputInterceptorCleanup) {
|
|
950
775
|
this.outputInterceptorCleanup();
|
|
@@ -1057,22 +882,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1057
882
|
this.toggleEditMode();
|
|
1058
883
|
return true;
|
|
1059
884
|
}
|
|
1060
|
-
|
|
1061
|
-
if (this.findPlaceholderAt(this.cursor)) {
|
|
1062
|
-
this.togglePasteExpansion();
|
|
1063
|
-
}
|
|
1064
|
-
else {
|
|
1065
|
-
this.toggleThinking();
|
|
1066
|
-
}
|
|
1067
|
-
return true;
|
|
1068
|
-
case 'escape':
|
|
1069
|
-
// Esc: interrupt if streaming, otherwise clear buffer
|
|
1070
|
-
if (this.mode === 'streaming') {
|
|
1071
|
-
this.emit('interrupt');
|
|
1072
|
-
}
|
|
1073
|
-
else if (this.buffer.length > 0) {
|
|
1074
|
-
this.clear();
|
|
1075
|
-
}
|
|
885
|
+
this.insertText(' ');
|
|
1076
886
|
return true;
|
|
1077
887
|
}
|
|
1078
888
|
return false;
|
|
@@ -1090,7 +900,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1090
900
|
this.insertPlainText(chunk, insertPos);
|
|
1091
901
|
this.cursor = insertPos + chunk.length;
|
|
1092
902
|
this.emit('change', this.buffer);
|
|
1093
|
-
this.updateSuggestions();
|
|
1094
903
|
this.scheduleRender();
|
|
1095
904
|
}
|
|
1096
905
|
insertNewline() {
|
|
@@ -1115,7 +924,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1115
924
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
1116
925
|
}
|
|
1117
926
|
this.emit('change', this.buffer);
|
|
1118
|
-
this.updateSuggestions();
|
|
1119
927
|
this.scheduleRender();
|
|
1120
928
|
}
|
|
1121
929
|
deleteForward() {
|
|
@@ -1365,7 +1173,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1365
1173
|
if (available <= 0)
|
|
1366
1174
|
return;
|
|
1367
1175
|
const chunk = clean.slice(0, available);
|
|
1368
|
-
|
|
1176
|
+
const isMultiline = isMultilinePaste(chunk);
|
|
1177
|
+
const isShortMultiline = isMultiline && this.shouldInlineMultiline(chunk);
|
|
1178
|
+
if (isMultiline && !isShortMultiline) {
|
|
1369
1179
|
this.insertPastePlaceholder(chunk);
|
|
1370
1180
|
}
|
|
1371
1181
|
else {
|
|
@@ -1385,6 +1195,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1385
1195
|
return;
|
|
1386
1196
|
this.applyScrollRegion();
|
|
1387
1197
|
this.scrollRegionActive = true;
|
|
1198
|
+
this.forceRender();
|
|
1388
1199
|
}
|
|
1389
1200
|
disableScrollRegion() {
|
|
1390
1201
|
if (!this.scrollRegionActive)
|
|
@@ -1535,17 +1346,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
1535
1346
|
this.shiftPlaceholders(position, text.length);
|
|
1536
1347
|
this.buffer = this.buffer.slice(0, position) + text + this.buffer.slice(position);
|
|
1537
1348
|
}
|
|
1349
|
+
shouldInlineMultiline(content) {
|
|
1350
|
+
const lines = content.split('\n').length;
|
|
1351
|
+
const maxInlineLines = 4;
|
|
1352
|
+
const maxInlineChars = 240;
|
|
1353
|
+
return lines <= maxInlineLines && content.length <= maxInlineChars;
|
|
1354
|
+
}
|
|
1538
1355
|
findPlaceholderAt(position) {
|
|
1539
1356
|
return this.pastePlaceholders.find((ph) => position >= ph.start && position < ph.end) ?? null;
|
|
1540
1357
|
}
|
|
1541
|
-
buildPlaceholder(
|
|
1358
|
+
buildPlaceholder(lineCount) {
|
|
1542
1359
|
const id = ++this.pasteCounter;
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
const preview = summary.preview.length > 30
|
|
1546
|
-
? `${summary.preview.slice(0, 30)}...`
|
|
1547
|
-
: summary.preview;
|
|
1548
|
-
const placeholder = `[📋 #${id}${lang} ${summary.lineCount}L] "${preview}"`;
|
|
1360
|
+
const plural = lineCount === 1 ? '' : 's';
|
|
1361
|
+
const placeholder = `[Pasted text #${id} +${lineCount} line${plural}]`;
|
|
1549
1362
|
return { id, placeholder };
|
|
1550
1363
|
}
|
|
1551
1364
|
insertPastePlaceholder(content) {
|
|
@@ -1553,67 +1366,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1553
1366
|
if (available <= 0)
|
|
1554
1367
|
return;
|
|
1555
1368
|
const cleanContent = content.slice(0, available);
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
if (summary.lineCount < 5) {
|
|
1559
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1560
|
-
const insertPos = placeholder && this.cursor > placeholder.start ? placeholder.end : this.cursor;
|
|
1561
|
-
this.insertPlainText(cleanContent, insertPos);
|
|
1562
|
-
this.cursor = insertPos + cleanContent.length;
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
const { id, placeholder } = this.buildPlaceholder(summary);
|
|
1369
|
+
const lineCount = cleanContent.split('\n').length;
|
|
1370
|
+
const { id, placeholder } = this.buildPlaceholder(lineCount);
|
|
1566
1371
|
const insertPos = this.cursor;
|
|
1567
1372
|
this.shiftPlaceholders(insertPos, placeholder.length);
|
|
1568
1373
|
this.pastePlaceholders.push({
|
|
1569
1374
|
id,
|
|
1570
1375
|
content: cleanContent,
|
|
1571
|
-
lineCount
|
|
1376
|
+
lineCount,
|
|
1572
1377
|
placeholder,
|
|
1573
1378
|
start: insertPos,
|
|
1574
1379
|
end: insertPos + placeholder.length,
|
|
1575
|
-
summary,
|
|
1576
|
-
expanded: false,
|
|
1577
1380
|
});
|
|
1578
1381
|
this.buffer = this.buffer.slice(0, insertPos) + placeholder + this.buffer.slice(insertPos);
|
|
1579
1382
|
this.cursor = insertPos + placeholder.length;
|
|
1580
1383
|
}
|
|
1581
|
-
/**
|
|
1582
|
-
* Toggle expansion of a paste placeholder at the current cursor position.
|
|
1583
|
-
* When expanded, shows first 3 and last 2 lines of the content.
|
|
1584
|
-
*/
|
|
1585
|
-
togglePasteExpansion() {
|
|
1586
|
-
const placeholder = this.findPlaceholderAt(this.cursor);
|
|
1587
|
-
if (!placeholder)
|
|
1588
|
-
return false;
|
|
1589
|
-
placeholder.expanded = !placeholder.expanded;
|
|
1590
|
-
// Update the placeholder text in buffer
|
|
1591
|
-
const newPlaceholder = placeholder.expanded
|
|
1592
|
-
? this.buildExpandedPlaceholder(placeholder)
|
|
1593
|
-
: this.buildPlaceholder(placeholder.summary).placeholder;
|
|
1594
|
-
const lengthDiff = newPlaceholder.length - placeholder.placeholder.length;
|
|
1595
|
-
// Update buffer
|
|
1596
|
-
this.buffer =
|
|
1597
|
-
this.buffer.slice(0, placeholder.start) +
|
|
1598
|
-
newPlaceholder +
|
|
1599
|
-
this.buffer.slice(placeholder.end);
|
|
1600
|
-
// Update placeholder tracking
|
|
1601
|
-
placeholder.placeholder = newPlaceholder;
|
|
1602
|
-
placeholder.end = placeholder.start + newPlaceholder.length;
|
|
1603
|
-
// Shift other placeholders
|
|
1604
|
-
this.shiftPlaceholders(placeholder.end, lengthDiff, placeholder.id);
|
|
1605
|
-
this.scheduleRender();
|
|
1606
|
-
return true;
|
|
1607
|
-
}
|
|
1608
|
-
buildExpandedPlaceholder(ph) {
|
|
1609
|
-
const lines = ph.content.split('\n');
|
|
1610
|
-
const firstLines = lines.slice(0, 3).map(l => l.slice(0, 60)).join('\n');
|
|
1611
|
-
const lastLines = lines.length > 5
|
|
1612
|
-
? '\n...\n' + lines.slice(-2).map(l => l.slice(0, 60)).join('\n')
|
|
1613
|
-
: '';
|
|
1614
|
-
const lang = ph.summary.language ? ` ${ph.summary.language.toUpperCase()}` : '';
|
|
1615
|
-
return `[📋 #${ph.id}${lang} ${ph.lineCount}L ▼]\n${firstLines}${lastLines}\n[/📋 #${ph.id}]`;
|
|
1616
|
-
}
|
|
1617
1384
|
deletePlaceholder(placeholder) {
|
|
1618
1385
|
const length = placeholder.end - placeholder.start;
|
|
1619
1386
|
this.buffer = this.buffer.slice(0, placeholder.start) + this.buffer.slice(placeholder.end);
|
|
@@ -1621,7 +1388,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1621
1388
|
this.shiftPlaceholders(placeholder.end, -length, placeholder.id);
|
|
1622
1389
|
this.cursor = placeholder.start;
|
|
1623
1390
|
}
|
|
1624
|
-
updateContextUsage(value) {
|
|
1391
|
+
updateContextUsage(value, autoCompactThreshold) {
|
|
1392
|
+
if (typeof autoCompactThreshold === 'number' && Number.isFinite(autoCompactThreshold)) {
|
|
1393
|
+
const boundedThreshold = Math.max(1, Math.min(100, Math.round(autoCompactThreshold)));
|
|
1394
|
+
this.contextAutoCompactThreshold = boundedThreshold;
|
|
1395
|
+
}
|
|
1625
1396
|
if (value === null || !Number.isFinite(value)) {
|
|
1626
1397
|
this.contextUsage = null;
|
|
1627
1398
|
}
|