Skip to content

feat(UI): add download link clipboard watcher - #122

Merged
YueMiyuki merged 4 commits into
masterfrom
next-dev
Jul 4, 2026
Merged

feat(UI): add download link clipboard watcher#122
YueMiyuki merged 4 commits into
masterfrom
next-dev

Conversation

@YueMiyuki

@YueMiyuki YueMiyuki commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary by cubic

Adds a clipboard watcher that prompts to download copied links, native OS notifications for completed downloads, and a first‑launch legal gate. Also improves the updater workflow and trims low‑level dependencies.

  • New Features

    • Clipboard Watcher: Detects magnet/ed2k/ftp/thunder and other P2P schemes (ADC, Gnutella, G2, GiFT), plus http(s) links with allowed extensions. Shows a small always‑on‑top prompt; “Download” opens New Task. Enabled by default on desktop; toggle and edit extensions in Settings → Basic.
    • Smart behavior: Ignores links we copy ourselves, remembers last seen text, restores previous app focus on macOS when dismissed, and auto‑starts the monitor on launch.
    • OS notifications: Uses @tauri-apps/plugin-notification for native notifications when the app is in the background; clicking opens the file’s folder.
    • Legal gate: Shows a Terms/Privacy prompt on first launch until accepted.
    • Sync: Adds a “Misc” category; excludes device‑local keys (e.g. legal acceptance and the clipboard notice flag) from sync.
  • Dependencies

    • Added @tauri-apps/plugin-notification (renderer), tauri-plugin-clipboard, tauri-plugin-notification, and macOS objc2-app-kit.
    • UI utils: Replaced class-variance-authority/clsx/tailwind-merge with cnfast; removed normalize.css.
    • Core: Replaced urlencoding with percent-encoding; removed the rc4 crate and implemented RC4 in-house for BitTorrent MSE.
    • Capabilities: Desktop now includes notification:default; new clip-prompt window capability.
    • Release: workflow forces bundle.createUpdaterArtifacts on tagged builds; updater endpoint switched to https://risuko.app.

Written for commit 823de14. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a native clipboard-watching flow that detects download-like clipboard contents, shows a clip-prompt window, emits download events into the app, and adds a legal consent gate. It also centralizes clipboard copying, updates notification and localization support, and adjusts release and bundle configuration.

Changes

Clipboard watch, clip-prompt window, notifications, legal gate

Layer / File(s) Summary
Platform config and shared defaults
.github/workflows/release.yml, package.json, src-tauri/Cargo.toml, src-tauri/tauri.conf.json, src-tauri/capabilities/clip-prompt.json, src-tauri/capabilities/desktop.json, src-tauri/risuko-engine/src/config/defaults.rs, src/shared/configKeys.ts, src/shared/constants.ts, src/shared/types/config.ts, src/shared/syncCategories.ts, src/shared/locales/en-US/sync.ts, src/shared/locales/zh-CN/sync.ts, src/shared/locales/zh-TW/sync.ts, src-tauri/src/managers/mod.rs, src-tauri/src/commands/mod.rs, src-tauri/risuko-bt/Cargo.toml, src-tauri/risuko-bt/src/peer/connection.rs, src-tauri/risuko-bt/src/wire.rs, src-tauri/risuko-bt/src/wire/mse.rs, src-tauri/risuko-engine/Cargo.toml, src-tauri/risuko-engine/src/engine/manager.rs
Adds updater, plugin, lint, capability, default-setting, config-shape, sync-category, and module/dependency updates.
Clipboard command backend
src-tauri/src/state.rs, src-tauri/src/commands/clipboard_cmds.rs, src-tauri/src/commands/engine_cmds.rs, src-tauri/src/commands/file_cmds.rs, src-tauri/src/managers/clip_prompt.rs
Adds clipboard state, download candidate matching, watch control, prompt accept/dismiss handling, strict percent decoding, and the native prompt window manager.
Tauri app and prompt page wiring
src-tauri/src/lib.rs, src/renderer/pages/index/clip-prompt.html, src/renderer/pages/index/clip-prompt.ts, src/renderer/components/ClipboardWatcher/ClipPrompt.vue, src/renderer/components/Native/Ipc.vue, vite.renderer.config.ts
Registers the new plugins and event listeners, mounts the standalone prompt page, and connects the prompt UI to clipboard-download task creation.
Clipboard copy utility and notifications
src/renderer/utils/clipboard.ts, src/renderer/components/Preference/Advanced.vue, src/renderer/components/Preference/Basic.vue, src/renderer/components/Native/EngineClient.vue, src/renderer/components/Share/Index.vue, src/renderer/components/Task/Index.vue, src/renderer/components/TaskDetail/TaskGeneral.vue, src/renderer/components/ui/button/index.ts, src/renderer/lib/utils.ts, src/renderer/styles/global.css, src/shared/locales/en-US/preferences.ts, src/shared/locales/en-US/task.ts, src/shared/locales/zh-CN/preferences.ts, src/shared/locales/zh-CN/task.ts, src/shared/locales/zh-TW/preferences.ts, src/shared/locales/zh-TW/task.ts
Adds copyText, migrates copy call sites, adds clipboard-watch settings and OS notifications, and updates the related UI strings.
Legal consent gate
src/renderer/components/Legal/LegalGate.vue, src/renderer/pages/index/App.vue, src/shared/locales/en-US/app.ts, src/shared/locales/zh-CN/app.ts, src/shared/locales/zh-TW/app.ts
Adds the modal consent flow and renders it from the app shell with localized copy.
Estimated code review effort: 4 (Complex) ~75 minutes

Sequence Diagram(s)

sequenceDiagram
    participant ClipboardMonitor
    participant ClipboardCmds
    participant ClipPromptManager
    participant ClipPromptVue
    participant IpcVue
    participant TaskManager

    ClipboardMonitor->>ClipboardCmds: update text event
    ClipboardCmds->>ClipboardCmds: watch_enabled + is_download_candidate + dedupe
    ClipboardCmds->>ClipPromptManager: show_clip_prompt(uri)
    ClipPromptManager->>ClipPromptVue: emit clip-prompt:show
    ClipPromptVue->>ClipboardCmds: get_clip_prompt_uri
    alt user accepts
        ClipPromptVue->>ClipboardCmds: clip_prompt_accept
        ClipboardCmds->>IpcVue: emit clipboard-download
        IpcVue->>TaskManager: application:new-task { uri }
    else user dismisses
        ClipPromptVue->>ClipboardCmds: clip_prompt_dismiss
    end
Loading

Possibly related PRs

  • YueMiyuki/Risuko#116: This PR also changes src/shared/syncCategories.ts and the associated sync-category translations.
  • YueMiyuki/Risuko#121: This PR touches the same Metalink test in src-tauri/risuko-engine/src/engine/manager.rs.

Suggested labels: next

Poem

I twitched my nose and found the link,
Then popped a prompt before you blink.
🐇 A click for download, or a skip?
The legal gate says “sign the trip.”
With ears up high and tail alight,
I hop through configs into night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly summarizes the main change: adding a clipboard watcher for download links.
Description check ✅ Passed The description matches the changeset and summarizes the clipboard watcher, notifications, legal gate, and release updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot added the next The "next" steps label Jul 4, 2026

@cubic-dev-ai cubic-dev-ai 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.

7 issues found across 46 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-tauri/src/commands/clipboard_cmds.rs">

<violation number="1" location="src-tauri/src/commands/clipboard_cmds.rs:161">
P1: The prompt URI is taken via `.take()` before the fallible `show_main_window()` call. If showing the main window fails (e.g., window `.show()` or `.set_focus()` returns an error), the URI is already consumed and lost, and `hide_clip_prompt` is never called — leaving the prompt window visible with stale/no state. Move the `.take()` (or the entire accept block) after the fallible call, or restore the URI on error.</violation>
</file>

<file name="src-tauri/src/managers/clip_prompt.rs">

<violation number="1" location="src-tauri/src/managers/clip_prompt.rs:117">
P3: Prompt positioning and window-construction behavior now has a second near-identical implementation, so future fixes to tray anchoring or monitor clamping can easily diverge between flyout and clip prompt. Suggest extracting the shared desktop popup builder/positioning path into a common helper and passing size/label-specific parameters.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

.pending_clip_uri
.lock()
.ok()
.and_then(|mut g| g.take());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The prompt URI is taken via .take() before the fallible show_main_window() call. If showing the main window fails (e.g., window .show() or .set_focus() returns an error), the URI is already consumed and lost, and hide_clip_prompt is never called — leaving the prompt window visible with stale/no state. Move the .take() (or the entire accept block) after the fallible call, or restore the URI on error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src-tauri/src/commands/clipboard_cmds.rs, line 161:

<comment>The prompt URI is taken via `.take()` before the fallible `show_main_window()` call. If showing the main window fails (e.g., window `.show()` or `.set_focus()` returns an error), the URI is already consumed and lost, and `hide_clip_prompt` is never called — leaving the prompt window visible with stale/no state. Move the `.take()` (or the entire accept block) after the fallible call, or restore the URI on error.</comment>

<file context>
@@ -0,0 +1,226 @@
+        .pending_clip_uri
+        .lock()
+        .ok()
+        .and_then(|mut g| g.take());
+    if let Some(uri) = uri {
+        crate::commands::app_cmds::show_main_window(&app)?;
</file context>

Comment thread src/renderer/components/ClipboardWatcher/ClipPrompt.vue
Comment thread src-tauri/src/managers/clip_prompt.rs Outdated
Comment thread src/renderer/components/Native/EngineClient.vue Outdated
Comment thread src/renderer/components/Legal/LegalGate.vue Outdated
}

#[cfg(not(target_os = "android"))]
fn position_prompt(app: &tauri::AppHandle, window: &WebviewWindow) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Prompt positioning and window-construction behavior now has a second near-identical implementation, so future fixes to tray anchoring or monitor clamping can easily diverge between flyout and clip prompt. Suggest extracting the shared desktop popup builder/positioning path into a common helper and passing size/label-specific parameters.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src-tauri/src/managers/clip_prompt.rs, line 117:

<comment>Prompt positioning and window-construction behavior now has a second near-identical implementation, so future fixes to tray anchoring or monitor clamping can easily diverge between flyout and clip prompt. Suggest extracting the shared desktop popup builder/positioning path into a common helper and passing size/label-specific parameters.</comment>

<file context>
@@ -0,0 +1,177 @@
+}
+
+#[cfg(not(target_os = "android"))]
+fn position_prompt(app: &tauri::AppHandle, window: &WebviewWindow) {
+    let anchor = app
+        .try_state::<crate::state::AppState>()
</file context>

Comment thread src-tauri/src/commands/clipboard_cmds.rs Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src-tauri/src/lib.rs (1)

389-403: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear pending_clip_uri on clip-prompt blur hide. Focused(false) only hides the window here, so a blur-based dismiss can leave pending_clip_uri set and make get_clip_prompt_uri return a stale URI later. Route this path through the same cleanup as clip_prompt_dismiss (minus the focus restore) before hiding.

🤖 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/src/lib.rs` around lines 389 - 403, The clip-prompt blur path in
the Focused(false) branch only hides the window and can leave pending_clip_uri
stale. Update the CLIP_PROMPT_LABEL handling in the tauri window event logic to
run the same cleanup used by clip_prompt_dismiss, including clearing
pending_clip_uri, but without restoring focus, before calling hide. Use the
existing managers::clip_prompt flow and get_clip_prompt_uri behavior as
reference points to keep the dismissal paths consistent.
src/renderer/components/TaskDetail/TaskGeneral.vue (1)

196-203: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add error handling to handleCopyClick.

Same gap as Task/Index.vue's handleCopyTaskLink: copyText(uri) can reject and this call has no .catch, so a failure is silent to the user and produces an unhandled promise rejection.

🐛 Proposed fix
 handleCopyClick() {
 	const uri = getTaskUri(this.task);
-	copyText(uri).then(() => {
-		this.$msg.success(this.$t("task.copy-link-success"));
-	});
+	copyText(uri)
+		.then(() => {
+			this.$msg.success(this.$t("task.copy-link-success"));
+		})
+		.catch((err) => {
+			this.$msg.error(`${err}`);
+		});
 },
🤖 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/renderer/components/TaskDetail/TaskGeneral.vue` around lines 196 - 203,
Add error handling to handleCopyClick in TaskGeneral.vue: copyText(uri) can
reject, so the current success-only promise chain should be updated to handle
failures just like Task/Index.vue’s handleCopyTaskLink. Update the
handleCopyClick method to catch rejections from copyText, surface a user-facing
error message via this.$msg, and prevent an unhandled promise rejection while
keeping the existing success path intact.
🤖 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/src/commands/clipboard_cmds.rs`:
- Around line 187-226: The current tests only cover the pure
is_download_candidate helper and miss the stateful dedup/self-write behavior in
on_clipboard_update. Add a unit test around on_clipboard_update that exercises
the self-write path and the seen-clipboard dedup logic, using the existing
symbols like on_clipboard_update, seen_clipboard, and the self-write suppression
state to verify repeated clipboard updates are handled correctly and the reset
bug is caught.
- Around line 99-104: The dedup logic in clipboard handling is retaining full
plaintext clipboard contents in memory; update the `last_clipboard_seen` and
`last_clipboard_self_write` path in `clipboard_cmds.rs` to store only a hash or
other fixed-size fingerprint of the text instead of `Some(text.clone())`. Keep
the existing equality-based behavior by comparing hashes in the same places
where `seen.as_deref()` is checked, and ensure the change is applied
consistently in the clipboard state update flow.

In `@src-tauri/src/managers/clip_prompt.rs`:
- Around line 52-69: The frontmost-app tracking in
show_clip_prompt/prev_focus::remember is being overwritten when the clip prompt
is already open, which can erase the original app PID. Update the clip prompt
flow so repeated calls to show_clip_prompt do not call remember() again if
CLIP_PROMPT_LABEL is already visible or focused; preserve the existing
PREV_APP_PID until dismiss/restore runs, using the existing prev_focus and
show_clip_prompt helpers to gate the save only on the first open.

In `@src/renderer/components/ClipboardWatcher/ClipPrompt.vue`:
- Around line 53-59: The ClipPrompt.vue onKey handler is globally intercepting
Enter/Escape and causing duplicate or conflicting button actions when focus is
on the native buttons. Update onKey in ClipPrompt so it only handles key presses
when appropriate, and avoid calling download() on Enter if the focused element
is already the accept/ignore button that will trigger its own native click. Make
sure the accept flow (download/clip_prompt_accept) and ignore() cannot both fire
from the same Enter/Escape interaction.

In `@src/renderer/components/Legal/LegalGate.vue`:
- Around line 44-48: The accept() flow in LegalGate.vue swallows persistence
failures from usePreferenceStore().save(), leaving the user with no indication
that consent was not saved. Update the accept() method to handle the rejected
save() promise by showing a user-facing notification through the existing
Toaster/vue-sonner setup in App.vue, and keep the gate open or otherwise
communicate failure instead of ignoring the error in the catch block.
- Around line 3-23: The LegalGate overlay is blocking the app without dialog
accessibility semantics or keyboard support. Update the LegalGate.vue template
and its associated script logic so the Teleport content behaves like a modal
dialog by adding the appropriate dialog roles/ARIA, trapping focus inside the
gate, and handling Escape consistently. Use the existing LegalGate, showGate,
and accept/openUrl handlers to locate the component logic and ensure keyboard
and screen-reader users can interact with the gate properly.

In `@src/renderer/components/Native/EngineClient.vue`:
- Around line 763-780: The sendOsNotification method in EngineClient.vue
currently only dispatches a basic plugin notification, so it loses the browser
fallback’s click-to-open-folder behavior. Update sendOsNotification to use the
tauri notification plugin’s action/click callback support (if available) and
wire that handler to the same “open containing folder” flow used by the browser
Notification path. Keep the existing permission check and error handling, but
ensure the notification click action is registered where sendNotification is
called.

In `@src/renderer/components/Task/Index.vue`:
- Around line 644-650: Add error handling to handleCopyTaskLink in
Task/Index.vue because copyText(uri) can reject and currently only handles
success. Update the handleCopyTaskLink method to catch failures from copyText
after getTaskUri(task), and show a user-facing failure message via this.$msg so
the rejection is handled and the user gets feedback.

In `@src/renderer/utils/clipboard.ts`:
- Line 11: The clipboard write in copyText is not protected, so failures from
navigator.clipboard.writeText(text) can reject the returned promise and bubble
uncaught to callers. Update copyText in clipboard.ts to handle the writeText
failure internally with try/catch and avoid rethrowing, so callers like the
Task/Index.vue and TaskGeneral.vue usages don’t need their own catch for this
path.
- Around line 4-6: The `copyText()` helper in `clipboard.ts` is swallowing
failures from `invoke("mark_clipboard_self_write", { text })`, which can leave
self-copied links unmarked and later trigger the clipboard watcher. Update
`copyText()` to handle the `mark_clipboard_self_write` error explicitly by
either surfacing/logging the failure or applying a fallback ignore window, and
keep the behavior tied to the existing `last_clipboard_self_write` tracking used
by the watcher.

In `@src/shared/locales/zh-CN/preferences.ts`:
- Around line 100-101: The clipboard-watch notice copy in preferences.ts is
using an abbreviated Basic tab label that does not match the localized UI text.
Update the "clipboard-watch-notice" string to reference the full localized label
used by the nearby "basic" key (the visible tab text), so the instruction
matches what users actually see in the Preferences UI.

In `@src/shared/locales/zh-TW/preferences.ts`:
- Around line 88-89: The zh-TW clipboard-watch notice uses a shortened tab label
that doesn’t match the visible Basic tab text, so update the copy in
preferences.ts for the "clipboard-watch-notice" entry to use the full localized
label consistent with the surrounding "basic" key. Keep the navigation text
aligned with the UI wording shown by the "basic" translation (the full label,
not just 「基本」) so the notice points users to the correct tab.

In `@src/shared/syncCategories.ts`:
- Around line 10-18: `legal-accepted` and `clipboard-watch-notice-seen` are not
being excluded from sync, so they fall into the derived `misc` category and can
be cloud-synced. Update `DEVICE_LOCAL_KEYS` in `syncCategories` to include these
new local-only config keys, and verify the `miscKeys` derivation no longer picks
them up through `namedCategories`/`miscKeys` so they remain device-scoped.

---

Outside diff comments:
In `@src-tauri/src/lib.rs`:
- Around line 389-403: The clip-prompt blur path in the Focused(false) branch
only hides the window and can leave pending_clip_uri stale. Update the
CLIP_PROMPT_LABEL handling in the tauri window event logic to run the same
cleanup used by clip_prompt_dismiss, including clearing pending_clip_uri, but
without restoring focus, before calling hide. Use the existing
managers::clip_prompt flow and get_clip_prompt_uri behavior as reference points
to keep the dismissal paths consistent.

In `@src/renderer/components/TaskDetail/TaskGeneral.vue`:
- Around line 196-203: Add error handling to handleCopyClick in TaskGeneral.vue:
copyText(uri) can reject, so the current success-only promise chain should be
updated to handle failures just like Task/Index.vue’s handleCopyTaskLink. Update
the handleCopyClick method to catch rejections from copyText, surface a
user-facing error message via this.$msg, and prevent an unhandled promise
rejection while keeping the existing success path intact.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f71f3c4-1fa3-4945-8e4b-19ac7fa94571

📥 Commits

Reviewing files that changed from the base of the PR and between 8746f62 and 0d0183a.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • .github/workflows/release.yml
  • package.json
  • src-tauri/Cargo.toml
  • src-tauri/capabilities/clip-prompt.json
  • src-tauri/capabilities/desktop.json
  • src-tauri/risuko-engine/src/config/defaults.rs
  • src-tauri/risuko-engine/src/engine/manager.rs
  • src-tauri/src/commands/clipboard_cmds.rs
  • src-tauri/src/commands/mod.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/managers/clip_prompt.rs
  • src-tauri/src/managers/mod.rs
  • src-tauri/src/state.rs
  • src-tauri/tauri.conf.json
  • src/renderer/components/ClipboardWatcher/ClipPrompt.vue
  • src/renderer/components/Legal/LegalGate.vue
  • src/renderer/components/Native/EngineClient.vue
  • src/renderer/components/Native/Ipc.vue
  • src/renderer/components/Preference/Advanced.vue
  • src/renderer/components/Preference/Basic.vue
  • src/renderer/components/Share/Index.vue
  • src/renderer/components/Task/Index.vue
  • src/renderer/components/TaskDetail/TaskGeneral.vue
  • src/renderer/pages/index/App.vue
  • src/renderer/pages/index/clip-prompt.html
  • src/renderer/pages/index/clip-prompt.ts
  • src/renderer/utils/clipboard.ts
  • src/shared/configKeys.ts
  • src/shared/constants.ts
  • src/shared/locales/en-US/app.ts
  • src/shared/locales/en-US/preferences.ts
  • src/shared/locales/en-US/sync.ts
  • src/shared/locales/en-US/task.ts
  • src/shared/locales/zh-CN/app.ts
  • src/shared/locales/zh-CN/preferences.ts
  • src/shared/locales/zh-CN/sync.ts
  • src/shared/locales/zh-CN/task.ts
  • src/shared/locales/zh-TW/app.ts
  • src/shared/locales/zh-TW/preferences.ts
  • src/shared/locales/zh-TW/sync.ts
  • src/shared/locales/zh-TW/task.ts
  • src/shared/syncCategories.ts
  • src/shared/types/config.ts
  • vite.renderer.config.ts

Comment on lines +99 to +104
if let Ok(mut seen) = state.last_clipboard_seen.lock() {
if seen.as_deref() == Some(text.as_str()) {
return;
}
*seen = Some(text.clone());
}

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 | 🔵 Trivial | 💤 Low value

Consider hashing instead of storing raw clipboard text for dedup.

last_clipboard_seen/last_clipboard_self_write retain the full plaintext of whatever the user last copied (which could include passwords, tokens, or other sensitive text unrelated to downloads) in process memory indefinitely. Since only equality comparison is needed, storing a hash (e.g., a fast non-cryptographic hash) would achieve the same dedup behavior while reducing the amount of raw sensitive clipboard content retained.

🤖 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/src/commands/clipboard_cmds.rs` around lines 99 - 104, The dedup
logic in clipboard handling is retaining full plaintext clipboard contents in
memory; update the `last_clipboard_seen` and `last_clipboard_self_write` path in
`clipboard_cmds.rs` to store only a hash or other fixed-size fingerprint of the
text instead of `Some(text.clone())`. Keep the existing equality-based behavior
by comparing hashes in the same places where `seen.as_deref()` is checked, and
ensure the change is applied consistently in the clipboard state update flow.

Comment on lines +187 to +226
#[cfg(test)]
mod tests {
use super::is_download_candidate;

fn exts() -> Vec<String> {
["iso", "zip", "torrent"]
.iter()
.map(|s| s.to_string())
.collect()
}

#[test]
fn classifies_download_candidates() {
let e = exts();
assert!(is_download_candidate("magnet:?xt=urn:btih:0123abc", &e));
assert!(is_download_candidate("thunder://QUFodHRwOi8v", &e));
assert!(is_download_candidate(
"https://mirror.example.com/ubuntu.iso",
&e
));
assert!(is_download_candidate(
"https://example.com/a/b.torrent?dl=1",
&e
));
assert!(!is_download_candidate(
"https://example.com/blog/post-123",
&e
));
assert!(!is_download_candidate("https://github.com/user/repo", &e));
assert!(!is_download_candidate("just some copied text", &e));
assert!(!is_download_candidate("", &e));
assert!(!is_download_candidate("https://example.com/page.html", &e));
// empty list: scheme-based links still match, extension-based don't
assert!(is_download_candidate("magnet:?xt=urn:btih:0123abc", &[]));
assert!(!is_download_candidate(
"https://mirror.example.com/ubuntu.iso",
&[]
));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage gap: stateful dedup/self-write logic untested.

Tests only cover the pure is_download_candidate classifier; on_clipboard_update's self-write/seen dedup logic (the part flagged above) has no unit test coverage, which is likely why the missing-reset bug wasn't caught.

🤖 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/src/commands/clipboard_cmds.rs` around lines 187 - 226, The current
tests only cover the pure is_download_candidate helper and miss the stateful
dedup/self-write behavior in on_clipboard_update. Add a unit test around
on_clipboard_update that exercises the self-write path and the seen-clipboard
dedup logic, using the existing symbols like on_clipboard_update,
seen_clipboard, and the self-write suppression state to verify repeated
clipboard updates are handled correctly and the reset bug is caught.

Comment thread src-tauri/src/managers/clip_prompt.rs
Comment thread src/renderer/components/ClipboardWatcher/ClipPrompt.vue
Comment thread src/renderer/components/Legal/LegalGate.vue Outdated
Comment thread src/renderer/utils/clipboard.ts
const { writeText } = await import("@tauri-apps/plugin-clipboard-manager");
await writeText(text);
} catch {
await navigator.clipboard.writeText(text);

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

Rejection here propagates uncaught to callers.

navigator.clipboard.writeText(text) isn't wrapped in its own try/catch, so if it also fails, copyText's returned promise rejects. See related comments in Task/Index.vue (line 647) and TaskGeneral.vue (line 200), which call this without a .catch.

🤖 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/renderer/utils/clipboard.ts` at line 11, The clipboard write in copyText
is not protected, so failures from navigator.clipboard.writeText(text) can
reject the returned promise and bubble uncaught to callers. Update copyText in
clipboard.ts to handle the writeText failure internally with try/catch and avoid
rethrowing, so callers like the Task/Index.vue and TaskGeneral.vue usages don’t
need their own catch for this path.

Comment thread src/shared/locales/zh-CN/preferences.ts Outdated
Comment thread src/shared/locales/zh-TW/preferences.ts Outdated
Comment thread src/shared/syncCategories.ts

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 17 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/commands/clipboard_cmds.rs">

<violation number="1" location="src-tauri/src/commands/clipboard_cmds.rs:161">
P1: The prompt URI is taken via `.take()` before the fallible `show_main_window()` call. If showing the main window fails (e.g., window `.show()` or `.set_focus()` returns an error), the URI is already consumed and lost, and `hide_clip_prompt` is never called — leaving the prompt window visible with stale/no state. Move the `.take()` (or the entire accept block) after the fallible call, or restore the URI on error.</violation>
</file>

<file name="src-tauri/src/managers/clip_prompt.rs">

<violation number="1" location="src-tauri/src/managers/clip_prompt.rs:117">
P3: Prompt positioning and window-construction behavior now has a second near-identical implementation, so future fixes to tray anchoring or monitor clamping can easily diverge between flyout and clip prompt. Suggest extracting the shared desktop popup builder/positioning path into a common helper and passing size/label-specific parameters.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/renderer/components/Native/EngineClient.vue Outdated
Comment thread src/renderer/components/Native/EngineClient.vue Outdated
Comment thread src/renderer/components/Native/EngineClient.vue Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/renderer/components/Native/EngineClient.vue (2)

753-780: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Silent notification loss when OS permission is denied.

When document.hidden && !is.android(), sendOsNotification is the only notification path; if permission was previously denied (granted stays false), the function returns at Line 779 with no fallback, so the user never sees a completion notification at all while the window is hidden.

🤖 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/renderer/components/Native/EngineClient.vue` around lines 753 - 780,
Silent notification loss occurs in sendOsNotification when permission is denied
and the app is hidden. Update the notification flow in EngineClient.vue so the
document.hidden && !is.android() branch still falls back to a visible in-app or
web Notification path when plugin-notification permission is not granted,
instead of returning early after the isPermissionGranted/requestPermission
checks. Use the existing sendOsNotification and notify/onAction handling to
centralize the fallback logic and ensure hidden-window completions still surface
to the user.

766-805: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Desktop hidden notifications need a desktop click path onAction is mobile-only in @tauri-apps/plugin-notification, so the document.hidden && !is.android() branch won’t open the folder on macOS/Windows/Linux. Use a desktop-supported click handler or fallback here.

🤖 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/renderer/components/Native/EngineClient.vue` around lines 766 - 805, In
sendOsNotification, the current onAction-based folder launch only works for
mobile, so desktop notifications won’t open the folder when the document is
hidden. Update the notification action wiring to use a desktop-supported
click/action callback or add a desktop fallback path for macOS/Windows/Linux,
and keep the folderPath handling and showItemInFolder call inside the same
sendOsNotification flow. Make sure the notifyActionUnlisten lifecycle still
clears correctly when the handler is unavailable or fails.
src-tauri/src/commands/engine_cmds.rs (1)

167-181: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sanitize ed2k filenames before joining the download path
src-tauri/risuko-engine/src/engine/ed2k/parser.rs decodes file_name directly, and src-tauri/risuko-engine/src/engine/ed2k/download.rs joins it into PathBuf::from(dir) with no safe_filename/separator check. A crafted %2F or %5C in the ed2k name can escape the target directory; apply the same filename sanitization used by gnutella/g2 here.

🤖 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/src/commands/engine_cmds.rs` around lines 167 - 181, The ed2k
filename handling is accepting decoded names that may still contain path
separators, allowing a crafted name to escape the intended download directory.
Update the ed2k parsing and download flow in the ed2k parser/download logic to
sanitize the decoded filename the same way as the gnutella/g2 path, using the
existing safe filename/separator checks before joining into PathBuf::from(dir).
Make the change in the ed2k parser that produces the name and ensure the
download code only uses a sanitized final filename when constructing the
destination path.
🤖 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/src/commands/file_cmds.rs`:
- Around line 920-951: The custom percent-decoding logic in
percent_decode_lossy, percent_decode_strict, and percent_decode_bytes should be
replaced with the standard percent-encoding crate instead of maintaining a
hand-rolled decoder. Update these helpers to use the crate’s decoding API for
URI filename parsing, preserving the current lossy vs strict behavior while
removing the local hex/parsing loop and relying on the well-tested
implementation.

In `@src/renderer/components/Native/EngineClient.vue`:
- Around line 794-798: The sendNotification call in EngineClient.vue is firing a
promise without handling rejections, so failures bypass the existing try/catch.
Update the notification path to await sendNotification (or otherwise chain a
.catch) inside the same async flow where title, body, and extra are built, so
any rejection is caught by the surrounding catch (err) block. Use the
sendNotification invocation in this notification-handling section as the target
for the fix.

---

Outside diff comments:
In `@src-tauri/src/commands/engine_cmds.rs`:
- Around line 167-181: The ed2k filename handling is accepting decoded names
that may still contain path separators, allowing a crafted name to escape the
intended download directory. Update the ed2k parsing and download flow in the
ed2k parser/download logic to sanitize the decoded filename the same way as the
gnutella/g2 path, using the existing safe filename/separator checks before
joining into PathBuf::from(dir). Make the change in the ed2k parser that
produces the name and ensure the download code only uses a sanitized final
filename when constructing the destination path.

In `@src/renderer/components/Native/EngineClient.vue`:
- Around line 753-780: Silent notification loss occurs in sendOsNotification
when permission is denied and the app is hidden. Update the notification flow in
EngineClient.vue so the document.hidden && !is.android() branch still falls back
to a visible in-app or web Notification path when plugin-notification permission
is not granted, instead of returning early after the
isPermissionGranted/requestPermission checks. Use the existing
sendOsNotification and notify/onAction handling to centralize the fallback logic
and ensure hidden-window completions still surface to the user.
- Around line 766-805: In sendOsNotification, the current onAction-based folder
launch only works for mobile, so desktop notifications won’t open the folder
when the document is hidden. Update the notification action wiring to use a
desktop-supported click/action callback or add a desktop fallback path for
macOS/Windows/Linux, and keep the folderPath handling and showItemInFolder call
inside the same sendOsNotification flow. Make sure the notifyActionUnlisten
lifecycle still clears correctly when the handler is unavailable or fails.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c5dd45a5-fd32-4563-998d-9760320f71f0

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0183a and d70a988.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • package.json
  • src-tauri/Cargo.toml
  • src-tauri/risuko-bt/Cargo.toml
  • src-tauri/risuko-bt/src/peer/connection.rs
  • src-tauri/risuko-bt/src/wire.rs
  • src-tauri/risuko-bt/src/wire/mse.rs
  • src-tauri/risuko-engine/Cargo.toml
  • src-tauri/src/commands/clipboard_cmds.rs
  • src-tauri/src/commands/engine_cmds.rs
  • src-tauri/src/commands/file_cmds.rs
  • src-tauri/src/managers/clip_prompt.rs
  • src/renderer/components/ClipboardWatcher/ClipPrompt.vue
  • src/renderer/components/Legal/LegalGate.vue
  • src/renderer/components/Native/EngineClient.vue
  • src/renderer/components/Task/Index.vue
  • src/renderer/components/TaskDetail/TaskGeneral.vue
  • src/renderer/components/ui/button/index.ts
  • src/renderer/lib/utils.ts
  • src/renderer/styles/global.css
  • src/shared/locales/en-US/app.ts
  • src/shared/locales/en-US/task.ts
  • src/shared/locales/zh-CN/app.ts
  • src/shared/locales/zh-CN/preferences.ts
  • src/shared/locales/zh-CN/task.ts
  • src/shared/locales/zh-TW/app.ts
  • src/shared/locales/zh-TW/preferences.ts
  • src/shared/locales/zh-TW/task.ts
  • src/shared/syncCategories.ts
💤 Files with no reviewable changes (2)
  • src-tauri/risuko-bt/Cargo.toml
  • src/renderer/styles/global.css

Comment thread src-tauri/src/commands/file_cmds.rs
Comment on lines +794 to +798
sendNotification({
title,
body,
extra: path ? { folderPath: path } : undefined,
});

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

Unhandled rejection risk on sendNotification.

sendNotification({...}) is called without await/.catch. Since it isn't chained inside the surrounding try, a rejected promise here becomes an unhandled rejection instead of hitting the existing catch (err) block below.

🐛 Proposed fix
-				sendNotification({
-					title,
-					body,
-					extra: path ? { folderPath: path } : undefined,
-				});
+				await sendNotification({
+					title,
+					body,
+					extra: path ? { folderPath: path } : undefined,
+				});
📝 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
sendNotification({
title,
body,
extra: path ? { folderPath: path } : undefined,
});
await sendNotification({
title,
body,
extra: path ? { folderPath: path } : undefined,
});
🤖 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/renderer/components/Native/EngineClient.vue` around lines 794 - 798, The
sendNotification call in EngineClient.vue is firing a promise without handling
rejections, so failures bypass the existing try/catch. Update the notification
path to await sendNotification (or otherwise chain a .catch) inside the same
async flow where title, body, and extra are built, so any rejection is caught by
the surrounding catch (err) block. Use the sendNotification invocation in this
notification-handling section as the target for the fix.

@YueMiyuki
YueMiyuki merged commit c144682 into master Jul 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

next The "next" steps

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant