Skip to content

Commit 5446951

Browse files
authored
Merge pull request #2 from ivanleomk/ivanleomk/migrate-representation
Implement AI-SDK
2 parents 8c24f89 + 8fca7d2 commit 5446951

20 files changed

+2187
-546
lines changed

agent.ts

Lines changed: 638 additions & 187 deletions
Large diffs are not rendered by default.

bun.lockb

832 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"ink": "^4.1.0",
2222
"ink-text-input": "^6.0.0",
2323
"meow": "^11.0.0",
24+
"openai": "^6.2.0",
2425
"react": "^18.2.0",
2526
"zod": "^4.0.5"
2627
},

source/app.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,42 @@
1-
import React, {useState} from 'react';
1+
import React, {useState, useMemo} from 'react';
22
import {Text, Box, useInput, useApp} from 'ink';
33
import TextInput from 'ink-text-input';
4-
import {useMessages} from './hooks/useMessages.js';
4+
import {useAgent} from './hooks/useAgent.js';
55
import Message from './components/Message.js';
6+
// import {XAIProvider} from './provider/xai.js';
7+
// import {AnthropicProvider} from './provider/anthropic.js';
8+
import {XAIProvider} from './provider/xai.js';
69

710
export default function App() {
8-
const {messages, input, setInput, sendMessage} = useMessages();
11+
const provider = useMemo(
12+
() =>
13+
new XAIProvider({
14+
baseURL: 'https://api.x.ai/v1',
15+
apiKey: process.env['XAI_API_KEY'],
16+
defaultModel: 'grok-code-fast-1',
17+
maxTokens: 4096,
18+
}),
19+
// new AnthropicProvider({
20+
// defaultModel: 'claude-3-7-sonnet-latest',
21+
// maxTokens: 8092,
22+
// reasoningBudget: 4096,
23+
// }),
24+
[],
25+
);
26+
27+
const {conversation, complete} = useAgent(provider);
28+
const [input, setInput] = useState('');
929
const [showShutdown, setShowShutdown] = useState(false);
1030
const {exit} = useApp();
1131

32+
const sendMessage = async () => {
33+
if (input.trim().length === 0) {
34+
return;
35+
}
36+
setInput('');
37+
await complete(input);
38+
};
39+
1240
useInput((input, key) => {
1341
if (key.ctrl && input === 'd') {
1442
setShowShutdown(true);
@@ -22,10 +50,10 @@ export default function App() {
2250
return (
2351
<Box flexDirection="column" height="100%">
2452
<Box flexGrow={1} flexDirection="column" gap={1}>
25-
{messages.map((message, index) => (
53+
{conversation.map((message, index) => (
2654
<Message key={index} message={message} />
2755
))}
28-
{messages.length > 0 && <Box flexGrow={1} />}
56+
{conversation.length > 0 && <Box flexGrow={1} />}
2957
</Box>
3058
<Box width="100%">
3159
<Text color="blue">$ </Text>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import {Box, Text} from 'ink';
3+
import type {Tool} from '../lib/types.js';
4+
5+
interface CreateFileDisplayProps {
6+
toolCall: Tool & {name: 'create_file'};
7+
}
8+
9+
export default function CreateFileDisplay({toolCall}: CreateFileDisplayProps) {
10+
const args = toolCall.args as {path: string; content: string};
11+
const lines = args.content.split('\n');
12+
const maxLines = 25;
13+
const displayLines = lines.slice(0, maxLines);
14+
const hasMore = lines.length > maxLines;
15+
16+
return (
17+
<Box flexDirection="column" marginY={0} width="90%" alignSelf="center">
18+
<Box
19+
borderStyle="round"
20+
borderColor="gray"
21+
borderBottom={false}
22+
paddingLeft={1}
23+
paddingRight={1}
24+
>
25+
<Text color="gray">create_file </Text>
26+
<Text color="white">[{args.path}]</Text>
27+
</Box>
28+
<Box
29+
borderStyle="round"
30+
borderColor="gray"
31+
borderTop={false}
32+
padding={1}
33+
flexDirection="column"
34+
>
35+
<Text color="white" wrap="wrap">
36+
{displayLines.join('\n')}
37+
</Text>
38+
{hasMore && <Text color="dim">... total({lines.length}) lines</Text>}
39+
</Box>
40+
</Box>
41+
);
42+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import {Box, Text} from 'ink';
3+
import type {Tool} from '../lib/types.js';
4+
5+
interface ListFileDisplayProps {
6+
toolCall: Tool & {name: 'list_files'};
7+
}
8+
9+
export default function ListFileDisplay({toolCall}: ListFileDisplayProps) {
10+
const textOutput = toolCall.output.find(item => item.type === 'text');
11+
const args = toolCall.args as {directory: string; maxDepth: number};
12+
13+
if (!textOutput || textOutput.type !== 'text') {
14+
return null;
15+
}
16+
17+
const lines = textOutput.text.split('\n');
18+
const maxLines = 25;
19+
const displayLines = lines.slice(0, maxLines);
20+
const hasMore = lines.length > maxLines;
21+
22+
return (
23+
<Box flexDirection="column" marginY={0} width="90%" alignSelf="center">
24+
<Box
25+
borderStyle="round"
26+
borderColor="gray"
27+
borderBottom={false}
28+
paddingLeft={1}
29+
paddingRight={1}
30+
>
31+
<Text color="gray">list_files </Text>
32+
<Text color="white"> [{args.directory}]</Text>
33+
</Box>
34+
<Box
35+
borderStyle="round"
36+
borderColor="gray"
37+
borderTop={false}
38+
padding={1}
39+
flexDirection="column"
40+
>
41+
<Text color="white" wrap="wrap">
42+
{displayLines.join('\n')}
43+
</Text>
44+
{hasMore && (
45+
<Text color="dim">
46+
... found a total of ({lines.length}) files/directories
47+
</Text>
48+
)}
49+
</Box>
50+
</Box>
51+
);
52+
}

source/components/MarkdownText.tsx

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import React from 'react';
2+
import {Box, Text} from 'ink';
3+
import {highlight} from 'cli-highlight';
4+
5+
interface MarkdownTextProps {
6+
children: string;
7+
color?: string;
8+
}
9+
10+
function CodeBlock({code, language}: {code: string; language?: string}) {
11+
const highlightedCode = language
12+
? highlight(code, {language, ignoreIllegals: true})
13+
: code;
14+
15+
return (
16+
<Box
17+
flexDirection="column"
18+
marginY={0}
19+
padding={0}
20+
width="90%"
21+
alignSelf="center"
22+
>
23+
<Box
24+
padding={1}
25+
borderStyle="round"
26+
borderColor="gray"
27+
justifyContent="space-between"
28+
>
29+
<Text color="white">{highlightedCode}</Text>
30+
{language && (
31+
<Text color="cyan" dimColor>
32+
{language}
33+
</Text>
34+
)}
35+
</Box>
36+
</Box>
37+
);
38+
}
39+
40+
function InlineMarkdown({children, color = 'white'}: MarkdownTextProps) {
41+
const parts: React.ReactNode[] = [];
42+
let currentIndex = 0;
43+
const text = children;
44+
45+
const headerStyles = {
46+
1: {
47+
bold: true,
48+
underline: true,
49+
color: 'cyan',
50+
fontSize: 20,
51+
marginBottom: 1,
52+
},
53+
2: {bold: true, color: 'cyan', fontSize: 18, marginBottom: 1},
54+
3: {bold: true, color: 'white', fontSize: 16, marginBottom: 1},
55+
} as const;
56+
57+
const patterns = [
58+
{
59+
regex: /^(#{1,4})\s+(.+)/gm,
60+
render: (match: string, level: number) => {
61+
const style =
62+
headerStyles[level as keyof typeof headerStyles] || headerStyles[3];
63+
return (
64+
<Text key={currentIndex++} {...style}>
65+
{match}
66+
</Text>
67+
);
68+
},
69+
},
70+
{
71+
regex: /`([^`]+)`/g,
72+
render: (match: string) => (
73+
<Text key={currentIndex++} backgroundColor="gray" color="white">
74+
{match}
75+
</Text>
76+
),
77+
},
78+
{
79+
regex: /\*\*([^*]+)\*\*/g,
80+
render: (match: string) => (
81+
<Text key={currentIndex++} bold>
82+
{match}
83+
</Text>
84+
),
85+
},
86+
{
87+
regex: /\*([^*]+)\*/g,
88+
render: (match: string) => (
89+
<Text key={currentIndex++} italic>
90+
{match}
91+
</Text>
92+
),
93+
},
94+
];
95+
96+
let lastIndex = 0;
97+
const matches: Array<{start: number; end: number; node: React.ReactNode}> =
98+
[];
99+
100+
for (const {regex, render} of patterns) {
101+
const re = new RegExp(regex);
102+
let match;
103+
while ((match = re.exec(text)) !== null) {
104+
if (match[1]) {
105+
const captureGroup = match[2] || match[1];
106+
const level = match[2] ? match[1].length : undefined;
107+
matches.push({
108+
start: match.index,
109+
end: match.index + match[0].length,
110+
node: render(captureGroup, level as number),
111+
});
112+
}
113+
}
114+
}
115+
116+
matches.sort((a, b) => a.start - b.start);
117+
118+
const nonOverlapping: Array<{
119+
start: number;
120+
end: number;
121+
node: React.ReactNode;
122+
}> = [];
123+
for (const match of matches) {
124+
const lastItem = nonOverlapping[nonOverlapping.length - 1];
125+
if (
126+
nonOverlapping.length === 0 ||
127+
(lastItem && match.start >= lastItem.end)
128+
) {
129+
nonOverlapping.push(match);
130+
}
131+
}
132+
133+
lastIndex = 0;
134+
for (const {start, end, node} of nonOverlapping) {
135+
if (start > lastIndex) {
136+
parts.push(
137+
<Text key={currentIndex++} color={color}>
138+
{text.slice(lastIndex, start)}
139+
</Text>,
140+
);
141+
}
142+
parts.push(node);
143+
lastIndex = end;
144+
}
145+
146+
if (lastIndex < text.length) {
147+
parts.push(
148+
<Text key={currentIndex++} color={color}>
149+
{text.slice(lastIndex)}
150+
</Text>,
151+
);
152+
}
153+
154+
return <>{parts}</>;
155+
}
156+
157+
export default function MarkdownText({children, color = 'white'}: MarkdownTextProps) {
158+
const codeBlockRegex = /```(\w+)?\n([\s\S]+?)\n?```/g;
159+
const segments: React.ReactNode[] = [];
160+
let lastIndex = 0;
161+
let match;
162+
let keyIndex = 0;
163+
164+
while ((match = codeBlockRegex.exec(children)) !== null) {
165+
if (match.index > lastIndex) {
166+
const textBefore = children.slice(lastIndex, match.index);
167+
segments.push(
168+
<Box key={keyIndex++} marginLeft={2}>
169+
<Box flexGrow={1} flexShrink={1}>
170+
<Text color={color} wrap="wrap">
171+
<InlineMarkdown color={color}>{textBefore}</InlineMarkdown>
172+
</Text>
173+
</Box>
174+
</Box>,
175+
);
176+
}
177+
178+
if (match[2]) {
179+
segments.push(
180+
<CodeBlock key={keyIndex++} code={match[2]} language={match[1]} />,
181+
);
182+
}
183+
lastIndex = match.index + match[0].length;
184+
}
185+
186+
if (lastIndex < children.length) {
187+
const textAfter = children.slice(lastIndex);
188+
segments.push(
189+
<Box key={keyIndex++} marginLeft={2}>
190+
<Box flexGrow={1} flexShrink={1}>
191+
<Text color={color} wrap="wrap">
192+
<InlineMarkdown color={color}>{textAfter}</InlineMarkdown>
193+
</Text>
194+
</Box>
195+
</Box>,
196+
);
197+
}
198+
199+
return <>{segments}</>;
200+
}

0 commit comments

Comments
 (0)