refine scrolling#468
Conversation
PR Summary by QodoRefine chat/log scrolling and prevent modal backdrop closes
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1. Pin observer lacks teardown
|
| function pinToBottomWhileSettling(timeoutMs = 3000) { | ||
| const scrollbar = scrollbars[0]; | ||
| if (!scrollbar) return; | ||
|
|
||
| const { viewport } = scrollbar.elements(); | ||
| const content = viewport.firstElementChild || viewport; | ||
| const pin = () => { viewport.scrollTop = viewport.scrollHeight; }; | ||
| pin(); | ||
|
|
||
| const observer = new ResizeObserver(pin); | ||
| observer.observe(content); | ||
|
|
||
| /** @type {ReturnType<typeof setTimeout>} */ | ||
| let timer; | ||
| const stop = () => { | ||
| observer.disconnect(); | ||
| clearTimeout(timer); | ||
| viewport.removeEventListener('wheel', stop); | ||
| viewport.removeEventListener('pointerdown', stop); | ||
| viewport.removeEventListener('keydown', stop); | ||
| }; | ||
| viewport.addEventListener('wheel', stop, { passive: true }); | ||
| viewport.addEventListener('pointerdown', stop); | ||
| viewport.addEventListener('keydown', stop); | ||
| timer = setTimeout(stop, timeoutMs); | ||
| } |
There was a problem hiding this comment.
1. Pin observer lacks teardown 🐞 Bug ☼ Reliability
In chat-box.svelte, pinToBottomWhileSettling() registers a ResizeObserver, timeout, and event listeners but nothing calls its stop()/disconnect logic during component teardown, so callbacks can run against detached DOM until the timeout fires. onDestroy() only destroys OverlayScrollbars instances and does not stop the pinning observer/listeners.
Agent Prompt
### Issue description
`pinToBottomWhileSettling()` creates a `ResizeObserver`, a timeout, and several event listeners, but its cleanup (`observer.disconnect()`, `clearTimeout`, `removeEventListener`) is only executed when `stop()` runs. If the route/component unmounts before that, these resources can remain active briefly and execute against DOM that’s no longer owned by the component.
### Issue Context
`onDestroy()` currently destroys OverlayScrollbars instances, but the pinning observer/listeners created by `pinToBottomWhileSettling()` are not referenced there.
### Fix Focus Areas
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[235-237]
- src/routes/chat/[agentId]/[conversationId]/chat-box.svelte[492-526]
### Suggested fix
- Make `pinToBottomWhileSettling()` return a disposer (e.g., `return stop;`) or store `stop` in an outer-scope variable.
- Invoke that disposer from `onDestroy()` (and optionally before re-running pinning) to ensure the `ResizeObserver`, timer, and listeners are always cleaned up on unmount.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| function pinToBottomWhileSettling(timeoutMs = 3000) { | ||
| scrollbars.forEach(scrollbar => { | ||
| if (!scrollbar) return; | ||
|
|
||
| const { viewport } = scrollbar.elements(); | ||
| const content = viewport.firstElementChild || viewport; | ||
| const pin = () => { viewport.scrollTop = viewport.scrollHeight; }; | ||
| pin(); | ||
|
|
||
| const observer = new ResizeObserver(pin); | ||
| observer.observe(content); | ||
|
|
||
| /** @type {ReturnType<typeof setTimeout>} */ | ||
| let timer; | ||
| const stop = () => { | ||
| observer.disconnect(); | ||
| clearTimeout(timer); | ||
| viewport.removeEventListener('wheel', stop); | ||
| viewport.removeEventListener('pointerdown', stop); | ||
| viewport.removeEventListener('keydown', stop); | ||
| }; | ||
| viewport.addEventListener('wheel', stop, { passive: true }); | ||
| viewport.addEventListener('pointerdown', stop); | ||
| viewport.addEventListener('keydown', stop); | ||
| timer = setTimeout(stop, timeoutMs); | ||
| }); |
There was a problem hiding this comment.
2. Persist-log leaks scroll resources 🐞 Bug ☼ Reliability
In persist-log.svelte, pinToBottomWhileSettling() adds ResizeObserver/listeners per panel but the onMount cleanup only calls cleanLogs(), so these resources may remain active after unmount until the timeout fires. Additionally, initScrollbars() creates OverlayScrollbars instances but the component never destroys them on teardown.
Agent Prompt
### Issue description
`pinToBottomWhileSettling()` attaches `ResizeObserver` + event listeners + a timer for each OverlayScrollbars viewport, but the component’s teardown path (the `onMount` return cleanup) doesn’t call any disposer, so resources can outlive the component briefly. Separately, OverlayScrollbars instances created in `initScrollbars()` are never `.destroy()`’d.
### Issue Context
PersistLog is conditionally mounted/unmounted from chat-box; when it unmounts, it should release all observers/listeners and destroy OverlayScrollbars instances.
### Fix Focus Areas
- src/routes/chat/[agentId]/[conversationId]/persist-log/persist-log.svelte[69-84]
- src/routes/chat/[agentId]/[conversationId]/persist-log/persist-log.svelte[114-149]
- src/routes/chat/[agentId]/[conversationId]/persist-log/persist-log.svelte[151-160]
### Suggested fix
- Refactor `pinToBottomWhileSettling()` to return an array of disposer functions (one per scrollbar) or store them in component scope.
- In the `onMount` cleanup (or `onDestroy`), call those disposers to `disconnect()` observers, remove listeners, and clear timers immediately.
- Also destroy OverlayScrollbars instances in teardown (e.g., `scrollbars.forEach(sb => sb?.destroy?.()); scrollbars = [];`).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
No description provided.