Skip to main content

agentmux_launcher/
start_at_login.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! `app:startatlogin`: one setting, three views
5//! (`docs/specs/SPEC_START_WITH_OS_2026_09_25.md` ยง3.9).
6//!
7//! The setting is the single source of truth, owned by srv like every other
8//! setting. The Settings toggle writes it with `setconfig`; the tray's check
9//! item writes it the same way, over the launcher's srv connection
10//! (`notify::request_start_at_login`, the path the tray's "pause
11//! notifications" already uses). srv broadcasts every change to all clients,
12//! so the Settings page, the tray check mark and the OS login entry all follow
13//! the one value: this module receives it (`observe`), refreshes the tray, and
14//! reconciles the login entry to it (`autostart::plan`).
15//!
16//! Off unless the user turns it on (owner decision, 2026-09-25).
17
18use std::sync::Mutex;
19
20/// Settings key.
21pub const SETTINGS_KEY: &str = "app:startatlogin";
22
23/// The last value srv reported. `None` until the first config frame arrives:
24/// the tray shows the item disabled until then, rather than guess.
25static SEEN: Mutex<Option<Option<bool>>> = Mutex::new(None);
26
27/// Serializes reconciles, so two quick toggles cannot interleave their writes.
28static RECONCILE: Mutex<()> = Mutex::new(());
29
30/// The setting as the tray should show it: `None` = not known yet.
31pub fn current() -> Option<bool> {
32    SEEN.lock().unwrap_or_else(|e| e.into_inner()).map(|v| v.unwrap_or(false))
33}
34
35/// srv reported the setting (on connect, and after every settings change).
36/// `None` means the key is not set.
37pub fn observe(setting: Option<bool>) {
38    {
39        let mut seen = SEEN.lock().unwrap_or_else(|e| e.into_inner());
40        // Every settings change broadcasts the whole config; only act on this key.
41        if *seen == Some(setting) {
42            return;
43        }
44        *seen = Some(setting);
45    }
46    crate::tray::notify_menu::wake();
47    // Blocking I/O (schtasks on Windows), so off the async session.
48    std::thread::Builder::new()
49        .name("agentmux-start-at-login".into())
50        .spawn(reconcile_latest)
51        .ok();
52}
53
54/// The tray's check item was clicked.
55pub fn toggle() {
56    if let Some(on) = current() {
57        crate::notify::request_start_at_login(!on);
58    }
59    // Put the item back to the known value until srv confirms the change:
60    // some menus flip their own check mark on click.
61    crate::tray::notify_menu::wake();
62}
63
64fn reconcile_latest() {
65    let _guard = RECONCILE.lock().unwrap_or_else(|e| e.into_inner());
66    // Apply the newest value, not the one that spawned this thread, so a
67    // burst of changes ends in the last state.
68    let Some(setting) = *SEEN.lock().unwrap_or_else(|e| e.into_inner()) else { return };
69    let channel = crate::autostart::current_channel();
70    // Only a write needs the target. Resolving it must not stand in the way of
71    // turning start-at-login off (ReAgent P1 on #3788).
72    let target = crate::autostart::stable_target();
73    let target_str = target.as_ref().ok().map(|t| t.display().to_string());
74    let entry = crate::autostart::read_entry();
75    let plan = crate::autostart::plan(setting, entry.as_ref(), &channel, target_str.as_deref());
76    let result = match plan {
77        crate::autostart::Plan::Nothing => return,
78        crate::autostart::Plan::Write => match &target {
79            Ok(t) => crate::autostart::enable(t, &channel),
80            Err(e) => Err(format!("no target to register: {e}")),
81        },
82        crate::autostart::Plan::Remove => crate::autostart::disable(),
83        crate::autostart::Plan::Adopt => {
84            crate::notify::request_start_at_login(true);
85            Ok(())
86        }
87    };
88    match result {
89        Ok(()) => crate::log(&format!(
90            "start-at-login: {plan:?} (setting={setting:?}, channel={channel}, target={target_str:?}, was={entry:?})"
91        )),
92        Err(e) => crate::log(&format!("start-at-login: {plan:?} failed: {e}")),
93    }
94}