Skip to content

fix(browser): stabilize yo lifecycle#1345

Merged
zerob13 merged 2 commits intodevfrom
codex/fix-browser-open
Mar 12, 2026
Merged

fix(browser): stabilize yo lifecycle#1345
zerob13 merged 2 commits intodevfrom
codex/fix-browser-open

Conversation

@zerob13
Copy link
Copy Markdown
Collaborator

@zerob13 zerob13 commented Mar 11, 2026

Summary by CodeRabbit

  • New Features

    • Browser can be embedded and opened in the chat side panel; navigation now supports waiting for DOM-ready.
    • Screenshots support selector-based and full-page captures.
  • Bug Fixes

    • More reliable bounds stabilization, visibility handling, and readiness gating for embedded windows.
    • Improved error reporting and lifecycle logging for browser actions.
  • Documentation

    • Updated tool description for opening the browser window.
  • UX

    • Slash-suggestion filtering centralized and limited (defaults to 20); suggestion list shows full items and preserves keyboard navigation.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Embedded browser lifecycle and host-readiness were added: a new OPEN_REQUESTED event starts embedded-window initialization; host bounds stabilization is awaited before navigation; BrowserTab enforces interactive readiness for CDP, DOM extraction, scripts, and screenshots; presenter and renderer components coordinate visibility and bounds sync.

Changes

Cohort / File(s) Summary
Event System
src/main/events.ts, src/renderer/src/events.ts
Added OPEN_REQUESTED = 'yo-browser:open-requested' to YO_BROWSER_EVENTS.
Browser Tab & Navigation
src/main/presenter/browser/BrowserTab.ts
Added interactive/full readiness flags, lifecycle bindings to webContents, prepareForNavigation/withTimeout helpers, ensureInteractiveReady gate, navigateUntilDomReady API, selector/full-page screenshot support, and centralized navigation error handling.
Presenter Host Readiness
src/main/presenter/browser/YoBrowserPresenter.ts
Introduced HostReadyWaiter flow, hostReady scheduling/timeout, bounds normalization/stability tracking, runPageAction wrapper, OPEN_REQUESTED emission, and updated openWindow flow to wait for host readiness before navigating.
Tooling & Handler
src/main/presenter/browser/YoBrowserToolDefinitions.ts, src/main/presenter/browser/YoBrowserToolHandler.ts
Updated tool description text for embedded behavior; wrapped CDP send in try/catch to log structured warnings on YoBrowserNotReadyError then rethrow.
Renderer — Browser Panel
src/renderer/src/components/sidepanel/BrowserPanel.vue
Added isBrowserPanelVisible gate, lastSyncedBounds, visibilityRunId; waitForStableRect sampling; captureContainerBounds; callPresenter wrapper; resolveWindowId/isCurrentHostWindow helpers; ensureVisibleAttachment and syncVisibleBounds to attach only after stable rect; updated event handling for OPEN_REQUESTED and window events.
Renderer — Chat Side Panel
src/renderer/src/components/sidepanel/ChatSidePanel.vue
Wired YO_BROWSER_EVENTS.OPEN_REQUESTED listener on mount/unmount and added handleBrowserOpenRequested to validate payload and call sidepanelStore.openBrowser().
Mentioning Utilities
src/renderer/src/components/chat/mentions/utils.ts, src/renderer/src/components/chat/composables/useChatInputMentions.ts, src/renderer/src/components/chat/mentions/SuggestionList.vue
Added MAX_FILTERED_SLASH_SUGGESTIONS and filterSlashSuggestionItems utility; delegated filtering to utility; SuggestionList now exposes full items (no slicing) and ensures immediate watcher invocation.
Tests — Presenter & BrowserTab
test/main/presenter/YoBrowserPresenter.test.ts, test/main/presenter/browser/BrowserTab.test.ts
Expanded MockWebContents with lifecycle helpers, loading/pendingLoad, navigationHistory; added fake timers; new BrowserTab unit tests validating navigateUntilDomReady and not-ready error behavior; updated presenter tests for host readiness sequencing and bounds stability.
Tests — Renderer Components
test/renderer/components/BrowserPanel.test.ts, test/renderer/components/ChatSidePanel.test.ts, test/renderer/components/SuggestionList.test.ts, test/renderer/composables/useChatInputMentions.test.ts
Updated test scaffolding with fake timers and IPC handler tracking; added tests for stable rect waiting, ignoring mismatched open requests, ChatSidePanel OPEN_REQUESTED wiring, SuggestionList full-items behavior, and filterSlashSuggestionItems behavior.

Sequence Diagram

sequenceDiagram
    participant User as "User/App"
    participant ChatPanel as "ChatSidePanel"
    participant IPC as "IPC Channel"
    participant Presenter as "YoBrowserPresenter"
    participant BrowserTab as "BrowserTab"
    participant WebContents as "WebContents"
    participant BrowserPanel as "BrowserPanel"

    User->>ChatPanel: request open browser
    ChatPanel->>IPC: emit OPEN_REQUESTED
    IPC->>Presenter: receive open request
    Presenter->>Presenter: mark host NOT ready & schedule readiness
    Presenter->>BrowserPanel: ensure attachment (await stable bounds)
    BrowserPanel->>BrowserPanel: capture container bounds / waitForStableRect
    BrowserPanel-->>Presenter: syncVisibleBounds
    Presenter->>Presenter: mark host READY
    Presenter->>BrowserTab: navigateUntilDomReady(url)
    BrowserTab->>WebContents: loadURL(url)
    WebContents-->>BrowserTab: did-start-loading
    WebContents-->>BrowserTab: dom-ready
    BrowserTab->>BrowserTab: set interactiveReady=true
    BrowserTab-->>Presenter: ready for interactions
    WebContents-->>BrowserTab: did-finish-load
    BrowserTab->>BrowserTab: set fullReady=true
    User->>BrowserPanel: request script/screenshot
    BrowserPanel->>BrowserTab: runPageAction (ensureInteractiveReady)
    BrowserTab->>WebContents: perform action
    WebContents-->>BrowserTab: result
    BrowserTab-->>BrowserPanel: respond
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • deepinfect

Poem

🐰 I hopped to see the panel wake,

bounds settled, not a jitter or quake.
OPEN_REQUESTED whispered soft and slow,
the tab waited till the DOM could glow.
Now screenshots and scripts may safely go.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(browser): stabilize yo lifecycle' clearly and concisely describes the main objective of the pull request, which focuses on stabilizing the browser lifecycle management with host readiness workflows and improved navigation control.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/fix-browser-open

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/main/presenter/browser/YoBrowserToolHandler.ts (1)

125-139: Avoid double-logging YoBrowserNotReadyError.

This block warns and then rethrows into callTool()'s outer catch, which still logs the same retryable condition as an error. The new not-ready path will therefore emit noisy duplicate logs for an expected lifecycle race.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/browser/YoBrowserToolHandler.ts` around lines 125 - 139,
The catch is double-logging YoBrowserNotReadyError downstream; modify the catch
in YoBrowserToolHandler around the browserPage.sendCdpCommand call so that when
error instanceof Error && error.name === 'YoBrowserNotReadyError' you mark the
error to suppress further logging (e.g. set a flag like error.suppressLogging =
true or error.handledBy = 'YoBrowserToolHandler') before rethrowing instead of
changing control flow—this preserves retry behavior while allowing callTool()'s
outer catch to skip noisy duplicate logging by checking that flag.
src/renderer/src/components/sidepanel/BrowserPanel.vue (1)

340-346: Void-returning presenter methods cannot be distinguished from errors.

The navigateWindow method returns Promise<void> per the interface (context snippet 3). When successful, callPresenter<void> will return undefined, but the check result === null treats this as success. However, if there's an IPC error, callPresenter returns null.

Since void methods return undefined on success and null on error, this works correctly. But using <void> as the generic type may be confusing since you're comparing against null. Consider adding a comment for clarity or using a more explicit pattern.

♻️ Suggested clarification
-  const result = await callPresenter<void>(
+  // callPresenter returns null on error, undefined on success for void methods
+  const result = await callPresenter<void>(
     'navigateWindow',
     yoBrowserPresenter.navigateWindow(browserWindowId.value, nextUrl)
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/sidepanel/BrowserPanel.vue` around lines 340 -
346, The null-vs-undefined ambiguity arises because callPresenter<void>(...)
returns undefined on success and null on IPC error; update the navigateWindow
call to explicitly check for null to detect errors (e.g., treat result === null
as failure) and add a short inline comment explaining that callPresenter<void>
returns undefined on success and null on error, referencing the call to
callPresenter with yoBrowserPresenter.navigateWindow(browserWindowId.value,
nextUrl) so future readers understand why the null comparison is used.
test/main/presenter/browser/BrowserTab.test.ts (1)

5-64: Consider extracting shared MockWebContents to reduce duplication.

This MockWebContents class is nearly identical to the one in YoBrowserPresenter.test.ts. While functional, maintaining two copies increases the risk of divergence.

♻️ Suggestion: Extract to shared test utility

Consider creating a shared test utility file:

// test/main/__mocks__/MockWebContents.ts
export class MockWebContents extends EventEmitter {
  // ... shared implementation
}

Then import in both test files:

import { MockWebContents } from '../__mocks__/MockWebContents'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/browser/BrowserTab.test.ts` around lines 5 - 64, The
MockWebContents class in BrowserTab.test.ts is duplicated in
YoBrowserPresenter.test.ts; extract the shared implementation into a single
exported class (e.g., MockWebContents) in a test utility module (suggested:
test/main/__mocks__/MockWebContents.ts) and replace the local class definitions
by importing that exported MockWebContents in both BrowserTab.test.ts and
YoBrowserPresenter.test.ts; ensure the exported class preserves all members used
by tests (url, title, destroyed, loading, pendingLoad, debugger, session,
navigationHistory, loadURL, isLoading, finishLoad, emitDomReady, getURL,
getTitle, isDestroyed, reload, goBack, goForward, sendInputEvent) so tests
continue to work.
test/renderer/components/ChatSidePanel.test.ts (1)

1-83: Consider adding test cleanup and additional edge case coverage.

The test correctly validates the happy path for OPEN_REQUESTED handling. However, there are a few considerations:

  1. The test doesn't clean up window.api and window.electron mocks after completion, which could affect other tests in the suite.
  2. Consider adding a test case for when windowId doesn't match (should not trigger openBrowser).
♻️ Suggested cleanup and edge case test
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { describe, expect, it, vi } from 'vitest'

 describe('ChatSidePanel', () => {
+  afterEach(() => {
+    vi.resetModules()
+    delete (window as any).api
+    delete (window as any).electron
+  })
+
   it('opens the browser sidepanel when OPEN_REQUESTED targets the current host window', async () => {
     // ... existing test ...
   })
+
+  it('ignores OPEN_REQUESTED when windowId does not match the current host', async () => {
+    // Similar setup but call handler with windowId: 999
+    // Verify openBrowser was NOT called
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/components/ChatSidePanel.test.ts` around lines 1 - 83, Add
teardown and negative-case coverage: after mounting and assertions,
remove/restore the globals and handlers by deleting window.api and
window.electron (or restoring originals if saved), clear the handlers Map, and
call the mocked ipcRenderer.removeListener if needed to mimic cleanup for the
ChatSidePanel test (references: handlers Map, window.api.getWindowId,
window.electron.ipcRenderer.on/removeListener, sidepanelStore.openBrowser). Also
add a second test that imports ChatSidePanel, registers the same mocks but calls
the stored handler with a non-matching payload (e.g., { windowId: 999 }) and
asserts sidepanelStore.openBrowser was not called to cover the mismatch edge
case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/browser/YoBrowserPresenter.ts`:
- Around line 396-409: The navigateWindow method currently calls
state.page.navigate which waits for full load; change it to use the same
dom-ready path as openWindow by calling state.page.navigateUntilDomReady(url,
timeoutMs) (or the equivalent signature on the Page API) so follow-up
navigations use the DOM-ready wait strategy; keep the existing lifecycle log,
state.updatedAt assignment and emitWindowUpdated call unchanged to preserve
telemetry and state updates.

In `@test/renderer/components/BrowserPanel.test.ts`:
- Around line 31-53: The test harness's setup uses the nullish coalescing
operator when assigning yoBrowserPresenter.getWindowById's mockResolvedValue, so
passing getWindowByIdResult: null still falls back to the default object; update
the assignment in setup (the getWindowById mock within yoBrowserPresenter in the
setup function) to check for undefined explicitly (e.g., if
options?.getWindowByIdResult !== undefined use that value, otherwise use the
default window object) so callers can pass null to simulate a missing window.

---

Nitpick comments:
In `@src/main/presenter/browser/YoBrowserToolHandler.ts`:
- Around line 125-139: The catch is double-logging YoBrowserNotReadyError
downstream; modify the catch in YoBrowserToolHandler around the
browserPage.sendCdpCommand call so that when error instanceof Error &&
error.name === 'YoBrowserNotReadyError' you mark the error to suppress further
logging (e.g. set a flag like error.suppressLogging = true or error.handledBy =
'YoBrowserToolHandler') before rethrowing instead of changing control flow—this
preserves retry behavior while allowing callTool()'s outer catch to skip noisy
duplicate logging by checking that flag.

In `@src/renderer/src/components/sidepanel/BrowserPanel.vue`:
- Around line 340-346: The null-vs-undefined ambiguity arises because
callPresenter<void>(...) returns undefined on success and null on IPC error;
update the navigateWindow call to explicitly check for null to detect errors
(e.g., treat result === null as failure) and add a short inline comment
explaining that callPresenter<void> returns undefined on success and null on
error, referencing the call to callPresenter with
yoBrowserPresenter.navigateWindow(browserWindowId.value, nextUrl) so future
readers understand why the null comparison is used.

In `@test/main/presenter/browser/BrowserTab.test.ts`:
- Around line 5-64: The MockWebContents class in BrowserTab.test.ts is
duplicated in YoBrowserPresenter.test.ts; extract the shared implementation into
a single exported class (e.g., MockWebContents) in a test utility module
(suggested: test/main/__mocks__/MockWebContents.ts) and replace the local class
definitions by importing that exported MockWebContents in both
BrowserTab.test.ts and YoBrowserPresenter.test.ts; ensure the exported class
preserves all members used by tests (url, title, destroyed, loading,
pendingLoad, debugger, session, navigationHistory, loadURL, isLoading,
finishLoad, emitDomReady, getURL, getTitle, isDestroyed, reload, goBack,
goForward, sendInputEvent) so tests continue to work.

In `@test/renderer/components/ChatSidePanel.test.ts`:
- Around line 1-83: Add teardown and negative-case coverage: after mounting and
assertions, remove/restore the globals and handlers by deleting window.api and
window.electron (or restoring originals if saved), clear the handlers Map, and
call the mocked ipcRenderer.removeListener if needed to mimic cleanup for the
ChatSidePanel test (references: handlers Map, window.api.getWindowId,
window.electron.ipcRenderer.on/removeListener, sidepanelStore.openBrowser). Also
add a second test that imports ChatSidePanel, registers the same mocks but calls
the stored handler with a non-matching payload (e.g., { windowId: 999 }) and
asserts sidepanelStore.openBrowser was not called to cover the mismatch edge
case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 703d1403-e26c-46a4-b9b9-e8cc9c7725af

📥 Commits

Reviewing files that changed from the base of the PR and between e1a089b and 815221c.

📒 Files selected for processing (12)
  • src/main/events.ts
  • src/main/presenter/browser/BrowserTab.ts
  • src/main/presenter/browser/YoBrowserPresenter.ts
  • src/main/presenter/browser/YoBrowserToolDefinitions.ts
  • src/main/presenter/browser/YoBrowserToolHandler.ts
  • src/renderer/src/components/sidepanel/BrowserPanel.vue
  • src/renderer/src/components/sidepanel/ChatSidePanel.vue
  • src/renderer/src/events.ts
  • test/main/presenter/YoBrowserPresenter.test.ts
  • test/main/presenter/browser/BrowserTab.test.ts
  • test/renderer/components/BrowserPanel.test.ts
  • test/renderer/components/ChatSidePanel.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/renderer/src/components/chat/mentions/utils.ts (1)

120-128: Avoid making the empty slash menu unbounded.

Because useChatInputMentions forwards this result straight into SuggestionList, and SuggestionList.vue now renders props.items verbatim, returning items unchanged here means typing / can create a button for every command, prompt, and tool before the user narrows the query. That is an easy UI latency path once MCP catalogs get large. Consider capping the empty state too, or virtualizing the list if showing everything is intentional.

♻️ Simple cap for the empty-query path
 export const filterSlashSuggestionItems = (
   items: SlashSuggestionItem[],
   query: string,
   limit = MAX_FILTERED_SLASH_SUGGESTIONS
 ): SlashSuggestionItem[] => {
   const normalized = query.trim().toLowerCase()
   if (!normalized) {
-    return items
+    return items.slice(0, limit)
   }

   return items
     .filter((item) => {
       if (item.label.toLowerCase().includes(normalized)) return true
       return item.description?.toLowerCase().includes(normalized)
     })
     .slice(0, limit)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/chat/mentions/utils.ts` around lines 120 - 128,
filterSlashSuggestionItems currently returns the full items array when the
normalized query is empty, which lets typing "/" render an unbounded list;
change the empty-query path in filterSlashSuggestionItems to cap the results
using the provided limit (e.g., return only the first limit entries of items)
instead of returning items unchanged so SuggestionList receives a bounded set
(or alternatively hook SuggestionList to a virtualization strategy if you intend
to show everything).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/renderer/src/components/chat/mentions/utils.ts`:
- Around line 120-128: filterSlashSuggestionItems currently returns the full
items array when the normalized query is empty, which lets typing "/" render an
unbounded list; change the empty-query path in filterSlashSuggestionItems to cap
the results using the provided limit (e.g., return only the first limit entries
of items) instead of returning items unchanged so SuggestionList receives a
bounded set (or alternatively hook SuggestionList to a virtualization strategy
if you intend to show everything).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a213d3e4-a5a1-4303-9b25-f46b0e2eee24

📥 Commits

Reviewing files that changed from the base of the PR and between 815221c and d64a7c8.

📒 Files selected for processing (5)
  • src/renderer/src/components/chat/composables/useChatInputMentions.ts
  • src/renderer/src/components/chat/mentions/SuggestionList.vue
  • src/renderer/src/components/chat/mentions/utils.ts
  • test/renderer/components/SuggestionList.test.ts
  • test/renderer/composables/useChatInputMentions.test.ts

@zerob13 zerob13 merged commit 1b44101 into dev Mar 12, 2026
2 checks passed
@zerob13 zerob13 deleted the codex/fix-browser-open branch March 29, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant