tui: preserve transcript window while scrolling (#44679)
This commit is contained in:
@@ -282,11 +282,13 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
const sessionTabs = useSessionTabs()
|
||||
const [awayFromBottom, setAwayFromBottom] = createSignal(false)
|
||||
const [latestHovered, setLatestHovered] = createSignal(false)
|
||||
let ensureAllRowsPending: (() => void)[] | undefined
|
||||
createEffect(() => {
|
||||
if (!awayFromBottom()) setLatestHovered(false)
|
||||
})
|
||||
|
||||
const clearMessageNavigation = () => {
|
||||
ensureAllRowsPending?.splice(0)
|
||||
setNavigationSlack(0)
|
||||
setNavigationMessage(undefined)
|
||||
}
|
||||
@@ -295,7 +297,9 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
on(
|
||||
() => [dimensions().width, dimensions().height, props.verticalTabsWidth] as const,
|
||||
(_, previous) => {
|
||||
if (previous) clearMessageNavigation()
|
||||
if (!previous) return
|
||||
clearMessageNavigation()
|
||||
if (scroll && !scroll.isDestroyed) updateAwayFromBottom()
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -355,6 +359,7 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
onCleanup(() => {
|
||||
if (awayTimer) clearTimeout(awayTimer)
|
||||
if (!scroll || scroll.isDestroyed) return
|
||||
scroll.verticalScrollBar.off("change", updateAwayFromBottom)
|
||||
saveScrollAnchor()
|
||||
})
|
||||
const [prompt, setPrompt] = createSignal<PromptRef>()
|
||||
@@ -377,8 +382,8 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
}
|
||||
|
||||
// Tail-first transcript mounting: only the newest rows mount when the session opens. Older rows
|
||||
// mount on demand near the top, keeping inactive tabs cheap to tear down. Until the first chunk
|
||||
// pins the count, the hidden span derives from the row count, so streaming appends remain visible.
|
||||
// mount on demand near the top, keeping inactive tabs cheap to tear down. While the reader stays
|
||||
// at the bottom the hidden span follows appends; leaving the bottom pins it to preserve the viewport.
|
||||
const [hiddenRows, setHiddenRows] = createSignal<number>()
|
||||
const [visibleRowsEnd, setVisibleRowsEnd] = createSignal<number>()
|
||||
const hidden = createMemo(() => Math.max(0, Math.min(hiddenRows() ?? Infinity, rows.length - TRANSCRIPT_TAIL_ROWS)))
|
||||
@@ -403,11 +408,12 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
if (current === 0) return prependHistory(scrollBy)
|
||||
revealingOlderRows = true
|
||||
const before = scroll.scrollHeight
|
||||
scroll.stickyScroll = false
|
||||
setHiddenRows(Math.max(0, current - TRANSCRIPT_BACKFILL_CHUNK))
|
||||
afterLayout(() => {
|
||||
revealingOlderRows = false
|
||||
scroll.scrollBy(scroll.scrollHeight - before + scrollBy)
|
||||
updateAwayFromBottom()
|
||||
scroll.stickyScroll = !navigationMessage()
|
||||
revealingOlderRows = false
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -434,23 +440,40 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
}
|
||||
/** Message navigation needs the full transcript mounted before walking or jumping. */
|
||||
const ensureAllRows = (continuation: () => void) => {
|
||||
if (hidden() === 0 && visibleEnd() === rows.length) return continuation()
|
||||
if (!ensureAllRowsPending && hidden() === 0 && visibleEnd() === rows.length) return continuation()
|
||||
if (ensureAllRowsPending) {
|
||||
ensureAllRowsPending.push(continuation)
|
||||
return
|
||||
}
|
||||
const pending = [continuation]
|
||||
ensureAllRowsPending = pending
|
||||
setHiddenRows(0)
|
||||
setVisibleRowsEnd(undefined)
|
||||
afterLayout(continuation)
|
||||
afterLayout(() => {
|
||||
if (ensureAllRowsPending === pending) ensureAllRowsPending = undefined
|
||||
pending.forEach((continuation) => continuation())
|
||||
updateAwayFromBottom()
|
||||
})
|
||||
}
|
||||
|
||||
function isAwayFromBottom() {
|
||||
if (revealingOlderRows || revealingNewerRows || ensureAllRowsPending || navigationMessage()) return true
|
||||
if (visibleEnd() < rows.length) return true
|
||||
return scroll.scrollTop < Math.max(0, scroll.scrollHeight - scroll.viewport.height) - 1
|
||||
}
|
||||
function updateAwayFromBottom() {
|
||||
const preserveWindow = revealingOlderRows || revealingNewerRows || !!ensureAllRowsPending
|
||||
if (isAwayFromBottom()) setHiddenRows((current) => current ?? hidden())
|
||||
if (awayTimer) clearTimeout(awayTimer)
|
||||
awayTimer = setTimeout(() => {
|
||||
awayTimer = undefined
|
||||
if (!scroll || scroll.isDestroyed) return
|
||||
const away = isAwayFromBottom()
|
||||
const away = preserveWindow || isAwayFromBottom()
|
||||
setAwayFromBottom(away)
|
||||
if (!away) {
|
||||
if (!renderer.getSelection()) setHiddenRows(undefined)
|
||||
scroll.stickyScroll = true
|
||||
}
|
||||
saveScrollAnchor()
|
||||
})
|
||||
}
|
||||
@@ -575,6 +598,7 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
const alignMessage = (messageID: string, top: number) => {
|
||||
scroll.stickyScroll = false
|
||||
setNavigationMessage(messageID)
|
||||
updateAwayFromBottom()
|
||||
setNavigationSlack(
|
||||
messageNavigationSlack({
|
||||
top,
|
||||
@@ -624,6 +648,7 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
|
||||
function toBottom() {
|
||||
clearMessageNavigation()
|
||||
ensureAllRowsPending = undefined
|
||||
if (awayTimer) clearTimeout(awayTimer)
|
||||
awayTimer = undefined
|
||||
setAwayFromBottom(false)
|
||||
@@ -712,7 +737,6 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
}
|
||||
ensureAllRows(() => {
|
||||
scroll.scrollTo(0)
|
||||
updateAwayFromBottom()
|
||||
})
|
||||
}
|
||||
first()
|
||||
@@ -1193,7 +1217,10 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
<Show when={session()}>
|
||||
<box flexGrow={1} minHeight={0} position="relative">
|
||||
<scrollbox
|
||||
ref={(r) => (scroll = r)}
|
||||
ref={(r) => {
|
||||
scroll = r
|
||||
scroll.verticalScrollBar.on("change", updateAwayFromBottom)
|
||||
}}
|
||||
viewportOptions={{
|
||||
paddingRight: showScrollbar() ? 1 : 0,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user