erosolar-cli 1.7.52 → 1.7.54

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.
@@ -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 streaming status line during tool execution
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
- // Update the streaming status line (Claude Code style)
180
- display.updateStreamingStatus(status);
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
- // Claude Code style: simple '> ' prompt
193
- prompt: '> ',
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
- // Process through bracketed paste manager which handles paste markers
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
- // NOTE: Raw data interception is disabled because it doesn't reliably prevent
498
- // readline from also receiving the data (prependListener doesn't block other handlers).
499
- // Instead, we rely on the line-based bracketed paste handling in the 'line' event
500
- // handler which processes the markers via bracketedPaste.process().
501
- //
502
- // The line-based approach works because:
503
- // 1. Readline splits paste content at newlines and emits 'line' events
504
- // 2. Each line event goes through bracketedPaste.process() which tracks markers
505
- // 3. Content between markers is accumulated and returned as a complete paste
506
- //
507
- // This is simpler and more reliable than trying to intercept raw data.
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
- try {
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 (include cursor position)
538
- this.pinnedChatBox.setInput(currentLine, cursorPos);
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
- // NOTE: Do NOT reset bracketedPaste here! Resetting clears the pasteJustCaptured
660
- // flag which is needed to ignore readline line events that come after a paste
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
- try {
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
- try {
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
- * All pastes are stored as composable blocks and shown as short descriptions.
779
- * The full content is sent to the AI when the user presses Enter.
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, _lineCount) {
800
+ capturePaste(content, lineCount) {
786
801
  this.resetBufferedInputLines();
787
- // If there's a pending interaction (menu selection, etc.), process the paste
788
- // as direct input to that interaction instead of storing it
789
- if (this.pendingInteraction) {
790
- // Clear any echoed content
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
- // Use only the first line for menu selections
793
- const firstLine = content.split('\n')[0]?.trim() || '';
794
- if (firstLine) {
795
- // Process through the normal input flow which handles pending interactions
796
- this.processInputBlock(firstLine).catch((err) => {
797
- display.showError(err instanceof Error ? err.message : String(err));
798
- });
799
- }
800
- return;
801
- }
802
- // Store ALL pastes (including short ones) as composable blocks
803
- // This ensures consistent behavior and avoids readline buffer issues
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 any echoed content from terminal
833
+ // Clear remaining echoed lines from terminal
806
834
  output.write('\r\x1b[K');
807
- // Build the paste description to show inline with prompt
808
- // Format: "[📋 Pasted: 15 lines] [📋 Pasted: 50 lines] "
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
- // Clear readline buffer completely to avoid duplication issues
815
- // Then show the paste chips in the prompt area without putting them in the buffer
816
- this.rl.write(null, { ctrl: true, name: 'u' }); // Clear line completely
817
- // Update persistent prompt display with paste chips
818
- this.persistentPrompt.updateInput(`${pasteChips} `, pasteChips.length + 1);
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 = 0;
848
+ this.rl.line = pasteChips + ' ';
849
+ this.rl.cursor = pasteChips.length + 1;
824
850
  }
825
- // Re-prompt without preserving cursor (fresh start)
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
- // With the new paste handling, chips are visual-only and NOT in the readline buffer
949
- // So userText is just the trimmed input directly (any text the user typed after pasting)
950
- const userText = trimmed;
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, this.workingDir);
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(`+${summary.additions}`)} ${theme.error(`-${summary.removals}`)} lines`);
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(`+${stats.additions}`)} ${theme.error(`-${stats.removals}`)}`;
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
- // Clear the pinned processing box before showing final output
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
- // Clear streaming status line (Claude Code style)
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
- // Style thinking blocks (Claude Code style)
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
- // Don't re-display thinking - it was already streamed in real-time
2922
- // Just extract the response part
2923
- const finalContent = parsed?.response?.trim() || content.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
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
- // Store last response for verification
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 and session time
2934
- const sessionElapsedMs = Date.now() - this.sessionStartTime;
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 = { ...contextInfo, percentage, tokens: total };
2744
+ contextInfo = { percentage, tokens: total };
2941
2745
  }
2942
2746
  }
2943
- // Check if the response ended with a question - show "Awaiting reply" instead
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
- // This content was already streamed in real-time via onStreamChunk
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
- // Continue processing - content already shown via streaming
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,