diff --git a/src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx b/src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx
index 54ed8f51d9b..97b6f846cf3 100644
--- a/src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx
+++ b/src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx
@@ -111,6 +111,8 @@ type ProbeProps = {
disabled?: boolean
initialDraft?: string
structured?: boolean
+ /** Overrides only the structured target, leaving the pane's scope key alone. */
+ structuredWorkspaceId?: string
workspaceId?: string
}
@@ -121,6 +123,7 @@ function ComposerProbe({
disabled = false,
initialDraft = '',
structured = true,
+ structuredWorkspaceId,
workspaceId = 'worktree-1'
}: ProbeProps): React.JSX.Element {
const [draft, setDraft] = useState(initialDraft)
@@ -143,7 +146,7 @@ function ComposerProbe({
})
const workspaceFileDropHandlers = useNativeChatWorkspaceFileDrop({
terminalTabId: 'terminal-tab-1',
- structuredWorktreeId: structured ? workspaceId : undefined,
+ structuredWorktreeId: structured ? (structuredWorkspaceId ?? workspaceId) : undefined,
disabled,
attachResolvedPaths: attachments.attachResolvedPaths,
setNotice
@@ -244,6 +247,7 @@ describe('native chat workspace file drops', () => {
testState.ownerSshGeneration = 4
testState.ownerWorktreePath = '/remote/repo'
testState.targetIsRemoteRuntime = false
+ testState.store.tabsByWorktree = { 'worktree-1': [{ id: 'terminal-tab-1' }] }
latestInput = null
bubbledDrop.mockReset()
})
@@ -425,6 +429,21 @@ describe('native chat workspace file drops', () => {
expect(screen.getByText('Files can only be attached to their source workspace.')).toBeTruthy()
})
+ // The queued check must ask which workspace this composer serves NOW. Comparing
+ // a captured id against itself would pass no matter where the pane ended up.
+ it('rejects an IME-queued path when the pane changes workspace before settling', () => {
+ const view = render()
+ const input = editor()
+ fireEvent.compositionStart(input)
+ dispatchDragEvent('drop', input, internalTransfer(['/repo/a.ts']))
+
+ view.rerender()
+ fireEvent.compositionEnd(input, { data: '' })
+
+ expect(screen.getByTestId('draft').textContent).toBe('preedit')
+ expect(screen.getByText('Files can only be attached to their source workspace.')).toBeTruthy()
+ })
+
it('rejects only the exact unresolved-owner sentinel', () => {
testState.executionHostId = 'runtime:unresolved-owner'
const view = render()
diff --git a/src/renderer/src/components/native-chat/use-native-chat-workspace-file-drop.ts b/src/renderer/src/components/native-chat/use-native-chat-workspace-file-drop.ts
index 638c2fa55d1..ee03aa6849c 100644
--- a/src/renderer/src/components/native-chat/use-native-chat-workspace-file-drop.ts
+++ b/src/renderer/src/components/native-chat/use-native-chat-workspace-file-drop.ts
@@ -1,4 +1,4 @@
-import { useCallback, type DragEventHandler } from 'react'
+import { useCallback, useLayoutEffect, useRef, type DragEventHandler } from 'react'
import { useAppStore } from '@/store'
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
import {
@@ -67,6 +67,14 @@ export function useNativeChatWorkspaceFileDrop({
structuredWorktreeId,
terminalTabId
}: Args): WorkspaceFileDropHandlers {
+ // The IME-flush check runs against a closure captured at drop time. Reading
+ // the prop through a ref keeps "is this still my workspace?" a real question
+ // rather than a comparison of one captured value against itself.
+ const structuredWorktreeIdRef = useRef(structuredWorktreeId)
+ useLayoutEffect(() => {
+ structuredWorktreeIdRef.current = structuredWorktreeId
+ }, [structuredWorktreeId])
+
const onDragOverCapture = useCallback>(
(event) => {
if (!hasWorkspaceFileDragType(event.dataTransfer)) {
@@ -130,7 +138,7 @@ export function useNativeChatWorkspaceFileDrop({
const targetOwnerIsCurrent = (): boolean => {
const currentState = useAppStore.getState()
const currentWorkspaceId =
- structuredWorktreeId ??
+ structuredWorktreeIdRef.current ??
findTerminalTabWorktreeId(currentState.tabsByWorktree, terminalTabId)
if (currentWorkspaceId !== source.workspaceId) {
return false
@@ -139,7 +147,7 @@ export function useNativeChatWorkspaceFileDrop({
const currentOwner = resolveNativeChatAttachmentOwnerForWorktree(
currentState,
currentWorkspaceId,
- structuredWorktreeId ? undefined : terminalTabId
+ structuredWorktreeIdRef.current ? undefined : terminalTabId
)
return (
isResolvedWorkspaceFileDragExecutionHost(currentHostId) &&