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:
Teknium
2026-08-29 03:34:04 -07:00
committed by GitHub
parent 71c823bbf7
commit 3b362acf48
2 changed files with 52 additions and 4 deletions

View File

@@ -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()
})
})

View File

@@ -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}