Files
NomadArch/packages/ui/src/lib/lite-mode.ts
Gemini AI 4bd2893864 v0.5.0: Binary-Free Mode - No OpenCode binary required
 Major Features:
- Native session management without OpenCode binary
- Provider routing: OpenCode Zen (free), Qwen OAuth, Z.AI
- Streaming chat with tool execution loop
- Mode detection API (/api/meta/mode)
- MCP integration fix (resolved infinite loading)
- NomadArch Native option in UI with comparison info

🆓 Free Models (No API Key):
- GPT-5 Nano (400K context)
- Grok Code Fast 1 (256K context)
- GLM-4.7 (205K context)
- Doubao Seed Code (256K context)
- Big Pickle (200K context)

📦 New Files:
- session-store.ts: Native session persistence
- native-sessions.ts: REST API for sessions
- lite-mode.ts: UI mode detection client
- native-sessions.ts (UI): SolidJS store

🔧 Updated:
- All installers: Optional binary download
- All launchers: Mode detection display
- Binary selector: Added NomadArch Native option
- README: Binary-Free Mode documentation
2025-12-26 02:12:42 +04:00

228 lines
7.6 KiB
TypeScript

/**
* Lite Mode API Client - Binary-Free Mode
*
* This provides a client for working with NomadArch in Binary-Free Mode,
* using native session management instead of the OpenCode binary.
*/
import { CODENOMAD_API_BASE } from "./api-client"
import { getLogger } from "./logger"
const log = getLogger("lite-mode")
export interface ModeInfo {
mode: "lite" | "full"
binaryFreeMode: boolean
nativeSessions: boolean
opencodeBinaryAvailable: boolean
providers: {
qwen: boolean
zai: boolean
zen: boolean
}
}
export interface NativeSession {
id: string
workspaceId: string
title?: string
parentId?: string | null
createdAt: number
updatedAt: number
messageIds: string[]
model?: {
providerId: string
modelId: string
}
agent?: string
}
export interface NativeMessage {
id: string
sessionId: string
role: "user" | "assistant" | "system" | "tool"
content?: string
createdAt: number
updatedAt: number
status?: "pending" | "streaming" | "completed" | "error"
}
let modeCache: ModeInfo | null = null
/**
* Get the current running mode (lite or full)
*/
export async function getMode(): Promise<ModeInfo> {
if (modeCache) return modeCache
try {
const response = await fetch(`${CODENOMAD_API_BASE}/api/meta/mode`)
if (!response.ok) {
throw new Error(`Failed to fetch mode: ${response.status}`)
}
modeCache = await response.json()
log.info(`Running in ${modeCache?.mode} mode`, { binaryFree: modeCache?.binaryFreeMode })
return modeCache!
} catch (error) {
log.warn("Failed to fetch mode, assuming lite mode", error)
// Default to lite mode if we can't determine
return {
mode: "lite",
binaryFreeMode: true,
nativeSessions: true,
opencodeBinaryAvailable: false,
providers: { qwen: true, zai: true, zen: true }
}
}
}
/**
* Check if running in Binary-Free (lite) mode
*/
export async function isLiteMode(): Promise<boolean> {
const mode = await getMode()
return mode.binaryFreeMode
}
/**
* Native Session API for Binary-Free Mode
*/
export const nativeSessionApi = {
async listSessions(workspaceId: string): Promise<NativeSession[]> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`)
if (!response.ok) throw new Error("Failed to list sessions")
const data = await response.json()
return data.sessions
},
async createSession(workspaceId: string, options?: {
title?: string
parentId?: string
model?: { providerId: string; modelId: string }
agent?: string
}): Promise<NativeSession> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options ?? {})
})
if (!response.ok) throw new Error("Failed to create session")
const data = await response.json()
return data.session
},
async getSession(workspaceId: string, sessionId: string): Promise<NativeSession | null> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`)
if (response.status === 404) return null
if (!response.ok) throw new Error("Failed to get session")
const data = await response.json()
return data.session
},
async updateSession(workspaceId: string, sessionId: string, updates: Partial<NativeSession>): Promise<NativeSession | null> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates)
})
if (response.status === 404) return null
if (!response.ok) throw new Error("Failed to update session")
const data = await response.json()
return data.session
},
async deleteSession(workspaceId: string, sessionId: string): Promise<boolean> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, {
method: "DELETE"
})
return response.ok || response.status === 204
},
async getMessages(workspaceId: string, sessionId: string): Promise<NativeMessage[]> {
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")
const data = await response.json()
return data.messages
},
/**
* Send a prompt to the session and get a streaming response
*/
async* streamPrompt(
workspaceId: string,
sessionId: string,
content: string,
options?: {
provider?: "qwen" | "zai" | "zen"
accessToken?: string
resourceUrl?: string
enableTools?: boolean
}
): AsyncGenerator<{ type: "content" | "done" | "error"; data?: string }> {
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content,
provider: options?.provider ?? "qwen",
accessToken: options?.accessToken,
resourceUrl: options?.resourceUrl,
enableTools: options?.enableTools ?? true
})
})
if (!response.ok) {
yield { type: "error", data: `Request failed: ${response.status}` }
return
}
const reader = response.body?.getReader()
if (!reader) {
yield { type: "error", data: "No response body" }
return
}
const decoder = new TextDecoder()
let buffer = ""
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.trim()) continue
if (line.startsWith("data: ")) {
const data = line.slice(6)
if (data === "[DONE]") {
yield { type: "done" }
return
}
try {
const parsed = JSON.parse(data)
if (parsed.error) {
yield { type: "error", data: parsed.error }
} else if (parsed.choices?.[0]?.delta?.content) {
yield { type: "content", data: parsed.choices[0].delta.content }
}
} catch {
// Skip invalid JSON
}
}
}
}
yield { type: "done" }
}
}
/**
* Clear mode cache (for testing or after config changes)
*/
export function clearModeCache(): void {
modeCache = null
}