From dee30d123defbb61ec53ec17bf7aae281563db49 Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Fri, 11 Sep 2026 05:34:39 -0700 Subject: [PATCH] fix(desktop): scope approval hints and omit zero message counts --- .../app/chat/scroll-to-bottom-button.test.tsx | 22 +++++++++++++--- .../src/app/chat/scroll-to-bottom-button.tsx | 22 +++++++++------- apps/desktop/src/store/prompts.test.ts | 25 +++++++++++++++++++ apps/desktop/src/store/prompts.ts | 16 ++++++++++-- 4 files changed, 71 insertions(+), 14 deletions(-) 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 (