# Take screenshots on the web beta, and of cloud strategies anywhere \(\#229\) · dara/icarus

[View on GitCafe](https://git.cafe/dara/icarus/commit/5424d767d036f3ff4be3466c8a81c8a401175983)

Repository: [dara/icarus](https://git.cafe/dara/icarus)

Visibility: public

Requested revision: 5424d767d036f3ff4be3466c8a81c8a401175983

Requested commit: 5424d767d036f3ff4be3466c8a81c8a401175983

Commit: 5424d767d036f3ff4be3466c8a81c8a401175983

Tree: dc8a2d3ba2a673a1fa0fcd6e9b8bbf39a3a7d838

Author: Dara Adedeji

Committer: GitHub

## Message

```
Take screenshots on the web beta, and of cloud strategies anywhere (#229)

* Take screenshots on the web beta, and of cloud strategies anywhere

The screenshot button read the open strategy back from the local Hive
box after a forced save. Cloud strategies are never in that box, so a
screenshot of one silently did nothing, on desktop as well as web. It
then wrote the PNG with dart:io, which a browser cannot do, which is why
the web beta hid the button behind "desktop-only".

The capture now reads the page as it is on the canvas (the editor's live
providers, drawings copied so their cached paths stay the editor's), so
local and cloud strategies capture the same way and nothing waits on a
save or a sync. The PNG goes out through FilePicker.saveFile(bytes:),
which writes the chosen file on desktop and downloads it in a browser.

Images: the capture renders in its own provider container, which has no
live cloud page, upload queue or pending bytes. Before it starts, every
image on the page is resolved to what it will paint and handed in through
captureImageSourcesProvider: files and in-memory bytes as they are, cloud
URLs fetched to bytes (downloadCloudImageBytes, shared with the desktop
media cache, including its one signed-URL refresh), and images the editor
shows as unavailable stay unavailable. An image still loading or a fetch
that fails stops the capture with a message instead of saving a picture
with the image missing. PendingImageBytes becomes ImageBytes, since bytes
fetched for a capture are not an upload.

Screenshot leaves PlatformPolicy.webBeta.desktopOnly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Copy the page and decode its images before a screenshot waits

Review follow-up. A capture awaits image downloads before it renders, and
the editor keeps editing its own objects in place meanwhile, so the page
snapshot now clones agents, abilities, text, images and utilities (and
deep-copies the lineup graph) instead of sharing them; an edit made while
a slow image downloads can no longer land in half the picture.

Fetched bytes were also never proven to be an image: a 200 with truncated
bytes, or a decode slower than the 800 ms settle, would have saved a PNG
with the picture missing. resolveCaptureImages now decodes every image
that paints into the image cache and holds it there (keepAlive) until the
capture releases it, so the capture's widgets find each picture already
decoded, and a decode failure stops the capture with the same message as
a failed fetch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Refresh a screenshot's image URLs through the reader's share link

A signed-out reader who opened a strategy link (#225) can take a
screenshot too. Their only access to its images is that link, so the
capture's signed-URL refresh carries the link's token, as the media cache
does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep a capture's images live until it is done, and bound their downloads

Review follow-up. A keepAlive handle keeps a decoded image's completer
alive, but once the last listener goes the image cache stops tracking it
as live, so with enough large images an earlier one could be evicted and
decoded again mid-capture, which is the missing-image timing the decode
step exists to prevent. Each image now stays listened to until the
capture releases it, which keeps it among the cache's live images.

A stalled image request could also hold a capture forever: each download
now times out after 60 s and fails the capture like any failed fetch.
resolveCaptureImages takes a checkpoint, run before each image, so a
caller can stop the work between images.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Let go of a capture image only once

Review follow-up: an image whose later frame failed to decode let go of
its stream in the error handler, and release() then removed the listener
again, which throws once the stream is disposed and would have stopped
the cleanup of the images after it. Letting go is now idempotent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep a capture from signing the editor out of cloud sync

Found in the browser run: each screenshot was followed by "Cloud
connection lost". A capture renders in its own provider container, and
hydrating its strategy provider builds that container's own auth
provider, since the strategy provider listens to auth. A real auth
provider configures the one Convex client the app syncs through: built
with no session it clears that client's auth, with one it replaces the
editor's token fetcher, and disposing it tears the auth down. Either way
the editor's session ends.

createCaptureContainer now builds every capture's container with an
inert signed-out auth provider (nothing a capture paints needs auth) and
the capture's image sources. A test records every call a capture makes
to the Convex auth API and requires none; before this change it saw
clearAuth.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Download a capture's images together, and take one screenshot at a time

Greptile follow-ups. A page's cloud images downloaded one after another,
so the wait was the sum of them; they now start together and decode in
order, with the same checkpoints and cleanup, and an image still loading
stops the capture before any download starts. And the screenshot guard
cleared before the save dialog closed, so a second click could capture
again and open a second dialog; the guard now holds until the save is
done. Both have tests; the second fails without the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Abort a capture's other downloads when it stops early

Greptile follow-up: when one image failed or the capture was cancelled,
the downloads already started for the others kept running, and a retry
started them again alongside. A capture's downloads now share one HTTP
client that is closed when resolution ends, which aborts whatever is
still in flight (BrowserClient and IOClient both abort on close). A test
fails one image while another waits and requires the client closed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
```

## Parents

- [0491d518b734b7115ae46402322d152d0e309fad](https://git.cafe/dara/icarus/commit/0491d518b734b7115ae46402322d152d0e309fad?format=markdown)

[Source at this commit](https://git.cafe/dara/icarus/tree/5424d767d036f3ff4be3466c8a81c8a401175983?format=markdown)
