Repository navigation
Assets page - #182
Merged
Merged
Assets page#182
Conversation
The shared `common` library and every `--examples-dir` one sit on every workspace's search path, so most of the grid can be identical in two workspaces - on a real box 34 shared assets against a handful the workspace owns. Switching the picker then looks like a page that ignored it. Three things say so rather than leaving it to be inferred: - the count breaks out `N from other libraries` - the hint names the rule - the library pick is offered whenever *anything* came from elsewhere, not only when two origins are in play. It used to unmount exactly in the workspace where the question comes up - every asset `common`, one origin, no control - and filtering to `workspace` is what shows you what is actually yours The upload destination becomes a named pick (`upload to [this workspace | shared library]`) rather than a bare `shared` tickbox. It is the one thing about an upload that cannot be changed afterwards and nothing on the page explained what the tickbox meant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hree archives The gallery's bulk selection had been copied into the assets page whole, and the copies had already drifted: the assets page's Escape guard knew that ConfirmDialog renders `alertdialog` and the gallery's did not, so Escape in the gallery's delete confirm cancelled the dialog *and* closed the detail behind it. That is what two copies of a keyboard contract does. It is now `picks.svelte.ts` - a `Picks` over a getter for the grid's current order, so a filter changing under the selection is seen rather than snapshotted, plus `actOnEach` for the sequential act-and-collect-failures loop and `dialogOpen()` for the guard - with `BulkBar.svelte` for the sticky bar and select-all, and the two rules that have to reach a tile the page lays out in app.css. The two pages lose 314 lines between them. The gallery's Escape bug is fixed by construction and pinned by a test. Server: the temp-zip dance - and specifically the `except BaseException: unlink` that stops a half-written archive leaking into tmp - was in three routes; it is now `_zip_download`. `AssetArchiveRequest` was a byte-identical copy of `ArchiveRequest`. `archive_assets` rebuilt the asset search path once per name, so a thousand-name archive stat'd every root a thousand times; `_asset_in` takes the path already built, and `_asset_file` delegates to it so the two resolvers cannot drift. Archives now store rather than deflate anything but `.bmp`/`.wav`. Every other extension the libraries hold is an already-compressed container, so deflating buys ~0.03% for a full CPU pass - and since the response does not start until the temp file is complete, that pass is latency the caller waits through: measured, about 13s for a gigabyte of video. Also: `originOffered` is `borrowed > 0` rather than that OR'd onto the rule it replaced; the upload destination is read straight off `uploadTo`; the header count's separator is one expression, since Svelte trims a block's leading whitespace and had eaten that space twice; and AssetsPage's tests reset their mocks rather than clearing them, so an implementation set by one test cannot leak into the next. Left alone deliberately, as open questions rather than cleanups: `size` counts every ticked name while the actions run over the visible ones (`Picks.hidden` is there for whichever way that is settled), the bulk-delete confirm does not say that a shared asset goes for every workspace, and the upload destination offers a shared library on servers that have none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_asset_in resolved each root by calling resolve_asset_reference, which
already walks the pinned fallbacks on its own - 107 syscalls per hit
where 29 would do. Validate the name once and let each root be a plain
join-and-isfile check instead, via a new _resolution_roots(ws) helper
shared with the validate route and _asset_file.
The compression policy inverted itself for anything outside {.bmp,
.wav}: COMPRESSIBLE_EXTENSIONS named the two formats that deflate, but
the check stored everything *not* in that set - including plain text
like workflow.json, manifest.json and the export README. Replace it
with RAW_MEDIA_EXTENSIONS (what it actually named) and store only a
recognized media extension that isn't raw.
archive_assets now strips and dedupes names before resolving, so a
repeated or whitespace-varied selection collapses onto one zip entry
instead of colliding on write. The two archive routes' duplicated
three-line tail (timestamp, log line, _zip_download call) is now
_archive_selection(entries, kind), logged after the archive is
written so a failed write never logs a false success.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Picks.size counted every ticked name, including ones a filter was hiding, so the bar's number and what Delete/Download would actually act on (names, which is visible-only) could disagree. keepFailed made it worse: it cleared the whole selection and re-added only the failures, silently dropping a hidden tick the action never attempted. size now returns names.length, hidden is gone (nothing rendered it), and keepFailed(attempted, failed) only removes names it was told were attempted and did not fail, so a hidden tick survives. Both pages early-return when there is nothing visible to act on, for the keyboard path that could still reach the handler with the bar hidden. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A client that only sees the resolved asset list cannot show which of its names hide a shared one, or say why a delete lands on one library and 403s on another. `libraries` reports the search path in order with origin and writability; `shadowed` reports the entries the resolution loop used to silently skip - same shape as an `assets` entry minus `url` (that URL would serve the shadowing file), plus `shadowed_by`. MCP `list_assets` returns this body verbatim, so its docstring and docs/MCP.md/docs/SERVER.md describe the new fields too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_asset_in used to call resolve_asset_reference once per root, which walks the pinned fallbacks on its own and let a SecurityError from an escaping symlink surface as a caught 404 - each call's resolution swallowed its own walk's failures. The rewrite in the prior commit validates the name once and probes each root with a plain isfile + validate_path, but validate_path raising SecurityError on a resolved-through-symlink escape was left uncaught, so the route handler's default exception path turned it into a 500 instead of a miss for that root. Wrap the per-root validate_path in a try/except SecurityError so an escaping symlink falls through to the next root like any other miss, ending in the same 404 the caller gets for a name that was never there. Adds test_archive_assets_rejects_a_name_behind_a_symlink: a symlink inside the asset library pointing outside it, archived through POST /api/assets/archive, now answers 404 rather than 500. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The search path is the real top level, and the page was hiding it. One mtime-sorted grid with an `origin` badge per tile meant folders cut across libraries, so a `cast/` group could hold two files the workspace owns and four it only borrows; shadowing was named in the hint and never shown, so "I uploaded it and asset: still loads the old one" was unanswerable from the page; deleting a shared asset looked exactly like deleting a workspace one, though it goes for every workspace under the root; and the library `select` could be left pointing at a library that unmounted on the next workspace switch - a stale pick this removes by removing the control. A library is now a section, in the server's own resolution order, with folders inside it. The header carries the label (`This workspace`, `Shared library`, `Examples`), the count, the root it reads and a collapse chevron, persisted under `collapsed-asset-libraries` and forced open while a filter is on, as FolderGroups has it. An empty library still shows its header, so an empty workspace says where an upload would land. Upload moves onto the section, because which library a file lands in is the one thing about an upload that cannot be changed afterwards: `Upload` on the workspace section, a quiet `Upload to shared` on the shared one, a muted `read-only` where the server would answer 403. For the same reason a tile from a read-only library carries no checkbox - nothing bulk can do to it - and the delete confirms say what a shared delete costs, singly and in the bulk count. `shadowed` is rendered rather than described: each section shows what it holds under a name a nearer library has taken, as dimmed inert tiles under a `shadowed/` heading. The server serves no url for one, so it is a label rather than a picture. The Picks order is the concatenation of the sections' visible assets, so a shift-range and the bulk actions follow the page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Picks order was every asset in every section, so `Select all` ticked two kinds of tile the user cannot act on. An examples tile renders no checkbox - nothing bulk can do to a read-only library - so it came back `.cellwrap.picked` with no control on it to untick, recoverable only through Clear, and Delete then 403'd on each one and left them ticked. A collapsed library section was worse: a bulk delete reached files that were not on screen at all, which is the one invariant ui/CLAUDE.md states about this selection. The order now spans open, writable sections only, so the bar's count, `selectAll`, `size` and `names` all follow what the page is showing with a checkbox on it. Collapsing a section is now as narrowing as a filter - deliberately, since it hides rows the same way. Also: the shadowed tile carried its explanation only in `title`, which a bare div does not expose - it is `role="note"` with the same text as an `aria-label`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four pages each rounded a byte count and rendered an mtime their own way, so the same number read differently depending which page showed it; format.ts is the one implementation now (formatBytes, formatMtime), covered by tests for the KB/MB/GB ranges and the 1 MB boundary. ModelsPage's day (date-only) and gb (used well beyond the size column) stay put - only the size/mb/kb/day pairs the review flagged move. .flex and button.withicon/.withicon were scoped copies in a dozen components; BulkBar, AssetsPage and GalleryPage's copies are gone in favour of one global rule each beside button.quiet/button.bare. .cellwrap.picked .cell tied a page's own scoped .cell.active/.cell:hover on specificity and only won by source order - true by accident of where a stylesheet happens to load. .cellwrap.picked > .cell.cell pushes the global rule to (0,4,0) so it outranks the scoped tie on purpose. Also: Picks.keepOnly/keepFailed used a Set for a one-shot lookup over a small local list, which is what tripped svelte/prefer-svelte-reactivity in eslint - swapped for a plain array, since nothing about either lookup is reactive state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.