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
|
|
|
|
// 手機與帳號 in the window: what the phone sees (published when something changes), carrying out
|
|
|
|
|
|
// what the phone asks, and the settings page (phone password, account at the relay, backups,
|
|
|
|
|
|
// 裝置連結).
|
|
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
use crate::remote::{self, Cmd, Settings};
|
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
|
pub struct RemoteUi {
|
|
|
|
|
|
sig: u64,
|
|
|
|
|
|
last_publish: Option<Instant>,
|
|
|
|
|
|
/// The settings the server threads were last given.
|
|
|
|
|
|
pushed: Option<Settings>,
|
|
|
|
|
|
pw1: String,
|
|
|
|
|
|
pw2: String,
|
|
|
|
|
|
relay_url: String,
|
|
|
|
|
|
user: String,
|
|
|
|
|
|
pass: String,
|
|
|
|
|
|
invite: String,
|
|
|
|
|
|
create: bool,
|
2026-11-05 06:25:01 +00:00
|
|
|
|
pub(super) note: Arc<Mutex<String>>,
|
|
|
|
|
|
join_rx: Option<Receiver<Result<(Settings, remote::AccountFind), String>>>,
|
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
|
|
|
|
restore_rx: Option<Receiver<Result<Vec<u8>, String>>>,
|
|
|
|
|
|
busy: Arc<AtomicBool>,
|
|
|
|
|
|
peers: Arc<Mutex<Vec<(String, String, bool)>>>,
|
|
|
|
|
|
last_seen_mark: Option<Instant>,
|
|
|
|
|
|
peers_at: Option<Instant>,
|
|
|
|
|
|
/// 團隊: (user, role) shared with, loaded from the relay; the form to add one.
|
|
|
|
|
|
members: Arc<Mutex<Option<Result<Vec<(String, String)>, String>>>>,
|
|
|
|
|
|
new_member: String,
|
|
|
|
|
|
new_admin: bool,
|
|
|
|
|
|
started: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Approvals as the phone shows them.
|
|
|
|
|
|
fn approval_json(bot: &Bot, a: &Approval) -> serde_json::Value {
|
|
|
|
|
|
let what = remote::tool_label(&a.tool);
|
|
|
|
|
|
let title = match a.kind {
|
|
|
|
|
|
ApprovalKind::EnableTools => format!("允許 {} 使用電腦工具嗎?", bot.name),
|
|
|
|
|
|
ApprovalKind::EnableThisComputer => format!("允許 {} 在你的電腦上執行任務嗎?", bot.name),
|
|
|
|
|
|
ApprovalKind::Action => format!("{} 想要{what}", bot.name),
|
2026-11-05 09:29:55 +00:00
|
|
|
|
ApprovalKind::Handoff => format!("{}:需要你", bot.name),
|
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
|
|
|
|
};
|
2026-11-05 09:29:55 +00:00
|
|
|
|
let handoff = a.kind == ApprovalKind::Handoff;
|
|
|
|
|
|
json!({"bot": bot.id, "title": title, "reason": a.reason, "tool": a.tool, "can_always": !handoff && (a.kind != ApprovalKind::Action || !a.grant.is_empty()), "kind": if handoff { "handoff" } else { "ask" }, "computer": if handoff { a.grant.as_str() } else { "" }})
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl App {
|
|
|
|
|
|
pub(super) fn remote_start(&mut self, ctx: &egui::Context) {
|
|
|
|
|
|
if self.demo || self.rm.started {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rm.started = true;
|
2026-11-04 09:46:43 +00:00
|
|
|
|
self.ctx_handle = Some(ctx.clone());
|
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
|
|
|
|
let c = ctx.clone();
|
|
|
|
|
|
remote::hub().set_repaint(Arc::new(move || c.request_repaint()));
|
|
|
|
|
|
self.remote_apply_settings();
|
2026-11-04 09:46:43 +00:00
|
|
|
|
self.mirror_boot();
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Hand changed settings to the server threads (and restart what depends on them).
|
|
|
|
|
|
fn remote_apply_settings(&mut self) {
|
|
|
|
|
|
let s = self.state.remote.clone();
|
|
|
|
|
|
if self.rm.pushed.as_ref() == Some(&s) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let old = self.rm.pushed.replace(s.clone());
|
|
|
|
|
|
remote::hub().set_settings(&s);
|
|
|
|
|
|
let lan_changed = old.as_ref().map(|o| o.lan) != Some(s.lan);
|
|
|
|
|
|
if lan_changed {
|
|
|
|
|
|
match remote::set_lan(s.lan) {
|
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
|
Err(e) => *self.rm.note.lock().unwrap() = e,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let relay_changed = old.as_ref().map(|o| (o.relay_url.clone(), o.host_token.clone())) != Some((s.relay_url.clone(), s.host_token.clone()));
|
|
|
|
|
|
if relay_changed {
|
|
|
|
|
|
remote::relay_restart(&s);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Every frame: run what the phone asked and refresh what it sees.
|
|
|
|
|
|
pub(super) fn remote_tick(&mut self, ctx: &egui::Context) {
|
|
|
|
|
|
if self.demo {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.remote_apply_settings();
|
|
|
|
|
|
for c in remote::hub().take_cmds() {
|
2026-11-04 09:46:43 +00:00
|
|
|
|
// A screen of another brain has no bots of its own to run phone requests on.
|
|
|
|
|
|
if self.mir.on && !matches!(c, Cmd::DeviceAdded(_) | Cmd::DeviceSeen(_)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
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
|
|
|
|
self.remote_cmd(ctx, c);
|
|
|
|
|
|
}
|
2026-11-04 09:46:43 +00:00
|
|
|
|
self.brain_serve();
|
|
|
|
|
|
self.brain_watch();
|
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
|
|
|
|
if let Some(rx) = &self.rm.join_rx {
|
|
|
|
|
|
if let Ok(r) = rx.try_recv() {
|
|
|
|
|
|
self.rm.join_rx = None;
|
|
|
|
|
|
match r {
|
2026-11-05 06:25:01 +00:00
|
|
|
|
Ok((s, found)) => {
|
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
|
|
|
|
self.state.remote = s;
|
|
|
|
|
|
self.rm.pass.clear();
|
2026-11-05 06:25:01 +00:00
|
|
|
|
self.account_sync(found);
|
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
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => *self.rm.note.lock().unwrap() = e,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(rx) = &self.rm.restore_rx {
|
|
|
|
|
|
if let Ok(r) = rx.try_recv() {
|
|
|
|
|
|
self.rm.restore_rx = None;
|
|
|
|
|
|
match r.and_then(|bytes| serde_json::from_slice::<State>(&bytes).map_err(|e| format!("備份內容讀不出來:{e}"))) {
|
|
|
|
|
|
Ok(mut st) => {
|
|
|
|
|
|
// Keep this computer's own phone and account settings.
|
|
|
|
|
|
st.remote = self.state.remote.clone();
|
|
|
|
|
|
let n = st.bots.len();
|
|
|
|
|
|
self.state = st;
|
|
|
|
|
|
self.state.save();
|
|
|
|
|
|
*self.rm.note.lock().unwrap() = format!("已從備份還原 {n} 個 Bot");
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => *self.rm.note.lock().unwrap() = e,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 自動備份: once a day while signed in.
|
|
|
|
|
|
let s = &self.state.remote;
|
2026-11-04 09:46:43 +00:00
|
|
|
|
if !self.mir.on && s.auto_backup && !s.backup_key.is_empty() && !s.relay_url.is_empty() && chrono::Utc::now().timestamp() - s.last_backup > 86_400 && !self.rm.busy.load(Ordering::SeqCst) {
|
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
|
|
|
|
self.remote_backup();
|
|
|
|
|
|
}
|
Remote brain: a computer that just came on is known at once; an offline computer is said, not swapped; snapshots survive an open browser; end-to-end test
- The brain looked the account's computers up once a minute: for up to a minute after the Mac
came on, 「我的電腦」 was unknown and steps went elsewhere. Now every 15 s, and at once when a
message comes from a computer it doesn't know yet.
- A step naming a computer that is off says so in words instead of running on the chosen one
with a note that the person hadn't asked.
- Snapshots: tar's 'file changed' (the open browser) is not a failure; browser caches are left
out; ten minutes for a large home folder.
- box_end_to_end clicks the terminal where it opened; restore only with ANYBOT_TEST_RESTORE=1.
- deploy/test/remote_e2e.py: account server + headless brain + hands + scripted model, 27 checks
(computers, redirect, agent-desktop on the hands, hand-off 3 ways, phone view, sync copy,
edits, hands offline/back, brain restart, clear). anybot --sample-state for such tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:14:23 +00:00
|
|
|
|
// 裝置連結: keep the list of this account's computers fresh (every 15 s: a computer that
|
|
|
|
|
|
// comes on is one the bots can use soon after).
|
|
|
|
|
|
if !self.state.remote.host_token.is_empty() && !self.rm.peers_at.map(|t| t.elapsed().as_secs() < 15).unwrap_or(false) {
|
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
|
|
|
|
self.rm.peers_at = Some(Instant::now());
|
|
|
|
|
|
let (peers, s) = (self.rm.peers.clone(), self.state.remote.clone());
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
let v = remote::devices_online(&s);
|
|
|
|
|
|
if !v.is_empty() {
|
|
|
|
|
|
*peers.lock().unwrap() = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-11-04 09:46:43 +00:00
|
|
|
|
if !self.mir.on && !self.rm.last_publish.map(|t| t.elapsed().as_millis() < 120).unwrap_or(false) {
|
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
|
|
|
|
self.remote_publish();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 12:56:06 +00:00
|
|
|
|
/// 加一台電腦: one line to run on the new computer (a Linux box, a server); it joins as hands for
|
|
|
|
|
|
/// the brain, with no password and nothing to answer.
|
|
|
|
|
|
fn pair_section(&mut self, ui: &mut egui::Ui) {
|
|
|
|
|
|
let p = self.pal;
|
|
|
|
|
|
ui.add_space(6.0);
|
|
|
|
|
|
ui.separator();
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.label(RichText::new(t("加一台電腦")).size(13.5).strong());
|
One-step install: install.sh, fixed Linux and server builds, clearer README
- install.sh (curl | sh): app, hands, server and deps modes; installs system packages
(right names per release, e.g. libasound2t64 on Ubuntu 24.04), Rust via rustup (1.95+),
Docker for the server; limits parallel compiles on small machines
- Server Docker image: add the ALSA build/runtime packages (the build failed), cache the
cargo registry and target between rebuilds, JOBS build arg
- 加一台電腦 now gives a line that installs AnyBot on a blank computer before joining it
- Makefile: make deps installs Rust too, make doctor, clear message when Rust is missing/old
- README rewritten: one line per use, getting started, update/remove, troubleshooting,
deployment options; docs/install.md and multi-device.md follow it
- Tested on fresh ubuntu:24.04 (app) and debian:12 (curl | sh hands) containers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 16:52:10 +00:00
|
|
|
|
help_mark(ui, "讓另一台電腦(例如沒有螢幕的 Linux)也當大腦的手腳:在那台的終端機貼上這一行就好:還沒裝 AnyBot 會先自動裝好,不用帳號密碼、不用回答問題。它會設成開機自動執行,只讓這個大腦使用。", p);
|
2026-11-04 12:56:06 +00:00
|
|
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
|
|
|
|
if ui.add(secondary_button("產生配對碼")).clicked() {
|
|
|
|
|
|
let (s, slot) = (self.state.remote.clone(), self.mir.pair.clone());
|
|
|
|
|
|
let for_host = if self.mir.on { s.brain_id.clone() } else { s.host_id.clone() };
|
|
|
|
|
|
*slot.lock().unwrap() = (t("正在產生…"), false, None);
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
*slot.lock().unwrap() = match crate::remote::pair_code(&s, &for_host) {
|
One-step install: install.sh, fixed Linux and server builds, clearer README
- install.sh (curl | sh): app, hands, server and deps modes; installs system packages
(right names per release, e.g. libasound2t64 on Ubuntu 24.04), Rust via rustup (1.95+),
Docker for the server; limits parallel compiles on small machines
- Server Docker image: add the ALSA build/runtime packages (the build failed), cache the
cargo registry and target between rebuilds, JOBS build arg
- 加一台電腦 now gives a line that installs AnyBot on a blank computer before joining it
- Makefile: make deps installs Rust too, make doctor, clear message when Rust is missing/old
- README rewritten: one line per use, getting started, update/remove, troubleshooting,
deployment options; docs/install.md and multi-device.md follow it
- Tested on fresh ubuntu:24.04 (app) and debian:12 (curl | sh hands) containers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 16:52:10 +00:00
|
|
|
|
Ok(code) => (crate::remote::pair_line(&s.relay_url, &code), true, Some(Instant::now())),
|
2026-11-04 12:56:06 +00:00
|
|
|
|
Err(e) => (e, false, None),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
let (line, ok, at) = self.mir.pair.lock().unwrap().clone();
|
|
|
|
|
|
if line.is_empty() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let left = at.map(|t| 600u64.saturating_sub(t.elapsed().as_secs())).unwrap_or(0);
|
|
|
|
|
|
if ok && left == 0 {
|
|
|
|
|
|
hint(ui, "配對碼過期了,再按一次「產生配對碼」。", p);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
let c = if line.ends_with('…') { p.text2 } else { p.danger };
|
|
|
|
|
|
ui.label(RichText::new(line).size(12.5).color(c));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
Frame::new().fill(p.hover).corner_radius(8).inner_margin(Margin::symmetric(10, 8)).show(ui, |ui| {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.add(egui::Label::new(RichText::new(&line).monospace().size(13.0)).selectable(true).wrap());
|
|
|
|
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
|
|
|
|
if ui.add(secondary_button("複製")).clicked() {
|
|
|
|
|
|
ui.ctx().copy_text(line.clone());
|
|
|
|
|
|
self.notice = Some(t("已複製,貼到那台電腦的終端機執行"));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
ui.label(RichText::new(format!("{}(還有 {} 分 {:02} 秒,只能用一次)", t("在那台電腦的終端機貼上執行"), left / 60, left % 60)).size(12.0).color(p.text3));
|
|
|
|
|
|
// Just made: bring it into view.
|
|
|
|
|
|
if at.map(|t| t.elapsed().as_millis() < 800).unwrap_or(false) {
|
|
|
|
|
|
ui.scroll_to_cursor(Some(Align::Max));
|
|
|
|
|
|
}
|
|
|
|
|
|
ui.ctx().request_repaint_after(std::time::Duration::from_secs(1));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
fn remote_signature(&self) -> u64 {
|
|
|
|
|
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
|
|
|
|
|
self.state.remote.host_name.hash(&mut h);
|
2026-11-06 12:33:06 +00:00
|
|
|
|
self.state.timezone.hash(&mut h);
|
2026-11-04 09:46:43 +00:00
|
|
|
|
self.scr.cfg_hash.hash(&mut h);
|
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
|
|
|
|
for b in &self.state.bots {
|
|
|
|
|
|
(b.id, &b.name, &b.color, b.unread, b.messages.len()).hash(&mut h);
|
|
|
|
|
|
if let Some(m) = b.messages.last() {
|
2026-11-04 11:07:55 +00:00
|
|
|
|
(m.text.len(), m.thinking.len(), m.tool_calls.len(), m.error).hash(&mut h);
|
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
|
|
|
|
for c in &m.tool_calls {
|
|
|
|
|
|
(c.status as u8).hash(&mut h);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(l) = self.streams.get(&b.id) {
|
|
|
|
|
|
(1u8, &l.status, l.paused).hash(&mut h);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.approvals.get(&b.id).map(|a| a.call_id.clone()).hash(&mut h);
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
// The phone's home and bot sheet: its model, computer, routines and past chats.
|
|
|
|
|
|
(b.provider, &b.model, &b.default_computer, b.threads.len(), b.id == self.state.primary_bot).hash(&mut h);
|
|
|
|
|
|
for r in &b.routines {
|
|
|
|
|
|
(r.id, &r.title, r.paused, r.next_run).hash(&mut h);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (id, _, online) in self.rm.peers.lock().unwrap().iter() {
|
|
|
|
|
|
(id, online).hash(&mut h);
|
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
|
|
|
|
}
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
crate::cloudbox::run_state().label().hash(&mut h);
|
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
|
|
|
|
for pg in &self.state.pages {
|
|
|
|
|
|
(pg.id, pg.revision, pg.updated, &pg.title).hash(&mut h);
|
|
|
|
|
|
}
|
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
|
|
|
|
h.finish()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn remote_publish(&mut self) {
|
|
|
|
|
|
let sig = self.remote_signature();
|
|
|
|
|
|
if sig == self.rm.sig {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rm.sig = sig;
|
|
|
|
|
|
self.rm.last_publish = Some(Instant::now());
|
2026-11-06 12:33:06 +00:00
|
|
|
|
crate::routines::set_person_timezone(&self.state.timezone);
|
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
|
|
|
|
let names: HashMap<u64, String> = self.state.bots.iter().map(|b| (b.id, b.name.clone())).collect();
|
|
|
|
|
|
let mut bots: Vec<&Bot> = self.state.bots.iter().collect();
|
|
|
|
|
|
// Pinned first, then the most recently active.
|
|
|
|
|
|
bots.sort_by_key(|b| (!b.pinned, -(b.last_turn.as_ref().map(|t| t.settled_at_ms).unwrap_or(0))));
|
|
|
|
|
|
let mut view = remote::View { host: self.state.remote.host_name.clone(), ..Default::default() };
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
let peers = self.rm.peers.lock().unwrap().clone();
|
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
|
|
|
|
for b in bots {
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
// Its computers, each as the phone shows it: what it is and whether it is there now.
|
|
|
|
|
|
let targets = self.targets_for(b);
|
|
|
|
|
|
let roles = self.roles_for(b, &targets);
|
|
|
|
|
|
let computer = Self::chosen_computer(b, &targets, &roles).0;
|
|
|
|
|
|
let computers: Vec<serde_json::Value> = targets.iter().map(|t| {
|
|
|
|
|
|
let (kind, online) = match &t.place {
|
|
|
|
|
|
tools::Place::Box { .. } => ("own", crate::cloudbox::available()),
|
|
|
|
|
|
tools::Place::Sealed { .. } => ("own", false),
|
|
|
|
|
|
tools::Place::Local { .. } => ("this", true),
|
|
|
|
|
|
tools::Place::Device { host, .. } => ("device", peers.iter().any(|(id, _, on)| id == host && *on)),
|
|
|
|
|
|
tools::Place::Ssh { .. } => ("ssh", true),
|
|
|
|
|
|
};
|
|
|
|
|
|
json!({"name": t.name, "kind": kind, "online": online, "person": roles.person.as_deref() == Some(t.name.as_str())})
|
|
|
|
|
|
}).collect();
|
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
|
|
|
|
let live = self.streams.get(&b.id);
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
// The last thing said (not an error the model service sent, which then sat on the home).
|
|
|
|
|
|
let last = b.messages.iter().rev().find(|m| !m.text.trim().is_empty() && m.role != Role::Tool && !m.error).map(|m| first_line(&m.text, 60)).unwrap_or_default();
|
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
|
|
|
|
let c = crate::avatar::color(&b.color);
|
|
|
|
|
|
let hex = format!("#{:02x}{:02x}{:02x}", c.r(), c.g(), c.b());
|
|
|
|
|
|
view.bots.push(json!({
|
|
|
|
|
|
"id": b.id, "name": b.name, "description": b.description, "color": hex, "shape": b.shape,
|
|
|
|
|
|
"busy": live.is_some(), "status": live.and_then(|l| l.status.clone()).unwrap_or_default(),
|
|
|
|
|
|
"unread": b.unread, "last": last, "members": !b.members.is_empty(), "pinned": b.pinned,
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
"primary": b.id == self.state.primary_bot,
|
|
|
|
|
|
"time": b.messages.iter().rev().find(|m| m.role != Role::Tool).map(|m| m.at).unwrap_or(0),
|
|
|
|
|
|
"via": format!("{} · {}", b.provider.label(), b.model),
|
|
|
|
|
|
"provider": serde_json::to_value(b.provider).unwrap_or_default(), "model": b.model,
|
|
|
|
|
|
"computer": computer, "computers": computers,
|
|
|
|
|
|
"routines": b.routines.iter().map(|r| json!({"id": r.id, "title": r.title, "paused": r.paused, "when": crate::appctl::routine_when(r)})).collect::<Vec<_>>(),
|
|
|
|
|
|
"threads": b.threads.iter().enumerate().rev().take(20).map(|(i, (title, msgs))| json!({"index": i, "title": title, "count": msgs.len(), "at": msgs.last().map(|m| m.at).unwrap_or(0)})).collect::<Vec<_>>(),
|
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
|
|
|
|
}));
|
|
|
|
|
|
let start = b.messages.len().saturating_sub(remote::PHONE_TAIL);
|
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
|
|
|
|
view.messages.insert(b.id, b.messages[start..].iter().filter_map(|m| remote::message_json(m, &names, &self.state.pages)).collect());
|
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
|
|
|
|
if let Some(a) = self.approvals.get(&b.id) {
|
|
|
|
|
|
view.approvals.push(approval_json(b, a));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
頁面: pages a bot makes and keeps up to date (like the original's tracking pages)
- write_page / read_page / list_pages: Markdown pages with versions (last 10 kept), per bot;
the reply gets a card (title, fading preview, page icon, 頁面 · 更新時間, 最近更新), and
[標題](page:ID) links open the page
- The page opens large: summary, what changed, the version picker, 複製 / 存成檔案 / 刪除;
listed under 資料庫 in the right panel
- Page blocks for richer pages, in the chat's Markdown too: ```stats tiles, ```steps progress,
> [!NOTE]/[!TIP]/[!WARNING]/[!IMPORTANT] call-outs (with tables and Mermaid charts)
- Phone: the card in chats, page links, a page view with the same blocks; charts drawn by the
AnyBot as SVG (GET /api/pages/<id>[/chart/<n>]); members may read pages
- Bots are told to make a page for things to track and to update it from a routine only when
there is real news
- Screenshot views: ANYBOT_SHOT_VIEW=pagecard / page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 17:41:41 +00:00
|
|
|
|
view.pages = self.state.pages.clone();
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
// The models a bot can be switched to from the phone: each provider that is ready.
|
|
|
|
|
|
view.models = crate::providers::ALL_PROVIDERS
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|&&k| !k.needs_key() || !crate::providers::resolve_key(k, &self.state.provider(k).api_key).is_empty())
|
|
|
|
|
|
// Only models that chat: an image or video generator picked for a bot would break it.
|
|
|
|
|
|
.map(|&k| json!({"provider": serde_json::to_value(k).unwrap_or_default(), "label": k.label(), "models": self.models_for(k).into_iter().filter(|m| chats(m)).collect::<Vec<_>>()}))
|
|
|
|
|
|
.collect();
|
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
|
|
|
|
remote::hub().publish(view);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn remote_cmd(&mut self, ctx: &egui::Context, c: Cmd) {
|
|
|
|
|
|
match c {
|
2026-11-05 08:47:28 +00:00
|
|
|
|
Cmd::Send { bot, text, attachments, from } => {
|
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
|
|
|
|
if self.state.bot(bot).is_none() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-11-05 08:47:28 +00:00
|
|
|
|
// Typed on one of the account's computers: that one is the person's now (for
|
|
|
|
|
|
// 「我的電腦」 and the model on their network). The phone isn't a computer.
|
|
|
|
|
|
if !from.is_empty() {
|
|
|
|
|
|
remote::set_person_host(&from);
|
Remote brain: a computer that just came on is known at once; an offline computer is said, not swapped; snapshots survive an open browser; end-to-end test
- The brain looked the account's computers up once a minute: for up to a minute after the Mac
came on, 「我的電腦」 was unknown and steps went elsewhere. Now every 15 s, and at once when a
message comes from a computer it doesn't know yet.
- A step naming a computer that is off says so in words instead of running on the chosen one
with a note that the person hadn't asked.
- Snapshots: tar's 'file changed' (the open browser) is not a failure; browser caches are left
out; ten minutes for a large home folder.
- box_end_to_end clicks the terminal where it opened; restore only with ANYBOT_TEST_RESTORE=1.
- deploy/test/remote_e2e.py: account server + headless brain + hands + scripted model, 27 checks
(computers, redirect, agent-desktop on the hands, hand-off 3 ways, phone view, sync copy,
edits, hands offline/back, brain restart, clear). anybot --sample-state for such tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:14:23 +00:00
|
|
|
|
// A computer this brain hasn't heard of yet (it just came on): look the
|
|
|
|
|
|
// account's computers up now, or its 「我的電腦」 wouldn't be known for a minute.
|
|
|
|
|
|
if !self.rm.peers.lock().unwrap().iter().any(|(id, _, online)| *id == from && *online) {
|
|
|
|
|
|
let v = remote::devices_online(&self.state.remote);
|
|
|
|
|
|
if !v.is_empty() {
|
|
|
|
|
|
*self.rm.peers.lock().unwrap() = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rm.peers_at = Some(Instant::now());
|
|
|
|
|
|
}
|
2026-11-05 08:47:28 +00:00
|
|
|
|
self.typed_on = Some(from);
|
|
|
|
|
|
}
|
2026-11-05 07:48:20 +00:00
|
|
|
|
// Only files the app itself stored in its attachments folder may ride along;
|
|
|
|
|
|
// resolve first, so "../../" paths cannot escape it (the phone can name any path).
|
|
|
|
|
|
let root = crate::model::State::path().parent().map(|p| p.join("attachments")).and_then(|p| p.canonicalize().ok());
|
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
|
|
|
|
let atts: Vec<crate::attach::Attachment> = attachments
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter_map(|p| {
|
2026-11-05 07:48:20 +00:00
|
|
|
|
let f = std::path::Path::new(p).canonicalize().ok()?;
|
|
|
|
|
|
if !root.as_ref().map(|r| f.starts_with(r) && f.is_file()).unwrap_or(false) {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
let meta = std::fs::metadata(&f).ok()?;
|
|
|
|
|
|
let file = f.file_name()?.to_string_lossy().to_string();
|
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
|
|
|
|
let name = file.split_once('-').map(|x| x.1.to_string()).unwrap_or(file);
|
2026-11-05 09:07:04 +00:00
|
|
|
|
// Also on the bot's own computer, where it works (a brain on a server
|
|
|
|
|
|
// has no other computer of its own to read it from).
|
|
|
|
|
|
let on_box = crate::cloudbox::copy_in(&f.display().to_string(), bot, &name).unwrap_or_default();
|
|
|
|
|
|
Some(crate::attach::Attachment { image: crate::attach::is_image_name(&name), name, path: f.display().to_string(), bytes: meta.len(), on_box })
|
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
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if let Some(l) = self.streams.get(&bot) {
|
|
|
|
|
|
let (extra, _) = crate::attach::for_model(&atts);
|
|
|
|
|
|
l.steer.lock().unwrap().push(format!("{text}{extra}"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(b) = self.state.bot_mut(bot) {
|
|
|
|
|
|
b.messages.push(Message { attachments: atts, ..Message::user(&text) });
|
|
|
|
|
|
}
|
|
|
|
|
|
self.start_reply(ctx, bot);
|
|
|
|
|
|
}
|
|
|
|
|
|
Cmd::Stop { bot } => self.stop(bot),
|
2026-11-06 12:33:06 +00:00
|
|
|
|
// The server's own zone (UTC) was never the person's choice: take theirs. A zone the
|
|
|
|
|
|
// person set on purpose stays.
|
|
|
|
|
|
Cmd::PersonTimezone(tz) => {
|
|
|
|
|
|
if crate::routines::is_server_default_zone(&self.state.timezone) && tz != self.state.timezone {
|
|
|
|
|
|
self.state.timezone = tz;
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
Cmd::SetBot { bot, provider, model, computer } => {
|
|
|
|
|
|
let kind = serde_json::from_value::<crate::providers::ProviderKind>(json!(provider)).ok();
|
|
|
|
|
|
if let (Some(k), false) = (kind, model.is_empty()) {
|
|
|
|
|
|
if let Some(b) = self.state.bot_mut(bot) {
|
|
|
|
|
|
b.provider = k;
|
|
|
|
|
|
b.model = model;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
if !computer.is_empty() {
|
|
|
|
|
|
let _ = self.update_bot_computer(bot, &json!({"default_computer": computer}).to_string());
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-04 09:46:43 +00:00
|
|
|
|
Cmd::Regenerate { bot } => self.regenerate(ctx, bot),
|
|
|
|
|
|
Cmd::Reply { bot } => {
|
|
|
|
|
|
if !self.streams.contains_key(&bot) && self.state.bot(bot).and_then(|b| b.messages.last()).map(|m| m.role == Role::User).unwrap_or(false) {
|
|
|
|
|
|
self.start_reply(ctx, bot);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Cmd::RestoreThread { bot, index } => {
|
|
|
|
|
|
self.stop(bot);
|
|
|
|
|
|
if let Some(b) = self.state.bot_mut(bot) {
|
|
|
|
|
|
if index < b.threads.len() {
|
|
|
|
|
|
let (_, msgs) = b.threads.remove(index);
|
|
|
|
|
|
if !b.messages.is_empty() {
|
|
|
|
|
|
let title = b.messages.iter().find(|m| m.role == Role::User).map(|m| first_line(&m.text, 24)).unwrap_or_else(|| "對話".into());
|
|
|
|
|
|
let cur = std::mem::take(&mut b.messages);
|
|
|
|
|
|
b.threads.insert(0, (title, cur));
|
|
|
|
|
|
}
|
|
|
|
|
|
b.messages = msgs;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
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
|
|
|
|
Cmd::Answer { bot, answer } => {
|
|
|
|
|
|
let grant = self.approvals.get(&bot).map(|a| a.grant.clone()).unwrap_or_default();
|
|
|
|
|
|
let a = match answer.as_str() {
|
|
|
|
|
|
"once" => ApprovalAnswer::Once,
|
|
|
|
|
|
"always" => ApprovalAnswer::Always(grant),
|
|
|
|
|
|
_ => ApprovalAnswer::Reject,
|
|
|
|
|
|
};
|
|
|
|
|
|
self.answer(ctx, bot, a);
|
|
|
|
|
|
}
|
|
|
|
|
|
Cmd::Read { bot } => {
|
|
|
|
|
|
if let Some(b) = self.state.bot_mut(bot) {
|
|
|
|
|
|
if b.unread {
|
|
|
|
|
|
b.unread = false;
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-05 08:47:28 +00:00
|
|
|
|
Cmd::Clear { bot } => {
|
|
|
|
|
|
self.stop(bot);
|
|
|
|
|
|
if let Some(b) = self.state.bot_mut(bot) {
|
|
|
|
|
|
b.messages.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
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
|
|
|
|
Cmd::NewThread { bot } => {
|
|
|
|
|
|
self.stop(bot);
|
|
|
|
|
|
if let Some(b) = self.state.bot_mut(bot) {
|
|
|
|
|
|
if !b.messages.is_empty() {
|
|
|
|
|
|
let title = b.messages.iter().find(|m| m.role == Role::User).map(|m| first_line(&m.text, 24)).unwrap_or_else(|| "對話".into());
|
|
|
|
|
|
let msgs = std::mem::take(&mut b.messages);
|
|
|
|
|
|
b.threads.insert(0, (title, msgs));
|
|
|
|
|
|
b.threads.truncate(50);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
Cmd::DeviceAdded(d) => {
|
|
|
|
|
|
self.state.remote.devices.push(d);
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
Cmd::DeviceSeen(h) => {
|
|
|
|
|
|
if let Some(gone) = h.strip_prefix('-') {
|
|
|
|
|
|
self.state.remote.devices.retain(|d| d.token_hash != gone);
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
} else if !self.rm.last_seen_mark.map(|t| t.elapsed().as_secs() < 60).unwrap_or(false) {
|
|
|
|
|
|
self.rm.last_seen_mark = Some(Instant::now());
|
|
|
|
|
|
if let Some(d) = self.state.remote.devices.iter_mut().find(|d| d.token_hash == h) {
|
|
|
|
|
|
d.last_seen = chrono::Utc::now().timestamp();
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 06:25:01 +00:00
|
|
|
|
/// 登入後同步: the account's bots and this computer's become one set.
|
|
|
|
|
|
/// • Another AnyBot of the account is the brain → show it, and copy this computer's bots
|
|
|
|
|
|
/// there (once it answers; bots it already has are skipped).
|
|
|
|
|
|
/// • This one is the only one → take in the backup's bots it doesn't have, then back up
|
|
|
|
|
|
/// the whole set, so the account has this computer's bots too.
|
|
|
|
|
|
fn account_sync(&mut self, found: remote::AccountFind) {
|
|
|
|
|
|
if let Some((id, name)) = found.brain {
|
|
|
|
|
|
*self.rm.note.lock().unwrap() = format!("已登入,改用「{name}」當大腦:那邊的 Bot 會同步過來,這台的也會搬過去");
|
|
|
|
|
|
self.mir.want = Some((id, name));
|
|
|
|
|
|
self.mir.move_after_join = true;
|
|
|
|
|
|
// The brain's bots may use this computer (each action still asks).
|
|
|
|
|
|
self.state.remote.device_link = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let got = found.backup.and_then(|b| serde_json::from_slice::<State>(&b).ok());
|
|
|
|
|
|
let n = got.map(|st| self.state.take_in_bots(&st)).unwrap_or(0);
|
|
|
|
|
|
*self.rm.note.lock().unwrap() = if n > 0 { format!("已登入,帳號裡的 {n} 個 Bot 已同步到這台") } else { "已登入,這台 AnyBot 已加入帳號".into() };
|
|
|
|
|
|
if !self.state.bots.is_empty() && self.state.remote.auto_backup {
|
|
|
|
|
|
self.remote_backup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
fn remote_backup(&mut self) {
|
|
|
|
|
|
if self.rm.busy.swap(true, Ordering::SeqCst) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.state.remote.last_backup = chrono::Utc::now().timestamp();
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
let s = self.state.remote.clone();
|
|
|
|
|
|
let mut st = self.state.clone();
|
|
|
|
|
|
// The backup is for the bots; this computer's own keys to the relay and phones stay here.
|
|
|
|
|
|
st.remote = Default::default();
|
|
|
|
|
|
let bytes = serde_json::to_vec(&st).unwrap_or_default();
|
|
|
|
|
|
let (note, busy) = (self.rm.note.clone(), self.rm.busy.clone());
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
*note.lock().unwrap() = match remote::backup_now(&s, &bytes) {
|
|
|
|
|
|
Ok(n) => format!("已備份({},已加密)", crate::cloudbox::human(n as u64)),
|
|
|
|
|
|
Err(e) => e,
|
|
|
|
|
|
};
|
|
|
|
|
|
busy.store(false, Ordering::SeqCst);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn remote_page(&mut self, ui: &mut egui::Ui, _ctx: &egui::Context) {
|
|
|
|
|
|
let p = self.pal;
|
|
|
|
|
|
let mut changed = false;
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
setting_title(ui, "手機與帳號", "在手機上和 Bot 聊天、核准它要做的事、看著它的電腦。", p);
|
|
|
|
|
|
hint(ui, "兩種連法:同一個網路(或 Tailscale)直接連這台電腦;或登入帳號,透過你的帳號伺服器從任何地方連,這台電腦不用開放任何連接埠。", p);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 帳號(薄後端) ──
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
setting_title(ui, "AnyBot 帳號", "帳號伺服器保管帳號、裝置清單和加密備份,手機從外面連進來也靠它轉送。", p);
|
|
|
|
|
|
let s = &self.state.remote;
|
|
|
|
|
|
if !s.host_token.is_empty() {
|
|
|
|
|
|
let status = remote::hub().relay_status();
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
let ok = status == "已連線";
|
|
|
|
|
|
let (r, _) = ui.allocate_exact_size(vec2(10.0, 10.0), egui::Sense::hover());
|
|
|
|
|
|
ui.painter().circle_filled(r.center(), 4.0, if ok { Color32::from_rgb(0x14, 0xc3, 0x70) } else { p.text3 });
|
|
|
|
|
|
ui.label(RichText::new(format!("{} · {}", s.relay_user, s.relay_url)).size(FONT_BASE));
|
|
|
|
|
|
ui.label(RichText::new(if status.is_empty() { "—".to_string() } else { status }).size(12.0).color(p.text3));
|
|
|
|
|
|
});
|
|
|
|
|
|
hint(ui, format!("手機打開 {} 用同一個帳號登入就能用。可以「加入主畫面」當成 App。", s.relay_url), p);
|
|
|
|
|
|
ui.add_space(4.0);
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.label(RichText::new(t("這台電腦在手機上叫")).size(13.0).color(p.text2));
|
|
|
|
|
|
if ui.add(egui::TextEdit::singleline(&mut self.state.remote.host_name).desired_width(180.0)).lost_focus() {
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
if ui.checkbox(&mut self.state.remote.auto_backup, t("每天自動備份到帳號(加密,金鑰由你的密碼產生,伺服器打不開)")).changed() {
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
let busy = self.rm.busy.load(Ordering::SeqCst);
|
|
|
|
|
|
if ui.add_enabled(!busy, secondary_button(if busy { "備份中…" } else { "立刻備份" })).clicked() {
|
|
|
|
|
|
self.remote_backup();
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.state.remote.last_backup > 0 {
|
|
|
|
|
|
let t = chrono::DateTime::from_timestamp(self.state.remote.last_backup, 0).map(|d| d.with_timezone(&chrono::Local).format("%m/%d %H:%M").to_string()).unwrap_or_default();
|
|
|
|
|
|
ui.label(RichText::new(format!("上次備份 {t}")).size(12.0).color(p.text3));
|
|
|
|
|
|
}
|
|
|
|
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
|
|
|
|
if ui.add(egui::Button::new(RichText::new(t("登出帳號")).size(12.5).color(p.danger)).frame(false)).clicked() {
|
|
|
|
|
|
let keep_name = self.state.remote.host_name.clone();
|
2026-11-05 07:48:20 +00:00
|
|
|
|
// Say goodbye to the account server: the machine's registration is
|
|
|
|
|
|
// removed, so its key dies with the sign-out.
|
|
|
|
|
|
let (url, token) = (self.state.remote.relay_url.clone(), self.state.remote.host_token.clone());
|
|
|
|
|
|
if !url.is_empty() && !token.is_empty() {
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
let _ = remote::relay_post(&format!("{url}/relay/host/remove"), &token, &serde_json::json!({}));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
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
|
|
|
|
let s = &mut self.state.remote;
|
|
|
|
|
|
s.host_id.clear();
|
|
|
|
|
|
s.host_token.clear();
|
|
|
|
|
|
s.backup_key.clear();
|
|
|
|
|
|
s.host_name = keep_name;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let busy = self.rm.join_rx.is_some();
|
|
|
|
|
|
form_row(ui, "帳號伺服器", p, |ui| {
|
|
|
|
|
|
ui.add(egui::TextEdit::singleline(&mut self.rm.relay_url).hint_text(t("https://anybot.你的網域")).desired_width(f32::INFINITY));
|
|
|
|
|
|
});
|
|
|
|
|
|
form_row(ui, "帳號", p, |ui| {
|
|
|
|
|
|
ui.add(egui::TextEdit::singleline(&mut self.rm.user).desired_width(f32::INFINITY));
|
|
|
|
|
|
});
|
|
|
|
|
|
form_row(ui, "密碼", p, |ui| {
|
|
|
|
|
|
ui.add(egui::TextEdit::singleline(&mut self.rm.pass).password(true).desired_width(f32::INFINITY));
|
|
|
|
|
|
});
|
|
|
|
|
|
ui.checkbox(&mut self.rm.create, t("建立新帳號"));
|
|
|
|
|
|
if self.rm.create {
|
|
|
|
|
|
form_row(ui, "邀請碼", p, |ui| {
|
|
|
|
|
|
ui.add(egui::TextEdit::singleline(&mut self.rm.invite).hint_text(t("伺服器設定了才需要")).desired_width(f32::INFINITY));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
let ready = !self.rm.relay_url.trim().is_empty() && !self.rm.user.trim().is_empty() && self.rm.pass.len() >= 8;
|
|
|
|
|
|
if ui.add_enabled(ready && !busy, primary_button(if busy { "連線中…" } else if self.rm.create { "建立並登入" } else { "登入" }, p)).clicked() {
|
|
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
|
|
self.rm.join_rx = Some(rx);
|
|
|
|
|
|
let (url, user, pass, create, invite, base) = (self.rm.relay_url.clone(), self.rm.user.trim().to_string(), self.rm.pass.clone(), self.rm.create, self.rm.invite.clone(), self.state.remote.clone());
|
|
|
|
|
|
std::thread::spawn(move || {
|
2026-11-05 06:25:01 +00:00
|
|
|
|
// Signed in: look right away for the bots the account already has.
|
|
|
|
|
|
let r = remote::relay_join(&base, &url, &user, &pass, create, &invite).map(|s| {
|
|
|
|
|
|
let found = remote::find_account_bots(&s, &pass);
|
|
|
|
|
|
(s, found)
|
|
|
|
|
|
});
|
|
|
|
|
|
let _ = tx.send(r);
|
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
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if ui.add_enabled(ready && self.rm.restore_rx.is_none(), secondary_button("從帳號備份還原")).on_hover_text(t("把這台電腦的 Bot 換成帳號裡最新的備份")).clicked() {
|
|
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
|
|
self.rm.restore_rx = Some(rx);
|
|
|
|
|
|
let (url, user, pass) = (self.rm.relay_url.clone(), self.rm.user.trim().to_string(), self.rm.pass.clone());
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
let _ = tx.send(remote::restore(&url, &user, &pass));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
hint(ui, "密碼至少 8 個字。帳號伺服器在 relay/ 資料夾,可以放在任何一台小主機(說明見 relay/README.md)。", p);
|
|
|
|
|
|
}
|
|
|
|
|
|
let note = self.rm.note.lock().unwrap().clone();
|
|
|
|
|
|
if !note.is_empty() {
|
|
|
|
|
|
ui.label(RichText::new(note).size(12.5).color(p.text2));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-11-04 09:46:43 +00:00
|
|
|
|
// ── 大腦 ──
|
|
|
|
|
|
if !self.state.remote.host_token.is_empty() {
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.label(RichText::new(t("大腦")).size(FONT_BASE).strong());
|
|
|
|
|
|
help_mark(ui, "帳號裡選一台 AnyBot 當大腦(通常是跟帳號伺服器放在一起、一直開著的那台)。所有 Bot、聊天、記憶和例行任務都放在大腦上,由它思考和排程。其他電腦打開 AnyBot 看到的都是同一份,同時也是大腦的手腳:大腦的 Bot 可以透過裝置連結操作它們。大腦連不到時,這裡還能看之前的對話,但不能傳訊息,連回來就恢復。", p);
|
|
|
|
|
|
});
|
|
|
|
|
|
let s = &self.state.remote;
|
|
|
|
|
|
let status = self.mir.status.lock().unwrap().clone();
|
|
|
|
|
|
if self.mir.on {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
let (r, _) = ui.allocate_exact_size(vec2(10.0, 10.0), egui::Sense::hover());
|
|
|
|
|
|
ui.painter().circle_filled(r.center(), 4.0, if self.mir.online { Color32::from_rgb(0x14, 0xc3, 0x70) } else { p.text3 });
|
|
|
|
|
|
ui.label(RichText::new(status).size(13.0).color(p.text2));
|
|
|
|
|
|
});
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
let busy = self.mir.moving.load(Ordering::SeqCst);
|
|
|
|
|
|
if ui.add_enabled(self.mir.online && !busy, secondary_button("把這台的 Bot 搬過去")).on_hover_text(t("把這台電腦原本的 Bot(連同聊天)、模型金鑰、技能和連接器複製到大腦。這台的原始資料不會刪掉。")).clicked() {
|
|
|
|
|
|
self.mirror_move_local();
|
|
|
|
|
|
}
|
|
|
|
|
|
if ui.add(secondary_button("改回用這台自己的 Bot")).clicked() {
|
|
|
|
|
|
self.mir.leave = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
let note = self.mir.move_note.lock().unwrap().clone();
|
|
|
|
|
|
if !note.is_empty() {
|
|
|
|
|
|
ui.label(RichText::new(note).size(12.5).color(p.text2));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ui.label(RichText::new(t("這台就是大腦:Bot 在這台電腦上思考和工作。")).size(13.0).color(p.text2));
|
|
|
|
|
|
let me = s.host_id.clone();
|
2026-11-04 12:56:06 +00:00
|
|
|
|
let peers: Vec<(String, String, bool)> = self.rm.peers.lock().unwrap().iter().filter(|(id, _, _)| !id.is_empty() && *id != me && !remote::is_hands(id)).cloned().collect();
|
2026-11-04 09:46:43 +00:00
|
|
|
|
if peers.is_empty() {
|
2026-11-04 12:56:06 +00:00
|
|
|
|
hint(ui, "帳號裡還沒有別台可以當大腦的 AnyBot。", p);
|
2026-11-04 09:46:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
for (id, name, online) in peers {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
let (r, _) = ui.allocate_exact_size(vec2(10.0, 10.0), egui::Sense::hover());
|
|
|
|
|
|
ui.painter().circle_filled(r.center(), 4.0, if online { Color32::from_rgb(0x14, 0xc3, 0x70) } else { p.text3 });
|
|
|
|
|
|
ui.label(RichText::new(&name).size(13.5));
|
|
|
|
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
|
|
|
|
if ui.add_enabled(online, secondary_button("改用它當大腦")).clicked() {
|
|
|
|
|
|
self.mir.want = Some((id.clone(), name.clone()));
|
|
|
|
|
|
// The brain's bots may use this computer (each action still asks).
|
|
|
|
|
|
self.state.remote.device_link = true;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-04 12:56:06 +00:00
|
|
|
|
self.pair_section(ui);
|
2026-11-04 09:46:43 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// ── 同一個網路 ──
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
setting_title(ui, "同一個網路直接連", "手機和這台電腦在同一個 Wi-Fi,或都裝了 Tailscale 時使用。", p);
|
|
|
|
|
|
let has_pw = !self.state.remote.password.is_empty();
|
|
|
|
|
|
if ui.add_enabled(has_pw, egui::Checkbox::new(&mut self.state.remote.lan, t("讓手機直接連這台電腦"))).changed() {
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.state.remote.lan {
|
|
|
|
|
|
if let Some(port) = remote::lan_port() {
|
|
|
|
|
|
for a in remote::addresses(port) {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.label(RichText::new(&a).monospace().size(13.0));
|
|
|
|
|
|
if ui.small_button(t("複製")).clicked() {
|
|
|
|
|
|
ui.ctx().copy_text(a.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ui.add_space(4.0);
|
|
|
|
|
|
form_row(ui, if has_pw { "更改手機密碼" } else { "設定手機密碼" }, p, |ui| {
|
|
|
|
|
|
ui.add(egui::TextEdit::singleline(&mut self.rm.pw1).password(true).hint_text(t("至少 8 個字")).desired_width(f32::INFINITY));
|
|
|
|
|
|
});
|
|
|
|
|
|
form_row(ui, "再輸入一次", p, |ui| {
|
|
|
|
|
|
ui.add(egui::TextEdit::singleline(&mut self.rm.pw2).password(true).desired_width(f32::INFINITY));
|
|
|
|
|
|
});
|
|
|
|
|
|
let ok = self.rm.pw1.chars().count() >= 8 && self.rm.pw1 == self.rm.pw2;
|
|
|
|
|
|
if ui.add_enabled(ok, secondary_button("儲存密碼")).clicked() {
|
|
|
|
|
|
self.state.remote.password = crate::httpd::hash_password(&self.rm.pw1);
|
|
|
|
|
|
// A new password signs every phone out.
|
|
|
|
|
|
self.state.remote.devices.clear();
|
|
|
|
|
|
self.rm.pw1.clear();
|
|
|
|
|
|
self.rm.pw2.clear();
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if !self.rm.pw2.is_empty() && self.rm.pw1 != self.rm.pw2 {
|
|
|
|
|
|
hint(ui, "兩次輸入的密碼不一樣", p);
|
|
|
|
|
|
}
|
|
|
|
|
|
hint(ui, "不要把這個連接埠開放到網際網路上;從外面連請用上面的帳號,或 Tailscale。", p);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 允許手機做什麼 ──
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
setting_title(ui, "手機可以做什麼", "聊天、核准、看 Bot 的電腦一律可以。下面這些會碰到這台電腦本身,預設關閉。", p);
|
|
|
|
|
|
if ui.checkbox(&mut self.state.remote.mac_from_phone, t("從手機看這台電腦的螢幕並操作(點、打字、按鍵)")).changed() {
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ui.checkbox(&mut self.state.remote.device_link, t("裝置連結:讓帳號裡其他的 AnyBot(例如放在伺服器上的)操作這台電腦")).changed() {
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
hint(ui, "裝置連結開著時,伺服器上的 Bot 可以把「你的電腦」的螢幕、檔案、指令和 Mac App 工具交給這台電腦執行。每個動作仍照那邊的權限規則詢問你(手機上也能核准)。", p);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 已登入的裝置 ──
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
setting_title(ui, "直接連線的裝置", "用手機密碼登入過的手機和瀏覽器", p);
|
|
|
|
|
|
if self.state.remote.devices.is_empty() {
|
|
|
|
|
|
hint(ui, "還沒有", p);
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut remove = None;
|
|
|
|
|
|
for (i, d) in self.state.remote.devices.iter().enumerate() {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.label(RichText::new(&d.name).size(FONT_BASE));
|
|
|
|
|
|
let seen = chrono::DateTime::from_timestamp(d.last_seen.max(d.created), 0).map(|t| t.with_timezone(&chrono::Local).format("最近 %m/%d %H:%M").to_string()).unwrap_or_default();
|
|
|
|
|
|
ui.label(RichText::new(seen).size(12.0).color(p.text3));
|
|
|
|
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
|
|
|
|
if ui.add(egui::Button::new(RichText::new(t("登出")).size(12.0).color(p.danger)).frame(false)).clicked() {
|
|
|
|
|
|
remove = Some(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(i) = remove {
|
|
|
|
|
|
self.state.remote.devices.remove(i);
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 團隊 ──
|
|
|
|
|
|
if !self.state.remote.host_token.is_empty() {
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
setting_title(ui, "分享給團隊", "讓帳號伺服器上的其他帳號也能用這台 AnyBot 的 Bot。", p);
|
|
|
|
|
|
hint(ui, "成員:可以和 Bot 聊天、附加檔案;Bot 會知道是誰在說話。管理員:另外可以核准 Bot 要做的事、停止、看和操作電腦畫面。需要你允許的事只通知你和管理員。", p);
|
|
|
|
|
|
let loaded = self.rm.members.lock().unwrap().clone();
|
|
|
|
|
|
if loaded.is_none() {
|
|
|
|
|
|
*self.rm.members.lock().unwrap() = Some(Ok(vec![]));
|
|
|
|
|
|
let (m, s) = (self.rm.members.clone(), self.state.remote.clone());
|
|
|
|
|
|
std::thread::spawn(move || *m.lock().unwrap() = Some(remote::members(&s)));
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut change: Option<(String, &str)> = None;
|
|
|
|
|
|
match loaded {
|
|
|
|
|
|
Some(Ok(list)) => {
|
|
|
|
|
|
for (user, role) in list {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.label(RichText::new(&user).size(FONT_BASE));
|
|
|
|
|
|
ui.label(RichText::new(if role == "admin" { "管理員" } else { "成員" }).size(12.0).color(p.text3));
|
|
|
|
|
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
|
|
|
|
|
if ui.add(egui::Button::new(RichText::new(t("移除")).size(12.0).color(p.danger)).frame(false)).clicked() {
|
|
|
|
|
|
change = Some((user.clone(), "remove"));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(Err(e)) => hint(ui, format!("讀不到分享清單:{e}"), p),
|
|
|
|
|
|
None => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
ui.add(egui::TextEdit::singleline(&mut self.rm.new_member).hint_text(t("對方的帳號")).desired_width(160.0));
|
|
|
|
|
|
ui.checkbox(&mut self.rm.new_admin, t("管理員"));
|
|
|
|
|
|
if ui.add_enabled(!self.rm.new_member.trim().is_empty(), secondary_button("分享")).clicked() {
|
|
|
|
|
|
change = Some((self.rm.new_member.trim().to_string(), if self.rm.new_admin { "admin" } else { "member" }));
|
|
|
|
|
|
self.rm.new_member.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// Talk to the relay off the window's thread, then show the new list.
|
|
|
|
|
|
if let Some((user, role)) = change {
|
|
|
|
|
|
let (m, note, s, role) = (self.rm.members.clone(), self.rm.note.clone(), self.state.remote.clone(), role.to_string());
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
let r = remote::set_member(&s, &user, &role);
|
|
|
|
|
|
*note.lock().unwrap() = match r {
|
|
|
|
|
|
Ok(()) if role == "remove" => format!("已停止分享給 {user}"),
|
|
|
|
|
|
Ok(()) => format!("已分享給 {user}"),
|
|
|
|
|
|
Err(e) => e,
|
|
|
|
|
|
};
|
|
|
|
|
|
*m.lock().unwrap() = Some(remote::members(&s));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 帳號裡的其他電腦 ──
|
|
|
|
|
|
if !self.state.remote.host_token.is_empty() {
|
|
|
|
|
|
setting_card(ui, p, |ui| {
|
|
|
|
|
|
setting_title(ui, "帳號裡的電腦", "登入同一個帳號的 AnyBot。開了裝置連結的電腦,Bot 可以操作它。", p);
|
|
|
|
|
|
if ui.small_button(t("重新整理")).clicked() || (self.rm.peers.lock().unwrap().is_empty() && !self.rm.busy.load(Ordering::SeqCst)) {
|
|
|
|
|
|
let (peers, s) = (self.rm.peers.clone(), self.state.remote.clone());
|
|
|
|
|
|
let first = peers.lock().unwrap().is_empty();
|
|
|
|
|
|
if first {
|
|
|
|
|
|
peers.lock().unwrap().push((String::new(), "讀取中…".into(), false));
|
|
|
|
|
|
}
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
let v = remote::devices_online(&s);
|
|
|
|
|
|
*peers.lock().unwrap() = if v.is_empty() { vec![(String::new(), "(沒有)".into(), false)] } else { v };
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
for (id, name, online) in self.rm.peers.lock().unwrap().iter() {
|
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
|
let (r, _) = ui.allocate_exact_size(vec2(10.0, 10.0), egui::Sense::hover());
|
|
|
|
|
|
if !id.is_empty() {
|
|
|
|
|
|
ui.painter().circle_filled(r.center(), 4.0, if *online { Color32::from_rgb(0x14, 0xc3, 0x70) } else { p.text3 });
|
|
|
|
|
|
}
|
|
|
|
|
|
let me = *id == self.state.remote.host_id;
|
|
|
|
|
|
ui.label(RichText::new(format!("{name}{}", if me { "(這台)" } else { "" })).size(13.5));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if changed {
|
|
|
|
|
|
self.mark();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Other AnyBots of this account that let bots operate them (裝置連結), as computers.
|
|
|
|
|
|
pub(super) fn linked_devices(&self) -> Vec<tools::Target> {
|
|
|
|
|
|
if self.state.remote.host_token.is_empty() {
|
|
|
|
|
|
return vec![];
|
|
|
|
|
|
}
|
2026-11-05 06:40:01 +00:00
|
|
|
|
let has_hands = !remote::my_hands(&self.state.remote.host_id).is_empty();
|
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
|
|
|
|
self.rm
|
|
|
|
|
|
.peers
|
|
|
|
|
|
.lock()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|(id, _, online)| !id.is_empty() && *online && *id != self.state.remote.host_id)
|
2026-11-05 06:40:01 +00:00
|
|
|
|
// A brain with hands of its own works on those, not on the account's other brains.
|
|
|
|
|
|
.filter(|(id, _, _)| !has_hands || remote::is_hands(id))
|
2026-11-05 08:47:28 +00:00
|
|
|
|
// A computer showing another brain lends its hands to that one, not to this.
|
|
|
|
|
|
.filter(|(id, _, _)| {
|
|
|
|
|
|
let uses = remote::peer_uses(id);
|
|
|
|
|
|
uses.is_empty() || uses == self.state.remote.host_id
|
|
|
|
|
|
})
|
2026-11-05 13:28:32 +00:00
|
|
|
|
.map(|(id, name, _)| tools::Target { name: name.clone(), place: tools::Place::Device { host: id.clone(), dir: String::new() } })
|
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
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Phone: a home with the computers, pages and routines; each bot's settings a tap away; long jobs fold up
The phone page was bare: one row per bot, a chat listing every step, a menu with four lines.
- Home: the main bot large at the top with its model and last words, the other bots with times,
then its computers (own box and the account's, online or not, 看畫面), the newest pages and the
routines with their next run.
- Chat: the bot's name opens its settings — model (chat models only), computer, past conversations
(restore), regenerate, new chat. A turn's steps fold into 「已完成 N 個步驟」 when done (the newest
three stay while it works); copy and regenerate under replies; four ready starts in a new chat;
a jump back to the newest message.
- Menu: 外觀 (follow the phone, light, dark).
- API: api/state carries pages and models; each bot its model, computers, routines, past
conversations; POST api/bots/{id}/set | regenerate | restore. The home's last line skips errors.
Checked on a local copy of the brain's data (cut off from the account and Docker) at iPhone size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 12:28:08 +00:00
|
|
|
|
|
|
|
|
|
|
/// A model one can chat with (not an image, video, speech or embedding one).
|
|
|
|
|
|
fn chats(model: &str) -> bool {
|
|
|
|
|
|
let m = model.to_lowercase();
|
|
|
|
|
|
!["imagine", "image", "video", "embed", "tts", "whisper", "audio", "moderation", "dall-e", "sora", "veo"].iter().any(|k| m.contains(k))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod phone_tests {
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn only_chat_models_are_offered() {
|
|
|
|
|
|
assert!(super::chats("grok-4.7") && super::chats("claude-opus-5-5") && super::chats("gpt-5-mini"));
|
|
|
|
|
|
assert!(!super::chats("grok-imagine-video-1.5") && !super::chats("text-embedding-3-large") && !super::chats("gpt-4o-mini-tts"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|