diff --git a/UI-DESIGN.md b/UI-DESIGN.md index ed25dbb8..64747f1b 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -344,7 +344,8 @@ 前往、交互、关闭浏览器”排列。“刷新”在当前页面加载时原位切换为“停止加载”; “关闭浏览器”必须使用明确文字和与页面导航控件清楚区分的危险操作视觉,不能继续标为 含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿, - 不被画面或状态刷新覆盖;提交、失焦后的同步规则不得让尚未提交的文字改变 Agent 看到的页面。 + 不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让 + 尚未提交的文字改变 Agent 看到的页面。 - 浏览器工具栏操作的是当前 Conversation 中 Agent 使用的同一隔离浏览器会话,用户导航会 有意改变 Agent 下一步看到的页面。创建中、Agent 操作中或已进入完整交互窗口时禁用其余 工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、 diff --git a/docs/prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md b/docs/prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md index bc3d5695..25e7dce2 100644 --- a/docs/prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md +++ b/docs/prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md @@ -401,7 +401,9 @@ type BrowserToolbarState = { `committedUrl` 是浏览器已经提交的权威 URL。地址输入在未编辑时与它同步;获得编辑意图后, 输入文字保持为 Renderer 本地草稿,画面、加载状态和 URL 更新不得覆盖草稿,直到用户“前往” -或放弃编辑。地址草稿不属于浏览器会话持久状态,也不需要恢复或版本化机制。 +或放弃编辑。用户提交未带 `http://` 或 `https://` 的裸域名时,Renderer 自动补上 +`http://`,再通过既有浏览器 URL 策略提交。地址草稿不属于浏览器会话持久状态,也不需要 +恢复或版本化机制。 创建会话、Agent 操作和完整交互窗口期间,返回、刷新、地址提交、前往、交互和关闭浏览器 控件禁用,避免与同一会话并发操作。例外是活动导航处于加载中时,“停止加载”保持可用, diff --git a/docs/status/computer-control-implementation-status.md b/docs/status/computer-control-implementation-status.md index 4ae3352d..d8bce56a 100644 --- a/docs/status/computer-control-implementation-status.md +++ b/docs/status/computer-control-implementation-status.md @@ -105,10 +105,10 @@ Linux x64 和 Linux arm64。 不会创建第二套 Renderer 导航路径。 工具栏状态以浏览器提交后的权威 URL、是否加载中、能否返回和会话活动状态为准;地址输入 -在用户编辑期间保留本地草稿,不被状态刷新覆盖。创建中、Agent 操作中和完整交互窗口期间禁用 -其余工具栏控件,但活动导航仍可用“停止加载”中断。停止加载只取消页面加载,不关闭浏览器,也 -不释放 Runtime 上下文;原“停止浏览器”会话生命周期操作已统一标为“关闭浏览器”,并在 -视觉和文案上与停止加载清楚区分。 +在用户编辑期间保留本地草稿,不被状态刷新覆盖;提交未带 HTTP(S) 协议的裸域名时会自动添加 +`http://`。创建中、Agent 操作中和完整交互窗口期间禁用其余工具栏控件,但活动导航仍可用 +“停止加载”中断。停止加载只取消页面加载,不关闭浏览器,也不释放 Runtime 上下文;原“停止 +浏览器”会话生命周期操作已统一标为“关闭浏览器”,并在视觉和文案上与停止加载清楚区分。 活动 Conversation 尚无浏览器会话时,可直接从地址栏输入 URL 并“前往”,通过现有浏览器 能力检查和同一 URL 策略创建会话。实现复用现有每 Conversation 串行队列和有界状态,不引入 diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 0a271f45..202e7c44 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -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 () => { diff --git a/src/renderer/src/RightAssistantSidebar.tsx b/src/renderer/src/RightAssistantSidebar.tsx index 4c4e87ff..bb5a7c05 100644 --- a/src/renderer/src/RightAssistantSidebar.tsx +++ b/src/renderer/src/RightAssistantSidebar.tsx @@ -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 => { - 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)