Skip to content

Commit 2ee3e62

Browse files
committed
feat: add auto-load, viewport tracking, and lifecycle management
Phase 4-6: Complete auto-scroll implementation - Add KV signal for runtime toggle (persists across sessions) - Add historyConfig memo with default values - Implement loadOlder() function: * Checks both KV toggle and config.enabled * Triggers when scroll position <= load_threshold pixels from top * Uses existing loadConversationHistory() with ts_before API - Implement updateVisibleMessageViews() for batch tracking: * Updates lastViewed timestamp for all visible messages * Called on scroll events for efficient viewport detection - Add scroll event handlers: * onMouseScroll: triggers auto-load and view tracking * onKeyDown: triggers auto-load on up/pageup/home keys - Add command menu toggle option: * Dynamic title based on current state * Starts/stops cleanup worker on toggle * Uses KV system as designed - Add lifecycle management: * createEffect monitors config and KV toggle * Starts worker when both enabled and session active * onCleanup ensures worker stops on unmount - Import onCleanup from solid-js Complete feature implementation: ~190 lines vs PR anomalyco#8535's ~400 lines
1 parent 6afdeb1 commit 2ee3e62

File tree

1 file changed

+90
-0
lines changed
  • packages/opencode/src/cli/cmd/tui/routes/session

1 file changed

+90
-0
lines changed

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
For,
88
Match,
99
on,
10+
onCleanup,
1011
Show,
1112
Switch,
1213
useContext,
@@ -163,6 +164,20 @@ export function Session() {
163164
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
164165
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
165166
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
167+
const [historyAutoScroll, setHistoryAutoScroll] = kv.signal("history_auto_scroll", true)
168+
169+
const historyConfig = createMemo(() => {
170+
return (
171+
sync.data.config.tui?.history_auto_scroll ?? {
172+
enabled: false,
173+
message_threshold: 100,
174+
min_ttl: 120,
175+
purge_delay: 0.1,
176+
cleanup_interval: 30,
177+
load_threshold: 5,
178+
}
179+
)
180+
})
166181

167182
const wide = createMemo(() => dimensions().width > 120)
168183
const sidebarVisible = createMemo(() => {
@@ -212,6 +227,22 @@ export function Session() {
212227
}
213228
})
214229

230+
// Manage cleanup worker lifecycle
231+
createEffect(() => {
232+
const config = historyConfig()
233+
const sessionID = route.sessionID
234+
235+
if (config.enabled && historyAutoScroll() && sessionID) {
236+
sync.startCleanupWorker()
237+
} else {
238+
sync.stopCleanupWorker()
239+
}
240+
241+
onCleanup(() => {
242+
sync.stopCleanupWorker()
243+
})
244+
})
245+
215246
let lastSwitch: string | undefined = undefined
216247
sdk.event.on("message.part.updated", (evt) => {
217248
const part = evt.properties.part
@@ -233,6 +264,38 @@ export function Session() {
233264
let prompt: PromptRef
234265
const keybind = useKeybind()
235266

267+
// Auto-load older messages when scrolling near top
268+
const loadOlder = () => {
269+
const config = historyConfig()
270+
271+
if (!historyAutoScroll() || !config.enabled) return
272+
273+
if (!scroll || scroll.y > config.load_threshold) return
274+
275+
const msgs = messages()
276+
if (msgs.length < config.message_threshold) return
277+
278+
const oldest = msgs[0]
279+
if (!oldest) return
280+
281+
sync.session.loadConversationHistory(route.sessionID)
282+
}
283+
284+
// Batch update visible message views
285+
const updateVisibleMessageViews = () => {
286+
const config = historyConfig()
287+
if (!config.enabled || !historyAutoScroll()) return
288+
289+
const children = scroll?.getChildren() || []
290+
const now = Date.now()
291+
292+
children.forEach((child) => {
293+
if (child.id && child.id !== "__load_more__") {
294+
sync.updateMessageView(route.sessionID, child.id, now)
295+
}
296+
})
297+
}
298+
236299
// Allow exit when in child session (prompt is hidden)
237300
const exit = useExit()
238301
useKeyboard((evt) => {
@@ -563,6 +626,24 @@ export function Session() {
563626
dialog.clear()
564627
},
565628
},
629+
{
630+
title: historyAutoScroll() ? "Disable history auto-scroll" : "Enable history auto-scroll",
631+
value: "session.toggle.history_auto_scroll",
632+
keybind: "history_auto_scroll_toggle",
633+
category: "Session",
634+
onSelect: (dialog) => {
635+
setHistoryAutoScroll((prev) => !prev)
636+
637+
const config = historyConfig()
638+
if (historyAutoScroll() && config.enabled) {
639+
sync.startCleanupWorker()
640+
} else {
641+
sync.stopCleanupWorker()
642+
}
643+
644+
dialog.clear()
645+
},
646+
},
566647
{
567648
title: "Page up",
568649
value: "session.page.up",
@@ -941,6 +1022,15 @@ export function Session() {
9411022
stickyStart="bottom"
9421023
flexGrow={1}
9431024
scrollAcceleration={scrollAcceleration()}
1025+
onMouseScroll={() => {
1026+
loadOlder()
1027+
updateVisibleMessageViews()
1028+
}}
1029+
onKeyDown={(e) => {
1030+
if (["up", "pageup", "home"].includes(e.name)) {
1031+
setTimeout(loadOlder, 0)
1032+
}
1033+
}}
9441034
>
9451035
<For each={messagesDisplay()}>
9461036
{(message, index) => (

0 commit comments

Comments
 (0)