diff --git a/.gitignore b/.gitignore index b5ab428..6c534bc 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,7 @@ install.log # ===================== OS Generated Files =============== Thumbs.db ehthumbs.db -Desktop.ini +44: Desktop.ini # ===================== Temporary Files ================== *.tmp @@ -100,4 +100,14 @@ packages/server/dist/ # ===================== Backup Files ===================== *.backup *_backup* -_backup_original/ \ No newline at end of file +_backup_original/ + +# ===================== NomadArch Specific Data ============ +.codenomad-data/ +**/logs/ +**/.codenomad-data/ +sdk-sync-debug.log +**/sessions.json +**/messages.json +**/workspaces.json +*.json.bak \ No newline at end of file diff --git a/packages/server/src/server/routes/sdk-sync.ts b/packages/server/src/server/routes/sdk-sync.ts index 2c27e47..e09219b 100644 --- a/packages/server/src/server/routes/sdk-sync.ts +++ b/packages/server/src/server/routes/sdk-sync.ts @@ -9,10 +9,9 @@ */ import { FastifyInstance } from "fastify" -import { readdir, readFile } from "fs/promises" +import { readdir, readFile, appendFile } from "fs/promises" import { existsSync } from "fs" import { join } from "path" -import { createHash } from "crypto" import { homedir } from "os" import { Logger } from "../../logger" import { getSessionManager } from "../../storage/session-store" @@ -59,54 +58,92 @@ function getOpenCodeStorageDir(): string { 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 */ async function readOpenCodeSessions(folderPath: string, logger: Logger): Promise { const storageDir = getOpenCodeStorageDir() - const projectId = generateProjectId(folderPath) - const sessionDir = join(storageDir, "session", projectId) + const sessionBaseDir = join(storageDir, "session") + 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)) { - logger.info({ sessionDir }, "OpenCode session directory not found") + // Normalize target folder path for comparison + 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 [] } - const sessions: OpenCodeSession[] = [] - try { - const files = await readdir(sessionDir) - const sessionFiles = files.filter(f => f.startsWith("ses_") && f.endsWith(".json")) + const projectDirs = await readdir(sessionBaseDir, { withFileTypes: true }) + 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 { - const filePath = join(sessionDir, file) - const content = await readFile(filePath, "utf-8") - const session = JSON.parse(content) as OpenCodeSession - sessions.push(session) - } catch (error) { - logger.warn({ file, error }, "Failed to read session file") + const files = await readdir(sessionDir) + const firstSessionFile = files.find(f => f.startsWith("ses_") && f.endsWith(".json")) + + if (firstSessionFile) { + const content = await readFile(join(sessionDir, firstSessionFile), "utf-8") + 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) { - 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) { const logger = deps.logger.child({ component: "sdk-sync" }) const sessionManager = getSessionManager(deps.dataDir) diff --git a/packages/ui/src/components/instance/sidebar.tsx b/packages/ui/src/components/instance/sidebar.tsx index aabbe81..66e4e3b 100644 --- a/packages/ui/src/components/instance/sidebar.tsx +++ b/packages/ui/src/components/instance/sidebar.tsx @@ -7,6 +7,8 @@ import { Settings, Plug, Sparkles, + RefreshCw, + Download, ChevronRight, ChevronDown, Folder, @@ -21,6 +23,7 @@ import InstanceServiceStatus from "../instance-service-status" import McpManager from "../mcp-manager" import { catalog, catalogLoading, catalogError, loadCatalog } from "../../stores/skills" import { getSessionSkills, setSessionSkills } from "../../stores/session-state" +import { syncSessionsFromSdk } from "../../stores/session-api" export interface FileNode { name: string @@ -132,6 +135,7 @@ export const Sidebar: Component = (props) => { const [rootFiles, setRootFiles] = createSignal([]) const [lastRequestedTab, setLastRequestedTab] = createSignal(null) const [searchQuery, setSearchQuery] = createSignal("") + const [syncing, setSyncing] = createSignal(false) const [searchResults, setSearchResults] = createSignal([]) const [searchLoading, setSearchLoading] = createSignal(false) const [gitStatus, setGitStatus] = createSignal<{ @@ -322,6 +326,25 @@ export const Sidebar: Component = (props) => {
+
+ +
{(session) => (
= (props) => { type="button" onClick={() => toggleSkillSelection(skill.id)} 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-white/10 bg-white/5 text-zinc-300 hover:text-white" + ? "border-blue-500/60 bg-blue-500/10 text-blue-200" + : "border-white/10 bg-white/5 text-zinc-300 hover:text-white" }`} >
{skill.name}
diff --git a/packages/ui/src/stores/session-api.ts b/packages/ui/src/stores/session-api.ts index 963ae1e..52aa13f 100644 --- a/packages/ui/src/stores/session-api.ts +++ b/packages/ui/src/stores/session-api.ts @@ -1153,6 +1153,27 @@ async function loadMessages(instanceId: string, sessionId: string, force = false updateSessionInfo(instanceId, sessionId) } +async function syncSessionsFromSdk(instanceId: string): Promise { + 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 { createSession, deleteSession, @@ -1160,6 +1181,7 @@ export { fetchProviders, fetchSessions, + syncSessionsFromSdk, forkSession, loadMessages, } diff --git a/packages/ui/src/stores/sessions.ts b/packages/ui/src/stores/sessions.ts index 2ab6f2b..5125ddc 100644 --- a/packages/ui/src/stores/sessions.ts +++ b/packages/ui/src/stores/sessions.ts @@ -26,7 +26,7 @@ import { setActiveParentSession, setActiveSession, setSessionDraftPrompt, - } from "./session-state" +} from "./session-state" import { getDefaultModel } from "./session-models" import { @@ -35,6 +35,7 @@ import { fetchAgents, fetchProviders, fetchSessions, + syncSessionsFromSdk, forkSession, loadMessages, } from "./session-api" @@ -88,6 +89,7 @@ export { fetchAgents, fetchProviders, fetchSessions, + syncSessionsFromSdk, forkSession, getActiveParentSession, getActiveSession,