Skip to content

Commit 84e6ba9

Browse files
feat(tui): wire pagination UI and Home/End jumps
1 parent b1c792a commit 84e6ba9

1 file changed

Lines changed: 113 additions & 7 deletions

File tree

  • packages/opencode/src/cli/cmd/tui/routes/session

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export function Session() {
123123
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
124124
})
125125
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
126+
const paging = createMemo(() => sync.data.message_page[route.sessionID])
126127
const permissions = createMemo(() => {
127128
if (session()?.parentID) return []
128129
return children().flatMap((x) => sync.data.permission[x.id] ?? [])
@@ -132,6 +133,53 @@ export function Session() {
132133
return children().flatMap((x) => sync.data.question[x.id] ?? [])
133134
})
134135

136+
const LOAD_MORE_THRESHOLD = 5
137+
138+
const loadOlder = () => {
139+
const page = paging()
140+
if (!page?.hasOlder || page.loading || !scroll) return
141+
if (scroll.scrollTop > LOAD_MORE_THRESHOLD) return
142+
143+
const anchor = (() => {
144+
const scrollTop = scroll.scrollTop
145+
const children = scroll.getChildren()
146+
for (const child of children) {
147+
if (!child.id) continue
148+
if (child.y + child.height > scrollTop) {
149+
return { id: child.id, offset: scrollTop - child.y }
150+
}
151+
}
152+
return undefined
153+
})()
154+
155+
const height = scroll.scrollHeight
156+
const scrollTop = scroll.scrollTop
157+
sync.session.loadOlder(route.sessionID).then(() => {
158+
queueMicrotask(() => {
159+
requestAnimationFrame(() => {
160+
if (anchor) {
161+
const child = scroll.getChildren().find((item) => item.id === anchor.id)
162+
if (child) {
163+
scroll.scrollTo(child.y + anchor.offset)
164+
return
165+
}
166+
}
167+
168+
const delta = scroll.scrollHeight - height
169+
if (delta > 0) scroll.scrollTo(scrollTop + delta)
170+
})
171+
})
172+
})
173+
}
174+
175+
const loadNewer = () => {
176+
const page = paging()
177+
if (!page?.hasNewer || page.loading || !scroll) return
178+
const bottomDistance = scroll.scrollHeight - scroll.scrollTop - scroll.viewport.height
179+
if (bottomDistance > LOAD_MORE_THRESHOLD) return
180+
sync.session.loadNewer(route.sessionID)
181+
}
182+
135183
const pending = createMemo(() => {
136184
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
137185
})
@@ -247,7 +295,7 @@ export function Session() {
247295
const findNextVisibleMessage = (direction: "next" | "prev"): string | null => {
248296
const children = scroll.getChildren()
249297
const messagesList = messages()
250-
const scrollTop = scroll.y
298+
const scrollTop = scroll.scrollTop
251299

252300
// Get visible messages sorted by position, filtering for valid non-synthetic, non-ignored content
253301
const visibleMessages = children
@@ -285,7 +333,7 @@ export function Session() {
285333
}
286334

287335
const child = scroll.getChildren().find((c) => c.id === targetID)
288-
if (child) scroll.scrollBy(child.y - scroll.y - 1)
336+
if (child) scroll.scrollBy(child.y - scroll.scrollTop - 1)
289337
dialog.clear()
290338
}
291339

@@ -365,7 +413,7 @@ export function Session() {
365413
const child = scroll.getChildren().find((child) => {
366414
return child.id === messageID
367415
})
368-
if (child) scroll.scrollBy(child.y - scroll.y - 1)
416+
if (child) scroll.scrollBy(child.y - scroll.scrollTop - 1)
369417
}}
370418
sessionID={route.sessionID}
371419
setPrompt={(promptInfo) => prompt.set(promptInfo)}
@@ -388,7 +436,7 @@ export function Session() {
388436
const child = scroll.getChildren().find((child) => {
389437
return child.id === messageID
390438
})
391-
if (child) scroll.scrollBy(child.y - scroll.y - 1)
439+
if (child) scroll.scrollBy(child.y - scroll.scrollTop - 1)
392440
}}
393441
sessionID={route.sessionID}
394442
/>
@@ -650,7 +698,16 @@ export function Session() {
650698
category: "Session",
651699
hidden: true,
652700
onSelect: (dialog) => {
653-
scroll.scrollTo(0)
701+
const page = paging()
702+
if (page?.hasOlder && !page.loading) {
703+
sync.session.jumpToOldest(route.sessionID).then(() => {
704+
requestAnimationFrame(() => {
705+
if (scroll) scroll.scrollTo(0)
706+
})
707+
})
708+
} else {
709+
scroll.scrollTo(0)
710+
}
654711
dialog.clear()
655712
},
656713
},
@@ -661,7 +718,16 @@ export function Session() {
661718
category: "Session",
662719
hidden: true,
663720
onSelect: (dialog) => {
664-
scroll.scrollTo(scroll.scrollHeight)
721+
const page = paging()
722+
if (page?.hasNewer && !page.loading) {
723+
sync.session.jumpToLatest(route.sessionID).then(() => {
724+
requestAnimationFrame(() => {
725+
if (scroll) scroll.scrollTo(scroll.scrollHeight)
726+
})
727+
})
728+
} else {
729+
scroll.scrollTo(scroll.scrollHeight)
730+
}
665731
dialog.clear()
666732
},
667733
},
@@ -691,7 +757,7 @@ export function Session() {
691757
const child = scroll.getChildren().find((child) => {
692758
return child.id === message.id
693759
})
694-
if (child) scroll.scrollBy(child.y - scroll.y - 1)
760+
if (child) scroll.scrollBy(child.y - scroll.scrollTop - 1)
695761
break
696762
}
697763
}
@@ -961,8 +1027,38 @@ export function Session() {
9611027
<Show when={!sidebarVisible() || !wide()}>
9621028
<Header />
9631029
</Show>
1030+
<Show when={paging()?.loading && paging()?.loadingDirection === "older"}>
1031+
<box flexShrink={0} paddingLeft={1}>
1032+
<text fg={theme.textMuted}>Loading older messages...</text>
1033+
</box>
1034+
</Show>
1035+
<Show when={!paging()?.loading && paging()?.hasOlder}>
1036+
<box flexShrink={0} paddingLeft={1}>
1037+
<text fg={theme.textMuted}>(scroll up for more)</text>
1038+
</box>
1039+
</Show>
1040+
<Show when={paging()?.error}>
1041+
<box flexShrink={0} paddingLeft={1}>
1042+
<text fg={theme.error}>Failed to load: {paging()?.error}</text>
1043+
<text fg={theme.textMuted}> (scroll to retry)</text>
1044+
</box>
1045+
</Show>
9641046
<scrollbox
9651047
ref={(r) => (scroll = r)}
1048+
onMouseScroll={() => {
1049+
loadOlder()
1050+
loadNewer()
1051+
}}
1052+
onKeyDown={(e) => {
1053+
// Standard scroll triggers incremental load
1054+
if (["up", "pageup", "home"].includes(e.name)) {
1055+
setTimeout(loadOlder, 0)
1056+
}
1057+
if (["down", "pagedown", "end"].includes(e.name)) {
1058+
setTimeout(loadNewer, 0)
1059+
}
1060+
}}
1061+
viewportCulling={true}
9661062
viewportOptions={{
9671063
paddingRight: showScrollbar() ? 1 : 0,
9681064
}}
@@ -1075,6 +1171,16 @@ export function Session() {
10751171
)}
10761172
</For>
10771173
</scrollbox>
1174+
<Show when={paging()?.loading && paging()?.loadingDirection === "newer"}>
1175+
<box flexShrink={0} paddingLeft={1}>
1176+
<text fg={theme.textMuted}>Loading newer messages...</text>
1177+
</box>
1178+
</Show>
1179+
<Show when={!paging()?.loading && paging()?.hasNewer}>
1180+
<box flexShrink={0} paddingLeft={1}>
1181+
<text fg={theme.textMuted}>(scroll down for more)</text>
1182+
</box>
1183+
</Show>
10781184
<box flexShrink={0}>
10791185
<Show when={permissions().length > 0}>
10801186
<PermissionPrompt request={permissions()[0]} />

0 commit comments

Comments
 (0)