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.
- package/dist/shell/composableMessage.d.ts +57 -1
- package/dist/shell/composableMessage.d.ts.map +1 -1
- package/dist/shell/composableMessage.js +147 -1
- package/dist/shell/composableMessage.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +31 -0
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +314 -15
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/tools/diffUtils.d.ts +23 -1
- package/dist/tools/diffUtils.d.ts.map +1 -1
- package/dist/tools/diffUtils.js +245 -1
- package/dist/tools/diffUtils.js.map +1 -1
- package/dist/tools/editTools.d.ts.map +1 -1
- package/dist/tools/editTools.js +27 -18
- package/dist/tools/editTools.js.map +1 -1
- package/dist/ui/shortcutsHelp.d.ts.map +1 -1
- package/dist/ui/shortcutsHelp.js +1 -0
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
593
|
-
if (key.name === 'tab' && key.shift
|
|
594
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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'));
|