@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 MAX_SUGGESTIONS_WITH_EMPTY_QUERY = 24;
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, MAX_SUGGESTIONS_WITH_EMPTY_QUERY);
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: [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: () => {
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawwee/interactive-mcp",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {