import type { Session } from "@opencode-ai/sdk/v2/client" import { Avatar } from "@opencode-ai/ui/avatar" 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 { getFilename } from "@opencode-ai/util/path" import { A, useParams } from "@solidjs/router" import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" import { useNotification } from "@/context/notification" 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" const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => { const globalSync = useGlobalSync() const notification = useNotification() const permission = usePermission() const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) const unseenCount = createMemo(() => dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), ) const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) const hasPermissions = createMemo(() => dirs().some((directory) => { const [store] = globalSync.child(directory, { bootstrap: false }) return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory)) }), ) const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0)) const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) return (
) } export type SessionItemProps = { session: Session list: Session[] navList?: Accessor slug: string mobile?: boolean dense?: boolean showTooltip?: boolean showChild?: boolean level?: number sidebarExpanded: Accessor clearHoverProjectSoon: () => void prefetchSession: (session: Session, priority?: "high" | "low") => void archiveSession: (session: Session) => Promise } const SessionRow = (props: { session: Session slug: string mobile?: boolean dense?: boolean tint: Accessor isWorking: Accessor hasPermissions: Accessor hasError: Accessor unseenCount: Accessor clearHoverProjectSoon: () => void sidebarOpened: Accessor warmPress: () => void warmFocus: () => void }): JSX.Element => { const title = () => sessionTitle(props.session.title) return ( { if (props.sidebarOpened()) return props.clearHoverProjectSoon() }} > 0}>
0}>
{title()}
) } export const SessionItem = (props: SessionItemProps): JSX.Element => { const params = useParams() const layout = useLayout() const language = useLanguage() const notification = useNotification() const permission = usePermission() const globalSync = useGlobalSync() const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) const [sessionStore] = globalSync.child(props.session.directory) const hasPermissions = createMemo(() => { return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => { return !permission.autoResponds(item, props.session.directory) }) }) const isWorking = createMemo(() => { if (hasPermissions()) return false const pending = (sessionStore.message[props.session.id] ?? []).findLast( (message) => message.role === "assistant" && typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number", ) const status = sessionStore.session_status[props.session.id] return ( pending !== undefined || status?.type === "busy" || status?.type === "retry" || (status !== undefined && status.type !== "idle") ) }) const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)) const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded())) const currentChild = createMemo(() => { if (!props.showChild) return return childSessionOnPath(sessionStore.session, props.session.id, params.id) }) const warm = (span: number, priority: "high" | "low") => { const nav = props.navList?.() const list = nav?.some((item) => item.id === props.session.id && item.directory === props.session.directory) ? nav : props.list props.prefetchSession(props.session, priority) const idx = list.findIndex((item) => item.id === props.session.id && item.directory === props.session.directory) if (idx === -1) return for (let step = 1; step <= span; step++) { const next = list[idx + step] if (next) props.prefetchSession(next, step === 1 ? "high" : priority) const prev = list[idx - step] if (prev) props.prefetchSession(prev, step === 1 ? "high" : priority) } } const item = ( warm(2, "high")} warmFocus={() => warm(2, "high")} /> ) return ( <>
{item} } > {item}
{ event.preventDefault() event.stopPropagation() void props.archiveSession(props.session) }} />
{(child) => (
)}
) } export const NewSessionItem = (props: { slug: string mobile?: boolean dense?: boolean sidebarExpanded: Accessor clearHoverProjectSoon: () => void }): JSX.Element => { const layout = useLayout() const language = useLanguage() const label = language.t("command.session.new") const tooltip = () => props.mobile || !props.sidebarExpanded() const item = ( { if (layout.sidebar.opened()) return props.clearHoverProjectSoon() }} >
{label}
) return (
{item} } > {item}
) } export const SessionSkeleton = (props: { count?: number }): JSX.Element => { const items = Array.from({ length: props.count ?? 4 }, (_, index) => index) return (
{() =>
}
) }