Skip to content

Render HTML artifacts in the guest pool - #94

Closed
mutewinter wants to merge 9 commits into
mainfrom
worktree-html-artifacts-guest-pool
Closed

Render HTML artifacts in the guest pool#94
mutewinter wants to merge 9 commits into
mainfrom
worktree-html-artifacts-guest-pool

Conversation

@mutewinter

@mutewinter mutewinter commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

HTML file artifacts rendered in a sandboxed, opaque-origin <iframe> while the agent loaded the identical asset URL in a <webview> guest. So everything origin-scoped behaved differently between the surface the agent screenshots to check its own work and the surface the human reads: localStorage, cookies, IndexedDB, same-origin fetch of a sibling file. Agent-authored HTML that persists a filter selection, or reads its own data.json, is exactly the class of thing that passed the agent's check and failed in front of the user.

Both surfaces now render as a real origin through the same pool. Back/forward chrome is the visible payoff and the smaller half.

Plan: docs/plans/active/html-artifacts-in-the-guest-pool.md. Resolves the open finding html-artifact-iframe-navigation.

What changed

  • A second target kind. BrowserTargetId gains ${taskId}/artifact alongside ${taskId}/${sessionId}. artifact is a fixed sentinel; session ids are ses_-prefixed ULIDs, so no collision. decodeBrowserTargetId now returns a discriminated {kind}.
  • One guest per task, not per file. Switching files is a loadURL, the way a browser tab does it. The panel and the expand modal share the one guest.
  • Its own everything. workspace.artifactPreview RPC, machines/artifact-preview.ts (presence lease + 30s grace), and a separate storage profile. Deliberately not task-browser.ts, whose session key, 1-hour agent-idle clock and agent-browser close --session fan-out all describe something else.
  • The agent surface is byte-for-byte unchanged. listTargets skips artifact targets and the CDP bridge refuses them.
  • Both reload nonces deleted. Escape-to-root is loadURL(entryUrl) from any depth; true reload of a navigated-to sub-page works for the first time.
  • browser-panel.tsx splits: the target-generic parts become use-guest-navigation + a shared load-error notice, used by both surfaces.

Security: a tightening, not a loss

The finding assumed moving off the opaque sandbox was a one-way loss. Measured against what the iframe actually granted, it isn't:

iframe (before) artifact guest (now)
Origin opaque real (assets.<taskId>.localhost)
Storage / cookies none own profile, isolated from the browsing profile
Camera, mic, geolocation, USB granted via allow denied by the session handler
Popups granted denied

The iframe's allow list granted camera, microphone, geolocation, clipboard, display-capture, MIDI, payment and USB. What is genuinely new is storage, scoped to a per-task asset origin holding nothing but that task's own files.

Also fixes a pre-existing bug on main

A body-mounted guest kept painting through an app-wide modal's overlay — opening a dialog is not a tab switch, so nothing told the slot it was covered. Open Settings over a live browser panel today and you can see it.

Verification

Driven against a running Studio with an artifact page written to exercise the differences. Confirmed: real origin; localStorage works and survives navigation; fetch("data.json") succeeds; zero iframes; artifact storage lands only in artifact-preview-session and is absent from browser-session; /json?id= omits the artifact target and its CDP upgrade is refused while a session-shaped id is still accepted; in-page link → toolbar tracks it → escape-to-root returns home; guest paints above the modal overlay; close → reap after grace → reopen creates a fresh guest; viewMode: "raw" unaffected.

The item flagged as most likely to break did not. Radix does set pointer-events: none on the body, but showOverSlot already sets pointer-events: auto on the container, so the guest stays hit-testable under the dialog. No modal={false} workaround and no non-modal Content, so the focus trap and outside-dismiss are kept.

Two defects found this way and fixed, neither visible from reading:

  1. The panel never re-claimed the guest after the expand modal closed (two slots, one guest; the loser never re-showed).
  2. Reopening a reaped preview hung on "Opening preview…" — a cached query replayed its old answer instead of asking for a new guest.

Still unverified, listed in the plan: app zoom at 0.5x/2x (mechanism unchanged and shared with the browser panel; bounds invariant confirmed at 1x), range requests, and a physical OS-level click (host CDP input does not route into a <webview> at all — a control with no modal open failed identically — so this was established by hit testing).

pnpm check-and-test:ci green, 22/22.


Open in Devin Review

Summary by CodeRabbit

Release Notes

  • New Features

    • HTML artifact previews now support navigation, reload, find-in-page, zoom, and returning to the entry page.
    • Added retryable error messaging for failed artifact loads.
    • Links from artifact previews can open safely in the external browser.
    • Artifact previews now support downloads and maintain separate task-specific browsing state.
  • Improvements

    • Improved modal and overlay coordination for consistent layering and visibility.
    • Reopening artifacts now preserves the viewer while returning to the starting page.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

HTML artifacts now render through task-scoped pooled webview guests. The change adds target contracts, lifecycle management, navigation controls, overlay coordination, isolated storage, and Studio integration.

Changes

Artifact target contracts and isolation

Layer / File(s) Summary
Artifact target identity and storage
packages/workspace/src/types.ts, packages/workspace/src/lib/task-dir-utils.ts, packages/workspace/src/constants.ts
Artifact targets use a dedicated target ID form and per-task Chromium profile paths.
Target creation and access policy
apps/studio/src/electron-main/browser-view/*, packages/workspace/src/logic/server/routes/cdp-bridge.ts
Artifact targets use nullable sessions, deny popups, remain outside agent discovery, and are rejected by the CDP bridge.
Cleanup and supporting fixtures
packages/workspace/src/lib/trash-task.ts, packages/workspace/src/machines/task-browser.test.ts, packages/workspace/src/test/*
Task trashing removes artifact profiles. Browser test configurations support artifact target creation.

Workspace lifecycle and RPC coordination

Layer / File(s) Summary
Artifact preview lifetime machine
packages/workspace/src/machines/artifact-preview.ts, packages/workspace/src/machines/artifact-preview.test.ts
The XState machine tracks presence leases, grace-period cleanup, target closure, forced reaping, and teardown races.
Workspace actor integration
packages/workspace/src/machines/workspace/*
The workspace spawns artifact actors, forwards target and presence events, cleans stopped actors, and force-reaps previews during task trashing.
Artifact preview RPC
packages/workspace/src/rpc/routes/artifact-preview.ts, packages/workspace/src/rpc/index.ts
RPC handlers open task previews and manage presence subscriptions.

Shared guest controls and overlay coordination

Layer / File(s) Summary
Guest navigation and controls
apps/studio/src/client/hooks/use-guest-navigation.ts, apps/studio/src/client/hooks/use-guest-menu-state.ts
Navigation, URL state, history, reload, zoom, load errors, and menu focus handling move into reusable hooks.
Guest coverage and slot management
apps/studio/src/client/hooks/use-guest-covered.ts, apps/studio/src/client/hooks/use-browser-slot.ts, apps/studio/src/client/hooks/use-browser-find.ts
Covered guests park their slots and release find ownership. Visible guests can receive a configured z-index.
Browser panel error handling
apps/studio/src/client/components/task/browser-panel.tsx, apps/studio/src/client/components/task/guest-load-error.tsx
The browser panel uses guest hooks and renders a shared load-error notice with retry support.
Browser pool stacking
apps/studio/src/client/lib/browser-pool.ts, apps/studio/src/client/atoms/studio-modal.ts
Body-mounted guests support dynamic z-index values and studio modal coverage state.

Studio artifact preview integration

Layer / File(s) Summary
Pooled artifact preview
apps/studio/src/client/components/html-artifact-preview.tsx
HtmlArtifactPreview leases shared guests, coordinates hosts, supports navigation chrome, find-in-page, zoom, reload, external links, and load recovery.
File viewer wiring
apps/studio/src/client/components/file-viewer.tsx, apps/studio/src/client/components/task/view.tsx
HTML files use the pooled preview. Home navigation uses a nonce without remounting the viewer.
Modal stacking and documentation
apps/studio/src/client/components/task/file-viewer-modal.tsx, docs/findings/html-artifact-iframe-navigation.md
The file viewer modal supplies guest z-index 51. The finding records the webview-based implementation and its storage and permission behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant HtmlArtifactPreview
  participant ArtifactPreviewRPC
  participant Workspace
  participant BrowserGuest
  User->>HtmlArtifactPreview: Open HTML artifact
  HtmlArtifactPreview->>ArtifactPreviewRPC: Request task preview target
  ArtifactPreviewRPC->>Workspace: Register target and acquire presence
  Workspace->>BrowserGuest: Create or reuse artifact guest
  HtmlArtifactPreview->>BrowserGuest: Attach slot and navigate entry URL
  BrowserGuest-->>HtmlArtifactPreview: Navigation and load state
  HtmlArtifactPreview-->>User: Render preview controls and content
  User->>HtmlArtifactPreview: Close or change tab
  HtmlArtifactPreview->>ArtifactPreviewRPC: Release presence
  ArtifactPreviewRPC->>Workspace: Start grace-period lifecycle
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: rendering HTML artifacts in the pooled browser guest system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-html-artifacts-guest-pool

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

❤️ Share

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

devin-ai-integration[bot]

This comment was marked as resolved.

HTML file artifacts rendered in a sandboxed, opaque-origin `<iframe>` while the
agent loaded the identical asset URL in a `<webview>` guest. Everything
origin-scoped therefore behaved differently between the surface the agent
screenshots to check its own work and the surface the user reads: localStorage,
cookies, and same-origin fetch of a sibling file. Agent-authored HTML that
persists a selection, or reads its own data.json, could pass the agent's check
and fail in front of the user.

Both now render as a real origin through the same pool, which also gives the
preview real back/forward/reload and makes escape-to-root a loadURL from any
depth rather than a remount of `src`. Both reload nonces are gone.

The artifact guest is a second target kind, `${taskId}/artifact`, with its own
RPC, its own lifetime machine (a presence lease plus a 30s grace period, without
task-browser's agent-idle clock or agent-browser daemon fan-out), and its own
storage profile. It is excluded from listTargets and refused by the CDP bridge,
so the agent's target list and CDP surface are unchanged.

Net security tightening rather than the loss the finding assumed: the iframe's
allow list granted camera, microphone, geolocation, clipboard and USB, and its
sandbox granted popups; the guest session denies every permission and every
window open. What is new is storage, scoped to a per-task asset origin in a
profile separate from the agent's browsing profile.

Also fixes a pre-existing bug this would otherwise inherit: a body-mounted guest
kept painting through an app-wide modal's overlay, since opening a dialog is not
a tab switch and nothing else told the slot it was covered.
The artifact panel and the expand modal are two mounted previews of a single
pooled guest, and three review findings were the same mistake: something
singular -- the current page, the Cmd+F opener, the presence lease -- was driven
from a host's own props, which cannot say which host owns it.

- Expanding threw the reader back to the entry page, because the modal's mount
  ran the same navigate-on-mount effect the panel does. A host mounting over a
  guest another preview is already showing now adopts the page on screen; later
  runs navigate as before, so go-home still works from the modal.
- Cmd+F died after closing the modal. The find opener is a single slot: the
  modal claimed it, cleared it on unmount, and the panel never re-registered.
  useBrowserFind takes the same `covered` signal the slot does.
- Every backgrounded tab pinned a webContents, since presence was leased on
  mount rather than on being the foreground tab, so the machine never left
  Observed. Gated on isActiveTab like the session browser's lease. The open
  effect is gated too, or a reaped background tab would re-create the guest and
  feed the reaper every grace period.

Separately, the preview's overflow menu had no window-blur dismiss, so clicking
into the guest left it stuck open over the page. The browser panel had already
solved this; the logic moves to a shared useGuestMenuState.

The new toolbar's icon-only buttons carried tooltips but no accessible name,
which icon-button-accessible-name.test.ts flags: a tooltip is portalled and
never reaches assistive clients or the scripts that drive Studio by name.
@mutewinter
mutewinter force-pushed the worktree-html-artifacts-guest-pool branch from 7cda8ab to e7c50a6 Compare August 4, 2026 13:03
devin-ai-integration[bot]

This comment was marked as resolved.

Both come from the same place as the last round: a single guest with more than
one thing acting on it, where the timing of who acts first was assumed rather
than established.

Switching to a different HTML file showed the previous file's page. The
mounted-host count that decides whether to adopt a live page or navigate is read
while rendering the incoming host, and React runs every cleanup after that, so
the outgoing host was still counted and the new file looked like a second viewer
of the old one. Keying the count by file as well as target makes a different
file a different key, which is the only pair that should ever share a page.

A lease arriving while the guest was being closed was dropped, because Stopping
handled no presence events. The machine reached Stopped, the parent forgot it,
and nothing re-leased -- so the panel's open effect rebuilt a guest that was
reaped again every grace period for as long as the user kept looking at it.
Stopping now keeps counting and returns to Observed if someone is still watching
once the close settles. A forced reap is exempt: trashing a task is final, and a
lease landing in the same tick must not revive a preview for a task on its way
out.
devin-ai-integration[bot]

This comment was marked as resolved.

Being raised above the expand modal's overlay was treated as immunity from
every overlay, so opening Settings over an expanded HTML file left the page
painted on top of the dialog. The two are independent slots -- the file viewer
runs off taskFileViewerAtom and settings off studioModalAtom -- so a menu
accelerator reaches one straight through the other, and an app-wide dialog
draws at z-50 while the raised guest sits at 51.

Split the overlay signal into its two sources so a host can ignore the one it
lives inside and still park for the one above it.
devin-ai-integration[bot]

This comment was marked as resolved.

The profile was one shared directory, on the reasoning that the distinct asset
origins already isolate tasks from each other. That holds for localStorage and
IndexedDB, which are keyed by origin, but cookies are scoped by domain: a page
served from assets.<a>.localhost can set one for localhost, and every preview
sharing a jar would then send and read it. Both ends are untrusted
agent-authored HTML, so that is a channel between two tasks -- and a new one,
since the opaque-origin iframe had no storage at all.

A directory per task closes it. It stays beside the workspace's other private
state rather than moving into the task folder, so a Chromium profile never lands
somewhere the user browses or exports; trash-task removes it with the task,
best-effort, after the guest that owned it is already reaped.

The docs asserted the origin-only reasoning, so they are corrected rather than
just updated.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +72 to +83
// The artifact preview's storage profile lives outside the task folder
// (a Chromium profile has no business in a directory the user browses),
// so trashing the task does not take it. Best-effort: a profile left
// behind is dead weight, not a correctness problem, and it must not
// fail the deletion the user asked for. The guest that owned it is
// already reaped by the time we get here.
const previewProfile = getArtifactPreviewSessionDir(taskId);
if (await pathExists(previewProfile)) {
await rmrf(previewProfile).catch(() => {
// Nothing to do: the task itself is gone.
});
}

@devin-ai-integration devin-ai-integration Bot Aug 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Deleting a task can leave its preview's browser data behind on disk

The preview's stored browser data is deleted (rmrf(previewProfile) at packages/workspace/src/lib/trash-task.ts:80) without ever waiting for the preview window that owns it to be shut down, so the deletion can fail and leave the data behind forever.
Impact: Deleting a task can silently leave a folder of that task's preview browser data on disk, which nothing ever cleans up.

Nothing awaits artifactPreview.stopped, unlike the taskBrowser reap

trashTask blocks on browserReaped (packages/workspace/src/lib/trash-task.ts:35-44), which is resolved only by taskBrowser.stopped (packages/workspace/src/machines/workspace/index.ts handleTaskBrowserStopped). The artifact preview is sent forceReap in prepareToTrashTask but is deliberately excluded from matchingTaskIds, so no resolver waits on artifactPreview.stopped. The reap is asynchronous (Stopping invokes closeTargetLogic, then the guest's destroyed/detach chain runs), so by the time rmrf executes the Chromium session at artifact-preview-session/<taskId> may still be open. On Windows in particular an open profile holds file locks and fs.rm throws; the .catch(() => {}) swallows it and nothing retries. The inline comment "The guest that owned it is already reaped by the time we get here" is therefore not guaranteed by the code.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread apps/studio/src/client/components/task/browser-panel.tsx Outdated
Comment on lines +114 to +120
// Mirror the guest's URL into the address bar. Held in local state (rather
// than read straight off the hook) because the user types into the same box.
useEffect(() => {
if (!editingUrlRef.current) {
setDraftUrl(guest.url);
}
}, [guest.url]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Address bar can keep half-typed text after the page reloads to the same address

The address bar is only refreshed when the page's address changes value (useEffect(..., [guest.url]) at apps/studio/src/client/components/task/browser-panel.tsx:116-120), so a reload or a navigation back to the identical address leaves whatever the user half-typed sitting in the bar.
Impact: After typing in the address bar without pressing Enter and then reloading, the bar keeps showing the abandoned text instead of the page's real address.

Event-driven sync replaced by a value-diffed effect

The previous implementation called sync() directly from every did-navigate / did-navigate-in-page handler, which unconditionally wrote setDraftUrl(webview.getURL()). The refactor moves the guest URL into useGuestNavigation's url state (apps/studio/src/client/hooks/use-guest-navigation.ts:69-71) and mirrors it via an effect keyed on guest.url. When a navigation resolves to the same URL, setUrl(next) is a no-op for React, guest.url never changes identity, and the mirroring effect does not re-run — so draftUrl, which the user mutated via onChange, is never restored. Focus/blur alone does not reset it either (onBlur only clears editingUrlRef).

Prompt for agents
apps/studio/src/client/components/task/browser-panel.tsx now mirrors the guest URL into the address bar with an effect keyed on `guest.url`. Because useGuestNavigation stores the URL in state, a navigation that resolves to the identical URL (a reload, a re-submit of the same address) does not change that state, so the effect never re-runs and any text the user typed but never submitted stays in the box. The old code re-synced on every did-navigate event regardless of whether the URL changed. Consider exposing a navigation counter/epoch from useGuestNavigation (bumped on every did-navigate/did-fail-load) and keying the mirror effect on that as well, or having the panel reset draftUrl on blur.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread packages/workspace/src/constants.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
packages/workspace/src/machines/workspace/index.ts (1)

716-736: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Fence artifact opens during task disposal.

createArtifactTarget publishes a manager entry before its attach promise resolves (apps/studio/src/electron-main/browser-view/manager.ts:394-403), but artifactPreview.open registers it only after that await (packages/workspace/src/rpc/routes/artifact-preview.ts:31-44). prepareToTrashTask only reaps actor refs already in artifactPreviewRefs (packages/workspace/src/machines/workspace/index.ts:716-736). A pending open can therefore register a new actor after disposal starts, while trashTask removes the task directory and profile (packages/workspace/src/lib/trash-task.ts:70-83).

  • packages/workspace/src/machines/workspace/index.ts#L716-L736: Track in-flight artifact operations and wait for artifact shutdown before cleanup proceeds.
  • packages/workspace/src/rpc/routes/artifact-preview.ts#L31-L44: Coordinate target creation with task disposal and close a target when disposal wins the race.
  • packages/workspace/src/machines/workspace/index.ts#L267-L291: Reject late registration for a disposing or deleted task instead of spawning a new actor.
  • packages/workspace/src/lib/trash-task.ts#L72-L83: Remove the profile only after the artifact disposal barrier resolves.

Add a regression test that starts an open, begins disposal before attach settles, then settles the open and verifies that no target or profile remains.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/workspace/src/machines/workspace/index.ts` around lines 716 - 736,
Fence artifact opens during task disposal: in
packages/workspace/src/machines/workspace/index.ts:716-736, track in-flight
artifact operations and wait for their shutdown before cleanup; in
packages/workspace/src/rpc/routes/artifact-preview.ts:31-44, coordinate target
creation with disposal and close the target if disposal wins; in
packages/workspace/src/machines/workspace/index.ts:267-291, reject late
registration for disposing or deleted tasks; in
packages/workspace/src/lib/trash-task.ts:72-83, remove the profile only after
the artifact disposal barrier resolves. Add a regression test covering an open
whose attach settles after disposal begins, verifying no target or profile
remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/studio/src/client/components/html-artifact-preview.tsx`:
- Around line 136-156: Update the error-rendering branch alongside the artifact
preview mutation to add an in-place retry control that invokes openPreview with
the current taskId. Preserve the existing error message and use the mutation’s
openFailed state to show the control only when target creation fails.
- Around line 360-366: Update the retry callback in the guest.loadError
rendering to call guest.navigateTo with the failed URL from guest.loadError,
falling back to entryUrl when that URL is unavailable. Preserve the existing
GuestLoadErrorNotice wiring and retry behavior.
- Around line 203-219: When the modal closes (active transitions from true to
false), the guest has navigated to a different file but the panel state remains
unchanged, causing a desynchronization. Add restoration logic in the useEffect
to sync the panel file state or restore the guest navigation back to entryUrl
when active becomes false. This could be done by navigating the guest back to
entryUrl before the early return when !active, or by syncing
artifactPanel.filePath and artifactPanel.modifiedAt to reflect the panel's
original file context instead of the modal's file.

In `@apps/studio/src/client/components/task/browser-panel.tsx`:
- Around line 103-112: Read useIsGuestCovered() once into a shared covered value
in the browser panel, then pass that value to both useBrowserFind and
useBrowserSlot. Update the useBrowserFind invocation so its coverage dependency
changes alongside the slot, allowing the shared opener to be restored after
overlay unmounts.

In `@apps/studio/src/client/components/task/file-viewer-modal.tsx`:
- Around line 22-26: Raise the carousel controls rendered by the file viewer
modal above GUEST_Z_INDEX so they remain clickable when the body-mounted HTML
guest is visible. Update the controls near DialogPrimitive.Content and its
previous/next buttons to use a body-level or guest-aware layer above 51, while
preserving their existing positioning and behavior.

In `@apps/studio/src/client/hooks/use-guest-navigation.ts`:
- Around line 43-49: Update the state and rebind logic in useGuestNavigation so
loadError is scoped to the current targetId rather than surviving across
sessions. Clear the error when targetId changes and when the target becomes
inactive, ensuring useBrowserSlot and TaskBrowserPanel cannot reuse the previous
session’s parked state or URL. Add a regression test covering a failed session
followed by switching to a new session.

In `@packages/workspace/src/machines/artifact-preview.ts`:
- Around line 178-205: The `Stopping` state at
packages/workspace/src/machines/artifact-preview.ts line 178-205 does not handle
registerTarget events in its on: object (currently showing only acquirePresence
and releasePresence), causing incoming target registrations to be silently
dropped during teardown. Add registerTarget handling to the on: object in the
Stopping state to ensure targets registered during teardown are either closed or
transferred before the machine reaches Stopped. Apply the same registerTarget
event handling to the grace-period teardown path at
packages/workspace/src/machines/artifact-preview.ts line 124-145 to preserve
consistency. Finally, add a regression test at
packages/workspace/src/machines/artifact-preview.test.ts line 216-280 that
transitions to Stopping, sends a registerTarget event before closeTargetLogic
settles, and asserts that the new target is closed or rejected rather than left
dangling.

---

Outside diff comments:
In `@packages/workspace/src/machines/workspace/index.ts`:
- Around line 716-736: Fence artifact opens during task disposal: in
packages/workspace/src/machines/workspace/index.ts:716-736, track in-flight
artifact operations and wait for their shutdown before cleanup; in
packages/workspace/src/rpc/routes/artifact-preview.ts:31-44, coordinate target
creation with disposal and close the target if disposal wins; in
packages/workspace/src/machines/workspace/index.ts:267-291, reject late
registration for disposing or deleted tasks; in
packages/workspace/src/lib/trash-task.ts:72-83, remove the profile only after
the artifact disposal barrier resolves. Add a regression test covering an open
whose attach settles after disposal begins, verifying no target or profile
remains.
🪄 Autofix

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 38e7d042-7390-473c-b084-4330cb6e8b1d

📥 Commits

Reviewing files that changed from the base of the PR and between 14fc30a and 6d91231.

📒 Files selected for processing (36)
  • apps/studio/src/client/atoms/studio-modal.ts
  • apps/studio/src/client/components/file-viewer.tsx
  • apps/studio/src/client/components/html-artifact-preview.tsx
  • apps/studio/src/client/components/sandboxed-html-iframe.tsx
  • apps/studio/src/client/components/task/browser-panel.tsx
  • apps/studio/src/client/components/task/file-viewer-modal.tsx
  • apps/studio/src/client/components/task/guest-load-error.tsx
  • apps/studio/src/client/components/task/view.tsx
  • apps/studio/src/client/hooks/use-browser-find.ts
  • apps/studio/src/client/hooks/use-browser-slot.ts
  • apps/studio/src/client/hooks/use-guest-covered.ts
  • apps/studio/src/client/hooks/use-guest-menu-state.ts
  • apps/studio/src/client/hooks/use-guest-navigation.ts
  • apps/studio/src/client/lib/browser-pool.ts
  • apps/studio/src/electron-main/browser-view/debug-snapshot.ts
  • apps/studio/src/electron-main/browser-view/entry.ts
  • apps/studio/src/electron-main/browser-view/manager.ts
  • docs/architecture/in-app-browser.md
  • docs/findings/html-artifact-iframe-navigation.md
  • docs/plans/active/html-artifacts-in-the-guest-pool.md
  • packages/workspace/src/client.ts
  • packages/workspace/src/constants.ts
  • packages/workspace/src/electron.ts
  • packages/workspace/src/lib/task-dir-utils.ts
  • packages/workspace/src/lib/trash-task.ts
  • packages/workspace/src/logic/server/routes/cdp-bridge.ts
  • packages/workspace/src/machines/artifact-preview.test.ts
  • packages/workspace/src/machines/artifact-preview.ts
  • packages/workspace/src/machines/task-browser.test.ts
  • packages/workspace/src/machines/workspace/index.ts
  • packages/workspace/src/machines/workspace/types.ts
  • packages/workspace/src/rpc/index.ts
  • packages/workspace/src/rpc/routes/artifact-preview.ts
  • packages/workspace/src/test/helpers/mock-task-config.ts
  • packages/workspace/src/test/setup.ts
  • packages/workspace/src/types.ts
💤 Files with no reviewable changes (1)
  • apps/studio/src/client/components/sandboxed-html-iframe.tsx

Comment thread apps/studio/src/client/components/html-artifact-preview.tsx
Comment on lines +203 to +219
useEffect(() => {
if (!active) {
return;
}
const isAdoptingLivePage =
!hasNavigatedRef.current &&
isSecondaryHost &&
Boolean(guest.currentUrl());
hasNavigatedRef.current = true;
if (!isAdoptingLivePage) {
guest.navigateTo(entryUrl);
}
// `guest` is rebuilt every render; navigating is keyed on the file, on the
// guest becoming available, and on the go-home gesture, not on that
// identity.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, entryUrl, goHomeNonce, isSecondaryHost, targetId]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the state writer and both viewer call paths before inspecting behavior.
ast-grep outline apps/studio/src/client/atoms/task-file-viewer.ts --items all
ast-grep outline apps/studio/src/client/components/task/view.tsx --match TaskView --view expanded
ast-grep outline apps/studio/src/client/components/task/file-viewer-modal.tsx --match TaskFileViewerModal --view expanded

# Expect either route synchronization or explicit guest restoration on modal close.
rg -n -C 8 '\b(setTaskFileViewerIndexAtom|openFileViewerAtom|artifactPanel|FileViewer)\b' \
  apps/studio/src/client/atoms/task-file-viewer.ts \
  apps/studio/src/client/components/task/view.tsx \
  apps/studio/src/client/components/task/file-viewer-modal.tsx \
  apps/studio/src/client/components/html-artifact-preview.tsx

Repository: instrument-org/instrument

Length of output: 20664


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect modal navigation/closing behavior and the panel route derivation around target isolation.
sed -n '40,90p;150,195p' apps/studio/src/client/components/task/file-viewer-modal.tsx
sed -n '80,230p' apps/studio/src/client/components/task/view.tsx
rg -n -C 5 '(artifactPanel:|searchState|artifactPanel|currentFile|handleFileSelect|FileViewer|setArtifactHomeNonce|openFileViewerAtom|setTaskFileViewerIndexAtom|currentFile)' \
  apps/studio/src/client/components/task/file-viewer-modal.tsx \
  apps/studio/src/client/components/task/view.tsx \
  apps/studio/src/client/atoms/task-file-viewer.ts

Repository: instrument-org/instrument

Length of output: 33333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect HTML artifact preview host behavior and FileViewer ownership paths.
sed -n '1,240p' apps/studio/src/client/components/html-artifact-preview.tsx
sed -n '340,390p' apps/studio/src/client/components/html-artifact-preview.tsx

rg -n -C 4 '\b(guest\.navigateTo|isSecondaryHost|currentUrl|onCurrentUrl|openFileViewer|closeFileViewer|onExpand|onClose)\b' \
  apps/studio/src/client/components/html-artifact-preview.tsx \
  apps/studio/src/client/components/file-viewer.tsx \
  apps/studio/src/client/components/task/view.tsx

Repository: instrument-org/instrument

Length of output: 29837


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the useGuestNavigation source and related guest leasing/overlay cleanup to resolve the ownership handoff.
git ls-files | rg 'use-guest-navigation|browser-target|guest|slots|workspace/artifactPreview'
rg -n -C 5 'function useGuestNavigation|useEffect\(\\(\\) => \\{(?!\\n(?:\\s*\\/\\*|\\/\\/|return|active))\\s*\\[[^\\]]*guest.*useGuestNavigation' \
  apps/studio/src/client/hooks \
  apps/studio/src/client/components \
  apps/studio/src/client/atoms \
  apps/studio/src/client/rpc \
  apps/backend src

Repository: instrument-org/instrument

Length of output: 889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect guest navigation state and slot cleanup behavior.
sed -n '1,240p' apps/studio/src/client/hooks/use-guest-navigation.ts
sed -n '1,220p' apps/studio/src/client/hooks/use-browser-slot.ts
sed -n '1,220p' apps/studio/src/client/hooks/use-guest-covered.ts

# Search for guest ownership/cleanup paths that may clear the modal file host.
rg -n -C 5 'active=|targetId|useBrowserSlot|useGuestNavigation|navigateTo\(|currentUrl\(\)\?|isSecondaryHost|fileViewerModalOpen|closeFileViewerAtom|mountedHosts' \
  apps/studio/src/client/hooks \
  apps/studio/src/client/components/html-artifact-preview.tsx \
  apps/studio/src/client/components/task/view.tsx \
  apps/studio/src/client/atoms/task-file-viewer.ts

Repository: instrument-org/instrument

Length of output: 50381


Restore the panel’s file URL after modal file navigation.

setTaskFileViewerIndexAtom only changes modal state and onBeforeLoad closes the modal, so A → modal B → close can leave the task route still pointing at A while the shared guest has navigated to B. Sync artifactPanel.filePath/artifactPanel.modifiedAt when modal navigation changes files, or restore the panel host to its entry URL when the modal closes. Add a regression for A → modal B → close.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/studio/src/client/components/html-artifact-preview.tsx` around lines 203
- 219, When the modal closes (active transitions from true to false), the guest
has navigated to a different file but the panel state remains unchanged, causing
a desynchronization. Add restoration logic in the useEffect to sync the panel
file state or restore the guest navigation back to entryUrl when active becomes
false. This could be done by navigating the guest back to entryUrl before the
early return when !active, or by syncing artifactPanel.filePath and
artifactPanel.modifiedAt to reflect the panel's original file context instead of
the modal's file.

Comment thread apps/studio/src/client/components/html-artifact-preview.tsx
Comment thread apps/studio/src/client/components/task/browser-panel.tsx Outdated
Comment thread apps/studio/src/client/components/task/file-viewer-modal.tsx
Comment thread apps/studio/src/client/hooks/use-guest-navigation.ts Outdated
Comment thread packages/workspace/src/machines/artifact-preview.ts
devin-ai-integration[bot]

This comment was marked as resolved.

Two things an agent-written report can ordinarily do worked on the sandboxed
iframe by inheriting the host page's behavior, and the guest's stricter defaults
silently took both away.

A `target="_blank"` link used to bubble to the main window's window-open handler,
which sends it to the OS browser. The artifact guest denied every open, so such
links became inert -- and with no address bar, the user had no other way to
follow one. It still opens no child window of its own, but hands http(s) opens to
openExternal. The decision is a predicate so the policy module stays free of
Electron runtime imports and testable; the manager does the opening, and loads
open-external on demand because its exception capture reaches the preferences and
app-state stores, which breaks node tests of unrelated modules that transitively
import the manager.

Downloads were cancelled outright: the guest session refuses anything without an
agent-authorized save path, which is right for an agent-driven guest and wrong
for a preview. The iframe carried `allow-downloads`, so a "Download CSV" button
in a report used to produce a save dialog. An artifact guest now leaves the item
to Electron, which prompts.
…guest

The browser panel had the same find bug the preview did: it parks its guest
under an overlay but kept claiming the single Cmd+F opener slot, so the overlay's
host took it, cleared it on unmount, and the panel never re-registered. Both
hooks now read one coverage value.

A load failure was stored without the guest it happened on. The browser panel
swaps targetId in place when the session changes, with no remount, so the error
survived into the next guest -- parking it behind a notice naming the previous
session's URL. Stamped and filtered on read, so no failed page is briefly shown
as fine on the way through.

A target registered while teardown was already running was dropped. The close
captured the id it was given on entry, so that guest was one nothing would ever
close. Recorded and closed on a second pass, or kept if a lease means it is now
the live one.

The panel and the modal can also disagree about which file is on screen: the
carousel moves the shared guest while the panel behind still owns the route
naming a different one. A host becoming visible again re-asserts its own file,
while leaving a sub-page inside that file alone -- which is what keeps following
a link and collapsing the modal from resetting the page.

Carousel arrows cannot be lifted over the guest, since their z-50 content is a
stacking context the body-mounted guest sits outside. The guest is inset past
them instead, and only when they exist.

Also: "Try again" on a failed load retries that URL rather than the artifact's
entry page, and a failed open now offers a retry, since the open effect fires
once per state by design and will not retry itself.
A guest that goes away takes the page with it and its replacement arrives on
about:blank, but the host still believed the file was already in place -- so
backgrounding a tab past the grace period, or a crash in untrusted artifact
HTML, left a blank preview with no error and no way back except Home. The host
now forgets what was pointed where when its guest goes, and treats the next one
as somewhere to navigate. Verified by crashing a guest under a mounted host.

The preview's own menu and tooltips portal to the body at z-50, which a raised
guest sits above, so inside the expand modal they opened underneath the page
they belong to. They cannot be lifted from within their host -- that content is
a stacking context the guest is not in -- so they are raised alongside it.

Coming forward in a second tab did not re-assert that tab's file either, so two
tabs on one task could show a file the route did not name.

The destruction watcher this machine's `targetDestroyedExternally` handler
depends on was never wired, so the handler and its test described something
production could not reach. Wired the way taskBrowser does it, and the test now
fires it through the subscription rather than by hand.

Also: the chat's image/diagram preview is a third full-window dialog a guest can
be showing behind, so it joins the ones a host parks for; and trash-task's
comment claimed the preview guest was already reaped when it gets there, which
is not something anything waits for.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
apps/studio/src/electron-main/browser-view/manager.ts (1)

400-407: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wait for the attachment handshake before reuse.

bindGuest sets entry.webContents before it resolves entry.attach at apps/studio/src/electron-main/browser-view/manager.ts:153-154 and apps/studio/src/electron-main/browser-view/manager.ts:279-285. A concurrent createTarget call returns at Line 404 before the guest has a RenderFrame and debugger setup. A caller can then send CDP commands during the state that Lines 275-282 identify as able to hang.

Always await waitForAttach(existing) for an existing entry. Add a regression test that issues two creates after did-attach-webview and before did-finish-load.

Proposed fix
     const existing = entries.get(targetId);
     if (existing) {
-      // Idempotent: a target id owns at most one guest.
-      // Already bound -> reuse it; mount still in flight -> wait on it.
-      if (existing.webContents && !existing.webContents.isDestroyed()) {
-        return Promise.resolve({ targetId });
-      }
       return waitForAttach(existing).then(() => ({ targetId }));
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/studio/src/electron-main/browser-view/manager.ts` around lines 400 -
407, Update the existing-entry branch in bindGuest so it always awaits
waitForAttach(existing) before returning, including when existing.webContents is
already set and alive; remove the immediate Promise.resolve reuse path. Add a
regression test covering two create requests issued after did-attach-webview but
before did-finish-load, verifying both wait for attachment completion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/studio/src/client/components/task/file-viewer-modal.tsx`:
- Around line 194-199: Update FileViewer and its header menu flow to accept and
forward a z-index value of GUEST_Z_INDEX + 1, reusing the existing floatingStyle
pattern. Apply that value to both the main DropdownMenuContent and nested
submenu content so menu rows render above the raised guest while preserving
existing behavior otherwise.

In `@apps/studio/src/electron-main/browser-view/window-open-policy.test.ts`:
- Line 100: Remove the duplicated closing `satisfies { name: string; overrides:
Partial<HandlerDetails> }[]` expressions at the ends of both test-case
definitions in the `it.each` blocks. Keep one valid closing expression per block
so the test file parses correctly.

In `@packages/workspace/src/machines/artifact-preview.ts`:
- Around line 61-63: Include the destroyed targetId when sending
targetDestroyedExternally from the onTargetDestroyed callback, then update the
corresponding handler around the targetDestroyedExternally transition to clear
context.targetId and stop only when the event ID matches the current target. Add
a regression test covering target B being registered during target A teardown,
A’s destruction callback firing, and B remaining adopted when presence exists.

---

Outside diff comments:
In `@apps/studio/src/electron-main/browser-view/manager.ts`:
- Around line 400-407: Update the existing-entry branch in bindGuest so it
always awaits waitForAttach(existing) before returning, including when
existing.webContents is already set and alive; remove the immediate
Promise.resolve reuse path. Add a regression test covering two create requests
issued after did-attach-webview but before did-finish-load, verifying both wait
for attachment completion.
🪄 Autofix

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

Review profile: CHILL

Plan: Pro Plus

Run ID: d28a928e-3de0-4e12-afda-31fa2e83c68c

📥 Commits

Reviewing files that changed from the base of the PR and between 6d91231 and 24d4fb5.

📒 Files selected for processing (17)
  • apps/studio/src/client/components/file-viewer.tsx
  • apps/studio/src/client/components/html-artifact-preview.tsx
  • apps/studio/src/client/components/task/browser-panel.tsx
  • apps/studio/src/client/components/task/file-viewer-modal.tsx
  • apps/studio/src/client/hooks/use-guest-covered.ts
  • apps/studio/src/client/hooks/use-guest-navigation.ts
  • apps/studio/src/electron-main/browser-view/downloads.ts
  • apps/studio/src/electron-main/browser-view/manager.ts
  • apps/studio/src/electron-main/browser-view/window-open-policy.test.ts
  • apps/studio/src/electron-main/browser-view/window-open-policy.ts
  • docs/architecture/in-app-browser.md
  • docs/findings/html-artifact-iframe-navigation.md
  • docs/plans/active/html-artifacts-in-the-guest-pool.md
  • packages/workspace/src/constants.ts
  • packages/workspace/src/lib/trash-task.ts
  • packages/workspace/src/machines/artifact-preview.test.ts
  • packages/workspace/src/machines/artifact-preview.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/workspace/src/constants.ts
  • docs/architecture/in-app-browser.md
  • docs/findings/html-artifact-iframe-navigation.md
  • apps/studio/src/client/components/task/browser-panel.tsx
  • apps/studio/src/client/components/file-viewer.tsx
  • packages/workspace/src/machines/artifact-preview.test.ts
  • apps/studio/src/client/hooks/use-guest-navigation.ts
  • apps/studio/src/client/hooks/use-guest-covered.ts
  • docs/plans/active/html-artifacts-in-the-guest-pool.md
  • packages/workspace/src/lib/trash-task.ts

Comment on lines +194 to +199
<FileViewer
file={currentFile}
guestSideGutter={hasMultipleFiles ? GUEST_SIDE_GUTTER : 0}
guestZIndex={GUEST_Z_INDEX}
onClose={collapseViewer}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Raise the FileViewer header menu above the guest.

apps/studio/src/client/components/html-artifact-preview.tsx:275-281 establishes that a raised guest renders above normal z-50 portals. Line 197 enables that mode, but the FileViewer header menu at apps/studio/src/client/components/file-viewer.tsx:466-706 has no elevated style. Its shared primitive uses z-50 at apps/studio/src/client/components/ui/dropdown-menu.tsx:40-64.

When menu rows overlap the HTML guest, the guest receives the clicks. Forward GUEST_Z_INDEX + 1 through FileViewer to its DropdownMenuContent and nested submenu content. Reuse the existing floatingStyle pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/studio/src/client/components/task/file-viewer-modal.tsx` around lines
194 - 199, Update FileViewer and its header menu flow to accept and forward a
z-index value of GUEST_Z_INDEX + 1, reusing the existing floatingStyle pattern.
Apply that value to both the main DropdownMenuContent and nested submenu content
so menu rows render above the raised guest while preserving existing behavior
otherwise.

{ name: "http new tab", overrides: { url: "http://example.test/docs" } },
{ name: "background tab", overrides: { disposition: "background-tab" } },
{ name: "new-window popup", overrides: { disposition: "new-window" } },
] satisfies { name: string; overrides: Partial<HandlerDetails> }[])(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicated satisfies clauses.

Line 100 repeats the closing expression from Line 99. Line 111 repeats the closing expression from Line 110. Each second expression is outside the it.each call and makes apps/studio/src/electron-main/browser-view/window-open-policy.test.ts fail to parse.

Also applies to: 111-111

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/studio/src/electron-main/browser-view/window-open-policy.test.ts` at
line 100, Remove the duplicated closing `satisfies { name: string; overrides:
Partial<HandlerDetails> }[]` expressions at the ends of both test-case
definitions in the `it.each` blocks. Keep one valid closing expression per block
so the test file parses correctly.

Comment on lines +61 to +63
input.browser.onTargetDestroyed(input.targetId, () => {
sendBack({ type: "targetDestroyedExternally" });
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Ignore destruction events from non-current targets.

packages/workspace/src/machines/artifact-preview.ts:61-63 sends targetDestroyedExternally without the watched targetId. packages/workspace/src/machines/artifact-preview.ts:175-178 then clears context.targetId for every such event.

If target A is closing and late registration installs target B, the destruction callback for A can clear B. The next Stopping pass receives null, so it does not close or retain B. This can leave a blank observed preview or orphan the guest.

Include the destroyed targetId in the event. Only clear and stop when it equals context.targetId. Add a regression test that registers B during A’s teardown, fires A’s destruction callback, and verifies that B remains adopted when presence exists.

As per coding guidelines, “Flag deterministic tool, prompt, message-assembly, or state-management defects that make agent turns fail, hang, or silently drop state.”

Also applies to: 175-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/workspace/src/machines/artifact-preview.ts` around lines 61 - 63,
Include the destroyed targetId when sending targetDestroyedExternally from the
onTargetDestroyed callback, then update the corresponding handler around the
targetDestroyedExternally transition to clear context.targetId and stop only
when the event ID matches the current target. Add a regression test covering
target B being registered during target A teardown, A’s destruction callback
firing, and B remaining adopted when presence exists.

Source: Coding guidelines

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +50 to +59
export function shouldOpenArtifactLinkExternally(
details: HandlerDetails,
): boolean {
try {
const { protocol } = new URL(details.url);
return protocol === "http:" || protocol === "https:";
} catch {
return false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 Agent-authored HTML preview can silently launch the user's real browser via window.open

The artifact-preview guest hands every http(s) window-open request to the OS browser (shouldOpenArtifactLinkExternally at apps/studio/src/electron-main/browser-view/window-open-policy.ts:50-59, invoked from apps/studio/src/electron-main/browser-view/manager.ts:170-183), and the check ignores details.disposition entirely — the added tests assert new-window popups are handed off too. Agent-authored HTML is untrusted content; a script in a previewed report can call window.open("https://attacker.example/...") repeatedly with no user gesture requirement enforced here and open arbitrary pages, with arbitrary URL-encoded data, in the user's default browser (where their real cookies live).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@mutewinter

Copy link
Copy Markdown
Contributor Author

Superseded by #97, which does the same thing in ~1,100 lines instead of ~2,600.

Two decisions here made this bigger than the change needed to be, and #97 drops both:

  • The lifetime machine. A presence lease plus a grace period cost an XState machine, a 391-line test, and a family of teardown races. Render HTML artifacts in the guest pool #97 derives the target id from the task id, so trash-task closes it with no registry, and nothing else reaps it.
  • Expand-modal support. That created two hosts sharing one guest, which produced the mountedHosts/lastPointedAt coordination, the z-index and gutter work, and roughly five of the thirteen findings on this PR. Render HTML artifacts in the guest pool #97 withholds Expand for HTML instead; the panel already gives an artifact the full pane.

The three pre-existing browser-panel fixes found here (guests painting through app-wide modals, Cmd+F dying after an overlay closes, load errors surviving a session switch) carry over to #97.

Closing rather than merging. The analysis is preserved in docs/findings/html-artifact-iframe-navigation.md on #97, including the cookie/domain isolation reasoning and the iframe-vs-guest permission ledger.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant