summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-12 16:14:51 -0500
committerAdam <[email protected]>2026-03-12 16:25:48 -0500
commitf2cad046e6c38885b454d01cb28888152a54b375 (patch)
tree99023b61403435e9042039ae55e143897cac1a02
parentd722026a8dffe3a9678ffb82cab72bcde0fde720 (diff)
downloadopencode-f2cad046e6c38885b454d01cb28888152a54b375.tar.gz
opencode-f2cad046e6c38885b454d01cb28888152a54b375.zip
fix(app): message loading
-rw-r--r--packages/app/src/context/sync.tsx11
-rw-r--r--packages/app/src/pages/session.tsx109
2 files changed, 104 insertions, 16 deletions
diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx
index db7b06388..3e3696924 100644
--- a/packages/app/src/context/sync.tsx
+++ b/packages/app/src/context/sync.tsx
@@ -233,8 +233,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
})
.finally(() => {
- if (!tracked(input.directory, input.sessionID)) return
- setMeta("loading", key, false)
+ setMeta(
+ produce((draft) => {
+ if (!tracked(input.directory, input.sessionID)) {
+ delete draft.loading[key]
+ return
+ }
+ draft.loading[key] = false
+ }),
+ )
})
}
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 7a8318374..57ef1853d 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -60,6 +60,7 @@ const emptyFollowups: (FollowupDraft & { id: string })[] = []
type SessionHistoryWindowInput = {
sessionID: () => string | undefined
messagesReady: () => boolean
+ loaded: () => number
visibleUserMessages: () => UserMessage[]
historyMore: () => boolean
historyLoading: () => boolean
@@ -157,23 +158,39 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
const start = turnStart()
const beforeVisible = input.visibleUserMessages().length
+ let loaded = input.loaded()
if (start > 0) setTurnStart(0)
if (!input.historyMore() || input.historyLoading()) return
- await input.loadMore(id)
- if (input.sessionID() !== id) return
+ let afterVisible = beforeVisible
+ let added = 0
- const afterVisible = input.visibleUserMessages().length
- const growth = afterVisible - beforeVisible
+ while (true) {
+ await input.loadMore(id)
+ if (input.sessionID() !== id) return
+
+ afterVisible = input.visibleUserMessages().length
+ const nextLoaded = input.loaded()
+ const raw = nextLoaded - loaded
+ added += raw
+ loaded = nextLoaded
+
+ if (afterVisible > beforeVisible) break
+ if (raw <= 0) break
+ if (!input.historyMore()) break
+ }
+
+ if (added <= 0) return
if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0)
+
+ const growth = afterVisible - beforeVisible
if (growth <= 0) return
if (turnStart() !== 0) return
- const target = Math.min(afterVisible, Math.max(beforeVisible, renderedUserMessages().length) + turnBatch)
- const nextStart = Math.max(0, afterVisible - target)
- preserveScroll(() => setTurnStart(nextStart))
+ const target = Math.min(afterVisible, beforeVisible + turnBatch)
+ setTurnStart(Math.max(0, afterVisible - target))
}
/** Scroll/prefetch path: fetch older history from server. */
@@ -192,19 +209,35 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
const start = turnStart()
const beforeVisible = input.visibleUserMessages().length
const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length
-
- await input.loadMore(id)
- if (input.sessionID() !== id) return
+ let loaded = input.loaded()
+ let added = 0
+ let growth = 0
+
+ while (true) {
+ await input.loadMore(id)
+ if (input.sessionID() !== id) return
+
+ const nextLoaded = input.loaded()
+ const raw = nextLoaded - loaded
+ added += raw
+ loaded = nextLoaded
+ growth = input.visibleUserMessages().length - beforeVisible
+
+ if (growth > 0) break
+ if (raw <= 0) break
+ if (opts?.prefetch) break
+ if (!input.historyMore()) break
+ }
const afterVisible = input.visibleUserMessages().length
- const growth = afterVisible - beforeVisible
if (opts?.prefetch) {
- setState("prefetchNoGrowth", growth > 0 ? 0 : state.prefetchNoGrowth + 1)
- } else if (growth > 0 && state.prefetchNoGrowth) {
+ setState("prefetchNoGrowth", added > 0 ? 0 : state.prefetchNoGrowth + 1)
+ } else if (added > 0 && state.prefetchNoGrowth) {
setState("prefetchNoGrowth", 0)
}
+ if (added <= 0) return
if (growth <= 0) return
if (turnStart() !== start) return
@@ -1161,6 +1194,7 @@ export default function Page() {
let scrollStateFrame: number | undefined
let scrollStateTarget: HTMLDivElement | undefined
+ let fillFrame: number | undefined
const updateScrollState = (el: HTMLDivElement) => {
const max = el.scrollHeight - el.clientHeight
@@ -1208,10 +1242,14 @@ export default function Page() {
),
)
+ let fill = () => {}
+
const setScrollRef = (el: HTMLDivElement | undefined) => {
scroller = el
autoScroll.scrollRef(el)
- if (el) scheduleScrollState(el)
+ if (!el) return
+ scheduleScrollState(el)
+ fill()
}
const markUserScroll = () => {
@@ -1223,12 +1261,14 @@ export default function Page() {
() => {
const el = scroller
if (el) scheduleScrollState(el)
+ fill()
},
)
const historyWindow = createSessionHistoryWindow({
sessionID: () => params.id,
messagesReady,
+ loaded: () => messages().length,
visibleUserMessages,
historyMore,
historyLoading,
@@ -1237,6 +1277,45 @@ export default function Page() {
scroller: () => scroller,
})
+ fill = () => {
+ if (fillFrame !== undefined) return
+
+ fillFrame = requestAnimationFrame(() => {
+ fillFrame = undefined
+
+ if (!params.id || !messagesReady()) return
+ if (autoScroll.userScrolled() || historyLoading()) return
+
+ const el = scroller
+ if (!el) return
+ if (el.scrollHeight > el.clientHeight + 1) return
+ if (historyWindow.turnStart() <= 0 && !historyMore()) return
+
+ void historyWindow.loadAndReveal()
+ })
+ }
+
+ createEffect(
+ on(
+ () =>
+ [
+ params.id,
+ messagesReady(),
+ historyWindow.turnStart(),
+ historyMore(),
+ historyLoading(),
+ autoScroll.userScrolled(),
+ visibleUserMessages().length,
+ ] as const,
+ ([id, ready, start, more, loading, scrolled]) => {
+ if (!id || !ready || loading || scrolled) return
+ if (start <= 0 && !more) return
+ fill()
+ },
+ { defer: true },
+ ),
+ )
+
const draft = (id: string) =>
extractPromptFromParts(sync.data.part[id] ?? [], {
directory: sdk.directory,
@@ -1532,6 +1611,7 @@ export default function Page() {
if (stick) autoScroll.forceScrollToBottom()
if (el) scheduleScrollState(el)
+ fill()
},
)
@@ -1565,6 +1645,7 @@ export default function Page() {
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
+ if (fillFrame !== undefined) cancelAnimationFrame(fillFrame)
})
return (