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.
@@ -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
- onCancel: () => void;
12
+ onAddNew: () => void;
10
13
  }
11
14
 
12
- const COMMON_MODELS = [
13
- { label: 'deepseek-chat', value: 'deepseek-chat' },
14
- { label: 'deepseek-reasoner', value: 'deepseek-reasoner' },
15
- { label: 'gpt-4o', value: 'gpt-4o' },
16
- { label: 'gpt-4o-mini', value: 'gpt-4o-mini' },
17
- { label: 'claude-sonnet-4-20250514', value: 'claude-sonnet-4-20250514' },
18
- { label: 'claude-3-5-haiku-20241022', value: 'claude-3-5-haiku-20241022' },
19
- { label: 'gemini-2.0-flash', value: 'gemini-2.0-flash' },
20
- { label: 'gemini-2.5-pro', value: 'gemini-2.5-pro' },
21
- { label: 'qwen3-235b-a22b', value: 'qwen3-235b-a22b' },
22
- { label: 'glm-4-flash', value: 'glm-4-flash' },
23
- { label: 'kimi-k2.6', value: 'kimi-k2.6' },
24
- { label: 'mimo-v2.5-pro', value: 'mimo-v2.5-pro' },
25
- { label: '─ type custom name below ─', value: '__custom__' },
26
- ];
27
-
28
- export function ModelSelector({ currentModel, onSelect, onCancel }: Props) {
29
- const [customMode, setCustomMode] = useState(false);
30
- const [customValue, setCustomValue] = useState('');
31
-
32
- if (customMode) {
33
- return (
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>Current: {currentModel}</Text>
48
- <Text dimColor>Select model (↑↓ Enter):</Text>
72
+ <Box flexDirection="column" paddingLeft={1}>
73
+ <Text color="#06B6D4" bold>当前模型: {currentModel}</Text>
74
+ <Text dimColor>选择模型 (↑↓ 回车):</Text>
49
75
  <SelectInput
50
- items={COMMON_MODELS}
76
+ items={items}
51
77
  onSelect={(item) => {
52
- if (item.value === '__custom__') {
53
- setCustomMode(true);
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} messages</Text>
20
+ <Text dimColor> │ {messageCount} 条消息</Text>
21
21
  </Box>
22
22
  <Box>
23
23
  {skills.length > 0 && (
24
- <Text dimColor> skills: {activeSkills}{moreSkills} </Text>
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
- const { isFocused } = useFocus({ autoFocus: true });
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 paddingY={0}>
71
- <Text color="#06B6D4" bold>{disabled ? ' ' : '❯ '}</Text>
72
- <Text>{value}</Text>
73
- {!disabled && <Text color="#06B6D4">█</Text>}
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
  }
@@ -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 sendMessage = useCallback((input: string) => {
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; // Update UI at most every 50ms
41
-
42
- const processStream = async () => {
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
- };
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
- } catch {
74
- fullContent += part;
67
+ setState(prev => ({
68
+ ...prev,
69
+ streamingToolCalls: [...currentToolCalls],
70
+ isThinking: true,
71
+ }));
75
72
  }
76
- } else if (part) {
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
- // Final update
93
- if (fullContent.trim() || currentToolCalls.length > 0) {
94
- app.session.addMessage('assistant', fullContent);
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
- setState(prev => ({
98
- ...prev,
99
- messages: [
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
- setState(prev => ({
128
- ...prev,
129
- messages: [
130
- ...prev.messages,
131
- ...(fullContent.trim()
132
- ? [{ role: 'assistant' as const, content: fullContent }]
133
- : []),
134
- { role: 'assistant', content: errorMsg },
135
- ],
136
- streaming: '',
137
- streamingToolCalls: [],
138
- isThinking: false,
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
- processStream();
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
- const name = parts[0].toLowerCase();
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
- `Current: ${c.provider} / ${c.model}`,
53
+ `当前: ${c.provider} / ${c.model}`,
26
54
  '',
27
- 'Usage: /model <name>',
28
- ' /model deepseek-chat',
29
- ' /model gpt-4o',
30
- ' /model claude-sonnet-4-20250514',
31
- ' /model qwen3-235b-a22b',
55
+ '用法: /模型 <名称>',
56
+ ' /模型 deepseek-chat',
57
+ ' /模型 gpt-4o',
58
+ ' /模型 claude-sonnet-4-20250514',
32
59
  '',
33
- 'Or: /provider to change provider',
60
+ '或: /服务商 更换服务商',
34
61
  ].join('\n'),
35
62
  };
36
63
  }
37
64
  app.config.save({ model: arg });
38
- return { handled: true, output: `Model → ${arg}` };
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' || name === '/clear') {
74
+ if (name === '/new') {
48
75
  app.session.clear();
49
- return { handled: true, output: 'New session.', action: 'clear' };
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: `Compacted: ${before} → ${after} messages` };
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: `Unknown skill: ${arg}` };
96
+ return { handled: true, output: `未知技能: ${arg}` };
71
97
  }
72
98
 
73
99
  const all = app.skills.list();
74
100
  const lines = [
75
- 'Skills:',
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
- 'Usage: /skills <id> to toggle',
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: ~/.thatgfsj/mcp.json',
117
+ 'MCP 配置: ~/.thatgfsj/mcp.json',
92
118
  '',
93
119
  ' {',
94
120
  ' "servers": {',
95
- ' "name": { "command": "npx", "args": ["-y", "server"] }',
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' || name === 'help') {
129
+ if (name === '/help') {
104
130
  return {
105
131
  handled: true,
106
132
  output: [
107
- '/model <name> Switch model',
108
- '/provider Change provider',
109
- '/new New session',
110
- '/compact Compress context',
111
- '/skills [id] List/toggle skills',
112
- '/mcp MCP settings',
113
- '/help This help',
114
- 'exit Quit',
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
  }