Conversation
WalkthroughThis 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. ChangesClipboard watch, clip-prompt window, notifications, legal gate
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
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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>
| } | ||
|
|
||
| #[cfg(not(target_os = "android"))] | ||
| fn position_prompt(app: &tauri::AppHandle, window: &WebviewWindow) { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 winClear
pending_clip_urion clip-prompt blur hide.Focused(false)only hides the window here, so a blur-based dismiss can leavepending_clip_uriset and makeget_clip_prompt_urireturn a stale URI later. Route this path through the same cleanup asclip_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 winAdd error handling to
handleCopyClick.Same gap as
Task/Index.vue'shandleCopyTaskLink: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
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlsrc-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (44)
.github/workflows/release.ymlpackage.jsonsrc-tauri/Cargo.tomlsrc-tauri/capabilities/clip-prompt.jsonsrc-tauri/capabilities/desktop.jsonsrc-tauri/risuko-engine/src/config/defaults.rssrc-tauri/risuko-engine/src/engine/manager.rssrc-tauri/src/commands/clipboard_cmds.rssrc-tauri/src/commands/mod.rssrc-tauri/src/lib.rssrc-tauri/src/managers/clip_prompt.rssrc-tauri/src/managers/mod.rssrc-tauri/src/state.rssrc-tauri/tauri.conf.jsonsrc/renderer/components/ClipboardWatcher/ClipPrompt.vuesrc/renderer/components/Legal/LegalGate.vuesrc/renderer/components/Native/EngineClient.vuesrc/renderer/components/Native/Ipc.vuesrc/renderer/components/Preference/Advanced.vuesrc/renderer/components/Preference/Basic.vuesrc/renderer/components/Share/Index.vuesrc/renderer/components/Task/Index.vuesrc/renderer/components/TaskDetail/TaskGeneral.vuesrc/renderer/pages/index/App.vuesrc/renderer/pages/index/clip-prompt.htmlsrc/renderer/pages/index/clip-prompt.tssrc/renderer/utils/clipboard.tssrc/shared/configKeys.tssrc/shared/constants.tssrc/shared/locales/en-US/app.tssrc/shared/locales/en-US/preferences.tssrc/shared/locales/en-US/sync.tssrc/shared/locales/en-US/task.tssrc/shared/locales/zh-CN/app.tssrc/shared/locales/zh-CN/preferences.tssrc/shared/locales/zh-CN/sync.tssrc/shared/locales/zh-CN/task.tssrc/shared/locales/zh-TW/app.tssrc/shared/locales/zh-TW/preferences.tssrc/shared/locales/zh-TW/sync.tssrc/shared/locales/zh-TW/task.tssrc/shared/syncCategories.tssrc/shared/types/config.tsvite.renderer.config.ts
| if let Ok(mut seen) = state.last_clipboard_seen.lock() { | ||
| if seen.as_deref() == Some(text.as_str()) { | ||
| return; | ||
| } | ||
| *seen = Some(text.clone()); | ||
| } |
There was a problem hiding this comment.
🔒 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.
| #[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", | ||
| &[] | ||
| )); | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 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.
| const { writeText } = await import("@tauri-apps/plugin-clipboard-manager"); | ||
| await writeText(text); | ||
| } catch { | ||
| await navigator.clipboard.writeText(text); |
There was a problem hiding this comment.
🩺 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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 winSilent notification loss when OS permission is denied.
When
document.hidden && !is.android(),sendOsNotificationis the only notification path; if permission was previously denied (grantedstaysfalse), 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 winDesktop hidden notifications need a desktop click path
onActionis mobile-only in@tauri-apps/plugin-notification, so thedocument.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 winSanitize ed2k filenames before joining the download path
src-tauri/risuko-engine/src/engine/ed2k/parser.rsdecodesfile_namedirectly, andsrc-tauri/risuko-engine/src/engine/ed2k/download.rsjoins it intoPathBuf::from(dir)with nosafe_filename/separator check. A crafted%2For%5Cin 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
⛔ Files ignored due to path filters (2)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlsrc-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (28)
package.jsonsrc-tauri/Cargo.tomlsrc-tauri/risuko-bt/Cargo.tomlsrc-tauri/risuko-bt/src/peer/connection.rssrc-tauri/risuko-bt/src/wire.rssrc-tauri/risuko-bt/src/wire/mse.rssrc-tauri/risuko-engine/Cargo.tomlsrc-tauri/src/commands/clipboard_cmds.rssrc-tauri/src/commands/engine_cmds.rssrc-tauri/src/commands/file_cmds.rssrc-tauri/src/managers/clip_prompt.rssrc/renderer/components/ClipboardWatcher/ClipPrompt.vuesrc/renderer/components/Legal/LegalGate.vuesrc/renderer/components/Native/EngineClient.vuesrc/renderer/components/Task/Index.vuesrc/renderer/components/TaskDetail/TaskGeneral.vuesrc/renderer/components/ui/button/index.tssrc/renderer/lib/utils.tssrc/renderer/styles/global.csssrc/shared/locales/en-US/app.tssrc/shared/locales/en-US/task.tssrc/shared/locales/zh-CN/app.tssrc/shared/locales/zh-CN/preferences.tssrc/shared/locales/zh-CN/task.tssrc/shared/locales/zh-TW/app.tssrc/shared/locales/zh-TW/preferences.tssrc/shared/locales/zh-TW/task.tssrc/shared/syncCategories.ts
💤 Files with no reviewable changes (2)
- src-tauri/risuko-bt/Cargo.toml
- src/renderer/styles/global.css
| sendNotification({ | ||
| title, | ||
| body, | ||
| extra: path ? { folderPath: path } : undefined, | ||
| }); |
There was a problem hiding this comment.
🩺 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.
| 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.
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
@tauri-apps/plugin-notificationfor native notifications when the app is in the background; clicking opens the file’s folder.Dependencies
@tauri-apps/plugin-notification(renderer),tauri-plugin-clipboard,tauri-plugin-notification, and macOSobjc2-app-kit.class-variance-authority/clsx/tailwind-mergewithcnfast; removednormalize.css.urlencodingwithpercent-encoding; removed therc4crate and implemented RC4 in-house for BitTorrent MSE.notification:default; newclip-promptwindow capability.bundle.createUpdaterArtifactson tagged builds; updater endpoint switched to https://risuko.app.Written for commit 823de14. Summary will update on new commits.