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:
committed by
GitHub
parent
037a70f114
commit
d1a41000ca
+11
-11
@@ -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)"/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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() : '';
|
||||
|
||||
Reference in New Issue
Block a user