mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
fix(ui): reasoning blocks respect scroll position and expanded state (#8642)
Guard streaming auto-scroll behind a near-bottom check so the user can scroll up without being snapped back on the next token. Track explicit user opens in a module-level Set so auto-collapse is skipped when the user has the block open. Closes #8586 Co-authored-by: Thomas Brugman <thomas.brugman.teb3@gmail.com>
This commit is contained in:
@@ -1254,6 +1254,9 @@ const streamed = new Set<string>()
|
||||
// Tracks parts that have already been auto-collapsed once, so component
|
||||
// recreation (from store updates while other parts stream) won't collapse again.
|
||||
const autocollapsed = new Set<string>()
|
||||
// Tracks parts that the user has explicitly opened, so auto-collapse won't
|
||||
// override the user's intent when reasoning finishes or a tool call starts.
|
||||
const userOpened = new Set<string>()
|
||||
|
||||
// Overrides upstream flat markdown render with streaming reasoning block + auto-collapse.
|
||||
// Also filters encrypted reasoning data from OpenRouter that appears as [REDACTED].
|
||||
@@ -1283,14 +1286,24 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
|
||||
|
||||
// Streaming → open. Just finished (was streaming, now done) → open briefly
|
||||
// then collapse. Historical → collapsed from the start.
|
||||
const [open, setOpen] = createSignal(!done() || was)
|
||||
// Restore user's explicit open preference across component recreations.
|
||||
const [open, setOpen] = createSignal(!done() || was || userOpened.has(id))
|
||||
|
||||
// Propagate user intent to the module-level set so it survives component
|
||||
// recreations (e.g. when a tool call arrives while reading reasoning).
|
||||
const track = (value: boolean) => {
|
||||
if (value) userOpened.add(id)
|
||||
else userOpened.delete(id)
|
||||
setOpen(value)
|
||||
}
|
||||
|
||||
// Auto-collapse once when reasoning finishes (streaming → done transition).
|
||||
// Collapses immediately so the grid transition runs in sync with the
|
||||
// streaming-height removal. Module-level Set prevents re-triggering on
|
||||
// component recreation or when the user manually reopens.
|
||||
// component recreation. Skipped entirely if the user has explicitly opened
|
||||
// the block, so reading is not interrupted by a subsequent tool call.
|
||||
createEffect(() => {
|
||||
if (done() && open() && !autocollapsed.has(id)) {
|
||||
if (done() && open() && !autocollapsed.has(id) && !userOpened.has(id)) {
|
||||
autocollapsed.add(id)
|
||||
setOpen(false)
|
||||
}
|
||||
@@ -1298,13 +1311,32 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
|
||||
|
||||
onCleanup(() => {
|
||||
if (done()) streamed.delete(id)
|
||||
// userOpened is intentionally NOT deleted here. The component recreates
|
||||
// frequently while other parts stream (same as autocollapsed), so removing
|
||||
// the entry on unmount would discard the user's explicit preference and
|
||||
// re-collapse the block on the next remount.
|
||||
})
|
||||
|
||||
// Auto-scroll the content container while streaming
|
||||
// Auto-scroll the content container while streaming.
|
||||
// Use a plain mutable flag rather than checking dist inside the reactive
|
||||
// effect: by the time the effect runs the DOM has already grown, so reading
|
||||
// scrollHeight post-update incorrectly reports the user as scrolled away
|
||||
// whenever a streaming chunk is > 10px tall.
|
||||
let ref: HTMLDivElement | undefined
|
||||
let scrolled = false
|
||||
|
||||
const onScroll = (e: Event) => {
|
||||
const el = e.currentTarget as HTMLDivElement
|
||||
if (el.scrollHeight - el.clientHeight - el.scrollTop < 10) scrolled = false
|
||||
}
|
||||
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
if (e.deltaY < 0) scrolled = true
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
display()
|
||||
if (!done() && ref) {
|
||||
if (!done() && ref && !scrolled) {
|
||||
ref.scrollTop = ref.scrollHeight
|
||||
}
|
||||
})
|
||||
@@ -1312,7 +1344,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
|
||||
return (
|
||||
<Show when={display()}>
|
||||
<div data-component="reasoning-part" data-streaming={!done() ? "" : undefined}>
|
||||
<Collapsible open={open()} onOpenChange={setOpen} class="tool-collapsible">
|
||||
<Collapsible open={open()} onOpenChange={track} class="tool-collapsible">
|
||||
<Collapsible.Trigger>
|
||||
<div data-slot="reasoning-header">
|
||||
<Icon name="brain" size="small" />
|
||||
@@ -1321,7 +1353,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
|
||||
<Collapsible.Arrow />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<div data-slot="reasoning-content" ref={ref}>
|
||||
<div data-slot="reasoning-content" ref={ref} onScroll={onScroll} onWheel={onWheel}>
|
||||
<Markdown text={display()} cacheKey={id} />
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6b0f41c54aca88874a3c74151b77ef20f0f17fa9bb2f149c13adfaf4f48de286
|
||||
size 14702
|
||||
oid sha256:cc32d99eeff1cb3061caa4a75e3353e30e6373073bbd7ebf17048c917367e11a
|
||||
size 28005
|
||||
|
||||
Reference in New Issue
Block a user