@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 MAX_REPOSITORY_FILES = 6000;
15
- const MAX_SUGGESTIONS = 6;
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, MAX_SUGGESTIONS);
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 >= MAX_REPOSITORY_FILES) {
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 >= MAX_REPOSITORY_FILES) {
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
- ? 'File suggestions • ↑/↓ or Ctrl+N/P navigate • Enter apply'
16
- : 'File suggestions • ↑/↓ 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: () => {
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} files indexed` })] }));
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' }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawwee/interactive-mcp",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {