erosolar-cli 1.7.53 → 1.7.55
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/agent.d.ts.map +1 -1
- package/dist/core/agent.js +14 -4
- package/dist/core/agent.js.map +1 -1
- package/dist/providers/anthropicProvider.d.ts.map +1 -1
- package/dist/providers/anthropicProvider.js +2 -1
- package/dist/providers/anthropicProvider.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +4 -54
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +183 -422
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/unifiedInputProcessor.d.ts +23 -0
- package/dist/shell/unifiedInputProcessor.d.ts.map +1 -0
- package/dist/shell/unifiedInputProcessor.js +92 -0
- package/dist/shell/unifiedInputProcessor.js.map +1 -0
- package/dist/ui/persistentPrompt.d.ts +24 -0
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +86 -4
- package/dist/ui/persistentPrompt.js.map +1 -1
- package/package.json +1 -1
|
@@ -23,7 +23,6 @@ import { PersistentPrompt, PinnedChatBox } from '../ui/persistentPrompt.js';
|
|
|
23
23
|
import { formatShortcutsHelp } from '../ui/shortcutsHelp.js';
|
|
24
24
|
import { MetricsTracker } from '../alpha-zero/index.js';
|
|
25
25
|
import { listAvailablePlugins } from '../plugins/index.js';
|
|
26
|
-
import { verifyResponse, formatVerificationReport, } from '../core/responseVerifier.js';
|
|
27
26
|
const DROPDOWN_COLORS = [
|
|
28
27
|
theme.primary,
|
|
29
28
|
theme.info,
|
|
@@ -74,7 +73,6 @@ export class InteractiveShell {
|
|
|
74
73
|
workspaceOptions;
|
|
75
74
|
sessionState;
|
|
76
75
|
isProcessing = false;
|
|
77
|
-
isInsideThinkingBlock = false;
|
|
78
76
|
pendingInteraction = null;
|
|
79
77
|
pendingSecretRetry = null;
|
|
80
78
|
bufferedInputLines = [];
|
|
@@ -107,12 +105,8 @@ export class InteractiveShell {
|
|
|
107
105
|
pendingHistoryLoad = null;
|
|
108
106
|
cachedHistory = [];
|
|
109
107
|
activeSessionId = null;
|
|
110
|
-
sessionStartTime = Date.now();
|
|
111
108
|
activeSessionTitle = null;
|
|
112
109
|
sessionResumeNotice = null;
|
|
113
|
-
lastAssistantResponse = null;
|
|
114
|
-
verificationRetryCount = 0;
|
|
115
|
-
maxVerificationRetries = 2;
|
|
116
110
|
customCommands;
|
|
117
111
|
customCommandMap;
|
|
118
112
|
sessionRestoreConfig;
|
|
@@ -173,11 +167,16 @@ export class InteractiveShell {
|
|
|
173
167
|
// Update persistent prompt status bar with file changes
|
|
174
168
|
this.updatePersistentPromptFileChanges();
|
|
175
169
|
});
|
|
176
|
-
// Set up tool status callback to update
|
|
177
|
-
// Uses Claude Code style: single line at bottom that updates in-place
|
|
170
|
+
// Set up tool status callback to update pinned chat box during tool execution
|
|
178
171
|
this.uiAdapter.setToolStatusCallback((status) => {
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
if (status) {
|
|
173
|
+
this.pinnedChatBox.setStatusMessage(status);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Clear status but keep processing indicator if still processing
|
|
177
|
+
this.pinnedChatBox.setStatusMessage(null);
|
|
178
|
+
}
|
|
179
|
+
this.pinnedChatBox.forceRender();
|
|
181
180
|
});
|
|
182
181
|
this.skillRepository = new SkillRepository({
|
|
183
182
|
workingDir: this.workingDir,
|
|
@@ -189,8 +188,9 @@ export class InteractiveShell {
|
|
|
189
188
|
this.rl = readline.createInterface({
|
|
190
189
|
input,
|
|
191
190
|
output,
|
|
192
|
-
//
|
|
193
|
-
|
|
191
|
+
// Use empty prompt since PinnedChatBox handles all prompt rendering
|
|
192
|
+
// This prevents duplicate '>' characters from appearing
|
|
193
|
+
prompt: '',
|
|
194
194
|
terminal: true,
|
|
195
195
|
historySize: 100, // Enable native readline history
|
|
196
196
|
});
|
|
@@ -281,10 +281,7 @@ export class InteractiveShell {
|
|
|
281
281
|
this.pinnedChatBox.show();
|
|
282
282
|
this.pinnedChatBox.forceRender();
|
|
283
283
|
if (initialPrompt) {
|
|
284
|
-
// For command-line prompts, show the user's input with separator (Claude Code style)
|
|
285
284
|
display.newLine();
|
|
286
|
-
const cols = Math.min(process.stdout.columns || 80, 72);
|
|
287
|
-
console.log(theme.ui.border('─'.repeat(cols)));
|
|
288
285
|
console.log(`${formatUserPrompt(this.profileLabel || this.profile)}${initialPrompt}`);
|
|
289
286
|
await this.processInputBlock(initialPrompt);
|
|
290
287
|
return;
|
|
@@ -421,7 +418,24 @@ export class InteractiveShell {
|
|
|
421
418
|
// Set up raw data interception for bracketed paste
|
|
422
419
|
this.setupRawPasteHandler();
|
|
423
420
|
this.rl.on('line', (line) => {
|
|
424
|
-
//
|
|
421
|
+
// If we're capturing raw paste data, ignore readline line events
|
|
422
|
+
// (they've already been handled by the raw data handler)
|
|
423
|
+
if (this.bracketedPaste.isCapturingRaw()) {
|
|
424
|
+
this.resetBufferedInputLines();
|
|
425
|
+
// Clear the line that readline just echoed - move up and clear
|
|
426
|
+
output.write('\x1b[A\r\x1b[K');
|
|
427
|
+
// Show paste progress (this will update in place)
|
|
428
|
+
this.showMultiLinePastePreview(this.bracketedPaste.getRawBufferLineCount(), this.bracketedPaste.getRawBufferPreview());
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// If a paste was just captured via raw handler, ignore readline's line events
|
|
432
|
+
// (readline still emits line events after we've already captured the paste)
|
|
433
|
+
if (this.bracketedPaste.shouldIgnoreLineEvent()) {
|
|
434
|
+
this.resetBufferedInputLines();
|
|
435
|
+
// Clear the echoed line that readline wrote
|
|
436
|
+
output.write('\x1b[A\r\x1b[K');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
425
439
|
const normalized = this.bracketedPaste.process(line);
|
|
426
440
|
if (normalized.handled) {
|
|
427
441
|
// Clear any partially buffered lines so we don't auto-submit the first line of a paste
|
|
@@ -472,6 +486,8 @@ export class InteractiveShell {
|
|
|
472
486
|
display.newLine();
|
|
473
487
|
const highlightedEmail = theme.info('support@ero.solar');
|
|
474
488
|
const infoMessage = [
|
|
489
|
+
'Thank you to Anthropic for allowing me to use Claude Code to build erosolar-cli.',
|
|
490
|
+
'',
|
|
475
491
|
`Email ${highlightedEmail} with any bugs or feedback`,
|
|
476
492
|
'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
|
|
477
493
|
'npm: https://www.npmjs.com/package/erosolar-cli',
|
|
@@ -494,17 +510,36 @@ export class InteractiveShell {
|
|
|
494
510
|
* capture complete multi-line pastes without readline splitting them.
|
|
495
511
|
*/
|
|
496
512
|
setupRawPasteHandler() {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
513
|
+
if (!this.bracketedPasteEnabled) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const inputStream = input;
|
|
517
|
+
if (!inputStream || !inputStream.isTTY) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
// Set up callback for when a complete paste is captured
|
|
521
|
+
this.bracketedPaste.setRawPasteCallback((content) => {
|
|
522
|
+
this.clearMultiLinePastePreview();
|
|
523
|
+
const lines = content.split('\n');
|
|
524
|
+
const lineCount = lines.length;
|
|
525
|
+
// All pastes (single or multi-line) are captured for confirmation before submit
|
|
526
|
+
this.capturePaste(content, lineCount);
|
|
527
|
+
});
|
|
528
|
+
// Set up raw data interception to catch bracketed paste before readline processes it
|
|
529
|
+
// We prepend our listener so it runs before readline's listener
|
|
530
|
+
this.rawDataHandler = (data) => {
|
|
531
|
+
const str = data.toString();
|
|
532
|
+
const result = this.bracketedPaste.processRawData(str);
|
|
533
|
+
if (result.consumed) {
|
|
534
|
+
// Don't show preview here - readline will still echo lines to the terminal,
|
|
535
|
+
// and our preview would get clobbered. Instead, we show the preview in the
|
|
536
|
+
// line handler after clearing readline's echoed output.
|
|
537
|
+
// The processRawData() sets flags that the line handler will check.
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
// Use prependListener to ensure our handler runs before readline's handlers
|
|
541
|
+
// This gives us first look at the raw data including bracketed paste markers
|
|
542
|
+
inputStream.prependListener('data', this.rawDataHandler);
|
|
508
543
|
}
|
|
509
544
|
setupSlashCommandPreviewHandler() {
|
|
510
545
|
const inputStream = input;
|
|
@@ -512,12 +547,7 @@ export class InteractiveShell {
|
|
|
512
547
|
return;
|
|
513
548
|
}
|
|
514
549
|
if (inputStream.listenerCount('keypress') === 0) {
|
|
515
|
-
|
|
516
|
-
readline.emitKeypressEvents(inputStream, this.rl);
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
// emitKeypressEvents can throw EIO when setting raw mode - safe to ignore
|
|
520
|
-
}
|
|
550
|
+
readline.emitKeypressEvents(inputStream, this.rl);
|
|
521
551
|
}
|
|
522
552
|
this.keypressHandler = (_str, key) => {
|
|
523
553
|
// Handle special keys
|
|
@@ -534,8 +564,8 @@ export class InteractiveShell {
|
|
|
534
564
|
const currentLine = this.rl.line || '';
|
|
535
565
|
const cursorPos = this.rl.cursor || 0;
|
|
536
566
|
this.persistentPrompt.updateInput(currentLine, cursorPos);
|
|
537
|
-
// Sync to pinned chat box for display only
|
|
538
|
-
this.pinnedChatBox.setInput(currentLine
|
|
567
|
+
// Sync to pinned chat box for display only
|
|
568
|
+
this.pinnedChatBox.setInput(currentLine);
|
|
539
569
|
if (this.composableMessage.hasContent()) {
|
|
540
570
|
this.composableMessage.setDraft(currentLine);
|
|
541
571
|
this.updateComposeStatusSummary();
|
|
@@ -656,9 +686,8 @@ export class InteractiveShell {
|
|
|
656
686
|
* left the stream paused, raw mode disabled, or keypress listeners detached.
|
|
657
687
|
*/
|
|
658
688
|
ensureReadlineReady() {
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
// is captured via the raw data handler.
|
|
689
|
+
// Clear any stuck bracketed paste state so new input isn't dropped
|
|
690
|
+
this.bracketedPaste.reset();
|
|
662
691
|
// Always ensure the pinned chat box is visible when readline is ready
|
|
663
692
|
this.pinnedChatBox.show();
|
|
664
693
|
this.pinnedChatBox.forceRender();
|
|
@@ -677,13 +706,7 @@ export class InteractiveShell {
|
|
|
677
706
|
if (inputStream.isTTY && typeof inputStream.isRaw === 'boolean') {
|
|
678
707
|
const ttyStream = inputStream;
|
|
679
708
|
if (!ttyStream.isRaw) {
|
|
680
|
-
|
|
681
|
-
ttyStream.setRawMode(true);
|
|
682
|
-
}
|
|
683
|
-
catch (err) {
|
|
684
|
-
// EIO errors can occur when terminal is in unusual state
|
|
685
|
-
// Safe to ignore - readline will still work in cooked mode
|
|
686
|
-
}
|
|
709
|
+
ttyStream.setRawMode(true);
|
|
687
710
|
}
|
|
688
711
|
}
|
|
689
712
|
// Reattach keypress handler if it was removed
|
|
@@ -692,12 +715,7 @@ export class InteractiveShell {
|
|
|
692
715
|
const hasHandler = listeners.includes(this.keypressHandler);
|
|
693
716
|
if (!hasHandler) {
|
|
694
717
|
if (inputStream.listenerCount('keypress') === 0) {
|
|
695
|
-
|
|
696
|
-
readline.emitKeypressEvents(inputStream, this.rl);
|
|
697
|
-
}
|
|
698
|
-
catch {
|
|
699
|
-
// emitKeypressEvents can throw EIO when setting raw mode - safe to ignore
|
|
700
|
-
}
|
|
718
|
+
readline.emitKeypressEvents(inputStream, this.rl);
|
|
701
719
|
}
|
|
702
720
|
inputStream.on('keypress', this.keypressHandler);
|
|
703
721
|
}
|
|
@@ -775,55 +793,62 @@ export class InteractiveShell {
|
|
|
775
793
|
}
|
|
776
794
|
/**
|
|
777
795
|
* Capture any paste (single or multi-line) without immediately submitting it.
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
* Display format:
|
|
782
|
-
* - Single paste: "[📋 Pasted: 15 lines] "
|
|
783
|
-
* - Multiple pastes: "[📋 Pasted: 15 lines] [📋 Pasted: 50 lines] "
|
|
796
|
+
* - Short pastes (1-2 lines) are displayed inline like normal typed text
|
|
797
|
+
* - Longer pastes (3+ lines) show as collapsed block chips
|
|
798
|
+
* Supports multiple pastes - user can paste multiple times before submitting.
|
|
784
799
|
*/
|
|
785
|
-
capturePaste(content,
|
|
800
|
+
capturePaste(content, lineCount) {
|
|
786
801
|
this.resetBufferedInputLines();
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
if (
|
|
790
|
-
//
|
|
802
|
+
// Short pastes (1-2 lines) display inline like normal text
|
|
803
|
+
const isShortPaste = lineCount <= 2;
|
|
804
|
+
if (isShortPaste) {
|
|
805
|
+
// For short pastes, display inline like normal typed text
|
|
806
|
+
// No composableMessage storage - just treat as typed input
|
|
807
|
+
// For 2-line pastes, join with a visual newline indicator
|
|
808
|
+
const displayContent = lineCount === 1
|
|
809
|
+
? content
|
|
810
|
+
: content.replace(/\n/g, ' ↵ '); // Visual newline indicator for 2-line pastes
|
|
811
|
+
// Clear any echoed content first
|
|
791
812
|
output.write('\r\x1b[K');
|
|
792
|
-
//
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
813
|
+
// Get current readline content and append paste
|
|
814
|
+
const currentLine = this.rl.line || '';
|
|
815
|
+
const cursorPos = this.rl.cursor || 0;
|
|
816
|
+
// Insert paste at cursor position
|
|
817
|
+
const before = currentLine.slice(0, cursorPos);
|
|
818
|
+
const after = currentLine.slice(cursorPos);
|
|
819
|
+
const newLine = before + displayContent + after;
|
|
820
|
+
const newCursor = cursorPos + displayContent.length;
|
|
821
|
+
// Update readline buffer - write directly without storing in composableMessage
|
|
822
|
+
// This allows short pastes to flow through as normal typed text
|
|
823
|
+
this.rl.write(null, { ctrl: true, name: 'u' }); // Clear line
|
|
824
|
+
this.rl.write(newLine); // Write new content
|
|
825
|
+
// Update persistent prompt display
|
|
826
|
+
this.persistentPrompt.updateInput(newLine, newCursor);
|
|
827
|
+
// Re-prompt to show the inline content
|
|
828
|
+
this.rl.prompt(true);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// For longer pastes (3+ lines), store as a composable block
|
|
804
832
|
this.composableMessage.addPaste(content);
|
|
805
|
-
// Clear
|
|
833
|
+
// Clear remaining echoed lines from terminal
|
|
806
834
|
output.write('\r\x1b[K');
|
|
807
|
-
// Build the paste
|
|
808
|
-
// Format:
|
|
835
|
+
// Build the paste chips to show inline with prompt
|
|
836
|
+
// Format: [Pasted text #1 +104 lines] [Pasted text #2 +50 lines]
|
|
809
837
|
const pasteChips = this.composableMessage.formatPasteChips();
|
|
810
838
|
// Update status bar with instructions
|
|
811
839
|
this.persistentPrompt.updateStatusBar({
|
|
812
840
|
message: 'Paste more, type text, or press Enter to send (/cancel to discard)',
|
|
813
841
|
});
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
this.
|
|
817
|
-
// Update
|
|
818
|
-
|
|
819
|
-
// Reset readline's internal state to empty
|
|
820
|
-
// The paste chips are visual-only; actual content is in composableMessage
|
|
842
|
+
// Set the prompt to show paste chips, then position cursor after them
|
|
843
|
+
// The user can type additional text after the chips
|
|
844
|
+
this.persistentPrompt.updateInput(pasteChips + ' ', pasteChips.length + 1);
|
|
845
|
+
// Update readline's line buffer to include the chips as prefix
|
|
846
|
+
// This ensures typed text appears after the chips
|
|
821
847
|
if (this.rl.line !== undefined) {
|
|
822
|
-
this.rl.line = '';
|
|
823
|
-
this.rl.cursor =
|
|
848
|
+
this.rl.line = pasteChips + ' ';
|
|
849
|
+
this.rl.cursor = pasteChips.length + 1;
|
|
824
850
|
}
|
|
825
|
-
//
|
|
826
|
-
this.rl.prompt(false);
|
|
851
|
+
this.rl.prompt(true); // preserveCursor=true to keep position after chips
|
|
827
852
|
}
|
|
828
853
|
/**
|
|
829
854
|
* Update the status bar to reflect any pending composed message parts
|
|
@@ -943,19 +968,21 @@ export class InteractiveShell {
|
|
|
943
968
|
if (await this.handlePendingInteraction(trimmed)) {
|
|
944
969
|
return;
|
|
945
970
|
}
|
|
946
|
-
// If we have captured paste blocks, respect control commands before assembling
|
|
971
|
+
// If we have captured multi-line paste blocks, respect control commands before assembling
|
|
947
972
|
if (this.composableMessage.hasContent()) {
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
const
|
|
973
|
+
// Strip paste chip prefixes from input since actual content is in composableMessage
|
|
974
|
+
// Chips look like: [Pasted text #1 +X lines] [Pasted text #2 +Y lines]
|
|
975
|
+
const chipsPrefix = this.composableMessage.formatPasteChips();
|
|
976
|
+
let userText = trimmed;
|
|
977
|
+
if (chipsPrefix && trimmed.startsWith(chipsPrefix)) {
|
|
978
|
+
userText = trimmed.slice(chipsPrefix.length).trim();
|
|
979
|
+
}
|
|
951
980
|
const lower = userText.toLowerCase();
|
|
952
981
|
// Control commands that should NOT consume the captured paste
|
|
953
982
|
if (lower === '/cancel' || lower === 'cancel') {
|
|
954
983
|
this.composableMessage.clear();
|
|
955
984
|
this.updateComposeStatusSummary();
|
|
956
985
|
this.persistentPrompt.updateInput('', 0);
|
|
957
|
-
// Clear paste state to prevent blocking follow-up prompts
|
|
958
|
-
this.bracketedPaste.clearPasteJustCaptured();
|
|
959
986
|
display.showInfo('Discarded captured paste.');
|
|
960
987
|
this.rl.prompt();
|
|
961
988
|
return;
|
|
@@ -990,8 +1017,6 @@ export class InteractiveShell {
|
|
|
990
1017
|
this.composableMessage.clear();
|
|
991
1018
|
this.updateComposeStatusSummary();
|
|
992
1019
|
this.persistentPrompt.updateInput('', 0);
|
|
993
|
-
// Clear paste state to prevent blocking follow-up prompts
|
|
994
|
-
this.bracketedPaste.clearPasteJustCaptured();
|
|
995
1020
|
if (!assembled) {
|
|
996
1021
|
this.rl.prompt();
|
|
997
1022
|
return;
|
|
@@ -1162,9 +1187,6 @@ export class InteractiveShell {
|
|
|
1162
1187
|
case '/discover':
|
|
1163
1188
|
await this.discoverModelsCommand();
|
|
1164
1189
|
break;
|
|
1165
|
-
case '/verify':
|
|
1166
|
-
await this.handleVerifyCommand();
|
|
1167
|
-
break;
|
|
1168
1190
|
default:
|
|
1169
1191
|
if (!(await this.tryCustomSlashCommand(command, input))) {
|
|
1170
1192
|
display.showWarning(`Unknown command "${command}".`);
|
|
@@ -1278,7 +1300,7 @@ export class InteractiveShell {
|
|
|
1278
1300
|
const context = buildWorkspaceContext(this.workingDir, this.workspaceOptions);
|
|
1279
1301
|
const profileConfig = this.runtimeSession.refreshWorkspaceContext(context);
|
|
1280
1302
|
const tools = this.runtimeSession.toolRuntime.listProviderTools();
|
|
1281
|
-
this.baseSystemPrompt = buildInteractiveSystemPrompt(profileConfig.systemPrompt, profileConfig.label, tools
|
|
1303
|
+
this.baseSystemPrompt = buildInteractiveSystemPrompt(profileConfig.systemPrompt, profileConfig.label, tools);
|
|
1282
1304
|
if (this.rebuildAgent()) {
|
|
1283
1305
|
display.showInfo(`Workspace snapshot refreshed (${this.describeWorkspaceOptions()}).`);
|
|
1284
1306
|
}
|
|
@@ -1481,7 +1503,7 @@ export class InteractiveShell {
|
|
|
1481
1503
|
lines.push(theme.bold('Session File Changes'));
|
|
1482
1504
|
lines.push('');
|
|
1483
1505
|
lines.push(`${theme.info('•')} ${summary.files} file${summary.files === 1 ? '' : 's'} modified`);
|
|
1484
|
-
lines.push(`${theme.info('•')} ${theme.success(
|
|
1506
|
+
lines.push(`${theme.info('•')} ${theme.success('+' + summary.additions)} ${theme.error('-' + summary.removals)} lines`);
|
|
1485
1507
|
lines.push('');
|
|
1486
1508
|
// Group changes by file
|
|
1487
1509
|
const fileMap = new Map();
|
|
@@ -1505,7 +1527,7 @@ export class InteractiveShell {
|
|
|
1505
1527
|
if (stats.writes > 0)
|
|
1506
1528
|
operations.push(`${stats.writes} write${stats.writes === 1 ? '' : 's'}`);
|
|
1507
1529
|
const opsText = operations.join(', ');
|
|
1508
|
-
const diffText = `${theme.success(
|
|
1530
|
+
const diffText = `${theme.success('+' + stats.additions)} ${theme.error('-' + stats.removals)}`;
|
|
1509
1531
|
lines.push(` ${theme.dim(path)}`);
|
|
1510
1532
|
lines.push(` ${opsText} • ${diffText}`);
|
|
1511
1533
|
}
|
|
@@ -1515,211 +1537,6 @@ export class InteractiveShell {
|
|
|
1515
1537
|
const summary = this.alphaZeroMetrics.getPerformanceSummary();
|
|
1516
1538
|
display.showSystemMessage(summary);
|
|
1517
1539
|
}
|
|
1518
|
-
/**
|
|
1519
|
-
* Create a verification context for isolated process verification.
|
|
1520
|
-
*
|
|
1521
|
-
* Verification now runs in a completely separate Node.js process for full isolation.
|
|
1522
|
-
* This ensures:
|
|
1523
|
-
* - Separate memory space from main CLI
|
|
1524
|
-
* - Independent event loop
|
|
1525
|
-
* - No shared state
|
|
1526
|
-
* - Errors in verification cannot crash main process
|
|
1527
|
-
*/
|
|
1528
|
-
createVerificationContext() {
|
|
1529
|
-
// Build conversation history for context
|
|
1530
|
-
const conversationHistory = this.cachedHistory
|
|
1531
|
-
.filter(msg => msg.role === 'user' || msg.role === 'assistant')
|
|
1532
|
-
.slice(-10) // Last 10 messages for context
|
|
1533
|
-
.map(msg => `${msg.role}: ${typeof msg.content === 'string' ? msg.content.slice(0, 500) : '[complex content]'}`);
|
|
1534
|
-
return {
|
|
1535
|
-
workingDirectory: this.workingDir,
|
|
1536
|
-
conversationHistory,
|
|
1537
|
-
provider: this.sessionState.provider,
|
|
1538
|
-
model: this.sessionState.model,
|
|
1539
|
-
};
|
|
1540
|
-
}
|
|
1541
|
-
/**
|
|
1542
|
-
* Handle /verify command - verify the last assistant response
|
|
1543
|
-
*/
|
|
1544
|
-
async handleVerifyCommand() {
|
|
1545
|
-
if (!this.lastAssistantResponse) {
|
|
1546
|
-
display.showWarning('No assistant response to verify. Send a message first.');
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
display.showSystemMessage('Verifying last response in isolated process...\n');
|
|
1550
|
-
try {
|
|
1551
|
-
const context = this.createVerificationContext();
|
|
1552
|
-
const report = await verifyResponse(this.lastAssistantResponse, context);
|
|
1553
|
-
const formattedReport = formatVerificationReport(report);
|
|
1554
|
-
display.showSystemMessage(formattedReport);
|
|
1555
|
-
// Show actionable summary
|
|
1556
|
-
if (report.overallVerdict === 'contradicted') {
|
|
1557
|
-
display.showError('Some claims in the response could not be verified!');
|
|
1558
|
-
}
|
|
1559
|
-
else if (report.overallVerdict === 'verified') {
|
|
1560
|
-
display.showInfo('All verifiable claims in the response were verified.');
|
|
1561
|
-
}
|
|
1562
|
-
else if (report.overallVerdict === 'partially_verified') {
|
|
1563
|
-
display.showWarning('Some claims were verified, but not all.');
|
|
1564
|
-
}
|
|
1565
|
-
else {
|
|
1566
|
-
display.showInfo('No verifiable claims found in the response.');
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
catch (err) {
|
|
1570
|
-
display.showError(`Verification failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
/**
|
|
1574
|
-
* Check if a response looks like a completion (claims to be done)
|
|
1575
|
-
* vs. asking follow-up questions or waiting for user input.
|
|
1576
|
-
* Uses LLM to intelligently determine if verification should run.
|
|
1577
|
-
* Only run auto-verification when assistant claims task completion.
|
|
1578
|
-
*/
|
|
1579
|
-
async shouldRunAutoVerification(response) {
|
|
1580
|
-
// Quick pre-filter: very short responses are unlikely to have verifiable claims
|
|
1581
|
-
if (response.length < 100) {
|
|
1582
|
-
return false;
|
|
1583
|
-
}
|
|
1584
|
-
try {
|
|
1585
|
-
// Use LLM to determine if this response contains verifiable completion claims
|
|
1586
|
-
const prompt = `Analyze this AI assistant response and determine if it claims to have COMPLETED a task that can be verified.
|
|
1587
|
-
|
|
1588
|
-
RESPONSE:
|
|
1589
|
-
---
|
|
1590
|
-
${response.slice(0, 2000)}
|
|
1591
|
-
---
|
|
1592
|
-
|
|
1593
|
-
Answer with ONLY "YES" or "NO":
|
|
1594
|
-
- YES: The response claims to have completed something verifiable (created/modified files, ran commands, fixed bugs, implemented features, etc.)
|
|
1595
|
-
- NO: The response is asking questions, requesting clarification, explaining concepts, or hasn't completed any verifiable action yet.
|
|
1596
|
-
|
|
1597
|
-
Answer:`;
|
|
1598
|
-
const agent = this.runtimeSession.createAgent({
|
|
1599
|
-
provider: this.sessionState.provider,
|
|
1600
|
-
model: this.sessionState.model,
|
|
1601
|
-
temperature: 0,
|
|
1602
|
-
maxTokens: 10,
|
|
1603
|
-
systemPrompt: 'You are a classifier. Answer only YES or NO.',
|
|
1604
|
-
});
|
|
1605
|
-
const result = await agent.send(prompt);
|
|
1606
|
-
const answer = result.trim().toUpperCase();
|
|
1607
|
-
return answer.startsWith('YES');
|
|
1608
|
-
}
|
|
1609
|
-
catch {
|
|
1610
|
-
// On error, fall back to not running verification
|
|
1611
|
-
return false;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
/**
|
|
1615
|
-
* Schedule auto-verification after assistant response.
|
|
1616
|
-
* Uses LLM-based semantic analysis to verify ALL claims.
|
|
1617
|
-
* Runs asynchronously to not block the UI.
|
|
1618
|
-
* Only runs when assistant claims completion, not when asking questions.
|
|
1619
|
-
*/
|
|
1620
|
-
scheduleAutoVerification(response) {
|
|
1621
|
-
// Run verification asynchronously after a short delay
|
|
1622
|
-
// This allows the UI to update first
|
|
1623
|
-
setTimeout(async () => {
|
|
1624
|
-
try {
|
|
1625
|
-
// Use LLM to determine if this response should be verified
|
|
1626
|
-
const shouldVerify = await this.shouldRunAutoVerification(response);
|
|
1627
|
-
if (!shouldVerify) {
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
display.showSystemMessage(`\n🔍 Auto-verifying response in isolated process...`);
|
|
1631
|
-
const context = this.createVerificationContext();
|
|
1632
|
-
const report = await verifyResponse(response, context);
|
|
1633
|
-
const formattedReport = formatVerificationReport(report);
|
|
1634
|
-
// Show compact result
|
|
1635
|
-
if (report.summary.total === 0) {
|
|
1636
|
-
display.showInfo('No verifiable claims found in the response.');
|
|
1637
|
-
this.verificationRetryCount = 0;
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
if (report.overallVerdict === 'verified') {
|
|
1641
|
-
display.showInfo(`✅ Verified: ${report.summary.verified}/${report.summary.total} claims confirmed`);
|
|
1642
|
-
// Reset retry count on success
|
|
1643
|
-
this.verificationRetryCount = 0;
|
|
1644
|
-
}
|
|
1645
|
-
else if (report.overallVerdict === 'contradicted' || report.overallVerdict === 'partially_verified') {
|
|
1646
|
-
const failedCount = report.summary.failed;
|
|
1647
|
-
const icon = report.overallVerdict === 'contradicted' ? '❌' : '⚠️';
|
|
1648
|
-
const label = report.overallVerdict === 'contradicted' ? 'Verification failed' : 'Partial verification';
|
|
1649
|
-
display.showError(`${icon} ${label}: ${failedCount} claim${failedCount > 1 ? 's' : ''} could not be verified`);
|
|
1650
|
-
display.showSystemMessage(formattedReport);
|
|
1651
|
-
// Attempt to fix if we have retries left
|
|
1652
|
-
if (this.verificationRetryCount < this.maxVerificationRetries) {
|
|
1653
|
-
this.verificationRetryCount++;
|
|
1654
|
-
this.requestVerificationFix(report);
|
|
1655
|
-
}
|
|
1656
|
-
else {
|
|
1657
|
-
display.showWarning(`Max verification retries (${this.maxVerificationRetries}) reached. Use /verify to check manually.`);
|
|
1658
|
-
this.verificationRetryCount = 0;
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
catch (err) {
|
|
1663
|
-
// Silently ignore verification errors to not disrupt the flow
|
|
1664
|
-
// User can always run /verify manually
|
|
1665
|
-
}
|
|
1666
|
-
}, 500);
|
|
1667
|
-
}
|
|
1668
|
-
/**
|
|
1669
|
-
* Request the AI to fix failed verification claims.
|
|
1670
|
-
* Generates a strategic fix request with context about what failed and why.
|
|
1671
|
-
*/
|
|
1672
|
-
requestVerificationFix(report) {
|
|
1673
|
-
const failedResults = report.results.filter(r => !r.verified && r.confidence === 'high');
|
|
1674
|
-
if (failedResults.length === 0) {
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
// Build detailed failure descriptions with suggested fixes
|
|
1678
|
-
const failureDetails = failedResults.map(r => {
|
|
1679
|
-
const claim = r.claim;
|
|
1680
|
-
const evidence = r.evidence;
|
|
1681
|
-
// Generate specific fix strategy based on claim category
|
|
1682
|
-
let suggestedFix = '';
|
|
1683
|
-
switch (claim.category) {
|
|
1684
|
-
case 'file_op':
|
|
1685
|
-
suggestedFix = `Re-create or update the file at: ${claim.context['path'] || 'specified path'}`;
|
|
1686
|
-
break;
|
|
1687
|
-
case 'code':
|
|
1688
|
-
suggestedFix = 'Fix any type errors or syntax issues, then run the build again';
|
|
1689
|
-
break;
|
|
1690
|
-
case 'command':
|
|
1691
|
-
suggestedFix = 'Re-run the command and verify it completes successfully';
|
|
1692
|
-
break;
|
|
1693
|
-
case 'state':
|
|
1694
|
-
suggestedFix = 'Verify the state change was applied correctly';
|
|
1695
|
-
break;
|
|
1696
|
-
case 'behavior':
|
|
1697
|
-
suggestedFix = 'Test the feature manually or check implementation';
|
|
1698
|
-
break;
|
|
1699
|
-
default:
|
|
1700
|
-
suggestedFix = 'Retry the operation';
|
|
1701
|
-
}
|
|
1702
|
-
return `• ${claim.statement}
|
|
1703
|
-
Evidence: ${evidence.slice(0, 150)}
|
|
1704
|
-
Suggested fix: ${suggestedFix}`;
|
|
1705
|
-
}).join('\n\n');
|
|
1706
|
-
const fixMessage = `🔧 VERIFICATION FAILED - AUTO-RETRY (attempt ${this.verificationRetryCount}/${this.maxVerificationRetries})
|
|
1707
|
-
|
|
1708
|
-
The following claims could not be verified:
|
|
1709
|
-
|
|
1710
|
-
${failureDetails}
|
|
1711
|
-
|
|
1712
|
-
Think through this carefully, then:
|
|
1713
|
-
1. Analyze why each operation failed (check files, errors, state)
|
|
1714
|
-
2. Identify the root cause
|
|
1715
|
-
3. Fix the underlying issue
|
|
1716
|
-
4. Re-execute the failed operation(s)
|
|
1717
|
-
5. Verify the fix worked`;
|
|
1718
|
-
display.showSystemMessage(`\n🔧 Auto-retry: Generating fix strategy for ${failedResults.length} failed claim${failedResults.length > 1 ? 's' : ''}...`);
|
|
1719
|
-
// Queue the fix request
|
|
1720
|
-
this.followUpQueue.push({ type: 'request', text: fixMessage });
|
|
1721
|
-
this.scheduleQueueProcessing();
|
|
1722
|
-
}
|
|
1723
1540
|
showImprovementSuggestions() {
|
|
1724
1541
|
const suggestions = this.alphaZeroMetrics.getImprovementSuggestions();
|
|
1725
1542
|
if (suggestions.length === 0) {
|
|
@@ -2404,14 +2221,6 @@ Think through this carefully, then:
|
|
|
2404
2221
|
this.rl.prompt();
|
|
2405
2222
|
return;
|
|
2406
2223
|
}
|
|
2407
|
-
// Handle slash commands - exit secret input mode and process the command
|
|
2408
|
-
if (trimmed.startsWith('/')) {
|
|
2409
|
-
this.pendingInteraction = null;
|
|
2410
|
-
this.pendingSecretRetry = null;
|
|
2411
|
-
display.showInfo('Secret input cancelled.');
|
|
2412
|
-
await this.processSlashCommand(trimmed);
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
2224
|
if (trimmed.toLowerCase() === 'cancel') {
|
|
2416
2225
|
this.pendingInteraction = null;
|
|
2417
2226
|
this.pendingSecretRetry = null;
|
|
@@ -2454,20 +2263,25 @@ Think through this carefully, then:
|
|
|
2454
2263
|
return;
|
|
2455
2264
|
}
|
|
2456
2265
|
this.isProcessing = true;
|
|
2457
|
-
this.resetThinkingState(); // Reset thinking block styling state
|
|
2458
2266
|
const requestStartTime = Date.now(); // Alpha Zero 2 timing
|
|
2267
|
+
// Keep persistent prompt visible during processing so users can type follow-up requests
|
|
2268
|
+
// The prompt will show a "processing" indicator but remain interactive
|
|
2269
|
+
this.persistentPrompt.updateStatusBar({ message: '⏳ Processing... (type to queue follow-up)' });
|
|
2459
2270
|
// Update pinned chat box to show processing state
|
|
2271
|
+
// Clear the input display since the request was already submitted
|
|
2272
|
+
// Note: Don't set statusMessage here - the isProcessing flag already shows "⏳ Processing..."
|
|
2460
2273
|
this.pinnedChatBox.setProcessing(true);
|
|
2461
|
-
this.pinnedChatBox.setStatusMessage(null);
|
|
2274
|
+
this.pinnedChatBox.setStatusMessage(null); // Clear any previous status to avoid duplication
|
|
2462
2275
|
this.pinnedChatBox.clearInput();
|
|
2463
|
-
// Add newline so user's submitted input stays visible
|
|
2464
|
-
// (readline already displayed their input, we just need to preserve it)
|
|
2465
|
-
process.stdout.write('\n');
|
|
2466
|
-
// Note: Don't render pinned box during streaming - it interferes with content
|
|
2467
|
-
// The spinner will handle showing activity
|
|
2468
2276
|
this.uiAdapter.startProcessing('Working on your request');
|
|
2469
2277
|
this.setProcessingStatus();
|
|
2470
2278
|
try {
|
|
2279
|
+
display.newLine();
|
|
2280
|
+
// Pinned chat box already shows processing state - skip redundant spinner
|
|
2281
|
+
// which would conflict with the pinned area at terminal bottom
|
|
2282
|
+
// display.showThinking('Working on your request...');
|
|
2283
|
+
// Force render the pinned chat box to ensure it's visible during processing
|
|
2284
|
+
this.pinnedChatBox.forceRender();
|
|
2471
2285
|
// Enable streaming for real-time text output (Claude Code style)
|
|
2472
2286
|
await agent.send(request, true);
|
|
2473
2287
|
await this.awaitPendingCleanup();
|
|
@@ -2489,21 +2303,14 @@ Think through this carefully, then:
|
|
|
2489
2303
|
this.isProcessing = false;
|
|
2490
2304
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
2491
2305
|
this.setIdleStatus();
|
|
2492
|
-
|
|
2493
|
-
this.pinnedChatBox.clear();
|
|
2494
|
-
this.pinnedChatBox.setProcessing(false);
|
|
2495
|
-
this.pinnedChatBox.setStatusMessage(null);
|
|
2496
|
-
// SAFETY: Clear any stale composable message state to prevent blocking follow-ups
|
|
2497
|
-
// This ensures the user can type normal follow-up prompts without being in "paste mode"
|
|
2498
|
-
if (this.composableMessage.hasContent()) {
|
|
2499
|
-
this.composableMessage.clear();
|
|
2500
|
-
}
|
|
2501
|
-
this.bracketedPaste.clearPasteJustCaptured();
|
|
2502
|
-
// Also reset any stuck paste state to ensure clean input handling
|
|
2503
|
-
this.bracketedPaste.reset();
|
|
2306
|
+
display.newLine();
|
|
2504
2307
|
// Clear the processing status and ensure persistent prompt is visible
|
|
2505
2308
|
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
2506
2309
|
this.persistentPrompt.show();
|
|
2310
|
+
// Update pinned chat box to show ready state and force render
|
|
2311
|
+
this.pinnedChatBox.setProcessing(false);
|
|
2312
|
+
this.pinnedChatBox.setStatusMessage(null);
|
|
2313
|
+
this.pinnedChatBox.forceRender();
|
|
2507
2314
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
2508
2315
|
// This is a safety net in case the caller doesn't call rl.prompt()
|
|
2509
2316
|
this.rl.prompt();
|
|
@@ -2721,11 +2528,10 @@ What's the next action?`;
|
|
|
2721
2528
|
// Clear the processing status and ensure persistent prompt is visible
|
|
2722
2529
|
this.persistentPrompt.updateStatusBar({ message: undefined });
|
|
2723
2530
|
this.persistentPrompt.show();
|
|
2724
|
-
//
|
|
2725
|
-
display.clearStreamingStatus();
|
|
2726
|
-
// Update pinned chat box to show ready state
|
|
2531
|
+
// Update pinned chat box to show ready state and force render
|
|
2727
2532
|
this.pinnedChatBox.setProcessing(false);
|
|
2728
2533
|
this.pinnedChatBox.setStatusMessage(null);
|
|
2534
|
+
this.pinnedChatBox.forceRender();
|
|
2729
2535
|
// CRITICAL: Ensure readline prompt is active for user input
|
|
2730
2536
|
// This is a safety net in case the caller doesn't call rl.prompt()
|
|
2731
2537
|
this.rl.prompt();
|
|
@@ -2908,9 +2714,7 @@ What's the next action?`;
|
|
|
2908
2714
|
display.stopThinking(false);
|
|
2909
2715
|
process.stdout.write('\n'); // Newline after spinner
|
|
2910
2716
|
}
|
|
2911
|
-
|
|
2912
|
-
const styledChunk = this.styleStreamingChunk(chunk);
|
|
2913
|
-
process.stdout.write(styledChunk);
|
|
2717
|
+
process.stdout.write(chunk);
|
|
2914
2718
|
});
|
|
2915
2719
|
},
|
|
2916
2720
|
onAssistantMessage: (content, metadata) => {
|
|
@@ -2918,40 +2722,37 @@ What's the next action?`;
|
|
|
2918
2722
|
// Update spinner based on message type
|
|
2919
2723
|
if (metadata.isFinal) {
|
|
2920
2724
|
const parsed = this.splitThinkingResponse(content);
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2725
|
+
if (parsed?.thinking) {
|
|
2726
|
+
const summary = this.extractThoughtSummary(parsed.thinking);
|
|
2727
|
+
if (summary) {
|
|
2728
|
+
display.updateThinking(`💭 ${summary}`);
|
|
2729
|
+
}
|
|
2730
|
+
display.showAssistantMessage(parsed.thinking, { ...enriched, isFinal: false });
|
|
2731
|
+
}
|
|
2732
|
+
const finalContent = parsed?.response?.trim() || content;
|
|
2924
2733
|
if (finalContent) {
|
|
2925
2734
|
display.showAssistantMessage(finalContent, enriched);
|
|
2926
2735
|
}
|
|
2927
|
-
//
|
|
2928
|
-
this.lastAssistantResponse = content;
|
|
2929
|
-
// Auto-verify if response contains verifiable claims
|
|
2930
|
-
this.scheduleAutoVerification(content);
|
|
2931
|
-
// Show status line at end (Claude Code style: "Session 5m • Context X% used • Ready for prompts (2s)")
|
|
2736
|
+
// Show status line at end (Claude Code style: "• Context X% used • Ready for prompts (2s)")
|
|
2932
2737
|
display.stopThinking();
|
|
2933
|
-
// Calculate context usage
|
|
2934
|
-
|
|
2935
|
-
let contextInfo = { sessionElapsedMs };
|
|
2738
|
+
// Calculate context usage
|
|
2739
|
+
let contextInfo;
|
|
2936
2740
|
if (enriched.contextWindowTokens && metadata.usage) {
|
|
2937
2741
|
const total = this.totalTokens(metadata.usage);
|
|
2938
2742
|
if (total && total > 0) {
|
|
2939
2743
|
const percentage = Math.round((total / enriched.contextWindowTokens) * 100);
|
|
2940
|
-
contextInfo = {
|
|
2744
|
+
contextInfo = { percentage, tokens: total };
|
|
2941
2745
|
}
|
|
2942
2746
|
}
|
|
2943
|
-
|
|
2944
|
-
const trimmedContent = finalContent?.trim() || content.trim();
|
|
2945
|
-
const endsWithQuestion = trimmedContent.endsWith('?');
|
|
2946
|
-
const statusText = endsWithQuestion ? 'Awaiting reply' : 'Ready for prompts';
|
|
2947
|
-
display.showStatusLine(statusText, enriched.elapsedMs, contextInfo);
|
|
2747
|
+
display.showStatusLine('Ready for prompts', enriched.elapsedMs, contextInfo);
|
|
2948
2748
|
}
|
|
2949
2749
|
else {
|
|
2950
|
-
// Non-final message = narrative text before tool calls
|
|
2951
|
-
//
|
|
2952
|
-
// Don't display it again - just stop the spinner and continue
|
|
2750
|
+
// Non-final message = narrative text before tool calls (Claude Code style)
|
|
2751
|
+
// Stop spinner and show the narrative text directly
|
|
2953
2752
|
display.stopThinking();
|
|
2954
|
-
|
|
2753
|
+
display.showNarrative(content.trim());
|
|
2754
|
+
// The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
|
|
2755
|
+
this.pinnedChatBox.forceRender();
|
|
2955
2756
|
return;
|
|
2956
2757
|
}
|
|
2957
2758
|
const cleanup = this.handleContextTelemetry(metadata, enriched);
|
|
@@ -2999,12 +2800,6 @@ What's the next action?`;
|
|
|
2999
2800
|
this.pinnedChatBox.setStatusMessage('Retrying with reduced context...');
|
|
3000
2801
|
this.pinnedChatBox.forceRender();
|
|
3001
2802
|
},
|
|
3002
|
-
onAutoContinue: (attempt, maxAttempts, message) => {
|
|
3003
|
-
// Update UI to show auto-continuation is happening
|
|
3004
|
-
display.showSystemMessage(`🔄 ${message} (${attempt}/${maxAttempts})`);
|
|
3005
|
-
this.pinnedChatBox.setStatusMessage('Auto-continuing...');
|
|
3006
|
-
this.pinnedChatBox.forceRender();
|
|
3007
|
-
},
|
|
3008
2803
|
});
|
|
3009
2804
|
const historyToLoad = (this.pendingHistoryLoad && this.pendingHistoryLoad.length
|
|
3010
2805
|
? this.pendingHistoryLoad
|
|
@@ -3389,6 +3184,27 @@ What's the next action?`;
|
|
|
3389
3184
|
const fileChangesText = `${summary.files} file${summary.files === 1 ? '' : 's'} +${summary.additions} -${summary.removals}`;
|
|
3390
3185
|
this.persistentPrompt.updateStatusBar({ fileChanges: fileChangesText });
|
|
3391
3186
|
}
|
|
3187
|
+
extractThoughtSummary(thought) {
|
|
3188
|
+
// Extract first non-empty line
|
|
3189
|
+
const lines = thought?.split('\n').filter(line => line.trim()) ?? [];
|
|
3190
|
+
if (!lines.length) {
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
// Remove common thought prefixes
|
|
3194
|
+
const cleaned = lines[0]
|
|
3195
|
+
.trim()
|
|
3196
|
+
.replace(/^(Thinking|Analyzing|Considering|Looking at|Let me)[:.\s]+/i, '')
|
|
3197
|
+
.replace(/^I (should|need to|will|am)[:.\s]+/i, '')
|
|
3198
|
+
.trim();
|
|
3199
|
+
if (!cleaned) {
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
// Truncate to reasonable length
|
|
3203
|
+
const maxLength = 50;
|
|
3204
|
+
return cleaned.length > maxLength
|
|
3205
|
+
? cleaned.slice(0, maxLength - 3) + '...'
|
|
3206
|
+
: cleaned;
|
|
3207
|
+
}
|
|
3392
3208
|
splitThinkingResponse(content) {
|
|
3393
3209
|
if (!content?.includes('<thinking') && !content?.includes('<response')) {
|
|
3394
3210
|
return null;
|
|
@@ -3411,61 +3227,6 @@ What's the next action?`;
|
|
|
3411
3227
|
response: responseBody ?? '',
|
|
3412
3228
|
};
|
|
3413
3229
|
}
|
|
3414
|
-
/**
|
|
3415
|
-
* Style streaming chunks in real-time (Claude Code style)
|
|
3416
|
-
* Detects <thinking> blocks and applies cyan styling, hides XML tags
|
|
3417
|
-
*/
|
|
3418
|
-
styleStreamingChunk(chunk) {
|
|
3419
|
-
let result = '';
|
|
3420
|
-
let remaining = chunk;
|
|
3421
|
-
while (remaining.length > 0) {
|
|
3422
|
-
if (this.isInsideThinkingBlock) {
|
|
3423
|
-
// Look for </thinking> end tag
|
|
3424
|
-
const endIdx = remaining.indexOf('</thinking>');
|
|
3425
|
-
if (endIdx !== -1) {
|
|
3426
|
-
// End of thinking block found
|
|
3427
|
-
const thinkingContent = remaining.slice(0, endIdx);
|
|
3428
|
-
// Apply cyan thinking styling to content (hide the closing tag)
|
|
3429
|
-
result += theme.thinking.text(thinkingContent);
|
|
3430
|
-
remaining = remaining.slice(endIdx + '</thinking>'.length);
|
|
3431
|
-
this.isInsideThinkingBlock = false;
|
|
3432
|
-
// Add separator and newline after thinking block ends
|
|
3433
|
-
result += `\n${theme.thinking.border('─'.repeat(40))}\n`;
|
|
3434
|
-
}
|
|
3435
|
-
else {
|
|
3436
|
-
// Still inside thinking block, apply cyan styling to all remaining
|
|
3437
|
-
result += theme.thinking.text(remaining);
|
|
3438
|
-
remaining = '';
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
else {
|
|
3442
|
-
// Look for <thinking> start tag
|
|
3443
|
-
const startIdx = remaining.indexOf('<thinking>');
|
|
3444
|
-
if (startIdx !== -1) {
|
|
3445
|
-
// Output text before thinking tag normally
|
|
3446
|
-
if (startIdx > 0) {
|
|
3447
|
-
result += remaining.slice(0, startIdx);
|
|
3448
|
-
}
|
|
3449
|
-
// Show thinking header with cyan styling (Claude Code style)
|
|
3450
|
-
result += `${theme.thinking.icon('💭')} ${theme.thinking.label('Thinking')}\n`;
|
|
3451
|
-
remaining = remaining.slice(startIdx + '<thinking>'.length);
|
|
3452
|
-
this.isInsideThinkingBlock = true;
|
|
3453
|
-
}
|
|
3454
|
-
else {
|
|
3455
|
-
// No thinking tag, output normally
|
|
3456
|
-
result += remaining;
|
|
3457
|
-
remaining = '';
|
|
3458
|
-
}
|
|
3459
|
-
}
|
|
3460
|
-
}
|
|
3461
|
-
return result;
|
|
3462
|
-
}
|
|
3463
|
-
/**
|
|
3464
|
-
* Reset thinking block state (call at start of new request)
|
|
3465
|
-
*/
|
|
3466
|
-
resetThinkingState() {
|
|
3467
|
-
this.isInsideThinkingBlock = false;
|
|
3468
|
-
}
|
|
3469
3230
|
persistSessionPreference() {
|
|
3470
3231
|
saveModelPreference(this.profile, {
|
|
3471
3232
|
provider: this.sessionState.provider,
|