From 3c26fdd204e541313fa6f952593b619c328c5ebb Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:05:57 -0700 Subject: [PATCH 01/10] feat(web): keep standard and creator presets available without coding tools --- apps/web/tests/agent-preset-selection.e2e.ts | 10 ++- .../web/tests/developer-tools-settings.e2e.ts | 76 +++++++++++++++---- .../desktop-locale/automatic.expected.md | 2 +- .../desktop-locale/selected.expected.md | 2 +- .../settings-chrome/dialog-en.expected.md | 2 +- .../dialog-no-browser.expected.md | 2 +- .../settings-chrome/dialog.expected.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 | 29 ++++--- .../src/client/AgentPresetSection.tsx | 15 ++-- .../ui-agent-preset/src/client/index.ts | 16 +--- .../ui-agent-preset/src/client/seat-store.ts | 17 +---- .../tests/apply.client.spec.ts | 51 +++---------- .../tests/components.client.spec.tsx | 23 +++++- .../tests/section.client.spec.tsx | 27 ++++--- .../tests/settings-store.client.spec.ts | 30 +++----- .../ui-settings-general/src/client/locales.ts | 4 +- 19 files changed, 152 insertions(+), 164 deletions(-) diff --git a/apps/web/tests/agent-preset-selection.e2e.ts b/apps/web/tests/agent-preset-selection.e2e.ts index 48f1722d7c..197757d32b 100644 --- a/apps/web/tests/agent-preset-selection.e2e.ts +++ b/apps/web/tests/agent-preset-selection.e2e.ts @@ -343,7 +343,7 @@ describe('web e2e: agent-preset selection', () => { await writeComposerDraft(page, composer, '') }, 90_000) - it('keeps the saved default composing sessions while Developer tools only gate the choice', async () => { + it('keeps the saved default composing sessions while Coding Tools hide PTC and Minimal choices', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-disabled')) await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('standard') @@ -359,8 +359,12 @@ describe('web e2e: agent-preset selection', () => { await expect.poll(() => developerTools.getAttribute('aria-checked')).toBe('false') await dialog.getByRole('button', { name: 'Close' }).last().click() - // The gate hides the choice; the blank task keeps its saved composition. - await expect.poll(() => page.getByRole('button', { name: / mode$/ }).count()).toBe(0) + // A selected hidden option keeps its label and composition until another choice is made. + await page.getByRole('button', { name: 'Minimal mode', exact: true }).click() + await page.getByRole('menu').waitFor() + expect(await page.getByRole('menuitem', { name: /^PTC mode|^Minimal mode/ }).count()).toBe(0) + expect(await page.getByRole('menuitem', { name: /^Standard mode|^Creator mode/ }).count()).toBe(2) + await page.keyboard.press('Escape') await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal') await openSettings(page, 'en') diff --git a/apps/web/tests/developer-tools-settings.e2e.ts b/apps/web/tests/developer-tools-settings.e2e.ts index 40481128d2..8e5272d5f2 100644 --- a/apps/web/tests/developer-tools-settings.e2e.ts +++ b/apps/web/tests/developer-tools-settings.e2e.ts @@ -1,17 +1,23 @@ -/** Developer tools default on for a fresh Host and survive browser reload through the ordinary settings UI. */ +/** Coding Tools persist while Standard, Creator, and custom presets remain selectable when disabled. */ import { readFile } from 'node:fs/promises' import { join } from 'node:path' +import type {} from '@deepseek-ai/dsh-api-session-controller' import { chromium } from 'playwright' import { expect, it, onTestFinished } from 'vitest' -import { launchWebScaffold } from './scaffold.ts' -import { openSettings, newEnglishPage } from './support.ts' +import { launchWebScaffold, watchConsole } from './scaffold.ts' +import { connectFreshWorkspace, openSettings, newEnglishPage } from './support.ts' -it('persists developer tools in the Host settings document and restores the accepted choice', async () => { - const scaffold = await launchWebScaffold() +it('persists Coding Tools and limits only the built-in PTC and Minimal session choices', async () => { + const scaffold = await launchWebScaffold({ + agentPresets: { default: 'standard', definitions: [{ id: 'custom', name: 'Custom mode', plugins: [] }] }, + }) onTestFinished(() => scaffold.close()) const browser = await chromium.launch() onTestFinished(() => browser.close()) const page = await newEnglishPage(browser) + const tripwire = watchConsole(page) + const livePresets = async () => (await scaffold.ctx.sessionController.list({}, new AbortController().signal)) + .items.map(item => item.projections?.values.agentPreset) await page.goto(scaffold.authenticatedUrl) await openSettings(page, 'en') const toggle = page.getByRole('switch', { name: 'Coding Tools' }) @@ -26,17 +32,55 @@ it('persists developer tools in the Host settings document and restores the acce await page.getByRole('button', { name: 'Agent presets', exact: true }).click() await expect.poll(() => page.getByRole('heading', { name: 'Agent presets', exact: true }).count()).toBe(1) - // The page owns no selection switch: only the General one gates the choice. - const section = page.locator('section') - .filter({ has: page.getByRole('heading', { name: 'Agent presets', exact: true }) }) - const refused = section.getByRole('button', { name: /^Turn on Coding Tools in General settings to choose a default: / }) - expect(await section.getByRole('switch').count()).toBe(0) - await expect.poll(() => refused.count()).toBeGreaterThan(0) + const settings = page.getByRole('dialog', { name: 'Settings', exact: true }) + expect(await settings.getByRole('switch').count()).toBe(0) + await expect.poll(() => settings.locator('[data-agent-preset-id]').count()).toBe(5) + await settings.getByRole('button', { name: 'Set as new task default: Minimal mode', exact: true }).click() + await settings.getByRole('button', { name: 'New task default: Minimal mode', exact: true }).waitFor() + await settings.getByRole('button', { name: 'Set as new task default: Standard mode', exact: true }).click() + await settings.getByRole('button', { name: 'New task default: Standard mode', exact: true }).waitFor() + expect(await settings.getByRole('button', { name: 'Let the agent help me create a preset', exact: true }).isEnabled()).toBe(true) + await settings.getByRole('button', { name: 'Close', exact: true }).last().click() + await connectFreshWorkspace(page, scaffold.workspaceCwd) + await expect.poll(livePresets).toEqual(['standard']) + + const menu = page.getByRole('menu') + await page.getByRole('button', { name: 'Standard mode', exact: true }).click() + await expect.poll(() => menu.getByRole('menuitem').count()).toBe(3) + expect(await menu.getByRole('menuitem', { name: /^PTC mode|^Minimal mode/ }).count()).toBe(0) + expect(await menu.getByRole('menuitem', { name: /^Custom mode/ }).count()).toBe(1) + await menu.getByRole('menuitem', { name: /^Creator mode/ }).click() + await expect.poll(livePresets, { timeout: 15_000 }).toEqual(['cordis']) + + await page.getByRole('button', { name: 'Creator mode', exact: true }).click() + await menu.waitFor() await scaffold.ctx.settings.update('ui-settings', { enabled: true }) - await expect.poll(() => section.getByRole('button', { name: 'Set as new task default: Minimal mode' }).count()).toBe(1) - await expect.poll(() => refused.count()).toBe(0) - expect(await section.getByRole('switch').count()).toBe(0) + await menu.waitFor({ state: 'detached' }) + await page.getByRole('button', { name: 'Creator mode', exact: true }).click() + await expect.poll(() => menu.getByRole('menuitem').count()).toBe(5) + expect(await menu.getByRole('menuitem', { name: /^PTC mode/ }).count()).toBe(1) + expect(await menu.getByRole('menuitem', { name: /^Minimal mode/ }).count()).toBe(1) + expect(await livePresets()).toEqual(['cordis']) await scaffold.ctx.settings.update('ui-settings', { enabled: false }) - await expect.poll(() => refused.count()).toBeGreaterThan(0) - expect(await section.getByRole('switch').count()).toBe(0) + await menu.waitFor({ state: 'detached' }) + expect(await livePresets()).toEqual(['cordis']) + await page.getByRole('button', { name: 'Creator mode', exact: true }).click() + await expect.poll(() => menu.getByRole('menuitem').count()).toBe(3) + await menu.getByRole('menuitem', { name: /^Standard mode/ }).click() + await expect.poll(livePresets, { timeout: 15_000 }).toEqual(['standard']) + + await page.reload() + await page.getByRole('button', { name: 'Standard mode', exact: true }).click() + await expect.poll(() => menu.getByRole('menuitem').count()).toBe(3) + await page.keyboard.press('Escape') + await openSettings(page, 'en') + await settings.getByRole('button', { name: 'General', exact: true }).click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false') + await settings.getByRole('button', { name: 'Agent presets', exact: true }).click() + await settings.getByRole('button', { name: 'New task default: Standard mode', exact: true }).waitFor() + await settings.getByRole('button', { name: 'Let the agent help me create a preset', exact: true }).click() + await settings.waitFor({ state: 'detached' }) + await expect.poll(livePresets, { timeout: 15_000 }).toContain('cordis') + expect((await scaffold.ctx.agentPresets.remoteExportList()).presets.find(preset => preset.isDefault)?.id).toBe('standard') + expect(tripwire.pageErrors).toEqual([]) }) diff --git a/apps/web/tests/expected/desktop-locale/automatic.expected.md b/apps/web/tests/expected/desktop-locale/automatic.expected.md index 77b4593b90..0f2b533897 100644 --- a/apps/web/tests/expected/desktop-locale/automatic.expected.md +++ b/apps/web/tests/expected/desktop-locale/automatic.expected.md @@ -20,7 +20,7 @@ - button "减小字号" - text: px 工作步骤展示 选择希望看到多少工具调用细节 - button "标准" - - text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换 + - text: 代码工作工具 开启后显示轨迹、本轮代码差异,并在新对话中提供 PTC 和极简模式 - switch "代码工作工具" [checked] - text: 快捷键 - paragraph: 查看和编辑当前可用的快捷键和输入操作 diff --git a/apps/web/tests/expected/desktop-locale/selected.expected.md b/apps/web/tests/expected/desktop-locale/selected.expected.md index c4fd5e5eff..53cdd35b32 100644 --- a/apps/web/tests/expected/desktop-locale/selected.expected.md +++ b/apps/web/tests/expected/desktop-locale/selected.expected.md @@ -20,7 +20,7 @@ - button "Decrease font size" - text: px Work details Choose how much detail to show for tool calls - button "Standard" - - text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats + - text: Coding Tools Shows trajectory and code diffs, and adds PTC and Minimal modes to new chats - switch "Coding Tools" [checked] - text: Keyboard shortcuts - paragraph: View and edit available shortcuts and input actions 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 ad3348ea5a..953038a5dd 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md @@ -20,7 +20,7 @@ - button "Decrease font size" - text: px Work details Choose how much detail to show for tool calls - button "Detailed" - - text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats + - text: Coding Tools Shows trajectory and code diffs, and adds PTC and Minimal modes to new chats - switch "Coding Tools" - text: Keyboard shortcuts - paragraph: View and edit available shortcuts and input actions diff --git a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md index ad3348ea5a..953038a5dd 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md @@ -20,7 +20,7 @@ - button "Decrease font size" - text: px Work details Choose how much detail to show for tool calls - button "Detailed" - - text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats + - text: Coding Tools Shows trajectory and code diffs, and adds PTC and Minimal modes to new chats - switch "Coding Tools" - text: Keyboard shortcuts - paragraph: View and edit available shortcuts and input actions diff --git a/apps/web/tests/expected/settings-chrome/dialog.expected.md b/apps/web/tests/expected/settings-chrome/dialog.expected.md index 0f17fa643f..92a8c17a75 100644 --- a/apps/web/tests/expected/settings-chrome/dialog.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog.expected.md @@ -20,7 +20,7 @@ - button "减小字号" - text: px 工作步骤展示 选择希望看到多少工具调用细节 - button "详细" - - text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换 + - text: 代码工作工具 开启后显示轨迹、本轮代码差异,并在新对话中提供 PTC 和极简模式 - switch "代码工作工具" - text: 快捷键 - paragraph: 查看和编辑当前可用的快捷键和输入操作 diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index 7f08d21282..59f91f142c 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: b3ad7867f23dad1c /deepseek-ai-dsh-client-ui-agent-preset/use-this-package: - en: cf381c4f00b42c3f - zh: 1b226687dcbd4875 + en: 584999ed508d4146 + zh: 1426a8b908bd178a /deepseek-ai-dsh-client-ui-agent-preset/understand-the-implementation: en: 7813f0063c15e241 zh: 6eabc242edfc45ec diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index ca7bba393e..30ba8e2389 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -27,7 +27,7 @@ Choose Agent presets and the new-task default in Web, read what each mode does a Settings shows the built-in and custom card groups with default highlighting and card-body selection; a group without presets is omitted, except the custom group, which keeps its Creator entry on screen. Every card offers “View configuration”, which opens the preset's declared plugin list as read-only YAML in the Loader's own dialect (`!!js` conditions included); a failed preset stays readable because its diagnostic points into that YAML. Escape closes only the viewer and returns focus to its card; leaving Settings clears the viewer, and a late read does not reopen it. The page edits nothing: the Creator entry starts a Creator-mode task that authors or overrides a preset as a bundle, offered while the `cordis` preset is on the roster and a conversation flow exists. -Coding Tools in General Settings decide whether a mode can be chosen at all: with them off the new-session picker disappears and the cards refuse selection, while the saved default keeps composing new tasks. Choosing a healthy default also synchronizes the blank session on the current new-task surface. Creator starts a new task using the `cordis` preset. +With Coding Tools off in General Settings, the new-session picker offers Standard, Creator, and custom presets; turning them on also offers PTC and Minimal. The preference changes only these menu choices: it preserves the staged choice, saved default, and current session preset. Settings keeps the full roster, default selection, and Creator entry available in both states. Choosing a healthy default also synchronizes the blank session on the current new-task surface. Creator starts a new task using the `cordis` preset. Known shipped presets offer mode details and usage examples in a read-only dialog. Its tabs preserve each page's scroll position; closing returns focus to the opening action. Help does not change the new-task default. The default badge replaces the card's group badge, and the preset id appears beside the title. Guide copy and examples belong to this package. diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index d2a3471f45..73dcf4b0f7 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -27,7 +27,7 @@ kind: "package-reference" 设置页显示内置与自定义卡片分组、默认项高亮和点击卡片选择;没有 preset 的分组不显示,但自定义分组会保留其创造入口。每张卡片都提供「查看配置」,以只读 YAML 打开该 preset 声明的插件列表,使用 Loader 自己的方言(含 `!!js` 条件);加载失败的 preset 同样可读,因为其诊断信息正指向这份 YAML。Escape 只关闭查看器并将焦点还给卡片;离开设置页会清空查看器,迟到的读取结果不会重新打开它。本页不编辑任何内容:创造入口启动一个创造模式任务,以 bundle 形式创建或覆盖 preset;当 `cordis` preset 在列表中且存在会话流程时提供。 -通用设置中的代码工作工具决定能否选择模式:关闭后新会话选择器消失、卡片拒绝选择,而已保存的默认值继续用于新任务。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。 +关闭通用设置中的代码工作工具后,新会话选择器提供标准、创造和自定义预设;开启后还提供 PTC 和极简模式。此偏好只改变这些菜单选项,保留暂存的选择、已保存的默认值和当前会话预设。两种状态下,设置页均保留完整列表、默认值选择和创造入口。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。 已知的内置预设提供只读的模式说明与使用示例对话框。各页签保留各自的滚动位置;关闭后焦点回到打开它的操作。帮助不会改变新任务默认值。默认徽标取代卡片的分组徽标,预设 id 显示在标题旁。指南文案与示例归本包所有。 diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx index d32bc78f63..0dd51b06c1 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx @@ -21,13 +21,13 @@ import { // Type-only: pulls the ui-conversation SlotMap merge (the hero seat). import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type { AgentPresetSeatState } from './seat-store.ts' -import { presetDisplayText } from './locales.ts' +import { isBuiltInPreset, presetDisplayText } from './locales.ts' import css from './AgentPresetSeat.module.css' /** Registration-side business face for the hero chip. */ export interface AgentPresetSeatInjected { hooks: { - /** Shared Developer tools preference; off hides the chip. */ + /** Shared Developer tools preference; off hides the PTC and Minimal menu choices. */ developerTools: ObservableSnapshot /** Seat snapshot bound by the renderer as useAgentPresetSeat. */ agentPresetSeat: SnapshotStore @@ -73,8 +73,7 @@ export type AgentPresetSeatProps = /** * Render the new-session agent-preset chip. * @param props - composed slot props. - * @returns The chip, or null outside the main view, with Developer tools off, - * or before the roster provides a preset choice. + * @returns The chip, or null outside the main view or before the roster provides a preset choice. */ export function AgentPresetSeat({ sessionId, useSessionRetainInfo, load, select, introduced, useAgentPresetSeat, useDeveloperTools, t, @@ -88,22 +87,20 @@ export function AgentPresetSeat({ // it rather than leaving the first one silently in place. const toastSeq = useRef(0) const [toast, setToast] = useState<{ seq: number; text: string } | null>(null) - const pickerVisible = useRef(developerTools) - pickerVisible.current = developerTools useEffect(() => { void load() }, [load]) - // The component stays registered while hidden, so clear local disclosure - // state explicitly; otherwise an external off/on edit can revive an old - // menu or refusal banner. + // Close a menu whose available choices just changed. useEffect(() => { - if (developerTools) return setOpen(false) - setToast(null) }, [developerTools]) + const options = state.options.filter(option => developerTools + || !isBuiltInPreset(option) + || (option.id !== 'ptc' && option.id !== 'minimal')) + const chosen = state.options.find(option => option.id === state.current) const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t) const label = chosenText?.name ?? state.current @@ -131,7 +128,7 @@ export function AgentPresetSeat({ // Nothing to choose between: the deployment composes no presets and every // session shares the host composition. - if (!main || !developerTools || !ready) return null + if (!main || !ready) return null // One wrapper span: the chip is a flex row with a gap, so loose character // spans would each pick up the gap between them. @@ -158,7 +155,7 @@ export function AgentPresetSeat({ { setOpen(false) }} - items={state.options.map((option) => { + items={options.map((option) => { const text = presetDisplayText(option, t) return { id: option.id, @@ -175,16 +172,16 @@ export function AgentPresetSeat({ selectedId={state.current} onSelect={(id) => { setOpen(false) - const picked = state.options.find(option => option.id === id) + const picked = options.find(option => option.id === id) // The fallback is for the row shape `find` cannot promise; the menu's - // items ARE `state.options`, so an emitted id is always one of them. + // items ARE `options`, so an emitted id is always one of them. /* v8 ignore next */ const name = picked === undefined ? id : presetDisplayText(picked, t).name void select(id).then((refusal) => { // Announced only for a pick a person just made: `apply()` also runs // when a session becomes current, and a banner over that would // report a refusal nobody asked for. - if (refusal === undefined || !pickerVisible.current) return + if (refusal === undefined) return toastSeq.current += 1 setToast({ seq: toastSeq.current, text: t('switchRefused', { name, reason: refusal }) }) }) diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx index 9d193d94ff..25c59f944a 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx @@ -4,7 +4,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { Button, IconBrowseOutlineRegular, IconPlusOutlineRegular, Modal, Tag, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' -import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { SnapshotStore } from '@deepseek-ai/dsh-client-store' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { AgentPresetSectionState } from './section-store.ts' import { isBuiltInPreset, presetDisplayText } from './locales.ts' @@ -15,8 +15,6 @@ import css from './AgentPresetSection.module.css' export interface AgentPresetSectionInjected { hooks: { agentPresetSection: SnapshotStore - /** Shared Developer tools preference; off hides every selection action. */ - developerTools: ObservableSnapshot } /** Stage the `cordis` preset and start a Creator-mode task; absent without a conversation flow. */ startCreatorDraft?: () => void @@ -62,10 +60,9 @@ function CardDescription({ text }: { text: string }): ReactNode { */ export function AgentPresetSection({ useAgentPresetSection, load, view, closeView, makeDefault, startCreatorDraft, - close: closeSettings, useDeveloperTools, t, + close: closeSettings, t, }: AgentPresetSectionProps) { const state = useAgentPresetSection(value => value) - const developerTools = useDeveloperTools(enabled => enabled) const [guide, setGuide] = useState<{ content: NonNullable> page: PresetGuidePage @@ -90,8 +87,7 @@ export function AgentPresetSection({