AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
// Getting the person's attention when a bot needs them (or finished) while they're away.
|
|
|
|
|
//
|
2026-11-09 08:37:33 +00:00
|
|
|
// Behaviour spec:
|
|
|
|
|
// - macOS 10.14+ and a real .app bundle: ask UNUserNotificationCenter, waiting at most 3 s.
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
// authorizationStatus 0..4 = notDetermined/denied/authorized/provisional/ephemeral (else "unknown");
|
|
|
|
|
// alertSetting 0..2 = notSupported/disabled/enabled (else "unknown").
|
2026-11-09 08:37:33 +00:00
|
|
|
// - authorized/provisional/ephemeral → granted, denied → denied, notDetermined → undetermined.
|
|
|
|
|
// Banners only when authorized and alerts enabled.
|
|
|
|
|
// - The desktop counts as present only when not locked, not idle for 90 s and the window is
|
|
|
|
|
// alive; re-checked every 10 s.
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
|
|
|
|
use std::process::{Command, Stdio};
|
|
|
|
|
|
2026-11-09 08:37:33 +00:00
|
|
|
/// Away after this much keyboard/mouse silence `Z_e=9e4`).
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
pub const IDLE_THRESHOLD_SECS: f64 = 90.0;
|
2026-11-09 08:37:33 +00:00
|
|
|
/// How often presence is re-read `uBt=1e4`).
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
pub const PRESENCE_POLL_SECS: f64 = 10.0;
|
|
|
|
|
|
|
|
|
|
/// Seconds since the last keyboard or mouse input (macOS `HIDIdleTime`, in nanoseconds).
|
|
|
|
|
pub fn parse_hid_idle(ioreg: &str) -> Option<f64> {
|
|
|
|
|
let line = ioreg.lines().find(|l| l.contains("\"HIDIdleTime\""))?;
|
|
|
|
|
let n: f64 = line.rsplit('=').next()?.trim().parse().ok()?;
|
|
|
|
|
Some(n / 1e9)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn system_idle_secs() -> Option<f64> {
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
return crate::desk::idle_secs();
|
|
|
|
|
#[allow(unreachable_code)]
|
|
|
|
|
if !cfg!(target_os = "macos") {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let out = Command::new("ioreg").args(["-c", "IOHIDSystem", "-d", "4"]).stderr(Stdio::null()).output().ok()?;
|
|
|
|
|
parse_hid_idle(&String::from_utf8_lossy(&out.stdout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Notify unless the person is looking at AnyBot right now: the window is in front and they
|
|
|
|
|
/// were active within the last 90 s.
|
|
|
|
|
pub fn should_notify(window_focused: bool, idle_secs: Option<f64>) -> bool {
|
|
|
|
|
!window_focused || idle_secs.map(|s| s >= IDLE_THRESHOLD_SECS).unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- applying a new version while you're away ----------
|
2026-11-09 08:37:33 +00:00
|
|
|
// Behaviour spec: apply when 自動更新 is on (default) and an update is staged, and the machine
|
|
|
|
|
// is idle: (screen locked || screensaver on) and idle for >= 600 s, and no bot work running.
|
|
|
|
|
// Checks: first 30 s after launch, then every 3600 s plus up to 300 s of random jitter.
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
|
|
|
|
pub const AUTO_UPDATE_IDLE_SECS: f64 = 600.0;
|
|
|
|
|
pub const UPDATE_FIRST_CHECK_SECS: u64 = 30;
|
|
|
|
|
pub const UPDATE_CHECK_INTERVAL_SECS: u64 = 3600;
|
|
|
|
|
pub const UPDATE_CHECK_JITTER_SECS: u64 = 300;
|
|
|
|
|
|
2026-11-09 08:37:33 +00:00
|
|
|
/// Every input of the idle-update rule, in one place.
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
pub fn should_apply_update_now(enabled: bool, update_ready: bool, screen_locked: bool, screensaver: bool, idle_secs: Option<f64>, bots_idle: bool) -> bool {
|
|
|
|
|
enabled && update_ready && (screen_locked || screensaver) && idle_secs.map(|s| s >= AUTO_UPDATE_IDLE_SECS).unwrap_or(false) && bots_idle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The screen is locked (Electron's `getSystemIdleState` "locked"; here the session flag).
|
|
|
|
|
pub fn screen_locked() -> bool {
|
|
|
|
|
if !cfg!(target_os = "macos") {
|
|
|
|
|
// systemd-logind knows when the session is locked.
|
|
|
|
|
let id = std::env::var("XDG_SESSION_ID").unwrap_or_default();
|
|
|
|
|
let mut c = Command::new("loginctl");
|
|
|
|
|
c.arg("show-session");
|
|
|
|
|
if !id.is_empty() {
|
|
|
|
|
c.arg(&id);
|
|
|
|
|
}
|
|
|
|
|
return c.args(["-p", "LockedHint"]).stderr(Stdio::null()).output().map(|o| String::from_utf8_lossy(&o.stdout).trim() == "LockedHint=yes").unwrap_or(false);
|
|
|
|
|
}
|
|
|
|
|
Command::new("ioreg").args(["-n", "Root", "-d1"]).stderr(Stdio::null()).output().map(|o| String::from_utf8_lossy(&o.stdout).contains("\"CGSSessionScreenIsLocked\" = Yes")).unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The screensaver is running: the original's `NRn` runs `pgrep -x ScreenSaverEngine` (2 s timeout).
|
|
|
|
|
pub fn screensaver_active() -> bool {
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
return crate::desk::screensaver_on();
|
|
|
|
|
#[allow(unreachable_code)]
|
|
|
|
|
if !cfg!(target_os = "macos") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let Ok(mut c) = Command::new("pgrep").args(["-x", "ScreenSaverEngine"]).stdout(Stdio::null()).stderr(Stdio::null()).spawn() else { return false };
|
|
|
|
|
let start = std::time::Instant::now();
|
|
|
|
|
while start.elapsed() < std::time::Duration::from_secs(2) {
|
|
|
|
|
if let Ok(Some(st)) = c.try_wait() {
|
|
|
|
|
return st.success();
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
|
|
|
}
|
|
|
|
|
let _ = c.kill();
|
2026-11-05 07:48:22 +00:00
|
|
|
let _ = c.wait();
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Next periodic check: one hour plus a random part of five minutes.
|
|
|
|
|
pub fn next_update_check_secs(random_0_1: f64) -> u64 {
|
|
|
|
|
UPDATE_CHECK_INTERVAL_SECS + (random_0_1.clamp(0.0, 1.0) * UPDATE_CHECK_JITTER_SECS as f64).floor() as u64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn applescript_quote(s: &str) -> String {
|
|
|
|
|
let clean: String = s.chars().filter(|c| !c.is_control() || *c == ' ').collect();
|
|
|
|
|
format!("\"{}\"", clean.replace('\\', "\\\\").replace('"', "\\\""))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The AppleScript line for a banner (title + message), with text safely quoted.
|
|
|
|
|
pub fn banner_script(title: &str, body: &str) -> String {
|
|
|
|
|
let body: String = body.chars().take(180).collect();
|
|
|
|
|
format!("display notification {} with title {} sound name \"Glass\"", applescript_quote(&body), applescript_quote(title))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Show a macOS banner. AnyBot isn't a signed .app bundle when run from a build folder, so it can't
|
|
|
|
|
/// ask the notification center itself (the original's module needs a bundle); macOS's own
|
|
|
|
|
/// `display notification` is used instead. Doesn't wait.
|
|
|
|
|
pub fn banner(title: &str, body: &str) {
|
|
|
|
|
if !cfg!(target_os = "macos") {
|
|
|
|
|
// The desktop's notification service (GNOME, KDE, XFCE…) through notify-send.
|
|
|
|
|
let body: String = body.chars().take(180).collect();
|
Clippy is clean on macOS and Linux, for the app and the relay
Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on
Linux, 1 in the relay), fixed without changing what anything does:
- long types get a name (Peer, Members, Imported, Joining, Mention, Stream…);
- the two big enum variants are boxed (the vault's login plan, joining an
account);
- a test now waits for the copy it relaunched (it left a zombie behind);
- a hint's format had a dead `if` whose two branches were both "";
- the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or,
is_multiple_of, while let, from_ref, next_back, struct init, no casts to
the same type.
Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e
28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing,
scroll and screenshots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-07 02:10:28 +00:00
|
|
|
let _ = crate::computer::spawn_reaped(Command::new("notify-send").args(["-a", "AnyBot", title, &body]).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()));
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
return;
|
|
|
|
|
}
|
Clippy is clean on macOS and Linux, for the app and the relay
Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on
Linux, 1 in the relay), fixed without changing what anything does:
- long types get a name (Peer, Members, Imported, Joining, Mention, Stream…);
- the two big enum variants are boxed (the vault's login plan, joining an
account);
- a test now waits for the copy it relaunched (it left a zombie behind);
- a hint's format had a dead `if` whose two branches were both "";
- the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or,
is_multiple_of, while let, from_ref, next_back, struct init, no casts to
the same type.
Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e
28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing,
scroll and screenshots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-07 02:10:28 +00:00
|
|
|
let _ = crate::computer::spawn_reaped(Command::new("osascript").arg("-e").arg(banner_script(title, body)).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()));
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn presence_rule() {
|
|
|
|
|
assert!(!should_notify(true, Some(5.0)));
|
|
|
|
|
assert!(should_notify(true, Some(90.0)));
|
|
|
|
|
assert!(should_notify(false, Some(1.0)));
|
|
|
|
|
assert!(!should_notify(true, None));
|
|
|
|
|
let sample = " | | \"HIDIdleTime\" = 123000000000\n";
|
|
|
|
|
assert_eq!(parse_hid_idle(sample), Some(123.0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn update_applies_only_when_truly_away() {
|
|
|
|
|
assert!(should_apply_update_now(true, true, true, false, Some(600.0), true));
|
|
|
|
|
assert!(should_apply_update_now(true, true, false, true, Some(900.0), true));
|
|
|
|
|
assert!(!should_apply_update_now(true, true, false, false, Some(900.0), true), "screen in use");
|
|
|
|
|
assert!(!should_apply_update_now(true, true, true, false, Some(599.0), true), "not idle long enough");
|
|
|
|
|
assert!(!should_apply_update_now(true, true, true, false, Some(900.0), false), "a bot is working");
|
|
|
|
|
assert!(!should_apply_update_now(false, true, true, false, Some(900.0), true), "turned off");
|
|
|
|
|
assert!(!should_apply_update_now(true, false, true, false, Some(900.0), true), "nothing to apply");
|
|
|
|
|
assert_eq!(next_update_check_secs(0.0), 3600);
|
|
|
|
|
assert_eq!(next_update_check_secs(0.999), 3899);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn banner_text_is_quoted() {
|
|
|
|
|
let s = banner_script("電腦助手 需要你允許", "執行 \"rm\" \\ 指令\n下一行");
|
|
|
|
|
assert_eq!(s, "display notification \"執行 \\\"rm\\\" \\\\ 指令下一行\" with title \"電腦助手 需要你允許\" sound name \"Glass\"");
|
|
|
|
|
}
|
|
|
|
|
}
|