Skip to content

Commit f8be98c

Browse files
ZD Studiosclaude
andcommitted
feat: live action monitor + install themes from plain CSS
LIVE ACTIONS — you can now watch everything the agents do on this machine, as it happens, from the dashboard. A 👁 toggle in the header turns streaming on or off (persisted), and a Live Actions view shows the feed: every shell command, web read, dashboard change, config write, skill install and blocked attempt, with the actor that asked for it, colour-coded, filterable to commands only. Two things made it actually useful rather than a log dump: - Cursor-based streaming. GET /api/actions?since=<id> returns only rows newer than what the client holds, so polling never replays and switching views doesn't duplicate. brain.audit_since()/audit_max_id() back it. - In-flight tracking. The audit log only records FINISHED actions, so a long-running command was invisible until it ended — exactly when you least want to be blind. tools.inflight() registers work as it starts, so a running command shows immediately with a live elapsed timer, and /api/exec and /api/web both register too. Verified in-browser: toggle flips and persists, 26 rows rendered with blocked attempts highlighted, a new action streamed in 26 -> 28 with no refresh, and a `sleep 5` appeared in the running panel at 1.2s elapsed while still executing. THEMES FROM CSS — the installer took a JSON blob of variables, but themes get shared as CSS: people paste a `:root{--accent:…}` block from a gist, not JSON. It now accepts either. CSS is parsed for custom properties, the name/label come from `/* name: … */` comments, and light-vs-dark is derived from --bg's relative luminance instead of asking. JSON keeps working unchanged. Verified: an 11-variable `:root` block installed as "Neon Ice", applied instantly (#caa63f -> #00e5ff), landed in the theme dropdown, dark correctly detected — and the JSON path still installs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6ec77e0 commit f8be98c

4 files changed

Lines changed: 206 additions & 4 deletions

File tree

aios_brain.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,22 @@ def audit_tail(n: int = 100) -> list[dict]:
199199
return [dict(r) for r in rows]
200200

201201

202+
def audit_since(after_id: int = 0, limit: int = 200) -> list[dict]:
203+
"""Rows newer than a cursor, oldest-first — for a live stream that never
204+
re-sends what the client already has. `id` is the cursor."""
205+
with _lock:
206+
rows = db().execute(
207+
"SELECT * FROM audit WHERE id > ? ORDER BY id ASC LIMIT ?",
208+
(int(after_id or 0), limit)).fetchall()
209+
return [dict(r) for r in rows]
210+
211+
212+
def audit_max_id() -> int:
213+
with _lock:
214+
r = db().execute("SELECT COALESCE(MAX(id), 0) m FROM audit").fetchone()
215+
return int(r["m"] if r else 0)
216+
217+
202218
# --------------------------------------------------------------------------- #
203219
# Task Brain — cron + interval + agent prompts + background CLI, one scheduler #
204220
# --------------------------------------------------------------------------- #

aios_hub.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,17 @@ def do_GET(self):
15001500
elif self.path == "/api/activity":
15011501
self._send(200, activity(self._query.get("turn", [""])[0],
15021502
int(self._query.get("since", ["0"])[0] or 0)))
1503+
elif self.path == "/api/actions":
1504+
# Cursor-based so the live monitor never re-sends what it already has.
1505+
since = int(self._query.get("since", ["0"])[0] or 0)
1506+
fresh = brain.audit_since(since, 300)
1507+
self._send(200, {
1508+
"actions": fresh,
1509+
"cursor": (fresh[-1]["id"] if fresh else max(since, brain.audit_max_id())),
1510+
"running": tools.inflight(),
1511+
"full_control": tools.full_control(),
1512+
"guardrails": tools.guardrails_on(),
1513+
})
15031514
elif self.path == "/api/swarm":
15041515
self._send(200, {**swarm_state(), "enabled": swarm_enabled()})
15051516
elif self.path == "/api/dashboard":
@@ -1729,6 +1740,7 @@ def do_POST(self):
17291740
# an agent never has to hand-roll scraping.
17301741
op = (payload.get("op") or "fetch").lower()
17311742
q = payload.get("url") or payload.get("query") or payload.get("target") or ""
1743+
_tr = tools._inflight_add(payload.get("by", "agent"), "web", f"{op} {q}"[:200])
17321744
try:
17331745
if op in ("youtube", "yt"):
17341746
r = web.youtube(q, want_transcript=payload.get("transcript", True),
@@ -1745,6 +1757,8 @@ def do_POST(self):
17451757
r = web.fetch(q, max_chars=int(payload.get("max_chars", 20000)))
17461758
except Exception as e:
17471759
r = {"ok": False, "error": f"{op} failed: {e}"}
1760+
finally:
1761+
tools._inflight_done(_tr)
17481762
brain.audit(payload.get("by", "agent"), "web." + op,
17491763
f"{q[:120]} -> ok={r.get('ok')}")
17501764
self._send(200, r)

aios_tools.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import platform
2222
import shutil
2323
import subprocess
24+
import threading
2425
import time
2526
from pathlib import Path
2627

@@ -49,6 +50,35 @@ def exec_timeout() -> int:
4950
# --------------------------------------------------------------------------- #
5051
# Shell — the capability that makes "full control" mean something #
5152
# --------------------------------------------------------------------------- #
53+
# What's executing right now. The audit log only records finished actions, so a
54+
# long-running command would be invisible in the live monitor until it ended —
55+
# which is exactly when you most want to see it.
56+
_INFLIGHT: dict[int, dict] = {}
57+
_INFLIGHT_LOCK = threading.Lock()
58+
_INFLIGHT_SEQ = [0]
59+
60+
61+
def inflight() -> list[dict]:
62+
now = time.time()
63+
with _INFLIGHT_LOCK:
64+
return [{**v, "elapsed": round(now - v["started"], 1)}
65+
for v in sorted(_INFLIGHT.values(), key=lambda x: x["started"])]
66+
67+
68+
def _inflight_add(actor: str, kind: str, detail: str) -> int:
69+
with _INFLIGHT_LOCK:
70+
_INFLIGHT_SEQ[0] += 1
71+
i = _INFLIGHT_SEQ[0]
72+
_INFLIGHT[i] = {"id": i, "actor": actor, "kind": kind,
73+
"detail": str(detail)[:400], "started": time.time()}
74+
return i
75+
76+
77+
def _inflight_done(i: int):
78+
with _INFLIGHT_LOCK:
79+
_INFLIGHT.pop(i, None)
80+
81+
5282
def shell(cmd: str, actor: str = "brain", cwd: str | None = None,
5383
timeout: int | None = None) -> dict:
5484
"""Run a command as the user who installed AIOS. Audited, guardrailed, bounded."""
@@ -70,6 +100,7 @@ def shell(cmd: str, actor: str = "brain", cwd: str | None = None,
70100
shell_cmd = (["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd] if IS_WIN
71101
else ["/bin/bash", "-lc", cmd])
72102
started = time.time()
103+
tracker = _inflight_add(actor, "shell", cmd) # visible while it runs
73104
try:
74105
p = subprocess.run(shell_cmd, cwd=cwd or str(ROOT), capture_output=True, text=True,
75106
timeout=timeout or exec_timeout(), errors="replace")
@@ -81,6 +112,8 @@ def shell(cmd: str, actor: str = "brain", cwd: str | None = None,
81112
except Exception as e:
82113
brain.audit(actor, "shell.error", f"{cmd} :: {e}", ok=False)
83114
return {"ok": False, "code": -1, "out": "", "err": str(e), "blocked": False}
115+
finally:
116+
_inflight_done(tracker)
84117

85118
brain.audit(actor, "shell", f"$ {cmd}\n(exit {code}, {time.time()-started:.1f}s)", ok=code == 0)
86119
return {"ok": code == 0, "code": code, "out": out[-8000:], "err": err[-4000:], "blocked": False}

0 commit comments

Comments
 (0)