erosolar-cli 1.7.190 → 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 -1476
- 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/persistentPrompt.d.ts +9 -13
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +12 -57
- package/dist/ui/persistentPrompt.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,380 +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
|
-
// Handle SIGINT (Ctrl+C) - clear text if present, otherwise close
|
|
530
|
-
// This prevents readline from auto-closing when there's text to clear
|
|
531
|
-
this.rl.on('SIGINT', () => {
|
|
532
|
-
// During processing, check pinnedChatBox input first
|
|
533
|
-
if (this.isProcessing) {
|
|
534
|
-
const chatBoxInput = this.pinnedChatBox.getInput();
|
|
535
|
-
if (chatBoxInput.length > 0) {
|
|
536
|
-
// Clear the chat box input instead of exiting
|
|
537
|
-
this.pinnedChatBox.clearPastedBlockState();
|
|
538
|
-
this.pinnedChatBox.updatePersistentInput();
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
// No text in chat box during processing - cancel the agent
|
|
542
|
-
if (this.agent) {
|
|
543
|
-
this.agent.requestCancellation();
|
|
544
|
-
output.write('\n');
|
|
545
|
-
display.showWarning('Cancelling current operation... (Ctrl+C again to force quit)');
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
// Not processing - check readline input
|
|
550
|
-
const currentLine = this.rl.line || '';
|
|
551
|
-
if (currentLine.length > 0) {
|
|
552
|
-
// Clear the input buffer instead of exiting
|
|
553
|
-
this.rl.line = '';
|
|
554
|
-
this.rl.cursor = 0;
|
|
555
|
-
this.persistentPrompt.updateInput('', 0);
|
|
556
|
-
this.pinnedChatBox.setInput('');
|
|
557
|
-
this.pinnedChatBox.clearPastedBlockState();
|
|
558
|
-
// Clear the displayed line and show fresh prompt
|
|
559
|
-
output.write('\r\x1b[K'); // Clear current line
|
|
560
|
-
output.write('^C\n'); // Show ^C indicator
|
|
561
|
-
this.rl.prompt(); // Re-show prompt
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
// No text anywhere - close readline to trigger exit
|
|
565
|
-
this.rl.close();
|
|
566
|
-
});
|
|
567
444
|
// Handle terminal resize
|
|
568
445
|
output.on('resize', () => {
|
|
569
|
-
this.
|
|
570
|
-
this.pinnedChatBox.handleResize();
|
|
571
|
-
});
|
|
572
|
-
this.setupSlashCommandPreviewHandler();
|
|
573
|
-
// Show initial persistent prompt
|
|
574
|
-
this.persistentPrompt.show();
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Set up raw stdin data interception for bracketed paste mode.
|
|
578
|
-
* This intercepts data before readline processes it, allowing us to
|
|
579
|
-
* capture complete multi-line pastes without readline splitting them.
|
|
580
|
-
*/
|
|
581
|
-
setupRawPasteHandler() {
|
|
582
|
-
if (!this.bracketedPasteEnabled) {
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
const inputStream = input;
|
|
586
|
-
if (!inputStream || !inputStream.isTTY) {
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
// Set up callback for when a complete paste is captured
|
|
590
|
-
this.bracketedPaste.setRawPasteCallback((content) => {
|
|
591
|
-
this.clearMultiLinePastePreview();
|
|
592
|
-
const lines = content.split('\n');
|
|
593
|
-
const lineCount = lines.length;
|
|
594
|
-
// When streaming, route pastes into the pinned chat box instead of readline
|
|
595
|
-
if (this.isProcessing) {
|
|
596
|
-
this.pinnedChatBox.handlePaste(content);
|
|
597
|
-
this.pinnedChatBox.updatePersistentInput();
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
// All pastes (single or multi-line) are captured for confirmation before submit
|
|
601
|
-
void this.capturePaste(content, lineCount);
|
|
446
|
+
this.terminalInput.handleResize();
|
|
602
447
|
});
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
// Strategy: Replace stdin's 'data' event emission during paste capture.
|
|
606
|
-
const originalEmit = inputStream.emit.bind(inputStream);
|
|
607
|
-
inputStream.emit = (event, ...args) => {
|
|
608
|
-
if (event === 'data' && args[0]) {
|
|
609
|
-
const data = args[0];
|
|
610
|
-
const str = typeof data === 'string' ? data : data.toString();
|
|
611
|
-
const result = this.bracketedPaste.processRawData(str);
|
|
612
|
-
if (result.consumed) {
|
|
613
|
-
// Data was consumed by paste handler - don't pass to readline
|
|
614
|
-
// If there's passThrough data, emit that instead
|
|
615
|
-
if (result.passThrough) {
|
|
616
|
-
return originalEmit('data', Buffer.from(result.passThrough));
|
|
617
|
-
}
|
|
618
|
-
return true; // Event "handled" but not passed to other listeners
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
// Pass through all other events and non-paste data normally
|
|
622
|
-
return originalEmit(event, ...args);
|
|
623
|
-
};
|
|
624
|
-
// Store reference for cleanup
|
|
625
|
-
this.rawDataHandler = () => {
|
|
626
|
-
inputStream.emit = originalEmit;
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
setupSlashCommandPreviewHandler() {
|
|
630
|
-
const inputStream = input;
|
|
631
|
-
if (!inputStream || typeof inputStream.on !== 'function' || !inputStream.isTTY) {
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
if (inputStream.listenerCount('keypress') === 0) {
|
|
635
|
-
readline.emitKeypressEvents(inputStream, this.rl);
|
|
636
|
-
}
|
|
637
|
-
// Ensure raw mode for keypress events
|
|
638
|
-
if (inputStream.setRawMode && !inputStream.isRaw) {
|
|
639
|
-
inputStream.setRawMode(true);
|
|
640
|
-
}
|
|
641
|
-
this.keypressHandler = (_str, key) => {
|
|
642
|
-
// Handle special keys
|
|
643
|
-
if (key) {
|
|
644
|
-
// Shift+Tab: Cycle paste preview options if paste blocks exist, otherwise profile switching
|
|
645
|
-
if (key.name === 'tab' && key.shift) {
|
|
646
|
-
// Check if there are paste blocks to cycle through
|
|
647
|
-
const pasteBlockCount = this.composableMessage.getPasteBlockCount();
|
|
648
|
-
if (pasteBlockCount > 0) {
|
|
649
|
-
this.cyclePastePreview();
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
// Fall through to profile switching if no paste blocks
|
|
653
|
-
if (this.agentMenu) {
|
|
654
|
-
this.showProfileSwitcher();
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
// Tab: Autocomplete slash commands
|
|
659
|
-
if (key.name === 'tab' && !key.shift && !key.ctrl) {
|
|
660
|
-
this.handleTabCompletion();
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
// Escape: Cancel current operation if agent is running
|
|
664
|
-
if (key.name === 'escape') {
|
|
665
|
-
if (this.isProcessing && this.agent) {
|
|
666
|
-
this.agent.requestCancellation();
|
|
667
|
-
output.write('\n');
|
|
668
|
-
display.showWarning('Cancelling current operation... (waiting for safe stop point)');
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
// If not processing, clear any queued follow-ups
|
|
672
|
-
if (this.followUpQueue.length > 0) {
|
|
673
|
-
const cleared = this.followUpQueue.length;
|
|
674
|
-
this.followUpQueue.length = 0;
|
|
675
|
-
this.refreshQueueIndicators();
|
|
676
|
-
output.write('\n');
|
|
677
|
-
display.showInfo(`Cleared ${cleared} queued follow-up${cleared === 1 ? '' : 's'}.`);
|
|
678
|
-
this.rl.prompt();
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
// Ctrl+C: Clear input first, then cancel operation if no input
|
|
683
|
-
if (key.ctrl && key.name === 'c') {
|
|
684
|
-
// During processing, check pinnedChatBox input first
|
|
685
|
-
if (this.isProcessing) {
|
|
686
|
-
const chatBoxInput = this.pinnedChatBox.getInput();
|
|
687
|
-
if (chatBoxInput.length > 0) {
|
|
688
|
-
// Clear the chat box input instead of cancelling
|
|
689
|
-
this.pinnedChatBox.clearPastedBlockState();
|
|
690
|
-
this.pinnedChatBox.updatePersistentInput();
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
// No text in chat box, cancel the agent
|
|
694
|
-
if (this.agent) {
|
|
695
|
-
this.agent.requestCancellation();
|
|
696
|
-
output.write('\n');
|
|
697
|
-
display.showWarning('Cancelling current operation... (Ctrl+C again to force quit)');
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
// Not processing - check readline input
|
|
702
|
-
const currentLine = this.rl.line || '';
|
|
703
|
-
if (currentLine.length > 0) {
|
|
704
|
-
// Clear the input buffer instead of exiting
|
|
705
|
-
this.rl.line = '';
|
|
706
|
-
this.rl.cursor = 0;
|
|
707
|
-
this.persistentPrompt.updateInput('', 0);
|
|
708
|
-
this.pinnedChatBox.setInput('');
|
|
709
|
-
this.pinnedChatBox.clearPastedBlockState();
|
|
710
|
-
// Clear the displayed line and show fresh prompt
|
|
711
|
-
output.write('\r\x1b[K'); // Clear current line
|
|
712
|
-
output.write('^C\n'); // Show ^C indicator
|
|
713
|
-
this.rl.prompt(); // Re-show prompt
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
// If no text in buffer, let default Ctrl+C behavior exit
|
|
717
|
-
}
|
|
718
|
-
// Ctrl+G: Edit pasted content blocks
|
|
719
|
-
if (key.ctrl && key.name === 'g') {
|
|
720
|
-
this.showPasteBlockEditor();
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
// Claude Code approach: During processing, route input to PinnedChatBox
|
|
725
|
-
// - User can type and see their input in the visible chat box at bottom
|
|
726
|
-
// - Input is queued until streaming ends
|
|
727
|
-
// - After each keystroke, we re-render the bottom area to show the input
|
|
728
|
-
if (this.isProcessing) {
|
|
729
|
-
// Handle paste: if _str contains newlines, it's a paste - don't auto-submit
|
|
730
|
-
const isPaste = typeof _str === 'string' && (_str.length > 1 || _str.includes('\n'));
|
|
731
|
-
if (key) {
|
|
732
|
-
if (key.name === 'backspace') {
|
|
733
|
-
this.pinnedChatBox.handleBackspace();
|
|
734
|
-
}
|
|
735
|
-
else if (key.name === 'delete') {
|
|
736
|
-
this.pinnedChatBox.handleDelete();
|
|
737
|
-
}
|
|
738
|
-
else if (key.name === 'left') {
|
|
739
|
-
if (key.meta || key.alt) {
|
|
740
|
-
this.pinnedChatBox.handleWordLeft();
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
this.pinnedChatBox.handleCursorLeft();
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
else if (key.name === 'right') {
|
|
747
|
-
if (key.meta || key.alt) {
|
|
748
|
-
this.pinnedChatBox.handleWordRight();
|
|
749
|
-
}
|
|
750
|
-
else {
|
|
751
|
-
this.pinnedChatBox.handleCursorRight();
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
else if (key.name === 'up') {
|
|
755
|
-
this.pinnedChatBox.handleHistoryUp();
|
|
756
|
-
}
|
|
757
|
-
else if (key.name === 'down') {
|
|
758
|
-
this.pinnedChatBox.handleHistoryDown();
|
|
759
|
-
}
|
|
760
|
-
else if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
|
|
761
|
-
this.pinnedChatBox.handleHome();
|
|
762
|
-
}
|
|
763
|
-
else if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
|
|
764
|
-
this.pinnedChatBox.handleEnd();
|
|
765
|
-
}
|
|
766
|
-
else if (key.ctrl && key.name === 'u') {
|
|
767
|
-
this.pinnedChatBox.handleDeleteToStart();
|
|
768
|
-
}
|
|
769
|
-
else if (key.ctrl && key.name === 'k') {
|
|
770
|
-
this.pinnedChatBox.handleDeleteToEnd();
|
|
771
|
-
}
|
|
772
|
-
else if (key.ctrl && key.name === 'w') {
|
|
773
|
-
this.pinnedChatBox.handleDeleteWord();
|
|
774
|
-
}
|
|
775
|
-
else if (key.name === 'return' && (key.shift || key.meta || key.ctrl)) {
|
|
776
|
-
// Shift+Enter or Option+Enter: Insert newline for multi-line input
|
|
777
|
-
this.pinnedChatBox.handleNewline();
|
|
778
|
-
}
|
|
779
|
-
else if (key.name === 'return' && !isPaste) {
|
|
780
|
-
// Queue the command - but NOT if this is part of a paste
|
|
781
|
-
const result = this.pinnedChatBox.handleSubmit();
|
|
782
|
-
if (result) {
|
|
783
|
-
const preview = result.replace(/\s+/g, ' ').trim();
|
|
784
|
-
const displayText = preview.length > 50 ? `${preview.slice(0, 47)}...` : preview;
|
|
785
|
-
display.showInfo(`📝 Queued: "${displayText}"`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
else if (_str && !key.ctrl && !key.meta) {
|
|
789
|
-
// Route raw input (including multi-line paste) to the pinned chat box
|
|
790
|
-
this.pinnedChatBox.handleInput(_str, { allowNewlines: true, isPaste });
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
else if (_str) {
|
|
794
|
-
// String without key info (paste) - pass through for summary handling
|
|
795
|
-
this.pinnedChatBox.handleInput(_str, { allowNewlines: true, isPaste: true });
|
|
796
|
-
}
|
|
797
|
-
// Re-render the bottom chat box to show the updated input
|
|
798
|
-
this.pinnedChatBox.updatePersistentInput();
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
// Readline handles all keyboard input natively (history, shortcuts, etc.)
|
|
802
|
-
// We just sync the current state to our display components
|
|
803
|
-
// Use setImmediate to get the updated line after readline processes the key
|
|
804
|
-
setImmediate(() => {
|
|
805
|
-
const currentLine = this.rl.line || '';
|
|
806
|
-
const cursorPos = this.rl.cursor || 0;
|
|
807
|
-
this.persistentPrompt.updateInput(currentLine, cursorPos);
|
|
808
|
-
// Sync to pinned chat box for display only
|
|
809
|
-
this.pinnedChatBox.setInput(currentLine, cursorPos);
|
|
810
|
-
if (this.composableMessage.hasContent()) {
|
|
811
|
-
this.composableMessage.setDraft(currentLine);
|
|
812
|
-
this.updateComposeStatusSummary();
|
|
813
|
-
}
|
|
814
|
-
this.handleSlashCommandPreviewChange();
|
|
815
|
-
});
|
|
816
|
-
};
|
|
817
|
-
// Use prependListener to handle before readline
|
|
818
|
-
inputStream.prependListener('keypress', this.keypressHandler);
|
|
448
|
+
// Show initial input UI
|
|
449
|
+
this.terminalInput.render();
|
|
819
450
|
}
|
|
820
451
|
setupStatusTracking() {
|
|
821
452
|
this.statusSubscription = this.statusTracker.subscribe((_state) => {
|
|
@@ -873,14 +504,10 @@ export class InteractiveShell {
|
|
|
873
504
|
if (!shouldShow && this.slashPreviewVisible) {
|
|
874
505
|
this.slashPreviewVisible = false;
|
|
875
506
|
this.uiAdapter.hideSlashCommandPreview();
|
|
876
|
-
// Show persistent prompt again after hiding overlay
|
|
877
|
-
if (!this.isProcessing) {
|
|
878
|
-
this.persistentPrompt.show();
|
|
879
|
-
}
|
|
880
507
|
}
|
|
881
508
|
}
|
|
882
509
|
shouldShowSlashCommandPreview() {
|
|
883
|
-
const line = this.
|
|
510
|
+
const line = this.currentInput ?? '';
|
|
884
511
|
if (!line.trim()) {
|
|
885
512
|
return false;
|
|
886
513
|
}
|
|
@@ -889,642 +516,28 @@ export class InteractiveShell {
|
|
|
889
516
|
}
|
|
890
517
|
showSlashCommandPreview() {
|
|
891
518
|
// Filter commands based on current input
|
|
892
|
-
const line = this.
|
|
519
|
+
const line = this.currentInput ?? '';
|
|
893
520
|
const trimmed = line.trimStart();
|
|
894
521
|
// Filter commands that match the current input
|
|
895
522
|
const filtered = this.slashCommands.filter(cmd => cmd.command.startsWith(trimmed) || trimmed === '/');
|
|
896
|
-
// Hide persistent prompt to avoid conflicts with overlay
|
|
897
|
-
this.persistentPrompt.hide();
|
|
898
523
|
// Show in the unified UI with dynamic overlay
|
|
899
524
|
this.uiAdapter.showSlashCommandPreview(filtered);
|
|
900
525
|
// Don't reprompt - this causes flickering
|
|
901
526
|
}
|
|
902
|
-
showProfileSwitcher() {
|
|
903
|
-
if (!this.agentMenu) {
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
// Build profile options with current/next indicators
|
|
907
|
-
const profiles = this.agentMenu.options.map((option, index) => {
|
|
908
|
-
const badges = [];
|
|
909
|
-
const nextProfile = this.agentMenu.persistedProfile ?? this.agentMenu.defaultProfile;
|
|
910
|
-
if (option.name === this.profile) {
|
|
911
|
-
badges.push('current');
|
|
912
|
-
}
|
|
913
|
-
if (option.name === nextProfile && option.name !== this.profile) {
|
|
914
|
-
badges.push('next');
|
|
915
|
-
}
|
|
916
|
-
const badgeText = badges.length > 0 ? ` (${badges.join(', ')})` : '';
|
|
917
|
-
return {
|
|
918
|
-
command: `${index + 1}. ${option.label}${badgeText}`,
|
|
919
|
-
description: `${this.providerLabel(option.defaultProvider)} • ${option.defaultModel}`,
|
|
920
|
-
};
|
|
921
|
-
});
|
|
922
|
-
// Show profile switcher overlay
|
|
923
|
-
this.uiAdapter.showProfileSwitcher(profiles, this.profileLabel);
|
|
924
|
-
}
|
|
925
527
|
/**
|
|
926
|
-
* Ensure
|
|
927
|
-
* This recovers from cases where a nested readline/prompt or partial paste
|
|
928
|
-
* left the stream paused, raw mode disabled, or keypress listeners detached.
|
|
528
|
+
* Ensure the terminal input is ready for interactive input.
|
|
929
529
|
*/
|
|
930
530
|
ensureReadlineReady() {
|
|
931
|
-
|
|
932
|
-
this.bracketedPaste.reset();
|
|
933
|
-
// Always ensure the pinned chat box is visible when readline is ready
|
|
934
|
-
this.pinnedChatBox.show();
|
|
935
|
-
this.pinnedChatBox.forceRender();
|
|
936
|
-
const inputStream = input;
|
|
937
|
-
if (!inputStream) {
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
// Resume stdin if another consumer paused it
|
|
941
|
-
if (typeof inputStream.isPaused === 'function' && inputStream.isPaused()) {
|
|
942
|
-
inputStream.resume();
|
|
943
|
-
}
|
|
944
|
-
else if (inputStream.readableFlowing === false) {
|
|
945
|
-
inputStream.resume();
|
|
946
|
-
}
|
|
947
|
-
// Restore raw mode if a nested readline turned it off
|
|
948
|
-
if (inputStream.isTTY && typeof inputStream.isRaw === 'boolean') {
|
|
949
|
-
const ttyStream = inputStream;
|
|
950
|
-
if (!ttyStream.isRaw) {
|
|
951
|
-
ttyStream.setRawMode(true);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
// Reattach keypress handler if it was removed
|
|
955
|
-
if (this.keypressHandler) {
|
|
956
|
-
const listeners = inputStream.listeners('keypress');
|
|
957
|
-
const hasHandler = listeners.includes(this.keypressHandler);
|
|
958
|
-
if (!hasHandler) {
|
|
959
|
-
if (inputStream.listenerCount('keypress') === 0) {
|
|
960
|
-
readline.emitKeypressEvents(inputStream, this.rl);
|
|
961
|
-
}
|
|
962
|
-
inputStream.on('keypress', this.keypressHandler);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
// Restore readline output if it was suppressed
|
|
966
|
-
// Note: Stream handler manages its own state, this is for legacy compatibility
|
|
967
|
-
if (!this.streamHandler.isStreaming()) {
|
|
968
|
-
this.restoreReadlineOutput();
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
/**
|
|
972
|
-
* Suppress readline's character echo during streaming.
|
|
973
|
-
* Characters typed will be captured but not echoed to the main output.
|
|
974
|
-
* Instead, they appear only in the persistent input box at the bottom.
|
|
975
|
-
*
|
|
976
|
-
* @deprecated Use streamHandler.startStreaming() instead - kept for backwards compatibility
|
|
977
|
-
* @internal Kept for legacy code paths that may still reference this method
|
|
978
|
-
*/
|
|
979
|
-
// @ts-expect-error - Legacy method kept for backwards compatibility, unused in favor of streamHandler
|
|
980
|
-
suppressReadlineOutput() {
|
|
981
|
-
// Delegate to stream handler if it's not already streaming
|
|
982
|
-
if (this.streamHandler.isStreaming()) {
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
if (this.readlineOutputSuppressed || !output.isTTY) {
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
this.originalStdoutWrite = output.write.bind(output);
|
|
989
|
-
const self = this;
|
|
990
|
-
// Replace stdout.write to filter readline echo
|
|
991
|
-
// Readline writes single characters for echo - we filter those out
|
|
992
|
-
// but allow multi-character writes (actual output) through
|
|
993
|
-
output.write = function (chunk, encodingOrCallback, callback) {
|
|
994
|
-
if (!self.originalStdoutWrite) {
|
|
995
|
-
return true;
|
|
996
|
-
}
|
|
997
|
-
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
998
|
-
// Filter out readline echo patterns:
|
|
999
|
-
// - Single printable characters (user typing)
|
|
1000
|
-
// - Cursor movement sequences for single chars
|
|
1001
|
-
// - Backspace sequences
|
|
1002
|
-
// - Prompt redraws
|
|
1003
|
-
// But allow through:
|
|
1004
|
-
// - Multi-character content (actual AI output)
|
|
1005
|
-
// - Newlines and control sequences for formatting
|
|
1006
|
-
// If it's a single printable char, suppress it (user typing)
|
|
1007
|
-
if (str.length === 1 && str.charCodeAt(0) >= 32 && str.charCodeAt(0) < 127) {
|
|
1008
|
-
return true;
|
|
1009
|
-
}
|
|
1010
|
-
// Suppress backspace sequences (readline's delete char)
|
|
1011
|
-
if (str === '\b \b' || str === '\x1b[D \x1b[D' || str === '\b' || str === '\x7f') {
|
|
1012
|
-
return true;
|
|
1013
|
-
}
|
|
1014
|
-
// Suppress cursor movement sequences (readline cursor positioning)
|
|
1015
|
-
if (/^\x1b\[\d*[ABCD]$/.test(str)) {
|
|
1016
|
-
return true;
|
|
1017
|
-
}
|
|
1018
|
-
// Suppress readline prompt redraw patterns (starts with \r or cursor home)
|
|
1019
|
-
if (/^\r/.test(str) && str.length < 20 && /^[\r\x1b\[\dGK> ]+$/.test(str)) {
|
|
1020
|
-
return true;
|
|
1021
|
-
}
|
|
1022
|
-
// Suppress clear line + prompt patterns from readline
|
|
1023
|
-
if (/^\x1b\[\d*[GK]/.test(str) && str.length < 15) {
|
|
1024
|
-
return true;
|
|
1025
|
-
}
|
|
1026
|
-
// Suppress short sequences that look like readline control (not AI content)
|
|
1027
|
-
// AI content is typically longer or contains actual text
|
|
1028
|
-
if (str.length <= 3 && /^[\x1b\[\]\d;GKJ]+$/.test(str)) {
|
|
1029
|
-
return true;
|
|
1030
|
-
}
|
|
1031
|
-
// Allow everything else through (actual AI output)
|
|
1032
|
-
if (typeof encodingOrCallback === 'function') {
|
|
1033
|
-
return self.originalStdoutWrite(chunk, encodingOrCallback);
|
|
1034
|
-
}
|
|
1035
|
-
return self.originalStdoutWrite(chunk, encodingOrCallback, callback);
|
|
1036
|
-
};
|
|
1037
|
-
this.readlineOutputSuppressed = true;
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Restore normal readline output after streaming completes.
|
|
1041
|
-
*
|
|
1042
|
-
* @deprecated Use streamHandler.endStreaming() instead
|
|
1043
|
-
*/
|
|
1044
|
-
restoreReadlineOutput() {
|
|
1045
|
-
if (!this.readlineOutputSuppressed || !this.originalStdoutWrite) {
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
output.write = this.originalStdoutWrite;
|
|
1049
|
-
this.originalStdoutWrite = null;
|
|
1050
|
-
this.readlineOutputSuppressed = false;
|
|
1051
|
-
}
|
|
1052
|
-
enqueueUserInput(line, flushImmediately = false) {
|
|
1053
|
-
this.bufferedInputLines.push(line);
|
|
1054
|
-
if (flushImmediately) {
|
|
1055
|
-
if (this.bufferedInputTimer) {
|
|
1056
|
-
clearTimeout(this.bufferedInputTimer);
|
|
1057
|
-
this.bufferedInputTimer = null;
|
|
1058
|
-
}
|
|
1059
|
-
void this.flushBufferedInput();
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
if (this.bufferedInputTimer) {
|
|
1063
|
-
clearTimeout(this.bufferedInputTimer);
|
|
1064
|
-
}
|
|
1065
|
-
this.bufferedInputTimer = setTimeout(() => {
|
|
1066
|
-
void this.flushBufferedInput();
|
|
1067
|
-
}, MULTILINE_INPUT_FLUSH_DELAY_MS);
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Clear any buffered input lines and pending flush timers.
|
|
1071
|
-
* Prevents partial multi-line pastes from being auto-submitted.
|
|
1072
|
-
*/
|
|
1073
|
-
resetBufferedInputLines() {
|
|
1074
|
-
if (this.bufferedInputTimer) {
|
|
1075
|
-
clearTimeout(this.bufferedInputTimer);
|
|
1076
|
-
this.bufferedInputTimer = null;
|
|
1077
|
-
}
|
|
1078
|
-
if (this.bufferedInputLines.length) {
|
|
1079
|
-
this.bufferedInputLines = [];
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
/** Track paste preview state to properly clear/update display */
|
|
1083
|
-
lastPastePreviewLineCount = 0;
|
|
1084
|
-
inPasteCapture = false;
|
|
1085
|
-
/**
|
|
1086
|
-
* Show a preview indicator while accumulating multi-line paste.
|
|
1087
|
-
* The line handler has already cleared readline's echoed line, so we just
|
|
1088
|
-
* write our preview in place using carriage return.
|
|
1089
|
-
*/
|
|
1090
|
-
showMultiLinePastePreview(lineCount, preview) {
|
|
1091
|
-
// Only update if we're actually accumulating (avoid spam during rapid paste)
|
|
1092
|
-
if (lineCount === 0 && !preview) {
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
// Mark that we're in paste capture mode
|
|
1096
|
-
this.inPasteCapture = true;
|
|
1097
|
-
// Throttle updates - only update if line count changed to avoid flicker
|
|
1098
|
-
if (lineCount === this.lastPastePreviewLineCount) {
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
this.lastPastePreviewLineCount = lineCount;
|
|
1102
|
-
// Clear current line and write preview (the line handler already moved cursor up)
|
|
1103
|
-
output.write('\r\x1b[K');
|
|
1104
|
-
const statusText = preview
|
|
1105
|
-
? `${theme.ui.muted('📋 Pasting:')} ${theme.ui.muted(preview.slice(0, 50))}${preview.length > 50 ? '...' : ''}`
|
|
1106
|
-
: `${theme.ui.muted(`📋 Pasting ${lineCount} line${lineCount !== 1 ? 's' : ''}...`)}`;
|
|
1107
|
-
output.write(statusText);
|
|
1108
|
-
}
|
|
1109
|
-
/**
|
|
1110
|
-
* Clear the multi-line paste preview
|
|
1111
|
-
*/
|
|
1112
|
-
clearMultiLinePastePreview() {
|
|
1113
|
-
if (!this.inPasteCapture) {
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
// Clear current line
|
|
1117
|
-
output.write('\r\x1b[K');
|
|
1118
|
-
// Reset tracking state
|
|
1119
|
-
this.lastPastePreviewLineCount = 0;
|
|
1120
|
-
this.inPasteCapture = false;
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* Capture any paste (single or multi-line) without immediately submitting it.
|
|
1124
|
-
* - Short pastes (1-2 lines) are displayed inline like normal typed text
|
|
1125
|
-
* - Longer pastes (3+ lines) show as collapsed block chips
|
|
1126
|
-
* Supports multiple pastes - user can paste multiple times before submitting.
|
|
1127
|
-
*/
|
|
1128
|
-
async capturePaste(content, lineCount) {
|
|
1129
|
-
this.resetBufferedInputLines();
|
|
1130
|
-
// Short pastes (1-2 lines) display inline like normal text
|
|
1131
|
-
const isShortPaste = lineCount <= 2;
|
|
1132
|
-
if (isShortPaste) {
|
|
1133
|
-
// For short pastes, display inline like normal typed text
|
|
1134
|
-
// No composableMessage storage - just treat as typed input
|
|
1135
|
-
// For 2-line pastes, join with a visual newline indicator
|
|
1136
|
-
const displayContent = lineCount === 1
|
|
1137
|
-
? content
|
|
1138
|
-
: content.replace(/\n/g, ' ↵ '); // Visual newline indicator for 2-line pastes
|
|
1139
|
-
// Clear any echoed content first
|
|
1140
|
-
output.write('\r\x1b[K');
|
|
1141
|
-
// Get current readline content and append paste
|
|
1142
|
-
const currentLine = this.rl.line || '';
|
|
1143
|
-
const cursorPos = this.rl.cursor || 0;
|
|
1144
|
-
// Insert paste at cursor position
|
|
1145
|
-
const before = currentLine.slice(0, cursorPos);
|
|
1146
|
-
const after = currentLine.slice(cursorPos);
|
|
1147
|
-
const newLine = before + displayContent + after;
|
|
1148
|
-
const newCursor = cursorPos + displayContent.length;
|
|
1149
|
-
// Update readline buffer - write directly without storing in composableMessage
|
|
1150
|
-
// This allows short pastes to flow through as normal typed text
|
|
1151
|
-
this.rl.write(null, { ctrl: true, name: 'u' }); // Clear line
|
|
1152
|
-
this.rl.write(newLine); // Write new content
|
|
1153
|
-
// Update persistent prompt display
|
|
1154
|
-
this.persistentPrompt.updateInput(newLine, newCursor);
|
|
1155
|
-
// NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
|
|
1156
|
-
// will decrement for each readline line event and auto-clear when all are processed.
|
|
1157
|
-
// Clearing prematurely causes the remaining readline-echoed lines to pass through.
|
|
1158
|
-
// Re-prompt to show the inline content
|
|
1159
|
-
this.rl.prompt(true);
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
// For longer pastes (3+ lines), store as a composable block
|
|
1163
|
-
// Check size limits first
|
|
1164
|
-
const { ComposableMessageBuilder } = await import('./composableMessage.js');
|
|
1165
|
-
const sizeCheck = ComposableMessageBuilder.checkPasteSize(content);
|
|
1166
|
-
if (!sizeCheck.ok) {
|
|
1167
|
-
// Paste rejected - show error and abort
|
|
1168
|
-
display.showError(sizeCheck.error || 'Paste rejected due to size limits');
|
|
1169
|
-
this.rl.prompt();
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
// Show warning for large (but acceptable) pastes
|
|
1173
|
-
if (sizeCheck.warning) {
|
|
1174
|
-
display.showWarning(`⚠️ ${sizeCheck.warning}`);
|
|
1175
|
-
}
|
|
1176
|
-
const pasteId = this.composableMessage.addPaste(content);
|
|
1177
|
-
if (!pasteId) {
|
|
1178
|
-
// Should not happen since we checked above, but handle defensively
|
|
1179
|
-
display.showError('Failed to store paste block');
|
|
1180
|
-
this.rl.prompt();
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
// Clear remaining echoed lines from terminal
|
|
1184
|
-
output.write('\r\x1b[K');
|
|
1185
|
-
// Build the paste chips to show inline with prompt
|
|
1186
|
-
// Format: [Pasted text #1 +104 lines] [Pasted text #2 +50 lines]
|
|
1187
|
-
const pasteChips = this.composableMessage.formatPasteChips();
|
|
1188
|
-
// Update status bar - minimal hint, no confirmation required
|
|
1189
|
-
this.persistentPrompt.updateStatusBar({
|
|
1190
|
-
message: 'Press Enter to send',
|
|
1191
|
-
});
|
|
1192
|
-
// Set the prompt to show paste chips, then position cursor after them
|
|
1193
|
-
// The user can type additional text after the chips
|
|
1194
|
-
this.persistentPrompt.updateInput(pasteChips + ' ', pasteChips.length + 1);
|
|
1195
|
-
// Update readline's line buffer to include the chips as prefix
|
|
1196
|
-
// This ensures typed text appears after the chips
|
|
1197
|
-
if (this.rl.line !== undefined) {
|
|
1198
|
-
this.rl.line = pasteChips + ' ';
|
|
1199
|
-
this.rl.cursor = pasteChips.length + 1;
|
|
1200
|
-
}
|
|
1201
|
-
// NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
|
|
1202
|
-
// will decrement for each readline line event (one per pasted line) and auto-clear when done.
|
|
1203
|
-
// Clearing prematurely causes remaining readline-echoed lines to pass through and get displayed.
|
|
1204
|
-
this.rl.prompt(true); // preserveCursor=true to keep position after chips
|
|
1205
|
-
}
|
|
1206
|
-
/**
|
|
1207
|
-
* Update the status bar to reflect any pending composed message parts
|
|
1208
|
-
*/
|
|
1209
|
-
updateComposeStatusSummary() {
|
|
1210
|
-
if (this.composableMessage.hasContent()) {
|
|
1211
|
-
// Chips are shown inline - minimal status hint
|
|
1212
|
-
this.persistentPrompt.updateStatusBar({
|
|
1213
|
-
message: 'Press Enter to send',
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
else {
|
|
1217
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Show paste block editor (Ctrl+G)
|
|
1222
|
-
* Allows viewing and editing captured paste blocks before sending
|
|
1223
|
-
*
|
|
1224
|
-
* Features:
|
|
1225
|
-
* - View block content with line numbers
|
|
1226
|
-
* - Edit blocks inline (replace content)
|
|
1227
|
-
* - Remove individual blocks
|
|
1228
|
-
* - Undo/redo paste operations
|
|
1229
|
-
* - Clear all blocks
|
|
1230
|
-
*/
|
|
1231
|
-
showPasteBlockEditor() {
|
|
1232
|
-
const state = this.composableMessage.getState();
|
|
1233
|
-
const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
|
|
1234
|
-
if (pasteBlocks.length === 0) {
|
|
1235
|
-
display.showInfo('No pasted content blocks to edit. Paste some content first.');
|
|
1236
|
-
this.rl.prompt();
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
output.write('\n');
|
|
1240
|
-
display.showSystemMessage('📋 Paste Block Editor');
|
|
1241
|
-
output.write('\n');
|
|
1242
|
-
// Display each paste block with preview
|
|
1243
|
-
pasteBlocks.forEach((block, index) => {
|
|
1244
|
-
const lines = block.content.split('\n');
|
|
1245
|
-
const preview = lines.slice(0, 3).map((l) => ` ${l.slice(0, 60)}${l.length > 60 ? '...' : ''}`).join('\n');
|
|
1246
|
-
const moreLines = lines.length > 3 ? `\n ... +${lines.length - 3} more lines` : '';
|
|
1247
|
-
const editedFlag = block.edited ? theme.warning(' [edited]') : '';
|
|
1248
|
-
output.write(theme.ui.muted(`[${index + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + editedFlag + '\n');
|
|
1249
|
-
output.write(theme.secondary(preview + moreLines) + '\n\n');
|
|
1250
|
-
});
|
|
1251
|
-
// Show undo/redo status
|
|
1252
|
-
const canUndo = this.composableMessage.canUndo();
|
|
1253
|
-
const canRedo = this.composableMessage.canRedo();
|
|
1254
|
-
if (canUndo || canRedo) {
|
|
1255
|
-
const undoStatus = canUndo ? theme.success('undo available') : theme.ui.muted('undo unavailable');
|
|
1256
|
-
const redoStatus = canRedo ? theme.success('redo available') : theme.ui.muted('redo unavailable');
|
|
1257
|
-
output.write(theme.ui.muted('History: ') + undoStatus + theme.ui.muted(' │ ') + redoStatus + '\n\n');
|
|
1258
|
-
}
|
|
1259
|
-
output.write(theme.ui.muted('Commands: ') + '\n');
|
|
1260
|
-
output.write(theme.ui.muted(' • Enter number to view full block') + '\n');
|
|
1261
|
-
output.write(theme.ui.muted(' • "edit N" to replace block N content') + '\n');
|
|
1262
|
-
output.write(theme.ui.muted(' • "remove N" to remove block N') + '\n');
|
|
1263
|
-
output.write(theme.ui.muted(' • "undo" / "redo" to undo/redo changes') + '\n');
|
|
1264
|
-
output.write(theme.ui.muted(' • "clear" to remove all blocks') + '\n');
|
|
1265
|
-
output.write(theme.ui.muted(' • Enter to return to prompt') + '\n');
|
|
1266
|
-
output.write('\n');
|
|
1267
|
-
// Set up interaction mode for paste editing
|
|
1268
|
-
this.pendingInteraction = { type: 'paste-edit' };
|
|
1269
|
-
this.rl.prompt();
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Handle paste block editor input
|
|
1273
|
-
*/
|
|
1274
|
-
async handlePasteEditInput(trimmedInput) {
|
|
1275
|
-
const trimmed = trimmedInput.toLowerCase();
|
|
1276
|
-
const state = this.composableMessage.getState();
|
|
1277
|
-
const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
|
|
1278
|
-
if (!trimmed) {
|
|
1279
|
-
// Exit paste edit mode
|
|
1280
|
-
this.pendingInteraction = null;
|
|
1281
|
-
display.showInfo('Returned to prompt.');
|
|
1282
|
-
this.rl.prompt();
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
// Handle undo command
|
|
1286
|
-
if (trimmed === 'undo') {
|
|
1287
|
-
if (this.composableMessage.undo()) {
|
|
1288
|
-
this.updateComposeStatusSummary();
|
|
1289
|
-
this.refreshPasteChipsDisplay();
|
|
1290
|
-
display.showInfo('Undone.');
|
|
1291
|
-
// Refresh the paste block view
|
|
1292
|
-
this.showPasteBlockEditor();
|
|
1293
|
-
}
|
|
1294
|
-
else {
|
|
1295
|
-
display.showWarning('Nothing to undo.');
|
|
1296
|
-
this.rl.prompt();
|
|
1297
|
-
}
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
// Handle redo command
|
|
1301
|
-
if (trimmed === 'redo') {
|
|
1302
|
-
if (this.composableMessage.redo()) {
|
|
1303
|
-
this.updateComposeStatusSummary();
|
|
1304
|
-
this.refreshPasteChipsDisplay();
|
|
1305
|
-
display.showInfo('Redone.');
|
|
1306
|
-
// Refresh the paste block view
|
|
1307
|
-
this.showPasteBlockEditor();
|
|
1308
|
-
}
|
|
1309
|
-
else {
|
|
1310
|
-
display.showWarning('Nothing to redo.');
|
|
1311
|
-
this.rl.prompt();
|
|
1312
|
-
}
|
|
1313
|
-
return;
|
|
1314
|
-
}
|
|
1315
|
-
if (trimmed === 'clear') {
|
|
1316
|
-
this.composableMessage.clear();
|
|
1317
|
-
this.updateComposeStatusSummary();
|
|
1318
|
-
this.persistentPrompt.updateInput('', 0);
|
|
1319
|
-
this.rl.line = '';
|
|
1320
|
-
this.rl.cursor = 0;
|
|
1321
|
-
this.pendingInteraction = null;
|
|
1322
|
-
display.showInfo('Cleared all paste blocks.');
|
|
1323
|
-
this.rl.prompt();
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
// Handle "edit N" command - start inline editing mode
|
|
1327
|
-
const editMatch = trimmed.match(/^edit\s+(\d+)$/);
|
|
1328
|
-
if (editMatch) {
|
|
1329
|
-
const blockNum = parseInt(editMatch[1], 10);
|
|
1330
|
-
if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
|
|
1331
|
-
const block = pasteBlocks[blockNum - 1];
|
|
1332
|
-
if (block) {
|
|
1333
|
-
// Enter edit mode for this block
|
|
1334
|
-
this.pendingInteraction = { type: 'paste-edit-block', blockId: block.id, blockNum };
|
|
1335
|
-
output.write('\n');
|
|
1336
|
-
display.showSystemMessage(`Editing Block ${blockNum}:`);
|
|
1337
|
-
output.write(theme.ui.muted('Paste or type new content. Press Enter twice to finish, or "cancel" to abort.\n'));
|
|
1338
|
-
output.write('\n');
|
|
1339
|
-
// Show current content as reference
|
|
1340
|
-
const preview = block.content.split('\n').slice(0, 5).map((l) => theme.ui.muted(` ${l.slice(0, 60)}`)).join('\n');
|
|
1341
|
-
output.write(theme.ui.muted('Current content (first 5 lines):\n') + preview + '\n\n');
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
else {
|
|
1345
|
-
display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
|
|
1346
|
-
}
|
|
1347
|
-
this.rl.prompt();
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
// Handle "remove N" command
|
|
1351
|
-
const removeMatch = trimmed.match(/^remove\s+(\d+)$/);
|
|
1352
|
-
if (removeMatch) {
|
|
1353
|
-
const blockNum = parseInt(removeMatch[1], 10);
|
|
1354
|
-
if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
|
|
1355
|
-
const block = pasteBlocks[blockNum - 1];
|
|
1356
|
-
if (block) {
|
|
1357
|
-
this.composableMessage.removePart(block.id);
|
|
1358
|
-
this.updateComposeStatusSummary();
|
|
1359
|
-
this.refreshPasteChipsDisplay();
|
|
1360
|
-
display.showInfo(`Removed block ${blockNum}.`);
|
|
1361
|
-
// Check if any blocks remain
|
|
1362
|
-
const remaining = this.composableMessage.getState().parts.filter(p => p.type === 'paste');
|
|
1363
|
-
if (remaining.length === 0) {
|
|
1364
|
-
this.pendingInteraction = null;
|
|
1365
|
-
display.showInfo('No more paste blocks.');
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
else {
|
|
1370
|
-
display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
|
|
1371
|
-
}
|
|
1372
|
-
this.rl.prompt();
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
// Handle viewing a block by number
|
|
1376
|
-
const blockNum = parseInt(trimmed, 10);
|
|
1377
|
-
if (!Number.isNaN(blockNum) && blockNum >= 1 && blockNum <= pasteBlocks.length) {
|
|
1378
|
-
const block = pasteBlocks[blockNum - 1];
|
|
1379
|
-
if (block) {
|
|
1380
|
-
output.write('\n');
|
|
1381
|
-
display.showSystemMessage(`Block ${blockNum} Content:`);
|
|
1382
|
-
output.write('\n');
|
|
1383
|
-
// Show full content with line numbers
|
|
1384
|
-
const lines = block.content.split('\n');
|
|
1385
|
-
lines.forEach((line, i) => {
|
|
1386
|
-
const lineNum = theme.ui.muted(`${String(i + 1).padStart(4)} │ `);
|
|
1387
|
-
output.write(lineNum + line + '\n');
|
|
1388
|
-
});
|
|
1389
|
-
output.write('\n');
|
|
1390
|
-
}
|
|
1391
|
-
this.rl.prompt();
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
display.showWarning('Enter a block number, "edit N", "remove N", "undo", "redo", "clear", or press Enter to return.');
|
|
1395
|
-
this.rl.prompt();
|
|
1396
|
-
}
|
|
1397
|
-
/**
|
|
1398
|
-
* Handle paste block inline editing mode
|
|
1399
|
-
*/
|
|
1400
|
-
editBlockBuffer = [];
|
|
1401
|
-
async handlePasteBlockEditInput(input, blockId, blockNum) {
|
|
1402
|
-
// Check for cancel
|
|
1403
|
-
if (input.toLowerCase() === 'cancel') {
|
|
1404
|
-
this.editBlockBuffer = [];
|
|
1405
|
-
this.pendingInteraction = { type: 'paste-edit' };
|
|
1406
|
-
display.showInfo('Edit cancelled.');
|
|
1407
|
-
this.showPasteBlockEditor();
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
// Empty line - check if this is end of edit (double Enter)
|
|
1411
|
-
if (!input.trim()) {
|
|
1412
|
-
if (this.editBlockBuffer.length > 0) {
|
|
1413
|
-
// Complete the edit
|
|
1414
|
-
const newContent = this.editBlockBuffer.join('\n');
|
|
1415
|
-
this.composableMessage.editPaste(blockId, newContent);
|
|
1416
|
-
this.editBlockBuffer = [];
|
|
1417
|
-
this.updateComposeStatusSummary();
|
|
1418
|
-
this.refreshPasteChipsDisplay();
|
|
1419
|
-
display.showInfo(`Block ${blockNum} updated (${newContent.split('\n').length} lines).`);
|
|
1420
|
-
this.pendingInteraction = { type: 'paste-edit' };
|
|
1421
|
-
this.showPasteBlockEditor();
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
// First empty line with no buffer - just prompt again
|
|
1425
|
-
this.rl.prompt();
|
|
1426
|
-
return;
|
|
1427
|
-
}
|
|
1428
|
-
// Add line to edit buffer
|
|
1429
|
-
this.editBlockBuffer.push(input);
|
|
1430
|
-
this.rl.prompt();
|
|
1431
|
-
}
|
|
1432
|
-
/**
|
|
1433
|
-
* Refresh paste chips display in prompt
|
|
1434
|
-
*/
|
|
1435
|
-
refreshPasteChipsDisplay() {
|
|
1436
|
-
const chips = this.composableMessage.formatPasteChips();
|
|
1437
|
-
this.persistentPrompt.updateInput(chips ? chips + ' ' : '', chips ? chips.length + 1 : 0);
|
|
1438
|
-
this.rl.line = chips ? chips + ' ' : '';
|
|
1439
|
-
this.rl.cursor = chips ? chips.length + 1 : 0;
|
|
1440
|
-
}
|
|
1441
|
-
/**
|
|
1442
|
-
* Cycle paste preview mode (Shift+Tab)
|
|
1443
|
-
* Cycles through: collapsed → summary → expanded
|
|
1444
|
-
*/
|
|
1445
|
-
pastePreviewMode = 'collapsed';
|
|
1446
|
-
cyclePastePreview() {
|
|
1447
|
-
const state = this.composableMessage.getState();
|
|
1448
|
-
const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
|
|
1449
|
-
if (pasteBlocks.length === 0) {
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
// Cycle through modes
|
|
1453
|
-
const modes = ['collapsed', 'summary', 'expanded'];
|
|
1454
|
-
const currentIndex = modes.indexOf(this.pastePreviewMode);
|
|
1455
|
-
this.pastePreviewMode = modes[(currentIndex + 1) % modes.length];
|
|
1456
|
-
output.write('\n');
|
|
1457
|
-
switch (this.pastePreviewMode) {
|
|
1458
|
-
case 'collapsed':
|
|
1459
|
-
// Show just chips
|
|
1460
|
-
display.showSystemMessage(`📋 Preview: Collapsed (${pasteBlocks.length} block${pasteBlocks.length > 1 ? 's' : ''})`);
|
|
1461
|
-
output.write(theme.ui.muted(this.composableMessage.formatPasteChips()) + '\n');
|
|
1462
|
-
break;
|
|
1463
|
-
case 'summary':
|
|
1464
|
-
// Show blocks with first line preview
|
|
1465
|
-
display.showSystemMessage('📋 Preview: Summary');
|
|
1466
|
-
pasteBlocks.forEach((block, i) => {
|
|
1467
|
-
const preview = block.summary.length > 60 ? block.summary.slice(0, 57) + '...' : block.summary;
|
|
1468
|
-
output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount}L`) + theme.ui.muted(` ${preview}`) + '\n');
|
|
1469
|
-
});
|
|
1470
|
-
break;
|
|
1471
|
-
case 'expanded':
|
|
1472
|
-
// Show full content (up to 10 lines each)
|
|
1473
|
-
display.showSystemMessage('📋 Preview: Expanded');
|
|
1474
|
-
pasteBlocks.forEach((block, i) => {
|
|
1475
|
-
output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + '\n');
|
|
1476
|
-
const lines = block.content.split('\n').slice(0, 10);
|
|
1477
|
-
lines.forEach((line, j) => {
|
|
1478
|
-
const lineNum = theme.ui.muted(`${String(j + 1).padStart(3)} │ `);
|
|
1479
|
-
output.write(lineNum + line.slice(0, 80) + (line.length > 80 ? '...' : '') + '\n');
|
|
1480
|
-
});
|
|
1481
|
-
if (block.lineCount > 10) {
|
|
1482
|
-
output.write(theme.ui.muted(` ... +${block.lineCount - 10} more lines\n`));
|
|
1483
|
-
}
|
|
1484
|
-
output.write('\n');
|
|
1485
|
-
});
|
|
1486
|
-
break;
|
|
1487
|
-
}
|
|
1488
|
-
output.write(theme.ui.muted('(Shift+Tab to cycle preview modes)') + '\n\n');
|
|
1489
|
-
this.rl.prompt(true);
|
|
1490
|
-
}
|
|
1491
|
-
async flushBufferedInput() {
|
|
1492
|
-
if (!this.bufferedInputLines.length) {
|
|
1493
|
-
this.bufferedInputTimer = null;
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
const lineCount = this.bufferedInputLines.length;
|
|
1497
|
-
const combined = this.bufferedInputLines.join('\n');
|
|
1498
|
-
this.bufferedInputLines = [];
|
|
1499
|
-
this.bufferedInputTimer = null;
|
|
1500
|
-
try {
|
|
1501
|
-
await this.processInputBlock(combined, lineCount > 1);
|
|
1502
|
-
}
|
|
1503
|
-
catch (error) {
|
|
1504
|
-
// Pass full error object for enhanced formatting
|
|
1505
|
-
display.showError(error instanceof Error ? error.message : String(error), error);
|
|
1506
|
-
this.rl.prompt();
|
|
1507
|
-
}
|
|
531
|
+
this.terminalInput.render();
|
|
1508
532
|
}
|
|
1509
533
|
refreshQueueIndicators() {
|
|
1510
|
-
const queued = this.followUpQueue.length;
|
|
1511
|
-
// Build status message based on processing state and queue
|
|
1512
|
-
let message;
|
|
1513
|
-
if (this.isProcessing) {
|
|
1514
|
-
const queuePart = queued > 0 ? `${queued} queued · ` : '';
|
|
1515
|
-
// Show helpful hints: Esc to cancel, type to queue
|
|
1516
|
-
message = `⏳ Processing... ${queuePart}[Esc: cancel · Enter: queue follow-up]`;
|
|
1517
|
-
}
|
|
1518
|
-
else if (queued > 0) {
|
|
1519
|
-
message = `${queued} follow-up${queued === 1 ? '' : 's'} queued · [Esc: clear queue]`;
|
|
1520
|
-
}
|
|
1521
|
-
this.persistentPrompt.updateStatusBar({ message });
|
|
1522
534
|
if (this.isProcessing) {
|
|
1523
535
|
this.setProcessingStatus();
|
|
1524
536
|
}
|
|
1525
537
|
else {
|
|
1526
538
|
this.setIdleStatus();
|
|
1527
539
|
}
|
|
540
|
+
this.terminalInput.render();
|
|
1528
541
|
}
|
|
1529
542
|
enqueueFollowUpAction(action) {
|
|
1530
543
|
this.followUpQueue.push(action);
|
|
@@ -1533,23 +546,17 @@ export class InteractiveShell {
|
|
|
1533
546
|
const preview = normalized.length > previewLimit ? `${normalized.slice(0, previewLimit - 3)}...` : normalized;
|
|
1534
547
|
const position = this.followUpQueue.length === 1 ? 'to run next' : `#${this.followUpQueue.length} in queue`;
|
|
1535
548
|
const label = action.type === 'continuous' ? 'continuous command' : 'follow-up';
|
|
1536
|
-
const queueCount = this.followUpQueue.length;
|
|
1537
549
|
// Show immediate acknowledgment with checkmark
|
|
1538
|
-
const queueLabel = queueCount === 1 ? '1 queued' : `${queueCount} queued`;
|
|
1539
550
|
if (preview) {
|
|
1540
551
|
display.showInfo(`✓ ${theme.info(label)} ${position}: ${theme.ui.muted(preview)}`);
|
|
1541
552
|
}
|
|
1542
553
|
else {
|
|
1543
554
|
display.showInfo(`✓ ${theme.info(label)} queued ${position}.`);
|
|
1544
555
|
}
|
|
1545
|
-
// Update status bar to show queue count
|
|
1546
|
-
this.persistentPrompt.updateStatusBar({
|
|
1547
|
-
message: `⏳ Processing... (${queueLabel})`
|
|
1548
|
-
});
|
|
1549
556
|
this.refreshQueueIndicators();
|
|
1550
557
|
this.scheduleQueueProcessing();
|
|
1551
558
|
// Re-show the prompt so user can continue typing more follow-ups
|
|
1552
|
-
this.
|
|
559
|
+
this.terminalInput.render();
|
|
1553
560
|
}
|
|
1554
561
|
scheduleQueueProcessing() {
|
|
1555
562
|
if (!this.followUpQueue.length) {
|
|
@@ -1561,71 +568,8 @@ export class InteractiveShell {
|
|
|
1561
568
|
});
|
|
1562
569
|
}
|
|
1563
570
|
/**
|
|
1564
|
-
* Process
|
|
1565
|
-
* Claude Code style: User can type while streaming, input is stored in queue,
|
|
1566
|
-
* processed after streaming ends.
|
|
1567
|
-
*
|
|
1568
|
-
* @deprecated Use processUnifiedChatBoxQueue() instead
|
|
1569
|
-
*/
|
|
1570
|
-
// @ts-expect-error - Legacy method kept for backwards compatibility
|
|
1571
|
-
processQueuedInputs() {
|
|
1572
|
-
if (!this.streamHandler.hasQueuedInput()) {
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
// Transfer queued inputs from stream handler to follow-up queue
|
|
1576
|
-
while (this.streamHandler.hasQueuedInput()) {
|
|
1577
|
-
const input = this.streamHandler.getNextQueuedInput();
|
|
1578
|
-
if (!input) {
|
|
1579
|
-
break;
|
|
1580
|
-
}
|
|
1581
|
-
// Handle interrupts specially
|
|
1582
|
-
if (input.type === 'interrupt') {
|
|
1583
|
-
// Interrupt was already processed during capture
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
// Add to follow-up queue for processing
|
|
1587
|
-
const actionType = input.type === 'command' ? 'request' : 'request';
|
|
1588
|
-
this.followUpQueue.push({
|
|
1589
|
-
type: actionType,
|
|
1590
|
-
text: input.text,
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
// Show queue summary if there are items
|
|
1594
|
-
const summary = this.streamHandler.getQueueSummary();
|
|
1595
|
-
if (summary) {
|
|
1596
|
-
display.showInfo(`📝 ${summary}`);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
/**
|
|
1600
|
-
* Process any inputs that were queued in UnifiedChatBox during streaming.
|
|
1601
|
-
* Claude Code style: User can type while streaming, input is stored invisibly,
|
|
1602
|
-
* processed after streaming ends.
|
|
571
|
+
* Process queued follow-up actions.
|
|
1603
572
|
*/
|
|
1604
|
-
processUnifiedChatBoxQueue() {
|
|
1605
|
-
if (!this.unifiedChatBox.hasQueuedInput()) {
|
|
1606
|
-
return;
|
|
1607
|
-
}
|
|
1608
|
-
// Transfer queued inputs from unified chat box to follow-up queue
|
|
1609
|
-
while (this.unifiedChatBox.hasQueuedInput()) {
|
|
1610
|
-
const input = this.unifiedChatBox.dequeue();
|
|
1611
|
-
if (!input) {
|
|
1612
|
-
break;
|
|
1613
|
-
}
|
|
1614
|
-
// Handle interrupts specially - they cancel current operation
|
|
1615
|
-
if (input.type === 'interrupt') {
|
|
1616
|
-
// Request cancellation if agent is running
|
|
1617
|
-
if (this.agent) {
|
|
1618
|
-
this.agent.requestCancellation();
|
|
1619
|
-
}
|
|
1620
|
-
continue;
|
|
1621
|
-
}
|
|
1622
|
-
// Add to follow-up queue for processing
|
|
1623
|
-
this.followUpQueue.push({
|
|
1624
|
-
type: 'request',
|
|
1625
|
-
text: input.text,
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
573
|
async processQueuedActions() {
|
|
1630
574
|
if (this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
|
|
1631
575
|
return;
|
|
@@ -1659,85 +603,22 @@ export class InteractiveShell {
|
|
|
1659
603
|
if (await this.handlePendingInteraction(trimmed)) {
|
|
1660
604
|
return;
|
|
1661
605
|
}
|
|
1662
|
-
// If we have captured multi-line paste blocks, respect control commands before assembling
|
|
1663
|
-
if (this.composableMessage.hasContent()) {
|
|
1664
|
-
// Strip paste chip prefixes from input since actual content is in composableMessage
|
|
1665
|
-
// Chips look like: [Pasted text #1 +X lines] [Pasted text #2 +Y lines]
|
|
1666
|
-
const chipsPrefix = this.composableMessage.formatPasteChips();
|
|
1667
|
-
let userText = trimmed;
|
|
1668
|
-
if (chipsPrefix && trimmed.startsWith(chipsPrefix)) {
|
|
1669
|
-
userText = trimmed.slice(chipsPrefix.length).trim();
|
|
1670
|
-
}
|
|
1671
|
-
const lower = userText.toLowerCase();
|
|
1672
|
-
// Control commands that should NOT consume the captured paste
|
|
1673
|
-
if (lower === '/cancel' || lower === 'cancel') {
|
|
1674
|
-
this.composableMessage.clear();
|
|
1675
|
-
this.updateComposeStatusSummary();
|
|
1676
|
-
this.persistentPrompt.updateInput('', 0);
|
|
1677
|
-
display.showInfo('Discarded captured paste.');
|
|
1678
|
-
this.rl.prompt();
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
if (lower === 'exit' || lower === 'quit') {
|
|
1682
|
-
this.rl.close();
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
if (lower === 'clear') {
|
|
1686
|
-
display.clear();
|
|
1687
|
-
this.rl.prompt();
|
|
1688
|
-
return;
|
|
1689
|
-
}
|
|
1690
|
-
if (lower === 'help') {
|
|
1691
|
-
this.showHelp();
|
|
1692
|
-
this.rl.prompt();
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
// Slash commands operate independently of captured paste
|
|
1696
|
-
if (userText.startsWith('/')) {
|
|
1697
|
-
await this.processSlashCommand(userText);
|
|
1698
|
-
this.updateComposeStatusSummary();
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
// If userText is empty OR it's additional content, assemble and send
|
|
1702
|
-
// Empty enter sends the captured paste; non-empty content is appended first
|
|
1703
|
-
if (userText) {
|
|
1704
|
-
this.composableMessage.setDraft(userText);
|
|
1705
|
-
this.composableMessage.commitDraft();
|
|
1706
|
-
}
|
|
1707
|
-
const assembled = this.composableMessage.assemble();
|
|
1708
|
-
this.composableMessage.clear();
|
|
1709
|
-
this.updateComposeStatusSummary();
|
|
1710
|
-
this.persistentPrompt.updateInput('', 0);
|
|
1711
|
-
if (!assembled) {
|
|
1712
|
-
this.rl.prompt();
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
// Check if assembled content is a continuous command
|
|
1716
|
-
if (this.isContinuousCommand(assembled)) {
|
|
1717
|
-
await this.processContinuousRequest(assembled);
|
|
1718
|
-
this.rl.prompt();
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
await this.processRequest(assembled);
|
|
1722
|
-
this.rl.prompt();
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
606
|
if (!trimmed) {
|
|
1726
|
-
this.rl.prompt();
|
|
1727
607
|
return;
|
|
1728
608
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
609
|
+
const lower = trimmed.toLowerCase();
|
|
610
|
+
if (lower === 'exit' || lower === 'quit') {
|
|
611
|
+
this.shutdown();
|
|
1731
612
|
return;
|
|
1732
613
|
}
|
|
1733
|
-
if (
|
|
614
|
+
if (lower === 'clear') {
|
|
1734
615
|
display.clear();
|
|
1735
|
-
this.
|
|
616
|
+
this.terminalInput.render();
|
|
1736
617
|
return;
|
|
1737
618
|
}
|
|
1738
|
-
if (
|
|
619
|
+
if (lower === 'help') {
|
|
1739
620
|
this.showHelp();
|
|
1740
|
-
this.
|
|
621
|
+
this.terminalInput.render();
|
|
1741
622
|
return;
|
|
1742
623
|
}
|
|
1743
624
|
if (trimmed.startsWith('/')) {
|
|
@@ -1747,12 +628,12 @@ export class InteractiveShell {
|
|
|
1747
628
|
// Check for continuous/infinite loop commands
|
|
1748
629
|
if (this.isContinuousCommand(trimmed)) {
|
|
1749
630
|
await this.processContinuousRequest(trimmed);
|
|
1750
|
-
this.
|
|
631
|
+
this.terminalInput.render();
|
|
1751
632
|
return;
|
|
1752
633
|
}
|
|
1753
634
|
// Direct execution for all inputs, including multi-line pastes
|
|
1754
635
|
await this.processRequest(trimmed);
|
|
1755
|
-
this.
|
|
636
|
+
this.terminalInput.render();
|
|
1756
637
|
}
|
|
1757
638
|
/**
|
|
1758
639
|
* Check if the command is a continuous/infinite loop command
|
|
@@ -1804,12 +685,6 @@ export class InteractiveShell {
|
|
|
1804
685
|
case 'agent-selection':
|
|
1805
686
|
await this.handleAgentSelectionInput(input);
|
|
1806
687
|
return true;
|
|
1807
|
-
case 'paste-edit':
|
|
1808
|
-
await this.handlePasteEditInput(input);
|
|
1809
|
-
return true;
|
|
1810
|
-
case 'paste-edit-block':
|
|
1811
|
-
await this.handlePasteBlockEditInput(input, this.pendingInteraction.blockId, this.pendingInteraction.blockNum);
|
|
1812
|
-
return true;
|
|
1813
688
|
default:
|
|
1814
689
|
return false;
|
|
1815
690
|
}
|
|
@@ -1818,7 +693,7 @@ export class InteractiveShell {
|
|
|
1818
693
|
const [command] = input.split(/\s+/);
|
|
1819
694
|
if (!command) {
|
|
1820
695
|
display.showWarning('Enter a slash command.');
|
|
1821
|
-
this.
|
|
696
|
+
this.terminalInput.render();
|
|
1822
697
|
return;
|
|
1823
698
|
}
|
|
1824
699
|
switch (command) {
|
|
@@ -1893,7 +768,7 @@ export class InteractiveShell {
|
|
|
1893
768
|
}
|
|
1894
769
|
break;
|
|
1895
770
|
}
|
|
1896
|
-
this.
|
|
771
|
+
this.terminalInput.render();
|
|
1897
772
|
}
|
|
1898
773
|
async tryCustomSlashCommand(command, fullInput) {
|
|
1899
774
|
const custom = this.customCommandMap.get(command);
|
|
@@ -1927,100 +802,6 @@ export class InteractiveShell {
|
|
|
1927
802
|
];
|
|
1928
803
|
display.showSystemMessage(info.join('\n'));
|
|
1929
804
|
}
|
|
1930
|
-
/**
|
|
1931
|
-
* Handle Tab key for slash command autocompletion
|
|
1932
|
-
* Completes partial slash commands like /mo -> /model
|
|
1933
|
-
*/
|
|
1934
|
-
handleTabCompletion() {
|
|
1935
|
-
const currentLine = this.rl.line || '';
|
|
1936
|
-
const cursorPos = this.rl.cursor || 0;
|
|
1937
|
-
// Only complete if line starts with /
|
|
1938
|
-
if (!currentLine.startsWith('/')) {
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
// Get the partial command (from / to cursor)
|
|
1942
|
-
const partial = currentLine.slice(0, cursorPos).toLowerCase();
|
|
1943
|
-
// Available slash commands
|
|
1944
|
-
const commands = [
|
|
1945
|
-
'/help',
|
|
1946
|
-
'/model',
|
|
1947
|
-
'/secrets',
|
|
1948
|
-
'/tools',
|
|
1949
|
-
'/doctor',
|
|
1950
|
-
'/clear',
|
|
1951
|
-
'/cancel',
|
|
1952
|
-
'/compact',
|
|
1953
|
-
'/cost',
|
|
1954
|
-
'/status',
|
|
1955
|
-
'/undo',
|
|
1956
|
-
'/redo',
|
|
1957
|
-
];
|
|
1958
|
-
// Find matching commands
|
|
1959
|
-
const matches = commands.filter(cmd => cmd.startsWith(partial));
|
|
1960
|
-
if (matches.length === 0) {
|
|
1961
|
-
// No matches - beep or do nothing
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
if (matches.length === 1) {
|
|
1965
|
-
// Single match - complete it
|
|
1966
|
-
const completion = matches[0];
|
|
1967
|
-
const suffix = currentLine.slice(cursorPos);
|
|
1968
|
-
const newLine = completion + suffix;
|
|
1969
|
-
const newCursor = completion.length;
|
|
1970
|
-
// Update readline
|
|
1971
|
-
this.rl.line = newLine;
|
|
1972
|
-
this.rl.cursor = newCursor;
|
|
1973
|
-
// Redraw
|
|
1974
|
-
output.write('\r\x1b[K'); // Clear line
|
|
1975
|
-
output.write(formatUserPrompt(this.profileLabel || this.profile));
|
|
1976
|
-
output.write(newLine);
|
|
1977
|
-
// Position cursor
|
|
1978
|
-
if (suffix.length > 0) {
|
|
1979
|
-
output.write(`\x1b[${suffix.length}D`);
|
|
1980
|
-
}
|
|
1981
|
-
// Sync to display components
|
|
1982
|
-
this.persistentPrompt.updateInput(newLine, newCursor);
|
|
1983
|
-
this.pinnedChatBox.setInput(newLine, newCursor);
|
|
1984
|
-
}
|
|
1985
|
-
else {
|
|
1986
|
-
// Multiple matches - show them
|
|
1987
|
-
output.write('\n');
|
|
1988
|
-
output.write(theme.ui.muted('Completions: ') + matches.join(' ') + '\n');
|
|
1989
|
-
this.rl.prompt();
|
|
1990
|
-
// Find common prefix for partial completion
|
|
1991
|
-
const commonPrefix = this.findCommonPrefix(matches);
|
|
1992
|
-
if (commonPrefix.length > partial.length) {
|
|
1993
|
-
const suffix = currentLine.slice(cursorPos);
|
|
1994
|
-
const newLine = commonPrefix + suffix;
|
|
1995
|
-
const newCursor = commonPrefix.length;
|
|
1996
|
-
this.rl.line = newLine;
|
|
1997
|
-
this.rl.cursor = newCursor;
|
|
1998
|
-
this.persistentPrompt.updateInput(newLine, newCursor);
|
|
1999
|
-
this.pinnedChatBox.setInput(newLine, newCursor);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
/**
|
|
2004
|
-
* Find the longest common prefix among strings
|
|
2005
|
-
*/
|
|
2006
|
-
findCommonPrefix(strings) {
|
|
2007
|
-
if (strings.length === 0)
|
|
2008
|
-
return '';
|
|
2009
|
-
if (strings.length === 1)
|
|
2010
|
-
return strings[0];
|
|
2011
|
-
let prefix = strings[0];
|
|
2012
|
-
for (let i = 1; i < strings.length; i++) {
|
|
2013
|
-
const str = strings[i];
|
|
2014
|
-
let j = 0;
|
|
2015
|
-
while (j < prefix.length && j < str.length && prefix[j] === str[j]) {
|
|
2016
|
-
j++;
|
|
2017
|
-
}
|
|
2018
|
-
prefix = prefix.slice(0, j);
|
|
2019
|
-
if (prefix.length === 0)
|
|
2020
|
-
break;
|
|
2021
|
-
}
|
|
2022
|
-
return prefix;
|
|
2023
|
-
}
|
|
2024
805
|
runDoctor() {
|
|
2025
806
|
const lines = [];
|
|
2026
807
|
lines.push(theme.bold('Environment diagnostics'));
|
|
@@ -2914,29 +1695,29 @@ export class InteractiveShell {
|
|
|
2914
1695
|
const trimmed = input.trim();
|
|
2915
1696
|
if (!trimmed) {
|
|
2916
1697
|
display.showWarning('Enter a number or type cancel.');
|
|
2917
|
-
this.
|
|
1698
|
+
this.terminalInput.render();
|
|
2918
1699
|
return;
|
|
2919
1700
|
}
|
|
2920
1701
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2921
1702
|
this.pendingInteraction = null;
|
|
2922
1703
|
display.showInfo('Model selection cancelled.');
|
|
2923
|
-
this.
|
|
1704
|
+
this.terminalInput.render();
|
|
2924
1705
|
return;
|
|
2925
1706
|
}
|
|
2926
1707
|
const choice = Number.parseInt(trimmed, 10);
|
|
2927
1708
|
if (!Number.isFinite(choice)) {
|
|
2928
1709
|
display.showWarning('Please enter a valid number.');
|
|
2929
|
-
this.
|
|
1710
|
+
this.terminalInput.render();
|
|
2930
1711
|
return;
|
|
2931
1712
|
}
|
|
2932
1713
|
const option = pending.options[choice - 1];
|
|
2933
1714
|
if (!option) {
|
|
2934
1715
|
display.showWarning('That option is not available.');
|
|
2935
|
-
this.
|
|
1716
|
+
this.terminalInput.render();
|
|
2936
1717
|
return;
|
|
2937
1718
|
}
|
|
2938
1719
|
this.showProviderModels(option);
|
|
2939
|
-
this.
|
|
1720
|
+
this.terminalInput.render();
|
|
2940
1721
|
}
|
|
2941
1722
|
async handleModelSelection(input) {
|
|
2942
1723
|
const pending = this.pendingInteraction;
|
|
@@ -2946,35 +1727,35 @@ export class InteractiveShell {
|
|
|
2946
1727
|
const trimmed = input.trim();
|
|
2947
1728
|
if (!trimmed) {
|
|
2948
1729
|
display.showWarning('Enter a number, type "back", or type "cancel".');
|
|
2949
|
-
this.
|
|
1730
|
+
this.terminalInput.render();
|
|
2950
1731
|
return;
|
|
2951
1732
|
}
|
|
2952
1733
|
if (trimmed.toLowerCase() === 'back') {
|
|
2953
1734
|
this.showModelMenu();
|
|
2954
|
-
this.
|
|
1735
|
+
this.terminalInput.render();
|
|
2955
1736
|
return;
|
|
2956
1737
|
}
|
|
2957
1738
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2958
1739
|
this.pendingInteraction = null;
|
|
2959
1740
|
display.showInfo('Model selection cancelled.');
|
|
2960
|
-
this.
|
|
1741
|
+
this.terminalInput.render();
|
|
2961
1742
|
return;
|
|
2962
1743
|
}
|
|
2963
1744
|
const choice = Number.parseInt(trimmed, 10);
|
|
2964
1745
|
if (!Number.isFinite(choice)) {
|
|
2965
1746
|
display.showWarning('Please enter a valid number.');
|
|
2966
|
-
this.
|
|
1747
|
+
this.terminalInput.render();
|
|
2967
1748
|
return;
|
|
2968
1749
|
}
|
|
2969
1750
|
const preset = pending.options[choice - 1];
|
|
2970
1751
|
if (!preset) {
|
|
2971
1752
|
display.showWarning('That option is not available.');
|
|
2972
|
-
this.
|
|
1753
|
+
this.terminalInput.render();
|
|
2973
1754
|
return;
|
|
2974
1755
|
}
|
|
2975
1756
|
this.pendingInteraction = null;
|
|
2976
1757
|
await this.applyModelPreset(preset);
|
|
2977
|
-
this.
|
|
1758
|
+
this.terminalInput.render();
|
|
2978
1759
|
}
|
|
2979
1760
|
async applyModelPreset(preset) {
|
|
2980
1761
|
try {
|
|
@@ -3007,30 +1788,30 @@ export class InteractiveShell {
|
|
|
3007
1788
|
const trimmed = input.trim();
|
|
3008
1789
|
if (!trimmed) {
|
|
3009
1790
|
display.showWarning('Enter a number or type cancel.');
|
|
3010
|
-
this.
|
|
1791
|
+
this.terminalInput.render();
|
|
3011
1792
|
return;
|
|
3012
1793
|
}
|
|
3013
1794
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
3014
1795
|
this.pendingInteraction = null;
|
|
3015
1796
|
display.showInfo('Secret management cancelled.');
|
|
3016
|
-
this.
|
|
1797
|
+
this.terminalInput.render();
|
|
3017
1798
|
return;
|
|
3018
1799
|
}
|
|
3019
1800
|
const choice = Number.parseInt(trimmed, 10);
|
|
3020
1801
|
if (!Number.isFinite(choice)) {
|
|
3021
1802
|
display.showWarning('Please enter a valid number.');
|
|
3022
|
-
this.
|
|
1803
|
+
this.terminalInput.render();
|
|
3023
1804
|
return;
|
|
3024
1805
|
}
|
|
3025
1806
|
const secret = pending.options[choice - 1];
|
|
3026
1807
|
if (!secret) {
|
|
3027
1808
|
display.showWarning('That option is not available.');
|
|
3028
|
-
this.
|
|
1809
|
+
this.terminalInput.render();
|
|
3029
1810
|
return;
|
|
3030
1811
|
}
|
|
3031
1812
|
display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
|
|
3032
1813
|
this.pendingInteraction = { type: 'secret-input', secret };
|
|
3033
|
-
this.
|
|
1814
|
+
this.terminalInput.render();
|
|
3034
1815
|
}
|
|
3035
1816
|
async handleSecretInput(input) {
|
|
3036
1817
|
const pending = this.pendingInteraction;
|
|
@@ -3040,14 +1821,14 @@ export class InteractiveShell {
|
|
|
3040
1821
|
const trimmed = input.trim();
|
|
3041
1822
|
if (!trimmed) {
|
|
3042
1823
|
display.showWarning('Enter a value or type cancel.');
|
|
3043
|
-
this.
|
|
1824
|
+
this.terminalInput.render();
|
|
3044
1825
|
return;
|
|
3045
1826
|
}
|
|
3046
1827
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
3047
1828
|
this.pendingInteraction = null;
|
|
3048
1829
|
this.pendingSecretRetry = null;
|
|
3049
1830
|
display.showInfo('Secret unchanged.');
|
|
3050
|
-
this.
|
|
1831
|
+
this.terminalInput.render();
|
|
3051
1832
|
return;
|
|
3052
1833
|
}
|
|
3053
1834
|
try {
|
|
@@ -3071,7 +1852,7 @@ export class InteractiveShell {
|
|
|
3071
1852
|
this.pendingInteraction = null;
|
|
3072
1853
|
this.pendingSecretRetry = null;
|
|
3073
1854
|
}
|
|
3074
|
-
this.
|
|
1855
|
+
this.terminalInput.render();
|
|
3075
1856
|
}
|
|
3076
1857
|
async processRequest(request) {
|
|
3077
1858
|
if (this.isProcessing) {
|
|
@@ -3087,11 +1868,8 @@ export class InteractiveShell {
|
|
|
3087
1868
|
return;
|
|
3088
1869
|
}
|
|
3089
1870
|
this.isProcessing = true;
|
|
1871
|
+
this.terminalInput.setStreaming(true);
|
|
3090
1872
|
const requestStartTime = Date.now(); // Alpha Zero 2 timing
|
|
3091
|
-
// Claude Code style: Enable scroll region for fixed chat box at bottom
|
|
3092
|
-
this.pinnedChatBox.setProcessing(true);
|
|
3093
|
-
// Claude Code style: Update status (optional, won't interfere with streaming)
|
|
3094
|
-
this.persistentPrompt.updateStatusBar({ message: '⏳ Processing...' });
|
|
3095
1873
|
this.uiAdapter.startProcessing('Working on your request');
|
|
3096
1874
|
this.setProcessingStatus();
|
|
3097
1875
|
try {
|
|
@@ -3100,13 +1878,6 @@ export class InteractiveShell {
|
|
|
3100
1878
|
// Claude Code style: Show unified streaming header before response
|
|
3101
1879
|
// This provides visual consistency with the startup Ready bar
|
|
3102
1880
|
display.showStreamingHeader();
|
|
3103
|
-
// NOTE: Do NOT call showWaitingPrompt() here - the PinnedChatBox scroll region
|
|
3104
|
-
// handles rendering the prompt at the bottom via renderPersistentInput().
|
|
3105
|
-
// Calling showWaitingPrompt() writes to the scroll area instead of the reserved bottom.
|
|
3106
|
-
// Claude Code style: Start streaming mode using UnifiedChatBox
|
|
3107
|
-
// - User input is captured and queued
|
|
3108
|
-
// - After response ends, new prompt appears at bottom
|
|
3109
|
-
this.unifiedChatBox.startStreaming();
|
|
3110
1881
|
// DISABLED streaming - wait for full response instead
|
|
3111
1882
|
// This keeps cursor at the > prompt during processing
|
|
3112
1883
|
await agent.send(request, false);
|
|
@@ -3125,30 +1896,20 @@ export class InteractiveShell {
|
|
|
3125
1896
|
}
|
|
3126
1897
|
}
|
|
3127
1898
|
finally {
|
|
3128
|
-
// Claude Code style: End streaming mode using UnifiedChatBox
|
|
3129
|
-
// This restores readline echo and returns queue info
|
|
3130
|
-
const streamResult = this.unifiedChatBox.endStreaming();
|
|
3131
|
-
// Process any queued inputs that came in during streaming
|
|
3132
|
-
this.processUnifiedChatBoxQueue();
|
|
3133
|
-
// Show queue indicator if there were queued inputs
|
|
3134
|
-
if (streamResult.queuedCount > 0) {
|
|
3135
|
-
this.unifiedChatBox.showQueueIndicator();
|
|
3136
|
-
}
|
|
3137
1899
|
display.stopThinking(false);
|
|
3138
1900
|
this.isProcessing = false;
|
|
3139
|
-
this.
|
|
1901
|
+
this.terminalInput.setStreaming(false);
|
|
3140
1902
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
3141
1903
|
this.setIdleStatus();
|
|
3142
1904
|
display.newLine();
|
|
3143
|
-
|
|
3144
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
1905
|
+
this.updateStatusMessage(null);
|
|
3145
1906
|
// Claude Code style: Show unified status bar before prompt
|
|
3146
1907
|
// This creates consistent UI between startup and post-streaming
|
|
3147
1908
|
this.showUnifiedStatusBar();
|
|
3148
1909
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
3149
1910
|
// Claude Code style: New prompt naturally appears at bottom
|
|
3150
1911
|
this.ensureReadlineReady();
|
|
3151
|
-
this.
|
|
1912
|
+
this.terminalInput.render();
|
|
3152
1913
|
this.scheduleQueueProcessing();
|
|
3153
1914
|
this.refreshQueueIndicators();
|
|
3154
1915
|
}
|
|
@@ -3180,26 +1941,17 @@ export class InteractiveShell {
|
|
|
3180
1941
|
return;
|
|
3181
1942
|
}
|
|
3182
1943
|
this.isProcessing = true;
|
|
1944
|
+
this.terminalInput.setStreaming(true);
|
|
3183
1945
|
const overallStartTime = Date.now();
|
|
3184
|
-
// Claude Code style: Enable scroll region for fixed chat box at bottom
|
|
3185
|
-
this.pinnedChatBox.setProcessing(true);
|
|
3186
1946
|
// Initialize the task completion detector
|
|
3187
1947
|
const completionDetector = getTaskCompletionDetector();
|
|
3188
1948
|
completionDetector.reset();
|
|
3189
|
-
// Claude Code style: Update status
|
|
3190
|
-
this.persistentPrompt.updateStatusBar({ message: '🔄 Continuous mode...' });
|
|
3191
1949
|
display.showSystemMessage(`🔄 Starting continuous execution mode. Press Ctrl+C to stop.`);
|
|
3192
1950
|
display.showSystemMessage(`📊 Using intelligent task completion detection with AI verification.`);
|
|
3193
1951
|
this.uiAdapter.startProcessing('Continuous execution mode');
|
|
3194
1952
|
this.setProcessingStatus();
|
|
3195
1953
|
// Claude Code style: Show unified streaming header before response
|
|
3196
1954
|
display.showStreamingHeader();
|
|
3197
|
-
// NOTE: Do NOT call showWaitingPrompt() here - the PinnedChatBox scroll region
|
|
3198
|
-
// handles rendering the prompt at the bottom via renderPersistentInput().
|
|
3199
|
-
// Calling showWaitingPrompt() writes to the scroll area instead of the reserved bottom.
|
|
3200
|
-
// Claude Code style: Start streaming mode using UnifiedChatBox
|
|
3201
|
-
// - User input is captured and queued
|
|
3202
|
-
this.unifiedChatBox.startStreaming();
|
|
3203
1955
|
let iteration = 0;
|
|
3204
1956
|
let lastResponse = '';
|
|
3205
1957
|
let consecutiveNoProgress = 0;
|
|
@@ -3225,9 +1977,7 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
|
|
|
3225
1977
|
while (iteration < MAX_ITERATIONS) {
|
|
3226
1978
|
iteration++;
|
|
3227
1979
|
display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
|
|
3228
|
-
|
|
3229
|
-
this.pinnedChatBox.setStatusMessage(`Working on iteration ${iteration}...`);
|
|
3230
|
-
this.pinnedChatBox.forceRender();
|
|
1980
|
+
this.updateStatusMessage(`Working on iteration ${iteration}...`);
|
|
3231
1981
|
try {
|
|
3232
1982
|
// Send the request and capture the response (streaming disabled)
|
|
3233
1983
|
const response = await agent.send(currentPrompt, false);
|
|
@@ -3361,28 +2111,18 @@ What's the next action?`;
|
|
|
3361
2111
|
display.showSystemMessage(`\n🏁 Continuous execution completed: ${iteration} iterations, ${minutes}m ${seconds}s total`);
|
|
3362
2112
|
// Reset completion detector for next task
|
|
3363
2113
|
resetTaskCompletionDetector();
|
|
3364
|
-
// Claude Code style: End streaming mode using UnifiedChatBox
|
|
3365
|
-
const streamResult = this.unifiedChatBox.endStreaming();
|
|
3366
|
-
// Process any queued inputs that came in during streaming
|
|
3367
|
-
this.processUnifiedChatBoxQueue();
|
|
3368
|
-
// Show queue indicator if there were queued inputs
|
|
3369
|
-
if (streamResult.queuedCount > 0) {
|
|
3370
|
-
this.unifiedChatBox.showQueueIndicator();
|
|
3371
|
-
}
|
|
3372
2114
|
this.isProcessing = false;
|
|
3373
|
-
this.
|
|
2115
|
+
this.terminalInput.setStreaming(false);
|
|
2116
|
+
this.updateStatusMessage(null);
|
|
3374
2117
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
3375
2118
|
this.setIdleStatus();
|
|
3376
2119
|
display.newLine();
|
|
3377
|
-
// Clear the processing status
|
|
3378
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
3379
2120
|
// Claude Code style: Show unified status bar before prompt
|
|
3380
2121
|
// This creates consistent UI between startup and post-streaming
|
|
3381
2122
|
this.showUnifiedStatusBar();
|
|
3382
2123
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
3383
2124
|
// Claude Code style: New prompt naturally appears at bottom
|
|
3384
2125
|
this.ensureReadlineReady();
|
|
3385
|
-
this.rl.prompt();
|
|
3386
2126
|
this.scheduleQueueProcessing();
|
|
3387
2127
|
this.refreshQueueIndicators();
|
|
3388
2128
|
}
|
|
@@ -3590,8 +2330,7 @@ What's the next action?`;
|
|
|
3590
2330
|
this.autoTestInFlight = true;
|
|
3591
2331
|
const command = 'npm test -- --runInBand';
|
|
3592
2332
|
display.showSystemMessage(`🧪 Auto-testing recent changes (${trigger}) with "${command}"...`);
|
|
3593
|
-
this.
|
|
3594
|
-
this.pinnedChatBox.forceRender();
|
|
2333
|
+
this.updateStatusMessage('Running tests automatically...');
|
|
3595
2334
|
try {
|
|
3596
2335
|
const { stdout, stderr } = await execAsync(command, {
|
|
3597
2336
|
cwd: this.workingDir,
|
|
@@ -3619,8 +2358,7 @@ What's the next action?`;
|
|
|
3619
2358
|
});
|
|
3620
2359
|
}
|
|
3621
2360
|
finally {
|
|
3622
|
-
this.
|
|
3623
|
-
this.pinnedChatBox.forceRender();
|
|
2361
|
+
this.updateStatusMessage(null);
|
|
3624
2362
|
this.autoTestInFlight = false;
|
|
3625
2363
|
}
|
|
3626
2364
|
}
|
|
@@ -3691,7 +2429,7 @@ What's the next action?`;
|
|
|
3691
2429
|
display.showNarrative(content.trim());
|
|
3692
2430
|
}
|
|
3693
2431
|
// The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
|
|
3694
|
-
this.
|
|
2432
|
+
this.terminalInput.render();
|
|
3695
2433
|
return;
|
|
3696
2434
|
}
|
|
3697
2435
|
const cleanup = this.handleContextTelemetry(metadata, enriched);
|
|
@@ -3710,10 +2448,6 @@ What's the next action?`;
|
|
|
3710
2448
|
onContextRecovery: (attempt, maxAttempts, message) => {
|
|
3711
2449
|
// Show recovery progress in UI
|
|
3712
2450
|
display.showSystemMessage(`⚡ Context Recovery (${attempt}/${maxAttempts}): ${message}`);
|
|
3713
|
-
// Update persistent prompt to show recovery status
|
|
3714
|
-
this.persistentPrompt.updateStatusBar({
|
|
3715
|
-
contextUsage: 100, // Show as full during recovery
|
|
3716
|
-
});
|
|
3717
2451
|
},
|
|
3718
2452
|
onContextPruned: (removedCount, stats) => {
|
|
3719
2453
|
// Clear squish overlay if active
|
|
@@ -3727,30 +2461,28 @@ What's the next action?`;
|
|
|
3727
2461
|
// Update context usage in UI
|
|
3728
2462
|
if (typeof percentage === 'number') {
|
|
3729
2463
|
this.uiAdapter.updateContextUsage(percentage);
|
|
3730
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentage });
|
|
3731
2464
|
}
|
|
3732
2465
|
// Ensure prompt remains visible at bottom after context messages
|
|
3733
|
-
this.
|
|
2466
|
+
this.terminalInput.render();
|
|
3734
2467
|
},
|
|
3735
2468
|
onContinueAfterRecovery: () => {
|
|
3736
2469
|
// Update UI to show we're continuing after context recovery
|
|
3737
2470
|
display.showSystemMessage(`🔄 Continuing after context recovery...`);
|
|
3738
|
-
|
|
3739
|
-
this.
|
|
3740
|
-
this.pinnedChatBox.forceRender();
|
|
2471
|
+
this.updateStatusMessage('Retrying with reduced context...');
|
|
2472
|
+
this.terminalInput.render();
|
|
3741
2473
|
},
|
|
3742
2474
|
onAutoContinue: (attempt, maxAttempts, _message) => {
|
|
3743
2475
|
// Show auto-continue progress in UI
|
|
3744
2476
|
display.showSystemMessage(`🔄 Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
|
|
3745
|
-
this.
|
|
3746
|
-
this.
|
|
2477
|
+
this.updateStatusMessage('Auto-continuing...');
|
|
2478
|
+
this.terminalInput.render();
|
|
3747
2479
|
},
|
|
3748
2480
|
onCancelled: () => {
|
|
3749
2481
|
// Update UI to show operation was cancelled
|
|
3750
2482
|
display.showWarning('Operation cancelled.');
|
|
3751
|
-
this.
|
|
3752
|
-
this.
|
|
3753
|
-
this.
|
|
2483
|
+
this.updateStatusMessage(null);
|
|
2484
|
+
this.terminalInput.setStreaming(false);
|
|
2485
|
+
this.terminalInput.render();
|
|
3754
2486
|
},
|
|
3755
2487
|
onVerificationNeeded: () => {
|
|
3756
2488
|
void this.enforceAutoTests('verification');
|
|
@@ -3784,10 +2516,9 @@ What's the next action?`;
|
|
|
3784
2516
|
* just like on fresh startup.
|
|
3785
2517
|
*/
|
|
3786
2518
|
resetChatBoxAfterModelSwap() {
|
|
3787
|
-
this.
|
|
3788
|
-
this.
|
|
3789
|
-
this.
|
|
3790
|
-
this.pinnedChatBox.forceRender();
|
|
2519
|
+
this.updateStatusMessage(null);
|
|
2520
|
+
this.terminalInput.setStreaming(false);
|
|
2521
|
+
this.terminalInput.render();
|
|
3791
2522
|
this.ensureReadlineReady();
|
|
3792
2523
|
}
|
|
3793
2524
|
buildSystemPrompt() {
|
|
@@ -3854,10 +2585,6 @@ What's the next action?`;
|
|
|
3854
2585
|
// Always update context usage in the UI
|
|
3855
2586
|
const percentUsed = Math.round(usageRatio * 100);
|
|
3856
2587
|
this.uiAdapter.updateContextUsage(percentUsed);
|
|
3857
|
-
// Update persistent prompt status bar with context usage
|
|
3858
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
|
|
3859
|
-
// Update pinned chat box with context usage
|
|
3860
|
-
this.pinnedChatBox.setContextUsage(percentUsed);
|
|
3861
2588
|
if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
|
|
3862
2589
|
return null;
|
|
3863
2590
|
}
|
|
@@ -3905,8 +2632,6 @@ What's the next action?`;
|
|
|
3905
2632
|
const percentUsed = Math.round((totalTokens / windowTokens) * 100);
|
|
3906
2633
|
// Update context usage in unified UI
|
|
3907
2634
|
this.uiAdapter.updateContextUsage(percentUsed);
|
|
3908
|
-
// Update persistent prompt status bar with context usage
|
|
3909
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
|
|
3910
2635
|
display.showSystemMessage([
|
|
3911
2636
|
`Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
|
|
3912
2637
|
`(${percentUsed}% full). Running automatic cleanup...`,
|
|
@@ -4143,14 +2868,6 @@ What's the next action?`;
|
|
|
4143
2868
|
const entry = this.agentMenu.options.find((option) => option.name === name);
|
|
4144
2869
|
return entry?.label ?? name;
|
|
4145
2870
|
}
|
|
4146
|
-
updatePersistentPromptFileChanges() {
|
|
4147
|
-
const summary = this._fileChangeTracker.getSummary();
|
|
4148
|
-
if (summary.files === 0) {
|
|
4149
|
-
return;
|
|
4150
|
-
}
|
|
4151
|
-
const fileChangesText = `${summary.files} file${summary.files === 1 ? '' : 's'} +${summary.additions} -${summary.removals}`;
|
|
4152
|
-
this.persistentPrompt.updateStatusBar({ fileChanges: fileChangesText });
|
|
4153
|
-
}
|
|
4154
2871
|
extractThoughtSummary(thought) {
|
|
4155
2872
|
// Extract first non-empty line
|
|
4156
2873
|
const lines = thought?.split('\n').filter(line => line.trim()) ?? [];
|
|
@@ -4440,32 +3157,6 @@ What's the next action?`;
|
|
|
4440
3157
|
];
|
|
4441
3158
|
display.showSystemMessage(lines.join('\n'));
|
|
4442
3159
|
}
|
|
4443
|
-
enableBracketedPasteMode() {
|
|
4444
|
-
if (!input.isTTY || !output.isTTY) {
|
|
4445
|
-
return false;
|
|
4446
|
-
}
|
|
4447
|
-
try {
|
|
4448
|
-
output.write(BRACKETED_PASTE_ENABLE);
|
|
4449
|
-
return true;
|
|
4450
|
-
}
|
|
4451
|
-
catch (error) {
|
|
4452
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4453
|
-
display.showWarning(`Unable to enable bracketed paste: ${message}`);
|
|
4454
|
-
return false;
|
|
4455
|
-
}
|
|
4456
|
-
}
|
|
4457
|
-
disableBracketedPasteMode() {
|
|
4458
|
-
if (!this.bracketedPasteEnabled || !output.isTTY) {
|
|
4459
|
-
return;
|
|
4460
|
-
}
|
|
4461
|
-
try {
|
|
4462
|
-
output.write(BRACKETED_PASTE_DISABLE);
|
|
4463
|
-
}
|
|
4464
|
-
finally {
|
|
4465
|
-
this.bracketedPasteEnabled = false;
|
|
4466
|
-
this.bracketedPaste.reset();
|
|
4467
|
-
}
|
|
4468
|
-
}
|
|
4469
3160
|
/**
|
|
4470
3161
|
* Set the cached provider status for unified status bar display.
|
|
4471
3162
|
* Called once at startup after checking providers.
|
|
@@ -4473,10 +3164,6 @@ What's the next action?`;
|
|
|
4473
3164
|
setProviderStatus(providers) {
|
|
4474
3165
|
this.cachedProviderStatus = providers;
|
|
4475
3166
|
}
|
|
4476
|
-
// NOTE: showWaitingPrompt() was removed because the PinnedChatBox scroll region
|
|
4477
|
-
// now handles rendering the prompt at the bottom via renderPersistentInput().
|
|
4478
|
-
// The scroll region approach keeps the cursor in the correct location during
|
|
4479
|
-
// streaming while allowing the user to type in the reserved bottom area.
|
|
4480
3167
|
/**
|
|
4481
3168
|
* Show the unified status bar (Claude Code style).
|
|
4482
3169
|
* Displays provider indicators and ready hints before the prompt.
|