worktrees - Implementation

This commit is contained in:
Shantur Rathore
2026-02-07 11:46:56 +00:00
parent 6f73adaef6
commit ef14b9acb6
17 changed files with 1399 additions and 72 deletions

View File

@@ -18,6 +18,7 @@ import {
fetchProviders,
clearInstanceDraftPrompts,
} from "./sessions"
import { ensureWorktreesLoaded, ensureWorktreeMapLoaded } from "./worktrees"
import { fetchCommands, clearCommands } from "./commands"
import { preferences } from "./preferences"
import { setSessionPendingPermission, setSessionPendingQuestion } from "./session-state"
@@ -136,10 +137,10 @@ function attachClient(descriptor: WorkspaceDescriptor) {
}
if (instance.client) {
sdkManager.destroyClient(descriptor.id)
sdkManager.destroyClientsForInstance(descriptor.id)
}
const client = sdkManager.createClient(descriptor.id, nextProxyPath)
const client = sdkManager.createClient(descriptor.id, nextProxyPath, "root")
updateInstance(descriptor.id, {
client,
port: nextPort ?? 0,
@@ -157,7 +158,7 @@ function releaseInstanceResources(instanceId: string) {
if (!instance) return
if (instance.client) {
sdkManager.destroyClient(instanceId)
sdkManager.destroyClientsForInstance(instanceId)
}
sseManager.seedStatus(instanceId, "disconnected")
}
@@ -227,6 +228,8 @@ async function syncPendingQuestions(instanceId: string): Promise<void> {
async function hydrateInstanceData(instanceId: string) {
try {
await ensureWorktreesLoaded(instanceId)
await ensureWorktreeMapLoaded(instanceId)
await fetchSessions(instanceId)
await fetchAgents(instanceId)
await fetchProviders(instanceId)

View File

@@ -1,5 +1,6 @@
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
import { instances } from "./instances"
import { getOrCreateWorktreeClient, getWorktreeSlugForSession } from "./worktrees"
import { addRecentModelPreference, getModelThinkingSelection, setAgentModelPreference } from "./preferences"
import { providers, sessions, withSession } from "./session-state"
@@ -83,6 +84,9 @@ async function sendMessage(
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const instanceSessions = sessions().get(instanceId)
const session = instanceSessions?.get(sessionId)
if (!session) {
@@ -204,7 +208,7 @@ async function sendMessage(
try {
log.info("session.promptAsync", { instanceId, sessionId, requestBody })
await requestData(
instance.client.session.promptAsync({
client.session.promptAsync({
sessionID: sessionId,
...(requestBody as any),
}),
@@ -227,6 +231,9 @@ async function executeCustomCommand(
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const session = sessions().get(instanceId)?.get(sessionId)
if (!session) {
throw new Error("Session not found")
@@ -256,7 +263,7 @@ async function executeCustomCommand(
}
await requestData(
instance.client.session.command({
client.session.command({
sessionID: sessionId,
...(body as any),
}),
@@ -270,6 +277,9 @@ async function runShellCommand(instanceId: string, sessionId: string, command: s
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const session = sessions().get(instanceId)?.get(sessionId)
if (!session) {
throw new Error("Session not found")
@@ -278,7 +288,7 @@ async function runShellCommand(instanceId: string, sessionId: string, command: s
const agent = session.agent || "build"
await requestData(
instance.client.session.shell({
client.session.shell({
sessionID: sessionId,
agent,
command,
@@ -293,12 +303,15 @@ async function abortSession(instanceId: string, sessionId: string): Promise<void
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
log.info("abortSession", { instanceId, sessionId })
try {
log.info("session.abort", { instanceId, sessionId })
await requestData(
instance.client.session.abort({
client.session.abort({
sessionID: sessionId,
}),
"session.abort",
@@ -370,6 +383,9 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const session = sessions().get(instanceId)?.get(sessionId)
if (!session) {
throw new Error("Session not found")
@@ -381,7 +397,7 @@ async function renameSession(instanceId: string, sessionId: string, nextTitle: s
}
await requestData(
instance.client.session.update({
client.session.update({
sessionID: sessionId,
title: trimmedTitle,
}),
@@ -403,8 +419,11 @@ async function deleteMessagePart(instanceId: string, sessionId: string, messageI
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
await requestData(
instance.client.part.delete({
client.part.delete({
sessionID: sessionId,
messageID: messageId,
partID: partId,

View File

@@ -32,6 +32,13 @@ import { messageStoreBus } from "./message-v2/bus"
import { clearCacheForSession } from "../lib/global-cache"
import { getLogger } from "../lib/logger"
import { requestData } from "../lib/opencode-api"
import {
getOrCreateWorktreeClient,
getRootClient,
getWorktreeSlugForSession,
removeParentSessionMapping,
setWorktreeSlugForParentSession,
} from "./worktrees"
const log = getLogger("api")
@@ -62,6 +69,8 @@ async function fetchSessions(instanceId: string): Promise<void> {
throw new Error("Instance not ready")
}
const rootClient = getRootClient(instanceId)
setLoading((prev) => {
const next = { ...prev }
next.fetchingSessions.set(instanceId, true)
@@ -70,7 +79,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
try {
log.info("session.list", { instanceId })
const response = await instance.client.session.list()
const response = await rootClient.session.list()
const sessionMap = new Map<string, Session>()
@@ -80,7 +89,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
let statusById: Record<string, any> = {}
try {
const statusResponse = await instance.client.session.status()
const statusResponse = await rootClient.session.status()
if (statusResponse.data && typeof statusResponse.data === "object") {
statusById = statusResponse.data as Record<string, any>
}
@@ -171,6 +180,12 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
throw new Error("Instance not ready")
}
// New parent sessions inherit the currently active session's worktree.
// If no session is active (fresh instance), fall back to root.
const activeId = activeSessionId().get(instanceId)
const worktreeSlug = activeId && activeId !== "info" ? getWorktreeSlugForSession(instanceId, activeId) : "root"
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const instanceAgents = agents().get(instanceId) || []
const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent")
const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "")
@@ -189,7 +204,7 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
try {
log.info(`[HTTP] POST /session.create for instance ${instanceId}`)
const response = await instance.client.session.create()
const response = await client.session.create()
if (!response.data) {
throw new Error("Failed to create session: No data returned")
@@ -260,6 +275,11 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
await cleanupBlankSessions(instanceId, session.id)
}
// Persist mapping for this *parent* session (best-effort).
await setWorktreeSlugForParentSession(instanceId, session.id, worktreeSlug).catch((error) => {
log.warn("Failed to persist session worktree mapping", { instanceId, sessionId: session.id, worktreeSlug, error })
})
return session
} catch (error) {
log.error("Failed to create session:", error)
@@ -283,6 +303,9 @@ async function forkSession(
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sourceSessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const request: { sessionID: string; messageID?: string } = {
sessionID: sourceSessionId,
messageID: options?.messageId,
@@ -290,7 +313,7 @@ async function forkSession(
log.info(`[HTTP] POST /session.fork for instance ${instanceId}`, request)
const info = await requestData<SessionForkResponse>(
instance.client.session.fork(request),
client.session.fork(request),
"session.fork",
)
const forkedSession = {
@@ -362,6 +385,11 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const deletingSession = sessions().get(instanceId)?.get(sessionId)
setLoading((prev) => {
const next = { ...prev }
const deleting = next.deletingSession.get(instanceId) || new Set()
@@ -372,7 +400,7 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
try {
log.info(`[HTTP] DELETE /session.delete for instance ${instanceId}`, { sessionId })
await requestData(instance.client.session.delete({ sessionID: sessionId }), "session.delete")
await requestData(client.session.delete({ sessionID: sessionId }), "session.delete")
setSessions((prev) => {
const next = new Map(prev)
@@ -416,6 +444,11 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
return next
})
}
// Clean up mapping for deleted parent sessions.
if (deletingSession?.parentId === null) {
await removeParentSessionMapping(instanceId, sessionId).catch(() => undefined)
}
} catch (error) {
log.error("Failed to delete session:", error)
throw error
@@ -437,9 +470,11 @@ async function fetchAgents(instanceId: string): Promise<void> {
throw new Error("Instance not ready")
}
const rootClient = getRootClient(instanceId)
try {
log.info(`[HTTP] GET /app.agents for instance ${instanceId}`)
const response = await instance.client.app.agents()
const response = await rootClient.app.agents()
const agentList = (response.data ?? []).map((agent) => ({
name: agent.name,
description: agent.description || "",
@@ -468,9 +503,11 @@ async function fetchProviders(instanceId: string): Promise<void> {
throw new Error("Instance not ready")
}
const rootClient = getRootClient(instanceId)
try {
log.info(`[HTTP] GET /config.providers for instance ${instanceId}`)
const response = await instance.client.config.providers()
const response = await rootClient.config.providers()
if (!response.data) return
const providerList = response.data.providers.map((provider) => ({
@@ -524,6 +561,9 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
throw new Error("Instance not ready")
}
const worktreeSlug = getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const instanceSessions = sessions().get(instanceId)
const session = instanceSessions?.get(sessionId)
if (!session) {
@@ -541,7 +581,7 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
try {
log.info(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId })
const apiMessages = await requestData<any[]>(
instance.client.session.messages({ sessionID: sessionId }),
client.session.messages({ sessionID: sessionId }),
"session.messages",
)

View File

@@ -37,6 +37,7 @@ import { updateSessionInfo } from "./message-v2/session-info"
import { tGlobal } from "../lib/i18n"
import { loadMessages } from "./session-api"
import { getOrCreateWorktreeClient, getRootClient, getWorktreeSlugForDirectory, getWorktreeSlugForSession } from "./worktrees"
import {
applyPartUpdateV2,
replaceMessageIdV2,
@@ -81,19 +82,34 @@ function applySessionStatus(instanceId: string, sessionId: string, status: Sessi
})
}
async function fetchSessionInfo(instanceId: string, sessionId: string): Promise<Session | null> {
async function fetchSessionInfo(instanceId: string, sessionId: string, directory?: string): Promise<Session | null> {
const instance = instances().get(instanceId)
if (!instance?.client) return null
const slugFromDirectory = getWorktreeSlugForDirectory(instanceId, directory)
const slug = slugFromDirectory ?? getWorktreeSlugForSession(instanceId, sessionId)
const client = getOrCreateWorktreeClient(instanceId, slug)
const rootClient = getRootClient(instanceId)
try {
const info = await requestData<any>(
instance.client.session.get({ sessionID: sessionId }),
client.session.get({ sessionID: sessionId }),
"session.get",
)
let fetchedStatus: SessionStatus = "idle"
try {
const statuses = await requestData<Record<string, any>>(instance.client.session.status(), "session.status")
let statuses: Record<string, any> = {}
try {
statuses = await requestData<Record<string, any>>(rootClient.session.status(), "session.status")
} catch {
statuses = await requestData<Record<string, any>>(client.session.status(), "session.status")
}
// Session status is global-ish; prefer the root context when available.
// (OpenCode may scope status by directory in older builds.)
// If root fails, fall back to the worktree-scoped client.
//
// Note: requestData throws on error, so we catch below.
const rawStatus = (info as any)?.status ?? statuses?.[sessionId]
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
fetchedStatus = hasType ? mapSdkSessionStatus(rawStatus) : "idle"
@@ -132,7 +148,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string): Promise<
}
}
function ensureSessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
function ensureSessionStatus(instanceId: string, sessionId: string, status: SessionStatus, directory?: string) {
const instanceSessions = sessions().get(instanceId)
const existing = instanceSessions?.get(sessionId)
if (existing) {
@@ -149,7 +165,7 @@ function ensureSessionStatus(instanceId: string, sessionId: string, status: Sess
}
const pending = (async () => {
const fetched = await fetchSessionInfo(instanceId, sessionId)
const fetched = await fetchSessionInfo(instanceId, sessionId, directory)
if (!fetched) return
applySessionStatus(instanceId, sessionId, status)
})()
@@ -197,7 +213,7 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
const messageId = typeof part.messageID === "string" ? part.messageID : fallbackMessageId
if (!sessionId || !messageId) return
if (part.type === "compaction") {
ensureSessionStatus(instanceId, sessionId, "compacting")
ensureSessionStatus(instanceId, sessionId, "compacting", (event as any)?.directory)
}
const store = messageStoreBus.getOrCreate(instanceId)
@@ -381,7 +397,7 @@ function handleSessionIdle(instanceId: string, event: EventSessionIdle): void {
const sessionId = event.properties?.sessionID
if (!sessionId) return
ensureSessionStatus(instanceId, sessionId, "idle")
ensureSessionStatus(instanceId, sessionId, "idle", (event as any)?.directory)
log.info(`[SSE] Session idle: ${sessionId}`)
}
@@ -390,7 +406,7 @@ function handleSessionStatus(instanceId: string, event: EventSessionStatus): voi
if (!sessionId) return
const status = mapSdkSessionStatus(event.properties.status)
ensureSessionStatus(instanceId, sessionId, status)
ensureSessionStatus(instanceId, sessionId, status, (event as any)?.directory)
log.info(`[SSE] Session status updated: ${sessionId}`, { status })
}
@@ -406,7 +422,7 @@ function handleSessionCompacted(instanceId: string, event: EventSessionCompacted
session.status = "working"
})
} else {
ensureSessionStatus(instanceId, sessionID, "working")
ensureSessionStatus(instanceId, sessionID, "working", (event as any)?.directory)
}
loadMessages(instanceId, sessionID, true).catch((error) => log.error("Failed to reload session after compaction", error))

View File

@@ -8,6 +8,7 @@ import { instances } from "./instances"
import { showConfirmDialog } from "./alerts"
import { getLogger } from "../lib/logger"
import { requestData } from "../lib/opencode-api"
import { getOrCreateWorktreeClient, getWorktreeSlugForSession } from "./worktrees"
import { tGlobal } from "../lib/i18n"
const log = getLogger("session")
@@ -602,12 +603,14 @@ async function isBlankSession(session: Session, instanceId: string, fetchIfNeede
return isFreshSession
}
let messages: any[] = []
try {
messages = await requestData<any[]>(
instance.client.session.messages({ sessionID: session.id }),
"session.messages",
)
} catch (error) {
try {
const worktreeSlug = getWorktreeSlugForSession(instanceId, session.id)
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
messages = await requestData<any[]>(
client.session.messages({ sessionID: session.id }),
"session.messages",
)
} catch (error) {
log.error(`Failed to fetch messages for session ${session.id}`, error)
return isFreshSession
}

View File

@@ -0,0 +1,264 @@
import { createSignal } from "solid-js"
import type { WorktreeDescriptor, WorktreeMap } from "../../../server/src/api-types"
import { serverApi } from "../lib/api-client"
import { sdkManager, type OpencodeClient } from "../lib/sdk-manager"
import { sessions } from "./session-state"
import { getLogger } from "../lib/logger"
const log = getLogger("api")
const [worktreesByInstance, setWorktreesByInstance] = createSignal<Map<string, WorktreeDescriptor[]>>(new Map())
const [worktreeMapByInstance, setWorktreeMapByInstance] = createSignal<Map<string, WorktreeMap>>(new Map())
const worktreeLoads = new Map<string, Promise<void>>()
const mapLoads = new Map<string, Promise<void>>()
function normalizeMap(input?: WorktreeMap | null): WorktreeMap {
if (!input || typeof input !== "object") {
return { version: 1, defaultWorktreeSlug: "root", parentSessionWorktreeSlug: {} }
}
return {
version: 1,
defaultWorktreeSlug: input.defaultWorktreeSlug || "root",
parentSessionWorktreeSlug: input.parentSessionWorktreeSlug ?? {},
}
}
async function ensureWorktreesLoaded(instanceId: string): Promise<void> {
if (!instanceId) return
if (worktreesByInstance().has(instanceId)) return
const existing = worktreeLoads.get(instanceId)
if (existing) return existing
const task = serverApi
.fetchWorktrees(instanceId)
.then((response) => {
setWorktreesByInstance((prev) => {
const next = new Map(prev)
next.set(instanceId, response.worktrees ?? [])
return next
})
})
.catch((error) => {
log.warn("Failed to load worktrees", { instanceId, error })
setWorktreesByInstance((prev) => {
const next = new Map(prev)
next.set(instanceId, [])
return next
})
})
.finally(() => {
worktreeLoads.delete(instanceId)
})
worktreeLoads.set(instanceId, task)
return task
}
async function reloadWorktrees(instanceId: string): Promise<void> {
if (!instanceId) return
await serverApi
.fetchWorktrees(instanceId)
.then((response) => {
setWorktreesByInstance((prev) => {
const next = new Map(prev)
next.set(instanceId, response.worktrees ?? [])
return next
})
})
.catch((error) => {
log.warn("Failed to reload worktrees", { instanceId, error })
})
}
async function createWorktree(instanceId: string, slug: string): Promise<{ slug: string; directory: string; branch?: string }> {
if (!instanceId) {
throw new Error("Missing instanceId")
}
const trimmed = (slug ?? "").trim()
if (!trimmed) {
throw new Error("Worktree name is required")
}
return await serverApi.createWorktree(instanceId, { slug: trimmed })
}
async function deleteWorktree(instanceId: string, slug: string, options?: { force?: boolean }): Promise<void> {
if (!instanceId) {
throw new Error("Missing instanceId")
}
const trimmed = (slug ?? "").trim()
if (!trimmed || trimmed === "root") {
throw new Error("Invalid worktree")
}
await serverApi.deleteWorktree(instanceId, trimmed, options)
}
async function ensureWorktreeMapLoaded(instanceId: string): Promise<void> {
if (!instanceId) return
if (worktreeMapByInstance().has(instanceId)) return
const existing = mapLoads.get(instanceId)
if (existing) return existing
const task = serverApi
.readWorktreeMap(instanceId)
.then((map) => {
setWorktreeMapByInstance((prev) => {
const next = new Map(prev)
next.set(instanceId, normalizeMap(map))
return next
})
})
.catch((error) => {
log.warn("Failed to load worktree map", { instanceId, error })
setWorktreeMapByInstance((prev) => {
const next = new Map(prev)
next.set(instanceId, normalizeMap(null))
return next
})
})
.finally(() => {
mapLoads.delete(instanceId)
})
mapLoads.set(instanceId, task)
return task
}
async function reloadWorktreeMap(instanceId: string): Promise<void> {
if (!instanceId) return
await serverApi
.readWorktreeMap(instanceId)
.then((map) => {
setWorktreeMapByInstance((prev) => {
const next = new Map(prev)
next.set(instanceId, normalizeMap(map))
return next
})
})
.catch((error) => {
log.warn("Failed to reload worktree map", { instanceId, error })
})
}
function getWorktrees(instanceId: string): WorktreeDescriptor[] {
return worktreesByInstance().get(instanceId) ?? []
}
function getWorktreeMap(instanceId: string): WorktreeMap {
return worktreeMapByInstance().get(instanceId) ?? normalizeMap(null)
}
function getDefaultWorktreeSlug(instanceId: string): string {
return getWorktreeMap(instanceId).defaultWorktreeSlug || "root"
}
async function setDefaultWorktreeSlug(instanceId: string, slug: string): Promise<void> {
await ensureWorktreeMapLoaded(instanceId)
const current = getWorktreeMap(instanceId)
const next: WorktreeMap = { ...current, defaultWorktreeSlug: slug }
setWorktreeMapByInstance((prev) => {
const map = new Map(prev)
map.set(instanceId, next)
return map
})
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
log.warn("Failed to persist default worktree", { instanceId, slug, error })
})
}
function getParentSessionId(instanceId: string, sessionId: string): string {
const session = sessions().get(instanceId)?.get(sessionId)
if (!session) return sessionId
return session.parentId ?? session.id
}
function getWorktreeSlugForParentSession(instanceId: string, parentSessionId: string): string {
const map = getWorktreeMap(instanceId)
return map.parentSessionWorktreeSlug[parentSessionId] ?? map.defaultWorktreeSlug ?? "root"
}
function getWorktreeSlugForSession(instanceId: string, sessionId: string): string {
const parentId = getParentSessionId(instanceId, sessionId)
return getWorktreeSlugForParentSession(instanceId, parentId)
}
async function setWorktreeSlugForParentSession(instanceId: string, parentSessionId: string, slug: string): Promise<void> {
await ensureWorktreeMapLoaded(instanceId)
const current = getWorktreeMap(instanceId)
const nextMapping = { ...(current.parentSessionWorktreeSlug ?? {}) }
nextMapping[parentSessionId] = slug
const next: WorktreeMap = { ...current, parentSessionWorktreeSlug: nextMapping }
setWorktreeMapByInstance((prev) => {
const map = new Map(prev)
map.set(instanceId, next)
return map
})
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
log.warn("Failed to persist session worktree mapping", { instanceId, parentSessionId, slug, error })
})
}
async function removeParentSessionMapping(instanceId: string, parentSessionId: string): Promise<void> {
await ensureWorktreeMapLoaded(instanceId)
const current = getWorktreeMap(instanceId)
if (!current.parentSessionWorktreeSlug[parentSessionId]) return
const nextMapping = { ...(current.parentSessionWorktreeSlug ?? {}) }
delete nextMapping[parentSessionId]
const next: WorktreeMap = { ...current, parentSessionWorktreeSlug: nextMapping }
setWorktreeMapByInstance((prev) => {
const map = new Map(prev)
map.set(instanceId, next)
return map
})
await serverApi.writeWorktreeMap(instanceId, next).catch((error) => {
log.warn("Failed to persist session worktree mapping removal", { instanceId, parentSessionId, error })
})
}
function getWorktreeSlugForDirectory(instanceId: string, directory: string | undefined): string | null {
if (!directory) return null
const list = getWorktrees(instanceId)
const match = list.find((wt) => wt.directory === directory)
return match?.slug ?? null
}
function buildWorktreeProxyPath(instanceId: string, slug: string): string {
const normalizedSlug = slug || "root"
return `/workspaces/${encodeURIComponent(instanceId)}/worktrees/${encodeURIComponent(normalizedSlug)}/instance`
}
function getOrCreateWorktreeClient(instanceId: string, slug: string): OpencodeClient {
const proxyPath = buildWorktreeProxyPath(instanceId, slug)
return sdkManager.createClient(instanceId, proxyPath, slug)
}
function getRootClient(instanceId: string): OpencodeClient {
return getOrCreateWorktreeClient(instanceId, "root")
}
export {
worktreesByInstance,
worktreeMapByInstance,
ensureWorktreesLoaded,
reloadWorktrees,
reloadWorktreeMap,
ensureWorktreeMapLoaded,
getWorktrees,
getWorktreeMap,
getDefaultWorktreeSlug,
setDefaultWorktreeSlug,
getParentSessionId,
getWorktreeSlugForParentSession,
getWorktreeSlugForSession,
setWorktreeSlugForParentSession,
removeParentSessionMapping,
getWorktreeSlugForDirectory,
buildWorktreeProxyPath,
getOrCreateWorktreeClient,
getRootClient,
createWorktree,
deleteWorktree,
}