fix(file-explorer): report the saved filename in the download toast (#12959)

* fix(file-explorer): report the saved filename in the download toast

The success toast named the remote node, so renaming a file in the native
save dialog left the label disagreeing with its own Open action, which
opens the real destination. Folder downloads had the same gap whenever
sanitizeLocalDownloadFilename rewrote the remote basename.

* fix(file-explorer): respect local download path semantics

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
This commit is contained in:
seoo2001 2026-08-07 11:50:38 -07:00 committed by GitHub
parent a77002c42b
commit 6382b8a053
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 52 additions and 5 deletions

View File

@ -293,6 +293,14 @@ function makeToolbar(overrides: Partial<Parameters<typeof FileExplorerToolbar>[0
})
}
function setDownloadPlatform(platform: NodeJS.Platform): void {
;(
window as unknown as {
api: { platform: { get: () => { platform: NodeJS.Platform } } }
}
).api.platform = { get: () => ({ platform }) }
}
beforeEach(() => {
toastErrorMock.mockReset()
toastSuccessMock.mockReset()
@ -677,7 +685,10 @@ describe('FileExplorerRow collapse folder action', () => {
it('calls the preload download API and shows success only when not canceled', async () => {
const downloadFile = vi
.fn()
.mockResolvedValueOnce({ canceled: false, destinationPath: '/downloads/index.ts' })
.mockResolvedValueOnce({
canceled: false,
destinationPath: '/downloads/renamed\\entry.ts'
})
.mockResolvedValueOnce({ canceled: true })
const openPath = vi.fn().mockResolvedValue(undefined)
;(
@ -690,6 +701,7 @@ describe('FileExplorerRow collapse folder action', () => {
}
}
).window = { api: { fs: { downloadFile }, shell: { openPath } } }
setDownloadPlatform('linux')
await downloadRemoteFile(fileNode, 'ssh-1')
await downloadRemoteFile(fileNode, 'ssh-1')
@ -699,7 +711,7 @@ describe('FileExplorerRow collapse folder action', () => {
connectionId: 'ssh-1'
})
expect(toastSuccessMock).toHaveBeenCalledTimes(1)
expect(toastSuccessMock).toHaveBeenCalledWith("Downloaded 'index.ts'", {
expect(toastSuccessMock).toHaveBeenCalledWith("Downloaded 'renamed\\entry.ts'", {
action: {
label: 'Open',
onClick: expect.any(Function)
@ -709,10 +721,30 @@ describe('FileExplorerRow collapse folder action', () => {
| { onClick: () => void }
| undefined
action?.onClick()
expect(openPath).toHaveBeenCalledWith('/downloads/index.ts')
expect(openPath).toHaveBeenCalledWith('/downloads/renamed\\entry.ts')
expect(toastErrorMock).not.toHaveBeenCalled()
})
it('reports the saved folder name from a Windows destination path', async () => {
const downloadFolder = vi.fn().mockResolvedValue({
canceled: false,
destinationPath: 'C:\\Users\\dev\\Downloads\\src-copy'
})
;(
globalThis as unknown as {
window: { api: { fs: { downloadFolder: typeof downloadFolder } } }
}
).window = { api: { fs: { downloadFolder } } }
setDownloadPlatform('win32')
await downloadRemoteFile(directoryNode, 'ssh-1')
expect(toastSuccessMock).toHaveBeenCalledWith(
"Downloaded folder 'src-copy'",
expect.objectContaining({ action: expect.anything() })
)
})
it('calls the preload folder download API for SSH directory rows', async () => {
const downloadFolder = vi.fn().mockResolvedValue({
canceled: false,
@ -729,6 +761,7 @@ describe('FileExplorerRow collapse folder action', () => {
}
}
).window = { api: { fs: { downloadFolder }, shell: { openPath } } }
setDownloadPlatform('linux')
await downloadRemoteFile(directoryNode, 'ssh-1')
@ -765,6 +798,7 @@ describe('FileExplorerRow collapse folder action', () => {
}
}
).window = { api: { shell: { openPath } } }
setDownloadPlatform('linux')
await downloadRemoteFile(fileNode, runtimeContext)

View File

@ -342,6 +342,14 @@ export function shouldShowCopyFileAction(
)
}
function getLocalDownloadName(destinationPath: string, platform: NodeJS.Platform): string {
const lastSeparatorIndex =
platform === 'win32'
? Math.max(destinationPath.lastIndexOf('/'), destinationPath.lastIndexOf('\\'))
: destinationPath.lastIndexOf('/')
return destinationPath.slice(lastSeparatorIndex + 1)
}
export async function downloadRemoteFile(
node: TreeNode,
connectionIdOrRuntimeContext: string | RuntimeFileOperationArgs
@ -363,17 +371,22 @@ export async function downloadRemoteFile(
if (result.canceled) {
return
}
// Why: POSIX permits backslashes in saved names; only Windows treats them as separators.
const savedName = getLocalDownloadName(
result.destinationPath,
window.api.platform.get().platform
)
toast.success(
node.isDirectory
? translate(
'auto.components.right.sidebar.FileExplorerRow.a4029c996b',
"Downloaded folder '{{value0}}'",
{ value0: node.name }
{ value0: savedName }
)
: translate(
'auto.components.right.sidebar.FileExplorerRow.bce4d4e44f',
"Downloaded '{{value0}}'",
{ value0: node.name }
{ value0: savedName }
),
{
action: {