Skip to content

Commit d2e0cbc

Browse files
authored
Merge pull request #3 from ivanleomk/feat/add-grep-glob
Adding Agentic Search
2 parents 527a556 + 028117d commit d2e0cbc

23 files changed

+1029
-730
lines changed

agent.ts

Lines changed: 0 additions & 654 deletions
This file was deleted.

bun.lockb

13.2 KB
Binary file not shown.

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@
99
},
1010
"scripts": {
1111
"build": "tsc",
12+
"release": "tsc --outDir stable",
1213
"dev": "tsc --watch",
1314
"test": "prettier --check . && xo && ava",
14-
"start": "tsc && node ./dist/cli.js"
15+
"start": "bun run ./source/cli.tsx"
1516
},
1617
"files": [
1718
"dist"
1819
],
1920
"dependencies": {
2021
"@anthropic-ai/sdk": "^0.56.0",
22+
"@ast-grep/cli": "^0.39.6",
23+
"exa-js": "^1.10.2",
24+
"execa": "^9.6.0",
25+
"globby": "^13.0.0",
2126
"ink": "^4.1.0",
2227
"ink-text-input": "^6.0.0",
2328
"meow": "^11.0.0",
@@ -27,6 +32,7 @@
2732
},
2833
"devDependencies": {
2934
"@sindresorhus/tsconfig": "^3.0.1",
35+
"@types/bun": "^1.3.0",
3036
"@types/react": "^18.0.32",
3137
"@vdemedes/prettier-config": "^2.0.1",
3238
"ava": "^5.2.0",

source/app.tsx

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React, {useState, useMemo} from 'react';
22
import {Text, Box, useInput, useApp} from 'ink';
3-
import TextInput from 'ink-text-input';
3+
44
import {useAgent} from './hooks/useAgent.js';
55
import Message from './components/Message.js';
6+
import Autocomplete from './components/Autocomplete.js';
7+
import TextInput from './components/TextInput.js';
68
// import {XAIProvider} from './provider/xai.js';
79
// import {AnthropicProvider} from './provider/anthropic.js';
810
import {XAIProvider} from './provider/xai.js';
@@ -26,9 +28,13 @@ export default function App() {
2628

2729
const {conversation, complete} = useAgent(provider);
2830
const [input, setInput] = useState('');
31+
const [cursorOffset, setCursorOffset] = useState(0);
2932
const [showShutdown, setShowShutdown] = useState(false);
3033
const {exit} = useApp();
3134

35+
const lastTerm = input.split(' ').at(-1) || '';
36+
const showAutocomplete = lastTerm.includes('@');
37+
3238
const sendMessage = async () => {
3339
if (input.trim().length === 0) {
3440
return;
@@ -55,13 +61,45 @@ export default function App() {
5561
))}
5662
{conversation.length > 0 && <Box flexGrow={1} />}
5763
</Box>
58-
<Box width="100%">
59-
<Text color="blue">$ </Text>
60-
<TextInput
61-
value={input}
62-
onChange={setInput}
63-
placeholder="Type a command..."
64-
/>
64+
<Box>
65+
{showAutocomplete ? (
66+
<Autocomplete
67+
searchTerm={lastTerm}
68+
updateUserInput={newTerm => {
69+
const prevParts = input.split(/\s+/);
70+
prevParts[prevParts.length - 1] = newTerm;
71+
const newInput = prevParts.join(' ') + ' ';
72+
setInput(newInput);
73+
setCursorOffset(newInput.length);
74+
}}
75+
/>
76+
) : null}
77+
</Box>
78+
<Box
79+
flexDirection="column"
80+
width="100%"
81+
borderStyle="round"
82+
borderColor="gray"
83+
paddingX={1}
84+
paddingTop={0.5}
85+
paddingBottom={1}
86+
>
87+
<Box flexDirection="column">
88+
<Box paddingBottom={0.5}>
89+
<Text color="green">Current Directory: </Text>
90+
<Text color="cyan">{process.cwd()}</Text>
91+
</Box>
92+
<Box>
93+
<Text color="green">&gt; </Text>
94+
<TextInput
95+
value={input}
96+
onChange={setInput}
97+
cursorOffset={cursorOffset}
98+
onCursorOffsetChange={setCursorOffset}
99+
placeholder="Type a command..."
100+
/>
101+
</Box>
102+
</Box>
65103
</Box>
66104
{showShutdown && (
67105
<Box>

source/components/Autocomplete.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, {useState} from 'react';
2+
import {Text, Box, useInput} from 'ink';
3+
import {execSync} from 'child_process';
4+
5+
interface AutocompleteProps {
6+
searchTerm: string;
7+
updateUserInput: (newInput: string) => void;
8+
}
9+
10+
const generateSuggestions = (input: string): string[] => {
11+
const lastWord = input.split(' ').at(-1);
12+
if (!lastWord?.includes('@')) {
13+
return [];
14+
}
15+
16+
const searchTerm = lastWord.split('@')[1] || '';
17+
if (!searchTerm) {
18+
return [];
19+
}
20+
21+
try {
22+
const result = execSync(`rg --files | rg -i "${searchTerm}"`, {
23+
encoding: 'utf-8',
24+
cwd: process.cwd(),
25+
stdio: ['pipe', 'pipe', 'ignore'],
26+
});
27+
return result.trim().split('\n').filter(Boolean).slice(0, 5);
28+
} catch (error) {
29+
return [];
30+
}
31+
};
32+
33+
export default function Autocomplete({
34+
searchTerm,
35+
updateUserInput,
36+
}: AutocompleteProps) {
37+
const suggestions = generateSuggestions(searchTerm);
38+
const [selectedIndex, setSelectedIndex] = useState(0);
39+
40+
useInput((_, key) => {
41+
if (key.upArrow) {
42+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : suggestions.length - 1));
43+
}
44+
if (key.downArrow) {
45+
setSelectedIndex(prev => (prev < suggestions.length - 1 ? prev + 1 : 0));
46+
}
47+
if ((key.tab || key.return) && suggestions[selectedIndex]) {
48+
const inputParts = searchTerm.split(' ');
49+
inputParts[inputParts.length - 1] = `@${suggestions[selectedIndex]}`;
50+
updateUserInput(inputParts.join(' '));
51+
}
52+
});
53+
54+
return (
55+
<Box flexDirection="column" borderStyle="round" borderColor="cyan">
56+
{suggestions.map((suggestion, index) => {
57+
const isSelected = index === selectedIndex;
58+
return (
59+
<Box key={index}>
60+
<Text>{isSelected ? '> ' : ' '}</Text>
61+
<Text bold={isSelected}>{suggestion}</Text>
62+
</Box>
63+
);
64+
})}
65+
</Box>
66+
);
67+
}

source/components/BashDisplay.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react';
2+
import {Box, Text} from 'ink';
3+
import type {Tool} from '../lib/types.js';
4+
5+
interface BashDisplayProps {
6+
toolCall: Tool & {name: 'bash'};
7+
}
8+
9+
export default function BashDisplay({toolCall}: BashDisplayProps) {
10+
const textOutputs = toolCall.output.filter(item => item.type === 'text');
11+
const args = toolCall.args as {
12+
command: string;
13+
cwd?: string;
14+
};
15+
16+
return (
17+
<Box flexDirection="column" marginLeft={2} gap={1}>
18+
{textOutputs.map((output, index) => {
19+
if (output.type !== 'text') return null;
20+
21+
const lines = output.text
22+
.split('\n')
23+
.map(line => line.trimEnd());
24+
25+
return (
26+
<Box key={index} flexDirection="column">
27+
<Box
28+
borderStyle="round"
29+
borderColor="green"
30+
padding={1}
31+
flexDirection="column"
32+
>
33+
<Text color="green">$ {args.command}</Text>
34+
{args.cwd && <Text color="gray" dimColor>cwd: {args.cwd}</Text>}
35+
<Text>{'\n'}</Text>
36+
<Text color="white">{lines.join('\n')}</Text>
37+
</Box>
38+
</Box>
39+
);
40+
})}
41+
</Box>
42+
);
43+
}

source/components/CreateFileDisplay.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,52 @@
11
import React from 'react';
22
import {Box, Text} from 'ink';
3+
import {highlight} from 'cli-highlight';
34
import type {Tool} from '../lib/types.js';
45

6+
const languageMap: Record<string, string> = {
7+
'.ts': 'typescript',
8+
'.tsx': 'typescript',
9+
'.js': 'javascript',
10+
'.jsx': 'javascript',
11+
'.py': 'python',
12+
'.rb': 'ruby',
13+
'.java': 'java',
14+
'.cpp': 'cpp',
15+
'.c': 'c',
16+
'.php': 'php',
17+
'.go': 'go',
18+
'.rs': 'rust',
19+
'.sh': 'bash',
20+
'.json': 'json',
21+
'.xml': 'xml',
22+
'.html': 'html',
23+
'.css': 'css',
24+
'.sql': 'sql',
25+
'.md': 'markdown',
26+
'.yaml': 'yaml',
27+
'.yml': 'yaml',
28+
};
29+
30+
function getLanguageFromPath(filePath: string): string | undefined {
31+
const ext = filePath.substring(filePath.lastIndexOf('.'));
32+
return languageMap[ext];
33+
}
34+
535
interface CreateFileDisplayProps {
636
toolCall: Tool & {name: 'create_file'};
737
}
838

939
export default function CreateFileDisplay({toolCall}: CreateFileDisplayProps) {
1040
const args = toolCall.args as {filePath: string; content: string};
11-
const lines = args.content.split('\n');
41+
const processedContent = args.content
42+
.split('\n')
43+
.map(line => line.replace(/\t/g, ' ').trimEnd())
44+
.join('\n');
45+
const language = getLanguageFromPath(args.filePath);
46+
const highlightedContent = language
47+
? highlight(processedContent, {language, ignoreIllegals: true})
48+
: processedContent;
49+
const lines = highlightedContent.split('\n');
1250
const maxLines = 25;
1351
const displayLines = lines.slice(0, maxLines);
1452
const hasMore = lines.length > maxLines;
@@ -24,6 +62,7 @@ export default function CreateFileDisplay({toolCall}: CreateFileDisplayProps) {
2462
>
2563
<Text color="gray">create_file </Text>
2664
<Text color="white">[{args.filePath}]</Text>
65+
{language && <Text color="cyan"> ({language})</Text>}
2766
</Box>
2867
<Box
2968
borderStyle="round"

source/components/GlobDisplay.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import {Box, Text} from 'ink';
3+
import type {Tool} from '../lib/types.js';
4+
5+
interface GlobDisplayProps {
6+
toolCall: Tool & {name: 'glob'};
7+
}
8+
9+
export default function GlobDisplay({toolCall}: GlobDisplayProps) {
10+
const textOutput = toolCall.output.find(item => item.type === 'text');
11+
const args = toolCall.args as {
12+
pattern: string;
13+
directory: string;
14+
limit?: number;
15+
offset?: number;
16+
};
17+
18+
if (!textOutput || textOutput.type !== 'text') {
19+
return null;
20+
}
21+
22+
const lines = textOutput.text.split('\n').filter(Boolean);
23+
const maxLines = 25;
24+
const displayLines = lines.slice(0, maxLines);
25+
const hasMore = lines.length > maxLines;
26+
27+
return (
28+
<Box flexDirection="column" marginY={0} width="90%" alignSelf="center">
29+
<Box
30+
borderStyle="round"
31+
borderColor="gray"
32+
borderBottom={false}
33+
paddingLeft={1}
34+
paddingRight={1}
35+
>
36+
<Text color="gray">glob </Text>
37+
<Text color="white">
38+
{' '}
39+
[{args.pattern}] in [{args.directory}]
40+
</Text>
41+
</Box>
42+
<Box
43+
borderStyle="round"
44+
borderColor="gray"
45+
borderTop={false}
46+
padding={1}
47+
flexDirection="column"
48+
>
49+
<Text color="white" wrap="wrap">
50+
{displayLines.join('\n')}
51+
</Text>
52+
{hasMore && (
53+
<Text color="dim">... found a total of ({lines.length}) files</Text>
54+
)}
55+
</Box>
56+
</Box>
57+
);
58+
}

0 commit comments

Comments
 (0)