feat(desktop): incrementally load sessions in side nav

This commit is contained in:
Adam
2025-11-05 16:32:05 -06:00
parent d7e31f76c4
commit e006e3355c
2 changed files with 70 additions and 47 deletions

View File

@@ -16,6 +16,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
config: Config config: Config
path: Path path: Path
session: Session[] session: Session[]
limit: number
more: boolean
message: { message: {
[sessionID: string]: Message[] [sessionID: string]: Message[]
} }
@@ -32,6 +34,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
agent: [], agent: [],
provider: [], provider: [],
session: [], session: [],
limit: 10,
more: false,
message: {}, message: {},
part: {}, part: {},
node: [], node: [],
@@ -106,12 +110,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)), path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])), agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
session: () => session: () =>
sdk.client.session.list().then((x) => sdk.client.session.list().then((x) => {
setStore( const sessions = (x.data ?? [])
"session", .slice()
(x.data ?? []).slice().sort((a, b) => a.id.localeCompare(b.id)), .sort((a, b) => a.id.localeCompare(b.id))
), .slice(0, store.limit)
), setStore("session", sessions)
setStore("more", sessions.length === store.limit)
}),
config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)), config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)), changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)), node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
@@ -184,6 +190,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}), }),
) )
}, },
fetch: async (count = 10) => {
setStore("limit", (x) => x + count)
await load.session()
},
}, },
load, load,
absolute, absolute,

View File

@@ -1,5 +1,5 @@
import { Button, Tooltip, DiffChanges } from "@opencode-ai/ui" import { Button, Tooltip, DiffChanges } from "@opencode-ai/ui"
import { createMemo, ParentProps, Show } from "solid-js" import { createMemo, For, ParentProps, Show } from "solid-js"
import { getFilename } from "@/utils" import { getFilename } from "@/utils"
import { DateTime } from "luxon" import { DateTime } from "luxon"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
@@ -9,61 +9,74 @@ import { A, useParams } from "@solidjs/router"
export default function Layout(props: ParentProps) { export default function Layout(props: ParentProps) {
const params = useParams() const params = useParams()
const sync = useSync() const sync = useSync()
return ( return (
<div class="relative h-screen flex flex-col"> <div class="relative h-screen flex flex-col">
<header class="hidden h-12 shrink-0 bg-background-strong border-b border-border-weak-base"></header> <header class="hidden h-12 shrink-0 bg-background-strong border-b border-border-weak-base"></header>
<div class="h-[calc(100vh-0rem)] flex"> <div class="h-[calc(100vh-0rem)] flex">
<div class="w-70 shrink-0 bg-background-weak border-r border-border-weak-base flex flex-col items-start"> <div class="w-70 shrink-0 bg-background-weak border-r border-border-weak-base flex flex-col items-start">
<div class="h-10 flex items-center self-stretch px-5 border-b border-border-weak-base"> <div class="h-10 shrink-0 flex items-center self-stretch px-5 border-b border-border-weak-base">
<span class="text-14-regular overflow-hidden text-ellipsis">{getFilename(sync.data.path.directory)}</span> <span class="text-14-regular overflow-hidden text-ellipsis">{getFilename(sync.data.path.directory)}</span>
</div> </div>
<div class="flex flex-col items-start gap-4 self-stretch flex-1 py-4 px-3"> <div class="flex flex-col items-start gap-4 self-stretch flex-1 py-4 px-3 overflow-hidden">
<A href="/session" class="w-full"> <A href="/session" class="w-full">
<Button class="w-full" size="large" icon="edit-small-2"> <Button class="w-full" size="large" icon="edit-small-2">
New Session New Session
</Button> </Button>
</A> </A>
<VList data={sync.data.session} class="no-scrollbar"> <div class="w-full h-full overflow-y-auto no-scrollbar flex flex-col flex-1">
{(session) => { <nav class="w-full">
const updated = createMemo(() => DateTime.fromMillis(session.time.updated)) <For each={sync.data.session}>
return ( {(session) => {
<A const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
data-active={session.id === params.id} return (
href={`/session/${session.id}`} <A
class="group/session focus:outline-none" data-active={session.id === params.id}
> href={`/session/${session.id}`}
<Tooltip placement="right" value={session.title}> class="group/session focus:outline-none"
<div >
class="w-full mb-1.5 px-3 py-1 rounded-md <Tooltip placement="right" value={session.title}>
<div
class="w-full mb-1.5 px-3 py-1 rounded-md
group-data-[active=true]/session:bg-surface-raised-base-hover group-data-[active=true]/session:bg-surface-raised-base-hover
group-hover/session:bg-surface-raised-base-hover group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover" group-focus/session:bg-surface-raised-base-hover"
> >
<div class="flex items-center self-stretch gap-6 justify-between"> <div class="flex items-center self-stretch gap-6 justify-between">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate"> <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
{session.title} {session.title}
</span> </span>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap"> <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{Math.abs(updated().diffNow().as("seconds")) < 60 {Math.abs(updated().diffNow().as("seconds")) < 60
? "Now" ? "Now"
: updated() : updated()
.toRelative({ style: "short", unit: ["days", "hours", "minutes"] }) .toRelative({ style: "short", unit: ["days", "hours", "minutes"] })
?.replace(" ago", "") ?.replace(" ago", "")
?.replace(/ days?/, "d") ?.replace(/ days?/, "d")
?.replace(" min.", "m") ?.replace(" min.", "m")
?.replace(" hr.", "h")} ?.replace(" hr.", "h")}
</span> </span>
</div> </div>
<div class="flex justify-between items-center self-stretch"> <div class="flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span> <span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show> <Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div> </div>
</div> </div>
</Tooltip> </Tooltip>
</A> </A>
) )
}} }}
</VList> </For>
</nav>
<Show when={sync.data.more}>
<button
class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong"
onClick={() => sync.session.fetch()}
>
Show more
</button>
</Show>
</div>
</div> </div>
</div> </div>
<main class="size-full overflow-x-hidden">{props.children}</main> <main class="size-full overflow-x-hidden">{props.children}</main>