buddy scroll
This commit is contained in:
@@ -350,6 +350,7 @@ export function AssistantPanel({
|
|||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const threadRef = useRef<HTMLDivElement>(null);
|
||||||
const { width, setWidth, reset } = useAssistantWidth();
|
const { width, setWidth, reset } = useAssistantWidth();
|
||||||
const [isDragging, setDragging] = useState(false);
|
const [isDragging, setDragging] = useState(false);
|
||||||
|
|
||||||
@@ -405,6 +406,30 @@ export function AssistantPanel({
|
|||||||
setThread([]);
|
setThread([]);
|
||||||
}, [key]);
|
}, [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.
|
* Approving is its own call, with no question in it.
|
||||||
*
|
*
|
||||||
@@ -569,6 +594,35 @@ export function AssistantPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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
|
{/* Greeting — top-aligned, because it is the first thing in a
|
||||||
conversation, not a splash screen. */}
|
conversation, not a splash screen. */}
|
||||||
<div>
|
<div>
|
||||||
@@ -608,9 +662,9 @@ export function AssistantPanel({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The conversation. Above the spacer, so a short thread sits under the
|
{/* The conversation. Inside the scroller with the greeting above it, so
|
||||||
greeting and a long one pushes the chips and composer down rather
|
a short thread reads straight on from the greeting and a long one
|
||||||
than scrolling away from them. */}
|
scrolls under it — with the chips and the composer staying put. */}
|
||||||
{thread.length > 0 ? (
|
{thread.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
role="log"
|
role="log"
|
||||||
@@ -629,8 +683,7 @@ export function AssistantPanel({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
|
|
||||||
{/* Chips wrap, never scroll — a half-visible button at a scroller's edge
|
{/* Chips wrap, never scroll — a half-visible button at a scroller's edge
|
||||||
is a bug no amount of fade masking fixes. */}
|
is a bug no amount of fade masking fixes. */}
|
||||||
|
|||||||
Reference in New Issue
Block a user