diff --git a/src/components/chat/GlobalChatbox.tsx b/src/components/chat/GlobalChatbox.tsx index 51f7b2a..0ac1d9d 100644 --- a/src/components/chat/GlobalChatbox.tsx +++ b/src/components/chat/GlobalChatbox.tsx @@ -113,6 +113,7 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { const [conversationId, setConversationId] = useState(undefined); const abortRef = useRef(null); const bottomRef = useRef(null); + const inputRef = useRef(null); const theme = useTheme(); const canSend = useMemo(() => input.trim().length > 0 && !isStreaming, [input, isStreaming]); @@ -122,6 +123,14 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isStreaming]); + useEffect(() => { + if (!open) return; + const timer = window.setTimeout(() => { + inputRef.current?.focus(); + }, 0); + return () => window.clearTimeout(timer); + }, [open]); + const handleSend = async () => { const prompt = input.trim(); if (!prompt || isStreaming) return; @@ -443,6 +452,7 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { }} > setInput(e.target.value)} onKeyDown={(e) => { diff --git a/src/lib/chatStream.test.ts b/src/lib/chatStream.test.ts index b4bff7f..d477ce3 100644 --- a/src/lib/chatStream.test.ts +++ b/src/lib/chatStream.test.ts @@ -97,4 +97,18 @@ describe("streamCopilotChat", () => { { type: "error", message: "Login expired. Please sign in again.", detail: undefined }, ]); }); + + it("emits network error when fetch throws", async () => { + apiFetch.mockRejectedValue(new TypeError("Failed to fetch")); + + const events: Array<{ type: string; message?: string; detail?: string }> = []; + await streamCopilotChat({ + message: "hi", + onEvent: (event) => events.push(event), + }); + + expect(events).toEqual([ + { type: "error", message: "network request failed", detail: "Failed to fetch" }, + ]); + }); }); diff --git a/src/lib/chatStream.ts b/src/lib/chatStream.ts index 25492ee..b848f6d 100644 --- a/src/lib/chatStream.ts +++ b/src/lib/chatStream.ts @@ -38,19 +38,30 @@ export const streamCopilotChat = async ({ signal, onEvent, }: StreamOptions) => { - const response = await apiFetch(`${config.BACKEND_URL}/api/v1/copilot/chat/stream`, { - method: "POST", - signal, - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - }, - body: JSON.stringify({ - message, - conversation_id: conversationId, - }), - skipAuthRedirect: true, - }); + let response: Response; + try { + response = await apiFetch(`${config.BACKEND_URL}/api/v1/copilot/chat/stream`, { + method: "POST", + signal, + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + message, + conversation_id: conversationId, + }), + skipAuthRedirect: true, + }); + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + onEvent({ + type: "error", + message: "network request failed", + detail, + }); + return; + } if (!response.ok || !response.body) { const detail = await response.text();