perf: speed up desktop startup and unify theme-aware branding (#596)

* fix: unify startup logo and loading theme behavior

Show desktop window immediately with animated splash logo
Align splash/logo colors with selected app theme and default themes
Keep auth loading state on full-screen logo without size jump
Make project SVG icons follow active app theme
Use the active theme foreground color for project icons discovered from favicons
Apply server-side SVG color overrides for currentColor icons via icon request params
Keep non-SVG project icons unchanged while preserving existing fallback behavior

* perf: speed up desktop startup and unify loading logo visuals

Desktop startup now shows UI sooner while backend boot continues in background
Startup host probing uses a faster local path with safer remote fallback retries
OpenChamber logo cube highlights now match splash screens consistently

* fix: keep macOS traffic-light buttons in the correct position on load

Stop native window title updates on macOS during app initialization
Prevent title bar relayout that reset custom traffic-light positioning

* fix: recover missing providers and agents after fast startup

Retries provider/agent loading when connection is up but core config is still empty
Prevents cold-start state where models/agents appear only after manual project switch
Keeps startup responsive with throttled background recovery in app bootstrap
This commit is contained in:
Bohdan Triapitsyn
2026-03-04 19:16:45 +02:00
committed by GitHub
parent 037a70f114
commit d1a41000ca
22 changed files with 763 additions and 190 deletions
+11 -11
View File
@@ -299,7 +299,7 @@
var variant = localStorage.getItem('selectedThemeVariant');
var useSystem = localStorage.getItem('useSystemTheme');
var isDark;
// Check themeMode first (new storage key)
if (themeMode === 'dark') {
isDark = true;
@@ -315,7 +315,7 @@
// Default to system
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
// Apply theme class and data attribute
document.documentElement.classList.add(isDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-splash-variant', isDark ? 'dark' : 'light');
@@ -380,9 +380,9 @@
:root {
--splash-background-dark: #151313;
--splash-stroke-dark: white;
--splash-background-light: #F6F4EF;
--splash-stroke-light: black;
--splash-stroke-dark: #CECDC3;
--splash-background-light: #FFFCF0;
--splash-stroke-light: #100F0F;
--splash-background: var(--splash-background-dark);
--splash-stroke: var(--splash-stroke-dark);
@@ -447,10 +447,10 @@
<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OpenChamber loading icon">
<!-- Isometric cube: edge=48, centerY=50 -->
<!-- Points: top(50,2), left(8.432,26), right(91.568,26), center(50,50), bottomLeft(8.432,74), bottomRight(91.568,74), bottom(50,98) -->
<!-- Left face - base fill with stroke -->
<path d="M50 50 L8.432 26 L8.432 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<!-- Left face grid cells (4x4) with varying opacity -->
<path d="M50 50 L39.608 44 L39.608 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M39.608 44 L29.216 38 L29.216 50 L39.608 56 Z" fill="var(--splash-cell-fill)" opacity="0.45"/>
@@ -468,10 +468,10 @@
<path d="M39.608 80 L29.216 74 L29.216 86 L39.608 92 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<path d="M29.216 74 L18.824 68 L18.824 80 L29.216 86 Z" fill="var(--splash-cell-fill)" opacity="0.35"/>
<path d="M18.824 68 L8.432 62 L8.432 74 L18.824 80 Z" fill="var(--splash-cell-fill)" opacity="0.1"/>
<!-- Right face - base fill with stroke -->
<path d="M50 50 L91.568 26 L91.568 74 L50 98 Z" fill="var(--splash-face-fill)" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<!-- Right face grid cells (4x4) with varying opacity -->
<path d="M50 50 L60.392 44 L60.392 56 L50 62 Z" fill="var(--splash-cell-fill)" opacity="0.3"/>
<path d="M60.392 44 L70.784 38 L70.784 50 L60.392 56 Z" fill="var(--splash-cell-fill)" opacity="0.15"/>
@@ -489,10 +489,10 @@
<path d="M60.392 80 L70.784 74 L70.784 86 L60.392 92 Z" fill="var(--splash-cell-fill)" opacity="0.25"/>
<path d="M70.784 74 L81.176 68 L81.176 80 L70.784 86 Z" fill="var(--splash-cell-fill)" opacity="0.4"/>
<path d="M81.176 68 L91.568 62 L91.568 74 L81.176 80 Z" fill="var(--splash-cell-fill)" opacity="0.2"/>
<!-- Top face - open (no fill), only stroke -->
<path d="M50 2 L8.432 26 L50 50 L91.568 26 Z" fill="none" stroke="var(--splash-stroke)" stroke-width="2" stroke-linejoin="round"/>
<!-- OpenCode logo on top face -->
<g class="logo-pulse" transform="matrix(0.866, 0.5, -0.866, 0.5, 50, 26) scale(0.75)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-16 -20 L16 -20 L16 20 L-16 20 Z M-8 -12 L-8 12 L8 12 L8 -12 Z" fill="var(--splash-logo-fill)"/>
+1 -1
View File
@@ -28,7 +28,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "^1.2.15",
"@opencode-ai/sdk": "^1.2.17",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
+15 -4
View File
@@ -1,15 +1,26 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<style>
:root {
color: #111111;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f5f5f5;
}
}
</style>
<!-- OpenChamber logo - simplified for favicon -->
<g transform="translate(16, 16) scale(0.48)">
<!-- Left face - with fill -->
<path d="M0 0 L-26 -15 L-26 15 L0 30 Z" fill="white" fill-opacity="0.25" stroke="white" stroke-width="4" stroke-linejoin="round"/>
<path d="M0 0 L-26 -15 L-26 15 L0 30 Z" fill="currentColor" fill-opacity="0.25" stroke="currentColor" stroke-width="4" stroke-linejoin="round"/>
<!-- Right face - with fill -->
<path d="M0 0 L26 -15 L26 15 L0 30 Z" fill="white" fill-opacity="0.4" stroke="white" stroke-width="4" stroke-linejoin="round"/>
<path d="M0 0 L26 -15 L26 15 L0 30 Z" fill="currentColor" fill-opacity="0.4" stroke="currentColor" stroke-width="4" stroke-linejoin="round"/>
<!-- Top face - open -->
<path d="M0 -30 L-26 -15 L0 0 L26 -15 Z" fill="none" stroke="white" stroke-width="4" stroke-linejoin="round"/>
<path d="M0 -30 L-26 -15 L0 0 L26 -15 Z" fill="none" stroke="currentColor" stroke-width="4" stroke-linejoin="round"/>
<!-- O logo - hollow rectangle frame on top face with gray fill inside -->
<g transform="matrix(0.866, 0.5, -0.866, 0.5, 0, -15) scale(0.55)">
<rect x="-12" y="-14" width="24" height="28" fill="white" fill-opacity="0.4" stroke="white" stroke-width="8"/>
<rect x="-12" y="-14" width="24" height="28" fill="currentColor" fill-opacity="0.4" stroke="currentColor" stroke-width="8"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 958 B

After

Width:  |  Height:  |  Size: 1.1 KiB

+190 -93
View File
@@ -784,6 +784,36 @@ const resolveZenModel = async (override) => {
return validatedZenFallback || ZEN_DEFAULT_MODEL;
};
const validateZenModelAtStartup = async () => {
try {
const freeModels = await fetchFreeZenModels();
const freeModelIds = freeModels.map((m) => m.id);
if (freeModelIds.length > 0) {
validatedZenFallback = freeModelIds[0];
const settings = await readSettingsFromDisk();
const storedModel = typeof settings?.zenModel === 'string' ? settings.zenModel.trim() : '';
if (!storedModel || !freeModelIds.includes(storedModel)) {
const fallback = freeModelIds[0];
console.log(
storedModel
? `[zen] Stored model "${storedModel}" not found in free models, falling back to "${fallback}"`
: `[zen] No model configured, setting default to "${fallback}"`
);
await persistSettings({ zenModel: fallback });
} else {
console.log(`[zen] Stored model "${storedModel}" verified as available`);
}
} else {
console.warn('[zen] No free models returned from API, skipping validation');
}
} catch (error) {
console.warn('[zen] Startup model validation failed (non-blocking):', error?.message || error);
}
};
const summarizeText = async (text, targetLength, zenModel) => {
if (!text || typeof text !== 'string' || text.trim().length === 0) return text;
@@ -1148,6 +1178,11 @@ const PROJECT_ICON_EXTENSION_TO_MIME = Object.fromEntries(
);
const PROJECT_ICON_SUPPORTED_MIMES = new Set(Object.keys(PROJECT_ICON_MIME_TO_EXTENSION));
const PROJECT_ICON_MAX_BYTES = 5 * 1024 * 1024;
const PROJECT_ICON_THEME_COLORS = {
light: '#111111',
dark: '#f5f5f5',
};
const PROJECT_ICON_HEX_COLOR_PATTERN = /^#(?:[\da-fA-F]{3}|[\da-fA-F]{4}|[\da-fA-F]{6}|[\da-fA-F]{8})$/;
const normalizeProjectIconMime = (value) => {
if (typeof value !== 'string') {
@@ -1230,6 +1265,54 @@ const parseProjectIconDataUrl = (value) => {
}
};
const normalizeProjectIconThemeVariant = (value) => {
if (typeof value !== 'string') {
return null;
}
const normalized = value.trim().toLowerCase();
if (normalized === 'light' || normalized === 'dark') {
return normalized;
}
return null;
};
const normalizeProjectIconColor = (value) => {
if (typeof value !== 'string') {
return null;
}
const normalized = value.trim();
if (!PROJECT_ICON_HEX_COLOR_PATTERN.test(normalized)) {
return null;
}
return normalized;
};
const applyProjectIconSvgTheme = (svgMarkup, themeVariant, iconColor) => {
if (typeof svgMarkup !== 'string') {
return svgMarkup;
}
const color = iconColor || PROJECT_ICON_THEME_COLORS[themeVariant];
if (!color) {
return svgMarkup;
}
const svgTagIndex = svgMarkup.search(/<svg\b/i);
if (svgTagIndex === -1) {
return svgMarkup;
}
const svgOpenTagEndIndex = svgMarkup.indexOf('>', svgTagIndex);
if (svgOpenTagEndIndex === -1) {
return svgMarkup;
}
const overrideStyle = `<style data-openchamber-theme-icon="1">:root{color:${color}!important;}</style>`;
return `${svgMarkup.slice(0, svgOpenTagEndIndex + 1)}${overrideStyle}${svgMarkup.slice(svgOpenTagEndIndex + 1)}`;
};
const findProjectById = (settings, projectId) => {
const projects = sanitizeProjects(settings?.projects) || [];
const index = projects.findIndex((project) => project.id === projectId);
@@ -1795,6 +1878,18 @@ const sanitizeSettingsUpdate = (payload) => {
if (typeof candidate.darkThemeId === 'string' && candidate.darkThemeId.length > 0) {
result.darkThemeId = candidate.darkThemeId;
}
if (typeof candidate.splashBgLight === 'string' && candidate.splashBgLight.trim().length > 0) {
result.splashBgLight = candidate.splashBgLight.trim();
}
if (typeof candidate.splashFgLight === 'string' && candidate.splashFgLight.trim().length > 0) {
result.splashFgLight = candidate.splashFgLight.trim();
}
if (typeof candidate.splashBgDark === 'string' && candidate.splashBgDark.trim().length > 0) {
result.splashBgDark = candidate.splashBgDark.trim();
}
if (typeof candidate.splashFgDark === 'string' && candidate.splashFgDark.trim().length > 0) {
result.splashFgDark = candidate.splashFgDark.trim();
}
if (typeof candidate.lastDirectory === 'string' && candidate.lastDirectory.length > 0) {
result.lastDirectory = candidate.lastDirectory;
}
@@ -5929,6 +6024,72 @@ async function refreshOpenCodeAfterConfigChange(reason, options = {}) {
}
}
async function bootstrapOpenCodeAtStartup() {
try {
syncFromHmrState();
if (await isOpenCodeProcessHealthy()) {
console.log(`[HMR] Reusing existing OpenCode process on port ${openCodePort}`);
} else if (ENV_SKIP_OPENCODE_START && ENV_EFFECTIVE_PORT) {
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
console.log(`Using external OpenCode server at ${label} (skip-start mode)`);
openCodeBaseUrl = ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
setOpenCodePort(ENV_EFFECTIVE_PORT);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else if (ENV_EFFECTIVE_PORT && await probeExternalOpenCode(ENV_EFFECTIVE_PORT, ENV_CONFIGURED_OPENCODE_HOST?.origin)) {
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
console.log(`Auto-detected existing OpenCode server at ${label}`);
openCodeBaseUrl = ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
setOpenCodePort(ENV_EFFECTIVE_PORT);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else if (!ENV_EFFECTIVE_PORT && await probeExternalOpenCode(4096)) {
console.log('Auto-detected existing OpenCode server on default port 4096');
setOpenCodePort(4096);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else {
if (ENV_EFFECTIVE_PORT) {
console.log(`Using OpenCode port from environment: ${ENV_EFFECTIVE_PORT}`);
setOpenCodePort(ENV_EFFECTIVE_PORT);
} else {
openCodePort = null;
syncToHmrState();
}
lastOpenCodeError = null;
openCodeProcess = await startOpenCode();
syncToHmrState();
}
await waitForOpenCodePort();
try {
await waitForOpenCodeReady();
} catch (error) {
console.error(`OpenCode readiness check failed: ${error.message}`);
scheduleOpenCodeApiDetection();
}
scheduleOpenCodeApiDetection();
startHealthMonitoring();
void startGlobalEventWatcher().catch((error) => {
console.warn(`Global event watcher startup failed: ${error?.message || error}`);
});
} catch (error) {
console.error(`Failed to start OpenCode: ${error.message}`);
console.log('Continuing without OpenCode integration...');
lastOpenCodeError = error.message;
scheduleOpenCodeApiDetection();
}
}
function setupProxy(app) {
if (app.get('opencodeProxyConfigured')) {
return;
@@ -6564,35 +6725,8 @@ async function main(options = {}) {
sayTTSCapability = { available: false, voices: [], reason: 'Not macOS' };
}
// Validate stored zen model at startup best-effort, never blocks startup
try {
const freeModels = await fetchFreeZenModels();
const freeModelIds = freeModels.map((m) => m.id);
if (freeModelIds.length > 0) {
// Set the validated fallback to the first available free model
validatedZenFallback = freeModelIds[0];
const settings = await readSettingsFromDisk();
const storedModel = typeof settings?.zenModel === 'string' ? settings.zenModel.trim() : '';
if (!storedModel || !freeModelIds.includes(storedModel)) {
const fallback = freeModelIds[0];
console.log(
storedModel
? `[zen] Stored model "${storedModel}" not found in free models, falling back to "${fallback}"`
: `[zen] No model configured, setting default to "${fallback}"`
);
await persistSettings({ zenModel: fallback });
} else {
console.log(`[zen] Stored model "${storedModel}" verified as available`);
}
} else {
console.warn('[zen] No free models returned from API, skipping validation');
}
} catch (error) {
console.warn('[zen] Startup model validation failed (non-blocking):', error?.message || error);
}
// Startup model validation is best-effort and runs in background.
void validateZenModelAtStartup();
const app = express();
const serverStartedAt = new Date().toISOString();
@@ -8091,11 +8225,34 @@ async function main(options = {}) {
? [preferredPath, ...projectIconPathCandidates(projectId).filter((candidate) => candidate !== preferredPath)]
: projectIconPathCandidates(projectId);
const themeQuery = Array.isArray(req.query?.theme) ? req.query.theme[0] : req.query?.theme;
const requestedThemeVariant = normalizeProjectIconThemeVariant(themeQuery);
const iconColorQuery = Array.isArray(req.query?.iconColor) ? req.query.iconColor[0] : req.query?.iconColor;
const requestedIconColor = normalizeProjectIconColor(iconColorQuery);
for (const iconPath of candidates) {
try {
const data = await fsPromises.readFile(iconPath);
const ext = path.extname(iconPath).slice(1).toLowerCase();
const contentType = metadataMime || PROJECT_ICON_EXTENSION_TO_MIME[ext] || 'application/octet-stream';
const resolvedMime = metadataMime || PROJECT_ICON_EXTENSION_TO_MIME[ext] || 'application/octet-stream';
const contentType = resolvedMime === 'image/svg+xml' ? 'image/svg+xml; charset=utf-8' : resolvedMime;
if (resolvedMime === 'image/svg+xml' && requestedThemeVariant) {
const svgMarkup = data.toString('utf8');
const themedSvgMarkup = applyProjectIconSvgTheme(svgMarkup, requestedThemeVariant, requestedIconColor);
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
return res.send(themedSvgMarkup);
}
if (resolvedMime === 'image/svg+xml' && requestedIconColor) {
const svgMarkup = data.toString('utf8');
const themedSvgMarkup = applyProjectIconSvgTheme(svgMarkup, requestedThemeVariant, requestedIconColor);
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
return res.send(themedSvgMarkup);
}
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
return res.send(data);
@@ -13006,69 +13163,9 @@ async function main(options = {}) {
res.json({ success: true, killedCount });
});
try {
syncFromHmrState();
if (await isOpenCodeProcessHealthy()) {
console.log(`[HMR] Reusing existing OpenCode process on port ${openCodePort}`);
} else if (ENV_SKIP_OPENCODE_START && ENV_EFFECTIVE_PORT) {
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
console.log(`Using external OpenCode server at ${label} (skip-start mode)`);
openCodeBaseUrl = ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
setOpenCodePort(ENV_EFFECTIVE_PORT);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else if (ENV_EFFECTIVE_PORT && await probeExternalOpenCode(ENV_EFFECTIVE_PORT, ENV_CONFIGURED_OPENCODE_HOST?.origin)) {
const label = ENV_CONFIGURED_OPENCODE_HOST ? ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${ENV_EFFECTIVE_PORT}`;
console.log(`Auto-detected existing OpenCode server at ${label}`);
openCodeBaseUrl = ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
setOpenCodePort(ENV_EFFECTIVE_PORT);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else if (!ENV_EFFECTIVE_PORT && await probeExternalOpenCode(4096)) {
console.log('Auto-detected existing OpenCode server on default port 4096');
setOpenCodePort(4096);
isOpenCodeReady = true;
isExternalOpenCode = true;
lastOpenCodeError = null;
openCodeNotReadySince = 0;
syncToHmrState();
} else {
if (ENV_EFFECTIVE_PORT) {
console.log(`Using OpenCode port from environment: ${ENV_EFFECTIVE_PORT}`);
setOpenCodePort(ENV_EFFECTIVE_PORT);
} else {
openCodePort = null;
syncToHmrState();
}
lastOpenCodeError = null;
openCodeProcess = await startOpenCode();
syncToHmrState();
}
await waitForOpenCodePort();
try {
await waitForOpenCodeReady();
} catch (error) {
console.error(`OpenCode readiness check failed: ${error.message}`);
scheduleOpenCodeApiDetection();
}
setupProxy(app);
scheduleOpenCodeApiDetection();
startHealthMonitoring();
void startGlobalEventWatcher();
} catch (error) {
console.error(`Failed to start OpenCode: ${error.message}`);
console.log('Continuing without OpenCode integration...');
lastOpenCodeError = error.message;
setupProxy(app);
scheduleOpenCodeApiDetection();
}
setupProxy(app);
scheduleOpenCodeApiDetection();
void bootstrapOpenCodeAtStartup();
const distPath = (() => {
const env = typeof process.env.OPENCHAMBER_DIST_DIR === 'string' ? process.env.OPENCHAMBER_DIST_DIR.trim() : '';