Skip to content

Feat/notebook copy output image#12331

Merged
nstrayer merged 20 commits intomainfrom
feat/notebook-copy-output-image
Mar 16, 2026
Merged

Feat/notebook copy output image#12331
nstrayer merged 20 commits intomainfrom
feat/notebook-copy-output-image

Conversation

@nstrayer
Copy link
Copy Markdown
Contributor

@nstrayer nstrayer commented Mar 6, 2026

Addresses #10806.

Adds the ability to copy plot/image outputs from Positron notebook cells to the clipboard, matching feature parity with old notebooks and the plots pane.

Summary

  • New positronNotebookCellHasImageOutput context key tracks whether the active cell has image outputs
  • "Copy Image" action appears in the output ellipsis menu and right-click context menu when the cell has image output and output is not collapsed
  • Right-clicking a specific image in multi-image output copies that image (not the first)
  • Uses IClipboardService.writeImage() which routes through Electron's native clipboard API
  • New "Copy" action group in the output menu, shown above visibility actions

Demo

Showing copying multiple plots work by respecting which one is clicked on. Also demonstrates the dynamic copy plot menu for a cell output.

Screen.Recording.2026-03-13.at.3.26.22.PM.mov

Architecture

Context key approach: Rather than checking for image outputs at action execution time only, a reactive hasImageOutput context key is maintained via the existing useCellContextKeys observable pattern. This keeps the menu visibility declarative and consistent with how other cell state (e.g., hasOutputs, outputIsCollapsed) is managed.

Clipboard path: Reuses the same IClipboardService.writeImage() path used by the plots pane, which routes through Electron's native clipboard API. This avoids duplicating clipboard logic and ensures consistent behavior across the IDE.

Multi-image right-click: When the user right-clicks directly on an <img> in the output area, the image's data URL is stored on the cell (targetImageDataUrl) before showing the context menu. The "Copy Image" action uses this targeted URL when available, falling back to the first image output when triggered from the ellipsis menu. The stored URL is cleared when the menu closes.

Cmd+C keybinding: Deferred to #12434, which will add an output-focused state so Cmd+C can be context-dependent -- copying images when the output area is focused and copying cell source when the cell container is focused.

Action group ordering: A new Copy action group (0_copy) is added before the existing Visibility group (renumbered to 1_visibility) so copy actions appear at the top of the output menu.

Tests added

Unit tests (notebookOutputUtils.test.ts, positronNotebookCellOutputs.test.ts):

  • Output parsing for PNG (base64 data URL with payload validation), SVG, text, and stdout MIME types
  • Cell outputs observable correctly reports image outputs for PNG and SVG
  • Dynamic output addition via applyEdits updates the observable
  • Context key hasImageOutput is correctly set via bindCellContextKeys when cell has mixed output types

E2E tests (notebook-copy-output-image.test.ts):

  • Ellipsis menu shows "Copy Image" for matplotlib plot output
  • Clicking "Copy Image" from ellipsis menu writes image data to clipboard

Release Notes

New Features

QA Notes

@:positron-notebooks

  1. Open a Python notebook and run a cell that generates a matplotlib plot:
import matplotlib.pyplot as plt
plt.figure(figsize=(3, 2))
plt.plot([1, 2, 3], [1, 4, 9])
plt.show()
  1. Hover over the output area and verify a "Copy Image" button appears in the output ellipsis menu (three dots)
  2. Click the "Copy Image" button and paste into an image editor to verify the image was copied
  3. Right-click the output area and verify "Copy Image" appears in the context menu
  4. Collapse the output and verify "Copy Image" is hidden from both menus
  5. Run a cell with text-only output (e.g., print("hello")) and verify "Copy Image" does not appear
  6. Run a cell that generates multiple plots and right-click the second plot -- verify "Copy Image" copies that specific image, not the first

@nstrayer nstrayer requested review from dhruvisompura and seeM March 6, 2026 17:52
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:positron-notebooks

readme  valid tags

@nstrayer nstrayer force-pushed the feat/notebook-copy-output-image branch from 9139c0e to 4329f09 Compare March 6, 2026 17:52
nstrayer added a commit that referenced this pull request Mar 13, 2026
- Replace `targetImageDataUrl` mutable property on cell model with
  `menuActionOptions` arg forwarding through context menu service
- Add `POSITRON_NOTEBOOK_OUTPUT_IMAGE_TARGETED` context key so "Copy
  Image" only appears when an image is actually targeted (right-click
  on image or ellipsis menu for cell with image output)
- Fix misclick UX: right-clicking near but not on an image no longer
  shows "Copy Image"
- Add unit test for new context key
- Wrap e2e test menu verification in toPass retry for CI stability

Addresses PR #12331 review feedback.
@nstrayer nstrayer force-pushed the feat/notebook-copy-output-image branch from 7170b69 to 23a30d9 Compare March 13, 2026 18:37
@nstrayer nstrayer requested a review from seeM March 13, 2026 19:07
seeM
seeM previously approved these changes Mar 16, 2026
Copy link
Copy Markdown
Contributor

@seeM seeM left a comment

Choose a reason for hiding this comment

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

Looks great! One question about the dynamic actions in the overflow menu. It would also be nice if the e2e test could cover a case with >1 image, but not a blocker.

const logService = accessor.get(ILogService);

// Look for a CopyImageMenuArg forwarded from the context menu
const menuArg = args.find(isCopyImageMenuArg);
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.

Neat pattern for command args

const imageDataUrl = parsed.dataUrl;
actions.push(toAction({
id: `positronNotebook.cell.copyOutputImage.${i}`,
label: localize('positronNotebook.cell.copyPlotN', "Copy Plot {0}", i + 1),
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.

Hmm do you think these actions are helpful? It feels easier to right click on image. Even if you have a really long output with images scattered throughout, you'd need to scroll to them to see which image you want.

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.

If we keep them, can you please rename to Copy Image N since they could be images other than plots and to match the current single action case Copy Image?

nstrayer added 18 commits March 16, 2026 13:27
Add ability to copy plot/image outputs from Positron notebook cells
via right-click context menu and Cmd+C keyboard shortcut.

- New context key `positronNotebookCellHasImageOutput` tracks whether
  the active cell has image outputs
- New "Copy Image" action in the output context menu (ellipsis button
  and right-click), visible only when the cell has image output
- Cmd+C keybinding in command mode copies the image when available
- Uses IClipboardService.writeImage() which routes through Electron's
  native clipboard API, matching the plots pane approach
- New "Copy" action group in output menu, shown above visibility actions
Unit tests:
- Output parsing for PNG, SVG, text, and stdout mime types
- Cell outputs observable correctly reports image outputs
- Dynamic output addition updates the observable
- Image detection works with mixed output types (text + image)

E2E tests:
- Right-click context menu shows "Copy Image" for plot output
- Clicking "Copy Image" writes image data to clipboard
- Cmd+C in command mode copies image when cell has plot output
- Clear clipboard before asserting in e2e copy image test to prevent
  false positives from stale clipboard data
- Add full payload validation to PNG parsing unit test
- Let browser handle right-click on images natively so the correct
  image is copied in multi-image outputs (addresses @seeM's comment
  about always copying the first image)
- Update unit test to check context keys directly via
  bindCellContextKeys and getContextKeyValue instead of re-checking
  the raw expression
- Rewrite e2e test to use the ellipsis menu instead of right-click,
  which is more reliable in CI and matches the new behavior
Remove the Cmd+C keybinding that copies images in command mode,
as it overrides the expected cell source copy behavior. Image copy
via Cmd+C will be re-added once output-focused state is implemented
(tracked in #12434). The ellipsis menu and browser right-click
remain available for copying images.
When a cell has multiple image outputs, right-clicking on a specific
image now copies that image rather than always the first. The clicked
image's data URL is stored on the cell before the context menu opens,
and the Copy Image action reads it before falling back to the first
image (for the ellipsis menu path).
- Replace `targetImageDataUrl` mutable property on cell model with
  `menuActionOptions` arg forwarding through context menu service
- Add `POSITRON_NOTEBOOK_OUTPUT_IMAGE_TARGETED` context key so "Copy
  Image" only appears when an image is actually targeted (right-click
  on image or ellipsis menu for cell with image output)
- Fix misclick UX: right-clicking near but not on an image no longer
  shows "Copy Image"
- Add unit test for new context key
- Wrap e2e test menu verification in toPass retry for CI stability

Addresses PR #12331 review feedback.
- Use useMemo to bind POSITRON_NOTEBOOK_OUTPUT_IMAGE_TARGETED once per
  context key service instead of re-binding on every render (which
  resets the key to its default)
- Clear outputImageTargeted to false in onHide callbacks so stale
  interaction state does not persist after menu dismissal
- Remove BRANCH-SUMMARY development artifact file
- Replace hardcoded app.code.wait(500) with toPass retry in e2e test
SVG notebook outputs use URL-encoded data URIs (data:image/svg+xml,...),
but IClipboardService.writeImage() expects base64 encoding. Add
toBase64DataUrl() to convert URL-encoded data URIs to base64 before
writing to the clipboard.
Replace btoa() with encodeBase64(VSBuffer.fromString(...)) in
toBase64DataUrl(). btoa() only handles Latin-1 characters and would
throw for SVG content containing Unicode. The existing encodeBase64
utility handles arbitrary bytes correctly via UTF-8 encoding.
SVG payloads may contain literal '%' characters that are not
URL-encoded, causing decodeURIComponent to throw URIError. Wrap in
try/catch and fall back to using the raw payload.
When a cell produces multiple plot outputs, the ellipsis menu now shows
"Copy Plot 1", "Copy Plot 2", etc. (up to 5) instead of a single
"Copy Image" that only copies the first image. When there are more than
5 plots, a disabled hint tells users to right-click to copy additional
plots. Single-image output retains the existing "Copy Image" behavior.
…ImageUtils

Moves CopyImageMenuArg, isCopyImageMenuArg, and toBase64DataUrl from
contribution.ts to a shared copyImageUtils.ts module, eliminating the
duplicated interface in CellOutputLeftActionMenu.tsx. Adds unit tests
covering base64 passthrough, URL-encoded SVG conversion, raw percent
signs, and Unicode content.
For multiple image outputs, users can right-click individual images to
copy them instead of using menu entries.
@nstrayer nstrayer force-pushed the feat/notebook-copy-output-image branch from 1925421 to 689c454 Compare March 16, 2026 17:33
@nstrayer nstrayer merged commit 0dfec6a into main Mar 16, 2026
21 checks passed
@nstrayer nstrayer deleted the feat/notebook-copy-output-image branch March 16, 2026 18:00
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants