Improve sidebar default width and message autoscroll
This commit is contained in:
@@ -527,6 +527,22 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
||||||
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
let lastScrollHeight = 0
|
||||||
|
let autoScrollLocked = true
|
||||||
|
|
||||||
|
function setAutoScrollState(enabled: boolean) {
|
||||||
|
autoScrollLocked = enabled
|
||||||
|
setAutoScroll(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockAutoScroll() {
|
||||||
|
setAutoScrollState(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockAutoScroll() {
|
||||||
|
setAutoScrollState(false)
|
||||||
|
}
|
||||||
|
|
||||||
function isNearBottom(element: HTMLDivElement, offset = 48) {
|
function isNearBottom(element: HTMLDivElement, offset = 48) {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = element
|
const { scrollTop, scrollHeight, clientHeight } = element
|
||||||
@@ -542,12 +558,90 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
setShowScrollBottomButton(hasItems && !isNearBottom(element))
|
setShowScrollBottomButton(hasItems && !isNearBottom(element))
|
||||||
setShowScrollTopButton(hasItems && !isNearTop(element))
|
setShowScrollTopButton(hasItems && !isNearTop(element))
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom(immediate = false) {
|
function detachResizeObserver() {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeEvent() {
|
||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
|
const currentHeight = containerRef.scrollHeight
|
||||||
|
const heightDecreased = currentHeight < lastScrollHeight
|
||||||
|
lastScrollHeight = currentHeight
|
||||||
|
if (heightDecreased && shouldMaintainAutoScroll()) {
|
||||||
|
containerRef.scrollTop = Math.max(currentHeight - containerRef.clientHeight, 0)
|
||||||
|
lockAutoScroll()
|
||||||
|
queueAutoScroll(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (shouldMaintainAutoScroll()) {
|
||||||
|
queueAutoScroll(true)
|
||||||
|
} else {
|
||||||
|
updateScrollIndicators(containerRef)
|
||||||
|
scheduleScrollPersist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachResizeObserver(element: HTMLDivElement | undefined) {
|
||||||
|
detachResizeObserver()
|
||||||
|
if (!element) return
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
handleResizeEvent()
|
||||||
|
})
|
||||||
|
resizeObserver.observe(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setContainerRef(element: HTMLDivElement | null) {
|
||||||
|
containerRef = element || undefined
|
||||||
|
if (containerRef) {
|
||||||
|
lastScrollHeight = containerRef.scrollHeight
|
||||||
|
}
|
||||||
|
attachResizeObserver(containerRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldMaintainAutoScroll() {
|
||||||
|
return autoScrollLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applyScrollToBottom(immediate: boolean, options?: { preserveAuto?: boolean }) {
|
||||||
|
if (!containerRef) return
|
||||||
|
const preserveAuto = options?.preserveAuto ?? false
|
||||||
|
if (preserveAuto && !shouldMaintainAutoScroll()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!preserveAuto) {
|
||||||
|
lockAutoScroll()
|
||||||
|
}
|
||||||
const behavior = immediate ? "auto" : "smooth"
|
const behavior = immediate ? "auto" : "smooth"
|
||||||
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior })
|
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior })
|
||||||
setAutoScroll(true)
|
requestAnimationFrame(() => {
|
||||||
|
if (!containerRef) return
|
||||||
|
if (preserveAuto && !shouldMaintainAutoScroll()) {
|
||||||
|
updateScrollIndicators(containerRef)
|
||||||
|
scheduleScrollPersist()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isNearBottom(containerRef)) {
|
||||||
|
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior: "auto" })
|
||||||
|
}
|
||||||
|
updateScrollIndicators(containerRef)
|
||||||
|
scheduleScrollPersist()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom(immediate = false) {
|
||||||
|
applyScrollToBottom(immediate, { preserveAuto: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToTop(immediate = false) {
|
||||||
|
if (!containerRef) return
|
||||||
|
const behavior = immediate ? "auto" : "smooth"
|
||||||
|
unlockAutoScroll()
|
||||||
|
containerRef.scrollTo({ top: 0, behavior })
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
updateScrollIndicators(containerRef)
|
updateScrollIndicators(containerRef)
|
||||||
@@ -555,28 +649,37 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToTop(immediate = false) {
|
let pendingAutoScrollId: number | null = null
|
||||||
|
|
||||||
if (!containerRef) return
|
function cancelPendingAutoScroll() {
|
||||||
const behavior = immediate ? "auto" : "smooth"
|
if (pendingAutoScrollId !== null) {
|
||||||
setAutoScroll(false)
|
cancelAnimationFrame(pendingAutoScrollId)
|
||||||
containerRef.scrollTo({ top: 0, behavior })
|
pendingAutoScrollId = null
|
||||||
requestAnimationFrame(() => {
|
}
|
||||||
if (!containerRef) return
|
}
|
||||||
updateScrollIndicators(containerRef)
|
|
||||||
scheduleScrollPersist()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let pendingScrollPersist: number | null = null
|
function queueAutoScroll(immediate = true) {
|
||||||
function scheduleScrollPersist() {
|
cancelPendingAutoScroll()
|
||||||
if (pendingScrollPersist !== null) return
|
if (!shouldMaintainAutoScroll()) {
|
||||||
pendingScrollPersist = requestAnimationFrame(() => {
|
return
|
||||||
pendingScrollPersist = null
|
}
|
||||||
if (!containerRef) return
|
pendingAutoScrollId = requestAnimationFrame(() => {
|
||||||
scrollCache.persist(containerRef, { atBottomOffset: 48 })
|
pendingAutoScrollId = null
|
||||||
})
|
applyScrollToBottom(immediate, { preserveAuto: true })
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingScrollPersist: number | null = null
|
||||||
|
function scheduleScrollPersist() {
|
||||||
|
if (pendingScrollPersist !== null) return
|
||||||
|
pendingScrollPersist = requestAnimationFrame(() => {
|
||||||
|
pendingScrollPersist = null
|
||||||
|
if (!containerRef) return
|
||||||
|
lastScrollHeight = containerRef.scrollHeight
|
||||||
|
scrollCache.persist(containerRef, { atBottomOffset: 48 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleScroll(event: Event) {
|
function handleScroll(event: Event) {
|
||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
@@ -584,9 +687,9 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
if (event.isTrusted) {
|
if (event.isTrusted) {
|
||||||
const atBottom = isNearBottom(containerRef)
|
const atBottom = isNearBottom(containerRef)
|
||||||
if (!atBottom) {
|
if (!atBottom) {
|
||||||
setAutoScroll(false)
|
unlockAutoScroll()
|
||||||
} else {
|
} else {
|
||||||
setAutoScroll(true)
|
lockAutoScroll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scheduleScrollPersist()
|
scheduleScrollPersist()
|
||||||
@@ -599,10 +702,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
fallback: () => scrollToBottom(true),
|
fallback: () => scrollToBottom(true),
|
||||||
onApplied: (snapshot) => {
|
onApplied: (snapshot) => {
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
setAutoScroll(snapshot.atBottom)
|
setAutoScrollState(snapshot.atBottom)
|
||||||
} else {
|
} else {
|
||||||
const atBottom = isNearBottom(target)
|
const atBottom = isNearBottom(target)
|
||||||
setAutoScroll(atBottom)
|
setAutoScrollState(atBottom)
|
||||||
}
|
}
|
||||||
updateScrollIndicators(target)
|
updateScrollIndicators(target)
|
||||||
},
|
},
|
||||||
@@ -619,7 +722,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
}
|
}
|
||||||
previousToken = token
|
previousToken = token
|
||||||
if (autoScroll()) {
|
if (autoScroll()) {
|
||||||
requestAnimationFrame(() => scrollToBottom(true))
|
queueAutoScroll(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -627,11 +730,14 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
if (messageRecords().length === 0) {
|
if (messageRecords().length === 0) {
|
||||||
setShowScrollTopButton(false)
|
setShowScrollTopButton(false)
|
||||||
setShowScrollBottomButton(false)
|
setShowScrollBottomButton(false)
|
||||||
setAutoScroll(true)
|
lockAutoScroll()
|
||||||
|
cancelPendingAutoScroll()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
detachResizeObserver()
|
||||||
|
cancelPendingAutoScroll()
|
||||||
if (pendingScrollPersist !== null) {
|
if (pendingScrollPersist !== null) {
|
||||||
cancelAnimationFrame(pendingScrollPersist)
|
cancelAnimationFrame(pendingScrollPersist)
|
||||||
pendingScrollPersist = null
|
pendingScrollPersist = null
|
||||||
@@ -641,6 +747,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="message-stream-container">
|
<div class="message-stream-container">
|
||||||
<div class="connection-status">
|
<div class="connection-status">
|
||||||
@@ -691,9 +798,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="message-stream"
|
class="message-stream"
|
||||||
ref={(element) => {
|
ref={setContainerRef}
|
||||||
containerRef = element || undefined
|
|
||||||
}}
|
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
<Show when={!props.loading && displayBlocks().length === 0}>
|
<Show when={!props.loading && displayBlocks().length === 0}>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface SessionListProps {
|
|||||||
|
|
||||||
const MIN_WIDTH = 200
|
const MIN_WIDTH = 200
|
||||||
const MAX_WIDTH = 520
|
const MAX_WIDTH = 520
|
||||||
const DEFAULT_WIDTH = 350
|
const DEFAULT_WIDTH = 360
|
||||||
const STORAGE_KEY = "opencode-session-sidebar-width-v7"
|
const STORAGE_KEY = "opencode-session-sidebar-width-v7"
|
||||||
|
|
||||||
function formatSessionStatus(status: SessionStatus): string {
|
function formatSessionStatus(status: SessionStatus): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user