import { createSignal } from "solid-js" import type { Session, Agent, Provider } from "../types/session" import type { Message } from "../types/message" import { instances } from "./instances" import { sseManager } from "../lib/sse-manager" interface SessionInfo { tokens: number cost: number contextWindow: number isSubscriptionModel: boolean } const [sessions, setSessions] = createSignal>>(new Map()) const [activeSessionId, setActiveSessionId] = createSignal>(new Map()) const [activeParentSessionId, setActiveParentSessionId] = createSignal>(new Map()) const [agents, setAgents] = createSignal>(new Map()) const [providers, setProviders] = createSignal>(new Map()) const [loading, setLoading] = createSignal({ fetchingSessions: new Map(), creatingSession: new Map(), deletingSession: new Map>(), loadingMessages: new Map>(), }) const [messagesLoaded, setMessagesLoaded] = createSignal>>(new Map()) const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal>(new Map()) async function fetchSessions(instanceId: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } setLoading((prev) => { const next = { ...prev } next.fetchingSessions.set(instanceId, true) return next }) try { const response = await instance.client.session.list() const sessionMap = new Map() if (!response.data || !Array.isArray(response.data)) { return } for (const apiSession of response.data) { sessionMap.set(apiSession.id, { id: apiSession.id, instanceId, title: apiSession.title || "Untitled", parentId: apiSession.parentID || null, agent: "", model: { providerId: "", modelId: "" }, time: { created: apiSession.time.created, updated: apiSession.time.updated, }, revert: apiSession.revert ? { messageID: apiSession.revert.messageID, partID: apiSession.revert.partID, snapshot: apiSession.revert.snapshot, diff: apiSession.revert.diff, } : undefined, messages: [], messagesInfo: new Map(), }) } setSessions((prev) => { const next = new Map(prev) next.set(instanceId, sessionMap) return next }) } catch (error) { console.error("Failed to fetch sessions:", error) throw error } finally { setLoading((prev) => { const next = { ...prev } next.fetchingSessions.set(instanceId, false) return next }) } } async function getDefaultModel( instanceId: string, agentName?: string, ): Promise<{ providerId: string; modelId: string }> { const instanceProviders = providers().get(instanceId) || [] const instanceAgents = agents().get(instanceId) || [] if (agentName) { const agent = instanceAgents.find((a) => a.name === agentName) if (agent?.model?.providerId && agent.model.modelId) { return { providerId: agent.model.providerId, modelId: agent.model.modelId, } } } const anthropicProvider = instanceProviders.find((p) => p.id === "anthropic") if (anthropicProvider) { const defaultModelId = anthropicProvider.defaultModelId || anthropicProvider.models[0]?.id if (defaultModelId) { return { providerId: "anthropic", modelId: defaultModelId, } } } if (instanceProviders.length > 0) { const firstProvider = instanceProviders[0] const defaultModelId = firstProvider.defaultModelId || firstProvider.models[0]?.id if (defaultModelId) { return { providerId: firstProvider.id, modelId: defaultModelId, } } } return { providerId: "", modelId: "" } } function updateSessionInfo(instanceId: string) { const instanceSessions = sessions().get(instanceId) if (!instanceSessions) return let totalTokens = 0 let totalCost = 0 let contextWindow = 0 let isSubscriptionModel = false let modelID = "" let providerID = "" // Calculate from last assistant message in each session (like original calculateSessionInfo) for (const session of instanceSessions.values()) { if (session.messagesInfo.size === 0) continue // Go backwards through messagesInfo to find the last relevant assistant message (like TUI) const messageArray = Array.from(session.messagesInfo.values()).reverse() for (const info of messageArray) { if (info.role === "assistant" && info.tokens) { const usage = info.tokens if (usage.output > 0) { if (info.summary) { // If summary message, only count output tokens and stop (like TUI) totalTokens = usage.output || 0 totalCost = info.cost || 0 } else { // Regular message - count all token types (like TUI) totalTokens = (usage.input || 0) + (usage.cache?.read || 0) + (usage.cache?.write || 0) + (usage.output || 0) + (usage.reasoning || 0) totalCost = info.cost || 0 } // Get model info for context window and subscription check modelID = info.modelID || "" providerID = info.providerID || "" isSubscriptionModel = totalCost === 0 break // Break after finding the last assistant message } } } } // Get context window from providers if (modelID && providerID) { const instanceProviders = providers().get(instanceId) || [] const provider = instanceProviders.find((p) => p.id === providerID) if (provider) { const model = provider.models.find((m) => m.id === modelID) if (model?.limit?.context) { contextWindow = model.limit.context } // Check if it's a subscription model (cost is 0 for both input and output) if (model?.cost?.input === 0 && model?.cost?.output === 0) { isSubscriptionModel = true } } } setSessionInfoByInstance((prev) => { const next = new Map(prev) next.set(instanceId, { tokens: totalTokens, cost: totalCost, contextWindow, isSubscriptionModel, }) return next }) } async function createSession(instanceId: string, agent?: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } const instanceAgents = agents().get(instanceId) || [] const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent") const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "") const defaultModel = await getDefaultModel(instanceId, selectedAgent) setLoading((prev) => { const next = { ...prev } next.creatingSession.set(instanceId, true) return next }) try { const response = await instance.client.session.create() if (!response.data) { throw new Error("Failed to create session: No data returned") } const session: Session = { id: response.data.id, instanceId, title: response.data.title || "New Session", parentId: null, agent: selectedAgent, model: defaultModel, time: { created: response.data.time.created, updated: response.data.time.updated, }, revert: response.data.revert ? { messageID: response.data.revert.messageID, partID: response.data.revert.partID, snapshot: response.data.revert.snapshot, diff: response.data.revert.diff, } : undefined, messages: [], messagesInfo: new Map(), } setSessions((prev) => { const next = new Map(prev) const instanceSessions = next.get(instanceId) || new Map() instanceSessions.set(session.id, session) next.set(instanceId, instanceSessions) return next }) updateSessionInfo(instanceId) return session } catch (error) { console.error("Failed to create session:", error) throw error } finally { setLoading((prev) => { const next = { ...prev } next.creatingSession.set(instanceId, false) return next }) } } async function deleteSession(instanceId: string, sessionId: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } setLoading((prev) => { const next = { ...prev } const deleting = next.deletingSession.get(instanceId) || new Set() deleting.add(sessionId) next.deletingSession.set(instanceId, deleting) return next }) try { await instance.client.session.delete({ path: { id: sessionId } }) setSessions((prev) => { const next = new Map(prev) const instanceSessions = next.get(instanceId) if (instanceSessions) { instanceSessions.delete(sessionId) } return next }) if (activeSessionId().get(instanceId) === sessionId) { setActiveSessionId((prev) => { const next = new Map(prev) next.delete(instanceId) return next }) } } catch (error) { console.error("Failed to delete session:", error) throw error } finally { setLoading((prev) => { const next = { ...prev } const deleting = next.deletingSession.get(instanceId) if (deleting) { deleting.delete(sessionId) } return next }) } } async function fetchAgents(instanceId: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } try { const response = await instance.client.app.agents() const agentList = (response.data ?? []).map((agent) => ({ name: agent.name, description: agent.description || "", mode: agent.mode, model: agent.model?.modelID ? { providerId: agent.model.providerID || "", modelId: agent.model.modelID, } : undefined, })) setAgents((prev) => { const next = new Map(prev) next.set(instanceId, agentList) return next }) } catch (error) { console.error("Failed to fetch agents:", error) } } async function fetchProviders(instanceId: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } try { const response = await instance.client.config.providers() if (!response.data) return const providerList = response.data.providers.map((provider) => ({ id: provider.id, name: provider.name, defaultModelId: response.data?.default?.[provider.id], models: Object.entries(provider.models).map(([id, model]) => ({ id, name: model.name, providerId: provider.id, limit: model.limit, cost: model.cost, })), })) setProviders((prev) => { const next = new Map(prev) next.set(instanceId, providerList) return next }) } catch (error) { console.error("Failed to fetch providers:", error) } } function setActiveSession(instanceId: string, sessionId: string): void { setActiveSessionId((prev) => { const next = new Map(prev) next.set(instanceId, sessionId) return next }) } function setActiveParentSession(instanceId: string, parentSessionId: string): void { setActiveParentSessionId((prev) => { const next = new Map(prev) next.set(instanceId, parentSessionId) return next }) setActiveSession(instanceId, parentSessionId) } function clearActiveParentSession(instanceId: string): void { setActiveParentSessionId((prev) => { const next = new Map(prev) next.delete(instanceId) return next }) setActiveSessionId((prev) => { const next = new Map(prev) next.delete(instanceId) return next }) } function getActiveParentSession(instanceId: string): Session | null { const parentId = activeParentSessionId().get(instanceId) if (!parentId) return null const instanceSessions = sessions().get(instanceId) return instanceSessions?.get(parentId) || null } function getActiveSession(instanceId: string): Session | null { const sessionId = activeSessionId().get(instanceId) if (!sessionId) return null const instanceSessions = sessions().get(instanceId) return instanceSessions?.get(sessionId) || null } function getSessions(instanceId: string): Session[] { const instanceSessions = sessions().get(instanceId) return instanceSessions ? Array.from(instanceSessions.values()) : [] } function getParentSessions(instanceId: string): Session[] { const allSessions = getSessions(instanceId) return allSessions.filter((s) => s.parentId === null) } function getChildSessions(instanceId: string, parentId: string): Session[] { const allSessions = getSessions(instanceId) return allSessions.filter((s) => s.parentId === parentId) } function getSessionFamily(instanceId: string, parentId: string): Session[] { const parent = sessions().get(instanceId)?.get(parentId) if (!parent) return [] const children = getChildSessions(instanceId, parentId) return [parent, ...children] } async function loadMessages(instanceId: string, sessionId: string, force = false): Promise { // If force reload, clear the loaded cache if (force) { setMessagesLoaded((prev) => { const next = new Map(prev) const loadedSet = next.get(instanceId) if (loadedSet) { loadedSet.delete(sessionId) } return next }) } const alreadyLoaded = messagesLoaded().get(instanceId)?.has(sessionId) if (alreadyLoaded && !force) { return } const isLoading = loading().loadingMessages.get(instanceId)?.has(sessionId) if (isLoading) { return } const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } const instanceSessions = sessions().get(instanceId) const session = instanceSessions?.get(sessionId) if (!session) { throw new Error("Session not found") } setLoading((prev) => { const next = { ...prev } const loadingSet = next.loadingMessages.get(instanceId) || new Set() loadingSet.add(sessionId) next.loadingMessages.set(instanceId, loadingSet) return next }) try { const response = await instance.client.session.messages({ path: { id: sessionId } }) if (!response.data || !Array.isArray(response.data)) { return } const messagesInfo = new Map() const messages: Message[] = response.data.map((apiMessage: any) => { const info = apiMessage.info || apiMessage const role = info.role || "assistant" const messageId = info.id || String(Date.now()) messagesInfo.set(messageId, info) return { id: messageId, sessionId, type: role === "user" ? "user" : "assistant", parts: apiMessage.parts || [], timestamp: info.time?.created || Date.now(), status: "complete" as const, } }) let agentName = "" let providerID = "" let modelID = "" for (let i = response.data.length - 1; i >= 0; i--) { const apiMessage = response.data[i] const info = apiMessage.info || apiMessage if (info.role === "assistant") { agentName = (info as any).mode || (info as any).agent || "" providerID = (info as any).providerID || "" modelID = (info as any).modelID || "" if (agentName && providerID && modelID) break } } if (!agentName && !providerID && !modelID) { const defaultModel = await getDefaultModel(instanceId, session.agent) agentName = session.agent providerID = defaultModel.providerId modelID = defaultModel.modelId } setSessions((prev) => { const next = new Map(prev) const instanceSessions = next.get(instanceId) if (instanceSessions) { const session = instanceSessions.get(sessionId) if (session) { const updatedSession = { ...session, messages, messagesInfo, agent: agentName || session.agent, model: providerID && modelID ? { providerId: providerID, modelId: modelID } : session.model, } const updatedInstanceSessions = new Map(instanceSessions) updatedInstanceSessions.set(sessionId, updatedSession) next.set(instanceId, updatedInstanceSessions) } } return next }) setMessagesLoaded((prev) => { const next = new Map(prev) const loadedSet = next.get(instanceId) || new Set() loadedSet.add(sessionId) next.set(instanceId, loadedSet) return next }) } catch (error) { console.error("Failed to load messages:", error) throw error } finally { setLoading((prev) => { const next = { ...prev } const loadingSet = next.loadingMessages.get(instanceId) if (loadingSet) { loadingSet.delete(sessionId) } return next }) } updateSessionInfo(instanceId) } function handleMessageUpdate(instanceId: string, event: any): void { const instanceSessions = sessions().get(instanceId) if (!instanceSessions) return if (event.type === "message.part.updated") { const part = event.properties?.part if (!part) return setSessions((prev) => { const next = new Map(prev) const instanceSessions = new Map(prev.get(instanceId)) const session = instanceSessions.get(part.sessionID) if (!session) return prev const messages = [...session.messages] const messageIndex = messages.findIndex((m) => m.id === part.messageID) if (messageIndex === -1) { messages.push({ id: part.messageID, sessionId: part.sessionID, type: "assistant", parts: [part], timestamp: Date.now(), status: "streaming", }) } else { const message = messages[messageIndex] const parts = [...message.parts] const partIndex = parts.findIndex((p: any) => p.id === part.id) if (partIndex === -1) { parts.push(part) } else { parts[partIndex] = part } messages[messageIndex] = { ...message, parts } } instanceSessions.set(part.sessionID, { ...session, messages }) next.set(instanceId, instanceSessions) return next }) updateSessionInfo(instanceId) } else if (event.type === "message.updated") { const info = event.properties?.info if (!info) return setSessions((prev) => { const next = new Map(prev) const instanceSessions = new Map(prev.get(instanceId)) const session = instanceSessions.get(info.sessionID) if (!session) return prev const messages = [...session.messages] const messageIndex = messages.findIndex((m) => m.id === info.id) const tempMessageIndex = messages.findIndex( (m) => m.id.startsWith("temp-") && m.type === (info.role === "user" ? "user" : "assistant") && m.status === "sending", ) if (messageIndex > -1) { messages[messageIndex] = { ...messages[messageIndex], status: "complete", } } else if (tempMessageIndex > -1) { messages[tempMessageIndex] = { id: info.id, sessionId: info.sessionID, type: info.role === "user" ? "user" : "assistant", parts: [], timestamp: info.time?.created || Date.now(), status: "complete", } } else { messages.push({ id: info.id, sessionId: info.sessionID, type: info.role === "user" ? "user" : "assistant", parts: [], timestamp: info.time?.created || Date.now(), status: "complete", }) } const messagesInfo = new Map(session.messagesInfo) messagesInfo.set(info.id, info) instanceSessions.set(info.sessionID, { ...session, messages, messagesInfo }) next.set(instanceId, instanceSessions) return next }) updateSessionInfo(instanceId) } } function handleSessionUpdate(instanceId: string, event: any): void { const info = event.properties?.info if (!info) return const instanceSessions = sessions().get(instanceId) if (!instanceSessions) return const existingSession = instanceSessions.get(info.id) if (!existingSession) { const newSession: Session = { id: info.id, instanceId, title: info.title || "Untitled", parentId: info.parentID || null, agent: info.agent || "", model: { providerId: info.model?.providerID || "", modelId: info.model?.modelID || "", }, time: { created: info.time?.created || Date.now(), updated: info.time?.updated || Date.now(), }, messages: [], messagesInfo: new Map(), } setSessions((prev) => { const next = new Map(prev) const instanceSessions = new Map(prev.get(instanceId)) instanceSessions.set(newSession.id, newSession) next.set(instanceId, instanceSessions) return next }) console.log(`[SSE] New session created: ${info.id}`, newSession) } else { const updatedSession = { ...existingSession, title: info.title || existingSession.title, agent: info.agent || existingSession.agent, model: info.model ? { providerId: info.model.providerID || existingSession.model.providerId, modelId: info.model.modelID || existingSession.model.modelId, } : existingSession.model, time: { ...existingSession.time, updated: info.time?.updated || Date.now(), }, revert: info.revert ? { messageID: info.revert.messageID, partID: info.revert.partID, snapshot: info.revert.snapshot, diff: info.revert.diff, } : undefined, } setSessions((prev) => { const next = new Map(prev) const instanceSessions = new Map(prev.get(instanceId)) instanceSessions.set(existingSession.id, updatedSession) next.set(instanceId, instanceSessions) return next }) } } async function sendMessage( instanceId: string, sessionId: string, prompt: string, attachments: any[] = [], ): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } const instanceSessions = sessions().get(instanceId) const session = instanceSessions?.get(sessionId) if (!session) { throw new Error("Session not found") } const tempMessageId = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}` const textParts: any[] = [] textParts.push({ type: "text" as const, text: prompt, id: `${tempMessageId}-text`, }) const optimisticMessage: Message = { id: tempMessageId, sessionId, type: "user", parts: textParts, timestamp: Date.now(), status: "sending", } setSessions((prev) => { const next = new Map(prev) const instanceSessions = new Map(prev.get(instanceId)) const session = instanceSessions.get(sessionId) if (session) { const messages = [...session.messages, optimisticMessage] instanceSessions.set(sessionId, { ...session, messages }) next.set(instanceId, instanceSessions) } return next }) const parts: any[] = [ { type: "text" as const, text: prompt, }, ] if (attachments.length > 0) { for (const att of attachments) { const source = att.source if (source.type === "file") { parts.push({ type: "file" as const, url: att.url, mime: source.mime, filename: att.filename, }) } else if (source.type === "text") { parts.push({ type: "text" as const, text: source.value, }) } } } const requestBody = { parts, ...(session.agent && { agent: session.agent }), ...(session.model.providerId && session.model.modelId && { model: { providerID: session.model.providerId, modelID: session.model.modelId, }, }), } console.log("[sendMessage] Sending prompt:", { sessionId, requestBody, }) try { const response = await instance.client.session.prompt({ path: { id: sessionId }, body: requestBody, }) console.log("[sendMessage] Response:", response) if (response.error) { console.error("[sendMessage] Server returned error:", response.error) throw new Error(JSON.stringify(response.error) || "Failed to send message") } } catch (error) { console.error("[sendMessage] Failed to send prompt:", error) throw error } } async function abortSession(instanceId: string, sessionId: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } console.log("[abortSession] Aborting session:", { instanceId, sessionId }) try { await instance.client.session.abort({ path: { id: sessionId }, }) console.log("[abortSession] Session aborted successfully") } catch (error) { console.error("[abortSession] Failed to abort session:", error) throw error } } async function updateSessionAgent(instanceId: string, sessionId: string, agent: string): Promise { const instanceSessions = sessions().get(instanceId) const session = instanceSessions?.get(sessionId) if (!session) { throw new Error("Session not found") } setSessions((prev) => { const next = new Map(prev) const instanceSessions = new Map(prev.get(instanceId)) const session = instanceSessions.get(sessionId) if (session) { instanceSessions.set(sessionId, { ...session, agent }) next.set(instanceId, instanceSessions) } return next }) } async function updateSessionModel( instanceId: string, sessionId: string, model: { providerId: string; modelId: string }, ): Promise { const instanceSessions = sessions().get(instanceId) const session = instanceSessions?.get(sessionId) if (!session) { throw new Error("Session not found") } setSessions((prev) => { const next = new Map(prev) const instanceSessions = new Map(prev.get(instanceId)) const session = instanceSessions.get(sessionId) if (session) { instanceSessions.set(sessionId, { ...session, model }) next.set(instanceId, instanceSessions) } return next }) } function handleSessionCompacted(instanceId: string, event: any): void { const sessionID = event.properties?.sessionID if (!sessionID) return console.log(`[SSE] Session compacted: ${sessionID}`) loadMessages(instanceId, sessionID, true).catch(console.error) } function handleSessionError(instanceId: string, event: any): void { const error = event.properties?.error const sessionID = event.properties?.sessionID console.error(`[SSE] Session error:`, error) let message = error?.data?.message || error?.message || "Unknown error" if (error?.data?.responseBody) { try { const body = JSON.parse(error.data.responseBody) if (body.error) { message = body.error } } catch {} } alert(`Error: ${message}`) } function handleMessageRemoved(instanceId: string, event: any): void { const sessionID = event.properties?.sessionID if (!sessionID) return console.log(`[SSE] Message removed from session ${sessionID}, reloading messages`) loadMessages(instanceId, sessionID, true).catch(console.error) } function handleMessagePartRemoved(instanceId: string, event: any): void { const sessionID = event.properties?.sessionID if (!sessionID) return console.log(`[SSE] Message part removed from session ${sessionID}, reloading messages`) loadMessages(instanceId, sessionID, true).catch(console.error) } sseManager.onMessageUpdate = handleMessageUpdate sseManager.onMessageRemoved = handleMessageRemoved sseManager.onMessagePartRemoved = handleMessagePartRemoved sseManager.onSessionUpdate = handleSessionUpdate sseManager.onSessionCompacted = handleSessionCompacted sseManager.onSessionError = handleSessionError export { sessions, activeSessionId, activeParentSessionId, agents, providers, loading, sessionInfoByInstance, fetchSessions, createSession, deleteSession, fetchAgents, fetchProviders, loadMessages, sendMessage, abortSession, setActiveSession, setActiveParentSession, clearActiveParentSession, getActiveSession, getActiveParentSession, getSessions, getParentSessions, getChildSessions, getSessionFamily, updateSessionAgent, updateSessionModel, getDefaultModel, }