fix(native-chat): keep agent responses selectable (#17437)

Co-authored-by: Merge Sim <sim@local>
This commit is contained in:
Brennan Benson
2026-08-30 14:07:06 -07:00
committed by GitHub
co-authored by Merge Sim
parent ba5f33402f
commit 8ce3fd8b32
2 changed files with 61 additions and 11 deletions
@@ -0,0 +1,52 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { NativeChatLiveSession } from './use-native-chat-live-session'
import { NativeChatMessageList } from './NativeChatMessageList'
afterEach(cleanup)
const session: NativeChatLiveSession = {
messages: [
{
id: 'assistant-1',
role: 'assistant',
blocks: [{ type: 'text', text: 'Selectable agent response.' }],
timestamp: 1,
source: 'transcript'
}
],
status: 'ready',
sessionId: 'session-1',
agent: 'codex',
hasMore: false,
loadingEarlier: false,
loadEarlier: vi.fn(),
readPhase: 'ready'
}
describe('NativeChatMessageList assistant messages', () => {
it('keeps prose selectable and places non-selectable controls after it', () => {
render(
<NativeChatMessageList
session={session}
isWorking={false}
expandSignal={false}
fontScale={1}
/>
)
const prose = screen.getByText('Selectable agent response.')
const row = prose.closest('.group')
const copyButton = screen.getByRole('button', { name: 'Copy message' })
const controls = copyButton.parentElement
expect(row).toHaveClass('select-text')
expect(controls).toHaveClass('select-none', 'pointer-events-none', 'mt-1')
expect(controls).not.toHaveClass('absolute')
expect(prose.compareDocumentPosition(controls!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING)
})
})
@@ -68,9 +68,7 @@ function ImageAttachmentRefs({ blocks }: { blocks: NativeChatBlock[] }): React.J
)
}
/** Inline controls for an agent message (mobile AgentControls parity): copy the
* message's prose, and scroll so this message's top aligns to the viewport top.
* Reveals on hover / keyboard focus like the prior copy affordance. */
/** Footer controls for an agent message: copy its prose or align it to the viewport top. */
function AgentControls({
markdown,
onScrollToTop,
@@ -243,19 +241,12 @@ function MessageRow({
<div
ref={rowRef}
className={cn(
'group relative max-w-full text-sm leading-relaxed text-foreground',
'group relative max-w-full select-text text-sm leading-relaxed text-foreground',
// Reasoning is the agent thinking aloud — quieter, italic, like an aside.
isReasoning && 'border-l-2 border-border/60 pl-3 italic text-muted-foreground',
isSystem && 'text-xs text-muted-foreground'
)}
>
{showControls ? (
<AgentControls
markdown={markdown}
onScrollToTop={scrollToTop}
className="absolute -top-8 right-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100"
/>
) : null}
<ImageAttachmentRefs blocks={prose} />
{markdown ? (
<CommentMarkdown
@@ -267,6 +258,13 @@ function MessageRow({
/>
) : null}
{tools.length > 0 ? <NativeChatToolRun blocks={tools} expandSignal={expandSignal} /> : null}
{showControls ? (
<AgentControls
markdown={markdown}
onScrollToTop={scrollToTop}
className="pointer-events-none mt-1 -mb-5 w-fit select-none opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100"
/>
) : null}
</div>
)
}