From c5327a3faf16f8210212295788f41d13fe0e7241 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Wed, 2 Sep 2026 12:17:12 +0800 Subject: [PATCH 1/7] fix(desktop): label the About build pill by update channel, not build mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The About page mapped `buildMode === 'packaged'` to 正式版, so a packaged nightly install (version `0.2.0-dev..`) wore a release badge that contradicted its own version string. Build mode and release channel answer different questions - how the binary was produced vs which feed it follows - and the page only ever saw the former. `app:info` now carries `updateChannel`, resolved from the packaged manifest by the same `desktopUpdateChannelFromManifest` parse that drives the updater, so the pill and the update feed share one authority. The pill logic moves into a pure `aboutChannelBadge` helper: nightly → Nightly (orange), release → 正式版 (blue), dev → 本地开发版 · commit (neutral, a checkout is not a release artifact either). Generated-by: Maka --- .../__tests__/about-channel-badge.test.ts | 55 ++++++++++++++++++ apps/desktop/src/main/app-ipc-main.ts | 5 +- apps/desktop/src/main/runtime-host-boot.ts | 1 + apps/desktop/src/preload/bridge-contract.d.ts | 4 ++ .../locales/settings-preferences-copy.ts | 5 +- .../renderer/settings/about-channel-badge.ts | 58 +++++++++++++++++++ .../renderer/settings/about-settings-page.tsx | 11 +--- 7 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/about-channel-badge.test.ts create mode 100644 apps/desktop/src/renderer/settings/about-channel-badge.ts diff --git a/apps/desktop/src/main/__tests__/about-channel-badge.test.ts b/apps/desktop/src/main/__tests__/about-channel-badge.test.ts new file mode 100644 index 0000000000..d083da9b1f --- /dev/null +++ b/apps/desktop/src/main/__tests__/about-channel-badge.test.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { aboutChannelBadge } from '../../renderer/settings/about-channel-badge.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' }); +}); + +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' }); +}); + +test('a dev checkout carries its commit on a neutral pill', () => { + const withCommit = aboutChannelBadge( + { buildMode: 'dev', buildCommit: 'abc1234', updateChannel: 'release' }, + copy, + ); + assert.deepEqual(withCommit, { label: '本地开发版 · abc1234', variant: 'neutral' }); + + const withoutCommit = aboutChannelBadge( + { buildMode: 'dev', buildCommit: null, updateChannel: 'nightly' }, + copy, + ); + assert.deepEqual(withoutCommit, { label: '本地开发版', variant: 'neutral' }); +}); \ No newline at end of file diff --git a/apps/desktop/src/main/app-ipc-main.ts b/apps/desktop/src/main/app-ipc-main.ts index ceb9ac4dd2..0260ccf205 100644 --- a/apps/desktop/src/main/app-ipc-main.ts +++ b/apps/desktop/src/main/app-ipc-main.ts @@ -26,6 +26,7 @@ import type { ProjectRootController } from './project-root-controller.js'; import { resolveOpenPath, type OpenPathResult } from './open-path-guard.js'; import { getE2eFixtureState, type resolveE2eFixture } from './e2e-fixture.js'; import type { resolveBuildInfo } from './build-info.js'; +import type { DesktopUpdateChannel } from './app-update-attestation.js'; import type { AppUpdateInstallRequest, AppUpdateService, @@ -47,6 +48,7 @@ export interface AppIpcDeps { getProjectRoot(sessionId: unknown): Promise; workspaceRoot: string; buildInfo: BuildInfo; + updateChannel: DesktopUpdateChannel; e2eFixture: E2eFixture; projectManagement: ProjectManagementService; allowLocalProjectPaths?: boolean; @@ -89,7 +91,7 @@ export function registerAppIpc( deps: AppIpcDeps, targetIpc: ReconnectableReadIpcMain = ipcMain, ): void { - const { projectRoot, workspaceRoot, buildInfo, e2eFixture } = deps; + const { projectRoot, workspaceRoot, buildInfo, updateChannel, e2eFixture } = deps; const allowLocalProjectPaths = deps.allowLocalProjectPaths !== false; // Call-time read of the shared project-root authority: every handler must // observe the latest selection, not a snapshot taken at registration. @@ -120,6 +122,7 @@ export function registerAppIpc( : { isGitRepo: false }, buildMode: buildInfo.mode, buildCommit: buildInfo.commit, + updateChannel, }; }); handleReconnectableRead(targetIpc, 'projects:getSnapshot', () => diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index cee0633822..75e05259fb 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -1530,6 +1530,7 @@ function registerHostClientIpc( getProjectRoot: resolveProjectRootForContext, workspaceRoot, buildInfo, + updateChannel: desktopUpdateChannel, e2eFixture, projectManagement: targetProjectManagement, allowLocalProjectPaths: !usesHostWorkspace, diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index f825aaf1cb..8c4c5431d9 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -711,6 +711,10 @@ export interface DesktopAppInfo { readonly projectGit: { readonly isGitRepo: boolean; readonly branch?: string }; readonly buildMode: 'dev' | 'packaged'; readonly buildCommit: string | null; + /** Packaged update channel, resolved from the packaged manifest — the same + * authority the updater itself reads. 'release' is also the dev-mode + * fallback, where the channel is meaningless. */ + readonly updateChannel: 'release' | 'nightly'; } /** diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts index 3c742388d7..65f904784b 100644 --- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts @@ -227,6 +227,7 @@ export type SettingsPreferencesCopy = { clipboardUnavailable: string; devBuild: string; packagedBuild: string; + nightlyBuild: string; subtitle: string; privacyLabel: string; privacyTitle: string; @@ -339,7 +340,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { passwordSavedPlaceholder: '密码已保存;输入新密码以替换', }, about: { - loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制诊断信息', pasteHint: '检查内容后,可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', subtitle: '本地优先的 AI 助手 · 桌面端运行环境', privacyLabel: '隐私与安全', privacyTitle: '本地优先 · 隐私默认', privacyPoints: ['所有任务、设置、凭据和 Skill 指令文件都保留在本机工作区。', '模型密钥保存在本机凭据文件内;订阅账号令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在任务内明示授权。', '每个任务都会在本机保留消息、工具调用、权限决策与模式变更记录。'], copying: '复制中…', copyDiagnostics: '复制诊断信息', copyHelp: '复制版本、平台、隐藏主目录后的工作区路径,以及近期脱敏的 Desktop 与 Runtime Host 日志;仅写入剪贴板,不会自动上传。', keyboardShortcuts: '键盘快捷键', keyboardShortcutsHelp: 'Maka 支持的全部快捷键一览。', keyboardShortcutsOpen: '查看', reportIssueLabel: '报告问题', + loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制诊断信息', pasteHint: '检查内容后,可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', nightlyBuild: 'Nightly', subtitle: '本地优先的 AI 助手 · 桌面端运行环境', privacyLabel: '隐私与安全', privacyTitle: '本地优先 · 隐私默认', privacyPoints: ['所有任务、设置、凭据和 Skill 指令文件都保留在本机工作区。', '模型密钥保存在本机凭据文件内;订阅账号令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在任务内明示授权。', '每个任务都会在本机保留消息、工具调用、权限决策与模式变更记录。'], copying: '复制中…', copyDiagnostics: '复制诊断信息', copyHelp: '复制版本、平台、隐藏主目录后的工作区路径,以及近期脱敏的 Desktop 与 Runtime Host 日志;仅写入剪贴板,不会自动上传。', keyboardShortcuts: '键盘快捷键', keyboardShortcutsHelp: 'Maka 支持的全部快捷键一览。', keyboardShortcutsOpen: '查看', reportIssueLabel: '报告问题', updatesTitle: '软件更新', checkForUpdates: '检查更新', checkingForUpdates: '检查中…', @@ -393,7 +394,7 @@ 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', subtitle: 'A local-first AI assistant · Desktop runtime', privacyLabel: 'Privacy and security', privacyTitle: 'Local first · Private by default', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay in the local workspace.', 'Model keys stay in a local credential file; 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 task.', 'Messages, tool calls, permission decisions, and mode changes are retained locally for each task.'], copying: 'Copying…', copyDiagnostics: 'Copy diagnostics', 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.', keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View', reportIssueLabel: 'Report an issue', + 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', privacyLabel: 'Privacy and security', privacyTitle: 'Local first · Private by default', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay in the local workspace.', 'Model keys stay in a local credential file; 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 task.', 'Messages, tool calls, permission decisions, and mode changes are retained locally for each task.'], copying: 'Copying…', copyDiagnostics: 'Copy diagnostics', 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.', keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View', reportIssueLabel: 'Report an issue', updatesTitle: 'Software updates', checkForUpdates: 'Check for updates', checkingForUpdates: 'Checking…', diff --git a/apps/desktop/src/renderer/settings/about-channel-badge.ts b/apps/desktop/src/renderer/settings/about-channel-badge.ts new file mode 100644 index 0000000000..3d54463cb6 --- /dev/null +++ b/apps/desktop/src/renderer/settings/about-channel-badge.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +import type { DesktopAppInfo } from '../../preload/bridge-contract.js'; +import type { SettingsPreferencesCopy } from '../locales/settings-preferences-copy.js'; + +type AboutCopy = SettingsPreferencesCopy['about']; + +/** The channel pill's Astryx Badge variant, narrow enough to assign directly. */ +export type AboutChannelBadgeVariant = 'neutral' | 'blue' | 'orange'; + +export interface AboutChannelBadge { + readonly label: string; + readonly variant: AboutChannelBadgeVariant; +} + +/** + * What the version pill next to "Maka" actually says, 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. + */ +export function aboutChannelBadge( + info: Pick, + copy: AboutCopy, +): AboutChannelBadge { + if (info.buildMode === 'dev') { + return { + label: info.buildCommit ? `${copy.devBuild} · ${info.buildCommit}` : copy.devBuild, + variant: 'neutral', + }; + } + if (info.updateChannel === 'nightly') { + return { label: copy.nightlyBuild, variant: 'orange' }; + } + return { label: copy.packagedBuild, variant: 'blue' }; +} \ No newline at end of file diff --git a/apps/desktop/src/renderer/settings/about-settings-page.tsx b/apps/desktop/src/renderer/settings/about-settings-page.tsx index 6b5c0c056e..8abc5e2899 100644 --- a/apps/desktop/src/renderer/settings/about-settings-page.tsx +++ b/apps/desktop/src/renderer/settings/about-settings-page.tsx @@ -36,6 +36,7 @@ import { settingsActionErrorMessage } from './settings-error-copy.js'; import { SettingsSkeletonStack } from './settings-skeleton.js'; import { useActionGuard } from './use-action-guard.js'; import { aboutUpdateStatusDetail } from './about-update-status.js'; +import { aboutChannelBadge } from './about-channel-badge.js'; import { getSettingsPreferencesCopy } from '../locales/settings-preferences-copy.js'; import { getSettingsSharedCopy } from '../locales/settings-shared-copy.js'; import { @@ -161,6 +162,7 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) { description={infoError} /> ); } else { + const channelBadge = aboutChannelBadge(info, copy); aboutContent = ( <> - + } subtitle={copy.subtitle} From 1504655d3b0a5e30bbdaba94ce76eb1e8325d0c2 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Wed, 2 Sep 2026 12:25:53 +0800 Subject: [PATCH 2/7] refactor(desktop): rebuild the About page on the Astryx settings-template anatomy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old page had no focus: five privacy bullets opened it, the actions a user actually needs were scattered (update check mid-page, diagnostics + shortcut + issue link crammed into one bottom row), and the page promised 运行环境 without showing any. A dev build also printed the same 本地开发版不检查 GitHub 发布 更新 sentence twice - once as row detail, once as help text. The page now follows the vendored Astryx pages/settings template anatomy: two-column Grid sections (Heading 3 + supporting lede left, content right) with a bare Divider between sections, and no row hairlines - the kit makes dividers opt-in everywhere. Facts (channel / runtime / workspace) render as a MetadataList, the same label-to-value construction the MCP detail panel uses. Order follows the page's real jobs: identity facts, software updates, support and diagnostics (merged from the old 参考 + 版本信息 sections), privacy last, compressed from five bullets to three. Support stays outside the info conditional so diagnostics remain copyable when app.info fails - the state the existing SSR test locks in. The channel pill's helper moves into about-update-status.ts instead of a new module: the architecture ledger forbids growing the legacy AppShell closure, and this was that module's seam already. The ledger shrinks by the two dependencies the page no longer imports. Generated-by: Maka --- apps/desktop/e2e/about-page.spec.ts | 55 ++++ apps/desktop/renderer-architecture.json | 2 - ...ge.test.ts => about-update-status.test.ts} | 28 +- .../locales/settings-preferences-copy.ts | 36 ++- .../renderer/locales/settings-shared-copy.ts | 6 - .../renderer/settings/about-channel-badge.ts | 58 ---- .../renderer/settings/about-settings-page.tsx | 282 +++++++++++------- .../renderer/settings/about-update-status.ts | 41 ++- 8 files changed, 332 insertions(+), 176 deletions(-) create mode 100644 apps/desktop/e2e/about-page.spec.ts rename apps/desktop/src/main/__tests__/{about-channel-badge.test.ts => about-update-status.test.ts} (75%) delete mode 100644 apps/desktop/src/renderer/settings/about-channel-badge.ts diff --git a/apps/desktop/e2e/about-page.spec.ts b/apps/desktop/e2e/about-page.spec.ts new file mode 100644 index 0000000000..26ca1022df --- /dev/null +++ b/apps/desktop/e2e/about-page.spec.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +import { ensureSidebarExpanded, expect, test } from './fixtures'; + +test('About renders channel facts, support, and privacy sections', async ({ window: page }) => { + await ensureSidebarExpanded(page); + 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 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(); + await expect(page.getByText(/Electron \d+\.\d+\.\d+/)).toBeVisible(); + await expect(page.locator('dd code')).toBeVisible(); + + // Dev builds do not poll GitHub releases; the check stays disabled and the + // copy says why instead of pretending "已是最新版本". + await expect(page.getByRole('heading', { name: '软件更新' })).toBeVisible(); + await expect(page.getByText('本地开发版不检查 GitHub 发布更新。请使用正式安装包。')).toBeVisible(); + await expect(page.getByRole('button', { name: '检查更新' })).toBeDisabled(); + + // 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('button', { name: '复制', exact: true })).toBeEnabled(); + await expect(page.getByRole('button', { name: '查看' })).toBeEnabled(); + + // Three commitments, not the old wall of five bullets. + const privacyList = page.getByRole('list', { name: '隐私承诺' }); + await expect(privacyList.getByRole('listitem')).toHaveCount(3); +}); \ No newline at end of file diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 77fd99edad..52caaeb730 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -2585,10 +2585,8 @@ "../../preload/bridge-contract.js": 1, "../default-runtime-host-operation.js": 1, "../locales/settings-preferences-copy.js": 1, - "../locales/settings-shared-copy.js": 1, "./about-update-status.js": 1, "./settings-error-copy.js": 1, - "./settings-rows.js": 1, "./settings-section.js": 1, "./settings-skeleton.js": 1, "./use-action-guard.js": 1, diff --git a/apps/desktop/src/main/__tests__/about-channel-badge.test.ts b/apps/desktop/src/main/__tests__/about-update-status.test.ts similarity index 75% rename from apps/desktop/src/main/__tests__/about-channel-badge.test.ts rename to apps/desktop/src/main/__tests__/about-update-status.test.ts index d083da9b1f..2e0043c805 100644 --- a/apps/desktop/src/main/__tests__/about-channel-badge.test.ts +++ b/apps/desktop/src/main/__tests__/about-update-status.test.ts @@ -19,7 +19,9 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; -import { aboutChannelBadge } from '../../renderer/settings/about-channel-badge.js'; +import { + aboutChannelBadge, +} from '../../renderer/settings/about-update-status.js'; import { getSettingsPreferencesCopy } from '../../renderer/locales/settings-preferences-copy.js'; const copy = getSettingsPreferencesCopy('zh').about; @@ -29,7 +31,11 @@ test('a packaged nightly build is labelled Nightly, never 正式版', () => { { buildMode: 'packaged', buildCommit: null, updateChannel: 'nightly' }, copy, ); - assert.deepEqual(badge, { label: 'Nightly', variant: 'orange' }); + assert.deepEqual(badge, { + label: 'Nightly', + variant: 'orange', + channelName: 'Nightly', + }); }); test('a packaged release build keeps the release pill', () => { @@ -37,7 +43,11 @@ test('a packaged release build keeps the release pill', () => { { buildMode: 'packaged', buildCommit: null, updateChannel: 'release' }, copy, ); - assert.deepEqual(badge, { label: '正式版', variant: 'blue' }); + assert.deepEqual(badge, { + label: '正式版', + variant: 'blue', + channelName: '正式版', + }); }); test('a dev checkout carries its commit on a neutral pill', () => { @@ -45,11 +55,19 @@ test('a dev checkout carries its commit on a neutral pill', () => { { buildMode: 'dev', buildCommit: 'abc1234', updateChannel: 'release' }, copy, ); - assert.deepEqual(withCommit, { label: '本地开发版 · abc1234', variant: 'neutral' }); + assert.deepEqual(withCommit, { + label: '本地开发版 · abc1234', + variant: 'neutral', + channelName: '本地开发版', + }); const withoutCommit = aboutChannelBadge( { buildMode: 'dev', buildCommit: null, updateChannel: 'nightly' }, copy, ); - assert.deepEqual(withoutCommit, { label: '本地开发版', variant: 'neutral' }); + assert.deepEqual(withoutCommit, { + label: '本地开发版', + variant: 'neutral', + channelName: '本地开发版', + }); }); \ No newline at end of file diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts index 65f904784b..25d195cc4d 100644 --- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts @@ -229,6 +229,18 @@ export type SettingsPreferencesCopy = { packagedBuild: string; nightlyBuild: string; subtitle: string; + channelLabel: string; + runtimeLabel: string; + workspaceLabel: string; + channelSummaries: Record<'dev' | 'nightly' | 'release', string>; + platformNames: Record; + updatesLede: string; + supportTitle: string; + supportLede: string; + reportIssueHelp: string; + reportIssueOpen: string; + copyAction: string; + privacyLede: string; privacyLabel: string; privacyTitle: string; privacyPoints: readonly string[]; @@ -340,8 +352,16 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = { passwordSavedPlaceholder: '密码已保存;输入新密码以替换', }, about: { - loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制诊断信息', pasteHint: '检查内容后,可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', nightlyBuild: 'Nightly', subtitle: '本地优先的 AI 助手 · 桌面端运行环境', privacyLabel: '隐私与安全', privacyTitle: '本地优先 · 隐私默认', privacyPoints: ['所有任务、设置、凭据和 Skill 指令文件都保留在本机工作区。', '模型密钥保存在本机凭据文件内;订阅账号令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在任务内明示授权。', '每个任务都会在本机保留消息、工具调用、权限决策与模式变更记录。'], copying: '复制中…', copyDiagnostics: '复制诊断信息', copyHelp: '复制版本、平台、隐藏主目录后的工作区路径,以及近期脱敏的 Desktop 与 Runtime Host 日志;仅写入剪贴板,不会自动上传。', keyboardShortcuts: '键盘快捷键', keyboardShortcutsHelp: 'Maka 支持的全部快捷键一览。', keyboardShortcutsOpen: '查看', reportIssueLabel: '报告问题', - updatesTitle: '软件更新', + loadFailed: '载入关于信息失败', loading: '正在加载关于页', unavailable: '无法载入关于信息', copied: '已复制诊断信息', pasteHint: '检查内容后,可直接粘贴到问题报告', copyFailed: '复制失败', clipboardUnavailable: '剪贴板不可用或被系统拒绝。', devBuild: '本地开发版', packagedBuild: '正式版', nightlyBuild: 'Nightly', subtitle: '本地优先的 AI 助手 · 桌面端运行环境', + channelLabel: '渠道', runtimeLabel: '运行环境', workspaceLabel: '工作区', + channelSummaries: { dev: '不自动更新', nightly: '每晚构建,自动跟随更新', release: '打包安装,自动检查更新' }, + platformNames: { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }, + updatesTitle: '软件更新', updatesLede: 'Maka 检查并安装新版本的渠道与状态。', + supportTitle: '支持与诊断', supportLede: '出问题时的三件事:取证、上报、查快捷键。', + copying: '复制中…', copyDiagnostics: '复制诊断信息', copyAction: '复制', copyHelp: '复制版本、平台、隐藏主目录后的工作区路径,以及近期脱敏的 Desktop 与 Runtime Host 日志;仅写入剪贴板,不会自动上传。', + reportIssueLabel: '报告问题', reportIssueHelp: '带上诊断信息去 GitHub Issues,回复更快。', reportIssueOpen: '打开', + keyboardShortcuts: '键盘快捷键', keyboardShortcutsHelp: 'Maka 支持的全部快捷键一览。', keyboardShortcutsOpen: '查看', + privacyLabel: '隐私承诺', privacyTitle: '隐私承诺', privacyLede: 'Maka 对本机数据与通信的三条承诺。', privacyPoints: ['任务、设置、凭据和 Skill 指令文件都留在本机;模型密钥保存在本机凭据文件内,订阅令牌使用系统安全存储。', 'Maka 不发送使用遥测;只在你显式启用时与所选模型供应商通信。', '高风险工具操作需要在任务内明示授权;每个任务都会在本机保留消息、工具调用、权限决策与模式变更记录。'], checkForUpdates: '检查更新', checkingForUpdates: '检查中…', updateHelp: '后台也会定期检查;需要重启安装时侧栏会提示。', @@ -394,8 +414,16 @@ 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', privacyLabel: 'Privacy and security', privacyTitle: 'Local first · Private by default', privacyPoints: ['Tasks, settings, credentials, and Skill instructions stay in the local workspace.', 'Model keys stay in a local credential file; 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 task.', 'Messages, tool calls, permission decisions, and mode changes are retained locally for each task.'], copying: 'Copying…', copyDiagnostics: 'Copy diagnostics', 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.', keyboardShortcuts: 'Keyboard shortcuts', keyboardShortcutsHelp: 'Every shortcut Maka responds to.', keyboardShortcutsOpen: 'View', reportIssueLabel: 'Report an issue', - updatesTitle: 'Software updates', + 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' }, + 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.', + 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 task; messages, tool calls, permission decisions, and mode changes are retained locally for each task.'], 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/locales/settings-shared-copy.ts b/apps/desktop/src/renderer/locales/settings-shared-copy.ts index 834619160d..f414806ed0 100644 --- a/apps/desktop/src/renderer/locales/settings-shared-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-shared-copy.ts @@ -64,8 +64,6 @@ export type SettingsSharedCopy = { memoryEntriesHelp: string; reviewSchedule: string; reviewScheduleHelp: string; - buildInfo: string; - reference: string; }; }; @@ -108,8 +106,6 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { dataLocationHelp: '任务、设置、使用统计与凭据都以文件形式存放在本机的这个位置。', reviewSchedule: '回顾计划', reviewScheduleHelp: '每日回顾的生成时间与使用的模型。', - buildInfo: '版本信息', - reference: '参考', }, }, en: { @@ -150,8 +146,6 @@ const SETTINGS_SHARED_COPY_BY_LOCALE = { dataLocationHelp: 'Tasks, settings, usage statistics, and credentials are stored as files in this location on your machine.', reviewSchedule: 'Review schedule', reviewScheduleHelp: 'When the daily review runs, and which model writes it.', - buildInfo: 'Build info', - reference: 'Reference', }, }, } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/settings/about-channel-badge.ts b/apps/desktop/src/renderer/settings/about-channel-badge.ts deleted file mode 100644 index 3d54463cb6..0000000000 --- a/apps/desktop/src/renderer/settings/about-channel-badge.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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. - */ - -import type { DesktopAppInfo } from '../../preload/bridge-contract.js'; -import type { SettingsPreferencesCopy } from '../locales/settings-preferences-copy.js'; - -type AboutCopy = SettingsPreferencesCopy['about']; - -/** The channel pill's Astryx Badge variant, narrow enough to assign directly. */ -export type AboutChannelBadgeVariant = 'neutral' | 'blue' | 'orange'; - -export interface AboutChannelBadge { - readonly label: string; - readonly variant: AboutChannelBadgeVariant; -} - -/** - * What the version pill next to "Maka" actually says, 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. - */ -export function aboutChannelBadge( - info: Pick, - copy: AboutCopy, -): AboutChannelBadge { - if (info.buildMode === 'dev') { - return { - label: info.buildCommit ? `${copy.devBuild} · ${info.buildCommit}` : copy.devBuild, - variant: 'neutral', - }; - } - if (info.updateChannel === 'nightly') { - return { label: copy.nightlyBuild, variant: 'orange' }; - } - return { label: copy.packagedBuild, variant: 'blue' }; -} \ No newline at end of file diff --git a/apps/desktop/src/renderer/settings/about-settings-page.tsx b/apps/desktop/src/renderer/settings/about-settings-page.tsx index 8abc5e2899..45cc3c1833 100644 --- a/apps/desktop/src/renderer/settings/about-settings-page.tsx +++ b/apps/desktop/src/renderer/settings/about-settings-page.tsx @@ -18,7 +18,20 @@ */ import { useEffect, useId, useState, type ReactNode } from 'react'; -import { Badge, Link, List, ListItem } from '@astryxdesign/core'; +import { + Badge, + Divider, + Grid, + Heading, + HStack, + Link, + List, + ListItem, + MetadataList, + MetadataListItem, + Text, + VStack, +} from '@astryxdesign/core'; import { Kbd } from '@astryxdesign/core/Kbd'; import { Sparkles } from '@maka/ui/icons'; import { @@ -30,15 +43,12 @@ import { useUiLocale, } from '@maka/ui'; import type { AppUpdateStatus } from '../../preload/bridge-contract.js'; -import { SettingsActions, SettingsPage, SettingsSection } from './settings-section.js'; -import { SettingRow } from './settings-rows.js'; +import { SettingsPage } 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 { aboutUpdateStatusDetail } from './about-update-status.js'; -import { aboutChannelBadge } from './about-channel-badge.js'; +import { aboutChannelBadge, aboutUpdateStatusDetail } from './about-update-status.js'; import { getSettingsPreferencesCopy } from '../locales/settings-preferences-copy.js'; -import { getSettingsSharedCopy } from '../locales/settings-shared-copy.js'; import { defaultRuntimeHostDiagnosticTarget, runOnDefaultRuntimeHost, @@ -51,7 +61,6 @@ const ISSUE_TRACKER_URL = 'https://github.com/apache/maka/issues'; export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) { const locale = useUiLocale(); const copy = getSettingsPreferencesCopy(locale).about; - const sharedCopy = getSettingsSharedCopy(locale); const [info, setInfo] = useState(null); const [infoError, setInfoError] = useState(null); const [copyingDiagnostics, setCopyingDiagnostics] = useState(false); @@ -63,6 +72,9 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) { const toast = useToast(); const diagnosticsHelpId = useId(); const updateHelpId = useId(); + const updatesHeadingId = useId(); + const supportHeadingId = useId(); + const privacyHeadingId = useId(); useEffect(() => { let cancelled = false; @@ -74,16 +86,17 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) { } }) .catch((error) => { - if (cancelled) return; - const message = settingsActionErrorMessage(error, locale); - setInfoError(message); - toast.error( - copy.loadFailed, - message, - undefined, - defaultRuntimeHostDiagnosticTarget(error), - ); - }); + if (!cancelled) { + const message = settingsActionErrorMessage(error, locale); + setInfoError(message); + toast.error( + copy.loadFailed, + message, + undefined, + defaultRuntimeHostDiagnosticTarget(error), + ); + } + }); return () => { cancelled = true; }; @@ -163,75 +176,86 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) { ); } else { const channelBadge = aboutChannelBadge(info, copy); + const channelKey = info.buildMode === 'dev' ? 'dev' : info.updateChannel; + 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 = ( <> - /* 64% of the 48px plate, matching .providerLogo's fill */} - iconClassName="settingsAboutLogo" - headingRowClassName="settingsAboutHeading" - title="Maka" - badge={ - <> - - - - } - subtitle={copy.subtitle} - subtitleClassName="settingsAboutTagline" - /> - {/* Detail audit: the five privacy commitments rendered inside an info - Banner — five lines of bold status-blue body copy, the exact blue - flood DESIGN.md's Signal-Not-Texture rule forbids. They are ordinary - statements, so they read as a quiet marker list in a labeled group. */} - - - {/* Fragment-wrapped: ListItem single-line-truncates STRING labels, - and a privacy commitment must wrap, not ellipsize. */} - {copy.privacyPoints.map((point) => {point}} />)} - - - {/* The keyboard sheet's home. It used to be reachable only from the - titlebar's `…` drawer and from two shortcuts — which made the panel - that lists the shortcuts openable only by shortcut. It is reference - material about the app, so it belongs on 关于, and this is the entry - a mouse can find. */} - {props.onOpenKeyboardHelp && ( - - - )} - /> - - )} - - void checkForUpdates()} - label={checkingUpdate || updateStatus?.state === 'checking' - ? copy.checkingForUpdates - : copy.checkForUpdates} - /> - )} + + /* 64% of the 48px plate, matching .providerLogo's fill */} + iconClassName="settingsAboutLogo" + headingRowClassName="settingsAboutHeading" + title="Maka" + badge={ + <> + + + + } + subtitle={copy.subtitle} + subtitleClassName="settingsAboutTagline" /> -

- {info.buildMode === 'dev' ? copy.updateDevBuildHelp : copy.updateHelp} -

-
+ {/* 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. */} + + + + {channelBadge.channelName} · {copy.channelSummaries[channelKey]} + + + + + {copy.platformNames[info.platform] ?? info.platform} · {info.arch} · Electron {info.electronVersion} + + + + {workspaceDisplay} + + + + +
+ + + {copy.updatesTitle} + {copy.updatesLede} + + + {/* 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. */} + + {aboutUpdateStatusDetail(updateStatus, copy, { isDevBuild })} + + {!isDevBuild && ( + + {copy.updateHelp} + + )} + +
+ ); } @@ -239,22 +263,80 @@ export function AboutSettingsPage(props: { onOpenKeyboardHelp?(): void }) { return ( {aboutContent} - - -