feat(desktop): Download button on preview file cards — save any delivered file via the authenticated backend bridge (works local and remote) (#97816)
This commit is contained in:
@@ -29,7 +29,10 @@ describe('markdown documents delivered via MEDIA', () => {
|
||||
|
||||
// PreviewAttachment renders an "open preview" toggle button; the old
|
||||
// MediaAttachment 'file' fallback rendered a bare "Open ..." anchor.
|
||||
expect(await screen.findByRole('button')).toBeTruthy()
|
||||
// Two buttons now: Download + Open preview (maintainer-requested).
|
||||
const buttons = await screen.findAllByRole('button')
|
||||
expect(buttons.length).toBe(2)
|
||||
expect(screen.getByText('Download')).toBeTruthy()
|
||||
expect(screen.queryByText(/^Loading /)).toBeNull()
|
||||
expect(screen.getByText('report.md')).toBeTruthy()
|
||||
})
|
||||
@@ -44,7 +47,9 @@ describe('markdown documents delivered via MEDIA', () => {
|
||||
|
||||
render(<MarkdownTextContent isRunning={false} text={`[archive.zip](${href})`} />)
|
||||
|
||||
expect(await screen.findByRole('button')).toBeTruthy()
|
||||
const buttons = await screen.findAllByRole('button')
|
||||
expect(buttons.length).toBe(2)
|
||||
expect(screen.getByText('Download')).toBeTruthy()
|
||||
expect(screen.getByText('archive.zip')).toBeTruthy()
|
||||
expect(screen.queryByText(/^Open archive/)).toBeNull()
|
||||
})
|
||||
@@ -54,7 +59,8 @@ describe('markdown documents delivered via MEDIA', () => {
|
||||
|
||||
render(<MarkdownTextContent isRunning={false} text={`[report.pdf](${href})`} />)
|
||||
|
||||
expect(await screen.findByRole('button')).toBeTruthy()
|
||||
const buttons = await screen.findAllByRole('button')
|
||||
expect(buttons.length).toBe(2)
|
||||
expect(screen.getByText('report.pdf')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useSessionView } from '@/app/chat/session-view'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { MonitorPlay } from '@/lib/icons'
|
||||
import { Download, MonitorPlay } from '@/lib/icons'
|
||||
import { normalizeOrLocalPreviewTarget } from '@/lib/local-preview'
|
||||
import { downloadGatewayMediaFile } from '@/lib/media'
|
||||
import { previewName } from '@/lib/preview-targets'
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import { $previewTabSources, closePreviewForSource, openPreview, type PreviewRecordSource } from '@/store/preview'
|
||||
@@ -16,6 +17,8 @@ export function PreviewAttachment({ source = 'manual', target }: { source?: Prev
|
||||
const cwd = useStore(useSessionView().$cwd)
|
||||
const openSources = useStore($previewTabSources)
|
||||
const [opening, setOpening] = useState(false)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [downloaded, setDownloaded] = useState(false)
|
||||
const cwdRef = useRef(cwd)
|
||||
const mountedRef = useRef(false)
|
||||
const requestTokenRef = useRef(0)
|
||||
@@ -94,6 +97,34 @@ export function PreviewAttachment({ source = 'manual', target }: { source?: Prev
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile() {
|
||||
if (downloading) {
|
||||
return
|
||||
}
|
||||
|
||||
setDownloading(true)
|
||||
|
||||
try {
|
||||
// Works in both modes: the Electron main process fetches the bytes
|
||||
// through the session's backend connection (local gateway or remote)
|
||||
// and prompts for a save location.
|
||||
const result = await downloadGatewayMediaFile(target)
|
||||
|
||||
if (mountedRef.current && result.saved) {
|
||||
setDownloaded(true)
|
||||
setTimeout(() => mountedRef.current && setDownloaded(false), 2000)
|
||||
}
|
||||
} catch (error) {
|
||||
if (mountedRef.current) {
|
||||
notifyError(error, t.fileMenu.downloadFailed)
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setDownloading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-160 items-center gap-2 rounded-lg border border-(--ui-stroke-tertiary) bg-card/55 px-2.5 py-1.5 text-sm">
|
||||
<span className="grid size-6 shrink-0 place-items-center rounded-md bg-muted/55 text-muted-foreground/85">
|
||||
@@ -102,6 +133,17 @@ export function PreviewAttachment({ source = 'manual', target }: { source?: Prev
|
||||
<span className="min-w-0 flex-1 truncate text-[0.78rem] font-medium text-foreground/90" title={target}>
|
||||
{name}
|
||||
</span>
|
||||
<button
|
||||
aria-label={t.fileMenu.download}
|
||||
className="flex shrink-0 items-center gap-1 rounded-md border border-(--ui-stroke-tertiary) bg-background/40 px-2 py-1 text-[0.7rem] font-medium text-muted-foreground transition-colors hover:bg-accent/55 hover:text-foreground disabled:opacity-50"
|
||||
disabled={downloading}
|
||||
onClick={() => void downloadFile()}
|
||||
title={t.fileMenu.download}
|
||||
type="button"
|
||||
>
|
||||
<Download className="size-3" />
|
||||
{downloaded ? t.fileMenu.downloadSaved : t.fileMenu.download}
|
||||
</button>
|
||||
<button
|
||||
className="shrink-0 rounded-md border border-(--ui-stroke-tertiary) bg-background/40 px-2 py-1 text-[0.7rem] font-medium text-muted-foreground transition-colors hover:bg-accent/55 hover:text-foreground disabled:opacity-50"
|
||||
disabled={opening}
|
||||
|
||||
Reference in New Issue
Block a user