diff --git a/.agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml b/.agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml new file mode 100644 index 0000000000..dfd49a8ee4 --- /dev/null +++ b/.agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml @@ -0,0 +1,2 @@ +2026-10-14-built-in-plugin-feature-previews.md: ae9b32340d9b1e77a99465445a05ccee05df0e79 +2026-10-14-built-in-plugin-feature-previews.zh.md: 9229b0835a23a0eefffa1bd157efece333aebfa9 diff --git a/.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.md b/.agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.md similarity index 99% rename from .agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.md rename to .agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.md index 9c1efae0ab..ae9b32340d 100644 --- a/.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.md +++ b/.agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.md @@ -1,6 +1,7 @@ # Agent Note: Built-in plugin feature previews Status: implemented +Archived: 2026-10-19 English | [中文](2026-10-14-built-in-plugin-feature-previews.zh.md) diff --git a/.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.zh.md b/.agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.zh.md similarity index 99% rename from .agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.zh.md rename to .agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.zh.md index 28f991a975..9229b0835a 100644 --- a/.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.zh.md +++ b/.agents/notes/archived/feature/2026-10-14-built-in-plugin-feature-previews.zh.md @@ -1,6 +1,7 @@ # Agent Note: 内置插件功能配图 Status: implemented +Archived: 2026-10-19 [English](2026-10-14-built-in-plugin-feature-previews.md) | 中文 diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index 87ef49e0df..2dbf920a69 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -2020,6 +2020,9 @@ "feature/2026-09-28-desktop-login-shell-environment.i18n.yaml": "sha256:6129edeb0a7ecf087911193ca5a15223a730ad01e30d2ead7739d01f589fafa7", "feature/2026-09-28-desktop-login-shell-environment.md": "sha256:cd133b1ef1a178828d30b9db176a91597eedf9e67802551356f34d3e0f80206d", "feature/2026-09-28-desktop-login-shell-environment.zh.md": "sha256:1422667a44aacc51736f31b328a8e171a39cb6b5168d6993e980e4324b23b98f", + "feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml": "sha256:4769fd7fa4881a38fa8e2682b2ea62ed5a5ec6588e87f225c2b106e1bd308200", + "feature/2026-10-14-built-in-plugin-feature-previews.md": "sha256:093e00da4cebf732861c1e5c47465fb00359eb89a86dc212f3f90b43f9b80cec", + "feature/2026-10-14-built-in-plugin-feature-previews.zh.md": "sha256:56adb70945f68b073357c437bbec9e69f110587cf302291f2f135d9fcb5537e7", "process/2026-06-11-doc-sync-enforcement.i18n.yaml": "sha256:33b6d5874427bd7a2bd82e7e2f4f482b12448b2464aef15a9c57975edb48554d", "process/2026-06-11-doc-sync-enforcement.md": "sha256:aa2fe83d519fc30d48dff19e596e83c8922aacc9e063e14fe2cc35b769b9100e", "process/2026-06-11-doc-sync-enforcement.zh.md": "sha256:698017bd35f030fdea3eac51df9e43138c48140f504739d687b7251d13fced2b", diff --git a/.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml b/.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml deleted file mode 100644 index 40de44c9d4..0000000000 --- a/.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Bilingual-pair consistency record for 2026-10-14-built-in-plugin-feature-previews.md (docs/i18n/README.md): per heading -# section, a hash of its English and Chinese blocks outside code blocks and generated regions. -# After editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.md -/agent-note-built-in-plugin-feature-previews: - en: f6abb7d00b13d362 - zh: b89d5da18677f31a -/agent-note-built-in-plugin-feature-previews/problem: - en: e38fa1e5b3a62b5a - zh: c612ea229705d695 -/agent-note-built-in-plugin-feature-previews/decision: - en: aa12e3f72f7c2498 - zh: 7dda9f061009aaa0 -/agent-note-built-in-plugin-feature-previews/alternatives-considered: - en: 4a4416610894d7c6 - zh: 4256e81e7ab70db5 -/agent-note-built-in-plugin-feature-previews/consequences: - en: a3dbd70080416314 - zh: 4d802da45085dfb0 diff --git a/.agents/skills/dsh-official-plugin-artwork/SKILL.md b/.agents/skills/dsh-official-plugin-artwork/SKILL.md index 7df034c409..809409c8b5 100644 --- a/.agents/skills/dsh-official-plugin-artwork/SKILL.md +++ b/.agents/skills/dsh-official-plugin-artwork/SKILL.md @@ -9,7 +9,7 @@ Draw recognizable artwork that belongs beside the existing official icons. Offic ## Optional feature previews -Before drawing a larger image below a built-in bundle's description, read and follow [adding a built-in plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, display size and position, language variants, asset limits, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions below do not apply to previews. Inspect the real feature, preserve its produced assets, and keep built-in previews in the Plugin Manager client assets directory and its internal bilingual map. +Before drawing a larger image for the top of an official bundle's detail document, read and follow [adding a plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, position, language variants, file location, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions below do not apply to previews. Inspect the real feature and preserve its produced assets. ## Establish the subject diff --git a/apps/web/tests/expected/plugin-manager/preview.expected.md b/apps/web/tests/expected/plugin-manager/preview.expected.md index b72a046686..9e44db86f7 100644 --- a/apps/web/tests/expected/plugin-manager/preview.expected.md +++ b/apps/web/tests/expected/plugin-manager/preview.expected.md @@ -5,54 +5,9 @@ - paragraph: - code: "@deepseek-ai/dsh-experimental-badge-skill-bundle" - paragraph: 让 Agent 为文档、PR 等产出添加「powered by dsh」徽章。 -- img "DSH 徽章技能功能预览" - region "详细介绍": - - heading "@deepseek-ai/dsh-experimental-badge-skill-bundle" [level=1] - - paragraph: English | 中文 - - heading "Summary" [level=2] - - paragraph: Let Agents whose presets load skills add a “powered by dsh” badge to documents, pull requests, and other outputs. The shared provider adds no model tool. It ships switched off in Web and Desktop. Select it in Plugins to enable it. - - heading "Table of Contents" [level=2] - - list: - - listitem: Use this package - - listitem: Understand the implementation - - listitem: Further Exploration - - listitem: Model Experience - - listitem: Known Limitations and Deferred Work - - listitem: Dev Note - - separator - - paragraph - - heading "Use this package" [level=2] - paragraph: - - text: In Web or Desktop, open Plugins and enable - - strong: DSH badge skill - - text: in Official. Switch it off to remove its profile layer. - - paragraph: The switch adds or removes a Host-wide skill provider for existing and new Agents. Presets that expose skill loading can use the badge; minimal keeps its tool catalog unchanged. - - separator - - paragraph - - heading "Understand the implementation" [level=2] - - paragraph: - - code: cordis.patch.yml - - text: contributes the - - code: skill-badge - - text: provider row on the Host. Later root patches can disable that row; an override alone does not select the bundle. The profile composer owns patch ordering and errors. - - separator - - paragraph - - heading "Further Exploration" [level=2] - - paragraph: Capability implementation - - separator - - paragraph - - heading "Model Experience" [level=2] - - paragraph: Indirectly, through the capability implementation, which owns model context and results. - - heading "KV Cache effect" [level=4] - - paragraph: The layer adds no request content directly; the capability implementation owns cache effects from tools, prompts, and results. - - heading "Known Limitations and Deferred Work" [level=2] - - paragraph - - list: - - listitem: A preset must include skill discovery and loading to use the badge. The minimal preset does not gain skill tools from this bundle. - - separator - - paragraph - - heading "Dev Note" [level=3] - - paragraph: None. + - img "DSH 徽章技能功能预览" - heading "包含的组件" [level=4] - text: 共 1 个 · 1 已停用 - list: diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts index d70558dab4..2f96305714 100644 --- a/apps/web/tests/message-actions.e2e.ts +++ b/apps/web/tests/message-actions.e2e.ts @@ -15,7 +15,7 @@ import { acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, parseSeedFixture, realizeSeedFixture, renderSeedFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { openSettings, newEnglishPage, pinBrowserClock, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts' +import { openSettings, newEnglishPage, pinBrowserClock, pinHostClock, saveFailureShot, WEB_FIXTURE_TIME } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/message-actions', import.meta.url)) // Borrowed read-only: this scenario needs any settled user+assistant pair, not @@ -191,6 +191,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => { let browser: Browser let page: Page let tripwire: ReturnType + let hostClock: ReturnType | undefined beforeAll(async () => { scaffold = await launchWebScaffold() @@ -211,6 +212,10 @@ describe('web e2e: message IconActions and clocks on settled history', () => { await seedSession(scaffold, raw, SEED_ID, undefined, { createdAt: WEB_FIXTURE_TIME - 90_000 }) browser = await chromium.launch() page = await newEnglishPage(browser) + // Fork summaries use Host time; placeholders use browser time. Each pin + // starts at the fixture date, so keep these calls adjacent: setup between + // them can create enough clock skew to reverse the fork order. + hostClock = pinHostClock() await pinBrowserClock(page) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) @@ -218,8 +223,12 @@ describe('web e2e: message IconActions and clocks on settled history', () => { }, 120_000) afterAll(async () => { - await browser?.close() - await scaffold?.close() + try { + await browser?.close() + await scaffold?.close() + } finally { + hostClock?.mockRestore() + } }) it.skipIf(MODE === 'record')('enables branch only on the completed transcript tail', async () => { diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index 6b6585f4e6..695dcad4dc 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -691,13 +691,13 @@ describe('web e2e: plugin manager', () => { expect(tripwire.pageErrors).toEqual([]) }) - it('shows an optional feature preview before activation and leaves other plugin details unchanged', async () => { + it('shows a bundle detail document image before activation and leaves other plugin details unchanged', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-preview')) const panel = await openPluginsPanel() await panel.getByRole('button', { name: '更多', exact: true }).click() await panel.getByRole('button', { name: '查看 DSH 徽章技能', exact: true }).click() const detail = panel.locator('[data-plugin-detail]') - const preview = detail.getByRole('img', { name: 'DSH 徽章技能功能预览', exact: true }) + const preview = detail.locator('[data-detail-document]').getByRole('img', { name: 'DSH 徽章技能功能预览', exact: true }) await preview.waitFor() await preview.evaluate(async (image: HTMLImageElement) => { await image.decode() }) expect(await preview.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(640) @@ -710,8 +710,6 @@ describe('web e2e: plugin manager', () => { expect(fits).toBe(true) await detail.getByText(`v${badgeVersion}`, { exact: true }).waitFor() await detail.getByText(badgeVersion, { exact: true }).waitFor() - // The bundle's README is its detail document, shown between its components and its source. - await detail.locator('[data-detail-document] h1').waitFor() await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'preview.expected.md'), await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, { replacements: [[badgeVersion, '{{version}}']], @@ -719,7 +717,8 @@ describe('web e2e: plugin manager', () => { const chineseSource = await preview.getAttribute('src') try { await setLanguage('en') - const englishPreview = detail.getByRole('img', { name: 'DSH badge skill feature preview', exact: true }) + const englishPreview = detail.locator('[data-detail-document]') + .getByRole('img', { name: 'DSH badge skill feature preview', exact: true }) await englishPreview.waitFor() await englishPreview.evaluate(async (image: HTMLImageElement) => { await image.decode() }) expect(await englishPreview.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(640) @@ -730,7 +729,8 @@ describe('web e2e: plugin manager', () => { expect(await preview.getAttribute('src')).toBe(chineseSource) await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click() await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click() - // Detail icons have alt=""; only feature previews expose an image role. + // Detail icons have alt=""; this bundle's README detail document has no images. + await panel.locator('[data-detail-document] h1').waitFor() expect(await panel.getByRole('img').count()).toBe(0) await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click() expect(tripwire.pageErrors).toEqual([]) diff --git a/apps/web/tests/voice-download.e2e.ts b/apps/web/tests/voice-download.e2e.ts index 0acd3489bc..a541bef2fb 100644 --- a/apps/web/tests/voice-download.e2e.ts +++ b/apps/web/tests/voice-download.e2e.ts @@ -12,6 +12,8 @@ import { newEnglishPage } from './support.ts' const bundle = fileURLToPath(new URL('../../../packages/experimental/voice-input-bundle', import.meta.url)) const expected = fileURLToPath(new URL('./expected/voice-download.expected.md', import.meta.url)) +// The input-device picker on the same page shows its own alert when headless Chromium offers no microphone. +const DOWNLOAD_ALERT = '[role="alert"]:has-text("Download source")' it('shows the failed asset, actual download source and recovery advice, then retries on request', async () => { const resources: { scaffold?: WebScaffold; browser?: Browser } = {} @@ -45,14 +47,14 @@ it('shows the failed asset, actual download source and recovery advice, then ret await page.getByRole('button', { name: 'Plugins', exact: true }).click() await page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]').getByRole('button').click() await page.getByRole('button', { name: 'Download and prepare', exact: true }).click() - const alert = page.getByRole('alert') + const alert = page.getByRole('alert').filter({ hasText: 'Download source' }) await alert.waitFor() expect(await alert.textContent()).toContain('HTTP 503') expect(await alert.textContent()).toContain(`Download source: ${origin}`) expect(requests).toHaveLength(1) expect(requests[0]).toContain('/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07/model.int8.onnx') await compareOrRefreshGolden(expected, - (await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(origin, 'https://model-mirror.example'), webSnapshotMode()) + (await captureStableAria(page, DOWNLOAD_ALERT, scaffold.workspaceCwd)).replaceAll(origin, 'https://model-mirror.example'), webSnapshotMode()) status = 404 await page.getByRole('button', { name: 'Retry preparation', exact: true }).click() await expect.poll(() => alert.textContent()).toContain('HTTP 404') @@ -108,7 +110,7 @@ it.each([{ winner: 0, manual: false }, { winner: 1, manual: false }, { winner: 0 await picker.waitFor() if (manual) await picker.selectOption(origins[winner]!) await page.getByRole('button', { name: 'Download and prepare', exact: true }).click() - const alert = page.getByRole('alert') + const alert = page.getByRole('alert').filter({ hasText: 'Download source' }) await alert.waitFor() expect(downloads).toEqual(manual ? [winner] : [winner, 1 - winner]) expect(requestedPaths).toHaveLength(manual ? 1 : 4) @@ -118,7 +120,7 @@ it.each([{ winner: 0, manual: false }, { winner: 1, manual: false }, { winner: 0 expect(await alert.textContent()).toContain(manual ? 'HTTP 503' : 'HTTP 502') expect(await alert.textContent()).toContain(`Download source: ${failedOrigin}`) await compareOrRefreshGolden(manual ? expected : fileURLToPath(new URL('./expected/voice-download-fallback.expected.md', import.meta.url)), - (await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(failedOrigin, manual ? 'https://model-mirror.example' : 'https://fallback-model-source.example'), + (await captureStableAria(page, DOWNLOAD_ALERT, scaffold.workspaceCwd)).replaceAll(failedOrigin, manual ? 'https://model-mirror.example' : 'https://fallback-model-source.example'), webSnapshotMode()) expect(tripwire.pageErrors).toEqual([]) }) diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml index 3776cb0096..2fb8b9bd67 100644 --- a/docs/cookbook/adding-a-package.i18n.yaml +++ b/docs/cookbook/adding-a-package.i18n.yaml @@ -21,8 +21,8 @@ en: 0894bd9f35430753 zh: 2d28090e71134edd /cookbook-adding-a-workspace-package/5-add-optional-plugin-display-metadata: - en: 68b463cd3d2e5fb0 - zh: 80ea797c5770c573 + en: ed501a72ab8ba6e7 + zh: 8ad775b735887a10 /cookbook-adding-a-workspace-package/6-verify: en: 80c7fb90b1175aae zh: 2ba37962dbc57299 diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 11cca9a60d..1f86f1b9bd 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -147,7 +147,7 @@ Export `/locale/en.json` for locale lookup; expose ` For an image on bundle cards, details, and component rows, a package-root plugin sets top-level `"icon": "./icon.svg"` in its exported manifest, or exports `./icon` when that field is omitted. A subpath plugin exports its own icon, such as `"./search/icon": "./search.svg"`. Include each image in `files`. A manifest `icon` is relative to the manifest's directory; absolute paths, URLs, paths outside that directory, and symlinks resolving outside it are rejected. An exported icon resolves through Node exports like locale resources and must remain inside its package. SVG, PNG, JPEG (`.jpg`/`.jpeg`), and WebP files are supported up to 256 KiB. Images must be self-contained; SVG is rendered as an image, not inline HTML. The Host returns a data URL without activating the plugin. Invalid declarations or unreadable files produce a metadata diagnostic while retaining valid text, without trying the export; missing or undecodable images use the panel's default artwork. -A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; include the file and its images in `files`. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file. +A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; keep its images beside it, and list its top-level directory, such as `docs`, in `files` right after the locale resources, as `check-workspace-constraints` requires. The named file must exist in a subdirectory of the package; a root-level document is rejected because its neighboring images would not be published. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file. Installed bundle cards and details, component lists and configuration details, and Settings' plugin inventory display this metadata, including disabled and preset plugins. Reads do not activate plugins. diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md index b22976fb9d..3e13580fe9 100644 --- a/docs/cookbook/adding-a-package.zh.md +++ b/docs/cookbook/adding-a-package.zh.md @@ -147,7 +147,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex 要在组合包卡片、详情和组件行显示图片,包根插件在导出清单顶层设置 `"icon": "./icon.svg"`,或在省略该字段时导出 `./icon`。子路径插件导出自己的图标,例如 `"./search/icon": "./search.svg"`。将每张图片加入 `files`。清单 `icon` 相对于清单所在目录;绝对路径、URL、目录外路径,以及解析到目录外的符号链接均被拒绝。导出的图标像 locale 资源一样经 Node exports 解析,且必须位于所属包内。支持不超过 256 KiB 的 SVG、PNG、JPEG(`.jpg`/`.jpeg`)和 WebP 文件。图片必须自包含;SVG 作为图片渲染,不作为内联 HTML。Host 返回 data URL,不激活插件。声明无效或文件不可读时,显示元信息诊断并保留有效文本,不再尝试导出的图标;图片缺失或无法解码时使用面板的默认插画。 -组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;将该文件及其图片加入 `files`。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。 +组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;图片放在该文件旁边,并按 `check-workspace-constraints` 的要求,在 `files` 中紧接语言资源之后列出该文件的顶层目录,例如 `docs`。该文件必须位于包的子目录中且已存在;包根目录下的文档会被拒绝,因为其旁边的图片不会被发布。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。 已安装 bundle 的卡片和详情、组件列表与配置详情、设置中的插件清单都展示这些元信息,包括禁用插件和预设内插件。读取时不激活插件。 diff --git a/docs/cookbook/adding-a-plugin-preview.i18n.yaml b/docs/cookbook/adding-a-plugin-preview.i18n.yaml index 04bf27f55f..0111677ab5 100644 --- a/docs/cookbook/adding-a-plugin-preview.i18n.yaml +++ b/docs/cookbook/adding-a-plugin-preview.i18n.yaml @@ -2,21 +2,21 @@ # section, a hash of its English and Chinese blocks outside code blocks and generated regions. # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/cookbook/adding-a-plugin-preview.md -/cookbook-add-a-built-in-plugin-feature-preview: - en: 1038debc95283b43 - zh: a41eae3d9203c400 -/cookbook-add-a-built-in-plugin-feature-preview/1-decide-whether-an-image-helps: - en: d2b84c3a4d590f43 - zh: b064b42f148bd323 -/cookbook-add-a-built-in-plugin-feature-preview/2-draw-one-fixed-size-svg: - en: 1abb5284f499ae7e - zh: b4dacd16d8f04728 -/cookbook-add-a-built-in-plugin-feature-preview/3-register-the-internal-pair: - en: e79355a7184d82d3 - zh: 92a0e188dee2717c -/cookbook-add-a-built-in-plugin-feature-preview/4-keep-the-page-contract: - en: 095cb97e16c2232e - zh: 2ed9b60710ed522c -/cookbook-add-a-built-in-plugin-feature-preview/5-validate-and-review: - en: 053b2105cc00b8d0 - zh: d06467ded2ccac42 +/cookbook-add-a-plugin-feature-preview: + en: c37096cd3cdb886c + zh: fc6850d68c638898 +/cookbook-add-a-plugin-feature-preview/1-decide-whether-an-image-helps: + en: ee84f52d834cbccf + zh: 7bf3a425a231b5e6 +/cookbook-add-a-plugin-feature-preview/2-draw-one-fixed-size-svg: + en: 3a28640aa605dce8 + zh: c5336958f92b5a70 +/cookbook-add-a-plugin-feature-preview/3-put-it-at-the-top-of-the-detail-document: + en: 17e2ace32a89c419 + zh: c635a890e86a41ec +/cookbook-add-a-plugin-feature-preview/4-page-behavior: + en: 20ce358cb110104c + zh: abb8d72fed81f3c8 +/cookbook-add-a-plugin-feature-preview/5-validate-and-review: + en: 2888c02bac267662 + zh: 5b0e95611c8d5d94 diff --git a/docs/cookbook/adding-a-plugin-preview.md b/docs/cookbook/adding-a-plugin-preview.md index ab3a470517..1683aab10a 100644 --- a/docs/cookbook/adding-a-plugin-preview.md +++ b/docs/cookbook/adding-a-plugin-preview.md @@ -1,52 +1,45 @@ -# Cookbook: Add a built-in plugin feature preview +# Cookbook: Add a plugin feature preview English | [中文](adding-a-plugin-preview.zh.md) -Plugin Manager owns optional feature illustrations for built-in bundles. Follow this guide when editing its internal `BUNDLE_PREVIEWS` map. Packages cannot declare illustrations through their manifest or exports; third-party plugins have no preview interface. +A feature preview is an optional illustration at the top of a bundle's detail document, the long description that its page in **Plugins** shows first. Follow this guide when drawing one for an official bundle. The detail document itself is declared through [`meta.detail`](adding-a-package.md#plugin-display-metadata). ## 1. Decide whether an image helps -Show one concrete result or recognizable capability. Omit the map entry when the description is enough: no image is required, and omission reserves no space. Essential instructions stay in localized text or configuration outside the illustration. +Show one concrete result or recognizable capability. Omit the image when the description is enough: no image is required. Essential instructions stay in the document's text or in configuration outside the illustration. ## 2. Draw one fixed-size SVG | Item | Requirement | |---|---| -| Count | Zero or one logical feature image per bundle. English and Chinese files are alternatives for that image, not a gallery or carousel. | -| Format | SVG only: `width="640" height="300" viewBox="0 0 640 300"` (32:15). Raster preview files and other dimensions are unsupported. | -| Languages | Supply both `en` and `zh`. Translate headings, annotations, and example content; preserve official brand assets and names. No single strings, arrays, or extra languages. | +| Count | Zero or one feature image per bundle. English and Chinese files are alternatives for that image, not a gallery or carousel. | +| Format | SVG: `width="640" height="300" viewBox="0 0 640 300"` (32:15). | +| Languages | Supply both an English and a Chinese file. Translate headings, annotations, and example content; preserve official brand assets and names. | | Composition | One main example, optionally with one close-up. Keep important content about 32 canvas units from the edges. Review text at the page's actual size. | | Typography | Outline lettering into paths using a suitable font. Match the feature and neighboring UI; do not rely on browser font substitution. | | Theme | Compare light and dark themes. One illustration serves both; avoid transparent dark text that disappears on dark backgrounds. | -The SVG is self-contained: no fonts, remote resources, scripts, handlers, animation, stylesheets, `text`, or `foreignObject`. An `image` element may embed an unchanged official PNG/JPEG/WebP brand asset as a base64 data URL. Small plugin icons remain geometry-only. This exception preserves brand artwork and does not permit raster preview files. Validation checks the data-URL envelope, not decoded pixels; the page renders the SVG through ``. Both raw language files together must stay within 1 MiB. They are embedded as base64 data URLs in the Plugin Manager client bundle, so keep them small. +The SVG is self-contained: no fonts, remote resources, scripts, handlers, animation, stylesheets, `text`, or `foreignObject`. An `image` element may embed an unchanged official PNG/JPEG/WebP brand asset as a base64 data URL. Small plugin icons remain geometry-only. The page renders the SVG through ``. Keep each file small; the Host refuses images above plugin-manager's `detailImageMaxBytes`, 5 MiB by default. -## 3. Register the internal pair +## 3. Put it at the top of the detail document -Put `-preview.svg` and `-preview.zh.svg` under [Plugin Manager assets](../../packages/client/ui-plugin-manager/src/client/assets/). Import both into [PluginManagerPage.tsx](../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx), then add one exact built-in package name to `BUNDLE_PREVIEWS`: +Save the pair as `docs/preview.svg` and `docs/preview.zh.svg` in the bundle package, beside `docs/DETAIL.md` and `docs/DETAIL.zh.md`, and make the image the first line of each document: -```ts ignore-check -import examplePreview from './assets/example-preview.svg' -import examplePreviewZh from './assets/example-preview.zh.svg' - -// One entry in the existing Map>>. -['@deepseek-ai/dsh-example-bundle', { en: examplePreview, zh: examplePreviewZh }] +```markdown +![Example feature preview](preview.svg) ``` -Keep the map's required `en` and `zh` type. Do not add package metadata, a Host read method, exported preview resources, or a third-party declaration path. Use the [badge skill](../../packages/client/ui-plugin-manager/src/client/assets/badge-skill-preview.svg) and [voice input](../../packages/client/ui-plugin-manager/src/client/assets/voice-input-preview.svg) illustrations as examples. The voice transcript is explanatory sample content, not a recording result. +The Chinese document references `preview.zh.svg` with Chinese alternative text. Use the [badge skill](../../packages/experimental/badge-skill-bundle/docs/DETAIL.md) and [voice input](../../packages/experimental/voice-input-bundle/docs/DETAIL.md) documents as examples. The voice transcript is explanatory sample content, not a recording result. -## 4. Keep the page contract +## 4. Page behavior | Item | Current behavior | |---|---| -| Position | Below the description and status notices, above components and source information; aligned with the description. | -| Spacing | 24 px above the image; 32 px before the following details section. | -| Display | One ``, natural 640×300 canvas, at most the available width and 320 px high; proportional scaling. The page supplies border and 16 px corners. | -| Language | The current UI language selects one of the required files. Alternative text uses the localized plugin name. | -| Availability | Visible before activation. Decode failure shows a localized notice without blocking controls. Lists and component rows retain their small icons. | +| Position | First in the detail document, which is the first section of the bundle page, below the description and status notices. | +| Display | The detail document renders the image at most its own width, inside its height-bounded scroll area. | +| Language | The document for the UI language references its own file. | +| Availability | Visible before activation. Lists and component rows keep their small icons. | ## 5. Validate and review -Run `pnpm exec vitest run scripts/builtin-plugin-preview.spec.ts packages/client/ui-plugin-manager/tests/components.client.spec.tsx scripts/client-bundle-css.spec.ts`, then `pnpm run build`. The asset test checks every registered pair, fixed SVG dimensions, self-contained content, and combined size. Type checking requires both languages. These checks enforce format; visual quality and equivalent meaning in both languages still require review. - -Open the real plugin details in English and Chinese before activation. Decode the image and confirm 640×300 natural dimensions, placement, light/dark readability, and no horizontal overflow at 390 px. Also check a built-in without artwork and a third-party plugin. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately. +`scripts/plugin-preview-assets.spec.ts` checks every preview pair in a bundle's `docs` directory: the fixed canvas, absent scripts, stylesheets, text elements, and external references, the 1 MiB pair limit, and English and Chinese files that differ. Then open the real plugin details in English and Chinese before activation and confirm the image loads, its placement, light/dark readability, and no horizontal overflow at 390 px; review owns visual quality and equivalent meaning in both languages. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately. diff --git a/docs/cookbook/adding-a-plugin-preview.zh.md b/docs/cookbook/adding-a-plugin-preview.zh.md index 3b57e446bf..3d3caeb1bf 100644 --- a/docs/cookbook/adding-a-plugin-preview.zh.md +++ b/docs/cookbook/adding-a-plugin-preview.zh.md @@ -1,52 +1,45 @@ -# Cookbook: 添加内置插件功能配图 +# Cookbook: 添加插件功能配图 [English](adding-a-plugin-preview.md) | 中文 -插件管理拥有内置组合包的可选功能配图。编辑其内部 `BUNDLE_PREVIEWS` 映射时遵循本指引。包不能通过 manifest 或 exports 声明配图;第三方插件没有配图接口。 +功能配图是位于组合包详细介绍文档顶部的可选插图;详细介绍文档是该组合包在**插件**中的页面最先显示的长描述。为官方组合包绘制配图时遵循本指引。详细介绍文档本身通过 [`meta.detail`](adding-a-package.zh.md#plugin-display-metadata) 声明。 ## 1. 判断是否需要配图 -展示一个具体结果或容易辨认的能力。文字说明足够时不添加映射项:配图不是必需的,省略时不占空间。必要操作说明仍放在图外的本地化文字或配置中。 +展示一个具体结果或容易辨认的能力。文字说明足够时省略配图:配图不是必需的。必要操作说明仍放在文档文字或图外的配置中。 ## 2. 绘制一张固定尺寸 SVG | 项目 | 要求 | |---|---| -| 数量 | 每个组合包零张或一张逻辑配图。英文和中文文件是同一张图的替代版本,不是图库或轮播。 | -| 格式 | 仅 SVG:`width="640" height="300" viewBox="0 0 640 300"`(32:15)。不支持位图配图文件或其他尺寸。 | -| 语言 | 同时提供 `en` 和 `zh`。翻译标题、标注和示例内容,保留官方品牌素材和名称。不使用单个字符串、数组或额外语言。 | +| 数量 | 每个组合包零张或一张配图。英文和中文文件是同一张图的替代版本,不是图库或轮播。 | +| 格式 | SVG:`width="640" height="300" viewBox="0 0 640 300"`(32:15)。 | +| 语言 | 同时提供英文和中文文件。翻译标题、标注和示例内容,保留官方品牌素材和名称。 | | 构图 | 一个主体示例,可选一个局部特写。重要内容距边缘约 32 个画布单位;按页面实际显示尺寸检查文字。 | | 字体 | 选择合适字体并把文字转成路径,匹配功能及相邻界面,不依赖浏览器字体替换。 | | 主题 | 对比浅色和深色主题。同一张图服务两种主题,避免透明底深色文字在深色背景中消失。 | -SVG 必须自包含:不使用字体、远程资源、脚本、事件处理器、动画、样式表、`text` 或 `foreignObject`。`image` 元素可以通过 base64 data URL 嵌入未改动的官方 PNG/JPEG/WebP 品牌素材。小插件图标仍仅使用几何图形。此例外用于保留品牌素材,不允许直接使用位图配图文件。校验检查 data URL 结构,不解码像素;页面通过 `` 渲染 SVG。两种语言的原始文件合计不超过 1 MiB。图片会以 base64 data URL 嵌入插件管理 client bundle,因此应控制大小。 +SVG 必须自包含:不使用字体、远程资源、脚本、事件处理器、动画、样式表、`text` 或 `foreignObject`。`image` 元素可以通过 base64 data URL 嵌入未改动的官方 PNG/JPEG/WebP 品牌素材。小插件图标仍仅使用几何图形。页面通过 `` 渲染 SVG。每个文件都应控制大小;Host 会拒绝超过 plugin-manager 的 `detailImageMaxBytes`(默认 5 MiB)的图片。 -## 3. 注册内部双语配图 +## 3. 放在详细介绍文档顶部 -将 `-preview.svg` 和 `-preview.zh.svg` 放在[插件管理素材目录](../../packages/client/ui-plugin-manager/src/client/assets/)下。在 [PluginManagerPage.tsx](../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx) 中导入两者,再向 `BUNDLE_PREVIEWS` 添加一个精确的内置包名: +将这对文件保存为组合包中的 `docs/preview.svg` 和 `docs/preview.zh.svg`,与 `docs/DETAIL.md` 和 `docs/DETAIL.zh.md` 放在一起,并把图片作为每份文档的第一行: -```ts ignore-check -import examplePreview from './assets/example-preview.svg' -import examplePreviewZh from './assets/example-preview.zh.svg' - -// One entry in the existing Map>>. -['@deepseek-ai/dsh-example-bundle', { en: examplePreview, zh: examplePreviewZh }] +```markdown +![Example feature preview](preview.svg) ``` -保留映射中必需的 `en` 和 `zh` 类型。不添加包元数据、Host 读取方法、导出的配图资源或第三方声明入口。可参考[徽章技能](../../packages/client/ui-plugin-manager/src/client/assets/badge-skill-preview.svg)和[语音输入](../../packages/client/ui-plugin-manager/src/client/assets/voice-input-preview.svg)配图。语音图中的转写文字是说明性示例,不是录音结果。 +中文文档引用 `preview.zh.svg`,并使用中文替代文字。可参考[徽章技能](../../packages/experimental/badge-skill-bundle/docs/DETAIL.md)和[语音输入](../../packages/experimental/voice-input-bundle/docs/DETAIL.md)文档。语音图中的转写文字是说明性示例,不是录音结果。 -## 4. 保持页面契约 +## 4. 页面行为 | 项目 | 当前行为 | |---|---| -| 位置 | 在描述和状态提示下方、组件与来源信息上方,与说明左边缘对齐。 | -| 间距 | 图片上方 24 px,后续详情区前 32 px。 | -| 显示 | 一个 ``,原始画布 640×300,最大宽度为可用宽度,最大高度 320 px,等比缩放;页面提供边框及 16 px 圆角。 | -| 语言 | 当前 UI 语言选择必需的两份文件之一;替代文字使用本地化插件名。 | -| 可用性 | 启用前可见;解码失败显示本地化提示,不阻塞操作;列表和组件行保留小图标。 | +| 位置 | 位于详细介绍文档开头;该文档是组合包页面的第一个区域,在描述和状态提示下方。 | +| 显示 | 详细介绍文档以不超过文档自身宽度的尺寸渲染图片,图片位于有高度上限的滚动区域内。 | +| 语言 | 当前 UI 语言对应的文档引用其自己的文件。 | +| 可用性 | 启用前可见;列表和组件行保留小图标。 | ## 5. 校验与验收 -运行 `pnpm exec vitest run scripts/builtin-plugin-preview.spec.ts packages/client/ui-plugin-manager/tests/components.client.spec.tsx scripts/client-bundle-css.spec.ts`,再运行 `pnpm run build`。素材测试检查每个注册条目的双语文件、固定 SVG 尺寸、自包含内容和合计大小;类型检查要求同时提供两种语言。这些检查约束格式;视觉质量及双语含义一致仍需人工审阅。 - -在真实插件详情页分别使用英文和中文,启用前解码图片,确认原始尺寸 640×300、插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出。同时检查一个无配图的内置插件和一个第三方插件。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。 +`scripts/plugin-preview-assets.spec.ts` 检查组合包 `docs` 目录中的每一对配图:固定画布;不使用脚本、样式表、`text` 元素或外部引用;每对文件合计不超过 1 MiB;英文和中文文件不相同。然后在真实插件详情页分别使用英文和中文,启用前确认图片能加载,并检查插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出;审阅负责检查视觉质量及双语含义是否一致。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。 diff --git a/docs/i18n/README.i18n.yaml b/docs/i18n/README.i18n.yaml index ccb3f9a8ba..bc8b3b400d 100644 --- a/docs/i18n/README.i18n.yaml +++ b/docs/i18n/README.i18n.yaml @@ -12,8 +12,8 @@ en: 08386e96eebf06d9 zh: c6d2b41199b0c466 /bilingual-documentation/scope-and-exclusions: - en: db70e758813d339b - zh: 3dcea283a08c47cb + en: eeda71b7ce01c121 + zh: a6361361c1e3d578 /bilingual-documentation/division-of-labor: en: 88097c97cb72f3ba zh: 78a18000626cc818 diff --git a/docs/i18n/README.md b/docs/i18n/README.md index 7ed0a4bf7c..fc4b23279c 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -47,6 +47,8 @@ The gate's limit, stated plainly: **a green gate means the pair was confirmed co **Scope**: the root `CONTRIBUTING.md`, `BRAND_GUIDELINES.md`, and `SAFETY.md` documents, every non-vendor README, and every active document under `.agents/notes/**`, `docs/**`, and `python/**`. README matching is case-insensitive on the basename and covers future directories without another manifest edit. Dependency and ignored build-output trees and the frozen `.agents/notes/archived/` tree are discovery exclusions, not evolving translation source. +**Outside scope**: a bundle's detail documents under `packages///docs/`, which locale `meta.detail` names for the plugin page. The page shows the file for the active UI language, so these documents carry no in-document language switcher and no consistency record; [`verify-md-links`](../../scripts/verify-md-links.ts) and [`verify-md-wrap`](../../scripts/verify-md-wrap.ts) still check both languages, and review keeps them in step. + Generated English references and graphs participate in pairing when a reviewed Chinese counterpart is available. A generator puts its generated data in generated regions and writes each region into both pages, so regeneration never requires a manual Chinese update or a new record; only the prose outside the regions is translated and recorded. Freshness and pairing gates enforce their respective invariants independently. Shared generated regions, such as the Cordis subsystem regions and the per-package regions of the [config catalog](../config-catalog.md), keep every generated byte identical across languages except localized paired-document paths; labels inside a region are code identifiers rather than translated words. A full-page generator can also render source-owned paired prose, such as [persistence-catalog-text.ts](../../scripts/persistence-catalog-text.ts), from the same structural data with matching Markdown structure and identical code blocks. It regenerates both pages and their consistency record together; review still verifies the paired prose's meaning and terminology. diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index b098af1072..927646cdf8 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -49,6 +49,8 @@ **范围**:根目录 `CONTRIBUTING.md`、`BRAND_GUIDELINES.md` 与 `SAFETY.md` 文档、除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。 +**范围外**:组合包 `packages///docs/` 下由 locale `meta.detail` 指定给插件页的详情文档。插件页只显示当前界面语言对应的文件,因此这些文档不带文档内的语言切换行,也没有一致性记录;[`verify-md-links`](../../scripts/verify-md-links.ts) 与 [`verify-md-wrap`](../../scripts/verify-md-wrap.ts) 仍会检查两种语言,两侧是否同步由评审把关。 + 有经评审的中文对侧的生成英文参考文档和图文档遵循配对规则。生成器把生成数据放在生成区域中,并把每个区域写入两种语言的页面,因此重新生成不需要手工更新中文,也不需要重新记录;只有区域外的说明文字需要翻译并计入记录。新鲜度门禁与配对门禁各自独立强制其约束。 共享生成区域(例如 Cordis 子系统区域和[配置目录](../config-catalog.zh.md)中每个包的区域)在两种语言间保持每个生成字节相同,仅配对文档路径按语言本地化;区域内的标签使用代码标识符而非译文。整页生成器也可以从相同的结构数据渲染源码维护的双语说明,例如 [persistence-catalog-text.ts](../../scripts/persistence-catalog-text.ts),并保持 Markdown 结构对应、代码块完全相同。它同时重新生成两页及其一致性记录;评审仍须验证双语说明的含义与术语。 diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index a3f547e289..fad9260ee9 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package: - en: e37d578192901fe4 - zh: 4cb252f8b297fae6 + en: 793bb9459e4928a0 + zh: 1c2158a2754d51ca /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle: en: fcb2e69c8b028c0a zh: d505a075679974eb @@ -36,8 +36,8 @@ en: c7cd3de96e538815 zh: ca603c132c0885fb /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/page-tabs: - en: 76e0908903ab8fde - zh: ce6ae1cdd6aea971 + en: 25451392e1118869 + zh: 8ba84f8fdbb47b3f /deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation: en: 203168fc87e939bd zh: 72d0ccd2c54f3f44 @@ -48,8 +48,8 @@ en: 21a0bbe8eb92229c zh: 096ff63db61992bb /deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation/configuration-slots: - en: 4406ecf602ebad0e - zh: c0f3d5c34ac82bb3 + en: 9b37ad9966d54468 + zh: 801d8d83bd70f480 /deepseek-ai-dsh-client-ui-plugin-manager/further-exploration: en: f9a42dfed0360d6a zh: 455a70bcb6c69624 @@ -60,8 +60,8 @@ en: ca75c51c89c2c9b0 zh: 2064958b0754647e /deepseek-ai-dsh-client-ui-plugin-manager/known-limitations-and-deferred-work: - en: 344ba1e802163bb4 - zh: f3cc2e30ffb037cb + en: 149c0d23a56a4944 + zh: e2feaadc871581ae /deepseek-ai-dsh-client-ui-plugin-manager/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index 74679a2e1c..dead9f3f09 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -27,7 +27,7 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b ## Use this package -Select **Plugins** in the sidebar. The information button uses an 11px caption-colored icon in a 20px square hover area, with a 4px gap after the subtitle. It explains that this page configures official plugins and installs or manages other plugins, while **Settings → Built-in plugins** shows the built-in inventory and runtime status. Hover or focus shows the shared tooltip; clicking keeps it open until another click, Escape, Tab, or an outside click. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built-in plugin settings** lists official configuration pages by their configured order: Subagent, web search, agent loop, then shell. **Experimental plugins** shows the primary optional bundles, with **More** beside its heading opening a separate page of the same title. That page lists all offered official bundles, including those on the main list, with Codex and Claude Code subagents last. Its back button returns to the main list, where **Installed plugins** remains below Experimental plugins. Bundle details return to the list they were opened from. Switching pages or tabs starts at the top; refreshing the current page preserves its scroll position. Catalog entries keep their position after installation or removal. **Installed plugins** lists other profile bundles. Installation-carried optional bundles have no uninstall action. Experimental packages retain the **Experimental** marker on both lists. Bundle details can show a feature preview below the description, also while the bundle is disabled; **DSH badge skill** shows one in English or Chinese following the UI language, and a decode failure shows a short notice without blocking management. Switching on a bundle that page holds back adds it to the main list in the position that page shows it, and switching it off leaves it on that page alone; other bundles keep their place on both. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Built-in plugins** section. +Select **Plugins** in the sidebar. The information button uses an 11px caption-colored icon in a 20px square hover area, with a 4px gap after the subtitle. It explains that this page configures official plugins and installs or manages other plugins, while **Settings → Built-in plugins** shows the built-in inventory and runtime status. Hover or focus shows the shared tooltip; clicking keeps it open until another click, Escape, Tab, or an outside click. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built-in plugin settings** lists official configuration pages by their configured order: Subagent, web search, agent loop, then shell. **Experimental plugins** shows the primary optional bundles, with **More** beside its heading opening a separate page of the same title. That page lists all offered official bundles, including those on the main list, with Codex and Claude Code subagents last. Its back button returns to the main list, where **Installed plugins** remains below Experimental plugins. Bundle details return to the list they were opened from. Switching pages or tabs starts at the top; refreshing the current page preserves its scroll position. Catalog entries keep their position after installation or removal. **Installed plugins** lists other profile bundles. Installation-carried optional bundles have no uninstall action. Experimental packages retain the **Experimental** marker on both lists. Switching on a bundle that page holds back adds it to the main list in the position that page shows it, and switching it off leaves it on that page alone; other bundles keep their place on both. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Built-in plugins** section. The heading moves its complete action group to another line when space is insufficient. Action labels stay on one line, and the information button follows the subtitle text. @@ -118,8 +118,6 @@ Another plugin adds a tab after the page's own **Installed** tab by registering ----- -Built-in illustrations follow the [feature preview authoring guide](../../../docs/cookbook/adding-a-plugin-preview.md): optional, one 640×300 SVG per language, with both English and Chinese required. The internal map and its asset test enforce the format; packages have no preview declaration interface. - ## Understand the implementation @@ -144,7 +142,7 @@ The add control reuses `Button`, `Menu`, and `MenuItemButton`. Both installation Custom item pages use the Host entry id as their registration id; row pages use the bundle package and row id. The page owner supplies `form.state` and `form.mutate(operations, expectedRevision)` when the entry exposes editable Config fields. A custom page owns its draft and validation display, and may reuse `ConfigField` from ui-primitives. Bundle-wide pages can contain several entries and have no single form. -The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. The registration-owned navigation store selects the main list, the full experimental plugin list, a bundle, an official plugin, or a bundle row; switching away from the Plugins panel resets it to the list, while React remounts preserve the selected target. A fresh registration starts on the main list. Installation highlighting scrolls to a card only when it is on the current page; it does not change the selected page. Other Client plugins inject `pluginNavigation` and call `ctx.pluginNavigation.openBundle(packageName)` to open bundle details without changing the current Session. The target survives the initial inventory load; an absent bundle displays the list. A registration lives with the browser half that made it. `dsh-client-modules` attaches a package's browser half to the Loader row whose specifier is the bare package name, so every page a bundle registers, for itself or for any of its rows, goes away when that row is switched off; a sub-plugin whose page must outlive the other rows ships as its own package. Official packages whose names begin with `@deepseek-ai/dsh-experimental-` display the Experimental marker. Feature previews are SVG files under `src/client/assets/`, keyed by package name in the page and embedded in the Client bundle; packages cannot declare their own ([rationale](../../../.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.md)). +The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. The registration-owned navigation store selects the main list, the full experimental plugin list, a bundle, an official plugin, or a bundle row; switching away from the Plugins panel resets it to the list, while React remounts preserve the selected target. A fresh registration starts on the main list. Installation highlighting scrolls to a card only when it is on the current page; it does not change the selected page. Other Client plugins inject `pluginNavigation` and call `ctx.pluginNavigation.openBundle(packageName)` to open bundle details without changing the current Session. The target survives the initial inventory load; an absent bundle displays the list. A registration lives with the browser half that made it. `dsh-client-modules` attaches a package's browser half to the Loader row whose specifier is the bare package name, so every page a bundle registers, for itself or for any of its rows, goes away when that row is switched off; a sub-plugin whose page must outlive the other rows ships as its own package. Official packages whose names begin with `@deepseek-ai/dsh-experimental-` display the Experimental marker. `plugins.bundle.config` supplies bundle detail configuration, keyed by npm package name, and receives `busy` while the owning bundle is being changed. `plugins.bundle.activation` offers bundle-owned guidance after explicit enablement from either the list or detail page, with callbacks to dismiss it or open the bundle details. It does not appear merely because an enabled bundle was listed. @@ -175,7 +173,6 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work -- Feature previews have one image per language and no dark-theme variant, so the badge skill preview keeps its light background in dark theme. - Ping response time does not measure package-download throughput. Probes use the Host fetch route, which pnpm-specific proxy settings can differ from; users can always choose the registry manually, and installation retains its existing fallback rules. diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index dfb41ce074..1e536ef0fc 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -27,7 +27,7 @@ kind: "package-reference" ## 使用本包 -在侧栏选择**插件**。信息按钮使用 11px 的 caption 色图标和 20px 方形悬停区域,与副标题间隔 4px。它说明当前页用于配置官方插件、安装和管理其他插件,**设置 → 内置插件**则展示内置插件列表及运行状态。悬停或聚焦显示共享 tooltip;点击后保持展开,直至再次点击、Escape、Tab 或外部点击。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置插件配置**按配置的顺序列出官方配置页:子智能体、网页搜索、Agent 循环、终端。**实验性插件**展示主要的可选组合包,标题旁的**更多**打开同名的独立页面。该页列出全部官方组合包,也包含主列表已有的条目,Codex 和 Claude Code 子智能体排在最后。返回按钮回到主列表,**已安装插件**保持在实验性插件下方。组合包详情返回打开它的列表。切换页面或标签时从顶部开始,同一页面刷新数据时保留滚动位置。目录条目在安装或移除后保持原位置。**已安装插件**列出其他 profile 组合包。安装随附的可选组合包没有卸载操作。实验性包在两个列表中均保留**实验性**标签。组合包详情可在说明下方展示功能配图,未启用时也可查看;**DSH 徽章技能**按界面语言展示中文或英文配图,解码失败时显示简短提示,不影响管理操作。开启仅在该页列出的组合包会按该页中的位置把它加入主列表,关闭后它只留在该页;其他组合包在两个列表中均保持原位。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**内置插件**分区中编辑。 +在侧栏选择**插件**。信息按钮使用 11px 的 caption 色图标和 20px 方形悬停区域,与副标题间隔 4px。它说明当前页用于配置官方插件、安装和管理其他插件,**设置 → 内置插件**则展示内置插件列表及运行状态。悬停或聚焦显示共享 tooltip;点击后保持展开,直至再次点击、Escape、Tab 或外部点击。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置插件配置**按配置的顺序列出官方配置页:子智能体、网页搜索、Agent 循环、终端。**实验性插件**展示主要的可选组合包,标题旁的**更多**打开同名的独立页面。该页列出全部官方组合包,也包含主列表已有的条目,Codex 和 Claude Code 子智能体排在最后。返回按钮回到主列表,**已安装插件**保持在实验性插件下方。组合包详情返回打开它的列表。目录条目在安装或移除后保持原位置。**已安装插件**列出其他 profile 组合包。安装随附的可选组合包没有卸载操作。实验性包在两个列表中均保留**实验性**标签。开启仅在该页列出的组合包会按该页中的位置把它加入主列表,关闭后它只留在该页;其他组合包在两个列表中均保持原位。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**内置插件**分区中编辑。 切换页面或标签时从顶部开始,同一页面刷新数据时保留滚动位置。 空间不足时,页头将整个操作组移到下一行。操作文案保持单行,信息按钮紧随副标题文字。 @@ -118,8 +118,6 @@ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({ ----- -内置配图遵循[功能配图创作指引](../../../docs/cookbook/adding-a-plugin-preview.zh.md):可选,每种语言一张 640×300 SVG,必须同时提供英文和中文。内部映射及素材测试约束格式,包没有配图声明接口。 - ## 理解实现 @@ -144,7 +142,7 @@ Host 入口通过生成的 Remote 接口暴露 `pluginRegistryProbe.fastest()` 自定义条目页以 Host 条目 id 作为注册 id;行页面使用 bundle 包名和行 id。当条目提供可编辑 Config 字段时,页面宿主传入 `form.state` 和 `form.mutate(operations, expectedRevision)`。自定义页面负责草稿和校验提示,并可复用 ui-primitives 的 `ConfigField`。整个 bundle 的页面可以包含多个条目,因此没有单一表单。 -页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。注册拥有的导航 store 选择主列表、完整的实验性插件列表、某个组合包、某个官方插件或组合包的某一行;切换离开插件面板时重置为列表,React 重新挂载则保留所选目标。新的注册从主列表开始。安装后的高亮仅在当前页面有对应卡片时滚动定位,不改变所选页面。其他 Client 插件注入 `pluginNavigation`,调用 `ctx.pluginNavigation.openBundle(packageName)` 即可打开组合包详情,不改变当前 Session。首次读取清单期间保留导航目标;组合包不存在时显示列表。注册与做出它的浏览器半侧同生共死。`dsh-client-modules` 只把一个包的浏览器半侧挂在说明符恰为包名的那一行 Loader 行上,所以组合包为自己或任一行注册的页面,都会在那一行被关闭时一起消失;需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。 名称以 `@deepseek-ai/dsh-experimental-` 开头的官方包显示实验性标记。功能配图是 `src/client/assets/` 下的 SVG 文件,由页面按包名指定并嵌入 Client bundle;包不能自行声明配图([决策依据](../../../.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.zh.md))。 +页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。注册拥有的导航 store 选择主列表、完整的实验性插件列表、某个组合包、某个官方插件或组合包的某一行;切换离开插件面板时重置为列表,React 重新挂载则保留所选目标。新的注册从主列表开始。安装后的高亮仅在当前页面有对应卡片时滚动定位,不改变所选页面。其他 Client 插件注入 `pluginNavigation`,调用 `ctx.pluginNavigation.openBundle(packageName)` 即可打开组合包详情,不改变当前 Session。首次读取清单期间保留导航目标;组合包不存在时显示列表。注册与做出它的浏览器半侧同生共死。`dsh-client-modules` 只把一个包的浏览器半侧挂在说明符恰为包名的那一行 Loader 行上,所以组合包为自己或任一行注册的页面,都会在那一行被关闭时一起消失;需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。 名称以 `@deepseek-ai/dsh-experimental-` 开头的官方包显示实验性标记。 `plugins.bundle.config` 以 npm 包名为 key,提供 Bundle 详情配置,并通过 `busy` 接收所属 Bundle 正在变更的状态。`plugins.bundle.activation` 在用户从列表或详情页显式启用后提供 Bundle 自有引导,并传入关闭引导和打开详情的回调。仅列出已启用的 Bundle 不会触发引导。 @@ -175,7 +173,6 @@ Host 入口通过生成的 Remote 接口暴露 `pluginRegistryProbe.fastest()` ## 已知限制与延期工作 -- 功能配图每种语言只有一张,没有深色主题版本,因此徽章技能配图在深色主题中仍为浅色背景。 - Ping 响应时间不代表包下载吞吐量。探测使用 Host fetch 的网络路径,pnpm 专用代理设置可能使用不同路径;用户始终可以手动选择安装源,安装仍遵循现有回退规则。 diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css index 0655437cc2..9cdfe3aa36 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css @@ -1321,17 +1321,6 @@ label.registryOption { margin-top: 12px; } -.packagePreview { - display: block; - align-self: flex-start; - max-width: 100%; - max-height: 320px; - margin-top: 24px; - border: 0.5px solid var(--dsw-alias-border-l4); - border-radius: var(--dsw-radius-lg); - object-fit: contain; -} - .detailSections { display: flex; flex-direction: column; diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index 2c308e449e..94753947d3 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -35,10 +35,6 @@ import type { ConfigPageForm } from './slot-contract.ts' import { UpdateDialog, VersionSection } from './versions.tsx' import { BundleDetailSection } from './BundleDetailSection.tsx' import css from './PluginManagerPage.module.css' -import badgeSkillPreview from './assets/badge-skill-preview.svg' -import badgeSkillPreviewZh from './assets/badge-skill-preview.zh.svg' -import voiceInputPreview from './assets/voice-input-preview.svg' -import voiceInputPreviewZh from './assets/voice-input-preview.zh.svg' /** Full component props assembled by the main slot renderer. */ export type PluginManagerPageProps = @@ -161,12 +157,6 @@ function orderOptionalBundles(packages: readonly PackageView[]): PackageView[] { return [...packages].sort((a, b) => optionalBundleRank(a.name) - optionalBundleRank(b.name)) } -/** Feature previews of official bundles, shown on their detail pages and selected by UI language. */ -const BUNDLE_PREVIEWS = new Map>>([ - ['@deepseek-ai/dsh-experimental-badge-skill-bundle', { en: badgeSkillPreview, zh: badgeSkillPreviewZh }], - ['@deepseek-ai/dsh-experimental-voice-input-bundle', { en: voiceInputPreview, zh: voiceInputPreviewZh }], -]) - /** How long a toast holds: long enough to read a failure that names what broke. */ function toastHoldMs(text: string): number { return Math.min(8_000, Math.max(3_000, text.length * 80)) @@ -250,14 +240,6 @@ function PackageArtwork({ src, row = false, size = row ? ROW_ARTWORK_SIZE : CARD : { setFailedSource(src) }} /> } -/** A bundle's feature preview; a failed decode is reported in place without blocking management. */ -function PackagePreview({ src, title, t }: { readonly src: string; readonly title: string; readonly t: Translate }): ReactNode { - const [failedSource, setFailedSource] = useState() - return src === failedSource - ?

{t('previewUnavailable')}

- : {t('previewAlt', { setFailedSource(src) }} /> -} - /** A row's switch: locked, saying why, when the Host refuses to address the row through the profile patch. */ function RowSwitch({ row, title, t, busy, onChange }: { readonly row: PackageRow @@ -763,8 +745,6 @@ function PackageDetail({ const { title, description, beta } = packageText(pkg, resolveText) const newer = versionUpdate(versions) const status = packageStatus(pkg) - const previews = BUNDLE_PREVIEWS.get(pkg.name) - const preview = previews === undefined ? undefined : resolveText(previews) const subject: PluginsSubject = { kind: 'bundle', pkg: packageRef(pkg) } return (
@@ -823,7 +803,6 @@ function PackageDetail({ {pkg.error === undefined ? null :

{t('reasonLabel')}: {managementText(pkg.error, t)}

} {pkg.readOnlyReason === undefined ? null :

{managementText({ code: pkg.readOnlyReason }, t)}

} - {preview === undefined ? null : }
{detail} {configured diff --git a/packages/client/ui-plugin-manager/src/client/assets.d.ts b/packages/client/ui-plugin-manager/src/client/assets.d.ts deleted file mode 100644 index 2d2032abd9..0000000000 --- a/packages/client/ui-plugin-manager/src/client/assets.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** Illustration imports are embedded in the plugin manager client bundle. */ -declare module '*.svg' { - const url: string - export default url -} diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 3b156c2540..dcde50582d 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -41,8 +41,6 @@ export const zh = { statusBeta: '实验性', reasonLabel: '原因', metadataError: '包元信息错误:{error}', - previewAlt: '{name}功能预览', - previewUnavailable: '无法显示功能配图', versionTag: 'v{version}', partsLabel: '包含的组件', sourceTitle: '来源信息', @@ -336,8 +334,6 @@ export const en = { statusBeta: 'Experimental', reasonLabel: 'Reason', metadataError: 'Package metadata error: {error}', - previewAlt: '{name} feature preview', - previewUnavailable: 'Could not display feature preview', versionTag: 'v{version}', partsLabel: 'Components', sourceTitle: 'Source', diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx index 3831059a6f..3ebfab4b01 100644 --- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx +++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx @@ -877,47 +877,6 @@ describe('PluginManagerPage', () => { expect(detailImage.width).toBe(36) }) - it('shows a known bundle preview in its details before activation, follows the UI language, and survives decode failure', () => { - const name = '@deepseek-ai/dsh-experimental-badge-skill-bundle' - const { actions, setLanguage } = renderTab({ packages: [pkg({ name, enabled: false }), pkg()] }) - expect(screen.queryByRole('img')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: `View ${name}` })) - const english = screen.getByRole('img', { name: en.previewAlt.replace('{name}', name) }).getAttribute('src') - expect(english).toMatch(/badge-skill-preview\.svg$/) - setLanguage(zh) - const chinese = screen.getByRole('img', { name: zh.previewAlt.replace('{name}', name) }) - expect(chinese.getAttribute('src')).toMatch(/badge-skill-preview\.zh\.svg$/) - fireEvent.error(chinese) - expect(screen.queryByRole('img')).toBeNull() - expect(screen.getByText(zh.previewUnavailable).getAttribute('role')).toBe('status') - fireEvent.click(screen.getByRole('switch', { name: zh.enableToggle.replace('{name}', name) })) - expect(actions.setEnabled).toHaveBeenCalledWith(name, true) - setLanguage(en) - expect(screen.getByRole('img').getAttribute('src')).toBe(english) - expect(screen.queryByText(en.previewUnavailable)).toBeNull() - }) - - it('shows no preview for bundles without one', () => { - renderTab({ packages: [pkg()] }) - fireEvent.click(screen.getByRole('button', { name: 'View dsh-better-sidebar' })) - expect(screen.queryByRole('img')).toBeNull() - }) - - it('places the new voice illustration after its description and switches the single image with the UI language', () => { - const name = '@deepseek-ai/dsh-experimental-voice-input-bundle' - const description = 'Turn speech into local notes.' - const { setLanguage } = renderTab({ packages: [pkg({ name, enabled: false, meta: { description }, rows: [row()] })] }) - fireEvent.click(screen.getByRole('button', { name: `View ${name}` })) - const image = screen.getByRole('img') - expect(image.getAttribute('src')).toMatch(/voice-input-preview\.svg$/u) - expect(screen.getByText(description).compareDocumentPosition(image) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() - expect(image.compareDocumentPosition(screen.getByRole('heading', { name: en.partsLabel })) & Node.DOCUMENT_POSITION_FOLLOWING) - .toBeTruthy() - setLanguage(zh) - expect(screen.getAllByRole('img')).toHaveLength(1) - expect(screen.getByRole('img').getAttribute('src')).toMatch(/voice-input-preview\.zh\.svg$/u) - }) - it('shows metadata diagnostics without blocking management or displaying legacy descriptions', () => { const error = 'locale/zh.json: invalid title' const { actions, set, setLanguage } = renderTab({ diff --git a/packages/client/ui-primitives/src/DetailDocument.module.css b/packages/client/ui-primitives/src/DetailDocument.module.css index 6e75b56606..3b4fdf0653 100644 --- a/packages/client/ui-primitives/src/DetailDocument.module.css +++ b/packages/client/ui-primitives/src/DetailDocument.module.css @@ -10,9 +10,11 @@ /* A long README scrolls inside the section instead of pushing the page's later sections out of reach. */ max-height: min(560px, 60vh); overflow-y: auto; - padding: 4px 16px; - border: 0.5px solid var(--dsw-alias-border-l1); - border-radius: var(--dsw-radius-lg); + /* Horizontal rules separate the document from its neighbors; a full frame would clash with the images' own corners. */ + border-block: 0.5px solid var(--dsw-alias-border-l1); + /* Keep the scrollbar off the text and the text width independent of overflow. */ + scrollbar-gutter: stable; + padding: 12px 0; overflow-wrap: anywhere; } diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index 205e4c6b84..8a2a17dc02 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 405b30feaf48c087 /deepseek-ai-dsh-client-ui-schedule/use-this-package: - en: edba0e1573f3ce42 - zh: 77875b469d94cc62 + en: 7d44cd1640bebfad + zh: 1d6b6c28a3c27115 /deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source: en: 6bd6882dbbbefcb0 zh: 03aa15f1a65d13dc diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index 220af1e6c7..6f02ed050a 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -25,7 +25,7 @@ Manage active and inactive tasks from the Automation tasks page and the right Si ## Use this package -The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. +The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index 88575c4405..b7de6727ed 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 +随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。 diff --git a/packages/client/ui-schedule/src/client/ScheduleEmptyIllustration.tsx b/packages/client/ui-schedule/src/client/ScheduleEmptyIllustration.tsx new file mode 100644 index 0000000000..e6cee2a839 --- /dev/null +++ b/packages/client/ui-schedule/src/client/ScheduleEmptyIllustration.tsx @@ -0,0 +1,37 @@ +/** Decorative document-and-clock artwork with theme-owned colors. */ + +interface ScheduleEmptyIllustrationProps { + readonly className?: string | undefined +} + +/** + * Render the empty-state illustration without adding an accessible announcement. + * @param props - optional CSS class applied to the root SVG. + * @returns the decorative inline SVG element. + */ +export function ScheduleEmptyIllustration({ className }: ScheduleEmptyIllustrationProps) { + return ( + + ) +} diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css index ba3ee2a030..ebdb77b4b8 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css @@ -388,6 +388,14 @@ color: var(--dsw-alias-label-caption); } +.emptyIllustration { + flex: none; + width: 110px; + max-width: 100%; + height: auto; + margin-bottom: 12px; +} + /* The mock's `.empty h3 + .btn`: the empty state's action keeps the mock's 16px above. */ .emptyAction { diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx index 8f09c96137..7d18063ae8 100644 --- a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx +++ b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx @@ -13,6 +13,7 @@ import { TaskDetail, useTaskDetail, type TaskDetailInjected } from './TaskDetail import { useRelativeClock } from './relative-clock.ts' import { formatScheduleFrequency, nextRunParts, taskName, zoneLabel } from './schedule-format.ts' import type { FrequencyZone } from './schedule-format.ts' +import { ScheduleEmptyIllustration } from './ScheduleEmptyIllustration.tsx' import css from './TaskManagerPage.module.css' /** Injected catalog and task actions for the management page. */ @@ -151,7 +152,7 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
{selected === undefined && 0} onRetry={onRetry} t={t} />} {status === 'ready' && rows.length === 0 &&
- +

{t(emptyTitle)}