diff --git a/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx b/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx
index 00bc80ff95..16b173c8f7 100644
--- a/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx
+++ b/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx
@@ -1,7 +1,7 @@
-import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { clearAllPrompts, setApprovalRequest } from '@/store/prompts'
+import { clearAllPrompts, clearApprovalRequest, setApprovalRequest } from '@/store/prompts'
import { $activeSessionId } from '@/store/session'
import {
onScrollToBottomRequest,
@@ -45,7 +45,7 @@ describe('ScrollToBottomButton', () => {
it('morphs into the approval pill when scrolled up with a pending approval', () => {
pendingApproval()
setThreadAtBottom(false)
- render()
+ render()
expect(screen.getByRole('button', { name: 'Approval needed' })).toBeTruthy()
expect(screen.getByText('Approval needed')).toBeTruthy()
@@ -59,6 +59,22 @@ describe('ScrollToBottomButton', () => {
expect(screen.queryByRole('button')).toBeNull()
})
+ it('labels uncounted content without zero and follows only its own approval', () => {
+ pendingApproval()
+ setThreadAtBottom(false)
+ const view = render()
+ expect(screen.getByRole('button', { name: 'Scroll to bottom' }).textContent).toBe('Scroll to bottom')
+
+ act(() => setApprovalRequest({ command: 'x', description: 'd', sessionId: 'tile-runtime', requestId: 'r1' }))
+ expect(screen.getByRole('button', { name: 'Approval needed' })).toBeTruthy()
+ act(() => clearApprovalRequest('tile-runtime', 'r1'))
+ expect(screen.queryByText('Approval needed')).toBeNull()
+ expect(screen.getByRole('button').textContent).toBe('Scroll to bottom')
+
+ view.rerender()
+ expect(screen.getByRole('button', { name: 'Approval needed' })).toBeTruthy()
+ })
+
it('re-arms sticky-bottom on click', () => {
const handler = vi.fn()
const stop = onScrollToBottomRequest(handler, 'sess-1')
diff --git a/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx b/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx
index 6141e35dc3..4ff648315d 100644
--- a/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx
+++ b/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx
@@ -1,13 +1,13 @@
import { useStore } from '@nanostores/react'
import { useReducedMotion } from 'motion/react'
-import { useRef } from 'react'
+import { useMemo, useRef } from 'react'
import { Codicon } from '@/components/ui/codicon'
import { AnimatedInt } from '@/components/ui/diff-count'
import { useI18n } from '@/i18n'
import { triggerHaptic } from '@/lib/haptics'
import { cn } from '@/lib/utils'
-import { $approvalRequest } from '@/store/prompts'
+import { sessionApprovalRequest } from '@/store/prompts'
import { $threadJumpButtonVisible, $threadMessagesBelow, requestScrollToBottom } from '@/store/thread-scroll'
/**
@@ -36,11 +36,11 @@ export function ScrollToBottomButton({ sessionId }: { sessionId: string | null }
const visible = useStore($threadJumpButtonVisible)
const count = useStore($threadMessagesBelow)
const reducedMotion = useReducedMotion()
- const request = useStore($approvalRequest)
+ const request = useStore(useMemo(() => sessionApprovalRequest(sessionId), [sessionId]))
// Scrolled away while an approval is pending → the inline Run/Reject bar is
// below the fold. Relabel so the user knows the session needs them, not just
// that there's more to read.
- const approval = visible && Boolean(request)
+ const visibleApproval = Boolean(request)
const hasShownRef = useRef(false)
if (visible) {
@@ -48,10 +48,14 @@ export function ScrollToBottomButton({ sessionId }: { sessionId: string | null }
}
const state = visible ? 'in' : hasShownRef.current ? 'out' : 'idle'
- const countLabel = t.sidebar.messageCount(count)
- const [beforeCount, afterCount] = countLabel.split(String(count))
+ const countLabel = count > 0 ? t.sidebar.messageCount(count) : ''
+ const [beforeCount, afterCount] = countLabel ? countLabel.split(String(count)) : ['', '']
- const label = approval ? t.assistant.approval.jumpToApproval : `${t.assistant.thread.scrollToBottom} · ${countLabel}`
+ const label = visibleApproval
+ ? t.assistant.approval.jumpToApproval
+ : countLabel
+ ? `${t.assistant.thread.scrollToBottom} · ${countLabel}`
+ : t.assistant.thread.scrollToBottom
return (