thatgfsj-code 0.4.1 → 0.6.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/README.md +7 -12
- package/dist/app/index.d.ts +6 -14
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +29 -31
- package/dist/app/index.js.map +1 -1
- package/dist/cmd/index.d.ts +1 -2
- package/dist/cmd/index.d.ts.map +1 -1
- package/dist/cmd/index.js +58 -47
- package/dist/cmd/index.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +0 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/tui/app.d.ts +9 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +40 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/components/ChatList.d.ts +14 -0
- package/dist/tui/components/ChatList.d.ts.map +1 -0
- package/dist/tui/components/ChatList.js +11 -0
- package/dist/tui/components/ChatList.js.map +1 -0
- package/dist/tui/components/ChatMessage.d.ts +14 -0
- package/dist/tui/components/ChatMessage.d.ts.map +1 -0
- package/dist/tui/components/ChatMessage.js +17 -0
- package/dist/tui/components/ChatMessage.js.map +1 -0
- package/dist/tui/components/Header.d.ts +9 -0
- package/dist/tui/components/Header.d.ts.map +1 -0
- package/dist/tui/components/Header.js +6 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/Markdown.d.ts +8 -0
- package/dist/tui/components/Markdown.d.ts.map +1 -0
- package/dist/tui/components/Markdown.js +31 -0
- package/dist/tui/components/Markdown.js.map +1 -0
- package/dist/tui/components/Thinking.d.ts +8 -0
- package/dist/tui/components/Thinking.d.ts.map +1 -0
- package/dist/tui/components/Thinking.js +7 -0
- package/dist/tui/components/Thinking.js.map +1 -0
- package/dist/tui/components/ToolCall.d.ts +14 -0
- package/dist/tui/components/ToolCall.d.ts.map +1 -0
- package/dist/tui/components/ToolCall.js +37 -0
- package/dist/tui/components/ToolCall.js.map +1 -0
- package/dist/tui/components/UserInput.d.ts +9 -0
- package/dist/tui/components/UserInput.d.ts.map +1 -0
- package/dist/tui/components/UserInput.js +82 -0
- package/dist/tui/components/UserInput.js.map +1 -0
- package/dist/tui/hooks/useChat.d.ts +11 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -0
- package/dist/tui/hooks/useChat.js +138 -0
- package/dist/tui/hooks/useChat.js.map +1 -0
- package/dist/tui/hooks/useCommands.d.ts +10 -0
- package/dist/tui/hooks/useCommands.d.ts.map +1 -0
- package/dist/tui/hooks/useCommands.js +51 -0
- package/dist/tui/hooks/useCommands.js.map +1 -0
- package/dist/tui/index.d.ts +2 -4
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +2 -4
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/output.d.ts +12 -3
- package/dist/tui/output.d.ts.map +1 -1
- package/dist/tui/output.js +43 -47
- package/dist/tui/output.js.map +1 -1
- package/dist/tui/repl.d.ts.map +1 -1
- package/dist/tui/repl.js +42 -28
- package/dist/tui/repl.js.map +1 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +23 -5
- package/dist/tui/welcome.js.map +1 -1
- package/install.ps1 +171 -192
- package/package.json +9 -1
- package/src/app/index.ts +29 -34
- package/src/cmd/{index.ts → index.tsx} +52 -47
- package/src/config/index.ts +1 -0
- package/src/config/types.ts +1 -0
- package/src/llm/index.ts +0 -1
- package/src/tui/app.tsx +60 -0
- package/src/tui/components/ChatList.tsx +29 -0
- package/src/tui/components/ChatMessage.tsx +51 -0
- package/src/tui/components/Header.tsx +22 -0
- package/src/tui/components/Markdown.tsx +35 -0
- package/src/tui/components/Thinking.tsx +19 -0
- package/src/tui/components/ToolCall.tsx +72 -0
- package/src/tui/components/UserInput.tsx +101 -0
- package/src/tui/hooks/useChat.ts +154 -0
- package/src/tui/hooks/useCommands.ts +63 -0
- package/src/tui/index.ts +2 -4
- package/src/tui/output.ts +43 -44
- package/src/tui/repl.ts +40 -29
- package/src/tui/welcome.ts +25 -6
- package/tsconfig.json +5 -3
- package/src/app/agent.ts +0 -140
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Thatgfsj Code - CLI Entry Point
|
|
5
|
-
* Simplified from old src/index.ts (655 lines → ~60 lines)
|
|
6
5
|
*
|
|
7
6
|
* Two modes:
|
|
8
|
-
* gfcode → Interactive
|
|
7
|
+
* gfcode → Interactive TUI (Ink)
|
|
9
8
|
* gfcode "prompt" → Single prompt mode
|
|
10
9
|
* gfcode init → Setup wizard
|
|
11
10
|
*/
|
|
@@ -20,18 +19,18 @@ if (process.platform === 'win32') {
|
|
|
20
19
|
import { program } from 'commander';
|
|
21
20
|
import chalk from 'chalk';
|
|
22
21
|
import { App } from '../app/index.js';
|
|
23
|
-
import { REPL } from '../tui/repl.js';
|
|
24
22
|
import { WelcomeScreen } from '../tui/welcome.js';
|
|
23
|
+
import { REPLOutput } from '../tui/output.js';
|
|
25
24
|
|
|
26
25
|
// ==================== Error Handling ====================
|
|
27
26
|
|
|
28
27
|
process.on('uncaughtException', (error) => {
|
|
29
|
-
console.error(chalk.red('\n
|
|
28
|
+
console.error(chalk.red('\n Error:'), error.message);
|
|
30
29
|
process.exit(1);
|
|
31
30
|
});
|
|
32
31
|
|
|
33
32
|
process.on('unhandledRejection', (reason) => {
|
|
34
|
-
console.error(chalk.red('\n
|
|
33
|
+
console.error(chalk.red('\n Error:'), reason);
|
|
35
34
|
process.exit(1);
|
|
36
35
|
});
|
|
37
36
|
|
|
@@ -39,15 +38,13 @@ process.on('unhandledRejection', (reason) => {
|
|
|
39
38
|
|
|
40
39
|
program
|
|
41
40
|
.name('gfcode')
|
|
42
|
-
.description('
|
|
43
|
-
.version('0.
|
|
41
|
+
.description('Thatgfsj Code - AI Coding Assistant')
|
|
42
|
+
.version('0.6.0')
|
|
44
43
|
.argument('[prompt]', 'Task to execute (omit to start interactive mode)')
|
|
45
44
|
.option('-m, --model <model>', 'Specify model')
|
|
46
45
|
.option('-i, --interactive', 'Force interactive mode')
|
|
47
|
-
.
|
|
48
|
-
.action(async (prompt: string | undefined, options: { model?: string; interactive?: boolean; hooks?: boolean }) => {
|
|
46
|
+
.action(async (prompt: string | undefined, options: { model?: string; interactive?: boolean }) => {
|
|
49
47
|
try {
|
|
50
|
-
// Initialize app
|
|
51
48
|
const app = await App.create();
|
|
52
49
|
|
|
53
50
|
// Show welcome if no API key
|
|
@@ -58,21 +55,27 @@ program
|
|
|
58
55
|
await app.config.save({ model: options.model });
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
// Enable hooks if requested
|
|
62
|
-
if (options.hooks) {
|
|
63
|
-
const { auditLogHook } = await import('../hooks/index.js');
|
|
64
|
-
app.hooks.register('afterToolCall', auditLogHook);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
if (!prompt || options.interactive) {
|
|
68
|
-
// Interactive
|
|
69
|
-
const
|
|
70
|
-
await
|
|
59
|
+
// Interactive mode - use Ink TUI
|
|
60
|
+
const { render } = await import('ink');
|
|
61
|
+
const { TuiApp } = await import('../tui/app.js');
|
|
62
|
+
|
|
63
|
+
const { unmount } = render(<TuiApp app={app} />);
|
|
64
|
+
|
|
65
|
+
// Wait for the app to exit
|
|
66
|
+
await new Promise<void>((resolve) => {
|
|
67
|
+
const check = setInterval(() => {
|
|
68
|
+
// Ink will call process.exit or we detect it
|
|
69
|
+
}, 1000);
|
|
70
|
+
process.on('exit', () => {
|
|
71
|
+
clearInterval(check);
|
|
72
|
+
unmount();
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
71
76
|
} else {
|
|
72
|
-
// Single prompt mode - use
|
|
73
|
-
const { REPLOutput } = await import('../tui/output.js');
|
|
77
|
+
// Single prompt mode - use simple output
|
|
74
78
|
const out = new REPLOutput();
|
|
75
|
-
|
|
76
79
|
out.printBanner();
|
|
77
80
|
out.printUserInput(prompt);
|
|
78
81
|
out.startThinking();
|
|
@@ -82,31 +85,34 @@ program
|
|
|
82
85
|
let hasStarted = false;
|
|
83
86
|
|
|
84
87
|
try {
|
|
85
|
-
const stream = app.
|
|
88
|
+
const stream = app.streamResponse();
|
|
86
89
|
for await (const chunk of stream) {
|
|
87
90
|
out.stopThinking();
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
92
|
+
if (chunk.includes('@@TOOL@@')) {
|
|
93
|
+
const parts = chunk.split('\n');
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (part.startsWith('@@TOOL@@')) {
|
|
96
|
+
try {
|
|
97
|
+
const data = JSON.parse(part.slice(8));
|
|
98
|
+
if (data.action === 'call') {
|
|
99
|
+
if (hasStarted) { out.endAssistant(); hasStarted = false; }
|
|
100
|
+
out.printToolCall(data.name, data.args || '');
|
|
101
|
+
out.startExecuting(data.name);
|
|
102
|
+
} else if (data.action === 'result') {
|
|
103
|
+
out.stopThinking();
|
|
104
|
+
out.printToolResult(data.output || data.error || '', !!data.error);
|
|
105
|
+
out.printToolEnd();
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
} else if (part) {
|
|
109
|
+
if (!hasStarted) { out.beginAssistant(); hasStarted = true; }
|
|
110
|
+
out.writeChunk(part + '\n');
|
|
111
|
+
}
|
|
109
112
|
}
|
|
113
|
+
} else {
|
|
114
|
+
if (!hasStarted) { out.beginAssistant(); hasStarted = true; }
|
|
115
|
+
out.writeChunk(chunk);
|
|
110
116
|
}
|
|
111
117
|
fullResponse += chunk;
|
|
112
118
|
}
|
|
@@ -120,18 +126,17 @@ program
|
|
|
120
126
|
app.session.addMessage('assistant', fullResponse);
|
|
121
127
|
}
|
|
122
128
|
} catch (error: any) {
|
|
123
|
-
console.error(chalk.red(`\n
|
|
129
|
+
console.error(chalk.red(`\n ${error.message}`));
|
|
124
130
|
process.exit(1);
|
|
125
131
|
}
|
|
126
132
|
});
|
|
127
133
|
|
|
128
|
-
// Init command
|
|
134
|
+
// Init command
|
|
129
135
|
program
|
|
130
136
|
.command('init')
|
|
131
|
-
.description('
|
|
137
|
+
.description('Configure API key and model')
|
|
132
138
|
.action(async () => {
|
|
133
139
|
await WelcomeScreen.interactiveSetup();
|
|
134
140
|
});
|
|
135
141
|
|
|
136
|
-
// Parse and run
|
|
137
142
|
program.parse(process.argv);
|
package/src/config/index.ts
CHANGED
package/src/config/types.ts
CHANGED
package/src/llm/index.ts
CHANGED
|
@@ -91,7 +91,6 @@ export class LLMService {
|
|
|
91
91
|
let fullContent = '';
|
|
92
92
|
let detectedToolCalls: ToolCall[] | undefined;
|
|
93
93
|
|
|
94
|
-
// Stream the response, passing tools so the API knows about them
|
|
95
94
|
const stream = this.provider.chatStream(currentMessages, options, hasTools ? toolsArray : undefined);
|
|
96
95
|
|
|
97
96
|
for await (const chunk of stream) {
|
package/src/tui/app.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useStdout } from 'ink';
|
|
4
|
+
import { Header } from './components/Header.js';
|
|
5
|
+
import { ChatList } from './components/ChatList.js';
|
|
6
|
+
import { Thinking } from './components/Thinking.js';
|
|
7
|
+
import { UserInput } from './components/UserInput.js';
|
|
8
|
+
import { useChat } from './hooks/useChat.js';
|
|
9
|
+
import { useCommands } from './hooks/useCommands.js';
|
|
10
|
+
import type { App } from '../app/index.js';
|
|
11
|
+
import type { MessageData } from './components/ChatMessage.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
app: App;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TuiApp({ app }: Props) {
|
|
18
|
+
const config = app.config.get();
|
|
19
|
+
const { messages, isThinking, streaming, streamingToolCalls, sendMessage } = useChat(app);
|
|
20
|
+
const { handleCommand } = useCommands(app);
|
|
21
|
+
const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
|
|
22
|
+
const { exit } = useApp();
|
|
23
|
+
|
|
24
|
+
const onSubmit = useCallback(async (input: string) => {
|
|
25
|
+
// Check for built-in commands first
|
|
26
|
+
const result = handleCommand(input);
|
|
27
|
+
if (result.handled) {
|
|
28
|
+
if (result.output) {
|
|
29
|
+
setSystemMessages(prev => [
|
|
30
|
+
...prev,
|
|
31
|
+
{ role: 'user', content: input },
|
|
32
|
+
{ role: 'assistant', content: result.output! },
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
// Handle clear
|
|
36
|
+
if (input.toLowerCase().trim() === 'clear') {
|
|
37
|
+
setSystemMessages([]);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Send to AI
|
|
43
|
+
await sendMessage(input);
|
|
44
|
+
}, [handleCommand, sendMessage]);
|
|
45
|
+
|
|
46
|
+
const allMessages = [...systemMessages, ...messages];
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Box flexDirection="column" padding={1}>
|
|
50
|
+
<Header provider={config.provider} model={config.model} />
|
|
51
|
+
<ChatList
|
|
52
|
+
messages={allMessages}
|
|
53
|
+
streaming={streaming}
|
|
54
|
+
streamingToolCalls={streamingToolCalls}
|
|
55
|
+
/>
|
|
56
|
+
{isThinking && <Thinking />}
|
|
57
|
+
<UserInput onSubmit={onSubmit} disabled={isThinking} />
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box } from 'ink';
|
|
4
|
+
import { ChatMessage, type MessageData } from './ChatMessage.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
messages: MessageData[];
|
|
8
|
+
streaming?: string;
|
|
9
|
+
streamingToolCalls?: Array<{ name: string; args: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ChatList({ messages, streaming, streamingToolCalls }: Props) {
|
|
13
|
+
return (
|
|
14
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
15
|
+
{messages.map((msg, i) => (
|
|
16
|
+
<ChatMessage key={i} message={msg} />
|
|
17
|
+
))}
|
|
18
|
+
{(streaming || streamingToolCalls) && (
|
|
19
|
+
<ChatMessage
|
|
20
|
+
message={{
|
|
21
|
+
role: 'assistant',
|
|
22
|
+
content: streaming || '',
|
|
23
|
+
toolCalls: streamingToolCalls,
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
)}
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { Markdown } from './Markdown.js';
|
|
5
|
+
import { ToolCall, type ToolCallData } from './ToolCall.js';
|
|
6
|
+
|
|
7
|
+
interface MessageData {
|
|
8
|
+
role: 'user' | 'assistant' | 'tool';
|
|
9
|
+
content: string;
|
|
10
|
+
toolCalls?: ToolCallData[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
message: MessageData;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function UserMessage({ content }: { content: string }) {
|
|
18
|
+
return (
|
|
19
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
20
|
+
<Text bold>You</Text>
|
|
21
|
+
<Box paddingLeft={2}>
|
|
22
|
+
<Text>{content}</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
</Box>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AssistantMessage({ content, toolCalls }: { content: string; toolCalls?: ToolCallData[] }) {
|
|
29
|
+
return (
|
|
30
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
31
|
+
<Text bold color="cyan">AI</Text>
|
|
32
|
+
{toolCalls && toolCalls.map((tc, i) => (
|
|
33
|
+
<ToolCall key={i} tool={tc} />
|
|
34
|
+
))}
|
|
35
|
+
{content && (
|
|
36
|
+
<Box paddingLeft={2} flexDirection="column">
|
|
37
|
+
<Markdown content={content} />
|
|
38
|
+
</Box>
|
|
39
|
+
)}
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ChatMessage({ message }: Props) {
|
|
45
|
+
if (message.role === 'user') {
|
|
46
|
+
return <UserMessage content={message.content} />;
|
|
47
|
+
}
|
|
48
|
+
return <AssistantMessage content={message.content} toolCalls={message.toolCalls} />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type { MessageData };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Header({ provider, model }: Props) {
|
|
11
|
+
return (
|
|
12
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
13
|
+
<Box>
|
|
14
|
+
<Text bold color="cyan"> ⚡ Thatgfsj Code </Text>
|
|
15
|
+
<Text dimColor> v0.6.0</Text>
|
|
16
|
+
<Text dimColor> │ </Text>
|
|
17
|
+
<Text dimColor>{provider} / {model}</Text>
|
|
18
|
+
</Box>
|
|
19
|
+
<Text dimColor>{'─'.repeat(60)}</Text>
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
import TerminalRenderer from 'marked-terminal';
|
|
6
|
+
|
|
7
|
+
// Configure marked to use terminal renderer
|
|
8
|
+
marked.setOptions({
|
|
9
|
+
// @ts-ignore - marked-terminal types are incomplete
|
|
10
|
+
renderer: new TerminalRenderer({
|
|
11
|
+
showSectionPrefix: false,
|
|
12
|
+
tab: 2,
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Markdown({ content }: Props) {
|
|
21
|
+
const rendered = useMemo(() => {
|
|
22
|
+
try {
|
|
23
|
+
const result = marked.parse(content);
|
|
24
|
+
// marked.parse returns string | Promise<string> depending on async option
|
|
25
|
+
if (typeof result === 'string') {
|
|
26
|
+
return result.trimEnd();
|
|
27
|
+
}
|
|
28
|
+
return content;
|
|
29
|
+
} catch {
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
}, [content]);
|
|
33
|
+
|
|
34
|
+
return <Text>{rendered}</Text>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Thinking({ message = 'Thinking' }: Props) {
|
|
11
|
+
return (
|
|
12
|
+
<Box>
|
|
13
|
+
<Text color="cyan">
|
|
14
|
+
<Spinner type="dots" />
|
|
15
|
+
</Text>
|
|
16
|
+
<Text dimColor> {message}</Text>
|
|
17
|
+
</Box>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
|
|
5
|
+
interface ToolCallData {
|
|
6
|
+
name: string;
|
|
7
|
+
args: string;
|
|
8
|
+
result?: string;
|
|
9
|
+
isError?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
tool: ToolCallData;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatArgs(args: string): string {
|
|
17
|
+
try {
|
|
18
|
+
const obj = JSON.parse(args);
|
|
19
|
+
const entries = Object.entries(obj);
|
|
20
|
+
if (entries.length === 0) return '';
|
|
21
|
+
return entries
|
|
22
|
+
.map(([k, v]) => {
|
|
23
|
+
const val = typeof v === 'string' && v.length > 60
|
|
24
|
+
? v.slice(0, 60) + '...'
|
|
25
|
+
: JSON.stringify(v);
|
|
26
|
+
return `${k}=${val}`;
|
|
27
|
+
})
|
|
28
|
+
.join(' ');
|
|
29
|
+
} catch {
|
|
30
|
+
return args.length > 80 ? args.slice(0, 80) + '...' : args;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatResult(output: string, maxLines = 15): { text: string; truncated: boolean } {
|
|
35
|
+
const lines = output.split('\n');
|
|
36
|
+
if (lines.length <= maxLines) {
|
|
37
|
+
return { text: output, truncated: false };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
text: lines.slice(0, maxLines).join('\n'),
|
|
41
|
+
truncated: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ToolCall({ tool }: Props) {
|
|
46
|
+
const argsStr = formatArgs(tool.args);
|
|
47
|
+
const result = tool.result ? formatResult(tool.result) : null;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Box flexDirection="column" marginY={0}>
|
|
51
|
+
<Box>
|
|
52
|
+
<Text color="yellow"> ⚙ </Text>
|
|
53
|
+
<Text bold color="yellow">{tool.name}</Text>
|
|
54
|
+
{argsStr && <Text dimColor> {argsStr}</Text>}
|
|
55
|
+
</Box>
|
|
56
|
+
{result && (
|
|
57
|
+
<Box paddingLeft={4} flexDirection="column">
|
|
58
|
+
{result.text.split('\n').map((line, i) => (
|
|
59
|
+
<Text key={i} dimColor={tool.isError} color={tool.isError ? 'red' : undefined}>
|
|
60
|
+
{tool.isError ? '✖ ' : '│ '}{line}
|
|
61
|
+
</Text>
|
|
62
|
+
))}
|
|
63
|
+
{result.truncated && (
|
|
64
|
+
<Text dimColor>│ ... (truncated)</Text>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
67
|
+
)}
|
|
68
|
+
</Box>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type { ToolCallData };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React, { useState, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
onSubmit: (input: string) => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function UserInput({ onSubmit, disabled }: Props) {
|
|
11
|
+
const [value, setValue] = useState('');
|
|
12
|
+
const [cursorPos, setCursorPos] = useState(0);
|
|
13
|
+
const [history, setHistory] = useState<string[]>([]);
|
|
14
|
+
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
15
|
+
const { exit } = useApp();
|
|
16
|
+
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (disabled) return;
|
|
19
|
+
|
|
20
|
+
if (key.return) {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (trimmed) {
|
|
23
|
+
if (trimmed === 'exit' || trimmed === 'quit') {
|
|
24
|
+
exit();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
setHistory(prev => [...prev, trimmed]);
|
|
28
|
+
setHistoryIdx(-1);
|
|
29
|
+
onSubmit(trimmed);
|
|
30
|
+
setValue('');
|
|
31
|
+
setCursorPos(0);
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (key.upArrow) {
|
|
37
|
+
if (history.length > 0) {
|
|
38
|
+
const newIdx = historyIdx === -1 ? history.length - 1 : Math.max(0, historyIdx - 1);
|
|
39
|
+
setHistoryIdx(newIdx);
|
|
40
|
+
setValue(history[newIdx]);
|
|
41
|
+
setCursorPos(history[newIdx].length);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key.downArrow) {
|
|
47
|
+
if (historyIdx >= 0) {
|
|
48
|
+
const newIdx = historyIdx + 1;
|
|
49
|
+
if (newIdx >= history.length) {
|
|
50
|
+
setHistoryIdx(-1);
|
|
51
|
+
setValue('');
|
|
52
|
+
setCursorPos(0);
|
|
53
|
+
} else {
|
|
54
|
+
setHistoryIdx(newIdx);
|
|
55
|
+
setValue(history[newIdx]);
|
|
56
|
+
setCursorPos(history[newIdx].length);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (key.backspace || key.delete) {
|
|
63
|
+
if (cursorPos > 0) {
|
|
64
|
+
setValue(v => v.slice(0, cursorPos - 1) + v.slice(cursorPos));
|
|
65
|
+
setCursorPos(p => p - 1);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (key.leftArrow) {
|
|
71
|
+
setCursorPos(p => Math.max(0, p - 1));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (key.rightArrow) {
|
|
76
|
+
setCursorPos(p => Math.min(value.length, p + 1));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (key.ctrl && input === 'c') {
|
|
81
|
+
exit();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Regular character input
|
|
86
|
+
if (input && !key.ctrl && !key.meta) {
|
|
87
|
+
setValue(v => v.slice(0, cursorPos) + input + v.slice(cursorPos));
|
|
88
|
+
setCursorPos(p => p + input.length);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const prefix = disabled ? ' ' : '❯ ';
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Box>
|
|
96
|
+
<Text bold color="cyan">{prefix}</Text>
|
|
97
|
+
<Text>{value}</Text>
|
|
98
|
+
{!disabled && <Text color="cyan">█</Text>}
|
|
99
|
+
</Box>
|
|
100
|
+
);
|
|
101
|
+
}
|