Files
CodeNomad/packages/ui/src/stores/session-actions.ts
2025-11-17 12:06:58 +00:00

353 lines
9.0 KiB
TypeScript

import type { Message } from "../types/message"
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
import { instances } from "./instances"
import {
addRecentModelPreference,
preferences,
setAgentModelPreference,
} from "./preferences"
import { sessions, withSession } from "./session-state"
import { getDefaultModel, isModelValid } from "./session-models"
import {
computeDisplayParts,
getSessionIndex,
initializePartVersion,
updateSessionInfo,
} from "./session-messages"
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<number>(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<void> {
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 optimisticMessage: Message = {
id: messageId,
sessionId,
type: "user",
parts: optimisticParts,
timestamp: Date.now(),
status: "sending",
version: 0,
}
optimisticParts.forEach((part: any) => initializePartVersion(part))
optimisticMessage.displayParts = computeDisplayParts(optimisticMessage, preferences().showThinkingBlocks)
withSession(instanceId, sessionId, (session) => {
session.messages.push(optimisticMessage)
const index = getSessionIndex(instanceId, sessionId)
index.messageIndex.set(optimisticMessage.id, session.messages.length - 1)
})
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 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,
},
}),
}
console.log("[sendMessage] Sending prompt:", {
sessionId,
requestBody,
})
try {
console.log(`[HTTP] POST /session.prompt for instance ${instanceId}`, { sessionId, requestBody })
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 executeCustomCommand(
instanceId: string,
sessionId: string,
commandName: string,
args: string,
): Promise<void> {
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 instance.client.session.command({
path: { id: sessionId },
body,
})
}
async function runShellCommand(instanceId: string, sessionId: string, command: string): Promise<void> {
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 instance.client.session.shell({
path: { id: sessionId },
body: {
agent,
command,
},
})
}
async function abortSession(instanceId: string, sessionId: string): Promise<void> {
const instance = instances().get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
console.log("[abortSession] Aborting session:", { instanceId, sessionId })
try {
console.log(`[HTTP] POST /session.abort for instance ${instanceId}`, { sessionId })
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<void> {
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) {
setAgentModelPreference(instanceId, agent, nextModel)
}
if (shouldApplyModel) {
updateSessionInfo(instanceId, sessionId)
}
}
async function updateSessionModel(
instanceId: string,
sessionId: string,
model: { providerId: string; modelId: string },
): Promise<void> {
const instanceSessions = sessions().get(instanceId)
const session = instanceSessions?.get(sessionId)
if (!session) {
throw new Error("Session not found")
}
if (!isModelValid(instanceId, model)) {
console.warn("Invalid model selection", model)
return
}
withSession(instanceId, sessionId, (current) => {
current.model = model
})
if (session.agent) {
setAgentModelPreference(instanceId, session.agent, model)
}
addRecentModelPreference(model)
updateSessionInfo(instanceId, sessionId)
}
export {
abortSession,
executeCustomCommand,
runShellCommand,
sendMessage,
updateSessionAgent,
updateSessionModel,
}