Merge pull request 'Assistant: scroll the thread to a new reply for real' (#134) from fix/assistant-thread-scroll into main
Build image / build-and-push (push) Successful in 13s
Build image / build-and-push (push) Successful in 13s
This commit was merged in pull request #134.
This commit is contained in:
@@ -36,18 +36,35 @@ export function AssistantTab({ gardenId, canEdit, undo, large = false }: { garde
|
|||||||
const [warning, setWarning] = useState<string | null>(null)
|
const [warning, setWarning] = useState<string | null>(null)
|
||||||
const abort = useRef<AbortController | null>(null)
|
const abort = useRef<AbortController | null>(null)
|
||||||
const bottom = useRef<HTMLDivElement>(null)
|
const bottom = useRef<HTMLDivElement>(null)
|
||||||
|
const thread = useRef<HTMLDivElement>(null)
|
||||||
|
// Whether the view is pinned to the end of the thread. It follows new
|
||||||
|
// content only while it is; a person who scrolled up to read something is
|
||||||
|
// left there until they come back down or send the next message.
|
||||||
|
const stuck = useRef(true)
|
||||||
|
|
||||||
// Deliberately NOT aborted on unmount: selecting a bed switches the rail to
|
// Deliberately NOT aborted on unmount: selecting a bed switches the rail to
|
||||||
// the inspector, and that must not kill a turn mid-flight. The request runs
|
// the inspector, and that must not kill a turn mid-flight. The request runs
|
||||||
// on; the exchange is persisted server-side; coming back shows it.
|
// on; the exchange is persisted server-side; coming back shows it.
|
||||||
|
// Instant, not smooth: Chrome left the thread at the top with
|
||||||
|
// `behavior: 'smooth'` — a smooth scrollIntoView into this nested scroller
|
||||||
|
// never moved it, so every new reply landed out of view below a long
|
||||||
|
// conversation (found live, 2026-08-23). The instant form scrolls. It runs
|
||||||
|
// on every step of a turn too, so it is gated on `stuck`: following the
|
||||||
|
// stream is right when the person is at the end, and a snap they didn't
|
||||||
|
// ask for when they had scrolled up.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bottom.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
|
if (stuck.current) bottom.current?.scrollIntoView({ block: 'end' })
|
||||||
}, [history.data, pending])
|
}, [history.data, pending])
|
||||||
|
const onThreadScroll = () => {
|
||||||
|
const el = thread.current
|
||||||
|
if (el) stuck.current = el.scrollHeight - el.clientHeight - el.scrollTop < 80
|
||||||
|
}
|
||||||
|
|
||||||
const send = () => {
|
const send = () => {
|
||||||
const message = input.trim()
|
const message = input.trim()
|
||||||
if (!message || pending) return
|
if (!message || pending) return
|
||||||
setInput('')
|
setInput('')
|
||||||
|
stuck.current = true // sending is a return to the end of the thread
|
||||||
setError(null)
|
setError(null)
|
||||||
setWarning(null)
|
setWarning(null)
|
||||||
setPending({ message, steps: [] })
|
setPending({ message, steps: [] })
|
||||||
@@ -91,7 +108,7 @@ export function AssistantTab({ gardenId, canEdit, undo, large = false }: { garde
|
|||||||
{/* The thread scrolls on its own so the composer stays put: with the whole
|
{/* The thread scrolls on its own so the composer stays put: with the whole
|
||||||
tab scrolling, a long conversation pushed the input off the bottom and
|
tab scrolling, a long conversation pushed the input off the bottom and
|
||||||
every new message scrolled it further away. */}
|
every new message scrolled it further away. */}
|
||||||
<div className="flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto">
|
<div ref={thread} onScroll={onThreadScroll} className="flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto">
|
||||||
{!canEdit && <Alert tone="info">You can only view this garden, so the assistant can't change anything in it.</Alert>}
|
{!canEdit && <Alert tone="info">You can only view this garden, so the assistant can't change anything in it.</Alert>}
|
||||||
{history.isPending && <p className="text-[13px] text-ink-mute">Loading the conversation…</p>}
|
{history.isPending && <p className="text-[13px] text-ink-mute">Loading the conversation…</p>}
|
||||||
{history.isError && <Alert>{errorMessage(history.error, "Couldn't load the conversation.")}</Alert>}
|
{history.isError && <Alert>{errorMessage(history.error, "Couldn't load the conversation.")}</Alert>}
|
||||||
|
|||||||
Reference in New Issue
Block a user