Skip to content

feat(plugins): waveflow:ui/v1 world host surface + redacted library read (#443 Part 1) - #474

Merged
InstaZDLL merged 3 commits into
mainfrom
feat/443-plugin-ui-world
Aug 1, 2026
Merged

feat(plugins): waveflow:ui/v1 world host surface + redacted library read (#443 Part 1)#474
InstaZDLL merged 3 commits into
mainfrom
feat/443-plugin-ui-world

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Part 1 of #443 (Release Radar + UI plugin surface), backend-first. A clean rebuild of the closed WIP #312, fixing its two biggest smells: the sidebar is driven off manifest() (not hardcoded to one plugin), and the ui world reuses source's host types via bindgen with: (not a duplicated impl set).

The frontend generic renderer + dynamic sidebar land in a follow-up PR — this one is self-contained and fully covered by an integration test.

What a ui-world plugin is

It renders a custom view without shipping any React. The guest returns a JSON view descriptor (sections / cards / images / action buttons) the host draws with native components — never HTML/CSS/JS/React, so a hostile descriptor can't run code in the app's origin. Exported extension interface:

  • manifest() -> mount-point — sidebar registration (label + optional lucide icon + initial path).
  • render(path) -> result<string, string> — the current view as a descriptor string.
  • on-event(event, payload) -> result<string, string> — a user action round-trips here; the plugin returns the next full descriptor (no diff protocol). open-url actions are handled host-side and never reach the guest.

The redacted library.read_artists capability

A new host import, library.list-artists(limit) -> [{id, name, track-count}]names + aggregate counts + an opaque id only, no file paths or per-track rows. Permission-gated (library.read_artists), doubly clamped (MAX_LIBRARY_ARTISTS), and snapshot-injected: the host queries the active profile async-side and hands the guest a ready list, so the guest never touches SQLite (local-only ⇒ works offline). The redaction is host-enforced — a plugin without the permission gets Err("permission denied: library.read_artists") even when the snapshot is present.

Changes

  • WIT/SDK: promote the ui/v1 skeleton to the manifest/render/on-event triad; align the ui host.wit with source (adds config) + the new library interface; add the library.read_artists SDK permission.
  • Core: ui bindgen (with: reuse); library::Host on HostCtx (gated + clamped + snapshot); manifest gate; runtime::{ui_manifest, ui_render, ui_event}.
  • App: list_ui_plugins / plugin_ui_render / plugin_ui_event + an async redacted-snapshot loader; registered in lib.rs.
  • Test: a minimal, never-bundled plugins/ui-fixture/ component + tests/plugin_ui.rs proving the round-trip and both sides of the permission gate against the same wasm.
  • Docs: plugins.md UI-world section + CLAUDE.md clause.

Validation

  • cargo clippy -p waveflow-core --features plugins,sqlite --all-targets — clean
  • cargo clippy -p waveflow --lib — clean
  • cargo test -p waveflow-core --features plugins,sqlite — 193 passed (188 unit + 4 new ui + 1 web-radio)

Relates to #443. Follow-up: frontend renderer + dynamic sidebar.

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Prise en charge des plugins d’interface utilisateur avec vues JSON déclaratives, navigation latérale dynamique et gestion des événements.
    • Ajout d’un accès contrôlé à une liste limitée et anonymisée d’artistes.
    • Les actions d’ouverture d’URL sont gérées par l’application hôte, tandis que les autres actions peuvent mettre à jour la vue.
    • Les erreurs de configuration des plugins sont ignorées afin de préserver l’affichage des autres extensions.
  • Documentation

    • Documentation mise à jour sur le développement et les permissions des plugins UI.
  • Tests

    • Tests ajoutés pour le rendu, les événements, les limites et les autorisations.

…ry read (#443)

Part 1 of #443, backend-first (frontend renderer + dynamic sidebar land in a
follow-up PR). Clean rebuild of the closed WIP #312 — drives sidebar off
manifest() instead of hardcoding one plugin, reuses source host types via
bindgen with: instead of duplicating them.

WIT + SDK:
- Promote the ui/v1 world skeleton to the manifest / render / on-event triad
  (guest returns a JSON view descriptor the host draws with native React — no
  code injection).
- Align the ui host.wit with source (adds config) + a new redacted library
  interface: list-artists -> [{id, name, track-count}].
- Add the library.read_artists SDK permission constant.

Core runtime:
- ui bindgen reuses source's http/log/storage/config host types via with:
  (one impl set); only library is fresh.
- library::Host on HostCtx: permission-gated, doubly clamped (MAX_LIBRARY_ARTISTS),
  snapshot-injected so the guest never touches the DB; local-only so it works
  offline. Denied plugins get Err even with the snapshot present.
- manifest.rs gates library_read_artists; runtime ui_manifest/ui_render/ui_event.

App commands: list_ui_plugins / plugin_ui_render / plugin_ui_event + an
async-side redacted artist-snapshot loader; registered in lib.rs.

Test: a minimal, never-bundled ui-fixture component (plugins/ui-fixture/) +
tests/plugin_ui.rs proving manifest/render/on-event round-trip and BOTH sides
of the permission gate against the same wasm.

Validated: cargo clippy (core + app) clean, core suite green (193 tests).
Docs: plugins.md ui-world section + CLAUDE.md clause.
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: plugins Plugin runtime, SDK, store, and bundled plugins type: feat New feature size: xl > 500 lines labels Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cd19c5d5-4433-4028-bf79-025857f6cd80

📥 Commits

Reviewing files that changed from the base of the PR and between 12bba19 and 241e46d.

📒 Files selected for processing (1)
  • src-tauri/crates/app/src/commands/plugins.rs

📝 Walkthrough

Walkthrough

Le PR ajoute le monde waveflow:ui/v1 pour les plugins. Il définit les contrats WIT, le rendu JSON, les événements, la navigation dynamique et l’import library.list-artists. Le runtime, les commandes Tauri et un fixture E2E prennent en charge ce flux.

Changes

Contrats UI et permissions

Layer / File(s) Summary
Contrats WIT et permission d’accès
src-tauri/crates/plugin-sdk/wit/ui/*, src-tauri/crates/plugin-sdk/src/lib.rs, src-tauri/crates/core/src/plugin/manifest.rs, src-tauri/plugins/ui-fixture/wit/*
Le monde UI expose manifest, render(path) et on-event. Les imports config et library sont ajoutés. La permission library.read_artists est déclarée, validée et désactivée par défaut.
Documentation du protocole UI
docs/features/plugins.md, CLAUDE.md
La documentation décrit les descripteurs JSON, les actions hôte, la navigation dynamique et la liste d’artistes redacted.

Runtime et services hôte

Layer / File(s) Summary
Bindings et contexte hôte
src-tauri/crates/core/src/plugin/bindings.rs, src-tauri/crates/core/src/plugin/host_impl.rs, src-tauri/crates/core/src/plugin/runtime.rs
Le runtime génère les bindings UI, conserve un snapshot d’artistes et expose library.list-artists avec permission et plafond de 500 résultats.
Invocations UI
src-tauri/crates/core/src/plugin/runtime.rs
Les fonctions ui_manifest, ui_render et ui_event instancient les plugins et propagent les résultats JSON ou les erreurs.

Commandes et intégration Tauri

Layer / File(s) Summary
Énumération et exécution des plugins UI
src-tauri/crates/app/src/commands/plugins.rs, src-tauri/crates/app/src/lib.rs
Tauri ajoute les commandes de listing, de rendu et d’événement. Les erreurs de manifeste sont journalisées puis ignorées. Les artistes sont chargés, triés et limités avant l’exécution.

Fixture et validation E2E

Layer / File(s) Summary
Fixture UI compilable
src-tauri/plugins/ui-fixture/*
Le fixture implémente le manifeste, le rendu JSON avec artistes et le retour des événements. Les bindings WIT générés prennent en charge les interfaces hôte et les exports du composant.
Tests du cycle UI et des permissions
src-tauri/crates/core/tests/plugin_ui.rs, src-tauri/crates/core/tests/fixtures/ui-fixture/manifest.toml
Les tests vérifient le point de montage, le rendu autorisé, le refus sans permission, la limitation des données et le round-trip des événements.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Frontend
  participant TauriPlugins
  participant PluginRuntime
  participant HostLibrary
  Frontend->>TauriPlugins: plugin_ui_render(plugin_id, path)
  TauriPlugins->>HostLibrary: charger le snapshot autorisé
  TauriPlugins->>PluginRuntime: ui_render(plugin_id, path, snapshot)
  PluginRuntime->>HostLibrary: library.list-artists(limit)
  HostLibrary-->>PluginRuntime: artistes limités
  PluginRuntime-->>TauriPlugins: descripteur JSON validé
  TauriPlugins-->>Frontend: vue UI
  Frontend->>TauriPlugins: plugin_ui_event(event, payload)
  TauriPlugins->>PluginRuntime: ui_event(event, payload, snapshot)
  PluginRuntime-->>TauriPlugins: nouveau descripteur JSON
  TauriPlugins-->>Frontend: vue UI mise à jour
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre décrit clairement l’ajout du monde waveflow:ui/v1 et de l’accès redacted à la bibliothèque.
Description check ✅ Passed La description détaille le périmètre, les choix techniques, les tests exécutés et le suivi prévu dans une PR ultérieure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/443-plugin-ui-world

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

@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: 4

🤖 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 1026-1032: Update load_library_artist_snapshot to check the plugin
manifest’s library.read_artists permission before acquiring the profile pool or
running the database query, returning Vec::new() immediately when permission is
absent. Preserve the existing capped query behavior for authorized plugins so
both shared callers inherit the optimization.
- Around line 1113-1119: Validate the strings returned by both plugin_ui_render
and plugin_ui_event as JSON before returning them to the UI, using serde_json
parsing. Convert parsing failures into an AppError that identifies the plugin,
while preserving the existing spawn_blocking and successful descriptor flow.

In `@src-tauri/plugins/ui-fixture/Cargo.toml`:
- Around line 28-30: Align the wit-bindgen runtime dependency with the generated
bindings: either regenerate the bindings using wit-bindgen 0.44, or change the
wit-bindgen-rt dependency in Cargo.toml to version 0.41. Ensure both components
use the same minor version.

In `@src-tauri/plugins/ui-fixture/src/lib.rs`:
- Around line 64-79: Update the render function’s call to library::list_artists
so its limit is derived from the received path or otherwise exceeds the host’s
MAX_LIBRARY_ARTISTS, ensuring the fixture exercises host-side clamping while
preserving the existing success and error handling.
🪄 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: e637769c-48dd-4655-a153-08243fa4582b

📥 Commits

Reviewing files that changed from the base of the PR and between b250be6 and a48cab8.

⛔ Files ignored due to path filters (2)
  • src-tauri/crates/core/tests/fixtures/ui-fixture/plugin.wasm is excluded by !**/*.wasm
  • src-tauri/plugins/ui-fixture/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • CLAUDE.md
  • docs/features/plugins.md
  • src-tauri/crates/app/src/commands/plugins.rs
  • src-tauri/crates/app/src/lib.rs
  • 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/runtime.rs
  • src-tauri/crates/core/tests/fixtures/ui-fixture/manifest.toml
  • src-tauri/crates/core/tests/plugin_ui.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/ui-fixture/.gitignore
  • src-tauri/plugins/ui-fixture/Cargo.toml
  • src-tauri/plugins/ui-fixture/src/bindings.rs
  • src-tauri/plugins/ui-fixture/src/lib.rs
  • src-tauri/plugins/ui-fixture/wit/deps/waveflow-host/host.wit
  • src-tauri/plugins/ui-fixture/wit/world.wit

Comment thread src-tauri/crates/app/src/commands/plugins.rs
Comment thread src-tauri/crates/app/src/commands/plugins.rs
Comment thread src-tauri/plugins/ui-fixture/Cargo.toml
Comment thread src-tauri/plugins/ui-fixture/src/lib.rs
…r JSON (#443)

CodeRabbit review on #474:
- load_library_artist_snapshot short-circuits to an empty list (skipping the
  profile pool + query) when the plugin didn't declare library.read_artists —
  the host gate denies it anyway, so there's no reason to materialize the
  redacted data. Authorized plugins keep the capped query.
- plugin_ui_render / plugin_ui_event validate the guest's output is well-formed
  JSON before returning it, surfacing a malformed descriptor as a plugin-tagged
  error instead of a raw frontend JSON.parse throw.
- ui-fixture now requests u32::MAX artists so the new
  ui_fixture_render_clamps_snapshot_to_host_max test exercises the host-side
  MAX_LIBRARY_ARTISTS clamp against an oversized snapshot.

Skipped: wit-bindgen-rt 0.44 vs bindings generated by 0.41 — mirrors the shipped
web-radio plugin exactly (cargo-component bundles its own 0.41 generator; the
0.44 runtime crate is compatible, build + tests green).
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai addressed in 12bba19:

  • load_library_artist_snapshot — gate on permission before the query ✅ Added plugin_grants_library_read (manifest read, id-pinned, default-closed); the snapshot loader now short-circuits to an empty list before acquiring the profile pool or running the query when the plugin didn't declare library.read_artists. Authorized plugins keep the capped query, so both callers inherit it.

  • Validate plugin_ui_render / plugin_ui_event output as JSON ✅ Added validate_ui_descriptor; both commands now parse the guest's output with serde_json before returning, surfacing a malformed descriptor as a clean plugin-tagged AppError instead of a raw frontend JSON.parse throw. The spawn_blocking + success flow is unchanged; the frontend still validates schemaVersion itself.

  • Fixture exercises host-side clampingui-fixture now requests u32::MAX artists, and a new ui_fixture_render_clamps_snapshot_to_host_max test feeds a snapshot larger than the cap and asserts the guest sees exactly MAX_LIBRARY_ARTISTS. Fixture wasm rebuilt + refreshed.

  • wit-bindgen-rt 0.44 vs bindings generated by 0.41 — skipped: this mirrors the shipped web-radio plugin exactly. cargo-component bundles its own 0.41 generator while the runtime crate is wit-bindgen-rt = "0.44"; the two are compatible (build + tests green), and changing it would diverge the fixture from the one bundled plugin we already ship this way.

Validation: cargo test -p waveflow-core --test plugin_ui (5 passed), cargo clippy -p waveflow --lib clean.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

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 --decorate --no-renames 12bba19a
printf '\n--- changed files ---\n'
git diff-tree --no-commit-id --name-only -r 12bba19a

printf '\n--- command module outline ---\n'
ast-grep outline src-tauri/crates/app/src/commands/plugins.rs --items all --type function --match 'load_library_artist_snapshot|plugin_grants_library_read|validate_ui_descriptor|plugin_ui_render|plugin_ui_event'

printf '\n--- relevant command implementations ---\n'
rg -n -C 20 'fn (plugin_grants_library_read|load_library_artist_snapshot|validate_ui_descriptor|plugin_ui_render|plugin_ui_event)\b' src-tauri/crates/app/src/commands/plugins.rs

printf '\n--- UI test outline and relevant assertions ---\n'
ast-grep outline src-tauri/crates/core/tests/plugin_ui.rs --items all --type function --match 'ui_fixture_render_clamps_snapshot_to_host_max|ui_fixture'
rg -n -C 16 'ui_fixture_render_clamps_snapshot_to_host_max|MAX_LIBRARY_ARTISTS|u32::MAX|list_artists' src-tauri/crates/core/tests/plugin_ui.rs src-tauri/plugins/ui-fixture/src/lib.rs

printf '\n--- inspect commit patch for affected files ---\n'
git show --format=fuller --find-renames --find-copies 12bba19a -- \
  src-tauri/crates/app/src/commands/plugins.rs \
  src-tauri/crates/core/tests/plugin_ui.rs \
  src-tauri/plugins/ui-fixture/src/lib.rs

Length of output: 28095


@InstaZDLL Vérifié dans 12bba19a.

  • load_library_artist_snapshot contrôle la permission avant require_profile_pool() et avant la requête SQL.
  • plugin_ui_render et plugin_ui_event valident les descripteurs avec validate_ui_descriptor.
  • Le test de clamp injecte un snapshot supérieur à MAX_LIBRARY_ARTISTS. Le fixture demande u32::MAX et le test vérifie que le guest reçoit exactement la limite.
  • Je n’ai pas de remarque supplémentaire sur l’écart wit-bindgen-rt dans ce PR.

Les remarques sont traitées. 🐇

[resolve]

You are interacting with an AI system.

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

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/app/src/commands/plugins.rs (1)

1044-1089: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Évitez le tronquage de track_count.

COUNT(DISTINCT t.id) est lu comme un i64. La conversion as u32 tronque une valeur supérieure à u32::MAX. Le plugin peut alors recevoir un nombre de pistes incorrect.

Bornez la valeur haute avant la conversion.

Correction proposée
-            track_count: r.track_count.max(0) as u32,
+            track_count: r.track_count.clamp(0, i64::from(u32::MAX)) as u32,
🤖 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 1044 - 1089, In
load_library_artist_snapshot, clamp each row’s track_count to u32::MAX before
converting it to u32, instead of using a direct as u32 cast. Preserve the
existing non-negative handling so negative database values still produce zero.
🤖 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/app/src/commands/plugins.rs`:
- Around line 1044-1089: In load_library_artist_snapshot, clamp each row’s
track_count to u32::MAX before converting it to u32, instead of using a direct
as u32 cast. Preserve the existing non-negative handling so negative database
values still produce zero.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2506c546-203d-451a-82cc-fa1e8e1cec0a

📥 Commits

Reviewing files that changed from the base of the PR and between a48cab8 and 12bba19.

⛔ Files ignored due to path filters (1)
  • src-tauri/crates/core/tests/fixtures/ui-fixture/plugin.wasm is excluded by !**/*.wasm
📒 Files selected for processing (3)
  • src-tauri/crates/app/src/commands/plugins.rs
  • src-tauri/crates/core/tests/plugin_ui.rs
  • src-tauri/plugins/ui-fixture/src/lib.rs

CodeRabbit review on #474: a bare `as u32` on the i64 `track_count` would wrap a
value above u32::MAX. Unreachable in practice (it's a track COUNT) but make the
cast lossless with `.clamp(0, u32::MAX as i64)` — the `.max(0)` floor on
negatives is preserved by the clamp's lower bound.
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai addressed in 241e46d: load_library_artist_snapshot now clamps track_count with .clamp(0, u32::MAX as i64) as u32 instead of a bare .max(0) as u32, so a value above u32::MAX saturates instead of wrapping. Unreachable in practice (it's a track COUNT), but the narrowing is now lossless; negatives still floor to 0 via the clamp's lower bound. clippy clean.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL Confirmé. Le clamp borne track_count dans la plage de u32 avant la conversion. Les valeurs négatives deviennent 0, et les valeurs supérieures à u32::MAX saturent sans overflow. La remarque est traitée.

You are interacting with an AI system.

@InstaZDLL
InstaZDLL merged commit 5885099 into main Aug 1, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/443-plugin-ui-world branch August 1, 2026 23:03
InstaZDLL added a commit that referenced this pull request Aug 1, 2026
…art 2)

Frontend half of the waveflow:ui/v1 surface (backend = #474). Consumes the
list_ui_plugins / plugin_ui_render / plugin_ui_event commands to draw a ui
plugin's JSON view descriptor with native React — the guest ships no UI code.

- PluginUIView: generic descriptor renderer (header + sections + item cards +
  badges + action buttons). open-url actions open in the OS browser via
  openUrl; event actions round-trip through the plugin and swap in the next
  descriptor. Respects section titles; bakes in no per-plugin assumptions.
  Non-destructive error banner keeps the last good view.
- Dynamic sidebar: useUiPlugins -> list_ui_plugins renders one nav entry per
  enabled ui plugin, label + icon + landing path all from the plugin's
  manifest() (no hardcoded per-plugin wiring). Icon resolved from a curated
  lucide allowlist (pluginIcons), unknown/absent -> generic Puzzle glyph.
- Routing: a plugin-ui HistoryEntry carries the plugin id; AppLayout keys the
  view remount on it so switching plugins reloads cleanly.
- plugins.ts: descriptor types (single source) + parsePluginUiDescriptor
  (validates schemaVersion) + wrappers; libraryReadArtists on
  PluginPermissionsInfo.
- PluginsCard: a "reads your artists" permission chip for library.read_artists.
- i18n: pluginView.* + settings.plugins.permissions.libraryReadArtists x17.

Validated: bun run typecheck + bun run lint clean.
InstaZDLL added a commit that referenced this pull request Aug 1, 2026
…art 2)

Frontend half of the waveflow:ui/v1 surface (backend = #474). Consumes the
list_ui_plugins / plugin_ui_render / plugin_ui_event commands to draw a ui
plugin's JSON view descriptor with native React — the guest ships no UI code.

- PluginUIView: generic descriptor renderer (header + sections + item cards +
  badges + action buttons). open-url actions open in the OS browser via
  openUrl; event actions round-trip through the plugin and swap in the next
  descriptor. Respects section titles; bakes in no per-plugin assumptions.
  Non-destructive error banner keeps the last good view.
- Dynamic sidebar: useUiPlugins -> list_ui_plugins renders one nav entry per
  enabled ui plugin, label + icon + landing path all from the plugin's
  manifest() (no hardcoded per-plugin wiring). Icon resolved from a curated
  lucide allowlist (pluginIcons), unknown/absent -> generic Puzzle glyph.
- Routing: a plugin-ui HistoryEntry carries the plugin id; AppLayout keys the
  view remount on it so switching plugins reloads cleanly.
- plugins.ts: descriptor types (single source) + parsePluginUiDescriptor
  (validates schemaVersion) + wrappers; libraryReadArtists on
  PluginPermissionsInfo.
- PluginsCard: a "reads your artists" permission chip for library.read_artists.
- i18n: pluginView.* + settings.plugins.permissions.libraryReadArtists x17.

Validated: bun run typecheck + bun run lint clean.
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: docs Docs, README, assets scope: plugins Plugin runtime, SDK, store, and bundled plugins size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant