import { resolvePastedPlaceholders } from "../lib/prompt-placeholders" import { instances } from "./instances" import { addRecentModelPreference, setAgentModelPreference } from "./preferences" import { sessions, withSession } from "./session-state" import { getDefaultModel, isModelValid } from "./session-models" import { updateSessionInfo } from "./message-v2/session-info" import { messageStoreBus } from "./message-v2/bus" import { getLogger } from "../lib/logger" import { requestData } from "../lib/opencode-api" const log = getLogger("actions") const ID_LENGTH = 26 const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" let lastTimestamp = 0 let localCounter = 0 function randomBase62(length: number): string { let result = "" const cryptoObj = (globalThis as unknown as { crypto?: Crypto }).crypto if (cryptoObj && typeof cryptoObj.getRandomValues === "function") { const bytes = new Uint8Array(length) cryptoObj.getRandomValues(bytes) for (let i = 0; i < length; i++) { result += BASE62_CHARS[bytes[i] % BASE62_CHARS.length] } } else { for (let i = 0; i < length; i++) { const idx = Math.floor(Math.random() * BASE62_CHARS.length) result += BASE62_CHARS[idx] } } return result } function createId(prefix: string): string { const timestamp = Date.now() if (timestamp !== lastTimestamp) { lastTimestamp = timestamp localCounter = 0 } localCounter++ const value = (BigInt(timestamp) << BigInt(12)) + BigInt(localCounter) const bytes = new Array(6) for (let i = 0; i < 6; i++) { const shift = BigInt(8 * (5 - i)) bytes[i] = Number((value >> shift) & BigInt(0xff)) } const hex = bytes.map((b) => b.toString(16).padStart(2, "0")).join("") const random = randomBase62(ID_LENGTH - 12) return `${prefix}_${hex}${random}` } 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 messageId = createId("msg") const textPartId = createId("part") const resolvedPrompt = resolvePastedPlaceholders(prompt, attachments) const optimisticParts: any[] = [ { id: textPartId, type: "text" as const, text: resolvedPrompt, synthetic: true, renderCache: undefined, }, ] const requestParts: any[] = [ { id: textPartId, type: "text" as const, text: resolvedPrompt, }, ] if (attachments.length > 0) { for (const att of attachments) { const source = att.source if (source.type === "file") { const partId = createId("part") requestParts.push({ id: partId, type: "file" as const, url: att.url, mime: source.mime, filename: att.filename, }) optimisticParts.push({ id: partId, type: "file" as const, url: att.url, mime: source.mime, filename: att.filename, synthetic: true, }) } else if (source.type === "text") { const display: string | undefined = att.display const value: unknown = source.value const isPastedPlaceholder = typeof display === "string" && /^pasted #\d+/.test(display) if (isPastedPlaceholder || typeof value !== "string") { continue } const partId = createId("part") requestParts.push({ id: partId, type: "text" as const, text: value, }) optimisticParts.push({ id: partId, type: "text" as const, text: value, synthetic: true, renderCache: undefined, }) } } } const store = messageStoreBus.getOrCreate(instanceId) const createdAt = Date.now() store.upsertMessage({ id: messageId, sessionId, role: "user", status: "sending", parts: optimisticParts, createdAt, updatedAt: createdAt, isEphemeral: true, }) withSession(instanceId, sessionId, () => { /* trigger reactivity for legacy session data */ }) const requestBody = { messageID: messageId, parts: requestParts, ...(session.agent && { agent: session.agent }), ...(session.model.providerId && session.model.modelId && { model: { providerID: session.model.providerId, modelID: session.model.modelId, }, }), } log.info("sendMessage", { instanceId, sessionId, requestBody, }) try { log.info("session.promptAsync", { instanceId, sessionId, requestBody }) await requestData( instance.client.session.promptAsync({ sessionID: sessionId, ...(requestBody as any), }), "session.promptAsync", ) } catch (error) { log.error("Failed to send prompt", error) throw error } } async function executeCustomCommand( instanceId: string, sessionId: string, commandName: string, args: string, ): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } const session = sessions().get(instanceId)?.get(sessionId) if (!session) { throw new Error("Session not found") } const body: { command: string arguments: string messageID: string agent?: string model?: string } = { command: commandName, arguments: args, messageID: createId("msg"), } if (session.agent) { body.agent = session.agent } if (session.model.providerId && session.model.modelId) { body.model = `${session.model.providerId}/${session.model.modelId}` } await requestData( instance.client.session.command({ sessionID: sessionId, ...(body as any), }), "session.command", ) } async function runShellCommand(instanceId: string, sessionId: string, command: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } const session = sessions().get(instanceId)?.get(sessionId) if (!session) { throw new Error("Session not found") } const agent = session.agent || "build" await requestData( instance.client.session.shell({ sessionID: sessionId, agent, command, }), "session.shell", ) } async function abortSession(instanceId: string, sessionId: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } log.info("abortSession", { instanceId, sessionId }) try { log.info("session.abort", { instanceId, sessionId }) await requestData( instance.client.session.abort({ sessionID: sessionId, }), "session.abort", ) log.info("abortSession complete", { instanceId, sessionId }) } catch (error) { log.error("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") } const nextModel = await getDefaultModel(instanceId, agent) const shouldApplyModel = isModelValid(instanceId, nextModel) withSession(instanceId, sessionId, (current) => { current.agent = agent if (shouldApplyModel) { current.model = nextModel } }) if (agent && shouldApplyModel) { await setAgentModelPreference(instanceId, agent, nextModel) } if (shouldApplyModel) { updateSessionInfo(instanceId, sessionId) } } 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") } if (!isModelValid(instanceId, model)) { log.warn("Invalid model selection", model) return } withSession(instanceId, sessionId, (current) => { current.model = model }) if (session.agent) { await setAgentModelPreference(instanceId, session.agent, model) } addRecentModelPreference(model) updateSessionInfo(instanceId, sessionId) } async function renameSession(instanceId: string, sessionId: string, nextTitle: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { throw new Error("Instance not ready") } const session = sessions().get(instanceId)?.get(sessionId) if (!session) { throw new Error("Session not found") } const trimmedTitle = nextTitle.trim() if (!trimmedTitle) { throw new Error("Session title is required") } await requestData( instance.client.session.update({ sessionID: sessionId, title: trimmedTitle, }), "session.update", ) withSession(instanceId, sessionId, (current) => { current.title = trimmedTitle const time = { ...(current.time ?? {}) } time.updated = Date.now() current.time = time }) } export { abortSession, executeCustomCommand, renameSession, runShellCommand, sendMessage, updateSessionAgent, updateSessionModel, }