import { useNavigate, useParams } from "@solidjs/router" import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { createSortable } from "@thisbeyond/solid-dnd" import { createMediaQuery } from "@solid-primitives/media" import { base64Encode } from "@opencode-ai/util/encode" import { getFilename } from "@opencode-ai/util/path" import { Button } from "@opencode-ai/ui/button" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" import { type Session } from "@opencode-ai/sdk/v2/client" import { type LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" import { childMapByParent, sortedRootSessions } from "./helpers" type InlineEditorComponent = (props: { id: string value: Accessor onSave: (next: string) => void class?: string displayClass?: string editing?: boolean stopPropagation?: boolean openOnDblClick?: boolean }) => JSX.Element export type WorkspaceSidebarContext = { currentDir: Accessor sidebarExpanded: Accessor sidebarHovering: Accessor nav: Accessor hoverSession: Accessor setHoverSession: (id: string | undefined) => void clearHoverProjectSoon: () => void prefetchSession: (session: Session, priority?: "high" | "low") => void archiveSession: (session: Session) => Promise workspaceName: (directory: string, projectId?: string, branch?: string) => string | undefined renameWorkspace: (directory: string, next: string, projectId?: string, branch?: string) => void editorOpen: (id: string) => boolean openEditor: (id: string, value: string) => void closeEditor: () => void setEditor: (key: "value", value: string) => void InlineEditor: InlineEditorComponent isBusy: (directory: string) => boolean workspaceExpanded: (directory: string, local: boolean) => boolean setWorkspaceExpanded: (directory: string, value: boolean) => void showResetWorkspaceDialog: (root: string, directory: string) => void showDeleteWorkspaceDialog: (root: string, directory: string) => void setScrollContainerRef: (el: HTMLDivElement | undefined, mobile?: boolean) => void } export const WorkspaceDragOverlay = (props: { sidebarProject: Accessor activeWorkspace: Accessor workspaceLabel: (directory: string, branch?: string, projectId?: string) => string }): JSX.Element => { const globalSync = useGlobalSync() const language = useLanguage() const label = createMemo(() => { const project = props.sidebarProject() if (!project) return const directory = props.activeWorkspace() if (!directory) return const [workspaceStore] = globalSync.child(directory, { bootstrap: false }) const kind = directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) return `${kind} : ${name}` }) return ( {(value) =>
{value()}
}
) } const WorkspaceHeader = (props: { local: Accessor busy: Accessor open: Accessor directory: string language: ReturnType branch: Accessor workspaceValue: Accessor workspaceEditActive: Accessor InlineEditor: WorkspaceSidebarContext["InlineEditor"] renameWorkspace: WorkspaceSidebarContext["renameWorkspace"] setEditor: WorkspaceSidebarContext["setEditor"] projectId?: string }): JSX.Element => (
}>
{props.local() ? props.language.t("workspace.type.local") : props.language.t("workspace.type.sandbox")} : {props.branch() ?? getFilename(props.directory)} } > { const trimmed = next.trim() if (!trimmed) return props.renameWorkspace(props.directory, trimmed, props.projectId, props.branch()) props.setEditor("value", props.workspaceValue()) }} class="text-14-medium text-text-base min-w-0 truncate" displayClass="text-14-medium text-text-base min-w-0 truncate" editing={props.workspaceEditActive()} stopPropagation={false} openOnDblClick={false} />
) const WorkspaceActions = (props: { directory: string local: Accessor busy: Accessor menuOpen: Accessor pendingRename: Accessor setMenuOpen: (open: boolean) => void setPendingRename: (value: boolean) => void sidebarHovering: Accessor touch: Accessor language: ReturnType workspaceValue: Accessor openEditor: WorkspaceSidebarContext["openEditor"] showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"] showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"] root: string setHoverSession: WorkspaceSidebarContext["setHoverSession"] clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"] navigateToNewSession: () => void }): JSX.Element => (
props.setMenuOpen(open)} > { if (!props.pendingRename()) return event.preventDefault() props.setPendingRename(false) props.openEditor(`workspace:${props.directory}`, props.workspaceValue()) }} > { props.setPendingRename(true) props.setMenuOpen(false) }} > {props.language.t("common.rename")} props.showResetWorkspaceDialog(props.root, props.directory)} > {props.language.t("common.reset")} props.showDeleteWorkspaceDialog(props.root, props.directory)} > {props.language.t("common.delete")} { event.preventDefault() event.stopPropagation() props.setHoverSession(undefined) props.clearHoverProjectSoon() props.navigateToNewSession() }} />
) const WorkspaceSessionList = (props: { slug: Accessor mobile?: boolean popover?: boolean ctx: WorkspaceSidebarContext showNew: Accessor loading: Accessor sessions: Accessor children: Accessor> hasMore: Accessor loadMore: () => Promise language: ReturnType }): JSX.Element => ( ) export const SortableWorkspace = (props: { ctx: WorkspaceSidebarContext directory: string project: LocalProject sortNow: Accessor mobile?: boolean popover?: boolean }): JSX.Element => { const navigate = useNavigate() const params = useParams() const globalSync = useGlobalSync() const language = useLanguage() const sortable = createSortable(props.directory) const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) const [menu, setMenu] = createStore({ open: false, pendingRename: false, }) const slug = createMemo(() => base64Encode(props.directory)) const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow())) const children = createMemo(() => childMapByParent(workspaceStore.session)) const local = createMemo(() => props.directory === props.project.worktree) const active = createMemo(() => props.ctx.currentDir() === props.directory) const workspaceValue = createMemo(() => { const branch = workspaceStore.vcs?.branch const name = branch ?? getFilename(props.directory) return props.ctx.workspaceName(props.directory, props.project.id, branch) ?? name }) const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) const boot = createMemo(() => open() || active()) const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false) const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) const busy = createMemo(() => props.ctx.isBusy(props.directory)) const wasBusy = createMemo((prev) => prev || busy(), false) const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy()) const touch = createMediaQuery("(hover: none)") const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id))) const loadMore = async () => { setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5) await globalSync.project.loadSessions(props.directory) } const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`)) const header = () => ( workspaceStore.vcs?.branch} workspaceValue={workspaceValue} workspaceEditActive={workspaceEditActive} InlineEditor={props.ctx.InlineEditor} renameWorkspace={props.ctx.renameWorkspace} setEditor={props.ctx.setEditor} projectId={props.project.id} /> ) const openWrapper = (value: boolean) => { props.ctx.setWorkspaceExpanded(props.directory, value) if (value) return if (props.ctx.editorOpen(`workspace:${props.directory}`)) props.ctx.closeEditor() } createEffect(() => { if (!boot()) return globalSync.child(props.directory, { bootstrap: true }) }) return (
{header()} } >
{header()}
menu.open} pendingRename={() => menu.pendingRename} setMenuOpen={(open) => setMenu("open", open)} setPendingRename={(value) => setMenu("pendingRename", value)} sidebarHovering={props.ctx.sidebarHovering} touch={touch} language={language} workspaceValue={workspaceValue} openEditor={props.ctx.openEditor} showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog} showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog} root={props.project.worktree} setHoverSession={props.ctx.setHoverSession} clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} navigateToNewSession={() => navigate(`/${slug()}/session`)} />
) } export const LocalWorkspace = (props: { ctx: WorkspaceSidebarContext project: LocalProject sortNow: Accessor mobile?: boolean popover?: boolean }): JSX.Element => { const globalSync = useGlobalSync() const language = useLanguage() const workspace = createMemo(() => { const [store, setStore] = globalSync.child(props.project.worktree) return { store, setStore } }) const slug = createMemo(() => base64Encode(props.project.worktree)) const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const children = createMemo(() => childMapByParent(workspace().store.session)) const booted = createMemo((prev) => prev || workspace().store.status === "complete", false) const loading = createMemo(() => !booted() && sessions().length === 0) const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length) const loadMore = async () => { workspace().setStore("limit", (limit) => (limit ?? 0) + 5) await globalSync.project.loadSessions(props.project.worktree) } return (
props.ctx.setScrollContainerRef(el, props.mobile)} class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]" > false} loading={loading} sessions={sessions} children={children} hasMore={hasMore} loadMore={loadMore} language={language} />
) }