diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 7d68ff293b..f8b5140b20 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -831,7 +831,7 @@
"react": 1
},
"importSpecifiers": 98,
- "nonTriviaTokens": 12731
+ "nonTriviaTokens": 12730
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 0,
@@ -1085,7 +1085,6 @@
"./features/conversation/index.js": 1,
"./locales/shell-copy": 1,
"./onboarding-hero": 1,
- "@astryxdesign/core": 1,
"@maka/ui": 1,
"react": 1
}
diff --git a/apps/desktop/src/main/__tests__/main-startup-lifetime.test.ts b/apps/desktop/src/main/__tests__/main-startup-lifetime.test.ts
index 83ad0528d3..a7c9aa3361 100644
--- a/apps/desktop/src/main/__tests__/main-startup-lifetime.test.ts
+++ b/apps/desktop/src/main/__tests__/main-startup-lifetime.test.ts
@@ -46,6 +46,10 @@ const appSource = readFileSync(
fileURLToPath(new URL('../../../src/renderer/app.tsx', import.meta.url)),
'utf8',
);
+const indexHtmlSource = readFileSync(
+ fileURLToPath(new URL('../../../src/renderer/index.html', import.meta.url)),
+ 'utf8',
+);
test('retains process lifetime before a standalone startup dialog can close', () => {
const retentionPolicy = mainSource.search(
@@ -205,3 +209,34 @@ test('routes the first-paint IPC only to the active Renderer recovery listener',
);
assert.match(readyHandler, /revealGate\.markReady\(mainWindow\)/u);
});
+
+test('retires the launch overlay only when a surface marks ready content', () => {
+ // The overlay's dismissal and its emitters live in different files; pinning
+ // both sides keeps the attribute from drifting into a permanent logo.
+ const readRenderer = (path: string) =>
+ readFileSync(
+ fileURLToPath(new URL(`../../../src/renderer/${path}`, import.meta.url)),
+ 'utf8',
+ );
+ const workHubRoot = readRenderer('features/workhub/ui/workhub-root.tsx');
+ const handoffOverlay = readRenderer(
+ 'features/runtime-host-management/ui/runtime-host-handoff-overlay.tsx',
+ );
+ const errorBoundary = readRenderer('error-boundary.tsx');
+ const chatMessageSurface = readRenderer('chat-message-surface.tsx');
+
+ assert.match(
+ indexHtmlSource,
+ /body:has\(#root \[data-maka-content-ready\]\) > \.maka-preload/u,
+ );
+ assert.doesNotMatch(indexHtmlSource, /body:has\(#root > \*\)/u);
+ // The shell marks ready only once the first snapshot settles; the floating
+ // composer is content-complete at mount; a pending handoff decision and the
+ // error surface must not wait on either.
+ assert.match(appShellSource, /data-maka-content-ready=\{!isOnboardingLoading/u);
+ assert.match(workHubRoot, /data-maka-content-ready/u);
+ assert.match(handoffOverlay, /data-maka-content-ready/u);
+ assert.match(errorBoundary, /data-maka-content-ready/u);
+ // The second loading surface is deleted: the overlay alone covers the gap.
+ assert.doesNotMatch(chatMessageSurface, /maka-onboarding-loading/u);
+});
diff --git a/apps/desktop/src/main/main-window.ts b/apps/desktop/src/main/main-window.ts
index 5086629842..37f7f94a11 100644
--- a/apps/desktop/src/main/main-window.ts
+++ b/apps/desktop/src/main/main-window.ts
@@ -552,7 +552,7 @@ export function createMainWindowController(deps: MainWindowControllerDeps): Main
// PR-SHOW-AFTER-FIRST-COMMIT: reveal fallback. Start this budget only once
// the renderer document has loaded. Starting it before loadURL/loadFile
// let a cold Vite transform or slow disk consume the whole timeout and
- // reveal index.html's preload skeleton before React had a chance to paint.
+ // reveal index.html's launch overlay before React had a chance to paint.
// If renderer-ready arrived while loadURL/loadFile was resolving, the
// window is already visible and no timer is needed. E2e-fixture windows
// remain hidden for their whole lifecycle.
@@ -830,7 +830,7 @@ function emitRealWindowSmokeDiagnostic(stage: string): void {
bodyTextSample: document.body?.innerText?.trim().slice(0, 240) ?? '',
stylesheetCount: document.styleSheets.length,
rootChildren: document.getElementById('root')?.children.length ?? 0,
- elements: ['body', '#root', '.appFrame', '.app', '.maka-panel-detail', '.mainColumn', '.maka-onboarding-loading'].map((selector) => {
+ elements: ['body', '#root', '.appFrame', '.app', '.maka-panel-detail', '.mainColumn', '.maka-preload'].map((selector) => {
const element = document.querySelector(selector);
if (!element) return { selector, present: false };
const rect = element.getBoundingClientRect();
diff --git a/apps/desktop/src/renderer/README.md b/apps/desktop/src/renderer/README.md
index 4d0c8b46c8..2ac1bce1fd 100644
--- a/apps/desktop/src/renderer/README.md
+++ b/apps/desktop/src/renderer/README.md
@@ -25,9 +25,9 @@ For the main/preload/renderer split and the IPC contract, see `apps/desktop/READ
## Entry
-`main.tsx` → `app.tsx` → `AppShell` (`app-shell.tsx`). `index.html` is the Vite HTML shell. `main.tsx` mounts React immediately — the `.maka-preload` skeleton covers the load gap and each surface hydrates its own data after mount; `app.tsx` wraps `AppShell` in `ToastProvider` + `ErrorBoundary`.
+`main.tsx` → `app.tsx` → `AppShell` (`app-shell.tsx`). `index.html` is the Vite HTML shell. `main.tsx` mounts React immediately — the `.maka-preload` launch overlay covers the load gap and stays until a surface commits `data-maka-content-ready`; `app.tsx` wraps `AppShell` in `ToastProvider` + `ErrorBoundary`.
-`styles.css` is the **only** bundled style entry: it imports Astryx, fonts, `maka-tokens.css`, `reference-shell.css`, and every `styles/*.css`. It contains only top-level orchestration; real selector rules go in `styles/*.css`. One contract-pinned exception: `index.html` carries an inline `.maka-preload` skeleton with hardcoded colors (no CSS variables — `maka-tokens.css` hasn't loaded yet) so there's no blank window during the CSS + JS load gap; `createRoot` replaces it on mount.
+`styles.css` is the **only** bundled style entry: it imports Astryx, fonts, `maka-tokens.css`, `reference-shell.css`, and every `styles/*.css`. It contains only top-level orchestration; real selector rules go in `styles/*.css`. One contract-pinned exception: `index.html` carries an inline `.maka-preload` launch overlay with hardcoded colors (no CSS variables — `maka-tokens.css` hasn't loaded yet) so there's no blank window during the CSS + JS load gap; it retires once a surface commits `data-maka-content-ready`.
## Renderer ownership boundary
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 279145e090..eaafc7b180 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -1031,10 +1031,10 @@ function AppShellContent({
void defaultHostConnections.refreshConnections();
}
}, [onboarding.error, onboarding.snapshot]);
- // PR110c (@kenji review): suppress hero AND the fallback EmptyChatHero
- // while the initial snapshot is in flight. Otherwise sessions.length===0
- // + snapshot===null flashes the prompt-suggestion EmptyChatHero before
- // the state-routed OnboardingHero mounts.
+ // Nothing settled to show while the first snapshot pull is in flight. The
+ // flag keeps the composer hidden and — through `data-maka-content-ready` on
+ // .appFrame — holds the launch overlay until a real frame exists: sessions,
+ // a hero, or the load-error fallback.
const isOnboardingLoading =
sessionCount === 0 && onboardingState === undefined && !onboardingSettled && !onboarding.error;
// Only unfinished setup takes the chat surface over. A configured user with
@@ -2143,6 +2143,7 @@ function AppShellContent({
{/* Settings owns the full window chrome. Keep this empty header mounted
as the frameless window's drag authority, but remove every control
@@ -2261,7 +2262,7 @@ function AppShellContent({
contentPadding={0}
mobileNav={{ breakpoint: 'none', hasToggle: false }}
aria-hidden={shellObscured ? 'true' : undefined}
- inert={shellObscured ? true : undefined}
+ inert={shellObscured || undefined}
sideNav={
(
@@ -2603,7 +2604,6 @@ function AppShellContent({
}
showOnboardingHero={showOnboardingHero}
onboardingState={onboardingState}
- isOnboardingLoading={isOnboardingLoading}
onOpenSettings={openSettingsSection}
onOpenConnectionDetail={openConnectionDetail}
onAddProvider={openProviderCreate}
diff --git a/apps/desktop/src/renderer/app.tsx b/apps/desktop/src/renderer/app.tsx
index 66daac3a48..d8244285fa 100644
--- a/apps/desktop/src/renderer/app.tsx
+++ b/apps/desktop/src/renderer/app.tsx
@@ -24,13 +24,13 @@ import { AppShell } from './composition/legacy-desktop-region';
import { useAstryxThemeMode } from './astryx-theme-mode';
export function App() {
- // The launch overlay (`#maka-preload` in index.html) hides itself once this
- // commit lands children into #root; this signal is only a backstop for the
- // main-process reveal gate — `ready-to-show` normally beats it. A layout
- // effect is too early: it runs after the DOM commit but before Chromium
- // paints, so two animation frames put the signal after at least one paint
- // of the committed AppShell. `window.maka` is undefined outside Electron
- // (storybook), so guard it.
+ // The launch overlay (`#maka-preload` in index.html) retires on its own once
+ // a surface commits `data-maka-content-ready`; this signal's live job is the
+ // crash-recovery reload, where `ready-to-show` does not re-fire and the
+ // re-hidden window waits on it. A layout effect is too early: it runs after
+ // the DOM commit but before Chromium paints, so two animation frames put the
+ // signal after at least one paint of the committed AppShell. `window.maka`
+ // is undefined outside Electron (storybook), so guard it.
useEffect(() => {
let secondFrame = 0;
const firstFrame = requestAnimationFrame(() => {
diff --git a/apps/desktop/src/renderer/chat-message-surface.tsx b/apps/desktop/src/renderer/chat-message-surface.tsx
index 738cd7f435..c1acf59143 100644
--- a/apps/desktop/src/renderer/chat-message-surface.tsx
+++ b/apps/desktop/src/renderer/chat-message-surface.tsx
@@ -21,7 +21,6 @@ import { useMemo, useState, type ComponentProps, type ReactNode } from 'react';
import { type LlmConnection, type ProviderType } from '@maka/core/llm-connections';
import { type OnboardingState } from '@maka/core/onboarding';
import { type SettingsSection } from '@maka/core/settings';
-import { Skeleton } from '@astryxdesign/core';
import {
ChatView,
ChatViewGoalProjectionConsumer,
@@ -78,7 +77,6 @@ interface ChatMessageSurfaceProps extends Omit<
onTaskReadinessAction?: () => void;
showOnboardingHero: boolean;
onboardingState: OnboardingState | undefined;
- isOnboardingLoading: boolean;
onOpenSettings: (section?: SettingsSection) => void;
onOpenConnectionDetail: (connectionSlug: string) => void;
onAddProvider: (providerType: ProviderType) => void;
@@ -110,7 +108,6 @@ export function ChatMessageSurface({
onTaskReadinessAction,
showOnboardingHero,
onboardingState,
- isOnboardingLoading,
onOpenSettings,
onOpenConnectionDetail,
onAddProvider,
@@ -196,20 +193,6 @@ export function ChatMessageSurface({
onSkip={onSkip}
/>
- ) : isOnboardingLoading ? (
- // Blocks EmptyChatHero from flashing while the first snapshot resolves.
- // Astryx Skeleton bars (DESIGN.md §10) in the ready card's own frame —
- // the hand-drawn static ::before/::after bars this replaces never pulsed,
- // so the first screen a new user saw read as frozen.
- (
-
-
-
)
) : undefined;
return (
diff --git a/apps/desktop/src/renderer/error-boundary.tsx b/apps/desktop/src/renderer/error-boundary.tsx
index 1958e2e7b4..28852891f1 100644
--- a/apps/desktop/src/renderer/error-boundary.tsx
+++ b/apps/desktop/src/renderer/error-boundary.tsx
@@ -166,7 +166,7 @@ export function ErrorBoundaryFallback({
const CopyIcon = copyState === 'copied' ? Check : Clipboard;
return (
-
+
{/* Astryx Card owns the card face: red tint for the destructive
surface, high elevation for the former shadow-modal. The class
keeps only the icon/copy grid geometry. */}
diff --git a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-handoff-overlay.tsx b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-handoff-overlay.tsx
index fbc7e0748b..93766dc7f0 100644
--- a/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-handoff-overlay.tsx
+++ b/apps/desktop/src/renderer/features/runtime-host-management/ui/runtime-host-handoff-overlay.tsx
@@ -65,7 +65,11 @@ export function RuntimeHostHandoffOverlay() {
};
return (
-