From 0495ad9846a52e7e694e7e1b827c74e02e930e9d Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 17 Sep 2026 17:50:32 +0800 Subject: [PATCH 01/28] fix(web): use the plain text file icon for plan tabs --- packages/client/ui-plan/README.i18n.yaml | 4 ++-- packages/client/ui-plan/README.md | 2 +- packages/client/ui-plan/README.zh.md | 2 +- packages/client/ui-plan/src/client/PlanPreview.module.css | 2 +- packages/client/ui-plan/src/client/PlanPreview.tsx | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/client/ui-plan/README.i18n.yaml b/packages/client/ui-plan/README.i18n.yaml index 0023f8b06c..17181b1dde 100644 --- a/packages/client/ui-plan/README.i18n.yaml +++ b/packages/client/ui-plan/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-plan/README.md -README.md: f7e7aede2c76717228d101153e4d3d2f8d3f88c4 -README.zh.md: 9ab37a25dd7cb406b42bf5c531445357d6725e2e +README.md: e269ad270ed6a0d72e4cb7424a86328d3eb6cfb3 +README.zh.md: 202a6737986630f18ffdf7d010174c98be732ba5 diff --git a/packages/client/ui-plan/README.md b/packages/client/ui-plan/README.md index f7e7aede2c..e269ad270e 100644 --- a/packages/client/ui-plan/README.md +++ b/packages/client/ui-plan/README.md @@ -33,7 +33,7 @@ While the effective target is plan mode, the seat renders the warn-colored "Plan ### Reading submitted plans -When a Turn ends, each submitted plan appears in its final artifact area, using the file-delivery card treatment with a Markdown icon, title, and Open action. A pending plan opens automatically once per submission in the current browser session. Closing it stays effective across review remounts; a new submission opens its own plan. Historical cards open only when clicked. Use the card or review strip’s View full plan link to read and copy the complete Markdown. Plan tabs show a document-and-pen icon. Different submissions retain separate tabs; the review buttons alone decide whether implementation may begin. +When a Turn ends, each submitted plan appears in its final artifact area, using the file-delivery card treatment with a Markdown icon, title, and Open action. A pending plan opens automatically once per submission in the current browser session. Closing it stays effective across review remounts; a new submission opens its own plan. Historical cards open only when clicked. Use the card or review strip’s View full plan link to read and copy the complete Markdown. Plan tabs show a plain text file icon. Different submissions retain separate tabs; the review buttons alone decide whether implementation may begin. A review without a logged invocation also opens automatically. Its complete text lives only in the tab’s navigation memory, and the pending review card can reopen it. Reloading the page loses that text; an expired preview directs the user back to a pending review. diff --git a/packages/client/ui-plan/README.zh.md b/packages/client/ui-plan/README.zh.md index 9ab37a25dd..202a673798 100644 --- a/packages/client/ui-plan/README.zh.md +++ b/packages/client/ui-plan/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" ### 查看已提交的计划 -回合结束后,每次提交的计划都会显示在该回合末尾的产物区域,并采用文件产物卡片的样式,包含 Markdown 图标、标题和“打开”操作。在当前浏览器会话中,每份待审计划自动打开一次;关闭后,重新挂载审批组件不会再次展开,新提交会打开其对应计划。历史卡片仅在点击后打开。通过卡片或审批条带上的“查看全文”链接可阅读和复制完整 Markdown。计划标签页显示文档加笔的图标。不同提交保留独立标签页;是否开始实施仍由审批按钮决定。 +回合结束后,每次提交的计划都会显示在该回合末尾的产物区域,并采用文件产物卡片的样式,包含 Markdown 图标、标题和“打开”操作。在当前浏览器会话中,每份待审计划自动打开一次;关闭后,重新挂载审批组件不会再次展开,新提交会打开其对应计划。历史卡片仅在点击后打开。通过卡片或审批条带上的“查看全文”链接可阅读和复制完整 Markdown。计划标签页显示纯文本文件图标。不同提交保留独立标签页;是否开始实施仍由审批按钮决定。 没有已记录调用标识的审批也会自动打开。完整正文只保存在标签页的导航内存中,待处理审批卡片可以重新打开它。刷新页面会丢失该正文;失效的预览会提示用户返回待处理审批。 diff --git a/packages/client/ui-plan/src/client/PlanPreview.module.css b/packages/client/ui-plan/src/client/PlanPreview.module.css index 0fcac5cd7b..c0f2761be0 100644 --- a/packages/client/ui-plan/src/client/PlanPreview.module.css +++ b/packages/client/ui-plan/src/client/PlanPreview.module.css @@ -37,5 +37,5 @@ .toolbar { display: flex; justify-content: flex-end; margin-bottom: 8px; } .document { color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 1.75; overflow-wrap: anywhere; } .message { padding: 24px; color: var(--dsw-alias-label-secondary); font-size: 14px; } -.titleIcon { display: inline-flex; flex: none; } +.titleIcon { flex: none; } @media (max-width: 767px) { .preview { padding: 16px 18px 32px; } } diff --git a/packages/client/ui-plan/src/client/PlanPreview.tsx b/packages/client/ui-plan/src/client/PlanPreview.tsx index b417507c77..2f0c05508a 100644 --- a/packages/client/ui-plan/src/client/PlanPreview.tsx +++ b/packages/client/ui-plan/src/client/PlanPreview.tsx @@ -1,6 +1,6 @@ /** Read-only Markdown viewer for logged plans and temporary review documents. */ import { useMemo } from 'react' -import { IconCopyOutline16, IconPlanOutline14, MarkdownText, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives' +import { FileTypeIcon, IconCopyOutline16, MarkdownText, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from './plan-resource.ts' @@ -42,9 +42,9 @@ export function PlanPreview({ useTabInfo, useResource, t }: PlanPreviewProps) { } /** - * Display a plan icon and the heading in its tab after resource recovery. + * Display a plain file icon and the heading in its tab after resource recovery. * @param props - Framework-bound tab identity and resource reader. - * @returns a decorative plan icon followed by the recovered title or initial localized label. + * @returns a decorative file icon followed by the recovered title or initial localized label. */ export function PlanTitle({ useTabInfo, useResource }: PropsRuntime<'sidebar.right.pane.tab.title'>) { const tab = useTabInfo() @@ -52,5 +52,5 @@ export function PlanTitle({ useTabInfo, useResource }: PropsRuntime<'sidebar.rig const params = tab.tab.navigation.params const plan = isReviewPreviewAddress(tab.tab.navigation.address) ? (params !== undefined && 'planReview' in params ? params.planReview : undefined) : resource.value - return <>{plan?.title ?? tab.tab.title} + return <>{plan?.title ?? tab.tab.title} } From 29126c0a32001dae24abadd419eaf7d5178887df Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 17 Sep 2026 19:57:18 +0800 Subject: [PATCH 02/28] fix(web): align plan preview copy controls with Markdown files --- packages/client/ui-plan/README.i18n.yaml | 4 ++-- packages/client/ui-plan/README.md | 2 +- packages/client/ui-plan/README.zh.md | 2 +- .../ui-plan/src/client/PlanPreview.module.css | 5 +---- .../client/ui-plan/src/client/PlanPreview.tsx | 6 ++---- .../tests/plan-preview.client.spec.tsx | 20 ++++--------------- 6 files changed, 11 insertions(+), 28 deletions(-) diff --git a/packages/client/ui-plan/README.i18n.yaml b/packages/client/ui-plan/README.i18n.yaml index 17181b1dde..20e6f1952e 100644 --- a/packages/client/ui-plan/README.i18n.yaml +++ b/packages/client/ui-plan/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-plan/README.md -README.md: e269ad270ed6a0d72e4cb7424a86328d3eb6cfb3 -README.zh.md: 202a6737986630f18ffdf7d010174c98be732ba5 +README.md: f5cec26aff9ec7fc478915c20bca86c06474f9f7 +README.zh.md: 4bd416b757b7ff6bae2947b84db9f8d50101aa00 diff --git a/packages/client/ui-plan/README.md b/packages/client/ui-plan/README.md index e269ad270e..f5cec26aff 100644 --- a/packages/client/ui-plan/README.md +++ b/packages/client/ui-plan/README.md @@ -33,7 +33,7 @@ While the effective target is plan mode, the seat renders the warn-colored "Plan ### Reading submitted plans -When a Turn ends, each submitted plan appears in its final artifact area, using the file-delivery card treatment with a Markdown icon, title, and Open action. A pending plan opens automatically once per submission in the current browser session. Closing it stays effective across review remounts; a new submission opens its own plan. Historical cards open only when clicked. Use the card or review strip’s View full plan link to read and copy the complete Markdown. Plan tabs show a plain text file icon. Different submissions retain separate tabs; the review buttons alone decide whether implementation may begin. +When a Turn ends, each submitted plan appears in its final artifact area, using the file-delivery card treatment with a Markdown icon, title, and Open action. A pending plan opens automatically once per submission in the current browser session. Closing it stays effective across review remounts; a new submission opens its own plan. Historical cards open only when clicked. Use the card or review strip’s View full plan link to read the complete Markdown. Plan tabs show a plain text file icon. Different submissions retain separate tabs; the review buttons alone decide whether implementation may begin. A review without a logged invocation also opens automatically. Its complete text lives only in the tab’s navigation memory, and the pending review card can reopen it. Reloading the page loses that text; an expired preview directs the user back to a pending review. diff --git a/packages/client/ui-plan/README.zh.md b/packages/client/ui-plan/README.zh.md index 202a673798..4bd416b757 100644 --- a/packages/client/ui-plan/README.zh.md +++ b/packages/client/ui-plan/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" ### 查看已提交的计划 -回合结束后,每次提交的计划都会显示在该回合末尾的产物区域,并采用文件产物卡片的样式,包含 Markdown 图标、标题和“打开”操作。在当前浏览器会话中,每份待审计划自动打开一次;关闭后,重新挂载审批组件不会再次展开,新提交会打开其对应计划。历史卡片仅在点击后打开。通过卡片或审批条带上的“查看全文”链接可阅读和复制完整 Markdown。计划标签页显示纯文本文件图标。不同提交保留独立标签页;是否开始实施仍由审批按钮决定。 +回合结束后,每次提交的计划都会显示在该回合末尾的产物区域,并采用文件产物卡片的样式,包含 Markdown 图标、标题和“打开”操作。在当前浏览器会话中,每份待审计划自动打开一次;关闭后,重新挂载审批组件不会再次展开,新提交会打开其对应计划。历史卡片仅在点击后打开。通过卡片或审批条带上的“查看全文”链接可阅读完整 Markdown。计划标签页显示纯文本文件图标。不同提交保留独立标签页;是否开始实施仍由审批按钮决定。 没有已记录调用标识的审批也会自动打开。完整正文只保存在标签页的导航内存中,待处理审批卡片可以重新打开它。刷新页面会丢失该正文;失效的预览会提示用户返回待处理审批。 diff --git a/packages/client/ui-plan/src/client/PlanPreview.module.css b/packages/client/ui-plan/src/client/PlanPreview.module.css index c0f2761be0..d63529070b 100644 --- a/packages/client/ui-plan/src/client/PlanPreview.module.css +++ b/packages/client/ui-plan/src/client/PlanPreview.module.css @@ -22,7 +22,7 @@ } :global(body[data-ds-dark-theme]) .card { --plan-card-fill: var(--dsw-static-neutral-850); --plan-card-hover: var(--dsw-static-neutral-800); } .card:hover { background: var(--plan-card-hover); } -.card:focus-visible, .iconButton:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 3px; } +.card:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 3px; } .cardIcon { box-sizing: border-box; flex: none; display: grid; place-items: center; width: 40px; height: 40px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--plan-card-fill); } .cardDetails { display: flex; flex: 1; flex-direction: column; gap: 2px; min-width: 0; } .cardTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 500; line-height: 20px; } @@ -31,10 +31,7 @@ .reviewLink { display: inline-flex; align-items: center; gap: 4px; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; cursor: pointer; } .reviewLink:hover { color: var(--dsw-alias-label-primary); } .reviewLink:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 3px; } -.iconButton { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; } -.iconButton:hover { background: var(--dsw-alias-interactive-bg-hover); } .preview { position: relative; height: 100%; overflow: auto; padding: 20px 24px 40px; box-sizing: border-box; } -.toolbar { display: flex; justify-content: flex-end; margin-bottom: 8px; } .document { color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 1.75; overflow-wrap: anywhere; } .message { padding: 24px; color: var(--dsw-alias-label-secondary); font-size: 14px; } .titleIcon { flex: none; } diff --git a/packages/client/ui-plan/src/client/PlanPreview.tsx b/packages/client/ui-plan/src/client/PlanPreview.tsx index 2f0c05508a..b1e1689fe8 100644 --- a/packages/client/ui-plan/src/client/PlanPreview.tsx +++ b/packages/client/ui-plan/src/client/PlanPreview.tsx @@ -1,6 +1,6 @@ /** Read-only Markdown viewer for logged plans and temporary review documents. */ import { useMemo } from 'react' -import { FileTypeIcon, IconCopyOutline16, MarkdownText, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives' +import { FileTypeIcon, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from './plan-resource.ts' @@ -11,7 +11,7 @@ import css from './PlanPreview.module.css' type PlanPreviewProps = PropsRuntime<'sidebar.right.pane.tab'> & PropsLocale<'plan'> /** - * Render the submitted plan with its complete Markdown and a copy action. + * Render the submitted plan with its complete Markdown. * @param props - Framework-bound tab identity, resource, and copy. * @returns the plan document or a localized loading/failure state. */ @@ -34,8 +34,6 @@ export function PlanPreview({ useTabInfo, useResource, t }: PlanPreviewProps) { ) return (
-
) diff --git a/packages/client/ui-plan/tests/plan-preview.client.spec.tsx b/packages/client/ui-plan/tests/plan-preview.client.spec.tsx index 22b2fb006e..05208d847e 100644 --- a/packages/client/ui-plan/tests/plan-preview.client.spec.tsx +++ b/packages/client/ui-plan/tests/plan-preview.client.spec.tsx @@ -223,22 +223,10 @@ describe('plan entry points and document', () => { render([0]} />) expect(screen.getByText(plan.title)).toBeTruthy() }) - it('copies the complete Markdown and keeps a tab label while history loads', async () => { - const writeText = vi.fn().mockResolvedValue(undefined) - const clipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard') - Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }) - try { - const props = { t, useTabInfo: () => ({ tab: { title: 'Plan', navigation: { address: planAddress(target) } } }), useResource: () => ({ status: 'live', value: plan }) } - const view = render([0]} />) - fireEvent.click(screen.getByRole('button', { name: 'Copy' })) - await Promise.resolve() - expect(writeText).toHaveBeenCalledWith(markdown) - view.rerender( ({ status: 'loading' }) } as unknown as Parameters[0]} />) - expect(screen.getByText('Plan')).toBeTruthy() - } finally { - if (clipboard === undefined) Reflect.deleteProperty(navigator, 'clipboard') - else Object.defineProperty(navigator, 'clipboard', clipboard) - } + it('keeps a tab label while history loads', () => { + const props = { useTabInfo: () => ({ tab: { title: 'Plan', navigation: { address: planAddress(target) } } }), useResource: () => ({ status: 'loading' }) } + render([0]} />) + expect(screen.getByText('Plan')).toBeTruthy() }) it.each([en, zh])('localizes plan failures and unavailable providers', (dictionary) => { const props = { t: makeTranslate(dictionary, commonEn), From fd01a407b24b7a04befa4a87e484aeb5d14c1b5d Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 17 Sep 2026 20:12:42 +0800 Subject: [PATCH 03/28] test(web): update plan preview snapshots after removing copy button --- apps/web/tests/expected/plan-review/temporary.expected.md | 2 -- snapshots/web/plan-review/preview.expected.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/apps/web/tests/expected/plan-review/temporary.expected.md b/apps/web/tests/expected/plan-review/temporary.expected.md index 246480407c..7b214d1c16 100644 --- a/apps/web/tests/expected/plan-review/temporary.expected.md +++ b/apps/web/tests/expected/plan-review/temporary.expected.md @@ -1,6 +1,4 @@ - region "Temporary review": - - button "Copy": - - img - heading "Temporary review" [level=1] - paragraph: Review without a tool invocation. - heading "Implementation" [level=2] diff --git a/snapshots/web/plan-review/preview.expected.md b/snapshots/web/plan-review/preview.expected.md index 8b9c5daeb5..1bddbaa901 100644 --- a/snapshots/web/plan-review/preview.expected.md +++ b/snapshots/web/plan-review/preview.expected.md @@ -1,6 +1,4 @@ - 'region "Add `--greeting` flag to CLI"': - - button "Copy": - - img - heading "Add --greeting flag to CLI" [level=1]: - text: Add - code: "--greeting" From 9f21d7842a3f54a8a1c7ac8fac6b488cba211dd0 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 17 Sep 2026 20:41:39 +0800 Subject: [PATCH 04/28] fix(agent-team): enable tools and Web UI with one bundle --- ...xperimental-agent-teams-packages.i18n.yaml | 4 +- ...08-18-experimental-agent-teams-packages.md | 10 +- ...18-experimental-agent-teams-packages.zh.md | 10 +- ...6-09-15-shipped-optional-bundles.i18n.yaml | 4 +- .../2026-09-15-shipped-optional-bundles.md | 4 +- .../2026-09-15-shipped-optional-bundles.zh.md | 4 +- apps/cli/package.json | 1 - apps/web/tests/agent-team-panel.e2e.ts | 13 +-- apps/web/tests/agent-team-panel.overlay.yml | 4 +- .../plugin-config/official.expected.md | 8 +- .../plugin-manager/live-enabled.expected.md | 8 +- .../plugin-manager/manager.expected.md | 8 +- apps/web/tests/plugin-config.e2e.ts | 4 +- apps/web/tests/plugin-manager.e2e.ts | 46 +++++++-- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 1 - docs/config-catalog.zh.md | 1 - docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 2 - docs/module-graph.zh.md | 2 - packages/boot/app-boot/src/profile.ts | 1 - .../client/ui-plugin-manager/README.i18n.yaml | 4 +- packages/client/ui-plugin-manager/README.md | 2 +- .../client/ui-plugin-manager/README.zh.md | 2 +- .../ui-plugin-manager/src/client/locales.ts | 8 +- .../src/client/presentation.ts | 3 - .../tests/components.client.spec.tsx | 1 - packages/experimental/README.i18n.yaml | 4 +- packages/experimental/README.md | 3 +- packages/experimental/README.zh.md | 3 +- .../agent-team-profile/README.i18n.yaml | 4 +- .../experimental/agent-team-profile/README.md | 16 ++- .../agent-team-profile/README.zh.md | 16 ++- .../agent-team-profile/cordis.patch.yml | 3 + .../agent-team-profile/package.json | 3 +- .../agent-team-profile/tests/profile.spec.ts | 4 + .../agent-team-web-profile/README.i18n.yaml | 6 -- .../agent-team-web-profile/README.md | 98 ------------------- .../agent-team-web-profile/README.zh.md | 98 ------------------- .../agent-team-web-profile/cordis.patch.yml | 6 -- .../agent-team-web-profile/package.json | 47 --------- .../agent-team-web-profile/src/index.ts | 3 - .../tests/profile.spec.ts | 34 ------- .../agent-team-web-profile/tsconfig.json | 11 --- .../client-ui-agent-team/README.i18n.yaml | 4 +- .../client-ui-agent-team/README.md | 6 +- .../client-ui-agent-team/README.zh.md | 6 +- pnpm-lock.yaml | 22 +---- scripts/release/families.spec.ts | 1 - .../verify-package-readme-model-experience.ts | 1 - tsconfig.base.json | 1 - tsconfig.host.json | 1 - 52 files changed, 130 insertions(+), 434 deletions(-) delete mode 100644 packages/experimental/agent-team-web-profile/README.i18n.yaml delete mode 100644 packages/experimental/agent-team-web-profile/README.md delete mode 100644 packages/experimental/agent-team-web-profile/README.zh.md delete mode 100644 packages/experimental/agent-team-web-profile/cordis.patch.yml delete mode 100644 packages/experimental/agent-team-web-profile/package.json delete mode 100644 packages/experimental/agent-team-web-profile/src/index.ts delete mode 100644 packages/experimental/agent-team-web-profile/tests/profile.spec.ts delete mode 100644 packages/experimental/agent-team-web-profile/tsconfig.json diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml index 0ac7d7109d..fb8587decb 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.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 .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md -2026-08-18-experimental-agent-teams-packages.md: 4a78a60c2ad7463c06b670e6ec664579ffd767b9 -2026-08-18-experimental-agent-teams-packages.zh.md: 8056cd8194695e8ce41acb359e51c040d4ed3aec +2026-08-18-experimental-agent-teams-packages.md: a59e7c209c4c1c1f36260dddedf14b00f66f1e80 +2026-08-18-experimental-agent-teams-packages.zh.md: ab36e8eeae1725f8541373087981a2f917297a1f diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md index 4a78a60c2a..a59e7c209c 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md @@ -12,17 +12,17 @@ Moving the packages into product-role groups would remove their experimental nam ## Decision -`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, `packages/experimental/client-ui-agent-team`, and `packages/experimental/agent-team-web-profile` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [publication denylist decision](../process/2026-09-12-experimental-publication-denylist.md) owns the public default and private exceptions; the [experimental package rules](../../../../packages/experimental/AGENTS.md) own dependency isolation and later promotion. +`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, and `packages/experimental/client-ui-agent-team` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [publication denylist decision](../process/2026-09-12-experimental-publication-denylist.md) owns the public default and private exceptions; the [experimental package rules](../../../../packages/experimental/AGENTS.md) own dependency isolation and later promotion. -The dsh pack and publish set and the local baseline publisher include these five Agent Teams directories and the [Cua Driver providers](2026-09-12-computer-use-provider-registration.md). Workspace constraints require them to omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other. +The dsh pack and publish set and the local baseline publisher include these four Agent Teams directories and the [Cua Driver providers](2026-09-12-computer-use-provider-registration.md). Workspace constraints require them to omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other. The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction. -The published Host-side Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The separate published Web profile applies after `dsh-web-app` and the Host profile; it inserts the Team UI, which mounts the Remote contribution generated by the Team package. Both layers remain opt-in and leave the shipped base, CLI, Web, and Python runtime dependency graphs unchanged. +The published Agent Teams profile bundle depends on the Team service, tools, and UI packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The Web Client loader loads the UI and Remote contribution generated by the Team package; headless runs only Host plugins. The complete bundle ships as one [optional bundle](../process/2026-09-15-shipped-optional-bundles.md), inactive in default compositions. Profile startup resolves selected bundles before computing the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md). The generation retains installation-first precedence, traverses each explicit bundle root completely in profile order, and keeps pnpm-managed profile packages authoritative. Runtime mode enforces the result in memory; retained link and dual modes materialize the same result as shared and profile-owned projections. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution. -Experimental status changes compatibility and support expectations, not publication for these five packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations. +Experimental status changes compatibility and support expectations, not publication for these four packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations. ## Alternatives considered @@ -36,6 +36,6 @@ Experimental status changes compatibility and support expectations, not publicat ## Consequences -Agent Teams publishes as five installable tarballs in the dsh release family without changing package names or enabling Team in a shipped profile. Public availability does not make the packages stable or supported by default, and stable release packages cannot take runtime dependencies on them. +Agent Teams publishes as four installable tarballs in the dsh release family without changing package names or enabling Team in a shipped profile. Public availability does not make the packages stable or supported by default, and stable release packages cannot take runtime dependencies on them. The release family carries the experimental npm names. Promotion still creates path and npm-name churn as specified by the experimental package rules. diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md index 8056cd8194..ab36e8eeae 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md @@ -12,17 +12,17 @@ Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Sess ## 决策 -`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile`、`packages/experimental/client-ui-agent-team` 与 `packages/experimental/agent-team-web-profile` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[发布拒绝列表决策](../process/2026-09-12-experimental-publication-denylist.zh.md)负责默认公开与私有例外;[实验性包规则](../../../../packages/experimental/AGENTS.md)负责依赖隔离与后续 promotion。 +`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile` 与 `packages/experimental/client-ui-agent-team` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[发布拒绝列表决策](../process/2026-09-12-experimental-publication-denylist.zh.md)负责默认公开与私有例外;[实验性包规则](../../../../packages/experimental/AGENTS.md)负责依赖隔离与后续 promotion。 -dsh 打包与发布集合以及本地基线发布器包含这五个 Agent Teams 目录和 [Cua Driver 提供方](2026-09-12-computer-use-provider-registration.zh.md)。workspace 约束要求它们省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。 +dsh 打包与发布集合以及本地基线发布器包含这四个 Agent Teams 目录和 [Cua Driver 提供方](2026-09-12-computer-use-provider-registration.zh.md)。workspace 约束要求它们省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。 通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。 -公开发布的 Host 侧 Agent Teams profile bundle 依赖 Team 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。独立公开发布的 Web profile 在 `dsh-web-app` 与 Host profile 之后应用;它会插入 Team UI,后者挂载 Team package 生成的 Remote contribution。两个层都保持显式启用,不改变随附 base、CLI、Web 与 Python runtime 的依赖图。 +公开发布的 Agent Teams profile bundle 依赖 Team 服务、工具和 UI 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。Web Client loader 加载 UI 及 Team package 生成的 Remote contribution;headless 仅运行 Host 插件。整个组合包作为一个[随附可选组合包](../process/2026-09-15-shipped-optional-bundles.zh.md)交付,默认组合不启用它。 profile 启动会先解析所选 bundle,再计算[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md)。generation 保留安装优先顺序,按 profile 顺序完整遍历每个显式 bundle 根,并让 pnpm 管理的 profile 包保持优先。runtime 模式在内存中强制该结果;保留的 link 与 dual 模式把同一结果物化为共享和 profile 自有投影。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。 -对这五个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。 +对这四个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。 ## 曾考虑的替代方案 @@ -36,6 +36,6 @@ profile 启动会先解析所选 bundle,再计算[不可变 profile resolution ## 后果 -Agent Teams 会作为 dsh 发布系列中的五个可安装 tarball 发布,同时保持包名不变,也不会在随附 profile 中启用 Team。公开可用不代表这些包稳定或默认受支持,稳定发布包也不能对其建立运行时依赖。 +Agent Teams 会作为 dsh 发布系列中的四个可安装 tarball 发布,同时保持包名不变,也不会在随附 profile 中启用 Team。公开可用不代表这些包稳定或默认受支持,稳定发布包也不能对其建立运行时依赖。 发布系列保留实验性 npm 名称。promotion 仍会按照实验性包规则产生路径和 npm 名改动。 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml index 2b32ebfee1..cf7cd8111c 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.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 .agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md -2026-09-15-shipped-optional-bundles.md: 40ef69d3880f8d1c925c1e3c423e59a628f22e09 -2026-09-15-shipped-optional-bundles.zh.md: adf2b979b9d23b7510c077f3633e2d030732a7b4 +2026-09-15-shipped-optional-bundles.md: ec16c333c49551fc53a278c57f2c3d9bf6c2cceb +2026-09-15-shipped-optional-bundles.zh.md: a856c5eb1960c3b033019fab22a6867784659efd diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md index 40ef69d388..ec16c333c4 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md @@ -14,7 +14,7 @@ The launcher names in `OPTIONAL_BUNDLES` (`packages/boot/app-boot/src/profile.ts Default-product isolation keeps its rules with one declared exception: an optional bundle's dependency graph is outside the default product. The static gate skips the `dependencies` edge from `@deepseek-ai/dsh` to a listed bundle and still rejects a runtime import, a shipped composition, a preset, or a default template that names it, an experimental dependency the list does not name, and a listed name that is not a runtime dependency or not a bundle. The workspace-constraints check accepts the same `dependencies` edges and no other runtime section, and the packed-install release check skips them from the installed entry package while requiring each listed bundle to be installed. -Agent Teams ships this way, as `@deepseek-ai/dsh-experimental-agent-team-profile` and `@deepseek-ai/dsh-experimental-agent-team-web-profile`. Auto review is a published experimental package the page's install guide names as its example, not an optional bundle. +Agent Teams ships this way, as one `@deepseek-ai/dsh-experimental-agent-team-profile` bundle containing its service, tools, and browser UI. Auto review is a published experimental package the page's install guide names as its example, not an optional bundle. ## Alternatives considered @@ -26,4 +26,4 @@ Agent Teams ships this way, as `@deepseek-ai/dsh-experimental-agent-team-profile ## Consequences -Optional bundles are downloaded with the product and stay inactive until selected; the runtime isolation smokes still observe no experimental module in a default composition. Switching on an optional Web layer loads its client plugin through the live client module graph. A bundle that needs a companion layer, such as the Agent Teams Web layer over its Host layer, says so in its description; the manager does not select companions automatically. +Optional bundles are downloaded with the product and stay inactive until selected; the runtime isolation smokes still observe no experimental module in a default composition. Switching on an optional Web layer loads its client plugin through the live client module graph. Agent Teams exposes one switch for its complete composition; the manager does not select companion bundles automatically. diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md index adf2b979b9..a856c5eb19 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md @@ -14,7 +14,7 @@ Web 插件页只管理用户装进 profile 的组合包。像 Agent Teams 这样 默认产品隔离的规则保持不变,只声明一个例外:可选组合包的依赖图在默认产品之外。静态门禁跳过从 `@deepseek-ai/dsh` 到列表中组合包的 `dependencies` 边,仍然拒绝运行时 import、随附组合、preset 或默认模板对它的引用,拒绝列表没有点名的实验依赖,也拒绝不是运行时依赖或不是组合包的列表项。workspace 约束检查接受同样的 `dependencies` 边而不接受其他运行时依赖段;发布时的 packed-install 检查对已安装入口包跳过这些边,并要求列表中的每个组合包都已安装。 -Agent Teams 以这种方式交付,即 `@deepseek-ai/dsh-experimental-agent-team-profile` 与 `@deepseek-ai/dsh-experimental-agent-team-web-profile`。Auto review 是一个已发布的实验包,插件页的安装引导拿它作示例,它不是可选组合包。 +Agent Teams 以这种方式交付,由一个 `@deepseek-ai/dsh-experimental-agent-team-profile` 组合包包含服务、工具和浏览器 UI。Auto review 是一个已发布的实验包,插件页的安装引导拿它作示例,它不是可选组合包。 ## 考虑过的替代方案 @@ -26,4 +26,4 @@ Agent Teams 以这种方式交付,即 `@deepseek-ai/dsh-experimental-agent-tea ## 影响 -可选组合包随产品一起下载,选中前保持不活动;运行时隔离 smoke 在默认组合中仍观察不到任何实验模块。开启一个可选 Web 层会通过实时客户端模块图加载它的客户端插件。需要配套层的组合包,例如 Agent Teams Web 层依赖其 Host 层,会在描述里说明;管理器不会自动选中配套层。 +可选组合包随产品一起下载,选中前保持不活动;运行时隔离 smoke 在默认组合中仍观察不到任何实验模块。开启一个可选 Web 层会通过实时客户端模块图加载它的客户端插件。Agent Teams 用一个开关管理完整组合;管理器不会自动选中配套组合包。 diff --git a/apps/cli/package.json b/apps/cli/package.json index 5204f55a29..3c1a15fc98 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -48,7 +48,6 @@ "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^", "@deepseek-ai/dsh-cordis-client-runner": "workspace:^", "@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:^", - "@deepseek-ai/dsh-experimental-agent-team-web-profile": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-goal-round-driver": "workspace:^", diff --git a/apps/web/tests/agent-team-panel.e2e.ts b/apps/web/tests/agent-team-panel.e2e.ts index b3192c3655..ac4e484db0 100644 --- a/apps/web/tests/agent-team-panel.e2e.ts +++ b/apps/web/tests/agent-team-panel.e2e.ts @@ -1,4 +1,4 @@ -// Keyless assembled-browser coverage for the opt-in Agent Teams Web profiles +// Keyless assembled-browser coverage for the opt-in Agent Teams bundle // over the real Host Typert Remote flow. import { fileURLToPath } from 'node:url' import { join } from 'node:path' @@ -18,11 +18,9 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-team-panel', import.meta.url)) const PANEL_EXPECTED = join(SNAPSHOT_DIR, 'task.expected.md') const OVERLAY = fileURLToPath(new URL('./agent-team-panel.overlay.yml', import.meta.url)) -const HOST_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/cordis.patch.yml', import.meta.url)) -const WEB_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-web-profile/cordis.patch.yml', import.meta.url)) +const TEAM_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/cordis.patch.yml', import.meta.url)) const INSTALL_ANCHORS = [ fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/package.json', import.meta.url)), - fileURLToPath(new URL('../../../packages/experimental/agent-team-web-profile/package.json', import.meta.url)), ] const MODE = webSnapshotMode() @@ -33,11 +31,8 @@ function profileEntries(path: string): unknown[] { } describe('Agent Teams panel overlay', () => { - it('matches the shipped Host and Web profile layers', () => { - expect(profileEntries(OVERLAY)).toEqual([ - ...profileEntries(HOST_PATCH), - ...profileEntries(WEB_PATCH), - ]) + it('matches the shipped Agent Teams bundle', () => { + expect(profileEntries(OVERLAY)).toEqual(profileEntries(TEAM_PATCH)) }) }) diff --git a/apps/web/tests/agent-team-panel.overlay.yml b/apps/web/tests/agent-team-panel.overlay.yml index cb6dd2decd..20c49c23a1 100644 --- a/apps/web/tests/agent-team-panel.overlay.yml +++ b/apps/web/tests/agent-team-panel.overlay.yml @@ -1,5 +1,4 @@ -# Equivalent to the two explicit Agent Teams profile layers over the scaffold's -# stable base and Web bundles. +# Equivalent to the Agent Teams bundle over the stable base and Web bundles. - id: tool-subagent-control disabled: true @@ -28,6 +27,5 @@ freshProvider: spawn forkProvider: fork -- insert: - id: ui-agent-team name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/apps/web/tests/expected/plugin-config/official.expected.md b/apps/web/tests/expected/plugin-config/official.expected.md index 7f69fe8231..43778ec668 100644 --- a/apps/web/tests/expected/plugin-config/official.expected.md +++ b/apps/web/tests/expected/plugin-config/official.expected.md @@ -5,16 +5,12 @@ - img - text: 添加插件 - heading "官方" [level=3] -- text: "6" +- text: "5" - list: - listitem: - button "查看 智能体团队": 智能体团队 - - text: Beta 启用智能体团队协作与团队工具。 + - text: Beta 启用团队协作、团队工具及成员与任务看板。 - switch "启用 智能体团队" - - listitem: - - button "查看 智能体团队 Web 界面": 智能体团队 Web 界面 - - text: Beta 在浏览器中查看团队成员、任务看板和成员会话。 - - switch "启用 智能体团队 Web 界面" - listitem: - button "查看 终端": 终端 - text: 限制 agent 运行的每一条命令。 diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index 7302e8cc31..51dcbd91ef 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -5,16 +5,12 @@ - img - text: 添加插件 - heading "官方" [level=3] -- text: "6" +- text: "5" - list: - listitem: - button "查看 智能体团队": 智能体团队 - - text: Beta 启用智能体团队协作与团队工具。 + - text: Beta 启用团队协作、团队工具及成员与任务看板。 - switch "启用 智能体团队" - - listitem: - - button "查看 智能体团队 Web 界面": 智能体团队 Web 界面 - - text: Beta 在浏览器中查看团队成员、任务看板和成员会话。 - - switch "启用 智能体团队 Web 界面" - listitem: - button "查看 终端": 终端 - text: 限制 agent 运行的每一条命令。 diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index 023bd3b8bd..ed8a8aa530 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -5,16 +5,12 @@ - img - text: 添加插件 - heading "官方" [level=3] -- text: "6" +- text: "5" - list: - listitem: - button "查看 智能体团队": 智能体团队 - - text: Beta 启用智能体团队协作与团队工具。 + - text: Beta 启用团队协作、团队工具及成员与任务看板。 - switch "启用 智能体团队" - - listitem: - - button "查看 智能体团队 Web 界面": 智能体团队 Web 界面 - - text: Beta 在浏览器中查看团队成员、任务看板和成员会话。 - - switch "启用 智能体团队 Web 界面" - listitem: - button "查看 终端": 终端 - text: 限制 agent 运行的每一条命令。 diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts index db20eed409..475084a550 100644 --- a/apps/web/tests/plugin-config.e2e.ts +++ b/apps/web/tests/plugin-config.e2e.ts @@ -86,10 +86,10 @@ describe('web e2e: plugin configuration pages', () => { // Every page the shipped web composition exposes: the shell executor, the // agent loop, subagent selection, and the DeepSeek search provider, after - // the two official bundles the installation ships switched off. + // the official Team bundle the installation ships switched off. await panel.getByRole('button', { name: '查看 网页搜索', exact: true }).waitFor({ timeout: 20_000 }) const official = panel.locator('[data-plugin-group="official"]') - expect(await official.locator('[data-plugin-package]').count()).toBe(2) + expect(await official.locator('[data-plugin-package]').count()).toBe(1) expect(await official.locator('[data-plugin-item]').count()).toBe(4) for (const title of ['终端', 'Agent 循环', 'Subagent', '网页搜索']) { expect(await official.getByRole('button', { name: `查看 ${title}`, exact: true }).count()).toBe(1) diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index c8a07c1d79..ebe90edae4 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -6,6 +6,7 @@ import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' +import { FiberState } from '@deepseek-ai/cordis' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { join } from 'node:path' import { @@ -75,9 +76,9 @@ describe('web e2e: plugin manager', () => { // group, followed by the official plugins that registered their configuration, and its other bundles // stay off the page. expect(await panel.locator('[data-plugin-group="bundles"] [data-plugin-package]').count()).toBe(1) - expect(await panel.locator('[data-plugin-group="official"] [data-plugin-package]').count()).toBe(2) + expect(await panel.locator('[data-plugin-group="official"] [data-plugin-package]').count()).toBe(1) expect(await panel.locator('[data-plugin-group="official"] [data-plugin-item]').count()).toBe(4) - expect(await panel.getByText('Beta', { exact: true }).count()).toBe(2) + expect(await panel.getByText('Beta', { exact: true }).count()).toBe(1) // A bundle that is off still shows the rows its patch declares, without switches. await panel.getByRole('button', { name: '查看 bundle' }).click() await panel.locator('[data-plugin-row]', { hasText: 'fixture-row' }).waitFor({ timeout: 10_000 }) @@ -97,7 +98,7 @@ describe('web e2e: plugin manager', () => { await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click() const packageName = panel.locator('[data-plugin-name]') expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-agent-team-profile') - expect(await panel.getByText('启用智能体团队协作与团队工具。').count()).toBe(1) + expect(await panel.getByText('启用团队协作、团队工具及成员与任务看板。').count()).toBe(1) try { await page.getByRole('button', { name: '设置', exact: true }).click() await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '中文' }).click() @@ -106,13 +107,10 @@ describe('web e2e: plugin manager', () => { await page.keyboard.press('Escape') await panel.getByRole('heading', { name: 'Agent Teams', exact: true }).waitFor() expect(await packageName.textContent()).toBe('@deepseek-ai/dsh-experimental-agent-team-profile') - expect(await panel.getByText('Enable agent team collaboration and team tools.').count()).toBe(1) + expect(await panel.getByText('Enable team collaboration, team tools, and the members and task board.').count()).toBe(1) await panel.getByRole('button', { name: 'Back to plugins' }).click() - for (const title of ['Agent Teams', 'Agent Teams Web UI']) { - await panel.getByRole('button', { name: `View ${title}`, exact: true }).waitFor() - expect(await panel.getByRole('switch', { name: `Enable ${title}`, exact: true }).count()).toBe(1) - } - expect(await panel.getByText('View team members, the task board, and teammate sessions in the browser.').count()).toBe(1) + await panel.getByRole('button', { name: 'View Agent Teams', exact: true }).waitFor() + expect(await panel.getByRole('switch', { name: 'Enable Agent Teams', exact: true }).count()).toBe(1) // The official configuration pages follow the language too, from their own dictionary. for (const title of ['Shell', 'Agent loop', 'Subagent', 'Web search']) { await panel.getByRole('button', { name: `View ${title}`, exact: true }).waitFor() @@ -132,6 +130,36 @@ describe('web e2e: plugin manager', () => { expect(tripwire.pageErrors).toEqual([]) }, 60_000) + it('enables the Team tools and browser plugin with one bundle switch', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-team')) + const panel = await openPluginsPanel() + const toggle = panel.getByRole('switch', { name: '启用 智能体团队', exact: true }) + const teamRows = () => [...scaffold.ctx.loader.entries()] + .filter(entry => ['agent-team', 'tool-agent-team', 'ui-agent-team'].includes(entry.options.id)) + await toggle.click() + try { + await expect.poll(() => teamRows().filter(entry => entry.fiber?.state === FiberState.ACTIVE).length, { timeout: 20_000 }).toBe(3) + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + const manifest = JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as { + dsh: { profile: { bundles: string[] } } + } + expect(manifest.dsh.profile.bundles).toEqual([ + '@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-experimental-agent-team-profile', + ]) + await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click() + for (const id of ['agent-team', 'tool-agent-team', 'ui-agent-team']) { + await panel.locator('[data-plugin-row]', { hasText: id }).first().waitFor() + } + await panel.getByRole('button', { name: '返回插件列表' }).click() + } finally { + const back = panel.getByRole('button', { name: '返回插件列表' }) + if (await back.count() > 0) await back.click() + await toggle.click() + await expect.poll(() => teamRows().filter(entry => entry.fiber?.state === FiberState.ACTIVE).length, { timeout: 20_000 }).toBe(0) + } + expect(tripwire.pageErrors).toEqual([]) + }, 60_000) + it('checks a spec before installing it and words what the check refused', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-install')) const panel = await openPluginsPanel() diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 7da9acd525..9abb7e2f19 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.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 docs/config-catalog.md -config-catalog.md: 631ce6a34b120f44534a7935ff976662287fa879 -config-catalog.zh.md: 8d72786a79ceff85e35aff72fa6622af7373a3b4 +config-catalog.md: fd7ddf0b0ea3eef331283531a860a2079d02cf81 +config-catalog.zh.md: 7f58a328ec0650ee3032950d7afadd8a7fdd6a5b diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 631ce6a34b..fd7ddf0b0e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3905,7 +3905,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts)) - `@deepseek-ai/dsh-deque` ([`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts)) - `@deepseek-ai/dsh-experimental-agent-team-profile` ([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts)) -- `@deepseek-ai/dsh-experimental-agent-team-web-profile` ([`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts)) - `@deepseek-ai/dsh-experimental-browser-use-runtime` ([`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts)) - `@deepseek-ai/dsh-experimental-webworker-packer` ([`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts)) - `@deepseek-ai/dsh-experimental-webworker-runtime` ([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 8d72786a79..7f58a328ec 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3906,7 +3906,6 @@ export interface Config { - `@deepseek-ai/dsh-cmdline`([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts)) - `@deepseek-ai/dsh-deque`([`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts)) - `@deepseek-ai/dsh-experimental-agent-team-profile`([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts)) -- `@deepseek-ai/dsh-experimental-agent-team-web-profile`([`packages/experimental/agent-team-web-profile/src/index.ts`](../packages/experimental/agent-team-web-profile/src/index.ts)) - `@deepseek-ai/dsh-experimental-browser-use-runtime` ([`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts)) - `@deepseek-ai/dsh-experimental-webworker-packer`([`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts)) - `@deepseek-ai/dsh-experimental-webworker-runtime`([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index ff54d13bd5..78f2897d67 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.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 docs/module-graph.md -module-graph.md: a886619fa5cba78b2c243c525b8f2a173c1e8038 -module-graph.zh.md: 9c40a086df916cd638599176a95c649d6a2adc0d +module-graph.md: 2e2899c3a74e1d2c5fe50ab137408e7ed3beca82 +module-graph.zh.md: 49291ba55842462e5d532cce1de4f3803610d908 diff --git a/docs/module-graph.md b/docs/module-graph.md index a886619fa5..2e2899c3a7 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -231,7 +231,6 @@ flowchart TD subgraph group_experimental["packages/experimental"] pkg_experimental_agent_team["experimental-agent-team"] pkg_experimental_agent_team_profile["experimental-agent-team-profile"] - pkg_experimental_agent_team_web_profile["experimental-agent-team-web-profile"] pkg_experimental_auto_review["experimental-auto-review"] pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"] pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"] @@ -1382,7 +1381,6 @@ flowchart TD | [`client-web`](../packages/client/web) | `client` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | -| [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | — | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 9c40a086df..49291ba558 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -233,7 +233,6 @@ flowchart TD subgraph group_experimental["packages/experimental"] pkg_experimental_agent_team["experimental-agent-team"] pkg_experimental_agent_team_profile["experimental-agent-team-profile"] - pkg_experimental_agent_team_web_profile["experimental-agent-team-web-profile"] pkg_experimental_auto_review["experimental-auto-review"] pkg_experimental_browser_use_chrome_devtools_mcp["experimental-browser-use-chrome-devtools-mcp"] pkg_experimental_browser_use_playwright_mcp["experimental-browser-use-playwright-mcp"] @@ -1384,7 +1383,6 @@ flowchart TD | [`client-web`](../packages/client/web) | `client` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | -| [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | — | diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index d187f3c10a..d5c0a32ff7 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -166,7 +166,6 @@ export const DEFAULT_PROFILE_BUNDLES: readonly string[] = ['@deepseek-ai/dsh-bas */ export const OPTIONAL_BUNDLES: readonly string[] = [ '@deepseek-ai/dsh-experimental-agent-team-profile', - '@deepseek-ai/dsh-experimental-agent-team-web-profile', ] const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer: diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index e50cdb00de..6fe4900da4 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/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-plugin-manager/README.md -README.md: e7128dd2146cc0553ab36fdef5c31470be7877f2 -README.zh.md: ff7706f1d9747f553bf91f45a76fa5f50472330d +README.md: bd99187b7cc9ad6abfcba22c6f6b9296b486e367 +README.zh.md: 996c6e35d057b7909c764e504cdfe3d19c2f6f72 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index e7128dd214..bd99187b7c 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 Select **Plugins** in the sidebar. 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. **Official** comes first and lists the bundles the installation ships for switching on — off until switched on, without an uninstall, and tagged **Beta** where the feature is one — followed by the official plugins that registered a configuration page; **Installed** lists the bundles the profile holds. Cards are listed by name, so switching a bundle on or off does not move its card. 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 **Plugins** section. -The Agent Teams, Agent Teams Web UI, and Auto Authorization Review packages have localized names and descriptions that follow the UI language. Their detail pages retain the full npm package name; other packages display their short package name and original description. +The Agent Teams and Auto Authorization Review packages have localized names and descriptions that follow the UI language. Their detail pages retain the full npm package name; other packages display their short package name and original description. ### Installing a bundle diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index ff7706f1d9..996c6e35d0 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" 在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**官方**排在前面,列出安装随附、供开启的组合包——开启前保持关闭、没有卸载、属于 beta 功能的带 **Beta** 标签——其后是注册了配置页的官方插件;**已安装**列出 profile 持有的组合包。卡片按名称排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**插件**分区中编辑。 -Agent Teams、Agent Teams Web UI 和 Auto Authorization Review 三个包使用随界面语言切换的本地化名称和描述。详情页保留完整 npm 包名;其他包显示简写包名和原始描述。 +Agent Teams 和 Auto Authorization Review 两个包使用随界面语言切换的本地化名称和描述。详情页保留完整 npm 包名;其他包显示简写包名和原始描述。 ### 安装一个组合包 diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 63071a5cb7..58576ca278 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -17,9 +17,7 @@ export const zh = { bundlesTitle: '已安装', officialTitle: '官方', builtinAgentTeamTitle: '智能体团队', - builtinAgentTeamDescription: '启用智能体团队协作与团队工具。', - builtinAgentTeamWebTitle: '智能体团队 Web 界面', - builtinAgentTeamWebDescription: '在浏览器中查看团队成员、任务看板和成员会话。', + builtinAgentTeamDescription: '启用团队协作、团队工具及成员与任务看板。', builtinAutoReviewTitle: '自动授权审查', builtinAutoReviewDescription: '提供自动审查权限模式,由模型在每次工具调用前判断是否授权。', statusProblem: '异常', @@ -179,9 +177,7 @@ export const en = { bundlesTitle: 'Installed', officialTitle: 'Official', builtinAgentTeamTitle: 'Agent Teams', - builtinAgentTeamDescription: 'Enable agent team collaboration and team tools.', - builtinAgentTeamWebTitle: 'Agent Teams Web UI', - builtinAgentTeamWebDescription: 'View team members, the task board, and teammate sessions in the browser.', + builtinAgentTeamDescription: 'Enable team collaboration, team tools, and the members and task board.', builtinAutoReviewTitle: 'Auto Authorization Review', builtinAutoReviewDescription: 'Add an Auto review permission mode that uses the model to assess authorization before each tool call.', statusProblem: 'Problem', diff --git a/packages/client/ui-plugin-manager/src/client/presentation.ts b/packages/client/ui-plugin-manager/src/client/presentation.ts index b2a50a5d72..5641e71138 100644 --- a/packages/client/ui-plugin-manager/src/client/presentation.ts +++ b/packages/client/ui-plugin-manager/src/client/presentation.ts @@ -13,9 +13,6 @@ const BUILTIN_COPY = new Map { it.each([ ['agent-team-profile', 'builtinAgentTeamTitle', 'builtinAgentTeamDescription'], - ['agent-team-web-profile', 'builtinAgentTeamWebTitle', 'builtinAgentTeamWebDescription'], ['auto-review', 'builtinAutoReviewTitle', 'builtinAutoReviewDescription'], ] as const)('localizes %s across cards, details, switches, and uninstall confirmation', (suffix, titleKey, descriptionKey) => { const name = `@deepseek-ai/dsh-experimental-${suffix}` diff --git a/packages/experimental/README.i18n.yaml b/packages/experimental/README.i18n.yaml index fba483caa6..af98f96967 100644 --- a/packages/experimental/README.i18n.yaml +++ b/packages/experimental/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/experimental/README.md -README.md: 6bb562cf21703fb153c04d9fc839610563bba6cf -README.zh.md: c586a3a89611bfc78587c2b9a48245d79e67515f +README.md: f9fa7be744ff84993373b0cb15451e1a5d5641a9 +README.zh.md: 58e7d1614f8ab200e0dad8ebb53122b371b5430d diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 6bb562cf21..f9fa7be744 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -24,9 +24,8 @@ The experimental group contains prototype capabilities whose contracts can chang | Package | Role | ctx key | |---|---|---| -| [`agent-team-profile`](agent-team-profile/README.md) | Published opt-in profile layer for Agent Teams | — | +| [`agent-team-profile`](agent-team-profile/README.md) | Agent Teams collaboration, tools, and Web UI bundle | — | | [`agent-team`](agent-team/README.md) | Named teammates with durable messages and a shared task board | `ctx.agentTeams` | -| [`agent-team-web-profile`](agent-team-web-profile/README.md) | Published opt-in Web layer for Agent Teams | — | | [`client-ui-agent-team`](client-ui-agent-team/README.md) | Team roster, task board, and teammate navigation for Web | — | | [`auto-review`](auto-review/README.md) | Explicit Web layer for same-model review before each native or PTC inner tool call | — | | [`ptc-runtime-python`](ptc-runtime-python/README.md) | CPython subprocess backend for the PTC execution seam | `ctx.ptcRuntime` | diff --git a/packages/experimental/README.zh.md b/packages/experimental/README.zh.md index c586a3a896..58e7d1614f 100644 --- a/packages/experimental/README.zh.md +++ b/packages/experimental/README.zh.md @@ -24,9 +24,8 @@ kind: "package-group" | 包 | 职责 | ctx 键 | |---|---|---| -| [`agent-team-profile`](agent-team-profile/README.zh.md) | Agent Teams 的公开 opt-in profile 层 | — | +| [`agent-team-profile`](agent-team-profile/README.zh.md) | Agent Teams 协作、工具与 Web UI 组合包 | — | | [`agent-team`](agent-team/README.zh.md) | 具名 teammate,成员之间持久消息与共享任务板 | `ctx.agentTeams` | -| [`agent-team-web-profile`](agent-team-web-profile/README.zh.md) | Agent Teams 的公开 opt-in Web 层 | — | | [`client-ui-agent-team`](client-ui-agent-team/README.zh.md) | Web Team roster、任务板与 teammate 导航 | — | | [`auto-review`](auto-review/README.zh.md) | 显式 Web 层,在每个原生或 PTC inner 工具调用前使用同一模型审查 | — | | [`ptc-runtime-python`](ptc-runtime-python/README.zh.md) | PTC 执行 seam 的 CPython 子进程后端 | `ctx.ptcRuntime` | diff --git a/packages/experimental/agent-team-profile/README.i18n.yaml b/packages/experimental/agent-team-profile/README.i18n.yaml index 362d87d9f8..92c5c140a4 100644 --- a/packages/experimental/agent-team-profile/README.i18n.yaml +++ b/packages/experimental/agent-team-profile/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/experimental/agent-team-profile/README.md -README.md: 9aa294991d76d63a596c1d9afed2f9991f424f20 -README.zh.md: 98db275bbf583b02d1f370008345fa68882dbba1 +README.md: a966d8b0075dba352f7988f6ac19a2bf9d04f0b1 +README.zh.md: 7a5a2a3c2ece254ee5e130c143139d57785c4e00 diff --git a/packages/experimental/agent-team-profile/README.md b/packages/experimental/agent-team-profile/README.md index 9aa294991d..a966d8b007 100644 --- a/packages/experimental/agent-team-profile/README.md +++ b/packages/experimental/agent-team-profile/README.md @@ -1,5 +1,5 @@ --- -description: "Published experimental Agent Teams profile layer over dsh-base with teammate delegation and fresh workflow children." +description: "Enable team collaboration, tools, and the Web roster and task board with one experimental bundle." kind: "package-bundle" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-experimental-agent-team-profile` is a published experimental profile layer that enables [Agent Teams](../agent-team/README.md) over `@deepseek-ai/dsh-base`. Its patch inserts the Team domain and Team-scoped tools and disables ordinary subagent delegation and the overlapping global continuable-child controls. Workflow remains available with fresh children. The dsh installation ships it as an optional bundle that no shipped profile enables; switch it on from the Web sidebar's Plugins page, or add it explicitly to an initialized profile. +`dsh-experimental-agent-team-profile` enables [Agent Teams](../agent-team/README.md) collaboration, tools, and Web UI with one bundle. Delegate work to teammates and view members, shared tasks, and teammate sessions in Web. Ordinary subagent delegation and overlapping global child controls are disabled; Workflow can still create fresh children. The bundle ships with dsh switched off. Enable it from the Plugins page or add it to an initialized profile. ## Table of Contents @@ -36,6 +36,14 @@ dsh --profile headless "Use Agent Teams to split this task between two teammates The profile must already contain `@deepseek-ai/dsh-base`, whose Subagent services and provider rows this layer consumes. Removing the package with `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` removes the bundle from the profile's ordered layer list. +Enable Agent Teams on the Web or Desktop Plugins page to activate both tools and UI. For a CLI Web profile, use: + +```sh +dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile +``` + +In an existing profile’s `package.json`, keep `@deepseek-ai/dsh-experimental-agent-team-profile` in `dsh.profile.bundles` and remove the separate `@deepseek-ai/dsh-experimental-agent-team-web-profile` entry. User patches targeting `ui-agent-team` still apply. + ### What you get The layer adds the Agent Teams domain and its scoped creation, roster, messaging, interruption, waiting, and task-board tools. Direct delegation uses `spawn_teammate`, which supports fresh and fork context. The `subagent` and `subagent_fork` tools and overlapping global child controls are disabled. Workflow retains the base profile’s `spawn` provider, while the underlying Subagent services and both providers remain available to teammates and workflow. @@ -48,7 +56,7 @@ The layer adds the Agent Teams domain and its scoped creation, roster, messaging
Implementation internals — click to expand -The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch disables `tool-subagent-control`, `tool-subagent-list-agents`, `tool-subagent`, and `tool-subagent-fork`, and inserts the Team service and tool rows with explicit providers and limits. +The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch disables `tool-subagent-control`, `tool-subagent-list-agents`, `tool-subagent`, and `tool-subagent-fork`, and inserts the Team service, tools, and UI with explicit providers and limits. The UI plugin has an inert Host entry; only the Web Client loader mounts its browser entry, so headless needs no Web server. | File | Role | |---|---| @@ -66,6 +74,7 @@ The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied - [Experimental packages](../README.md) — incubation status and publication policy. - [Agent Teams service](../agent-team/README.md) — durable roster, messaging, and task-board behavior. - [Agent Teams tools](../tool-agent-team/README.md) — the Team-scoped model tool surface. +- [Agent Teams browser UI](../client-ui-agent-team/README.md) — roster, task board, and teammate session navigation. - [Base bundle](../../bundle/base/README.md) — the profile layer this patch extends. ----- @@ -94,6 +103,7 @@ The bundle's composition is prefix-stable while its patch, Team identity, and co - **Opt-in only** — the package ships with the installation switched off; no shipped CLI, Web, SDK, ACP, or Python profile enables it. - **Workflow child tools** — the [Team tool visibility limitation](../tool-agent-team/README.md#known-limitations-and-deferred-work) also applies to workflow children. - **Shared checkout** — every teammate observes the same working directory; this bundle adds no worktree isolation or filesystem locking. +- **Preset-scoped child controls** — Web presets can still mount continuable Subagent controls in their own scope; this top-level bundle does not replace those registrations. - **Base profile required** — the patch depends on row ids and Subagent providers supplied by `dsh-base`; it is not a standalone profile. diff --git a/packages/experimental/agent-team-profile/README.zh.md b/packages/experimental/agent-team-profile/README.zh.md index 98db275bbf..7a5a2a3c2e 100644 --- a/packages/experimental/agent-team-profile/README.zh.md +++ b/packages/experimental/agent-team-profile/README.zh.md @@ -1,5 +1,5 @@ --- -description: "叠加在 dsh-base 上公开发布的实验性 Agent Teams profile 层,提供 teammate 委派与 fresh workflow 子代理。" +description: "在一个实验性组合包中启用团队协作、工具与 Web 成员和任务看板。" kind: "package-bundle" --- @@ -9,7 +9,7 @@ kind: "package-bundle" ## 概述 -`dsh-experimental-agent-team-profile` 是在 `@deepseek-ai/dsh-base` 之上启用 [Agent Teams](../agent-team/README.zh.md) 的公开实验性 profile 层。它的 patch 会插入 Team domain 与 Team-scoped 工具,并禁用普通 subagent 委派和名称重叠的全局 continuable-child control。Workflow 仍可创建 fresh 子代理。dsh 安装随附本包作为可选组合包,随附 profile 都不会启用它;可在 Web 侧栏的插件页开启,或显式添加到已初始化的 profile。 +`dsh-experimental-agent-team-profile` 让 [Agent Teams](../agent-team/README.zh.md) 的团队协作、工具和 Web 界面通过一个组合包启用。开启后可委派 teammate,并在 Web 中查看成员、任务看板和成员会话。普通 subagent 委派及名称重叠的全局 child control 会被禁用;Workflow 仍可创建 fresh 子代理。本包随 dsh 安装提供,默认关闭,可在插件页开启或添加到已初始化的 profile。 ## 目录 @@ -36,6 +36,14 @@ dsh --profile headless "Use Agent Teams to split this task between two teammates profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Subagent 服务与提供方配置行。执行 `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` 移除本包时,bundle 也会从 profile 的有序层列表中移除。 +在 Web 或 Desktop 的插件页开启「智能体团队」,即可同时启用工具与界面。CLI 的 Web profile 也可使用以下命令: + +```sh +dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile +``` + +已有 profile 的 `package.json` 中,`dsh.profile.bundles` 应只保留 `@deepseek-ai/dsh-experimental-agent-team-profile`,删除独立的 `@deepseek-ai/dsh-experimental-agent-team-web-profile` 条目。用户 patch 中的 `ui-agent-team` 配置仍然有效。 + ### 获得的功能 本层会添加 Agent Teams domain,以及 Team-scoped 创建、roster、消息、interrupt、等待与任务板工具。直接委派使用支持 fresh 和 fork 上下文的 `spawn_teammate`。`subagent`、`subagent_fork` 工具和名称重叠的全局 child control 均被禁用。Workflow 保留 base profile 的 `spawn` 提供方,底层 Subagent 服务和两个提供方仍供 teammate 与 workflow 使用。 @@ -48,7 +56,7 @@ profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Su
实现细节——点击展开 -本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 会禁用 `tool-subagent-control`、`tool-subagent-list-agents`、`tool-subagent` 和 `tool-subagent-fork`,并以显式 provider 和限制插入 Team 服务与工具行。 +本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 会禁用 `tool-subagent-control`、`tool-subagent-list-agents`、`tool-subagent` 和 `tool-subagent-fork`,并以显式 provider 和限制插入 Team 服务、工具和 UI 行。UI 插件的 Host 入口不执行逻辑;只有 Web 客户端加载器会挂载其浏览器入口,因此 headless 无需启动 Web 服务。 | 文件 | 职责 | |---|---| @@ -66,6 +74,7 @@ profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Su - [实验性包](../README.zh.md)——孵化状态与发布规则。 - [Agent Teams service](../agent-team/README.zh.md)——持久 roster、消息与任务板行为。 - [Agent Teams 工具](../tool-agent-team/README.zh.md)——Team-scoped 模型工具表层。 +- [Agent Teams 浏览器 UI](../client-ui-agent-team/README.zh.md)——成员列表、任务看板与成员会话导航。 - [Base bundle](../../bundle/base/README.zh.md)——本 patch 扩展的 profile 层。 ----- @@ -94,6 +103,7 @@ Team 策略与 schema 由 [`@deepseek-ai/dsh-experimental-tool-agent-team`](../t - **仅显式启用**——本包随安装提供但默认关闭;随附 CLI、Web、SDK、ACP 与 Python profile 都不会启用它。 - **Workflow 子代理工具**——[Team 工具可见性限制](../tool-agent-team/README.zh.md#known-limitations-and-deferred-work)也适用于 workflow 子代理。 - **共享 checkout**——所有 teammate 都观察同一个工作目录;本 bundle 不提供 worktree 隔离或文件系统锁。 +- **预设内的子代理控件**——Web 预设仍可在预设作用域挂载 continuable Subagent 控件;顶层组合包不会替换这些注册。 - **需要 base profile**——本 patch 依赖 `dsh-base` 提供的配置行 id 与 Subagent 提供方;它不是独立 profile。 diff --git a/packages/experimental/agent-team-profile/cordis.patch.yml b/packages/experimental/agent-team-profile/cordis.patch.yml index f1eee0e9ba..5543297c91 100644 --- a/packages/experimental/agent-team-profile/cordis.patch.yml +++ b/packages/experimental/agent-team-profile/cordis.patch.yml @@ -28,3 +28,6 @@ config: freshProvider: spawn forkProvider: fork + + - id: ui-agent-team + name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index 400b82db62..0f110c985b 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", - "description": "Experimental profile bundle enabling Agent Teams over dsh-base", + "description": "Agent Teams collaboration, tools, and Web UI in one experimental bundle", "version": "0.1.6-alpha.1", "publishConfig": { "access": "public" @@ -35,6 +35,7 @@ }, "dependencies": { "@deepseek-ai/dsh-experimental-agent-team": "workspace:^", + "@deepseek-ai/dsh-experimental-client-ui-agent-team": "workspace:^", "@deepseek-ai/dsh-experimental-tool-agent-team": "workspace:^" }, "peerDependencies": { diff --git a/packages/experimental/agent-team-profile/tests/profile.spec.ts b/packages/experimental/agent-team-profile/tests/profile.spec.ts index fdebc49182..dd65a021ec 100644 --- a/packages/experimental/agent-team-profile/tests/profile.spec.ts +++ b/packages/experimental/agent-team-profile/tests/profile.spec.ts @@ -21,6 +21,7 @@ describe('Agent Teams profile bundle', () => { expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') expect(manifest.dependencies).toMatchObject({ '@deepseek-ai/dsh-experimental-agent-team': 'workspace:^', + '@deepseek-ai/dsh-experimental-client-ui-agent-team': 'workspace:^', '@deepseek-ai/dsh-experimental-tool-agent-team': 'workspace:^', }) @@ -48,5 +49,8 @@ describe('Agent Teams profile bundle', () => { name: '@deepseek-ai/dsh-experimental-tool-agent-team', config: { freshProvider: 'spawn', forkProvider: 'fork' }, }) + expect(inserted.find(entry => entry.id === 'ui-agent-team')).toMatchObject({ + name: '@deepseek-ai/dsh-experimental-client-ui-agent-team', + }) }) }) diff --git a/packages/experimental/agent-team-web-profile/README.i18n.yaml b/packages/experimental/agent-team-web-profile/README.i18n.yaml deleted file mode 100644 index 9febb49882..0000000000 --- a/packages/experimental/agent-team-web-profile/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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/experimental/agent-team-web-profile/README.md -README.md: e21a7eeebc83a97d3e4630c95a9aeaa4213bc227 -README.zh.md: 9966697cb7225083a6fff515675b9dd9ecc51c82 diff --git a/packages/experimental/agent-team-web-profile/README.md b/packages/experimental/agent-team-web-profile/README.md deleted file mode 100644 index e21a7eeebc..0000000000 --- a/packages/experimental/agent-team-web-profile/README.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -description: "Add the published experimental Agent Teams panel to a Web profile after the Host Team layer." -kind: "package-bundle" ---- - -# @deepseek-ai/dsh-experimental-agent-team-web-profile - -English | [中文](README.zh.md) - -## Summary - -`dsh-experimental-agent-team-web-profile` is the published experimental Web layer for [Agent Teams](../agent-team/README.md). Add it after `@deepseek-ai/dsh-web-app` and [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.md) to show the Team roster, task board, and teammate navigation in the browser. Removing either experimental layer leaves the stable base and Web composition unchanged. The dsh installation ships it as an optional bundle that no shipped Web profile enables; switch it on from the Web sidebar's Plugins page after the Host layer. - -## Table of Contents - -- [Use this package](#use-this-package) -- [Understand the implementation](#understand-the-implementation) -- [Further Exploration](#further-exploration) -- [Model Experience](#model-experience) -- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) -- [Dev Note](#dev-note) - ------ - - -## Use this package - -### Install into a profile - -Add the Host and Web Agent Teams layers to an initialized `web` profile in this order: - -```sh -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-web-profile -``` - -The first command supplies the Team domain, generated Remote methods, and model tools. The second command activates this package's declared patch and its browser presentation. Removing the package with `dsh plugin --profile web remove @deepseek-ai/dsh-experimental-agent-team-web-profile` removes the Web layer from the profile's ordered bundle list. - -### What you get - -The conversation header gains the Team roster, shared task board, and teammate navigation. [`@deepseek-ai/dsh-experimental-client-ui-agent-team`](../client-ui-agent-team/README.md) owns those browser interactions and mounts the generated Client Remote namespace used to reach the Host Team service. - ------ - - -## Understand the implementation - -
-Implementation internals — click to expand - -The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-web-app` and the Host Agent Teams layer, its single `insert` entry adds the `ui-agent-team` row for `@deepseek-ai/dsh-experimental-client-ui-agent-team`. The inserted Client plugin owns the generated Remote assembly and Team UI; this static bundle holds no mutable state and installs no runtime invariant. - -| File | Role | -|---|---| -| [`cordis.patch.yml`](cordis.patch.yml) | Ordered Web patch containing the `ui-agent-team` row | -| [`src/index.ts`](src/index.ts) | Empty module entry; the patch is the runtime content | -| — | No runtime invariant companion is published; the package carries only a static profile patch. The Remote assembly and Team UI own their activation requirements. | - -
- ------ - - -## Further Exploration - -- [Experimental packages](../README.md) — incubation status and publication policy. -- [Agent Teams Host profile](../agent-team-profile/README.md) — the required domain, Remote, and model-tool layer. -- [Agent Teams browser UI](../client-ui-agent-team/README.md) — roster, task-board, and teammate-navigation behavior. -- [Web bundle](../../bundle/web-app/README.md) — the stable browser layer this patch extends. - ------ - - -## Model Experience - -Indirectly, through the Host-side Agent Teams profile selected alongside this Web layer. - -#### KV Cache effect - -This Web bundle adds no model request content; the Host-side Team tools own prompt, schema, and cache effects. - -## Known Limitations and Deferred Work - - - -- **Ordered composition** — `dsh-base`, `dsh-web-app`, `dsh-experimental-agent-team-profile`, and this package must remain in that order. -- **Preset-scoped legacy controls** — stable Web presets still mount continuable Subagent controls inside the preset scope. Top-level Host profile overrides do not replace those scoped registrations, so the Team roster and legacy child controls can both appear until Web has a Team-aware preset. The [Web Agent Teams decision](../../../.agents/notes/archived/feature/2026-08-06-agent-teams-web.md) records this deferred composition work. -- **Opt-in only** — the package ships with the installation switched off; no shipped Web profile enables either Agent Teams layer. - - -### Dev Note - -
-Working context for maintainers — click to expand - -None. - -
diff --git a/packages/experimental/agent-team-web-profile/README.zh.md b/packages/experimental/agent-team-web-profile/README.zh.md deleted file mode 100644 index 9966697cb7..0000000000 --- a/packages/experimental/agent-team-web-profile/README.zh.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -description: "在 Host Team 层之后,为 Web profile 添加公开发布的实验性 Agent Teams 面板。" -kind: "package-bundle" ---- - -# @deepseek-ai/dsh-experimental-agent-team-web-profile - -[English](README.md) | 中文 - -## 概述 - -`dsh-experimental-agent-team-web-profile` 是 [Agent Teams](../agent-team/README.zh.md) 公开发布的实验性 Web 层。把它放在 `@deepseek-ai/dsh-web-app` 与 [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.zh.md) 之后,即可在浏览器中显示 Team roster、任务板与 teammate 导航。移除任一实验层都会让稳定的 base 与 Web composition 保持不变。dsh 安装随附本包作为可选组合包,随附 Web profile 不会启用它;在开启 Host 层后可在 Web 侧栏的插件页开启。 - -## 目录 - -- [使用本包](#use-this-package) -- [理解实现](#understand-the-implementation) -- [进一步探索](#further-exploration) -- [模型体验](#model-experience) -- [已知限制与延期工作](#known-limitations-and-deferred-work) -- [开发备注](#dev-note) - ------ - - -## 使用本包 - -### 安装到 profile - -按以下顺序把 Host 与 Web Agent Teams 层添加到已初始化的 `web` profile: - -```sh -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-web-profile -``` - -第一条命令提供 Team domain、生成的 Remote 方法与模型工具。第二条命令激活本包声明的 patch 及其浏览器 presentation。执行 `dsh plugin --profile web remove @deepseek-ai/dsh-experimental-agent-team-web-profile` 移除本包时,Web 层也会从 profile 的有序 bundle 列表中移除。 - -### 获得的功能 - -对话标题栏会获得 Team roster、共享任务板与 teammate 导航。[`@deepseek-ai/dsh-experimental-client-ui-agent-team`](../client-ui-agent-team/README.zh.md) 负责这些浏览器交互,并挂载用于访问 Host Team service 的生成 Client Remote namespace。 - ------ - - -## 理解实现 - -
-实现细节——点击展开 - -本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-web-app` 与 Host Agent Teams 层之后应用时,它唯一的 `insert` 条目会为 `@deepseek-ai/dsh-experimental-client-ui-agent-team` 添加 `ui-agent-team` 行。插入的 Client 插件负责生成的 Remote assembly 与 Team UI;这个静态 bundle 不持有可变状态,也不安装运行时不变式。 - -| 文件 | 职责 | -|---|---| -| [`cordis.patch.yml`](cordis.patch.yml) | 包含 `ui-agent-team` 行的有序 Web patch | -| [`src/index.ts`](src/index.ts) | 空模块入口;patch 是运行时内容 | -| — | 不发布运行时不变式伴生入口;本包只携带静态 profile patch,Remote assembly 与 Team UI 负责各自的激活要求。 | - -
- ------ - - -## 进一步探索 - -- [实验性包](../README.zh.md)——孵化状态与发布规则。 -- [Agent Teams Host profile](../agent-team-profile/README.zh.md)——所需的 domain、Remote 与模型工具层。 -- [Agent Teams 浏览器 UI](../client-ui-agent-team/README.zh.md)——roster、任务板与 teammate 导航行为。 -- [Web bundle](../../bundle/web-app/README.zh.md)——本 patch 扩展的稳定浏览器层。 - ------ - - -## 模型体验 - -通过与本 Web 层同时选择的 Host-side Agent Teams profile 间接产生影响。 - -#### KV Cache 影响 - -本 Web bundle 不添加任何模型请求内容;Host-side Team 工具负责提示词、schema 与缓存影响。 - -## 已知限制与延期工作 - - - -- **有序组合**——`dsh-base`、`dsh-web-app`、`dsh-experimental-agent-team-profile` 与本包必须保持这个顺序。 -- **Preset-scoped 旧控制项**——稳定 Web preset 仍会在 preset scope 内挂载 continuable Subagent 控制项。顶层 Host profile override 不会替换这些 scoped registration,因此在 Web 获得 Team-aware preset 前,Team roster 与旧 child 控制项可能同时出现。[Web Agent Teams 决策](../../../.agents/notes/archived/feature/2026-08-06-agent-teams-web.md)记录了这项暂缓的 composition 工作。 -- **仅显式启用**——本包随安装提供但默认关闭;随附 Web profile 不会启用任何 Agent Teams 层。 - - -### 开发备注 - -
-维护者的工作上下文——点击展开 - -无。 - -
diff --git a/packages/experimental/agent-team-web-profile/cordis.patch.yml b/packages/experimental/agent-team-web-profile/cordis.patch.yml deleted file mode 100644 index e9cffef438..0000000000 --- a/packages/experimental/agent-team-web-profile/cordis.patch.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Experimental Agent Teams Web layer. Apply after dsh-web-app and the host-side -# dsh-agent-team-profile so the browser mounts only when the Team service is present. - -- insert: - - id: ui-agent-team - name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/packages/experimental/agent-team-web-profile/package.json b/packages/experimental/agent-team-web-profile/package.json deleted file mode 100644 index ad91bced8d..0000000000 --- a/packages/experimental/agent-team-web-profile/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-experimental-agent-team-web-profile", - "description": "Experimental Web profile layer for Agent Teams Remote and UI plugins", - "version": "0.1.6-alpha.1", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", - "directory": "packages/experimental/agent-team-web-profile" - }, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./cordis.patch.yml": "./cordis.patch.yml", - "./src/*": "./src/*", - "./package.json": "./package.json" - }, - "files": [ - "lib/index.js", - "cordis.patch.yml", - "lib/types/**/*.d.ts" - ], - "license": "MIT", - "dsh": { - "bundle": { - "patch": "./cordis.patch.yml" - } - }, - "dependencies": { - "@deepseek-ai/dsh-experimental-client-ui-agent-team": "workspace:^" - }, - "peerDependencies": { - "@deepseek-ai/cordis": "workspace:^" - }, - "devDependencies": { - "@deepseek-ai/cordis-plugin-include": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", - "js-yaml": "^4.2.0" - } -} diff --git a/packages/experimental/agent-team-web-profile/src/index.ts b/packages/experimental/agent-team-web-profile/src/index.ts deleted file mode 100644 index 726f066041..0000000000 --- a/packages/experimental/agent-team-web-profile/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** Experimental Web profile layer for the Agent Teams Client plugins. */ - -export {} diff --git a/packages/experimental/agent-team-web-profile/tests/profile.spec.ts b/packages/experimental/agent-team-web-profile/tests/profile.spec.ts deleted file mode 100644 index 21fde2cab5..0000000000 --- a/packages/experimental/agent-team-web-profile/tests/profile.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** The experimental Web bundle must carry one parseable Team Client layer. */ - -import { readFileSync } from 'node:fs' -import { fileURLToPath } from 'node:url' -import { resolve } from 'node:path' -import { describe, expect, it } from 'vitest' -import * as yaml from 'js-yaml' -import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' - -describe('Agent Teams Web profile bundle', () => { - it('declares a public parseable layer containing the Team UI', () => { - const root = fileURLToPath(new URL('..', import.meta.url)) - const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { - private?: boolean - publishConfig?: { access?: string } - dependencies?: Record - dsh?: { bundle?: { patch?: string } } - } - expect(manifest.private).toBeUndefined() - expect(manifest.publishConfig?.access).toBe('public') - expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') - expect(manifest.dependencies).toEqual({ - '@deepseek-ai/dsh-experimental-client-ui-agent-team': 'workspace:^', - }) - - const parsed = yaml.load( - readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), - { schema: entryListSchema }, - ) as { insert?: { id?: string; name?: string }[] }[] - expect(parsed.flatMap(patch => patch.insert ?? [])).toEqual([ - { id: 'ui-agent-team', name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' }, - ]) - }) -}) diff --git a/packages/experimental/agent-team-web-profile/tsconfig.json b/packages/experimental/agent-team-web-profile/tsconfig.json deleted file mode 100644 index d4186340bf..0000000000 --- a/packages/experimental/agent-team-web-profile/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": ["src"], - "references": [ - { "path": "../../../vendor/cordis" } - ] -} diff --git a/packages/experimental/client-ui-agent-team/README.i18n.yaml b/packages/experimental/client-ui-agent-team/README.i18n.yaml index d4a0ae340d..cd2160bf66 100644 --- a/packages/experimental/client-ui-agent-team/README.i18n.yaml +++ b/packages/experimental/client-ui-agent-team/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/experimental/client-ui-agent-team/README.md -README.md: ab40e2de09406991c55160005f2f16cf6931e195 -README.zh.md: 2913ff65930f260d53a0bee3b3ed6cb9634fcd5a +README.md: 01e3c0e94ba0258f042ed405cd4e7e9be0dc91da +README.zh.md: 04adada51e3b1c106e27e80d2b1f40a7d15c2a26 diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index ab40e2de09..01e3c0e94b 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package adds an Agent Teams action to the Web conversation header, where a user can inspect the current roster, manage the shared task board, and navigate into a teammate's conversation. It reads authoritative Team state through the generated `ctx.remote.agentTeams` contribution and keeps ordinary child-history navigation on the stable addressed-subagent path. Choose it through the published experimental Agent Teams Web profile. The browser projection does not extend the stable API Proxy, store Team state, or register model-facing input. +This package adds an Agent Teams action to the Web conversation header, where a user can inspect the current roster, manage the shared task board, and navigate into a teammate's conversation. It reads authoritative Team state through the generated `ctx.remote.agentTeams` contribution and keeps ordinary child-history navigation on the stable addressed-subagent path. Choose it through the published experimental Agent Teams bundle. The browser projection does not extend the stable API Proxy, store Team state, or register model-facing input. ## Table of Contents @@ -25,7 +25,7 @@ This package adds an Agent Teams action to the Web conversation header, where a ## Use this package -Install the package through [`@deepseek-ai/dsh-experimental-agent-team-web-profile`](../agent-team-web-profile/README.md) after the stable Web bundle and the Host-side Agent Teams profile. The Web Client loader mounts the `/client` export; the root Host export is inert, and the package has no user configuration fields. +Enable this package through [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.md), which supplies the Team service, tools, and Web UI together. The Web Client loader mounts the `/client` export; the root Host export is inert, and the package has no user configuration fields. ### Inspect and navigate the roster @@ -61,7 +61,7 @@ Starting a create or update invalidates older refreshes. Success reloads the com ## Further Exploration -- [Agent Teams Web profile](../agent-team-web-profile/README.md) — the published opt-in bundle that mounts this Client plugin. +- [Agent Teams bundle](../agent-team-profile/README.md) — the published opt-in bundle that mounts this Client plugin. - [Agent Teams service](../agent-team/README.md) — authoritative roster, task, and Remote behavior. - [Conversation UI](../../client/ui-conversation/README.md) — the stable header slot and addressed-subagent navigation surface. - [Experimental packages](../README.md) — incubation status and publication policy. diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index 2913ff6593..04adada51e 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包向 Web 会话页头添加 Agent Teams action,让用户检查当前 roster、管理共享任务板并导航到 teammate 会话。它通过生成的 `ctx.remote.agentTeams` contribution 读取权威 Team 状态,并让普通 child history 导航继续使用稳定的 addressed-subagent 路径。通过公开发布的实验性 Agent Teams Web profile 选择本包。这个浏览器 projection 不扩展稳定 API Proxy、不存储 Team 状态,也不注册面向模型的输入。 +本包向 Web 会话页头添加 Agent Teams action,让用户检查当前 roster、管理共享任务板并导航到 teammate 会话。它通过生成的 `ctx.remote.agentTeams` contribution 读取权威 Team 状态,并让普通 child history 导航继续使用稳定的 addressed-subagent 路径。通过公开发布的实验性 Agent Teams bundle 选择本包。这个浏览器 projection 不扩展稳定 API Proxy、不存储 Team 状态,也不注册面向模型的输入。 ## 目录 @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -在稳定 Web bundle 与 Host-side Agent Teams profile 之后,通过 [`@deepseek-ai/dsh-experimental-agent-team-web-profile`](../agent-team-web-profile/README.zh.md) 安装本包。Web Client loader 挂载 `/client` export;root Host export 不执行行为,本包也没有用户配置字段。 +通过 [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.zh.md) 启用本包。这个组合包同时提供团队服务、工具与 Web 界面。Web Client loader 挂载 `/client` export;root Host export 不执行行为,本包也没有用户配置字段。 ### 检查并导航 roster @@ -61,7 +61,7 @@ Client export 挂载来自 [`@deepseek-ai/dsh-experimental-agent-team/remote`](. ## 进一步探索 -- [Agent Teams Web profile](../agent-team-web-profile/README.zh.md)——挂载本 Client plugin 的公开 opt-in bundle。 +- [Agent Teams bundle](../agent-team-profile/README.zh.md)——挂载本 Client plugin 的公开 opt-in bundle。 - [Agent Teams service](../agent-team/README.zh.md)——权威 roster、task 与 Remote 行为。 - [会话 UI](../../client/ui-conversation/README.zh.md)——稳定 header slot 与 addressed-subagent 导航表层。 - [实验性包](../README.zh.md)——孵化状态与发布规则。 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13ab4ef874..530592e245 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,9 +219,6 @@ importers: '@deepseek-ai/dsh-experimental-agent-team-profile': specifier: workspace:^ version: link:../../packages/experimental/agent-team-profile - '@deepseek-ai/dsh-experimental-agent-team-web-profile': - specifier: workspace:^ - version: link:../../packages/experimental/agent-team-web-profile '@deepseek-ai/dsh-fs-local': specifier: workspace:^ version: link:../../packages/fs/fs-local @@ -6202,25 +6199,12 @@ importers: '@deepseek-ai/dsh-experimental-agent-team': specifier: workspace:^ version: link:../agent-team - '@deepseek-ai/dsh-experimental-tool-agent-team': - specifier: workspace:^ - version: link:../tool-agent-team - devDependencies: - '@deepseek-ai/cordis': - specifier: workspace:^ - version: link:../../../vendor/cordis - '@deepseek-ai/cordis-plugin-include': - specifier: workspace:^ - version: link:../../../vendor/include - js-yaml: - specifier: ^4.2.0 - version: 4.3.1 - - packages/experimental/agent-team-web-profile: - dependencies: '@deepseek-ai/dsh-experimental-client-ui-agent-team': specifier: workspace:^ version: link:../client-ui-agent-team + '@deepseek-ai/dsh-experimental-tool-agent-team': + specifier: workspace:^ + version: link:../tool-agent-team devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ diff --git a/scripts/release/families.spec.ts b/scripts/release/families.spec.ts index 2353f6a81e..6ae3d23702 100644 --- a/scripts/release/families.spec.ts +++ b/scripts/release/families.spec.ts @@ -49,7 +49,6 @@ describe('release families', () => { .filter(member => member.directory.startsWith('packages/experimental/')) .map(member => member.name)).toEqual([ '@deepseek-ai/dsh-experimental-agent-team-profile', - '@deepseek-ai/dsh-experimental-agent-team-web-profile', '@deepseek-ai/dsh-experimental-agent-team', '@deepseek-ai/dsh-experimental-auto-review', '@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp', diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index a3c030bc0b..33363b380e 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -93,7 +93,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/file-upload': { kind: 'none', reason: 'Browser-side request-body transport; registers nothing model-facing.' }, 'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and forwarded events; selected services own any model-visible effect.' }, - 'packages/experimental/agent-team-web-profile': { kind: 'indirect', reason: 'The Web layer delegates model composition to the Host-side Agent Teams profile.' }, 'packages/experimental/client-ui-agent-team': { kind: 'none', reason: 'The browser projection and task controls register no model-facing input.' }, 'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/ui-dockkit': { kind: 'none', reason: 'Browser-side docking layout engine and components; registers nothing model-facing.' }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 6ec529b39d..3cace3bae1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -272,7 +272,6 @@ "@deepseek-ai/dsh-experimental-agent-team": ["./packages/experimental/agent-team/src"], "@deepseek-ai/dsh-experimental-auto-review": ["./packages/experimental/auto-review/src"], "@deepseek-ai/dsh-experimental-agent-team-profile": ["./packages/experimental/agent-team-profile/src"], - "@deepseek-ai/dsh-experimental-agent-team-web-profile": ["./packages/experimental/agent-team-web-profile/src"], "@deepseek-ai/dsh-experimental-tool-agent-team": ["./packages/experimental/tool-agent-team/src"], "@deepseek-ai/dsh-experimental-webworker-runtime": ["./packages/experimental/webworker-runtime/src"], "@deepseek-ai/dsh-experimental-webworker-packer": ["./packages/experimental/webworker-packer/src"], diff --git a/tsconfig.host.json b/tsconfig.host.json index 2b98af6f58..95eae65e56 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -369,7 +369,6 @@ { "path": "./packages/experimental/agent-team" }, { "path": "./packages/experimental/agent-team-profile" }, { "path": "./packages/experimental/tool-agent-team" }, - { "path": "./packages/experimental/agent-team-web-profile" }, { "path": "./packages/workflow/workflow" }, { "path": "./packages/workflow/workflow-ptc" }, { "path": "./packages/workflow/tool-workflow" }, From cdaafbc456b307d88c499199d67ff6e5341dd48d Mon Sep 17 00:00:00 2001 From: zhc Date: Thu, 17 Sep 2026 16:29:12 +0800 Subject: [PATCH 05/28] feat(client): add shared developer tools setting --- ...9-08-document-preview-operations.i18n.yaml | 4 +- .../2026-09-08-document-preview-operations.md | 2 +- ...26-09-08-document-preview-operations.zh.md | 2 +- ...09-workspace-file-read-authority.i18n.yaml | 4 +- ...026-09-09-workspace-file-read-authority.md | 4 +- ...-09-09-workspace-file-read-authority.zh.md | 4 +- ...6-09-17-developer-tools-settings.i18n.yaml | 6 +++ .../2026-09-17-developer-tools-settings.md | 29 +++++++++++ .../2026-09-17-developer-tools-settings.zh.md | 29 +++++++++++ apps/web/tests/changed-files-turn.e2e.ts | 7 +++ .../web/tests/developer-tools-settings.e2e.ts | 29 +++++++++++ apps/web/tests/document-preview.e2e.ts | 44 +++++++++++++++- .../settings-chrome/dialog-en.expected.md | 2 + .../settings-chrome/dialog.expected.md | 2 + apps/web/tests/scaffold.ts | 5 ++ apps/web/tsconfig.json | 1 + docs/subsystems/web-client.i18n.yaml | 4 +- docs/subsystems/web-client.md | 2 + docs/subsystems/web-client.zh.md | 2 + .../client/ui-agent-preset/README.i18n.yaml | 4 +- packages/client/ui-agent-preset/README.md | 2 + packages/client/ui-agent-preset/README.zh.md | 2 + .../src/client/AgentPresetSeat.tsx | 18 ++++--- .../ui-agent-preset/src/client/index.ts | 4 +- .../tests/apply.client.spec.ts | 2 +- .../tests/components.client.spec.tsx | 17 ++++++- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 2 + packages/client/ui-conversation/README.zh.md | 2 + .../ui-conversation/src/client/apply.ts | 4 ++ .../tests/apply-inject.client.spec.tsx | 2 +- .../tests/apply-wiring.client.spec.tsx | 21 ++++++-- .../client/ui-deliverables/README.i18n.yaml | 4 +- packages/client/ui-deliverables/README.md | 2 + packages/client/ui-deliverables/README.zh.md | 2 + packages/client/ui-deliverables/package.json | 6 ++- .../src/client/Deliverables.tsx | 6 ++- .../ui-deliverables/src/client/index.ts | 6 ++- .../tests/deliverables.client.spec.tsx | 16 +++++- packages/client/ui-deliverables/tsconfig.json | 3 ++ .../ui-settings-general/README.i18n.yaml | 4 +- packages/client/ui-settings-general/README.md | 2 + .../client/ui-settings-general/README.zh.md | 2 + .../src/client/DeveloperToolsRow.module.css | 20 ++++++++ .../src/client/DeveloperToolsRow.tsx | 34 +++++++++++++ .../ui-settings-general/src/client/index.ts | 8 +++ .../ui-settings-general/src/client/locales.ts | 4 ++ .../tests/apply.client.spec.ts | 6 +-- .../tests/components.client.spec.tsx | 19 +++++++ packages/client/ui-settings/README.i18n.yaml | 4 +- packages/client/ui-settings/README.md | 2 + packages/client/ui-settings/README.zh.md | 2 + packages/client/ui-settings/package.json | 6 ++- .../ui-settings/src/client/developer-tools.ts | 27 ++++++++++ .../ui-settings/src/client/settings-scope.ts | 5 ++ .../src/developer-tools-settings.ts | 16 ++++++ packages/client/ui-settings/src/index.ts | 16 ++++-- .../tests/developer-tools.client.spec.ts | 50 +++++++++++++++++++ .../ui-settings/tests/plugin.client.spec.ts | 4 +- .../README.i18n.yaml | 4 +- .../ui-sidebar-documentpreview/README.md | 4 +- .../ui-sidebar-documentpreview/README.zh.md | 4 +- .../ui-sidebar-documentpreview/package.json | 6 ++- .../src/client/html/HtmlBody.tsx | 28 +++++++++-- .../src/client/html/basic-document.ts | 23 +++++++++ .../src/client/html/index.ts | 6 ++- .../src/client/index.ts | 2 +- .../tests/apply.client.spec.ts | 1 + .../tests/basic-html.client.spec.ts | 23 +++++++++ .../tests/document-seat.client.spec.tsx | 1 + .../tests/html-apply.client.spec.ts | 3 +- .../tests/html-body.client.spec.tsx | 23 +++++++++ .../tsconfig.client.json | 3 ++ .../src/client/slot-catalog.ts | 1 + pnpm-lock.yaml | 13 +++-- .../web/document-preview/document.expected.md | 8 +++ tsconfig.host.json | 1 + 77 files changed, 625 insertions(+), 71 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md create mode 100644 .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md create mode 100644 apps/web/tests/developer-tools-settings.e2e.ts create mode 100644 packages/client/ui-settings-general/src/client/DeveloperToolsRow.module.css create mode 100644 packages/client/ui-settings-general/src/client/DeveloperToolsRow.tsx create mode 100644 packages/client/ui-settings/src/client/developer-tools.ts create mode 100644 packages/client/ui-settings/src/developer-tools-settings.ts create mode 100644 packages/client/ui-settings/tests/developer-tools.client.spec.ts create mode 100644 packages/client/ui-sidebar-documentpreview/src/client/html/basic-document.ts create mode 100644 packages/client/ui-sidebar-documentpreview/tests/basic-html.client.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml index 60e03dbff7..414fda0806 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.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 .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md -2026-09-08-document-preview-operations.md: 980e2e443db13d6a956d554172bc765a5c11b435 -2026-09-08-document-preview-operations.zh.md: 364ef9ced6f8d98313f9bc287588c98daa408837 +2026-09-08-document-preview-operations.md: be3a835a6296cb28d9a90c5c5e8b9bf7367598c2 +2026-09-08-document-preview-operations.zh.md: efb87e16cc126001c1a6eb2bf938124d7e00044a diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md index 980e2e443d..be3a835a62 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md @@ -18,7 +18,7 @@ Readable files use `dsh-resource://file/session//`. The path ma [Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists supported alternatives and remembers a manual choice per tab. Plain text remains available for unknown extensions and text-compatible renderers. Registered binary suffixes determine text compatibility independently of loading mode; HTML and SVG remain outside those suffixes and retain source viewing. A single candidate renders no viewer control. Registered binary suffixes omit plain text; known binary suffixes without a renderer show an unsupported state without reading the file ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls existing `read`, `readAll`, and `readRelated` through ordinary injection and decodes bytes in its own `rpc.ts`. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session. -Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `` static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL. +Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `` static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL. PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Responsive sizing uses the CSS `scale` property independently of PDF.js's page rotation and translation transforms. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages. diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md index 364ef9ced6..efb87e16cc 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md @@ -18,7 +18,7 @@ Document Preview 将资源观察与内容读取分开。[资源模型](2026-09-0 [Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出受支持的候选,按 tab 记住手动选择。未知扩展名及文本兼容的渲染器仍可使用纯文本。文本兼容性由注册的二进制后缀决定,与加载方式无关;HTML 和 SVG 不属于二进制后缀,保留源码查看。只有一个候选时不显示查看器控件。注册声明为二进制的后缀不提供纯文本;已知二进制后缀没有注册渲染器时不发起读取,并显示不支持预览的空态([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 经普通注入调用既有 `read`、`readAll` 与 `readRelated`,在自己的 `rpc.ts` 解码字节。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。 -Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `` 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。 +Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `` 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。 PDF.js 官方 TextLayerBuilder 在适配宽度的 canvas 上负责选择边界和复制规范化,共享页面清理,并使用由组件拥有的 resize observer。响应式尺寸适配使用独立的 CSS `scale` 属性,与 PDF.js 的页面旋转和平移变换组合。其内容结束标记和堆叠规则限制空白区域中的选择;换行高亮被抑制。逐页取消使用 builder 的清理操作,而不 abort 第一页的信号,因为官方选择监听器跨页面共享。 diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml index e4d4d80da4..97cdb07d3f 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.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 .agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md -2026-09-09-workspace-file-read-authority.md: 35ac15f5e463910be3b3a0cfe7b80dc69c129ad3 -2026-09-09-workspace-file-read-authority.zh.md: 559a59af0bfa988ff880fad22470397e4e74f296 +2026-09-09-workspace-file-read-authority.md: d1a5b9d9634381cb74b17651c67f2b701402ff1c +2026-09-09-workspace-file-read-authority.zh.md: f8b0fae4325a90548c6b9dc33ac46b17248b04e7 diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md index 35ac15f5e4..d1a5b9d963 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md +++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md @@ -14,7 +14,7 @@ Workspace Files serves both file content and workspace navigation. Applying work `list` and `changes` remain workspace-scoped because they expose workspace navigation and observation rather than a named file read. `list` rejects a directory outside the root, and `changes` filters observations through the backend's workspace-containment predicate. -`readRelated` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML. +`readRelated` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML. The [Workspace Files service](2026-09-05-workspace-files-service.md) owns paging, file checks, listing, and observation. [Document Preview](2026-09-08-document-preview-operations.md) owns which related files are packaged and the iframe sandbox. @@ -26,4 +26,4 @@ The [Workspace Files service](2026-09-05-workspace-files-service.md) owns paging ## Consequences -Any caller holding a valid Session file address can receive bytes from every regular file that the Session filesystem backend permits it to read, including files outside the workspace. A previewed HTML document can execute packaged local JavaScript and make network requests. Outside files do not produce `changes` frames, so their previews require explicit refresh to observe updates. +Any caller holding a valid Session file address can receive bytes from every regular file that the Session filesystem backend permits it to read, including files outside the workspace. With Developer tools enabled, a previewed HTML document can execute packaged local JavaScript and make network requests. Outside files do not produce `changes` frames, so their previews require explicit refresh to observe updates. diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md index 559a59af0b..f8b0fae432 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md @@ -14,7 +14,7 @@ Workspace Files 同时提供文件内容与工作区导航。对所有操作应 `list` 与 `changes` 仍限于工作区,因为它们暴露工作区导航和观察,而不是读取一个具名文件。`list` 拒绝根外目录,`changes` 通过后端的工作区包含判定过滤观察。 -`readRelated` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。 +`readRelated` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。 [Workspace Files 服务](2026-09-05-workspace-files-service.zh.md)负责分页、文件检查、列举和观察。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责选择要打包的关联文件及 iframe sandbox。 @@ -26,4 +26,4 @@ Workspace Files 同时提供文件内容与工作区导航。对所有操作应 ## Consequences -持有有效 Session 文件地址的调用方可以接收 Session 文件系统后端允许读取的每个普通文件的字节,包括工作区外文件。预览的 HTML 文档可以执行已打包的本地 JavaScript,并发起网络请求。工作区外文件不会产生 `changes` 帧,因此其预览需要显式刷新才能观察更新。 +持有有效 Session 文件地址的调用方可以接收 Session 文件系统后端允许读取的每个普通文件的字节,包括工作区外文件。开启开发者工具时,预览的 HTML 文档可以执行已打包的本地 JavaScript,并发起网络请求。工作区外文件不会产生 `changes` 帧,因此其预览需要显式刷新才能观察更新。 diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml new file mode 100644 index 0000000000..945d2c9c60 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md +2026-09-17-developer-tools-settings.md: cfef2402b8e332f0db60eda50c59637865709130 +2026-09-17-developer-tools-settings.zh.md: 675ffb2812d554d57d5109bfe3b8ac045f2e1138 diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md new file mode 100644 index 0000000000..cfef2402b8 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md @@ -0,0 +1,29 @@ +# Agent Note: Shared developer-tool settings + +Status: implemented + +English | [中文](2026-09-17-developer-tools-settings.zh.md) + +## Problem + +Diagnostic views, preset selection and changed-file summaries add complexity to routine tasks. Scripted HTML also grants capabilities that a basic document preview does not need. Desktop and Web share these renderers, so independent switches would let the same setting produce different results across clients. + +## Decision + +The settings owner exposes one accepted `ui-developer-tools.enabled` preference, defaulting off. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; the browser adds no second copy of the preference. Loopback Web and desktop use Host persistence, while remote Web retains the existing memory policy. + +Off exposes only Chat, removes the conversation View tab bar, hides the new-session preset chip, and omits the changed-files card and its summary read. Turning it off from another View activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. + +The builtin HTML renderer enforces its own preview policy. Off parses into an inert template, removes active documents and navigation, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. + +## Alternatives considered + +**Limit the switch to desktop.** The selected requirement contains no desktop-only restriction, and the affected UI is shared. A desktop check would leave the Web renderer with inconsistent visibility and preview behavior. + +**Hide controls without changing the active View or iframe.** Previously selected diagnostic content and already-running scripts would survive the toggle. Filtering the available View roster and remounting the HTML context enforce the selected presentation immediately. + +## Consequences + +Basic HTML sacrifices JavaScript, external and related resources, and link navigation; inline styles and data images remain available. Advanced mode retains normal browser networking and does not gain parent-origin, popup, form or top-navigation privileges. The setting does not constrain third-party preview implementations or revoke Host filesystem authority. + +Component and settings tests cover defaulting, schema rejection, accepted-state updates, chip disclosure, changed-file visibility and iframe disposal. The recorded-session [document preview](../../../../apps/web/tests/document-preview.e2e.ts) and [changed-files](../../../../apps/web/tests/changed-files-turn.e2e.ts) scenarios exercise the real settings control and the shared renderer. The browser scaffold explicitly seeds developer mode for diagnostic scenarios; these two scenarios leave the shipped default untouched. Native Electron execution is a separate acceptance surface. diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md new file mode 100644 index 0000000000..675ffb2812 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 共享开发者工具设置 + +Status: implemented + +[English](2026-09-17-developer-tools-settings.md) | 中文 + +## Problem + +诊断视图、预设选择和改动文件摘要增加了日常任务的复杂度。支持脚本的 HTML 还授予了基础文档预览不需要的能力。桌面端和 Web 共享这些渲染器,分别设置开关会让同一设置在不同客户端产生不同结果。 + +## Decision + +设置所有者提供一个已接受的 `ui-developer-tools.enabled` 偏好,默认关闭。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;浏览器不再复制偏好状态。回环 Web 和桌面端使用 Host 持久化,远程 Web 保持现有的内存策略。 + +关闭时仅展示 Chat,移除会话 View 标签栏,隐藏新会话预设入口,并省略改动文件卡片及其摘要读取。在其他 View 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 + +内置 HTML 渲染器执行自己的预览策略。关闭时,内容被解析到惰性 template 中,移除主动文档与导航,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 + +## Alternatives considered + +**仅在桌面端提供开关。** 所选需求没有仅限桌面的约束,相关 UI 也是共享的。桌面端条件判断会让 Web 渲染器的展示和预览行为不一致。 + +**隐藏控件但不改变激活的 View 或 iframe。** 之前选择的诊断内容和已经运行的脚本会在切换后继续存在。过滤可用 View 并重新挂载 HTML 上下文,可以立即执行所选展示策略。 + +## Consequences + +基础 HTML 放弃 JavaScript、外部与关联资源和链接导航;行内样式与 data 图片仍可用。高级模式保留正常浏览器网络能力,不增加父源、弹窗、表单或顶层导航权限。此设置不约束第三方预览实现,也不撤销 Host 文件系统读取权限。 + +组件和设置测试覆盖默认值、schema 拒绝、已接受状态更新、入口折叠、改动文件展示和 iframe 清理。基于已记录 Session 的[文档预览](../../../../apps/web/tests/document-preview.e2e.ts)与[改动文件](../../../../apps/web/tests/changed-files-turn.e2e.ts)场景验证真实设置控件及共用渲染器。浏览器脚手架为诊断场景显式开启开发者模式;这两个场景保留产品默认值。原生 Electron 执行是独立的验收面。 diff --git a/apps/web/tests/changed-files-turn.e2e.ts b/apps/web/tests/changed-files-turn.e2e.ts index 08045544fc..170adedbe7 100644 --- a/apps/web/tests/changed-files-turn.e2e.ts +++ b/apps/web/tests/changed-files-turn.e2e.ts @@ -51,6 +51,7 @@ describe('web e2e: a git workspace turn ends with its changed files', () => { await writeFile(replayOverride, JSON.stringify(script).replaceAll('{{cwd}}', JSON.stringify(cwdToken).slice(1, -1))) } scaffold = await launchWebScaffold({ + developerTools: false, compareReplaySession: true, extraOverlayPath: fileURLToPath(new URL('./changed-files-turn.overlay.yml', import.meta.url)), ...(replayOverride === undefined ? {} : { replayFixture: FIXTURE, replayOverride }), @@ -106,6 +107,12 @@ describe('web e2e: a git workspace turn ends with its changed files', () => { expect(await readFile(join(cwd, 'notes.txt'), 'utf8')).toBe('start\ndone\n') const card = page.locator('[data-changed-files]') + expect(await card.count()).toBe(0) + await page.getByRole('button', { name: '设置', exact: true }).click() + const settings = page.getByRole('dialog', { name: '设置' }) + await settings.getByRole('switch', { name: '开发者工具' }).click() + await expect.poll(() => settings.getByRole('switch', { name: '开发者工具' }).getAttribute('aria-checked')).toBe('true') + await settings.getByRole('button', { name: '关闭', exact: true }).click() await card.waitFor({ state: 'visible' }) expect(await card.getByText('已编辑 4 个文件', { exact: true }).count()).toBe(1) expect(await card.getByRole('listitem').count()).toBe(3) diff --git a/apps/web/tests/developer-tools-settings.e2e.ts b/apps/web/tests/developer-tools-settings.e2e.ts new file mode 100644 index 0000000000..16c0e6500a --- /dev/null +++ b/apps/web/tests/developer-tools-settings.e2e.ts @@ -0,0 +1,29 @@ +/** Shared settings are off on a fresh Host and survive browser reload through the ordinary settings UI. */ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { chromium } from 'playwright' +import { expect, it, onTestFinished } from 'vitest' +import { launchWebScaffold } from './scaffold.ts' +import { newEnglishPage } from './support.ts' + +it('persists developer tools in the Host settings document and restores the accepted choice', async () => { + const scaffold = await launchWebScaffold({ developerTools: false }) + onTestFinished(() => scaffold.close()) + const browser = await chromium.launch() + onTestFinished(() => browser.close()) + const page = await newEnglishPage(browser) + await page.goto(scaffold.authenticatedUrl) + await page.getByRole('button', { name: 'Settings', exact: true }).click() + const toggle = page.getByRole('switch', { name: 'Developer tools' }) + expect(await toggle.getAttribute('aria-checked')).toBe('false') + expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).not.toContain('ui-developer-tools:') + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).toContain('ui-developer-tools:\n enabled: true') + await page.reload() + await page.getByRole('button', { name: 'Settings', exact: true }).click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false') + expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).toContain('ui-developer-tools:\n enabled: false') +}) diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 24137b06ad..548abeb18d 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -86,7 +86,9 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () beforeAll(async () => { outsideRoot = await mkdtemp(join(tmpdir(), 'dsh-preview-outside-')) - scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5, compareReplaySession: false, extraOverlayPath: PAGING_PATCH }) + scaffold = await launchWebScaffold({ + developerTools: false, replayFixture: FIXTURE, paceMs: 5, compareReplaySession: false, extraOverlayPath: PAGING_PATCH, + }) browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) @@ -111,17 +113,25 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () await mkdir(SHOT_DIR, { recursive: true }) await saveFailureShot(page, `screenshots/0908-document-preview/smoke-${process.pid}`) }) + expect(await page.locator('[data-slot="conversation.hero.agentPreset"] button').count()).toBe(0) + expect(scaffold.ctx.settings.get('ui-developer-tools')).toEqual({ enabled: false }) const settled = scaffold.whenTurnSettled() const input = page.locator('[data-composer-input]').first() await input.fill(PROMPT) await input.press('Enter') const sessionId = await settled await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) + expect(await page.getByRole('tablist').count()).toBe(0) const cwd = scaffold.ctx.agents.get(sessionId)?.session.header.cwd if (cwd === undefined) throw new Error('settled Session has no workspace cwd') if (outsideRoot === undefined) throw new Error('outside fixture directory is unavailable') const outsideScript = join(outsideRoot, 'outside.js') const outsideReference = relative(cwd, outsideScript).replace(/\\/g, '/') + let previewNetworkRequests = 0 + await page.route('https://preview.invalid/developer-tools.png', async (route) => { + previewNetworkRequests += 1 + await route.fulfill({ status: 200, contentType: 'image/png', body: TINY_PNG }) + }) const markdownText = [ '# Markdown smoke', '', 'Rendered from the workspace.', '', ...Array.from({ length: (PAGE_LINES - 4) / 2 }, (_, index) => [`Paragraph ${index + 1}: ${'visible prefix '.repeat(20)}`, '']).flat(), @@ -138,6 +148,7 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () writeFile(join(cwd, 'smoke.html'), [ '', '

HTML smoke

pending

pending

pending

', + '', '

pending

', ' + + LinkSVG + `)) + const parsed = new DOMParser().parseFromString(html, 'text/html') + expect(parsed.head.firstElementChild?.getAttribute('http-equiv')).toBe('Content-Security-Policy') + expect(parsed.querySelector('meta')?.getAttribute('content')).toContain("connect-src 'none'") + expect(parsed.querySelectorAll('script, iframe, object, embed, base, a[href], a[xlink\\:href]')).toHaveLength(0) + expect(parsed.querySelectorAll('meta[http-equiv]')).toHaveLength(1) + expect(parsed.querySelector('p')?.textContent).toBe('Static') + expect(parsed.querySelector('style')?.textContent).toContain('color: red') + expect(parsed.querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,AA==') +}) diff --git a/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx index 5c8f1c258c..a5c1ca9433 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx @@ -36,6 +36,7 @@ afterEach(async () => { async function boot() { const rt = await SlotTestRuntime.create() runtime = rt + rt.ctx.provide('settingsScope', { developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) rt.ctx.provide('layout', { openRightbar: vi.fn(), closeRightbar: vi.fn() } as never) const locale = new LocaleRuntime(rt.ctx) rt.ctx.provide('locale', locale) diff --git a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts index ccb8ec82c8..554f5e2b06 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts @@ -30,6 +30,7 @@ describe('HTML registration', () => { it('registers its dictionary and matching keyed body, and removes all contributions on disposal', async () => { const ctx = new Context() + ctx.provide('settingsScope', { developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) // No Session or Tab services are mounted; the global callback must use its file address. const registry = new DocumentPreviewRegistry() const dictionaries = new Map() @@ -46,7 +47,7 @@ describe('HTML registration', () => { bind: () => (key: keyof typeof en) => en[key], register: (name: string, value: unknown) => { dictionaries.set(name, value); return () => { dictionaries.delete(name) } }, } as never) - const fiber = ctx.plugin({ apply }) + const fiber = ctx.plugin({ inject: ['settingsScope'], apply }) dispose = async () => { await fiber.dispose() } await fiber.await() expect(registry.candidates('INDEX.HTM').map(entry => entry.id)).toEqual([HTML_BODY_ID]) diff --git a/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx index 78eca6a5f2..38f19351fe 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx @@ -34,6 +34,7 @@ afterEach(() => { function props(text = '

hello

'): HtmlBodyProps { const signal = new AbortController().signal return { + useDeveloperTools: select => select(true), resourceAddress: 'dsh-resource://file/session/html/index.html', content: { kind: 'bytes', data: utf8(text) }, wrap: false, @@ -48,6 +49,28 @@ function props(text = '

hello

'): HtmlBodyProps { const utf8 = (text: string): Uint8Array => new TextEncoder().encode(text) describe('HtmlBody', () => { + it('renders static HTML without reading related files, running scripts or retaining an advanced frame', async () => { + const initial = props('

Preview

Static content

') + const basic = { ...initial, useDeveloperTools: ((select: (enabled: boolean) => unknown) => select(false)) as HtmlBodyProps['useDeveloperTools'] } + const view = render() + const frame = screen.getByTitle(en.frame) + expect(frame.getAttribute('sandbox')).toBe('') + expect(frame.getAttribute('srcdoc')).toContain("default-src 'none'") + expect(frame.getAttribute('srcdoc')).not.toContain('Advanced

') + view.rerender() + const advanced = await screen.findByTitle(en.frame) + expect(advanced).not.toBe(frame) + expect(advanced.getAttribute('sandbox')).toBe('allow-scripts') + view.rerender() + expect(advanced.isConnected).toBe(false) + expect(revoke).toHaveBeenCalledOnce() + view.rerender() + expect(screen.getByRole('alert').textContent).toBe(en.failed) + }) + it('renders a Blob iframe with only scripts allowed, keeping it mounted for unrelated props', async () => { const initial = props() const view = render() diff --git a/packages/client/ui-sidebar-documentpreview/tsconfig.client.json b/packages/client/ui-sidebar-documentpreview/tsconfig.client.json index f1a8702766..c02235dfc2 100644 --- a/packages/client/ui-sidebar-documentpreview/tsconfig.client.json +++ b/packages/client/ui-sidebar-documentpreview/tsconfig.client.json @@ -64,6 +64,9 @@ }, { "path": "../../document/office-to-pdf" + }, + { + "path": "../ui-settings" } ] } diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 0caa5b38bf..011791768c 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -1967,6 +1967,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'client-ui-chat TranscriptViewRow id \'transcript-view\'', 'client-ui-conversation EnterBehaviorRow id \'composer-enter\'', 'client-ui-permission-presets PermissionRow id \'permission\'', + 'client-ui-settings-general DeveloperToolsRow id \'developer-tools\'', 'client-ui-theme AppearanceRow id \'appearance\'', 'client-ui-theme FontSizeRow id \'font-size\'', ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13ab4ef874..27966301e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3077,6 +3077,9 @@ importers: '@deepseek-ai/dsh-client-ui-renderer': specifier: workspace:^ version: link:../ui-renderer + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-sidebar-right': specifier: workspace:^ version: link:../ui-sidebar-right @@ -4015,6 +4018,10 @@ importers: version: 18.3.1 packages/client/ui-settings: + dependencies: + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -4037,9 +4044,6 @@ importers: '@deepseek-ai/dsh-util-values': specifier: workspace:^ version: link:../../util/values - '@deepseek-ai/schemastery': - specifier: link:../../../vendor/schemastery - version: link:../../../vendor/schemastery '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -4429,6 +4433,9 @@ importers: '@deepseek-ai/dsh-client-ui-session': specifier: workspace:^ version: link:../ui-session + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-sidebar-right': specifier: workspace:^ version: link:../ui-sidebar-right diff --git a/snapshots/web/document-preview/document.expected.md b/snapshots/web/document-preview/document.expected.md index 28689fa8ca..41159c8576 100644 --- a/snapshots/web/document-preview/document.expected.md +++ b/snapshots/web/document-preview/document.expected.md @@ -14,6 +14,14 @@ - Viewers: Markdown -> Code -> Plain text - Same tab: true +## Basic HTML + +- Developer tools: off by default on Web and desktop +- Sandbox: no permissions +- Inline script: pending +- Local script: pending +- Network requests: 0 + ## HTML - Viewer: HTML diff --git a/tsconfig.host.json b/tsconfig.host.json index 2b98af6f58..edcbb07ff1 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -15,6 +15,7 @@ "apps/web/tests/default-product-isolation.e2e.ts", "apps/web/tests/diff-context.e2e.ts", "apps/web/tests/scaffold.ts", + "apps/web/tests/developer-tools-settings.e2e.ts", "apps/web/tests/auto-review-fixture.ts", "apps/web/tests/auto-review-denial.e2e.ts", "apps/web/tests/scaffold-generation.spec.ts", From 012f993ed4b0809c266a05cd797bd92accd19815 Mon Sep 17 00:00:00 2001 From: zhc Date: Thu, 17 Sep 2026 18:08:34 +0800 Subject: [PATCH 06/28] fix: address developer tools preference review --- ...6-09-17-developer-tools-settings.i18n.yaml | 4 +-- .../2026-09-17-developer-tools-settings.md | 8 ++--- .../2026-09-17-developer-tools-settings.zh.md | 8 ++--- apps/web/tests/document-preview.e2e.ts | 28 +++++++++++++++++ .../settings-chrome/dialog-en.expected.md | 2 +- .../settings-chrome/dialog.expected.md | 2 +- apps/web/tests/settings-chrome.e2e.ts | 12 ++++---- .../ui-chat/src/client/chat/ChatView.tsx | 4 +-- .../ui-chat/src/client/contract/slots.ts | 2 +- .../tests/apply-inject.client.spec.tsx | 2 +- .../ui-chat/tests/chat-apply.client.spec.tsx | 1 + .../ui-chat/tests/chat-view.client.spec.tsx | 13 ++++++-- .../client/ui-conversation/README.i18n.yaml | 4 +-- packages/client/ui-conversation/README.md | 2 +- packages/client/ui-conversation/README.zh.md | 2 +- .../ui-conversation/src/client/apply.ts | 4 +-- .../src/client/contract/slots.ts | 2 ++ .../skeleton/DefaultConversationViews.tsx | 1 + .../src/client/view-selection.ts | 3 +- .../tests/apply-inject.client.spec.tsx | 5 ++++ .../tests/assembly-surfaces.client.spec.tsx | 8 ++--- .../tests/browser-plugin.client.spec.ts | 2 +- .../ui-layout/tests/apply.client.spec.ts | 2 +- .../tests/browser-plugin.client.spec.ts | 2 +- .../ui-settings-general/README.i18n.yaml | 4 +-- packages/client/ui-settings-general/README.md | 10 +++---- .../client/ui-settings-general/README.zh.md | 10 +++---- .../src/client/DeveloperToolsRow.tsx | 5 +++- .../ui-settings-general/src/client/locales.ts | 2 ++ .../tests/components.client.spec.tsx | 14 +++++++++ packages/client/ui-settings/README.i18n.yaml | 4 +-- packages/client/ui-settings/README.md | 6 ++-- packages/client/ui-settings/README.zh.md | 6 ++-- .../ui-settings/src/client/developer-tools.ts | 23 ++++++++++---- .../tests/developer-tools.client.spec.ts | 30 +++++++++++++++++++ .../tests/settings-scope.client.spec.ts | 6 ++++ .../src/client/html/basic-document.ts | 22 ++++++++------ .../tests/basic-html.client.spec.ts | 17 +++++++++++ .../tests/browser-plugin.client.spec.ts | 2 +- packages/client/ui-tool/README.i18n.yaml | 4 +-- packages/client/ui-tool/README.md | 2 +- packages/client/ui-tool/README.zh.md | 2 +- .../ui-tool/src/client/tool/ToolCallTree.tsx | 2 +- .../tests/assembly-surfaces.client.spec.tsx | 2 +- .../tests/chat-ptc-subcalls.client.spec.tsx | 3 +- .../tests/tool-call-tree.client.spec.tsx | 6 ++++ .../tests/toolview-slot.client.spec.tsx | 4 +-- .../tests/client-bundle.client.spec.ts | 2 +- .../ui-trajectory/tests/views.client.spec.tsx | 7 +++-- .../tests/workflow-run.client.spec.tsx | 2 +- .../src/client/slot-catalog.ts | 5 ++-- 51 files changed, 235 insertions(+), 90 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml index 945d2c9c60..43b546d998 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.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 .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md -2026-09-17-developer-tools-settings.md: cfef2402b8e332f0db60eda50c59637865709130 -2026-09-17-developer-tools-settings.zh.md: 675ffb2812d554d57d5109bfe3b8ac045f2e1138 +2026-09-17-developer-tools-settings.md: 694e2a09fc20ad3185ba9c09058e8684a2247e46 +2026-09-17-developer-tools-settings.zh.md: e03aa258675be8f49dc13a9b43f2e3e00c8b85fa diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md index cfef2402b8..694e2a09fc 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md @@ -10,11 +10,11 @@ Diagnostic views, preset selection and changed-file summaries add complexity to ## Decision -The settings owner exposes one accepted `ui-developer-tools.enabled` preference, defaulting off. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; the browser adds no second copy of the preference. Loopback Web and desktop use Host persistence, while remote Web retains the existing memory policy. +The settings owner exposes one accepted `ui-developer-tools.enabled` preference, defaulting off. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes. -Off exposes only Chat, removes the conversation View tab bar, hides the new-session preset chip, and omits the changed-files card and its summary read. Turning it off from another View activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. +Off exposes only Chat, hides tool Inspect actions whose trajectory target is unavailable, removes the conversation View tab bar, hides the new-session preset chip, and omits the changed-files card and its summary read. Turning it off from another View activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. -The builtin HTML renderer enforces its own preview policy. Off parses into an inert template, removes active documents and navigation, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. +The builtin HTML renderer enforces its own preview policy. Off parses a complete inert document, preserves root styling, recursively sanitizes template contents, removes active documents, resource hints, SVG animation and navigation, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. ## Alternatives considered @@ -26,4 +26,4 @@ The builtin HTML renderer enforces its own preview policy. Off parses into an in Basic HTML sacrifices JavaScript, external and related resources, and link navigation; inline styles and data images remain available. Advanced mode retains normal browser networking and does not gain parent-origin, popup, form or top-navigation privileges. The setting does not constrain third-party preview implementations or revoke Host filesystem authority. -Component and settings tests cover defaulting, schema rejection, accepted-state updates, chip disclosure, changed-file visibility and iframe disposal. The recorded-session [document preview](../../../../apps/web/tests/document-preview.e2e.ts) and [changed-files](../../../../apps/web/tests/changed-files-turn.e2e.ts) scenarios exercise the real settings control and the shared renderer. The browser scaffold explicitly seeds developer mode for diagnostic scenarios; these two scenarios leave the shipped default untouched. Native Electron execution is a separate acceptance surface. +Component and settings tests cover defaulting, schema rejection, accepted-state updates, chip disclosure, changed-file visibility and iframe disposal. The recorded-session [document preview](../../../../apps/web/tests/document-preview.e2e.ts) and [changed-files](../../../../apps/web/tests/changed-files-turn.e2e.ts) scenarios exercise the real settings control and the shared renderer. The browser scaffold explicitly seeds developer mode for diagnostic scenarios; settings chrome, document preview and changed-file scenarios leave the shipped default untouched. Native Electron execution remains unverified. diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md index 675ffb2812..e03aa25867 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md @@ -10,11 +10,11 @@ Status: implemented ## Decision -设置所有者提供一个已接受的 `ui-developer-tools.enabled` 偏好,默认关闭。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;浏览器不再复制偏好状态。回环 Web 和桌面端使用 Host 持久化,远程 Web 保持现有的内存策略。 +设置所有者提供一个已接受的 `ui-developer-tools.enabled` 偏好,默认关闭。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。 -关闭时仅展示 Chat,移除会话 View 标签栏,隐藏新会话预设入口,并省略改动文件卡片及其摘要读取。在其他 View 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 +关闭时仅展示 Chat,隐藏轨迹目标不可用的工具 Inspect 操作,移除会话 View 标签栏,隐藏新会话预设入口,并省略改动文件卡片及其摘要读取。在其他 View 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 -内置 HTML 渲染器执行自己的预览策略。关闭时,内容被解析到惰性 template 中,移除主动文档与导航,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 +内置 HTML 渲染器执行自己的预览策略。关闭时,内容被解析为完整的惰性文档,保留根节点样式,递归清理 template 内容,移除主动文档、资源提示、SVG 动画与导航,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 ## Alternatives considered @@ -26,4 +26,4 @@ Status: implemented 基础 HTML 放弃 JavaScript、外部与关联资源和链接导航;行内样式与 data 图片仍可用。高级模式保留正常浏览器网络能力,不增加父源、弹窗、表单或顶层导航权限。此设置不约束第三方预览实现,也不撤销 Host 文件系统读取权限。 -组件和设置测试覆盖默认值、schema 拒绝、已接受状态更新、入口折叠、改动文件展示和 iframe 清理。基于已记录 Session 的[文档预览](../../../../apps/web/tests/document-preview.e2e.ts)与[改动文件](../../../../apps/web/tests/changed-files-turn.e2e.ts)场景验证真实设置控件及共用渲染器。浏览器脚手架为诊断场景显式开启开发者模式;这两个场景保留产品默认值。原生 Electron 执行是独立的验收面。 +组件和设置测试覆盖默认值、schema 拒绝、已接受状态更新、入口折叠、改动文件展示和 iframe 清理。基于已记录 Session 的[文档预览](../../../../apps/web/tests/document-preview.e2e.ts)与[改动文件](../../../../apps/web/tests/changed-files-turn.e2e.ts)场景验证真实设置控件及共用渲染器。浏览器脚手架为诊断场景显式开启开发者模式;设置外壳、文档预览和改动文件场景保留产品默认值。原生 Electron 执行尚未验证。 diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 548abeb18d..57d513c72a 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -132,6 +132,11 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () previewNetworkRequests += 1 await route.fulfill({ status: 200, contentType: 'image/png', body: TINY_PNG }) }) + const blockedRequests: string[] = [] + await page.route('https://blocked-preview.invalid/**', async (route) => { + blockedRequests.push(route.request().url()) + await route.fulfill({ status: 200, contentType: 'text/html', body: 'ESCAPED' }) + }) const markdownText = [ '# Markdown smoke', '', 'Rendered from the workspace.', '', ...Array.from({ length: (PAGE_LINES - 4) / 2 }, (_, index) => [`Paragraph ${index + 1}: ${'visible prefix '.repeat(20)}`, '']).flat(), @@ -142,6 +147,15 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () 'const tail = "CODE_TAIL";', ] await Promise.all([ + writeFile(join(cwd, 'hostile.html'), ` + + +

Static adversarial preview

+ Plain link +
+ SVG link + + `), writeFile(join(cwd, 'smoke.md'), markdownText), writeFile(join(cwd, 'pages.ts'), codeLines.join('\n')), writeFile(join(cwd, 'notes.unknown'), 'UNKNOWN_SUFFIX\nPlain fallback.'), @@ -274,6 +288,20 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () `- Same tab: ${String(await markdownTab.getAttribute('data-dockkit-tab') === markdownTabId)}`, ].join('\n')) + await openFile('hostile.html') + const hostileFrame = page.frameLocator('[data-html-preview]') + await hostileFrame.getByRole('heading', { name: 'Static adversarial preview' }).waitFor() + expect(await hostileFrame.locator('html').getAttribute('class')).toBe('dark') + expect(await hostileFrame.locator('body').evaluate(node => getComputedStyle(node).margin)).toBe('0px') + for (const id of ['plain-link', 'shadow-link', 'svg-link']) { + const link = hostileFrame.locator(`#${id}`) + expect(await link.getAttribute('href')).toBeNull() + await link.click() + } + expect(await hostileFrame.locator('noscript, link, set, animate, iframe').count()).toBe(0) + expect(blockedRequests).toEqual([]) + await hostileFrame.getByRole('heading', { name: 'Static adversarial preview' }).waitFor() + await openFile('smoke.html') await expect.poll(() => viewer.innerText()).toBe('HTML') const iframe = preview.locator('[data-html-preview]') diff --git a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md index a7ca7e1f39..f06841fc54 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md @@ -22,6 +22,6 @@ - text: px Conversation display Controls process content in completed turns - button "Compact" - text: Developer tools Show tools and information for debugging and troubleshooting - - switch "Developer tools" [checked] + - switch "Developer tools" - text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior - button "Queue" diff --git a/apps/web/tests/expected/settings-chrome/dialog.expected.md b/apps/web/tests/expected/settings-chrome/dialog.expected.md index 272a15368c..0cd84e6a3c 100644 --- a/apps/web/tests/expected/settings-chrome/dialog.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog.expected.md @@ -22,6 +22,6 @@ - text: px 对话显示 控制已完成轮次的过程内容 - button "紧凑" - text: 开发者工具 显示用于调试和排查问题的工具与信息 - - switch "开发者工具" [checked] + - switch "开发者工具" - text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为 - button "排队发送" diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index ceed3263cb..b396d5f969 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -37,7 +37,7 @@ describe('web e2e: settings modal and General preferences', () => { let tripwire: ReturnType beforeAll(async () => { - scaffold = await launchWebScaffold({}) + scaffold = await launchWebScaffold({ developerTools: false }) browser = await chromium.launch() // Chinese browser: the shared page asserts the localized settings surface // the client derives from it (the English default has its own spec below). @@ -384,7 +384,7 @@ describe('web e2e: settings modal and General preferences', () => { // A second live Host binds another ephemeral port but shares the same // user-settings home. Its fresh origin has no theme localStorage and still // converges to dark before the settings dialog opens. - const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome }) + const second = await launchWebScaffold({ developerTools: false, harnessHome: scaffold.harnessHome }) const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) const secondTripwire = watchConsole(secondPage) try { @@ -549,7 +549,7 @@ describe('web e2e: settings modal and General preferences', () => { const reloaded = page.getByRole('dialog', { name: '设置' }) await reloaded.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 }) - const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome }) + const second = await launchWebScaffold({ developerTools: false, harnessHome: scaffold.harnessHome }) const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) const secondTripwire = watchConsole(secondPage) try { @@ -615,7 +615,7 @@ describe('web e2e: settings modal and General preferences', () => { // A Chinese browser on another port still receives the explicit English // preference from the shared Host settings document. - const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome }) + const second = await launchWebScaffold({ developerTools: false, harnessHome: scaffold.harnessHome }) const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) const secondTripwire = watchConsole(secondPage) try { @@ -649,7 +649,7 @@ describe('web e2e: settings modal and General preferences', () => { // browser. English is also FALLBACK_LOCALE, so this scenario alone cannot // distinguish detection from the default — the zh scenarios above supply // the discriminating half (a Chinese browser must NOT land on the default). - const fresh = await launchWebScaffold({}) + const fresh = await launchWebScaffold({ developerTools: false }) const enPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'en-US' }) const enTripwire = watchConsole(enPage) onTestFailed(() => saveFailureShot(enPage, 'web-e2e-settings-browser-language')) @@ -688,7 +688,7 @@ describe('web e2e: settings modal and General preferences', () => { // The product default for "no usable signal": a French browser ships // neither zh nor en, so resolution falls to FALLBACK_LOCALE (en) rather // than to Chinese. - const fresh = await launchWebScaffold({}) + const fresh = await launchWebScaffold({ developerTools: false }) const frPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'fr-FR' }) const frTripwire = watchConsole(frPage) onTestFailed(() => saveFailureShot(frPage, 'web-e2e-settings-unshipped-language')) diff --git a/packages/client/ui-chat/src/client/chat/ChatView.tsx b/packages/client/ui-chat/src/client/chat/ChatView.tsx index 12a6f42546..acec184158 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatView.tsx @@ -220,7 +220,7 @@ const ChatNodeList = memo(function ChatNodeList({ order, ...seatProps }: ChatNod export function ChatView({ useSession, useChat, useChatNode, useChatNodeProcess, useSessions, useStore, actions, renderSlot, sessionId, openFile, openSkill, openExternalLink, loadOlder, loadThrough, loadImage, openView, chatScroll, forkAt, fileMentions, - useTranscriptView, useProjection, t, + useTranscriptView, useProjection, availableViews, t, }: ChatViewSlotProps) { const order = useChat(s => s.order) const nodeStore = useChat(s => s.nodes) @@ -796,7 +796,7 @@ export function ChatView({ cwd={cwd} openFile={requestOpenFile} openSkill={openSkill} - inspectCall={inspectCall} + inspectCall={availableViews.some(view => view.id === 'trajectory') ? inspectCall : undefined} forkAt={forkAt} loadImage={loadImage} renderMessageImages={renderMessageImages} diff --git a/packages/client/ui-chat/src/client/contract/slots.ts b/packages/client/ui-chat/src/client/contract/slots.ts index 81cee7d021..80b29002b1 100644 --- a/packages/client/ui-chat/src/client/contract/slots.ts +++ b/packages/client/ui-chat/src/client/contract/slots.ts @@ -82,7 +82,7 @@ export interface ChatNodeOwnerProps { /** Open the current source file of a skill referenced by a sent message. */ openSkill: (name: string) => void openFile: (path: string, options?: OpenFileOptions) => void - inspectCall: (callId: ToolCallId) => void + inspectCall?: ((callId: ToolCallId) => void) | undefined forkAt: (seq: number) => void /** * Session-authorized image loader, down-threaded from the Chat view so a diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx index eab2fdf96e..15aed51516 100644 --- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx @@ -48,7 +48,7 @@ function sessionFakeFor() { async function bench() { const runtime = await SlotTestRuntime.create() - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const layout = { closeRightbar: vi.fn(), openRightbar: vi.fn() } runtime.ctx.provide('layout', layout as never) const sidebarRight = { diff --git a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx index 83fe28642b..72233351ef 100644 --- a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx @@ -38,6 +38,7 @@ async function bench() { const runtime = await SlotTestRuntime.create() const chatSettings = stubSettingsScope() runtime.ctx.provide('settingsScope', { + developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } }, bind: ({ namespace }: { namespace: string }) => namespace === CHAT_SETTINGS_NAMESPACE ? chatSettings.scope : stubSettingsScope().scope, diff --git a/packages/client/ui-chat/tests/chat-view.client.spec.tsx b/packages/client/ui-chat/tests/chat-view.client.spec.tsx index d62466703f..2066cca99c 100644 --- a/packages/client/ui-chat/tests/chat-view.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-view.client.spec.tsx @@ -400,6 +400,7 @@ function makeHarness( useTranscriptView: bindSnapshotSelector(transcriptView), renderSlot, SessionProvider: SessionProviderStub, + availableViews: [{ id: 'chat', label: 'Chat' }, { id: 'trajectory', label: 'Trajectory' }], viewRequest: null, openView, completeViewRequest: () => {}, @@ -1320,12 +1321,20 @@ describe('ChatView', () => { expect(view.queryByText('本轮运行失败')).toBeNull() }) + it('removes Inspect when trajectory is unavailable and restores it with the view', () => { + const h = makeHarness({ nodes: [toolResult(3, 'a')] }) + const view = render() + expect(h.toolOwners.at(-1)?.inspectCall).toBeUndefined() + view.rerender() + expect(h.toolOwners.at(-1)?.inspectCall).toBeTypeOf('function') + }) + it('hands the trajectory callback to the Tool seat', () => { const h = makeHarness({ nodes: [toolResult(3, 'a')], }) render() - h.toolOwners[0]?.inspectCall('a') + h.toolOwners[0]?.inspectCall?.('a') expect(h.openView).toHaveBeenCalledWith('trajectory', 'a') }) @@ -2233,7 +2242,7 @@ describe('ChatView', () => { expect(owner.openFile).not.toBe(h.openFile) owner.openFile('src/a.ts') expect(h.openFile).toHaveBeenCalledWith('src/a.ts') - owner.inspectCall('a') + owner.inspectCall?.('a') expect(h.openView).toHaveBeenCalledWith('trajectory', 'a') }) diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index c64a330c57..bfa1ac0977 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/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-conversation/README.md -README.md: 11ef2b6bfbe0443d37eaabce17f7ee44c8d267ec -README.zh.md: 864332bc685ca496849bf92bf7905e8ecab9a328 +README.md: ac7c4b14d10acdf3addd7939244cd1f614daa42e +README.zh.md: 5755358e22f59c31c02078fa8081e2cdd754f736 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 11ef2b6bfb..ac7c4b14d1 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -40,7 +40,7 @@ The shared image slot props keep display choices separate from durable reference The context-occupancy button shows a ring and percentage below the input card, after the Session statistics. Clicking it opens the token breakdown in a panel kept inside the viewport, including when no statistics are shown; the button stays hidden until context usage and capacity are available. -With Developer tools off, the shell exposes only Chat and hides the View tab bar. Turning it off while another View is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes registered Views available again. +With Developer tools off, the shell exposes only Chat and hides the View tab bar. Turning it off while another View is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes registered Views available again. View owners receive the available roster so their navigation actions can follow the same visibility. The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 864332bc68..5755358e22 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -40,7 +40,7 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map, 上下文占用按钮在输入卡片下方、会话统计右侧显示圆环和百分比。点击按钮可在视口内的面板查看 token 构成,没有统计项时面板也不会越界;上下文用量和容量尚不可用时,按钮保持隐藏。 -关闭开发者工具时,外壳仅展示 Chat 并隐藏 View 标签栏。在其他 View 激活时关闭开关会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,已注册的 View 恢复可用。 +关闭开发者工具时,外壳仅展示 Chat 并隐藏 View 标签栏。在其他 View 激活时关闭开关会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,已注册的 View 恢复可用。View 所有者接收可用列表,使导航操作跟随相同的可见性。 输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 3c7c444a83..7879f8e388 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -34,7 +34,7 @@ import { ConversationPanel } from './skeleton/ConversationPanel.tsx' import { ConversationSession, ConversationSessionHeader } from './skeleton/ConversationSession.tsx' import { InputBar } from './skeleton/InputBar.tsx' import { todoDockEntry } from './skeleton/TodoPanel.tsx' -import { resolveActiveView } from './view-selection.ts' +import { DEFAULT_VIEW_ID, resolveActiveView } from './view-selection.ts' import { en, NS, zh, type ConversationKey } from './locales.ts' import { CONVERSATION_SETTINGS_NAMESPACE, type ConversationSettings } from '../submission-settings.ts' @@ -158,7 +158,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { for (const entry of slots.entries('conversation.view')) { /* v8 ignore next -- list registration validates id at load. */ if (entry.options.id === undefined) continue - if (!ctx.settingsScope.developerTools.enabled.getSnapshot() && entry.options.id !== 'chat') continue + if (!ctx.settingsScope.developerTools.enabled.getSnapshot() && entry.options.id !== DEFAULT_VIEW_ID) continue tabs.push({ id: entry.options.id, label: resolveSlotLabel(entry.options.label) ?? entry.options.id, diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index d37387ec4d..c82e273e3a 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -299,6 +299,8 @@ export interface InputZone { /** Conversation View entries obtain their data from registered standard hooks. */ export interface ConvViewOwnerProps { + /** Currently available navigation targets. */ + availableViews: readonly ViewTab[] /** Focus request addressed to the selected View. */ viewRequest: import('./views.ts').ConversationViewRequest | null /** Select a View and address one opaque focus identity to it. */ diff --git a/packages/client/ui-conversation/src/client/skeleton/DefaultConversationViews.tsx b/packages/client/ui-conversation/src/client/skeleton/DefaultConversationViews.tsx index 16d1aedc47..5f4d6ffe40 100644 --- a/packages/client/ui-conversation/src/client/skeleton/DefaultConversationViews.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/DefaultConversationViews.tsx @@ -36,6 +36,7 @@ export function DefaultConversationViews({ return (
{viewId !== undefined && renderSlot('conversation.view', { + availableViews: tabs, viewRequest, openView, completeViewRequest: actions.completeViewRequest, diff --git a/packages/client/ui-conversation/src/client/view-selection.ts b/packages/client/ui-conversation/src/client/view-selection.ts index 7639dbae03..0d777c9b7d 100644 --- a/packages/client/ui-conversation/src/client/view-selection.ts +++ b/packages/client/ui-conversation/src/client/view-selection.ts @@ -1,6 +1,7 @@ import type { ViewTab } from './contract/views.ts' -const DEFAULT_VIEW_ID = 'chat' +/** Chat remains available when developer tools are disabled. */ +export const DEFAULT_VIEW_ID = 'chat' /** * Resolve a preferred registered View, then Chat, without choosing another View. diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index cf6cf29590..da59f1a80c 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -214,6 +214,11 @@ describe('Conversation inject API', () => { expect(header.instance.store.getSnapshot().view).toBe('chat') removeTrajectory() + await b.runtime.flush() + activate.mockClear() + body.injected.openView('trajectory', 'hidden-call') + expect(activate).not.toHaveBeenCalled() + expect(body.instance.store.getSnapshot().view).toBe('chat') removeChat() await b.runtime.dispose() }) diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx index 39f656a5ac..2ae375104c 100644 --- a/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-conversation/tests/assembly-surfaces.client.spec.tsx @@ -79,7 +79,7 @@ function WorkspaceProbe({ open }: EmptyWorkspaceOwnerProps) { async function bench(opts?: { blank?: boolean }) { const runtime = await SlotTestRuntime.create() const openSession = provideWorkspaceNavigation(runtime) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) @@ -102,7 +102,7 @@ describe('resident composer', () => { it('renders the locked view state while no session exists at all', async () => { const runtime = await SlotTestRuntime.create() provideWorkspaceNavigation(runtime) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) @@ -129,7 +129,7 @@ describe('resident composer', () => { it('keeps the complete Hero tree mounted when the first Workspace session appears', async () => { const runtime = await SlotTestRuntime.create() const openSession = provideWorkspaceNavigation(runtime) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) @@ -196,7 +196,7 @@ describe('prompt rejection through the assembled composer', () => { it('renders the promptError alert strip and keeps the draft in the machine', async () => { const runtime = await SlotTestRuntime.create() const openSession = provideWorkspaceNavigation(runtime) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) diff --git a/packages/client/ui-jobs/tests/browser-plugin.client.spec.ts b/packages/client/ui-jobs/tests/browser-plugin.client.spec.ts index db6245819f..c5fb1571b6 100644 --- a/packages/client/ui-jobs/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-jobs/tests/browser-plugin.client.spec.ts @@ -34,7 +34,7 @@ async function bench(): Promise<{ ctx: Context; fiber: ReturnType () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() // These specs assert the shipped Chinese copy. There is no jsdom `window` in // this lane, so browser-language detection never runs and the locale comes diff --git a/packages/client/ui-layout/tests/apply.client.spec.ts b/packages/client/ui-layout/tests/apply.client.spec.ts index 74c58022c9..adda0cef68 100644 --- a/packages/client/ui-layout/tests/apply.client.spec.ts +++ b/packages/client/ui-layout/tests/apply.client.spec.ts @@ -60,7 +60,7 @@ async function bench() { ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) // ui-theme's Appearance row binds a durable scope through these two. ctx.provide('remote', { $on: () => () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) await ctx.plugin({ inject: themeInject, apply: themeApply }).await() await slotsFiber.await() const slots = ctx.get('slots') as SlotRegistry diff --git a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts index ada8829181..a8aea3c0f9 100644 --- a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts @@ -20,7 +20,7 @@ async function baseContext(): Promise { await ctx.plugin(SlotRegistry).await() ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) ctx.provide('remote', { $on: () => () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() return ctx } diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 8d04e90769..a485e99405 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: 7bc122f76b86ffdb93b90ab73823adde2ab301af -README.zh.md: 8f0a84cf216edf1eebb782fd8207cd0bb8a538cd +README.md: cf8bb5a75aa90842108646b57ceec67ada8ae711 +README.zh.md: 7df3eee13df7bd041cc5f09288d1b7fe28acded6 diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md index 7bc122f76b..cf8bb5a75a 100644 --- a/packages/client/ui-settings-general/README.md +++ b/packages/client/ui-settings-general/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use this package to give the dsh web client a Settings panel, connection-recovery control, feature-contributed navigation, and sequential first-run onboarding. Users can open it from the sidebar, retry a failed connection immediately, and access a local configuration file when the Host makes one available on a loopback browser. Feature packages supply their own settings rows, sections, and onboarding steps; this package supplies their shared presentation and does not add onboarding copy or built-in General rows. +Use this package to give the dsh web client a Settings panel, connection-recovery control, feature-contributed navigation, and sequential first-run onboarding. Users can open it from the sidebar, retry a failed connection immediately, and access a local configuration file when the Host makes one available on a loopback browser. Feature packages supply their own settings rows, sections, and onboarding steps; this package supplies their shared presentation and the Developer tools switch without adding onboarding copy. ## Table of Contents @@ -31,9 +31,9 @@ In Desktop, the account-row update control shows availability, progress, verific ### The General section -The Developer tools switch controls the shared preference described by [ui-settings](../ui-settings/README.md#use-this-package). It is available in both Web and desktop, follows accepted changes immediately, and disables duplicate input while a write settles. +The Developer tools switch controls the shared preference described by [ui-settings](../ui-settings/README.md#use-this-package). It is available in both Web and desktop, follows accepted changes immediately, and disables duplicate input while a write settles. A failed write displays localized retry guidance. -The General section holds rows registered into `settings.general.item` by feature packages — it has no built-in rows. Feature plugins own the row copy and behavior; the shell only provides the section and its slot. The Appearance row, for example, lives in ui-theme. +The General section holds the built-in Developer tools row and rows registered into `settings.general.item` by feature packages. Each registrant owns its row copy and behavior. The Appearance row, for example, lives in ui-theme. ### Opening the configuration file @@ -51,7 +51,7 @@ The onboarding ledger projects in ascending order and mounts exactly one step at
Implementation internals — click to expand -The shell owns the chrome and the projections; every piece of content and copy belongs to a registrant. +The shell owns the chrome and the projections; it contributes the Developer tools row, while feature registrants own their additional content and copy. ### Ledger projections @@ -102,7 +102,7 @@ None; this package neither assembles nor sends a provider request. These limits define what the shell itself provides versus what features must supply; they are current package constraints. -- **The General section has no built-in rows** — each row appears only when its owning feature plugin is mounted; the shell cannot fill the section alone. +- **Additional General rows require their feature plugins** — the shell supplies Developer tools; feature plugins supply the remaining preferences. ### Dev Note diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md index 8f0a84cf21..7df3eee13d 100644 --- a/packages/client/ui-settings-general/README.zh.md +++ b/packages/client/ui-settings-general/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用本包可为 dsh Web 客户端提供 Settings 面板、连接恢复控件、由功能包贡献的导航,以及依次进行的首次运行引导。用户可以从侧边栏打开面板、立即重试失败的连接,并在宿主为回环浏览器提供本地配置文件时访问该文件。各功能包提供自己的设置行、分区和引导步骤;本包提供共享的界面展示,但不添加引导文案或「通用」分区的内置行。 +使用本包可为 dsh Web 客户端提供 Settings 面板、连接恢复控件、由功能包贡献的导航,以及依次进行的首次运行引导。用户可以从侧边栏打开面板、立即重试失败的连接,并在宿主为回环浏览器提供本地配置文件时访问该文件。各功能包提供自己的设置行、分区和引导步骤;本包提供共享的界面展示和开发者工具开关,但不添加引导文案。 ## 目录 @@ -31,9 +31,9 @@ kind: "package-reference" ### 「通用」分区 -开发者工具开关控制 [ui-settings](../ui-settings/README.zh.md#use-this-package) 定义的共享偏好。Web 和桌面端均提供此开关,立即跟随已接受的变更,并在写入完成前禁用重复输入。 +开发者工具开关控制 [ui-settings](../ui-settings/README.zh.md#use-this-package) 定义的共享偏好。Web 和桌面端均提供此开关,立即跟随已接受的变更,并在写入完成前禁用重复输入。写入失败时显示本地化的重试提示。 -「通用」分区承载由功能包注册进 `settings.general.item` 的行——它没有内置行。功能插件拥有行文案与行为;外壳只提供分区及其 slot。例如「外观」行位于 ui-theme。 +「通用」分区承载内置的开发者工具行和功能包注册进 `settings.general.item` 的行。每个注册方拥有自己的行文案与行为。例如「外观」行位于 ui-theme。 ### 打开配置文件 @@ -51,7 +51,7 @@ kind: "package-reference"
实现细节——点击展开 -外壳拥有界面框架与投影;每段内容与文案都属于某个注册方。 +外壳拥有界面框架与投影,并提供开发者工具行;功能注册方拥有其余内容与文案。 ### 账本投影 @@ -102,7 +102,7 @@ kind: "package-reference" 这些限制说明外壳自身提供什么、功能包必须提供什么;它们是当前包约束。 -- **「通用」分区没有内置行**:每一行仅在其所属功能插件挂载时出现;外壳单独无法填满该分区。 +- **额外的通用设置行需要相应功能插件**:外壳提供开发者工具;功能插件提供其他偏好。 ### 开发备注 diff --git a/packages/client/ui-settings-general/src/client/DeveloperToolsRow.tsx b/packages/client/ui-settings-general/src/client/DeveloperToolsRow.tsx index 972aef2a08..bedc732892 100644 --- a/packages/client/ui-settings-general/src/client/DeveloperToolsRow.tsx +++ b/packages/client/ui-settings-general/src/client/DeveloperToolsRow.tsx @@ -20,15 +20,18 @@ export function DeveloperToolsRow({ useDeveloperTools, setEnabled, t }: PropsRuntime<'settings.general.item'> & PropsLocale<'settings'> & InjectFace) { const enabled = useDeveloperTools(value => value) const [busy, setBusy] = useState(false) + const [failed, setFailed] = useState(false) return
{t('developerTools.title')}
{t('developerTools.description')}
+ {failed &&
{t('developerTools.error')}
}
{ + setFailed(false) setBusy(true) - void setEnabled(next).finally(() => { setBusy(false) }) + void setEnabled(next).catch(() => { setFailed(true) }).finally(() => { setBusy(false) }) }} />
} diff --git a/packages/client/ui-settings-general/src/client/locales.ts b/packages/client/ui-settings-general/src/client/locales.ts index eb806470d2..e906f1714c 100644 --- a/packages/client/ui-settings-general/src/client/locales.ts +++ b/packages/client/ui-settings-general/src/client/locales.ts @@ -27,6 +27,7 @@ export const zh = { 'openDocument.error': '无法打开配置文件', 'general.nav': '通用设置', 'developerTools.title': '开发者工具', + 'developerTools.error': '保存失败,请重试', 'developerTools.description': '显示用于调试和排查问题的工具与信息', 'connection.error': '连接异常,刷新重试', 'connection.connecting': '重新连接中', @@ -65,6 +66,7 @@ export const en = { 'openDocument.error': 'Could not open configuration file', 'general.nav': 'General', 'developerTools.title': 'Developer tools', + 'developerTools.error': 'Could not save. Please try again.', 'developerTools.description': 'Show tools and information for debugging and troubleshooting', 'connection.error': 'Disconnected', 'connection.connecting': 'Reconnecting', 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 cf941fa13b..89c78fe1af 100644 --- a/packages/client/ui-settings-general/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-general/tests/components.client.spec.tsx @@ -208,3 +208,17 @@ describe('SettingsDocumentAction', () => { expect(screen.getByRole('button', { name: 'Open configuration file' })).toBeTruthy() }) }) + +it('reports a failed developer-tool write and allows retry', async () => { + const state = createSnapshotStore(false) + const setEnabled = vi.fn().mockRejectedValueOnce(new Error('offline')).mockImplementation(async (enabled: boolean) => { state.set(enabled) }) + render() + const toggle = screen.getByRole('switch', { name: 'Developer tools' }) + fireEvent.click(toggle) + expect((await screen.findByRole('alert')).textContent).toBe('Could not save. Please try again.') + expect(toggle.hasAttribute('disabled')).toBe(false) + expect(toggle.getAttribute('aria-checked')).toBe('false') + fireEvent.click(toggle) + await waitFor(() => { expect(toggle.getAttribute('aria-checked')).toBe('true') }) + expect(screen.queryByRole('alert')).toBeNull() +}) diff --git a/packages/client/ui-settings/README.i18n.yaml b/packages/client/ui-settings/README.i18n.yaml index 06a40f9d9f..12d06cb627 100644 --- a/packages/client/ui-settings/README.i18n.yaml +++ b/packages/client/ui-settings/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/README.md -README.md: 1f5d2dae13d0bdeb4e1eb776c0cb05629031e4e8 -README.zh.md: 768fa156dadb335e6571d16014ec97db414d9677 +README.md: 7bb05738e7100daaec622f6e34269c78526cdbce +README.zh.md: 6f332fcbc1c6ae8fa04818c65559c15043ee037b diff --git a/packages/client/ui-settings/README.md b/packages/client/ui-settings/README.md index 1f5d2dae13..7bb05738e7 100644 --- a/packages/client/ui-settings/README.md +++ b/packages/client/ui-settings/README.md @@ -29,7 +29,7 @@ Feature plugins use this package to store and edit their preferences without re- ### Binding a namespace -`ctx.settingsScope.developerTools` owns the shared Web and desktop preference `ui-developer-tools.enabled`, defaulting to `false`. Its `enabled` observable publishes accepted choices and `setEnabled` uses the same ordered settings writes. Desktop and loopback Web persist to the Host document; remote Web retains the existing in-memory settings policy. This controls presentation and HTML preview permissions, not Host authorization or Session recording. +`ctx.settingsScope.developerTools` owns the shared Web and desktop preference `ui-developer-tools.enabled`, defaulting to `false`. Its `enabled` observable publishes accepted choices and `setEnabled` uses the same ordered settings writes. Desktop and loopback Web persist to the Host document; remote Web keeps this choice in one browser-local observable until reload without issuing Host writes. This controls presentation and HTML preview permissions, not Host authorization or Session recording. A feature calls `ctx.settingsScope.bind(spec)` with a per-namespace spec and gets a scope derived from the shared document mirror. The scope snapshot carries the resolved section, composition `base`, raw `user`, revision, writability, and host/memory mode; a field is overridden when it is present in `user`, even when its value equals `base`, and `unset` clears that override. Writes go through the scope: `set` and `unset` submit one operation, while `mutate` submits several ordered operations atomically. Each write is fenced by the namespace revision as `expectedRevision`, so a concurrent write from another surface is refused instead of silently overwritten. A staged editor can supply the revision where its draft began as a fixed fence; otherwise the scope uses the latest queued or mirrored revision. @@ -53,7 +53,7 @@ The package realizes one ownership rule: the browser keeps one shared mirror of ### The describe mirror -The plugin injects `remote` with its `settings` namespace, resolves Host persistence once from the fixed `remote.$host` facts, and owns the one `settings.describe` reader in the browser: a shared mirror refreshed on every forwarded `settings/document-updated` event and on `connection/reset` (the first connection included, closing the window where a commit lands between the eager read and the SSE subscription). Cross-namespace surfaces read it through `ctx.settingsScope.describe()`, a read/fold face (`getSnapshot`/`subscribe`/`ensure`, plus `acceptView` folding a write answer in). +The Host half registers the `ui-developer-tools` schema and its default-off value with settings. The Client plugin injects `remote` with its `settings` namespace, resolves Host persistence once from the fixed `remote.$host` facts, and owns the one `settings.describe` reader in the browser: a shared mirror refreshed on every forwarded `settings/document-updated` event and on `connection/reset` (the first connection included, closing the window where a commit lands between the eager read and the SSE subscription). Cross-namespace surfaces read it through `ctx.settingsScope.describe()`, a read/fold face (`getSnapshot`/`subscribe`/`ensure`, plus `acceptView` folding a write answer in). ### Scope derivation @@ -96,7 +96,7 @@ None; this package neither assembles nor sends a provider request. These limits define where the settings transport cannot reach; they are current package constraints. -- **Non-loopback pages get no durable settings** — this Client keeps Host persistence disabled there, so a scope starts `unavailable` and never crosses the wire; every row it backs is inert even though Connection authentication covers the API. +- **Non-loopback pages get no durable settings** — this Client keeps Host persistence disabled there, so a scope starts `unavailable` and never crosses the wire; generic scope writes are inert even though Connection authentication covers the API. The shared Developer tools preference instead provides browser-local changes. ### Dev Note diff --git a/packages/client/ui-settings/README.zh.md b/packages/client/ui-settings/README.zh.md index 768fa156da..6f332fcbc1 100644 --- a/packages/client/ui-settings/README.zh.md +++ b/packages/client/ui-settings/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" ### 绑定命名空间 -`ctx.settingsScope.developerTools` 管理 Web 和桌面端共享的偏好 `ui-developer-tools.enabled`,默认为 `false`。其 `enabled` 可观察值发布已接受的选择,`setEnabled` 使用相同的有序设置写入。桌面端和回环 Web 将设置持久化到 Host 文档;远程 Web 保持现有的内存设置策略。此设置控制界面展示和 HTML 预览权限,不控制 Host 授权或 Session 记录。 +`ctx.settingsScope.developerTools` 管理 Web 和桌面端共享的偏好 `ui-developer-tools.enabled`,默认为 `false`。其 `enabled` 可观察值发布已接受的选择,`setEnabled` 使用相同的有序设置写入。桌面端和回环 Web 将设置持久化到 Host 文档;远程 Web 将此选择保存在单个浏览器本地可观察值中,刷新后重置,不发送 Host 写入。此设置控制界面展示和 HTML 预览权限,不控制 Host 授权或 Session 记录。 功能调用 `ctx.settingsScope.bind(spec)` 并传入按命名空间的 spec,得到一个由共享文档镜像派生的 scope。scope 快照携带解析后的分区、组合 `base`、原始 `user`、revision、可写性以及 host/内存模式;字段只要出现在 `user` 中即视为覆盖,即使其值与 `base` 相等,`unset` 会清除该覆盖。写入经 scope 进行:`set` 与 `unset` 提交一个操作,`mutate` 则原子提交多个有序操作。每次写入都以命名空间 revision 作为 `expectedRevision` 围栏,因此来自另一界面的并发写入会被拒绝,而不是被静默覆盖。暂存编辑器可以把开始草拟时读取的 revision 作为固定围栏传入;否则 scope 使用最新排队或镜像 revision。 @@ -53,7 +53,7 @@ kind: "package-reference" ### Describe 镜像 -插件注入 `remote` 及其 `settings` 命名空间,从固定的 `remote.$host` 事实一次性解析 Host 持久化模式,并持有浏览器中唯一的 `settings.describe` 读取方:一面共享镜像,在每次转发的 `settings/document-updated` 事件与 `connection/reset` 时刷新(首次连接也包含在内,关闭「提交落在急切读取与 SSE 订阅之间」的窗口)。跨命名空间表面通过 `ctx.settingsScope.describe()` 读它,这是一个读取/折叠面(`getSnapshot`/`subscribe`/`ensure`,另有把写应答折入的 `acceptView`)。 +Host 部分向 settings 注册 `ui-developer-tools` schema 及其默认关闭值。Client 插件注入 `remote` 及其 `settings` 命名空间,从固定的 `remote.$host` 事实一次性解析 Host 持久化模式,并持有浏览器中唯一的 `settings.describe` 读取方:一面共享镜像,在每次转发的 `settings/document-updated` 事件与 `connection/reset` 时刷新(首次连接也包含在内,关闭「提交落在急切读取与 SSE 订阅之间」的窗口)。跨命名空间表面通过 `ctx.settingsScope.describe()` 读它,这是一个读取/折叠面(`getSnapshot`/`subscribe`/`ensure`,另有把写应答折入的 `acceptView`)。 ### Scope 派生 @@ -96,7 +96,7 @@ kind: "package-reference" 这些限制说明设置传输层够不到的地方;它们是当前包约束。 -- **非 loopback 页面没有持久化设置**:本 Client 在那里禁用 Host 持久化,因此 scope 以 `unavailable` 起步且从不跨线路;尽管 Connection 认证覆盖 API,它支撑的每一行仍在那里无效。 +- **非 loopback 页面没有持久化设置**:本 Client 在那里禁用 Host 持久化,因此 scope 以 `unavailable` 起步且从不跨线路;尽管 Connection 认证覆盖 API,通用 scope 写入仍在那里无效。共享的开发者工具偏好单独提供浏览器本地变更。 ### 开发备注 diff --git a/packages/client/ui-settings/src/client/developer-tools.ts b/packages/client/ui-settings/src/client/developer-tools.ts index 5041d35fd1..e34897372a 100644 --- a/packages/client/ui-settings/src/client/developer-tools.ts +++ b/packages/client/ui-settings/src/client/developer-tools.ts @@ -1,5 +1,5 @@ /** One accepted preference drives every developer-tool consumer. */ -import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' +import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store' import type { DeveloperToolsSettings } from '../developer-tools-settings.ts' import type { SettingsScope } from './settings-contract.ts' @@ -7,21 +7,34 @@ import type { SettingsScope } from './settings-contract.ts' export class DeveloperToolsPreference { /** Accepted enablement, observable through renderer-bound hooks. */ readonly enabled: ObservableSnapshot + private readonly local = createSnapshotStore(false) /** @param scope - settings-owned namespace controller. */ constructor(private readonly scope: SettingsScope) { - this.enabled = { + this.enabled = scope.getSnapshot().mode === 'memory' ? this.local : { getSnapshot: () => scope.getSnapshot().value?.enabled ?? false, - subscribe: listener => scope.subscribe(listener), + subscribe: (listener) => { + let previous = this.enabled.getSnapshot() + return scope.subscribe(() => { + const next = this.enabled.getSnapshot() + if (next === previous) return + previous = next + listener() + }) + }, } } /** - * Persist a choice using the settings transport's ordered write and recovery policy. + * Persist a Host choice with ordered writes, or update the shared browser-local choice. * @param enabled - requested developer-tool mode. - * @returns settlement after the write or recovery read. + * @returns settlement after local publication, the Host write or its recovery read. */ setEnabled(enabled: boolean): Promise { + if (this.scope.getSnapshot().mode === 'memory') { + this.local.set(enabled) + return Promise.resolve() + } return this.scope.set('enabled', enabled) } } diff --git a/packages/client/ui-settings/tests/developer-tools.client.spec.ts b/packages/client/ui-settings/tests/developer-tools.client.spec.ts index 1b2f180a18..7d3344ffcf 100644 --- a/packages/client/ui-settings/tests/developer-tools.client.spec.ts +++ b/packages/client/ui-settings/tests/developer-tools.client.spec.ts @@ -48,3 +48,33 @@ describe('developer tools settings', () => { dispose() }) }) + +it('keeps remote browser choices local and publishes only changed values', async () => { + const host = stubSettingsScope() + host.publish({ mode: 'memory' }) + const preference = new DeveloperToolsPreference(host.scope) + const notify = vi.fn() + const dispose = preference.enabled.subscribe(notify) + expect(preference.enabled.getSnapshot()).toBe(false) + await preference.setEnabled(true) + expect(preference.enabled.getSnapshot()).toBe(true) + await preference.setEnabled(true) + expect(notify).toHaveBeenCalledOnce() + await preference.setEnabled(false) + expect(preference.enabled.getSnapshot()).toBe(false) + expect(host.set).not.toHaveBeenCalled() + dispose() +}) + +it('ignores host revisions that do not change enablement', () => { + const host = stubSettingsScope() + const preference = new DeveloperToolsPreference(host.scope) + const notify = vi.fn() + const dispose = preference.enabled.subscribe(notify) + host.publish({ revision: 1 }) + host.publish({ value: { enabled: false } }) + expect(notify).not.toHaveBeenCalled() + host.publish({ value: { enabled: true } }) + expect(notify).toHaveBeenCalledOnce() + dispose() +}) diff --git a/packages/client/ui-settings/tests/settings-scope.client.spec.ts b/packages/client/ui-settings/tests/settings-scope.client.spec.ts index 1eef32e6f0..1feed95b72 100644 --- a/packages/client/ui-settings/tests/settings-scope.client.spec.ts +++ b/packages/client/ui-settings/tests/settings-scope.client.spec.ts @@ -555,6 +555,12 @@ describe('SettingsScopeBinder.bind', () => { }) await fiber.await() expect(scope.getSnapshot()).toMatchObject({ status: 'unavailable', mode: 'memory', writable: false }) + const preference = ctx.settingsScope.developerTools + expect(fiber.ctx.settingsScope.developerTools).toBe(preference) + await preference.setEnabled(true) + expect(fiber.ctx.settingsScope.developerTools.enabled.getSnapshot()).toBe(true) + await fiber.ctx.settingsScope.developerTools.setEnabled(false) + expect(preference.enabled.getSnapshot()).toBe(false) await fiber.dispose() expect(describeCall).not.toHaveBeenCalled() }) diff --git a/packages/client/ui-sidebar-documentpreview/src/client/html/basic-document.ts b/packages/client/ui-sidebar-documentpreview/src/client/html/basic-document.ts index ded1af828d..9c3c28040f 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/html/basic-document.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/html/basic-document.ts @@ -7,17 +7,21 @@ import { decodeText } from './bytes.ts' * @returns document with the restrictive CSP first in its head. */ export function createBasicHtmlDocument(data: Uint8Array): string { - const template = document.createElement('template') - template.innerHTML = decodeText(data) - for (const element of template.content.querySelectorAll('script, base, meta[http-equiv], iframe, frame, object, embed')) { - element.remove() - } - for (const link of template.content.querySelectorAll('a, area')) { - link.removeAttribute('href') - link.removeAttribute('xlink:href') + const parsed = new DOMParser().parseFromString(decodeText(data), 'text/html') + const sanitize = (root: Document | DocumentFragment): void => { + for (const element of root.querySelectorAll('script, noscript, base, link, meta[http-equiv], iframe, frame, object, embed, set, animate, animateMotion, animateTransform')) { + element.remove() + } + for (const link of root.querySelectorAll('a, area')) { + link.removeAttribute('href') + link.removeAttribute('xlink:href') + } + for (const template of root.querySelectorAll('template')) sanitize(template.content) } + sanitize(parsed) const policy = document.createElement('meta') policy.setAttribute('http-equiv', 'Content-Security-Policy') policy.setAttribute('content', "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:; media-src data:; connect-src 'none'; frame-src 'none'; form-action 'none'; base-uri 'none'") - return `${policy.outerHTML}${template.innerHTML}` + parsed.head.prepend(policy) + return `${parsed.documentElement.outerHTML}` } diff --git a/packages/client/ui-sidebar-documentpreview/tests/basic-html.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/basic-html.client.spec.ts index 0545081a67..9f713ea78e 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/basic-html.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/basic-html.client.spec.ts @@ -21,3 +21,20 @@ it('preserves static content and removes navigation, policy overrides and active expect(parsed.querySelector('style')?.textContent).toContain('color: red') expect(parsed.querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,AA==') }) + +it('preserves document styling and removes navigation inside nested templates and SVG animation', () => { + const html = createBasicHtmlDocument(new TextEncoder().encode(` + +
+ + `)) + const parsed = new DOMParser().parseFromString(html, 'text/html') + expect(parsed.documentElement.lang).toBe('zh') + expect(parsed.documentElement.className).toBe('dark') + expect(parsed.body.style.margin).toBe('0px') + expect(parsed.body.dir).toBe('rtl') + expect(parsed.querySelectorAll('noscript, link, set, animate')).toHaveLength(0) + const nested = parsed.querySelector('template')!.content.querySelector('template')!.content + expect(nested.querySelector('a')!.hasAttribute('href')).toBe(false) + expect(nested.querySelector('iframe')).toBeNull() +}) diff --git a/packages/client/ui-subagent/tests/browser-plugin.client.spec.ts b/packages/client/ui-subagent/tests/browser-plugin.client.spec.ts index e2fa92ac49..7101c7f465 100644 --- a/packages/client/ui-subagent/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-subagent/tests/browser-plugin.client.spec.ts @@ -78,7 +78,7 @@ async function fullBench(sessions: SessionSummary[]) { }, } as never) ctx.provide('remote', { $on: () => () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) await provideSlotFaces(ctx) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() await ctx.plugin({ inject: [...inject], apply }).await() diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index ae29876c1d..04b554ad36 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/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-tool/README.md -README.md: 69a4c2d380ff1198e38e8ac7e72cdef90ca1da88 -README.zh.md: 27cbfbfe6ab8e956ec2268014e0549220fc2eeec +README.md: 067fa16bad9f6a34b26da3402de5e0a574323500 +README.zh.md: 9376493b780511c6e412cff4c5b976126c230693 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index 69a4c2d380..067fa16bad 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -62,7 +62,7 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool ### Cards -Every card is read in place in the call tree; there is no second, full-height presentation of a selected call. Row renderers share one pure card model for each terminal, read, diff, search, and web card, and the image card's gallery renders through the tool-owned `tool.call.images` slot. These models validate raw call arguments, result content, failure state, persisted metadata, PTC dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. A file-path summary opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar's text preview; `inspect` opens the trajectory view. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules. +Every card is read in place in the call tree; there is no second, full-height presentation of a selected call. Row renderers share one pure card model for each terminal, read, diff, search, and web card, and the image card's gallery renders through the tool-owned `tool.call.images` slot. These models validate raw call arguments, result content, failure state, persisted metadata, PTC dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. A file-path summary opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar's text preview; `inspect` opens the trajectory view and is absent when that View is unavailable; cards then omit Inspect. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules. Chat diff cards keep nine rows before folding, enough for a file header, one removed/added pair, and three context lines on either side. The collapsed row and expanded footer share the primitive's exact or coarse-replacement counts. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 27cbfbfe6a..9376493b78 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -62,7 +62,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block` ### 卡片 -每张卡片都直接在调用树中查看;选中调用后不会再显示第二个全高视图。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` slot 渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、PTC dispatch 的 `parentCallId` 与会话路径信息。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。 +每张卡片都直接在调用树中查看;选中调用后不会再显示第二个全高视图。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` slot 渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、PTC dispatch 的 `parentCallId` 与会话路径信息。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图;该视图不可用时不提供此回调,卡片随之隐藏 Inspect。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。 Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删除与新增行及两侧各三行上下文。折叠工具行与展开卡片底部采用原语一致的精确或粗粒度替换统计。 diff --git a/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx b/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx index f4d62697f7..6338f89f1c 100644 --- a/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx +++ b/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx @@ -29,7 +29,7 @@ const ToolCall = memo(function ToolCall({ cwd, home, loadImage, - inspect: () => { inspectCall(callId) }, + inspect: inspectCall === undefined ? undefined : () => { inspectCall(callId) }, }), [callId, toolName, block, openFile, cwd, home, loadImage, inspectCall]) const autoReviewDenied = useMemo( () => toolRowModel(toolName, block).autoReviewDenial !== null, diff --git a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx index 4b48f88c0b..50facd04d2 100644 --- a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx @@ -76,7 +76,7 @@ async function bench(nodes: ToolResultNode[]) { openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })), }, }) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { diff --git a/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx b/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx index 58a4eece75..b35e755512 100644 --- a/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx +++ b/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx @@ -80,6 +80,7 @@ function snapshotWith( /** Test-owned AppFrame role: declares and renders the Chat view list. */ type AppRootProps = PropsRenderSlots<'conversation.view'> const VIEW_OWNER: ConvViewOwnerProps = { + availableViews: [{ id: 'chat', label: 'Chat' }, { id: 'trajectory', label: 'Trajectory' }], viewRequest: null, openView: () => {}, completeViewRequest: () => {}, @@ -103,7 +104,7 @@ async function bench(snapshot: ChatSnapshot) { const chat = createSnapshotStore(snapshot) const events = new ConversationEventRegistry(ctx) const views = new ConversationViewRegistry(ctx) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) ctx.provide('uiConversation', { events, views, diff --git a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx index 093295b407..f8215ed61c 100644 --- a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx @@ -93,6 +93,12 @@ describe('ToolCallTree', () => { ]) }) + it('omits Inspect from tool owners when the target view is unavailable', () => { + const owners: ToolCallOwnerProps[] = [] + render() + expect(owners[0]?.inspect).toBeUndefined() + }) + it('dispatches a running call by its wire name and forwards inspect', () => { const owners: ToolCallOwnerProps[] = [] const block: ToolCallBlock = { diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx index c13ac0965a..9a7520117f 100644 --- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx @@ -67,7 +67,7 @@ async function bench(nodes: ToolResultNode[]) { const runtime = await SlotTestRuntime.create() const openWorkspacePath = vi.fn(async () => ({ ok: true, value: { opened: true } })) runtime.remote.provideNamespaces({ session: { openWorkspacePath } }) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const layout = { openDetails: vi.fn(), closeDetails: vi.fn() } runtime.ctx.provide('layout', layout) const sidebarRight = { openResource: vi.fn<(address: string) => void>() } @@ -288,7 +288,7 @@ describe('registrant declaration injection', () => { openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })), }, }) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { diff --git a/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts b/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts index e1ca41bfb2..82dd7573ce 100644 --- a/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts +++ b/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts @@ -89,7 +89,7 @@ describe('tsdown client artifact', () => { const { events, views } = uiConversation ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) ctx.provide('remote', { $on: () => () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const locale = await import('@deepseek-ai/dsh-client-locale/client') ctx.plugin({ inject: [...locale.inject], apply: locale.apply }) const fiber = ctx.plugin(exports as { apply: (ctx: Context) => void }) diff --git a/packages/client/ui-trajectory/tests/views.client.spec.tsx b/packages/client/ui-trajectory/tests/views.client.spec.tsx index e1707c6aee..05980a12ca 100644 --- a/packages/client/ui-trajectory/tests/views.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.client.spec.tsx @@ -234,6 +234,7 @@ function standaloneProps( useInput: bindSnapshotSelector(input), inputActions, useProjection, + availableViews: [{ id: 'chat', label: 'Chat' }, { id: 'trajectory', label: 'Trajectory' }], viewRequest: null, openView: () => {}, completeViewRequest: () => {}, @@ -290,7 +291,7 @@ async function bench(snapshot = historySnapshot(NODES)) { // The locale plugin backs the locale-aware view tab label ('locale' in // inject); its settings scope needs a connection handle. ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) await runtime.mount({ inject: [...localeInject], apply: localeApply }) const provide = vi.spyOn(ctx.uiSession, 'provide') const feature = await runtime.mount({ inject: [...inject], apply }) @@ -308,10 +309,10 @@ function tabsOf(slots: SlotRegistry): ViewTab[] { .map(e => ({ id: e.options.id!, label: resolveSlotLabel(e.options.label) ?? e.options.id! })) } -type ConvViewOwner = Pick +type ConvViewOwner = Pick function isConvViewOwner(owner: object): owner is ConvViewOwner { - return 'viewRequest' in owner + return 'availableViews' in owner && 'viewRequest' in owner && 'openView' in owner && typeof owner.openView === 'function' && 'completeViewRequest' in owner && typeof owner.completeViewRequest === 'function' } diff --git a/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx b/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx index 6c999db2b6..e43dbf163d 100644 --- a/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx +++ b/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx @@ -884,7 +884,7 @@ describe('plugin lifecycle', () => { await ctx.plugin(SlotRegistry).await() ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) ctx.provide('remote', { $on: () => () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope, developerTools: { enabled: { getSnapshot: () => true, subscribe: () => () => {} } } } as never) const sessions = new TestSessions(async (action) => { await action() }, ctx) ctx.provide('sessions', sessions) const openSession = vi.fn(async () => {}) diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 011791768c..de039295d5 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -241,7 +241,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Stable owner currency delivered to a keyed Chat renderer. */\nexport interface ChatNodeOwnerProps {\n cwd?: string | undefined\n /** Open the current source file of a skill referenced by a sent message. */\n openSkill: (name: string) => void\n openFile: (path: string, options?: OpenFileOptions) => void\n inspectCall: (callId: ToolCallId) => void\n forkAt: (seq: number) => void\n /**\n * Session-authorized image loader, down-threaded from the Chat view so a\n * chat-node renderer can render the attachment presentation slot directly\n * with only the durable references plus this loader, instead of receiving a\n * rendering closure.\n */\n loadImage: MessageImageLoader\n renderMessageImages: RenderMessageImages\n fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined\n /** Turn-process state when this Node belongs to a projected Turn. */\n turnProcess?: TurnProcessOwnerProps | undefined\n}', + '/** Stable owner currency delivered to a keyed Chat renderer. */\nexport interface ChatNodeOwnerProps {\n cwd?: string | undefined\n /** Open the current source file of a skill referenced by a sent message. */\n openSkill: (name: string) => void\n openFile: (path: string, options?: OpenFileOptions) => void\n inspectCall?: ((callId: ToolCallId) => void) | undefined\n forkAt: (seq: number) => void\n /**\n * Session-authorized image loader, down-threaded from the Chat view so a\n * chat-node renderer can render the attachment presentation slot directly\n * with only the durable references plus this loader, instead of receiving a\n * rendering closure.\n */\n loadImage: MessageImageLoader\n renderMessageImages: RenderMessageImages\n fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined\n /** Turn-process state when this Node belongs to a projected Turn. */\n turnProcess?: TurnProcessOwnerProps | undefined\n}', ], ownerPropsReferences: [ 'MarkdownFileMentions', @@ -1506,10 +1506,11 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Conversation View entries obtain their data from registered standard hooks. */\nexport interface ConvViewOwnerProps {\n /** Focus request addressed to the selected View. */\n viewRequest: import(\'./views.ts\').ConversationViewRequest | null\n /** Select a View and address one opaque focus identity to it. */\n openView: (view: string, focus: string) => void\n /** Acknowledge the current one-shot focus request. */\n completeViewRequest: () => void\n}', + '/** Conversation View entries obtain their data from registered standard hooks. */\nexport interface ConvViewOwnerProps {\n /** Currently available navigation targets. */\n availableViews: readonly ViewTab[]\n /** Focus request addressed to the selected View. */\n viewRequest: import(\'./views.ts\').ConversationViewRequest | null\n /** Select a View and address one opaque focus identity to it. */\n openView: (view: string, focus: string) => void\n /** Acknowledge the current one-shot focus request. */\n completeViewRequest: () => void\n}', ], ownerPropsReferences: [ 'ConversationViewRequest', + 'ViewTab', ], standardProps: [ 'useResource: UseResource', From 07e3619ec3344bfac54aa2eee620fdf0ef2c3ee2 Mon Sep 17 00:00:00 2001 From: zhc Date: Thu, 17 Sep 2026 18:32:59 +0800 Subject: [PATCH 07/28] test(web): enable developer tools in trajectory image fixture --- apps/web/tests/assembled-remote.ts | 22 +++++++++++++++++-- .../trajectory-image-display.expected.e2e.ts | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts index 69739dc7a7..36eab3cb42 100644 --- a/apps/web/tests/assembled-remote.ts +++ b/apps/web/tests/assembled-remote.ts @@ -71,7 +71,14 @@ interface ControlBaseline { interface CapturedFixture { readonly sessionList: { readonly ok: true; readonly value: { readonly items: readonly SessionSummary[] } } - readonly settingsDescribe: unknown + readonly settingsDescribe: { + readonly ok: true + readonly value: { + readonly writable: boolean + readonly hasDocument: boolean + readonly namespaces: readonly unknown[] + } + } readonly credentialsDescribe: unknown readonly modelCatalog: unknown readonly agentPresets: unknown @@ -90,6 +97,8 @@ interface CapturedFixture { } export interface AssembledRemoteOptions { + /** Supply an enabled Host preference for diagnostic View scenarios. */ + readonly developerTools?: boolean /** Return the fixture's image-dimension admission error from Session prompt. */ readonly rejectPrompt?: boolean } @@ -131,7 +140,16 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass const mock = RemoteMock.create().load(remoteDefaultResponses) mock.load({ unary: { - 'settings/describe': structuredClone(fixture.settingsDescribe), + 'settings/describe': options.developerTools === true + ? ok({ + ...fixture.settingsDescribe.value, + namespaces: [...fixture.settingsDescribe.value.namespaces, { + ns: 'ui-developer-tools', + schema: { type: 'object', dict: { enabled: { type: 'boolean' } } }, + value: { enabled: true }, applies: 'live', secrets: [], revision: 0, + }], + }) + : structuredClone(fixture.settingsDescribe), 'credentials/describe': structuredClone(fixture.credentialsDescribe), 'session/modelCatalog': structuredClone(fixture.modelCatalog), 'agentPresets/list': structuredClone(fixture.agentPresets), diff --git a/apps/web/tests/trajectory-image-display.expected.e2e.ts b/apps/web/tests/trajectory-image-display.expected.e2e.ts index 855e798e55..2a3ffc9aca 100644 --- a/apps/web/tests/trajectory-image-display.expected.e2e.ts +++ b/apps/web/tests/trajectory-image-display.expected.e2e.ts @@ -81,7 +81,7 @@ it('renders durable record images in the Trajectory details panel from the share configurable: true, value: () => {}, }) - mountAssembledApp() + mountAssembledApp({ remote: { developerTools: true } }) await openFixtureSession() const chatSrc = document.querySelector('[data-align="end"] img')?.getAttribute('src') if (chatSrc === null || chatSrc === undefined) throw new Error('chat gallery image missing') From 54e6aabf0c26ccead0c257b52af37fa6886cc414 Mon Sep 17 00:00:00 2001 From: zhc Date: Thu, 17 Sep 2026 18:35:05 +0800 Subject: [PATCH 08/28] test(client): cover developer tools row settings injection --- .../client/ui-settings-general/tests/apply.client.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) 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 659a494bcc..1d11e8ea30 100644 --- a/packages/client/ui-settings-general/tests/apply.client.spec.ts +++ b/packages/client/ui-settings-general/tests/apply.client.spec.ts @@ -13,6 +13,7 @@ import { createClientTest, type TestClient, webApp } from '@deepseek-ai/dsh-clie import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { LOCALE_SETTINGS_NAMESPACE, LocaleSettingsSchema } from '@deepseek-ai/dsh-client-locale/src/locale-settings.ts' import { inject } from '../src/client/index.ts' +import type { DeveloperToolsRowInjected } from '../src/client/DeveloperToolsRow.tsx' import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx' import { GeneralSection } from '../src/client/GeneralSection.tsx' import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx' @@ -103,6 +104,13 @@ describe('ui-settings-general apply', () => { // The shared developer-tool control belongs to General; onboarding remains feature-owned. expect(c.ctx.slots.entries('settings.general.item').filter(row => row.locale === NS).map(row => row.options.id)).toEqual(['developer-tools']) expect(c.ctx.slots.entries('settings.onboarding').filter(row => row.locale === NS)).toEqual([]) + const developerRow = c.ctx.slots.entries('settings.general.item').find(row => row.options.id === 'developer-tools')! + const developer = (developerRow.inject as unknown as () => DeveloperToolsRowInjected)() + expect(developer.hooks.developerTools).toBe(c.ctx.settingsScope.developerTools.enabled) + expect(developer.hooks.developerTools.getSnapshot()).toBe(false) + const setEnabled = vi.spyOn(c.ctx.settingsScope.developerTools, 'setEnabled').mockResolvedValue(undefined) + await developer.setEnabled(true) + expect(setEnabled).toHaveBeenCalledExactlyOnceWith(true) const { controller, hooks } = actionInjectedOf(c) expect(controller.store.getSnapshot().status).toBe('idle') expect(hooks.snapshot).toBe(controller.store) From caaaf89b1e39055d22b419dd645119aadb8dd4da Mon Sep 17 00:00:00 2001 From: zhc Date: Thu, 17 Sep 2026 20:34:33 +0800 Subject: [PATCH 09/28] fix(client): separate developer tool policy and harden static previews --- ...6-09-17-developer-tools-settings.i18n.yaml | 4 +- .../2026-09-17-developer-tools-settings.md | 8 +-- .../2026-09-17-developer-tools-settings.zh.md | 8 +-- THIRD_PARTY_NOTICES.md | 1 + apps/web/tests/document-preview.e2e.ts | 9 +++- apps/web/tests/scaffold.ts | 2 +- docs/subsystems/conversation.i18n.yaml | 4 +- docs/subsystems/conversation.md | 2 +- docs/subsystems/conversation.zh.md | 2 +- .../src/client/AgentPresetSeat.tsx | 10 ++-- .../ui-agent-preset/src/client/index.ts | 4 +- .../tests/apply.client.spec.ts | 2 +- .../tests/components.client.spec.tsx | 2 +- .../ui-chat/src/client/chat/ChatView.tsx | 9 ++-- .../ui-chat/src/client/contract/slots.ts | 2 +- .../tests/apply-inject.client.spec.tsx | 5 +- .../ui-chat/tests/chat-apply.client.spec.tsx | 5 +- .../ui-chat/tests/chat-view.client.spec.tsx | 4 +- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 2 +- packages/client/ui-conversation/README.zh.md | 2 +- .../ui-conversation/src/client/apply.ts | 39 +++++++++++---- .../src/client/contract/conversation.ts | 6 +++ .../src/client/contract/slots.ts | 9 ++-- .../skeleton/DefaultConversationViews.tsx | 5 +- .../tests/apply-inject.client.spec.tsx | 44 ++++++++++++++++- .../tests/apply-wiring.client.spec.tsx | 5 +- .../tests/assembly-surfaces.client.spec.tsx | 14 ++++-- .../tests/skeleton.client.spec.tsx | 1 + .../src/client/Deliverables.tsx | 8 +-- .../ui-deliverables/src/client/index.ts | 4 +- .../tests/deliverables.client.spec.tsx | 9 ++-- .../ui-deliverables/tsconfig.client.json | 3 ++ packages/client/ui-deliverables/tsconfig.json | 3 -- .../tests/browser-plugin.client.spec.ts | 5 +- .../ui-layout/tests/apply.client.spec.ts | 5 +- .../tests/browser-plugin.client.spec.ts | 5 +- .../ui-settings-general/src/client/index.ts | 6 +-- .../tests/apply.client.spec.ts | 6 +-- .../tests/shell.client.spec.ts | 2 +- .../tests/stores.client.spec.ts | 2 + packages/client/ui-settings/README.i18n.yaml | 4 +- packages/client/ui-settings/README.md | 4 +- packages/client/ui-settings/README.zh.md | 4 +- .../ui-settings/src/client/developer-tools.ts | 25 +++++++--- .../client/ui-settings/src/client/index.ts | 4 ++ .../src/client/settings-contract.ts | 15 +++--- .../ui-settings/src/client/settings-scope.ts | 32 ++++++------ .../tests/developer-tools.client.spec.ts | 49 +++++++++++++++++-- .../tests/settings-scope.client.spec.ts | 12 ++--- .../README.i18n.yaml | 4 +- .../ui-sidebar-documentpreview/README.md | 2 +- .../ui-sidebar-documentpreview/README.zh.md | 2 +- .../ui-sidebar-documentpreview/package.json | 7 +-- .../src/client/html/HtmlBody.tsx | 10 ++-- .../src/client/html/basic-document.ts | 21 +++----- .../src/client/html/index.ts | 2 +- .../src/client/index.ts | 2 +- .../tests/apply.client.spec.ts | 4 +- .../tests/basic-html.client.spec.ts | 13 +++++ .../tests/document-seat.client.spec.tsx | 4 +- .../tests/html-apply.client.spec.ts | 5 +- .../tests/html-body.client.spec.tsx | 4 +- .../tests/browser-plugin.client.spec.ts | 5 +- .../tests/assembly-surfaces.client.spec.tsx | 5 +- .../tests/chat-ptc-subcalls.client.spec.tsx | 7 +-- .../tests/toolview-slot.client.spec.tsx | 8 +-- .../src/client/trajectory-snapshot-builder.ts | 1 + .../tests/client-bundle.client.spec.ts | 5 +- .../conversation-definitions.client.spec.ts | 4 ++ .../ui-trajectory/tests/views.client.spec.tsx | 12 +++-- .../tests/workflow-run.client.spec.tsx | 5 +- .../src/client/slot-catalog.ts | 5 +- .../client-runtime/README.i18n.yaml | 4 +- .../test-support/client-runtime/README.md | 2 +- .../test-support/client-runtime/README.zh.md | 2 +- .../test-support/client-runtime/src/index.ts | 2 +- .../client-runtime/src/settings-scope.ts | 25 ++++++++-- .../tests/runtime.client.spec.tsx | 17 ++++++- pnpm-lock.yaml | 3 ++ scripts/gen-cordis-catalog.ts | 1 + 81 files changed, 404 insertions(+), 210 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml index 43b546d998..fe05798168 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.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 .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md -2026-09-17-developer-tools-settings.md: 694e2a09fc20ad3185ba9c09058e8684a2247e46 -2026-09-17-developer-tools-settings.zh.md: e03aa258675be8f49dc13a9b43f2e3e00c8b85fa +2026-09-17-developer-tools-settings.md: 62ec235d8d21ad621956363898da26935e1af487 +2026-09-17-developer-tools-settings.zh.md: b320c973be0e8b4d4a061db00d871d7e95f5460e diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md index 694e2a09fc..62ec235d8d 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md @@ -10,11 +10,11 @@ Diagnostic views, preset selection and changed-file summaries add complexity to ## Decision -The settings owner exposes one accepted `ui-developer-tools.enabled` preference, defaulting off. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes. +The dedicated `ctx.developerTools` service binds the generic settings scope and exposes one accepted `ui-developer-tools.enabled` preference, defaulting off. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes. -Off exposes only Chat, hides tool Inspect actions whose trajectory target is unavailable, removes the conversation View tab bar, hides the new-session preset chip, and omits the changed-files card and its summary read. Turning it off from another View activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. +Off exposes only Chat, hides tool Inspect actions when no visible View declares tool-call inspection, removes the conversation View tab bar, hides the new-session preset chip, and omits the changed-files card and its summary read. Turning it off from another View activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. -The builtin HTML renderer enforces its own preview policy. Off parses a complete inert document, preserves root styling, recursively sanitizes template contents, removes active documents, resource hints, SVG animation and navigation, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. +Plugin assembly translates the shared preference into component-specific capabilities (`showCodeDiff`, `showPresetPicker`, and `interactivePreview`). Conversation supplies the inspection callback using the visible target's `toolCallFocus`; Chat does not name the target. The builtin HTML renderer uses DOMPurify for complete-document sanitization and parser-mutation defenses. Off preserves root styling, removes active documents, resource hints, SVG animation and all `href` attributes, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. ## Alternatives considered @@ -26,4 +26,4 @@ The builtin HTML renderer enforces its own preview policy. Off parses a complete Basic HTML sacrifices JavaScript, external and related resources, and link navigation; inline styles and data images remain available. Advanced mode retains normal browser networking and does not gain parent-origin, popup, form or top-navigation privileges. The setting does not constrain third-party preview implementations or revoke Host filesystem authority. -Component and settings tests cover defaulting, schema rejection, accepted-state updates, chip disclosure, changed-file visibility and iframe disposal. The recorded-session [document preview](../../../../apps/web/tests/document-preview.e2e.ts) and [changed-files](../../../../apps/web/tests/changed-files-turn.e2e.ts) scenarios exercise the real settings control and the shared renderer. The browser scaffold explicitly seeds developer mode for diagnostic scenarios; settings chrome, document preview and changed-file scenarios leave the shipped default untouched. Native Electron execution remains unverified. +Component and settings tests cover defaulting, schema rejection, accepted-state updates, chip disclosure, changed-file visibility and iframe disposal. The recorded-session [document preview](../../../../apps/web/tests/document-preview.e2e.ts) and [changed-files](../../../../apps/web/tests/changed-files-turn.e2e.ts) scenarios exercise the real settings control and the shared renderer. The browser scaffold enables developer tools by default. Only settings chrome, document preview, changed-files and developer-tools scenarios explicitly retain the shipped default; together they cover hidden diagnostic controls, static previews, and switching back to Chat. Refused Host writes recover accepted state and show the localized retry message. Native Electron execution remains unverified. diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md index e03aa25867..b320c973be 100644 --- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md @@ -10,11 +10,11 @@ Status: implemented ## Decision -设置所有者提供一个已接受的 `ui-developer-tools.enabled` 偏好,默认关闭。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。 +独立的 `ctx.developerTools` 服务绑定通用设置 scope,提供一个已接受的 `ui-developer-tools.enabled` 偏好,默认关闭。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。 -关闭时仅展示 Chat,隐藏轨迹目标不可用的工具 Inspect 操作,移除会话 View 标签栏,隐藏新会话预设入口,并省略改动文件卡片及其摘要读取。在其他 View 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 +关闭时仅展示 Chat,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,移除会话 View 标签栏,隐藏新会话预设入口,并省略改动文件卡片及其摘要读取。在其他 View 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 -内置 HTML 渲染器执行自己的预览策略。关闭时,内容被解析为完整的惰性文档,保留根节点样式,递归清理 template 内容,移除主动文档、资源提示、SVG 动画与导航,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 +插件组装层将共享偏好转换为组件自身的能力(`showCodeDiff`、`showPresetPicker` 和 `interactivePreview`)。Conversation 根据可见目标的 `toolCallFocus` 提供检查回调;Chat 不再指定目标名称。内置 HTML 渲染器使用 DOMPurify 清理完整文档并防护解析变异。关闭时保留根节点样式,移除主动文档、资源提示、SVG 动画及所有 `href` 属性,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 ## Alternatives considered @@ -26,4 +26,4 @@ Status: implemented 基础 HTML 放弃 JavaScript、外部与关联资源和链接导航;行内样式与 data 图片仍可用。高级模式保留正常浏览器网络能力,不增加父源、弹窗、表单或顶层导航权限。此设置不约束第三方预览实现,也不撤销 Host 文件系统读取权限。 -组件和设置测试覆盖默认值、schema 拒绝、已接受状态更新、入口折叠、改动文件展示和 iframe 清理。基于已记录 Session 的[文档预览](../../../../apps/web/tests/document-preview.e2e.ts)与[改动文件](../../../../apps/web/tests/changed-files-turn.e2e.ts)场景验证真实设置控件及共用渲染器。浏览器脚手架为诊断场景显式开启开发者模式;设置外壳、文档预览和改动文件场景保留产品默认值。原生 Electron 执行尚未验证。 +组件和设置测试覆盖默认值、schema 拒绝、已接受状态更新、入口折叠、改动文件展示和 iframe 清理。基于已记录 Session 的[文档预览](../../../../apps/web/tests/document-preview.e2e.ts)与[改动文件](../../../../apps/web/tests/changed-files-turn.e2e.ts)场景验证真实设置控件及共用渲染器。浏览器脚手架默认开启开发者工具,仅设置外壳、文档预览、改动文件和开发者工具场景显式保留产品默认值;这些场景共同覆盖诊断控件隐藏、静态预览和切回 Chat。Host 拒绝写入后会恢复已接受状态,并显示本地化重试提示。原生 Electron 执行尚未验证。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 7eb0c36e18..ec698dca33 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -76,6 +76,7 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`commander`](https://github.com/tj/commander.js) | MIT | | [`compression`](https://github.com/expressjs/compression) | MIT | | [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause | +| [`dompurify`](https://github.com/cure53/DOMPurify) | (MPL-2.0 OR Apache-2.0) | | [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT | | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | | [`execa`](https://github.com/sindresorhus/execa) | MIT | diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 57d513c72a..bee048c828 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -155,6 +155,8 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', ()
SVG link + x +