diff --git a/src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts b/src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts index ba0d134598f..0193d61c52d 100644 --- a/src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts +++ b/src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts @@ -34,6 +34,21 @@ describe('tab create entry classification', () => { }) }) + it('opens local-dev URLs with root suffixes as browser tabs', () => { + expect(classifyTabEntryQuery('localhost:3000/', readyFiles([]))).toEqual({ + kind: 'host-url', + url: 'http://localhost:3000/' + }) + expect(classifyTabEntryQuery('localhost:3000?debug=1', readyFiles([]))).toEqual({ + kind: 'host-url', + url: 'http://localhost:3000/?debug=1' + }) + expect(classifyTabEntryQuery('localhost:3000#preview', readyFiles([]))).toEqual({ + kind: 'host-url', + url: 'http://localhost:3000/#preview' + }) + }) + it('keeps common source/document filenames as file candidates', () => { expect(classifyTabEntryQuery('README.md', readyFiles([]))).toEqual({ kind: 'new-file', diff --git a/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts b/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts index d96fc88d0d5..dcfe9fde228 100644 --- a/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts +++ b/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts @@ -19,6 +19,8 @@ const HOST_FILE_EXTENSIONS = new Set([ 'yaml', 'yml' ]) +const LOCAL_ADDRESS_PATTERN = + /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[[0-9a-f:]+\])(?::\d+)?(?:[/?#].*)?$/i export type TabEntryClassification = | { kind: 'empty'; message: string } @@ -110,6 +112,9 @@ function findExistingFileMatches( function classifyExplicitUrl( query: string ): Extract | null { + if (LOCAL_ADDRESS_PATTERN.test(query)) { + return null + } let url: URL try { url = new URL(query) @@ -122,6 +127,20 @@ function classifyExplicitUrl( return { kind: 'explicit-url', url: url.href } } +function classifyLocalDevUrl( + query: string +): Extract | null { + if (!LOCAL_ADDRESS_PATTERN.test(query)) { + return null + } + try { + const url = new URL(`http://${query}`) + return url.hostname ? { kind: 'host-url', url: url.href } : null + } catch { + return null + } +} + function classifyHostLikeUrl( query: string ): Extract | null { @@ -242,7 +261,7 @@ export function getTabEntryOptions( newFile = null } - const hostUrl = classifyHostLikeUrl(trimmed) + const hostUrl = classifyLocalDevUrl(trimmed) ?? classifyHostLikeUrl(trimmed) const options: TabEntryActionClassification[] = [] if (exactExistingFiles.length > 0) {