mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 08:02:28 +00:00
Fix native chat drop ownership across session surfaces
This commit is contained in:
@@ -28,12 +28,13 @@ export function useNativeChatPaneFileDropClaim(claim: NativeChatPaneDropClaim):
|
||||
useLayoutEffect(() => {
|
||||
claimRef.current = claim
|
||||
})
|
||||
const { scopeKey } = claim
|
||||
const { scopeKey, disabled } = claim
|
||||
const registration = useMemo<NativeChatPaneDropRegistration>(
|
||||
() => ({ getClaim: () => claimRef.current, scopeKey }),
|
||||
[scopeKey]
|
||||
)
|
||||
useLayoutEffect(() => register?.(registration), [register, registration])
|
||||
// A guard transition ends the current hover before the next paint.
|
||||
useLayoutEffect(() => register?.(registration), [disabled, register, registration])
|
||||
}
|
||||
|
||||
export function NativeChatPaneFileDropSurface({
|
||||
@@ -55,15 +56,13 @@ export function NativeChatPaneFileDropSurface({
|
||||
},
|
||||
[]
|
||||
)
|
||||
const registrationRef = useRef(registration)
|
||||
registrationRef.current = registration
|
||||
const handlers = useMemo(
|
||||
() =>
|
||||
makeNativeChatPaneFileDropHandlers({
|
||||
getClaim: () => registrationRef.current?.getClaim() ?? null,
|
||||
getClaim: () => registration?.getClaim() ?? null,
|
||||
setDragActive: setIsDragActive
|
||||
}),
|
||||
[]
|
||||
[registration]
|
||||
)
|
||||
// An OS drag never reaches React: the preload drop route consumes that event
|
||||
// before it leaves `document`. Its end is the only signal the overlay gets.
|
||||
|
||||
@@ -3,15 +3,21 @@ import { NativeChatStructuredSession } from './NativeChatStructuredSession'
|
||||
import { NativeChatResolvedView } from './NativeChatResolvedView'
|
||||
import { useNativeChatStatusEntry } from './use-native-chat-status-entry'
|
||||
import type { NativeChatViewProps } from './native-chat-view-types'
|
||||
import { NativeChatPaneFileDropSurface } from './NativeChatPaneFileDropSurface'
|
||||
|
||||
export type { NativeChatViewProps } from './native-chat-view-types'
|
||||
|
||||
/** Resolves an agent terminal into its native conversation and composer UI. */
|
||||
export default function NativeChatView(props: NativeChatViewProps): React.JSX.Element {
|
||||
if (props.mode === 'structured') {
|
||||
return <NativeChatStructuredSession key={props.sessionId} {...props} />
|
||||
}
|
||||
return <NativeChatBridgeView {...props} />
|
||||
return (
|
||||
<NativeChatPaneFileDropSurface className="relative flex h-full min-h-0 min-w-0 w-full">
|
||||
{props.mode === 'structured' ? (
|
||||
<NativeChatStructuredSession key={props.sessionId} {...props} />
|
||||
) : (
|
||||
<NativeChatBridgeView {...props} />
|
||||
)}
|
||||
</NativeChatPaneFileDropSurface>
|
||||
)
|
||||
}
|
||||
|
||||
function NativeChatBridgeView({
|
||||
|
||||
@@ -152,6 +152,26 @@ describe('NativeChatPaneFileDropSurface', () => {
|
||||
expect(onDropCapture).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears the active invitation when the composer becomes guarded', () => {
|
||||
const pane = (disabled: boolean) => (
|
||||
<NativeChatPaneFileDropSurface className="pane">
|
||||
<ClaimingComposer disabled={disabled} />
|
||||
</NativeChatPaneFileDropSurface>
|
||||
)
|
||||
const { container, rerender } = render(pane(false))
|
||||
fireDrag(screen.getByTestId('composer'), 'dragover', workspaceDrag())
|
||||
expect(container.querySelector(OVERLAY)).not.toBeNull()
|
||||
|
||||
rerender(pane(true))
|
||||
expect(container.querySelector(OVERLAY)).toBeNull()
|
||||
expect(container.querySelector('.pane')?.getAttribute('data-composer-scope-key')).toBe('pane:1')
|
||||
|
||||
rerender(pane(false))
|
||||
expect(container.querySelector(OVERLAY)).toBeNull()
|
||||
fireDrag(screen.getByTestId('composer'), 'dragover', workspaceDrag())
|
||||
expect(container.querySelector(OVERLAY)).not.toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the overlay up while the cursor crosses children, and drops it on exit', () => {
|
||||
const { transcript, container } = renderPane(<ClaimingComposer />)
|
||||
fireDrag(transcript, 'dragover', workspaceDrag())
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { WORKSPACE_FILE_PATH_MIME } from '@/lib/workspace-file-drag'
|
||||
import NativeChatView from './NativeChatView'
|
||||
import { useNativeChatPaneFileDropClaim } from './NativeChatPaneFileDropSurface'
|
||||
|
||||
const drop = vi.hoisted(() => ({ onDrop: vi.fn() }))
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
|
||||
vi.mock('./NativeChatSessionGate', () => ({ NativeChatSessionGate: () => null }))
|
||||
vi.mock('./NativeChatResolvedView', () => ({ NativeChatResolvedView: () => null }))
|
||||
vi.mock('./use-native-chat-status-entry', () => ({ useNativeChatStatusEntry: vi.fn() }))
|
||||
vi.mock('./NativeChatStructuredSession', () => ({
|
||||
NativeChatStructuredSession: () => {
|
||||
useNativeChatPaneFileDropClaim({
|
||||
scopeKey: 'structured:session',
|
||||
disabled: false,
|
||||
onDragOverCapture: (event) => event.preventDefault(),
|
||||
onDropCapture: drop.onDrop
|
||||
})
|
||||
return <div data-testid="transcript">Conversation above the composer</div>
|
||||
}
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
it('owns pane drops for standalone structured sessions without a terminal portal', () => {
|
||||
const { container } = render(
|
||||
<NativeChatView
|
||||
mode="structured"
|
||||
tabId="structured-tab"
|
||||
sessionId="session"
|
||||
agent="codex"
|
||||
target={{ kind: 'local' }}
|
||||
isVisible
|
||||
isFocusedGroup
|
||||
/>
|
||||
)
|
||||
const transcript = screen.getByTestId('transcript')
|
||||
const dataTransfer = { types: [WORKSPACE_FILE_PATH_MIME] }
|
||||
|
||||
fireEvent.dragOver(transcript, { dataTransfer })
|
||||
expect(container.querySelector('[data-native-chat-drop-overlay]')).not.toBeNull()
|
||||
expect(
|
||||
transcript
|
||||
.closest('[data-native-file-drop-target="composer"]')
|
||||
?.getAttribute('data-composer-scope-key')
|
||||
).toBe('structured:session')
|
||||
|
||||
fireEvent.drop(transcript, { dataTransfer })
|
||||
expect(drop.onDrop).toHaveBeenCalledTimes(1)
|
||||
expect(container.querySelector('[data-native-chat-drop-overlay]')).toBeNull()
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import NativeChatView from '../native-chat/NativeChatView'
|
||||
import { NativeChatPaneFileDropSurface } from '../native-chat/NativeChatPaneFileDropSurface'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { canContinueAgentSessionInNewSession } from './terminal-agent-session-continuation'
|
||||
import type { TerminalPaneController } from './use-terminal-pane-controller'
|
||||
@@ -69,7 +68,7 @@ export function TerminalPaneNativeChatPortal({
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<NativeChatPaneFileDropSurface className="native-chat-pane-shell absolute inset-0 z-10 flex min-h-0 min-w-0 bg-background">
|
||||
<div className="native-chat-pane-shell absolute inset-0 z-10 flex min-h-0 min-w-0 bg-background">
|
||||
{structuredSessionId && structuredChatAgent ? (
|
||||
<NativeChatView
|
||||
mode="structured"
|
||||
@@ -96,7 +95,7 @@ export function TerminalPaneNativeChatPortal({
|
||||
contextMenuActions={contextMenuActions}
|
||||
/>
|
||||
)}
|
||||
</NativeChatPaneFileDropSurface>,
|
||||
</div>,
|
||||
chatPane.container,
|
||||
`native-chat-${tabId}-${chatPane.leafId}`
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user