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
@@ -276,12 +276,6 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
New Session in Worktree
</DropdownMenuItem>
)}
{isRepo && !hideDirectoryControls && onOpenBranchPicker && (
<DropdownMenuItem onClick={onOpenBranchPicker}>
<RiGitRepositoryLine className="mr-1.5 h-4 w-4" />
Browse Branches
</DropdownMenuItem>
)}
{isRepo && !hideDirectoryControls && onNewSessionFromGitHubIssue && (
<DropdownMenuItem onClick={onNewSessionFromGitHubIssue}>
<RiGithubLine className="mr-1.5 h-4 w-4" />
@@ -300,6 +294,12 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
New Multi-Run
</DropdownMenuItem>
)}
{isRepo && !hideDirectoryControls && onOpenBranchPicker && (
<DropdownMenuItem onClick={onOpenBranchPicker}>
<RiGitRepositoryLine className="mr-1.5 h-4 w-4" />
Manage Branches
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={onClose}
className="text-destructive focus:text-destructive"
@@ -420,6 +420,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
const [branchPickerOpen, setBranchPickerOpen] = React.useState(false);
const [branchPickerProjectId, setBranchPickerProjectId] = React.useState<string | null>(null);
const [issuePickerOpen, setIssuePickerOpen] = React.useState(false);
const [pullRequestPickerOpen, setPullRequestPickerOpen] = React.useState(false);
const [activeDragId, setActiveDragId] = React.useState<string | null>(null);
@@ -619,7 +620,29 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
return next;
});
})
.catch(() => {
.catch(async () => {
// SDK worktrees can be outside UI runtime FS permissions.
// Probe via OpenCode API instead of local FS.
const looksLikeSdkWorktree =
directory.includes('/opencode/worktree/') ||
directory.includes('/.opencode/data/worktree/') ||
directory.includes('/.local/share/opencode/worktree/');
if (looksLikeSdkWorktree) {
const ok = await opencodeClient.probeDirectory(directory).catch(() => false);
if (ok) {
setDirectoryStatus((prev) => {
const next = new Map(prev);
if (next.get(directory) === 'exists') {
return prev;
}
next.set(directory, 'exists');
return next;
});
return;
}
}
setDirectoryStatus((prev) => {
const next = new Map(prev);
if (next.get(directory) === 'missing') {
@@ -1653,7 +1676,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}
createWorktreeSession();
}}
onOpenBranchPicker={() => setBranchPickerOpen(true)}
onOpenBranchPicker={() => {
setBranchPickerProjectId(projectKey);
setBranchPickerOpen(true);
}}
onNewSessionFromGitHubIssue={() => {
if (projectKey !== activeProjectId) {
setActiveProject(projectKey);
@@ -1712,8 +1738,9 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
<BranchPickerDialog
open={branchPickerOpen}
onOpenChange={setBranchPickerOpen}
projects={normalizedProjects}
activeProjectId={activeProjectId}
project={branchPickerProjectId
? normalizedProjects.find((p) => p.id === branchPickerProjectId) ?? null
: null}
/>
<GitHubIssuePickerDialog