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:
Gemini AI
2025-12-26 02:08:13 +04:00
Unverified
parent 8dddf4d0cf
commit 4bd2893864
83 changed files with 10678 additions and 1290 deletions

View File

@@ -0,0 +1,284 @@
/**
* Session Store - Native session management without OpenCode binary
*
* This provides a complete replacement for OpenCode's session management,
* allowing NomadArch to work in "Binary-Free Mode".
*/
import { readFile, writeFile, mkdir } from "fs/promises"
import { existsSync } from "fs"
import path from "path"
import { ulid } from "ulid"
import { createLogger } from "../logger"
const log = createLogger({ component: "session-store" })
// Types matching OpenCode's schema for compatibility
export interface SessionMessage {
id: string
sessionId: string
role: "user" | "assistant" | "system" | "tool"
content?: string
parts?: MessagePart[]
createdAt: number
updatedAt: number
toolCalls?: ToolCall[]
toolCallId?: string
status?: "pending" | "streaming" | "completed" | "error"
}
export interface MessagePart {
type: "text" | "tool_call" | "tool_result" | "thinking" | "code"
content?: string
toolCall?: ToolCall
toolResult?: ToolResult
}
export interface ToolCall {
id: string
type: "function"
function: {
name: string
arguments: string
}
}
export interface ToolResult {
toolCallId: string
content: string
isError?: boolean
}
export interface Session {
id: string
workspaceId: string
title?: string
parentId?: string | null
createdAt: number
updatedAt: number
messageIds: string[]
model?: {
providerId: string
modelId: string
}
agent?: string
revert?: {
messageID: string
reason?: string
} | null
}
export interface SessionStore {
sessions: Record<string, Session>
messages: Record<string, SessionMessage>
}
/**
* Native session management for Binary-Free Mode
*/
export class NativeSessionManager {
private stores = new Map<string, SessionStore>()
private dataDir: string
constructor(dataDir: string) {
this.dataDir = dataDir
}
private getStorePath(workspaceId: string): string {
return path.join(this.dataDir, workspaceId, "sessions.json")
}
private async ensureDir(workspaceId: string): Promise<void> {
const dir = path.join(this.dataDir, workspaceId)
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true })
}
}
private async loadStore(workspaceId: string): Promise<SessionStore> {
if (this.stores.has(workspaceId)) {
return this.stores.get(workspaceId)!
}
const storePath = this.getStorePath(workspaceId)
let store: SessionStore = { sessions: {}, messages: {} }
if (existsSync(storePath)) {
try {
const data = await readFile(storePath, "utf-8")
store = JSON.parse(data)
} catch (error) {
log.error({ workspaceId, error }, "Failed to load session store")
}
}
this.stores.set(workspaceId, store)
return store
}
private async saveStore(workspaceId: string): Promise<void> {
const store = this.stores.get(workspaceId)
if (!store) return
await this.ensureDir(workspaceId)
const storePath = this.getStorePath(workspaceId)
await writeFile(storePath, JSON.stringify(store, null, 2), "utf-8")
}
// Session CRUD operations
async listSessions(workspaceId: string): Promise<Session[]> {
const store = await this.loadStore(workspaceId)
return Object.values(store.sessions).sort((a, b) => b.updatedAt - a.updatedAt)
}
async getSession(workspaceId: string, sessionId: string): Promise<Session | null> {
const store = await this.loadStore(workspaceId)
return store.sessions[sessionId] ?? null
}
async createSession(workspaceId: string, options?: {
title?: string
parentId?: string
model?: { providerId: string; modelId: string }
agent?: string
}): Promise<Session> {
const store = await this.loadStore(workspaceId)
const now = Date.now()
const session: Session = {
id: ulid(),
workspaceId,
title: options?.title ?? "New Session",
parentId: options?.parentId ?? null,
createdAt: now,
updatedAt: now,
messageIds: [],
model: options?.model,
agent: options?.agent,
}
store.sessions[session.id] = session
await this.saveStore(workspaceId)
log.info({ workspaceId, sessionId: session.id }, "Created new session")
return session
}
async updateSession(workspaceId: string, sessionId: string, updates: Partial<Session>): Promise<Session | null> {
const store = await this.loadStore(workspaceId)
const session = store.sessions[sessionId]
if (!session) return null
const updated = {
...session,
...updates,
id: session.id, // Prevent ID change
workspaceId: session.workspaceId, // Prevent workspace change
updatedAt: Date.now(),
}
store.sessions[sessionId] = updated
await this.saveStore(workspaceId)
return updated
}
async deleteSession(workspaceId: string, sessionId: string): Promise<boolean> {
const store = await this.loadStore(workspaceId)
const session = store.sessions[sessionId]
if (!session) return false
// Delete all messages in the session
for (const messageId of session.messageIds) {
delete store.messages[messageId]
}
delete store.sessions[sessionId]
await this.saveStore(workspaceId)
log.info({ workspaceId, sessionId }, "Deleted session")
return true
}
// Message operations
async getSessionMessages(workspaceId: string, sessionId: string): Promise<SessionMessage[]> {
const store = await this.loadStore(workspaceId)
const session = store.sessions[sessionId]
if (!session) return []
return session.messageIds
.map(id => store.messages[id])
.filter((msg): msg is SessionMessage => msg !== undefined)
}
async addMessage(workspaceId: string, sessionId: string, message: Omit<SessionMessage, "id" | "sessionId" | "createdAt" | "updatedAt">): Promise<SessionMessage> {
const store = await this.loadStore(workspaceId)
const session = store.sessions[sessionId]
if (!session) throw new Error(`Session not found: ${sessionId}`)
const now = Date.now()
const newMessage: SessionMessage = {
...message,
id: ulid(),
sessionId,
createdAt: now,
updatedAt: now,
}
store.messages[newMessage.id] = newMessage
session.messageIds.push(newMessage.id)
session.updatedAt = now
await this.saveStore(workspaceId)
return newMessage
}
async updateMessage(workspaceId: string, messageId: string, updates: Partial<SessionMessage>): Promise<SessionMessage | null> {
const store = await this.loadStore(workspaceId)
const message = store.messages[messageId]
if (!message) return null
const updated = {
...message,
...updates,
id: message.id, // Prevent ID change
sessionId: message.sessionId, // Prevent session change
updatedAt: Date.now(),
}
store.messages[messageId] = updated
await this.saveStore(workspaceId)
return updated
}
// Utility
async clearWorkspace(workspaceId: string): Promise<void> {
this.stores.delete(workspaceId)
// Optionally delete file
}
getActiveSessionCount(workspaceId: string): number {
const store = this.stores.get(workspaceId)
return store ? Object.keys(store.sessions).length : 0
}
}
// Singleton instance
let sessionManager: NativeSessionManager | null = null
export function getSessionManager(dataDir?: string): NativeSessionManager {
if (!sessionManager) {
if (!dataDir) {
throw new Error("Session manager not initialized - provide dataDir")
}
sessionManager = new NativeSessionManager(dataDir)
}
return sessionManager
}
export function initSessionManager(dataDir: string): NativeSessionManager {
sessionManager = new NativeSessionManager(dataDir)
return sessionManager
}