diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml index b681c0c699..a69f80895a 100644 --- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml +++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml @@ -211,6 +211,9 @@ # ── remaining model-facing rows ───────────────────────────────────────────── +- id: tool-ask-user + name: '@deepseek-ai/dsh-tool-ask-user' + - id: tool-todo name: '@deepseek-ai/dsh-tool-todo' diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml index b79c0b3975..e029517f91 100644 --- a/apps/cli/config/agent-presets/standard/agent.cordis.yml +++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml @@ -226,6 +226,9 @@ # ── remaining model-facing rows ───────────────────────────────────────────── +- id: tool-ask-user + name: '@deepseek-ai/dsh-tool-ask-user' + - id: tool-todo name: '@deepseek-ai/dsh-tool-todo' diff --git a/apps/cli/tests/web-agent-presets.spec.ts b/apps/cli/tests/web-agent-presets.spec.ts index 4d2a23bd00..5a9ba88d35 100644 --- a/apps/cli/tests/web-agent-presets.spec.ts +++ b/apps/cli/tests/web-agent-presets.spec.ts @@ -70,11 +70,13 @@ beforeAll(async () => { }, 120_000) describe('the shipped Web composition', () => { - it('leaves only the host UI tool in the global layer', () => { - // `ask_user_question` is the host's own interaction surface, not an agent - // capability, so it stays global. Every other tool now belongs to a - // preset; a regression here means an agent-plane row came back to base. - expect(toolNames(ctx)).toEqual(['ask_user_question']) + it('leaves the global tool layer empty', () => { + // Every model-facing tool belongs to a preset, `ask_user_question` + // included: a tool in the global layer reaches EVERY agent regardless of + // which preset composed it, so a two-tool benchmark surface would really + // present three. A regression here means an agent-plane row came back to + // the host composition. + expect(toolNames(ctx)).toEqual([]) }) it('supplies both shipped presets, and only those, from the system root', async () => { @@ -114,7 +116,8 @@ describe('the shipped Web composition', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined), }) try { - expect(toolNames(ctx, handle.agent)).toEqual(['ask_user_question', 'bash', 'str_replace_editor']) + // Exactly what the preset lists — nothing arrives from the host. + expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor']) } finally { await handle.dispose() } @@ -130,14 +133,14 @@ describe('the shipped Web composition', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined), }) try { - expect(toolNames(ctx, minimal.agent)).toEqual(['ask_user_question', 'bash', 'str_replace_editor']) + expect(toolNames(ctx, minimal.agent)).toEqual(['bash', 'str_replace_editor']) expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10) await minimal.dispose() // Tearing the minimal session down leaves the full one whole. expect(toolNames(ctx, full.agent).length).toBeGreaterThan(10) - expect(toolNames(ctx)).toEqual(['ask_user_question']) + expect(toolNames(ctx)).toEqual([]) } finally { await full.dispose() } @@ -351,7 +354,7 @@ describe('authoring a preset on the shipped composition', () => { try { // The same tools the shipped `core-web` composes, from a file written // through the service into a root outside the installed harness. - expect(toolNames(authorCtx, handle.agent)).toEqual(['ask_user_question', 'bash', 'str_replace_editor']) + expect(toolNames(authorCtx, handle.agent)).toEqual(['bash', 'str_replace_editor']) } finally { await handle.dispose() } @@ -388,7 +391,7 @@ describe('the default preset as a user setting', () => { try { // `mount()` with no id resolves the effective default. Two tools, not // `standard`'s catalog: the setting decided the composition. - expect(toolNames(ctx, handle.agent)).toEqual(['ask_user_question', 'bash', 'str_replace_editor']) + expect(toolNames(ctx, handle.agent)).toEqual(['bash', 'str_replace_editor']) } finally { await handle.dispose() } diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 88a429f14e..7a1f74328f 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2491,7 +2491,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-client-ui-models` ([`packages/client/ui-models/src/index.ts`](../packages/client/ui-models/src/index.ts)) - `@deepseek-ai/dsh-client-ui-permission` ([`packages/client/ui-permission/src/index.ts`](../packages/client/ui-permission/src/index.ts)) - `@deepseek-ai/dsh-client-ui-plan` ([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-question` — requires `tools` · `userInteraction` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-question` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts)) - `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts)) - `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts)) diff --git a/packages/client/ui-question/README.i18n.yaml b/packages/client/ui-question/README.i18n.yaml index bca51d908b..d7a62af6a4 100644 --- a/packages/client/ui-question/README.i18n.yaml +++ b/packages/client/ui-question/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-question/README.md -README.md: 72d94396771eec0a90b96008b1fd5e4a736a398c -README.zh.md: 6344327d268f1d0c2ec0aaaf29657ea040e51691 +README.md: 00763e03234468e80dce56d99e6abb719a0d97e3 +README.zh.md: b0dd70264e885c73d7be76add800326e9740ef44 diff --git a/packages/client/ui-question/README.md b/packages/client/ui-question/README.md index 72d9439677..d31ceb62c4 100644 --- a/packages/client/ui-question/README.md +++ b/packages/client/ui-question/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Web `ask_user_question` feature plugin. Its host half mounts `dsh-tool-ask-user` only when the Web feature is selected; its browser half registers the `question` entry in the conversation-owned `conversation.composer` keyed slot. +Web question feature plugin: its browser half registers the `question` entry in the conversation-owned `conversation.composer` keyed slot. Its host half is empty on purpose — mounting `dsh-tool-ask-user` there put the tool in the registry's GLOBAL layer, which merges into every agent regardless of the preset that composed it, so a two-tool benchmark preset really presented three. Rendering a question is a host UI capability; having the tool is an agent capability, so the `tool-ask-user` row belongs to the presets that want it (and to the TUI composition, which has no presets). The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges derived from label suffixes, and custom answers. A multi-select draft keeps its selected labels while the user opens or edits the custom answer, so its submitted item may carry both `selected` and `custom`; a single-select custom answer remains exclusive. Question detail reuses the assistant-output `MarkdownText` primitive, including its GFM rendering and untrusted-content policy. The capped card keeps its title, navigation, and submission actions fixed while long detail and choices share an internal scroll region. Single-select choices advance immediately, and Enter submits once every question is answered or skipped; Enter during IME composition confirms the input candidate without advancing. It submits one structured answer batch for the whole request: “Skip this question” retains other drafts and emits the existing blank `{ selected: [] }` shape for that item, while close rejects the whole wait as `ASK_CANCELLED`. diff --git a/packages/client/ui-question/README.zh.md b/packages/client/ui-question/README.zh.md index 6344327d26..9f9ad01c3f 100644 --- a/packages/client/ui-question/README.zh.md +++ b/packages/client/ui-question/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -Web `ask_user_question` 功能插件。只有选择 Web 功能时,其主机侧才会挂载 `dsh-tool-ask-user`;浏览器侧会把 `question` 配置项注册到会话拥有的 `conversation.composer` 键控 slot 中。 +Web 提问功能插件:其浏览器侧把 `question` 配置项注册到会话拥有的 `conversation.composer` 键控 slot 中。其主机侧刻意为空——在那里挂载 `dsh-tool-ask-user` 会把工具放进注册表的**全局层**,而全局层会并入每一个 agent,无论它由哪个 preset 组装,于是一个"两工具"的 benchmark preset 实际会呈现三个。渲染提问是宿主的 UI 能力,拥有该工具则是 agent 的能力,因此 `tool-ask-user` 行属于需要它的各个 preset(以及没有 preset 的 TUI 组装)。 组件每次渲染一个问题,提供进度导航、单选和多选选项、由标签后缀派生的推荐徽标,以及自定义答案。用户打开或编辑自定义答案时,多选题草稿会保留已选中的标签,因此提交项可以同时携带 `selected` 与 `custom`;单选题的自定义答案仍保持互斥。问题详情复用助手输出的 `MarkdownText` 原语,包括其 GFM 渲染与不受信任内容策略。限高卡片保持标题、导航与提交动作固定,超长的详情与选项共享内部滚动区。单选选项会立即前进;所有问题均已回答或跳过后,Enter 会提交;IME 输入法组合期间按 Enter 只会确认输入候选,不会前进。组件为整个请求提交一批结构化答案:「跳过此问题」会保留其他草稿,并为该项发出既有的空 `{ selected: [] }` 形状;关闭则以 `ASK_CANCELLED` 拒绝整个等待。 diff --git a/packages/client/ui-question/package.json b/packages/client/ui-question/package.json index 7416596284..7e129c9731 100644 --- a/packages/client/ui-question/package.json +++ b/packages/client/ui-question/package.json @@ -40,7 +40,6 @@ "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", - "@deepseek-ai/dsh-tool-ask-user": "workspace:^", "clsx": "^2.0.0", "react": "^18.2.0" }, diff --git a/packages/client/ui-question/src/index.ts b/packages/client/ui-question/src/index.ts index 901e832c14..4ceb9e0bd1 100644 --- a/packages/client/ui-question/src/index.ts +++ b/packages/client/ui-question/src/index.ts @@ -1,17 +1,14 @@ /** - * Web question plugin, node half: enabling this UI feature also exposes the - * model-facing ask_user_question tool on the host composition. + * Web question plugin, node half. + * + * Deliberately empty. Mounting `ask_user_question` here put it in the tools + * registry's GLOBAL layer, so every agent saw it no matter which preset + * composed it — a two-tool benchmark preset actually presented three, and a + * locally authored `bash-only` preset presented two. Rendering a question is + * a host UI capability; having the tool is an agent capability, and only a + * preset decides that. The `tool-ask-user` row belongs in the presets that + * want it (and in the TUI composition, which has no presets). */ -import type { Context } from 'cordis' -import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user' -/** Host services required by the model-facing tool. */ -export const inject = ['tools', 'userInteraction'] - -/** - * Mount ask_user_question for hosts that selected the Web question plugin. - * @param ctx - Host plugin context carrying tools and userInteraction. - */ -export function apply(ctx: Context): void { - toolAskUser.apply(ctx) -} +/** Host plugin body — the model-facing tool is composed per preset, not here. */ +export function apply(): void {} diff --git a/packages/client/ui-question/tests/node-plugin.spec.ts b/packages/client/ui-question/tests/node-plugin.spec.ts index 9bc34e9599..4602ef0bed 100644 --- a/packages/client/ui-question/tests/node-plugin.spec.ts +++ b/packages/client/ui-question/tests/node-plugin.spec.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it } from 'vitest' import ToolRegistry from '@deepseek-ai/dsh-tools' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' -import { apply, inject } from '../src/index.ts' +import { apply } from '../src/index.ts' let ctx: Context | undefined @@ -13,16 +13,19 @@ afterEach(async () => { }) describe('ui-question node plugin', () => { - it('exposes ask_user_question only for the selected Web feature lifecycle', async () => { + it('mounts no model-facing tool', async () => { ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(UserInteractionService) - const feature = ctx.plugin({ inject: [...inject], apply }) - await feature.await() - expect(ctx.tools.get('ask_user_question')).toBeDefined() - await feature.dispose() + await ctx.plugin({ apply }).await() + + // Selecting the Web question FEATURE must not hand every agent the tool. + // `ctx.tools.register` on an unscoped host context files into the global + // layer, which merges into every agent's view regardless of the preset + // that composed it — so a two-tool benchmark preset would really present + // three. The `tool-ask-user` row belongs to the presets that want it. expect(ctx.tools.get('ask_user_question')).toBeUndefined() }) }) diff --git a/packages/client/ui-question/tsconfig.json b/packages/client/ui-question/tsconfig.json index 6b5b0acc3a..72d4e96b0f 100644 --- a/packages/client/ui-question/tsconfig.json +++ b/packages/client/ui-question/tsconfig.json @@ -29,9 +29,7 @@ { "path": "../ui-slots" }, - { - "path": "../../ui/tool-ask-user" - }, + { "path": "../../support/invariants" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b068a990f..c1ccd303c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1917,9 +1917,6 @@ importers: '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots - '@deepseek-ai/dsh-tool-ask-user': - specifier: workspace:^ - version: link:../../ui/tool-ask-user clsx: specifier: ^2.0.0 version: 2.1.1