Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/clients/openclaw-live-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { open, readdir, readFile } from "node:fs/promises";
import { open, readdir, readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import type {
Expand Down Expand Up @@ -478,6 +478,12 @@ async function readSessionHistoryFile(
): Promise<SessionHistoryFileReadResult> {
const targetLineCount = Math.max(limit * SESSION_HISTORY_TAIL_LINE_MULTIPLIER, SESSION_HISTORY_TAIL_MIN_LINES);
try {
// Directories (or other non-files) can look readable but produce empty/noisy output.
// Treat them as errors so we fall back to bounded CLI recovery.
const entry = await stat(sessionFile);
if (!entry.isFile()) {
throw new Error("session history path is not a file");
}
const raw = await readRecentSessionHistoryChunk(sessionFile, targetLineCount);
return {
response: normalizeSessionHistoryChunk(raw, limit),
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/digest-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ export function renderLatestDigestPage(digest: LatestDigest): string {
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Digest Latest</title>
<style>
body { font-family: "SF Mono", Menlo, monospace; background: #0b1016; color: #d6e7f9; margin: 0; padding: 16px; }
.meta { color: #93aac2; font-size: 12px; }
a { color: #7dd3fc; }
.card { margin-top: 10px; border: 1px solid #27405a; border-radius: 8px; padding: 12px; background: #111923; }
@media (max-width: 740px) { body { padding: 12px; } }
</style>
</head>
<body>
Expand All @@ -66,6 +68,7 @@ export function renderLatestDigestPage(digest: LatestDigest): string {
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Digest ${escapeHtml(digest.date ?? "latest")}</title>
<style>
body { font-family: "SF Mono", Menlo, monospace; background: #0b1016; color: #d6e7f9; margin: 0; padding: 16px; }
Expand All @@ -77,6 +80,7 @@ export function renderLatestDigestPage(digest: LatestDigest): string {
p { margin: 8px 0; }
ul { margin: 8px 0 8px 18px; padding: 0; }
code { color: #9bd5ff; }
@media (max-width: 740px) { body { padding: 12px; } }
</style>
</head>
<body>
Expand Down
104 changes: 102 additions & 2 deletions src/ui/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ export function startUiServer(port: number, toolClient: ToolClient): Server {
).join("");
const docsHref = buildHomeHref({ quick: "all" }, true, "docs", language);
const homeHref = buildHomeHref({ quick: "all" }, true, "overview", language);
const html = `<!doctype html><html><head><meta charset="utf-8" /><title>${escapeHtml(t("OpenClaw Control Center Docs", "OpenClaw Control Center 文档"))}</title></head><body><h1>${escapeHtml(t("OpenClaw Control Center Docs", "OpenClaw Control Center 文档"))}</h1><ul>${links}</ul><p><a href="${escapeHtml(docsHref)}">${escapeHtml(t("Open document workbench", "打开文档工作台"))}</a> · <a href="${escapeHtml(homeHref)}">${escapeHtml(t("Back to control center", "返回控制中心"))}</a></p></body></html>`;
const html = `<!doctype html><html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /><title>${escapeHtml(t("OpenClaw Control Center Docs", "OpenClaw Control Center 文档"))}</title></head><body><h1>${escapeHtml(t("OpenClaw Control Center Docs", "OpenClaw Control Center 文档"))}</h1><ul>${links}</ul><p><a href="${escapeHtml(docsHref)}">${escapeHtml(t("Open document workbench", "打开文档工作台"))}</a> · <a href="${escapeHtml(homeHref)}">${escapeHtml(t("Back to control center", "返回控制中心"))}</a></p></body></html>`;
return writeText(res, 200, html, "text/html; charset=utf-8");
}

Expand Down Expand Up @@ -6746,6 +6746,7 @@ async function renderHtml(
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>OpenClaw Control Center</title>
<script>
(() => {
Expand Down Expand Up @@ -6846,6 +6847,71 @@ async function renderHtml(
}
})();
</script>
<script>
(() => {
const MOBILE_BREAKPOINT_PX = 1080;
const navKey = 'openclaw:nav-open';

const isMobile = () => {
try {
return window.matchMedia && window.matchMedia('(max-width: ' + MOBILE_BREAKPOINT_PX + 'px)').matches;
} catch {
return false;
}
};

const readStoredOpen = () => {
try {
return window.sessionStorage.getItem(navKey) === '1';
} catch {
return false;
}
};

const setOpen = (open) => {
document.documentElement.dataset.navOpen = open ? '1' : '0';
try {
window.sessionStorage.setItem(navKey, open ? '1' : '0');
} catch {}
const btn = document.getElementById('nav-toggle');
if (btn) {
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
}
};

const boot = () => {
if (!isMobile()) {
document.documentElement.dataset.navOpen = '1';
return;
}
setOpen(readStoredOpen());
const btn = document.getElementById('nav-toggle');
if (btn) {
btn.addEventListener('click', () => {
const open = document.documentElement.dataset.navOpen === '1';
setOpen(!open);
});
}
document.addEventListener(
'click',
(evt) => {
const target = evt.target;
if (!target || !(target instanceof Element)) return;
if (target.closest('#nav-toggle')) return;
if (target.closest('#nav-links')) return;
if (document.documentElement.dataset.navOpen === '1') setOpen(false);
},
{ capture: true },
);
};

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot, { once: true });
} else {
boot();
}
})();
</script>
<style>
:root {
--bg: #eef2f6;
Expand Down Expand Up @@ -7156,6 +7222,21 @@ async function renderHtml(
.brand .meta { margin-top: 6px; }
.meta { color: var(--muted); font-size: 13px; line-height: 1.62; }
.meta-inline { color: var(--muted); font-size: 12px; margin-left: 6px; }
.nav-toggle {
display: none;
width: 100%;
margin-top: 12px;
border: 1px solid rgba(17, 24, 39, 0.08);
border-radius: 16px;
padding: 12px 13px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(251, 253, 255, 0.86));
color: var(--text);
font-weight: 680;
text-align: left;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
}
.nav-toggle:focus-visible { outline: none; box-shadow: var(--ring-soft); }
.nav-links { margin-top: 14px; display: grid; gap: 9px; }
.nav-link {
display: block;
Expand Down Expand Up @@ -9921,6 +10002,14 @@ async function renderHtml(
.file-editor-head { flex-direction: column; }
.file-editor-panel { grid-template-rows: auto minmax(280px, 1fr) auto; }
.file-editor-textarea { min-height: 360px; }
/* Mobile nav: collapse section links into a menu */
.nav-toggle { display: block; }
.nav-links { display: none; }
html[data-nav-open="1"] .nav-links { display: grid; }
html[data-nav-open="1"] .nav-links { animation: panel-in 220ms ease both; }
/* Mobile: status-strip cards should stack one per row */
.status-strip,
.status-strip.compact { grid-template-columns: 1fr !important; }
}
@media (prefers-reduced-motion: reduce) {
* {
Expand Down Expand Up @@ -9956,7 +10045,10 @@ async function renderHtml(
<div class="meta">${escapeHtml(t("Updated", "更新时间"))}${escapeHtml(options.language === "en" ? ": " : ":")}${escapeHtml(snapshot.generatedAt ?? t("Not available", "暂无"))}</div>
${languageToggle}
</div>
<nav class="nav-links">${sectionNav}</nav>
<button id="nav-toggle" type="button" class="nav-toggle" aria-expanded="false" aria-controls="nav-links">
${escapeHtml(t("Menu", "菜单"))}
</button>
<nav id="nav-links" class="nav-links">${sectionNav}</nav>
</aside>
<main class="panel">
<header class="section-hero-head">
Expand Down Expand Up @@ -17094,6 +17186,7 @@ function renderSessionDrilldownPage(
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>${escapeHtml(t("OpenClaw Control Center Session Drilldown", "OpenClaw 控制中心会话详情"))}</title>
<style>
body { font-family: "SF Mono", Menlo, monospace; background: #0b1016; color: #d6e7f9; padding: 16px; margin: 0; }
Expand All @@ -17110,6 +17203,7 @@ function renderSessionDrilldownPage(
.badge.idle, .badge.todo, .badge.message, .badge.tool_event { color: #9ca3af; border-color: #9ca3af; }
.badge.accepted { color: #22c55e; border-color: #22c55e; }
.cell-content { white-space: pre-wrap; word-break: break-word; max-width: 760px; }
@media (max-width: 740px) { body { padding: 12px; } }
</style>
</head>
<body>
Expand Down Expand Up @@ -17185,6 +17279,7 @@ function renderAuditPage(timeline: AuditTimelineSnapshot, severity: AuditSeverit
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>OpenClaw Control Center Audit Timeline</title>
<style>
body { font-family: "SF Mono", Menlo, monospace; background: #0b1016; color: #d6e7f9; padding: 16px; margin: 0; }
Expand All @@ -17201,6 +17296,7 @@ function renderAuditPage(timeline: AuditTimelineSnapshot, severity: AuditSeverit
.badge.error { color: #f43f5e; border-color: #f43f5e; }
label { color: #93aac2; font-size: 12px; margin-right: 8px; }
select, button { background: #09141f; color: #d6e7f9; border: 1px solid #27405a; border-radius: 6px; padding: 6px; font-size: 12px; }
@media (max-width: 740px) { body { padding: 12px; } }
</style>
</head>
<body>
Expand Down Expand Up @@ -17259,6 +17355,7 @@ function renderTaskDetailPage(input: {
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>${escapeHtml(t("Task detail", "任务详情"))} · ${escapeHtml(task.taskId)}</title>
<style>
body { font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; margin:0; padding:20px; background:#f5f5f7; color:#1d1d1f; }
Expand All @@ -17271,6 +17368,7 @@ function renderTaskDetailPage(input: {
a { color:#0068d3; }
code { font-size:12px; color:#005ab6; }
.story-list { margin:0; padding-left:18px; display:grid; gap:8px; }
@media (max-width: 740px) { body { padding: 12px; } h1 { font-size: 24px; } }
</style>
</head>
<body>
Expand Down Expand Up @@ -17336,6 +17434,7 @@ function renderCronJobDetailPage(
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>${escapeHtml(pickUiText(language, "Cron detail", "Cron 详情"))} · ${escapeHtml(job.jobId)}</title>
<style>
body { font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; margin:0; padding:20px; background:#f5f5f7; color:#1d1d1f; }
Expand All @@ -17347,6 +17446,7 @@ function renderCronJobDetailPage(
.badge { display:inline-block; border-radius:999px; padding:3px 10px; font-size:12px; border:1px solid rgba(17,24,39,0.14); background:#f7f8fb; }
a { color:#0068d3; }
code { font-size:12px; color:#005ab6; }
@media (max-width: 740px) { body { padding: 12px; } h1 { font-size: 24px; } }
</style>
</head>
<body>
Expand Down
41 changes: 20 additions & 21 deletions test/oss-readiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import os from "node:os";
import test from "node:test";

const ROOT = process.cwd();
const TSX_BIN = path.join(ROOT, "node_modules", ".bin", "tsx");
const TSX_BIN =
process.platform === "win32"
? path.join(ROOT, "node_modules", ".bin", "tsx.cmd")
: path.join(ROOT, "node_modules", ".bin", "tsx");
const MACOS_HOME_PATH_PATTERN = /\/Users\/[^/\s]+\//;
const EMBEDDED_LOCAL_TOKEN_PATTERN = /LOCAL_API_TOKEN\s*[:=]\s*["'][^"']{8,}["']/;
const PUBLIC_FILES = [
Expand Down Expand Up @@ -71,16 +74,14 @@ test("config loads LOCAL_API_TOKEN from cwd .env when env is not preloaded", ()
const tempDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-config-env-"));
try {
writeFileSync(path.join(tempDir, ".env"), "LOCAL_API_TOKEN=from-dotenv\n", "utf8");
const output = execFileSync(
TSX_BIN,
[
"--eval",
`delete process.env.LOCAL_API_TOKEN; const mod = require(${JSON.stringify(path.join(ROOT, "src", "config.ts"))}); process.stdout.write(mod.LOCAL_API_TOKEN);`,
],
{
cwd: tempDir,
encoding: "utf8",
},
const args = [
"--eval",
`delete process.env.LOCAL_API_TOKEN; const mod = require(${JSON.stringify(path.join(ROOT, "src", "config.ts"))}); process.stdout.write(mod.LOCAL_API_TOKEN);`,
];
const output = (
process.platform === "win32"
? execFileSync("cmd.exe", ["/c", TSX_BIN, ...args], { cwd: tempDir, encoding: "utf8" })
: execFileSync(TSX_BIN, args, { cwd: tempDir, encoding: "utf8" })
).trim();

assert.equal(output, "from-dotenv");
Expand All @@ -92,16 +93,14 @@ test("config loads LOCAL_API_TOKEN from cwd .env when env is not preloaded", ()
test("config keeps defaults when cwd .env is absent", () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-config-no-env-"));
try {
const output = execFileSync(
TSX_BIN,
[
"--eval",
`delete process.env.LOCAL_API_TOKEN; delete process.env.GATEWAY_URL; const mod = require(${JSON.stringify(path.join(ROOT, "src", "config.ts"))}); process.stdout.write(JSON.stringify({ token: mod.LOCAL_API_TOKEN, gateway: mod.GATEWAY_URL }));`,
],
{
cwd: tempDir,
encoding: "utf8",
},
const args = [
"--eval",
`delete process.env.LOCAL_API_TOKEN; delete process.env.GATEWAY_URL; const mod = require(${JSON.stringify(path.join(ROOT, "src", "config.ts"))}); process.stdout.write(JSON.stringify({ token: mod.LOCAL_API_TOKEN, gateway: mod.GATEWAY_URL }));`,
];
const output = (
process.platform === "win32"
? execFileSync("cmd.exe", ["/c", TSX_BIN, ...args], { cwd: tempDir, encoding: "utf8" })
: execFileSync(TSX_BIN, args, { cwd: tempDir, encoding: "utf8" })
).trim();

assert.deepEqual(JSON.parse(output), {
Expand Down
54 changes: 34 additions & 20 deletions test/ui-render-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ test("usage dashboard includes token type share and cron token share sections",
});

test("dashboard wires CLI insight cards into overview, usage, memory, and settings", async () => {
const source = await readFile("src/ui/server.ts", "utf8");
const source = (await readFile("src/ui/server.ts", "utf8")).replace(/\r\n/g, "\n");
assert(source.includes('id="overview-connection-health"'));
assert(source.includes('pickUiText(language, "Connection health", "接线状态")'));
assert(source.includes('pickUiText(language, "Gateway", "网关")'));
Expand Down Expand Up @@ -568,32 +568,46 @@ test("editable agent scopes follow configured agents before workspace folders",
resolveEditableAgentScopesWithFallbackForSmoke,
} = await import("../src/ui/server");

const normalizePath = (value: string): string => value.replace(/\r\n/g, "\n").replace(/\\/g, "/");
const stripWindowsDrivePrefix = (value: string): string => value.replace(/^[A-Za-z]:/, "");
assert.equal(
resolveOpenClawWorkspaceRootForSmoke({
openclawHomeDir: "/home/test/.openclaw",
configPath: "/home/test/.openclaw/openclaw.json",
configText: JSON.stringify({
agents: {
list: [
{ id: "main" },
{ id: "pandas", workspace: "/srv/openclaw/workspace/agents/pandas" },
],
},
}),
}),
stripWindowsDrivePrefix(
normalizePath(
resolveOpenClawWorkspaceRootForSmoke({
openclawHomeDir: "/home/test/.openclaw",
configPath: "/home/test/.openclaw/openclaw.json",
configText: JSON.stringify({
agents: {
list: [
{ id: "main" },
{ id: "pandas", workspace: "/srv/openclaw/workspace/agents/pandas" },
],
},
}),
}),
),
),
"/srv/openclaw/workspace",
);
assert.equal(
resolveOpenClawWorkspaceRootForSmoke({
explicitWorkspaceRoot: "/data/openclaw/workspace",
openclawHomeDir: "/home/test/.openclaw",
}),
stripWindowsDrivePrefix(
normalizePath(
resolveOpenClawWorkspaceRootForSmoke({
explicitWorkspaceRoot: "/data/openclaw/workspace",
openclawHomeDir: "/home/test/.openclaw",
}),
),
),
"/data/openclaw/workspace",
);
assert.equal(
resolveOpenClawWorkspaceRootForSmoke({
openclawHomeDir: "/home/test/.openclaw",
}),
stripWindowsDrivePrefix(
normalizePath(
resolveOpenClawWorkspaceRootForSmoke({
openclawHomeDir: "/home/test/.openclaw",
}),
),
),
"/home/test/.openclaw/workspace",
);

Expand Down