777 lines
23 KiB
TypeScript
777 lines
23 KiB
TypeScript
import { mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
|
import type { Message } from "../types/message"
|
|
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
|
|
|
|
import { instances } from "./instances"
|
|
import { preferences, setAgentModelPreference } from "./preferences"
|
|
import {
|
|
activeSessionId,
|
|
agents,
|
|
clearSessionDraftPrompt,
|
|
getChildSessions,
|
|
isBlankSession,
|
|
messagesLoaded,
|
|
pruneDraftPrompts,
|
|
providers,
|
|
setActiveSessionId,
|
|
setAgents,
|
|
setMessagesLoaded,
|
|
setProviders,
|
|
setSessionInfoByInstance,
|
|
setSessions,
|
|
sessions,
|
|
withSession,
|
|
loading,
|
|
setLoading,
|
|
cleanupBlankSessions,
|
|
syncInstanceSessionIndicator,
|
|
} from "./session-state"
|
|
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
|
|
import { normalizeMessagePart } from "./message-v2/normalizers"
|
|
import { updateSessionInfo } from "./message-v2/session-info"
|
|
import { seedSessionMessagesV2, reconcilePendingPermissionsV2, reconcilePendingQuestionsV2 } from "./message-v2/bridge"
|
|
import { messageStoreBus } from "./message-v2/bus"
|
|
import { clearCacheForSession } from "../lib/global-cache"
|
|
import { getLogger } from "../lib/logger"
|
|
import { requestData } from "../lib/opencode-api"
|
|
import {
|
|
getOrCreateWorktreeClient,
|
|
getRootClient,
|
|
getWorktreeSlugForSession,
|
|
removeParentSessionMapping,
|
|
setWorktreeSlugForParentSession,
|
|
} from "./worktrees"
|
|
|
|
const log = getLogger("api")
|
|
|
|
const pendingSessionDiffFetches = new Map<string, Promise<void>>()
|
|
|
|
async function loadSessionDiff(instanceId: string, sessionId: string, force = false): Promise<void> {
|
|
if (!instanceId || !sessionId) return
|
|
|
|
const key = `${instanceId}:${sessionId}`
|
|
if (!force) {
|
|
const existing = sessions().get(instanceId)?.get(sessionId)
|
|
if (existing?.diff !== undefined) return
|
|
const pending = pendingSessionDiffFetches.get(key)
|
|
if (pending) return pending
|
|
}
|
|
|
|
const promise = (async () => {
|
|
const instance = instances().get(instanceId)
|
|
if (!instance?.client) return
|
|
|
|
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
|
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
|
|
|
try {
|
|
const diffs = await requestData<FileDiff[]>(
|
|
client.session.diff({ sessionID: sessionId }),
|
|
"session.diff",
|
|
)
|
|
|
|
if (!Array.isArray(diffs)) {
|
|
return
|
|
}
|
|
|
|
withSession(instanceId, sessionId, (session) => {
|
|
session.diff = diffs
|
|
})
|
|
} catch (error) {
|
|
log.warn("Failed to fetch session diff", { instanceId, sessionId, error })
|
|
}
|
|
})()
|
|
|
|
pendingSessionDiffFetches.set(key, promise)
|
|
void promise.finally(() => pendingSessionDiffFetches.delete(key))
|
|
return promise
|
|
}
|
|
|
|
interface SessionForkResponse {
|
|
id: string
|
|
title?: string
|
|
parentID?: string | null
|
|
agent?: string
|
|
model?: {
|
|
providerID?: string
|
|
modelID?: string
|
|
}
|
|
time?: {
|
|
created?: number
|
|
updated?: number
|
|
}
|
|
revert?: {
|
|
messageID?: string
|
|
partID?: string
|
|
snapshot?: string
|
|
diff?: string
|
|
}
|
|
}
|
|
|
|
async function fetchSessions(instanceId: string): Promise<void> {
|
|
const instance = instances().get(instanceId)
|
|
if (!instance || !instance.client) {
|
|
throw new Error("Instance not ready")
|
|
}
|
|
|
|
const rootClient = getRootClient(instanceId)
|
|
|
|
setLoading((prev) => {
|
|
const next = { ...prev }
|
|
next.fetchingSessions.set(instanceId, true)
|
|
return next
|
|
})
|
|
|
|
try {
|
|
log.info("session.list", { instanceId })
|
|
const response = await rootClient.session.list()
|
|
|
|
const sessionMap = new Map<string, Session>()
|
|
|
|
if (!response.data || !Array.isArray(response.data)) {
|
|
return
|
|
}
|
|
|
|
let statusById: Record<string, any> = {}
|
|
try {
|
|
const statusResponse = await rootClient.session.status()
|
|
if (statusResponse.data && typeof statusResponse.data === "object") {
|
|
statusById = statusResponse.data as Record<string, any>
|
|
}
|
|
} catch (error) {
|
|
log.error("Failed to fetch session status:", error)
|
|
}
|
|
|
|
const existingSessions = sessions().get(instanceId)
|
|
|
|
for (const apiSession of response.data) {
|
|
const existingSession = existingSessions?.get(apiSession.id)
|
|
const existingStatus = existingSession?.status
|
|
|
|
let status: SessionStatus
|
|
if (existingStatus === "compacting") {
|
|
status = "compacting"
|
|
} else {
|
|
const rawStatus = (apiSession as any)?.status ?? statusById[apiSession.id]
|
|
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
|
status = hasType ? mapSdkSessionStatus(rawStatus) : existingStatus ?? "idle"
|
|
}
|
|
|
|
sessionMap.set(apiSession.id, {
|
|
id: apiSession.id,
|
|
instanceId,
|
|
title: apiSession.title || "Untitled",
|
|
parentId: apiSession.parentID || null,
|
|
agent: existingSession?.agent ?? "",
|
|
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
|
status,
|
|
version: apiSession.version,
|
|
time: {
|
|
...apiSession.time,
|
|
},
|
|
revert: apiSession.revert
|
|
? {
|
|
messageID: apiSession.revert.messageID,
|
|
partID: apiSession.revert.partID,
|
|
snapshot: apiSession.revert.snapshot,
|
|
diff: apiSession.revert.diff,
|
|
}
|
|
: undefined,
|
|
})
|
|
}
|
|
|
|
const validSessionIds = new Set(sessionMap.keys())
|
|
|
|
setSessions((prev) => {
|
|
const next = new Map(prev)
|
|
next.set(instanceId, sessionMap)
|
|
return next
|
|
})
|
|
|
|
syncInstanceSessionIndicator(instanceId, sessionMap)
|
|
|
|
setMessagesLoaded((prev) => {
|
|
const next = new Map(prev)
|
|
const loadedSet = next.get(instanceId)
|
|
if (loadedSet) {
|
|
const filtered = new Set<string>()
|
|
for (const id of loadedSet) {
|
|
if (validSessionIds.has(id)) {
|
|
filtered.add(id)
|
|
}
|
|
}
|
|
next.set(instanceId, filtered)
|
|
}
|
|
return next
|
|
})
|
|
|
|
|
|
pruneDraftPrompts(instanceId, new Set(sessionMap.keys()))
|
|
} catch (error) {
|
|
log.error("Failed to fetch sessions:", error)
|
|
throw error
|
|
} finally {
|
|
setLoading((prev) => {
|
|
const next = { ...prev }
|
|
next.fetchingSessions.set(instanceId, false)
|
|
return next
|
|
})
|
|
}
|
|
}
|
|
|
|
async function createSession(instanceId: string, agent?: string): Promise<Session> {
|
|
const instance = instances().get(instanceId)
|
|
if (!instance || !instance.client) {
|
|
throw new Error("Instance not ready")
|
|
}
|
|
|
|
// New parent sessions inherit the currently active session's worktree.
|
|
// If no session is active (fresh instance), fall back to root.
|
|
const activeId = activeSessionId().get(instanceId)
|
|
const worktreeSlug = activeId && activeId !== "info" ? getWorktreeSlugForSession(instanceId, activeId) : "root"
|
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
|
|
|
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)
|
|
|
|
if (selectedAgent && isModelValid(instanceId, defaultModel)) {
|
|
await setAgentModelPreference(instanceId, selectedAgent, defaultModel)
|
|
}
|
|
|
|
setLoading((prev) => {
|
|
const next = { ...prev }
|
|
next.creatingSession.set(instanceId, true)
|
|
return next
|
|
})
|
|
|
|
try {
|
|
log.info(`[HTTP] POST /session.create for instance ${instanceId}`)
|
|
const response = await 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,
|
|
status: "idle",
|
|
version: response.data.version,
|
|
time: {
|
|
...response.data.time,
|
|
},
|
|
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,
|
|
}
|
|
|
|
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
|
|
})
|
|
|
|
syncInstanceSessionIndicator(instanceId)
|
|
|
|
const instanceProviders = providers().get(instanceId) || []
|
|
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
|
|
const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
|
|
const initialContextWindow = initialModel?.limit?.context ?? 0
|
|
const initialInputLimit = initialModel?.limit?.input ?? 0
|
|
const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
|
|
const initialOutputLimit =
|
|
initialModel?.limit?.output && initialModel.limit.output > 0
|
|
? initialModel.limit.output
|
|
: DEFAULT_MODEL_OUTPUT_LIMIT
|
|
const initialContextAvailable = initialInputLimit > 0 ? initialInputLimit : initialContextWindow > 0 ? initialContextWindow : null
|
|
|
|
setSessionInfoByInstance((prev) => {
|
|
const next = new Map(prev)
|
|
const instanceInfo = new Map(prev.get(instanceId))
|
|
instanceInfo.set(session.id, {
|
|
cost: 0,
|
|
contextWindow: initialContextWindow,
|
|
isSubscriptionModel: Boolean(initialSubscriptionModel),
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
reasoningTokens: 0,
|
|
actualUsageTokens: 0,
|
|
modelOutputLimit: initialOutputLimit,
|
|
contextAvailableTokens: initialContextAvailable,
|
|
})
|
|
next.set(instanceId, instanceInfo)
|
|
return next
|
|
})
|
|
|
|
if (preferences().autoCleanupBlankSessions) {
|
|
await cleanupBlankSessions(instanceId, session.id)
|
|
}
|
|
|
|
// Persist mapping for this *parent* session (best-effort).
|
|
await setWorktreeSlugForParentSession(instanceId, session.id, worktreeSlug).catch((error) => {
|
|
log.warn("Failed to persist session worktree mapping", { instanceId, sessionId: session.id, worktreeSlug, error })
|
|
})
|
|
|
|
return session
|
|
} catch (error) {
|
|
log.error("Failed to create session:", error)
|
|
throw error
|
|
} finally {
|
|
setLoading((prev) => {
|
|
const next = { ...prev }
|
|
next.creatingSession.set(instanceId, false)
|
|
return next
|
|
})
|
|
}
|
|
}
|
|
|
|
async function forkSession(
|
|
instanceId: string,
|
|
sourceSessionId: string,
|
|
options?: { messageId?: string },
|
|
): Promise<Session> {
|
|
const instance = instances().get(instanceId)
|
|
if (!instance || !instance.client) {
|
|
throw new Error("Instance not ready")
|
|
}
|
|
|
|
const worktreeSlug = getWorktreeSlugForSession(instanceId, sourceSessionId)
|
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
|
|
|
const request: { sessionID: string; messageID?: string } = {
|
|
sessionID: sourceSessionId,
|
|
messageID: options?.messageId,
|
|
}
|
|
|
|
log.info(`[HTTP] POST /session.fork for instance ${instanceId}`, request)
|
|
const info = await requestData<SessionForkResponse>(
|
|
client.session.fork(request),
|
|
"session.fork",
|
|
)
|
|
const forkedSession = {
|
|
id: info.id,
|
|
instanceId,
|
|
title: info.title || "Forked Session",
|
|
parentId: info.parentID || null,
|
|
agent: info.agent || "",
|
|
model: {
|
|
providerId: info.model?.providerID || "",
|
|
modelId: info.model?.modelID || "",
|
|
},
|
|
status: "idle",
|
|
version: "0",
|
|
time: info.time ? { ...info.time } : { created: Date.now(), updated: Date.now() },
|
|
revert: info.revert
|
|
? {
|
|
messageID: info.revert.messageID,
|
|
partID: info.revert.partID,
|
|
snapshot: info.revert.snapshot,
|
|
diff: info.revert.diff,
|
|
}
|
|
: undefined,
|
|
} as unknown as Session
|
|
|
|
setSessions((prev) => {
|
|
const next = new Map(prev)
|
|
const instanceSessions = next.get(instanceId) || new Map()
|
|
instanceSessions.set(forkedSession.id, forkedSession)
|
|
next.set(instanceId, instanceSessions)
|
|
return next
|
|
})
|
|
|
|
syncInstanceSessionIndicator(instanceId)
|
|
|
|
const instanceProviders = providers().get(instanceId) || []
|
|
const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
|
|
const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
|
|
const forkContextWindow = forkModel?.limit?.context ?? 0
|
|
const forkInputLimit = forkModel?.limit?.input ?? 0
|
|
const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
|
|
const forkOutputLimit =
|
|
forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
|
|
const forkContextAvailable = forkInputLimit > 0 ? forkInputLimit : forkContextWindow > 0 ? forkContextWindow : null
|
|
|
|
setSessionInfoByInstance((prev) => {
|
|
const next = new Map(prev)
|
|
const instanceInfo = new Map(prev.get(instanceId))
|
|
instanceInfo.set(forkedSession.id, {
|
|
cost: 0,
|
|
contextWindow: forkContextWindow,
|
|
isSubscriptionModel: Boolean(forkSubscriptionModel),
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
reasoningTokens: 0,
|
|
actualUsageTokens: 0,
|
|
modelOutputLimit: forkOutputLimit,
|
|
contextAvailableTokens: forkContextAvailable,
|
|
})
|
|
next.set(instanceId, instanceInfo)
|
|
return next
|
|
})
|
|
|
|
return forkedSession
|
|
}
|
|
|
|
async function deleteSession(instanceId: string, sessionId: string): Promise<void> {
|
|
const instance = instances().get(instanceId)
|
|
if (!instance || !instance.client) {
|
|
throw new Error("Instance not ready")
|
|
}
|
|
|
|
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
|
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
|
|
|
const deletingSession = sessions().get(instanceId)?.get(sessionId)
|
|
|
|
setLoading((prev) => {
|
|
const next = { ...prev }
|
|
const deleting = next.deletingSession.get(instanceId) || new Set()
|
|
deleting.add(sessionId)
|
|
next.deletingSession.set(instanceId, deleting)
|
|
return next
|
|
})
|
|
|
|
try {
|
|
log.info(`[HTTP] DELETE /session.delete for instance ${instanceId}`, { sessionId })
|
|
await requestData(client.session.delete({ sessionID: sessionId }), "session.delete")
|
|
|
|
setSessions((prev) => {
|
|
const next = new Map(prev)
|
|
const instanceSessions = next.get(instanceId)
|
|
if (instanceSessions) {
|
|
instanceSessions.delete(sessionId)
|
|
if (instanceSessions.size === 0) {
|
|
next.delete(instanceId)
|
|
}
|
|
}
|
|
return next
|
|
})
|
|
|
|
syncInstanceSessionIndicator(instanceId)
|
|
|
|
clearSessionDraftPrompt(instanceId, sessionId)
|
|
|
|
// Drop normalized message state and caches for this session
|
|
messageStoreBus.getOrCreate(instanceId).clearSession(sessionId)
|
|
clearCacheForSession(instanceId, sessionId)
|
|
|
|
setSessionInfoByInstance((prev) => {
|
|
const next = new Map(prev)
|
|
const instanceInfo = next.get(instanceId)
|
|
if (instanceInfo) {
|
|
const updatedInstanceInfo = new Map(instanceInfo)
|
|
updatedInstanceInfo.delete(sessionId)
|
|
if (updatedInstanceInfo.size === 0) {
|
|
next.delete(instanceId)
|
|
} else {
|
|
next.set(instanceId, updatedInstanceInfo)
|
|
}
|
|
}
|
|
return next
|
|
})
|
|
|
|
if (activeSessionId().get(instanceId) === sessionId) {
|
|
setActiveSessionId((prev) => {
|
|
const next = new Map(prev)
|
|
next.delete(instanceId)
|
|
return next
|
|
})
|
|
}
|
|
|
|
// Clean up mapping for deleted parent sessions.
|
|
if (deletingSession?.parentId === null) {
|
|
await removeParentSessionMapping(instanceId, sessionId).catch(() => undefined)
|
|
}
|
|
} catch (error) {
|
|
log.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<void> {
|
|
const instance = instances().get(instanceId)
|
|
if (!instance || !instance.client) {
|
|
throw new Error("Instance not ready")
|
|
}
|
|
|
|
const rootClient = getRootClient(instanceId)
|
|
|
|
try {
|
|
log.info(`[HTTP] GET /app.agents for instance ${instanceId}`)
|
|
const response = await rootClient.app.agents()
|
|
const agentList = (response.data ?? []).map((agent) => ({
|
|
name: agent.name,
|
|
description: agent.description || "",
|
|
mode: agent.mode,
|
|
hidden: agent.hidden,
|
|
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) {
|
|
log.error("Failed to fetch agents:", error)
|
|
}
|
|
}
|
|
|
|
async function fetchProviders(instanceId: string): Promise<void> {
|
|
const instance = instances().get(instanceId)
|
|
if (!instance || !instance.client) {
|
|
throw new Error("Instance not ready")
|
|
}
|
|
|
|
const rootClient = getRootClient(instanceId)
|
|
|
|
try {
|
|
log.info(`[HTTP] GET /config.providers for instance ${instanceId}`)
|
|
const response = await rootClient.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,
|
|
variantKeys: Object.keys(model.variants ?? {}),
|
|
})),
|
|
}))
|
|
|
|
setProviders((prev) => {
|
|
const next = new Map(prev)
|
|
next.set(instanceId, providerList)
|
|
return next
|
|
})
|
|
} catch (error) {
|
|
log.error("Failed to fetch providers:", error)
|
|
}
|
|
}
|
|
|
|
async function loadMessages(instanceId: string, sessionId: string, force = false): Promise<void> {
|
|
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 worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
|
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
|
|
|
const instanceSessions = sessions().get(instanceId)
|
|
const session = instanceSessions?.get(sessionId)
|
|
if (!session) {
|
|
throw new Error("Session not found")
|
|
}
|
|
|
|
// Fetch session-level diffs in the background once the session is opened.
|
|
void loadSessionDiff(instanceId, sessionId).catch((error) => {
|
|
log.warn("Failed to load session diff", { instanceId, sessionId, error })
|
|
})
|
|
|
|
setLoading((prev) => {
|
|
const next = { ...prev }
|
|
const loadingSet = next.loadingMessages.get(instanceId) || new Set()
|
|
loadingSet.add(sessionId)
|
|
next.loadingMessages.set(instanceId, loadingSet)
|
|
return next
|
|
})
|
|
|
|
try {
|
|
log.info(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId })
|
|
const apiMessages = await requestData<any[]>(
|
|
client.session.messages({ sessionID: sessionId }),
|
|
"session.messages",
|
|
)
|
|
|
|
if (!Array.isArray(apiMessages)) {
|
|
return
|
|
}
|
|
|
|
// Treat empty sessions as loaded to avoid re-fetch loops.
|
|
setMessagesLoaded((prev) => {
|
|
const next = new Map(prev)
|
|
const loadedSet = next.get(instanceId) || new Set()
|
|
loadedSet.add(sessionId)
|
|
next.set(instanceId, loadedSet)
|
|
return next
|
|
})
|
|
|
|
if (apiMessages.length === 0) {
|
|
return
|
|
}
|
|
|
|
const messagesInfo = new Map<string, any>()
|
|
const messages: Message[] = apiMessages.map((apiMessage: any) => {
|
|
const info = apiMessage.info || apiMessage
|
|
const role = info.role || "assistant"
|
|
const messageId = info.id || String(Date.now())
|
|
|
|
messagesInfo.set(messageId, info)
|
|
|
|
const parts: any[] = (apiMessage.parts || []).map((part: any) => normalizeMessagePart(part))
|
|
|
|
const message: Message = {
|
|
id: messageId,
|
|
sessionId,
|
|
type: role === "user" ? "user" : "assistant",
|
|
parts,
|
|
timestamp: info.time?.created || Date.now(),
|
|
status: "complete" as const,
|
|
version: 0,
|
|
}
|
|
|
|
return message
|
|
})
|
|
|
|
let agentName = ""
|
|
let providerID = ""
|
|
let modelID = ""
|
|
|
|
for (let i = apiMessages.length - 1; i >= 0; i--) {
|
|
const apiMessage = apiMessages[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 nextInstanceSessions = next.get(instanceId)
|
|
if (!nextInstanceSessions) {
|
|
return next
|
|
}
|
|
|
|
const existingSession = nextInstanceSessions.get(sessionId)
|
|
if (!existingSession) {
|
|
return next
|
|
}
|
|
|
|
const updatedSession = {
|
|
...existingSession,
|
|
agent: agentName || existingSession.agent,
|
|
model: providerID && modelID ? { providerId: providerID, modelId: modelID } : existingSession.model,
|
|
}
|
|
|
|
nextInstanceSessions.set(sessionId, updatedSession)
|
|
next.set(instanceId, nextInstanceSessions)
|
|
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
|
|
})
|
|
|
|
const sessionForV2 = sessions().get(instanceId)?.get(sessionId) ?? {
|
|
id: sessionId,
|
|
title: session?.title,
|
|
parentId: session?.parentId ?? null,
|
|
revert: session?.revert,
|
|
}
|
|
seedSessionMessagesV2(instanceId, sessionForV2, messages, messagesInfo)
|
|
|
|
// Permissions can be hydrated before messages/tool parts exist in the store.
|
|
// After message hydration, try to attach any pending permissions to tool-call part ids.
|
|
reconcilePendingPermissionsV2(instanceId, sessionId)
|
|
reconcilePendingQuestionsV2(instanceId, sessionId)
|
|
|
|
|
|
|
|
} catch (error) {
|
|
log.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, sessionId)
|
|
}
|
|
|
|
export {
|
|
createSession,
|
|
deleteSession,
|
|
fetchAgents,
|
|
fetchProviders,
|
|
|
|
fetchSessions,
|
|
forkSession,
|
|
loadMessages,
|
|
}
|