Date: Sun, 1 Mar 2026 14:09:40 -0800
Subject: [PATCH 007/417] Update README.md
---
README.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6efd3a58..063fe8e3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
-
-
+
AlphaClaw
From a59c494be0358c839ab362ef66effe7de03771a5 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 14:43:47 -0800
Subject: [PATCH 008/417] Refine README copy and add Telegram topic wizard
feature
Made-with: Cursor
---
README.md | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 063fe8e3..c6e3e5b4 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@
AlphaClaw
- Setup UI. Gateway manager. Watchdog. Zero config to production.
- One package. One command. Full OpenClaw infrastructure.
+ The ops layer for OpenClaw. Deploy in minutes. Stay running for months. No CLI required.
+ Setup wizard. Self-healing watchdog. Anti-drift prompt hardening.
@@ -13,7 +13,9 @@
-The management harness for OpenClaw — a web-based Setup UI, gateway lifecycle manager, crash watchdog, and channel orchestrator that turns a bare OpenClaw install into a production-ready deployment.
+AlphaClaw wraps OpenClaw with a convenient setup wizard, self-healing watchdog, Git-backed rollback, and full browser-based observability. Ships with anti-drift prompt hardening to keep your agent disciplined, and simplifies integrations (e.g. Google Workspace) so you can manage everything from one UI instead of config files.
+
+First deploy to first message in under five minutes.
@@ -28,7 +30,7 @@
- **Guided Onboarding:** Step-by-step setup wizard — model selection, provider credentials, GitHub repo, channel pairing.
- **Gateway Manager:** Spawns, monitors, restarts, and proxies the OpenClaw gateway as a managed child process.
- **Watchdog:** Crash detection, crash-loop recovery, auto-repair (`openclaw doctor --fix`), and Telegram/Discord notifications.
-- **Channel Orchestration:** Telegram and Discord bot pairing, topic management, and credential sync from a single UI.
+- **Channel Orchestration:** Telegram and Discord bot pairing, credential sync, and a guided wizard for splitting Telegram into multi-threaded topic groups as your usage grows.
- **Webhooks:** Named webhook endpoints with per-hook transform modules, request logging, and payload inspection.
- **Google Workspace:** OAuth integration for Gmail, Calendar, Drive, Docs, Sheets, Tasks, Contacts, and Meet.
- **Prompt Hardening:** Ships anti-drift bootstrap prompts (`AGENTS.md`, `TOOLS.md`) injected into your agent's system prompt on every message — enforcing safe practices, commit discipline, and change summaries out of the box.
From 13da5d1e770c807417e93b797ffcba87c7c9a52d Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 14:53:06 -0800
Subject: [PATCH 009/417] Update README.md
---
README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index c6e3e5b4..5c9cd66d 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-
+
AlphaClaw
- The ops layer for OpenClaw. Deploy in minutes. Stay running for months. No CLI required.
- Setup wizard. Self-healing watchdog. Anti-drift prompt hardening.
+ The ops layer for OpenClaw. Deploy in minutes. Stay running for months.
+ Observability. Reliability. Agent discipline. Zero SSH rescue missions.
@@ -13,7 +13,7 @@
-AlphaClaw wraps OpenClaw with a convenient setup wizard, self-healing watchdog, Git-backed rollback, and full browser-based observability. Ships with anti-drift prompt hardening to keep your agent disciplined, and simplifies integrations (e.g. Google Workspace) so you can manage everything from one UI instead of config files.
+AlphaClaw wraps OpenClaw with a convenient setup wizard, self-healing watchdog, Git-backed rollback, and full browser-based observability. Ships with anti-drift prompt hardening to keep your agent disciplined, and simplifies integrations (e.g. Google Workspace, Telegram Topics) so you can manage everything from one UI instead of config files.
First deploy to first message in under five minutes.
From 8be5a0103443bcc873e5fc8deb2d2e2b65e77c96 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 15:22:48 -0800
Subject: [PATCH 010/417] Add no-lock-in section and hide watchdog vars from
Envars UI
Made-with: Cursor
---
README.md | 6 ++++++
lib/server/constants.js | 2 ++
2 files changed, 8 insertions(+)
diff --git a/README.md b/README.md
index 5c9cd66d..04d89dba 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,12 @@
- **Everything in the browser:** No SSH, no config files to hand-edit, no CLI required after first deploy.
- **Stays out of the way:** AlphaClaw manages infrastructure; OpenClaw handles the AI.
+## No Lock-in. Eject Anytime.
+
+AlphaClaw writes standard OpenClaw config (`openclaw.json`, `.env`). Remove AlphaClaw and your agent keeps running — nothing proprietary, nothing to migrate.
+
+---
+
## Quick Start
### Deploy (recommended)
diff --git a/lib/server/constants.js b/lib/server/constants.js
index 4c472af2..c7b2c453 100644
--- a/lib/server/constants.js
+++ b/lib/server/constants.js
@@ -143,6 +143,8 @@ const kSystemVars = new Set([
"OPENCLAW_GATEWAY_TOKEN",
"SETUP_PASSWORD",
"PORT",
+ "WATCHDOG_AUTO_REPAIR",
+ "WATCHDOG_NOTIFICATIONS_DISABLED",
]);
const kKnownVars = [
{
From 904ef694a37981ba6022460e73470859f8178082 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 15:22:54 -0800
Subject: [PATCH 011/417] 0.3.1
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index e34cd654..43869380 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.0",
+ "version": "0.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@chrysb/alphaclaw",
- "version": "0.3.0",
+ "version": "0.3.1",
"dependencies": {
"express": "^4.21.0",
"http-proxy": "^1.18.1",
diff --git a/package.json b/package.json
index 569353ff..fb151e47 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.0",
+ "version": "0.3.1",
"publishConfig": {
"access": "public"
},
From 76b40b5ce4a9551fceca3d943dcb8fd58a595650 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 18:32:04 -0800
Subject: [PATCH 012/417] Add system resource monitoring to Watchdog tab
Made-with: Cursor
---
lib/public/js/components/watchdog-tab.js | 149 +++++++++++++++++------
lib/public/js/lib/api.js | 5 +
lib/server/routes/watchdog.js | 10 ++
lib/server/system-resources.js | 104 ++++++++++++++++
4 files changed, 232 insertions(+), 36 deletions(-)
create mode 100644 lib/server/system-resources.js
diff --git a/lib/public/js/components/watchdog-tab.js b/lib/public/js/components/watchdog-tab.js
index bfe3f7ad..450b5fcd 100644
--- a/lib/public/js/components/watchdog-tab.js
+++ b/lib/public/js/components/watchdog-tab.js
@@ -4,6 +4,7 @@ import htm from "https://esm.sh/htm";
import {
fetchWatchdogEvents,
fetchWatchdogLogs,
+ fetchWatchdogResources,
fetchWatchdogSettings,
updateWatchdogSettings,
triggerWatchdogRepair,
@@ -16,9 +17,48 @@ import { showToast } from "./toast.js";
const html = htm.bind(h);
+const formatBytes = (bytes) => {
+ if (bytes == null) return "—";
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
+ if (bytes < 1024 * 1024 * 1024)
+ return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
+};
+
+const barColor = (percent) => {
+ if (percent == null) return "bg-gray-600";
+ if (percent >= 90) return "bg-red-500";
+ if (percent >= 70) return "bg-yellow-400";
+ return "bg-cyan-400";
+};
+
+const ResourceBar = ({ label, percent, detail }) => html`
+
+
${label}
+
+
${detail}
+
+`;
+
const getIncidentStatusTone = (event) => {
- const eventType = String(event?.eventType || "").trim().toLowerCase();
- const status = String(event?.status || "").trim().toLowerCase();
+ const eventType = String(event?.eventType || "")
+ .trim()
+ .toLowerCase();
+ const status = String(event?.status || "")
+ .trim()
+ .toLowerCase();
if (status === "failed") {
return {
dotClass: "bg-red-500/90",
@@ -53,6 +93,7 @@ export const WatchdogTab = ({
restartSignal = 0,
}) => {
const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
+ const resourcesPoll = usePolling(() => fetchWatchdogResources(), 5000);
const [settings, setSettings] = useState({
autoRepair: false,
notificationsEnabled: true,
@@ -66,7 +107,8 @@ export const WatchdogTab = ({
const currentWatchdogStatus = watchdogStatus || {};
const events = eventsPoll.data?.events || [];
- const isRepairInProgress = repairing || !!currentWatchdogStatus?.operationInProgress;
+ const isRepairInProgress =
+ repairing || !!currentWatchdogStatus?.operationInProgress;
useEffect(() => {
let active = true;
@@ -172,7 +214,10 @@ export const WatchdogTab = ({
},
);
onRefreshStatuses();
- showToast(`Notifications ${nextValue ? "enabled" : "disabled"}`, "success");
+ showToast(
+ `Notifications ${nextValue ? "enabled" : "disabled"}`,
+ "success",
+ );
} catch (err) {
showToast(err.message || "Could not update notifications", "error");
} finally {
@@ -210,6 +255,32 @@ export const WatchdogTab = ({
repairing=${isRepairInProgress}
/>
+ ${(() => {
+ const r = resourcesPoll.data?.resources;
+ if (!r) return null;
+ return html`
+
+
+ <${ResourceBar}
+ label="Memory"
+ percent=${r.memory?.percent}
+ detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
+ />
+ <${ResourceBar}
+ label="Disk"
+ percent=${r.disk?.percent}
+ detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
+ />
+ <${ResourceBar}
+ label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} core${r.cpu.cores > 1 ? "s" : ""})` : ""}`}
+ percent=${r.cpu?.percent}
+ detail=${r.cpu?.percent != null ? `${r.cpu.percent}%` : "—"}
+ />
+
+
+ `;
+ })()}
+
@@ -256,7 +327,9 @@ export const WatchdogTab = ({
${loadingLogs ? "Loading logs..." : logs || "No logs yet."}
+ >
+${loadingLogs ? "Loading logs..." : logs || "No logs yet."}
@@ -272,39 +345,43 @@ export const WatchdogTab = ({
${events.length === 0 &&
html`
No incidents recorded.
`}
- ${events.map(
- (event) => {
- const tone = getIncidentStatusTone(event);
- return html`
-
-
-
-
- ▸
-
- ${event.createdAt || ""} · ${event.eventType || "event"} · ${event.status ||
- "unknown"}
-
-
+ ${events.map((event) => {
+ const tone = getIncidentStatusTone(event);
+ return html`
+
+
+
+
-
-
-
-
Source: ${event.source || "unknown"}
-
${typeof event.details === "string"
- ? event.details
- : JSON.stringify(event.details || {}, null, 2)}
+ class="ac-history-toggle shrink-0"
+ aria-hidden="true"
+ >▸
+
+ ${event.createdAt || ""} · ${event.eventType || "event"}
+ · ${event.status || "unknown"}
+
+
+
-
- `;
- },
- )}
+
+
+
Source: ${event.source || "unknown"}
+
+${typeof event.details === "string"
+ ? event.details
+ : JSON.stringify(event.details || {}, null, 2)}
+
+
+ `;
+ })}
diff --git a/lib/public/js/lib/api.js b/lib/public/js/lib/api.js
index 6ab987d8..02dc397d 100644
--- a/lib/public/js/lib/api.js
+++ b/lib/public/js/lib/api.js
@@ -93,6 +93,11 @@ export async function triggerWatchdogRepair() {
return parseJsonOrThrow(res, 'Could not trigger watchdog repair');
}
+export async function fetchWatchdogResources() {
+ const res = await authFetch('/api/watchdog/resources');
+ return parseJsonOrThrow(res, 'Could not load system resources');
+}
+
export async function fetchWatchdogSettings() {
const res = await authFetch('/api/watchdog/settings');
return parseJsonOrThrow(res, 'Could not load watchdog settings');
diff --git a/lib/server/routes/watchdog.js b/lib/server/routes/watchdog.js
index c9949edd..ff1d3308 100644
--- a/lib/server/routes/watchdog.js
+++ b/lib/server/routes/watchdog.js
@@ -1,3 +1,5 @@
+const { getSystemResources } = require("../system-resources");
+
const registerWatchdogRoutes = ({
app,
requireAuth,
@@ -55,6 +57,14 @@ const registerWatchdogRoutes = ({
}
});
+ app.get("/api/watchdog/resources", requireAuth, (req, res) => {
+ try {
+ res.json({ ok: true, resources: getSystemResources() });
+ } catch (err) {
+ res.status(500).json({ ok: false, error: err.message });
+ }
+ });
+
app.put("/api/watchdog/settings", requireAuth, (req, res) => {
try {
const settings = watchdog.updateSettings(req.body || {});
diff --git a/lib/server/system-resources.js b/lib/server/system-resources.js
new file mode 100644
index 00000000..d0e231bb
--- /dev/null
+++ b/lib/server/system-resources.js
@@ -0,0 +1,104 @@
+const os = require("os");
+const fs = require("fs");
+
+const readCgroupFile = (filePath) => {
+ try {
+ return fs.readFileSync(filePath, "utf8").trim();
+ } catch {
+ return null;
+ }
+};
+
+const parseCgroupMemory = () => {
+ const current = readCgroupFile("/sys/fs/cgroup/memory.current");
+ const max = readCgroupFile("/sys/fs/cgroup/memory.max");
+ if (!current) return null;
+ const usedBytes = Number.parseInt(current, 10);
+ if (Number.isNaN(usedBytes)) return null;
+ const limitBytes =
+ max && max !== "max" ? Number.parseInt(max, 10) : null;
+ return {
+ usedBytes,
+ totalBytes: Number.isNaN(limitBytes) ? null : limitBytes,
+ };
+};
+
+const parseCgroupCpu = () => {
+ const stat = readCgroupFile("/sys/fs/cgroup/cpu.stat");
+ if (!stat) return null;
+ const lines = stat.split("\n");
+ const map = {};
+ for (const line of lines) {
+ const [key, val] = line.split(/\s+/);
+ if (key && val) map[key] = Number.parseInt(val, 10);
+ }
+ return {
+ usageUsec: map.usage_usec ?? null,
+ userUsec: map.user_usec ?? null,
+ systemUsec: map.system_usec ?? null,
+ };
+};
+
+let prevCpuSnapshot = null;
+let prevCpuSnapshotAt = 0;
+
+const getSystemResources = () => {
+ const cgroupMem = parseCgroupMemory();
+ const mem = {
+ usedBytes: cgroupMem?.usedBytes ?? process.memoryUsage().rss,
+ totalBytes: cgroupMem?.totalBytes ?? os.totalmem(),
+ };
+
+ let diskUsedBytes = null;
+ let diskTotalBytes = null;
+ try {
+ const stat = fs.statfsSync("/");
+ diskTotalBytes = stat.bsize * stat.blocks;
+ diskUsedBytes = stat.bsize * (stat.blocks - stat.bfree);
+ } catch {
+ // statfsSync unavailable
+ }
+
+ const cgroupCpu = parseCgroupCpu();
+ let cpuPercent = null;
+ if (cgroupCpu?.usageUsec != null) {
+ const now = Date.now();
+ if (prevCpuSnapshot && prevCpuSnapshotAt) {
+ const elapsedMs = now - prevCpuSnapshotAt;
+ if (elapsedMs > 0) {
+ const usageDeltaUs = cgroupCpu.usageUsec - prevCpuSnapshot.usageUsec;
+ const elapsedUs = elapsedMs * 1000;
+ cpuPercent = Math.min(100, Math.max(0, (usageDeltaUs / elapsedUs) * 100));
+ }
+ }
+ prevCpuSnapshot = cgroupCpu;
+ prevCpuSnapshotAt = now;
+ } else {
+ const load = os.loadavg();
+ const cpus = os.cpus().length || 1;
+ cpuPercent = Math.min(100, Math.max(0, (load[0] / cpus) * 100));
+ }
+
+ return {
+ memory: {
+ usedBytes: mem.usedBytes,
+ totalBytes: mem.totalBytes,
+ percent: mem.totalBytes
+ ? Math.round((mem.usedBytes / mem.totalBytes) * 1000) / 10
+ : null,
+ },
+ disk: {
+ usedBytes: diskUsedBytes,
+ totalBytes: diskTotalBytes,
+ percent: diskTotalBytes
+ ? Math.round((diskUsedBytes / diskTotalBytes) * 1000) / 10
+ : null,
+ },
+ cpu: {
+ percent: cpuPercent != null ? Math.round(cpuPercent * 10) / 10 : null,
+ cores: os.cpus().length,
+ },
+ };
+};
+
+module.exports = { getSystemResources };
From f5e355c71d133581ba364568340ed8d05d002bb4 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 18:40:50 -0800
Subject: [PATCH 013/417] Update README.md
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 04d89dba..89a6a870 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
AlphaClaw
@@ -47,7 +47,7 @@
## No Lock-in. Eject Anytime.
-AlphaClaw writes standard OpenClaw config (`openclaw.json`, `.env`). Remove AlphaClaw and your agent keeps running — nothing proprietary, nothing to migrate.
+AlphaClaw simply wraps OpenClaw, it's not a dependency. Remove AlphaClaw and your agent keeps running. Nothing proprietary, nothing to migrate.
---
From 86dea9dee3c1d7760657f2447b1870022fcc21bd Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 18:41:16 -0800
Subject: [PATCH 014/417] Update README.md
---
README.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/README.md b/README.md
index 89a6a870..ecbc6d51 100644
--- a/README.md
+++ b/README.md
@@ -49,8 +49,6 @@
AlphaClaw simply wraps OpenClaw, it's not a dependency. Remove AlphaClaw and your agent keeps running. Nothing proprietary, nothing to migrate.
----
-
## Quick Start
### Deploy (recommended)
From 5bed15a842bb066892bfd62a3c3e707bdd1f7f28 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 18:50:44 -0800
Subject: [PATCH 015/417] Add per-process memory breakdown with expandable
stacked bar
Made-with: Cursor
---
lib/public/js/components/watchdog-tab.js | 163 ++++++++++++++++++-----
lib/server/routes/watchdog.js | 3 +-
lib/server/system-resources.js | 47 ++++++-
lib/server/watchdog.js | 5 +-
4 files changed, 182 insertions(+), 36 deletions(-)
diff --git a/lib/public/js/components/watchdog-tab.js b/lib/public/js/components/watchdog-tab.js
index 450b5fcd..ad5df786 100644
--- a/lib/public/js/components/watchdog-tab.js
+++ b/lib/public/js/components/watchdog-tab.js
@@ -33,22 +33,70 @@ const barColor = (percent) => {
return "bg-cyan-400";
};
-const ResourceBar = ({ label, percent, detail }) => html`
-
-
${label}
-
-
${detail} html`
+
+
${label}
+
+ ${expanded && segments
+ ? segments.map(
+ (seg) => html`
+
+ `,
+ )
+ : html`
+
+ `}
+
+
+ ${detail}
+ ${expanded &&
+ segments &&
+ segments
+ .filter((s) => s.label)
+ .map(
+ (seg) => html`
+
+
+ ${seg.label}
+
+ `,
+ )}
+
`;
@@ -94,6 +142,7 @@ export const WatchdogTab = ({
}) => {
const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
const resourcesPoll = usePolling(() => fetchWatchdogResources(), 5000);
+ const [memoryExpanded, setMemoryExpanded] = useState(false);
const [settings, setSettings] = useState({
autoRepair: false,
notificationsEnabled: true,
@@ -260,23 +309,71 @@ export const WatchdogTab = ({
if (!r) return null;
return html`
-
- <${ResourceBar}
- label="Memory"
- percent=${r.memory?.percent}
- detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
- />
- <${ResourceBar}
- label="Disk"
- percent=${r.disk?.percent}
- detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
- />
- <${ResourceBar}
- label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} core${r.cpu.cores > 1 ? "s" : ""})` : ""}`}
- percent=${r.cpu?.percent}
- detail=${r.cpu?.percent != null ? `${r.cpu.percent}%` : "—"}
- />
-
+ ${memoryExpanded
+ ? html`
+ <${ResourceBar}
+ label="Memory"
+ detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
+ percent=${r.memory?.percent}
+ expanded=${true}
+ onToggle=${() => setMemoryExpanded(false)}
+ segments=${(() => {
+ const p = r.processes;
+ const total = r.memory?.totalBytes;
+ const used = r.memory?.usedBytes;
+ if (!p || !total || !used) return null;
+ const segs = [];
+ let tracked = 0;
+ if (p.gateway?.rssBytes != null) {
+ tracked += p.gateway.rssBytes;
+ segs.push({
+ percent: (p.gateway.rssBytes / total) * 100,
+ color: "#22d3ee",
+ label: `Gateway ${formatBytes(p.gateway.rssBytes)}`,
+ });
+ }
+ if (p.alphaclaw?.rssBytes != null) {
+ tracked += p.alphaclaw.rssBytes;
+ segs.push({
+ percent: (p.alphaclaw.rssBytes / total) * 100,
+ color: "#a78bfa",
+ label: `AlphaClaw ${formatBytes(p.alphaclaw.rssBytes)}`,
+ });
+ }
+ const other = Math.max(0, used - tracked);
+ if (other > 0) {
+ segs.push({
+ percent: (other / total) * 100,
+ color: "#4b5563",
+ label: `Other ${formatBytes(other)}`,
+ });
+ }
+ return segs.length ? segs : null;
+ })()}
+ />
+ `
+ : html`
+
+ <${ResourceBar}
+ label="Memory"
+ percent=${r.memory?.percent}
+ detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
+ onToggle=${() => setMemoryExpanded(true)}
+ />
+ <${ResourceBar}
+ label="Disk"
+ percent=${r.disk?.percent}
+ detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
+ />
+ <${ResourceBar}
+ label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} core${r.cpu.cores > 1 ? "s" : ""})` : ""}`}
+ percent=${r.cpu?.percent}
+ detail=${r.cpu?.percent != null
+ ? `${r.cpu.percent}%`
+ : "—"}
+ />
+
+ `}
`;
})()}
@@ -300,7 +397,7 @@ export const WatchdogTab = ({
Notifications
<${InfoTooltip}
- text="Sends Telegram notices for watchdog alerts and auto-repair outcomes."
+ text="Sends channel notices for watchdog alerts and auto-repair outcomes."
/>
<${ToggleSwitch}
diff --git a/lib/server/routes/watchdog.js b/lib/server/routes/watchdog.js
index ff1d3308..59d015bb 100644
--- a/lib/server/routes/watchdog.js
+++ b/lib/server/routes/watchdog.js
@@ -59,7 +59,8 @@ const registerWatchdogRoutes = ({
app.get("/api/watchdog/resources", requireAuth, (req, res) => {
try {
- res.json({ ok: true, resources: getSystemResources() });
+ const status = watchdog.getStatus();
+ res.json({ ok: true, resources: getSystemResources({ gatewayPid: status.gatewayPid }) });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
diff --git a/lib/server/system-resources.js b/lib/server/system-resources.js
index d0e231bb..5bbbca13 100644
--- a/lib/server/system-resources.js
+++ b/lib/server/system-resources.js
@@ -1,5 +1,6 @@
const os = require("os");
const fs = require("fs");
+const { execSync } = require("child_process");
const readCgroupFile = (filePath) => {
try {
@@ -39,10 +40,46 @@ const parseCgroupCpu = () => {
};
};
+const readProcStatus = (pid) => {
+ try {
+ const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
+ const vmRss = status.match(/VmRSS:\s+(\d+)\s+kB/);
+ return { rssBytes: vmRss ? Number.parseInt(vmRss[1], 10) * 1024 : null };
+ } catch {
+ return null;
+ }
+};
+
+const readPsStats = (pid) => {
+ try {
+ const out = execSync(`ps -o rss=,pcpu= -p ${pid}`, {
+ encoding: "utf8",
+ timeout: 2000,
+ stdio: ["ignore", "pipe", "ignore"],
+ }).trim();
+ const [rss, pcpu] = out.split(/\s+/);
+ return {
+ rssBytes: rss ? Number.parseInt(rss, 10) * 1024 : null,
+ cpuPercent: pcpu ? Number.parseFloat(pcpu) : null,
+ };
+ } catch {
+ return null;
+ }
+};
+
+const getProcessUsage = (pid) => {
+ if (!pid) return null;
+ const proc = readProcStatus(pid);
+ if (proc) return { rssBytes: proc.rssBytes };
+ const ps = readPsStats(pid);
+ if (ps) return { rssBytes: ps.rssBytes };
+ return null;
+};
+
let prevCpuSnapshot = null;
let prevCpuSnapshotAt = 0;
-const getSystemResources = () => {
+const getSystemResources = ({ gatewayPid = null } = {}) => {
const cgroupMem = parseCgroupMemory();
const mem = {
usedBytes: cgroupMem?.usedBytes ?? process.memoryUsage().rss,
@@ -79,6 +116,10 @@ const getSystemResources = () => {
cpuPercent = Math.min(100, Math.max(0, (load[0] / cpus) * 100));
}
+ const alphaclawRss = process.memoryUsage().rss;
+ const gatewayUsage = getProcessUsage(gatewayPid);
+ const gatewayRss = gatewayUsage?.rssBytes ?? null;
+
return {
memory: {
usedBytes: mem.usedBytes,
@@ -98,6 +139,10 @@ const getSystemResources = () => {
percent: cpuPercent != null ? Math.round(cpuPercent * 10) / 10 : null,
cores: os.cpus().length,
},
+ processes: {
+ alphaclaw: { rssBytes: alphaclawRss },
+ gateway: { rssBytes: gatewayRss, pid: gatewayPid },
+ },
};
};
diff --git a/lib/server/watchdog.js b/lib/server/watchdog.js
index 7120ebfd..708189f8 100644
--- a/lib/server/watchdog.js
+++ b/lib/server/watchdog.js
@@ -59,6 +59,7 @@ const createWatchdog = ({
notificationsDisabled: isTruthy(process.env.WATCHDOG_NOTIFICATIONS_DISABLED),
operationInProgress: false,
gatewayStartedAt: null,
+ gatewayPid: null,
crashRecoveryActive: false,
expectedRestartInProgress: false,
expectedRestartUntilMs: 0,
@@ -502,13 +503,14 @@ const createWatchdog = ({
void restartAfterCrash(correlationId);
};
- const onGatewayLaunch = ({ startedAt = Date.now() } = {}) => {
+ const onGatewayLaunch = ({ startedAt = Date.now(), pid = null } = {}) => {
state.lifecycle = "running";
state.health = "unknown";
state.crashRecoveryActive = false;
clearExpectedRestartWindow();
state.uptimeStartedAt = startedAt;
state.gatewayStartedAt = startedAt;
+ state.gatewayPid = pid;
startBootstrapHealthChecks();
};
@@ -566,6 +568,7 @@ const createWatchdog = ({
crashLoopThreshold: kWatchdogCrashLoopThreshold,
crashLoopWindowMs: kWatchdogCrashLoopWindowMs,
operationInProgress: state.operationInProgress,
+ gatewayPid: state.gatewayPid,
};
};
From 83d25692bd3681085122f864815a675c0497d1bd Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 18:50:44 -0800
Subject: [PATCH 016/417] 0.3.2-beta.0
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 43869380..7ccf6bc7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.1",
+ "version": "0.3.2-beta.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@chrysb/alphaclaw",
- "version": "0.3.1",
+ "version": "0.3.2-beta.0",
"dependencies": {
"express": "^4.21.0",
"http-proxy": "^1.18.1",
diff --git a/package.json b/package.json
index fb151e47..6052ce68 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.1",
+ "version": "0.3.2-beta.0",
"publishConfig": {
"access": "public"
},
From bd2ec1289ea51e195b66d7375993f08ebd857b1f Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:00:05 -0800
Subject: [PATCH 017/417] Fix update check to use semver comparison instead of
string equality
Made-with: Cursor
---
lib/server/alphaclaw-version.js | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/lib/server/alphaclaw-version.js b/lib/server/alphaclaw-version.js
index fd954cad..111e1bcb 100644
--- a/lib/server/alphaclaw-version.js
+++ b/lib/server/alphaclaw-version.js
@@ -10,6 +10,20 @@ const {
kRootDir,
} = require("./constants");
+const isNewerVersion = (latest, current) => {
+ if (!latest || !current) return false;
+ const parse = (v) => {
+ const [core] = String(v).replace(/^v/, "").split("-");
+ const parts = core.split(".").map(Number);
+ return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
+ };
+ const l = parse(latest);
+ const c = parse(current);
+ if (l.major !== c.major) return l.major > c.major;
+ if (l.minor !== c.minor) return l.minor > c.minor;
+ return l.patch > c.patch;
+};
+
const createAlphaclawVersionService = () => {
let kUpdateStatusCache = {
latestVersion: null,
@@ -82,11 +96,7 @@ const createAlphaclawVersionService = () => {
}
const currentVersion = readAlphaclawVersion();
const latestVersion = await fetchLatestVersionFromRegistry();
- const hasUpdate = !!(
- currentVersion &&
- latestVersion &&
- latestVersion !== currentVersion
- );
+ const hasUpdate = isNewerVersion(latestVersion, currentVersion);
kUpdateStatusCache = { latestVersion, hasUpdate, fetchedAt: Date.now() };
if (hasUpdate) {
console.log(
From defae25d50febde7380717a4909ac9ee77af8ab2 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:00:06 -0800
Subject: [PATCH 018/417] 0.3.2-beta.1
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 7ccf6bc7..ef12bde6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.0",
+ "version": "0.3.2-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.0",
+ "version": "0.3.2-beta.1",
"dependencies": {
"express": "^4.21.0",
"http-proxy": "^1.18.1",
diff --git a/package.json b/package.json
index 6052ce68..a2018154 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.0",
+ "version": "0.3.2-beta.1",
"publishConfig": {
"access": "public"
},
From 5e845eb581e570a53f386f4e649fe7e86287e96c Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:15:32 -0800
Subject: [PATCH 019/417] Fix Discord notifications using italic instead of
bold
Made-with: Cursor
---
lib/server/watchdog-notify.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/server/watchdog-notify.js b/lib/server/watchdog-notify.js
index 6c2971a8..b1ddb6b6 100644
--- a/lib/server/watchdog-notify.js
+++ b/lib/server/watchdog-notify.js
@@ -33,7 +33,8 @@ const getPairedIds = (channel) => {
return Array.from(ids);
};
-const formatDiscordMessage = (message) => String(message || "");
+const formatDiscordMessage = (message) =>
+ String(message || "").replace(/(? {
const notify = async (message) => {
From 33ee62bb705bca7974f9f957bcd88541be10e32f Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:15:32 -0800
Subject: [PATCH 020/417] 0.3.2-beta.2
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index ef12bde6..414def04 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.1",
+ "version": "0.3.2-beta.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.1",
+ "version": "0.3.2-beta.2",
"dependencies": {
"express": "^4.21.0",
"http-proxy": "^1.18.1",
diff --git a/package.json b/package.json
index a2018154..e5857452 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.1",
+ "version": "0.3.2-beta.2",
"publishConfig": {
"access": "public"
},
From 03e61cc3513145e48defb56990b98df18db69eaf Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:37:04 -0800
Subject: [PATCH 021/417] Align disk metric with data volume and show disk
source path
Made-with: Cursor
---
lib/public/js/components/watchdog-tab.js | 2 +-
lib/server/system-resources.js | 37 +++++++++++++++---------
2 files changed, 25 insertions(+), 14 deletions(-)
diff --git a/lib/public/js/components/watchdog-tab.js b/lib/public/js/components/watchdog-tab.js
index ad5df786..f1de07c9 100644
--- a/lib/public/js/components/watchdog-tab.js
+++ b/lib/public/js/components/watchdog-tab.js
@@ -361,7 +361,7 @@ export const WatchdogTab = ({
onToggle=${() => setMemoryExpanded(true)}
/>
<${ResourceBar}
- label="Disk"
+ label=${`Disk${r.disk?.path ? ` (${r.disk.path})` : ""}`}
percent=${r.disk?.percent}
detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
/>
diff --git a/lib/server/system-resources.js b/lib/server/system-resources.js
index 5bbbca13..75d8c87f 100644
--- a/lib/server/system-resources.js
+++ b/lib/server/system-resources.js
@@ -1,6 +1,7 @@
const os = require("os");
const fs = require("fs");
const { execSync } = require("child_process");
+const { kRootDir } = require("./constants");
const readCgroupFile = (filePath) => {
try {
@@ -76,6 +77,23 @@ const getProcessUsage = (pid) => {
return null;
};
+const readDiskUsage = () => {
+ const paths = [kRootDir, "/data", "/"];
+ for (const diskPath of paths) {
+ try {
+ const stat = fs.statfsSync(diskPath);
+ return {
+ usedBytes: stat.bsize * (stat.blocks - stat.bfree),
+ totalBytes: stat.bsize * stat.blocks,
+ path: diskPath,
+ };
+ } catch {
+ // Try next path.
+ }
+ }
+ return { usedBytes: null, totalBytes: null, path: null };
+};
+
let prevCpuSnapshot = null;
let prevCpuSnapshotAt = 0;
@@ -86,15 +104,7 @@ const getSystemResources = ({ gatewayPid = null } = {}) => {
totalBytes: cgroupMem?.totalBytes ?? os.totalmem(),
};
- let diskUsedBytes = null;
- let diskTotalBytes = null;
- try {
- const stat = fs.statfsSync("/");
- diskTotalBytes = stat.bsize * stat.blocks;
- diskUsedBytes = stat.bsize * (stat.blocks - stat.bfree);
- } catch {
- // statfsSync unavailable
- }
+ const diskUsage = readDiskUsage();
const cgroupCpu = parseCgroupCpu();
let cpuPercent = null;
@@ -129,10 +139,11 @@ const getSystemResources = ({ gatewayPid = null } = {}) => {
: null,
},
disk: {
- usedBytes: diskUsedBytes,
- totalBytes: diskTotalBytes,
- percent: diskTotalBytes
- ? Math.round((diskUsedBytes / diskTotalBytes) * 1000) / 10
+ usedBytes: diskUsage.usedBytes,
+ totalBytes: diskUsage.totalBytes,
+ path: diskUsage.path,
+ percent: diskUsage.totalBytes
+ ? Math.round((diskUsage.usedBytes / diskUsage.totalBytes) * 1000) / 10
: null,
},
cpu: {
From 19b9b50ac720788cc5c8658d70c69b423f8c174e Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:37:04 -0800
Subject: [PATCH 022/417] 0.3.2-beta.3
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 414def04..5551d466 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.2",
+ "version": "0.3.2-beta.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.2",
+ "version": "0.3.2-beta.3",
"dependencies": {
"express": "^4.21.0",
"http-proxy": "^1.18.1",
diff --git a/package.json b/package.json
index e5857452..46683c76 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.2",
+ "version": "0.3.2-beta.3",
"publishConfig": {
"access": "public"
},
From c83967b06e67c3b0d08969a6ce3d3a939f2d74de Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:53:54 -0800
Subject: [PATCH 023/417] Improve resource metrics across cgroup v1/v2
containers
Made-with: Cursor
---
lib/public/js/components/watchdog-tab.js | 2 +-
lib/server/system-resources.js | 106 +++++++++++++++++++++--
2 files changed, 98 insertions(+), 10 deletions(-)
diff --git a/lib/public/js/components/watchdog-tab.js b/lib/public/js/components/watchdog-tab.js
index f1de07c9..b0af1bdd 100644
--- a/lib/public/js/components/watchdog-tab.js
+++ b/lib/public/js/components/watchdog-tab.js
@@ -366,7 +366,7 @@ export const WatchdogTab = ({
detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
/>
<${ResourceBar}
- label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} core${r.cpu.cores > 1 ? "s" : ""})` : ""}`}
+ label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} vCPU)` : ""}`}
percent=${r.cpu?.percent}
detail=${r.cpu?.percent != null
? `${r.cpu.percent}%`
diff --git a/lib/server/system-resources.js b/lib/server/system-resources.js
index 75d8c87f..0815109c 100644
--- a/lib/server/system-resources.js
+++ b/lib/server/system-resources.js
@@ -11,17 +11,54 @@ const readCgroupFile = (filePath) => {
}
};
+const readFirstCgroupFile = (paths) => {
+ for (const filePath of paths) {
+ const value = readCgroupFile(filePath);
+ if (value != null) return value;
+ }
+ return null;
+};
+
+const countCpuSet = (cpuSet) => {
+ if (!cpuSet) return null;
+ let count = 0;
+ const parts = String(cpuSet)
+ .split(",")
+ .map((part) => part.trim())
+ .filter(Boolean);
+ for (const part of parts) {
+ const [startRaw, endRaw] = part.split("-");
+ const start = Number.parseInt(startRaw, 10);
+ const end = endRaw == null ? start : Number.parseInt(endRaw, 10);
+ if (Number.isNaN(start) || Number.isNaN(end)) continue;
+ count += Math.max(0, end - start + 1);
+ }
+ return count > 0 ? count : null;
+};
+
const parseCgroupMemory = () => {
- const current = readCgroupFile("/sys/fs/cgroup/memory.current");
- const max = readCgroupFile("/sys/fs/cgroup/memory.max");
+ const current = readFirstCgroupFile([
+ "/sys/fs/cgroup/memory.current",
+ "/sys/fs/cgroup/memory/memory.usage_in_bytes",
+ ]);
+ const max = readFirstCgroupFile([
+ "/sys/fs/cgroup/memory.max",
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes",
+ ]);
if (!current) return null;
const usedBytes = Number.parseInt(current, 10);
if (Number.isNaN(usedBytes)) return null;
- const limitBytes =
+ const parsedLimit =
max && max !== "max" ? Number.parseInt(max, 10) : null;
+ const limitBytes = Number.isNaN(parsedLimit) ? null : parsedLimit;
+ // Cgroup v1 uses huge sentinel values to mean "no limit".
+ const unlimited =
+ limitBytes == null ||
+ limitBytes <= 0 ||
+ limitBytes >= 9_000_000_000_000_000_000;
return {
usedBytes,
- totalBytes: Number.isNaN(limitBytes) ? null : limitBytes,
+ totalBytes: unlimited ? null : limitBytes,
};
};
@@ -41,6 +78,54 @@ const parseCgroupCpu = () => {
};
};
+const parseCgroupCpuV1 = () => {
+ const usageNs = readFirstCgroupFile([
+ "/sys/fs/cgroup/cpuacct/cpuacct.usage",
+ "/sys/fs/cgroup/cpu/cpuacct.usage",
+ ]);
+ if (!usageNs) return null;
+ const usageNsParsed = Number.parseInt(usageNs, 10);
+ if (Number.isNaN(usageNsParsed)) return null;
+ return {
+ usageUsec: Math.floor(usageNsParsed / 1000),
+ userUsec: null,
+ systemUsec: null,
+ };
+};
+
+const getAllocatedCpuCores = () => {
+ const cpuMax = readCgroupFile("/sys/fs/cgroup/cpu.max");
+ if (cpuMax) {
+ const [quotaRaw, periodRaw] = cpuMax.split(/\s+/);
+ const quota = Number.parseInt(quotaRaw, 10);
+ const period = Number.parseInt(periodRaw, 10);
+ if (quotaRaw !== "max" && !Number.isNaN(quota) && !Number.isNaN(period) && period > 0) {
+ return quota / period;
+ }
+ }
+
+ const quotaV1 = readFirstCgroupFile([
+ "/sys/fs/cgroup/cpu/cpu.cfs_quota_us",
+ "/sys/fs/cgroup/cpuacct/cpu.cfs_quota_us",
+ ]);
+ const periodV1 = readFirstCgroupFile([
+ "/sys/fs/cgroup/cpu/cpu.cfs_period_us",
+ "/sys/fs/cgroup/cpuacct/cpu.cfs_period_us",
+ ]);
+ if (quotaV1 && periodV1) {
+ const quota = Number.parseInt(quotaV1, 10);
+ const period = Number.parseInt(periodV1, 10);
+ if (!Number.isNaN(quota) && !Number.isNaN(period) && quota > 0 && period > 0) {
+ return quota / period;
+ }
+ }
+
+ const cpuSet =
+ readCgroupFile("/sys/fs/cgroup/cpuset.cpus.effective") ||
+ readCgroupFile("/sys/fs/cgroup/cpuset.cpus");
+ return countCpuSet(cpuSet);
+};
+
const readProcStatus = (pid) => {
try {
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
@@ -98,6 +183,8 @@ let prevCpuSnapshot = null;
let prevCpuSnapshotAt = 0;
const getSystemResources = ({ gatewayPid = null } = {}) => {
+ const hostCores = os.cpus().length || 1;
+ const allocatedCores = getAllocatedCpuCores() || hostCores;
const cgroupMem = parseCgroupMemory();
const mem = {
usedBytes: cgroupMem?.usedBytes ?? process.memoryUsage().rss,
@@ -106,7 +193,7 @@ const getSystemResources = ({ gatewayPid = null } = {}) => {
const diskUsage = readDiskUsage();
- const cgroupCpu = parseCgroupCpu();
+ const cgroupCpu = parseCgroupCpu() || parseCgroupCpuV1();
let cpuPercent = null;
if (cgroupCpu?.usageUsec != null) {
const now = Date.now();
@@ -115,15 +202,15 @@ const getSystemResources = ({ gatewayPid = null } = {}) => {
if (elapsedMs > 0) {
const usageDeltaUs = cgroupCpu.usageUsec - prevCpuSnapshot.usageUsec;
const elapsedUs = elapsedMs * 1000;
- cpuPercent = Math.min(100, Math.max(0, (usageDeltaUs / elapsedUs) * 100));
+ const rawPercent = (usageDeltaUs / elapsedUs) * 100;
+ cpuPercent = Math.min(100, Math.max(0, rawPercent / allocatedCores));
}
}
prevCpuSnapshot = cgroupCpu;
prevCpuSnapshotAt = now;
} else {
const load = os.loadavg();
- const cpus = os.cpus().length || 1;
- cpuPercent = Math.min(100, Math.max(0, (load[0] / cpus) * 100));
+ cpuPercent = Math.min(100, Math.max(0, (load[0] / allocatedCores) * 100));
}
const alphaclawRss = process.memoryUsage().rss;
@@ -148,7 +235,8 @@ const getSystemResources = ({ gatewayPid = null } = {}) => {
},
cpu: {
percent: cpuPercent != null ? Math.round(cpuPercent * 10) / 10 : null,
- cores: os.cpus().length,
+ cores: Math.round(allocatedCores * 10) / 10,
+ hostCores,
},
processes: {
alphaclaw: { rssBytes: alphaclawRss },
From 5b8f761c62ba6779ce5f146718c936c916cf4cbe Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 19:53:54 -0800
Subject: [PATCH 024/417] 0.3.2-beta.4
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5551d466..877cb8f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.3",
+ "version": "0.3.2-beta.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.3",
+ "version": "0.3.2-beta.4",
"dependencies": {
"express": "^4.21.0",
"http-proxy": "^1.18.1",
diff --git a/package.json b/package.json
index 46683c76..d5908293 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.3",
+ "version": "0.3.2-beta.4",
"publishConfig": {
"access": "public"
},
From 11d572c450dc4c0a29761ee2e73c5ad455880b1a Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 20:10:38 -0800
Subject: [PATCH 025/417] Document beta-to-production release flow in AGENTS
---
lib/setup/core-prompts/AGENTS.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/lib/setup/core-prompts/AGENTS.md b/lib/setup/core-prompts/AGENTS.md
index 81f1496f..29aca2c8 100644
--- a/lib/setup/core-prompts/AGENTS.md
+++ b/lib/setup/core-prompts/AGENTS.md
@@ -32,6 +32,24 @@ Changes committed ([abc1234](commit url)): <-- linked commit hash
• path/or/resource (new|edit|delete) — brief description
```
+### Release Flow (Beta -> Production)
+
+Use this release flow when promoting tested beta builds to production:
+
+1. Ensure `main` is clean and synced, and tests pass.
+2. Publish beta iterations as needed:
+ - `npm version prerelease --preid=beta`
+ - `git push && git push --tags`
+ - `npm publish --tag beta`
+3. For beta template testing, pin exact beta in template `package.json` (for example `0.3.2-beta.4`) to avoid Docker layer cache reusing old installs.
+4. When ready for production, publish a stable release version (for example `0.3.2`):
+ - `npm version 0.3.2`
+ - `git push && git push --tags`
+ - `npm publish` (publishes to `latest`)
+5. Return templates to production channel:
+ - `@chrysb/alphaclaw: "latest"`
+6. Optionally keep beta branch/tag flows active for next release cycle.
+
### Telegram Notice Format (AlphaClaw)
Use this format for any Telegram notices sent from AlphaClaw services (watchdog, system alerts, repair notices):
From ad5f3341cbb1d9ca6f7704452035721b07877dd5 Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 20:10:38 -0800
Subject: [PATCH 026/417] 0.3.2
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 877cb8f4..28ed1a50 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.4",
+ "version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.4",
+ "version": "0.3.2",
"dependencies": {
"express": "^4.21.0",
"http-proxy": "^1.18.1",
diff --git a/package.json b/package.json
index d5908293..0766743c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrysb/alphaclaw",
- "version": "0.3.2-beta.4",
+ "version": "0.3.2",
"publishConfig": {
"access": "public"
},
From 50752da6ab11ce85cceb50131e0e34da4ddbda5f Mon Sep 17 00:00:00 2001
From: Chrys Bader
Date: Sun, 1 Mar 2026 23:00:04 -0800
Subject: [PATCH 027/417] Add browse explorer with syntax-aware editing and git
summary panel.
This introduces a full sidebar browse workflow with resizable panes, file-type icons/highlighting, markdown preview improvements, and scoped git sync/summary updates for faster in-app editing feedback.
---
bin/alphaclaw.js | 32 +-
lib/public/css/explorer.css | 955 ++++++++++++++++++
lib/public/css/shell.css | 34 +-
lib/public/css/theme.css | 5 +
lib/public/icons/folder-line.svg | 1 +
lib/public/icons/hashtag.svg | 3 +
lib/public/icons/home-5-line.svg | 1 +
lib/public/js/app.js | 394 +++++---
lib/public/js/components/file-tree.js | 263 +++++
lib/public/js/components/file-viewer.js | 524 ++++++++++
lib/public/js/components/icons.js | 169 ++++
lib/public/js/components/sidebar-git-panel.js | 149 +++
lib/public/js/components/sidebar.js | 267 +++++
lib/public/js/lib/api.js | 26 +
lib/public/js/lib/file-highlighting.js | 6 +
lib/public/js/lib/syntax-highlighters/css.js | 124 +++
.../js/lib/syntax-highlighters/frontmatter.js | 49 +
lib/public/js/lib/syntax-highlighters/html.js | 209 ++++
.../js/lib/syntax-highlighters/index.js | 28 +
.../js/lib/syntax-highlighters/javascript.js | 134 +++
lib/public/js/lib/syntax-highlighters/json.js | 61 ++
.../js/lib/syntax-highlighters/markdown.js | 37 +
.../js/lib/syntax-highlighters/utils.js | 13 +
lib/public/setup.html | 1 +
lib/server.js | 6 +
lib/server/constants.js | 1 +
lib/server/routes/browse.js | 295 ++++++
27 files changed, 3643 insertions(+), 144 deletions(-)
create mode 100644 lib/public/css/explorer.css
create mode 100644 lib/public/icons/folder-line.svg
create mode 100644 lib/public/icons/hashtag.svg
create mode 100644 lib/public/icons/home-5-line.svg
create mode 100644 lib/public/js/components/file-tree.js
create mode 100644 lib/public/js/components/file-viewer.js
create mode 100644 lib/public/js/components/sidebar-git-panel.js
create mode 100644 lib/public/js/components/sidebar.js
create mode 100644 lib/public/js/lib/file-highlighting.js
create mode 100644 lib/public/js/lib/syntax-highlighters/css.js
create mode 100644 lib/public/js/lib/syntax-highlighters/frontmatter.js
create mode 100644 lib/public/js/lib/syntax-highlighters/html.js
create mode 100644 lib/public/js/lib/syntax-highlighters/index.js
create mode 100644 lib/public/js/lib/syntax-highlighters/javascript.js
create mode 100644 lib/public/js/lib/syntax-highlighters/json.js
create mode 100644 lib/public/js/lib/syntax-highlighters/markdown.js
create mode 100644 lib/public/js/lib/syntax-highlighters/utils.js
create mode 100644 lib/server/routes/browse.js
diff --git a/bin/alphaclaw.js b/bin/alphaclaw.js
index 51cd77d1..ea717db8 100755
--- a/bin/alphaclaw.js
+++ b/bin/alphaclaw.js
@@ -84,6 +84,7 @@ start options:
git-sync options:
--message, -m Commit message
+ --file, -f Optional file path in .openclaw to sync only one file
telegram topic add options:
--thread Telegram thread ID
@@ -93,6 +94,7 @@ telegram topic add options:
Examples:
alphaclaw git-sync --message "sync workspace"
+ alphaclaw git-sync --message "update config" --file "workspace/app/config.json"
alphaclaw telegram topic add --thread 12 --name "Testing"
alphaclaw telegram topic add --thread 12 --name "Testing" --system "Handle QA requests"
`);
@@ -234,10 +236,26 @@ const runGitSync = () => {
const commitMessage = String(
flagValue(commandArgs, "--message", "-m") || "",
).trim();
+ const requestedFilePath = String(
+ flagValue(commandArgs, "--file", "-f") || "",
+ ).trim();
+ const normalizedFilePath = requestedFilePath
+ ? requestedFilePath.replace(/\\/g, "/").replace(/^\.\/+/, "")
+ : "";
if (!commitMessage) {
console.error("[alphaclaw] Missing --message for git-sync");
return 1;
}
+ if (normalizedFilePath) {
+ if (
+ normalizedFilePath.startsWith("/") ||
+ normalizedFilePath.startsWith("../") ||
+ normalizedFilePath.includes("/../")
+ ) {
+ console.error("[alphaclaw] --file must stay within /data/.openclaw");
+ return 1;
+ }
+ }
if (!githubToken) {
console.error("[alphaclaw] Missing GITHUB_TOKEN for git-sync");
return 1;
@@ -312,13 +330,23 @@ const runGitSync = () => {
`[alphaclaw] Remote branch "${branch}" not found, skipping pull`,
);
}
- runGit("add -A");
+ if (normalizedFilePath) {
+ runGit(`add -A -- ${quoteArg(normalizedFilePath)}`);
+ } else {
+ runGit("add -A");
+ }
try {
runGit("diff --cached --quiet");
console.log("[alphaclaw] No changes to commit");
return 0;
} catch {}
- runGit(`commit -m ${quoteArg(commitMessage)}`);
+ if (normalizedFilePath) {
+ runGit(
+ `commit -m ${quoteArg(commitMessage)} -- ${quoteArg(normalizedFilePath)}`,
+ );
+ } else {
+ runGit(`commit -m ${quoteArg(commitMessage)}`);
+ }
runGit(`push origin ${quoteArg(branch)}`, { withAuth: true });
const hash = String(runGit("rev-parse --short HEAD")).trim();
console.log(`[alphaclaw] Git sync complete (${hash})`);
diff --git a/lib/public/css/explorer.css b/lib/public/css/explorer.css
new file mode 100644
index 00000000..eada1ee6
--- /dev/null
+++ b/lib/public/css/explorer.css
@@ -0,0 +1,955 @@
+/* ── Browse/Explorer mode ─────────────────────── */
+
+.app-content.browse-mode {
+ padding: 0;
+}
+
+.sidebar-tabs {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0px 12px 6px;
+ background: transparent;
+}
+
+.sidebar-tab {
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ color: rgba(99, 235, 255, 0.8);
+ border: 0;
+ border-radius: 10px;
+ background: transparent;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: color 0.12s, background 0.12s, box-shadow 0.12s, transform 0.12s;
+}
+
+.sidebar-tab:hover {
+ color: #a9eefb;
+}
+
+.sidebar-tab.active {
+ color: #b9f5ff;
+ background: rgba(99, 235, 255, 0.08);
+ box-shadow: 0 0 14px rgba(99, 235, 255, 0.16);
+}
+
+.sidebar-tab-icon {
+ width: 17px;
+ height: 17px;
+ display: block;
+ opacity: 0.92;
+}
+
+.sidebar-tab:hover .sidebar-tab-icon,
+.sidebar-tab.active .sidebar-tab-icon {
+ opacity: 1;
+}
+
+.sidebar-browse-layout {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+.sidebar-browse-panel {
+ display: flex;
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.sidebar-browse-resizer {
+ height: 6px;
+ cursor: row-resize;
+ position: relative;
+}
+
+.sidebar-browse-resizer::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 2px;
+ height: 2px;
+ background: transparent;
+ transition: background 0.12s;
+}
+
+.sidebar-browse-resizer:hover::before,
+.sidebar-browse-resizer.is-resizing::before {
+ background: rgba(99, 235, 255, 0.55);
+}
+
+.sidebar-browse-bottom {
+ flex: 0 0 auto;
+ min-height: 0;
+ overflow: hidden;
+ padding-top: 0;
+}
+
+.sidebar-browse-bottom-inner {
+ min-height: 120px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.file-tree-wrap {
+ width: 100%;
+ overflow-y: auto;
+ padding: 8px 0;
+}
+
+.file-tree-wrap::-webkit-scrollbar {
+ width: 6px;
+}
+
+.file-tree-wrap::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.file-tree-wrap::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 3px;
+}
+
+.file-tree {
+ list-style: none;
+}
+
+.tree-item {
+ position: relative;
+}
+
+.tree-item > a {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 10px 2px 18px;
+ color: var(--text-muted);
+ text-decoration: none;
+ font-size: 13px;
+ font-weight: 400;
+ transition: background 0.1s, color 0.1s;
+ cursor: pointer;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ user-select: none;
+}
+
+.tree-item > a:hover {
+ background: var(--bg-hover);
+ color: var(--text);
+}
+
+.tree-item > a.active {
+ background: var(--bg-active);
+ color: var(--accent);
+}
+
+.tree-item > a.active::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ background: var(--accent);
+}
+
+.tree-folder {
+ padding: 2px 10px 2px 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--text);
+ font-weight: 400;
+ cursor: pointer;
+ user-select: none;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.tree-folder:hover {
+ background: var(--bg-hover);
+}
+
+.arrow {
+ font-size: 10px;
+ transition: transform 0.15s;
+ color: var(--text-dim);
+ flex-shrink: 0;
+}
+
+.tree-folder.collapsed .arrow {
+ transform: rotate(-90deg);
+}
+
+.file-icon {
+ flex-shrink: 0;
+ width: 15px;
+ height: 15px;
+ display: block;
+ color: var(--text-dim);
+}
+
+.file-icon-md {
+ color: var(--accent);
+}
+
+.file-icon-js {
+ color: #f4d03f;
+}
+
+.file-icon-json {
+ color: #9b7bff;
+}
+
+.file-icon-css {
+ color: #7ec8ff;
+}
+
+.file-icon-html {
+ color: #ff9d57;
+}
+
+.file-icon-image {
+ color: #ff7ac6;
+}
+
+.file-icon-shell {
+ color: #71f8a7;
+}
+
+.file-icon-db {
+ color: #67b3ff;
+}
+
+.file-icon-generic {
+ color: var(--text-muted);
+}
+
+.tree-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.tree-children {
+ list-style: none;
+}
+
+.tree-children.hidden {
+ display: none;
+}
+
+.file-tree-state {
+ padding: 10px 14px;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.file-tree-state-error {
+ color: #f87171;
+}
+
+.file-viewer {
+ width: 100%;
+ min-height: 100%;
+ height: calc(100vh - 24px);
+ display: flex;
+ flex-direction: column;
+ background: transparent;
+}
+
+.file-viewer-tabbar {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ background: var(--bg-sidebar);
+ border-bottom: 1px solid var(--border);
+ height: 35px;
+}
+
+.file-viewer-tabbar-spacer {
+ flex: 1;
+}
+
+.file-viewer-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ height: 35px;
+ padding: 0 16px;
+ font-size: 12px;
+ line-height: 1;
+ color: var(--text-muted);
+ border-right: 1px solid var(--border);
+ white-space: nowrap;
+}
+
+.file-viewer-tab.active {
+ color: var(--text);
+ background: var(--bg);
+ border-bottom: 1px solid var(--accent);
+ margin-bottom: -1px;
+}
+
+.file-viewer-breadcrumb {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.file-viewer-breadcrumb-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--text-muted);
+}
+
+.file-viewer-breadcrumb-item .is-current {
+ color: var(--text);
+}
+
+.file-viewer-sep {
+ color: var(--text-dim);
+}
+
+.frontmatter-box {
+ margin-top: 16px;
+ margin-bottom: 4px;
+ margin-right: 20px;
+ margin-left: 36px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+ background: rgba(99, 235, 255, 0.025);
+}
+
+.frontmatter-title {
+ width: 100%;
+ border: 0;
+ text-align: left;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+ background: rgba(13, 17, 23, 0.55);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
+ padding: 4px 10px;
+}
+
+.frontmatter-title:hover {
+ background: rgba(13, 17, 23, 0.75);
+}
+
+.frontmatter-chevron {
+ width: 12px;
+ height: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--accent);
+ transition: transform 0.15s ease;
+}
+
+.frontmatter-chevron.open {
+ transform: rotate(90deg);
+}
+
+.frontmatter-chevron svg {
+ width: 12px;
+ height: 12px;
+ display: block;
+}
+
+.frontmatter-chevron path {
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.frontmatter-grid {
+ display: grid;
+}
+
+.frontmatter-row {
+ display: grid;
+ grid-template-columns: 160px 1fr;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.frontmatter-row:first-child {
+ border-top: 0;
+}
+
+.frontmatter-key {
+ padding: 6px 10px;
+ color: var(--keyword);
+ border-right: 1px solid rgba(255, 255, 255, 0.05);
+ word-break: break-word;
+ opacity: 0.85;
+}
+
+.frontmatter-value {
+ padding: 6px 10px;
+ color: var(--string);
+ white-space: pre-wrap;
+ word-break: break-word;
+ opacity: 0.88;
+}
+
+.frontmatter-value-pre {
+ margin: 0;
+ font-family: inherit;
+ font-size: 12px;
+}
+
+.file-viewer-save-btn {
+ display: inline-flex;
+ align-items: center;
+ margin-right: 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.02);
+ color: var(--text-muted);
+ font-family: inherit;
+ font-size: 11px;
+ letter-spacing: 0.03em;
+ padding: 5px 10px;
+ cursor: pointer;
+}
+
+.file-viewer-save-btn:hover:not(:disabled) {
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.file-viewer-save-btn.active {
+ color: var(--accent);
+ background: var(--bg-active);
+}
+
+.file-viewer-save-btn:disabled {
+ opacity: 0.6;
+ cursor: default;
+}
+
+.file-viewer-view-toggle {
+ display: flex;
+ align-items: center;
+ margin-right: 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.file-viewer-view-toggle-button {
+ border: 0;
+ background: transparent;
+ color: var(--text-muted);
+ font-family: inherit;
+ font-size: 11px;
+ text-transform: lowercase;
+ letter-spacing: 0.03em;
+ padding: 5px 10px;
+ cursor: pointer;
+}
+
+.file-viewer-view-toggle-button:hover {
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.file-viewer-view-toggle-button.active {
+ color: var(--accent);
+ background: var(--bg-active);
+}
+
+.file-viewer-editor-shell {
+ width: 100%;
+ min-height: 0;
+ height: 100%;
+ flex: 1;
+ display: flex;
+ align-items: stretch;
+}
+
+.file-viewer-editor-line-num-col {
+ width: 56px;
+ flex-shrink: 0;
+ overflow: hidden;
+ padding: 16px 16px 112px 0;
+ text-align: right;
+}
+
+.file-viewer-editor-line-num {
+ min-height: 22px;
+ line-height: 22px;
+ color: var(--text-dim);
+ font-size: 12px;
+ user-select: none;
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-start;
+}
+
+.file-viewer-editor {
+ width: 100%;
+ min-height: 0;
+ height: 100%;
+ flex: 1;
+ border: 0;
+ outline: none;
+ resize: none;
+ overflow-y: auto;
+ background: transparent;
+ color: var(--text);
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 13px;
+ line-height: 22px;
+ padding: 16px 20px 112px 0;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.file-viewer-editor-stack {
+ position: relative;
+ flex: 1;
+ min-height: 0;
+}
+
+.file-viewer-editor-highlight {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ pointer-events: none;
+ background: transparent;
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 13px;
+ line-height: 22px;
+ color: var(--text);
+ padding: 16px 20px 112px 0;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.file-viewer-editor-highlight-line {
+ min-height: 22px;
+}
+
+.file-viewer-editor-highlight-line-content {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.file-viewer-editor-overlay {
+ position: absolute;
+ inset: 0;
+ color: transparent;
+ caret-color: var(--text);
+ -webkit-text-fill-color: transparent;
+}
+
+.file-viewer-editor-overlay::selection {
+ background: rgba(99, 235, 255, 0.25);
+}
+
+.file-viewer-preview {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 20px 112px 36px;
+ line-height: 1.75;
+ background: transparent;
+}
+
+.file-viewer-pane-hidden {
+ display: none;
+}
+
+.file-viewer-preview h1,
+.file-viewer-preview h2,
+.file-viewer-preview h3,
+.file-viewer-preview h4,
+.file-viewer-preview h5,
+.file-viewer-preview h6 {
+ color: var(--accent);
+ margin: 18px 0 10px;
+ font-weight: 600;
+}
+
+.file-viewer-preview h1 {
+ font-size: 2em;
+}
+
+.file-viewer-preview h2 {
+ font-size: 1.5em;
+}
+
+.file-viewer-preview h3 {
+ font-size: 1.25em;
+}
+
+.file-viewer-preview h4 {
+ font-size: 1.1em;
+}
+
+.file-viewer-preview h5 {
+ font-size: 1em;
+}
+
+.file-viewer-preview h6 {
+ font-size: 0.9em;
+}
+
+.file-viewer-preview p,
+.file-viewer-preview ul,
+.file-viewer-preview ol,
+.file-viewer-preview blockquote,
+.file-viewer-preview pre,
+.file-viewer-preview table {
+ margin: 10px 0;
+}
+
+.file-viewer-preview ul,
+.file-viewer-preview ol {
+ padding-left: 20px;
+}
+
+.file-viewer-preview ul {
+ list-style: disc;
+}
+
+.file-viewer-preview ol {
+ list-style: decimal;
+}
+
+.file-viewer-preview a {
+ color: var(--accent);
+}
+
+.file-viewer-preview code {
+ color: var(--string);
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 1px 6px;
+}
+
+.file-viewer-preview pre {
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 12px;
+ overflow-x: auto;
+}
+
+.file-viewer-preview pre code {
+ background: transparent;
+ border: 0;
+ padding: 0;
+}
+
+.file-viewer-preview blockquote {
+ border-left: 2px solid var(--accent-dim);
+ padding-left: 12px;
+ color: var(--comment);
+}
+
+.file-viewer-preview table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.file-viewer-preview th,
+.file-viewer-preview td {
+ border: 1px solid var(--border);
+ padding: 6px 8px;
+ text-align: left;
+}
+
+.file-viewer-preview th {
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.file-viewer-editor-highlight-line-content .hl-comment {
+ color: var(--comment);
+ font-style: italic;
+}
+
+.file-viewer-editor-highlight-line-content .hl-heading {
+ color: var(--accent);
+ font-weight: 700;
+}
+
+.file-viewer-editor-highlight-line-content .hl-string {
+ color: var(--string);
+}
+
+.file-viewer-editor-highlight-line-content .hl-bullet {
+ color: var(--orange);
+}
+
+.file-viewer-editor-highlight-line-content .hl-bold {
+ color: var(--text);
+ font-weight: 700;
+}
+
+.file-viewer-editor-highlight-line-content .hl-link {
+ color: var(--accent);
+ text-decoration: underline;
+ text-decoration-style: dotted;
+}
+
+.file-viewer-editor-highlight-line-content .hl-meta {
+ color: var(--text-dim);
+}
+
+.file-viewer-editor-highlight-line-content .hl-key {
+ color: var(--keyword);
+}
+
+.file-viewer-editor-highlight-line-content .hl-keyword {
+ color: var(--keyword);
+}
+
+.file-viewer-editor-highlight-line-content .hl-tag {
+ color: var(--accent);
+}
+
+.file-viewer-editor-highlight-line-content .hl-attr {
+ color: var(--keyword);
+}
+
+.file-viewer-editor-highlight-line-content .hl-entity {
+ color: var(--number);
+}
+
+.file-viewer-editor-highlight-line-content .hl-number {
+ color: var(--number);
+}
+
+.file-viewer-editor-highlight-line-content .hl-boolean,
+.file-viewer-editor-highlight-line-content .hl-null {
+ color: var(--orange);
+}
+
+.file-viewer-editor-highlight-line-content .hl-punc {
+ color: #5f6674;
+}
+
+.file-viewer-state {
+ padding: 20px;
+ color: var(--text-muted);
+ font-size: 12px;
+}
+
+.file-viewer-state-error {
+ color: #f87171;
+}
+
+.file-viewer-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: calc(100vh - 120px);
+ color: var(--text-dim);
+ text-align: center;
+ padding: 48px;
+ gap: 10px;
+}
+
+.file-viewer-empty-mark {
+ font-size: 26px;
+ font-weight: 500;
+ letter-spacing: 0.08em;
+ color: var(--accent);
+ text-shadow:
+ 0 0 12px rgba(99, 235, 255, 0.38),
+ 0 0 22px rgba(99, 235, 255, 0.2);
+}
+
+.file-viewer-empty-title {
+ font-size: 13px;
+ font-weight: 400;
+ color: var(--text);
+ letter-spacing: 0.01em;
+}
+
+.sidebar-git-panel {
+ padding: 0;
+ margin: 0;
+ font-size: 11px;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
+.sidebar-git-loading {
+ min-height: 58px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.sidebar-git-panel-error {
+ color: #f87171;
+}
+
+.sidebar-git-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-shrink: 0;
+ min-height: 28px;
+ padding: 0 12px;
+ border-top: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.018);
+}
+
+.sidebar-git-bar:first-child {
+ border-bottom: 1px double var(--border);
+}
+
+.sidebar-git-bar-secondary {
+ margin-top: 0;
+ border-top: 0;
+ border-bottom: 0;
+ background: rgba(255, 255, 255, 0.012);
+}
+
+.sidebar-git-bar-main {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.sidebar-git-link {
+ color: inherit;
+ text-decoration: none;
+}
+
+.sidebar-git-link:hover .sidebar-git-repo-name {
+ color: var(--accent);
+}
+
+.sidebar-git-bar-icon {
+ width: 14px;
+ height: 14px;
+ color: var(--text-muted);
+ flex-shrink: 0;
+}
+
+.sidebar-git-repo-name {
+ color: var(--text);
+ font-size: 11px;
+ letter-spacing: 0.03em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.sidebar-git-branch {
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.sidebar-git-dirty {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.sidebar-git-dirty.is-clean {
+ color: #71f8a7;
+}
+
+.sidebar-git-dirty.is-dirty {
+ color: #f3a86a;
+}
+
+.sidebar-git-meta {
+ color: var(--text-muted);
+}
+
+.sidebar-git-list {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ padding: 6px 10px 0;
+ overflow-y: auto;
+ min-height: 0;
+ flex: 1 1 auto;
+}
+
+.sidebar-git-list li {
+ display: flex;
+ align-items: baseline;
+ flex: 0 0 auto;
+ gap: 6px;
+ color: var(--text-muted);
+ line-height: 1.4;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.sidebar-git-commit-link {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 auto;
+ color: inherit;
+ text-decoration: none;
+ min-width: 0;
+ line-height: 1.4;
+}
+
+.sidebar-git-commit-link:hover {
+ color: var(--text);
+}
+
+.sidebar-git-commit-link:hover .sidebar-git-hash {
+ color: var(--accent);
+}
+
+.sidebar-git-hash {
+ color: var(--accent);
+ flex-shrink: 0;
+}
+
+@media (max-width: 768px) {
+ .sidebar-browse-resizer {
+ display: none;
+ }
+
+ .app-content.browse-mode {
+ padding: 0;
+ }
+}
diff --git a/lib/public/css/shell.css b/lib/public/css/shell.css
index 4f865863..b81394de 100644
--- a/lib/public/css/shell.css
+++ b/lib/public/css/shell.css
@@ -2,7 +2,7 @@
.app-shell {
display: grid;
- grid-template-columns: 220px 1fr;
+ grid-template-columns: 220px 6px minmax(0, 1fr);
grid-template-rows: auto 1fr 24px;
height: 100vh;
position: relative;
@@ -42,6 +42,7 @@
}
.app-content {
+ grid-column: 3;
grid-row: 2;
overflow-y: auto;
padding: 24px 32px;
@@ -52,6 +53,7 @@
/* ── Sidebar ───────────────────────────────────── */
.app-sidebar {
+ grid-column: 1;
grid-row: 2;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
@@ -62,6 +64,29 @@
flex-direction: column;
}
+.sidebar-resizer {
+ grid-column: 2;
+ grid-row: 2;
+ cursor: col-resize;
+ position: relative;
+}
+
+.sidebar-resizer::before {
+ content: "";
+ position: absolute;
+ left: 2px;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ background: transparent;
+ transition: background 0.12s;
+}
+
+.sidebar-resizer:hover::before,
+.sidebar-resizer.is-resizing::before {
+ background: rgba(99, 235, 255, 0.55);
+}
+
.sidebar-brand {
padding: 16px;
font-size: 14px;
@@ -122,7 +147,7 @@
.sidebar-footer:empty { display: none; }
.sidebar-footer:not(:empty) {
- padding: 12px 16px;
+ padding: 0 16px 12px 16px;
border-top: 1px solid var(--border);
}
@@ -267,10 +292,15 @@
transform: none;
}
.app-content {
+ grid-column: 1;
grid-row: 2;
padding: 0 14px 12px;
}
+ .sidebar-resizer {
+ display: none;
+ }
+
.mobile-topbar {
display: flex;
align-items: center;
diff --git a/lib/public/css/theme.css b/lib/public/css/theme.css
index 4766a94c..c19d5978 100644
--- a/lib/public/css/theme.css
+++ b/lib/public/css/theme.css
@@ -12,6 +12,11 @@
--accent: #63ebff;
--accent-dim: rgba(99, 235, 255, 0.4);
--accent-link: rgba(99, 235, 255, 0.6);
+ --orange: #d98a58;
+ --comment: #6a737d;
+ --keyword: #ff7b72;
+ --string: #a5d6ff;
+ --number: #79c0ff;
--panel-bg-contrast: rgba(255, 255, 255, 0.028);
--panel-border-contrast: rgba(255, 255, 255, 0.11);
--field-bg-contrast: rgba(0, 0, 0, 0.3);
diff --git a/lib/public/icons/folder-line.svg b/lib/public/icons/folder-line.svg
new file mode 100644
index 00000000..4a0d1dfb
--- /dev/null
+++ b/lib/public/icons/folder-line.svg
@@ -0,0 +1 @@
+
diff --git a/lib/public/icons/hashtag.svg b/lib/public/icons/hashtag.svg
new file mode 100644
index 00000000..f67763ae
--- /dev/null
+++ b/lib/public/icons/hashtag.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/lib/public/icons/home-5-line.svg b/lib/public/icons/home-5-line.svg
new file mode 100644
index 00000000..904517d6
--- /dev/null
+++ b/lib/public/icons/home-5-line.svg
@@ -0,0 +1 @@
+
diff --git a/lib/public/js/app.js b/lib/public/js/app.js
index 1f8608ae..28bc6cb3 100644
--- a/lib/public/js/app.js
+++ b/lib/public/js/app.js
@@ -40,8 +40,29 @@ import { UpdateActionButton } from "./components/update-action-button.js";
import { GlobalRestartBanner } from "./components/global-restart-banner.js";
import { LoadingSpinner } from "./components/loading-spinner.js";
import { WatchdogTab } from "./components/watchdog-tab.js";
+import { FileViewer } from "./components/file-viewer.js";
+import { AppSidebar } from "./components/sidebar.js";
const html = htm.bind(h);
const kDefaultUiTab = "general";
+const kUiSettingsStorageKey = "alphaclawUiSettings";
+const kDefaultSidebarWidthPx = 220;
+const kSidebarMinWidthPx = 180;
+const kSidebarMaxWidthPx = 460;
+const kBrowseLastPathUiSettingKey = "browseLastPath";
+
+const clampSidebarWidth = (value) =>
+ Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
+
+const readUiSettings = () => {
+ try {
+ const raw = window.localStorage.getItem(kUiSettingsStorageKey);
+ if (!raw) return {};
+ const parsed = JSON.parse(raw);
+ return parsed && typeof parsed === "object" ? parsed : {};
+ } catch {
+ return {};
+ }
+};
const getHashPath = () => {
const hash = window.location.hash.replace(/^#/, "");
@@ -377,6 +398,7 @@ const GeneralTab = ({
};
const App = () => {
+ const appShellRef = useRef(null);
const [onboarded, setOnboarded] = useState(null);
const [location, setLocation] = useLocation();
const [acVersion, setAcVersion] = useState(null);
@@ -386,6 +408,19 @@ const App = () => {
const [acDismissed, setAcDismissed] = useState(false);
const [authEnabled, setAuthEnabled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
+ const [sidebarTab, setSidebarTab] = useState("menu");
+ const [sidebarWidthPx, setSidebarWidthPx] = useState(() => {
+ const settings = readUiSettings();
+ if (!Number.isFinite(settings.sidebarWidthPx)) return kDefaultSidebarWidthPx;
+ return clampSidebarWidth(settings.sidebarWidthPx);
+ });
+ const [lastBrowsePath, setLastBrowsePath] = useState(() => {
+ const settings = readUiSettings();
+ return typeof settings[kBrowseLastPathUiSettingKey] === "string"
+ ? settings[kBrowseLastPathUiSettingKey]
+ : "";
+ });
+ const [isResizingSidebar, setIsResizingSidebar] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
const [restartRequired, setRestartRequired] = useState(false);
@@ -568,10 +603,45 @@ const App = () => {
`;
}
+ const buildBrowseRoute = (relativePath) => {
+ const encodedPath = String(relativePath || "")
+ .split("/")
+ .filter(Boolean)
+ .map((segment) => encodeURIComponent(segment))
+ .join("/");
+ return encodedPath ? `/browse/${encodedPath}` : "/browse";
+ };
const navigateToSubScreen = (screen) => {
setLocation(`/${screen}`);
setMobileSidebarOpen(false);
};
+ const navigateToBrowseFile = (relativePath) => {
+ setLocation(buildBrowseRoute(relativePath));
+ setMobileSidebarOpen(false);
+ };
+ const handleSidebarLogout = async () => {
+ setMenuOpen(false);
+ await logout();
+ try {
+ window.localStorage.clear();
+ window.sessionStorage.clear();
+ } catch {}
+ window.location.href = "/login.html";
+ };
+ const handleSelectSidebarTab = (nextTab) => {
+ setSidebarTab(nextTab);
+ if (nextTab === "menu" && location.startsWith("/browse")) {
+ setLocation("/general");
+ return;
+ }
+ if (nextTab === "browse" && !location.startsWith("/browse")) {
+ setLocation(buildBrowseRoute(lastBrowsePath));
+ }
+ };
+ const handleSelectNavItem = (itemId) => {
+ setLocation(`/${itemId}`);
+ setMobileSidebarOpen(false);
+ };
const exitSubScreen = () => {
setLocation(`/${kDefaultUiTab}`);
setMobileSidebarOpen(false);
@@ -601,18 +671,91 @@ const App = () => {
},
];
- const selectedNavId =
- location === "/telegram"
- ? ""
- : location.startsWith("/providers")
- ? "providers"
- : location.startsWith("/watchdog")
- ? "watchdog"
- : location.startsWith("/envars")
- ? "envars"
- : location.startsWith("/webhooks")
- ? "webhooks"
- : "general";
+ const isBrowseRoute = location.startsWith("/browse");
+ const selectedBrowsePath = isBrowseRoute
+ ? location
+ .replace(/^\/browse\/?/, "")
+ .split("/")
+ .filter(Boolean)
+ .map((segment) => {
+ try {
+ return decodeURIComponent(segment);
+ } catch {
+ return segment;
+ }
+ })
+ .join("/")
+ : "";
+ const selectedNavId = isBrowseRoute
+ ? "browse"
+ : location === "/telegram"
+ ? ""
+ : location.startsWith("/providers")
+ ? "providers"
+ : location.startsWith("/watchdog")
+ ? "watchdog"
+ : location.startsWith("/envars")
+ ? "envars"
+ : location.startsWith("/webhooks")
+ ? "webhooks"
+ : "general";
+
+ useEffect(() => {
+ setSidebarTab((currentTab) => {
+ if (location.startsWith("/browse")) return "browse";
+ if (currentTab === "browse") return "menu";
+ return currentTab;
+ });
+ }, [location]);
+
+ useEffect(() => {
+ if (!isBrowseRoute) return;
+ if (!selectedBrowsePath) return;
+ setLastBrowsePath((currentPath) =>
+ currentPath === selectedBrowsePath ? currentPath : selectedBrowsePath,
+ );
+ }, [isBrowseRoute, selectedBrowsePath]);
+
+ useEffect(() => {
+ const settings = readUiSettings();
+ settings.sidebarWidthPx = sidebarWidthPx;
+ settings[kBrowseLastPathUiSettingKey] = lastBrowsePath;
+ try {
+ window.localStorage.setItem(kUiSettingsStorageKey, JSON.stringify(settings));
+ } catch {}
+ }, [sidebarWidthPx, lastBrowsePath]);
+
+ const resizeSidebarWithClientX = useCallback((clientX) => {
+ const shellElement = appShellRef.current;
+ if (!shellElement) return;
+ const shellBounds = shellElement.getBoundingClientRect();
+ const nextWidth = clampSidebarWidth(Math.round(clientX - shellBounds.left));
+ setSidebarWidthPx(nextWidth);
+ }, []);
+
+ const onSidebarResizerPointerDown = (event) => {
+ event.preventDefault();
+ setIsResizingSidebar(true);
+ resizeSidebarWithClientX(event.clientX);
+ };
+
+ useEffect(() => {
+ if (!isResizingSidebar) return () => {};
+ const onPointerMove = (event) => resizeSidebarWithClientX(event.clientX);
+ const onPointerUp = () => setIsResizingSidebar(false);
+ window.addEventListener("pointermove", onPointerMove);
+ window.addEventListener("pointerup", onPointerUp);
+ const previousUserSelect = document.body.style.userSelect;
+ const previousCursor = document.body.style.cursor;
+ document.body.style.userSelect = "none";
+ document.body.style.cursor = "col-resize";
+ return () => {
+ window.removeEventListener("pointermove", onPointerMove);
+ window.removeEventListener("pointerup", onPointerUp);
+ document.body.style.userSelect = previousUserSelect;
+ document.body.style.cursor = previousCursor;
+ };
+ }, [isResizingSidebar, resizeSidebarWithClientX]);
const renderWebhooks = (hookName = "") => html`
@@ -626,91 +769,53 @@ const App = () => {
`;
return html`
-
+
<${GlobalRestartBanner}
visible=${restartRequired}
restarting=${restartingGateway}
onRestart=${handleGatewayRestart}
/>
-
+ <${AppSidebar}
+ mobileSidebarOpen=${mobileSidebarOpen}
+ authEnabled=${authEnabled}
+ menuRef=${menuRef}
+ menuOpen=${menuOpen}
+ onToggleMenu=${() => setMenuOpen((open) => !open)}
+ onLogout=${handleSidebarLogout}
+ sidebarTab=${sidebarTab}
+ onSelectSidebarTab=${handleSelectSidebarTab}
+ navSections=${kNavSections}
+ selectedNavId=${selectedNavId}
+ onSelectNavItem=${handleSelectNavItem}
+ selectedBrowsePath=${selectedBrowsePath}
+ onSelectBrowseFile=${navigateToBrowseFile}
+ acHasUpdate=${acHasUpdate}
+ acLatest=${acLatest}
+ acDismissed=${acDismissed}
+ acUpdating=${acUpdating}
+ onAcUpdate=${handleAcUpdate}
+ />
+