import type { Session } from "@opencode-ai/sdk/v2/client" import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js" import { createStore } from "solid-js/store" import { useQuery } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" import { Logo } from "@opencode-ai/ui/logo" import { Spinner } from "@opencode-ai/ui/spinner" import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx" import { ButtonV2 } from "@opencode-ai/ui/v2/components/button-v2.jsx" import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx" import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx" import { MenuV2 } from "@opencode-ai/ui/v2/components/menu-v2.jsx" import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout" import { useNavigate } from "@solidjs/router" import { base64Encode } from "@opencode-ai/core/util/encode" import { Icon } from "@opencode-ai/ui/icon" import { usePlatform } from "@/context/platform" import { DateTime } from "luxon" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectServer } from "@/components/dialog-select-server" import { ServerConnection, useServer } from "@/context/server" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" import { displayName, getProjectAvatarSource, projectForSession, sortedRootSessions } from "@/pages/layout/helpers" import { sessionTitle } from "@/utils/session-title" import { pathKey } from "@/utils/path-key" import { messageAgentColor } from "@/utils/agent" import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree" import { ServerHealthIndicator } from "@/components/server/server-row" import { useServers } from "@/context/servers" import { useSettings } from "@/context/settings" const HOME_SESSION_LIMIT = 15 const HOME_ROW = "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none" const HOME_PROJECT_NAV_ROW = `${HOME_ROW} h-7 gap-2 px-1.5 [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap` const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]" type HomeSessionRecord = { session: Session project: LocalProject projectName: string } type HomeSessionGroup = { id: "today" | "yesterday" | "older" title: string sessions: HomeSessionRecord[] } export default function Home() { const settings = useSettings() return ( }> ) } function HomeDesign() { const sync = useServerSync() const layout = useLayout() const platform = usePlatform() const dialog = useDialog() const navigate = useNavigate() const server = useServer() const language = useLanguage() const notification = useNotification() const [state, setState] = createStore({ search: "", project: undefined as string | undefined }) const projects = createMemo(() => layout.projects.list()) const selectedProject = createMemo( () => projects().find((project) => project.worktree === state.project) ?? projects()[0], ) const directories = (project: LocalProject) => [project.worktree, ...(project.sandboxes ?? [])] const projectDirectories = createMemo(() => { const project = selectedProject() if (!project) return [] return directories(project) }) const search = createMemo(() => state.search.trim()) const sessionLoad = useQuery(() => ({ queryKey: ["home", "sessions", ...projectDirectories()] as const, queryFn: async () => { await Promise.all(projectDirectories().map((directory) => sync.project.loadSessions(directory))) return null }, })) const projectByID = createMemo( () => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))), ) const records = createMemo(() => [ ...new Map( projectDirectories() .flatMap((directory) => sortedRootSessions(sync.child(directory, { bootstrap: false })[0], Date.now())) .map((session) => [`${pathKey(session.directory)}:${session.id}`, session] as const), ).values(), ] .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) .flatMap((session) => { const project = projectForSession(session, projects(), projectByID()) if (!project) return [] return { session, project, projectName: displayName(project), } }) .filter((record) => { const value = search().toLowerCase() if (!value) return true return `${record.session.title} ${record.projectName}`.toLowerCase().includes(value) }) .slice(0, HOME_SESSION_LIMIT), ) const groups = createMemo(() => groupSessions(records(), language)) function selectProject(directory: string) { if (!projects().some((project) => project.worktree === directory)) return setState("project", directory) } function addProject(directory: string) { layout.projects.open(directory) server.projects.touch(directory) setState("project", directory) } function openNewSession() { const project = selectedProject() if (!project) { void chooseProject() return } layout.projects.open(project.worktree) server.projects.touch(project.worktree) navigate(`/${base64Encode(project.worktree)}/session`) } function openProjectNewSession(directory: string) { layout.projects.open(directory) server.projects.touch(directory) navigate(`/${base64Encode(directory)}/session`) } const showEditProjectDialog = (project: LocalProject) => { void import("@/components/dialog-edit-project").then((x) => { dialog.show(() => ) }) } const unseenCount = (project: LocalProject) => directories(project).reduce((total, directory) => total + notification.project.unseenCount(directory), 0) const clearNotifications = (project: LocalProject) => directories(project) .filter((directory) => notification.project.unseenCount(directory) > 0) .forEach((directory) => notification.project.markViewed(directory)) function openSession(session: Session) { const project = projectForSession(session, projects(), projectByID()) layout.projects.open(project?.worktree ?? session.directory) server.projects.touch(project?.worktree ?? session.directory) navigate(`/${base64Encode(session.directory)}/session/${session.id}`) } async function chooseProject() { function resolve(result: string | string[] | null) { if (Array.isArray(result)) { result.forEach(addProject) if (result[0]) setState("project", result[0]) return } if (result) addProject(result) } if (platform.openDirectoryPickerDialog && server.isLocal()) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, }) resolve(result) return } dialog.show( () => , () => resolve(null), ) } function openSettings() { void import("@/components/dialog-settings").then((x) => { dialog.show(() => ) }) } return (
void chooseProject()} editProject={showEditProjectDialog} closeProject={(directory) => { layout.projects.close(directory) if (state.project === directory) setState("project", undefined) }} clearNotifications={clearNotifications} unseenCount={unseenCount} openSettings={openSettings} openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} language={language} />
void chooseProject()} /> } > setState("search", value)} clearLabel={language.t("common.clear")} onClear={() => setState("search", "")} />
} > 0} fallback={ } > {(group, index) => (
{(record) => }
)}
) } function HomeProjectColumn(props: { selectedProject?: string selectProject: (directory: string) => void openNewSession: (directory: string) => void chooseProject: () => void editProject: (project: LocalProject) => void closeProject: (directory: string) => void clearNotifications: (project: LocalProject) => void unseenCount: (project: LocalProject) => number openSettings: () => void openHelp: () => void language: ReturnType }) { const servers = useServers() const layout = useLayout() const projects = createMemo(() => layout.projects.list()) return ( ) } function HomeProjectRow(props: { project: LocalProject selected: boolean unseenCount: number selectProject: (directory: string) => void openNewSession: (directory: string) => void editProject: (project: LocalProject) => void closeProject: (directory: string) => void clearNotifications: (project: LocalProject) => void language: ReturnType }) { const name = createMemo(() => displayName(props.project)) return (
} aria-label={props.language.t("command.session.new")} onClick={(event) => { event.stopPropagation() props.openNewSession(props.project.worktree) }} /> } aria-label={props.language.t("common.moreOptions")} /> props.openNewSession(props.project.worktree)}> {props.language.t("command.session.new")} props.editProject(props.project)}> {props.language.t("common.edit")} props.clearNotifications(props.project)}> {props.language.t("sidebar.project.clearNotifications")} props.closeProject(props.project.worktree)}> {props.language.t("common.close")}
) } function HomeProjectAvatar(props: { project: LocalProject }) { const name = createMemo(() => displayName(props.project)) return ( ) } function HomeSessionSearch(props: { value: string placeholder: string clearLabel: string onInput: (value: string) => void onClear: () => void }) { return ( ) } function HomeEmptyState(props: { icon: Parameters[0]["name"] title: string description: string action: string onAction: () => void }) { return (
{props.title}
{props.description}
{props.action}
) } function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) { const language = useLanguage() return (
{(onNewSession) => ( {language.t("command.session.new")} )}
) } function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) { const serverSync = useServerSync() const notification = useNotification() const permission = usePermission() const [sessionStore] = serverSync.child(props.record.session.directory, { bootstrap: false }) const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id)) const hasPermissions = createMemo( () => !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.record.session.id, (item) => { return !permission.autoResponds(item, props.record.session.directory) }), ) const isWorking = createMemo(() => { if (hasPermissions()) return false return sessionStore.session_working(props.record.session.id) }) const tint = createMemo(() => messageAgentColor(sessionStore.message[props.record.session.id], sessionStore.agent)) const showStatus = createMemo(() => isWorking() || hasPermissions() || hasError() || unseenCount() > 0) return ( ) } function HomeSessionSkeleton(props: { label: string }) { return (
) } function groupSessions(records: HomeSessionRecord[], language: ReturnType): HomeSessionGroup[] { const now = DateTime.local() const yesterday = now.minus({ days: 1 }) const todaySessions = records.filter((record) => DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(now, "day"), ) const yesterdaySessions = records.filter((record) => DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(yesterday, "day"), ) const olderSessions = records.filter((record) => { const time = DateTime.fromMillis(record.session.time.updated ?? record.session.time.created) return !time.hasSame(now, "day") && !time.hasSame(yesterday, "day") }) const olderTitle = todaySessions.length === 0 && yesterdaySessions.length === 0 ? language.t("sidebar.project.recentSessions") : language.t("home.sessions.group.older") return [ { id: "today" as const, title: language.t("home.sessions.group.today"), sessions: todaySessions }, { id: "yesterday" as const, title: language.t("home.sessions.group.yesterday"), sessions: yesterdaySessions }, { id: "older" as const, title: olderTitle, sessions: olderSessions }, ].filter((group) => group.sessions.length > 0) } function LegacyHome() { const sync = useServerSync() const layout = useLayout() const platform = usePlatform() const dialog = useDialog() const navigate = useNavigate() const servers = useServers() const server = useServer() const language = useLanguage() const homedir = createMemo(() => sync.data.path.home) const recent = createMemo(() => { return sync.data.project .slice() .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) .slice(0, 5) }) const serverDotClass = createMemo(() => { const healthy = servers.health[server.key]?.healthy if (healthy === true) return "bg-icon-success-base" if (healthy === false) return "bg-icon-critical-base" return "bg-border-weak-base" }) function openProject(directory: string) { layout.projects.open(directory) server.projects.touch(directory) navigate(`/${base64Encode(directory)}`) } async function chooseProject() { function resolve(result: string | string[] | null) { if (Array.isArray(result)) { for (const directory of result) { openProject(directory) } } else if (result) { openProject(result) } } if (platform.openDirectoryPickerDialog && server.isLocal()) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, }) resolve(result) } else { dialog.show( () => , () => resolve(null), ) } } return (
0}>
{language.t("home.recentProjects")}
    {(project) => ( )}
{language.t("common.loading")}
{language.t("home.empty.title")}
{language.t("home.empty.description")}
) } function ProjectList(props: { projects: LocalProject[] selectedProject?: string onSelectedProjectChange?(project: string): void onChooseProject?(): void openNewSession: (directory: string) => void editProject: (project: LocalProject) => void closeProject: (directory: string) => void clearNotifications: (project: LocalProject) => void unseenCount: (project: LocalProject) => number language: ReturnType }) { return ( 0} fallback={ } >
{(project) => ( props.onSelectedProjectChange?.(directory)} openNewSession={props.openNewSession} editProject={props.editProject} closeProject={props.closeProject} clearNotifications={props.clearNotifications} language={props.language} /> )}
) }