@rawwee/interactive-mcp 1.5.2 → 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
|
-
const
|
|
15
|
-
const
|
|
32
|
+
const MAX_REPOSITORY_ENTRIES = 50000;
|
|
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,25 +119,17 @@ 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
|
-
.slice(0, MAX_SUGGESTIONS)
|
|
89
|
-
.map((entry) => entry.filePath);
|
|
124
|
+
return collectTopRankedSuggestions(files, query, MAX_VISIBLE_SUGGESTIONS).map((entry) => entry.filePath);
|
|
90
125
|
};
|
|
91
126
|
export const readRepositoryFiles = async (repoRoot) => {
|
|
92
127
|
const discoveredFiles = [];
|
|
93
128
|
const visitDirectory = async (directoryPath) => {
|
|
94
|
-
if (discoveredFiles.length >=
|
|
129
|
+
if (discoveredFiles.length >= MAX_REPOSITORY_ENTRIES) {
|
|
95
130
|
return;
|
|
96
131
|
}
|
|
97
|
-
let entries
|
|
132
|
+
let entries;
|
|
98
133
|
try {
|
|
99
134
|
entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
100
135
|
}
|
|
@@ -102,22 +137,26 @@ export const readRepositoryFiles = async (repoRoot) => {
|
|
|
102
137
|
return;
|
|
103
138
|
}
|
|
104
139
|
for (const entry of entries) {
|
|
105
|
-
if (discoveredFiles.length >=
|
|
140
|
+
if (discoveredFiles.length >= MAX_REPOSITORY_ENTRIES) {
|
|
106
141
|
return;
|
|
107
142
|
}
|
|
108
143
|
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
109
144
|
continue;
|
|
110
145
|
}
|
|
146
|
+
if (entry.isSymbolicLink()) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
111
149
|
const entryAbsolutePath = path.join(directoryPath, entry.name);
|
|
150
|
+
const relativePath = path.relative(repoRoot, entryAbsolutePath);
|
|
151
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
112
154
|
if (entry.isDirectory()) {
|
|
155
|
+
discoveredFiles.push(`${toPosixPath(relativePath)}/`);
|
|
113
156
|
await visitDirectory(entryAbsolutePath);
|
|
114
157
|
continue;
|
|
115
158
|
}
|
|
116
159
|
if (entry.isFile()) {
|
|
117
|
-
const relativePath = path.relative(repoRoot, entryAbsolutePath);
|
|
118
|
-
if (!relativePath || relativePath.startsWith('..')) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
160
|
discoveredFiles.push(toPosixPath(relativePath));
|
|
122
161
|
}
|
|
123
162
|
}
|
|
@@ -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
|
|
15
|
-
? '
|
|
16
|
-
: '
|
|
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
|
+
? '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: [_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');
|
|
@@ -25,7 +27,7 @@ export const SearchStatus = ({ isIndexingFiles, repositoryFiles, searchRoot, has
|
|
|
25
27
|
? `#search root: ${searchRoot}`
|
|
26
28
|
: '#search root: no search root' }), _jsx("text", { fg: "gray", children: isIndexingFiles
|
|
27
29
|
? '#search index: indexing...'
|
|
28
|
-
: `#search index: ${repositoryFiles.length}
|
|
30
|
+
: `#search index: ${repositoryFiles.length} paths indexed` })] }));
|
|
29
31
|
export const InputStatus = ({ mode, isNarrow, inputValue, queuedAttachments, }) => (_jsxs("box", { flexDirection: isNarrow ? 'column' : 'row', justifyContent: "space-between", marginBottom: 0, gap: isNarrow ? 0 : undefined, children: [_jsx("text", { fg: "gray", children: mode === 'input' ? 'Custom input' : 'Option selection' }), _jsx("text", { fg: "gray", children: mode === 'input' && queuedAttachments.length > 0
|
|
30
32
|
? `${inputValue.length} chars + ${queuedAttachments.length} queued`
|
|
31
33
|
: `${inputValue.length} chars` })] }));
|
|
@@ -33,5 +35,5 @@ export const ClipboardStatus = ({ status }) => (_jsx("text", { fg: status.starts
|
|
|
33
35
|
export const AttachmentsDisplay = ({ queuedAttachments, }) => (_jsxs("box", { flexDirection: "column", width: "100%", gap: 0, children: [_jsxs("text", { fg: "yellow", children: [_jsx("strong", { children: "QUEUED ATTACHMENTS" }), " (Delete placeholder text to remove)"] }), queuedAttachments.map((attachment, index) => (_jsxs("text", { fg: "gray", wrapMode: "word", children: ["[File ", index + 1, "] ", attachment.label] }, attachment.id)))] }));
|
|
34
36
|
export const SendButton = () => (_jsx("box", { backgroundColor: "cyan", paddingLeft: 1, paddingRight: 1, alignSelf: "flex-start", marginBottom: 0, children: _jsxs("text", { fg: "black", children: [_jsx("strong", { children: "Send" }), " \u2303S"] }) }));
|
|
35
37
|
export const HelpText = ({ hasOptions }) => (_jsx("text", { fg: "gray", wrapMode: "word", children: hasOptions
|
|
36
|
-
? 'Enter/Ctrl+J newline (or #search apply) • #search nav: ↑/↓ or Ctrl+N/P • Tab mode switch • #path for repo file autocomplete • Cmd/Ctrl+C copy input • Cmd/Ctrl+V paste/attach • Cmd/Ctrl+Z undo • Cmd/Ctrl+Shift+Z redo'
|
|
37
|
-
: 'Enter/Ctrl+J newline • #search nav: ↑/↓ or Ctrl+N/P • Enter/Tab #search apply • #path for repo file autocomplete • Cmd/Ctrl+C copy input • Cmd/Ctrl+V paste/attach • Cmd/Ctrl+Z undo • Cmd/Ctrl+Shift+Z redo' }));
|
|
38
|
+
? 'Enter/Ctrl+J newline (or #search apply) • #search nav: ↑/↓ or Ctrl+N/P • Tab mode switch • #path for repo file/folder autocomplete • Cmd/Ctrl+C copy input • Cmd/Ctrl+V paste/attach • Cmd/Ctrl+Z undo • Cmd/Ctrl+Shift+Z redo'
|
|
39
|
+
: 'Enter/Ctrl+J newline • #search nav: ↑/↓ or Ctrl+N/P • Enter/Tab #search apply • #path for repo file/folder autocomplete • Cmd/Ctrl+C copy input • Cmd/Ctrl+V paste/attach • Cmd/Ctrl+Z undo • Cmd/Ctrl+Shift+Z redo' }));
|