fix(desktop): external gateway mode and start script alignment

- Add desktop env for non-embedded gateway (OCLAW_DESKTOP_EMBED_BACKEND, base URL, skip channel).
- Wire start_all/start_desktop to pass gateway URL when keeping existing gateway; fix root start_all flags.
- Prefer electron.cmd for background desktop launch; admin static theme presets and chat boot timeouts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-06 11:06:16 +08:00
parent 19e51e2c12
commit 16169b3de2
12 changed files with 566 additions and 45 deletions

View file

@ -96,6 +96,47 @@ function pickPort(preferredPort) {
});
}
function envFlagOff(v) {
return ["0", "false", "no", "off"].includes(String(v ?? "").trim().toLowerCase());
}
/** When false, connect to OCLAW_DESKTOP_GATEWAY_BASE_URL (or default :8787) instead of spawning a child gateway. */
function shouldEmbedBackend() {
if (envFlagOff(process.env.OCLAW_DESKTOP_EMBED_BACKEND)) return false;
return !String(process.env.OCLAW_DESKTOP_GATEWAY_BASE_URL || "").trim();
}
function normalizeGatewayBaseUrl(raw) {
const s = String(raw || "").trim();
if (!s) throw new Error("gateway base URL is empty");
const u = new URL(s);
const host = u.hostname || DEFAULT_HOST;
const port = u.port ? parseInt(u.port, 10) : u.protocol === "https:" ? 443 : 80;
const baseUrl = `${u.protocol}//${u.host}`;
return { host, port, baseUrl };
}
async function attachToExistingGateway() {
const raw =
String(process.env.OCLAW_DESKTOP_GATEWAY_BASE_URL || "").trim() ||
`http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
const parsed = normalizeGatewayBaseUrl(raw);
runtimeState = {
host: parsed.host,
port: parsed.port,
baseUrl: parsed.baseUrl,
pythonBin: resolvePythonBin(),
externalAttach: true,
};
const deadlineAtMs = Date.now() + STARTUP_TIMEOUT_MS;
await waitForBackend(`${runtimeState.baseUrl}/health`, deadlineAtMs);
return runtimeState;
}
function shouldSkipDesktopChannel() {
return ["1", "true", "yes", "on"].includes(String(process.env.OCLAW_DESKTOP_SKIP_CHANNEL || "").trim().toLowerCase());
}
function waitForBackend(url, deadlineAtMs) {
return new Promise((resolve, reject) => {
const attempt = () => {
@ -126,6 +167,22 @@ function waitForBackend(url, deadlineAtMs) {
});
}
/** Avoid hanging forever on session.clearStorageData / clearCache (seen on some Windows setups). */
function awaitWithTimeout(promise, timeoutMs, logLabel) {
const ms = Math.max(200, Number(timeoutMs) || 8000);
return Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => {
try {
logDesktop(`${logLabel}:timeout_after_${ms}ms`);
} catch (_) {}
resolve();
}, ms);
}),
]);
}
function loadingHtml(text) {
const safe = String(text || "").replace(/</g, "&lt;").replace(/>/g, "&gt;");
return `<!doctype html>
@ -190,7 +247,7 @@ async function startBackendProcess() {
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
});
runtimeState = { host, port, baseUrl, pythonBin };
runtimeState = { host, port, baseUrl, pythonBin, externalAttach: false };
backendProc.stdout.on("data", (chunk) => {
logStream.write(chunk);
@ -513,8 +570,13 @@ async function boot() {
app.setName(APP_DISPLAY_NAME);
createMainWindow();
// Show window immediately to avoid "black screen" during backend startup.
const embedGateway = shouldEmbedBackend();
try {
await mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(loadingHtml("Starting local gateway…"))}`);
await mainWindow.loadURL(
`data:text/html;charset=utf-8,${encodeURIComponent(
loadingHtml(embedGateway ? "Starting local gateway…" : "Connecting to existing gateway…"),
)}`,
);
} catch (_) {}
mainWindow.show();
try {
@ -546,8 +608,13 @@ async function boot() {
}
})();
if (embedGateway) {
await startBackendProcess();
logDesktop(`boot:backendReady:${Date.now() - t0}ms`);
logDesktop(`boot:backendReady:embedded:${Date.now() - t0}ms`);
} else {
await attachToExistingGateway();
logDesktop(`boot:backendReady:external:${Date.now() - t0}ms`);
}
try {
await mainWindow.loadURL(
@ -555,7 +622,11 @@ async function boot() {
);
} catch (_) {}
const channelOk = await startChannelProcess();
let channelOk = true;
if (shouldSkipDesktopChannel()) {
logDesktop(`boot:channel:skipped:${Date.now() - t0}ms`);
} else {
channelOk = await startChannelProcess();
logDesktop(`boot:channel:${channelOk ? "ok" : "fail"}:${Date.now() - t0}ms`);
if (!channelOk && !channelStartWarningShown) {
channelStartWarningShown = true;
@ -566,8 +637,18 @@ async function boot() {
detail: "你可以在 Admin -> 运行时中查看并重试服务。",
});
}
await clearStoragePromise;
await clearCachePromise;
}
try {
await mainWindow.loadURL(
`data:text/html;charset=utf-8,${encodeURIComponent(
loadingHtml(
`Backend ready at ${runtimeState.baseUrl}\nClearing login/session storage…\nThen opening chat…`,
),
)}`,
);
} catch (_) {}
// Do not block the window forever if Electron's clearStorageData/clearCache never settle.
await awaitWithTimeout(Promise.all([clearStoragePromise, clearCachePromise]), 12000, "boot:storageCleanup");
// Desktop policy: always require fresh login on every app restart.
logDesktop(`boot:loadChat:start:${Date.now() - t0}ms`);
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
@ -593,6 +674,28 @@ ipcMain.handle("desktop:getRuntimeInfo", async () => {
});
ipcMain.handle("desktop:restartBackend", async () => {
if (runtimeState && runtimeState.externalAttach) {
if (mainWindow && !mainWindow.isDestroyed()) {
try {
await mainWindow.loadURL(
`data:text/html;charset=utf-8,${encodeURIComponent(loadingHtml("Reloading chat (external gateway)…"))}`,
);
mainWindow.show();
} catch (_) {}
try {
await waitForBackend(`${runtimeState.baseUrl}/health`, Date.now() + 15000);
} catch (_) {}
try {
await awaitWithTimeout(mainWindow.webContents.session.clearCache(), 8000, "restartBackend:clearCache");
} catch (_) {}
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
try {
mainWindow.show();
mainWindow.focus();
} catch (_) {}
}
return true;
}
if (mainWindow && !mainWindow.isDestroyed()) {
try {
await mainWindow.loadURL(
@ -607,10 +710,12 @@ ipcMain.handle("desktop:restartBackend", async () => {
await stopChannelProcess();
await stopBackendProcess();
await startBackendProcess();
if (!shouldSkipDesktopChannel()) {
await startChannelProcess();
}
if (mainWindow && !mainWindow.isDestroyed()) {
try {
await mainWindow.webContents.session.clearCache();
await awaitWithTimeout(mainWindow.webContents.session.clearCache(), 8000, "restartBackend:clearCache");
} catch (_) {}
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
try {

View file

@ -0,0 +1,51 @@
/* Admin + standalone /chat: restore palette from localStorage before first paint. */
(function () {
const STORAGE_KEY = "oclaw_admin_theme";
const THEMES = ["deepseek", "github", "nord", "dracula", "forest", "catppuccin", "light"];
const LEGIT = new Set(THEMES);
function applyAdminTheme(theme) {
const t = theme && LEGIT.has(theme) ? theme : "deepseek";
if (t === "deepseek") document.body.removeAttribute("data-admin-theme");
else document.body.setAttribute("data-admin-theme", t);
}
function initAdminThemeFromStorage() {
let raw = "deepseek";
try {
raw = localStorage.getItem(STORAGE_KEY) || "deepseek";
} catch (_) {
raw = "deepseek";
}
if (!LEGIT.has(raw)) raw = "deepseek";
applyAdminTheme(raw);
return raw;
}
function persistAdminTheme(theme) {
const t = theme && LEGIT.has(theme) ? theme : "deepseek";
try {
localStorage.setItem(STORAGE_KEY, t);
} catch (_) {}
applyAdminTheme(t);
}
function currentAdminTheme() {
try {
const v = localStorage.getItem(STORAGE_KEY);
return v && LEGIT.has(v) ? v : "deepseek";
} catch (_) {
return "deepseek";
}
}
window.OclawAdminTheme = {
STORAGE_KEY,
THEMES,
applyAdminTheme,
initAdminThemeFromStorage,
persistAdminTheme,
currentAdminTheme,
};
initAdminThemeFromStorage();
})();

View file

@ -232,6 +232,14 @@ const I18N = {
"common.notFound": "未找到页面",
"common.forbidden": "无权访问该页面。",
"common.error": "错误",
"theme.label": "界面配色",
"theme.deepseek": "DeepSeek 默认",
"theme.github": "GitHub 暗色",
"theme.nord": "Nord",
"theme.dracula": "Dracula",
"theme.forest": "森绿",
"theme.catppuccin": "Catppuccin Mocha",
"theme.light": "浅色",
"lang.switch": "English",
"memory.title": "记忆治理",
"memory.hits": "最近命中",
@ -665,6 +673,14 @@ const I18N = {
"common.notFound": "Not found",
"common.forbidden": "You do not have access to this page.",
"common.error": "Error",
"theme.label": "Color theme",
"theme.deepseek": "DeepSeek (default)",
"theme.github": "GitHub Dark",
"theme.nord": "Nord",
"theme.dracula": "Dracula",
"theme.forest": "Forest",
"theme.catppuccin": "Catppuccin Mocha",
"theme.light": "Light",
"lang.switch": "中文",
"memory.title": "Memory Governance",
"memory.hits": "Recent Hits",
@ -997,6 +1013,12 @@ function applyI18nStatic() {
});
const langBtn = document.getElementById("btnLang");
if (langBtn) langBtn.textContent = t("lang.switch");
const themeSel = document.getElementById("adminThemeSelect");
if (themeSel && window.OclawAdminTheme) {
try {
themeSel.value = OclawAdminTheme.currentAdminTheme();
} catch (_) {}
}
}
function toggleLang() {
@ -9164,6 +9186,14 @@ if (btnLangEl) btnLangEl.addEventListener("click", () => {
toggleLang();
router();
});
const adminThemeSelectEl = document.getElementById("adminThemeSelect");
if (adminThemeSelectEl && window.OclawAdminTheme) {
adminThemeSelectEl.addEventListener("change", () => {
try {
window.OclawAdminTheme.persistAdminTheme(adminThemeSelectEl.value);
} catch (_) {}
});
}
const btnBackChatEl = document.getElementById("btnBackChat");
if (btnBackChatEl) {
try {

View file

@ -7,6 +7,7 @@
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
<link rel="stylesheet" href="/admin/assets/styles.css" />
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css" />
<link rel="stylesheet" href="/admin/assets/theme-presets.css" />
<style>
body.theme-ds-body.chat-standalone-page {
margin: 0;
@ -561,9 +562,10 @@
onload="(function(){try{ if (window.__oclawHydrateMermaidAll) window.__oclawHydrateMermaidAll(); }catch(e){}})()"
></script>
<!-- Cache-bust for desktop webview: avoid stale chat.js -->
<script defer src="/admin/assets/admin-theme.js"></script>
<script
defer
src="/admin/assets/chat.js?v=20260501-1"
src="/admin/assets/chat.js?v=20260507-2"
onerror="(function(){var s=document.getElementById('chat-boot-splash');if(s){s.querySelector('.chat-boot-splash__title span:last-child').textContent='无法加载 chat.js';s.querySelector('.chat-boot-splash__muted').textContent='请确认网关已启动且 /admin/assets/chat.js 可访问。';}})()"
></script>
</head>

View file

@ -66,6 +66,14 @@ const I18N = {
"auth.chatLoginDenied": "当前账号无法使用对话(请联系管理员)。",
"auth.disabled": "账号已被禁用,请联系管理员",
"common.error": "错误",
"theme.label": "界面配色",
"theme.deepseek": "DeepSeek 默认",
"theme.github": "GitHub 暗色",
"theme.nord": "Nord",
"theme.dracula": "Dracula",
"theme.forest": "森绿",
"theme.catppuccin": "Catppuccin Mocha",
"theme.light": "浅色",
"lang.switch": "English",
"chat.imageViewerClose": "关闭",
"chat.imageViewerHint": "点击查看大图,空白处或 Esc 关闭",
@ -234,6 +242,14 @@ const I18N = {
"auth.chatLoginDenied": "This account cannot use chat (contact an admin).",
"auth.disabled": "Account is disabled",
"common.error": "Error",
"theme.label": "Color theme",
"theme.deepseek": "DeepSeek (default)",
"theme.github": "GitHub Dark",
"theme.nord": "Nord",
"theme.dracula": "Dracula",
"theme.forest": "Forest",
"theme.catppuccin": "Catppuccin Mocha",
"theme.light": "Light",
"lang.switch": "中文",
"chat.imageViewerClose": "Close",
"chat.imageViewerHint": "Click image to enlarge; click outside or Esc to close",
@ -2372,6 +2388,26 @@ function syncAuthUserLabel() {
text: t("chat.myProfile"),
}),
];
items.push(el("div", { class: "chat-sess-menu-sep" }));
items.push(
el("div", { class: "muted", style: "padding:6px 10px 2px;font-size:12px;", text: t("theme.label") }),
);
const themeSelMenu = el("select", {
class: "input",
style: "width:100%;margin:4px 8px 8px;max-width:calc(100% - 16px);",
});
try {
(window.OclawAdminTheme && window.OclawAdminTheme.THEMES ? window.OclawAdminTheme.THEMES : ["deepseek"]).forEach((tid) => {
themeSelMenu.appendChild(el("option", { value: tid, text: t(`theme.${tid}`) }));
});
themeSelMenu.value = window.OclawAdminTheme ? window.OclawAdminTheme.currentAdminTheme() : "deepseek";
} catch (_) {}
themeSelMenu.addEventListener("change", () => {
try {
if (window.OclawAdminTheme) window.OclawAdminTheme.persistAdminTheme(themeSelMenu.value);
} catch (_) {}
});
items.push(themeSelMenu);
const bridge = window.__chatUserMenuPrefs;
if (bridge && typeof bridge === "object") {
items.push(el("div", { class: "chat-sess-menu-sep" }));

View file

@ -7,6 +7,7 @@
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
<link rel="stylesheet" href="/admin/assets/styles.css" />
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css" />
<link rel="stylesheet" href="/admin/assets/theme-presets.css" />
<style>
/* First paint before app.js runs: keep admin from showing a blank dark screen. */
.admin-boot-splash {
@ -47,6 +48,7 @@
</style>
</head>
<body class="theme-ds-body">
<script src="/admin/assets/admin-theme.js"></script>
<div class="layout">
<aside class="sidebar">
<div class="brand">
@ -101,6 +103,18 @@
<div id="topTitle" class="topbar__title">Runtime</div>
<div class="topbar__actions">
<span id="authUser" class="muted"></span>
<label class="admin-theme-wrap" style="display: inline-flex; align-items: center; gap: 6px; margin-right: 4px">
<span class="muted" style="font-size: 12px" data-i18n="theme.label">配色</span>
<select id="adminThemeSelect" class="input input--compact" style="min-width: 132px; max-width: 200px; padding: 6px 8px" aria-label="theme">
<option value="deepseek" data-i18n="theme.deepseek">DeepSeek</option>
<option value="github" data-i18n="theme.github">GitHub</option>
<option value="nord" data-i18n="theme.nord">Nord</option>
<option value="dracula" data-i18n="theme.dracula">Dracula</option>
<option value="forest" data-i18n="theme.forest">Forest</option>
<option value="catppuccin" data-i18n="theme.catppuccin">Catppuccin</option>
<option value="light" data-i18n="theme.light">Light</option>
</select>
</label>
<a id="btnBackChat" class="btn" data-i18n="nav.chat" href="chat">对话</a>
<button id="btnLogout" class="btn" data-i18n="auth.logout">退出登录</button>
<button id="btnLang" class="btn">中文</button>
@ -120,7 +134,7 @@
</div>
<script
defer
src="/admin/assets/app.js?v=20260425-1"
src="/admin/assets/app.js?v=20260507-2"
onerror="(function(){var s=document.getElementById('admin-boot-splash');if(s){s.querySelector('.admin-boot-splash__title span:last-child').textContent='无法加载 app.js';s.querySelector('.admin-boot-splash__muted').textContent='请确认 /admin/assets/app.js 可访问。';}})()"
></script>
</body>

View file

@ -11,6 +11,17 @@ body.theme-ds-body {
--ds-text-muted: rgba(232, 232, 232, 0.55);
--ds-accent: #5eb3ff;
--ds-accent-soft: rgba(94, 179, 255, 0.18);
--ds-sidebar: #161618;
--ds-topbar-bg: rgba(22, 22, 24, 0.92);
--ds-accent-border: rgba(94, 179, 255, 0.25);
--ds-accent-border-strong: rgba(94, 179, 255, 0.45);
--ds-accent-link: rgba(94, 179, 255, 0.95);
--ds-accent-scrollbar: rgba(94, 179, 255, 0.45);
--ds-accent-scrollbar-hover: rgba(94, 179, 255, 0.65);
--ds-panel: #222224;
--ds-chat-scroll-track: #2e2e2e;
--ds-chat-scroll-thumb: #7a7a7a;
--ds-chat-scroll-thumb-hover: #909090;
/* 左侧会话列表:贴近侧栏 #0d0d0d,选中仅略提亮 */
--chat-sess-hover-bg: rgba(255, 255, 255, 0.05);
--chat-sess-active-bg: rgba(255, 255, 255, 0.062);
@ -21,7 +32,7 @@ body.theme-ds-body {
}
body.theme-ds-body .layout .sidebar {
background: #161618;
background: var(--ds-sidebar);
border-right-color: var(--ds-border);
}
@ -36,7 +47,7 @@ body.theme-ds-body .nav__item:hover {
body.theme-ds-body .nav__item--active {
background: var(--ds-accent-soft);
border: 1px solid rgba(94, 179, 255, 0.25);
border: 1px solid var(--ds-accent-border);
color: var(--ds-text);
}
@ -49,7 +60,7 @@ body.theme-ds-body .main {
}
body.theme-ds-body .topbar {
background: rgba(22, 22, 24, 0.92);
background: var(--ds-topbar-bg);
border-bottom-color: var(--ds-border);
}
@ -78,7 +89,7 @@ body.theme-ds-body .btn:hover {
body.theme-ds-body .btn--primary {
background: var(--ds-accent-soft);
border-color: rgba(94, 179, 255, 0.45);
border-color: var(--ds-accent-border-strong);
color: var(--ds-text);
}
@ -276,7 +287,7 @@ body.theme-ds-body .card {
.chat-nav__link {
display: inline-block;
font-size: 13px;
color: rgba(94, 179, 255, 0.95);
color: var(--ds-accent-link);
text-decoration: none;
}
@ -379,7 +390,7 @@ body.theme-ds-body .card {
.chat-pending-files:hover,
.table-wrap:hover,
.session-monitor-modal__card:hover {
scrollbar-color: rgba(94, 179, 255, 0.45) rgba(255, 255, 255, 0.06);
scrollbar-color: var(--ds-accent-scrollbar) rgba(255, 255, 255, 0.06);
}
.chat-msg__reasoning-pre::-webkit-scrollbar,
@ -425,14 +436,14 @@ body.theme-ds-body .card {
.chat-pending-files:hover::-webkit-scrollbar-thumb,
.table-wrap:hover::-webkit-scrollbar-thumb,
.session-monitor-modal__card:hover::-webkit-scrollbar-thumb {
background: rgba(94, 179, 255, 0.45);
background: var(--ds-accent-scrollbar);
}
.chat-msg__reasoning-pre::-webkit-scrollbar-thumb:hover,
.chat-pending-files::-webkit-scrollbar-thumb:hover,
.table-wrap::-webkit-scrollbar-thumb:hover,
.session-monitor-modal__card::-webkit-scrollbar-thumb:hover {
background: rgba(94, 179, 255, 0.65);
background: var(--ds-accent-scrollbar-hover);
}
.chat-msg {
@ -973,7 +984,7 @@ body.chat-standalone-page .chat-nav__scroll::-webkit-scrollbar {
body.chat-standalone-page .chat-messages {
scrollbar-width: thin;
scrollbar-color: #7a7a7a #2e2e2e;
scrollbar-color: var(--ds-chat-scroll-thumb) var(--ds-chat-scroll-track);
}
body.chat-standalone-page .chat-messages::-webkit-scrollbar {
@ -982,14 +993,14 @@ body.chat-standalone-page .chat-messages::-webkit-scrollbar {
}
body.chat-standalone-page .chat-messages::-webkit-scrollbar-track {
background: #2e2e2e;
background: var(--ds-chat-scroll-track);
}
body.chat-standalone-page .chat-messages::-webkit-scrollbar-thumb {
background: #7a7a7a;
background: var(--ds-chat-scroll-thumb);
border-radius: 0;
}
body.chat-standalone-page .chat-messages::-webkit-scrollbar-thumb:hover {
background: #909090;
background: var(--ds-chat-scroll-thumb-hover);
}

View file

@ -0,0 +1,233 @@
/* Classic admin/chat palettes: override CSS variables on body.theme-ds-body[data-admin-theme="…"].
Default (no attribute or deepseek) stays in theme-deepseek.css. */
body.theme-ds-body[data-admin-theme="github"] {
--ds-bg: #0d1117;
--ds-chat-main: #010409;
--ds-surface: #161b22;
--ds-surface-2: #21262d;
--ds-border: rgba(240, 246, 252, 0.1);
--ds-text: #f0f6fc;
--ds-text-muted: rgba(240, 246, 252, 0.55);
--ds-accent: #58a6ff;
--ds-accent-soft: rgba(88, 166, 255, 0.16);
--ds-sidebar: #161b22;
--ds-topbar-bg: rgba(22, 27, 34, 0.94);
--ds-accent-border: rgba(88, 166, 255, 0.32);
--ds-accent-border-strong: rgba(88, 166, 255, 0.55);
--ds-accent-link: rgba(88, 166, 255, 0.95);
--ds-accent-scrollbar: rgba(88, 166, 255, 0.45);
--ds-accent-scrollbar-hover: rgba(88, 166, 255, 0.68);
--ds-panel: #21262d;
--ds-chat-scroll-track: #21262d;
--ds-chat-scroll-thumb: #484f58;
--ds-chat-scroll-thumb-hover: #6e7681;
--chat-sess-hover-bg: rgba(240, 246, 252, 0.06);
--chat-sess-active-bg: rgba(240, 246, 252, 0.08);
--chat-sess-border: rgba(240, 246, 252, 0.12);
}
body.theme-ds-body[data-admin-theme="nord"] {
--ds-bg: #2e3440;
--ds-chat-main: #242933;
--ds-surface: #3b4252;
--ds-surface-2: #434c5e;
--ds-border: rgba(236, 239, 244, 0.12);
--ds-text: #eceff4;
--ds-text-muted: rgba(236, 239, 244, 0.55);
--ds-accent: #88c0d0;
--ds-accent-soft: rgba(136, 192, 208, 0.2);
--ds-sidebar: #3b4252;
--ds-topbar-bg: rgba(59, 66, 82, 0.95);
--ds-accent-border: rgba(136, 192, 208, 0.35);
--ds-accent-border-strong: rgba(136, 192, 208, 0.55);
--ds-accent-link: rgba(143, 188, 187, 0.98);
--ds-accent-scrollbar: rgba(136, 192, 208, 0.5);
--ds-accent-scrollbar-hover: rgba(143, 188, 187, 0.75);
--ds-panel: #434c5e;
--ds-chat-scroll-track: #3b4252;
--ds-chat-scroll-thumb: #5e81ac;
--ds-chat-scroll-thumb-hover: #81a1c1;
--chat-user-bubble-bg: #a3be8c;
--chat-user-bubble-fg: #2e3440;
}
body.theme-ds-body[data-admin-theme="dracula"] {
--ds-bg: #282a36;
--ds-chat-main: #1e1f29;
--ds-surface: #44475a;
--ds-surface-2: #6272a4;
--ds-border: rgba(248, 248, 242, 0.1);
--ds-text: #f8f8f2;
--ds-text-muted: rgba(248, 248, 242, 0.55);
--ds-accent: #bd93f9;
--ds-accent-soft: rgba(189, 147, 249, 0.22);
--ds-sidebar: #44475a;
--ds-topbar-bg: rgba(68, 71, 90, 0.95);
--ds-accent-border: rgba(189, 147, 249, 0.4);
--ds-accent-border-strong: rgba(189, 147, 249, 0.6);
--ds-accent-link: rgba(255, 121, 198, 0.95);
--ds-accent-scrollbar: rgba(189, 147, 249, 0.5);
--ds-accent-scrollbar-hover: rgba(255, 121, 198, 0.65);
--ds-panel: #44475a;
--ds-chat-scroll-track: #21222c;
--ds-chat-scroll-thumb: #6272a4;
--ds-chat-scroll-thumb-hover: #bd93f9;
}
body.theme-ds-body[data-admin-theme="forest"] {
--ds-bg: #0c1412;
--ds-chat-main: #070d0b;
--ds-surface: #13251f;
--ds-surface-2: #1a332b;
--ds-border: rgba(167, 243, 208, 0.1);
--ds-text: #ecfdf5;
--ds-text-muted: rgba(209, 250, 229, 0.55);
--ds-accent: #34d399;
--ds-accent-soft: rgba(52, 211, 153, 0.18);
--ds-sidebar: #13251f;
--ds-topbar-bg: rgba(19, 37, 31, 0.94);
--ds-accent-border: rgba(52, 211, 153, 0.35);
--ds-accent-border-strong: rgba(52, 211, 153, 0.55);
--ds-accent-link: rgba(110, 231, 183, 0.95);
--ds-accent-scrollbar: rgba(52, 211, 153, 0.45);
--ds-accent-scrollbar-hover: rgba(110, 231, 183, 0.7);
--ds-panel: #1a332b;
--ds-chat-scroll-track: #13251f;
--ds-chat-scroll-thumb: #047857;
--ds-chat-scroll-thumb-hover: #059669;
}
body.theme-ds-body[data-admin-theme="catppuccin"] {
--ds-bg: #1e1e2e;
--ds-chat-main: #181825;
--ds-surface: #313244;
--ds-surface-2: #45475a;
--ds-border: rgba(205, 214, 244, 0.1);
--ds-text: #cdd6f4;
--ds-text-muted: rgba(205, 214, 244, 0.52);
--ds-accent: #89b4fa;
--ds-accent-soft: rgba(137, 180, 250, 0.2);
--ds-sidebar: #313244;
--ds-topbar-bg: rgba(49, 50, 68, 0.95);
--ds-accent-border: rgba(137, 180, 250, 0.38);
--ds-accent-border-strong: rgba(137, 180, 250, 0.58);
--ds-accent-link: rgba(166, 218, 249, 0.96);
--ds-accent-scrollbar: rgba(137, 180, 250, 0.48);
--ds-accent-scrollbar-hover: rgba(166, 218, 249, 0.72);
--ds-panel: #45475a;
--ds-chat-scroll-track: #313244;
--ds-chat-scroll-thumb: #6c7086;
--ds-chat-scroll-thumb-hover: #89b4fa;
}
/* Light: same pipeline as dark presets (CSS variables on body) + a small set of chat/sidebar
tweaks where theme-deepseek.css still used “white mist” tints meant for dark canvases. */
body.theme-ds-body[data-admin-theme="light"] {
--ds-bg: #f1f5f9;
--ds-chat-main: #ffffff;
--ds-surface: #ffffff;
--ds-surface-2: #e2e8f0;
--ds-border: rgba(15, 23, 42, 0.12);
--ds-text: #0f172a;
--ds-text-muted: rgba(15, 23, 42, 0.55);
--ds-accent: #2563eb;
--ds-accent-soft: rgba(37, 99, 235, 0.14);
--ds-sidebar: #f8fafc;
--ds-topbar-bg: rgba(248, 250, 252, 0.92);
--ds-accent-border: rgba(37, 99, 235, 0.35);
--ds-accent-border-strong: rgba(37, 99, 235, 0.55);
--ds-accent-link: #1d4ed8;
--ds-accent-scrollbar: rgba(37, 99, 235, 0.42);
--ds-accent-scrollbar-hover: rgba(29, 78, 216, 0.62);
--ds-panel: #ffffff;
--ds-chat-scroll-track: #e2e8f0;
--ds-chat-scroll-thumb: #94a3b8;
--ds-chat-scroll-thumb-hover: #64748b;
--chat-sess-hover-bg: rgba(15, 23, 42, 0.06);
--chat-sess-active-bg: rgba(15, 23, 42, 0.09);
--chat-sess-border: rgba(15, 23, 42, 0.1);
--chat-user-bubble-bg: #10b981;
--chat-user-bubble-fg: #ffffff;
}
body.theme-ds-body[data-admin-theme="light"] .nav__groupTitle {
color: rgba(15, 23, 42, 0.48);
}
body.theme-ds-body[data-admin-theme="light"] .brand__sub {
color: rgba(15, 23, 42, 0.55);
}
body.theme-ds-body[data-admin-theme="light"] .chat-nav__newGlyph {
background-color: rgba(15, 23, 42, 0.08);
}
body.theme-ds-body[data-admin-theme="light"] .chat-nav__new:hover .chat-nav__newGlyph {
background-color: rgba(15, 23, 42, 0.14);
}
body.theme-ds-body[data-admin-theme="light"] .chat-nav__newGlyph::before,
body.theme-ds-body[data-admin-theme="light"] .chat-nav__newGlyph::after {
background-color: #0f172a;
}
body.theme-ds-body[data-admin-theme="light"] .chat-avatar--bot,
body.theme-ds-body[data-admin-theme="light"] .chat-avatar--userBuiltin {
background: rgba(15, 23, 42, 0.06);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg--assistant {
background: rgba(15, 23, 42, 0.045);
border-color: var(--ds-border);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg__action-btn {
border-color: rgba(15, 23, 42, 0.2);
color: rgba(15, 23, 42, 0.78);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg__action-btn:hover {
background: rgba(15, 23, 42, 0.06);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg--tool {
background: rgba(15, 23, 42, 0.06);
border-color: rgba(15, 23, 42, 0.18);
color: rgba(15, 23, 42, 0.88);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg--thinking {
background: rgba(15, 23, 42, 0.04);
border-color: rgba(15, 23, 42, 0.12);
}
body.theme-ds-body[data-admin-theme="light"] .chat-task-stage {
border-color: rgba(15, 23, 42, 0.12);
background: rgba(15, 23, 42, 0.03);
}
body.theme-ds-body[data-admin-theme="light"] .chat-task-stage__toggle {
border-color: rgba(15, 23, 42, 0.14);
background: rgba(15, 23, 42, 0.04);
}
body.theme-ds-body[data-admin-theme="light"] .chat-composer {
border-top-color: rgba(15, 23, 42, 0.08);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg__reasoning {
border-color: rgba(15, 23, 42, 0.1);
background: rgba(15, 23, 42, 0.03);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg__reasoning-pre {
background: rgba(15, 23, 42, 0.05);
border-color: rgba(15, 23, 42, 0.08);
color: var(--ds-text);
}
body.theme-ds-body[data-admin-theme="light"] .chat-msg__md .mermaid {
border-color: rgba(15, 23, 42, 0.14);
background: rgba(248, 250, 252, 0.95);
}

View file

@ -34,14 +34,19 @@ Write-Step "Starting gateway + desktop"
& "$PSScriptRoot/start_gateway.ps1" -BindHost $BindHost -Port $Port -SkipInstall:$SkipInstall -Background:$Background -WithWikiWorker:$WithWikiWorker
# When gateway is started in foreground, the call above blocks; desktop won't start.
# When gateway is started in foreground, the call above blocks; desktop / sidecars won't run in this script.
if (-not $Background) {
Write-Host ""
Write-Host "[INFO] Gateway started in foreground and will block. Use -Background to start both." -ForegroundColor Yellow
Write-Host "[INFO] Gateway is running in the foreground (this window blocks). Desktop was not started." -ForegroundColor Yellow
Write-Host " For gateway + desktop + sidecars in one go, run: start_all.ps1 -Background" -ForegroundColor Yellow
Write-Host " Or start desktop in another terminal after the gateway is listening: start_desktop.ps1 -Background -KeepExistingGateway" -ForegroundColor Yellow
exit 0
}
& "$PSScriptRoot/start_desktop.ps1" -SkipInstall:$SkipInstall -Background -WithWikiWorker:$WithWikiWorker
# start_desktop.ps1 defaults to stop_gateway.ps1 to free the port when launching desktop alone.
# After start_gateway above we must keep the listener — pass -KeepExistingGateway.
$gwUrl = "http://$BindHost`:$Port"
& "$PSScriptRoot/start_desktop.ps1" -SkipInstall:$SkipInstall -Background -WithWikiWorker:$WithWikiWorker -KeepExistingGateway -GatewayBaseUrl $gwUrl
if (-not $WithoutWeixin) {
$gwBase = ($WeixinGatewayBaseUrl | ForEach-Object { "$_".Trim() })

View file

@ -2,6 +2,7 @@
[switch]$SkipInstall = $false,
[switch]$Background = $false,
[switch]$KeepExistingGateway = $false,
[string]$GatewayBaseUrl = "",
[bool]$WithWikiWorker = $true
)
@ -27,6 +28,19 @@ $env:AIA_WORKSPACE_ROOT = $repoRoot
$env:OPS_WORKSPACE_ROOT = $repoRoot
$env:OCLAW_WORKSPACE = $repoRoot
# Do not inherit attach-mode env from a parent shell when launching standalone desktop.
Remove-Item Env:OCLAW_DESKTOP_EMBED_BACKEND -ErrorAction SilentlyContinue
Remove-Item Env:OCLAW_DESKTOP_GATEWAY_BASE_URL -ErrorAction SilentlyContinue
Remove-Item Env:OCLAW_DESKTOP_SKIP_CHANNEL -ErrorAction SilentlyContinue
if ($KeepExistingGateway) {
$gw = if ($GatewayBaseUrl) { $GatewayBaseUrl.Trim() } else { "http://127.0.0.1:8787" }
$env:OCLAW_DESKTOP_EMBED_BACKEND = "0"
$env:OCLAW_DESKTOP_GATEWAY_BASE_URL = $gw
$env:OCLAW_DESKTOP_SKIP_CHANNEL = "1"
Write-Host "==> Desktop will use existing gateway (no second listener): $gw" -ForegroundColor Cyan
}
if (-not $KeepExistingGateway) {
Write-Host "==> Cleaning previous gateway listener" -ForegroundColor Cyan
try {
@ -48,14 +62,21 @@ if ($WithWikiWorker) {
Write-Host "==> Launching desktop app" -ForegroundColor Cyan
if ($Background) {
# Avoid launching npm.ps1 directly (can be file-associated and open in Notepad on some setups).
# Use hidden cmd and redirect logs to avoid black console window popup.
$p = Start-Process -FilePath "cmd.exe" -ArgumentList @("/c","npm.cmd","run","dev") -WorkingDirectory $desktopDir -PassThru -WindowStyle Hidden -RedirectStandardOutput $outLog -RedirectStandardError $errLog
# Do NOT redirect stdout/stderr here: on Windows, piping npm/electron stdio can prevent the GUI from showing.
# Prefer the Electron shim directly; fallback to npm run dev (still without redirects).
$electronCmd = Join-Path $desktopDir "node_modules\.bin\electron.cmd"
$p = $null
if (Test-Path $electronCmd) {
$p = Start-Process -FilePath $electronCmd -ArgumentList @(".") -WorkingDirectory $desktopDir -PassThru -WindowStyle Hidden
}
if (-not $p) {
$p = Start-Process -FilePath "npm.cmd" -ArgumentList @("run","dev") -WorkingDirectory $desktopDir -PassThru -WindowStyle Hidden
}
Set-Content -Path $pidFile -Value "$($p.Id)" -Encoding ascii
Write-Host "desktop.pid = $pidFile" -ForegroundColor DarkGray
Write-Host "desktop.out = $outLog" -ForegroundColor DarkGray
Write-Host "desktop.err = $errLog" -ForegroundColor DarkGray
Write-Host "Embedded backend/chat logs (Electron): see %APPDATA%\oclaw\logs\desktop.log / backend.log" -ForegroundColor DarkGray
Write-Host "PID = $($p.Id)" -ForegroundColor Green
Write-Host "==> Desktop launched in background (this script exits now)." -ForegroundColor Cyan
exit 0
}
& "npm.cmd" "run" "dev"

View file

@ -3,10 +3,12 @@ param(
[int]$Port = 8787,
[switch]$SkipInstall = $false,
[switch]$Background = $false,
[switch]$WithWeixin = $false,
[switch]$WithoutWeixin = $false,
[switch]$WithoutWhatsApp = $false,
[bool]$WithWikiWorker = $true,
[string]$WeixinChannelId = "oclaw-weixin",
[string]$WeixinGatewayBaseUrl = ""
[string]$WeixinGatewayBaseUrl = "",
[string]$WhatsAppChannelId = "whatsapp"
)
$ErrorActionPreference = "Stop"
@ -16,5 +18,14 @@ if (-not (Test-Path $real)) {
throw "Forward script target not found: $real"
}
& $real -BindHost $BindHost -Port $Port -SkipInstall:$SkipInstall -Background:$Background -WithWeixin:$WithWeixin -WithWikiWorker:$WithWikiWorker -WeixinChannelId $WeixinChannelId -WeixinGatewayBaseUrl $WeixinGatewayBaseUrl
& $real `
-BindHost $BindHost `
-Port $Port `
-SkipInstall:$SkipInstall `
-Background:$Background `
-WithoutWeixin:$WithoutWeixin `
-WithoutWhatsApp:$WithoutWhatsApp `
-WithWikiWorker:$WithWikiWorker `
-WeixinChannelId $WeixinChannelId `
-WeixinGatewayBaseUrl $WeixinGatewayBaseUrl `
-WhatsAppChannelId $WhatsAppChannelId

View file

@ -1,6 +1,8 @@
param(
[switch]$SkipInstall = $false,
[switch]$Background = $false,
[switch]$KeepExistingGateway = $false,
[string]$GatewayBaseUrl = "",
[bool]$WithWikiWorker = $true
)
@ -11,5 +13,5 @@ if (-not (Test-Path $real)) {
throw "Forward script target not found: $real"
}
& $real -SkipInstall:$SkipInstall -Background:$Background -WithWikiWorker:$WithWikiWorker
& $real -SkipInstall:$SkipInstall -Background:$Background -KeepExistingGateway:$KeepExistingGateway -GatewayBaseUrl $GatewayBaseUrl -WithWikiWorker:$WithWikiWorker