import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { base64Encode } from "@opencode-ai/util/encode" import { Button } from "@opencode-ai/ui/button" import { ContextMenu } from "@opencode-ai/ui/context-menu" import { HoverCard } from "@opencode-ai/ui/hover-card" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { createSortable } from "@thisbeyond/solid-dnd" import { useLayout, type LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" import { childMapByParent, displayName, sortedRootSessions } from "./helpers" import { projectSelected, projectTileActive } from "./sidebar-project-helpers" export type ProjectSidebarContext = { currentDir: Accessor sidebarOpened: Accessor sidebarHovering: Accessor hoverProject: Accessor nav: Accessor onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void onProjectFocus: (worktree: string) => void navigateToProject: (directory: string) => void openSidebar: () => void closeProject: (directory: string) => void showEditProjectDialog: (project: LocalProject) => void toggleProjectWorkspaces: (project: LocalProject) => void workspacesEnabled: (project: LocalProject) => boolean workspaceIds: (project: LocalProject) => string[] workspaceLabel: (directory: string, branch?: string, projectId?: string) => string sessionProps: Omit setHoverSession: (id: string | undefined) => void } export const ProjectDragOverlay = (props: { projects: Accessor activeProject: Accessor }): JSX.Element => { const project = createMemo(() => props.projects().find((p) => p.worktree === props.activeProject())) return ( {(p) => (
)}
) } const ProjectTile = (props: { project: LocalProject mobile?: boolean nav: Accessor sidebarHovering: Accessor selected: Accessor active: Accessor overlay: Accessor suppressHover: Accessor dirs: Accessor onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void onProjectFocus: (worktree: string) => void navigateToProject: (directory: string) => void showEditProjectDialog: (project: LocalProject) => void toggleProjectWorkspaces: (project: LocalProject) => void workspacesEnabled: (project: LocalProject) => boolean closeProject: (directory: string) => void setMenu: (value: boolean) => void setOpen: (value: boolean) => void setSuppressHover: (value: boolean) => void language: ReturnType }): JSX.Element => { const notification = useNotification() const layout = useLayout() const unseenCount = createMemo(() => props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) const clear = () => props .dirs() .filter((directory) => notification.project.unseenCount(directory) > 0) .forEach((directory) => notification.project.markViewed(directory)) return ( { props.setMenu(value) if (value) props.setOpen(false) }} > { if (!props.overlay()) return if (props.suppressHover()) return props.onProjectMouseEnter(props.project.worktree, event) }} onMouseLeave={() => { if (props.suppressHover()) props.setSuppressHover(false) if (!props.overlay()) return props.onProjectMouseLeave(props.project.worktree) }} onFocus={() => { if (!props.overlay()) return if (props.suppressHover()) return props.onProjectFocus(props.project.worktree) }} onClick={() => { if (props.selected()) { props.setSuppressHover(true) layout.sidebar.toggle() return } props.setSuppressHover(false) props.navigateToProject(props.project.worktree) }} onBlur={() => props.setOpen(false)} > props.showEditProjectDialog(props.project)}> {props.language.t("common.edit")} props.toggleProjectWorkspaces(props.project)} > {props.workspacesEnabled(props.project) ? props.language.t("sidebar.workspaces.disable") : props.language.t("sidebar.workspaces.enable")} {props.language.t("sidebar.project.clearNotifications")} props.closeProject(props.project.worktree)} > {props.language.t("common.close")} ) } const ProjectPreviewPanel = (props: { project: LocalProject mobile?: boolean selected: Accessor workspaceEnabled: Accessor workspaces: Accessor label: (directory: string) => string projectSessions: Accessor> projectChildren: Accessor> workspaceSessions: (directory: string) => ReturnType workspaceChildren: (directory: string) => Map setOpen: (value: boolean) => void ctx: ProjectSidebarContext language: ReturnType }): JSX.Element => (
{displayName(props.project)}
{ event.stopPropagation() props.setOpen(false) props.ctx.closeProject(props.project.worktree) }} />
{props.language.t("sidebar.project.recentSessions")}
{(session) => ( )} } > {(directory) => { const sessions = createMemo(() => props.workspaceSessions(directory)) const children = createMemo(() => props.workspaceChildren(directory)) return (
{props.label(directory)}
{(session) => ( )}
) }}
) export const SortableProject = (props: { project: LocalProject mobile?: boolean ctx: ProjectSidebarContext sortNow: Accessor }): JSX.Element => { const globalSync = useGlobalSync() const language = useLanguage() const sortable = createSortable(props.project.worktree) const selected = createMemo(() => projectSelected(props.ctx.currentDir(), props.project.worktree, props.project.sandboxes), ) const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) const [state, setState] = createStore({ open: false, menu: false, suppressHover: false, }) const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) const active = createMemo(() => projectTileActive({ menu: state.menu, preview: preview(), open: state.open, overlay: overlay(), hoverProject: props.ctx.hoverProject(), worktree: props.project.worktree, }), ) createEffect(() => { if (preview()) return if (!state.open) return setState("open", false) }) createEffect(() => { if (!selected()) return if (!state.open) return setState("open", false) }) const label = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) const kind = directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id) return `${kind} : ${name}` } const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0]) const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()).slice(0, 2)) const projectChildren = createMemo(() => childMapByParent(projectStore().session)) const workspaceSessions = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) return sortedRootSessions(data, props.sortNow()).slice(0, 2) } const workspaceChildren = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) return childMapByParent(data.session) } const tile = () => ( state.suppressHover} dirs={dirs} onProjectMouseEnter={props.ctx.onProjectMouseEnter} onProjectMouseLeave={props.ctx.onProjectMouseLeave} onProjectFocus={props.ctx.onProjectFocus} navigateToProject={props.ctx.navigateToProject} showEditProjectDialog={props.ctx.showEditProjectDialog} toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces} workspacesEnabled={props.ctx.workspacesEnabled} closeProject={props.ctx.closeProject} setMenu={(value) => setState("menu", value)} setOpen={(value) => setState("open", value)} setSuppressHover={(value) => setState("suppressHover", value)} language={language} /> ) return ( // @ts-ignore
{ if (state.menu) return if (value && state.suppressHover) return setState("open", value) if (value) props.ctx.setHoverSession(undefined) }} > setState("open", value)} ctx={props.ctx} language={language} />
) }