Skip to content

feat(plugin): report broken plugin with GitHub issue pre-fill#110

Merged
mpiton merged 5 commits intomainfrom
feat/task-16-report-broken-plugin
Apr 26, 2026
Merged

feat(plugin): report broken plugin with GitHub issue pre-fill#110
mpiton merged 5 commits intomainfrom
feat/task-16-report-broken-plugin

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented Apr 26, 2026

Summary

• Plugin Store rows now expose "Report broken plugin" menu action
• Opens pre-filled GitHub issue with diagnostic metadata (Vortex version, OS, plugin version, optional tested URL, last 50 log lines)
• New UrlOpener domain port with platform-native implementation (xdg-open on Linux, open on macOS, cmd start on Windows)
• RFC 3986 percent encoder in domain layer to safely build GitHub URLs without external dependencies
• Plugin manifest gains optional [plugin].repository field (backward compatible) for GitHub repository reference
• 11 new Rust tests for URL builder and command handler, 2 new Vitest tests for row menu
• All tests pass: 909 cargo, 582 vitest

Type

feat


Summary by cubic

Adds a “Report broken plugin” action in the Plugin Store that opens a pre-filled GitHub issue with diagnostics. Implements Task 16 and tightens http(s) URL validation (now rejects empty/missing authority, whitespace, malformed IPv6, and non-numeric ports); always returns the issue URL so the UI can fall back to clipboard.

  • New Features
    • Plugin Store row: new “Report broken plugin” menu item; opens the default browser via UrlOpener (xdg-open/open/rundll32 url.dll,FileProtocolHandler), http(s) only with strict validation (rejects missing/empty authority, whitespace, malformed IPv6 bracket tails, and non-numeric ports).
    • UI shows the action only for GitHub-backed entries; on success it copies the returned URL to the clipboard when available (guarded) and shows success/error toasts.
    • GitHub issue is pre-filled with plugin name/version, Vortex version, OS, optional tested URL, and last 50 log lines; URLs built with an RFC 3986 encoder; GitHub repositories only; .git suffix accepted.
    • Manifest: optional [plugin].repository parsed into repository_url; action renders only when present.
    • Loader: adds find_installed_manifest and uses a metadata-only parser when the plugin isn’t loaded or .wasm is missing; canonicalizes paths and enforces plugins-dir containment to prevent symlink escapes.
    • IPC: plugin_report_broken(pluginName, logLines?, testedUrl?) -> string; always returns the issue URL for clipboard fallback; tests cover URL builder, handler (including launcher failures and authority/IPv6/port validation), and menu rendering.

Written for commit cd0fe04. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added "Report broken plugin" action in the plugin store menu (enabled for GitHub-hosted plugins). Opens a pre-filled GitHub issue URL and attempts to copy it to the clipboard; shows success (with/without copy) or error toasts.
  • i18n

    • Added English and French strings for the action and three toast variants.
  • Tests

    • Added tests for UI behavior, manifest parsing, URL generation/encoding, opener behavior, and error paths.
  • Documentation

    • CHANGELOG updated with the new entry.

…ill (task 16)

Plugins now expose a "Report broken plugin" item in the kebab menu of
the Plugin Store row. Clicking it opens the user's default browser at
a pre-filled GitHub issue on the plugin's repository, with diagnostic
metadata (plugin name + version, Vortex version, OS, optional URL
under test, last 50 log lines) inlined into the issue body.

Domain
- `PluginInfo` gains `repository_url: Option<String>` (parsed from a
  new `[plugin].repository` key in `plugin.toml`).
- New `UrlOpener` driven port with platform-native `SystemUrlOpener`
  (xdg-open / open / cmd start), `http(s)://` only by validation so
  the OS launcher never receives `javascript:` / `file://` payloads.
- `build_report_broken_url` is std-only: RFC 3986 unreserved-set
  percent encoder, last 50 log lines kept, GitHub-only repository
  hosts, accepts `.git` suffix, rejects malformed URLs with
  `DomainError::ValidationError`.

Application
- `ReportBrokenPluginCommand` handler returns `AppError::Validation`
  when the manifest carries no `repository_url` and `AppError::NotFound`
  when the plugin is not loaded; the URL is opened off the async
  runtime via `tokio::task::spawn_blocking`.

Adapters
- `manifest.rs` parses `[plugin].repository` (optional, backward
  compatible — plugins without the field still install).
- New Tauri IPC `plugin_report_broken(pluginName, logLines?, testedUrl?)
  -> string` returns the issue URL so the UI can fall back to clipboard
  copy if the launcher fails.

Frontend
- `PluginStoreRow` renders an extra menu item when `onReportBroken` is
  wired (hidden otherwise), and the parent `PluginsView` invokes the
  IPC through `useTauriMutation` with success / error toasts.
- i18n (en/fr): `plugins.action.reportBroken`,
  `plugins.toast.reportBroken{Success,Error}`.

Coverage
- 11 new Rust tests (URL builder edge cases + handler) and 2 new
  Vitest cases for the row's menu entry. cargo test --lib reports
  909 passed, npx vitest run reports 582 passed.
@github-actions github-actions Bot added documentation Improvements or additions to documentation rust frontend labels Apr 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Parses optional repository from plugin manifests, exposes it on PluginInfo, builds GitHub "new issue" URLs with metadata, adds a UrlOpener port and SystemUrlOpener implementation, exposes a plugin_report_broken Tauri IPC command, wires the command bus and plugin loader to use metadata-only manifest parsing, and adds UI/menu, i18n, and tests for reporting broken plugins.

Changes

Cohort / File(s) Summary
Plugin manifest & model
src-tauri/src/adapters/driven/plugin/manifest.rs, src-tauri/src/domain/model/plugin.rs
Parse optional repository in plugin.toml, add metadata-only manifest parsing API, add repository_url on PluginInfo with setter/accessor, and add build_report_broken_url(...) to create encoded GitHub issue URLs.
UrlOpener port & system impl
src-tauri/src/domain/ports/driven/url_opener.rs, src-tauri/src/domain/ports/driven/mod.rs, src-tauri/src/adapters/driven/filesystem/url_opener.rs, src-tauri/src/adapters/driven/filesystem/mod.rs
Add UrlOpener trait and re-export; implement SystemUrlOpener that strictly validates http(s) URLs and launches OS-specific open commands; re-export type from filesystem module.
Command bus & command
src-tauri/src/application/command_bus.rs, src-tauri/src/application/commands/mod.rs, src-tauri/src/application/commands/report_broken_plugin.rs
Add ReportBrokenPluginCommand, wire optional UrlOpener into CommandBus (setter/accessors), implement handle_report_broken_plugin to resolve manifest (loaded or installed), require repository_url, build issue URL, attempt to open it (log failures), and return the URL or typed errors.
Tauri IPC, bootstrap & exports
src-tauri/src/adapters/driving/tauri_ipc.rs, src-tauri/src/lib.rs
Add plugin_report_broken Tauri command and register it; inject SystemUrlOpener into CommandBus at startup; update public re-exports to include new command/type.
Plugin loader & trait
src-tauri/src/adapters/driven/plugin/extism_loader.rs, src-tauri/src/domain/ports/driven/plugin_loader.rs
Add PluginLoader::find_installed_manifest to trait (default Ok(None)); implement find_installed_manifest in Extism loader with name validation, canonicalization/containment checks, and metadata-only parsing tolerant of unreadable manifests.
Frontend UI, types & tests
src/views/PluginsView.tsx, src/views/PluginsView/PluginStoreRow.tsx, src/types/plugin-store.ts, src/views/PluginsView/__tests__/*
Add repository to PluginStoreEntry, add onReportBroken prop and menu item in PluginStoreRow, wire plugin_report_broken IPC call and clipboard-copy logic in UI, gate action on GitHub repo prefix, and add/adjust tests to cover new behavior.
i18n & changelog
src/i18n/locales/en.json, src/i18n/locales/fr.json, CHANGELOG.md
Add plugins.action.reportBroken and plugins.toast.reportBroken* strings in English and French; document the new feature in CHANGELOG.
Misc tests/helpers
src/views/PluginsView/__tests__/groupByCategory.test.ts, src/views/PluginsView/__tests__/usePluginStore.test.ts
Update test fixtures/mocks to include the new repository field for plugin entries.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as React UI
    participant Tauri as Tauri IPC
    participant Bus as CommandBus
    participant Domain as Domain Logic
    participant Opener as SystemUrlOpener
    participant Browser as Browser

    User->>UI: click "Report Broken"
    UI->>Tauri: plugin_report_broken(name, log_lines?, tested_url?)
    Tauri->>Bus: handle_report_broken_plugin(command)
    Bus->>Domain: resolve manifest (loaded or find_installed_manifest)
    Domain-->>Bus: PluginInfo (with repository_url)
    Bus->>Domain: build_report_broken_url(repository_url, metadata)
    Domain-->>Bus: encoded GitHub issue URL
    Bus->>Opener: open_url(issue_url)
    Opener->>Opener: validate http(s) and authority (strict)
    Opener->>Browser: spawn OS open command (xdg-open/open/Windows)
    Browser-->>User: open GitHub new issue form
    Opener-->>Bus: success/error (logged)
    Bus-->>Tauri: return issue URL or error
    Tauri-->>UI: show toast / copy URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ui

Poem

🐰 I found a bug beneath a log and stitched a tiny map,
I packed the version, OS, and lines into a GitHub scrap.
One hop, one click — the issue waits, the URL I bring,
A carrot, a ticket, a hopeful little ping.
Hop on — the rabbit filed the bug and did a happy spring.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding a 'Report broken plugin' action that creates a pre-filled GitHub issue.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/task-16-report-broken-plugin

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 26, 2026

Greptile Summary

This PR implements the "Report broken plugin" feature (task 16): a new kebab-menu action in the Plugin Store that builds a pre-filled GitHub issue URL with diagnostic metadata and opens it via a platform-native UrlOpener port (xdg-open / open / rundll32). The previous reviewer concern about unconditionally wiring onReportBroken is resolved — PluginsView now guards behind a canReportBroken GitHub-prefix check and passes the prop conditionally.

Confidence Score: 5/5

Safe to merge; only P2 findings remain and no security issues were identified.

All findings are P2 (a single UX edge-case around log-line length in the generated URL). The security surface (URL validation, symlink containment, scheme restriction) is well-hardened and well-tested. The previous P1 about unconditional onReportBroken is addressed.

src-tauri/src/domain/model/plugin.rs — build_report_broken_url bounds line count but not line length.

Important Files Changed

Filename Overview
src-tauri/src/domain/model/plugin.rs Adds repository_url to PluginInfo, build_report_broken_url, parse_github_owner_repo, and percent_encode. Logic is solid; per-line log length is unbounded which can produce URLs exceeding GitHub/browser limits.
src-tauri/src/adapters/driven/filesystem/url_opener.rs New SystemUrlOpener with validate_http_url (IPv6, port, authority checks) and platform-specific launchers. Windows discards exit-status intentionally and with a comment explaining why; well-tested.
src-tauri/src/application/commands/report_broken_plugin.rs Handler falls back to find_installed_manifest for unloaded plugins, always returns URL for clipboard fallback, and surfaced launcher failures are logged but non-fatal. Well-tested with mocks.
src/views/PluginsView.tsx Adds canReportBroken guard (GitHub prefix check), reportBrokenMutation with clipboard fallback and toast handling, and conditionally passes onReportBroken to rows — addressing the concern from the previous review thread.
src/views/PluginsView/PluginStoreRow.tsx Adds onReportBroken prop (optional); menu item only renders when prop is provided and plugin is installed. Clean implementation.
src-tauri/src/adapters/driven/plugin/extism_loader.rs Adds find_installed_manifest with name sanitisation (rejects /, \, ..) and symlink-containment check via canonicalize. Silently returns Ok(None) for unreadable manifests, which is correct for the broken-plugin use-case.
src-tauri/src/adapters/driven/plugin/manifest.rs Adds parse_manifest_metadata (no .wasm requirement) and repository deserialization via Option<String> in RawPluginSection. Backward-compatible.
src/types/plugin-store.ts Adds repository: string field; consistent with backend DTO which always serializes as a (possibly empty) string.

Sequence Diagram

sequenceDiagram
    participant UI as PluginsView (React)
    participant Row as PluginStoreRow
    participant IPC as Tauri IPC
    participant Bus as CommandBus
    participant Loader as PluginLoader
    participant Builder as build_report_broken_url
    participant Opener as SystemUrlOpener

    UI->>Row: onReportBroken(name) [only if canReportBroken]
    Row->>IPC: plugin_report_broken(pluginName, logLines?, testedUrl?)
    IPC->>Bus: handle_report_broken_plugin(cmd)
    Bus->>Loader: list_loaded()
    alt plugin found in loaded list
        Loader-->>Bus: PluginInfo
    else not loaded (e.g. broken)
        Bus->>Loader: find_installed_manifest(name)
        Loader-->>Bus: PluginInfo (from plugin.toml)
    end
    Bus->>Builder: build_report_broken_url(repo_url, meta, logs)
    Builder-->>Bus: issue_url (percent-encoded)
    Bus->>Opener: open_url(issue_url) [spawn_blocking]
    Opener-->>Bus: Ok / Err (logged, non-fatal)
    Bus-->>IPC: Ok(issue_url)
    IPC-->>UI: issue_url
    UI->>UI: clipboard.writeText(url) [best-effort]
    UI->>UI: toast.success / toast.error
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src-tauri/src/domain/model/plugin.rs
Line: 951-1006

Comment:
**No per-line length cap for log lines in URL body**

`MAX_LOG_LINES` bounds the *number* of lines included but not their individual length. A single line containing a stack trace, serialised JSON, or base64 payload could be hundreds of kilobytes; 50 such lines would produce a URL well above GitHub's ~8 KB practical limit (acknowledged in the comment) and likely above browser URL limits (~2 MB). The URL would either be silently truncated by GitHub or rejected by the browser entirely, making the pre-filled issue arrive empty.

Consider adding a per-line character limit (e.g. 500 chars) before the percent-encode step:

```rust
const MAX_LOG_LINE_CHARS: usize = 500;

for line in &log_lines[start..] {
    let truncated = &line[..line.len().min(MAX_LOG_LINE_CHARS)];
    body.push_str(truncated);
    body.push('\n');
}
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "fix: validate port and IPv6 bracket tail..." | Re-trigger Greptile

Comment thread src/views/PluginsView.tsx Outdated
Comment thread src-tauri/src/adapters/driven/filesystem/url_opener.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src-tauri/src/adapters/driven/filesystem/url_opener.rs (1)

76-83: Add rationale comment for Windows status handling.

run_launcher on Windows intentionally ignores exit status; adding the same explanatory comment used in src-tauri/src/adapters/driven/filesystem/file_opener.rs would prevent future “fixes” that reintroduce false failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driven/filesystem/url_opener.rs` around lines 76 - 83,
The Windows implementation of run_launcher currently discards the process exit
status but lacks the explanatory rationale; update the #[cfg(target_os =
"windows")] fn run_launcher(program: &str, args: &[std::ffi::OsString]) to
include a concise comment explaining that on Windows we intentionally do not
treat non-zero/exit codes as failures (mirroring the rationale in
file_opener.rs) so future maintainers won't convert the status into a
DomainError—place the comment immediately above or inside run_launcher near the
Command::new(...) call and keep the behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/application/commands/report_broken_plugin.rs`:
- Around line 53-56: The current code aborts on opener.open_url errors so
issue_url never gets returned; change the behavior in the block that spawns the
blocking call (the use of url_for_browser, opener.open_url, and the map_err ->
AppError::Plugin path) to make launcher failures non-fatal and always return the
generated URL plus an open-status indicator. Concretely, replace the fallible
map_err/?? chain with code that captures the Result from opener.open_url (or the
JoinHandle error), maps it to a boolean or a small enum (e.g., opened: bool or
ReportLaunchStatus), and then return a structured outcome containing issue_url
and that status instead of erroring out via AppError::Plugin. Ensure the
function signature/result type is adjusted accordingly so callers receive both
the URL (issue_url) and whether opener.open_url succeeded.
- Around line 27-34: The current lookup uses self.plugin_loader().list_loaded()
and returns AppError::NotFound if the plugin isn't loaded, which prevents
reporting installed-but-not-loaded plugins; change the resolution to first try
finding the plugin in installed manifests (e.g., a list_installed() or the
repository of installed manifests) by matching cmd.plugin_name, and only fall
back to list_loaded() if necessary, so that a manifest for an installed plugin
(even if failed to load) is returned instead of raising AppError::NotFound;
update references around manifest, plugin_loader(), list_loaded(),
cmd.plugin_name, and the NotFound error to reflect the new search order.

In `@src/views/PluginsView.tsx`:
- Around line 101-116: The report flow currently sends only pluginName and
ignores the mutation result URL; update the call sites that invoke
reportBrokenMutation.mutate (the UI path that only passes pluginName) to include
logLines and testedUrl collected from the component state/console output so the
backend gets prefilled diagnostics, and modify the reportBrokenMutation handlers
to use the returned value (the mutation success payload/URL from
reportBrokenMutation) — onSuccess should receive and open/use that URL and also
expose it to the UI as a fallback (e.g., show a copy-to-clipboard/link button)
when window.open fails; look for the reportBrokenMutation definition and the
mutate invocation(s) to implement these changes.

---

Nitpick comments:
In `@src-tauri/src/adapters/driven/filesystem/url_opener.rs`:
- Around line 76-83: The Windows implementation of run_launcher currently
discards the process exit status but lacks the explanatory rationale; update the
#[cfg(target_os = "windows")] fn run_launcher(program: &str, args:
&[std::ffi::OsString]) to include a concise comment explaining that on Windows
we intentionally do not treat non-zero/exit codes as failures (mirroring the
rationale in file_opener.rs) so future maintainers won't convert the status into
a DomainError—place the comment immediately above or inside run_launcher near
the Command::new(...) call and keep the behavior unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 01bb8fab-ca6a-4b90-ade7-34f47b924651

📥 Commits

Reviewing files that changed from the base of the PR and between 60f85b3 and 5e9cd18.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • src-tauri/src/adapters/driven/filesystem/mod.rs
  • src-tauri/src/adapters/driven/filesystem/url_opener.rs
  • src-tauri/src/adapters/driven/plugin/manifest.rs
  • src-tauri/src/adapters/driving/tauri_ipc.rs
  • src-tauri/src/application/command_bus.rs
  • src-tauri/src/application/commands/mod.rs
  • src-tauri/src/application/commands/report_broken_plugin.rs
  • src-tauri/src/domain/model/plugin.rs
  • src-tauri/src/domain/ports/driven/mod.rs
  • src-tauri/src/domain/ports/driven/url_opener.rs
  • src-tauri/src/lib.rs
  • src/i18n/locales/en.json
  • src/i18n/locales/fr.json
  • src/views/PluginsView.tsx
  • src/views/PluginsView/PluginStoreRow.tsx
  • src/views/PluginsView/__tests__/PluginStoreRow.test.tsx

Comment thread src-tauri/src/application/commands/report_broken_plugin.rs Outdated
Comment thread src-tauri/src/application/commands/report_broken_plugin.rs Outdated
Comment thread src/views/PluginsView.tsx
Comment on lines +101 to +116
const reportBrokenMutation = useTauriMutation<
string,
{ pluginName: string; logLines?: string[]; testedUrl?: string }
>("plugin_report_broken", {
onSuccess: (_url, variables) => {
toast.success(t("plugins.toast.reportBrokenSuccess", { name: variables.pluginName }));
},
onError: (error, variables) => {
toast.error(
t("plugins.toast.reportBrokenError", {
name: variables.pluginName,
reason: error.message,
}),
);
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Report flow drops diagnostics and does not use the returned issue URL.

Line 232 only sends pluginName, so logLines/testedUrl are never populated from the UI path. Also, Line 105 ignores the returned URL, so this component currently has no manual fallback path (e.g., copy URL) when opening behavior is unreliable.

This undercuts the intended “pre-filled diagnostic issue + fallback URL” workflow.

Also applies to: 232-232

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/PluginsView.tsx` around lines 101 - 116, The report flow currently
sends only pluginName and ignores the mutation result URL; update the call sites
that invoke reportBrokenMutation.mutate (the UI path that only passes
pluginName) to include logLines and testedUrl collected from the component
state/console output so the backend gets prefilled diagnostics, and modify the
reportBrokenMutation handlers to use the returned value (the mutation success
payload/URL from reportBrokenMutation) — onSuccess should receive and open/use
that URL and also expose it to the UI as a fallback (e.g., show a
copy-to-clipboard/link button) when window.open fails; look for the
reportBrokenMutation definition and the mutate invocation(s) to implement these
changes.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/views/PluginsView.tsx">

<violation number="1" location="src/views/PluginsView.tsx:232">
P2: The report-broken action only sends `pluginName`, so generated GitHub issues miss optional diagnostics (`testedUrl`, recent log lines) that the command supports.</violation>

<violation number="2" location="src/views/PluginsView.tsx:232">
P2: Only pass `onReportBroken` for plugins that actually expose a repository URL; otherwise the menu advertises an action that immediately fails for unsupported manifests.</violation>
</file>

<file name="src-tauri/src/adapters/driven/filesystem/url_opener.rs">

<violation number="1" location="src-tauri/src/adapters/driven/filesystem/url_opener.rs:39">
P1: Windows URL launching uses `cmd /C start` with an unescaped URL, so GitHub issue URLs containing `&`/`%` can be misparsed by `cmd` and fail to open correctly.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src-tauri/src/adapters/driven/filesystem/url_opener.rs Outdated
Comment thread src/views/PluginsView.tsx Outdated
onEnable={(name) => enableMutation.mutate({ name })}
onUninstall={(name) => uninstallMutation.mutate({ name })}
onConfigure={(name) => setConfigPluginName(name)}
onReportBroken={(name) => reportBrokenMutation.mutate({ pluginName: name })}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 26, 2026

Choose a reason for hiding this comment

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

P2: The report-broken action only sends pluginName, so generated GitHub issues miss optional diagnostics (testedUrl, recent log lines) that the command supports.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/views/PluginsView.tsx, line 232:

<comment>The report-broken action only sends `pluginName`, so generated GitHub issues miss optional diagnostics (`testedUrl`, recent log lines) that the command supports.</comment>

<file context>
@@ -213,6 +229,7 @@ export function PluginsView() {
                       onEnable={(name) => enableMutation.mutate({ name })}
                       onUninstall={(name) => uninstallMutation.mutate({ name })}
                       onConfigure={(name) => setConfigPluginName(name)}
+                      onReportBroken={(name) => reportBrokenMutation.mutate({ pluginName: name })}
                       hasConfig={hasConfig(entry.name)}
                       isInstalling={isInstalling(entry.name)}
</file context>
Fix with Cubic

Comment thread src/views/PluginsView.tsx Outdated
- Frontend gates the menu item on `entry.repository` so plugins without
  a GitHub manifest no longer surface a button that immediately fails.
- Handler falls back to `find_installed_manifest` when `list_loaded`
  doesn't have the plugin — broken (failed-to-load) plugins are the
  whole point of the action and must be reportable.
- Launcher failure is now non-fatal: the URL is always returned so the
  frontend can offer a clipboard fallback when the OS browser is
  unavailable.
- Frontend writes the returned URL to the clipboard on success and
  surfaces a dedicated toast acknowledging the copy.
- Windows launcher swaps `cmd /C start` for
  `rundll32 url.dll,FileProtocolHandler` so URLs containing `&` / `%`
  reach the shell-execute call intact, plus a comment explaining why
  the exit status is intentionally ignored.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/views/PluginsView/__tests__/PluginStoreRow.test.tsx (1)

99-111: Consider extracting a tiny helper to open the kebab menu.

The same interaction is repeated multiple times; a local helper would reduce duplication and improve readability.

Refactor sketch
+async function openActionsMenu(user: ReturnType<typeof userEvent.setup>) {
+  await user.click(
+    screen.getByRole("button", { name: /(more actions|plus d['\u2019]actions)/i }),
+  );
+}
...
-    await user.click(
-      screen.getByRole("button", { name: /(more actions|plus d['\u2019]actions)/i }),
-    );
+    await openActionsMenu(user);
...
-    await user.click(screen.getByRole("button", { name: /(more actions|plus d['’]actions)/i }));
+    await openActionsMenu(user);

Also applies to: 134-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/PluginsView/__tests__/PluginStoreRow.test.tsx` around lines 99 -
111, Extract a small helper (e.g., openKebabMenu) to encapsulate the repeated
interaction that opens the kebab menu: it should accept the userEvent instance
(user) and call user.click(screen.getByRole("button", { name: /(more
actions|plus d['\u2019]actions)/i })); then replace the duplicated calls in
PluginStoreRow.test.tsx (the tests that use user.click(...getByRole button...)
around the onDisable and uninstall assertions and the similar block at lines
134-156) with await openKebabMenu(user) to reduce duplication and improve
readability while keeping the rest of each test unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driven/filesystem/url_opener.rs`:
- Around line 56-64: The current validate_http_url function only checks the
prefix and lets malformed scheme-only URLs (like "https://") through; update
validate_http_url to parse the string with url::Url::parse, ensure the scheme is
"http" or "https" and that Url::host() (or authority) is Some, returning Ok(())
only if parse succeeds and a host exists, otherwise return
DomainError::ValidationError with the offending URL in the message; reference
the validate_http_url function and DomainError type when making this change.

In `@src-tauri/src/adapters/driven/plugin/extism_loader.rs`:
- Around line 259-264: The code builds dir = self.plugins_dir.join(name) and
then reads parse_manifest(&dir), but dir can be a symlink escaping the plugins
root; update the flow in the function that uses dir (reference: plugins_dir,
name, dir, parse_manifest) to canonicalize both the plugins_dir and the
candidate dir (using std::fs::canonicalize), verify the canonicalized dir
starts_with the canonicalized plugins_dir root, and if not return Ok(None); only
after this containment check proceed with is_dir and parse_manifest to prevent
symlink escape.

---

Nitpick comments:
In `@src/views/PluginsView/__tests__/PluginStoreRow.test.tsx`:
- Around line 99-111: Extract a small helper (e.g., openKebabMenu) to
encapsulate the repeated interaction that opens the kebab menu: it should accept
the userEvent instance (user) and call user.click(screen.getByRole("button", {
name: /(more actions|plus d['\u2019]actions)/i })); then replace the duplicated
calls in PluginStoreRow.test.tsx (the tests that use user.click(...getByRole
button...) around the onDisable and uninstall assertions and the similar block
at lines 134-156) with await openKebabMenu(user) to reduce duplication and
improve readability while keeping the rest of each test unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d53b5168-d9f7-49cb-91cd-2b8252be2433

📥 Commits

Reviewing files that changed from the base of the PR and between 5e9cd18 and fd8d6de.

📒 Files selected for processing (11)
  • src-tauri/src/adapters/driven/filesystem/url_opener.rs
  • src-tauri/src/adapters/driven/plugin/extism_loader.rs
  • src-tauri/src/application/commands/report_broken_plugin.rs
  • src-tauri/src/domain/ports/driven/plugin_loader.rs
  • src/i18n/locales/en.json
  • src/i18n/locales/fr.json
  • src/types/plugin-store.ts
  • src/views/PluginsView.tsx
  • src/views/PluginsView/__tests__/PluginStoreRow.test.tsx
  • src/views/PluginsView/__tests__/groupByCategory.test.ts
  • src/views/PluginsView/__tests__/usePluginStore.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/views/PluginsView/tests/groupByCategory.test.ts
  • src/views/PluginsView/tests/usePluginStore.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/i18n/locales/en.json
  • src/i18n/locales/fr.json
  • src/views/PluginsView.tsx

Comment thread src-tauri/src/adapters/driven/filesystem/url_opener.rs
Comment thread src-tauri/src/adapters/driven/plugin/extism_loader.rs Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/views/PluginsView.tsx">

<violation number="1" location="src/views/PluginsView.tsx:111">
P2: Guard `navigator.clipboard` before calling `.writeText`; otherwise this can throw synchronously and break the success flow.</violation>
</file>

<file name="src-tauri/src/adapters/driven/plugin/extism_loader.rs">

<violation number="1" location="src-tauri/src/adapters/driven/plugin/extism_loader.rs:263">
P2: Using `parse_manifest` here incorrectly treats installed plugins with broken/missing `.wasm` as “not installed”, so the broken-plugin report flow can fail for exactly the failure mode it should support.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/views/PluginsView.tsx Outdated
Comment thread src-tauri/src/adapters/driven/plugin/extism_loader.rs Outdated
- `validate_http_url` rejects scheme-only and missing-authority shapes
  (`https://`, `https:///foo`, `https://?x`) and any whitespace, so
  garbage URLs fail at validation instead of bubbling up as a useless
  launcher error.
- `find_installed_manifest` now uses a metadata-only manifest parser
  that doesn't require `<plugin>.wasm` on disk — the broken-plugin
  flow needs to surface plugins whose binary is missing or corrupted.
- The same path canonicalizes both `plugins_dir` and the candidate
  plugin directory and verifies containment before reading anything,
  so a hostile symlink can't make the loader read a manifest from
  outside the plugins root.
- Frontend guards `navigator.clipboard` before calling `.writeText`:
  the API is undefined in non-secure contexts and falling back to the
  no-copy toast is preferable to a synchronous throw.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src-tauri/src/adapters/driven/plugin/extism_loader.rs (1)

251-298: Add unit tests for find_installed_manifest method.

This method contains several critical security and error-handling paths that should be covered by tests:

  • Valid plugin lookup returning Some(PluginInfo)
  • Invalid name validation (empty, contains /, \, ..) returning ValidationError
  • Non-existent directory returning Ok(None)
  • Symlink escape attempt returning ValidationError
  • Missing/corrupt manifest returning Ok(None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driven/plugin/extism_loader.rs` around lines 251 -
298, Add unit tests exercising find_installed_manifest on the key paths: create
a temp plugins_dir and test (1) valid plugin returning Some(PluginInfo) by
writing a correct manifest metadata file and asserting the returned PluginInfo
matches; (2) invalid name inputs ("", names containing '/', '\\', or "..") by
calling find_installed_manifest and asserting a DomainError::ValidationError is
returned; (3) non-existent plugin directory returns Ok(None); (4) symlink escape
by creating plugins_dir/<name> as a symlink to a directory outside the canonical
plugins_dir and assert DomainError::ValidationError is returned; and (5)
missing/corrupt manifest by placing a plugin directory without a valid manifest
(or with corrupted metadata) and asserting it yields Ok(None); use the
find_installed_manifest method, parse_manifest_metadata behavior (metadata-only
parsing), tempdir utilities and canonicalize semantics to set up each scenario
and ensure cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driven/filesystem/url_opener.rs`:
- Around line 56-79: The validate_http_url function currently only checks simple
prefixes and leading chars, but still permits hostless authorities like
"https://:443/path" or "https://@/x"; update validate_http_url to extract the
authority portion (take rest up to the first '/', '?' or '#') and reject if that
authority is empty or begins with ':' or '@', or ends with '@' (e.g., ":443" or
"@/x" or "user@/x"), and also preserve existing whitespace checks and error type
DomainError::ValidationError; update the error message in the same
ValidationError path to reference the original url string.

---

Nitpick comments:
In `@src-tauri/src/adapters/driven/plugin/extism_loader.rs`:
- Around line 251-298: Add unit tests exercising find_installed_manifest on the
key paths: create a temp plugins_dir and test (1) valid plugin returning
Some(PluginInfo) by writing a correct manifest metadata file and asserting the
returned PluginInfo matches; (2) invalid name inputs ("", names containing '/',
'\\', or "..") by calling find_installed_manifest and asserting a
DomainError::ValidationError is returned; (3) non-existent plugin directory
returns Ok(None); (4) symlink escape by creating plugins_dir/<name> as a symlink
to a directory outside the canonical plugins_dir and assert
DomainError::ValidationError is returned; and (5) missing/corrupt manifest by
placing a plugin directory without a valid manifest (or with corrupted metadata)
and asserting it yields Ok(None); use the find_installed_manifest method,
parse_manifest_metadata behavior (metadata-only parsing), tempdir utilities and
canonicalize semantics to set up each scenario and ensure cleanup.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6e4815d6-9464-4ea5-a036-9cddf8ae9efc

📥 Commits

Reviewing files that changed from the base of the PR and between fd8d6de and d55b925.

📒 Files selected for processing (4)
  • src-tauri/src/adapters/driven/filesystem/url_opener.rs
  • src-tauri/src/adapters/driven/plugin/extism_loader.rs
  • src-tauri/src/adapters/driven/plugin/manifest.rs
  • src/views/PluginsView.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/views/PluginsView.tsx
  • src-tauri/src/adapters/driven/plugin/manifest.rs

Comment thread src-tauri/src/adapters/driven/filesystem/url_opener.rs
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/adapters/driven/filesystem/url_opener.rs">

<violation number="1" location="src-tauri/src/adapters/driven/filesystem/url_opener.rs:68">
P3: Reject URLs with an empty authority host before launching. Cases like `https://:443/path` and `https://@/x` currently pass this check even though HTTP(S) URLs require a non-empty host.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src-tauri/src/adapters/driven/filesystem/url_opener.rs
Forms like `https://:443/path` and `https://user@/x` survived the
prefix / leading-char check even though they carry no usable host.
Strip userinfo, then look at `host[:port]`, with an extra arm for
IPv6 literals so `[]` and an unclosed `[` get rejected too. Tests
cover the new rejection paths plus a few authorities (userinfo,
IPv6) that must still pass.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driven/filesystem/url_opener.rs`:
- Around line 85-94: The current host_missing check only ensures a host string
exists but doesn't reject malformed authorities (e.g., non-numeric ports or
stray chars after an IPv6 `]`), so update the validation around host_port,
host_missing and the IPv6 branch: when host_port starts with '[' require a
matching ']' and ensure any characters following the ']' are either empty or a
single ':' followed by a non-empty all-digit port; when not IPv6, split on the
last ':' and ensure the portion after the last ':' (if present) is all digits (a
valid port) and the host portion is non-empty; if any of these checks fail,
treat the authority as missing/invalid (set host_missing = true) so malformed
inputs like `:abc` or `[::1]oops` are rejected early.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6fdc318f-ecd5-44c5-9bd4-b626673705e6

📥 Commits

Reviewing files that changed from the base of the PR and between d55b925 and b4fe66d.

📒 Files selected for processing (1)
  • src-tauri/src/adapters/driven/filesystem/url_opener.rs

Comment thread src-tauri/src/adapters/driven/filesystem/url_opener.rs Outdated
`https://example.com:abc/x` and `https://[::1]oops/x` slipped past
the empty-host check because the validator only looked at "is the
host present". Parse the authority into `(host, port)` instead:
require digits-only port when present, and only accept `:port` or
nothing after the IPv6 closing bracket. Tests cover the new
rejections.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src-tauri/src/adapters/driven/filesystem/url_opener.rs (1)

114-119: Consider enforcing valid TCP port range in validation.

On Line 115, digit-only validation still allows out-of-range ports (for example, :999999). Parsing as u16 keeps failures in validation instead of deferring to launcher/browser behavior.

🔧 Suggested patch
-    if let Some(p) = port
-        && (p.is_empty() || !p.bytes().all(|b| b.is_ascii_digit()))
+    if let Some(p) = port
+        && p.parse::<u16>().is_err()
     {
         return Err(DomainError::ValidationError(format!(
-            "http(s) URL has non-numeric port: '{url}'"
+            "http(s) URL has invalid port: '{url}'"
         )));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driven/filesystem/url_opener.rs` around lines 114 -
119, The current validation only checks that the port string contains digits
which allows out-of-range values like ":999999"; change the check on the
extracted port (variable port) to attempt parsing it as u16 (e.g., using
str::parse::<u16>()) and return DomainError::ValidationError with the same
message when parsing fails so only valid TCP ports 0–65535 are accepted; update
the branch where port is inspected (the block that now checks p.is_empty() ||
!p.bytes().all(|b| b.is_ascii_digit())) to instead reject empty strings and any
port that fails parse::<u16>(), keeping the same error message format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src-tauri/src/adapters/driven/filesystem/url_opener.rs`:
- Around line 114-119: The current validation only checks that the port string
contains digits which allows out-of-range values like ":999999"; change the
check on the extracted port (variable port) to attempt parsing it as u16 (e.g.,
using str::parse::<u16>()) and return DomainError::ValidationError with the same
message when parsing fails so only valid TCP ports 0–65535 are accepted; update
the branch where port is inspected (the block that now checks p.is_empty() ||
!p.bytes().all(|b| b.is_ascii_digit())) to instead reject empty strings and any
port that fails parse::<u16>(), keeping the same error message format.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d75864d9-da5d-460e-853b-c59c8a823025

📥 Commits

Reviewing files that changed from the base of the PR and between b4fe66d and cd0fe04.

📒 Files selected for processing (1)
  • src-tauri/src/adapters/driven/filesystem/url_opener.rs

@mpiton mpiton merged commit 41fcb7b into main Apr 26, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation frontend rust

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant