thatgfsj-code 2.2.9 → 3.0.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/CHANGELOG.md +15 -67
- package/dist/app/index.d.ts +28 -5
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +69 -10
- package/dist/app/index.js.map +1 -1
- package/dist/cache/fingerprint.d.ts +49 -0
- package/dist/cache/fingerprint.d.ts.map +1 -0
- package/dist/cache/fingerprint.js +100 -0
- package/dist/cache/fingerprint.js.map +1 -0
- package/dist/cache/index.d.ts +15 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +15 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/smartModel.d.ts +40 -0
- package/dist/cache/smartModel.d.ts.map +1 -0
- package/dist/cache/smartModel.js +43 -0
- package/dist/cache/smartModel.js.map +1 -0
- package/dist/cache/stats.d.ts +93 -0
- package/dist/cache/stats.d.ts.map +1 -0
- package/dist/cache/stats.js +155 -0
- package/dist/cache/stats.js.map +1 -0
- package/dist/cache/volatile.d.ts +32 -0
- package/dist/cache/volatile.d.ts.map +1 -0
- package/dist/cache/volatile.js +44 -0
- package/dist/cache/volatile.js.map +1 -0
- package/dist/cmd/index.js +49 -57
- package/dist/cmd/index.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +14 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +55 -5
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +172 -26
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/index.d.ts +26 -8
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +83 -15
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +28 -8
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +58 -8
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/provider.d.ts +40 -8
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +8 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/prompts/index.d.ts +32 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +33 -11
- package/dist/prompts/index.js.map +1 -1
- package/dist/session/index.d.ts +32 -3
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +51 -36
- package/dist/session/index.js.map +1 -1
- package/dist/skills/brainstorming.js +33 -33
- package/dist/skills/code-review.js +35 -35
- package/dist/skills/executing-plans.js +18 -18
- package/dist/skills/frontend-design.js +25 -25
- package/dist/skills/git-workflow.js +26 -26
- package/dist/skills/improve-architecture.js +28 -28
- package/dist/skills/neuroweave.js +37 -37
- package/dist/skills/playwright.js +62 -62
- package/dist/skills/prototype.js +20 -20
- package/dist/skills/subagent.js +19 -19
- package/dist/skills/supabase.js +34 -34
- package/dist/skills/systematic-debugging.js +34 -34
- package/dist/skills/tdd.js +29 -29
- package/dist/skills/triage.js +25 -25
- package/dist/skills/verification.js +22 -22
- package/dist/skills/writing-plans.js +32 -32
- package/dist/tools/nwt.js +16 -16
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +9 -2
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/Header.d.ts +8 -0
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +17 -4
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/InitWizard.d.ts +12 -1
- package/dist/tui/components/InitWizard.d.ts.map +1 -1
- package/dist/tui/components/InitWizard.js +58 -3
- package/dist/tui/components/InitWizard.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +1 -6
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts +22 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +82 -46
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +57 -0
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +2 -3
- package/dist/tui/welcome.js.map +1 -1
- package/dist/types.d.ts +79 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/stableStringify.d.ts +21 -0
- package/dist/utils/stableStringify.d.ts.map +1 -0
- package/dist/utils/stableStringify.js +53 -0
- package/dist/utils/stableStringify.js.map +1 -0
- package/dist/utils/thinking.d.ts.map +1 -1
- package/dist/utils/thinking.js +0 -3
- package/dist/utils/thinking.js.map +1 -1
- package/package.json +6 -6
- package/src/app/index.ts +180 -124
- package/src/cache/fingerprint.ts +101 -0
- package/src/cache/index.ts +15 -0
- package/src/cache/smartModel.ts +52 -0
- package/src/cache/stats.ts +199 -0
- package/src/cache/volatile.ts +47 -0
- package/src/cmd/index.tsx +288 -292
- package/src/config/index.ts +156 -148
- package/src/config/providers.ts +234 -234
- package/src/config/types.ts +67 -53
- package/src/hooks/index.ts +111 -111
- package/src/llm/anthropic.ts +388 -243
- package/src/llm/gemini.ts +169 -169
- package/src/llm/index.ts +265 -200
- package/src/llm/openai.ts +243 -196
- package/src/llm/provider.ts +66 -34
- package/src/prompts/index.ts +260 -220
- package/src/session/compactor.ts +103 -103
- package/src/session/index.ts +181 -168
- package/src/session/message.ts +42 -42
- package/src/skills/brainstorming.ts +43 -43
- package/src/skills/code-review.ts +45 -45
- package/src/skills/executing-plans.ts +27 -27
- package/src/skills/frontend-design.ts +35 -35
- package/src/skills/git-workflow.ts +36 -36
- package/src/skills/improve-architecture.ts +38 -38
- package/src/skills/index.ts +136 -136
- package/src/skills/neuroweave.ts +47 -47
- package/src/skills/playwright.ts +72 -72
- package/src/skills/prototype.ts +30 -30
- package/src/skills/subagent.ts +28 -28
- package/src/skills/supabase.ts +44 -44
- package/src/skills/systematic-debugging.ts +44 -44
- package/src/skills/tdd.ts +39 -39
- package/src/skills/triage.ts +35 -35
- package/src/skills/verification.ts +31 -31
- package/src/skills/writing-plans.ts +42 -42
- package/src/tools/nwt.ts +598 -598
- package/src/tools/types.ts +122 -122
- package/src/tui/app.tsx +199 -186
- package/src/tui/components/ChatList.tsx +41 -41
- package/src/tui/components/ChatMessage.tsx +54 -54
- package/src/tui/components/Header.tsx +56 -29
- package/src/tui/components/InitWizard.tsx +217 -132
- package/src/tui/components/Markdown.tsx +35 -35
- package/src/tui/components/ModelSelector.tsx +87 -87
- package/src/tui/components/StatusBar.tsx +30 -30
- package/src/tui/components/Thinking.tsx +17 -17
- package/src/tui/components/ToolCall.tsx +102 -107
- package/src/tui/components/UserInput.tsx +131 -131
- package/src/tui/hooks/useChat.ts +287 -238
- package/src/tui/hooks/useCommands.ts +234 -176
- package/src/tui/index.ts +6 -6
- package/src/tui/welcome.ts +177 -178
- package/src/types.ts +104 -42
- package/src/utils/diff.ts +71 -71
- package/src/utils/project.ts +99 -99
- package/src/utils/stableStringify.ts +47 -0
- package/src/utils/thinking.ts +118 -121
- package/tests/cache/fingerprint.test.ts +75 -0
- package/tests/cache/stableStringify.test.ts +43 -0
- package/tests/cache/stats.test.ts +146 -0
- package/tests/cache/volatile.test.ts +75 -0
- package/tests/smoke-pollution.mjs +78 -0
- package/tests/smoke-thinking.mjs +110 -0
- package/tests/smoke-tool-stream.mjs +69 -0
- package/tests/smoke-tool.mjs +117 -0
- package/.nwt/meta.json +0 -5
- package/dist/version.d.ts +0 -16
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -16
- package/dist/version.js.map +0 -1
- package/src/version.ts +0 -16
package/src/tui/hooks/useChat.ts
CHANGED
|
@@ -1,238 +1,287 @@
|
|
|
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
|
-
import { compressThinking, splitThinking, summarizeThinking } from '../../utils/thinking.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
import { compressThinking, splitThinking, summarizeThinking } from '../../utils/thinking.js';
|
|
6
|
+
import type { StreamChunk, Usage } from '../../types.js';
|
|
7
|
+
|
|
8
|
+
interface ChatState {
|
|
9
|
+
messages: MessageData[];
|
|
10
|
+
isThinking: boolean;
|
|
11
|
+
streaming: string;
|
|
12
|
+
streamingToolCalls: ToolCallData[];
|
|
13
|
+
queuedMessage: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* v3.0.0: Latest cache usage emitted by the provider, surfaced to the TUI
|
|
16
|
+
* Header. Null when no usage data was returned (e.g. provider does not
|
|
17
|
+
* support cache stats or streaming without stream_options.include_usage).
|
|
18
|
+
*/
|
|
19
|
+
lastUsage: Usage | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hook for managing chat state and the streaming response lifecycle.
|
|
24
|
+
*
|
|
25
|
+
* v3.0.0: streamResponse yields structured StreamChunks (text / tool_calls /
|
|
26
|
+
* thinking / usage). The previous @@TOOL@@ sentinel-string parsing is gone.
|
|
27
|
+
*
|
|
28
|
+
* Persisted message order matches the source chunks exactly:
|
|
29
|
+
* - text chunks → accumulated into fullContent (later compressed via
|
|
30
|
+
* compressThinking before persistence)
|
|
31
|
+
* - tool_calls chunks → results are summarized into a `[tool: name → result]`
|
|
32
|
+
* suffix on the assistant message (same belt-and-suspenders behavior as
|
|
33
|
+
* v2.2.6)
|
|
34
|
+
* - usage chunks → surfaced via state.lastUsage so the TUI Header can render
|
|
35
|
+
* cache hit-rate (consumed by app.tsx via /cache command in M3)
|
|
36
|
+
*/
|
|
37
|
+
export function useChat(app: App) {
|
|
38
|
+
const [state, setState] = useState<ChatState>({
|
|
39
|
+
messages: [],
|
|
40
|
+
isThinking: false,
|
|
41
|
+
streaming: '',
|
|
42
|
+
streamingToolCalls: [],
|
|
43
|
+
queuedMessage: null,
|
|
44
|
+
lastUsage: null,
|
|
45
|
+
});
|
|
46
|
+
const processingRef = useRef(false);
|
|
47
|
+
const queuedRef = useRef<string | null>(null);
|
|
48
|
+
const abortRef = useRef(false);
|
|
49
|
+
|
|
50
|
+
const processStream = async (input: string) => {
|
|
51
|
+
abortRef.current = false;
|
|
52
|
+
setState(prev => ({
|
|
53
|
+
...prev,
|
|
54
|
+
messages: [...prev.messages, { role: 'user', content: input }],
|
|
55
|
+
isThinking: true,
|
|
56
|
+
streaming: '',
|
|
57
|
+
streamingToolCalls: [],
|
|
58
|
+
queuedMessage: null,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
app.session.addMessage('user', input);
|
|
62
|
+
|
|
63
|
+
const stream = app.streamResponse();
|
|
64
|
+
let fullContent = '';
|
|
65
|
+
let currentToolCalls: ToolCallData[] = [];
|
|
66
|
+
// Latest usage from this round; surfaced via state.lastUsage.
|
|
67
|
+
let lastUsage: Usage | null = null;
|
|
68
|
+
let lastUpdateTime = 0;
|
|
69
|
+
const THROTTLE_MS = 50;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Flush streaming state to React. Throttled to ~20fps so the terminal
|
|
73
|
+
* doesn't flicker on long completions (combined with <Static> below the
|
|
74
|
+
* completion line stays put while the streaming line updates in place).
|
|
75
|
+
*/
|
|
76
|
+
const flushStreaming = () => {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (now - lastUpdateTime < THROTTLE_MS) return;
|
|
79
|
+
lastUpdateTime = now;
|
|
80
|
+
setState(prev => ({ ...prev, isThinking: false, streaming: fullContent }));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
for await (const chunk of stream as AsyncIterable<StreamChunk>) {
|
|
85
|
+
// Check abort
|
|
86
|
+
if (abortRef.current) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
switch (chunk.type) {
|
|
91
|
+
case 'text':
|
|
92
|
+
if (chunk.content) {
|
|
93
|
+
fullContent += chunk.content;
|
|
94
|
+
flushStreaming();
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'thinking':
|
|
99
|
+
// Reasoning text. We do not append it to fullContent (it is
|
|
100
|
+
// stripped from persistence in compressThinking), but we surface
|
|
101
|
+
// it via the streaming display when app.showThinking is on.
|
|
102
|
+
if (app.showThinking && chunk.content) {
|
|
103
|
+
fullContent += chunk.content;
|
|
104
|
+
flushStreaming();
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'tool_calls':
|
|
109
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
110
|
+
// Replace rather than append — LLMService emits one combined
|
|
111
|
+
// tool_calls chunk per iteration. This is the dispatch plan.
|
|
112
|
+
currentToolCalls = chunk.toolCalls.map(tc => ({
|
|
113
|
+
name: tc.function.name,
|
|
114
|
+
args: tc.function.arguments,
|
|
115
|
+
// Results are appended by the *next* text chunk via the
|
|
116
|
+
// `[tool: name → result]` suffix we add on persistence. The
|
|
117
|
+
// streaming ToolCall panel below renders a "running" state
|
|
118
|
+
// until the next iteration's tool_calls chunk comes back
|
|
119
|
+
// with `result` (we look up by name).
|
|
120
|
+
result: undefined,
|
|
121
|
+
isError: false,
|
|
122
|
+
}));
|
|
123
|
+
setState(prev => ({
|
|
124
|
+
...prev,
|
|
125
|
+
isThinking: false,
|
|
126
|
+
streamingToolCalls: [...currentToolCalls],
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'usage':
|
|
132
|
+
lastUsage = chunk.usage;
|
|
133
|
+
setState(prev => ({ ...prev, lastUsage: chunk.usage }));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// v2.2.4 (port from v2.1.0): DO NOT persist truncated assistant
|
|
139
|
+
// messages. The previous code literally wrote `'\n\n[已中断]'`
|
|
140
|
+
// as a suffix and persisted it — which is what created the
|
|
141
|
+
// hallucination loop where the next turn's LLM echoed the
|
|
142
|
+
// marker back. The fix is two-pronged:
|
|
143
|
+
// 1. Never persist when the stream was aborted (here).
|
|
144
|
+
// 2. SessionManager.addMessageSafe drops messages that match
|
|
145
|
+
// the pollution filter as a belt-and-suspenders check
|
|
146
|
+
// for cases where we somehow persist a polluted message.
|
|
147
|
+
const wasAborted = abortRef.current;
|
|
148
|
+
const shouldPersist = !wasAborted &&
|
|
149
|
+
(fullContent.trim() || currentToolCalls.length > 0);
|
|
150
|
+
|
|
151
|
+
if (shouldPersist) {
|
|
152
|
+
// v2.2.5: strip blocks from the persisted message
|
|
153
|
+
// when compression is enabled. Same rationale as in
|
|
154
|
+
// cmd/index.tsx — keeps history compact, avoids re-feeding
|
|
155
|
+
// reasoning into the next turn's context window.
|
|
156
|
+
const toPersist = compressThinking(fullContent, app.showThinking);
|
|
157
|
+
app.session.addMessageSafe('assistant', toPersist);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// v2.2.5: build a displayable version. When thinking is hidden
|
|
161
|
+
// we still want the user to see a one-line indicator of how
|
|
162
|
+
// much reasoning the model did, plus the conclusion.
|
|
163
|
+
const split = splitThinking(fullContent);
|
|
164
|
+
const displayContent = app.showThinking
|
|
165
|
+
? fullContent
|
|
166
|
+
: (split.thinking
|
|
167
|
+
? `${summarizeThinking(split)}\n${split.conclusion}`
|
|
168
|
+
: fullContent);
|
|
169
|
+
|
|
170
|
+
// v2.2.6 (tool-result belt-and-suspenders): if any tool call
|
|
171
|
+
// returned text, also append a compact "[tool: name → result]"
|
|
172
|
+
// summary to the persisted/displayed content. This guarantees
|
|
173
|
+
// the user sees the tool output regardless of whether the Ink
|
|
174
|
+
// <ToolCall/> component renders it correctly. Past sessions
|
|
175
|
+
// have had cases where the streaming ToolCall rendering failed
|
|
176
|
+
// silently (e.g. result was empty string, wrap=truncate cut
|
|
177
|
+
// long output off-screen) and the user had no idea what the
|
|
178
|
+
// tool actually returned.
|
|
179
|
+
//
|
|
180
|
+
// v3.0.0: since LLMService now folds tool results into
|
|
181
|
+
// currentMessages without surfacing them as separate chunks, we no
|
|
182
|
+
// longer have a `result` field on each ToolCallData here. The
|
|
183
|
+
// result summary is built by reading back from
|
|
184
|
+
// app.session.getMessages() — the latest `tool` role message per
|
|
185
|
+
// tool call id. To keep this lightweight we fall back to a brief
|
|
186
|
+
// "[tool: name → see message]" suffix when the result is not
|
|
187
|
+
// available in our local streaming state.
|
|
188
|
+
const toolSummary = currentToolCalls.length > 0
|
|
189
|
+
? '\n\n' + currentToolCalls.map((tc) => {
|
|
190
|
+
const r = tc.result !== undefined ? tc.result : '(see tool result above)';
|
|
191
|
+
const short = r.length > 200 ? r.slice(0, 197) + '...' : r;
|
|
192
|
+
return `[tool: ${tc.name} → ${short}]`;
|
|
193
|
+
}).join('\n')
|
|
194
|
+
: '';
|
|
195
|
+
|
|
196
|
+
setState(prev => ({
|
|
197
|
+
...prev,
|
|
198
|
+
messages: [
|
|
199
|
+
...prev.messages,
|
|
200
|
+
...(shouldPersist
|
|
201
|
+
? [{
|
|
202
|
+
role: 'assistant' as const,
|
|
203
|
+
content: displayContent + toolSummary,
|
|
204
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
205
|
+
}]
|
|
206
|
+
: []),
|
|
207
|
+
],
|
|
208
|
+
streaming: '',
|
|
209
|
+
streamingToolCalls: [],
|
|
210
|
+
isThinking: false,
|
|
211
|
+
lastUsage,
|
|
212
|
+
}));
|
|
213
|
+
|
|
214
|
+
app.session.truncate();
|
|
215
|
+
} catch (error: any) {
|
|
216
|
+
if (abortRef.current) {
|
|
217
|
+
setState(prev => ({
|
|
218
|
+
...prev,
|
|
219
|
+
messages: [
|
|
220
|
+
...prev.messages,
|
|
221
|
+
...(fullContent.trim()
|
|
222
|
+
? [{ role: 'assistant' as const, content: fullContent + '\n\n[已中断]' }]
|
|
223
|
+
: [{ role: 'assistant' as const, content: '[已中断]' }]),
|
|
224
|
+
],
|
|
225
|
+
streaming: '',
|
|
226
|
+
streamingToolCalls: [],
|
|
227
|
+
isThinking: false,
|
|
228
|
+
}));
|
|
229
|
+
} else {
|
|
230
|
+
const msg = error.message || String(error);
|
|
231
|
+
let errorMsg = `Error: ${msg}`;
|
|
232
|
+
|
|
233
|
+
if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
|
|
234
|
+
errorMsg = `❌ API key invalid. Run \`gfcode init\` to reconfigure.`;
|
|
235
|
+
} else if (msg.includes('429') || msg.includes('rate limit') || msg.includes('quota')) {
|
|
236
|
+
errorMsg = `❌ Rate limit exceeded. Wait or run \`gfcode init\` to switch provider.`;
|
|
237
|
+
} else if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
238
|
+
errorMsg = `❌ Cannot connect. Check network or run \`gfcode init\`.`;
|
|
239
|
+
} else if (msg.includes('abort') || msg.includes('AbortError')) {
|
|
240
|
+
errorMsg = `[已中断]`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setState(prev => ({
|
|
244
|
+
...prev,
|
|
245
|
+
messages: [
|
|
246
|
+
...prev.messages,
|
|
247
|
+
...(fullContent.trim()
|
|
248
|
+
? [{ role: 'assistant' as const, content: fullContent }]
|
|
249
|
+
: []),
|
|
250
|
+
{ role: 'assistant', content: errorMsg },
|
|
251
|
+
],
|
|
252
|
+
streaming: '',
|
|
253
|
+
streamingToolCalls: [],
|
|
254
|
+
isThinking: false,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Process queued message
|
|
260
|
+
if (!abortRef.current && queuedRef.current) {
|
|
261
|
+
const next = queuedRef.current;
|
|
262
|
+
queuedRef.current = null;
|
|
263
|
+
await processStream(next);
|
|
264
|
+
} else {
|
|
265
|
+
processingRef.current = false;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const sendMessage = useCallback((input: string) => {
|
|
270
|
+
if (processingRef.current) {
|
|
271
|
+
queuedRef.current = input;
|
|
272
|
+
setState(prev => ({ ...prev, queuedMessage: input }));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
processingRef.current = true;
|
|
277
|
+
processStream(input);
|
|
278
|
+
}, [app]);
|
|
279
|
+
|
|
280
|
+
const cancel = useCallback(() => {
|
|
281
|
+
abortRef.current = true;
|
|
282
|
+
queuedRef.current = null;
|
|
283
|
+
setState(prev => ({ ...prev, queuedMessage: null, isThinking: false }));
|
|
284
|
+
}, []);
|
|
285
|
+
|
|
286
|
+
return { ...state, sendMessage, cancel };
|
|
287
|
+
}
|