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
This commit is contained in:
227
packages/ui/src/lib/lite-mode.ts
Normal file
227
packages/ui/src/lib/lite-mode.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user