fix(desktop): render remote PDFs in preview rail

PDFs were classified as generic binary/text previews, rendering raw %PDF
bytes locally and failing entirely for remote-only files. Classify PDFs as
their own preview kind, load bytes through the existing local/remote
filesystem bridge, convert them to revocable Blob URLs for Chromium's
embedded viewer, migrate persisted pre-PDF tabs at restore, and retry
restored previews when the active filesystem connection changes.

Salvaged from #76008-era base onto current main: PDF classification now
composes with the remote-HTML enrichment branch, and the persisted-tab
migration runs before the One-Browser URL rekey in decodePreviewTabs.

Supersedes #76565.

Co-authored-by: Brooklyn Nicholson <brooklyn@brooklyn.sh>
This commit is contained in:
Matt Prusak 2026-08-01 22:56:11 -05:00 committed by Brooklyn Nicholson
parent c8648278c3
commit c8fdc51740
9 changed files with 507 additions and 27 deletions

View File

@ -862,6 +862,7 @@ const MEDIA_MIME_TYPES = {
'.mp4': 'video/mp4',
'.ogg': 'audio/ogg',
'.opus': 'audio/ogg; codecs=opus',
'.pdf': 'application/pdf',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
@ -870,6 +871,7 @@ const MEDIA_MIME_TYPES = {
}
const PREVIEW_HTML_EXTENSIONS = new Set(['.html', '.htm'])
const PREVIEW_PDF_EXTENSIONS = new Set(['.pdf'])
const PREVIEW_WATCH_DEBOUNCE_MS = 120
const LOCAL_PREVIEW_HOSTS = new Set(['0.0.0.0', '127.0.0.1', '::1', '[::1]', 'localhost'])
const TEXT_PREVIEW_MAX_BYTES = 512 * 1024
@ -4904,7 +4906,8 @@ async function previewFileTarget(rawTarget, baseDir) {
const metadata = previewFileMetadata(resolved, mimeType)
const isHtml = PREVIEW_HTML_EXTENSIONS.has(ext)
const isImage = mimeType.startsWith('image/')
const previewKind = isHtml ? 'html' : isImage ? 'image' : metadata.binary ? 'binary' : 'text'
const isPdf = PREVIEW_PDF_EXTENSIONS.has(ext) || mimeType === 'application/pdf'
const previewKind = isHtml ? 'html' : isImage ? 'image' : isPdf ? 'pdf' : metadata.binary ? 'binary' : 'text'
return {
binary: metadata.binary,

View File

@ -1,3 +1,4 @@
import { useStore } from '@nanostores/react'
import type * as React from 'react'
import type {
ComponentProps,
@ -23,6 +24,7 @@ import { Tip } from '@/components/ui/tooltip'
import { translateNow, useI18n } from '@/i18n'
import {
desktopFileDiff,
desktopFsCacheKey,
desktopGitRoot,
readDesktopFileDataUrl,
readDesktopFileText,
@ -33,7 +35,7 @@ import { shikiLanguageForFilename } from '@/lib/markdown-code'
import { cn } from '@/lib/utils'
import type { PreviewTarget } from '@/store/preview'
import { setPreviewDirty } from '@/store/preview-edit'
import { $currentCwd } from '@/store/session'
import { $connection, $currentCwd } from '@/store/session'
import { notifyWorkspaceChanged } from '@/store/workspace-events'
const SHIKI_THEME = { dark: 'github-dark-default', light: 'github-light-default' } as const
@ -217,6 +219,45 @@ function looksBinaryBytes(bytes: Uint8Array) {
return suspicious / Math.min(bytes.length, 4096) > 0.12
}
function dataUrlToBlob(dataUrl: string) {
const comma = dataUrl.indexOf(',')
if (comma < 0 || !dataUrl.startsWith('data:')) {
throw new Error('Invalid PDF data URL')
}
const metadata = dataUrl
.slice(5, comma)
.split(';')
.map(part => part.trim().toLowerCase())
const payload = dataUrl.slice(comma + 1)
if (metadata[0] !== 'application/pdf' || !metadata.slice(1).includes('base64')) {
throw new Error('Invalid PDF data URL type')
}
let binary: string
try {
binary = atob(decodeURIComponent(payload))
} catch {
throw new Error('Invalid PDF data URL payload')
}
if (!binary.startsWith('%PDF-')) {
throw new Error('Invalid PDF file header')
}
const bytes = new Uint8Array(binary.length)
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index)
}
return new Blob([bytes], { type: 'application/pdf' })
}
async function readTextPreview(filePath: string) {
try {
return await readDesktopFileText(filePath)
@ -579,6 +620,8 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
const { t } = useI18n()
const [state, setState] = useState<LocalPreviewState>({ loading: true })
const [forcePreview, setForcePreview] = useState(false)
const [pdfError, setPdfError] = useState<string>()
const [pdfUrl, setPdfUrl] = useState<string>()
// User-picked view; null = auto (diff when changed, else rendered markdown,
// else source). Reset when the previewed file changes.
const [userMode, setUserMode] = useState<null | PreviewViewMode>(null)
@ -600,8 +643,11 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
// hover flag (no state — only the keydown handler reads it).
const readViewRef = useRef<HTMLDivElement>(null)
const hoverRef = useRef(false)
const connection = useStore($connection)
const fsCacheKey = desktopFsCacheKey(connection)
const filePath = filePathForTarget(target)
const isImage = target.previewKind === 'image'
const isPdf = target.previewKind === 'pdf'
// eslint-disable-next-line no-restricted-syntax -- legitimate non-atom ref write (see eslint rule comment)
useEffect(() => {
@ -620,7 +666,7 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
// when the file is forcibly previewed past the binary refusal screen.
const isText = target.previewKind === 'text' || target.previewKind === 'binary' || target.previewKind === 'html'
const blockedByTarget = !isImage && !forcePreview && (target.binary || target.large)
const blockedByTarget = !isImage && !isPdf && !forcePreview && (target.binary || target.large)
useEffect(() => {
let active = true
@ -632,7 +678,7 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
return
}
if (!isImage && !isText) {
if (!isImage && !isPdf && !isText) {
setState({ loading: false })
return
@ -641,7 +687,7 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
setState({ loading: true })
try {
if (isImage) {
if (isImage || isPdf) {
// Prefer bytes the caller already handed us (a pasted/dropped
// screenshot) over re-reading a path that may be transient/unreadable.
const dataUrl = target.dataUrl || (await readDesktopFileDataUrl(filePath))
@ -698,7 +744,49 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
return () => {
active = false
}
}, [blockedByTarget, filePath, forcePreview, isImage, isText, reloadKey, selfReload, target.dataUrl, target.language])
}, [
blockedByTarget,
filePath,
forcePreview,
fsCacheKey,
isImage,
isPdf,
isText,
reloadKey,
selfReload,
target.dataUrl,
target.language
])
useEffect(() => {
setPdfUrl(undefined)
setPdfError(undefined)
if (!isPdf || !state.dataUrl) {
return
}
// Chromium's PDF viewer is blank for large data: URLs in an iframe. Use a
// blob URL instead, and revoke it when the target or loaded bytes change.
if (typeof URL.createObjectURL !== 'function') {
setPdfError('PDF preview requires object URL support')
return
}
let objectUrl: string
try {
objectUrl = URL.createObjectURL(dataUrlToBlob(state.dataUrl))
setPdfUrl(objectUrl)
} catch (error) {
setPdfError(error instanceof Error ? error.message : String(error))
return
}
return () => URL.revokeObjectURL(objectUrl)
}, [isPdf, state.dataUrl])
// Editing is only offered for whole, readable text — never images, binaries,
// or files we only loaded the first 512 KB of (saving would drop the tail).
@ -889,8 +977,13 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
return <PreviewEmptyState body={state.error} title={t.preview.unavailable} />
}
if (pdfError) {
return <PreviewEmptyState body={pdfError} title={t.preview.unavailable} />
}
if (
!isImage &&
!isPdf &&
!forcePreview &&
(target.binary || target.large || state.binary || (state.byteSize ?? 0) > TEXT_PREVIEW_MAX_BYTES)
) {
@ -920,6 +1013,23 @@ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; tar
)
}
if (isPdf && state.dataUrl && pdfUrl) {
return (
<div className="h-full w-full overflow-hidden bg-transparent">
<iframe
aria-label={target.label}
className="h-full w-full border-0 bg-white"
src={pdfUrl}
title={target.label}
/>
</div>
)
}
if (isPdf && state.dataUrl) {
return <PageLoader label={t.preview.loading} />
}
if (isText && state.text !== undefined) {
const isMarkdown = (state.language || target.language) === 'markdown'
const hasDiff = Boolean(state.diff && state.diff.trim())

View File

@ -1,4 +1,4 @@
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { $connection } from '@/store/session'
@ -6,6 +6,23 @@ import { $connection } from '@/store/session'
import { PreviewPane } from './preview-pane'
import { forgetPreviewStripTools, previewConsoleState } from './preview-strip-tools'
function stubPdfObjectUrls() {
const NativeUrl = URL
let objectUrlIndex = 0
const createObjectURL = vi.fn((_blob: Blob) => `blob:pdf-preview-${(objectUrlIndex += 1)}`)
const revokeObjectURL = vi.fn()
class TestUrl extends NativeUrl {}
Object.defineProperties(TestUrl, {
createObjectURL: { configurable: true, value: createObjectURL },
revokeObjectURL: { configurable: true, value: revokeObjectURL }
})
vi.stubGlobal('URL', TestUrl)
return { createObjectURL, revokeObjectURL }
}
describe('PreviewPane console state', () => {
beforeEach(() => {
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
@ -133,4 +150,191 @@ describe('PreviewPane console state', () => {
expect(sourceLink?.getAttribute('target')).toBeNull()
expect(fireEvent.click(sourceLink!)).toBe(false)
})
it('renders PDF targets in an embedded viewer', async () => {
const dataUrl = 'data:application/pdf;base64,JVBERi0xLjQ='
const readFileDataUrl = vi.fn(async () => dataUrl)
const { createObjectURL, revokeObjectURL } = stubPdfObjectUrls()
$connection.set({ mode: 'local' } as never)
vi.stubGlobal('window', {
...window,
hermesDesktop: {
readFileDataUrl
}
})
let rendered!: ReturnType<typeof render>
await act(async () => {
rendered = render(
<PreviewPane
target={{
kind: 'file',
label: 'spec.pdf',
path: '/tmp/spec.pdf',
previewKind: 'pdf',
source: '/tmp/spec.pdf',
url: 'file:///tmp/spec.pdf'
}}
/>
)
})
await waitFor(
() => expect(rendered.container.querySelector('iframe')).not.toBeNull(),
{ container: rendered.container }
)
expect(rendered.container.querySelector('iframe')?.getAttribute('src')).toBe('blob:pdf-preview-1')
expect(readFileDataUrl).toHaveBeenCalledWith('/tmp/spec.pdf')
const blob = createObjectURL.mock.calls[0]?.[0]
expect(blob).toBeInstanceOf(Blob)
expect(blob?.type).toBe('application/pdf')
expect(await blob?.text()).toBe('%PDF-1.4')
await act(async () => {
rendered.rerender(
<PreviewPane
target={{
kind: 'file',
label: 'other.pdf',
path: '/tmp/other.pdf',
previewKind: 'pdf',
source: '/tmp/other.pdf',
url: 'file:///tmp/other.pdf'
}}
/>
)
})
await waitFor(() => expect(createObjectURL).toHaveBeenCalledTimes(2), {
container: rendered.container
})
expect(revokeObjectURL).toHaveBeenCalledWith('blob:pdf-preview-1')
expect(rendered.container.querySelector('iframe')?.getAttribute('src')).toBe('blob:pdf-preview-2')
rendered.unmount()
expect(revokeObjectURL).toHaveBeenCalledWith('blob:pdf-preview-2')
})
it('accepts case-insensitive metadata and percent-escaped base64', async () => {
const readFileDataUrl = vi.fn(async () => 'data:APPLICATION/PDF;BASE64,%4AVBERi0xLjQ=')
const { createObjectURL } = stubPdfObjectUrls()
$connection.set({ mode: 'local' } as never)
vi.stubGlobal('window', {
...window,
hermesDesktop: {
readFileDataUrl
}
})
let rendered!: ReturnType<typeof render>
await act(async () => {
rendered = render(
<PreviewPane
target={{
kind: 'file',
label: 'spec.pdf',
path: '/tmp/spec.pdf',
previewKind: 'pdf',
source: '/tmp/spec.pdf',
url: 'file:///tmp/spec.pdf'
}}
/>
)
})
await waitFor(() => expect(createObjectURL).toHaveBeenCalledTimes(1), {
container: rendered.container
})
expect(rendered.container.querySelector('iframe')?.getAttribute('src')).toBe('blob:pdf-preview-1')
})
it.each([
['a non-PDF MIME type', 'data:text/html;base64,JVBERi0xLjQ=', 'Invalid PDF data URL type'],
['bytes without a PDF header', 'data:application/pdf;base64,PGh0bWw+', 'Invalid PDF file header'],
['a malformed payload', 'data:application/pdf;base64,%', 'Invalid PDF data URL payload']
])('rejects %s before creating an object URL', async (_case, dataUrl, expectedError) => {
const readFileDataUrl = vi.fn(async () => dataUrl)
const { createObjectURL } = stubPdfObjectUrls()
$connection.set({ mode: 'local' } as never)
vi.stubGlobal('window', {
...window,
hermesDesktop: {
readFileDataUrl
}
})
let rendered!: ReturnType<typeof render>
await act(async () => {
rendered = render(
<PreviewPane
target={{
kind: 'file',
label: 'spec.pdf',
path: '/tmp/spec.pdf',
previewKind: 'pdf',
source: '/tmp/spec.pdf',
url: 'file:///tmp/spec.pdf'
}}
/>
)
})
await waitFor(() => expect(rendered.container.textContent).toContain(expectedError), {
container: rendered.container
})
expect(rendered.container.querySelector('iframe')).toBeNull()
expect(createObjectURL).not.toHaveBeenCalled()
})
it('retries a restored PDF when the filesystem connection becomes remote', async () => {
const filePath = '/remote/spec.pdf'
const dataUrl = 'data:application/pdf;base64,JVBERi0xLjQ='
stubPdfObjectUrls()
const readFileDataUrl = vi.fn(async () => {
throw new Error('File preview failed: file does not exist')
})
const api = vi.fn(async () => dataUrl)
$connection.set({ mode: 'local' } as never)
vi.stubGlobal('window', {
...window,
hermesDesktop: {
api,
readFileDataUrl
}
})
let rendered!: ReturnType<typeof render>
await act(async () => {
rendered = render(
<PreviewPane
target={{
kind: 'file',
label: 'spec.pdf',
path: filePath,
previewKind: 'pdf',
source: filePath,
url: `file://${filePath}`
}}
/>
)
})
await waitFor(() => expect(readFileDataUrl).toHaveBeenCalledTimes(1), { container: rendered.container })
await act(async () => {
$connection.set({ baseUrl: 'http://macmini', mode: 'remote', profile: 'macmini' } as never)
})
await waitFor(
() => expect(rendered.container.querySelector('iframe')).not.toBeNull(),
{ container: rendered.container }
)
expect(api).toHaveBeenCalledWith({
path: `/api/fs/read-data-url?path=${encodeURIComponent(filePath)}`,
profile: 'macmini'
})
})
})

View File

@ -790,7 +790,7 @@ export interface HermesPreviewTarget {
language?: string
mimeType?: string
path?: string
previewKind?: 'binary' | 'html' | 'image' | 'text'
previewKind?: 'binary' | 'html' | 'image' | 'pdf' | 'text'
renderMode?: 'preview' | 'source'
source: string
url: string

View File

@ -29,8 +29,8 @@ function connectionCacheKey(connection: HermesConnection | null) {
return `${connection.mode || 'local'}:${connection.remoteKind || ''}:${connection.profile || ''}:${target}`
}
export function desktopFsCacheKey() {
return connectionCacheKey($connection.get())
export function desktopFsCacheKey(connection: HermesConnection | null = $connection.get()) {
return connectionCacheKey(connection)
}
export function isDesktopFsRemoteMode() {

View File

@ -168,3 +168,32 @@ describe('remote HTML previews', () => {
expect(openPreviewInBrowser).not.toHaveBeenCalled()
})
})
describe('PDF previews', () => {
it('classifies PDF files as PDF previews', () => {
expect(localPreviewTarget('/tmp/spec.pdf')).toMatchObject({
path: '/tmp/spec.pdf',
previewKind: 'pdf'
})
})
it('keeps ordinary text files on the source-preview path', () => {
expect(localPreviewTarget('/tmp/spec.md')).toMatchObject({
language: 'markdown',
previewKind: 'text'
})
})
it('does not UTF-8-enrich remote PDFs before loading their bytes', async () => {
vi.clearAllMocks()
window.hermesDesktop = {
normalizePreviewTarget: vi.fn(async () => null)
} as never
await expect(normalizeOrLocalPreviewTarget('/remote/spec.pdf')).resolves.toMatchObject({
path: '/remote/spec.pdf',
previewKind: 'pdf'
})
expect(readDesktopFileDataUrl).not.toHaveBeenCalled()
})
})

View File

@ -5,6 +5,7 @@ import type { PreviewTarget } from '@/store/preview'
const HTML_EXTENSIONS = new Set(['.htm', '.html'])
const IMAGE_EXTENSIONS = new Set(['.bmp', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp'])
const PDF_EXTENSIONS = new Set(['.pdf'])
// Mirrors `_FS_DATA_URL_MAX_BYTES` in the backend filesystem endpoint.
const REMOTE_HTML_PREVIEW_MAX_BYTES = 16 * 1024 * 1024
const REMOTE_HTML_PREVIEW_MAX_BASE64_BYTES = Math.ceil(REMOTE_HTML_PREVIEW_MAX_BYTES / 3) * 4
@ -203,6 +204,7 @@ export function localPreviewTarget(rawTarget: string, cwd?: string | null): Prev
const ext = extension(path)
const isHtml = HTML_EXTENSIONS.has(ext)
const isImage = IMAGE_EXTENSIONS.has(ext)
const isPdf = PDF_EXTENSIONS.has(ext)
return {
kind: 'file',
@ -210,16 +212,22 @@ export function localPreviewTarget(rawTarget: string, cwd?: string | null): Prev
language: LANGUAGE_BY_EXT[ext] || 'text',
path,
// Renderer fallback can't stat/sniff without reading; assume text unless
// image/html extension says otherwise. LocalFilePreview still guards
// image/html/pdf extension says otherwise. LocalFilePreview still guards
// binary/large files when readFileText/readFileDataUrl returns metadata.
previewKind: isHtml ? 'html' : isImage ? 'image' : 'text',
previewKind: isHtml ? 'html' : isImage ? 'image' : isPdf ? 'pdf' : 'text',
source: raw,
url: pathToFileUrl(path)
}
}
async function enrichPreviewTarget(target: PreviewTarget | null): Promise<PreviewTarget | null> {
if (!isDesktopFsRemoteMode() || !target || target.kind !== 'file' || target.previewKind === 'image') {
if (
!isDesktopFsRemoteMode() ||
!target ||
target.kind !== 'file' ||
target.previewKind === 'image' ||
target.previewKind === 'pdf'
) {
return target
}

View File

@ -0,0 +1,97 @@
import { describe, expect, it } from 'vitest'
import { decodePreviewTabs } from './preview'
describe('persisted preview migration', () => {
it('upgrades a pre-PDF remote tab from binary to pdf', () => {
const source = '/remote/.hermes/desktop-attachments/spec.pdf'
const [restored] = decodePreviewTabs(
JSON.stringify([
{
id: `file:file://${source}`,
target: {
binary: true,
kind: 'file',
label: 'spec.pdf',
large: true,
path: source,
previewKind: 'binary',
source,
url: `file://${source}`
}
}
])
)
expect(restored?.target.previewKind).toBe('pdf')
})
it('leaves a persisted non-PDF binary tab unchanged', () => {
const source = '/work/archive.zip'
const [restored] = decodePreviewTabs(
JSON.stringify([
{
id: `file:file://${source}`,
target: {
binary: true,
kind: 'file',
label: 'archive.zip',
path: source,
previewKind: 'binary',
source,
url: `file://${source}`
}
}
])
)
expect(restored?.target.previewKind).toBe('binary')
})
it.each(['report.pdf#notes', 'report.pdf?draft'])('treats %s as a literal filesystem path', sourceName => {
const source = `/work/${sourceName}`
const [restored] = decodePreviewTabs(
JSON.stringify([
{
id: `file:file://${encodeURI(source)}`,
target: {
binary: true,
kind: 'file',
label: sourceName,
path: source,
previewKind: 'binary',
source,
url: `file:///work/${encodeURIComponent(sourceName)}`
}
}
])
)
expect(restored?.target.previewKind).toBe('binary')
})
it('does not overwrite a non-binary PDF preview kind', () => {
const source = '/work/spec.pdf'
const [restored] = decodePreviewTabs(
JSON.stringify([
{
id: `file:file://${source}`,
target: {
kind: 'file',
label: 'spec.pdf',
path: source,
previewKind: 'text',
source,
url: `file://${source}`
}
}
])
)
expect(restored?.target.previewKind).toBe('text')
})
})

View File

@ -35,7 +35,7 @@ export interface PreviewTarget {
language?: string
mimeType?: string
path?: string
previewKind?: 'binary' | 'html' | 'image' | 'text'
previewKind?: 'binary' | 'html' | 'image' | 'pdf' | 'text'
renderMode?: 'preview' | 'source'
source: string
/** Runtime-only target that cannot be restored from persisted state. */
@ -92,20 +92,49 @@ function isPreviewTab(value: unknown): value is PreviewTab {
return typeof r.id === 'string' && (r.id.startsWith('file:') || r.id.startsWith('url:')) && isPreviewTarget(r.target)
}
function isPdfFileTarget(target: PreviewTarget): boolean {
if (target.kind !== 'file') {
return false
}
if (target.mimeType?.toLowerCase() === 'application/pdf') {
return true
}
if ([target.path, target.source].some(value => (value ? /\.pdf$/i.test(value) : false))) {
return true
}
try {
return /\.pdf$/i.test(new URL(target.url).pathname)
} catch {
return false
}
}
/** Upgrade tabs persisted by builds that classified PDFs as generic binary.
* Without this restore-time migration, an already-open PDF keeps taking the
* obsolete raw-binary path after Desktop itself has been upgraded. */
export function decodePreviewTabs(raw: string): PreviewTab[] {
const parsed = JSON.parse(raw) as unknown
const tabs = (Array.isArray(parsed) ? parsed.filter(isPreviewTab) : []).map(tab =>
isPdfFileTarget(tab.target) && tab.target.previewKind === 'binary'
? { ...tab, target: { ...tab.target, previewKind: 'pdf' as const } }
: tab
)
// One Browser: rekey restored URL tabs onto the singleton id (rows written
// before the id existed carried one id per address) and keep only the
// LAST — the most recently opened page is the one the browser shows.
const lastUrl = tabs.findLast(tab => tab.target.kind === 'url')
return tabs
.filter(tab => tab.target.kind !== 'url' || tab === lastUrl)
.map(tab => (tab.target.kind === 'url' ? { ...tab, id: previewTabId(tab.target) } : tab))
}
export const $previewTabs = persistentAtom<PreviewTab[]>(TABS_STORAGE_KEY, [], {
decode: raw => {
const parsed = JSON.parse(raw) as unknown
const tabs = Array.isArray(parsed) ? parsed.filter(isPreviewTab) : []
// One Browser: rekey restored URL tabs onto the singleton id (rows written
// before the id existed carried one id per address) and keep only the
// LAST — the most recently opened page is the one the browser shows.
const lastUrl = tabs.findLast(tab => tab.target.kind === 'url')
return tabs
.filter(tab => tab.target.kind !== 'url' || tab === lastUrl)
.map(tab => (tab.target.kind === 'url' ? { ...tab, id: previewTabId(tab.target) } : tab))
},
decode: decodePreviewTabs,
// Inline bytes are not restorable. Strip them from images, and skip remote
// HTML and artifact tabs that cannot render without their in-memory payload.
encode: tabs =>