WIP: add release radar plugin#312
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces WaveFlow’s first cut of a UI plugin world (waveflow:ui/v1) and ships a bundled Release Radar UI plugin that renders into the host via JSON view descriptors, extending the plugin permission model to allow a redacted library-artist snapshot for UI plugins.
Changes:
- Added a new UI plugin invocation surface end-to-end (WIT world, core runtime helpers, Tauri commands, TS invoke wrappers).
- Added a generic React
PluginUIViewrenderer for plugin-provided JSON descriptors and wired a “Release Radar” sidebar view. - Bundled a new
release-radarwasm plugin + manifest and introducedlibrary.read_artistspermission plumbing.
Reviewed changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/index.ts | Adds release-radar to the routed ViewId union. |
| src/lib/tauri/plugins.ts | Adds TS-facing UI plugin invoke wrappers + UI descriptor types. |
| src/components/views/settings/PluginsCard.tsx | Displays the new libraryReadArtists permission as a chip. |
| src/components/views/PluginUIView.tsx | New generic React renderer for plugin JSON view descriptors + actions. |
| src/components/layout/Sidebar.tsx | Adds conditional “Release Radar” sidebar entry based on plugin availability. |
| src/components/layout/AppLayout.tsx | Adds release-radar route and lazy-loads PluginUIView. |
| src-tauri/resources/plugins/release-radar/manifest.toml | Bundled plugin manifest for the installer resource tree. |
| src-tauri/plugins/release-radar/wit/world.wit | Plugin-local UI world definition used by the Release Radar component. |
| src-tauri/plugins/release-radar/wit/deps/waveflow-host/host.wit | Plugin-local vendored host WIT including library.list-artists. |
| src-tauri/plugins/release-radar/src/lib.rs | Release Radar plugin implementation (cache + MusicBrainz/Cover Art queries + JSON descriptors). |
| src-tauri/plugins/release-radar/src/bindings.rs | Generated WIT bindings for the Release Radar plugin component. |
| src-tauri/plugins/release-radar/manifest.toml | Plugin manifest for building/packaging the Release Radar component. |
| src-tauri/plugins/release-radar/Cargo.toml | Standalone wasm-component crate setup for the Release Radar plugin. |
| src-tauri/plugins/release-radar/Cargo.lock | Locked dependencies for the standalone plugin crate. |
| src-tauri/plugins/release-radar/.gitignore | Ignores the plugin crate’s target/ outputs. |
| src-tauri/crates/plugin-sdk/wit/ui/plugin.wit | Defines the real UI extension world (manifest/render/on-event) + library import. |
| src-tauri/crates/plugin-sdk/wit/ui/deps/host/host.wit | Adds waveflow:host/library interface for UI plugins. |
| src-tauri/crates/plugin-sdk/src/lib.rs | Adds library.read_artists permission constant and recognition in is_known. |
| src-tauri/crates/core/tests/plugin_release_radar.rs | Smoke test for waveflow:ui/v1 linkage using the bundled Release Radar wasm. |
| src-tauri/crates/core/src/plugin/runtime.rs | Adds LibraryArtist + UI instantiation helpers and store plumbing for artist snapshots. |
| src-tauri/crates/core/src/plugin/mod.rs | Bundles release-radar alongside web-radio. |
| src-tauri/crates/core/src/plugin/manifest.rs | Adds library_read_artists manifest permission field and validation hook. |
| src-tauri/crates/core/src/plugin/host_impl.rs | Adds permission bit + host implementation for waveflow:host/library.list-artists (UI bindings). |
| src-tauri/crates/core/src/plugin/bindings.rs | Adds wasmtime bindgen module for the UI plugin world. |
| src-tauri/crates/app/tauri.conf.json | Bundles Release Radar wasm + manifest as app resources. |
| src-tauri/crates/app/src/lib.rs | Registers new Tauri commands: plugin_ui_manifest/render/event. |
| src-tauri/crates/app/src/commands/plugins.rs | Implements UI plugin commands + SQL-backed artist snapshot loader. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const runAction = async (action: PluginUiAction) => { | ||
| if (action.kind === "open-url" && action.url) { | ||
| await openUrl(action.url); | ||
| return; | ||
| } |
| {busyAction?.startsWith(action.event ?? "") ? ( | ||
| <Loader2 size={15} className="animate-spin" /> | ||
| ) : ( | ||
| <RefreshCw size={15} /> | ||
| )} |
| interface library { | ||
| record artist { | ||
| id: u64, | ||
| name: string, | ||
| track-count: u32, | ||
| } | ||
|
|
||
| /// Redacted active-profile artist snapshot. The host sorts by | ||
| /// track-count descending and clamps the result to a safe maximum. | ||
| /// Gated by manifest permission `library.read_artists`. | ||
| list-artists: func(limit: u32) -> result<list<artist>, string>; | ||
| } |
There was a problem hiding this comment.
Actionable comments posted: 9
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/plugin/host_impl.rs (1)
331-418: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFactorisez les politiques
source/uiavant qu’elles ne divergent.Les implémentations HTTP/log/storage des deux mondes recopient la même logique offline, allowlist, quota et taille max. La prochaine correction de politique a de fortes chances d’être appliquée à un seul côté.
Also applies to: 422-478, 482-590
🤖 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/plugin/host_impl.rs` around lines 331 - 418, The HTTP policy logic in HostCtx::send is duplicated across the source/ui paths, so future offline/allowlist/quota/body-size changes can drift; factor the shared behavior into a single helper used by source_wit_host::http::Host::send and the other affected HTTP/log/storage handlers. Keep the existing ordering and semantics intact, but move the repeated checks and response/error mapping into a common policy layer or reusable function so both worlds stay aligned.
🤖 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/plugins.rs`:
- Around line 780-781: `plugin_ui_render` / `plugin_ui_event` load the artist
snapshot unconditionally, which still requires an active profile and reads user
library data even when the plugin manifest lacks
`permissions.library_read_artists`. Update the flow around `ui_preamble` and
`load_library_artist_snapshot` so `artists` is only fetched when
`permissions.library_read_artists` is declared on the plugin manifest; otherwise
initialize it with `Vec::new()` before passing it to the UI. Make the same
permission-gated change at both call sites so `require_profile_pool()` /
`require_profile_id` are not exercised for plugins without that permission.
In `@src-tauri/crates/core/src/plugin/host_impl.rs`:
- Around line 592-610: The HostCtx::list_artists implementation currently relies
on the existing order of library_artists instead of enforcing the host.wit
contract. Update this method to sort the returned artists by track_count in
descending order before applying the limit, so the ui_wit_host::library::Host
implementation always matches the promised ordering even if the upstream data is
unsorted.
In `@src-tauri/crates/core/src/plugin/manifest.rs`:
- Around line 84-87: The permissions parser currently accepts unknown keys
silently, so typos or unsupported future permissions in the [permissions] block
bypass validation. Update the permissions type used by Manifest/toml::from_str
to reject extra fields at parse time, e.g. by enforcing unknown-field denial on
the struct that contains library_read_artists and the other permissions flags,
so validate() only runs after malformed permission keys have already failed.
In `@src-tauri/plugins/release-radar/src/lib.rs`:
- Around line 54-56: `last_updated_at` is being set from the current render time
instead of the actual data timestamp, which makes stale cached results look
fresh. Update the release-radar descriptor flow in `read_cache`/the response
builder so `last_updated_at` comes from the served data’s real refresh time (or
preserved cache metadata) rather than `now_ms()`, especially when a refresh
fails and stale cache is returned.
- Around line 53-76: The synchronous refresh path in render_descriptor currently
blocks rendering by calling refresh_releases during the cache-miss/TTL check,
which can stall the UI. Refactor render_descriptor so it returns the cached or
placeholder descriptor immediately, and move the network refresh work out of the
render() / "refresh" synchronous path into an asynchronous or background update
flow. Keep the cache handling and status labeling in render_descriptor, but make
refresh_releases, write_json, and the retry/sleep sequence run without blocking
the initial render.
In `@src/components/layout/AppLayout.tsx`:
- Around line 458-459: The release-radar route in AppLayout still renders
PluginUIView even after the plugin is disabled, so add a fallback in the routing
logic for the "release-radar" case. Update the AppLayout route handling to
detect when the plugin availability changes to false and redirect or replace the
current entry with "home" instead of keeping PluginUIView mounted, using the
existing plugin-state/navigation flow around PluginUIView and the route switch.
In `@src/components/views/PluginUIView.tsx`:
- Around line 61-64: Do not flatten descriptor sections into a single items
array in PluginUIView, because that loses section grouping and can create React
key collisions when different sections share the same item.id. Update the
rendering logic around the items/sections mapping so each section is rendered
separately, preserving section.title, and if any keyed list remains, compose the
key with the section identity instead of relying on item.id alone.
- Around line 42-45: The open-url branch in runAction currently returns before
the shared try/catch, so failures from openUrl(action.url) bypass the existing
error handling. Move the open-url execution inside the same try/catch used for
other PluginUIView actions, or wrap that branch with equivalent error handling,
so openUrl() rejections are caught and the user gets the same error banner
behavior as the other actions.
In `@src/lib/tauri/plugins.ts`:
- Around line 186-188: The plugin UI descriptor parsing currently trusts
JSON.parse(raw) as PluginUiDescriptor, which can let malformed payloads reach
the frontend and fail later in PluginUIView. Update parsePluginUiDescriptor to
validate or normalize the decoded object before returning it, especially for
sections and actions, so invalid plugin payloads fail at the bridge with a
controlled error instead of causing downstream .flatMap()/.map() crashes. Keep
the frontend/backend shape aligned by enforcing the expected PluginUiDescriptor
fields in this helper.
---
Outside diff comments:
In `@src-tauri/crates/core/src/plugin/host_impl.rs`:
- Around line 331-418: The HTTP policy logic in HostCtx::send is duplicated
across the source/ui paths, so future offline/allowlist/quota/body-size changes
can drift; factor the shared behavior into a single helper used by
source_wit_host::http::Host::send and the other affected HTTP/log/storage
handlers. Keep the existing ordering and semantics intact, but move the repeated
checks and response/error mapping into a common policy layer or reusable
function so both worlds stay aligned.
🪄 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: 2115433b-a3ef-465f-91d4-9bc7d2ffb842
⛔ Files ignored due to path filters (2)
src-tauri/plugins/release-radar/Cargo.lockis excluded by!**/*.locksrc-tauri/resources/plugins/release-radar/plugin.wasmis excluded by!**/*.wasm
📒 Files selected for processing (26)
src-tauri/crates/app/src/commands/plugins.rssrc-tauri/crates/app/src/lib.rssrc-tauri/crates/app/tauri.conf.jsonsrc-tauri/crates/core/src/plugin/bindings.rssrc-tauri/crates/core/src/plugin/host_impl.rssrc-tauri/crates/core/src/plugin/manifest.rssrc-tauri/crates/core/src/plugin/mod.rssrc-tauri/crates/core/src/plugin/runtime.rssrc-tauri/crates/core/tests/plugin_release_radar.rssrc-tauri/crates/plugin-sdk/src/lib.rssrc-tauri/crates/plugin-sdk/wit/ui/deps/host/host.witsrc-tauri/crates/plugin-sdk/wit/ui/plugin.witsrc-tauri/plugins/release-radar/.gitignoresrc-tauri/plugins/release-radar/Cargo.tomlsrc-tauri/plugins/release-radar/manifest.tomlsrc-tauri/plugins/release-radar/src/bindings.rssrc-tauri/plugins/release-radar/src/lib.rssrc-tauri/plugins/release-radar/wit/deps/waveflow-host/host.witsrc-tauri/plugins/release-radar/wit/world.witsrc-tauri/resources/plugins/release-radar/manifest.tomlsrc/components/layout/AppLayout.tsxsrc/components/layout/Sidebar.tsxsrc/components/views/PluginUIView.tsxsrc/components/views/settings/PluginsCard.tsxsrc/lib/tauri/plugins.tssrc/types/index.ts
| let _guard = ui_preamble(&state, &plugin_id).await?; | ||
| let artists = load_library_artist_snapshot(&state, 200).await?; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Ne charge pas le snapshot artistes sans permission manifeste.
Ici, plugin_ui_render et plugin_ui_event lisent toujours la bibliothèque via require_profile_pool() avant que le runtime applique library_read_artists. Résultat : un plugin UI sans cette permission exige quand même un profil actif et déclenche une lecture de données utilisateur. Charge artists uniquement si le manifeste déclare permissions.library_read_artists, sinon passe Vec::new().
Correctif proposé
+async fn load_library_artist_snapshot_if_permitted(
+ state: &AppState,
+ plugin_id: &str,
+) -> AppResult<Vec<LibraryArtist>> {
+ let paths = state.paths.plugin_paths();
+ let manifest_path = paths
+ .manifest_path(plugin_id)
+ .map_err(|e| AppError::Other(format!("plugin {plugin_id}: {e}")))?;
+ let manifest = Manifest::load_from_path(&manifest_path)
+ .map_err(|e| AppError::Other(format!("plugin {plugin_id}: {e}")))?;
+
+ if !manifest.permissions.library_read_artists {
+ return Ok(Vec::new());
+ }
+
+ load_library_artist_snapshot(state, 200).await
+}
+
#[tauri::command]
pub async fn plugin_ui_render(
@@
- let artists = load_library_artist_snapshot(&state, 200).await?;
+ let artists = load_library_artist_snapshot_if_permitted(&state, &plugin_id).await?;
@@
- let artists = load_library_artist_snapshot(&state, 200).await?;
+ let artists = load_library_artist_snapshot_if_permitted(&state, &plugin_id).await?;As per path instructions, « Vérifie les contrôles de profil actif (require_profile_pool/require_profile_id), les accès SQLx, les erreurs retournées à l’UI ».
Also applies to: 800-801
🤖 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/app/src/commands/plugins.rs` around lines 780 - 781,
`plugin_ui_render` / `plugin_ui_event` load the artist snapshot unconditionally,
which still requires an active profile and reads user library data even when the
plugin manifest lacks `permissions.library_read_artists`. Update the flow around
`ui_preamble` and `load_library_artist_snapshot` so `artists` is only fetched
when `permissions.library_read_artists` is declared on the plugin manifest;
otherwise initialize it with `Vec::new()` before passing it to the UI. Make the
same permission-gated change at both call sites so `require_profile_pool()` /
`require_profile_id` are not exercised for plugins without that permission.
Source: Path instructions
| impl ui_wit_host::library::Host for HostCtx { | ||
| fn list_artists( | ||
| &mut self, | ||
| limit: u32, | ||
| ) -> wasmtime::Result<Result<Vec<ui_wit_host::library::Artist>, String>> { | ||
| if !self.permissions.library_read_artists { | ||
| return Ok(Err("permission denied: library.read_artists".into())); | ||
| } | ||
| let limit = limit.min(200) as usize; | ||
| Ok(Ok(self | ||
| .library_artists | ||
| .iter() | ||
| .take(limit) | ||
| .map(|a| ui_wit_host::library::Artist { | ||
| id: a.id, | ||
| name: a.name.clone(), | ||
| track_count: a.track_count, | ||
| }) | ||
| .collect())) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Garantissez ici l’ordre promis par host.wit.
La doc WIT annonce un tri décroissant par track-count, mais cette implémentation fait seulement take(limit) sur l’ordre injecté dans HostCtx. Si le chargement amont cesse d’être trié, les plugins UI recevront un snapshot hors contrat.
🧭 Correctif suggéré
if !self.permissions.library_read_artists {
return Ok(Err("permission denied: library.read_artists".into()));
}
let limit = limit.min(200) as usize;
- Ok(Ok(self
- .library_artists
- .iter()
+ let mut artists: Vec<_> = self.library_artists.iter().collect();
+ artists.sort_by(|a, b| {
+ b.track_count
+ .cmp(&a.track_count)
+ .then_with(|| a.name.cmp(&b.name))
+ });
+ Ok(Ok(artists
+ .into_iter()
.take(limit)
.map(|a| ui_wit_host::library::Artist {
id: a.id,
name: a.name.clone(),
track_count: a.track_count,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| impl ui_wit_host::library::Host for HostCtx { | |
| fn list_artists( | |
| &mut self, | |
| limit: u32, | |
| ) -> wasmtime::Result<Result<Vec<ui_wit_host::library::Artist>, String>> { | |
| if !self.permissions.library_read_artists { | |
| return Ok(Err("permission denied: library.read_artists".into())); | |
| } | |
| let limit = limit.min(200) as usize; | |
| Ok(Ok(self | |
| .library_artists | |
| .iter() | |
| .take(limit) | |
| .map(|a| ui_wit_host::library::Artist { | |
| id: a.id, | |
| name: a.name.clone(), | |
| track_count: a.track_count, | |
| }) | |
| .collect())) | |
| impl ui_wit_host::library::Host for HostCtx { | |
| fn list_artists( | |
| &mut self, | |
| limit: u32, | |
| ) -> wasmtime::Result<Result<Vec<ui_wit_host::library::Artist>, String>> { | |
| if !self.permissions.library_read_artists { | |
| return Ok(Err("permission denied: library.read_artists".into())); | |
| } | |
| let limit = limit.min(200) as usize; | |
| let mut artists: Vec<_> = self.library_artists.iter().collect(); | |
| artists.sort_by(|a, b| { | |
| b.track_count | |
| .cmp(&a.track_count) | |
| .then_with(|| a.name.cmp(&b.name)) | |
| }); | |
| Ok(Ok(artists | |
| .into_iter() | |
| .take(limit) | |
| .map(|a| ui_wit_host::library::Artist { | |
| id: a.id, | |
| name: a.name.clone(), | |
| track_count: a.track_count, | |
| }) | |
| .collect())) |
🤖 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/plugin/host_impl.rs` around lines 592 - 610, The
HostCtx::list_artists implementation currently relies on the existing order of
library_artists instead of enforcing the host.wit contract. Update this method
to sort the returned artists by track_count in descending order before applying
the limit, so the ui_wit_host::library::Host implementation always matches the
promised ordering even if the upstream data is unsorted.
| /// Whether the plugin can read a redacted artist snapshot from the | ||
| /// active profile. The host exposes names and aggregate counts only. | ||
| #[serde(default)] | ||
| pub library_read_artists: bool, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Refusez les permissions inconnues dès le parsing.
Avec le schéma actuel, toml::from_str ignore silencieusement les clés non mappées dans [permissions]. Une permission typoée ou future passera donc sans erreur, et ce bloc validate() ne pourra jamais la détecter, alors que le SDK documente l’inverse.
🛠️ Correctif suggéré
+#[serde(deny_unknown_fields)]
pub struct Permissions {Also applies to: 202-208
🤖 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/plugin/manifest.rs` around lines 84 - 87, The
permissions parser currently accepts unknown keys silently, so typos or
unsupported future permissions in the [permissions] block bypass validation.
Update the permissions type used by Manifest/toml::from_str to reject extra
fields at parse time, e.g. by enforcing unknown-field denial on the struct that
contains library_read_artists and the other permissions flags, so validate()
only runs after malformed permission keys have already failed.
| fn render_descriptor(force_refresh: bool) -> Result<String, String> { | ||
| let now = now_ms(); | ||
| let dismissed = read_dismissed(); | ||
| let mut cache = read_cache(); | ||
| let needs_refresh = force_refresh | ||
| || cache | ||
| .as_ref() | ||
| .map(|c| now.saturating_sub(c.last_refresh_at) > CACHE_TTL_MS) | ||
| .unwrap_or(true); | ||
|
|
||
| let mut status = "cached"; | ||
| if needs_refresh { | ||
| match refresh_releases(now) { | ||
| Ok(next) => { | ||
| write_json(CACHE_KEY, &next)?; | ||
| cache = Some(next); | ||
| status = "fresh"; | ||
| } | ||
| Err(err) => { | ||
| log::emit(Level::Warn, &format!("release radar refresh failed: {err}")); | ||
| status = if cache.is_some() { "stale" } else { "error" }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Évite le refresh réseau bloquant dans le chemin de rendu.
Ici, un premier affichage sans cache attend déjà au minimum 7 * 1.1s de sleep, puis 8 requêtes HTTP séquentielles. Comme ce chemin est appelé directement depuis render() et l’événement "refresh", l’ouverture de la vue peut rester bloquée plusieurs secondes côté UI. Il faut renvoyer le cache/placeholder immédiatement, puis rafraîchir hors du rendu synchrone.
Also applies to: 107-156
🤖 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/plugins/release-radar/src/lib.rs` around lines 53 - 76, The
synchronous refresh path in render_descriptor currently blocks rendering by
calling refresh_releases during the cache-miss/TTL check, which can stall the
UI. Refactor render_descriptor so it returns the cached or placeholder
descriptor immediately, and move the network refresh work out of the render() /
"refresh" synchronous path into an asynchronous or background update flow. Keep
the cache handling and status labeling in render_descriptor, but make
refresh_releases, write_json, and the retry/sleep sequence run without blocking
the initial render.
| let now = now_ms(); | ||
| let dismissed = read_dismissed(); | ||
| let mut cache = read_cache(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
last_updated_at doit refléter la donnée servie, pas l’heure du rendu.
Si le refresh échoue mais qu’un cache stale est renvoyé, le descripteur annonce quand même last_updated_at = now. L’UI verra donc une donnée “mise à jour maintenant” alors qu’elle date du dernier refresh réussi.
Correctif proposé
let now = now_ms();
let dismissed = read_dismissed();
let mut cache = read_cache();
@@
- let releases = cache.map(|c| c.releases).unwrap_or_default();
+ let last_updated_at = cache
+ .as_ref()
+ .map(|c| c.last_refresh_at)
+ .unwrap_or(now);
+ let releases = cache.map(|c| c.releases).unwrap_or_default();
@@
- last_updated_at: now,
+ last_updated_at,Also applies to: 90-95
🤖 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/plugins/release-radar/src/lib.rs` around lines 54 - 56,
`last_updated_at` is being set from the current render time instead of the
actual data timestamp, which makes stale cached results look fresh. Update the
release-radar descriptor flow in `read_cache`/the response builder so
`last_updated_at` comes from the served data’s real refresh time (or preserved
cache metadata) rather than `now_ms()`, especially when a refresh fails and
stale cache is returned.
| case "release-radar": | ||
| return <PluginUIView pluginId="release-radar" />; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Ajoutez un repli quand le plugin devient indisponible.
Cette route est rendue sans vérifier que "release-radar" est toujours installé/activé. Si l’utilisateur désactive le plugin depuis les réglages alors que cette vue est ouverte, PluginUIView reste montée et bascule sur une page cassée au prochain rendu/action. Faites un fallback vers "home" (ou remplacez l’entrée d’historique courante) quand la disponibilité passe à false. As per path instructions, src/**: vérifie les régressions de navigation/player.
🤖 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/components/layout/AppLayout.tsx` around lines 458 - 459, The
release-radar route in AppLayout still renders PluginUIView even after the
plugin is disabled, so add a fallback in the routing logic for the
"release-radar" case. Update the AppLayout route handling to detect when the
plugin availability changes to false and redirect or replace the current entry
with "home" instead of keeping PluginUIView mounted, using the existing
plugin-state/navigation flow around PluginUIView and the route switch.
Source: Path instructions
| const runAction = async (action: PluginUiAction) => { | ||
| if (action.kind === "open-url" && action.url) { | ||
| await openUrl(action.url); | ||
| return; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Capturez aussi les échecs de openUrl().
La branche open-url retourne avant le try/catch. Si l’ouverture système échoue, la promesse remonte en rejet non géré et l’utilisateur n’a pas le bandeau d’erreur affiché pour les autres actions.
🤖 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/components/views/PluginUIView.tsx` around lines 42 - 45, The open-url
branch in runAction currently returns before the shared try/catch, so failures
from openUrl(action.url) bypass the existing error handling. Move the open-url
execution inside the same try/catch used for other PluginUIView actions, or wrap
that branch with equivalent error handling, so openUrl() rejections are caught
and the user gets the same error banner behavior as the other actions.
| const items = | ||
| descriptor?.sections?.flatMap((section) => | ||
| section.items.map((item) => ({ section: section.title, item })), | ||
| ) ?? []; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Ne fusionnez pas les sections dans un tableau plat.
Ici, section.title est perdu et la clé React devient seulement item.id. Dès qu’un plugin renvoie plusieurs sections, les en-têtes disparaissent et deux items portant le même id dans des sections différentes entrent en collision. Rendez les sections séparément, ou composez au minimum la clé avec l’identité de la section.
Also applies to: 124-127
🤖 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/components/views/PluginUIView.tsx` around lines 61 - 64, Do not flatten
descriptor sections into a single items array in PluginUIView, because that
loses section grouping and can create React key collisions when different
sections share the same item.id. Update the rendering logic around the
items/sections mapping so each section is rendered separately, preserving
section.title, and if any keyed list remains, compose the key with the section
identity instead of relying on item.id alone.
| function parsePluginUiDescriptor(raw: string): PluginUiDescriptor { | ||
| return JSON.parse(raw) as PluginUiDescriptor; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validez le descripteur au décodage.
JSON.parse(raw) as PluginUiDescriptor laisse passer n’importe quelle forme à travers la frontière plugin → frontend. Un payload partiellement invalide (sections: {}, actions: null, etc.) plantera ensuite PluginUIView sur .flatMap() / .map() au lieu de remonter une erreur contrôlée depuis le bridge. Ajoutez une validation minimale ou normalisez les champs optionnels ici avant de retourner le résultat. As per path instructions, src/lib/tauri/**: vérifie que les types frontend/backend restent alignés et que les erreurs importantes ne sont pas masquées.
🤖 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/lib/tauri/plugins.ts` around lines 186 - 188, The plugin UI descriptor
parsing currently trusts JSON.parse(raw) as PluginUiDescriptor, which can let
malformed payloads reach the frontend and fail later in PluginUIView. Update
parsePluginUiDescriptor to validate or normalize the decoded object before
returning it, especially for sections and actions, so invalid plugin payloads
fail at the bridge with a controlled error instead of causing downstream
.flatMap()/.map() crashes. Keep the frontend/backend shape aligned by enforcing
the expected PluginUiDescriptor fields in this helper.
Source: Path instructions
Summary
waveflow:ui/v1plugin invocation surface and a redactedlibrary.read_artistshost permissionNotes
This intentionally does not add YouTube downloading, audio extraction, or hidden YouTube playback. Release Radar opens external search/listening links only.
Validation
cargo component build --releasecargo check --manifest-path src-tauri/Cargo.toml -p waveflow --all-targetsbun run typecheckbun run lintcargo test --manifest-path src-tauri/Cargo.toml -p waveflow-core --test plugin_release_radar --features plugins,sqlitecargo test --manifest-path src-tauri/Cargo.toml -p waveflow-core plugin::runtime::tests::build_linker_succeeds --features plugins,sqliteSummary by CodeRabbit
New Features
Bug Fixes