otherwise-cli 0.1.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 +193 -0
- package/bin/otherwise.js +5 -0
- package/frontend/404.html +84 -0
- package/frontend/assets/OpenDyslexic3-Bold-CDyRs55Y.ttf +0 -0
- package/frontend/assets/OpenDyslexic3-Regular-CIBXa4WE.ttf +0 -0
- package/frontend/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/frontend/assets/conversational-worker-CeKiciGk.js +2929 -0
- package/frontend/assets/dictation-worker-D0aYfq8b.js +29 -0
- package/frontend/assets/gemini-color-CgSQmmva.png +0 -0
- package/frontend/assets/index-BLux5ps4.js +21 -0
- package/frontend/assets/index-Blh8_TEM.js +5272 -0
- package/frontend/assets/index-BpQ1PuKu.js +18 -0
- package/frontend/assets/index-Df737c8w.css +1 -0
- package/frontend/assets/index-xaYHL6wb.js +113 -0
- package/frontend/assets/ort-wasm-simd-threaded.asyncify-BynIiDiv.wasm +0 -0
- package/frontend/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
- package/frontend/assets/transformers-tULNc5V3.js +31 -0
- package/frontend/assets/tts-worker-DPJWqT7N.js +2899 -0
- package/frontend/assets/voice-mode-worker-GzvIE_uh.js +2927 -0
- package/frontend/assets/worker-2d5ABSLU.js +31 -0
- package/frontend/banner.png +0 -0
- package/frontend/favicon.svg +3 -0
- package/frontend/google55e5ec47ee14a5f8.html +1 -0
- package/frontend/index.html +234 -0
- package/frontend/manifest.json +17 -0
- package/frontend/pdf.worker.min.mjs +21 -0
- package/frontend/robots.txt +5 -0
- package/frontend/sitemap.xml +27 -0
- package/package.json +81 -0
- package/src/agent/index.js +1066 -0
- package/src/agent/location.js +51 -0
- package/src/agent/prompt.js +548 -0
- package/src/agent/tools.js +4372 -0
- package/src/browser/detect.js +68 -0
- package/src/browser/session.js +1109 -0
- package/src/config.js +137 -0
- package/src/email/client.js +503 -0
- package/src/index.js +557 -0
- package/src/inference/anthropic.js +113 -0
- package/src/inference/google.js +373 -0
- package/src/inference/index.js +81 -0
- package/src/inference/ollama.js +383 -0
- package/src/inference/openai.js +140 -0
- package/src/inference/openrouter.js +378 -0
- package/src/inference/xai.js +200 -0
- package/src/logBridge.js +9 -0
- package/src/models.js +146 -0
- package/src/remote/client.js +225 -0
- package/src/scheduler/cron.js +243 -0
- package/src/server.js +3876 -0
- package/src/storage/db.js +1135 -0
- package/src/storage/supabase.js +364 -0
- package/src/tunnel/cloudflare.js +241 -0
- package/src/ui/components/App.jsx +687 -0
- package/src/ui/components/BrowserSelect.jsx +111 -0
- package/src/ui/components/FilePicker.jsx +472 -0
- package/src/ui/components/Header.jsx +444 -0
- package/src/ui/components/HelpPanel.jsx +173 -0
- package/src/ui/components/HistoryPanel.jsx +158 -0
- package/src/ui/components/MessageList.jsx +235 -0
- package/src/ui/components/ModelSelector.jsx +304 -0
- package/src/ui/components/PromptInput.jsx +515 -0
- package/src/ui/components/StreamingResponse.jsx +134 -0
- package/src/ui/components/ThinkingIndicator.jsx +365 -0
- package/src/ui/components/ToolExecution.jsx +714 -0
- package/src/ui/components/index.js +82 -0
- package/src/ui/context/TerminalContext.jsx +150 -0
- package/src/ui/context/index.js +13 -0
- package/src/ui/hooks/index.js +16 -0
- package/src/ui/hooks/useChatState.js +675 -0
- package/src/ui/hooks/useCommands.js +280 -0
- package/src/ui/hooks/useFileAttachments.js +216 -0
- package/src/ui/hooks/useKeyboardShortcuts.js +173 -0
- package/src/ui/hooks/useNotifications.js +185 -0
- package/src/ui/hooks/useTerminalSize.js +151 -0
- package/src/ui/hooks/useWebSocket.js +273 -0
- package/src/ui/index.js +94 -0
- package/src/ui/ink-runner.js +22 -0
- package/src/ui/utils/formatters.js +424 -0
- package/src/ui/utils/index.js +6 -0
- package/src/ui/utils/markdown.js +166 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat state management hook for Ink UI
|
|
3
|
+
* Manages chat messages, streaming content, and tool executions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useReducer, useRef, useEffect } from "react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Message types
|
|
10
|
+
*/
|
|
11
|
+
export const MessageRole = {
|
|
12
|
+
USER: "user",
|
|
13
|
+
ASSISTANT: "assistant",
|
|
14
|
+
SYSTEM: "system",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generation states
|
|
19
|
+
*/
|
|
20
|
+
export const GenerationState = {
|
|
21
|
+
IDLE: "idle",
|
|
22
|
+
THINKING: "thinking",
|
|
23
|
+
GENERATING: "generating",
|
|
24
|
+
TOOL_EXECUTING: "tool_executing",
|
|
25
|
+
STOPPED: "stopped",
|
|
26
|
+
ERROR: "error",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tool execution states
|
|
31
|
+
*/
|
|
32
|
+
export const ToolState = {
|
|
33
|
+
PREPARING: "preparing",
|
|
34
|
+
RUNNING: "running",
|
|
35
|
+
STREAMING: "streaming",
|
|
36
|
+
COMPLETE: "complete",
|
|
37
|
+
ERROR: "error",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tool reducer actions
|
|
42
|
+
*/
|
|
43
|
+
const toolActions = {
|
|
44
|
+
START: "tool/start",
|
|
45
|
+
UPDATE: "tool/update",
|
|
46
|
+
STREAMING: "tool/streaming",
|
|
47
|
+
COMPLETE: "tool/complete",
|
|
48
|
+
ERROR: "tool/error",
|
|
49
|
+
CLEAR: "tool/clear",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tool state reducer
|
|
54
|
+
*/
|
|
55
|
+
function toolReducer(state, action) {
|
|
56
|
+
switch (action.type) {
|
|
57
|
+
case toolActions.START: {
|
|
58
|
+
// Clean up any existing streaming entry for this tool name (streaming → running transition)
|
|
59
|
+
const cleaned = { ...state };
|
|
60
|
+
for (const [id, tool] of Object.entries(cleaned)) {
|
|
61
|
+
if (
|
|
62
|
+
id !== action.id &&
|
|
63
|
+
tool.name === action.name &&
|
|
64
|
+
(tool.status === ToolState.STREAMING || tool.status === ToolState.PREPARING)
|
|
65
|
+
) {
|
|
66
|
+
delete cleaned[id];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...cleaned,
|
|
71
|
+
[action.id]: {
|
|
72
|
+
id: action.id,
|
|
73
|
+
name: action.name,
|
|
74
|
+
args: action.args,
|
|
75
|
+
status: ToolState.RUNNING,
|
|
76
|
+
startTime: state[action.id]?.startTime || Date.now(),
|
|
77
|
+
streamingContent: state[action.id]?.streamingContent || "",
|
|
78
|
+
result: null,
|
|
79
|
+
error: null,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case toolActions.STREAMING: {
|
|
85
|
+
const existing = state[action.id];
|
|
86
|
+
if (existing) {
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
[action.id]: {
|
|
90
|
+
...existing,
|
|
91
|
+
status: ToolState.STREAMING,
|
|
92
|
+
args: { ...existing.args, ...action.args },
|
|
93
|
+
streamingContent: action.content || "",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Create entry when it doesn't exist yet (tool_streaming arrives before tool_start)
|
|
98
|
+
return {
|
|
99
|
+
...state,
|
|
100
|
+
[action.id]: {
|
|
101
|
+
id: action.id,
|
|
102
|
+
name: action.name,
|
|
103
|
+
args: action.args || {},
|
|
104
|
+
status: ToolState.STREAMING,
|
|
105
|
+
startTime: Date.now(),
|
|
106
|
+
streamingContent: action.content || "",
|
|
107
|
+
result: null,
|
|
108
|
+
error: null,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case toolActions.COMPLETE:
|
|
114
|
+
if (!state[action.id]) return state;
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
[action.id]: {
|
|
118
|
+
...state[action.id],
|
|
119
|
+
status: ToolState.COMPLETE,
|
|
120
|
+
result: action.result,
|
|
121
|
+
endTime: Date.now(),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
case toolActions.ERROR:
|
|
126
|
+
if (!state[action.id]) return state;
|
|
127
|
+
return {
|
|
128
|
+
...state,
|
|
129
|
+
[action.id]: {
|
|
130
|
+
...state[action.id],
|
|
131
|
+
status: ToolState.ERROR,
|
|
132
|
+
error: action.error,
|
|
133
|
+
endTime: Date.now(),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
case toolActions.CLEAR:
|
|
138
|
+
return {};
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Custom hook for chat state management
|
|
147
|
+
* @param {object} options - Configuration options
|
|
148
|
+
* @returns {object} - Chat state and methods
|
|
149
|
+
*/
|
|
150
|
+
export function useChatState(options = {}) {
|
|
151
|
+
const { initialChatId = null, initialModel = "claude-sonnet-4-20250514" } =
|
|
152
|
+
options;
|
|
153
|
+
|
|
154
|
+
// Chat metadata
|
|
155
|
+
const [currentChatId, setCurrentChatId] = useState(initialChatId);
|
|
156
|
+
const currentChatIdRef = useRef(initialChatId);
|
|
157
|
+
const [currentChatTitle, setCurrentChatTitle] = useState(null);
|
|
158
|
+
const [currentModel, setCurrentModel] = useState(initialModel);
|
|
159
|
+
|
|
160
|
+
// Messages (completed)
|
|
161
|
+
const [messages, setMessages] = useState([]);
|
|
162
|
+
|
|
163
|
+
// Generation state
|
|
164
|
+
const [generationState, setGenerationState] = useState(GenerationState.IDLE);
|
|
165
|
+
const [streamingContent, setStreamingContent] = useState("");
|
|
166
|
+
const [thinkingContent, setThinkingContent] = useState("");
|
|
167
|
+
const [thinkingStartTime, setThinkingStartTime] = useState(null);
|
|
168
|
+
|
|
169
|
+
// Ref to track current streaming content (avoids stale closure issues)
|
|
170
|
+
const streamingContentRef = useRef("");
|
|
171
|
+
|
|
172
|
+
// Ref to track current tools for message history
|
|
173
|
+
const toolsRef = useRef({});
|
|
174
|
+
|
|
175
|
+
// Tool executions
|
|
176
|
+
const [tools, dispatchTool] = useReducer(toolReducer, {});
|
|
177
|
+
|
|
178
|
+
// Stats from last generation
|
|
179
|
+
const [lastStats, setLastStats] = useState(null);
|
|
180
|
+
|
|
181
|
+
// Error state
|
|
182
|
+
const [error, setError] = useState(null);
|
|
183
|
+
|
|
184
|
+
// Keep refs in sync (must be after state declarations)
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
streamingContentRef.current = streamingContent;
|
|
187
|
+
}, [streamingContent]);
|
|
188
|
+
|
|
189
|
+
// Throttled flush so CLI re-renders during streaming (fixes "text only on frontend, full message at end on CLI")
|
|
190
|
+
// Without this, many WebSocket 'text' chunks in the same tick batch into one React update
|
|
191
|
+
const isStreaming =
|
|
192
|
+
generationState === GenerationState.THINKING ||
|
|
193
|
+
generationState === GenerationState.GENERATING ||
|
|
194
|
+
generationState === GenerationState.TOOL_EXECUTING;
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!isStreaming) return;
|
|
197
|
+
const interval = setInterval(() => {
|
|
198
|
+
const current = streamingContentRef.current;
|
|
199
|
+
setStreamingContent((prev) => (prev === current ? prev : current));
|
|
200
|
+
}, 50);
|
|
201
|
+
return () => clearInterval(interval);
|
|
202
|
+
}, [isStreaming]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
toolsRef.current = tools;
|
|
206
|
+
}, [tools]);
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Start a new generation
|
|
210
|
+
*/
|
|
211
|
+
const startGeneration = useCallback(() => {
|
|
212
|
+
setGenerationState(GenerationState.THINKING);
|
|
213
|
+
setStreamingContent("");
|
|
214
|
+
streamingContentRef.current = "";
|
|
215
|
+
setThinkingContent("");
|
|
216
|
+
setThinkingStartTime(Date.now());
|
|
217
|
+
setError(null);
|
|
218
|
+
dispatchTool({ type: toolActions.CLEAR });
|
|
219
|
+
}, []);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle thinking content
|
|
223
|
+
* @param {string} content - Thinking text chunk
|
|
224
|
+
* @param {number} totalChars - Total character count from server
|
|
225
|
+
*/
|
|
226
|
+
const handleThinking = useCallback(
|
|
227
|
+
(content, totalChars) => {
|
|
228
|
+
setThinkingContent((prev) => prev + content);
|
|
229
|
+
if (generationState === GenerationState.IDLE) {
|
|
230
|
+
setGenerationState(GenerationState.THINKING);
|
|
231
|
+
setThinkingStartTime(Date.now());
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
[generationState],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Handle text content
|
|
239
|
+
* @param {string} content - Text chunk
|
|
240
|
+
*/
|
|
241
|
+
const handleText = useCallback((content) => {
|
|
242
|
+
if (!content) return;
|
|
243
|
+
|
|
244
|
+
// Skip empty whitespace at start
|
|
245
|
+
if (streamingContentRef.current.length === 0 && !content.trim()) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setStreamingContent((prev) => {
|
|
250
|
+
const newContent = prev + content;
|
|
251
|
+
streamingContentRef.current = newContent;
|
|
252
|
+
return newContent;
|
|
253
|
+
});
|
|
254
|
+
setGenerationState((prev) =>
|
|
255
|
+
prev === GenerationState.THINKING ? GenerationState.GENERATING : prev,
|
|
256
|
+
);
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Handle tool start
|
|
261
|
+
* Transitions existing streaming entry to running, or creates a new one
|
|
262
|
+
* @param {object} data - Tool start data
|
|
263
|
+
*/
|
|
264
|
+
const handleToolStart = useCallback((data) => {
|
|
265
|
+
if (data.name === "set_title") return;
|
|
266
|
+
|
|
267
|
+
setGenerationState(GenerationState.TOOL_EXECUTING);
|
|
268
|
+
// START action in the reducer automatically cleans up streaming entries
|
|
269
|
+
// with the same tool name, so no explicit removal needed here
|
|
270
|
+
dispatchTool({
|
|
271
|
+
type: toolActions.START,
|
|
272
|
+
id: data.callId,
|
|
273
|
+
name: data.name,
|
|
274
|
+
args: data.args,
|
|
275
|
+
});
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle tool streaming update
|
|
280
|
+
* @param {object} data - Tool streaming data (has streamingId, not callId)
|
|
281
|
+
*/
|
|
282
|
+
const handleToolStreaming = useCallback((data) => {
|
|
283
|
+
if (data.name === "set_title" || data.name === "_preparing") return;
|
|
284
|
+
|
|
285
|
+
setGenerationState(GenerationState.TOOL_EXECUTING);
|
|
286
|
+
dispatchTool({
|
|
287
|
+
type: toolActions.STREAMING,
|
|
288
|
+
id: data.streamingId || data.callId || `streaming-${data.name}`,
|
|
289
|
+
name: data.name,
|
|
290
|
+
args: data.args,
|
|
291
|
+
content: data.streamingContent,
|
|
292
|
+
});
|
|
293
|
+
}, []);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Handle tool result
|
|
297
|
+
* @param {object} data - Tool result data
|
|
298
|
+
*/
|
|
299
|
+
const handleToolResult = useCallback((data) => {
|
|
300
|
+
dispatchTool({
|
|
301
|
+
type: toolActions.COMPLETE,
|
|
302
|
+
id: data.callId,
|
|
303
|
+
result: data.result,
|
|
304
|
+
});
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Handle tool error
|
|
309
|
+
* @param {object} data - Tool error data
|
|
310
|
+
*/
|
|
311
|
+
const handleToolError = useCallback((data) => {
|
|
312
|
+
dispatchTool({
|
|
313
|
+
type: toolActions.ERROR,
|
|
314
|
+
id: data.callId,
|
|
315
|
+
error: data.error,
|
|
316
|
+
});
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Handle generation done
|
|
321
|
+
* @param {object} data - Done data with stats
|
|
322
|
+
*/
|
|
323
|
+
const handleDone = useCallback(
|
|
324
|
+
(data) => {
|
|
325
|
+
// Use refs to get current values (avoids stale closure)
|
|
326
|
+
const currentContent = streamingContentRef.current;
|
|
327
|
+
const currentTools = toolsRef.current;
|
|
328
|
+
|
|
329
|
+
// Convert tools to array for storage, filtering out hidden tools
|
|
330
|
+
const toolsArray = Object.values(currentTools).filter(
|
|
331
|
+
(t) => t.name !== "set_title" && t.name !== "_preparing",
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Add assistant message to history (even if empty, to show tools)
|
|
335
|
+
if (currentContent || toolsArray.length > 0) {
|
|
336
|
+
setMessages((prev) => [
|
|
337
|
+
...prev,
|
|
338
|
+
{
|
|
339
|
+
id: `assistant-${Date.now()}`,
|
|
340
|
+
role: MessageRole.ASSISTANT,
|
|
341
|
+
content: currentContent || "",
|
|
342
|
+
model: data.message?.model || currentModel,
|
|
343
|
+
timestamp: new Date().toISOString(),
|
|
344
|
+
tools: toolsArray.length > 0 ? toolsArray : undefined,
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
setLastStats({
|
|
350
|
+
tps: data.tps,
|
|
351
|
+
numTokens: data.numTokens,
|
|
352
|
+
model: data.message?.model || currentModel,
|
|
353
|
+
finishReason: data.finishReason,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
setGenerationState(GenerationState.IDLE);
|
|
357
|
+
setStreamingContent("");
|
|
358
|
+
streamingContentRef.current = "";
|
|
359
|
+
setThinkingContent("");
|
|
360
|
+
setThinkingStartTime(null);
|
|
361
|
+
dispatchTool({ type: toolActions.CLEAR });
|
|
362
|
+
toolsRef.current = {};
|
|
363
|
+
},
|
|
364
|
+
[currentModel],
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Handle generation error
|
|
369
|
+
* @param {string} message - Error message
|
|
370
|
+
*/
|
|
371
|
+
const handleError = useCallback((message) => {
|
|
372
|
+
setGenerationState(GenerationState.ERROR);
|
|
373
|
+
setError(message);
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Handle generation stopped
|
|
378
|
+
* Preserves any partial content that was streamed
|
|
379
|
+
*/
|
|
380
|
+
const handleStopped = useCallback(() => {
|
|
381
|
+
// Get current content before clearing
|
|
382
|
+
const currentContent = streamingContentRef.current;
|
|
383
|
+
const currentTools = toolsRef.current;
|
|
384
|
+
|
|
385
|
+
// Convert tools to array for storage
|
|
386
|
+
const toolsArray = Object.values(currentTools).filter(
|
|
387
|
+
(t) => t.name !== "set_title" && t.name !== "_preparing",
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// If we have partial content, save it with a [stopped] indicator
|
|
391
|
+
if (currentContent || toolsArray.length > 0) {
|
|
392
|
+
const stoppedContent = currentContent
|
|
393
|
+
? currentContent + "\n\n*[Generation stopped]*"
|
|
394
|
+
: "*[Generation stopped]*";
|
|
395
|
+
|
|
396
|
+
setMessages((prev) => [
|
|
397
|
+
...prev,
|
|
398
|
+
{
|
|
399
|
+
id: `assistant-${Date.now()}`,
|
|
400
|
+
role: MessageRole.ASSISTANT,
|
|
401
|
+
content: stoppedContent,
|
|
402
|
+
timestamp: new Date().toISOString(),
|
|
403
|
+
tools: toolsArray.length > 0 ? toolsArray : undefined,
|
|
404
|
+
stopped: true,
|
|
405
|
+
},
|
|
406
|
+
]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
setGenerationState(GenerationState.STOPPED);
|
|
410
|
+
setStreamingContent("");
|
|
411
|
+
streamingContentRef.current = "";
|
|
412
|
+
setThinkingContent("");
|
|
413
|
+
setThinkingStartTime(null);
|
|
414
|
+
dispatchTool({ type: toolActions.CLEAR });
|
|
415
|
+
toolsRef.current = {};
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Handle chat title update
|
|
420
|
+
* @param {string} title - New title
|
|
421
|
+
*/
|
|
422
|
+
const handleTitle = useCallback((title) => {
|
|
423
|
+
setCurrentChatTitle(title);
|
|
424
|
+
}, []);
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Handle chat created event
|
|
428
|
+
* @param {number} chatId - New chat ID
|
|
429
|
+
*/
|
|
430
|
+
const handleChatCreated = useCallback((chatId) => {
|
|
431
|
+
currentChatIdRef.current = chatId;
|
|
432
|
+
setCurrentChatId(chatId);
|
|
433
|
+
}, []);
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Handle user message from another client (e.g., frontend)
|
|
437
|
+
* @param {object} data - Message data with chatId and content
|
|
438
|
+
*/
|
|
439
|
+
const handleUserMessage = useCallback(
|
|
440
|
+
(data) => {
|
|
441
|
+
const { chatId, content } = data;
|
|
442
|
+
const activeChatId = currentChatIdRef.current;
|
|
443
|
+
|
|
444
|
+
// Accept message if it's for the current chat, or if no chat is active yet
|
|
445
|
+
if (!activeChatId || chatId === activeChatId) {
|
|
446
|
+
if (!activeChatId && chatId) {
|
|
447
|
+
currentChatIdRef.current = chatId;
|
|
448
|
+
setCurrentChatId(chatId);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
setMessages((prev) => [
|
|
452
|
+
...prev,
|
|
453
|
+
{
|
|
454
|
+
id: `user-${Date.now()}`,
|
|
455
|
+
role: MessageRole.USER,
|
|
456
|
+
content,
|
|
457
|
+
source: "web",
|
|
458
|
+
timestamp: new Date().toISOString(),
|
|
459
|
+
},
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
setThinkingContent("");
|
|
463
|
+
setStreamingContent("");
|
|
464
|
+
streamingContentRef.current = "";
|
|
465
|
+
dispatchTool({ type: toolActions.CLEAR });
|
|
466
|
+
toolsRef.current = {};
|
|
467
|
+
setGenerationState(GenerationState.THINKING);
|
|
468
|
+
setThinkingStartTime(Date.now());
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
[],
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Handle chat selection from another client
|
|
476
|
+
* @param {number} chatId - Selected chat ID
|
|
477
|
+
*/
|
|
478
|
+
const handleChatSelected = useCallback(
|
|
479
|
+
(chatId) => {
|
|
480
|
+
if (chatId !== currentChatIdRef.current) {
|
|
481
|
+
currentChatIdRef.current = chatId;
|
|
482
|
+
setCurrentChatId(chatId);
|
|
483
|
+
setMessages([]);
|
|
484
|
+
setCurrentChatTitle(null);
|
|
485
|
+
setGenerationState(GenerationState.IDLE);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
[],
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Handle model change from another client
|
|
493
|
+
* @param {string} model - New model ID
|
|
494
|
+
*/
|
|
495
|
+
const handleModelChanged = useCallback((model) => {
|
|
496
|
+
setCurrentModel(model);
|
|
497
|
+
}, []);
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Add a user message
|
|
501
|
+
* @param {string} content - Message content
|
|
502
|
+
*/
|
|
503
|
+
const addUserMessage = useCallback((content) => {
|
|
504
|
+
setMessages((prev) => [
|
|
505
|
+
...prev,
|
|
506
|
+
{
|
|
507
|
+
id: `user-${Date.now()}`,
|
|
508
|
+
role: MessageRole.USER,
|
|
509
|
+
content,
|
|
510
|
+
timestamp: new Date().toISOString(),
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
}, []);
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Start a new chat
|
|
517
|
+
*/
|
|
518
|
+
const newChat = useCallback(() => {
|
|
519
|
+
currentChatIdRef.current = null;
|
|
520
|
+
setCurrentChatId(null);
|
|
521
|
+
setCurrentChatTitle(null);
|
|
522
|
+
setMessages([]);
|
|
523
|
+
setStreamingContent("");
|
|
524
|
+
setThinkingContent("");
|
|
525
|
+
setThinkingStartTime(null);
|
|
526
|
+
setGenerationState(GenerationState.IDLE);
|
|
527
|
+
setError(null);
|
|
528
|
+
dispatchTool({ type: toolActions.CLEAR });
|
|
529
|
+
}, []);
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Load a chat with messages
|
|
533
|
+
* @param {object} chat - Chat object with messages
|
|
534
|
+
*/
|
|
535
|
+
const loadChat = useCallback((chat) => {
|
|
536
|
+
currentChatIdRef.current = chat.id;
|
|
537
|
+
setCurrentChatId(chat.id);
|
|
538
|
+
setCurrentChatTitle(chat.title);
|
|
539
|
+
setMessages(chat.messages || []);
|
|
540
|
+
setGenerationState(GenerationState.IDLE);
|
|
541
|
+
setStreamingContent("");
|
|
542
|
+
setThinkingContent("");
|
|
543
|
+
setError(null);
|
|
544
|
+
dispatchTool({ type: toolActions.CLEAR });
|
|
545
|
+
}, []);
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Process a WebSocket message
|
|
549
|
+
* @param {object} data - Message data
|
|
550
|
+
*/
|
|
551
|
+
const processMessage = useCallback(
|
|
552
|
+
(data) => {
|
|
553
|
+
switch (data.type) {
|
|
554
|
+
// Chat lifecycle events
|
|
555
|
+
case "chat_created":
|
|
556
|
+
handleChatCreated(data.chatId);
|
|
557
|
+
break;
|
|
558
|
+
case "chat_selected":
|
|
559
|
+
handleChatSelected(data.chatId);
|
|
560
|
+
break;
|
|
561
|
+
|
|
562
|
+
// User message from another client (frontend)
|
|
563
|
+
case "user_message":
|
|
564
|
+
handleUserMessage(data);
|
|
565
|
+
break;
|
|
566
|
+
|
|
567
|
+
// Model change from another client
|
|
568
|
+
case "model_changed":
|
|
569
|
+
handleModelChanged(data.model);
|
|
570
|
+
break;
|
|
571
|
+
|
|
572
|
+
// Generation streaming events
|
|
573
|
+
case "thinking":
|
|
574
|
+
handleThinking(data.content, data.totalChars);
|
|
575
|
+
break;
|
|
576
|
+
case "text":
|
|
577
|
+
handleText(data.content);
|
|
578
|
+
break;
|
|
579
|
+
|
|
580
|
+
// Tool events
|
|
581
|
+
case "tool_start":
|
|
582
|
+
handleToolStart(data);
|
|
583
|
+
break;
|
|
584
|
+
case "tool_streaming":
|
|
585
|
+
handleToolStreaming(data);
|
|
586
|
+
break;
|
|
587
|
+
case "tool_result":
|
|
588
|
+
handleToolResult(data);
|
|
589
|
+
break;
|
|
590
|
+
case "tool_error":
|
|
591
|
+
handleToolError(data);
|
|
592
|
+
break;
|
|
593
|
+
|
|
594
|
+
// Chat metadata
|
|
595
|
+
case "title":
|
|
596
|
+
handleTitle(data.content);
|
|
597
|
+
break;
|
|
598
|
+
|
|
599
|
+
// Generation completion
|
|
600
|
+
case "done":
|
|
601
|
+
handleDone(data);
|
|
602
|
+
break;
|
|
603
|
+
case "error":
|
|
604
|
+
handleError(data.message);
|
|
605
|
+
break;
|
|
606
|
+
case "stopped":
|
|
607
|
+
handleStopped();
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
[
|
|
612
|
+
handleChatCreated,
|
|
613
|
+
handleChatSelected,
|
|
614
|
+
handleUserMessage,
|
|
615
|
+
handleModelChanged,
|
|
616
|
+
handleThinking,
|
|
617
|
+
handleText,
|
|
618
|
+
handleToolStart,
|
|
619
|
+
handleToolStreaming,
|
|
620
|
+
handleToolResult,
|
|
621
|
+
handleToolError,
|
|
622
|
+
handleTitle,
|
|
623
|
+
handleDone,
|
|
624
|
+
handleError,
|
|
625
|
+
handleStopped,
|
|
626
|
+
],
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
// Chat metadata
|
|
631
|
+
currentChatId,
|
|
632
|
+
currentChatTitle,
|
|
633
|
+
currentModel,
|
|
634
|
+
setCurrentModel,
|
|
635
|
+
|
|
636
|
+
// Messages
|
|
637
|
+
messages,
|
|
638
|
+
addUserMessage,
|
|
639
|
+
|
|
640
|
+
// Generation state
|
|
641
|
+
generationState,
|
|
642
|
+
isGenerating:
|
|
643
|
+
generationState !== GenerationState.IDLE &&
|
|
644
|
+
generationState !== GenerationState.STOPPED &&
|
|
645
|
+
generationState !== GenerationState.ERROR,
|
|
646
|
+
isThinking: generationState === GenerationState.THINKING,
|
|
647
|
+
streamingContent,
|
|
648
|
+
thinkingContent,
|
|
649
|
+
thinkingStartTime,
|
|
650
|
+
|
|
651
|
+
// Tools
|
|
652
|
+
tools,
|
|
653
|
+
activeTools: Object.values(tools).filter(
|
|
654
|
+
(t) => t.status === ToolState.RUNNING || t.status === ToolState.STREAMING,
|
|
655
|
+
),
|
|
656
|
+
|
|
657
|
+
// Stats
|
|
658
|
+
lastStats,
|
|
659
|
+
|
|
660
|
+
// Error
|
|
661
|
+
error,
|
|
662
|
+
|
|
663
|
+
// Actions
|
|
664
|
+
startGeneration,
|
|
665
|
+
newChat,
|
|
666
|
+
loadChat,
|
|
667
|
+
processMessage,
|
|
668
|
+
|
|
669
|
+
// For external state updates
|
|
670
|
+
setCurrentChatId,
|
|
671
|
+
setCurrentChatTitle,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export default useChatState;
|