Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ tokio = { version = "=1.45.1", features = ["sync", "time"] }
[target.'cfg(target_os = "linux")'.dependencies]
dbus = "=0.9.12"

# Already present in the lock graph; only Windows writes its quoted Run command.
[target.'cfg(target_os = "windows")'.dependencies]
winreg = "=0.55.0"

[profile.release]
codegen-units = 1
lto = "thin"
Expand Down
9 changes: 7 additions & 2 deletions desktop/src-tauri/src/first_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ use tauri_plugin_autostart::ManagerExt;
const MARKER: &str = "start-at-login-claimed";

/// Marker file recording that the login item names the launch-origin argument.
#[cfg(not(target_os = "windows"))]
const ORIGIN_MARKER: &str = "start-at-login-origin-flag";

// Windows must revisit registrations claimed by the earlier, unquoted writer.
#[cfg(target_os = "windows")]
const ORIGIN_MARKER: &str = "start-at-login-quoted-origin-flag";

/// What the one-time Start at Login decision did on this launch.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StartAtLogin {
Expand Down Expand Up @@ -63,7 +68,7 @@ pub fn apply_start_at_login_default(app: &AppHandle) -> StartAtLogin {
if app.autolaunch().is_enabled().unwrap_or(false) {
return StartAtLogin::AlreadyDecided;
}
match app.autolaunch().enable() {
match crate::login_autostart::enable(app) {
Ok(()) => StartAtLogin::Enabled,
Err(_) => StartAtLogin::Unavailable,
}
Expand Down Expand Up @@ -99,7 +104,7 @@ pub fn adopt_launch_origin_argument(app: &AppHandle) {
// leave a login launch showing its window forever.
match app.autolaunch().is_enabled() {
Ok(true) => {
if app.autolaunch().enable().is_err() {
if crate::login_autostart::enable(app).is_err() {
return;
}
}
Expand Down
3 changes: 3 additions & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ mod first_run;
mod formatting;
mod identity;
mod logging;
mod login_autostart;
#[cfg(any(target_os = "windows", test))]
mod windows_autostart_command;
// macOS only: it exists to replace one item in a menu no other platform installs. Compiling it
// elsewhere would leave its contents unreachable, which -D warnings rejects.
#[cfg(target_os = "macos")]
Expand Down
45 changes: 45 additions & 0 deletions desktop/src-tauri/src/login_autostart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use tauri::AppHandle;

/// All shell-owned enables use this entry point; the plugin still owns status and disable.
#[cfg(not(target_os = "windows"))]
pub fn enable(app: &AppHandle) -> Result<(), String> {
use tauri_plugin_autostart::ManagerExt;
app.autolaunch().enable().map_err(|error| error.to_string())
}

#[cfg(target_os = "windows")]
pub fn enable(app: &AppHandle) -> Result<(), String> {
use winreg::enums::{HKEY_CURRENT_USER, KEY_SET_VALUE, REG_BINARY};
use winreg::{RegKey, RegValue};

let executable = std::env::current_exe().map_err(|error| error.to_string())?;
let executable = executable
.to_str()
.ok_or("autostart executable path is not Unicode")?;
let command =
crate::windows_autostart_command::command(executable, crate::startup::AUTOSTART_FLAG)?;
let name = &app.package_info().name;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
hkcu.open_subkey_with_flags(
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
KEY_SET_VALUE,
)
.and_then(|key| key.set_value(name, &command))
.map_err(|error| error.to_string())?;

// Match the plugin's explicit-enable behavior when Task Manager has an override.
if let Ok(key) = hkcu.open_subkey_with_flags(
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run",
KEY_SET_VALUE,
) {
key.set_raw_value(
name,
&RegValue {
vtype: REG_BINARY,
bytes: vec![2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
)
.map_err(|error| error.to_string())?;
}
Ok(())
}
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ pub fn install(app: &AppHandle) -> tauri::Result<()> {
if enabled {
let _ = app.autolaunch().disable();
} else {
let _ = app.autolaunch().enable();
let _ = crate::login_autostart::enable(app);
}
}
"stop-proxy" => {
Expand Down
41 changes: 41 additions & 0 deletions desktop/src-tauri/src/windows_autostart_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// Build the Run value without letting spaces split the executable path.
pub fn command(executable: &str, argument: &str) -> Result<String, &'static str> {
if executable.is_empty() || executable.contains(['"', '\0']) {
return Err("invalid executable path for Windows autostart");
}
Ok(format!("\"{executable}\" {argument}"))
}

#[cfg(test)]
mod tests {
use super::command;

#[test]
fn quotes_program_files_path_and_keeps_launch_origin() {
assert_eq!(
command(
r"C:\Program Files\OpenCodex\opencodex-desktop.exe",
"--autostart"
)
.unwrap(),
r#""C:\Program Files\OpenCodex\opencodex-desktop.exe" --autostart"#
);
}

#[test]
fn quotes_paths_without_spaces_and_preserves_unicode() {
for path in [r"C:\OpenCodex\app.exe", r"C:\用户\OpenCodex\app.exe"] {
assert_eq!(
command(path, "--autostart").unwrap(),
format!("\"{path}\" --autostart")
);
}
}

#[test]
fn rejects_paths_that_cannot_be_represented_in_a_run_command() {
for path in ["", "bad\"path.exe", "bad\0path.exe"] {
assert!(command(path, "--autostart").is_err());
}
}
}
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/guides/desktop-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ if it fails, use **Retry** to resolve the current runtime again.

On macOS, closing the dashboard keeps the app running in the menu bar. Open OpenCodex again from Dock or Finder to restore the dashboard without restarting the proxy.

On Windows, **Start at Login** quotes the executable path in the current-user startup
registration, including installations under `Program Files`. Previously enabled
registrations are updated once on launch. Startup entries you disabled in the tray
or Task Manager remain disabled.

## Startup safety on macOS

Startup safety reports **Desktop app** protection when OpenCodex's recorded ownership,
Expand Down
8 changes: 6 additions & 2 deletions structure/desktop-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,12 @@ that is not running has no menu bar item, so leaving autostart off by default le
installed app absent after a reboot. The marker in the app config directory is written
before the login item is touched and is never removed, so a user who turns the setting
off keeps it off; writing it afterwards would let a failed enable retry on every launch.
The behaviour is not macOS-only — the autostart plugin implements the Linux autostart
entry and the current-user Windows Run registration too.
The behaviour is not macOS-only. The autostart plugin implements the macOS and Linux
registrations and reads or disables the current-user Windows Run entry. Shell enables
on Windows write a quoted executable path followed by `--autostart`, preserving the
plugin's registration name and Task Manager explicit-enable semantics. A Windows-specific
launch-origin migration marker revisits older registrations once, only when already
enabled; entries disabled in the tray or Task Manager stay disabled.

The WidgetKit extension in `app/` needs three things that Xcode's app-extension target
would supply on its own, and SwiftPM has no such target: `@main` on
Expand Down
17 changes: 13 additions & 4 deletions tests/clients/desktop-start-at-login-default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe("start at login default", () => {

test("an existing marker returns before anything is changed", () => {
const early = firstRun.indexOf("marker.exists()");
const enable = firstRun.indexOf("autolaunch().enable()");
const enable = firstRun.indexOf("crate::login_autostart::enable(app)");
expect(early).toBeGreaterThan(-1);
expect(enable).toBeGreaterThan(-1);
expect(early).toBeLessThan(enable);
Expand All @@ -37,14 +37,14 @@ describe("start at login default", () => {

test("the marker is written before the login item is registered", () => {
const write = firstRun.indexOf("fs::write(&marker");
const enable = firstRun.indexOf("autolaunch().enable()");
const enable = firstRun.indexOf("crate::login_autostart::enable(app)");
expect(write).toBeGreaterThan(-1);
expect(write).toBeLessThan(enable);
});

test("enabling is guarded by the current autolaunch state", () => {
const guard = firstRun.indexOf("autolaunch().is_enabled()");
const enable = firstRun.indexOf("autolaunch().enable()");
const enable = firstRun.indexOf("crate::login_autostart::enable(app)");
expect(guard).toBeGreaterThan(-1);
expect(guard).toBeLessThan(enable);
});
Expand All @@ -67,7 +67,7 @@ describe("start at login default", () => {
const start = firstRun.indexOf("pub fn adopt_launch_origin_argument");
expect(start).toBeGreaterThan(-1);
const body = firstRun.slice(start);
const enable = body.indexOf("autolaunch().enable()");
const enable = body.indexOf("crate::login_autostart::enable(app)");
const claim = body.indexOf("fs::write(&claimed");
expect(enable).toBeGreaterThan(-1);
expect(claim).toBeGreaterThan(enable);
Expand All @@ -76,3 +76,12 @@ describe("start at login default", () => {
expect(body).not.toContain("autolaunch().disable()");
});
});

test("Windows revisits old migration markers and every shell enable uses the quoted writer", () => {
const firstRun = code(FIRST_RUN);
expect(firstRun).toContain('const ORIGIN_MARKER: &str = "start-at-login-quoted-origin-flag"');
const tray = code(repoPath("desktop/src-tauri/src/tray.rs"));
expect(tray).toContain("crate::login_autostart::enable(app)");
expect(tray).not.toContain("autolaunch().enable()");
expect(firstRun).not.toContain("autolaunch().enable()");
});
Loading