@code4bug/jarvis-agent 1.0.2 → 1.0.4
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/LICENSE +1 -1
- package/dist/components/MessageItem.js +9 -1
- package/dist/components/MultilineInput.d.ts +7 -1
- package/dist/components/MultilineInput.js +148 -4
- package/dist/config/loader.d.ts +2 -0
- package/dist/core/QueryEngine.d.ts +3 -3
- package/dist/core/QueryEngine.js +13 -11
- package/dist/core/WorkerBridge.d.ts +9 -0
- package/dist/core/WorkerBridge.js +109 -0
- package/dist/core/query.d.ts +8 -1
- package/dist/core/query.js +276 -54
- package/dist/core/queryWorker.d.ts +44 -0
- package/dist/core/queryWorker.js +66 -0
- package/dist/core/safeguard.js +1 -1
- package/dist/hooks/useDoubleCtrlCExit.d.ts +5 -0
- package/dist/hooks/useDoubleCtrlCExit.js +34 -0
- package/dist/hooks/useInputHistory.js +35 -3
- package/dist/hooks/useSlashMenu.d.ts +36 -0
- package/dist/hooks/useSlashMenu.js +216 -0
- package/dist/hooks/useStreamThrottle.d.ts +20 -0
- package/dist/hooks/useStreamThrottle.js +120 -0
- package/dist/hooks/useTerminalWidth.d.ts +2 -0
- package/dist/hooks/useTerminalWidth.js +13 -0
- package/dist/hooks/useTokenDisplay.d.ts +13 -0
- package/dist/hooks/useTokenDisplay.js +45 -0
- package/dist/screens/repl.js +153 -625
- package/dist/screens/slashCommands.d.ts +7 -0
- package/dist/screens/slashCommands.js +134 -0
- package/dist/services/api/llm.d.ts +2 -0
- package/dist/services/api/llm.js +65 -11
- package/dist/skills/index.js +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +3 -2
- package/dist/tools/runCommand.js +37 -6
- package/dist/tools/semanticSearch.d.ts +9 -0
- package/dist/tools/semanticSearch.js +159 -0
- package/dist/tools/writeFile.js +124 -24
- package/dist/types/index.d.ts +10 -1
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Message } from '../types/index.js';
|
|
2
|
+
import { QueryEngine } from '../core/QueryEngine.js';
|
|
3
|
+
import { SlashCommand } from '../commands/index.js';
|
|
4
|
+
interface UseSlashMenuOptions {
|
|
5
|
+
engineRef: React.RefObject<QueryEngine | null>;
|
|
6
|
+
sessionRef: React.MutableRefObject<import('../types/index.js').Session>;
|
|
7
|
+
tokenCountRef: React.MutableRefObject<number>;
|
|
8
|
+
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
|
|
9
|
+
setDisplayTokens: (n: number) => void;
|
|
10
|
+
setLoopState: React.Dispatch<React.SetStateAction<import('../types/index.js').LoopState | null>>;
|
|
11
|
+
setIsProcessing: React.Dispatch<React.SetStateAction<boolean>>;
|
|
12
|
+
setShowWelcome: React.Dispatch<React.SetStateAction<boolean>>;
|
|
13
|
+
setInput: React.Dispatch<React.SetStateAction<string>>;
|
|
14
|
+
/** resume 时重置所有流式状态(含 thinkingIdRef) */
|
|
15
|
+
stopAll: () => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 斜杠命令菜单状态管理 hook
|
|
19
|
+
*
|
|
20
|
+
* 管理菜单可见性、选中项、二级菜单(agent / resume)等。
|
|
21
|
+
*/
|
|
22
|
+
export declare function useSlashMenu(opts: UseSlashMenuOptions): {
|
|
23
|
+
slashMenuVisible: boolean;
|
|
24
|
+
slashMenuItems: SlashCommand[];
|
|
25
|
+
slashMenuIndex: number;
|
|
26
|
+
agentMenuMode: boolean;
|
|
27
|
+
resumeMenuMode: boolean;
|
|
28
|
+
handleSlashMenuUp: () => void;
|
|
29
|
+
handleSlashMenuDown: () => void;
|
|
30
|
+
handleSlashMenuSelect: () => void;
|
|
31
|
+
handleSlashMenuClose: () => void;
|
|
32
|
+
updateSlashMenu: (val: string) => void;
|
|
33
|
+
setSlashMenuVisible: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
34
|
+
resumeSession: (sessionId: string) => void;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { QueryEngine } from '../core/QueryEngine.js';
|
|
3
|
+
import { filterCommands, filterAgentCommands } from '../commands/index.js';
|
|
4
|
+
import { setActiveAgent } from '../config/agentState.js';
|
|
5
|
+
import { executeSlashCommand } from '../screens/slashCommands.js';
|
|
6
|
+
/**
|
|
7
|
+
* 斜杠命令菜单状态管理 hook
|
|
8
|
+
*
|
|
9
|
+
* 管理菜单可见性、选中项、二级菜单(agent / resume)等。
|
|
10
|
+
*/
|
|
11
|
+
export function useSlashMenu(opts) {
|
|
12
|
+
const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, } = opts;
|
|
13
|
+
const [slashMenuVisible, setSlashMenuVisible] = useState(false);
|
|
14
|
+
const [slashMenuItems, setSlashMenuItems] = useState([]);
|
|
15
|
+
const [slashMenuIndex, setSlashMenuIndex] = useState(0);
|
|
16
|
+
const [agentMenuMode, setAgentMenuMode] = useState(false);
|
|
17
|
+
const [resumeMenuMode, setResumeMenuMode] = useState(false);
|
|
18
|
+
// 上移
|
|
19
|
+
const handleSlashMenuUp = useCallback(() => {
|
|
20
|
+
setSlashMenuIndex((prev) => (prev > 0 ? prev - 1 : slashMenuItems.length - 1));
|
|
21
|
+
}, [slashMenuItems.length]);
|
|
22
|
+
// 下移
|
|
23
|
+
const handleSlashMenuDown = useCallback(() => {
|
|
24
|
+
setSlashMenuIndex((prev) => (prev < slashMenuItems.length - 1 ? prev + 1 : 0));
|
|
25
|
+
}, [slashMenuItems.length]);
|
|
26
|
+
// 关闭
|
|
27
|
+
const handleSlashMenuClose = useCallback(() => {
|
|
28
|
+
if (agentMenuMode || resumeMenuMode) {
|
|
29
|
+
setAgentMenuMode(false);
|
|
30
|
+
setResumeMenuMode(false);
|
|
31
|
+
setInput('/');
|
|
32
|
+
const matched = filterCommands('');
|
|
33
|
+
setSlashMenuItems(matched);
|
|
34
|
+
setSlashMenuIndex(0);
|
|
35
|
+
setSlashMenuVisible(matched.length > 0);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
setSlashMenuVisible(false);
|
|
39
|
+
}
|
|
40
|
+
}, [agentMenuMode, resumeMenuMode, setInput]);
|
|
41
|
+
// 恢复会话的通用逻辑
|
|
42
|
+
const resumeSession = useCallback((sessionId) => {
|
|
43
|
+
if (!engineRef.current)
|
|
44
|
+
return;
|
|
45
|
+
const result = engineRef.current.loadSession(sessionId);
|
|
46
|
+
if (result) {
|
|
47
|
+
// 先重置所有流式状态(含 thinkingIdRef),避免旧 id 残留导致新会话 thinking 永不结束
|
|
48
|
+
stopAll();
|
|
49
|
+
setMessages(result.messages);
|
|
50
|
+
sessionRef.current = result.session;
|
|
51
|
+
tokenCountRef.current = result.session.totalTokens;
|
|
52
|
+
setDisplayTokens(result.session.totalTokens);
|
|
53
|
+
setLoopState(null);
|
|
54
|
+
setIsProcessing(false);
|
|
55
|
+
setShowWelcome(false);
|
|
56
|
+
const resumeMsg = {
|
|
57
|
+
id: `resume-${Date.now()}`,
|
|
58
|
+
type: 'system',
|
|
59
|
+
status: 'success',
|
|
60
|
+
content: `已恢复会话 ${sessionId.slice(0, 8)}...(${result.messages.length} 条消息)`,
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
};
|
|
63
|
+
setMessages((prev) => [...prev, resumeMsg]);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const errMsg = {
|
|
67
|
+
id: `resume-err-${Date.now()}`,
|
|
68
|
+
type: 'error',
|
|
69
|
+
status: 'error',
|
|
70
|
+
content: `会话 ${sessionId} 不存在或已损坏`,
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
setMessages((prev) => [...prev, errMsg]);
|
|
74
|
+
}
|
|
75
|
+
}, [engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, stopAll, setLoopState, setIsProcessing, setShowWelcome]);
|
|
76
|
+
// 选中
|
|
77
|
+
const handleSlashMenuSelect = useCallback(() => {
|
|
78
|
+
if (slashMenuItems.length === 0)
|
|
79
|
+
return;
|
|
80
|
+
const cmd = slashMenuItems[slashMenuIndex];
|
|
81
|
+
if (!cmd)
|
|
82
|
+
return;
|
|
83
|
+
// 二级 agent 菜单
|
|
84
|
+
if (agentMenuMode) {
|
|
85
|
+
setActiveAgent(cmd.name);
|
|
86
|
+
setInput('');
|
|
87
|
+
setSlashMenuVisible(false);
|
|
88
|
+
setAgentMenuMode(false);
|
|
89
|
+
const switchMsg = {
|
|
90
|
+
id: `switch-${Date.now()}`,
|
|
91
|
+
type: 'system',
|
|
92
|
+
status: 'success',
|
|
93
|
+
content: `已切换智能体为 ${cmd.name},请重启以生效(Ctrl+C 两次退出后重新启动)`,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
};
|
|
96
|
+
setMessages((prev) => [...prev, switchMsg]);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// 二级 resume 菜单
|
|
100
|
+
if (resumeMenuMode) {
|
|
101
|
+
setInput('');
|
|
102
|
+
setSlashMenuVisible(false);
|
|
103
|
+
setResumeMenuMode(false);
|
|
104
|
+
resumeSession(cmd.name);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// 一级菜单选中 /agent -> 进入二级菜单
|
|
108
|
+
if (cmd.name === 'agent') {
|
|
109
|
+
setInput('/agent ');
|
|
110
|
+
const matched = filterAgentCommands('');
|
|
111
|
+
setSlashMenuItems(matched);
|
|
112
|
+
setSlashMenuIndex(0);
|
|
113
|
+
setAgentMenuMode(true);
|
|
114
|
+
setResumeMenuMode(false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// 一级菜单选中 /resume -> 进入二级菜单
|
|
118
|
+
if (cmd.name === 'resume') {
|
|
119
|
+
setInput('/resume ');
|
|
120
|
+
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
121
|
+
const items = sessions.map((s) => {
|
|
122
|
+
const date = new Date(s.updatedAt);
|
|
123
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
124
|
+
return {
|
|
125
|
+
name: s.id,
|
|
126
|
+
description: `[${dateStr}] ${s.summary}`,
|
|
127
|
+
category: 'builtin',
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
setSlashMenuItems(items);
|
|
131
|
+
setSlashMenuIndex(0);
|
|
132
|
+
setSlashMenuVisible(items.length > 0);
|
|
133
|
+
setResumeMenuMode(true);
|
|
134
|
+
setAgentMenuMode(false);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// 内置命令:直接执行
|
|
138
|
+
if (cmd.category === 'builtin') {
|
|
139
|
+
setInput('');
|
|
140
|
+
setSlashMenuVisible(false);
|
|
141
|
+
const msg = executeSlashCommand(cmd.name);
|
|
142
|
+
if (msg)
|
|
143
|
+
setMessages((prev) => [...prev, msg]);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// 工具命令:填入输入框
|
|
147
|
+
setInput(`/${cmd.name} `);
|
|
148
|
+
setSlashMenuVisible(false);
|
|
149
|
+
}, [slashMenuItems, slashMenuIndex, agentMenuMode, resumeMenuMode, setInput, setMessages, resumeSession]);
|
|
150
|
+
// 输入变化时更新菜单
|
|
151
|
+
const updateSlashMenu = useCallback((val) => {
|
|
152
|
+
if (val.startsWith('/') && !val.includes('\n')) {
|
|
153
|
+
const query = val.slice(1);
|
|
154
|
+
// /agent 二级菜单
|
|
155
|
+
if (/^agent\s/i.test(query)) {
|
|
156
|
+
const subQuery = query.replace(/^agent\s*/i, '');
|
|
157
|
+
const matched = filterAgentCommands(subQuery);
|
|
158
|
+
setSlashMenuItems(matched);
|
|
159
|
+
setSlashMenuIndex(0);
|
|
160
|
+
setSlashMenuVisible(matched.length > 0);
|
|
161
|
+
setAgentMenuMode(true);
|
|
162
|
+
setResumeMenuMode(false);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// /resume 二级菜单
|
|
166
|
+
if (/^resume\s/i.test(query)) {
|
|
167
|
+
const subQuery = query.replace(/^resume\s*/i, '').toLowerCase();
|
|
168
|
+
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
169
|
+
const items = sessions.map((s) => {
|
|
170
|
+
const date = new Date(s.updatedAt);
|
|
171
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
172
|
+
return {
|
|
173
|
+
name: s.id,
|
|
174
|
+
description: `[${dateStr}] ${s.summary}`,
|
|
175
|
+
category: 'builtin',
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
const matched = subQuery
|
|
179
|
+
? items.filter((i) => i.name.includes(subQuery) || i.description.toLowerCase().includes(subQuery))
|
|
180
|
+
: items;
|
|
181
|
+
setSlashMenuItems(matched);
|
|
182
|
+
setSlashMenuIndex(0);
|
|
183
|
+
setSlashMenuVisible(matched.length > 0);
|
|
184
|
+
setResumeMenuMode(true);
|
|
185
|
+
setAgentMenuMode(false);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// 一级菜单
|
|
189
|
+
const matched = filterCommands(query);
|
|
190
|
+
setSlashMenuItems(matched);
|
|
191
|
+
setSlashMenuIndex(0);
|
|
192
|
+
setSlashMenuVisible(matched.length > 0);
|
|
193
|
+
setAgentMenuMode(false);
|
|
194
|
+
setResumeMenuMode(false);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
setSlashMenuVisible(false);
|
|
198
|
+
setAgentMenuMode(false);
|
|
199
|
+
setResumeMenuMode(false);
|
|
200
|
+
}
|
|
201
|
+
}, []);
|
|
202
|
+
return {
|
|
203
|
+
slashMenuVisible,
|
|
204
|
+
slashMenuItems,
|
|
205
|
+
slashMenuIndex,
|
|
206
|
+
agentMenuMode,
|
|
207
|
+
resumeMenuMode,
|
|
208
|
+
handleSlashMenuUp,
|
|
209
|
+
handleSlashMenuDown,
|
|
210
|
+
handleSlashMenuSelect,
|
|
211
|
+
handleSlashMenuClose,
|
|
212
|
+
updateSlashMenu,
|
|
213
|
+
setSlashMenuVisible,
|
|
214
|
+
resumeSession,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Message } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 流式文本 + thinking 文本节流 hook
|
|
4
|
+
*
|
|
5
|
+
* 用 ref 积累 chunk,定时刷新到 state,减少 re-render 频率。
|
|
6
|
+
*/
|
|
7
|
+
export declare function useStreamThrottle(setMessages: React.Dispatch<React.SetStateAction<Message[]>>): {
|
|
8
|
+
streamText: string;
|
|
9
|
+
streamBufferRef: import("react").MutableRefObject<string>;
|
|
10
|
+
startStreamTimer: () => void;
|
|
11
|
+
stopStreamTimer: () => void;
|
|
12
|
+
startThinkingTimer: () => void;
|
|
13
|
+
stopThinkingTimer: () => void;
|
|
14
|
+
appendStreamChunk: (text: string) => void;
|
|
15
|
+
clearStream: () => void;
|
|
16
|
+
handleThinkingUpdate: (id: string, content: string) => void;
|
|
17
|
+
finishThinking: () => void;
|
|
18
|
+
thinkingIdRef: import("react").MutableRefObject<string | null>;
|
|
19
|
+
stopAll: () => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
const STREAM_FLUSH_INTERVAL = 80; // ms
|
|
3
|
+
const THINKING_FLUSH_INTERVAL = 100; // ms
|
|
4
|
+
/**
|
|
5
|
+
* 流式文本 + thinking 文本节流 hook
|
|
6
|
+
*
|
|
7
|
+
* 用 ref 积累 chunk,定时刷新到 state,减少 re-render 频率。
|
|
8
|
+
*/
|
|
9
|
+
export function useStreamThrottle(setMessages) {
|
|
10
|
+
const [streamText, setStreamText] = useState('');
|
|
11
|
+
// 流式文本节流
|
|
12
|
+
const streamBufferRef = useRef('');
|
|
13
|
+
const streamTimerRef = useRef(null);
|
|
14
|
+
// thinking 文本节流
|
|
15
|
+
const thinkingBufferRef = useRef('');
|
|
16
|
+
const thinkingIdRef = useRef(null);
|
|
17
|
+
const thinkingTimerRef = useRef(null);
|
|
18
|
+
// 启动流式文本节流定时器
|
|
19
|
+
const startStreamTimer = useCallback(() => {
|
|
20
|
+
if (streamTimerRef.current)
|
|
21
|
+
return;
|
|
22
|
+
streamTimerRef.current = setInterval(() => {
|
|
23
|
+
if (streamBufferRef.current) {
|
|
24
|
+
const buf = streamBufferRef.current;
|
|
25
|
+
streamBufferRef.current = '';
|
|
26
|
+
setStreamText((prev) => prev + buf);
|
|
27
|
+
}
|
|
28
|
+
}, STREAM_FLUSH_INTERVAL);
|
|
29
|
+
}, []);
|
|
30
|
+
const stopStreamTimer = useCallback(() => {
|
|
31
|
+
if (streamTimerRef.current) {
|
|
32
|
+
clearInterval(streamTimerRef.current);
|
|
33
|
+
streamTimerRef.current = null;
|
|
34
|
+
}
|
|
35
|
+
if (streamBufferRef.current) {
|
|
36
|
+
const buf = streamBufferRef.current;
|
|
37
|
+
streamBufferRef.current = '';
|
|
38
|
+
setStreamText((prev) => prev + buf);
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
// 启动 thinking 文本节流定时器
|
|
42
|
+
const startThinkingTimer = useCallback(() => {
|
|
43
|
+
if (thinkingTimerRef.current)
|
|
44
|
+
return;
|
|
45
|
+
thinkingTimerRef.current = setInterval(() => {
|
|
46
|
+
const id = thinkingIdRef.current;
|
|
47
|
+
const buf = thinkingBufferRef.current;
|
|
48
|
+
if (id && buf) {
|
|
49
|
+
setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, content: buf } : msg)));
|
|
50
|
+
}
|
|
51
|
+
}, THINKING_FLUSH_INTERVAL);
|
|
52
|
+
}, [setMessages]);
|
|
53
|
+
const stopThinkingTimer = useCallback(() => {
|
|
54
|
+
if (thinkingTimerRef.current) {
|
|
55
|
+
clearInterval(thinkingTimerRef.current);
|
|
56
|
+
thinkingTimerRef.current = null;
|
|
57
|
+
}
|
|
58
|
+
const id = thinkingIdRef.current;
|
|
59
|
+
const buf = thinkingBufferRef.current;
|
|
60
|
+
if (id && buf) {
|
|
61
|
+
setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, content: buf } : msg)));
|
|
62
|
+
}
|
|
63
|
+
}, [setMessages]);
|
|
64
|
+
/** 追加流式文本到 buffer(不直接 setState) */
|
|
65
|
+
const appendStreamChunk = useCallback((text) => {
|
|
66
|
+
streamBufferRef.current += text;
|
|
67
|
+
}, []);
|
|
68
|
+
/** 清空流式文本(每轮迭代结束时调用) */
|
|
69
|
+
const clearStream = useCallback(() => {
|
|
70
|
+
streamBufferRef.current = '';
|
|
71
|
+
setStreamText('');
|
|
72
|
+
}, []);
|
|
73
|
+
/** 处理 thinking 消息的 content 更新 */
|
|
74
|
+
const handleThinkingUpdate = useCallback((id, content) => {
|
|
75
|
+
if (thinkingIdRef.current === null) {
|
|
76
|
+
thinkingIdRef.current = id;
|
|
77
|
+
thinkingBufferRef.current = content;
|
|
78
|
+
startThinkingTimer();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
thinkingBufferRef.current = content;
|
|
82
|
+
}
|
|
83
|
+
}, [startThinkingTimer]);
|
|
84
|
+
/** thinking 完成时调用 */
|
|
85
|
+
const finishThinking = useCallback(() => {
|
|
86
|
+
stopThinkingTimer();
|
|
87
|
+
thinkingIdRef.current = null;
|
|
88
|
+
thinkingBufferRef.current = '';
|
|
89
|
+
}, [stopThinkingTimer]);
|
|
90
|
+
/** 全部停止(loop 结束时调用) */
|
|
91
|
+
const stopAll = useCallback(() => {
|
|
92
|
+
stopStreamTimer();
|
|
93
|
+
stopThinkingTimer();
|
|
94
|
+
thinkingIdRef.current = null;
|
|
95
|
+
thinkingBufferRef.current = '';
|
|
96
|
+
streamBufferRef.current = '';
|
|
97
|
+
setStreamText('');
|
|
98
|
+
}, [stopStreamTimer, stopThinkingTimer]);
|
|
99
|
+
// 组件卸载时清理
|
|
100
|
+
useEffect(() => () => {
|
|
101
|
+
if (streamTimerRef.current)
|
|
102
|
+
clearInterval(streamTimerRef.current);
|
|
103
|
+
if (thinkingTimerRef.current)
|
|
104
|
+
clearInterval(thinkingTimerRef.current);
|
|
105
|
+
}, []);
|
|
106
|
+
return {
|
|
107
|
+
streamText,
|
|
108
|
+
streamBufferRef,
|
|
109
|
+
startStreamTimer,
|
|
110
|
+
stopStreamTimer,
|
|
111
|
+
startThinkingTimer,
|
|
112
|
+
stopThinkingTimer,
|
|
113
|
+
appendStreamChunk,
|
|
114
|
+
clearStream,
|
|
115
|
+
handleThinkingUpdate,
|
|
116
|
+
finishThinking,
|
|
117
|
+
thinkingIdRef,
|
|
118
|
+
stopAll,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/** 响应式终端宽度 */
|
|
3
|
+
export function useTerminalWidth() {
|
|
4
|
+
const [width, setWidth] = useState(() => process.stdout.columns || 80);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const onResize = () => {
|
|
7
|
+
setWidth(process.stdout.columns || 80);
|
|
8
|
+
};
|
|
9
|
+
process.stdout.on('resize', onResize);
|
|
10
|
+
return () => { process.stdout.off('resize', onResize); };
|
|
11
|
+
}, []);
|
|
12
|
+
return width;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token 计数显示 hook
|
|
3
|
+
*
|
|
4
|
+
* 用 ref 缓存实时 token 数,定时器驱动 UI 刷新,避免每个 chunk 都触发 re-render。
|
|
5
|
+
*/
|
|
6
|
+
export declare function useTokenDisplay(): {
|
|
7
|
+
displayTokens: number;
|
|
8
|
+
tokenCountRef: import("react").MutableRefObject<number>;
|
|
9
|
+
startTokenTimer: () => void;
|
|
10
|
+
stopTokenTimer: () => void;
|
|
11
|
+
updateTokenCount: (count: number) => void;
|
|
12
|
+
resetTokens: () => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Token 计数显示 hook
|
|
4
|
+
*
|
|
5
|
+
* 用 ref 缓存实时 token 数,定时器驱动 UI 刷新,避免每个 chunk 都触发 re-render。
|
|
6
|
+
*/
|
|
7
|
+
export function useTokenDisplay() {
|
|
8
|
+
const tokenCountRef = useRef(0);
|
|
9
|
+
const tokenTimerRef = useRef(null);
|
|
10
|
+
const [displayTokens, setDisplayTokens] = useState(0);
|
|
11
|
+
const startTokenTimer = useCallback(() => {
|
|
12
|
+
if (tokenTimerRef.current)
|
|
13
|
+
return;
|
|
14
|
+
tokenTimerRef.current = setInterval(() => {
|
|
15
|
+
setDisplayTokens(tokenCountRef.current);
|
|
16
|
+
}, 100);
|
|
17
|
+
}, []);
|
|
18
|
+
const stopTokenTimer = useCallback(() => {
|
|
19
|
+
if (tokenTimerRef.current) {
|
|
20
|
+
clearInterval(tokenTimerRef.current);
|
|
21
|
+
tokenTimerRef.current = null;
|
|
22
|
+
}
|
|
23
|
+
setDisplayTokens(tokenCountRef.current);
|
|
24
|
+
}, []);
|
|
25
|
+
const updateTokenCount = useCallback((count) => {
|
|
26
|
+
tokenCountRef.current = count;
|
|
27
|
+
}, []);
|
|
28
|
+
const resetTokens = useCallback(() => {
|
|
29
|
+
tokenCountRef.current = 0;
|
|
30
|
+
setDisplayTokens(0);
|
|
31
|
+
}, []);
|
|
32
|
+
// 组件卸载时清理
|
|
33
|
+
useEffect(() => () => {
|
|
34
|
+
if (tokenTimerRef.current)
|
|
35
|
+
clearInterval(tokenTimerRef.current);
|
|
36
|
+
}, []);
|
|
37
|
+
return {
|
|
38
|
+
displayTokens,
|
|
39
|
+
tokenCountRef,
|
|
40
|
+
startTokenTimer,
|
|
41
|
+
stopTokenTimer,
|
|
42
|
+
updateTokenCount,
|
|
43
|
+
resetTokens,
|
|
44
|
+
};
|
|
45
|
+
}
|