refactor(project): apply simplify review

- Fix prunable parser to match 'prunable <reason>' 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.
This commit is contained in:
Kit Langton
2026-05-11 11:21:14 -04:00
parent c4132543c3
commit 7c09c2f17f
10 changed files with 72 additions and 81 deletions

View File

@@ -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) {

View File

@@ -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
})

View File

@@ -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
}

View File

@@ -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(() => ({

View File

@@ -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: [] })
}),
)
}

View File

@@ -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`)

View File

@@ -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

View File

@@ -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),
)

View File

@@ -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

View File

@@ -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)))
})