opencode/packages/app/src/utils/workspace.ts
2026-08-06 15:14:44 +10:00

99 lines
4 KiB
TypeScript

import { pathKey } from "@/utils/path-key"
import type { WorkspaceDefaultDestination, WorkspaceLastUsed } from "@/context/settings"
import type { Session } from "@opencode-ai/sdk/v2/client"
type WorkspaceProject = { worktree: string; sandboxes?: readonly string[] }
export function workspaceDirectories(project: WorkspaceProject) {
return (project.sandboxes ?? []).filter(
(directory) => !containsDirectory(project.worktree, directory) || !containsDirectory(directory, project.worktree),
)
}
export function workspaceInventory<T extends WorkspaceProject & { id: string }>(projects: readonly T[]) {
return projects.flatMap((project) => workspaceDirectories(project).map((directory) => ({ directory, project })))
}
export function filterWorkspaceInventory<T extends { project: { id: string } }>(
workspaces: readonly T[],
project: string,
) {
if (project === "all") return [...workspaces]
return workspaces.filter((workspace) => workspace.project.id === project)
}
export function sessionsForWorkspace(sessions: readonly Session[], workspace: string) {
return sessions
.filter((session) => session.time.archived === undefined)
.filter((session) => containsDirectory(workspace, session.directory))
.toSorted((a, b) => b.time.updated - a.time.updated)
}
export function mergeWorkspaceSessionInventory(server: readonly Session[], cached: readonly Session[]) {
const sessions = new Map(server.map((session) => [session.id, session]))
cached.forEach((session) => {
const current = sessions.get(session.id)
if (!current || session.time.updated > current.time.updated) sessions.set(session.id, session)
})
return [...sessions.values()]
}
export function removeWorkspacesSequentially<T>(workspaces: readonly T[], remove: (workspace: T) => Promise<void>) {
return workspaces.reduce((previous, workspace) => previous.then(() => remove(workspace)), Promise.resolve())
}
export type WorkspaceDeleteInspection = "safe" | "active" | "linked" | "dirty"
export function inspectWorkspaceDeletion(input: {
workspace: string
activeDirectory?: string
sessions: readonly Session[]
status: "clean" | "dirty"
}): WorkspaceDeleteInspection {
if (input.activeDirectory && containsDirectory(input.workspace, input.activeDirectory)) return "active"
if (
input.sessions.some(
(session) => session.time?.archived === undefined && containsDirectory(input.workspace, session.directory),
)
)
return "linked"
if (input.status === "dirty") return "dirty"
return "safe"
}
export function isWorkspaceDirectory(project: WorkspaceProject | undefined, directory: string) {
if (!project || (containsDirectory(project.worktree, directory) && containsDirectory(directory, project.worktree)))
return false
return workspaceDirectories(project).some((workspace) => containsDirectory(workspace, directory))
}
export function isProjectDirectory(project: WorkspaceProject | undefined, directory: string) {
if (!project) return false
return [project.worktree, ...(project.sandboxes ?? [])].some((root) => containsDirectory(root, directory))
}
export function containsDirectory(parent: string, child: string) {
const normalize = (value: string) => {
const key = pathKey(value)
return /^[a-z]:\//i.test(key) || key.startsWith("//") ? key.toLowerCase() : key
}
const root = normalize(parent)
const target = normalize(child)
return target === root || target.startsWith(root.endsWith("/") ? root : `${root}/`)
}
export function isWorkspaceSelection(project: WorkspaceProject | undefined, selection: string) {
if (selection === "main" || selection === "create") return true
if (!project) return false
if (containsDirectory(project.worktree, selection) && containsDirectory(selection, project.worktree)) return true
return isWorkspaceDirectory(project, selection)
}
export function workspaceDefaultSelection(
setting: WorkspaceDefaultDestination,
lastUsed: WorkspaceLastUsed | undefined,
) {
if (setting === "local") return "main"
if (setting === "new") return "create"
return lastUsed === "workspace" ? "create" : "main"
}