Skip to content

WIP: add release radar plugin#312

Draft
InstaZDLL wants to merge 1 commit into
mainfrom
codex/release-radar-plugin
Draft

WIP: add release radar plugin#312
InstaZDLL wants to merge 1 commit into
mainfrom
codex/release-radar-plugin

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • adds the waveflow:ui/v1 plugin invocation surface and a redacted library.read_artists host permission
  • bundles a new Release Radar UI plugin powered by MusicBrainz and Cover Art Archive
  • adds a generic React renderer for plugin-provided JSON view descriptors and a sidebar entry for Release Radar

Notes

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 --release
  • cargo check --manifest-path src-tauri/Cargo.toml -p waveflow --all-targets
  • bun run typecheck
  • bun run lint
  • cargo test --manifest-path src-tauri/Cargo.toml -p waveflow-core --test plugin_release_radar --features plugins,sqlite
  • cargo test --manifest-path src-tauri/Cargo.toml -p waveflow-core plugin::runtime::tests::build_linker_succeeds --features plugins,sqlite

Summary by CodeRabbit

  • New Features

    • Ajout d’une nouvelle vue Release Radar dans l’application.
    • Les plugins peuvent désormais afficher une interface dédiée, recevoir des actions utilisateur et mettre à jour leur contenu en temps réel.
    • Le plugin Release Radar est désormais disponible avec un affichage enrichi et des actions rapides.
  • Bug Fixes

    • Amélioration de la prise en charge des permissions affichées dans les paramètres des plugins.
    • Meilleure gestion de l’accès à la bibliothèque pour certains plugins.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3c56ea12-4ff5-4cc5-ae2f-9f641490c2b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/release-radar-plugin

Comment @coderabbitai help to get the list of available commands.

@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) size: xl > 500 lines labels Jun 24, 2026
@InstaZDLL InstaZDLL changed the title WIP: [codex] add release radar plugin WIP: add release radar plugin Jun 24, 2026
@InstaZDLL InstaZDLL self-assigned this Jun 24, 2026
@InstaZDLL InstaZDLL requested a review from Copilot June 28, 2026 20:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PluginUIView renderer for plugin-provided JSON descriptors and wired a “Release Radar” sidebar view.
  • Bundled a new release-radar wasm plugin + manifest and introduced library.read_artists permission 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.

Comment on lines +42 to +46
const runAction = async (action: PluginUiAction) => {
if (action.kind === "open-url" && action.url) {
await openUrl(action.url);
return;
}
Comment on lines +92 to +96
{busyAction?.startsWith(action.event ?? "") ? (
<Loader2 size={15} className="animate-spin" />
) : (
<RefreshCw size={15} />
)}
Comment on lines +75 to +86
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>;
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Factorisez les politiques source/ui avant 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1e70cdb and 6d7b58a.

⛔ Files ignored due to path filters (2)
  • src-tauri/plugins/release-radar/Cargo.lock is excluded by !**/*.lock
  • src-tauri/resources/plugins/release-radar/plugin.wasm is excluded by !**/*.wasm
📒 Files selected for processing (26)
  • src-tauri/crates/app/src/commands/plugins.rs
  • src-tauri/crates/app/src/lib.rs
  • src-tauri/crates/app/tauri.conf.json
  • src-tauri/crates/core/src/plugin/bindings.rs
  • src-tauri/crates/core/src/plugin/host_impl.rs
  • src-tauri/crates/core/src/plugin/manifest.rs
  • src-tauri/crates/core/src/plugin/mod.rs
  • src-tauri/crates/core/src/plugin/runtime.rs
  • src-tauri/crates/core/tests/plugin_release_radar.rs
  • src-tauri/crates/plugin-sdk/src/lib.rs
  • src-tauri/crates/plugin-sdk/wit/ui/deps/host/host.wit
  • src-tauri/crates/plugin-sdk/wit/ui/plugin.wit
  • src-tauri/plugins/release-radar/.gitignore
  • src-tauri/plugins/release-radar/Cargo.toml
  • src-tauri/plugins/release-radar/manifest.toml
  • src-tauri/plugins/release-radar/src/bindings.rs
  • src-tauri/plugins/release-radar/src/lib.rs
  • src-tauri/plugins/release-radar/wit/deps/waveflow-host/host.wit
  • src-tauri/plugins/release-radar/wit/world.wit
  • src-tauri/resources/plugins/release-radar/manifest.toml
  • src/components/layout/AppLayout.tsx
  • src/components/layout/Sidebar.tsx
  • src/components/views/PluginUIView.tsx
  • src/components/views/settings/PluginsCard.tsx
  • src/lib/tauri/plugins.ts
  • src/types/index.ts

Comment on lines +780 to +781
let _guard = ui_preamble(&state, &plugin_id).await?;
let artists = load_library_artist_snapshot(&state, 200).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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

Comment on lines +592 to +610
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()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +84 to +87
/// 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +53 to +76
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" };
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 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.

Comment on lines +54 to +56
let now = now_ms();
let dismissed = read_dismissed();
let mut cache = read_cache();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +458 to +459
case "release-radar":
return <PluginUIView pluginId="release-radar" />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment on lines +42 to +45
const runAction = async (action: PluginUiAction) => {
if (action.kind === "open-url" && action.url) {
await openUrl(action.url);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment on lines +61 to +64
const items =
descriptor?.sections?.flatMap((section) =>
section.items.map((item) => ({ section: section.title, item })),
) ?? [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread src/lib/tauri/plugins.ts
Comment on lines +186 to +188
function parsePluginUiDescriptor(raw: string): PluginUiDescriptor {
return JSON.parse(raw) as PluginUiDescriptor;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: frontend React/Vite frontend (src/) size: xl > 500 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants