mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
fix(native-chat): decide attachment ownership per path, not per batch
A queued batch can mix sources — a workspace drop the target host owns and a client-local paste it cannot read — because IME composition holds both until it settles. Collapsing the batch to one verdict refused the whole thing on a remote target, including the drop the user was entitled to make. The verdict now follows the path it belongs to: owned paths attach, client-local ones are refused, and the refusal is reported rather than dropped. A stale owner still refuses everything, since that means the target moved under all of them. Also guards the empty-batch case, which previously read as "every path owned".
This commit is contained in:
+17
-1
@@ -207,7 +207,9 @@ describe('useNativeChatComposerAttachments', () => {
|
||||
act(() => probe.root.unmount())
|
||||
})
|
||||
|
||||
it('rejects a mixed queued batch after the target becomes remote', async () => {
|
||||
// Ownership is per path: the target-owned drop still lands, the client-local
|
||||
// paste is refused, and the refusal is reported rather than hidden.
|
||||
it('keeps the owned half of a mixed queued batch after the target becomes remote', async () => {
|
||||
let composing = true
|
||||
const probe = await renderProbe('pty-1', false, { isComposing: () => composing })
|
||||
|
||||
@@ -221,6 +223,20 @@ describe('useNativeChatComposerAttachments', () => {
|
||||
composing = false
|
||||
act(() => probe.latest().flushPendingAttachments())
|
||||
|
||||
expect(probe.draft()).toBe('@/remote/trusted.txt ')
|
||||
expect(probe.notice()).toBe('Local attachments are not available for remote sessions.')
|
||||
act(() => probe.root.unmount())
|
||||
})
|
||||
|
||||
it('refuses a wholly client-local queued batch on a remote target', async () => {
|
||||
let composing = true
|
||||
const probe = await renderProbe('pty-1', false, { isComposing: () => composing })
|
||||
|
||||
act(() => probe.latest().attachResolvedPaths(['/local/untrusted.txt']))
|
||||
runtimeTarget.remote = true
|
||||
composing = false
|
||||
act(() => probe.latest().flushPendingAttachments())
|
||||
|
||||
expect(probe.draft()).toBe('')
|
||||
expect(probe.notice()).toBe('Local attachments are not available for remote sessions.')
|
||||
act(() => probe.root.unmount())
|
||||
|
||||
+22
-9
@@ -80,28 +80,41 @@ export function useNativeChatResolvedPathAttachments({
|
||||
|
||||
const applyResolvedPaths = useCallback(
|
||||
(resolvedPaths: ResolvedAttachmentPath[], focus: boolean, preserveNotice = false) => {
|
||||
const targetOwnership = resolvedPaths.map(({ targetOwnerIsCurrent }) =>
|
||||
targetOwnerIsCurrent ? targetOwnerIsCurrent() : null
|
||||
)
|
||||
if (targetOwnership.some((isCurrent) => isCurrent === false)) {
|
||||
if (resolvedPaths.length === 0) {
|
||||
return
|
||||
}
|
||||
if (resolvedPaths.some(({ targetOwnerIsCurrent }) => targetOwnerIsCurrent?.() === false)) {
|
||||
setNotice(nativeChatWorkspaceAttachmentMismatchNotice())
|
||||
return
|
||||
}
|
||||
if (attachmentTargetBlocked(targetOwnership.every((isCurrent) => isCurrent === true))) {
|
||||
// Ownership is per path, so the verdict is too: a queued batch can mix a
|
||||
// workspace drop the target owns with a client-local paste it does not,
|
||||
// and one verdict for the batch would refuse the drop the user can make.
|
||||
const owned = resolvedPaths.filter(({ targetOwnerIsCurrent }) => targetOwnerIsCurrent)
|
||||
const clientLocal = resolvedPaths.filter(({ targetOwnerIsCurrent }) => !targetOwnerIsCurrent)
|
||||
const ownedBlocked = owned.length > 0 && attachmentTargetBlocked(true)
|
||||
const clientLocalBlocked = clientLocal.length > 0 && attachmentTargetBlocked(false)
|
||||
const attachable = [
|
||||
...(ownedBlocked ? [] : owned),
|
||||
...(clientLocalBlocked ? [] : clientLocal)
|
||||
]
|
||||
if (attachable.length === 0) {
|
||||
noteAttachmentTargetBlocked()
|
||||
return
|
||||
}
|
||||
const imagePaths = resolvedPaths.filter(({ path }) => isNativeChatImageAttachmentPath(path))
|
||||
const filePaths = resolvedPaths
|
||||
const imagePaths = attachable.filter(({ path }) => isNativeChatImageAttachmentPath(path))
|
||||
const filePaths = attachable
|
||||
.filter(({ path }) => !isNativeChatImageAttachmentPath(path))
|
||||
.map(({ path }) => path)
|
||||
// Images ride along on submit so chips and the TUI input cannot diverge.
|
||||
appendImageAttachments(imagePaths.map(({ path, connectionId }) => ({ path, connectionId })))
|
||||
insertFileReferences(filePaths)
|
||||
if (!preserveNotice) {
|
||||
if (ownedBlocked || clientLocalBlocked) {
|
||||
noteAttachmentTargetBlocked()
|
||||
} else if (!preserveNotice) {
|
||||
setNotice(null)
|
||||
}
|
||||
if (focus && resolvedPaths.length > 0) {
|
||||
if (focus) {
|
||||
requestAnimationFrame(() => textareaRef.current?.focus())
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user