Skip to content

Commit 243cd37

Browse files
fix(mothership): debt-aware drain fast-path and settle-window handle retention
1 parent 4757422 commit 243cd37

2 files changed

Lines changed: 29 additions & 6 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,16 +352,19 @@ export function MothershipChat({
352352
floorDrainRafRef.current = 0
353353
return
354354
}
355-
// Re-checked per frame: a user scrolling away mid-drain makes the
356-
// remaining shrink invisible, so finish instantly instead of clamping.
357-
const pinned = el.scrollHeight - el.scrollTop - el.clientHeight <= 2
358-
const next = Math.floor(current - Math.max(1, (current - target) * SMOOTH_CHASE_RATE))
359-
if (!pinned || next - target <= 1) {
355+
// Instant-clear only when the whole remaining debt sits BELOW the
356+
// viewport (debt ≤ distance-from-bottom) — then the shrink is
357+
// invisible. A merely-unpinned viewport with debt larger than its
358+
// slack would still clamp, so it keeps the eased drain instead.
359+
const distance = el.scrollHeight - el.scrollTop - el.clientHeight
360+
const debt = current - target
361+
if (debt <= 1 || debt <= distance) {
360362
sizerFloorAppliedRef.current = 0
361363
floorDrainRafRef.current = 0
362364
sizer.style.minHeight = ''
363365
return
364366
}
367+
const next = Math.floor(current - Math.max(1, debt * SMOOTH_CHASE_RATE))
365368
sizerFloorAppliedRef.current = next
366369
sizer.style.minHeight = `${next}px`
367370
floorDrainRafRef.current = requestAnimationFrame(drain)

apps/sim/hooks/use-auto-scroll.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,21 @@ export function useAutoScroll(isStreaming: boolean) {
6666
containerRef.current = el
6767
}, [])
6868

69+
/**
70+
* Cancels the previous teardown's settle window (chase, temp listeners,
71+
* removal timer). Invoked when a new stream starts — a stop-then-resend must
72+
* not leave the old settle chase writing beside the new stream's chase — and
73+
* on unmount.
74+
*/
75+
const settleCleanupRef = useRef<(() => void) | null>(null)
76+
useEffect(() => () => settleCleanupRef.current?.(), [])
77+
6978
useEffect(() => {
7079
if (!isStreaming) return
7180
const el = containerRef.current
7281
if (!el) return
82+
settleCleanupRef.current?.()
83+
settleCleanupRef.current = null
7384

7485
/**
7586
* Eased bottom-chase shared by the mutation observer and the seed below —
@@ -224,10 +235,19 @@ export function useAutoScroll(isStreaming: boolean) {
224235
}
225236
el.addEventListener('wheel', cancelOnGesture, { passive: true })
226237
el.addEventListener('touchmove', cancelOnGesture, { passive: true })
227-
setTimeout(() => {
238+
const removeGestureGuard = () => {
228239
el.removeEventListener('wheel', cancelOnGesture)
229240
el.removeEventListener('touchmove', cancelOnGesture)
241+
}
242+
const guardTimeout = setTimeout(() => {
243+
removeGestureGuard()
244+
settleCleanupRef.current = null
230245
}, POST_STOP_SETTLE_WINDOW + 100)
246+
settleCleanupRef.current = () => {
247+
chase.cancel()
248+
clearTimeout(guardTimeout)
249+
removeGestureGuard()
250+
}
231251
}
232252
}, [isStreaming])
233253

0 commit comments

Comments
 (0)