Skip to content

Commit 1d9e9f9

Browse files
ZD Studiosclaude
andcommitted
feat: agents restyle the dashboard via API, OpenUI panels in the hub, colourful CLI
DASHBOARD CUSTOMIZATION API — agents can now change how the Control Room looks without touching a line of code. All styling lives in one server-side JSON doc (.aios/dashboard.json) the page applies on top of the active theme, so a bad instruction can't break the UI and one reset undoes everything. GET/POST /api/dashboard — ops: set (vars/css/scale/density) · nav (hidden/order) · panel (add/del/clear) · reset (all|vars|css|panels|nav) OPENUI IN THE HUB CHROME: panels are self-contained agent HTML rendered in the same sandboxed iframes as chat widgets, placed in a top / chat / sidebar slot — so generative UI is no longer confined to chat and the Canvas. Reusing a panel id replaces it, which is how an agent live-updates a panel. Input is refused rather than applied when hostile: CSS-escape attempts in variable values ({ } < > ;), malformed keys, and CSS containing @import, </style> or a remote url(http...) (which could beacon out) are rejected and reported in "rejected". Every change is audited. Settings gains an "Agent-applied appearance" card showing what's active with per-category reset buttons. The new "dashboard-designer" skill teaches every agent the API, and the Brain's system prompt now explicitly tells it to use the API and NOT edit dashboard.html. Two bugs found and fixed while verifying: - Overrides silently did nothing: applyTheme() sets the same CSS variables as INLINE styles on :root, which beat an injected stylesheet. Overrides now use setProperty the same way, applied after the theme (and re-applied on theme switch, so agent tweaks survive changing themes). - reset restored the values it was meant to clear: dashboard_cfg() returned {**DASH_DEFAULT}, a shallow copy sharing the nested vars/nav dicts, so writing a variable mutated the module-level defaults. Now deepcopied. CLI: "aios" help is grouped and colourised instead of argparse's flat 25-command dump, and an unknown command gives "Did you mean doctor?" (difflib) rather than a wall of choices — "aios docter" now suggests "doctor". Tagline corrected to eight projects across CLI + README. Verified in-browser: agent restyle applies live (#caa63f -> #4f9dff, 16 -> 18.4px), survives a theme switch, panel renders in a sandboxed iframe, nav hiding works, reset returns every value to the theme default, Settings reports state accurately, zero console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f807be6 commit 1d9e9f9

5 files changed

Lines changed: 412 additions & 10 deletions

File tree

README.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# 🧠 The AI OS
44

5-
### Five open-source AI agents. One operating system. One Control Room.
5+
### Eight open-source AI projects. One operating system. One Control Room.
66

77
<p align="center">
88
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-8B5CF6?style=for-the-badge" alt="MIT License"></a>
@@ -20,6 +20,9 @@
2020
<img src="https://img.shields.io/badge/LifeOS-shared_skills-8B5CF6?style=flat-square" alt="LifeOS">
2121
<img src="https://img.shields.io/badge/+_openclaw--os-dashboard-64748B?style=flat-square" alt="openclaw-os">
2222
<img src="https://img.shields.io/badge/+_OpenUI-generative_UI-10B981?style=flat-square" alt="OpenUI">
23+
<img src="https://img.shields.io/badge/+_Fabric-255_patterns-0EA5E9?style=flat-square" alt="Fabric">
24+
<img src="https://img.shields.io/badge/+_Caveman-say_less-A16207?style=flat-square" alt="Caveman">
25+
<img src="https://img.shields.io/badge/+_Ponytail-build_less-DB2777?style=flat-square" alt="Ponytail">
2326
</p>
2427

2528
**[Install](#-install-one-line)** · **[Control Room](#-the-control-room)** · **[Architecture](#️-architecture)** · **[Commands](#️-command-reference)** · **[Full docs](README.aios.md)** · **[Website](https://zdstudios.github.io/AIOS/)**
@@ -28,7 +31,7 @@
2831

2932
---
3033

31-
**The AI OS** takes five independent open-source AI agents and makes them work as **one auto-configured
34+
**The AI OS** takes eight independent open-source AI projects and makes them work as **one auto-configured
3235
system** you drive from a single command — **`aios`** — and a single web **Control Room** where you can
3336
**talk to everything and have the agents talk to each other**.
3437

@@ -38,7 +41,7 @@ together, mounts your shared skills, and adds **OpenUI** generative-UI context t
3841

3942
> You can't fuse three runtimes and multiple package managers into a single file — that would just break
4043
> everything. So "one file" here means **one control surface** (`aios`) + **one dashboard** (the Hub) over
41-
> five real, unmodified projects. It installs, configures, wires, runs, tests, and debugs all of them.
44+
> eight real, unmodified projects. It installs, configures, wires, runs, tests, and debugs all of them.
4245
4346
<div align="center">
4447

@@ -92,7 +95,7 @@ aios url # open the Control Room
9295
- **Render generative UI (OpenUI) in the hub** — any agent can emit a ```ui``` block (self-contained HTML) that the hub renders **live and interactive** in chat (sandboxed, theme-aware). openclaw-os also renders OpenUI Lang natively.
9396
- **Shared Canvas any agent can edit** — agents `POST /api/ui` (CrewAI has a `build_dashboard_ui` tool) to pin widgets to a **Canvas** everyone sees. This is how "all agents edit the dashboard."
9497

95-
## 🧩 The five agents
98+
## 🧩 The agents
9699

97100
| Agent | Role in The AI OS | Port | Upstream |
98101
|---|---|---|---|
@@ -237,12 +240,35 @@ That is deliberately the "ambient authority" posture that made **OpenClaw's CVE-
237240

238241
**Active Memory:** a memory sub-agent runs on **every** turn — recall is a free FTS5 query, fact-extraction is one small async call — so the agents actually learn your workflow over time instead of only reading memory at session start (`memory.active`).
239242

240-
## 🧵 Fabric patterns & 🗿 Caveman mode
243+
## 🎨 Agents can redesign the dashboard (safely)
241244

242-
Two more open-source projects, wired in at the prompt level (no extra service to run):
245+
Ask in chat — *"make the accent blue and the text bigger"*, *"hide Automations"*, *"put a
246+
deploy-status panel on the dashboard"* — and the agents do it **through the hub API, never by
247+
editing `dashboard.html`**. That's the point: styling lives in one server-side JSON document
248+
the page applies on top of your theme, so an agent can't break the UI by writing bad code, and
249+
**one Reset undoes everything** (Settings → *Agent-applied appearance*).
250+
251+
| What they can change | How |
252+
|---|---|
253+
| Colours | any theme CSS variable — `--accent`, `--bg`, `--text`, `--border`|
254+
| Text size / density | `scale` 0.7–1.6, `density` compact \| normal \| comfortable |
255+
| Custom CSS | appended last, for anything variables can't express |
256+
| Sidebar | hide or reorder any nav item |
257+
| **OpenUI panels** | self-contained agent HTML in sandboxed iframes, in the `top`, `chat` or `sidebar` slot |
258+
259+
Changes appear within ~4s, survive a theme switch, and are audited. Hostile or careless input is
260+
refused, not applied: variable values containing `{ } < > ;`, malformed keys, and CSS with
261+
`@import`, `</style>` or a remote `url(http…)` (which could beacon out) are rejected and reported
262+
back. The agents learn this from the bundled **`dashboard-designer`** skill, and the endpoint is
263+
`GET`/`POST /api/dashboard` (ops: `set` · `nav` · `panel` · `reset`).
264+
265+
## 🧵 Fabric patterns · 🗿 Caveman · 🎀 Ponytail
266+
267+
Three more open-source projects, wired in at the prompt level (no extra service to run):
243268

244269
- **[Fabric](https://github.com/danielmiessler/fabric) — 255 patterns.** Fabric's "patterns" are curated system-prompts (`summarize`, `extract_wisdom`, `analyze_claims`, `write_essay`, `create_quiz`…). AIOS reads them straight from `data/patterns/` and runs them **on your configured model** — including your Claude subscription — so there's no Go binary to install. Use them in the **Patterns** view (pick → paste → run) or inline in chat: `/p summarize <text>`. The `fabric` chat target also works: `fabric` with a message `pattern: your text`.
245270
- **[Caveman](https://github.com/JuliusBrussee/caveman) — conciseness mode.** A system-prompt overlay that makes every agent ~65% terser while keeping code, commands, and error strings exact. Toggle the **🗿 Caveman** button in the composer (cycles off → lite → full → ultra → wenyan), or type `/caveman [level]` / `/caveman off` in chat. It's also mounted as a skill into opencode/hermes/openclaw, so they respect it too. Levels come straight from Caveman's own SKILL.md.
271+
- **[Ponytail](https://github.com/DietrichGebert/ponytail) — build-less mode.** The same idea applied to *code* instead of prose: a "laziness ladder" that forces the simplest thing that works (~54% less code, YAGNI enforced, stdlib before dependencies). Toggle **🎀 Ponytail** in the composer or `/ponytail [lite|full|ultra]`. Caveman trims what an agent **says**; Ponytail trims what it **builds** — they compose, so you can run both.
246272

247273
**On WSL?** `127.0.0.1:8787` often won't reach WSL from your Windows browser (localhost-forwarding is flaky). `aios start`/`aios url` now print your **WSL IP** URL — use that (e.g. `http://172.31.x.x:8787/`). The hub binds `0.0.0.0` so the WSL IP always works.
248274

aios.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import argparse
1515
import json
1616
import os
17+
import re
1718
import shutil
1819
import socket
1920
import subprocess
@@ -2051,8 +2052,77 @@ def _print_urls(cfg):
20512052
# --------------------------------------------------------------------------- #
20522053
# CLI #
20532054
# --------------------------------------------------------------------------- #
2055+
# Commands grouped for the help screen — argparse's flat alphabetical dump is
2056+
# unreadable once you have 25 subcommands.
2057+
HELP_GROUPS = [
2058+
("Get started", ["setup", "start", "stop", "restart", "status", "url"]),
2059+
("Everyday", ["logs", "doctor", "exec", "channels", "profile", "token"]),
2060+
("Agents & updates", ["updates", "update", "claude-login", "attach", "migrate", "wire"]),
2061+
("Advanced", ["test", "debug", "autostart", "install-cli", "bootstrap"]),
2062+
]
2063+
TAGLINE = "Eight open-source AI projects. One operating system."
2064+
2065+
2066+
class AiosParser(argparse.ArgumentParser):
2067+
"""Colourful grouped help, and 'did you mean …?' instead of a wall of choices."""
2068+
2069+
def format_help(self):
2070+
if self.prog != "aios":
2071+
return super().format_help()
2072+
# `add_parser(name, help=…)` stores the text on the subparsers action's
2073+
# pseudo-actions, not on the returned subparser — read it from there.
2074+
choices, helps = {}, {}
2075+
for a in (self._subparsers._group_actions if self._subparsers else []):
2076+
choices.update(getattr(a, "_name_parser_map", {}))
2077+
for ca in getattr(a, "_choices_actions", []):
2078+
helps[ca.dest] = ca.help or ""
2079+
L = []
2080+
L.append("")
2081+
L.append(f" {C.B}{C.CYN}The AI OS{C.R} {C.GRY}·{C.R} {C.GRY}{TAGLINE}{C.R}")
2082+
L.append("")
2083+
L.append(f" {C.GRY}usage:{C.R} {C.B}aios{C.R} {C.GRY}<command> [options]{C.R}")
2084+
L.append("")
2085+
seen = set()
2086+
for title, names in HELP_GROUPS:
2087+
rows = [(n, choices[n]) for n in names if n in choices]
2088+
if not rows:
2089+
continue
2090+
L.append(f" {C.B}{title}{C.R}")
2091+
for n, _sp in rows:
2092+
seen.add(n)
2093+
L.append(f" {C.CYN}{n:<15}{C.R}{C.GRY}{helps.get(n, '')[:64]}{C.R}")
2094+
L.append("")
2095+
extra = [n for n in choices if n not in seen]
2096+
if extra:
2097+
L.append(f" {C.B}Other{C.R}")
2098+
for n in sorted(extra):
2099+
L.append(f" {C.CYN}{n:<15}{C.R}")
2100+
L.append("")
2101+
L.append(f" {C.GRY}aios <command> --help for details · docs: "
2102+
f"https://github.com/ZDStudios/AIOS{C.R}")
2103+
L.append("")
2104+
return "\n".join(L)
2105+
2106+
def error(self, message):
2107+
import difflib
2108+
m = re.search(r"invalid choice: '([^']+)'", message or "")
2109+
if m and self.prog == "aios":
2110+
bad = m.group(1)
2111+
names = []
2112+
for a in (self._subparsers._group_actions if self._subparsers else []):
2113+
names += list(getattr(a, "_name_parser_map", {}))
2114+
near = difflib.get_close_matches(bad, names, n=3, cutoff=0.55)
2115+
say(f"\n {C.RED}{C.R} {C.B}{bad}{C.R} isn't an aios command.")
2116+
if near:
2117+
say(f" {C.GRY}Did you mean{C.R} " +
2118+
f"{C.GRY},{C.R} ".join(f"{C.CYN}{C.B}{n}{C.R}" for n in near) + "?")
2119+
say(f" {C.GRY}Run{C.R} {C.B}aios{C.R} {C.GRY}to see every command.{C.R}\n")
2120+
sys.exit(2)
2121+
super().error(message)
2122+
2123+
20542124
def build_parser():
2055-
p = argparse.ArgumentParser(prog="aios", description="The AI OS — one control surface for five agent projects")
2125+
p = AiosParser(prog="aios", description=f"The AI OS — {TAGLINE}")
20562126
sub = p.add_subparsers(dest="cmd")
20572127

20582128
s = sub.add_parser("setup", help="install toolchains+deps, create .env/config, render + wire")

aios_hub.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
"""
1313
from __future__ import annotations
1414

15+
import copy
1516
import json
1617
import os
18+
import re
1719
import shutil
1820
import socket
1921
import subprocess
@@ -446,6 +448,17 @@ def brain_system_prompt(message: str) -> str:
446448
"OpenUI-style generative UI (https://www.openui.com).")
447449
if tools.full_control():
448450
sysp += TOOL_PROTOCOL
451+
sysp += (
452+
"\n\nRESTYLING THIS DASHBOARD: if the user asks to change how the Control Room "
453+
f"looks — colours, text size, spacing/density, which sidebar items show, or adding "
454+
f"a live panel — do it through the hub API, NEVER by editing docs/dashboard.html "
455+
f"(editing the file can break the UI; the API cannot, and `reset` undoes it).\n"
456+
f" RUN: curl -s http://127.0.0.1:{PORT}/api/dashboard\n"
457+
f" RUN: curl -s -X POST http://127.0.0.1:{PORT}/api/dashboard -H 'Content-Type: application/json' "
458+
"-d '{\"op\":\"set\",\"vars\":{\"--accent\":\"#4f9dff\"},\"scale\":1.1,\"density\":\"compact\"}'\n"
459+
"Ops: set (vars/css/scale/density) · nav (hidden/order) · panel (add/del/clear, "
460+
"slot top|chat|sidebar, self-contained HTML styled with var(--accent) etc.) · "
461+
"reset (what: all|vars|css|panels|nav). Full reference: the `dashboard-designer` skill.")
449462
sysp += active_recall(message) # Active Memory: relevant context, every turn
450463
sysp += modes_prompt_suffix() # caveman / ponytail overlays when toggled on
451464
return sysp
@@ -693,6 +706,111 @@ def _watchdog_loop():
693706
# --------------------------------------------------------------------------- #
694707
# Claude login from the dashboard (interactive CLI session over HTTP) #
695708
# --------------------------------------------------------------------------- #
709+
# --------------------------------------------------------------------------- #
710+
# Live dashboard customization — agents restyle the Control Room through an API, #
711+
# never by editing dashboard.html. Overrides live server-side in one JSON doc #
712+
# the page applies on top of the active theme, so a bad instruction is undone #
713+
# with `reset` instead of a broken file. Panels are OpenUI: arbitrary agent HTML #
714+
# rendered in the same sandboxed iframes the chat/canvas widgets use. #
715+
# --------------------------------------------------------------------------- #
716+
DASH_FILE = ROOT / ".aios" / "dashboard.json"
717+
DASH_DEFAULT = {"vars": {}, "css": "", "scale": 1.0, "density": "normal",
718+
"nav": {"hidden": [], "order": []}, "panels": [], "updated": 0}
719+
DASH_SLOTS = ["top", "chat", "sidebar"]
720+
721+
_VAR_OK = re.compile(r"^--[A-Za-z0-9_-]{1,40}$")
722+
# CSS can't execute script in a <style>, but it CAN beacon out via url(http…) and
723+
# @import. Agents already own the machine, so this isn't a privilege boundary —
724+
# it's to stop a careless instruction from silently phoning home.
725+
_CSS_BAD = re.compile(r"</\s*style|@import|url\(\s*['\"]?\s*(https?:|//)", re.I)
726+
727+
728+
def dashboard_cfg() -> dict:
729+
# deepcopy, not {**DASH_DEFAULT}: a shallow copy shares the nested `vars`/`nav`
730+
# dicts with the module-level default, so writing a var would mutate the
731+
# defaults themselves and `reset` would restore the very values it should clear.
732+
cfg = copy.deepcopy(DASH_DEFAULT)
733+
cfg.update(_load_json(DASH_FILE, {}))
734+
for k, v in DASH_DEFAULT.items(): # heal older/partial files
735+
cfg.setdefault(k, copy.deepcopy(v))
736+
return cfg
737+
738+
739+
def _clean_var(k: str, v) -> tuple[str, str] | None:
740+
k = str(k).strip()
741+
if not k.startswith("--"):
742+
k = "--" + k.lstrip("-")
743+
if not _VAR_OK.match(k):
744+
return None
745+
val = str(v).strip()
746+
# A value containing } or < would break out of the rule we inject it into.
747+
if not val or len(val) > 200 or any(c in val for c in "}<>;{"):
748+
return None
749+
return k, val
750+
751+
752+
def dashboard_update(payload: dict) -> dict:
753+
cfg = dashboard_cfg()
754+
op = (payload.get("op") or "set").lower()
755+
rejected = []
756+
757+
if op == "reset":
758+
what = payload.get("what", "all")
759+
if what == "all":
760+
cfg = copy.deepcopy(DASH_DEFAULT)
761+
else:
762+
cfg[what] = copy.deepcopy(DASH_DEFAULT.get(what, ""))
763+
elif op == "set":
764+
for k, v in (payload.get("vars") or {}).items():
765+
cleaned = _clean_var(k, v)
766+
if cleaned:
767+
cfg["vars"][cleaned[0]] = cleaned[1]
768+
else:
769+
rejected.append(str(k))
770+
if "css" in payload:
771+
css = str(payload["css"] or "")[:20000]
772+
if _CSS_BAD.search(css):
773+
rejected.append("css (contains @import, remote url(), or </style>)")
774+
else:
775+
cfg["css"] = css
776+
if "scale" in payload:
777+
try:
778+
cfg["scale"] = max(0.7, min(1.6, float(payload["scale"])))
779+
except (TypeError, ValueError):
780+
rejected.append("scale")
781+
if payload.get("density") in ("normal", "compact", "comfortable"):
782+
cfg["density"] = payload["density"]
783+
elif op == "nav":
784+
if isinstance(payload.get("hidden"), list):
785+
cfg["nav"]["hidden"] = [str(x)[:40] for x in payload["hidden"]][:40]
786+
if isinstance(payload.get("order"), list):
787+
cfg["nav"]["order"] = [str(x)[:40] for x in payload["order"]][:40]
788+
elif op == "panel":
789+
act = (payload.get("action") or "add").lower()
790+
if act == "add":
791+
pid = payload.get("id") or f"p{int(time.time() * 1000) % 10**9}"
792+
panel = {"id": str(pid)[:40],
793+
"title": str(payload.get("title", "panel"))[:80],
794+
"html": str(payload.get("html", ""))[:200000],
795+
"slot": payload.get("slot") if payload.get("slot") in DASH_SLOTS else "top",
796+
"height": max(60, min(1200, int(payload.get("height", 220) or 220))),
797+
"by": str(payload.get("by", "agent"))[:40]}
798+
cfg["panels"] = [p for p in cfg["panels"] if p["id"] != panel["id"]] + [panel]
799+
cfg["panels"] = cfg["panels"][-24:]
800+
elif act in ("del", "delete", "remove"):
801+
cfg["panels"] = [p for p in cfg["panels"] if p["id"] != str(payload.get("id"))]
802+
elif act == "clear":
803+
cfg["panels"] = []
804+
else:
805+
return {"ok": False, "error": f"unknown op '{op}'", "config": cfg}
806+
807+
cfg["updated"] = time.time()
808+
_save_json(DASH_FILE, cfg)
809+
brain.audit(payload.get("by", "agent"), "dashboard." + op,
810+
json.dumps({k: v for k, v in payload.items() if k != "html"})[:400])
811+
return {"ok": True, "config": cfg, "rejected": rejected}
812+
813+
696814
# --------------------------------------------------------------------------- #
697815
# Supervised updates — watch every bundled project's upstream, have an AGENT #
698816
# review what changed, then apply only what's safe (and roll back if the #
@@ -1165,6 +1283,8 @@ def do_GET(self):
11651283
self._send(200, {"patterns": tools.fabric_patterns(), "bin": bool(tools.fabric_bin())})
11661284
elif self.path == "/api/modes":
11671285
self._send(200, {"state": modes_state(), "modes": tools.agent_modes_meta()})
1286+
elif self.path == "/api/dashboard":
1287+
self._send(200, dashboard_cfg())
11681288
elif self.path == "/api/updates":
11691289
self._send(200, {"pending": pending_updates(), "pins": updates.pins(),
11701290
"reports": updates.reports()[:20],
@@ -1355,6 +1475,8 @@ def do_POST(self):
13551475
st = set_mode(payload.get("mode", ""), payload.get("enabled", False),
13561476
payload.get("level", "full"))
13571477
self._send(200, {"state": st})
1478+
elif self.path == "/api/dashboard":
1479+
self._send(200, dashboard_update(payload))
13581480
elif self.path == "/api/updates":
13591481
op = payload.get("op", "scan")
13601482
if op == "scan":

0 commit comments

Comments
 (0)