erosolar-cli 1.7.383 → 1.7.386
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/contextManager.d.ts +4 -0
- package/dist/core/contextManager.d.ts.map +1 -1
- package/dist/core/contextManager.js +16 -0
- package/dist/core/contextManager.js.map +1 -1
- package/dist/core/secretStore.d.ts +1 -0
- package/dist/core/secretStore.d.ts.map +1 -1
- package/dist/core/secretStore.js +3 -0
- package/dist/core/secretStore.js.map +1 -1
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +9 -0
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +6 -0
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +7 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/runtime/agentHost.d.ts +3 -1
- package/dist/runtime/agentHost.d.ts.map +1 -1
- package/dist/runtime/agentHost.js +3 -0
- package/dist/runtime/agentHost.js.map +1 -1
- package/dist/runtime/agentSession.d.ts +2 -1
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +1 -0
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/runtime/universal.d.ts +2 -1
- package/dist/runtime/universal.d.ts.map +1 -1
- package/dist/runtime/universal.js +1 -0
- package/dist/runtime/universal.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +34 -0
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +424 -139
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +5 -0
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -0
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +43 -0
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +273 -28
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +9 -0
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +8 -2
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/tools/fileTools.d.ts.map +1 -1
- package/dist/tools/fileTools.js +29 -5
- package/dist/tools/fileTools.js.map +1 -1
- package/dist/tools/grepTools.d.ts.map +1 -1
- package/dist/tools/grepTools.js +22 -4
- package/dist/tools/grepTools.js.map +1 -1
- package/dist/tools/searchTools.d.ts.map +1 -1
- package/dist/tools/searchTools.js +47 -13
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/tools/webTools.d.ts.map +1 -1
- package/dist/tools/webTools.js +36 -9
- package/dist/tools/webTools.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +13 -0
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +40 -3
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +1 -0
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +61 -12
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/layout.js +8 -7
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/unified/layout.js +2 -2
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
|
@@ -70,6 +70,9 @@ const PROVIDER_LABELS = Object.fromEntries(getProviders().map((provider) => [pro
|
|
|
70
70
|
// Allow enough time for paste detection to kick in before flushing buffered lines
|
|
71
71
|
const CONTEXT_USAGE_THRESHOLD = 0.9;
|
|
72
72
|
const CONTEXT_AUTOCOMPACT_PERCENT = Math.round(CONTEXT_USAGE_THRESHOLD * 100);
|
|
73
|
+
const CONTEXT_AUTOCOMPACT_FLOOR = 0.6;
|
|
74
|
+
const MIN_COMPACTION_TOKEN_SAVINGS = 200;
|
|
75
|
+
const MIN_COMPACTION_PERCENT_SAVINGS = 0.5;
|
|
73
76
|
const CONTEXT_RECENT_MESSAGE_COUNT = 12;
|
|
74
77
|
const CONTEXT_CLEANUP_CHARS_PER_CHUNK = 6000;
|
|
75
78
|
const CONTEXT_CLEANUP_MAX_OUTPUT_TOKENS = 800;
|
|
@@ -99,6 +102,8 @@ export class InteractiveShell {
|
|
|
99
102
|
pendingCleanup = null;
|
|
100
103
|
cleanupInProgress = false;
|
|
101
104
|
slashPreviewVisible = false;
|
|
105
|
+
lastLoggedPrompt = null;
|
|
106
|
+
lastLoggedPromptAt = 0;
|
|
102
107
|
skillRepository;
|
|
103
108
|
skillToolHandlers = new Map();
|
|
104
109
|
thinkingMode = 'balanced';
|
|
@@ -116,6 +121,9 @@ export class InteractiveShell {
|
|
|
116
121
|
isDrainingQueue = false;
|
|
117
122
|
activeContextWindowTokens = null;
|
|
118
123
|
latestTokenUsage = { used: null, limit: null };
|
|
124
|
+
planApprovalBridgeRegistered = false;
|
|
125
|
+
contextCompactionInFlight = false;
|
|
126
|
+
lastContextWarningLevel = null;
|
|
119
127
|
sessionPreferences;
|
|
120
128
|
autosaveEnabled;
|
|
121
129
|
autoContinueEnabled;
|
|
@@ -145,9 +153,12 @@ export class InteractiveShell {
|
|
|
145
153
|
autoBuildInFlight = false;
|
|
146
154
|
lastAutoBuildRun = null;
|
|
147
155
|
// Streaming UX tracking
|
|
156
|
+
batchedOutputMode;
|
|
148
157
|
streamingHeartbeatStart = null;
|
|
149
158
|
streamingHeartbeatFrame = 0;
|
|
150
159
|
streamingStatusLabel = null;
|
|
160
|
+
streamingUiActive = false;
|
|
161
|
+
pendingStreamBuffer = '';
|
|
151
162
|
lastStreamingElapsedSeconds = null; // Preserve final elapsed time
|
|
152
163
|
statusLineState = null;
|
|
153
164
|
statusMessageOverride = null;
|
|
@@ -169,6 +180,8 @@ export class InteractiveShell {
|
|
|
169
180
|
this.sessionRestoreConfig = config.sessionRestore ?? { mode: 'none' };
|
|
170
181
|
this._enabledPlugins = config.enabledPlugins ?? [];
|
|
171
182
|
this.version = config.version ?? '0.0.0';
|
|
183
|
+
const streamingEnv = process.env['EROSOLAR_STREAMING_UI']?.toLowerCase();
|
|
184
|
+
this.batchedOutputMode = streamingEnv !== 'on' && streamingEnv !== 'true' && streamingEnv !== '1';
|
|
172
185
|
// Alternate screen disabled - use terminal-native mode for proper scrollback and text selection
|
|
173
186
|
this.alternateScreenEnabled = false;
|
|
174
187
|
this.initializeSessionHistory();
|
|
@@ -255,6 +268,11 @@ export class InteractiveShell {
|
|
|
255
268
|
this.refreshContextGauge();
|
|
256
269
|
// Start terminal input (sets up handlers)
|
|
257
270
|
this.terminalInput.start();
|
|
271
|
+
// Allow planning tools (e.g., ProposePlan) to open the interactive approval UI just like Codex CLI
|
|
272
|
+
this.registerPlanApprovalBridge();
|
|
273
|
+
// Capture display output into the scrollback/chat log so system messages
|
|
274
|
+
// (e.g., /evolve) render above the pinned chat box.
|
|
275
|
+
this.terminalInput.registerOutputInterceptor(display);
|
|
258
276
|
// Set up command autocomplete with all slash commands
|
|
259
277
|
this.setupCommandAutocomplete();
|
|
260
278
|
// Enter alternate screen buffer when enabled, otherwise clear the main screen for layout
|
|
@@ -266,7 +284,7 @@ export class InteractiveShell {
|
|
|
266
284
|
}
|
|
267
285
|
// Stream banner first - this sets up scroll region dynamically
|
|
268
286
|
const banner = this.buildBanner();
|
|
269
|
-
this.terminalInput.streamContent(banner + '\n
|
|
287
|
+
this.terminalInput.streamContent(banner + '\n');
|
|
270
288
|
// Render chat box after banner is streamed
|
|
271
289
|
this.refreshControlBar();
|
|
272
290
|
this.terminalInput.forceRender();
|
|
@@ -360,7 +378,6 @@ export class InteractiveShell {
|
|
|
360
378
|
void maybeOfferCliUpdate(this.version);
|
|
361
379
|
}
|
|
362
380
|
if (initialPrompt) {
|
|
363
|
-
this.logUserPrompt(initialPrompt);
|
|
364
381
|
await this.processInputBlock(initialPrompt);
|
|
365
382
|
return;
|
|
366
383
|
}
|
|
@@ -388,7 +405,6 @@ export class InteractiveShell {
|
|
|
388
405
|
}
|
|
389
406
|
// DON'T clear the input here - keep it visible while streaming.
|
|
390
407
|
// The input will be cleared after streaming completes in the finally block.
|
|
391
|
-
this.logUserPrompt(approved);
|
|
392
408
|
void this.processInputBlock(approved).catch((err) => {
|
|
393
409
|
display.showError(err instanceof Error ? err.message : String(err), err);
|
|
394
410
|
});
|
|
@@ -505,15 +521,9 @@ export class InteractiveShell {
|
|
|
505
521
|
this.editGuardMode = mode;
|
|
506
522
|
this.pendingPermissionInput = null;
|
|
507
523
|
if (mode === 'plan') {
|
|
508
|
-
// Register plan approval callback for interactive UI
|
|
509
|
-
setPlanApprovalCallback((steps, explanation) => {
|
|
510
|
-
this.showPlanApproval(steps, explanation);
|
|
511
|
-
});
|
|
512
524
|
display.showSystemMessage('📋 Plan mode enabled. AI will create a plan and ask for approval before implementing.');
|
|
513
525
|
}
|
|
514
526
|
else {
|
|
515
|
-
// Unregister callback when not in plan mode
|
|
516
|
-
setPlanApprovalCallback(null);
|
|
517
527
|
if (mode === 'ask-permission') {
|
|
518
528
|
display.showSystemMessage('🛡️ Ask-to-edit mode enabled. Confirm each request before sending.');
|
|
519
529
|
}
|
|
@@ -729,6 +739,8 @@ export class InteractiveShell {
|
|
|
729
739
|
if (this.alternateScreenEnabled) {
|
|
730
740
|
this.terminalInput.exitAlternateScreen();
|
|
731
741
|
}
|
|
742
|
+
// Unregister plan approval bridge
|
|
743
|
+
setPlanApprovalCallback(null);
|
|
732
744
|
// Dispose terminal input handler
|
|
733
745
|
this.terminalInput.dispose();
|
|
734
746
|
// Dispose unified UI adapter
|
|
@@ -752,6 +764,18 @@ export class InteractiveShell {
|
|
|
752
764
|
// Write directly to stdout after exiting alternate screen to preserve the transcript
|
|
753
765
|
process.stdout.write(`\n${separator}\n${header}\n${transcript}\n${separator}\n`);
|
|
754
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Wire the planning tool suite to the interactive plan approval UI so ProposePlan behaves like Codex CLI.
|
|
769
|
+
*/
|
|
770
|
+
registerPlanApprovalBridge() {
|
|
771
|
+
if (this.planApprovalBridgeRegistered) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
setPlanApprovalCallback((steps, explanation) => {
|
|
775
|
+
this.showPlanApproval(steps, explanation);
|
|
776
|
+
});
|
|
777
|
+
this.planApprovalBridgeRegistered = true;
|
|
778
|
+
}
|
|
755
779
|
/**
|
|
756
780
|
* Update status bar message
|
|
757
781
|
*/
|
|
@@ -760,6 +784,21 @@ export class InteractiveShell {
|
|
|
760
784
|
// During streaming we still want the spinner prefix; when idle force a fast refresh.
|
|
761
785
|
this.refreshStatusLine(!this.isProcessing);
|
|
762
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* Inline menu/panel helpers (rendered in TerminalInput, not in scrollback).
|
|
789
|
+
*/
|
|
790
|
+
showInlineMenu(title, lines, tone = 'info', hint) {
|
|
791
|
+
const merged = [...lines];
|
|
792
|
+
if (hint) {
|
|
793
|
+
merged.push('', tone === 'error' ? theme.error(hint) : theme.warning(hint));
|
|
794
|
+
}
|
|
795
|
+
this.terminalInput.setInlinePanel({ title, lines: merged, tone });
|
|
796
|
+
this.terminalInput.render();
|
|
797
|
+
}
|
|
798
|
+
clearInlineMenu() {
|
|
799
|
+
this.terminalInput.setInlinePanel(null);
|
|
800
|
+
this.terminalInput.render();
|
|
801
|
+
}
|
|
763
802
|
async handleToolSettingsInput(input) {
|
|
764
803
|
const pending = this.pendingInteraction;
|
|
765
804
|
if (!pending || pending.type !== 'tool-settings') {
|
|
@@ -1105,9 +1144,9 @@ export class InteractiveShell {
|
|
|
1105
1144
|
};
|
|
1106
1145
|
}
|
|
1107
1146
|
}
|
|
1108
|
-
updateContextUsage(percentage) {
|
|
1147
|
+
updateContextUsage(percentage, autoCompactThreshold = CONTEXT_AUTOCOMPACT_PERCENT) {
|
|
1109
1148
|
this.uiAdapter.updateContextUsage(percentage);
|
|
1110
|
-
this.terminalInput.setContextUsage(percentage,
|
|
1149
|
+
this.terminalInput.setContextUsage(percentage, autoCompactThreshold);
|
|
1111
1150
|
}
|
|
1112
1151
|
refreshControlBar() {
|
|
1113
1152
|
this.terminalInput.setModeToggles({
|
|
@@ -1190,10 +1229,6 @@ export class InteractiveShell {
|
|
|
1190
1229
|
if (state.detail) {
|
|
1191
1230
|
parts.push(state.detail);
|
|
1192
1231
|
}
|
|
1193
|
-
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - state.startedAt) / 1000));
|
|
1194
|
-
if (elapsedSeconds > 0) {
|
|
1195
|
-
parts.push(`${elapsedSeconds}s`);
|
|
1196
|
-
}
|
|
1197
1232
|
return parts.join(' • ');
|
|
1198
1233
|
}
|
|
1199
1234
|
handleSlashCommandPreviewChange() {
|
|
@@ -1240,10 +1275,18 @@ export class InteractiveShell {
|
|
|
1240
1275
|
* Log user prompt to the scroll region so it's part of the conversation flow.
|
|
1241
1276
|
*/
|
|
1242
1277
|
logUserPrompt(text) {
|
|
1243
|
-
|
|
1278
|
+
const normalized = text.trim();
|
|
1279
|
+
if (!normalized)
|
|
1280
|
+
return;
|
|
1281
|
+
// Skip duplicate renders of the exact same prompt if they happen back-to-back.
|
|
1282
|
+
const now = Date.now();
|
|
1283
|
+
if (this.lastLoggedPrompt === normalized && now - this.lastLoggedPromptAt < 2000) {
|
|
1244
1284
|
return;
|
|
1285
|
+
}
|
|
1286
|
+
this.lastLoggedPrompt = normalized;
|
|
1287
|
+
this.lastLoggedPromptAt = now;
|
|
1245
1288
|
// Format with user prompt prefix and write to scroll region
|
|
1246
|
-
const formatted = `${theme.user('>')} ${
|
|
1289
|
+
const formatted = `${theme.user('>')} ${normalized}\n`;
|
|
1247
1290
|
this.terminalInput.writeToScrollRegion(formatted);
|
|
1248
1291
|
}
|
|
1249
1292
|
requestPromptRefresh(force = false) {
|
|
@@ -1265,17 +1308,49 @@ export class InteractiveShell {
|
|
|
1265
1308
|
this.promptRefreshTimer = null;
|
|
1266
1309
|
}
|
|
1267
1310
|
}
|
|
1311
|
+
resetStreamBuffer() {
|
|
1312
|
+
this.pendingStreamBuffer = '';
|
|
1313
|
+
}
|
|
1314
|
+
captureStreamChunk(chunk) {
|
|
1315
|
+
if (!this.batchedOutputMode || !chunk) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
this.pendingStreamBuffer += chunk;
|
|
1319
|
+
}
|
|
1320
|
+
consumeStreamBuffer(fallback) {
|
|
1321
|
+
if (!this.batchedOutputMode) {
|
|
1322
|
+
return fallback;
|
|
1323
|
+
}
|
|
1324
|
+
const buffered = this.pendingStreamBuffer;
|
|
1325
|
+
this.pendingStreamBuffer = '';
|
|
1326
|
+
return buffered || fallback;
|
|
1327
|
+
}
|
|
1328
|
+
currentRunElapsedMs() {
|
|
1329
|
+
if (!this.streamingHeartbeatStart) {
|
|
1330
|
+
return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
return Date.now() - this.streamingHeartbeatStart;
|
|
1333
|
+
}
|
|
1268
1334
|
startStreamingHeartbeat(label = 'Streaming') {
|
|
1269
1335
|
this.stopStreamingHeartbeat();
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
// Set up scroll region for streaming content
|
|
1273
|
-
this.terminalInput.enterStreamingScrollRegion();
|
|
1274
|
-
this.uiUpdates.setMode('streaming');
|
|
1336
|
+
this.resetStreamBuffer();
|
|
1337
|
+
this.lastStreamingElapsedSeconds = null;
|
|
1275
1338
|
this.streamingHeartbeatStart = Date.now();
|
|
1276
1339
|
this.streamingHeartbeatFrame = 0;
|
|
1277
1340
|
const initialFrame = STREAMING_SPINNER_FRAMES[this.streamingHeartbeatFrame];
|
|
1278
1341
|
this.streamingStatusLabel = this.buildStreamingStatus(`${initialFrame} ${label}`, 0);
|
|
1342
|
+
if (this.batchedOutputMode) {
|
|
1343
|
+
this.streamingUiActive = false;
|
|
1344
|
+
this.uiUpdates.setMode('processing');
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
// Enter global streaming mode - blocks all non-streaming UI output
|
|
1348
|
+
enterStreamingMode();
|
|
1349
|
+
// Set up scroll region for streaming content
|
|
1350
|
+
this.terminalInput.enterStreamingScrollRegion();
|
|
1351
|
+
this.streamingUiActive = true;
|
|
1352
|
+
this.uiUpdates.setMode('streaming');
|
|
1353
|
+
}
|
|
1279
1354
|
display.updateStreamingStatus(this.streamingStatusLabel);
|
|
1280
1355
|
this.refreshStatusLine(true);
|
|
1281
1356
|
// Periodically refresh the pinned input/status region while streaming so
|
|
@@ -1305,18 +1380,21 @@ export class InteractiveShell {
|
|
|
1305
1380
|
});
|
|
1306
1381
|
}
|
|
1307
1382
|
stopStreamingHeartbeat() {
|
|
1308
|
-
// Exit global streaming mode - allows UI to render again
|
|
1309
|
-
exitStreamingMode();
|
|
1310
1383
|
// Preserve final elapsed time before clearing heartbeat start
|
|
1311
1384
|
if (this.streamingHeartbeatStart) {
|
|
1312
1385
|
this.lastStreamingElapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
|
|
1313
1386
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1387
|
+
if (this.streamingUiActive) {
|
|
1388
|
+
// Exit global streaming mode - allows UI to render again
|
|
1389
|
+
exitStreamingMode();
|
|
1390
|
+
// Exit scroll region mode
|
|
1391
|
+
this.terminalInput.exitStreamingScrollRegion();
|
|
1392
|
+
}
|
|
1316
1393
|
this.uiUpdates.stopHeartbeat('streaming');
|
|
1317
1394
|
this.streamingHeartbeatStart = null;
|
|
1318
1395
|
this.streamingHeartbeatFrame = 0;
|
|
1319
1396
|
this.streamingStatusLabel = null;
|
|
1397
|
+
this.streamingUiActive = false;
|
|
1320
1398
|
// Clear streaming label specifically (keeps override and main status if set)
|
|
1321
1399
|
this.terminalInput.setStreamingLabel(null);
|
|
1322
1400
|
// Clear streaming status from display
|
|
@@ -1324,20 +1402,11 @@ export class InteractiveShell {
|
|
|
1324
1402
|
// Force refresh to update the input area now that streaming has ended
|
|
1325
1403
|
this.refreshStatusLine(true);
|
|
1326
1404
|
}
|
|
1327
|
-
buildStreamingStatus(label,
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
? theme.ui.muted(this.formatElapsedShort(elapsedSeconds))
|
|
1331
|
-
: null;
|
|
1405
|
+
buildStreamingStatus(label, _elapsedSeconds) {
|
|
1406
|
+
// Model + elapsed time already live in the pinned meta header; keep the streaming
|
|
1407
|
+
// status focused on the activity to avoid duplicate info.
|
|
1332
1408
|
const prefix = theme.info('⏺');
|
|
1333
|
-
|
|
1334
|
-
if (detail) {
|
|
1335
|
-
parts.push(theme.ui.muted('·'), detail);
|
|
1336
|
-
}
|
|
1337
|
-
if (elapsedLabel) {
|
|
1338
|
-
parts.push(theme.ui.muted('·'), elapsedLabel);
|
|
1339
|
-
}
|
|
1340
|
-
return `${prefix} ${parts.join(' ')}`.trim();
|
|
1409
|
+
return `${prefix} ${label}`.trim();
|
|
1341
1410
|
}
|
|
1342
1411
|
formatElapsedShort(seconds) {
|
|
1343
1412
|
if (seconds < 60) {
|
|
@@ -1493,6 +1562,10 @@ export class InteractiveShell {
|
|
|
1493
1562
|
return false;
|
|
1494
1563
|
}
|
|
1495
1564
|
switch (this.pendingInteraction.type) {
|
|
1565
|
+
case 'model-loading':
|
|
1566
|
+
display.showInfo('Still fetching model options. Please wait a moment.');
|
|
1567
|
+
this.terminalInput.render();
|
|
1568
|
+
return true;
|
|
1496
1569
|
case 'model-provider':
|
|
1497
1570
|
await this.handleModelProviderSelection(input);
|
|
1498
1571
|
return true;
|
|
@@ -3627,28 +3700,25 @@ export class InteractiveShell {
|
|
|
3627
3700
|
return lines.join('\n');
|
|
3628
3701
|
}
|
|
3629
3702
|
async showModelMenu() {
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3703
|
+
// Hold input immediately so numeric selections don't get queued as prompts while we fetch
|
|
3704
|
+
this.pendingInteraction = { type: 'model-loading' };
|
|
3705
|
+
this.showInlineMenu('Model provider', [theme.ui.muted('Fetching latest models from providers...')], 'info');
|
|
3706
|
+
try {
|
|
3707
|
+
// Fetch live models from all configured providers
|
|
3708
|
+
const providerStatuses = await quickCheckProviders();
|
|
3709
|
+
const providerOptions = this.buildProviderOptionsWithDiscovery(providerStatuses);
|
|
3710
|
+
if (!providerOptions.length) {
|
|
3711
|
+
this.pendingInteraction = null;
|
|
3712
|
+
this.showInlineMenu('Model provider', [theme.warning('No providers are available.')], 'warning');
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
this.pendingInteraction = { type: 'model-provider', options: providerOptions };
|
|
3716
|
+
this.renderProviderMenu(providerOptions);
|
|
3717
|
+
}
|
|
3718
|
+
catch (error) {
|
|
3719
|
+
this.pendingInteraction = null;
|
|
3720
|
+
this.showInlineMenu('Model provider', [theme.error('Failed to load model list. Try again in a moment.')], 'error');
|
|
3637
3721
|
}
|
|
3638
|
-
const lines = [
|
|
3639
|
-
theme.bold('Select a provider:'),
|
|
3640
|
-
...providerOptions.map((option, index) => {
|
|
3641
|
-
const isCurrent = option.provider === this.sessionState.provider;
|
|
3642
|
-
const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
|
|
3643
|
-
const latestLabel = option.latestModel ? theme.success(` (latest: ${option.latestModel})`) : '';
|
|
3644
|
-
const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}${latestLabel}`, index);
|
|
3645
|
-
const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
|
|
3646
|
-
return `${label}${suffix}`;
|
|
3647
|
-
}),
|
|
3648
|
-
'Type the number of the provider to continue, or type "cancel".',
|
|
3649
|
-
];
|
|
3650
|
-
display.showSystemMessage(lines.join('\n'));
|
|
3651
|
-
this.pendingInteraction = { type: 'model-provider', options: providerOptions };
|
|
3652
3722
|
}
|
|
3653
3723
|
buildProviderOptions() {
|
|
3654
3724
|
const counts = new Map();
|
|
@@ -3738,6 +3808,21 @@ export class InteractiveShell {
|
|
|
3738
3808
|
};
|
|
3739
3809
|
});
|
|
3740
3810
|
}
|
|
3811
|
+
renderProviderMenu(options, hint) {
|
|
3812
|
+
const lines = [
|
|
3813
|
+
theme.bold('Select a provider:'),
|
|
3814
|
+
...options.map((option, index) => {
|
|
3815
|
+
const isCurrent = option.provider === this.sessionState.provider;
|
|
3816
|
+
const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
|
|
3817
|
+
const latestLabel = option.latestModel ? theme.success(` (latest: ${option.latestModel})`) : '';
|
|
3818
|
+
const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}${latestLabel}`, index);
|
|
3819
|
+
const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
|
|
3820
|
+
return `${label}${suffix}`;
|
|
3821
|
+
}),
|
|
3822
|
+
'Type the number of the provider to continue, or type "cancel".',
|
|
3823
|
+
];
|
|
3824
|
+
this.showInlineMenu('Model provider', lines, 'info', hint);
|
|
3825
|
+
}
|
|
3741
3826
|
showProviderModels(option) {
|
|
3742
3827
|
// Start with static presets
|
|
3743
3828
|
const staticModels = MODEL_PRESETS.filter((preset) => preset.provider === option.provider);
|
|
@@ -3777,13 +3862,17 @@ export class InteractiveShell {
|
|
|
3777
3862
|
}
|
|
3778
3863
|
}
|
|
3779
3864
|
if (!allModels.length) {
|
|
3780
|
-
|
|
3865
|
+
this.showInlineMenu('Model selection', [theme.warning(`No models available for ${option.label}.`)], 'warning');
|
|
3781
3866
|
this.pendingInteraction = null;
|
|
3782
3867
|
return;
|
|
3783
3868
|
}
|
|
3869
|
+
this.renderModelSelection(allModels, option, null);
|
|
3870
|
+
this.pendingInteraction = { type: 'model', provider: option.provider, options: allModels };
|
|
3871
|
+
}
|
|
3872
|
+
renderModelSelection(models, option, hint) {
|
|
3784
3873
|
const lines = [
|
|
3785
3874
|
theme.bold(`Select a model from ${option.label}:`),
|
|
3786
|
-
...
|
|
3875
|
+
...models.map((preset, index) => {
|
|
3787
3876
|
const isCurrent = preset.id === this.sessionState.model;
|
|
3788
3877
|
const isLatest = preset.id === option.latestModel;
|
|
3789
3878
|
const latestBadge = isLatest ? theme.success(' ★ LATEST') : '';
|
|
@@ -3794,11 +3883,23 @@ export class InteractiveShell {
|
|
|
3794
3883
|
}),
|
|
3795
3884
|
'Type the number of the model to select it, type "back" to change provider, or type "cancel".',
|
|
3796
3885
|
];
|
|
3797
|
-
|
|
3798
|
-
|
|
3886
|
+
this.showInlineMenu('Model selection', lines, 'info', hint ?? undefined);
|
|
3887
|
+
}
|
|
3888
|
+
buildProviderContext(provider, models) {
|
|
3889
|
+
return {
|
|
3890
|
+
provider,
|
|
3891
|
+
label: this.providerLabel(provider),
|
|
3892
|
+
modelCount: models.length,
|
|
3893
|
+
latestModel: models[0]?.id,
|
|
3894
|
+
discoveredModels: [],
|
|
3895
|
+
};
|
|
3799
3896
|
}
|
|
3800
3897
|
showSecretsMenu() {
|
|
3801
3898
|
const definitions = listSecretDefinitions();
|
|
3899
|
+
this.pendingInteraction = { type: 'secret-select', options: definitions };
|
|
3900
|
+
this.renderSecretsMenu(definitions);
|
|
3901
|
+
}
|
|
3902
|
+
renderSecretsMenu(definitions, hint) {
|
|
3802
3903
|
const lines = [
|
|
3803
3904
|
theme.bold('Manage Secrets:'),
|
|
3804
3905
|
...definitions.map((definition, index) => {
|
|
@@ -3810,8 +3911,18 @@ export class InteractiveShell {
|
|
|
3810
3911
|
}),
|
|
3811
3912
|
'Enter the number to update a key, or type "cancel".',
|
|
3812
3913
|
];
|
|
3813
|
-
|
|
3814
|
-
|
|
3914
|
+
this.showInlineMenu('Secrets', lines, 'info', hint);
|
|
3915
|
+
}
|
|
3916
|
+
renderSecretInput(secret, hint) {
|
|
3917
|
+
const value = getSecretValue(secret.id);
|
|
3918
|
+
const status = value ? maskSecret(value) : theme.warning('not set');
|
|
3919
|
+
const providers = secret.providers.map((id) => this.providerLabel(id)).join(', ');
|
|
3920
|
+
const lines = [
|
|
3921
|
+
`${secret.label} (${providers})`,
|
|
3922
|
+
`Current: ${status}`,
|
|
3923
|
+
'Enter a new value or type "cancel".',
|
|
3924
|
+
];
|
|
3925
|
+
this.showInlineMenu('Update secret', lines, 'info', hint);
|
|
3815
3926
|
}
|
|
3816
3927
|
showToolsMenu() {
|
|
3817
3928
|
const options = getToolToggleOptions();
|
|
@@ -4191,26 +4302,23 @@ export class InteractiveShell {
|
|
|
4191
4302
|
}
|
|
4192
4303
|
const trimmed = input.trim();
|
|
4193
4304
|
if (!trimmed) {
|
|
4194
|
-
|
|
4195
|
-
this.terminalInput.render();
|
|
4305
|
+
this.renderProviderMenu(pending.options, 'Enter a number or type cancel.');
|
|
4196
4306
|
return;
|
|
4197
4307
|
}
|
|
4198
4308
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
4199
4309
|
this.pendingInteraction = null;
|
|
4200
|
-
|
|
4201
|
-
this.
|
|
4310
|
+
this.clearInlineMenu();
|
|
4311
|
+
this.updateStatusMessage('Model selection cancelled.');
|
|
4202
4312
|
return;
|
|
4203
4313
|
}
|
|
4204
4314
|
const choice = Number.parseInt(trimmed, 10);
|
|
4205
4315
|
if (!Number.isFinite(choice)) {
|
|
4206
|
-
|
|
4207
|
-
this.terminalInput.render();
|
|
4316
|
+
this.renderProviderMenu(pending.options, 'Please enter a valid number.');
|
|
4208
4317
|
return;
|
|
4209
4318
|
}
|
|
4210
4319
|
const option = pending.options[choice - 1];
|
|
4211
4320
|
if (!option) {
|
|
4212
|
-
|
|
4213
|
-
this.terminalInput.render();
|
|
4321
|
+
this.renderProviderMenu(pending.options, 'That option is not available.');
|
|
4214
4322
|
return;
|
|
4215
4323
|
}
|
|
4216
4324
|
this.showProviderModels(option);
|
|
@@ -4221,10 +4329,10 @@ export class InteractiveShell {
|
|
|
4221
4329
|
if (!pending || pending.type !== 'model') {
|
|
4222
4330
|
return;
|
|
4223
4331
|
}
|
|
4332
|
+
const providerContext = this.buildProviderContext(pending.provider, pending.options);
|
|
4224
4333
|
const trimmed = input.trim();
|
|
4225
4334
|
if (!trimmed) {
|
|
4226
|
-
|
|
4227
|
-
this.terminalInput.render();
|
|
4335
|
+
this.renderModelSelection(pending.options, providerContext, 'Enter a number, type "back", or type "cancel".');
|
|
4228
4336
|
return;
|
|
4229
4337
|
}
|
|
4230
4338
|
if (trimmed.toLowerCase() === 'back') {
|
|
@@ -4234,20 +4342,18 @@ export class InteractiveShell {
|
|
|
4234
4342
|
}
|
|
4235
4343
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
4236
4344
|
this.pendingInteraction = null;
|
|
4237
|
-
|
|
4238
|
-
this.
|
|
4345
|
+
this.clearInlineMenu();
|
|
4346
|
+
this.updateStatusMessage('Model selection cancelled.');
|
|
4239
4347
|
return;
|
|
4240
4348
|
}
|
|
4241
4349
|
const choice = Number.parseInt(trimmed, 10);
|
|
4242
4350
|
if (!Number.isFinite(choice)) {
|
|
4243
|
-
|
|
4244
|
-
this.terminalInput.render();
|
|
4351
|
+
this.renderModelSelection(pending.options, providerContext, 'Please enter a valid number.');
|
|
4245
4352
|
return;
|
|
4246
4353
|
}
|
|
4247
4354
|
const preset = pending.options[choice - 1];
|
|
4248
4355
|
if (!preset) {
|
|
4249
|
-
|
|
4250
|
-
this.terminalInput.render();
|
|
4356
|
+
this.renderModelSelection(pending.options, providerContext, 'That option is not available.');
|
|
4251
4357
|
return;
|
|
4252
4358
|
}
|
|
4253
4359
|
this.pendingInteraction = null;
|
|
@@ -4271,11 +4377,14 @@ export class InteractiveShell {
|
|
|
4271
4377
|
};
|
|
4272
4378
|
this.applyPresetReasoningDefaults();
|
|
4273
4379
|
if (this.rebuildAgent()) {
|
|
4274
|
-
|
|
4380
|
+
this.showInlineMenu('Model updated', [`Switched to ${preset.label}.`], 'success');
|
|
4275
4381
|
this.refreshBannerSessionInfo();
|
|
4276
4382
|
this.persistSessionPreference();
|
|
4277
4383
|
this.resetChatBoxAfterModelSwap();
|
|
4278
4384
|
}
|
|
4385
|
+
else {
|
|
4386
|
+
this.showInlineMenu('Model updated', [`Using ${preset.label}.`], 'info');
|
|
4387
|
+
}
|
|
4279
4388
|
}
|
|
4280
4389
|
async handleSecretSelection(input) {
|
|
4281
4390
|
const pending = this.pendingInteraction;
|
|
@@ -4284,30 +4393,27 @@ export class InteractiveShell {
|
|
|
4284
4393
|
}
|
|
4285
4394
|
const trimmed = input.trim();
|
|
4286
4395
|
if (!trimmed) {
|
|
4287
|
-
|
|
4288
|
-
this.terminalInput.render();
|
|
4396
|
+
this.renderSecretsMenu(pending.options, 'Enter a number or type cancel.');
|
|
4289
4397
|
return;
|
|
4290
4398
|
}
|
|
4291
4399
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
4292
4400
|
this.pendingInteraction = null;
|
|
4293
|
-
|
|
4294
|
-
this.
|
|
4401
|
+
this.clearInlineMenu();
|
|
4402
|
+
this.updateStatusMessage('Secret management cancelled.');
|
|
4295
4403
|
return;
|
|
4296
4404
|
}
|
|
4297
4405
|
const choice = Number.parseInt(trimmed, 10);
|
|
4298
4406
|
if (!Number.isFinite(choice)) {
|
|
4299
|
-
|
|
4300
|
-
this.terminalInput.render();
|
|
4407
|
+
this.renderSecretsMenu(pending.options, 'Please enter a valid number.');
|
|
4301
4408
|
return;
|
|
4302
4409
|
}
|
|
4303
4410
|
const secret = pending.options[choice - 1];
|
|
4304
4411
|
if (!secret) {
|
|
4305
|
-
|
|
4306
|
-
this.terminalInput.render();
|
|
4412
|
+
this.renderSecretsMenu(pending.options, 'That option is not available.');
|
|
4307
4413
|
return;
|
|
4308
4414
|
}
|
|
4309
|
-
display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
|
|
4310
4415
|
this.pendingInteraction = { type: 'secret-input', secret };
|
|
4416
|
+
this.renderSecretInput(secret);
|
|
4311
4417
|
this.terminalInput.render();
|
|
4312
4418
|
}
|
|
4313
4419
|
async handleSecretInput(input) {
|
|
@@ -4317,20 +4423,20 @@ export class InteractiveShell {
|
|
|
4317
4423
|
}
|
|
4318
4424
|
const trimmed = input.trim();
|
|
4319
4425
|
if (!trimmed) {
|
|
4320
|
-
|
|
4321
|
-
this.terminalInput.render();
|
|
4426
|
+
this.renderSecretInput(pending.secret, 'Enter a value or type cancel.');
|
|
4322
4427
|
return;
|
|
4323
4428
|
}
|
|
4324
4429
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
4325
4430
|
this.pendingInteraction = null;
|
|
4326
4431
|
this.pendingSecretRetry = null;
|
|
4327
|
-
|
|
4432
|
+
this.clearInlineMenu();
|
|
4433
|
+
this.updateStatusMessage('Secret unchanged.');
|
|
4328
4434
|
this.terminalInput.render();
|
|
4329
4435
|
return;
|
|
4330
4436
|
}
|
|
4331
4437
|
try {
|
|
4332
4438
|
setSecretValue(pending.secret.id, trimmed);
|
|
4333
|
-
|
|
4439
|
+
this.showInlineMenu('Secret updated', [`${pending.secret.label} saved.`], 'success');
|
|
4334
4440
|
this.pendingInteraction = null;
|
|
4335
4441
|
const deferred = this.pendingSecretRetry;
|
|
4336
4442
|
this.pendingSecretRetry = null;
|
|
@@ -4345,7 +4451,7 @@ export class InteractiveShell {
|
|
|
4345
4451
|
}
|
|
4346
4452
|
catch (error) {
|
|
4347
4453
|
const message = error instanceof Error ? error.message : String(error);
|
|
4348
|
-
|
|
4454
|
+
this.showInlineMenu('Secret error', [message], 'error');
|
|
4349
4455
|
this.pendingInteraction = null;
|
|
4350
4456
|
this.pendingSecretRetry = null;
|
|
4351
4457
|
}
|
|
@@ -4364,6 +4470,7 @@ export class InteractiveShell {
|
|
|
4364
4470
|
if (!agent) {
|
|
4365
4471
|
return;
|
|
4366
4472
|
}
|
|
4473
|
+
this.logUserPrompt(request);
|
|
4367
4474
|
this.isProcessing = true;
|
|
4368
4475
|
this.uiUpdates.setMode('processing');
|
|
4369
4476
|
this.terminalInput.setStreaming(true);
|
|
@@ -4383,7 +4490,8 @@ export class InteractiveShell {
|
|
|
4383
4490
|
let responseText = '';
|
|
4384
4491
|
try {
|
|
4385
4492
|
// Start streaming - no header needed, the input area already provides context
|
|
4386
|
-
this.
|
|
4493
|
+
const heartbeatLabel = this.batchedOutputMode ? 'Processing response' : 'Streaming response';
|
|
4494
|
+
this.startStreamingHeartbeat(heartbeatLabel);
|
|
4387
4495
|
responseText = await agent.send(request, true);
|
|
4388
4496
|
await this.awaitPendingCleanup();
|
|
4389
4497
|
this.captureHistorySnapshot();
|
|
@@ -4499,7 +4607,8 @@ export class InteractiveShell {
|
|
|
4499
4607
|
this.uiAdapter.startProcessing('Continuous execution mode');
|
|
4500
4608
|
this.setProcessingStatus();
|
|
4501
4609
|
// No streaming header - just start streaming directly
|
|
4502
|
-
this.
|
|
4610
|
+
const continuousLabel = this.batchedOutputMode ? 'Processing' : 'Streaming';
|
|
4611
|
+
this.startStreamingHeartbeat(continuousLabel);
|
|
4503
4612
|
let iteration = 0;
|
|
4504
4613
|
let lastResponse = '';
|
|
4505
4614
|
let consecutiveNoProgress = 0;
|
|
@@ -4527,9 +4636,10 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
|
|
|
4527
4636
|
display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
|
|
4528
4637
|
this.updateStatusMessage(`Working on iteration ${iteration}...`);
|
|
4529
4638
|
try {
|
|
4530
|
-
// Send the request and capture the response
|
|
4639
|
+
// Send the request and capture the response as a single block
|
|
4531
4640
|
display.showThinking('Responding...');
|
|
4532
4641
|
this.refreshStatusLine(true);
|
|
4642
|
+
this.resetStreamBuffer();
|
|
4533
4643
|
const response = await agent.send(currentPrompt, true);
|
|
4534
4644
|
await this.awaitPendingCleanup();
|
|
4535
4645
|
this.captureHistorySnapshot();
|
|
@@ -5030,6 +5140,7 @@ What's the next action?`;
|
|
|
5030
5140
|
// Send the error to the agent for fixing
|
|
5031
5141
|
display.showThinking('Analyzing build errors');
|
|
5032
5142
|
this.refreshStatusLine(true);
|
|
5143
|
+
this.resetStreamBuffer();
|
|
5033
5144
|
const response = await this.agent.send(prompt, true);
|
|
5034
5145
|
display.stopThinking();
|
|
5035
5146
|
this.refreshStatusLine(true);
|
|
@@ -5059,27 +5170,33 @@ What's the next action?`;
|
|
|
5059
5170
|
};
|
|
5060
5171
|
this.agent = this.runtimeSession.createAgent(selection, {
|
|
5061
5172
|
onStreamChunk: (chunk) => {
|
|
5173
|
+
if (this.batchedOutputMode) {
|
|
5174
|
+
this.captureStreamChunk(chunk);
|
|
5175
|
+
return;
|
|
5176
|
+
}
|
|
5062
5177
|
// Stream output using clean streamContent() - chat box floats below
|
|
5063
5178
|
this.terminalInput.streamContent(chunk);
|
|
5064
5179
|
},
|
|
5065
5180
|
onStreamFallback: (info) => this.handleStreamingFallback(info),
|
|
5066
5181
|
onAssistantMessage: (content, metadata) => {
|
|
5067
5182
|
const enriched = this.buildDisplayMetadata(metadata);
|
|
5183
|
+
const alreadyStreamedToUi = !this.batchedOutputMode && metadata.wasStreamed;
|
|
5184
|
+
const bufferedContent = this.consumeStreamBuffer(content);
|
|
5068
5185
|
// Update spinner based on message type
|
|
5069
5186
|
if (metadata.isFinal) {
|
|
5070
5187
|
// Skip display if content was already streamed to avoid double-display
|
|
5071
|
-
if (!
|
|
5072
|
-
const parsed = this.splitThinkingResponse(
|
|
5188
|
+
if (!alreadyStreamedToUi) {
|
|
5189
|
+
const parsed = this.splitThinkingResponse(bufferedContent);
|
|
5073
5190
|
if (parsed?.thinking) {
|
|
5074
5191
|
const summary = this.extractThoughtSummary(parsed.thinking);
|
|
5075
5192
|
if (summary) {
|
|
5076
5193
|
display.updateThinking(`💭 ${summary}`);
|
|
5077
5194
|
}
|
|
5078
|
-
display.
|
|
5195
|
+
display.showThinkingBlock(parsed.thinking, enriched.elapsedMs ?? this.currentRunElapsedMs());
|
|
5079
5196
|
}
|
|
5080
|
-
const finalContent = parsed?.response?.trim() ||
|
|
5197
|
+
const finalContent = parsed?.response?.trim() || bufferedContent.trim();
|
|
5081
5198
|
if (finalContent) {
|
|
5082
|
-
display.showAssistantMessage(finalContent, enriched);
|
|
5199
|
+
display.showAssistantMessage(finalContent, { ...enriched, isFinal: true });
|
|
5083
5200
|
}
|
|
5084
5201
|
}
|
|
5085
5202
|
// Status shown in mode controls bar - no separate status line needed
|
|
@@ -5101,8 +5218,13 @@ What's the next action?`;
|
|
|
5101
5218
|
// Stop spinner and show the narrative text directly
|
|
5102
5219
|
display.stopThinking();
|
|
5103
5220
|
// Skip display if content was already streamed to avoid double-display
|
|
5104
|
-
if (!
|
|
5105
|
-
|
|
5221
|
+
if (!alreadyStreamedToUi) {
|
|
5222
|
+
const parsed = this.splitThinkingResponse(bufferedContent);
|
|
5223
|
+
const thoughtContent = parsed?.thinking ?? parsed?.response ?? bufferedContent;
|
|
5224
|
+
const trimmed = thoughtContent.trim();
|
|
5225
|
+
if (trimmed) {
|
|
5226
|
+
display.showThinkingBlock(trimmed, enriched.elapsedMs ?? this.currentRunElapsedMs());
|
|
5227
|
+
}
|
|
5106
5228
|
}
|
|
5107
5229
|
// The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
|
|
5108
5230
|
this.requestPromptRefresh();
|
|
@@ -5304,15 +5426,17 @@ What's the next action?`;
|
|
|
5304
5426
|
};
|
|
5305
5427
|
// Always update context usage in the UI
|
|
5306
5428
|
const percentUsed = Math.round(usageRatio * 100);
|
|
5307
|
-
this.updateContextUsage(percentUsed);
|
|
5429
|
+
this.updateContextUsage(percentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
|
|
5430
|
+
this.maybeShowContextWarning(percentUsed);
|
|
5308
5431
|
this.refreshStatusLine(true);
|
|
5309
|
-
if (
|
|
5432
|
+
if (!this.agent || this.cleanupInProgress || this.contextCompactionInFlight) {
|
|
5310
5433
|
return null;
|
|
5311
5434
|
}
|
|
5312
|
-
|
|
5435
|
+
const trigger = this.shouldAutoCompactContext(usageRatio, windowTokens, total);
|
|
5436
|
+
if (!trigger) {
|
|
5313
5437
|
return null;
|
|
5314
5438
|
}
|
|
5315
|
-
return this.runContextCleanup(windowTokens, total);
|
|
5439
|
+
return this.runContextCleanup(windowTokens, total, trigger);
|
|
5316
5440
|
}
|
|
5317
5441
|
totalTokens(usage) {
|
|
5318
5442
|
if (!usage) {
|
|
@@ -5326,52 +5450,204 @@ What's the next action?`;
|
|
|
5326
5450
|
const sum = input + output;
|
|
5327
5451
|
return sum > 0 ? sum : null;
|
|
5328
5452
|
}
|
|
5329
|
-
async runContextCleanup(windowTokens, totalTokens) {
|
|
5330
|
-
|
|
5453
|
+
async runContextCleanup(windowTokens, totalTokens, trigger) {
|
|
5454
|
+
const agent = this.agent;
|
|
5455
|
+
if (!agent) {
|
|
5456
|
+
return;
|
|
5457
|
+
}
|
|
5458
|
+
const contextManager = agent.getContextManager();
|
|
5459
|
+
if (!contextManager) {
|
|
5460
|
+
return;
|
|
5461
|
+
}
|
|
5462
|
+
const history = agent.getHistory();
|
|
5463
|
+
if (history.length <= 1) {
|
|
5331
5464
|
return;
|
|
5332
5465
|
}
|
|
5333
5466
|
this.cleanupInProgress = true;
|
|
5467
|
+
this.contextCompactionInFlight = true;
|
|
5334
5468
|
const cleanupStatusId = 'context-cleanup';
|
|
5335
5469
|
let cleanupOverlayActive = false;
|
|
5336
5470
|
try {
|
|
5337
|
-
const
|
|
5338
|
-
const
|
|
5339
|
-
if (
|
|
5340
|
-
|
|
5341
|
-
}
|
|
5342
|
-
const preserveCount = Math.min(conversation.length, CONTEXT_RECENT_MESSAGE_COUNT);
|
|
5343
|
-
const preserved = conversation.slice(conversation.length - preserveCount);
|
|
5344
|
-
const toSummarize = conversation.slice(0, conversation.length - preserveCount);
|
|
5345
|
-
if (!toSummarize.length) {
|
|
5346
|
-
return;
|
|
5471
|
+
const percentUsed = Math.round((totalTokens / windowTokens) * 100);
|
|
5472
|
+
const statusDetailParts = [`${percentUsed}% full`];
|
|
5473
|
+
if (trigger?.reason) {
|
|
5474
|
+
statusDetailParts.push(trigger.reason);
|
|
5347
5475
|
}
|
|
5348
|
-
cleanupOverlayActive = true;
|
|
5349
5476
|
this.statusTracker.pushOverride(cleanupStatusId, 'Running context cleanup', {
|
|
5350
|
-
detail:
|
|
5477
|
+
detail: statusDetailParts.join(' · '),
|
|
5351
5478
|
tone: 'warning',
|
|
5352
5479
|
});
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
this.updateContextUsage(percentUsed);
|
|
5480
|
+
cleanupOverlayActive = true;
|
|
5481
|
+
const triggerReason = trigger?.reason ?? 'Context optimization';
|
|
5356
5482
|
display.showSystemMessage([
|
|
5357
5483
|
`Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
|
|
5358
|
-
`(${percentUsed}% full).
|
|
5484
|
+
`(${percentUsed}% full). Auto-compacting${triggerReason ? `: ${triggerReason}` : '...'}`,
|
|
5359
5485
|
].join(' '));
|
|
5360
|
-
const
|
|
5361
|
-
|
|
5362
|
-
|
|
5486
|
+
const beforeStats = contextManager.getStats(history);
|
|
5487
|
+
const result = await contextManager.intelligentCompact(history);
|
|
5488
|
+
const afterStats = contextManager.getStats(result.compacted);
|
|
5489
|
+
const tokenSavings = Math.max(0, beforeStats.totalTokens - afterStats.totalTokens);
|
|
5490
|
+
const percentSavings = beforeStats.totalTokens > 0 ? (tokenSavings / beforeStats.totalTokens) * 100 : 0;
|
|
5491
|
+
const changed = this.historiesDiffer(history, result.compacted);
|
|
5492
|
+
const meaningfulSavings = changed &&
|
|
5493
|
+
(tokenSavings >= MIN_COMPACTION_TOKEN_SAVINGS || percentSavings >= MIN_COMPACTION_PERCENT_SAVINGS);
|
|
5494
|
+
if (!meaningfulSavings) {
|
|
5495
|
+
if (trigger?.forced || percentUsed >= 85) {
|
|
5496
|
+
display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
|
|
5497
|
+
}
|
|
5498
|
+
return;
|
|
5363
5499
|
}
|
|
5364
|
-
|
|
5365
|
-
this.
|
|
5366
|
-
|
|
5500
|
+
agent.loadHistory(result.compacted);
|
|
5501
|
+
this.cachedHistory = result.compacted;
|
|
5502
|
+
const newPercentUsed = windowTokens > 0
|
|
5503
|
+
? Math.round((afterStats.totalTokens / windowTokens) * 100)
|
|
5504
|
+
: afterStats.percentage;
|
|
5505
|
+
this.latestTokenUsage = {
|
|
5506
|
+
used: afterStats.totalTokens,
|
|
5507
|
+
limit: windowTokens,
|
|
5508
|
+
};
|
|
5509
|
+
this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
|
|
5510
|
+
this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
|
|
5511
|
+
this.refreshStatusLine(true);
|
|
5512
|
+
const primarySignal = result.analysis.signals[0]?.reason ?? triggerReason;
|
|
5513
|
+
display.showSystemMessage([
|
|
5514
|
+
`Context compacted: ${beforeStats.percentage}% → ${afterStats.percentage}%`,
|
|
5515
|
+
`(saved ~${tokenSavings.toLocaleString('en-US')} tokens).`,
|
|
5516
|
+
primarySignal ? `Reason: ${primarySignal}.` : '',
|
|
5517
|
+
].filter(Boolean).join(' '));
|
|
5518
|
+
}
|
|
5519
|
+
catch (error) {
|
|
5520
|
+
display.showError('Context compaction failed.', error);
|
|
5367
5521
|
}
|
|
5368
5522
|
finally {
|
|
5369
5523
|
if (cleanupOverlayActive) {
|
|
5370
5524
|
this.statusTracker.clearOverride(cleanupStatusId);
|
|
5371
5525
|
}
|
|
5372
5526
|
this.cleanupInProgress = false;
|
|
5527
|
+
this.contextCompactionInFlight = false;
|
|
5373
5528
|
}
|
|
5374
5529
|
}
|
|
5530
|
+
shouldAutoCompactContext(usageRatio, windowTokens, totalTokens) {
|
|
5531
|
+
const featureFlags = loadFeatureFlags();
|
|
5532
|
+
if (featureFlags.autoCompact === false) {
|
|
5533
|
+
return null;
|
|
5534
|
+
}
|
|
5535
|
+
const agent = this.agent;
|
|
5536
|
+
if (!agent) {
|
|
5537
|
+
return null;
|
|
5538
|
+
}
|
|
5539
|
+
const contextManager = agent.getContextManager();
|
|
5540
|
+
if (!contextManager) {
|
|
5541
|
+
return null;
|
|
5542
|
+
}
|
|
5543
|
+
const history = agent.getHistory();
|
|
5544
|
+
if (history.length <= 1) {
|
|
5545
|
+
return null;
|
|
5546
|
+
}
|
|
5547
|
+
const percentUsed = Math.round(usageRatio * 100);
|
|
5548
|
+
const analysis = contextManager.shouldTriggerCompaction(history);
|
|
5549
|
+
const overflowRisk = this.hasContextOverflowRisk(contextManager);
|
|
5550
|
+
if (usageRatio >= CONTEXT_USAGE_THRESHOLD) {
|
|
5551
|
+
return { reason: analysis.reason ?? `Context usage at ${percentUsed}%`, forced: true };
|
|
5552
|
+
}
|
|
5553
|
+
if (usageRatio >= CONTEXT_AUTOCOMPACT_FLOOR && (analysis.shouldCompact || overflowRisk)) {
|
|
5554
|
+
const reason = analysis.reason
|
|
5555
|
+
?? (overflowRisk ? 'Heavy tool output detected' : 'Compaction signals detected');
|
|
5556
|
+
return { reason, forced: false };
|
|
5557
|
+
}
|
|
5558
|
+
return null;
|
|
5559
|
+
}
|
|
5560
|
+
hasContextOverflowRisk(contextManager) {
|
|
5561
|
+
if (!contextManager?.detectContextOverflowRisk) {
|
|
5562
|
+
return false;
|
|
5563
|
+
}
|
|
5564
|
+
try {
|
|
5565
|
+
const toolHistory = this.runtimeSession.toolRuntime.getToolHistory?.();
|
|
5566
|
+
if (!toolHistory?.length) {
|
|
5567
|
+
return false;
|
|
5568
|
+
}
|
|
5569
|
+
const serialized = toolHistory.map((entry) => {
|
|
5570
|
+
const args = entry.args ?? {};
|
|
5571
|
+
const argsText = Object.keys(args).length ? ` ${JSON.stringify(args)}` : '';
|
|
5572
|
+
return `${entry.toolName}${argsText}`;
|
|
5573
|
+
});
|
|
5574
|
+
return contextManager.detectContextOverflowRisk(serialized);
|
|
5575
|
+
}
|
|
5576
|
+
catch {
|
|
5577
|
+
return false;
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
getContextWarningLevel(percentage) {
|
|
5581
|
+
if (percentage >= 90) {
|
|
5582
|
+
return 'danger';
|
|
5583
|
+
}
|
|
5584
|
+
if (percentage >= 70) {
|
|
5585
|
+
return 'warning';
|
|
5586
|
+
}
|
|
5587
|
+
if (percentage >= 50) {
|
|
5588
|
+
return 'info';
|
|
5589
|
+
}
|
|
5590
|
+
return null;
|
|
5591
|
+
}
|
|
5592
|
+
maybeShowContextWarning(percentage) {
|
|
5593
|
+
const level = this.getContextWarningLevel(percentage);
|
|
5594
|
+
if (level === this.lastContextWarningLevel) {
|
|
5595
|
+
if (level === null) {
|
|
5596
|
+
this.lastContextWarningLevel = null;
|
|
5597
|
+
}
|
|
5598
|
+
return;
|
|
5599
|
+
}
|
|
5600
|
+
this.lastContextWarningLevel = level;
|
|
5601
|
+
switch (level) {
|
|
5602
|
+
case 'info':
|
|
5603
|
+
display.showInfo('Context usage is climbing. Auto-compaction will engage if it keeps growing.');
|
|
5604
|
+
break;
|
|
5605
|
+
case 'warning':
|
|
5606
|
+
display.showWarning('Context is getting full. Auto-compaction will try to preserve space.');
|
|
5607
|
+
break;
|
|
5608
|
+
case 'danger':
|
|
5609
|
+
display.showWarning('Context is near capacity. Attempting automatic compaction.');
|
|
5610
|
+
break;
|
|
5611
|
+
default:
|
|
5612
|
+
break;
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
historiesDiffer(original, compacted) {
|
|
5616
|
+
if (original.length !== compacted.length) {
|
|
5617
|
+
return true;
|
|
5618
|
+
}
|
|
5619
|
+
for (let i = 0; i < original.length; i++) {
|
|
5620
|
+
const a = original[i];
|
|
5621
|
+
const b = compacted[i];
|
|
5622
|
+
if (a.role !== b.role) {
|
|
5623
|
+
return true;
|
|
5624
|
+
}
|
|
5625
|
+
if ((a.content ?? '') !== (b.content ?? '')) {
|
|
5626
|
+
return true;
|
|
5627
|
+
}
|
|
5628
|
+
const aName = 'name' in a ? a.name : undefined;
|
|
5629
|
+
const bName = 'name' in b ? b.name : undefined;
|
|
5630
|
+
if ((aName ?? '') !== (bName ?? '')) {
|
|
5631
|
+
return true;
|
|
5632
|
+
}
|
|
5633
|
+
const aToolCallId = 'toolCallId' in a ? a.toolCallId : undefined;
|
|
5634
|
+
const bToolCallId = 'toolCallId' in b ? b.toolCallId : undefined;
|
|
5635
|
+
if ((aToolCallId ?? '') !== (bToolCallId ?? '')) {
|
|
5636
|
+
return true;
|
|
5637
|
+
}
|
|
5638
|
+
const aTools = 'toolCalls' in a ? a.toolCalls ?? [] : [];
|
|
5639
|
+
const bTools = 'toolCalls' in b ? b.toolCalls ?? [] : [];
|
|
5640
|
+
if (aTools.length !== bTools.length) {
|
|
5641
|
+
return true;
|
|
5642
|
+
}
|
|
5643
|
+
for (let j = 0; j < aTools.length; j++) {
|
|
5644
|
+
if (JSON.stringify(aTools[j]) !== JSON.stringify(bTools[j])) {
|
|
5645
|
+
return true;
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
return false;
|
|
5650
|
+
}
|
|
5375
5651
|
partitionHistory(history) {
|
|
5376
5652
|
const system = [];
|
|
5377
5653
|
const conversation = [];
|
|
@@ -5507,7 +5783,12 @@ What's the next action?`;
|
|
|
5507
5783
|
const reason = info.reason ? ` (${info.reason.replace(/-/g, ' ')})` : '';
|
|
5508
5784
|
const partialNote = info.partialResponse ? ' Received partial stream before failure.' : '';
|
|
5509
5785
|
display.showWarning(`Streaming failed${reason}, retrying without streaming.${detail}${partialNote}`);
|
|
5510
|
-
this.
|
|
5786
|
+
const bufferedPartial = this.consumeStreamBuffer(info.partialResponse ?? '');
|
|
5787
|
+
if (this.batchedOutputMode && bufferedPartial.trim()) {
|
|
5788
|
+
display.showThinkingBlock(bufferedPartial.trim(), this.currentRunElapsedMs());
|
|
5789
|
+
}
|
|
5790
|
+
const fallbackLabel = this.batchedOutputMode ? 'Retrying (batched)' : 'Fallback in progress';
|
|
5791
|
+
this.startStreamingHeartbeat(fallbackLabel);
|
|
5511
5792
|
this.requestPromptRefresh(true);
|
|
5512
5793
|
}
|
|
5513
5794
|
handleProviderError(error, retryAction) {
|
|
@@ -5518,9 +5799,9 @@ What's the next action?`;
|
|
|
5518
5799
|
this.handleApiKeyIssue(apiKeyIssue, retryAction);
|
|
5519
5800
|
return true;
|
|
5520
5801
|
}
|
|
5521
|
-
handleApiKeyIssue(info, retryAction) {
|
|
5802
|
+
handleApiKeyIssue(info, retryAction, contextLabel) {
|
|
5522
5803
|
const secret = info.secret ?? null;
|
|
5523
|
-
const providerLabel = info.provider ? this.providerLabel(info.provider) : 'the selected provider';
|
|
5804
|
+
const providerLabel = contextLabel ?? (info.provider ? this.providerLabel(info.provider) : 'the selected provider');
|
|
5524
5805
|
if (!secret) {
|
|
5525
5806
|
this.pendingSecretRetry = null;
|
|
5526
5807
|
const guidance = 'Run "/secrets" to configure the required API key or export it (e.g., EXPORT KEY=value) before launching the CLI.';
|
|
@@ -5542,6 +5823,10 @@ What's the next action?`;
|
|
|
5542
5823
|
this.pendingInteraction = { type: 'secret-input', secret };
|
|
5543
5824
|
this.showSecretGuidance(secret, isMissing);
|
|
5544
5825
|
}
|
|
5826
|
+
handleToolApiKeyIssue(info, call) {
|
|
5827
|
+
const label = call?.name ? `${call.name} tool` : 'web tools';
|
|
5828
|
+
this.handleApiKeyIssue(info, undefined, label);
|
|
5829
|
+
}
|
|
5545
5830
|
showSecretGuidance(secret, promptForInput) {
|
|
5546
5831
|
const lines = [];
|
|
5547
5832
|
if (promptForInput) {
|
|
@@ -5551,7 +5836,7 @@ What's the next action?`;
|
|
|
5551
5836
|
lines.push(`Update the stored value for ${secret.label} or type "cancel".`);
|
|
5552
5837
|
}
|
|
5553
5838
|
lines.push(`Tip: run "/secrets" anytime to manage credentials or export ${secret.envVar}=<value> before launching the CLI.`);
|
|
5554
|
-
|
|
5839
|
+
this.showInlineMenu('Secrets', lines, promptForInput ? 'warning' : 'info');
|
|
5555
5840
|
}
|
|
5556
5841
|
colorizeDropdownLine(text, index) {
|
|
5557
5842
|
if (!DROPDOWN_COLORS.length) {
|