import type { Session } from "@opencode-ai/sdk/v2/client" import { type ComponentProps, createEffect, createMemo, createResource, createRoot, For, Match, on, onCleanup, onMount, Show, startTransition, Switch, } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" import { createStore, produce } 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 { ScrollView } from "@opencode-ai/ui/scroll-view" import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { getProjectAvatarVariant, useLayout, type HomeProjectSelection, 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 { useDirectoryPicker } from "@/components/directory-picker" import { useSettingsCommand } from "@/components/settings-dialog" import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server" import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2" import { ServerConnection, serverName, useServer } from "@/context/server" import { sessionHasOpenTab, useTabs } from "@/context/tabs" import { useServerSync, type ServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useNotification } from "@/context/notification" import { closeHomeProject, displayName, errorMessage, getProjectAvatarSource, homeProjectDirectories, projectForSession, sortedRootSessions, toggleHomeProjectSelection, } from "@/pages/layout/helpers" import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar" import { sessionTitle } from "@/utils/session-title" import { pathKey } from "@/utils/path-key" import { useGlobal } from "@/context/global" import { useCommand } from "@/context/command" import { Binary } from "@opencode-ai/core/util/binary" import { ServerRowMenu } from "@/components/server/server-row-menu" import { ServerHealthIndicator } from "@/components/server/server-row" import { type ServerHealth } from "@/utils/server-health" import { Persist, persisted } from "@/utils/persist" import { useMarked } from "@opencode-ai/ui/context/marked" import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache" import { archiveHomeSession } from "./home-session-archive" import { showToast } from "@/utils/toast" const HOME_SESSION_LIMIT = 64 const HOME_SESSION_HEADER_STICKY_TOP = 12 const HOME_SESSION_HEADER_TEXT_HEIGHT = 16 const HOME_SESSION_HEADER_FADE_DISTANCE = 16 const SHOW_HOME_SESSION_ARCHIVE = false const HOME_ROW_LAYOUT = "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none" const HOME_ROW_BASE = `${HOME_ROW_LAYOUT} border-0` const HOME_ROW = `${HOME_ROW_BASE} [font-weight:530] text-v2-text-text-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover` const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap" const HOME_PROJECT_NAV_ROW = `${HOME_ROW_LAYOUT} h-7 gap-2 px-1.5 [font-weight:440] text-v2-text-text-muted hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base hover:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:bg-v2-background-bg-layer-03 data-[selected]:text-v2-text-text-base data-[selected]:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:hover:bg-v2-background-bg-layer-03 focus-visible:bg-v2-background-bg-layer-01 focus-visible:text-v2-text-text-base focus-visible:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]` 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[] } const HOME_SESSION_SEARCH_RESULTS_ID = "home-session-search-results" const HOME_SEARCH_RESULT_ROW = "flex h-10 w-full shrink-0 cursor-default items-center gap-2 border-0 py-3 pl-[18px] pr-6 text-left transition-[background-color] 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_SEARCH_RESULT_TITLE = "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:530]" const HOME_SEARCH_RESULT_META = "min-w-0 flex-[1_1_auto] overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]" let pendingHomeNavigation: { server: ServerConnection.Key; href: string } | undefined function buildHomeSessionRecords(input: { sync: Pick projectDirectories: () => string[] projects: () => LocalProject[] projectByID: () => Map }) { return [ ...new Map( input .projectDirectories() .flatMap((directory) => sortedRootSessions(input.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, input.projects(), input.projectByID()) if (!project) return [] return { session, project, projectName: displayName(project), } }) } function matchesHomeSessionSearch(record: HomeSessionRecord, query: string) { return `${record.session.title} ${record.projectName}`.toLowerCase().includes(query) } function homeSessionSearchKey(record: HomeSessionRecord) { return `${pathKey(record.session.directory)}:${record.session.id}` } function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) { let viewport: HTMLDivElement | undefined let content: HTMLDivElement | undefined let positionFrame: number | undefined let resizeObserver: ResizeObserver | undefined const headerRefs = new Map() const headerOffsets = new Map() const [state, setState] = createStore({ titleOpacity: {} as Partial>, }) createEffect(() => { const items = groups() const ids = new Set(items.map((group) => group.id)) headerRefs.forEach((_, id) => { if (!ids.has(id)) headerRefs.delete(id) }) headerOffsets.forEach((_, id) => { if (!ids.has(id)) headerOffsets.delete(id) }) if (items.length === 0) { content = undefined bindResizeObserver() } queuePositionUpdate() }) onCleanup(() => { if (positionFrame !== undefined) cancelAnimationFrame(positionFrame) resizeObserver?.disconnect() }) function setViewport(el: HTMLDivElement) { viewport = el bindResizeObserver() queuePositionUpdate() } function setContentRef(el: HTMLDivElement) { content = el bindResizeObserver() queuePositionUpdate() } function setHeaderRef(id: HomeSessionGroup["id"], el: HTMLDivElement) { headerRefs.set(id, el) queuePositionUpdate() } function queuePositionUpdate() { if (typeof requestAnimationFrame === "undefined") { updatePositionCache() return } if (positionFrame !== undefined) return positionFrame = requestAnimationFrame(() => { positionFrame = undefined updatePositionCache() }) } function updatePositionCache() { if (!viewport) return groups().forEach((group) => { const el = headerRefs.get(group.id) if (!el) return headerOffsets.set(group.id, el.offsetTop) }) update(viewport.scrollTop) } function update(scrollTop: number) { const items = groups() items.forEach((group, index) => { const nextOffset = items .slice(index + 1) .map((item) => headerOffsets.get(item.id)) .find((offset) => offset !== undefined) const fadeEnd = HOME_SESSION_HEADER_STICKY_TOP + HOME_SESSION_HEADER_TEXT_HEIGHT const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop const opacity = nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE)) setState("titleOpacity", group.id, Math.round(opacity * 1000) / 1000) }) } function titleOpacity(id: HomeSessionGroup["id"]) { return state.titleOpacity[id] ?? 1 } function bindResizeObserver() { resizeObserver?.disconnect() if (typeof ResizeObserver === "undefined") return resizeObserver = new ResizeObserver(() => queuePositionUpdate()) if (viewport) resizeObserver.observe(viewport) if (content) resizeObserver.observe(content) } return { setViewport, setContentRef, setHeaderRef, update, titleOpacity } } export function NewHome() { const sync = useServerSync() const layout = useLayout() const platform = usePlatform() const pickDirectory = useDirectoryPicker() const dialog = useDialog() const navigate = useNavigate() const server = useServer() const language = useLanguage() const global = useGlobal() const tabs = useTabs() const command = useCommand() const notification = useNotification() const marked = useMarked() const openSettings = useSettingsCommand() let focusSessionSearch: (() => void) | undefined const [state, setState] = createStore({ search: "", searchFocused: false, }) const selection = layout.home.selection const focusedServer = createMemo( () => global.servers.list().find((conn) => ServerConnection.key(conn) === selection().server) ?? server.current, ) const focusedServerCtx = createMemo(() => { const conn = focusedServer() if (!conn) return return global.ensureServerCtx(conn) }) const focusedSync = () => focusedServerCtx()?.sync ?? sync() const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list()) const selectedProject = createMemo(() => projects().find((project) => project.worktree === selection().directory)) const newSessionProject = createMemo( () => selectedProject() ?? projects().find((project) => project.worktree === focusedServerCtx()?.projects.last()) ?? projects()[0], ) const directories = (project: LocalProject) => [project.worktree, ...(project.sandboxes ?? [])] const projectDirectories = createMemo(() => { const project = selectedProject() if (!project) return projects().flatMap(directories) return directories(project) }) const search = createMemo(() => state.search.trim()) const searchPlaceholder = createMemo(() => { const project = selectedProject() if (project) { return language.t("home.sessions.search.placeholder.scoped", { scope: displayName(project) }) } if (global.servers.list().length > 1) { const conn = focusedServer() if (conn) { return language.t("home.sessions.search.placeholder.scoped", { scope: serverName(conn) }) } } return language.t("home.sessions.search.placeholder") }) const sessionLoad = useQuery(() => ({ queryKey: ["home", "sessions", selection().server, ...projectDirectories()] as const, queryFn: async () => { await Promise.all( projectDirectories().map((directory) => focusedSync().project.loadSessions(directory, { limit: HOME_SESSION_LIMIT }), ), ) return null }, })) const projectByID = createMemo( () => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))), ) const allRecords = createMemo(() => buildHomeSessionRecords({ sync: focusedSync(), projectDirectories, projects, projectByID, }), ) const records = createMemo(() => allRecords().slice(0, HOME_SESSION_LIMIT)) const searchResults = createMemo(() => { const query = search().toLowerCase() if (!query) return [] return allRecords().filter((record) => matchesHomeSessionSearch(record, query)) }) const searchOpen = createMemo(() => state.searchFocused && search().length > 0) const groups = createMemo(() => groupSessions(records(), language)) const sessionHeaderOpacity = useHomeSessionHeaderOpacity(groups) const prefetched = new Set() createEffect(() => { const ctx = focusedServerCtx() if (!ctx) return records() .slice(0, 2) .forEach((record) => { const key = `${ServerConnection.key(focusedServer()!)}\0${record.session.id}` if (prefetched.has(key)) return prefetched.add(key) createRoot((dispose) => { try { const directory = ctx.sync.ensureDirSyncContext(record.session.directory) void directory.session .sync(record.session.id) .then(() => { return Promise.all( (ctx.sync.session.data.message[record.session.id] ?? []).flatMap((message) => (ctx.sync.session.data.part[message.id] ?? []).flatMap((part) => { if (part.type !== "text" || !part.text) return [] return preloadMarkdown(part.text, part.id, marked) }), ), ) }) .catch(() => {}) .finally(dispose) } catch { dispose() } }) }) }) function setSelection(next: HomeProjectSelection) { layout.home.setSelection(next) } function closeSearch() { setState("search", "") setState("searchFocused", false) } function selectSearchSession(session: Session) { openSession(session) closeSearch() } command.register("home", () => [ { id: "home.sessions.search.focus", title: searchPlaceholder(), keybind: "mod+f", hidden: true, onSelect: () => focusSessionSearch?.(), }, ]) createEffect(() => { const list = global.servers.list() if (list.some((conn) => ServerConnection.key(conn) === selection().server)) return const conn = list.find((conn) => ServerConnection.key(conn) === server.key) ?? list[0] if (conn) setSelection({ server: ServerConnection.key(conn) }) }) createEffect(() => { const pending = pendingHomeNavigation if (!pending || pending.server !== server.key) return pendingHomeNavigation = undefined navigate(pending.href) }) function focusServer(conn: ServerConnection.Any) { setSelection({ server: ServerConnection.key(conn) }) } function selectProject(conn: ServerConnection.Any, directory: string) { const key = ServerConnection.key(conn) if (global.servers.health[key]?.healthy === false) return if ( !global .ensureServerCtx(conn) .projects.list() .some((project) => project.worktree === directory) ) return setSelection(toggleHomeProjectSelection(selection(), key, directory)) } function addProjects(conn: ServerConnection.Any, directories: string[]) { const directory = directories[0] if (!directory) return const ctx = global.ensureServerCtx(conn) directories.forEach(ctx.projects.open) ctx.projects.touch(directory) setSelection({ server: ServerConnection.key(conn), directory }) } function openNewSession() { const conn = focusedServer() const project = newSessionProject() if (!conn || !project) return openProjectNewSession(conn, project.worktree) } function openProjectNewSession(conn: ServerConnection.Any, directory: string) { const ctx = global.ensureServerCtx(conn) ctx.projects.open(directory) ctx.projects.touch(directory) tabs.newDraft({ server: ServerConnection.key(conn), directory }) } function editProject(conn: ServerConnection.Any, project: LocalProject) { void import("@/components/dialog-edit-project").then((x) => { dialog.show(() => ) }) } function unseenCount(conn: ServerConnection.Any, project: LocalProject) { const state = notification.ensureServerState(ServerConnection.key(conn)) return directories(project).reduce((total, directory) => total + state.project.unseenCount(directory), 0) } function clearNotifications(conn: ServerConnection.Any, project: LocalProject) { const state = notification.ensureServerState(ServerConnection.key(conn)) directories(project) .filter((directory) => state.project.unseenCount(directory) > 0) .forEach((directory) => state.project.markViewed(directory)) } function openSession(session: Session) { const project = projectForSession(session, projects(), projectByID()) const conn = focusedServer() if (!conn) return const directory = project?.worktree ?? session.directory const ctx = global.ensureServerCtx(conn) ctx.projects.open(directory) ctx.projects.touch(directory) startTransition(() => { const tab = tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id }) tabs.select(tab) }) } async function archiveSession(session: Session) { const conn = focusedServer() const ctx = focusedServerCtx() if (!conn || !ctx) return const [, setStore] = ctx.sync.child(session.directory) await archiveHomeSession({ server: ServerConnection.key(conn), session, update: (value) => ctx.sdk.client.session.update(value), remove: () => setStore( produce((draft) => { const match = Binary.search(draft.session, session.id, (s) => s.id) if (match.found) draft.session.splice(match.index, 1) }), ), onError: (error) => showToast({ title: language.t("common.requestFailed"), description: errorMessage(error, language.t("common.requestFailed")), }), }) } function chooseProject(conn: ServerConnection.Any) { if (global.servers.health[ServerConnection.key(conn)]?.healthy === false) return function resolve(result: string | string[] | null) { addProjects(conn, homeProjectDirectories(result)) } pickDirectory({ server: conn, title: language.t("command.project.open"), multiple: true, onSelect: resolve, }) } return (
void chooseProject(conn)} editProject={editProject} closeProject={(conn, directory) => { const next = closeHomeProject( selection(), ServerConnection.key(conn), global.ensureServerCtx(conn).projects, directory, ) if (next) setSelection(next) }} clearNotifications={clearNotifications} unseenCount={unseenCount} openSettings={openSettings} openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} language={language} />
{ focusSessionSearch = focus }} onInput={(value) => setState("search", value)} onFocus={() => setState("searchFocused", true)} onClose={closeSearch} onSelect={selectSearchSession} /> sessionHeaderOpacity.update(event.currentTarget.scrollTop)} > 0 && newSessionProject()}>
{language.t("command.session.new")}
} > 0} fallback={} >
{(group, index) => ( <> sessionHeaderOpacity.setHeaderRef(group.id, el)} elevated={index() === 0} />
{(record) => ( )}
)}
platform.openLink("https://opencode.ai/desktop-feedback")} language={language} />
) } function HomeProjectColumn(props: { projects: LocalProject[] selected: HomeProjectSelection focusServer: (server: ServerConnection.Any) => void selectProject: (server: ServerConnection.Any, directory: string) => void openNewSession: (server: ServerConnection.Any, directory: string) => void chooseProject: (server: ServerConnection.Any) => void editProject: (server: ServerConnection.Any, project: LocalProject) => void closeProject: (server: ServerConnection.Any, directory: string) => void clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void unseenCount: (server: ServerConnection.Any, project: LocalProject) => number openSettings: () => void openHelp: () => void language: ReturnType }) { const global = useGlobal() const dialog = useDialog() const controller = useServerManagementController({ navigateOnAdd: false }) const [_state, setState, _, ready] = persisted( Persist.global("home.servers", ["home.servers.v1"]), createStore({ collapsed: {} as Record }), ) const [state] = createResource( () => ready.promise ?? Promise.resolve(), (p) => p.then(() => _state), { initialValue: _state }, ) return ( ) } function HomeUtilityNav(props: { class?: string openSettings: () => void openHelp: () => void language: ReturnType }) { return (
) } function HomeServerRow(props: { server: ServerConnection.Any selected: boolean collapsed: boolean health: ServerHealth | undefined controller: ReturnType focusServer: (server: ServerConnection.Any) => void chooseProject: (server: ServerConnection.Any) => void openEdit: (server: ServerConnection.Http) => void toggleCollapsed: () => void language: ReturnType }) { const global = useGlobal() const [state, setState] = createStore({ menuOpen: false }) const healthy = () => !!props.health?.healthy const canToggle = () => healthy() && global.ensureServerCtx(props.server).projects.list().length > 0 return (
setState("menuOpen", open)} /> } aria-label={props.language.t("home.project.add")} disabled={props.health?.healthy === false} onClick={() => props.chooseProject(props.server)} />
) } function HomeProjectList(props: { server: ServerConnection.Any projects: LocalProject[] selected: HomeProjectSelection selectProject: (server: ServerConnection.Any, directory: string) => void openNewSession: (server: ServerConnection.Any, directory: string) => void editProject: (server: ServerConnection.Any, project: LocalProject) => void closeProject: (server: ServerConnection.Any, directory: string) => void clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void unseenCount: (server: ServerConnection.Any, project: LocalProject) => number language: ReturnType }) { return (
{(project) => ( )}
) } function HomeProjectRow(props: { project: LocalProject server: ServerConnection.Any selected: boolean unseenCount: number selectProject: (server: ServerConnection.Any, directory: string) => void openNewSession: (server: ServerConnection.Any, directory: string) => void editProject: (server: ServerConnection.Any, project: LocalProject) => void closeProject: (server: ServerConnection.Any, directory: string) => void clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void language: ReturnType }) { const global = useGlobal() const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false const [state, setState] = createStore({ menuOpen: false }) return (
setState("menuOpen", open)} > } aria-label={props.language.t("common.moreOptions")} /> props.openNewSession(props.server, props.project.worktree)}> {props.language.t("command.session.new")} props.editProject(props.server, props.project)}> {props.language.t("dialog.project.edit.title")} props.clearNotifications(props.server, props.project)} > {props.language.t("sidebar.project.clearNotifications")} props.closeProject(props.server, props.project.worktree)}> {props.language.t("common.close")} } aria-label={props.language.t("command.session.new")} onClick={() => props.openNewSession(props.server, props.project.worktree)} />
) } function HomeProjectAvatar(props: { project: LocalProject }) { const name = createMemo(() => displayName(props.project)) return ( ) } function HomeSessionLeading(props: { project: LocalProject session: Session server: ServerConnection.Key activeServer: boolean revealProjectOnHover: boolean }) { const tabs = useTabs() const hasOpenTab = createMemo(() => sessionHasOpenTab(tabs.store, props.server, props.session)) return (
) } function HomeSessionSearch(props: { value: string placeholder: string open: boolean loading: boolean results: HomeSessionRecord[] showProjectName: boolean server: ServerConnection.Key activeServer: boolean noResultsLabel: string bindFocus: (focus: () => void) => void onInput: (value: string) => void onFocus: () => void onClose: () => void onSelect: (session: Session) => void }) { const language = useLanguage() const [store, setStore] = createStore({ active: "" }) let root: HTMLDivElement | undefined let input: HTMLInputElement | undefined let listRef: HTMLDivElement | undefined const focusInput = () => { input?.focus() props.onFocus() } onMount(() => { props.bindFocus(focusInput) }) const syncActive = (results: HomeSessionRecord[]) => { if (results.length === 0) { setStore("active", "") return } if (!results.some((record) => homeSessionSearchKey(record) === store.active)) { setStore("active", homeSessionSearchKey(results[0])) } } createEffect(() => syncActive(props.results)) createEffect( on( () => props.value, () => syncActive(props.results), ), ) const scrollActiveIntoView = () => { const key = store.active if (!key || !listRef) return const element = listRef.querySelector(`[data-key="${key}"]`) element?.scrollIntoView({ block: "nearest" }) } const moveActive = (delta: number) => { const results = props.results if (results.length === 0) return const index = results.findIndex((record) => homeSessionSearchKey(record) === store.active) const start = index === -1 ? 0 : index const next = (start + delta + results.length) % results.length setStore("active", homeSessionSearchKey(results[next])) scrollActiveIntoView() } const selectActive = () => { const record = props.results.find((item) => homeSessionSearchKey(item) === store.active) if (!record) return props.onSelect(record.session) } onCleanup( makeEventListener(document, "pointerdown", (event) => { if (!props.open) return const target = event.target if (!(target instanceof Node)) return if (root?.contains(target)) return props.onClose() }), ) return (
} > 0} fallback={

{props.noResultsLabel}

} >

{language.t("home.sessions.search.sessions")}

(listRef = el)}>
{(record) => ( setStore("active", homeSessionSearchKey(record))} onSelect={(session) => props.onSelect(session)} /> )}
) } function HomeSessionSearchResultRow(props: { record: HomeSessionRecord showProjectName: boolean server: ServerConnection.Key activeServer: boolean selected: boolean onHighlight: () => void onSelect: (session: Session) => void }) { const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) const showProjectName = () => props.showProjectName && props.record.projectName const key = () => homeSessionSearchKey(props.record) return ( ) } function HomeSessionGroupHeader(props: { title: string titleOpacity: number ref: ComponentProps<"div">["ref"] elevated?: boolean }) { return (
) } function HomeSessionRow(props: { record: HomeSessionRecord showProjectName: boolean server: ServerConnection.Key activeServer: boolean openSession: (session: Session) => void archiveSession: (session: Session) => Promise }) { const language = useLanguage() const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) const showProjectName = () => props.showProjectName && props.record.projectName return (
} aria-label={language.t("common.archive")} onClick={(event) => { event.preventDefault() event.stopPropagation() void props.archiveSession(props.record.session) }} />
) } function HomeSessionsEmpty(props: { onNewSession?: () => void }) { const language = useLanguage() return (
{language.t("home.sessions.empty")}

{language.t("home.sessions.empty.description")}

{(onNewSession) => ( {language.t("command.session.new")} )}
) } 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) } export function LegacyHome() { const sync = useServerSync() const platform = usePlatform() const pickDirectory = useDirectoryPicker() const dialog = useDialog() const navigate = useNavigate() const global = useGlobal() const server = useServer() const language = useLanguage() const homedir = createMemo(() => sync().data.path.home) const serverUnreachable = createMemo(() => global.servers.health[server.key]?.healthy === false) 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 = global.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(server: ServerConnection.Any, directory: string) { const serverCtx = global.ensureServerCtx(server) serverCtx.projects.open(directory) serverCtx.projects.touch(directory) navigate(`/${base64Encode(directory)}`) } function chooseProject() { if (serverUnreachable()) return const s = server.current if (!s) return const resolve = (result: string | string[] | null) => { if (Array.isArray(result)) { for (const directory of result) { openProject(s, directory) } } else if (result) { openProject(s, result) } } pickDirectory({ server: s, title: language.t("command.project.open"), multiple: true, onSelect: resolve, }) } return (
0}>
{language.t("home.recentProjects")}
    {(project) => ( )}
{language.t("common.loading")}
{language.t("home.empty.title")}
{language.t("home.empty.description")}
) }