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.
This commit is contained in:
parent
52f7ba7d4d
commit
c4132543c3
31 changed files with 1866 additions and 519 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<T extends { id: string; sessionID: string }>(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<State>, session: Session) {
|
||||
|
|
@ -207,7 +215,7 @@ export async function bootstrapDirectory(input: {
|
|||
global: {
|
||||
config: Config
|
||||
path: Path
|
||||
project: Project[]
|
||||
project: ProjectInfo[]
|
||||
provider: ProviderListResponse
|
||||
}
|
||||
queryClient: QueryClient
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Project> & { worktree: string; expanded: boolean }
|
||||
export type LocalProject = Partial<ProjectInfo> & { 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<string, string>()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue