Skip to content

feat(app): open agent-referenced files as rich artifact tabs - #49882

Merged
Hona merged 11 commits into
anomalyco:v2from
Hona:artifact-viewer
Sep 21, 2026
Merged

Hona merged 11 commits into
anomalyco:v2from
Hona:artifact-viewer

Conversation

@Hona

@Hona Hona commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Agents drop files everywhere: screenshots, recordings, reports, generated pages, CSV dumps. Referencing one in a reply used to be a dead end. file:// links were stripped by DOMPurify and blocked by Electron, relative links tried to navigate the renderer, and the file tab decoded every byte as text.

Now a reference in the timeline opens the file in the side panel, the tab renders whatever it is, and the agent can do the same on its own with browser.preview.

flowchart LR
  A["markdown link · `inline/path`"] --> R
  T["agent: browser.preview({ path })"] --> R
  R{inside workspace?}
  R -- no --> N["absolute path · read from its own folder"] --> D
  R -- yes --> D{kind}
  D -- html, local desktop<br/>with browser pane --> E["browser tab: file://"]
  D -- anything else --> F["file tab viewer + Preview / Source"]
Loading

Viewers

kind tab renders toolbar
image fit to pane on a checkerboard, click for 1:1 with scroll w × h · size
svg Preview (rendered) / Source w × h · size
audio player card duration · size
video fills the width, native controls w × h · duration · size
pdf Chromium PDF viewer size
html sandboxed <iframe>, Preview / Source, Open in browser on a local desktop server size
markdown rendered; relative links and images resolve against the file's folder size
mermaid .mmd rendered diagram through the timeline's fence pipeline size
csv / tsv sticky-header table, RFC 4180 quoting, first 1000 rows rows · columns · size
font ttf otf woff woff2 specimen at 12–64px via FontFace size
code / text unchanged —
other binary name + size, bytes are not retained size

Before / after

before after
image
video
pdf
html

Everything else

timeline links large image: fit
large image: click → 1:1 svg preview
svg source audio
markdown preview markdown source
csv table font specimen
code via link code via `inline/path`
mermaid binary
outside the workspace (C:/tmp/…)

Agent tool

// packages/plugin-browser/src/rpc.ts
operation("preview", "Show a file in the user's Review pane and focus its tab…", { path: short }, Schema.Struct({ path: short }))
server tool browser.preview ─rpc─▶ desktop browser-pane ─ipc─▶ renderer ArtifactOpener.open(path)

Discoverable through search(): the description carries the words agents use (show, display, view, artifact, media, screenshot, image, video, pdf, csv, font, …) and a test pins each phrase. browser.navigate / tabs.open with a file:// URL fail with a message that points here.

The desktop answers immediately and reports { type: "preview", path } on the pane attachment; the renderer resolves the path against the session workspace and applies the same rules as a clicked link. Older desktops answer unsupported through the existing version handshake.

How it fits together

  • packages/session-ui: the DOMPurify hook keeps local hrefs as data-local-link (same pattern as data-local-image), and Markdown routes clicks on those anchors and on `path` inline code to a new optional openLocalFile on MarkdownProvider.
  • packages/app/src/workspaces/files/artifact.ts: extension → kind/mime, fileContentFromBytes (media stays base64 with a mime type, NUL-sniffed unknown binaries keep only their size), resolveArtifactPath for ../, parseDelimited, contentBytes.
  • packages/app/src/session/files/open-artifact.tsx: ArtifactOpenerProvider resolves a link against the workspace root or the referencing file's directory, keeps files outside the root absolute so they still open in a tab, picks a browser tab or a file tab, and subscribes to browser.preview requests.
  • packages/app/src/session/files/artifact-view.tsx: the viewers and the toolbar.
  • packages/desktop/src/main/browser/policy.ts: the browser pane accepts file:// only when the server endpoint is loopback, since that agent already has the machine's disk. Popups and the agent-facing browser.navigate tool stay HTTP only.
resolveArtifactPath("docs/api", "../index.md") // "docs/index.md"
resolveArtifactPath("docs", "../../etc/passwd") // undefined → outside workspace

fileContentFromBytes("a.png", bytes)
// { type: "binary", content: "iVBOR…", encoding: "base64", mimeType: "image/png" }

Notes

  • The HTML preview is an opaque-origin blob document: no app storage, cookies, or credentialed requests reach it. Relative assets inside it do not resolve; on a local desktop server Open in browser loads the real file:// page instead.
  • The HTML→browser-tab path and browser.preview need the desktop app with the experimental browser pane; link clicks fall back to the in-app viewer everywhere else.

@Hona
Hona requested a review from Brendonovich as a code owner September 19, 2026 02:15
Copilot AI lite review requested due to automatic review settings September 19, 2026 02:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Hona
Hona enabled auto-merge (squash) September 21, 2026 02:34
@Hona
Hona disabled auto-merge September 21, 2026 04:53
@Hona
Hona enabled auto-merge (squash) September 21, 2026 04:53
@Hona
Hona disabled auto-merge September 21, 2026 04:53
@Hona
Hona enabled auto-merge (squash) September 21, 2026 04:53
@Hona
Hona disabled auto-merge September 21, 2026 04:53
Hona added 11 commits September 21, 2026 14:55
Markdown links and inline paths in the timeline open the referenced
workspace file in the side panel. The file tab now renders images,
SVG, audio, video, PDF, sandboxed HTML previews, and rendered Markdown
with a Preview/Source toggle instead of decoding every file as text.
On a local desktop server, HTML can open in the browser pane through
file://, which the pane now permits only for loopback endpoints.
…rowser.preview

Dedicated viewers replace the media fallback: images fit the pane with
click-to-1:1, video fills the width, audio gets a card, SVG and other
documents share the Preview/Source toggle, and the toolbar reports
dimensions, duration, and size. CSV/TSV render as tables, fonts as a
specimen, and Mermaid sources as diagrams.

The browser plugin gains browser.preview so an agent can open a file in
the user's Review pane and focus its tab; the desktop forwards it to the
renderer's artifact opener.
Absolute paths that are not under the workspace root stay absolute
through tab normalization and are read from their own directory, so a
link or browser.preview to /tmp or C:\\tmp output opens in the viewer
instead of handing off to the OS.
…iew toggle to its labels

optimizeDeps.exclude keeps the markdown worker's dependencies unbundled, but
Vite applies it to imports inside pre-bundled packages too, so mermaid's
chunk kept a bare import of marked that the browser could not resolve from
packages/app. A rolldown plugin resolves those ids for node_modules
importers so the pre-bundle carries its own copy.
- Confine browser-pane file:// URLs to the session workspace and only for
  the desktop's own sidecar; a forwarded loopback server no longer counts.
- Load before opening a tab so guessed inline paths that miss leave no tab.
- Strip :line[:col] suffixes; resolve links that climb past the root from
  the referencing folder, not the workspace root.
- Keep drive-root directories valid when reading absolute Windows paths.
- Cap media at 25 MB and fall back to the binary placeholder when the
  browser cannot decode an image, video, or audio file.
- Pad CSV headers to the widest row.
SessionFileView now owns only the code view and asks one question: is
this a text file. Everything else (kind dispatch, Preview/Source, decode
fallback, toolbar meta) lives in ArtifactView with a three-prop interface
and no knowledge of tabs, scroll sync, or comments. The sub-viewers are
module-private. The opener reuses createOpenSessionFileTab instead of a
copy, and the PDF frame opens with the thumbnail pane closed.
…markdown links

- Sub-frame file: navigations and file: subresources in the browser pane
  now honor the workspace roots; only HTTP sub-frames keep Chromium's
  default rules.
- A root-level markdown preview resolved relative links against '/'
  because getDirectory yields '/' for a bare filename.
- Drop the unused toast import, the single-use fileUrl export, and the
  repeated checksum; the parent passes its cacheKey down.
@Hona
Hona enabled auto-merge (squash) September 21, 2026 04:57
@Hona
Hona disabled auto-merge September 21, 2026 05:20
@Hona
Hona merged commit 60ed84e into anomalyco:v2 Sep 21, 2026
7 of 8 checks passed
jinhuang712 pushed a commit to jinhuang712/opencode that referenced this pull request Sep 26, 2026
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