From 8aba58f663fe4574f42e94e0bb665e27e290c57a Mon Sep 17 00:00:00 2001 From: lucas77778 <3098274296@qq.com> Date: Tue, 21 Jul 2026 12:30:44 +0000 Subject: [PATCH] fix(desktop): exclude preview URLs from persistence --- .../src/__tests__/shell-state.test.ts | 24 +++++++++++++++---- .../src/renderer/src/shell/store/model.ts | 5 ++-- .../ui/src/shell/browser/index.ts | 2 +- .../ui/src/shell/browser/normalize.ts | 14 +++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/src/__tests__/shell-state.test.ts b/apps/desktop/src/renderer/src/__tests__/shell-state.test.ts index 1637480b6..ba0966b7e 100644 --- a/apps/desktop/src/renderer/src/__tests__/shell-state.test.ts +++ b/apps/desktop/src/renderer/src/__tests__/shell-state.test.ts @@ -9,6 +9,7 @@ import { createRightFileTab, createRightTerminalTab, DEFAULT_LAYOUT, + durableBrowserUrl, openFileTabState, parsePersistedDesktopShellState, RIGHT_PANEL_MAX_SIZE, @@ -234,7 +235,7 @@ describe('desktop shell state persistence', () => { activeSection: 'browser', terminal: { tabs: [createRightTerminalTab(), createRightTerminalTab()], activeTabId: null }, files: { tabs: [fileTab, createRightFileTab('/w/report.pdf')], activeTabId: fileTab.id }, - browser: { url: 'http://web--app-1a2b3c.localhost:19523' }, + browser: { url: 'https://example.com' }, }; const source: DesktopShellState = { sidebarOpen: false, @@ -259,13 +260,16 @@ describe('desktop shell state persistence', () => { '/w/report.pdf', ]); expect(parsed.rightPanel.files.activeTabId).toBe(parsed.rightPanel.files.tabs[0].id); - expect(parsed.rightPanel.browser.url).toBe('http://web--app-1a2b3c.localhost:19523'); + expect(parsed.rightPanel.browser.url).toBe('https://example.com'); expect(panelTypes(parsed.bottomPanel)).toEqual(['files']); }); - it('drops renderer-scoped blob URLs from persisted browser state', () => { + it.each([ + 'blob:http://localhost:5173/expired-preview', + 'http://file--3e2d018e14777dcb.localhost:19523/', + ])('drops ephemeral URL %s from persisted browser state', (url) => { const source = createDefaultDesktopShellState(); - source.rightPanel.browser.url = 'blob:http://localhost:5173/expired-preview'; + source.rightPanel.browser.url = url; const serialized = serializeDesktopShellState(source); expect(serialized.rightPanel.browserUrl).toBeNull(); @@ -274,11 +278,21 @@ describe('desktop shell state persistence', () => { ...serialized, rightPanel: { ...serialized.rightPanel, - browserUrl: 'blob:http://localhost:5173/expired-preview', + browserUrl: url, }, }); expect(parsed.rightPanel.browser.url).toBeNull(); }); + + it.each([ + ['blob:http://localhost:5173/expired-preview', null], + ['http://file--3e2d018e14777dcb.localhost:19523/', null], + ['http://artifact--turn-123.localhost:19523/', null], + ['http://web--app-1a2b3c.localhost:19523/', null], + ['https://example.com/path', 'https://example.com/path'], + ])('filters durable browser URL %s', (url, expected) => { + expect(durableBrowserUrl(url)).toBe(expected); + }); }); describe('closeSectionTabState', () => { diff --git a/apps/desktop/src/renderer/src/shell/store/model.ts b/apps/desktop/src/renderer/src/shell/store/model.ts index e28e74b9c..17c4a3635 100644 --- a/apps/desktop/src/renderer/src/shell/store/model.ts +++ b/apps/desktop/src/renderer/src/shell/store/model.ts @@ -1,3 +1,4 @@ +import { isPreviewBrowserUrl } from '@linkcode/ui/shell/browser'; import type { PanelSection, PanelSectionTab, @@ -314,8 +315,8 @@ export function serializeDesktopShellState(state: DesktopShellState): PersistedD }; } -function durableBrowserUrl(url: string | null): string | null { - return url?.startsWith('blob:') ? null : url; +export function durableBrowserUrl(url: string | null): string | null { + return url?.startsWith('blob:') || (url !== null && isPreviewBrowserUrl(url)) ? null : url; } function createPersistedShellStateSchema(): z.ZodType { diff --git a/packages/presentation/ui/src/shell/browser/index.ts b/packages/presentation/ui/src/shell/browser/index.ts index 18f8f7a1c..a12898cba 100644 --- a/packages/presentation/ui/src/shell/browser/index.ts +++ b/packages/presentation/ui/src/shell/browser/index.ts @@ -1,3 +1,3 @@ export type { BrowserPaneProps } from './browser-pane'; export { BrowserPane } from './browser-pane'; -export { isAllowedBrowserUrl, normalizeBrowserUrl } from './normalize'; +export { isAllowedBrowserUrl, isPreviewBrowserUrl, normalizeBrowserUrl } from './normalize'; diff --git a/packages/presentation/ui/src/shell/browser/normalize.ts b/packages/presentation/ui/src/shell/browser/normalize.ts index 84f6dfb92..63e0f621f 100644 --- a/packages/presentation/ui/src/shell/browser/normalize.ts +++ b/packages/presentation/ui/src/shell/browser/normalize.ts @@ -2,6 +2,8 @@ * `*.localhost` hostnames — plus bare IPv4/IPv6) default to http; the web defaults to https. */ const LOOPBACK_AUTHORITY_RE = /^(?:(?:[\w-]+\.)*localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[\d.:a-f]+\])(?::\d+)?(?:[#/?]|$)/i; +const LOCALHOST_SUFFIX = '.localhost'; +const PREVIEW_LABEL_RE = /^[a-z0-9-]+$/; const SCHEME_RE = /^[a-z][\d+.a-z-]*:/i; /** Address-bar input → navigable URL (paseo's normalization rules). */ @@ -18,3 +20,15 @@ export function normalizeBrowserUrl(value: string): string { export function isAllowedBrowserUrl(url: string): boolean { return url.startsWith('http://') || url.startsWith('https://'); } + +/** Whether a URL belongs to the daemon's ephemeral `--.localhost` preview namespace. */ +export function isPreviewBrowserUrl(url: string): boolean { + try { + const { hostname } = new URL(url); + if (!hostname.endsWith(LOCALHOST_SUFFIX)) return false; + const label = hostname.slice(0, -LOCALHOST_SUFFIX.length); + return label.includes('--') && PREVIEW_LABEL_RE.test(label); + } catch { + return false; + } +}