diff --git a/src/renderer/src/components/native-chat/NativeChatJumpToLatest.test.tsx b/src/renderer/src/components/native-chat/NativeChatJumpToLatest.test.tsx
new file mode 100644
index 00000000000..a7d12b2b2d3
--- /dev/null
+++ b/src/renderer/src/components/native-chat/NativeChatJumpToLatest.test.tsx
@@ -0,0 +1,39 @@
+// @vitest-environment happy-dom
+
+import '@testing-library/jest-dom/vitest'
+
+import { cleanup, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { afterEach, expect, it, vi } from 'vitest'
+import { NativeChatJumpToLatest } from './NativeChatJumpToLatest'
+
+afterEach(cleanup)
+
+// The reader is usually mid-draft when they reach for this: a press must not take the caret.
+it('leaves focus in the composer when pressed with the pointer', async () => {
+ const onJump = vi.fn()
+ render(
+ <>
+
+
+ >
+ )
+ const composer = screen.getByRole('textbox', { name: 'Message' })
+ composer.focus()
+
+ await userEvent.click(screen.getByRole('button', { name: 'Jump to latest' }))
+
+ // Anti-vacuous: the press did act.
+ expect(onJump).toHaveBeenCalledOnce()
+ expect(document.activeElement).toBe(composer)
+})
+
+// Mounted while hidden so it can fade out; it must not stay reachable meanwhile.
+it('stays mounted but unreachable while hidden', () => {
+ const { container } = render()
+
+ expect(screen.queryByRole('button', { name: 'Jump to latest' })).toBeNull()
+ const button = container.querySelector('button')
+ expect(button).toHaveAttribute('inert')
+ expect(button).toHaveAttribute('data-shown', 'false')
+})
diff --git a/src/renderer/src/components/native-chat/NativeChatJumpToLatest.tsx b/src/renderer/src/components/native-chat/NativeChatJumpToLatest.tsx
new file mode 100644
index 00000000000..66a0d4e8830
--- /dev/null
+++ b/src/renderer/src/components/native-chat/NativeChatJumpToLatest.tsx
@@ -0,0 +1,36 @@
+// The way back to the latest message once the reader has scrolled up.
+//
+// It stays mounted so it can fade rather than pop, and while hidden it is inert
+// and hidden from assistive tech, so it can be neither clicked, tabbed to nor read.
+
+import { ArrowDown } from 'lucide-react'
+import { translate } from '@/i18n/i18n'
+
+/** Keeps focus where the reader was typing: a pointer press never moves it here. */
+function keepFocus(event: React.MouseEvent): void {
+ event.preventDefault()
+}
+
+export function NativeChatJumpToLatest({
+ visible,
+ onJump
+}: {
+ visible: boolean
+ onJump: () => void
+}): React.JSX.Element {
+ const label = translate('components.native-chat.jumpToLatest', 'Jump to latest')
+ return (
+
+ )
+}
diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx
index 8f4f8833a48..a442d18ddf2 100644
--- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx
@@ -2,11 +2,10 @@ import {
NATIVE_CHAT_TRANSCRIPT_OUTER_CLASS,
NATIVE_CHAT_TRANSCRIPT_COLUMN_CLASS
} from './native-chat-appearance-style'
+import { NativeChatJumpToLatest } from './NativeChatJumpToLatest'
import { useNativeChatRowTypography } from './use-native-chat-row-typography'
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
-import { ArrowDown } from 'lucide-react'
import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown'
-import { translate } from '@/i18n/i18n'
import type { NativeChatLiveSession } from './use-native-chat-live-session'
import { useNativeChatTranscriptProjection } from './use-native-chat-transcript-projection'
import { structuredQuestionTranscript } from './structured-agent-question-projection'
@@ -404,17 +403,7 @@ export function NativeChatMessageList({
onReaderScroll={beginNavigation}
pendingId={railHistoryJump.pendingId}
/>
- {showJump ? (
-
- ) : null}
+
{taskListState.list && taskListState.list.tasks.length > 0 ? (