| undefined }
+ const cancelHoverPrefetch = () => {
+ if (hoverPrefetch.current === undefined) return
+ clearTimeout(hoverPrefetch.current)
+ hoverPrefetch.current = undefined
+ }
+ const scheduleHoverPrefetch = () => {
+ if (hoverPrefetch.current !== undefined) return
+ hoverPrefetch.current = setTimeout(() => {
+ hoverPrefetch.current = undefined
+ prefetchSession(props.session)
+ }, 200)
+ }
+
+ onCleanup(cancelHoverPrefetch)
+
+ const messageLabel = (message: Message) => {
+ const parts = sessionStore.part[message.id] ?? []
+ const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
+ return text?.text
+ }
+
+ const item = (
+ prefetchSession(props.session, "high")}
+ onClick={() => {
+ setState("hoverSession", undefined)
+ if (layout.sidebar.opened()) return
+ queueMicrotask(() => setState("hoverProject", undefined))
+ }}
+ >
+
+
+
}>
+
+
+
+
+
+
+
+
+
+
0}>
+
+
+
+
+
props.session.title}
+ onSave={(next) => renameSession(props.session, next)}
+ class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
+ displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"
+ stopPropagation
+ />
+
+ {(summary) => (
+
+
+
+ )}
+
+
+
+ )
+
+ return (
+
+
+ {item}
+
+ }
+ >
+ setState("hoverSession", open ? props.session.id : undefined)}
+ >
+ {language.t("session.messages.loading")}
}
+ >
+
+ {
+ if (!isActive()) {
+ sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`)
+ navigate(`${props.slug}/session/${props.session.id}`)
+ return
+ }
+ window.history.replaceState(null, "", `#message-${message.id}`)
+ window.dispatchEvent(new HashChangeEvent("hashchange"))
+ }}
+ size="normal"
+ class="w-60"
+ />
+
+
+
+
+
+ setMenu("open", open)}>
+
+
+
+
+ {
+ if (!menu.pendingRename) return
+ event.preventDefault()
+ setMenu("pendingRename", false)
+ openEditor(`session:${props.session.id}`, props.session.title)
+ }}
+ >
+ {
+ setMenu("pendingRename", true)
+ setMenu("open", false)
+ }}
+ >
+ {language.t("common.rename")}
+
+
+ {/* WIP: archive session feature is work in progress */}
+ {/* //TODO: enable this when there's a way to load/restore archive sessions from the UI */}
+
+ {language.t("common.archive")}
+
+
+ dialog.show(() => )}>
+ {language.t("common.delete")}
+
+
+
+
+
+
+ )
+ }
+
+ const NewSessionItem = (props: { slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => {
+ const label = language.t("command.session.new")
+ const tooltip = () => props.mobile || !sidebarExpanded()
+ const item = (
+ {
+ setState("hoverSession", undefined)
+ if (layout.sidebar.opened()) return
+ queueMicrotask(() => setState("hoverProject", undefined))
+ }}
+ >
+
+
+ )
+
+ return (
+
+
+ {item}
+
+ }
+ >
+ {item}
+
+
+ )
+ }
+
+ const SessionSkeleton = (props: { count?: number }): JSX.Element => {
+ const items = Array.from({ length: props.count ?? 4 }, (_, index) => index)
+ return (
+
+ )
+ }
+
+ const ProjectDragOverlay = (): JSX.Element => {
+ const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeProject))
+ return (
+
+ {(p) => (
+
+ )}
+
+ )
+ }
+
+ const WorkspaceDragOverlay = (): JSX.Element => {
+ const label = createMemo(() => {
+ const project = sidebarProject()
+ if (!project) return
+ const directory = store.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 = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
+ return `${kind} : ${name}`
+ })
+
+ return (
+
+ {(value) => (
+ {value()}
+ )}
+
+ )
+ }
+
+ const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
+ 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(() =>
+ workspaceStore.session
+ .filter((session) => session.directory === workspaceStore.path.directory)
+ .filter((session) => !session.parentID && !session.time?.archived)
+ .toSorted(sortSessions(Date.now())),
+ )
+ const children = createMemo(() => {
+ const map = new Map()
+ for (const session of workspaceStore.session) {
+ if (!session.parentID) continue
+ const existing = map.get(session.parentID)
+ if (existing) {
+ existing.push(session.id)
+ continue
+ }
+ map.set(session.parentID, [session.id])
+ }
+ return map
+ })
+ const local = createMemo(() => props.directory === props.project.worktree)
+ const active = createMemo(() => {
+ const current = decode64(params.dir) ?? ""
+ return current === props.directory
+ })
+ const workspaceValue = createMemo(() => {
+ const branch = workspaceStore.vcs?.branch
+ const name = branch ?? getFilename(props.directory)
+ return workspaceName(props.directory, props.project.id, branch) ?? name
+ })
+ const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local())
+ const boot = createMemo(() => open() || active())
+ const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
+ const loading = createMemo(() => open() && !booted() && sessions().length === 0)
+ const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
+ const busy = createMemo(() => isBusy(props.directory))
+ const loadMore = async () => {
+ setWorkspaceStore("limit", (limit) => limit + 5)
+ await globalSync.project.loadSessions(props.directory)
+ }
+
+ const workspaceEditActive = createMemo(() => editorOpen(`workspace:${props.directory}`))
+
+ const openWrapper = (value: boolean) => {
+ setStore("workspaceExpanded", props.directory, value)
+ if (value) return
+ if (editorOpen(`workspace:${props.directory}`)) closeEditor()
+ }
+
+ createEffect(() => {
+ if (!boot()) return
+ globalSync.child(props.directory, { bootstrap: true })
+ })
+
+ const header = () => (
+
+
+ }>
+
+
+
+
+ {local() ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")} :
+
+
+ {workspaceStore.vcs?.branch ?? getFilename(props.directory)}
+
+ }
+ >
+ {
+ const trimmed = next.trim()
+ if (!trimmed) return
+ renameWorkspace(props.directory, trimmed, props.project.id, workspaceStore.vcs?.branch)
+ setEditor("value", workspaceValue())
+ }}
+ class="text-14-medium text-text-base min-w-0 truncate"
+ displayClass="text-14-medium text-text-base min-w-0 truncate"
+ editing={workspaceEditActive()}
+ stopPropagation={false}
+ openOnDblClick={false}
+ />
+
+
+
+ )
+
+ return (
+
+
+
+
+
+
+ {header()}
+
+ }
+ >
+ {header()}
+
+
+ setMenu("open", open)}
+ >
+
+
+
+
+ {
+ if (!menu.pendingRename) return
+ event.preventDefault()
+ setMenu("pendingRename", false)
+ openEditor(`workspace:${props.directory}`, workspaceValue())
+ }}
+ >
+ {
+ setMenu("pendingRename", true)
+ setMenu("open", false)
+ }}
+ >
+ {language.t("common.rename")}
+
+
+ dialog.show(() => (
+
+ ))
+ }
+ >
+ {language.t("common.reset")}
+
+
+ dialog.show(() => (
+
+ ))
+ }
+ >
+ {language.t("common.delete")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
+ const sortable = createSortable(props.project.worktree)
+ const selected = createMemo(() => {
+ const current = decode64(params.dir) ?? ""
+ return props.project.worktree === current || props.project.sandboxes?.includes(current)
+ })
+
+ const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2))
+ const workspaceEnabled = createMemo(
+ () => props.project.vcs === "git" && layout.sidebar.workspaces(props.project.worktree)(),
+ )
+ const [open, setOpen] = createSignal(false)
+ const [menu, setMenu] = createSignal(false)
+
+ const preview = createMemo(() => !props.mobile && layout.sidebar.opened())
+ const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened())
+ const active = createMemo(
+ () => menu() || (preview() ? open() : overlay() && state.hoverProject === props.project.worktree),
+ )
+
+ createEffect(() => {
+ if (preview()) return
+ if (!open()) return
+ setOpen(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 = workspaceLabel(directory, data.vcs?.branch, props.project.id)
+ return `${kind} : ${name}`
+ }
+
+ const sessions = (directory: string) => {
+ const [data] = globalSync.child(directory, { bootstrap: false })
+ const root = workspaceKey(directory)
+ return data.session
+ .filter((session) => workspaceKey(session.directory) === root)
+ .filter((session) => !session.parentID && !session.time?.archived)
+ .toSorted(sortSessions(Date.now()))
+ .slice(0, 2)
+ }
+
+ const projectSessions = () => {
+ const directory = props.project.worktree
+ const [data] = globalSync.child(directory, { bootstrap: false })
+ const root = workspaceKey(directory)
+ return data.session
+ .filter((session) => workspaceKey(session.directory) === root)
+ .filter((session) => !session.parentID && !session.time?.archived)
+ .toSorted(sortSessions(Date.now()))
+ .slice(0, 2)
+ }
+
+ const projectName = () => props.project.name || getFilename(props.project.worktree)
+ const Trigger = () => (
+ {
+ setMenu(value)
+ if (value) setOpen(false)
+ }}
+ >
+ {
+ if (!overlay()) return
+ globalSync.child(props.project.worktree)
+ setState("hoverProject", props.project.worktree)
+ setState("hoverSession", undefined)
+ }}
+ onFocus={() => {
+ if (!overlay()) return
+ globalSync.child(props.project.worktree)
+ setState("hoverProject", props.project.worktree)
+ setState("hoverSession", undefined)
+ }}
+ onClick={() => navigateToProject(props.project.worktree)}
+ onBlur={() => setOpen(false)}
+ >
+
+
+
+
+ dialog.show(() => )}>
+ {language.t("common.edit")}
+
+ {
+ const enabled = layout.sidebar.workspaces(props.project.worktree)()
+ if (enabled) {
+ layout.sidebar.toggleWorkspaces(props.project.worktree)
+ return
+ }
+ if (props.project.vcs !== "git") return
+ layout.sidebar.toggleWorkspaces(props.project.worktree)
+ }}
+ >
+
+ {layout.sidebar.workspaces(props.project.worktree)()
+ ? language.t("sidebar.workspaces.disable")
+ : language.t("sidebar.workspaces.enable")}
+
+
+
+ closeProject(props.project.worktree)}
+ >
+ {language.t("common.close")}
+
+
+
+
+ )
+
+ return (
+ // @ts-ignore
+
+
}>
+
}
+ onOpenChange={(value) => {
+ if (menu()) return
+ setOpen(value)
+ if (value) setState("hoverSession", undefined)
+ }}
+ >
+
+
+
{displayName(props.project)}
+
+ {
+ event.stopPropagation()
+ setOpen(false)
+ closeProject(props.project.worktree)
+ }}
+ />
+
+
+
{language.t("sidebar.project.recentSessions")}
+
+
+ {(session) => (
+
+ )}
+
+ }
+ >
+
+ {(directory) => (
+
+
+
+
+
+
{label(directory)}
+
+
+ {(session) => (
+
+ )}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
+ const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree)
+ const slug = createMemo(() => base64Encode(props.project.worktree))
+ const sessions = createMemo(() =>
+ workspaceStore.session
+ .filter((session) => session.directory === workspaceStore.path.directory)
+ .filter((session) => !session.parentID && !session.time?.archived)
+ .toSorted(sortSessions(Date.now())),
+ )
+ const children = createMemo(() => {
+ const map = new Map()
+ for (const session of workspaceStore.session) {
+ if (!session.parentID) continue
+ const existing = map.get(session.parentID)
+ if (existing) {
+ existing.push(session.id)
+ continue
+ }
+ map.set(session.parentID, [session.id])
+ }
+ return map
+ })
+ const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
+ const loading = createMemo(() => !booted() && sessions().length === 0)
+ const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
+ const loadMore = async () => {
+ setWorkspaceStore("limit", (limit) => limit + 5)
+ await globalSync.project.loadSessions(props.project.worktree)
+ }
+
+ return (
+ {
+ if (!props.mobile) scrollContainerRef = el
+ }}
+ class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]"
+ >
+
+
+ )
+ }
+
const createWorkspace = async (project: LocalProject) => {
clearSidebarHoverState()
const created = await globalSDK.client.worktree