feat(themes): custom loader art for installed packs (images, SVG, animated) - #9493
Conversation
|
Intent: Let an installed theme pack supply the chat loader's art and motion — its own carousel images, or a fully sandboxed custom loader — without running untrusted pack content in the dashboard's trusted window. |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo findings. CANDIDATE 1 ( [OPUS-REVIEWED] 55a19b1 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSDesign-level review of The asset route serves any containment-checked file in the theme dir, so Design-Verdict: PASS Extends an existing, verified trust boundary (asset CSP + [DESIGN-REVIEWED] 55a19b1 |
UX Review (Fable 5) — ✅ PASSUX-level review of Reconciliation complete. The diff adds no new controls, strings, or settings — only new artwork sources for the existing passive chat-loader band (one pack image solo, 2–8 through the existing carousel). The committed screenshot shows both states; the blind reader correctly identified the left strip as "a preview of the app's busy / please wait spinner while the assistant is working" and the right strip's shapes as "sample frames of that flipping animation," and recognized the two strips as the same waiting element. Its jargon confusion ("stock carousel," "loader band") is about the demo card's caption text, which is a committed screenshot artifact, not shipped UI. No lens-13 transition exists (loader appearance is async lifecycle), so no recording is required. The one gap I found: an UX-Verdict: PASS Passive loader artwork riding the existing carousel — no new controls or copy, and the blind reader correctly identified both states as the busy indicator. Suggestions
[UX-REVIEWED] 55a19b1 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All verification is done. The asset route serves any classified file under CSP + nosniff (consistent with the description's safety claims), First-Principles-Verdict: CONCERNS The headline capability rests on asserted demand — "pack authors want their own mark", no linked issue, no named request; the description admits it. Not justified as shipped
What this change shipsInventory (10 items) — 9 justifiedIntent: let an installed theme pack supply its own loader artwork and animation — an ADDITION.
WatchThe whole feature's provenance is one sentence of asserted demand; if no pack author ever ships SubtractionsDrop the [FIRST-PRINCIPLES-REVIEWED] 55a19b1 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
cdb7377 to
5e5193a
Compare
|
|
|
|
|
|
|
|
|
5e5193a to
ed20247
Compare
|
37b6a12 to
e5439b6
Compare
|
self-added: yes
|
|
self-added: yes
|
…mated) Installed theme packs can supply their own chat-footer loader art via loader/*.png|webp|gif|svg. One image renders on its own; 2-8 are cycled by the stock carousel. Animated WebP/APNG/GIF and animated SVG self-animate in the <img>, so a pack can ship a single fully-authored loop. SVG rides the same <img>-secure-mode asset path as logo.svg (served under _THEME_ASSET_CSP: default-src 'none'; sandbox), so scripts are disabled and external references are not fetched by the browser -- no HTML-serving route, no content sanitizer, no egress surface.
e5439b6 to
55a19b1
Compare
|
|
|
|
|
|
|
|
|
|
bolichen97
left a comment
There was a problem hiding this comment.
Approving after a targeted security pass on the loader-art seam at 55a19b1. Verified: loader art is always a same-origin <img> to /api/theme/<slug>/assets/<file> (ChatFooter.tsx ~L103-147, ~L319-326) — never inline SVG, never data:, never CSS url() — so SVG is rendered in the browser's image mode where scripts, on*, <foreignObject>, external <image>/<use>/@import and <a> are inert; direct navigation to the asset URL gets Content-Security-Policy: default-src 'none'; sandbox + nosniff (handlers/themes.py ~L826-832), the same posture branding/logo.svg already has. Validation is server-side: extension allowlist png/webp/gif/svg, 256 KB per file, ≤8 files, _validate_theme_dir rejects symlinks/escapes, _resolve_theme_asset + O_NOFOLLOW at serve time; the URL suffix is generated from real filenames so a pack cannot point off-origin.
Non-blocking follow-ups, in severity order:
- No image-dimension cap — a ≤256 KB PNG/GIF/WebP decompression bomb can exhaust renderer memory in the footer (
theme_validate.py~L313, ~L1167-1180). A header-based dimension parse would close it. - No test asserts a script-bearing
loader/x.svgis accepted-but-inert and served with the asset CSP; extendtest/test_dashboard_themes_coverage.py~L1267-1283 to aloader/path. builtin_skills/theme-pack-authoring/SKILL.mdL53 still says packs cannot ship loader SVG — now misleading; please update in a follow-up.temp-screenshots/custom-loader/loader-band-demo.pngis committed into the tree; the repo keeps screenshots in the PR body, not in git — please drop it before merge if you get to it, otherwise a follow-up.loaderImagesbypasses the client-sidesafeAssetPath()branding uses (ChatFooter.tsxL135/L147 vsuseTheme.tsxL325/L335); safe under the literal prefix, but worth aligning.
Problem / Motivation
An installed theme pack can restyle almost everything, but not the chat loading
indicator. PR #7650 let a pack
pick the loader icons — but only 4–8 names from a fixed 8-symbol allowlist. A pack
author cannot ship their own icon art, and cannot ship their own loader animation.
The full custom-loader seam from PR #894
(
loader) is reachable only from themes compiled into the app, never from aninstalled
theme.jsonpack.Why it matters
Themes are a supported extension surface. The loader is the one spot a user stares
at while they wait, so it is where a pack's identity matters most — and it is the
one spot a pack still can't touch beyond eight stock icons. Pack authors want their
own mark and their own motion there without editing app source.
What changed (motivation → approach → change)
The goal is to let an installed pack supply the loader art and motion, without
letting untrusted pack content run in the dashboard's trusted window.
So a pack ships its own loader images —
loader/*.png,.webp,.gif, or.svg. Ship one and it renders on its own; ship 2–8 and the stock carousel cyclesthem. Animated WebP/APNG/GIF and animated SVG self-animate inside the
<img>, so asingle fully-authored loop is a first-class loader.
The key safety point is that nothing new is served as a document. Every loader
image goes through the ordinary theme-asset route, under
_THEME_ASSET_CSP(
default-src 'none'; sandbox) with a strict Content-Type +nosniff, and isreferenced only as an
<img>. SVG is safe the same waylogo.svgalready is: an<img>-loaded SVG runs in the browser's secure static/animated mode — scriptsdisabled, external references not fetched — while its SMIL/CSS animation still
plays. So there is no HTML-serving route, no content sanitizer, and no
navigation/DNS/beacon egress surface: the browser sandbox is the boundary.
The backend classifies
loader/images, validates the count (1–8) and byte caps,and surfaces
loaderImagesin the theme descriptor. The frontendresolveLoaderrenders one image on its own or feeds 2–8 to the existing carousel; the default
render (the Kiro ghost poses) is unchanged.
flowchart LR A[turn running]:::ctx --> R{resolveLoader}:::changed R --> L1[compiled loader]:::ctx R --> L2[pack images: 1 solo / 2-8 cycled]:::added R --> L3[loaderIcons names]:::ctx R --> L4[default ghost poses]:::ctx classDef added fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px classDef changed fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px classDef ctx fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E linkStyle 1 stroke:#16A34A,stroke-width:2px🟩 added · 🟨 changed · 🟦 unchanged
An installed pack can now win the loader with its own images — a single self-animating file, or 2–8 cycled — checked before the stock icons.
Tests
test/test_theme_install.py— pack loader images are accepted and described(
loaderImages); a single image is accepted;.svgand.gifframes areaccepted; a count over 8 (9, 12) is rejected at install.
website/src/test/ChatFooter.test.tsx— a single pack image resolves to a solo<img>loader at the correct/api/theme/{slug}/assets/...URL; multiple packimages resolve to the cycled carousel; raster images resolve to
<img>icons.Manual verification
Frontend gates run locally:
tsc -bis clean, all 49ChatFootertests pass, andi18n:checkis green. The default loader render is unchanged — the new path appearsonly when an installed pack ships
loader/images. The authenticated dashboardcan't be driven from this environment, so the loader band is shown as a standalone
render below; the backend suite and the AI review lanes run on CI.
Screenshots / video
Standalone render of the loader band (the real authed dashboard cannot be driven
from the build environment):
Related Issues
no linked issue: extends the loader seam from #894 / #7650; no tracked issue.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)