erosolar-cli 1.7.189 → 1.7.191
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/dist/shell/interactiveShell.d.ts +29 -126
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +163 -1438
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +186 -0
- package/dist/shell/terminalInput.d.ts.map +1 -0
- package/dist/shell/terminalInput.js +855 -0
- package/dist/shell/terminalInput.js.map +1 -0
- package/dist/shell/terminalInputAdapter.d.ts +94 -0
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -0
- package/dist/shell/terminalInputAdapter.js +187 -0
- package/dist/shell/terminalInputAdapter.js.map +1 -0
- package/dist/ui/keyboardShortcuts.js +1 -1
- package/dist/ui/keyboardShortcuts.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +11 -13
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +22 -57
- package/dist/ui/persistentPrompt.js.map +1 -1
- package/dist/ui/shortcutsHelp.js +1 -1
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import readline from 'node:readline';
|
|
2
1
|
import { stdin as input, stdout as output, exit } from 'node:process';
|
|
3
2
|
import { exec } from 'node:child_process';
|
|
4
3
|
import { promisify } from 'node:util';
|
|
@@ -8,8 +7,6 @@ import { getContextWindowTokens } from '../core/contextWindow.js';
|
|
|
8
7
|
import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
|
|
9
8
|
import { saveActiveProfilePreference, saveModelPreference, loadToolSettings, saveToolSettings, clearToolSettings, clearActiveProfilePreference, loadSessionPreferences, saveSessionPreferences, } from '../core/preferences.js';
|
|
10
9
|
import { buildEnabledToolSet, evaluateToolPermissions, getToolToggleOptions, } from '../capabilities/toolRegistry.js';
|
|
11
|
-
import { BracketedPasteManager } from './bracketedPasteManager.js';
|
|
12
|
-
import { ComposableMessageBuilder } from './composableMessage.js';
|
|
13
10
|
import { detectApiKeyError } from '../core/errors/apiKeyErrors.js';
|
|
14
11
|
import { buildWorkspaceContext } from '../workspace.js';
|
|
15
12
|
import { buildInteractiveSystemPrompt } from './systemPrompt.js';
|
|
@@ -21,12 +18,10 @@ import { buildCustomCommandPrompt, loadCustomSlashCommands, } from '../core/cust
|
|
|
21
18
|
import { SkillRepository } from '../skills/skillRepository.js';
|
|
22
19
|
import { createSkillTools } from '../tools/skillTools.js';
|
|
23
20
|
import { FileChangeTracker } from './fileChangeTracker.js';
|
|
24
|
-
import { PersistentPrompt, PinnedChatBox } from '../ui/persistentPrompt.js';
|
|
25
21
|
import { formatShortcutsHelp } from '../ui/shortcutsHelp.js';
|
|
26
22
|
import { MetricsTracker } from '../alpha-zero/index.js';
|
|
27
23
|
import { listAvailablePlugins } from '../plugins/index.js';
|
|
28
|
-
import {
|
|
29
|
-
import { getUnifiedChatBox } from './unifiedChatBox.js';
|
|
24
|
+
import { TerminalInputAdapter } from './terminalInputAdapter.js';
|
|
30
25
|
const execAsync = promisify(exec);
|
|
31
26
|
const DROPDOWN_COLORS = [
|
|
32
27
|
theme.primary,
|
|
@@ -54,9 +49,6 @@ const BASE_SLASH_COMMANDS = getSlashCommands().map((cmd) => ({
|
|
|
54
49
|
// Load PROVIDER_LABELS from centralized schema
|
|
55
50
|
const PROVIDER_LABELS = Object.fromEntries(getProviders().map((provider) => [provider.id, provider.label]));
|
|
56
51
|
// Allow enough time for paste detection to kick in before flushing buffered lines
|
|
57
|
-
const MULTILINE_INPUT_FLUSH_DELAY_MS = 120;
|
|
58
|
-
const BRACKETED_PASTE_ENABLE = '\u001b[?2004h';
|
|
59
|
-
const BRACKETED_PASTE_DISABLE = '\u001b[?2004l';
|
|
60
52
|
const CONTEXT_USAGE_THRESHOLD = 0.9;
|
|
61
53
|
const CONTEXT_RECENT_MESSAGE_COUNT = 12;
|
|
62
54
|
const CONTEXT_CLEANUP_CHARS_PER_CHUNK = 6000;
|
|
@@ -68,7 +60,6 @@ const CONTEXT_CLEANUP_SYSTEM_PROMPT = `You condense earlier IDE collaboration lo
|
|
|
68
60
|
- Keep the response under roughly 200 words, prefer short bullet lists.
|
|
69
61
|
- Never call tools or run shell commands; respond with plain Markdown text only.`;
|
|
70
62
|
export class InteractiveShell {
|
|
71
|
-
rl;
|
|
72
63
|
agent = null;
|
|
73
64
|
profile;
|
|
74
65
|
profileLabel;
|
|
@@ -78,18 +69,14 @@ export class InteractiveShell {
|
|
|
78
69
|
workspaceOptions;
|
|
79
70
|
sessionState;
|
|
80
71
|
isProcessing = false;
|
|
72
|
+
shuttingDown = false;
|
|
81
73
|
pendingInteraction = null;
|
|
82
74
|
pendingSecretRetry = null;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
bracketedPaste;
|
|
86
|
-
bracketedPasteEnabled = false;
|
|
87
|
-
composableMessage = new ComposableMessageBuilder();
|
|
75
|
+
terminalInput;
|
|
76
|
+
currentInput = '';
|
|
88
77
|
pendingCleanup = null;
|
|
89
78
|
cleanupInProgress = false;
|
|
90
79
|
slashPreviewVisible = false;
|
|
91
|
-
keypressHandler = null;
|
|
92
|
-
rawDataHandler = null;
|
|
93
80
|
skillRepository;
|
|
94
81
|
skillToolHandlers = new Map();
|
|
95
82
|
thinkingMode = 'balanced';
|
|
@@ -99,7 +86,6 @@ export class InteractiveShell {
|
|
|
99
86
|
statusTracker;
|
|
100
87
|
uiAdapter;
|
|
101
88
|
_fileChangeTracker = new FileChangeTracker(); // Reserved for future file tracking features
|
|
102
|
-
persistentPrompt;
|
|
103
89
|
alphaZeroMetrics; // Alpha Zero 2 performance tracking
|
|
104
90
|
statusSubscription = null;
|
|
105
91
|
followUpQueue = [];
|
|
@@ -117,11 +103,6 @@ export class InteractiveShell {
|
|
|
117
103
|
customCommandMap;
|
|
118
104
|
sessionRestoreConfig;
|
|
119
105
|
_enabledPlugins;
|
|
120
|
-
pinnedChatBox;
|
|
121
|
-
readlineOutputSuppressed = false;
|
|
122
|
-
originalStdoutWrite = null;
|
|
123
|
-
streamHandler;
|
|
124
|
-
unifiedChatBox;
|
|
125
106
|
// Cached provider status for unified status bar display after streaming
|
|
126
107
|
cachedProviderStatus = [];
|
|
127
108
|
// Auto-test tracking
|
|
@@ -180,19 +161,10 @@ export class InteractiveShell {
|
|
|
180
161
|
// Set up file change tracking callback
|
|
181
162
|
this.uiAdapter.setFileChangeCallback((path, type, additions, removals) => {
|
|
182
163
|
this._fileChangeTracker.recordChange(path, type, additions, removals);
|
|
183
|
-
// Update persistent prompt status bar with file changes
|
|
184
|
-
this.updatePersistentPromptFileChanges();
|
|
185
164
|
});
|
|
186
|
-
// Set up tool status callback to update
|
|
165
|
+
// Set up tool status callback to update status during tool execution
|
|
187
166
|
this.uiAdapter.setToolStatusCallback((status) => {
|
|
188
|
-
|
|
189
|
-
this.pinnedChatBox.setStatusMessage(status);
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
// Clear status but keep processing indicator if still processing
|
|
193
|
-
this.pinnedChatBox.setStatusMessage(null);
|
|
194
|
-
}
|
|
195
|
-
this.pinnedChatBox.forceRender();
|
|
167
|
+
this.updateStatusMessage(status ?? null);
|
|
196
168
|
});
|
|
197
169
|
this.skillRepository = new SkillRepository({
|
|
198
170
|
workingDir: this.workingDir,
|
|
@@ -201,62 +173,18 @@ export class InteractiveShell {
|
|
|
201
173
|
for (const definition of createSkillTools({ repository: this.skillRepository })) {
|
|
202
174
|
this.skillToolHandlers.set(definition.name, definition.handler);
|
|
203
175
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
historySize: 100, // Enable native readline history
|
|
211
|
-
});
|
|
212
|
-
// Wrap prompt() so we always re-arm stdin before showing it
|
|
213
|
-
const originalPrompt = this.rl.prompt.bind(this.rl);
|
|
214
|
-
this.rl.prompt = (preserveCursor) => {
|
|
215
|
-
this.ensureReadlineReady();
|
|
216
|
-
originalPrompt(preserveCursor);
|
|
217
|
-
};
|
|
218
|
-
// Initialize persistent prompt (Claude Code style)
|
|
219
|
-
this.persistentPrompt = new PersistentPrompt(output, formatUserPrompt(this.profileLabel || this.profile));
|
|
220
|
-
// Initialize pinned chat box for always-visible input during AI processing
|
|
221
|
-
// Note: Pass empty string for promptText since render() adds its own '> ' prefix
|
|
222
|
-
this.pinnedChatBox = new PinnedChatBox(output, '', // Don't pass formatUserPrompt - the render method adds '> ' prefix
|
|
223
|
-
{
|
|
224
|
-
onCommandQueued: (cmd) => {
|
|
225
|
-
// Bridge to existing follow-up queue system
|
|
226
|
-
this.followUpQueue.push({
|
|
227
|
-
type: cmd.type === 'slash' ? 'request' : cmd.type,
|
|
228
|
-
text: cmd.text,
|
|
229
|
-
});
|
|
230
|
-
this.refreshQueueIndicators();
|
|
231
|
-
},
|
|
232
|
-
onInputSubmit: (input) => {
|
|
233
|
-
// Process the input through the normal flow
|
|
234
|
-
this.processInputBlock(input).catch((err) => {
|
|
235
|
-
display.showError(err instanceof Error ? err.message : String(err), err);
|
|
236
|
-
});
|
|
237
|
-
},
|
|
176
|
+
// Initialize unified terminal input handler
|
|
177
|
+
this.terminalInput = new TerminalInputAdapter(input, output, {
|
|
178
|
+
onSubmit: (text) => this.processInput(text),
|
|
179
|
+
onQueue: (text) => this.handleQueuedInput(text),
|
|
180
|
+
onInterrupt: () => this.handleInterrupt(),
|
|
181
|
+
onChange: (text) => this.handleInputChange(text),
|
|
238
182
|
});
|
|
239
|
-
// Register pinned chat box with display's output interceptor system
|
|
240
|
-
// This ensures the pinned area is cleared before output and re-rendered after,
|
|
241
|
-
// preventing ghost lines when the terminal scrolls
|
|
242
|
-
this.pinnedChatBox.registerOutputInterceptor(display);
|
|
243
|
-
// Initialize Claude Code style stream handler for natural output flow
|
|
244
|
-
// Key insight: No cursor manipulation during streaming - output just appends
|
|
245
|
-
this.streamHandler = getClaudeCodeStreamHandler();
|
|
246
|
-
this.streamHandler.attachToReadline(this.rl);
|
|
247
|
-
// Initialize UnifiedChatBox - Claude Code exact style
|
|
248
|
-
// The chat box is always at the bottom naturally through readline
|
|
249
|
-
// During streaming: output flows up, typed input queued invisibly
|
|
250
|
-
// After streaming: new prompt appears at bottom naturally
|
|
251
|
-
this.unifiedChatBox = getUnifiedChatBox();
|
|
252
|
-
this.unifiedChatBox.attachToReadline(this.rl);
|
|
253
|
-
this.unifiedChatBox.setPrompt(formatUserPrompt(this.profileLabel || this.profile));
|
|
254
183
|
// Initialize Alpha Zero 2 metrics tracking
|
|
255
184
|
this.alphaZeroMetrics = new MetricsTracker(`${this.profile}-${Date.now()}`);
|
|
256
185
|
this.setupStatusTracking();
|
|
257
186
|
this.refreshContextGauge();
|
|
258
|
-
this.
|
|
259
|
-
this.bracketedPaste = new BracketedPasteManager(this.bracketedPasteEnabled);
|
|
187
|
+
this.terminalInput.start();
|
|
260
188
|
this.rebuildAgent();
|
|
261
189
|
this.setupHandlers();
|
|
262
190
|
this.refreshBannerSessionInfo();
|
|
@@ -303,17 +231,88 @@ export class InteractiveShell {
|
|
|
303
231
|
this.sessionResumeNotice = null;
|
|
304
232
|
}
|
|
305
233
|
async start(initialPrompt) {
|
|
306
|
-
// Claude Code style: Just use readline's prompt
|
|
307
|
-
// The prompt naturally appears at the bottom
|
|
308
|
-
// No complex UI rendering needed - terminal handles it naturally
|
|
309
234
|
if (initialPrompt) {
|
|
310
235
|
display.newLine();
|
|
311
236
|
console.log(`${formatUserPrompt(this.profileLabel || this.profile)}${initialPrompt}`);
|
|
312
237
|
await this.processInputBlock(initialPrompt);
|
|
313
238
|
return;
|
|
314
239
|
}
|
|
315
|
-
//
|
|
316
|
-
this.
|
|
240
|
+
// Ensure the terminal input is visible
|
|
241
|
+
this.terminalInput.render();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* TerminalInputAdapter submit handler
|
|
245
|
+
*/
|
|
246
|
+
processInput(text) {
|
|
247
|
+
this.handleInputChange('');
|
|
248
|
+
void this.processInputBlock(text).catch((err) => {
|
|
249
|
+
display.showError(err instanceof Error ? err.message : String(err), err);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* TerminalInputAdapter queue handler (streaming mode)
|
|
254
|
+
*/
|
|
255
|
+
handleQueuedInput(text) {
|
|
256
|
+
// Keep adapter queue trimmed so hints stay accurate
|
|
257
|
+
this.terminalInput.dequeue();
|
|
258
|
+
this.followUpQueue.push({ type: 'request', text });
|
|
259
|
+
display.showInfo(`Queued: "${text}"`);
|
|
260
|
+
this.refreshQueueIndicators();
|
|
261
|
+
this.scheduleQueueProcessing();
|
|
262
|
+
this.handleInputChange('');
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* TerminalInputAdapter change handler
|
|
266
|
+
*/
|
|
267
|
+
handleInputChange(text) {
|
|
268
|
+
this.currentInput = text;
|
|
269
|
+
this.handleSlashCommandPreviewChange();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* TerminalInputAdapter interrupt handler (Ctrl+C with empty buffer)
|
|
273
|
+
*/
|
|
274
|
+
handleInterrupt() {
|
|
275
|
+
if (this.isProcessing && this.agent) {
|
|
276
|
+
this.agent.requestCancellation();
|
|
277
|
+
display.showWarning('Cancelling current operation... (Ctrl+C again to force quit)');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.shutdown();
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Gracefully tear down the shell and exit
|
|
284
|
+
*/
|
|
285
|
+
shutdown() {
|
|
286
|
+
if (this.shuttingDown) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.shuttingDown = true;
|
|
290
|
+
// Stop any active spinner to prevent process hang
|
|
291
|
+
display.stopThinking(false);
|
|
292
|
+
this.teardownStatusTracking();
|
|
293
|
+
// Clear any pending cleanup to prevent hanging
|
|
294
|
+
this.pendingCleanup = null;
|
|
295
|
+
// Dispose terminal input handler
|
|
296
|
+
this.terminalInput.dispose();
|
|
297
|
+
// Dispose unified UI adapter
|
|
298
|
+
this.uiAdapter.dispose();
|
|
299
|
+
display.newLine();
|
|
300
|
+
const highlightedEmail = theme.info('support@ero.solar');
|
|
301
|
+
const infoMessage = [
|
|
302
|
+
'Thank you to Anthropic for allowing me to use Claude Code to build erosolar-cli.',
|
|
303
|
+
'',
|
|
304
|
+
`Email ${highlightedEmail} with any bugs or feedback`,
|
|
305
|
+
'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
|
|
306
|
+
'npm: https://www.npmjs.com/package/erosolar-cli',
|
|
307
|
+
].join('\n');
|
|
308
|
+
display.showInfo(infoMessage);
|
|
309
|
+
exit(0);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Update status bar message
|
|
313
|
+
*/
|
|
314
|
+
updateStatusMessage(_message) {
|
|
315
|
+
this.terminalInput.render();
|
|
317
316
|
}
|
|
318
317
|
async handleToolSettingsInput(input) {
|
|
319
318
|
const pending = this.pendingInteraction;
|
|
@@ -323,26 +322,26 @@ export class InteractiveShell {
|
|
|
323
322
|
const trimmed = input.trim();
|
|
324
323
|
if (!trimmed) {
|
|
325
324
|
display.showWarning('Enter a number, "save", "defaults", or "cancel".');
|
|
326
|
-
this.
|
|
325
|
+
this.terminalInput.render();
|
|
327
326
|
return;
|
|
328
327
|
}
|
|
329
328
|
const normalized = trimmed.toLowerCase();
|
|
330
329
|
if (normalized === 'cancel') {
|
|
331
330
|
this.pendingInteraction = null;
|
|
332
331
|
display.showInfo('Tool selection cancelled.');
|
|
333
|
-
this.
|
|
332
|
+
this.terminalInput.render();
|
|
334
333
|
return;
|
|
335
334
|
}
|
|
336
335
|
if (normalized === 'defaults') {
|
|
337
336
|
pending.selection = buildEnabledToolSet(null);
|
|
338
337
|
this.renderToolMenu(pending);
|
|
339
|
-
this.
|
|
338
|
+
this.terminalInput.render();
|
|
340
339
|
return;
|
|
341
340
|
}
|
|
342
341
|
if (normalized === 'save') {
|
|
343
342
|
await this.persistToolSelection(pending);
|
|
344
343
|
this.pendingInteraction = null;
|
|
345
|
-
this.
|
|
344
|
+
this.terminalInput.render();
|
|
346
345
|
return;
|
|
347
346
|
}
|
|
348
347
|
const choice = Number.parseInt(trimmed, 10);
|
|
@@ -360,11 +359,11 @@ export class InteractiveShell {
|
|
|
360
359
|
}
|
|
361
360
|
this.renderToolMenu(pending);
|
|
362
361
|
}
|
|
363
|
-
this.
|
|
362
|
+
this.terminalInput.render();
|
|
364
363
|
return;
|
|
365
364
|
}
|
|
366
365
|
display.showWarning('Enter a number, "save", "defaults", or "cancel".');
|
|
367
|
-
this.
|
|
366
|
+
this.terminalInput.render();
|
|
368
367
|
}
|
|
369
368
|
async persistToolSelection(interaction) {
|
|
370
369
|
if (setsEqual(interaction.selection, interaction.initialSelection)) {
|
|
@@ -391,36 +390,36 @@ export class InteractiveShell {
|
|
|
391
390
|
if (!this.agentMenu) {
|
|
392
391
|
this.pendingInteraction = null;
|
|
393
392
|
display.showWarning('Agent selection is unavailable in this CLI.');
|
|
394
|
-
this.
|
|
393
|
+
this.terminalInput.render();
|
|
395
394
|
return;
|
|
396
395
|
}
|
|
397
396
|
const trimmed = input.trim();
|
|
398
397
|
if (!trimmed) {
|
|
399
398
|
display.showWarning('Enter a number or type "cancel".');
|
|
400
|
-
this.
|
|
399
|
+
this.terminalInput.render();
|
|
401
400
|
return;
|
|
402
401
|
}
|
|
403
402
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
404
403
|
this.pendingInteraction = null;
|
|
405
404
|
display.showInfo('Agent selection cancelled.');
|
|
406
|
-
this.
|
|
405
|
+
this.terminalInput.render();
|
|
407
406
|
return;
|
|
408
407
|
}
|
|
409
408
|
const choice = Number.parseInt(trimmed, 10);
|
|
410
409
|
if (!Number.isFinite(choice)) {
|
|
411
410
|
display.showWarning('Please enter a valid number.');
|
|
412
|
-
this.
|
|
411
|
+
this.terminalInput.render();
|
|
413
412
|
return;
|
|
414
413
|
}
|
|
415
414
|
const option = pending.options[choice - 1];
|
|
416
415
|
if (!option) {
|
|
417
416
|
display.showWarning('That option is not available.');
|
|
418
|
-
this.
|
|
417
|
+
this.terminalInput.render();
|
|
419
418
|
return;
|
|
420
419
|
}
|
|
421
420
|
await this.persistAgentSelection(option.name);
|
|
422
421
|
this.pendingInteraction = null;
|
|
423
|
-
this.
|
|
422
|
+
this.terminalInput.render();
|
|
424
423
|
}
|
|
425
424
|
async persistAgentSelection(profileName) {
|
|
426
425
|
if (!this.agentMenu) {
|
|
@@ -442,342 +441,12 @@ export class InteractiveShell {
|
|
|
442
441
|
display.showInfo(`${this.agentMenuLabel(profileName)} will load the next time you start the CLI. Restart to switch now.`);
|
|
443
442
|
}
|
|
444
443
|
setupHandlers() {
|
|
445
|
-
// Set up raw data interception for bracketed paste
|
|
446
|
-
this.setupRawPasteHandler();
|
|
447
|
-
this.rl.on('line', (line) => {
|
|
448
|
-
// If we're capturing raw paste data, ignore readline line events
|
|
449
|
-
// (they've already been handled by the raw data handler)
|
|
450
|
-
if (this.bracketedPaste.isCapturingRaw()) {
|
|
451
|
-
this.resetBufferedInputLines();
|
|
452
|
-
// Clear the line that readline just echoed - move up and clear
|
|
453
|
-
output.write('\x1b[A\r\x1b[K');
|
|
454
|
-
// Show paste progress (this will update in place)
|
|
455
|
-
this.showMultiLinePastePreview(this.bracketedPaste.getRawBufferLineCount(), this.bracketedPaste.getRawBufferPreview());
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
// If a paste was just captured via raw handler, ignore readline's line events
|
|
459
|
-
// (readline still emits line events after we've already captured the paste)
|
|
460
|
-
if (this.bracketedPaste.shouldIgnoreLineEvent()) {
|
|
461
|
-
this.resetBufferedInputLines();
|
|
462
|
-
// Clear the echoed line that readline wrote
|
|
463
|
-
output.write('\x1b[A\r\x1b[K');
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const normalized = this.bracketedPaste.process(line);
|
|
467
|
-
if (normalized.handled) {
|
|
468
|
-
// Clear any partially buffered lines so we don't auto-submit the first line of a paste
|
|
469
|
-
this.resetBufferedInputLines();
|
|
470
|
-
// If still accumulating multi-line paste, show preview
|
|
471
|
-
if (normalized.isPending) {
|
|
472
|
-
// Clear the line that readline just echoed - move up and clear
|
|
473
|
-
output.write('\x1b[A\r\x1b[K');
|
|
474
|
-
this.showMultiLinePastePreview(normalized.lineCount || 0, normalized.preview);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
// Paste complete, store or submit the full content
|
|
478
|
-
if (typeof normalized.result === 'string') {
|
|
479
|
-
this.clearMultiLinePastePreview();
|
|
480
|
-
const lineCount = normalized.lineCount ?? normalized.result.split('\n').length;
|
|
481
|
-
// All pastes (single or multi-line) are captured for confirmation before submit
|
|
482
|
-
void this.capturePaste(normalized.result, lineCount);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
this.enqueueUserInput(line);
|
|
488
|
-
// Clear input in persistent prompt after submission
|
|
489
|
-
this.persistentPrompt.updateInput('', 0);
|
|
490
|
-
});
|
|
491
|
-
this.rl.on('close', () => {
|
|
492
|
-
// Stop any active spinner to prevent process hang
|
|
493
|
-
display.stopThinking(false);
|
|
494
|
-
this.disableBracketedPasteMode();
|
|
495
|
-
this.teardownStatusTracking();
|
|
496
|
-
// Remove keypress listener to prevent memory leaks
|
|
497
|
-
const inputStream = input;
|
|
498
|
-
if (inputStream && this.keypressHandler) {
|
|
499
|
-
inputStream.off('keypress', this.keypressHandler);
|
|
500
|
-
this.keypressHandler = null;
|
|
501
|
-
}
|
|
502
|
-
// Restore original stdin emit (cleanup from paste interception)
|
|
503
|
-
if (this.rawDataHandler) {
|
|
504
|
-
this.rawDataHandler(); // This restores the original emit function
|
|
505
|
-
this.rawDataHandler = null;
|
|
506
|
-
}
|
|
507
|
-
// Clear any pending cleanup to prevent hanging
|
|
508
|
-
this.pendingCleanup = null;
|
|
509
|
-
// Dispose persistent prompt
|
|
510
|
-
this.persistentPrompt.dispose();
|
|
511
|
-
// Dispose Claude Code stream handler
|
|
512
|
-
this.streamHandler.dispose();
|
|
513
|
-
// Dispose unified chat box
|
|
514
|
-
this.unifiedChatBox.dispose();
|
|
515
|
-
// Dispose unified UI adapter
|
|
516
|
-
this.uiAdapter.dispose();
|
|
517
|
-
display.newLine();
|
|
518
|
-
const highlightedEmail = theme.info('support@ero.solar');
|
|
519
|
-
const infoMessage = [
|
|
520
|
-
'Thank you to Anthropic for allowing me to use Claude Code to build erosolar-cli.',
|
|
521
|
-
'',
|
|
522
|
-
`Email ${highlightedEmail} with any bugs or feedback`,
|
|
523
|
-
'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
|
|
524
|
-
'npm: https://www.npmjs.com/package/erosolar-cli',
|
|
525
|
-
].join('\n');
|
|
526
|
-
display.showInfo(infoMessage);
|
|
527
|
-
exit(0);
|
|
528
|
-
});
|
|
529
444
|
// Handle terminal resize
|
|
530
445
|
output.on('resize', () => {
|
|
531
|
-
this.
|
|
532
|
-
this.pinnedChatBox.handleResize();
|
|
533
|
-
});
|
|
534
|
-
this.setupSlashCommandPreviewHandler();
|
|
535
|
-
// Show initial persistent prompt
|
|
536
|
-
this.persistentPrompt.show();
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Set up raw stdin data interception for bracketed paste mode.
|
|
540
|
-
* This intercepts data before readline processes it, allowing us to
|
|
541
|
-
* capture complete multi-line pastes without readline splitting them.
|
|
542
|
-
*/
|
|
543
|
-
setupRawPasteHandler() {
|
|
544
|
-
if (!this.bracketedPasteEnabled) {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const inputStream = input;
|
|
548
|
-
if (!inputStream || !inputStream.isTTY) {
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
// Set up callback for when a complete paste is captured
|
|
552
|
-
this.bracketedPaste.setRawPasteCallback((content) => {
|
|
553
|
-
this.clearMultiLinePastePreview();
|
|
554
|
-
const lines = content.split('\n');
|
|
555
|
-
const lineCount = lines.length;
|
|
556
|
-
// When streaming, route pastes into the pinned chat box instead of readline
|
|
557
|
-
if (this.isProcessing) {
|
|
558
|
-
this.pinnedChatBox.handlePaste(content);
|
|
559
|
-
this.pinnedChatBox.updatePersistentInput();
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
// All pastes (single or multi-line) are captured for confirmation before submit
|
|
563
|
-
void this.capturePaste(content, lineCount);
|
|
446
|
+
this.terminalInput.handleResize();
|
|
564
447
|
});
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
// Strategy: Replace stdin's 'data' event emission during paste capture.
|
|
568
|
-
const originalEmit = inputStream.emit.bind(inputStream);
|
|
569
|
-
inputStream.emit = (event, ...args) => {
|
|
570
|
-
if (event === 'data' && args[0]) {
|
|
571
|
-
const data = args[0];
|
|
572
|
-
const str = typeof data === 'string' ? data : data.toString();
|
|
573
|
-
const result = this.bracketedPaste.processRawData(str);
|
|
574
|
-
if (result.consumed) {
|
|
575
|
-
// Data was consumed by paste handler - don't pass to readline
|
|
576
|
-
// If there's passThrough data, emit that instead
|
|
577
|
-
if (result.passThrough) {
|
|
578
|
-
return originalEmit('data', Buffer.from(result.passThrough));
|
|
579
|
-
}
|
|
580
|
-
return true; // Event "handled" but not passed to other listeners
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// Pass through all other events and non-paste data normally
|
|
584
|
-
return originalEmit(event, ...args);
|
|
585
|
-
};
|
|
586
|
-
// Store reference for cleanup
|
|
587
|
-
this.rawDataHandler = () => {
|
|
588
|
-
inputStream.emit = originalEmit;
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
setupSlashCommandPreviewHandler() {
|
|
592
|
-
const inputStream = input;
|
|
593
|
-
if (!inputStream || typeof inputStream.on !== 'function' || !inputStream.isTTY) {
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
if (inputStream.listenerCount('keypress') === 0) {
|
|
597
|
-
readline.emitKeypressEvents(inputStream, this.rl);
|
|
598
|
-
}
|
|
599
|
-
// Ensure raw mode for keypress events
|
|
600
|
-
if (inputStream.setRawMode && !inputStream.isRaw) {
|
|
601
|
-
inputStream.setRawMode(true);
|
|
602
|
-
}
|
|
603
|
-
this.keypressHandler = (_str, key) => {
|
|
604
|
-
// Handle special keys
|
|
605
|
-
if (key) {
|
|
606
|
-
// Shift+Tab: Cycle paste preview options if paste blocks exist, otherwise profile switching
|
|
607
|
-
if (key.name === 'tab' && key.shift) {
|
|
608
|
-
// Check if there are paste blocks to cycle through
|
|
609
|
-
const pasteBlockCount = this.composableMessage.getPasteBlockCount();
|
|
610
|
-
if (pasteBlockCount > 0) {
|
|
611
|
-
this.cyclePastePreview();
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
// Fall through to profile switching if no paste blocks
|
|
615
|
-
if (this.agentMenu) {
|
|
616
|
-
this.showProfileSwitcher();
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
// Tab: Autocomplete slash commands
|
|
621
|
-
if (key.name === 'tab' && !key.shift && !key.ctrl) {
|
|
622
|
-
this.handleTabCompletion();
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
// Escape: Cancel current operation if agent is running
|
|
626
|
-
if (key.name === 'escape') {
|
|
627
|
-
if (this.isProcessing && this.agent) {
|
|
628
|
-
this.agent.requestCancellation();
|
|
629
|
-
output.write('\n');
|
|
630
|
-
display.showWarning('Cancelling current operation... (waiting for safe stop point)');
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
// If not processing, clear any queued follow-ups
|
|
634
|
-
if (this.followUpQueue.length > 0) {
|
|
635
|
-
const cleared = this.followUpQueue.length;
|
|
636
|
-
this.followUpQueue.length = 0;
|
|
637
|
-
this.refreshQueueIndicators();
|
|
638
|
-
output.write('\n');
|
|
639
|
-
display.showInfo(`Cleared ${cleared} queued follow-up${cleared === 1 ? '' : 's'}.`);
|
|
640
|
-
this.rl.prompt();
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
// Ctrl+C: Clear input first, then cancel operation if no input
|
|
645
|
-
if (key.ctrl && key.name === 'c') {
|
|
646
|
-
// During processing, check pinnedChatBox input first
|
|
647
|
-
if (this.isProcessing) {
|
|
648
|
-
const chatBoxInput = this.pinnedChatBox.getInput();
|
|
649
|
-
if (chatBoxInput.length > 0) {
|
|
650
|
-
// Clear the chat box input instead of cancelling
|
|
651
|
-
this.pinnedChatBox.clearPastedBlockState();
|
|
652
|
-
this.pinnedChatBox.updatePersistentInput();
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
// No text in chat box, cancel the agent
|
|
656
|
-
if (this.agent) {
|
|
657
|
-
this.agent.requestCancellation();
|
|
658
|
-
output.write('\n');
|
|
659
|
-
display.showWarning('Cancelling current operation... (Ctrl+C again to force quit)');
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Not processing - check readline input
|
|
664
|
-
const currentLine = this.rl.line || '';
|
|
665
|
-
if (currentLine.length > 0) {
|
|
666
|
-
// Clear the input buffer instead of exiting
|
|
667
|
-
this.rl.line = '';
|
|
668
|
-
this.rl.cursor = 0;
|
|
669
|
-
this.persistentPrompt.updateInput('', 0);
|
|
670
|
-
this.pinnedChatBox.setInput('');
|
|
671
|
-
this.pinnedChatBox.clearPastedBlockState();
|
|
672
|
-
// Clear the displayed line and show fresh prompt
|
|
673
|
-
output.write('\r\x1b[K'); // Clear current line
|
|
674
|
-
output.write('^C\n'); // Show ^C indicator
|
|
675
|
-
this.rl.prompt(); // Re-show prompt
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
// If no text in buffer, let default Ctrl+C behavior exit
|
|
679
|
-
}
|
|
680
|
-
// Ctrl+G: Edit pasted content blocks
|
|
681
|
-
if (key.ctrl && key.name === 'g') {
|
|
682
|
-
this.showPasteBlockEditor();
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
// Claude Code approach: During processing, route input to PinnedChatBox
|
|
687
|
-
// - User can type and see their input in the visible chat box at bottom
|
|
688
|
-
// - Input is queued until streaming ends
|
|
689
|
-
// - After each keystroke, we re-render the bottom area to show the input
|
|
690
|
-
if (this.isProcessing) {
|
|
691
|
-
// Handle paste: if _str contains newlines, it's a paste - don't auto-submit
|
|
692
|
-
const isPaste = typeof _str === 'string' && (_str.length > 1 || _str.includes('\n'));
|
|
693
|
-
if (key) {
|
|
694
|
-
if (key.name === 'backspace') {
|
|
695
|
-
this.pinnedChatBox.handleBackspace();
|
|
696
|
-
}
|
|
697
|
-
else if (key.name === 'delete') {
|
|
698
|
-
this.pinnedChatBox.handleDelete();
|
|
699
|
-
}
|
|
700
|
-
else if (key.name === 'left') {
|
|
701
|
-
if (key.meta || key.alt) {
|
|
702
|
-
this.pinnedChatBox.handleWordLeft();
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
this.pinnedChatBox.handleCursorLeft();
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
else if (key.name === 'right') {
|
|
709
|
-
if (key.meta || key.alt) {
|
|
710
|
-
this.pinnedChatBox.handleWordRight();
|
|
711
|
-
}
|
|
712
|
-
else {
|
|
713
|
-
this.pinnedChatBox.handleCursorRight();
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
else if (key.name === 'up') {
|
|
717
|
-
this.pinnedChatBox.handleHistoryUp();
|
|
718
|
-
}
|
|
719
|
-
else if (key.name === 'down') {
|
|
720
|
-
this.pinnedChatBox.handleHistoryDown();
|
|
721
|
-
}
|
|
722
|
-
else if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
|
|
723
|
-
this.pinnedChatBox.handleHome();
|
|
724
|
-
}
|
|
725
|
-
else if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
|
|
726
|
-
this.pinnedChatBox.handleEnd();
|
|
727
|
-
}
|
|
728
|
-
else if (key.ctrl && key.name === 'u') {
|
|
729
|
-
this.pinnedChatBox.handleDeleteToStart();
|
|
730
|
-
}
|
|
731
|
-
else if (key.ctrl && key.name === 'k') {
|
|
732
|
-
this.pinnedChatBox.handleDeleteToEnd();
|
|
733
|
-
}
|
|
734
|
-
else if (key.ctrl && key.name === 'w') {
|
|
735
|
-
this.pinnedChatBox.handleDeleteWord();
|
|
736
|
-
}
|
|
737
|
-
else if (key.name === 'return' && (key.shift || key.meta || key.ctrl)) {
|
|
738
|
-
// Shift+Enter or Option+Enter: Insert newline for multi-line input
|
|
739
|
-
this.pinnedChatBox.handleNewline();
|
|
740
|
-
}
|
|
741
|
-
else if (key.name === 'return' && !isPaste) {
|
|
742
|
-
// Queue the command - but NOT if this is part of a paste
|
|
743
|
-
const result = this.pinnedChatBox.handleSubmit();
|
|
744
|
-
if (result) {
|
|
745
|
-
const preview = result.replace(/\s+/g, ' ').trim();
|
|
746
|
-
const displayText = preview.length > 50 ? `${preview.slice(0, 47)}...` : preview;
|
|
747
|
-
display.showInfo(`📝 Queued: "${displayText}"`);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
else if (_str && !key.ctrl && !key.meta) {
|
|
751
|
-
// Route raw input (including multi-line paste) to the pinned chat box
|
|
752
|
-
this.pinnedChatBox.handleInput(_str, { allowNewlines: true, isPaste });
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
else if (_str) {
|
|
756
|
-
// String without key info (paste) - pass through for summary handling
|
|
757
|
-
this.pinnedChatBox.handleInput(_str, { allowNewlines: true, isPaste: true });
|
|
758
|
-
}
|
|
759
|
-
// Re-render the bottom chat box to show the updated input
|
|
760
|
-
this.pinnedChatBox.updatePersistentInput();
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
// Readline handles all keyboard input natively (history, shortcuts, etc.)
|
|
764
|
-
// We just sync the current state to our display components
|
|
765
|
-
// Use setImmediate to get the updated line after readline processes the key
|
|
766
|
-
setImmediate(() => {
|
|
767
|
-
const currentLine = this.rl.line || '';
|
|
768
|
-
const cursorPos = this.rl.cursor || 0;
|
|
769
|
-
this.persistentPrompt.updateInput(currentLine, cursorPos);
|
|
770
|
-
// Sync to pinned chat box for display only
|
|
771
|
-
this.pinnedChatBox.setInput(currentLine, cursorPos);
|
|
772
|
-
if (this.composableMessage.hasContent()) {
|
|
773
|
-
this.composableMessage.setDraft(currentLine);
|
|
774
|
-
this.updateComposeStatusSummary();
|
|
775
|
-
}
|
|
776
|
-
this.handleSlashCommandPreviewChange();
|
|
777
|
-
});
|
|
778
|
-
};
|
|
779
|
-
// Use prependListener to handle before readline
|
|
780
|
-
inputStream.prependListener('keypress', this.keypressHandler);
|
|
448
|
+
// Show initial input UI
|
|
449
|
+
this.terminalInput.render();
|
|
781
450
|
}
|
|
782
451
|
setupStatusTracking() {
|
|
783
452
|
this.statusSubscription = this.statusTracker.subscribe((_state) => {
|
|
@@ -835,14 +504,10 @@ export class InteractiveShell {
|
|
|
835
504
|
if (!shouldShow && this.slashPreviewVisible) {
|
|
836
505
|
this.slashPreviewVisible = false;
|
|
837
506
|
this.uiAdapter.hideSlashCommandPreview();
|
|
838
|
-
// Show persistent prompt again after hiding overlay
|
|
839
|
-
if (!this.isProcessing) {
|
|
840
|
-
this.persistentPrompt.show();
|
|
841
|
-
}
|
|
842
507
|
}
|
|
843
508
|
}
|
|
844
509
|
shouldShowSlashCommandPreview() {
|
|
845
|
-
const line = this.
|
|
510
|
+
const line = this.currentInput ?? '';
|
|
846
511
|
if (!line.trim()) {
|
|
847
512
|
return false;
|
|
848
513
|
}
|
|
@@ -851,642 +516,28 @@ export class InteractiveShell {
|
|
|
851
516
|
}
|
|
852
517
|
showSlashCommandPreview() {
|
|
853
518
|
// Filter commands based on current input
|
|
854
|
-
const line = this.
|
|
519
|
+
const line = this.currentInput ?? '';
|
|
855
520
|
const trimmed = line.trimStart();
|
|
856
521
|
// Filter commands that match the current input
|
|
857
522
|
const filtered = this.slashCommands.filter(cmd => cmd.command.startsWith(trimmed) || trimmed === '/');
|
|
858
|
-
// Hide persistent prompt to avoid conflicts with overlay
|
|
859
|
-
this.persistentPrompt.hide();
|
|
860
523
|
// Show in the unified UI with dynamic overlay
|
|
861
524
|
this.uiAdapter.showSlashCommandPreview(filtered);
|
|
862
525
|
// Don't reprompt - this causes flickering
|
|
863
526
|
}
|
|
864
|
-
showProfileSwitcher() {
|
|
865
|
-
if (!this.agentMenu) {
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
// Build profile options with current/next indicators
|
|
869
|
-
const profiles = this.agentMenu.options.map((option, index) => {
|
|
870
|
-
const badges = [];
|
|
871
|
-
const nextProfile = this.agentMenu.persistedProfile ?? this.agentMenu.defaultProfile;
|
|
872
|
-
if (option.name === this.profile) {
|
|
873
|
-
badges.push('current');
|
|
874
|
-
}
|
|
875
|
-
if (option.name === nextProfile && option.name !== this.profile) {
|
|
876
|
-
badges.push('next');
|
|
877
|
-
}
|
|
878
|
-
const badgeText = badges.length > 0 ? ` (${badges.join(', ')})` : '';
|
|
879
|
-
return {
|
|
880
|
-
command: `${index + 1}. ${option.label}${badgeText}`,
|
|
881
|
-
description: `${this.providerLabel(option.defaultProvider)} • ${option.defaultModel}`,
|
|
882
|
-
};
|
|
883
|
-
});
|
|
884
|
-
// Show profile switcher overlay
|
|
885
|
-
this.uiAdapter.showProfileSwitcher(profiles, this.profileLabel);
|
|
886
|
-
}
|
|
887
527
|
/**
|
|
888
|
-
* Ensure
|
|
889
|
-
* This recovers from cases where a nested readline/prompt or partial paste
|
|
890
|
-
* left the stream paused, raw mode disabled, or keypress listeners detached.
|
|
528
|
+
* Ensure the terminal input is ready for interactive input.
|
|
891
529
|
*/
|
|
892
530
|
ensureReadlineReady() {
|
|
893
|
-
|
|
894
|
-
this.bracketedPaste.reset();
|
|
895
|
-
// Always ensure the pinned chat box is visible when readline is ready
|
|
896
|
-
this.pinnedChatBox.show();
|
|
897
|
-
this.pinnedChatBox.forceRender();
|
|
898
|
-
const inputStream = input;
|
|
899
|
-
if (!inputStream) {
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
// Resume stdin if another consumer paused it
|
|
903
|
-
if (typeof inputStream.isPaused === 'function' && inputStream.isPaused()) {
|
|
904
|
-
inputStream.resume();
|
|
905
|
-
}
|
|
906
|
-
else if (inputStream.readableFlowing === false) {
|
|
907
|
-
inputStream.resume();
|
|
908
|
-
}
|
|
909
|
-
// Restore raw mode if a nested readline turned it off
|
|
910
|
-
if (inputStream.isTTY && typeof inputStream.isRaw === 'boolean') {
|
|
911
|
-
const ttyStream = inputStream;
|
|
912
|
-
if (!ttyStream.isRaw) {
|
|
913
|
-
ttyStream.setRawMode(true);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
// Reattach keypress handler if it was removed
|
|
917
|
-
if (this.keypressHandler) {
|
|
918
|
-
const listeners = inputStream.listeners('keypress');
|
|
919
|
-
const hasHandler = listeners.includes(this.keypressHandler);
|
|
920
|
-
if (!hasHandler) {
|
|
921
|
-
if (inputStream.listenerCount('keypress') === 0) {
|
|
922
|
-
readline.emitKeypressEvents(inputStream, this.rl);
|
|
923
|
-
}
|
|
924
|
-
inputStream.on('keypress', this.keypressHandler);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
// Restore readline output if it was suppressed
|
|
928
|
-
// Note: Stream handler manages its own state, this is for legacy compatibility
|
|
929
|
-
if (!this.streamHandler.isStreaming()) {
|
|
930
|
-
this.restoreReadlineOutput();
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Suppress readline's character echo during streaming.
|
|
935
|
-
* Characters typed will be captured but not echoed to the main output.
|
|
936
|
-
* Instead, they appear only in the persistent input box at the bottom.
|
|
937
|
-
*
|
|
938
|
-
* @deprecated Use streamHandler.startStreaming() instead - kept for backwards compatibility
|
|
939
|
-
* @internal Kept for legacy code paths that may still reference this method
|
|
940
|
-
*/
|
|
941
|
-
// @ts-expect-error - Legacy method kept for backwards compatibility, unused in favor of streamHandler
|
|
942
|
-
suppressReadlineOutput() {
|
|
943
|
-
// Delegate to stream handler if it's not already streaming
|
|
944
|
-
if (this.streamHandler.isStreaming()) {
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
if (this.readlineOutputSuppressed || !output.isTTY) {
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
this.originalStdoutWrite = output.write.bind(output);
|
|
951
|
-
const self = this;
|
|
952
|
-
// Replace stdout.write to filter readline echo
|
|
953
|
-
// Readline writes single characters for echo - we filter those out
|
|
954
|
-
// but allow multi-character writes (actual output) through
|
|
955
|
-
output.write = function (chunk, encodingOrCallback, callback) {
|
|
956
|
-
if (!self.originalStdoutWrite) {
|
|
957
|
-
return true;
|
|
958
|
-
}
|
|
959
|
-
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
960
|
-
// Filter out readline echo patterns:
|
|
961
|
-
// - Single printable characters (user typing)
|
|
962
|
-
// - Cursor movement sequences for single chars
|
|
963
|
-
// - Backspace sequences
|
|
964
|
-
// - Prompt redraws
|
|
965
|
-
// But allow through:
|
|
966
|
-
// - Multi-character content (actual AI output)
|
|
967
|
-
// - Newlines and control sequences for formatting
|
|
968
|
-
// If it's a single printable char, suppress it (user typing)
|
|
969
|
-
if (str.length === 1 && str.charCodeAt(0) >= 32 && str.charCodeAt(0) < 127) {
|
|
970
|
-
return true;
|
|
971
|
-
}
|
|
972
|
-
// Suppress backspace sequences (readline's delete char)
|
|
973
|
-
if (str === '\b \b' || str === '\x1b[D \x1b[D' || str === '\b' || str === '\x7f') {
|
|
974
|
-
return true;
|
|
975
|
-
}
|
|
976
|
-
// Suppress cursor movement sequences (readline cursor positioning)
|
|
977
|
-
if (/^\x1b\[\d*[ABCD]$/.test(str)) {
|
|
978
|
-
return true;
|
|
979
|
-
}
|
|
980
|
-
// Suppress readline prompt redraw patterns (starts with \r or cursor home)
|
|
981
|
-
if (/^\r/.test(str) && str.length < 20 && /^[\r\x1b\[\dGK> ]+$/.test(str)) {
|
|
982
|
-
return true;
|
|
983
|
-
}
|
|
984
|
-
// Suppress clear line + prompt patterns from readline
|
|
985
|
-
if (/^\x1b\[\d*[GK]/.test(str) && str.length < 15) {
|
|
986
|
-
return true;
|
|
987
|
-
}
|
|
988
|
-
// Suppress short sequences that look like readline control (not AI content)
|
|
989
|
-
// AI content is typically longer or contains actual text
|
|
990
|
-
if (str.length <= 3 && /^[\x1b\[\]\d;GKJ]+$/.test(str)) {
|
|
991
|
-
return true;
|
|
992
|
-
}
|
|
993
|
-
// Allow everything else through (actual AI output)
|
|
994
|
-
if (typeof encodingOrCallback === 'function') {
|
|
995
|
-
return self.originalStdoutWrite(chunk, encodingOrCallback);
|
|
996
|
-
}
|
|
997
|
-
return self.originalStdoutWrite(chunk, encodingOrCallback, callback);
|
|
998
|
-
};
|
|
999
|
-
this.readlineOutputSuppressed = true;
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Restore normal readline output after streaming completes.
|
|
1003
|
-
*
|
|
1004
|
-
* @deprecated Use streamHandler.endStreaming() instead
|
|
1005
|
-
*/
|
|
1006
|
-
restoreReadlineOutput() {
|
|
1007
|
-
if (!this.readlineOutputSuppressed || !this.originalStdoutWrite) {
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
output.write = this.originalStdoutWrite;
|
|
1011
|
-
this.originalStdoutWrite = null;
|
|
1012
|
-
this.readlineOutputSuppressed = false;
|
|
1013
|
-
}
|
|
1014
|
-
enqueueUserInput(line, flushImmediately = false) {
|
|
1015
|
-
this.bufferedInputLines.push(line);
|
|
1016
|
-
if (flushImmediately) {
|
|
1017
|
-
if (this.bufferedInputTimer) {
|
|
1018
|
-
clearTimeout(this.bufferedInputTimer);
|
|
1019
|
-
this.bufferedInputTimer = null;
|
|
1020
|
-
}
|
|
1021
|
-
void this.flushBufferedInput();
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (this.bufferedInputTimer) {
|
|
1025
|
-
clearTimeout(this.bufferedInputTimer);
|
|
1026
|
-
}
|
|
1027
|
-
this.bufferedInputTimer = setTimeout(() => {
|
|
1028
|
-
void this.flushBufferedInput();
|
|
1029
|
-
}, MULTILINE_INPUT_FLUSH_DELAY_MS);
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Clear any buffered input lines and pending flush timers.
|
|
1033
|
-
* Prevents partial multi-line pastes from being auto-submitted.
|
|
1034
|
-
*/
|
|
1035
|
-
resetBufferedInputLines() {
|
|
1036
|
-
if (this.bufferedInputTimer) {
|
|
1037
|
-
clearTimeout(this.bufferedInputTimer);
|
|
1038
|
-
this.bufferedInputTimer = null;
|
|
1039
|
-
}
|
|
1040
|
-
if (this.bufferedInputLines.length) {
|
|
1041
|
-
this.bufferedInputLines = [];
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
/** Track paste preview state to properly clear/update display */
|
|
1045
|
-
lastPastePreviewLineCount = 0;
|
|
1046
|
-
inPasteCapture = false;
|
|
1047
|
-
/**
|
|
1048
|
-
* Show a preview indicator while accumulating multi-line paste.
|
|
1049
|
-
* The line handler has already cleared readline's echoed line, so we just
|
|
1050
|
-
* write our preview in place using carriage return.
|
|
1051
|
-
*/
|
|
1052
|
-
showMultiLinePastePreview(lineCount, preview) {
|
|
1053
|
-
// Only update if we're actually accumulating (avoid spam during rapid paste)
|
|
1054
|
-
if (lineCount === 0 && !preview) {
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
// Mark that we're in paste capture mode
|
|
1058
|
-
this.inPasteCapture = true;
|
|
1059
|
-
// Throttle updates - only update if line count changed to avoid flicker
|
|
1060
|
-
if (lineCount === this.lastPastePreviewLineCount) {
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
this.lastPastePreviewLineCount = lineCount;
|
|
1064
|
-
// Clear current line and write preview (the line handler already moved cursor up)
|
|
1065
|
-
output.write('\r\x1b[K');
|
|
1066
|
-
const statusText = preview
|
|
1067
|
-
? `${theme.ui.muted('📋 Pasting:')} ${theme.ui.muted(preview.slice(0, 50))}${preview.length > 50 ? '...' : ''}`
|
|
1068
|
-
: `${theme.ui.muted(`📋 Pasting ${lineCount} line${lineCount !== 1 ? 's' : ''}...`)}`;
|
|
1069
|
-
output.write(statusText);
|
|
1070
|
-
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Clear the multi-line paste preview
|
|
1073
|
-
*/
|
|
1074
|
-
clearMultiLinePastePreview() {
|
|
1075
|
-
if (!this.inPasteCapture) {
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
// Clear current line
|
|
1079
|
-
output.write('\r\x1b[K');
|
|
1080
|
-
// Reset tracking state
|
|
1081
|
-
this.lastPastePreviewLineCount = 0;
|
|
1082
|
-
this.inPasteCapture = false;
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Capture any paste (single or multi-line) without immediately submitting it.
|
|
1086
|
-
* - Short pastes (1-2 lines) are displayed inline like normal typed text
|
|
1087
|
-
* - Longer pastes (3+ lines) show as collapsed block chips
|
|
1088
|
-
* Supports multiple pastes - user can paste multiple times before submitting.
|
|
1089
|
-
*/
|
|
1090
|
-
async capturePaste(content, lineCount) {
|
|
1091
|
-
this.resetBufferedInputLines();
|
|
1092
|
-
// Short pastes (1-2 lines) display inline like normal text
|
|
1093
|
-
const isShortPaste = lineCount <= 2;
|
|
1094
|
-
if (isShortPaste) {
|
|
1095
|
-
// For short pastes, display inline like normal typed text
|
|
1096
|
-
// No composableMessage storage - just treat as typed input
|
|
1097
|
-
// For 2-line pastes, join with a visual newline indicator
|
|
1098
|
-
const displayContent = lineCount === 1
|
|
1099
|
-
? content
|
|
1100
|
-
: content.replace(/\n/g, ' ↵ '); // Visual newline indicator for 2-line pastes
|
|
1101
|
-
// Clear any echoed content first
|
|
1102
|
-
output.write('\r\x1b[K');
|
|
1103
|
-
// Get current readline content and append paste
|
|
1104
|
-
const currentLine = this.rl.line || '';
|
|
1105
|
-
const cursorPos = this.rl.cursor || 0;
|
|
1106
|
-
// Insert paste at cursor position
|
|
1107
|
-
const before = currentLine.slice(0, cursorPos);
|
|
1108
|
-
const after = currentLine.slice(cursorPos);
|
|
1109
|
-
const newLine = before + displayContent + after;
|
|
1110
|
-
const newCursor = cursorPos + displayContent.length;
|
|
1111
|
-
// Update readline buffer - write directly without storing in composableMessage
|
|
1112
|
-
// This allows short pastes to flow through as normal typed text
|
|
1113
|
-
this.rl.write(null, { ctrl: true, name: 'u' }); // Clear line
|
|
1114
|
-
this.rl.write(newLine); // Write new content
|
|
1115
|
-
// Update persistent prompt display
|
|
1116
|
-
this.persistentPrompt.updateInput(newLine, newCursor);
|
|
1117
|
-
// NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
|
|
1118
|
-
// will decrement for each readline line event and auto-clear when all are processed.
|
|
1119
|
-
// Clearing prematurely causes the remaining readline-echoed lines to pass through.
|
|
1120
|
-
// Re-prompt to show the inline content
|
|
1121
|
-
this.rl.prompt(true);
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
// For longer pastes (3+ lines), store as a composable block
|
|
1125
|
-
// Check size limits first
|
|
1126
|
-
const { ComposableMessageBuilder } = await import('./composableMessage.js');
|
|
1127
|
-
const sizeCheck = ComposableMessageBuilder.checkPasteSize(content);
|
|
1128
|
-
if (!sizeCheck.ok) {
|
|
1129
|
-
// Paste rejected - show error and abort
|
|
1130
|
-
display.showError(sizeCheck.error || 'Paste rejected due to size limits');
|
|
1131
|
-
this.rl.prompt();
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
// Show warning for large (but acceptable) pastes
|
|
1135
|
-
if (sizeCheck.warning) {
|
|
1136
|
-
display.showWarning(`⚠️ ${sizeCheck.warning}`);
|
|
1137
|
-
}
|
|
1138
|
-
const pasteId = this.composableMessage.addPaste(content);
|
|
1139
|
-
if (!pasteId) {
|
|
1140
|
-
// Should not happen since we checked above, but handle defensively
|
|
1141
|
-
display.showError('Failed to store paste block');
|
|
1142
|
-
this.rl.prompt();
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
// Clear remaining echoed lines from terminal
|
|
1146
|
-
output.write('\r\x1b[K');
|
|
1147
|
-
// Build the paste chips to show inline with prompt
|
|
1148
|
-
// Format: [Pasted text #1 +104 lines] [Pasted text #2 +50 lines]
|
|
1149
|
-
const pasteChips = this.composableMessage.formatPasteChips();
|
|
1150
|
-
// Update status bar - minimal hint, no confirmation required
|
|
1151
|
-
this.persistentPrompt.updateStatusBar({
|
|
1152
|
-
message: 'Press Enter to send',
|
|
1153
|
-
});
|
|
1154
|
-
// Set the prompt to show paste chips, then position cursor after them
|
|
1155
|
-
// The user can type additional text after the chips
|
|
1156
|
-
this.persistentPrompt.updateInput(pasteChips + ' ', pasteChips.length + 1);
|
|
1157
|
-
// Update readline's line buffer to include the chips as prefix
|
|
1158
|
-
// This ensures typed text appears after the chips
|
|
1159
|
-
if (this.rl.line !== undefined) {
|
|
1160
|
-
this.rl.line = pasteChips + ' ';
|
|
1161
|
-
this.rl.cursor = pasteChips.length + 1;
|
|
1162
|
-
}
|
|
1163
|
-
// NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
|
|
1164
|
-
// will decrement for each readline line event (one per pasted line) and auto-clear when done.
|
|
1165
|
-
// Clearing prematurely causes remaining readline-echoed lines to pass through and get displayed.
|
|
1166
|
-
this.rl.prompt(true); // preserveCursor=true to keep position after chips
|
|
1167
|
-
}
|
|
1168
|
-
/**
|
|
1169
|
-
* Update the status bar to reflect any pending composed message parts
|
|
1170
|
-
*/
|
|
1171
|
-
updateComposeStatusSummary() {
|
|
1172
|
-
if (this.composableMessage.hasContent()) {
|
|
1173
|
-
// Chips are shown inline - minimal status hint
|
|
1174
|
-
this.persistentPrompt.updateStatusBar({
|
|
1175
|
-
message: 'Press Enter to send',
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
else {
|
|
1179
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* Show paste block editor (Ctrl+G)
|
|
1184
|
-
* Allows viewing and editing captured paste blocks before sending
|
|
1185
|
-
*
|
|
1186
|
-
* Features:
|
|
1187
|
-
* - View block content with line numbers
|
|
1188
|
-
* - Edit blocks inline (replace content)
|
|
1189
|
-
* - Remove individual blocks
|
|
1190
|
-
* - Undo/redo paste operations
|
|
1191
|
-
* - Clear all blocks
|
|
1192
|
-
*/
|
|
1193
|
-
showPasteBlockEditor() {
|
|
1194
|
-
const state = this.composableMessage.getState();
|
|
1195
|
-
const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
|
|
1196
|
-
if (pasteBlocks.length === 0) {
|
|
1197
|
-
display.showInfo('No pasted content blocks to edit. Paste some content first.');
|
|
1198
|
-
this.rl.prompt();
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
output.write('\n');
|
|
1202
|
-
display.showSystemMessage('📋 Paste Block Editor');
|
|
1203
|
-
output.write('\n');
|
|
1204
|
-
// Display each paste block with preview
|
|
1205
|
-
pasteBlocks.forEach((block, index) => {
|
|
1206
|
-
const lines = block.content.split('\n');
|
|
1207
|
-
const preview = lines.slice(0, 3).map((l) => ` ${l.slice(0, 60)}${l.length > 60 ? '...' : ''}`).join('\n');
|
|
1208
|
-
const moreLines = lines.length > 3 ? `\n ... +${lines.length - 3} more lines` : '';
|
|
1209
|
-
const editedFlag = block.edited ? theme.warning(' [edited]') : '';
|
|
1210
|
-
output.write(theme.ui.muted(`[${index + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + editedFlag + '\n');
|
|
1211
|
-
output.write(theme.secondary(preview + moreLines) + '\n\n');
|
|
1212
|
-
});
|
|
1213
|
-
// Show undo/redo status
|
|
1214
|
-
const canUndo = this.composableMessage.canUndo();
|
|
1215
|
-
const canRedo = this.composableMessage.canRedo();
|
|
1216
|
-
if (canUndo || canRedo) {
|
|
1217
|
-
const undoStatus = canUndo ? theme.success('undo available') : theme.ui.muted('undo unavailable');
|
|
1218
|
-
const redoStatus = canRedo ? theme.success('redo available') : theme.ui.muted('redo unavailable');
|
|
1219
|
-
output.write(theme.ui.muted('History: ') + undoStatus + theme.ui.muted(' │ ') + redoStatus + '\n\n');
|
|
1220
|
-
}
|
|
1221
|
-
output.write(theme.ui.muted('Commands: ') + '\n');
|
|
1222
|
-
output.write(theme.ui.muted(' • Enter number to view full block') + '\n');
|
|
1223
|
-
output.write(theme.ui.muted(' • "edit N" to replace block N content') + '\n');
|
|
1224
|
-
output.write(theme.ui.muted(' • "remove N" to remove block N') + '\n');
|
|
1225
|
-
output.write(theme.ui.muted(' • "undo" / "redo" to undo/redo changes') + '\n');
|
|
1226
|
-
output.write(theme.ui.muted(' • "clear" to remove all blocks') + '\n');
|
|
1227
|
-
output.write(theme.ui.muted(' • Enter to return to prompt') + '\n');
|
|
1228
|
-
output.write('\n');
|
|
1229
|
-
// Set up interaction mode for paste editing
|
|
1230
|
-
this.pendingInteraction = { type: 'paste-edit' };
|
|
1231
|
-
this.rl.prompt();
|
|
1232
|
-
}
|
|
1233
|
-
/**
|
|
1234
|
-
* Handle paste block editor input
|
|
1235
|
-
*/
|
|
1236
|
-
async handlePasteEditInput(trimmedInput) {
|
|
1237
|
-
const trimmed = trimmedInput.toLowerCase();
|
|
1238
|
-
const state = this.composableMessage.getState();
|
|
1239
|
-
const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
|
|
1240
|
-
if (!trimmed) {
|
|
1241
|
-
// Exit paste edit mode
|
|
1242
|
-
this.pendingInteraction = null;
|
|
1243
|
-
display.showInfo('Returned to prompt.');
|
|
1244
|
-
this.rl.prompt();
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
// Handle undo command
|
|
1248
|
-
if (trimmed === 'undo') {
|
|
1249
|
-
if (this.composableMessage.undo()) {
|
|
1250
|
-
this.updateComposeStatusSummary();
|
|
1251
|
-
this.refreshPasteChipsDisplay();
|
|
1252
|
-
display.showInfo('Undone.');
|
|
1253
|
-
// Refresh the paste block view
|
|
1254
|
-
this.showPasteBlockEditor();
|
|
1255
|
-
}
|
|
1256
|
-
else {
|
|
1257
|
-
display.showWarning('Nothing to undo.');
|
|
1258
|
-
this.rl.prompt();
|
|
1259
|
-
}
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
// Handle redo command
|
|
1263
|
-
if (trimmed === 'redo') {
|
|
1264
|
-
if (this.composableMessage.redo()) {
|
|
1265
|
-
this.updateComposeStatusSummary();
|
|
1266
|
-
this.refreshPasteChipsDisplay();
|
|
1267
|
-
display.showInfo('Redone.');
|
|
1268
|
-
// Refresh the paste block view
|
|
1269
|
-
this.showPasteBlockEditor();
|
|
1270
|
-
}
|
|
1271
|
-
else {
|
|
1272
|
-
display.showWarning('Nothing to redo.');
|
|
1273
|
-
this.rl.prompt();
|
|
1274
|
-
}
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
if (trimmed === 'clear') {
|
|
1278
|
-
this.composableMessage.clear();
|
|
1279
|
-
this.updateComposeStatusSummary();
|
|
1280
|
-
this.persistentPrompt.updateInput('', 0);
|
|
1281
|
-
this.rl.line = '';
|
|
1282
|
-
this.rl.cursor = 0;
|
|
1283
|
-
this.pendingInteraction = null;
|
|
1284
|
-
display.showInfo('Cleared all paste blocks.');
|
|
1285
|
-
this.rl.prompt();
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
// Handle "edit N" command - start inline editing mode
|
|
1289
|
-
const editMatch = trimmed.match(/^edit\s+(\d+)$/);
|
|
1290
|
-
if (editMatch) {
|
|
1291
|
-
const blockNum = parseInt(editMatch[1], 10);
|
|
1292
|
-
if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
|
|
1293
|
-
const block = pasteBlocks[blockNum - 1];
|
|
1294
|
-
if (block) {
|
|
1295
|
-
// Enter edit mode for this block
|
|
1296
|
-
this.pendingInteraction = { type: 'paste-edit-block', blockId: block.id, blockNum };
|
|
1297
|
-
output.write('\n');
|
|
1298
|
-
display.showSystemMessage(`Editing Block ${blockNum}:`);
|
|
1299
|
-
output.write(theme.ui.muted('Paste or type new content. Press Enter twice to finish, or "cancel" to abort.\n'));
|
|
1300
|
-
output.write('\n');
|
|
1301
|
-
// Show current content as reference
|
|
1302
|
-
const preview = block.content.split('\n').slice(0, 5).map((l) => theme.ui.muted(` ${l.slice(0, 60)}`)).join('\n');
|
|
1303
|
-
output.write(theme.ui.muted('Current content (first 5 lines):\n') + preview + '\n\n');
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
else {
|
|
1307
|
-
display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
|
|
1308
|
-
}
|
|
1309
|
-
this.rl.prompt();
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
// Handle "remove N" command
|
|
1313
|
-
const removeMatch = trimmed.match(/^remove\s+(\d+)$/);
|
|
1314
|
-
if (removeMatch) {
|
|
1315
|
-
const blockNum = parseInt(removeMatch[1], 10);
|
|
1316
|
-
if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
|
|
1317
|
-
const block = pasteBlocks[blockNum - 1];
|
|
1318
|
-
if (block) {
|
|
1319
|
-
this.composableMessage.removePart(block.id);
|
|
1320
|
-
this.updateComposeStatusSummary();
|
|
1321
|
-
this.refreshPasteChipsDisplay();
|
|
1322
|
-
display.showInfo(`Removed block ${blockNum}.`);
|
|
1323
|
-
// Check if any blocks remain
|
|
1324
|
-
const remaining = this.composableMessage.getState().parts.filter(p => p.type === 'paste');
|
|
1325
|
-
if (remaining.length === 0) {
|
|
1326
|
-
this.pendingInteraction = null;
|
|
1327
|
-
display.showInfo('No more paste blocks.');
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
else {
|
|
1332
|
-
display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
|
|
1333
|
-
}
|
|
1334
|
-
this.rl.prompt();
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
// Handle viewing a block by number
|
|
1338
|
-
const blockNum = parseInt(trimmed, 10);
|
|
1339
|
-
if (!Number.isNaN(blockNum) && blockNum >= 1 && blockNum <= pasteBlocks.length) {
|
|
1340
|
-
const block = pasteBlocks[blockNum - 1];
|
|
1341
|
-
if (block) {
|
|
1342
|
-
output.write('\n');
|
|
1343
|
-
display.showSystemMessage(`Block ${blockNum} Content:`);
|
|
1344
|
-
output.write('\n');
|
|
1345
|
-
// Show full content with line numbers
|
|
1346
|
-
const lines = block.content.split('\n');
|
|
1347
|
-
lines.forEach((line, i) => {
|
|
1348
|
-
const lineNum = theme.ui.muted(`${String(i + 1).padStart(4)} │ `);
|
|
1349
|
-
output.write(lineNum + line + '\n');
|
|
1350
|
-
});
|
|
1351
|
-
output.write('\n');
|
|
1352
|
-
}
|
|
1353
|
-
this.rl.prompt();
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
display.showWarning('Enter a block number, "edit N", "remove N", "undo", "redo", "clear", or press Enter to return.');
|
|
1357
|
-
this.rl.prompt();
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Handle paste block inline editing mode
|
|
1361
|
-
*/
|
|
1362
|
-
editBlockBuffer = [];
|
|
1363
|
-
async handlePasteBlockEditInput(input, blockId, blockNum) {
|
|
1364
|
-
// Check for cancel
|
|
1365
|
-
if (input.toLowerCase() === 'cancel') {
|
|
1366
|
-
this.editBlockBuffer = [];
|
|
1367
|
-
this.pendingInteraction = { type: 'paste-edit' };
|
|
1368
|
-
display.showInfo('Edit cancelled.');
|
|
1369
|
-
this.showPasteBlockEditor();
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
// Empty line - check if this is end of edit (double Enter)
|
|
1373
|
-
if (!input.trim()) {
|
|
1374
|
-
if (this.editBlockBuffer.length > 0) {
|
|
1375
|
-
// Complete the edit
|
|
1376
|
-
const newContent = this.editBlockBuffer.join('\n');
|
|
1377
|
-
this.composableMessage.editPaste(blockId, newContent);
|
|
1378
|
-
this.editBlockBuffer = [];
|
|
1379
|
-
this.updateComposeStatusSummary();
|
|
1380
|
-
this.refreshPasteChipsDisplay();
|
|
1381
|
-
display.showInfo(`Block ${blockNum} updated (${newContent.split('\n').length} lines).`);
|
|
1382
|
-
this.pendingInteraction = { type: 'paste-edit' };
|
|
1383
|
-
this.showPasteBlockEditor();
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
// First empty line with no buffer - just prompt again
|
|
1387
|
-
this.rl.prompt();
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
// Add line to edit buffer
|
|
1391
|
-
this.editBlockBuffer.push(input);
|
|
1392
|
-
this.rl.prompt();
|
|
1393
|
-
}
|
|
1394
|
-
/**
|
|
1395
|
-
* Refresh paste chips display in prompt
|
|
1396
|
-
*/
|
|
1397
|
-
refreshPasteChipsDisplay() {
|
|
1398
|
-
const chips = this.composableMessage.formatPasteChips();
|
|
1399
|
-
this.persistentPrompt.updateInput(chips ? chips + ' ' : '', chips ? chips.length + 1 : 0);
|
|
1400
|
-
this.rl.line = chips ? chips + ' ' : '';
|
|
1401
|
-
this.rl.cursor = chips ? chips.length + 1 : 0;
|
|
1402
|
-
}
|
|
1403
|
-
/**
|
|
1404
|
-
* Cycle paste preview mode (Shift+Tab)
|
|
1405
|
-
* Cycles through: collapsed → summary → expanded
|
|
1406
|
-
*/
|
|
1407
|
-
pastePreviewMode = 'collapsed';
|
|
1408
|
-
cyclePastePreview() {
|
|
1409
|
-
const state = this.composableMessage.getState();
|
|
1410
|
-
const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
|
|
1411
|
-
if (pasteBlocks.length === 0) {
|
|
1412
|
-
return;
|
|
1413
|
-
}
|
|
1414
|
-
// Cycle through modes
|
|
1415
|
-
const modes = ['collapsed', 'summary', 'expanded'];
|
|
1416
|
-
const currentIndex = modes.indexOf(this.pastePreviewMode);
|
|
1417
|
-
this.pastePreviewMode = modes[(currentIndex + 1) % modes.length];
|
|
1418
|
-
output.write('\n');
|
|
1419
|
-
switch (this.pastePreviewMode) {
|
|
1420
|
-
case 'collapsed':
|
|
1421
|
-
// Show just chips
|
|
1422
|
-
display.showSystemMessage(`📋 Preview: Collapsed (${pasteBlocks.length} block${pasteBlocks.length > 1 ? 's' : ''})`);
|
|
1423
|
-
output.write(theme.ui.muted(this.composableMessage.formatPasteChips()) + '\n');
|
|
1424
|
-
break;
|
|
1425
|
-
case 'summary':
|
|
1426
|
-
// Show blocks with first line preview
|
|
1427
|
-
display.showSystemMessage('📋 Preview: Summary');
|
|
1428
|
-
pasteBlocks.forEach((block, i) => {
|
|
1429
|
-
const preview = block.summary.length > 60 ? block.summary.slice(0, 57) + '...' : block.summary;
|
|
1430
|
-
output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount}L`) + theme.ui.muted(` ${preview}`) + '\n');
|
|
1431
|
-
});
|
|
1432
|
-
break;
|
|
1433
|
-
case 'expanded':
|
|
1434
|
-
// Show full content (up to 10 lines each)
|
|
1435
|
-
display.showSystemMessage('📋 Preview: Expanded');
|
|
1436
|
-
pasteBlocks.forEach((block, i) => {
|
|
1437
|
-
output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + '\n');
|
|
1438
|
-
const lines = block.content.split('\n').slice(0, 10);
|
|
1439
|
-
lines.forEach((line, j) => {
|
|
1440
|
-
const lineNum = theme.ui.muted(`${String(j + 1).padStart(3)} │ `);
|
|
1441
|
-
output.write(lineNum + line.slice(0, 80) + (line.length > 80 ? '...' : '') + '\n');
|
|
1442
|
-
});
|
|
1443
|
-
if (block.lineCount > 10) {
|
|
1444
|
-
output.write(theme.ui.muted(` ... +${block.lineCount - 10} more lines\n`));
|
|
1445
|
-
}
|
|
1446
|
-
output.write('\n');
|
|
1447
|
-
});
|
|
1448
|
-
break;
|
|
1449
|
-
}
|
|
1450
|
-
output.write(theme.ui.muted('(Shift+Tab to cycle preview modes)') + '\n\n');
|
|
1451
|
-
this.rl.prompt(true);
|
|
1452
|
-
}
|
|
1453
|
-
async flushBufferedInput() {
|
|
1454
|
-
if (!this.bufferedInputLines.length) {
|
|
1455
|
-
this.bufferedInputTimer = null;
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
const lineCount = this.bufferedInputLines.length;
|
|
1459
|
-
const combined = this.bufferedInputLines.join('\n');
|
|
1460
|
-
this.bufferedInputLines = [];
|
|
1461
|
-
this.bufferedInputTimer = null;
|
|
1462
|
-
try {
|
|
1463
|
-
await this.processInputBlock(combined, lineCount > 1);
|
|
1464
|
-
}
|
|
1465
|
-
catch (error) {
|
|
1466
|
-
// Pass full error object for enhanced formatting
|
|
1467
|
-
display.showError(error instanceof Error ? error.message : String(error), error);
|
|
1468
|
-
this.rl.prompt();
|
|
1469
|
-
}
|
|
531
|
+
this.terminalInput.render();
|
|
1470
532
|
}
|
|
1471
533
|
refreshQueueIndicators() {
|
|
1472
|
-
const queued = this.followUpQueue.length;
|
|
1473
|
-
// Build status message based on processing state and queue
|
|
1474
|
-
let message;
|
|
1475
|
-
if (this.isProcessing) {
|
|
1476
|
-
const queuePart = queued > 0 ? `${queued} queued · ` : '';
|
|
1477
|
-
// Show helpful hints: Esc to cancel, type to queue
|
|
1478
|
-
message = `⏳ Processing... ${queuePart}[Esc: cancel · Enter: queue follow-up]`;
|
|
1479
|
-
}
|
|
1480
|
-
else if (queued > 0) {
|
|
1481
|
-
message = `${queued} follow-up${queued === 1 ? '' : 's'} queued · [Esc: clear queue]`;
|
|
1482
|
-
}
|
|
1483
|
-
this.persistentPrompt.updateStatusBar({ message });
|
|
1484
534
|
if (this.isProcessing) {
|
|
1485
535
|
this.setProcessingStatus();
|
|
1486
536
|
}
|
|
1487
537
|
else {
|
|
1488
538
|
this.setIdleStatus();
|
|
1489
539
|
}
|
|
540
|
+
this.terminalInput.render();
|
|
1490
541
|
}
|
|
1491
542
|
enqueueFollowUpAction(action) {
|
|
1492
543
|
this.followUpQueue.push(action);
|
|
@@ -1495,23 +546,17 @@ export class InteractiveShell {
|
|
|
1495
546
|
const preview = normalized.length > previewLimit ? `${normalized.slice(0, previewLimit - 3)}...` : normalized;
|
|
1496
547
|
const position = this.followUpQueue.length === 1 ? 'to run next' : `#${this.followUpQueue.length} in queue`;
|
|
1497
548
|
const label = action.type === 'continuous' ? 'continuous command' : 'follow-up';
|
|
1498
|
-
const queueCount = this.followUpQueue.length;
|
|
1499
549
|
// Show immediate acknowledgment with checkmark
|
|
1500
|
-
const queueLabel = queueCount === 1 ? '1 queued' : `${queueCount} queued`;
|
|
1501
550
|
if (preview) {
|
|
1502
551
|
display.showInfo(`✓ ${theme.info(label)} ${position}: ${theme.ui.muted(preview)}`);
|
|
1503
552
|
}
|
|
1504
553
|
else {
|
|
1505
554
|
display.showInfo(`✓ ${theme.info(label)} queued ${position}.`);
|
|
1506
555
|
}
|
|
1507
|
-
// Update status bar to show queue count
|
|
1508
|
-
this.persistentPrompt.updateStatusBar({
|
|
1509
|
-
message: `⏳ Processing... (${queueLabel})`
|
|
1510
|
-
});
|
|
1511
556
|
this.refreshQueueIndicators();
|
|
1512
557
|
this.scheduleQueueProcessing();
|
|
1513
558
|
// Re-show the prompt so user can continue typing more follow-ups
|
|
1514
|
-
this.
|
|
559
|
+
this.terminalInput.render();
|
|
1515
560
|
}
|
|
1516
561
|
scheduleQueueProcessing() {
|
|
1517
562
|
if (!this.followUpQueue.length) {
|
|
@@ -1523,71 +568,8 @@ export class InteractiveShell {
|
|
|
1523
568
|
});
|
|
1524
569
|
}
|
|
1525
570
|
/**
|
|
1526
|
-
* Process
|
|
1527
|
-
* Claude Code style: User can type while streaming, input is stored in queue,
|
|
1528
|
-
* processed after streaming ends.
|
|
1529
|
-
*
|
|
1530
|
-
* @deprecated Use processUnifiedChatBoxQueue() instead
|
|
1531
|
-
*/
|
|
1532
|
-
// @ts-expect-error - Legacy method kept for backwards compatibility
|
|
1533
|
-
processQueuedInputs() {
|
|
1534
|
-
if (!this.streamHandler.hasQueuedInput()) {
|
|
1535
|
-
return;
|
|
1536
|
-
}
|
|
1537
|
-
// Transfer queued inputs from stream handler to follow-up queue
|
|
1538
|
-
while (this.streamHandler.hasQueuedInput()) {
|
|
1539
|
-
const input = this.streamHandler.getNextQueuedInput();
|
|
1540
|
-
if (!input) {
|
|
1541
|
-
break;
|
|
1542
|
-
}
|
|
1543
|
-
// Handle interrupts specially
|
|
1544
|
-
if (input.type === 'interrupt') {
|
|
1545
|
-
// Interrupt was already processed during capture
|
|
1546
|
-
continue;
|
|
1547
|
-
}
|
|
1548
|
-
// Add to follow-up queue for processing
|
|
1549
|
-
const actionType = input.type === 'command' ? 'request' : 'request';
|
|
1550
|
-
this.followUpQueue.push({
|
|
1551
|
-
type: actionType,
|
|
1552
|
-
text: input.text,
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
// Show queue summary if there are items
|
|
1556
|
-
const summary = this.streamHandler.getQueueSummary();
|
|
1557
|
-
if (summary) {
|
|
1558
|
-
display.showInfo(`📝 ${summary}`);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
/**
|
|
1562
|
-
* Process any inputs that were queued in UnifiedChatBox during streaming.
|
|
1563
|
-
* Claude Code style: User can type while streaming, input is stored invisibly,
|
|
1564
|
-
* processed after streaming ends.
|
|
571
|
+
* Process queued follow-up actions.
|
|
1565
572
|
*/
|
|
1566
|
-
processUnifiedChatBoxQueue() {
|
|
1567
|
-
if (!this.unifiedChatBox.hasQueuedInput()) {
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
// Transfer queued inputs from unified chat box to follow-up queue
|
|
1571
|
-
while (this.unifiedChatBox.hasQueuedInput()) {
|
|
1572
|
-
const input = this.unifiedChatBox.dequeue();
|
|
1573
|
-
if (!input) {
|
|
1574
|
-
break;
|
|
1575
|
-
}
|
|
1576
|
-
// Handle interrupts specially - they cancel current operation
|
|
1577
|
-
if (input.type === 'interrupt') {
|
|
1578
|
-
// Request cancellation if agent is running
|
|
1579
|
-
if (this.agent) {
|
|
1580
|
-
this.agent.requestCancellation();
|
|
1581
|
-
}
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
// Add to follow-up queue for processing
|
|
1585
|
-
this.followUpQueue.push({
|
|
1586
|
-
type: 'request',
|
|
1587
|
-
text: input.text,
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
573
|
async processQueuedActions() {
|
|
1592
574
|
if (this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
|
|
1593
575
|
return;
|
|
@@ -1621,85 +603,22 @@ export class InteractiveShell {
|
|
|
1621
603
|
if (await this.handlePendingInteraction(trimmed)) {
|
|
1622
604
|
return;
|
|
1623
605
|
}
|
|
1624
|
-
// If we have captured multi-line paste blocks, respect control commands before assembling
|
|
1625
|
-
if (this.composableMessage.hasContent()) {
|
|
1626
|
-
// Strip paste chip prefixes from input since actual content is in composableMessage
|
|
1627
|
-
// Chips look like: [Pasted text #1 +X lines] [Pasted text #2 +Y lines]
|
|
1628
|
-
const chipsPrefix = this.composableMessage.formatPasteChips();
|
|
1629
|
-
let userText = trimmed;
|
|
1630
|
-
if (chipsPrefix && trimmed.startsWith(chipsPrefix)) {
|
|
1631
|
-
userText = trimmed.slice(chipsPrefix.length).trim();
|
|
1632
|
-
}
|
|
1633
|
-
const lower = userText.toLowerCase();
|
|
1634
|
-
// Control commands that should NOT consume the captured paste
|
|
1635
|
-
if (lower === '/cancel' || lower === 'cancel') {
|
|
1636
|
-
this.composableMessage.clear();
|
|
1637
|
-
this.updateComposeStatusSummary();
|
|
1638
|
-
this.persistentPrompt.updateInput('', 0);
|
|
1639
|
-
display.showInfo('Discarded captured paste.');
|
|
1640
|
-
this.rl.prompt();
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
if (lower === 'exit' || lower === 'quit') {
|
|
1644
|
-
this.rl.close();
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
if (lower === 'clear') {
|
|
1648
|
-
display.clear();
|
|
1649
|
-
this.rl.prompt();
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
if (lower === 'help') {
|
|
1653
|
-
this.showHelp();
|
|
1654
|
-
this.rl.prompt();
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
// Slash commands operate independently of captured paste
|
|
1658
|
-
if (userText.startsWith('/')) {
|
|
1659
|
-
await this.processSlashCommand(userText);
|
|
1660
|
-
this.updateComposeStatusSummary();
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
// If userText is empty OR it's additional content, assemble and send
|
|
1664
|
-
// Empty enter sends the captured paste; non-empty content is appended first
|
|
1665
|
-
if (userText) {
|
|
1666
|
-
this.composableMessage.setDraft(userText);
|
|
1667
|
-
this.composableMessage.commitDraft();
|
|
1668
|
-
}
|
|
1669
|
-
const assembled = this.composableMessage.assemble();
|
|
1670
|
-
this.composableMessage.clear();
|
|
1671
|
-
this.updateComposeStatusSummary();
|
|
1672
|
-
this.persistentPrompt.updateInput('', 0);
|
|
1673
|
-
if (!assembled) {
|
|
1674
|
-
this.rl.prompt();
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
// Check if assembled content is a continuous command
|
|
1678
|
-
if (this.isContinuousCommand(assembled)) {
|
|
1679
|
-
await this.processContinuousRequest(assembled);
|
|
1680
|
-
this.rl.prompt();
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
await this.processRequest(assembled);
|
|
1684
|
-
this.rl.prompt();
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
606
|
if (!trimmed) {
|
|
1688
|
-
this.rl.prompt();
|
|
1689
607
|
return;
|
|
1690
608
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
609
|
+
const lower = trimmed.toLowerCase();
|
|
610
|
+
if (lower === 'exit' || lower === 'quit') {
|
|
611
|
+
this.shutdown();
|
|
1693
612
|
return;
|
|
1694
613
|
}
|
|
1695
|
-
if (
|
|
614
|
+
if (lower === 'clear') {
|
|
1696
615
|
display.clear();
|
|
1697
|
-
this.
|
|
616
|
+
this.terminalInput.render();
|
|
1698
617
|
return;
|
|
1699
618
|
}
|
|
1700
|
-
if (
|
|
619
|
+
if (lower === 'help') {
|
|
1701
620
|
this.showHelp();
|
|
1702
|
-
this.
|
|
621
|
+
this.terminalInput.render();
|
|
1703
622
|
return;
|
|
1704
623
|
}
|
|
1705
624
|
if (trimmed.startsWith('/')) {
|
|
@@ -1709,12 +628,12 @@ export class InteractiveShell {
|
|
|
1709
628
|
// Check for continuous/infinite loop commands
|
|
1710
629
|
if (this.isContinuousCommand(trimmed)) {
|
|
1711
630
|
await this.processContinuousRequest(trimmed);
|
|
1712
|
-
this.
|
|
631
|
+
this.terminalInput.render();
|
|
1713
632
|
return;
|
|
1714
633
|
}
|
|
1715
634
|
// Direct execution for all inputs, including multi-line pastes
|
|
1716
635
|
await this.processRequest(trimmed);
|
|
1717
|
-
this.
|
|
636
|
+
this.terminalInput.render();
|
|
1718
637
|
}
|
|
1719
638
|
/**
|
|
1720
639
|
* Check if the command is a continuous/infinite loop command
|
|
@@ -1766,12 +685,6 @@ export class InteractiveShell {
|
|
|
1766
685
|
case 'agent-selection':
|
|
1767
686
|
await this.handleAgentSelectionInput(input);
|
|
1768
687
|
return true;
|
|
1769
|
-
case 'paste-edit':
|
|
1770
|
-
await this.handlePasteEditInput(input);
|
|
1771
|
-
return true;
|
|
1772
|
-
case 'paste-edit-block':
|
|
1773
|
-
await this.handlePasteBlockEditInput(input, this.pendingInteraction.blockId, this.pendingInteraction.blockNum);
|
|
1774
|
-
return true;
|
|
1775
688
|
default:
|
|
1776
689
|
return false;
|
|
1777
690
|
}
|
|
@@ -1780,7 +693,7 @@ export class InteractiveShell {
|
|
|
1780
693
|
const [command] = input.split(/\s+/);
|
|
1781
694
|
if (!command) {
|
|
1782
695
|
display.showWarning('Enter a slash command.');
|
|
1783
|
-
this.
|
|
696
|
+
this.terminalInput.render();
|
|
1784
697
|
return;
|
|
1785
698
|
}
|
|
1786
699
|
switch (command) {
|
|
@@ -1855,7 +768,7 @@ export class InteractiveShell {
|
|
|
1855
768
|
}
|
|
1856
769
|
break;
|
|
1857
770
|
}
|
|
1858
|
-
this.
|
|
771
|
+
this.terminalInput.render();
|
|
1859
772
|
}
|
|
1860
773
|
async tryCustomSlashCommand(command, fullInput) {
|
|
1861
774
|
const custom = this.customCommandMap.get(command);
|
|
@@ -1889,100 +802,6 @@ export class InteractiveShell {
|
|
|
1889
802
|
];
|
|
1890
803
|
display.showSystemMessage(info.join('\n'));
|
|
1891
804
|
}
|
|
1892
|
-
/**
|
|
1893
|
-
* Handle Tab key for slash command autocompletion
|
|
1894
|
-
* Completes partial slash commands like /mo -> /model
|
|
1895
|
-
*/
|
|
1896
|
-
handleTabCompletion() {
|
|
1897
|
-
const currentLine = this.rl.line || '';
|
|
1898
|
-
const cursorPos = this.rl.cursor || 0;
|
|
1899
|
-
// Only complete if line starts with /
|
|
1900
|
-
if (!currentLine.startsWith('/')) {
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
// Get the partial command (from / to cursor)
|
|
1904
|
-
const partial = currentLine.slice(0, cursorPos).toLowerCase();
|
|
1905
|
-
// Available slash commands
|
|
1906
|
-
const commands = [
|
|
1907
|
-
'/help',
|
|
1908
|
-
'/model',
|
|
1909
|
-
'/secrets',
|
|
1910
|
-
'/tools',
|
|
1911
|
-
'/doctor',
|
|
1912
|
-
'/clear',
|
|
1913
|
-
'/cancel',
|
|
1914
|
-
'/compact',
|
|
1915
|
-
'/cost',
|
|
1916
|
-
'/status',
|
|
1917
|
-
'/undo',
|
|
1918
|
-
'/redo',
|
|
1919
|
-
];
|
|
1920
|
-
// Find matching commands
|
|
1921
|
-
const matches = commands.filter(cmd => cmd.startsWith(partial));
|
|
1922
|
-
if (matches.length === 0) {
|
|
1923
|
-
// No matches - beep or do nothing
|
|
1924
|
-
return;
|
|
1925
|
-
}
|
|
1926
|
-
if (matches.length === 1) {
|
|
1927
|
-
// Single match - complete it
|
|
1928
|
-
const completion = matches[0];
|
|
1929
|
-
const suffix = currentLine.slice(cursorPos);
|
|
1930
|
-
const newLine = completion + suffix;
|
|
1931
|
-
const newCursor = completion.length;
|
|
1932
|
-
// Update readline
|
|
1933
|
-
this.rl.line = newLine;
|
|
1934
|
-
this.rl.cursor = newCursor;
|
|
1935
|
-
// Redraw
|
|
1936
|
-
output.write('\r\x1b[K'); // Clear line
|
|
1937
|
-
output.write(formatUserPrompt(this.profileLabel || this.profile));
|
|
1938
|
-
output.write(newLine);
|
|
1939
|
-
// Position cursor
|
|
1940
|
-
if (suffix.length > 0) {
|
|
1941
|
-
output.write(`\x1b[${suffix.length}D`);
|
|
1942
|
-
}
|
|
1943
|
-
// Sync to display components
|
|
1944
|
-
this.persistentPrompt.updateInput(newLine, newCursor);
|
|
1945
|
-
this.pinnedChatBox.setInput(newLine, newCursor);
|
|
1946
|
-
}
|
|
1947
|
-
else {
|
|
1948
|
-
// Multiple matches - show them
|
|
1949
|
-
output.write('\n');
|
|
1950
|
-
output.write(theme.ui.muted('Completions: ') + matches.join(' ') + '\n');
|
|
1951
|
-
this.rl.prompt();
|
|
1952
|
-
// Find common prefix for partial completion
|
|
1953
|
-
const commonPrefix = this.findCommonPrefix(matches);
|
|
1954
|
-
if (commonPrefix.length > partial.length) {
|
|
1955
|
-
const suffix = currentLine.slice(cursorPos);
|
|
1956
|
-
const newLine = commonPrefix + suffix;
|
|
1957
|
-
const newCursor = commonPrefix.length;
|
|
1958
|
-
this.rl.line = newLine;
|
|
1959
|
-
this.rl.cursor = newCursor;
|
|
1960
|
-
this.persistentPrompt.updateInput(newLine, newCursor);
|
|
1961
|
-
this.pinnedChatBox.setInput(newLine, newCursor);
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
/**
|
|
1966
|
-
* Find the longest common prefix among strings
|
|
1967
|
-
*/
|
|
1968
|
-
findCommonPrefix(strings) {
|
|
1969
|
-
if (strings.length === 0)
|
|
1970
|
-
return '';
|
|
1971
|
-
if (strings.length === 1)
|
|
1972
|
-
return strings[0];
|
|
1973
|
-
let prefix = strings[0];
|
|
1974
|
-
for (let i = 1; i < strings.length; i++) {
|
|
1975
|
-
const str = strings[i];
|
|
1976
|
-
let j = 0;
|
|
1977
|
-
while (j < prefix.length && j < str.length && prefix[j] === str[j]) {
|
|
1978
|
-
j++;
|
|
1979
|
-
}
|
|
1980
|
-
prefix = prefix.slice(0, j);
|
|
1981
|
-
if (prefix.length === 0)
|
|
1982
|
-
break;
|
|
1983
|
-
}
|
|
1984
|
-
return prefix;
|
|
1985
|
-
}
|
|
1986
805
|
runDoctor() {
|
|
1987
806
|
const lines = [];
|
|
1988
807
|
lines.push(theme.bold('Environment diagnostics'));
|
|
@@ -2876,29 +1695,29 @@ export class InteractiveShell {
|
|
|
2876
1695
|
const trimmed = input.trim();
|
|
2877
1696
|
if (!trimmed) {
|
|
2878
1697
|
display.showWarning('Enter a number or type cancel.');
|
|
2879
|
-
this.
|
|
1698
|
+
this.terminalInput.render();
|
|
2880
1699
|
return;
|
|
2881
1700
|
}
|
|
2882
1701
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2883
1702
|
this.pendingInteraction = null;
|
|
2884
1703
|
display.showInfo('Model selection cancelled.');
|
|
2885
|
-
this.
|
|
1704
|
+
this.terminalInput.render();
|
|
2886
1705
|
return;
|
|
2887
1706
|
}
|
|
2888
1707
|
const choice = Number.parseInt(trimmed, 10);
|
|
2889
1708
|
if (!Number.isFinite(choice)) {
|
|
2890
1709
|
display.showWarning('Please enter a valid number.');
|
|
2891
|
-
this.
|
|
1710
|
+
this.terminalInput.render();
|
|
2892
1711
|
return;
|
|
2893
1712
|
}
|
|
2894
1713
|
const option = pending.options[choice - 1];
|
|
2895
1714
|
if (!option) {
|
|
2896
1715
|
display.showWarning('That option is not available.');
|
|
2897
|
-
this.
|
|
1716
|
+
this.terminalInput.render();
|
|
2898
1717
|
return;
|
|
2899
1718
|
}
|
|
2900
1719
|
this.showProviderModels(option);
|
|
2901
|
-
this.
|
|
1720
|
+
this.terminalInput.render();
|
|
2902
1721
|
}
|
|
2903
1722
|
async handleModelSelection(input) {
|
|
2904
1723
|
const pending = this.pendingInteraction;
|
|
@@ -2908,35 +1727,35 @@ export class InteractiveShell {
|
|
|
2908
1727
|
const trimmed = input.trim();
|
|
2909
1728
|
if (!trimmed) {
|
|
2910
1729
|
display.showWarning('Enter a number, type "back", or type "cancel".');
|
|
2911
|
-
this.
|
|
1730
|
+
this.terminalInput.render();
|
|
2912
1731
|
return;
|
|
2913
1732
|
}
|
|
2914
1733
|
if (trimmed.toLowerCase() === 'back') {
|
|
2915
1734
|
this.showModelMenu();
|
|
2916
|
-
this.
|
|
1735
|
+
this.terminalInput.render();
|
|
2917
1736
|
return;
|
|
2918
1737
|
}
|
|
2919
1738
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2920
1739
|
this.pendingInteraction = null;
|
|
2921
1740
|
display.showInfo('Model selection cancelled.');
|
|
2922
|
-
this.
|
|
1741
|
+
this.terminalInput.render();
|
|
2923
1742
|
return;
|
|
2924
1743
|
}
|
|
2925
1744
|
const choice = Number.parseInt(trimmed, 10);
|
|
2926
1745
|
if (!Number.isFinite(choice)) {
|
|
2927
1746
|
display.showWarning('Please enter a valid number.');
|
|
2928
|
-
this.
|
|
1747
|
+
this.terminalInput.render();
|
|
2929
1748
|
return;
|
|
2930
1749
|
}
|
|
2931
1750
|
const preset = pending.options[choice - 1];
|
|
2932
1751
|
if (!preset) {
|
|
2933
1752
|
display.showWarning('That option is not available.');
|
|
2934
|
-
this.
|
|
1753
|
+
this.terminalInput.render();
|
|
2935
1754
|
return;
|
|
2936
1755
|
}
|
|
2937
1756
|
this.pendingInteraction = null;
|
|
2938
1757
|
await this.applyModelPreset(preset);
|
|
2939
|
-
this.
|
|
1758
|
+
this.terminalInput.render();
|
|
2940
1759
|
}
|
|
2941
1760
|
async applyModelPreset(preset) {
|
|
2942
1761
|
try {
|
|
@@ -2969,30 +1788,30 @@ export class InteractiveShell {
|
|
|
2969
1788
|
const trimmed = input.trim();
|
|
2970
1789
|
if (!trimmed) {
|
|
2971
1790
|
display.showWarning('Enter a number or type cancel.');
|
|
2972
|
-
this.
|
|
1791
|
+
this.terminalInput.render();
|
|
2973
1792
|
return;
|
|
2974
1793
|
}
|
|
2975
1794
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2976
1795
|
this.pendingInteraction = null;
|
|
2977
1796
|
display.showInfo('Secret management cancelled.');
|
|
2978
|
-
this.
|
|
1797
|
+
this.terminalInput.render();
|
|
2979
1798
|
return;
|
|
2980
1799
|
}
|
|
2981
1800
|
const choice = Number.parseInt(trimmed, 10);
|
|
2982
1801
|
if (!Number.isFinite(choice)) {
|
|
2983
1802
|
display.showWarning('Please enter a valid number.');
|
|
2984
|
-
this.
|
|
1803
|
+
this.terminalInput.render();
|
|
2985
1804
|
return;
|
|
2986
1805
|
}
|
|
2987
1806
|
const secret = pending.options[choice - 1];
|
|
2988
1807
|
if (!secret) {
|
|
2989
1808
|
display.showWarning('That option is not available.');
|
|
2990
|
-
this.
|
|
1809
|
+
this.terminalInput.render();
|
|
2991
1810
|
return;
|
|
2992
1811
|
}
|
|
2993
1812
|
display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
|
|
2994
1813
|
this.pendingInteraction = { type: 'secret-input', secret };
|
|
2995
|
-
this.
|
|
1814
|
+
this.terminalInput.render();
|
|
2996
1815
|
}
|
|
2997
1816
|
async handleSecretInput(input) {
|
|
2998
1817
|
const pending = this.pendingInteraction;
|
|
@@ -3002,14 +1821,14 @@ export class InteractiveShell {
|
|
|
3002
1821
|
const trimmed = input.trim();
|
|
3003
1822
|
if (!trimmed) {
|
|
3004
1823
|
display.showWarning('Enter a value or type cancel.');
|
|
3005
|
-
this.
|
|
1824
|
+
this.terminalInput.render();
|
|
3006
1825
|
return;
|
|
3007
1826
|
}
|
|
3008
1827
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
3009
1828
|
this.pendingInteraction = null;
|
|
3010
1829
|
this.pendingSecretRetry = null;
|
|
3011
1830
|
display.showInfo('Secret unchanged.');
|
|
3012
|
-
this.
|
|
1831
|
+
this.terminalInput.render();
|
|
3013
1832
|
return;
|
|
3014
1833
|
}
|
|
3015
1834
|
try {
|
|
@@ -3033,7 +1852,7 @@ export class InteractiveShell {
|
|
|
3033
1852
|
this.pendingInteraction = null;
|
|
3034
1853
|
this.pendingSecretRetry = null;
|
|
3035
1854
|
}
|
|
3036
|
-
this.
|
|
1855
|
+
this.terminalInput.render();
|
|
3037
1856
|
}
|
|
3038
1857
|
async processRequest(request) {
|
|
3039
1858
|
if (this.isProcessing) {
|
|
@@ -3049,11 +1868,8 @@ export class InteractiveShell {
|
|
|
3049
1868
|
return;
|
|
3050
1869
|
}
|
|
3051
1870
|
this.isProcessing = true;
|
|
1871
|
+
this.terminalInput.setStreaming(true);
|
|
3052
1872
|
const requestStartTime = Date.now(); // Alpha Zero 2 timing
|
|
3053
|
-
// Claude Code style: Enable scroll region for fixed chat box at bottom
|
|
3054
|
-
this.pinnedChatBox.setProcessing(true);
|
|
3055
|
-
// Claude Code style: Update status (optional, won't interfere with streaming)
|
|
3056
|
-
this.persistentPrompt.updateStatusBar({ message: '⏳ Processing...' });
|
|
3057
1873
|
this.uiAdapter.startProcessing('Working on your request');
|
|
3058
1874
|
this.setProcessingStatus();
|
|
3059
1875
|
try {
|
|
@@ -3062,13 +1878,6 @@ export class InteractiveShell {
|
|
|
3062
1878
|
// Claude Code style: Show unified streaming header before response
|
|
3063
1879
|
// This provides visual consistency with the startup Ready bar
|
|
3064
1880
|
display.showStreamingHeader();
|
|
3065
|
-
// NOTE: Do NOT call showWaitingPrompt() here - the PinnedChatBox scroll region
|
|
3066
|
-
// handles rendering the prompt at the bottom via renderPersistentInput().
|
|
3067
|
-
// Calling showWaitingPrompt() writes to the scroll area instead of the reserved bottom.
|
|
3068
|
-
// Claude Code style: Start streaming mode using UnifiedChatBox
|
|
3069
|
-
// - User input is captured and queued
|
|
3070
|
-
// - After response ends, new prompt appears at bottom
|
|
3071
|
-
this.unifiedChatBox.startStreaming();
|
|
3072
1881
|
// DISABLED streaming - wait for full response instead
|
|
3073
1882
|
// This keeps cursor at the > prompt during processing
|
|
3074
1883
|
await agent.send(request, false);
|
|
@@ -3087,30 +1896,20 @@ export class InteractiveShell {
|
|
|
3087
1896
|
}
|
|
3088
1897
|
}
|
|
3089
1898
|
finally {
|
|
3090
|
-
// Claude Code style: End streaming mode using UnifiedChatBox
|
|
3091
|
-
// This restores readline echo and returns queue info
|
|
3092
|
-
const streamResult = this.unifiedChatBox.endStreaming();
|
|
3093
|
-
// Process any queued inputs that came in during streaming
|
|
3094
|
-
this.processUnifiedChatBoxQueue();
|
|
3095
|
-
// Show queue indicator if there were queued inputs
|
|
3096
|
-
if (streamResult.queuedCount > 0) {
|
|
3097
|
-
this.unifiedChatBox.showQueueIndicator();
|
|
3098
|
-
}
|
|
3099
1899
|
display.stopThinking(false);
|
|
3100
1900
|
this.isProcessing = false;
|
|
3101
|
-
this.
|
|
1901
|
+
this.terminalInput.setStreaming(false);
|
|
3102
1902
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
3103
1903
|
this.setIdleStatus();
|
|
3104
1904
|
display.newLine();
|
|
3105
|
-
|
|
3106
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
1905
|
+
this.updateStatusMessage(null);
|
|
3107
1906
|
// Claude Code style: Show unified status bar before prompt
|
|
3108
1907
|
// This creates consistent UI between startup and post-streaming
|
|
3109
1908
|
this.showUnifiedStatusBar();
|
|
3110
1909
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
3111
1910
|
// Claude Code style: New prompt naturally appears at bottom
|
|
3112
1911
|
this.ensureReadlineReady();
|
|
3113
|
-
this.
|
|
1912
|
+
this.terminalInput.render();
|
|
3114
1913
|
this.scheduleQueueProcessing();
|
|
3115
1914
|
this.refreshQueueIndicators();
|
|
3116
1915
|
}
|
|
@@ -3142,26 +1941,17 @@ export class InteractiveShell {
|
|
|
3142
1941
|
return;
|
|
3143
1942
|
}
|
|
3144
1943
|
this.isProcessing = true;
|
|
1944
|
+
this.terminalInput.setStreaming(true);
|
|
3145
1945
|
const overallStartTime = Date.now();
|
|
3146
|
-
// Claude Code style: Enable scroll region for fixed chat box at bottom
|
|
3147
|
-
this.pinnedChatBox.setProcessing(true);
|
|
3148
1946
|
// Initialize the task completion detector
|
|
3149
1947
|
const completionDetector = getTaskCompletionDetector();
|
|
3150
1948
|
completionDetector.reset();
|
|
3151
|
-
// Claude Code style: Update status
|
|
3152
|
-
this.persistentPrompt.updateStatusBar({ message: '🔄 Continuous mode...' });
|
|
3153
1949
|
display.showSystemMessage(`🔄 Starting continuous execution mode. Press Ctrl+C to stop.`);
|
|
3154
1950
|
display.showSystemMessage(`📊 Using intelligent task completion detection with AI verification.`);
|
|
3155
1951
|
this.uiAdapter.startProcessing('Continuous execution mode');
|
|
3156
1952
|
this.setProcessingStatus();
|
|
3157
1953
|
// Claude Code style: Show unified streaming header before response
|
|
3158
1954
|
display.showStreamingHeader();
|
|
3159
|
-
// NOTE: Do NOT call showWaitingPrompt() here - the PinnedChatBox scroll region
|
|
3160
|
-
// handles rendering the prompt at the bottom via renderPersistentInput().
|
|
3161
|
-
// Calling showWaitingPrompt() writes to the scroll area instead of the reserved bottom.
|
|
3162
|
-
// Claude Code style: Start streaming mode using UnifiedChatBox
|
|
3163
|
-
// - User input is captured and queued
|
|
3164
|
-
this.unifiedChatBox.startStreaming();
|
|
3165
1955
|
let iteration = 0;
|
|
3166
1956
|
let lastResponse = '';
|
|
3167
1957
|
let consecutiveNoProgress = 0;
|
|
@@ -3187,9 +1977,7 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
|
|
|
3187
1977
|
while (iteration < MAX_ITERATIONS) {
|
|
3188
1978
|
iteration++;
|
|
3189
1979
|
display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
|
|
3190
|
-
|
|
3191
|
-
this.pinnedChatBox.setStatusMessage(`Working on iteration ${iteration}...`);
|
|
3192
|
-
this.pinnedChatBox.forceRender();
|
|
1980
|
+
this.updateStatusMessage(`Working on iteration ${iteration}...`);
|
|
3193
1981
|
try {
|
|
3194
1982
|
// Send the request and capture the response (streaming disabled)
|
|
3195
1983
|
const response = await agent.send(currentPrompt, false);
|
|
@@ -3323,28 +2111,18 @@ What's the next action?`;
|
|
|
3323
2111
|
display.showSystemMessage(`\n🏁 Continuous execution completed: ${iteration} iterations, ${minutes}m ${seconds}s total`);
|
|
3324
2112
|
// Reset completion detector for next task
|
|
3325
2113
|
resetTaskCompletionDetector();
|
|
3326
|
-
// Claude Code style: End streaming mode using UnifiedChatBox
|
|
3327
|
-
const streamResult = this.unifiedChatBox.endStreaming();
|
|
3328
|
-
// Process any queued inputs that came in during streaming
|
|
3329
|
-
this.processUnifiedChatBoxQueue();
|
|
3330
|
-
// Show queue indicator if there were queued inputs
|
|
3331
|
-
if (streamResult.queuedCount > 0) {
|
|
3332
|
-
this.unifiedChatBox.showQueueIndicator();
|
|
3333
|
-
}
|
|
3334
2114
|
this.isProcessing = false;
|
|
3335
|
-
this.
|
|
2115
|
+
this.terminalInput.setStreaming(false);
|
|
2116
|
+
this.updateStatusMessage(null);
|
|
3336
2117
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
3337
2118
|
this.setIdleStatus();
|
|
3338
2119
|
display.newLine();
|
|
3339
|
-
// Clear the processing status
|
|
3340
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
3341
2120
|
// Claude Code style: Show unified status bar before prompt
|
|
3342
2121
|
// This creates consistent UI between startup and post-streaming
|
|
3343
2122
|
this.showUnifiedStatusBar();
|
|
3344
2123
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
3345
2124
|
// Claude Code style: New prompt naturally appears at bottom
|
|
3346
2125
|
this.ensureReadlineReady();
|
|
3347
|
-
this.rl.prompt();
|
|
3348
2126
|
this.scheduleQueueProcessing();
|
|
3349
2127
|
this.refreshQueueIndicators();
|
|
3350
2128
|
}
|
|
@@ -3552,8 +2330,7 @@ What's the next action?`;
|
|
|
3552
2330
|
this.autoTestInFlight = true;
|
|
3553
2331
|
const command = 'npm test -- --runInBand';
|
|
3554
2332
|
display.showSystemMessage(`🧪 Auto-testing recent changes (${trigger}) with "${command}"...`);
|
|
3555
|
-
this.
|
|
3556
|
-
this.pinnedChatBox.forceRender();
|
|
2333
|
+
this.updateStatusMessage('Running tests automatically...');
|
|
3557
2334
|
try {
|
|
3558
2335
|
const { stdout, stderr } = await execAsync(command, {
|
|
3559
2336
|
cwd: this.workingDir,
|
|
@@ -3581,8 +2358,7 @@ What's the next action?`;
|
|
|
3581
2358
|
});
|
|
3582
2359
|
}
|
|
3583
2360
|
finally {
|
|
3584
|
-
this.
|
|
3585
|
-
this.pinnedChatBox.forceRender();
|
|
2361
|
+
this.updateStatusMessage(null);
|
|
3586
2362
|
this.autoTestInFlight = false;
|
|
3587
2363
|
}
|
|
3588
2364
|
}
|
|
@@ -3653,7 +2429,7 @@ What's the next action?`;
|
|
|
3653
2429
|
display.showNarrative(content.trim());
|
|
3654
2430
|
}
|
|
3655
2431
|
// The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
|
|
3656
|
-
this.
|
|
2432
|
+
this.terminalInput.render();
|
|
3657
2433
|
return;
|
|
3658
2434
|
}
|
|
3659
2435
|
const cleanup = this.handleContextTelemetry(metadata, enriched);
|
|
@@ -3672,10 +2448,6 @@ What's the next action?`;
|
|
|
3672
2448
|
onContextRecovery: (attempt, maxAttempts, message) => {
|
|
3673
2449
|
// Show recovery progress in UI
|
|
3674
2450
|
display.showSystemMessage(`⚡ Context Recovery (${attempt}/${maxAttempts}): ${message}`);
|
|
3675
|
-
// Update persistent prompt to show recovery status
|
|
3676
|
-
this.persistentPrompt.updateStatusBar({
|
|
3677
|
-
contextUsage: 100, // Show as full during recovery
|
|
3678
|
-
});
|
|
3679
2451
|
},
|
|
3680
2452
|
onContextPruned: (removedCount, stats) => {
|
|
3681
2453
|
// Clear squish overlay if active
|
|
@@ -3689,30 +2461,28 @@ What's the next action?`;
|
|
|
3689
2461
|
// Update context usage in UI
|
|
3690
2462
|
if (typeof percentage === 'number') {
|
|
3691
2463
|
this.uiAdapter.updateContextUsage(percentage);
|
|
3692
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentage });
|
|
3693
2464
|
}
|
|
3694
2465
|
// Ensure prompt remains visible at bottom after context messages
|
|
3695
|
-
this.
|
|
2466
|
+
this.terminalInput.render();
|
|
3696
2467
|
},
|
|
3697
2468
|
onContinueAfterRecovery: () => {
|
|
3698
2469
|
// Update UI to show we're continuing after context recovery
|
|
3699
2470
|
display.showSystemMessage(`🔄 Continuing after context recovery...`);
|
|
3700
|
-
|
|
3701
|
-
this.
|
|
3702
|
-
this.pinnedChatBox.forceRender();
|
|
2471
|
+
this.updateStatusMessage('Retrying with reduced context...');
|
|
2472
|
+
this.terminalInput.render();
|
|
3703
2473
|
},
|
|
3704
2474
|
onAutoContinue: (attempt, maxAttempts, _message) => {
|
|
3705
2475
|
// Show auto-continue progress in UI
|
|
3706
2476
|
display.showSystemMessage(`🔄 Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
|
|
3707
|
-
this.
|
|
3708
|
-
this.
|
|
2477
|
+
this.updateStatusMessage('Auto-continuing...');
|
|
2478
|
+
this.terminalInput.render();
|
|
3709
2479
|
},
|
|
3710
2480
|
onCancelled: () => {
|
|
3711
2481
|
// Update UI to show operation was cancelled
|
|
3712
2482
|
display.showWarning('Operation cancelled.');
|
|
3713
|
-
this.
|
|
3714
|
-
this.
|
|
3715
|
-
this.
|
|
2483
|
+
this.updateStatusMessage(null);
|
|
2484
|
+
this.terminalInput.setStreaming(false);
|
|
2485
|
+
this.terminalInput.render();
|
|
3716
2486
|
},
|
|
3717
2487
|
onVerificationNeeded: () => {
|
|
3718
2488
|
void this.enforceAutoTests('verification');
|
|
@@ -3746,10 +2516,9 @@ What's the next action?`;
|
|
|
3746
2516
|
* just like on fresh startup.
|
|
3747
2517
|
*/
|
|
3748
2518
|
resetChatBoxAfterModelSwap() {
|
|
3749
|
-
this.
|
|
3750
|
-
this.
|
|
3751
|
-
this.
|
|
3752
|
-
this.pinnedChatBox.forceRender();
|
|
2519
|
+
this.updateStatusMessage(null);
|
|
2520
|
+
this.terminalInput.setStreaming(false);
|
|
2521
|
+
this.terminalInput.render();
|
|
3753
2522
|
this.ensureReadlineReady();
|
|
3754
2523
|
}
|
|
3755
2524
|
buildSystemPrompt() {
|
|
@@ -3816,10 +2585,6 @@ What's the next action?`;
|
|
|
3816
2585
|
// Always update context usage in the UI
|
|
3817
2586
|
const percentUsed = Math.round(usageRatio * 100);
|
|
3818
2587
|
this.uiAdapter.updateContextUsage(percentUsed);
|
|
3819
|
-
// Update persistent prompt status bar with context usage
|
|
3820
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
|
|
3821
|
-
// Update pinned chat box with context usage
|
|
3822
|
-
this.pinnedChatBox.setContextUsage(percentUsed);
|
|
3823
2588
|
if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
|
|
3824
2589
|
return null;
|
|
3825
2590
|
}
|
|
@@ -3867,8 +2632,6 @@ What's the next action?`;
|
|
|
3867
2632
|
const percentUsed = Math.round((totalTokens / windowTokens) * 100);
|
|
3868
2633
|
// Update context usage in unified UI
|
|
3869
2634
|
this.uiAdapter.updateContextUsage(percentUsed);
|
|
3870
|
-
// Update persistent prompt status bar with context usage
|
|
3871
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
|
|
3872
2635
|
display.showSystemMessage([
|
|
3873
2636
|
`Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
|
|
3874
2637
|
`(${percentUsed}% full). Running automatic cleanup...`,
|
|
@@ -4105,14 +2868,6 @@ What's the next action?`;
|
|
|
4105
2868
|
const entry = this.agentMenu.options.find((option) => option.name === name);
|
|
4106
2869
|
return entry?.label ?? name;
|
|
4107
2870
|
}
|
|
4108
|
-
updatePersistentPromptFileChanges() {
|
|
4109
|
-
const summary = this._fileChangeTracker.getSummary();
|
|
4110
|
-
if (summary.files === 0) {
|
|
4111
|
-
return;
|
|
4112
|
-
}
|
|
4113
|
-
const fileChangesText = `${summary.files} file${summary.files === 1 ? '' : 's'} +${summary.additions} -${summary.removals}`;
|
|
4114
|
-
this.persistentPrompt.updateStatusBar({ fileChanges: fileChangesText });
|
|
4115
|
-
}
|
|
4116
2871
|
extractThoughtSummary(thought) {
|
|
4117
2872
|
// Extract first non-empty line
|
|
4118
2873
|
const lines = thought?.split('\n').filter(line => line.trim()) ?? [];
|
|
@@ -4402,32 +3157,6 @@ What's the next action?`;
|
|
|
4402
3157
|
];
|
|
4403
3158
|
display.showSystemMessage(lines.join('\n'));
|
|
4404
3159
|
}
|
|
4405
|
-
enableBracketedPasteMode() {
|
|
4406
|
-
if (!input.isTTY || !output.isTTY) {
|
|
4407
|
-
return false;
|
|
4408
|
-
}
|
|
4409
|
-
try {
|
|
4410
|
-
output.write(BRACKETED_PASTE_ENABLE);
|
|
4411
|
-
return true;
|
|
4412
|
-
}
|
|
4413
|
-
catch (error) {
|
|
4414
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4415
|
-
display.showWarning(`Unable to enable bracketed paste: ${message}`);
|
|
4416
|
-
return false;
|
|
4417
|
-
}
|
|
4418
|
-
}
|
|
4419
|
-
disableBracketedPasteMode() {
|
|
4420
|
-
if (!this.bracketedPasteEnabled || !output.isTTY) {
|
|
4421
|
-
return;
|
|
4422
|
-
}
|
|
4423
|
-
try {
|
|
4424
|
-
output.write(BRACKETED_PASTE_DISABLE);
|
|
4425
|
-
}
|
|
4426
|
-
finally {
|
|
4427
|
-
this.bracketedPasteEnabled = false;
|
|
4428
|
-
this.bracketedPaste.reset();
|
|
4429
|
-
}
|
|
4430
|
-
}
|
|
4431
3160
|
/**
|
|
4432
3161
|
* Set the cached provider status for unified status bar display.
|
|
4433
3162
|
* Called once at startup after checking providers.
|
|
@@ -4435,10 +3164,6 @@ What's the next action?`;
|
|
|
4435
3164
|
setProviderStatus(providers) {
|
|
4436
3165
|
this.cachedProviderStatus = providers;
|
|
4437
3166
|
}
|
|
4438
|
-
// NOTE: showWaitingPrompt() was removed because the PinnedChatBox scroll region
|
|
4439
|
-
// now handles rendering the prompt at the bottom via renderPersistentInput().
|
|
4440
|
-
// The scroll region approach keeps the cursor in the correct location during
|
|
4441
|
-
// streaming while allowing the user to type in the reserved bottom area.
|
|
4442
3167
|
/**
|
|
4443
3168
|
* Show the unified status bar (Claude Code style).
|
|
4444
3169
|
* Displays provider indicators and ready hints before the prompt.
|