Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions apps/desktop/src/renderer/src/__tests__/shell-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createRightFileTab,
createRightTerminalTab,
DEFAULT_LAYOUT,
durableBrowserUrl,
openFileTabState,
parsePersistedDesktopShellState,
RIGHT_PANEL_MAX_SIZE,
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand All @@ -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', () => {
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/renderer/src/shell/store/model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isPreviewBrowserUrl } from '@linkcode/ui/shell/browser';
import type {
PanelSection,
PanelSectionTab,
Expand Down Expand Up @@ -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<DesktopShellState> {
Expand Down
2 changes: 1 addition & 1 deletion packages/presentation/ui/src/shell/browser/index.ts
Original file line number Diff line number Diff line change
@@ -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';
14 changes: 14 additions & 0 deletions packages/presentation/ui/src/shell/browser/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand All @@ -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 `<kind>--<id>.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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Durable Local URLs Are Discarded

Any single-label .localhost hostname containing -- passes this predicate, including an unrelated local service such as http://my--app.localhost:3000. Navigating there is valid, but desktop serialization now clears the URL, so the browser location disappears after restart.

Artifacts

Repro: focused desktop persistence Vitest test

  • Contains supporting evidence from the run (text/typescript; charset=utf-8).

Repro: focused Vitest configuration

  • Contains supporting evidence from the run (text/typescript; charset=utf-8).

Repro: verbose failing test output showing the input, serialized, and rehydrated URL values

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

return false;
}
}