erosolar-cli 1.7.190 → 1.7.192
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/core/toolRuntime.d.ts +19 -7
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +11 -9
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +29 -126
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +164 -1476
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +3 -1
- package/dist/shell/systemPrompt.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 +192 -0
- package/dist/shell/terminalInputAdapter.js.map +1 -0
- package/dist/tools/devTools.d.ts.map +1 -1
- package/dist/tools/devTools.js +554 -0
- package/dist/tools/devTools.js.map +1 -1
- 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,89 @@ 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
|
+
'https://www.anthropic.com/news/disrupting-AI-espionage',
|
|
304
|
+
'',
|
|
305
|
+
`Email ${highlightedEmail} with any bugs or feedback`,
|
|
306
|
+
'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
|
|
307
|
+
'npm: https://www.npmjs.com/package/erosolar-cli',
|
|
308
|
+
].join('\n');
|
|
309
|
+
display.showInfo(infoMessage);
|
|
310
|
+
exit(0);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Update status bar message
|
|
314
|
+
*/
|
|
315
|
+
updateStatusMessage(_message) {
|
|
316
|
+
this.terminalInput.render();
|
|
317
317
|
}
|
|
318
318
|
async handleToolSettingsInput(input) {
|
|
319
319
|
const pending = this.pendingInteraction;
|
|
@@ -323,26 +323,26 @@ export class InteractiveShell {
|
|
|
323
323
|
const trimmed = input.trim();
|
|
324
324
|
if (!trimmed) {
|
|
325
325
|
display.showWarning('Enter a number, "save", "defaults", or "cancel".');
|
|
326
|
-
this.
|
|
326
|
+
this.terminalInput.render();
|
|
327
327
|
return;
|
|
328
328
|
}
|
|
329
329
|
const normalized = trimmed.toLowerCase();
|
|
330
330
|
if (normalized === 'cancel') {
|
|
331
331
|
this.pendingInteraction = null;
|
|
332
332
|
display.showInfo('Tool selection cancelled.');
|
|
333
|
-
this.
|
|
333
|
+
this.terminalInput.render();
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
336
|
if (normalized === 'defaults') {
|
|
337
337
|
pending.selection = buildEnabledToolSet(null);
|
|
338
338
|
this.renderToolMenu(pending);
|
|
339
|
-
this.
|
|
339
|
+
this.terminalInput.render();
|
|
340
340
|
return;
|
|
341
341
|
}
|
|
342
342
|
if (normalized === 'save') {
|
|
343
343
|
await this.persistToolSelection(pending);
|
|
344
344
|
this.pendingInteraction = null;
|
|
345
|
-
this.
|
|
345
|
+
this.terminalInput.render();
|
|
346
346
|
return;
|
|
347
347
|
}
|
|
348
348
|
const choice = Number.parseInt(trimmed, 10);
|
|
@@ -360,11 +360,11 @@ export class InteractiveShell {
|
|
|
360
360
|
}
|
|
361
361
|
this.renderToolMenu(pending);
|
|
362
362
|
}
|
|
363
|
-
this.
|
|
363
|
+
this.terminalInput.render();
|
|
364
364
|
return;
|
|
365
365
|
}
|
|
366
366
|
display.showWarning('Enter a number, "save", "defaults", or "cancel".');
|
|
367
|
-
this.
|
|
367
|
+
this.terminalInput.render();
|
|
368
368
|
}
|
|
369
369
|
async persistToolSelection(interaction) {
|
|
370
370
|
if (setsEqual(interaction.selection, interaction.initialSelection)) {
|
|
@@ -391,36 +391,36 @@ export class InteractiveShell {
|
|
|
391
391
|
if (!this.agentMenu) {
|
|
392
392
|
this.pendingInteraction = null;
|
|
393
393
|
display.showWarning('Agent selection is unavailable in this CLI.');
|
|
394
|
-
this.
|
|
394
|
+
this.terminalInput.render();
|
|
395
395
|
return;
|
|
396
396
|
}
|
|
397
397
|
const trimmed = input.trim();
|
|
398
398
|
if (!trimmed) {
|
|
399
399
|
display.showWarning('Enter a number or type "cancel".');
|
|
400
|
-
this.
|
|
400
|
+
this.terminalInput.render();
|
|
401
401
|
return;
|
|
402
402
|
}
|
|
403
403
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
404
404
|
this.pendingInteraction = null;
|
|
405
405
|
display.showInfo('Agent selection cancelled.');
|
|
406
|
-
this.
|
|
406
|
+
this.terminalInput.render();
|
|
407
407
|
return;
|
|
408
408
|
}
|
|
409
409
|
const choice = Number.parseInt(trimmed, 10);
|
|
410
410
|
if (!Number.isFinite(choice)) {
|
|
411
411
|
display.showWarning('Please enter a valid number.');
|
|
412
|
-
this.
|
|
412
|
+
this.terminalInput.render();
|
|
413
413
|
return;
|
|
414
414
|
}
|
|
415
415
|
const option = pending.options[choice - 1];
|
|
416
416
|
if (!option) {
|
|
417
417
|
display.showWarning('That option is not available.');
|
|
418
|
-
this.
|
|
418
|
+
this.terminalInput.render();
|
|
419
419
|
return;
|
|
420
420
|
}
|
|
421
421
|
await this.persistAgentSelection(option.name);
|
|
422
422
|
this.pendingInteraction = null;
|
|
423
|
-
this.
|
|
423
|
+
this.terminalInput.render();
|
|
424
424
|
}
|
|
425
425
|
async persistAgentSelection(profileName) {
|
|
426
426
|
if (!this.agentMenu) {
|
|
@@ -442,380 +442,12 @@ export class InteractiveShell {
|
|
|
442
442
|
display.showInfo(`${this.agentMenuLabel(profileName)} will load the next time you start the CLI. Restart to switch now.`);
|
|
443
443
|
}
|
|
444
444
|
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
445
|
// Handle terminal resize
|
|
568
446
|
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);
|
|
447
|
+
this.terminalInput.handleResize();
|
|
602
448
|
});
|
|
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);
|
|
449
|
+
// Show initial input UI
|
|
450
|
+
this.terminalInput.render();
|
|
819
451
|
}
|
|
820
452
|
setupStatusTracking() {
|
|
821
453
|
this.statusSubscription = this.statusTracker.subscribe((_state) => {
|
|
@@ -873,14 +505,10 @@ export class InteractiveShell {
|
|
|
873
505
|
if (!shouldShow && this.slashPreviewVisible) {
|
|
874
506
|
this.slashPreviewVisible = false;
|
|
875
507
|
this.uiAdapter.hideSlashCommandPreview();
|
|
876
|
-
// Show persistent prompt again after hiding overlay
|
|
877
|
-
if (!this.isProcessing) {
|
|
878
|
-
this.persistentPrompt.show();
|
|
879
|
-
}
|
|
880
508
|
}
|
|
881
509
|
}
|
|
882
510
|
shouldShowSlashCommandPreview() {
|
|
883
|
-
const line = this.
|
|
511
|
+
const line = this.currentInput ?? '';
|
|
884
512
|
if (!line.trim()) {
|
|
885
513
|
return false;
|
|
886
514
|
}
|
|
@@ -889,642 +517,28 @@ export class InteractiveShell {
|
|
|
889
517
|
}
|
|
890
518
|
showSlashCommandPreview() {
|
|
891
519
|
// Filter commands based on current input
|
|
892
|
-
const line = this.
|
|
520
|
+
const line = this.currentInput ?? '';
|
|
893
521
|
const trimmed = line.trimStart();
|
|
894
522
|
// Filter commands that match the current input
|
|
895
523
|
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
524
|
// Show in the unified UI with dynamic overlay
|
|
899
525
|
this.uiAdapter.showSlashCommandPreview(filtered);
|
|
900
526
|
// Don't reprompt - this causes flickering
|
|
901
527
|
}
|
|
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
528
|
/**
|
|
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.
|
|
529
|
+
* Ensure the terminal input is ready for interactive input.
|
|
929
530
|
*/
|
|
930
531
|
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
|
-
}
|
|
532
|
+
this.terminalInput.render();
|
|
1508
533
|
}
|
|
1509
534
|
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
535
|
if (this.isProcessing) {
|
|
1523
536
|
this.setProcessingStatus();
|
|
1524
537
|
}
|
|
1525
538
|
else {
|
|
1526
539
|
this.setIdleStatus();
|
|
1527
540
|
}
|
|
541
|
+
this.terminalInput.render();
|
|
1528
542
|
}
|
|
1529
543
|
enqueueFollowUpAction(action) {
|
|
1530
544
|
this.followUpQueue.push(action);
|
|
@@ -1533,23 +547,17 @@ export class InteractiveShell {
|
|
|
1533
547
|
const preview = normalized.length > previewLimit ? `${normalized.slice(0, previewLimit - 3)}...` : normalized;
|
|
1534
548
|
const position = this.followUpQueue.length === 1 ? 'to run next' : `#${this.followUpQueue.length} in queue`;
|
|
1535
549
|
const label = action.type === 'continuous' ? 'continuous command' : 'follow-up';
|
|
1536
|
-
const queueCount = this.followUpQueue.length;
|
|
1537
550
|
// Show immediate acknowledgment with checkmark
|
|
1538
|
-
const queueLabel = queueCount === 1 ? '1 queued' : `${queueCount} queued`;
|
|
1539
551
|
if (preview) {
|
|
1540
552
|
display.showInfo(`✓ ${theme.info(label)} ${position}: ${theme.ui.muted(preview)}`);
|
|
1541
553
|
}
|
|
1542
554
|
else {
|
|
1543
555
|
display.showInfo(`✓ ${theme.info(label)} queued ${position}.`);
|
|
1544
556
|
}
|
|
1545
|
-
// Update status bar to show queue count
|
|
1546
|
-
this.persistentPrompt.updateStatusBar({
|
|
1547
|
-
message: `⏳ Processing... (${queueLabel})`
|
|
1548
|
-
});
|
|
1549
557
|
this.refreshQueueIndicators();
|
|
1550
558
|
this.scheduleQueueProcessing();
|
|
1551
559
|
// Re-show the prompt so user can continue typing more follow-ups
|
|
1552
|
-
this.
|
|
560
|
+
this.terminalInput.render();
|
|
1553
561
|
}
|
|
1554
562
|
scheduleQueueProcessing() {
|
|
1555
563
|
if (!this.followUpQueue.length) {
|
|
@@ -1561,71 +569,8 @@ export class InteractiveShell {
|
|
|
1561
569
|
});
|
|
1562
570
|
}
|
|
1563
571
|
/**
|
|
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.
|
|
572
|
+
* Process queued follow-up actions.
|
|
1603
573
|
*/
|
|
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
574
|
async processQueuedActions() {
|
|
1630
575
|
if (this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
|
|
1631
576
|
return;
|
|
@@ -1659,85 +604,22 @@ export class InteractiveShell {
|
|
|
1659
604
|
if (await this.handlePendingInteraction(trimmed)) {
|
|
1660
605
|
return;
|
|
1661
606
|
}
|
|
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
607
|
if (!trimmed) {
|
|
1726
|
-
this.rl.prompt();
|
|
1727
608
|
return;
|
|
1728
609
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
610
|
+
const lower = trimmed.toLowerCase();
|
|
611
|
+
if (lower === 'exit' || lower === 'quit') {
|
|
612
|
+
this.shutdown();
|
|
1731
613
|
return;
|
|
1732
614
|
}
|
|
1733
|
-
if (
|
|
615
|
+
if (lower === 'clear') {
|
|
1734
616
|
display.clear();
|
|
1735
|
-
this.
|
|
617
|
+
this.terminalInput.render();
|
|
1736
618
|
return;
|
|
1737
619
|
}
|
|
1738
|
-
if (
|
|
620
|
+
if (lower === 'help') {
|
|
1739
621
|
this.showHelp();
|
|
1740
|
-
this.
|
|
622
|
+
this.terminalInput.render();
|
|
1741
623
|
return;
|
|
1742
624
|
}
|
|
1743
625
|
if (trimmed.startsWith('/')) {
|
|
@@ -1747,12 +629,12 @@ export class InteractiveShell {
|
|
|
1747
629
|
// Check for continuous/infinite loop commands
|
|
1748
630
|
if (this.isContinuousCommand(trimmed)) {
|
|
1749
631
|
await this.processContinuousRequest(trimmed);
|
|
1750
|
-
this.
|
|
632
|
+
this.terminalInput.render();
|
|
1751
633
|
return;
|
|
1752
634
|
}
|
|
1753
635
|
// Direct execution for all inputs, including multi-line pastes
|
|
1754
636
|
await this.processRequest(trimmed);
|
|
1755
|
-
this.
|
|
637
|
+
this.terminalInput.render();
|
|
1756
638
|
}
|
|
1757
639
|
/**
|
|
1758
640
|
* Check if the command is a continuous/infinite loop command
|
|
@@ -1804,12 +686,6 @@ export class InteractiveShell {
|
|
|
1804
686
|
case 'agent-selection':
|
|
1805
687
|
await this.handleAgentSelectionInput(input);
|
|
1806
688
|
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
689
|
default:
|
|
1814
690
|
return false;
|
|
1815
691
|
}
|
|
@@ -1818,7 +694,7 @@ export class InteractiveShell {
|
|
|
1818
694
|
const [command] = input.split(/\s+/);
|
|
1819
695
|
if (!command) {
|
|
1820
696
|
display.showWarning('Enter a slash command.');
|
|
1821
|
-
this.
|
|
697
|
+
this.terminalInput.render();
|
|
1822
698
|
return;
|
|
1823
699
|
}
|
|
1824
700
|
switch (command) {
|
|
@@ -1893,7 +769,7 @@ export class InteractiveShell {
|
|
|
1893
769
|
}
|
|
1894
770
|
break;
|
|
1895
771
|
}
|
|
1896
|
-
this.
|
|
772
|
+
this.terminalInput.render();
|
|
1897
773
|
}
|
|
1898
774
|
async tryCustomSlashCommand(command, fullInput) {
|
|
1899
775
|
const custom = this.customCommandMap.get(command);
|
|
@@ -1927,100 +803,6 @@ export class InteractiveShell {
|
|
|
1927
803
|
];
|
|
1928
804
|
display.showSystemMessage(info.join('\n'));
|
|
1929
805
|
}
|
|
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
806
|
runDoctor() {
|
|
2025
807
|
const lines = [];
|
|
2026
808
|
lines.push(theme.bold('Environment diagnostics'));
|
|
@@ -2914,29 +1696,29 @@ export class InteractiveShell {
|
|
|
2914
1696
|
const trimmed = input.trim();
|
|
2915
1697
|
if (!trimmed) {
|
|
2916
1698
|
display.showWarning('Enter a number or type cancel.');
|
|
2917
|
-
this.
|
|
1699
|
+
this.terminalInput.render();
|
|
2918
1700
|
return;
|
|
2919
1701
|
}
|
|
2920
1702
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2921
1703
|
this.pendingInteraction = null;
|
|
2922
1704
|
display.showInfo('Model selection cancelled.');
|
|
2923
|
-
this.
|
|
1705
|
+
this.terminalInput.render();
|
|
2924
1706
|
return;
|
|
2925
1707
|
}
|
|
2926
1708
|
const choice = Number.parseInt(trimmed, 10);
|
|
2927
1709
|
if (!Number.isFinite(choice)) {
|
|
2928
1710
|
display.showWarning('Please enter a valid number.');
|
|
2929
|
-
this.
|
|
1711
|
+
this.terminalInput.render();
|
|
2930
1712
|
return;
|
|
2931
1713
|
}
|
|
2932
1714
|
const option = pending.options[choice - 1];
|
|
2933
1715
|
if (!option) {
|
|
2934
1716
|
display.showWarning('That option is not available.');
|
|
2935
|
-
this.
|
|
1717
|
+
this.terminalInput.render();
|
|
2936
1718
|
return;
|
|
2937
1719
|
}
|
|
2938
1720
|
this.showProviderModels(option);
|
|
2939
|
-
this.
|
|
1721
|
+
this.terminalInput.render();
|
|
2940
1722
|
}
|
|
2941
1723
|
async handleModelSelection(input) {
|
|
2942
1724
|
const pending = this.pendingInteraction;
|
|
@@ -2946,35 +1728,35 @@ export class InteractiveShell {
|
|
|
2946
1728
|
const trimmed = input.trim();
|
|
2947
1729
|
if (!trimmed) {
|
|
2948
1730
|
display.showWarning('Enter a number, type "back", or type "cancel".');
|
|
2949
|
-
this.
|
|
1731
|
+
this.terminalInput.render();
|
|
2950
1732
|
return;
|
|
2951
1733
|
}
|
|
2952
1734
|
if (trimmed.toLowerCase() === 'back') {
|
|
2953
1735
|
this.showModelMenu();
|
|
2954
|
-
this.
|
|
1736
|
+
this.terminalInput.render();
|
|
2955
1737
|
return;
|
|
2956
1738
|
}
|
|
2957
1739
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2958
1740
|
this.pendingInteraction = null;
|
|
2959
1741
|
display.showInfo('Model selection cancelled.');
|
|
2960
|
-
this.
|
|
1742
|
+
this.terminalInput.render();
|
|
2961
1743
|
return;
|
|
2962
1744
|
}
|
|
2963
1745
|
const choice = Number.parseInt(trimmed, 10);
|
|
2964
1746
|
if (!Number.isFinite(choice)) {
|
|
2965
1747
|
display.showWarning('Please enter a valid number.');
|
|
2966
|
-
this.
|
|
1748
|
+
this.terminalInput.render();
|
|
2967
1749
|
return;
|
|
2968
1750
|
}
|
|
2969
1751
|
const preset = pending.options[choice - 1];
|
|
2970
1752
|
if (!preset) {
|
|
2971
1753
|
display.showWarning('That option is not available.');
|
|
2972
|
-
this.
|
|
1754
|
+
this.terminalInput.render();
|
|
2973
1755
|
return;
|
|
2974
1756
|
}
|
|
2975
1757
|
this.pendingInteraction = null;
|
|
2976
1758
|
await this.applyModelPreset(preset);
|
|
2977
|
-
this.
|
|
1759
|
+
this.terminalInput.render();
|
|
2978
1760
|
}
|
|
2979
1761
|
async applyModelPreset(preset) {
|
|
2980
1762
|
try {
|
|
@@ -3007,30 +1789,30 @@ export class InteractiveShell {
|
|
|
3007
1789
|
const trimmed = input.trim();
|
|
3008
1790
|
if (!trimmed) {
|
|
3009
1791
|
display.showWarning('Enter a number or type cancel.');
|
|
3010
|
-
this.
|
|
1792
|
+
this.terminalInput.render();
|
|
3011
1793
|
return;
|
|
3012
1794
|
}
|
|
3013
1795
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
3014
1796
|
this.pendingInteraction = null;
|
|
3015
1797
|
display.showInfo('Secret management cancelled.');
|
|
3016
|
-
this.
|
|
1798
|
+
this.terminalInput.render();
|
|
3017
1799
|
return;
|
|
3018
1800
|
}
|
|
3019
1801
|
const choice = Number.parseInt(trimmed, 10);
|
|
3020
1802
|
if (!Number.isFinite(choice)) {
|
|
3021
1803
|
display.showWarning('Please enter a valid number.');
|
|
3022
|
-
this.
|
|
1804
|
+
this.terminalInput.render();
|
|
3023
1805
|
return;
|
|
3024
1806
|
}
|
|
3025
1807
|
const secret = pending.options[choice - 1];
|
|
3026
1808
|
if (!secret) {
|
|
3027
1809
|
display.showWarning('That option is not available.');
|
|
3028
|
-
this.
|
|
1810
|
+
this.terminalInput.render();
|
|
3029
1811
|
return;
|
|
3030
1812
|
}
|
|
3031
1813
|
display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
|
|
3032
1814
|
this.pendingInteraction = { type: 'secret-input', secret };
|
|
3033
|
-
this.
|
|
1815
|
+
this.terminalInput.render();
|
|
3034
1816
|
}
|
|
3035
1817
|
async handleSecretInput(input) {
|
|
3036
1818
|
const pending = this.pendingInteraction;
|
|
@@ -3040,14 +1822,14 @@ export class InteractiveShell {
|
|
|
3040
1822
|
const trimmed = input.trim();
|
|
3041
1823
|
if (!trimmed) {
|
|
3042
1824
|
display.showWarning('Enter a value or type cancel.');
|
|
3043
|
-
this.
|
|
1825
|
+
this.terminalInput.render();
|
|
3044
1826
|
return;
|
|
3045
1827
|
}
|
|
3046
1828
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
3047
1829
|
this.pendingInteraction = null;
|
|
3048
1830
|
this.pendingSecretRetry = null;
|
|
3049
1831
|
display.showInfo('Secret unchanged.');
|
|
3050
|
-
this.
|
|
1832
|
+
this.terminalInput.render();
|
|
3051
1833
|
return;
|
|
3052
1834
|
}
|
|
3053
1835
|
try {
|
|
@@ -3071,7 +1853,7 @@ export class InteractiveShell {
|
|
|
3071
1853
|
this.pendingInteraction = null;
|
|
3072
1854
|
this.pendingSecretRetry = null;
|
|
3073
1855
|
}
|
|
3074
|
-
this.
|
|
1856
|
+
this.terminalInput.render();
|
|
3075
1857
|
}
|
|
3076
1858
|
async processRequest(request) {
|
|
3077
1859
|
if (this.isProcessing) {
|
|
@@ -3087,11 +1869,8 @@ export class InteractiveShell {
|
|
|
3087
1869
|
return;
|
|
3088
1870
|
}
|
|
3089
1871
|
this.isProcessing = true;
|
|
1872
|
+
this.terminalInput.setStreaming(true);
|
|
3090
1873
|
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
1874
|
this.uiAdapter.startProcessing('Working on your request');
|
|
3096
1875
|
this.setProcessingStatus();
|
|
3097
1876
|
try {
|
|
@@ -3100,13 +1879,6 @@ export class InteractiveShell {
|
|
|
3100
1879
|
// Claude Code style: Show unified streaming header before response
|
|
3101
1880
|
// This provides visual consistency with the startup Ready bar
|
|
3102
1881
|
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
1882
|
// DISABLED streaming - wait for full response instead
|
|
3111
1883
|
// This keeps cursor at the > prompt during processing
|
|
3112
1884
|
await agent.send(request, false);
|
|
@@ -3125,30 +1897,20 @@ export class InteractiveShell {
|
|
|
3125
1897
|
}
|
|
3126
1898
|
}
|
|
3127
1899
|
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
1900
|
display.stopThinking(false);
|
|
3138
1901
|
this.isProcessing = false;
|
|
3139
|
-
this.
|
|
1902
|
+
this.terminalInput.setStreaming(false);
|
|
3140
1903
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
3141
1904
|
this.setIdleStatus();
|
|
3142
1905
|
display.newLine();
|
|
3143
|
-
|
|
3144
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
1906
|
+
this.updateStatusMessage(null);
|
|
3145
1907
|
// Claude Code style: Show unified status bar before prompt
|
|
3146
1908
|
// This creates consistent UI between startup and post-streaming
|
|
3147
1909
|
this.showUnifiedStatusBar();
|
|
3148
1910
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
3149
1911
|
// Claude Code style: New prompt naturally appears at bottom
|
|
3150
1912
|
this.ensureReadlineReady();
|
|
3151
|
-
this.
|
|
1913
|
+
this.terminalInput.render();
|
|
3152
1914
|
this.scheduleQueueProcessing();
|
|
3153
1915
|
this.refreshQueueIndicators();
|
|
3154
1916
|
}
|
|
@@ -3180,26 +1942,17 @@ export class InteractiveShell {
|
|
|
3180
1942
|
return;
|
|
3181
1943
|
}
|
|
3182
1944
|
this.isProcessing = true;
|
|
1945
|
+
this.terminalInput.setStreaming(true);
|
|
3183
1946
|
const overallStartTime = Date.now();
|
|
3184
|
-
// Claude Code style: Enable scroll region for fixed chat box at bottom
|
|
3185
|
-
this.pinnedChatBox.setProcessing(true);
|
|
3186
1947
|
// Initialize the task completion detector
|
|
3187
1948
|
const completionDetector = getTaskCompletionDetector();
|
|
3188
1949
|
completionDetector.reset();
|
|
3189
|
-
// Claude Code style: Update status
|
|
3190
|
-
this.persistentPrompt.updateStatusBar({ message: '🔄 Continuous mode...' });
|
|
3191
1950
|
display.showSystemMessage(`🔄 Starting continuous execution mode. Press Ctrl+C to stop.`);
|
|
3192
1951
|
display.showSystemMessage(`📊 Using intelligent task completion detection with AI verification.`);
|
|
3193
1952
|
this.uiAdapter.startProcessing('Continuous execution mode');
|
|
3194
1953
|
this.setProcessingStatus();
|
|
3195
1954
|
// Claude Code style: Show unified streaming header before response
|
|
3196
1955
|
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
1956
|
let iteration = 0;
|
|
3204
1957
|
let lastResponse = '';
|
|
3205
1958
|
let consecutiveNoProgress = 0;
|
|
@@ -3225,9 +1978,7 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
|
|
|
3225
1978
|
while (iteration < MAX_ITERATIONS) {
|
|
3226
1979
|
iteration++;
|
|
3227
1980
|
display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
|
|
3228
|
-
|
|
3229
|
-
this.pinnedChatBox.setStatusMessage(`Working on iteration ${iteration}...`);
|
|
3230
|
-
this.pinnedChatBox.forceRender();
|
|
1981
|
+
this.updateStatusMessage(`Working on iteration ${iteration}...`);
|
|
3231
1982
|
try {
|
|
3232
1983
|
// Send the request and capture the response (streaming disabled)
|
|
3233
1984
|
const response = await agent.send(currentPrompt, false);
|
|
@@ -3361,28 +2112,18 @@ What's the next action?`;
|
|
|
3361
2112
|
display.showSystemMessage(`\n🏁 Continuous execution completed: ${iteration} iterations, ${minutes}m ${seconds}s total`);
|
|
3362
2113
|
// Reset completion detector for next task
|
|
3363
2114
|
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
2115
|
this.isProcessing = false;
|
|
3373
|
-
this.
|
|
2116
|
+
this.terminalInput.setStreaming(false);
|
|
2117
|
+
this.updateStatusMessage(null);
|
|
3374
2118
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
3375
2119
|
this.setIdleStatus();
|
|
3376
2120
|
display.newLine();
|
|
3377
|
-
// Clear the processing status
|
|
3378
|
-
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
3379
2121
|
// Claude Code style: Show unified status bar before prompt
|
|
3380
2122
|
// This creates consistent UI between startup and post-streaming
|
|
3381
2123
|
this.showUnifiedStatusBar();
|
|
3382
2124
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
3383
2125
|
// Claude Code style: New prompt naturally appears at bottom
|
|
3384
2126
|
this.ensureReadlineReady();
|
|
3385
|
-
this.rl.prompt();
|
|
3386
2127
|
this.scheduleQueueProcessing();
|
|
3387
2128
|
this.refreshQueueIndicators();
|
|
3388
2129
|
}
|
|
@@ -3590,8 +2331,7 @@ What's the next action?`;
|
|
|
3590
2331
|
this.autoTestInFlight = true;
|
|
3591
2332
|
const command = 'npm test -- --runInBand';
|
|
3592
2333
|
display.showSystemMessage(`🧪 Auto-testing recent changes (${trigger}) with "${command}"...`);
|
|
3593
|
-
this.
|
|
3594
|
-
this.pinnedChatBox.forceRender();
|
|
2334
|
+
this.updateStatusMessage('Running tests automatically...');
|
|
3595
2335
|
try {
|
|
3596
2336
|
const { stdout, stderr } = await execAsync(command, {
|
|
3597
2337
|
cwd: this.workingDir,
|
|
@@ -3619,8 +2359,7 @@ What's the next action?`;
|
|
|
3619
2359
|
});
|
|
3620
2360
|
}
|
|
3621
2361
|
finally {
|
|
3622
|
-
this.
|
|
3623
|
-
this.pinnedChatBox.forceRender();
|
|
2362
|
+
this.updateStatusMessage(null);
|
|
3624
2363
|
this.autoTestInFlight = false;
|
|
3625
2364
|
}
|
|
3626
2365
|
}
|
|
@@ -3691,7 +2430,7 @@ What's the next action?`;
|
|
|
3691
2430
|
display.showNarrative(content.trim());
|
|
3692
2431
|
}
|
|
3693
2432
|
// The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
|
|
3694
|
-
this.
|
|
2433
|
+
this.terminalInput.render();
|
|
3695
2434
|
return;
|
|
3696
2435
|
}
|
|
3697
2436
|
const cleanup = this.handleContextTelemetry(metadata, enriched);
|
|
@@ -3710,10 +2449,6 @@ What's the next action?`;
|
|
|
3710
2449
|
onContextRecovery: (attempt, maxAttempts, message) => {
|
|
3711
2450
|
// Show recovery progress in UI
|
|
3712
2451
|
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
2452
|
},
|
|
3718
2453
|
onContextPruned: (removedCount, stats) => {
|
|
3719
2454
|
// Clear squish overlay if active
|
|
@@ -3727,30 +2462,28 @@ What's the next action?`;
|
|
|
3727
2462
|
// Update context usage in UI
|
|
3728
2463
|
if (typeof percentage === 'number') {
|
|
3729
2464
|
this.uiAdapter.updateContextUsage(percentage);
|
|
3730
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentage });
|
|
3731
2465
|
}
|
|
3732
2466
|
// Ensure prompt remains visible at bottom after context messages
|
|
3733
|
-
this.
|
|
2467
|
+
this.terminalInput.render();
|
|
3734
2468
|
},
|
|
3735
2469
|
onContinueAfterRecovery: () => {
|
|
3736
2470
|
// Update UI to show we're continuing after context recovery
|
|
3737
2471
|
display.showSystemMessage(`🔄 Continuing after context recovery...`);
|
|
3738
|
-
|
|
3739
|
-
this.
|
|
3740
|
-
this.pinnedChatBox.forceRender();
|
|
2472
|
+
this.updateStatusMessage('Retrying with reduced context...');
|
|
2473
|
+
this.terminalInput.render();
|
|
3741
2474
|
},
|
|
3742
2475
|
onAutoContinue: (attempt, maxAttempts, _message) => {
|
|
3743
2476
|
// Show auto-continue progress in UI
|
|
3744
2477
|
display.showSystemMessage(`🔄 Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
|
|
3745
|
-
this.
|
|
3746
|
-
this.
|
|
2478
|
+
this.updateStatusMessage('Auto-continuing...');
|
|
2479
|
+
this.terminalInput.render();
|
|
3747
2480
|
},
|
|
3748
2481
|
onCancelled: () => {
|
|
3749
2482
|
// Update UI to show operation was cancelled
|
|
3750
2483
|
display.showWarning('Operation cancelled.');
|
|
3751
|
-
this.
|
|
3752
|
-
this.
|
|
3753
|
-
this.
|
|
2484
|
+
this.updateStatusMessage(null);
|
|
2485
|
+
this.terminalInput.setStreaming(false);
|
|
2486
|
+
this.terminalInput.render();
|
|
3754
2487
|
},
|
|
3755
2488
|
onVerificationNeeded: () => {
|
|
3756
2489
|
void this.enforceAutoTests('verification');
|
|
@@ -3784,10 +2517,9 @@ What's the next action?`;
|
|
|
3784
2517
|
* just like on fresh startup.
|
|
3785
2518
|
*/
|
|
3786
2519
|
resetChatBoxAfterModelSwap() {
|
|
3787
|
-
this.
|
|
3788
|
-
this.
|
|
3789
|
-
this.
|
|
3790
|
-
this.pinnedChatBox.forceRender();
|
|
2520
|
+
this.updateStatusMessage(null);
|
|
2521
|
+
this.terminalInput.setStreaming(false);
|
|
2522
|
+
this.terminalInput.render();
|
|
3791
2523
|
this.ensureReadlineReady();
|
|
3792
2524
|
}
|
|
3793
2525
|
buildSystemPrompt() {
|
|
@@ -3854,10 +2586,6 @@ What's the next action?`;
|
|
|
3854
2586
|
// Always update context usage in the UI
|
|
3855
2587
|
const percentUsed = Math.round(usageRatio * 100);
|
|
3856
2588
|
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
2589
|
if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
|
|
3862
2590
|
return null;
|
|
3863
2591
|
}
|
|
@@ -3905,8 +2633,6 @@ What's the next action?`;
|
|
|
3905
2633
|
const percentUsed = Math.round((totalTokens / windowTokens) * 100);
|
|
3906
2634
|
// Update context usage in unified UI
|
|
3907
2635
|
this.uiAdapter.updateContextUsage(percentUsed);
|
|
3908
|
-
// Update persistent prompt status bar with context usage
|
|
3909
|
-
this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
|
|
3910
2636
|
display.showSystemMessage([
|
|
3911
2637
|
`Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
|
|
3912
2638
|
`(${percentUsed}% full). Running automatic cleanup...`,
|
|
@@ -4143,14 +2869,6 @@ What's the next action?`;
|
|
|
4143
2869
|
const entry = this.agentMenu.options.find((option) => option.name === name);
|
|
4144
2870
|
return entry?.label ?? name;
|
|
4145
2871
|
}
|
|
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
2872
|
extractThoughtSummary(thought) {
|
|
4155
2873
|
// Extract first non-empty line
|
|
4156
2874
|
const lines = thought?.split('\n').filter(line => line.trim()) ?? [];
|
|
@@ -4440,32 +3158,6 @@ What's the next action?`;
|
|
|
4440
3158
|
];
|
|
4441
3159
|
display.showSystemMessage(lines.join('\n'));
|
|
4442
3160
|
}
|
|
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
3161
|
/**
|
|
4470
3162
|
* Set the cached provider status for unified status bar display.
|
|
4471
3163
|
* Called once at startup after checking providers.
|
|
@@ -4473,10 +3165,6 @@ What's the next action?`;
|
|
|
4473
3165
|
setProviderStatus(providers) {
|
|
4474
3166
|
this.cachedProviderStatus = providers;
|
|
4475
3167
|
}
|
|
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
3168
|
/**
|
|
4481
3169
|
* Show the unified status bar (Claude Code style).
|
|
4482
3170
|
* Displays provider indicators and ready hints before the prompt.
|