fix(native-chat): route app menu image paste in codex chat (#17498)

Co-authored-by: Merge Sim <sim@local>
This commit is contained in:
Brennan Benson
2026-08-30 17:35:24 -07:00
committed by GitHub
co-authored by Merge Sim
parent 906bf54cc4
commit b2eec5980b
2 changed files with 41 additions and 6 deletions
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import type React from 'react'
import React, { forwardRef, useImperativeHandle } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
@@ -13,6 +13,8 @@ const mocks = vi.hoisted(() => ({
onLinkClick?: (...args: unknown[]) => void
},
composerProps: null as null | { structuredTransport?: Record<string, unknown> },
handlePasteEvent: vi.fn(),
pasteFromClipboard: vi.fn(),
submissions: [] as unknown[]
}))
@@ -110,10 +112,16 @@ vi.mock('./NativeChatMessageList', () => ({
}))
vi.mock('./NativeChatComposer', () => ({
NativeChatComposer: (props: typeof mocks.composerProps) => {
NativeChatComposer: forwardRef((props: typeof mocks.composerProps, ref) => {
mocks.composerProps = props
return null
}
useImperativeHandle(ref, () => ({
focus: () => true,
insertTypedText: () => true,
handlePasteEvent: mocks.handlePasteEvent,
pasteFromClipboard: mocks.pasteFromClipboard
}))
return <textarea data-testid="structured-composer" />
})
}))
vi.mock('./NativeChatEmptyState', () => ({ NativeChatEmptyState: () => null }))
vi.mock('./NativeChatApprovalCard', () => ({ NativeChatApprovalCard: () => null }))
@@ -128,9 +136,30 @@ describe('NativeChatStructuredSession', () => {
mocks.mode = 'static'
mocks.messageListProps = null
mocks.composerProps = null
mocks.handlePasteEvent.mockReset()
mocks.pasteFromClipboard.mockReset()
mocks.submissions = []
})
it('routes app-menu paste into the structured composer', () => {
render(
<NativeChatStructuredSession
isVisible
tabId="structured-tab-paste"
sessionId="session-paste"
target={{ kind: 'local' }}
agent="codex"
allowFileUriLinks
/>
)
const composer = screen.getByTestId('structured-composer')
composer.focus()
window.dispatchEvent(new Event('orca-app-menu-paste', { cancelable: true }))
expect(mocks.pasteFromClipboard).toHaveBeenCalledOnce()
})
it('wires local structured file links through the native chat opener', () => {
render(
<NativeChatStructuredSession
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { RotateCcw } from 'lucide-react'
import type {
AgentStatusOrchestrationContext,
@@ -10,7 +10,7 @@ import type { NativeChatLiveSession } from './use-native-chat-live-session'
import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
import { Button } from '@/components/ui/button'
import { NativeChatApprovalCard } from './NativeChatApprovalCard'
import { NativeChatComposer } from './NativeChatComposer'
import { NativeChatComposer, type NativeChatComposerHandle } from './NativeChatComposer'
import { NativeChatEmptyState } from './NativeChatEmptyState'
import { NativeChatMessageList } from './NativeChatMessageList'
import { NativeChatQuestionCard } from './NativeChatQuestionCard'
@@ -21,6 +21,7 @@ import { useNativeChatFileLinkContext } from './use-native-chat-file-link-contex
import { useStructuredAgentSession } from './use-structured-agent-session'
import { translate } from '@/i18n/i18n'
import { NativeChatOrchestrationPausedNotice } from './NativeChatOrchestrationPausedNotice'
import { useNativeChatPasteBridge } from './use-native-chat-paste-bridge'
function encodeQuestionAnswer(questionId: string, answer: string): string {
return `${encodeURIComponent(questionId)}:${encodeURIComponent(answer)}`
@@ -45,6 +46,9 @@ export function NativeChatStructuredSession(props: {
() => structuredAgentSessionPaneKey(props.tabId, props.sessionId),
[props.sessionId, props.tabId]
)
const rootRef = useRef<HTMLDivElement>(null)
const composerRef = useRef<NativeChatComposerHandle>(null)
useNativeChatPasteBridge({ rootRef, composerRef })
const session = useMemo<NativeChatLiveSession>(
() => ({
messages: controller.messages,
@@ -117,6 +121,7 @@ export function NativeChatStructuredSession(props: {
return (
<div
ref={rootRef}
data-native-chat-root="true"
data-native-chat-working={controller.isWorking ? 'true' : 'false'}
tabIndex={-1}
@@ -220,6 +225,7 @@ export function NativeChatStructuredSession(props: {
) : null}
{prompt ? null : (
<NativeChatComposer
ref={composerRef}
terminalTabId={props.tabId}
paneKey={paneKey}
targetPtyId={null}