From c4132543c368cdb3690f1f7ef0125e875518380e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 11 May 2026 11:07:36 -0400 Subject: [PATCH 1/2] refactor(project): drop persisted Project.sandboxes column The Project row stored a sandboxes string array that duplicated what git already tracks. The /experimental/worktree GET handler now reads from `Worktree.list()` (git worktree list --porcelain) instead, the write-side addSandbox/removeSandbox bookkeeping is gone, and the column is dropped from the schema with a Drizzle migration. The app maps the live list onto project.worktrees client-side instead of reading project.sandboxes. --- .../components/dialog-select-directory.tsx | 2 +- .../app/src/components/dialog-select-file.tsx | 4 +- .../app/src/components/prompt-input/submit.ts | 12 + .../src/components/session/session-header.tsx | 2 +- .../components/session/session-new-view.tsx | 4 +- packages/app/src/context/global-sync.tsx | 19 +- .../app/src/context/global-sync/bootstrap.ts | 32 +- .../src/context/global-sync/event-reducer.ts | 9 +- packages/app/src/context/global-sync/types.ts | 5 + packages/app/src/context/global-sync/utils.ts | 5 +- packages/app/src/context/layout.tsx | 9 +- packages/app/src/pages/layout.tsx | 24 +- .../app/src/pages/layout/sidebar-items.tsx | 2 +- .../migration.sql | 1 + .../snapshot.json | 1480 +++++++++++++++++ packages/opencode/src/project/project.sql.ts | 1 - packages/opencode/src/project/project.ts | 70 - .../instance/httpapi/groups/experimental.ts | 2 +- .../instance/httpapi/handlers/experimental.ts | 8 +- .../opencode/src/storage/json-migration.ts | 1 - packages/opencode/src/worktree/index.ts | 17 +- .../test/control-plane/workspace.test.ts | 1 - .../opencode/test/effect/run-service.test.ts | 1 - .../test/project/migrate-global.test.ts | 1 - .../opencode/test/project/project.test.ts | 64 - .../opencode/test/project/worktree.test.ts | 2 +- .../test/server/httpapi-experimental.test.ts | 15 +- .../test/storage/json-migration.test.ts | 1 - packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 167 +- packages/sdk/openapi.json | 422 +++-- 31 files changed, 1866 insertions(+), 519 deletions(-) create mode 100644 packages/opencode/migration/20260511143053_drop_project_sandboxes/migration.sql create mode 100644 packages/opencode/migration/20260511143053_drop_project_sandboxes/snapshot.json diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 005d287091..6f397f86c9 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -284,7 +284,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { for (const project of projects) { let at = 0 - const dirs = [project.worktree, ...(project.sandboxes ?? [])] + const dirs = [project.worktree, ...(project.worktrees ?? [])] for (const directory of dirs) { const sessions = sync.child(directory, { bootstrap: false })[0].session for (const session of sessions) { diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 63a321e46a..8f447c02a4 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -280,14 +280,14 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const project = createMemo(() => { const directory = projectDirectory() if (!directory) return - return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) + return layout.projects.list().find((p) => p.worktree === directory || p.worktrees?.includes(directory)) }) const workspaces = createMemo(() => { const directory = projectDirectory() const current = project() if (!current) return directory ? [directory] : [] - const dirs = [current.worktree, ...(current.sandboxes ?? [])] + const dirs = [current.worktree, ...(current.worktrees ?? [])] if (directory && !dirs.includes(directory)) return [...dirs, directory] return dirs }) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 05f0a3ed2c..15920ca5e2 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -4,6 +4,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { Binary } from "@opencode-ai/core/util/binary" import { useNavigate, useParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" +import { produce } from "solid-js/store" import type { FileSelection } from "@/context/file" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" @@ -342,6 +343,17 @@ export function createPromptSubmit(input: PromptSubmitInput) { return } WorktreeState.pending(createdWorktree.directory) + globalSync.set( + "project", + produce((draft) => { + const project = draft.find((item) => item.worktree === projectDirectory) + if (!project) return + project.worktrees = [ + createdWorktree.directory, + ...(project.worktrees ?? []).filter((worktree) => worktree !== createdWorktree.directory), + ] + }), + ) sessionDirectory = createdWorktree.directory } diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 3d4f58deec..94bdce6c0d 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -144,7 +144,7 @@ export function SessionHeader() { const project = createMemo(() => { const directory = projectDirectory() if (!directory) return - return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) + return layout.projects.list().find((p) => p.worktree === directory || p.worktrees?.includes(directory)) }) const name = createMemo(() => { const current = project() diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx index 36c1eb42c3..085f7e4d90 100644 --- a/packages/app/src/components/session/session-new-view.tsx +++ b/packages/app/src/components/session/session-new-view.tsx @@ -20,8 +20,8 @@ export function NewSessionView(props: NewSessionViewProps) { const sdk = useSDK() const language = useLanguage() - const sandboxes = createMemo(() => sync.project?.sandboxes ?? []) - const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE]) + const worktrees = createMemo(() => sync.project?.worktrees ?? []) + const options = createMemo(() => [MAIN_WORKTREE, ...worktrees(), CREATE_WORKTREE]) const current = createMemo(() => { const selection = props.worktree if (options().includes(selection)) return selection diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 31c90463d8..4d86d2842a 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -2,7 +2,6 @@ import type { Config, OpencodeClient, Path, - Project, ProviderAuthResponse, ProviderListResponse, Todo, @@ -27,7 +26,7 @@ import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } fr import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch" import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load" import { trimSessions } from "./global-sync/session-trim" -import type { ProjectMeta } from "./global-sync/types" +import type { ProjectInfo, ProjectMeta } from "./global-sync/types" import { SESSION_RECENT_LIMIT } from "./global-sync/types" import { formatServerError } from "@/utils/server-errors" import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" @@ -38,7 +37,7 @@ type GlobalStore = { ready: boolean error?: InitError path: Path - project: Project[] + project: ProjectInfo[] session_todo: { [sessionID: string]: Todo[] } @@ -48,6 +47,10 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } +const isProjectList = (input: unknown): input is ProjectInfo[] => Array.isArray(input) +const isProjectUpdate = (input: unknown): input is (draft: ProjectInfo[]) => ProjectInfo[] => + typeof input === "function" + export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const @@ -122,13 +125,13 @@ function createGlobalSync() { if (eventTimer !== undefined) clearTimeout(eventTimer) }) - const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => { + const setProjects = (next: ProjectInfo[] | ((draft: ProjectInfo[]) => ProjectInfo[])) => { setGlobalStore("project", next) } const setBootStore = ((...input: unknown[]) => { - if (input[0] === "project" && Array.isArray(input[1])) { - setProjects(input[1] as Project[]) + if (input[0] === "project" && isProjectList(input[1])) { + setProjects(input[1]) return input[1] } return (setGlobalStore as (...args: unknown[]) => unknown)(...input) @@ -151,8 +154,8 @@ function createGlobalSync() { })) const set = ((...input: unknown[]) => { - if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) { - setProjects(input[1] as Project[] | ((draft: Project[]) => Project[])) + if (input[0] === "project" && (isProjectList(input[1]) || isProjectUpdate(input[1]))) { + setProjects(input[1]) return input[1] } return (setGlobalStore as (...args: unknown[]) => unknown)(...input) diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 531917bde6..e8eb814809 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -3,7 +3,6 @@ import type { OpencodeClient, Path, PermissionRequest, - Project, ProviderAuthResponse, ProviderListResponse, QuestionRequest, @@ -15,7 +14,7 @@ import { getFilename } from "@opencode-ai/core/util/path" import { retry } from "@opencode-ai/core/util/retry" import { batch } from "solid-js" import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" -import type { State, VcsCache } from "./types" +import type { ProjectInfo, State, VcsCache } from "./types" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" import { QueryClient, queryOptions } from "@tanstack/solid-query" @@ -24,7 +23,7 @@ import { loadMcpQuery } from "../global-sync" type GlobalStore = { ready: boolean path: Path - project: Project[] + project: ProjectInfo[] session_todo: { [sessionID: string]: Todo[] } @@ -94,12 +93,21 @@ export const loadProjectsQuery = (sdk: OpencodeClient) => queryKey: ["project"], queryFn: () => retry(() => - sdk.project.list().then((x) => { - return (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => cmp(a.id, b.id)) + sdk.project.list().then(async (x) => { + return Promise.all( + (x.data ?? []) + .filter((p) => !!p?.id) + .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) + .slice() + .sort((a, b) => cmp(a.id, b.id)) + .map(async (project) => ({ + ...project, + worktrees: await sdk.worktree + .list({ directory: project.worktree }) + .then((x) => x.data ?? []) + .catch(() => []), + })), + ) }), ), }) @@ -140,8 +148,8 @@ function groupBySession(input: T[]) }, {}) } -function projectID(directory: string, projects: Project[]) { - return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id +function projectID(directory: string, projects: ProjectInfo[]) { + return projects.find((project) => project.worktree === directory || project.worktrees?.includes(directory))?.id } function mergeSession(setStore: SetStoreFunction, session: Session) { @@ -207,7 +215,7 @@ export async function bootstrapDirectory(input: { global: { config: Config path: Path - project: Project[] + project: ProjectInfo[] provider: ProviderListResponse } queryClient: QueryClient diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 13d34ef6c5..4c929c66e3 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -4,14 +4,13 @@ import type { Message, Part, PermissionRequest, - Project, QuestionRequest, Session, SessionStatus, SnapshotFileDiff, Todo, } from "@opencode-ai/sdk/v2/client" -import type { State, VcsCache } from "./types" +import type { ProjectInfo, State, VcsCache } from "./types" import { trimSessions } from "./session-trim" import { dropSessionCaches } from "./session-cache" import { diffs as list, message as clean } from "@/utils/diffs" @@ -20,8 +19,8 @@ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) export function applyGlobalEvent(input: { event: { type: string; properties?: unknown } - project: Project[] - setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void + project: ProjectInfo[] + setGlobalProject: (next: ProjectInfo[] | ((draft: ProjectInfo[]) => ProjectInfo[])) => void refresh: () => void }) { if (input.event.type === "global.disposed" || input.event.type === "server.connected") { @@ -30,7 +29,7 @@ export function applyGlobalEvent(input: { } if (input.event.type !== "project.updated") return - const properties = input.event.properties as Project + const properties = input.event.properties as ProjectInfo const result = Binary.search(input.project, properties.id, (s) => s.id) if (result.found) { input.setGlobalProject( diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 6bf42a0737..43230c1eab 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -8,6 +8,7 @@ import type { Part, Path, PermissionRequest, + Project, ProviderListResponse, QuestionRequest, Session, @@ -30,6 +31,10 @@ export type ProjectMeta = { } } +export type ProjectInfo = Project & { + worktrees?: string[] +} + export type State = { status: "loading" | "partial" | "complete" agent: Agent[] diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index b982990884..07268270d1 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -1,4 +1,5 @@ -import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" +import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2/client" +import type { ProjectInfo } from "./types" export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key" export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) @@ -27,7 +28,7 @@ export function normalizeProviderList(input: ProviderListResponse): ProviderList } } -export function sanitizeProject(project: Project) { +export function sanitizeProject(project: ProjectInfo) { if (!project.icon?.url && !project.icon?.override) return project return { ...project, diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index cacc875c54..43a2a3b6f8 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -6,8 +6,8 @@ import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" import { useServer } from "./server" import { usePlatform } from "./platform" -import { Project } from "@opencode-ai/sdk/v2" import { Persist, persisted, removePersisted } from "@/utils/persist" +import type { ProjectInfo } from "./global-sync/types" import { decode64 } from "@/utils/base64" import { same } from "@/utils/same" import { createScrollPersistence, type SessionScroll } from "./layout-scroll" @@ -51,7 +51,7 @@ type TabHandoff = { at: number } -export type LocalProject = Partial & { worktree: string; expanded: boolean } +export type LocalProject = Partial & { worktree: string; expanded: boolean } export type ReviewDiffStyle = "unified" | "split" @@ -404,9 +404,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const roots = createMemo(() => { const map = new Map() for (const project of globalSync.data.project) { - const sandboxes = project.sandboxes ?? [] - for (const sandbox of sandboxes) { - map.set(sandbox, project.worktree) + for (const worktree of project.worktrees ?? []) { + map.set(worktree, project.worktree) } } return map diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index a08372649f..f75dbd58d5 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -560,8 +560,8 @@ export default function Layout(props: ParentProps) { const projects = layout.projects.list() - const sandbox = projects.find((p) => p.sandboxes?.some((item) => pathKey(item) === key)) - if (sandbox) return sandbox + const worktree = projects.find((p) => p.worktrees?.some((item) => pathKey(item) === key)) + if (worktree) return worktree const direct = projects.find((p) => pathKey(p.worktree) === key) if (direct) return direct @@ -646,7 +646,7 @@ export default function Layout(props: ParentProps) { if (!expanded) continue const key = pathKey(directory) const project = projects.find( - (item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key), + (item) => pathKey(item.worktree) === key || item.worktrees?.some((worktree) => pathKey(worktree) === key), ) if (!project) continue if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue @@ -1223,7 +1223,7 @@ export default function Layout(props: ParentProps) { const key = pathKey(directory) const project = layout.projects .list() - .find((item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key)) + .find((item) => pathKey(item.worktree) === key || item.worktrees?.some((worktree) => pathKey(worktree) === key)) if (project) return project.worktree const known = Object.entries(store.workspaceOrder).find( @@ -1275,7 +1275,7 @@ export default function Layout(props: ParentProps) { server.projects.touch(root) const project = layout.projects.list().find((item) => item.worktree === root) let dirs = project - ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) + ? effectiveWorkspaceOrder(root, [root, ...(project.worktrees ?? [])], store.workspaceOrder[root]) : [root] const canOpen = (value: string | undefined) => { if (!value) return false @@ -1514,7 +1514,7 @@ export default function Layout(props: ParentProps) { produce((draft) => { const project = draft.find((item) => item.worktree === root) if (!project) return - project.sandboxes = (project.sandboxes ?? []).filter((sandbox) => sandbox !== directory) + project.worktrees = (project.worktrees ?? []).filter((worktree) => worktree !== directory) }), ) setStore("workspaceOrder", root, (order) => (order ?? []).filter((workspace) => workspace !== directory)) @@ -1528,7 +1528,7 @@ export default function Layout(props: ParentProps) { const nextKey = pathKey(nextCurrent) const project = layout.projects.list().find((item) => item.worktree === root) const dirs = project - ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) + ? effectiveWorkspaceOrder(root, [root, ...(project.worktrees ?? [])], store.workspaceOrder[root]) : [root] const valid = dirs.some((item) => pathKey(item) === nextKey) @@ -1862,7 +1862,7 @@ export default function Layout(props: ParentProps) { function workspaceIds(project: LocalProject | undefined) { if (!project) return [] const local = project.worktree - const dirs = [local, ...(project.sandboxes ?? [])] + const dirs = [local, ...(project.worktrees ?? [])] const active = currentProject() const directory = pathKey(active?.worktree ?? "") === pathKey(project.worktree) ? currentDir() : undefined const extra = @@ -1954,6 +1954,14 @@ export default function Layout(props: ParentProps) { }) return [created.directory, ...next] }) + globalSync.set( + "project", + produce((draft) => { + const item = draft.find((item) => item.worktree === project.worktree) + if (!item) return + item.worktrees = [created.directory, ...(item.worktrees ?? []).filter((worktree) => pathKey(worktree) !== key)] + }), + ) globalSync.child(created.directory) navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index f27a9bb7a9..9b90c28b79 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -35,7 +35,7 @@ export const ProjectIcon = (props: { const globalSync = useGlobalSync() const notification = useNotification() const permission = usePermission() - const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) + const dirs = createMemo(() => [props.project.worktree, ...(props.project.worktrees ?? [])]) const unseenCount = createMemo(() => dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) diff --git a/packages/opencode/migration/20260511143053_drop_project_sandboxes/migration.sql b/packages/opencode/migration/20260511143053_drop_project_sandboxes/migration.sql new file mode 100644 index 0000000000..4195d31f69 --- /dev/null +++ b/packages/opencode/migration/20260511143053_drop_project_sandboxes/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `project` DROP COLUMN `sandboxes`; \ No newline at end of file diff --git a/packages/opencode/migration/20260511143053_drop_project_sandboxes/snapshot.json b/packages/opencode/migration/20260511143053_drop_project_sandboxes/snapshot.json new file mode 100644 index 0000000000..22a34c720c --- /dev/null +++ b/packages/opencode/migration/20260511143053_drop_project_sandboxes/snapshot.json @@ -0,0 +1,1480 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "a74c730d-3c94-47de-94fc-252a43282f27", + "prevIds": ["fdfcccee-fb3a-481f-b801-b9835fa30d5d"], + "ddl": [ + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "columns": ["active_account_id"], + "tableTo": "account", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["aggregate_id"], + "tableTo": "event_sequence", + "columnsTo": ["aggregate_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": ["email", "url"], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": ["name"], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": ["aggregate_id"], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts index 2d486114a3..2eeeb1a2e6 100644 --- a/packages/opencode/src/project/project.sql.ts +++ b/packages/opencode/src/project/project.sql.ts @@ -12,6 +12,5 @@ export const ProjectTable = sqliteTable("project", { icon_color: text(), ...Timestamps, time_initialized: integer(), - sandboxes: text({ mode: "json" }).notNull().$type(), commands: text({ mode: "json" }).$type<{ start?: string }>(), }) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 91d272ea63..a0903422e8 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -51,7 +51,6 @@ export const Info = Schema.Struct({ icon: optionalOmitUndefined(ProjectIcon), commands: optionalOmitUndefined(ProjectCommands), time: ProjectTime, - sandboxes: Schema.Array(Schema.String), }) .annotate({ identifier: "Project" }) .pipe(withStatics((s) => ({ zod: zod(s) }))) @@ -83,7 +82,6 @@ export function fromRow(row: Row): Info { updated: row.time_updated, initialized: row.time_initialized ?? undefined, }, - sandboxes: row.sandboxes, commands: row.commands ?? undefined, } } @@ -123,9 +121,6 @@ export interface Interface { readonly update: (input: UpdateInput) => Effect.Effect readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect readonly setInitialized: (id: ProjectID) => Effect.Effect - readonly sandboxes: (id: ProjectID) => Effect.Effect - readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect - readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect } export class Service extends Context.Service()("@opencode/Project") {} @@ -283,7 +278,6 @@ export const layer: Layer.Layer< id: data.id, worktree: data.worktree, vcs: data.vcs, - sandboxes: [] as string[], time: { created: Date.now(), updated: Date.now() }, } @@ -295,17 +289,6 @@ export const layer: Layer.Layer< vcs: data.vcs, time: { ...existing.time, updated: Date.now() }, } - if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox)) - result.sandboxes.push(data.sandbox) - result.sandboxes = yield* Effect.forEach( - result.sandboxes, - (s) => - fs.exists(s).pipe( - Effect.orDie, - Effect.map((exists) => (exists ? s : undefined)), - ), - { concurrency: "unbounded" }, - ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined))) yield* db((d) => d @@ -321,7 +304,6 @@ export const layer: Layer.Layer< time_created: result.time.created, time_updated: result.time.updated, time_initialized: result.time.initialized, - sandboxes: result.sandboxes, commands: result.commands, }) .onConflictDoUpdate({ @@ -335,7 +317,6 @@ export const layer: Layer.Layer< icon_color: result.icon?.color, time_updated: result.time.updated, time_initialized: result.time.initialized, - sandboxes: result.sandboxes, commands: result.commands, }, }) @@ -441,54 +422,6 @@ export const layer: Layer.Layer< yield* InstanceState.get(initState) }) - const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) { - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - if (!row) return [] - const data = fromRow(row) - return yield* Effect.forEach( - data.sandboxes, - (dir) => - fs.isDir(dir).pipe( - Effect.orDie, - Effect.map((ok) => (ok ? dir : undefined)), - ), - { concurrency: "unbounded" }, - ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined))) - }) - - const addSandbox = Effect.fn("Project.addSandbox")(function* (id: ProjectID, directory: string) { - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - if (!row) throw new Error(`Project not found: ${id}`) - const sboxes = [...row.sandboxes] - if (!sboxes.includes(directory)) sboxes.push(directory) - const result = yield* db((d) => - d - .update(ProjectTable) - .set({ sandboxes: sboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, id)) - .returning() - .get(), - ) - if (!result) throw new Error(`Project not found: ${id}`) - yield* emitUpdated(fromRow(result)) - }) - - const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectID, directory: string) { - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - if (!row) throw new Error(`Project not found: ${id}`) - const sboxes = row.sandboxes.filter((s) => s !== directory) - const result = yield* db((d) => - d - .update(ProjectTable) - .set({ sandboxes: sboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, id)) - .returning() - .get(), - ) - if (!result) throw new Error(`Project not found: ${id}`) - yield* emitUpdated(fromRow(result)) - }) - return Service.of({ init, fromDirectory, @@ -498,9 +431,6 @@ export const layer: Layer.Layer< update, initGit, setInitialized, - sandboxes, - addSandbox, - removeSandbox, }) }), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts index 99a8a21a9e..b65df6fe68 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts @@ -145,7 +145,7 @@ export const ExperimentalApi = HttpApi.make("experimental") OpenApi.annotations({ identifier: "worktree.list", summary: "List worktrees", - description: "List all sandbox worktrees for the current project.", + description: "List all git worktrees for the current project.", }), ), HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts index 55272fc2f2..20f100f5ae 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts @@ -1,9 +1,7 @@ import { Account } from "@/account/account" import { Agent } from "@/agent/agent" import { Config } from "@/config/config" -import { InstanceState } from "@/effect/instance-state" import { MCP } from "@/mcp" -import { Project } from "@/project/project" import { Session } from "@/session/session" import { ToolRegistry } from "@/tool/registry" import * as EffectZod from "@opencode-ai/core/effect-zod" @@ -20,7 +18,6 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper const agents = yield* Agent.Service const config = yield* Config.Service const mcp = yield* MCP.Service - const project = yield* Project.Service const registry = yield* ToolRegistry.Service const worktreeSvc = yield* Worktree.Service @@ -93,8 +90,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper }) const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () { - const ctx = yield* InstanceState.context - return yield* project.sandboxes(ctx.project.id) + return yield* worktreeSvc.list().pipe(Effect.map((items) => items.map((item) => item.directory))) }) const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: { @@ -106,9 +102,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: { payload: Worktree.RemoveInput }) { - const ctx = yield* InstanceState.context yield* worktreeSvc.remove(input.payload) - yield* project.removeSandbox(ctx.project.id, input.payload.directory) return true }) diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index bc1aae6fa2..6c19c6fed6 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -173,7 +173,6 @@ export async function run(db: SQLiteBunDatabase | NodeSQLiteDatabase = Layer.effect( Service, Effect.gen(function* () { @@ -172,7 +167,6 @@ export const layer: Layer.Layer< const pathSvc = yield* Path.Path const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const gitSvc = yield* Git.Service - const project = yield* Project.Service const store = yield* InstanceStore.Service const git = Effect.fnUntraced( @@ -233,8 +227,6 @@ export const layer: Layer.Layer< if (created.code !== 0) { throw new CreateFailedError({ message: created.stderr || created.text || "Failed to create git worktree" }) } - - yield* project.addSandbox(ctx.project.id, info.directory).pipe(Effect.catch(() => Effect.void)) }) const boot = Effect.fnUntraced(function* (info: Info, startCommand?: string) { @@ -312,7 +304,7 @@ export const layer: Layer.Layer< return text .split("\n") .map((line) => line.trim()) - .reduce<{ path?: string; branch?: string }[]>((acc, line) => { + .reduce<{ path?: string; branch?: string; prunable?: boolean }[]>((acc, line) => { if (!line) return acc if (line.startsWith("worktree ")) { acc.push({ path: line.slice("worktree ".length).trim() }) @@ -323,6 +315,9 @@ export const layer: Layer.Layer< if (line.startsWith("branch ")) { current.branch = line.slice("branch ".length).trim() } + if (line === "prunable") { + current.prunable = true + } return acc }, []) } @@ -354,6 +349,7 @@ export const layer: Layer.Layer< const primaryName = pathSvc.basename(primary).toLowerCase() return yield* Effect.forEach(parseWorktreeList(result.text), (entry) => Effect.gen(function* () { + if (entry.prunable) return undefined if (!entry.path) return undefined const directory = yield* canonical(entry.path) if (directory === primary) return undefined @@ -612,7 +608,6 @@ export const layer: Layer.Layer< export const appLayer = layer.pipe( Layer.provide(Git.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer), - Layer.provide(Project.defaultLayer), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer), ) diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index 3c4837e318..32876b5d85 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -324,7 +324,6 @@ function insertProject(id: ProjectID, worktree: string) { name: null, time_created: Date.now(), time_updated: Date.now(), - sandboxes: [], }) .run(), ) diff --git a/packages/opencode/test/effect/run-service.test.ts b/packages/opencode/test/effect/run-service.test.ts index 16538bb8ae..b6dc62d02e 100644 --- a/packages/opencode/test/effect/run-service.test.ts +++ b/packages/opencode/test/effect/run-service.test.ts @@ -82,7 +82,6 @@ it.live("makeRuntime inherits InstanceRef from the current fiber", () => id: ProjectID.global, worktree: testDirectory, time: { created: 0, updated: 0 }, - sandboxes: [], }, }), ), diff --git a/packages/opencode/test/project/migrate-global.test.ts b/packages/opencode/test/project/migrate-global.test.ts index c476c108b4..1590fbfd6f 100644 --- a/packages/opencode/test/project/migrate-global.test.ts +++ b/packages/opencode/test/project/migrate-global.test.ts @@ -55,7 +55,6 @@ function ensureGlobal() { worktree: "/", time_created: Date.now(), time_updated: Date.now(), - sandboxes: [], }) .onConflictDoNothing() .run(), diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 9906b31645..433e2bef04 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -153,7 +153,6 @@ describe("Project.fromDirectory with worktrees", () => { expect(project.worktree).toBe(tmp.path) expect(sandbox).toBe(tmp.path) - expect(project.sandboxes).not.toContain(tmp.path) }) test("should set worktree to root when called from a worktree", async () => { @@ -167,8 +166,6 @@ describe("Project.fromDirectory with worktrees", () => { expect(project.worktree).toBe(tmp.path) expect(sandbox).toBe(worktreePath) - expect(project.sandboxes).toContain(worktreePath) - expect(project.sandboxes).not.toContain(tmp.path) } finally { await $`git worktree remove ${worktreePath}` .cwd(tmp.path) @@ -220,34 +217,6 @@ describe("Project.fromDirectory with worktrees", () => { await $`rm -rf ${bare} ${clone}`.quiet().nothrow() } }) - - test("should accumulate multiple worktrees in sandboxes", async () => { - await using tmp = await tmpdir({ git: true }) - - const worktree1 = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt1") - const worktree2 = path.join(tmp.path, "..", path.basename(tmp.path) + "-wt2") - try { - await $`git worktree add ${worktree1} -b branch-${Date.now()}`.cwd(tmp.path).quiet() - await $`git worktree add ${worktree2} -b branch-${Date.now() + 1}`.cwd(tmp.path).quiet() - - await run((svc) => svc.fromDirectory(worktree1)) - const { project } = await run((svc) => svc.fromDirectory(worktree2)) - - expect(project.worktree).toBe(tmp.path) - expect(project.sandboxes).toContain(worktree1) - expect(project.sandboxes).toContain(worktree2) - expect(project.sandboxes).not.toContain(tmp.path) - } finally { - await $`git worktree remove ${worktree1}` - .cwd(tmp.path) - .quiet() - .catch(() => {}) - await $`git worktree remove ${worktree2}` - .cwd(tmp.path) - .quiet() - .catch(() => {}) - } - }) }) describe("Project.discover", () => { @@ -485,39 +454,6 @@ describe("Project.setInitialized", () => { }) }) -describe("Project.addSandbox and Project.removeSandbox", () => { - test("addSandbox adds directory and removeSandbox removes it", async () => { - await using tmp = await tmpdir({ git: true }) - const { project } = await run((svc) => svc.fromDirectory(tmp.path)) - const sandboxDir = path.join(tmp.path, "sandbox-test") - - await run((svc) => svc.addSandbox(project.id, sandboxDir)) - - let found = Project.get(project.id) - expect(found?.sandboxes).toContain(sandboxDir) - - await run((svc) => svc.removeSandbox(project.id, sandboxDir)) - - found = Project.get(project.id) - expect(found?.sandboxes).not.toContain(sandboxDir) - }) - - test("addSandbox emits GlobalBus event", async () => { - await using tmp = await tmpdir({ git: true }) - const { project } = await run((svc) => svc.fromDirectory(tmp.path)) - const sandboxDir = path.join(tmp.path, "sandbox-event") - - const events: any[] = [] - const on = (evt: any) => events.push(evt) - GlobalBus.on("event", on) - - await run((svc) => svc.addSandbox(project.id, sandboxDir)) - - GlobalBus.off("event", on) - expect(events.some((e) => e.payload.type === Project.Event.Updated.type)).toBe(true) - }) -}) - describe("Project.fromDirectory with bare repos", () => { test("worktree from bare repo should cache in bare repo, not parent", async () => { await using tmp = await tmpdir({ git: true }) diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index 4f0ead54e4..e6e2ba2150 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -219,7 +219,7 @@ describe("Worktree", () => { expect(list).toContainEqual({ name: path.basename(parent), branch, - directory: directory.toLowerCase(), + directory: process.platform === "win32" ? directory.toLowerCase() : directory, }) yield* svc.remove({ directory: target }) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts index 0b8d8051bc..18cb08248e 100644 --- a/packages/opencode/test/server/httpapi-experimental.test.ts +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -57,12 +57,11 @@ describe("experimental HttpApi", () => { }) const headers = { "x-opencode-directory": tmp.path } - const [consoleState, consoleOrgs, toolList, toolIDs, worktrees, resources] = await Promise.all([ + const [consoleState, consoleOrgs, toolList, toolIDs, resources] = await Promise.all([ app().request(ExperimentalPaths.console, { headers }), app().request(ExperimentalPaths.consoleOrgs, { headers }), app().request(`${ExperimentalPaths.tool}?provider=opencode&model=gpt-5`, { headers }), app().request(ExperimentalPaths.toolIDs, { headers }), - app().request(ExperimentalPaths.worktree, { headers }), app().request(ExperimentalPaths.resource, { headers }), ]) @@ -87,9 +86,6 @@ describe("experimental HttpApi", () => { expect(toolIDs.status).toBe(200) expect(await toolIDs.json()).toContain("bash") - expect(worktrees.status).toBe(200) - expect(await worktrees.json()).toEqual([]) - expect(resources.status).toBe(200) expect(await resources.json()).toEqual({}) }) @@ -172,10 +168,6 @@ describe("experimental HttpApi", () => { expect(info).toMatchObject({ name: "api-test", branch: "opencode/api-test" }) await waitReady(info.directory) - const listed = await app().request(ExperimentalPaths.worktree, { headers }) - expect(listed.status).toBe(200) - expect(await listed.json()).toContain(info.directory) - if (process.platform !== "win32") { const reset = await app().request(ExperimentalPaths.worktreeReset, { method: "POST", @@ -195,9 +187,6 @@ describe("experimental HttpApi", () => { expect(removed.status).toBe(200) expect(await removed.json()).toBe(true) - - const afterRemove = await app().request(ExperimentalPaths.worktree, { headers }) - expect(afterRemove.status).toBe(200) - expect(await afterRemove.json()).toEqual([]) + expect(await Bun.file(info.directory).exists()).toBe(false) }) }) diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 598a635cd4..2e8584ad01 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -130,7 +130,6 @@ describe("JSON to SQLite migration", () => { expect(projects[0].id).toBe(ProjectID.make("proj_test123abc")) expect(projects[0].worktree).toBe("/test/path") expect(projects[0].name).toBe("Test Project") - expect(projects[0].sandboxes).toEqual(["/test/sandbox"]) }) test("uses filename for project id when JSON has different value", async () => { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index bf3201a5c0..030fbf9168 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -1236,7 +1236,7 @@ export class Worktree extends HeyApiClient { /** * List worktrees * - * List all sandbox worktrees for the current project. + * List all git worktrees for the current project. */ public list( parameters?: { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index da80645ad7..735c8b04f6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -5,6 +5,12 @@ export type ClientOptions = { } export type Event = + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow1 + | EventTuiSessionSelect + | EventServerConnected + | EventGlobalDisposed | EventServerInstanceDisposed | EventFileEdited | EventFileWatcherUpdated @@ -24,10 +30,6 @@ export type Event = | EventSessionStatus | EventSessionIdle | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow1 - | EventTuiSessionSelect | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted @@ -75,8 +77,6 @@ export type Event = | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded - | EventServerConnected - | EventGlobalDisposed export type OAuth = { type: "oauth" @@ -103,6 +103,61 @@ export type WellKnownAuth = { export type Auth = OAuth | ApiAuth | WellKnownAuth +export type EventTuiPromptAppend = { + id: string + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + id: string + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + id: string + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type EventTuiSessionSelect = { + id: string + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + export type PermissionRequest = { id: string sessionID: string @@ -280,61 +335,6 @@ export type SessionStatus = type: "busy" } -export type EventTuiPromptAppend = { - id: string - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - id: string - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - id: string - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } -} - -export type EventTuiSessionSelect = { - id: string - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - export type Project = { id: string worktree: string @@ -356,7 +356,6 @@ export type Project = { updated: number initialized?: number } - sandboxes: Array } export type Pty = { @@ -779,6 +778,12 @@ export type GlobalEvent = { project?: string workspace?: string payload: + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventServerConnected + | EventGlobalDisposed | EventServerInstanceDisposed | EventFileEdited | EventFileWatcherUpdated @@ -798,10 +803,6 @@ export type GlobalEvent = { | EventSessionStatus | EventSessionIdle | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted @@ -849,8 +850,6 @@ export type GlobalEvent = { | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded - | EventServerConnected - | EventGlobalDisposed | SyncEventMessageUpdated | SyncEventMessageRemoved | SyncEventMessagePartUpdated @@ -2331,6 +2330,22 @@ export type SyncEventSessionNextCompactionEnded = { } } +export type EventServerConnected = { + id: string + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + id: string + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + export type EventServerInstanceDisposed = { id: string type: "server.instance.disposed" @@ -3057,22 +3072,6 @@ export type EventSessionNextCompactionEnded = { } } -export type EventServerConnected = { - id: string - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - id: string - type: "global.disposed" - properties: { - [key: string]: unknown - } -} - export type SessionInfo = { id: string parentID?: string diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index df0427f455..0d160dfa8f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1015,7 +1015,7 @@ } } }, - "description": "List all sandbox worktrees for the current project.", + "description": "List all git worktrees for the current project.", "summary": "List worktrees", "x-codeSamples": [ { @@ -8878,6 +8878,24 @@ "schemas": { "Event": { "anyOf": [ + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/EventTuiToastShow1" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, + { + "$ref": "#/components/schemas/EventServerConnected" + }, + { + "$ref": "#/components/schemas/EventGlobalDisposed" + }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, @@ -8935,18 +8953,6 @@ { "$ref": "#/components/schemas/EventSessionCompacted" }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/EventTuiToastShow1" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, { "$ref": "#/components/schemas/EventMcpToolsChanged" }, @@ -9087,12 +9093,6 @@ }, { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" } ] }, @@ -9173,6 +9173,140 @@ } ] }, + "Event.tui.prompt.append": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.prompt.append"] + }, + "properties": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.command.execute": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.command.execute"] + }, + "properties": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": ["command"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.toast.show": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.toast.show"] + }, + "properties": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "type": "integer", + "exclusiveMinimum": 0 + } + }, + "required": ["message", "variant"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.session.select": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.session.select"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses", + "description": "Session ID to navigate to" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "PermissionRequest": { "type": "object", "properties": { @@ -9632,140 +9766,6 @@ } ] }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.session.select": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "Project": { "type": "object", "properties": { @@ -9825,15 +9825,9 @@ }, "required": ["created", "updated"], "additionalProperties": false - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } } }, - "required": ["id", "worktree", "time", "sandboxes"], + "required": ["id", "worktree", "time"], "additionalProperties": false }, "Pty": { @@ -11145,6 +11139,24 @@ }, "payload": { "anyOf": [ + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/Event.tui.toast.show" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, + { + "$ref": "#/components/schemas/EventServerConnected" + }, + { + "$ref": "#/components/schemas/EventGlobalDisposed" + }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, @@ -11202,18 +11214,6 @@ { "$ref": "#/components/schemas/EventSessionCompacted" }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, { "$ref": "#/components/schemas/EventMcpToolsChanged" }, @@ -11355,12 +11355,6 @@ { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" - }, { "$ref": "#/components/schemas/SyncEventMessageUpdated" }, @@ -15859,6 +15853,42 @@ "required": ["type", "name", "id", "seq", "aggregateID", "data"], "additionalProperties": false }, + "EventServerConnected": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["server.connected"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventGlobalDisposed": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["global.disposed"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventServerInstanceDisposed": { "type": "object", "properties": { @@ -18062,42 +18092,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventServerConnected": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["server.connected"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventGlobalDisposed": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["global.disposed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "SessionInfo": { "type": "object", "properties": { From 7c09c2f17ff29e12b9353a29133dc1587fbdd48b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 11 May 2026 11:21:14 -0400 Subject: [PATCH 2/2] refactor(project): apply simplify review - Fix prunable parser to match 'prunable ' lines, not just bare 'prunable'. - Parallelize Effect.forEach in Worktree.list with concurrency: 'unbounded' to match the rest of the file. - Simplify the experimental worktree GET handler to use Effect.fn(fn, Effect.map(...)). - Drop unused isProjectList/isProjectUpdate type guards and the unknown-cast set/setBootStore wrappers in global-sync; expose setGlobalStore directly. - Centralize project worktree mutations as globalSync.project.addWorktree / removeWorktree, replacing three near-duplicate produce blocks in pages/layout.tsx and prompt-input/submit.ts. Use pathKey for dedup in all sites. - Cast project.updated event properties as Project (not ProjectInfo) and seed worktrees: [] on insert. - Extract projectDirectories(project) helper in pages/layout/helpers.ts; use at six call sites that built [project.worktree, ...(project.worktrees ?? [])] inline. --- .../components/dialog-select-directory.tsx | 3 +- .../app/src/components/dialog-select-file.tsx | 3 +- .../app/src/components/prompt-input/submit.ts | 13 +---- packages/app/src/context/global-sync.tsx | 55 ++++++++++--------- .../src/context/global-sync/event-reducer.ts | 5 +- packages/app/src/pages/layout.tsx | 27 ++------- packages/app/src/pages/layout/helpers.ts | 5 ++ .../app/src/pages/layout/sidebar-items.tsx | 4 +- .../instance/httpapi/handlers/experimental.ts | 7 ++- packages/opencode/src/worktree/index.ts | 31 ++++++----- 10 files changed, 72 insertions(+), 81 deletions(-) diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 6f397f86c9..3ffd48e938 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -10,6 +10,7 @@ import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { useLayout } from "@/context/layout" import { useLanguage } from "@/context/language" +import { projectDirectories } from "@/pages/layout/helpers" interface DialogSelectDirectoryProps { title?: string @@ -284,7 +285,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { for (const project of projects) { let at = 0 - const dirs = [project.worktree, ...(project.worktrees ?? [])] + const dirs = projectDirectories(project) for (const directory of dirs) { const sessions = sync.child(directory, { bootstrap: false })[0].session for (const session of sessions) { diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 8f447c02a4..9adf87000f 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -16,6 +16,7 @@ import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionTabs } from "@/pages/session/helpers" +import { projectDirectories } from "@/pages/layout/helpers" import { decode64 } from "@/utils/base64" import { getRelativeTime } from "@/utils/time" @@ -287,7 +288,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const current = project() if (!current) return directory ? [directory] : [] - const dirs = [current.worktree, ...(current.worktrees ?? [])] + const dirs = projectDirectories(current) if (directory && !dirs.includes(directory)) return [...dirs, directory] return dirs }) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 15920ca5e2..be0ab40111 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -4,7 +4,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { Binary } from "@opencode-ai/core/util/binary" import { useNavigate, useParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" -import { produce } from "solid-js/store" import type { FileSelection } from "@/context/file" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" @@ -343,17 +342,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { return } WorktreeState.pending(createdWorktree.directory) - globalSync.set( - "project", - produce((draft) => { - const project = draft.find((item) => item.worktree === projectDirectory) - if (!project) return - project.worktrees = [ - createdWorktree.directory, - ...(project.worktrees ?? []).filter((worktree) => worktree !== createdWorktree.directory), - ] - }), - ) + globalSync.project.addWorktree(projectDirectory, createdWorktree.directory) sessionDirectory = createdWorktree.directory } diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 4d86d2842a..d1d4d52eb3 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -32,6 +32,7 @@ import { formatServerError } from "@/utils/server-errors" import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" +import { pathKey } from "@/utils/path-key" type GlobalStore = { ready: boolean @@ -47,10 +48,6 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } -const isProjectList = (input: unknown): input is ProjectInfo[] => Array.isArray(input) -const isProjectUpdate = (input: unknown): input is (draft: ProjectInfo[]) => ProjectInfo[] => - typeof input === "function" - export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const @@ -125,18 +122,6 @@ function createGlobalSync() { if (eventTimer !== undefined) clearTimeout(eventTimer) }) - const setProjects = (next: ProjectInfo[] | ((draft: ProjectInfo[]) => ProjectInfo[])) => { - setGlobalStore("project", next) - } - - const setBootStore = ((...input: unknown[]) => { - if (input[0] === "project" && isProjectList(input[1])) { - setProjects(input[1]) - return input[1] - } - return (setGlobalStore as (...args: unknown[]) => unknown)(...input) - }) as typeof setGlobalStore - const bootstrap = useQuery(() => ({ queryKey: ["bootstrap"], queryFn: async () => { @@ -145,7 +130,7 @@ function createGlobalSync() { requestFailedTitle: language.t("common.requestFailed"), translate: language.t, formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), - setGlobalStore: setBootStore, + setGlobalStore, queryClient, }) bootedAt = Date.now() @@ -153,13 +138,31 @@ function createGlobalSync() { }, })) - const set = ((...input: unknown[]) => { - if (input[0] === "project" && (isProjectList(input[1]) || isProjectUpdate(input[1]))) { - setProjects(input[1]) - return input[1] - } - return (setGlobalStore as (...args: unknown[]) => unknown)(...input) - }) as typeof setGlobalStore + const set = setGlobalStore + + const addProjectWorktree = (projectWorktree: string, directory: string) => { + setGlobalStore( + "project", + produce((draft) => { + const project = draft.find((item) => item.worktree === projectWorktree) + if (!project) return + const key = pathKey(directory) + project.worktrees = [directory, ...(project.worktrees ?? []).filter((item) => pathKey(item) !== key)] + }), + ) + } + + const removeProjectWorktree = (projectWorktree: string, directory: string) => { + setGlobalStore( + "project", + produce((draft) => { + const project = draft.find((item) => item.worktree === projectWorktree) + if (!project) return + const key = pathKey(directory) + project.worktrees = (project.worktrees ?? []).filter((item) => pathKey(item) !== key) + }), + ) + } const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => { if (!sessionID) return @@ -347,7 +350,7 @@ function createGlobalSync() { if (recent) return bootstrap.refetch() }, - setGlobalProject: setProjects, + setGlobalProject: (next) => setGlobalStore("project", next), }) if (event.type === "server.connected" || event.type === "global.disposed") { if (recent) return @@ -411,6 +414,8 @@ function createGlobalSync() { icon(directory: string, value: string | undefined) { children.projectIcon(directory, value) }, + addWorktree: addProjectWorktree, + removeWorktree: removeProjectWorktree, } const updateConfigMutation = useMutation(() => ({ diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 4c929c66e3..2c05de8899 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -4,6 +4,7 @@ import type { Message, Part, PermissionRequest, + Project, QuestionRequest, Session, SessionStatus, @@ -29,7 +30,7 @@ export function applyGlobalEvent(input: { } if (input.event.type !== "project.updated") return - const properties = input.event.properties as ProjectInfo + const properties = input.event.properties as Project const result = Binary.search(input.project, properties.id, (s) => s.id) if (result.found) { input.setGlobalProject( @@ -41,7 +42,7 @@ export function applyGlobalEvent(input: { } input.setGlobalProject( produce((draft) => { - draft.splice(result.index, 0, properties) + draft.splice(result.index, 0, { ...properties, worktrees: [] }) }), ) } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index f75dbd58d5..c798d99179 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -70,6 +70,7 @@ import { effectiveWorkspaceOrder, errorMessage, latestRootSession, + projectDirectories, sortedRootSessions, } from "./layout/helpers" import { @@ -1274,9 +1275,7 @@ export default function Layout(props: ParentProps) { const root = projectRoot(directory) server.projects.touch(root) const project = layout.projects.list().find((item) => item.worktree === root) - let dirs = project - ? effectiveWorkspaceOrder(root, [root, ...(project.worktrees ?? [])], store.workspaceOrder[root]) - : [root] + let dirs = project ? effectiveWorkspaceOrder(root, projectDirectories(project), store.workspaceOrder[root]) : [root] const canOpen = (value: string | undefined) => { if (!value) return false return dirs.some((item) => pathKey(item) === pathKey(value)) @@ -1509,14 +1508,7 @@ export default function Layout(props: ParentProps) { clearLastProjectSession(root) } - globalSync.set( - "project", - produce((draft) => { - const project = draft.find((item) => item.worktree === root) - if (!project) return - project.worktrees = (project.worktrees ?? []).filter((worktree) => worktree !== directory) - }), - ) + globalSync.project.removeWorktree(root, directory) setStore("workspaceOrder", root, (order) => (order ?? []).filter((workspace) => workspace !== directory)) layout.projects.close(directory) @@ -1528,7 +1520,7 @@ export default function Layout(props: ParentProps) { const nextKey = pathKey(nextCurrent) const project = layout.projects.list().find((item) => item.worktree === root) const dirs = project - ? effectiveWorkspaceOrder(root, [root, ...(project.worktrees ?? [])], store.workspaceOrder[root]) + ? effectiveWorkspaceOrder(root, projectDirectories(project), store.workspaceOrder[root]) : [root] const valid = dirs.some((item) => pathKey(item) === nextKey) @@ -1862,7 +1854,7 @@ export default function Layout(props: ParentProps) { function workspaceIds(project: LocalProject | undefined) { if (!project) return [] const local = project.worktree - const dirs = [local, ...(project.worktrees ?? [])] + const dirs = projectDirectories(project) const active = currentProject() const directory = pathKey(active?.worktree ?? "") === pathKey(project.worktree) ? currentDir() : undefined const extra = @@ -1954,14 +1946,7 @@ export default function Layout(props: ParentProps) { }) return [created.directory, ...next] }) - globalSync.set( - "project", - produce((draft) => { - const item = draft.find((item) => item.worktree === project.worktree) - if (!item) return - item.worktrees = [created.directory, ...(item.worktrees ?? []).filter((worktree) => pathKey(worktree) !== key)] - }), - ) + globalSync.project.addWorktree(project.worktree, created.directory) globalSync.child(created.directory) navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index d53381e404..439ca8a2ef 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -55,6 +55,11 @@ export const childSessionOnPath = (sessions: Session[] | undefined, rootID: stri export const displayName = (project: { name?: string; worktree: string }) => project.name || getFilename(project.worktree) +export const projectDirectories = (project: { worktree: string; worktrees?: string[] }) => [ + project.worktree, + ...(project.worktrees ?? []), +] + export const errorMessage = (err: unknown, fallback: string) => { if (err && typeof err === "object" && "data" in err) { const data = (err as { data?: { message?: string } }).data diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 9b90c28b79..4a07fcd7b6 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -15,7 +15,7 @@ import { usePermission } from "@/context/permission" import { messageAgentColor } from "@/utils/agent" import { sessionTitle } from "@/utils/session-title" import { sessionPermissionRequest } from "../session/composer/session-request-tree" -import { childSessionOnPath, hasProjectPermissions } from "./helpers" +import { childSessionOnPath, hasProjectPermissions, projectDirectories } from "./helpers" const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" @@ -35,7 +35,7 @@ export const ProjectIcon = (props: { const globalSync = useGlobalSync() const notification = useNotification() const permission = usePermission() - const dirs = createMemo(() => [props.project.worktree, ...(props.project.worktrees ?? [])]) + const dirs = createMemo(() => projectDirectories(props.project)) const unseenCount = createMemo(() => dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts index 20f100f5ae..cc102a87fc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts @@ -89,9 +89,10 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper return yield* registry.ids() }) - const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () { - return yield* worktreeSvc.list().pipe(Effect.map((items) => items.map((item) => item.directory))) - }) + const worktree = Effect.fn("ExperimentalHttpApi.worktree")( + () => worktreeSvc.list(), + Effect.map((items) => items.map((item) => item.directory)), + ) const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: { payload: Worktree.CreateInput | undefined diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 9cf1c90c44..d6b240144c 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -315,7 +315,7 @@ export const layer: Layer.Layer< if (line.startsWith("branch ")) { current.branch = line.slice("branch ".length).trim() } - if (line === "prunable") { + if (line === "prunable" || line.startsWith("prunable ")) { current.prunable = true } return acc @@ -347,19 +347,22 @@ export const layer: Layer.Layer< const primary = yield* canonical(ctx.worktree) const primaryName = pathSvc.basename(primary).toLowerCase() - return yield* Effect.forEach(parseWorktreeList(result.text), (entry) => - Effect.gen(function* () { - if (entry.prunable) return undefined - if (!entry.path) return undefined - const directory = yield* canonical(entry.path) - if (directory === primary) return undefined - const name = pathSvc.basename(directory).toLowerCase() - return { - name: name === primaryName ? pathSvc.basename(pathSvc.dirname(directory)) : name, - directory, - ...(entry.branch ? { branch: entry.branch.replace(/^refs\/heads\//, "") } : {}), - } - }), + return yield* Effect.forEach( + parseWorktreeList(result.text), + (entry) => + Effect.gen(function* () { + if (entry.prunable) return undefined + if (!entry.path) return undefined + const directory = yield* canonical(entry.path) + if (directory === primary) return undefined + const name = pathSvc.basename(directory).toLowerCase() + return { + name: name === primaryName ? pathSvc.basename(pathSvc.dirname(directory)) : name, + directory, + ...(entry.branch ? { branch: entry.branch.replace(/^refs\/heads\//, "") } : {}), + } + }), + { concurrency: "unbounded" }, ).pipe(Effect.map((items) => items.filter((item) => item !== undefined))) })