minovative-mind-cli 2.13.5 → 2.14.1
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/README.md +104 -39
- package/dist/services/agent/slashCommands.js +57 -8
- package/dist/services/agent/syntaxAgent.js +13 -0
- package/dist/services/agent-tools.d.ts +1 -1
- package/dist/services/agent-tools.js +2 -2
- package/dist/services/agent.js +119 -7
- package/dist/services/ai.d.ts +19 -0
- package/dist/services/ai.js +97 -5
- package/dist/services/contextAgent.js +23 -0
- package/dist/services/investigationComplexity.js +1 -1
- package/dist/services/mentionEngine.d.ts +385 -0
- package/dist/services/mentionEngine.js +1395 -0
- package/dist/services/orchestration/investigationAgent.js +6 -2
- package/dist/services/orchestration/messageBus.d.ts +26 -3
- package/dist/services/orchestration/messageBus.js +204 -14
- package/dist/services/orchestration/orchestrator.js +4 -0
- package/dist/services/orchestration/scopedTools.js +16 -2
- package/dist/services/orchestration/subAgent.js +14 -2
- package/dist/services/proxyClient.d.ts +38 -2
- package/dist/services/proxyClient.js +42 -24
- package/dist/utils/config.d.ts +75 -0
- package/dist/utils/config.js +93 -0
- package/dist/utils/contextPrompts.d.ts +28 -4
- package/dist/utils/contextPrompts.js +70 -1
- package/dist/utils/historyPrompt.d.ts +166 -7
- package/dist/utils/historyPrompt.js +775 -30
- package/dist/utils/symbolExtractor.d.ts +111 -8
- package/dist/utils/symbolExtractor.js +616 -64
- package/dist/utils/systemPrompts.d.ts +3 -3
- package/dist/utils/systemPrompts.js +5 -3
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import { TextPrompt } from '@clack/core';
|
|
2
4
|
import pc from 'picocolors';
|
|
5
|
+
import { distance } from 'fastest-levenshtein';
|
|
6
|
+
import { workspaceRegistry } from '../services/workspaceRegistry.js';
|
|
7
|
+
import { extractSymbolIndex } from './symbolExtractor.js';
|
|
3
8
|
/**
|
|
4
9
|
* Approximate token count for a given text string using character and word heuristics.
|
|
5
10
|
* Defaults to a safe ratio of ~3.8 characters per token for multi-language code and text.
|
|
@@ -46,6 +51,8 @@ export function calculateDynamicTokenBudget(config) {
|
|
|
46
51
|
* @param text The text to prune.
|
|
47
52
|
* @param maxTokens Maximum allowable tokens for this text.
|
|
48
53
|
* @param options Pruning options.
|
|
54
|
+
* @param options.truncationMarker Marker to append/prepend indicating truncation.
|
|
55
|
+
* @param options.fromStart If true, prunes from the start.
|
|
49
56
|
* @returns Pruned string, with an optional truncation marker.
|
|
50
57
|
*/
|
|
51
58
|
export function pruneTextToTokenBudget(text, maxTokens, options) {
|
|
@@ -97,11 +104,14 @@ export function optimizeHistoryForContext(history, maxEntries = 100) {
|
|
|
97
104
|
* pruning older turns if the total estimated tokens exceed the allocated budget.
|
|
98
105
|
*
|
|
99
106
|
* @param history Array of conversation turns with role and text content.
|
|
100
|
-
* @param maxTokens Maximum allowable tokens for the formatted history.
|
|
107
|
+
* @param maxTokens Maximum allowable tokens for the formatted history. Defaults to 16_384.
|
|
101
108
|
* @param options Formatting options.
|
|
109
|
+
* @param options.keepRecentTurns Minimum number of recent turns to always keep regardless of budget if possible. Defaults to 2.
|
|
110
|
+
* @param options.userLabel Custom label prefix for user turns. Defaults to "User".
|
|
111
|
+
* @param options.modelLabel Custom label prefix for assistant/model turns. Defaults to "Assistant".
|
|
102
112
|
* @returns Formatted and budget-constrained conversation history string.
|
|
103
113
|
*/
|
|
104
|
-
export function formatHistoryWithTokenBudget(history, maxTokens =
|
|
114
|
+
export function formatHistoryWithTokenBudget(history, maxTokens = 16_384, options) {
|
|
105
115
|
if (!history || history.length === 0 || maxTokens <= 0)
|
|
106
116
|
return '';
|
|
107
117
|
const keepRecent = options?.keepRecentTurns ?? 2;
|
|
@@ -169,6 +179,552 @@ const S_STEP_ACTIVE = '◆';
|
|
|
169
179
|
const S_STEP_CANCEL = '■';
|
|
170
180
|
const S_STEP_ERROR = '▲';
|
|
171
181
|
const S_STEP_SUBMIT = '◇';
|
|
182
|
+
const IGNORED_SCAN_DIRS = new Set([
|
|
183
|
+
'node_modules',
|
|
184
|
+
'.git',
|
|
185
|
+
'dist',
|
|
186
|
+
'build',
|
|
187
|
+
'.next',
|
|
188
|
+
'coverage',
|
|
189
|
+
'.turbo',
|
|
190
|
+
'.cache',
|
|
191
|
+
'out',
|
|
192
|
+
'.vscode',
|
|
193
|
+
'.idea',
|
|
194
|
+
]);
|
|
195
|
+
const EXCLUDED_SCAN_EXTENSIONS = new Set([
|
|
196
|
+
'.png',
|
|
197
|
+
'.jpg',
|
|
198
|
+
'.jpeg',
|
|
199
|
+
'.gif',
|
|
200
|
+
'.svg',
|
|
201
|
+
'.ico',
|
|
202
|
+
'.pdf',
|
|
203
|
+
'.zip',
|
|
204
|
+
'.tar',
|
|
205
|
+
'.gz',
|
|
206
|
+
'.lock',
|
|
207
|
+
'.pyc',
|
|
208
|
+
'.exe',
|
|
209
|
+
'.dylib',
|
|
210
|
+
'.so',
|
|
211
|
+
'.wasm',
|
|
212
|
+
'.ttf',
|
|
213
|
+
'.woff',
|
|
214
|
+
'.woff2',
|
|
215
|
+
'.eot',
|
|
216
|
+
'.mp4',
|
|
217
|
+
'.mp3',
|
|
218
|
+
'.wav',
|
|
219
|
+
]);
|
|
220
|
+
/**
|
|
221
|
+
* Extracts an active mention trigger token (such as `@query`) directly preceding the cursor.
|
|
222
|
+
*
|
|
223
|
+
* @param line - The full input line buffer.
|
|
224
|
+
* @param cursor - The current cursor index within the line buffer.
|
|
225
|
+
* @param triggers - Allowed trigger prefix characters (defaults to `['@']`).
|
|
226
|
+
* @returns An object containing query string and starting trigger index, or `null` if not active.
|
|
227
|
+
*/
|
|
228
|
+
export function extractMentionQuery(line, cursor, triggers = ['@']) {
|
|
229
|
+
if (typeof line !== 'string' || cursor < 0)
|
|
230
|
+
return null;
|
|
231
|
+
const textBeforeCursor = line.slice(0, cursor);
|
|
232
|
+
const triggerPattern = triggers.map((t) => (t === '@' ? '@' : `\\${t}`)).join('|');
|
|
233
|
+
const regex = new RegExp(`(?:^|[\\s([{\\'"<,:])(${triggerPattern})([^\\s)\\]}\\'">]*)$`);
|
|
234
|
+
const match = regex.exec(textBeforeCursor);
|
|
235
|
+
if (!match)
|
|
236
|
+
return null;
|
|
237
|
+
const trigger = match[1];
|
|
238
|
+
const query = match[2];
|
|
239
|
+
const triggerIndex = textBeforeCursor.length - query.length - trigger.length;
|
|
240
|
+
return {
|
|
241
|
+
query,
|
|
242
|
+
triggerIndex,
|
|
243
|
+
trigger,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Scores an autocomplete item against a search query for fuzzy and prefix ranking.
|
|
248
|
+
*
|
|
249
|
+
* @param item - The autocomplete candidate.
|
|
250
|
+
* @param query - The user search query without trigger prefix.
|
|
251
|
+
* @returns Numerical score (higher score = better match, 0 = no match).
|
|
252
|
+
*/
|
|
253
|
+
export function scoreAutocompleteItem(item, query) {
|
|
254
|
+
if (!query)
|
|
255
|
+
return 100;
|
|
256
|
+
const rawQ = query.toLowerCase().trim();
|
|
257
|
+
const valLower = (item.value || '').toLowerCase();
|
|
258
|
+
const labelLower = (item.label || '').toLowerCase();
|
|
259
|
+
if (valLower === rawQ || labelLower === rawQ || valLower === `@${rawQ}` || labelLower === `@${rawQ}`) {
|
|
260
|
+
return 1000;
|
|
261
|
+
}
|
|
262
|
+
// Normalize by stripping triggers/prefixes (@, #, :, /, symbol:, sym:, file:)
|
|
263
|
+
const normalize = (s) => (s || '')
|
|
264
|
+
.toLowerCase()
|
|
265
|
+
.replace(/^[@:/#]+/, '')
|
|
266
|
+
.replace(/^(?:symbol|sym|file):/, '')
|
|
267
|
+
.trim();
|
|
268
|
+
const q = normalize(query);
|
|
269
|
+
const rawVal = normalize(item.value);
|
|
270
|
+
const rawLabel = normalize(item.label);
|
|
271
|
+
if (!q)
|
|
272
|
+
return 100;
|
|
273
|
+
if (rawVal === q || rawLabel === q)
|
|
274
|
+
return 950;
|
|
275
|
+
if (rawVal.startsWith(q) || rawLabel.startsWith(q))
|
|
276
|
+
return 850 - Math.min(100, rawVal.length - q.length);
|
|
277
|
+
const baseName = (rawLabel.split('/').pop() || rawLabel).toLowerCase();
|
|
278
|
+
const baseNoExt = baseName.replace(/\.[^/.]+$/, '');
|
|
279
|
+
if (baseName.startsWith(q) || baseNoExt.startsWith(q))
|
|
280
|
+
return 750 - Math.min(100, baseName.length - q.length);
|
|
281
|
+
const idx = rawVal.indexOf(q);
|
|
282
|
+
if (idx !== -1)
|
|
283
|
+
return 500 - Math.min(100, idx);
|
|
284
|
+
const baseIdx = baseName.indexOf(q);
|
|
285
|
+
if (baseIdx !== -1)
|
|
286
|
+
return 600 - Math.min(100, baseIdx);
|
|
287
|
+
if (q.length >= 3) {
|
|
288
|
+
const dist = distance(q, baseNoExt.slice(0, Math.min(baseNoExt.length, q.length + 1)));
|
|
289
|
+
if (dist <= 2)
|
|
290
|
+
return 300 - dist * 50;
|
|
291
|
+
const fullDist = distance(q, baseNoExt);
|
|
292
|
+
if (fullDist <= 2)
|
|
293
|
+
return 300 - fullDist * 50;
|
|
294
|
+
}
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Filters and ranks suggestions according to match quality.
|
|
299
|
+
*
|
|
300
|
+
* @param items - Candidate autocomplete suggestions.
|
|
301
|
+
* @param query - The search query string.
|
|
302
|
+
* @param maxItems - Maximum number of top items to return.
|
|
303
|
+
* @returns Ranked subset of suggestions.
|
|
304
|
+
*/
|
|
305
|
+
export function filterAndRankSuggestions(items, query, maxItems = 10) {
|
|
306
|
+
if (!query)
|
|
307
|
+
return items.slice(0, maxItems);
|
|
308
|
+
const scored = [];
|
|
309
|
+
for (const item of items) {
|
|
310
|
+
const score = scoreAutocompleteItem(item, query);
|
|
311
|
+
if (score > 0) {
|
|
312
|
+
scored.push({ item, score });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
scored.sort((a, b) => b.score - a.score);
|
|
316
|
+
return scored.slice(0, maxItems).map((s) => s.item);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Formats a terminal-styled category badge with picocolors.
|
|
320
|
+
*
|
|
321
|
+
* @param category - Category name (e.g. 'lines', 'sym', 'git', 'diag', 'term', 'ws', 'file').
|
|
322
|
+
* @returns Formatted ANSI badge string.
|
|
323
|
+
*/
|
|
324
|
+
export function formatCategoryBadge(category) {
|
|
325
|
+
switch (category?.toLowerCase()) {
|
|
326
|
+
case 'lines':
|
|
327
|
+
case 'line':
|
|
328
|
+
case 'slice':
|
|
329
|
+
return pc.blue('[lines]');
|
|
330
|
+
case 'file':
|
|
331
|
+
return pc.cyan('[file]');
|
|
332
|
+
case 'symbol':
|
|
333
|
+
case 'sym':
|
|
334
|
+
return pc.magenta('[sym]');
|
|
335
|
+
case 'git':
|
|
336
|
+
case 'diff':
|
|
337
|
+
return pc.green('[git]');
|
|
338
|
+
case 'workspace':
|
|
339
|
+
case 'ws':
|
|
340
|
+
return pc.blue('[ws]');
|
|
341
|
+
case 'diag':
|
|
342
|
+
case 'diagnostics':
|
|
343
|
+
case 'errors':
|
|
344
|
+
case 'error':
|
|
345
|
+
return pc.yellow('[diag]');
|
|
346
|
+
case 'prob':
|
|
347
|
+
case 'problem':
|
|
348
|
+
case 'problems':
|
|
349
|
+
return pc.yellow('[prob]');
|
|
350
|
+
case 'terminal':
|
|
351
|
+
case 'term':
|
|
352
|
+
case 'console':
|
|
353
|
+
return pc.magenta('[term]');
|
|
354
|
+
case 'doc':
|
|
355
|
+
case 'docs':
|
|
356
|
+
return pc.cyan('[doc]');
|
|
357
|
+
default:
|
|
358
|
+
return pc.dim(`[${category || 'item'}]`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Renders an aesthetically styled autocomplete popup dropdown box for terminal display.
|
|
363
|
+
*
|
|
364
|
+
* @param suggestions - Current list of visible suggestions.
|
|
365
|
+
* @param selectedIndex - Currently selected item index.
|
|
366
|
+
* @param maxVisible - Maximum number of items visible simultaneously.
|
|
367
|
+
* @returns Multi-line ANSI string representing the autocomplete popup.
|
|
368
|
+
*/
|
|
369
|
+
export function renderAutocompletePopup(suggestions, selectedIndex, maxVisible = 10) {
|
|
370
|
+
if (!suggestions || suggestions.length === 0)
|
|
371
|
+
return '';
|
|
372
|
+
const total = suggestions.length;
|
|
373
|
+
const halfVisible = Math.floor(maxVisible / 2);
|
|
374
|
+
let startIndex = Math.max(0, selectedIndex - halfVisible);
|
|
375
|
+
const endIndex = Math.min(total, startIndex + maxVisible);
|
|
376
|
+
if (endIndex - startIndex < maxVisible && startIndex > 0) {
|
|
377
|
+
startIndex = Math.max(0, endIndex - maxVisible);
|
|
378
|
+
}
|
|
379
|
+
const visibleItems = suggestions.slice(startIndex, endIndex);
|
|
380
|
+
const lines = [];
|
|
381
|
+
const countBadge = pc.dim(`(${selectedIndex + 1}/${total})`);
|
|
382
|
+
lines.push(`${pc.cyan(S_BAR)} ${pc.dim('┌─')} ${pc.bold(pc.cyan('@ Context Mentions'))} ${countBadge} ${pc.dim('─'.repeat(24))}`);
|
|
383
|
+
for (const [idx, item] of visibleItems.entries()) {
|
|
384
|
+
const actualIndex = startIndex + idx;
|
|
385
|
+
const isSelected = actualIndex === selectedIndex;
|
|
386
|
+
const pointer = isSelected ? pc.cyan('❯ ') : ' ';
|
|
387
|
+
const badge = formatCategoryBadge(item.category);
|
|
388
|
+
const label = isSelected ? pc.bold(pc.white(item.label)) : pc.white(item.label);
|
|
389
|
+
const desc = item.description ? ` ${pc.dim(item.description)}` : '';
|
|
390
|
+
lines.push(`${pc.cyan(S_BAR)} ${pc.dim('│')} ${pointer}${badge} ${label}${desc}`);
|
|
391
|
+
}
|
|
392
|
+
lines.push(`${pc.cyan(S_BAR)} ${pc.dim('└' + '─'.repeat(48))}`);
|
|
393
|
+
lines.push(`${pc.cyan(S_BAR)} ${pc.cyan('Tab to insert • ↑/↓ navigate • Esc dismiss')}`);
|
|
394
|
+
return lines.join('\n');
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Resolver for dynamic workspace files, git actions, and registered external workspaces.
|
|
398
|
+
*/
|
|
399
|
+
export class ContextMentionResolver {
|
|
400
|
+
cachedFiles = null;
|
|
401
|
+
externalWorkspaceCache = new Map();
|
|
402
|
+
symbolCache = new Map();
|
|
403
|
+
cacheTimestamp = 0;
|
|
404
|
+
cacheTtlMs;
|
|
405
|
+
workspaceRoot;
|
|
406
|
+
constructor(workspaceRoot = process.cwd(), cacheTtlMs = 5000) {
|
|
407
|
+
this.workspaceRoot = workspaceRoot;
|
|
408
|
+
this.cacheTtlMs = cacheTtlMs;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Clears in-memory file scanning caches.
|
|
412
|
+
*/
|
|
413
|
+
clearCache() {
|
|
414
|
+
this.cachedFiles = null;
|
|
415
|
+
this.cacheTimestamp = 0;
|
|
416
|
+
this.externalWorkspaceCache.clear();
|
|
417
|
+
this.symbolCache.clear();
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Returns standard built-in git, diagnostic, terminal, and symbol context mention options.
|
|
421
|
+
*/
|
|
422
|
+
getBuiltInMentions() {
|
|
423
|
+
return [
|
|
424
|
+
{
|
|
425
|
+
value: '@git:diff',
|
|
426
|
+
label: 'git:diff',
|
|
427
|
+
category: 'git',
|
|
428
|
+
description: 'Working tree unstaged and staged changes',
|
|
429
|
+
metadata: { helperLabel: '[git] working tree diff' },
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
value: '@diff',
|
|
433
|
+
label: 'diff',
|
|
434
|
+
category: 'git',
|
|
435
|
+
description: 'Quick uncommitted diff alias',
|
|
436
|
+
metadata: { helperLabel: '[git] uncommitted diff' },
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
value: '@git:staged',
|
|
440
|
+
label: 'git:staged',
|
|
441
|
+
category: 'git',
|
|
442
|
+
description: 'Staged git changes only',
|
|
443
|
+
metadata: { helperLabel: '[git] staged changes' },
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
value: '@git:status',
|
|
447
|
+
label: 'git:status',
|
|
448
|
+
category: 'git',
|
|
449
|
+
description: 'Git status and untracked files',
|
|
450
|
+
metadata: { helperLabel: '[git] status summary' },
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
value: '@git:branch',
|
|
454
|
+
label: 'git:branch',
|
|
455
|
+
category: 'git',
|
|
456
|
+
description: 'Current git branch & HEAD commit',
|
|
457
|
+
metadata: { helperLabel: '[git] branch info' },
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
value: '@git:log',
|
|
461
|
+
label: 'git:log',
|
|
462
|
+
category: 'git',
|
|
463
|
+
description: 'Recent commit history log',
|
|
464
|
+
metadata: { helperLabel: '[git] recent commits' },
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
value: '@diagnostics',
|
|
468
|
+
label: 'diagnostics',
|
|
469
|
+
category: 'diag',
|
|
470
|
+
description: 'Workspace syntax and linter diagnostics',
|
|
471
|
+
metadata: { helperLabel: '[diag] workspace syntax scan' },
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
value: '@problems',
|
|
475
|
+
label: 'problems',
|
|
476
|
+
category: 'prob',
|
|
477
|
+
description: 'Workspace diagnostics / problem scan',
|
|
478
|
+
metadata: { helperLabel: '[prob] problem scan' },
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
value: '@errors',
|
|
482
|
+
label: 'errors',
|
|
483
|
+
category: 'diag',
|
|
484
|
+
description: 'Workspace errors & syntax diagnostics',
|
|
485
|
+
metadata: { helperLabel: '[diag] syntax error scan' },
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
value: '@terminal',
|
|
489
|
+
label: 'terminal',
|
|
490
|
+
category: 'term',
|
|
491
|
+
description: 'Recent terminal output and shell environment',
|
|
492
|
+
metadata: { helperLabel: '[term] shell environment' },
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
value: '@console',
|
|
496
|
+
label: 'console',
|
|
497
|
+
category: 'term',
|
|
498
|
+
description: 'Console environment and terminal session',
|
|
499
|
+
metadata: { helperLabel: '[term] console session' },
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
value: '@symbol:',
|
|
503
|
+
label: 'symbol:<name>',
|
|
504
|
+
category: 'sym',
|
|
505
|
+
description: 'AST symbol extraction (@symbol:name, @#name)',
|
|
506
|
+
metadata: { helperLabel: '[sym] AST symbol extraction' },
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Scans and caches workspace relative file paths up to depth limit.
|
|
512
|
+
*
|
|
513
|
+
* @param targetRoot - Directory root to scan. Defaults to this.workspaceRoot.
|
|
514
|
+
* @returns Array of relative file paths within the target root.
|
|
515
|
+
*/
|
|
516
|
+
getWorkspaceFiles(targetRoot = this.workspaceRoot) {
|
|
517
|
+
const resolvedTarget = path.resolve(targetRoot);
|
|
518
|
+
const isPrimary = resolvedTarget === path.resolve(this.workspaceRoot);
|
|
519
|
+
const now = Date.now();
|
|
520
|
+
if (isPrimary && this.cachedFiles && now - this.cacheTimestamp < this.cacheTtlMs) {
|
|
521
|
+
return this.cachedFiles;
|
|
522
|
+
}
|
|
523
|
+
const externalEntry = this.externalWorkspaceCache.get(resolvedTarget);
|
|
524
|
+
if (!isPrimary && externalEntry && now - externalEntry.timestamp < this.cacheTtlMs) {
|
|
525
|
+
return externalEntry.files;
|
|
526
|
+
}
|
|
527
|
+
const files = [];
|
|
528
|
+
const scan = (dir, depth = 0) => {
|
|
529
|
+
if (depth > 5)
|
|
530
|
+
return;
|
|
531
|
+
try {
|
|
532
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
533
|
+
for (const entry of entries) {
|
|
534
|
+
if (entry.name.startsWith('.') && entry.name !== '.env')
|
|
535
|
+
continue;
|
|
536
|
+
const fullPath = path.join(dir, entry.name);
|
|
537
|
+
const relPath = path.relative(resolvedTarget, fullPath).replace(/\\/g, '/');
|
|
538
|
+
if (entry.isDirectory()) {
|
|
539
|
+
if (!IGNORED_SCAN_DIRS.has(entry.name)) {
|
|
540
|
+
scan(fullPath, depth + 1);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else if (entry.isFile()) {
|
|
544
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
545
|
+
if (!EXCLUDED_SCAN_EXTENSIONS.has(ext)) {
|
|
546
|
+
files.push(relPath);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// Ignore unreadable directories
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
scan(resolvedTarget);
|
|
556
|
+
if (isPrimary) {
|
|
557
|
+
this.cachedFiles = files;
|
|
558
|
+
this.cacheTimestamp = now;
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
this.externalWorkspaceCache.set(resolvedTarget, { files, timestamp: now });
|
|
562
|
+
}
|
|
563
|
+
return files;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Returns registered external workspaces as autocomplete items.
|
|
567
|
+
*/
|
|
568
|
+
getWorkspaceMentions() {
|
|
569
|
+
try {
|
|
570
|
+
const list = workspaceRegistry.list();
|
|
571
|
+
return list.map((ws) => ({
|
|
572
|
+
value: `@${ws.alias}/`,
|
|
573
|
+
label: `@${ws.alias}/`,
|
|
574
|
+
category: 'workspace',
|
|
575
|
+
description: `External workspace: ${ws.absolutePath}`,
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Collects base mention items including builtins, registered workspace descriptors,
|
|
584
|
+
* local files, and external workspace files if matching an external workspace alias query.
|
|
585
|
+
*/
|
|
586
|
+
collectMentionItems(cleanQuery) {
|
|
587
|
+
const allItems = [
|
|
588
|
+
...this.getBuiltInMentions(),
|
|
589
|
+
...this.getWorkspaceMentions(),
|
|
590
|
+
];
|
|
591
|
+
// Check if query targets an external workspace alias (@<alias>/... or <alias>/...)
|
|
592
|
+
const slashIdx = cleanQuery.indexOf('/');
|
|
593
|
+
if (slashIdx !== -1) {
|
|
594
|
+
const candidateAlias = cleanQuery.slice(0, slashIdx).toLowerCase().trim();
|
|
595
|
+
const ws = workspaceRegistry.get(candidateAlias);
|
|
596
|
+
if (ws) {
|
|
597
|
+
const extFiles = this.getWorkspaceFiles(ws.absolutePath);
|
|
598
|
+
for (const file of extFiles) {
|
|
599
|
+
allItems.push({
|
|
600
|
+
value: `@${ws.alias}/${file}`,
|
|
601
|
+
label: `@${ws.alias}/${file}`,
|
|
602
|
+
category: 'ws',
|
|
603
|
+
description: `External file: ${ws.alias}`,
|
|
604
|
+
metadata: {
|
|
605
|
+
workspaceAlias: ws.alias,
|
|
606
|
+
filePath: file,
|
|
607
|
+
helperLabel: `[ws] @${ws.alias}/${file}`,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const files = this.getWorkspaceFiles();
|
|
614
|
+
for (const file of files) {
|
|
615
|
+
allItems.push({
|
|
616
|
+
value: `@${file}`,
|
|
617
|
+
label: file,
|
|
618
|
+
category: 'file',
|
|
619
|
+
description: 'Workspace file',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
// Extract symbols if query is symbol-related or has at least 2 characters
|
|
623
|
+
const isSymbolQuery = cleanQuery.startsWith('symbol:') ||
|
|
624
|
+
cleanQuery.startsWith('sym:') ||
|
|
625
|
+
cleanQuery.startsWith('#');
|
|
626
|
+
const isHash = cleanQuery.startsWith('#');
|
|
627
|
+
if (isSymbolQuery || cleanQuery.length >= 2) {
|
|
628
|
+
const sourceExts = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs']);
|
|
629
|
+
const sourceFiles = files
|
|
630
|
+
.filter((f) => sourceExts.has(path.extname(f).toLowerCase()))
|
|
631
|
+
.sort((a, b) => {
|
|
632
|
+
const aIsSrc = a.startsWith('src/') || a.startsWith('lib/') || a.startsWith('app/') ? 0 : 1;
|
|
633
|
+
const bIsSrc = b.startsWith('src/') || b.startsWith('lib/') || b.startsWith('app/') ? 0 : 1;
|
|
634
|
+
if (aIsSrc !== bIsSrc)
|
|
635
|
+
return aIsSrc - bIsSrc;
|
|
636
|
+
return a.localeCompare(b);
|
|
637
|
+
})
|
|
638
|
+
.slice(0, 150);
|
|
639
|
+
for (const relPath of sourceFiles) {
|
|
640
|
+
const fullPath = path.resolve(this.workspaceRoot, relPath);
|
|
641
|
+
try {
|
|
642
|
+
const stat = fs.statSync(fullPath);
|
|
643
|
+
let fileSymbols = this.symbolCache.get(fullPath);
|
|
644
|
+
if (!fileSymbols || fileSymbols.mtime !== stat.mtimeMs) {
|
|
645
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
646
|
+
const raw = extractSymbolIndex(content, fullPath);
|
|
647
|
+
fileSymbols = { mtime: stat.mtimeMs, symbols: raw };
|
|
648
|
+
this.symbolCache.set(fullPath, fileSymbols);
|
|
649
|
+
}
|
|
650
|
+
for (const s of fileSymbols.symbols) {
|
|
651
|
+
const prefix = isHash ? '@#' : '@symbol:';
|
|
652
|
+
allItems.push({
|
|
653
|
+
value: `${prefix}${s.symbol}`,
|
|
654
|
+
label: `${prefix}${s.symbol}`,
|
|
655
|
+
category: 'sym',
|
|
656
|
+
description: `${s.kind ? s.kind + ' in ' : ''}${relPath}`,
|
|
657
|
+
metadata: {
|
|
658
|
+
symbol: s.symbol,
|
|
659
|
+
filePath: relPath,
|
|
660
|
+
helperLabel: `[sym] ${s.symbol}`,
|
|
661
|
+
symbolKind: s.kind,
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
// ignore unreadable files
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return allItems;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Resolves, filters, and ranks suggestions matching the user's @ query.
|
|
675
|
+
*/
|
|
676
|
+
async resolveSuggestions(query, options) {
|
|
677
|
+
const q = (query || '').trim();
|
|
678
|
+
const cleanQuery = q.replace(/^@/, '');
|
|
679
|
+
const allItems = this.collectMentionItems(cleanQuery);
|
|
680
|
+
if (options?.getSuggestions) {
|
|
681
|
+
try {
|
|
682
|
+
const custom = await options.getSuggestions(cleanQuery, { fullText: query, cursor: query.length });
|
|
683
|
+
if (Array.isArray(custom)) {
|
|
684
|
+
allItems.push(...custom);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
// Fallback to default items on custom provider error
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const customFilter = options?.filterSuggestions || options?.filter;
|
|
692
|
+
if (typeof customFilter === 'function') {
|
|
693
|
+
return customFilter(allItems, cleanQuery);
|
|
694
|
+
}
|
|
695
|
+
const maxItems = options?.maxVisibleItems ?? 10;
|
|
696
|
+
return filterAndRankSuggestions(allItems, cleanQuery, maxItems);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Resolves, filters, and ranks suggestions synchronously for real-time prompt keystrokes.
|
|
700
|
+
*/
|
|
701
|
+
resolveSuggestionsSync(query, options) {
|
|
702
|
+
const q = (query || '').trim();
|
|
703
|
+
const cleanQuery = q.replace(/^@/, '');
|
|
704
|
+
const allItems = this.collectMentionItems(cleanQuery);
|
|
705
|
+
const customFilter = options?.filterSuggestions || options?.filter;
|
|
706
|
+
if (typeof customFilter === 'function') {
|
|
707
|
+
return customFilter(allItems, cleanQuery);
|
|
708
|
+
}
|
|
709
|
+
const maxItems = options?.maxVisibleItems ?? 10;
|
|
710
|
+
return filterAndRankSuggestions(allItems, cleanQuery, maxItems);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/** Default singleton resolver instance */
|
|
714
|
+
const defaultMentionResolver = new ContextMentionResolver();
|
|
715
|
+
/**
|
|
716
|
+
* Resolves context mentions for an @ query string.
|
|
717
|
+
*
|
|
718
|
+
* @param query - The search query string without the @ prefix.
|
|
719
|
+
* @param options - Optional autocomplete configuration.
|
|
720
|
+
* @returns A promise resolving to ranked autocomplete suggestions.
|
|
721
|
+
*/
|
|
722
|
+
export async function resolveContextMentions(query, options) {
|
|
723
|
+
const resolver = options?.workspaceRoot || options?.cacheTtlMs
|
|
724
|
+
? new ContextMentionResolver(options.workspaceRoot, options.cacheTtlMs)
|
|
725
|
+
: defaultMentionResolver;
|
|
726
|
+
return resolver.resolveSuggestions(query, options);
|
|
727
|
+
}
|
|
172
728
|
function symbol(state) {
|
|
173
729
|
switch (state) {
|
|
174
730
|
case 'initial':
|
|
@@ -186,7 +742,7 @@ function symbol(state) {
|
|
|
186
742
|
}
|
|
187
743
|
/**
|
|
188
744
|
* A custom text prompt that supports command history navigation using Up/Down arrow keys
|
|
189
|
-
* with memory bounds
|
|
745
|
+
* with memory bounds, dynamic input optimization, and real-time interactive @ autocomplete popups.
|
|
190
746
|
*
|
|
191
747
|
* @param opts - The configuration options for the prompt.
|
|
192
748
|
* @returns A promise that resolves to the user's input string or a symbol if cancelled.
|
|
@@ -195,7 +751,26 @@ export const historyText = (opts) => {
|
|
|
195
751
|
const maxLen = opts.maxHistoryLength ?? 100;
|
|
196
752
|
const history = optimizeHistoryForContext(opts.history || [], maxLen);
|
|
197
753
|
let historyIndex = -1;
|
|
754
|
+
const autocompleteEnabled = opts.autocomplete !== false;
|
|
755
|
+
const autocompleteOpts = typeof opts.autocomplete === 'object' ? opts.autocomplete : {};
|
|
756
|
+
const maxVisibleItems = autocompleteOpts.maxVisibleItems ?? 10;
|
|
757
|
+
const tabCompletion = autocompleteOpts.tabCompletion !== false;
|
|
758
|
+
const enterCompletion = autocompleteOpts.enterCompletion === true;
|
|
759
|
+
const resolver = autocompleteOpts.workspaceRoot || autocompleteOpts.cacheTtlMs
|
|
760
|
+
? new ContextMentionResolver(autocompleteOpts.workspaceRoot, autocompleteOpts.cacheTtlMs)
|
|
761
|
+
: defaultMentionResolver;
|
|
762
|
+
const customGetSuggestions = opts.getSuggestions || autocompleteOpts.getSuggestions;
|
|
763
|
+
let autocompleteState = {
|
|
764
|
+
isOpen: false,
|
|
765
|
+
query: '',
|
|
766
|
+
triggerIndex: -1,
|
|
767
|
+
selectedIndex: 0,
|
|
768
|
+
suggestions: [],
|
|
769
|
+
};
|
|
770
|
+
let activeQueryVersion = 0;
|
|
198
771
|
const prompt = new TextPrompt({
|
|
772
|
+
input: opts.input,
|
|
773
|
+
output: opts.output,
|
|
199
774
|
validate: opts.validate,
|
|
200
775
|
placeholder: opts.placeholder,
|
|
201
776
|
defaultValue: opts.defaultValue,
|
|
@@ -215,43 +790,213 @@ export const historyText = (opts) => {
|
|
|
215
790
|
}
|
|
216
791
|
case 'cancel':
|
|
217
792
|
return `${title}${pc.gray(S_BAR)} ${pc.strikethrough(pc.dim(this.value ?? ''))}${this.value?.trim() ? `\n${pc.gray(S_BAR)}` : ''}`;
|
|
218
|
-
default:
|
|
219
|
-
|
|
793
|
+
default: {
|
|
794
|
+
let body = `${title}${pc.cyan(S_BAR)} ${value}\n`;
|
|
795
|
+
if (autocompleteState.isOpen && autocompleteState.suggestions.length > 0) {
|
|
796
|
+
const popup = renderAutocompletePopup(autocompleteState.suggestions, autocompleteState.selectedIndex, maxVisibleItems);
|
|
797
|
+
body += `${popup}\n`;
|
|
798
|
+
}
|
|
799
|
+
body += `${pc.cyan(S_BAR_END)}\n`;
|
|
800
|
+
return body;
|
|
801
|
+
}
|
|
220
802
|
}
|
|
221
803
|
},
|
|
222
804
|
});
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
805
|
+
function updateAutocomplete(line, cursor) {
|
|
806
|
+
if (!autocompleteEnabled)
|
|
807
|
+
return;
|
|
808
|
+
const extracted = extractMentionQuery(line, cursor, autocompleteOpts.triggers || ['@']);
|
|
809
|
+
if (!extracted) {
|
|
810
|
+
if (autocompleteState.isOpen) {
|
|
811
|
+
autocompleteState.isOpen = false;
|
|
812
|
+
autocompleteState.suggestions = [];
|
|
813
|
+
prompt.render();
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const currentVersion = ++activeQueryVersion;
|
|
818
|
+
const { query, triggerIndex } = extracted;
|
|
819
|
+
if (customGetSuggestions) {
|
|
820
|
+
try {
|
|
821
|
+
const res = customGetSuggestions(query, { fullText: line, cursor });
|
|
822
|
+
if (res && typeof res.then === 'function') {
|
|
823
|
+
;
|
|
824
|
+
res.then((items) => {
|
|
825
|
+
if (currentVersion !== activeQueryVersion)
|
|
826
|
+
return;
|
|
827
|
+
const ranked = filterAndRankSuggestions(items || [], query, maxVisibleItems);
|
|
828
|
+
autocompleteState = {
|
|
829
|
+
isOpen: ranked.length > 0,
|
|
830
|
+
query,
|
|
831
|
+
triggerIndex,
|
|
832
|
+
selectedIndex: Math.min(autocompleteState.selectedIndex, Math.max(0, ranked.length - 1)),
|
|
833
|
+
suggestions: ranked,
|
|
834
|
+
};
|
|
835
|
+
prompt.render();
|
|
836
|
+
}).catch(() => { });
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
else if (Array.isArray(res)) {
|
|
840
|
+
const ranked = filterAndRankSuggestions(res, query, maxVisibleItems);
|
|
841
|
+
autocompleteState = {
|
|
842
|
+
isOpen: ranked.length > 0,
|
|
843
|
+
query,
|
|
844
|
+
triggerIndex,
|
|
845
|
+
selectedIndex: Math.min(autocompleteState.selectedIndex, Math.max(0, ranked.length - 1)),
|
|
846
|
+
suggestions: ranked,
|
|
847
|
+
};
|
|
848
|
+
prompt.render();
|
|
849
|
+
return;
|
|
232
850
|
}
|
|
233
851
|
}
|
|
852
|
+
catch {
|
|
853
|
+
// Fallback to default resolver
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const items = resolver.resolveSuggestionsSync(query, {
|
|
857
|
+
...autocompleteOpts,
|
|
858
|
+
maxVisibleItems,
|
|
859
|
+
});
|
|
860
|
+
autocompleteState = {
|
|
861
|
+
isOpen: items.length > 0,
|
|
862
|
+
query,
|
|
863
|
+
triggerIndex,
|
|
864
|
+
selectedIndex: Math.min(autocompleteState.selectedIndex, Math.max(0, items.length - 1)),
|
|
865
|
+
suggestions: items,
|
|
866
|
+
};
|
|
867
|
+
prompt.render();
|
|
868
|
+
}
|
|
869
|
+
const originalOnKeypress = prompt.onKeypress;
|
|
870
|
+
prompt.onKeypress = function (char, key) {
|
|
871
|
+
const rl = prompt.rl;
|
|
872
|
+
const currentLine = rl ? rl.line : prompt.value || '';
|
|
873
|
+
const currentCursor = rl ? rl.cursor : prompt.cursor || 0;
|
|
874
|
+
// Escape dismisses active autocomplete popup
|
|
875
|
+
if (key?.name === 'escape' && autocompleteState.isOpen) {
|
|
876
|
+
autocompleteState.isOpen = false;
|
|
877
|
+
autocompleteState.suggestions = [];
|
|
878
|
+
prompt.render();
|
|
879
|
+
return;
|
|
234
880
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
881
|
+
// Tab completion of selected suggestion
|
|
882
|
+
if (tabCompletion &&
|
|
883
|
+
(key?.name === 'tab' || char === '\t') &&
|
|
884
|
+
autocompleteState.isOpen &&
|
|
885
|
+
autocompleteState.suggestions.length > 0) {
|
|
886
|
+
const selected = autocompleteState.suggestions[autocompleteState.selectedIndex];
|
|
887
|
+
const rawValue = selected.value.startsWith('@') ? selected.value : `@${selected.value}`;
|
|
888
|
+
const isDir = rawValue.endsWith('/');
|
|
889
|
+
const replacement = isDir ? rawValue : `${rawValue} `;
|
|
890
|
+
const before = currentLine.slice(0, autocompleteState.triggerIndex);
|
|
891
|
+
const after = currentLine.slice(currentCursor);
|
|
892
|
+
const newLine = before + replacement + after;
|
|
893
|
+
const newCursor = before.length + replacement.length;
|
|
894
|
+
if (rl) {
|
|
895
|
+
rl.line = newLine;
|
|
896
|
+
rl.cursor = newCursor;
|
|
897
|
+
}
|
|
898
|
+
prompt.value = newLine;
|
|
899
|
+
prompt._cursor = newCursor;
|
|
900
|
+
if (isDir) {
|
|
901
|
+
updateAutocomplete(newLine, newCursor);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
autocompleteState.isOpen = false;
|
|
905
|
+
autocompleteState.suggestions = [];
|
|
906
|
+
prompt.render();
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
// Enter completion (if explicitly configured)
|
|
911
|
+
if (enterCompletion &&
|
|
912
|
+
key?.name === 'return' &&
|
|
913
|
+
autocompleteState.isOpen &&
|
|
914
|
+
autocompleteState.suggestions.length > 0) {
|
|
915
|
+
const selected = autocompleteState.suggestions[autocompleteState.selectedIndex];
|
|
916
|
+
const rawValue = selected.value.startsWith('@') ? selected.value : `@${selected.value}`;
|
|
917
|
+
const isDir = rawValue.endsWith('/');
|
|
918
|
+
const replacement = isDir ? rawValue : `${rawValue} `;
|
|
919
|
+
const before = currentLine.slice(0, autocompleteState.triggerIndex);
|
|
920
|
+
const after = currentLine.slice(currentCursor);
|
|
921
|
+
const newLine = before + replacement + after;
|
|
922
|
+
const newCursor = before.length + replacement.length;
|
|
923
|
+
if (rl) {
|
|
924
|
+
rl.line = newLine;
|
|
925
|
+
rl.cursor = newCursor;
|
|
926
|
+
}
|
|
927
|
+
prompt.value = newLine;
|
|
928
|
+
prompt._cursor = newCursor;
|
|
929
|
+
if (isDir) {
|
|
930
|
+
updateAutocomplete(newLine, newCursor);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
autocompleteState.isOpen = false;
|
|
934
|
+
autocompleteState.suggestions = [];
|
|
935
|
+
prompt.render();
|
|
936
|
+
}
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
// Arrow navigation when autocomplete is open
|
|
940
|
+
if (autocompleteState.isOpen && autocompleteState.suggestions.length > 0) {
|
|
941
|
+
if (key?.name === 'up') {
|
|
942
|
+
autocompleteState.selectedIndex =
|
|
943
|
+
(autocompleteState.selectedIndex - 1 + autocompleteState.suggestions.length) %
|
|
944
|
+
autocompleteState.suggestions.length;
|
|
945
|
+
prompt.render();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (key?.name === 'down') {
|
|
949
|
+
autocompleteState.selectedIndex =
|
|
950
|
+
(autocompleteState.selectedIndex + 1) % autocompleteState.suggestions.length;
|
|
951
|
+
prompt.render();
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Normal History Navigation (when autocomplete popup is not active)
|
|
956
|
+
if (!autocompleteState.isOpen) {
|
|
957
|
+
if (key?.name === 'up') {
|
|
958
|
+
if (history.length > 0 && historyIndex < history.length - 1) {
|
|
959
|
+
historyIndex++;
|
|
960
|
+
prompt.value = history[history.length - 1 - historyIndex];
|
|
961
|
+
if (rl) {
|
|
962
|
+
rl.line = prompt.value;
|
|
963
|
+
rl.cursor = prompt.value.length;
|
|
964
|
+
}
|
|
965
|
+
;
|
|
966
|
+
prompt.render();
|
|
243
967
|
}
|
|
968
|
+
return;
|
|
244
969
|
}
|
|
245
|
-
|
|
246
|
-
historyIndex
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
970
|
+
if (key?.name === 'down') {
|
|
971
|
+
if (historyIndex > 0) {
|
|
972
|
+
historyIndex--;
|
|
973
|
+
prompt.value = history[history.length - 1 - historyIndex];
|
|
974
|
+
if (rl) {
|
|
975
|
+
rl.line = prompt.value;
|
|
976
|
+
rl.cursor = prompt.value.length;
|
|
977
|
+
}
|
|
978
|
+
;
|
|
979
|
+
prompt.render();
|
|
980
|
+
}
|
|
981
|
+
else if (historyIndex === 0) {
|
|
982
|
+
historyIndex = -1;
|
|
983
|
+
prompt.value = '';
|
|
984
|
+
if (rl) {
|
|
985
|
+
rl.line = '';
|
|
986
|
+
rl.cursor = 0;
|
|
987
|
+
}
|
|
988
|
+
;
|
|
989
|
+
prompt.render();
|
|
252
990
|
}
|
|
991
|
+
return;
|
|
253
992
|
}
|
|
254
993
|
}
|
|
255
|
-
|
|
994
|
+
// Fall through to Clack's original keypress handler
|
|
995
|
+
originalOnKeypress?.call(prompt, char, key);
|
|
996
|
+
// Evaluate autocomplete trigger after keypress
|
|
997
|
+
const updatedLine = rl ? rl.line : prompt.value || '';
|
|
998
|
+
const updatedCursor = rl ? rl.cursor : prompt.cursor || 0;
|
|
999
|
+
updateAutocomplete(updatedLine, updatedCursor);
|
|
1000
|
+
};
|
|
256
1001
|
return prompt.prompt();
|
|
257
1002
|
};
|