feat(canvas): opt-in local cache for plugin Canvases (#473) - #479
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughLe changement ajoute un cache local optionnel pour les vidéos Canvas fournies par les plugins. Le backend expose sa gestion via Tauri. Le frontend ajoute les réglages, les traductions et la détection des plugins Canvas. La validation des URL et des redirections est renforcée. ChangesCache local des Canvas
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Application
participant PluginCanvas
participant motion_cache
participant InterfaceReglages
Application->>PluginCanvas: Résout le Canvas du morceau
PluginCanvas-->>Application: Retourne une URL MP4 distante
Application->>motion_cache: Valide et télécharge le MP4 si le cache est actif
motion_cache-->>Application: Retourne le chemin local ou l’URL distante
InterfaceReglages->>Application: Consulte ou modifie l’état du cache
Application-->>InterfaceReglages: Retourne l’activation et l’empreinte du cache
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/crates/app/src/commands/canvas.rs`:
- Around line 160-165: Persist a durable track/plugin-to-cached-file association
when the local cache stores a resolved MP4, then consult that association in the
canvas command before the offline early return around the offline check. In
offline mode, return the cached local path when a matching association exists;
otherwise preserve the existing None result, and keep the short-circuit before
any plugin invocation or HTTP download.
- Around line 225-245: Update the motion_cache::cache_mp4 download flow used by
the canvas URL handling to prevent unsafe redirects. Configure reqwest with a
custom redirect policy that validates every redirect destination via
is_safe_motion_url, or disable automatic redirects and handle each redirect
explicitly while applying the same validation; preserve the existing
cache-failure fallback to canvas.url.
In `@src/lib/tauri/plugins.ts`:
- Around line 617-623: Update the documentation for the PluginInfo.world field
to include the waveflow:canvas/v1 world, or accurately document the
waveflow:canvas prefix accepted by isCanvasPlugin. Keep the documented world
values aligned with the plugin worlds recognized by the interface.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4ddd5313-e7ab-44c9-8851-a3a10bba2ee7
📒 Files selected for processing (28)
CLAUDE.mddocs/features/plugins.mddocs/features/ui.mdsrc-tauri/crates/app/src/commands/canvas.rssrc-tauri/crates/app/src/lib.rssrc-tauri/crates/app/src/paths.rssrc-tauri/crates/app/tauri.conf.jsonsrc/components/views/settings/PluginOptions.tsxsrc/components/views/settings/PluginsCard.tsxsrc/i18n/locales/ar.jsonsrc/i18n/locales/de.jsonsrc/i18n/locales/en.jsonsrc/i18n/locales/es.jsonsrc/i18n/locales/fr.jsonsrc/i18n/locales/hi.jsonsrc/i18n/locales/id.jsonsrc/i18n/locales/it.jsonsrc/i18n/locales/ja.jsonsrc/i18n/locales/ko.jsonsrc/i18n/locales/nl.jsonsrc/i18n/locales/pt-BR.jsonsrc/i18n/locales/pt.jsonsrc/i18n/locales/ru.jsonsrc/i18n/locales/tr.jsonsrc/i18n/locales/zh-CN.jsonsrc/i18n/locales/zh-TW.jsonsrc/lib/tauri/canvas.tssrc/lib/tauri/plugins.ts
|
@coderabbitai verified each finding. Applied 2, skipped 1 — pushed in 10f904b. Applied
Skipped
Validation: |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
git show --stat --oneline 10f904b2
git show --format=fuller --find-renames --find-copies -- \
10f904b2 -- \
src-tauri/crates/app/src/commands/motion_cache.rs \
src/lib/tauri/plugins.ts
motion_cache_file="$(fd -t f '^motion_cache\.rs$' src-tauri)"
motion_artwork_file="$(fd -t f '^motion_artwork\.rs$' src-tauri)"
plugins_file="src/lib/tauri/plugins.ts"
printf '\n== motion_cache outline ==\n'
ast-grep outline "$motion_cache_file" --items all --type function --match 'cache_mp4|is_safe_motion_url' || true
printf '\n== redirect and safe-url implementation ==\n'
rg -n -C 18 'fn is_safe_motion_url|async fn cache_mp4|redirect::Policy|attempt\.url|previous\(\)' "$motion_cache_file"
printf '\n== offline behavior ==\n'
rg -n -C 12 'offline::is_offline|fetch_track_motion_artwork|cache_mp4' "$motion_artwork_file"
printf '\n== PluginInfo world documentation ==\n'
rg -n -C 6 'interface PluginInfo|world:|waveflow:canvas|isCanvasPlugin' "$plugins_file"Length of output: 12569
Les validations signalées sont suffisantes pour ces changements. Je n’ai pas exécuté les commandes de compilation dans cet environnement. Souhaitez-vous que je crée une issue GitHub de suivi pour l’association durable nécessaire à la lecture hors ligne des caches Canvas et motion-artwork ? ✏️ Learnings added
You are interacting with an AI system. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 126-129: Update the redirect validation around is_safe_motion_url
in motion-cache handling to normalize hostnames by removing trailing dots before
applying safety checks, and reject targets whose resolved addresses are loopback
or otherwise internal at every redirect hop. Ensure the fallback to the remote
URL cannot expose an unsafe target, and add coverage for trailing-dot localhost
and 127.0.0.1 variants.
- Around line 123-126: In the redirect policy closure, update the
previous-attempt threshold in the visible redirect handling logic from >= 10 to
> 10 so exactly ten redirects are permitted while larger counts remain rejected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a1f739db-539f-4321-879f-1aa85d19c355
📒 Files selected for processing (2)
src-tauri/crates/core/src/artwork/motion_cache.rssrc/lib/tauri/plugins.ts
|
@coderabbitai both applied — pushed.
Skipped the "resolve each hop's address and reject internal resolutions" part: that's DNS resolution (DNS-rebinding), which the guard's doc explicitly calls out as an accepted residual ("a hostname that resolves to an internal IP at connect time … is a residual we accept — plugins are curated and user-installed"). Adding per-hop DNS resolution is a larger change beyond this PR's scope and a deliberate existing trade-off; the literal + scheme + trailing-dot checks cover the practical case. Validation: |
|
Tip For best results, initiate chat on the files or code changes.
Je n’insiste pas sur la résolution DNS par saut. Le risque de DNS rebinding reste documenté et accepté pour les plugins sélectionnés ou installés par l’utilisateur. ✏️ Learnings added
You are interacting with an AI system. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src-tauri/crates/core/src/artwork/motion_cache.rs (2)
122-138: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBloquez les requêtes HTTP en mode hors ligne.
cache_mp4peut appelersend()sans vérifieroffline::is_offline(). Conservez le retour du fichier déjà en cache, puis quittez avant de créer le client HTTP lorsqu’aucun cache n’est disponible.Correction proposée
if path.exists() { // ... return Ok(path); } + if offline::is_offline() { + return Err("offline".to_owned()); + } + // Follow redirects, but re-validate EVERY hop against the SSRF guardAs per coding guidelines, chaque chemin HTTP sortant dans
src-tauri/crates/core/src/**/*.rsdoit vérifieroffline::is_offline()avant la requête et retourner un résultat en cache ou vide.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 122 - 138, Update cache_mp4 to check offline::is_offline() after attempting to retrieve the cached file and before constructing the HTTP client; return the cached file when available, otherwise return the existing empty result without calling send().Source: Coding guidelines
122-138: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winValidez l’URL initiale dans
cache_mp4.Les deux appelants actuels effectuent ce contrôle, mais
cache_mp4est publique. Ajoutezis_safe_motion_url(url)avant toute requête réseau, puis conservez la validation de chaque redirection.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 122 - 138, Validate the initial URL at the start of the public cache_mp4 flow by calling is_safe_motion_url(url) before constructing or using the HTTP client, and reject unsafe URLs without any network request. Preserve the existing per-hop validation in the redirect policy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 122-138: Update cache_mp4 to check offline::is_offline() after
attempting to retrieve the cached file and before constructing the HTTP client;
return the cached file when available, otherwise return the existing empty
result without calling send().
- Around line 122-138: Validate the initial URL at the start of the public
cache_mp4 flow by calling is_safe_motion_url(url) before constructing or using
the HTTP client, and reject unsafe URLs without any network request. Preserve
the existing per-hop validation in the redirect policy.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2c730bf3-d1f1-4cd2-9252-de7f03de802b
📒 Files selected for processing (1)
src-tauri/crates/core/src/artwork/motion_cache.rs
|
@coderabbitai one applied, one skipped.
Validation: |
|
Tip For best results, initiate chat on the files or code changes.
Je retire la demande de contrôle Les validations ciblées et ✏️ Learnings added
You are interacting with an AI system. |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src-tauri/crates/core/src/artwork/motion_cache.rs (1)
129-145: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftNe repliez pas vers l’URL distante après un rejet de redirection.
cache_mp4valide les redirections, maisfetch_track_canvasrenvoiecanvas.urlsi le cache est désactivé ou si le téléchargement échoue.CanvasStagetransmet alors cette URL à<video>, qui suit ses redirections. Une URL HTTPS publique peut ainsi atteindre127.0.0.1, un réseau privé ou HTTP.Séparez le rejet de sécurité des erreurs ordinaires. Dans ce cas, ignorez le plugin sans renvoyer l’URL distante. Lorsque le cache est désactivé, utilisez un proxy contrôlé ou un autre chemin qui valide chaque redirection. Ajoutez un test public → loopback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 129 - 145, Update fetch_track_canvas and its cache_mp4 failure handling so a rejected or unsafe redirect is treated as a security failure: skip the plugin and never return canvas.url to CanvasStage. Keep ordinary download failures distinct, and when caching is disabled route the media through a controlled path that validates every redirect instead of handing the remote URL to <video>. Add a public-URL-to-loopback redirect test covering this behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 342-360: Make cache_mp4_refuses_unsafe_url_before_network assert
that both returned errors begin with “refusing unsafe url” rather than only
checking is_err(), proving the in-function guard rejected each URL. Also verify
the temporary cache directory remains free of created files after both calls.
- Around line 109-115: Update the unsafe-URL error returned by the motion-cache
validation in cache_mp4 so it does not interpolate the full url or expose query,
fragment, or userinfo; return a generic safe message instead. Audit the other
errors in the same URL-fetching flow that interpolate url and apply the same
redaction rule, while preserving the existing rejection behavior.
---
Outside diff comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 129-145: Update fetch_track_canvas and its cache_mp4 failure
handling so a rejected or unsafe redirect is treated as a security failure: skip
the plugin and never return canvas.url to CanvasStage. Keep ordinary download
failures distinct, and when caching is disabled route the media through a
controlled path that validates every redirect instead of handing the remote URL
to <video>. Add a public-URL-to-loopback redirect test covering this behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 536092c0-b6fc-4bf1-87fa-cd6aeeb20705
📒 Files selected for processing (1)
src-tauri/crates/core/src/artwork/motion_cache.rs
CodeRabbit round on #479. Three hardening fixes to the shared motion/canvas mp4 cache: - `cache_mp4` now returns a typed `CacheError { UnsafeUrl, Other }` instead of a bare String. An `UnsafeUrl` (initial url or a redirect hop failing the SSRF guard, or the redirect chain aborting) is a security rejection; `Other` is an ordinary failure (network / HTTP / oversize / disk). Previously both callers fell back to streaming the raw remote url on ANY cache error — so an unsafe redirect the per-hop guard refused was handed straight to the webview `<video>`, which would follow it unchecked, fully defeating the guard. Both `fetch_track_canvas` and the motion-cover fanout now skip the plugin on an `UnsafeUrl` and only degrade to streaming on an `Other` failure. - Redact plugin-supplied urls in logged error strings (drop userinfo / query / fragment via `redact_url`) so a signed-token CDN url can't leak into logs; the `UnsafeUrl` message interpolates no url at all. - Strengthen `cache_mp4_refuses_unsafe_url_before_network` to assert the typed `UnsafeUrl` variant and that no file is staged before the rejection; add a `redact_url` unit test. Cache-off still streams the (initial-validated) remote url to `<video>`: a redirect after that point is the same documented accepted residual as DNS rebinding (initial url validated, `<video>` can't read cross-origin bodies, plugins are curated/user-installed).
|
@coderabbitai Traité dans Finding — unsafe redirect must be a security failure (skip, never stream the raw url) [FIXED] Finding — redact url in error strings [FIXED] Finding — strengthen the guard test [FIXED] Declined — cache-OFF hands the remote url to |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
git show --stat --oneline 8d87e8c2
git show --format=fuller --find-renames --find-copies 8d87e8c2 -- \
src-tauri/crates/core/src/artwork/motion_cache.rs \
src-tauri/crates/app/src/commands/canvas.rs \
src-tauri/crates/app/src/commands/motion_artwork.rs
cache_file="src-tauri/crates/core/src/artwork/motion_cache.rs"
canvas_file="src-tauri/crates/app/src/commands/canvas.rs"
motion_file="src-tauri/crates/app/src/commands/motion_artwork.rs"
printf '\n== cache module outline ==\n'
ast-grep outline "$cache_file" --items all --type function,struct,enum --match 'CacheError|cache_mp4|redact_url|is_safe_motion_url' || true
printf '\n== cache module implementation and tests ==\n'
rg -n -C 18 'enum CacheError|fn redact_url|async fn cache_mp4|is_safe_motion_url|is_redirect|cache_mp4_refuses_unsafe_url_before_network|redact' "$cache_file"
printf '\n== Canvas cache-error handling ==\n'
rg -n -C 16 'cache_mp4|CacheError|UnsafeUrl|fetch_track_canvas|canvas\.url' "$canvas_file"
printf '\n== motion-artwork cache-error handling ==\n'
rg -n -C 16 'cache_mp4|CacheError|UnsafeUrl|fetch_album_motion_artwork|motion' "$motion_file"Length of output: 54223 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '== resolved reqwest dependency ==\n'
rg -n -C 2 '^name = "reqwest"$|^version = ' Cargo.lock src-tauri/Cargo.lock 2>/dev/null || true
rg -n -C 3 'reqwest' src-tauri/Cargo.toml src-tauri/crates/**/Cargo.toml 2>/dev/null || true
printf '\n== locally available reqwest Error display implementation ==\n'
for root in "${CARGO_HOME:-$HOME/.cargo}/registry/src" /usr/local/cargo/registry/src; do
if [ -d "$root" ]; then
fd -t f '^error\.rs$' "$root" -p | while IFS= read -r f; do
if rg -q 'pub struct Error|impl.*Display.*Error' "$f"; then
printf '\n-- %s --\n' "$f"
rg -n -C 12 'impl.*Display.*Error|fmt\(&self|with_url|url:' "$f" || true
fi
done
fi
doneLength of output: 6244 🤖 web_search executed:
Length of output: 20848
Je maintiens toutefois le constat sur la redaction des URL. Dans format!("download {}: {e}", redact_url(url))
Il faut retirer l’URL attachée de l’erreur avant son formatage, puis conserver seulement la version produite par Le comportement cache désactivé reste le risque résiduel documenté. Je ne maintiens pas de constat supplémentaire sur ce point. You are interacting with an AI system. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src-tauri/crates/core/src/artwork/motion_cache.rs (1)
384-448: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAjoutez un test de régression pour la limite de dix redirections.
Les nouveaux tests couvrent bien les hôtes avec point final, le rejet pré-réseau typé, et
redact_url. Ils ne couvrent pas la limite exacte de dix redirections déjà corrigée deux fois par le passé (>= 10puis> 10). Un test avec un serveur HTTP local simulé (par exemplewiremockoumockito) qui enchaîne dix redirections puis vérifie le rejet de la onzième éviterait une régression silencieuse sur cette limite.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src-tauri/crates/core/src/artwork/motion_cache.rs` around lines 384 - 448, Ajoutez un test de régression autour de la logique de redirection de cache_mp4, en utilisant un serveur HTTP local simulé pour enchaîner exactement dix redirections puis vérifier que la onzième est rejetée. Le test doit confirmer que la limite autorise dix redirections mais refuse toute redirection supplémentaire, sans effectuer de requête réseau externe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src-tauri/crates/core/src/artwork/motion_cache.rs`:
- Around line 384-448: Ajoutez un test de régression autour de la logique de
redirection de cache_mp4, en utilisant un serveur HTTP local simulé pour
enchaîner exactement dix redirections puis vérifier que la onzième est rejetée.
Le test doit confirmer que la limite autorise dix redirections mais refuse toute
redirection supplémentaire, sans effectuer de requête réseau externe.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4e07e270-f038-4f4f-b254-17c89daa6473
📒 Files selected for processing (3)
src-tauri/crates/app/src/commands/canvas.rssrc-tauri/crates/app/src/commands/motion_artwork.rssrc-tauri/crates/core/src/artwork/motion_cache.rs
CodeRabbit round on #479. Extract the redirect-policy decision into a pure `redirect_decision(previous_hops, url)` -> `RedirectDecision` and unit-test the "10 hops allowed, 11th rejected" boundary + per-hop SSRF re-validation. A live mock-server test is infeasible here: `is_safe_motion_url` refuses loopback, so every hop to a 127.0.0.1 mock is rejected as unsafe before the count matters (and the initial request to it is refused up front). Splitting the decision out makes the limit deterministically testable with no network.
|
@coderabbitai Traité dans Finding — regression-test the redirect hop limit [FIXED, adapted] À la place j'ai extrait la décision de la closure en fonction pure |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
git show --stat --oneline b5beee66
git show --format=fuller --find-renames --find-copies b5beee66 -- \
src-tauri/crates/core/src/artwork/motion_cache.rs
cache_file="src-tauri/crates/core/src/artwork/motion_cache.rs"
printf '\n== symbol outline ==\n'
ast-grep outline "$cache_file" --items all --type function,enum --match 'redirect_decision|RedirectDecision|cache_mp4|is_safe_motion_url' || true
printf '\n== redirect production path and tests ==\n'
rg -n -C 18 'RedirectDecision|redirect_decision|redirect::Policy|previous\(\)|TooMany|Unsafe|Follow|redirect.*hop' "$cache_file"Length of output: 17629
Le test confirme les limites requises :
La solution sans serveur mock est adaptée. Le constat est traité. You are interacting with an AI system. |
Follow-up to #478 (the
waveflow:canvas/v1world). Adds an opt-in local cache for plugin-sourced Canvases — mirroring the motion-artwork cache — so a Canvas can play offline and without re-streaming the CDN mp4 on every replay.By default a plugin Canvas is a remote URL the webview streams; nothing is written to disk. With the cache on,
fetch_track_canvasdownloads the resolved mp4 into an app-wide LRU and returns the on-disk path instead.Backend
paths.rs:canvas_cache_dir(<app-data>/waveflow/canvas_cache/) — a separate dir frommotion_cacheso the two caches size/evict/clear independently.commands/canvas.rs:fetch_track_canvasreadsapp_setting['canvas.cache_enabled'](default OFF) and, when on, downloads the safe URL viamotion_cache::cache_mp4(fallback to the remote URL if the download fails). Addsget_canvas_cache_info/set_canvas_cache_enabled/clear_canvas_cache, reusing the sharedmotion_cacheprimitives (download + LRU eviction + SSRF guard) — no new cache engine.tauri.conf.json: asset-protocol scopecanvas_cache/**soconvertFileSrcresolves the local path.Frontend
lib/tauri/canvas.ts:CanvasCacheInfo+getCanvasCacheInfo/setCanvasCacheEnabled/clearCanvasCache.lib/tauri/plugins.ts:isCanvasPluginpredicate (mirrorsisMetadataPlugin).PluginOptions.tsx: extracted a sharedLocalCacheOptioncomponent (toggle + footprint + clear) and reused it for both the motion-artwork cache and the new Canvas cache — no duplicated 130-line component. Rendered for canvas-world plugins.PluginsCard.tsx: the ⚙️ gear now shows for canvas-world plugins too.settings.canvasCache.*across all 17 locales (reuses each locale's existingclear/clearConfirm).CanvasStagealready tells a local path from a remote URL (theisRemoteCanvasUrlhelper from #478), so no renderer change was needed.Docs
CLAUDE.md(canvas clause),docs/features/plugins.md+docs/features/ui.md(opt-in cache paragraphs).Validation
cargo check -p waveflow+cargo clippy -p waveflow --all-targets— cleanbun run typecheck/bun run lint— cleanSummary by CodeRabbit
Nouvelles fonctionnalités
Améliorations
Documentation