thatgfsj-code 0.5.0 → 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.js +1 -1
- 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 +26 -23
- 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/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/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +22 -4
- package/dist/tui/welcome.js.map +1 -1
- package/install.ps1 +171 -192
- package/package.json +9 -1
- package/src/app/index.ts +1 -1
- package/src/cmd/{index.ts → index.tsx} +28 -27
- package/src/config/index.ts +1 -0
- package/src/config/types.ts +1 -0
- 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/welcome.ts +24 -5
- package/tsconfig.json +5 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import type { MessageData } from '../components/ChatMessage.js';
|
|
3
|
+
import type { ToolCallData } from '../components/ToolCall.js';
|
|
4
|
+
import type { App } from '../../app/index.js';
|
|
5
|
+
|
|
6
|
+
interface ChatState {
|
|
7
|
+
messages: MessageData[];
|
|
8
|
+
isThinking: boolean;
|
|
9
|
+
streaming: string;
|
|
10
|
+
streamingToolCalls: ToolCallData[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useChat(app: App) {
|
|
14
|
+
const [state, setState] = useState<ChatState>({
|
|
15
|
+
messages: [],
|
|
16
|
+
isThinking: false,
|
|
17
|
+
streaming: '',
|
|
18
|
+
streamingToolCalls: [],
|
|
19
|
+
});
|
|
20
|
+
const processingRef = useRef(false);
|
|
21
|
+
|
|
22
|
+
const sendMessage = useCallback(async (input: string) => {
|
|
23
|
+
if (processingRef.current) return;
|
|
24
|
+
processingRef.current = true;
|
|
25
|
+
|
|
26
|
+
// Add user message
|
|
27
|
+
setState(prev => ({
|
|
28
|
+
...prev,
|
|
29
|
+
messages: [...prev.messages, { role: 'user', content: input }],
|
|
30
|
+
isThinking: true,
|
|
31
|
+
streaming: '',
|
|
32
|
+
streamingToolCalls: [],
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
app.session.addMessage('user', input);
|
|
36
|
+
|
|
37
|
+
let fullContent = '';
|
|
38
|
+
let currentToolCalls: ToolCallData[] = [];
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const stream = app.streamResponse();
|
|
42
|
+
|
|
43
|
+
for await (const chunk of stream) {
|
|
44
|
+
// Check for tool messages
|
|
45
|
+
if (chunk.includes('@@TOOL@@')) {
|
|
46
|
+
const parts = chunk.split('\n');
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
if (part.startsWith('@@TOOL@@')) {
|
|
49
|
+
try {
|
|
50
|
+
const data = JSON.parse(part.slice(8));
|
|
51
|
+
if (data.action === 'call') {
|
|
52
|
+
// Tool call - show it immediately
|
|
53
|
+
currentToolCalls.push({
|
|
54
|
+
name: data.name,
|
|
55
|
+
args: data.args || '',
|
|
56
|
+
});
|
|
57
|
+
setState(prev => ({
|
|
58
|
+
...prev,
|
|
59
|
+
isThinking: false,
|
|
60
|
+
streamingToolCalls: [...currentToolCalls],
|
|
61
|
+
}));
|
|
62
|
+
} else if (data.action === 'result') {
|
|
63
|
+
// Tool result - update the last tool call
|
|
64
|
+
const lastIdx = currentToolCalls.length - 1;
|
|
65
|
+
if (lastIdx >= 0) {
|
|
66
|
+
currentToolCalls[lastIdx] = {
|
|
67
|
+
...currentToolCalls[lastIdx],
|
|
68
|
+
result: data.output || data.error || '',
|
|
69
|
+
isError: !!data.error,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
setState(prev => ({
|
|
73
|
+
...prev,
|
|
74
|
+
streamingToolCalls: [...currentToolCalls],
|
|
75
|
+
isThinking: true, // Continue thinking after tool
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Not a tool message, treat as text
|
|
80
|
+
fullContent += part;
|
|
81
|
+
setState(prev => ({
|
|
82
|
+
...prev,
|
|
83
|
+
isThinking: false,
|
|
84
|
+
streaming: fullContent,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
} else if (part) {
|
|
88
|
+
fullContent += part;
|
|
89
|
+
setState(prev => ({
|
|
90
|
+
...prev,
|
|
91
|
+
isThinking: false,
|
|
92
|
+
streaming: fullContent,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// Regular text chunk
|
|
98
|
+
fullContent += chunk;
|
|
99
|
+
setState(prev => ({
|
|
100
|
+
...prev,
|
|
101
|
+
isThinking: false,
|
|
102
|
+
streaming: fullContent,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Finalize: add the complete message
|
|
108
|
+
if (fullContent.trim() || currentToolCalls.length > 0) {
|
|
109
|
+
app.session.addMessage('assistant', fullContent);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setState(prev => ({
|
|
113
|
+
...prev,
|
|
114
|
+
messages: [
|
|
115
|
+
...prev.messages,
|
|
116
|
+
...(fullContent.trim() || currentToolCalls.length > 0
|
|
117
|
+
? [{
|
|
118
|
+
role: 'assistant' as const,
|
|
119
|
+
content: fullContent,
|
|
120
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
121
|
+
}]
|
|
122
|
+
: []),
|
|
123
|
+
],
|
|
124
|
+
streaming: '',
|
|
125
|
+
streamingToolCalls: [],
|
|
126
|
+
isThinking: false,
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
app.session.truncate();
|
|
130
|
+
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
setState(prev => ({
|
|
133
|
+
...prev,
|
|
134
|
+
messages: [
|
|
135
|
+
...prev.messages,
|
|
136
|
+
...(fullContent.trim()
|
|
137
|
+
? [{ role: 'assistant' as const, content: fullContent }]
|
|
138
|
+
: []),
|
|
139
|
+
{ role: 'assistant', content: `Error: ${error.message}` },
|
|
140
|
+
],
|
|
141
|
+
streaming: '',
|
|
142
|
+
streamingToolCalls: [],
|
|
143
|
+
isThinking: false,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
processingRef.current = false;
|
|
148
|
+
}, [app]);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...state,
|
|
152
|
+
sendMessage,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { App } from '../../app/index.js';
|
|
3
|
+
|
|
4
|
+
interface CommandResult {
|
|
5
|
+
handled: boolean;
|
|
6
|
+
output?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useCommands(app: App) {
|
|
10
|
+
const handleCommand = useCallback((input: string): CommandResult => {
|
|
11
|
+
const cmd = input.toLowerCase().trim();
|
|
12
|
+
|
|
13
|
+
switch (cmd) {
|
|
14
|
+
case 'help':
|
|
15
|
+
return {
|
|
16
|
+
handled: true,
|
|
17
|
+
output: [
|
|
18
|
+
'Commands:',
|
|
19
|
+
' help Show this help',
|
|
20
|
+
' tools List available tools',
|
|
21
|
+
' model Show current model',
|
|
22
|
+
' clear Clear screen',
|
|
23
|
+
' exit Exit',
|
|
24
|
+
'',
|
|
25
|
+
'Keyboard:',
|
|
26
|
+
' ↑/↓ Browse history',
|
|
27
|
+
' Ctrl+C Exit',
|
|
28
|
+
].join('\n'),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
case 'tools':
|
|
32
|
+
return {
|
|
33
|
+
handled: true,
|
|
34
|
+
output: app.tools.list()
|
|
35
|
+
.map(t => ` ${t.name.padEnd(16)} ${t.description}`)
|
|
36
|
+
.join('\n'),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
case 'model': {
|
|
40
|
+
const c = app.config.get();
|
|
41
|
+
return {
|
|
42
|
+
handled: true,
|
|
43
|
+
output: [
|
|
44
|
+
` Provider: ${c.provider}`,
|
|
45
|
+
` Model: ${c.model}`,
|
|
46
|
+
` Base URL: ${c.baseUrl || 'default'}`,
|
|
47
|
+
` API Key: ${c.apiKey ? '••••' + c.apiKey.slice(-4) : 'not set'}`,
|
|
48
|
+
` Context Length: ${c.contextLength || 50} messages`,
|
|
49
|
+
].join('\n'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case 'clear':
|
|
54
|
+
// Handled separately in the App component
|
|
55
|
+
return { handled: true };
|
|
56
|
+
|
|
57
|
+
default:
|
|
58
|
+
return { handled: false };
|
|
59
|
+
}
|
|
60
|
+
}, [app]);
|
|
61
|
+
|
|
62
|
+
return { handleCommand };
|
|
63
|
+
}
|
package/src/tui/index.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TUI Module - Terminal
|
|
2
|
+
* TUI Module - Ink-based Terminal UI
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export {
|
|
6
|
-
export { REPLInput } from './input.js';
|
|
7
|
-
export { REPLOutput } from './output.js';
|
|
5
|
+
export { TuiApp } from './app.js';
|
|
8
6
|
export { WelcomeScreen } from './welcome.js';
|
package/src/tui/welcome.ts
CHANGED
|
@@ -89,17 +89,17 @@ export class WelcomeScreen {
|
|
|
89
89
|
console.log(chalk.bold(' 3. Choose Model'));
|
|
90
90
|
console.log();
|
|
91
91
|
|
|
92
|
+
let model: string;
|
|
92
93
|
if (isCustomProvider(providerName)) {
|
|
93
94
|
console.log(chalk.gray(' Enter model name for your relay station'));
|
|
94
95
|
console.log(chalk.gray(' e.g. gpt-4o-mini, claude-sonnet-4-20250514'));
|
|
95
96
|
console.log();
|
|
96
|
-
|
|
97
|
+
model = await ask(chalk.cyan(' Model ❯ '));
|
|
97
98
|
if (!model) {
|
|
98
99
|
console.log(chalk.red(' ❌ Model name required'));
|
|
99
100
|
rl.close();
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
|
-
this.saveConfig(providerName, model, apiKey, baseUrl);
|
|
103
103
|
} else {
|
|
104
104
|
const models = getModelsForProvider(providerName);
|
|
105
105
|
models.forEach((m, i) => {
|
|
@@ -107,12 +107,30 @@ export class WelcomeScreen {
|
|
|
107
107
|
console.log(` ${num} ${chalk.white(m.name)} ${chalk.gray(m.desc)}`);
|
|
108
108
|
});
|
|
109
109
|
console.log();
|
|
110
|
-
|
|
111
110
|
const modelChoice = await ask(chalk.cyan(' ❯ '));
|
|
112
111
|
const selectedModel = models[parseInt(modelChoice) - 1] || models[0];
|
|
113
|
-
|
|
112
|
+
model = selectedModel.id;
|
|
114
113
|
}
|
|
115
114
|
|
|
115
|
+
// ── Step 4: Context Length ───────────────────────────
|
|
116
|
+
console.log();
|
|
117
|
+
console.log(chalk.bold(' 4. Context Length'));
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(chalk.gray(' Max messages kept in conversation history'));
|
|
120
|
+
console.log(chalk.gray(' Higher = more context, more tokens used'));
|
|
121
|
+
console.log();
|
|
122
|
+
console.log(chalk.cyan(' 1') + ' 20 ' + chalk.gray('(short,节省 token)'));
|
|
123
|
+
console.log(chalk.cyan(' 2') + ' 50 ' + chalk.gray('(default,推荐)'));
|
|
124
|
+
console.log(chalk.cyan(' 3') + ' 100 ' + chalk.gray('(long,长对话)'));
|
|
125
|
+
console.log(chalk.cyan(' 4') + ' 200 ' + chalk.gray('(very long,超长对话)'));
|
|
126
|
+
console.log();
|
|
127
|
+
const ctxChoice = await ask(chalk.cyan(' ❯ '));
|
|
128
|
+
const ctxMap: Record<string, number> = { '1': 20, '2': 50, '3': 100, '4': 200 };
|
|
129
|
+
const contextLength = ctxMap[ctxChoice] || 50;
|
|
130
|
+
|
|
131
|
+
// ── Save ────────────────────────────────────────────
|
|
132
|
+
this.saveConfig(providerName, model, apiKey, baseUrl, contextLength);
|
|
133
|
+
|
|
116
134
|
// ── Done ────────────────────────────────────────────
|
|
117
135
|
console.log();
|
|
118
136
|
console.log(line);
|
|
@@ -128,7 +146,7 @@ export class WelcomeScreen {
|
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
148
|
|
|
131
|
-
private static saveConfig(provider: ProviderName, model: string, apiKey: string, baseUrl?: string): void {
|
|
149
|
+
private static saveConfig(provider: ProviderName, model: string, apiKey: string, baseUrl?: string, contextLength = 50): void {
|
|
132
150
|
const configDir = join(homedir(), '.thatgfsj');
|
|
133
151
|
const configPath = join(configDir, 'config.json');
|
|
134
152
|
|
|
@@ -142,6 +160,7 @@ export class WelcomeScreen {
|
|
|
142
160
|
apiKey,
|
|
143
161
|
temperature: 0.7,
|
|
144
162
|
maxTokens: 4096,
|
|
163
|
+
contextLength,
|
|
145
164
|
};
|
|
146
165
|
|
|
147
166
|
if (baseUrl) config.baseUrl = baseUrl;
|
package/tsconfig.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"module": "
|
|
5
|
-
"moduleResolution": "
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "node16",
|
|
6
6
|
"outDir": "./dist",
|
|
7
7
|
"rootDir": "./src",
|
|
8
8
|
"strict": true,
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"resolveJsonModule": true,
|
|
13
13
|
"declaration": true,
|
|
14
14
|
"declarationMap": true,
|
|
15
|
-
"sourceMap": true
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"jsx": "react-jsx",
|
|
17
|
+
"jsxImportSource": "react"
|
|
16
18
|
},
|
|
17
19
|
"include": ["src/**/*"],
|
|
18
20
|
"exclude": ["node_modules", "dist"]
|