From 6dafde48e7ea2b5fbf7e6940e9114c0958213e92 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sun, 11 Oct 2026 14:56:50 -0700 Subject: [PATCH] refactor(renderer): no client SSH operation arms in project creation or native chat (#27593) * refactor(renderer): no client SSH operation arms in project creation or native chat * fix(add-project): show the start step when the remote step loses its server --------- Co-authored-by: m4air --- config/scripts/locale-ko-key-overrides.json | 9 - .../NativeChatComposer.paste-remount.test.tsx | 31 +-- .../native-chat-attachment-upload.test.ts | 142 +------------- .../native-chat-attachment-upload.ts | 90 +-------- .../native-chat-clipboard-image-save.ts | 14 +- .../native-chat-composer-drop-scope.test.tsx | 50 ++--- ...ative-chat-composer-ime-file-drop.test.tsx | 20 +- ...chat-composer-workspace-file-drop.test.tsx | 69 +------ ...-external-attachments-destination.test.tsx | 54 +----- .../native-chat-image-runtime-context.test.ts | 12 ++ .../native-chat-image-runtime-context.ts | 7 +- ...ative-chat-resolved-path-ownership.test.ts | 28 +-- .../native-chat-resolved-path-ownership.ts | 14 +- .../use-native-chat-composer-paste.test.tsx | 91 +++------ .../use-native-chat-composer-paste.ts | 7 +- ...-native-chat-external-attachments.test.tsx | 172 +--------------- .../use-native-chat-external-attachments.ts | 21 +- ...e-native-chat-menu-paste-lifetime.test.tsx | 148 ++++++-------- .../use-native-chat-paste-lifetime.test.tsx | 14 +- .../use-native-chat-paste-lifetime.ts | 6 +- .../use-native-chat-workspace-file-drop.ts | 10 +- .../file-explorer-add-project-action.test.ts | 23 +-- .../file-explorer-add-project-action.ts | 12 +- .../AddProjectFromFolderDialog.test.tsx | 68 +------ .../sidebar/AddProjectFromFolderDialog.tsx | 48 +---- .../components/sidebar/AddRepoCloneStep.tsx | 39 ++-- .../components/sidebar/AddRepoCreateStep.tsx | 12 +- .../src/components/sidebar/AddRepoDialog.tsx | 31 +-- .../sidebar/AddRepoDialogStepContent.test.tsx | 118 +---------- .../sidebar/AddRepoDialogStepContent.tsx | 39 +--- .../components/sidebar/AddRepoRemoteStep.tsx | 176 +++++------------ .../sidebar/AddRepoStartSteps.test.tsx | 118 ++--------- .../components/sidebar/AddRepoStartSteps.tsx | 11 +- .../AddRepoSteps.default-checkout.test.ts | 156 +++------------ .../src/components/sidebar/AddRepoSteps.tsx | 183 ++++-------------- .../sidebar/CreateProjectLocationField.tsx | 4 +- .../sidebar/NonGitFolderDialog.test.tsx | 96 +++++---- .../components/sidebar/NonGitFolderDialog.tsx | 38 ++-- .../sidebar/add-repo-browse-authority.test.ts | 19 +- .../sidebar/add-repo-browse-authority.ts | 5 - .../sidebar/add-repo-host-availability.ts | 3 +- .../sidebar/add-repo-local-start-actions.ts | 74 ++----- .../sidebar/add-repo-store-upsert.ts | 8 +- .../components/sidebar/clone-defaults.test.ts | 13 -- .../src/components/sidebar/clone-defaults.ts | 4 +- .../use-add-repo-host-selection.test.ts | 22 ++- .../sidebar/use-add-repo-host-selection.ts | 10 +- .../use-add-repo-hosted-controller.test.ts | 22 +-- .../sidebar/use-add-repo-hosted-controller.ts | 17 +- .../use-add-repo-remote-nested-scan.ts | 62 ------ .../sidebar/useAddRepoCloneFlow.test.ts | 68 +------ .../components/sidebar/useAddRepoCloneFlow.ts | 40 ++-- .../useAddRepoNestedReviewController.ts | 10 +- .../sidebar/useCreateProjectDefaults.test.ts | 13 -- .../sidebar/useCreateProjectDefaults.ts | 31 ++- .../useCreateRepo.default-checkout.test.ts | 37 +--- .../src/components/sidebar/useCreateRepo.ts | 43 ++-- src/renderer/src/i18n/locales/en.json | 18 +- src/renderer/src/i18n/locales/es.json | 20 +- src/renderer/src/i18n/locales/fr.json | 18 +- src/renderer/src/i18n/locales/ja.json | 20 +- src/renderer/src/i18n/locales/ko.json | 20 +- src/renderer/src/i18n/locales/zh.json | 20 +- .../src/runtime/runtime-client-target.ts | 5 +- .../projects/project-host-setup-actions.ts | 60 +++--- src/renderer/src/store/slices/repos.test.ts | 51 +---- 66 files changed, 579 insertions(+), 2335 deletions(-) delete mode 100644 src/renderer/src/components/sidebar/use-add-repo-remote-nested-scan.ts diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json index b7ec782b42b..b1195ea2064 100644 --- a/config/scripts/locale-ko-key-overrides.json +++ b/config/scripts/locale-ko-key-overrides.json @@ -4403,15 +4403,6 @@ "auto.components.sidebar.add.repo.local.start.actions.runtimeBrowseDescription": { "ko": "이 호스트의 기존 Git 리포지토리 또는 폴더" }, - "auto.components.sidebar.add.repo.local.start.actions.sshBrowseDescription": { - "ko": "이 SSH 호스트의 기존 Git 리포지토리 또는 폴더" - }, - "auto.components.sidebar.add.repo.local.start.actions.sshBrowseTitle": { - "ko": "SSH 호스트에서 프로젝트 열기" - }, - "auto.components.sidebar.add.repo.local.start.actions.sshCreateUnavailable": { - "ko": "아직 SSH 호스트에서는 사용할 수 없습니다." - }, "auto.components.sidebar.index.b826a98b6f": { "ko": "사용 중" }, diff --git a/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx index 6357b604916..c9bb62b6b26 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx @@ -304,25 +304,10 @@ it('keeps a local image paste after the real composer closes and reopens', async await waitFor(() => expect(mocks.state.fieldProps?.imageAttachments?.[0]?.pending).toBeFalsy()) }) -it.each([ - { kind: 'local', acceptsImages: false }, - { kind: 'ssh', acceptsImages: false }, - { kind: 'local', acceptsImages: true }, - { kind: 'ssh', acceptsImages: true } -] as const)( - 'can remove a stalled $kind paste after reopen with acceptsImages=$acceptsImages', - async ({ kind, acceptsImages }) => { - mocks.state.owner = - kind === 'local' - ? { kind: 'local' } - : { - kind: 'ssh', - connectionId: 'ssh-1', - worktreePath: '/remote/folder', - expectedExecutionHostId: 'ssh:ssh-1', - expectedSshTargetId: 'ssh-1', - expectedSshConnectionGeneration: 1 - } +it.each([false, true])( + 'can remove a stalled paste after reopen with acceptsImages=%s', + async (acceptsImages) => { + mocks.state.owner = { kind: 'local' } vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] }) const finishUpload = holdUpload() writeNativeChatDraftCache(PANE, 'look at this') @@ -338,13 +323,7 @@ it.each([ fireEvent.click(screen.getByRole('button', { name: 'Remove attachment' })) ) expect(mocks.state.fieldProps?.sendButtonDisabled).toBe(false) - await act(async () => - finishUpload( - kind === 'local' - ? '/local/native-chat-pastes/orca-paste-1-image.png' - : '/tmp/orca-paste-1-image.png' - ) - ) + await act(async () => finishUpload('/local/native-chat-pastes/orca-paste-1-image.png')) expect(readNativeChatAttachmentCache(PANE)).toEqual([]) expect(readNativeChatDraftCache(PANE)).toBe('look at this') expect(mocks.state.fieldProps?.imageAttachments).toEqual([]) diff --git a/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts b/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts index e94dbad20d5..94d9ea6816e 100644 --- a/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts @@ -5,25 +5,11 @@ import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { getDefaultSettings } from '../../../../shared/constants' const mocks = vi.hoisted(() => ({ - toastLoading: vi.fn(() => 'toast-1'), - toastDismiss: vi.fn(), - toastError: vi.fn(), - toastMessage: vi.fn(), - resolveDroppedPathsForAgent: vi.fn(), callRuntimeRpc: vi.fn() })) vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc })) -vi.mock('sonner', () => ({ - toast: { - loading: mocks.toastLoading, - dismiss: mocks.toastDismiss, - error: mocks.toastError, - message: mocks.toastMessage - } -})) - vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string, values?: Record) => fallback.replace(/\{\{(\w+)\}\}/g, (_match, name: string) => String(values?.[name])) @@ -34,8 +20,7 @@ import { prepareNativeChatSessionAttachmentUpload, resolveNativeChatAttachmentOwner, resolveNativeChatAttachmentOwnerForWorktree, - resolveNativeChatRuntimeSessionAttachmentOwner, - uploadNativeChatAttachmentPaths + resolveNativeChatRuntimeSessionAttachmentOwner } from './native-chat-attachment-upload' import { replaceRuntimeEnvironmentRevisions } from '@/runtime/runtime-environment-revision' import { AGENT_SESSION_ATTACHMENTS_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' @@ -95,42 +80,15 @@ describe('resolveNativeChatAttachmentOwner', () => { }) }) - it('resolves a structured SSH owner directly from its worktree', () => { - expect( - resolveNativeChatAttachmentOwnerForWorktree( - state({ - repos: [{ id: 'repo', connectionId: 'conn-1' }] as never, - sshConnectionStates: new Map([['conn-1', { connectionGeneration: 4 } as never]]) - }), - 'wt-1' - ) - ).toMatchObject({ - kind: 'ssh', - connectionId: 'conn-1', - worktreePath: '/repo/worktree' + it('refuses client files for an SSH-owned worktree, whatever its connection state', () => { + const sshState = state({ repos: [repoFixture({ connectionId: 'conn-1' })] }) + expect(resolveNativeChatAttachmentOwner(sshState, 'tab-1')).toEqual({ kind: 'remote' }) + expect(resolveNativeChatAttachmentOwnerForWorktree(sshState, 'wt-1')).toEqual({ + kind: 'remote' }) }) - it('resolves an SSH repo worktree to ssh with the worktree path', () => { - expect( - resolveNativeChatAttachmentOwner( - state({ - repos: [{ id: 'repo', connectionId: 'conn-1' }] as never, - sshConnectionStates: new Map([['conn-1', { connectionGeneration: 4 } as never]]) - }), - 'tab-1' - ) - ).toEqual({ - kind: 'ssh', - connectionId: 'conn-1', - worktreePath: '/repo/worktree', - expectedExecutionHostId: 'ssh:conn-1', - expectedSshTargetId: 'conn-1', - expectedSshConnectionGeneration: 4 - }) - }) - - it('resolves a runtime-owned repo to runtime', () => { + it('refuses client files for a runtime-owned repo', () => { expect( resolveNativeChatAttachmentOwner( state({ @@ -138,7 +96,7 @@ describe('resolveNativeChatAttachmentOwner', () => { }), 'tab-1' ) - ).toEqual({ kind: 'runtime' }) + ).toEqual({ kind: 'remote' }) }) it('keeps a recorded local workspace local when a runtime is focused', () => { @@ -161,90 +119,6 @@ describe('resolveNativeChatAttachmentOwner', () => { kind: 'not-ready' }) }) - - it('reports not-ready instead of throwing when the SSH generation is gone', () => { - expect( - resolveNativeChatAttachmentOwner( - state({ - repos: [{ id: 'repo', connectionId: 'conn-1' }] as never, - sshConnectionStates: new Map() - }), - 'tab-1' - ) - ).toEqual({ kind: 'not-ready' }) - }) - - it('reports not-ready when an SSH worktree has no known path yet', () => { - expect( - resolveNativeChatAttachmentOwner( - state({ - repos: [{ id: 'repo', connectionId: 'conn-1' }] as never, - worktreesByRepo: { repo: [{ id: 'wt-1', repoId: 'repo' } as never] }, - tabsByWorktree: { 'wt-1': [terminalTab()] } - }), - 'tab-1' - ) - ).toEqual({ kind: 'not-ready' }) - }) -}) - -describe('uploadNativeChatAttachmentPaths', () => { - const owner = { - kind: 'ssh' as const, - connectionId: 'conn-1', - worktreePath: '/remote/worktree', - expectedExecutionHostId: 'ssh:conn-1' as const, - expectedSshTargetId: 'conn-1', - expectedSshConnectionGeneration: 4 - } - - beforeEach(() => { - vi.clearAllMocks() - vi.stubGlobal('window', { - api: { fs: { resolveDroppedPathsForAgent: mocks.resolveDroppedPathsForAgent } } - }) - }) - - it('uploads through the terminal drop resolver and returns remote paths', async () => { - mocks.resolveDroppedPathsForAgent.mockResolvedValue({ - resolvedPaths: ['/remote/worktree/.orca/drops/a.txt'], - skipped: [], - failed: [] - }) - await expect(uploadNativeChatAttachmentPaths(['/local/a.txt'], owner)).resolves.toEqual([ - '/remote/worktree/.orca/drops/a.txt' - ]) - expect(mocks.resolveDroppedPathsForAgent).toHaveBeenCalledWith({ - paths: ['/local/a.txt'], - worktreePath: '/remote/worktree', - connectionId: 'conn-1', - expectedExecutionHostId: 'ssh:conn-1', - expectedSshTargetId: 'conn-1', - expectedSshConnectionGeneration: 4 - }) - expect(mocks.toastLoading).toHaveBeenCalledTimes(1) - expect(mocks.toastDismiss).toHaveBeenCalledWith('toast-1') - }) - - it('surfaces per-file skips and failures through the shared drop toasts', async () => { - mocks.resolveDroppedPathsForAgent.mockResolvedValue({ - resolvedPaths: [], - skipped: [{ sourcePath: '/local/link', reason: 'symlink' }], - failed: [{ sourcePath: '/local/b.txt', reason: 'boom' }] - }) - await expect( - uploadNativeChatAttachmentPaths(['/local/link', '/local/b.txt'], owner) - ).resolves.toEqual([]) - expect(mocks.toastMessage).toHaveBeenCalledTimes(1) - expect(mocks.toastError).toHaveBeenCalledTimes(1) - }) - - it('returns null and reports when the upload IPC fails', async () => { - mocks.resolveDroppedPathsForAgent.mockRejectedValue(new Error('sftp down')) - await expect(uploadNativeChatAttachmentPaths(['/local/a.txt'], owner)).resolves.toBeNull() - expect(mocks.toastError).toHaveBeenCalledTimes(1) - expect(mocks.toastDismiss).toHaveBeenCalledWith('toast-1') - }) }) describe('a structured chat on a paired server', () => { diff --git a/src/renderer/src/components/native-chat/native-chat-attachment-upload.ts b/src/renderer/src/components/native-chat/native-chat-attachment-upload.ts index 0ca498fa617..68a4351006c 100644 --- a/src/renderer/src/components/native-chat/native-chat-attachment-upload.ts +++ b/src/renderer/src/components/native-chat/native-chat-attachment-upload.ts @@ -1,16 +1,12 @@ // Host-aware resolution for composer attachments (STA-1465). The composer's // attach surfaces (file drop, file picker, image paste) receive client-local -// paths, but an SSH worktree's agent runs on the remote host — local paths must -// be uploaded first, exactly like terminal drops (docs/terminal-drop-ssh.md). A -// structured chat on a paired server uploads into that server's attachment store. +// paths. A structured chat on a paired server uploads into that server's +// attachment store; any other remote chat refuses client files. -import { toast } from 'sonner' import { translate } from '@/i18n/i18n' -import { extractIpcErrorMessage } from '@/lib/ipc-error' import { findKnownWorktreeById } from '@/store/slices/worktrees/listing/detected-worktree-meta' import { getConnectionExecutionHostId, - parseExecutionHostId, toRuntimeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' @@ -30,19 +26,8 @@ import type { AgentSessionAttachmentUploadTarget } from '../../../../shared/agent-session-attachments' import type { AppState } from '@/store/types' -import { reportTerminalDropUploadSkipsAndFailures } from '../terminal-pane/terminal-drop-upload-report' import { NATIVE_FILE_DROP_MAX_PATHS } from '../../../../shared/native-file-drop' import { findTerminalTabWorktreeId } from './native-chat-file-link' -import { - captureDirectSshMutationExpectation, - type DirectSshMutationExpectation -} from '@/lib/ssh-mutation-expectation' - -export type NativeChatSshAttachmentOwner = DirectSshMutationExpectation & { - kind: 'ssh' - connectionId: string - worktreePath: string -} /** A structured chat on a paired server: files upload into that server's attachment store, which * checks every stored path a message names when it admits the message. */ @@ -55,11 +40,11 @@ export type NativeChatRuntimeSessionAttachmentOwner = { export type NativeChatAttachmentOwner = | { kind: 'local' } - | NativeChatSshAttachmentOwner | NativeChatRuntimeSessionAttachmentOwner - /** A terminal-backed chat on a paired server: its agent may run on that server's SSH or WSL - * host, which the server's store cannot reach, so it keeps refusing client files. */ - | { kind: 'runtime' } + /** Refuses client files: a terminal-backed chat on a paired server (its agent may run on that + * server's SSH or WSL host, which the server's store cannot reach), or an `ssh:` host, which + * only its paired server serves. */ + | { kind: 'remote' } /** Store not hydrated / worktree unknown. Callers must not attach local * paths in this window — the worktree may turn out to be remote, and the * agent would silently receive paths it cannot read (see #6648). */ @@ -118,35 +103,10 @@ export function resolveNativeChatAttachmentOwnerForWorktree( if (!hostId) { return { kind: 'not-ready' } } - if (parseExecutionHostId(hostId)?.kind === 'runtime') { - return { kind: 'runtime' } - } if (hostId === 'local') { return { kind: 'local' } } - const connectionId = getConnectionIdFromState(state, worktreeId) - if (connectionId === undefined) { - return { kind: 'not-ready' } - } - if (connectionId === null) { - return { kind: 'not-ready' } - } - const worktreePath = findKnownWorktreeById(state, worktreeId, hostId)?.path - if (!worktreePath) { - return { kind: 'not-ready' } - } - try { - return { - kind: 'ssh', - connectionId, - worktreePath, - ...captureDirectSshMutationExpectation(state, connectionId) - } - } catch { - // The connection's generation is gone (disconnect mid-attach). That is an - // unknown owner, not a reason to throw out of the drop/IME handler. - return { kind: 'not-ready' } - } + return { kind: 'remote' } } /** The chat behind a structured composer, as the attachment owner needs it. */ @@ -220,42 +180,6 @@ export function nativeChatTooManyAttachmentsNotice(): string { export { nativeChatLocalAttachmentUnsupportedNotice } from './native-chat-composer-target' -/** - * Upload client-local paths into `${worktreePath}/.orca/drops` on the SSH - * remote and return the remote paths the agent can read (input order - * preserved). Returns null when the upload IPC itself failed; per-file - * skips/failures surface through the shared drop toasts. - */ -export async function uploadNativeChatAttachmentPaths( - paths: string[], - owner: NativeChatSshAttachmentOwner -): Promise { - const pending = toast.loading( - translate( - 'components.native-chat.composer.uploadingAttachments', - 'Uploading {{value0}} file(s) to remote…', - { value0: paths.length } - ) - ) - try { - const { resolvedPaths, skipped, failed } = await window.api.fs.resolveDroppedPathsForAgent({ - paths, - worktreePath: owner.worktreePath, - connectionId: owner.connectionId, - expectedExecutionHostId: owner.expectedExecutionHostId, - expectedSshTargetId: owner.expectedSshTargetId, - expectedSshConnectionGeneration: owner.expectedSshConnectionGeneration - }) - reportTerminalDropUploadSkipsAndFailures(skipped, failed) - return resolvedPaths - } catch (err) { - toast.error(extractIpcErrorMessage(err, 'Failed to upload files.')) - return null - } finally { - toast.dismiss(pending) - } -} - /** * Ask the chat's server, right before uploading, whether it keeps chat attachments, and which * server process will receive the bytes. An older server gets the update notice instead of an diff --git a/src/renderer/src/components/native-chat/native-chat-clipboard-image-save.ts b/src/renderer/src/components/native-chat/native-chat-clipboard-image-save.ts index d0fd3b9268d..1e65eb59caa 100644 --- a/src/renderer/src/components/native-chat/native-chat-clipboard-image-save.ts +++ b/src/renderer/src/components/native-chat/native-chat-clipboard-image-save.ts @@ -6,20 +6,17 @@ import { type NativeChatAttachmentOwner } from './native-chat-attachment-upload' -type ClipboardImageOwner = Extract< - NativeChatAttachmentOwner, - { kind: 'local' | 'ssh' | 'runtime-session' } -> +type ClipboardImageOwner = Extract /** Owners whose attachment path is a file this client can write right now. */ export function ownerAcceptsClipboardImage( owner: NativeChatAttachmentOwner ): owner is ClipboardImageOwner { - return owner.kind === 'local' || owner.kind === 'ssh' || owner.kind === 'runtime-session' + return owner.kind === 'local' || owner.kind === 'runtime-session' } -/** Where the clipboard image is saved: this machine, the SSH host, or the chat's store on its - * paired server, after asking that server whether it keeps one. */ +/** Where the clipboard image is saved: this machine, or the chat's store on its paired server, + * after asking that server whether it keeps one. */ async function clipboardImageSaveArgs( owner: ClipboardImageOwner ): Promise< @@ -29,9 +26,6 @@ async function clipboardImageSaveArgs( if (owner.kind === 'local') { return { ok: true, args: { forNativeChatDraft: true } } } - if (owner.kind === 'ssh') { - return { ok: true, args: { connectionId: owner.connectionId } } - } const prepared = await prepareNativeChatSessionAttachmentUpload(owner) if (!prepared.ok) { return { ...prepared, cause: 'serverTooOld' } diff --git a/src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx b/src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx index a4a2ca227ce..a769887d443 100644 --- a/src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx +++ b/src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx @@ -25,20 +25,21 @@ const electron = vi.hoisted(() => ({ getPathForFile: vi.fn((file: File) => `/repro/${file.name}`) })) -const intake = vi.hoisted(() => ({ - owner: { kind: 'local' } as { kind: string; connectionId?: string }, - stat: vi.fn(), - readFile: vi.fn(), - upload: vi.fn(), - prepare: vi.fn(async ({ paths }: { paths: string[] }) => ({ paths, failures: [] })), - pick: vi.fn() -})) +const intake = vi.hoisted(() => { + const owner: { kind: string } = { kind: 'local' } + return { + owner, + stat: vi.fn(), + readFile: vi.fn(), + prepare: vi.fn(async ({ paths }: { paths: string[] }) => ({ paths, failures: [] })), + pick: vi.fn() + } +}) vi.mock('@/store', () => ({ useAppStore: { getState: () => ({ tabsByWorktree: {} }) } })) // Keeps the real notice strings so the silent-failure guards assert what users see. vi.mock('./native-chat-attachment-upload', async (importOriginal) => ({ ...(await importOriginal()), - resolveNativeChatAttachmentOwner: () => intake.owner, - uploadNativeChatAttachmentPaths: intake.upload + resolveNativeChatAttachmentOwner: () => intake.owner })) vi.mock('electron', () => ({ @@ -182,7 +183,6 @@ describe('native chat composer drop scoping', () => { electron.getPathForFile.mockReset().mockImplementation((file: File) => `/repro/${file.name}`) intake.stat.mockReset().mockResolvedValue(undefined) intake.readFile.mockReset().mockResolvedValue({ content: '', isBinary: false }) - intake.upload.mockReset() intake.prepare.mockReset().mockImplementation(async ({ paths }) => ({ paths, failures: [] })) intake.pick.mockReset() vi.stubGlobal('IntersectionObserver', undefined) @@ -232,10 +232,9 @@ describe('native chat composer drop scoping', () => { intake.prepare.mockImplementationOnce(() => gate.promise) const view = render() await dropTwoImages(view.container.querySelector('.ProseMirror')!) - intake.owner = { kind: 'ssh', connectionId: 'other-host' } + intake.owner = { kind: 'remote' } await act(async () => gate.resolve({ paths: ['/prepared/a.png'], failures: [] })) expect(intake.stat).not.toHaveBeenCalled() - expect(intake.upload).not.toHaveBeenCalled() expect(readNativeChatAttachmentCache('chat-a')).toEqual([]) }) it('refuses preparation captured for a draft that changed in the same pane', async () => { @@ -281,7 +280,7 @@ describe('native chat composer drop scoping', () => { it('notices an OS drop whose owner changes while checking the files', async () => { intake.stat.mockImplementation(async () => { - intake.owner = { kind: 'ssh', connectionId: 'conn-1' } + intake.owner = { kind: 'remote' } }) const view = render() @@ -415,33 +414,10 @@ describe('native chat composer drop scoping', () => { [{ filePath: '/repro/second.png', access: { kind: 'user-file' } }] ]) expect(intake.readFile).toHaveBeenCalledTimes(2) - expect(intake.upload).not.toHaveBeenCalled() expect(readNativeChatAttachmentCache('chat-a')).toHaveLength(2) expect(readNativeChatAttachmentCache('chat-b')).toEqual([]) await expect(intake.readFile({ filePath: '/repro/sibling.png' })).rejects.toThrow( 'Access denied' ) }) - - it('uploads once for the SSH drop owner without checking remote paths locally', async () => { - intake.owner = { kind: 'ssh', connectionId: 'conn-1' } - intake.upload.mockResolvedValue(['/remote/first.png', '/remote/second.png']) - const view = render( - <> - -