feat: migrate to OpenCode SDK worktrees with per-project config

Add SDK-based worktree management that lists and starts SDK worktrees
Migrate per-project setup to ~/.config/openchamber/<projectId>.json
Deprecate .openchamber legacy paths and adapt UI to new config
This commit is contained in:
Bohdan Triapitsyn
2026-01-27 20:00:07 +02:00
parent 63a4c32dfa
commit 415b043326
32 changed files with 1514 additions and 897 deletions
+1 -1
View File
@@ -26,7 +26,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.1.34",
"@opencode-ai/sdk": "^1.1.36",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
+21 -4
View File
@@ -64,6 +64,17 @@ const normalizeDirectoryPath = (value) => {
return trimmed;
};
const OPENCHAMBER_USER_CONFIG_ROOT = path.join(os.homedir(), '.config', 'openchamber');
const isPathWithinRoot = (resolvedPath, rootPath) => {
const resolvedRoot = path.resolve(rootPath || os.homedir());
const relative = path.relative(resolvedRoot, resolvedPath);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
};
const resolveWorkspacePath = (targetPath, baseDirectory) => {
const normalized = normalizeDirectoryPath(targetPath);
if (!normalized || typeof normalized !== 'string') {
@@ -72,13 +83,18 @@ const resolveWorkspacePath = (targetPath, baseDirectory) => {
const resolved = path.resolve(normalized);
const resolvedBase = path.resolve(baseDirectory || os.homedir());
const relative = path.relative(resolvedBase, resolved);
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
return { ok: false, error: 'Path is outside of active workspace' };
if (isPathWithinRoot(resolved, resolvedBase)) {
return { ok: true, base: resolvedBase, resolved };
}
return { ok: true, base: resolvedBase, resolved };
// Allow writing OpenChamber per-project config under ~/.config/openchamber.
// LEGACY_PROJECT_CONFIG: migration target root; allowed outside workspace.
if (isPathWithinRoot(resolved, OPENCHAMBER_USER_CONFIG_ROOT)) {
return { ok: true, base: path.resolve(OPENCHAMBER_USER_CONFIG_ROOT), resolved };
}
return { ok: false, error: 'Path is outside of active workspace' };
};
const resolveWorkspacePathFromContext = async (req, targetPath) => {
@@ -5942,6 +5958,7 @@ async function main(options = {}) {
});
app.post('/api/git/ignore-openchamber', async (req, res) => {
// LEGACY_WORKTREES: only needed for <project>/.openchamber era. Safe to remove after legacy support dropped.
const { ensureOpenChamberIgnored } = await getGitLibraries();
try {
const directory = req.query.directory;
+1
View File
@@ -56,6 +56,7 @@ export async function isGitRepository(directory) {
}
export async function ensureOpenChamberIgnored(directory) {
// LEGACY_WORKTREES: only needed for <project>/.openchamber era. Safe to remove after legacy support dropped.
const directoryPath = normalizeDirectoryPath(directory);
if (!directoryPath || !fs.existsSync(directoryPath)) {
return false;