diff --git a/packages/vscode/src/gitService.ts b/packages/vscode/src/gitService.ts index 4d915d80..35f24347 100644 --- a/packages/vscode/src/gitService.ts +++ b/packages/vscode/src/gitService.ts @@ -1385,74 +1385,11 @@ const loadProjectStartCommand = async (projectID: string): Promise => { } }; -const getProjectStoragePath = (projectID: string) => { - return path.join(getOpenCodeDataPath(), 'storage', 'project', `${projectID}.json`); -}; - -const updateProjectSandboxes = async ( - projectID: string, - primaryWorktree: string, - updater: (project: { - id: string; - worktree: string; - vcs: string; - sandboxes: string[]; - time: { created: number; updated: number }; - }) => void -) => { - const storagePath = getProjectStoragePath(projectID); - await fs.promises.mkdir(path.dirname(storagePath), { recursive: true }); - - const now = Date.now(); - const base = { - id: projectID, - worktree: primaryWorktree, - vcs: 'git', - sandboxes: [] as string[], - time: { created: now, updated: now }, - }; - - const parsed = await fs.promises.readFile(storagePath, 'utf8').then((raw) => JSON.parse(raw) as typeof base).catch(() => null); - const current = parsed && typeof parsed === 'object' ? { ...base, ...parsed } : base; - current.id = String(current.id || projectID); - current.worktree = String(current.worktree || primaryWorktree); - current.vcs = current.vcs || 'git'; - current.sandboxes = Array.isArray(current.sandboxes) - ? current.sandboxes.map((entry) => String(entry || '').trim()).filter(Boolean) - : []; - const createdAt = Number(current?.time?.created); - current.time = { - created: Number.isFinite(createdAt) && createdAt > 0 ? createdAt : now, - updated: now, - }; - - updater(current); - - current.sandboxes = [...new Set(current.sandboxes.map((entry) => String(entry || '').trim()).filter(Boolean))]; - await fs.promises.writeFile(storagePath, `${JSON.stringify(current, null, 2)}\n`, 'utf8'); -}; - -const syncProjectSandboxAdd = async (projectID: string, primaryWorktree: string, sandboxPath: string) => { - const sandbox = String(sandboxPath || '').trim(); - if (!sandbox) { - return; - } - await updateProjectSandboxes(projectID, primaryWorktree, (project) => { - if (!project.sandboxes.includes(sandbox)) { - project.sandboxes.push(sandbox); - } - }); -}; - -const syncProjectSandboxRemove = async (projectID: string, primaryWorktree: string, sandboxPath: string) => { - const sandbox = String(sandboxPath || '').trim(); - if (!sandbox) { - return; - } - await updateProjectSandboxes(projectID, primaryWorktree, (project) => { - project.sandboxes = project.sandboxes.filter((entry) => entry !== sandbox); - }); -}; +// OpenCode owns its own project/sandbox registry and records a worktree as a +// sandbox itself when an instance boots for that directory. OpenChamber used to +// write that state into OpenCode's storage JSON directly, behind the back of the +// running process — and since OpenCode v2 reads sandboxes from its database, the +// JSON write did not even reach it. Registration is not ours to perform. const isInsideOrSameDirectory = (root: string, target: string): boolean => { const relative = path.relative(root, target); @@ -1477,14 +1414,6 @@ const cleanupFailedFastWorktreeCreate = async ( const isInsideWorktreeRoot = isInsideOrSameDirectory(worktreeRoot, candidateDirectory) && candidateDirectory !== worktreeRoot; const isAttached = await isAttachedGitWorktreeDirectory(candidateDirectory); - if (!isAttached) { - try { - await syncProjectSandboxRemove(context.projectID, context.primaryWorktree, candidateDirectory); - } catch (error) { - console.warn('[GitService] Failed to clean up OpenCode sandbox metadata after worktree failure:', error instanceof Error ? error.message : String(error)); - } - } - if (!isInsideWorktreeRoot || isAttached) { return; } @@ -1963,12 +1892,6 @@ async function attachGitWorktreeToCandidate( await runGitCommandOrThrow(context.primaryWorktree, worktreeAddArgs, 'Failed to create git worktree'); - try { - await syncProjectSandboxAdd(context.projectID, context.primaryWorktree, candidate.directory); - } catch (error) { - console.warn('[GitService] Failed to sync OpenCode sandbox metadata (add):', error instanceof Error ? error.message : String(error)); - } - const shouldSetUpstream = Boolean(input?.setUpstream); const upstreamRemote = String(input?.upstreamRemote || inferredUpstream?.remote || '').trim(); const upstreamBranch = String(input?.upstreamBranch || inferredUpstream?.branch || '').trim(); @@ -2033,12 +1956,6 @@ export async function createWorktree(directory: string, input: CreateGitWorktree if (input?.returnAfterDirectoryCreated === true) { await fs.promises.mkdir(candidate.directory, { recursive: false }); - try { - await syncProjectSandboxAdd(context.projectID, context.primaryWorktree, candidate.directory); - } catch (error) { - console.warn('[GitService] Failed to sync OpenCode sandbox metadata (add):', error instanceof Error ? error.message : String(error)); - } - const bootstrapStatus = setWorktreeBootstrapState( candidate.directory, WORKTREE_BOOTSTRAP_PENDING, @@ -2129,12 +2046,6 @@ export async function removeWorktree(directory: string, input: RemoveGitWorktree await fs.promises.rm(targetDirectory, { recursive: true, force: true }); } - try { - await syncProjectSandboxRemove(context.projectID, context.primaryWorktree, targetDirectory); - } catch (error) { - console.warn('[GitService] Failed to sync OpenCode sandbox metadata (remove):', error instanceof Error ? error.message : String(error)); - } - clearWorktreeBootstrapState(targetDirectory); return true; @@ -2157,12 +2068,6 @@ export async function removeWorktree(directory: string, input: RemoveGitWorktree } } - try { - await syncProjectSandboxRemove(context.projectID, context.primaryWorktree, matchedEntry.worktree); - } catch (error) { - console.warn('[GitService] Failed to sync OpenCode sandbox metadata (remove):', error instanceof Error ? error.message : String(error)); - } - clearWorktreeBootstrapState(matchedEntry.worktree); return true; diff --git a/packages/web/server/lib/git/service.js b/packages/web/server/lib/git/service.js index ebe8c334..a3a99f62 100644 --- a/packages/web/server/lib/git/service.js +++ b/packages/web/server/lib/git/service.js @@ -1644,94 +1644,13 @@ const loadProjectStartCommand = async (projectID) => { } }; -const getProjectStoragePath = (projectID) => { - return path.join(getOpenCodeDataPath(), 'storage', 'project', `${projectID}.json`); -}; - -const syncSandboxesToOpenCodeDb = (projectID, sandboxes) => { - try { - const Database = require('better-sqlite3'); - const dbPath = path.join(getOpenCodeDataPath(), 'opencode.db'); - if (!fs.existsSync(dbPath)) return; - const db = new Database(dbPath); - try { - const row = db.prepare('SELECT sandboxes FROM project WHERE id = ?').get(projectID); - if (!row) return; - const json = JSON.stringify(sandboxes); - db.prepare('UPDATE project SET sandboxes = ?, time_updated = ? WHERE id = ?').run(json, Date.now(), projectID); - } finally { - db.close(); - } - } catch (error) { - console.warn('Failed to sync sandboxes to OpenCode DB:', error instanceof Error ? error.message : String(error)); - } -}; - -const updateProjectSandboxes = async (projectID, primaryWorktree, updater) => { - const storagePath = getProjectStoragePath(projectID); - await fsp.mkdir(path.dirname(storagePath), { recursive: true }); - - const now = Date.now(); - const base = { - id: projectID, - worktree: primaryWorktree, - vcs: 'git', - sandboxes: [], - time: { - created: now, - updated: now, - }, - }; - - const parsed = await fsp.readFile(storagePath, 'utf8').then((raw) => JSON.parse(raw)).catch(() => null); - const current = parsed && typeof parsed === 'object' ? { ...base, ...parsed } : base; - current.id = String(current.id || projectID); - current.worktree = String(current.worktree || primaryWorktree); - current.vcs = current.vcs || 'git'; - current.sandboxes = Array.isArray(current.sandboxes) - ? current.sandboxes.map((entry) => String(entry || '').trim()).filter(Boolean) - : []; - const createdAt = Number(current?.time?.created); - current.time = { - created: Number.isFinite(createdAt) && createdAt > 0 ? createdAt : now, - updated: now, - }; - - updater(current); - - current.sandboxes = [...new Set( - (Array.isArray(current.sandboxes) ? current.sandboxes : []) - .map((entry) => String(entry || '').trim()) - .filter(Boolean) - )]; - - await fsp.writeFile(storagePath, `${JSON.stringify(current, null, 2)}\n`, 'utf8'); - - // Sync to OpenCode's SQLite database so project.sandboxes is visible via the SDK - syncSandboxesToOpenCodeDb(projectID, current.sandboxes); -}; - -const syncProjectSandboxAdd = async (projectID, primaryWorktree, sandboxPath) => { - const sandbox = String(sandboxPath || '').trim(); - if (!sandbox) { - return; - } - await updateProjectSandboxes(projectID, primaryWorktree, (project) => { - if (!project.sandboxes.includes(sandbox)) { - project.sandboxes.push(sandbox); - } - }); -}; - -const syncProjectSandboxRemove = async (projectID, primaryWorktree, sandboxPath) => { - const sandbox = String(sandboxPath || '').trim(); - if (!sandbox) { - return; - } - await updateProjectSandboxes(projectID, primaryWorktree, (project) => { - project.sandboxes = project.sandboxes.filter((entry) => entry !== sandbox); - }); -}; +// OpenCode owns its own project/sandbox registry. It records a worktree as a +// sandbox itself when an instance boots for that directory, and filters entries +// whose directory no longer exists when reading them back. OpenChamber used to +// write that state directly into OpenCode's storage JSON and SQLite database, +// behind the back of the running process: the row changed but the server was +// never told, so a worktree created while OpenCode was running stayed unknown +// to it until a restart. Registration is not ours to perform. const isAttachedGitWorktreeDirectory = async (directory) => { try { @@ -1748,14 +1667,6 @@ const cleanupFailedFastWorktreeCreate = async (context, candidate) => { const isInsideWorktreeRoot = isInsideOrSameDirectory(worktreeRoot, candidateDirectory) && candidateDirectory !== worktreeRoot; const isAttached = await isAttachedGitWorktreeDirectory(candidateDirectory); - if (!isAttached) { - try { - await syncProjectSandboxRemove(context.projectID, context.primaryWorktree, candidateDirectory); - } catch (error) { - console.warn('Failed to clean up OpenCode sandbox metadata after worktree failure:', error instanceof Error ? error.message : String(error)); - } - } - if (!isInsideWorktreeRoot || isAttached) { return; } @@ -3940,12 +3851,6 @@ async function attachGitWorktreeToCandidate(context, candidate, input = {}) { await runGitCommandOrThrow(context.primaryWorktree, worktreeAddArgs, 'Failed to create git worktree'); - try { - await syncProjectSandboxAdd(context.projectID, context.primaryWorktree, candidate.directory); - } catch (error) { - console.warn('Failed to sync OpenCode sandbox metadata (add):', error instanceof Error ? error.message : String(error)); - } - const shouldSetUpstream = Boolean(input?.setUpstream); const upstreamRemote = String(input?.upstreamRemote || inferredUpstream?.remote || '').trim(); const upstreamBranch = String(input?.upstreamBranch || inferredUpstream?.branch || '').trim(); @@ -4005,12 +3910,6 @@ export async function createWorktree(directory, input = {}) { if (input?.returnAfterDirectoryCreated === true) { await fsp.mkdir(candidate.directory, { recursive: false }); - try { - await syncProjectSandboxAdd(context.projectID, context.primaryWorktree, candidate.directory); - } catch (error) { - console.warn('Failed to sync OpenCode sandbox metadata (add):', error instanceof Error ? error.message : String(error)); - } - const bootstrapStatus = setWorktreeBootstrapState( candidate.directory, WORKTREE_BOOTSTRAP_PENDING, @@ -4103,12 +4002,6 @@ export async function removeWorktree(directory, input = {}) { await fsp.rm(targetDirectory, { recursive: true, force: true }); } - try { - await syncProjectSandboxRemove(context.projectID, context.primaryWorktree, targetDirectory); - } catch (error) { - console.warn('Failed to sync OpenCode sandbox metadata (remove):', error instanceof Error ? error.message : String(error)); - } - clearWorktreeBootstrapState(targetDirectory); return true; @@ -4131,12 +4024,6 @@ export async function removeWorktree(directory, input = {}) { } } - try { - await syncProjectSandboxRemove(context.projectID, context.primaryWorktree, matchedEntry.worktree); - } catch (error) { - console.warn('Failed to sync OpenCode sandbox metadata (remove):', error instanceof Error ? error.message : String(error)); - } - clearWorktreeBootstrapState(matchedEntry.worktree); return true;