| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- 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<Message>) => void
- removeMessage: (messageID: string) => void
- addPart: (messageID: string, part: Part) => void
- updatePart: (messageID: string, partID: string, update: Partial<Part>) => void
- removePart: (messageID: string, partID: string) => void
- clearMessages: () => void
- getMessagesBySession: (sessionID: string) => Message[]
- loadSessionMessages: (sessionID: string) => Promise<void>
- 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<boolean>
- }
- const MessagesContext = createContext<MessagesContextValue | undefined>(undefined)
- interface MessagesProviderProps {
- children: ReactNode
- emitter?: EventEmitter | null | undefined
- }
- export function MessagesProvider({ children, emitter }: MessagesProviderProps) {
- const [messages, setMessages] = useState<Message[]>([])
- const [permissions, setPermissions] = useState<Permission[]>([])
- 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<Message>) => {
- 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<Part>) => {
- 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<Part> }>
- 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 <MessagesContext.Provider value={value}>{children}</MessagesContext.Provider>
- }
- // 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
- }
|