feat: implement manual SDK session sync and fix UI crash
Some checks failed
Release Binaries / release (push) Has been cancelled

This commit is contained in:
Gemini AI
2025-12-27 12:11:08 +04:00
Unverified
parent 251fad85b1
commit 0e5059fc88
5 changed files with 127 additions and 33 deletions

14
.gitignore vendored
View File

@@ -41,7 +41,7 @@ install.log
# ===================== OS Generated Files =============== # ===================== OS Generated Files ===============
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
Desktop.ini 44: Desktop.ini
# ===================== Temporary Files ================== # ===================== Temporary Files ==================
*.tmp *.tmp
@@ -100,4 +100,14 @@ packages/server/dist/
# ===================== Backup Files ===================== # ===================== Backup Files =====================
*.backup *.backup
*_backup* *_backup*
_backup_original/ _backup_original/
# ===================== NomadArch Specific Data ============
.codenomad-data/
**/logs/
**/.codenomad-data/
sdk-sync-debug.log
**/sessions.json
**/messages.json
**/workspaces.json
*.json.bak

View File

@@ -9,10 +9,9 @@
*/ */
import { FastifyInstance } from "fastify" import { FastifyInstance } from "fastify"
import { readdir, readFile } from "fs/promises" import { readdir, readFile, appendFile } from "fs/promises"
import { existsSync } from "fs" import { existsSync } from "fs"
import { join } from "path" import { join } from "path"
import { createHash } from "crypto"
import { homedir } from "os" import { homedir } from "os"
import { Logger } from "../../logger" import { Logger } from "../../logger"
import { getSessionManager } from "../../storage/session-store" import { getSessionManager } from "../../storage/session-store"
@@ -59,54 +58,92 @@ function getOpenCodeStorageDir(): string {
return join(homeDir, ".local", "share", "opencode", "storage") return join(homeDir, ".local", "share", "opencode", "storage")
} }
/**
* Generate the project ID hash that OpenCode uses
* OpenCode uses a SHA1 hash of the folder path
*/
function generateProjectId(folderPath: string): string {
return createHash("sha1").update(folderPath).digest("hex")
}
/** /**
* Read all sessions for a project from OpenCode's storage * Read all sessions for a project from OpenCode's storage
*/ */
async function readOpenCodeSessions(folderPath: string, logger: Logger): Promise<OpenCodeSession[]> { async function readOpenCodeSessions(folderPath: string, logger: Logger): Promise<OpenCodeSession[]> {
const storageDir = getOpenCodeStorageDir() const storageDir = getOpenCodeStorageDir()
const projectId = generateProjectId(folderPath) const sessionBaseDir = join(storageDir, "session")
const sessionDir = join(storageDir, "session", projectId) const debugLogPath = join(process.cwd(), "sdk-sync-debug.log")
logger.info({ folderPath, projectId, sessionDir }, "Looking for OpenCode sessions") const logDebug = async (msg: string, obj?: any) => {
const line = `[${new Date().toISOString()}] ${msg}${obj ? ' ' + JSON.stringify(obj) : ''}\n`
await appendFile(debugLogPath, line).catch(() => { })
logger.info(obj || {}, msg)
}
if (!existsSync(sessionDir)) { // Normalize target folder path for comparison
logger.info({ sessionDir }, "OpenCode session directory not found") const targetPath = folderPath.replace(/\\/g, '/').toLowerCase().trim()
await logDebug("Starting SDK session search", { folderPath, targetPath, sessionBaseDir })
if (!existsSync(sessionBaseDir)) {
await logDebug("OpenCode session base directory not found", { sessionBaseDir })
return [] return []
} }
const sessions: OpenCodeSession[] = []
try { try {
const files = await readdir(sessionDir) const projectDirs = await readdir(sessionBaseDir, { withFileTypes: true })
const sessionFiles = files.filter(f => f.startsWith("ses_") && f.endsWith(".json")) const dirs = projectDirs.filter(d => d.isDirectory()).map(d => d.name)
logger.info({ count: sessionFiles.length }, "Found OpenCode session files") await logDebug("Scanning project directories", { count: dirs.length })
for (const projectId of dirs) {
const sessionDir = join(sessionBaseDir, projectId)
for (const file of sessionFiles) {
try { try {
const filePath = join(sessionDir, file) const files = await readdir(sessionDir)
const content = await readFile(filePath, "utf-8") const firstSessionFile = files.find(f => f.startsWith("ses_") && f.endsWith(".json"))
const session = JSON.parse(content) as OpenCodeSession
sessions.push(session) if (firstSessionFile) {
} catch (error) { const content = await readFile(join(sessionDir, firstSessionFile), "utf-8")
logger.warn({ file, error }, "Failed to read session file") const sessionData = JSON.parse(content) as OpenCodeSession
if (!sessionData.directory) {
await logDebug("Session file missing directory field", { projectId, firstSessionFile })
continue
}
const sessionPath = sessionData.directory.replace(/\\/g, '/').toLowerCase().trim()
if (sessionPath === targetPath) {
await logDebug("MATCH FOUND!", { projectId, sessionPath })
// This is the correct directory, read all sessions
const sessions: OpenCodeSession[] = [sessionData]
const otherFiles = files.filter(f => f !== firstSessionFile && f.startsWith("ses_") && f.endsWith(".json"))
for (const file of otherFiles) {
try {
const fileContent = await readFile(join(sessionDir, file), "utf-8")
sessions.push(JSON.parse(fileContent) as OpenCodeSession)
} catch (e) {
logger.warn({ file, error: e }, "Failed to read session file")
}
}
await logDebug("Read sessions count", { count: sessions.length })
return sessions
} else {
// Just log a few mismatches to avoid bloating
// await logDebug("Mismatch", { sessionPath, targetPath })
}
}
} catch (e) {
await logDebug("Error scanning project directory", { projectId, error: String(e) })
} }
} }
} catch (error) { } catch (error) {
logger.error({ error }, "Failed to read OpenCode sessions directory") await logDebug("Failed to scan OpenCode sessions directory", { error: String(error) })
} }
return sessions await logDebug("No sessions found after scan")
return []
} }
export function registerSdkSyncRoutes(app: FastifyInstance, deps: SdkSyncRouteDeps) { export function registerSdkSyncRoutes(app: FastifyInstance, deps: SdkSyncRouteDeps) {
const logger = deps.logger.child({ component: "sdk-sync" }) const logger = deps.logger.child({ component: "sdk-sync" })
const sessionManager = getSessionManager(deps.dataDir) const sessionManager = getSessionManager(deps.dataDir)

View File

@@ -7,6 +7,8 @@ import {
Settings, Settings,
Plug, Plug,
Sparkles, Sparkles,
RefreshCw,
Download,
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
Folder, Folder,
@@ -21,6 +23,7 @@ import InstanceServiceStatus from "../instance-service-status"
import McpManager from "../mcp-manager" import McpManager from "../mcp-manager"
import { catalog, catalogLoading, catalogError, loadCatalog } from "../../stores/skills" import { catalog, catalogLoading, catalogError, loadCatalog } from "../../stores/skills"
import { getSessionSkills, setSessionSkills } from "../../stores/session-state" import { getSessionSkills, setSessionSkills } from "../../stores/session-state"
import { syncSessionsFromSdk } from "../../stores/session-api"
export interface FileNode { export interface FileNode {
name: string name: string
@@ -132,6 +135,7 @@ export const Sidebar: Component<SidebarProps> = (props) => {
const [rootFiles, setRootFiles] = createSignal<FileNode[]>([]) const [rootFiles, setRootFiles] = createSignal<FileNode[]>([])
const [lastRequestedTab, setLastRequestedTab] = createSignal<string | null>(null) const [lastRequestedTab, setLastRequestedTab] = createSignal<string | null>(null)
const [searchQuery, setSearchQuery] = createSignal("") const [searchQuery, setSearchQuery] = createSignal("")
const [syncing, setSyncing] = createSignal(false)
const [searchResults, setSearchResults] = createSignal<FileNode[]>([]) const [searchResults, setSearchResults] = createSignal<FileNode[]>([])
const [searchLoading, setSearchLoading] = createSignal(false) const [searchLoading, setSearchLoading] = createSignal(false)
const [gitStatus, setGitStatus] = createSignal<{ const [gitStatus, setGitStatus] = createSignal<{
@@ -322,6 +326,25 @@ export const Sidebar: Component<SidebarProps> = (props) => {
</Show> </Show>
<Show when={activeTab() === "sessions"}> <Show when={activeTab() === "sessions"}>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="px-2 mb-2">
<button
onClick={async () => {
setSyncing(true)
try {
await syncSessionsFromSdk(props.instanceId)
} finally {
setSyncing(false)
}
}}
disabled={syncing()}
class="w-full flex items-center justify-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-wide rounded-md bg-blue-500/10 text-blue-400 border border-blue-500/20 hover:bg-blue-500/20 disabled:opacity-50 transition-all"
>
<Show when={syncing()} fallback={<Download size={14} />}>
<RefreshCw size={14} class="animate-spin" />
</Show>
{syncing() ? "Syncing..." : "Sync SDK Sessions"}
</button>
</div>
<For each={props.sessions}> <For each={props.sessions}>
{(session) => ( {(session) => (
<div <div
@@ -479,8 +502,8 @@ export const Sidebar: Component<SidebarProps> = (props) => {
type="button" type="button"
onClick={() => toggleSkillSelection(skill.id)} onClick={() => toggleSkillSelection(skill.id)}
class={`w-full text-left px-3 py-2 rounded-md border transition-colors ${isSelected() class={`w-full text-left px-3 py-2 rounded-md border transition-colors ${isSelected()
? "border-blue-500/60 bg-blue-500/10 text-blue-200" ? "border-blue-500/60 bg-blue-500/10 text-blue-200"
: "border-white/10 bg-white/5 text-zinc-300 hover:text-white" : "border-white/10 bg-white/5 text-zinc-300 hover:text-white"
}`} }`}
> >
<div class="text-xs font-semibold">{skill.name}</div> <div class="text-xs font-semibold">{skill.name}</div>

View File

@@ -1153,6 +1153,27 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
updateSessionInfo(instanceId, sessionId) updateSessionInfo(instanceId, sessionId)
} }
async function syncSessionsFromSdk(instanceId: string): Promise<void> {
const instance = instances().get(instanceId)
if (!instance) throw new Error("Instance not ready")
const folderPath = instance.folder
if (!folderPath) throw new Error("No folder path for instance")
log.info({ instanceId, folderPath }, "Manual SDK sync requested")
try {
const result = await nativeSessionApi.syncFromSdk(instanceId, folderPath)
log.info({ instanceId, result }, "Manual SDK sync result")
// Refresh sessions after sync
await fetchSessions(instanceId)
} catch (error) {
log.error({ instanceId, error }, "Manual SDK sync failed")
throw error
}
}
export { export {
createSession, createSession,
deleteSession, deleteSession,
@@ -1160,6 +1181,7 @@ export {
fetchProviders, fetchProviders,
fetchSessions, fetchSessions,
syncSessionsFromSdk,
forkSession, forkSession,
loadMessages, loadMessages,
} }

View File

@@ -26,7 +26,7 @@ import {
setActiveParentSession, setActiveParentSession,
setActiveSession, setActiveSession,
setSessionDraftPrompt, setSessionDraftPrompt,
} from "./session-state" } from "./session-state"
import { getDefaultModel } from "./session-models" import { getDefaultModel } from "./session-models"
import { import {
@@ -35,6 +35,7 @@ import {
fetchAgents, fetchAgents,
fetchProviders, fetchProviders,
fetchSessions, fetchSessions,
syncSessionsFromSdk,
forkSession, forkSession,
loadMessages, loadMessages,
} from "./session-api" } from "./session-api"
@@ -88,6 +89,7 @@ export {
fetchAgents, fetchAgents,
fetchProviders, fetchProviders,
fetchSessions, fetchSessions,
syncSessionsFromSdk,
forkSession, forkSession,
getActiveParentSession, getActiveParentSession,
getActiveSession, getActiveSession,