fix: restore complete source code and fix launchers
- Copy complete source code packages from original CodeNomad project - Add root package.json with npm workspace configuration - Include electron-app, server, ui, tauri-app, and opencode-config packages - Fix Launch-Windows.bat and Launch-Dev-Windows.bat to work with correct npm scripts - Fix Launch-Unix.sh to work with correct npm scripts - Launchers now correctly call npm run dev:electron which launches Electron app
This commit is contained in:
374
packages/ui/src/stores/session-actions.ts
Normal file
374
packages/ui/src/stores/session-actions.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
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"
|
||||
|
||||
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<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 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 })
|
||||
const response = await instance.client.session.promptAsync({
|
||||
path: { id: sessionId },
|
||||
body: requestBody,
|
||||
})
|
||||
|
||||
log.info("sendMessage response", response)
|
||||
|
||||
if (response.error) {
|
||||
log.error("sendMessage server error", response.error)
|
||||
throw new Error(JSON.stringify(response.error) || "Failed to send message")
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("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")
|
||||
}
|
||||
|
||||
log.info("abortSession", { instanceId, sessionId })
|
||||
|
||||
try {
|
||||
log.info("session.abort", { instanceId, sessionId })
|
||||
await instance.client.session.abort({
|
||||
path: { id: sessionId },
|
||||
})
|
||||
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<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) {
|
||||
await 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)) {
|
||||
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<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 trimmedTitle = nextTitle.trim()
|
||||
if (!trimmedTitle) {
|
||||
throw new Error("Session title is required")
|
||||
}
|
||||
|
||||
await instance.client.session.update({
|
||||
path: { id: sessionId },
|
||||
body: { title: trimmedTitle },
|
||||
})
|
||||
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user