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
// The window: bot list on the left, the chosen bot's chat on the right,
// plus the bot editor and the settings (look and AI providers).
use chrono ::Datelike ;
use eframe ::egui ::{ self , vec2 , Align , Color32 , CornerRadius , Frame , Key , Layout , Margin , RichText , Stroke } ;
use std ::collections ::HashMap ;
use std ::path ::PathBuf ;
use std ::sync ::atomic ::{ AtomicBool , Ordering } ;
use std ::sync ::mpsc ::{ channel , Receiver , Sender } ;
use std ::sync ::{ Arc , Mutex } ;
use std ::time ::Instant ;
use crate ::agent ;
use crate ::appctl ;
use crate ::avatar ;
use crate ::notify ;
use crate ::policy ;
use crate ::web ;
use crate ::model ::{ Accent , Bot , Message , Role , State , Theme , ToolStatus , TurnOutcome , TurnSettlement } ;
use crate ::providers ::{ self , AppCall , Approval , ApprovalAnswer , ApprovalKind , ChatRequest , ProviderKind , StreamEvent , ALL_PROVIDERS } ;
use crate ::tools ;
use crate ::i18n ::t ;
use crate ::theme ::{ Palette , DARK , LIGHT } ;
mod attach_ui ;
mod channels_ui ;
mod computers_ui ;
mod chat_ui ;
mod files_ui ;
mod panel_ui ;
mod remote_ui ;
mod sidebar_ui ;
mod menu_ui ;
mod vault_ui ;
mod markdown_ui ;
2026-11-05 06:25:01 +00:00
mod welcome_ui ;
2026-11-04 09:46:43 +00:00
mod mirror_ui ;
頁面: 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
mod pages_ui ;
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
// Sidebar rules from index.eager-app: defaults `FT={expandedWidth:280}` (@912025),
// clamp `jT(..,240,400)` (@914394), and the drag rule in `QJe` (@969523):
// folds to an 88 px rail below 210 px, unfolds from the rail only past 220 px.
pub const SIDEBAR_DEFAULT_WIDTH : f32 = 280.0 ;
pub const SIDEBAR_MIN_WIDTH : f32 = 240.0 ;
pub const SIDEBAR_MAX_WIDTH : f32 = 400.0 ;
pub const SIDEBAR_RAIL_WIDTH : f32 = 88.0 ;
2026-11-05 09:29:55 +00:00
/// Mac: the window has no title bar of its own (the content reaches the top); this strip at the
/// top moves the window, and the traffic lights sit in its left corner.
pub const TITLEBAR_H : f32 = 28.0 ;
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 SIDEBAR_COLLAPSE_BELOW : f32 = 210.0 ;
pub const SIDEBAR_EXPAND_FROM : f32 = 220.0 ;
/// `--sand-chat-min-width:424px` (index CSS, class sand-1fiktjy).
pub const CHAT_MIN_WIDTH : f32 = 424.0 ;
/// Transcript text: `--sand-transcript-prose-line-height:22px` pairs with `--sand-font-size-lg:15px`.
/// The original's type scale (renderer index.css `--sand-font-size-*`): xxs 11, xs 12, sm 13,
/// base 14, lg 15, xl 17, xxl 22, xxxl 26 px, with `--sand-line-height-*` 16/16/18/20/22/24/28/32.
pub const FONT_XS : f32 = 12.0 ;
pub const FONT_SM : f32 = 13.0 ;
pub const FONT_BASE : f32 = 14.0 ;
pub const FONT_LG : f32 = 15.0 ;
pub const FONT_XL : f32 = 17.0 ;
pub const LINE_HEIGHT_SM : f32 = 18.0 ;
pub const LINE_HEIGHT_LG : f32 = 22.0 ;
pub const LINE_HEIGHT_XL : f32 = 24.0 ;
/// Chat text uses the lg step (15 px on 22 px lines).
pub const TRANSCRIPT_FONT : f32 = FONT_LG ;
pub const TRANSCRIPT_LINE_HEIGHT : f32 = LINE_HEIGHT_LG ;
/// `--sand-transcript-prose-bubble-radius:18px`, one-line bubble `--sand-transcript-prose-one-line-height:36px`.
pub const BUBBLE_RADIUS : u8 = 18 ;
pub const BUBBLE_ONE_LINE_HEIGHT : f32 = 36.0 ;
pub const TRANSCRIPT_MAX_WIDTH : f32 = 720.0 ;
/// Space kept above and below Settings / Bot 設定 so they never run off the window (estimated).
const WINDOW_SCREEN_MARGIN : f32 = 80.0 ;
/// Provider names in Settings fit on one line (「自訂( OpenAI 相容)」) (estimated).
const PROVIDER_LIST_WIDTH : f32 = 180.0 ;
/// Settings: dialog width, its left-hand page list, and a steady height so tabs don't jump.
const SETTINGS_WIDTH : f32 = 760.0 ;
const SETTINGS_NAV_WIDTH : f32 = 150.0 ;
const SETTINGS_HEIGHT : f32 = 600.0 ;
/// Bot 設定 dialog width.
const EDITOR_WIDTH : f32 = 540.0 ;
/// The label column in form rows ("預設電腦", "模型可讀長度"…).
const FORM_LABEL_WIDTH : f32 = 96.0 ;
/// Tool steps under a reply start a little in from the bubble's edge.
pub const SCREENSHOT_FRAMES : u32 = 120 ;
#[ derive(PartialEq, Clone, Copy) ]
/// Like the original's seven panes (一般、整合、電腦、權限、保險庫、用量與帳單、更新); AnyBot has
/// 模型 (its own AI keys) where the original has 用量與帳單, and 帳戶 opens from 一般.
enum SettingsTab {
General ,
Providers ,
Channels ,
Computers ,
Review ,
Vault ,
Updates ,
Remote ,
}
pub struct App {
state : State ,
pal : Palette ,
demo : bool ,
screenshot : Option < PathBuf > ,
frames : u32 ,
shot_requested : bool ,
shot : Arc < Mutex < Option < Arc < egui ::ColorImage > > > > ,
drafts : HashMap < u64 , String > ,
search : String ,
streams : HashMap < u64 , Live > ,
/// A tool waiting for the person's yes/no, per bot.
approvals : HashMap < u64 , Approval > ,
/// App actions (e.g. deleting a routine) waiting for that yes/no.
app_pending : HashMap < u64 , AppCall > ,
/// Messages that arrived as a turn ended: they start the next turn.
pending_turns : Vec < ( u64 , Vec < String > ) > ,
last_tick : Instant ,
/// The routines list for this bot is open.
show_routines : Option < u64 > ,
new_remote : ( String , String ) ,
new_browser_profile : String ,
browser_note : std ::sync ::Arc < std ::sync ::Mutex < String > > ,
confirm_remove_profile : Option < String > ,
confirm_delete_routine : Option < ( u64 , u64 ) > ,
/// A short message shown above the message box (e.g. after a restart).
notice : Option < String > ,
/// Requests whose pop-up the person put off ("稍後再決定"); the chat card stays.
popup_hidden : std ::collections ::HashSet < u64 > ,
/// "全部允許,之後直接操作" was clicked in a request.
want_direct : std ::cell ::Cell < bool > ,
/// Admin locks (policy.rs), read at start and when Settings opens.
managed : policy ::Managed ,
settings_was_open : bool ,
/// When the program file this copy started from was built (its version).
exe_built : Option < std ::time ::SystemTime > ,
/// A newer build has replaced the program file since this copy started.
newer_build : Option < std ::time ::SystemTime > ,
last_build_check : Instant ,
/// Next time to consider switching to a newer build by itself (30 s after start, then hourly).
next_update_apply : Instant ,
window_focused : bool ,
idle_secs : Option < f64 > ,
last_presence : Instant ,
relaunching : bool ,
/// The sidebar's "new version" banner was clicked.
relaunch_requested : bool ,
new_allow_rule : String ,
new_ask_rule : String ,
side_drag : Option < f32 > ,
tx : Sender < ( u64 , StreamEvent ) > ,
rx : Receiver < ( u64 , StreamEvent ) > ,
show_settings : bool ,
settings_tab : SettingsTab ,
settings_provider : ProviderKind ,
edit_bot : Option < ( Bot , bool ) > ,
confirm_delete : Option < u64 > ,
model_fetch : HashMap < ProviderKind , Receiver < Result < Vec < String > , String > > > ,
fetch_status : HashMap < ProviderKind , String > ,
dirty : bool ,
last_save : Instant ,
stick_bottom : bool ,
/// Bot 的電腦 viewer, rebuild dialog and the computers page.
cu : computers_ui ::ComputerUi ,
vault : vault_ui ::VaultUi ,
md : markdown_ui ::MdState ,
頁面: 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
/// The page open large (頁面): its id, and an older revision when looking at one.
page_view : Option < ( u64 , Option < u32 > ) > ,
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
/// Files and pictures waiting to go with the next message.
att : attach_ui ::AttachUi ,
/// 訊息管道 (Slack / Discord / Telegram).
ch : channels_ui ::ChannelUi ,
/// A shared bot file being picked.
share_rx : Option < Receiver < Result < String , String > > > ,
/// 群組聊天: members still to answer in this round, and groups whose next member should start.
group_queue : HashMap < u64 , std ::collections ::VecDeque < u64 > > ,
group_next : Vec < u64 > ,
/// 引用: the message being quoted in the next message, per bot.
quoting : HashMap < u64 , String > ,
/// Index of the message being drawn (for reactions).
msg_index : usize ,
/// 手機與帳號.
rm : remote_ui ::RemoteUi ,
/// Running as `anybot --serve` (no window; the phone is the screen).
headless : bool ,
2026-11-05 08:47:28 +00:00
/// The computer the person last typed to a bot on: an account computer's id, "" for this
/// one, None when not known (the phone isn't a computer). It is "我的電腦" to the bots.
typed_on : Option < 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
/// 右側面板 and its tab (None = closed).
panel : Option < panel_ui ::PanelTab > ,
/// When each emoji reaction appeared (for its pop) and when each message first showed (fade-in).
popped : HashMap < ( u64 , usize , String ) , Instant > ,
appeared : HashMap < ( u64 , usize ) , Instant > ,
/// Messages whose steps / thinking are unfolded (⋯ › 顯示步驟).
steps_open : std ::collections ::HashSet < ( u64 , usize ) > ,
/// The file being previewed.
file_view : Option < files_ui ::FileView > ,
started_ms : i64 ,
/// The message whose hover menu (表情 / ⋯) is open: its buttons stay while it is.
menu_for : Option < ( u64 , usize ) > ,
/// Each face's motion (gaze, blink, pokes).
faces : HashMap < u64 , sidebar_ui ::Face > ,
search_open : bool ,
confirm_section_delete : Option < String > ,
/// A group being renamed: (old name, what is typed).
renaming : Option < ( String , String ) > ,
/// The group whose ⋯ menu is open (its button stays while it is).
group_menu_for : Option < String > ,
/// When the launch greeting started.
splash_at : Option < f64 > ,
2026-11-04 09:46:43 +00:00
/// 大腦: this window shows another AnyBot's bots (a screen), or answers screens (the brain).
mir : mirror_ui ::Mirror ,
scr : mirror_ui ::Screens ,
ctx_handle : Option < egui ::Context > ,
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 fn new ( ctx : & egui ::Context , screenshot : Option < PathBuf > ) -> Self {
2026-11-04 09:46:43 +00:00
// ANYBOT_SHOT_REAL=1: a screenshot of the real data (and connections), not the demo.
let demo = screenshot . is_some ( ) & & std ::env ::var ( " ANYBOT_SHOT_REAL " ) . is_err ( ) ;
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 state = if demo { State ::demo ( ) } else { State ::load ( ) } ;
2026-11-06 12:33:06 +00:00
crate ::routines ::set_person_timezone ( & 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 ( tx , rx ) = channel ( ) ;
let mut app = App {
state ,
pal : LIGHT ,
demo ,
screenshot ,
frames : 0 ,
shot_requested : false ,
shot : Arc ::new ( Mutex ::new ( None ) ) ,
drafts : HashMap ::new ( ) ,
search : String ::new ( ) ,
streams : HashMap ::new ( ) ,
approvals : HashMap ::new ( ) ,
app_pending : HashMap ::new ( ) ,
pending_turns : vec ! [ ] ,
last_tick : Instant ::now ( ) ,
show_routines : None ,
new_remote : Default ::default ( ) ,
new_browser_profile : String ::new ( ) ,
browser_note : Default ::default ( ) ,
confirm_remove_profile : None ,
confirm_delete_routine : None ,
notice : None ,
popup_hidden : Default ::default ( ) ,
want_direct : Default ::default ( ) ,
managed : if demo { policy ::Managed ::default ( ) } else { policy ::read ( ) } ,
settings_was_open : false ,
exe_built : exe_modified ( ) ,
newer_build : None ,
last_build_check : Instant ::now ( ) ,
next_update_apply : Instant ::now ( ) + std ::time ::Duration ::from_secs ( notify ::UPDATE_FIRST_CHECK_SECS ) ,
window_focused : true ,
idle_secs : None ,
last_presence : Instant ::now ( ) ,
relaunching : false ,
relaunch_requested : false ,
new_allow_rule : String ::new ( ) ,
new_ask_rule : String ::new ( ) ,
side_drag : None ,
tx ,
rx ,
show_settings : false ,
settings_tab : SettingsTab ::General ,
settings_provider : ProviderKind ::Xai ,
edit_bot : None ,
confirm_delete : None ,
model_fetch : HashMap ::new ( ) ,
fetch_status : HashMap ::new ( ) ,
dirty : false ,
last_save : Instant ::now ( ) ,
stick_bottom : true ,
cu : Default ::default ( ) ,
vault : Default ::default ( ) ,
md : Default ::default ( ) ,
頁面: 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
page_view : None ,
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
att : Default ::default ( ) ,
ch : Default ::default ( ) ,
share_rx : None ,
group_queue : HashMap ::new ( ) ,
group_next : vec ! [ ] ,
quoting : HashMap ::new ( ) ,
msg_index : 0 ,
rm : Default ::default ( ) ,
headless : false ,
2026-11-05 08:47:28 +00:00
typed_on : None ,
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
panel : None ,
popped : HashMap ::new ( ) ,
appeared : HashMap ::new ( ) ,
steps_open : Default ::default ( ) ,
file_view : None ,
started_ms : crate ::model ::now_ms ( ) ,
menu_for : None ,
faces : HashMap ::new ( ) ,
search_open : false ,
confirm_section_delete : None ,
renaming : None ,
group_menu_for : None ,
splash_at : None ,
2026-11-04 09:46:43 +00:00
mir : Default ::default ( ) ,
scr : Default ::default ( ) ,
ctx_handle : None ,
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
} ;
crate ::i18n ::set ( & app . state . language ) ;
app . computers_start ( ) ;
2026-11-05 11:41:49 +00:00
// The computer the person used last time (a brain that restarted for an update).
if ! app . demo {
if let Some ( h ) = crate ::remote ::load_person_host ( ) {
app . typed_on = Some ( 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
if demo {
// The computer helper's pending step, so the picture shows the approval card.
let ( reply , _ ) = channel ( ) ;
// This Mac isn't allowed yet, so the bot's step pops up a request.
app . state . local_execution = false ;
app . approvals . insert ( 5 , Approval {
call_id : " c2 " . into ( ) ,
tool : " shell " . into ( ) ,
reason : " 在你的電腦上執行任務, 而不只是在它自己的電腦上: mkdir -p 2026-04 2026-05 2026-06 && mv IMG_04*.jpg 2026-04/ \n (在 你的電腦 上) " . into ( ) ,
grant : " local:on " . into ( ) ,
reply ,
kind : ApprovalKind ::EnableThisComputer ,
} ) ;
app . streams . insert ( 5 , Live {
cancel : Arc ::new ( AtomicBool ::new ( false ) ) ,
started : Instant ::now ( ) ,
last : Instant ::now ( ) ,
produced : true ,
status : None ,
paused : true ,
steer : Arc ::new ( Mutex ::new ( vec! [ " 2026-06 的照片先不要動 " . into ( ) ] ) ) ,
direct : Arc ::new ( AtomicBool ::new ( false ) ) ,
turn_id : " demo " . into ( ) ,
} ) ;
// Optional view for checking the dialogs in a picture.
match std ::env ::var ( " ANYBOT_SHOT_VIEW " ) . as_deref ( ) {
Ok ( " chat " ) = > app . state . selected = 1 ,
頁面: 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
// 頁面: a tracking page in the chat (pagecard), and opened (page).
Ok ( v @ ( " pagecard " | " page " ) ) = > {
app . state . selected = 1 ;
app . approvals . clear ( ) ;
app . streams . remove ( & 5 ) ;
let ( pages , next , id ) = crate ::pages ::demo ( crate ::model ::now_ms ( ) ) ;
app . state . pages = pages ;
app . state . next_page_id = next ;
if let Some ( b ) = app . state . bot_mut ( 1 ) {
b . messages . push ( Message ::user ( " 幫我追蹤一下台灣 FSD 狀況 " ) ) ;
let mut m = Message ::assistant ( " 交通部今天對台灣 FSD 有新的官方說法,但你 12 月交車時多半還是不能用,現在先別花錢。 \n \n NOWnews 今天專訪交通部監理司專門委員。他說 7 月和 9 月已經開過兩次專家審查會議,交通部希望年底前完成相關審查,但不保證年底開放,而且路測計畫還沒核定。來源和細節都更新在[台灣 Tesla FSD 追蹤](page:1)。 " , " Master · Custom · qwen3.8-flash-next " ) ;
m . tool_calls . push ( crate ::model ::ToolCall { id : " pg1 " . into ( ) , name : " write_page " . into ( ) , args : serde_json ::json! ( { " id " : id , " title " : " 台灣 Tesla FSD 追蹤 " } ) . to_string ( ) , status : crate ::model ::ToolStatus ::Done } ) ;
b . messages . push ( m ) ;
}
if v = = " page " {
app . page_view = Some ( ( id , None ) ) ;
}
}
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
Ok ( " settings " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Providers ;
app . settings_provider = ProviderKind ::Anthropic ;
}
Ok ( " editor " ) = > {
app . edit_bot = app . state . bot ( 5 ) . cloned ( ) . map ( | mut b | {
b . allow_rules = vec! [ " edit_file " . into ( ) , " shell:npm install " . into ( ) ] ;
( b , false )
} )
}
Ok ( " menu " ) = > app . state . selected = 4 ,
Ok ( " rail " ) = > app . state . sidebar_collapsed = true ,
Ok ( " routines " ) = > {
app . state . selected = 2 ;
app . show_routines = Some ( 2 ) ;
}
Ok ( " attach " ) = > {
app . state . selected = 1 ;
let dir = std ::env ::temp_dir ( ) . join ( " anybot-shot-attach " ) ;
let _ = std ::fs ::create_dir_all ( & dir ) ;
let img = dir . join ( " 行程截圖.png " ) ;
let mut pic = image ::RgbImage ::from_pixel ( 640 , 400 , image ::Rgb ( [ 0xe8 , 0xf0 , 0xfb ] ) ) ;
for x in 40 .. 600 { for y in 60 .. 90 { pic . put_pixel ( x , y , image ::Rgb ( [ 0x16 , 0x80 , 0xf3 ] ) ) ; } }
let _ = pic . save ( & img ) ;
let doc = dir . join ( " 台南景點清單.md " ) ;
let _ = std ::fs ::write ( & doc , " # 台南 \n - 赤崁樓 \n - 安平古堡 \n " ) ;
for f in [ & img , & doc ] {
if let Ok ( a ) = crate ::attach ::store ( 1 , f ) {
app . att . pending . entry ( 1 ) . or_default ( ) . push ( a ) ;
}
}
}
Ok ( " channels " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Channels ;
app . state . channels . push ( crate ::channels ::Channel { id : 1 , kind : crate ::channels ::Kind ::Slack , name : " 客服群 " . into ( ) , bot_id : 2 , token : " xoxb-demo " . into ( ) , target : " C0123ABCD " . into ( ) , enabled : true , client_id : String ::new ( ) , tenant : String ::new ( ) } ) ;
}
Ok ( " group " ) = > {
let id = app . state . next_id ;
app . state . next_id + = 1 ;
let mut g = app . state . bots [ 0 ] . clone ( ) ;
g . id = id ;
g . name = " 旅遊小組 " . into ( ) ;
g . description = " 研究助理、寫作夥伴一起回答 " . into ( ) ;
g . shape = " cloud " . into ( ) ;
g . members = vec! [ 2 , 3 ] ;
let mut m1 = Message ::assistant ( " 台南兩天:第一天安平、第二天府城老街。 " , " 研究助理 · Claude · claude-opus-5-5 " ) ;
m1 . author = 2 ;
m1 . reactions = vec! [ " 👍 " . into ( ) ] ;
m1 . tool_calls . push ( crate ::model ::ToolCall { id : " card1 " . into ( ) , name : " show_card " . into ( ) , args : serde_json ::json! ( { " kind " : " weather " , " title " : " 台南 · 下週六 " , " subtitle " : " 中央氣象署預報 " , " rows " : [ { " label " : " 天氣 " , " value " : " 晴時多雲 " } , { " label " : " 溫度 " , " value " : " 22– 29°C " } , { " label " : " 降雨機率 " , " value " : " 10% " } ] , " link " : " https://www.cwa.gov.tw " } ) . to_string ( ) , status : crate ::model ::ToolStatus ::Done } ) ;
let mut m2 = Message ::assistant ( " 補充一個寫遊記的角度:用「老屋裡的光」當主題,照片挑早上拍的。 " , " 寫作夥伴 · OpenAI · gpt-5 " ) ;
m2 . author = 3 ;
g . messages = vec! [ Message ::user ( " 下個月去台南,大家給點建議 " ) , m1 , m2 ] ;
app . quoting . insert ( id , " 台南兩天:第一天安平、第二天府城老街。 " . into ( ) ) ;
app . state . bots . push ( g ) ;
app . state . selected = id ;
}
Ok ( " remote " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Remote ;
app . state . remote . password = " x$y " . into ( ) ;
app . state . remote . lan = true ;
app . state . remote . relay_url = " https://anybot.example.com " . into ( ) ;
app . state . remote . relay_user = " daniel " . into ( ) ;
app . state . remote . host_token = " demo " . into ( ) ;
app . state . remote . host_name = " 工作室 Mac " . into ( ) ;
app . state . remote . last_backup = chrono ::Utc ::now ( ) . timestamp ( ) - 3_600 ;
app . state . remote . device_link = true ;
app . state . remote . devices = vec! [ crate ::remote ::Device { name : " iPhone " . into ( ) , token_hash : String ::new ( ) , created : chrono ::Utc ::now ( ) . timestamp ( ) - 86_400 , last_seen : chrono ::Utc ::now ( ) . timestamp ( ) - 600 } ] ;
}
Ok ( " working " ) = > {
// The computer helper mid-task with no pending request: the ring and phrase.
app . state . selected = 5 ;
app . approvals . remove ( & 5 ) ;
if let Some ( b ) = app . state . bot_mut ( 5 ) {
if let Some ( m ) = b . messages . iter_mut ( ) . rev ( ) . find ( | m | m . role = = Role ::Assistant ) {
for c in m . tool_calls . iter_mut ( ) {
c . name = " read_file " . into ( ) ;
c . status = ToolStatus ::Running ;
}
}
}
}
Ok ( " files " ) | Ok ( " fileview " ) = > {
for b in app . state . bots . iter_mut ( ) {
if b . id = = 2 | | b . id = = 3 {
b . folder = " 工作 " . into ( ) ;
}
}
let dir = std ::env ::temp_dir ( ) . join ( " anybot-shot-files " ) ;
let _ = std ::fs ::create_dir_all ( & dir ) ;
let csv = dir . join ( " orders_20261102_packages.csv " ) ;
let mut rows = String ::from ( " 訂單號,訂單時間,店鋪,商品名稱,數量 \n " ) ;
for i in 0 .. 22 {
rows . push_str ( & format! ( " 33165343590 {:04} ,2026-10-29 20:13, {} ,特斯拉 Model Y 配件 {} , {} \n " , 5101 + i , [ " 3w旗艦店 " , " Tesla特斯拉官方 " , " 特友之家旗艦店 " ] [ i % 3 ] , i + 1 , 1 + i % 3 ) ) ;
}
let _ = std ::fs ::write ( & csv , rows ) ;
let md = dir . join ( " 台南兩天一夜.md " ) ;
let _ = std ::fs ::write ( & md , " # 台南兩天一夜 \n \n ## 第一天 \n - 赤崁樓 \n - 國華街小吃 \n \n ## 第二天 \n - 安平古堡 \n - **四草綠色隧道** \n " ) ;
let html = dir . join ( " 報價頁.html " ) ;
let _ = std ::fs ::write ( & html , " <html><body><h1>報價</h1><p>總計 NT$ 12,800</p></body></html> " ) ;
let png = dir . join ( " 行程地圖.png " ) ;
let mut pic = image ::RgbImage ::from_pixel ( 800 , 500 , image ::Rgb ( [ 0xe8 , 0xf0 , 0xfb ] ) ) ;
for x in 100 .. 700 { for y in 230 .. 270 { pic . put_pixel ( x , y , image ::Rgb ( [ 0x16 , 0x80 , 0xf3 ] ) ) ; } }
let _ = pic . save ( & png ) ;
let now = crate ::model ::now_ms ( ) ;
let mut atts = vec! [ ] ;
for f in [ & csv , & md , & html , & png ] {
if let Ok ( a ) = crate ::attach ::store ( 1 , f ) {
atts . push ( a ) ;
}
}
let mut u = Message ::user ( " 我有多了兩項訂單,請幫我更新,順便把行程整理成文件給我 " ) ;
u . at = now - 26 * 3_600_000 ;
u . reactions = vec! [ " ❤️ " . into ( ) ] ;
let mut a1 = Message ::assistant ( " 好,我去把新的兩張訂單抓下來,補進表格。 " , " " ) ;
a1 . at = u . at + 4_000 ;
let mut a2 = Message ::assistant ( " 更新好了,兩張 10/31 的新訂單已經補在最上面。 \n \n - 特友之家的車門垃圾桶( 2 個裝)買 1 組 \n - 特友黑洛第二代油門剎車踏板買 2 個 \n \n 行程和報價也整理好了: " , " " ) ;
a2 . at = u . at + 60_000 ;
a2 . attachments = atts ;
a2 . reactions = vec! [ " 👍 " . into ( ) ] ;
let mut u2 = Message ::user ( " 太好了,謝謝! " ) ;
u2 . at = now - 60_000 ;
let mut b = app . state . bots [ 0 ] . clone ( ) ;
b . messages = vec! [ u , a1 , a2 , u2 ] ;
app . state . bots [ 0 ] = b ;
app . state . selected = app . state . bots [ 0 ] . id ;
if std ::env ::var ( " ANYBOT_SHOT_VIEW " ) . as_deref ( ) = = Ok ( " fileview " ) {
// ANYBOT_SHOT_FILE: preview that file instead of the sample spreadsheet.
match std ::env ::var ( " ANYBOT_SHOT_FILE " ) {
Ok ( f ) = > {
let name = std ::path ::Path ::new ( & f ) . file_name ( ) . map ( | n | n . to_string_lossy ( ) . to_string ( ) ) . unwrap_or_default ( ) ;
app . open_file ( & name , & f ) ;
}
Err ( _ ) = > app . open_file ( " orders_20261102_packages.csv " , & csv . display ( ) . to_string ( ) ) ,
}
}
}
2026-11-05 09:29:55 +00:00
// 需要處理: the bot waits for the person on its own computer (the hand-off card).
Ok ( " handoff " ) = > {
app . state . selected = 1 ;
let ( reply , _ ) = channel ( ) ;
let own = app . state . bot ( 1 ) . map ( | b | crate ::model ::own_computer_name ( & b . name ) ) . unwrap_or_default ( ) ;
app . approvals . insert ( 1 , Approval { call_id : " h1 " . into ( ) , tool : tools ::HANDOFF_TOOL . into ( ) , reason : " 請在 Threads 登入頁輸入帳號和密碼後登入,好了按「我完成了」。 " . into ( ) , grant : own , reply , kind : ApprovalKind ::Handoff } ) ;
}
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
Ok ( " panel " ) = > {
// The real box, so the 電腦 card shows its screen.
crate ::cloudbox ::start_background ( ) ;
let _ = crate ::cloudbox ::ensure_running ( ) ;
app . state . selected = 1 ;
app . panel = Some ( panel_ui ::PanelTab ::Activity ) ;
}
Ok ( " general " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::General ;
}
Ok ( " computers " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Computers ;
}
Ok ( " markdown " ) = > {
// Every Markdown element the chat draws, in one reply.
let text = " ## 出差流程整理 \n 先看重點,**下週三前**要送出申請;細節在 [公司內網](https://example.com/travel)。 \n 第二行直接換行,不用空一行。 \n \n ### 步驟 \n 1. 填出差單 \n 2. 主管核准 \n - 金額超過 3 萬要*總經理*簽 \n - ~~紙本~~ 改用線上表單 \n 3. 訂機票和住宿 \n \n - [x] 護照效期確認 \n - [ ] 換外幣 \n \n > 提醒:發票抬頭要寫公司全名。 \n \n | 項目 | 上限 | 備註 | \n |---|--:|---| \n | 住宿 | 3,500 | 每晚 | \n | 交通 | 實報實銷 | 留收據 | \n | 餐費 | 800 | `每日` | \n \n ```python \n # 算總額 \n def total(days, rate=3500): \n return days * rate + 800 * days \n ``` \n \n ```mermaid \n flowchart LR \n A[填出差單] --> B{超過 3 萬?} \n B -->|是| C[總經理簽核] \n B -->|否| D[主管簽核] \n C --> E[訂機票住宿] \n D --> E \n ``` \n \n --- \n 有問題再問我! " ;
let mut u = Message ::user ( " 幫我整理出差流程 " ) ;
u . at = crate ::model ::now_ms ( ) - 120_000 ;
let mut a = Message ::assistant ( text , " " ) ;
a . at = u . at + 5_000 ;
let mut b = app . state . bots [ 0 ] . clone ( ) ;
b . messages = vec! [ u , a ] ;
app . state . bots [ 0 ] = b ;
app . state . selected = app . state . bots [ 0 ] . id ;
}
Ok ( " vault " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Vault ;
}
Ok ( " review " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Review ;
app . state . auto_review = true ;
app . state . review_allow_rules = vec! [ " 在工作資料夾裡建立或修改文字檔 " . into ( ) ] ;
app . state . review_ask_rules = vec! [ " 刪除任何檔案 " . into ( ) , " 花錢或付款 " . into ( ) ] ;
}
_ = > { }
}
if let Ok ( l ) = std ::env ::var ( " ANYBOT_SHOT_LANG " ) {
app . state . language = l . clone ( ) ;
crate ::i18n ::set ( & l ) ;
}
if std ::env ::var ( " ANYBOT_SHOT_DARK " ) . is_ok ( ) {
app . state . theme = Theme ::Dark ;
}
// For checking each screen without the demo's waiting pop-up on top.
if std ::env ::var ( " ANYBOT_SHOT_NOPOPUP " ) . is_ok ( ) {
app . popup_hidden . extend ( app . state . bots . iter ( ) . map ( | b | b . id ) ) ;
}
}
app . apply_theme ( ctx ) ;
2026-11-06 16:19:56 +00:00
// Real-data screenshots can open 設定 › 手機與帳號 (to press its buttons with ANYBOT_SHOT_CLICK)
// or 設定 › 電腦 (容器隔離 shows what the box really runs on).
if app . screenshot . is_some ( ) & & ! app . demo {
match std ::env ::var ( " ANYBOT_SHOT_VIEW " ) . as_deref ( ) {
Ok ( " remote " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Remote ;
}
Ok ( " computers " ) = > {
app . show_settings = true ;
app . settings_tab = SettingsTab ::Computers ;
}
_ = > { }
}
2026-11-04 11:30:32 +00:00
}
2026-11-05 09:29:55 +00:00
// …or the side panel, or the computer viewer, as they look with that data.
if app . screenshot . is_some ( ) & & ! app . demo {
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
if let Ok ( " panel " ) = std ::env ::var ( " ANYBOT_SHOT_VIEW " ) . as_deref ( ) {
app . panel = Some ( panel_ui ::PanelTab ::Activity ) ;
2026-11-05 09:29:55 +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
app
}
fn apply_theme ( & mut self , ctx : & egui ::Context ) {
let dark = match self . state . theme {
Theme ::Light = > false ,
Theme ::Dark = > true ,
Theme ::System = > ctx . input ( | i | i . raw . system_theme ) = = Some ( egui ::Theme ::Dark ) ,
} ;
self . pal = if dark { DARK } else { LIGHT } ;
let p = self . pal ;
let mut v = if dark { egui ::Visuals ::dark ( ) } else { egui ::Visuals ::light ( ) } ;
v . panel_fill = p . pane ;
v . window_fill = p . elevated ;
v . extreme_bg_color = p . sidebar ;
v . text_edit_bg_color = Some ( p . sidebar ) ;
v . faint_bg_color = p . hover ;
v . override_text_color = Some ( p . text ) ;
v . window_stroke = Stroke ::new ( 1.0 , p . border ) ;
v . window_corner_radius = CornerRadius ::same ( 14 ) ;
v . menu_corner_radius = CornerRadius ::same ( 10 ) ;
v . selection . bg_fill = p . focus . gamma_multiply ( 0.35 ) ;
v . selection . stroke = Stroke ::new ( 1.0 , p . focus ) ;
v . hyperlink_color = p . accent ;
for w in [ & mut v . widgets . inactive , & mut v . widgets . hovered , & mut v . widgets . active , & mut v . widgets . open ] {
w . corner_radius = CornerRadius ::same ( 8 ) ;
}
v . widgets . inactive . weak_bg_fill = p . hover ;
v . widgets . inactive . bg_fill = p . hover ;
v . widgets . hovered . weak_bg_fill = p . active ;
v . widgets . hovered . bg_fill = p . active ;
v . widgets . active . weak_bg_fill = p . active ;
v . widgets . inactive . bg_stroke = Stroke ::NONE ;
v . widgets . hovered . bg_stroke = Stroke ::NONE ;
v . widgets . noninteractive . bg_stroke = Stroke ::new ( 1.0 , p . border ) ;
ctx . set_visuals ( v ) ;
ctx . all_styles_mut ( | s | {
// Menus like the original's: 16 px corners, 6 px inside, rows touching.
s . visuals . menu_corner_radius = CornerRadius ::same ( 16 ) ;
s . spacing . menu_margin = Margin ::same ( 6 ) ;
s . spacing . menu_spacing = 0.0 ;
s . visuals . popup_shadow = egui ::epaint ::Shadow { offset : [ 0 , 10 ] , blur : 30 , spread : 0 , color : Color32 ::from_black_alpha ( 70 ) } ;
s . spacing . item_spacing = vec2 ( 8.0 , 6.0 ) ;
s . spacing . button_padding = vec2 ( 10.0 , 5.0 ) ;
s . text_styles . insert ( egui ::TextStyle ::Body , egui ::FontId ::proportional ( FONT_BASE ) ) ;
s . text_styles . insert ( egui ::TextStyle ::Button , egui ::FontId ::proportional ( FONT_BASE ) ) ;
s . text_styles . insert ( egui ::TextStyle ::Small , egui ::FontId ::proportional ( FONT_XS ) ) ;
s . text_styles . insert ( egui ::TextStyle ::Heading , egui ::FontId ::proportional ( FONT_XL ) ) ;
s . text_styles . insert ( egui ::TextStyle ::Monospace , egui ::FontId ::monospace ( FONT_SM ) ) ;
} ) ;
}
fn mark ( & mut self ) {
self . dirty = true ;
}
fn bot_color ( & self , b : & Bot ) -> Color32 {
let c = avatar ::color ( & b . color ) ;
// A black avatar would vanish on the dark page: lift it to a dark grey there.
if self . pal . pane ! = LIGHT . pane & & ( c . r ( ) as u32 + c . g ( ) as u32 + c . b ( ) as u32 ) < 120 {
return Color32 ::from_gray ( 0x4a ) ;
}
c
}
fn user_bubble ( & self , b : & Bot ) -> Color32 {
match self . state . accent {
Accent ::BotColor if b . color ! = " black " = > self . bot_color ( b ) ,
_ = > self . pal . bubble_user ,
}
}
// ---------- chat ----------
fn send ( & mut self , ctx : & egui ::Context , bot_id : u64 ) {
let mut text = self . drafts . get ( & bot_id ) . cloned ( ) . unwrap_or_default ( ) ;
if text . trim ( ) . is_empty ( ) & & ! self . has_pending_attachments ( bot_id ) {
return ;
}
// 引用: the quoted words go first, as a Markdown quote.
if let Some ( q ) = self . quoting . remove ( & bot_id ) {
let quoted : String = q . lines ( ) . take ( 8 ) . map ( | l | format! ( " > {l} \n " ) ) . collect ( ) ;
text = format! ( " {quoted} \n {} " , text . trim ( ) ) ;
}
let attachments = self . take_attachments ( bot_id ) ;
2026-11-04 09:46:43 +00:00
if self . mir . on {
if self . mirror_send ( bot_id , text . trim ( ) . to_string ( ) , attachments ) {
self . drafts . insert ( bot_id , String ::new ( ) ) ;
}
return ;
}
self . drafts . insert ( bot_id , String ::new ( ) ) ;
2026-11-05 08:47:28 +00:00
// Typed here: this computer is the person's.
self . typed_on = Some ( 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
// While it works, a new message steers the running turn (the original's `turnSteer`:
// it doesn't count as a new turn) and reaches the model at its next step.
if let Some ( l ) = self . streams . get ( & bot_id ) {
let ( extra , _ ) = crate ::attach ::for_model ( & attachments ) ;
l . steer . lock ( ) . unwrap ( ) . push ( format! ( " {} {extra} " , text . trim ( ) ) ) ;
return ;
}
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
b . messages . push ( Message { attachments , .. Message ::user ( text . trim ( ) ) } ) ;
}
self . start_reply ( ctx , bot_id ) ;
}
fn start_reply ( & mut self , ctx : & egui ::Context , bot_id : u64 ) {
2026-11-04 09:46:43 +00:00
if self . mir . on {
self . mirror_act ( serde_json ::json! ( { " bot " : bot_id , " action " : " reply " } ) ) ;
return ;
}
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 Some ( conv ) = self . state . bot ( bot_id ) . cloned ( ) else { return } ;
// 群組聊天: the next member answers, with its own model, instructions and computers.
let speaker = if conv . members . is_empty ( ) {
None
} else {
let q = self . group_queue . entry ( bot_id ) . or_default ( ) ;
if q . is_empty ( ) {
q . extend ( conv . members . iter ( ) . copied ( ) . filter ( | m | * m ! = bot_id ) ) ;
}
let mut next = None ;
while let Some ( m ) = q . pop_front ( ) {
if self . state . bot ( m ) . is_some ( ) {
next = Some ( m ) ;
break ;
}
}
match next {
Some ( m ) = > Some ( m ) ,
None = > return ,
}
} ;
let b = match speaker . and_then ( | m | self . state . bot ( m ) . cloned ( ) ) {
Some ( member ) = > member ,
None = > conv . clone ( ) ,
} ;
let ps = self . state . provider ( b . provider ) ;
// Computer tools are always offered; if they're off for this bot (or this Mac isn't
// allowed yet), using one pops up a request instead of failing quietly.
let computers = self . targets_for ( & b ) ;
2026-11-05 08:47:28 +00:00
let roles = self . roles_for ( & b , & computers ) ;
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 : Vec < String > = computers . iter ( ) . map ( | t | t . name . clone ( ) ) . collect ( ) ;
2026-11-05 08:47:28 +00:00
let ( default_computer , missing_computer ) = Self ::chosen_computer ( & b , & computers , & roles ) ;
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 mut tool_specs = appctl ::specs ( ) ;
tool_specs . extend ( tools ::specs_for ( & names , & default_computer ) ) ;
2026-11-05 08:47:28 +00:00
// Browsers: the bot's own computer's Chromium, this computer's AnyBot browser, and the
// account's other computers' ones (each with its own logins).
let browsers : Vec < String > = computers
. iter ( )
. filter ( | t | match t . place {
tools ::Place ::Box { .. } = > crate ::cloudbox ::available ( ) ,
tools ::Place ::Local { .. } = > crate ::botbrowser ::available ( ) ,
tools ::Place ::Device { .. } = > true ,
_ = > false ,
} )
. map ( | t | t . name . clone ( ) )
. collect ( ) ;
let browser_default = if browsers . contains ( & default_computer ) { default_computer . clone ( ) } else { roles . person . clone ( ) . filter ( | p | browsers . contains ( p ) ) . or_else ( | | browsers . first ( ) . cloned ( ) ) . unwrap_or_default ( ) } ;
tool_specs . extend ( web ::specs_for ( & browsers , & browser_default ) ) ;
// Screens: the bot's own desktop (when it has one), this computer's (when it has one), and
// the account's other computers (their own AnyBot looks and clicks there).
let screens : Vec < String > = computers
. iter ( )
. filter ( | t | match t . place {
tools ::Place ::Box { .. } = > crate ::cloudbox ::available ( ) ,
tools ::Place ::Local { .. } = > crate ::computer ::screen_supported ( ) ,
tools ::Place ::Device { .. } = > true ,
_ = > false ,
} )
. map ( | t | t . name . clone ( ) )
. collect ( ) ;
// The person's Mac apps (mail, notes, calendar…): here on a Mac, or on the computer the
// person is using when this is a server.
let person_is_device = roles . person . as_ref ( ) . map ( | n | computers . iter ( ) . any ( | t | & t . name = = n & & matches! ( t . place , tools ::Place ::Device { .. } ) ) ) . unwrap_or ( false ) ;
let mac_apps = cfg! ( target_os = " macos " ) | | ( roles . server & & person_is_device ) ;
if mac_apps {
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
tool_specs . extend ( crate ::macapps ::specs ( ) . into_iter ( ) . filter ( | t | self . state . messages_send | | t [ " name " ] ! = crate ::macapps ::SEND_MESSAGE ) ) ;
}
tool_specs . extend ( crate ::mcp ::specs ( ) ) ;
tool_specs . extend ( crate ::phone ::specs ( ) ) ;
tool_specs . push ( serde_json ::json! ( { " name " : " show_card " , " description " : " Show a tidy card in the chat for things like a flight, a restaurant or place, the weather, a product or an event (the original's life cards). Fill it from what you found; give the source link. " ,
" parameters " : { " type " : " object " , " properties " : {
" kind " : { " type " : " string " , " enum " : [ " flight " , " place " , " weather " , " product " , " event " , " other " ] } ,
" title " : { " type " : " string " } , " subtitle " : { " type " : " string " } ,
" rows " : { " type " : " array " , " items " : { " type " : " object " , " properties " : { " label " : { " type " : " string " } , " value " : { " type " : " string " } } , " required " : [ " label " , " value " ] } } ,
" link " : { " type " : " string " } } , " required " : [ " kind " , " title " ] } } ) ) ;
tool_specs . push ( serde_json ::json! ( { " name " : " react " , " description " : " Put an emoji reaction on the person's latest message, like a friend would (e.g. ❤️ for thanks or good news, 👍 for OK, 😂 for a joke, 🎉 for a win, 👀 when you'll look into it). Use it now and then where it fits; you can still answer in words. " ,
" parameters " : { " type " : " object " , " properties " : { " emoji " : { " type " : " string " , " description " : " One emoji " } } , " required " : [ " emoji " ] } } ) ) ;
2026-11-06 11:47:43 +00:00
tool_specs . push ( serde_json ::json! ( { " name " : " send_file " , " description " : " Send a file (spreadsheet, document, Markdown, web page, picture, PDF…) into the chat as a file card the person can preview and download, straight from whichever computer it is on — your own or one of the person's (pass computer; up to 20 MB from theirs). Use this to hand over results instead of only saying where the file is; never copy a file to another computer first just to send it. " ,
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
" parameters " : { " type " : " object " , " properties " : { " path " : { " type " : " string " , " description " : " File path (relative to your folder) " } , " computer " : { " type " : " string " , " enum " : names . clone ( ) } } , " required " : [ " path " ] } } ) ) ;
tool_specs . push ( serde_json ::json! ( { " name " : " open_for_person " , " description " : " Open a file you made (a web page, chart, report, picture, document) on the person's screen so they can see it, or a web address. From the bot's computer it is copied over first. " ,
" parameters " : { " type " : " object " , " properties " : { " path " : { " type " : " string " , " description " : " File path (relative to your folder) or http(s) address " } , " computer " : { " type " : " string " , " enum " : names . clone ( ) } } , " required " : [ " path " ] } } ) ) ;
if ! self . state . remote . host_token . is_empty ( ) {
tool_specs . push ( serde_json ::json! ( { " name " : " publish_site " , " description " : " Publish a web page or a folder with index.html that you made as a public website on the person's AnyBot account server. Publishing again with the same name replaces it. Give the person the address you get back. " ,
" parameters " : { " type " : " object " , " properties " : { " path " : { " type " : " string " , " description " : " Folder with index.html, or one .html file " } , " name " : { " type " : " string " , " description " : " Short site name: letters, digits and - " } , " computer " : { " type " : " string " , " enum " : names . clone ( ) } } , " required " : [ " path " , " name " ] } } ) ) ;
}
if crate ::botbrowser ::available ( ) {
tool_specs . extend ( crate ::vault ::specs ( ) ) ;
}
if b . demos . iter ( ) . any ( | d | d . shots . iter ( ) . any ( | s | ! s . is_empty ( ) ) ) {
tool_specs . push ( crate ::teach ::view_step_spec ( ) ) ;
}
if ! screens . is_empty ( ) {
let default_screen = if screens . contains ( & default_computer ) { default_computer . clone ( ) } else { screens [ 0 ] . clone ( ) } ;
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
tool_specs . extend ( crate ::computer ::specs_for ( & screens , & default_screen , screens . contains ( & default_computer ) ) ) ;
2026-11-05 09:29:55 +00:00
// 需要處理: hand a step to the person on that screen (the original's box hand-off).
tool_specs . push ( serde_json ::json! ( { " name " : tools ::HANDOFF_TOOL ,
" description " : " Ask the person to do one step themselves on a computer's screen: signing in (you never type their passwords), a captcha or puzzle, a code sent to their phone or email, approving a payment, or anything else only they may do. First get the computer to that point (open the page, click into the sign-in form), then call this with a short instruction. A card with that computer's live screen appears in the chat; they take over, do it, and press 「我完成了」 (or 「跳過」). The result says which; after 「我完成了」 take a screenshot and carry on. Don't ask them in words to find a window themselves. " ,
" parameters " : { " type " : " object " , " properties " : {
" instruction " : { " type " : " string " , " description " : " What they should do, one or two short sentences in their language, e.g. 「請在 Threads 登入頁輸入帳號和密碼後登入」. " } ,
" computer " : { " type " : " string " , " enum " : screens . clone ( ) , " description " : format ! ( " Which computer's screen. Default: {default_screen}. " ) } } ,
" required " : [ " instruction " ] } } ) ) ;
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-04 13:52:06 +00:00
// 看圖小幫手: the person's models that may see pictures, for a bot whose model can't.
providers ::set_eyes ( self . eyes ( ) ) ;
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 req = ChatRequest {
kind : b . provider ,
base_url : providers ::resolve_base ( b . provider , & ps . base_url ) ,
api_key : providers ::resolve_key ( b . provider , & ps . api_key ) ,
model : b . model . clone ( ) ,
system : {
2026-11-05 08:47:28 +00:00
let mut sys = system_prompt ( & b , & self . state , & computers , & default_computer , & roles , missing_computer . as_deref ( ) , & self . computer_lines ( & computers , & roles ) , mac_apps ) ;
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 speaker . is_some ( ) {
let names : Vec < String > = conv . members . iter ( ) . filter_map ( | m | self . state . bot ( * m ) ) . map ( | x | x . name . clone ( ) ) . collect ( ) ;
sys . push_str ( & format! ( " \n \n 你在群組聊天「 {} 」裡,成員依序回答: {} 。其他成員的發言以【名字】標示。只回答你擅長的部分,補充不同的觀點,不要重複別人說過的;沒有要補充就簡短說一句。 " , conv . name , names . join ( " 、 " ) ) ) ;
}
sys
} ,
history : group_history ( & conv . messages , speaker , & self . state ) ,
tools : tool_specs ,
session : format ! ( " anybot-bot-{bot_id} " ) ,
} ;
let via = if speaker . is_some ( ) { format! ( " {} · {} · {} " , b . name , b . provider . label ( ) , b . model ) } else { format! ( " {} · {} " , b . provider . label ( ) , b . model ) } ;
if let Some ( c ) = self . state . bot_mut ( bot_id ) {
c . messages . push ( Message { author : speaker . unwrap_or ( 0 ) , .. Message ::assistant ( " " , & via ) } ) ;
}
let cancel = Arc ::new ( AtomicBool ::new ( false ) ) ;
let steer = Arc ::new ( Mutex ::new ( Vec ::new ( ) ) ) ;
let turn_id = format! ( " turn- {bot_id} - {} " , chrono ::Utc ::now ( ) . timestamp_millis ( ) ) ;
let direct = Arc ::new ( AtomicBool ::new ( self . direct_on ( ) ) ) ;
self . streams . insert ( bot_id , Live { cancel : cancel . clone ( ) , started : Instant ::now ( ) , last : Instant ::now ( ) , produced : false , status : None , paused : false , steer : steer . clone ( ) , turn_id , direct : direct . clone ( ) } ) ;
self . stick_bottom = true ;
let review = self . state . auto_review . then ( | | agent ::Review { allow : self . state . review_allow_rules . clone ( ) , ask : self . state . review_ask_rules . clone ( ) } ) ;
let job = agent ::Job {
req ,
computers ,
default_computer ,
2026-11-05 08:47:28 +00:00
roles ,
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
local_execution : self . local_on ( ) ,
local_locked : ! self . managed . local . allows ( ) | | self . state . local_never ,
tools_enabled : b . tools_enabled ,
direct ,
grants : {
let mut g = b . allow_rules . clone ( ) ;
if self . state . messages_no_ask {
g . push ( " messages:no-ask " . into ( ) ) ;
}
g
} ,
context_tokens : b . context_tokens . max ( 4_000 ) ,
via ,
via_name : b . name . clone ( ) ,
steer ,
review ,
demos : b . demos . clone ( ) ,
2026-11-06 15:12:53 +00:00
skills : self . state . skills . 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
} ;
agent ::spawn ( job , bot_id , self . tx . clone ( ) , cancel , ctx . clone ( ) ) ;
// (Its eyes start reading while it works; see face_pose.)
self . mark ( ) ;
}
2026-11-05 08:47:28 +00:00
/// The computers a bot can be told to use, each under its own name: the bot's own
/// (「Master 的電腦」), this computer, the SSH ones, then the account's other computers.
pub ( super ) fn targets_for ( & self , b : & Bot ) -> Vec < tools ::Target > {
let mut v = vec! [ tools ::Target {
name : crate ::model ::own_computer_name ( & b . name ) ,
place : if crate ::cloudbox ::available ( ) { tools ::Place ::Box { dir : crate ::cloudbox ::bot_dir ( b . id ) } } else { tools ::Place ::Sealed { root : crate ::model ::bot_computer_dir ( b . id ) } } ,
} ] ;
// Two computers never share a name (the bot and the person name them to choose).
let add = | v : & mut Vec < tools ::Target > , mut t : tools ::Target | {
let base = t . name . clone ( ) ;
let mut n = 2 ;
while v . iter ( ) . any ( | x | x . name = = t . name ) {
t . name = format! ( " {base} ( {n} ) " ) ;
n + = 1 ;
}
v . push ( t ) ;
} ;
2026-11-05 09:07:04 +00:00
// A brain on a server works on the bots' own computer and the person's computers, never on
// the server itself (the person asked for it to be off the list).
if ! self . headless {
add ( & mut v , tools ::Target { name : self . state . this_computer_name ( ) , place : tools ::Place ::Local { workdir : tools ::expand_home ( if b . workdir . trim ( ) . is_empty ( ) { " ~ " } else { b . workdir . trim ( ) } ) } } ) ;
}
2026-11-05 08:47:28 +00:00
for c in & self . state . remote_computers {
add ( & mut v , tools ::Target { name : c . name . clone ( ) , place : tools ::Place ::Ssh { target : c . ssh_target . clone ( ) , dir : c . dir . clone ( ) } } ) ;
}
2026-11-05 13:28:32 +00:00
// 裝置連結: other AnyBots of this account that allow it, working in the bot's work folder.
2026-11-05 08:47:28 +00:00
for d in self . linked_devices ( ) {
2026-11-05 13:28:32 +00:00
let mut d = d ;
if let tools ::Place ::Device { dir , .. } = & mut d . place {
dir . clone_from ( & b . workdir ) ;
}
2026-11-05 08:47:28 +00:00
add ( & mut v , d ) ;
}
v
}
/// Which of these computers is the bot's own, which one the person is using, and whether
/// this AnyBot is a server. The person's computer is the one they last typed on; a server
/// that hasn't heard from any yet takes the account's only other computer, if there is one.
pub ( super ) fn roles_for ( & self , b : & Bot , targets : & [ tools ::Target ] ) -> tools ::Roles {
let server = self . headless ;
let local = targets . iter ( ) . find ( | t | matches! ( t . place , tools ::Place ::Local { .. } ) ) . map ( | t | t . name . clone ( ) ) ;
2026-11-05 13:28:32 +00:00
let device = | h : & str | targets . iter ( ) . find ( | t | matches! ( & t . place , tools ::Place ::Device { host , .. } if host = = h ) ) . map ( | t | t . name . clone ( ) ) ;
2026-11-05 08:47:28 +00:00
let person = match self . typed_on . as_deref ( ) {
Some ( h ) if ! h . is_empty ( ) = > device ( h ) . or_else ( | | if server { None } else { local . clone ( ) } ) ,
_ if ! server = > local ,
// Not heard yet (or only the phone): when the account has one other computer, that
// is it; with more, nobody guesses (the bot asks which one).
_ = > {
let mut devices = targets . iter ( ) . filter ( | t | matches! ( t . place , tools ::Place ::Device { .. } ) ) ;
match ( devices . next ( ) , devices . next ( ) ) {
( Some ( only ) , None ) = > Some ( only . name . clone ( ) ) ,
_ = > None ,
2026-11-05 08:03:34 +00:00
}
2026-11-05 06:40:01 +00:00
}
2026-11-05 08:47:28 +00:00
} ;
tools ::Roles { own : crate ::model ::own_computer_name ( & b . name ) , person , server }
}
/// The computer the person chose for this bot, by its name now (the bot's own is kept under a
/// fixed key so renaming the bot keeps it), and the chosen one when it isn't connected.
pub ( super ) fn chosen_computer ( b : & Bot , targets : & [ tools ::Target ] , roles : & tools ::Roles ) -> ( String , Option < String > ) {
let want = b . default_computer . trim ( ) ;
if want . is_empty ( ) | | want = = crate ::model ::BOT_COMPUTER_NAME | | want = = roles . own {
return ( roles . own . clone ( ) , None ) ;
2026-11-05 06:40:01 +00:00
}
2026-11-05 09:07:04 +00:00
// Unless the person chose another computer, a bot works on its own computer (in the
// cloud). 「你的電腦」 from older versions named no computer in particular: the bot's own.
2026-11-05 08:47:28 +00:00
if want = = crate ::model ::THIS_COMPUTER_NAME {
2026-11-05 09:07:04 +00:00
return ( roles . own . clone ( ) , None ) ;
2026-11-05 08:47:28 +00:00
}
if targets . iter ( ) . any ( | t | t . name = = want ) {
return ( want . to_string ( ) , None ) ;
}
( roles . own . clone ( ) , Some ( want . 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
}
2026-11-05 08:47:28 +00:00
/// What to keep in `default_computer` for a computer chosen by name.
pub ( super ) fn computer_to_keep ( b : & Bot , name : & str ) -> String {
if name = = crate ::model ::own_computer_name ( & b . name ) {
crate ::model ::BOT_COMPUTER_NAME . to_string ( )
} else {
name . to_string ( )
}
}
/// The computers to choose from for a bot (name, what it is) and the chosen one. A screen of a
/// brain asks the brain (only it knows the account's computers and which one is chosen).
pub ( super ) fn computer_choices ( & self , b : & Bot ) -> ( Vec < ( String , String ) > , String ) {
if self . mir . on {
let own = crate ::model ::own_computer_name ( & b . name ) ;
return match self . mir . computers . get ( & b . id ) {
Some ( c ) = > (
c [ " names " ] . as_array ( ) . map ( | a | a . iter ( ) . filter_map ( | x | Some ( ( x [ " name " ] . as_str ( ) ? . to_string ( ) , x [ " kind " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ) ) ) . collect ( ) ) . unwrap_or_default ( ) ,
c [ " current " ] . as_str ( ) . unwrap_or ( & own ) . to_string ( ) ,
2026-11-05 06:40:01 +00:00
) ,
2026-11-05 08:47:28 +00:00
None = > ( vec! [ ( own . clone ( ) , " own " . into ( ) ) ] , own ) ,
} ;
}
let t = self . targets_for ( b ) ;
let r = self . roles_for ( b , & t ) ;
let ( current , _ ) = Self ::chosen_computer ( b , & t , & r ) ;
( t . iter ( ) . map ( | x | ( x . name . clone ( ) , mirror_ui ::computer_kind ( x , & r ) . to_string ( ) ) ) . collect ( ) , current )
}
/// Choose the computer a bot works on (a screen sends it to the brain with its other edits).
pub ( super ) fn set_computer ( & mut self , bot_id : u64 , name : & str ) {
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
let keep = Self ::computer_to_keep ( b , name ) ;
if b . default_computer ! = keep {
b . default_computer = keep ;
self . mark ( ) ;
}
}
// Shown at once on a screen (the brain's answer follows).
if let Some ( c ) = self . mir . computers . get_mut ( & bot_id ) {
c [ " current " ] = serde_json ::json! ( name ) ;
}
}
/// 查看電腦 and the instructions: each computer by its name, what it is, in plain words.
pub ( super ) fn computer_lines ( & self , targets : & [ tools ::Target ] , roles : & tools ::Roles ) -> Vec < String > {
targets
. iter ( )
. map ( | t | {
let yours = roles . person . as_deref ( ) = = Some ( t . name . as_str ( ) ) ;
match & t . place {
tools ::Place ::Box { dir } = > {
let secrets = crate ::cloudbox ::secret_names ( ) ;
format! (
" 「{}」:你自己的 Linux 電腦( Debian, 有桌面和 Chromium; 你的資料夾是 {dir},可以 sudo 安裝套件),不是使用者的電腦,一律可用,需要時會自動喚醒。{} " ,
t . name ,
if secrets . is_empty ( ) { String ::new ( ) } else { format! ( " 使用者設好的環境密鑰(已是環境變數,值不要印出來): {} 。 " , secrets . join ( " 、 " ) ) }
)
2026-11-05 06:40:01 +00:00
}
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
tools ::Place ::Sealed { root } if tools ::sealed_runs_commands ( ) = > format! ( " 「 {} 」:你自己的電腦(獨立資料夾 {} ,看不到使用者的檔案;沒有螢幕),一律可用。 " , t . name , root . display ( ) ) ,
tools ::Place ::Sealed { root } = > format! ( " 「 {} 」: 你自己的電腦現在沒有開( AnyBot 所在的這台沒有 Docker) : 只能在你的資料夾 {} 讀寫檔案,不能執行指令,也沒有螢幕。 " , t . name , root . display ( ) ) ,
2026-11-05 08:47:28 +00:00
tools ::Place ::Local { workdir } = > format! (
" 「{}」: AnyBot 開著的這台{}(起始資料夾 {}) {}, {}。 " ,
t . name ,
if cfg! ( target_os = " macos " ) { " Mac " } else { " 電腦(快捷鍵用 Ctrl, 不是 ⌘) " } ,
workdir . display ( ) ,
if yours { " ,使用者現在就在用這台 " } else { " " } ,
if self . state . local_execution { " 已允許 " } else { " 還沒預先允許( 直接用工具, App 會問使用者) " }
) ,
tools ::Place ::Ssh { target , .. } = > format! ( " 「 {} 」:使用者的另一台電腦,透過 SSH 連到 {target} 。 " , t . name ) ,
tools ::Place ::Device { .. } = > format! (
" 「{}」:使用者帳號裡的電腦,透過它上面開著的 AnyBot 操作(檔案、指令、瀏覽器、看畫面和點擊){}。 " ,
t . name ,
if yours { " ,使用者現在就是在這台跟你說話 " } else { " " }
) ,
2026-11-05 06:40:01 +00:00
}
} )
2026-11-05 08:47:28 +00:00
. collect ( )
}
/// 查看電腦: the computers this bot really has, in plain words.
fn computers_text ( & self , b : & Bot ) -> String {
let targets = self . targets_for ( b ) ;
let roles = self . roles_for ( b , & targets ) ;
let ( chosen , missing ) = Self ::chosen_computer ( b , & targets , & roles ) ;
let mut lines = self . computer_lines ( & targets , & roles ) ;
lines . push ( format! ( " 使用者選的電腦:「 {chosen} 」 " ) ) ;
if let Some ( m ) = missing {
lines . push ( format! ( " (使用者原本選的「 {m} 」現在沒有連線:那台的 AnyBot 沒開或沒登入。) " ) ) ;
}
lines . join ( " \n " )
}
/// update_bot with a computer: the names are this bot's computers (the account's ones too),
/// which only the app knows; the rest of the change goes through appctl as usual.
fn update_bot_computer ( & mut self , bot_id : u64 , args : & str ) -> appctl ::Outcome {
let mut v : serde_json ::Value = serde_json ::from_str ( args ) . unwrap_or_default ( ) ;
let want = v [ " default_computer " ] . as_str ( ) . unwrap_or ( " " ) . trim ( ) . to_string ( ) ;
let fail = | text : String | appctl ::Outcome { text , ok : false , wake : vec ! [ ] , theme_changed : false , page : None } ;
let target = match v [ " bot " ] . as_str ( ) . map ( str ::trim ) . filter ( | n | ! n . is_empty ( ) ) {
Some ( n ) = > match self . state . bots . iter ( ) . find ( | b | b . name = = n ) {
Some ( b ) = > b . id ,
None = > return fail ( format! ( " 找不到叫「 {n} 」的 Bot " ) ) ,
} ,
None = > bot_id ,
} ;
let Some ( b ) = self . state . bot ( target ) . cloned ( ) else { return fail ( " 找不到這個 Bot " . into ( ) ) } ;
let targets = self . targets_for ( & b ) ;
let roles = self . roles_for ( & b , & targets ) ;
let name = if want = = crate ::model ::BOT_COMPUTER_NAME | | want = = crate ::model ::THIS_COMPUTER_NAME { Self ::chosen_computer ( & Bot { default_computer : want . clone ( ) , .. b . clone ( ) } , & targets , & roles ) . 0 } else { want . clone ( ) } ;
if ! targets . iter ( ) . any ( | t | t . name = = name ) {
return fail ( format! ( " 沒有「 {want} 」這台電腦。可以用: {} " , targets . iter ( ) . map ( | t | t . name . as_str ( ) ) . collect ::< Vec < _ > > ( ) . join ( " 、 " ) ) ) ;
}
if let Some ( bm ) = self . state . bot_mut ( target ) {
bm . default_computer = Self ::computer_to_keep ( & b , & name ) ;
}
self . mark ( ) ;
if let Some ( o ) = v . as_object_mut ( ) {
o . remove ( " default_computer " ) ;
}
let more = v . as_object ( ) . map ( | o | o . keys ( ) . any ( | k | k ! = " bot " ) ) . unwrap_or ( false ) ;
let note = format! ( " 已把「 {} 」的電腦改成「 {name} 」。 " , b . name ) ;
if more {
let mut out = appctl ::apply ( & mut self . state , bot_id , " update_bot " , & v . to_string ( ) ) ;
out . text = format! ( " {note} {} " , out . text ) ;
out
} else {
appctl ::Outcome { text : note , ok : true , wake : vec ! [ ] , theme_changed : false , page : None }
}
2026-11-05 06:40:01 +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
/// Record how a turn ended (success / cancelled / error) and clear its live state.
fn settle ( & mut self , bot_id : u64 , outcome : TurnOutcome ) {
// 群組聊天: the next member answers after this one finishes well.
if self . group_queue . get ( & bot_id ) . map ( | q | ! q . is_empty ( ) ) . unwrap_or ( false ) {
if outcome = = TurnOutcome ::Success {
self . group_next . push ( bot_id ) ;
} else {
self . group_queue . remove ( & bot_id ) ;
}
}
let group_continues = self . group_next . contains ( & bot_id ) ;
if ! group_continues {
self . channels_settled ( bot_id , outcome ) ;
}
if outcome = = TurnOutcome ::Success & & ! self . demo {
let in_call = self . call_state ( ) . map ( | c | c . 0 = = bot_id ) . unwrap_or ( false ) ;
if let Some ( b ) = self . state . bot ( bot_id ) . filter ( | b | b . speak_replies & & ! in_call ) {
if let Some ( m ) = b . messages . iter ( ) . rev ( ) . find ( | m | m . role = = Role ::Assistant & & ! m . text . trim ( ) . is_empty ( ) & & ! m . error ) {
crate ::voice ::speak_as ( & m . text , & b . voice , b . voice_rate ) ;
}
}
}
if outcome = = TurnOutcome ::Success {
let now = self . faces . get ( & bot_id ) . map ( | f | f . now ( ) ) . unwrap_or ( 0.0 ) ;
self . poke ( bot_id , sidebar_ui ::Poke ::Nod , now ) ;
}
let away = self . away ( ) & & self . state . notify ;
if let Some ( l ) = self . streams . remove ( & bot_id ) {
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
b . last_turn = Some ( TurnSettlement { turn_id : l . turn_id , outcome , settled_at_ms : chrono ::Utc ::now ( ) . timestamp_millis ( ) } ) ;
// 「Bot 完成任務…時通知你」: a banner when it finishes while you're away.
if outcome = = TurnOutcome ::Success & & away & & ! self . demo {
let last = b . messages . iter ( ) . rev ( ) . find ( | m | m . role = = Role ::Assistant & & ! m . text . is_empty ( ) ) . map ( | m | first_line ( & m . text , 80 ) ) . unwrap_or_default ( ) ;
if ! self . headless {
notify ::banner ( & format! ( " {} 完成了 " , b . name ) , & last ) ;
}
crate ::remote ::notify ( & format! ( " {} 完成了 " , b . name ) , & last , bot_id ) ;
}
}
// Messages that arrived too late to steer start the next turn.
let leftover : Vec < String > = std ::mem ::take ( & mut * l . steer . lock ( ) . unwrap ( ) ) ;
if ! leftover . is_empty ( ) {
self . pending_turns . push ( ( bot_id , leftover ) ) ;
}
}
self . approvals . remove ( & bot_id ) ;
self . app_pending . remove ( & bot_id ) ;
}
/// Wake a bot with a message: a new turn when idle, steering when it is already working.
fn wake ( & mut self , ctx : & egui ::Context , bot_id : u64 , msg : Message ) {
if let Some ( l ) = self . streams . get ( & bot_id ) {
l . steer . lock ( ) . unwrap ( ) . push ( msg . text ) ;
return ;
}
let selected = self . state . selected ;
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
b . messages . push ( msg ) ;
if bot_id ! = selected {
b . unread = true ;
}
}
self . start_reply ( ctx , bot_id ) ;
}
/// Run due routines (a routine firing is a "wake" turn).
fn tick_routines ( & mut self , ctx : & egui ::Context ) {
if self . demo | | self . last_tick . elapsed ( ) < std ::time ::Duration ::from_secs ( 1 ) {
return ;
}
self . last_tick = Instant ::now ( ) ;
let now = crate ::routines ::now ( ) ;
let mut fire = vec! [ ] ;
for b in self . state . bots . iter_mut ( ) {
if self . streams . contains_key ( & b . id ) {
continue ; // fires once the bot is idle
}
for r in b . routines . iter_mut ( ) . filter ( | r | ! r . paused ) {
if r . next_run . map ( | t | t < = now ) . unwrap_or ( false ) {
r . last_run = Some ( now ) ;
r . next_run = crate ::routines ::next_run ( & r . schedule , r . created_at , now ) ;
fire . push ( ( b . id , appctl ::wake_message ( r ) ) ) ;
break ;
}
}
}
for ( id , msg ) in fire {
self . wake ( ctx , id , msg ) ;
self . dirty = true ;
}
for id in std ::mem ::take ( & mut self . group_next ) {
if ! self . streams . contains_key ( & id ) {
self . start_reply ( ctx , id ) ;
2026-11-05 07:48:22 +00:00
} else {
// Still busy: keep the turn queued instead of dropping it with the take().
self . group_next . push ( id ) ;
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 ( id , texts ) in std ::mem ::take ( & mut self . pending_turns ) {
if let Some ( b ) = self . state . bot_mut ( id ) {
for t in texts {
b . messages . push ( Message ::user ( & t ) ) ;
}
}
2026-11-05 07:48:22 +00:00
if self . streams . contains_key ( & id ) {
// A turn just settled in the same tick we queued this one; starting now would
// replace the running Live and let two agent threads write one chat. Retry idle.
self . group_next . push ( id ) ;
} else {
self . start_reply ( ctx , id ) ;
}
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 run_app_action ( & mut self , ctx : & egui ::Context , bot_id : u64 , call : AppCall ) {
2026-11-05 06:40:01 +00:00
let out = if call . name = = " list_computers " {
let text = self . state . bot ( bot_id ) . cloned ( ) . map ( | b | self . computers_text ( & b ) ) . unwrap_or_default ( ) ;
appctl ::Outcome { text , ok : true , wake : vec ! [ ] , theme_changed : false , page : None }
2026-11-05 08:47:28 +00:00
} else if call . name = = " update_bot " & & serde_json ::from_str ::< serde_json ::Value > ( & call . args ) . map ( | v | v [ " default_computer " ] . is_string ( ) ) . unwrap_or ( false ) {
self . update_bot_computer ( bot_id , & call . args )
2026-11-05 06:40:01 +00:00
} else {
appctl ::apply ( & mut self . state , bot_id , & call . name , & call . args )
} ;
頁面: 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
// write_page: remember which page the call made, so its card shows under the reply.
if let Some ( pid ) = out . page {
if let Some ( c ) = self . state . bot_mut ( bot_id ) . and_then ( | b | b . messages . iter_mut ( ) . rev ( ) . take ( 12 ) . flat_map ( | m | m . tool_calls . iter_mut ( ) ) . find ( | c | c . id = = call . call_id ) ) {
let mut a : serde_json ::Value = serde_json ::from_str ( & c . args ) . unwrap_or ( serde_json ::json! ( { } ) ) ;
a [ " id " ] = serde_json ::json! ( pid ) ;
// The card shows the page itself; the long content needn't stay twice in the chat.
if let Some ( o ) = a . as_object_mut ( ) {
o . remove ( " content " ) ;
}
c . args = a . 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
if out . theme_changed {
self . apply_theme ( ctx ) ;
}
let _ = call . reply . send ( ( out . text , out . ok ) ) ;
for ( target , msg ) in out . wake {
self . wake ( ctx , target , msg ) ;
}
self . dirty = true ;
}
fn stop ( & mut self , bot_id : u64 ) {
2026-11-04 09:46:43 +00:00
if self . mir . on {
if self . streams . contains_key ( & bot_id ) | | self . approvals . contains_key ( & bot_id ) {
self . mirror_act ( serde_json ::json! ( { " bot " : bot_id , " action " : " stop " } ) ) ;
}
return ;
}
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 . approvals . remove ( & bot_id ) ;
if let Some ( call ) = self . app_pending . remove ( & bot_id ) {
let _ = call . reply . send ( ( " 已停止 " . into ( ) , false ) ) ;
}
if let Some ( live ) = self . streams . get ( & bot_id ) {
live . cancel . store ( true , Ordering ::Relaxed ) ;
// Stopping keeps what was typed but not yet read: it becomes the next turn only if sent again.
live . steer . lock ( ) . unwrap ( ) . clear ( ) ;
self . settle ( bot_id , TurnOutcome ::Cancelled ) ;
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
for c in b . messages . iter_mut ( ) . rev ( ) . take ( 6 ) . flat_map ( | m | m . tool_calls . iter_mut ( ) ) {
if matches! ( c . status , ToolStatus ::Waiting | ToolStatus ::Running ) {
c . status = ToolStatus ::Denied ;
}
}
if let Some ( m ) = b . messages . last_mut ( ) {
if m . role = = Role ::Assistant & & m . text . is_empty ( ) & & m . tool_calls . is_empty ( ) {
m . text = " (已停止) " . into ( ) ;
}
}
}
self . mark ( ) ;
}
}
/// "Restart" a stuck bot: stop its turn, approvals and background commands, and mark any
/// half-finished reply as interrupted so it can be retried. Chats and files stay.
fn restart_bot ( & mut self , bot_id : u64 ) {
self . stop ( bot_id ) ;
let stopped = tools ::stop_for ( & bot_id . to_string ( ) ) ;
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
b . repair_interrupted ( ) ;
let name = b . name . clone ( ) ;
self . notice = Some ( if stopped > 0 { format! ( " 已重新啟動「 {name} 」,也停止了 {stopped} 個背景指令。 " ) } else { format! ( " 已重新啟動「 {name} 」。 " ) } ) ;
}
self . dirty = true ;
}
/// 直接操作: one switch instead of many. Turns on this Mac, every bot's tools and auto-review,
/// starts the "always ask" rules with a few safe defaults if there are none, and tells running
/// turns to stop asking.
/// The switches as they actually apply: an admin lock (policy) wins over the saved setting.
fn direct_on ( & self ) -> bool {
self . state . direct_mode & & self . managed . direct . allows ( ) & & self . managed . local . allows ( )
}
fn local_on ( & self ) -> bool {
( self . state . local_execution | | self . state . direct_mode & & self . managed . direct . allows ( ) ) & & self . managed . local . allows ( )
}
fn enable_direct_mode ( & mut self ) {
if ! self . managed . direct . allows ( ) | | ! self . managed . local . allows ( ) {
self . notice = Some ( " 「直接操作」已由管理員關閉,沒辦法打開。 " . into ( ) ) ;
return ;
}
let st = & mut self . state ;
st . direct_mode = true ;
st . local_execution = true ;
st . auto_review = true ;
for b in st . bots . iter_mut ( ) {
b . tools_enabled = true ;
}
if st . review_ask_rules . is_empty ( ) {
st . review_ask_rules = crate ::model ::DEFAULT_ASK_RULES . iter ( ) . map ( | s | s . to_string ( ) ) . collect ( ) ;
}
for l in self . streams . values ( ) {
l . direct . store ( true , Ordering ::Relaxed ) ;
}
// Other bots waiting on a "may I use the computer?" question can go on too.
let waiting : Vec < u64 > = self . approvals . iter ( ) . filter ( | ( _ , a ) | a . kind ! = ApprovalKind ::Action ) . map ( | ( id , _ ) | * id ) . collect ( ) ;
for id in waiting {
if let Some ( a ) = self . approvals . remove ( & id ) {
let _ = a . reply . send ( ApprovalAnswer ::Always ( a . grant . clone ( ) ) ) ;
}
}
self . notice = Some ( " 已打開「直接操作」: Bot 會直接做事,只有危險操作和你的「一律詢問」規則會停下來問你。 " . into ( ) ) ;
self . dirty = true ;
}
fn disable_direct_mode ( & mut self ) {
self . state . direct_mode = false ;
for l in self . streams . values ( ) {
l . direct . store ( false , Ordering ::Relaxed ) ;
}
self . dirty = true ;
}
fn answer ( & mut self , ctx : & egui ::Context , bot_id : u64 , answer : ApprovalAnswer ) {
2026-11-04 09:46:43 +00:00
if self . mir . on {
let a = match answer {
ApprovalAnswer ::Once = > " once " ,
ApprovalAnswer ::Always ( _ ) = > " always " ,
ApprovalAnswer ::Reject = > " reject " ,
} ;
self . approvals . remove ( & bot_id ) ;
self . popup_hidden . remove ( & bot_id ) ;
self . mirror_act ( serde_json ::json! ( { " bot " : bot_id , " action " : " answer " , " answer " : a } ) ) ;
return ;
}
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 . want_direct . replace ( false ) {
self . enable_direct_mode ( ) ;
}
if let Some ( call ) = self . app_pending . remove ( & bot_id ) {
self . approvals . remove ( & bot_id ) ;
if answer = = ApprovalAnswer ::Reject {
let _ = call . reply . send ( ( " 使用者拒絕了這個操作。 " . into ( ) , false ) ) ;
} else {
self . run_app_action ( ctx , bot_id , call ) ;
}
self . mark ( ) ;
return ;
}
if let Some ( a ) = self . approvals . remove ( & bot_id ) {
self . popup_hidden . remove ( & bot_id ) ;
if let ApprovalAnswer ::Always ( g ) = & answer {
match a . kind {
// "Always" on these turns the setting on, so it won't ask again.
ApprovalKind ::EnableTools = > {
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
b . tools_enabled = true ;
}
}
ApprovalKind ::EnableThisComputer = > self . state . local_execution = true ,
2026-11-05 09:29:55 +00:00
ApprovalKind ::Handoff = > { }
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
ApprovalKind ::Action = > {
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
if ! g . is_empty ( ) & & ! b . allow_rules . contains ( g ) {
b . allow_rules . push ( g . clone ( ) ) ;
}
}
}
}
}
let _ = a . reply . send ( answer ) ;
if let Some ( l ) = self . streams . get_mut ( & bot_id ) {
l . paused = false ;
l . last = Instant ::now ( ) ;
}
self . mark ( ) ;
}
}
fn regenerate ( & mut self , ctx : & egui ::Context , bot_id : u64 ) {
2026-11-04 09:46:43 +00:00
if self . mir . on {
self . mirror_act ( serde_json ::json! ( { " bot " : bot_id , " action " : " regenerate " } ) ) ;
return ;
}
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 . streams . contains_key ( & bot_id ) {
return ;
}
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
while b . messages . last ( ) . map ( | m | m . role ! = Role ::User ) . unwrap_or ( false ) {
b . messages . pop ( ) ;
}
if b . messages . is_empty ( ) {
return ;
}
}
self . start_reply ( ctx , bot_id ) ;
}
fn drain_events ( & mut self , ctx : & egui ::Context ) {
while let Ok ( ( id , ev ) ) = self . rx . try_recv ( ) {
2026-11-06 15:12:53 +00:00
// 自己進化 arrives after the turn has ended: keep the skill, say so under the reply.
if let StreamEvent ::Learned ( k , reply_at ) = ev {
let name = k . name . clone ( ) ;
let fresh = crate ::memory ::upsert_skill ( & mut self . state . skills , k ) ;
if let Some ( m ) = self . state . bot_mut ( id ) . and_then ( | b | b . messages . iter_mut ( ) . rev ( ) . find ( | m | m . role = = Role ::Assistant & & m . at = = reply_at ) ) {
m . text . push_str ( & crate ::memory ::learned_note ( & name , fresh ) ) ;
}
self . dirty = true ;
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
// A stopped request may still deliver a few pieces; ignore them.
if ! self . streams . contains_key ( & id ) {
continue ;
}
let selected = self . state . selected ;
if let Some ( live ) = self . streams . get_mut ( & id ) {
live . last = Instant ::now ( ) ;
match & ev {
StreamEvent ::Text ( _ ) | StreamEvent ::Thinking ( _ ) | StreamEvent ::ToolCall ( _ ) = > {
live . produced = true ;
live . status = None ;
}
StreamEvent ::Retrying ( msg ) = > {
live . status = Some ( msg . clone ( ) ) ;
live . started = Instant ::now ( ) ;
}
// A new model step gets the first-response deadline again.
StreamEvent ::NextStep ( _ ) = > {
live . produced = false ;
live . started = Instant ::now ( ) ;
live . status = None ;
}
// Waiting for the person or a running tool is not a stall.
StreamEvent ::NeedApproval ( _ ) = > live . paused = true ,
StreamEvent ::ToolStatus ( _ , st ) = > live . paused = matches! ( st , ToolStatus ::Running | ToolStatus ::Waiting ) ,
StreamEvent ::ToolResult ( _ ) = > live . paused = false ,
_ = > { }
}
}
if let StreamEvent ::NeedApproval ( a ) = ev {
if let Some ( b ) = self . state . bot_mut ( id ) {
if id ! = selected {
b . unread = true ;
}
if let Some ( c ) = b . messages . iter_mut ( ) . rev ( ) . flat_map ( | m | m . tool_calls . iter_mut ( ) ) . find ( | c | c . id = = a . call_id ) {
c . status = ToolStatus ::Waiting ;
}
}
self . approvals . insert ( id , a ) ;
self . on_new_approval ( ctx , id ) ;
continue ;
}
if let StreamEvent ::React ( emoji ) = & ev {
if let Some ( b ) = self . state . bot_mut ( id ) {
if let Some ( i ) = b . messages . iter ( ) . rposition ( | m | m . role = = Role ::User & & ! m . wake ) {
let m = & mut b . messages [ i ] ;
if ! m . reactions . contains ( emoji ) {
m . reactions . push ( emoji . clone ( ) ) ;
self . popped . insert ( ( id , i , emoji . clone ( ) ) , Instant ::now ( ) ) ;
}
}
}
self . dirty = true ;
continue ;
}
if let StreamEvent ::Attach ( a ) = ev {
if let Some ( b ) = self . state . bot_mut ( id ) {
if let Some ( m ) = b . messages . iter_mut ( ) . rev ( ) . find ( | m | m . role = = Role ::Assistant ) {
m . attachments . push ( a ) ;
}
}
self . dirty = true ;
continue ;
}
if let StreamEvent ::AppAction ( call ) = ev {
match appctl ::needs_approval ( & call . name , & call . args , & self . state ) {
Some ( reason ) = > {
let ( reply , _ ) = channel ( ) ;
self . approvals . insert ( id , Approval { call_id : call . call_id . clone ( ) , tool : call . name . clone ( ) , reason , grant : String ::new ( ) , reply , kind : ApprovalKind ::Action } ) ;
self . on_new_approval ( ctx , id ) ;
self . app_pending . insert ( id , call ) ;
if let Some ( l ) = self . streams . get_mut ( & id ) {
l . paused = true ;
}
}
None = > self . run_app_action ( ctx , id , call ) ,
}
continue ;
}
let mut settle_with = None ;
let Some ( b ) = self . state . bot_mut ( id ) else { continue } ;
match ev {
StreamEvent ::Alive | StreamEvent ::Retrying ( _ ) | StreamEvent ::NeedApproval ( _ ) | StreamEvent ::AppAction ( _ ) | StreamEvent ::React ( _ ) | StreamEvent ::Attach ( _ ) = > continue ,
StreamEvent ::Steered ( m ) = > b . messages . push ( m ) ,
StreamEvent ::NextStep ( via ) = > b . messages . push ( Message ::assistant ( " " , & via ) ) ,
StreamEvent ::ToolResult ( m ) = > b . messages . push ( m ) ,
2026-11-05 07:48:22 +00:00
StreamEvent ::Compacted ( n , summary , boundary ) = > {
// The model's history can be shorter than the window's chat (group chats
// filter); land the cut at the real first-kept message, not the raw index.
let cut = b . messages . iter ( ) . position ( | m | m . at = = boundary & & m . role = = Role ::User ) . unwrap_or ( n . min ( b . messages . len ( ) ) ) ;
b . messages . splice ( .. cut , [ summary ] ) ;
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
}
StreamEvent ::ToolStatus ( call_id , st ) = > {
if let Some ( c ) = b . messages . iter_mut ( ) . rev ( ) . flat_map ( | m | m . tool_calls . iter_mut ( ) ) . find ( | c | c . id = = call_id ) {
c . status = st ;
}
2026-11-05 09:29:55 +00:00
// The step moved on without the buttons (a message answered a hand-off).
if st ! = ToolStatus ::Waiting & & self . approvals . get ( & id ) . map ( | a | a . call_id = = call_id ) . unwrap_or ( false ) {
self . approvals . remove ( & id ) ;
}
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
}
other = > {
let Some ( m ) = b . messages . last_mut ( ) else { continue } ;
match other {
2026-11-05 06:46:42 +00:00
StreamEvent ::Text ( t ) = > m . text . push_str ( & crate ::i18n ::reply_text ( & t ) ) ,
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
StreamEvent ::Thinking ( t ) = > m . thinking . push_str ( & t ) ,
StreamEvent ::ToolCall ( c ) = > m . tool_calls . push ( c ) ,
StreamEvent ::StepDone ( full ) = > {
if m . role = = Role ::Assistant & & ! m . summary {
// Keep statuses the stream already set.
let mut full = full ;
for c in full . tool_calls . iter_mut ( ) {
if let Some ( old ) = m . tool_calls . iter ( ) . find ( | o | o . id = = c . id ) {
c . status = old . status ;
}
}
* m = full ;
}
}
StreamEvent ::Done = > {
if m . role = = Role ::Assistant & & m . text . is_empty ( ) & & m . tool_calls . is_empty ( ) {
m . text = " (沒有回應內容) " . into ( ) ;
}
if id ! = selected {
b . unread = true ;
}
settle_with = Some ( TurnOutcome ::Success ) ;
}
StreamEvent ::Error ( e ) = > {
if m . role ! = Role ::Assistant | | ! m . tool_calls . is_empty ( ) {
b . messages . push ( Message { error : true , .. Message ::assistant ( & e , " " ) } ) ;
} else {
if m . text . is_empty ( ) {
m . text = e ;
} else {
m . text . push_str ( & format! ( " \n \n {e} " ) ) ;
}
m . error = true ;
}
settle_with = Some ( TurnOutcome ::Error ) ;
}
_ = > { }
}
}
}
if let Some ( o ) = settle_with {
self . settle ( id , o ) ;
}
self . dirty = true ;
}
}
/// Stall rules: SAND-E0402 firstTokenStall (deadline is server-side, estimated here)
/// and SAND-E0415 streamIdleStall (35 s of silence after output started).
fn watchdog ( & mut self ) {
let first = std ::time ::Duration ::from_secs ( providers ::FIRST_RESPONSE_TIMEOUT_SECS ) ;
let idle = std ::time ::Duration ::from_millis ( providers ::STREAM_IDLE_STALL_MS ) ;
let stalled : Vec < ( u64 , & str ) > = self
. streams
. iter ( )
. filter_map ( | ( id , l ) | {
if l . paused {
None
} else if ! l . produced & & l . started . elapsed ( ) > first {
Some ( ( * id , " 模型未能及時開始回應。請重試。 " ) )
} else if l . produced & & l . last . elapsed ( ) > idle {
Some ( ( * id , " 回應中斷:模型超過 35 秒沒有送出任何內容。 " ) )
} else {
None
}
} )
. collect ( ) ;
for ( id , msg ) in stalled {
if let Some ( l ) = self . streams . get ( & id ) {
l . cancel . store ( true , Ordering ::Relaxed ) ;
}
self . settle ( id , TurnOutcome ::Error ) ;
if let Some ( m ) = self . state . bot_mut ( id ) . and_then ( | b | b . messages . last_mut ( ) ) . filter ( | m | m . role = = Role ::Assistant ) {
if m . text . is_empty ( ) {
m . text = msg . into ( ) ;
} else {
m . text . push_str ( & format! ( " \n \n {msg} " ) ) ;
}
m . error = true ;
}
self . dirty = true ;
}
}
fn fetch_models ( & mut self , kind : ProviderKind ) {
2026-11-04 11:30:32 +00:00
// A screen of a brain: the brain has the key, so it asks.
if self . mir . on {
let ( tx , rx ) = channel ( ) ;
let s = self . state . remote . clone ( ) ;
std ::thread ::spawn ( move | | {
let name = serde_json ::to_value ( kind ) . unwrap_or_default ( ) ;
let r = crate ::remote ::sync_post ( & s , " models " , & serde_json ::json! ( { " provider " : name } ) )
. and_then ( | v | v [ " error " ] . as_str ( ) . map ( | e | Err ( e . to_string ( ) ) ) . unwrap_or_else ( | | Ok ( v [ " models " ] . as_array ( ) . into_iter ( ) . flatten ( ) . filter_map ( | m | m . as_str ( ) . map ( String ::from ) ) . collect ( ) ) ) ) ;
let _ = tx . send ( r ) ;
} ) ;
self . model_fetch . insert ( kind , rx ) ;
self . fetch_status . insert ( kind , " 正在請大腦取得模型清單… " . into ( ) ) ;
return ;
}
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 ps = self . state . provider ( kind ) ;
let base = providers ::resolve_base ( kind , & ps . base_url ) ;
let key = providers ::resolve_key ( kind , & ps . api_key ) ;
let ( tx , rx ) = channel ( ) ;
std ::thread ::spawn ( move | | {
let _ = tx . send ( providers ::list_models ( kind , & base , & key ) ) ;
} ) ;
self . model_fetch . insert ( kind , rx ) ;
self . fetch_status . insert ( kind , " 正在取得模型清單… " . into ( ) ) ;
}
fn poll_fetches ( & mut self ) {
let mut done = vec! [ ] ;
for ( k , rx ) in & self . model_fetch {
if let Ok ( r ) = rx . try_recv ( ) {
done . push ( ( * k , r ) ) ;
}
}
for ( k , r ) in done {
self . model_fetch . remove ( & k ) ;
match r {
Ok ( list ) = > {
self . fetch_status . insert ( k , format! ( " 連線成功,找到 {} 個模型 " , list . len ( ) ) ) ;
self . state . provider_mut ( k ) . known_models = list ;
self . mark ( ) ;
}
Err ( e ) = > {
self . fetch_status . insert ( k , e ) ;
}
}
}
}
2026-11-04 13:52:06 +00:00
/// Models to describe pictures with, best first: each provider with a key, its own default
/// model, then models of it known to see pictures.
fn eyes ( & self ) -> Vec < providers ::Eyes > {
use ProviderKind as K ;
let mut v = vec! [ ] ;
for kind in [ K ::Xai , K ::Anthropic , K ::OpenAi , K ::Gemini , K ::OpenRouter , K ::OpenCodeGo , K ::Custom , K ::Ollama ] {
let ps = self . state . provider ( kind ) ;
let key = providers ::resolve_key ( kind , & ps . api_key ) ;
if kind . needs_key ( ) & & key . is_empty ( ) {
continue ;
}
if matches! ( kind , K ::Custom | K ::Ollama ) & & ps . base_url . trim ( ) . is_empty ( ) {
continue ;
}
let base = providers ::resolve_base ( kind , & ps . base_url ) ;
let mut models : Vec < String > = vec! [ ] ;
if ! ps . default_model . trim ( ) . is_empty ( ) {
models . push ( ps . default_model . trim ( ) . to_string ( ) ) ;
}
models . extend ( providers ::seeing_models ( kind ) . iter ( ) . map ( | m | m . to_string ( ) ) ) ;
models . dedup ( ) ;
for m in models {
v . push ( providers ::Eyes { kind , base_url : base . clone ( ) , api_key : key . clone ( ) , model : m } ) ;
}
}
v
}
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 models_for ( & self , kind : ProviderKind ) -> Vec < String > {
let ps = self . state . provider ( kind ) ;
let mut v : Vec < String > = if ps . known_models . is_empty ( ) {
kind . starter_models ( ) . iter ( ) . map ( | s | s . to_string ( ) ) . collect ( )
} else {
ps . known_models . clone ( )
} ;
if ! ps . default_model . is_empty ( ) & & ! v . contains ( & ps . default_model ) {
v . insert ( 0 , ps . default_model . clone ( ) ) ;
}
v
}
fn import_bot_dialog ( & mut self ) {
let ( tx , rx ) = channel ( ) ;
self . share_rx = Some ( rx ) ;
std ::thread ::spawn ( move | | {
if let Some ( p ) = crate ::share ::choose_open ( ) {
let _ = tx . send ( std ::fs ::read_to_string ( & p ) . map_err ( | e | e . to_string ( ) ) ) ;
}
} ) ;
}
fn poll_import ( & mut self ) {
let Some ( rx ) = & self . share_rx else { return } ;
let Ok ( r ) = rx . try_recv ( ) else { return } ;
self . share_rx = None ;
let id = self . state . next_id ;
let mut next = self . state . next_routine_id ;
match r . and_then ( | t | crate ::share ::import ( & t , id , & mut next , crate ::routines ::now ( ) ) ) {
Ok ( b ) = > {
let paused = b . routines . iter ( ) . filter ( | r | r . trigger . is_some ( ) ) . count ( ) ;
self . state . next_id + = 1 ;
self . state . next_routine_id = next ;
self . notice = Some ( format! ( " 已匯入「 {} 」。 {} " , b . name , if paused > 0 { format! ( " 有 {paused} 個事件觸發的例行任務先暫停了:請跟 Bot 說要用的 Token, 再請它繼續。 " ) } else { String ::new ( ) } ) ) ;
self . state . bots . push ( b ) ;
self . state . selected = id ;
self . mark ( ) ;
}
Err ( e ) = > self . notice = Some ( format! ( " 匯入失敗: {e} " ) ) ,
}
}
fn new_bot ( & mut self ) {
let id = self . state . next_id ;
let shape = avatar ::SHAPES [ ( id as usize ) % avatar ::SHAPES . len ( ) ] ;
let color = avatar ::COLORS [ ( id as usize ) % avatar ::COLORS . len ( ) ] . 0 ;
let base = self . state . bot ( self . state . selected ) . map ( | b | ( b . provider , b . model . clone ( ) ) ) ;
2026-11-05 06:25:01 +00:00
// The first bot: the service whose key was just set.
let first = self . ready_provider ( ) . unwrap_or ( ProviderKind ::Xai ) ;
let ( provider , model ) = base . unwrap_or_else ( | | ( first , self . default_model ( first ) ) ) ;
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 b = Bot {
id ,
name : " 新 Bot " . into ( ) ,
description : String ::new ( ) ,
instructions : String ::new ( ) ,
shape : shape . into ( ) ,
color : color . into ( ) ,
provider ,
model ,
pinned : false ,
messages : vec ! [ ] ,
unread : false ,
tools_enabled : false ,
workdir : crate ::model ::default_workdir ( ) ,
allow_rules : vec ! [ ] ,
demos : vec ! [ ] ,
speak_replies : false ,
voice : String ::new ( ) ,
voice_rate : 1.0 ,
voice_lang : String ::new ( ) ,
folder : String ::new ( ) ,
memories : vec ! [ ] ,
members : vec ! [ ] ,
threads : vec ! [ ] ,
context_tokens : agent ::DEFAULT_CONTEXT_TOKENS ,
routines : vec ! [ ] ,
last_turn : None ,
default_computer : crate ::model ::default_computer ( ) ,
} ;
self . edit_bot = Some ( ( b , true ) ) ;
}
// ---------- sidebar ----------
/// Apply the original's drag rule (`QJe`) to a dragged width.
fn resize_sidebar ( & mut self , raw : f32 ) {
let st = & mut self . state ;
if st . sidebar_collapsed {
if raw > = SIDEBAR_EXPAND_FROM {
st . sidebar_collapsed = false ;
st . sidebar_width = raw . clamp ( SIDEBAR_MIN_WIDTH , SIDEBAR_MAX_WIDTH ) ;
}
} else if raw < SIDEBAR_COLLAPSE_BELOW {
st . sidebar_collapsed = true ;
} else {
st . sidebar_width = raw . clamp ( SIDEBAR_MIN_WIDTH , SIDEBAR_MAX_WIDTH ) ;
}
self . dirty = true ;
}
fn sidebar ( & mut self , root : & mut egui ::Ui ) {
let p = self . pal ;
let collapsed = self . state . sidebar_collapsed ;
let width = if collapsed { SIDEBAR_RAIL_WIDTH } else { self . state . sidebar_width . clamp ( SIDEBAR_MIN_WIDTH , SIDEBAR_MAX_WIDTH ) } ;
let panel = egui ::Panel ::left ( " sidebar " )
. exact_size ( width )
. resizable ( false )
. show_separator_line ( false )
. frame ( Frame ::new ( ) . fill ( p . sidebar ) . inner_margin ( Margin ::symmetric ( if collapsed { 8 } else { 10 } , 12 ) ) )
. show ( root , | ui | {
if collapsed {
self . rail ( ui ) ;
return ;
}
self . sidebar_body ( ui ) ;
} ) ;
// Drag the right edge to resize; narrow it far enough and it folds into the rail.
let edge = panel . response . rect ;
let handle = egui ::Rect ::from_min_max ( egui ::pos2 ( edge . right ( ) - 3.0 , edge . top ( ) ) , egui ::pos2 ( edge . right ( ) + 3.0 , edge . bottom ( ) ) ) ;
let r = root . interact ( handle , egui ::Id ::unique ( " sidebar-resize " ) , egui ::Sense ::drag ( ) ) . on_hover_cursor ( egui ::CursorIcon ::ResizeHorizontal ) ;
if r . drag_started ( ) {
self . side_drag = Some ( width ) ;
}
if let Some ( raw ) = self . side_drag . as_mut ( ) {
* raw + = r . drag_delta ( ) . x ;
let raw = * raw ;
self . resize_sidebar ( raw ) ;
}
if r . drag_stopped ( ) {
self . side_drag = None ;
}
if r . hovered ( ) | | r . dragged ( ) {
root . painter ( ) . vline ( edge . right ( ) , edge . y_range ( ) , Stroke ::new ( 2.0 , p . border ) ) ;
}
}
/// The folded sidebar: an 88 px rail of bot avatars.
fn rail ( & mut self , ui : & mut egui ::Ui ) {
let p = self . pal ;
ui . vertical_centered ( | ui | {
2026-11-05 09:29:55 +00:00
// (The Mac's window buttons sit in the top corner.)
if cfg! ( target_os = " macos " ) {
ui . add_space ( TITLEBAR_H - 6.0 ) ;
}
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 icon_button ( ui , " ◨ " , " 展開側邊欄 " , p ) . clicked ( ) {
self . state . sidebar_collapsed = false ;
self . mark ( ) ;
}
if icon_button ( ui , " + " , " 新 Bot " , p ) . clicked ( ) {
self . new_bot ( ) ;
}
ui . add_space ( 8.0 ) ;
let mut order : Vec < u64 > = vec! [ self . state . primary_bot ] ;
order . extend ( self . pinned_ids ( ) ) ;
order . extend ( self . state . bots . iter ( ) . filter ( | b | ! b . pinned & & b . id ! = self . state . primary_bot ) . map ( | b | b . id ) ) ;
egui ::ScrollArea ::vertical ( ) . auto_shrink ( [ false , false ] ) . max_height ( ui . available_height ( ) - 40.0 ) . show ( ui , | ui | {
for id in order {
let Some ( b ) = self . state . bot ( id ) . cloned ( ) else { continue } ;
let ( rect , resp ) = ui . allocate_exact_size ( vec2 ( 56.0 , 48.0 ) , egui ::Sense ::click ( ) ) ;
if self . state . selected = = id | | resp . hovered ( ) {
ui . painter ( ) . rect_filled ( rect , 10 , if self . state . selected = = id { p . active } else { p . hover } ) ;
}
avatar ::paint ( ui . painter ( ) , egui ::Rect ::from_center_size ( rect . center ( ) , vec2 ( 32.0 , 32.0 ) ) , & b . shape , self . bot_color ( & b ) ) ;
if b . unread | | self . streams . contains_key ( & id ) {
ui . painter ( ) . circle_filled ( rect . right_top ( ) + vec2 ( - 8.0 , 8.0 ) , 4.0 , if b . unread { p . accent } else { p . text3 } ) ;
}
let resp = resp . on_hover_text ( & b . name ) ;
if resp . clicked ( ) {
self . state . selected = id ;
if let Some ( b ) = self . state . bot_mut ( id ) {
b . unread = false ;
}
self . mark ( ) ;
}
resp . context_menu ( | ui | self . bot_menu_items ( ui , id ) ) ;
}
} ) ;
ui . with_layout ( Layout ::bottom_up ( Align ::Center ) , | ui | {
if icon_button ( ui , " ⚙ " , " 設定 " , p ) . clicked ( ) {
self . show_settings = true ;
}
} ) ;
} ) ;
}
/// A bot's menu (right-click, and ⋯ at the top), grouped like the original's.
fn bot_menu_items ( & mut self , ui : & mut egui ::Ui , id : u64 ) {
use crate ::icons ::Icon ;
let p = self . pal ;
let Some ( b ) = self . state . bot ( id ) . cloned ( ) else { return } ;
ui . set_min_width ( menu_ui ::MENU_W ) ;
if id ! = self . state . primary_bot & & menu_ui ::item ( ui , p , Icon ::Pin , if b . pinned { " 取消釘選 " } else { " 釘選 " } ) . clicked ( ) {
self . set_pinned ( id , ! b . pinned , None ) ;
ui . close ( ) ;
}
self . folder_menu ( ui , id ) ;
if menu_ui ::item ( ui , p , Icon ::Bell , if b . unread { " 標示為已讀 " } else { " 標示為未讀 " } ) . clicked ( ) {
if let Some ( b ) = self . state . bot_mut ( id ) {
b . unread = ! b . unread ;
}
self . mark ( ) ;
ui . close ( ) ;
}
if id ! = self . state . primary_bot & & menu_ui ::item ( ui , p , Icon ::Star , " 設為主要 Bot " ) . clicked ( ) {
self . state . primary_bot = id ;
self . mark ( ) ;
ui . close ( ) ;
}
menu_ui ::sep ( ui , p ) ;
if menu_ui ::item ( ui , p , Icon ::Pencil , " 編輯 Bot… " ) . clicked ( ) {
self . edit_bot = Some ( ( b . clone ( ) , false ) ) ;
ui . close ( ) ;
}
// 討論串: put this conversation aside and start fresh; earlier ones can come back.
if menu_ui ::item ( ui , p , Icon ::NewChat , " 開始新對話 " ) . clicked ( ) {
self . stop ( id ) ;
2026-11-04 09:46:43 +00:00
if self . mir . on {
self . mirror_act ( serde_json ::json! ( { " bot " : id , " action " : " new " } ) ) ;
} else if let Some ( b ) = self . state . bot_mut ( id ) {
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 ! 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 ( ) ;
ui . close ( ) ;
}
let threads : Vec < String > = b . threads . iter ( ) . map ( | t | t . 0. clone ( ) ) . collect ( ) ;
if ! threads . is_empty ( ) {
menu_ui ::sub ( ui , p , Icon ::History , " 過去的對話 " , | ui | {
for ( i , title ) in threads . iter ( ) . enumerate ( ) {
if menu_ui ::item_text ( ui , p , None , title ) . clicked ( ) {
self . stop ( id ) ;
2026-11-04 09:46:43 +00:00
if self . mir . on {
self . mirror_act ( serde_json ::json! ( { " bot " : id , " action " : " restore " , " index " : i } ) ) ;
} else if let Some ( b ) = self . state . bot_mut ( id ) {
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 ( _ , msgs ) = b . threads . remove ( i ) ;
if ! b . messages . is_empty ( ) {
let cur_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 , ( cur_title , cur ) ) ;
}
b . messages = msgs ;
}
self . mark ( ) ;
ui . close ( ) ;
}
}
} ) ;
}
if menu_ui ::item ( ui , p , Icon ::Record , " 錄會議 " ) . on_hover_text ( t ( " 用麥克風錄下會議,結束後轉成文字記錄交給這個 Bot 整理 " ) ) . clicked ( ) {
let ctx = ui . ctx ( ) . clone ( ) ;
self . state . selected = id ;
self . start_meeting ( & ctx , id ) ;
ui . close ( ) ;
}
menu_ui ::sep ( ui , p ) ;
// 分享: as a file (look, model, instructions, routines, demonstrations; no chat or keys).
menu_ui ::sub ( ui , p , Icon ::Share , " 分享與匯出 " , | ui | {
if menu_ui ::item ( ui , p , Icon ::Share , " 匯出 Bot( 分享給別人) … " ) . clicked ( ) {
if let Some ( b ) = self . state . bot ( id ) {
let ( text , name ) = ( crate ::share ::export ( b ) , format! ( " {} .anybot " , b . name ) ) ;
let note = self . ch . note . clone ( ) ;
std ::thread ::spawn ( move | | {
if let Some ( path ) = crate ::share ::choose_save ( & name ) {
let path = if path . extension ( ) . is_none ( ) { path . with_extension ( " anybot " ) } else { path } ;
* note . lock ( ) . unwrap ( ) = match std ::fs ::write ( & path , text ) {
Ok ( ( ) ) = > format! ( " 已匯出到 {} " , path . display ( ) ) ,
Err ( e ) = > format! ( " 匯出失敗: {e} " ) ,
} ;
}
} ) ;
self . notice = Some ( t ( " 選好位置後就會存成 .anybot 檔,可以傳給別人匯入。聊天記錄、金鑰和 Token 不會帶出去。 " ) ) ;
}
ui . close ( ) ;
}
if menu_ui ::item ( ui , p , Icon ::Download , " 匯出文字記錄… " ) . clicked ( ) {
if let Some ( b ) = self . state . bot ( id ) {
let ( md , name ) = ( transcript_markdown ( b ) , format! ( " {} 對話.md " , b . name ) ) ;
let note = self . ch . note . clone ( ) ;
std ::thread ::spawn ( move | | {
if let Some ( path ) = crate ::share ::choose_save ( & name ) {
* note . lock ( ) . unwrap ( ) = match std ::fs ::write ( & path , md ) {
Ok ( ( ) ) = > format! ( " 已匯出到 {} " , path . display ( ) ) ,
Err ( e ) = > format! ( " 匯出失敗: {e} " ) ,
} ;
}
} ) ;
}
ui . close ( ) ;
}
if menu_ui ::item ( ui , p , Icon ::Upload , " 匯入 Bot… " ) . clicked ( ) {
self . import_bot_dialog ( ) ;
ui . close ( ) ;
}
if menu_ui ::item ( ui , p , Icon ::Copy , " 複製對話 ID " ) . clicked ( ) {
ui . ctx ( ) . copy_text ( format! ( " {id} " ) ) ;
ui . close ( ) ;
}
} ) ;
menu_ui ::sep ( ui , p ) ;
if menu_ui ::item ( ui , p , Icon ::Refresh , " 重新啟動這個 Bot " ) . on_hover_text ( t ( " 卡住時用:停止它目前的工作和背景指令,聊天記錄和檔案都會保留 " ) ) . clicked ( ) {
self . restart_bot ( id ) ;
ui . close ( ) ;
}
if menu_ui ::item ( ui , p , Icon ::Eraser , " 清除聊天記錄 " ) . clicked ( ) {
2026-11-05 08:47:28 +00:00
if self . mir . on {
// The chat lives on the brain: clearing only this copy left the brain sure this
// screen still had it, and its next changes landed in the wrong place.
self . mirror_act ( serde_json ::json! ( { " bot " : id , " action " : " clear " } ) ) ;
} else {
self . stop ( id ) ;
if let Some ( b ) = self . state . bot_mut ( id ) {
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
}
ui . close ( ) ;
}
if menu_ui ::danger ( ui , p , Icon ::Trash , " 刪除 " ) . clicked ( ) {
self . confirm_delete = Some ( id ) ;
ui . close ( ) ;
}
}
// ---------- main pane ----------
fn main_pane ( & mut self , root : & mut egui ::Ui ) {
let p = self . pal ;
let ctx = & root . ctx ( ) . clone ( ) ;
if let Some ( bot ) = self . state . bot ( self . state . selected ) . cloned ( ) {
self . bot_panel ( root , & bot ) ;
}
egui ::CentralPanel ::default ( ) . frame ( Frame ::new ( ) . fill ( p . pane ) ) . show ( root , | ui | {
let id = self . state . selected ;
2026-11-05 06:25:01 +00:00
if self . state . bots . is_empty ( ) {
self . mirror_banner ( ui ) ;
self . welcome ( ui ) ;
return ;
}
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 Some ( bot ) = self . state . bot ( id ) . cloned ( ) else {
ui . centered_and_justified ( | ui | ui . label ( RichText ::new ( t ( " 未選取 Bot " ) ) . color ( p . text3 ) ) ) ;
return ;
} ;
self . header ( ui , & bot ) ;
2026-11-04 09:46:43 +00:00
self . mirror_banner ( ui ) ;
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
// The composer grows with a long draft: keep room for last frame's height.
let h_id = egui ::Id ::unique ( " composer-height " ) ;
let composer_h : f32 = ui . data ( | d | d . get_temp ( h_id ) ) . unwrap_or ( 116.0 ) ;
let avail = ui . available_height ( ) - composer_h ;
let w = ui . available_width ( ) ;
let col_w = ( w - 48.0 ) . min ( TRANSCRIPT_MAX_WIDTH ) ;
let pad = ( ( w - col_w ) / 2.0 ) . max ( 0.0 ) ;
egui ::ScrollArea ::vertical ( )
. id_salt ( ( " transcript " , id ) )
. auto_shrink ( [ false , false ] )
. max_height ( avail )
. stick_to_bottom ( self . stick_bottom )
. show ( ui , | ui | {
ui . horizontal ( | ui | {
ui . add_space ( pad ) ;
ui . vertical ( | ui | {
ui . set_width ( col_w ) ;
ui . add_space ( 18.0 ) ;
if bot . messages . is_empty ( ) {
self . empty_state ( ui , & bot ) ;
}
let n = bot . messages . len ( ) ;
for ( i , m ) in bot . messages . iter ( ) . enumerate ( ) {
let last = i + 1 = = n ;
self . msg_index = i ;
self . chat_message ( ui , ctx , & bot , m , last ) ;
}
// While it works: the ring and the shimmering phrase.
self . working_row ( ui , & bot ) ;
self . approval_card ( ui , ctx , & bot ) ;
ui . add_space ( 16.0 ) ;
} ) ;
} ) ;
} ) ;
let top = ui . cursor ( ) . top ( ) ;
ui . horizontal ( | ui | {
ui . add_space ( pad ) ;
ui . vertical ( | ui | {
ui . set_width ( col_w ) ;
self . composer ( ui , ctx , & bot ) ;
} ) ;
} ) ;
let used = ui . cursor ( ) . top ( ) - top ;
if ( used - composer_h ) . abs ( ) > 0.5 {
ui . data_mut ( | d | d . insert_temp ( h_id , used ) ) ;
ui . ctx ( ) . request_repaint ( ) ;
}
} ) ;
}
/// The top of the chat, like the original: no bar — the bot's name in a rounded pill in the
/// middle (it opens 對話詳細資訊), and 分享 on the right (pills: 1 px border at 6 %, 36– 41 px).
fn header ( & mut self , ui : & mut egui ::Ui , bot : & Bot ) {
use panel_ui ::PanelTab ;
let p = self . pal ;
let color = self . bot_color ( bot ) ;
let ( bar , _ ) = ui . allocate_exact_size ( vec2 ( ui . available_width ( ) , 58.0 ) , egui ::Sense ::hover ( ) ) ;
// Right: request / working, 分享, ⋯ — laid out from the right edge.
let mut right = ui . new_child ( egui ::UiBuilder ::new ( ) . max_rect ( bar . shrink2 ( vec2 ( 12.0 , 10.0 ) ) ) . layout ( Layout ::right_to_left ( Align ::Center ) ) ) ;
right . spacing_mut ( ) . item_spacing . x = 8.0 ;
let more = pill_icon ( & mut right , crate ::icons ::Icon ::More , p ) . on_hover_text ( t ( " 更多 " ) ) ;
egui ::Popup ::menu ( & more ) . show ( | ui | self . bot_menu_items ( ui , bot . id ) ) ;
if pill_text ( & mut right , & t ( " 分享 " ) , p ) . on_hover_text ( t ( " 匯出 Bot( 分享給別人) … " ) ) . clicked ( ) {
let ( text , name ) = ( crate ::share ::export ( bot ) , format! ( " {} .anybot " , bot . name ) ) ;
let note = self . ch . note . clone ( ) ;
std ::thread ::spawn ( move | | {
if let Some ( path ) = crate ::share ::choose_save ( & name ) {
let path = if path . extension ( ) . is_none ( ) { path . with_extension ( " anybot " ) } else { path } ;
* note . lock ( ) . unwrap ( ) = match std ::fs ::write ( & path , text ) {
Ok ( ( ) ) = > format! ( " 已匯出到 {} " , path . display ( ) ) ,
Err ( e ) = > format! ( " 匯出失敗: {e} " ) ,
} ;
}
} ) ;
}
if self . meeting_on ( ) {
let red = Color32 ::from_rgb ( 0xfc , 0x2c , 0x3e ) ;
if crate ::icons ::pill ( & mut right , crate ::icons ::Icon ::Record , & t ( " 正在錄會議 · 結束 " ) , red , Color32 ::WHITE , 36.0 ) . on_hover_text ( t ( " 停止錄音,轉成文字記錄 " ) ) . clicked ( ) {
let ctx = right . ctx ( ) . clone ( ) ;
self . voice_toggle ( & ctx , bot . id ) ;
}
}
2026-11-05 09:29:55 +00:00
if let Some ( kind ) = self . approvals . get ( & bot . id ) . map ( | a | a . kind ) {
let label = if kind = = ApprovalKind ::Handoff { t ( " 等你處理 " ) } else { t ( " 需要你允許 " ) } ;
if crate ::icons ::pill ( & mut right , crate ::icons ::Icon ::Shield , & label , p . accent , Color32 ::WHITE , 36.0 ) . on_hover_text ( t ( " 打開請求視窗 " ) ) . clicked ( ) {
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 . popup_hidden . remove ( & bot . id ) ;
2026-11-05 09:29:55 +00:00
self . stick_bottom = true ;
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 right_w = bar . right ( ) - right . min_rect ( ) . left ( ) ;
// Middle: the name pill (face + name), centred, never under the buttons on the right.
let font = egui ::FontId ::proportional ( 14.0 ) ;
let name_w = ui . painter ( ) . layout_no_wrap ( bot . name . clone ( ) , font . clone ( ) , p . text ) . size ( ) . x ;
let max_pill = ( bar . width ( ) - 2.0 * right_w - 24.0 ) . max ( 120.0 ) ;
let pill_w = ( name_w + 52.0 ) . min ( max_pill ) ;
let mut cx = bar . center ( ) . x ;
if cx + pill_w / 2.0 > bar . right ( ) - right_w - 8.0 {
cx = bar . right ( ) - right_w - 8.0 - pill_w / 2.0 ;
}
let rect = egui ::Rect ::from_center_size ( egui ::pos2 ( cx . max ( bar . left ( ) + pill_w / 2.0 + 8.0 ) , bar . center ( ) . y ) , vec2 ( pill_w , 40.0 ) ) ;
let r = ui . interact ( rect , egui ::Id ::unique ( ( " name-pill " , bot . id ) ) , egui ::Sense ::click ( ) ) ;
ui . painter ( ) . rect_filled ( rect , 20 , if r . hovered ( ) { p . hover } else { p . elevated } ) ;
ui . painter ( ) . rect_stroke ( rect , 20 , Stroke ::new ( 1.0 , p . border ) , egui ::StrokeKind ::Inside ) ;
let mut inner = ui . new_child ( egui ::UiBuilder ::new ( ) . max_rect ( rect . shrink2 ( vec2 ( 8.0 , 0.0 ) ) ) . layout ( Layout ::left_to_right ( Align ::Center ) ) ) ;
inner . spacing_mut ( ) . item_spacing . x = 8.0 ;
let ( face , _ ) = inner . allocate_exact_size ( vec2 ( 26.0 , 26.0 ) , egui ::Sense ::hover ( ) ) ;
let _ = color ;
self . live_face ( & mut inner , bot . id , face , true ) ;
inner . add ( egui ::Label ::new ( RichText ::new ( & bot . name ) . size ( 14.0 ) . color ( p . text ) ) . truncate ( ) . selectable ( false ) ) ;
let open = self . panel . is_some ( ) ;
if r . on_hover_text ( t ( if open { " 關閉對話詳細資訊 " } else { " 查看對話詳細資訊 " } ) ) . on_hover_cursor ( egui ::CursorIcon ::PointingHand ) . clicked ( ) {
self . panel = if open { None } else { Some ( PanelTab ::Activity ) } ;
}
}
fn empty_state ( & mut self , ui : & mut egui ::Ui , bot : & Bot ) {
let p = self . pal ;
ui . add_space ( 70.0 ) ;
ui . vertical_centered ( | ui | {
avatar ::avatar ( ui , 72.0 , & bot . shape , self . bot_color ( bot ) ) ;
ui . add_space ( 10.0 ) ;
ui . label ( RichText ::new ( & bot . name ) . size ( 22.0 ) . strong ( ) ) ;
if ! bot . description . is_empty ( ) {
ui . label ( RichText ::new ( & bot . description ) . color ( p . text2 ) ) ;
}
ui . add_space ( 6.0 ) ;
ui . label ( RichText ::new ( format! ( " 使用 {} · {} " , bot . provider . label ( ) , bot . model ) ) . size ( 12.0 ) . color ( p . text3 ) ) ;
if bot . tools_enabled {
ui . label ( RichText ::new ( format! ( " 可以在「 {} 」裡查看、修改檔案和執行指令,動手前會先問你 " , bot . workdir ) ) . size ( 12.0 ) . color ( p . text3 ) ) ;
}
let ps = self . state . provider ( bot . provider ) ;
if bot . provider . needs_key ( ) & & providers ::resolve_key ( bot . provider , & ps . api_key ) . is_empty ( ) {
ui . add_space ( 10.0 ) ;
if ui . button ( format! ( " 設定 {} 的 API 金鑰 " , bot . provider . label ( ) ) ) . clicked ( ) {
self . show_settings = true ;
self . settings_tab = SettingsTab ::Providers ;
self . settings_provider = bot . provider ;
}
}
} ) ;
}
/// "The bot wants to …": allow once, always allow this kind, or say no.
/// The question, the details and the buttons, shared by the chat card and the pop-up.
fn approval_body ( & self , ui : & mut egui ::Ui , bot : & Bot , big : bool ) -> Option < ApprovalAnswer > {
let p = self . pal ;
let a = self . approvals . get ( & bot . id ) ? ;
let ( tool , reason , grant , kind ) = ( a . tool . clone ( ) , a . reason . clone ( ) , a . grant . clone ( ) , a . kind ) ;
// "允許 Grok Bot 在 {targetMachineDisplay} 上使用 {app} 嗎?" (eQViQE): name the machine.
let ( reason , machine ) = match reason . rsplit_once ( " \n (在 " ) {
Some ( ( r , m ) ) = > ( r . to_string ( ) , m . trim_end_matches ( " 上) " ) . to_string ( ) ) ,
None = > ( reason . clone ( ) , String ::new ( ) ) ,
} ;
let what = if appctl ::is_app_tool ( & tool ) { appctl ::label ( & tool ) } else if crate ::macapps ::is_mac_tool ( & tool ) { crate ::macapps ::label ( & tool ) } else if crate ::computer ::is_computer_tool ( & tool ) { crate ::computer ::label ( & tool ) } else if web ::is_web_tool ( & tool ) { web ::label ( & tool ) } else { tools ::label ( & tool ) } ;
let title = match kind {
ApprovalKind ::EnableTools = > format! ( " 允許 {} 使用電腦工具嗎? " , bot . name ) ,
// Hd1KB+: 允許…在這台電腦上執行任務,而不只是在它自己的電腦上
ApprovalKind ::EnableThisComputer = > format! ( " 允許 {} 在你的電腦上執行任務嗎? " , bot . name ) ,
ApprovalKind ::Action if machine . is_empty ( ) = > format! ( " 允許 {} {} 嗎? " , bot . name , what ) ,
ApprovalKind ::Action = > format! ( " 允許 {} 在 {machine} 上 {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
} ;
let note = match kind {
ApprovalKind ::EnableTools = > Some ( " 這個 Bot 的電腦工具還沒打開。它想做的事: " ) ,
ApprovalKind ::EnableThisComputer = > Some ( " 「在這台電腦上執行」還沒打開, Bot 平常只在它自己的電腦上做事。它想在你的電腦上: " ) ,
2026-11-05 09:29:55 +00:00
ApprovalKind ::Action | ApprovalKind ::Handoff = > None ,
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 mut answer = None ;
ui . label ( RichText ::new ( title ) . strong ( ) . size ( if big { 17.0 } else { 14.0 } ) ) ;
ui . add_space ( 2.0 ) ;
let ( head , detail ) = reason . split_once ( ': ' ) . unwrap_or ( ( " " , reason . as_str ( ) ) ) ;
match note {
Some ( n ) = > ui . label ( RichText ::new ( n ) . size ( 12.5 ) . color ( p . text2 ) ) ,
None if ! head . is_empty ( ) = > ui . label ( RichText ::new ( head ) . size ( 12.0 ) . color ( p . text3 ) ) ,
None = > ui . label ( " " ) ,
} ;
Frame ::new ( ) . fill ( p . hover ) . corner_radius ( 8 ) . inner_margin ( Margin ::symmetric ( 10 , 6 ) ) . show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
ui . add ( egui ::Label ::new ( RichText ::new ( detail ) . monospace ( ) . size ( 12.5 ) . color ( p . text ) ) . wrap ( ) ) ;
} ) ;
if ! machine . is_empty ( ) & & kind ! = ApprovalKind ::Action {
ui . label ( RichText ::new ( format! ( " 在: {machine} " ) ) . size ( 12.0 ) . color ( p . text3 ) ) ;
}
ui . add_space ( 6.0 ) ;
ui . horizontal ( | ui | {
let once = if kind = = ApprovalKind ::Action { " 允許 " } else { " 允許這一次 " } ;
if ui . add ( egui ::Button ::new ( RichText ::new ( once ) . color ( p . on_primary ) ) . fill ( p . primary ) . corner_radius ( 8 ) . min_size ( vec2 ( 0.0 , if big { 30.0 } else { 0.0 } ) ) ) . clicked ( ) {
answer = Some ( ApprovalAnswer ::Once ) ;
}
let always = match kind {
ApprovalKind ::EnableTools = > Some ( " 一律允許這個 Bot 使用電腦 " . to_string ( ) ) ,
ApprovalKind ::EnableThisComputer = > Some ( " 一律允許(打開「在這台電腦上執行」) " . to_string ( ) ) ,
ApprovalKind ::Action if ! grant . is_empty ( ) = > Some ( format! ( " 一律允許 {} " , grant_label ( & grant ) ) ) ,
2026-11-05 09:29:55 +00:00
ApprovalKind ::Action | ApprovalKind ::Handoff = > None ,
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 ( label ) = always {
if ui . button ( label ) . clicked ( ) {
answer = Some ( ApprovalAnswer ::Always ( grant . clone ( ) ) ) ;
}
}
if ui . button ( t ( " 拒絕 " ) ) . clicked ( ) {
answer = Some ( ApprovalAnswer ::Reject ) ;
}
} ) ;
// One click to work like Grok Bot: allow this and stop asking from now on.
if ! self . direct_on ( ) & & self . managed . direct . allows ( ) & & self . managed . local . allows ( ) {
ui . add_space ( 4.0 ) ;
let b = crate ::icons ::label ( ui , crate ::icons ::Icon ::Zap , & t ( " 全部允許,之後直接操作(像 Grok Bot 一樣) " ) , 13.5 , p . accent ) . interact ( egui ::Sense ::click ( ) ) . on_hover_cursor ( egui ::CursorIcon ::PointingHand ) ;
if b . on_hover_text ( t ( " 打開「直接操作」: Bot 可以直接使用這台電腦和瀏覽器,不再一個個問你。危險指令、密碼檔和你的「一律詢問」規則仍會擋下來。可在設定 → 電腦關掉。 " ) ) . clicked ( ) {
self . want_direct . set ( true ) ;
answer = Some ( ApprovalAnswer ::Always ( grant . clone ( ) ) ) ;
}
}
answer
}
fn approval_card ( & mut self , ui : & mut egui ::Ui , ctx : & egui ::Context , bot : & Bot ) {
let p = self . pal ;
2026-11-05 09:29:55 +00:00
let Some ( kind ) = self . approvals . get ( & bot . id ) . map ( | a | a . kind ) else { return } ;
if kind = = ApprovalKind ::Handoff {
self . handoff_card ( ui , ctx , bot ) ;
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 ;
}
let mut answer = None ;
let width = ui . available_width ( ) - 32.0 - 28.0 ;
ui . horizontal_top ( | ui | {
ui . add_space ( 32.0 ) ;
Frame ::new ( ) . fill ( p . elevated ) . stroke ( Stroke ::new ( 2.0 , p . accent ) ) . corner_radius ( 14 ) . inner_margin ( Margin ::symmetric ( 14 , 12 ) ) . show ( ui , | ui | {
ui . vertical ( | ui | {
ui . set_width ( width ) ;
answer = self . approval_body ( ui , bot , false ) ;
} ) ;
} ) ;
} ) ;
ui . add_space ( 10.0 ) ;
if let Some ( ans ) = answer {
self . answer ( ctx , bot . id , ans ) ;
}
}
2026-11-05 09:29:55 +00:00
/// 需要處理 (the original's box hand-off card): what the bot needs the person to do, the
/// computer's live screen with 「接管」 on it, and 跳過 / 我完成了 / 接管.
fn handoff_card ( & mut self , ui : & mut egui ::Ui , ctx : & egui ::Context , bot : & Bot ) {
let p = self . pal ;
let Some ( a ) = self . approvals . get ( & bot . id ) else { return } ;
let ( instruction , computer ) = ( a . reason . clone ( ) , a . grant . clone ( ) ) ;
// Its own computer shows here (on a screen of a brain, the brain's); another computer of
// the person's is the one in front of them.
let own = computer = = crate ::model ::own_computer_name ( & bot . name ) | | computer = = crate ::model ::BOT_COMPUTER_NAME ;
let shows = own & & ( self . mir . on | | crate ::cloudbox ::available ( ) ) ;
let thumb = if shows { self . box_thumbnail ( ctx ) } else { None } ;
let width = ( ui . available_width ( ) - 32.0 - 28.0 ) . clamp ( 200.0 , 560.0 ) ;
enum Act {
Skip ,
Done ,
TakeOver ,
}
let mut act = None ;
ui . horizontal_top ( | ui | {
ui . add_space ( 32.0 ) ;
Frame ::new ( ) . fill ( p . elevated ) . stroke ( Stroke ::new ( 2.0 , p . accent ) ) . corner_radius ( 14 ) . inner_margin ( Margin ::symmetric ( 14 , 12 ) ) . show ( ui , | ui | {
ui . vertical ( | ui | {
ui . set_width ( width ) ;
ui . horizontal ( | ui | {
crate ::icons ::label ( ui , crate ::icons ::Icon ::Monitor , & t ( " 電腦 " ) , FONT_XS , p . text3 ) ;
ui . label ( RichText ::new ( sidebar_ui ::shorten_width ( ui , & format! ( " · {computer} " ) , ( ui . available_width ( ) - 4.0 ) . max ( 40.0 ) , FONT_XS ) ) . size ( FONT_XS ) . color ( p . text3 ) ) ;
} ) ;
ui . add_space ( 2.0 ) ;
ui . label ( RichText ::new ( t ( " 需要處理 " ) ) . strong ( ) . size ( 15.0 ) ) ;
ui . add ( egui ::Label ::new ( RichText ::new ( & instruction ) . size ( 13.5 ) . color ( p . text2 ) ) . wrap ( ) ) ;
ui . add_space ( 8.0 ) ;
// The screen, 16:10, with 「接管」 on it.
let sw = width . min ( 420.0 ) ;
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( sw , sw * 0.625 ) , if shows { egui ::Sense ::click ( ) } else { egui ::Sense ::hover ( ) } ) ;
match thumb {
Some ( ( tex , size ) ) = > {
egui ::Image ::new ( ( tex , size ) ) . fit_to_exact_size ( rect . size ( ) ) . corner_radius ( 10 ) . paint_at ( ui , rect ) ;
}
None = > {
ui . painter ( ) . rect_filled ( rect , 10 , Color32 ::from_rgb ( 0x2b , 0x2f , 0x36 ) ) ;
let text = if shows { t ( " 連結中 " ) } else { t ( " 請到「{name}」上操作 " ) . replace ( " {name} " , & computer ) } ;
ui . painter ( ) . text ( rect . center ( ) , egui ::Align2 ::CENTER_CENTER , sidebar_ui ::shorten_width ( ui , & text , rect . width ( ) - 20.0 , FONT_BASE ) , egui ::FontId ::proportional ( FONT_BASE ) , Color32 ::from_gray ( 220 ) ) ;
}
}
ui . painter ( ) . rect_stroke ( rect , 10 , Stroke ::new ( 1.0 , p . border ) , egui ::StrokeKind ::Inside ) ;
if shows {
let label = t ( " 接管 " ) ;
let g = ui . painter ( ) . layout_no_wrap ( label . clone ( ) , egui ::FontId ::proportional ( 13.0 ) , Color32 ::WHITE ) ;
let pill = egui ::Rect ::from_min_size ( rect . right_bottom ( ) - vec2 ( g . size ( ) . x + 28.0 , 38.0 ) , vec2 ( g . size ( ) . x + 20.0 , 28.0 ) ) ;
ui . painter ( ) . rect_filled ( pill , 14 , Color32 ::from_black_alpha ( if r . hovered ( ) { 200 } else { 160 } ) ) ;
ui . painter ( ) . galley ( pill . center ( ) - g . size ( ) / 2.0 , g , Color32 ::WHITE ) ;
if r . on_hover_cursor ( egui ::CursorIcon ::PointingHand ) . on_hover_text ( t ( " 接管電腦 " ) ) . clicked ( ) {
act = Some ( Act ::TakeOver ) ;
}
}
ui . add_space ( 10.0 ) ;
ui . horizontal ( | ui | {
if ui . button ( t ( " 跳過 " ) ) . on_hover_text ( t ( " 取消此要求且不執行該步驟; Bot 會跳過它並繼續 " ) ) . clicked ( ) {
act = Some ( Act ::Skip ) ;
}
if ui . button ( t ( " 我完成了 " ) ) . clicked ( ) {
act = Some ( Act ::Done ) ;
}
if shows & & ui . add ( egui ::Button ::new ( RichText ::new ( t ( " 接管 " ) ) . color ( p . on_primary ) ) . fill ( p . primary ) . corner_radius ( 8 ) ) . clicked ( ) {
act = Some ( Act ::TakeOver ) ;
}
} ) ;
} ) ;
} ) ;
} ) ;
ui . add_space ( 10.0 ) ;
match act {
Some ( Act ::Skip ) = > {
self . give_back_computer ( ) ;
self . answer ( ctx , bot . id , ApprovalAnswer ::Reject ) ;
}
Some ( Act ::Done ) = > {
self . give_back_computer ( ) ;
self . answer ( ctx , bot . id , ApprovalAnswer ::Once ) ;
}
Some ( Act ::TakeOver ) = > self . take_over_computer ( ) ,
None = > { }
}
}
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
/// A new request: show it as a pop-up (even over another bot's chat) and get the person's
/// attention (the Dock icon bounces when AnyBot isn't in front).
fn on_new_approval ( & mut self , ctx : & egui ::Context , bot_id : u64 ) {
self . popup_hidden . remove ( & bot_id ) ;
if ! self . demo {
ctx . send_viewport_cmd ( egui ::ViewportCommand ::RequestUserAttention ( egui ::UserAttentionType ::Critical ) ) ;
// Away from AnyBot (other app in front, or idle 90 s+): also a macOS banner.
if self . state . notify & & self . away ( ) {
if let ( Some ( b ) , Some ( a ) ) = ( self . state . bot ( bot_id ) , self . approvals . get ( & bot_id ) ) {
let what = a . reason . split ( '\n' ) . next ( ) . unwrap_or ( " " ) . to_string ( ) ;
2026-11-05 09:29:55 +00:00
// 「{callee} 需要你」 for a step only the person can do.
let title = if a . kind = = ApprovalKind ::Handoff { format! ( " {} :需要你 " , b . name ) } else { format! ( " {} 需要你允許 " , b . 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
if ! self . headless {
2026-11-05 09:29:55 +00:00
notify ::banner ( & title , & what ) ;
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
crate ::remote ::notify ( & title , & what , bot_id ) ;
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
}
}
}
}
/// The person isn't looking at AnyBot: window not in front, or no input for 90 s
/// (the original's presence rule `K4`, idle `Z_e=9e4`).
fn away ( & self ) -> bool {
if self . headless {
return true ;
}
notify ::should_notify ( self . window_focused , self . idle_secs )
}
/// Re-read focus every frame and system idle time every 10 s (`uBt=1e4`).
fn refresh_presence ( & mut self , ctx : & egui ::Context ) {
self . window_focused = ctx . input ( | i | i . viewport ( ) . focused . unwrap_or ( true ) ) ;
if ! self . demo & & self . last_presence . elapsed ( ) . as_secs_f64 ( ) > = notify ::PRESENCE_POLL_SECS {
self . last_presence = Instant ::now ( ) ;
self . idle_secs = notify ::system_idle_secs ( ) ;
}
}
/// The pop-up: a dimmed window with the request in the middle, so it can't be missed.
fn approval_popup ( & mut self , ctx : & egui ::Context ) {
let selected = self . state . selected ;
2026-11-05 09:29:55 +00:00
// (A hand-off is the card in the chat, with the live screen, like the original's.)
let mut ids : Vec < u64 > = self . approvals . iter ( ) . filter ( | ( id , a ) | ! self . popup_hidden . contains ( id ) & & a . kind ! = ApprovalKind ::Handoff ) . map ( | ( id , _ ) | * id ) . 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
ids . sort_by_key ( | id | ( * id ! = selected , * id ) ) ;
let Some ( & bot_id ) = ids . first ( ) else { return } ;
let Some ( bot ) = self . state . bot ( bot_id ) . cloned ( ) else { return } ;
let p = self . pal ;
let screen = ctx . content_rect ( ) ;
egui ::Area ::new ( egui ::Id ::unique ( " approval-dim " ) ) . order ( egui ::Order ::Foreground ) . fixed_pos ( screen . min ) . interactable ( true ) . show ( ctx , | ui | {
let ( rect , _ ) = ui . allocate_exact_size ( screen . size ( ) , egui ::Sense ::click ( ) ) ;
ui . painter ( ) . rect_filled ( rect , 0 , Color32 ::from_black_alpha ( if p . pane = = LIGHT . pane { 70 } else { 120 } ) ) ;
} ) ;
let mut answer = None ;
let mut later = false ;
let mut go = false ;
egui ::Window ::new ( " 需要你的允許 " )
. id ( egui ::Id ::unique ( " approval-popup " ) )
. order ( egui ::Order ::Foreground )
. collapsible ( false )
. resizable ( false )
. title_bar ( false )
. fixed_size ( vec2 ( 440.0 , 0.0 ) )
. anchor ( egui ::Align2 ::CENTER_CENTER , vec2 ( 0.0 , - 20.0 ) )
. frame ( Frame ::new ( ) . fill ( p . elevated ) . stroke ( Stroke ::new ( 2.0 , p . accent ) ) . corner_radius ( 16 ) . inner_margin ( Margin ::same ( 18 ) ) . shadow ( egui ::epaint ::Shadow { offset : [ 0 , 8 ] , blur : 28 , spread : 0 , color : Color32 ::from_black_alpha ( 60 ) } ) )
. show ( ctx , | ui | {
ui . horizontal ( | ui | {
avatar ::avatar ( ui , 28.0 , & bot . shape , avatar ::color ( & bot . color ) ) ;
ui . label ( RichText ::new ( format! ( " {} 正在等你決定 " , bot . name ) ) . size ( 12.5 ) . color ( p . text3 ) ) ;
if ids . len ( ) > 1 {
ui . label ( RichText ::new ( format! ( " (還有 {} 個請求) " , ids . len ( ) - 1 ) ) . size ( 12.0 ) . color ( p . text3 ) ) ;
}
} ) ;
ui . add_space ( 6.0 ) ;
answer = self . approval_body ( ui , & bot , true ) ;
ui . add_space ( 4.0 ) ;
ui . horizontal ( | ui | {
if bot_id ! = selected & & small_button ( ui , " 前往聊天 " , p ) . clicked ( ) {
go = true ;
}
if small_button ( ui , " 稍後再決定 " , p ) . on_hover_text ( t ( " 先關掉這個視窗; 聊天裡的卡片還在, Bot 會一直等你 " ) ) . clicked ( ) {
later = true ;
}
} ) ;
} ) ;
if let Some ( ans ) = answer {
self . answer ( ctx , bot_id , ans ) ;
} else if later {
self . popup_hidden . insert ( bot_id ) ;
} else if go {
self . state . selected = bot_id ;
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
b . unread = false ;
}
self . stick_bottom = true ;
}
}
fn composer ( & mut self , ui : & mut egui ::Ui , ctx : & egui ::Context , bot : & Bot ) {
let p = self . pal ;
let streaming = self . streams . contains_key ( & bot . id ) ;
let composer_id = egui ::Id ::unique ( ( " composer " , bot . id ) ) ;
let had_focus = ui . memory ( | m | m . has_focus ( composer_id ) ) ;
let mut send_now = false ;
// Enter sends, Shift+Enter is a new line, and Enter while an input method is
// composing (e.g. picking Chinese characters) is left alone: index.eager-app `mwt` (@2002346).
let composing = ui . input ( | i | i . events . iter ( ) . any ( | e | matches! ( e , egui ::Event ::Ime ( egui ::ImeEvent ::Preedit { text , .. } ) if ! text . is_empty ( ) ) ) ) ;
if had_focus & & ! composing & & ui . input_mut ( | i | i . consume_key ( egui ::Modifiers ::NONE , Key ::Enter ) ) {
send_now = true ;
}
self . attachments_poll ( ctx , bot . id , had_focus ) ;
self . voice_poll ( ) ;
let mut draft = self . drafts . remove ( & bot . id ) . unwrap_or_default ( ) ;
ui . add_space ( 4.0 ) ;
// Quote and attachments sit just above the pill.
if let Some ( q ) = self . quoting . get ( & bot . id ) . cloned ( ) {
ui . horizontal ( | ui | {
ui . add_space ( 14.0 ) ;
let ( r , _ ) = ui . allocate_exact_size ( vec2 ( 3.0 , 18.0 ) , egui ::Sense ::hover ( ) ) ;
ui . painter ( ) . rect_filled ( r , 1 , p . accent ) ;
let short : String = q . lines ( ) . next ( ) . unwrap_or ( " " ) . chars ( ) . take ( 60 ) . collect ( ) ;
ui . label ( RichText ::new ( format! ( " {} {short} … " , t ( " 引用: " ) ) ) . size ( FONT_XS ) . color ( p . text2 ) ) ;
if ui . add ( egui ::Button ::new ( RichText ::new ( " × " ) . size ( 13.0 ) . color ( p . text3 ) ) . frame ( false ) ) . clicked ( ) {
self . quoting . remove ( & bot . id ) ;
}
} ) ;
ui . add_space ( 4.0 ) ;
}
if self . has_pending_attachments ( bot . id ) {
ui . horizontal ( | ui | {
ui . add_space ( 10.0 ) ;
self . pending_chips ( ui , bot . id ) ;
} ) ;
ui . add_space ( 4.0 ) ;
}
// The original's input: one 44 px pill — + , the words, the mic, and a round button in the
// bot's colour (voice chat while empty, send ↑ once there is something to send).
Frame ::new ( )
. fill ( p . elevated )
. stroke ( Stroke ::new ( 1.0 , if had_focus { p . border . gamma_multiply ( 1.8 ) } else { p . border } ) )
. corner_radius ( 22 )
. inner_margin ( Margin { left : 8 , right : 8 , top : 8 , bottom : 8 } )
. show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
ui . horizontal ( | ui | {
ui . spacing_mut ( ) . item_spacing . x = 8.0 ;
// + : 附加檔案 / 教它一項任務 / the model.
let plus = round_tool ( ui , crate ::icons ::Icon ::Plus , p . hover , p . text2 ) . on_hover_text ( t ( " 輸入框操作 " ) ) ;
egui ::Popup ::menu ( & plus ) . show ( | ui | {
ui . set_min_width ( menu_ui ::MENU_W ) ;
if menu_ui ::item ( ui , p , crate ::icons ::Icon ::Paperclip , " 附加檔案 " ) . clicked ( ) {
self . pick_attachments ( ctx , bot . id ) ;
ui . close ( ) ;
}
if crate ::computer ::demo_supported ( ) & & menu_ui ::item ( ui , p , crate ::icons ::Icon ::Record , " 教它一項任務 " ) . clicked ( ) {
self . teach_quick ( bot . id ) ;
ui . close ( ) ;
}
menu_ui ::sep ( ui , p ) ;
if bot . members . is_empty ( ) {
self . model_picker ( ui , bot ) ;
} else {
let names : Vec < String > = bot . members . iter ( ) . filter_map ( | m | self . state . bot ( * m ) ) . map ( | b | b . name . clone ( ) ) . collect ( ) ;
ui . label ( RichText ::new ( format! ( " {} {} " , t ( " 成員: " ) , names . join ( " 、 " ) ) ) . size ( FONT_XS ) . color ( p . text2 ) ) ;
}
} ) ;
let right_w = 28.0 * 2.0 + 8.0 + 4.0 ;
let text_w = ( ui . available_width ( ) - right_w ) . max ( 60.0 ) ;
2026-11-04 09:46:43 +00:00
let hint = if self . mir . on & & ! self . mir . online { t ( " 連不到大腦,現在只能看 " ) } else { t ( " 傳訊息給 {name} " ) . replace ( " {name} " , & 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
ui . allocate_ui_with_layout ( vec2 ( text_w , 28.0 ) , Layout ::top_down ( Align ::Min ) , | ui | {
egui ::ScrollArea ::vertical ( ) . id_salt ( ( " composer_scroll " , bot . id ) ) . max_height ( 160.0 ) . show ( ui , | ui | {
ui . add_space ( 4.0 ) ;
ui . add ( egui ::TextEdit ::multiline ( & mut draft ) . id ( composer_id ) . hint_text ( RichText ::new ( hint ) . color ( p . text3 ) ) . frame ( Frame ::NONE ) . margin ( Margin ::ZERO ) . desired_rows ( 1 ) . desired_width ( text_w ) . lock_focus ( true ) ) ;
} ) ;
} ) ;
ui . with_layout ( Layout ::right_to_left ( Align ::Max ) , | ui | {
ui . spacing_mut ( ) . item_spacing . x = 8.0 ;
let has_text = ! draft . trim ( ) . is_empty ( ) | | self . has_pending_attachments ( bot . id ) ;
let call = self . call_state ( ) ;
let in_call = call . map ( | c | c . 0 = = bot . id ) . unwrap_or ( false ) ;
let colour = self . bot_color ( bot ) ;
let colour = if colour . r ( ) as u32 + colour . g ( ) as u32 + colour . b ( ) as u32 < = 0x30 * 3 { p . primary } else { colour } ;
let ink = if colour = = p . primary { p . on_primary } else { Color32 ::WHITE } ;
if has_text {
let tip = if streaming { " 補充給正在進行的工作( Enter) " } else { " 傳送訊息 " } ;
if round_tool ( ui , crate ::icons ::Icon ::ArrowUp , colour , ink ) . on_hover_text ( t ( tip ) ) . clicked ( ) {
send_now = true ;
}
} else if streaming {
if round_tool ( ui , crate ::icons ::Icon ::Stop , colour , ink ) . on_hover_text ( t ( " 停止 " ) ) . clicked ( ) {
self . stop ( bot . id ) ;
}
} else {
let fill = if in_call { Color32 ::from_rgb ( 0xfc , 0x2c , 0x3e ) } else { colour } ;
let tip = match call {
Some ( ( id , phase ) ) if id = = bot . id = > format! ( " {} · {} " , t ( phase . label ( ) ) , t ( " 結束通話 " ) ) ,
_ = > t ( " 發起語音聊天 " ) ,
} ;
if round_tool ( ui , if in_call { crate ::icons ::Icon ::Stop } else { crate ::icons ::Icon ::Wave } , fill , if in_call { Color32 ::WHITE } else { ink } ) . on_hover_text ( tip ) . clicked ( ) {
if in_call {
self . call_end ( ) ;
} else if call . is_none ( ) {
self . call_start ( bot . id ) ;
}
}
}
// 語音輸入: click to record, click again to turn it into text.
if let Some ( started ) = self . voice_recording ( bot . id ) {
let secs = started . elapsed ( ) . as_secs ( ) ;
let tt = ui . input ( | i | i . time ) as f32 ;
let red = Color32 ::from_rgb ( 0xfc , 0x2c , 0x3e ) ;
let fill = red . gamma_multiply ( 0.75 + 0.25 * ( tt * 3.0 ) . sin ( ) . abs ( ) ) ;
if crate ::icons ::pill ( ui , crate ::icons ::Icon ::Record , & format! ( " {:02} : {:02} " , secs / 60 , secs % 60 ) , fill , Color32 ::WHITE , 28.0 ) . on_hover_text ( crate ::i18n ::t ( " 停止錄音並轉成文字 " ) ) . clicked ( ) {
self . voice_toggle ( ctx , bot . id ) ;
}
ctx . request_repaint_after ( std ::time ::Duration ::from_millis ( 100 ) ) ;
} else if self . voice_busy ( ) {
chat_ui ::spinner ( ui , 18.0 , p . text3 ) ;
} else if round_tool ( ui , crate ::icons ::Icon ::Mic , p . hover , p . text2 ) . on_hover_text ( t ( " 開始語音輸入 " ) ) . clicked ( ) {
self . voice_toggle ( ctx , bot . id ) ;
}
if let Some ( ( id , phase ) ) = call {
if id = = bot . id {
ui . label ( RichText ::new ( t ( phase . label ( ) ) ) . size ( FONT_XS ) . color ( p . text3 ) ) ;
}
}
} ) ;
} ) ;
} ) ;
ui . add_space ( 4.0 ) ;
let queued : Vec < String > = self . streams . get ( & bot . id ) . map ( | l | l . steer . lock ( ) . unwrap ( ) . clone ( ) ) . unwrap_or_default ( ) ;
let notice = self . notice . clone ( ) ;
ui . vertical_centered ( | ui | {
if let Some ( n ) = notice . filter ( | _ | queued . is_empty ( ) ) {
ui . horizontal ( | ui | {
ui . label ( RichText ::new ( n ) . size ( 11.0 ) . color ( p . accent ) ) ;
if small_button ( ui , " 知道了 " , p ) . clicked ( ) {
self . notice = None ;
}
} ) ;
} else if queued . is_empty ( ) {
// Like the original: nothing under the input.
} else {
let first : String = queued . last ( ) . unwrap ( ) . chars ( ) . take ( 30 ) . collect ( ) ;
let more = if queued . len ( ) > 1 { format! ( " 等 {} 則 " , queued . len ( ) ) } else { String ::new ( ) } ;
ui . label ( RichText ::new ( format! ( " 已排入「 {first} 」 {more} , Bot 會在下一步看到" ) ) . size ( 11.0 ) . color ( p . accent ) ) ;
}
} ) ;
ui . add_space ( 10.0 ) ;
self . drafts . insert ( bot . id , draft ) ;
if send_now {
self . send ( ctx , bot . id ) ;
ui . memory_mut ( | m | m . request_focus ( composer_id ) ) ;
}
}
/// Settings → 電腦: AnyBot browser profiles (each with its own logins), switch / open / add.
fn browser_profiles_section ( & mut self , ui : & mut egui ::Ui , p : Palette ) {
setting_title ( ui , " AnyBot 瀏覽器設定檔 " , " 每個設定檔是一個獨立的瀏覽器視窗, 有自己的登入( 例如工作和私人帳號) 。Bot 會用「使用中」的那個,你也可以在聊天裡叫它換。 " , p ) ;
if ! crate ::botbrowser ::available ( ) {
ui . label ( RichText ::new ( t ( " 需要 Chrome、Brave、Edge 或 Chromium 才能用 AnyBot 自己的瀏覽器。 " ) ) . size ( 11.5 ) . color ( p . text3 ) ) ;
return ;
}
let current = crate ::botbrowser ::current ( ) ;
for name in crate ::botbrowser ::choices ( ) {
ui . horizontal ( | ui | {
let mut on = name = = current ;
if ui . radio_value ( & mut on , true , RichText ::new ( & name ) . strong ( ) ) . clicked ( ) & & name ! = current {
let _ = crate ::botbrowser ::set_current ( & name ) ;
}
if name = = current {
ui . label ( RichText ::new ( t ( " 使用中 " ) ) . size ( 11.5 ) . color ( p . accent ) ) ;
}
if name = = crate ::botbrowser ::PERSON_PROFILE {
ui . label ( RichText ::new ( t ( " 你平常的 Chrome, 切到哪個設定檔就讀哪個 " ) ) . size ( 11.5 ) . color ( p . text2 ) ) ;
if crate ::axread ::trusted ( false ) {
crate ::icons ::label ( ui , crate ::icons ::Icon ::Check , & t ( " 輔助使用已打開 " ) , 11.5 , p . text3 ) ;
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
} else if ui . small_button ( t ( " 打開輔助使用 " ) ) . on_hover_text ( t ( " macOS 要你允許一次, AnyBot 才看得到你的瀏覽器視窗 " ) ) . clicked ( ) & & ! crate ::axread ::trusted ( true ) {
let _ = crate ::computer ::spawn_reaped ( std ::process ::Command ::new ( " open " ) . arg ( " x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility " ) ) ;
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 ;
}
if ui . small_button ( t ( " 開啟視窗 " ) ) . on_hover_text ( t ( " 打開這個設定檔的瀏覽器視窗,在裡面登入網站 " ) ) . clicked ( ) {
let note = self . browser_note . clone ( ) ;
let name = name . clone ( ) ;
* note . lock ( ) . unwrap ( ) = format! ( " 正在打開「 {name} 」… " ) ;
std ::thread ::spawn ( move | | {
let r = crate ::botbrowser ::show ( & name ) ;
* note . lock ( ) . unwrap ( ) = match r {
Ok ( ( ) ) = > format! ( " 已打開「 {name} 」的視窗。在裡面登入一次,之後會記住。 " ) ,
Err ( e ) = > e ,
} ;
} ) ;
}
if name ! = crate ::botbrowser ::DEFAULT_PROFILE {
if self . confirm_remove_profile . as_deref ( ) = = Some ( name . as_str ( ) ) {
if ui . small_button ( RichText ::new ( t ( " 確定移除(登入也會刪掉) " ) ) . color ( p . danger ) ) . clicked ( ) {
* self . browser_note . lock ( ) . unwrap ( ) = match crate ::botbrowser ::remove ( & name ) {
Ok ( ( ) ) = > format! ( " 已移除「 {name} 」。 " ) ,
Err ( e ) = > e ,
} ;
self . confirm_remove_profile = None ;
}
if ui . small_button ( crate ::i18n ::t ( " 取消 " ) ) . clicked ( ) {
self . confirm_remove_profile = None ;
}
} else if ui . small_button ( t ( " 移除 " ) ) . clicked ( ) {
self . confirm_remove_profile = Some ( name . clone ( ) ) ;
}
}
} ) ;
}
ui . horizontal ( | ui | {
ui . add ( egui ::TextEdit ::singleline ( & mut self . new_browser_profile ) . hint_text ( t ( " 新設定檔名稱,例如 工作 " ) ) . desired_width ( 180.0 ) ) ;
let ok = crate ::botbrowser ::check_name ( & self . new_browser_profile ) . is_ok ( ) ;
if ui . add_enabled ( ok , egui ::Button ::new ( " 新增設定檔 " ) ) . clicked ( ) {
let r = crate ::botbrowser ::add ( & self . new_browser_profile ) . and_then ( | n | crate ::botbrowser ::set_current ( & n ) ) ;
* self . browser_note . lock ( ) . unwrap ( ) = match r {
Ok ( n ) = > format! ( " 已新增「 {n} 」並設為使用中。按「開啟視窗」登入你要的網站。 " ) ,
Err ( e ) = > e ,
} ;
self . new_browser_profile . clear ( ) ;
}
} ) ;
let note = self . browser_note . lock ( ) . unwrap ( ) . clone ( ) ;
if ! note . is_empty ( ) {
ui . label ( RichText ::new ( note ) . size ( 11.5 ) . color ( p . text2 ) ) ;
}
ui . label ( RichText ::new ( t ( " 選「你的瀏覽器」: Bot 像 Grok Bot 一樣從畫面讀你平常的 Chrome( macOS 會請你打開一次「輔助使用」),你在 Chrome 切到哪個設定檔,它就看哪個;讀之前會先問你。其他的是 AnyBot 自己的視窗,各自登入一次就好。 " ) ) . size ( 11.0 ) . color ( p . text3 ) ) ;
}
/// The bot's routines: what repeats, when it runs next, pause / run now / delete.
fn routines_window ( & mut self , ctx : & egui ::Context ) {
let Some ( bot_id ) = self . show_routines else { return } ;
let Some ( bot ) = self . state . bot ( bot_id ) . cloned ( ) else {
self . show_routines = None ;
return ;
} ;
let p = self . pal ;
let mut action : Option < ( & str , u64 ) > = None ;
let body_h = dialog_max_height ( ctx ) - 48.0 ;
let dismissed = dialog ( ctx , " routines " , & format! ( " {} {} " , bot . name , t ( " 的例行任務 " ) ) , 500.0 , true , p , | ui | {
dialog_body ( ui , " routines-body " , body_h , | ui | self . routines_body ( ui , & bot , & mut action ) ) ;
} ) ;
let open = ! dismissed ;
self . routine_action ( ctx , bot_id , action ) ;
if ! open {
self . show_routines = None ;
}
}
/// The routines of one bot with 暫停/立即執行/刪除 (the window and the side panel share it).
fn routines_body ( & self , ui : & mut egui ::Ui , bot : & Bot , action : & mut Option < ( & 'static str , u64 ) > ) {
let p = self . pal ;
if bot . routines . is_empty ( ) {
ui . label ( RichText ::new ( t ( " 例行任務是這個 Bot 依排程重複執行的任務。請在聊天中請它設定一個。 " ) ) . color ( p . text2 ) ) ;
ui . add_space ( 4.0 ) ;
hint ( ui , " 例如:「每個工作日早上 9 點整理新聞給我」 " , p ) ;
return ;
}
for r in & bot . routines {
setting_card ( ui , p , | ui | {
ui . horizontal ( | ui | {
ui . label ( RichText ::new ( & r . title ) . strong ( ) . size ( FONT_LG ) ) ;
if r . paused {
ui . label ( RichText ::new ( t ( " 已暫停 " ) ) . size ( 11.5 ) . color ( p . text3 ) ) ;
}
} ) ;
let next = match ( r . paused , r . next_run ) {
( false , Some ( t ) ) = > format! ( " · 下次 {} " , crate ::routines ::describe_when ( t , & r . schedule . timezone ) ) ,
_ = > String ::new ( ) ,
} ;
match & r . trigger {
Some ( t ) = > {
crate ::icons ::label ( ui , crate ::icons ::Icon ::Zap , & t . describe ( ) , 12.5 , p . text2 ) ;
if let Some ( st ) = crate ::triggers ::statuses ( ) . lock ( ) . unwrap ( ) . get ( & r . id ) . cloned ( ) {
ui . label ( RichText ::new ( st ) . size ( 11.5 ) . color ( p . text3 ) ) ;
}
}
None = > {
ui . label ( RichText ::new ( format! ( " {} {next} " , crate ::routines ::describe ( & r . schedule ) ) ) . size ( 12.5 ) . color ( p . text2 ) ) ;
}
}
hint ( ui , r . prompt . clone ( ) , p ) ;
ui . add_space ( 4.0 ) ;
ui . horizontal ( | ui | {
if ui . add ( secondary_button ( if r . paused { " 繼續 " } else { " 暫停 " } ) ) . clicked ( ) {
* action = Some ( ( " toggle " , r . id ) ) ;
}
if ui . add ( secondary_button ( " 立即執行 " ) ) . clicked ( ) {
* action = Some ( ( " run " , r . id ) ) ;
}
ui . with_layout ( Layout ::right_to_left ( Align ::Center ) , | ui | {
if ui . add ( egui ::Button ::new ( RichText ::new ( t ( " 刪除 " ) ) . color ( p . danger ) ) . frame ( false ) ) . clicked ( ) {
* action = Some ( ( " delete " , r . id ) ) ;
}
} ) ;
} ) ;
} ) ;
}
hint ( ui , " 要新增或修改,直接在聊天裡告訴 Bot。 " , p ) ;
}
fn routine_action ( & mut self , ctx : & egui ::Context , bot_id : u64 , action : Option < ( & str , u64 ) > ) {
if let Some ( ( what , id ) ) = action {
let now = crate ::routines ::now ( ) ;
let mut wake = None ;
if let Some ( b ) = self . state . bot_mut ( bot_id ) {
match what {
" toggle " = > {
if let Some ( r ) = b . routines . iter_mut ( ) . find ( | r | r . id = = id ) {
r . paused = ! r . paused ;
r . next_run = crate ::routines ::next_run ( & r . schedule , r . created_at , now ) ;
}
}
" run " = > {
if let Some ( r ) = b . routines . iter_mut ( ) . find ( | r | r . id = = id ) {
r . last_run = Some ( now ) ;
wake = Some ( appctl ::wake_message ( r ) ) ;
}
}
_ = > self . confirm_delete_routine = Some ( ( bot_id , id ) ) ,
}
}
if let Some ( m ) = wake {
self . state . selected = bot_id ;
self . wake ( ctx , bot_id , m ) ;
}
self . dirty = true ;
}
}
fn model_picker ( & mut self , ui : & mut egui ::Ui , bot : & Bot ) {
let p = self . pal ;
let label = format! ( " {} · {} " , short_provider ( bot . provider ) , bot . model ) ;
let mut choice : Option < ( ProviderKind , String ) > = None ;
let mut open_editor = false ;
let models : Vec < ( ProviderKind , bool , Vec < String > ) > = ALL_PROVIDERS
. iter ( )
. map ( | & kind | {
let ps = self . state . provider ( kind ) ;
( kind , ! kind . needs_key ( ) | | ! providers ::resolve_key ( kind , & ps . api_key ) . is_empty ( ) , self . models_for ( kind ) )
} )
. collect ( ) ;
let row = menu_ui ::item_text ( ui , p , Some ( crate ::icons ::Icon ::Sparkle ) , & label ) . on_hover_text ( t ( " 選擇這個 Bot 用來思考的模型 " ) ) ;
egui ::containers ::menu ::SubMenu ::new ( ) . show ( ui , & row , | ui | {
ui . set_min_width ( menu_ui ::MENU_W ) ;
for ( kind , ready , list ) in & models {
let title = if * ready { kind . label ( ) . to_string ( ) } else { format! ( " {} ( {} ) " , kind . label ( ) , t ( " 未設定金鑰 " ) ) } ;
let icon = if * kind = = bot . provider { crate ::icons ::Icon ::Check } else { crate ::icons ::Icon ::Sparkle } ;
let r = menu_ui ::item_text ( ui , p , Some ( icon ) , & title ) ;
egui ::containers ::menu ::SubMenu ::new ( ) . show ( ui , & r , | ui | {
ui . set_min_width ( 220.0 ) ;
for m in list {
let sel = * kind = = bot . provider & & * m = = bot . model ;
if menu_ui ::item_text ( ui , p , Some ( if sel { crate ::icons ::Icon ::Check } else { crate ::icons ::Icon ::Sparkle } ) , m ) . clicked ( ) {
choice = Some ( ( * kind , m . clone ( ) ) ) ;
ui . close ( ) ;
}
}
menu_ui ::sep ( ui , p ) ;
if menu_ui ::item ( ui , p , crate ::icons ::Icon ::Pencil , " 其他模型… " ) . clicked ( ) {
choice = Some ( ( * kind , self . default_model ( * kind ) ) ) ;
open_editor = true ;
ui . close ( ) ;
}
} ) ;
}
} ) ;
if let Some ( ( k , m ) ) = choice {
if let Some ( b ) = self . state . bot_mut ( bot . id ) {
b . provider = k ;
b . model = m ;
if open_editor {
self . edit_bot = Some ( ( b . clone ( ) , false ) ) ;
}
}
self . mark ( ) ;
}
}
fn default_model ( & self , kind : ProviderKind ) -> String {
let ps = self . state . provider ( kind ) ;
if ps . default_model . is_empty ( ) { kind . suggested_model ( ) . to_string ( ) } else { ps . default_model }
}
// ---------- dialogs ----------
fn bot_editor ( & mut self , ctx : & egui ::Context ) {
let Some ( ( mut b , is_new ) ) = self . edit_bot . take ( ) else { return } ;
let p = self . pal ;
let mut save = false ;
let mut cancel = false ;
// Title bar ~48 px and footer ~56 px; the form scrolls in between.
let body_h = dialog_max_height ( ctx ) - 104.0 ;
let title = if is_new { " 建立 Bot " } else { " Bot 設定 " } ;
let dismissed = dialog ( ctx , " bot-editor " , title , EDITOR_WIDTH , false , p , | ui | {
dialog_body ( ui , " bot-editor-body " , body_h , | ui | {
ui . horizontal_top ( | ui | {
ui . spacing_mut ( ) . item_spacing . x = 16.0 ;
avatar ::avatar ( ui , 64.0 , & b . shape , avatar ::color ( & b . color ) ) ;
ui . vertical ( | ui | {
let w = ui . available_width ( ) ;
ui . label ( RichText ::new ( t ( " Bot 名稱 " ) ) . size ( FONT_XS ) . color ( p . text2 ) ) ;
ui . add ( egui ::TextEdit ::singleline ( & mut b . name ) . desired_width ( w ) ) ;
field_label ( ui , " Bot 描述 " , p ) ;
ui . add ( egui ::TextEdit ::singleline ( & mut b . description ) . hint_text ( t ( " 它負責做什麼? " ) ) . desired_width ( w ) ) ;
} ) ;
} ) ;
ui . add_space ( 16.0 ) ;
setting_card ( ui , p , | ui | {
setting_title ( ui , " 頭像 " , " " , p ) ;
let inactive_grey = if p . pane = = LIGHT . pane { Color32 ::from_gray ( 185 ) } else { Color32 ::from_gray ( 90 ) } ;
ui . horizontal_wrapped ( | ui | {
ui . spacing_mut ( ) . item_spacing . x = 6.0 ;
for s in avatar ::SHAPES {
let r = avatar ::avatar ( ui , 28.0 , s , if b . shape = = s { avatar ::color ( & b . color ) } else { inactive_grey } ) . on_hover_text ( avatar ::shape_label ( s ) ) ;
if r . clicked ( ) {
b . shape = s . into ( ) ;
}
}
} ) ;
ui . add_space ( 4.0 ) ;
ui . horizontal_wrapped ( | ui | {
ui . spacing_mut ( ) . item_spacing . x = 6.0 ;
for ( key , name , _ ) in avatar ::COLORS {
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( 28.0 , 28.0 ) , egui ::Sense ::click ( ) ) ;
ui . painter ( ) . circle_filled ( rect . center ( ) , 10.0 , avatar ::color ( key ) ) ;
if b . color = = key {
ui . painter ( ) . circle_stroke ( rect . center ( ) , 13.0 , Stroke ::new ( 2.0 , p . focus ) ) ;
}
if r . on_hover_text ( name ) . clicked ( ) {
b . color = key . into ( ) ;
}
}
} ) ;
} ) ;
setting_card ( ui , p , | ui | {
setting_title ( ui , " AI 供應商與模型 " , " " , p ) ;
ui . horizontal ( | ui | {
let before = b . provider ;
egui ::ComboBox ::from_id_salt ( " bot_provider " ) . selected_text ( b . provider . label ( ) ) . width ( 170.0 ) . show_ui ( ui , | ui | {
for k in ALL_PROVIDERS {
ui . selectable_value ( & mut b . provider , k , k . label ( ) ) ;
}
} ) ;
if b . provider ! = before {
b . model = self . default_model ( b . provider ) ;
}
let models = self . models_for ( b . provider ) ;
let w = ui . available_width ( ) - 80.0 ;
ui . add ( egui ::TextEdit ::singleline ( & mut b . model ) . hint_text ( t ( " 模型名稱 " ) ) . desired_width ( w ) ) ;
egui ::ComboBox ::from_id_salt ( " bot_model " ) . selected_text ( " 選擇 " ) . width ( 60.0 ) . show_ui ( ui , | ui | {
for m in models {
let sel = m = = b . model ;
if ui . selectable_label ( sel , & m ) . clicked ( ) {
b . model = m ;
}
}
} ) ;
} ) ;
let ps = self . state . provider ( b . provider ) ;
if b . provider . needs_key ( ) & & providers ::resolve_key ( b . provider , & ps . api_key ) . is_empty ( ) {
ui . horizontal ( | ui | {
ui . label ( RichText ::new ( format! ( " 還沒有設定 {} 的 API 金鑰 " , b . provider . label ( ) ) ) . size ( 12.0 ) . color ( p . danger ) ) ;
if ui . add ( egui ::Button ::new ( RichText ::new ( t ( " 前往設定 " ) ) . size ( 12.0 ) . color ( p . accent ) ) . frame ( false ) ) . clicked ( ) {
self . show_settings = true ;
self . settings_tab = SettingsTab ::Providers ;
self . settings_provider = b . provider ;
}
} ) ;
}
} ) ;
setting_card ( ui , p , | ui | {
setting_title ( ui , " 電腦操作 " , " " , p ) ;
ui . checkbox ( & mut b . tools_enabled , t ( " 讓這個 Bot 查看、修改檔案和執行指令 " ) ) ;
if b . tools_enabled {
ui . add_space ( 6.0 ) ;
2026-11-05 08:47:28 +00:00
form_row ( ui , " 做事的電腦 " , p , | ui | {
let ( choices , current ) = self . computer_choices ( & b ) ;
let mut pick = None ;
egui ::ComboBox ::from_id_salt ( " default_computer " ) . selected_text ( current . clone ( ) ) . width ( 190.0 ) . show_ui ( ui , | ui | {
for ( n , kind ) in choices {
let label = if kind = = " this " & & ! self . state . local_execution { format! ( " {n} (要先在設定裡允許) " ) } else { n . clone ( ) } ;
if ui . selectable_label ( n = = current , label ) . clicked ( ) {
pick = Some ( n ) ;
}
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 08:47:28 +00:00
if let Some ( n ) = pick {
b . default_computer = Self ::computer_to_keep ( & b , & n ) ;
}
2026-11-05 08:49:39 +00:00
ui . label ( RichText ::new ( t ( " 聊天時指名別台就會換 " ) ) . size ( 11.5 ) . color ( p . text3 ) ) ;
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
} ) ;
form_row ( ui , " 起始資料夾 " , p , | ui | {
ui . add ( egui ::TextEdit ::singleline ( & mut b . workdir ) . hint_text ( t ( " 在你的電腦上,例如 ~/Documents/專案 " ) ) . desired_width ( ui . available_width ( ) ) ) ;
} ) ;
form_row ( ui , " 模型可讀長度 " , p , | ui | {
egui ::ComboBox ::from_id_salt ( " ctx_tokens " ) . selected_text ( format! ( " {} K tokens " , b . context_tokens / 1000 ) ) . width ( 150.0 ) . show_ui ( ui , | ui | {
for t in [ 32_000 u64 , 128_000 , 200_000 , 400_000 , 1_000_000 ] {
ui . selectable_value ( & mut b . context_tokens , t , format! ( " {} K tokens " , t / 1000 ) ) ;
}
} ) ;
ui . label ( RichText ::new ( t ( " 用到 70% 時會把舊對話整理成摘要 " ) ) . size ( 11.5 ) . color ( p . text3 ) ) ;
} ) ;
ui . add_space ( 2.0 ) ;
2026-11-05 08:49:39 +00:00
hint ( ui , " 在它自己的電腦上可以自由做事(看不到你的檔案);在你的電腦上,改檔案和會改變東西的指令會先問你。密碼和金鑰檔一律不碰。 " , p ) ;
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 ! b . allow_rules . is_empty ( ) {
ui . add_space ( 6.0 ) ;
ui . label ( RichText ::new ( t ( " 已設定「一律允許」 " ) ) . size ( FONT_XS ) . color ( p . text2 ) ) ;
let mut remove = None ;
ui . horizontal_wrapped ( | ui | {
for ( i , g ) in b . allow_rules . iter ( ) . enumerate ( ) {
let chip = egui ::Button ::new ( RichText ::new ( format! ( " {} × " , grant_label ( g ) . trim_matches ( [ '「' , '」' ] ) ) ) . size ( 12.5 ) ) . fill ( p . elevated ) . stroke ( Stroke ::new ( 1.0 , p . border ) ) . corner_radius ( 14 ) ;
if ui . add ( chip ) . on_hover_text ( t ( " 移除這個允許 " ) ) . clicked ( ) {
remove = Some ( i ) ;
}
}
} ) ;
if let Some ( i ) = remove {
b . allow_rules . remove ( i ) ;
}
}
}
} ) ;
setting_card ( ui , p , | ui | {
setting_title ( ui , " 指示 " , " Bot 應該記得的事、說話風格… " , p ) ;
ui . add ( egui ::TextEdit ::multiline ( & mut b . instructions ) . hint_text ( t ( " 例如:用繁體中文回答,簡短一點。 " ) ) . desired_rows ( 4 ) . desired_width ( f32 ::INFINITY ) ) ;
} ) ;
if ! b . members . is_empty ( ) {
let others : Vec < ( u64 , String ) > = self . state . bots . iter ( ) . filter ( | x | x . id ! = b . id & & x . members . is_empty ( ) ) . map ( | x | ( x . id , x . name . clone ( ) ) ) . collect ( ) ;
setting_card ( ui , p , | ui | {
setting_title ( ui , " 群組成員 " , " 你傳訊息後,勾選的 Bot 依序回答(每個用自己的模型和設定),後面的會看到前面的回答。這個群組自己的模型不會用到。 " , p ) ;
for ( id , name ) in others {
let mut on = b . members . contains ( & id ) ;
if ui . checkbox ( & mut on , name ) . changed ( ) {
if on {
b . members . push ( id ) ;
} else if b . members . len ( ) > 1 {
b . members . retain ( | m | * m ! = id ) ;
}
}
}
} ) ;
}
setting_card ( ui , p , | ui | {
setting_title ( ui , " 記得的事 " , " 你在聊天裡請 Bot 記住的事(「記住…」),每次聊天都會帶著。 " , p ) ;
if b . memories . is_empty ( ) {
hint ( ui , " 還沒有。跟 Bot 說「記住我喜歡靠窗的座位」試試看。 " , p ) ;
}
let mut remove = None ;
for ( i , m ) in b . memories . iter ( ) . enumerate ( ) {
ui . horizontal ( | ui | {
ui . add ( egui ::Label ::new ( RichText ::new ( format! ( " {} . {m} " , i + 1 ) ) . size ( FONT_SM ) ) . wrap ( ) ) ;
ui . with_layout ( Layout ::right_to_left ( Align ::Center ) , | ui | {
if ui . add ( egui ::Button ::new ( RichText ::new ( t ( " 忘記 " ) ) . size ( 12.0 ) . color ( p . text3 ) ) . frame ( false ) ) . clicked ( ) {
remove = Some ( i ) ;
}
} ) ;
} ) ;
}
if let Some ( i ) = remove {
b . memories . remove ( i ) ;
}
} ) ;
} ) ;
dialog_footer ( ui , p , | ui | {
ui . checkbox ( & mut b . pinned , t ( " 釘選到側邊欄 " ) ) ;
ui . with_layout ( Layout ::right_to_left ( Align ::Center ) , | ui | {
if ui . add_enabled ( ! b . name . trim ( ) . is_empty ( ) & & ! b . model . trim ( ) . is_empty ( ) , primary_button ( if is_new { " 建立 Bot " } else { " 儲存 " } , p ) ) . clicked ( ) {
save = true ;
}
if ui . add ( secondary_button ( " 取消 " ) ) . clicked ( ) {
cancel = true ;
}
} ) ;
} ) ;
} ) ;
let open = ! dismissed ;
if save {
b . name = b . name . trim ( ) . to_string ( ) ;
b . model = b . model . trim ( ) . to_string ( ) ;
if is_new {
self . state . next_id + = 1 ;
self . state . selected = b . id ;
self . state . bots . push ( b ) ;
} else if let Some ( slot ) = self . state . bot_mut ( b . id ) {
// Keep messages that arrived while the editor was open.
let msgs = std ::mem ::take ( & mut slot . messages ) ;
* slot = b ;
slot . messages = msgs ;
}
self . mark ( ) ;
} else if open & & ! cancel {
self . edit_bot = Some ( ( b , is_new ) ) ;
}
}
fn settings ( & mut self , ctx : & egui ::Context ) {
if ! self . show_settings {
self . settings_was_open = false ;
self . vault . dialog = None ;
return ;
}
self . vault_dialogs ( ctx ) ;
// Re-read admin locks each time Settings opens.
if ! self . settings_was_open & & ! self . demo {
self . managed = policy ::read ( ) ;
}
self . settings_was_open = true ;
let p = self . pal ;
let mut fetch : Option < ProviderKind > = None ;
let mut theme_changed = false ;
let mut relaunch = false ;
let max_h = dialog_max_height ( ctx ) . min ( SETTINGS_HEIGHT ) ;
// The title bar is about 48 px; the rest is the page list and the page.
let page_h = max_h - 48.0 ;
let close = dialog ( ctx , " settings " , " 設定 " , SETTINGS_WIDTH , true , p , | ui | {
ui . horizontal_top ( | ui | {
ui . spacing_mut ( ) . item_spacing . x = 0.0 ;
Frame ::new ( ) . fill ( p . sidebar ) . corner_radius ( CornerRadius { sw : 16 , .. Default ::default ( ) } ) . inner_margin ( Margin ::symmetric ( 10 , 12 ) ) . show ( ui , | ui | {
// The page list narrows with a small window so the page keeps its room.
let nav_w = if ctx . content_rect ( ) . width ( ) < SETTINGS_WIDTH + 40.0 { 118.0 } else { SETTINGS_NAV_WIDTH } ;
ui . set_width ( nav_w - 20.0 ) ;
ui . set_min_height ( page_h - 24.0 ) ;
ui . vertical ( | ui | {
ui . spacing_mut ( ) . item_spacing . y = 2.0 ;
use crate ::icons ::Icon ;
for ( tab , name , icon ) in [ ( SettingsTab ::General , " 一般 " , Icon ::Gear ) , ( SettingsTab ::Providers , " 模型 " , Icon ::Sparkle ) , ( SettingsTab ::Channels , " 整合 " , Icon ::Chat ) , ( SettingsTab ::Computers , " 電腦 " , Icon ::Monitor ) , ( SettingsTab ::Review , " 權限 " , Icon ::Shield ) , ( SettingsTab ::Vault , " 保險庫 " , Icon ::Vault ) , ( SettingsTab ::Updates , " 更新 " , Icon ::CloudDownload ) ] {
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( ui . available_width ( ) , 32.0 ) , egui ::Sense ::click ( ) ) ;
let on = self . settings_tab = = tab | | ( tab = = SettingsTab ::General & & self . settings_tab = = SettingsTab ::Remote ) ;
if on | | r . hovered ( ) {
ui . painter ( ) . rect_filled ( rect , 8 , if on { p . active } else { p . hover } ) ;
}
let col = if on { p . text } else { p . text2 } ;
crate ::icons ::paint ( ui . painter ( ) , egui ::Rect ::from_center_size ( rect . left_center ( ) + vec2 ( 18.0 , 0.0 ) , vec2 ( 16.0 , 16.0 ) ) , icon , col ) ;
ui . painter ( ) . text ( rect . left_center ( ) + vec2 ( 34.0 , 0.0 ) , egui ::Align2 ::LEFT_CENTER , t ( name ) , egui ::FontId ::proportional ( FONT_BASE ) , col ) ;
if r . on_hover_cursor ( egui ::CursorIcon ::PointingHand ) . clicked ( ) {
self . settings_tab = tab ;
}
}
} ) ;
} ) ;
let r = ui . min_rect ( ) ;
ui . painter ( ) . vline ( r . right ( ) , r . y_range ( ) , Stroke ::new ( 1.0 , p . border ) ) ;
ui . vertical ( | ui | {
ui . spacing_mut ( ) . item_spacing = vec2 ( 8.0 , 6.0 ) ;
dialog_body ( ui , ( " settings-page " , self . settings_tab as u8 ) , page_h , | ui | match self . settings_tab {
SettingsTab ::General = > {
section ( ui , " 帳戶 " , p ) ;
let acct = if self . state . remote . host_token . is_empty ( ) { t ( " 尚未登入帳號 · 手機、帳號、團隊、備份 " ) } else { format! ( " {} · {} " , self . state . remote . relay_user , self . state . remote . relay_url ) } ;
setting_row ( ui , p , " 帳戶 " , & acct , | ui | {
if ui . add ( secondary_button ( " 管理 " ) ) . clicked ( ) {
self . settings_tab = SettingsTab ::Remote ;
}
} ) ;
section ( ui , " 外觀 " , p ) ;
setting_row ( ui , p , " 主題 " , " 為 AnyBot 選擇淺色或深色外觀 " , | ui | {
for ( th , n ) in [ ( Theme ::System , " 跟隨系統 " ) , ( Theme ::Dark , " 深色 " ) , ( Theme ::Light , " 淺色 " ) ] {
if ui . selectable_value ( & mut self . state . theme , th , t ( n ) ) . clicked ( ) {
theme_changed = true ;
}
}
} ) ;
setting_row ( ui , p , " 強調色 " , " 用 Bot 的顏色為 1:1 Bot 訊息和卡片上色,或保持黑色 " , | ui | {
for ( a , n ) in [ ( Accent ::Black , " 黑色 " ) , ( Accent ::BotColor , " Bot 顏色 " ) ] {
if ui . selectable_value ( & mut self . state . accent , a , t ( n ) ) . clicked ( ) {
self . dirty = true ;
}
}
} ) ;
setting_row ( ui , p , " 字型 " , " AnyBot 畫面使用的字型 " , | ui | {
let cur = crate ::font_choice ( ) ;
for ( v , n ) in [ ( " system " , " 系統字型 " ) , ( " huninn " , " 粉圓體 " ) ] {
if ui . selectable_label ( cur = = v , t ( n ) ) . clicked ( ) & & cur ! = v {
if let Some ( d ) = State ::path ( ) . parent ( ) {
let _ = std ::fs ::create_dir_all ( d ) ;
let _ = std ::fs ::write ( d . join ( " font.txt " ) , v ) ;
}
crate ::load_fonts ( ctx ) ;
}
}
} ) ;
setting_row ( ui , p , " 語言 " , " 選擇 AnyBot 選單和 Bot 回答使用的語言 " , | ui | {
let cur = self . state . language . clone ( ) ;
let shown = if cur = = " auto " { format! ( " {} ( {} ) " , t ( " 自動偵測 " ) , crate ::i18n ::endonym ( & crate ::i18n ::system ( ) ) ) } else { crate ::i18n ::endonym ( & cur ) . to_string ( ) } ;
egui ::ComboBox ::from_id_salt ( " language " ) . selected_text ( shown ) . height ( 360.0 ) . show_ui ( ui , | ui | {
let mut pick = | ui : & mut egui ::Ui , v : & str , label : String | {
if ui . selectable_label ( cur = = v , label ) . clicked ( ) {
self . state . language = v . to_string ( ) ;
crate ::i18n ::set ( v ) ;
self . dirty = true ;
}
} ;
pick ( ui , " auto " , t ( " 自動偵測 " ) ) ;
let mut rows : Vec < _ > = crate ::i18n ::LANGS . to_vec ( ) ;
rows . sort_by ( | a , b | a . 1. cmp ( b . 1 ) ) ;
for ( tag , endo , eng ) in rows {
pick ( ui , tag , if endo = = eng { endo . to_string ( ) } else { format! ( " {endo} · {eng} " ) } ) ;
}
} ) ;
} ) ;
if ! matches! ( crate ::i18n ::lang ( ) . as_str ( ) , " zh-TW " | " zh-CN " | " en " | " ja " ) {
hint ( ui , " 選單會用英文顯示; Bot 會用你選的語言回答。 " , p ) ;
}
section ( ui , " 通知 " , p ) ;
setting_row ( ui , p , " 通知 " , " Bot 完成任務或等你處理時通知你 " , | ui | {
if ui . add ( egui ::Checkbox ::without_text ( & mut self . state . notify ) ) . changed ( ) {
self . dirty = true ;
}
} ) ;
hint ( ui , " 視窗不在最前面,或 90 秒沒有動鍵盤滑鼠,就會通知。登入帳號後也會推播到手機。 " , p ) ;
section ( ui , " Bot " , p ) ;
setting_row ( ui , p , " 時區 " , " 設定 Bot 用於排程和提醒的時區 " , | ui | {
if ui . add ( egui ::TextEdit ::singleline ( & mut self . state . timezone ) . desired_width ( 150.0 ) ) . changed ( ) {
self . dirty = true ;
}
if ui . add ( secondary_button ( " 用這台電腦的 " ) ) . clicked ( ) {
self . state . timezone = crate ::routines ::local_timezone ( ) ;
self . dirty = true ;
}
} ) ;
if self . state . timezone . parse ::< chrono_tz ::Tz > ( ) . is_err ( ) {
ui . label ( RichText ::new ( t ( " 看不懂這個時區,請用像 Asia/Taipei 這樣的名稱 " ) ) . size ( 11.5 ) . color ( p . danger ) ) ;
}
section ( ui , " 系統 " , p ) ;
setting_row ( ui , p , " 資料 " , " 聊天記錄和金鑰只存在這台電腦上 " , | ui | {
if ui . add ( secondary_button ( if cfg! ( target_os = " macos " ) { " 在 Finder 中顯示 " } else { " 打開所在資料夾 " } ) ) . clicked ( ) {
crate ::preview ::reveal ( & State ::path ( ) ) ;
}
} ) ;
}
SettingsTab ::Updates = > {
section ( ui , " AnyBot 更新 " , p ) ;
setting_row ( ui , p , " 版本 " , & version_label ( self . exe_built ) , | ui | {
if let Some ( tm ) = self . newer_build {
ui . label ( RichText ::new ( format! ( " {} {} " , t ( " 有更新的版本: " ) , version_label ( Some ( tm ) ) ) ) . size ( 12.0 ) . color ( p . accent ) ) ;
} else {
ui . label ( RichText ::new ( t ( " 已是最新版本 " ) ) . size ( 12.0 ) . color ( p . text3 ) ) ;
}
} ) ;
setting_row ( ui , p , " 自動更新 " , " 在你離開時自動換成新版本,你的 Bot 和工作都會保留 " , | ui | {
if ui . add ( egui ::Checkbox ::without_text ( & mut self . state . auto_update_when_away ) ) . changed ( ) {
self . dirty = true ;
}
} ) ;
hint ( ui , " 螢幕鎖定或螢幕保護程式開著、10 分鐘沒動作、而且沒有 Bot 在工作時,才會自己換成新版本。 " , p ) ;
setting_row ( ui , p , " 重新啟動 " , " App 不太對勁時用:進行中的工作會停下,聊天記錄都會保留 " , | ui | {
let label = if self . relaunching { " 正在重新啟動… " } else { " 重新啟動 AnyBot " } ;
if ui . add_enabled ( ! self . relaunching , secondary_button ( label ) ) . clicked ( ) {
relaunch = true ;
}
} ) ;
}
SettingsTab ::Vault = > {
self . vault_page ( ui ) ;
}
SettingsTab ::Computers = > {
self . computers_page ( ui ) ;
}
SettingsTab ::Remote = > {
if ui . add ( egui ::Button ::new ( RichText ::new ( format! ( " ‹ {} " , t ( " 一般 " ) ) ) . size ( FONT_SM ) . color ( p . text2 ) ) . frame ( false ) ) . clicked ( ) {
self . settings_tab = SettingsTab ::General ;
}
self . remote_page ( ui , ctx ) ;
}
SettingsTab ::Channels = > {
section ( ui , " 訊息管道 " , p ) ;
self . channels_page ( ui , ctx ) ;
section ( ui , " 連接器 " , p ) ;
self . connectors_page ( ui , ctx ) ;
}
SettingsTab ::Review = > {
Frame ::new ( ) . fill ( p . accent . gamma_multiply ( 0.07 ) ) . stroke ( Stroke ::new ( 1.0 , p . accent . gamma_multiply ( 0.45 ) ) ) . corner_radius ( 12 ) . inner_margin ( Margin ::symmetric ( 16 , 14 ) ) . show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
crate ::icons ::label ( ui , crate ::icons ::Icon ::Zap , & t ( " 直接操作(像 Grok Bot 一樣) " ) , FONT_BASE , p . text ) ;
hint ( ui , " 一個開關:讓 Bot 直接使用這台電腦和你的瀏覽器,不再一個個問你 " , p ) ;
ui . add_space ( 4.0 ) ;
let allowed = self . managed . direct . allows ( ) & & self . managed . local . allows ( ) ;
let mut on = self . direct_on ( ) ;
if ui . add_enabled ( allowed , egui ::Checkbox ::new ( & mut on , t ( " 打開直接操作 " ) ) ) . changed ( ) {
if on {
self . enable_direct_mode ( ) ;
} else {
self . disable_direct_mode ( ) ;
}
}
if ! allowed {
crate ::icons ::label ( ui , crate ::icons ::Icon ::Lock , & t ( " 由你的管理員設定,無法變更 " ) , 12.0 , p . danger ) ;
}
hint ( ui , " 仍會擋下: 危險指令( sudo、格式化…) 、密碼和金鑰檔、AnyBot 自己的資料, 以及你在「自動審核」寫的「一律詢問」規則( 預設: 刪除檔案、付款、用你的帳號發文或傳訊息) 。macOS 第一次讓 AnyBot 控制瀏覽器時還是會自己問一次,按「好」就行。 " , p ) ;
} ) ;
ui . add_space ( 12.0 ) ;
setting_card ( ui , p , | ui | {
setting_title ( ui , " 自動審核 " , " 依照自訂規則審核 Bot 的操作 " , p ) ;
if ui . checkbox ( & mut self . state . auto_review , t ( " 開啟自動審核 " ) ) . changed ( ) {
self . dirty = true ;
}
hint ( ui , " 為每種操作寫一條簡短的自然語言規則。規則衝突時,「先詢問」優先。這些規則只對你生效。內建的安全檢查一律有效。 " , p ) ;
if ! self . state . auto_review {
hint ( ui , " 自動審核關閉時不會使用規則 " , p ) ;
}
} ) ;
for ( title , desc , which ) in [ ( " 「無需詢問直接允許」規則 " , " 操作符合這些規則時,自動繼續 " , 0 ) , ( " 「一律詢問」規則 " , " 操作符合這些規則時,停下來詢問 " , 1 ) ] {
setting_card ( ui , p , | ui | {
setting_title ( ui , title , desc , p ) ;
let rules = if which = = 0 { & mut self . state . review_allow_rules } else { & mut self . state . review_ask_rules } ;
let mut remove = None ;
for ( i , r ) in rules . iter ( ) . enumerate ( ) {
Frame ::new ( ) . fill ( p . elevated ) . corner_radius ( 8 ) . inner_margin ( Margin ::symmetric ( 10 , 4 ) ) . show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
ui . horizontal ( | ui | {
ui . label ( r ) ;
ui . with_layout ( Layout ::right_to_left ( Align ::Center ) , | ui | {
if ui . add ( egui ::Button ::new ( RichText ::new ( t ( " 刪除 " ) ) . size ( 12.0 ) . color ( p . text3 ) ) . frame ( false ) ) . clicked ( ) {
remove = Some ( i ) ;
}
} ) ;
} ) ;
} ) ;
}
if let Some ( i ) = remove {
rules . remove ( i ) ;
self . dirty = true ;
}
let draft = if which = = 0 { & mut self . new_allow_rule } else { & mut self . new_ask_rule } ;
ui . add_space ( 2.0 ) ;
ui . horizontal ( | ui | {
let w = ui . available_width ( ) - 96.0 ;
ui . add ( egui ::TextEdit ::singleline ( draft ) . hint_text ( if which = = 0 { " 例如:在工作資料夾裡建立文字檔 " } else { " 例如:刪除任何檔案 " } ) . desired_width ( w ) ) ;
if ui . add_enabled ( ! draft . trim ( ) . is_empty ( ) , secondary_button ( " 新增規則 " ) ) . clicked ( ) {
rules . push ( draft . trim ( ) . to_string ( ) ) ;
draft . clear ( ) ;
self . dirty = true ;
}
} ) ;
} ) ;
}
}
SettingsTab ::Providers = > {
ui . horizontal_top ( | ui | {
ui . vertical ( | ui | {
ui . set_width ( PROVIDER_LIST_WIDTH ) ;
ui . spacing_mut ( ) . item_spacing . y = 2.0 ;
for k in ALL_PROVIDERS {
let ps = self . state . provider ( k ) ;
let ready = ! k . needs_key ( ) | | ! providers ::resolve_key ( k , & ps . api_key ) . is_empty ( ) ;
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( PROVIDER_LIST_WIDTH , 32.0 ) , egui ::Sense ::click ( ) ) ;
let on = self . settings_provider = = k ;
if on | | r . hovered ( ) {
ui . painter ( ) . rect_filled ( rect , 8 , if on { p . active } else { p . hover } ) ;
}
let dot = rect . left_center ( ) + vec2 ( 14.0 , 0.0 ) ;
if ready {
ui . painter ( ) . circle_filled ( dot , 4.0 , Color32 ::from_rgb ( 0x2e , 0xa0 , 0x43 ) ) ;
} else {
ui . painter ( ) . circle_stroke ( dot , 3.5 , Stroke ::new ( 1.2 , p . text3 ) ) ;
}
ui . painter ( ) . text ( rect . left_center ( ) + vec2 ( 28.0 , 0.0 ) , egui ::Align2 ::LEFT_CENTER , k . label ( ) , egui ::FontId ::proportional ( FONT_BASE ) , if on { p . text } else { p . text2 } ) ;
let tip = if ready { " 已可使用 " } else { " 還沒設定金鑰 " } ;
if r . on_hover_text ( tip ) . clicked ( ) {
self . settings_provider = k ;
}
}
} ) ;
ui . add_space ( 16.0 ) ;
ui . vertical ( | ui | {
let k = self . settings_provider ;
let w = ui . available_width ( ) ;
ui . label ( RichText ::new ( k . label ( ) ) . strong ( ) . size ( FONT_XL ) ) ;
hint ( ui , k . key_help ( ) , p ) ;
ui . add_space ( 6.0 ) ;
let ps = self . state . provider_mut ( k ) ;
let mut changed = false ;
if k . needs_key ( ) | | k = = ProviderKind ::Custom {
field_label ( ui , " API 金鑰 " , p ) ;
changed | = ui . add ( egui ::TextEdit ::singleline ( & mut ps . api_key ) . password ( true ) . hint_text ( k . env_var ( ) . map ( | v | format! ( " 貼上金鑰,或設定環境變數 {v} " ) ) . unwrap_or_else ( | | " (選填) " . into ( ) ) ) . desired_width ( w ) ) . changed ( ) ;
}
field_label ( ui , " 服務網址 " , p ) ;
changed | = ui . add ( egui ::TextEdit ::singleline ( & mut ps . base_url ) . hint_text ( k . default_base_url ( ) ) . desired_width ( w ) ) . changed ( ) ;
field_label ( ui , " 新 Bot 預設模型 " , p ) ;
changed | = ui . add ( egui ::TextEdit ::singleline ( & mut ps . default_model ) . hint_text ( k . suggested_model ( ) ) . desired_width ( w ) ) . changed ( ) ;
let count = ps . known_models . len ( ) ;
if changed {
self . dirty = true ;
}
ui . add_space ( 12.0 ) ;
ui . horizontal ( | ui | {
let busy = self . model_fetch . contains_key ( & k ) ;
if ui . add_enabled ( ! busy , secondary_button ( " 測試連線並取得模型清單 " ) ) . clicked ( ) {
fetch = Some ( k ) ;
}
if busy {
ui . spinner ( ) ;
}
} ) ;
if let Some ( s ) = self . fetch_status . get ( & k ) {
hint ( ui , s . clone ( ) , p ) ;
} else if count > 0 {
hint ( ui , format! ( " 已記住 {count} 個模型 " ) , p ) ;
}
} ) ;
} ) ;
}
} ) ;
} ) ;
} ) ;
} ) ;
let open = ! close ;
if let Some ( k ) = fetch {
self . fetch_models ( k ) ;
}
if theme_changed {
self . apply_theme ( ctx ) ;
self . dirty = true ;
}
self . show_settings = open ;
if relaunch {
self . relaunch ( ctx ) ;
}
}
/// Every 10 s, see whether a newer build replaced the program file (then the banner shows).
fn check_for_newer_build ( & mut self ) {
if self . demo {
return ;
}
// Noticing a new build is a cheap file check, so the banner keeps up quickly (estimated 10 s).
if self . last_build_check . elapsed ( ) . as_secs ( ) > = 10 {
self . last_build_check = Instant ::now ( ) ;
self . newer_build = match ( exe_modified ( ) , self . exe_built ) {
( Some ( now ) , Some ( started ) ) if now > started = > Some ( now ) ,
_ = > None ,
} ;
}
// Switching to it by itself follows the original's schedule and away rule.
if Instant ::now ( ) > = self . next_update_apply {
let jitter = ( chrono ::Utc ::now ( ) . timestamp_subsec_nanos ( ) as f64 ) / 1e9 ;
self . next_update_apply = Instant ::now ( ) + std ::time ::Duration ::from_secs ( notify ::next_update_check_secs ( jitter ) ) ;
let bots_idle = self . streams . is_empty ( ) & & self . approvals . is_empty ( ) ;
let ready = self . newer_build . is_some ( ) ;
if ready
& & notify ::should_apply_update_now (
self . state . auto_update_when_away ,
ready ,
notify ::screen_locked ( ) ,
notify ::screensaver_active ( ) ,
notify ::system_idle_secs ( ) ,
bots_idle ,
)
{
self . relaunch_requested = true ;
}
}
}
/// Save, start a fresh copy of the app, and close this one.
fn relaunch ( & mut self , ctx : & egui ::Context ) {
let ids : Vec < u64 > = self . streams . keys ( ) . copied ( ) . collect ( ) ;
for id in ids {
self . stop ( id ) ;
}
tools ::stop_all ( ) ;
crate ::cloudbox ::shutdown ( ) ;
crate ::mcp ::shutdown ( ) ;
if ! self . demo {
self . state . save ( ) ;
}
match std ::env ::current_exe ( ) . and_then ( | exe | relaunch_command ( std ::process ::id ( ) , & exe ) . spawn ( ) ) {
Ok ( _ ) = > {
self . relaunching = true ;
ctx . send_viewport_cmd ( egui ::ViewportCommand ::Close ) ;
}
Err ( e ) = > self . notice = Some ( format! ( " 沒辦法重新啟動: {e} 。請手動關掉再打開 AnyBot。 " ) ) ,
}
}
fn delete_dialog ( & mut self , ctx : & egui ::Context ) {
let Some ( id ) = self . confirm_delete else { return } ;
let name = self . state . bot ( id ) . map ( | b | b . name . clone ( ) ) . unwrap_or_default ( ) ;
let mut close = false ;
let p = self . pal ;
let dismissed = dialog ( ctx , " delete-bot " , " 刪除 Bot? " , 380.0 , true , p , | ui | {
dialog_body ( ui , " delete-bot-body " , 200.0 , | ui | {
ui . label ( RichText ::new ( format! ( " 這會刪除「 {name} 」和它的聊天記錄。此操作無法復原。 " ) ) . color ( p . text2 ) ) ;
} ) ;
dialog_footer ( ui , p , | ui | { ui . with_layout ( Layout ::right_to_left ( Align ::Center ) , | ui | {
if ui . add ( danger_button ( " 刪除 " , p ) ) . clicked ( ) {
self . stop ( id ) ;
self . state . bots . retain ( | b | b . id ! = id ) ;
2026-11-05 06:25:01 +00:00
// The last one gone: back to the getting-started guide.
self . state . fix_primary ( ) ;
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 . dirty = true ;
close = true ;
}
if ui . add ( secondary_button ( " 取消 " ) ) . clicked ( ) {
close = true ;
}
} ) ; } ) ;
} ) ;
if close | | dismissed {
self . confirm_delete = None ;
}
}
fn handle_screenshot ( & mut self , ctx : & egui ::Context ) {
let Some ( path ) = self . screenshot . clone ( ) else { return } ;
self . frames + = 1 ;
let need = std ::env ::var ( " ANYBOT_SHOT_FRAMES " ) . ok ( ) . and_then ( | v | v . parse ( ) . ok ( ) ) . unwrap_or ( SCREENSHOT_FRAMES ) ;
// ANYBOT_SHOT_RESIZE=WxH: halfway through, resize the open window (the same path as the
// person dragging its edge), so the picture shows the layout after a live resize.
if self . frames = = need / 2 {
if let Some ( ( w , h ) ) = std ::env ::var ( " ANYBOT_SHOT_RESIZE " ) . ok ( ) . and_then ( | v | {
let ( w , h ) = v . split_once ( 'x' ) ? ;
Some ( ( w . parse ::< f32 > ( ) . ok ( ) ? , h . parse ::< f32 > ( ) . ok ( ) ? ) )
} ) {
ctx . send_viewport_cmd ( egui ::ViewportCommand ::InnerSize ( vec2 ( w , h ) ) ) ;
}
}
2026-11-05 00:33:23 +00:00
// ANYBOT_SHOT_PAGE=id: open that page late (a screen gets its pages from the brain first).
if self . frames = = need * 3 / 4 {
if let Some ( id ) = std ::env ::var ( " ANYBOT_SHOT_PAGE " ) . ok ( ) . and_then ( | v | v . parse ( ) . ok ( ) ) {
self . page_view = Some ( ( id , None ) ) ;
}
}
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 . frames > = need & & ! self . shot_requested {
self . shot_requested = true ;
let slot = self . shot . clone ( ) ;
let repaint = ctx . clone ( ) ;
ctx . send_viewport_cmd ( egui ::ViewportCommand ::Screenshot ( egui ::ScreenshotCallback ::new ( move | img | {
* slot . lock ( ) . unwrap ( ) = Some ( img ) ;
repaint . request_repaint ( ) ;
} ) ) ) ;
}
let shot = self . shot . lock ( ) . unwrap ( ) . take ( ) ;
if let Some ( img ) = shot {
let [ w , h ] = img . size ;
let bytes : Vec < u8 > = img . pixels . iter ( ) . flat_map ( | c | c . to_array ( ) ) . collect ( ) ;
if let Some ( dir ) = path . parent ( ) {
let _ = std ::fs ::create_dir_all ( dir ) ;
}
match image ::RgbaImage ::from_raw ( w as u32 , h as u32 , bytes ) {
Some ( buf ) = > {
if let Err ( e ) = buf . save ( & path ) {
eprintln! ( " could not save screenshot: {e} " ) ;
}
}
None = > eprintln! ( " screenshot had an unexpected size " ) ,
}
ctx . send_viewport_cmd ( egui ::ViewportCommand ::Close ) ;
}
ctx . request_repaint ( ) ;
}
}
impl App {
/// Everything that keeps bots working, without drawing (the window runs it every frame;
/// `anybot --serve` runs it on its own).
fn background ( & mut self , ctx : & egui ::Context ) {
2026-11-04 09:46:43 +00:00
self . remote_start ( ctx ) ;
self . mirror_tick ( ) ;
// A screen of another AnyBot's brain: its bots work there, not here.
if self . mir . on {
self . remote_tick ( ctx ) ;
2026-11-04 11:30:32 +00:00
self . poll_fetches ( ) ;
2026-11-04 09:46:43 +00:00
return ;
}
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 . drain_events ( ctx ) ;
if ! self . demo {
self . channels_poll ( ctx ) ;
}
self . remote_tick ( ctx ) ;
self . tick_routines ( ctx ) ;
self . watchdog ( ) ;
self . poll_fetches ( ) ;
self . poll_import ( ) ;
}
fn save_if_dirty ( & mut self ) {
if self . dirty & & ! self . demo & & ( self . streams . is_empty ( ) | | self . last_save . elapsed ( ) . as_secs ( ) > = 2 ) {
2026-11-04 09:46:43 +00:00
if self . mir . on {
self . mirror_save ( ) ;
} else {
self . state . save ( ) ;
}
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 . dirty = false ;
self . last_save = Instant ::now ( ) ;
}
}
/// `anybot --serve` with ANYBOT_ACCOUNT=url|user and ANYBOT_ACCOUNT_PASSWORD: join the account
/// (and, with ANYBOT_RESTORE_FROM_ACCOUNT=1, start once from the newest backup).
fn serve_join ( & mut self ) {
let ( Ok ( acct ) , Ok ( pw ) ) = ( std ::env ::var ( " ANYBOT_ACCOUNT " ) , std ::env ::var ( " ANYBOT_ACCOUNT_PASSWORD " ) ) else { return } ;
if ! self . state . remote . host_token . is_empty ( ) {
return ;
}
let Some ( ( url , user ) ) = acct . rsplit_once ( '|' ) else { return } ;
2026-11-04 12:38:45 +00:00
// ANYBOT_ACCOUNT_CREATE=1 (the server's own brain, first start): make the account if it isn't there.
let mut r = crate ::remote ::relay_join ( & self . state . remote , url , user , & pw , false , " " ) ;
if r . is_err ( ) & & std ::env ::var ( " ANYBOT_ACCOUNT_CREATE " ) . map ( | v | v = = " 1 " ) . unwrap_or ( false ) {
let invite = std ::env ::var ( " ANYBOT_RELAY_INVITE " ) . unwrap_or_default ( ) ;
if let Ok ( s ) = crate ::remote ::relay_join ( & self . state . remote , url , user , & pw , true , & invite ) {
eprintln! ( " AnyBot: 已建立帳號 {user} " ) ;
r = Ok ( s ) ;
}
}
match 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
Ok ( s ) = > {
self . state . remote = s ;
self . dirty = true ;
eprintln! ( " AnyBot: 已加入帳號 {user} ( {url} ) " ) ;
}
Err ( e ) = > {
eprintln! ( " AnyBot: 登入帳號失敗( 30 秒後再試): {e} " ) ;
return ;
}
}
let marker = State ::path ( ) . with_file_name ( " restored-from-account " ) ;
if std ::env ::var ( " ANYBOT_RESTORE_FROM_ACCOUNT " ) . map ( | v | v = = " 1 " ) . unwrap_or ( false ) & & ! marker . exists ( ) {
match crate ::remote ::restore ( url , user , & pw ) . and_then ( | b | serde_json ::from_slice ::< State > ( & b ) . map_err ( | e | e . to_string ( ) ) ) {
Ok ( mut st ) = > {
st . remote = self . state . remote . clone ( ) ;
eprintln! ( " AnyBot: 已從帳號備份接手 {} 個 Bot " , st . bots . len ( ) ) ;
self . state = st ;
self . dirty = true ;
let _ = std ::fs ::write ( & marker , chrono ::Utc ::now ( ) . to_rfc3339 ( ) ) ;
}
Err ( e ) = > eprintln! ( " AnyBot: 沒有接手備份: {e} " ) ,
}
}
}
/// `anybot --serve`: no window. The bots, routines, triggers, channels and the phone page keep
/// running (e.g. on an always-on Mac mini or a server); the person uses the phone.
/// For testing a bot's tool use: `anybot --ask <message>` sends one message to the primary bot
/// with no window, refuses every approval (and says so), and prints the turn's steps. Run it
/// with ANYBOT_DATA_DIR pointing at a copy of the data folder. It doesn't connect to the
/// account, channels or routines.
pub fn ask_cli ( ctx : & egui ::Context , text : & str ) -> ! {
let mut app = App ::new ( ctx , None ) ;
app . headless = true ;
let id = app . state . primary_bot ;
let start = app . state . bot ( id ) . map ( | b | b . messages . len ( ) ) . unwrap_or ( 0 ) ;
app . drafts . insert ( id , text . to_string ( ) ) ;
app . send ( ctx , id ) ;
let t0 = Instant ::now ( ) ;
let limit = std ::env ::var ( " ANYBOT_ASK_SECS " ) . ok ( ) . and_then ( | v | v . parse ( ) . ok ( ) ) . unwrap_or ( 900 u64 ) ;
while app . streams . contains_key ( & id ) & & t0 . elapsed ( ) . as_secs ( ) < limit {
if let Some ( a ) = app . approvals . remove ( & id ) {
println! ( " ⚠️ 要求核准(自動拒絕): {} — {} " , a . tool , a . reason ) ;
let _ = a . reply . send ( ApprovalAnswer ::Reject ) ;
}
app . drain_events ( ctx ) ;
app . watchdog ( ) ;
2026-11-04 09:46:43 +00:00
ctx . run_ui ( Default ::default ( ) , | _ | { } ) . textures_delta . clear ( ) ;
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
std ::thread ::sleep ( std ::time ::Duration ::from_millis ( 100 ) ) ;
}
let secs = t0 . elapsed ( ) . as_secs ( ) ;
2026-11-06 15:12:53 +00:00
// 自己進化 comes after the turn: wait for it when the turn was long enough to learn from.
let turn_msgs = app . state . bot ( id ) . and_then ( | b | b . messages . get ( start .. ) ) . map ( | m | m . to_vec ( ) ) . unwrap_or_default ( ) ;
if crate ::memory ::worth_learning ( & turn_msgs ) {
let before = app . state . skills . clone ( ) ;
let t1 = Instant ::now ( ) ;
while app . state . skills = = before & & t1 . elapsed ( ) . as_secs ( ) < 180 {
app . drain_events ( ctx ) ;
std ::thread ::sleep ( std ::time ::Duration ::from_millis ( 200 ) ) ;
}
match app . state . skills . iter ( ) . find ( | k | ! before . contains ( k ) ) {
Some ( k ) = > println! ( " 📚 學到( {} 秒): {} — {} \n {} \n " , t1 . elapsed ( ) . as_secs ( ) , k . name , k . description , k . instructions ) ,
None = > println! ( " 📚 這次沒有存技能( {} 秒) " , t1 . elapsed ( ) . as_secs ( ) ) ,
}
}
2026-11-05 07:48:22 +00:00
// The chat can shrink (compaction) between the snapshot and now: slice safely.
let msgs = app . state . bot ( id ) . and_then ( | b | b . messages . get ( start .. ) ) . map ( | m | m . to_vec ( ) ) . 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 clip = | s : & str , n : usize | -> String {
let one = s . replace ( '\n' , " ⏎ " ) ;
if one . chars ( ) . count ( ) > n { format! ( " {} … " , one . chars ( ) . take ( n ) . collect ::< String > ( ) ) } else { one }
} ;
let full = std ::env ::var ( " ANYBOT_ASK_FULL " ) . is_ok ( ) ;
let mut steps = 0 ;
for m in & msgs {
match m . role {
Role ::User = > println! ( " 👤 {} " , m . text ) ,
Role ::Assistant = > {
if ! m . text . trim ( ) . is_empty ( ) {
println! ( " 🤖 {} " , if full | | m . tool_calls . is_empty ( ) { m . text . clone ( ) } else { clip ( & m . text , 300 ) } ) ;
}
for c in & m . tool_calls {
steps + = 1 ;
println! ( " 🔧 {} {} " , c . name , clip ( & c . args , 200 ) ) ;
}
}
Role ::Tool = > println! ( " ↳ {} {} " , if m . images . is_empty ( ) { " " } else { " [附截圖] " } , clip ( & m . text , if full { 4000 } else { 400 } ) ) ,
}
}
println! ( " —— {steps} 個工具步驟, {secs} 秒 " ) ;
app . state . save ( ) ;
std ::process ::exit ( 0 )
}
pub fn serve ( ctx : & egui ::Context ) -> ! {
let mut app = App ::new ( ctx , None ) ;
app . headless = true ;
if let Ok ( pw ) = std ::env ::var ( " ANYBOT_PHONE_PASSWORD " ) {
if pw . chars ( ) . count ( ) > = 8 & & ! crate ::httpd ::check_password ( & pw , & app . state . remote . password ) {
app . state . remote . password = crate ::httpd ::hash_password ( & pw ) ;
app . state . remote . devices . clear ( ) ;
}
app . state . remote . lan = true ;
app . dirty = true ;
}
if let Ok ( name ) = std ::env ::var ( " ANYBOT_HOST_NAME " ) {
if ! name . trim ( ) . is_empty ( ) & & app . state . remote . host_name ! = name {
app . state . remote . host_name = name ;
app . dirty = true ;
}
}
2026-11-04 09:46:43 +00:00
// 手腳: a computer the brain's bots may operate (screen, files, browser).
if std ::env ::var ( " ANYBOT_DEVICE_LINK " ) . map ( | v | v = = " 1 " ) . unwrap_or ( false ) & & ! app . state . remote . device_link {
app . state . remote . device_link = true ;
app . dirty = true ;
}
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
app . serve_join ( ) ;
app . background ( ctx ) ;
if let Some ( port ) = crate ::remote ::lan_port ( ) {
for a in crate ::remote ::addresses ( port ) {
eprintln! ( " AnyBot: 手機可以打開 {a} " ) ;
}
}
if ! app . state . remote . relay_url . is_empty ( ) {
eprintln! ( " AnyBot: 透過帳號 {} 連線( {} ) " , app . state . remote . relay_user , app . state . remote . relay_url ) ;
}
if app . state . remote . password . is_empty ( ) & & app . state . remote . host_token . is_empty ( ) & & std ::env ::var ( " ANYBOT_ACCOUNT " ) . is_err ( ) {
eprintln! ( " AnyBot: 還沒有設定手機密碼或帳號。用 ANYBOT_PHONE_PASSWORD=… 或 ANYBOT_ACCOUNT=網址|帳號 + ANYBOT_ACCOUNT_PASSWORD=… 啟動。 " ) ;
}
let mut last_try = Instant ::now ( ) ;
2026-11-04 09:46:43 +00:00
let mut last_brain = Instant ::now ( ) - std ::time ::Duration ::from_secs ( 60 ) ;
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
loop {
// The account server may not be up yet (or the account not made yet): keep trying.
if app . state . remote . host_token . is_empty ( ) & & last_try . elapsed ( ) . as_secs ( ) > = 30 {
last_try = Instant ::now ( ) ;
app . serve_join ( ) ;
}
2026-11-04 09:46:43 +00:00
// ANYBOT_BRAIN=名稱: show that AnyBot's bots instead of running its own.
if let Ok ( want ) = std ::env ::var ( " ANYBOT_BRAIN " ) {
if ! want . trim ( ) . is_empty ( ) & & ! app . state . remote . host_token . is_empty ( ) & & app . state . remote . brain_name ! = want & & last_brain . elapsed ( ) . as_secs ( ) > = 20 {
last_brain = Instant ::now ( ) ;
if let Some ( ( id , name , _ ) ) = crate ::remote ::devices_online ( & app . state . remote ) . into_iter ( ) . find ( | ( _ , n , _ ) | n = = want . trim ( ) ) {
eprintln! ( " AnyBot: 用「 {name} 」當大腦,這台當手腳 " ) ;
app . mir . want = Some ( ( id , name ) ) ;
app . state . remote . device_link = true ;
}
}
}
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
app . background ( ctx ) ;
app . save_if_dirty ( ) ;
// egui wants a frame now and then to forget old state; nothing is drawn.
2026-11-04 09:46:43 +00:00
ctx . run_ui ( Default ::default ( ) , | _ | { } ) . textures_delta . clear ( ) ;
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
std ::thread ::sleep ( std ::time ::Duration ::from_millis ( if app . streams . is_empty ( ) { 250 } else { 60 } ) ) ;
}
}
}
impl eframe ::App for App {
/// Screenshot mode only: ANYBOT_SHOT_CLICK="x,y@frame[;x,y@frame…][:right]" clicks there at that
/// frame (to check buttons really respond).
fn raw_input_hook ( & mut self , _ctx : & egui ::Context , raw : & mut egui ::RawInput ) {
2026-11-04 11:30:32 +00:00
if self . screenshot . is_none ( ) {
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 ;
}
// ANYBOT_SHOT_DRAG="x1,y1>x2,y2@frame": press, move over 12 frames, release.
if let Ok ( d ) = std ::env ::var ( " ANYBOT_SHOT_DRAG " ) {
if let Some ( ( path , at ) ) = d . split_once ( '@' ) {
let at : u32 = at . trim ( ) . parse ( ) . unwrap_or ( 30 ) ;
let pts : Vec < f32 > = path . split ( [ '>' , ',' ] ) . filter_map ( | v | v . trim ( ) . parse ( ) . ok ( ) ) . collect ( ) ;
if pts . len ( ) = = 4 {
let ( a , b ) = ( egui ::pos2 ( pts [ 0 ] , pts [ 1 ] ) , egui ::pos2 ( pts [ 2 ] , pts [ 3 ] ) ) ;
let f = self . frames + 1 ;
if f = = at {
raw . events . push ( egui ::Event ::PointerMoved ( a ) ) ;
} else if f = = at + 1 {
raw . events . push ( egui ::Event ::PointerButton { pos : a , button : egui ::PointerButton ::Primary , pressed : true , modifiers : Default ::default ( ) } ) ;
} else if f > at + 1 & & f < = at + 13 {
let k = ( f - at - 1 ) as f32 / 12.0 ;
raw . events . push ( egui ::Event ::PointerMoved ( a + ( b - a ) * k ) ) ;
} else if f = = at + 15 {
raw . events . push ( egui ::Event ::PointerButton { pos : b , button : egui ::PointerButton ::Primary , pressed : false , modifiers : Default ::default ( ) } ) ;
}
}
}
}
// ANYBOT_SHOT_HOVER="x,y@frame": just move the pointer there (tooltips).
if let Ok ( h ) = std ::env ::var ( " ANYBOT_SHOT_HOVER " ) {
if let Some ( ( xy , at ) ) = h . split_once ( '@' ) {
if let ( Some ( ( x , y ) ) , Ok ( at ) ) = ( xy . split_once ( ',' ) , at . trim ( ) . parse ::< u32 > ( ) ) {
if self . frames + 1 = = at {
if let ( Ok ( x ) , Ok ( y ) ) = ( x . trim ( ) . parse ::< f32 > ( ) , y . trim ( ) . parse ::< f32 > ( ) ) {
raw . events . push ( egui ::Event ::PointerMoved ( egui ::pos2 ( x , y ) ) ) ;
}
}
}
}
}
2026-11-06 16:19:56 +00:00
// ANYBOT_SHOT_SCROLL="x,y,dy@frame": from that frame on, a wheel step of dy over x,y on
// each of 100 frames (egui eases a wheel in over real time; screenshot frames are quick).
if let Ok ( sc ) = std ::env ::var ( " ANYBOT_SHOT_SCROLL " ) {
if let Some ( ( v , at ) ) = sc . split_once ( '@' ) {
let n : Vec < f32 > = v . split ( ',' ) . filter_map ( | x | x . trim ( ) . parse ( ) . ok ( ) ) . collect ( ) ;
let ( at , f ) = ( at . trim ( ) . parse ::< u32 > ( ) . unwrap_or ( 30 ) , self . frames + 1 ) ;
if n . len ( ) = = 3 & & f > = at & & f < at + 100 {
raw . events . push ( egui ::Event ::PointerMoved ( egui ::pos2 ( n [ 0 ] , n [ 1 ] ) ) ) ;
raw . events . push ( egui ::Event ::MouseWheel { unit : egui ::MouseWheelUnit ::Point , delta : vec2 ( 0.0 , n [ 2 ] ) , phase : egui ::TouchPhase ::Move , source : egui ::MouseWheelSource ::Unknown , modifiers : Default ::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 Ok ( spec ) = std ::env ::var ( " ANYBOT_SHOT_CLICK " ) else { return } ;
for one in spec . split ( ';' ) {
let ( pos , right ) = match one . strip_suffix ( " :right " ) { Some ( p ) = > ( p , true ) , None = > ( one , false ) } ;
let Some ( ( xy , at ) ) = pos . split_once ( '@' ) else { continue } ;
let Some ( ( x , y ) ) = xy . split_once ( ',' ) else { continue } ;
let ( Ok ( x ) , Ok ( y ) , Ok ( at ) ) = ( x . trim ( ) . parse ::< f32 > ( ) , y . trim ( ) . parse ::< f32 > ( ) , at . trim ( ) . parse ::< u32 > ( ) ) else { continue } ;
let p = egui ::pos2 ( x , y ) ;
let button = if right { egui ::PointerButton ::Secondary } else { egui ::PointerButton ::Primary } ;
if self . frames + 1 = = at {
raw . events . push ( egui ::Event ::PointerMoved ( p ) ) ;
}
if self . frames + 1 = = at + 1 {
raw . events . push ( egui ::Event ::PointerButton { pos : p , button , pressed : true , modifiers : Default ::default ( ) } ) ;
}
if self . frames + 1 = = at + 2 {
raw . events . push ( egui ::Event ::PointerButton { pos : p , button , pressed : false , modifiers : Default ::default ( ) } ) ;
}
}
}
fn ui ( & mut self , ui : & mut egui ::Ui , _frame : & mut eframe ::Frame ) {
let ctx = & ui . ctx ( ) . clone ( ) ;
self . handle_screenshot ( ctx ) ;
2026-11-05 06:25:01 +00:00
self . state . fix_primary ( ) ;
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 . md_viewer ( ctx ) ;
頁面: 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
self . page_viewer ( ctx ) ;
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 . refresh_presence ( ctx ) ;
self . check_for_newer_build ( ) ;
if std ::mem ::take ( & mut self . relaunch_requested ) {
self . relaunch ( ctx ) ;
}
self . background ( ctx ) ;
self . call_poll ( ctx ) ;
if self . state . theme = = Theme ::System {
self . apply_theme ( ctx ) ;
}
if ctx . input ( | i | i . modifiers . command & & i . key_pressed ( Key ::N ) ) {
self . new_bot ( ) ;
}
if ctx . input ( | i | i . modifiers . command & & i . key_pressed ( Key ::Comma ) ) {
self . show_settings = true ;
}
2026-11-05 09:29:55 +00:00
// Mac: the top strip moves the window like a title bar would (buttons drawn over it still
// take their clicks: they come later, so they are on top); a double click zooms.
if cfg! ( target_os = " macos " ) {
let full = ui . max_rect ( ) ;
let strip = egui ::Rect ::from_min_max ( full . min , egui ::pos2 ( full . max . x , full . min . y + TITLEBAR_H ) ) ;
let r = ui . interact ( strip , egui ::Id ::unique ( " titlebar-drag " ) , egui ::Sense ::click_and_drag ( ) ) ;
if r . drag_started_by ( egui ::PointerButton ::Primary ) {
ctx . send_viewport_cmd ( egui ::ViewportCommand ::StartDrag ) ;
}
if r . double_clicked ( ) {
let zoomed = ctx . input ( | i | i . viewport ( ) . maximized . unwrap_or ( false ) ) ;
ctx . send_viewport_cmd ( egui ::ViewportCommand ::Maximized ( ! zoomed ) ) ;
}
}
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 . sidebar ( ui ) ;
self . main_pane ( ui ) ;
self . bot_editor ( ctx ) ;
self . settings ( ctx ) ;
self . delete_dialog ( ctx ) ;
self . routines_window ( ctx ) ;
self . rebuild_dialog ( ctx ) ;
self . local_control_bar ( ctx ) ;
self . teach_bar ( ctx ) ;
self . approval_popup ( ctx ) ;
self . file_viewer ( ctx ) ;
self . splash ( ctx ) ;
self . drop_overlay ( ctx ) ;
// Keep the routine clock ticking while the window sits idle.
if ! self . demo {
ctx . request_repaint_after ( std ::time ::Duration ::from_secs ( 15 ) ) ;
}
if ! self . streams . is_empty ( ) | | ! self . model_fetch . is_empty ( ) {
ctx . request_repaint_after ( std ::time ::Duration ::from_millis ( 100 ) ) ;
}
self . save_if_dirty ( ) ;
}
fn on_exit ( & mut self ) {
// Closing mid-reply ends that turn as cancelled, so it isn't left looking stuck.
2026-11-04 09:46:43 +00:00
// (On a screen of a brain the bots keep working there.)
if self . mir . on {
self . streams . clear ( ) ;
}
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 ids : Vec < u64 > = self . streams . keys ( ) . copied ( ) . collect ( ) ;
for id in ids {
self . stop ( id ) ;
}
tools ::stop_all ( ) ;
crate ::cloudbox ::shutdown ( ) ;
if ! self . demo {
2026-11-04 09:46:43 +00:00
if self . mir . on {
self . mirror_save ( ) ;
} else {
self . state . save ( ) ;
}
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
}
}
}
// ---------- helpers ----------
/// When the program file was last written (= when this build was made).
fn exe_modified ( ) -> Option < std ::time ::SystemTime > {
std ::env ::current_exe ( ) . ok ( ) . and_then ( | p | std ::fs ::metadata ( p ) . ok ( ) ) . and_then ( | m | m . modified ( ) . ok ( ) )
}
/// "2026-11-03 20:57" in local time, or "?" when unknown.
fn version_label ( t : Option < std ::time ::SystemTime > ) -> String {
t . map ( | t | chrono ::DateTime ::< chrono ::Local > ::from ( t ) . format ( " %Y-%m-%d %H:%M " ) . to_string ( ) ) . unwrap_or_else ( | | " ? " . into ( ) )
}
/// How often the relaunch helper checks whether the old copy has exited (main-app.cjs @3195964,
/// `PBt`: `while kill -0 "$pid" …; do sleep 0.2; done; exec "$app"`).
pub const RELAUNCH_POLL_SECS : f32 = 0.2 ;
/// A detached helper that waits for this copy (`pid`) to exit, then starts `exe`, so two copies
/// never run at once and the old one's last save lands first. Like the original's `Pur`
/// (`spawn(shell, …, {detached:true, stdio:"ignore"})`), its input and output go nowhere;
/// the main program also reopens closed std streams on /dev/null at start (`sub_1000004c0`,
/// 0x1000004c0). Start-up switches are cleared so the new copy opens normally (the original's
/// `entry`, 0x100000460, changes how it starts when ELECTRON_RUN_AS_NODE is set).
pub fn relaunch_command ( pid : u32 , exe : & std ::path ::Path ) -> std ::process ::Command {
let script = format! ( " while kill -0 \" $1 \" 2>/dev/null; do sleep {RELAUNCH_POLL_SECS} ; done; exec \" $2 \" " ) ;
let mut c = std ::process ::Command ::new ( " /bin/sh " ) ;
c . arg ( " -c " ) . arg ( script ) . arg ( " anybot-relaunch " ) . arg ( pid . to_string ( ) ) . arg ( exe ) ;
c . stdin ( std ::process ::Stdio ::null ( ) ) . stdout ( std ::process ::Stdio ::null ( ) ) . stderr ( std ::process ::Stdio ::null ( ) ) ;
for k in [ " ANYBOT_SHOT_VIEW " , tools ::ENV_CONVERSATION_ID , tools ::ENV_REQUEST_ID , " ELECTRON_RUN_AS_NODE " ] {
c . env_remove ( k ) ;
}
#[ cfg(unix) ]
{
use std ::os ::unix ::process ::CommandExt ;
c . process_group ( 0 ) ;
}
c
}
/// A reply that is still coming in.
struct Live {
cancel : Arc < AtomicBool > ,
started : Instant ,
last : Instant ,
produced : bool ,
status : Option < String > ,
/// Waiting for an approval or a running tool: the stall watchdog stays quiet.
paused : bool ,
/// What the person typed during this turn, waiting for the next step.
steer : Arc < Mutex < Vec < String > > > ,
turn_id : String ,
/// 直接操作 for this turn (switched on mid-turn by "全部允許").
direct : Arc < AtomicBool > ,
}
/// How an "always allow" grant reads in the UI.
fn grant_label ( g : & str ) -> String {
match g {
" edit_file " = > " 「修改檔案」 " . into ( ) ,
" browser:read " = > " 「讀取瀏覽器分頁」 " . into ( ) ,
" read:anywhere " = > " 「讀取工作資料夾以外」 " . into ( ) ,
" computer:see " = > " 「看你的螢幕」 " . into ( ) ,
g if g . starts_with ( " app: " ) = > format! ( " 操作「 {} 」 " , & g [ 4 .. ] ) ,
g if g . starts_with ( " mcp: " ) = > format! ( " 使用連接器「 {} 」 " , & g [ 4 .. ] ) ,
g if g . starts_with ( " login: " ) = > format! ( " 登入 {} " , & g [ 6 .. ] ) ,
g if g . starts_with ( " macapp-write: " ) = > format! ( " 修改「 {} 」 " , & g [ 13 .. ] ) ,
g if g . starts_with ( " macapp: " ) = > format! ( " 讀取「 {} 」 " , & g [ 7 .. ] ) ,
other = > format! ( " 「 {} 」 " , other . trim_start_matches ( " shell: " ) ) ,
}
}
/// Padding that makes a one-line bubble 36 px tall around 15 px text (egui row ≈ 18 px).
fn bubble_margin ( ) -> Margin {
// index CSS `message`: padding-block var(--sand-transcript-prose-block-padding) = 7px,
// padding-inline 12px (7 + 22 + 7 = the 36 px one-line bubble).
debug_assert_eq! ( 7.0 * 2.0 + TRANSCRIPT_LINE_HEIGHT , BUBBLE_ONE_LINE_HEIGHT ) ;
Margin ::symmetric ( 12 , 7 )
}
/// index.eager-app `messageColumnOfBlock`: max-width min(80cqi, 560px, 100cqi - 82px).
pub const MESSAGE_MAX_WIDTH : f32 = 560.0 ;
pub fn message_max_width ( container : f32 ) -> f32 {
( container * 0.8 ) . min ( MESSAGE_MAX_WIDTH ) . min ( container - 82.0 ) . max ( 120.0 )
}
/// What the model sees: attachments read in, 引用 and 表情回應 noted, and in a group chat the other
/// members' answers as labelled words (their tool steps and private blocks left out).
fn group_history ( messages : & [ Message ] , speaker : Option < u64 > , st : & State ) -> Vec < Message > {
let mut out = vec! [ ] ;
let mut skip_tool_ids : std ::collections ::HashSet < String > = Default ::default ( ) ;
for m in messages {
let mut m = m . clone ( ) ;
if ! m . attachments . is_empty ( ) {
let ( extra , images ) = crate ::attach ::for_model ( & m . attachments ) ;
m . text = format! ( " {} {extra} " , m . text ) ;
m . images = images ;
}
if m . role = = Role ::Assistant & & ! m . reactions . is_empty ( ) {
m . text = format! ( " {} \n (使用者對這則回應: {} ) " , m . text , m . reactions . join ( " " ) ) ;
}
if let Some ( me ) = speaker {
if m . role = = Role ::Assistant & & m . author ! = me & & ! m . summary {
for c in & m . tool_calls {
skip_tool_ids . insert ( c . id . clone ( ) ) ;
}
if m . text . trim ( ) . is_empty ( ) {
continue ;
}
let who = st . bot ( m . author ) . map ( | b | b . name . clone ( ) ) . unwrap_or_else ( | | " 其他成員 " . into ( ) ) ;
out . push ( Message ::user ( & format! ( " 【 {who} 】 {} " , m . text ) ) ) ;
continue ;
}
if m . role = = Role ::Tool & & skip_tool_ids . contains ( & m . tool_call_id ) {
continue ;
}
}
out . push ( m ) ;
}
out
}
/// 生活卡片: a flight, place, weather, product or event the bot found, drawn as a card.
fn card ( ui : & mut egui ::Ui , args : & str , p : Palette ) {
let v = tools ::args_of ( args ) ;
use crate ::icons ::Icon ;
let icon = match v [ " kind " ] . as_str ( ) . unwrap_or ( " " ) {
" flight " = > Icon ::Plane ,
" place " = > Icon ::MapPin ,
" weather " = > Icon ::CloudSun ,
" product " = > Icon ::ShoppingBag ,
" event " = > Icon ::Calendar ,
_ = > Icon ::Folder ,
} ;
ui . add_space ( 4.0 ) ;
Frame ::new ( )
. fill ( p . elevated )
. stroke ( Stroke ::new ( 1.0 , p . border ) )
. corner_radius ( 14 )
. inner_margin ( Margin ::symmetric ( 14 , 12 ) )
. shadow ( egui ::epaint ::Shadow { offset : [ 0 , 2 ] , blur : 8 , spread : 0 , color : Color32 ::from_black_alpha ( 14 ) } )
. show ( ui , | ui | {
ui . set_max_width ( message_max_width ( ui . available_width ( ) + 32.0 ) ) ;
ui . horizontal ( | ui | {
let ( r , _ ) = ui . allocate_exact_size ( vec2 ( 26.0 , 26.0 ) , egui ::Sense ::hover ( ) ) ;
crate ::icons ::paint ( ui . painter ( ) , r , icon , p . text2 ) ;
ui . vertical ( | ui | {
ui . spacing_mut ( ) . item_spacing . y = 1.0 ;
ui . label ( RichText ::new ( v [ " title " ] . as_str ( ) . unwrap_or ( " " ) ) . strong ( ) . size ( FONT_LG ) ) ;
if let Some ( st ) = v [ " subtitle " ] . as_str ( ) . filter ( | x | ! x . is_empty ( ) ) {
ui . label ( RichText ::new ( st ) . size ( FONT_XS ) . color ( p . text3 ) ) ;
}
} ) ;
} ) ;
let rows = v [ " rows " ] . as_array ( ) . cloned ( ) . unwrap_or_default ( ) ;
if ! rows . is_empty ( ) {
ui . add_space ( 6.0 ) ;
egui ::Grid ::new ( ( " card " , args . len ( ) , v [ " title " ] . as_str ( ) . unwrap_or ( " " ) ) ) . num_columns ( 2 ) . spacing ( [ 14.0 , 4.0 ] ) . show ( ui , | ui | {
for r in rows . iter ( ) . take ( 12 ) {
ui . label ( RichText ::new ( r [ " label " ] . as_str ( ) . unwrap_or ( " " ) ) . size ( FONT_SM ) . color ( p . text3 ) ) ;
ui . add ( egui ::Label ::new ( RichText ::new ( r [ " value " ] . as_str ( ) . unwrap_or ( " " ) ) . size ( FONT_SM ) ) . wrap ( ) ) ;
ui . end_row ( ) ;
}
} ) ;
}
if let Some ( link ) = v [ " link " ] . as_str ( ) . filter ( | l | l . starts_with ( " http " ) ) {
ui . add_space ( 4.0 ) ;
ui . hyperlink_to ( RichText ::new ( t ( " 查看來源 ↗ " ) ) . size ( FONT_XS ) . color ( p . accent ) , link ) ;
}
} ) ;
ui . add_space ( 4.0 ) ;
}
/// 匯出文字記錄: the chat as Markdown (tool steps folded in as quotes).
fn transcript_markdown ( b : & Bot ) -> String {
let mut s = format! ( " # {} 的對話 \n \n 匯出時間: {} \n \n " , b . name , chrono ::Local ::now ( ) . format ( " %Y-%m-%d %H:%M " ) ) ;
for m in & b . messages {
match m . role {
Role ::User = > {
s . push_str ( & format! ( " **你**: {} \n " , m . text ) ) ;
for a in & m . attachments {
s . push_str ( & format! ( " [附件] {} \n " , a . name ) ) ;
}
s . push ( '\n' ) ;
}
Role ::Assistant if m . summary = > s . push_str ( & format! ( " > (較早的對話摘要) {} \n \n " , m . text . replace ( '\n' , " " ) ) ) ,
Role ::Assistant = > {
if ! m . text . trim ( ) . is_empty ( ) {
s . push_str ( & format! ( " ** {} **: {} \n \n " , b . name , m . text ) ) ;
}
for c in & m . tool_calls {
s . push_str ( & format! ( " > [工具] {} {} \n " , c . name , c . args . chars ( ) . take ( 200 ) . collect ::< String > ( ) ) ) ;
}
if ! m . tool_calls . is_empty ( ) {
s . push ( '\n' ) ;
}
}
Role ::Tool = > { }
}
}
s
}
/// What the bot is told about itself, the app it lives in and the computers it can use.
2026-11-05 08:47:28 +00:00
#[ allow(clippy::too_many_arguments) ]
fn system_prompt ( b : & Bot , st : & State , computers : & [ tools ::Target ] , default_computer : & str , roles : & tools ::Roles , missing : Option < & str > , lines : & [ String ] , mac_apps : bool ) -> 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 mut s = format! ( " 你是「 {} 」。 " , b . name ) ;
if ! b . description . trim ( ) . is_empty ( ) {
s . push_str ( & format! ( " 你的角色: {} 。 " , b . description . trim ( ) ) ) ;
}
if b . id = = st . primary_bot {
s . push_str ( " 你是使用者的主要 Bot: 主要聯絡窗口, 可以主動幫忙, 也能用 message_bot 把工作交給其他 Bot。 " ) ;
}
let now = crate ::routines ::now_in ( & st . timezone ) ;
2026-11-05 08:47:28 +00:00
// Which screens can really be operated, each by its computer's name.
2026-11-05 08:03:34 +00:00
let mut screens : Vec < String > = Vec ::new ( ) ;
for t in computers {
match & t . place {
2026-11-05 08:47:28 +00:00
tools ::Place ::Box { .. } if crate ::cloudbox ::available ( ) = > screens . push ( format! ( " 「 {} 」 " , t . name ) ) ,
tools ::Place ::Local { .. } if crate ::computer ::screen_supported ( ) = > screens . push ( format! ( " 「 {} 」 " , t . name ) ) ,
tools ::Place ::Device { .. } = > screens . push ( format! ( " 「 {} 」 " , t . name ) ) ,
_ = > { }
2026-11-05 08:03:34 +00:00
}
}
2026-11-05 08:47:28 +00:00
let screens = if screens . is_empty ( ) { " 目前沒有任何可操作的螢幕 " . to_string ( ) } else { format! ( " 可操作的螢幕: {} " , screens . join ( " 、 " ) ) } ;
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
s . push_str ( & format! (
" \n \n 現在是 {}( {},時區 {})。 \n \
你 在 AnyBot 這 個 App 裡 , 可 以 直 接 操 作 App : \ n \
- 使 用 者 想 定 期 或 在 某 個 時 間 做 事 ( 每 天 、 每 週 一 、 每 月 一 號 、 提 醒 我 … ) , 就 用 create_routine 設 成 例 行 任 務 ; 時 間 照 使 用 者 的 時 區 。 查 看 、 修 改 、 暫 停 、 刪 除 用 list_routines / update_routine / delete_routine 。 \ n \
- 換 模 型 或 供 應 商 、 改 名 字 、 建 立 新 Bot : update_bot / create_bot ; 看 有 哪 些 Bot : list_bots 。 \ n \
- 主 題 、 強 調 色 、 時 區 : update_settings 。 \ n \
- 使 用 者 說 「 交 給 某 某 Bot 」 或 需 要 別 的 Bot 專 長 時 , 用 message_bot 。 \ n \
做 完 App 操 作 後 , 用 一 句 話 告 訴 使 用 者 結 果 ( 例 如 排 程 的 時 間 ) 。 \ n \ n \
你 也 可 以 上 網 和 用 使 用 者 的 瀏 覽 器 : \ n \
- 公 開 網 頁 : web_fetch 。 \ n \
Real-use fixes: the person sees the screens the bot shows, files reach the phone, no blank-tab loops
From real runs on the cloud brain with the person's Mac (Threads patrol, a post draft, "show me my
screen"):
- computer_use_show: a plain picture of any computer's screen goes into the chat (the bot sees it
too). The bot used to describe a screenshot in words and fail to send it.
- The phone shows the bot's attachments: pictures in place, other files download on a tap (api/file,
only from the chats' file folder). It showed nothing for the bot and names only for the person.
- send_file works from the person's other computers (fetch_file over the relay, 20 MB, secrets stay).
- The same call with the same answer again and again is warned the 3rd time and stopped from the
4th (a blank tab was read 11 times, then the bot said Threads was logged out without opening it);
a blank tab says so; checking a login means opening the site.
- Screenshot lists: a nameless row around a named button isn't numbered, a nameless row takes the
words inside it, empty spacer rows aren't numbered (System Settings: 63 numbers -> 33, all named).
- Writing "in my voice" reads the person's own recent posts first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 11:37:45 +00:00
- 要 登 入 才 看 得 到 的 ( Threads 、 Instagram 、 Facebook 、 Gmail 、 銀 行 … ) : 用 browser_open 在 AnyBot 自 己 的 瀏 覽 器 視 窗 打 開 : 它 會 等 頁 面 載 入 , 直 接 回 傳 頁 面 內 容 和 可 以 點 的 編 號 ( 不 用 再 browser_read ; 之 後 頁 面 變 了 才 用 browser_read 重 讀 ) 。 例 如 看 Threads 帳 號 : browser_open ( \ " https://www.threads.net \" )。要知道某個網站有沒有登入,一定要打開那個網站看,不要看目前的分頁(常常是空白頁)就下結論。 \n \
Bots click by number: every control on the screen is boxed and numbered, on every computer
A bot asked to open a Threads profile clicked an empty strip ten times: it guessed coordinates
from a picture, and the web tools' description told it they didn't work on its own computer.
- marks.rs: the picture a bot gets has every button, link, field and menu item boxed and
numbered (a tiny built-in digit font, pure Rust), with the same numbers in a list. click /
typing / scroll / mouse_move take the number (ref) and land on the control's centre.
- Where the controls come from: macOS AX and Linux AT-SPI through agent-desktop on a person's
computer (desktop::elements); AT-SPI in the bots' own computer (box/a11y, after XBot's
box-a11y), web pages included. On a Mac a control whose spot another app's window covers gets
no number (topmost window at its centre must be the app's; the Dock's invisible full-screen
window doesn't count). open_app waits until the app is really in front.
- No going round in circles: a step that changed nothing says so; the same step (clicks within
16 px) a third time in a turn comes with a warning, from the fourth time it isn't run.
- The box (version 4): one TigerVNC server instead of Xvfb + x11vnc, the AT-SPI bus and registry
at boot, one browser launcher (box/browser, after XBot's box-chrome: same profile and CDP,
renderer accessibility, a launch lock, stale profile locks cleared). One docker exec per look
(list + picture, ~0.4 s). A brain updates an older box by itself when it has been idle a
minute (files and logins stay); a new box is never made from an older image.
- Gone: computer_use_read_app (the list is in every picture), computer_use_mouse_button, the
per-call picture for the phone (it gets a plain one), the stale browser_read note; the prompt
says web work goes through browser_* by number, and never to read browser cookie or history
files.
Tests: marks (numbering, drawing, fingerprints), box_numbers_are_exact on a box from the new
image (a Threads-like page: the icon links get numbers, Profile clicked by number, Chinese typed
into the numbered field, a click on nothing called out), box_screen_is_exact, VNC e2e 11/11 on
TigerVNC, Calculator 7×8=56 by numbers through the signed app on this Mac, remote_e2e 28/28,
153 unit tests; Linux build + 157 tests + x11_live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:47:01 +00:00
- 網 頁 上 的 事 ( 任 何 電 腦 、 任 何 網 站 ) 一 律 用 browser_ * 工 具 : 先 browser_read ( 或 browser_screenshot ) 拿 到 每 個 連 結 、 按 鈕 、 欄 位 的 編 號 和 網 址 , 再 用 browser_click / browser_type / browser_select / browser_press 照 編 號 操 作 ; 要 去 的 頁 面 有 網 址 就 直 接 browser_open 。 網 頁 上 絕 對 不 要 看 截 圖 猜 座 標 點 。 表 格 用 browser_table 讀 , 找 字 用 browser_find , 要 等 載 入 用 browser_wait , 匯 出 報 表 用 browser_download 。 不 要 猜 網 址 ; 找 不 到 想 要 的 功 能 ( 例 如 排 序 、 篩 選 ) , 就 看 欄 位 標 題 、 篩 選 器 、 下 拉 選 單 或 分 頁 標 籤 , 還 是 沒 有 就 照 頁 面 現 有 的 資 料 自 己 整 理 , 並 說 明 網 站 沒 有 這 個 功 能 。
- 絕 對 不 要 直 接 呼 叫 網 站 的 內 部 API 、 下 載 網 站 的 程 式 碼 來 研 究 , 或 用 程 式 大 量 抓 資 料 : 網 站 會 把 帳 號 當 成 機 器 人 ( 例 如 跳 出 拼 圖 驗 證 、 封 鎖 帳 號 ) 。 也 絕 對 不 要 用 指 令 去 讀 瀏 覽 器 的 Cookie 、 瀏 覽 紀 錄 、 密 碼 或 設 定 檔 資 料 庫 。
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
- 需 要 使 用 者 親 手 做 的 步 驟 ( 讀 到 登 入 頁 、 輸 入 密 碼 、 驗 證 碼 、 拼 圖 、 簡 訊 或 email 驗 證 、 確 認 付 款 ) : 先 在 那 台 電 腦 上 把 那 一 頁 打 開 、 點 到 要 輸 入 的 地 方 , 再 呼 叫 hand_over_computer , instruction 寫 清 楚 要 他 做 什 麼 。 聊 天 裡 會 出 現 那 台 電 腦 的 即 時 畫 面 , 他 點 「 接 管 」 做 完 後 按 「 我 完 成 了 」 , 你 再 確 認 、 接 著 做 ( 登 入 一 次 之 後 就 會 記 住 ) 。 不 要 只 用 文 字 叫 他 自 己 去 找 視 窗 , 絕 對 不 要 替 他 打 密 碼 , 也 不 要 叫 他 改 瀏 覽 器 的 設 定 或 選 單 、 或 說 要 從 Chrome 複 製 登 入 或 Cookie ( 都 不 需 要 , 也 沒 有 這 個 功 能 ) 。 \ n \
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
- AnyBot 瀏 覽 器 有 設 定 檔 ( profile ) , 每 個 有 自 己 的 登 入 。 使 用 者 說 要 換 profile / 設 定 檔 / 另 一 個 帳 號 時 , 用 browser_profiles : list 看 有 哪 些 , use 切 換 , add 新 增 ( 新 的 要 登 入 一 次 ) 。 也 可 以 在 browser_open / browser_read 帶 profile 參 數 。 使 用 者 想 用 自 己 平 常 的 Chrome ( 自 己 的 Chrome 設 定 檔 、 已 經 登 入 的 帳 號 ) 時 , 用 profile 「 你 的 瀏 覽 器 」 : 請 使 用 者 在 Chrome 切 到 想 要 的 設 定 檔 , 再 browser_read 。 \ n \
Whole control flow checked against real chats: what the bot sees is what is there, every step lands, nothing done twice
Web
- web_fetch kept pages with a <header> only up to it (taken for <head>): articles came back as their menus.
- browser_open waits for the page to load and returns it like browser_read (one step fewer per page);
actions wait for the page to hold still instead of fixed 1-2.5 s sleeps; one way to reach the tab.
The bot's computer (box v6)
- Numbers only on what can be seen: clipped to the window and the page's visible area, none under
another app's window (a link scrolled under the toolbar was numbered on Back).
- press_key with a typed character no longer swallows the keys after it (xdotool type --args 1).
- The person's input through noVNC/TigerVNC is noticed again (fence, continuous updates, QEMU keys).
- "Nothing changed" only after clicks, scrolls and drags; long text is pasted; one screen step at a
time; one docker inspect per step when the box is known ready; poppler-utils for long PDFs.
- One typing path and fewer round trips (stdin variant, unused picture path, extra batch execs gone).
The person's computers
- Linux: the window system's front app when it has one, and the reason when there is none (was 「」);
hidden and off-screen controls get no number; wording no longer names a tool that doesn't exist.
The loop
- Without Docker, the bot's own computer runs no commands where there is no sandbox (Linux, Windows):
it was a plain shell on the brain itself, keys in reach, asked nothing.
- The bot's own computer is re-checked each step; unreachable computers are failures, not safety
blocks; no direct-mode note on every result; an empty reply is asked again; scrolling and keys
aren't counted as going in circles; steps aren't moved to a computer that can't do them.
- One finish() for every step's result, keep_grant(), dead appctl list_computers/SERVING and the
impossible server branches removed; duplicated prompt instructions merged.
Tests: 162 (Mac) / 166 (Linux) unit; real box drills 6/6 on box v6; a brain without Chrome browses
on its box; remote E2E 28/28.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 09:44:59 +00:00
- 你 也 能 像 人 一 樣 操 作 螢 幕 ( { screens } ) , 用 在 檔 案 、 指 令 和 網 頁 工 具 做 不 到 的 事 ( 例 如 操 作 其 他 App ) 。 照 截 圖 上 的 編 號 操 作 ( 工 具 說 明 裡 有 怎 麼 用 ) ; 只 有 沒 有 編 號 的 東 西 才 用 x 、 y 。 結 果 說 「 畫 面 和 上 一 步 完 全 一 樣 」 就 是 沒 點 到 : 換 一 個 編 號 或 換 方 法 , 絕 對 不 要 重 複 同 一 步 。 已 經 從 畫 面 知 道 接 下 來 幾 步 時 ( 填 一 張 表 、 一 組 捷 徑 ) , 用 computer_use_batch 一 次 做 完 ; 要 看 上 一 步 結 果 才 知 道 下 一 步 的 , 就 一 步 一 步 來 。 每 次 看 截 圖 用 一 句 話 說 畫 面 顯 示 什 麼 、 這 步 成 功 與 否 。 \ n \
2026-11-05 08:47:28 +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
- 不 要 說 「 我 沒 有 瀏 覽 器 或 工 具 」 : 先 試 這 些 工 具 。 需 要 權 限 時 App 會 自 己 跳 出 視 窗 問 使 用 者 ; 工 具 回 報 要 開 什 麼 設 定 時 , 照 實 轉 告 。 " ,
now . format ( " %Y-%m-%d %H:%M " ) ,
[ " 週一 " , " 週二 " , " 週三 " , " 週四 " , " 週五 " , " 週六 " , " 週日 " ] [ now . weekday ( ) . num_days_from_monday ( ) as usize ] ,
2026-11-05 08:47:28 +00:00
st . timezone ,
if mac_apps { " - 使用者 Mac 上的「提醒事項、行事曆、聯絡人、備忘錄、郵件、訊息」有專門的工具( ios_read_reminders、mobile_read_calendar、mobile_read_contacts、mac_read_notes、mac_read_mail 等),比操作畫面快又準,會在使用者正在用的那台 Mac 上做; 寄信或傳訊息每次都會先請使用者確認, 起草郵件( mac_draft_mail) 則讓使用者自己按寄出。 \n " } 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
) ) ;
if ! b . routines . is_empty ( ) {
s . push_str ( " \n \n 你目前的例行任務: \n " ) ;
s . push_str ( & b . routines . iter ( ) . map ( appctl ::routine_line ) . collect ::< Vec < _ > > ( ) . join ( " \n " ) ) ;
}
頁面: 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
// 頁面: what this bot already keeps, so a routine or a follow-up updates the same page.
let mine : Vec < String > = st . pages . iter ( ) . filter ( | p | p . bot = = b . id ) . map ( | p | format! ( " # {} 「 {} 」(第 {} 版,更新於 {} ) " , p . id , p . title , p . revision , crate ::pages ::when ( p . updated ) ) ) . collect ( ) ;
if ! mine . is_empty ( ) {
s . push_str ( " \n \n 你做過的頁面(更新時用 write_page 帶 id, 先 read_page 看現在的內容): \n " ) ;
s . push_str ( & mine . join ( " \n " ) ) ;
}
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 ! computers . is_empty ( ) {
2026-11-05 08:47:28 +00:00
let list : Vec < String > = lines . iter ( ) . map ( | l | format! ( " - {l} " ) ) . collect ( ) ;
let mine = match & roles . person {
Some ( p ) = > format! ( " 使用者說「我的電腦」「這台」「我的 Mac」時, 指的是「 {p} 」(他正在用的那台)。 " ) ,
None = > " 現在不知道使用者在用哪台電腦:他說「我的電腦」「這台」時,先問他是哪一台。 " . to_string ( ) ,
} ;
let gone = match missing {
Some ( m ) = > format! ( " \n - 使用者選的「 {m} 」現在沒有連線(那台的 AnyBot 沒開或沒登入)。要在那台做事時,告訴使用者先打開它;要改用別台,先問使用者。 " ) ,
None = > 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
s . push_str ( & format! (
2026-11-05 08:47:28 +00:00
" \n \n 你可以用工具在電腦上做事。每台電腦都用它自己的名字稱呼,工具的 computer 參數就填那個名字: \n {} \n \
- 使 用 者 替 你 選 的 電 腦 是 「 { default_computer } 」 : 檔 案 、 指 令 、 看 畫 面 、 點 擊 和 瀏 覽 器 都 在 這 台 做 , 不 要 自 己 換 到 別 台 。 只 有 使 用 者 在 訊 息 裡 指 名 另 一 台 , 才 把 computer 設 成 那 台 。 { gone } \ n \
- 「 { own } 」 是 你 自 己 的 電 腦 ; 使 用 者 說 「 你 的 電 腦 」 「 Bot 的 電 腦 」 指 的 就 是 它 。 { mine } \ n \
- 回 覆 使 用 者 時 , 說 到 電 腦 一 律 用 上 面 的 名 字 ( 例 如 「 我 在 「 { own } 」 打 開 了 Threads 」 ) , 不 要 說 「 你 的 電 腦 」 「 Bot 的 電 腦 」 這 種 會 混 淆 的 說 法 。 \ n \
- 不 確 定 使 用 者 指 的 是 哪 台 電 腦 時 ( 他 說 的 那 台 不 在 清 單 上 、 有 幾 台 都 說 得 通 、 或 他 的 說 法 和 你 手 上 的 預 設 不 一 致 ) , 先 用 一 句 話 問 他 要 哪 台 , 問 清 楚 再 動 手 ; 不 要 擅 作 主 張 選 一 台 就 開 始 做 , 也 不 要 把 一 台 說 成 另 一 台 。 \ n \
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
- 先 用 list_dir 、 search 、 read_file 看 清 楚 , 再 動 手 。 \ n \
2026-11-06 08:51:25 +00:00
- 一 回 合 的 步 數 有 限 , 每 一 步 都 要 做 多 一 點 : 互 不 相 依 的 工 具 在 同 一 步 一 起 呼 叫 ( 例 如 同 時 讀 幾 個 檔 案 、 同 時 查 幾 件 事 ) ; 查 東 西 的 指 令 合 成 一 條 ( 用 ; 或 & & 串 起 來 , 一 次 看 完 要 的 資 料 ) , 不 要 一 條 指 令 一 步 。 要 等 東 西 跑 完 時 , 把 等 待 寫 在 同 一 條 指 令 裡 ( 迴 圈 等 到 好 了 為 止 ) , 不 要 一 步 一 步 sleep 。 \ n \
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
- 需 要 權 限 時 ( 電 腦 工 具 還 沒 打 開 、 要 用 使 用 者 的 電 腦 、 會 改 變 東 西 的 操 作 ) , 直 接 呼 叫 工 具 , App 會 自 己 跳 出 視 窗 請 使 用 者 允 許 ; 不 要 叫 使 用 者 自 己 去 改 設 定 。 被 拒 絕 就 換 個 方 式 或 問 使 用 者 。 \ n \
- 跑 很 久 的 指 令 會 移 到 背 景 , 用 read_terminal 追 蹤 , 不 要 重 複 執 行 ; 它 在 等 輸 入 時 ( 例 如 問 y / n ) , 用 write_terminal 回 答 。 \ n \
- 只 有 工 具 結 果 證 明 做 完 了 , 才 說 做 完 ; 失 敗 就 照 實 說 。 \ n \
2026-11-05 08:16:25 +00:00
- 連 續 執 行 多 步 驟 工 具 時 不 要 每 步 都 發 訊 息 ( 先 說 「 好 , 我 先 … 」 , 做 完 一 個 工 具 播 一 段 , 最 後 再 總 結 — — 使 用 者 打 一 句 就 看 到 三 四 則 回 覆 ) 。 執 行 時 就 安 靜 地 連 續 呼 叫 工 具 , 全 部 做 完 後 用 一 段 話 講 結 果 ; 只 有 卡 住 、 需 要 使 用 者 動 手 ( 例 如 要 他 輸 入 密 碼 ) 、 或 要 先 問 清 哪 台 電 腦 時 , 才 中 途 說 話 。 \ n \
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 08:47:28 +00:00
list . join ( " \n " ) ,
own = roles . own ,
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 chans : Vec < String > = st . channels . iter ( ) . filter ( | c | c . bot_id = = b . id & & c . enabled ) . map ( | c | format! ( " {} 「 {} 」 " , c . kind . label ( ) , c . name ) ) . collect ( ) ;
if ! chans . is_empty ( ) {
s . push_str ( & format! ( " \n \n 你也在這些訊息管道回覆別人: {} 。以「[訊息管道]」開頭的訊息來自那裡,你這一回合最後的回答會自動傳回給對方,所以要簡潔、直接回答對方,不要用表格,也不要提到 AnyBot 的內部操作。對方不是使用者本人:不要透露使用者的私人資料,要動到使用者的電腦或帳號前先確認。 " , chans . join ( " 、 " ) ) ) ;
}
s . push_str ( & crate ::memory ::prompt_section ( & b . memories , & st . skills ) ) ;
let mcp_notes = crate ::mcp ::instructions ( & st . mcp_servers ) ;
let mcp_tools = crate ::mcp ::status ( ) ;
if ! mcp_tools . is_empty ( ) {
s . push_str ( & format! ( " \n \n 你也可以用使用者接上的連接器( MCP, 工具名稱以 mcp__ 開頭): {} 。第一次用某個連接器時 App 會問使用者。 {} " , mcp_tools . keys ( ) . cloned ( ) . collect ::< Vec < _ > > ( ) . join ( " 、 " ) , if mcp_notes . is_empty ( ) { String ::new ( ) } else { format! ( " \n 連接器的使用說明: \n {mcp_notes} " ) } ) ) ;
}
s . push_str ( & crate ::teach ::prompt_section ( & b . demos ) ) ;
if ! b . instructions . trim ( ) . is_empty ( ) {
s . push_str ( " \n \n " ) ;
s . push_str ( b . instructions . trim ( ) ) ;
}
頁面: 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
s . push_str ( " \n \n 使用者要你追蹤、整理、比較、研究一件之後還會回來看的事(例如「幫我追蹤台灣 FSD 狀況」),就用 write_page 做成一頁:先寫結論和現況,再寫細節和來源(附日期),適合時用表格、```stats、```steps、圖表。回答時簡短說重點, 並用 [頁面標題](page:ID) 連到它。要定期追蹤時,再用 create_routine 設例行任務, 每次執行: read_page、查新消息, 有實質進展才用 write_page 更新( note 寫改了什麼)並在聊天裡說;沒有新消息就簡短回答「沒有新消息」,不要更新頁面。 " ) ;
Real-use fixes: the person sees the screens the bot shows, files reach the phone, no blank-tab loops
From real runs on the cloud brain with the person's Mac (Threads patrol, a post draft, "show me my
screen"):
- computer_use_show: a plain picture of any computer's screen goes into the chat (the bot sees it
too). The bot used to describe a screenshot in words and fail to send it.
- The phone shows the bot's attachments: pictures in place, other files download on a tap (api/file,
only from the chats' file folder). It showed nothing for the bot and names only for the person.
- send_file works from the person's other computers (fetch_file over the relay, 20 MB, secrets stay).
- The same call with the same answer again and again is warned the 3rd time and stopped from the
4th (a blank tab was read 11 times, then the bot said Threads was logged out without opening it);
a blank tab says so; checking a login means opening the site.
- Screenshot lists: a nameless row around a named button isn't numbered, a nameless row takes the
words inside it, empty spacer rows aren't numbered (System Settings: 63 numbers -> 33, all named).
- Writing "in my voice" reads the person's own recent posts first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-06 11:37:45 +00:00
s . push_str ( " \n \n 幫使用者寫貼文、留言或文案,要「用我的語氣」時:先打開他的帳號,讀最近幾則他自己寫的貼文,照那樣的長度、用詞、標點和開頭結尾寫,不要只看自介。給草稿就好,沒有他說「發」不要自己發出去。 " ) ;
s . push_str ( " \n \n 使用者要看某台電腦的畫面(截圖給我看、讓我看看)時,用 computer_use_show: 畫面會直接出現在聊天裡。 " ) ;
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
s . push_str ( " \n \n 做好的檔案( 試算表、文件、Markdown、網頁、圖片) 用 send_file 傳進聊天,使用者點一下就能預覽;不要只說檔案放在哪裡。適合的時候可以用 react 對使用者的訊息按個表情(例如謝謝你時按 ❤️),不用每次都按。 " ) ;
s . push_str ( " \n \n " ) ;
s . push_str ( & crate ::i18n ::reply_instruction ( & st . language ) ) ;
s
}
fn short_provider ( k : ProviderKind ) -> & 'static str {
match k {
ProviderKind ::Xai = > " Grok " ,
ProviderKind ::OpenAi = > " OpenAI " ,
ProviderKind ::Anthropic = > " Claude " ,
ProviderKind ::Gemini = > " Gemini " ,
ProviderKind ::OpenRouter = > " OpenRouter " ,
ProviderKind ::OpenCodeGo = > " OpenCode Go " ,
ProviderKind ::Ollama = > " Ollama " ,
ProviderKind ::Custom = > " 自訂 " ,
}
}
/// The Chinese font's "·" already carries space on both sides; spaces around it doubled the gap.
fn tidy_dots ( s : & str ) -> String {
s . replace ( " · " , " · " )
}
fn first_line ( s : & str , max : usize ) -> String {
let line = s . lines ( ) . map ( str ::trim ) . find ( | l | ! l . is_empty ( ) ) . unwrap_or ( " " ) ;
let line = line . trim_start_matches ( [ '#' , '-' , '*' , ' ' ] ) ;
let mut out : String = line . chars ( ) . take ( max ) . collect ( ) ;
if line . chars ( ) . count ( ) > max {
out . push ( '…' ) ;
}
out
}
fn icon_button ( ui : & mut egui ::Ui , icon : & str , tip : & str , p : Palette ) -> egui ::Response {
if let Some ( ic ) = crate ::icons ::Icon ::from_name ( icon ) {
return crate ::icons ::button ( ui , ic , 28.0 , p . text2 , p . hover ) . on_hover_text ( t ( tip ) ) ;
}
ui . add ( egui ::Button ::new ( RichText ::new ( icon ) . size ( 15.0 ) . color ( p . text2 ) ) . frame ( false ) . min_size ( vec2 ( 26.0 , 26.0 ) ) ) . on_hover_text ( t ( tip ) )
}
fn small_button ( ui : & mut egui ::Ui , text : & str , p : Palette ) -> egui ::Response {
ui . add ( egui ::Button ::new ( RichText ::new ( t ( text ) ) . size ( 11.5 ) . color ( p . text3 ) ) . frame_when_inactive ( false ) . corner_radius ( 6 ) . min_size ( vec2 ( 0.0 , 22.0 ) ) )
}
/// The input's 28 px round buttons (+ , mic, voice chat / send).
fn round_tool ( ui : & mut egui ::Ui , icon : crate ::icons ::Icon , fill : Color32 , ink : Color32 ) -> egui ::Response {
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( 28.0 , 28.0 ) , egui ::Sense ::click ( ) ) ;
let f = if r . hovered ( ) { fill . gamma_multiply ( 0.82 ) . to_opaque ( ) . lerp_to_gamma ( fill , 0.3 ) } else { fill } ;
ui . painter ( ) . circle_filled ( rect . center ( ) , 14.0 , f ) ;
crate ::icons ::paint ( ui . painter ( ) , egui ::Rect ::from_center_size ( rect . center ( ) , vec2 ( 15.0 , 15.0 ) ) , icon , ink ) ;
r . on_hover_cursor ( egui ::CursorIcon ::PointingHand )
}
/// A 36 px rounded button with a hairline border (the original's 分享 / ⋯).
fn pill_text ( ui : & mut egui ::Ui , text : & str , p : Palette ) -> egui ::Response {
ui . add ( egui ::Button ::new ( RichText ::new ( text ) . size ( 14.0 ) . color ( p . text ) ) . fill ( p . elevated ) . stroke ( Stroke ::new ( 1.0 , p . border ) ) . corner_radius ( 18 ) . min_size ( vec2 ( 0.0 , 36.0 ) ) )
}
fn pill_icon ( ui : & mut egui ::Ui , icon : crate ::icons ::Icon , p : Palette ) -> egui ::Response {
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( 36.0 , 36.0 ) , egui ::Sense ::click ( ) ) ;
ui . painter ( ) . circle_filled ( rect . center ( ) , 18.0 , if r . hovered ( ) { p . hover } else { p . elevated } ) ;
ui . painter ( ) . circle_stroke ( rect . center ( ) , 17.5 , Stroke ::new ( 1.0 , p . border ) ) ;
crate ::icons ::paint ( ui . painter ( ) , egui ::Rect ::from_center_size ( rect . center ( ) , vec2 ( 16.0 , 16.0 ) ) , icon , p . text2 ) ;
r . on_hover_cursor ( egui ::CursorIcon ::PointingHand )
}
/// A group heading inside a settings page (like the original's section titles).
fn section ( ui : & mut egui ::Ui , title : & str , p : Palette ) {
ui . add_space ( 14.0 ) ;
ui . label ( RichText ::new ( t ( title ) ) . size ( FONT_SM ) . strong ( ) . color ( p . text3 ) ) ;
ui . add_space ( 2.0 ) ;
}
/// A small「?」after a setting's name. Its explanation shows as soon as the pointer is on it (no
/// waiting for the pointer to be still), and a click keeps it open until the next click.
fn help_mark ( ui : & mut egui ::Ui , text : & str , p : Palette ) {
if text . trim ( ) . is_empty ( ) {
return ;
}
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( 18.0 , 18.0 ) , egui ::Sense ::click ( ) ) ;
let pinned_id = r . id . with ( " pinned " ) ;
let mut pinned = ui . ctx ( ) . data ( | d | d . get_temp ::< bool > ( pinned_id ) ) . unwrap_or ( false ) ;
if r . clicked ( ) {
pinned = ! pinned ;
} else if pinned & & ui . input ( | i | i . pointer . any_pressed ( ) ) & & ! r . hovered ( ) {
pinned = false ;
}
ui . ctx ( ) . data_mut ( | d | d . insert_temp ( pinned_id , pinned ) ) ;
let show = r . hovered ( ) | | pinned ;
let col = if show { p . text } else { p . text3 } ;
ui . painter ( ) . circle_stroke ( rect . center ( ) , 6.5 , Stroke ::new ( 1.1 , col ) ) ;
ui . painter ( ) . text ( rect . center ( ) + vec2 ( 0.0 , 0.5 ) , egui ::Align2 ::CENTER_CENTER , " ? " , egui ::FontId ::proportional ( 10.0 ) , col ) ;
if show {
let text = t ( text ) ;
egui ::Area ::new ( r . id . with ( " help " ) )
. order ( egui ::Order ::Tooltip )
. fixed_pos ( rect . left_bottom ( ) + vec2 ( - 4.0 , 6.0 ) )
. constrain ( true )
. interactable ( false )
. show ( ui . ctx ( ) , | ui | {
Frame ::popup ( ui . style ( ) ) . corner_radius ( 10 ) . inner_margin ( Margin ::symmetric ( 12 , 10 ) ) . show ( ui , | ui | {
ui . set_max_width ( 300.0 ) ;
ui . add ( egui ::Label ::new ( RichText ::new ( text ) . size ( FONT_SM ) . color ( p . text ) ) . wrap ( ) ) ;
} ) ;
} ) ;
}
r . on_hover_cursor ( egui ::CursorIcon ::Help ) ;
}
/// One setting: its name (with「?」for the explanation) on the left, its control on the right.
fn setting_row ( ui : & mut egui ::Ui , p : Palette , label : & str , desc : & str , control : impl FnOnce ( & mut egui ::Ui ) ) {
Frame ::new ( ) . fill ( p . elevated ) . corner_radius ( 10 ) . inner_margin ( Margin ::symmetric ( 14 , 10 ) ) . show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
ui . horizontal ( | ui | {
let w = ( ui . available_width ( ) * 0.5 ) . max ( 180.0 ) ;
ui . horizontal ( | ui | {
ui . set_width ( w ) ;
ui . spacing_mut ( ) . item_spacing . x = 4.0 ;
ui . label ( RichText ::new ( t ( label ) ) . size ( FONT_BASE ) . color ( p . text ) ) ;
help_mark ( ui , desc , p ) ;
} ) ;
ui . with_layout ( Layout ::right_to_left ( Align ::Center ) , | ui | {
ui . horizontal ( control ) ;
} ) ;
} ) ;
} ) ;
}
fn setting_title ( ui : & mut egui ::Ui , title : & str , desc : & str , p : Palette ) {
ui . horizontal ( | ui | {
ui . spacing_mut ( ) . item_spacing . x = 4.0 ;
ui . label ( RichText ::new ( t ( title ) ) . strong ( ) . size ( FONT_BASE ) ) ;
help_mark ( ui , desc , p ) ;
} ) ;
ui . add_space ( 4.0 ) ;
}
/// Small grey text under a setting.
fn hint ( ui : & mut egui ::Ui , text : impl Into < String > , p : Palette ) {
ui . add ( egui ::Label ::new ( RichText ::new ( t ( & text . into ( ) ) ) . size ( 11.5 ) . color ( p . text3 ) ) . wrap ( ) ) ;
}
/// A rounded group of related settings, like a macOS settings card.
fn setting_card < R > ( ui : & mut egui ::Ui , p : Palette , body : impl FnOnce ( & mut egui ::Ui ) -> R ) -> R {
let r = Frame ::new ( )
. fill ( p . hover . gamma_multiply ( 0.6 ) )
. stroke ( Stroke ::new ( 1.0 , p . border . gamma_multiply ( 0.6 ) ) )
. corner_radius ( 12 )
. inner_margin ( Margin ::symmetric ( 16 , 14 ) )
. show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
body ( ui )
} )
. inner ;
ui . add_space ( 12.0 ) ;
r
}
/// A label column so form rows line up.
fn form_row ( ui : & mut egui ::Ui , label : & str , p : Palette , body : impl FnOnce ( & mut egui ::Ui ) ) {
ui . horizontal ( | ui | {
let ( rect , _ ) = ui . allocate_exact_size ( vec2 ( FORM_LABEL_WIDTH , 28.0 ) , egui ::Sense ::hover ( ) ) ;
ui . painter ( ) . text ( rect . left_center ( ) , egui ::Align2 ::LEFT_CENTER , t ( label ) , egui ::FontId ::proportional ( FONT_SM ) , p . text2 ) ;
body ( ui ) ;
} ) ;
}
fn field_label ( ui : & mut egui ::Ui , text : & str , p : Palette ) {
ui . add_space ( 4.0 ) ;
ui . label ( RichText ::new ( t ( text ) ) . size ( FONT_XS ) . color ( p . text2 ) ) ;
}
fn primary_button ( text : & str , p : Palette ) -> egui ::Button < 'static > {
egui ::Button ::new ( RichText ::new ( t ( text ) ) . color ( p . on_primary ) . strong ( ) ) . fill ( p . primary ) . corner_radius ( 8 ) . min_size ( vec2 ( 72.0 , 30.0 ) )
}
fn secondary_button ( text : & str ) -> egui ::Button < 'static > {
egui ::Button ::new ( t ( text ) ) . corner_radius ( 8 ) . min_size ( vec2 ( 64.0 , 30.0 ) )
}
fn danger_button ( text : & str , p : Palette ) -> egui ::Button < 'static > {
egui ::Button ::new ( RichText ::new ( t ( text ) ) . color ( Color32 ::WHITE ) . strong ( ) ) . fill ( p . danger ) . corner_radius ( 8 ) . min_size ( vec2 ( 72.0 , 30.0 ) )
}
/// Tallest a dialog may be: the window minus a margin above and below.
fn dialog_max_height ( ctx : & egui ::Context ) -> f32 {
( ctx . content_rect ( ) . height ( ) - WINDOW_SCREEN_MARGIN ) . max ( 240.0 )
}
/// A centred dialog over a dimmed window: title and × on top, then the caller's body.
/// Returns true when the person asked to close it (× , Esc, or a click outside when `outside_closes`).
fn dialog ( ctx : & egui ::Context , id : & str , title : & str , width : f32 , outside_closes : bool , p : Palette , content : impl FnOnce ( & mut egui ::Ui ) ) -> bool {
let dark = p . pane ! = LIGHT . pane ;
let mut close = false ;
let resp = egui ::Modal ::new ( egui ::Id ::unique ( ( " dialog " , id ) ) )
. backdrop_color ( Color32 ::from_black_alpha ( if dark { 130 } else { 70 } ) )
. frame (
Frame ::new ( )
. fill ( p . elevated )
. stroke ( Stroke ::new ( 1.0 , p . border ) )
. corner_radius ( 16 )
. shadow ( egui ::epaint ::Shadow { offset : [ 0 , 12 ] , blur : 36 , spread : 0 , color : Color32 ::from_black_alpha ( if dark { 110 } else { 45 } ) } ) ,
)
. show ( ctx , | ui | {
// Never wider than the window (a narrow window keeps the whole dialog on screen).
ui . set_width ( width . min ( ctx . content_rect ( ) . width ( ) - 32.0 ) . max ( 280.0 ) ) ;
// Header, body and footer sit flush; each sets its own inner spacing.
ui . spacing_mut ( ) . item_spacing . y = 0.0 ;
Frame ::new ( ) . inner_margin ( Margin { left : 20 , right : 12 , top : 12 , bottom : 12 } ) . show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
ui . horizontal ( | ui | {
ui . label ( RichText ::new ( t ( title ) ) . strong ( ) . size ( FONT_XL ) ) ;
ui . add_space ( ( ui . available_width ( ) - 28.0 ) . max ( 0.0 ) ) ;
// Drawn, not a glyph: not every Chinese font has "✕".
let ( rect , r ) = ui . allocate_exact_size ( vec2 ( 28.0 , 28.0 ) , egui ::Sense ::click ( ) ) ;
if r . hovered ( ) {
ui . painter ( ) . rect_filled ( rect , 8 , p . hover ) ;
}
let c = rect . center ( ) ;
let stroke = Stroke ::new ( 1.5 , if r . hovered ( ) { p . text } else { p . text2 } ) ;
ui . painter ( ) . line_segment ( [ c + vec2 ( - 5.0 , - 5.0 ) , c + vec2 ( 5.0 , 5.0 ) ] , stroke ) ;
ui . painter ( ) . line_segment ( [ c + vec2 ( - 5.0 , 5.0 ) , c + vec2 ( 5.0 , - 5.0 ) ] , stroke ) ;
if r . on_hover_text ( t ( " 關閉( Esc) " ) ) . clicked ( ) {
close = true ;
}
} ) ;
} ) ;
let r = ui . min_rect ( ) ;
ui . painter ( ) . hline ( r . x_range ( ) , r . bottom ( ) , Stroke ::new ( 1.0 , p . border ) ) ;
content ( ui ) ;
} ) ;
let escape = ctx . input ( | i | i . key_pressed ( Key ::Escape ) ) ;
close | | escape | | ( outside_closes & & resp . backdrop_response . clicked ( ) )
}
/// The scrolling middle of a dialog, `reserve` px kept free for its footer.
fn dialog_body < R > ( ui : & mut egui ::Ui , id : impl std ::hash ::Hash + std ::fmt ::Debug , max_height : f32 , body : impl FnOnce ( & mut egui ::Ui ) -> R ) -> R {
egui ::ScrollArea ::vertical ( )
. id_salt ( id )
. max_height ( max_height )
. auto_shrink ( [ false , true ] )
. show ( ui , | ui | {
Frame ::new ( ) . inner_margin ( Margin { left : 20 , right : 20 , top : 16 , bottom : 16 } ) . show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
ui . spacing_mut ( ) . item_spacing . y = 6.0 ;
body ( ui )
} )
. inner
} )
. inner
}
/// The bottom bar of a dialog: a line above, buttons laid out right to left.
fn dialog_footer ( ui : & mut egui ::Ui , p : Palette , body : impl FnOnce ( & mut egui ::Ui ) ) {
let top = ui . cursor ( ) . top ( ) ;
ui . painter ( ) . hline ( ui . max_rect ( ) . x_range ( ) , top , Stroke ::new ( 1.0 , p . border ) ) ;
Frame ::new ( ) . inner_margin ( Margin ::symmetric ( 20 , 12 ) ) . show ( ui , | ui | {
ui . set_width ( ui . available_width ( ) ) ;
ui . spacing_mut ( ) . item_spacing . y = 6.0 ;
ui . horizontal ( | ui | body ( ui ) ) ;
} ) ;
}
#[ cfg(test) ]
mod tests {
#[ test ]
fn group_members_see_each_other_as_labelled_words ( ) {
let st = State ::demo ( ) ;
let ( a , b ) = ( st . bots [ 1 ] . id , st . bots [ 2 ] . id ) ;
let mut tool_step = Message { author : a , .. Message ::assistant ( " " , " x " ) } ;
tool_step . tool_calls . push ( crate ::model ::ToolCall { id : " t1 " . into ( ) , name : " shell " . into ( ) , args : " {} " . into ( ) , status : Default ::default ( ) } ) ;
let msgs = vec! [
Message ::user ( " 大家覺得呢? " ) ,
tool_step ,
Message ::tool_result ( " t1 " , " shell " , " secret output " ) ,
Message { author : a , reactions : vec ! [ " 👍 " . into ( ) ] , .. Message ::assistant ( " 我覺得可以 " , " x " ) } ,
] ;
let h = super ::group_history ( & msgs , Some ( b ) , & st ) ;
assert_eq! ( h . len ( ) , 2 , " {h:?} " ) ;
assert_eq! ( h [ 1 ] . role , Role ::User ) ;
assert! ( h [ 1 ] . text . starts_with ( & format! ( " 【 {} 】我覺得可以 " , st . bots [ 1 ] . name ) ) & & h [ 1 ] . text . contains ( " 👍 " ) ) ;
// The speaker's own steps stay as they are.
let own = super ::group_history ( & msgs , Some ( a ) , & st ) ;
assert_eq! ( own . len ( ) , 4 ) ;
}
#[ test ]
fn transcript_export_reads_like_a_chat ( ) {
let st = State ::demo ( ) ;
let b = & st . bots [ 0 ] ;
let md = super ::transcript_markdown ( b ) ;
assert! ( md . starts_with ( & format! ( " # {} 的對話 " , b . name ) ) ) ;
assert! ( md . contains ( " **你**: " ) ) ;
}
use super ::* ;
/// The relaunch helper waits for the old copy to exit before starting the new one.
#[ test ]
fn relaunch_waits_for_the_old_copy ( ) {
let dir = std ::env ::temp_dir ( ) . join ( format! ( " anybot-relaunch- {} " , std ::process ::id ( ) ) ) ;
std ::fs ::create_dir_all ( & dir ) . unwrap ( ) ;
let marker = dir . join ( " started " ) ;
let fake_app = dir . join ( " fake-app.sh " ) ;
std ::fs ::write ( & fake_app , format! ( " #!/bin/sh \n echo \" $ {{ ANYBOT_SHOT_VIEW:-clean }} \" > ' {} ' \n " , marker . display ( ) ) ) . unwrap ( ) ;
std ::process ::Command ::new ( " chmod " ) . arg ( " +x " ) . arg ( & fake_app ) . status ( ) . unwrap ( ) ;
let mut old = std ::process ::Command ::new ( " sleep " ) . arg ( " 1 " ) . spawn ( ) . unwrap ( ) ;
// A leftover start-up switch in this copy's environment must not reach the new copy.
std ::env ::set_var ( " ANYBOT_SHOT_VIEW " , " leftover " ) ;
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 mut relaunched = relaunch_command ( old . id ( ) , & fake_app ) . spawn ( ) . unwrap ( ) ;
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
std ::env ::remove_var ( " ANYBOT_SHOT_VIEW " ) ;
std ::thread ::sleep ( std ::time ::Duration ::from_millis ( 500 ) ) ;
assert! ( ! marker . exists ( ) , " started before the old copy exited " ) ;
old . wait ( ) . unwrap ( ) ;
std ::thread ::sleep ( std ::time ::Duration ::from_millis ( 700 ) ) ;
assert_eq! ( std ::fs ::read_to_string ( & marker ) . unwrap ( ) . trim ( ) , " clean " ) ;
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
relaunched . wait ( ) . unwrap ( ) ;
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
}
}