You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/21 12:20:44 UTC

(maka) 01/01: fix(desktop): hold the launch logo until a surface commits ready content

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch fix/desktop-launch-hold-until-ready
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 4024c13a770a0a802fd10ca480b1631471851f7d
Author: AstroHan <le...@outlook.com>
AuthorDate: Mon Sep 21 20:18:04 2026 +0800

    fix(desktop): hold the launch logo until a surface commits ready content
    
    #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 <15...@users.noreply.github.com>
---
 .../main/__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 ++++-----
 apps/desktop/src/renderer/chat-message-surface.tsx | 17 -----------
 apps/desktop/src/renderer/error-boundary.tsx       |  2 +-
 .../ui/runtime-host-handoff-overlay.tsx            |  7 ++++-
 .../renderer/features/workhub/ui/workhub-root.tsx  |  2 +-
 apps/desktop/src/renderer/index.html               | 11 +++----
 apps/desktop/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 ` [...]
 
 ## 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({
     <div
       className="appFrame agents-layout-root"
       data-agents-page
+      data-maka-content-ready={!isOnboardingLoading || undefined}
       /* The single writer for sidebar state in the DOM. It sits on the frame,
          above both the chrome strip and the shell, so every rule that keys on
          it (shell-layout.css, sidebar.css) reaches its target as a descendant.
@@ -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}
         />
       </div>
-    ) : 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.
-      (<div
-        className="maka-onboarding-loading"
-        role="status"
-        aria-busy="true"
-        aria-label={copy.loading}
-      >
-        <Skeleton width="52%" height={16} radius="rounded" index={0} />
-        <Skeleton width="78%" height={12} radius="rounded" index={1} />
-      </div>)
     ) : 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 (
-    <div className="maka-error-surface" role="alert" aria-live="assertive">
+    <div className="maka-error-surface" role="alert" aria-live="assertive" data-maka-content-ready>
       {/* 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 (
-    <Dialog isOpen onOpenChange={() => {}} 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. */}
+      <span data-maka-content-ready hidden />
+      <Dialog isOpen onOpenChange={() => {}} purpose="required" width={480}>
       <Layout
         header={(
           <DialogHeader
@@ -111,5 +115,6 @@ export function RuntimeHostHandoffOverlay() {
         )}
       />
     </Dialog>
+    </>
   );
 }
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
index 91344a3a34..3480d4b193 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
@@ -277,7 +277,7 @@ export function WorkHubRoot() {
   return (
     <WorkHubHighlightContext.Provider value={highlight}>
     <WorkHubHueProvider sessionIds={[...tasks.map((task) => task.target.sessionId), ...delegatedSessionIds]}>
-    <section ref={surface} data-progress={progress} data-progress-editing={editingProgress} className="workHubLive workhub-surface" data-placement={presentation?.placement ?? 'docked'} data-conversation-expanded={showConversation} aria-label={t.title}>
+    <section ref={surface} data-progress={progress} data-progress-editing={editingProgress} className="workHubLive workhub-surface" data-maka-content-ready data-placement={presentation?.placement ?? 'docked'} data-conversation-expanded={showConversation} aria-label={t.title}>
       {!floating && presentation?.workbar && <WorkbarEdgeToggle label={getShellCopy(locale).chrome[presentation.workbar.collapsed ? 'expandWorkbar' : 'collapseWorkbar']} {...presentation.workbar} onToggle={() => call(services.presentation.toggleWorkbar())} />}
       {progress && <WorkHubProgressCard ref={progressHeader} request={presentation.progressRequest!} control={control} liveTurn={controller.liveTurn} messages={transcript.messages} busy={Boolean(controller.activeTurn) || controller.sending} onOpen={() => {
         setConversationExpanded(true);
diff --git a/apps/desktop/src/renderer/index.html b/apps/desktop/src/renderer/index.html
index 84c6a80f95..72eb85b455 100644
--- a/apps/desktop/src/renderer/index.html
+++ b/apps/desktop/src/renderer/index.html
@@ -33,10 +33,11 @@
        * CSS loads: a theme-matched background with the centered wordmark, in
        * the spirit of Codex's launch screen rather than a fake app skeleton.
        * It lives outside #root as a fixed overlay so React mounts underneath
-       * it; dismissal is pure CSS — once #root has children the overlay fades
-       * out, whichever surface (desktop, WorkHub) committed the frame. A
-       * zero-duration animation arms the same hidden state at 8s so a wedged
-       * bundle can never strand the logo over a live window.
+       * it; dismissal is pure CSS — the overlay fades once a surface commits
+       * content marked `data-maka-content-ready`: the shell's first settled
+       * frame, the WorkHub composer, a pending Host decision, or the error
+       * surface. A zero-duration animation arms the same hidden state at 8s
+       * so a wedged bundle can never strand the logo over a live window.
        *
        * Colors are hardcoded (not CSS variables) because maka-tokens.css
        * (70KB) hasn't loaded yet. The background must equal the BrowserWindow
@@ -63,7 +64,7 @@
         transition: opacity 160ms ease-out, visibility 0s linear 160ms;
         animation: maka-preload-timeout 0s linear 8s forwards;
       }
-      body:has(#root > *) > .maka-preload {
+      body:has(#root [data-maka-content-ready]) > .maka-preload {
         opacity: 0;
         visibility: hidden;
         pointer-events: none;
diff --git a/apps/desktop/src/renderer/styles/onboarding.css b/apps/desktop/src/renderer/styles/onboarding.css
index 79ea7cbf2b..b111dde8d5 100644
--- a/apps/desktop/src/renderer/styles/onboarding.css
+++ b/apps/desktop/src/renderer/styles/onboarding.css
@@ -79,23 +79,6 @@
   display: none;
 }
 
-/* The snapshot slot blocks the ordinary empty-chat hero while main resolves
-   onboarding state. The frame keeps the ready card's geometry; the bars inside
-   are Astryx Skeletons (DESIGN.md §10) rather than hand-drawn statics. */
-.maka-onboarding-loading {
-  display: grid;
-  align-content: start;
-  gap: var(--space-4);
-  width: min(460px, calc(100% - 32px));
-  min-height: 196px;
-  margin: auto;
-  padding: var(--space-8) var(--space-6);
-  overflow: hidden;
-  border: var(--border-width-hairline) solid var(--border-strong);
-  border-radius: var(--radius-modal);
-  background: var(--foreground-5);
-}
-
 .maka-onboarding-surface {
   display: grid;
   place-items: center;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org