Skip to content

feat(firstrun): let the user choose the portable folder (#766) - #1404

Merged
debpalash merged 2 commits into
mainfrom
feat/portable-folder-picker
Aug 7, 2026
Merged

feat(firstrun): let the user choose the portable folder (#766)#1404
debpalash merged 2 commits into
mainfrom
feat/portable-folder-picker

Conversation

@debpalash

@debpalash debpalash commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes #766.

Portable mode put everything in OmniVoiceStudio-Data beside the app — the only storage row on the setup screen you could look at but not change. Installed mode had a picker for all three of its directories.

Why it was pinned

portable_base() is computed from the executable's location and never read from config. That is exactly what makes a portable install self-discovering: plug the drive into another machine and the app finds its data with no per-machine state. A user-chosen folder breaks it, because the only record of the location would live inside the folder being located.

Resolution order

# Source Portability
1 portable.path beside the app Preserved — travels with the app, needs nothing from the host
2 portableDir in the per-user config Machine-bound — the fallback when the app folder is read-only
3 <anchor>/OmniVoiceStudio-Data The historical default

Step 3 keeps existing portable installs resolving byte-identically: they have neither a pointer nor a config field.

Step 2 has a trap worth naming — config_pathportable_config_fileportable_base → step 2 would recurse forever through load_config. It reads the platform config file directly instead, so it can never consult the portable one.

This retires #766

A default Program Files install no longer greys Portable out. You point it at a writable disk and get the machine-bound variant — and the setup screen says which of the two you're getting rather than quietly downgrading the promise:

  • app folder writable → "a marker beside the app records this location, so the install still finds itself if you move the app and this folder to another machine"
  • app folder read-only → "this location is remembered for this user account only — the install will not be found from another machine"

Tests

  • Rust: one test covering the entire resolution order — default, pointer wins, padded pointer, empty pointer (must not resolve to "" and root the install at /), plan override, blank override. Single fn because it mutates APPIMAGE, which is process-global, matching the existing precedent in that file.
  • Frontend: four tests pinning the JS↔Rust payload key (portableDirInstallPlan::portable_dir — rename either side and the folder is silently ignored) and both captions. Three fail before.

Frontend 1707 passed, Rust 108 passed, typecheck/format/lint clean. All 21 locales updated in lockstep; docs/install/windows.md rewritten for the new behaviour.

Portable installations can now use a user-selected data folder, with marker-file, per-user, and historical fallback resolution. This enables portable mode for read-only locations such as Program Files and explains whether the selected folder is portable across machines or machine-bound. Rust and frontend tests cover path precedence, folder validation, persistence, and setup messaging; reviewers should verify migration and fallback behavior for existing installations.

Portable mode put everything in `OmniVoiceStudio-Data` beside the app, and
that was the only storage row on the setup screen you could look at but not
change — installed mode had a picker for all three of its directories.

The pin had a real reason. `portable_base()` is computed from the
executable's location and never read from config, which is exactly what
makes a portable install self-discovering: plug the drive into another
machine and the app finds its data with no per-machine state. A
user-chosen folder breaks that, because the only record of the location
would live inside the folder being located.

So record it somewhere findable, in this order:

  1. `portable.path` beside the app. This keeps portability — it travels
     with the app and needs nothing from the host.
  2. `portableDir` in the per-user config, for app folders that are not
     writable (`/Applications`, `Program Files`) where no pointer can be
     written. Machine-bound by nature, which is why it is second.
  3. `<anchor>/OmniVoiceStudio-Data` — the historical default. Existing
     portable installs have neither a pointer nor a config field, so they
     resolve byte-identically to before.

Step 2 has a trap worth naming: `config_path` → `portable_config_file` →
`portable_base` → step 2 would recurse forever if it went through
`load_config`. It reads the platform config file directly instead, and can
therefore never consult the portable one.

This also retires #766. A default Program Files install no longer greys
Portable out — you point it at a writable disk and get the machine-bound
variant, and the setup screen says which of the two you are getting rather
than quietly downgrading the promise.

Tests: one Rust test covering the whole resolution order (default,
pointer, padded pointer, empty pointer, plan override, blank override) —
single fn because it mutates APPIMAGE, which is process-global. Four
frontend tests pinning the JS↔Rust payload key and both captions; three
fail before.

Frontend 1707 passed, Rust 108 passed. All 21 locales updated in lockstep;
docs/install/windows.md rewritten for the new behaviour.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Portable mode now supports a user-selected storage folder. The backend resolves relocated portable paths through pointer files and per-user configuration. First-run setup validates and submits the selected path, displays relocation status, and includes localized messages and tests.

Changes

Portable installation flow

Layer / File(s) Summary
Portable configuration resolution
frontend/src-tauri/src/config.rs
AppConfig stores an optional portable_dir. Machine-local configuration lookup uses the new public helper while preserving portable precedence.
Portable directory resolution and setup
frontend/src-tauri/src/setup.rs
Portable paths resolve through pointer, user configuration, and historical defaults. Setup validates the selected directory, checks space, persists relocation state, and supports read-only app anchors. Backend tests cover fallback and destination behavior.
Portable folder selection and messaging
frontend/src/components/FirstRunSetup.jsx, frontend/src/i18n/locales/*
First-run setup allows directory selection, submits portableDir, and displays writable or machine-bound relocation messages in supported locales.
Portable selection validation
frontend/src/test/FirstRunSetupPortableDir.test.jsx
Frontend tests cover folder selection, setup submission, relocation messaging, and default-directory behavior.
Installation documentation
docs/install/windows.md, CHANGELOG.md
Windows installation documentation and the changelog describe custom portable folders and portable mode after default installation.

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

🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Backward Compatibility ⚠️ Warning AppConfig serializes portable_dir (no camelCase rename), but portable_base() reads portableDir; read-only installs fall back to the default folder and miss existing data/models. Read the serialized key as portable_dir or add a serde alias, then test record→reload resolution and backend paths for a read-only app folder.
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commit format with scope and includes the linked issue reference; it accurately describes the portable-folder selection change.
Description check ✅ Passed The description clearly explains the change, resolution order, compatibility behavior, implementation risks, and test results, despite not using the repository template headings.
Linked Issues check ✅ Passed The changes address issue #766 by enabling portable installation when the default application directory is not writable and allowing a user-selected data folder.
Out of Scope Changes check ✅ Passed The documentation, localization, Rust and frontend changes, and tests all support the portable-installation behavior described in issue #766.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cross-Platform Default Parity ✅ Passed AppConfig still defaults to installed; first-run selects it unless portable is already configured, and changed path/persistence logic runs only for an explicit portable plan.
I18n Completeness (21 Locales) ✅ Passed The changed calls use firstrun.portable_moved and firstrun.portable_moved_machine_bound; both keys exist as valid strings in all 21 locale JSON files, with no new direct JSX user-facing text.
Local-First Guarantee ✅ Passed The PR adds only local filesystem path resolution, persistence, and UI changes; no network calls or dependencies. Reporting and analytics files are unchanged and remain opt-in/off by default.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
frontend/src/components/FirstRunSetup.jsx (1)

322-322: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset the target result when portableDir changes.

When Line 322 receives a new path, useTargetCheck retains the previous path result during its 250 ms debounce. The Start button can remain enabled for a folder that has not passed its own writable and space check. As per path instructions, clear check before scheduling the new probe so the loading gate blocks Start.

Proposed fix
  useEffect(() => {
+   setCheck(null);
    if (!path) {
-     setCheck(null);
      return;
    }
🤖 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 `@frontend/src/components/FirstRunSetup.jsx` at line 322, Update the
useTargetCheck hook used by portableCheck so its check result is cleared
immediately whenever the target path changes, before the 250 ms probe is
scheduled. Ensure the loading state blocks Start until the new path completes
its writable and space validation, while preserving the existing debounce
behavior.

Source: Path instructions

🤖 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 `@docs/install/windows.md`:
- Around line 123-125: Update the Portable setup description near the first-run
setup instructions to state that the adjacent OmniVoiceStudio-Data folder is the
default location, while retaining that users can choose any writable folder
through Change…. Do not describe the adjacent folder as the only portable
location.

In `@frontend/src-tauri/src/setup.rs`:
- Around line 223-233: Update clear_portable_dir to load the machine-local
configuration from config::config_path_for_machine() rather than
config::load_config(app), and clear portable_dir there unconditionally before
saving it back. Preserve the existing pointer removal and ensure the machine
config record is updated even when the currently loaded relocated config has no
portable_dir.
- Around line 198-202: Absolute paths written by portable_pointer_path in
setup.rs are not relocatable, so do not claim unsupported cross-machine
discovery. Update frontend/src-tauri/src/setup.rs:198-202 to persist a
relocatable reference or otherwise constrain the portability behavior; revise
docs/install/windows.md:130-137 and the corresponding portability claims in
frontend/src/i18n/locales/tr.json:1972, uk.json:1972, vi.json:1972,
zh-CN.json:1979, and zh-TW.json:1972 to remove the unconditional cross-machine
guarantee.

---

Outside diff comments:
In `@frontend/src/components/FirstRunSetup.jsx`:
- Line 322: Update the useTargetCheck hook used by portableCheck so its check
result is cleared immediately whenever the target path changes, before the 250
ms probe is scheduled. Ensure the loading state blocks Start until the new path
completes its writable and space validation, while preserving the existing
debounce behavior.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 720253dc-133c-41f0-91fd-3f9dca5bdabb

📥 Commits

Reviewing files that changed from the base of the PR and between 129fee7 and 8e73cfd.

📒 Files selected for processing (27)
  • CHANGELOG.md
  • docs/install/windows.md
  • frontend/src-tauri/src/config.rs
  • frontend/src-tauri/src/setup.rs
  • frontend/src/components/FirstRunSetup.jsx
  • frontend/src/i18n/locales/ar.json
  • frontend/src/i18n/locales/de.json
  • frontend/src/i18n/locales/en.json
  • frontend/src/i18n/locales/es.json
  • frontend/src/i18n/locales/fr.json
  • frontend/src/i18n/locales/hi.json
  • frontend/src/i18n/locales/id.json
  • frontend/src/i18n/locales/it.json
  • frontend/src/i18n/locales/ja.json
  • frontend/src/i18n/locales/ko.json
  • frontend/src/i18n/locales/nl.json
  • frontend/src/i18n/locales/pl.json
  • frontend/src/i18n/locales/pt.json
  • frontend/src/i18n/locales/ru.json
  • frontend/src/i18n/locales/sv.json
  • frontend/src/i18n/locales/th.json
  • frontend/src/i18n/locales/tr.json
  • frontend/src/i18n/locales/uk.json
  • frontend/src/i18n/locales/vi.json
  • frontend/src/i18n/locales/zh-CN.json
  • frontend/src/i18n/locales/zh-TW.json
  • frontend/src/test/FirstRunSetupPortableDir.test.jsx

Comment thread docs/install/windows.md
Comment thread frontend/src-tauri/src/setup.rs Outdated
Comment thread frontend/src-tauri/src/setup.rs Outdated
Both Major findings from CodeRabbit on #1404 are real, and both were mine.

An absolute pointer does not survive the mount path changing. I stored
`/Volumes/Stick/Data` and told the user the install "still finds itself"
on another machine — where that path is `E:\Data` and resolves to nothing.
The claim was false in exactly the case portable mode exists for.

A folder INSIDE the app's own directory is now recorded RELATIVE to it, so
app + folder move as a unit and the absolute path is free to change. That
is the USB-stick case, and the only one where the promise holds. Anywhere
else there is nothing relocatable to store, so the absolute path is kept
and the UI says the install is tied to it. Three honest states now instead
of one overclaim; `pointer_payload` is pure so the decision that governs
the promise is testable without an AppHandle.

`clear_portable_dir` did not clear. It called `load_config`, which resolves
via `config_path` → `portable_config_file` → `portable_base` → the very
`portableDir` being erased, loading the RELOCATED config whose own
`portable_dir` is None. The `is_some()` guard then skipped, the machine
record survived, and the old folder kept winning after the user picked the
default. It reads and writes the machine config directly now.

Also corrected the Windows doc, which still described the folder as fixed
beside the executable two paragraphs above the picker that moves it.

Rust 109 passed, frontend 1705 passed. All 21 locales retranslated — both
strings changed meaning, so leaving the old wording would have shipped the
retracted promise in 20 languages.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
frontend/src-tauri/src/setup.rs (1)

249-256: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Load the machine configuration directly before recording relocation.

Line 249 can load the existing portable configuration, then Lines 251-256 overwrite machine-local settings with that old portable configuration when a prior relocation exists. Load and update config_path_for_machine() directly, then add a regression test with an existing relocated config.json; as per coding guidelines, “Fix the root cause with a fail-before/pass-after regression test and the smallest correct change.”

🤖 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 `@frontend/src-tauri/src/setup.rs` around lines 249 - 256, Update the
relocation logic around config::load_config and config::save_config_at to load
the existing machine-specific configuration from config_path_for_machine()
directly before setting portable_dir, rather than loading the portable
configuration. Preserve the existing directory creation and save behavior, and
add a regression test covering an already-relocated config.json that fails
before the change and passes afterward.

Source: Coding guidelines

🧹 Nitpick comments (1)
frontend/src/test/FirstRunSetupPortableDir.test.jsx (1)

130-158: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add Windows portability cases.

At Lines 133-153, every relocation case uses the shared os: 'macos' fixture and POSIX paths, so a Windows drive-letter or separator regression can pass while the Windows portable flow fails; the repository lists Windows as a supported desktop target. (github.com) Parameterize os, anchorDir, and baseDir, then add app-contained, external, and read-only Windows cases. As per coding guidelines, “Keep default behavior identical on macOS, Windows, and Linux.”

🤖 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 `@frontend/src/test/FirstRunSetupPortableDir.test.jsx` around lines 130 - 158,
Extend the relocation portability tests around the existing cases to cover
Windows alongside macOS, parameterizing os, anchorDir, and baseDir with Windows
drive-letter and separator paths. Add Windows app-contained, external, and
read-only scenarios, preserving the existing expectations and default behavior
for macOS, Windows, and Linux.

Source: Coding guidelines

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

Outside diff comments:
In `@frontend/src-tauri/src/setup.rs`:
- Around line 249-256: Update the relocation logic around config::load_config
and config::save_config_at to load the existing machine-specific configuration
from config_path_for_machine() directly before setting portable_dir, rather than
loading the portable configuration. Preserve the existing directory creation and
save behavior, and add a regression test covering an already-relocated
config.json that fails before the change and passes afterward.

---

Nitpick comments:
In `@frontend/src/test/FirstRunSetupPortableDir.test.jsx`:
- Around line 130-158: Extend the relocation portability tests around the
existing cases to cover Windows alongside macOS, parameterizing os, anchorDir,
and baseDir with Windows drive-letter and separator paths. Add Windows
app-contained, external, and read-only scenarios, preserving the existing
expectations and default behavior for macOS, Windows, and Linux.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 331935ac-732a-4c25-9cd2-34846f7bcf05

📥 Commits

Reviewing files that changed from the base of the PR and between 8e73cfd and 1660983.

📒 Files selected for processing (25)
  • docs/install/windows.md
  • frontend/src-tauri/src/setup.rs
  • frontend/src/components/FirstRunSetup.jsx
  • frontend/src/i18n/locales/ar.json
  • frontend/src/i18n/locales/de.json
  • frontend/src/i18n/locales/en.json
  • frontend/src/i18n/locales/es.json
  • frontend/src/i18n/locales/fr.json
  • frontend/src/i18n/locales/hi.json
  • frontend/src/i18n/locales/id.json
  • frontend/src/i18n/locales/it.json
  • frontend/src/i18n/locales/ja.json
  • frontend/src/i18n/locales/ko.json
  • frontend/src/i18n/locales/nl.json
  • frontend/src/i18n/locales/pl.json
  • frontend/src/i18n/locales/pt.json
  • frontend/src/i18n/locales/ru.json
  • frontend/src/i18n/locales/sv.json
  • frontend/src/i18n/locales/th.json
  • frontend/src/i18n/locales/tr.json
  • frontend/src/i18n/locales/uk.json
  • frontend/src/i18n/locales/vi.json
  • frontend/src/i18n/locales/zh-CN.json
  • frontend/src/i18n/locales/zh-TW.json
  • frontend/src/test/FirstRunSetupPortableDir.test.jsx
🚧 Files skipped from review as they are similar to previous changes (23)
  • frontend/src/i18n/locales/es.json
  • frontend/src/i18n/locales/sv.json
  • frontend/src/i18n/locales/zh-TW.json
  • frontend/src/i18n/locales/ko.json
  • frontend/src/i18n/locales/en.json
  • frontend/src/i18n/locales/vi.json
  • frontend/src/i18n/locales/pt.json
  • frontend/src/i18n/locales/zh-CN.json
  • frontend/src/i18n/locales/uk.json
  • frontend/src/i18n/locales/de.json
  • frontend/src/i18n/locales/ja.json
  • frontend/src/i18n/locales/pl.json
  • frontend/src/i18n/locales/hi.json
  • frontend/src/components/FirstRunSetup.jsx
  • frontend/src/i18n/locales/tr.json
  • frontend/src/i18n/locales/th.json
  • frontend/src/i18n/locales/it.json
  • frontend/src/i18n/locales/ru.json
  • frontend/src/i18n/locales/nl.json
  • frontend/src/i18n/locales/fr.json
  • frontend/src/i18n/locales/id.json
  • frontend/src/i18n/locales/ar.json
  • docs/install/windows.md

@debpalash
debpalash merged commit 0c82167 into main Aug 7, 2026
15 checks passed
@debpalash
debpalash deleted the feat/portable-folder-picker branch August 7, 2026 02:22
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.

[Install] Portable Install

1 participant