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
<!doctype html>
< html lang = "zh-Hant" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" >
< meta name = "apple-mobile-web-app-capable" content = "yes" >
< meta name = "apple-mobile-web-app-status-bar-style" content = "black-translucent" >
< meta name = "apple-mobile-web-app-title" content = "AnyBot" >
< meta name = "theme-color" content = "#121212" media = "(prefers-color-scheme: dark)" >
< meta name = "theme-color" content = "#ffffff" media = "(prefers-color-scheme: light)" >
< link rel = "manifest" href = "manifest.webmanifest" >
2026-11-05 11:20:29 +00:00
< link rel = "icon" href = "icon.svg" type = "image/svg+xml" >
< link rel = "icon" href = "icon-192.png" type = "image/png" sizes = "192x192" >
< link rel = "apple-touch-icon" href = "icon-180.png" >
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
< title > AnyBot< / title >
< style >
:root{
--bg:#ffffff;--side:#f7f7f7;--surface:#ffffff;--raise:#f2f2f2;--line:rgba(0,0,0,.08);
--text:#141414;--text2:#5c5c5c;--text3:#8e8e8e;--accent:#1680f3;--accent-ink:#fff;
--user:rgba(22,128,243,.12);--danger:#e5484d;--ok:#2ea44f;--warn:#f5a524;
--radius:18px;--font:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang TC","Noto Sans TC",system-ui,sans-serif;
--mono:ui-monospace,SFMono-Regular,Menlo,monospace;
}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
--bg:#121212;--side:#0e0e0e;--surface:#1f1f1f;--raise:#161616;--line:rgba(255,255,255,.08);
--text:#f5f5f5;--text2:#c1c1c1;--text3:#8a8a8a;--user:rgba(255,255,255,.18);
}}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:15px/1.5 var(--font);-webkit-font-smoothing:antialiased}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
input,textarea{font:inherit;color:var(--text)}
.app{height:100dvh;display:flex;flex-direction:column;overflow:hidden}
.view{flex:1;display:none;flex-direction:column;min-height:0}
.view.on{display:flex}
header{display:flex;align-items:center;gap:10px;padding:calc(env(safe-area-inset-top) + 10px) 16px 10px;border-bottom:1px solid var(--line);background:var(--bg);min-height:56px}
header h1{font-size:17px;font-weight:600;margin:0;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
header .sub{font-size:12px;color:var(--text3);font-weight:400}
.icon-btn{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--text2)}
.icon-btn:active{background:var(--raise)}
.icon-btn svg{width:20px;height:20px}
.scroll{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}
2026-11-05 11:20:29 +00:00
/* A list that reaches the bottom of the screen keeps its last row above the iPhone's home bar and
Safari's toolbar (it looked cut off). */
.view > .scroll:last-child{padding-bottom:calc(env(safe-area-inset-bottom) + 16px)}
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
/* login */
.login{flex:1;display:flex;flex-direction:column;justify-content:center;padding:32px 24px calc(env(safe-area-inset-bottom) + 32px);max-width:420px;margin:0 auto;width:100%}
.logo{width:56px;height:56px;margin-bottom:20px}
.login h2{font-size:26px;margin:0 0 6px;font-weight:650;letter-spacing:-.01em}
.login p{color:var(--text2);margin:0 0 24px}
.field{display:block;width:100%;padding:13px 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface);margin-bottom:10px;outline:none;font-size:16px}
.field:focus{border-color:var(--accent)}
.primary{display:block;width:100%;padding:13px;border-radius:12px;background:var(--text);color:var(--bg);font-weight:600;margin-top:6px}
.primary:disabled{opacity:.45}
.err{color:var(--danger);font-size:13px;min-height:20px;margin-top:8px}
.link{color:var(--accent);font-size:14px;margin-top:14px}
/* list */
.row{display:flex;gap:12px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line)}
.row:active{background:var(--raise)}
.av{width:42px;height:42px;border-radius:50%;flex:none;display:grid;place-items:center;font-weight:600;color:#fff;font-size:16px;position:relative;box-shadow:inset 0 0 0 1px var(--line)}
2026-11-05 11:20:29 +00:00
.av.face{background:none !important;box-shadow:none;border-radius:0}
.av.face svg{width:100%;height:100%;display:block}
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
.av.sm{width:26px;height:26px;font-size:12px}
.row .mid{flex:1;min-width:0}
.row .name{font-weight:600;display:flex;gap:6px;align-items:center}
.row .last{color:var(--text3);font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.badge{min-width:9px;height:9px;border-radius:5px;background:var(--accent)}
.busy{font-size:12px;color:var(--accent);font-weight:500}
.wait{font-size:12px;color:var(--warn);font-weight:600}
.empty{color:var(--text3);text-align:center;padding:48px 24px}
.pill{font-size:11.5px;padding:2px 8px;border-radius:999px;background:var(--raise);color:var(--text2);font-weight:500}
/* chat */
.msgs{padding:12px 0 8px}
.m{display:flex;flex-direction:column;padding:4px 14px;max-width:100%}
.m.user{align-items:flex-end}
.bubble{max-width:min(85%,560px);padding:7px 12px;border-radius:var(--radius);white-space:pre-wrap;word-wrap:break-word;overflow-wrap:anywhere}
.m.user .bubble{background:var(--user)}
.m.bot .bubble{padding:4px 2px;max-width:100%}
.m.err .bubble{color:var(--danger)}
.who{font-size:12px;color:var(--text3);margin:6px 2px 2px;display:flex;align-items:center;gap:6px}
.via{font-size:11.5px;color:var(--text3);margin:2px 2px 4px}
.bubble p{margin:0 0 .55em}.bubble p:last-child{margin:0}
.bubble pre{background:var(--raise);padding:10px 12px;border-radius:10px;overflow-x:auto;font:12.5px/1.45 var(--mono);white-space:pre;margin:6px 0}
.bubble code{font:13px var(--mono);background:var(--raise);padding:1px 5px;border-radius:5px}
.bubble pre code{background:none;padding:0}
.bubble a{color:var(--accent)}
.bubble ul,.bubble ol{margin:.2em 0 .5em;padding-left:1.3em}
.bubble h1,.bubble h2,.bubble h3{font-size:16px;margin:.6em 0 .3em}
.bubble blockquote{margin:0 0 .5em;padding-left:10px;border-left:3px solid var(--line);color:var(--text2)}
.tool{display:flex;gap:8px;align-items:baseline;font-size:13px;color:var(--text2);padding:1px 4px}
.tool .st{width:14px;text-align:center;color:var(--text3);flex:none;display:inline-flex}
.ic{vertical-align:-2px;flex:none}
.tool.waiting .st{color:var(--accent)}.tool.failed .st{color:var(--danger)}
.tool .d{font:12px var(--mono);color:var(--text3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.att{display:inline-flex;gap:6px;align-items:center;font-size:12.5px;background:var(--raise);border-radius:8px;padding:3px 8px;margin:3px 0 0}
.card{border:1px solid var(--line);border-radius:14px;padding:12px 14px;margin:6px 0;background:var(--surface);max-width:420px}
.card .t{font-weight:600}.card .s{color:var(--text3);font-size:13px;margin-bottom:6px}
.card .r{display:flex;justify-content:space-between;gap:10px;font-size:13.5px;padding:3px 0;border-top:1px solid var(--line)}
.card .r span:first-child{color:var(--text2)}
頁面: 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 card in a chat, and the page */
.pcard{border:1px solid var(--line);border-radius:16px;margin:8px 0;background:var(--surface);max-width:520px;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:transparent}
.pcard:active{background:var(--raise)}
.pcard .top{padding:14px 16px 10px}
.pcard .t{font-weight:700;font-size:17px;margin-bottom:6px}
.pcard .l{font-size:13.5px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.55}
.pcard .foot{display:flex;gap:12px;align-items:center;padding:10px 16px;border-top:1px solid var(--line)}
.pcard .n{font-size:14px}.pcard .m{font-size:12px;color:var(--text3);padding:0;display:block}
.pcard .note{font-size:12px;color:var(--text2);padding:0 16px 10px}
.pglyph{flex:none}
a.plink{color:var(--accent);text-decoration:underline;text-underline-offset:3px;cursor:pointer}
2026-11-05 00:33:23 +00:00
.page{padding:18px 18px calc(env(safe-area-inset-bottom) + 40px);max-width:760px;margin:0 auto;line-height:1.65;font-size:15.5px;box-sizing:border-box;width:100%;min-width:0;overflow-wrap:anywhere}
#pscroll{overflow-x:hidden}
#v-page header{min-width:0}
頁面: 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 h1.pt{font-size:25px;line-height:1.3;margin:0 0 6px}
.page .meta{color:var(--text3);font-size:12.5px;margin-bottom:12px}
.page .sum{font-size:16px;margin:0 0 6px}
.page .chg{font-size:12.5px;color:var(--text2);margin-bottom:8px}
.page .old{background:var(--raise);border-radius:8px;padding:8px 10px;font-size:13px;color:var(--text2);margin-bottom:10px}
.page h2{font-size:19px;margin:1.3em 0 .4em}.page h3{font-size:16.5px;margin:1.1em 0 .3em}.page h4{font-size:15px;margin:1em 0 .3em}
.page p{margin:0 0 .75em}
.page ul,.page ol{margin:.2em 0 .8em;padding-left:1.3em}.page li{margin:.15em 0}
.page li.task{list-style:none;margin-left:-1.2em}
.page a{color:var(--accent)}
.page code{font:13px var(--mono);background:var(--raise);padding:1px 5px;border-radius:5px}
.page pre{background:var(--raise);padding:10px 12px;border-radius:10px;overflow-x:auto;font:12.5px/1.45 var(--mono)}
.page pre code{background:none;padding:0}
.page hr{border:0;border-top:1px solid var(--line);margin:1.2em 0}
.page blockquote{margin:0 0 .8em;padding-left:12px;border-left:3px solid var(--line);color:var(--text2)}
2026-11-05 00:33:23 +00:00
.tw{overflow-x:auto;margin:0 0 1em;border:1px solid var(--line);border-radius:10px;max-width:100%}
.page td{min-width:5em;overflow-wrap:normal;word-break:normal}
頁面: 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 table{border-collapse:collapse;width:100%;font-size:14px}
.page th,.page td{padding:7px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
.page th{background:var(--raise);font-weight:600;white-space:nowrap}
.page tr:last-child td{border-bottom:0}
.stats{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin:0 0 1em}
.stat{background:var(--raise);border-radius:12px;padding:10px 12px}
2026-11-05 00:33:23 +00:00
.stat .k{font-size:12px;color:var(--text2)}.stat .v{font-size:21px;font-weight:700;line-height:1.3}.stat .v.long{font-size:15.5px}.stat .s{font-size:11.5px;color:var(--text3)}
頁面: 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
.steps{margin:0 0 1em;padding:0;list-style:none}
.steps li{position:relative;padding:0 0 14px 30px}
.steps li:before{content:"";position:absolute;left:8px;top:20px;bottom:-2px;width:2px;background:var(--line)}
.steps li:last-child:before{display:none}
.steps li.done:before{background:var(--ok)}
.steps .dot{position:absolute;left:0;top:3px;width:18px;height:18px;border-radius:50%;box-sizing:border-box;border:1.5px solid var(--text3);background:var(--bg);display:grid;place-items:center;color:#fff;font-size:11px;animation:none;margin:0}
.steps .done .dot{background:var(--ok);border-color:var(--ok)}
.steps .now .dot{border:2.5px solid var(--accent)}
.steps .now .dot:after{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent)}
.steps .todo .nm{color:var(--text3)}.steps .now .nm{font-weight:700}
.steps .nt{font-size:13px;color:var(--text2)}
.call{border-radius:10px;padding:10px 12px 10px 14px;margin:0 0 1em;border-left:3px solid var(--accent);background:rgba(22,128,243,.08)}
.call .ct{font-size:12.5px;font-weight:700;color:var(--accent);margin-bottom:2px}
.call.warning{border-color:#e08a00;background:rgba(224,138,0,.10)}.call.warning .ct{color:#e08a00}
.call.important{border-color:#d03b3b;background:rgba(208,59,59,.09)}.call.important .ct{color:#d03b3b}
.call.tip{border-color:var(--ok);background:rgba(46,160,67,.09)}.call.tip .ct{color:var(--ok)}
.call p:last-child{margin:0}
.chart{margin:0 0 1em;text-align:center}.chart img{max-width:100%;height:auto}
.chart .wait{color:var(--text3);font-size:13px;padding:20px}
2026-11-05 00:33:23 +00:00
#pver{border:1px solid var(--line);background:var(--surface);border-radius:8px;padding:4px 6px;font-size:13px;color:var(--text);max-width:42%;flex: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
.typing{display:inline-flex;gap:4px;padding:8px 4px}
.typing i{width:6px;height:6px;border-radius:50%;background:var(--text3);animation:b 1.2s infinite}
.typing i:nth-child(2){animation-delay:.15s}.typing i:nth-child(3){animation-delay:.3s}
@keyframes b{0%,60%,100%{opacity:.25}30%{opacity:1}}
/* approval */
.ask{margin:8px 12px;border:1px solid var(--line);border-radius:16px;background:var(--surface);padding:14px;box-shadow:0 6px 24px rgba(0,0,0,.12)}
.ask .q{font-weight:600;margin-bottom:4px}
.ask .why{font-size:13.5px;color:var(--text2);white-space:pre-wrap;max-height:30vh;overflow:auto;margin-bottom:12px;font-family:var(--mono)}
.ask .btns{display:flex;gap:8px;flex-wrap:wrap}
.btn{padding:9px 14px;border-radius:10px;background:var(--raise);font-weight:500;font-size:14px}
.btn.main{background:var(--text);color:var(--bg)}
.btn.no{color:var(--danger)}
/* composer */
.composer{display:flex;gap:8px;align-items:flex-end;padding:8px 10px calc(env(safe-area-inset-bottom) + 8px);border-top:1px solid var(--line);background:var(--bg)}
.composer textarea{flex:1;resize:none;border:1px solid var(--line);background:var(--surface);border-radius:20px;padding:9px 14px;max-height:140px;outline:none;font-size:16px;line-height:1.35}
.send{width:38px;height:38px;border-radius:50%;background:var(--text);color:var(--bg);display:grid;place-items:center;flex:none}
.send:disabled{opacity:.3}
.send svg,.clip svg{width:18px;height:18px}
.clip{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;color:var(--text2);flex:none}
.chips{display:flex;gap:6px;flex-wrap:wrap;padding:0 12px}
.chip{font-size:12.5px;background:var(--raise);border-radius:8px;padding:4px 8px;display:flex;gap:6px;align-items:center}
/* screen */
.seg{display:flex;background:var(--raise);border-radius:10px;padding:3px;gap:3px}
.seg button{padding:6px 12px;border-radius:8px;font-size:13.5px;color:var(--text2)}
.seg button.on{background:var(--surface);color:var(--text);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12)}
.stage{flex:1;display:flex;align-items:center;justify-content:center;background:#000;position:relative;overflow:hidden;touch-action:pan-x pan-y pinch-zoom}
.stage img{max-width:100%;max-height:100%;display:block;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.stage .msg{color:#bbb;text-align:center;padding:24px}
.dot{position:absolute;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;border:2px solid #fff;background:rgba(22,128,243,.45);pointer-events:none;animation:f .5s forwards}
@keyframes f{to{opacity:0;transform:scale(1.6)}}
.tools{display:flex;gap:6px;padding:8px 10px calc(env(safe-area-inset-bottom) + 8px);overflow-x:auto;border-top:1px solid var(--line);background:var(--bg)}
.tools .btn{white-space:nowrap;flex:none}
.typebar{display:none;gap:8px;padding:8px 10px;border-top:1px solid var(--line)}
.typebar.on{display:flex}
.typebar input{flex:1;border:1px solid var(--line);background:var(--surface);border-radius:10px;padding:9px 12px;font-size:16px;outline:none}
.live{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:6px}
.sheet-bg{position:fixed;inset:0;background:rgba(0,0,0,.35);display:none;align-items:flex-end;z-index:10}
.sheet-bg.on{display:flex}
.sheet{width:100%;background:var(--surface);border-radius:18px 18px 0 0;padding:8px 8px calc(env(safe-area-inset-bottom) + 10px)}
.sheet button{display:block;width:100%;text-align:left;padding:14px 16px;border-radius:12px;font-size:16px}
.sheet button:active{background:var(--raise)}
.sheet .no{color:var(--danger)}
2026-11-04 12:56:06 +00:00
.pair{padding:18px 16px calc(env(safe-area-inset-bottom) + 12px)}
.pair-t{font-size:18px;font-weight:600;margin:2px 4px 6px}
.pair-code{font:600 34px/1.2 ui-monospace,Menlo,monospace;letter-spacing:6px;text-align:center;margin:16px 0 10px}
.pair-line{display:block;font:13px/1.5 ui-monospace,Menlo,monospace;background:var(--raise);border-radius:10px;padding:10px 12px;word-break:break-all;margin-bottom:12px;user-select:all}
.sheet .pair-copy,.pair .btn.main{text-align:center;padding:13px}
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
.sheet .note{color:var(--text3);font-size:12.5px;padding:4px 16px 8px}
.toast{position:fixed;left:50%;bottom:calc(env(safe-area-inset-bottom) + 90px);transform:translateX(-50%);background:var(--text);color:var(--bg);padding:9px 16px;border-radius:12px;font-size:14px;opacity:0;transition:opacity .2s;pointer-events:none;max-width:88vw;text-align:center}
.toast.on{opacity:.95}
< / style >
< / head >
< body >
< div class = "app" >
<!-- sign in -->
< section class = "view" id = "v-login" >
< div class = "login" >
< svg class = "logo" viewBox = "0 0 96 96" > < rect width = "96" height = "96" rx = "22" fill = "currentColor" opacity = ".08" / > < circle cx = "48" cy = "48" r = "24" fill = "none" stroke = "currentColor" stroke-width = "7" / > < circle cx = "48" cy = "48" r = "7" fill = "currentColor" / > < / svg >
< h2 id = "login-title" > 登入 AnyBot< / h2 >
< p id = "login-desc" > 用手機和你的 Bot 聊天、核准它要做的事,也能看著它的電腦。< / p >
< input class = "field" id = "f-user" placeholder = "帳號" autocomplete = "username" autocapitalize = "off" >
< input class = "field" id = "f-pass" type = "password" placeholder = "密碼" autocomplete = "current-password" >
< input class = "field" id = "f-invite" placeholder = "邀請碼(伺服器有設定才需要)" autocapitalize = "off" style = "display:none" >
< button class = "primary" id = "b-login" > 登入< / button >
< div class = "err" id = "login-err" > < / div >
< button class = "link" id = "b-mode" style = "display:none" > 還沒有帳號?建立一個< / button >
< / div >
< / section >
<!-- pick which AnyBot (account with several computers) -->
< section class = "view" id = "v-hosts" >
< header > < h1 > 選擇要連的 AnyBot< / h1 > < button class = "icon-btn" id = "b-out1" title = "登出" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.8" > < path d = "M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 17l-5-5 5-5M5 12h11" / > < / svg > < / button > < / header >
< div class = "scroll" id = "hosts" > < / div >
< / section >
<!-- bots -->
< section class = "view" id = "v-list" >
< header >
< h1 > AnyBot < span class = "sub" id = "host-name" > < / span > < / h1 >
< button class = "icon-btn" id = "b-screen" title = "看電腦畫面" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.8" > < rect x = "3" y = "4" width = "18" height = "12" rx = "2" / > < path d = "M8 20h8M12 16v4" / > < / svg > < / button >
< button class = "icon-btn" id = "b-more" title = "更多" > < svg viewBox = "0 0 24 24" fill = "currentColor" > < circle cx = "5" cy = "12" r = "1.7" / > < circle cx = "12" cy = "12" r = "1.7" / > < circle cx = "19" cy = "12" r = "1.7" / > < / svg > < / button >
< / header >
< div class = "scroll" id = "bots" > < / div >
< / section >
<!-- one chat -->
< section class = "view" id = "v-chat" >
< header >
< button class = "icon-btn" id = "b-back" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M15 5l-7 7 7 7" / > < / svg > < / button >
< div class = "av sm" id = "chat-av" > < / div >
< h1 id = "chat-name" > < / h1 >
< button class = "icon-btn" id = "b-new" title = "開始新對話" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.8" > < path d = "M12 5v14M5 12h14" / > < / svg > < / button >
< button class = "icon-btn" id = "b-screen2" title = "看電腦畫面" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.8" > < rect x = "3" y = "4" width = "18" height = "12" rx = "2" / > < path d = "M8 20h8M12 16v4" / > < / svg > < / button >
< / header >
< div class = "scroll" id = "scroller" > < div class = "msgs" id = "msgs" > < / div > < / div >
< div id = "ask" > < / div >
< div class = "chips" id = "chips" > < / div >
< div class = "composer" >
< button class = "clip" id = "b-clip" title = "附加照片或檔案" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.8" > < path d = "M21 11.5l-8.5 8.5a5 5 0 0 1-7-7L14 4.5a3.3 3.3 0 0 1 4.7 4.7L10.2 17.7a1.7 1.7 0 0 1-2.4-2.4L15.5 7.6" / > < / svg > < / button >
< input type = "file" id = "file" multiple hidden >
< textarea id = "input" rows = "1" placeholder = "傳訊息…" enterkeyhint = "send" > < / textarea >
< button class = "send" id = "b-send" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.2" > < path d = "M12 19V5M6 11l6-6 6 6" / > < / svg > < / button >
< / div >
< / section >
頁面: 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
< section class = "view" id = "v-page" >
< header >
< button class = "icon-btn" id = "b-back3" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M15 5l-7 7 7 7" / > < / svg > < / button >
< h1 id = "page-name" style = "flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" > 頁面< / h1 >
< select id = "pver" style = "display:none" > < / select >
< / header >
< div class = "scroll" id = "pscroll" > < div class = "page" id = "page" > < / div > < / div >
< / section >
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
<!-- screens -->
< section class = "view" id = "v-screen" >
< header >
< button class = "icon-btn" id = "b-back2" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M15 5l-7 7 7 7" / > < / svg > < / button >
< div class = "seg" id = "seg" > < button data-w = "box" class = "on" > Bot 的電腦< / button > < button data-w = "mac" > 這台 Mac< / button > < / div >
< span style = "flex:1" > < / span >
< span class = "sub" id = "screen-live" > < / span >
< / header >
< div class = "stage" id = "stage" > < div class = "msg" > 載入中…< / div > < / div >
< div class = "typebar" id = "typebar" > < input id = "typein" placeholder = "要打的字(會打進目前的視窗)" enterkeyhint = "send" > < button class = "btn main" id = "b-type" > 送出< / button > < / div >
< div class = "tools" >
< button class = "btn" id = "k-type" > < svg class = "ic" width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M10 8h.01" / > < path d = "M12 12h.01" / > < path d = "M14 8h.01" / > < path d = "M16 12h.01" / > < path d = "M18 8h.01" / > < path d = "M6 8h.01" / > < path d = "M7 16h10" / > < path d = "M8 12h.01" / > < rect width = "20" height = "16" x = "2" y = "4" rx = "2" / > < / svg > 打字< / button >
< button class = "btn" data-key = "Return" > Enter< / button >
< button class = "btn" data-key = "Escape" > Esc< / button >
< button class = "btn" data-key = "Tab" > Tab< / button >
< button class = "btn" data-key = "BackSpace" > 刪除< / button >
< button class = "btn" data-scroll = "up" > ↑ 捲動< / button >
< button class = "btn" data-scroll = "down" > ↓ 捲動< / button >
< button class = "btn" id = "k-right" > 右鍵:關< / button >
< button class = "btn" id = "k-wake" > 叫醒< / button >
< / div >
< / section >
< / div >
< div class = "toast" id = "toast" > < / div >
< div class = "sheet-bg" id = "sheet" > < div class = "sheet" >
< button id = "m-notify" > 開啟通知< / button >
< div class = "note" id = "m-note" > Bot 需要你允許、或在你不在時做完事,手機會跳通知。< / div >
2026-11-04 12:56:06 +00:00
< button id = "m-pair" style = "display:none" > 加一台電腦( Linux、伺服器) < / button >
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
< button id = "m-host" style = "display:none" > 切換到別台 AnyBot< / button >
< button class = "no" id = "m-out" > 登出這支手機< / button >
< button id = "m-cancel" > 取消< / button >
< / div > < / div >
2026-11-04 12:56:06 +00:00
< div class = "sheet-bg" id = "pair" > < div class = "sheet pair" >
< div class = "pair-t" > 加一台電腦< / div >
One-step install: install.sh, fixed Linux and server builds, clearer README
- install.sh (curl | sh): app, hands, server and deps modes; installs system packages
(right names per release, e.g. libasound2t64 on Ubuntu 24.04), Rust via rustup (1.95+),
Docker for the server; limits parallel compiles on small machines
- Server Docker image: add the ALSA build/runtime packages (the build failed), cache the
cargo registry and target between rebuilds, JOBS build arg
- 加一台電腦 now gives a line that installs AnyBot on a blank computer before joining it
- Makefile: make deps installs Rust too, make doctor, clear message when Rust is missing/old
- README rewritten: one line per use, getting started, update/remove, troubleshooting,
deployment options; docs/install.md and multi-device.md follow it
- Tested on fresh ubuntu:24.04 (app) and debian:12 (curl | sh hands) containers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 16:52:10 +00:00
< div class = "note" > 在那台電腦( Linux 或 Mac) 的終端機貼上這一行執行: 還沒裝 AnyBot 會先自動裝好,接著變成「< b id = "pair-for" > < / b > 」的手腳,開機自動執行。不用帳號密碼。< / div >
2026-11-04 12:56:06 +00:00
< div class = "pair-code" id = "pair-code" > ······< / div >
< code class = "pair-line" id = "pair-line" > < / code >
< button class = "btn main" id = "pair-copy" > 複製這一行< / button >
< div class = "note" id = "pair-left" > < / div >
< button id = "pair-close" > 關閉< / button >
< / div > < / div >
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
< script >
"use strict";
// Lucide icons (ISC licence), the same set as the app.
const LUCIDE = {"image": "< rect width = \"18\" height = \"18\" x = \"3\" y = \"3\" rx = \"2\" ry = \"2\" / > < circle cx = \"9\" cy = \"9\" r = \"2\" / > < path d = \"m21 15-3 . 086-3 . 086a2 2 0 0 0-2 . 828 0L6 21 \ " / > ", "file": "< path d = \"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2 . 4 2 . 4 0 0 1 1 . 704 . 706l3 . 588 3 . 588A2 . 4 2 . 4 0 0 1 20 8v12a2 2 0 0 1-2 2z \ " / > < path d = \"M14 2v5a1 1 0 0 0 1 1h5 \ " / > ", "circle-check": "< circle cx = \"12\" cy = \"12\" r = \"10\" / > < path d = \"m16 9-5 . 5 5 . 5L8 12 \ " / > ", "loader": "< path d = \"M12 2v4 \ " / > < path d = \"m16.2 7 . 8 2 . 9-2 . 9 \ " / > < path d = \"M18 12h4 \ " / > < path d = \"m16.2 16 . 2 2 . 9 2 . 9 \ " / > < path d = \"M12 18v4 \ " / > < path d = \"m4.9 19 . 1 2 . 9-2 . 9 \ " / > < path d = \"M2 12h4 \ " / > < path d = \"m4.9 4 . 9 2 . 9 2 . 9 \ " / > ", "circle-alert": "< circle cx = \"12\" cy = \"12\" r = \"10\" / > < line x1 = \"12\" x2 = \"12\" y1 = \"8\" y2 = \"12\" / > < line x1 = \"12\" x2 = \"12.01\" y1 = \"16\" y2 = \"16\" / > ", "minus": "< path d = \"M5 12h14 \ " / > ", "circle-x": "< circle cx = \"12\" cy = \"12\" r = \"10\" / > < path d = \"m15 9-6 6 \ " / > < path d = \"m9 9 6 6 \ " / > ", "x": "< path d = \"M18 6 6 18 \ " / > < path d = \"m6 6 12 12 \ " / > ", "keyboard": "< path d = \"M10 8h . 01 \ " / > < path d = \"M12 12h . 01 \ " / > < path d = \"M14 8h . 01 \ " / > < path d = \"M16 12h . 01 \ " / > < path d = \"M18 8h . 01 \ " / > < path d = \"M6 8h . 01 \ " / > < path d = \"M7 16h10 \ " / > < path d = \"M8 12h . 01 \ " / > < rect width = \"20\" height = \"16\" x = \"2\" y = \"4\" rx = \"2\" / > ", "paperclip": "< path d = \"m16 6-8 . 414 8 . 586a2 2 0 0 0 2 . 829 2 . 829l8 . 414-8 . 586a4 4 0 1 0-5 . 657-5 . 657l-8 . 379 8 . 551a6 6 0 1 0 8 . 485 8 . 485l8 . 379-8 . 551 \ " / > ", "alarm-clock": "< circle cx = \"12\" cy = \"13\" r = \"8\" / > < path d = \"M12 9v4l2 2 \ " / > < path d = \"M5 3 2 6 \ " / > < path d = \"m22 6-3-3 \ " / > < path d = \"M6.38 18 . 7 4 21 \ " / > < path d = \"M17.64 18 . 67 20 21 \ " / > "};
function ic(name, size = 16) { return `< svg class = "ic" width = "${size}" height = "${size}" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" > ${LUCIDE[name] || ""}< / svg > `; }
const $ = (id) => document.getElementById(id);
const S = {
relay: false, token: localStorage.getItem("anybot.token") || "", host: localStorage.getItem("anybot.host") || "",
version: 0, bots: [], approvals: [], open: 0, msgs: [], screen: "box", right: false, pending: [], hostName: "", mac: false,
polling: false, frameTimer: null, size: [0, 0],
};
// The app sends each bot's colour as #rrggbb (the same palette as the window).
const color = (c) => (/^#[0-9a-f]{6}$/i.test(String(c || "")) ? c : "#1c1c1c");
2026-11-05 11:20:29 +00:00
// A bot's face as in the app (src/avatar.rs): its shape in its colour, two slanted oval eyes.
// Drawn in a -1..1 box; the phone used to show the name's first letter in a circle.
function avatarSvg(shape, hex) {
const col = color(hex), P = Math.PI, f = (n) => n.toFixed(3);
const pts = (a) => `< polygon points = "${a.map(([x, y]) => f(x) + " , " + f ( y ) ) . join ( " " ) } " / > `;
const circ = (x, y, r) => `< circle cx = "${f(x)}" cy = "${f(y)}" r = "${f(r)}" / > `;
const range = (n) => [...Array(n).keys()];
let body;
switch (shape) {
case "squircle": body = `< rect x = "-0.92" y = "-0.92" width = "1.84" height = "1.84" rx = "0.42" / > `; break;
case "capsule": body = `< rect x = "-0.675" y = "-1" width = "1.35" height = "2" rx = "0.66" / > `; break;
case "egg": body = pts(range(48).map((i) => { const t = i / 48 * 2 * P; return [0.8 * Math.cos(t), 0.08 + (Math.sin(t) < 0 ? 1 : 0 . 86 ) * Math . sin ( t ) ] ; } ) ) ; break ;
case "hex": body = pts(range(6).map((i) => { const a = i / 6 * 2 * P + P / 6; return [Math.cos(a), Math.sin(a)]; })); break;
case "gem": body = pts([[-0.55, -0.8], [0.55, -0.8], [1, -0.2], [0, 1], [-1, -0.2]]); break;
case "arch": body = pts(range(25).map((i) => { const a = P + i / 24 * P; return [0.85 * Math.cos(a), -0.15 + 0.85 * Math.sin(a)]; }).concat([[0.85, 1], [-0.85, 1]])); break;
case "shield": body = pts([[-0.85, -0.9], [0.85, -0.9], [0.85, 0.05], [0.5, 0.65], [0, 1], [-0.5, 0.65], [-0.85, 0.05]]); break;
case "pebble": body = `< ellipse rx = "1" ry = "0.78" transform = "rotate(-14.3)" / > `; break;
case "cloud": body = circ(-0.45, 0.15, 0.55) + circ(0.45, 0.15, 0.55) + circ(0, -0.2, 0.65) + circ(0, 0.35, 0.55); break;
case "heart": body = circ(-0.45, -0.25, 0.55) + circ(0.45, -0.25, 0.55) + pts([[-0.97, -0.1], [0.97, -0.1], [0, 1]]); break;
case "star": body = pts(range(10).map((i) => { const a = -P / 2 + i * P / 5, r = i % 2 ? 0.48 : 1.05; return [r * Math.cos(a), 0.08 + r * Math.sin(a)]; })); break;
default: body = circ(0, 0, 0.95);
}
const rgb = [1, 3, 5].map((i) => parseInt(col.slice(i, i + 2), 16));
const eye = rgb[0] + rgb[1] + rgb[2] > 600 ? "#1c1c1c" : "#fcfcfc";
const eyes = [-0.27, 0.27].map((x) => `< ellipse cx = "${x}" cy = "0.02" rx = "0.11" ry = "0.22" transform = "rotate(-20 ${x} 0.02)" / > `).join("");
return `< svg viewBox = "-1.1 -1.1 2.2 2.2" aria-hidden = "true" > < g fill = "${col}" > ${body}< / g > < g fill = "${eye}" > ${eyes}< / g > < / svg > `;
}
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
function toast(t) { const e = $("toast"); e.textContent = t; e.classList.add("on"); clearTimeout(toast.t); toast.t = setTimeout(() => e.classList.remove("on"), 2600); }
function show(v) { document.querySelectorAll(".view").forEach((x) => x.classList.toggle("on", x.id === "v-" + v)); S.view = v; if (v !== "screen") stopFrames(); }
function base() { return S.relay ? "h/" + encodeURIComponent(S.host) + "/" : ""; }
function esc(s) { return String(s ?? "").replace(/[& < >"']/g, (c) => ({"&":"& ","< ":"< ",">":"> ",'"':"" ","'":"' "}[c])); }
async function api(path, opts = {}) {
const headers = Object.assign({}, opts.headers || {});
if (S.token) headers.Authorization = "Bearer " + S.token;
if (opts.json !== undefined) { headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(opts.json); }
const r = await fetch(path, {method: opts.method || (opts.body ? "POST" : "GET"), headers, body: opts.body});
const signingIn = /(^|\/)(login|signup)$/.test(path);
if (r.status === 401 & & !signingIn) { signOut(true); throw new Error("請重新登入"); }
if (opts.raw) return r;
const t = await r.text();
let v = {}; try { v = t ? JSON.parse(t) : {}; } catch (_) {}
if (!r.ok) throw new Error(v.error || ("HTTP " + r.status));
return v;
}
// ---------- sign in ----------
async function hello() {
try {
const r = await fetch("api/hello"); const v = await r.json();
S.relay = v.app === "anybot-relay";
} catch (_) { S.relay = false; }
$("f-user").style.display = S.relay ? "" : "none";
$("b-mode").style.display = S.relay ? "" : "none";
$("login-desc").textContent = S.relay ? "用你的 AnyBot 帳號登入。在哪裡都能用,電腦不用開任何連接埠。" : "輸入這台 AnyBot 的手機密碼(在 AnyBot 設定 › 手機與帳號 設定)。";
if (!S.token) return show("login");
if (S.relay) return pickHost();
start();
}
let signingUp = false;
$("b-mode").onclick = () => {
signingUp = !signingUp;
$("f-invite").style.display = signingUp ? "" : "none";
$("b-login").textContent = signingUp ? "建立帳號並登入" : "登入";
$("b-mode").textContent = signingUp ? "已經有帳號?登入" : "還沒有帳號?建立一個";
$("login-title").textContent = signingUp ? "建立 AnyBot 帳號" : "登入 AnyBot";
$("f-pass").placeholder = signingUp ? "密碼(至少 8 個字)" : "密碼";
};
$("b-login").onclick = async () => {
const b = $("b-login"); b.disabled = true; $("login-err").textContent = "";
const device = (navigator.userAgent.match(/iPhone|iPad|Android|Mac|Windows/) || ["手機"])[0];
try {
if (S.relay & & signingUp) await api("relay/signup", {json: {user: $("f-user").value.trim(), password: $("f-pass").value, invite: $("f-invite").value.trim()}});
const v = await api(S.relay ? "relay/login" : "api/login", {json: {user: $("f-user").value.trim(), password: $("f-pass").value, device}});
S.token = v.token; localStorage.setItem("anybot.token", S.token); $("f-pass").value = "";
if (S.relay) await pickHost(); else start();
} catch (e) { $("login-err").textContent = e.message; }
b.disabled = false;
};
$("f-pass").addEventListener("keydown", (e) => { if (e.key === "Enter") $("b-login").click(); });
function signOut(expired) {
if (S.token & & !expired) api(S.relay ? "relay/logout" : "api/logout", {json: {}}).catch(() => {});
S.token = ""; localStorage.removeItem("anybot.token"); S.version = 0; show("login");
if (expired) $("login-err").textContent = "登入已失效,請重新登入";
}
$("b-out1").onclick = () => signOut();
2026-11-05 11:07:43 +00:00
// The list waits for a brain that is restarting (or a computer that woke up): it looks again
// every few seconds and goes in by itself; it used to sit on 「離線」 until tapped.
let hostRetry = null;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
async function pickHost() {
2026-11-05 11:07:43 +00:00
clearTimeout(hostRetry);
let me;
try { me = await api("relay/me"); } catch (e) { toast(e.message); if (S.token) hostRetry = setTimeout(pickHost, 3000); return; }
2026-11-04 11:30:32 +00:00
const all = me.hosts || [];
// A computer that shows another one's bots (大腦) leads to that brain.
const byId = Object.fromEntries(all.map((h) => [h.id, h]));
const target = (id) => (byId[id] & & byId[id].uses & & byId[byId[id].uses]) ? byId[id].uses : id;
const hosts = all.filter((h) => !(h.uses & & byId[h.uses]));
const sharers = (id) => all.filter((h) => h.uses === id).map((h) => h.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
const live = hosts.filter((h) => h.online);
2026-11-05 06:40:01 +00:00
// Several brains on: the one this account's computers use is the one the chats are on.
const used = live.filter((h) => sharers(h.id).length);
2026-11-05 07:48:20 +00:00
// First visit picks the brain this account's computers use; a choice the person made (or the
// host stored from last time) is kept as it is.
if (!S.host & & used.length === 1) S.host = used[0].id;
2026-11-04 11:30:32 +00:00
if (S.host) {
const t = target(S.host);
if (byId[t] & & byId[t].online) { S.host = t; localStorage.setItem("anybot.host", t); return start(); }
}
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 (live.length === 1) { S.host = live[0].id; localStorage.setItem("anybot.host", S.host); return start(); }
2026-11-04 11:30:32 +00:00
const note = (h) => { const n = sharers(h.id); return n.length ? " · " + esc(n.join("、")) + " 也用這個大腦" : ""; };
2026-11-05 11:07:43 +00:00
const wanted = (S.host & & byId[target(S.host)]) || (all.find((h) => sharers(h.id).length)) || hosts[0];
const waiting = hosts.length & & !live.length & & wanted
? `< div class = "empty" > 「${esc(wanted.name)}」暫時沒有連線(可能正在重新啟動,或那台電腦睡著了)。< br > 連上後會自動進入,不用重新整理。< / div > ` : "";
$("hosts").innerHTML = waiting + (hosts.length ? hosts.map((h) => `< div class = "row" data-h = "${esc(h.id)}" > < div class = "av" style = "background:${h.online ? " var ( --ok ) " : " # 888 " } " > ${esc((h.name || "?")[0])}< / div > < div class = "mid" > < div class = "name" > ${esc(h.name)}< / div > < div class = "last" > ${h.online ? "在線上" : "離線(" + esc(h.last_seen_text || "很久沒連線") + ") "}${note(h)}< / div > < / div > < / div > `).join("")
: `< div class = "empty" > 這個帳號還沒有連上任何 AnyBot。< br > 在電腦上打開 AnyBot › 設定 › 手機與帳號,登入同一個帳號。< / div > `);
document.querySelectorAll("#hosts .row").forEach((r) => r.onclick = () => { clearTimeout(hostRetry); S.host = r.dataset.h; localStorage.setItem("anybot.host", S.host); start(); });
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
show("hosts");
2026-11-05 11:07:43 +00:00
if (S.token & & !live.length) hostRetry = setTimeout(pickHost, 3000);
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
}
// ---------- live state ----------
async function start() {
if (S.open) openChat(S.open); else show("list");
if (S.polling) return;
S.polling = true;
while (S.token) {
try {
const v = await api(base() + "api/state?since=" + S.version);
if (v.version !== S.version) { S.version = v.version; apply(v); }
} catch (e) {
if (!S.token) break;
if (S.relay & & /離線|offline|not connected/i.test(e.message)) toast("那台 AnyBot 目前沒有連線");
await new Promise((r) => setTimeout(r, 2500));
}
}
S.polling = false;
}
function apply(v) {
S.bots = v.bots || []; S.approvals = v.approvals || []; S.hostName = v.host || ""; S.mac = !!v.mac; S.box = v.box;
// 團隊: a member only chats; the owner and admins approve and use the screens.
S.role = v.role || "owner";
const member = S.role === "member";
["b-screen", "b-screen2", "b-new"].forEach((id) => $(id).style.display = member ? "none" : "");
$("host-name").textContent = S.hostName ? "· " + S.hostName : "";
renderList();
if (S.open) {
const b = S.bots.find((x) => x.id === S.open);
2026-11-05 11:20:29 +00:00
if (b) { $("chat-name").textContent = b.name; $("chat-av").innerHTML = avatarSvg(b.shape, b.color); $("chat-av").classList.add("face"); }
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
loadChat(); renderAsk();
}
}
function botName(id) { const b = S.bots.find((x) => x.id === id); return b ? b.name : ""; }
function renderList() {
const el = $("bots");
2026-11-05 06:25:01 +00:00
if (!S.bots.length) { el.innerHTML = `< div class = "empty" > 還沒有 Bot。打開電腦上的 AnyBot, 照著畫面建立第一個 Bot, 這裡就會出現。< / div > `; 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
el.innerHTML = S.bots.map((b) => {
const waiting = S.approvals.some((a) => a.bot === b.id);
const state = waiting ? `< span class = "wait" > 等你決定< / span > ` : b.busy ? `< span class = "busy" > ${esc(b.status || "工作中…")}< / span > ` : "";
2026-11-05 11:20:29 +00:00
return `< div class = "row" data-id = "${b.id}" > < div class = "av face" > ${avatarSvg(b.shape, b.color)}< / div > < div class = "mid" > < div class = "name" > ${esc(b.name)}${b.members ? '< span class = "pill" > 群組< / span > ' : ""}${b.unread ? '< span class = "badge" > < / span > ' : ""}< / div > < div class = "last" > ${state || esc(b.last || b.description || "")}< / div > < / div > < / div > `;
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
}).join("");
el.querySelectorAll(".row").forEach((r) => r.onclick = () => openChat(+r.dataset.id));
}
// ---------- chat ----------
function openChat(id) {
S.open = id; S.msgs = []; S.pending = []; renderChips();
const b = S.bots.find((x) => x.id === id) || {};
2026-11-05 11:20:29 +00:00
$("chat-name").textContent = b.name || ""; $("chat-av").innerHTML = avatarSvg(b.shape, b.color); $("chat-av").classList.add("face");
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
$("msgs").innerHTML = ""; show("chat"); loadChat(true); renderAsk();
api(base() + `api/bots/${id}/read`, {json: {}}).catch(() => {});
}
$("b-back").onclick = () => { S.open = 0; show("list"); };
let loading = false, again = false;
async function loadChat(jump) {
if (loading) { again = true; return; }
loading = true;
try {
const v = await api(base() + "api/bots/" + S.open);
const sc = $("scroller"); const near = sc.scrollHeight - sc.scrollTop - sc.clientHeight < 120 ;
S.msgs = v.messages || []; renderMsgs();
if (jump || near) sc.scrollTop = sc.scrollHeight;
} catch (e) { toast(e.message); }
loading = false;
if (again) { again = false; loadChat(); }
}
function md(text) {
const blocks = String(text || "").split(/```/);
return blocks.map((part, i) => {
if (i % 2) { const nl = part.indexOf("\n"); return `< pre > < code > ${esc(nl >= 0 ? part.slice(nl + 1) : part)}< / code > < / pre > `; }
let h = esc(part)
.replace(/`([^`\n]+)`/g, "< code > $1< / code > ")
.replace(/\*\*([^*\n]+)\*\*/g, "< b > $1< / b > ")
頁面: 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
.replace(/\[([^\]]+)\]\(page:(\d+)\)/g, (_, t, id) => `${GLYPH(14)}< a class = "plink" data-page = "${id}" > ${t}< / a > `)
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
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '< a href = "$2" target = "_blank" rel = "noopener" > $1< / a > ')
.replace(/(^|[\s(])(https?:\/\/[^\s< )]+)/g, '$1< a href = "$2" target = "_blank" rel = "noopener" > $2< / a > ');
const out = []; let list = null;
for (const line of h.split("\n")) {
const li = line.match(/^\s*(?:[-*•]|\d+[.)])\s+(.*)$/);
if (li) { if (!list) { list = []; } list.push(`< li > ${li[1]}< / li > `); continue; }
if (list) { out.push(`< ul > ${list.join("")}< / ul > `); list = null; }
const hd = line.match(/^(#{1,3})\s+(.*)$/);
if (hd) out.push(`< h3 > ${hd[2]}< / h3 > `);
else if (/^> \s?/.test(line)) out.push(`< blockquote > ${line.replace(/^> \s?/, "")}< / blockquote > `);
else out.push(line ? `< p > ${line}< / p > ` : "");
}
if (list) out.push(`< ul > ${list.join("")}< / ul > `);
return out.join("");
}).join("");
}
const ST = {waiting: ic("circle-alert", 14), running: ic("loader", 14), done: ic("circle-check", 14), denied: ic("minus", 14), failed: ic("circle-x", 14)};
function renderMsgs() {
const busy = (S.bots.find((b) => b.id === S.open) || {}).busy;
const html = S.msgs.map((m, i) => {
if (m.role === "user") {
const atts = (m.attachments || []).map((a) => `< div class = "att" > ${ic(a.image ? "image" : "file", 14)} ${esc(a.name)}< / div > `).join("");
if (m.summary) return "";
return `< div class = "m user" > ${m.wake ? `< div class = "via" > ${ic("alarm-clock", 13)} 例行任務< / div > ` : ""}${m.text ? `< div class = "bubble" > ${esc(m.text)}< / div > ` : ""}${atts}< / div > `;
}
if (m.summary) return `< div class = "m bot" > < div class = "via" > (較早的對話已整理成摘要)< / div > < / div > `;
頁面: 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
const tools = (m.tools || []).map((t) => t.page ? pageCard(t.page) : t.card ? card(t.card) : `< div class = "tool ${t.status}" > < span class = "st" > ${ST[t.status] || "·"}< / span > < span > ${esc(t.label)}< / span > < span class = "d" > ${esc(t.detail)}< / span > < / div > `).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
const last = i === S.msgs.length - 1;
const body = m.text ? `< div class = "bubble" > ${md(m.text)}< / div > ` : (last & & busy & & !tools ? `< div class = "typing" > < i > < / i > < i > < / i > < i > < / i > < / div > ` : "");
const who = m.author ? `< div class = "who" > ${esc(m.author)}< / div > ` : "";
const via = m.text & & m.via & & !busy ? `< div class = "via" > ${esc(m.via)}${(m.reactions || []).length ? " · " + m.reactions.join(" ") : ""}< / div > ` : "";
return `< div class = "m bot ${m.error ? " err " : " " } " > ${who}${body}${tools}${last ? "" : ""}${via}< / div > `;
}).join("");
$("msgs").innerHTML = html || `< div class = "empty" > 還沒有訊息,說聲嗨吧< / div > `;
$("b-send").innerHTML = busy & & !$("input").value.trim() & & !S.pending.length
? `< svg viewBox = "0 0 24 24" fill = "currentColor" > < rect x = "7" y = "7" width = "10" height = "10" rx = "2" / > < / svg > `
: `< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.2" > < path d = "M12 19V5M6 11l6-6 6 6" / > < / svg > `;
}
function card(c) {
if (!c) return "";
const rows = (c.rows || []).map((r) => `< div class = "r" > < span > ${esc(r.label)}< / span > < span > ${esc(r.value)}< / span > < / div > `).join("");
return `< div class = "card" > < div class = "t" > ${esc(c.title)}< / div > ${c.subtitle ? `< div class = "s" > ${esc(c.subtitle)}< / div > ` : ""}${rows}${c.link ? `< div class = "r" > < a href = "${esc(c.link)}" target = "_blank" rel = "noopener" > 來源< / a > < span > < / span > < / div > ` : ""}< / div > `;
}
頁面: 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
// ---------- 頁面 ----------
const GLYPH = (h = 34) => `< svg class = "pglyph" width = "${Math.round(h * .8)}" height = "${h}" viewBox = "0 0 24 30" style = "vertical-align:-3px" > < rect x = ".5" y = ".5" width = "23" height = "29" rx = "4" fill = "var(--raise)" stroke = "var(--line)" / > < path d = "M5 7h9M5 11h6" stroke = "var(--text3)" stroke-width = "2" stroke-linecap = "round" / > < rect x = "5" y = "20" width = "2.6" height = "5" rx = ".6" fill = "#34a853" / > < rect x = "9" y = "18" width = "2.6" height = "7" rx = ".6" fill = "#fbbc05" / > < rect x = "13" y = "16" width = "2.6" height = "9" rx = ".6" fill = "#ea4335" / > < rect x = "17" y = "14" width = "2.6" height = "11" rx = ".6" fill = "#4285f4" / > < / svg > `;
function pageCard(p) {
const lines = (p.preview || []).map((l, i, a) => `< div class = "l" style = "opacity:${1 - (i / Math.max(a.length, 1)) * .65}" > ${esc(l)}< / div > `).join("");
const meta = `頁面 · ${esc(p.updated)}${p.revision > 1 ? " · 已更新" : ""}`;
return `< div class = "pcard" data-page = "${p.id}" > < div class = "top" > < div class = "t" > ${esc(p.title)}< / div > ${lines}< / div > ${p.revision > 1 & & p.note ? `< div class = "note" > 最近更新:${esc(p.note)}< / div > ` : ""}< div class = "foot" > ${GLYPH()}< div style = "min-width:0" > < div class = "n" > ${esc(p.title)}< / div > < span class = "m" > ${meta}< / span > < / div > < / div > < / div > `;
}
document.addEventListener("click", (e) => {
const el = e.target.closest & & e.target.closest("[data-page]");
if (el) { e.preventDefault(); openPage(+el.dataset.page); }
});
async function openPage(id, rev) {
if (S.view !== "page") S.pageBack = S.view;
S.page = id; show("page"); $("pscroll").scrollTop = 0;
if (!rev) $("page").innerHTML = `< div class = "empty" > 載入中…< / div > `;
try {
const v = await api(base() + "api/pages/" + id + (rev ? "?rev=" + rev : ""));
$("page-name").textContent = v.title;
const old = v.revision !== v.latest;
const sel = $("pver");
if ((v.versions || []).length) {
sel.innerHTML = `< option value = "" > 第 ${v.latest} 版(最新)< / option > ` + v.versions.map((x) => `< option value = "${x.revision}" > 第 ${x.revision} 版 · ${esc(x.at)}< / option > `).join("");
sel.value = old ? String(v.revision) : ""; sel.style.display = "";
} else sel.style.display = "none";
let head = `< h1 class = "pt" > ${esc(v.title)}< / h1 > < div class = "meta" > ${esc(v.bot)} · 更新於 ${esc(v.updated)}${v.latest > 1 ? ` · 第 ${v.revision} 版` : ""}< / div > `;
if (old) head += `< div class = "old" > 你在看舊的版本。< a class = "plink" data-page = "${id}" > 回到最新版< / a > < / div > `;
else if (v.summary) head += `< p class = "sum" > ${esc(v.summary)}< / p > `;
if (v.note & & v.revision > 1) head += `< div class = "chg" > 這一版更新了:${esc(v.note)}< / div > `;
$("page").innerHTML = head + pageMd(v.body, id, old ? v.revision : 0);
loadCharts();
} catch (e) { $("page").innerHTML = `< div class = "empty" > ${esc(e.message)}< / div > `; }
}
$("pver").onchange = () => openPage(S.page, +$("pver").value || 0);
$("b-back3").onclick = () => { show(S.pageBack & & S.pageBack !== "page" ? S.pageBack : "list"); if (S.view === "chat") loadChat(); };
const dark = () => matchMedia("(prefers-color-scheme: dark)").matches;
async function loadCharts() {
for (const el of document.querySelectorAll(".chart[data-src]")) {
const src = el.dataset.src; el.removeAttribute("data-src");
try {
const r = await api(base() + src + (src.includes("?") ? "& " : "?") + "dark=" + (dark() ? 1 : 0), {raw: true});
if (!r.ok) throw new Error();
el.innerHTML = `< img alt = "圖表" src = "${URL.createObjectURL(await r.blob())}" > `;
} catch (_) { el.innerHTML = `< div class = "wait" > 這張圖畫不出來< / div > `; }
}
}
// Inline Markdown: code, bold, italics, strike, links (page:N opens the page).
function inl(s) {
return esc(s)
.replace(/`([^`]+)`/g, "< code > $1< / code > ")
.replace(/\*\*([^*]+)\*\*/g, "< b > $1< / b > ")
.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1< i > $2< / i > ")
.replace(/~~([^~]+)~~/g, "< s > $1< / s > ")
.replace(/\[([^\]]+)\]\(page:(\d+)\)/g, (_, t, id) => `< a class = "plink" data-page = "${id}" > ${t}< / a > `)
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '< a href = "$2" target = "_blank" rel = "noopener" > $1< / a > ')
.replace(/(^|[\s(( ])(https?:\/\/[^\s< )) ]+)/g, '$1< a href = "$2" target = "_blank" rel = "noopener" > $2< / a > ');
}
const CALL = {note: "說明", tip: "提示", warning: "注意", important: "重要"};
// A page's Markdown: headings, paragraphs, lists and tasks, tables, quotes and call-outs, code,
// Mermaid charts (drawn by the AnyBot as SVG), ```stats tiles and ```steps progress.
function pageMd(text, id, rev) {
const L = String(text || "").replace(/\r/g, "").split("\n");
const out = []; let i = 0, chart = 0;
const cells = (l) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
while (i < L.length ) {
const l = L[i], t = l.trim();
if (!t) { i++; continue; }
const fence = t.match(/^```\s*([\w-]*)/);
if (fence) {
const lang = fence[1].toLowerCase(); const body = []; i++;
while (i < L.length & & ! L [ i ] . trim ( ) . startsWith ( " ` ` ` " ) ) body . push ( L [ i + + ] ) ;
i++;
if (lang === "mermaid") { out.push(`< div class = "chart" data-src = "api/pages/${id}/chart/${chart++}${rev ? " ? rev = " + rev : " " } " > < div class = "wait" > 圖表載入中…< / div > < / div > `); continue; }
if (lang === "stats") {
2026-11-05 00:33:23 +00:00
const tiles = body.map(cells).filter((c) => c.length >= 2 & & c[0]).map((c) => `< div class = "stat" > < div class = "k" > ${inl(c[0])}< / div > < div class = "v${c[1].length > 10 ? " long " : " " } " > ${inl(c[1])}< / div > ${c[2] ? `< div class = "s" > ${inl(c[2])}< / div > ` : ""}< / div > `);
頁面: 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
if (tiles.length) { out.push(`< div class = "stats" > ${tiles.join("")}< / div > `); continue; }
}
if (lang === "steps") {
const items = body.map((b) => b.trim().replace(/^[-*]\s*/, "").match(/^\[(x|X|✓|>|~|-| )\]\s*(.*)$/)).filter(Boolean).map((m) => {
const st = /[xX✓]/.test(m[1]) ? "done" : /[>~-]/.test(m[1]) ? "now" : "todo";
const [nm, nt] = m[2].split(/\s*\|\s*/, 2);
return `< li class = "${st}" > < span class = "dot" > ${st === "done" ? "✓" : ""}< / span > < div class = "nm" > ${inl(nm)}< / div > ${nt ? `< div class = "nt" > ${inl(nt)}< / div > ` : ""}< / li > `;
});
if (items.length) { out.push(`< ul class = "steps" > ${items.join("")}< / ul > `); continue; }
}
out.push(`< pre > < code > ${esc(body.join("\n"))}< / code > < / pre > `); continue;
}
const h = t.match(/^(#{1,6})\s+(.*)$/);
if (h) { const n = Math.min(h[1].length + 1, 4); out.push(`< h $ { n } > ${inl(h[2])}< /h${n}>`); i++; continue; }
if (/^(-{3,}|\*{3,}|_{3,})$/.test(t)) { out.push("< hr > "); i++; continue; }
if (t.startsWith("|") & & i + 1 < L.length & & / ^ \ | ? \ s * : ? - { 2 , } / . test ( L [ i + 1 ] . trim ( ) ) ) {
const head = cells(t); i += 2; const rows = [];
while (i < L.length & & L [ i ] . trim ( ) . startsWith ( " | " ) ) rows . push ( cells ( L [ i + + ] ) ) ;
out.push(`< div class = "tw" > < table > < thead > < tr > ${head.map((c) => `< th > ${inl(c)}< / th > `).join("")}< / tr > < / thead > < tbody > ${rows.map((r) => `< tr > ${r.map((c) => `< td > ${inl(c)}< / td > `).join("")}< / tr > `).join("")}< / tbody > < / table > < / div > `);
continue;
}
if (t.startsWith(">")) {
const q = []; while (i < L.length & & L [ i ] . trim ( ) . startsWith ( " > ")) q.push(L[i++].trim().replace(/^>\s?/, ""));
const c = (q[0] || "").match(/^\[!(NOTE|INFO|TIP|WARNING|CAUTION|IMPORTANT)\]\s*(.*)$/i);
if (c) {
const k = {info: "note", caution: "warning"}[c[1].toLowerCase()] || c[1].toLowerCase();
q[0] = c[2];
out.push(`< div class = "call ${k}" > < div class = "ct" > ${CALL[k]}< / div > ${pageMd(q.join("\n"), id, rev)}< / div > `);
} else out.push(`< blockquote > ${pageMd(q.join("\n"), id, rev)}< / blockquote > `);
continue;
}
if (/^([-*+•]|\d+[.)])\s+/.test(t)) {
const ordered = /^\d/.test(t); const items = [];
while (i < L.length & & / ^ \ s * ( [ - * + • ] | \ d + [ . ) ] ) \ s + / . test ( L [ i ] ) ) {
let it = L[i++].trim().replace(/^([-*+•]|\d+[.)])\s+/, "");
const task = it.match(/^\[( |x|X)\]\s+(.*)$/);
items.push(task ? `< li class = "task" > ${task[1] === " " ? "☐" : "☑"} ${inl(task[2])}< / li > ` : `< li > ${inl(it)}< / li > `);
}
out.push(ordered ? `< ol > ${items.join("")}< / ol > ` : `< ul > ${items.join("")}< / ul > `); continue;
}
const para = [];
while (i < L.length & & L [ i ] . trim ( ) & & ! / ^ ( # { 1 , 6 } \ s | ` ` ` | > |\||([-*+•]|\d+[.)])\s)/.test(L[i].trim())) para.push(L[i++].trim());
if (!para.length) { para.push(t); i++; }
out.push(`< p > ${para.map(inl).join("< br > ")}< / p > `);
}
return out.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
function renderAsk() {
const a = S.approvals.find((x) => x.bot === S.open);
if (!a) { $("ask").innerHTML = ""; return; }
if (S.role === "member") { $("ask").innerHTML = `< div class = "ask" > < div class = "q" > ${esc(a.title)}< / div > < div class = "why" > 等擁有者或管理員決定< / div > < / div > `; return; }
2026-11-05 09:29:55 +00:00
// 需要處理 (a step only you can do on a computer): its screen is one tap away, then 我完成了 / 跳過.
if (a.kind === "handoff") {
$("ask").innerHTML = `< div class = "ask" > < div class = "q" > 需要處理 · ${esc(a.computer || "")}< / div > < div class = "why" > ${esc(a.reason)}< / div > < div class = "btns" > < button class = "btn" data-screen = "1" > 接管< / button > < button class = "btn main" data-a = "once" > 我完成了< / button > < button class = "btn no" data-a = "reject" title = "取消此要求且不執行該步驟; Bot 會跳過它並繼續" > 跳過< / button > < / div > < / div > `;
$("ask").querySelector("[data-screen]").onclick = () => { S.back = S.view; show("screen"); $("seg").children[0].textContent = ownComputer(); $("seg").children[1].style.display = S.mac ? "" : "none"; startFrames(); };
} 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
$("ask").innerHTML = `< div class = "ask" > < div class = "q" > ${esc(a.title)}< / div > < div class = "why" > ${esc(a.reason)}< / div > < div class = "btns" > < button class = "btn main" data-a = "once" > 允許一次< / button > ${a.can_always ? `< button class = "btn" data-a = "always" > 一律允許< / button > ` : ""}< button class = "btn no" data-a = "reject" > 拒絕< / button > < / div > < / div > `;
2026-11-05 09:29:55 +00:00
}
$("ask").querySelectorAll("button[data-a]").forEach((b) => b.onclick = async () => {
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
$("ask").innerHTML = "";
try { await api(base() + `api/bots/${S.open}/answer`, {json: {answer: b.dataset.a}}); } catch (e) { toast(e.message); }
});
}
const input = $("input");
input.addEventListener("input", () => { input.style.height = "auto"; input.style.height = Math.min(input.scrollHeight, 140) + "px"; renderMsgs(); });
input.addEventListener("keydown", (e) => { if (e.key === "Enter" & & !e.shiftKey & & !e.isComposing & & matchMedia("(pointer:fine)").matches) { e.preventDefault(); send(); } });
$("b-send").onclick = send;
async function send() {
const text = input.value.trim();
const busy = (S.bots.find((b) => b.id === S.open) || {}).busy;
if (!text & & !S.pending.length) {
if (busy & & S.role !== "member") { try { await api(base() + `api/bots/${S.open}/stop`, {json: {}}); toast("已停止"); } catch (e) { toast(e.message); } }
return;
}
const attachments = S.pending.map((p) => p.path);
input.value = ""; input.style.height = "auto"; S.pending = []; renderChips();
S.msgs.push({role: "user", text, attachments: []}); renderMsgs(); $("scroller").scrollTop = 1e9;
try { await api(base() + `api/bots/${S.open}/send`, {json: {text, attachments}}); } catch (e) { toast(e.message); input.value = text; }
}
$("b-new").onclick = async () => { if (!confirm("開始新對話?目前的對話會留在討論串裡。")) return; try { await api(base() + `api/bots/${S.open}/new`, {json: {}}); } catch (e) { toast(e.message); } };
$("b-clip").onclick = () => $("file").click();
$("file").onchange = async (e) => {
for (const f of e.target.files) {
toast("上傳 " + f.name + "…");
try {
const r = await api(base() + `api/bots/${S.open}/upload?name=` + encodeURIComponent(f.name), {method: "POST", body: f, headers: {"Content-Type": "application/octet-stream"}});
S.pending.push(r); renderChips();
} catch (err) { toast(err.message); }
}
e.target.value = "";
};
function renderChips() {
$("chips").innerHTML = S.pending.map((p, i) => `< span class = "chip" > ${ic(p.image ? "image" : "file", 14)} ${esc(p.name)} < button data-i = "${i}" > ${ic("x", 12)}< / button > < / span > `).join("");
$("chips").querySelectorAll("button").forEach((b) => b.onclick = () => { S.pending.splice(+b.dataset.i, 1); renderChips(); });
}
// ---------- screens ----------
2026-11-05 08:47:28 +00:00
// Each bot's own computer goes by the bot's name (「Master 的電腦」).
function ownComputer() { const n = botName(S.open); return n ? n + " 的電腦" : "Bot 的電腦"; }
$("b-screen").onclick = $("b-screen2").onclick = () => { S.back = S.view; show("screen"); $("seg").children[0].textContent = ownComputer(); $("seg").children[1].style.display = S.mac ? "" : "none"; startFrames(); };
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
$("b-back2").onclick = () => show(S.back === "chat" ? "chat" : "list");
document.querySelectorAll("#seg button").forEach((b) => b.onclick = () => { document.querySelectorAll("#seg button").forEach((x) => x.classList.toggle("on", x === b)); S.screen = b.dataset.w; $("k-wake").style.display = S.screen === "box" ? "" : "none"; startFrames(); });
2026-11-05 14:34:36 +00:00
// The bots' computer sends a picture the moment its screen changes: ask for the one after the
// picture shown (X-Seq), again as soon as it arrives. (It was one picture a second, so a tap
// showed its result a second or two later.) This computer's own screen: a picture at a time.
let lastUrl = null;
const nap = (ms) => new Promise((r) => setTimeout(r, ms));
function startFrames() { stopFrames(); const run = S.frameRun = {}; frames(run); }
function stopFrames() { S.frameRun = null; }
async function frames(run) {
let seq = 0;
while (S.frameRun === run) {
if (document.hidden) { await nap(800); continue; }
try {
const live = S.screen === "box";
const r = await api(base() + "api/screen/" + S.screen + (live ? "?after=" + seq : ""), {raw: true});
if (S.frameRun !== run) return;
if (!r.ok) { const v = await r.json().catch(() => ({})); $("stage").innerHTML = `< div class = "msg" > ${esc(v.error || "看不到畫面")}< / div > `; $("screen-live").textContent = ""; await nap(1500); continue; }
const n = +(r.headers.get("X-Seq") || 0);
if (live & & n & & n === seq) continue;
seq = n;
S.size = (r.headers.get("X-Size") || "0x0").split("x").map(Number);
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
const url = URL.createObjectURL(await r.blob());
let img = $("stage").querySelector("img");
if (!img) { $("stage").innerHTML = ""; img = document.createElement("img"); img.draggable = false; $("stage").appendChild(img); bindStage(img); }
img.src = url; if (lastUrl) URL.revokeObjectURL(lastUrl); lastUrl = url;
$("screen-live").innerHTML = `< span class = "live" > < / span > 即時`;
2026-11-05 14:34:36 +00:00
// At most about 8 pictures a second (an animated page would send all it can).
await nap(!live || !n ? 700 : 120);
} catch (e) { $("screen-live").textContent = ""; await nap(1500); }
}
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
}
function point(img, e) {
const r = img.getBoundingClientRect();
const w = S.size[0] || img.naturalWidth, h = S.size[1] || img.naturalHeight;
return {x: Math.round((e.clientX - r.left) / r.width * w), y: Math.round((e.clientY - r.top) / r.height * h), cx: e.clientX - $("stage").getBoundingClientRect().left, cy: e.clientY - $("stage").getBoundingClientRect().top};
}
function bindStage(img) {
let pressTimer = null, long = false;
img.addEventListener("pointerdown", (e) => { long = false; pressTimer = setTimeout(() => { long = true; act(img, e, "right"); }, 550); });
img.addEventListener("pointerup", (e) => { clearTimeout(pressTimer); if (!long) act(img, e, S.right ? "right" : "left"); });
img.addEventListener("pointercancel", () => clearTimeout(pressTimer));
img.addEventListener("dblclick", (e) => { const p = point(img, e); input2({kind: "click", x: p.x, y: p.y, count: 2}); });
}
function act(img, e, button) {
const p = point(img, e);
const d = document.createElement("div"); d.className = "dot"; d.style.left = p.cx + "px"; d.style.top = p.cy + "px"; $("stage").appendChild(d); setTimeout(() => d.remove(), 600);
input2({kind: "click", x: p.x, y: p.y, button});
}
async function input2(body) {
2026-11-05 14:34:36 +00:00
try { await api(base() + "api/screen/" + S.screen, {json: body}); } catch (e) { toast(e.message); }
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
}
document.querySelectorAll("[data-key]").forEach((b) => b.onclick = () => input2({kind: "key", key: b.dataset.key}));
document.querySelectorAll("[data-scroll]").forEach((b) => b.onclick = () => input2({kind: "scroll", x: Math.round(S.size[0] / 2), y: Math.round(S.size[1] / 2), direction: b.dataset.scroll, amount: 5}));
$("k-type").onclick = () => { $("typebar").classList.toggle("on"); if ($("typebar").classList.contains("on")) $("typein").focus(); };
$("b-type").onclick = () => { const t = $("typein").value; if (!t) return; $("typein").value = ""; input2({kind: "type", text: t}); };
$("typein").addEventListener("keydown", (e) => { if (e.key === "Enter" & & !e.isComposing) { e.preventDefault(); $("b-type").click(); } });
$("k-right").onclick = () => { S.right = !S.right; $("k-right").textContent = "右鍵:" + (S.right ? "開" : "關"); };
2026-11-05 08:47:28 +00:00
$("k-wake").onclick = () => { toast("正在叫醒" + ownComputer() + "…"); input2({kind: "wake"}); };
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
// ---------- menu, notifications ----------
2026-11-04 12:56:06 +00:00
$("b-more").onclick = () => { $("m-host").style.display = S.relay ? "" : "none"; $("m-pair").style.display = S.relay ? "" : "none"; notifyLabel(); $("sheet").classList.add("on"); };
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
$("sheet").onclick = (e) => { if (e.target.id === "sheet") $("sheet").classList.remove("on"); };
$("m-cancel").onclick = () => $("sheet").classList.remove("on");
$("m-out").onclick = () => { $("sheet").classList.remove("on"); if (confirm("要登出這支手機嗎?")) signOut(); };
$("m-host").onclick = () => { $("sheet").classList.remove("on"); S.host = ""; localStorage.removeItem("anybot.host"); S.version = 0; pickHost(); };
2026-11-04 12:56:06 +00:00
// 加一台電腦: a code for a new computer (ten minutes, once), as hands for this AnyBot.
$("m-pair").onclick = async () => {
$("sheet").classList.remove("on");
$("pair-for").textContent = $("host-name").textContent || "這台 AnyBot";
$("pair-code").textContent = "······"; $("pair-line").textContent = "正在產生…"; $("pair-left").textContent = "";
$("pair").classList.add("on");
try {
const v = await api("relay/pair", {json: {for: S.host}});
One-step install: install.sh, fixed Linux and server builds, clearer README
- install.sh (curl | sh): app, hands, server and deps modes; installs system packages
(right names per release, e.g. libasound2t64 on Ubuntu 24.04), Rust via rustup (1.95+),
Docker for the server; limits parallel compiles on small machines
- Server Docker image: add the ALSA build/runtime packages (the build failed), cache the
cargo registry and target between rebuilds, JOBS build arg
- 加一台電腦 now gives a line that installs AnyBot on a blank computer before joining it
- Makefile: make deps installs Rust too, make doctor, clear message when Rust is missing/old
- README rewritten: one line per use, getting started, update/remove, troubleshooting,
deployment options; docs/install.md and multi-device.md follow it
- Tested on fresh ubuntu:24.04 (app) and debian:12 (curl | sh hands) containers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 16:52:10 +00:00
const line = `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands ${location.origin} ${v.code}`;
2026-11-04 12:56:06 +00:00
$("pair-code").textContent = v.code.slice(0, 3) + " " + v.code.slice(3);
$("pair-line").textContent = line;
const end = Date.now() + (v.expires_in || 600) * 1000;
clearInterval(S.pairTimer);
S.pairTimer = setInterval(() => {
const left = Math.max(0, Math.round((end - Date.now()) / 1000));
$("pair-left").textContent = left ? `還有 ${Math.floor(left / 60)} 分 ${String(left % 60).padStart(2, "0")} 秒,只能用一次` : "過期了,關掉再按一次「加一台電腦」";
if (!left) clearInterval(S.pairTimer);
}, 1000);
} catch (e) { $("pair-line").textContent = "沒有拿到配對碼:" + e.message; }
};
$("pair-copy").onclick = async () => {
const line = $("pair-line").textContent;
One-step install: install.sh, fixed Linux and server builds, clearer README
- install.sh (curl | sh): app, hands, server and deps modes; installs system packages
(right names per release, e.g. libasound2t64 on Ubuntu 24.04), Rust via rustup (1.95+),
Docker for the server; limits parallel compiles on small machines
- Server Docker image: add the ALSA build/runtime packages (the build failed), cache the
cargo registry and target between rebuilds, JOBS build arg
- 加一台電腦 now gives a line that installs AnyBot on a blank computer before joining it
- Makefile: make deps installs Rust too, make doctor, clear message when Rust is missing/old
- README rewritten: one line per use, getting started, update/remove, troubleshooting,
deployment options; docs/install.md and multi-device.md follow it
- Tested on fresh ubuntu:24.04 (app) and debian:12 (curl | sh hands) containers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 16:52:10 +00:00
if (!line.startsWith("curl")) return;
2026-11-04 12:56:06 +00:00
try { await navigator.clipboard.writeText(line); toast("已複製"); } catch (_) { toast("請長按上面那一行複製"); }
};
$("pair-close").onclick = () => { clearInterval(S.pairTimer); $("pair").classList.remove("on"); };
$("pair").onclick = (e) => { if (e.target.id === "pair") $("pair-close").click(); };
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
const pushOK = () => S.relay & & "serviceWorker" in navigator & & "PushManager" in window & & "Notification" in window;
async function notifyLabel() {
const b = $("m-notify"), n = $("m-note");
if (!S.relay) { b.style.display = "none"; n.textContent = "直接連線時沒有通知;用帳號登入(透過帳號伺服器)就能收到通知。"; return; }
b.style.display = "";
if (!pushOK()) { b.disabled = true; b.textContent = "這個瀏覽器不支援通知"; n.textContent = /iPhone|iPad/.test(navigator.userAgent) ? "iPhone 要先按分享 › 加入主畫面,從主畫面打開 AnyBot 才能開通知。" : ""; return; }
const reg = await navigator.serviceWorker.getRegistration();
const sub = reg & & await reg.pushManager.getSubscription();
b.textContent = sub ? "關閉通知" : "開啟通知";
}
function keyBytes(b64) { const s = atob(b64.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((b64.length + 3) % 4)); return Uint8Array.from(s, (c) => c.charCodeAt(0)); }
$("m-notify").onclick = async () => {
try {
const reg = await navigator.serviceWorker.register("sw.js");
await navigator.serviceWorker.ready;
const old = await reg.pushManager.getSubscription();
if (old) { await api("relay/push/unsubscribe", {json: {endpoint: old.endpoint}}); await old.unsubscribe(); toast("已關閉通知"); }
else {
if (await Notification.requestPermission() !== "granted") { toast("沒有允許通知(可在系統設定裡打開)"); return; }
const {key} = await api("relay/push/key");
const sub = await reg.pushManager.subscribe({userVisibleOnly: true, applicationServerKey: keyBytes(key)});
await api("relay/push/subscribe", {json: sub.toJSON()});
toast("已開啟通知");
}
} catch (e) { toast("通知設定失敗:" + e.message); }
notifyLabel();
};
if (pushOK()) navigator.serviceWorker.register("sw.js").catch(() => {});
// A tapped notification opens that chat.
function openFromHash() {
const h = new URLSearchParams(location.hash.slice(1));
if (h.get("host")) { S.host = h.get("host"); localStorage.setItem("anybot.host", S.host); }
const bot = +h.get("bot");
if (bot) S.open = bot;
if (location.hash) history.replaceState(null, "", location.pathname);
}
openFromHash();
if ("serviceWorker" in navigator) navigator.serviceWorker.addEventListener("message", (e) => {
const d = e.data & & e.data.open; if (!d) return;
if (d.host & & d.host !== S.host & & S.relay) { S.host = d.host; localStorage.setItem("anybot.host", S.host); S.version = 0; }
if (d.bot) openChat(+d.bot);
});
2026-11-05 14:34:36 +00:00
document.addEventListener("visibilitychange", () => { if (!document.hidden & & S.view === "screen") startFrames(); });
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
hello();
< / script >
< / body >
< / html >