Skip to content

fix(linux): stop the file picker failing silently, in all 68 places - #515

Merged
axpnet merged 7 commits into
mainfrom
fix/picker-silent-failure
Jul 30, 2026
Merged

fix(linux): stop the file picker failing silently, in all 68 places#515
axpnet merged 7 commits into
mainfrom
fix/picker-silent-failure

Conversation

@axpnet

@axpnet axpnet commented Jul 29, 2026

Copy link
Copy Markdown
Member

Closes #510.

On a host with no xdg-desktop-portal, clicking any "Choose file…" control did nothing and said nothing. This makes the app say why, in 68 places.

Why the obvious fix was the wrong one

Measured by the #464 gate, not assumed. rfd exposes pick_file() -> Option<PathBuf> with no error channel, so tauri-plugin-dialog answers a portal-less host with a successful null — the same value as "the user pressed Cancel". open() is never rejected, so a try/catch at the call sites catches nothing, and if (!filePath) return; is not forgetful code: it genuinely cannot tell the two cases apart. The distinction has to be recreated before the dialog is invoked, which is what chooser_unavailable (first half of this branch, fff467092) exists for.

The count is now exact, and it changes the conclusion

68 picker call sites across 34 files: 39 open, 29 save. The "~37 of 67" figure in the issue came from a heuristic on surrounding try/catch, and that heuristic measures the wrong property here. Exactly 34 sites sat inside a try and 34 did not — and it makes no difference, because nothing is thrown. All 68 were silent, not 37. Two of the 34 files imported the plugin without ever calling it; those imports are removed rather than repointed.

What this adds

src/utils/pickPath.ts pickFile/pickSave, signature-compatible with the plugin's open/save. They ask whether a chooser can be presented; when it cannot, they surface a translated message and resolve to null without opening anything. A refusing portal — where an error genuinely exists — is caught here too, instead of at 34 separate call sites
68 call sites migrated Renamed, not aliased as pickFile as open. The defect was that a reader could not tell what a picker call would do; an open(...) that is secretly something else preserves exactly that
picker.unavailable.* All 47 locales, really translated, not placeheld. i18n:validate stays at 0 errors / 0 warnings, 5137 → 5140 keys
hookless translate() A helper called from a click handler cannot reach the provider's t. It shares one lookup function with the provider so the in-React and out-of-React paths cannot drift

The message goes out as an important toast. That flag is what makes App.tsx bypass showToastNotifications, and a user who turned ambient toasts off must still be told why their click did nothing — that is the bug.

Pinned at three levels, each verified by breaking the code

pickPath.test.ts — 12 tests on the message actually reaching the user. Mutation counts below are out of the 11 that existed when they were measured: deleting the pre-check fails 4 · dropping important: true fails 1 · pointing a reason at a non-existent translation key fails 3 · making the report a no-op fails 7. It also pins a non-English locale, because a portal-less host is most likely a minimal or containerised desktop, and those 46 translations are not decoration. The 12th is the extract-window pin described at the end.

pickPathIsTheOnlyPicker.test.ts — nothing but the helper may import the plugin, so the fix cannot decay by addition: the next new call site goes red instead of quietly restoring the silence in one file while 68 others stay correct. It reads sources through import.meta.glob(…, '?raw') rather than node:fs, because src/ is typed as a browser (lib: ES2020, DOM, no @types/node) and tsc sits on the production build chain (build: tsc && vite build). It asserts on real import statements, so a prose mention does not fail the build, and it checks the helper still wraps the plugin so the rule cannot pass by becoming vacuous.

tests/portal-chooser/portal-chooser-test.sh — case 3 asserted only that no extra window appeared, which the silent build also satisfied. It now also asserts the app said why. Case 1 asserts the check runs and stays quiet on a host that has a portal, so the guard cannot pass by crying wolf: a warning that fires everywhere is a warning nobody reads. The window-count assertion stays — it is still true, and the day a window does appear there, somebody has reintroduced the in-process chooser that corrupts the GLib heap.

chooser_unavailable now logs its verdict, and that is load-bearing rather than decorative: the remedy is a toast inside the WebView, so no window count, D-Bus trace or screenshot can observe it from outside the process. The log line is the only evidence that the frontend consulted the check on the real click path.

Gate run locally

tsc (TypeScript 7.0.2, after merging main) clean · vitest 544/544 · i18n:validate 0 errors / 0 warnings · cargo fmt --all --check clean · cargo clippy --all-targets -D warnings clean · cargo test --lib portal_chooser 4/4.

A second commit: the hookless translate() had to follow the window, not storage

Found while wiring the message up, and it would have shipped this branch's own bug in a different form. translate() first resolved the language from localStorage, reasoning that setLanguage persists before it re-renders so storage and the mounted provider cannot disagree. True of the main window, false of the other one: extract-main.tsx mounts I18nProvider with initialLanguage taken from the desktop language Rust injects, deliberately ignoring whatever language the main app was last left in — and it never persists that choice.

The extract window's folder picker is one of the 68 migrated call sites, so the message this PR adds would have appeared there in the main app's last language while everything around it rendered in the desktop language — exactly what that entry point exists to prevent.

It now reads <html lang>, which the provider already maintains for accessibility: one published source of truth instead of two derivations that can disagree. Storage and then English remain as fallbacks for code running before any provider mounts. Pinned by "follows the window it is rendered in, not the last language the main app was left in" — storage says Italian, <html lang> says German, the message must be German; restoring the storage-only lookup fails it with Italian.

Local gate re-run after this commit: tsc clean · vitest 545/545 · i18n:validate 0/0.

Summary by CodeRabbit

  • New Features

    • Added a consistent file and folder picker experience across the app.
    • Detects unavailable desktop file pickers and shows localized guidance instead of silently failing.
    • Added localized unavailable-picker messages across supported languages.
    • File picker errors are logged and handled without hanging or opening unexpected dialogs.
  • Bug Fixes

    • Improved handling of cancelled, refused, or unavailable picker dialogs.
    • Preserved language-aware translations for picker error messages.

axpnet and others added 3 commits July 29, 2026 21:46
…t a false claim

First half of #510. The chooser failure was invisible to every layer above it,
so this recovers the one piece of information that is destroyed on the way up.

## What was measured, not assumed

The #464 gate produces a host with no xdg-desktop-portal deterministically, and
its artefacts already carried the answer:

  Gtk-WARNING: Can't open portal file chooser:
  GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown

GTK notices, reports it, and presents nothing. It does NOT fall back, whatever
the comment in lib.rs claimed.

Then the information dies one layer down. `rfd 0.16`, which tauri-plugin-dialog
uses, exposes `pick_file() -> Option<PathBuf>`: no Result, no error channel at
all. So the plugin answers with a SUCCESSFUL null, and the frontend cannot tell
"the chooser could not open" from "the user pressed Cancel".

That kills the obvious fix. A try/catch around the call sites catches nothing
here, because nothing is thrown, and the `if (!filePath) return;` in
ExportImportDialog.tsx is not missing error handling: it is code that cannot
distinguish the two cases.

## What this adds

`portal_chooser::chooser_unavailable_reason()` answers whether a chooser can be
presented, before the dialog is ever invoked, using GDBus through the gtk crate
that is already a dependency. No new crate reaches the shipped binary.

It deliberately does not look for another chooser. On such a host there isn't
one: the in-process fallback is the heap corruption GTK_USE_PORTAL=1 exists to
avoid. The honest behaviour is to say why there is none.

Three things it refuses to get wrong, each pinned by a test that builds a
throwaway session bus rather than reasoning about one:

- a bus with no portal and nothing activatable reads as MISSING - the case the
  harness measured;
- a bus where the name is owned reads as AVAILABLE, so the check cannot pass by
  always crying wolf;
- an installed-but-not-yet-running portal reads as AVAILABLE. This is the subtle
  one: a portal is D-Bus activatable, so on a healthy machine that has not opened
  a chooser yet NOBODY owns the name. Checking only NameHasOwner would warn every
  user until their first file dialog, and a warning that cries wolf is a warning
  nobody reads.

Two more refusals, both deliberate: an explicit GTK_USE_PORTAL=0 reports no
problem, because that user asked for the in-process chooser on purpose; and a bus
that cannot be questioned at all reports no problem rather than accusing the host,
since a false alarm on a healthy machine is worse than the silence being fixed.
The bus timeout is 1s because this sits in front of a user gesture.

## Also

The lib.rs comment no longer claims a fallback that does not exist, and records
what happens instead.

Not done here, and tracked in #510: the frontend still has to consume this, and
the ~37 picker call sites with no error handling still want a shared helper for
the refusing-portal path, where an error genuinely is raised.

cargo fmt, clippy and the four new tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second half of #510. The first half recovered the lost information in Rust; this
delivers it to the person looking at the button.

## Why a try/catch at the call sites was never the fix

Measured by the #464 gate, not assumed. `rfd` exposes
`pick_file() -> Option<PathBuf>` with no error channel, so on a host with no
xdg-desktop-portal tauri-plugin-dialog answers with a SUCCESSFUL `null` — the
same value as "the user pressed Cancel". `open()` is never rejected, so there is
nothing to catch, and `if (!filePath) return;` at a call site is not forgetful
code: it genuinely cannot tell the two cases apart.

So the distinction has to be recreated BEFORE the dialog is invoked, which is
what `chooser_unavailable` (fff4670) exists for.

## The count is now exact

68 picker call sites across 34 files: 39 `open`, 29 `save`. The "~37 of 67"
figure in the issue came from a heuristic on surrounding try/catch, and that
heuristic measures the wrong property here — with nothing thrown, error handling
is irrelevant and **all 68 were silent**, not 37. Exactly 34 sat inside a `try`
block and 34 did not, which turns out to say nothing about the bug.

Two of the 34 files imported the plugin without ever calling it. Those imports
are removed rather than pointed at the helper.

## What this adds

- `src/utils/pickPath.ts` — `pickFile`/`pickSave`, signature-compatible with the
  plugin's `open`/`save`. They ask whether a chooser can be presented; when it
  cannot, they surface a translated message and resolve to `null` without opening
  anything. A refusing portal, where an error genuinely exists, is caught in the
  same place instead of at 34 separate call sites.
- All 68 call sites migrated, renamed rather than aliased as `pickFile as open`:
  the defect was that a reader could not tell what a picker call would do, and an
  `open(...)` that is secretly something else preserves that.
- `picker.unavailable.*` in all 47 locales, really translated, not placeheld.
  `i18n:validate` stays at 0 errors / 0 warnings, 5137 -> 5140 keys.
- A hookless `translate()` in the i18n module, because a helper called from a
  click handler cannot reach the provider's `t`. It shares one lookup function
  with the provider, so the in-React and out-of-React paths cannot drift.
- The message goes out as an `important` toast: that flag is what makes App.tsx
  bypass `showToastNotifications`, and a user who turned ambient toasts off must
  still be told why their click did nothing — that IS the bug.

## Pinned at three levels, each verified by breaking the code

- `pickPath.test.ts`, 11 tests on the message actually reaching the user. Out of
  11: deleting the pre-check fails 4, dropping `important: true` fails 1,
  pointing a reason at a non-existent translation key fails 3, making the report
  a no-op fails 7. It also pins a non-English locale, because a portal-less host
  is most likely a minimal or containerised desktop and those 46 translations are
  not decoration.
- `pickPathIsTheOnlyPicker.test.ts` — nothing but the helper may import the
  plugin, so the fix cannot decay by addition: the next new call site would go
  red instead of quietly restoring the silence. It reads sources through
  `import.meta.glob(..., '?raw')` rather than `node:fs`, because `src/` is typed
  as a browser (`lib: ES2020, DOM`, no `@types/node`) and `tsc` sits on the
  production build chain. It asserts on real import statements, so a prose
  mention does not fail the build, and it checks the helper still wraps the
  plugin so the rule cannot pass by becoming vacuous.
- `portal-chooser-test.sh` case 3 asserted only that no extra window appeared —
  which the silent build also satisfied. It now also asserts the app SAID why.
  Case 1 asserts the check runs and stays quiet on a host that has a portal, so
  the guard cannot pass by crying wolf; a warning that fires everywhere is a
  warning nobody reads.

The window-count assertion stays: it is still true, and the day a window does
appear there, somebody has reintroduced the in-process chooser that corrupts the
GLib heap.

`chooser_unavailable` now logs its verdict, and that is load-bearing rather than
decorative: the remedy is a toast inside the WebView, so no window count, D-Bus
trace or screenshot can observe it from outside the process. The log line is the
only evidence that the frontend consulted the check on the real click path.

Refs #510
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@axpnet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 82ae8a0c-a9d5-4529-b6eb-c9814751d75b

📥 Commits

Reviewing files that changed from the base of the PR and between 5d99fa9 and 54fc258.

📒 Files selected for processing (3)
  • .github/workflows/build.yml
  • src-tauri/src/portal_chooser.rs
  • src/i18n/locales/tr.json
📝 Walkthrough

Walkthrough

The PR adds Linux portal availability detection and shared pickFile/pickSave wrappers, migrates frontend chooser calls to those wrappers, adds localized unavailable-picker messages, and expands unit and portal-harness coverage.

Changes

Portal-aware picker flow

Layer / File(s) Summary
Portal availability command
src-tauri/src/lib.rs, src-tauri/src/portal_chooser.rs
Adds D-Bus portal reachability and activation checks, the chooser_unavailable Tauri command, diagnostic markers, and Linux classification tests.
Shared picker wrappers and translations
src/utils/pickPath.ts, src/i18n/*, src/i18n/locales/*
Adds centralized file/save picker wrappers with availability checks, refusal handling, translated toast messages, and picker.unavailable strings across locales.
Frontend picker migration
src/App.tsx, src/components/*, src/components/vault/*
Routes file, directory, upload, import, export, and save dialogs through pickFile or pickSave while preserving existing options and downstream operations.
Regression coverage
src/utils/*.test.ts, tests/portal-chooser/portal-chooser-test.sh
Tests unavailable, available, canceled, and refusing chooser behavior, enforces a single direct plugin import, and validates portal-harness logging and window-count behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant Frontend
  participant pickPath
  participant Tauri
  participant SessionBus
  participant DialogPlugin
  Frontend->>pickPath: pickFile or pickSave
  pickPath->>Tauri: invoke chooser_unavailable
  Tauri->>SessionBus: check portal ownership/activation
  SessionBus-->>Tauri: availability result
  Tauri-->>pickPath: optional reason
  alt chooser unavailable
    pickPath-->>Frontend: translated error toast and null
  else chooser available
    pickPath->>DialogPlugin: open or save
    DialogPlugin-->>pickPath: selected path, cancel, or refusal
    pickPath-->>Frontend: path or null
  end
Loading

Possibly related PRs

  • axpdev-lab/aeroftp#505 — Adds the related Linux portal-chooser harness and assertions for cancellation, refusal, and unavailable-portal behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clear and specific; it matches the Linux file-picker silent-failure fix and mentions the widespread picker call-site migration.
Linked Issues check ✅ Passed The PR adds shared picker helpers, migrates 68 call sites, adds user-facing failure tests, fixes lib.rs wording, and updates portal-harness assertions for #510.
Out of Scope Changes check ✅ Passed Changes stay focused on picker failure handling, localization, translation plumbing, tests, and harness updates; no unrelated scope is evident.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/picker-silent-failure

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.

@snyk-io

snyk-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…Storage

Found while wiring the picker message, and it would have shipped the bug the
fix is about, just in a different language.

`translate()` originally resolved the language from `localStorage`, on the
reasoning that `setLanguage` persists before it re-renders so storage and the
mounted provider cannot disagree. That is true of the main window and false of
the other one: `extract-main.tsx` mounts `I18nProvider` with `initialLanguage`
taken from the desktop language Rust injects, deliberately ignoring whatever
language the main app was last left in — and it never persists that choice.

So in the dedicated extract window every `translate()` call would have used the
main app's last language while everything around it rendered in the desktop
language. That window's folder picker is one of the 68 migrated call sites, so
the very message this branch adds would have come out in the wrong language,
which is exactly what that entry point goes out of its way to prevent.

It now reads `<html lang>`, which the provider already maintains for
accessibility, so there is one published source of truth rather than two
derivations that can disagree. `localStorage` and then English remain as
fallbacks for code that runs before any provider has mounted.

Pinned by `follows the window it is rendered in, not the last language the main
app was left in`: storage says Italian, `<html lang>` says German, the message
must be German. Restoring the storage-only lookup fails it with Italian.

Refs #510
@axpnet

axpnet commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Tauri runs a synchronous command on the main thread, and on Linux that is
the GTK thread. `chooser_unavailable` was sync while doing `bus_get_sync`
plus up to two `call_sync` round trips at BUS_TIMEOUT_MS each, so a wedged
session bus froze the whole window for about two seconds in front of the
very click this module exists to explain, with an unbounded tail if the bus
socket accepts a connection and never finishes authenticating.

That is the promise the BUS_TIMEOUT_MS comment already made and a sync
command cannot keep. Shipping a comment that claims "a wedged session bus
must not turn a click into a freeze" next to code that freezes the click is
the same defect #510 exists to remove, one file further along: lib.rs:17677
claimed a GTK fallback that measurement showed never happens.

The command is now async, which takes it off the main thread, with the
blocking round trip on `spawn_blocking` so it does not sit on an async
worker either. A failure to join reports "no problem", the same rule the
module already applies to a bus that cannot be questioned: a false "your
chooser is broken" on a healthy host is worse than the silence being fixed.

The pin is a test that `.await`s the command, so it acts at compile time:
reverting the command to `pub fn` fails the build with E0277 rather than
failing an assertion someone can delete. Verified by doing exactly that
before keeping the fix. It also asserts the wrapper only logs the verdict
and does not invent one.

The two tests that read GTK_USE_PORTAL now share a mutex. The harness runs
tests on several threads, so "restored immediately after" was not enough on
its own, and the new pin queries the same variable, which would have made
the old race reachable from a second place.

The class is wider than this command: 37 commands in this crate are still
sync, and two of them do filesystem work on the main thread. Audited and
opened as #517 rather than left in a paragraph.

Refs #510, #517

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

Caution

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

⚠️ Outside diff range comments (1)
src/components/SettingsPanel.tsx (1)

3355-3368: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale open() comment.

Line 3366 still documents the removed dialog API even though this flow now calls pickFile; replace or remove the comment so future maintenance reflects the shared helper contract.

🤖 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/components/SettingsPanel.tsx` around lines 3355 - 3368, Remove or update
the stale comment immediately before loadKeystoreFromPath in the pickFile flow
so it describes the shared pickFile contract rather than the removed dialog
open() API; preserve the existing filePath guard and keystore-loading behavior.
🧹 Nitpick comments (2)
src-tauri/src/portal_chooser.rs (2)

130-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer the log facade over eprintln! for these diagnostics.

Everything else in the crate goes through log::*, which tauri-plugin-log fans out to the log files and the in-app DebugPanel; eprintln! reaches raw stderr only, so a user reporting "the picker does nothing" won't have these lines in their exported diagnostics. log::warn!("{CHOOSER_UNAVAILABLE_LOG} {r}") / log::info!("{CHOOSER_CHECKED_LOG}") keeps the literals intact for the tests/portal-chooser/portal-chooser-test.sh grep.

🤖 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/portal_chooser.rs` around lines 130 - 147, Update
chooser_unavailable to use the log facade instead of eprintln!, emitting
log::warn! with CHOOSER_UNAVAILABLE_LOG and the reason, and log::info! with
CHOOSER_CHECKED_LOG when no reason exists. Preserve both message literals
unchanged so the existing test grep continues to match.

76-121: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the whole chooser pre-check to the advertised one-second budget.

bus_get_sync can block while connecting to the session bus, and reachable_on still runs two separate 1s call_sync operations. A wedged bus can therefore exceed the “caps it at one second” contract documented in src/utils/pickPath.ts; use a single deadline/timeout around the full check or reduce the per-call timeout so the total stays within one second.

🤖 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/portal_chooser.rs` around lines 76 - 121, The portal
reachability check in portal_is_reachable and reachable_on must respect the
documented one-second total budget, including session-bus connection and both
D-Bus calls. Replace the fixed per-call BUS_TIMEOUT_MS usage with a shared
deadline or reduced remaining-time calculation, and ensure bus_get_sync is also
bounded so a wedged bus cannot make the chooser pre-check exceed one second.
🤖 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/portal_chooser.rs`:
- Around line 186-203: Update the private_bus test helper’s temporary
configuration naming to include a unique per-invocation discriminator, rather
than deriving the filename only from servicedir and process ID. Ensure each
parallel private_bus(None) call creates and uses a distinct bus-*.conf path
while preserving the existing servicedir distinction.
- Around line 156-168: Update an_explicit_opt_out_reports_no_problem to remove
the unnecessary unsafe wrappers around GTK_USE_PORTAL environment-variable
operations and delete the misleading single-threaded SAFETY comment. Preserve
the existing save, restore, and assertion behavior; do not claim test-wide
single-threaded execution.

In `@src/i18n/locales/tr.json`:
- Line 5585: Update the Turkish portalMissing translation value to replace “bu
sistem dosya seçici açamıyor” with the grammatically correct “Bu sistemde dosya
seçici açılamıyor.” while preserving the remaining guidance and punctuation.

---

Outside diff comments:
In `@src/components/SettingsPanel.tsx`:
- Around line 3355-3368: Remove or update the stale comment immediately before
loadKeystoreFromPath in the pickFile flow so it describes the shared pickFile
contract rather than the removed dialog open() API; preserve the existing
filePath guard and keystore-loading behavior.

---

Nitpick comments:
In `@src-tauri/src/portal_chooser.rs`:
- Around line 130-147: Update chooser_unavailable to use the log facade instead
of eprintln!, emitting log::warn! with CHOOSER_UNAVAILABLE_LOG and the reason,
and log::info! with CHOOSER_CHECKED_LOG when no reason exists. Preserve both
message literals unchanged so the existing test grep continues to match.
- Around line 76-121: The portal reachability check in portal_is_reachable and
reachable_on must respect the documented one-second total budget, including
session-bus connection and both D-Bus calls. Replace the fixed per-call
BUS_TIMEOUT_MS usage with a shared deadline or reduced remaining-time
calculation, and ensure bus_get_sync is also bounded so a wedged bus cannot make
the chooser pre-check exceed one second.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5465b480-e47f-40ce-9936-51910ae53ac1

📥 Commits

Reviewing files that changed from the base of the PR and between 64efc8f and 5d99fa9.

📒 Files selected for processing (89)
  • src-tauri/src/lib.rs
  • src-tauri/src/portal_chooser.rs
  • src/App.tsx
  • src/components/AeroCryptKeyslotsModal.tsx
  • src/components/AeroCryptRecoveryKitModal.tsx
  • src/components/AeroCryptUnlock.tsx
  • src/components/AeroShare/AeroShareHandshakeBody.tsx
  • src/components/AeroSync/CompareTabContent.tsx
  • src/components/AeroSync/RemoteSyncResultDialog.tsx
  • src/components/AeroSync/SyncTabContent.tsx
  • src/components/ArchiveBrowser.tsx
  • src/components/BridgeSourcePanel.tsx
  • src/components/CloudPairsEditor.tsx
  • src/components/CloudPanel.tsx
  • src/components/ConnectionScreen.tsx
  • src/components/CryptomatorBrowser.tsx
  • src/components/CyberToolsModal.tsx
  • src/components/DebugPanel.tsx
  • src/components/DevTools/CodeBlockActions.tsx
  • src/components/DevTools/useAIChatConversations.ts
  • src/components/DevTools/useAIChatImages.ts
  • src/components/ExportImportDialog.tsx
  • src/components/ExtractWindow.tsx
  • src/components/FileVersionsDialog.tsx
  • src/components/GitHubReleaseBrowser.tsx
  • src/components/GitLabReleaseBrowser.tsx
  • src/components/IconPickerDialog.tsx
  • src/components/OAuthConnect.tsx
  • src/components/RcloneCryptUnlock.tsx
  • src/components/SavedServers.tsx
  • src/components/SettingsPanel.tsx
  • src/components/Sync/SyncTemplateDialog.tsx
  • src/components/VaultSyncDialog.tsx
  • src/components/vault/VaultCreate.tsx
  • src/components/vault/VaultReceipt.tsx
  • src/components/vault/useVaultState.ts
  • src/i18n/I18nContext.tsx
  • src/i18n/index.ts
  • src/i18n/locales/bg.json
  • src/i18n/locales/bn.json
  • src/i18n/locales/ca.json
  • src/i18n/locales/cs.json
  • src/i18n/locales/cy.json
  • src/i18n/locales/da.json
  • src/i18n/locales/de.json
  • src/i18n/locales/el.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/et.json
  • src/i18n/locales/eu.json
  • src/i18n/locales/fi.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/gl.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/hr.json
  • src/i18n/locales/hu.json
  • src/i18n/locales/hy.json
  • src/i18n/locales/id.json
  • src/i18n/locales/is.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ka.json
  • src/i18n/locales/km.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/lt.json
  • src/i18n/locales/lv.json
  • src/i18n/locales/mk.json
  • src/i18n/locales/ms.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/no.json
  • src/i18n/locales/pl.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ro.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/sk.json
  • src/i18n/locales/sl.json
  • src/i18n/locales/sr.json
  • src/i18n/locales/sv.json
  • src/i18n/locales/sw.json
  • src/i18n/locales/th.json
  • src/i18n/locales/tl.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/uk.json
  • src/i18n/locales/vi.json
  • src/i18n/locales/zh.json
  • src/utils/pickPath.test.ts
  • src/utils/pickPath.ts
  • src/utils/pickPathIsTheOnlyPicker.test.ts
  • tests/portal-chooser/portal-chooser-test.sh
💤 Files with no reviewable changes (2)
  • src/components/OAuthConnect.tsx
  • src/components/SavedServers.tsx

Comment thread src-tauri/src/portal_chooser.rs
Comment thread src-tauri/src/portal_chooser.rs
Comment thread src/i18n/locales/tr.json Outdated
axpnet and others added 2 commits July 29, 2026 23:54
…abbit round

Three findings from the review on #515, one of which was hiding something
larger.

Two tests call private_bus(None), so both resolved to the same
bus-bare-<pid>.conf and each truncated it with File::create while the other
test's dbus-daemon could still be reading it at startup. The consequence is
worse than a flake: when the daemon fails to start, private_bus returns None
and the test prints a line and returns, so it reports success while
asserting nothing. Fixed with an AtomicU32 discriminator per invocation.

That is only one way to lose the daemon, so the class is pinned too: a
missing dbus-daemon is now fatal when CI is set, and a skip only on a
developer machine. build.yml is the only job that runs cargo test on Linux
and it did not install dbus, so these three cases were most likely skipping
there all along with no trace, because eprintln! from a passing test is
captured and the log reads ok either way. dbus is now in that job's apt
list, with a comment saying it is there for the tests and not the build.

Verified by breaking it: with a dbus-daemon shim that refuses to start,
CI=1 turns the three cases red naming the cause, and the same shim without
CI still skips. 5/5 pass on a healthy host.

Turkish wording corrected: "bu sistemde dosya seçici açılamıyor" instead of
"bu sistem dosya seçici açamıyor", locative plus passive. Applied inside the
existing sentence so the half that tells the user which packages to install
survives, since that is the only actionable content the message has.

Rejected, with the measurement: the review also asked to drop the unsafe
around std::env::set_var as unnecessary in edition 2021 and warned it would
trip unused_unsafe under -D warnings. It does not. clippy --all-targets
-D warnings exits 0 with those blocks on the pinned 1.97.0 toolchain,
because set_var is an unsafe fn carrying #[rustc_deprecated_safe_2024],
which both allows pre-2024 callers to omit unsafe and suppresses
unused_unsafe for callers who write it. The block is allowed now and becomes
required at edition 2024. The other half of that finding was right and was
already fixed in 14e8bb7.

Refs #510

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's clippy caught what my own gate had declared clean:
clippy::await_holding_lock on the pin added in 14e8bb7. Both readings in
that test must see the same GTK_USE_PORTAL, so the guard spans them, and a
std::sync::MutexGuard held across an .await is denied for a real reason: the
future can be parked on one worker and resumed on another with the guard
still held.

Now a blocking #[test] driving its own runtime, so the whole exchange stays
on this thread and the lock is held only while this thread runs. The
compile-time pin survives unchanged in strength: block_on accepts only a
future, so reverting the command to `pub fn` still fails the build with
E0277. Re-verified by doing it.

Why my gate missed it, since that matters more than the lint. Each cargo
step was written as `( cmd 2>&1 | tail -N ); echo "rc=$?"`, and `$?` there is
tail's status, not the command's, so every cargo step printed 0 whatever it
did; the npm steps were sound because they used ${PIPESTATUS[0]}. On top of
that a warm target/ had not re-linted the test target after the edit, and the
denial only appeared after `cargo clean -p aeroftp`. A gate that cannot fail
is worse than no gate: it turns "unverified" into "verified" in the report.
The gate now redirects each step to a file, reads rc with no pipe in the way,
and ends with an explicit ALL-GREEN/RED verdict and a non-zero exit.

Measured after this change, with those real exit codes: cargo fmt 0, clippy
--all-targets -D warnings 0, cargo test --lib portal_chooser 5/5, tsc 0,
i18n:validate 0, vitest 545/545 across 56 files. Break-it checks: reverting
the command to sync exits 101 with E0277, CI=1 with a dbus-daemon that
refuses to start exits 101 with 3 failures, and the same shim without CI
exits 0 with 5 passing.

Refs #510

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File picker fails silently when the portal cannot serve it, and lib.rs documents a fallback that does not exist

1 participant