From 417ba5df6e48a8ca6f9b4382e04f775de380a362 Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Mon, 21 Sep 2026 13:19:23 +0700 Subject: [PATCH 01/30] fix(home): serve wallpaper bytes with cross-origin-resource-policy The gateway enforces Cross-Origin-Embedder-Policy: require-corp and the Home GUI desktop renders from an opaque sandboxed frame. The background image route only set Content-Type, so Chrome blocked the uploaded wallpaper with NotSameOriginAfterDefaultedToSameOriginByCoep even though the bytes were saved and served. Mirror the capsule asset route and assert the header in the existing round-trip test. Co-authored-by: Cursor --- .../src/api/gateway_home_system.rs | 17 ++++++++++++++++- .../src/api/gateway_tests/home_system.rs | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/elastos/crates/elastos-server/src/api/gateway_home_system.rs b/elastos/crates/elastos-server/src/api/gateway_home_system.rs index d7c002de8..e7f7bf331 100644 --- a/elastos/crates/elastos-server/src/api/gateway_home_system.rs +++ b/elastos/crates/elastos-server/src/api/gateway_home_system.rs @@ -7697,16 +7697,31 @@ pub(super) async fn home_background_image( ) { Ok(bytes) => { let mut response = bytes.into_response(); - response.headers_mut().insert( + let headers = response.headers_mut(); + headers.insert( axum::http::header::CONTENT_TYPE, HeaderValue::from_static(entry.content_type), ); + // Home GUI renders the wallpaper from an opaque sandboxed frame under + // the gateway's COEP `require-corp`. Without this header the browser + // blocks the image even though the bytes were served. + headers.insert( + axum::http::HeaderName::from_static("cross-origin-resource-policy"), + HeaderValue::from_static(HOME_BACKGROUND_IMAGE_RESOURCE_POLICY), + ); + headers.insert( + axum::http::header::X_CONTENT_TYPE_OPTIONS, + HeaderValue::from_static("nosniff"), + ); response } Err(err) => home_error_response(anyhow::anyhow!(err)), } } +/// Matches the capsule asset route so wallpaper bytes pass the same COEP check. +pub(super) const HOME_BACKGROUND_IMAGE_RESOURCE_POLICY: &str = "cross-origin"; + #[cfg(test)] mod home_realtime_tests { use super::*; diff --git a/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs b/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs index 3424d2a13..b5fe8f7b6 100644 --- a/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs +++ b/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs @@ -4838,6 +4838,14 @@ async fn test_system_updates_home_background_image() { .and_then(|value| value.to_str().ok()), Some("image/png") ); + // Home GUI loads the wallpaper from an opaque frame under COEP require-corp. + assert_eq!( + image + .headers() + .get("cross-origin-resource-policy") + .and_then(|value| value.to_str().ok()), + Some("cross-origin") + ); let image_body = axum::body::to_bytes(image.into_body(), usize::MAX) .await .unwrap(); @@ -4862,6 +4870,13 @@ async fn test_system_updates_home_background_image() { .and_then(|value| value.to_str().ok()), Some("image/jpeg") ); + assert_eq!( + guest_image + .headers() + .get("cross-origin-resource-policy") + .and_then(|value| value.to_str().ok()), + Some("cross-origin") + ); let guest_image_body = axum::body::to_bytes(guest_image.into_body(), usize::MAX) .await .unwrap(); From 42f44f30f9b86207207bb75a7655b665053a0d07 Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Mon, 21 Sep 2026 13:19:23 +0700 Subject: [PATCH 02/30] fix(home-gui): load wallpaper through the launch-token path The Home session cookie is SameSite=Strict, so a CSS url() request from the opaque Home GUI frame cannot be relied on to carry it. Fetch the image with x-elastos-home-token like every other Home request, apply it as an object URL keyed by the versioned summary URL, revoke on change, and fall back to the default wallpaper if the fetch fails. Co-authored-by: Cursor --- capsules/home-gui/browser/home-gui.js | 46 +++++++++++++++++++++++-- capsules/home-gui/browser/shell-core.js | 17 +++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/capsules/home-gui/browser/home-gui.js b/capsules/home-gui/browser/home-gui.js index 66e403cee..12afbf442 100644 --- a/capsules/home-gui/browser/home-gui.js +++ b/capsules/home-gui/browser/home-gui.js @@ -17,6 +17,7 @@ import { toolbarInboxButton, toolbarSignOutButton, ensureHomeGuiDom, + fetchBlob, initializeRecentTargets, initializeShellLayout, rememberSharedUiPreferences, @@ -884,11 +885,52 @@ export function syncHomeGuiAppearance(summary) { } desktopBackdrop.dataset.overlay = overlayEnabled ? "true" : "false"; desktopBackdrop.style.setProperty("--desktop-overlay-opacity", String(overlayOpacity)); + void applyDesktopWallpaper(imageUrl); +} + +// The summary URL carries `v=` so it changes whenever the user uploads or +// resets the image; that string is the cache key for the loaded object URL. +const wallpaperLoad = { sourceUrl: "", objectUrl: "" }; + +function clearDesktopWallpaper() { + desktopBackdrop.style.removeProperty("--desktop-wallpaper"); + if (wallpaperLoad.objectUrl) { + URL.revokeObjectURL(wallpaperLoad.objectUrl); + } + wallpaperLoad.objectUrl = ""; +} + +async function applyDesktopWallpaper(imageUrl) { + if (wallpaperLoad.sourceUrl === imageUrl) { + return; + } + wallpaperLoad.sourceUrl = imageUrl; if (!imageUrl) { - desktopBackdrop.style.removeProperty("--desktop-wallpaper"); + clearDesktopWallpaper(); return; } - desktopBackdrop.style.setProperty("--desktop-wallpaper", `url("${imageUrl}")`); + let objectUrl = ""; + try { + objectUrl = URL.createObjectURL(await fetchBlob(imageUrl)); + } catch (error) { + // Fail closed to the default wallpaper instead of a broken image. The + // source URL stays recorded so summary refreshes do not retry until the + // Runtime advertises a new image version. + if (wallpaperLoad.sourceUrl === imageUrl) { + clearDesktopWallpaper(); + } + console.warn("home-gui wallpaper unavailable", error); + return; + } + if (wallpaperLoad.sourceUrl !== imageUrl) { + URL.revokeObjectURL(objectUrl); + return; + } + if (wallpaperLoad.objectUrl) { + URL.revokeObjectURL(wallpaperLoad.objectUrl); + } + wallpaperLoad.objectUrl = objectUrl; + desktopBackdrop.style.setProperty("--desktop-wallpaper", `url("${objectUrl}")`); } function targetsChanged(previous, next) { diff --git a/capsules/home-gui/browser/shell-core.js b/capsules/home-gui/browser/shell-core.js index 95068c318..86f9234c3 100644 --- a/capsules/home-gui/browser/shell-core.js +++ b/capsules/home-gui/browser/shell-core.js @@ -284,6 +284,23 @@ export async function fetchJson(url, init) { return response.json(); } +/** + * Fetch a binary Home resource with the launch token. Home GUI runs in an + * opaque sandboxed frame, so cookie-authenticated CSS `url()` loads cannot be + * relied on; this is the same authenticated path every other Home request uses. + */ +export async function fetchBlob(url) { + const response = await fetch(url, { + headers: homeGuiLaunchToken ? { "x-elastos-home-token": homeGuiLaunchToken } : {}, + }); + if (!response.ok) { + const error = new Error(`request failed: ${response.status} ${response.statusText}`); + error.status = response.status; + throw error; + } + return response.blob(); +} + export async function mutateDesktopObject(op, payload = {}) { return fetchJson("/api/apps/home/desktop/objects", { method: "POST", From 426948f8dd367dc297f775b0130dfb507c2af1ed Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Mon, 21 Sep 2026 13:19:23 +0700 Subject: [PATCH 03/30] fix(library): keep preview status out of the toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dialog heading already names the file; writing 'Previewing …' into the toolbar pushed the search and view controls. Clear the status on preview and cap #status-text so no transient message can shift layout. Co-authored-by: Cursor --- capsules/library/browser/library.css | 23 +++++++++++++++++++++++ capsules/library/browser/src/preview.js | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/capsules/library/browser/library.css b/capsules/library/browser/library.css index 5e0d1faaa..5d1306caa 100644 --- a/capsules/library/browser/library.css +++ b/capsules/library/browser/library.css @@ -458,6 +458,16 @@ flex: 0 1 auto; } + /* Transient status must never push the search and view controls. */ + #status-text { + flex: 0 1 auto; + min-width: 0; + max-width: 220px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .toolbar-search { display: flex; align-items: center; @@ -731,6 +741,19 @@ display: block; } + /* Image files show their own thumbnail in place of the generic icon. */ + .item--thumbnail .file-icon img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 3px; + } + + .content[data-view="grid"] .item--thumbnail .file-icon { + overflow: hidden; + border-radius: 4px; + } + .item-name { max-width: 100%; overflow: hidden; diff --git a/capsules/library/browser/src/preview.js b/capsules/library/browser/src/preview.js index e682600e8..c09e7cbe0 100644 --- a/capsules/library/browser/src/preview.js +++ b/capsules/library/browser/src/preview.js @@ -76,7 +76,9 @@ export function createLibraryPreview({ `; dialog.dataset.previewUri = object.uri; dialog.classList.remove("hidden"); - setStatus(`Previewing ${object.name}.`); + // The dialog heading names the file; a toolbar message would only push + // the search and view controls out of place. + setStatus(""); } return { From 6cf258af08d97854dd2858f7fd080bcccceb0732 Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Mon, 21 Sep 2026 13:19:23 +0700 Subject: [PATCH 04/30] feat(library): lazy image thumbnails for image objects Render image files with their own thumbnail instead of the generic icon. Only on-screen items are fetched (IntersectionObserver), at most three at a time, capped at 4 MB, through the download authority Library already holds; object URLs are released past 160 entries. Prefers thumbnail_uri when the Runtime provides one. Co-authored-by: Cursor --- capsules/library/browser/src/app.js | 5 + capsules/library/browser/src/render.js | 4 + capsules/library/browser/src/thumbnails.js | 134 +++++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 capsules/library/browser/src/thumbnails.js diff --git a/capsules/library/browser/src/app.js b/capsules/library/browser/src/app.js index 0345b5e7e..3ae689d18 100644 --- a/capsules/library/browser/src/app.js +++ b/capsules/library/browser/src/app.js @@ -27,6 +27,7 @@ import { createLibraryPreview } from "./preview.js"; import { createLibraryRealtime } from "./realtime.js"; import { createLibraryRenderer, iconPlaceholder } from "./render.js?v=library-20260711d"; import { createLibrarySelection } from "./selection.js"; +import { createLibraryThumbnails } from "./thumbnails.js"; import { MUTATING_PROVIDER_OPS, cacheFolderListing, @@ -245,6 +246,10 @@ import { perf, selectedObjects, state, + thumbnails: createLibraryThumbnails({ + readObjectBlob: (uri) => downloadObjectRaw({ uri }).then((result) => result.blob), + root: elements.content, + }), visibleObjects, })); ({ diff --git a/capsules/library/browser/src/render.js b/capsules/library/browser/src/render.js index cbcce5e56..0d9113005 100644 --- a/capsules/library/browser/src/render.js +++ b/capsules/library/browser/src/render.js @@ -9,6 +9,7 @@ import { isDirectory, visibilityContract, } from "./model.js"; +import { isThumbnailCandidate } from "./thumbnails.js"; const LARGE_RENDER_THRESHOLD = 240; const INITIAL_RENDER_LIMIT = 120; @@ -26,6 +27,7 @@ export function createLibraryRenderer({ perf, selectedObjects, state, + thumbnails = null, visibleObjects, }) { function renderContent() { @@ -132,6 +134,7 @@ export function createLibraryRenderer({ cached.node.dataset.selected = isSelected(object.uri) ? "true" : "false"; state.objectNodeCache.delete(object.uri); state.objectNodeCache.set(object.uri, cached); + if (thumbnails && isThumbnailCandidate(object)) thumbnails.observe(cached.node, object); return cached.node; } perf.objectNodeCacheMisses += 1; @@ -161,6 +164,7 @@ export function createLibraryRenderer({ ${badgesMarkup} `; state.objectNodeCache.set(object.uri, { signature, node: item }); + if (thumbnails && isThumbnailCandidate(object)) thumbnails.observe(item, object); return item; } diff --git a/capsules/library/browser/src/thumbnails.js b/capsules/library/browser/src/thumbnails.js new file mode 100644 index 000000000..23c0e5a06 --- /dev/null +++ b/capsules/library/browser/src/thumbnails.js @@ -0,0 +1,134 @@ +// Lazy image thumbnails for Library items. +// +// Runtime models `thumbnail_uri` on objects but does not fill it yet. Until it +// does, Library renders a thumbnail from the image bytes it is already allowed +// to download. Only items on screen are fetched, a few at a time, and object +// URLs are released when the cache is full. + +const THUMBNAIL_MAX_BYTES = 4 * 1024 * 1024; +const THUMBNAIL_CACHE_LIMIT = 160; +const THUMBNAIL_CONCURRENCY = 3; +const THUMBNAIL_ROOT_MARGIN = "200px"; + +export function isThumbnailCandidate(object) { + if (!object || object.kind === "directory") return false; + if (!String(object.mime || "").startsWith("image/")) return false; + return Number(object.size || 0) <= THUMBNAIL_MAX_BYTES; +} + +export function thumbnailSignature(object) { + return [object.uri, object.revision || "", object.modified_at || 0, object.size || 0].join("\u0000"); +} + +export function createLibraryThumbnails({ readObjectBlob, root }) { + const cache = new Map(); + const pending = new Map(); + const targets = new WeakMap(); + const queue = []; + let active = 0; + const observer = typeof IntersectionObserver === "function" + ? new IntersectionObserver(onIntersect, { root, rootMargin: THUMBNAIL_ROOT_MARGIN }) + : null; + + function observe(node, object) { + const signature = thumbnailSignature(object); + targets.set(node, { object, signature }); + const cached = cache.get(signature); + if (cached) { + touch(signature, cached); + applyThumbnail(node, signature, cached.objectUrl); + return; + } + if (observer) { + observer.observe(node); + return; + } + enqueue(node); + } + + function onIntersect(entries) { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + observer.unobserve(entry.target); + enqueue(entry.target); + } + } + + function enqueue(node) { + const target = targets.get(node); + if (!target) return; + const cached = cache.get(target.signature); + if (cached) { + applyThumbnail(node, target.signature, cached.objectUrl); + return; + } + if (pending.has(target.signature)) { + pending.get(target.signature).push(node); + return; + } + pending.set(target.signature, [node]); + queue.push(target); + drain(); + } + + function drain() { + while (active < THUMBNAIL_CONCURRENCY && queue.length) { + const job = queue.shift(); + active += 1; + void load(job).finally(() => { + active -= 1; + drain(); + }); + } + } + + async function load({ object, signature }) { + const nodes = pending.get(signature) || []; + let objectUrl = ""; + try { + objectUrl = URL.createObjectURL(await readObjectBlob(object.thumbnail_uri || object.uri)); + } catch (_error) { + // Keep the generic file icon; the object may be unreadable or offline. + pending.delete(signature); + return; + } + pending.delete(signature); + remember(signature, objectUrl); + for (const node of nodes) { + applyThumbnail(node, signature, objectUrl); + } + } + + function applyThumbnail(node, signature, objectUrl) { + if (!node.isConnected || targets.get(node)?.signature !== signature) return; + const img = node.querySelector(".file-icon img"); + if (!img || img.dataset.thumbApplied === objectUrl) return; + img.dataset.thumbApplied = objectUrl; + img.src = objectUrl; + node.classList.add("item--thumbnail"); + } + + function remember(signature, objectUrl) { + cache.set(signature, { objectUrl }); + while (cache.size > THUMBNAIL_CACHE_LIMIT) { + const [oldest, entry] = cache.entries().next().value; + cache.delete(oldest); + URL.revokeObjectURL(entry.objectUrl); + } + } + + function touch(signature, entry) { + cache.delete(signature); + cache.set(signature, entry); + } + + function release() { + observer?.disconnect(); + for (const entry of cache.values()) URL.revokeObjectURL(entry.objectUrl); + cache.clear(); + pending.clear(); + queue.length = 0; + } + + return { observe, release }; +} From 2e5100986886b9fc9e1cf6e8249fbc654a37c8d4 Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Mon, 21 Sep 2026 14:04:31 +0700 Subject: [PATCH 05/30] fix(home): let the active shell fetch the wallpaper it renders The wallpaper GET only accepted the Home host token, yet the desktop is drawn by the active shell (home-gui) from an opaque frame with its own launch token, so every fetch returned 403 and the default wallpaper stayed. Accept the same authority the appearance-preferences POST already grants the active shell; ordinary app tokens are still refused. Co-authored-by: Cursor --- .../src/api/gateway_home_system.rs | 4 +- .../src/api/gateway_tests/home_system.rs | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/elastos/crates/elastos-server/src/api/gateway_home_system.rs b/elastos/crates/elastos-server/src/api/gateway_home_system.rs index e7f7bf331..df167902a 100644 --- a/elastos/crates/elastos-server/src/api/gateway_home_system.rs +++ b/elastos/crates/elastos-server/src/api/gateway_home_system.rs @@ -7678,7 +7678,9 @@ pub(super) async fn home_background_image( State(state): State, headers: HeaderMap, ) -> Response { - let context = match require_home_token_context(&state.data_dir, &headers) { + // The active shell renders the desktop, so it holds the same appearance + // authority it already uses to write preferences. + let context = match require_home_active_shell_token_context(&state.data_dir, &headers) { Ok(context) => context, Err(err) => return home_error_response(err), }; diff --git a/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs b/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs index b5fe8f7b6..d6201f43f 100644 --- a/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs +++ b/elastos/crates/elastos-server/src/api/gateway_tests/home_system.rs @@ -4754,6 +4754,43 @@ async fn test_system_updates_home_background_image() { serde_json::json!(HOME_BACKGROUND_OVERLAY_OPACITY_DEFAULT) ); + // The active Home GUI shell renders the desktop from an opaque frame and + // fetches the wallpaper with its own launch token. + let shell_token = + projection_launch_token_for_authority_context(dir.path(), HOME_GUI_SHELL_ID, &admin); + let shell_image = app + .clone() + .oneshot( + test_browser_request("localhost:61180", "null") + .uri(background_url) + .header("x-elastos-home-token", shell_token) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(shell_image.status(), StatusCode::OK); + let shell_image_body = axum::body::to_bytes(shell_image.into_body(), usize::MAX) + .await + .unwrap(); + assert_eq!(shell_image_body.as_ref(), b"admin-image"); + + // Ordinary app capsules hold no appearance authority. + let app_token = + projection_launch_token_for_authority_context(dir.path(), "regular-app", &admin); + let app_image = app + .clone() + .oneshot( + test_browser_request("localhost:61180", "null") + .uri(background_url) + .header("x-elastos-home-token", app_token) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(app_image.status(), StatusCode::FORBIDDEN); + let overlay = app .clone() .oneshot( From f5ebb9e870aac96833f3bf66bafbda260f155986 Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Mon, 21 Sep 2026 14:04:31 +0700 Subject: [PATCH 06/30] fix(system): keep the window still after the picker; show the wallpaper preview Chrome refocuses a file input when its picker closes and scrolls every scrollable ancestor to reveal it, including overflow:hidden .settings-container, which shifted the whole System window up. Hidden file inputs are now viewport-anchored so nothing needs to scroll. The background preview used a CSS url() that cannot carry the session from an opaque frame; load it through the launch token like other System requests. Co-authored-by: Cursor --- capsules/system/browser/style.css | 11 +++++++ capsules/system/browser/system.js | 48 ++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/capsules/system/browser/style.css b/capsules/system/browser/style.css index c579632ff..160ed66a2 100644 --- a/capsules/system/browser/style.css +++ b/capsules/system/browser/style.css @@ -543,6 +543,17 @@ code { border: 0; } +/* The browser refocuses a file input when its picker closes and scrolls the + nearest scrollable ancestors to reveal it — including overflow:hidden + .settings-container, which shifts the whole window. Anchoring the input to + the viewport means it is always "in view" and nothing has to scroll. */ +input[type="file"].visually-hidden { + position: fixed; + top: 0; + left: 0; + margin: 0; +} + .system-file-button { display: inline-flex; align-items: center; diff --git a/capsules/system/browser/system.js b/capsules/system/browser/system.js index 8f965b45a..6ad8aab45 100644 --- a/capsules/system/browser/system.js +++ b/capsules/system/browser/system.js @@ -1425,6 +1425,52 @@ function acceptAppearance(appearance) { return true; } +// System runs in an opaque sandboxed frame, so a CSS url() cannot carry the +// Home session. Fetch the wallpaper with the launch token like every other +// System request; the versioned summary URL is the cache key. +const backgroundPreviewLoad = { sourceUrl: "", objectUrl: "" }; + +function showDefaultBackgroundPreview() { + backgroundPreview.style.backgroundImage = `url("${DEFAULT_BACKGROUND_IMAGE_URL}")`; + if (backgroundPreviewLoad.objectUrl) { + URL.revokeObjectURL(backgroundPreviewLoad.objectUrl); + } + backgroundPreviewLoad.objectUrl = ""; +} + +async function applyBackgroundPreview(imageUrl) { + if (backgroundPreviewLoad.sourceUrl === imageUrl) { + return; + } + backgroundPreviewLoad.sourceUrl = imageUrl; + if (!imageUrl) { + showDefaultBackgroundPreview(); + return; + } + let objectUrl = ""; + try { + const response = await fetch(imageUrl, { headers: { "x-elastos-home-token": apiHomeToken } }); + if (!response.ok) { + throw new Error(`request failed: ${response.status}`); + } + objectUrl = URL.createObjectURL(await response.blob()); + } catch (_error) { + if (backgroundPreviewLoad.sourceUrl === imageUrl) { + showDefaultBackgroundPreview(); + } + return; + } + if (backgroundPreviewLoad.sourceUrl !== imageUrl) { + URL.revokeObjectURL(objectUrl); + return; + } + if (backgroundPreviewLoad.objectUrl) { + URL.revokeObjectURL(backgroundPreviewLoad.objectUrl); + } + backgroundPreviewLoad.objectUrl = objectUrl; + backgroundPreview.style.backgroundImage = `url("${objectUrl}")`; +} + function applyAppearance(appearance) { if (!appearance) { return; @@ -1436,8 +1482,8 @@ function applyAppearance(appearance) { window.elastosTheme?.setAccentCustom?.(appearance.accent_custom); window.elastosTheme?.setAccent?.(appearance.accent); if (backgroundPreview) { - backgroundPreview.style.backgroundImage = `url("${imageUrl || DEFAULT_BACKGROUND_IMAGE_URL}")`; backgroundPreview.dataset.empty = imageUrl ? "false" : "true"; + void applyBackgroundPreview(imageUrl); } if (backgroundResetButton) { backgroundResetButton.disabled = !hasShellAccess() || imageUrl.length === 0; From 6412da58f0caa490984715b842bad533ecc6709b Mon Sep 17 00:00:00 2001 From: SashaMIT Date: Mon, 21 Sep 2026 14:04:31 +0700 Subject: [PATCH 07/30] fix(library): transient status lives in the footer Status text in the toolbar displaced the search and view controls whenever a message appeared. Move it to the statusbar, where it takes the spare middle space and truncates first, and drop the file name from the preview loading message. Co-authored-by: Cursor --- capsules/library/browser/index.html | 2 +- capsules/library/browser/library.css | 18 ++++++++---------- capsules/library/browser/src/preview.js | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/capsules/library/browser/index.html b/capsules/library/browser/index.html index 2f0ffa428..f0a72dc10 100644 --- a/capsules/library/browser/index.html +++ b/capsules/library/browser/index.html @@ -45,7 +45,6 @@

Favorites

-