Skip to content

Add initial Android build and development support - #83

Merged
YueMiyuki merged 2 commits into
YueMiyuki:next-devfrom
slwyts:master
May 28, 2026
Merged

Add initial Android build and development support#83
YueMiyuki merged 2 commits into
YueMiyuki:next-devfrom
slwyts:master

Conversation

@slwyts

@slwyts slwyts commented May 25, 2026

Copy link
Copy Markdown

This pull request introduces initial Android build and development support as an opt-in feature, ensuring that desktop development remains unaffected. It adds documentation, scripts, and CI/CD integration for Android, and refactors capability and dependency management to isolate desktop-only features from Android builds.

Android Support:

  • Added new scripts (android:init, dev:android, build:android, run:android) in package.json and a dedicated scripts/android.mjs to automate Android project setup, build, signing, and icon sync. [1] [2]
  • Introduced detailed Android development documentation in both English and Chinese (docs/ANDROID.md, docs/ANDROID-CN.md), and updated README.md, README-CN.md, CONTRIBUTING.md, and CONTRIBUTING-CN.md to clarify that Android is an optional development path. [1] [2] [3] [4] [5] [6]
  • Added a release-android job to the GitHub Actions workflow for automated Android artifact builds, signing, and release/upload for tagged and manual builds.
  • Updated scripts/build.mjs to prevent accidental Android builds using the desktop build command, directing users to the new Android workflow.

Desktop/Android Isolation:

  • Refactored Rust dependencies in src-tauri/Cargo.toml to move desktop-only crates and plugins (open, trash, tauri-plugin-autostart, tauri-plugin-nosleep, tauri-plugin-single-instance) behind platform-specific targets, so they are excluded from Android builds. [1] [2] [3]
  • Split Tauri capability files: default.json now covers shared permissions, while a new desktop.json isolates desktop-only features, ensuring Android builds do not request or include desktop-specific APIs. [1] [2] [3]

Summary by CodeRabbit

  • New Features

    • Added comprehensive Android support including a native mobile interface with bottom navigation, back gesture handling, and Android-specific workflows
    • Introduced custom log directory override setting for advanced users
    • Added automatic locale detection with "auto" option
    • Implemented Android download notifications and directory picking functionality
  • Build & Release

    • Added automated Android APK build, signing, and release workflow
    • Configured Android-specific dependencies and conditional compilation
  • Improvements

    • Enhanced platform-specific file operations and UI behaviors
    • Improved task selection and batch operations for multi-file torrents
    • Optimized responsive styling for mobile devices

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f790b0d-c727-4e5c-87d1-8986265bb4fb

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 and usage tips.

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

2 issues found across 35 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="scripts/android.mjs">

<violation number="1" location="scripts/android.mjs:99">
P2: Keystore path written to Java properties file without normalizing separators, which will corrupt paths on Windows due to backslash escape handling in `java.util.Properties`.</violation>

<violation number="2" location="scripts/android.mjs:167">
P2: Signing block injection relies on hardcoded `\n\tbuildTypes {` / `\n    buildTypes {` patterns and silently falls back to a warning if neither matches. Different indentation, CRLF, or template changes will skip the release signing config insertion and leave CI builds unsigned.</violation>
</file>

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

Re-trigger cubic

Comment thread scripts/android.mjs Outdated
if (content.includes("\n\tbuildTypes {")) {
content = content.replace("\n\tbuildTypes {", `${signingBlock}\n\tbuildTypes {`);
changed = true;
} else if (content.includes("\n buildTypes {")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Signing block injection relies on hardcoded \n\tbuildTypes { / \n buildTypes { patterns and silently falls back to a warning if neither matches. Different indentation, CRLF, or template changes will skip the release signing config insertion and leave CI builds unsigned.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/android.mjs, line 167:

<comment>Signing block injection relies on hardcoded `\n\tbuildTypes {` / `\n    buildTypes {` patterns and silently falls back to a warning if neither matches. Different indentation, CRLF, or template changes will skip the release signing config insertion and leave CI builds unsigned.</comment>

<file context>
@@ -0,0 +1,240 @@
+		if (content.includes("\n\tbuildTypes {")) {
+			content = content.replace("\n\tbuildTypes {", `${signingBlock}\n\tbuildTypes {`);
+			changed = true;
+		} else if (content.includes("\n    buildTypes {")) {
+			content = content.replace("\n    buildTypes {", `${signingBlock}\n    buildTypes {`);
+			changed = true;
</file context>

Comment thread scripts/android.mjs Outdated
`keyAlias=${keyAlias}`,
`keyPassword=${keyPassword}`,
`storePassword=${storePassword}`,
`storeFile=${ciKeystorePath}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Keystore path written to Java properties file without normalizing separators, which will corrupt paths on Windows due to backslash escape handling in java.util.Properties.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/android.mjs, line 99:

<comment>Keystore path written to Java properties file without normalizing separators, which will corrupt paths on Windows due to backslash escape handling in `java.util.Properties`.</comment>

<file context>
@@ -0,0 +1,240 @@
+			`keyAlias=${keyAlias}`,
+			`keyPassword=${keyPassword}`,
+			`storePassword=${storePassword}`,
+			`storeFile=${ciKeystorePath}`,
+			"",
+		].join("\n"),
</file context>
Suggested change
`storeFile=${ciKeystorePath}`,
+ `storeFile=${ciKeystorePath.replace(/\\/g, '/')}`,

@YueMiyuki

Copy link
Copy Markdown
Owner

Rebased to next-dev:latest

@coderabbitai coderabbitai Bot added the next The "next" steps label May 28, 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.

3 issues found across 191 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/config_cmds.rs">

<violation number="1" location="src-tauri/src/commands/config_cmds.rs:200">
P2: Android `save_preference` silently persists `open-at-login` while `get_app_config` hardcodes it to `false`, creating inconsistent config round-trips.</violation>
</file>

<file name="src-tauri/src/commands/health_cmds.rs">

<violation number="1" location="src-tauri/src/commands/health_cmds.rs:242">
P2: The `tools` health check category is silently excluded on Android rather than returning a `Skipped`/`unsupported` check, breaking consistency with how other desktop-only features (autostart, sleep-inhibit) are gracefully handled in `check_system`.</violation>
</file>

<file name="src-tauri/risuko-engine/src/config/defaults.rs">

<violation number="1" location="src-tauri/risuko-engine/src/config/defaults.rs:67">
P2: Android default download path selection uses `parent.exists()` instead of verifying write access, and hardcodes the application package ID in the fallback path. On Android 10+ with scoped storage, `/storage/emulated/0/Download` always exists but may not be writable without `MANAGE_EXTERNAL_STORAGE` being granted by the user, causing silent download failures on first run. The fallback path hardcodes `app.risuko.mobile`, creating a fragile coupling with `build.gradle.kts`.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic

}
}

fn is_open_at_login_enabled(handle: &AppHandle) -> Result<bool, String> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Android save_preference silently persists open-at-login while get_app_config hardcodes it to false, creating inconsistent config round-trips.

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

<comment>Android `save_preference` silently persists `open-at-login` while `get_app_config` hardcodes it to `false`, creating inconsistent config round-trips.</comment>

<file context>
@@ -183,13 +180,33 @@ pub fn prepare_preference_patch(params: Value) -> Result<Value, String> {
+    }
+}
+
+fn is_open_at_login_enabled(handle: &AppHandle) -> Result<bool, String> {
+    #[cfg(target_os = "android")]
+    {
</file context>

cats.push(HealthCategory::from_checks("logs", check_logs(&log_dir)));
}
if want("tools") {
if want("tools") && !cfg!(target_os = "android") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The tools health check category is silently excluded on Android rather than returning a Skipped/unsupported check, breaking consistency with how other desktop-only features (autostart, sleep-inhibit) are gracefully handled in check_system.

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

<comment>The `tools` health check category is silently excluded on Android rather than returning a `Skipped`/`unsupported` check, breaking consistency with how other desktop-only features (autostart, sleep-inhibit) are gracefully handled in `check_system`.</comment>

<file context>
@@ -237,7 +239,7 @@ pub async fn run_health_checks(
         cats.push(HealthCategory::from_checks("logs", check_logs(&log_dir)));
     }
-    if want("tools") {
+    if want("tools") && !cfg!(target_os = "android") {
         cats.push(HealthCategory::from_checks("tools", check_tools().await));
     }
</file context>

// file manager. The app needs storage permission to write there
// on Android 10+; if missing, the user can pick another folder
// via the directory picker.
let public_downloads = std::path::PathBuf::from("/storage/emulated/0/Download/Risuko");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Android default download path selection uses parent.exists() instead of verifying write access, and hardcodes the application package ID in the fallback path. On Android 10+ with scoped storage, /storage/emulated/0/Download always exists but may not be writable without MANAGE_EXTERNAL_STORAGE being granted by the user, causing silent download failures on first run. The fallback path hardcodes app.risuko.mobile, creating a fragile coupling with build.gradle.kts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src-tauri/risuko-engine/src/config/defaults.rs, line 67:

<comment>Android default download path selection uses `parent.exists()` instead of verifying write access, and hardcodes the application package ID in the fallback path. On Android 10+ with scoped storage, `/storage/emulated/0/Download` always exists but may not be writable without `MANAGE_EXTERNAL_STORAGE` being granted by the user, causing silent download failures on first run. The fallback path hardcodes `app.risuko.mobile`, creating a fragile coupling with `build.gradle.kts`.</comment>

<file context>
@@ -53,6 +54,44 @@ pub fn system_defaults() -> Map<String, Value> {
+        // file manager. The app needs storage permission to write there
+        // on Android 10+; if missing, the user can pick another folder
+        // via the directory picker.
+        let public_downloads = std::path::PathBuf::from("/storage/emulated/0/Download/Risuko");
+        if let Some(parent) = public_downloads.parent() {
+            if parent.exists() {
</file context>

@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/Cargo.toml (1)

60-60: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move open = "5" under the desktop-only target and cfg-gate capture_user_agent too.

  • open = "5" is currently global in src-tauri/Cargo.toml (line 60).
  • Most open::that(...) callsites are already compiled out on Android (tray.rs, menu.rs, and the #[cfg(not(target_os = "android"))] branch of file_cmds.rs).
  • But open::that(&url) is used in #[tauri::command] capture_user_agent in src-tauri/src/commands/cookie_cmds.rs with no Android cfg, and commands::cookie_cmds::capture_user_agent is registered unconditionally in src-tauri/src/lib.rs, so moving the dependency alone would break Android builds (or keep Android exposed to an unlisted platform crate).
Suggested patch
-open = "5"
@@
 [target.'cfg(not(target_os = "android"))'.dependencies]
+open = "5"
 trash = "5"
 tauri-plugin-nosleep = { git = "https://github.com/pevers/tauri-plugin-nosleep", rev = "6f6ab76ec171d075476b585282290fcf2e1c40b5" }
 tauri-plugin-autostart = "2"
 tauri-plugin-single-instance = "2"
🤖 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/Cargo.toml` at line 60, The Cargo.toml entry open = "5" is
currently global and must be moved under a desktop-only target (e.g.
[target.'cfg(not(target_os = "android"))'.dependencies]) and the
capture_user_agent command must be cfg-gated so Android builds don't reference
open. Relocate the open dependency into the desktop-only target section in
Cargo.toml, add #[cfg(not(target_os = "android"))] to the capture_user_agent
function in commands::cookie_cmds::capture_user_agent (and any use/imports of
open there), and also gate its registration in src-tauri/src/lib.rs so
commands::cookie_cmds::capture_user_agent is only registered on non-Android
targets.
src-tauri/risuko-cookies/src/lib.rs (1)

358-448: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tests will fail to compile on Android.

Three tests call functions that are conditionally compiled out on Android: registrable_domain() and cookie_domain_matches_host(). This will cause compilation failures when running cargo test on the Android target, blocking test execution and potentially breaking CI.

🔧 Proposed fix to gate Android-incompatible tests
     #[test]
+    #[cfg(not(target_os = "android"))]
     fn registrable_strips_one_label() {
         assert_eq!(
             registrable_domain("a.b.example.com"),
             Some("example.com".into())
         );
         assert_eq!(
             registrable_domain("example.com"),
             Some("example.com".into())
         );
         assert_eq!(registrable_domain("localhost"), None);
     }

     #[test]
+    #[cfg(not(target_os = "android"))]
     fn registrable_widens_to_three_labels_for_known_multi_part_tlds() {
         assert_eq!(
             registrable_domain("foo.example.co.uk"),
             Some("example.co.uk".into())
         );
         assert_eq!(
             registrable_domain("a.b.bbc.co.uk"),
             Some("bbc.co.uk".into())
         );
         assert_eq!(
             registrable_domain("shop.example.com.au"),
             Some("example.com.au".into())
         );
         // 2-label inputs cannot widen further; return as-is
         assert_eq!(registrable_domain("co.uk"), Some("co.uk".into()));
     }

     #[test]
+    #[cfg(not(target_os = "android"))]
     fn cookie_domain_host_match_rules() {
         // Exact match
         assert!(cookie_domain_matches_host("example.com", "example.com"));
         // Leading dot is treated like host-only (legacy compatibility)
         assert!(cookie_domain_matches_host(".example.com", "dl.example.com"));
         // Subdomain match
         assert!(cookie_domain_matches_host("example.com", "dl.example.com"));
         // Suffix-but-not-dot-bounded must NOT match
         assert!(!cookie_domain_matches_host("example.com", "notexample.com"));
         // Unrelated domain
         assert!(!cookie_domain_matches_host("attacker.com", "example.com"));
         // Public-suffix-style cookie should not match unrelated subdomains
         assert!(!cookie_domain_matches_host(".co.uk", "example.com"));
         // But still legitimately matches when host is on that domain
         assert!(cookie_domain_matches_host(".co.uk", "foo.co.uk"));
     }
🤖 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/risuko-cookies/src/lib.rs` around lines 358 - 448, Gate the
Android-incompatible tests so they don't compile on Android: add a cfg attribute
like #[cfg(not(target_os = "android"))] to the tests that call the
conditionally-compiled functions (the test functions
registrable_strips_one_label,
registrable_widens_to_three_labels_for_known_multi_part_tlds, and
cookie_domain_host_match_rules) or wrap them in a sub-module annotated with
#[cfg(not(target_os = "android"))]; this prevents compilation on Android where
registrable_domain and cookie_domain_matches_host are absent while leaving other
tests (e.g. extract_host and cookies_to_header_format) unaffected.
🤖 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 @.github/workflows/release.yml:
- Line 21: The workflow uses mutable action tags (e.g., actions/checkout@v4,
actions/setup-node@v4, android-actions/setup-android@v3,
dtolnay/rust-toolchain@stable, softprops/action-gh-release@v2,
actions/upload-artifact@v4, tauri-apps/tauri-action@v0 and others referenced in
the file) which must be pinned to immutable commit SHAs; update every uses:
entry (including the ones called out in the review) to the corresponding full
commit SHA for that action, replacing tags like actions/checkout@v4 with
actions/checkout@<commit-sha> and do the same for actions/setup-node,
android-actions/setup-android, dtolnay/rust-toolchain,
softprops/action-gh-release, actions/upload-artifact, tauri-apps/tauri-action,
etc., ensuring each uses: line in the workflow is updated to a specific commit
SHA and then run the workflow to verify no breakages.
- Around line 20-22: Update both actions/checkout@v4 steps named "Check out Git
repository" used in the android-release job and in the release matrix to
explicitly disable persisted credentials: add a with block containing
persist-credentials: false so checkout does not leave GITHUB_TOKEN credentials
available to later steps; locate the steps that use actions/checkout@v4 and
insert the with: persist-credentials: false setting for each occurrence.

In @.gitignore:
- Around line 31-32: Replace the blanket unignore of src-tauri/gen/android/**
with targeted unignore rules: keep the existing negation for
src-tauri/gen/android/ but remove or restrict src-tauri/gen/android/** and
instead explicitly unignore only the specific source/config files needed (e.g.,
gradle wrapper, settings, and manifest files) and re-add ignore patterns for
transient artifacts like **/build/** and **/.gradle/** so generated build
outputs are not committed; update the .gitignore entries around
src-tauri/gen/android/, src-tauri/gen/android/**, **/build/** and .gradle/**
accordingly.

In `@scripts/android-env.mjs`:
- Line 4: The PATH is being constructed using a hardcoded ":" which fails on
Windows; update scripts/android-env.mjs to use path.delimiter instead of ":"
when joining PATH entries—import the delimiter (or the whole path module)
alongside join (e.g., include delimiter from "node:path") and replace the ":"
usage in the PATH construction (where the PATH variable is assembled) with
path.delimiter so it works cross-platform.

In `@src-tauri/src/lib.rs`:
- Around line 90-98: The code currently only calls
std::fs::create_dir_all(&candidate) and treats that as proof the override is
writable; instead, after create_dir_all(&candidate) succeeds, attempt to create
(and immediately remove) a small temp file inside candidate (e.g.
candidate.join(".writetest-<pid>-<uniq>")) using
OpenOptions::new().create(true).write(true).open(...), handle errors by logging
the error (use the same eprintln format) and return
default_log_dir.to_path_buf(); on success remove the temp file and return
candidate; keep references to candidate and default_log_dir so the change is
localized and ensures an existing but non-writable directory will fall back to
the default.

In `@src/renderer/components/Native/ShowInFolder.vue`:
- Around line 2-4: The icon-only button in ShowInFolder.vue (the <button
class="show-in-folder" `@click.stop`="onFolderClick"> wrapping the FolderOpen
component) lacks an accessible name; add one by providing an explicit label
(e.g., aria-label="Show in folder" or a localized string) on the button or
include offscreen/visually-hidden text inside the button so screen readers
announce the action; ensure the label is kept in sync with any i18n and does not
affect the existing `@click.stop`="onFolderClick" behavior.

In `@src/renderer/components/Preference/Advanced.vue`:
- Around line 1113-1116: The mo-show-in-folder button is bound to logPath
causing it to open the saved folder even when the textarea is showing an unsaved
override (visibleLogPath); change the binding so the reveal action uses the
currently visible value. Update the template to pass visibleLogPath (fallback to
logPath if empty) into <mo-show-in-folder> and ensure any handler or prop
consumers (e.g., the component receiving :path) use that same visible value so
the folder reveal matches what the user sees; adjust references around
handleLogDirSelected / visibleLogPath to keep behavior consistent.

In `@src/renderer/components/Task/Index.vue`:
- Around line 76-83: The Select emits string values so before persisting call
setTasksPerPage convert the incoming value to a number (e.g. Number(value) or
parseInt(value, 10)) in onTasksPerPageChange (the handler bound to
`@update`:model-value) and anywhere setTasksPerPage is used (referenced around the
setTasksPerPage function) to ensure tasksPerPage is stored as a numeric type
instead of a string; update the model-value binding initialization
`${tasksPerPage}` if necessary so that the component receives/works with numbers
consistently.

In `@src/renderer/components/Task/TaskItemActions.vue`:
- Around line 3-20: The icon-only action buttons rendered in TaskItemActions.vue
(the <button> iterating over taskActions and invoking onActionClick) lack
accessible names; add an accessible label by binding an aria-label (and
optionally title) that maps each action value to a human-readable string (e.g.,
map 'PAUSE'→'Pause task', 'STOP'→'Stop task', 'RESUME'→'Resume task',
'RESTART'→'Restart task', 'DELETE'→'Delete permanently', 'TRASH'→'Move to
trash', 'FOLDER'→'Open folder', 'LINK'→'Open link', 'INFO'→'Show info') and use
:aria-label="getActionLabel(action)" on the <button> (implement a getActionLabel
helper or computed that returns the labels) so screen readers can announce each
button while preserving the existing onActionClick behavior.

In `@src/renderer/store/task.ts`:
- Around line 474-481: The current filter only checks base gids which lets stale
compound keys like "gid#f3" remain in selectedGidList; instead build a Set of
actual current row keys from orderedData (use task.key or task.rowKey if
present, falling back to task.gid) and filter this.selectedGidList by exact
membership in that Set so keys with suffixes are validated correctly; after
that, ensure any derived selection state (e.g., selectedGids) is
recomputed/updated from the cleaned selectedGidList.

In `@src/renderer/styles/android.css`:
- Line 500: Fix Stylelint violations: change the color property value from
"currentColor" to the lint-expected casing (e.g., "currentcolor"), reformat any
calc(...) expressions to keep operators on the same line with proper spacing
(e.g., "calc(100% - 10px)" instead of breaking the operator to a new line), and
insert the required empty line(s) before declaration blocks per the
declaration-empty-line-before rule. Locate occurrences by searching for the
literal "color: currentColor", any "calc(" usage, and the declaration blocks
around the ranges flagged (near the calc and color occurrences mentioned) and
apply the three fixes consistently across those blocks.

In `@src/renderer/styles/components/preferences.css`:
- Around line 297-314: The .dev-log-path-input rule uses scrollbar-width: thin
(Firefox only), so add WebKit scrollbar pseudo-element rules for cross‑browser
parity: append ::-webkit-scrollbar, ::-webkit-scrollbar-track,
::-webkit-scrollbar-thumb and ::-webkit-scrollbar-thumb:hover selectors for
.dev-log-path-input to set scrollbar height (e.g. 6px), transparent track, thumb
color using var(--border) and hover color using var(--muted-foreground), and a
small border-radius (e.g. 3px); place these rules immediately after the
.dev-log-path-input block so Chromium/Safari/Edge render a thin styled scrollbar
like Firefox.

In `@src/renderer/utils/native.ts`:
- Around line 17-24: The dirname function fails for Windows paths because it
uses value.lastIndexOf("/") only; update dirname (function name: dirname) to
consider backslashes as well—either normalize backslashes to forward slashes
before computing the index or compute the last separator index with
Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\")). Then use that index
to slice the directory portion and keep the existing trimming of trailing
separators.

---

Outside diff comments:
In `@src-tauri/Cargo.toml`:
- Line 60: The Cargo.toml entry open = "5" is currently global and must be moved
under a desktop-only target (e.g. [target.'cfg(not(target_os =
"android"))'.dependencies]) and the capture_user_agent command must be cfg-gated
so Android builds don't reference open. Relocate the open dependency into the
desktop-only target section in Cargo.toml, add #[cfg(not(target_os =
"android"))] to the capture_user_agent function in
commands::cookie_cmds::capture_user_agent (and any use/imports of open there),
and also gate its registration in src-tauri/src/lib.rs so
commands::cookie_cmds::capture_user_agent is only registered on non-Android
targets.

In `@src-tauri/risuko-cookies/src/lib.rs`:
- Around line 358-448: Gate the Android-incompatible tests so they don't compile
on Android: add a cfg attribute like #[cfg(not(target_os = "android"))] to the
tests that call the conditionally-compiled functions (the test functions
registrable_strips_one_label,
registrable_widens_to_three_labels_for_known_multi_part_tlds, and
cookie_domain_host_match_rules) or wrap them in a sub-module annotated with
#[cfg(not(target_os = "android"))]; this prevents compilation on Android where
registrable_domain and cookie_domain_matches_host are absent while leaving other
tests (e.g. extract_host and cookies_to_header_format) unaffected.
🪄 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: a00e5d9e-4189-4cd0-ba97-13ce5d5e43c9

📥 Commits

Reviewing files that changed from the base of the PR and between 74c7042 and b5cadef.

⛔ Files ignored due to path filters (104)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src-tauri/Cargo.lock is excluded by !**/*.lock
  • src-tauri/gen/android/.editorconfig is excluded by !**/gen/**
  • src-tauri/gen/android/.gitignore is excluded by !**/gen/**
  • src-tauri/gen/android/app/.gitignore is excluded by !**/gen/**
  • src-tauri/gen/android/app/build.gradle.kts is excluded by !**/gen/**
  • src-tauri/gen/android/app/proguard-rules.pro is excluded by !**/gen/**
  • src-tauri/gen/android/app/proguard-tauri.pro is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/AndroidManifest.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/java/app/risuko/mobile/MainActivity.kt is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/java/app/risuko/mobile/RisukoForegroundService.kt is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/drawable/empty_splash_icon.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/drawable/ic_notification.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/layout/activity_main.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png, !**/gen/**
  • src-tauri/gen/android/app/src/main/res/values-night/themes.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/values-v31/themes.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/values/colors.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/values/strings.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/values/themes.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/src/main/res/xml/file_paths.xml is excluded by !**/gen/**
  • src-tauri/gen/android/app/tauri.build.gradle.kts is excluded by !**/gen/**
  • src-tauri/gen/android/app/tauri.properties is excluded by !**/gen/**
  • src-tauri/gen/android/build.gradle.kts is excluded by !**/gen/**
  • src-tauri/gen/android/buildSrc/build.gradle.kts is excluded by !**/gen/**
  • src-tauri/gen/android/buildSrc/src/main/java/app/risuko/mobile/kotlin/BuildTask.kt is excluded by !**/gen/**
  • src-tauri/gen/android/buildSrc/src/main/java/app/risuko/mobile/kotlin/RustPlugin.kt is excluded by !**/gen/**
  • src-tauri/gen/android/gradle.properties is excluded by !**/gen/**
  • src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar, !**/gen/**
  • src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties is excluded by !**/gen/**
  • src-tauri/gen/android/gradlew is excluded by !**/gen/**
  • src-tauri/gen/android/gradlew.bat is excluded by !**/gen/**
  • src-tauri/gen/android/settings.gradle is excluded by !**/gen/**
  • src-tauri/gen/android/tauri.settings.gradle is excluded by !**/gen/**
  • src-tauri/icons/128x128.png is excluded by !**/*.png
  • src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • src-tauri/icons/32x32.png is excluded by !**/*.png
  • src-tauri/icons/64x64.png is excluded by !**/*.png
  • src-tauri/icons/Square107x107Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square142x142Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square150x150Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square284x284Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square30x30Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square310x310Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square44x44Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square71x71Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square89x89Logo.png is excluded by !**/*.png
  • src-tauri/icons/StoreLogo.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/icon.ico is excluded by !**/*.ico
  • src-tauri/icons/icon.png is excluded by !**/*.png
  • src-tauri/icons/icon.svg is excluded by !**/*.svg
  • src-tauri/icons/ios/AppIcon-20x20@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-20x20@2x-1.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-20x20@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-20x20@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@2x-1.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@2x-1.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-512@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-60x60@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-60x60@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-76x76@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-76x76@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png is excluded by !**/*.png
  • src/renderer/assets/logo.svg is excluded by !**/*.svg
  • static/logo.svg is excluded by !**/*.svg
📒 Files selected for processing (87)
  • .github/workflows/release.yml
  • .gitignore
  • package.json
  • packages/risuko-app/package.json
  • packages/risuko-cli/npm/darwin-arm64/package.json
  • packages/risuko-cli/npm/darwin-x64/package.json
  • packages/risuko-cli/npm/linux-arm64-gnu/package.json
  • packages/risuko-cli/npm/linux-x64-gnu/package.json
  • packages/risuko-cli/npm/win32-arm64-msvc/package.json
  • packages/risuko-cli/npm/win32-x64-msvc/package.json
  • packages/risuko-cli/package.json
  • packages/risuko-js/npm/darwin-arm64/package.json
  • packages/risuko-js/npm/darwin-x64/package.json
  • packages/risuko-js/npm/linux-arm64-gnu/package.json
  • packages/risuko-js/npm/linux-x64-gnu/package.json
  • packages/risuko-js/npm/win32-arm64-msvc/package.json
  • packages/risuko-js/npm/win32-x64-msvc/package.json
  • packages/risuko-js/package.json
  • pnpm-workspace.yaml
  • scripts/android-env.mjs
  • scripts/sign-android-apks.mjs
  • src-tauri/Cargo.toml
  • src-tauri/capabilities/default.json
  • src-tauri/capabilities/desktop.json
  • src-tauri/icons/android/values/ic_launcher_background.xml
  • src-tauri/icons/icon.icns
  • src-tauri/risuko-cookies/Cargo.toml
  • src-tauri/risuko-cookies/src/lib.rs
  • src-tauri/risuko-engine/src/config/defaults.rs
  • src-tauri/risuko-engine/src/config/mod.rs
  • src-tauri/risuko-engine/src/engine/http.rs
  • src-tauri/src/commands/android_intent.rs
  • src-tauri/src/commands/app_cmds.rs
  • src-tauri/src/commands/config_cmds.rs
  • src-tauri/src/commands/event_cmds.rs
  • src-tauri/src/commands/file_cmds.rs
  • src-tauri/src/commands/health_cmds.rs
  • src-tauri/src/commands/mod.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/managers/menu.rs
  • src-tauri/src/managers/mod.rs
  • src-tauri/src/managers/tray.rs
  • src-tauri/tauri.android.conf.json
  • src/renderer/api/Api.ts
  • src/renderer/components/DragSelect/Index.vue
  • src/renderer/components/Health/Index.vue
  • src/renderer/components/Main.vue
  • src/renderer/components/Native/SelectDirectory.vue
  • src/renderer/components/Native/ShowInFolder.vue
  • src/renderer/components/Preference/Advanced.vue
  • src/renderer/components/Rss/Index.vue
  • src/renderer/components/Subnav/SubnavSwitcher.vue
  • src/renderer/components/Task/AddTask.vue
  • src/renderer/components/Task/Index.vue
  • src/renderer/components/Task/TaskActions.vue
  • src/renderer/components/Task/TaskItem.vue
  • src/renderer/components/Task/TaskItemActions.vue
  • src/renderer/components/Task/TaskList.vue
  • src/renderer/components/TaskDetail/Index.vue
  • src/renderer/components/TaskDetail/TaskGeneral.vue
  • src/renderer/components/ui/confirm-dialog/ConfirmDialog.vue
  • src/renderer/pages/index/App.vue
  • src/renderer/pages/index/main.ts
  • src/renderer/router/index.ts
  • src/renderer/shims/platform.ts
  • src/renderer/store/app.ts
  • src/renderer/store/preference.ts
  • src/renderer/store/task.ts
  • src/renderer/styles/android.css
  • src/renderer/styles/app.css
  • src/renderer/styles/components/input.css
  • src/renderer/styles/components/preferences.css
  • src/renderer/styles/components/task-detail.css
  • src/renderer/styles/components/task.css
  • src/renderer/utils/native.ts
  • src/shared/configKeys.ts
  • src/shared/locales/en-US/app.ts
  • src/shared/locales/en-US/preferences.ts
  • src/shared/locales/en-US/task.ts
  • src/shared/locales/index.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/types/config.ts

Comment on lines +20 to +22
- name: Check out Git repository
uses: actions/checkout@v4

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant workflow sections around the referenced lines
FILE=".github/workflows/release.yml"
echo "== File: $FILE =="
nl -ba "$FILE" | sed -n '1,120p'
echo
echo "== Snippet around lines 15-40 =="
nl -ba "$FILE" | sed -n '15,45p'

echo
echo "== Find all checkout uses in this workflow =="
rg -n "uses:\s*actions/checkout@v" "$FILE" -n || true

echo
echo "== For each checkout, show following lines to inspect 'with:' config =="
# Print checkout blocks (checkout line + next 12 lines)
python3 - <<'PY'
import re, itertools, pathlib
path = pathlib.Path(".github/workflows/release.yml")
lines = path.read_text().splitlines()
for i,l in enumerate(lines):
    if re.search(r"uses:\s*actions/checkout@v\d+", l):
        start = max(0, i-2)
        end = min(len(lines), i+14)
        print(f"\n--- checkout occurrence at line {i+1} ---")
        for j in range(start, end):
            ln = j+1
            print(f"{ln:4d} {lines[j]}")
PY

Repository: YueMiyuki/Risuko

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/release.yml"
echo "== File: $FILE (first 140 lines) =="
# Show with line numbers using awk
awk '{printf "%4d %s\n", NR, $0}' "$FILE" | sed -n '1,140p'

echo
echo "== Snippet around lines 15-45 (requested area) =="
awk '{if (NR>=15 && NR<=45) printf "%4d %s\n", NR, $0}' "$FILE"

echo
echo "== Find all checkout uses in this workflow =="
rg -n "uses:\s*actions/checkout@v" "$FILE" || true

echo
echo "== For each checkout occurrence, print nearby lines (uses line + 15 lines) =="
python3 - <<'PY'
import re, pathlib
p=pathlib.Path(".github/workflows/release.yml")
lines=p.read_text().splitlines()
for i,l in enumerate(lines):
    if re.search(r"uses:\s*actions/checkout@v\d+", l):
        print(f"\n--- checkout occurrence at line {i+1} ---")
        for j in range(i, min(len(lines), i+16)):
            print(f"{j+1:4d} {lines[j]}")
PY

Repository: YueMiyuki/Risuko

Length of output: 7222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/release.yml"
echo "== Snippet around line 130-190 =="
awk '{if (NR>=130 && NR<=200) printf "%4d %s\n", NR, $0}' "$FILE"

echo
echo "== Extract all 'uses: actions/checkout@v4' blocks including their immediate 'with:' sections (up to 15 lines) =="
python3 - <<'PY'
import re, pathlib
p=pathlib.Path(".github/workflows/release.yml")
lines=p.read_text().splitlines()
for i,l in enumerate(lines):
    if re.search(r"uses:\s*actions/checkout@v4\s*$", l):
        print(f"\n--- checkout occurrence at line {i+1} ---")
        # print until next step header or 20 lines
        for j in range(i, min(len(lines), i+22)):
            print(f"{j+1:4d} {lines[j]}")
            if j>i and re.search(r"^\s*-\s+name:\s+", lines[j]):
                break
PY

Repository: YueMiyuki/Risuko

Length of output: 3594


🌐 Web query:

actions/checkout@v4 persist-credentials default value

💡 Result:

For actions/checkout@v4, the input persist-credentials defaults to true (i.e., if you omit with: persist-credentials, credentials are configured/persisted in the local git config). [1][2] If you want to avoid persisting credentials, set: uses: actions/checkout@v4 with: persist-credentials: false [3][4]

Citations:


Disable persisted checkout credentials in the actions/checkout@v4 steps (android-release and release matrix).

Both actions/checkout@v4 usages omit with: persist-credentials: false, so credentials are persisted by default and may be exposed to later steps.

Suggested patch
       - name: Check out Git repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
       - name: Check out Git repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 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
- name: Check out Git repository
uses: actions/checkout@v4
- name: Check out Git repository
uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 20-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 21-21: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/release.yml around lines 20 - 22, Update both
actions/checkout@v4 steps named "Check out Git repository" used in the
android-release job and in the release matrix to explicitly disable persisted
credentials: add a with block containing persist-credentials: false so checkout
does not leave GITHUB_TOKEN credentials available to later steps; locate the
steps that use actions/checkout@v4 and insert the with: persist-credentials:
false setting for each occurrence.

runs-on: ubuntu-22.04
steps:
- name: Check out Git repository
uses: actions/checkout@v4

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/release.yml"
echo "== File = $FILE =="
if [ ! -f "$FILE" ]; then
  echo "Missing: $FILE"
  exit 1
fi

echo "== Line-numbered excerpt around the originally cited lines =="
nl -ba "$FILE" | sed -n '1,220p'

echo "== All uses: lines (with line numbers) =="
nl -ba "$FILE" | rg -n '^\s*uses:\s*' -n || true

echo "== Check whether any uses: entries use @<commit-sha> (40/64 hex) =="
# Extract uses lines and report if they match a likely SHA (40 hex or 64 hex)
nl -ba "$FILE" | rg '^\s*uses:\s*' | sed -E 's/^\s*[0-9]+\s+uses:\s*//' | while read -r uses; do
  if echo "$uses" | rg -q '@([0-9a-f]{40}|[0-9a-f]{64})$'; then
    echo "PINNED_SHA  $uses"
  else
    echo "NOT_PINNED  $uses"
  fi
done

Repository: YueMiyuki/Risuko

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/release.yml"
echo "== File = $FILE =="
if [ ! -f "$FILE" ]; then
  echo "Missing: $FILE"
  exit 1
fi

echo "== Line-numbered excerpt (1-220) =="
python3 - <<'PY'
path = ".github/workflows/release.yml"
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if i > 220: break
        print(f"{i:5d}  {line.rstrip()}")
PY

echo "== All uses: entries (line-numbered) =="
python3 - <<'PY'
import re
path = ".github/workflows/release.yml"
pat = re.compile(r'^\s*uses:\s*(.+)\s*$')
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        m = pat.match(line)
        if m:
            print(f"{i:5d}  {m.group(1)}")
PY

echo "== Check whether uses: use a commit SHA suffix =="
python3 - <<'PY'
import re
path = ".github/workflows/release.yml"
pat = re.compile(r'^\s*uses:\s*(.+)\s*$')
sha_pat = re.compile(r'@([0-9a-f]{40}|[0-9a-f]{64})$')
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        m = pat.match(line)
        if not m:
            continue
        uses = m.group(1)
        kind = "PINNED_SHA" if sha_pat.search(uses) else "NOT_PINNED"
        print(f"{kind:11s}  line {i}: {uses}")
PY

Repository: YueMiyuki/Risuko

Length of output: 10984


Pin third-party GitHub Actions to commit SHAs.

.github/workflows/release.yml uses mutable action tags (e.g., actions/checkout@v4, actions/setup-node@v4, android-actions/setup-android@v3, dtolnay/rust-toolchain@stable, softprops/action-gh-release@v2, actions/upload-artifact@v4, tauri-apps/tauri-action@v0), which weakens CI supply-chain guarantees—pin every uses: to a full commit SHA.

Applies to uses: lines: 21, 24, 29, 35, 41, 53, 104, 113, 152, 155, 160, 166, 196, 277, 286, 317, 326, 367, 376.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 20-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 21-21: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/release.yml at line 21, The workflow uses mutable action
tags (e.g., actions/checkout@v4, actions/setup-node@v4,
android-actions/setup-android@v3, dtolnay/rust-toolchain@stable,
softprops/action-gh-release@v2, actions/upload-artifact@v4,
tauri-apps/tauri-action@v0 and others referenced in the file) which must be
pinned to immutable commit SHAs; update every uses: entry (including the ones
called out in the review) to the corresponding full commit SHA for that action,
replacing tags like actions/checkout@v4 with actions/checkout@<commit-sha> and
do the same for actions/setup-node, android-actions/setup-android,
dtolnay/rust-toolchain, softprops/action-gh-release, actions/upload-artifact,
tauri-apps/tauri-action, etc., ensuring each uses: line in the workflow is
updated to a specific commit SHA and then run the workflow to verify no
breakages.

Comment thread .gitignore
Comment on lines +31 to +32
!src-tauri/gen/android/
!src-tauri/gen/android/**

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 | ⚡ Quick win

Avoid unignoring Android build artifacts wholesale.

Line 32 re-includes everything under src-tauri/gen/android/**, including generated build outputs. This is likely to bloat commits and leak transient artifacts. Re-allow only source/config files and keep **/build/** and .gradle/** ignored.

🤖 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 @.gitignore around lines 31 - 32, Replace the blanket unignore of
src-tauri/gen/android/** with targeted unignore rules: keep the existing
negation for src-tauri/gen/android/ but remove or restrict
src-tauri/gen/android/** and instead explicitly unignore only the specific
source/config files needed (e.g., gradle wrapper, settings, and manifest files)
and re-add ignore patterns for transient artifacts like **/build/** and
**/.gradle/** so generated build outputs are not committed; update the
.gitignore entries around src-tauri/gen/android/, src-tauri/gen/android/**,
**/build/** and .gradle/** accordingly.

Comment thread scripts/android-env.mjs
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="scripts/android-env.mjs"

echo "== File: $FILE =="
# Show imports near the top (including line 4)
nl -ba "$FILE" | sed -n '1,30p'

echo
echo "== Around line 55 =="
nl -ba "$FILE" | sed -n '45,70p'

echo
echo "== Search for hardcoded PATH separator usage =="
rg -n --hidden --no-ignore -S "PATH:|process\.env\.PATH|:'\s*\$\{|:\$|:\$\{|\:\$|:\"" "$FILE" || true

echo
echo "== Search for node:path delimiter usage =="
rg -n --hidden --no-ignore -S "path\.delimiter|delimiter" "$FILE" || true

Repository: YueMiyuki/Risuko

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="scripts/android-env.mjs"

echo "== File: $FILE =="
awk '{printf "%6d  %s\n", NR, $0}' "$FILE" | sed -n '1,30p'

echo
echo "== Around line 55 =="
awk '{printf "%6d  %s\n", NR, $0}' "$FILE" | sed -n '45,70p'

echo
echo "== Search for PATH construction and hardcoded ':' =="
# Print lines mentioning PATH config plus any ':' literals nearby
rg -n --hidden --no-ignore -S "PATH" "$FILE" || true
rg -n --hidden --no-ignore -S "PATH:.*:" "$FILE" || true
rg -n --hidden --no-ignore -S "process\.env\.PATH|javaBin|llvmBin|node:path" "$FILE" || true
rg -n --hidden --no-ignore -S ":\$\{|\:\$|:\`|'\:'|\"\:\"" "$FILE" || true

echo
echo "== Search for path.delimiter usage =="
rg -n --hidden --no-ignore -S "path\.delimiter|delimiter" "$FILE" || true

echo
echo "== Show import from node:path =="
rg -n --hidden --no-ignore -S "from \"node:path\"|from 'node:path'|node:path" "$FILE" || true

Repository: YueMiyuki/Risuko

Length of output: 4806


Use path.delimiter for PATH construction.

PATH is built with a hardcoded : on line 55, which breaks Windows where ; is required. Replace it with path.delimiter.

Suggested patch
-import { join } from "node:path";
+import { delimiter, join } from "node:path";
@@
-	PATH: `${javaBin ? `${javaBin}:` : ""}${llvmBin}:${process.env.PATH || ""}`,
+	PATH: `${javaBin ? `${javaBin}${delimiter}` : ""}${llvmBin}${delimiter}${process.env.PATH || ""}`,
🤖 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 `@scripts/android-env.mjs` at line 4, The PATH is being constructed using a
hardcoded ":" which fails on Windows; update scripts/android-env.mjs to use
path.delimiter instead of ":" when joining PATH entries—import the delimiter (or
the whole path module) alongside join (e.g., include delimiter from "node:path")
and replace the ":" usage in the PATH construction (where the PATH variable is
assembled) with path.delimiter so it works cross-platform.

Comment thread src-tauri/src/lib.rs
Comment on lines +90 to +98
if let Err(e) = std::fs::create_dir_all(&candidate) {
eprintln!(
"log-dir-override '{}' is not writable ({}). Falling back to default.",
candidate.display(),
e
);
return default_log_dir.to_path_buf();
}
candidate

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 | ⚡ Quick win

Validate actual log-file writability before accepting override.

Line 90 only checks directory creation. An existing but non-writable directory still passes, so logs can be silently dropped instead of falling back to default as documented.

Suggested fix
@@
     if let Err(e) = std::fs::create_dir_all(&candidate) {
         eprintln!(
             "log-dir-override '{}' is not writable ({}). Falling back to default.",
             candidate.display(),
             e
         );
         return default_log_dir.to_path_buf();
     }
+    // Probe actual file writability; create_dir_all alone is not sufficient.
+    let probe = candidate.join(format!(".risuko-log-write-test-{}", std::process::id()));
+    if let Err(e) = std::fs::OpenOptions::new()
+        .create(true)
+        .append(true)
+        .open(&probe)
+    {
+        eprintln!(
+            "log-dir-override '{}' is not writable ({}). Falling back to default.",
+            candidate.display(),
+            e
+        );
+        return default_log_dir.to_path_buf();
+    }
+    let _ = std::fs::remove_file(&probe);
     candidate
 }
📝 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
if let Err(e) = std::fs::create_dir_all(&candidate) {
eprintln!(
"log-dir-override '{}' is not writable ({}). Falling back to default.",
candidate.display(),
e
);
return default_log_dir.to_path_buf();
}
candidate
if let Err(e) = std::fs::create_dir_all(&candidate) {
eprintln!(
"log-dir-override '{}' is not writable ({}). Falling back to default.",
candidate.display(),
e
);
return default_log_dir.to_path_buf();
}
// Probe actual file writability; create_dir_all alone is not sufficient.
let probe = candidate.join(format!(".risuko-log-write-test-{}", std::process::id()));
if let Err(e) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&probe)
{
eprintln!(
"log-dir-override '{}' is not writable ({}). Falling back to default.",
candidate.display(),
e
);
return default_log_dir.to_path_buf();
}
let _ = std::fs::remove_file(&probe);
candidate
🤖 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 90 - 98, The code currently only calls
std::fs::create_dir_all(&candidate) and treats that as proof the override is
writable; instead, after create_dir_all(&candidate) succeeds, attempt to create
(and immediately remove) a small temp file inside candidate (e.g.
candidate.join(".writetest-<pid>-<uniq>")) using
OpenOptions::new().create(true).write(true).open(...), handle errors by logging
the error (use the same eprintln format) and return
default_log_dir.to_path_buf(); on success remove the temp file and return
candidate; keep references to candidate and default_log_dir so the change is
localized and ensures an existing but non-writable directory will fall back to
the default.

Comment on lines +3 to +20
<button
type="button"
v-for="(action, index) in taskActions"
:key="action"
class="task-item-action"
:style="{ '--stagger-index': index }"
@click.stop="onActionClick(action, $event)"
>
<i v-if="action === 'PAUSE'">
<Pause :size="14" />
</i>
<i v-if="action === 'STOP'">
<Square :size="14" />
</i>
<i v-if="action === 'RESUME'">
<Play :size="14" />
</i>
<i v-if="action === 'RESTART'">
<RotateCcw :size="14" />
</i>
<i v-if="action === 'DELETE'">
<Trash2 :size="14" />
</i>
<i v-if="action === 'TRASH'">
<Trash :size="14" />
</i>
<i v-if="action === 'FOLDER'">
<Folder :size="14" />
</i>
<i v-if="action === 'LINK'">
<Link :size="14" />
</i>
<i v-if="action === 'INFO'">
<Info :size="14" />
</i>
</li>
</ul>
<Pause v-if="action === 'PAUSE'" :size="14" />
<Square v-else-if="action === 'STOP'" :size="14" />
<Play v-else-if="action === 'RESUME'" :size="14" />
<RotateCcw v-else-if="action === 'RESTART'" :size="14" />
<Trash2 v-else-if="action === 'DELETE'" :size="14" />
<Trash v-else-if="action === 'TRASH'" :size="14" />
<Folder v-else-if="action === 'FOLDER'" :size="14" />
<Link v-else-if="action === 'LINK'" :size="14" />
<Info v-else-if="action === 'INFO'" :size="14" />
</button>

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 | ⚡ Quick win

Add accessible labels to icon-only action buttons.

Buttons in Line 3–20 render only icons, so screen readers have no actionable name.

Suggested fix
     <button
       type="button"
       v-for="(action, index) in taskActions"
       :key="action"
       class="task-item-action"
+      :aria-label="getActionLabel(action)"
+      :title="getActionLabel(action)"
       :style="{ '--stagger-index': index }"
       `@click.stop`="onActionClick(action, $event)"
     >
 	methods: {
+		getActionLabel(action: string) {
+			const map = {
+				PAUSE: this.$t("task.pause-task"),
+				STOP: this.$t("task.bt-stop-seeding"),
+				RESUME: this.$t("task.resume-task"),
+				RESTART: this.$t("task.restart-task"),
+				DELETE: this.$t("task.delete-task"),
+				TRASH: this.$t("task.remove-record"),
+				FOLDER: this.$t("task.show-in-folder"),
+				LINK: this.$t("task.copy-task-link"),
+				INFO: this.$t("task.task-detail-title"),
+			};
+			return map[action] || action;
+		},
 		onActionClick(action, event) {
🤖 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/Task/TaskItemActions.vue` around lines 3 - 20, The
icon-only action buttons rendered in TaskItemActions.vue (the <button> iterating
over taskActions and invoking onActionClick) lack accessible names; add an
accessible label by binding an aria-label (and optionally title) that maps each
action value to a human-readable string (e.g., map 'PAUSE'→'Pause task',
'STOP'→'Stop task', 'RESUME'→'Resume task', 'RESTART'→'Restart task',
'DELETE'→'Delete permanently', 'TRASH'→'Move to trash', 'FOLDER'→'Open folder',
'LINK'→'Open link', 'INFO'→'Show info') and use
:aria-label="getActionLabel(action)" on the <button> (implement a getActionLabel
helper or computed that returns the labels) so screen readers can announce each
button while preserving the existing onActionClick behavior.

Comment on lines +474 to +481
// Keep selected rows only when their underlying gid still exists
// Row keys can include `#f<index>`, so strip that before checking
const gids = new Set(orderedData.map((task) => task.gid));
this.selectedGidList = this.selectedGidList.filter((key) => {
const hashIdx = key.indexOf("#");
const gid = hashIdx === -1 ? key : key.slice(0, hashIdx);
return gids.has(gid);
});

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 | ⚡ Quick win

Reconcile selection by row keys, not only base gid.

On Line 474–481, filtering by underlying gid keeps stale file-row selections (e.g., gid#f3) after that row disappears. Downstream, batch actions still operate on selectedGids, so users can trigger actions on items that appear unselected.

Proposed fix
- const gids = new Set(orderedData.map((task) => task.gid));
- this.selectedGidList = this.selectedGidList.filter((key) => {
-   const hashIdx = key.indexOf("#");
-   const gid = hashIdx === -1 ? key : key.slice(0, hashIdx);
-   return gids.has(gid);
- });
+ const rowKeys = new Set(
+   this.displayTaskList.map((task: DisplayTask) => task._displayKey || task.gid),
+ );
+ this.selectedGidList = this.selectedGidList.filter((key) => rowKeys.has(key));
🤖 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/store/task.ts` around lines 474 - 481, The current filter only
checks base gids which lets stale compound keys like "gid#f3" remain in
selectedGidList; instead build a Set of actual current row keys from orderedData
(use task.key or task.rowKey if present, falling back to task.gid) and filter
this.selectedGidList by exact membership in that Set so keys with suffixes are
validated correctly; after that, ensure any derived selection state (e.g.,
selectedGids) is recomputed/updated from the cleaned selectedGidList.

html.platform-android .subnav-badge,
html.mobile-phone .subnav-badge {
background: rgba(103, 80, 164, 0.14);
color: currentColor;

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 | ⚡ Quick win

Resolve Stylelint violations in this stylesheet before merge.

This file currently includes lint errors (operator newline formatting in calc(...), empty-line-before declarations, and currentColor keyword casing). These are likely CI-blocking if stylelint is enforced.

Example cleanup pattern
- color: currentColor;
+ color: currentcolor;

- max-height: calc(
-   100dvh -
-   var(--android-bottom-bar) -
-   env(safe-area-inset-top) -
-   env(safe-area-inset-bottom) -
-   40px
- ) !important;
+ max-height: calc(100dvh - var(--android-bottom-bar) - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 40px) !important;

Also applies to: 724-724, 810-829, 889-890, 1049-1055

🧰 Tools
🪛 Stylelint (17.12.0)

[error] 500-500: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 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/styles/android.css` at line 500, Fix Stylelint violations:
change the color property value from "currentColor" to the lint-expected casing
(e.g., "currentcolor"), reformat any calc(...) expressions to keep operators on
the same line with proper spacing (e.g., "calc(100% - 10px)" instead of breaking
the operator to a new line), and insert the required empty line(s) before
declaration blocks per the declaration-empty-line-before rule. Locate
occurrences by searching for the literal "color: currentColor", any "calc("
usage, and the declaration blocks around the ranges flagged (near the calc and
color occurrences mentioned) and apply the three fixes consistently across those
blocks.

Comment on lines +297 to +314
.dev-log-path-input {
flex: 1 1 0;
min-width: 0;
height: 36px;
padding: 8px 10px;
border: 0;
outline: 0;
resize: none;
overflow-x: auto;
overflow-y: hidden;
white-space: pre;
background: transparent;
color: var(--foreground);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
font-size: 12px;
line-height: 18px;
scrollbar-width: thin;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider cross-browser scrollbar styling.

The scrollbar-width: thin property at line 313 only works in Firefox. For consistent scrollbar styling across Webkit browsers (Chrome, Safari, Edge), you'll need to add ::-webkit-scrollbar pseudo-element rules.

🎨 Proposed addition for Webkit scrollbar support

Add this rule after the .dev-log-path-input block:

	.dev-log-path-input::-webkit-scrollbar {
		height: 6px;
	}
	.dev-log-path-input::-webkit-scrollbar-track {
		background: transparent;
	}
	.dev-log-path-input::-webkit-scrollbar-thumb {
		background: var(--border);
		border-radius: 3px;
	}
	.dev-log-path-input::-webkit-scrollbar-thumb:hover {
		background: var(--muted-foreground);
	}
🤖 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/styles/components/preferences.css` around lines 297 - 314, The
.dev-log-path-input rule uses scrollbar-width: thin (Firefox only), so add
WebKit scrollbar pseudo-element rules for cross‑browser parity: append
::-webkit-scrollbar, ::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb and
::-webkit-scrollbar-thumb:hover selectors for .dev-log-path-input to set
scrollbar height (e.g. 6px), transparent track, thumb color using var(--border)
and hover color using var(--muted-foreground), and a small border-radius (e.g.
3px); place these rules immediately after the .dev-log-path-input block so
Chromium/Safari/Edge render a thin styled scrollbar like Firefox.

Comment on lines +17 to +24
function dirname(path = ""): string {
const value = `${path || ""}`.replace(/[/\\]+$/g, "");
const index = value.lastIndexOf("/");
if (index <= 0) {
return value;
}
return value.slice(0, index);
}

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 | ⚡ Quick win

Fix dirname for Windows path separators.

On Line 19, lastIndexOf("/") misses backslash-only paths, so reveal directory calculation can fail on Windows-style paths.

Proposed fix
 function dirname(path = ""): string {
 	const value = `${path || ""}`.replace(/[/\\]+$/g, "");
-	const index = value.lastIndexOf("/");
-	if (index <= 0) {
+	const normalized = value.replace(/\\/g, "/");
+	const index = normalized.lastIndexOf("/");
+	if (index < 0) {
 		return value;
 	}
+	if (index === 0) {
+		return "/";
+	}
 	return value.slice(0, index);
 }
🤖 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/native.ts` around lines 17 - 24, The dirname function
fails for Windows paths because it uses value.lastIndexOf("/") only; update
dirname (function name: dirname) to consider backslashes as well—either
normalize backslashes to forward slashes before computing the index or compute
the last separator index with Math.max(value.lastIndexOf("/"),
value.lastIndexOf("\\")). Then use that index to slice the directory portion and
keep the existing trimming of trailing separators.

@YueMiyuki
YueMiyuki merged commit 9f6451d into YueMiyuki:next-dev May 28, 2026
2 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.

2 participants