Skip to content

feat(app): add a session media panel behind the session_media flag - #62217

Open
neriousy wants to merge 6 commits into
devfrom
session-media
Open

neriousy wants to merge 6 commits into
devfrom
session-media

Conversation

@neriousy

@neriousy neriousy commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a Media tab to the session side panel that lists everything shared in a session, by you or the agent, so you can find a screenshot, file or link without scrolling the conversation.

  • Media: images and videos you attached or pasted, images the agent read or wrote by path, and images tools returned, such as browser screenshots. Shown as gallery cards grouped by day.
  • Gallery viewer: clicking an image opens the existing zoomable viewer as a gallery over the tab's images. Previous/next buttons and a "3 / 12" counter sit in its control bar, and ← / → step while the image isn't zoomed in (mirrored in right-to-left layouts). Images whose files are gone stay as "Couldn't load" cards but are skipped by the viewer.
  • Files: your attachments and @-mentioned files, plus files the agent created (the write tool and files a patch adds) and files tools returned. Clicking opens the file in the app.
  • Links: web addresses in your messages and the agent's replies, pages the agent fetched, and web search results. Each page is listed once. Clicking opens the in-app browser, or the system browser when that isn't available.
  • Filters: Media / Files / Links with counts, and an Everyone / You / Agent dropdown for who shared it.
  • Loading: thumbnails load as they scroll into view. A "Load earlier messages" button pages in older history, since the app only keeps recent messages.

Implementation

  • A built-in GUI plugin in packages/gui-plugins/src/media/, registered in src/renderer.ts and src/main.ts. All extraction is in collect.ts, which has unit tests.
  • ImagePreview (packages/ui) gains two optional props, position and onStep, for stepping through a set. It now remembers which origin it hid, so stepping to another image shows the thumbnail it left. Existing callers pass neither prop and behave as before.
  • session-ui exports its existing site icon for the link rows.

Feature flag

Guarded by the new session_media flag in packages/schema/src/config/flag.ts. It is on in local and dev builds and off in releases. When the flag is off, the tab closes and the + menu item and "Open media" command disappear.

The app reads the flag from the session directory's config the same way Core resolves it: experimental.session_media, then experimental.all_flags, then the build channel. It can't see OPENCODE_FLAGS, which only the server reads.

Not included

  • Clicking an item can't jump to the message it came from yet, because the plugin SDK has no way to scroll the conversation to a message.
  • The panel is hidden on narrow screens.

Testing

  • bun run check passes, plus bun test src/media in packages/gui-plugins and the image tests in packages/ui.
  • The plugin host and built-in graph component tests pass (plugin-graph, plugin-host).
  • Tried in the web app against a live server on a real session, in light and dark themes: the grid, file and link lists, the sender filter, and the gallery viewer (buttons, arrow keys, counter, the first/last limits, Esc).

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" color="currentColor">
  <style>
    .t{animation:in 3.2s ease-out infinite both}
    .t2{animation-delay:.25s}.t3{animation-delay:.5s}.t4{animation-delay:.75s}
    @keyframes in{0%{opacity:0}15%,85%{opacity:1}100%{opacity:1}}
    @media (prefers-reduced-motion:reduce){.t{animation:none}}
  </style>
  <rect x="8" y="10" width="92" height="52" rx="6" stroke="currentColor" stroke-opacity=".35"/>
  <rect x="14" y="16" width="56" height="4" rx="2" fill="currentColor" fill-opacity=".35"/>
  <rect x="14" y="26" width="72" height="4" rx="2" fill="currentColor" fill-opacity=".2"/>
  <rect x="14" y="36" width="64" height="4" rx="2" fill="currentColor" fill-opacity=".2"/>
  <rect x="44" y="46" width="48" height="10" rx="3" fill="currentColor" fill-opacity=".15"/>
  <rect x="108" y="10" width="124" height="52" rx="6" stroke="currentColor" stroke-opacity=".35"/>
  <rect x="114" y="15" width="44" height="7" rx="3" fill="currentColor" fill-opacity=".18"/>
  <rect x="200" y="15" width="26" height="7" rx="3" fill="currentColor" fill-opacity=".12"/>
  <g class="t"><rect x="114" y="27" width="25" height="19" rx="3" fill="currentColor" fill-opacity=".3"/><rect x="114" y="49" width="18" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
  <g class="t t2"><rect x="142" y="27" width="25" height="19" rx="3" fill="currentColor" fill-opacity=".22"/><rect x="142" y="49" width="14" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
  <g class="t t3"><rect x="170" y="27" width="25" height="19" rx="3" fill="currentColor" fill-opacity=".3"/><rect x="170" y="49" width="20" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
  <g class="t t4"><rect x="198" y="27" width="28" height="19" rx="3" fill="currentColor" fill-opacity=".22"/><rect x="198" y="49" width="16" height="3" rx="1.5" fill="currentColor" fill-opacity=".25"/></g>
</svg>

Added by the opencode bot (AI-generated).

Related: #58295

Public API: adds the experimental.session_media config flag and optional ImagePreview position/onStep props (backwards compatible)

@opencodejr opencodejr 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.

I couldn't run the panel: it only exists in the web/desktop app, and this environment has no browser (only the TUI can be driven here). Could you attach a couple of screenshots or a short video of the Media tab and the gallery viewer?

The new collect tests and the full gui-plugins suite pass, and so do the ui image tests. tsc is clean in gui-plugins, ui, schema and session-ui. From reading the code, the flag check mirrors Core's Flag.resolve order, and existing ImagePreview callers work as before. I found no blocking issues.

Public API, all backwards compatible:

  • Adds the optional boolean config key experimental.session_media (config schema, OpenAPI spec and generated client types).
  • Adds a new @opencode/session-ui/sources/site-icon export.
  • Adds two optional props, position and onStep, to ImagePreview.

One thing to keep in mind: enabled() in media/renderer.tsx is an app-side copy of Core's flag resolution and can't see OPENCODE_FLAGS. If more app features get flags, the server could expose resolved flags so the two can't drift.

Related: #58295

@opencodejr opencodejr 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.

The new commit only changes how the Files and Links lists look (rows grouped by day, a shared row layout, who-did-what labels and the item's folder or address), and I found nothing new to flag. bun test src/media and tsc -b in packages/gui-plugins both pass. I couldn't render the panel here because there's no browser in this environment. A screenshot of the new file and link rows would help reviewers.

The public API is the same as in the last review. This commit only touches the plugin's UI, CSS and English strings.

@opencodejr opencodejr 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.

The new commit replaces the "Load earlier messages" button with automatic loading as the panel scrolls. bun test src/media and tsc -b in packages/gui-plugins pass. I couldn't watch the scroll behaviour here because there's no browser in this environment.

One concern is worth fixing: when the open tab has few or no items, the automatic loading never stops until the session's whole history is in the app's message store (see the inline comment). The public API hasn't changed since the last review.


// Syncs with the server: one page at a time while the end is visible, so it stops once a page pushes the end off
// screen, the history ends, or a load fails.
createKeyed(() => end() && more() && !loading() && !failed() && `${props.session.id}:${settled()}`, page)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All three tabs read the same messages. If the open tab has few or no items (say Links in a session without links), the end marker never scrolls off screen, so this keeps calling loadMore until more() is false. In a long session, just opening the panel pulls the whole history into the app's shared message store, which works against the app keeping only recent messages and puts that history into the store the timeline also uses. Could automatic loading stop after a few pages, or only run when the user scrolls, with a button for the rest?

Separately, failed doesn't seem to reset when props.session changes. If so, after an error the next session won't load older messages until "Try again" is clicked.

@opencodejr opencodejr 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.

Reverting the automatic loading addresses my earlier concern: the panel only loads older messages when "Load older" is clicked, so opening it can't pull a session's whole history into memory, and the failed state that didn't reset between sessions is gone with it. bun test src/media and tsc -b in packages/gui-plugins pass. I couldn't render the panel here because there's no browser in this environment. Nothing new to raise, and the public API hasn't changed since the last review.

This branch has not been deployed

No deployments
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