erosolar-cli 1.7.14 → 1.7.16

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.
Files changed (61) hide show
  1. package/dist/core/responseVerifier.d.ts +79 -0
  2. package/dist/core/responseVerifier.d.ts.map +1 -0
  3. package/dist/core/responseVerifier.js +443 -0
  4. package/dist/core/responseVerifier.js.map +1 -0
  5. package/dist/shell/interactiveShell.d.ts +10 -0
  6. package/dist/shell/interactiveShell.d.ts.map +1 -1
  7. package/dist/shell/interactiveShell.js +80 -0
  8. package/dist/shell/interactiveShell.js.map +1 -1
  9. package/dist/ui/ShellUIAdapter.d.ts +3 -0
  10. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  11. package/dist/ui/ShellUIAdapter.js +4 -10
  12. package/dist/ui/ShellUIAdapter.js.map +1 -1
  13. package/dist/ui/persistentPrompt.d.ts +4 -0
  14. package/dist/ui/persistentPrompt.d.ts.map +1 -1
  15. package/dist/ui/persistentPrompt.js +10 -11
  16. package/dist/ui/persistentPrompt.js.map +1 -1
  17. package/package.json +1 -1
  18. package/dist/bin/core/agent.js +0 -362
  19. package/dist/bin/core/agentProfileManifest.js +0 -187
  20. package/dist/bin/core/agentProfiles.js +0 -34
  21. package/dist/bin/core/agentRulebook.js +0 -135
  22. package/dist/bin/core/agentSchemaLoader.js +0 -233
  23. package/dist/bin/core/contextManager.js +0 -412
  24. package/dist/bin/core/contextWindow.js +0 -122
  25. package/dist/bin/core/customCommands.js +0 -80
  26. package/dist/bin/core/errors/apiKeyErrors.js +0 -114
  27. package/dist/bin/core/errors/errorTypes.js +0 -340
  28. package/dist/bin/core/errors/safetyValidator.js +0 -304
  29. package/dist/bin/core/errors.js +0 -32
  30. package/dist/bin/core/modelDiscovery.js +0 -755
  31. package/dist/bin/core/preferences.js +0 -224
  32. package/dist/bin/core/schemaValidator.js +0 -92
  33. package/dist/bin/core/secretStore.js +0 -199
  34. package/dist/bin/core/sessionStore.js +0 -187
  35. package/dist/bin/core/toolRuntime.js +0 -290
  36. package/dist/bin/core/types.js +0 -1
  37. package/dist/bin/shell/bracketedPasteManager.js +0 -350
  38. package/dist/bin/shell/fileChangeTracker.js +0 -65
  39. package/dist/bin/shell/interactiveShell.js +0 -2908
  40. package/dist/bin/shell/liveStatus.js +0 -78
  41. package/dist/bin/shell/shellApp.js +0 -290
  42. package/dist/bin/shell/systemPrompt.js +0 -60
  43. package/dist/bin/shell/updateManager.js +0 -108
  44. package/dist/bin/ui/ShellUIAdapter.js +0 -459
  45. package/dist/bin/ui/UnifiedUIController.js +0 -183
  46. package/dist/bin/ui/animation/AnimationScheduler.js +0 -430
  47. package/dist/bin/ui/codeHighlighter.js +0 -854
  48. package/dist/bin/ui/designSystem.js +0 -121
  49. package/dist/bin/ui/display.js +0 -1222
  50. package/dist/bin/ui/interrupts/InterruptManager.js +0 -437
  51. package/dist/bin/ui/layout.js +0 -139
  52. package/dist/bin/ui/orchestration/StatusOrchestrator.js +0 -403
  53. package/dist/bin/ui/outputMode.js +0 -38
  54. package/dist/bin/ui/persistentPrompt.js +0 -183
  55. package/dist/bin/ui/richText.js +0 -338
  56. package/dist/bin/ui/shortcutsHelp.js +0 -87
  57. package/dist/bin/ui/telemetry/UITelemetry.js +0 -443
  58. package/dist/bin/ui/textHighlighter.js +0 -210
  59. package/dist/bin/ui/theme.js +0 -116
  60. package/dist/bin/ui/toolDisplay.js +0 -423
  61. package/dist/bin/ui/toolDisplayAdapter.js +0 -357
@@ -1,2908 +0,0 @@
1
- import readline from 'node:readline';
2
- import { stdin as input, stdout as output, exit } from 'node:process';
3
- import { display } from '../ui/display.js';
4
- import { formatUserPrompt, theme } from '../ui/theme.js';
5
- import { getContextWindowTokens } from '../core/contextWindow.js';
6
- import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
7
- import { saveActiveProfilePreference, saveModelPreference, loadToolSettings, saveToolSettings, clearToolSettings, clearActiveProfilePreference, loadSessionPreferences, saveSessionPreferences, } from '../core/preferences.js';
8
- import { buildEnabledToolSet, evaluateToolPermissions, getToolToggleOptions, } from '../capabilities/toolRegistry.js';
9
- import { BracketedPasteManager } from './bracketedPasteManager.js';
10
- import { detectApiKeyError } from '../core/errors/apiKeyErrors.js';
11
- import { buildWorkspaceContext } from '../workspace.js';
12
- import { buildInteractiveSystemPrompt } from './systemPrompt.js';
13
- import { discoverAllModels } from '../core/modelDiscovery.js';
14
- import { getModels, getSlashCommands, getProviders } from '../core/agentSchemaLoader.js';
15
- import { clearAutosaveSnapshot, deleteSession, listSessions, loadAutosaveSnapshot, loadSessionById, saveAutosaveSnapshot, saveSessionSnapshot, } from '../core/sessionStore.js';
16
- import { buildCustomCommandPrompt, loadCustomSlashCommands, } from '../core/customCommands.js';
17
- import { SkillRepository } from '../skills/skillRepository.js';
18
- import { createSkillTools } from '../tools/skillTools.js';
19
- import { FileChangeTracker } from './fileChangeTracker.js';
20
- import { PersistentPrompt } from '../ui/persistentPrompt.js';
21
- import { formatShortcutsHelp } from '../ui/shortcutsHelp.js';
22
- import { MetricsTracker } from '../alpha-zero/index.js';
23
- import { listAvailablePlugins } from '../plugins/index.js';
24
- const DROPDOWN_COLORS = [
25
- theme.primary,
26
- theme.info,
27
- theme.accent,
28
- theme.secondary,
29
- theme.success,
30
- theme.warning,
31
- ];
32
- // Load MODEL_PRESETS from centralized schema
33
- const MODEL_PRESETS = getModels().map((model) => ({
34
- id: model.id,
35
- label: model.label,
36
- provider: model.provider,
37
- description: model.description ?? '',
38
- reasoningEffort: model.reasoningEffort,
39
- temperature: model.temperature,
40
- maxTokens: model.maxTokens,
41
- }));
42
- // Load BASE_SLASH_COMMANDS from centralized schema
43
- const BASE_SLASH_COMMANDS = getSlashCommands().map((cmd) => ({
44
- command: cmd.command,
45
- description: cmd.description,
46
- }));
47
- // Load PROVIDER_LABELS from centralized schema
48
- const PROVIDER_LABELS = Object.fromEntries(getProviders().map((provider) => [provider.id, provider.label]));
49
- const MULTILINE_INPUT_FLUSH_DELAY_MS = 30;
50
- const BRACKETED_PASTE_ENABLE = '\u001b[?2004h';
51
- const BRACKETED_PASTE_DISABLE = '\u001b[?2004l';
52
- const CONTEXT_USAGE_THRESHOLD = 0.9;
53
- const CONTEXT_RECENT_MESSAGE_COUNT = 12;
54
- const CONTEXT_CLEANUP_CHARS_PER_CHUNK = 6000;
55
- const CONTEXT_CLEANUP_MAX_OUTPUT_TOKENS = 800;
56
- const CONTEXT_CLEANUP_SYSTEM_PROMPT = `You condense earlier IDE collaboration logs so the agent can keep working.
57
- - Merge any prior summary with the new conversation chunk.
58
- - Capture key decisions, TODOs, file edits, tool observations, and open questions.
59
- - Clearly distinguish resolved work from outstanding follow-ups.
60
- - Keep the response under roughly 200 words, prefer short bullet lists.
61
- - Never call tools or run shell commands; respond with plain Markdown text only.`;
62
- export class InteractiveShell {
63
- constructor(config) {
64
- this.agent = null;
65
- this.isProcessing = false;
66
- this.pendingInteraction = null;
67
- this.pendingSecretRetry = null;
68
- this.bufferedInputLines = [];
69
- this.bufferedInputTimer = null;
70
- this.bracketedPasteEnabled = false;
71
- this.pendingCleanup = null;
72
- this.cleanupInProgress = false;
73
- this.slashPreviewVisible = false;
74
- this.keypressHandler = null;
75
- this.rawDataHandler = null;
76
- this.skillToolHandlers = new Map();
77
- this.thinkingMode = 'balanced';
78
- this.bannerSessionState = null;
79
- this._fileChangeTracker = new FileChangeTracker(); // Reserved for future file tracking features
80
- this.statusSubscription = null;
81
- this.followUpQueue = [];
82
- this.isDrainingQueue = false;
83
- this.activeContextWindowTokens = null;
84
- this.pendingHistoryLoad = null;
85
- this.cachedHistory = [];
86
- this.activeSessionId = null;
87
- this.activeSessionTitle = null;
88
- this.sessionResumeNotice = null;
89
- this.profile = config.profile;
90
- this.profileLabel = config.profileLabel;
91
- this.workingDir = config.workingDir;
92
- this.runtimeSession = config.session;
93
- this.baseSystemPrompt = config.baseSystemPrompt;
94
- this.workspaceOptions = { ...config.workspaceOptions };
95
- this.sessionPreferences = loadSessionPreferences();
96
- this.thinkingMode = this.sessionPreferences.thinkingMode;
97
- this.autosaveEnabled = this.sessionPreferences.autosave;
98
- this.sessionRestoreConfig = config.sessionRestore ?? { mode: 'none' };
99
- this._enabledPlugins = config.enabledPlugins ?? [];
100
- this.initializeSessionHistory();
101
- this.sessionState = {
102
- provider: config.initialModel.provider,
103
- model: config.initialModel.model,
104
- temperature: config.initialModel.temperature,
105
- maxTokens: config.initialModel.maxTokens,
106
- reasoningEffort: config.initialModel.reasoningEffort,
107
- };
108
- this.applyPresetReasoningDefaults();
109
- // The welcome banner only includes model + provider on launch, so mark that as the initial state.
110
- this.bannerSessionState = {
111
- model: this.sessionState.model,
112
- provider: this.sessionState.provider,
113
- };
114
- this.agentMenu = config.agentSelection ?? null;
115
- this.slashCommands = [...BASE_SLASH_COMMANDS];
116
- if (this.agentMenu) {
117
- this.slashCommands.push({
118
- command: '/agents',
119
- description: 'Select the default agent profile (applies on next launch)',
120
- });
121
- }
122
- this.customCommands = loadCustomSlashCommands();
123
- this.customCommandMap = new Map(this.customCommands.map((command) => [command.command, command]));
124
- for (const custom of this.customCommands) {
125
- this.slashCommands.push({
126
- command: custom.command,
127
- description: `${custom.description} (custom)`,
128
- });
129
- }
130
- // Add /plugins command
131
- this.slashCommands.push({
132
- command: '/plugins',
133
- description: 'Show available and loaded plugins',
134
- });
135
- this.statusTracker = config.statusTracker;
136
- this.uiAdapter = config.uiAdapter;
137
- // Set up file change tracking callback
138
- this.uiAdapter.setFileChangeCallback((path, type, additions, removals) => {
139
- this._fileChangeTracker.recordChange(path, type, additions, removals);
140
- // Update persistent prompt status bar with file changes
141
- this.updatePersistentPromptFileChanges();
142
- });
143
- this.skillRepository = new SkillRepository({
144
- workingDir: this.workingDir,
145
- env: process.env,
146
- });
147
- for (const definition of createSkillTools({ repository: this.skillRepository })) {
148
- this.skillToolHandlers.set(definition.name, definition.handler);
149
- }
150
- this.rl = readline.createInterface({
151
- input,
152
- output,
153
- prompt: formatUserPrompt(this.profileLabel || this.profile),
154
- terminal: true,
155
- });
156
- // Wrap prompt() so we always re-arm stdin before showing it
157
- const originalPrompt = this.rl.prompt.bind(this.rl);
158
- this.rl.prompt = (preserveCursor) => {
159
- this.ensureReadlineReady();
160
- originalPrompt(preserveCursor);
161
- };
162
- // Initialize persistent prompt (Claude Code style)
163
- this.persistentPrompt = new PersistentPrompt(output, formatUserPrompt(this.profileLabel || this.profile));
164
- // Initialize Alpha Zero 2 metrics tracking
165
- this.alphaZeroMetrics = new MetricsTracker(`${this.profile}-${Date.now()}`);
166
- this.setupStatusTracking();
167
- this.refreshContextGauge();
168
- this.bracketedPasteEnabled = this.enableBracketedPasteMode();
169
- this.bracketedPaste = new BracketedPasteManager(this.bracketedPasteEnabled);
170
- this.rebuildAgent();
171
- this.setupHandlers();
172
- this.refreshBannerSessionInfo();
173
- }
174
- initializeSessionHistory() {
175
- this.cachedHistory = [];
176
- this.pendingHistoryLoad = null;
177
- this.activeSessionId = null;
178
- this.activeSessionTitle = null;
179
- this.sessionResumeNotice = null;
180
- // Handle explicit session restore requests via CLI flags
181
- if (this.sessionRestoreConfig.mode === 'session-id' && this.sessionRestoreConfig.sessionId) {
182
- const stored = loadSessionById(this.sessionRestoreConfig.sessionId);
183
- if (stored) {
184
- this.cachedHistory = stored.messages;
185
- this.pendingHistoryLoad = stored.messages;
186
- this.activeSessionId = stored.id;
187
- this.activeSessionTitle = stored.title;
188
- this.sessionResumeNotice = `Resumed session "${stored.title}".`;
189
- return;
190
- }
191
- display.showWarning(`Session "${this.sessionRestoreConfig.sessionId}" not found. Starting fresh session.`);
192
- return;
193
- }
194
- if (this.sessionRestoreConfig.mode === 'autosave') {
195
- const autosave = loadAutosaveSnapshot(this.profile);
196
- if (autosave) {
197
- this.cachedHistory = autosave.messages;
198
- this.pendingHistoryLoad = autosave.messages;
199
- this.activeSessionId = null;
200
- this.activeSessionTitle = autosave.title;
201
- this.sessionResumeNotice = 'Restored last autosaved session.';
202
- return;
203
- }
204
- display.showWarning('No autosaved session found. Starting fresh session.');
205
- }
206
- // Default: Start fresh (mode === 'none')
207
- }
208
- showSessionResumeNotice() {
209
- if (!this.sessionResumeNotice) {
210
- return;
211
- }
212
- display.showInfo(this.sessionResumeNotice);
213
- this.sessionResumeNotice = null;
214
- }
215
- async start(initialPrompt) {
216
- if (initialPrompt) {
217
- display.newLine();
218
- console.log(`${formatUserPrompt(this.profileLabel || this.profile)}${initialPrompt}`);
219
- await this.processInputBlock(initialPrompt);
220
- return;
221
- }
222
- this.rl.prompt();
223
- }
224
- async handleToolSettingsInput(input) {
225
- const pending = this.pendingInteraction;
226
- if (!pending || pending.type !== 'tool-settings') {
227
- return;
228
- }
229
- const trimmed = input.trim();
230
- if (!trimmed) {
231
- display.showWarning('Enter a number, "save", "defaults", or "cancel".');
232
- this.rl.prompt();
233
- return;
234
- }
235
- const normalized = trimmed.toLowerCase();
236
- if (normalized === 'cancel') {
237
- this.pendingInteraction = null;
238
- display.showInfo('Tool selection cancelled.');
239
- this.rl.prompt();
240
- return;
241
- }
242
- if (normalized === 'defaults') {
243
- pending.selection = buildEnabledToolSet(null);
244
- this.renderToolMenu(pending);
245
- this.rl.prompt();
246
- return;
247
- }
248
- if (normalized === 'save') {
249
- await this.persistToolSelection(pending);
250
- this.pendingInteraction = null;
251
- this.rl.prompt();
252
- return;
253
- }
254
- const choice = Number.parseInt(trimmed, 10);
255
- if (Number.isFinite(choice)) {
256
- const option = pending.options[choice - 1];
257
- if (!option) {
258
- display.showWarning('That option is not available.');
259
- }
260
- else {
261
- if (pending.selection.has(option.id)) {
262
- pending.selection.delete(option.id);
263
- }
264
- else {
265
- pending.selection.add(option.id);
266
- }
267
- this.renderToolMenu(pending);
268
- }
269
- this.rl.prompt();
270
- return;
271
- }
272
- display.showWarning('Enter a number, "save", "defaults", or "cancel".');
273
- this.rl.prompt();
274
- }
275
- async persistToolSelection(interaction) {
276
- if (setsEqual(interaction.selection, interaction.initialSelection)) {
277
- display.showInfo('No changes to save.');
278
- return;
279
- }
280
- const defaults = buildEnabledToolSet(null);
281
- if (setsEqual(interaction.selection, defaults)) {
282
- clearToolSettings();
283
- display.showInfo('Tool settings cleared. Defaults will be used on the next launch.');
284
- return;
285
- }
286
- const ordered = interaction.options
287
- .map((option) => option.id)
288
- .filter((id) => interaction.selection.has(id));
289
- saveToolSettings({ enabledTools: ordered });
290
- display.showInfo('Tool settings saved. Restart the CLI to apply them.');
291
- }
292
- async handleAgentSelectionInput(input) {
293
- const pending = this.pendingInteraction;
294
- if (!pending || pending.type !== 'agent-selection') {
295
- return;
296
- }
297
- if (!this.agentMenu) {
298
- this.pendingInteraction = null;
299
- display.showWarning('Agent selection is unavailable in this CLI.');
300
- this.rl.prompt();
301
- return;
302
- }
303
- const trimmed = input.trim();
304
- if (!trimmed) {
305
- display.showWarning('Enter a number or type "cancel".');
306
- this.rl.prompt();
307
- return;
308
- }
309
- if (trimmed.toLowerCase() === 'cancel') {
310
- this.pendingInteraction = null;
311
- display.showInfo('Agent selection cancelled.');
312
- this.rl.prompt();
313
- return;
314
- }
315
- const choice = Number.parseInt(trimmed, 10);
316
- if (!Number.isFinite(choice)) {
317
- display.showWarning('Please enter a valid number.');
318
- this.rl.prompt();
319
- return;
320
- }
321
- const option = pending.options[choice - 1];
322
- if (!option) {
323
- display.showWarning('That option is not available.');
324
- this.rl.prompt();
325
- return;
326
- }
327
- await this.persistAgentSelection(option.name);
328
- this.pendingInteraction = null;
329
- this.rl.prompt();
330
- }
331
- async persistAgentSelection(profileName) {
332
- if (!this.agentMenu) {
333
- return;
334
- }
335
- const currentDefault = this.agentMenu.persistedProfile ?? this.agentMenu.defaultProfile;
336
- if (profileName === currentDefault) {
337
- display.showInfo(`${this.agentMenuLabel(profileName)} is already configured for the next launch.`);
338
- return;
339
- }
340
- if (profileName === this.agentMenu.defaultProfile) {
341
- clearActiveProfilePreference();
342
- this.agentMenu.persistedProfile = null;
343
- display.showInfo(`${this.agentMenuLabel(profileName)} restored as the default agent. Restart the CLI to switch.`);
344
- return;
345
- }
346
- saveActiveProfilePreference(profileName);
347
- this.agentMenu.persistedProfile = profileName;
348
- display.showInfo(`${this.agentMenuLabel(profileName)} will load the next time you start the CLI. Restart to switch now.`);
349
- }
350
- setupHandlers() {
351
- // Set up raw data interception for bracketed paste
352
- this.setupRawPasteHandler();
353
- this.rl.on('line', (line) => {
354
- // If we're capturing raw paste data, ignore readline line events
355
- // (they've already been handled by the raw data handler)
356
- if (this.bracketedPaste.isCapturingRaw()) {
357
- // Show paste progress
358
- this.showMultiLinePastePreview(this.bracketedPaste.getRawBufferLineCount(), this.bracketedPaste.getRawBufferPreview());
359
- return;
360
- }
361
- const normalized = this.bracketedPaste.process(line);
362
- if (normalized.handled) {
363
- // If still accumulating multi-line paste, show preview
364
- if (normalized.isPending) {
365
- this.showMultiLinePastePreview(normalized.lineCount || 0, normalized.preview);
366
- return;
367
- }
368
- // Paste complete, submit the full content
369
- if (typeof normalized.result === 'string') {
370
- this.clearMultiLinePastePreview();
371
- // Show collapsed summary of what was submitted
372
- if (normalized.lineCount && normalized.lineCount > 1) {
373
- this.displayMultiLineSubmission(normalized.result, normalized.lineCount);
374
- }
375
- this.enqueueUserInput(normalized.result, true);
376
- }
377
- return;
378
- }
379
- this.enqueueUserInput(line);
380
- // Clear input in persistent prompt after submission
381
- this.persistentPrompt.updateInput('', 0);
382
- });
383
- this.rl.on('close', () => {
384
- // Stop any active spinner to prevent process hang
385
- display.stopThinking(false);
386
- this.disableBracketedPasteMode();
387
- this.teardownStatusTracking();
388
- // Remove keypress listener to prevent memory leaks
389
- const inputStream = input;
390
- if (inputStream && this.keypressHandler) {
391
- inputStream.off('keypress', this.keypressHandler);
392
- this.keypressHandler = null;
393
- }
394
- // Remove raw data handler
395
- if (inputStream && this.rawDataHandler) {
396
- inputStream.off('data', this.rawDataHandler);
397
- this.rawDataHandler = null;
398
- }
399
- // Clear any pending cleanup to prevent hanging
400
- this.pendingCleanup = null;
401
- // Dispose persistent prompt
402
- this.persistentPrompt.dispose();
403
- // Dispose unified UI adapter
404
- this.uiAdapter.dispose();
405
- display.newLine();
406
- const highlightedEmail = theme.info('support@ero.solar');
407
- const infoMessage = [
408
- 'Made available for defensive and offensive security operations.',
409
- '',
410
- 'Learn more:',
411
- 'https://www.anthropic.com/news/disrupting-AI-espionage',
412
- '',
413
- `Email ${highlightedEmail} with any bugs or feedback`,
414
- 'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
415
- 'npm: https://www.npmjs.com/package/erosolar-cli',
416
- ].join('\n');
417
- display.showInfo(infoMessage);
418
- exit(0);
419
- });
420
- // Handle terminal resize
421
- output.on('resize', () => {
422
- this.persistentPrompt.handleResize();
423
- });
424
- this.setupSlashCommandPreviewHandler();
425
- // Show initial persistent prompt
426
- this.persistentPrompt.show();
427
- }
428
- /**
429
- * Set up raw stdin data interception for bracketed paste mode.
430
- * This intercepts data before readline processes it, allowing us to
431
- * capture complete multi-line pastes without readline splitting them.
432
- */
433
- setupRawPasteHandler() {
434
- if (!this.bracketedPasteEnabled) {
435
- return;
436
- }
437
- const inputStream = input;
438
- if (!inputStream || !inputStream.isTTY) {
439
- return;
440
- }
441
- // Set up callback for when a complete paste is captured
442
- this.bracketedPaste.setRawPasteCallback((content) => {
443
- this.clearMultiLinePastePreview();
444
- const lines = content.split('\n');
445
- const lineCount = lines.length;
446
- // Show collapsed summary for multi-line pastes
447
- if (lineCount > 1) {
448
- this.displayMultiLineSubmission(content, lineCount);
449
- }
450
- // Submit the pasted content
451
- this.enqueueUserInput(content, true);
452
- // Restore the prompt
453
- this.persistentPrompt.updateInput('', 0);
454
- this.rl.prompt();
455
- });
456
- // We need to intercept raw data, but we can't easily do this with
457
- // readline already consuming stdin. Instead, we handle paste detection
458
- // via the bracketed paste markers that appear in line events.
459
- // The raw handler approach works better when we control the input stream.
460
- //
461
- // For now, we rely on the line-event-based approach with enhanced
462
- // detection in the BracketedPasteManager.
463
- }
464
- setupSlashCommandPreviewHandler() {
465
- const inputStream = input;
466
- if (!inputStream || typeof inputStream.on !== 'function' || !inputStream.isTTY) {
467
- return;
468
- }
469
- if (inputStream.listenerCount('keypress') === 0) {
470
- readline.emitKeypressEvents(inputStream, this.rl);
471
- }
472
- this.keypressHandler = (_str, key) => {
473
- // Update persistent prompt with current input
474
- const currentLine = this.rl.line || '';
475
- const cursorPos = this.rl.cursor || 0;
476
- this.persistentPrompt.updateInput(currentLine, cursorPos);
477
- this.handleSlashCommandPreviewChange();
478
- // Handle Shift+Tab for profile switching
479
- if (key && key.name === 'tab' && key.shift && this.agentMenu) {
480
- this.showProfileSwitcher();
481
- }
482
- };
483
- inputStream.on('keypress', this.keypressHandler);
484
- }
485
- setupStatusTracking() {
486
- this.statusSubscription = this.statusTracker.subscribe((_state) => {
487
- // Status tracking callback - currently no-op
488
- });
489
- this.setIdleStatus();
490
- }
491
- teardownStatusTracking() {
492
- if (this.statusSubscription) {
493
- this.statusSubscription();
494
- this.statusSubscription = null;
495
- }
496
- this.statusTracker.reset();
497
- }
498
- setIdleStatus(detail) {
499
- this.statusTracker.setBase('Ready for prompts', {
500
- detail: this.describeStatusDetail(detail),
501
- tone: 'success',
502
- });
503
- }
504
- setProcessingStatus(detail) {
505
- this.statusTracker.setBase('Working on your request', {
506
- detail: this.describeStatusDetail(detail),
507
- tone: 'info',
508
- });
509
- }
510
- describeStatusDetail(detail) {
511
- const parts = [detail?.trim() || this.describeModelDetail()];
512
- const queued = this.followUpQueue.length;
513
- if (queued > 0) {
514
- parts.push(`${queued} follow-up${queued === 1 ? '' : 's'} queued`);
515
- }
516
- return parts.join(' • ');
517
- }
518
- describeModelDetail() {
519
- const provider = this.providerLabel(this.sessionState.provider);
520
- return `${provider} · ${this.sessionState.model}`;
521
- }
522
- refreshContextGauge() {
523
- const tokens = getContextWindowTokens(this.sessionState.model);
524
- this.activeContextWindowTokens =
525
- typeof tokens === 'number' && Number.isFinite(tokens) ? tokens : null;
526
- }
527
- handleSlashCommandPreviewChange() {
528
- if (this.pendingInteraction) {
529
- this.slashPreviewVisible = false;
530
- return;
531
- }
532
- const shouldShow = this.shouldShowSlashCommandPreview();
533
- if (shouldShow && !this.slashPreviewVisible) {
534
- this.slashPreviewVisible = true;
535
- this.showSlashCommandPreview();
536
- return;
537
- }
538
- if (!shouldShow && this.slashPreviewVisible) {
539
- this.slashPreviewVisible = false;
540
- this.uiAdapter.hideSlashCommandPreview();
541
- // Show persistent prompt again after hiding overlay
542
- if (!this.isProcessing) {
543
- this.persistentPrompt.show();
544
- }
545
- }
546
- }
547
- shouldShowSlashCommandPreview() {
548
- const line = this.rl.line ?? '';
549
- if (!line.trim()) {
550
- return false;
551
- }
552
- const trimmed = line.trimStart();
553
- return trimmed.startsWith('/');
554
- }
555
- showSlashCommandPreview() {
556
- // Filter commands based on current input
557
- const line = this.rl.line ?? '';
558
- const trimmed = line.trimStart();
559
- // Filter commands that match the current input
560
- const filtered = this.slashCommands.filter(cmd => cmd.command.startsWith(trimmed) || trimmed === '/');
561
- // Hide persistent prompt to avoid conflicts with overlay
562
- this.persistentPrompt.hide();
563
- // Show in the unified UI with dynamic overlay
564
- this.uiAdapter.showSlashCommandPreview(filtered);
565
- // Don't reprompt - this causes flickering
566
- }
567
- showProfileSwitcher() {
568
- if (!this.agentMenu) {
569
- return;
570
- }
571
- // Build profile options with current/next indicators
572
- const profiles = this.agentMenu.options.map((option, index) => {
573
- const badges = [];
574
- const nextProfile = this.agentMenu.persistedProfile ?? this.agentMenu.defaultProfile;
575
- if (option.name === this.profile) {
576
- badges.push('current');
577
- }
578
- if (option.name === nextProfile && option.name !== this.profile) {
579
- badges.push('next');
580
- }
581
- const badgeText = badges.length > 0 ? ` (${badges.join(', ')})` : '';
582
- return {
583
- command: `${index + 1}. ${option.label}${badgeText}`,
584
- description: `${this.providerLabel(option.defaultProvider)} • ${option.defaultModel}`,
585
- };
586
- });
587
- // Show profile switcher overlay
588
- this.uiAdapter.showProfileSwitcher(profiles, this.profileLabel);
589
- }
590
- /**
591
- * Ensure stdin is ready for interactive input before showing the prompt.
592
- * This recovers from cases where a nested readline/prompt or partial paste
593
- * left the stream paused, raw mode disabled, or keypress listeners detached.
594
- */
595
- ensureReadlineReady() {
596
- // Clear any stuck bracketed paste state so new input isn't dropped
597
- this.bracketedPaste.reset();
598
- const inputStream = input;
599
- if (!inputStream) {
600
- return;
601
- }
602
- // Resume stdin if another consumer paused it
603
- if (typeof inputStream.isPaused === 'function' && inputStream.isPaused()) {
604
- inputStream.resume();
605
- }
606
- else if (inputStream.readableFlowing === false) {
607
- inputStream.resume();
608
- }
609
- // Restore raw mode if a nested readline turned it off
610
- if (inputStream.isTTY && typeof inputStream.isRaw === 'boolean') {
611
- const ttyStream = inputStream;
612
- if (!ttyStream.isRaw) {
613
- ttyStream.setRawMode(true);
614
- }
615
- }
616
- // Reattach keypress handler if it was removed
617
- if (this.keypressHandler) {
618
- const listeners = inputStream.listeners('keypress');
619
- const hasHandler = listeners.includes(this.keypressHandler);
620
- if (!hasHandler) {
621
- if (inputStream.listenerCount('keypress') === 0) {
622
- readline.emitKeypressEvents(inputStream, this.rl);
623
- }
624
- inputStream.on('keypress', this.keypressHandler);
625
- }
626
- }
627
- }
628
- enqueueUserInput(line, flushImmediately = false) {
629
- this.bufferedInputLines.push(line);
630
- if (flushImmediately) {
631
- if (this.bufferedInputTimer) {
632
- clearTimeout(this.bufferedInputTimer);
633
- this.bufferedInputTimer = null;
634
- }
635
- void this.flushBufferedInput();
636
- return;
637
- }
638
- if (this.bufferedInputTimer) {
639
- clearTimeout(this.bufferedInputTimer);
640
- }
641
- this.bufferedInputTimer = setTimeout(() => {
642
- void this.flushBufferedInput();
643
- }, MULTILINE_INPUT_FLUSH_DELAY_MS);
644
- }
645
- /**
646
- * Show a preview indicator while accumulating multi-line paste
647
- */
648
- showMultiLinePastePreview(lineCount, preview) {
649
- // Clear the current line and show paste accumulation status
650
- readline.clearLine(output, 0);
651
- readline.cursorTo(output, 0);
652
- const statusText = preview
653
- ? `${theme.ui.muted('📋 Pasting:')} ${theme.ui.muted(preview.slice(0, 50))}${preview.length > 50 ? '...' : ''}`
654
- : `${theme.ui.muted(`📋 Pasting ${lineCount} line${lineCount !== 1 ? 's' : ''}...`)}`;
655
- output.write(statusText);
656
- }
657
- /**
658
- * Clear the multi-line paste preview
659
- */
660
- clearMultiLinePastePreview() {
661
- readline.clearLine(output, 0);
662
- readline.cursorTo(output, 0);
663
- }
664
- /**
665
- * Display a collapsed summary of multi-line content that was submitted
666
- */
667
- displayMultiLineSubmission(content, lineCount) {
668
- const lines = content.split('\n');
669
- const firstLine = (lines[0] || '').trim();
670
- const maxPreviewLen = Math.min(60, (output.columns || 80) - 20);
671
- const truncatedFirst = firstLine.length > maxPreviewLen
672
- ? firstLine.slice(0, maxPreviewLen - 3) + '...'
673
- : firstLine;
674
- // Show collapsed block indicator
675
- const collapsedDisplay = theme.ui.muted(`📋 ${truncatedFirst} [${lineCount} lines]`);
676
- // Clear line and show the collapsed preview, then newline for response
677
- readline.clearLine(output, 0);
678
- readline.cursorTo(output, 0);
679
- console.log(`${formatUserPrompt(this.profileLabel || this.profile)}${collapsedDisplay}`);
680
- }
681
- async flushBufferedInput() {
682
- if (!this.bufferedInputLines.length) {
683
- this.bufferedInputTimer = null;
684
- return;
685
- }
686
- const lineCount = this.bufferedInputLines.length;
687
- const combined = this.bufferedInputLines.join('\n');
688
- this.bufferedInputLines = [];
689
- this.bufferedInputTimer = null;
690
- try {
691
- await this.processInputBlock(combined, lineCount > 1);
692
- }
693
- catch (error) {
694
- // Pass full error object for enhanced formatting
695
- display.showError(error instanceof Error ? error.message : String(error), error);
696
- this.rl.prompt();
697
- }
698
- }
699
- refreshQueueIndicators() {
700
- const queued = this.followUpQueue.length;
701
- const message = queued > 0 ? `${queued} follow-up${queued === 1 ? '' : 's'} queued` : undefined;
702
- this.persistentPrompt.updateStatusBar({ message });
703
- if (this.isProcessing) {
704
- this.setProcessingStatus();
705
- }
706
- else {
707
- this.setIdleStatus();
708
- }
709
- }
710
- enqueueFollowUpAction(action) {
711
- this.followUpQueue.push(action);
712
- const normalized = action.text.replace(/\s+/g, ' ').trim();
713
- const previewLimit = 120;
714
- const preview = normalized.length > previewLimit ? `${normalized.slice(0, previewLimit - 3)}...` : normalized;
715
- const position = this.followUpQueue.length === 1 ? 'to run next' : `#${this.followUpQueue.length} in queue`;
716
- const label = action.type === 'continuous' ? 'continuous command' : 'follow-up request';
717
- if (preview) {
718
- display.showInfo(`Queued ${label} ${position} after the current response: ${theme.ui.muted(preview)}`);
719
- }
720
- else {
721
- display.showInfo(`Queued ${label} ${position} after the current response.`);
722
- }
723
- this.refreshQueueIndicators();
724
- this.scheduleQueueProcessing();
725
- }
726
- scheduleQueueProcessing() {
727
- if (!this.followUpQueue.length) {
728
- this.refreshQueueIndicators();
729
- return;
730
- }
731
- queueMicrotask(() => {
732
- void this.processQueuedActions();
733
- });
734
- }
735
- async processQueuedActions() {
736
- if (this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
737
- return;
738
- }
739
- this.isDrainingQueue = true;
740
- try {
741
- while (!this.isProcessing && this.followUpQueue.length) {
742
- const next = this.followUpQueue.shift();
743
- const remaining = this.followUpQueue.length;
744
- const label = next.type === 'continuous' ? 'continuous command' : 'follow-up';
745
- const suffix = remaining ? ` (${remaining} left after this)` : '';
746
- display.showSystemMessage(`▶ Running queued ${label}${suffix}.`);
747
- this.refreshQueueIndicators();
748
- if (next.type === 'continuous') {
749
- await this.processContinuousRequest(next.text);
750
- }
751
- else {
752
- await this.processRequest(next.text);
753
- }
754
- }
755
- }
756
- finally {
757
- this.isDrainingQueue = false;
758
- this.refreshQueueIndicators();
759
- }
760
- }
761
- async processInputBlock(line, _wasRapidMultiLine = false) {
762
- this.slashPreviewVisible = false;
763
- this.uiAdapter.hideSlashCommandPreview();
764
- const trimmed = line.trim();
765
- if (await this.handlePendingInteraction(trimmed)) {
766
- return;
767
- }
768
- if (!trimmed) {
769
- this.rl.prompt();
770
- return;
771
- }
772
- if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
773
- this.rl.close();
774
- return;
775
- }
776
- if (trimmed.toLowerCase() === 'clear') {
777
- display.clear();
778
- this.rl.prompt();
779
- return;
780
- }
781
- if (trimmed.toLowerCase() === 'help') {
782
- this.showHelp();
783
- this.rl.prompt();
784
- return;
785
- }
786
- if (trimmed.startsWith('/')) {
787
- await this.processSlashCommand(trimmed);
788
- return;
789
- }
790
- // Check for continuous/infinite loop commands
791
- if (this.isContinuousCommand(trimmed)) {
792
- await this.processContinuousRequest(trimmed);
793
- this.rl.prompt();
794
- return;
795
- }
796
- // Direct execution for all inputs, including multi-line pastes
797
- await this.processRequest(trimmed);
798
- this.rl.prompt();
799
- }
800
- /**
801
- * Check if the command is a continuous/infinite loop command
802
- * These commands run until the task is complete or user interrupts
803
- */
804
- isContinuousCommand(input) {
805
- const lower = input.toLowerCase().trim();
806
- const patterns = [
807
- /^(keep going|continue|finish|until done|run until done|complete|finish it|keep working)/i,
808
- /\b(until done|until complete|until finished|to completion|to the end)\b/i,
809
- /^(do it|just do it|make it work|fix it all|finish everything)\b/i,
810
- ];
811
- return patterns.some(pattern => pattern.test(lower));
812
- }
813
- /**
814
- * Check if the request is a self-improvement task
815
- * These requests should use git to track changes
816
- */
817
- isSelfImprovementRequest(input) {
818
- const lower = input.toLowerCase();
819
- const patterns = [
820
- /\b(improve|enhance|upgrade|refactor|optimize)\b.*\b(codebase|code|project|app|application)\b/i,
821
- /\b(self[- ]?improv|self[- ]?modif|self[- ]?updat)/i,
822
- /\b(make.*better|add.*feature|fix.*bug|implement)/i,
823
- /\b(ability to code|coding capabilit)/i,
824
- ];
825
- return patterns.some(pattern => pattern.test(lower));
826
- }
827
- async handlePendingInteraction(input) {
828
- if (!this.pendingInteraction) {
829
- return false;
830
- }
831
- switch (this.pendingInteraction.type) {
832
- case 'model-provider':
833
- await this.handleModelProviderSelection(input);
834
- return true;
835
- case 'model':
836
- await this.handleModelSelection(input);
837
- return true;
838
- case 'secret-select':
839
- await this.handleSecretSelection(input);
840
- return true;
841
- case 'secret-input':
842
- await this.handleSecretInput(input);
843
- return true;
844
- case 'tool-settings':
845
- await this.handleToolSettingsInput(input);
846
- return true;
847
- case 'agent-selection':
848
- await this.handleAgentSelectionInput(input);
849
- return true;
850
- default:
851
- return false;
852
- }
853
- }
854
- async processSlashCommand(input) {
855
- const [command] = input.split(/\s+/);
856
- if (!command) {
857
- display.showWarning('Enter a slash command.');
858
- this.rl.prompt();
859
- return;
860
- }
861
- switch (command) {
862
- case '/model':
863
- this.showModelMenu();
864
- break;
865
- case '/secrets':
866
- this.showSecretsMenu();
867
- break;
868
- case '/tools':
869
- this.showToolsMenu();
870
- break;
871
- case '/doctor':
872
- this.runDoctor();
873
- break;
874
- case '/checks':
875
- await this.runRepoChecksCommand();
876
- break;
877
- case '/context':
878
- await this.refreshWorkspaceContextCommand(input);
879
- break;
880
- case '/agents':
881
- this.showAgentsMenu();
882
- break;
883
- case '/sessions':
884
- await this.handleSessionCommand(input);
885
- break;
886
- case '/skills':
887
- await this.handleSkillsCommand(input);
888
- break;
889
- case '/thinking':
890
- this.handleThinkingCommand(input);
891
- break;
892
- case '/shortcuts':
893
- case '/keys':
894
- this.handleShortcutsCommand();
895
- break;
896
- case '/changes':
897
- case '/summary':
898
- this.showFileChangeSummary();
899
- break;
900
- case '/metrics':
901
- case '/stats':
902
- case '/perf':
903
- this.showAlphaZeroMetrics();
904
- break;
905
- case '/suggestions':
906
- case '/improve':
907
- this.showImprovementSuggestions();
908
- break;
909
- case '/plugins':
910
- this.showPluginStatus();
911
- break;
912
- case '/provider':
913
- await this.handleProviderCommand(input);
914
- break;
915
- case '/providers':
916
- this.showConfiguredProviders();
917
- break;
918
- case '/local':
919
- await this.handleLocalCommand(input);
920
- break;
921
- case '/discover':
922
- await this.discoverModelsCommand();
923
- break;
924
- default:
925
- if (!(await this.tryCustomSlashCommand(command, input))) {
926
- display.showWarning(`Unknown command "${command}".`);
927
- }
928
- break;
929
- }
930
- this.rl.prompt();
931
- }
932
- async tryCustomSlashCommand(command, fullInput) {
933
- const custom = this.customCommandMap.get(command);
934
- if (!custom) {
935
- return false;
936
- }
937
- const args = fullInput.slice(command.length).trim();
938
- if (custom.requireInput && !args) {
939
- display.showWarning(`${command} requires additional input.`);
940
- return true;
941
- }
942
- const prompt = buildCustomCommandPrompt(custom, args, {
943
- workspace: this.workingDir,
944
- profile: this.profile,
945
- provider: this.sessionState.provider,
946
- model: this.sessionState.model,
947
- }).trim();
948
- if (!prompt) {
949
- display.showWarning(`Custom command ${command} did not produce any text. Check ${custom.source} for errors.`);
950
- return true;
951
- }
952
- display.showInfo(`Running ${command} from ${custom.source}...`);
953
- await this.processRequest(prompt);
954
- return true;
955
- }
956
- showHelp() {
957
- const info = [
958
- this.buildSlashCommandList('Available Commands:'),
959
- '',
960
- 'Type your request in natural language and press Enter.',
961
- ];
962
- display.showSystemMessage(info.join('\n'));
963
- }
964
- runDoctor() {
965
- const lines = [];
966
- lines.push(theme.bold('Environment diagnostics'));
967
- lines.push('');
968
- lines.push(`${theme.secondary('Workspace')}: ${this.workingDir}`);
969
- lines.push('');
970
- lines.push(theme.bold('Provider credentials'));
971
- const providerDefinition = getSecretDefinitionForProvider(this.sessionState.provider);
972
- if (providerDefinition) {
973
- const currentValue = getSecretValue(providerDefinition.id);
974
- if (currentValue) {
975
- lines.push(`${theme.success('✓')} ${providerDefinition.label} configured (${providerDefinition.envVar}).`);
976
- }
977
- else {
978
- lines.push(`${theme.warning('⚠')} Missing ${providerDefinition.label} (${providerDefinition.envVar}). Run /secrets to configure it.`);
979
- }
980
- }
981
- else {
982
- lines.push(`${theme.secondary('•')} ${this.providerLabel(this.sessionState.provider)} does not require an API key.`);
983
- }
984
- lines.push('');
985
- lines.push(theme.bold('Tool suites'));
986
- const toolSettings = loadToolSettings();
987
- const selection = buildEnabledToolSet(toolSettings);
988
- const permissions = evaluateToolPermissions(selection);
989
- const options = getToolToggleOptions();
990
- const enabledLabels = options
991
- .filter((option) => selection.has(option.id))
992
- .map((option) => option.label);
993
- lines.push(`Enabled: ${enabledLabels.length ? enabledLabels.join(', ') : 'none'}`);
994
- if (!permissions.warnings.length) {
995
- lines.push(theme.success('All enabled suites loaded successfully.'));
996
- }
997
- else {
998
- lines.push(theme.warning('Issues detected:'));
999
- for (const warning of permissions.warnings) {
1000
- const detail = this.describeToolWarning(warning);
1001
- lines.push(` - ${detail}`);
1002
- }
1003
- }
1004
- display.showSystemMessage(lines.join('\n'));
1005
- }
1006
- async runRepoChecksCommand() {
1007
- if (this.isProcessing) {
1008
- display.showWarning('Wait for the active response to finish before running checks.');
1009
- return;
1010
- }
1011
- const call = {
1012
- id: 'manual-run-repo-checks',
1013
- name: 'run_repo_checks',
1014
- arguments: {},
1015
- };
1016
- display.showInfo('Running repo checks (npm test/build/lint when available)...');
1017
- const output = await this.runtimeSession.toolRuntime.execute(call);
1018
- display.showSystemMessage(output);
1019
- }
1020
- async refreshWorkspaceContextCommand(input) {
1021
- if (this.isProcessing) {
1022
- display.showWarning('Wait for the active response to finish before refreshing the snapshot.');
1023
- return;
1024
- }
1025
- const { overrides, error } = this.parseContextOverrideTokens(input);
1026
- if (error) {
1027
- display.showWarning(`${error} ${this.describeContextOverrideUsage()}`);
1028
- return;
1029
- }
1030
- if (overrides) {
1031
- this.workspaceOptions = { ...this.workspaceOptions, ...overrides };
1032
- }
1033
- display.showInfo('Refreshing workspace snapshot...');
1034
- const context = buildWorkspaceContext(this.workingDir, this.workspaceOptions);
1035
- const profileConfig = this.runtimeSession.refreshWorkspaceContext(context);
1036
- const tools = this.runtimeSession.toolRuntime.listProviderTools();
1037
- this.baseSystemPrompt = buildInteractiveSystemPrompt(profileConfig.systemPrompt, profileConfig.label, tools);
1038
- if (this.rebuildAgent()) {
1039
- display.showInfo(`Workspace snapshot refreshed (${this.describeWorkspaceOptions()}).`);
1040
- }
1041
- else {
1042
- display.showWarning('Workspace snapshot refreshed, but the agent failed to rebuild. Run /doctor for details.');
1043
- }
1044
- }
1045
- parseContextOverrideTokens(input) {
1046
- const overrides = {};
1047
- let hasOverride = false;
1048
- const tokens = input
1049
- .trim()
1050
- .split(/\s+/)
1051
- .slice(1);
1052
- for (const token of tokens) {
1053
- if (!token) {
1054
- continue;
1055
- }
1056
- const [rawKey, rawValue] = token.split('=');
1057
- if (!rawKey || !rawValue) {
1058
- return { overrides: null, error: `Invalid option "${token}".` };
1059
- }
1060
- const key = rawKey.toLowerCase();
1061
- const value = Number.parseInt(rawValue, 10);
1062
- if (!Number.isFinite(value) || value <= 0) {
1063
- return { overrides: null, error: `Value for "${key}" must be a positive integer.` };
1064
- }
1065
- switch (key) {
1066
- case 'depth':
1067
- overrides.treeDepth = value;
1068
- hasOverride = true;
1069
- break;
1070
- case 'entries':
1071
- overrides.maxEntries = value;
1072
- hasOverride = true;
1073
- break;
1074
- case 'excerpt':
1075
- case 'doc':
1076
- case 'docs':
1077
- overrides.docExcerptLimit = value;
1078
- hasOverride = true;
1079
- break;
1080
- default:
1081
- return { overrides: null, error: `Unknown option "${key}".` };
1082
- }
1083
- }
1084
- return { overrides: hasOverride ? overrides : null };
1085
- }
1086
- async handleSessionCommand(input) {
1087
- const tokens = input
1088
- .trim()
1089
- .split(/\s+/)
1090
- .slice(1);
1091
- const action = (tokens.shift() ?? 'list').toLowerCase();
1092
- switch (action) {
1093
- case '':
1094
- case 'list':
1095
- this.showSessionList();
1096
- return;
1097
- case 'save':
1098
- await this.saveSessionCommand(tokens.join(' ').trim());
1099
- return;
1100
- case 'load':
1101
- await this.loadSessionCommand(tokens.join(' ').trim());
1102
- return;
1103
- case 'delete':
1104
- case 'remove':
1105
- this.deleteSessionCommand(tokens.join(' ').trim());
1106
- return;
1107
- case 'new':
1108
- this.newSessionCommand(tokens.join(' ').trim());
1109
- return;
1110
- case 'autosave':
1111
- this.toggleAutosaveCommand(tokens[0]);
1112
- return;
1113
- case 'clear':
1114
- this.clearAutosaveCommand();
1115
- return;
1116
- default:
1117
- display.showWarning('Usage: /sessions [list|save <title>|load <id>|delete <id>|new <title>|autosave on|off|clear]');
1118
- return;
1119
- }
1120
- }
1121
- async handleSkillsCommand(input) {
1122
- const raw = input.slice('/skills'.length).trim();
1123
- const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
1124
- let refresh = false;
1125
- const filtered = [];
1126
- for (const token of tokens) {
1127
- if (token === '--refresh' || token === '-r') {
1128
- refresh = true;
1129
- continue;
1130
- }
1131
- filtered.push(token);
1132
- }
1133
- let mode = filtered.shift()?.toLowerCase() ?? 'list';
1134
- if (mode === 'refresh') {
1135
- refresh = true;
1136
- mode = 'list';
1137
- }
1138
- try {
1139
- switch (mode) {
1140
- case '':
1141
- case 'list': {
1142
- const query = filtered.join(' ');
1143
- const output = await this.invokeSkillTool('ListSkills', {
1144
- query: query || undefined,
1145
- refresh_cache: refresh,
1146
- });
1147
- display.showSystemMessage(output);
1148
- break;
1149
- }
1150
- case 'show':
1151
- case 'view': {
1152
- const identifier = filtered.shift();
1153
- if (!identifier) {
1154
- display.showWarning('Usage: /skills show <skill-id> [sections=metadata,body]');
1155
- return;
1156
- }
1157
- let sectionsArg;
1158
- for (let i = 0; i < filtered.length; i += 1) {
1159
- const token = filtered[i];
1160
- if (!token) {
1161
- continue;
1162
- }
1163
- if (token.startsWith('sections=')) {
1164
- sectionsArg = token.slice('sections='.length);
1165
- filtered.splice(i, 1);
1166
- break;
1167
- }
1168
- }
1169
- const sections = sectionsArg
1170
- ? sectionsArg
1171
- .split(',')
1172
- .map((section) => section.trim())
1173
- .filter(Boolean)
1174
- : undefined;
1175
- const output = await this.invokeSkillTool('Skill', {
1176
- skill: identifier,
1177
- sections,
1178
- refresh_cache: refresh,
1179
- });
1180
- display.showSystemMessage(output);
1181
- break;
1182
- }
1183
- default:
1184
- display.showWarning('Usage: /skills [list|refresh|show <id> [sections=a,b]]');
1185
- break;
1186
- }
1187
- }
1188
- catch (error) {
1189
- const message = error instanceof Error ? error.message : String(error);
1190
- display.showError(`Skill command failed: ${message}`);
1191
- }
1192
- }
1193
- async invokeSkillTool(name, args) {
1194
- const handler = this.skillToolHandlers.get(name);
1195
- if (!handler) {
1196
- throw new Error(`Skill tool "${name}" is not registered.`);
1197
- }
1198
- const result = await handler(args);
1199
- return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1200
- }
1201
- handleThinkingCommand(input) {
1202
- const value = input.slice('/thinking'.length).trim().toLowerCase();
1203
- if (!value) {
1204
- display.showInfo(`Thinking mode is currently ${theme.info(this.thinkingMode)}. Usage: /thinking [concise|balanced|extended]`);
1205
- return;
1206
- }
1207
- if (value !== 'concise' && value !== 'balanced' && value !== 'extended') {
1208
- display.showWarning('Usage: /thinking [concise|balanced|extended]');
1209
- return;
1210
- }
1211
- if (this.isProcessing) {
1212
- display.showWarning('Wait until the current request finishes before changing thinking mode.');
1213
- return;
1214
- }
1215
- this.thinkingMode = value;
1216
- saveSessionPreferences({ thinkingMode: this.thinkingMode });
1217
- this.rebuildAgent();
1218
- const descriptions = {
1219
- concise: 'Hides internal reasoning and responds directly.',
1220
- balanced: 'Shows short thoughts only when helpful.',
1221
- extended: 'Always emits a <thinking> block before the final response.',
1222
- };
1223
- display.showInfo(`Thinking mode set to ${theme.info(value)} – ${descriptions[this.thinkingMode]}`);
1224
- }
1225
- handleShortcutsCommand() {
1226
- // Display keyboard shortcuts help (Claude Code style)
1227
- display.showSystemMessage(formatShortcutsHelp());
1228
- }
1229
- showFileChangeSummary() {
1230
- const summary = this._fileChangeTracker.getSummary();
1231
- const changes = this._fileChangeTracker.getAllChanges();
1232
- if (changes.length === 0) {
1233
- display.showInfo('No files modified in this session.');
1234
- return;
1235
- }
1236
- const lines = [];
1237
- lines.push(theme.bold('Session File Changes'));
1238
- lines.push('');
1239
- lines.push(`${theme.info('•')} ${summary.files} file${summary.files === 1 ? '' : 's'} modified`);
1240
- lines.push(`${theme.info('•')} ${theme.success('+' + summary.additions)} ${theme.error('-' + summary.removals)} lines`);
1241
- lines.push('');
1242
- // Group changes by file
1243
- const fileMap = new Map();
1244
- for (const change of changes) {
1245
- const existing = fileMap.get(change.path) || { edits: 0, writes: 0, additions: 0, removals: 0 };
1246
- if (change.type === 'edit') {
1247
- existing.edits++;
1248
- }
1249
- else if (change.type === 'write') {
1250
- existing.writes++;
1251
- }
1252
- existing.additions += change.additions;
1253
- existing.removals += change.removals;
1254
- fileMap.set(change.path, existing);
1255
- }
1256
- // Display each file
1257
- for (const [path, stats] of fileMap) {
1258
- const operations = [];
1259
- if (stats.edits > 0)
1260
- operations.push(`${stats.edits} edit${stats.edits === 1 ? '' : 's'}`);
1261
- if (stats.writes > 0)
1262
- operations.push(`${stats.writes} write${stats.writes === 1 ? '' : 's'}`);
1263
- const opsText = operations.join(', ');
1264
- const diffText = `${theme.success('+' + stats.additions)} ${theme.error('-' + stats.removals)}`;
1265
- lines.push(` ${theme.dim(path)}`);
1266
- lines.push(` ${opsText} • ${diffText}`);
1267
- }
1268
- display.showSystemMessage(lines.join('\n'));
1269
- }
1270
- showAlphaZeroMetrics() {
1271
- const summary = this.alphaZeroMetrics.getPerformanceSummary();
1272
- display.showSystemMessage(summary);
1273
- }
1274
- showImprovementSuggestions() {
1275
- const suggestions = this.alphaZeroMetrics.getImprovementSuggestions();
1276
- if (suggestions.length === 0) {
1277
- display.showInfo('No improvement suggestions at this time. Keep using the shell to generate metrics!');
1278
- return;
1279
- }
1280
- const lines = [
1281
- theme.bold('Improvement Suggestions (Alpha Zero 2):'),
1282
- '',
1283
- ];
1284
- for (let i = 0; i < suggestions.length; i++) {
1285
- const suggestion = suggestions[i];
1286
- const severityColor = suggestion.severity === 'high'
1287
- ? theme.error
1288
- : suggestion.severity === 'medium'
1289
- ? theme.warning
1290
- : theme.info;
1291
- lines.push(`${i + 1}. [${severityColor(suggestion.severity.toUpperCase())}] ${suggestion.message}`);
1292
- if (suggestion.suggestedAction) {
1293
- lines.push(` ${theme.dim('Action:')} ${suggestion.suggestedAction}`);
1294
- }
1295
- }
1296
- display.showSystemMessage(lines.join('\n'));
1297
- }
1298
- showPluginStatus() {
1299
- const available = listAvailablePlugins();
1300
- const lines = [];
1301
- lines.push(theme.bold('Plugin Status'));
1302
- lines.push('');
1303
- if (available.length === 0) {
1304
- lines.push(theme.secondary('No plugins registered.'));
1305
- }
1306
- else {
1307
- lines.push(theme.secondary('Available Plugins:'));
1308
- lines.push('');
1309
- for (const pluginId of available) {
1310
- const isEnabled = this._enabledPlugins.includes(pluginId);
1311
- const status = isEnabled
1312
- ? theme.success('[ENABLED]')
1313
- : theme.dim('[available]');
1314
- const displayName = pluginId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
1315
- lines.push(` ${status} ${theme.bold(displayName)}`);
1316
- lines.push(` ${theme.dim(`ID: ${pluginId}`)}`);
1317
- lines.push('');
1318
- }
1319
- }
1320
- lines.push(theme.secondary('CLI Flags:'));
1321
- lines.push(' --alpha-zero Enable Alpha Zero 2 RL framework');
1322
- lines.push(' --coding Enable enhanced coding tools');
1323
- lines.push(' --security Enable security research tools');
1324
- lines.push(' --all-plugins Enable all optional plugins');
1325
- display.showSystemMessage(lines.join('\n'));
1326
- }
1327
- showSessionList() {
1328
- const sessions = listSessions(this.profile);
1329
- const lines = [];
1330
- lines.push(theme.bold('Saved sessions'));
1331
- lines.push('Use "/sessions save <title>" to persist history or "/sessions load <id>" to resume.');
1332
- lines.push('');
1333
- if (!sessions.length) {
1334
- lines.push(theme.secondary('No saved sessions yet.'));
1335
- }
1336
- else {
1337
- sessions.forEach((session, index) => {
1338
- const prefix = `${index + 1}.`;
1339
- const label = session.title || '(untitled)';
1340
- const relative = this.describeRelativeTime(session.updatedAt);
1341
- const active = this.activeSessionId && session.id === this.activeSessionId
1342
- ? ` ${theme.success('[active]')}`
1343
- : '';
1344
- const messageCount = `${session.messageCount} msg`;
1345
- const shortId = this.formatSessionId(session.id);
1346
- lines.push(`${prefix.padEnd(3)} ${label} ${theme.secondary(`(${messageCount}, ${relative}, ${shortId})`)}${active}`);
1347
- });
1348
- }
1349
- lines.push('');
1350
- lines.push(`Autosave: ${this.autosaveEnabled ? theme.success('on') : theme.warning('off')} (toggle via "/sessions autosave on|off")`);
1351
- display.showSystemMessage(lines.join('\n'));
1352
- }
1353
- async saveSessionCommand(title) {
1354
- const agent = this.agent;
1355
- if (!agent) {
1356
- display.showWarning('Start a conversation before saving a session.');
1357
- return;
1358
- }
1359
- const history = agent.getHistory();
1360
- if (!history || history.length <= 1) {
1361
- display.showWarning('You need at least one user message before saving a session.');
1362
- return;
1363
- }
1364
- const summary = saveSessionSnapshot({
1365
- id: this.activeSessionId ?? undefined,
1366
- title: title || this.activeSessionTitle || null,
1367
- profile: this.profile,
1368
- provider: this.sessionState.provider,
1369
- model: this.sessionState.model,
1370
- workspaceRoot: this.workingDir,
1371
- messages: history,
1372
- });
1373
- this.cachedHistory = history;
1374
- this.updateActiveSession(summary, true);
1375
- this.sessionResumeNotice = null;
1376
- this.autosaveIfEnabled();
1377
- display.showInfo(`Session saved as "${summary.title}" (id ${this.formatSessionId(summary.id)}).`);
1378
- }
1379
- async loadSessionCommand(selector) {
1380
- const summary = this.resolveSessionBySelector(selector);
1381
- if (!summary) {
1382
- display.showWarning('No session matches that selection.');
1383
- return;
1384
- }
1385
- const stored = loadSessionById(summary.id);
1386
- if (!stored) {
1387
- display.showWarning('Failed to load that session. It may have been corrupted or deleted.');
1388
- return;
1389
- }
1390
- this.cachedHistory = stored.messages;
1391
- this.updateActiveSession(summary, true);
1392
- this.sessionResumeNotice = `Loaded session "${summary.title}".`;
1393
- if (this.agent) {
1394
- this.agent.loadHistory(stored.messages);
1395
- this.sessionResumeNotice = null;
1396
- display.showInfo(`Loaded session "${summary.title}".`);
1397
- this.refreshContextGauge();
1398
- this.captureHistorySnapshot();
1399
- this.pendingHistoryLoad = null;
1400
- }
1401
- else {
1402
- this.pendingHistoryLoad = stored.messages;
1403
- display.showInfo(`Session "${summary.title}" queued to load once the agent is ready.`);
1404
- }
1405
- this.autosaveIfEnabled();
1406
- }
1407
- deleteSessionCommand(selector) {
1408
- const summary = this.resolveSessionBySelector(selector);
1409
- if (!summary) {
1410
- display.showWarning('No session matches that selection.');
1411
- return;
1412
- }
1413
- if (!deleteSession(summary.id)) {
1414
- display.showWarning('Unable to delete that session.');
1415
- return;
1416
- }
1417
- display.showInfo(`Deleted session "${summary.title}".`);
1418
- if (this.activeSessionId === summary.id) {
1419
- this.activeSessionId = null;
1420
- this.activeSessionTitle = null;
1421
- saveSessionPreferences({ lastSessionId: null });
1422
- }
1423
- }
1424
- newSessionCommand(title) {
1425
- if (this.agent) {
1426
- this.agent.clearHistory();
1427
- this.cachedHistory = this.agent.getHistory();
1428
- this.pendingHistoryLoad = null;
1429
- }
1430
- if (!this.agent) {
1431
- this.cachedHistory = [];
1432
- this.pendingHistoryLoad = [];
1433
- }
1434
- this.activeSessionId = null;
1435
- this.activeSessionTitle = title || null;
1436
- this.sessionResumeNotice = null;
1437
- saveSessionPreferences({ lastSessionId: null });
1438
- clearAutosaveSnapshot(this.profile);
1439
- display.showInfo('Started a new empty session.');
1440
- this.refreshContextGauge();
1441
- }
1442
- toggleAutosaveCommand(value) {
1443
- if (!value) {
1444
- display.showWarning('Usage: /sessions autosave on|off');
1445
- return;
1446
- }
1447
- const normalized = value.toLowerCase();
1448
- if (normalized !== 'on' && normalized !== 'off') {
1449
- display.showWarning('Usage: /sessions autosave on|off');
1450
- return;
1451
- }
1452
- this.autosaveEnabled = normalized === 'on';
1453
- saveSessionPreferences({ autosave: this.autosaveEnabled });
1454
- display.showInfo(`Autosave ${this.autosaveEnabled ? 'enabled' : 'disabled'}.`);
1455
- if (!this.autosaveEnabled) {
1456
- clearAutosaveSnapshot(this.profile);
1457
- }
1458
- else {
1459
- this.autosaveIfEnabled();
1460
- }
1461
- }
1462
- clearAutosaveCommand() {
1463
- clearAutosaveSnapshot(this.profile);
1464
- display.showInfo('Cleared autosave history.');
1465
- }
1466
- updateActiveSession(summary, remember = false) {
1467
- this.activeSessionId = summary?.id ?? null;
1468
- this.activeSessionTitle = summary?.title ?? null;
1469
- if (remember) {
1470
- saveSessionPreferences({ lastSessionId: summary?.id ?? null });
1471
- }
1472
- }
1473
- resolveSessionBySelector(selector) {
1474
- const sessions = listSessions(this.profile);
1475
- if (!sessions.length) {
1476
- return null;
1477
- }
1478
- if (!selector) {
1479
- return sessions[0] ?? null;
1480
- }
1481
- const trimmed = selector.trim();
1482
- if (!trimmed) {
1483
- return sessions[0] ?? null;
1484
- }
1485
- const index = Number.parseInt(trimmed, 10);
1486
- if (Number.isFinite(index)) {
1487
- const entry = sessions[index - 1];
1488
- return entry ?? null;
1489
- }
1490
- const match = sessions.find((session) => session.id.startsWith(trimmed));
1491
- return match ?? null;
1492
- }
1493
- formatSessionId(id) {
1494
- return id.length > 8 ? `${id.slice(0, 8)}…` : id;
1495
- }
1496
- describeRelativeTime(timestamp) {
1497
- const updated = Date.parse(timestamp);
1498
- if (!updated) {
1499
- return 'unknown';
1500
- }
1501
- const deltaMs = Date.now() - updated;
1502
- const minutes = Math.round(deltaMs / 60000);
1503
- if (minutes < 1) {
1504
- return 'just now';
1505
- }
1506
- if (minutes < 60) {
1507
- return `${minutes}m ago`;
1508
- }
1509
- const hours = Math.round(minutes / 60);
1510
- if (hours < 24) {
1511
- return `${hours}h ago`;
1512
- }
1513
- const days = Math.round(hours / 24);
1514
- return `${days}d ago`;
1515
- }
1516
- captureHistorySnapshot() {
1517
- if (!this.agent) {
1518
- return;
1519
- }
1520
- this.cachedHistory = this.agent.getHistory();
1521
- }
1522
- autosaveIfEnabled() {
1523
- if (!this.autosaveEnabled) {
1524
- return;
1525
- }
1526
- if (!this.cachedHistory || this.cachedHistory.length <= 1) {
1527
- return;
1528
- }
1529
- saveAutosaveSnapshot(this.profile, {
1530
- provider: this.sessionState.provider,
1531
- model: this.sessionState.model,
1532
- workspaceRoot: this.workingDir,
1533
- title: this.activeSessionTitle,
1534
- messages: this.cachedHistory,
1535
- });
1536
- }
1537
- describeWorkspaceOptions() {
1538
- const depth = this.workspaceOptions.treeDepth ?? 'default';
1539
- const entries = this.workspaceOptions.maxEntries ?? 'default';
1540
- const excerpt = this.workspaceOptions.docExcerptLimit ?? 'default';
1541
- return `depth=${depth}, entries=${entries}, excerpt=${excerpt}`;
1542
- }
1543
- describeContextOverrideUsage() {
1544
- return 'Usage: /context [depth=<n>] [entries=<n>] [excerpt=<n>]';
1545
- }
1546
- describeToolWarning(warning) {
1547
- if (warning.reason === 'missing-secret' && warning.secretId) {
1548
- return `${warning.label}: missing ${warning.secretId}. Use /secrets to configure it.`;
1549
- }
1550
- return `${warning.label}: ${warning.reason}.`;
1551
- }
1552
- buildSlashCommandList(header) {
1553
- const lines = [theme.gradient.primary(header), ''];
1554
- for (const command of this.slashCommands) {
1555
- lines.push(`${theme.primary(command.command)} - ${command.description}`);
1556
- }
1557
- return lines.join('\n');
1558
- }
1559
- showModelMenu() {
1560
- const providerOptions = this.buildProviderOptions();
1561
- if (!providerOptions.length) {
1562
- display.showWarning('No providers are available.');
1563
- return;
1564
- }
1565
- const lines = [
1566
- theme.bold('Select a provider:'),
1567
- ...providerOptions.map((option, index) => {
1568
- const isCurrent = option.provider === this.sessionState.provider;
1569
- const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
1570
- const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}`, index);
1571
- const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
1572
- return `${label}${suffix}`;
1573
- }),
1574
- 'Type the number of the provider to continue, or type "cancel".',
1575
- ];
1576
- display.showSystemMessage(lines.join('\n'));
1577
- this.pendingInteraction = { type: 'model-provider', options: providerOptions };
1578
- }
1579
- buildProviderOptions() {
1580
- const counts = new Map();
1581
- for (const preset of MODEL_PRESETS) {
1582
- counts.set(preset.provider, (counts.get(preset.provider) ?? 0) + 1);
1583
- }
1584
- const orderedProviders = [];
1585
- const seen = new Set();
1586
- for (const preset of MODEL_PRESETS) {
1587
- if (seen.has(preset.provider)) {
1588
- continue;
1589
- }
1590
- seen.add(preset.provider);
1591
- orderedProviders.push(preset.provider);
1592
- }
1593
- return orderedProviders.map((provider) => ({
1594
- provider,
1595
- label: this.providerLabel(provider),
1596
- modelCount: counts.get(provider) ?? 0,
1597
- }));
1598
- }
1599
- showProviderModels(option) {
1600
- const models = MODEL_PRESETS.filter((preset) => preset.provider === option.provider);
1601
- if (!models.length) {
1602
- display.showWarning(`No models available for ${option.label}.`);
1603
- this.pendingInteraction = null;
1604
- return;
1605
- }
1606
- const lines = [
1607
- theme.bold(`Select a model from ${option.label}:`),
1608
- ...models.map((preset, index) => {
1609
- const isCurrent = preset.id === this.sessionState.model;
1610
- const label = this.colorizeDropdownLine(`${index + 1}. ${preset.label}`, index);
1611
- const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
1612
- const description = this.colorizeDropdownLine(` ${preset.description}`, index);
1613
- return `${label}${suffix}\n${description}`;
1614
- }),
1615
- 'Type the number of the model to select it, type "back" to change provider, or type "cancel".',
1616
- ];
1617
- display.showSystemMessage(lines.join('\n'));
1618
- this.pendingInteraction = { type: 'model', provider: option.provider, options: models };
1619
- }
1620
- showSecretsMenu() {
1621
- const definitions = listSecretDefinitions();
1622
- const lines = [
1623
- theme.bold('Manage Secrets:'),
1624
- ...definitions.map((definition, index) => {
1625
- const value = getSecretValue(definition.id);
1626
- const status = value ? maskSecret(value) : theme.warning('not set');
1627
- const providers = definition.providers.map((id) => this.providerLabel(id)).join(', ');
1628
- const label = this.colorizeDropdownLine(`${index + 1}. ${definition.label} (${providers})`, index);
1629
- return `${label} — ${status}`;
1630
- }),
1631
- 'Enter the number to update a key, or type "cancel".',
1632
- ];
1633
- display.showSystemMessage(lines.join('\n'));
1634
- this.pendingInteraction = { type: 'secret-select', options: definitions };
1635
- }
1636
- showToolsMenu() {
1637
- const options = getToolToggleOptions();
1638
- if (!options.length) {
1639
- display.showWarning('No configurable tools are available.');
1640
- return;
1641
- }
1642
- const selection = buildEnabledToolSet(loadToolSettings());
1643
- const interaction = {
1644
- type: 'tool-settings',
1645
- options,
1646
- selection,
1647
- initialSelection: new Set(selection),
1648
- };
1649
- this.pendingInteraction = interaction;
1650
- this.renderToolMenu(interaction);
1651
- }
1652
- renderToolMenu(interaction) {
1653
- const lines = [
1654
- theme.bold('Select which tools are enabled (changes apply on next launch):'),
1655
- ...interaction.options.map((option, index) => this.formatToolOptionLine(option, index, interaction.selection)),
1656
- '',
1657
- 'Enter the number to toggle, "save" to persist, "defaults" to restore recommended tools, or "cancel".',
1658
- ];
1659
- display.showSystemMessage(lines.join('\n'));
1660
- }
1661
- formatToolOptionLine(option, index, selection) {
1662
- const enabled = selection.has(option.id);
1663
- const checkbox = enabled ? theme.primary('[x]') : theme.ui.muted('[ ]');
1664
- const details = [option.description];
1665
- if (option.requiresSecret) {
1666
- const hasSecret = Boolean(getSecretValue(option.requiresSecret));
1667
- const status = hasSecret ? theme.success('API key set') : theme.warning('API key missing');
1668
- details.push(status);
1669
- }
1670
- const numberLabel = this.colorizeDropdownLine(`${index + 1}.`, index);
1671
- const optionLabel = this.colorizeDropdownLine(option.label, index);
1672
- const detailLine = this.colorizeDropdownLine(` ${details.join(' • ')}`, index);
1673
- return `${numberLabel} ${checkbox} ${optionLabel}\n${detailLine}`;
1674
- }
1675
- showAgentsMenu() {
1676
- if (!this.agentMenu) {
1677
- display.showWarning('Agent selection is not available in this CLI.');
1678
- return;
1679
- }
1680
- const lines = [
1681
- theme.bold('Select the default agent profile (changes apply on next launch):'),
1682
- ...this.agentMenu.options.map((option, index) => this.formatAgentOptionLine(option, index)),
1683
- '',
1684
- 'Enter the number to save it, or type "cancel".',
1685
- ];
1686
- display.showSystemMessage(lines.join('\n'));
1687
- this.pendingInteraction = { type: 'agent-selection', options: this.agentMenu.options };
1688
- }
1689
- formatAgentOptionLine(option, index) {
1690
- const numberLabel = this.colorizeDropdownLine(`${index + 1}. ${option.label}`, index);
1691
- if (!this.agentMenu) {
1692
- return numberLabel;
1693
- }
1694
- const badges = [];
1695
- const nextProfile = this.agentMenu.persistedProfile ?? this.agentMenu.defaultProfile;
1696
- if (option.name === nextProfile) {
1697
- badges.push(theme.primary('next launch'));
1698
- }
1699
- if (option.name === this.profile) {
1700
- badges.push(theme.success('current session'));
1701
- }
1702
- const badgeSuffix = badges.length ? ` ${badges.join(' • ')}` : '';
1703
- const rows = [
1704
- `${numberLabel}${badgeSuffix}`,
1705
- ` ${this.providerLabel(option.defaultProvider)} • ${option.defaultModel}`,
1706
- ];
1707
- if (option.description?.trim()) {
1708
- rows.push(` ${option.description.trim()}`);
1709
- }
1710
- return rows.join('\n');
1711
- }
1712
- async handleModelProviderSelection(input) {
1713
- const pending = this.pendingInteraction;
1714
- if (!pending || pending.type !== 'model-provider') {
1715
- return;
1716
- }
1717
- const trimmed = input.trim();
1718
- if (!trimmed) {
1719
- display.showWarning('Enter a number or type cancel.');
1720
- this.rl.prompt();
1721
- return;
1722
- }
1723
- if (trimmed.toLowerCase() === 'cancel') {
1724
- this.pendingInteraction = null;
1725
- display.showInfo('Model selection cancelled.');
1726
- this.rl.prompt();
1727
- return;
1728
- }
1729
- const choice = Number.parseInt(trimmed, 10);
1730
- if (!Number.isFinite(choice)) {
1731
- display.showWarning('Please enter a valid number.');
1732
- this.rl.prompt();
1733
- return;
1734
- }
1735
- const option = pending.options[choice - 1];
1736
- if (!option) {
1737
- display.showWarning('That option is not available.');
1738
- this.rl.prompt();
1739
- return;
1740
- }
1741
- this.showProviderModels(option);
1742
- this.rl.prompt();
1743
- }
1744
- async handleModelSelection(input) {
1745
- const pending = this.pendingInteraction;
1746
- if (!pending || pending.type !== 'model') {
1747
- return;
1748
- }
1749
- const trimmed = input.trim();
1750
- if (!trimmed) {
1751
- display.showWarning('Enter a number, type "back", or type "cancel".');
1752
- this.rl.prompt();
1753
- return;
1754
- }
1755
- if (trimmed.toLowerCase() === 'back') {
1756
- this.showModelMenu();
1757
- this.rl.prompt();
1758
- return;
1759
- }
1760
- if (trimmed.toLowerCase() === 'cancel') {
1761
- this.pendingInteraction = null;
1762
- display.showInfo('Model selection cancelled.');
1763
- this.rl.prompt();
1764
- return;
1765
- }
1766
- const choice = Number.parseInt(trimmed, 10);
1767
- if (!Number.isFinite(choice)) {
1768
- display.showWarning('Please enter a valid number.');
1769
- this.rl.prompt();
1770
- return;
1771
- }
1772
- const preset = pending.options[choice - 1];
1773
- if (!preset) {
1774
- display.showWarning('That option is not available.');
1775
- this.rl.prompt();
1776
- return;
1777
- }
1778
- this.pendingInteraction = null;
1779
- await this.applyModelPreset(preset);
1780
- this.rl.prompt();
1781
- }
1782
- async applyModelPreset(preset) {
1783
- try {
1784
- ensureSecretForProvider(preset.provider);
1785
- }
1786
- catch (error) {
1787
- this.handleAgentSetupError(error, () => this.applyModelPreset(preset), preset.provider);
1788
- return;
1789
- }
1790
- this.sessionState = {
1791
- provider: preset.provider,
1792
- model: preset.id,
1793
- temperature: preset.temperature,
1794
- maxTokens: preset.maxTokens,
1795
- reasoningEffort: preset.reasoningEffort,
1796
- };
1797
- this.applyPresetReasoningDefaults();
1798
- if (this.rebuildAgent()) {
1799
- display.showInfo(`Switched to ${preset.label}.`);
1800
- this.refreshBannerSessionInfo();
1801
- this.persistSessionPreference();
1802
- }
1803
- }
1804
- async handleSecretSelection(input) {
1805
- const pending = this.pendingInteraction;
1806
- if (!pending || pending.type !== 'secret-select') {
1807
- return;
1808
- }
1809
- const trimmed = input.trim();
1810
- if (!trimmed) {
1811
- display.showWarning('Enter a number or type cancel.');
1812
- this.rl.prompt();
1813
- return;
1814
- }
1815
- if (trimmed.toLowerCase() === 'cancel') {
1816
- this.pendingInteraction = null;
1817
- display.showInfo('Secret management cancelled.');
1818
- this.rl.prompt();
1819
- return;
1820
- }
1821
- const choice = Number.parseInt(trimmed, 10);
1822
- if (!Number.isFinite(choice)) {
1823
- display.showWarning('Please enter a valid number.');
1824
- this.rl.prompt();
1825
- return;
1826
- }
1827
- const secret = pending.options[choice - 1];
1828
- if (!secret) {
1829
- display.showWarning('That option is not available.');
1830
- this.rl.prompt();
1831
- return;
1832
- }
1833
- display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
1834
- this.pendingInteraction = { type: 'secret-input', secret };
1835
- this.rl.prompt();
1836
- }
1837
- async handleSecretInput(input) {
1838
- const pending = this.pendingInteraction;
1839
- if (!pending || pending.type !== 'secret-input') {
1840
- return;
1841
- }
1842
- const trimmed = input.trim();
1843
- if (!trimmed) {
1844
- display.showWarning('Enter a value or type cancel.');
1845
- this.rl.prompt();
1846
- return;
1847
- }
1848
- if (trimmed.toLowerCase() === 'cancel') {
1849
- this.pendingInteraction = null;
1850
- this.pendingSecretRetry = null;
1851
- display.showInfo('Secret unchanged.');
1852
- this.rl.prompt();
1853
- return;
1854
- }
1855
- try {
1856
- setSecretValue(pending.secret.id, trimmed);
1857
- display.showInfo(`${pending.secret.label} updated.`);
1858
- this.pendingInteraction = null;
1859
- const deferred = this.pendingSecretRetry;
1860
- this.pendingSecretRetry = null;
1861
- if (pending.secret.providers.includes(this.sessionState.provider)) {
1862
- this.rebuildAgent();
1863
- }
1864
- if (deferred) {
1865
- await deferred();
1866
- }
1867
- }
1868
- catch (error) {
1869
- const message = error instanceof Error ? error.message : String(error);
1870
- display.showError(message);
1871
- this.pendingInteraction = null;
1872
- this.pendingSecretRetry = null;
1873
- }
1874
- this.rl.prompt();
1875
- }
1876
- async processRequest(request) {
1877
- if (this.isProcessing) {
1878
- this.enqueueFollowUpAction({ type: 'request', text: request });
1879
- return;
1880
- }
1881
- if (!this.agent && !this.rebuildAgent()) {
1882
- display.showWarning('Configure an API key via /secrets before sending requests.');
1883
- return;
1884
- }
1885
- const agent = this.agent;
1886
- if (!agent) {
1887
- return;
1888
- }
1889
- this.isProcessing = true;
1890
- const requestStartTime = Date.now(); // Alpha Zero 2 timing
1891
- // Hide persistent prompt during processing to avoid UI conflicts
1892
- this.persistentPrompt.hide();
1893
- this.uiAdapter.startProcessing('Working on your request');
1894
- this.setProcessingStatus();
1895
- try {
1896
- display.newLine();
1897
- display.showThinking('Working on your request...');
1898
- // Enable streaming for real-time text output (Claude Code style)
1899
- await agent.send(request, true);
1900
- await this.awaitPendingCleanup();
1901
- this.captureHistorySnapshot();
1902
- this.autosaveIfEnabled();
1903
- // Track metrics with Alpha Zero 2
1904
- const elapsedMs = Date.now() - requestStartTime;
1905
- this.alphaZeroMetrics.recordMessage(elapsedMs);
1906
- }
1907
- catch (error) {
1908
- const handled = this.handleProviderError(error, () => this.processRequest(request));
1909
- if (!handled) {
1910
- // Pass full error object for enhanced formatting with stack trace
1911
- display.showError(error instanceof Error ? error.message : String(error), error);
1912
- }
1913
- }
1914
- finally {
1915
- display.stopThinking(false);
1916
- this.isProcessing = false;
1917
- this.uiAdapter.endProcessing('Ready for prompts');
1918
- this.setIdleStatus();
1919
- display.newLine();
1920
- // Ensure persistent prompt is visible after processing
1921
- this.persistentPrompt.show();
1922
- // CRITICAL: Ensure readline prompt is active for user input
1923
- // This is a safety net in case the caller doesn't call rl.prompt()
1924
- this.rl.prompt();
1925
- this.scheduleQueueProcessing();
1926
- this.refreshQueueIndicators();
1927
- }
1928
- }
1929
- /**
1930
- * Process a continuous/infinite loop request.
1931
- * Runs the agent in a loop until:
1932
- * 1. The agent indicates completion (no more actions needed)
1933
- * 2. User interrupts (Ctrl+C)
1934
- * 3. Maximum iterations reached (safety limit)
1935
- *
1936
- * Context is automatically managed - overflow errors trigger auto-recovery.
1937
- */
1938
- async processContinuousRequest(initialRequest) {
1939
- const MAX_ITERATIONS = 100; // Safety limit to prevent truly infinite loops
1940
- const COMPLETION_PATTERNS = [
1941
- /\b(completed?|done|finished|all.*done|task.*complete|nothing.*left|no.*more.*tasks?)\b/i,
1942
- /\b(everything.*done|all.*tasks?.*complete|successfully.*completed?)\b/i,
1943
- ];
1944
- if (this.isProcessing) {
1945
- this.enqueueFollowUpAction({ type: 'continuous', text: initialRequest });
1946
- return;
1947
- }
1948
- if (!this.agent && !this.rebuildAgent()) {
1949
- display.showWarning('Configure an API key via /secrets before sending requests.');
1950
- return;
1951
- }
1952
- const agent = this.agent;
1953
- if (!agent) {
1954
- return;
1955
- }
1956
- this.isProcessing = true;
1957
- const overallStartTime = Date.now();
1958
- // Hide persistent prompt during processing
1959
- this.persistentPrompt.hide();
1960
- display.showSystemMessage(`🔄 Starting continuous execution mode. Press Ctrl+C to stop.`);
1961
- this.uiAdapter.startProcessing('Continuous execution mode');
1962
- this.setProcessingStatus();
1963
- let iteration = 0;
1964
- let lastResponse = '';
1965
- let consecutiveNoProgress = 0;
1966
- const MAX_NO_PROGRESS = 3;
1967
- try {
1968
- // Enhance initial prompt with git context for self-improvement tasks
1969
- let currentPrompt = initialRequest;
1970
- if (this.isSelfImprovementRequest(initialRequest)) {
1971
- currentPrompt = `${initialRequest}
1972
-
1973
- IMPORTANT: You have full git access. After making improvements:
1974
- 1. Use bash to run: git status (see changes)
1975
- 2. Use bash to run: git add -A (stage changes)
1976
- 3. Use bash to run: git commit -m "descriptive message" (commit)
1977
- 4. Use bash to run: git push (when milestone reached)
1978
-
1979
- Commit frequently with descriptive messages. Push when ready.`;
1980
- }
1981
- while (iteration < MAX_ITERATIONS) {
1982
- iteration++;
1983
- display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
1984
- display.showThinking(`Working on iteration ${iteration}...`);
1985
- try {
1986
- // Send the request and capture the response
1987
- const response = await agent.send(currentPrompt, true);
1988
- await this.awaitPendingCleanup();
1989
- this.captureHistorySnapshot();
1990
- this.autosaveIfEnabled();
1991
- // Track metrics
1992
- const elapsedMs = Date.now() - overallStartTime;
1993
- this.alphaZeroMetrics.recordMessage(elapsedMs);
1994
- display.stopThinking(false);
1995
- // Check if the response indicates completion
1996
- const isComplete = COMPLETION_PATTERNS.some(pattern => pattern.test(response));
1997
- const responseChanged = response !== lastResponse;
1998
- if (isComplete && responseChanged) {
1999
- display.showSystemMessage(`\n✅ Task completed after ${iteration} iteration(s).`);
2000
- break;
2001
- }
2002
- // Check for no progress (same response multiple times)
2003
- if (!responseChanged) {
2004
- consecutiveNoProgress++;
2005
- if (consecutiveNoProgress >= MAX_NO_PROGRESS) {
2006
- display.showSystemMessage(`\n⚠️ No progress detected for ${MAX_NO_PROGRESS} iterations. Stopping.`);
2007
- break;
2008
- }
2009
- }
2010
- else {
2011
- consecutiveNoProgress = 0;
2012
- }
2013
- lastResponse = response;
2014
- // Prepare next iteration prompt - explicitly encourage git usage
2015
- currentPrompt = `Continue with the next step. Remember:
2016
- - Use bash to run git commands (git status, git add, git commit, git push)
2017
- - Commit your changes with descriptive messages after completing improvements
2018
- - Push changes when a logical milestone is reached
2019
- If all tasks are complete, say "done".`;
2020
- // Small delay between iterations to prevent rate limiting
2021
- await new Promise(resolve => setTimeout(resolve, 500));
2022
- }
2023
- catch (error) {
2024
- display.stopThinking(false);
2025
- // Handle context overflow specially - the agent should auto-recover
2026
- // but if it propagates here, we continue the loop
2027
- if (this.isContextOverflowError(error)) {
2028
- display.showSystemMessage(`⚡ Context overflow handled. Continuing with reduced context...`);
2029
- // The agent.ts should have already handled recovery
2030
- // Continue to next iteration
2031
- continue;
2032
- }
2033
- // For other errors, check if handled by provider error handler
2034
- const handled = this.handleProviderError(error, () => this.processContinuousRequest(initialRequest));
2035
- if (!handled) {
2036
- display.showError(error instanceof Error ? error.message : String(error), error);
2037
- break;
2038
- }
2039
- }
2040
- }
2041
- if (iteration >= MAX_ITERATIONS) {
2042
- display.showWarning(`\n⚠️ Reached maximum iterations (${MAX_ITERATIONS}). Stopping to prevent infinite loop.`);
2043
- }
2044
- }
2045
- finally {
2046
- const totalElapsed = Date.now() - overallStartTime;
2047
- const minutes = Math.floor(totalElapsed / 60000);
2048
- const seconds = Math.floor((totalElapsed % 60000) / 1000);
2049
- display.showSystemMessage(`\n🏁 Continuous execution completed: ${iteration} iterations, ${minutes}m ${seconds}s total`);
2050
- this.isProcessing = false;
2051
- this.uiAdapter.endProcessing('Ready for prompts');
2052
- this.setIdleStatus();
2053
- display.newLine();
2054
- this.persistentPrompt.show();
2055
- // CRITICAL: Ensure readline prompt is active for user input
2056
- // This is a safety net in case the caller doesn't call rl.prompt()
2057
- this.rl.prompt();
2058
- this.scheduleQueueProcessing();
2059
- this.refreshQueueIndicators();
2060
- }
2061
- }
2062
- /**
2063
- * Check if an error is a context overflow error
2064
- */
2065
- isContextOverflowError(error) {
2066
- if (!(error instanceof Error))
2067
- return false;
2068
- const message = error.message.toLowerCase();
2069
- return (message.includes('context length') ||
2070
- (message.includes('token') && (message.includes('limit') || message.includes('exceed') || message.includes('maximum'))) ||
2071
- message.includes('too long') ||
2072
- message.includes('too many tokens') ||
2073
- message.includes('max_tokens') ||
2074
- message.includes('context window'));
2075
- }
2076
- async awaitPendingCleanup() {
2077
- if (!this.pendingCleanup) {
2078
- return;
2079
- }
2080
- let timeoutId = null;
2081
- try {
2082
- // Add timeout to prevent indefinite hangs (10 second max wait)
2083
- const CLEANUP_TIMEOUT_MS = 10000;
2084
- const timeoutPromise = new Promise((_, reject) => {
2085
- timeoutId = setTimeout(() => reject(new Error('Cleanup timed out')), CLEANUP_TIMEOUT_MS);
2086
- });
2087
- await Promise.race([this.pendingCleanup, timeoutPromise]);
2088
- }
2089
- catch (error) {
2090
- const message = error instanceof Error ? error.message : String(error);
2091
- display.showWarning(`Context cleanup failed: ${message}`);
2092
- }
2093
- finally {
2094
- // Clear timeout to prevent leaking timers
2095
- if (timeoutId) {
2096
- clearTimeout(timeoutId);
2097
- }
2098
- this.pendingCleanup = null;
2099
- }
2100
- }
2101
- rebuildAgent() {
2102
- const previousHistory = this.agent ? this.agent.getHistory() : this.cachedHistory;
2103
- try {
2104
- ensureSecretForProvider(this.sessionState.provider);
2105
- this.runtimeSession.updateToolContext(this.sessionState);
2106
- const selection = {
2107
- provider: this.sessionState.provider,
2108
- model: this.sessionState.model,
2109
- temperature: this.sessionState.temperature,
2110
- maxTokens: this.sessionState.maxTokens,
2111
- systemPrompt: this.buildSystemPrompt(),
2112
- reasoningEffort: this.sessionState.reasoningEffort,
2113
- };
2114
- this.agent = this.runtimeSession.createAgent(selection, {
2115
- onStreamChunk: (chunk) => {
2116
- // Stream text directly to console for real-time display (Claude Code style)
2117
- // Use safe write to prevent interleaving with spinner animation frames
2118
- display.safeWrite(() => {
2119
- // Stop spinner on first chunk to avoid interference
2120
- if (display.isSpinnerActive()) {
2121
- display.stopThinking(false);
2122
- process.stdout.write('\n'); // Newline after spinner
2123
- }
2124
- process.stdout.write(chunk);
2125
- });
2126
- },
2127
- onAssistantMessage: (content, metadata) => {
2128
- const enriched = this.buildDisplayMetadata(metadata);
2129
- // Update spinner based on message type
2130
- if (metadata.isFinal) {
2131
- const parsed = this.splitThinkingResponse(content);
2132
- if (parsed?.thinking) {
2133
- const summary = this.extractThoughtSummary(parsed.thinking);
2134
- if (summary) {
2135
- display.updateThinking(`💭 ${summary}`);
2136
- }
2137
- display.showAssistantMessage(parsed.thinking, { ...enriched, isFinal: false });
2138
- }
2139
- const finalContent = parsed?.response?.trim() || content;
2140
- if (finalContent) {
2141
- display.showAssistantMessage(finalContent, enriched);
2142
- }
2143
- // Show status line at end (Claude Code style: "• Context X% used • Ready for prompts (2s)")
2144
- display.stopThinking();
2145
- // Calculate context usage
2146
- let contextInfo;
2147
- if (enriched.contextWindowTokens && metadata.usage) {
2148
- const total = this.totalTokens(metadata.usage);
2149
- if (total && total > 0) {
2150
- const percentage = Math.round((total / enriched.contextWindowTokens) * 100);
2151
- contextInfo = { percentage, tokens: total };
2152
- }
2153
- }
2154
- display.showStatusLine('Ready for prompts', enriched.elapsedMs, contextInfo);
2155
- }
2156
- else {
2157
- // Non-final message = narrative text before tool calls (Claude Code style)
2158
- // Stop spinner and show the narrative text directly
2159
- display.stopThinking();
2160
- display.showNarrative(content.trim());
2161
- // Restart spinner for tool execution
2162
- display.showThinking('Working on your request...');
2163
- return;
2164
- }
2165
- const cleanup = this.handleContextTelemetry(metadata, enriched);
2166
- if (cleanup) {
2167
- this.pendingCleanup = cleanup;
2168
- }
2169
- },
2170
- onContextSquishing: (message) => {
2171
- // Show notification in UI when auto context squishing occurs
2172
- display.showSystemMessage(`🔄 ${message}`);
2173
- this.statusTracker.pushOverride('context-squish', 'Auto-squishing context', {
2174
- detail: 'Reducing conversation history to fit within token limits',
2175
- tone: 'warning',
2176
- });
2177
- },
2178
- onContextRecovery: (attempt, maxAttempts, message) => {
2179
- // Show recovery progress in UI
2180
- display.showSystemMessage(`⚡ Context Recovery (${attempt}/${maxAttempts}): ${message}`);
2181
- // Update persistent prompt to show recovery status
2182
- this.persistentPrompt.updateStatusBar({
2183
- contextUsage: 100, // Show as full during recovery
2184
- });
2185
- },
2186
- onContextPruned: (removedCount, stats) => {
2187
- // Clear squish overlay if active
2188
- this.statusTracker.clearOverride('context-squish');
2189
- // Show notification that context was pruned
2190
- const method = stats['method'];
2191
- const percentage = stats['percentage'];
2192
- if (method === 'emergency-recovery') {
2193
- display.showSystemMessage(`✅ Context recovery complete. Removed ${removedCount} messages. Context now at ${percentage ?? 'unknown'}%`);
2194
- }
2195
- // Update context usage in UI
2196
- if (typeof percentage === 'number') {
2197
- this.uiAdapter.updateContextUsage(percentage);
2198
- this.persistentPrompt.updateStatusBar({ contextUsage: percentage });
2199
- }
2200
- },
2201
- });
2202
- const historyToLoad = (this.pendingHistoryLoad && this.pendingHistoryLoad.length
2203
- ? this.pendingHistoryLoad
2204
- : previousHistory && previousHistory.length
2205
- ? previousHistory
2206
- : null) ?? null;
2207
- if (historyToLoad && historyToLoad.length) {
2208
- this.agent.loadHistory(historyToLoad);
2209
- this.cachedHistory = historyToLoad;
2210
- }
2211
- else {
2212
- this.cachedHistory = [];
2213
- }
2214
- this.pendingHistoryLoad = null;
2215
- this.showSessionResumeNotice();
2216
- return true;
2217
- }
2218
- catch (error) {
2219
- this.agent = null;
2220
- this.handleAgentSetupError(error, () => this.rebuildAgent(), this.sessionState.provider);
2221
- return false;
2222
- }
2223
- }
2224
- buildSystemPrompt() {
2225
- const providerLabel = this.providerLabel(this.sessionState.provider);
2226
- const lines = [
2227
- this.baseSystemPrompt.trim(),
2228
- '',
2229
- 'ACTIVE RUNTIME METADATA:',
2230
- `- CLI profile: ${this.profileLabel} (${this.profile})`,
2231
- `- Provider: ${providerLabel} (${this.sessionState.provider})`,
2232
- `- Model: ${this.sessionState.model}`,
2233
- ];
2234
- if (typeof this.sessionState.temperature === 'number') {
2235
- lines.push(`- Temperature: ${this.sessionState.temperature}`);
2236
- }
2237
- if (typeof this.sessionState.maxTokens === 'number') {
2238
- lines.push(`- Max tokens: ${this.sessionState.maxTokens}`);
2239
- }
2240
- lines.push('', 'Use these values when describing your identity or answering model/provider questions. If anything feels stale, call the `profile_details` tool before responding.');
2241
- const thinkingDirective = this.buildThinkingDirective();
2242
- if (thinkingDirective) {
2243
- lines.push('', thinkingDirective);
2244
- }
2245
- return lines.join('\n').trim();
2246
- }
2247
- buildThinkingDirective() {
2248
- switch (this.thinkingMode) {
2249
- case 'concise':
2250
- return 'Concise thinking mode is enabled: respond directly with the final answer and skip <thinking> blocks unless the user explicitly asks for your reasoning.';
2251
- case 'extended':
2252
- return [
2253
- 'Extended thinking mode is enabled. Format every reply as:',
2254
- '<thinking>',
2255
- 'Detailed multi-step reasoning (reference tool runs/files when relevant, keep secrets redacted, no code blocks unless citing filenames).',
2256
- '</thinking>',
2257
- '<response>',
2258
- 'Final answer with actionable next steps and any code/commands requested.',
2259
- '</response>',
2260
- ].join('\n');
2261
- case 'balanced':
2262
- default:
2263
- return 'Balanced thinking mode: include a short <thinking>...</thinking> block before <response> when the reasoning is non-trivial; skip it for simple answers.';
2264
- }
2265
- }
2266
- buildDisplayMetadata(metadata) {
2267
- return {
2268
- ...metadata,
2269
- contextWindowTokens: this.activeContextWindowTokens,
2270
- };
2271
- }
2272
- handleContextTelemetry(metadata, displayMetadata) {
2273
- if (!metadata.isFinal) {
2274
- return null;
2275
- }
2276
- const windowTokens = displayMetadata.contextWindowTokens;
2277
- if (!windowTokens || windowTokens <= 0) {
2278
- return null;
2279
- }
2280
- const total = this.totalTokens(metadata.usage);
2281
- if (total === null) {
2282
- return null;
2283
- }
2284
- const usageRatio = total / windowTokens;
2285
- // Always update context usage in the UI
2286
- const percentUsed = Math.round(usageRatio * 100);
2287
- this.uiAdapter.updateContextUsage(percentUsed);
2288
- // Update persistent prompt status bar with context usage
2289
- this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
2290
- if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
2291
- return null;
2292
- }
2293
- if (!this.agent || this.cleanupInProgress) {
2294
- return null;
2295
- }
2296
- return this.runContextCleanup(windowTokens, total);
2297
- }
2298
- totalTokens(usage) {
2299
- if (!usage) {
2300
- return null;
2301
- }
2302
- if (typeof usage.totalTokens === 'number' && Number.isFinite(usage.totalTokens)) {
2303
- return usage.totalTokens;
2304
- }
2305
- const input = typeof usage.inputTokens === 'number' ? usage.inputTokens : 0;
2306
- const output = typeof usage.outputTokens === 'number' ? usage.outputTokens : 0;
2307
- const sum = input + output;
2308
- return sum > 0 ? sum : null;
2309
- }
2310
- async runContextCleanup(windowTokens, totalTokens) {
2311
- if (!this.agent) {
2312
- return;
2313
- }
2314
- this.cleanupInProgress = true;
2315
- const cleanupStatusId = 'context-cleanup';
2316
- let cleanupOverlayActive = false;
2317
- try {
2318
- const history = this.agent.getHistory();
2319
- const { system, conversation } = this.partitionHistory(history);
2320
- if (!conversation.length) {
2321
- return;
2322
- }
2323
- const preserveCount = Math.min(conversation.length, CONTEXT_RECENT_MESSAGE_COUNT);
2324
- const preserved = conversation.slice(conversation.length - preserveCount);
2325
- const toSummarize = conversation.slice(0, conversation.length - preserveCount);
2326
- if (!toSummarize.length) {
2327
- return;
2328
- }
2329
- cleanupOverlayActive = true;
2330
- this.statusTracker.pushOverride(cleanupStatusId, 'Running context cleanup', {
2331
- detail: `Summarizing ${toSummarize.length} earlier messages`,
2332
- tone: 'warning',
2333
- });
2334
- const percentUsed = Math.round((totalTokens / windowTokens) * 100);
2335
- // Update context usage in unified UI
2336
- this.uiAdapter.updateContextUsage(percentUsed);
2337
- // Update persistent prompt status bar with context usage
2338
- this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
2339
- display.showSystemMessage([
2340
- `Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
2341
- `(${percentUsed}% full). Running automatic cleanup...`,
2342
- ].join(' '));
2343
- const summary = await this.buildContextSummary(toSummarize);
2344
- if (!summary) {
2345
- throw new Error('Summary could not be generated.');
2346
- }
2347
- const trimmed = this.buildTrimmedHistory(system, summary, preserved);
2348
- this.agent.loadHistory(trimmed);
2349
- display.showSystemMessage(`Context cleanup complete. Summarized ${toSummarize.length} earlier messages and preserved the latest ${preserved.length}.`);
2350
- }
2351
- finally {
2352
- if (cleanupOverlayActive) {
2353
- this.statusTracker.clearOverride(cleanupStatusId);
2354
- }
2355
- this.cleanupInProgress = false;
2356
- }
2357
- }
2358
- partitionHistory(history) {
2359
- const system = [];
2360
- const conversation = [];
2361
- for (const message of history) {
2362
- if (message.role === 'system') {
2363
- if (system.length === 0) {
2364
- system.push(message);
2365
- }
2366
- continue;
2367
- }
2368
- conversation.push(message);
2369
- }
2370
- return { system, conversation };
2371
- }
2372
- async buildContextSummary(messages) {
2373
- const chunks = this.buildSummaryChunks(messages);
2374
- if (!chunks.length) {
2375
- return null;
2376
- }
2377
- const summarizer = this.runtimeSession.createAgent({
2378
- provider: this.sessionState.provider,
2379
- model: this.sessionState.model,
2380
- temperature: 0,
2381
- maxTokens: Math.min(this.sessionState.maxTokens ?? CONTEXT_CLEANUP_MAX_OUTPUT_TOKENS, CONTEXT_CLEANUP_MAX_OUTPUT_TOKENS),
2382
- systemPrompt: CONTEXT_CLEANUP_SYSTEM_PROMPT,
2383
- }, {});
2384
- let runningSummary = '';
2385
- for (const chunk of chunks) {
2386
- const prompt = this.buildSummaryPrompt(chunk, runningSummary);
2387
- runningSummary = (await summarizer.send(prompt)).trim();
2388
- summarizer.clearHistory();
2389
- }
2390
- return runningSummary || null;
2391
- }
2392
- buildSummaryChunks(messages) {
2393
- const serialized = messages.map((message) => this.serializeMessage(message)).filter((text) => text.length > 0);
2394
- if (!serialized.length) {
2395
- return [];
2396
- }
2397
- const chunks = [];
2398
- let buffer = '';
2399
- for (const entry of serialized) {
2400
- const segment = buffer ? `\n\n${entry}` : entry;
2401
- if (buffer && buffer.length + segment.length > CONTEXT_CLEANUP_CHARS_PER_CHUNK) {
2402
- chunks.push(buffer.trim());
2403
- buffer = entry;
2404
- continue;
2405
- }
2406
- buffer += buffer ? `\n\n${entry}` : entry;
2407
- }
2408
- if (buffer.trim()) {
2409
- chunks.push(buffer.trim());
2410
- }
2411
- return chunks;
2412
- }
2413
- serializeMessage(message) {
2414
- const role = this.describeRole(message);
2415
- const parts = [`${role}:`];
2416
- const content = message.content?.trim() ?? '';
2417
- if (content) {
2418
- parts.push(content);
2419
- }
2420
- if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
2421
- parts.push('Tool calls:', ...message.toolCalls.map((call) => {
2422
- const args = JSON.stringify(call.arguments ?? {});
2423
- return `- ${call.name} ${args}`;
2424
- }));
2425
- }
2426
- return parts.join('\n').trim();
2427
- }
2428
- describeRole(message) {
2429
- switch (message.role) {
2430
- case 'assistant':
2431
- return 'Assistant';
2432
- case 'user':
2433
- return 'User';
2434
- case 'tool':
2435
- return `Tool(${message.name ?? message.toolCallId ?? 'result'})`;
2436
- case 'system':
2437
- default:
2438
- return 'System';
2439
- }
2440
- }
2441
- buildSummaryPrompt(chunk, existingSummary) {
2442
- const sections = [];
2443
- if (existingSummary) {
2444
- sections.push(`Existing summary:\n${existingSummary}`);
2445
- }
2446
- sections.push(`Conversation chunk:\n${chunk}`);
2447
- sections.push([
2448
- 'Instructions:',
2449
- '- Merge the chunk into the running summary.',
2450
- '- Preserve critical TODOs, bugs, test gaps, and file references.',
2451
- '- Call out what is resolved vs. still pending.',
2452
- '- Keep the output concise (<= 200 words) using short headings or bullets.',
2453
- ].join('\n'));
2454
- return sections.join('\n\n');
2455
- }
2456
- buildTrimmedHistory(systemMessages, summary, preserved) {
2457
- const history = [];
2458
- if (systemMessages.length > 0) {
2459
- history.push(systemMessages[0]);
2460
- }
2461
- else {
2462
- history.push({ role: 'system', content: this.buildSystemPrompt() });
2463
- }
2464
- history.push({
2465
- role: 'system',
2466
- content: [
2467
- 'Condensed context summary (auto cleanup):',
2468
- summary.trim(),
2469
- `Last updated: ${new Date().toISOString()}`,
2470
- ].join('\n\n'),
2471
- });
2472
- history.push(...preserved);
2473
- return history;
2474
- }
2475
- handleAgentSetupError(error, retryAction, providerOverride) {
2476
- this.pendingInteraction = null;
2477
- const provider = providerOverride ?? this.sessionState.provider;
2478
- const apiKeyIssue = detectApiKeyError(error, provider);
2479
- if (apiKeyIssue) {
2480
- this.handleApiKeyIssue(apiKeyIssue, retryAction);
2481
- return;
2482
- }
2483
- this.pendingSecretRetry = null;
2484
- const message = error instanceof Error ? error.message : String(error);
2485
- display.showError(message);
2486
- }
2487
- handleProviderError(error, retryAction) {
2488
- const apiKeyIssue = detectApiKeyError(error, this.sessionState.provider);
2489
- if (!apiKeyIssue) {
2490
- return false;
2491
- }
2492
- this.handleApiKeyIssue(apiKeyIssue, retryAction);
2493
- return true;
2494
- }
2495
- handleApiKeyIssue(info, retryAction) {
2496
- const secret = info.secret ?? null;
2497
- const providerLabel = info.provider ? this.providerLabel(info.provider) : 'the selected provider';
2498
- if (!secret) {
2499
- this.pendingSecretRetry = null;
2500
- const guidance = 'Run "/secrets" to configure the required API key or export it (e.g., EXPORT KEY=value) before launching the CLI.';
2501
- const baseMessage = info.type === 'missing'
2502
- ? `An API key is required before using ${providerLabel}.`
2503
- : `API authentication failed for ${providerLabel}.`;
2504
- display.showWarning(`${baseMessage} ${guidance}`.trim());
2505
- return;
2506
- }
2507
- const isMissing = info.type === 'missing';
2508
- if (!isMissing && info.message && info.message.trim()) {
2509
- display.showWarning(info.message.trim());
2510
- }
2511
- const prefix = isMissing
2512
- ? `${secret.label} is required before you can use ${providerLabel}.`
2513
- : `${secret.label} appears to be invalid for ${providerLabel}.`;
2514
- display.showWarning(prefix);
2515
- this.pendingSecretRetry = retryAction ?? null;
2516
- this.pendingInteraction = { type: 'secret-input', secret };
2517
- this.showSecretGuidance(secret, isMissing);
2518
- }
2519
- showSecretGuidance(secret, promptForInput) {
2520
- const lines = [];
2521
- if (promptForInput) {
2522
- lines.push(`Enter a new value for ${secret.label} or type "cancel".`);
2523
- }
2524
- else {
2525
- lines.push(`Update the stored value for ${secret.label} or type "cancel".`);
2526
- }
2527
- lines.push(`Tip: run "/secrets" anytime to manage credentials or export ${secret.envVar}=<value> before launching the CLI.`);
2528
- display.showSystemMessage(lines.join('\n'));
2529
- }
2530
- colorizeDropdownLine(text, index) {
2531
- if (!DROPDOWN_COLORS.length) {
2532
- return text;
2533
- }
2534
- const color = DROPDOWN_COLORS[index % DROPDOWN_COLORS.length];
2535
- return color(text);
2536
- }
2537
- findModelPreset(modelId) {
2538
- return MODEL_PRESETS.find((preset) => preset.id === modelId);
2539
- }
2540
- applyPresetReasoningDefaults() {
2541
- if (this.sessionState.reasoningEffort) {
2542
- return;
2543
- }
2544
- const preset = this.findModelPreset(this.sessionState.model);
2545
- if (preset?.reasoningEffort) {
2546
- this.sessionState.reasoningEffort = preset.reasoningEffort;
2547
- }
2548
- }
2549
- refreshBannerSessionInfo() {
2550
- const nextState = {
2551
- model: this.sessionState.model,
2552
- provider: this.sessionState.provider,
2553
- };
2554
- const previous = this.bannerSessionState;
2555
- if (previous && previous.model === nextState.model && previous.provider === nextState.provider) {
2556
- return;
2557
- }
2558
- this.refreshContextGauge();
2559
- display.updateSessionInfo(nextState.model, nextState.provider);
2560
- if (!this.isProcessing) {
2561
- this.setIdleStatus();
2562
- }
2563
- this.bannerSessionState = nextState;
2564
- }
2565
- providerLabel(id) {
2566
- return PROVIDER_LABELS[id] ?? id;
2567
- }
2568
- agentMenuLabel(name) {
2569
- if (!this.agentMenu) {
2570
- return name;
2571
- }
2572
- const entry = this.agentMenu.options.find((option) => option.name === name);
2573
- return entry?.label ?? name;
2574
- }
2575
- updatePersistentPromptFileChanges() {
2576
- const summary = this._fileChangeTracker.getSummary();
2577
- if (summary.files === 0) {
2578
- return;
2579
- }
2580
- const fileChangesText = `${summary.files} file${summary.files === 1 ? '' : 's'} +${summary.additions} -${summary.removals}`;
2581
- this.persistentPrompt.updateStatusBar({ fileChanges: fileChangesText });
2582
- }
2583
- extractThoughtSummary(thought) {
2584
- // Extract first non-empty line
2585
- const lines = thought?.split('\n').filter(line => line.trim()) ?? [];
2586
- if (!lines.length) {
2587
- return null;
2588
- }
2589
- // Remove common thought prefixes
2590
- const cleaned = lines[0]
2591
- .trim()
2592
- .replace(/^(Thinking|Analyzing|Considering|Looking at|Let me)[:.\s]+/i, '')
2593
- .replace(/^I (should|need to|will|am)[:.\s]+/i, '')
2594
- .trim();
2595
- if (!cleaned) {
2596
- return null;
2597
- }
2598
- // Truncate to reasonable length
2599
- const maxLength = 50;
2600
- return cleaned.length > maxLength
2601
- ? cleaned.slice(0, maxLength - 3) + '...'
2602
- : cleaned;
2603
- }
2604
- splitThinkingResponse(content) {
2605
- if (!content?.includes('<thinking') && !content?.includes('<response')) {
2606
- return null;
2607
- }
2608
- const thinkingMatch = /<thinking>([\s\S]*?)<\/thinking>/i.exec(content);
2609
- const responseMatch = /<response>([\s\S]*?)<\/response>/i.exec(content);
2610
- if (!thinkingMatch && !responseMatch) {
2611
- return null;
2612
- }
2613
- const thinkingBody = thinkingMatch?.[1]?.trim() ?? null;
2614
- let responseBody = responseMatch?.[1]?.trim();
2615
- if (!responseBody) {
2616
- responseBody = content
2617
- .replace(thinkingMatch?.[0] ?? '', '')
2618
- .replace(/<\/?response>/gi, '')
2619
- .trim();
2620
- }
2621
- return {
2622
- thinking: thinkingBody && thinkingBody.length ? thinkingBody : null,
2623
- response: responseBody ?? '',
2624
- };
2625
- }
2626
- persistSessionPreference() {
2627
- saveModelPreference(this.profile, {
2628
- provider: this.sessionState.provider,
2629
- model: this.sessionState.model,
2630
- temperature: this.sessionState.temperature,
2631
- maxTokens: this.sessionState.maxTokens,
2632
- reasoningEffort: this.sessionState.reasoningEffort,
2633
- });
2634
- }
2635
- /**
2636
- * Handle the /provider command to switch AI providers.
2637
- */
2638
- async handleProviderCommand(input) {
2639
- const tokens = input.trim().split(/\s+/).slice(1);
2640
- const targetProvider = tokens[0]?.toLowerCase();
2641
- if (!targetProvider) {
2642
- this.showConfiguredProviders();
2643
- return;
2644
- }
2645
- // Find matching provider
2646
- const providerOptions = this.buildProviderOptions();
2647
- const match = providerOptions.find((opt) => opt.provider.toLowerCase() === targetProvider || opt.label.toLowerCase() === targetProvider);
2648
- if (!match) {
2649
- display.showWarning(`Unknown provider "${targetProvider}".`);
2650
- const available = providerOptions.map((opt) => opt.provider).join(', ');
2651
- display.showInfo(`Available providers: ${available}`);
2652
- return;
2653
- }
2654
- if (match.provider === this.sessionState.provider) {
2655
- display.showInfo(`Already using ${match.label}.`);
2656
- return;
2657
- }
2658
- // Get default model for the provider
2659
- const models = MODEL_PRESETS.filter((preset) => preset.provider === match.provider);
2660
- if (!models.length) {
2661
- display.showWarning(`No models available for ${match.label}.`);
2662
- return;
2663
- }
2664
- const defaultModel = models[0];
2665
- const oldProvider = this.sessionState.provider;
2666
- const oldModel = this.sessionState.model;
2667
- // Update session state
2668
- this.sessionState = {
2669
- provider: match.provider,
2670
- model: defaultModel.id,
2671
- temperature: defaultModel.temperature,
2672
- maxTokens: defaultModel.maxTokens,
2673
- reasoningEffort: defaultModel.reasoningEffort,
2674
- };
2675
- // Rebuild agent with new provider
2676
- if (this.rebuildAgent()) {
2677
- this.persistSessionPreference();
2678
- this.refreshBannerSessionInfo();
2679
- display.showInfo(`Switched from ${this.providerLabel(oldProvider)}/${oldModel} to ${match.label}/${defaultModel.id}`);
2680
- }
2681
- else {
2682
- // Revert on failure
2683
- this.sessionState.provider = oldProvider;
2684
- this.sessionState.model = oldModel;
2685
- display.showError(`Failed to switch to ${match.label}. Reverted to ${this.providerLabel(oldProvider)}.`);
2686
- }
2687
- }
2688
- /**
2689
- * Discover models from provider APIs.
2690
- */
2691
- async discoverModelsCommand() {
2692
- display.showInfo('Discovering models from provider APIs...');
2693
- try {
2694
- const result = await discoverAllModels();
2695
- const lines = [
2696
- theme.bold('Model Discovery Results:'),
2697
- '',
2698
- ];
2699
- for (const providerResult of result.results) {
2700
- if (providerResult.success) {
2701
- const count = providerResult.models.length;
2702
- lines.push(` ${theme.success('✓')} ${providerResult.provider}: ${count} model${count === 1 ? '' : 's'} found`);
2703
- // Show top 3 models
2704
- const topModels = providerResult.models.slice(0, 3).map(m => m.id);
2705
- if (topModels.length > 0) {
2706
- lines.push(` ${theme.secondary(topModels.join(', '))}${providerResult.models.length > 3 ? '...' : ''}`);
2707
- }
2708
- }
2709
- else {
2710
- const errorMsg = providerResult.error || 'Unknown error';
2711
- lines.push(` ${theme.warning('⚠')} ${providerResult.provider}: ${errorMsg}`);
2712
- }
2713
- }
2714
- lines.push('');
2715
- lines.push(`Total: ${result.totalModelsDiscovered} models discovered`);
2716
- if (result.errors.length > 0) {
2717
- lines.push('');
2718
- lines.push(theme.secondary('Some providers had errors. Check API keys with /secrets.'));
2719
- }
2720
- display.showSystemMessage(lines.join('\n'));
2721
- }
2722
- catch (error) {
2723
- const message = error instanceof Error ? error.message : String(error);
2724
- display.showError(`Discovery failed: ${message}`);
2725
- }
2726
- }
2727
- /**
2728
- * Show all configured providers and their status.
2729
- */
2730
- showConfiguredProviders() {
2731
- const providerOptions = this.buildProviderOptions();
2732
- if (!providerOptions.length) {
2733
- display.showWarning('No providers are configured.');
2734
- display.showInfo('Set an API key: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, etc.');
2735
- return;
2736
- }
2737
- const lines = [
2738
- theme.bold('Configured Providers:'),
2739
- '',
2740
- ...providerOptions.map((option) => {
2741
- const isCurrent = option.provider === this.sessionState.provider;
2742
- const marker = isCurrent ? theme.primary(' ← active') : '';
2743
- const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
2744
- return ` ${theme.success('✓')} ${option.label} (${option.provider}) — ${countLabel}${marker}`;
2745
- }),
2746
- '',
2747
- 'Switch with: /provider <name>',
2748
- 'Example: /provider xai',
2749
- ];
2750
- display.showSystemMessage(lines.join('\n'));
2751
- }
2752
- /**
2753
- * Handle the /local command for local/air-gapped LLM management.
2754
- */
2755
- async handleLocalCommand(input) {
2756
- const tokens = input.trim().split(/\s+/).slice(1);
2757
- const subcommand = (tokens[0] ?? '').toLowerCase();
2758
- switch (subcommand) {
2759
- case '':
2760
- case 'scan':
2761
- case 'status':
2762
- await this.scanLocalProviders();
2763
- break;
2764
- case 'use':
2765
- const provider = tokens[1]?.toLowerCase();
2766
- if (!provider) {
2767
- display.showWarning('Usage: /local use <provider>');
2768
- display.showInfo('Example: /local use ollama');
2769
- return;
2770
- }
2771
- await this.handleProviderCommand(`/provider ${provider}`);
2772
- break;
2773
- default:
2774
- this.showLocalHelp();
2775
- break;
2776
- }
2777
- }
2778
- /**
2779
- * Scan for running local LLM servers.
2780
- */
2781
- async scanLocalProviders() {
2782
- const localProviders = [
2783
- { id: 'ollama', label: 'Ollama', url: 'http://localhost:11434', envVar: 'OLLAMA_BASE_URL' },
2784
- { id: 'lmstudio', label: 'LM Studio', url: 'http://localhost:1234', envVar: 'LMSTUDIO_BASE_URL' },
2785
- { id: 'vllm', label: 'vLLM', url: 'http://localhost:8000', envVar: 'VLLM_BASE_URL' },
2786
- { id: 'llamacpp', label: 'llama.cpp', url: 'http://localhost:8080', envVar: 'LLAMACPP_BASE_URL' },
2787
- ];
2788
- display.showInfo('Scanning for local LLM servers...');
2789
- const results = [];
2790
- for (const provider of localProviders) {
2791
- const baseUrl = process.env[provider.envVar] || provider.url;
2792
- try {
2793
- const controller = new AbortController();
2794
- const timeout = setTimeout(() => controller.abort(), 2000);
2795
- const response = await fetch(`${baseUrl}/api/tags`, {
2796
- signal: controller.signal,
2797
- }).catch(() => fetch(`${baseUrl}/v1/models`, { signal: controller.signal }));
2798
- clearTimeout(timeout);
2799
- if (response.ok) {
2800
- const data = await response.json();
2801
- const models = data.models?.map((m) => m.name) ?? [];
2802
- results.push({ provider, running: true, models });
2803
- }
2804
- else {
2805
- results.push({ provider, running: false });
2806
- }
2807
- }
2808
- catch {
2809
- results.push({ provider, running: false });
2810
- }
2811
- }
2812
- const running = results.filter((r) => r.running);
2813
- const stopped = results.filter((r) => !r.running);
2814
- const lines = [theme.bold('Local LLM Servers:'), ''];
2815
- if (running.length) {
2816
- lines.push(theme.success('Running:'));
2817
- for (const r of running) {
2818
- const modelCount = r.models?.length ?? 0;
2819
- const modelInfo = modelCount ? ` (${modelCount} model${modelCount === 1 ? '' : 's'})` : '';
2820
- lines.push(` ${theme.success('●')} ${r.provider.label}${modelInfo}`);
2821
- if (r.models?.length) {
2822
- for (const model of r.models.slice(0, 5)) {
2823
- lines.push(` • ${model}`);
2824
- }
2825
- if (r.models.length > 5) {
2826
- lines.push(` • ... and ${r.models.length - 5} more`);
2827
- }
2828
- }
2829
- }
2830
- lines.push('');
2831
- }
2832
- if (stopped.length) {
2833
- lines.push(theme.secondary('Not detected:'));
2834
- for (const r of stopped) {
2835
- lines.push(` ${theme.ui.muted('○')} ${r.provider.label}`);
2836
- }
2837
- lines.push('');
2838
- }
2839
- if (running.length) {
2840
- lines.push(`Switch with: /local use ${running[0].provider.id}`);
2841
- }
2842
- else {
2843
- lines.push('No local servers running.');
2844
- lines.push('Install Ollama: https://ollama.ai');
2845
- }
2846
- display.showSystemMessage(lines.join('\n'));
2847
- }
2848
- /**
2849
- * Show help for /local commands.
2850
- */
2851
- showLocalHelp() {
2852
- const lines = [
2853
- theme.bold('/local Commands:'),
2854
- '',
2855
- ' /local Scan for running local LLM servers',
2856
- ' /local status Same as /local (scan for servers)',
2857
- ' /local use <name> Switch to a local provider',
2858
- '',
2859
- 'Supported Local Providers:',
2860
- ' ollama - Easy-to-use local LLM runner (recommended)',
2861
- ' lmstudio - GUI-based local model serving',
2862
- ' vllm - High-throughput production serving',
2863
- ' llamacpp - Lightweight llama.cpp server',
2864
- '',
2865
- 'Example:',
2866
- ' /local # Scan for running servers',
2867
- ' /local use ollama # Switch to Ollama',
2868
- ];
2869
- display.showSystemMessage(lines.join('\n'));
2870
- }
2871
- enableBracketedPasteMode() {
2872
- if (!input.isTTY || !output.isTTY) {
2873
- return false;
2874
- }
2875
- try {
2876
- output.write(BRACKETED_PASTE_ENABLE);
2877
- return true;
2878
- }
2879
- catch (error) {
2880
- const message = error instanceof Error ? error.message : String(error);
2881
- display.showWarning(`Unable to enable bracketed paste: ${message}`);
2882
- return false;
2883
- }
2884
- }
2885
- disableBracketedPasteMode() {
2886
- if (!this.bracketedPasteEnabled || !output.isTTY) {
2887
- return;
2888
- }
2889
- try {
2890
- output.write(BRACKETED_PASTE_DISABLE);
2891
- }
2892
- finally {
2893
- this.bracketedPasteEnabled = false;
2894
- this.bracketedPaste.reset();
2895
- }
2896
- }
2897
- }
2898
- function setsEqual(first, second) {
2899
- if (first.size !== second.size) {
2900
- return false;
2901
- }
2902
- for (const entry of first) {
2903
- if (!second.has(entry)) {
2904
- return false;
2905
- }
2906
- }
2907
- return true;
2908
- }