From b3a9b241ea440229972e6d4734ebe8cfd82df115 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 10:13:10 -0700 Subject: [PATCH] fix: open localhost tab entry suffix urls (#4266) --- .../tab-create-entry-classifier.test.ts | 15 +++++++++++++ .../tab-bar/tab-create-entry-classifier.ts | 21 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) 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) {