diff options
| author | Adam <[email protected]> | 2026-01-07 08:41:16 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-08 17:48:15 -0600 |
| commit | 374275eeb691006fa8f422d6267aa694ab38a992 (patch) | |
| tree | 9159db2dabb0ba2e10e82b7efb3cd4fbabf024d0 /packages/app/src/pages | |
| parent | faa848cfb13df4e435dc236fe895032ea6f26dde (diff) | |
| download | opencode-374275eeb691006fa8f422d6267aa694ab38a992.tar.gz opencode-374275eeb691006fa8f422d6267aa694ab38a992.zip | |
feat(app): chunk message loading, lazy load diffs
Diffstat (limited to 'packages/app/src/pages')
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 17 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 243 |
2 files changed, 192 insertions, 68 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index a0f364d9a..50553795c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -55,6 +55,7 @@ import { DialogEditProject } from "@/components/dialog-edit-project" import { DialogSelectServer } from "@/components/dialog-select-server" import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" +import { navStart } from "@/utils/perf" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { useServer } from "@/context/server" @@ -309,6 +310,14 @@ export default function Layout(props: ParentProps) { if (targetIndex >= 0 && targetIndex < sessions.length) { const session = sessions[targetIndex] + if (import.meta.env.DEV) { + navStart({ + dir: base64Encode(session.directory), + from: params.id, + to: session.id, + trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup", + }) + } navigateToSession(session) queueMicrotask(() => scrollToSession(session.id)) return @@ -325,6 +334,14 @@ export default function Layout(props: ParentProps) { } const targetSession = offset > 0 ? nextProjectSessions[0] : nextProjectSessions[nextProjectSessions.length - 1] + if (import.meta.env.DEV) { + navStart({ + dir: base64Encode(targetSession.directory), + from: params.id, + to: targetSession.id, + trigger: offset > 0 ? "alt+arrowdown" : "alt+arrowup", + }) + } navigateToSession(targetSession) queueMicrotask(() => scrollToSession(targetSession.id)) } diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d3d8ef387..4a40ae7d2 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -8,6 +8,7 @@ import { createStore } from "solid-js/store" import { PromptInput } from "@/components/prompt-input" import { SessionContextUsage } from "@/components/session-context-usage" import { IconButton } from "@opencode-ai/ui/icon-button" +import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { DiffChanges } from "@opencode-ai/ui/diff-changes" @@ -49,6 +50,7 @@ import { NewSessionView, } from "@/components/session" import { usePlatform } from "@/context/platform" +import { navMark, navParams } from "@/utils/perf" import { same } from "@/utils/same" type DiffStyle = "unified" | "split" @@ -162,6 +164,46 @@ export default function Page() { const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) + if (import.meta.env.DEV) { + createEffect( + on( + () => [params.dir, params.id] as const, + ([dir, id], prev) => { + if (!id) return + navParams({ dir, from: prev?.[1], to: id }) + }, + ), + ) + + createEffect(() => { + const id = params.id + if (!id) return + if (!prompt.ready()) return + navMark({ dir: params.dir, to: id, name: "storage:prompt-ready" }) + }) + + createEffect(() => { + const id = params.id + if (!id) return + if (!terminal.ready()) return + navMark({ dir: params.dir, to: id, name: "storage:terminal-ready" }) + }) + + createEffect(() => { + const id = params.id + if (!id) return + if (!file.ready()) return + navMark({ dir: params.dir, to: id, name: "storage:file-view-ready" }) + }) + + createEffect(() => { + const id = params.id + if (!id) return + if (sync.data.message[id] === undefined) return + navMark({ dir: params.dir, to: id, name: "session:data-ready" }) + }) + } + const isDesktop = createMediaQuery("(min-width: 768px)") function normalizeTab(tab: string) { @@ -216,6 +258,8 @@ export default function Page() { }) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) + const reviewCount = createMemo(() => info()?.summary?.files ?? 0) + const hasReview = createMemo(() => reviewCount() > 0) const revertMessageID = createMemo(() => info()?.revert?.messageID) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const messagesReady = createMemo(() => { @@ -223,6 +267,16 @@ export default function Page() { if (!id) return true return sync.data.message[id] !== undefined }) + const historyMore = createMemo(() => { + const id = params.id + if (!id) return false + return sync.session.history.more(id) + }) + const historyLoading = createMemo(() => { + const id = params.id + if (!id) return false + return sync.session.history.loading(id) + }) const emptyUserMessages: UserMessage[] = [] const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages) const visibleUserMessages = createMemo(() => { @@ -290,6 +344,12 @@ export default function Page() { } const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) + const diffsReady = createMemo(() => { + const id = params.id + if (!id) return true + if (!hasReview()) return true + return sync.data.session_diff[id] !== undefined + }) const idle = { type: "idle" as const } let inputRef!: HTMLDivElement @@ -643,12 +703,10 @@ export default function Page() { .filter((tab) => tab !== "context"), ) - const reviewTab = createMemo(() => diffs().length > 0 || tabs().active() === "review") - const mobileReview = createMemo(() => !isDesktop() && diffs().length > 0 && store.mobileTab === "review") + const reviewTab = createMemo(() => hasReview() || tabs().active() === "review") + const mobileReview = createMemo(() => !isDesktop() && hasReview() && store.mobileTab === "review") - const showTabs = createMemo( - () => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0 || contextOpen()), - ) + const showTabs = createMemo(() => layout.review.opened() && (hasReview() || tabs().all().length > 0 || contextOpen())) const activeTab = createMemo(() => { const active = tabs().active() @@ -664,10 +722,22 @@ export default function Page() { createEffect(() => { if (!layout.ready()) return if (tabs().active()) return - if (diffs().length === 0 && openedTabs().length === 0 && !contextOpen()) return + if (!hasReview() && openedTabs().length === 0 && !contextOpen()) return tabs().setActive(activeTab()) }) + createEffect(() => { + const id = params.id + if (!id) return + if (!hasReview()) return + + const wants = isDesktop() ? layout.review.opened() && activeTab() === "review" : store.mobileTab === "review" + if (!wants) return + if (diffsReady()) return + + sync.session.diff(id) + }) + const isWorking = createMemo(() => status().type !== "idle") const autoScroll = createAutoScroll({ working: isWorking, @@ -779,7 +849,7 @@ export default function Page() { <SessionHeader /> <div class="flex-1 min-h-0 flex flex-col md:flex-row"> {/* Mobile tab bar - only shown on mobile when there are diffs */} - <Show when={!isDesktop() && diffs().length > 0}> + <Show when={!isDesktop() && hasReview()}> <Tabs class="h-auto"> <Tabs.List> <Tabs.Trigger @@ -796,7 +866,7 @@ export default function Page() { classes={{ button: "w-full" }} onClick={() => setStore("mobileTab", "review")} > - {diffs().length} Files Changed + {reviewCount()} Files Changed </Tabs.Trigger> </Tabs.List> </Tabs> @@ -821,21 +891,26 @@ export default function Page() { when={!mobileReview()} fallback={ <div class="relative h-full overflow-hidden"> - <SessionReviewTab - diffs={diffs} - view={view} - diffStyle="unified" - onViewFile={(path) => { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - classes={{ - root: "pb-[calc(var(--prompt-height,8rem)+32px)]", - header: "px-4", - container: "px-4", - }} - /> + <Show + when={diffsReady()} + fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>} + > + <SessionReviewTab + diffs={diffs} + view={view} + diffStyle="unified" + onViewFile={(path) => { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + classes={{ + root: "pb-[calc(var(--prompt-height,8rem)+32px)]", + header: "px-4", + container: "px-4", + }} + /> + </Show> </div> } > @@ -868,42 +943,69 @@ export default function Page() { "mt-0": showTabs(), }} > - <For each={visibleUserMessages()}> - {(message) => ( - <div - id={anchor(message.id)} - data-message-id={message.id} - classList={{ - "min-w-0 w-full max-w-full": true, - "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]": - platform.platform !== "desktop", - "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]": - platform.platform === "desktop", + <Show when={historyMore()}> + <div class="w-full flex justify-center"> + <Button + variant="ghost" + size="large" + class="text-12-medium opacity-50" + disabled={historyLoading()} + onClick={() => { + const id = params.id + if (!id) return + sync.session.history.loadMore(id) }} > - <SessionTurn - sessionID={params.id!} - messageID={message.id} - lastUserMessageID={lastUserMessage()?.id} - stepsExpanded={store.expanded[message.id] ?? false} - onStepsExpandedToggle={() => - setStore("expanded", message.id, (open: boolean | undefined) => !open) - } - classes={{ - root: "min-w-0 w-full relative", - content: - "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", - container: - "px-4 md:px-6 " + - (!showTabs() - ? "md:max-w-200 md:mx-auto" - : visibleUserMessages().length > 1 - ? "md:pr-6 md:pl-18" - : ""), + {historyLoading() ? "Loading earlier messages..." : "Load earlier messages"} + </Button> + </div> + </Show> + <For each={visibleUserMessages()}> + {(message) => { + if (import.meta.env.DEV) { + onMount(() => { + const id = params.id + if (!id) return + navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" }) + }) + } + + return ( + <div + id={anchor(message.id)} + data-message-id={message.id} + classList={{ + "min-w-0 w-full max-w-full": true, + "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]": + platform.platform !== "desktop", + "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]": + platform.platform === "desktop", }} - /> - </div> - )} + > + <SessionTurn + sessionID={params.id!} + messageID={message.id} + lastUserMessageID={lastUserMessage()?.id} + stepsExpanded={store.expanded[message.id] ?? false} + onStepsExpandedToggle={() => + setStore("expanded", message.id, (open: boolean | undefined) => !open) + } + classes={{ + root: "min-w-0 w-full relative", + content: + "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", + container: + "px-4 md:px-6 " + + (!showTabs() + ? "md:max-w-200 md:mx-auto" + : visibleUserMessages().length > 1 + ? "md:pr-6 md:pl-18" + : ""), + }} + /> + </div> + ) + }} </For> </div> </div> @@ -1035,17 +1137,22 @@ export default function Page() { <Show when={reviewTab()}> <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> - <SessionReviewTab - diffs={diffs} - view={view} - diffStyle={layout.review.diffStyle()} - onDiffStyleChange={layout.review.setDiffStyle} - onViewFile={(path) => { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - /> + <Show + when={diffsReady()} + fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>} + > + <SessionReviewTab + diffs={diffs} + view={view} + diffStyle={layout.review.diffStyle()} + onDiffStyleChange={layout.review.setDiffStyle} + onViewFile={(path) => { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + /> + </Show> </div> </Tabs.Content> </Show> |
