erosolar-cli 1.7.166 → 1.7.168

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.
@@ -471,7 +471,7 @@ export class InteractiveShell {
471
471
  this.clearMultiLinePastePreview();
472
472
  const lineCount = normalized.lineCount ?? normalized.result.split('\n').length;
473
473
  // All pastes (single or multi-line) are captured for confirmation before submit
474
- this.capturePaste(normalized.result, lineCount);
474
+ void this.capturePaste(normalized.result, lineCount);
475
475
  return;
476
476
  }
477
477
  return;
@@ -546,7 +546,7 @@ export class InteractiveShell {
546
546
  const lines = content.split('\n');
547
547
  const lineCount = lines.length;
548
548
  // All pastes (single or multi-line) are captured for confirmation before submit
549
- this.capturePaste(content, lineCount);
549
+ void this.capturePaste(content, lineCount);
550
550
  });
551
551
  // Set up raw data interception to catch bracketed paste before readline processes it.
552
552
  // We need to actually PREVENT readline from seeing the paste content to avoid echo.
@@ -589,9 +589,24 @@ export class InteractiveShell {
589
589
  this.keypressHandler = (_str, key) => {
590
590
  // Handle special keys
591
591
  if (key) {
592
- // Shift+Tab for profile switching
593
- if (key.name === 'tab' && key.shift && this.agentMenu) {
594
- this.showProfileSwitcher();
592
+ // Shift+Tab: Cycle paste preview options if paste blocks exist, otherwise profile switching
593
+ if (key.name === 'tab' && key.shift) {
594
+ // Check if there are paste blocks to cycle through
595
+ const pasteBlockCount = this.composableMessage.getPasteBlockCount();
596
+ if (pasteBlockCount > 0) {
597
+ this.cyclePastePreview();
598
+ return;
599
+ }
600
+ // Fall through to profile switching if no paste blocks
601
+ if (this.agentMenu) {
602
+ this.showProfileSwitcher();
603
+ return;
604
+ }
605
+ }
606
+ // Tab: Autocomplete slash commands
607
+ if (key.name === 'tab' && !key.shift && !key.ctrl) {
608
+ this.handleTabCompletion();
609
+ return;
595
610
  }
596
611
  // Escape: Cancel current operation if agent is running
597
612
  if (key.name === 'escape') {
@@ -638,6 +653,11 @@ export class InteractiveShell {
638
653
  }
639
654
  // If no text in buffer, let default Ctrl+C behavior exit
640
655
  }
656
+ // Ctrl+G: Edit pasted content blocks
657
+ if (key.ctrl && key.name === 'g') {
658
+ this.showPasteBlockEditor();
659
+ return;
660
+ }
641
661
  }
642
662
  // Readline handles all keyboard input natively (history, shortcuts, etc.)
643
663
  // We just sync the current state to our display components
@@ -970,7 +990,7 @@ export class InteractiveShell {
970
990
  * - Longer pastes (3+ lines) show as collapsed block chips
971
991
  * Supports multiple pastes - user can paste multiple times before submitting.
972
992
  */
973
- capturePaste(content, lineCount) {
993
+ async capturePaste(content, lineCount) {
974
994
  this.resetBufferedInputLines();
975
995
  // Short pastes (1-2 lines) display inline like normal text
976
996
  const isShortPaste = lineCount <= 2;
@@ -1005,7 +1025,26 @@ export class InteractiveShell {
1005
1025
  return;
1006
1026
  }
1007
1027
  // For longer pastes (3+ lines), store as a composable block
1008
- this.composableMessage.addPaste(content);
1028
+ // Check size limits first
1029
+ const { ComposableMessageBuilder } = await import('./composableMessage.js');
1030
+ const sizeCheck = ComposableMessageBuilder.checkPasteSize(content);
1031
+ if (!sizeCheck.ok) {
1032
+ // Paste rejected - show error and abort
1033
+ display.showError(sizeCheck.error || 'Paste rejected due to size limits');
1034
+ this.rl.prompt();
1035
+ return;
1036
+ }
1037
+ // Show warning for large (but acceptable) pastes
1038
+ if (sizeCheck.warning) {
1039
+ display.showWarning(`⚠️ ${sizeCheck.warning}`);
1040
+ }
1041
+ const pasteId = this.composableMessage.addPaste(content);
1042
+ if (!pasteId) {
1043
+ // Should not happen since we checked above, but handle defensively
1044
+ display.showError('Failed to store paste block');
1045
+ this.rl.prompt();
1046
+ return;
1047
+ }
1009
1048
  // Clear remaining echoed lines from terminal
1010
1049
  output.write('\r\x1b[K');
1011
1050
  // Build the paste chips to show inline with prompt
@@ -1043,6 +1082,277 @@ export class InteractiveShell {
1043
1082
  this.persistentPrompt.updateStatusBar({ message: undefined });
1044
1083
  }
1045
1084
  }
1085
+ /**
1086
+ * Show paste block editor (Ctrl+G)
1087
+ * Allows viewing and editing captured paste blocks before sending
1088
+ *
1089
+ * Features:
1090
+ * - View block content with line numbers
1091
+ * - Edit blocks inline (replace content)
1092
+ * - Remove individual blocks
1093
+ * - Undo/redo paste operations
1094
+ * - Clear all blocks
1095
+ */
1096
+ showPasteBlockEditor() {
1097
+ const state = this.composableMessage.getState();
1098
+ const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
1099
+ if (pasteBlocks.length === 0) {
1100
+ display.showInfo('No pasted content blocks to edit. Paste some content first.');
1101
+ this.rl.prompt();
1102
+ return;
1103
+ }
1104
+ output.write('\n');
1105
+ display.showSystemMessage('📋 Paste Block Editor');
1106
+ output.write('\n');
1107
+ // Display each paste block with preview
1108
+ pasteBlocks.forEach((block, index) => {
1109
+ const lines = block.content.split('\n');
1110
+ const preview = lines.slice(0, 3).map((l) => ` ${l.slice(0, 60)}${l.length > 60 ? '...' : ''}`).join('\n');
1111
+ const moreLines = lines.length > 3 ? `\n ... +${lines.length - 3} more lines` : '';
1112
+ const editedFlag = block.edited ? theme.warning(' [edited]') : '';
1113
+ output.write(theme.ui.muted(`[${index + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + editedFlag + '\n');
1114
+ output.write(theme.secondary(preview + moreLines) + '\n\n');
1115
+ });
1116
+ // Show undo/redo status
1117
+ const canUndo = this.composableMessage.canUndo();
1118
+ const canRedo = this.composableMessage.canRedo();
1119
+ if (canUndo || canRedo) {
1120
+ const undoStatus = canUndo ? theme.success('undo available') : theme.ui.muted('undo unavailable');
1121
+ const redoStatus = canRedo ? theme.success('redo available') : theme.ui.muted('redo unavailable');
1122
+ output.write(theme.ui.muted('History: ') + undoStatus + theme.ui.muted(' │ ') + redoStatus + '\n\n');
1123
+ }
1124
+ output.write(theme.ui.muted('Commands: ') + '\n');
1125
+ output.write(theme.ui.muted(' • Enter number to view full block') + '\n');
1126
+ output.write(theme.ui.muted(' • "edit N" to replace block N content') + '\n');
1127
+ output.write(theme.ui.muted(' • "remove N" to remove block N') + '\n');
1128
+ output.write(theme.ui.muted(' • "undo" / "redo" to undo/redo changes') + '\n');
1129
+ output.write(theme.ui.muted(' • "clear" to remove all blocks') + '\n');
1130
+ output.write(theme.ui.muted(' • Enter to return to prompt') + '\n');
1131
+ output.write('\n');
1132
+ // Set up interaction mode for paste editing
1133
+ this.pendingInteraction = { type: 'paste-edit' };
1134
+ this.rl.prompt();
1135
+ }
1136
+ /**
1137
+ * Handle paste block editor input
1138
+ */
1139
+ async handlePasteEditInput(trimmedInput) {
1140
+ const trimmed = trimmedInput.toLowerCase();
1141
+ const state = this.composableMessage.getState();
1142
+ const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
1143
+ if (!trimmed) {
1144
+ // Exit paste edit mode
1145
+ this.pendingInteraction = null;
1146
+ display.showInfo('Returned to prompt.');
1147
+ this.rl.prompt();
1148
+ return;
1149
+ }
1150
+ // Handle undo command
1151
+ if (trimmed === 'undo') {
1152
+ if (this.composableMessage.undo()) {
1153
+ this.updateComposeStatusSummary();
1154
+ this.refreshPasteChipsDisplay();
1155
+ display.showInfo('Undone.');
1156
+ // Refresh the paste block view
1157
+ this.showPasteBlockEditor();
1158
+ }
1159
+ else {
1160
+ display.showWarning('Nothing to undo.');
1161
+ this.rl.prompt();
1162
+ }
1163
+ return;
1164
+ }
1165
+ // Handle redo command
1166
+ if (trimmed === 'redo') {
1167
+ if (this.composableMessage.redo()) {
1168
+ this.updateComposeStatusSummary();
1169
+ this.refreshPasteChipsDisplay();
1170
+ display.showInfo('Redone.');
1171
+ // Refresh the paste block view
1172
+ this.showPasteBlockEditor();
1173
+ }
1174
+ else {
1175
+ display.showWarning('Nothing to redo.');
1176
+ this.rl.prompt();
1177
+ }
1178
+ return;
1179
+ }
1180
+ if (trimmed === 'clear') {
1181
+ this.composableMessage.clear();
1182
+ this.updateComposeStatusSummary();
1183
+ this.persistentPrompt.updateInput('', 0);
1184
+ this.rl.line = '';
1185
+ this.rl.cursor = 0;
1186
+ this.pendingInteraction = null;
1187
+ display.showInfo('Cleared all paste blocks.');
1188
+ this.rl.prompt();
1189
+ return;
1190
+ }
1191
+ // Handle "edit N" command - start inline editing mode
1192
+ const editMatch = trimmed.match(/^edit\s+(\d+)$/);
1193
+ if (editMatch) {
1194
+ const blockNum = parseInt(editMatch[1], 10);
1195
+ if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
1196
+ const block = pasteBlocks[blockNum - 1];
1197
+ if (block) {
1198
+ // Enter edit mode for this block
1199
+ this.pendingInteraction = { type: 'paste-edit-block', blockId: block.id, blockNum };
1200
+ output.write('\n');
1201
+ display.showSystemMessage(`Editing Block ${blockNum}:`);
1202
+ output.write(theme.ui.muted('Paste or type new content. Press Enter twice to finish, or "cancel" to abort.\n'));
1203
+ output.write('\n');
1204
+ // Show current content as reference
1205
+ const preview = block.content.split('\n').slice(0, 5).map((l) => theme.ui.muted(` ${l.slice(0, 60)}`)).join('\n');
1206
+ output.write(theme.ui.muted('Current content (first 5 lines):\n') + preview + '\n\n');
1207
+ }
1208
+ }
1209
+ else {
1210
+ display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
1211
+ }
1212
+ this.rl.prompt();
1213
+ return;
1214
+ }
1215
+ // Handle "remove N" command
1216
+ const removeMatch = trimmed.match(/^remove\s+(\d+)$/);
1217
+ if (removeMatch) {
1218
+ const blockNum = parseInt(removeMatch[1], 10);
1219
+ if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
1220
+ const block = pasteBlocks[blockNum - 1];
1221
+ if (block) {
1222
+ this.composableMessage.removePart(block.id);
1223
+ this.updateComposeStatusSummary();
1224
+ this.refreshPasteChipsDisplay();
1225
+ display.showInfo(`Removed block ${blockNum}.`);
1226
+ // Check if any blocks remain
1227
+ const remaining = this.composableMessage.getState().parts.filter(p => p.type === 'paste');
1228
+ if (remaining.length === 0) {
1229
+ this.pendingInteraction = null;
1230
+ display.showInfo('No more paste blocks.');
1231
+ }
1232
+ }
1233
+ }
1234
+ else {
1235
+ display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
1236
+ }
1237
+ this.rl.prompt();
1238
+ return;
1239
+ }
1240
+ // Handle viewing a block by number
1241
+ const blockNum = parseInt(trimmed, 10);
1242
+ if (!Number.isNaN(blockNum) && blockNum >= 1 && blockNum <= pasteBlocks.length) {
1243
+ const block = pasteBlocks[blockNum - 1];
1244
+ if (block) {
1245
+ output.write('\n');
1246
+ display.showSystemMessage(`Block ${blockNum} Content:`);
1247
+ output.write('\n');
1248
+ // Show full content with line numbers
1249
+ const lines = block.content.split('\n');
1250
+ lines.forEach((line, i) => {
1251
+ const lineNum = theme.ui.muted(`${String(i + 1).padStart(4)} │ `);
1252
+ output.write(lineNum + line + '\n');
1253
+ });
1254
+ output.write('\n');
1255
+ }
1256
+ this.rl.prompt();
1257
+ return;
1258
+ }
1259
+ display.showWarning('Enter a block number, "edit N", "remove N", "undo", "redo", "clear", or press Enter to return.');
1260
+ this.rl.prompt();
1261
+ }
1262
+ /**
1263
+ * Handle paste block inline editing mode
1264
+ */
1265
+ editBlockBuffer = [];
1266
+ async handlePasteBlockEditInput(input, blockId, blockNum) {
1267
+ // Check for cancel
1268
+ if (input.toLowerCase() === 'cancel') {
1269
+ this.editBlockBuffer = [];
1270
+ this.pendingInteraction = { type: 'paste-edit' };
1271
+ display.showInfo('Edit cancelled.');
1272
+ this.showPasteBlockEditor();
1273
+ return;
1274
+ }
1275
+ // Empty line - check if this is end of edit (double Enter)
1276
+ if (!input.trim()) {
1277
+ if (this.editBlockBuffer.length > 0) {
1278
+ // Complete the edit
1279
+ const newContent = this.editBlockBuffer.join('\n');
1280
+ this.composableMessage.editPaste(blockId, newContent);
1281
+ this.editBlockBuffer = [];
1282
+ this.updateComposeStatusSummary();
1283
+ this.refreshPasteChipsDisplay();
1284
+ display.showInfo(`Block ${blockNum} updated (${newContent.split('\n').length} lines).`);
1285
+ this.pendingInteraction = { type: 'paste-edit' };
1286
+ this.showPasteBlockEditor();
1287
+ return;
1288
+ }
1289
+ // First empty line with no buffer - just prompt again
1290
+ this.rl.prompt();
1291
+ return;
1292
+ }
1293
+ // Add line to edit buffer
1294
+ this.editBlockBuffer.push(input);
1295
+ this.rl.prompt();
1296
+ }
1297
+ /**
1298
+ * Refresh paste chips display in prompt
1299
+ */
1300
+ refreshPasteChipsDisplay() {
1301
+ const chips = this.composableMessage.formatPasteChips();
1302
+ this.persistentPrompt.updateInput(chips ? chips + ' ' : '', chips ? chips.length + 1 : 0);
1303
+ this.rl.line = chips ? chips + ' ' : '';
1304
+ this.rl.cursor = chips ? chips.length + 1 : 0;
1305
+ }
1306
+ /**
1307
+ * Cycle paste preview mode (Shift+Tab)
1308
+ * Cycles through: collapsed → summary → expanded
1309
+ */
1310
+ pastePreviewMode = 'collapsed';
1311
+ cyclePastePreview() {
1312
+ const state = this.composableMessage.getState();
1313
+ const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
1314
+ if (pasteBlocks.length === 0) {
1315
+ return;
1316
+ }
1317
+ // Cycle through modes
1318
+ const modes = ['collapsed', 'summary', 'expanded'];
1319
+ const currentIndex = modes.indexOf(this.pastePreviewMode);
1320
+ this.pastePreviewMode = modes[(currentIndex + 1) % modes.length];
1321
+ output.write('\n');
1322
+ switch (this.pastePreviewMode) {
1323
+ case 'collapsed':
1324
+ // Show just chips
1325
+ display.showSystemMessage(`📋 Preview: Collapsed (${pasteBlocks.length} block${pasteBlocks.length > 1 ? 's' : ''})`);
1326
+ output.write(theme.ui.muted(this.composableMessage.formatPasteChips()) + '\n');
1327
+ break;
1328
+ case 'summary':
1329
+ // Show blocks with first line preview
1330
+ display.showSystemMessage('📋 Preview: Summary');
1331
+ pasteBlocks.forEach((block, i) => {
1332
+ const preview = block.summary.length > 60 ? block.summary.slice(0, 57) + '...' : block.summary;
1333
+ output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount}L`) + theme.ui.muted(` ${preview}`) + '\n');
1334
+ });
1335
+ break;
1336
+ case 'expanded':
1337
+ // Show full content (up to 10 lines each)
1338
+ display.showSystemMessage('📋 Preview: Expanded');
1339
+ pasteBlocks.forEach((block, i) => {
1340
+ output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + '\n');
1341
+ const lines = block.content.split('\n').slice(0, 10);
1342
+ lines.forEach((line, j) => {
1343
+ const lineNum = theme.ui.muted(`${String(j + 1).padStart(3)} │ `);
1344
+ output.write(lineNum + line.slice(0, 80) + (line.length > 80 ? '...' : '') + '\n');
1345
+ });
1346
+ if (block.lineCount > 10) {
1347
+ output.write(theme.ui.muted(` ... +${block.lineCount - 10} more lines\n`));
1348
+ }
1349
+ output.write('\n');
1350
+ });
1351
+ break;
1352
+ }
1353
+ output.write(theme.ui.muted('(Shift+Tab to cycle preview modes)') + '\n\n');
1354
+ this.rl.prompt(true);
1355
+ }
1046
1356
  async flushBufferedInput() {
1047
1357
  if (!this.bufferedInputLines.length) {
1048
1358
  this.bufferedInputTimer = null;
@@ -1359,6 +1669,12 @@ export class InteractiveShell {
1359
1669
  case 'agent-selection':
1360
1670
  await this.handleAgentSelectionInput(input);
1361
1671
  return true;
1672
+ case 'paste-edit':
1673
+ await this.handlePasteEditInput(input);
1674
+ return true;
1675
+ case 'paste-edit-block':
1676
+ await this.handlePasteBlockEditInput(input, this.pendingInteraction.blockId, this.pendingInteraction.blockNum);
1677
+ return true;
1362
1678
  default:
1363
1679
  return false;
1364
1680
  }
@@ -1473,6 +1789,100 @@ export class InteractiveShell {
1473
1789
  ];
1474
1790
  display.showSystemMessage(info.join('\n'));
1475
1791
  }
1792
+ /**
1793
+ * Handle Tab key for slash command autocompletion
1794
+ * Completes partial slash commands like /mo -> /model
1795
+ */
1796
+ handleTabCompletion() {
1797
+ const currentLine = this.rl.line || '';
1798
+ const cursorPos = this.rl.cursor || 0;
1799
+ // Only complete if line starts with /
1800
+ if (!currentLine.startsWith('/')) {
1801
+ return;
1802
+ }
1803
+ // Get the partial command (from / to cursor)
1804
+ const partial = currentLine.slice(0, cursorPos).toLowerCase();
1805
+ // Available slash commands
1806
+ const commands = [
1807
+ '/help',
1808
+ '/model',
1809
+ '/secrets',
1810
+ '/tools',
1811
+ '/doctor',
1812
+ '/clear',
1813
+ '/cancel',
1814
+ '/compact',
1815
+ '/cost',
1816
+ '/status',
1817
+ '/undo',
1818
+ '/redo',
1819
+ ];
1820
+ // Find matching commands
1821
+ const matches = commands.filter(cmd => cmd.startsWith(partial));
1822
+ if (matches.length === 0) {
1823
+ // No matches - beep or do nothing
1824
+ return;
1825
+ }
1826
+ if (matches.length === 1) {
1827
+ // Single match - complete it
1828
+ const completion = matches[0];
1829
+ const suffix = currentLine.slice(cursorPos);
1830
+ const newLine = completion + suffix;
1831
+ const newCursor = completion.length;
1832
+ // Update readline
1833
+ this.rl.line = newLine;
1834
+ this.rl.cursor = newCursor;
1835
+ // Redraw
1836
+ output.write('\r\x1b[K'); // Clear line
1837
+ output.write(formatUserPrompt(this.profileLabel || this.profile));
1838
+ output.write(newLine);
1839
+ // Position cursor
1840
+ if (suffix.length > 0) {
1841
+ output.write(`\x1b[${suffix.length}D`);
1842
+ }
1843
+ // Sync to display components
1844
+ this.persistentPrompt.updateInput(newLine, newCursor);
1845
+ this.pinnedChatBox.setInput(newLine, newCursor);
1846
+ }
1847
+ else {
1848
+ // Multiple matches - show them
1849
+ output.write('\n');
1850
+ output.write(theme.ui.muted('Completions: ') + matches.join(' ') + '\n');
1851
+ this.rl.prompt();
1852
+ // Find common prefix for partial completion
1853
+ const commonPrefix = this.findCommonPrefix(matches);
1854
+ if (commonPrefix.length > partial.length) {
1855
+ const suffix = currentLine.slice(cursorPos);
1856
+ const newLine = commonPrefix + suffix;
1857
+ const newCursor = commonPrefix.length;
1858
+ this.rl.line = newLine;
1859
+ this.rl.cursor = newCursor;
1860
+ this.persistentPrompt.updateInput(newLine, newCursor);
1861
+ this.pinnedChatBox.setInput(newLine, newCursor);
1862
+ }
1863
+ }
1864
+ }
1865
+ /**
1866
+ * Find the longest common prefix among strings
1867
+ */
1868
+ findCommonPrefix(strings) {
1869
+ if (strings.length === 0)
1870
+ return '';
1871
+ if (strings.length === 1)
1872
+ return strings[0];
1873
+ let prefix = strings[0];
1874
+ for (let i = 1; i < strings.length; i++) {
1875
+ const str = strings[i];
1876
+ let j = 0;
1877
+ while (j < prefix.length && j < str.length && prefix[j] === str[j]) {
1878
+ j++;
1879
+ }
1880
+ prefix = prefix.slice(0, j);
1881
+ if (prefix.length === 0)
1882
+ break;
1883
+ }
1884
+ return prefix;
1885
+ }
1476
1886
  runDoctor() {
1477
1887
  const lines = [];
1478
1888
  lines.push(theme.bold('Environment diagnostics'));
@@ -2523,7 +2933,9 @@ export class InteractiveShell {
2523
2933
  try {
2524
2934
  // Add visual separator between user prompt and AI response
2525
2935
  display.newLine();
2526
- display.newLine();
2936
+ // Claude Code style: Show unified streaming header before response
2937
+ // This provides visual consistency with the startup Ready bar
2938
+ display.showStreamingHeader();
2527
2939
  // Claude Code style: Start streaming mode using UnifiedChatBox
2528
2940
  // - Output flows naturally to stdout (NO cursor manipulation)
2529
2941
  // - User input is captured invisibly and queued
@@ -2611,6 +3023,8 @@ export class InteractiveShell {
2611
3023
  display.showSystemMessage(`📊 Using intelligent task completion detection with AI verification.`);
2612
3024
  this.uiAdapter.startProcessing('Continuous execution mode');
2613
3025
  this.setProcessingStatus();
3026
+ // Claude Code style: Show unified streaming header before response
3027
+ display.showStreamingHeader();
2614
3028
  // Claude Code style: Start streaming mode using UnifiedChatBox
2615
3029
  // - Output flows naturally to stdout (NO cursor manipulation)
2616
3030
  // - User input is captured invisibly and queued