erosolar-cli 1.7.167 → 1.7.169

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') {
@@ -975,7 +990,7 @@ export class InteractiveShell {
975
990
  * - Longer pastes (3+ lines) show as collapsed block chips
976
991
  * Supports multiple pastes - user can paste multiple times before submitting.
977
992
  */
978
- capturePaste(content, lineCount) {
993
+ async capturePaste(content, lineCount) {
979
994
  this.resetBufferedInputLines();
980
995
  // Short pastes (1-2 lines) display inline like normal text
981
996
  const isShortPaste = lineCount <= 2;
@@ -1010,7 +1025,26 @@ export class InteractiveShell {
1010
1025
  return;
1011
1026
  }
1012
1027
  // For longer pastes (3+ lines), store as a composable block
1013
- 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
+ }
1014
1048
  // Clear remaining echoed lines from terminal
1015
1049
  output.write('\r\x1b[K');
1016
1050
  // Build the paste chips to show inline with prompt
@@ -1051,6 +1085,13 @@ export class InteractiveShell {
1051
1085
  /**
1052
1086
  * Show paste block editor (Ctrl+G)
1053
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
1054
1095
  */
1055
1096
  showPasteBlockEditor() {
1056
1097
  const state = this.composableMessage.getState();
@@ -1061,19 +1102,30 @@ export class InteractiveShell {
1061
1102
  return;
1062
1103
  }
1063
1104
  output.write('\n');
1064
- display.showSystemMessage('Pasted Content Blocks:');
1105
+ display.showSystemMessage('📋 Paste Block Editor');
1065
1106
  output.write('\n');
1066
1107
  // Display each paste block with preview
1067
1108
  pasteBlocks.forEach((block, index) => {
1068
1109
  const lines = block.content.split('\n');
1069
1110
  const preview = lines.slice(0, 3).map((l) => ` ${l.slice(0, 60)}${l.length > 60 ? '...' : ''}`).join('\n');
1070
1111
  const moreLines = lines.length > 3 ? `\n ... +${lines.length - 3} more lines` : '';
1071
- output.write(theme.ui.muted(`[${index + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + '\n');
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');
1072
1114
  output.write(theme.secondary(preview + moreLines) + '\n\n');
1073
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
+ }
1074
1124
  output.write(theme.ui.muted('Commands: ') + '\n');
1075
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');
1076
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');
1077
1129
  output.write(theme.ui.muted(' • "clear" to remove all blocks') + '\n');
1078
1130
  output.write(theme.ui.muted(' • Enter to return to prompt') + '\n');
1079
1131
  output.write('\n');
@@ -1095,6 +1147,36 @@ export class InteractiveShell {
1095
1147
  this.rl.prompt();
1096
1148
  return;
1097
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
+ }
1098
1180
  if (trimmed === 'clear') {
1099
1181
  this.composableMessage.clear();
1100
1182
  this.updateComposeStatusSummary();
@@ -1106,6 +1188,30 @@ export class InteractiveShell {
1106
1188
  this.rl.prompt();
1107
1189
  return;
1108
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
+ }
1109
1215
  // Handle "remove N" command
1110
1216
  const removeMatch = trimmed.match(/^remove\s+(\d+)$/);
1111
1217
  if (removeMatch) {
@@ -1115,12 +1221,14 @@ export class InteractiveShell {
1115
1221
  if (block) {
1116
1222
  this.composableMessage.removePart(block.id);
1117
1223
  this.updateComposeStatusSummary();
1118
- // Rebuild paste chips display
1119
- const chips = this.composableMessage.formatPasteChips();
1120
- this.persistentPrompt.updateInput(chips ? chips + ' ' : '', chips ? chips.length + 1 : 0);
1121
- this.rl.line = chips ? chips + ' ' : '';
1122
- this.rl.cursor = chips ? chips.length + 1 : 0;
1224
+ this.refreshPasteChipsDisplay();
1123
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
+ }
1124
1232
  }
1125
1233
  }
1126
1234
  else {
@@ -1148,9 +1256,103 @@ export class InteractiveShell {
1148
1256
  this.rl.prompt();
1149
1257
  return;
1150
1258
  }
1151
- display.showWarning('Enter a block number, "remove N", "clear", or press Enter to return.');
1259
+ display.showWarning('Enter a block number, "edit N", "remove N", "undo", "redo", "clear", or press Enter to return.');
1152
1260
  this.rl.prompt();
1153
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
+ }
1154
1356
  async flushBufferedInput() {
1155
1357
  if (!this.bufferedInputLines.length) {
1156
1358
  this.bufferedInputTimer = null;
@@ -1470,6 +1672,9 @@ export class InteractiveShell {
1470
1672
  case 'paste-edit':
1471
1673
  await this.handlePasteEditInput(input);
1472
1674
  return true;
1675
+ case 'paste-edit-block':
1676
+ await this.handlePasteBlockEditInput(input, this.pendingInteraction.blockId, this.pendingInteraction.blockNum);
1677
+ return true;
1473
1678
  default:
1474
1679
  return false;
1475
1680
  }
@@ -1584,6 +1789,100 @@ export class InteractiveShell {
1584
1789
  ];
1585
1790
  display.showSystemMessage(info.join('\n'));
1586
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
+ }
1587
1886
  runDoctor() {
1588
1887
  const lines = [];
1589
1888
  lines.push(theme.bold('Environment diagnostics'));