Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@
"react": 1
},
"importSpecifiers": 98,
"nonTriviaTokens": 12731
"nonTriviaTokens": 12730
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 0,
Expand Down Expand Up @@ -1085,7 +1085,6 @@
"./features/conversation/index.js": 1,
"./locales/shell-copy": 1,
"./onboarding-hero": 1,
"@astryxdesign/core": 1,
"@maka/ui": 1,
"react": 1
}
Expand Down
35 changes: 35 additions & 0 deletions apps/desktop/src/main/__tests__/main-startup-lifetime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
});
4 changes: 2 additions & 2 deletions apps/desktop/src/main/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 7 additions & 7 deletions apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -2190,7 +2191,7 @@ function AppShellContent({
<header
className="maka-window-titlebar"
aria-hidden={shellObscured ? 'true' : undefined}
inert={hasModalOpen ? true : undefined}
inert={hasModalOpen || undefined}
>
{/* Settings owns the full window chrome. Keep this empty header mounted
as the frameless window's drag authority, but remove every control
Expand Down Expand Up @@ -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={
<ModuleHub.ModuleHubScheduledTasksBoundary
render={(scheduledTasks) => (
Expand Down Expand Up @@ -2603,7 +2604,6 @@ function AppShellContent({
}
showOnboardingHero={showOnboardingHero}
onboardingState={onboardingState}
isOnboardingLoading={isOnboardingLoading}
onOpenSettings={openSettingsSection}
onOpenConnectionDetail={openConnectionDetail}
onAddProvider={openProviderCreate}
Expand Down
14 changes: 7 additions & 7 deletions apps/desktop/src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
17 changes: 0 additions & 17 deletions apps/desktop/src/renderer/chat-message-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -110,7 +108,6 @@ export function ChatMessageSurface({
onTaskReadinessAction,
showOnboardingHero,
onboardingState,
isOnboardingLoading,
onOpenSettings,
onOpenConnectionDetail,
onAddProvider,
Expand Down Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,5 +115,6 @@ export function RuntimeHostHandoffOverlay() {
)}
/>
</Dialog>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 6 additions & 5 deletions apps/desktop/src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
17 changes: 0 additions & 17 deletions apps/desktop/src/renderer/styles/onboarding.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion docs/astryx-surface-file-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
| `apps/desktop/src/renderer/application/contracts/workbar-edge-toggle.tsx` | shell-chrome-or-panel | Button | aligned — uses Astryx (Button) | aligned |
| `apps/desktop/src/renderer/cascade-layers.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned |
| `apps/desktop/src/renderer/chat-composer-region.tsx` | shell-chrome-or-panel | Banner, Button | aligned — uses Astryx (Banner, Button) | aligned |
| `apps/desktop/src/renderer/chat-message-surface.tsx` | shell-chrome-or-panel | Skeleton | aligned — uses Astryx (Skeleton) | aligned |
| `apps/desktop/src/renderer/chat-message-surface.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `apps/desktop/src/renderer/chat-recovery-notice.tsx` | shell-chrome-or-panel | Banner, Button | aligned — uses Astryx (Banner, Button) | aligned |
| `apps/desktop/src/renderer/composer-mentions.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
| `apps/desktop/src/renderer/composition/desktop-feature-services.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
Expand Down