From 4024c13a770a0a802fd10ca480b1631471851f7d Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 21 Sep 2026 20:18:04 +0800 Subject: [PATCH 1/3] fix(desktop): hold the launch logo until a surface commits ready content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5494 removed the pre-mount onboarding prefetch, so AppShell's first commit lands before any data resolves. The launch overlay dismissed on that commit (`#root > *`), then the shell rendered the `.maka-onboarding-loading` skeleton card while the snapshot pull waited on the Host boot gate — the logo and the skeleton were two loading surfaces over the same gap, and the user saw logo → skeleton → app. Collapse them into one: the overlay now fades only when a surface commits `data-maka-content-ready`. The shell sets it once the first snapshot settles (the exact boundary where the skeleton used to disappear), the WorkHub composer sets it at mount, and the handoff dialog and error surface set it immediately since they cannot wait on the data they gate. With the overlay covering the gap, the skeleton card is deleted along with its styles, prop, and smoke-diagnostic selector. The 8s wedged-bundle timeout is unchanged. Generated-by: Devin Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../__tests__/main-startup-lifetime.test.ts | 35 +++++++++++++++++++ apps/desktop/src/main/main-window.ts | 4 +-- apps/desktop/src/renderer/README.md | 4 +-- apps/desktop/src/renderer/app-shell.tsx | 10 +++--- apps/desktop/src/renderer/app.tsx | 14 ++++---- .../src/renderer/chat-message-surface.tsx | 17 --------- apps/desktop/src/renderer/error-boundary.tsx | 2 +- .../ui/runtime-host-handoff-overlay.tsx | 7 +++- .../features/workhub/ui/workhub-root.tsx | 2 +- apps/desktop/src/renderer/index.html | 11 +++--- .../src/renderer/styles/onboarding.css | 17 --------- 11 files changed, 65 insertions(+), 58 deletions(-) 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..32aa3b0367 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({
{ 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 ( - {}} purpose="required" width={480}> + <> + {/* Retires the launch overlay: the data it waits on may be blocked + behind this very decision, so the dialog cannot wait for it. */} +