import { createContext, useContext, useState, useCallback, type ReactNode } from "react" import { useEventHandler, type EventEmitter, type ServerEvent } from "../lib/api/events" import type { Message, Part, SDKMessage } from "../types/messages" import type { Permission } from "@opencode-ai/sdk/client" import * as Store from "../lib/messagesStore" import { sdk } from "../lib/api/sdkClient" import { useSession } from "./SessionContext" import { reloadPath } from "../lib/ideBridge" // Re-export types for convenience export type { Message, Part, SDKMessage } from "../types/messages" interface MessagesContextValue { messages: Message[] addMessage: (message: Message) => void updateMessage: (messageID: string, update: Partial) => void removeMessage: (messageID: string) => void addPart: (messageID: string, part: Part) => void updatePart: (messageID: string, partID: string, update: Partial) => void removePart: (messageID: string, partID: string) => void clearMessages: () => void getMessagesBySession: (sessionID: string) => Message[] loadSessionMessages: (sessionID: string) => Promise setMessages: (messages: Message[]) => void // permissions permissions: Permission[] getPermissionForCall: (sessionID: string, callID?: string | null) => Permission | undefined respondPermission: ( sessionID: string, permissionID: string, response: "once" | "always" | "reject", ) => Promise } const MessagesContext = createContext(undefined) interface MessagesProviderProps { children: ReactNode emitter?: EventEmitter | null | undefined } export function MessagesProvider({ children, emitter }: MessagesProviderProps) { const [messages, setMessages] = useState([]) const [permissions, setPermissions] = useState([]) const session = useSession() const setReasoning = session.setReasoning // Add or update a message const addMessage = useCallback((message: Message) => { setMessages((prev) => Store.upsertMessage(prev, message)) }, []) // Update a message const updateMessage = useCallback((messageID: string, update: Partial) => { setMessages((prev) => Store.updateMessage(prev, messageID, update)) }, []) // Remove a message const removeMessage = useCallback((messageID: string) => { setMessages((prev) => Store.removeMessage(prev, messageID)) }, []) // Add a part to a message const addPart = useCallback((messageID: string, part: Part) => { setMessages((prev) => Store.upsertPart(prev, messageID, part)) }, []) // Update a specific part in a message const updatePart = useCallback((messageID: string, partID: string, update: Partial) => { setMessages((prev) => Store.updatePart(prev, messageID, partID, update)) }, []) // Remove a part from a message const removePart = useCallback((messageID: string, partID: string) => { setMessages((prev) => Store.removePart(prev, messageID, partID)) }, []) // Clear all messages const clearMessages = useCallback(() => { setMessages([]) }, []) // Get messages for a specific session const getMessagesBySession = useCallback( (sessionID: string) => Store.getMessagesBySession(messages, sessionID), [messages], ) // Listen to message.updated events (also handles message creation) const handleMessageUpdated = useCallback((event: ServerEvent) => { if (event.type === "message.updated") { const { info } = event.properties as { info: SDKMessage } console.log("[MessagesContext] Message updated:", info.id, info.role) // updateMessageInfo creates the message if it doesn't exist setMessages((prev) => Store.updateMessageInfo(prev, info.id, info)) } }, []) // Listen to message.part.updated events const handlePartUpdated = useCallback( (event: ServerEvent) => { if (event.type === "message.part.updated") { const { part, delta } = event.properties as { part: Part; delta?: string } console.log( "[MessagesContext] Part updated:", part.id, part.type, delta ? `(delta: ${delta.length} chars)` : "", ) if (delta && part.type === "text") { // Apply delta for streaming text setMessages((prev) => Store.applyPartDelta(prev, part.messageID, part, delta)) } else { // No delta, just upsert the part normally addPart(part.messageID, part) } // Reload file in IDE when write/edit tool completes if (part.type === "tool") { const toolPart = part as { tool?: string; state?: { status?: string; input?: { filePath?: string } } } if ( (toolPart.tool === "write" || toolPart.tool === "edit") && toolPart.state?.status === "completed" && toolPart.state?.input?.filePath ) { reloadPath(toolPart.state.input.filePath, toolPart.tool) } } if (part.type === "reasoning") { //const time = (part as { time?: { end?: number } }).time //const end = typeof time?.end === 'number' setReasoning(part.sessionID, true) } else { setReasoning(part.sessionID, false) } } }, [addPart, setReasoning], ) // Listen to message.removed events const handleMessageRemoved = useCallback( (event: ServerEvent) => { if (event.type === "message.removed") { const { sessionID, messageID } = event.properties as { sessionID: string; messageID: string } console.log("[MessagesContext] Message removed:", messageID) removeMessage(messageID) setReasoning(sessionID, false) } }, [removeMessage, setReasoning], ) // Listen to message.part.removed events const handlePartRemoved = useCallback( (event: ServerEvent) => { if (event.type === "message.part.removed") { const { sessionID, messageID, partID } = event.properties as { sessionID: string messageID: string partID: string } console.log("[MessagesContext] Part removed:", partID) removePart(messageID, partID) setReasoning(sessionID, false) } }, [removePart, setReasoning], ) // Load messages for a session const loadSessionMessages = useCallback(async (sessionID: string) => { // Skip loading for virtual sessions (not yet persisted to server) if (sessionID.startsWith("virtual-")) { console.log("[MessagesContext] Skipping load for virtual session:", sessionID) return } console.log("[MessagesContext] Loading messages for session:", sessionID) try { const response = await sdk.session.messages({ path: { id: sessionID } }) if (response.error) { console.error("[MessagesContext] Failed to load messages:", response.error) return } if (response.data) { console.log("[MessagesContext] Messages loaded:", response.data.length) // SDK response is already in the correct format: Array<{ info: Message, parts: Array }> const loadedMessages: Message[] = response.data console.log("[MessagesContext] Loaded messages sample:", loadedMessages[0]) // Replace messages for this session only if we received any; otherwise keep existing local state setMessages((prev) => { if (!loadedMessages || loadedMessages.length === 0) return prev const filtered = prev.filter((msg) => msg.info.sessionID !== sessionID) return [...filtered, ...loadedMessages] }) try { const { currentSession, setIsIdle } = session if (currentSession?.id === sessionID) { const last = [...loadedMessages].sort((a, b) => a.info.time.created - b.info.time.created).at(-1) if (last) { const completed = (last.info as any)?.time?.completed const isAssistant = (last.info as any)?.role === "assistant" const busy = isAssistant && (!completed || completed === 0) setIsIdle(!busy) } } } catch (e) {} } } catch (err) { console.error("[MessagesContext] Failed to load messages:", err) } }, []) // Permission events const handlePermissionUpdated = useCallback((event: ServerEvent) => { if (event.type !== "permission.updated") return const perm = event.properties as Permission setPermissions((prev) => { const exists = prev.some((p) => p.id === perm.id) if (exists) return prev.map((p) => (p.id === perm.id ? perm : p)) return [...prev, perm] }) }, []) const handlePermissionReplied = useCallback((event: ServerEvent) => { if (event.type !== "permission.replied") return const { permissionID } = event.properties as { sessionID: string; permissionID: string; response: string } setPermissions((prev) => prev.filter((p) => p.id !== permissionID)) }, []) const getPermissionForCall = useCallback( (sessionID: string, callID?: string | null) => { if (!sessionID || !callID) return undefined // Match by session + callID return permissions.find((p) => p.sessionID === sessionID && p.callID === callID) }, [permissions], ) const respondPermission = useCallback( async (sessionID: string, permissionID: string, response: "once" | "always" | "reject") => { try { const result = await sdk.permissions.respond({ path: { id: sessionID, permissionID }, body: { response }, }) const ok = Boolean(result && "data" in result && (result as any).data === true) if (ok) setPermissions((prev) => prev.filter((p) => p.id !== permissionID)) return ok } catch (e) { return false } }, [], ) // Subscribe to events if emitter is provided useEventHandler(emitter ?? null, "message.updated", handleMessageUpdated) useEventHandler(emitter ?? null, "message.part.updated", handlePartUpdated) useEventHandler(emitter ?? null, "message.removed", handleMessageRemoved) useEventHandler(emitter ?? null, "message.part.removed", handlePartRemoved) useEventHandler(emitter ?? null, "permission.updated", handlePermissionUpdated) useEventHandler(emitter ?? null, "permission.replied", handlePermissionReplied) const value: MessagesContextValue = { messages, addMessage, updateMessage, removeMessage, addPart, updatePart, removePart, clearMessages, getMessagesBySession, loadSessionMessages, setMessages, permissions, getPermissionForCall, respondPermission, } return {children} } // eslint-disable-next-line react-refresh/only-export-components export function useMessages() { const context = useContext(MessagesContext) if (context === undefined) { throw new Error("useMessages must be used within a MessagesProvider") } return context }