@rawwee/interactive-mcp 1.5.3 → 1.5.4
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.
|
@@ -28,6 +28,7 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
28
28
|
const [clipboardStatus, setClipboardStatus] = useState(null);
|
|
29
29
|
const [queuedAttachments, setQueuedAttachments] = useState([]);
|
|
30
30
|
const textareaRef = useRef(null);
|
|
31
|
+
const suggestionsScrollRef = useRef(null);
|
|
31
32
|
const latestInputValueRef = useRef(inputValue);
|
|
32
33
|
const latestCaretPositionRef = useRef(caretPosition);
|
|
33
34
|
const autocompleteTargetRef = useRef(null);
|
|
@@ -186,6 +187,16 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
186
187
|
textareaRenderVersion,
|
|
187
188
|
width,
|
|
188
189
|
]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (fileSuggestions.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
suggestionsScrollRef.current?.scrollTo?.({
|
|
195
|
+
x: 0,
|
|
196
|
+
y: selectedSuggestionIndex,
|
|
197
|
+
});
|
|
198
|
+
focusTextarea(textareaRef);
|
|
199
|
+
}, [fileSuggestions.length, selectedSuggestionIndex]);
|
|
189
200
|
useEffect(() => {
|
|
190
201
|
if (mode !== 'input' || repositoryFiles.length === 0) {
|
|
191
202
|
autocompleteTargetRef.current = null;
|
|
@@ -407,5 +418,5 @@ export function InteractiveInput({ question, questionId, predefinedOptions = [],
|
|
|
407
418
|
onInputActivity,
|
|
408
419
|
]);
|
|
409
420
|
useKeyboard(keyboardHandler);
|
|
410
|
-
return (_jsxs(_Fragment, { children: [_jsx(QuestionBox, { question: question, MarkdownTextComponent: MarkdownText }), _jsx(ModeTabs, { mode: mode, hasOptions: hasOptions, onSelectOptionMode: setModeToOption, onSelectInputMode: recoverInputFocusFromClick }), _jsx(OptionList, { mode: mode, options: predefinedOptions, selectedIndex: selectedIndex, onSelectOption: setSelectedIndex, onActivateOptionMode: setModeToOption }), mode === 'input' && (_jsx(InputEditor, { questionId: questionId, textareaRenderVersion: textareaRenderVersion, textareaRef: textareaRef, textareaContainerHeight: textareaContainerHeight, textareaRows: textareaRows, hasSuggestions: fileSuggestions.length > 0, keyBindings: textareaBindings, onFocusRequest: recoverInputFocusFromClick, onContentSync: syncInputStateFromTextarea, onSubmitFromTextarea: handleTextareaSubmit })), mode === 'input' && activeAutocompleteTarget !== null && (_jsx(SuggestionsPanel, { hasOptions: hasOptions, isIndexingFiles: isIndexingFiles, fileSuggestions: fileSuggestions, selectedSuggestionIndex: selectedSuggestionIndex, selectedSuggestionVscodeLink: selectedSuggestionVscodeLink, hasSearchRoot: hasSearchRoot })), mode === 'input' && (_jsx(SearchStatus, { isIndexingFiles: isIndexingFiles, repositoryFiles: repositoryFiles, searchRoot: searchRoot, hasSearchRoot: hasSearchRoot })), _jsx(InputStatus, { mode: mode, isNarrow: isNarrow, inputValue: inputValue, queuedAttachments: queuedAttachments }), mode === 'input' && clipboardStatus && (_jsx(ClipboardStatus, { status: clipboardStatus })), mode === 'input' && queuedAttachments.length > 0 && (_jsx(AttachmentsDisplay, { queuedAttachments: queuedAttachments })), mode === 'input' && _jsx(SendButton, {}), mode === 'input' && _jsx(HelpText, { hasOptions: hasOptions })] }));
|
|
421
|
+
return (_jsxs(_Fragment, { children: [_jsx(QuestionBox, { question: question, MarkdownTextComponent: MarkdownText }), _jsx(ModeTabs, { mode: mode, hasOptions: hasOptions, onSelectOptionMode: setModeToOption, onSelectInputMode: recoverInputFocusFromClick }), _jsx(OptionList, { mode: mode, options: predefinedOptions, selectedIndex: selectedIndex, onSelectOption: setSelectedIndex, onActivateOptionMode: setModeToOption }), mode === 'input' && (_jsx(InputEditor, { questionId: questionId, textareaRenderVersion: textareaRenderVersion, textareaRef: textareaRef, textareaContainerHeight: textareaContainerHeight, textareaRows: textareaRows, hasSuggestions: fileSuggestions.length > 0, keyBindings: textareaBindings, onFocusRequest: recoverInputFocusFromClick, onContentSync: syncInputStateFromTextarea, onSubmitFromTextarea: handleTextareaSubmit })), mode === 'input' && activeAutocompleteTarget !== null && (_jsx(SuggestionsPanel, { hasOptions: hasOptions, isIndexingFiles: isIndexingFiles, fileSuggestions: fileSuggestions, selectedSuggestionIndex: selectedSuggestionIndex, selectedSuggestionVscodeLink: selectedSuggestionVscodeLink, hasSearchRoot: hasSearchRoot, scrollRef: suggestionsScrollRef })), mode === 'input' && (_jsx(SearchStatus, { isIndexingFiles: isIndexingFiles, repositoryFiles: repositoryFiles, searchRoot: searchRoot, hasSearchRoot: hasSearchRoot })), _jsx(InputStatus, { mode: mode, isNarrow: isNarrow, inputValue: inputValue, queuedAttachments: queuedAttachments }), mode === 'input' && clipboardStatus && (_jsx(ClipboardStatus, { status: clipboardStatus })), mode === 'input' && queuedAttachments.length > 0 && (_jsx(AttachmentsDisplay, { queuedAttachments: queuedAttachments })), mode === 'input' && _jsx(SendButton, {}), mode === 'input' && _jsx(HelpText, { hasOptions: hasOptions })] }));
|
|
411
422
|
}
|
|
@@ -5,14 +5,32 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
5
5
|
'node_modules',
|
|
6
6
|
'dist',
|
|
7
7
|
'build',
|
|
8
|
+
'coverage',
|
|
9
|
+
'.cache',
|
|
10
|
+
'.bun',
|
|
11
|
+
'.yarn',
|
|
12
|
+
'.pnpm',
|
|
13
|
+
'.pnpm-store',
|
|
8
14
|
'.next',
|
|
15
|
+
'.nuxt',
|
|
16
|
+
'.svelte-kit',
|
|
17
|
+
'.vercel',
|
|
9
18
|
'.turbo',
|
|
19
|
+
'.output',
|
|
20
|
+
'out',
|
|
10
21
|
'.idea',
|
|
11
22
|
'.vscode',
|
|
23
|
+
'.history',
|
|
24
|
+
'.tmp',
|
|
25
|
+
'tmp',
|
|
26
|
+
'temp',
|
|
27
|
+
'.venv',
|
|
28
|
+
'venv',
|
|
29
|
+
'.pytest_cache',
|
|
12
30
|
'.DS_Store',
|
|
13
31
|
]);
|
|
14
32
|
const MAX_REPOSITORY_ENTRIES = 50000;
|
|
15
|
-
const
|
|
33
|
+
const MAX_VISIBLE_SUGGESTIONS = 50;
|
|
16
34
|
const toPosixPath = (value) => value.replaceAll(path.sep, '/');
|
|
17
35
|
const getFuzzyScore = (candidate, query) => {
|
|
18
36
|
const candidateLower = candidate.toLowerCase();
|
|
@@ -41,6 +59,31 @@ const getFuzzyScore = (candidate, query) => {
|
|
|
41
59
|
}
|
|
42
60
|
return score - candidate.length * 0.1;
|
|
43
61
|
};
|
|
62
|
+
const isHigherRanked = (left, right) => left.score > right.score ||
|
|
63
|
+
(left.score === right.score &&
|
|
64
|
+
left.filePath.localeCompare(right.filePath) < 0);
|
|
65
|
+
const collectTopRankedSuggestions = (files, query, limit) => {
|
|
66
|
+
const topRanked = [];
|
|
67
|
+
for (const filePath of files) {
|
|
68
|
+
const score = getFuzzyScore(filePath, query);
|
|
69
|
+
if (score === null) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const scoredSuggestion = { filePath, score };
|
|
73
|
+
const insertionIndex = topRanked.findIndex((candidate) => isHigherRanked(scoredSuggestion, candidate));
|
|
74
|
+
if (insertionIndex === -1) {
|
|
75
|
+
if (topRanked.length < limit) {
|
|
76
|
+
topRanked.push(scoredSuggestion);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
topRanked.splice(insertionIndex, 0, scoredSuggestion);
|
|
81
|
+
if (topRanked.length > limit) {
|
|
82
|
+
topRanked.pop();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return topRanked;
|
|
86
|
+
};
|
|
44
87
|
export const getAutocompleteTarget = (value, caret) => {
|
|
45
88
|
const clampedCaret = Math.max(0, Math.min(caret, value.length));
|
|
46
89
|
let start = clampedCaret;
|
|
@@ -76,16 +119,9 @@ export const getAutocompleteTarget = (value, caret) => {
|
|
|
76
119
|
};
|
|
77
120
|
export const rankFileSuggestions = (files, query) => {
|
|
78
121
|
if (query.length === 0) {
|
|
79
|
-
return files.slice(0,
|
|
122
|
+
return files.slice(0, MAX_VISIBLE_SUGGESTIONS);
|
|
80
123
|
}
|
|
81
|
-
return files
|
|
82
|
-
.map((filePath) => ({
|
|
83
|
-
filePath,
|
|
84
|
-
score: getFuzzyScore(filePath, query),
|
|
85
|
-
}))
|
|
86
|
-
.filter((entry) => typeof entry.score === 'number')
|
|
87
|
-
.sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
|
|
88
|
-
.map((entry) => entry.filePath);
|
|
124
|
+
return collectTopRankedSuggestions(files, query, MAX_VISIBLE_SUGGESTIONS).map((entry) => entry.filePath);
|
|
89
125
|
};
|
|
90
126
|
export const readRepositoryFiles = async (repoRoot) => {
|
|
91
127
|
const discoveredFiles = [];
|
|
@@ -107,6 +143,9 @@ export const readRepositoryFiles = async (repoRoot) => {
|
|
|
107
143
|
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
108
144
|
continue;
|
|
109
145
|
}
|
|
146
|
+
if (entry.isSymbolicLink()) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
110
149
|
const entryAbsolutePath = path.join(directoryPath, entry.name);
|
|
111
150
|
const relativePath = path.relative(repoRoot, entryAbsolutePath);
|
|
112
151
|
if (!relativePath || relativePath.startsWith('..')) {
|
|
@@ -11,9 +11,11 @@ export const OptionList = ({ mode, options, selectedIndex, onSelectOption, onAct
|
|
|
11
11
|
}, children: _jsxs("text", { wrapMode: "char", fg: index === selectedIndex && mode === 'option' ? 'cyan' : 'gray', children: [index === selectedIndex && mode === 'option' ? '› ' : ' ', opt] }) }, `${opt}-${index}`))) })] }));
|
|
12
12
|
};
|
|
13
13
|
export const InputEditor = ({ questionId, textareaRenderVersion, textareaRef, textareaContainerHeight, textareaRows, hasSuggestions, keyBindings, onFocusRequest, onContentSync, onSubmitFromTextarea, }) => (_jsxs("box", { flexDirection: "column", marginBottom: 0, width: "100%", children: [_jsx("text", { fg: "gray", children: "Input" }), _jsx("box", { border: true, borderStyle: "single", borderColor: hasSuggestions ? 'cyan' : 'gray', backgroundColor: "#1f1f1f", height: textareaContainerHeight, paddingLeft: 1, paddingRight: 1, onClick: onFocusRequest, children: _jsx("textarea", { ref: textareaRef, focused: true, height: textareaRows, wrapMode: "word", backgroundColor: "#1f1f1f", focusedBackgroundColor: "#1f1f1f", textColor: "white", focusedTextColor: "white", placeholderColor: "gray", placeholder: "Type your answer...", keyBindings: keyBindings, onContentChange: onContentSync, onCursorChange: onContentSync, onSubmit: onSubmitFromTextarea }, `textarea-${questionId}-${textareaRenderVersion}`) })] }));
|
|
14
|
-
export const SuggestionsPanel = ({ hasOptions, isIndexingFiles, fileSuggestions, selectedSuggestionIndex, selectedSuggestionVscodeLink, hasSearchRoot, }) => (_jsxs("box", { flexDirection: "column", marginBottom: 0, width: "100%", gap: 0, children: [_jsx("text", { fg: "gray", children: hasOptions
|
|
14
|
+
export const SuggestionsPanel = ({ hasOptions, isIndexingFiles, fileSuggestions, selectedSuggestionIndex, selectedSuggestionVscodeLink, hasSearchRoot, scrollRef, }) => (_jsxs("box", { flexDirection: "column", marginBottom: 0, width: "100%", gap: 0, children: [_jsx("text", { fg: "gray", children: hasOptions
|
|
15
15
|
? 'Path suggestions (files + folders) • ↑/↓ or Ctrl+N/P navigate • Enter apply'
|
|
16
|
-
: 'Path suggestions (files + folders) • ↑/↓ or Ctrl+N/P navigate • Enter/Tab apply' }), isIndexingFiles ? (_jsx("text", { fg: "gray", children: "Indexing files..." })) : fileSuggestions.length > 0 ? (_jsxs("box", { flexDirection: "column", width: "100%", children: [
|
|
16
|
+
: 'Path suggestions (files + folders) • ↑/↓ or Ctrl+N/P navigate • Enter/Tab apply' }), isIndexingFiles ? (_jsx("text", { fg: "gray", children: "Indexing files..." })) : fileSuggestions.length > 0 ? (_jsxs("box", { flexDirection: "column", width: "100%", children: [_jsx("text", { fg: "gray", children: "Showing up to 50 results" }), _jsx("scrollbox", { ref: scrollRef, width: "100%", height: 6, scrollY: true, viewportCulling: true, scrollbarOptions: {
|
|
17
|
+
showArrows: false,
|
|
18
|
+
}, children: _jsx("box", { flexDirection: "column", width: "100%", children: fileSuggestions.map((suggestion, index) => (_jsx("box", { paddingLeft: 0, paddingRight: 1, gap: 0, children: _jsxs("text", { fg: index === selectedSuggestionIndex ? 'cyan' : 'gray', wrapMode: "char", children: [index === selectedSuggestionIndex ? '› ' : ' ', suggestion] }) }, suggestion))) }) }), selectedSuggestionVscodeLink && (_jsxs("box", { flexDirection: "column", width: "100%", children: [_jsx("text", { fg: "gray", wrapMode: "word", children: "open file with:" }), _jsx("text", { fg: "cyan", wrapMode: "word", onMouseUp: () => {
|
|
17
19
|
void openExternalLink(selectedSuggestionVscodeLink, 'vscode');
|
|
18
20
|
}, children: "\u2022 VS Code" }), _jsx("text", { fg: "cyan", wrapMode: "word", onMouseUp: () => {
|
|
19
21
|
void openExternalLink(selectedSuggestionVscodeLink, 'vscode-insiders');
|