feat: session load perf (#17186)

This commit is contained in:
Adam
2026-03-12 11:31:52 -05:00
committed by GitHub
parent 776e7a9c15
commit 0e077f7483
9 changed files with 468 additions and 134 deletions
+42 -11
View File
@@ -10,6 +10,7 @@ import { base64Encode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path"
import { A, useNavigate, useParams } from "@solidjs/router"
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
@@ -67,6 +68,8 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
export type SessionItemProps = {
session: Session
list: Session[]
navList?: Accessor<Session[]>
slug: string
mobile?: boolean
dense?: boolean
@@ -95,18 +98,18 @@ const SessionRow = (props: {
setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void
sidebarOpened: Accessor<boolean>
prefetchSession: (session: Session, priority?: "high" | "low") => void
scheduleHoverPrefetch: () => void
warmHover: () => void
warmPress: () => void
warmFocus: () => void
cancelHoverPrefetch: () => void
}): JSX.Element => (
<A
href={`/${props.slug}/session/${props.session.id}`}
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
onPointerEnter={props.scheduleHoverPrefetch}
onPointerDown={props.warmPress}
onPointerEnter={props.warmHover}
onPointerLeave={props.cancelHoverPrefetch}
onMouseEnter={props.scheduleHoverPrefetch}
onMouseLeave={props.cancelHoverPrefetch}
onFocus={() => props.prefetchSession(props.session, "high")}
onFocus={props.warmFocus}
onClick={() => {
props.setHoverSession(undefined)
if (props.sidebarOpened()) return
@@ -225,11 +228,37 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const hoverMessages = createMemo(() =>
sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"),
)
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
const hoverReady = createMemo(() => {
if (sessionStore.message[props.session.id] === undefined) return false
if (props.session.id === params.id) return true
const info = getSessionPrefetch(props.session.directory, props.session.id)
if (!info) return false
return Date.now() - info.at < SESSION_PREFETCH_TTL
})
const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
const isActive = createMemo(() => 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 hoverPrefetch = {
current: undefined as ReturnType<typeof setTimeout> | undefined,
}
@@ -239,11 +268,12 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
hoverPrefetch.current = undefined
}
const scheduleHoverPrefetch = () => {
warm(1, "high")
if (hoverPrefetch.current !== undefined) return
hoverPrefetch.current = setTimeout(() => {
hoverPrefetch.current = undefined
props.prefetchSession(props.session)
}, 200)
warm(2, "low")
}, 80)
}
onCleanup(cancelHoverPrefetch)
@@ -267,8 +297,9 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
setHoverSession={props.setHoverSession}
clearHoverProjectSoon={props.clearHoverProjectSoon}
sidebarOpened={layout.sidebar.opened}
prefetchSession={props.prefetchSession}
scheduleHoverPrefetch={scheduleHoverPrefetch}
warmHover={scheduleHoverPrefetch}
warmPress={() => warm(2, "high")}
warmFocus={() => warm(2, "high")}
cancelHoverPrefetch={cancelHoverPrefetch}
/>
)
@@ -30,7 +30,7 @@ export type ProjectSidebarContext = {
workspacesEnabled: (project: LocalProject) => boolean
workspaceIds: (project: LocalProject) => string[]
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
sessionProps: Omit<SessionItemProps, "session" | "slug" | "children" | "mobile" | "dense" | "popover">
sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "children" | "mobile" | "dense" | "popover">
setHoverSession: (id: string | undefined) => void
}
@@ -204,11 +204,12 @@ const ProjectPreviewPanel = (props: {
<Show
when={props.workspaceEnabled()}
fallback={
<For each={props.projectSessions()}>
<For each={props.projectSessions().slice(0, 2)}>
{(session) => (
<SessionItem
{...props.ctx.sessionProps}
session={session}
list={props.projectSessions()}
slug={base64Encode(props.project.worktree)}
dense
mobile={props.mobile}
@@ -231,11 +232,12 @@ const ProjectPreviewPanel = (props: {
</div>
<span class="truncate text-14-medium text-text-base">{props.label(directory)}</span>
</div>
<For each={sessions()}>
<For each={sessions().slice(0, 2)}>
{(session) => (
<SessionItem
{...props.ctx.sessionProps}
session={session}
list={sessions()}
slug={base64Encode(directory)}
dense
mobile={props.mobile}
@@ -317,11 +319,11 @@ export const SortableProject = (props: {
}
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()).slice(0, 2))
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
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)
return sortedRootSessions(data, props.sortNow())
}
const workspaceChildren = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false })
@@ -32,6 +32,7 @@ type InlineEditorComponent = (props: {
export type WorkspaceSidebarContext = {
currentDir: Accessor<string>
navList: Accessor<Session[]>
sidebarExpanded: Accessor<boolean>
sidebarHovering: Accessor<boolean>
nav: Accessor<HTMLElement | undefined>
@@ -265,6 +266,8 @@ const WorkspaceSessionList = (props: {
{(session) => (
<SessionItem
session={session}
list={props.sessions()}
navList={props.ctx.navList}
slug={props.slug()}
mobile={props.mobile}
popover={props.popover}