Skip to content

Ade Browser#243

Merged
arul28 merged 8 commits into
mainfrom
ade/ade-browser-d5ca6ac8
May 4, 2026
Merged

Ade Browser#243
arul28 merged 8 commits into
mainfrom
ade/ade-browser-d5ca6ac8

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 4, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Summary by CodeRabbit

  • New Features

    • Built-in browser: tabs, navigation, screenshots, element inspection; CLI browser commands and desktop API surface; chat-integrated browser panel.
    • Resizable work sidebar with Git/Files/iOS/AppControl/Browser tools; automatic terminal URL detection opening in ADE browser.
  • Improvements

    • Better lane-aware behavior (props, launch/connect flows, workspace selection); unified link handling to open in ADE browser.
  • Removed

    • Right-edge floating pane and floating files workspace UI.
  • Tests

    • Added browser command, service, and filtering behavior tests.

Greptile Summary

This PR introduces a full built-in browser for the ADE desktop app: a new BuiltInBrowserService (multi-tab, CDP-backed inspection, screenshots), a large React panel (ChatBuiltInBrowserPanel) using <webview> tags, a resizable WorkSidebar replacing the removed floating panes, and terminal URL auto-detection that opens links in the ADE browser.

Three P1 issues remain:

  • switchTab calls clearSelectionInternal() unconditionally, silently discarding the user's selected element even when switching to the already-active tab.
  • Same-tab navigation in navigate() leaves inspecting = true in service state while the CDP Overlay.setInspectMode is lost on page reload, desynchronizing the UI badge from actual inspect capability.
  • The updated frame-src CSP now includes file: and app: origins in production (via cspSources), permitting file:// iframe loading in the renderer where frame-src 'none' was previously enforced.

Confidence Score: 3/5

Not safe to merge without addressing the CSP frame-src regression and the inspect-state desync bug.

Three P1 findings: inspect state desynchronization after same-tab navigation, unconditional selection clear in switchTab, and a security-relevant CSP broadening (file: in frame-src). Multiple P1s on a core-path feature pull the score to 3.

apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts (inspect desync + switchTab), apps/desktop/src/main/main.ts (frame-src CSP)

Security Review

  • frame-src allows file: in production (main.ts): The new frame-src directive includes cspSources which in production mode is 'self' file: app:. This permits file:// URLs inside renderer <iframe> elements, enabling local filesystem access if any XSS or injected iframe is possible. The previous value was frame-src 'none'.
  • webviewTag: true enabled (main.ts): Expands attack surface; mitigated by will-attach-webview and did-attach-webview handlers that enforce http/https-only navigation and strip preload scripts.
  • No credential leakage or injection vulnerabilities identified in the new browser service or IPC handlers.

Important Files Changed

Filename Overview
apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts New 1301-line browser service managing tabs, navigation, inspection, and screenshots; two P1s: inspect state desync on same-tab navigation and unconditional selection clear in switchTab
apps/desktop/src/main/main.ts Enables webviewTag, adds will-attach-webview/did-attach-webview guards, wires up builtInBrowserService; frame-src CSP now includes file: and app: origins in production
apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx 1778-line React panel with webview-backed tabs, screenshot capture, element inspection, and address-bar normalization; correctly rejects unsupported URL schemes
apps/desktop/src/renderer/lib/openExternal.ts Adds openUrlInAdeBrowser with module-level pending-navigation flag to prevent double-tab race when browser sidebar mounts concurrently with a link-click navigate IPC
apps/desktop/src/renderer/components/terminals/TerminalView.tsx Adds xterm link provider detecting http(s) and localhost URLs in terminal output, opening them in the ADE built-in browser
apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx New resizable work sidebar with Git, Files, iOS, AppControl, and Browser tabs, replacing the removed floating panes
apps/desktop/src/main/services/ipc/registerIpc.ts Registers IPC handlers for all browser operations; follows existing patterns for iosSimulator and appControl

Sequence Diagram

sequenceDiagram
    participant T as Terminal / Markdown Link
    participant OE as openExternal.ts
    participant TP as TerminalsPage
    participant BP as ChatBuiltInBrowserPanel
    participant IPC as Electron IPC
    participant SVC as BuiltInBrowserService

    T->>OE: openUrlInAdeBrowser(url)
    OE->>OE: markPendingBuiltInBrowserNavigation()
    OE->>TP: dispatch ADE_OPEN_BUILT_IN_BROWSER_EVENT
    TP->>BP: mount (sidebar switches to browser tab)
    OE->>IPC: browser.navigate({url, newTab:true})
    IPC->>SVC: navigate()
    SVC->>SVC: normalizeBrowserUrl, tab-limit check
    SVC->>SVC: createTabState(), push to tabs
    SVC-->>IPC: BuiltInBrowserStatus
    IPC-->>BP: status update via onEvent
    BP->>BP: useEffect: consumePendingBuiltInBrowserNavigation()
    note over BP: flag was true → skip Google tab creation
    BP->>IPC: attachWebview({tabId, webContentsId})
    IPC->>SVC: attachWebview()
    SVC-->>BP: webview attached
    BP->>SVC: setBounds({visible:true, ...})
    SVC->>SVC: attachViewsToCurrentWindow()
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/main/main.ts, line 471-483 (link)

    P1 security frame-src now allows file: and app: origins in production

    In production, cspSources = "'self' file: app:", so the new frame-src directive becomes frame-src 'self' file: app: http://localhost:* http://127.0.0.1:* about:. This permits any file:// URL to load inside an <iframe> in the renderer. Since the built-in browser uses <webview> tags (not iframes), consider tightening frame-src to 'self' about: in production, mirroring how webview access is separately gated by will-attach-webview.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/main.ts
    Line: 471-483
    
    Comment:
    **`frame-src` now allows `file:` and `app:` origins in production**
    
    In production, `cspSources = "'self' file: app:"`, so the new `frame-src` directive becomes `frame-src 'self' file: app: http://localhost:* http://127.0.0.1:* about:`. This permits any `file://` URL to load inside an `<iframe>` in the renderer. Since the built-in browser uses `<webview>` tags (not iframes), consider tightening `frame-src` to `'self' about:` in production, mirroring how `webview` access is separately gated by `will-attach-webview`.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts:511-516
**`switchTab` clears selection unconditionally, even for same-tab switches**

`clearSelectionInternal()` is called on every `switchTab` invocation, regardless of whether `tab.id === activeTabId`. If a caller (e.g., the UI re-rendering) calls `switchTab` with the already-active tab, the user's selected element context is silently discarded. The `stopInspectQuietly` call already has the correct guard (`tab.id !== activeTabId`); `clearSelectionInternal` should use the same guard.

### Issue 2 of 4
apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts:449-480
**Inspect mode desynchronizes with CDP overlay after same-tab navigation**

When `navigate()` is called without `newTab` and without a different `tabId`, `switchingTabs` evaluates to `false`, so `stopInspectQuietly()` is never called. However, Electron's CDP `Overlay.setInspectMode` command is page-level and is lost when the WebContents navigates. After the load completes, `inspecting` in service state remains `true` and `getStatus().isInspecting` returns `true`, but the CDP overlay is gone — the user sees the "Inspecting" badge in the UI but clicking elements has no effect. The fix is to always call `stopInspectQuietly()` when loading a URL into the current tab, not only when switching tabs.

### Issue 3 of 4
apps/desktop/src/main/main.ts:471-483
**`frame-src` now allows `file:` and `app:` origins in production**

In production, `cspSources = "'self' file: app:"`, so the new `frame-src` directive becomes `frame-src 'self' file: app: http://localhost:* http://127.0.0.1:* about:`. This permits any `file://` URL to load inside an `<iframe>` in the renderer. Since the built-in browser uses `<webview>` tags (not iframes), consider tightening `frame-src` to `'self' about:` in production, mirroring how `webview` access is separately gated by `will-attach-webview`.

### Issue 4 of 4
apps/desktop/src/renderer/components/terminals/TerminalView.tsx:155-196
**Link range `endX` uses cleaned text length**

`endX = startX + text.length` where `text` is the result of `cleanTerminalLinkText(match[0])` — which strips trailing punctuation. xterm clips range ends internally so this is safe, and the existing `if (!text.length) continue` guard prevents zero-width links. No change needed — noting for clarity.

Reviews (7): Last reviewed commit: "ship: iteration 5 — final review fixes" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview May 4, 2026 6:34am

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 4, 2026

@copilot review but do not make fixes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Adds a built-in browser service (Electron CDP-backed) with IPC/preload/renderer bindings, CLI browser commands, chat integration for browser context and screenshots, and replaces the right-edge floating pane with a persistent work sidebar; also threads laneId through App Control and iOS simulator sessions.

Changes

Built-In Browser Service

Layer / File(s) Summary
Type Definitions
apps/desktop/src/shared/types/builtInBrowser.ts, apps/desktop/src/shared/types/index.ts, apps/ade-cli/src/bootstrap.ts
New built-in browser types and event payloads; AdeRuntime extended with optional `builtInBrowserService?: BuiltInBrowserService
Core Service
apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts
New factory createBuiltInBrowserService implementing multi-tab model, webContents/view attachment, CDP-driven inspect/select, screenshot capture, bounds/visibility, event emission, and dispose. Exports BuiltInBrowserService type alias.
Main Process Wiring
apps/desktop/src/main/main.ts, apps/desktop/src/main/services/adeActions/registry.ts
Create service at startup (partitioned), attach to windows, include in per-project AdeRuntime, register action domain built_in_browser, and dispose on shutdown. Added webview security (partition, will-attach-webview) and CSP frame-src updates.
IPC Handlers
apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/shared/ipc.ts
Extended AppContext/registerIpc to accept optional builtInBrowserService; added IPC channels for builtInBrowser.*; added sender trust, per-sender rate-limiting, payload parsing/validation, and ipcMain handlers mapping to service methods.
Preload / Renderer Bridge
apps/desktop/src/preload/global.d.ts, apps/desktop/src/preload/preload.ts
Expose window.ade.builtInBrowser typed API; added short TTL status cache and event fanout clearing on events; implemented async bridge methods for status, bounds, attachWebview, navigation, tab lifecycle, inspect/select, screenshots, and onEvent subscription.
Renderer Integration & UX
apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx, apps/desktop/src/renderer/components/chat/AgentChatPane.tsx, apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
New ChatBuiltInBrowserPanel implementing tab strip, navigation, inspector, screenshot-crop capture, native webview attachment, and chat-context attachment flows. AgentChatPane/Composer accept and render built-in browser context items and chips; composer supports selecting/removing a browser context.
Open/Navigation Helpers
apps/desktop/src/renderer/lib/openExternal.ts, apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx, apps/desktop/src/renderer/components/run/LaneRuntimeBar.tsx, apps/desktop/src/renderer/components/terminals/TerminalView.tsx
Added openUrlInAdeBrowser with URL normalization/eligibility and pending-navigation guards; integrated into markdown links, preview OPEN buttons, and terminal link provider.
CLI
apps/ade-cli/src/cli.ts, apps/ade-cli/src/cli.test.ts
Added top-level browser command family building built_in_browser JSON-RPC calls; formatter browser-status; tests mapping browser CLI to built_in_browser actions.
Tests
apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts, apps/ade-cli/src/cli.test.ts
Vitest coverage for service status/dedupe/dispose and CLI plan tests for browser command mappings.

Work Sidebar Reorganization

Layer / File(s) Summary
Types / Session lane
apps/desktop/src/shared/types/appControl.ts, apps/desktop/src/shared/types/iosSimulator.ts
App Control and iOS simulator arg/session shapes now include laneId fields (connect/launch args and session objects).
Persisted Sidebar State
apps/desktop/src/renderer/state/appStore.ts, apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.ts
Added WorkSidebarTab type and new persisted workSidebarOpen, workSidebarTab, workSidebarWidthPct with normalization; default values updated.
Hook & URL handling
apps/desktop/src/renderer/components/terminals/useWorkSessions.ts, apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts
Route-aware useWorkSessions changes: isWorkRoute, sidebar state/setters, lane selection from active tab (work-route gated), filterStatus support, URL-filter stripping with replace, and tests for lane/status selection and URL-filter behavior.
Sidebar Component
apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx
New WorkSidebar rendering Git/Files/iOS/App Control/Browser tabs, per-tab content, lane-tool session syncing, dispatch-to-chat helpers, and gating with warning banners when tool/lane mismatch exists.
Work Area Integration
apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx, apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx, apps/desktop/src/renderer/components/terminals/WorkStartSurface.tsx
TerminalsPage adds a resizable right work sidebar, persists width, listens for ADE browser-open events to switch sidebar tab, and passes sidebar controls into WorkViewArea; WorkViewArea exposes toggle and renders toggle button; WorkStartSurface hides lane tool drawers when requested.
Floating Pane Removal
apps/desktop/src/renderer/components/app/RightEdgeFloatingPane.tsx, apps/desktop/src/renderer/components/app/FloatingFilesWorkspace.tsx, apps/desktop/src/renderer/components/app/AppShell.tsx, apps/desktop/src/renderer/index.css
Removed RightEdgeFloatingPane and FloatingFilesWorkspace components and associated CSS; AppShell no longer renders the right-edge pane.
Files Page
apps/desktop/src/renderer/components/files/FilesPage.tsx, apps/desktop/src/renderer/components/files/FilesPage.test.tsx
FilesPage gains preferredLaneId and embedded props; chooses default workspace per lane and hides header controls in embedded mode; test added for lane-based default workspace.
Session Filtering UI
apps/desktop/src/renderer/components/terminals/SessionListPane.tsx, apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
Added status filter toggle UI and test; filterStatus/setFilterStatus bound directly.
Chat Panels: iOS/App Control
apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx, apps/desktop/src/renderer/components/chat/ChatAppControlPanel.tsx
Panels accept optional laneId, onAddContext made optional with guards; ChatAppControlPanel scopes persisted UI key by laneId and passes laneId into connect/launch calls; messaging text updated to generic “in the terminal.”

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

desktop, ios

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/ade-browser-d5ca6ac8

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot review but do not make fixes

Here's the review. No fixes made.


Overall

Solid feature — the layered security model (URL filtering at both renderer and main, will-attach-webview lockdown, guardBuiltInBrowserIpc trusted-sender check, rate limiting, permission denial in the session handler) is well-thought-out. The debugger lifecycle management and withTimeout wrappers around CDP commands are good defensive choices. A few things worth addressing before shipping:


Security / Correctness

1. normalizeUrlForNavigation (renderer) passes blob:, data:, file:, devtools: through to the backend
ChatBuiltInBrowserPanel.tsx:233 returns those schemes verbatim. The backend's normalizeBrowserUrl throws on all of them, so users get a confusing IPC error rather than a UI-level rejection. The renderer should validate the same set of allowed protocols it knows the backend accepts.

2. NODE_METADATA_FUNCTION reads element.value unconditionally (service line ~1201)
This includes <input type="password"> fields — up to 300 chars land in BuiltInBrowserContextItem.metadata.value and can be forwarded to the agent prompt via formatBuiltInBrowserContextForPrompt. Consider masking or omitting value when element.type === "password".

3. Debugger state is shared across all tabs
debuggerAttachedForInspect, debuggerMessageListener, and debuggerDetachListener are closure-level singletons in createBuiltInBrowserService. If two tabs race through startInspect/stopInspect (e.g. a tab switch mid-inspect), the attach flag can be set by one WebContents and cleared against a different one. The listeners registered on one wc may fire after the tab switches to a different wc.

4. webviewTag: true is now globally enabled for the main window
The will-attach-webview guard is a good control, but any renderer code can now create a <webview> element — not just the browser panel. This is a meaningful surface expansion.


Architecture / Maintainability

5. Massive single-file components
builtInBrowserService.ts is 1,237 lines and ChatBuiltInBrowserPanel.tsx is 1,747 lines. The panel in particular bundles all local types, ~20 utility functions, the webview lifecycle logic, the capture/crop flow, and the full JSX. This will be hard to review and iterate on.

6. Renderer redeclares types that already exist in shared/types/builtInBrowser.ts
ChatBuiltInBrowserPanel.tsx has local BrowserFrame, BuiltInBrowserContextItem, BuiltInBrowserStatus, BuiltInBrowserTab, etc. They already differ — e.g. BuiltInBrowserTab.isLoading in shared types vs loading in the renderer local type. This drift is why normalizeTab has booleanField(value.loading, booleanField(value.isLoading, false)) as a double-fallback. Better to import from the shared package and derive renderer view models from it.

7. getBrowserApi() casts through unknown despite global.d.ts typing it directly
(window.ade as unknown as { builtInBrowser?: BuiltInBrowserApi }) at line 216. Since global.d.ts already types window.ade.builtInBrowser as the full API (non-optional), this cast hides a type sync problem — the renderer's local BuiltInBrowserApi shape and the preload declaration are parallel, not unified.

8. hideBuiltInBrowserView() in WorkSidebar.tsx is not null-safe
It calls window.ade.builtInBrowser.stopInspect() and window.ade.builtInBrowser.setBounds() directly without checking availability. The other tool panels (appControl, iosSimulator) use a guarded useEffect pattern; this function runs unconditionally on every tab change.


Minor

9. autoAttachedContextIdsRef grows unboundedly — IDs are added on element selection but never removed, so a long-lived panel session accumulates IDs indefinitely. Low impact but worth a size cap or clear-on-navigate.

10. Double stopInspect in handleInspectNodeRequested finally block — line 794 calls await stopInspect(), then the finally (line 796) checks if (inspecting) and calls it again. After a successful stopInspect(), inspecting is false, so the re-entry never fires in the happy path. But if stopInspect() throws, both the original and finally invocation fail separately. The intent should be clarified.

11. configureBrowserSession() call in attachWebview — called after the webview's WebContents already exist. The session was already initialized (or not) by the webview's own partition; calling configureBrowserSession() here configures it retroactively but that's only relevant for the permission handler and CSP webRequest headers that apply to future requests, not the already-initialized webview context.

Copilot finished work on behalf of arul28 May 4, 2026 04:42
Comment thread apps/desktop/src/main/main.ts
Comment thread apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx Outdated
Copy link
Copy Markdown

@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: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/desktop/src/renderer/components/files/FilesPage.tsx (1)

1279-1308: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't seed workspaceId from a stale lane selection.

This effect only runs once, so if selectedLaneIdFromStore hydrates or changes before listWorkspaces() resolves, the callback still uses the lane from the first render. That can lock the page onto the wrong workspace, and the fallback at Lines 1318-1322 will not repair it once workspaceId is non-empty.

Possible fix
+  const selectedLaneIdRef = useRef(selectedLaneId);
+  useEffect(() => {
+    selectedLaneIdRef.current = selectedLaneId;
+  }, [selectedLaneId]);
+
   useEffect(() => {
     const startedAt = performance.now();
     logRendererDebugEvent("renderer.files.list_workspaces.begin");
     window.ade.files.listWorkspaces()
       .then((items) => {
@@
         setWorkspaceId((current) => {
           if (current && items.some((workspace) => workspace.id === current)) return current;
           if (current) {
             setOpenTabs([]);
             setActiveTabPath(null);
             setSelectedNodePath(null);
           }
-          return defaultFilesWorkspaceId(items, selectedLaneId);
+          return defaultFilesWorkspaceId(items, selectedLaneIdRef.current);
         });
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/files/FilesPage.tsx` around lines 1279 -
1308, The then-handler for window.ade.files.listWorkspaces() captures a stale
selectedLaneId from the initial render which can cause setWorkspaceId to pick
the wrong workspace; change the handler to read the latest lane selection (e.g.,
keep selectedLaneId in a ref like selectedLaneIdRef that you update when
selectedLaneId changes) and use that ref inside the listWorkspaces().then(...)
callback when calling defaultFilesWorkspaceId and deciding whether to clear
tabs; update references to selectedLaneId in the setWorkspaceId callback to use
the ref (or otherwise obtain the current selectedLaneId at resolution time) so
workspace selection is based on the most recent lane.
apps/desktop/src/main/services/appControl/appControlService.ts (1)

1360-1368: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Launch lane resolution ignores explicit launchArgs.laneId when no resolver is provided.

Line 1360 uses only args.resolveLaneId?.(...); since that callback is optional, launch can fail even when the caller already supplied a valid laneId. Mirror the connect-path fallback and normalize before storing.

💡 Suggested fix
-    const laneId = await args.resolveLaneId?.({
+    const resolvedLaneId = await args.resolveLaneId?.({
       projectRoot,
       cwd: resolved.cwd,
       laneId: launchArgs.laneId ?? null,
       chatSessionId: launchArgs.chatSessionId ?? null,
     });
-    if (!laneId) {
+    const laneId = (resolvedLaneId ?? launchArgs.laneId ?? null)?.trim() || null;
+    if (!laneId) {
       throw new Error("App Control could not resolve a lane for the terminal. Select a lane or pass laneId.");
     }

Also applies to: 1374-1374

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

In `@apps/desktop/src/main/services/appControl/appControlService.ts` around lines
1360 - 1368, The current launch path only calls the optional resolver
(args.resolveLaneId?.(...)) and throws if it returns falsy, ignoring an explicit
launchArgs.laneId; change the logic around the laneId variable so you first
normalize/accept launchArgs.laneId when present, then call args.resolveLaneId as
a fallback (or use its result only if launchArgs.laneId is null/undefined),
normalize the final laneId before storing, and apply the same pattern where you
resolve/store laneId elsewhere (the other resolve usage). Update the code using
the resolveLaneId symbol and the laneId local variable to mirror the
connect-path fallback behavior.
apps/desktop/src/main/services/ipc/registerIpc.ts (1)

1763-1784: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redact built-in browser data from IPC trace logs.

These new browser channels now flow through ipc.invoke.begin/ipc.invoke.done, but the redaction map still only covers app-control and terminal payloads. In dev or with ADE_TRACE_IPC=1, navigated URLs and browser status/screenshot data can land in logs, including OAuth codes or embedded credentials.

🛡️ Minimal redaction patch
   const ipcChannelRedactionMap: Record<string, ReadonlySet<string>> = {
     [IPC.appControlLaunch]: new Set(["command", "env"]),
     [IPC.appControlLaunchInTerminal]: new Set(["command", "env"]),
     [IPC.appControlTypeText]: new Set(["text"]),
     [IPC.appControlDispatchKey]: new Set(["text", "unmodifiedText", "key", "code"]),
     [IPC.terminalWrite]: new Set(["data"]),
     [IPC.ptyWrite]: new Set(["data"]),
+    [IPC.builtInBrowserNavigate]: new Set(["url"]),
+    [IPC.builtInBrowserCreateTab]: new Set(["url"]),
   };

You’ll likely also want a small result-redaction hook for IPC.builtInBrowserGetStatus and IPC.builtInBrowserCaptureScreenshot, otherwise the same data can still leak on ipc.invoke.done. As per coding guidelines, apps/desktop/src/**: "Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices."

Also applies to: 1988-2021

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

In `@apps/desktop/src/main/services/ipc/registerIpc.ts` around lines 1763 - 1784,
The IPC redaction currently only covers app-control and terminal channels;
update ipcChannelRedactionMap to include the built-in browser channels (e.g.,
IPC.builtInBrowserGetStatus and IPC.builtInBrowserCaptureScreenshot) and list
sensitive keys to redact such as "url", "href", "location", "code", "token",
"accessToken", "cookies", "screenshot", "image", "data", "html", "title", and
"status" so redactIpcArgsForChannel masks those values; additionally add a
result-redaction hook for invoke responses (the ipc.invoke.done handling path)
to ensure returned payloads from built-in browser calls are scrubbed before
logging/trace output.
🟡 Minor comments (9)
apps/desktop/src/shared/adeCliGuidance.ts-24-24 (1)

24-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inline browser guidance omits clear-selection and drifts from full guidance.

Line 24’s inline browser command list should include clear-selection to match Line 11 and avoid partial command guidance.

Suggested patch
- ... `switch --tab <id>`, `screenshot --text`, and `inspect-start` / `select-current`; ...
+ ... `switch --tab <id>`, `screenshot --text`, and `inspect-start` / `select-current` / `clear-selection`; ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/shared/adeCliGuidance.ts` at line 24, The inline browser
command list in the ADE guidance string is missing the clear-selection command;
update the string literal that contains "inspect-start / select-current" to
include "clear-selection" in that same inline command list (e.g., change
"inspect-start / select-current" to "inspect-start / select-current /
clear-selection" or equivalent with backticks) so the guidance on browser
commands matches the full guidance elsewhere.
apps/desktop/src/renderer/components/terminals/TerminalView.tsx-102-103 (1)

102-103: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize scheme-less localhost links before opening.

Line 183 opens raw matched text. Since the matcher also captures values like localhost:3000, those can fail or be treated inconsistently unless a scheme is added first.

Suggested patch
 function cleanTerminalLinkText(raw: string): string {
   return raw.replace(/[),.;:!?]+$/g, "");
 }
 
+function normalizeTerminalLinkTarget(url: string): string {
+  return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/i.test(url)
+    ? `http://${url}`
+    : url;
+}
+
 function createTerminalLinkProvider(term: Terminal): ILinkProvider {
   return {
@@
           decorations: { underline: true, pointerCursor: true },
           activate(event: MouseEvent) {
             event.preventDefault();
-            openUrlInAdeBrowser(text);
+            openUrlInAdeBrowser(normalizeTerminalLinkTarget(text));
           },
         });
       }

Also applies to: 181-184

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

In `@apps/desktop/src/renderer/components/terminals/TerminalView.tsx` around lines
102 - 103, Normalize scheme-less localhost/127.0.0.1/[::1] matches before
opening by detecting if a matched URL (matched by TERMINAL_LINK_PATTERN) lacks a
scheme (e.g., "localhost:3000" or "127.0.0.1:5000") and prepending "http://" (or
"http://[::1]" for IPv6) before passing it to the opener; update the link-open
handler in TerminalView (the code that currently opens the raw matched text) to
perform this normalization so localhost-style matches are consistently valid
URLs when opened.
apps/ade-cli/src/cli.ts-2627-2637 (1)

2627-2637: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Parse only one positional URL here.

Line 2631 and Line 2641 currently join the whole remaining argv into the URL. That makes trailing flags or escape-hatch args part of the navigation target before collectGenericObjectArgs() can parse them.

Suggested fix
   if (sub === "open" || sub === "navigate" || sub === "go") {
     const explicitUrl = readValue(args, ["--url"]);
     const tabId = readValue(args, ["--tab", "--tab-id"]);
     const newTab = readFlag(args, ["--new-tab"]);
-    const url = explicitUrl ?? args.filter((arg) => arg !== "--active-tab").join(" ");
+    const url = explicitUrl ?? firstPositional(args);
     if (!url.trim()) throw new CliUsageError("browser open requires a URL.");
     return { kind: "execute", label: "browser open", steps: [actionStep("result", "built_in_browser", "navigate", collectGenericObjectArgs(args, {
       url,
       tabId,
       newTab: newTab ? true : undefined,
@@
   if (sub === "new-tab" || sub === "tab" || sub === "new") {
     const background = readFlag(args, ["--background"]);
-    const url = readValue(args, ["--url"]) ?? (args.length ? args.join(" ") : undefined);
+    const url = readValue(args, ["--url"]) ?? firstPositional(args) ?? undefined;
     return { kind: "execute", label: "browser new tab", steps: [actionStep("result", "built_in_browser", "createTab", collectGenericObjectArgs(args, {
       url,
       activate: background ? false : undefined,
     }))] };
   }

Also applies to: 2639-2645

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

In `@apps/ade-cli/src/cli.ts` around lines 2627 - 2637, The URL is built by
joining all remaining argv tokens which can accidentally include flags; instead
derive url as the first positional token (excluding tokens that start with "--"
and excluding "--active-tab") when explicitUrl is not provided. Replace the
current url = explicitUrl ?? args.filter((arg) => arg !== "--active-tab").join("
") logic with something like: use explicitUrl if present, otherwise find the
first arg that does not startWith("--") and is not "--active-tab" and use that
as the URL; keep passing the original args into collectGenericObjectArgs(args,
...) so flag parsing remains intact (references: readValue, readFlag, args, url,
collectGenericObjectArgs, newTab, tabId).
apps/ade-cli/src/cli.ts-3238-3238 (1)

3238-3238: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Map the new browser aliases into help resolution too.

Line 3238 adds executable aliases, but ade help built-in-browser and ade built-in-browser --help still fall back to top-level help because the alias table above does not normalize those names back to browser.

Suggested fix
   const aliases: Record<string, string> = {
     lane: "lanes",
+    "ade-browser": "browser",
+    "built-in-browser": "browser",
+    "builtin-browser": "browser",
     diff: "diff",
     diffs: "diff",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ade-cli/src/cli.ts` at line 3238, The new executable aliases
("ade-browser", "built-in-browser", "builtin-browser") are checked when
dispatching to buildBrowserPlan but are not normalized for the help-resolution
logic, so "ade built-in-browser --help" falls back to top-level help; update the
help alias normalization to map those aliases back to "browser" (the same
mapping used around the primary variable) — e.g., add the aliases into the alias
table or the normalization step that resolves the primary command so that when
primary === "ade-browser" | "built-in-browser" | "builtin-browser" it is
rewritten to "browser" before help lookups and before calling buildBrowserPlan.
apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx-45-46 (1)

45-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clean up resize listeners when the drag is interrupted.

mousemove/mouseup handlers and the body cursor/user-select styles are only cleared from onUp. If this component unmounts, the sidebar closes, or the window loses focus mid-drag, those globals can stick around and leave the app in a broken resize state.

🧹 Suggested fix
   const workContentPaneRef = useRef<HTMLDivElement | null>(null);
   const workSidebarPaneRef = useRef<HTMLDivElement | null>(null);
+  const activeResizeCleanupRef = useRef<(() => void) | null>(null);
+
+  useEffect(() => () => activeResizeCleanupRef.current?.(), []);
...
   const handleWorkSidebarResizeMouseDown = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
+    activeResizeCleanupRef.current?.();
     event.preventDefault();
     const container = event.currentTarget.parentElement;
     if (!container) return;
...
     const onUp = () => {
-      if (animationFrame != null) {
-        window.cancelAnimationFrame(animationFrame);
-        animationFrame = null;
-      }
-      applyWidth(pendingWidthPct);
-      document.removeEventListener("mousemove", onMove);
-      document.removeEventListener("mouseup", onUp);
-      document.body.style.cursor = "";
-      document.body.style.userSelect = "";
+      cleanup();
+      applyWidth(pendingWidthPct);
       work.setWorkSidebarWidthPct(pendingWidthPct);
     };
+    const cleanup = () => {
+      if (animationFrame != null) {
+        window.cancelAnimationFrame(animationFrame);
+        animationFrame = null;
+      }
+      document.removeEventListener("mousemove", onMove);
+      document.removeEventListener("mouseup", onUp);
+      window.removeEventListener("blur", onUp);
+      document.body.style.cursor = "";
+      document.body.style.userSelect = "";
+      activeResizeCleanupRef.current = null;
+    };
...
+    activeResizeCleanupRef.current = cleanup;
     document.addEventListener("mousemove", onMove);
     document.addEventListener("mouseup", onUp);
+    window.addEventListener("blur", onUp);
   }, [work]);

Also applies to: 440-487

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

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx` around
lines 45 - 46, The mousemove/mouseup handlers and document body styles set
during dragging are only cleared in onUp and can persist if the component
unmounts, sidebar closes, or the window blurs; add a centralized cleanup that
removes the document 'mousemove' and 'mouseup' listeners and resets
document.body.style.cursor and document.body.style.userSelect, and invoke that
cleanup from component unmount (useEffect cleanup), window 'blur' handler, and
any sidebar-close path. Locate the drag setup/teardown code around
workContentPaneRef and workSidebarPaneRef (and functions like
onMove/onUp/startDrag) and ensure the same removal/reset logic is called from
those lifecycle/blur handlers so no global listeners or styles remain if a drag
is interrupted.
apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx-2223-2224 (1)

2223-2224: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Show a user-visible message when context capture is unavailable

Line 2223 and Line 2396 return early when onAddContext is missing, so capture actions can appear to do nothing. Mirror the selectElementAt behavior and set a message before returning.

Proposed fix
  const addSimulatorCaptureContext = useCallback(async (frame: PreviewCrop["frame"]) => {
    if (!snapshot?.screenshot.dataUrl) return;
-   if (!onAddContext) return;
+   if (!onAddContext) {
+     setMessage("Chat attachments are not available in this panel.");
+     return;
+   }
    const capture = await attachSimulatorCapture(frame).catch(() => null);
    // ...
  }, [attachSimulatorCapture, onAddContext, snapshot]);

  const addPreviewCaptureContext = useCallback(async (frame: PreviewCrop["frame"]) => {
    if (!previewImage || !selectedPreviewTarget || !previewResult?.dataUrl) return;
-   if (!onAddContext) return;
+   if (!onAddContext) {
+     setMessage("Chat attachments are not available in this panel.");
+     return;
+   }
    const capture = await attachPreviewCapture(frame).catch(() => null);
    // ...
  }, [attachPreviewCapture, onAddContext, previewImage, previewResult, selectedPreviewTarget]);

Also applies to: 2396-2397

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

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx` around
lines 2223 - 2224, The early returns when onAddContext is falsy (and when
attachSimulatorCapture fails) silently do nothing; mirror selectElementAt's
behavior by showing a user-visible message before returning. Specifically, in
the branches that check onAddContext and after attachSimulatorCapture.catch(()
=> null) where the code currently returns, call the same UI message/notification
helper used by selectElementAt (e.g., the setMessage/showToast function that
selectElementAt uses) with a clear text like "Context capture is unavailable"
and then return; reference onAddContext, attachSimulatorCapture, and
selectElementAt to locate where to add the message.
apps/desktop/src/renderer/lib/openExternal.ts-39-47 (1)

39-47: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Only emit the built-in-browser event when ADE can actually use it.

Right now the sidebar/browser event fires before the window.ade?.builtInBrowser check, so unsupported environments still switch the UI toward the built-in browser even though the URL falls back to openExternalUrl().

Suggested fix
-  window.dispatchEvent(new CustomEvent<OpenBuiltInBrowserDetail>(ADE_OPEN_BUILT_IN_BROWSER_EVENT, {
-    detail: { url: normalized },
-  }));
   const browser = window.ade?.builtInBrowser;
   if (browser) {
+    window.dispatchEvent(new CustomEvent<OpenBuiltInBrowserDetail>(ADE_OPEN_BUILT_IN_BROWSER_EVENT, {
+      detail: { url: normalized },
+    }));
     void browser.navigate({ url: normalized, newTab: true }).catch(() => openExternalUrl(normalized));
     return;
   }

As per coding guidelines, "apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices."

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

In `@apps/desktop/src/renderer/lib/openExternal.ts` around lines 39 - 47, The
event ADE_OPEN_BUILT_IN_BROWSER_EVENT is dispatched before verifying the
built-in browser exists, causing the UI to switch even when we fallback to
openExternalUrl; modify openExternal.ts so you first check
window.ade?.builtInBrowser (and the browser variable) and only dispatch the
CustomEvent when builtInBrowser is present, then call
browser.navigate(...).catch(() => openExternalUrl(normalized)); otherwise call
openExternalUrl(normalized) without emitting the event; use the existing symbols
ADE_OPEN_BUILT_IN_BROWSER_EVENT, window.ade, builtInBrowser, browser.navigate,
openExternalUrl and normalized to locate and change the logic.
apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx-146-178 (1)

146-178: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the normalized browser fields before falling back to metadata.

ChatBuiltInBrowserPanel.normalizeContextItem() writes text, selector, role, and tagName onto the top-level item, but these helpers and the preview mostly read metadata.*. That means browser chips from the new panel will often degrade to "Browser element" and lose their selector/role hints unless the backend duplicates the same data into metadata.

Proposed fix
 function builtInBrowserContextDisplayLabel(item: BuiltInBrowserContextItem): string {
   const metadata = item.metadata ?? {};
-  for (const value of [metadata.label, metadata.text, metadata.value, item.componentId, metadata.selector, metadata.role, metadata.tagName]) {
+  for (const value of [
+    item.text,
+    item.selector,
+    item.title,
+    metadata.label,
+    metadata.text,
+    metadata.value,
+    item.componentId,
+    item.role,
+    item.tagName,
+    metadata.selector,
+    metadata.role,
+    metadata.tagName,
+  ]) {
     if (typeof value === "string" && value.trim()) return value.trim();
   }
   return "Browser element";
 }
 
 function builtInBrowserContextRoleHint(item: BuiltInBrowserContextItem): string | null {
   const metadata = item.metadata ?? {};
-  const role = typeof metadata.role === "string" ? metadata.role.trim() : null;
+  const role = typeof item.role === "string" && item.role.trim()
+    ? item.role.trim()
+    : typeof metadata.role === "string" ? metadata.role.trim() : null;
   if (role) return role;
-  const tag = typeof metadata.tagName === "string" ? metadata.tagName.trim() : null;
+  const tag = typeof item.tagName === "string" && item.tagName.trim()
+    ? item.tagName.trim()
+    : typeof metadata.tagName === "string" ? metadata.tagName.trim() : null;
   return tag ? tag.toLowerCase() : null;
 }
-                  {typeof selectedBuiltInBrowserContext.metadata.selector === "string" ? (
+                  {typeof (selectedBuiltInBrowserContext.selector ?? selectedBuiltInBrowserContext.metadata.selector) === "string" ? (
                     <div className="truncate font-mono text-[length:calc(var(--chat-font-size)*9/14)] text-teal-50/55">
-                      {selectedBuiltInBrowserContext.metadata.selector}
+                      {selectedBuiltInBrowserContext.selector ?? String(selectedBuiltInBrowserContext.metadata.selector)}
                     </div>
                   ) : null}

Also applies to: 2215-2259

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

In `@apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx` around lines
146 - 178, The helpers (builtInBrowserContextDisplayLabel,
builtInBrowserContextSourceDescription, builtInBrowserContextRoleHint,
builtInBrowserContextFrameHint) should prefer normalized top-level fields
written by ChatBuiltInBrowserPanel.normalizeContextItem (e.g., item.text,
item.selector, item.role, item.tagName, item.url) before falling back to
metadata.*, so update builtInBrowserContextDisplayLabel to check item.text,
item.selector, item.value, item.componentId, item.role, item.tagName (and
item.metadata.* only after those), update builtInBrowserContextRoleHint to check
item.role then item.tagName then metadata, and ensure
builtInBrowserContextSourceDescription keeps checking item.url first then
metadata.url; apply the same ordering change to any duplicate helper code in the
2215-2259 region so chips use the normalized fields.
apps/desktop/src/preload/preload.ts-888-891 (1)

888-891: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Invalidate the browser status cache after selectPoint().

selectPoint() mutates hasSelection in main, but unlike the other state-changing methods it doesn't clear builtInBrowserStatusCache. A caller that does await selectPoint(); await getStatus(); can still read the pre-selection snapshot until the async event arrives or the 500ms TTL expires.

Proposed fix
-    selectPoint: async (args: BuiltInBrowserSelectPointArgs): Promise<BuiltInBrowserSelectResult> =>
-      ipcRenderer.invoke(IPC.builtInBrowserSelectPoint, args),
+    selectPoint: async (args: BuiltInBrowserSelectPointArgs): Promise<BuiltInBrowserSelectResult> =>
+      clearAround(
+        () => builtInBrowserStatusCache.clear(),
+        () => ipcRenderer.invoke(IPC.builtInBrowserSelectPoint, args),
+      ),

Also applies to: 2488-2489

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

In `@apps/desktop/src/preload/preload.ts` around lines 888 - 891, selectPoint()
mutates hasSelection in main but doesn't clear the builtInBrowserStatusCache,
causing callers doing await selectPoint(); await getStatus(); to receive stale
status; after the selectPoint implementation (and the other occurrence at the
similar block around the second instance), call the cache invalidation method on
builtInBrowserStatusCache (e.g., builtInBrowserStatusCache.invalidate() or the
cache's clear() API) immediately after the IPC/selectPoint call so the next
getStatus() fetches fresh state.
🧹 Nitpick comments (1)
apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts (1)

89-96: ⚡ Quick win

dispose test title and assertions are misaligned.

The test says it “stops further events” but only checks post-dispose status fields. Add an explicit no-more-emits assertion (or rename the test to match current assertions).

Proposed test tightening
-it("dispose clears emitted state and stops further events", () => {
+it("dispose clears emitted state and stops further events", async () => {
   const service = createBuiltInBrowserService({ onEvent: collector.onEvent });
+  await service.setBounds({ x: 1, y: 1, width: 0, height: 0, visible: false });
+  const emitsBeforeDispose = collector.events.length;
   service.dispose();
   // dispose itself must not throw; subsequent getStatus reflects an empty service.
   const status = service.getStatus();
   expect(status.tabs).toEqual([]);
   expect(status.attached).toBe(false);
+
+  await service.setBounds({ x: 2, y: 2, width: 0, height: 0, visible: false });
+  expect(collector.events.length).toBe(emitsBeforeDispose);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts`
around lines 89 - 96, The test title and checks are misaligned: either rename
the test to reflect only status assertions or add an explicit assertion that no
further events are emitted after calling dispose; update the test that uses
createBuiltInBrowserService, collector.onEvent, service.dispose and
service.getStatus to also attempt to trigger or simulate an event after dispose
(or call whatever emit/trigger method exists on the service) and assert the
collector did not receive any new events (or that collector.events length is
unchanged), or alternatively change the test name to something like "dispose
clears emitted state and resets status" if you prefer not to add the
no-more-emits assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 2620-2677: buildBrowserPlan currently builds plans that target the
built_in_browser domain without checking for a desktop socket; add a guard at
the start of buildBrowserPlan that verifies a desktop socket is available and
fail fast (throw CliUsageError with a clear message mentioning --socket) when a
browser subcommand is requested and the socket is missing; implement this by
calling an existing helper like
checkDesktopSocketAvailable()/desktopSocketAvailable() or add a small helper
(e.g. ensureDesktopSocketAvailable) and use it before any branch that returns an
execute plan for the built_in_browser domain so execution will not attempt to
run when the runtime lacks the desktop socket.

In `@apps/desktop/src/main/main.ts`:
- Around line 3450-3451: The AppContext shape is inconsistent: dormant contexts
include builtInBrowserService but initContextForProjectRoot() returns live
contexts without it, causing getActiveContext().builtInBrowserService to be
undefined for open projects; fix by adding builtInBrowserService to the object
returned by initContextForProjectRoot() (or ensure it is injected/assigned on
the same code path that constructs the live context) and update the AppContext
type if needed so both dormant and live contexts expose the same
builtInBrowserService property (also mirror this change where getActiveContext()
/ orchestratorService are composed).

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts`:
- Around line 713-731: The debuggerDetachListener currently nulls
debuggerMessageListener and debuggerDetachListener without removing them from
wc.debugger, leaving stale callbacks registered; modify the detach branch inside
attachDebuggerListeners so it calls the existing detachDebuggerListeners(wc)
helper to invoke wc.debugger.off(...) and remove the listeners before clearing
the refs and updating inspecting/debuggerAttachedForInspect and emitting status,
ensuring debuggerMessageListener and debuggerDetachListener are deregistered via
detachDebuggerListeners rather than just being set to null.
- Around line 393-399: The call to
electronWebContents.fromId(input.webContentsId) must be hardened: after
obtaining nextWebContents, validate its origin and type before calling
configureBrowserSession()/configureBrowserWebContents(). Specifically, ensure
nextWebContents exists and is not destroyed, then verify
nextWebContents.getType() indicates a guest/webview (e.g., "guest-webview" or
"webview") and that it belongs to the dedicated browser partition/session
(compare nextWebContents.session or its partition name against the expected
browser partition constant used by configureBrowserSession). If any check fails,
throw and refuse to bind the webContents; only call
configureBrowserWebContents(nextWebContents) when these validations pass.

In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Around line 2230-2235: The parsed webContentsId is used directly in
attachWebview without proving it belongs to the requesting renderer; update the
handlers in builtInBrowserService.ts (the code path that calls
electronWebContents.fromId(...) and the call sites of attachWebview()) to verify
ownership before proceeding: after resolving with
electronWebContents.fromId(input.webContentsId) confirm the resolved WebContents
is owned by the same renderer that sent the IPC (compare to event.sender or the
sender's BrowserWindow via BrowserWindow.fromWebContents(event.sender)), or
consult/maintain an allowlist of valid webContents IDs tied to the requesting
renderer, and reject the request (return error) if the ownership check fails;
keep guardBuiltInBrowserIpc() for origin checks but add this additional
per-request ownership validation wherever attachWebview() is invoked.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 4243-4252: When isParallelLaunch is true the parallel-send branch
forwards only draft and attachments and therefore loses the visual context;
update the parallel launch forwarding logic in AgentChatPane (the code path that
handles sending to child lanes when isParallelLaunch is true) to include the
same visual context fields you build here—use builtInBrowserContextSnapshot (and
the ios/appControl snapshots if applicable) and pass visualContextPrefix and
visualContextDisplayChips along with draft and attachments so child lanes
receive the browser/context chips just like the normal send path; ensure the
forwarded payload keys match what child lanes expect (the same names used by the
non-parallel send branch).
- Around line 3624-3632: The browser temp screenshot save block currently calls
addAttachment(...) which updates latestAttachmentRef and causes subsequent
addIosElementContext/addAppControlContext calls to reuse the browser PNG; change
the flow so saving the temp attachment via
window.ade.agentChat.saveTempAttachment(...) does NOT call addAttachment.
Instead store saved.path in a local variable (e.g., browserTempPath) and update
any UI/state for attachments without touching latestAttachmentRef, or if you
must call addAttachment, capture the previous latestAttachmentRef and
immediately restore it after the call; reference the item.screenshotDataUrl save
block, window.ade.agentChat.saveTempAttachment, addAttachment,
latestAttachmentRef, addIosElementContext and addAppControlContext when making
the change.

In `@apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx`:
- Line 177: The component ChatBuiltInBrowserPanel currently auto-creates a
browser tab pointing to DEFAULT_BROWSER_URL ("https://www.google.com/") when no
tabs exist, causing unsolicited external requests and preventing the empty-state
UI from showing; change this to either (a) use a local blank/start URL constant
(e.g., "about:blank" or an internal route) instead of DEFAULT_BROWSER_URL, or
(b) delay creating a tab until the user explicitly opens a new tab or navigates
(lazy creation). Update the tab-creation logic in ChatBuiltInBrowserPanel and
any related handlers that reference DEFAULT_BROWSER_URL so the empty-state UI is
rendered by default and external navigation only happens on user action.

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx`:
- Around line 421-429: The ADE_OPEN_BUILT_IN_BROWSER_EVENT handler currently
only calls setViewMode("tabs") and setWorkSidebarTab("browser") but does not
open the sidebar when work.workSidebarOpen is false; update the
openBrowserSidebar function to also ensure the sidebar is shown (e.g. call the
appropriate setter such as setWorkSidebarOpen(true) or
setWorkSidebarVisible(true) / setWorkSidebarOpen to true depending on the
available API) so that the browser tab becomes visible; keep the other calls to
setViewMode and setWorkSidebarTab as-is.

In `@apps/desktop/src/renderer/components/terminals/useWorkSessions.ts`:
- Around line 343-350: selectLaneForActiveTab currently only returns early in an
effect but is also invoked from setSelectedSessionId, setActiveItemId, and
openSessionTab, so those callers can change the selected lane while the user is
on non-Work routes; move the route guard into selectLaneForActiveTab itself: at
the top of the selectLaneForActiveTab callback add a check for the current
route/view (e.g. viewMode !== "grid" or your app's Work-route predicate) and
return early if not on the Work route, then keep the existing sessionId/session
lookup and selectLane(session.laneId) behavior so all callers
(setSelectedSessionId, setActiveItemId, openSessionTab) automatically honor the
route gate.

In `@apps/desktop/src/renderer/lib/openExternal.ts`:
- Around line 27-31: The branch currently passes the original raw url to
openExternalUrl even when canOpenInAdeBrowser(normalized) rejects unsafe
schemes; update openUrlInAdeBrowser so it uses the normalized value and never
forwards a rejected scheme to the external opener: call
openExternalUrl(normalized) only when normalized is truthy and
canOpenInAdeBrowser(normalized) returns true, and otherwise drop/return without
calling openExternalUrl. Reference: openUrlInAdeBrowser,
normalizeBrowserUrlInput, canOpenInAdeBrowser, openExternalUrl.

In `@apps/ios/ADE/Views/Work/WorkRootScreen.swift`:
- Around line 46-47: Persisted selectedLaneId (AppStorage in WorkRootScreen) can
be stale and hide sessions; validate and reconcile it against the current lanes
whenever lane data changes. Update the code that receives/loads lane data (e.g.,
after fetching in WorkRootScreen or in the view model backing
WorkFiltersSection) to check if selectedLaneId exists in the current lanes' id
set and if not set selectedLaneId = "all" (or the WorkFiltersSection fallback)
so the UI won’t be stranded on an invalid lane id; you can implement this in the
lanes state observer, onAppear, or a validateSelectedLane() helper invoked after
lanes update.

---

Outside diff comments:
In `@apps/desktop/src/main/services/appControl/appControlService.ts`:
- Around line 1360-1368: The current launch path only calls the optional
resolver (args.resolveLaneId?.(...)) and throws if it returns falsy, ignoring an
explicit launchArgs.laneId; change the logic around the laneId variable so you
first normalize/accept launchArgs.laneId when present, then call
args.resolveLaneId as a fallback (or use its result only if launchArgs.laneId is
null/undefined), normalize the final laneId before storing, and apply the same
pattern where you resolve/store laneId elsewhere (the other resolve usage).
Update the code using the resolveLaneId symbol and the laneId local variable to
mirror the connect-path fallback behavior.

In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Around line 1763-1784: The IPC redaction currently only covers app-control and
terminal channels; update ipcChannelRedactionMap to include the built-in browser
channels (e.g., IPC.builtInBrowserGetStatus and
IPC.builtInBrowserCaptureScreenshot) and list sensitive keys to redact such as
"url", "href", "location", "code", "token", "accessToken", "cookies",
"screenshot", "image", "data", "html", "title", and "status" so
redactIpcArgsForChannel masks those values; additionally add a result-redaction
hook for invoke responses (the ipc.invoke.done handling path) to ensure returned
payloads from built-in browser calls are scrubbed before logging/trace output.

In `@apps/desktop/src/renderer/components/files/FilesPage.tsx`:
- Around line 1279-1308: The then-handler for window.ade.files.listWorkspaces()
captures a stale selectedLaneId from the initial render which can cause
setWorkspaceId to pick the wrong workspace; change the handler to read the
latest lane selection (e.g., keep selectedLaneId in a ref like selectedLaneIdRef
that you update when selectedLaneId changes) and use that ref inside the
listWorkspaces().then(...) callback when calling defaultFilesWorkspaceId and
deciding whether to clear tabs; update references to selectedLaneId in the
setWorkspaceId callback to use the ref (or otherwise obtain the current
selectedLaneId at resolution time) so workspace selection is based on the most
recent lane.

---

Minor comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 2627-2637: The URL is built by joining all remaining argv tokens
which can accidentally include flags; instead derive url as the first positional
token (excluding tokens that start with "--" and excluding "--active-tab") when
explicitUrl is not provided. Replace the current url = explicitUrl ??
args.filter((arg) => arg !== "--active-tab").join(" ") logic with something
like: use explicitUrl if present, otherwise find the first arg that does not
startWith("--") and is not "--active-tab" and use that as the URL; keep passing
the original args into collectGenericObjectArgs(args, ...) so flag parsing
remains intact (references: readValue, readFlag, args, url,
collectGenericObjectArgs, newTab, tabId).
- Line 3238: The new executable aliases ("ade-browser", "built-in-browser",
"builtin-browser") are checked when dispatching to buildBrowserPlan but are not
normalized for the help-resolution logic, so "ade built-in-browser --help" falls
back to top-level help; update the help alias normalization to map those aliases
back to "browser" (the same mapping used around the primary variable) — e.g.,
add the aliases into the alias table or the normalization step that resolves the
primary command so that when primary === "ade-browser" | "built-in-browser" |
"builtin-browser" it is rewritten to "browser" before help lookups and before
calling buildBrowserPlan.

In `@apps/desktop/src/preload/preload.ts`:
- Around line 888-891: selectPoint() mutates hasSelection in main but doesn't
clear the builtInBrowserStatusCache, causing callers doing await selectPoint();
await getStatus(); to receive stale status; after the selectPoint implementation
(and the other occurrence at the similar block around the second instance), call
the cache invalidation method on builtInBrowserStatusCache (e.g.,
builtInBrowserStatusCache.invalidate() or the cache's clear() API) immediately
after the IPC/selectPoint call so the next getStatus() fetches fresh state.

In `@apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx`:
- Around line 146-178: The helpers (builtInBrowserContextDisplayLabel,
builtInBrowserContextSourceDescription, builtInBrowserContextRoleHint,
builtInBrowserContextFrameHint) should prefer normalized top-level fields
written by ChatBuiltInBrowserPanel.normalizeContextItem (e.g., item.text,
item.selector, item.role, item.tagName, item.url) before falling back to
metadata.*, so update builtInBrowserContextDisplayLabel to check item.text,
item.selector, item.value, item.componentId, item.role, item.tagName (and
item.metadata.* only after those), update builtInBrowserContextRoleHint to check
item.role then item.tagName then metadata, and ensure
builtInBrowserContextSourceDescription keeps checking item.url first then
metadata.url; apply the same ordering change to any duplicate helper code in the
2215-2259 region so chips use the normalized fields.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx`:
- Around line 2223-2224: The early returns when onAddContext is falsy (and when
attachSimulatorCapture fails) silently do nothing; mirror selectElementAt's
behavior by showing a user-visible message before returning. Specifically, in
the branches that check onAddContext and after attachSimulatorCapture.catch(()
=> null) where the code currently returns, call the same UI message/notification
helper used by selectElementAt (e.g., the setMessage/showToast function that
selectElementAt uses) with a clear text like "Context capture is unavailable"
and then return; reference onAddContext, attachSimulatorCapture, and
selectElementAt to locate where to add the message.

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx`:
- Around line 45-46: The mousemove/mouseup handlers and document body styles set
during dragging are only cleared in onUp and can persist if the component
unmounts, sidebar closes, or the window blurs; add a centralized cleanup that
removes the document 'mousemove' and 'mouseup' listeners and resets
document.body.style.cursor and document.body.style.userSelect, and invoke that
cleanup from component unmount (useEffect cleanup), window 'blur' handler, and
any sidebar-close path. Locate the drag setup/teardown code around
workContentPaneRef and workSidebarPaneRef (and functions like
onMove/onUp/startDrag) and ensure the same removal/reset logic is called from
those lifecycle/blur handlers so no global listeners or styles remain if a drag
is interrupted.

In `@apps/desktop/src/renderer/components/terminals/TerminalView.tsx`:
- Around line 102-103: Normalize scheme-less localhost/127.0.0.1/[::1] matches
before opening by detecting if a matched URL (matched by TERMINAL_LINK_PATTERN)
lacks a scheme (e.g., "localhost:3000" or "127.0.0.1:5000") and prepending
"http://" (or "http://[::1]" for IPv6) before passing it to the opener; update
the link-open handler in TerminalView (the code that currently opens the raw
matched text) to perform this normalization so localhost-style matches are
consistently valid URLs when opened.

In `@apps/desktop/src/renderer/lib/openExternal.ts`:
- Around line 39-47: The event ADE_OPEN_BUILT_IN_BROWSER_EVENT is dispatched
before verifying the built-in browser exists, causing the UI to switch even when
we fallback to openExternalUrl; modify openExternal.ts so you first check
window.ade?.builtInBrowser (and the browser variable) and only dispatch the
CustomEvent when builtInBrowser is present, then call
browser.navigate(...).catch(() => openExternalUrl(normalized)); otherwise call
openExternalUrl(normalized) without emitting the event; use the existing symbols
ADE_OPEN_BUILT_IN_BROWSER_EVENT, window.ade, builtInBrowser, browser.navigate,
openExternalUrl and normalized to locate and change the logic.

In `@apps/desktop/src/shared/adeCliGuidance.ts`:
- Line 24: The inline browser command list in the ADE guidance string is missing
the clear-selection command; update the string literal that contains
"inspect-start / select-current" to include "clear-selection" in that same
inline command list (e.g., change "inspect-start / select-current" to
"inspect-start / select-current / clear-selection" or equivalent with backticks)
so the guidance on browser commands matches the full guidance elsewhere.

---

Nitpick comments:
In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts`:
- Around line 89-96: The test title and checks are misaligned: either rename the
test to reflect only status assertions or add an explicit assertion that no
further events are emitted after calling dispose; update the test that uses
createBuiltInBrowserService, collector.onEvent, service.dispose and
service.getStatus to also attempt to trigger or simulate an event after dispose
(or call whatever emit/trigger method exists on the service) and assert the
collector did not receive any new events (or that collector.events length is
unchanged), or alternatively change the test name to something like "dispose
clears emitted state and resets status" if you prefer not to add the
no-more-emits assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fcf2ce3a-e6a6-4e66-8dbe-653b8418f08a

📥 Commits

Reviewing files that changed from the base of the PR and between 7820970 and 3d31a5b.

⛔ Files ignored due to path filters (11)
  • .claude/scheduled_tasks.lock is excluded by !**/*.lock
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/chat/composer-and-ui.md is excluded by !docs/**
  • docs/features/computer-use/app-control.md is excluded by !docs/**
  • docs/features/files-and-editor/README.md is excluded by !docs/**
  • docs/features/files-and-editor/editor-surfaces.md is excluded by !docs/**
  • docs/features/ios-simulator/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/ui-surfaces.md is excluded by !docs/**
  • goal.md is excluded by !*.md
📒 Files selected for processing (48)
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/appControl/appControlService.ts
  • apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts
  • apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.test.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/renderer/components/app/FloatingFilesWorkspace.tsx
  • apps/desktop/src/renderer/components/app/RightEdgeFloatingPane.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatAppControlPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx
  • apps/desktop/src/renderer/components/chat/chatMarkdown.tsx
  • apps/desktop/src/renderer/components/files/FilesPage.test.tsx
  • apps/desktop/src/renderer/components/files/FilesPage.tsx
  • apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.ts
  • apps/desktop/src/renderer/components/run/LaneRuntimeBar.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalView.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx
  • apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx
  • apps/desktop/src/renderer/components/terminals/WorkStartSurface.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/renderer/lib/openExternal.ts
  • apps/desktop/src/renderer/state/appStore.test.ts
  • apps/desktop/src/renderer/state/appStore.ts
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/appControl.ts
  • apps/desktop/src/shared/types/builtInBrowser.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/shared/types/iosSimulator.ts
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
💤 Files with no reviewable changes (4)
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/renderer/components/app/RightEdgeFloatingPane.tsx
  • apps/desktop/src/renderer/components/app/FloatingFilesWorkspace.tsx
  • apps/desktop/src/renderer/index.css

Comment thread apps/ade-cli/src/cli.ts
Comment on lines +2620 to +2677
function buildBrowserPlan(args: string[]): CliPlan {
const sub = firstPositional(args) ?? "status";
if (sub === "help") return { kind: "help", text: HELP_BY_COMMAND.browser };
if (sub === "actions") return { kind: "execute", label: "browser actions", steps: [listActionsStep("actions", "built_in_browser")] };
if (sub === "status" || sub === "tabs" || sub === "list") {
return { kind: "execute", label: "browser status", steps: [actionStep("result", "built_in_browser", "getStatus", collectGenericObjectArgs(args))] };
}
if (sub === "open" || sub === "navigate" || sub === "go") {
const explicitUrl = readValue(args, ["--url"]);
const tabId = readValue(args, ["--tab", "--tab-id"]);
const newTab = readFlag(args, ["--new-tab"]);
const url = explicitUrl ?? args.filter((arg) => arg !== "--active-tab").join(" ");
if (!url.trim()) throw new CliUsageError("browser open requires a URL.");
return { kind: "execute", label: "browser open", steps: [actionStep("result", "built_in_browser", "navigate", collectGenericObjectArgs(args, {
url,
tabId,
newTab: newTab ? true : undefined,
}))] };
}
if (sub === "new-tab" || sub === "tab" || sub === "new") {
const background = readFlag(args, ["--background"]);
const url = readValue(args, ["--url"]) ?? (args.length ? args.join(" ") : undefined);
return { kind: "execute", label: "browser new tab", steps: [actionStep("result", "built_in_browser", "createTab", collectGenericObjectArgs(args, {
url,
activate: background ? false : undefined,
}))] };
}
if (sub === "switch" || sub === "activate") {
return { kind: "execute", label: "browser switch", steps: [actionStep("result", "built_in_browser", "switchTab", collectGenericObjectArgs(args, {
tabId: requireValue(readValue(args, ["--tab", "--tab-id"]) ?? firstPositional(args), "tabId"),
}))] };
}
if (sub === "close" || sub === "close-tab") {
return { kind: "execute", label: "browser close", steps: [actionStep("result", "built_in_browser", "closeTab", collectGenericObjectArgs(args, {
tabId: requireValue(readValue(args, ["--tab", "--tab-id"]) ?? firstPositional(args), "tabId"),
}))] };
}
if (sub === "reload" || sub === "refresh") return { kind: "execute", label: "browser reload", steps: [actionStep("result", "built_in_browser", "reload", collectGenericObjectArgs(args))] };
if (sub === "back") return { kind: "execute", label: "browser back", steps: [actionStep("result", "built_in_browser", "goBack", collectGenericObjectArgs(args))] };
if (sub === "forward") return { kind: "execute", label: "browser forward", steps: [actionStep("result", "built_in_browser", "goForward", collectGenericObjectArgs(args))] };
if (sub === "stop") return { kind: "execute", label: "browser stop", steps: [actionStep("result", "built_in_browser", "stop", collectGenericObjectArgs(args))] };
if (sub === "screenshot" || sub === "capture") return { kind: "execute", label: "browser screenshot", steps: [actionStep("result", "built_in_browser", "captureScreenshot", collectGenericObjectArgs(args))] };
if (sub === "select" || sub === "select-point" || sub === "point") {
const x = readNumberOption(args, ["--x"]);
const y = readNumberOption(args, ["--y"]);
if (x == null || y == null) throw new CliUsageError("browser select requires --x and --y.");
return { kind: "execute", label: "browser selection", steps: [actionStep("result", "built_in_browser", "selectPoint", collectGenericObjectArgs(args, {
x,
y,
includeScreenshot: readFlag(args, ["--no-screenshot"]) ? false : undefined,
}))] };
}
if (sub === "inspect-start" || sub === "start-inspect" || sub === "inspect") return { kind: "execute", label: "browser inspect start", steps: [actionStep("result", "built_in_browser", "startInspect", collectGenericObjectArgs(args))] };
if (sub === "inspect-stop" || sub === "stop-inspect") return { kind: "execute", label: "browser inspect stop", steps: [actionStep("result", "built_in_browser", "stopInspect", collectGenericObjectArgs(args))] };
if (sub === "select-current" || sub === "selection" || sub === "selected") return { kind: "execute", label: "browser selection", steps: [actionStep("result", "built_in_browser", "selectCurrent", collectGenericObjectArgs(args))] };
if (sub === "clear-selection" || sub === "clear") return { kind: "execute", label: "browser clear selection", steps: [actionStep("result", "built_in_browser", "clearSelection", collectGenericObjectArgs(args))] };
return { kind: "execute", label: `browser ${sub}`, steps: [actionStep("result", "built_in_browser", sub, collectGenericObjectArgs(args))] };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== browser action wiring =="
rg -n --type=ts 'built_in_browser|createTab|switchTab|closeTab|startInspect|stopInspect|captureScreenshot' apps/ade-cli/src apps/desktop/src

echo
echo "== headless runtime entry points =="
rg -n --type=ts 'createAdeRuntime|createAdeRpcRequestHandler|run_ade_action|list_ade_actions' apps/ade-cli/src apps/desktop/src

echo
echo "== built_in_browser references inside the CLI/headless side only =="
rg -n --type=ts 'built_in_browser' apps/ade-cli/src

Repository: arul28/ADE

Length of output: 50037


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AdeRuntime type definition =="
rg -n --type=ts 'interface AdeRuntime|type AdeRuntime' apps/ade-cli/src

echo
echo "== builtInBrowserService in bootstrap/runtime setup =="
rg -n --type=ts 'builtInBrowserService' apps/ade-cli/src apps/desktop/src

echo
echo "== Runtime creation and service initialization =="
rg -A 20 'export async function createAdeRuntime' apps/ade-cli/src/bootstrap.ts | head -60

echo
echo "== Check for fallback or error handling for missing domains =="
rg -n --type=ts 'domain.*not.*found|socket.*required|desktop.*required' apps/ade-cli/src

Repository: arul28/ADE

Length of output: 3549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== toService function implementation =="
rg -A 10 'function toService|const toService' apps/desktop/src/main/services/adeActions/registry.ts | head -30

echo
echo "== How runtime validates/enforces domains =="
rg -n --type=ts 'getService|getServiceForDomain|domain.*available' apps/ade-cli/src apps/desktop/src/main/services/adeActions/

echo
echo "== Full AdeRuntime type definition =="
sed -n '116,200p' apps/ade-cli/src/bootstrap.ts

echo
echo "== How createAdeRuntime initializes builtInBrowserService =="
rg -A 100 'export async function createAdeRuntime' apps/ade-cli/src/bootstrap.ts | grep -i 'builtin\|browser\|return' | head -20

Repository: arul28/ADE

Length of output: 7363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getService implementation in bootstrap =="
sed -n '640,700p' apps/ade-cli/src/bootstrap.ts

echo
echo "== adeRpcServer domain validation and error handling =="
rg -B 5 -A 5 "Domain.*is unavailable in this runtime" apps/ade-cli/src/adeRpcServer.ts

echo
echo "== Check if built_in_browser is initialized in headless bootstrap =="
rg -n 'builtInBrowserService.*=' apps/ade-cli/src/bootstrap.ts

Repository: arul28/ADE

Length of output: 1339


Browser commands require a desktop socket connection.

The built_in_browser domain is not initialized in headless CLI mode. These plans will fail at execution time with "Domain 'built_in_browser' is unavailable in this runtime" rather than failing fast. Add validation to buildBrowserPlan() to require the desktop socket, or remove browser subcommands from the headless CLI surface to match the help text that already documents --socket as required.

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

In `@apps/ade-cli/src/cli.ts` around lines 2620 - 2677, buildBrowserPlan currently
builds plans that target the built_in_browser domain without checking for a
desktop socket; add a guard at the start of buildBrowserPlan that verifies a
desktop socket is available and fail fast (throw CliUsageError with a clear
message mentioning --socket) when a browser subcommand is requested and the
socket is missing; implement this by calling an existing helper like
checkDesktopSocketAvailable()/desktopSocketAvailable() or add a small helper
(e.g. ensureDesktopSocketAvailable) and use it before any branch that returns an
execute plan for the built_in_browser domain so execution will not attempt to
run when the runtime lacks the desktop socket.

Comment thread apps/desktop/src/main/main.ts
Comment on lines +393 to +399
const nextWebContents = electronWebContents.fromId(input.webContentsId);
if (!nextWebContents || nextWebContents.isDestroyed()) {
throw new Error("Browser webview is not available.");
}

configureBrowserSession();
configureBrowserWebContents(nextWebContents);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts | sed -n '390,410p'

Repository: arul28/ADE

Length of output: 920


🏁 Script executed:

# Search for BROWSER_PARTITION definition
rg "BROWSER_PARTITION" apps/desktop/src/main/services/builtInBrowser/

Repository: arul28/ADE

Length of output: 565


🏁 Script executed:

# Check the full context and function signature
cat -n apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts | sed -n '370,420p'

Repository: arul28/ADE

Length of output: 2102


🏁 Script executed:

# Check imports at the top of the file
head -50 apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts

Repository: arul28/ADE

Length of output: 1525


🏁 Script executed:

# Check how attachWebview is exposed via IPC
rg "attachWebview" apps/desktop/src/main/services/builtInBrowser/ -A 2 -B 2

Repository: arul28/ADE

Length of output: 1057


🏁 Script executed:

# Search for getType() usage in the codebase to understand the API
rg "\.getType\(\)" apps/desktop/src/ --type ts

Repository: arul28/ADE

Length of output: 36


🏁 Script executed:

# Check for similar webContents validation patterns
rg "session\.fromPartition" apps/desktop/src/ --type ts -B 2 -A 2

Repository: arul28/ADE

Length of output: 731


🏁 Script executed:

# Look at more of the function to understand the full flow
cat -n apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts | sed -n '420,480p'

Repository: arul28/ADE

Length of output: 2588


🏁 Script executed:

# Find where these methods are registered as IPC handlers
rg "ipcMain\|handle\|on" apps/desktop/src/main/services/builtInBrowser/ --type ts -B 2 -A 2

Repository: arul28/ADE

Length of output: 36


🏁 Script executed:

# Search the whole main process for IPC handler registration of builtInBrowser
rg "builtInBrowser" apps/desktop/src/main/ --type ts -l

Repository: arul28/ADE

Length of output: 264


🏁 Script executed:

# Check if there's an IPC setup in the main file
find apps/desktop/src/main -name "*.ts" -type f | head -20

Repository: arul28/ADE

Length of output: 1140


🏁 Script executed:

# Check registerIpc.ts for IPC handler setup
cat -n apps/desktop/src/main/services/ipc/registerIpc.ts | head -100

Repository: arul28/ADE

Length of output: 4055


🏁 Script executed:

# Search for attachWebview in registerIpc
rg "attachWebview" apps/desktop/src/main/services/ipc/registerIpc.ts -B 3 -A 3

Repository: arul28/ADE

Length of output: 430


🏁 Script executed:

# Check the main.ts file
head -100 apps/desktop/src/main/main.ts

Repository: arul28/ADE

Length of output: 6473


Validate webContentsId origin and type before using in renderer-supplied fromId().

The electronWebContents.fromId(input.webContentsId) call accepts any ID the renderer passes without verifying it belongs to an actual browser guest on the dedicated partition. An XSS or compromised renderer can exploit this to bind privileged app content views to this service, then navigate, inspect, or screenshot them via the IPC surface.

Add validation to ensure the webContents is a guest webview on the browser partition:

Required hardening
const nextWebContents = electronWebContents.fromId(input.webContentsId);
if (!nextWebContents || nextWebContents.isDestroyed()) {
  throw new Error("Browser webview is not available.");
}
+if (nextWebContents.getType() !== "webview") {
+  throw new Error("Only browser webview guests can be attached.");
+}
+if (nextWebContents.session !== session.fromPartition(BROWSER_PARTITION)) {
+  throw new Error("Browser webview must use the ADE browser partition.");
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts`
around lines 393 - 399, The call to
electronWebContents.fromId(input.webContentsId) must be hardened: after
obtaining nextWebContents, validate its origin and type before calling
configureBrowserSession()/configureBrowserWebContents(). Specifically, ensure
nextWebContents exists and is not destroyed, then verify
nextWebContents.getType() indicates a guest/webview (e.g., "guest-webview" or
"webview") and that it belongs to the dedicated browser partition/session
(compare nextWebContents.session or its partition name against the expected
browser partition constant used by configureBrowserSession). If any check fails,
throw and refuse to bind the webContents; only call
configureBrowserWebContents(nextWebContents) when these validations pass.

Comment on lines +713 to +731
const attachDebuggerListeners = (wc: WebContents): void => {
if (debuggerMessageListener || debuggerDetachListener) return;
debuggerMessageListener = (_event, method, params) => {
if (method !== "Overlay.inspectNodeRequested") return;
const backendNodeId = isRecord(params) ? params.backendNodeId : null;
if (typeof backendNodeId !== "number" || !Number.isFinite(backendNodeId)) return;
void handleInspectNodeRequested(wc, backendNodeId).catch(emitError);
};
debuggerDetachListener = (_event, reason) => {
logger()?.debug("built_in_browser.debugger_detached", { reason });
inspecting = false;
debuggerAttachedForInspect = false;
debuggerMessageListener = null;
debuggerDetachListener = null;
emitStatus();
};
wc.debugger.on("message", debuggerMessageListener);
wc.debugger.on("detach", debuggerDetachListener);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts

Repository: arul28/ADE

Length of output: 130


🏁 Script executed:

cat -n apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts | sed -n '700,750p'

Repository: arul28/ADE

Length of output: 2147


🏁 Script executed:

rg "detachDebuggerListeners" apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts -B 5 -A 10

Repository: arul28/ADE

Length of output: 1368


Detach debugger listeners before clearing the listener refs.

When the debugger detaches at line 721–728, the handler nulls debuggerMessageListener and debuggerDetachListener without calling off(). This leaves the callbacks registered on wc.debugger, causing stale listeners to fire or double-deliver events in subsequent inspect sessions on the same WebContents.

Call the existing detachDebuggerListeners(wc) function (defined at lines 733–742) to properly deregister listeners via .off() before nulling the refs.

Proposed fix
     debuggerDetachListener = (_event, reason) => {
       logger()?.debug("built_in_browser.debugger_detached", { reason });
+      detachDebuggerListeners(wc);
       inspecting = false;
       debuggerAttachedForInspect = false;
-      debuggerMessageListener = null;
-      debuggerDetachListener = null;
       emitStatus();
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts`
around lines 713 - 731, The debuggerDetachListener currently nulls
debuggerMessageListener and debuggerDetachListener without removing them from
wc.debugger, leaving stale callbacks registered; modify the detach branch inside
attachDebuggerListeners so it calls the existing detachDebuggerListeners(wc)
helper to invoke wc.debugger.off(...) and remove the listeners before clearing
the refs and updating inspecting/debuggerAttachedForInspect and emitting status,
ensuring debuggerMessageListener and debuggerDetachListener are deregistered via
detachDebuggerListeners rather than just being set to null.

Comment on lines +3624 to +3632
if (item.screenshotDataUrl && !attachmentPath) {
try {
const saved = await window.ade.agentChat.saveTempAttachment({
data: stripDataUrlPrefix(item.screenshotDataUrl),
filename: "built-in-browser-selection.png",
});
attachmentPath = saved.path;
addAttachment({ path: saved.path, type: inferAttachmentType(saved.path, "image/png") });
} catch (error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep browser temp attachments out of the cross-tool pairing ref.

addAttachment() updates latestAttachmentRef, and addIosElementContext / addAppControlContext use that ref as the candidate screenshot for their next context packet. After Line 3631, the next iOS/App Control selection made within the 10s window can incorrectly reuse this browser PNG instead of its own capture.

Suggested fix
-        addAttachment({ path: saved.path, type: inferAttachmentType(saved.path, "image/png") });
+        setAttachments((current) => (
+          current.some((entry) => entry.path === saved.path)
+            ? current
+            : [...current, { path: saved.path, type: inferAttachmentType(saved.path, "image/png") }]
+        ));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (item.screenshotDataUrl && !attachmentPath) {
try {
const saved = await window.ade.agentChat.saveTempAttachment({
data: stripDataUrlPrefix(item.screenshotDataUrl),
filename: "built-in-browser-selection.png",
});
attachmentPath = saved.path;
addAttachment({ path: saved.path, type: inferAttachmentType(saved.path, "image/png") });
} catch (error) {
if (item.screenshotDataUrl && !attachmentPath) {
try {
const saved = await window.ade.agentChat.saveTempAttachment({
data: stripDataUrlPrefix(item.screenshotDataUrl),
filename: "built-in-browser-selection.png",
});
attachmentPath = saved.path;
setAttachments((current) => (
current.some((entry) => entry.path === saved.path)
? current
: [...current, { path: saved.path, type: inferAttachmentType(saved.path, "image/png") }]
));
} catch (error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` around lines
3624 - 3632, The browser temp screenshot save block currently calls
addAttachment(...) which updates latestAttachmentRef and causes subsequent
addIosElementContext/addAppControlContext calls to reuse the browser PNG; change
the flow so saving the temp attachment via
window.ade.agentChat.saveTempAttachment(...) does NOT call addAttachment.
Instead store saved.path in a local variable (e.g., browserTempPath) and update
any UI/state for attachments without touching latestAttachmentRef, or if you
must call addAttachment, capture the previous latestAttachmentRef and
immediately restore it after the call; reference the item.screenshotDataUrl save
block, window.ade.agentChat.saveTempAttachment, addAttachment,
latestAttachmentRef, addIosElementContext and addAppControlContext when making
the change.


const BOUNDS_SETTLE_MS = 1_200;
const BOUNDS_SETTLE_MIN_FRAME_MS = 32;
const DEFAULT_BROWSER_URL = "https://www.google.com/";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't hit Google just by opening the panel.

This effect creates a tab to https://www.google.com/ as soon as the panel sees no tabs. That sends unsolicited third-party traffic on mount and also makes the empty-state UI unreachable. Prefer lazy tab creation or a local blank/start page instead.

Also applies to: 1027-1052

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

In `@apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx` at
line 177, The component ChatBuiltInBrowserPanel currently auto-creates a browser
tab pointing to DEFAULT_BROWSER_URL ("https://www.google.com/") when no tabs
exist, causing unsolicited external requests and preventing the empty-state UI
from showing; change this to either (a) use a local blank/start URL constant
(e.g., "about:blank" or an internal route) instead of DEFAULT_BROWSER_URL, or
(b) delay creating a tab until the user explicitly opens a new tab or navigates
(lazy creation). Update the tab-creation logic in ChatBuiltInBrowserPanel and
any related handlers that reference DEFAULT_BROWSER_URL so the empty-state UI is
rendered by default and external navigation only happens on user action.

Comment on lines +421 to +429
const { setViewMode, setWorkSidebarTab } = work;
useEffect(() => {
const openBrowserSidebar = () => {
setViewMode("tabs");
setWorkSidebarTab("browser");
};
window.addEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
return () => window.removeEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
}, [setViewMode, setWorkSidebarTab]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Actually open the sidebar on ADE_OPEN_BUILT_IN_BROWSER_EVENT.

This handler only switches the view mode and selects the "browser" tab. When work.workSidebarOpen is false, workSidebarVisible stays false, so the event named “open built-in browser” still leaves the browser hidden.

🐛 Suggested fix
-  const { setViewMode, setWorkSidebarTab } = work;
+  const { setViewMode, setWorkSidebarOpen, setWorkSidebarTab } = work;
   useEffect(() => {
     const openBrowserSidebar = () => {
       setViewMode("tabs");
+      setWorkSidebarOpen(true);
       setWorkSidebarTab("browser");
     };
     window.addEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
     return () => window.removeEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
-  }, [setViewMode, setWorkSidebarTab]);
+  }, [setViewMode, setWorkSidebarOpen, setWorkSidebarTab]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { setViewMode, setWorkSidebarTab } = work;
useEffect(() => {
const openBrowserSidebar = () => {
setViewMode("tabs");
setWorkSidebarTab("browser");
};
window.addEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
return () => window.removeEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
}, [setViewMode, setWorkSidebarTab]);
const { setViewMode, setWorkSidebarOpen, setWorkSidebarTab } = work;
useEffect(() => {
const openBrowserSidebar = () => {
setViewMode("tabs");
setWorkSidebarOpen(true);
setWorkSidebarTab("browser");
};
window.addEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
return () => window.removeEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
}, [setViewMode, setWorkSidebarOpen, setWorkSidebarTab]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx` around
lines 421 - 429, The ADE_OPEN_BUILT_IN_BROWSER_EVENT handler currently only
calls setViewMode("tabs") and setWorkSidebarTab("browser") but does not open the
sidebar when work.workSidebarOpen is false; update the openBrowserSidebar
function to also ensure the sidebar is shown (e.g. call the appropriate setter
such as setWorkSidebarOpen(true) or setWorkSidebarVisible(true) /
setWorkSidebarOpen to true depending on the available API) so that the browser
tab becomes visible; keep the other calls to setViewMode and setWorkSidebarTab
as-is.

Comment on lines +343 to +350
const selectLaneForActiveTab = useCallback(
(sessionId: string | null) => {
if (!sessionId || viewMode === "grid") return;
const session = sessionsById.get(sessionId);
if (!session) return;
selectLane(session.laneId);
},
[selectLane, sessionsById, viewMode],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Route-gate the shared lane-sync helper, not just the effect.

selectLaneForActiveTab() is also called from setSelectedSessionId, setActiveItemId, and openSessionTab, so tab activation can still change the selected lane while the user is on /files or another non-Work route. The new useEffect guard only fixes the restore-after-load path.

Suggested fix
   const selectLaneForActiveTab = useCallback(
     (sessionId: string | null) => {
-      if (!sessionId || viewMode === "grid") return;
+      if (!isWorkRoute || !sessionId || viewMode === "grid") return;
       const session = sessionsById.get(sessionId);
       if (!session) return;
       selectLane(session.laneId);
     },
-    [selectLane, sessionsById, viewMode],
+    [isWorkRoute, selectLane, sessionsById, viewMode],
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selectLaneForActiveTab = useCallback(
(sessionId: string | null) => {
if (!sessionId || viewMode === "grid") return;
const session = sessionsById.get(sessionId);
if (!session) return;
selectLane(session.laneId);
},
[selectLane, sessionsById, viewMode],
const selectLaneForActiveTab = useCallback(
(sessionId: string | null) => {
if (!isWorkRoute || !sessionId || viewMode === "grid") return;
const session = sessionsById.get(sessionId);
if (!session) return;
selectLane(session.laneId);
},
[isWorkRoute, selectLane, sessionsById, viewMode],
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/terminals/useWorkSessions.ts` around
lines 343 - 350, selectLaneForActiveTab currently only returns early in an
effect but is also invoked from setSelectedSessionId, setActiveItemId, and
openSessionTab, so those callers can change the selected lane while the user is
on non-Work routes; move the route guard into selectLaneForActiveTab itself: at
the top of the selectLaneForActiveTab callback add a check for the current
route/view (e.g. viewMode !== "grid" or your app's Work-route predicate) and
return early if not on the Work route, then keep the existing sessionId/session
lookup and selectLane(session.laneId) behavior so all callers
(setSelectedSessionId, setActiveItemId, openSessionTab) automatically honor the
route gate.

Comment on lines +27 to +31
export function openUrlInAdeBrowser(url: string | undefined | null): void {
const normalized = normalizeBrowserUrlInput(url);
if (!normalized || !canOpenInAdeBrowser(normalized)) {
openExternalUrl(url);
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't forward rejected schemes to the external opener.

canOpenInAdeBrowser() correctly rejects non-HTTP(S) input, but this branch still passes the original value to openExternalUrl(). That means inputs like javascript:... or data:... still reach window.open/the bridge instead of being dropped or explicitly allowlisted.

Suggested fix
 export function openUrlInAdeBrowser(url: string | undefined | null): void {
   const normalized = normalizeBrowserUrlInput(url);
-  if (!normalized || !canOpenInAdeBrowser(normalized)) {
-    openExternalUrl(url);
+  if (!normalized) return;
+  if (!canOpenInAdeBrowser(normalized)) {
+    if (/^(mailto|tel):/i.test(normalized)) {
+      openExternalUrl(normalized);
+    }
     return;
   }

As per coding guidelines, "apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function openUrlInAdeBrowser(url: string | undefined | null): void {
const normalized = normalizeBrowserUrlInput(url);
if (!normalized || !canOpenInAdeBrowser(normalized)) {
openExternalUrl(url);
return;
export function openUrlInAdeBrowser(url: string | undefined | null): void {
const normalized = normalizeBrowserUrlInput(url);
if (!normalized) return;
if (!canOpenInAdeBrowser(normalized)) {
if (/^(mailto|tel):/i.test(normalized)) {
openExternalUrl(normalized);
}
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/lib/openExternal.ts` around lines 27 - 31, The
branch currently passes the original raw url to openExternalUrl even when
canOpenInAdeBrowser(normalized) rejects unsafe schemes; update
openUrlInAdeBrowser so it uses the normalized value and never forwards a
rejected scheme to the external opener: call openExternalUrl(normalized) only
when normalized is truthy and canOpenInAdeBrowser(normalized) returns true, and
otherwise drop/return without calling openExternalUrl. Reference:
openUrlInAdeBrowser, normalizeBrowserUrlInput, canOpenInAdeBrowser,
openExternalUrl.

Comment on lines +46 to +47
@AppStorage("ade.work.laneFilter") var selectedLaneId = "all"
@AppStorage("ade.work.statusFilter") private var selectedStatusRawValue = WorkSessionStatusFilter.all.rawValue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persisted lane filter can strand the list behind a stale lane id.

Line 46 persists selectedLaneId globally, but invalid/stale lane ids are not reconciled when lane data changes. With current WorkFiltersSection fallback behavior (apps/ios/ADE/Views/Work/WorkRootComponents.swift:9-31), this can hide all sessions while showing “All lanes.”

Suggested patch
+  private func sanitizePersistedLaneFilter() {
+    if selectedLaneId != "all" && !lanes.contains(where: { $0.id == selectedLaneId }) {
+      selectedLaneId = "all"
+    }
+  }
+
   var body: some View {
     NavigationStack(path: $path) {
       ScrollViewReader { proxy in
       List {
         ...
       }
+      .onAppear {
+        sanitizePersistedLaneFilter()
+      }
+      .onChange(of: lanes.map(\.id)) { _, _ in
+        sanitizePersistedLaneFilter()
+      }
       ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkRootScreen.swift` around lines 46 - 47, Persisted
selectedLaneId (AppStorage in WorkRootScreen) can be stale and hide sessions;
validate and reconcile it against the current lanes whenever lane data changes.
Update the code that receives/loads lane data (e.g., after fetching in
WorkRootScreen or in the view model backing WorkFiltersSection) to check if
selectedLaneId exists in the current lanes' id set and if not set selectedLaneId
= "all" (or the WorkFiltersSection fallback) so the UI won’t be stranded on an
invalid lane id; you can implement this in the lanes state observer, onAppear,
or a validateSelectedLane() helper invoked after lanes update.

arul28 and others added 3 commits May 4, 2026 00:54
@arul28 arul28 force-pushed the ade/ade-browser-d5ca6ac8 branch from 3d31a5b to f781138 Compare May 4, 2026 04:56
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 4, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot review but do not make fixes

Here's the updated review, incorporating the Greptile P1/P2 findings alongside the issues from my previous pass. Still no fixes.


P1 Bugs (state corruption confirmed)

1. navigate() — inspection cancelled before tab-limit guard (builtInBrowserService.ts:442-451)
When newTab: true and tabs.length >= MAX_BROWSER_TABS, stopInspectQuietly() and clearSelectionInternal() both run (emitting selection-cleared over IPC) before the throw. The tab is never created, but any active inspection session is permanently lost with no way to restore. The tab-limit check needs to move to before the if (switchingTabs) block.

2. createTab() — same pattern (builtInBrowserService.ts:470-477)
stopInspectQuietly() and clearSelectionInternal() run before tabs.length >= MAX_BROWSER_TABS is checked. Same irreversible state loss. Fix: move the guard above if (willActivate).


Previously Identified Issues (still open)

3. normalizeUrlForNavigation passes unsupported schemes to the backend (ChatBuiltInBrowserPanel.tsx:233)
file:, blob:, data:, devtools: pass through to IPC where normalizeBrowserUrl throws a generic error. The renderer should reject these with a clear UI message before the IPC call.

4. Password field value leaked via CDP (builtInBrowserService.ts:~1201)
element.value is read unconditionally in NODE_METADATA_FUNCTION, including for <input type="password">, and up to 300 chars end up in metadata.value — forwarded to the agent prompt via formatBuiltInBrowserContextForPrompt.

5. Debugger state is a singleton across all tabs
debuggerAttachedForInspect, debuggerMessageListener, and debuggerDetachListener are closure-level variables, not per-tab. A tab switch during an active inspect session can leave listeners registered on the old WebContents while the flag is cleared against the new one.

6. CSP frame-src 'none'https: about: (main.ts:454)
The https: wildcard allows any HTTPS origin to be embedded as a frame in the renderer. The will-attach-webview handler only constrains <webview> elements; it doesn't help with <iframe> elements, which are now also permitted. Worth scoping to about: only (since the webview manages its own frame navigation) unless <iframe> embedding in the renderer is a confirmed requirement.

7. hideBuiltInBrowserView() in WorkSidebar.tsx is not null-safe
Calls window.ade.builtInBrowser.stopInspect() and .setBounds() directly without an availability guard. Runs unconditionally on every tab change, including in environments where builtInBrowser may not be exposed.

8. autoAttachedContextIdsRef grows unboundedly
IDs added on every selection, never cleared. A long-running panel session accumulates these across navigations.

9. Double stopInspect in handleInspectNodeRequested finally block
stopInspect() is called at line 794, then finally re-checks if (inspecting) and calls it again at line 797. After a successful call inspecting is false so the re-entry is dead code, but if the first call throws, both paths fail independently.

10. Renderer type declarations diverge from shared types
BuiltInBrowserTab.isLoading (shared) vs .loading (renderer local) — the double-fallback in normalizeTab is a symptom. Importing from the shared types package and deriving renderer view models from them would eliminate this drift.

Copilot finished work on behalf of arul28 May 4, 2026 04:59
- Browser service: captureScreenshot/selectPoint throw on no tab instead of auto-creating a blank one
- navigate(newTab) and createTab throw the tab-limit error before clearing inspect/selection state
- NODE_METADATA_FUNCTION drops value for <input type=password> to prevent leaking it through agent prompts
- Debugger inspect listeners are tracked per-WebContents and detached when switching tabs mid-inspect
- handleInspectNodeRequested no longer double-calls stopInspect (finally-only)
- main.ts CSP: drop https: from frame-src; <webview>/WebContentsView don't need it
- ChatBuiltInBrowserPanel: reject file:/blob:/data:/devtools: in normalizeUrlForNavigation with a clear toast
- ChatBuiltInBrowserPanel: import shared BuiltInBrowserTab; collapse loading vs isLoading drift
- ChatBuiltInBrowserPanel: clear autoAttachedContextIdsRef on URL/active-tab change
- WorkSidebar: null-guard window.ade.builtInBrowser; only hide when previous tab was browser

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 4, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot review but do not make fixes

Here's the updated review for this round's Greptile findings. Still no fixes.


Greptile P1 — Already Fixed

captureScreenshot / selectPoint blank tab creation (builtInBrowserService.ts:621, 637)
Greptile flagged these as calling ensureActiveTab() (which silently creates a tab). In the current code both functions already call currentWebContents() and throw early with a user-facing error if null — the P1 is not present in the code as it stands.


Greptile P2 — Confirmed Open

dispose() stale inspecting / debuggerAttachedForInspect flags (builtInBrowserService.ts:699)
dispose() fires void stopInspect().catch(() => {}) then immediately closes all tabs. Because stopInspect() is async (it issues CDP Overlay.setInspectMode, Overlay.disable, and debugger.detach()), the tab-closure loop races those in-flight commands. If stopInspect() throws (silently swallowed), inspecting and debuggerAttachedForInspect remain true after tabs = []. The next attachToWindow() returns getStatus() with isInspecting: true and no live session — browser UI opens in a stale "Inspecting" state. Fix: synchronously reset both flags at the top of dispose().


Previously Identified Issues (still open)

P1 × 2 — navigate() / createTab() premature side-effects (builtInBrowserService.ts:442, 470)
Both still mutate inspection state and emit IPC events before the MAX_BROWSER_TABS guard. Confirmed in the current code.

CSP frame-src https: wildcard (main.ts:454)
Unchanged. Any HTTPS origin can be embedded as a <frame> or <iframe> in the renderer.

normalizeUrlForNavigation passes unsupported schemes (ChatBuiltInBrowserPanel.tsx:233)
file:, blob:, data:, devtools: still pass through to IPC unvalidated.

Password field value in CDP metadata (builtInBrowserService.ts:~1201)
element.value still read unconditionally for all input types.

Debugger state singleton across tabs (closure-level inspecting, debuggerAttachedForInspect, listeners)
Unchanged.

hideBuiltInBrowserView() null-safety (WorkSidebar.tsx)
Still calls .stopInspect() / .setBounds() unconditionally.

autoAttachedContextIdsRef unbounded growth
No clear-on-navigate or size cap.

Copilot finished work on behalf of arul28 May 4, 2026 05:21
Comment thread apps/desktop/src/renderer/lib/openExternal.ts
…b race

- Browser service: reload/goBack/goForward/stop/startInspect now use
  currentWebContents() + early throw, matching the iteration-1 fix for
  captureScreenshot/selectPoint (Greptile P1)
- dispose() clears inspecting/debuggerAttachedForInspect and detaches
  debugger listeners synchronously before the async teardown so an
  in-flight CDP callback can't run against torn-down state (Copilot P2)
- openExternal.ts marks a pendingBuiltInBrowserNavigation flag before
  dispatching ADE_OPEN_BUILT_IN_BROWSER_EVENT; ChatBuiltInBrowserPanel
  consumes the flag in the default-tab effect to suppress the empty-state
  Google tab when an external link triggered the open, eliminating the
  double-tab race (Greptile P1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 4, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot review but do not make fixes

Here's the updated review for this round's Greptile findings. Still no fixes.


Greptile P1 — Already Fixed: phantom blank tabs in page-control methods

reload / goBack / goForward / stop / startInspect (lines 540–616)
All five already call currentWebContents() (not ensureActiveTab()) and throw early with a user-facing message when no tab exists. The P1 Greptile described is not present in the current code.


Greptile P1 — Partially Addressed: default-tab race on link click

openUrlInAdeBrowser + ChatBuiltInBrowserPanel default-tab effect
The fix (markPendingBuiltInBrowserNavigation / consumePendingBuiltInBrowserNavigation module-level flag) addresses the race correctly when the panel mounts while getStatus() still returns tabs: []. However, there is a residual bug in the flag-cleanup path:

If the navigate IPC lands and the status update arrives before the default-tab useEffect fires (i.e., browserTabs.length > 0 is already true on the first render), the guard at line 1040 short-circuits the effect body and consumePendingBuiltInBrowserNavigation() is never called. pendingBuiltInBrowserNavigation stays true indefinitely. The next time the user opens the browser panel with zero tabs — e.g. after closing all tabs manually — the effect fires, sees the stale true, consumes it, sets defaultBrowserOpenedRef.current = true, and suppresses the default Google tab even though no race is occurring. The user ends up with an empty browser and no default tab.

Fix: the flag should always be consumed (and cleared) if it was set, regardless of whether the default tab was actually suppressed. One way: check and consume at the top of the effect unconditionally when browserTabs.length === 0 && !defaultBrowserOpenedRef.current, before any early return.


Previously Identified Issues (still open)

navigate() — inspection cancelled before tab-limit guard (line 445–451)
The MAX_BROWSER_TABS check now correctly precedes stopInspectQuietly / clearSelectionInternal — this P1 from the prior round is fixed.

createTab() — same pattern (line 471–478)
MAX_BROWSER_TABS guard is at the top — fixed in this round.

dispose() stale inspecting / debuggerAttachedForInspect flags (line 699)
void stopInspect().catch(() => {}) still races tab closure; both flags not reset synchronously. Still open.

normalizeUrlForNavigation (renderer)file:, blob:, data: still pass to IPC. Still open (though canOpenInAdeBrowser blocks these at the openUrlInAdeBrowser entry point, only the URL bar in the panel itself still has this gap).

Password field value in CDP metadataelement.value read unconditionally for all input types. Still open.

Debugger state singleton across tabsdebuggerAttachedForInspect and listeners are closure-level, not per-tab. Still open.

autoAttachedContextIdsRef unbounded growth — Still open.

CSP frame-src https: wildcard (main.ts) — Still open.

Copilot finished work on behalf of arul28 May 4, 2026 05:40
Consume consumePendingBuiltInBrowserNavigation() as soon as the panel has an
API and status, before the browserTabs.length early return. Without this, a
fast status update with tabs already present on first render leaves the flag
set, which then suppresses the default tab on a later panel reopen with zero
tabs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 4, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot review but do not make fixes

Here's the updated review for this round. Still no fixes.


Previously Identified Issues — Now Fixed

dispose() stale inspecting / debuggerAttachedForInspect flagsFixed. Both flags are now synchronously reset at the top of dispose() (lines 719–720) before the async stopInspect() fire-and-forget, eliminating the stale "Inspecting" state on the next attachToWindow().

consumePendingBuiltInBrowserNavigation() flag-leak (identified last round)Fixed. consumePendingBuiltInBrowserNavigation() is now called unconditionally at line 1046, before the browserTabs.length > 0 early return, so a fast navigate IPC that lands before the first effect run no longer strands a stale true flag.

Password field value in CDP metadataFixed. NODE_METADATA_FUNCTION now checks isPasswordInput and passes null for value on <input type="password"> fields (lines 1252–1257).


Greptile P2 — Confirmed Open

TerminalView.tsx:160 — off-by-one in getLine fallback
bufferLineNumber from xterm.js is 1-based. lineIndex = Math.max(0, bufferLineNumber - 1) correctly converts it. The fallback ?? term.buffer.active.getLine(bufferLineNumber) then passes the original 1-based value as a 0-based index, returning the wrong line when bufferLineNumber === 1 (and the wrong line for all other values where the primary call fails). Since if (!line) { callback(undefined); } already handles the null case cleanly, the fallback should be removed: const line = term.buffer.active.getLine(lineIndex);


Previously Identified Issues (still open)

normalizeUrlForNavigation (URL bar in panel)file:, blob:, data:, devtools: typed directly into the browser's URL bar still pass through to IPC unvalidated. (canOpenInAdeBrowser already blocks these at the openUrlInAdeBrowser entry point, so terminal/markdown links are safe; only the URL bar is affected.)

Debugger state singleton across tabsdebuggerAttachedForInspect, debuggerMessageListener, debuggerDetachListener, and inspectListenerWebContents remain closure-level. A tab switch mid-inspect can leave listeners registered on the old WebContents while the flag is cleared against the new one.

autoAttachedContextIdsRef unbounded growth — IDs accumulate across navigations with no cap or clear-on-navigate.

CSP frame-src includes ${cspSources} (main.ts:457) — In production, cspSources is 'self' file: app:, so frame-src permits file: and app: frames in the renderer alongside about: and http://localhost:*. This is broader than necessary.

Copilot finished work on behalf of arul28 May 4, 2026 05:55
Copy link
Copy Markdown

@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: 7

♻️ Duplicate comments (9)
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx (2)

3624-3632: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep browser temp screenshots out of the shared attachment pairing ref.

This save path still goes through addAttachment(), which updates latestAttachmentRef. The next iOS/App Control selection inside the 10s reuse window can therefore pick up this browser PNG instead of its own screenshot.

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

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` around lines
3624 - 3632, The browser temp screenshot save is still calling
addAttachment(...) which updates latestAttachmentRef and causes subsequent
iOS/App selections to reuse the browser PNG; change the flow in the
AgentChatPane logic that handles item.screenshotDataUrl (the block calling
window.ade.agentChat.saveTempAttachment and addAttachment) so that after saving
via agentChat.saveTempAttachment(...) you only set attachmentPath = saved.path
(and use inferAttachmentType(saved.path, "image/png") locally if needed) but do
NOT call addAttachment(...) or otherwise update latestAttachmentRef for this
temp browser screenshot; ensure addAttachment remains used for real user
attachments only.

4252-4260: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Parallel launch still drops visual context.

These snapshots/prefixes are only built on the normal send path. When isParallelLaunch is true, the early branch above still forwards only the draft and attachments, so the built-in browser/iOS/App Control chips shown in the composer never reach the child lanes.

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

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` around lines
4252 - 4260, When isParallelLaunch is true the early branch still only forwards
draft and attachments and never includes the visual context computed by
builtInBrowserContextSnapshot, iosContextSnapshot, appControlContextSnapshot or
their derived values (visualContextPrefix, visualContextDisplayChips). Update
the parallel-launch branch that forwards to child lanes to also compute or reuse
the same iosContextPrefix, appControlContextPrefix, builtInBrowserContextPrefix
and corresponding display chips and include visualContextPrefix and
visualContextDisplayChips in the payload sent to the child lanes (the same
fields used on the normal send path), so the built-in browser/iOS/App Control
chips present in the composer are forwarded in parallel launches.
apps/desktop/src/renderer/lib/openExternal.ts (1)

45-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't forward rejected schemes to the external opener.

canOpenInAdeBrowser() rejects non-HTTP(S) input, but this branch still passes the original url to openExternalUrl(). Inputs like javascript: or data: are therefore not actually blocked here.

As per coding guidelines, "apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices."

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

In `@apps/desktop/src/renderer/lib/openExternal.ts` around lines 45 - 49,
openUrlInAdeBrowser currently forwards the original unnormalized url to
openExternalUrl even when canOpenInAdeBrowser(normalized) rejects unsafe
schemes; update openUrlInAdeBrowser so it first computes normalized =
normalizeBrowserUrlInput(url) and then only calls openExternalUrl with the
normalized value for allowed schemes (i.e., call openExternalUrl(normalized)
only when normalized is truthy and canOpenInAdeBrowser(normalized) is true); if
normalized is falsy or canOpenInAdeBrowser(normalized) is false, do not call
openExternalUrl (simply return) to ensure rejected schemes like "javascript:" or
"data:" are never forwarded.
apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts (2)

748-769: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove debugger listeners in the detach callback instead of just nulling the refs.

Lines 759-765 clear the listener references without calling detachDebuggerListeners(wc). If the debugger detaches unexpectedly, the old callbacks stay registered on wc.debugger, so the next inspect session on the same WebContents can double-handle events.

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

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts`
around lines 748 - 769, In debuggerDetachListener (created inside
attachDebuggerListeners), replace the manual nulling of listener references with
a call to detachDebuggerListeners(wc) so the handlers are actually removed from
wc.debugger; ensure debuggerDetachListener still logs and updates
inspecting/debuggerAttachedForInspect state (and that detachDebuggerListeners
clears debuggerMessageListener, debuggerDetachListener,
inspectListenerWebContents and calls wc.debugger.off/.removeListener for the
stored callbacks) to prevent duplicated handlers on subsequent inspect sessions.

394-400: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Reject non-browser webContentsId values in attachWebview().

Lines 394-400 still trust any renderer-supplied webContentsId after the null check. A compromised renderer can bind arbitrary app WebContents into this service and then use navigate, startInspect, or captureScreenshot against privileged content. This needs a type/partition check before the rebinding step.

As per coding guidelines, "Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices."

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

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts`
around lines 394 - 400, Reject non-browser webContentsId values by verifying the
WebContents type and partition before rebinding: after obtaining nextWebContents
via electronWebContents.fromId(input.webContentsId) (and the existing
null/isDestroyed checks) assert nextWebContents.getType() === 'webview' and also
validate nextWebContents.getWebPreferences().partition matches the expected
browser partition pattern (or a configured allowed partition) and throw an Error
if not; perform these checks in attachWebview (the block that calls
configureBrowserSession() and configureBrowserWebContents(nextWebContents)) so
only genuine webview/guest contents are accepted.
apps/ade-cli/src/cli.ts (1)

2620-2677: ⚠️ Potential issue | 🟠 Major

Fail fast when browser subcommands run without a desktop socket.

These plans still target the built_in_browser domain after the CLI falls back to headless mode, so they fail late with Domain 'built_in_browser' is unavailable in this runtime instead of surfacing a clear usage error up front. The help text already documents --socket, so this still looks unresolved.

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

In `@apps/ade-cli/src/cli.ts` around lines 2620 - 2677, buildBrowserPlan currently
builds plans targeting the built_in_browser domain even when the CLI is running
in headless mode (no desktop socket), causing late runtime errors; fix by
validating desktop-socket availability up-front: add/consume a helper like
ensureDesktopSocketAvailable() (or implement a small check) and call it at the
start of buildBrowserPlan to throw a CliUsageError with a clear message
(mentioning --socket and HELP_BY_COMMAND.browser if helpful) whenever the
desktop socket is not available, so plans targeting "built_in_browser" fail fast
instead of producing "Domain 'built_in_browser' is unavailable in this runtime"
at execution time.
apps/desktop/src/main/services/ipc/registerIpc.ts (1)

6447-6450: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

webContentsId is still not bound to the requesting renderer.

guardBuiltInBrowserIpc() only verifies the caller is an ADE renderer. This handler still forwards an arbitrary webContentsId into attachWebview(), so a compromised renderer can target another WebContents unless ownership is checked against event.sender or its BrowserWindow first.

As per coding guidelines, "Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices."

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

In `@apps/desktop/src/main/services/ipc/registerIpc.ts` around lines 6447 - 6450,
The handler for IPC.builtInBrowserAttachWebview currently trusts the
client-supplied webContentsId; after guardBuiltInBrowserIpc(...) and before
calling ensureBuiltInBrowser().attachWebview(...), verify that the webContentsId
in parseBuiltInBrowserAttachWebviewArgs(arg, IPC.builtInBrowserAttachWebview)
belongs to the requesting renderer (compare it to event.sender.id or to
BrowserWindow.fromWebContents(event.sender).webContents.id/owner window id) and
reject/throw if it does not match; keep the existing guard call and only forward
the parsed args to attachWebview when ownership is confirmed.
apps/desktop/src/main/main.ts (1)

3450-3450: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the live AppContext shape consistent with the dormant one.

Line 3450 wires builtInBrowserService into rpcRuntime, and Line 3773 adds it to the dormant context, but the live object returned from initContextForProjectRoot() still omits builtInBrowserService. getActiveContext().builtInBrowserService will therefore be undefined for open projects and only null when dormant.

♻️ Suggested fix
       computerUseArtifactBrokerService,
       iosSimulatorService,
       appControlService,
+      builtInBrowserService,
       queueLandingService,
       issueInventoryService,

Also applies to: 3773-3773

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

In `@apps/desktop/src/main/main.ts` at line 3450, The live AppContext returned by
initContextForProjectRoot() is missing the builtInBrowserService property
(rpcRuntime wires it in and the dormant context includes it), causing
getActiveContext().builtInBrowserService to be undefined for active projects;
update initContextForProjectRoot() to include a builtInBrowserService field
(initialized the same way or to null/placeholder consistent with the dormant
context) so the live and dormant context shapes match, and ensure any callers
(e.g., code expecting getActiveContext().builtInBrowserService) handle the same
null/instance semantics.
apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx (1)

420-429: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Actually open the sidebar on ADE_OPEN_BUILT_IN_BROWSER_EVENT.

Line 420 makes visibility depend on work.workSidebarOpen, but Lines 423-425 only switch the view mode and active tab. If the sidebar is collapsed, the “open built-in browser” event still leaves the browser hidden.

♻️ Suggested fix
-  const { setViewMode, setWorkSidebarTab } = work;
+  const { setViewMode, setWorkSidebarOpen, setWorkSidebarTab } = work;
   useEffect(() => {
     const openBrowserSidebar = () => {
       setViewMode("tabs");
+      setWorkSidebarOpen(true);
       setWorkSidebarTab("browser");
     };
     window.addEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
     return () => window.removeEventListener(ADE_OPEN_BUILT_IN_BROWSER_EVENT, openBrowserSidebar);
-  }, [setViewMode, setWorkSidebarTab]);
+  }, [setViewMode, setWorkSidebarOpen, setWorkSidebarTab]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx` around
lines 420 - 429, The handler for ADE_OPEN_BUILT_IN_BROWSER_EVENT changes only
view/tab but doesn't actually open the sidebar when it's collapsed; update the
openBrowserSidebar function to also set the sidebar open (call the work sidebar
open setter, e.g. setWorkSidebarOpen(true) or the appropriate method on the work
object) so the browser becomes visible, and add that setter (setWorkSidebarOpen)
to the useEffect dependency array along with setViewMode and setWorkSidebarTab.
🧹 Nitpick comments (3)
apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx (2)

52-67: 💤 Low value

Local type duplicates and diverges from shared type.

The local BuiltInBrowserContextItem type differs from the shared type in apps/desktop/src/shared/types/builtInBrowser.ts. The shared type requires provider, componentId, pixelFrame, sourceFile, sourceLine while this local type adds selector, text, role, tagName, sessionId with an open index signature.

Consider importing and extending the shared type to maintain consistency:

import type { BuiltInBrowserContextItem as SharedContextItem } from "../../../shared/types/builtInBrowser";

type BuiltInBrowserContextItem = SharedContextItem & {
  selector?: string | null;
  text?: string | null;
  // ... additional fields
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx` around
lines 52 - 67, Replace the locally defined BuiltInBrowserContextItem with an
extension of the shared type: import the shared BuiltInBrowserContextItem (refer
to it here as SharedContextItem) and define the local type as SharedContextItem
& { selector?: string | null; text?: string | null; role?: string | null;
tagName?: string | null; sessionId?: string | null; screenshotDataUrl?: string |
null; selectedAt?: string }; ensure you remove the open index signature ([key:
string]: unknown) and any divergent required fields so the local type stays
consistent with the shared BuiltInBrowserContextItem while adding only the extra
optional properties used by ChatBuiltInBrowserPanel.tsx.

957-957: The dependency array optimization is safe but could be clearer.

The effect correctly depends on tabIdsSignature rather than the full browserTabs array because tab URL changes are driven by the main process, which directly controls the webview via Electron's webContents API (not the renderer's src attribute). The renderer's webview src is only set during initial load (when pendingUrl exists or the tab isn't yet attached).

When the main process navigates a tab, the URL is updated in the status object and statusRef is refreshed immediately. For the active tab's fallback URL, statusRef.current?.url will have the fresh value, so the logic remains sound.

However, the dependency array could be more maintainable if documented, since future developers might not immediately understand why tab.url is accessed without a corresponding dependency on URL changes. If you want to add a safety margin:

+const tabUrlsSignature = useMemo(() => browserTabs.map(t => `${t.id}:${t.url ?? ''}`).join('|'), [browserTabs]);
-}, [activeTabId, applyStatus, tabIdsSignature, captureBase, webviewNavigationNonce]);
+}, [activeTabId, applyStatus, tabUrlsSignature, captureBase, webviewNavigationNonce]);

This ensures the effect re-runs if tab URLs change externally, improving clarity at minimal performance cost.

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

In `@apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx` at
line 957, The effect currently depends on activeTabId, applyStatus,
tabIdsSignature, captureBase, and webviewNavigationNonce but accesses tab.url
via browserTabs/statusRef; add a short comment above the useEffect explaining
why tab URLs are not included (main process drives navigation and statusRef is
updated) and to be safer/clearer add the active tab's URL to the dependency
array (e.g. use activeTab?.url or a derived tabUrlsSignature) so the effect
re-runs when the active tab's URL changes externally; update the dependency list
in the useEffect that references activeTabId, applyStatus, tabIdsSignature,
captureBase, webviewNavigationNonce and keep statusRef/pendingUrl logic as-is.
apps/desktop/src/renderer/components/files/FilesPage.tsx (1)

169-176: 💤 Low value

Second find fallback in defaultFilesWorkspaceId is effectively unreachable

Per the FilesWorkspace type (laneId: string | null), primary workspaces have laneId = null. The second find (without kind !== "primary") can only produce a different result than the first if a primary workspace has a non-null laneId — which appears never to occur. The fallback can be simplified:

♻️ Suggested simplification
 function defaultFilesWorkspaceId(workspaces: FilesWorkspace[], preferredLaneId: string | null): string {
   if (preferredLaneId) {
-    const laneWorkspace = workspaces.find((workspace) => workspace.kind !== "primary" && workspace.laneId === preferredLaneId)
-      ?? workspaces.find((workspace) => workspace.laneId === preferredLaneId);
+    const laneWorkspace = workspaces.find((workspace) => workspace.laneId === preferredLaneId);
     if (laneWorkspace) return laneWorkspace.id;
   }
   return workspaces[0]?.id ?? "";
 }

If primary workspaces can have a non-null laneId in some future scenario, the current double-find order (prefer non-primary, fall back to primary) would need to be preserved.

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

In `@apps/desktop/src/renderer/components/files/FilesPage.tsx` around lines 169 -
176, The fallback second find in function defaultFilesWorkspaceId is redundant
given FilesWorkspace.laneId is null for primary workspaces; simplify by removing
the second find and just search once for a non-primary workspace matching
preferredLaneId (using workspaces.find workspace => workspace.kind !== "primary"
&& workspace.laneId === preferredLaneId), returning its id if found, otherwise
fall back to workspaces[0]?.id ?? ""; if you expect primary workspaces might
later carry a laneId, preserve the current two-step preference logic (first
prefer non-primary then primary) when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ade-cli/src/cli.ts`:
- Line 3238: The help-resolution alias map is missing the new browser names, so
help commands like "ade built-in-browser --help" still fall back to top-level
help; update the same alias set used by the help path to include "browser",
"ade-browser", "built-in-browser", and "builtin-browser" and point them to the
browser command so help lookup resolves the same command that the execution
dispatch (where primary is compared and buildBrowserPlan is called) uses. Locate
the alias mapping used by the help/resolution code (the same map that the help
path consults) and add entries for those four aliases that map to the existing
browser command key so help and execution are consistent.

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts`:
- Around line 443-468: In navigate(), don't clear inspect/selection state or
commit tab changes until the target tab is validated and navigation succeeds:
first resolve which tab will be used (validate input.tabId against tabs or use
ensureActiveTab) before calling stopInspectQuietly() or
clearSelectionInternal(); for new tabs created by createTabState(), treat them
as provisional (do not push to tabs or set activeTabId) until
wc.loadURL(targetUrl) completes successfully; perform wc.loadURL inside
try/catch and on failure rollback any provisional tab or avoid mutating
tabs/activeTabId, then only call emitStatus() and return getStatus() after
successful load; ensure references to navigate, stopInspectQuietly,
clearSelectionInternal, createTabState, tabs, activeTabId, wc.loadURL,
ensureActiveTab, emitStatus, and getStatus are updated accordingly.

In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Around line 2230-2235: The parser functions
parseBuiltInBrowserAttachWebviewArgs and parseBuiltInBrowserTabArgs currently
reject missing/blank tabId but the shared IPC types declare tabId?: string |
null; update these parsers to align with the shared contract by removing the
unconditional invalidBuiltInBrowserArg call and instead: use
optionalBuiltInBrowserString(record, "tabId", channel, 128) to allow
undefined/null, only call invalidBuiltInBrowserArg if tabId is present but
equals the empty string, and ensure the returned type allows tabId to be
undefined|null; reference the existing helpers builtInBrowserRecord,
optionalBuiltInBrowserString, and invalidBuiltInBrowserArg when making the
change.

In `@apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx`:
- Around line 2146-2148: The component introduced context-only sends via
hasBuiltInBrowserContext and singleReady, but submitComposerDraft() (Cursor
Cloud branch) and the active-turn steer button still require draft text, causing
inconsistent behavior; add a shared predicate (e.g., hasContextSelection)
computed from hasBuiltInBrowserContext || hasIosElementContext ||
hasAppControlContext and use that instead of ad-hoc checks so all submit/steer
paths (submitComposerDraft(), the steer button render logic, sendEnabled
calculation, and any Cursor Cloud branches) consistently allow context-only
sends, or alternatively revert those branches to require draft.trim().length > 0
to keep text-only behavior—update references to parallelChatMode, modelId,
draft, singleReady, parallelReady, composerInputLocked, parallelLaunchBusy and
busy to use the unified predicate.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx`:
- Around line 2221-2224: addSimulatorCaptureContext and addPreviewCaptureContext
currently return silently when onAddContext is missing; mirror selectElementAt's
behavior by showing the same user-facing message ("Chat attachments are not
available in this panel.") instead of silently returning. Locate
addSimulatorCaptureContext and addPreviewCaptureContext and add the same
early-check/how-to-notify logic used in selectElementAt (use the same
toast/notification call or helper) so the UI gives consistent feedback when
onAddContext is undefined.

In `@apps/desktop/src/renderer/components/files/FilesPage.tsx`:
- Around line 1311-1316: The reconciliation effect that sets workspaceId (using
defaultFilesWorkspaceId and setWorkspaceId) can change the workspace without
clearing per-workspace state (openTabs, activeTabPath, selectedNodePath),
causing stale tabs to operate against the wrong workspace; modify that effect to
either call the existing switchWorkspace(nextWorkspaceId) callback (preferred)
or, if calling it would cause cycles, replicate its behavior: when
nextWorkspaceId differs from current, clear openTabs, reset activeTabPath and
selectedNodePath to null (or their initial values), then
setWorkspaceId(nextWorkspaceId) so that tab state is reset consistently with the
switchWorkspace logic.

In `@apps/desktop/src/renderer/components/terminals/useWorkSessions.ts`:
- Around line 465-468: The setter setWorkSidebarWidthPct currently uses
Math.max/Math.min which produces NaN for non-finite inputs; change
setWorkSidebarWidthPct to first validate widthPct with Number.isFinite (or
isFinite) and, if not finite, substitute the same default used in
normalizeWorkSidebarWidthPct (36) before clamping to the allowed range (26–55)
and calling setProjectViewState; reference the setWorkSidebarWidthPct callback
and normalizeWorkSidebarWidthPct in appStore when implementing this guard.

---

Duplicate comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 2620-2677: buildBrowserPlan currently builds plans targeting the
built_in_browser domain even when the CLI is running in headless mode (no
desktop socket), causing late runtime errors; fix by validating desktop-socket
availability up-front: add/consume a helper like ensureDesktopSocketAvailable()
(or implement a small check) and call it at the start of buildBrowserPlan to
throw a CliUsageError with a clear message (mentioning --socket and
HELP_BY_COMMAND.browser if helpful) whenever the desktop socket is not
available, so plans targeting "built_in_browser" fail fast instead of producing
"Domain 'built_in_browser' is unavailable in this runtime" at execution time.

In `@apps/desktop/src/main/main.ts`:
- Line 3450: The live AppContext returned by initContextForProjectRoot() is
missing the builtInBrowserService property (rpcRuntime wires it in and the
dormant context includes it), causing getActiveContext().builtInBrowserService
to be undefined for active projects; update initContextForProjectRoot() to
include a builtInBrowserService field (initialized the same way or to
null/placeholder consistent with the dormant context) so the live and dormant
context shapes match, and ensure any callers (e.g., code expecting
getActiveContext().builtInBrowserService) handle the same null/instance
semantics.

In `@apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts`:
- Around line 748-769: In debuggerDetachListener (created inside
attachDebuggerListeners), replace the manual nulling of listener references with
a call to detachDebuggerListeners(wc) so the handlers are actually removed from
wc.debugger; ensure debuggerDetachListener still logs and updates
inspecting/debuggerAttachedForInspect state (and that detachDebuggerListeners
clears debuggerMessageListener, debuggerDetachListener,
inspectListenerWebContents and calls wc.debugger.off/.removeListener for the
stored callbacks) to prevent duplicated handlers on subsequent inspect sessions.
- Around line 394-400: Reject non-browser webContentsId values by verifying the
WebContents type and partition before rebinding: after obtaining nextWebContents
via electronWebContents.fromId(input.webContentsId) (and the existing
null/isDestroyed checks) assert nextWebContents.getType() === 'webview' and also
validate nextWebContents.getWebPreferences().partition matches the expected
browser partition pattern (or a configured allowed partition) and throw an Error
if not; perform these checks in attachWebview (the block that calls
configureBrowserSession() and configureBrowserWebContents(nextWebContents)) so
only genuine webview/guest contents are accepted.

In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Around line 6447-6450: The handler for IPC.builtInBrowserAttachWebview
currently trusts the client-supplied webContentsId; after
guardBuiltInBrowserIpc(...) and before calling
ensureBuiltInBrowser().attachWebview(...), verify that the webContentsId in
parseBuiltInBrowserAttachWebviewArgs(arg, IPC.builtInBrowserAttachWebview)
belongs to the requesting renderer (compare it to event.sender.id or to
BrowserWindow.fromWebContents(event.sender).webContents.id/owner window id) and
reject/throw if it does not match; keep the existing guard call and only forward
the parsed args to attachWebview when ownership is confirmed.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 3624-3632: The browser temp screenshot save is still calling
addAttachment(...) which updates latestAttachmentRef and causes subsequent
iOS/App selections to reuse the browser PNG; change the flow in the
AgentChatPane logic that handles item.screenshotDataUrl (the block calling
window.ade.agentChat.saveTempAttachment and addAttachment) so that after saving
via agentChat.saveTempAttachment(...) you only set attachmentPath = saved.path
(and use inferAttachmentType(saved.path, "image/png") locally if needed) but do
NOT call addAttachment(...) or otherwise update latestAttachmentRef for this
temp browser screenshot; ensure addAttachment remains used for real user
attachments only.
- Around line 4252-4260: When isParallelLaunch is true the early branch still
only forwards draft and attachments and never includes the visual context
computed by builtInBrowserContextSnapshot, iosContextSnapshot,
appControlContextSnapshot or their derived values (visualContextPrefix,
visualContextDisplayChips). Update the parallel-launch branch that forwards to
child lanes to also compute or reuse the same iosContextPrefix,
appControlContextPrefix, builtInBrowserContextPrefix and corresponding display
chips and include visualContextPrefix and visualContextDisplayChips in the
payload sent to the child lanes (the same fields used on the normal send path),
so the built-in browser/iOS/App Control chips present in the composer are
forwarded in parallel launches.

In `@apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx`:
- Around line 420-429: The handler for ADE_OPEN_BUILT_IN_BROWSER_EVENT changes
only view/tab but doesn't actually open the sidebar when it's collapsed; update
the openBrowserSidebar function to also set the sidebar open (call the work
sidebar open setter, e.g. setWorkSidebarOpen(true) or the appropriate method on
the work object) so the browser becomes visible, and add that setter
(setWorkSidebarOpen) to the useEffect dependency array along with setViewMode
and setWorkSidebarTab.

In `@apps/desktop/src/renderer/lib/openExternal.ts`:
- Around line 45-49: openUrlInAdeBrowser currently forwards the original
unnormalized url to openExternalUrl even when canOpenInAdeBrowser(normalized)
rejects unsafe schemes; update openUrlInAdeBrowser so it first computes
normalized = normalizeBrowserUrlInput(url) and then only calls openExternalUrl
with the normalized value for allowed schemes (i.e., call
openExternalUrl(normalized) only when normalized is truthy and
canOpenInAdeBrowser(normalized) is true); if normalized is falsy or
canOpenInAdeBrowser(normalized) is false, do not call openExternalUrl (simply
return) to ensure rejected schemes like "javascript:" or "data:" are never
forwarded.

---

Nitpick comments:
In `@apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx`:
- Around line 52-67: Replace the locally defined BuiltInBrowserContextItem with
an extension of the shared type: import the shared BuiltInBrowserContextItem
(refer to it here as SharedContextItem) and define the local type as
SharedContextItem & { selector?: string | null; text?: string | null; role?:
string | null; tagName?: string | null; sessionId?: string | null;
screenshotDataUrl?: string | null; selectedAt?: string }; ensure you remove the
open index signature ([key: string]: unknown) and any divergent required fields
so the local type stays consistent with the shared BuiltInBrowserContextItem
while adding only the extra optional properties used by
ChatBuiltInBrowserPanel.tsx.
- Line 957: The effect currently depends on activeTabId, applyStatus,
tabIdsSignature, captureBase, and webviewNavigationNonce but accesses tab.url
via browserTabs/statusRef; add a short comment above the useEffect explaining
why tab URLs are not included (main process drives navigation and statusRef is
updated) and to be safer/clearer add the active tab's URL to the dependency
array (e.g. use activeTab?.url or a derived tabUrlsSignature) so the effect
re-runs when the active tab's URL changes externally; update the dependency list
in the useEffect that references activeTabId, applyStatus, tabIdsSignature,
captureBase, webviewNavigationNonce and keep statusRef/pendingUrl logic as-is.

In `@apps/desktop/src/renderer/components/files/FilesPage.tsx`:
- Around line 169-176: The fallback second find in function
defaultFilesWorkspaceId is redundant given FilesWorkspace.laneId is null for
primary workspaces; simplify by removing the second find and just search once
for a non-primary workspace matching preferredLaneId (using workspaces.find
workspace => workspace.kind !== "primary" && workspace.laneId ===
preferredLaneId), returning its id if found, otherwise fall back to
workspaces[0]?.id ?? ""; if you expect primary workspaces might later carry a
laneId, preserve the current two-step preference logic (first prefer non-primary
then primary) when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 643a9d7d-0237-4b91-bc61-8fba413537f8

📥 Commits

Reviewing files that changed from the base of the PR and between 3d31a5b and 2c3a544.

⛔ Files ignored due to path filters (11)
  • .claude/scheduled_tasks.lock is excluded by !**/*.lock
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/chat/composer-and-ui.md is excluded by !docs/**
  • docs/features/computer-use/app-control.md is excluded by !docs/**
  • docs/features/files-and-editor/README.md is excluded by !docs/**
  • docs/features/files-and-editor/editor-surfaces.md is excluded by !docs/**
  • docs/features/ios-simulator/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/ui-surfaces.md is excluded by !docs/**
  • goal.md is excluded by !*.md
📒 Files selected for processing (48)
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/appControl/appControlService.ts
  • apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts
  • apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.test.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/renderer/components/app/FloatingFilesWorkspace.tsx
  • apps/desktop/src/renderer/components/app/RightEdgeFloatingPane.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatAppControlPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatBuiltInBrowserPanel.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx
  • apps/desktop/src/renderer/components/chat/chatMarkdown.tsx
  • apps/desktop/src/renderer/components/files/FilesPage.test.tsx
  • apps/desktop/src/renderer/components/files/FilesPage.tsx
  • apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.ts
  • apps/desktop/src/renderer/components/run/LaneRuntimeBar.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalView.tsx
  • apps/desktop/src/renderer/components/terminals/TerminalsPage.tsx
  • apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx
  • apps/desktop/src/renderer/components/terminals/WorkStartSurface.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/renderer/lib/openExternal.ts
  • apps/desktop/src/renderer/state/appStore.test.ts
  • apps/desktop/src/renderer/state/appStore.ts
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/appControl.ts
  • apps/desktop/src/shared/types/builtInBrowser.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/shared/types/iosSimulator.ts
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
💤 Files with no reviewable changes (4)
  • apps/desktop/src/renderer/components/app/AppShell.tsx
  • apps/desktop/src/renderer/components/app/FloatingFilesWorkspace.tsx
  • apps/desktop/src/renderer/index.css
  • apps/desktop/src/renderer/components/app/RightEdgeFloatingPane.tsx
✅ Files skipped from review due to trivial changes (19)
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/components/run/LaneRuntimeBar.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx
  • apps/desktop/src/shared/types/iosSimulator.ts
  • apps/desktop/src/renderer/state/appStore.test.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/desktop/src/renderer/components/lanes/useLaneWorkSessions.ts
  • apps/desktop/src/renderer/components/files/FilesPage.test.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/shared/types/appControl.ts
  • apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.test.ts
  • apps/desktop/src/shared/types/builtInBrowser.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx
  • apps/desktop/src/renderer/components/chat/ChatAppControlPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/desktop/src/renderer/components/terminals/WorkStartSurface.tsx
  • apps/desktop/src/main/services/appControl/appControlService.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.ts
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/chat/chatMarkdown.tsx
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/desktop/src/renderer/components/terminals/useWorkSessions.test.ts

Comment thread apps/ade-cli/src/cli.ts
Comment on lines +2230 to +2235
const parseBuiltInBrowserAttachWebviewArgs = (value: unknown, channel: string): BuiltInBrowserAttachWebviewArgs => {
const record = builtInBrowserRecord(value, channel, true);
const webContentsId = builtInBrowserNumber(record, "webContentsId", channel, { min: 1, max: Number.MAX_SAFE_INTEGER });
const tabId = optionalBuiltInBrowserString(record, "tabId", channel, 128);
if (!tabId) invalidBuiltInBrowserArg(channel, "tabId must be a non-empty string");
return { tabId, webContentsId };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Shared IPC types no longer match these parsers.

parseBuiltInBrowserAttachWebviewArgs() and parseBuiltInBrowserTabArgs() now reject missing/blank tabId, but the shared contracts still declare tabId?: string | null. That means preload/renderer code can type-check and still fail at runtime on these handlers.

As per coding guidelines, "Keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes".

Also applies to: 2268-2272

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

In `@apps/desktop/src/main/services/ipc/registerIpc.ts` around lines 2230 - 2235,
The parser functions parseBuiltInBrowserAttachWebviewArgs and
parseBuiltInBrowserTabArgs currently reject missing/blank tabId but the shared
IPC types declare tabId?: string | null; update these parsers to align with the
shared contract by removing the unconditional invalidBuiltInBrowserArg call and
instead: use optionalBuiltInBrowserString(record, "tabId", channel, 128) to
allow undefined/null, only call invalidBuiltInBrowserArg if tabId is present but
equals the empty string, and ensure the returned type allows tabId to be
undefined|null; reference the existing helpers builtInBrowserRecord,
optionalBuiltInBrowserString, and invalidBuiltInBrowserArg when making the
change.

Comment thread apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
Comment thread apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx
Comment thread apps/desktop/src/renderer/components/files/FilesPage.tsx
Comment thread apps/desktop/src/renderer/components/terminals/useWorkSessions.ts
- builtInBrowserService.createTab: normalizeBrowserUrl runs before any tab
  state mutation, so an invalid URL throws without leaving an orphan tab
- ade-cli: add ade-browser/built-in-browser/builtin-browser aliases to the
  help-resolution map so all four names route to the browser command
- IPC: tighten BuiltInBrowserAttachWebviewArgs.tabId and BuiltInBrowserTabArgs.tabId
  to required strings to match parser runtime; switch the two parsers to
  invalidBuiltInBrowserArg(...) so TS narrows
- AgentChatComposer: extract hasContextSelection so the cloud submit branch
  and active-turn steer button accept context-only sends, matching singleReady
- ChatIosSimulatorPanel: addSimulatorCaptureContext and addPreviewCaptureContext
  show the same "Chat attachments are not available in this panel." toast as
  selectElementAt instead of failing silently
- FilesPage: when the lane-driven workspace reconciliation changes workspaceId,
  clear openTabs/activeTabPath/selectedNodePath so save/read targets the right
  workspace
- useWorkSessions.setWorkSidebarWidthPct: fall back to 36 on non-finite input
  before clamping, mirroring appStore.normalizeWorkSidebarWidthPct
- TerminalView: drop the off-by-one fallback that re-called getLine with the
  1-based bufferLineNumber

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 4, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot review but do not make fixes

Here's the updated review for the new Greptile round. Still no fixes.


Greptile P1 — Already Fixed

createTab orphan-tab on invalid URLFixed. normalizeBrowserUrl(input.url) is now called at line 476 before createTabState() / tabs = [...tabs, tab], mirroring the navigate() pattern. An invalid URL throws without touching tabs, so no ghost tab is created.


Greptile P2 (security) — Confirmed Open

will-attach-webview allows about:blank and empty src unconditionally (main.ts:372)
isAllowedAdeBrowserWebviewSource returns true early for both "" and "about:blank" so the handler sets the persist:ade-browser partition and enforced sandbox flags — but does not prevent attachment. A compromised renderer context (e.g. XSS in loaded web content that reaches the parent renderer via a postMessage or prototype-pollution vector) could create a <webview src="about:blank">, pass this gate, and then call loadURL() on it after attachment since the handler only fires once. The partition's permission handler is the last line of defence.


Greptile P2 — Confirmed Open

stripUrlFilterParams circular dependency (useWorkSessions.ts:482-498)
stripUrlFilterParams closes over searchParams and is listed in the useEffect dependency array. When it calls navigate(..., { replace: true }), React Router updates searchParams, which changes the useCallback memoization (new function reference), which re-triggers the parent useEffect. The if (!changed) return guard prevents the second navigate call, but the full effect body still re-runs on every strip. The pattern is fragile — a future change to the effect body between stripUrlFilterParams() and the if (!changed) guard could reintroduce a loop. searchParams should be read from a ref inside the callback, or the effect should depend on a stable snapshot, to break the cycle.


Greptile P2 — Confirmed Open (residual)

consumePendingBuiltInBrowserNavigation() called on every status update
The default-tab useEffect (line 1038) re-runs whenever apiAvailable, api, status, browserTabs.length, or defaultBrowserOpenedRef changes. consumePendingBuiltInBrowserNavigation() at line 1046 is called on every such re-run. The fix from the previous round addressed the stale-flag-on-fast-IPC case, but if a user clicks two terminal links in quick succession:

  1. First click: sets flag true, panel opens, flag consumed on first effect run.
  2. Second link's navigate IPC is in-flight; panel is already mounted.
  3. A status update with browserTabs.length === 0 triggers a re-run: hadPendingNavigation is false (already consumed), so the default Google tab is created — racing the second link's tab.

The flag is single-use and cannot guard rapid sequential clicks.


Previously Identified Issues (still open)

normalizeUrlForNavigation (URL bar)file:, blob:, data:, devtools: still pass to IPC from the panel's URL bar input. Still open.

Debugger state singleton across tabs — closure-level inspecting, debuggerAttachedForInspect, listeners not per-tab. Still open.

autoAttachedContextIdsRef unbounded growth — no cap or clear-on-navigate. Still open.

TerminalView.tsx:160 off-by-one?? term.buffer.active.getLine(bufferLineNumber) fallback uses 1-based index as 0-based. Still open.

Copilot finished work on behalf of arul28 May 4, 2026 06:17
- builtInBrowserService.navigate: validate tabId before clearing inspect/
  selection state, so navigate() with an invalid id no longer leaves the
  service in a degraded state (Greptile P1)
- main.ts will-attach-webview hardening: did-attach-webview listener installs
  setWindowOpenHandler('deny') and a stricter will-navigate filter
  (isAllowedAdeBrowserWebviewNavigation, http(s) only) so a compromised
  renderer can't attach an empty-src webview and then loadURL elsewhere
  (Copilot P2)
- useWorkSessions stripUrlFilterParams: compare via
  URLSearchParams.toString() so the effect is self-stabilizing — navigate()
  only fires when stripping yields a different query string, regardless of
  effect-dep churn (Copilot P2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 4, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot review but do not make fixes

Here's the updated review for this Greptile round. Still no fixes.


Greptile P1 — Already Fixed

navigate() with invalid tabId cancels inspection before throwing (builtInBrowserService.ts:448-454)
Fixed. The code now validates input.tabId up front (lines 451–453), storing the found tab in existingTab and throwing immediately if null — before switchingTabs is evaluated and before stopInspectQuietly / clearSelectionInternal run. The pattern mirrors the newTab tab-limit fix applied in an earlier iteration.


Greptile Issue 2 — Confirmed Open

Module-level regex with g flag (TerminalView.tsx:102)
TERMINAL_LINK_PATTERN is a module-level singleton with the g flag. lastIndex is correctly reset to 0 at line 166 before each provideLinks call. In JavaScript's single-threaded model this is safe today. However, the g flag on a shared regex is a latent hazard: if xterm.js ever calls provideLinks re-entrantly on two terminal instances in the same microtask queue turn (e.g. after a React Strict Mode double-invoke, or if a future xterm version parallelises providers), the lastIndex interleaving would cause skipped or duplicated matches. Moving the regex literal inside createTerminalLinkProvider (or inside provideLinks) would eliminate the shared-state risk at zero runtime cost.


Greptile Issue 3 — Confirmed Open

status in the default-tab effect's dependency array (ChatBuiltInBrowserPanel.tsx:1078)
The dep array at line 1078 includes the full status object, but the effect body only reads browserTabs.length (already separately in deps) and ref-guarded fields — nothing else from status. Every IPC status event (URL change, title change, loading-state change) re-runs the effect and calls consumePendingBuiltInBrowserNavigation(). If a status event arrives between two rapid link-clicks, the flag set by the second click can be consumed by this extra run before the effect's browserTabs.length === 0 path runs for the second panel mount, resulting in an unexpected default Google tab being opened alongside the in-flight navigate. Removing status from the dep array (or replacing it with a narrower field like browserTabs.length which is already there) would fix this while keeping the existing early-return logic intact.


Previously Identified Issues (still open)

will-attach-webview about:blank gate (main.ts:372) — Still open. Acknowledged as a deliberate trade-off against the partition's permission handler.

stripUrlFilterParams circular dependency (useWorkSessions.ts:482-498) — Still open.

URL bar scheme validation (ChatBuiltInBrowserPanel.tsx) — file:, blob:, data:, devtools: in panel URL bar still bypass renderer-side validation. canOpenInAdeBrowser guards terminal/markdown links but not the URL bar input.

Debugger state singleton across tabsinspecting, debuggerAttachedForInspect, and listeners are closure-level; tab switch mid-inspect can leave listeners on the wrong WebContents. Still open.

Comment on lines +511 to +516
return getStatus();
}

async function closeTab(input: BuiltInBrowserTabArgs): Promise<BuiltInBrowserStatus> {
const tabId = input.tabId?.trim();
if (!tabId) throw new Error("Browser tab id is required.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 switchTab clears selection unconditionally, even for same-tab switches

clearSelectionInternal() is called on every switchTab invocation, regardless of whether tab.id === activeTabId. If a caller (e.g., the UI re-rendering) calls switchTab with the already-active tab, the user's selected element context is silently discarded. The stopInspectQuietly call already has the correct guard (tab.id !== activeTabId); clearSelectionInternal should use the same guard.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts
Line: 511-516

Comment:
**`switchTab` clears selection unconditionally, even for same-tab switches**

`clearSelectionInternal()` is called on every `switchTab` invocation, regardless of whether `tab.id === activeTabId`. If a caller (e.g., the UI re-rendering) calls `switchTab` with the already-active tab, the user's selected element context is silently discarded. The `stopInspectQuietly` call already has the correct guard (`tab.id !== activeTabId`); `clearSelectionInternal` should use the same guard.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +449 to +480
// doesn't leave the service with cleared inspect/selection state.
let existingTab: BrowserTabState | null = null;
if (!input.newTab && input.tabId) {
existingTab = tabs.find((entry) => entry.id === input.tabId) ?? null;
if (!existingTab) throw new Error(`Browser tab not found: ${input.tabId}`);
}
const switchingTabs = input.newTab || (input.tabId && input.tabId !== activeTabId);
if (switchingTabs) {
await stopInspectQuietly("built_in_browser.navigate_stop_inspect_failed");
clearSelectionInternal();
}
let tab = input.newTab ? createTabState() : null;
if (tab) {
tabs = [...tabs, tab];
activeTabId = tab.id;
} else if (existingTab) {
tab = existingTab;
activeTabId = tab.id;
} else {
tab = ensureActiveTab();
}
const wc = tab.webContents;
attachViewsToCurrentWindow();
await wc.loadURL(targetUrl);
emitStatus();
return getStatus();
}

async function createTab(input: BuiltInBrowserCreateTabArgs = {}): Promise<BuiltInBrowserStatus> {
if (tabs.length >= MAX_BROWSER_TABS) {
throw new Error(`ADE browser is limited to ${MAX_BROWSER_TABS} tabs. Close a tab before opening another.`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Inspect mode desynchronizes with CDP overlay after same-tab navigation

When navigate() is called without newTab and without a different tabId, switchingTabs evaluates to false, so stopInspectQuietly() is never called. However, Electron's CDP Overlay.setInspectMode command is page-level and is lost when the WebContents navigates. After the load completes, inspecting in service state remains true and getStatus().isInspecting returns true, but the CDP overlay is gone — the user sees the "Inspecting" badge in the UI but clicking elements has no effect. The fix is to always call stopInspectQuietly() when loading a URL into the current tab, not only when switching tabs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts
Line: 449-480

Comment:
**Inspect mode desynchronizes with CDP overlay after same-tab navigation**

When `navigate()` is called without `newTab` and without a different `tabId`, `switchingTabs` evaluates to `false`, so `stopInspectQuietly()` is never called. However, Electron's CDP `Overlay.setInspectMode` command is page-level and is lost when the WebContents navigates. After the load completes, `inspecting` in service state remains `true` and `getStatus().isInspecting` returns `true`, but the CDP overlay is gone — the user sees the "Inspecting" badge in the UI but clicking elements has no effect. The fix is to always call `stopInspectQuietly()` when loading a URL into the current tab, not only when switching tabs.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@arul28 arul28 merged commit d33c23e into main May 4, 2026
24 of 25 checks passed
@arul28 arul28 deleted the ade/ade-browser-d5ca6ac8 branch May 4, 2026 06:50
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.

2 participants