fix: accept bare browser addresses

The Browser workbar required users to enter a complete HTTP(S) URL, so submitting a bare domain or host with a port failed URL validation. Enter and Go now prepend http:// before using the existing browser navigation path.

Explicit protocols remain unchanged so the Main browser URL policy continues to reject unsupported schemes with the correct error.

Release note: 浏览器地址栏现在支持直接输入域名或主机端口,回车或点击“前往”时会自动补全 http://。
This commit is contained in:
mesalogo
2026-09-01 08:54:18 +08:00
parent 77ce13aba3
commit dba3380a06
5 changed files with 51 additions and 11 deletions
+2 -1
View File
@@ -344,7 +344,8 @@
前往、交互、关闭浏览器”排列。“刷新”在当前页面加载时原位切换为“停止加载”;
“关闭浏览器”必须使用明确文字和与页面导航控件清楚区分的危险操作视觉,不能继续标为
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
不被画面或状态刷新覆盖;提交、失焦后的同步规则不得让尚未提交的文字改变 Agent 看到的页面。
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
尚未提交的文字改变 Agent 看到的页面。
- 浏览器工具栏操作的是当前 Conversation 中 Agent 使用的同一隔离浏览器会话,用户导航会
有意改变 Agent 下一步看到的页面。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、
@@ -401,7 +401,9 @@ type BrowserToolbarState = {
`committedUrl` 是浏览器已经提交的权威 URL。地址输入在未编辑时与它同步;获得编辑意图后,
输入文字保持为 Renderer 本地草稿,画面、加载状态和 URL 更新不得覆盖草稿,直到用户“前往”
或放弃编辑。地址草稿不属于浏览器会话持久状态,也不需要恢复或版本化机制。
或放弃编辑。用户提交未带 `http://` 或 `https://` 的裸域名时,Renderer 自动补上
`http://`,再通过既有浏览器 URL 策略提交。地址草稿不属于浏览器会话持久状态,也不需要
恢复或版本化机制。
创建会话、Agent 操作和完整交互窗口期间,返回、刷新、地址提交、前往、交互和关闭浏览器
控件禁用,避免与同一会话并发操作。例外是活动导航处于加载中时,“停止加载”保持可用,
@@ -105,10 +105,10 @@ Linux x64 和 Linux arm64。
不会创建第二套 Renderer 导航路径。
工具栏状态以浏览器提交后的权威 URL、是否加载中、能否返回和会话活动状态为准;地址输入
在用户编辑期间保留本地草稿,不被状态刷新覆盖。创建中、Agent 操作中和完整交互窗口期间禁用
其余工具栏控件,但活动导航仍可用“停止加载”中断。停止加载只取消页面加载,不关闭浏览器,也
不释放 Runtime 上下文;原“停止浏览器”会话生命周期操作已统一标为“关闭浏览器”,并在
视觉和文案上与停止加载清楚区分。
在用户编辑期间保留本地草稿,不被状态刷新覆盖;提交未带 HTTP(S) 协议的裸域名时会自动添加
`http://`。创建中、Agent 操作中和完整交互窗口期间禁用其余工具栏控件,但活动导航仍可用
“停止加载”中断。停止加载只取消页面加载,不关闭浏览器,也不释放 Runtime 上下文;原“停止
浏览器”会话生命周期操作已统一标为“关闭浏览器”,并在视觉和文案上与停止加载清楚区分。
活动 Conversation 尚无浏览器会话时,可直接从地址栏输入 URL 并“前往”,通过现有浏览器
能力检查和同一 URL 策略创建会话。实现复用现有每 Conversation 串行队列和有界状态,不引入
+26 -3
View File
@@ -9630,16 +9630,39 @@ describe('App', () => {
expect(address).toHaveValue('https://example.com/start')
fireEvent.change(address, {
target: { value: 'https://example.com/enter' }
target: { value: 'example.com/enter' }
})
fireEvent.keyDown(address, { key: 'Enter' })
await waitFor(() =>
expect(api.browser.navigate).toHaveBeenLastCalledWith(
expect.any(String),
'https://example.com/enter'
'http://example.com/enter'
)
)
expect(api.browser.navigate).toHaveBeenCalledTimes(2)
expect(address).toHaveValue('http://example.com/enter')
fireEvent.change(address, {
target: { value: 'mailto:user@example.com' }
})
fireEvent.keyDown(address, { key: 'Enter' })
await waitFor(() =>
expect(api.browser.navigate).toHaveBeenLastCalledWith(
expect.any(String),
'mailto:user@example.com'
)
)
fireEvent.change(address, {
target: { value: 'localhost:3000/path' }
})
fireEvent.keyDown(address, { key: 'Enter' })
await waitFor(() =>
expect(api.browser.navigate).toHaveBeenLastCalledWith(
expect.any(String),
'http://localhost:3000/path'
)
)
expect(api.browser.navigate).toHaveBeenCalledTimes(4)
})
it('suppresses duplicate browser actions while keeping stop loading available', async () => {
+16 -2
View File
@@ -237,6 +237,18 @@ function measureSplitLayoutWidth(
return measuredWidth > 0 ? measuredWidth : fallbackWidth
}
function addDefaultBrowserProtocol(address: string): string {
const protocol = /^[a-z][a-z\d+.-]*:/iu.exec(address)?.[0]
if (
protocol &&
(/^https?:$/iu.test(protocol) ||
!/^\d+(?:[/?#]|$)/u.test(address.slice(protocol.length)))
) {
return address
}
return `http://${address}`
}
function BrowserToolbar({
activeConversationId,
browserState,
@@ -325,10 +337,12 @@ function BrowserToolbar({
}
const submitAddress = async (): Promise<void> => {
const url = displayedAddress.trim()
if (goDisabled || !url) {
const address = displayedAddress.trim()
if (goDisabled || !address) {
return
}
const url = addDefaultBrowserProtocol(address)
setAddressDraft(url)
if (await runToolbarAction(() => onNavigate(url))) {
setDirty(false)
setEditing(false)