refactor: simplify worktree management by removing legacy API

- Remove legacy worktree API usage and related state
- Add Manage Branches button in the Git header for quick access
- Introduce worktree status utilities to derive root branch hints
This commit is contained in:
Bohdan Triapitsyn
2026-02-06 12:38:09 +02:00
parent 2f336a0716
commit 0503f51357
26 changed files with 504 additions and 1450 deletions
+38 -30
View File
@@ -4,7 +4,7 @@ import type { Session } from "@opencode-ai/sdk/v2";
import { opencodeClient } from "@/lib/opencode/client";
import { getSafeStorage } from "./utils/safeStorage";
import type { WorktreeMetadata } from "@/types/worktree";
import { getWorktreeStatus, listWorktrees, mapWorktreeToMetadata } from "@/lib/git/worktreeService";
import { getWorktreeStatus } from "@/lib/worktrees/worktreeStatus";
import { listProjectWorktrees, removeProjectWorktree } from "@/lib/worktrees/worktreeManager";
import { useDirectoryStore } from "./useDirectoryStore";
import { useProjectsStore } from "./useProjectsStore";
@@ -52,8 +52,6 @@ type SessionStore = SessionState & SessionActions;
const safeStorage = getSafeStorage();
const SESSION_SELECTION_STORAGE_KEY = "oc.sessionSelectionByDirectory";
const WORKTREE_ROOT = ".openchamber";
type SessionSelectionMap = Record<string, string>;
const readSessionSelectionMap = (): SessionSelectionMap => {
@@ -278,11 +276,11 @@ const hydrateSessionWorktreeMetadata = async (
return null;
}
let worktreeEntries;
let worktreeEntries: WorktreeMetadata[];
try {
worktreeEntries = await listWorktrees(normalizedProject);
worktreeEntries = await listProjectWorktrees({ id: `path:${normalizedProject}`, path: normalizedProject });
} catch (error) {
console.debug("Failed to hydrate worktree metadata from git worktree list:", error);
console.debug("Failed to hydrate worktree metadata from worktree list:", error);
return null;
}
@@ -298,8 +296,7 @@ const hydrateSessionWorktreeMetadata = async (
}
const worktreeMapByPath = new Map<string, WorktreeMetadata>();
worktreeEntries.forEach((info) => {
const metadata = mapWorktreeToMetadata(normalizedProject, info);
worktreeEntries.forEach((metadata) => {
const normalizedPath = normalizePath(metadata.path) ?? metadata.path;
if (normalizedPath === normalizedProject) {
@@ -312,6 +309,26 @@ const hydrateSessionWorktreeMetadata = async (
let mutated = false;
const next = new Map(existingMetadata);
const mergeHydratedMetadata = (
hydrated: WorktreeMetadata,
previous?: WorktreeMetadata
): WorktreeMetadata => {
if (!previous) {
return hydrated;
}
return {
...previous,
...hydrated,
branch: hydrated.branch || previous.branch,
label: hydrated.label || previous.label,
name: hydrated.name || previous.name,
projectDirectory: hydrated.projectDirectory || previous.projectDirectory,
createdFromBranch: hydrated.createdFromBranch || previous.createdFromBranch,
kind: hydrated.kind || previous.kind,
status: hydrated.status || previous.status,
};
};
sessionsWithDirectory.forEach(({ id, directory }) => {
const metadata = worktreeMapByPath.get(directory);
if (!metadata) {
@@ -322,8 +339,19 @@ const hydrateSessionWorktreeMetadata = async (
}
const previous = next.get(id);
if (!previous || previous.path !== metadata.path || previous.branch !== metadata.branch || previous.label !== metadata.label) {
next.set(id, metadata);
const merged = mergeHydratedMetadata(metadata, previous);
if (
!previous ||
previous.path !== merged.path ||
previous.branch !== merged.branch ||
previous.label !== merged.label ||
previous.name !== merged.name ||
previous.projectDirectory !== merged.projectDirectory ||
previous.createdFromBranch !== merged.createdFromBranch ||
previous.kind !== merged.kind ||
previous.source !== merged.source
) {
next.set(id, merged);
mutated = true;
}
});
@@ -569,7 +597,6 @@ export const useSessionStore = create<SessionStore>()(
validPaths.add(normalizedProject);
if (isGitRepo) {
const worktreeRoot = `${normalizedProject}/${WORKTREE_ROOT}`;
try {
const candidates = new Set<string>();
@@ -584,25 +611,6 @@ export const useSessionStore = create<SessionStore>()(
}
});
// LEGACY_WORKTREES: check if .openchamber directory exists before listing it
// LEGACY_WORKTREES: filesystem scan fallback for legacy <project>/.openchamber/*
const projectEntriesList = await opencodeClient.listLocalDirectory(normalizedProject);
const worktreeDirExists = projectEntriesList.some(
(entry) => entry.isDirectory && entry.name === WORKTREE_ROOT
);
if (worktreeDirExists) {
const entries = await opencodeClient.listLocalDirectory(worktreeRoot);
entries
.filter((entry) => entry.isDirectory)
.forEach((entry) => {
const isAbsolutePath = /^([A-Za-z]:)?\//.test(entry.path);
const resolvedPath = isAbsolutePath ? entry.path : `${worktreeRoot}/${entry.name}`;
const normalizedPath = normalizePath(resolvedPath) ?? resolvedPath;
candidates.add(normalizedPath);
});
}
candidates.forEach((candidate) => {
const normalizedCandidate = normalizePath(candidate) ?? candidate;
validPaths.add(normalizedCandidate);