Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
include fmt.mk

.PHONY: all build dev start clean help
.PHONY: build-renderer version build-icons
.PHONY: build-renderer version build-icons build-static
.PHONY: lint lint-fix typecheck static-check
.PHONY: test test-unit test-integration test-watch test-coverage test-e2e
.PHONY: dist dist-mac dist-win dist-linux
Expand Down Expand Up @@ -62,7 +62,7 @@ start: node_modules/.installed build-main build-preload ## Build and start Elect
@bun x electron --remote-debugging-port=9222 .

## Build targets (can run in parallel)
build: node_modules/.installed src/version.ts build-renderer build-main build-preload build-icons ## Build all targets
build: node_modules/.installed src/version.ts build-renderer build-main build-preload build-icons build-static ## Build all targets

build-main: node_modules/.installed dist/main.js ## Build main process

Expand All @@ -86,6 +86,10 @@ build-renderer: node_modules/.installed src/version.ts ## Build renderer process
@echo "Building renderer..."
@bun x vite build

build-static: ## Copy static assets to dist
@echo "Copying static assets..."
@cp static/splash.html dist/splash.html

# Always regenerate version file (marked as .PHONY above)
version: ## Generate version file
@./scripts/generate-version.sh
Expand Down
55 changes: 54 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ if (!gotTheLock) {
}

let mainWindow: BrowserWindow | null = null;
let splashWindow: BrowserWindow | null = null;

function createMenu() {
const template: MenuItemConstructorOptions[] = [
Expand Down Expand Up @@ -182,6 +183,44 @@ function createMenu() {
Menu.setApplicationMenu(menu);
}

/**
* Create and show splash screen - instant visual feedback (<100ms)
*
* Shows a lightweight native window with static HTML while services load.
* No IPC, no React, no heavy dependencies - just immediate user feedback.
*/
function showSplashScreen() {
splashWindow = new BrowserWindow({
width: 400,
height: 300,
frame: false,
transparent: true,
alwaysOnTop: true,
center: true,
resizable: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});

void splashWindow.loadFile(path.join(__dirname, "splash.html"));

splashWindow.on("closed", () => {
splashWindow = null;
});
}

/**
* Close splash screen
*/
function closeSplashScreen() {
if (splashWindow) {
splashWindow.close();
splashWindow = null;
}
}

async function createWindow() {
// Lazy-load Config and IpcMain only when window is created
// This defers loading heavy AI SDK dependencies until actually needed
Expand Down Expand Up @@ -218,11 +257,17 @@ async function createWindow() {
// Hide menu bar on Linux by default (like VS Code)
// User can press Alt to toggle it
autoHideMenuBar: process.platform === "linux",
show: false, // Don't show until ready-to-show event
});

// Register IPC handlers with the main window
ipcMain.register(electronIpcMain, mainWindow);

// Show window once it's ready to avoid white flash
mainWindow.once("ready-to-show", () => {
mainWindow?.show();
});

// Open all external links in default browser
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
void shell.openExternal(url);
Expand Down Expand Up @@ -278,7 +323,12 @@ if (gotTheLock) {
}

createMenu();

// Show splash screen immediately, then load services, then show main window
// This gives instant visual feedback (<100ms) while services load (~6-13s)
showSplashScreen();
await createWindow();
closeSplashScreen();

// Start loading tokenizer modules in background after window is created
// This ensures accurate token counts for first API calls (especially in e2e tests)
Expand All @@ -301,7 +351,10 @@ if (gotTheLock) {
// Only create window if app is ready and no window exists
// This prevents "Cannot create BrowserWindow before app is ready" error
if (app.isReady() && mainWindow === null) {
void createWindow();
showSplashScreen();
void createWindow().then(() => {
closeSplashScreen();
});
}
});
}
81 changes: 81 additions & 0 deletions static/splash.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cmux - Loading</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
width: 400px;
height: 300px;
/* Match --color-background (hsl(0 0% 12%)) */
background: hsl(0 0% 12%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Match --color-text (hsl(0 0% 83%)) */
color: hsl(0 0% 83%);
overflow: hidden;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}

.logo {
font-size: 48px;
font-weight: 700;
letter-spacing: -2px;
margin-bottom: 24px;
/* Gradient from plan mode blue (hsl(210 70% 40%)) to exec mode purple (hsl(268.56deg 94.04% 55.19%)) */
background: linear-gradient(135deg, hsl(210 70% 45%) 0%, hsl(268.56deg 94.04% 55.19%) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}

.loading-text {
font-size: 16px;
/* Match --color-text-secondary (hsl(0 0% 42%)) */
color: hsl(0 0% 42%);
margin-bottom: 16px;
}

.spinner {
width: 40px;
height: 40px;
/* Match --color-border (hsl(240 2% 25%)) */
border: 3px solid hsl(240 2% 25%);
/* Match --color-plan-mode (hsl(210 70% 40%)) */
border-top-color: hsl(210 70% 40%);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}

@keyframes spin {
to { transform: rotate(360deg); }
}

.version {
position: absolute;
bottom: 20px;
font-size: 11px;
/* Match --color-text-secondary (hsl(0 0% 42%)) */
color: hsl(0 0% 42%);
}
</style>
</head>
<body>
<div class="logo">cmux</div>
<div class="loading-text">Loading services...</div>
<div class="spinner"></div>
<div class="version">coder multiplexer</div>
</body>
</html>

Loading