@rawwee/interactive-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/commands/input/index.js +216 -0
- package/dist/commands/input/ui.js +231 -0
- package/dist/commands/intensive-chat/index.js +289 -0
- package/dist/commands/intensive-chat/ui.js +301 -0
- package/dist/components/InteractiveInput.js +420 -0
- package/dist/components/MarkdownText.js +285 -0
- package/dist/components/PromptStatus.js +46 -0
- package/dist/components/TextProgressBar.js +10 -0
- package/dist/components/interactive-input/autocomplete.js +127 -0
- package/dist/components/interactive-input/keyboard.js +51 -0
- package/dist/components/interactive-input/types.js +1 -0
- package/dist/constants.js +13 -0
- package/dist/index.js +318 -0
- package/dist/tool-definitions/intensive-chat.js +236 -0
- package/dist/tool-definitions/message-complete-notification.js +66 -0
- package/dist/tool-definitions/request-user-input.js +117 -0
- package/dist/tool-definitions/types.js +1 -0
- package/dist/utils/base-directory.js +44 -0
- package/dist/utils/clipboard.js +67 -0
- package/dist/utils/logger.js +65 -0
- package/dist/utils/search-root.js +85 -0
- package/dist/utils/spawn-detached-terminal.js +101 -0
- package/package.json +74 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import * as OpenTuiCore from '@opentui/core';
|
|
3
|
+
import { createElement, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { copyTextToClipboard } from '../utils/clipboard.js';
|
|
5
|
+
const { SyntaxStyle, RGBA } = OpenTuiCore;
|
|
6
|
+
const CODE_BLOCK_REGEX = /```([^\n`]*)\n?([\s\S]*?)```/g;
|
|
7
|
+
const DIFF_LANGUAGES = new Set(['diff', 'patch']);
|
|
8
|
+
const LANGUAGE_TO_FILETYPE = {
|
|
9
|
+
js: 'javascript',
|
|
10
|
+
jsx: 'javascript',
|
|
11
|
+
ts: 'typescript',
|
|
12
|
+
tsx: 'tsx',
|
|
13
|
+
sh: 'bash',
|
|
14
|
+
shell: 'bash',
|
|
15
|
+
zsh: 'bash',
|
|
16
|
+
yml: 'yaml',
|
|
17
|
+
md: 'markdown',
|
|
18
|
+
};
|
|
19
|
+
function normalizeFiletype(language) {
|
|
20
|
+
if (!language) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const normalized = language.trim().toLowerCase();
|
|
24
|
+
return LANGUAGE_TO_FILETYPE[normalized] ?? normalized;
|
|
25
|
+
}
|
|
26
|
+
function extractDiffFiletype(diffContent) {
|
|
27
|
+
const fileLineMatch = diffContent.match(/^(?:\+\+\+|---)\s+(?:[ab]\/)?(.+)$/m);
|
|
28
|
+
if (!fileLineMatch || !fileLineMatch[1]) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const extensionMatch = fileLineMatch[1].match(/\.([a-zA-Z0-9]+)$/);
|
|
32
|
+
if (!extensionMatch || !extensionMatch[1]) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return normalizeFiletype(extensionMatch[1]);
|
|
36
|
+
}
|
|
37
|
+
function isLikelyUnifiedDiff(content) {
|
|
38
|
+
const hasFileHeaders = /^---\s+/m.test(content) && /^\+\+\+\s+/m.test(content);
|
|
39
|
+
const hasHunkHeader = /^@@\s+/m.test(content);
|
|
40
|
+
const hasAddedOrRemovedLines = /^[+-](?![+-])\s?.+/m.test(content);
|
|
41
|
+
const hasGitHeader = /^diff --git\s+/m.test(content);
|
|
42
|
+
return ((hasFileHeaders || hasGitHeader || hasHunkHeader) && hasAddedOrRemovedLines);
|
|
43
|
+
}
|
|
44
|
+
function parseUnifiedDiff(content) {
|
|
45
|
+
const normalizedContent = content.replace(/\r\n/g, '\n');
|
|
46
|
+
const lines = normalizedContent.split('\n');
|
|
47
|
+
const oldLines = [];
|
|
48
|
+
const newLines = [];
|
|
49
|
+
const hunkLines = [];
|
|
50
|
+
let oldPath = 'a/file';
|
|
51
|
+
let newPath = 'b/file';
|
|
52
|
+
let inHunk = false;
|
|
53
|
+
let hasHunkContent = false;
|
|
54
|
+
for (const rawLine of lines) {
|
|
55
|
+
if (!inHunk) {
|
|
56
|
+
const oldPathMatch = rawLine.match(/^---\s+(.+)$/);
|
|
57
|
+
if (oldPathMatch) {
|
|
58
|
+
oldPath = oldPathMatch[1].split('\t')[0].trim() || oldPath;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const newPathMatch = rawLine.match(/^\+\+\+\s+(.+)$/);
|
|
62
|
+
if (newPathMatch) {
|
|
63
|
+
newPath = newPathMatch[1].split('\t')[0].trim() || newPath;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (rawLine.startsWith('@@')) {
|
|
68
|
+
inHunk = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!inHunk) {
|
|
72
|
+
const startsLikeDiffLine = rawLine.startsWith('+') ||
|
|
73
|
+
rawLine.startsWith('-') ||
|
|
74
|
+
rawLine.startsWith(' ');
|
|
75
|
+
if (startsLikeDiffLine &&
|
|
76
|
+
!rawLine.startsWith('+++') &&
|
|
77
|
+
!rawLine.startsWith('---')) {
|
|
78
|
+
inHunk = true;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (rawLine === '\') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (rawLine.startsWith('+') && !rawLine.startsWith('+++')) {
|
|
88
|
+
const line = rawLine.slice(1);
|
|
89
|
+
newLines.push(line);
|
|
90
|
+
hunkLines.push(`+${line}`);
|
|
91
|
+
hasHunkContent = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (rawLine.startsWith('-') && !rawLine.startsWith('---')) {
|
|
95
|
+
const line = rawLine.slice(1);
|
|
96
|
+
oldLines.push(line);
|
|
97
|
+
hunkLines.push(`-${line}`);
|
|
98
|
+
hasHunkContent = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (rawLine.startsWith(' ')) {
|
|
102
|
+
const line = rawLine.slice(1);
|
|
103
|
+
oldLines.push(line);
|
|
104
|
+
newLines.push(line);
|
|
105
|
+
hunkLines.push(` ${line}`);
|
|
106
|
+
hasHunkContent = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (rawLine.length === 0) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (!hasHunkContent) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const oldCount = oldLines.length;
|
|
118
|
+
const newCount = newLines.length;
|
|
119
|
+
const unifiedDiff = [
|
|
120
|
+
`--- ${oldPath}`,
|
|
121
|
+
`+++ ${newPath}`,
|
|
122
|
+
`@@ -1,${oldCount} +1,${newCount} @@`,
|
|
123
|
+
...hunkLines,
|
|
124
|
+
].join('\n');
|
|
125
|
+
return {
|
|
126
|
+
oldCode: oldLines.join('\n'),
|
|
127
|
+
newCode: newLines.join('\n'),
|
|
128
|
+
unifiedDiff,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function splitMarkdownSegments(content) {
|
|
132
|
+
const segments = [];
|
|
133
|
+
let lastIndex = 0;
|
|
134
|
+
for (const match of content.matchAll(CODE_BLOCK_REGEX)) {
|
|
135
|
+
const [fullMatch, rawLanguage = '', rawCode = ''] = match;
|
|
136
|
+
if (typeof fullMatch !== 'string' || typeof match.index !== 'number') {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (match.index > lastIndex) {
|
|
140
|
+
segments.push({
|
|
141
|
+
type: 'markdown',
|
|
142
|
+
value: content.slice(lastIndex, match.index),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
segments.push({
|
|
146
|
+
type: 'code',
|
|
147
|
+
language: rawLanguage.trim() || undefined,
|
|
148
|
+
value: rawCode,
|
|
149
|
+
});
|
|
150
|
+
lastIndex = match.index + fullMatch.length;
|
|
151
|
+
}
|
|
152
|
+
if (lastIndex < content.length) {
|
|
153
|
+
segments.push({
|
|
154
|
+
type: 'markdown',
|
|
155
|
+
value: content.slice(lastIndex),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (segments.length === 0) {
|
|
159
|
+
return [{ type: 'markdown', value: content }];
|
|
160
|
+
}
|
|
161
|
+
return segments;
|
|
162
|
+
}
|
|
163
|
+
export function MarkdownText({ content, streaming = false, showContentCopyControl = false, contentCopyLabel = 'Copy text', showCodeCopyControls = false, codeBlockMaxVisibleLines, }) {
|
|
164
|
+
const syntaxStyle = useMemo(() => {
|
|
165
|
+
const toColor = (hex) => typeof RGBA?.fromHex === 'function' ? RGBA.fromHex(hex) : hex;
|
|
166
|
+
if (typeof SyntaxStyle.fromStyles === 'function') {
|
|
167
|
+
return SyntaxStyle.fromStyles({
|
|
168
|
+
keyword: { fg: toColor('#FF7B72'), bold: true },
|
|
169
|
+
string: { fg: toColor('#A5D6FF') },
|
|
170
|
+
comment: { fg: toColor('#8B949E'), italic: true },
|
|
171
|
+
number: { fg: toColor('#79C0FF') },
|
|
172
|
+
function: { fg: toColor('#D2A8FF') },
|
|
173
|
+
type: { fg: toColor('#FFA657') },
|
|
174
|
+
operator: { fg: toColor('#FF7B72') },
|
|
175
|
+
property: { fg: toColor('#79C0FF') },
|
|
176
|
+
default: { fg: toColor('#E6EDF3') },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (typeof SyntaxStyle.create === 'function') {
|
|
180
|
+
return SyntaxStyle.create();
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}, []);
|
|
184
|
+
const segments = useMemo(() => splitMarkdownSegments(content), [content]);
|
|
185
|
+
const [clipboardHint, setClipboardHint] = useState(null);
|
|
186
|
+
const [copiedSnippetIndex, setCopiedSnippetIndex] = useState(null);
|
|
187
|
+
const copiedSnippetTimeoutRef = useRef(null);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
return () => {
|
|
190
|
+
if (copiedSnippetTimeoutRef.current) {
|
|
191
|
+
clearTimeout(copiedSnippetTimeoutRef.current);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}, []);
|
|
195
|
+
if (!content) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const copyWithHint = async (value, successMessage) => {
|
|
199
|
+
if (!value) {
|
|
200
|
+
setClipboardHint('Nothing to copy.');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
await copyTextToClipboard(value);
|
|
205
|
+
setClipboardHint(successMessage);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
setClipboardHint(`Copy failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
return (_jsxs("box", { flexDirection: "column", width: "100%", gap: 1, children: [showContentCopyControl && (_jsx("box", { width: "100%", justifyContent: "flex-end", children: _jsxs("text", { fg: "cyan", onMouseUp: () => {
|
|
212
|
+
void copyWithHint(content, 'Prompt copied to clipboard.');
|
|
213
|
+
}, children: ["[", contentCopyLabel, "]"] }) })), segments.map((segment, index) => {
|
|
214
|
+
if (segment.type === 'markdown') {
|
|
215
|
+
if (!segment.value.trim()) {
|
|
216
|
+
return _jsx("box", {}, `segment-${index}`);
|
|
217
|
+
}
|
|
218
|
+
return (_jsx("markdown", { content: segment.value, syntaxStyle: syntaxStyle, conceal: true, streaming: streaming }, `segment-${index}`));
|
|
219
|
+
}
|
|
220
|
+
const normalizedLanguage = normalizeFiletype(segment.language);
|
|
221
|
+
const isDiffSegment = normalizedLanguage !== undefined &&
|
|
222
|
+
DIFF_LANGUAGES.has(normalizedLanguage);
|
|
223
|
+
const lineCount = segment.value.split('\n').length;
|
|
224
|
+
const shouldLimitCodeHeight = typeof codeBlockMaxVisibleLines === 'number' &&
|
|
225
|
+
codeBlockMaxVisibleLines > 0 &&
|
|
226
|
+
lineCount > codeBlockMaxVisibleLines;
|
|
227
|
+
const parsedDiff = isDiffSegment
|
|
228
|
+
? parseUnifiedDiff(segment.value)
|
|
229
|
+
: null;
|
|
230
|
+
const diffLanguage = extractDiffFiletype(segment.value);
|
|
231
|
+
const codeProps = {
|
|
232
|
+
code: segment.value,
|
|
233
|
+
language: normalizedLanguage,
|
|
234
|
+
content: segment.value,
|
|
235
|
+
filetype: normalizedLanguage,
|
|
236
|
+
syntaxStyle: syntaxStyle,
|
|
237
|
+
conceal: true,
|
|
238
|
+
streaming,
|
|
239
|
+
wrapMode: 'word',
|
|
240
|
+
width: '100%',
|
|
241
|
+
};
|
|
242
|
+
const codeElement = createElement('code', codeProps);
|
|
243
|
+
const codeRenderable = codeElement;
|
|
244
|
+
const diffProps = parsedDiff
|
|
245
|
+
? {
|
|
246
|
+
oldCode: parsedDiff.oldCode,
|
|
247
|
+
newCode: parsedDiff.newCode,
|
|
248
|
+
language: diffLanguage,
|
|
249
|
+
mode: 'unified',
|
|
250
|
+
diff: parsedDiff.unifiedDiff,
|
|
251
|
+
view: 'unified',
|
|
252
|
+
filetype: diffLanguage,
|
|
253
|
+
syntaxStyle: syntaxStyle,
|
|
254
|
+
showLineNumbers: true,
|
|
255
|
+
conceal: true,
|
|
256
|
+
wrapMode: 'word',
|
|
257
|
+
width: '100%',
|
|
258
|
+
}
|
|
259
|
+
: null;
|
|
260
|
+
const diffRenderable = parsedDiff
|
|
261
|
+
? createElement('diff', diffProps)
|
|
262
|
+
: null;
|
|
263
|
+
const codeDescription = isDiffSegment
|
|
264
|
+
? 'Diff snippet'
|
|
265
|
+
: normalizedLanguage
|
|
266
|
+
? `${normalizedLanguage} snippet`
|
|
267
|
+
: 'Code snippet';
|
|
268
|
+
const shouldRenderDiff = isDiffSegment && isLikelyUnifiedDiff(segment.value) && diffRenderable;
|
|
269
|
+
const snippetRenderable = shouldRenderDiff
|
|
270
|
+
? diffRenderable
|
|
271
|
+
: codeRenderable;
|
|
272
|
+
return (_jsxs("box", { flexDirection: "column", width: "100%", children: [showCodeCopyControls && (_jsxs("box", { width: "100%", justifyContent: "space-between", children: [_jsx("text", { fg: "gray", children: codeDescription }), _jsxs("text", { fg: "cyan", onMouseUp: () => {
|
|
273
|
+
setCopiedSnippetIndex(index);
|
|
274
|
+
if (copiedSnippetTimeoutRef.current) {
|
|
275
|
+
clearTimeout(copiedSnippetTimeoutRef.current);
|
|
276
|
+
}
|
|
277
|
+
copiedSnippetTimeoutRef.current = setTimeout(() => {
|
|
278
|
+
setCopiedSnippetIndex((currentIndex) => currentIndex === index ? null : currentIndex);
|
|
279
|
+
}, 1200);
|
|
280
|
+
void copyWithHint(segment.value, 'Code snippet copied to clipboard.');
|
|
281
|
+
}, children: ["[", copiedSnippetIndex === index ? 'Copied!' : 'Copy code', "]"] })] })), _jsx("box", { width: "100%", border: true, borderStyle: "single", borderColor: "gray", paddingLeft: 1, marginLeft: 1, children: shouldLimitCodeHeight ? (_jsx("scrollbox", { width: "100%", height: codeBlockMaxVisibleLines, scrollY: true, viewportCulling: false, scrollbarOptions: {
|
|
282
|
+
showArrows: false,
|
|
283
|
+
}, children: snippetRenderable })) : (snippetRenderable) })] }, `segment-${index}`));
|
|
284
|
+
}), clipboardHint && _jsx("text", { fg: "gray", children: clipboardHint })] }));
|
|
285
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import * as OpenTuiReact from '@opentui/react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { TextProgressBar } from './TextProgressBar.js';
|
|
5
|
+
const { useTerminalDimensions } = OpenTuiReact;
|
|
6
|
+
const { useTimeline } = OpenTuiReact;
|
|
7
|
+
export function PromptStatus({ value, timeLeftSeconds, critical, }) {
|
|
8
|
+
const { width } = useTerminalDimensions();
|
|
9
|
+
const [pulseLevel, setPulseLevel] = useState(0);
|
|
10
|
+
const timeline = useTimeline({ duration: 900, loop: true, autoplay: false });
|
|
11
|
+
const suffixLength = ` • ${timeLeftSeconds}s left`.length;
|
|
12
|
+
const availableWidth = Math.max(16, width - 4);
|
|
13
|
+
const reservedWidth = 2 + 1 + 4 + suffixLength;
|
|
14
|
+
const computedBarWidth = availableWidth - reservedWidth;
|
|
15
|
+
const barWidth = Math.max(6, Math.min(28, computedBarWidth));
|
|
16
|
+
const shortcutHint = width < 80 ? '⌃S send • ⇧↹ mode' : '⌃S send • ⇥ mode • ⇧↹ reverse';
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!critical) {
|
|
19
|
+
timeline.pause();
|
|
20
|
+
timeline.resetItems();
|
|
21
|
+
setPulseLevel(0);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const pulseTarget = { level: 0 };
|
|
25
|
+
timeline.resetItems();
|
|
26
|
+
timeline.add(pulseTarget, {
|
|
27
|
+
level: 1,
|
|
28
|
+
duration: 900,
|
|
29
|
+
ease: 'inOutSine',
|
|
30
|
+
loop: true,
|
|
31
|
+
alternate: true,
|
|
32
|
+
onUpdate: (animation) => {
|
|
33
|
+
const nextValue = animation.targets[0]?.level;
|
|
34
|
+
if (typeof nextValue === 'number') {
|
|
35
|
+
setPulseLevel(nextValue);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
timeline.play();
|
|
40
|
+
return () => {
|
|
41
|
+
timeline.pause();
|
|
42
|
+
timeline.resetItems();
|
|
43
|
+
};
|
|
44
|
+
}, [critical, timeline]);
|
|
45
|
+
return (_jsxs("box", { flexDirection: "column", alignItems: "flex-start", width: "100%", gap: 0, children: [_jsx("text", { fg: "gray", wrapMode: "word", children: shortcutHint }), _jsxs("text", { fg: critical && pulseLevel > 0.5 ? 'red' : 'gray', children: [critical ? '●' : '○', " ", timeLeftSeconds, "s remaining"] }), _jsx(TextProgressBar, { value: value, width: barWidth, timeLeftSeconds: timeLeftSeconds, critical: critical })] }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
export function TextProgressBar({ value, width = 28, timeLeftSeconds, critical = false, }) {
|
|
3
|
+
const clamped = Number.isFinite(value)
|
|
4
|
+
? Math.max(0, Math.min(100, value))
|
|
5
|
+
: 0;
|
|
6
|
+
const filledWidth = Math.round((clamped / 100) * width);
|
|
7
|
+
const bar = `${'█'.repeat(filledWidth)}${'░'.repeat(width - filledWidth)}`;
|
|
8
|
+
const suffix = typeof timeLeftSeconds === 'number' ? ` • ${timeLeftSeconds}s left` : '';
|
|
9
|
+
return (_jsx("text", { fg: critical ? 'red' : 'yellow', children: `[${bar}] ${Math.round(clamped)}%${suffix}` }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
4
|
+
'.git',
|
|
5
|
+
'node_modules',
|
|
6
|
+
'dist',
|
|
7
|
+
'build',
|
|
8
|
+
'.next',
|
|
9
|
+
'.turbo',
|
|
10
|
+
'.idea',
|
|
11
|
+
'.vscode',
|
|
12
|
+
'.DS_Store',
|
|
13
|
+
]);
|
|
14
|
+
const MAX_REPOSITORY_FILES = 6000;
|
|
15
|
+
const MAX_SUGGESTIONS = 6;
|
|
16
|
+
const toPosixPath = (value) => value.replaceAll(path.sep, '/');
|
|
17
|
+
const getFuzzyScore = (candidate, query) => {
|
|
18
|
+
const candidateLower = candidate.toLowerCase();
|
|
19
|
+
const queryLower = query.toLowerCase();
|
|
20
|
+
if (candidateLower.includes(queryLower)) {
|
|
21
|
+
const index = candidateLower.indexOf(queryLower);
|
|
22
|
+
const startsWithBonus = index === 0 ? 20 : 0;
|
|
23
|
+
return 1000 - index * 5 - candidate.length + startsWithBonus;
|
|
24
|
+
}
|
|
25
|
+
let queryIndex = 0;
|
|
26
|
+
let score = 0;
|
|
27
|
+
let runLength = 0;
|
|
28
|
+
for (let i = 0; i < candidateLower.length && queryIndex < queryLower.length; i++) {
|
|
29
|
+
if (candidateLower[i] === queryLower[queryIndex]) {
|
|
30
|
+
queryIndex += 1;
|
|
31
|
+
runLength += 1;
|
|
32
|
+
score += 2 + runLength;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
runLength = 0;
|
|
36
|
+
score -= 0.2;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (queryIndex !== queryLower.length) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return score - candidate.length * 0.1;
|
|
43
|
+
};
|
|
44
|
+
export const getAutocompleteTarget = (value, caret) => {
|
|
45
|
+
const clampedCaret = Math.max(0, Math.min(caret, value.length));
|
|
46
|
+
let start = clampedCaret;
|
|
47
|
+
const separators = new Set([' ', '\n', '\t', '"', "'", '(', ')', '[', ']']);
|
|
48
|
+
const isWhitespace = (character) => character === ' ' || character === '\n' || character === '\t';
|
|
49
|
+
while (start > 0 && !separators.has(value[start - 1])) {
|
|
50
|
+
start -= 1;
|
|
51
|
+
}
|
|
52
|
+
const token = value.slice(start, clampedCaret);
|
|
53
|
+
if (token.startsWith('#')) {
|
|
54
|
+
return {
|
|
55
|
+
start,
|
|
56
|
+
end: clampedCaret,
|
|
57
|
+
query: token.slice(1),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let probe = start;
|
|
61
|
+
while (probe > 0 && isWhitespace(value[probe - 1])) {
|
|
62
|
+
probe -= 1;
|
|
63
|
+
}
|
|
64
|
+
if (probe <= 0 || value[probe - 1] !== '#') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const hashIndex = probe - 1;
|
|
68
|
+
if (hashIndex > 0 && !separators.has(value[hashIndex - 1])) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
start: hashIndex,
|
|
73
|
+
end: clampedCaret,
|
|
74
|
+
query: value.slice(hashIndex + 1, clampedCaret).trimStart(),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export const rankFileSuggestions = (files, query) => {
|
|
78
|
+
if (query.length === 0) {
|
|
79
|
+
return files.slice(0, MAX_SUGGESTIONS);
|
|
80
|
+
}
|
|
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);
|
|
90
|
+
};
|
|
91
|
+
export const readRepositoryFiles = async (repoRoot) => {
|
|
92
|
+
const discoveredFiles = [];
|
|
93
|
+
const visitDirectory = async (directoryPath) => {
|
|
94
|
+
if (discoveredFiles.length >= MAX_REPOSITORY_FILES) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let entries = [];
|
|
98
|
+
try {
|
|
99
|
+
entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (discoveredFiles.length >= MAX_REPOSITORY_FILES) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const entryAbsolutePath = path.join(directoryPath, entry.name);
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
await visitDirectory(entryAbsolutePath);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (entry.isFile()) {
|
|
117
|
+
const relativePath = path.relative(repoRoot, entryAbsolutePath);
|
|
118
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
discoveredFiles.push(toPosixPath(relativePath));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
await visitDirectory(repoRoot);
|
|
126
|
+
return discoveredFiles.sort((a, b) => a.localeCompare(b));
|
|
127
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const CTRL_MODIFIED_KEY_CSI_U_SEQUENCE = new RegExp(`^${String.fromCharCode(27)}\\[(\\d+);(\\d+)u$`);
|
|
2
|
+
const hasCtrlOrMetaModifier = (modifierCode) => {
|
|
3
|
+
const modifierFlags = modifierCode - 1;
|
|
4
|
+
const includesCtrl = (modifierFlags & 4) !== 0;
|
|
5
|
+
const includesMeta = (modifierFlags & 8) !== 0;
|
|
6
|
+
return includesCtrl || includesMeta;
|
|
7
|
+
};
|
|
8
|
+
export const isControlKeyShortcut = (key, letter) => {
|
|
9
|
+
const lowerName = key.name.toLowerCase();
|
|
10
|
+
if ((key.ctrl || key.meta) && lowerName === letter) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
const csiUMatch = key.sequence.match(CTRL_MODIFIED_KEY_CSI_U_SEQUENCE);
|
|
14
|
+
if (!csiUMatch) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const keyCode = Number(csiUMatch[1]);
|
|
18
|
+
const modifierCode = Number(csiUMatch[2]);
|
|
19
|
+
const lowercaseKeyCode = letter.charCodeAt(0);
|
|
20
|
+
const uppercaseKeyCode = letter.toUpperCase().charCodeAt(0);
|
|
21
|
+
return ((keyCode === lowercaseKeyCode || keyCode === uppercaseKeyCode) &&
|
|
22
|
+
hasCtrlOrMetaModifier(modifierCode));
|
|
23
|
+
};
|
|
24
|
+
export const isSubmitShortcut = (key) => isControlKeyShortcut(key, 's');
|
|
25
|
+
export const isReverseTabShortcut = (key) => key.name === 'backtab' ||
|
|
26
|
+
(key.name === 'tab' && key.shift) ||
|
|
27
|
+
key.sequence === '\u001b[Z';
|
|
28
|
+
export const isPrintableCharacter = (key) => {
|
|
29
|
+
if (key.ctrl || key.meta || key.option || key.name === 'tab') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (key.name === 'space') {
|
|
33
|
+
return ' ';
|
|
34
|
+
}
|
|
35
|
+
if (key.sequence.length === 1 && key.sequence >= ' ') {
|
|
36
|
+
return key.sequence;
|
|
37
|
+
}
|
|
38
|
+
if (key.name.length === 1) {
|
|
39
|
+
return key.shift ? key.name.toUpperCase() : key.name;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
export const textareaKeyBindings = [
|
|
44
|
+
{ name: 's', ctrl: true, action: 'submit' },
|
|
45
|
+
{ name: 's', meta: true, action: 'submit' },
|
|
46
|
+
{ name: 's', super: true, action: 'submit' },
|
|
47
|
+
{ name: 'a', ctrl: true, action: 'select-all' },
|
|
48
|
+
{ name: 'a', meta: true, action: 'select-all' },
|
|
49
|
+
{ name: 'a', super: true, action: 'select-all' },
|
|
50
|
+
{ name: 'j', ctrl: true, action: 'newline' },
|
|
51
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for the application.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Timeout duration in seconds for waiting for user input in both single-input and intensive chat modes.
|
|
6
|
+
* This aligns with the default timeout expected by the MCP tool.
|
|
7
|
+
*/
|
|
8
|
+
export const USER_INPUT_TIMEOUT_SECONDS = 60;
|
|
9
|
+
/**
|
|
10
|
+
* Sentinel string written by UI processes when a prompt times out.
|
|
11
|
+
* This should be handled consistently across command and tool layers.
|
|
12
|
+
*/
|
|
13
|
+
export const USER_INPUT_TIMEOUT_SENTINEL = '__TIMEOUT__';
|