buddy scroll
This commit is contained in:
@@ -350,6 +350,7 @@ export function AssistantPanel({
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const threadRef = useRef<HTMLDivElement>(null);
|
||||
const { width, setWidth, reset } = useAssistantWidth();
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
|
||||
@@ -405,6 +406,30 @@ export function AssistantPanel({
|
||||
setThread([]);
|
||||
}, [key]);
|
||||
|
||||
/*
|
||||
* Follow the conversation down, unless the person has scrolled away from it.
|
||||
*
|
||||
* A chat that does not follow leaves the newest answer below the fold, which
|
||||
* reads as nothing having happened. One that follows unconditionally yanks
|
||||
* somebody out of an earlier answer they were still reading the moment a
|
||||
* reply lands — and a reply can land a while after the question, because a
|
||||
* tool call and a model round trip are seconds, not milliseconds.
|
||||
*
|
||||
* So: only when they were already at the bottom. The 40px allowance covers
|
||||
* fractional scroll heights, which browsers disagree about by a pixel or two
|
||||
* at non-integer zoom levels — without it this silently stops following for
|
||||
* anybody not at 100%.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const el = threadRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
if (distanceFromBottom > 40) return;
|
||||
|
||||
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
||||
}, [thread]);
|
||||
|
||||
/*
|
||||
* Approving is its own call, with no question in it.
|
||||
*
|
||||
@@ -569,6 +594,35 @@ export function AssistantPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/*
|
||||
The scrolling part, and the only one.
|
||||
|
||||
Everything above this is chrome that stays put; everything below it is
|
||||
the composer, which must never leave the screen. Before this existed
|
||||
the panel was one `height: 100%` column with no overflow anywhere: a
|
||||
thread longer than the panel pushed the chips and the composer past
|
||||
the bottom edge, and there was nothing to scroll — the box you type
|
||||
into simply left the screen and the conversation could not be read
|
||||
back.
|
||||
|
||||
`minHeight: 0` is the load-bearing half. A flex child defaults to
|
||||
`min-height: auto`, which means it refuses to shrink below its content,
|
||||
so `flex: 1` alone would let this grow to the height of the whole
|
||||
conversation and overflow the panel exactly as before — the scrollbar
|
||||
never appears and nothing looks wrong in the code.
|
||||
*/}
|
||||
<div
|
||||
ref={threadRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
overscrollBehavior: 'contain',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{/* Greeting — top-aligned, because it is the first thing in a
|
||||
conversation, not a splash screen. */}
|
||||
<div>
|
||||
@@ -608,9 +662,9 @@ export function AssistantPanel({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* The conversation. Above the spacer, so a short thread sits under the
|
||||
greeting and a long one pushes the chips and composer down rather
|
||||
than scrolling away from them. */}
|
||||
{/* The conversation. Inside the scroller with the greeting above it, so
|
||||
a short thread reads straight on from the greeting and a long one
|
||||
scrolls under it — with the chips and the composer staying put. */}
|
||||
{thread.length > 0 ? (
|
||||
<div
|
||||
role="log"
|
||||
@@ -629,8 +683,7 @@ export function AssistantPanel({
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
</div>
|
||||
|
||||
{/* Chips wrap, never scroll — a half-visible button at a scroller's edge
|
||||
is a bug no amount of fade masking fixes. */}
|
||||
|
||||
Reference in New Issue
Block a user