Skip to content
Merged
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
1 change: 1 addition & 0 deletions openspec/changes/open-html-preview-in-browser/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Reuse preview path resolution, native browser ownership, toolbar styling and uns
- Pass the existing browser-opening callback through PreviewDetailsPanel to FilePreviewContent. Extend it with an optional URL and navigate the newly created page using the existing bridge. This preserves session ownership instead of bypassing ChatView from a leaf component.
- Reuse the globe icon and ace-code-action-btn styling before the wrap action. Hide the action in runtimes without native browser support.
- Reuse save/discard/cancel before opening. The browser reads the saved disk file, never an unsaved draft. Guard session changes across asynchronous work and prevent duplicate opens while pending.
- Native page creation is asynchronous and may return `ready:false`. Wait for the native ready state before navigation, bounded to 15 seconds, preserving cancellation when the session changes or page closes. This was verified against real Windows WebView2, not inferred from an immediate-ready fixture.

## Risks / Trade-offs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ The file source preview SHALL offer a keyboard-accessible browser icon before th
#### Scenario: Browser failure
- **WHEN** browser page creation or navigation fails
- **THEN** the application displays the failure and retains the source file tab

#### Scenario: Native page is still initializing
- **WHEN** the newly created native browser page is not yet ready
- **THEN** the application waits for readiness before navigating to the file
- **AND** a closed page, startup failure, timeout, or session switch stops that navigation
4 changes: 4 additions & 0 deletions openspec/changes/open-html-preview-in-browser/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
## 2. Validation

- [x] 2.1 Run Web tests/build, strict OpenSpec validation, diff checks and focused browser checks; all passed on 2026-10-08.
- [x] 2.2 Handle asynchronous native page startup before immediate HTML navigation; readiness/failure/cancellation tests, delayed-ready App fixture, and native Windows file loading passed before publication.

## Validation evidence

Expand All @@ -15,3 +16,6 @@
- Real App/ChatView browser fixture passed toolbar ordering, keyboard activation, session ownership, encoded navigation, unsaved cancel/save, non-HTML exclusion, and creation/navigation error reporting. Evidence: `%TEMP%/ace-html-browser-pTOugM/`.
- Initial fixture attempts selected a non-button Files tab and a collapsed file panel; corrected the fixture to reveal the panel through the real shortcut. No product change was needed.
- Browser proof uses an isolated native bridge fixture. Native package and cross-platform release verification are tracked separately for v1.0.0.
- Native Windows verification exposed a real startup race: page creation returned `ready:false` and immediate navigation was rejected. The action now waits up to 15 seconds for readiness, stops on close/session changes, and reports startup failures. Added deterministic readiness/timeout/closed/cancellation regressions; the real App fixture now rejects navigation during a 200 ms startup delay and passes.
- A packaged v1.0.0 Desktop with the actual readiness helper loaded a real `file://` page containing Unicode, spaces, `#` and `%` in its filename and reported the expected title. Evidence is in the release's `native-html-smoke.json`; updated App captures are under `%TEMP%/ace-html-browser-pit2jD/`.
- Full Web tests/build, i18n generation and strict OpenSpec validation passed again after the readiness fix. The initial native harness required a standard `USERPROFILE/AppData/{Local,Roaming}` hierarchy; this was a test-profile correction, not a production source change.
2 changes: 2 additions & 0 deletions web/scripts/i18n-en-overrides.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
export const ENGLISH_SOURCE_OVERRIDES = Object.freeze({
'打开设置': 'Open settings',
'在内置浏览器中打开': 'Open in built-in browser',
'浏览器页面已关闭': 'The browser page has been closed',
'浏览器启动超时,请重试': 'Browser startup timed out. Please try again',
'发送消息': 'Send message',
'思考深度 ({{p0}} / {{p1}})': 'Reasoning depth ({{p0}} / {{p1}})',
'keyboard shortcuts hotkeys 快捷键 键盘 搜索 前进 后退 思维深度 Ctrl Alt Cmd': 'keyboard shortcuts hotkeys search forward back reasoning depth Ctrl Alt Cmd',
Expand Down
15 changes: 14 additions & 1 deletion web/src/components/ChatView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ import {
hasNativeAgentBrowser,
runAgentBrowserBridgeAction,
selectAgentBrowserPage,
waitForAgentBrowserPageReady,
} from '../lib/agentBrowser.js';
import {
agentBrowserPageStore,
Expand Down Expand Up @@ -5478,12 +5479,24 @@ export function ChatView({ titleTarget, actionsTarget, children, sessionRef, ses
await closeAgentBrowserPage(created.page_id);
return;
}
showBrowserPage(
const shown = await showBrowserPage(
created.page_id,
created.title || defaultBrowserTabTitle(),
created.favicon,
);
if (!shown) {
await closeAgentBrowserPage(created.page_id);
return;
}
if (typeof url === 'string' && url) {
const ready = await waitForAgentBrowserPageReady(created.page_id, {
isCurrent: () => sidRef.current === sid,
});
if (ready.cancelled) {
await closeAgentBrowserPage(created.page_id);
return;
}
if (ready.ok === false) throw new Error(ready.error || '浏览器操作失败');
const navigated = await runAgentBrowserBridgeAction('aceDesktop_agentBrowserNavigate', {
page_id: created.page_id, url,
});
Expand Down
4 changes: 4 additions & 0 deletions web/src/i18n/sourceCatalog.generated.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions web/src/lib/agentBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ export async function getAgentBrowserState(pageId = '', win = globalThis.window)
);
}

export async function waitForAgentBrowserPageReady(
pageId, { isCurrent = () => true, timeoutMs = 15000, pollMs = 50 } = {}, win = globalThis.window,
) {
const deadline = Date.now() + timeoutMs;
while (isCurrent()) {
const state = await getAgentBrowserState(pageId, win);
if (!isCurrent()) break;
if (state.ok === false) return state;
if (state.closed) return { ok: false, error: '浏览器页面已关闭' };
if (state.ready) return state;
if (Date.now() >= deadline) return { ok: false, error: '浏览器启动超时,请重试' };
await new Promise(resolve => setTimeout(resolve, Math.min(pollMs, deadline - Date.now())));
}
return { ok: false, cancelled: true };
}

// 页面归属参数:daemon 工具与 UI 自建页都用同一形态,session_id 是唯一联结键,
// workspace_hash 只作附带信息。
export function agentBrowserOwnerForSession(sessionRef) {
Expand Down
26 changes: 26 additions & 0 deletions web/src/lib/agentBrowser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
setAgentBrowserShared,
toggleAgentBrowserDevTools,
toggleAgentBrowserElementSelection,
waitForAgentBrowserPageReady,
} from './agentBrowser.js';

async function run(name, fn) {
Expand All @@ -25,6 +26,31 @@ async function run(name, fn) {
}
}

await run('Agent Browser waits for native page readiness before immediate navigation', async () => {
const states = [{ ok: true, ready: false }, { ok: true, ready: true }];
const calls = [];
const win = {
__ACECODE_DESKTOP_SHELL__: true,
__ACECODE_OS__: 'windows',
aceDesktop_agentBrowserCreatePage() {},
aceDesktop_agentBrowserSetLayout() {},
aceDesktop_agentBrowserGetState(pageId) { calls.push(pageId); return states.shift(); },
};
assert.equal((await waitForAgentBrowserPageReady('page-1', { pollMs: 0 }, win)).ready, true);
assert.deepEqual(calls, ['page-1', 'page-1']);
win.aceDesktop_agentBrowserGetState = () => ({ ok: true, ready: false });
assert.match((await waitForAgentBrowserPageReady('page-1', { timeoutMs: 0 }, win)).error, /超时/);
win.aceDesktop_agentBrowserGetState = () => ({ ok: false, error: 'native failed' });
assert.equal((await waitForAgentBrowserPageReady('page-1', {}, win)).error, 'native failed');
win.aceDesktop_agentBrowserGetState = () => ({ ok: true, closed: true });
assert.match((await waitForAgentBrowserPageReady('page-1', {}, win)).error, /已关闭/);
let current = true;
win.aceDesktop_agentBrowserGetState = () => { current = false; return { ok: true, ready: true }; };
assert.equal((await waitForAgentBrowserPageReady('page-1', { isCurrent: () => current }, win)).cancelled, true);
win.aceDesktop_agentBrowserGetState = () => { throw new Error('must not read a stale page'); };
assert.equal((await waitForAgentBrowserPageReady('page-1', { isCurrent: () => false }, win)).cancelled, true);
});

await run('Agent Browser address normalization accepts web and absolute local paths', () => {
assert.equal(normalizeAgentBrowserAddress(' example.com/a '), 'https://example.com/a');
assert.equal(normalizeAgentBrowserAddress('HTTP://localhost:3000'), 'HTTP://localhost:3000');
Expand Down
Loading