Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bc4291d
fix(native-chat): sending brings the latest into view, with visible j…
AmoabaKelvin Oct 2, 2026
ffd5405
fix(native-chat): a late or unsent answer no longer moves a reader wh…
AmoabaKelvin Oct 2, 2026
70a02d5
fix(native-chat): an empty answer no longer moves the reader
AmoabaKelvin Oct 2, 2026
f351fc5
fix(native-chat): preserve upstream retry and question cancellation
nwparker Oct 5, 2026
0ec7978
merge: origin/main into fix/desktop-chat-scrolling
brennanb2025 Oct 5, 2026
b5974c1
refactor(native-chat): leave Jump to top out of this change
brennanb2025 Oct 5, 2026
da90399
refactor(native-chat): keep Jump to latest's existing look in this ch…
brennanb2025 Oct 6, 2026
8281270
Merge remote-tracking branch 'origin/main' into stack-24514-logic
brennanb2025 Oct 6, 2026
334013e
Merge origin/main into native chat scrolling review branch
brennanb2025 Oct 6, 2026
cf26c91
Merge origin/main into native chat scrolling review branch
brennanb2025 Oct 6, 2026
8639591
Preserve native chat reader intent across submissions and disclosure …
brennanb2025 Oct 6, 2026
5441267
Merge current main into native chat scrolling fix
brennanb2025 Oct 6, 2026
ac137ff
fix(native-chat): wait for Resume and recover following on reopen
brennanb2025 Oct 6, 2026
efefff8
fix(native-chat): assert reader takeover before rail alignment
brennanb2025 Oct 6, 2026
04a1286
fix(native-chat): retain reader takeover until its frame ends
brennanb2025 Oct 6, 2026
47a1fd9
Merge main into desktop chat scrolling
brennanb2025 Oct 6, 2026
d080d70
Merge remote-tracking branch 'origin/main' into stack-24514-main2
brennanb2025 Oct 6, 2026
37a5372
fix(native-chat): only a scroll gesture stops following; sends reveal…
brennanb2025 Oct 6, 2026
d862a82
chore: leave an unrelated fixture as main has it
brennanb2025 Oct 6, 2026
e9afe53
Merge remote-tracking branch 'origin/main' into rework-24514-v2
brennanb2025 Oct 6, 2026
5667d90
refactor(native-chat): share the terminal send paths' common options …
brennanb2025 Oct 6, 2026
0b03326
test(native-chat): drop a field the main merge declared twice
brennanb2025 Oct 6, 2026
a699e53
test(native-chat): the pane's steer still steers the queue, and also …
brennanb2025 Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 19 additions & 25 deletions src/renderer/src/components/native-chat/NativeChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const NativeChatComposerPane = forwardRef<NativeChatComposerHandle, NativeChatCo
optimisticSendOutcome,
onOptimisticSendCanceled,
onSlashCommand,
onSubmitted,
answerCommandLocally,
onSwitchToTerminal,
readTerminalScreen,
Expand Down Expand Up @@ -225,6 +226,7 @@ const NativeChatComposerPane = forwardRef<NativeChatComposerHandle, NativeChatCo
agent,
disabled,
onSlashCommand,
onSubmitted,
resolveTarget,
setHistory
})
Expand Down Expand Up @@ -255,30 +257,35 @@ const NativeChatComposerPane = forwardRef<NativeChatComposerHandle, NativeChatCo
setCaret
})

const sendPty = useNativeChatPtyComposerSend({
// What both terminal send paths share: where a command goes and what the composer resets.
const ptyCommandRouting = {
agent,
draft,
imageAttachments,
disabled,
isDispatchingSessionOption,
launchDraft: launchSeed?.launchDraft,
launchDraftResolved: launchSeed?.launchDraftResolved === true,
readTerminalScreen,
resolveTarget,
classifySend,
onOptimisticSend,
optimisticSendOutcome,
onSlashCommand,
onSubmitted,
answerCommandLocally,
sessionOptionsSurface: ptySessionOptionsSurface,
terminalTabId,
trackPendingSend,
setHistory,
setDraft,
setCaret,
clearSkillOrigin,
clearImageAttachments,
setNotice
}
const sendPty = useNativeChatPtyComposerSend({
...ptyCommandRouting,
draft,
imageAttachments,
launchDraft: launchSeed?.launchDraft,
launchDraftResolved: launchSeed?.launchDraftResolved === true,
readTerminalScreen,
classifySend,
onOptimisticSend,
optimisticSendOutcome,
terminalTabId
})
const { send, goalMode } = useNativeChatComposerSubmit({
structuredTransport,
Expand All @@ -303,21 +310,8 @@ const NativeChatComposerPane = forwardRef<NativeChatComposerHandle, NativeChatCo
})

const dispatchPtyPickerCommand = useNativeChatPickerCommandDispatch({
agent,
disabled,
isDispatchingSessionOption,
resolveTarget,
onSlashCommand,
answerCommandLocally,
sessionOptionsSurface: ptySessionOptionsSurface,
trackPendingSend,
setHistory,
setDraft,
setCaret,
setActiveSuggestion,
clearSkillOrigin,
clearImageAttachments,
setNotice
...ptyCommandRouting,
setActiveSuggestion
})
const dispatchPickerCommand = useCallback(
(command: Parameters<typeof dispatchPtyPickerCommand>[0]) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ describe('transcript follow ownership across growth and appends', () => {
const scheduleSpy = vi.spyOn(window, 'requestAnimationFrame')
const scrollToSpy = vi.spyOn(scroller, 'scrollTo')
const readingAt = 2000
fireEvent.wheel(scroller, { deltaY: -100 })
scroller.scrollTop = readingAt
fireEvent.scroll(scroller)
expect(scrollToSpy).toHaveBeenLastCalledWith({ behavior: 'auto', top: readingAt })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,19 @@ afterEach(cleanup)
describe('revealing a diff from a turn rollup', () => {
let restoreLayout = (): void => {}
beforeEach(() => {
restoreLayout = stubLayout()
restoreLayout = stubLayout({ scrollGeometry: true })
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
this: HTMLElement
) {
const row = this.closest<HTMLElement>('[data-index]')
const scroller = this.closest<HTMLElement>('[data-native-chat-scroll]')
return new DOMRect(
0,
row ? Number.parseFloat(row.style.top) - (scroller?.scrollTop ?? 0) : 0,
100,
this.offsetHeight
)
})
})
afterEach(() => {
restoreLayout()
Expand Down Expand Up @@ -79,29 +91,47 @@ describe('revealing a diff from a turn rollup', () => {
].map((item, index) => ({ ...item, sequence: index + 1 }))
const scrollTo = vi.fn()
vi.spyOn(HTMLElement.prototype, 'scrollTo').mockImplementation(scrollTo)
const { container } = render(
const transcript = (): React.JSX.Element => (
<NativeChatMessageList
session={session(projectStructuredItemsToNativeChat(withPrompts))}
journalItems={withPrompts}
isWorking={false}
expandSignal={false}
/>
)
const { container, rerender } = render(transcript())
const selectedTop = Number.parseFloat(
screen.getByText('Second prompt').closest<HTMLElement>('[data-index]')?.style.top ?? ''
)
expect(selectedTop).toBeGreaterThan(0)
fireEvent.click(screen.getByRole('button', { name: /1 changed file/ }))
fireEvent.click(screen.getByRole('button', { name: /src\/a.ts/ }))
scrollTranscript(container, 6000)
expect(screen.getByText('Edited')).toBeInTheDocument()
scrollTo.mockClear()
fireEvent.click(screen.getByRole('button', { name: 'Your messages' }))
fireEvent.click(screen.getByRole('button', { name: 'Second prompt' }))
expect(scrollTo).toHaveBeenCalledTimes(1)
expect(scrollTo.mock.calls).toEqual([
[{ top: 6000, behavior: 'auto' }],
[{ top: selectedTop, behavior: 'smooth' }]
])
expect(screen.queryByRole('button', { name: /^Edited .*a\.ts(?:\s|$)/ })).toBeNull()
scrollTo.mockClear()
rerender(transcript())
rerender(transcript())
expect(scrollTo).not.toHaveBeenCalled()

scrollTranscript(container, 0)
scrollTo.mockClear()
fireEvent.click(screen.getByRole('button', { name: /1 changed file/ }))
fireEvent.click(screen.getByRole('button', { name: /src\/a.ts/ }))
expect(scrollTo).toHaveBeenCalledTimes(1)
const diffTop = Number.parseFloat(
screen.getByText('Edited').closest<HTMLElement>('[data-index]')?.style.top ?? ''
)
expect(Number.isFinite(diffTop)).toBe(true)
expect(scrollTo.mock.calls.filter(([options]) => options?.behavior === 'smooth')).toEqual([
[{ top: diffTop, behavior: 'smooth' }]
])
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ describe('jumping from the rail while following the end', () => {
await settle(10)
// A reader parked above the end, so the button shows.
act(() => {
fireEvent.wheel(scroller(), { deltaY: -100 })
scroller().scrollTop = 200
})
await settle(2)
Expand All @@ -450,6 +451,29 @@ describe('jumping from the rail while following the end', () => {
expect(screen.queryByText('prompt-5')).toBeNull()
expect(distanceFromBottom()).toBe(0)
})

it('jumps to a message without animating when the reader asks for reduced motion', async () => {
vi.spyOn(window, 'matchMedia').mockImplementation((query: string) => ({
matches: query === '(prefers-reduced-motion: reduce)',
media: query,
onchange: null,
addEventListener: () => {},
removeEventListener: () => {},
addListener: () => {},
removeListener: () => {},
dispatchEvent: () => false
}))
render(<PagedTranscript />)
await settle(10)

fireEvent.click(screen.getByRole('button', { name: 'Your messages' }))
await frame()
fireEvent.click(screen.getByRole('button', { name: 'prompt-45' }))
await frame()

// A smooth scroll would still be easing in here, a few pixels from where it started.
expect(Math.abs(rowOffsetFromViewportTop('prompt-45'))).toBeLessThanOrEqual(2)
})
})

describe('revealing a diff while a rail jump pages', () => {
Expand Down
Loading
Loading