Skip to content

Commit 33af843

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 . Every change is audited. Settings gains an 'Agent-applied appearance' card showing what's active with per-category reset buttons. The new 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: 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 — now suggests . 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 33af843

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)