Assistant: scroll the thread to a new reply for real #134
@@ -36,18 +36,35 @@ export function AssistantTab({ gardenId, canEdit, undo, large = false }: { garde
|
||||
const [warning, setWarning] = useState<string | null>(null)
|
||||
const abort = useRef<AbortController | null>(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
|
||||
// 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.
|
||||
// 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(() => {
|
||||
bottom.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
|
||||
if (stuck.current) bottom.current?.scrollIntoView({ block: 'end' })
|
||||
}, [history.data, pending])
|
||||
const onThreadScroll = () => {
|
||||
const el = thread.current
|
||||
if (el) stuck.current = el.scrollHeight - el.clientHeight - el.scrollTop < 80
|
||||
}
|
||||
|
||||
const send = () => {
|
||||
const message = input.trim()
|
||||
if (!message || pending) return
|
||||
setInput('')
|
||||
stuck.current = true // sending is a return to the end of the thread
|
||||
setError(null)
|
||||
setWarning(null)
|
||||
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
|
||||
tab scrolling, a long conversation pushed the input off the bottom and
|
||||
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>}
|
||||
{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>}
|
||||
|
||||
Reference in New Issue
Block a user