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 ( - {}} 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. */} +