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/02 14:52:12 UTC

(maka) 04/04: refactor(desktop): rebuild the About page as labeled settings groups

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

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

commit 77199c821fce857281634970dffd18ddc061f777
Author: AstroHan <le...@outlook.com>
AuthorDate: Wed Sep 2 22:51:36 2026 +0800

    refactor(desktop): rebuild the About page as labeled settings groups
    
    The About page was the only settings page that bypassed `SettingsSection`: a bespoke hero with two Badges, three hand-written Dividers, two-column Grids, and a primary button on a support action. The release channel — the one fact a nightly user needs first — was a pill label.
    
    Make the channel the page's lead: a Token next to the product name (nightly orange, dev gray, release unmarked), the version and build as supporting text, and one sentence on what the channel means. Every group below is a `SettingsSection`: update status as a single row, version facts as a bare MetadataList with the workspace path collapsed to `~`, support as three quiet row actions, and the privacy list. No Badge, Grid, Divider, or primary button remains; Astryx's Badge guidance excl [...]
    
    `PageHeader` in `@maka/ui` lost its last caller and is removed with its styles. Stories gain packaged nightly and release variants so both channels render in Storybook; the About e2e and channel tests follow the new structure.
---
 apps/desktop/e2e/about-page.spec.ts                |  20 +-
 .../src/main/__tests__/about-update-status.test.ts |  97 ++++---
 .../renderer/locales/settings-preferences-copy.ts  |  40 ++-
 .../src/renderer/settings/about-settings-page.tsx  | 311 +++++++++------------
 .../src/renderer/settings/about-update-status.ts   |  61 ++--
 .../desktop/src/renderer/styles/settings/about.css |  44 ---
 apps/desktop/src/renderer/styles/sidebar.css       |   5 +-
 .../stories/settings/settings-pages.stories.tsx    |  65 +++++
 packages/ui/src/index.ts                           |   6 -
 packages/ui/src/styles.css                         |   6 -
 10 files changed, 331 insertions(+), 324 deletions(-)

diff --git a/apps/desktop/e2e/about-page.spec.ts b/apps/desktop/e2e/about-page.spec.ts
index 26ca1022df..f9e6714210 100644
--- a/apps/desktop/e2e/about-page.spec.ts
+++ b/apps/desktop/e2e/about-page.spec.ts
@@ -24,16 +24,16 @@ test('About renders channel facts, support, and privacy sections', async ({ wind
   await page.getByRole('button', { name: '设置' }).click();
   await page.getByRole('button', { name: '关于', exact: true }).click();
 
-  // The channel pill must agree with the version string: the fixture app is a
-  // dev checkout, so the pill reads 本地开发版 (with commit), never 正式版.
-  const heroBadge = page.locator('.settingsAboutHeading');
-  await expect(heroBadge.getByText(/本地开发版/)).toBeVisible();
-  await expect(heroBadge.getByText('正式版')).toHaveCount(0);
+  // The channel token must agree with the version string: the fixture app is a
+  // dev checkout, so it reads 本地开发版, never 正式版.
+  await expect(page.getByText('本地开发版', { exact: true }).first()).toBeVisible();
+  await expect(page.getByText('正式版', { exact: true })).toHaveCount(0);
+  await expect(page.getByText('本地开发构建,不检查更新。')).toBeVisible();
 
-  // The archive readout: channel meaning, runtime, and workspace path. The
-  // fixture's workspace is a throwaway temp dir, so assert the code-wrapped
-  // path itself rather than a `~` prefix.
-  await expect(page.getByText('不自动更新')).toBeVisible();
+  // The archive readout: channel, runtime, and workspace path. The fixture's
+  // workspace is a throwaway temp dir, so assert the code-wrapped path itself
+  // rather than a `~` prefix.
+  await expect(page.getByRole('heading', { name: '版本信息' })).toBeVisible();
   await expect(page.getByText(/Electron \d+\.\d+\.\d+/)).toBeVisible();
   await expect(page.locator('dd code')).toBeVisible();
 
@@ -45,7 +45,7 @@ test('About renders channel facts, support, and privacy sections', async ({ wind
 
   // Support lives outside the info conditional: usable even when `app.info`
   // fails, which is exactly when a user reaches for it.
-  await expect(page.getByRole('heading', { name: '支持与诊断' })).toBeVisible();
+  await expect(page.getByRole('heading', { name: '支持', exact: true })).toBeVisible();
   await expect(page.getByRole('button', { name: '复制', exact: true })).toBeEnabled();
   await expect(page.getByRole('button', { name: '查看' })).toBeEnabled();
 
diff --git a/apps/desktop/src/main/__tests__/about-update-status.test.ts b/apps/desktop/src/main/__tests__/about-update-status.test.ts
index 2e0043c805..2515939345 100644
--- a/apps/desktop/src/main/__tests__/about-update-status.test.ts
+++ b/apps/desktop/src/main/__tests__/about-update-status.test.ts
@@ -20,54 +20,73 @@
 import assert from 'node:assert/strict';
 import { test } from 'node:test';
 import {
-  aboutChannelBadge,
+  aboutChannelFacts,
+  aboutUpdateStatusDetail,
 } from '../../renderer/settings/about-update-status.js';
 import { getSettingsPreferencesCopy } from '../../renderer/locales/settings-preferences-copy.js';
 
 const copy = getSettingsPreferencesCopy('zh').about;
 
-test('a packaged nightly build is labelled Nightly, never 正式版', () => {
-  const badge = aboutChannelBadge(
-    { buildMode: 'packaged', buildCommit: null, updateChannel: 'nightly' },
-    copy,
-  );
-  assert.deepEqual(badge, {
-    label: 'Nightly',
-    variant: 'orange',
-    channelName: 'Nightly',
-  });
+test('a packaged nightly is tokened Nightly, never 正式版', () => {
+  const facts = aboutChannelFacts({ buildMode: 'packaged', updateChannel: 'nightly' }, copy);
+  assert.equal(facts.key, 'nightly');
+  assert.equal(facts.name, 'Nightly');
+  assert.deepEqual(facts.token, { label: 'Nightly', color: 'orange' });
+  assert.match(facts.summary, /会覆盖正式版安装/);
 });
 
-test('a packaged release build keeps the release pill', () => {
-  const badge = aboutChannelBadge(
-    { buildMode: 'packaged', buildCommit: null, updateChannel: 'release' },
-    copy,
-  );
-  assert.deepEqual(badge, {
-    label: '正式版',
-    variant: 'blue',
-    channelName: '正式版',
-  });
+test('a packaged release wears no token — it is the default state', () => {
+  const facts = aboutChannelFacts({ buildMode: 'packaged', updateChannel: 'release' }, copy);
+  assert.equal(facts.key, 'release');
+  assert.equal(facts.name, '正式版');
+  assert.equal(facts.token, null);
 });
 
-test('a dev checkout carries its commit on a neutral pill', () => {
-  const withCommit = aboutChannelBadge(
-    { buildMode: 'dev', buildCommit: 'abc1234', updateChannel: 'release' },
-    copy,
+test('buildMode decides before updateChannel, whose dev value is a placeholder', () => {
+  const facts = aboutChannelFacts({ buildMode: 'dev', updateChannel: 'nightly' }, copy);
+  assert.equal(facts.key, 'dev');
+  assert.equal(facts.name, '本地开发版');
+  assert.deepEqual(facts.token, { label: '本地开发版', color: 'gray' });
+});
+
+test('a dev build reports why it never updates, whatever the updater says', () => {
+  assert.equal(
+    aboutUpdateStatusDetail(
+      { state: 'downloaded', currentVersion: '0.2.0', latestVersion: '0.2.1' },
+      copy,
+      { isDevBuild: true },
+    ),
+    copy.updateDevBuildHelp,
   );
-  assert.deepEqual(withCommit, {
-    label: '本地开发版 · abc1234',
-    variant: 'neutral',
-    channelName: '本地开发版',
-  });
+});
 
-  const withoutCommit = aboutChannelBadge(
-    { buildMode: 'dev', buildCommit: null, updateChannel: 'nightly' },
-    copy,
+test('the nightly steady states each read as themselves', () => {
+  const detail = (status: Parameters<typeof aboutUpdateStatusDetail>[0]) =>
+    aboutUpdateStatusDetail(status, copy, { isDevBuild: false });
+
+  assert.equal(
+    detail({
+      state: 'downloading',
+      currentVersion: '0.2.0-dev.11.20260831',
+      latestVersion: '0.2.0-dev.12.20260901',
+      progress: { percent: 42.4, bytesPerSecond: 1, transferred: 1, total: 2 },
+    }),
+    '正在下载 v0.2.0-dev.12.20260901(42%)…',
+  );
+  assert.equal(
+    detail({
+      state: 'verifying',
+      currentVersion: '0.2.0-dev.11.20260831',
+      latestVersion: '0.2.0-dev.12.20260901',
+    }),
+    '正在验证 v0.2.0-dev.12.20260901 的发布来源…',
   );
-  assert.deepEqual(withoutCommit, {
-    label: '本地开发版',
-    variant: 'neutral',
-    channelName: '本地开发版',
-  });
-});
\ No newline at end of file
+  assert.equal(
+    detail({
+      state: 'downloaded',
+      currentVersion: '0.2.0-dev.11.20260831',
+      latestVersion: '0.2.0-dev.12.20260901',
+    }),
+    'v0.2.0-dev.12.20260901 已下载,可在侧栏选择重启安装。',
+  );
+});
diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
index 25d195cc4d..24a511dc80 100644
--- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
@@ -228,15 +228,17 @@ export type SettingsPreferencesCopy = {
     devBuild: string;
     packagedBuild: string;
     nightlyBuild: string;
-    subtitle: string;
+    buildInfoTitle: string;
     channelLabel: string;
+    versionLabel: string;
+    buildLabel: string;
     runtimeLabel: string;
     workspaceLabel: string;
+    /** One sentence saying what following this channel means for the user. */
     channelSummaries: Record<'dev' | 'nightly' | 'release', string>;
     platformNames: Record<string, string>;
-    updatesLede: string;
+    updateStatusLabel: string;
     supportTitle: string;
-    supportLede: string;
     reportIssueHelp: string;
     reportIssueOpen: string;
     copyAction: string;
@@ -352,16 +354,20 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
       passwordSavedPlaceholder: '密码已保存;输入新密码以替换',
     },
     about: {
-      loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制诊断信息', pasteHint: '检查内容后,可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', nightlyBuild: 'Nightly', subtitle: '本地优先的 AI 助手 · 桌面端运行环境',
-      channelLabel: '渠道', runtimeLabel: '运行环境', workspaceLabel: '工作区',
-      channelSummaries: { dev: '不自动更新', nightly: '每晚构建,自动跟随更新', release: '打包安装,自动检查更新' },
+      loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制诊断信息', pasteHint: '检查内容后,可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', nightlyBuild: 'Nightly',
+      buildInfoTitle: '版本信息', channelLabel: '渠道', versionLabel: '版本', buildLabel: '构建', runtimeLabel: '运行环境', workspaceLabel: '工作区',
+      channelSummaries: {
+        dev: '本地开发构建,不检查更新。',
+        nightly: '每日构建的预发布版,自动更新到最新 nightly,会覆盖正式版安装。',
+        release: '正式发布版,自动接收稳定更新。',
+      },
       platformNames: { darwin: 'macOS', win32: 'Windows', linux: 'Linux' },
-      updatesTitle: '软件更新', updatesLede: 'Maka 检查并安装新版本的渠道与状态。',
-      supportTitle: '支持与诊断', supportLede: '出问题时的三件事:取证、上报、查快捷键。',
+      updatesTitle: '软件更新', updateStatusLabel: '更新状态',
+      supportTitle: '支持',
       copying: '复制中…', copyDiagnostics: '复制诊断信息', copyAction: '复制', copyHelp: '复制版本、平台、隐藏主目录后的工作区路径,以及近期脱敏的 Desktop 与 Runtime Host 日志;仅写入剪贴板,不会自动上传。',
       reportIssueLabel: '报告问题', reportIssueHelp: '带上诊断信息去 GitHub Issues,回复更快。', reportIssueOpen: '打开',
       keyboardShortcuts: '键盘快捷键', keyboardShortcutsHelp: 'Maka 支持的全部快捷键一览。', keyboardShortcutsOpen: '查看',
-      privacyLabel: '隐私承诺', privacyTitle: '隐私承诺', privacyLede: 'Maka 对本机数据与通信的三条承诺。', privacyPoints: ['任务、设置、凭据和 Skill 指令文件都留在本机;模型密钥保存在本机凭据文件内,订阅令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在任务内明示授权;每个任务都会在本机保留消息、工具调用、权限决策与模式变更记录。'],
+      privacyLabel: '隐私承诺', privacyTitle: '本地优先 · 隐私默认', privacyLede: 'Maka 对本机数据与通信的三条承诺。', privacyPoints: ['任务、设置、凭据和 Skill 指令文件都留在本机;模型密钥保存在本机凭据文件内,订阅令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在任务内明示授权;每个任务都会在本机保留消息、工具调用、权限决策与模式变更记录。'],
       checkForUpdates: '检查更新',
       checkingForUpdates: '检查中…',
       updateHelp: '后台也会定期检查;需要重启安装时侧栏会提示。',
@@ -414,16 +420,20 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
       passwordSavedPlaceholder: 'Password saved; enter a new password to replace it',
     },
     about: {
-      loadFailed: 'Could not load About information', loading: 'Loading About', unavailable: 'About information is unavailable', copied: 'Diagnostics copied', pasteHint: 'Review the content, then paste it into an issue report', copyFailed: 'Copy failed', clipboardUnavailable: 'The clipboard is unavailable or access was denied.', devBuild: 'Local development build', packagedBuild: 'Release build', nightlyBuild: 'Nightly', subtitle: 'A local-first AI assistant · Desktop runtime',
-      channelLabel: 'Channel', runtimeLabel: 'Runtime', workspaceLabel: 'Workspace',
-      channelSummaries: { dev: 'no automatic updates', nightly: 'Nightly build, updates follow automatically', release: 'Packaged install, checks for updates automatically' },
+      loadFailed: 'Could not load About information', loading: 'Loading About', unavailable: 'About information is unavailable', copied: 'Diagnostics copied', pasteHint: 'Review the content, then paste it into an issue report', copyFailed: 'Copy failed', clipboardUnavailable: 'The clipboard is unavailable or access was denied.', devBuild: 'Local development build', packagedBuild: 'Release build', nightlyBuild: 'Nightly',
+      buildInfoTitle: 'Version details', channelLabel: 'Channel', versionLabel: 'Version', buildLabel: 'Build', runtimeLabel: 'Runtime', workspaceLabel: 'Workspace',
+      channelSummaries: {
+        dev: 'A local development build. It does not check for updates.',
+        nightly: 'A daily prerelease build. It updates itself to the latest nightly and replaces a release install.',
+        release: 'The official release build. It receives stable updates automatically.',
+      },
       platformNames: { darwin: 'macOS', win32: 'Windows', linux: 'Linux' },
-      updatesTitle: 'Software updates', updatesLede: 'How Maka checks for and installs new versions.',
-      supportTitle: 'Support & diagnostics', supportLede: 'Three things when something breaks: capture, report, look up shortcuts.',
+      updatesTitle: 'Software updates', updateStatusLabel: 'Update status',
+      supportTitle: 'Support',
       copying: 'Copying…', copyDiagnostics: 'Copy diagnostics', copyAction: 'Copy', copyHelp: 'Copy version, platform, a home-redacted workspace path, and recent redacted Desktop and Runtime Host logs. The report is written only to the clipboard and is never uploaded automatically.',
       reportIssueLabel: 'Report an issue', reportIssueHelp: 'Open a GitHub issue with your diagnostics attached — replies come faster.', reportIssueOpen: 'Open',
       keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View',
-      privacyLabel: 'Privacy commitments', privacyTitle: 'Privacy commitments', privacyLede: 'Three commitments about your local data and network use.', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay on this machine; model keys live in a local credential file and subscription tokens use secure system storage.', 'Maka sends no usage telemetry and contacts a model provider only when you enable it.', 'High-risk tool operations require explicit permission in the t [...]
+      privacyLabel: 'Privacy commitments', privacyTitle: 'Local first · Private by default', privacyLede: 'Three commitments about your local data and network use.', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay on this machine; model keys live in a local credential file and subscription tokens use secure system storage.', 'Maka sends no usage telemetry and contacts a model provider only when you enable it.', 'High-risk tool operations require explicit permis [...]
       checkForUpdates: 'Check for updates',
       checkingForUpdates: 'Checking…',
       updateHelp: 'Maka also checks in the background. When a restart is required, the sidebar will prompt you.',
diff --git a/apps/desktop/src/renderer/settings/about-settings-page.tsx b/apps/desktop/src/renderer/settings/about-settings-page.tsx
index 45cc3c1833..b9b15e475c 100644
--- a/apps/desktop/src/renderer/settings/about-settings-page.tsx
+++ b/apps/desktop/src/renderer/settings/about-settings-page.tsx
@@ -17,11 +17,9 @@
  * under the License.
  */
 
-import { useEffect, useId, useState, type ReactNode } from 'react';
+import { useEffect, useState, type ReactNode } from 'react';
 import {
-  Badge,
-  Divider,
-  Grid,
+  Code,
   Heading,
   HStack,
   Link,
@@ -30,24 +28,18 @@ import {
   MetadataList,
   MetadataListItem,
   Text,
+  Token,
   VStack,
 } from '@astryxdesign/core';
 import { Kbd } from '@astryxdesign/core/Kbd';
 import { Sparkles } from '@maka/ui/icons';
-import {
-  Banner,
-  Button,
-  PageHeader,
-  useMountedRef,
-  useToast,
-  useUiLocale,
-} from '@maka/ui';
+import { Banner, Button, useMountedRef, useToast, useUiLocale } from '@maka/ui';
 import type { AppUpdateStatus } from '../../preload/bridge-contract.js';
-import { SettingsPage } from './settings-section.js';
+import { SettingsPage, SettingsRow, SettingsSection } from './settings-section.js';
 import { settingsActionErrorMessage } from './settings-error-copy.js';
 import { SettingsSkeletonStack } from './settings-skeleton.js';
 import { useActionGuard } from './use-action-guard.js';
-import { aboutChannelBadge, aboutUpdateStatusDetail } from './about-update-status.js';
+import { aboutChannelFacts, aboutUpdateStatusDetail } from './about-update-status.js';
 import { getSettingsPreferencesCopy } from '../locales/settings-preferences-copy.js';
 import {
   defaultRuntimeHostDiagnosticTarget,
@@ -70,11 +62,6 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) {
   const checkUpdateGuard = useActionGuard<'check'>();
   const aboutPageMountedRef = useMountedRef();
   const toast = useToast();
-  const diagnosticsHelpId = useId();
-  const updateHelpId = useId();
-  const updatesHeadingId = useId();
-  const supportHeadingId = useId();
-  const privacyHeadingId = useId();
 
   useEffect(() => {
     let cancelled = false;
@@ -154,189 +141,167 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) {
     }
   }
 
-  let aboutContent: ReactNode;
+  let identity: ReactNode;
   if (!info && !infoError) {
-    aboutContent = (
-      <SettingsSkeletonStack
-        label={copy.loading}
-        lines={[
-          { width: '38%', size: 'lg' },
-          { width: '70%' },
-          { width: '52%' },
-        ]}
-      />
+    identity = (
+      <SettingsSection variant="bare">
+        <SettingsSkeletonStack
+          label={copy.loading}
+          lines={[
+            { width: '38%', size: 'lg' },
+            { width: '70%' },
+            { width: '52%' },
+          ]}
+        />
+      </SettingsSection>
     );
   } else if (!info) {
-    aboutContent = (
-      <Banner
-        status="info"
-        role="alert"
-        title={copy.unavailable}
-        description={infoError} />
+    identity = (
+      <SettingsSection variant="bare">
+        <Banner status="info" role="alert" title={copy.unavailable} description={infoError} />
+      </SettingsSection>
     );
   } else {
-    const channelBadge = aboutChannelBadge(info, copy);
-    const channelKey = info.buildMode === 'dev' ? 'dev' : info.updateChannel;
+    const channel = aboutChannelFacts(info, copy);
     const isDevBuild = info.buildMode === 'dev';
     // The contract hands us `homePath` for exactly this collapse.
     const workspaceDisplay = info.workspacePath.startsWith(info.homePath)
       ? `~${info.workspacePath.slice(info.homePath.length)}`
       : info.workspacePath;
-    aboutContent = (
+    identity = (
       <>
-        <VStack gap={5}>
-          <PageHeader
-            as_wrapper="div"
-            className="settingsAboutHero"
-            as="h2"
-            icon={<Sparkles size={30} /> /* 64% of the 48px plate, matching .providerLogo's fill */}
-            iconClassName="settingsAboutLogo"
-            headingRowClassName="settingsAboutHeading"
-            title="Maka"
-            badge={
-              <>
-                <Badge variant="neutral" label={`v${info.appVersion}`} />
-                <Badge variant={channelBadge.variant} label={channelBadge.label} />
-              </>
-            }
-            subtitle={copy.subtitle}
-            subtitleClassName="settingsAboutTagline"
+        {/* The lead group: what this app is, which channel it follows, and what
+            that means. Unlabeled on purpose — the page title already says 关于,
+            so a heading here would only repeat it. */}
+        <SettingsSection variant="bare">
+          <VStack gap={2}>
+            <HStack gap={4} vAlign="center">
+              <span className="settingsAboutLogo" aria-hidden="true">
+                {/* 64% of the 48px plate, matching .providerLogo's fill */}
+                <Sparkles size={30} />
+              </span>
+              <HStack gap={2} vAlign="center" wrap="wrap">
+                <Heading level={2}>Maka</Heading>
+                {/* Release installs carry no token: they are the default state,
+                    and Astryx keeps colour for what departs from it. */}
+                {channel.token ? (
+                  <Token size="sm" label={channel.token.label} color={channel.token.color} />
+                ) : null}
+              </HStack>
+            </HStack>
+            <Text type="supporting" color="secondary">
+              {info.buildCommit
+                ? `v${info.appVersion} · ${copy.buildLabel} ${info.buildCommit}`
+                : `v${info.appVersion}`}
+            </Text>
+            <Text type="body">{channel.summary}</Text>
+          </VStack>
+        </SettingsSection>
+        <SettingsSection
+          title={copy.updatesTitle}
+          /* A dev build's own status line already explains why nothing updates,
+             so the background-check sentence must not contradict it. */
+          description={isDevBuild ? undefined : copy.updateHelp}
+        >
+          <SettingsRow
+            label={copy.updateStatusLabel}
+            description={aboutUpdateStatusDetail(updateStatus, copy, { isDevBuild })}
+            end={(
+              <Button
+                variant="secondary"
+                size="sm"
+                isDisabled={checkingUpdate || isDevBuild}
+                onClick={() => void checkForUpdates()}
+                label={checkingUpdate || updateStatus?.state === 'checking'
+                  ? copy.checkingForUpdates
+                  : copy.checkForUpdates}
+              />
+            )}
           />
-          {/* The archive readout: what channel this is, what it runs on, and
-              where the data lives. Astryx's label → value primitive, the same
-              construction the MCP detail panel uses — no hairlines of ours. */}
-          <MetadataList columns="single" label={{ position: 'start', width: 88 }}>
-            <MetadataListItem label={copy.channelLabel}>
-              <Text type="body">
-                {channelBadge.channelName} · {copy.channelSummaries[channelKey]}
-              </Text>
-            </MetadataListItem>
+        </SettingsSection>
+        {/* The archive readout, in Astryx's label → value primitive. No label
+            width: the component sizes the column, so adding a language does not
+            mean re-tuning a pixel count. */}
+        <SettingsSection title={copy.buildInfoTitle} variant="bare">
+          <MetadataList>
+            <MetadataListItem label={copy.channelLabel}>{channel.name}</MetadataListItem>
+            <MetadataListItem label={copy.versionLabel}>{info.appVersion}</MetadataListItem>
+            {info.buildCommit ? (
+              <MetadataListItem label={copy.buildLabel}>{info.buildCommit}</MetadataListItem>
+            ) : null}
             <MetadataListItem label={copy.runtimeLabel}>
-              <Text type="body">
-                {copy.platformNames[info.platform] ?? info.platform} · {info.arch} · Electron {info.electronVersion}
-              </Text>
+              {copy.platformNames[info.platform] ?? info.platform} · {info.arch} · Electron {info.electronVersion}
             </MetadataListItem>
+            {/* Astryx's `Code`, not a raw <code> leaning on the renderer's
+                global monospace rule — a path is read character by character. */}
             <MetadataListItem label={copy.workspaceLabel}>
-              <Text type="body"><code>{workspaceDisplay}</code></Text>
+              <Code>{workspaceDisplay}</Code>
             </MetadataListItem>
           </MetadataList>
-        </VStack>
-        <Divider />
-        <section aria-labelledby={updatesHeadingId}>
-          <Grid columns={{ minWidth: 320, max: 2, repeat: 'fit' }} gap={10}>
-            <VStack gap={1}>
-              <Heading level={3} id={updatesHeadingId}>{copy.updatesTitle}</Heading>
-              <Text type="supporting" size="sm" color="secondary">{copy.updatesLede}</Text>
-            </VStack>
-            <VStack gap={2}>
-              {/* A dev build's status detail IS the explanation (本地开发版不检查
-                  GitHub 发布更新…), so the background-check paragraph must not
-                  repeat it — the old page printed that sentence twice. */}
-              <Text type="body" id={isDevBuild ? updateHelpId : undefined}>
-                {aboutUpdateStatusDetail(updateStatus, copy, { isDevBuild })}
-              </Text>
-              {!isDevBuild && (
-                <Text type="supporting" size="sm" color="secondary" id={updateHelpId}>
-                  {copy.updateHelp}
-                </Text>
-              )}
-              <HStack gap={2}>
-                <Button
-                  variant="secondary"
-                  size="sm"
-                  isDisabled={checkingUpdate || isDevBuild}
-                  aria-describedby={updateHelpId}
-                  onClick={() => void checkForUpdates()}
-                  label={checkingUpdate || updateStatus?.state === 'checking'
-                    ? copy.checkingForUpdates
-                    : copy.checkForUpdates}
-                />
-              </HStack>
-            </VStack>
-          </Grid>
-        </section>
-        <Divider />
+        </SettingsSection>
       </>
     );
   }
 
   return (
     <SettingsPage>
-      {aboutContent}
+      {identity}
       {/* Support lives OUTSIDE the info conditional on purpose: copying
           diagnostics must not depend on `app.info` succeeding — that is the
           very moment a user needs it. The keyboard sheet used to be reachable
           only from the titlebar's `…` drawer and two shortcuts, which made
           the panel listing the shortcuts openable only by shortcut; this is
           the entry a mouse can find. */}
-      <section aria-labelledby={supportHeadingId}>
-        <Grid columns={{ minWidth: 320, max: 2, repeat: 'fit' }} gap={10}>
-          <VStack gap={1}>
-            <Heading level={3} id={supportHeadingId}>{copy.supportTitle}</Heading>
-            <Text type="supporting" size="sm" color="secondary">{copy.supportLede}</Text>
-          </VStack>
-          <VStack gap={5}>
-            <VStack gap={1} align="start">
-              <Text type="label">{copy.copyDiagnostics}</Text>
-              <Text type="supporting" size="sm" color="secondary" id={diagnosticsHelpId}>
-                {copy.copyHelp}
-              </Text>
-              <HStack gap={2}>
-                <Button
-                  variant="primary"
-                  size="sm"
-                  isDisabled={copyingDiagnostics}
-                  aria-describedby={diagnosticsHelpId}
-                  onClick={() => void copyDiagnostics()}
-                  label={copyingDiagnostics ? copy.copying : copy.copyAction}
-                />
-                <Kbd keys="mod+shift+d" />
-              </HStack>
-            </VStack>
-            <VStack gap={1} align="start">
-              <Text type="label">{copy.reportIssueLabel}</Text>
-              <Text type="supporting" size="sm" color="secondary">{copy.reportIssueHelp}</Text>
-              <HStack gap={2}>
-                <Link href={ISSUE_TRACKER_URL} target="_blank" rel="noreferrer noopener">
-                  {copy.reportIssueOpen}
-                </Link>
-              </HStack>
-            </VStack>
-            {props.onOpenKeyboardHelp && (
-              <VStack gap={1} align="start">
-                <Text type="label">{copy.keyboardShortcuts}</Text>
-                <Text type="supporting" size="sm" color="secondary">{copy.keyboardShortcutsHelp}</Text>
-                <HStack gap={2}>
-                  <Button
-                    variant="ghost"
-                    size="sm"
-                    onClick={props.onOpenKeyboardHelp}
-                    label={copy.keyboardShortcutsOpen}
-                  />
-                </HStack>
-              </VStack>
+      <SettingsSection title={copy.supportTitle}>
+        <SettingsRow
+          label={copy.copyDiagnostics}
+          description={copy.copyHelp}
+          end={(
+            <HStack gap={2} vAlign="center">
+              <Button
+                variant="ghost"
+                size="sm"
+                isDisabled={copyingDiagnostics}
+                onClick={() => void copyDiagnostics()}
+                label={copyingDiagnostics ? copy.copying : copy.copyAction}
+              />
+              <Kbd keys="mod+shift+d" />
+            </HStack>
+          )}
+        />
+        <SettingsRow
+          label={copy.reportIssueLabel}
+          description={copy.reportIssueHelp}
+          end={(
+            <Link href={ISSUE_TRACKER_URL} target="_blank" rel="noreferrer noopener">
+              {copy.reportIssueOpen}
+            </Link>
+          )}
+        />
+        {props.onOpenKeyboardHelp ? (
+          <SettingsRow
+            label={copy.keyboardShortcuts}
+            description={copy.keyboardShortcutsHelp}
+            end={(
+              <Button
+                variant="ghost"
+                size="sm"
+                onClick={props.onOpenKeyboardHelp}
+                label={copy.keyboardShortcutsOpen}
+              />
             )}
-          </VStack>
-        </Grid>
-      </section>
-      {info && (
-        <>
-          <Divider />
-          <section aria-labelledby={privacyHeadingId}>
-            <Grid columns={{ minWidth: 320, max: 2, repeat: 'fit' }} gap={10}>
-              <VStack gap={1}>
-                <Heading level={3} id={privacyHeadingId}>{copy.privacyTitle}</Heading>
-                <Text type="supporting" size="sm" color="secondary">{copy.privacyLede}</Text>
-              </VStack>
-              <List aria-label={copy.privacyLabel} density="compact" listStyle="disc">
-                {/* Fragment-wrapped: ListItem single-line-truncates STRING labels,
-                    and a privacy commitment must wrap, not ellipsize. */}
-                {copy.privacyPoints.map((point) => <ListItem key={point} label={<>{point}</>} />)}
-              </List>
-            </Grid>
-          </section>
-        </>
-      )}
+          />
+        ) : null}
+      </SettingsSection>
+      {info ? (
+        <SettingsSection title={copy.privacyTitle} description={copy.privacyLede} variant="bare">
+          <List aria-label={copy.privacyLabel} density="compact" listStyle="disc">
+            {/* Fragment-wrapped: ListItem single-line-truncates STRING labels,
+                and a privacy commitment must wrap, not ellipsize. */}
+            {copy.privacyPoints.map((point) => <ListItem key={point} label={<>{point}</>} />)}
+          </List>
+        </SettingsSection>
+      ) : null}
     </SettingsPage>
   );
-}
\ No newline at end of file
+}
diff --git a/apps/desktop/src/renderer/settings/about-update-status.ts b/apps/desktop/src/renderer/settings/about-update-status.ts
index 1f02f2a948..0a101b8e0e 100644
--- a/apps/desktop/src/renderer/settings/about-update-status.ts
+++ b/apps/desktop/src/renderer/settings/about-update-status.ts
@@ -22,43 +22,48 @@ import type { SettingsPreferencesCopy } from '../locales/settings-preferences-co
 
 type AboutCopy = SettingsPreferencesCopy['about'];
 
-/** The channel pill's Astryx Badge variant, narrow enough to assign directly. */
-export type AboutChannelBadgeVariant = 'neutral' | 'blue' | 'orange';
+export type AboutChannelKey = 'dev' | 'nightly' | 'release';
 
-export interface AboutChannelBadge {
-  readonly label: string;
-  readonly variant: AboutChannelBadgeVariant;
-  /** The bare channel name without the dev commit suffix — for readouts that
-   *  already sit next to the version pill and don't need it repeated. */
-  readonly channelName: string;
+export interface AboutChannelFacts {
+  readonly key: AboutChannelKey;
+  /** The channel's own name, as the 版本信息 readout and the lead Token use it. */
+  readonly name: string;
+  /** One sentence saying what following this channel means. */
+  readonly summary: string;
+  /**
+   * The lead `Token`, or null. A release install is the default state and gets
+   * no mark: Astryx reserves colour for what departs from it, so tokening every
+   * channel would make none of them stand out.
+   */
+  readonly token: { readonly label: string; readonly color: 'orange' | 'gray' } | null;
 }
 
 /**
- * What the version pill next to "Maka" actually says, pure for unit tests.
+ * What the About lead says about this build's channel, pure for unit tests.
  *
  * Build mode and release channel answer different questions: `buildMode` says
  * how this binary was produced (a checkout vs a packaged install), while
- * `updateChannel` says which release feed it follows. A packaged nightly is a
- * real install but NOT a release, so the old "packaged → 正式版" mapping lied
- * to exactly the users running nightly builds. Dev mode keeps the commit in
- * the label and drops to `neutral`: a checkout is not a release artifact
- * either, so it must not wear the release blue.
+ * `updateChannel` says which release feed it follows. A dev checkout follows no
+ * feed at all — its `updateChannel` is the updater's `release` placeholder — so
+ * `buildMode` decides first, and the old "packaged → 正式版" mapping that lied
+ * to nightly users stays gone.
  */
-export function aboutChannelBadge(
-  info: Pick<DesktopAppInfo, 'buildMode' | 'buildCommit' | 'updateChannel'>,
+export function aboutChannelFacts(
+  info: Pick<DesktopAppInfo, 'buildMode' | 'updateChannel'>,
   copy: AboutCopy,
-): AboutChannelBadge {
-  if (info.buildMode === 'dev') {
-    return {
-      label: info.buildCommit ? `${copy.devBuild} · ${info.buildCommit}` : copy.devBuild,
-      variant: 'neutral',
-      channelName: copy.devBuild,
-    };
-  }
-  if (info.updateChannel === 'nightly') {
-    return { label: copy.nightlyBuild, variant: 'orange', channelName: copy.nightlyBuild };
-  }
-  return { label: copy.packagedBuild, variant: 'blue', channelName: copy.packagedBuild };
+): AboutChannelFacts {
+  const key: AboutChannelKey = info.buildMode === 'dev' ? 'dev' : info.updateChannel;
+  const name = key === 'nightly'
+    ? copy.nightlyBuild
+    : key === 'dev'
+      ? copy.devBuild
+      : copy.packagedBuild;
+  return {
+    key,
+    name,
+    summary: copy.channelSummaries[key],
+    token: key === 'release' ? null : { label: name, color: key === 'nightly' ? 'orange' : 'gray' },
+  };
 }
 
 /** Map updater state to About-page detail copy (pure for unit tests). */
diff --git a/apps/desktop/src/renderer/styles/settings/about.css b/apps/desktop/src/renderer/styles/settings/about.css
index f7774c76fa..28cd1da23c 100644
--- a/apps/desktop/src/renderer/styles/settings/about.css
+++ b/apps/desktop/src/renderer/styles/settings/about.css
@@ -22,20 +22,6 @@
    the About-only `.settingsAboutPage` grid (16px rhythm, breaking the shared
    32px page rhythm) is retired. */
 
-.settingsAboutHero {
-  display: grid;
-  grid-template-columns: 48px minmax(0, 1fr);
-  align-items: center;
-  gap: var(--space-4);
-  padding: 0;
-  border: 0;
-  /* Not a shape decision: this is a bare layout grid — transparent, borderless,
-     unpadded — so it paints no box for a corner to appear on. The declarations
-     strip inherited chrome; DESIGN.md §6 governs boxes that render. */
-  border-radius: 0;
-  background: transparent;
-}
-
 .settingsAboutLogo {
   width: 48px;
   height: 48px;
@@ -52,34 +38,4 @@
   color: var(--brand-deep);
 }
 
-.settingsAboutHeading {
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-  gap: var(--space-2-5);
-  margin: 0 0 var(--space-1);
-}
-
-.settingsAboutHeading h2 {
-  font: var(--maka-text-heading-2);
-  margin: 0;
-  color: var(--foreground);
-  letter-spacing: var(--tracking-normal);
-}
-
-/* #1879: both hero pills are Astryx `Badge`s, and neither has a product rule
-   left. The version keeps the mono stack — a version string is read digit by
-   digit — but takes it from being a `<code>` inside the badge rather than from
-   a rule here: `:where(code, kbd, samp, pre)` in maka-tokens.css is the one
-   monospace authority in the renderer, and it composes the roles a second time
-   against that family, so nothing else about the badge's type moves. The
-   channel pill was a hand-tinted `--brand-deep` wash; `blue` is the tinted
-   variant that keeps it distinguishable from the neutral version pill. */
-
-.settingsAboutTagline {
-  font: var(--maka-text-body);
-  margin: 0;
-  color: var(--muted-foreground);
-}
-
 
diff --git a/apps/desktop/src/renderer/styles/sidebar.css b/apps/desktop/src/renderer/styles/sidebar.css
index 87cff5a137..67b7ddc359 100644
--- a/apps/desktop/src/renderer/styles/sidebar.css
+++ b/apps/desktop/src/renderer/styles/sidebar.css
@@ -524,9 +524,8 @@
   margin-left: auto;
   padding: 0;
   border: 0;
-  /* Same as .settingsAboutHero: un-chroming a <kbd>, not squaring a box. With
-     no padding, border, fill or shadow there is nothing for a radius to act
-     on. */
+  /* Un-chroming a <kbd>, not squaring a box. With no padding, border, fill or
+     shadow there is nothing for a radius to act on. */
   border-radius: 0;
   background: transparent;
   box-shadow: none;
diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx
index d810b0b4c8..afa5b36dbd 100644
--- a/apps/desktop/stories/settings/settings-pages.stories.tsx
+++ b/apps/desktop/stories/settings/settings-pages.stories.tsx
@@ -41,6 +41,7 @@ import type {
 } from '@maka/core/capabilities';
 import type { HealthSignal, HealthSnapshot } from '@maka/core/health';
 import type { DesktopExternalSessionCatalogItem } from '../../src/preload/external-session-catalog';
+import type { AppUpdateStatus } from '../../src/preload/bridge-contract';
 import type { SessionSummary } from '@maka/core/session';
 import { revisionFamilySessionIds } from '@maka/core/session-revisions';
 import type {
@@ -847,6 +848,8 @@ const makaBridge = {
       osRelease: '23.4.0',
       arch: 'arm64',
       buildMode: 'dev',
+      // Dev installs report the updater's release fallback, not a real channel.
+      updateChannel: 'release',
       buildCommit: 'a63ae4d',
       appVersion: '0.9.0-dev',
       electronVersion: '33.2.0',
@@ -857,6 +860,9 @@ const makaBridge = {
       // deep so the mono value exercises its wrap contract.
       workspacePath:
         '/Users/storybook-fixture-user/Library/Application Support/Maka/workspaces/infra-observability-platform-desktop',
+      // The prefix About collapses to `~`. Without it the story showed the
+      // absolute path, which is not what any user sees.
+      homePath: '/Users/storybook-fixture-user',
     }),
     openPath: async () => ({ ok: true as const, opened: '/Users/storybook' }),
     // The 外观 page mounts the app-icon picker as soon as it opens. Without
@@ -964,6 +970,32 @@ const makaBridge = {
 
 const withSettingsBridge = withScopedMakaBridge(makaBridge);
 
+/**
+ * A PACKAGED install, which the shared fixture cannot be: it is a dev checkout,
+ * and `buildMode` short-circuits the About lead before `updateChannel` is ever
+ * read. Only these two facts move — everything else stays the shared bridge, so
+ * the channel stories differ from `About` by exactly what they are about.
+ */
+function withPackagedChannelBridge(channel: {
+  updateChannel: 'nightly' | 'release';
+  appVersion: string;
+  updateStatus: AppUpdateStatus;
+}) {
+  return withScopedMakaBridge({
+    ...makaBridge,
+    app: {
+      ...makaBridge.app,
+      info: async () => ({
+        ...(await makaBridge.app.info()),
+        buildMode: 'packaged' as const,
+        updateChannel: channel.updateChannel,
+        appVersion: channel.appVersion,
+      }),
+      updateStatus: async () => channel.updateStatus,
+    },
+  } satisfies Record<string, unknown>);
+}
+
 function pendingForever<T>(): Promise<T> {
   return new Promise(() => undefined);
 }
@@ -1670,6 +1702,7 @@ function SettingsStoryFrame(props: SettingsStoryProps) {
               initialConnectionSlug={props.initialConnectionSlug}
               initialFocusRef={initialFocusRef}
               onOpenDailyReview={noop}
+              onOpenKeyboardHelp={noop}
               onOpenSession={noop}
               archivedTasks={archivedTasks}
               onTaskImported={noop}
@@ -2584,6 +2617,38 @@ export const About: Story = {
   render: () => <SettingsStory section="about" />,
 };
 
+// Real path: the same page inside a packaged Nightly. Nightly publishes daily
+// and auto-downloads, so `downloaded` — not `not-available` — is what a nightly
+// user actually opens this page to. The version string is the shipped shape:
+// <product>-dev.<run>.<UTC day>.
+export const AboutNightly: Story = {
+  decorators: [
+    withPackagedChannelBridge({
+      updateChannel: 'nightly',
+      appVersion: '0.2.0-dev.12.20260901',
+      updateStatus: {
+        state: 'downloaded',
+        currentVersion: '0.2.0-dev.12.20260901',
+        latestVersion: '0.2.0-dev.13.20260902',
+      },
+    }),
+  ],
+  render: () => <SettingsStory section="about" />,
+};
+
+// Real path: the same page inside a packaged release — the default state, which
+// wears no channel token at all.
+export const AboutRelease: Story = {
+  decorators: [
+    withPackagedChannelBridge({
+      updateChannel: 'release',
+      appVersion: '0.2.0',
+      updateStatus: { state: 'not-available', currentVersion: '0.2.0' },
+    }),
+  ],
+  render: () => <SettingsStory section="about" />,
+};
+
 // Real path: 设置 → 已归档任务, after archiving tasks from the rail's row menu.
 export const ArchivedTasks: Story = {
   decorators: [withSettingsBridge],
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 7942da832c..2ebac40575 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -96,12 +96,6 @@ export * from './primitives/stat-tile.js';
 // slot, implementation swapped behind it. badgeVariants retired with the cva
 // recipe (no consumers).
 export { Badge, type BadgeProps, type BadgeVariant } from '@astryxdesign/core';
-// PageHeader — the shared page-header shell (convergence round 3). One shell
-// for the module hero (as='h2': 技能 / 定时任务) and the settings intros
-// (as='h3': permission / health / about). Wrapper class + per-slot
-// CSS stay at the call site; the primitive converges STRUCTURE only.
-export { PageHeader } from './primitives/page-header.js';
-export type { PageHeaderProps } from './primitives/page-header.js';
 // ModulePage — the ONE shell every module page renders into (Astryx Layout,
 // incident-console archetype). Born in this package for 定时任务 / 每日回顾;
 // exported so the renderer-owned MCP page renders the same surface.
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index 613c843667..659d9415f7 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -82,12 +82,6 @@
 .maka-quote-chip-remove:hover { color: var(--foreground); background: var(--state-hover-bg); }
 .maka-quote-chip-remove:focus-visible { color: var(--foreground); background: var(--state-hover-bg); outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: 1px; }
 
-.maka-page-header-title,
-.maka-page-header-eyebrow,
-.maka-page-header-subtitle { margin: 0; }
-.maka-page-header-content { min-width: 0; }
-.maka-page-header-heading-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
-
 
 /* One shape. The tone and emphasis modifiers that used to sit here — a filled
    plate, four tinted borders, four value inks, a dimmed zero — were reachable


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