diff --git a/packages/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx
index 549dc31997..da7cce570e 100644
--- a/packages/client/ui-primitives/src/icons/index.tsx
+++ b/packages/client/ui-primitives/src/icons/index.tsx
@@ -856,6 +856,20 @@ export const IconQuestionOutline14 = ({ size = 14, className }: IconProps) => (
)
+/** ic_ds_info_outline_16: ring + 'i' glyph, used for the About settings section. */
+export const IconInfoOutline16 = ({ size = 16, className }: IconProps) => (
+
+)
+
/** ic_ds_archive_outline_20 (figma extract): lidded box + label slot. The export's
* 0.11px stroke ring around the box contour is dropped — it restates the same
* contour in the same ink, which currentColor already carries. */
diff --git a/packages/client/ui-primitives/tests/icons.client.spec.tsx b/packages/client/ui-primitives/tests/icons.client.spec.tsx
index 469c5aaf55..87944b6698 100644
--- a/packages/client/ui-primitives/tests/icons.client.spec.tsx
+++ b/packages/client/ui-primitives/tests/icons.client.spec.tsx
@@ -16,8 +16,8 @@ const icons = Object.fromEntries(
const iconNames = Object.keys(icons)
describe('ic_ds_ icon set', () => {
- it('exports the full icon set (46 deepsuite + 20 figma extracts + four product glyphs outside those sets)', () => {
- expect(iconNames.length).toBe(70)
+ it('exports the full icon set (46 deepsuite + 20 figma extracts + product glyphs outside those sets)', () => {
+ expect(iconNames.length).toBe(71)
})
it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', (name) => {
diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml
index 77b5c4e2d3..9d067652f2 100644
--- a/packages/client/ui-settings-general/README.i18n.yaml
+++ b/packages/client/ui-settings-general/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-settings-general/README.md
-README.md: c59b77617cfd5848553ba340e97bafe37b7b2a2f
-README.zh.md: ea4896284b677a7b430cceb949b53aa6e3a5a242
+README.md: 4c3d00b31c4961d2b19afbfa6d5647652a0dbe62
+README.zh.md: b1a5c4a58fddda84567095396aa571fd97bbb832
diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md
index c59b77617c..4c3d00b31c 100644
--- a/packages/client/ui-settings-general/README.md
+++ b/packages/client/ui-settings-general/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-Settings shell, ownerless copy, and durable product-onboarding namespace. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, projects the `settings.section` ledger into the navigation and the `settings.onboarding` ledger into one mounted step at a time, and registers everything on the Settings pages that belongs to no single feature — the trigger/header/close chrome content, the local configuration-file action, the General section and its `settings.general.item` slot, and the `settings` dictionaries. The slot types it renders into belong to ui-settings, the settings domain base; only the shell's own contract types live here, because they reference ui-sidebar's slot type and the base layer must depend on no `ui-*` package. Feature-owned rows (Permission, Language, Appearance), sections (Models), and conditional onboarding steps stay with their feature packages.
+Settings shell, ownerless copy, and durable product-onboarding namespace. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, projects the `settings.section` ledger into the navigation and the `settings.onboarding` ledger into one mounted step at a time, and registers everything on the Settings pages that belongs to no single feature — the trigger/header/close chrome content, the local configuration-file action, the General section and its `settings.general.item` slot, the About section (PineSound company copy and a client-side version check), and the `settings` dictionaries. The slot types it renders into belong to ui-settings, the settings domain base; only the shell's own contract types live here, because they reference ui-sidebar's slot type and the base layer must depend on no `ui-*` package. Feature-owned rows (Permission, Language, Appearance), sections (Models), and conditional onboarding steps stay with their feature packages.
The shell ships no onboarding copy of its own — all text arrives from registrants. Nav labels may be locale-following thunks, so the nav projection resolves them through `resolveSlotLabel` and re-renders on the section ledger bump or the locale revision (an optional `ctx.get('locale')` read; no hard locale dependency). The onboarding ledger projects in ascending order and mounts exactly one step at a time. Visible steps own their dialog chrome and app-root `inert` lifecycle; a mounted step still resolving private facts renders null, so nothing paints or blocks while it decides. The active registrant receives its id, `complete()`, and an `openSection(id)` callback; completing or skipping transfers ownership to the next entry. Registrants own durable completion, capability readiness, copy, mutations, and their visible wrapper, so independently registered flows cannot stack and the shell does not become a second configuration fact source.
diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md
index ea4896284b..b1a5c4a58f 100644
--- a/packages/client/ui-settings-general/README.zh.md
+++ b/packages/client/ui-settings-general/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-设置外壳、无特定功能归属文案与持久化产品引导 namespace。它以触发控件和模态设置面板占用 `sidebar.settings`,把 `settings.section` 账本投影成导航、把 `settings.onboarding` 账本投影成每次只挂载一个步骤的引导流程,并在设置页面上注册所有不属于单一功能的内容:触发器、标题栏与关闭控件内容、本地配置文件操作,「通用」分区及其 `settings.general.item` slot,以及 `settings` 字典。它渲染进的那些 slot 类型归 ui-settings——设置领域底座——所有;只有外壳自身的契约类型放在这里,因为它们引用 ui-sidebar 的 slot 类型,而底座不得依赖任何 `ui-*` 包。归具体功能所有的行(「权限」、「语言」、「外观」)、分区(「模型」)和条件式首次使用引导步骤仍由各自的功能包提供。
+设置外壳、无特定功能归属文案与持久化产品引导 namespace。它以触发控件和模态设置面板占用 `sidebar.settings`,把 `settings.section` 账本投影成导航、把 `settings.onboarding` 账本投影成每次只挂载一个步骤的引导流程,并在设置页面上注册所有不属于单一功能的内容:触发器、标题栏与关闭控件内容、本地配置文件操作,「通用」分区及其 `settings.general.item` slot、「关于」分区(PineSound 企业介绍与客户端侧版本检查),以及 `settings` 字典。它渲染进的那些 slot 类型归 ui-settings——设置领域底座——所有;只有外壳自身的契约类型放在这里,因为它们引用 ui-sidebar 的 slot 类型,而底座不得依赖任何 `ui-*` 包。归具体功能所有的行(「权限」、「语言」、「外观」)、分区(「模型」)和条件式首次使用引导步骤仍由各自的功能包提供。
外壳不自带引导文案:所有文本都来自注册方。导航 label 可以是跟随语言的 thunk,因此导航投影经 `resolveSlotLabel` 解析,并在分区账本更新或 locale revision 变化时重新渲染(`ctx.get('locale')` 可选读取,无硬 locale 依赖)。首次使用引导记录按升序投影,每次只挂载一个步骤;可见步骤自行持有弹窗框架和应用根节点 `inert` 生命周期。已挂载但仍在判定私有事实的步骤渲染 null,因此判定期间不绘制也不阻塞任何内容。当前注册方会收到该条目的 id、`complete()` 和 `openSection(id)` 回调;完成或跳过当前步骤后,所有权转交给下一项。持久化完成状态、能力就绪状态、文案、变更操作以及可见包装均由注册方持有,因此独立注册的流程无法堆叠,外壳也不会成为第二个配置事实来源。
diff --git a/packages/client/ui-settings-general/src/client/AboutSection.module.css b/packages/client/ui-settings-general/src/client/AboutSection.module.css
new file mode 100644
index 0000000000..1ab33971c3
--- /dev/null
+++ b/packages/client/ui-settings-general/src/client/AboutSection.module.css
@@ -0,0 +1,72 @@
+.section {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ width: 100%;
+}
+
+.company {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.companyName {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+.tagline {
+ margin: 0;
+ font-weight: 500;
+}
+
+.intro {
+ margin: 0.5rem 0 0;
+ line-height: 1.6;
+ opacity: 0.85;
+}
+
+.product {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--dsh-border);
+}
+
+.productName {
+ margin: 0;
+}
+
+.version {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ margin: 0;
+}
+
+.version dt {
+ opacity: 0.7;
+}
+
+.version dd {
+ margin: 0;
+ font-variant-numeric: tabular-nums;
+ font-weight: 600;
+}
+
+.check {
+ align-self: flex-start;
+}
+
+.check:disabled {
+ cursor: default;
+ opacity: 0.6;
+}
+
+.status {
+ margin: 0;
+ opacity: 0.75;
+}
diff --git a/packages/client/ui-settings-general/src/client/AboutSection.tsx b/packages/client/ui-settings-general/src/client/AboutSection.tsx
new file mode 100644
index 0000000000..2a13bed0f0
--- /dev/null
+++ b/packages/client/ui-settings-general/src/client/AboutSection.tsx
@@ -0,0 +1,66 @@
+/**
+ * The About section: PineSound company introduction and a software-update check.
+ *
+ * The update check is a client-side placeholder: it compares the build version
+ * against a fixed known-latest constant and reports up-to-date, pending a real
+ * update channel. The two constants are product metadata, not tunables.
+ */
+
+import { useState } from 'react'
+import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import css from './AboutSection.module.css'
+
+/** This build's version, mirrored from apps/desktop/package.json. */
+const APP_VERSION = '0.1.0-rc.5'
+
+/** Latest version known to this build; the update check compares against it. */
+const LATEST_VERSION = APP_VERSION
+
+type UpdateStatus = 'idle' | 'checking' | 'upToDate'
+
+/** Full component props for the About section. */
+export type AboutSectionComponentProps =
+ PropsRuntime<'settings.section'> & PropsLocale<'settings'>
+
+/**
+ * Render the About section: company copy and a check-updates control.
+ * @param props - composed slot props (contract/slots.ts).
+ * @returns the About section element tree.
+ */
+export function AboutSection({ t }: AboutSectionComponentProps) {
+ const [status, setStatus] = useState('idle')
+ const checking = status === 'checking'
+ const check = (): void => {
+ if (checking) return
+ setStatus('checking')
+ window.setTimeout(() => { setStatus('upToDate') }, 600)
+ }
+ return (
+
+
+
{t('about.companyName')}
+
{t('about.companyTagline')}
+
{t('about.companyIntro')}
+
+
+
{t('about.product')}
+
+
{t('about.currentVersion')}
+
{APP_VERSION}
+
+
+ {status === 'upToDate' && !checking
+ ?
{t('about.upToDate')} · v{LATEST_VERSION}
+ : null}
+
+
+ )
+}
diff --git a/packages/client/ui-settings-general/src/client/SettingsRoot.tsx b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx
index 762747500d..59e6ca37fb 100644
--- a/packages/client/ui-settings-general/src/client/SettingsRoot.tsx
+++ b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx
@@ -14,7 +14,7 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react'
import clsx from 'clsx'
import {
IconAgentPresetOutline16, IconCloseOutline16, IconDataOutline16,
- IconPersonalizationOutline16, IconSettingsOutline16,
+ IconInfoOutline16, IconPersonalizationOutline16, IconSettingsOutline16,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { SettingsRootComponentProps, SettingsSectionRow } from './shell-contract.ts'
import css from './SettingsRoot.module.css'
@@ -24,6 +24,7 @@ function navIcon(id: string) {
if (id === 'models') return
if (id === 'agent-presets') return
if (id === 'plugins') return
+ if (id === 'about') return
return
}
diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts
index e1a94934c7..4b39c80a5b 100644
--- a/packages/client/ui-settings-general/src/client/index.ts
+++ b/packages/client/ui-settings-general/src/client/index.ts
@@ -3,8 +3,8 @@
* `sidebar.settings` occupant — panel chrome, section navigation, and the
* onboarding stage — and registers everything on the Settings pages that
* belongs to no single feature: the trigger/header chrome content,
- * local-document action, General section, and `settings` dictionaries.
- * Feature-owned rows and sections stay with their features.
+ * local-document action, General and About sections, and `settings`
+ * dictionaries. Feature-owned rows and sections stay with their features.
* Export discipline: packages/client/AGENTS.md.
*/
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
@@ -22,6 +22,7 @@ import type {
} from './shell-contract.ts'
import { SettingsRoot } from './SettingsRoot.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from './chrome.tsx'
+import { AboutSection } from './AboutSection.tsx'
import { GeneralSection } from './GeneralSection.tsx'
import { SettingsDocumentAction } from './SettingsDocumentAction.tsx'
import type { SettingsDocumentActionInjected } from './SettingsDocumentAction.tsx'
@@ -34,6 +35,9 @@ export type {
export type {
GeneralSectionComponentProps,
} from './GeneralSection.tsx'
+export type {
+ AboutSectionComponentProps,
+} from './AboutSection.tsx'
export type { SettingsDocumentActionInjected, SettingsDocumentActionProps } from './SettingsDocumentAction.tsx'
export type { SettingsDocumentState } from './settings-document-store.ts'
export { SettingsDocumentStore } from './settings-document-store.ts'
@@ -175,4 +179,11 @@ export function apply(ctx: ClientContext): void {
locale: NS,
children: { 'settings.general.item': { kind: 'list', scope: 'root' } },
}, GeneralSection))
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
+ name: 'settings.section',
+ id: 'about',
+ order: 100,
+ label: () => t('about.nav'),
+ locale: NS,
+ }, AboutSection))
}
diff --git a/packages/client/ui-settings-general/src/client/locales.ts b/packages/client/ui-settings-general/src/client/locales.ts
index a557855323..d252a87a3f 100644
--- a/packages/client/ui-settings-general/src/client/locales.ts
+++ b/packages/client/ui-settings-general/src/client/locales.ts
@@ -1,4 +1,4 @@
-/** Shell chrome and General-nav dictionaries; feature rows own their copy. */
+/** Shell chrome, General-nav, and About-section dictionaries; feature rows own their copy. */
/** Simplified Chinese dictionary (the key-set source of truth). */
export const zh = {
@@ -8,6 +8,15 @@ export const zh = {
'openDocument': '打开配置文件',
'openDocument.error': '无法打开配置文件',
'general.nav': '通用设置',
+ 'about.nav': '关于',
+ 'about.companyName': 'PineSound',
+ 'about.companyTagline': 'AI 音频创作平台',
+ 'about.companyIntro': 'PineSound 是一家 AI 音频创作平台,提供 AI 音效、配乐生成、配音、音色克隆与设计,以及云端音频素材搜索与识别,服务广大的音频与视频创作者。',
+ 'about.product': 'DeepSeek Harness Desktop(由 PineSound 基于 deepseek-harness 构建)',
+ 'about.currentVersion': '当前版本',
+ 'about.checkUpdates': '检查更新',
+ 'about.checking': '正在检查更新…',
+ 'about.upToDate': '已是最新版本',
} satisfies Record
/** The settings namespace key union. */
@@ -21,4 +30,13 @@ export const en = {
'openDocument': 'Open configuration file',
'openDocument.error': 'Could not open configuration file',
'general.nav': 'General',
+ 'about.nav': 'About',
+ 'about.companyName': 'PineSound',
+ 'about.companyTagline': 'AI audio creation platform',
+ 'about.companyIntro': 'PineSound is an AI audio creation platform offering AI sound-effect and score generation, voiceover, voice cloning and design, plus cloud audio asset search and recognition, serving audio and video creators.',
+ 'about.product': 'DeepSeek Harness Desktop (built by PineSound on the deepseek-harness project)',
+ 'about.currentVersion': 'Current version',
+ 'about.checkUpdates': 'Check for updates',
+ 'about.checking': 'Checking for updates…',
+ 'about.upToDate': 'You are on the latest version',
} satisfies Record
diff --git a/packages/client/ui-settings-general/tests/apply.client.spec.ts b/packages/client/ui-settings-general/tests/apply.client.spec.ts
index d6c3ffff02..dc15b55326 100644
--- a/packages/client/ui-settings-general/tests/apply.client.spec.ts
+++ b/packages/client/ui-settings-general/tests/apply.client.spec.ts
@@ -6,6 +6,7 @@ import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
+import { AboutSection } from '../src/client/AboutSection.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx'
@@ -73,6 +74,10 @@ function generalEntry(slots: SlotRegistry) {
return slots.entries('settings.section').find(e => e.component === GeneralSection)
}
+function aboutEntry(slots: SlotRegistry) {
+ return slots.entries('settings.section').find(e => e.component === AboutSection)
+}
+
describe('ui-settings-general apply', () => {
it('declares the services it uses', () => {
expect(inject).toEqual(['slots', 'locale', 'connection'])
@@ -91,6 +96,9 @@ describe('ui-settings-general apply', () => {
expect(resolveSlotLabel(entry.options.label)).toBe('通用设置')
expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
expect(before.slots.entries('settings.general.item')).toEqual([])
+ const about = aboutEntry(before.slots)!
+ expect(about.options).toMatchObject({ id: 'about', order: 100 })
+ expect(resolveSlotLabel(about.options.label)).toBe('关于')
// The onboarding hole stays declared for feature-owned steps; this plugin
// no longer seats one.
expect(before.slots.entries('settings.onboarding')).toEqual([])
@@ -110,7 +118,7 @@ describe('ui-settings-general apply', () => {
for (const [name, component] of SEATS) {
expect(after.slots.entries(name)[0]!.component).toBe(component)
// The self-inflicted ledger notifications hit the duplicate guard.
- expect(after.slots.entries(name)).toHaveLength(1)
+ expect(after.slots.entries(name)).toHaveLength(name === 'settings.section' ? 2 : 1)
}
await vi.waitFor(() => {
expect(after.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
@@ -142,7 +150,7 @@ describe('ui-settings-general apply', () => {
// subscription), not re-registration.
SEATS.forEach(([name], i) => {
expect(b.slots.getVersion(name)).toBe(zhVersions[i]!)
- expect(b.slots.entries(name)).toHaveLength(1)
+ expect(b.slots.entries(name)).toHaveLength(name === 'settings.section' ? 2 : 1)
})
expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('General')
b.locale.setLocale('zh')
diff --git a/packages/client/ui-settings-general/tests/components.client.spec.tsx b/packages/client/ui-settings-general/tests/components.client.spec.tsx
index 874bd44630..7c26557e97 100644
--- a/packages/client/ui-settings-general/tests/components.client.spec.tsx
+++ b/packages/client/ui-settings-general/tests/components.client.spec.tsx
@@ -4,6 +4,8 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type { GeneralSectionComponentProps } from '../src/client/GeneralSection.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
+import { AboutSection } from '../src/client/AboutSection.tsx'
+import type { AboutSectionComponentProps } from '../src/client/AboutSection.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import type { TriggerContentProps } from '../src/client/chrome.tsx'
import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx'
@@ -58,6 +60,28 @@ describe('GeneralSection', () => {
})
})
+describe('AboutSection', () => {
+ const aboutProps: AboutSectionComponentProps = { close: vi.fn(), t } as never
+
+ it('renders the company copy and the current version', () => {
+ render()
+ expect(screen.getByText('PineSound')).toBeTruthy()
+ expect(screen.getByText('AI audio creation platform')).toBeTruthy()
+ expect(screen.getByText(en['about.currentVersion'])).toBeTruthy()
+ expect(screen.getByText('0.1.0-rc.5')).toBeTruthy()
+ expect(screen.getByRole('button', { name: en['about.checkUpdates'] })).toBeTruthy()
+ })
+
+ it('checks for updates and reports the latest version', async () => {
+ render()
+ fireEvent.click(screen.getByRole('button', { name: en['about.checkUpdates'] }))
+ expect(screen.getByText(en['about.checking'])).toBeTruthy()
+ await waitFor(() => {
+ expect(screen.getByRole('status').textContent).toContain(en['about.upToDate'])
+ })
+ })
+})
+
describe('SettingsDocumentAction', () => {
it('appears only for a file-backed provider and requests its Host-owned document', async () => {
const openDocument = vi.fn(() => Promise.resolve({
diff --git a/packages/client/ui-settings-general/tests/shell.client.spec.ts b/packages/client/ui-settings-general/tests/shell.client.spec.ts
index 32c354deda..79932a0574 100644
--- a/packages/client/ui-settings-general/tests/shell.client.spec.ts
+++ b/packages/client/ui-settings-general/tests/shell.client.spec.ts
@@ -76,10 +76,11 @@ describe('ui-settings apply', () => {
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const { sections } = injectedOf(b.slots).hooks
- // This package registers the General section itself; every other section
- // arrives from a feature registrant.
+ // This package registers the General and About sections itself; every
+ // other section arrives from a feature registrant.
const GENERAL = { id: 'general', order: 0, label: 'general.nav' }
- expect(sections.getSnapshot()).toEqual([GENERAL])
+ const ABOUT = { id: 'about', order: 100, label: 'about.nav' }
+ expect(sections.getSnapshot()).toEqual([GENERAL, ABOUT])
b.slots.register({ name: 'settings.section', id: 'z', order: 20, label: 'Z' } as never, () => null)
// No order and no label: both projection defaults apply.
b.slots.register({ name: 'settings.section', id: 'a' } as never, () => null)
@@ -88,6 +89,7 @@ describe('ui-settings apply', () => {
GENERAL,
{ id: 'a', order: 0, label: '' },
{ id: 'z', order: 20, label: 'Z' },
+ ABOUT,
])
// Snapshot identity is stable until the ledger moves (uSES contract).
expect(sections.getSnapshot()).toBe(rows)