Skip to content

Commit 09d4e5e

Browse files
OpenSource03claude
andcommitted
feat: startup performance — deferred analytics, lazy model revalidation, and flash-free window
- Show window only after ready-to-show to eliminate white flash on launch - Fire-and-forget PostHog init to avoid blocking app.whenReady() - Defer syncAnalyticsSettings to useEffect (after first paint, not before) - Delay model cache revalidation by 3s to reduce startup IPC contention - Add 50ms yield between sequential session prefetch loads - Bump version to 0.20.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8d6d633 commit 09d4e5e

File tree

5 files changed

+40
-19
lines changed

5 files changed

+40
-19
lines changed

electron/src/main.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ function isMainRendererPermissionRequest(webContents: Electron.WebContents | nul
7171

7272
function createWindow(): void {
7373
const windowOptions: Electron.BrowserWindowConstructorOptions = {
74+
show: false,
7475
width: 1200,
7576
height: 800,
7677
// Matches the renderer's stricter island-layout minimum before first IPC sync,
@@ -109,6 +110,10 @@ function createWindow(): void {
109110

110111
mainWindow = new BrowserWindow(windowOptions);
111112

113+
mainWindow.once("ready-to-show", () => {
114+
mainWindow?.show();
115+
});
116+
112117
contextMenu({
113118
window: mainWindow,
114119
showSearchWithGoogle: false,
@@ -294,15 +299,17 @@ ipcMain.handle("speech:request-mic-permission", async () => {
294299
return { granted: true };
295300
});
296301

297-
app.whenReady().then(async () => {
302+
app.whenReady().then(() => {
298303
// Migrate data from old "OpenACP UI" app directory before anything reads it
299304
migrateFromOpenAcpUi();
300305

301306
createWindow();
302307
initAutoUpdater(getMainWindow);
303308

304-
// Initialize PostHog analytics (if enabled in settings)
305-
await initPostHog();
309+
// Initialize PostHog analytics (if enabled in settings) — fire-and-forget to avoid blocking startup
310+
initPostHog().catch((err) => {
311+
reportError("POSTHOG", err, { context: "startup-init" });
312+
});
306313

307314
// Allow microphone access for Whisper voice dictation (getUserMedia in renderer)
308315
session.defaultSession.setPermissionRequestHandler(

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "harnss",
3-
"version": "0.19.1",
3+
"version": "0.20.0",
44
"productName": "Harnss",
55
"description": "Harness your AI coding agents — one desktop app for Claude Code, Codex, and any ACP agent",
66
"author": {

src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import { useEffect } from "react";
12
import { Toaster } from "sonner";
23
import { TooltipProvider } from "@/components/ui/tooltip";
34
import { AppLayout } from "@/components/AppLayout";
5+
import { syncAnalyticsSettings } from "@/lib/posthog";
46

57
export function App() {
8+
// Sync analytics opt-in state after mount — avoids blocking first paint with IPC calls
9+
useEffect(() => {
10+
syncAnalyticsSettings();
11+
}, []);
612
// Guard: if the preload script failed, window.claude won't exist.
713
// Throwing here lets the ErrorBoundary show a visible message instead of a blank window.
814
if (!window.claude) {

src/hooks/session/useSessionLifecycle.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -222,19 +222,25 @@ export function useSessionLifecycle({
222222
}
223223
}).catch(() => { /* cache read is optional */ });
224224

225-
window.claude.modelsCacheRevalidate(preferredCwd ? { cwd: preferredCwd } : undefined).then((result) => {
226-
if (cancelled) return;
227-
if (result.models?.length) {
228-
setCachedModels(result.models);
229-
return;
230-
}
231-
if (result.error) {
232-
toast.error("Failed to load Claude models", { description: result.error });
233-
}
234-
}).catch(() => { /* keep stale cache if revalidation fails */ });
225+
// Defer revalidation (spawns a Claude SDK subprocess) to avoid competing with
226+
// the startup IPC burst. The cached models from modelsCacheGet() above are
227+
// sufficient for the initial render.
228+
const revalidateTimer = setTimeout(() => {
229+
window.claude.modelsCacheRevalidate(preferredCwd ? { cwd: preferredCwd } : undefined).then((result) => {
230+
if (cancelled) return;
231+
if (result.models?.length) {
232+
setCachedModels(result.models);
233+
return;
234+
}
235+
if (result.error) {
236+
toast.error("Failed to load Claude models", { description: result.error });
237+
}
238+
}).catch(() => { /* keep stale cache if revalidation fails */ });
239+
}, 3000);
235240

236241
return () => {
237242
cancelled = true;
243+
clearTimeout(revalidateTimer);
238244
};
239245
}, [getProjectCwd]);
240246

@@ -284,17 +290,19 @@ export function useSessionLifecycle({
284290
} finally {
285291
inFlightPrefetchRef.current.delete(session.id);
286292
}
293+
// Yield between sequential loads to let the main process event loop breathe
294+
await new Promise((r) => setTimeout(r, 50));
287295
}
288296
};
289297

290298
if (typeof window.requestIdleCallback === "function") {
291299
idleId = window.requestIdleCallback(() => {
292300
void run();
293-
}, { timeout: 1500 });
301+
}, { timeout: 5000 });
294302
} else {
295303
timerId = setTimeout(() => {
296304
void run();
297-
}, 250);
305+
}, 3000);
298306
}
299307

300308
return () => {

src/main.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
33
import { PostHogProvider } from "@posthog/react";
44
import { ErrorBoundary } from "./components/ErrorBoundary";
55
import { migrateLocalStorage } from "./lib/local-storage-migration";
6-
import { initPostHog, syncAnalyticsSettings, posthog } from "./lib/posthog";
6+
import { initPostHog, posthog } from "./lib/posthog";
77
import { App } from "./App";
88
import "./index.css";
99

@@ -13,8 +13,8 @@ migrateLocalStorage();
1313
// Initialize posthog-js (starts opted-out until settings confirm opt-in)
1414
initPostHog();
1515

16-
// Sync analytics opt-in state from main process settings once available
17-
syncAnalyticsSettings();
16+
// Analytics opt-in sync is deferred to after React mount (in App.tsx useEffect)
17+
// to avoid firing IPC calls before first paint.
1818

1919
createRoot(document.getElementById("root")!).render(
2020
<StrictMode>

0 commit comments

Comments
 (0)