Compare commits
2 Commits
610057c058
...
eaf93e2924
@@ -105,6 +105,42 @@ export function registerNativeSessionsRoutes(app: FastifyInstance, deps: NativeS
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Fork a session
|
||||||
|
app.post<{
|
||||||
|
Params: { workspaceId: string; sessionId: string }
|
||||||
|
}>("/api/native/workspaces/:workspaceId/sessions/:sessionId/fork", async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const session = await sessionManager.forkSession(
|
||||||
|
request.params.workspaceId,
|
||||||
|
request.params.sessionId
|
||||||
|
)
|
||||||
|
return { session }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "Failed to fork session")
|
||||||
|
reply.code(500)
|
||||||
|
return { error: "Failed to fork session" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Revert a session
|
||||||
|
app.post<{
|
||||||
|
Params: { workspaceId: string; sessionId: string }
|
||||||
|
Body: { messageId?: string }
|
||||||
|
}>("/api/native/workspaces/:workspaceId/sessions/:sessionId/revert", async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const session = await sessionManager.revert(
|
||||||
|
request.params.workspaceId,
|
||||||
|
request.params.sessionId,
|
||||||
|
request.body.messageId
|
||||||
|
)
|
||||||
|
return { session }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "Failed to revert session")
|
||||||
|
reply.code(500)
|
||||||
|
return { error: "Failed to revert session" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Delete a session
|
// Delete a session
|
||||||
app.delete<{ Params: { workspaceId: string; sessionId: string } }>("/api/native/workspaces/:workspaceId/sessions/:sessionId", async (request, reply) => {
|
app.delete<{ Params: { workspaceId: string; sessionId: string } }>("/api/native/workspaces/:workspaceId/sessions/:sessionId", async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -200,6 +200,54 @@ export class NativeSessionManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async forkSession(workspaceId: string, sessionId: string): Promise<Session> {
|
||||||
|
const store = await this.loadStore(workspaceId)
|
||||||
|
const original = store.sessions[sessionId]
|
||||||
|
if (!original) throw new Error(`Session not found: ${sessionId}`)
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const forked: Session = {
|
||||||
|
...original,
|
||||||
|
id: ulid(),
|
||||||
|
title: original.title ? `${original.title} (fork)` : "Forked Session",
|
||||||
|
parentId: original.parentId || original.id,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
messageIds: [...original.messageIds], // Shallow copy of message IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
store.sessions[forked.id] = forked
|
||||||
|
await this.saveStore(workspaceId)
|
||||||
|
return forked
|
||||||
|
}
|
||||||
|
|
||||||
|
async revert(workspaceId: string, sessionId: string, messageId?: string): Promise<Session> {
|
||||||
|
const store = await this.loadStore(workspaceId)
|
||||||
|
const session = store.sessions[sessionId]
|
||||||
|
if (!session) throw new Error(`Session not found: ${sessionId}`)
|
||||||
|
|
||||||
|
if (!messageId) {
|
||||||
|
// Revert last message
|
||||||
|
if (session.messageIds.length > 0) {
|
||||||
|
const lastId = session.messageIds.pop()
|
||||||
|
if (lastId) delete store.messages[lastId]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Revert to specific message
|
||||||
|
const index = session.messageIds.indexOf(messageId)
|
||||||
|
if (index !== -1) {
|
||||||
|
const toDelete = session.messageIds.splice(index + 1)
|
||||||
|
for (const id of toDelete) {
|
||||||
|
delete store.messages[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.updatedAt = Date.now()
|
||||||
|
await this.saveStore(workspaceId)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
// Message operations
|
// Message operations
|
||||||
|
|
||||||
async getSessionMessages(workspaceId: string, sessionId: string): Promise<SessionMessage[]> {
|
async getSessionMessages(workspaceId: string, sessionId: string): Promise<SessionMessage[]> {
|
||||||
|
|||||||
@@ -601,7 +601,7 @@ You are committed to excellence and take pride in delivering code that professio
|
|||||||
<div class="px-3 py-1.5 flex items-center justify-between border-t border-white/5 bg-zinc-950/30">
|
<div class="px-3 py-1.5 flex items-center justify-between border-t border-white/5 bg-zinc-950/30">
|
||||||
<span class="text-[9px] font-bold text-zinc-500 uppercase tracking-widest">Saved Agents</span>
|
<span class="text-[9px] font-bold text-zinc-500 uppercase tracking-widest">Saved Agents</span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); loadAgents(); fetchAgents(); }}
|
onClick={(e) => { e.stopPropagation(); loadAgents(); fetchAgents(props.instanceId); }}
|
||||||
class="p-1 hover:bg-white/5 rounded text-zinc-500 hover:text-zinc-300 transition-colors"
|
class="p-1 hover:bg-white/5 rounded text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||||
title="Refresh agents"
|
title="Refresh agents"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ interface ToolCallProps {
|
|||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -671,6 +671,7 @@ export default function ToolCall(props: ToolCallProps) {
|
|||||||
<Markdown
|
<Markdown
|
||||||
part={markdownPart}
|
part={markdownPart}
|
||||||
isDark={isDark()}
|
isDark={isDark()}
|
||||||
|
instanceId={props.instanceId}
|
||||||
disableHighlight={disableHighlight}
|
disableHighlight={disableHighlight}
|
||||||
onRendered={handleMarkdownRendered}
|
onRendered={handleMarkdownRendered}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -138,6 +138,26 @@ export const nativeSessionApi = {
|
|||||||
return response.ok || response.status === 204
|
return response.ok || response.status === 204
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async forkSession(workspaceId: string, sessionId: string): Promise<NativeSession> {
|
||||||
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/fork`, {
|
||||||
|
method: "POST"
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error("Failed to fork session")
|
||||||
|
const data = await response.json()
|
||||||
|
return data.session
|
||||||
|
},
|
||||||
|
|
||||||
|
async revertSession(workspaceId: string, sessionId: string, messageId?: string): Promise<NativeSession> {
|
||||||
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/revert`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ messageId })
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error("Failed to revert session")
|
||||||
|
const data = await response.json()
|
||||||
|
return data.session
|
||||||
|
},
|
||||||
|
|
||||||
async getMessages(workspaceId: string, sessionId: string): Promise<NativeMessage[]> {
|
async getMessages(workspaceId: string, sessionId: string): Promise<NativeMessage[]> {
|
||||||
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/messages`)
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/messages`)
|
||||||
if (!response.ok) throw new Error("Failed to get messages")
|
if (!response.ok) throw new Error("Failed to get messages")
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
import { createSignal, createMemo, batch } from "solid-js"
|
import { createSignal, createMemo, batch } from "solid-js"
|
||||||
import type { Session } from "../types/session"
|
import type { Session } from "../types/session"
|
||||||
import type { Message, Part } from "../types/message"
|
import type { Message } from "../types/message"
|
||||||
import { nativeSessionApi, isLiteMode, NativeSession, NativeMessage } from "../lib/lite-mode"
|
import { nativeSessionApi, isLiteMode } from "../lib/lite-mode"
|
||||||
|
import type { NativeSession, NativeMessage } from "../lib/lite-mode"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
|
|
||||||
const log = getLogger("native-sessions")
|
const log = getLogger("native-sessions")
|
||||||
@@ -53,24 +54,29 @@ export function forceLiteMode(enabled: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert native session to UI session format
|
// Convert native session to UI session format
|
||||||
function nativeToUiSession(native: NativeSession): Session {
|
function nativeToUiSession(native: NativeSession, workspaceId?: string): Session {
|
||||||
return {
|
return {
|
||||||
id: native.id,
|
id: native.id,
|
||||||
title: native.title,
|
instanceId: workspaceId || native.workspaceId,
|
||||||
parentId: native.parentId ?? undefined,
|
title: native.title || "",
|
||||||
createdAt: native.createdAt,
|
parentId: native.parentId ?? null,
|
||||||
updatedAt: native.updatedAt,
|
agent: native.agent || "Assistant",
|
||||||
agent: native.agent,
|
|
||||||
model: native.model ? {
|
model: native.model ? {
|
||||||
providerId: native.model.providerId,
|
providerId: native.model.providerId,
|
||||||
modelId: native.model.modelId,
|
modelId: native.model.modelId,
|
||||||
} : undefined,
|
} : { providerId: "", modelId: "" },
|
||||||
|
version: "0",
|
||||||
|
time: {
|
||||||
|
created: native.createdAt,
|
||||||
|
updated: native.updatedAt
|
||||||
|
},
|
||||||
|
skills: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert native message to UI message format
|
// Convert native message to UI message format
|
||||||
function nativeToUiMessage(native: NativeMessage): Message {
|
function nativeToUiMessage(native: NativeMessage): Message {
|
||||||
const parts: Part[] = []
|
const parts: any[] = []
|
||||||
|
|
||||||
if (native.content) {
|
if (native.content) {
|
||||||
parts.push({
|
parts.push({
|
||||||
@@ -82,19 +88,22 @@ function nativeToUiMessage(native: NativeMessage): Message {
|
|||||||
return {
|
return {
|
||||||
id: native.id,
|
id: native.id,
|
||||||
sessionId: native.sessionId,
|
sessionId: native.sessionId,
|
||||||
role: native.role,
|
type: native.role === "user" ? "user" : "assistant",
|
||||||
createdAt: native.createdAt,
|
|
||||||
parts,
|
parts,
|
||||||
|
timestamp: native.createdAt,
|
||||||
|
status: native.status === "completed" ? "complete" : "streaming",
|
||||||
|
version: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch sessions from native API
|
* Fetch sessions from native API
|
||||||
*/
|
*/
|
||||||
export async function fetchNativeSessions(workspaceId: string): Promise<Session[]> {
|
export async function fetchNativeSessions(workspaceId: string): Promise<Session[]> {
|
||||||
try {
|
try {
|
||||||
const sessions = await nativeSessionApi.listSessions(workspaceId)
|
const sessions = await nativeSessionApi.listSessions(workspaceId)
|
||||||
const uiSessions = sessions.map(nativeToUiSession)
|
const uiSessions = sessions.map(s => nativeToUiSession(s, workspaceId))
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
setNativeSessions(prev => {
|
setNativeSessions(prev => {
|
||||||
@@ -227,9 +236,11 @@ export async function sendNativeMessage(
|
|||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: `temp-${Date.now()}`,
|
id: `temp-${Date.now()}`,
|
||||||
sessionId,
|
sessionId,
|
||||||
role: "user",
|
type: "user",
|
||||||
createdAt: Date.now(),
|
timestamp: Date.now(),
|
||||||
parts: [{ type: "text", text: content }],
|
parts: [{ type: "text", text: content } as any],
|
||||||
|
status: "complete",
|
||||||
|
version: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = `${workspaceId}:${sessionId}`
|
const key = `${workspaceId}:${sessionId}`
|
||||||
@@ -264,9 +275,11 @@ export async function sendNativeMessage(
|
|||||||
const assistantMessage: Message = {
|
const assistantMessage: Message = {
|
||||||
id: `msg-${Date.now()}`,
|
id: `msg-${Date.now()}`,
|
||||||
sessionId,
|
sessionId,
|
||||||
role: "assistant",
|
type: "assistant",
|
||||||
createdAt: Date.now(),
|
timestamp: Date.now(),
|
||||||
parts: [{ type: "text", text: fullContent }],
|
parts: [{ type: "text", text: fullContent } as any],
|
||||||
|
status: "complete",
|
||||||
|
version: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
setNativeMessages(prev => {
|
setNativeMessages(prev => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { instances, activeInstanceId } from "./instances"
|
|||||||
import { addTaskMessage } from "./task-actions"
|
import { addTaskMessage } from "./task-actions"
|
||||||
|
|
||||||
import { addRecentModelPreference, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
import { addRecentModelPreference, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
||||||
import { sessions, withSession, providers, setActiveParentSession, setActiveSession } from "./session-state"
|
import { sessions, setSessions, withSession, providers, setActiveParentSession, setActiveSession } from "./session-state"
|
||||||
import { getDefaultModel, isModelValid } from "./session-models"
|
import { getDefaultModel, isModelValid } from "./session-models"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
import { messageStoreBus } from "./message-v2/bus"
|
import { messageStoreBus } from "./message-v2/bus"
|
||||||
@@ -25,6 +25,8 @@ import { QwenOAuthManager } from "../lib/integrations/qwen-oauth"
|
|||||||
import { getUserScopedKey } from "../lib/user-storage"
|
import { getUserScopedKey } from "../lib/user-storage"
|
||||||
import { loadSkillDetails } from "./skills"
|
import { loadSkillDetails } from "./skills"
|
||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
|
import { nativeSessionApi } from "../lib/lite-mode"
|
||||||
|
import type { Session } from "../types/session"
|
||||||
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
@@ -1936,10 +1938,18 @@ async function updateSessionAgent(instanceId: string, sessionId: string, agent:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if (agent && shouldApplyModel && !agentModelPreference) {
|
if (agent && shouldApplyModel && !agentModelPreference) {
|
||||||
await setAgentModelPreference(instanceId, agent, nextModel)
|
await setAgentModelPreference(instanceId, agent, nextModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
const isNative = instance?.binaryPath === "__nomadarch_native__"
|
||||||
|
if (isNative) {
|
||||||
|
await nativeSessionApi.updateSession(instanceId, sessionId, { agent })
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldApplyModel) {
|
if (shouldApplyModel) {
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
}
|
}
|
||||||
@@ -1965,6 +1975,16 @@ async function updateSessionModel(
|
|||||||
current.model = model
|
current.model = model
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (instance?.binaryPath === "__nomadarch_native__") {
|
||||||
|
await nativeSessionApi.updateSession(instanceId, sessionId, {
|
||||||
|
model: {
|
||||||
|
providerId: model.providerId,
|
||||||
|
modelId: model.modelId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const propagateModel = (targetSessionId?: string | null) => {
|
const propagateModel = (targetSessionId?: string | null) => {
|
||||||
if (!targetSessionId || targetSessionId === sessionId) return
|
if (!targetSessionId || targetSessionId === sessionId) return
|
||||||
withSession(instanceId, targetSessionId, (current) => {
|
withSession(instanceId, targetSessionId, (current) => {
|
||||||
@@ -2014,16 +2034,31 @@ async function updateSessionModelForSession(
|
|||||||
current.model = model
|
current.model = model
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const instance = instances().get(instanceId)
|
||||||
|
if (instance?.binaryPath === "__nomadarch_native__") {
|
||||||
|
await nativeSessionApi.updateSession(instanceId, sessionId, {
|
||||||
|
model: {
|
||||||
|
providerId: model.providerId,
|
||||||
|
modelId: model.modelId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
addRecentModelPreference(model)
|
addRecentModelPreference(model)
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameSession(instanceId: string, sessionId: string, nextTitle: string): Promise<void> {
|
async function renameSession(instanceId: string, sessionId: string, nextTitle: string): Promise<void> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
if (!isNative && !instance.client) {
|
||||||
|
throw new Error("Instance client not ready")
|
||||||
|
}
|
||||||
|
|
||||||
const session = sessions().get(instanceId)?.get(sessionId)
|
const session = sessions().get(instanceId)?.get(sessionId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error("Session not found")
|
throw new Error("Session not found")
|
||||||
@@ -2034,10 +2069,14 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s
|
|||||||
throw new Error("Session title is required")
|
throw new Error("Session title is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
await instance.client.session.update({
|
if (isNative) {
|
||||||
|
await nativeSessionApi.updateSession(instanceId, sessionId, { title: trimmedTitle })
|
||||||
|
} else {
|
||||||
|
await instance.client!.session.update({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: { title: trimmedTitle },
|
body: { title: trimmedTitle },
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
withSession(instanceId, sessionId, (current) => {
|
withSession(instanceId, sessionId, (current) => {
|
||||||
current.title = trimmedTitle
|
current.title = trimmedTitle
|
||||||
@@ -2049,19 +2088,28 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s
|
|||||||
|
|
||||||
async function revertSession(instanceId: string, sessionId: string): Promise<void> {
|
async function revertSession(instanceId: string, sessionId: string): Promise<void> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
if (!isNative && !instance.client) {
|
||||||
|
throw new Error("Instance client not ready")
|
||||||
|
}
|
||||||
|
|
||||||
const session = sessions().get(instanceId)?.get(sessionId)
|
const session = sessions().get(instanceId)?.get(sessionId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error("Session not found")
|
throw new Error("Session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await instance.client.session.revert({
|
if (isNative) {
|
||||||
|
await nativeSessionApi.revertSession(instanceId, sessionId)
|
||||||
|
} else {
|
||||||
|
await instance.client!.session.revert({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
})
|
})
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to revert session", error)
|
log.error("Failed to revert session", error)
|
||||||
throw error
|
throw error
|
||||||
@@ -2070,30 +2118,76 @@ async function revertSession(instanceId: string, sessionId: string): Promise<voi
|
|||||||
|
|
||||||
async function forkSession(instanceId: string, sessionId: string): Promise<string> {
|
async function forkSession(instanceId: string, sessionId: string): Promise<string> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
if (!isNative && !instance.client) {
|
||||||
|
throw new Error("Instance client not ready")
|
||||||
|
}
|
||||||
|
|
||||||
const session = sessions().get(instanceId)?.get(sessionId)
|
const session = sessions().get(instanceId)?.get(sessionId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error("Session not found")
|
throw new Error("Session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await instance.client.session.fork({
|
let forkedId: string = ""
|
||||||
|
let forkedVersion: string = "0"
|
||||||
|
let forkedTime: any = { created: Date.now(), updated: Date.now() }
|
||||||
|
let forkedRevert: any = undefined
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
const response = await nativeSessionApi.forkSession(instanceId, sessionId)
|
||||||
|
forkedId = response.id
|
||||||
|
forkedTime = { created: response.createdAt, updated: response.updatedAt }
|
||||||
|
} else {
|
||||||
|
const response = await instance.client!.session.fork({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
})
|
})
|
||||||
|
if (!response.data) {
|
||||||
if (response.error) {
|
throw new Error("Failed to fork session: No data returned")
|
||||||
throw new Error(JSON.stringify(response.error) || "Failed to fork session")
|
}
|
||||||
|
forkedId = response.data.id
|
||||||
|
forkedVersion = response.data.version
|
||||||
|
forkedTime = response.data.time
|
||||||
|
forkedRevert = response.data.revert
|
||||||
|
? {
|
||||||
|
messageID: response.data.revert.messageID,
|
||||||
|
partID: response.data.revert.partID,
|
||||||
|
snapshot: response.data.revert.snapshot,
|
||||||
|
diff: response.data.revert.diff,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSessionId = response.data?.id
|
if (!forkedId) {
|
||||||
if (!newSessionId) {
|
|
||||||
throw new Error("No session ID returned from fork operation")
|
throw new Error("No session ID returned from fork operation")
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSessionId
|
const forkedSession: Session = {
|
||||||
|
id: forkedId,
|
||||||
|
instanceId,
|
||||||
|
title: session.title ? `${session.title} (fork)` : "Forked Session",
|
||||||
|
parentId: session.parentId || session.id,
|
||||||
|
agent: session.agent,
|
||||||
|
model: session.model,
|
||||||
|
skills: [...(session.skills || [])],
|
||||||
|
version: forkedVersion,
|
||||||
|
time: forkedTime,
|
||||||
|
revert: forkedRevert
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessions((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
const instanceSessions = new Map(next.get(instanceId) || [])
|
||||||
|
instanceSessions.set(forkedSession.id, forkedSession)
|
||||||
|
next.set(instanceId, instanceSessions)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
return forkedId
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to fork session", error)
|
log.error("Failed to fork session", error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Session, Provider, Model } from "../types/session"
|
|||||||
import type { Message } from "../types/message"
|
import type { Message } from "../types/message"
|
||||||
|
|
||||||
import { instances } from "./instances"
|
import { instances } from "./instances"
|
||||||
|
import { nativeSessionApi } from "../lib/lite-mode"
|
||||||
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
||||||
import { setSessionCompactionState } from "./session-compaction"
|
import { setSessionCompactionState } from "./session-compaction"
|
||||||
import {
|
import {
|
||||||
@@ -366,10 +367,16 @@ interface SessionForkResponse {
|
|||||||
|
|
||||||
async function fetchSessions(instanceId: string): Promise<void> {
|
async function fetchSessions(instanceId: string): Promise<void> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
|
||||||
|
if (!isNative && !instance.client) {
|
||||||
|
throw new Error("Instance client not ready")
|
||||||
|
}
|
||||||
|
|
||||||
setLoading((prev) => {
|
setLoading((prev) => {
|
||||||
const next = { ...prev }
|
const next = { ...prev }
|
||||||
next.fetchingSessions.set(instanceId, true)
|
next.fetchingSessions.set(instanceId, true)
|
||||||
@@ -377,13 +384,38 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info("session.list", { instanceId })
|
log.info("session.list", { instanceId, isNative })
|
||||||
const response = await instance.client.session.list()
|
|
||||||
|
let responseData: any[] = []
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
const nativeSessions = await nativeSessionApi.listSessions(instanceId)
|
||||||
|
responseData = nativeSessions.map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
parentID: s.parentId,
|
||||||
|
version: "0",
|
||||||
|
time: {
|
||||||
|
created: s.createdAt,
|
||||||
|
updated: s.updatedAt
|
||||||
|
},
|
||||||
|
model: s.model ? {
|
||||||
|
providerID: s.model.providerId,
|
||||||
|
modelID: s.model.modelId
|
||||||
|
} : undefined,
|
||||||
|
agent: s.agent
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
const response = await instance.client!.session.list()
|
||||||
|
if (response.data && Array.isArray(response.data)) {
|
||||||
|
responseData = response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sessionMap = new Map<string, Session>()
|
const sessionMap = new Map<string, Session>()
|
||||||
|
|
||||||
if (!response.data || !Array.isArray(response.data)) {
|
if (responseData.length === 0 && !isNative) {
|
||||||
return
|
// In SDK mode we still check response.data for empty
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingSessions = sessions().get(instanceId)
|
const existingSessions = sessions().get(instanceId)
|
||||||
@@ -394,13 +426,13 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
const sessionTasks = instanceData.sessionTasks || {}
|
const sessionTasks = instanceData.sessionTasks || {}
|
||||||
const sessionSkills = instanceData.sessionSkills || {}
|
const sessionSkills = instanceData.sessionSkills || {}
|
||||||
|
|
||||||
for (const apiSession of response.data) {
|
for (const apiSession of responseData) {
|
||||||
const existingSession = existingSessions?.get(apiSession.id)
|
const existingSession = existingSessions?.get(apiSession.id)
|
||||||
|
|
||||||
const existingModel = existingSession?.model ?? { providerId: "", modelId: "" }
|
const existingModel = existingSession?.model ?? { providerId: "", modelId: "" }
|
||||||
const hasUserSelectedModel = existingModel.providerId && existingModel.modelId
|
const hasUserSelectedModel = existingModel.providerId && existingModel.modelId
|
||||||
const apiModel = (apiSession as any).model?.providerID && (apiSession as any).model?.modelID
|
const apiModel = apiSession.model?.providerID && apiSession.model?.modelID
|
||||||
? { providerId: (apiSession as any).model.providerID, modelId: (apiSession as any).model.modelID }
|
? { providerId: apiSession.model.providerID, modelId: apiSession.model.modelID }
|
||||||
: { providerId: "", modelId: "" }
|
: { providerId: "", modelId: "" }
|
||||||
|
|
||||||
sessionMap.set(apiSession.id, {
|
sessionMap.set(apiSession.id, {
|
||||||
@@ -408,7 +440,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
instanceId,
|
instanceId,
|
||||||
title: apiSession.title || "Untitled",
|
title: apiSession.title || "Untitled",
|
||||||
parentId: apiSession.parentID || null,
|
parentId: apiSession.parentID || null,
|
||||||
agent: existingSession?.agent ?? (apiSession as any).agent ?? "",
|
agent: existingSession?.agent ?? apiSession.agent ?? "",
|
||||||
model: hasUserSelectedModel ? existingModel : apiModel,
|
model: hasUserSelectedModel ? existingModel : apiModel,
|
||||||
version: apiSession.version,
|
version: apiSession.version,
|
||||||
time: {
|
time: {
|
||||||
@@ -475,10 +507,15 @@ async function createSession(
|
|||||||
options?: { skipAutoCleanup?: boolean },
|
options?: { skipAutoCleanup?: boolean },
|
||||||
): Promise<Session> {
|
): Promise<Session> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
if (!isNative && !instance.client) {
|
||||||
|
throw new Error("Instance client not ready")
|
||||||
|
}
|
||||||
|
|
||||||
const instanceAgents = agents().get(instanceId) || []
|
const instanceAgents = agents().get(instanceId) || []
|
||||||
const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent")
|
const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent")
|
||||||
const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "")
|
const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "")
|
||||||
@@ -498,31 +535,57 @@ async function createSession(
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info(`[HTTP] POST /session.create for instance ${instanceId}`)
|
log.info(`[HTTP] POST session create for instance ${instanceId}, isNative: ${isNative}`)
|
||||||
const response = await instance.client.session.create()
|
|
||||||
|
|
||||||
if (!response.data) {
|
let sessionData: any = null
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
const native = await nativeSessionApi.createSession(instanceId, {
|
||||||
|
agent: selectedAgent,
|
||||||
|
model: sessionModel
|
||||||
|
})
|
||||||
|
sessionData = {
|
||||||
|
id: native.id,
|
||||||
|
title: native.title || "New Session",
|
||||||
|
parentID: native.parentId,
|
||||||
|
version: "0",
|
||||||
|
time: {
|
||||||
|
created: native.createdAt,
|
||||||
|
updated: native.updatedAt
|
||||||
|
},
|
||||||
|
agent: native.agent,
|
||||||
|
model: native.model ? {
|
||||||
|
providerID: native.model.providerId,
|
||||||
|
modelID: native.model.modelId
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const response = await instance.client!.session.create()
|
||||||
|
sessionData = response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionData) {
|
||||||
throw new Error("Failed to create session: No data returned")
|
throw new Error("Failed to create session: No data returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
id: response.data.id,
|
id: sessionData.id,
|
||||||
instanceId,
|
instanceId,
|
||||||
title: response.data.title || "New Session",
|
title: sessionData.title || "New Session",
|
||||||
parentId: null,
|
parentId: null,
|
||||||
agent: selectedAgent,
|
agent: selectedAgent,
|
||||||
model: sessionModel,
|
model: sessionModel,
|
||||||
skills: [],
|
skills: [],
|
||||||
version: response.data.version,
|
version: sessionData.version,
|
||||||
time: {
|
time: {
|
||||||
...response.data.time,
|
...sessionData.time,
|
||||||
},
|
},
|
||||||
revert: response.data.revert
|
revert: sessionData.revert
|
||||||
? {
|
? {
|
||||||
messageID: response.data.revert.messageID,
|
messageID: sessionData.revert.messageID,
|
||||||
partID: response.data.revert.partID,
|
partID: sessionData.revert.partID,
|
||||||
snapshot: response.data.revert.snapshot,
|
snapshot: sessionData.revert.snapshot,
|
||||||
diff: response.data.revert.diff,
|
diff: sessionData.revert.diff,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
@@ -683,9 +746,10 @@ async function forkSession(
|
|||||||
|
|
||||||
async function deleteSession(instanceId: string, sessionId: string): Promise<void> {
|
async function deleteSession(instanceId: string, sessionId: string): Promise<void> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) return
|
||||||
throw new Error("Instance not ready")
|
|
||||||
}
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
if (!isNative && !instance.client) return
|
||||||
|
|
||||||
setLoading((prev) => {
|
setLoading((prev) => {
|
||||||
const next = { ...prev }
|
const next = { ...prev }
|
||||||
@@ -696,8 +760,13 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info(`[HTTP] DELETE /session.delete for instance ${instanceId}`, { sessionId })
|
log.info("session.delete", { instanceId, sessionId, isNative })
|
||||||
await instance.client.session.delete({ path: { id: sessionId } })
|
|
||||||
|
if (isNative) {
|
||||||
|
await nativeSessionApi.deleteSession(instanceId, sessionId)
|
||||||
|
} else {
|
||||||
|
await instance.client!.session.delete({ path: { id: sessionId } })
|
||||||
|
}
|
||||||
|
|
||||||
setSessions((prev) => {
|
setSessions((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
@@ -754,25 +823,42 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
|
|||||||
|
|
||||||
async function fetchAgents(instanceId: string): Promise<void> {
|
async function fetchAgents(instanceId: string): Promise<void> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
if (!isNative && !instance.client) {
|
||||||
|
throw new Error("Instance client not ready")
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureInstanceConfigLoaded(instanceId)
|
log.info("agents.list", { instanceId, isNative })
|
||||||
log.info(`[HTTP] GET /app.agents for instance ${instanceId}`)
|
|
||||||
const response = await instance.client.app.agents()
|
let agentList: any[] = []
|
||||||
const agentList = (response.data ?? []).map((agent) => ({
|
|
||||||
|
if (isNative) {
|
||||||
|
// In native mode, we don't have agents from the SDK yet
|
||||||
|
// We can return a default agent or common agents
|
||||||
|
agentList = [{
|
||||||
|
name: "Assistant",
|
||||||
|
description: "Native assistant agent",
|
||||||
|
mode: "native"
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
const response = await instance.client!.app.agents()
|
||||||
|
agentList = (response.data || []).map((agent: any) => ({
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
description: agent.description || "",
|
description: agent.description || "",
|
||||||
mode: agent.mode,
|
mode: agent.mode as "standard" | "subagent",
|
||||||
model: agent.model?.modelID
|
model: agent.model
|
||||||
? {
|
? {
|
||||||
providerId: agent.model.providerID || "",
|
providerId: agent.model.providerID || "",
|
||||||
modelId: agent.model.modelID,
|
modelId: agent.model.modelID,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const customAgents = getInstanceConfig(instanceId)?.customAgents ?? []
|
const customAgents = getInstanceConfig(instanceId)?.customAgents ?? []
|
||||||
const customList = customAgents.map((agent) => ({
|
const customList = customAgents.map((agent) => ({
|
||||||
@@ -793,16 +879,29 @@ async function fetchAgents(instanceId: string): Promise<void> {
|
|||||||
|
|
||||||
async function fetchProviders(instanceId: string): Promise<void> {
|
async function fetchProviders(instanceId: string): Promise<void> {
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
log.info(`[HTTP] GET /config.providers for instance ${instanceId}`)
|
if (!isNative && !instance.client) {
|
||||||
const response = await instance.client.config.providers()
|
throw new Error("Instance client not ready")
|
||||||
if (!response.data) return
|
}
|
||||||
|
|
||||||
const providerList = response.data.providers.map((provider) => ({
|
try {
|
||||||
|
log.info("config.providers", { instanceId, isNative })
|
||||||
|
|
||||||
|
let providerList: any[] = []
|
||||||
|
let defaultProviders: any = {}
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
// For native mode, we mainly rely on extra providers
|
||||||
|
// but we could add "zen" (OpenCode Zen) if it's available via server API
|
||||||
|
providerList = []
|
||||||
|
} else {
|
||||||
|
const response = await instance.client!.config.providers()
|
||||||
|
if (response.data) {
|
||||||
|
providerList = response.data.providers.map((provider) => ({
|
||||||
id: provider.id,
|
id: provider.id,
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
defaultModelId: response.data?.default?.[provider.id],
|
defaultModelId: response.data?.default?.[provider.id],
|
||||||
@@ -814,6 +913,9 @@ async function fetchProviders(instanceId: string): Promise<void> {
|
|||||||
cost: model.cost,
|
cost: model.cost,
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
|
defaultProviders = response.data.default || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out Z.AI providers from SDK to use our custom routing with full message history
|
// Filter out Z.AI providers from SDK to use our custom routing with full message history
|
||||||
const filteredBaseProviders = providerList.filter((provider) =>
|
const filteredBaseProviders = providerList.filter((provider) =>
|
||||||
@@ -859,10 +961,15 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
|||||||
}
|
}
|
||||||
|
|
||||||
const instance = instances().get(instanceId)
|
const instance = instances().get(instanceId)
|
||||||
if (!instance || !instance.client) {
|
if (!instance) {
|
||||||
throw new Error("Instance not ready")
|
throw new Error("Instance not ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||||
|
if (!isNative && !instance.client) {
|
||||||
|
throw new Error("Instance client not ready")
|
||||||
|
}
|
||||||
|
|
||||||
const instanceSessions = sessions().get(instanceId)
|
const instanceSessions = sessions().get(instanceId)
|
||||||
const session = instanceSessions?.get(sessionId)
|
const session = instanceSessions?.get(sessionId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -878,15 +985,37 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId })
|
log.info("session.getMessages", { instanceId, sessionId, isNative })
|
||||||
const response = await instance.client.session["messages"]({ path: { id: sessionId } })
|
|
||||||
|
|
||||||
|
let apiMessages: any[] = []
|
||||||
|
let apiMessagesInfo: any = {}
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
const nativeMessages = await nativeSessionApi.getMessages(instanceId, sessionId)
|
||||||
|
apiMessages = nativeMessages.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
role: m.role,
|
||||||
|
content: m.content || "",
|
||||||
|
createdAt: m.createdAt,
|
||||||
|
status: m.status,
|
||||||
|
info: {
|
||||||
|
id: m.id,
|
||||||
|
role: m.role,
|
||||||
|
time: { created: m.createdAt },
|
||||||
|
// Add other native message properties to info if needed for later processing
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
const response = await instance.client!.session.messages({ path: { id: sessionId } })
|
||||||
if (!response.data || !Array.isArray(response.data)) {
|
if (!response.data || !Array.isArray(response.data)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
apiMessages = response.data || []
|
||||||
|
apiMessagesInfo = (response as any).info || {} // Assuming 'info' might be on the response object itself for some cases
|
||||||
|
}
|
||||||
|
|
||||||
const messagesInfo = new Map<string, any>()
|
const messagesInfo = new Map<string, any>()
|
||||||
const messages: Message[] = response.data.map((apiMessage: any) => {
|
const messages: Message[] = apiMessages.map((apiMessage: any) => {
|
||||||
const info = apiMessage.info || apiMessage
|
const info = apiMessage.info || apiMessage
|
||||||
const role = info.role || "assistant"
|
const role = info.role || "assistant"
|
||||||
const messageId = info.id || String(Date.now())
|
const messageId = info.id || String(Date.now())
|
||||||
@@ -912,8 +1041,8 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
|||||||
let providerID = ""
|
let providerID = ""
|
||||||
let modelID = ""
|
let modelID = ""
|
||||||
|
|
||||||
for (let i = response.data.length - 1; i >= 0; i--) {
|
for (let i = apiMessages.length - 1; i >= 0; i--) {
|
||||||
const apiMessage = response.data[i]
|
const apiMessage = apiMessages[i]
|
||||||
const info = apiMessage.info || apiMessage
|
const info = apiMessage.info || apiMessage
|
||||||
|
|
||||||
if (info.role === "assistant") {
|
if (info.role === "assistant") {
|
||||||
@@ -924,6 +1053,7 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!agentName && !providerID && !modelID) {
|
if (!agentName && !providerID && !modelID) {
|
||||||
const defaultModel = await getDefaultModel(instanceId, session.agent)
|
const defaultModel = await getDefaultModel(instanceId, session.agent)
|
||||||
agentName = session.agent
|
agentName = session.agent
|
||||||
|
|||||||
Reference in New Issue
Block a user