Skip to content

Commit 311970a

Browse files
committed
fix(tui): fix pagination bugs - error display, scroll triggers, and boundary hints
- Fix '[object Object]' error display: extract message from non-Error thrown objects - Fix PgUp/PgDn/line/half-page scroll not triggering page loads at boundaries - Make '(scroll up/down for more)' hints only show near page boundaries - Add missing Binary.lowerBound helper that PR anomalyco#8535 referenced but didn't include
1 parent feb0c78 commit 311970a

3 files changed

Lines changed: 59 additions & 6 deletions

File tree

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,13 +619,21 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
619619
)
620620
} catch (e) {
621621
const page = store.message_page[sessionID]
622+
const msg =
623+
e instanceof Error
624+
? e.message
625+
: typeof e === "object" && e !== null && "message" in e
626+
? String((e as Record<string, unknown>).message)
627+
: typeof e === "string"
628+
? e
629+
: "Unknown error"
622630
setStore("message_page", sessionID, {
623631
hasOlder: page?.hasOlder ?? false,
624632
hasNewer: page?.hasNewer ?? false,
625633
loading: false,
626634
oldest: page?.oldest,
627635
newest: page?.newest,
628-
error: e instanceof Error ? e.message : String(e),
636+
error: msg,
629637
})
630638
} finally {
631639
loadingGuard.delete(sessionID)
@@ -688,13 +696,21 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
688696
)
689697
} catch (e) {
690698
const page = store.message_page[sessionID]
699+
const msg =
700+
e instanceof Error
701+
? e.message
702+
: typeof e === "object" && e !== null && "message" in e
703+
? String((e as Record<string, unknown>).message)
704+
: typeof e === "string"
705+
? e
706+
: "Unknown error"
691707
setStore("message_page", sessionID, {
692708
hasOlder: page?.hasOlder ?? false,
693709
hasNewer: page?.hasNewer ?? false,
694710
loading: false,
695711
oldest: page?.oldest,
696712
newest: page?.newest,
697-
error: e instanceof Error ? e.message : String(e),
713+
error: msg,
698714
})
699715
} finally {
700716
loadingGuard.delete(sessionID)

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ export function Session() {
199199
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
200200
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
201201
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
202+
const [nearTop, setNearTop] = createSignal(false)
203+
const [nearBottom, setNearBottom] = createSignal(false)
204+
const BOUNDARY_THRESHOLD = 20
202205

203206
const wide = createMemo(() => dimensions().width > 120)
204207
const sidebarVisible = createMemo(() => {
@@ -633,6 +636,7 @@ export function Session() {
633636
hidden: true,
634637
onSelect: (dialog) => {
635638
scroll.scrollBy(-scroll.height / 2)
639+
setTimeout(loadOlder, 0)
636640
dialog.clear()
637641
},
638642
},
@@ -644,6 +648,7 @@ export function Session() {
644648
hidden: true,
645649
onSelect: (dialog) => {
646650
scroll.scrollBy(scroll.height / 2)
651+
setTimeout(loadNewer, 0)
647652
dialog.clear()
648653
},
649654
},
@@ -655,6 +660,7 @@ export function Session() {
655660
disabled: true,
656661
onSelect: (dialog) => {
657662
scroll.scrollBy(-1)
663+
setTimeout(loadOlder, 0)
658664
dialog.clear()
659665
},
660666
},
@@ -666,6 +672,7 @@ export function Session() {
666672
disabled: true,
667673
onSelect: (dialog) => {
668674
scroll.scrollBy(1)
675+
setTimeout(loadNewer, 0)
669676
dialog.clear()
670677
},
671678
},
@@ -677,6 +684,7 @@ export function Session() {
677684
hidden: true,
678685
onSelect: (dialog) => {
679686
scroll.scrollBy(-scroll.height / 4)
687+
setTimeout(loadOlder, 0)
680688
dialog.clear()
681689
},
682690
},
@@ -688,6 +696,7 @@ export function Session() {
688696
hidden: true,
689697
onSelect: (dialog) => {
690698
scroll.scrollBy(scroll.height / 4)
699+
setTimeout(loadNewer, 0)
691700
dialog.clear()
692701
},
693702
},
@@ -1032,7 +1041,7 @@ export function Session() {
10321041
<text fg={theme.textMuted}>Loading older messages...</text>
10331042
</box>
10341043
</Show>
1035-
<Show when={!paging()?.loading && paging()?.hasOlder}>
1044+
<Show when={!paging()?.loading && paging()?.hasOlder && nearTop()}>
10361045
<box flexShrink={0} paddingLeft={1}>
10371046
<text fg={theme.textMuted}>(scroll up for more)</text>
10381047
</box>
@@ -1046,16 +1055,24 @@ export function Session() {
10461055
<scrollbox
10471056
ref={(r) => (scroll = r)}
10481057
onMouseScroll={() => {
1058+
setNearTop(scroll.scrollTop <= BOUNDARY_THRESHOLD)
1059+
setNearBottom(scroll.scrollHeight - scroll.scrollTop - scroll.viewport.height <= BOUNDARY_THRESHOLD)
10491060
loadOlder()
10501061
loadNewer()
10511062
}}
10521063
onKeyDown={(e) => {
10531064
// Standard scroll triggers incremental load
10541065
if (["up", "pageup", "home"].includes(e.name)) {
1055-
setTimeout(loadOlder, 0)
1066+
setTimeout(() => {
1067+
setNearTop(scroll.scrollTop <= BOUNDARY_THRESHOLD)
1068+
loadOlder()
1069+
}, 0)
10561070
}
10571071
if (["down", "pagedown", "end"].includes(e.name)) {
1058-
setTimeout(loadNewer, 0)
1072+
setTimeout(() => {
1073+
setNearBottom(scroll.scrollHeight - scroll.scrollTop - scroll.viewport.height <= BOUNDARY_THRESHOLD)
1074+
loadNewer()
1075+
}, 0)
10591076
}
10601077
}}
10611078
viewportCulling={true}
@@ -1176,7 +1193,7 @@ export function Session() {
11761193
<text fg={theme.textMuted}>Loading newer messages...</text>
11771194
</box>
11781195
</Show>
1179-
<Show when={!paging()?.loading && paging()?.hasNewer}>
1196+
<Show when={!paging()?.loading && paging()?.hasNewer && nearBottom()}>
11801197
<box flexShrink={0} paddingLeft={1}>
11811198
<text fg={theme.textMuted}>(scroll down for more)</text>
11821199
</box>

packages/util/src/binary.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ export namespace Binary {
1919
return { found: false, index: left }
2020
}
2121

22+
/**
23+
* Find the index of the first element >= id (lower bound).
24+
* Returns array.length if all elements are less than id.
25+
*/
26+
export function lowerBound(array: string[], id: string): number {
27+
let left = 0
28+
let right = array.length
29+
30+
while (left < right) {
31+
const mid = Math.floor((left + right) / 2)
32+
if (array[mid] < id) {
33+
left = mid + 1
34+
} else {
35+
right = mid
36+
}
37+
}
38+
39+
return left
40+
}
41+
2242
export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
2343
const id = compare(item)
2444
let left = 0

0 commit comments

Comments
 (0)