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/12 10:54:56 UTC

(maka) branch main updated: test(desktop): reduce Electron coverage from 41 to 34 tests (#5228)

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

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new d92fed919a test(desktop): reduce Electron coverage from 41 to 34 tests (#5228)
d92fed919a is described below

commit d92fed919a0a63daa1e576102c723d882d249447
Author: AstroHan <le...@outlook.com>
AuthorDate: Sat Sep 12 18:54:51 2026 +0800

    test(desktop): reduce Electron coverage from 41 to 34 tests (#5228)
    
    * test(desktop): move renderer checks out of Electron
    
    Generated-by: OpenAI Codex
    
    * docs(desktop): define Electron test admission rules
    
    Generated-by: OpenAI Codex
    
    * docs(storybook): add scoped test tier guidance
    
    Generated-by: OpenAI Codex
    
    * fix(desktop): remove unused settings modal re-exports
    
    Generated-by: OpenAI Codex
---
 apps/desktop/e2e-budget.json                       |  12 +-
 apps/desktop/e2e/AGENTS.md                         |  72 ++++++++
 apps/desktop/e2e/session-workbar.spec.ts           |  22 +--
 apps/desktop/e2e/settings.spec.ts                  | 161 ++---------------
 apps/desktop/e2e/transcript-scroll-cost.spec.ts    | 199 ++-------------------
 apps/desktop/renderer-architecture.json            |   4 +-
 apps/desktop/src/preload/preload.ts                |   7 +-
 apps/desktop/src/renderer/app-shell-overlays.tsx   |  70 +++-----
 .../src/renderer/settings/settings-modal.tsx       |   3 -
 apps/desktop/stories/AGENTS.md                     |  48 +++++
 apps/desktop/stories/FIDELITY.md                   |   6 +-
 apps/desktop/stories/app-shell.stories.tsx         | 106 ++++++++++-
 .../stories/settings/settings-pages.stories.tsx    |  51 +++++-
 packages/ui/stories/AGENTS.md                      |  41 +++++
 14 files changed, 381 insertions(+), 421 deletions(-)

diff --git a/apps/desktop/e2e-budget.json b/apps/desktop/e2e-budget.json
index fa464f77f1..3d49a2a75d 100644
--- a/apps/desktop/e2e-budget.json
+++ b/apps/desktop/e2e-budget.json
@@ -42,12 +42,12 @@
       "electron": "SQLite admission crosses preload and main, survives both renderer reload and complete Electron restart, and cached history must render when the live IPC endpoint is unavailable"
     },
     "session-workbar.spec.ts": {
-      "tests": 6,
-      "electron": "Git changes re-read on window focus, terminal PTY ownership across Sessions, Side Chat's fork lifecycle, a first send that has to reach the Host, and per-Session collapse persisted across a renderer reload; the composer-usage test is renderer-only and rides along on those windows until app-shell.tsx's composer-to-workbar wiring has a story host"
+      "tests": 5,
+      "electron": "Git changes re-read on native window focus; terminal PTY ownership changes across Sessions; Side Chat owns a Host fork lifecycle; first send reaches the Host; the composer usage entry opens a workbar whose per-Session visibility survives renderer reload."
     },
     "settings.spec.ts": {
-      "tests": 5,
-      "electron": "Code Mode persistence crosses renderer/preload/main/Host settings IPC; the preload makaE2eLatch holds the settings chunk mid-load, and the rename it commits is a Host write; the workbar-chrome test rides along on that window and would move to a story the day app-shell.tsx's settings wiring has one"
+      "tests": 2,
+      "electron": "Code Mode reads back Host settings through IPC; opening Settings commits a pending title rename to the Host, with workbar hide/restore asserted in that same Session."
     },
     "sidebar-project-reload.spec.ts": {
       "tests": 1,
@@ -66,8 +66,8 @@
       "electron": "observation seeding, reconnect and settle are Host subscriptions surviving a renderer remount"
     },
     "transcript-scroll-cost.spec.ts": {
-      "tests": 4,
-      "electron": "the perf budget is measured from CDP wheel input and the browser's own render skipping, and reader displacement only exists against a real layout"
+      "tests": 1,
+      "electron": "One traversal exercises real Host transcript paging through preload/IPC, bounded retained ranges, reader displacement when pages install, and a return-to-tail Host read. Chromium containment and fixture motion run in the TranscriptRenderCost story."
     },
     "workhub-layout.spec.ts": {
       "tests": 2,
diff --git a/apps/desktop/e2e/AGENTS.md b/apps/desktop/e2e/AGENTS.md
new file mode 100644
index 0000000000..d919de0885
--- /dev/null
+++ b/apps/desktop/e2e/AGENTS.md
@@ -0,0 +1,72 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Electron test admission
+
+Before adding or extending a test here, choose the lowest tier that can expose
+the actual defect. Apply this to each behavioral assertion, not just each file.
+Existing tests and entries in `../e2e-budget.json` are not exemptions. See #4761,
+#4877 and #4892 for the migration history.
+
+1. Use existing unit, component or integration tests for state, event ordering,
+   request routing, storage and recovery. Desktop renderer tests already run
+   from `../src/main/__tests__` through `node --test`; React tests use the existing
+   fake DOM. The directory name does not restrict them to main-process code.
+   Shared UI tests live in `../../../packages/ui/src/__tests__`.
+2. Use Storybook or a focused browser test when the assertion needs Chromium:
+   layout, scrolling, selection, focus, animation, viewport or theme behavior.
+   Reuse `../stories`, `../../../packages/ui/stories` and
+   [the fidelity convention](../stories/FIDELITY.md).
+3. Use Electron only when a lower tier would miss a concrete Electron boundary:
+   native window/input behavior, Electron preload/main integration, or a
+   cross-process persistence or lifecycle failure requiring the actual app.
+
+## Prove the boundary
+
+- State beside each test which Electron-owned mechanism it verifies and what
+  defect a lower-tier test would miss. A call used only to prepare the scenario
+  is not that mechanism. Creating a Host Session does not make a focus test an
+  IPC test; reading a Host snapshot does not by itself prove Electron is needed.
+- Real wheel input, CDP, geometry, localStorage, page reload, or a preload test
+  latch alone do not establish an Electron requirement. Trace the owner of the
+  behavior being asserted. Node integration tests can also exercise real Host
+  and storage boundaries.
+- Do not justify a renderer-only assertion by another test in the same file,
+  an already-open window, or an unchanged test count. Keep Electron journeys
+  focused on their necessary boundaries; move independent renderer contracts.
+- Search existing lower-tier coverage before adding a replacement. Extend the
+  actual component/controller/service seam; do not copy product logic into a
+  second shell or introduce a global render-completion protocol for tests.
+
+## Migrate and verify
+
+- Establish that the replacement detects the original defect, preferably by
+  reverting the relevant behavior or a targeted mutation. Verify the behavioral
+  failure, not merely an import/type error or the absence of a new helper.
+- Delete replaced E2E cases, duplicate assertions and unreferenced fixture hooks
+  in the same PR. Preserve any independent native or cross-process protection.
+- Do not raise timeouts, add retries, or weaken assertions to make a migration
+  pass. Diagnose a red replacement before deciding it is a harness problem.
+- Update `../e2e-budget.json` with counts and concrete boundary reasons, then run
+  `npm run check:e2e-budget` from the repository root. This checks inventory
+  consistency; a green result is not proof of correct tier selection.
+- Build before running compiled tests or Storybook smoke. Run Electron tests
+  from `apps/desktop` because the fixtures use that working directory. Report
+  transferred/lost protection and fixture launches or traversals removed,
+  separately from the number of test declarations.
diff --git a/apps/desktop/e2e/session-workbar.spec.ts b/apps/desktop/e2e/session-workbar.spec.ts
index c7c2124b0b..8e9b909230 100644
--- a/apps/desktop/e2e/session-workbar.spec.ts
+++ b/apps/desktop/e2e/session-workbar.spec.ts
@@ -53,22 +53,6 @@ async function createSession(page: Page, prompt: string) {
   return { composer, sessionId: sessionId!, sidebar };
 }
 
-test('the composer usage action opens Task trace in the right workbar', async ({
-  accessibilityNarrativeWindow: page,
-}) => {
-  const action = page.getByRole('button', { name: '打开用量追踪' });
-  await expect(action).toBeVisible();
-
-  await action.click();
-
-  const rightPanel = page.locator(
-    '.maka-session-workbar-panel[data-overlay][data-placement="right"]',
-  );
-  await expect(
-    rightPanel.locator('[data-maka-contract="session-inspector"]'),
-  ).toBeVisible();
-});
-
 test('right workbar visibility belongs to each Session and survives reload', async ({
   window: page,
 }) => {
@@ -79,8 +63,10 @@ test('right workbar visibility belongs to each Session and survives reload', asy
     .getByRole('list', { name: '打开工具' })
     .getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ })
     .click();
-  await page.getByRole('button', { name: '打开或关闭工作栏的面' }).click();
-  await page.getByRole('menu').getByRole('menuitem', { name: '追踪', exact: true }).click();
+  await page.getByRole('button', { name: '打开用量追踪' }).click();
+  await expect(page.locator(
+    '.maka-session-workbar-panel[data-overlay][data-placement="right"] [data-maka-contract="session-inspector"]',
+  )).toBeVisible();
   await expect(panel).toBeVisible();
   await first.sidebar.getByRole('button', { name: '新任务', exact: true }).click();
   const second = await createSession(page, 'second workbar owner');
diff --git a/apps/desktop/e2e/settings.spec.ts b/apps/desktop/e2e/settings.spec.ts
index 412e035844..67f04c1fae 100644
--- a/apps/desktop/e2e/settings.spec.ts
+++ b/apps/desktop/e2e/settings.spec.ts
@@ -19,13 +19,6 @@
 
 import { awaitSendReady, COMPOSER_INPUT, ensureSidebarExpanded, expect, test } from './fixtures';
 
-interface SettingsChunkLatchWindow extends Window {
-  makaE2eLatch?: {
-    arm(key: 'settings.chunk'): void;
-    release(key: 'settings.chunk'): void;
-  };
-}
-
 test('Code Mode persists as a global setting after reopening settings', async ({ window: page }, testInfo) => {
   await ensureSidebarExpanded(page);
   await page.getByRole('button', { name: '设置' }).click();
@@ -48,84 +41,12 @@ test('Code Mode persists as a global setting after reopening settings', async ({
   await expect.poll(() => page.evaluate(async () => (await window.maka.settings.get()).chatDefaults.codeModeEnabled === true)).toBe(false);
 });
 
-test('Settings loading surface owns unmodified Escape', async ({ window: page }) => {
-  const latchInstalled = await page.evaluate(() => {
-    const e2eLatch = (window as unknown as SettingsChunkLatchWindow).makaE2eLatch;
-    e2eLatch?.arm('settings.chunk');
-    return e2eLatch !== undefined;
-  });
-  expect(latchInstalled, 'the preload E2E latch is installed').toBe(true);
-
-  try {
-    await ensureSidebarExpanded(page);
-    await page.getByRole('button', { name: '设置' }).click();
-
-    const loadingSurface = page.locator('.maka-lazy-fallback');
-    await expect(loadingSurface).toBeVisible();
-
-    for (const modifier of ['ctrlKey', 'metaKey', 'altKey'] as const) {
-      const wasNotPrevented = await page.evaluate((key) =>
-        window.dispatchEvent(
-          new KeyboardEvent('keydown', {
-            key: 'Escape',
-            [key]: true,
-            bubbles: true,
-            cancelable: true,
-          }),
-        ), modifier);
-      expect(wasNotPrevented).toBe(true);
-      await expect(loadingSurface).toBeVisible();
-    }
-
-    const wasNotPrevented = await page.evaluate(() =>
-      window.dispatchEvent(
-        new KeyboardEvent('keydown', {
-          key: 'Escape',
-          bubbles: true,
-          cancelable: true,
-        }),
-      ),
-    );
-    expect(wasNotPrevented).toBe(false);
-    await expect(page.locator('.settingsModal')).toHaveCount(0);
-  } finally {
-    await page.evaluate(() =>
-      (window as unknown as SettingsChunkLatchWindow).makaE2eLatch?.release(
-        'settings.chunk',
-      ),
-    );
-  }
-});
-
 test('opening settings commits an active titlebar rename', async ({ window: page }) => {
   const composer = page.locator(COMPOSER_INPUT);
   await composer.fill('create a session for settings rename');
   await awaitSendReady(page);
   await composer.press('Enter');
 
-  const identity = page.locator('[data-maka-contract="titlebar-identity"]');
-  await expect(identity).toBeVisible();
-  await page.getByRole('button', { name: '展开侧边栏' }).click();
-  await identity.getByRole('button', { name: /重命名任务/ }).click();
-  await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');
-
-  // Programmatic activation preserves input focus, matching the macOS
-  // application-menu command that opens Settings before Chromium can blur it.
-  await page.getByRole('button', { name: '设置' }).evaluate((button) => button.click());
-  await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();
-  await page.keyboard.press('Escape');
-
-  await expect(identity).toContainText('renamed before settings');
-});
-
-test('settings hides expanded workbar chrome and restores it on close', async ({
-  window: page,
-}) => {
-  const composer = page.locator(COMPOSER_INPUT);
-  await composer.fill('create a session with an expanded workbar');
-  await awaitSendReady(page);
-  await composer.press('Enter');
-
   await page.getByRole('button', { name: '展开任务工作栏' }).click();
   const workbar = page.locator('.maka-session-workbar[data-placement="right"]');
   const workbarToolbar = workbar.getByRole('toolbar', { name: '任务工作栏标签' });
@@ -140,79 +61,23 @@ test('settings hides expanded workbar chrome and restores it on close', async ({
   const openFaceTab = workbarToolbar.getByRole('tab', { name: '变更' });
   await expect(openFaceTab).toBeVisible();
 
-  await ensureSidebarExpanded(page);
-  await page.getByRole('button', { name: '设置' }).click();
+  const identity = page.locator('[data-maka-contract="titlebar-identity"]');
+  await expect(identity).toBeVisible();
+  await page.getByRole('button', { name: '展开侧边栏' }).click();
+  await identity.getByRole('button', { name: /重命名任务/ }).click();
+  await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');
+
+  // Programmatic activation preserves input focus, matching the macOS
+  // application-menu command that opens Settings before Chromium can blur it.
+  await page.getByRole('button', { name: '设置' }).evaluate((button) => button.click());
   await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();
   await expect(workbar).not.toBeVisible();
-
   await page.keyboard.press('Escape');
   await expect(workbarToolbar).toBeVisible();
   await expect(openFaceTab).toBeVisible();
-});
-
-test('reopening settings keeps the last-ready General page stable while refreshing', async ({
-  window: page,
-}) => {
-  await ensureSidebarExpanded(page);
-  await page.getByRole('button', { name: '设置' }).click();
-  const settings = page.locator('.settingsSurface');
-  await page.getByRole('button', { name: '通用', exact: true }).click();
-  await expect(settings.getByRole('textbox', { name: '助手语气偏好' })).toBeEnabled();
-  await expect(settings.getByRole('button', { name: '默认模型' })).toBeEnabled();
-
-  await page.keyboard.press('Escape');
-  await expect(settings).toHaveCount(0);
-  await expect(page.getByRole('button', { name: '设置' })).toBeVisible();
 
-  await page.evaluate(() => {
-    const state = {
-      sawLoadingWarning: false,
-      sawGeneralWithoutReadyHostControls: false,
-    };
-    const inspect = () => {
-      const surface = document.querySelector('.settingsSurface');
-      const main = surface?.querySelector('main, [role="main"]');
-      if (!surface || !main) return;
-      state.sawLoadingWarning ||= Array.from(
-        surface.querySelectorAll('[role="alert"]'),
-      ).some((banner) => banner.textContent?.includes('正在加载设置') === true);
-      const defaultModelReady = Array.from(main.querySelectorAll('*')).some(
-        (element) =>
-          element.children.length === 0 &&
-          element.textContent?.trim() === '默认模型' &&
-          Boolean(element.closest('.astryx-item')?.querySelector('button')),
-      );
-      state.sawGeneralWithoutReadyHostControls ||=
-        main.querySelector('textarea') === null || !defaultModelReady;
-    };
-    const observer = new MutationObserver(inspect);
-    observer.observe(document.body, { childList: true, subtree: true, characterData: true });
-    Object.assign(window, {
-      __makaSettingsReopenProbe: {
-        finish() {
-          inspect();
-          observer.disconnect();
-          return state;
-        },
-      },
-    });
-  });
-
-  await page.getByRole('button', { name: '设置' }).click();
-  await expect(settings.getByRole('textbox', { name: '助手语气偏好' })).toBeEnabled();
-  await expect(settings.getByRole('button', { name: '默认模型' })).toBeEnabled();
-
-  const probe = await page.evaluate(() => {
-    const target = window as typeof window & {
-      __makaSettingsReopenProbe: {
-        finish(): {
-          sawLoadingWarning: boolean;
-          sawGeneralWithoutReadyHostControls: boolean;
-        };
-      };
-    };
-    return target.__makaSettingsReopenProbe.finish();
-  });
-  expect(probe.sawLoadingWarning).toBe(false);
-  expect(probe.sawGeneralWithoutReadyHostControls).toBe(false);
+  await expect(identity).toContainText('renamed before settings');
+  await expect.poll(() => page.evaluate(async () =>
+    (await window.maka.sessions.list()).some((session) => session.name === 'renamed before settings'),
+  )).toBe(true);
 });
diff --git a/apps/desktop/e2e/transcript-scroll-cost.spec.ts b/apps/desktop/e2e/transcript-scroll-cost.spec.ts
index 388ad4546b..4b5efff725 100644
--- a/apps/desktop/e2e/transcript-scroll-cost.spec.ts
+++ b/apps/desktop/e2e/transcript-scroll-cost.spec.ts
@@ -62,12 +62,6 @@ const BOUNDARY_DISPLACEMENT_MAX_PX = 40;
 
 declare global {
   interface Window {
-    __makaTranscriptCost?: {
-      transitionRuns: number;
-      animationStarts: number;
-      skipped: WeakSet<Element>;
-      skippedCount: number;
-    };
     __makaTranscriptDisplacement?: {
       boundaries: TranscriptBoundary[];
       record(on: boolean): void;
@@ -123,47 +117,6 @@ async function wheel(
   ));
 }
 
-/**
- * Count every transition and animation the page starts, and track which Turns
- * the browser is currently skipping.
- *
- * `contentvisibilityautostatechange` rather than
- * `checkVisibility({ contentVisibilityAuto: true })`: the flag that method
- * reads is updated during rendering, so a synchronous call right after a
- * scroll reports every Turn visible even when the browser is skipping most of
- * them. Measured on this fixture, the method returned 0 skipped Turns in every
- * position the event reported between 1 and 8.
- */
-async function observe(page: Page): Promise<void> {
-  await page.evaluate(() => {
-    const state = {
-      transitionRuns: 0,
-      animationStarts: 0,
-      skipped: new WeakSet<Element>(),
-      skippedCount: 0,
-    };
-    window.__makaTranscriptCost = state;
-    document.addEventListener('transitionrun', () => { state.transitionRuns += 1; }, true);
-    document.addEventListener('animationstart', () => { state.animationStarts += 1; }, true);
-    const bound = new WeakSet<Element>();
-    const bind = (): void => {
-      for (const turn of document.querySelectorAll('.maka-transcript-turn')) {
-        if (bound.has(turn)) continue;
-        bound.add(turn);
-        turn.addEventListener('contentvisibilityautostatechange', (event) => {
-          const skipped = (event as Event & { skipped: boolean }).skipped;
-          if (skipped === state.skipped.has(turn)) return;
-          if (skipped) state.skipped.add(turn);
-          else state.skipped.delete(turn);
-          state.skippedCount += skipped ? 1 : -1;
-        });
-      }
-    };
-    bind();
-    new MutationObserver(bind).observe(document.body, { childList: true, subtree: true });
-  });
-}
-
 /**
  * Watch every frame for a change in the mounted range, and measure what that
  * change did to the reader.
@@ -289,30 +242,6 @@ async function displacement(page: Page): Promise<readonly TranscriptBoundary[]>
   });
 }
 
-interface CostSample {
-  transitionRuns: number;
-  animationStarts: number;
-  unfinished: number;
-  skippedTurns: number;
-  mountedTurns: number;
-}
-
-async function sample(page: Page): Promise<CostSample> {
-  return page.evaluate(() => {
-    const state = window.__makaTranscriptCost;
-    if (!state) throw new Error('the transcript cost observer is missing');
-    return {
-      transitionRuns: state.transitionRuns,
-      animationStarts: state.animationStarts,
-      unfinished: document.body
-        .getAnimations({ subtree: true })
-        .filter((animation) => animation.playState !== 'finished').length,
-      skippedTurns: state.skippedCount,
-      mountedTurns: document.querySelectorAll('[data-turn-id]').length,
-    };
-  });
-}
-
 /**
  * A transcript opened at its tail keeps fetching older history until two
  * screens of it sit above the reader, and trims what falls outside the band it
@@ -363,124 +292,15 @@ async function returnToLatest(page: Page): Promise<void> {
   await returnLatest.click();
 }
 
-/**
- * The fixture's own motion contract, asserted as the count it is.
- *
- * `[data-maka-e2e-fixture]` collapses motion so a fixture render does not
- * depend on the millisecond it settles. It used to do that with
- * `transition-duration: 0.01ms`, which is not "no transition": the initial
- * `transition-property` is `all`, so every element kept a live transition on
- * every animatable property and fired transitionrun/start/end on every style
- * recalculation — measured here, ~1,200 transitions for one sweep over ten
- * mounted Turns, and tens of thousands over a long one. Every timing number
- * the replaced suite reported was mostly that.
- *
- * Nothing downstream can measure the product while the harness generates work
- * of its own, so the harness asserts zero.
- */
-test('a scroll through the fixture transcript starts no transitions', async ({
-  promptRailWindow: page,
-}) => {
-  await page.setViewportSize({ width: 1_000, height: 700 });
-  await expect(page.locator(`[data-turn-id="turn-prompt-rail-${PROMPT_RAIL_PROMPT_COUNT}"]`))
-    .toHaveCount(1);
-  const cdp = await page.context().newCDPSession(page);
-  await observe(page);
-  await moveToTail(page);
-  await wheel(page, cdp, { ticks: 40, deltaY: -120 });
-  await wheel(page, cdp, { ticks: 40, deltaY: 120 });
-
-  const cost = await sample(page);
-  expect(cost.transitionRuns).toBe(0);
-  expect(cost.animationStarts).toBe(0);
-  // The reason the declaration exists: a fixture render is a settled state,
-  // never an entry frame. `none` serves that strictly better than a near-zero
-  // duration did — that one left transitions still running at sample time.
-  expect(cost.unfinished).toBe(0);
-});
-
-/**
- * Containment is engaging at all. A `content-visibility: auto` that stops
- * skipping — a Turn that gains a property forcing layout, a container query,
- * an ancestor that breaks the containment chain — costs nothing that a timing
- * threshold would notice on a ten-Turn range, and everything on a long one.
- */
-test('the browser skips the Turns the reader has scrolled past', async ({
-  promptRailWindow: page,
-}) => {
-  await page.setViewportSize({ width: 1_000, height: 700 });
-  await expect(page.locator(`[data-turn-id="turn-prompt-rail-${PROMPT_RAIL_PROMPT_COUNT}"]`))
-    .toHaveCount(1);
-  const cdp = await page.context().newCDPSession(page);
-  await observe(page);
-  await moveToTail(page);
-  // Two viewports up and back: enough for the Turns at the far end of the
-  // mounted range to leave the browser's relevance margin in both directions.
-  await wheel(page, cdp, { ticks: 20, deltaY: -120 });
-  await wheel(page, cdp, { ticks: 20, deltaY: 120 });
-
-  expect((await sample(page)).skippedTurns).toBeGreaterThan(0);
-});
-
-/**
- * The bound the Desktop transcript is built on: paging back through a history
- * far longer than the retained band mounts a bounded number of Turns, not a
- * growing one. Sampled at every page rather than only at the end, because the
- * regression is a range that grows while the reader travels and is only trimmed
- * once they stop.
- */
-test('paging back through the whole history keeps the mounted range bounded', async ({
-  promptRailWindow: page,
-}) => {
-  test.setTimeout(120_000);
-  await page.setViewportSize({ width: 1_000, height: 700 });
-  await expect(page.locator(`[data-turn-id="turn-prompt-rail-${PROMPT_RAIL_PROMPT_COUNT}"]`))
-    .toHaveCount(1);
-  const cdp = await page.context().newCDPSession(page);
-  const turns = page.locator('[data-turn-id]');
-  let mountedMax = 0;
-  let pages = 0;
-
-  for (let iteration = 0; iteration < PROMPT_RAIL_PROMPT_COUNT; iteration += 1) {
-    const firstBefore = await turns.first().getAttribute('data-turn-id');
-    if (firstBefore === 'turn-prompt-rail-1') break;
-    // The product asks for history on an upward wheel near the start, so the
-    // gesture that pages is the gesture a reader makes. How many gestures it
-    // takes is how tall the resident range happens to be, which is not what
-    // this test is about — keep scrolling until the range moves.
-    await expect
-      .poll(async () => {
-        await wheel(page, cdp, { ticks: 12, deltaY: -120 });
-        return turns.first().getAttribute('data-turn-id');
-      })
-      .not.toBe(firstBefore);
-    pages += 1;
-    mountedMax = Math.max(mountedMax, await turns.count());
-  }
-
-  expect(pages).toBeGreaterThan(0);
-  await expect(turns.first()).toHaveAttribute('data-turn-id', 'turn-prompt-rail-1');
-  expect(mountedMax).toBeLessThanOrEqual(MOUNTED_TURNS_MAX);
-
-  // Coming back from the far end reads the tail page and rebuilds the window
-  // around it, so it is slower than the scrolling above. The suite's 10s expect
-  // timeout is sized for UI that is already on screen, and this step measured
-  // past it on a loaded CI runner.
-  await returnToLatest(page);
-  await expect(page.locator(`[data-turn-id="turn-prompt-rail-${PROMPT_RAIL_PROMPT_COUNT}"]`))
-    .toHaveCount(1, { timeout: 30_000 });
-  expect(await turns.count()).toBeLessThanOrEqual(MOUNTED_TURNS_MAX);
-});
-
 /**
  * The scenario #5163 was reported from: quit Desktop, start it again, open a
  * long Session, and scroll upward through history without stopping. The reader
  * perceives stalls or jumps around range boundaries.
  *
- * The tests above establish that paging works and stays bounded. Neither says
+ * A mounted-range bound alone does not say
  * where the reader ended up while a page was installing, which is the whole of
  * what that report is about. This one measures it: every frame the mounted
- * range changes, whatever Turn the reader can still see must hold its document
+ * range changes, whatever Turn the reader can still see must hold its viewport
  * position.
  *
  * Displacement in pixels rather than frame timings on purpose — see this file's
@@ -489,7 +309,7 @@ test('paging back through the whole history keeps the mounted range bounded', as
  * content out from under the reader) and only one of them can be asserted
  * without a clock.
  */
-test('paging back never moves the reader at a range boundary', async ({
+test('Host history paging stays bounded, preserves the reader and returns to latest', async ({
   promptRailWindow: page,
 }) => {
   test.setTimeout(120_000);
@@ -500,6 +320,8 @@ test('paging back never moves the reader at a range boundary', async ({
   const turns = page.locator('[data-turn-id]');
   await moveToTail(page);
   await observeDisplacement(page);
+  let pages = 0;
+  let mountedMax = await turns.count();
 
   for (let iteration = 0; iteration < PROMPT_RAIL_PROMPT_COUNT; iteration += 1) {
     const firstBefore = await turns.first().getAttribute('data-turn-id');
@@ -516,8 +338,14 @@ test('paging back never moves the reader at a range boundary', async ({
         return turns.first().getAttribute('data-turn-id');
       })
       .not.toBe(firstBefore);
+    pages += 1;
+    mountedMax = Math.max(mountedMax, await turns.count());
   }
 
+  expect(pages).toBeGreaterThan(0);
+  await expect(turns.first()).toHaveAttribute('data-turn-id', 'turn-prompt-rail-1');
+  expect(mountedMax).toBeLessThanOrEqual(MOUNTED_TURNS_MAX);
+
   const boundaries = await displacement(page);
   // The probe has to have seen the thing it measures: a run that paged nothing,
   // or one where every boundary replaced the range wholesale and carried no
@@ -528,4 +356,9 @@ test('paging back never moves the reader at a range boundary', async ({
   const displaced = boundaries.filter((boundary) => boundary.worstPx > BOUNDARY_DISPLACEMENT_MAX_PX);
   expect(displaced, `range boundaries moved the reader: ${JSON.stringify(displaced)}`)
     .toEqual([]);
+
+  await returnToLatest(page);
+  await expect(page.locator(`[data-turn-id="turn-prompt-rail-${PROMPT_RAIL_PROMPT_COUNT}"]`))
+    .toHaveCount(1, { timeout: 30_000 });
+  expect(await turns.count()).toBeLessThanOrEqual(MOUNTED_TURNS_MAX);
 });
diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index e68255dab9..8918f334e9 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -502,7 +502,6 @@
         "importDeclarations": 7,
         "bridgePaths": {},
         "environmentCapabilities": {
-          "window": 2,
           "window.addEventListener": 1,
           "window.removeEventListener": 1
         },
@@ -528,7 +527,7 @@
           "react": 1
         },
         "importSpecifiers": 11,
-        "nonTriviaTokens": 977
+        "nonTriviaTokens": 962
       },
       "src/renderer/app-shell-project-actions.ts": {
         "importDeclarations": 4,
@@ -3457,7 +3456,6 @@
         "actionFactories": [],
         "dependencyPaths": {
           "../locales/settings-shared-copy": 1,
-          "./settings-nav": 1,
           "./settings-surface": 1,
           "@maka/ui": 1,
           "react": 1
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 04a4e1e3a7..81db4d9305 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -3913,14 +3913,14 @@ const makaBridge = {
 // E2E-only async controls. Real users never get these: the preload mirrors the
 // main process's isolated-E2E gate (startup-context.ts) — MAKA_E2E alone is
 // not enough without the throwaway profile dir. An armed latch holds the next
-// bridge call or an explicitly gated renderer boundary until the test releases
+// bridge call until the test releases
 // it, while a settled-call waiter exposes a deterministic completion boundary
 // for work whose visible result may intentionally keep the same DOM identity.
 // The wrappers must be installed BEFORE
 // exposeInMainWorld: the bridge is cloned into the main world at expose time,
 // and the exposed clone is sealed against later patching.
 if (process.env.MAKA_E2E === '1' && process.env.MAKA_E2E_USER_DATA_DIR) {
-  type LatchKey = 'newTasks.listInvocableSkills' | 'sessions.list' | 'sessions.observe' | 'settings.chunk';
+  type LatchKey = 'newTasks.listInvocableSkills' | 'sessions.list' | 'sessions.observe';
   const gates = new Map<LatchKey, { promise: Promise<void>; oneShot: boolean }>();
   const releases = new Map<LatchKey, { resolve: () => void; reject: (error: Error) => void }>();
   let nextSessionObservationError: Error | undefined;
@@ -4009,9 +4009,6 @@ if (process.env.MAKA_E2E === '1' && process.env.MAKA_E2E_USER_DATA_DIR) {
       gates.set(key, { promise, oneShot: options?.oneShot === true });
       releases.set(key, { resolve, reject });
     },
-    wait(key: 'settings.chunk') {
-      return waitForLatch(key);
-    },
     waitForInvocableSkillsCall(sessionId: string) {
       return new Promise<void>((resolve) => {
         const waiters = invocableSkillsWaiters.get(sessionId) ?? [];
diff --git a/apps/desktop/src/renderer/app-shell-overlays.tsx b/apps/desktop/src/renderer/app-shell-overlays.tsx
index d675b618e1..9a466e8272 100644
--- a/apps/desktop/src/renderer/app-shell-overlays.tsx
+++ b/apps/desktop/src/renderer/app-shell-overlays.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { lazy, Suspense, useLayoutEffect, useRef } from 'react';
+import { lazy, Suspense, useLayoutEffect, useRef, type ReactNode } from 'react';
 import type { ChatDefaultPermissionMode, SettingsSection, ThemePalette, ThemePreference } from '@maka/core/settings';
 import type { ProviderType } from '@maka/core/llm-connections';
 import type { DesktopSessionSummary } from '../preload/bridge-contract.js';
@@ -32,15 +32,7 @@ import type { ArchivedTasksBridge } from './settings/tasks-settings-page';
 import type { UiLocaleUpdateGate } from './settings/ui-locale-update-gate';
 import { getShellRemainingCopy } from './locales/shell-remaining-copy.js';
 
-const SettingsModal = lazy(async () => {
-  const e2eLatch = (
-    window as typeof window & {
-      makaE2eLatch?: { wait(key: 'settings.chunk'): Promise<void> };
-    }
-  ).makaE2eLatch;
-  await e2eLatch?.wait('settings.chunk');
-  return import('./settings/settings-modal');
-});
+const SettingsModal = lazy(() => import('./settings/settings-modal'));
 
 type SearchModalProps = Parameters<typeof SearchModal>[0];
 
@@ -61,6 +53,30 @@ function SettingsModalFallback() {
   );
 }
 
+// Own dismissal outside the lazy chunk, including its Suspense fallback.
+export function SettingsOverlay({ onClose, children }: {
+  onClose(): void;
+  children: ReactNode;
+}) {
+  const closeRef = useRef(onClose);
+  useLayoutEffect(() => {
+    closeRef.current = onClose;
+  });
+  useLayoutEffect(() => {
+    function onKeyDown(event: KeyboardEvent) {
+      if (
+        event.key.toLowerCase() !== 'escape' || event.defaultPrevented ||
+        event.ctrlKey || event.metaKey || event.altKey
+      ) return;
+      event.preventDefault();
+      closeRef.current();
+    }
+    window.addEventListener('keydown', onKeyDown);
+    return () => window.removeEventListener('keydown', onKeyDown);
+  }, []);
+  return <Suspense fallback={<SettingsModalFallback />}>{children}</Suspense>;
+}
+
 export function AppShellOverlays(props: {
   settingsOpen: boolean;
   closeSettings(): void;
@@ -126,36 +142,6 @@ export function AppShellOverlays(props: {
     onExternalSessionImported,
   } = props;
 
-  const closeSettingsRef = useRef(closeSettings);
-  useLayoutEffect(() => {
-    closeSettingsRef.current = closeSettings;
-  });
-
-  // The overlay boundary, rather than the lazy Settings chunk, owns Escape.
-  // That keeps one owner installed before paint for both the Suspense loading
-  // surface and the resolved Settings surface. Keep the listener stable while
-  // Settings is open, but read the latest shell callback after every commit.
-  useLayoutEffect(() => {
-    if (!settingsOpen) return;
-
-    function onKeyDown(event: globalThis.KeyboardEvent) {
-      if (
-        event.key.toLowerCase() !== 'escape' ||
-        event.defaultPrevented ||
-        event.ctrlKey ||
-        event.metaKey ||
-        event.altKey
-      ) {
-        return;
-      }
-      event.preventDefault();
-      closeSettingsRef.current();
-    }
-
-    window.addEventListener('keydown', onKeyDown);
-    return () => window.removeEventListener('keydown', onKeyDown);
-  }, [settingsOpen]);
-
   // #1045: base commands freeze per open/close; session rows stay live on
   // visibleSessions/activeId. run() closures read latest options via ref.
   const commands = useAppShellCommands(paletteOpen, commandOptions);
@@ -169,7 +155,7 @@ export function AppShellOverlays(props: {
   return (
     <>
       {settingsOpen && (
-        <Suspense fallback={<SettingsModalFallback />}>
+        <SettingsOverlay onClose={closeSettings}>
           <SettingsModal
             onClose={closeSettings}
             themePref={themePref}
@@ -192,7 +178,7 @@ export function AppShellOverlays(props: {
             onRemoteHostAdded={props.onRemoteHostAdded}
             onSelectedRuntimeHostProfileIdChange={props.onSelectedRuntimeHostProfileIdChange}
           />
-        </Suspense>
+        </SettingsOverlay>
       )}
       <KeyboardHelpModal
         isOpen={helpOpen}
diff --git a/apps/desktop/src/renderer/settings/settings-modal.tsx b/apps/desktop/src/renderer/settings/settings-modal.tsx
index 3cf5cce934..fff4b75115 100644
--- a/apps/desktop/src/renderer/settings/settings-modal.tsx
+++ b/apps/desktop/src/renderer/settings/settings-modal.tsx
@@ -28,9 +28,6 @@ import { SettingsSurface } from './settings-surface';
 import type { ArchivedTasksBridge } from './tasks-settings-page';
 import type { UiLocaleUpdateGate } from './ui-locale-update-gate';
 
-export { SETTINGS_NAV } from './settings-nav';
-export type { SettingsNavGroup } from './settings-nav';
-
 export default function SettingsModal(props: {
   onClose(): void;
   themePref: ThemePreference;
diff --git a/apps/desktop/stories/AGENTS.md b/apps/desktop/stories/AGENTS.md
new file mode 100644
index 0000000000..cc802fbf99
--- /dev/null
+++ b/apps/desktop/stories/AGENTS.md
@@ -0,0 +1,48 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Desktop Storybook
+
+Before adding or changing stories, read [FIDELITY.md](FIDELITY.md), the shared
+convention for both Desktop and UI stories.
+
+- Prefer existing unit/component tests for state, event ordering, request
+  routing and persistence. Desktop renderer tests can live in
+  `../src/main/__tests__` using the existing Node/React test seam.
+- Use this browser tier for real layout, scrolling, selection, focus and
+  animation that a fake DOM cannot verify. Another viewport or theme is not a
+  reason to move the test to Electron; apply [Electron admission](../e2e/AGENTS.md)
+  before escalating.
+- Reuse the production component, controller and frame. Extend an existing
+  story when it already reaches the behavior; do not duplicate product logic or
+  build a second shell to make the test easy.
+- Check the smoke runner's actual viewport/theme jobs. Storybook toolbar
+  settings alone do not establish automated coverage. Follow FIDELITY for
+  reachable states, framing and assertion timing.
+- For migrated regression coverage, demonstrate that the assertion detects the
+  original defect, then remove the replaced E2E assertions and unused fixtures
+  in the same PR. Do not compensate for failures with longer timeouts, retries
+  or a global render-completion protocol.
+
+The Desktop Storybook host also discovers `packages/ui/stories`. From the
+repository root, run `npm --workspace @maka/desktop run typecheck:stories` and
+`npm --workspace @maka/desktop run build-storybook` before
+`npm --workspace @maka/desktop run smoke:storybook`. Smoke does not rebuild the
+catalog. For focused runs, verify that the relevant `play` functions finish;
+an image of the final screen alone does not prove their assertions passed.
diff --git a/apps/desktop/stories/FIDELITY.md b/apps/desktop/stories/FIDELITY.md
index 17bc65f42b..cba6648f2b 100644
--- a/apps/desktop/stories/FIDELITY.md
+++ b/apps/desktop/stories/FIDELITY.md
@@ -44,10 +44,10 @@ A story earns its place by rendering pixels no other story renders. A second lev
 
 Two facts decide it, and both were guessed wrong once:
 
-- **Where a story renders.** CI mounts every story exactly once at 1280 wide in light. It does not maintain a viewport, theme or screenshot matrix. Responsive and theme behaviour belongs in a focused component contract or the real desktop E2E harness.
+- **Where a story renders.** The smoke runner owns the actual viewport and theme coverage; check `scripts/storybook-visual-smoke.mjs` rather than assuming Storybook toolbar parameters create CI jobs. It currently selects a narrow viewport for story IDs containing `narrow` and additional theme/palette/forced-color runs for selected sentinels. A responsive or theme contract needs an explicit browser scenario for its required conditions, not an Electron window merely because the default smo [...]
 - **Whether `play` reaches the state.** `play` drives a story into the state a reviewer needs to see, and CI runs it — so the state it lands on is the state the smoke reads, and a story that only differs by a `play` step is a second state, not a variant.
 
-Extra stories still cost: a reviewer scanning the sidebar cannot tell which entry is the page, and duplicates re-render the same pixels every run while claiming coverage they do not add. Where a state matters but renders nothing new, pin it somewhere that runs — a `packages/ui` test or an e2e journey.
+Extra stories still cost: a reviewer scanning the sidebar cannot tell which entry is the page, and duplicates re-render the same pixels every run while claiming coverage they do not add. Where a state matters but renders nothing new, prefer an existing unit or component test. A browser or Electron test needs a boundary that the lower tier cannot verify; see [Electron admission](../e2e/AGENTS.md).
 
 ## The frame matters, not just the component
 
@@ -67,7 +67,7 @@ The render smoke waits for Storybook's `storyFinished` event before it reads the
 
 That makes `play` the right home for a behavioural contract whose subject is the browser: a live Selection, a caret between text nodes, an undo transaction, a portal's identity across a re-render. None of those exist in a `packages/ui` DOM shim, and none of them need Electron.
 
-It is still not a place for geometry or theme matrices. CI mounts every story once, at 1280 wide, in light; a contract that depends on any other viewport or scheme belongs in a `packages/ui` test or the desktop E2E harness. And a rule that is pure state — which commands a Session offers, what a query parses to — belongs in a unit test, where it costs milliseconds instead of a browser.
+Real layout, browser scrolling, and responsive/theme behavior belong in this browser tier. Assert the relevant geometry against the production component and frame, with the viewport and theme explicitly exercised by the runner. A fake DOM cannot establish real layout, and needing another viewport is not a reason to launch Electron. Pure state — which commands a Session offers, what a query parses to — belongs in a unit test, where it costs milliseconds instead of a browser.
 
 Write the assertion so it can only pass for the reason it names. A story that mounts the surface and then observes it cannot see anything that happened during the mount, so a probe that must be installed first (a constructor count, an event before the first paint) belongs in a test that owns the global.
 
diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx
index ee1388e81c..9555a69344 100644
--- a/apps/desktop/stories/app-shell.stories.tsx
+++ b/apps/desktop/stories/app-shell.stories.tsx
@@ -35,6 +35,7 @@ import {
 import type { ChatModelChoice, SessionViewMode, TurnViewModel } from '@maka/ui';
 import { SessionRail, type SessionRailStoryProps } from '../../../packages/ui/stories/session-rail-harness.js';
 import { AppShellTopbarActions } from '../src/renderer/app-shell-chrome-actions';
+import { SettingsOverlay } from '../src/renderer/app-shell-overlays';
 import {
   WorkbarServicesProvider,
   WorkbarTitlebarActions,
@@ -942,24 +943,113 @@ export const SafeResumeAfterRestart: Story = {
 // Real path: a long session with 120 turns — past the transcript virtualizer's
 // window, so it must stay correct and quiet where a handful of seeded turns
 // would never trip the virtualization path.
+const manyTurnMessages = Array.from({ length: 120 }, (_, index) => {
+  const turnId = `turn-m-${index}`;
+  const minutesAgo = (120 - index) * 3;
+  return [
+    user(`msg-m-u-${index}`, turnId, minutesAgo, `第 ${index + 1} 轮:这个模块的边界条件该怎么覆盖?`),
+    assistant(`msg-m-a-${index}`, turnId, minutesAgo - 1, `第 ${index + 1} 轮回答:先列输入域,再对空、超长、并发三类分别加断言。`),
+  ];
+}).flat();
+
 export const ManyTurns: Story = {
   render: () => (
     <ComposedShell
       session={{ lastMessageAt: NOW - 60_000 }}
       chat={{
-        messages: Array.from({ length: 120 }, (_, index) => {
-          const turnId = `turn-m-${index}`;
-          const minutesAgo = (120 - index) * 3;
-          return [
-            user(`msg-m-u-${index}`, turnId, minutesAgo, `第 ${index + 1} 轮:这个模块的边界条件该怎么覆盖?`),
-            assistant(`msg-m-a-${index}`, turnId, minutesAgo - 1, `第 ${index + 1} 轮回答:先列输入域,再对空、超长、并发三类分别加断言。`),
-          ];
-        }).flat(),
+        messages: manyTurnMessages,
       }}
     />
   ),
 };
 
+// The same transcript and fixture CSS, with enough resident Turns for native
+// render skipping but no need to mount the 120-Turn catalog demonstration.
+export const TranscriptRenderCost: Story = {
+  render: () => <ComposedShell frameHeight={700} chat={{ messages: manyTurnMessages.slice(-64) }} />,
+  play: async ({ canvasElement }) => {
+    const scroller = canvasElement.querySelector<HTMLElement>('[data-chat-scroll-container="true"]');
+    if (!scroller) throw new Error('Transcript scrollport is missing');
+    const frame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
+    await frame();
+    await frame();
+    let transitions = 0;
+    let animations = 0;
+    const skipped = new Set<Element>();
+    const turns = [...canvasElement.querySelectorAll<HTMLElement>('.maka-transcript-turn')];
+    expect(turns.length).toBeGreaterThan(0);
+    for (const pseudo of [null, '::before', '::after']) {
+      const style = getComputedStyle(turns[0], pseudo);
+      expect(style.transitionProperty).toBe('none');
+      expect(style.animationName).toBe('none');
+    }
+    const visibility = (event: Event) => {
+      if (event.target !== event.currentTarget) return;
+      const turn = event.currentTarget as Element;
+      if ((event as Event & { skipped: boolean }).skipped) skipped.add(turn);
+      else skipped.delete(turn);
+    };
+    for (const turn of turns) turn.addEventListener('contentvisibilityautostatechange', visibility);
+    const transition = () => { transitions += 1; };
+    const animation = () => { animations += 1; };
+    canvasElement.addEventListener('transitionrun', transition, true);
+    canvasElement.addEventListener('animationstart', animation, true);
+    try {
+      // No Host paging or wheel routing is under test: move the real Chromium
+      // scrollport to exercise the fixture CSS and browser render skipping.
+      // Two distant positions expose and skip resident Turns. Forty incremental
+      // paints added cost under CI contention without testing another contract.
+      for (const direction of [-1, 1]) {
+        scroller.dispatchEvent(new WheelEvent('wheel', { deltaY: direction * 120, bubbles: true }));
+        scroller.scrollTop = direction < 0 ? 0 : scroller.scrollHeight;
+        await frame();
+        await frame();
+      }
+      await waitFor(() => expect(skipped.size).toBeGreaterThan(0));
+      expect(transitions).toBe(0);
+      expect(animations).toBe(0);
+      expect(canvasElement.getAnimations({ subtree: true })
+        .filter((animation) => animation.playState !== 'finished')).toHaveLength(0);
+    } finally {
+      for (const turn of turns) turn.removeEventListener('contentvisibilityautostatechange', visibility);
+      canvasElement.removeEventListener('transitionrun', transition, true);
+      canvasElement.removeEventListener('animationstart', animation, true);
+    }
+  },
+};
+
+const pendingSettingsChunk = new Promise<never>(() => {});
+function PendingSettingsChunk(): never { throw pendingSettingsChunk; }
+function SettingsLoadingScene() {
+  const [open, setOpen] = useState(true);
+  return open ? (
+    <SettingsOverlay onClose={() => setOpen(false)}><PendingSettingsChunk /></SettingsOverlay>
+  ) : null;
+}
+
+// Real path: AppShellOverlays owns dismissal while its Settings chunk suspends.
+export const SettingsLoadingEscape: Story = {
+  render: () => <SettingsLoadingScene />,
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await canvas.findByRole('status', { name: '正在加载设置' });
+    for (const modifier of ['ctrlKey', 'metaKey', 'altKey'] as const) {
+      const event = new KeyboardEvent('keydown', {
+        key: 'Escape', [modifier]: true, bubbles: true, cancelable: true,
+      });
+      expect(window.dispatchEvent(event)).toBe(true);
+      expect(canvas.getByRole('status', { name: '正在加载设置' })).toBeVisible();
+    }
+    expect(window.dispatchEvent(new KeyboardEvent('keydown', {
+      key: 'Escape', bubbles: true, cancelable: true,
+    }))).toBe(false);
+    await waitFor(() => expect(canvas.queryByRole('status', { name: '正在加载设置' })).not.toBeInTheDocument());
+    expect(window.dispatchEvent(new KeyboardEvent('keydown', {
+      key: 'Escape', bubbles: true, cancelable: true,
+    }))).toBe(true);
+  },
+};
+
 // Real path: enough task history to overflow the sidebar. The rail owns the
 // scrollport while its footer remains inside the fixed shell frame.
 export const OverflowingSidebar: Story = {
diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx
index 8064f31381..676e0ea180 100644
--- a/apps/desktop/stories/settings/settings-pages.stories.tsx
+++ b/apps/desktop/stories/settings/settings-pages.stories.tsx
@@ -1848,6 +1848,7 @@ function renderedLinkColors(renderedLink: HTMLElement) {
 }
 
 type SettingsStoryProps = {
+  reopenable?: boolean;
   section: SettingsSection;
   connections?: LlmConnection[];
   defaultSlug?: string | null;
@@ -1895,6 +1896,7 @@ function SettingsStory(props: SettingsStoryProps) {
 }
 
 function SettingsStoryFrame(props: SettingsStoryProps) {
+  const [open, setOpen] = useState(true);
   const archivedTasks = useArchivedTasksStoryBridge(props.archivedTaskSessions ?? []);
   const initialFocusRef = useRef<HTMLButtonElement>(null);
   const [uiLocaleUpdateGate] = useState(createUiLocaleUpdateGate);
@@ -1918,6 +1920,7 @@ function SettingsStoryFrame(props: SettingsStoryProps) {
 
   return (
     <>
+      {props.reopenable && <button onClick={() => setOpen(!open)}>{open ? 'Close settings' : 'Reopen settings'}</button>}
       {/* `100dvh`, not `100%`: `SettingsSurface` is a `Layout height="fill"`,
           which needs a bounded ancestor to hand its content pane a scroll
           box. Under Storybook's fullscreen body a percentage height resolves
@@ -1936,7 +1939,7 @@ function SettingsStoryFrame(props: SettingsStoryProps) {
         <ConnectionSettingsServicesProvider services={connectionSettingsServices}>
           <RuntimeHostManagementServicesProvider services={runtimeHostManagementServices}>
             <SessionBundleServicesProvider services={sessionBundleServices}>
-            <SettingsSurface
+            {open && <SettingsSurface
               onClose={noop}
               themePref={themePref}
               onThemeChange={setThemePref}
@@ -1957,7 +1960,7 @@ function SettingsStoryFrame(props: SettingsStoryProps) {
               onRemoteHostAdded={noop}
               onSelectedRuntimeHostProfileIdChange={noop}
               snapshotCache={snapshotCache}
-            />
+            />}
             </SessionBundleServicesProvider>
           </RuntimeHostManagementServicesProvider>
         </ConnectionSettingsServicesProvider>
@@ -2212,6 +2215,50 @@ export const GeneralCachedRevalidation: Story = {
     await expect(canvas.queryByRole('alert')).not.toBeInTheDocument();
   },
 };
+
+// Real path: unmount and reopen Settings with its renderer-owned snapshot cache.
+// Observe every DOM commit, not just the final ready screen after refresh.
+export const GeneralReopenKeepsReadyControls: Story = {
+  decorators: [withGeneralHostGenerationRevalidationBridge],
+  render: () => {
+    resetGenerationStoryBridge();
+    return <SettingsStory section="general" reopenable />;
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await waitFor(() => {
+      expect(canvas.getByRole('textbox', { name: '助手语气偏好' })).toBeEnabled();
+      expect(canvas.getByRole('button', { name: '默认模型' })).toBeEnabled();
+    });
+    await userEvent.click(canvas.getByRole('button', { name: 'Close settings' }));
+    expect(canvas.queryByRole('textbox', { name: '助手语气偏好' })).not.toBeInTheDocument();
+    let missingControls = false;
+    let loadingAlert = false;
+    const inspect = () => {
+      const surface = canvasElement.querySelector('.settingsSurface');
+      const main = surface?.querySelector('main, [role="main"]');
+      if (!surface || !main) return;
+      missingControls ||= main.querySelector('textarea') === null ||
+        within(main as HTMLElement).queryByRole('button', { name: '默认模型' }) === null;
+      loadingAlert ||= [...surface.querySelectorAll('[role="alert"]')]
+        .some((alert) => alert.textContent?.includes('正在加载设置'));
+    };
+    const observer = new MutationObserver(inspect);
+    observer.observe(canvasElement, { childList: true, subtree: true, characterData: true });
+    try {
+      await userEvent.click(canvas.getByRole('button', { name: 'Reopen settings' }));
+      await waitFor(() => {
+        expect(canvas.getByRole('textbox', { name: '助手语气偏好' })).toBeEnabled();
+        expect(canvas.getByRole('button', { name: '默认模型' })).toBeEnabled();
+      });
+      inspect();
+      expect(missingControls).toBe(false);
+      expect(loadingAlert).toBe(false);
+    } finally {
+      observer.disconnect();
+    }
+  },
+};
 // A Runtime Host can be replaced without changing its renderer-facing
 // profileId:hostId key. The lifecycle epoch is the generation boundary: keep
 // cached Host values visible, revoke their write authority immediately, and
diff --git a/packages/ui/stories/AGENTS.md b/packages/ui/stories/AGENTS.md
new file mode 100644
index 0000000000..8c4399b55c
--- /dev/null
+++ b/packages/ui/stories/AGENTS.md
@@ -0,0 +1,41 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Shared UI Storybook
+
+Read [the shared fidelity convention](../../../apps/desktop/stories/FIDELITY.md)
+before adding or changing stories. Use its rules rather than maintaining a
+second version here.
+
+- Prefer `../src/__tests__` for pure state and component behavior that the
+  existing Node/React harness can verify. Do not create a browser story solely
+  because a test happens to render React.
+- Keep real Chromium layout, scrolling, selection, focus and animation checks
+  in the browser tier. Fake DOM geometry is not layout evidence; viewport,
+  theme or wheel requirements alone do not justify Electron. Follow
+  [Electron admission](../../../apps/desktop/e2e/AGENTS.md) before escalating.
+- Exercise the shared component through its production props and behavior.
+  Desktop-specific shell wiring belongs in the Desktop stories; do not copy
+  that shell into a UI fixture. Preserve the real owning frame for geometry.
+- Reuse existing scenarios and imports, and prove migrated regression
+  assertions can detect the defect before deleting their old coverage.
+
+These stories run in the Desktop Storybook host. Follow the
+[Desktop validation instructions](../../../apps/desktop/stories/AGENTS.md)
+for typecheck, build and smoke; there is no separate UI Storybook runner.


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