thatgfsj-code 0.9.3 → 0.9.5
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/dist/cmd/index.js +1 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +49 -20
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/Header.js +1 -1
- package/dist/tui/components/ModelSelector.d.ts +2 -2
- package/dist/tui/components/ModelSelector.d.ts.map +1 -1
- package/dist/tui/components/ModelSelector.js +53 -25
- package/dist/tui/components/ModelSelector.js.map +1 -1
- package/dist/tui/components/StatusBar.js +1 -1
- package/dist/tui/components/StatusBar.js.map +1 -1
- package/dist/tui/components/UserInput.d.ts.map +1 -1
- package/dist/tui/components/UserInput.js +28 -2
- package/dist/tui/components/UserInput.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts +1 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +109 -96
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts +4 -0
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +58 -30
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/package.json +1 -1
- package/src/cmd/index.tsx +1 -1
- package/src/tui/app.tsx +57 -21
- package/src/tui/components/Header.tsx +1 -1
- package/src/tui/components/ModelSelector.tsx +65 -39
- package/src/tui/components/StatusBar.tsx +2 -2
- package/src/tui/components/UserInput.tsx +53 -6
- package/src/tui/hooks/useChat.ts +109 -95
- package/src/tui/hooks/useCommands.ts +61 -30
|
@@ -1,56 +1,82 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
|
-
import React, { useState } from 'react';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import SelectInput from 'ink-select-input';
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
5
8
|
|
|
6
9
|
interface Props {
|
|
7
10
|
currentModel: string;
|
|
8
11
|
onSelect: (model: string) => void;
|
|
9
|
-
|
|
12
|
+
onAddNew: () => void;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<Box flexDirection="column">
|
|
35
|
-
<Text color="#06B6D4" bold>Enter model name:</Text>
|
|
36
|
-
<Box>
|
|
37
|
-
<Text color="#06B6D4">❯ </Text>
|
|
38
|
-
<Text>{customValue}</Text>
|
|
39
|
-
<Text color="#06B6D4">█</Text>
|
|
40
|
-
</Box>
|
|
41
|
-
</Box>
|
|
42
|
-
);
|
|
15
|
+
interface SavedModel {
|
|
16
|
+
label: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadSavedModels(): SavedModel[] {
|
|
21
|
+
const configPath = join(homedir(), '.thatgfsj', 'config.json');
|
|
22
|
+
const models: SavedModel[] = [];
|
|
23
|
+
const seen = new Set<string>();
|
|
24
|
+
|
|
25
|
+
// Load from history if exists
|
|
26
|
+
const historyPath = join(homedir(), '.thatgfsj', 'models.json');
|
|
27
|
+
if (existsSync(historyPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const history = JSON.parse(readFileSync(historyPath, 'utf-8'));
|
|
30
|
+
for (const m of history) {
|
|
31
|
+
if (!seen.has(m)) {
|
|
32
|
+
seen.add(m);
|
|
33
|
+
models.push({ label: m, value: m });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
43
37
|
}
|
|
44
38
|
|
|
39
|
+
// Always include current model
|
|
40
|
+
if (existsSync(configPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
43
|
+
if (config.model && !seen.has(config.model)) {
|
|
44
|
+
seen.add(config.model);
|
|
45
|
+
models.push({ label: `${config.model} (当前)`, value: config.model });
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add some common defaults if list is empty
|
|
51
|
+
if (models.length === 0) {
|
|
52
|
+
const defaults = ['deepseek-chat', 'gpt-4o', 'mimo-v2.5-pro'];
|
|
53
|
+
for (const m of defaults) {
|
|
54
|
+
models.push({ label: m, value: m });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return models;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ModelSelector({ currentModel, onSelect, onAddNew }: Props) {
|
|
62
|
+
const [items, setItems] = useState<SavedModel[]>([]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const saved = loadSavedModels();
|
|
66
|
+
// Add "add new" option at the end
|
|
67
|
+
saved.push({ label: '+ 添加新模型', value: '__add_new__' });
|
|
68
|
+
setItems(saved);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
45
71
|
return (
|
|
46
|
-
<Box flexDirection="column">
|
|
47
|
-
<Text color="#06B6D4" bold
|
|
48
|
-
<Text dimColor
|
|
72
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
73
|
+
<Text color="#06B6D4" bold>当前模型: {currentModel}</Text>
|
|
74
|
+
<Text dimColor>选择模型 (↑↓ 回车):</Text>
|
|
49
75
|
<SelectInput
|
|
50
|
-
items={
|
|
76
|
+
items={items}
|
|
51
77
|
onSelect={(item) => {
|
|
52
|
-
if (item.value === '
|
|
53
|
-
|
|
78
|
+
if (item.value === '__add_new__') {
|
|
79
|
+
onAddNew();
|
|
54
80
|
} else {
|
|
55
81
|
onSelect(item.value);
|
|
56
82
|
}
|
|
@@ -17,11 +17,11 @@ export const StatusBar = React.memo(function StatusBar({ messageCount, skills }:
|
|
|
17
17
|
<Box justifyContent="space-between" width="100%">
|
|
18
18
|
<Box>
|
|
19
19
|
<Text backgroundColor="#06B6D4" color="#0F172A" bold> ⚡ THATGFSJ CODE </Text>
|
|
20
|
-
<Text dimColor> │ {messageCount}
|
|
20
|
+
<Text dimColor> │ {messageCount} 条消息</Text>
|
|
21
21
|
</Box>
|
|
22
22
|
<Box>
|
|
23
23
|
{skills.length > 0 && (
|
|
24
|
-
<Text dimColor>
|
|
24
|
+
<Text dimColor> 技能: {activeSkills}{moreSkills} </Text>
|
|
25
25
|
)}
|
|
26
26
|
</Box>
|
|
27
27
|
</Box>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
|
-
import React, { useState } from 'react';
|
|
2
|
+
import React, { useState, useCallback } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useApp, useFocus } from 'ink';
|
|
4
|
+
import { COMMAND_LIST } from '../hooks/useCommands.js';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
onSubmit: (input: string) => void;
|
|
@@ -11,12 +12,36 @@ export function UserInput({ onSubmit, disabled }: Props) {
|
|
|
11
12
|
const [value, setValue] = useState('');
|
|
12
13
|
const [history, setHistory] = useState<string[]>([]);
|
|
13
14
|
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
15
|
+
const [selectedCmd, setSelectedCmd] = useState(0);
|
|
14
16
|
const { exit } = useApp();
|
|
15
|
-
|
|
17
|
+
useFocus({ autoFocus: true });
|
|
18
|
+
|
|
19
|
+
const showCommands = value.startsWith('/') && value.length > 0 && !value.includes(' ');
|
|
20
|
+
const filteredCommands = showCommands
|
|
21
|
+
? COMMAND_LIST.filter(c => c.name.startsWith(value))
|
|
22
|
+
: [];
|
|
16
23
|
|
|
17
24
|
useInput((input, key) => {
|
|
18
25
|
if (disabled) return;
|
|
19
26
|
|
|
27
|
+
// Command selector navigation
|
|
28
|
+
if (showCommands && filteredCommands.length > 0) {
|
|
29
|
+
if (key.upArrow) {
|
|
30
|
+
setSelectedCmd(prev => Math.max(0, prev - 1));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (key.downArrow) {
|
|
34
|
+
setSelectedCmd(prev => Math.min(filteredCommands.length - 1, prev + 1));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (key.tab || (key.return && filteredCommands.length === 1)) {
|
|
38
|
+
const selected = filteredCommands[selectedCmd] || filteredCommands[0];
|
|
39
|
+
setValue(selected.name + ' ');
|
|
40
|
+
setSelectedCmd(0);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
20
45
|
if (key.return) {
|
|
21
46
|
const trimmed = value.trim();
|
|
22
47
|
if (trimmed) {
|
|
@@ -28,6 +53,7 @@ export function UserInput({ onSubmit, disabled }: Props) {
|
|
|
28
53
|
setHistoryIdx(-1);
|
|
29
54
|
onSubmit(trimmed);
|
|
30
55
|
setValue('');
|
|
56
|
+
setSelectedCmd(0);
|
|
31
57
|
}
|
|
32
58
|
return;
|
|
33
59
|
}
|
|
@@ -53,6 +79,7 @@ export function UserInput({ onSubmit, disabled }: Props) {
|
|
|
53
79
|
|
|
54
80
|
if (key.backspace || key.delete) {
|
|
55
81
|
setValue(v => v.slice(0, -1));
|
|
82
|
+
setSelectedCmd(0);
|
|
56
83
|
return;
|
|
57
84
|
}
|
|
58
85
|
|
|
@@ -63,14 +90,34 @@ export function UserInput({ onSubmit, disabled }: Props) {
|
|
|
63
90
|
|
|
64
91
|
if (input && !key.ctrl && !key.meta) {
|
|
65
92
|
setValue(v => v + input);
|
|
93
|
+
setSelectedCmd(0);
|
|
66
94
|
}
|
|
67
95
|
});
|
|
68
96
|
|
|
69
97
|
return (
|
|
70
|
-
<Box
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
<Box flexDirection="column">
|
|
99
|
+
{/* Command selector */}
|
|
100
|
+
{showCommands && filteredCommands.length > 0 && (
|
|
101
|
+
<Box flexDirection="column" paddingLeft={2} marginBottom={0}>
|
|
102
|
+
{filteredCommands.map((cmd, i) => (
|
|
103
|
+
<Box key={cmd.name}>
|
|
104
|
+
<Text color={i === selectedCmd ? '#06B6D4' : '#64748B'}>
|
|
105
|
+
{i === selectedCmd ? '▸ ' : ' '}
|
|
106
|
+
</Text>
|
|
107
|
+
<Text color={i === selectedCmd ? '#06B6D4' : '#94A3B8'} bold={i === selectedCmd}>
|
|
108
|
+
{cmd.name}
|
|
109
|
+
</Text>
|
|
110
|
+
<Text color="#64748B"> {cmd.desc}</Text>
|
|
111
|
+
</Box>
|
|
112
|
+
))}
|
|
113
|
+
</Box>
|
|
114
|
+
)}
|
|
115
|
+
{/* Input line */}
|
|
116
|
+
<Box paddingY={0}>
|
|
117
|
+
<Text color="#06B6D4" bold>{disabled ? ' ' : '❯ '}</Text>
|
|
118
|
+
<Text>{value}</Text>
|
|
119
|
+
{!disabled && <Text color="#06B6D4">█</Text>}
|
|
120
|
+
</Box>
|
|
74
121
|
</Box>
|
|
75
122
|
);
|
|
76
123
|
}
|
package/src/tui/hooks/useChat.ts
CHANGED
|
@@ -8,6 +8,7 @@ interface ChatState {
|
|
|
8
8
|
isThinking: boolean;
|
|
9
9
|
streaming: string;
|
|
10
10
|
streamingToolCalls: ToolCallData[];
|
|
11
|
+
queuedMessage: string | null;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function useChat(app: App) {
|
|
@@ -16,19 +17,19 @@ export function useChat(app: App) {
|
|
|
16
17
|
isThinking: false,
|
|
17
18
|
streaming: '',
|
|
18
19
|
streamingToolCalls: [],
|
|
20
|
+
queuedMessage: null,
|
|
19
21
|
});
|
|
20
22
|
const processingRef = useRef(false);
|
|
23
|
+
const queuedRef = useRef<string | null>(null);
|
|
21
24
|
|
|
22
|
-
const
|
|
23
|
-
if (processingRef.current) return;
|
|
24
|
-
processingRef.current = true;
|
|
25
|
-
|
|
25
|
+
const processStream = async (input: string) => {
|
|
26
26
|
setState(prev => ({
|
|
27
27
|
...prev,
|
|
28
28
|
messages: [...prev.messages, { role: 'user', content: input }],
|
|
29
29
|
isThinking: true,
|
|
30
30
|
streaming: '',
|
|
31
31
|
streamingToolCalls: [],
|
|
32
|
+
queuedMessage: null,
|
|
32
33
|
}));
|
|
33
34
|
|
|
34
35
|
app.session.addMessage('user', input);
|
|
@@ -37,112 +38,125 @@ export function useChat(app: App) {
|
|
|
37
38
|
let fullContent = '';
|
|
38
39
|
let currentToolCalls: ToolCallData[] = [];
|
|
39
40
|
let lastUpdateTime = 0;
|
|
40
|
-
const THROTTLE_MS = 50;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
currentToolCalls[lastIdx]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
setState(prev => ({
|
|
68
|
-
...prev,
|
|
69
|
-
streamingToolCalls: [...currentToolCalls],
|
|
70
|
-
isThinking: true,
|
|
71
|
-
}));
|
|
41
|
+
const THROTTLE_MS = 50;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
for await (const chunk of stream) {
|
|
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
|
+
currentToolCalls.push({ name: data.name, args: data.args || '' });
|
|
53
|
+
setState(prev => ({
|
|
54
|
+
...prev,
|
|
55
|
+
isThinking: false,
|
|
56
|
+
streamingToolCalls: [...currentToolCalls],
|
|
57
|
+
}));
|
|
58
|
+
} else if (data.action === 'result') {
|
|
59
|
+
const lastIdx = currentToolCalls.length - 1;
|
|
60
|
+
if (lastIdx >= 0) {
|
|
61
|
+
currentToolCalls[lastIdx] = {
|
|
62
|
+
...currentToolCalls[lastIdx],
|
|
63
|
+
result: data.output || data.error || '',
|
|
64
|
+
isError: !!data.error,
|
|
65
|
+
};
|
|
72
66
|
}
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
setState(prev => ({
|
|
68
|
+
...prev,
|
|
69
|
+
streamingToolCalls: [...currentToolCalls],
|
|
70
|
+
isThinking: true,
|
|
71
|
+
}));
|
|
75
72
|
}
|
|
76
|
-
}
|
|
73
|
+
} catch {
|
|
77
74
|
fullContent += part;
|
|
78
75
|
}
|
|
76
|
+
} else if (part) {
|
|
77
|
+
fullContent += part;
|
|
79
78
|
}
|
|
80
|
-
} else {
|
|
81
|
-
fullContent += chunk;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Throttle UI updates
|
|
85
|
-
const now = Date.now();
|
|
86
|
-
if (now - lastUpdateTime >= THROTTLE_MS) {
|
|
87
|
-
lastUpdateTime = now;
|
|
88
|
-
setState(prev => ({ ...prev, isThinking: false, streaming: fullContent }));
|
|
89
79
|
}
|
|
80
|
+
} else {
|
|
81
|
+
fullContent += chunk;
|
|
90
82
|
}
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
if (now - lastUpdateTime >= THROTTLE_MS) {
|
|
86
|
+
lastUpdateTime = now;
|
|
87
|
+
setState(prev => ({ ...prev, isThinking: false, streaming: fullContent }));
|
|
95
88
|
}
|
|
89
|
+
}
|
|
96
90
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
...prev.messages,
|
|
101
|
-
...(fullContent.trim() || currentToolCalls.length > 0
|
|
102
|
-
? [{
|
|
103
|
-
role: 'assistant' as const,
|
|
104
|
-
content: fullContent,
|
|
105
|
-
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
106
|
-
}]
|
|
107
|
-
: []),
|
|
108
|
-
],
|
|
109
|
-
streaming: '',
|
|
110
|
-
streamingToolCalls: [],
|
|
111
|
-
isThinking: false,
|
|
112
|
-
}));
|
|
113
|
-
|
|
114
|
-
app.session.truncate();
|
|
115
|
-
} catch (error: any) {
|
|
116
|
-
const msg = error.message || String(error);
|
|
117
|
-
let errorMsg = `Error: ${msg}`;
|
|
118
|
-
|
|
119
|
-
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
120
|
-
errorMsg = `❌ API key invalid. Run \`gfcode init\` to reconfigure.`;
|
|
121
|
-
} else if (msg.includes('429') || msg.includes('rate limit') || msg.includes('quota')) {
|
|
122
|
-
errorMsg = `❌ Rate limit exceeded. Wait or run \`gfcode init\` to switch provider.`;
|
|
123
|
-
} else if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
124
|
-
errorMsg = `❌ Cannot connect. Check network or run \`gfcode init\`.`;
|
|
125
|
-
}
|
|
91
|
+
if (fullContent.trim() || currentToolCalls.length > 0) {
|
|
92
|
+
app.session.addMessage('assistant', fullContent);
|
|
93
|
+
}
|
|
126
94
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
95
|
+
setState(prev => ({
|
|
96
|
+
...prev,
|
|
97
|
+
messages: [
|
|
98
|
+
...prev.messages,
|
|
99
|
+
...(fullContent.trim() || currentToolCalls.length > 0
|
|
100
|
+
? [{
|
|
101
|
+
role: 'assistant' as const,
|
|
102
|
+
content: fullContent,
|
|
103
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
104
|
+
}]
|
|
105
|
+
: []),
|
|
106
|
+
],
|
|
107
|
+
streaming: '',
|
|
108
|
+
streamingToolCalls: [],
|
|
109
|
+
isThinking: false,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
app.session.truncate();
|
|
113
|
+
} catch (error: any) {
|
|
114
|
+
const msg = error.message || String(error);
|
|
115
|
+
let errorMsg = `Error: ${msg}`;
|
|
116
|
+
|
|
117
|
+
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
118
|
+
errorMsg = `❌ API key invalid. Run \`gfcode init\` to reconfigure.`;
|
|
119
|
+
} else if (msg.includes('429') || msg.includes('rate limit') || msg.includes('quota')) {
|
|
120
|
+
errorMsg = `❌ Rate limit exceeded. Wait or run \`gfcode init\` to switch provider.`;
|
|
121
|
+
} else if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
122
|
+
errorMsg = `❌ Cannot connect. Check network or run \`gfcode init\`.`;
|
|
140
123
|
}
|
|
141
124
|
|
|
125
|
+
setState(prev => ({
|
|
126
|
+
...prev,
|
|
127
|
+
messages: [
|
|
128
|
+
...prev.messages,
|
|
129
|
+
...(fullContent.trim()
|
|
130
|
+
? [{ role: 'assistant' as const, content: fullContent }]
|
|
131
|
+
: []),
|
|
132
|
+
{ role: 'assistant', content: errorMsg },
|
|
133
|
+
],
|
|
134
|
+
streaming: '',
|
|
135
|
+
streamingToolCalls: [],
|
|
136
|
+
isThinking: false,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if there's a queued message
|
|
141
|
+
if (queuedRef.current) {
|
|
142
|
+
const next = queuedRef.current;
|
|
143
|
+
queuedRef.current = null;
|
|
144
|
+
await processStream(next);
|
|
145
|
+
} else {
|
|
142
146
|
processingRef.current = false;
|
|
143
|
-
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
144
149
|
|
|
145
|
-
|
|
150
|
+
const sendMessage = useCallback((input: string) => {
|
|
151
|
+
// If currently processing, queue the message
|
|
152
|
+
if (processingRef.current) {
|
|
153
|
+
queuedRef.current = input;
|
|
154
|
+
setState(prev => ({ ...prev, queuedMessage: input }));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
processingRef.current = true;
|
|
159
|
+
processStream(input);
|
|
146
160
|
}, [app]);
|
|
147
161
|
|
|
148
162
|
return { ...state, sendMessage };
|
|
@@ -7,14 +7,42 @@ interface CommandResult {
|
|
|
7
7
|
action?: 'clear' | 'reinit';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// 中文命令别名映射
|
|
11
|
+
const CMD_ALIASES: Record<string, string> = {
|
|
12
|
+
'/模型': '/model',
|
|
13
|
+
'/新建': '/new',
|
|
14
|
+
'/清空': '/new',
|
|
15
|
+
'/压缩': '/compact',
|
|
16
|
+
'/技能': '/skills',
|
|
17
|
+
'/技能管理': '/skills',
|
|
18
|
+
'/mcp': '/mcp',
|
|
19
|
+
'/帮助': '/help',
|
|
20
|
+
'/服务商': '/provider',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const COMMAND_LIST = [
|
|
24
|
+
{ name: '/模型', desc: '切换模型' },
|
|
25
|
+
{ name: '/服务商', desc: '更换服务商' },
|
|
26
|
+
{ name: '/新建', desc: '新建会话' },
|
|
27
|
+
{ name: '/压缩', desc: '压缩上下文' },
|
|
28
|
+
{ name: '/技能', desc: '管理技能' },
|
|
29
|
+
{ name: '/mcp', desc: 'MCP 设置' },
|
|
30
|
+
{ name: '/帮助', desc: '查看帮助' },
|
|
31
|
+
];
|
|
32
|
+
|
|
10
33
|
export function useCommands(app: App) {
|
|
11
34
|
|
|
12
35
|
const handleCommand = useCallback((input: string): CommandResult => {
|
|
13
36
|
const cmd = input.trim();
|
|
14
37
|
const parts = cmd.split(/\s+/);
|
|
15
|
-
|
|
38
|
+
let name = parts[0].toLowerCase();
|
|
16
39
|
const arg = parts.slice(1).join(' ');
|
|
17
40
|
|
|
41
|
+
// 中文别名映射
|
|
42
|
+
if (CMD_ALIASES[name]) {
|
|
43
|
+
name = CMD_ALIASES[name];
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
// ── /model [name] ───────────────────────────────────
|
|
19
47
|
if (name === '/model') {
|
|
20
48
|
if (!arg) {
|
|
@@ -22,20 +50,19 @@ export function useCommands(app: App) {
|
|
|
22
50
|
return {
|
|
23
51
|
handled: true,
|
|
24
52
|
output: [
|
|
25
|
-
|
|
53
|
+
`当前: ${c.provider} / ${c.model}`,
|
|
26
54
|
'',
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
' /model qwen3-235b-a22b',
|
|
55
|
+
'用法: /模型 <名称>',
|
|
56
|
+
' /模型 deepseek-chat',
|
|
57
|
+
' /模型 gpt-4o',
|
|
58
|
+
' /模型 claude-sonnet-4-20250514',
|
|
32
59
|
'',
|
|
33
|
-
'
|
|
60
|
+
'或: /服务商 更换服务商',
|
|
34
61
|
].join('\n'),
|
|
35
62
|
};
|
|
36
63
|
}
|
|
37
64
|
app.config.save({ model: arg });
|
|
38
|
-
return { handled: true, output:
|
|
65
|
+
return { handled: true, output: `模型 → ${arg}` };
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
// ── /provider ───────────────────────────────────────
|
|
@@ -44,9 +71,9 @@ export function useCommands(app: App) {
|
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
// ── /new, /clear ────────────────────────────────────
|
|
47
|
-
if (name === '/new'
|
|
74
|
+
if (name === '/new') {
|
|
48
75
|
app.session.clear();
|
|
49
|
-
return { handled: true, output: '
|
|
76
|
+
return { handled: true, output: '新会话已创建。', action: 'clear' };
|
|
50
77
|
}
|
|
51
78
|
|
|
52
79
|
// ── /compact ────────────────────────────────────────
|
|
@@ -54,31 +81,30 @@ export function useCommands(app: App) {
|
|
|
54
81
|
const before = app.session.getMessageCount();
|
|
55
82
|
app.session.truncate();
|
|
56
83
|
const after = app.session.getMessageCount();
|
|
57
|
-
return { handled: true, output:
|
|
84
|
+
return { handled: true, output: `上下文已压缩: ${before} → ${after} 条消息` };
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
// ── /skills [id] ────────────────────────────────────
|
|
61
88
|
if (name === '/skills') {
|
|
62
89
|
if (arg) {
|
|
63
|
-
// Toggle specific skill
|
|
64
90
|
if (app.skills.isActive(arg)) {
|
|
65
91
|
app.skills.deactivate(arg);
|
|
66
|
-
return { handled: true, output: `✗ ${arg}` };
|
|
92
|
+
return { handled: true, output: `✗ 已关闭: ${arg}` };
|
|
67
93
|
} else if (app.skills.activate(arg)) {
|
|
68
|
-
return { handled: true, output: `✓ ${arg}` };
|
|
94
|
+
return { handled: true, output: `✓ 已开启: ${arg}` };
|
|
69
95
|
}
|
|
70
|
-
return { handled: true, output:
|
|
96
|
+
return { handled: true, output: `未知技能: ${arg}` };
|
|
71
97
|
}
|
|
72
98
|
|
|
73
99
|
const all = app.skills.list();
|
|
74
100
|
const lines = [
|
|
75
|
-
'
|
|
101
|
+
'技能列表:',
|
|
76
102
|
...all.map(s => {
|
|
77
103
|
const mark = app.skills.isActive(s.id) ? '✓' : '·';
|
|
78
|
-
return ` ${mark} ${s.id}`;
|
|
104
|
+
return ` ${mark} ${s.id} ${s.description}`;
|
|
79
105
|
}),
|
|
80
106
|
'',
|
|
81
|
-
'
|
|
107
|
+
'用法: /技能 <id> 切换',
|
|
82
108
|
];
|
|
83
109
|
return { handled: true, output: lines.join('\n') };
|
|
84
110
|
}
|
|
@@ -88,11 +114,11 @@ export function useCommands(app: App) {
|
|
|
88
114
|
return {
|
|
89
115
|
handled: true,
|
|
90
116
|
output: [
|
|
91
|
-
'MCP
|
|
117
|
+
'MCP 配置: ~/.thatgfsj/mcp.json',
|
|
92
118
|
'',
|
|
93
119
|
' {',
|
|
94
120
|
' "servers": {',
|
|
95
|
-
' "
|
|
121
|
+
' "名称": { "command": "npx", "args": ["-y", "server"] }',
|
|
96
122
|
' }',
|
|
97
123
|
' }',
|
|
98
124
|
].join('\n'),
|
|
@@ -100,18 +126,23 @@ export function useCommands(app: App) {
|
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
// ── /help ───────────────────────────────────────────
|
|
103
|
-
if (name === '/help'
|
|
129
|
+
if (name === '/help') {
|
|
104
130
|
return {
|
|
105
131
|
handled: true,
|
|
106
132
|
output: [
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'/
|
|
114
|
-
'
|
|
133
|
+
'命令列表:',
|
|
134
|
+
' /模型 <名称> 切换模型',
|
|
135
|
+
' /服务商 更换服务商',
|
|
136
|
+
' /新建 新建会话',
|
|
137
|
+
' /压缩 压缩上下文',
|
|
138
|
+
' /技能 [id] 管理技能',
|
|
139
|
+
' /mcp MCP 设置',
|
|
140
|
+
' /帮助 查看帮助',
|
|
141
|
+
' exit 退出',
|
|
142
|
+
'',
|
|
143
|
+
'快捷键:',
|
|
144
|
+
' ↑/↓ 历史记录',
|
|
145
|
+
' Ctrl+C 退出',
|
|
115
146
|
].join('\n'),
|
|
116
147
|
};
|
|
117
148
|
}
|