Compare commits

..

2 Commits

8 changed files with 453 additions and 111 deletions

View File

@@ -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 {

View File

@@ -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[]> {

View File

@@ -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"
> >

View File

@@ -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}
/> />
@@ -906,11 +907,11 @@ export default function ToolCall(props: ToolCallProps) {
{expanded() && ( {expanded() && (
<div class="tool-call-details"> <div class="tool-call-details">
{renderToolBody()} {renderToolBody()}
{renderError()} {renderError()}
{renderPermissionBlock()} {renderPermissionBlock()}
<Show when={status() === "pending" && !pendingPermission()}> <Show when={status() === "pending" && !pendingPermission()}>
<div class="tool-call-pending-message"> <div class="tool-call-pending-message">
<span class="spinner-small"></span> <span class="spinner-small"></span>
@@ -919,7 +920,7 @@ export default function ToolCall(props: ToolCallProps) {
</Show> </Show>
</div> </div>
)} )}
<Show when={diagnosticsEntries().length}> <Show when={diagnosticsEntries().length}>
{renderDiagnosticsSection( {renderDiagnosticsSection(

View File

@@ -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")

View File

@@ -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 => {

View File

@@ -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) {
path: { id: sessionId }, await nativeSessionApi.updateSession(instanceId, sessionId, { title: trimmedTitle })
body: { title: trimmedTitle }, } else {
}) await instance.client!.session.update({
path: { id: sessionId },
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) {
path: { id: sessionId }, await nativeSessionApi.revertSession(instanceId, sessionId)
}) } else {
await instance.client!.session.revert({
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 = ""
path: { id: sessionId }, let forkedVersion: string = "0"
}) let forkedTime: any = { created: Date.now(), updated: Date.now() }
let forkedRevert: any = undefined
if (response.error) { if (isNative) {
throw new Error(JSON.stringify(response.error) || "Failed to fork session") 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 },
})
if (!response.data) {
throw new Error("Failed to fork session: No data returned")
}
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

View File

@@ -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) => ({
name: agent.name, if (isNative) {
description: agent.description || "", // In native mode, we don't have agents from the SDK yet
mode: agent.mode, // We can return a default agent or common agents
model: agent.model?.modelID agentList = [{
? { name: "Assistant",
providerId: agent.model.providerID || "", description: "Native assistant agent",
modelId: agent.model.modelID, mode: "native"
} }]
: undefined, } else {
})) const response = await instance.client!.app.agents()
agentList = (response.data || []).map((agent: any) => ({
name: agent.name,
description: agent.description || "",
mode: agent.mode as "standard" | "subagent",
model: agent.model
? {
providerId: agent.model.providerID || "",
modelId: agent.model.modelID,
}
: undefined,
}))
}
const customAgents = getInstanceConfig(instanceId)?.customAgents ?? [] const customAgents = getInstanceConfig(instanceId)?.customAgents ?? []
const customList = customAgents.map((agent) => ({ const customList = customAgents.map((agent) => ({
@@ -793,27 +879,43 @@ 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 {
id: provider.id, log.info("config.providers", { instanceId, isNative })
name: provider.name,
defaultModelId: response.data?.default?.[provider.id], let providerList: any[] = []
models: Object.entries(provider.models).map(([id, model]) => ({ let defaultProviders: any = {}
id,
name: model.name, if (isNative) {
providerId: provider.id, // For native mode, we mainly rely on extra providers
limit: model.limit, // but we could add "zen" (OpenCode Zen) if it's available via server API
cost: model.cost, providerList = []
})), } else {
})) const response = await instance.client!.config.providers()
if (response.data) {
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,
})),
}))
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 } })
if (!response.data || !Array.isArray(response.data)) { let apiMessages: any[] = []
return 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)) {
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