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
+40
View File
@@ -136,6 +136,7 @@ const getDesktopFilesApi = (): FilesAPI | null => {
class OpencodeService {
private client: OpencodeClient;
private baseUrl: string;
private scopedClients: Map<string, OpencodeClient> = new Map();
private sseAbortControllers: Map<string, AbortController> = new Map();
private currentDirectory: string | undefined = undefined;
@@ -154,6 +155,26 @@ class OpencodeService {
this.client = createOpencodeClient({ baseUrl: this.baseUrl });
}
getBaseUrl(): string {
return this.baseUrl;
}
/**
* Returns an SDK client scoped to a project directory.
* Needed for worktree APIs where backend ignores per-call directory.
*/
getScopedApiClient(directory: string): OpencodeClient {
const normalized = this.normalizeCandidatePath(directory) ?? directory;
const key = normalized || '';
const existing = this.scopedClients.get(key);
if (existing) {
return existing;
}
const scoped = createOpencodeClient({ baseUrl: this.baseUrl, directory: normalized });
this.scopedClients.set(key, scoped);
return scoped;
}
private normalizeCandidatePath(path?: string | null): string | null {
if (typeof path !== 'string') {
return null;
@@ -309,6 +330,25 @@ class OpencodeService {
return this.deriveHomeDirectory(primary);
}
/**
* Best-effort probe whether a directory is accessible to OpenCode.
* This is intentionally NOT the same as local filesystem access in the UI runtime.
*/
async probeDirectory(directory: string): Promise<boolean> {
const normalized = this.normalizeCandidatePath(directory);
if (!normalized) {
return false;
}
try {
const response = await this.client.path.get({ directory: normalized });
const info = response.data as { directory?: unknown } | undefined;
const returned = typeof info?.directory === 'string' ? info.directory : null;
return Boolean(returned && returned.trim().length > 0);
} catch {
return false;
}
}
// Session Management
async listSessions(): Promise<Session[]> {
const response = await this.client.session.list(