fix(web): 设置页切换默认预设不再受代码工作工具开关限制

This commit is contained in:
lsdsjy committed 2026-09-29 13:19:50 +08:00
1 parent 17da46e4f3
commit 3442930ebf
13 files changed
+42 -39

No files matched your search

@@ -9,8 +9,8 @@
en: e22ee243b1503319
zh: b53c1a932d547f55
/agent-note-shared-developer-tool-settings/decision:
en: 62f71b49af7b9971
zh: f0f12da4cb0c7fc0
en: f184c99a04a5dc3e
zh: 38f1b2d5616f1f9e
/agent-note-shared-developer-tool-settings/alternatives-considered:
en: df1b80e5b8d2a178
zh: 0136d16fc44d57bf
@@ -12,7 +12,7 @@ Diagnostic views, preset selection and changed-file summaries add complexity to
The settings-owned `ctx.configForms.developerTools` preference uses a shared entry form and exposes one accepted `ui-settings.enabled` preference. The initial default-off choice reduces routine-task complexity; [Developer tools default on after preference resolution](2026-09-20-developer-tools-default-on.md) supersedes that default and owns startup enablement. 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 hides the Trajectory View, hides tool Inspect actions when no visible View declares tool-call inspection, hides the conversation View tab bar while fewer than two Views are available, hides the new-session preset chip and refuses the Agent-preset cards' selection, and omits the changed-files card and its summary read. Views registered by other plugins stay available. Turning it off while Trajectory is active activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization.
Off hides the Trajectory View, hides tool Inspect actions when no visible View declares tool-call inspection, hides the conversation View tab bar while fewer than two Views are available, hides the new-session preset chip and disables the Agent-preset Creator entry, and omits the changed-files card and its summary read. Views registered by other plugins stay available. Turning it off while Trajectory is active activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization.
Plugin assembly translates the shared preference into component-specific capabilities (`showCodeDiff`, the Agent-preset surfaces' `developerTools`, 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.
@@ -12,7 +12,7 @@ Status: implemented
设置域内的 `ctx.configForms.developerTools` 偏好对象使用共享条目表单,提供一个已接受的 `ui-settings.enabled` 偏好。最初默认关闭的选择用于降低日常任务的复杂度;[偏好解析后默认开启开发者工具](2026-09-20-developer-tools-default-on.zh.md)取代该默认值并负责启动时的启用策略。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。
关闭时隐藏 Trajectory View,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,可见 View 少于两个时隐藏会话 View 标签栏,隐藏新会话预设入口并拒绝 Agent 预设卡片的选择操作,并省略改动文件卡片及其摘要读取。其他插件注册的 View 保持可用。在 Trajectory 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。
关闭时隐藏 Trajectory View,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,可见 View 少于两个时隐藏会话 View 标签栏,隐藏新会话预设入口并禁用 Agent 预设的 Creator 入口,并省略改动文件卡片及其摘要读取。其他插件注册的 View 保持可用。在 Trajectory 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。
插件组装层将共享偏好转换为组件自身的能力(`showCodeDiff`、Agent 预设各界面读取的 `developerTools`,以及 `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 读取访问。
@@ -26,17 +26,13 @@ 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.
// The page owns no selection switch, and the cards choose a default with Coding Tools off.
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: / })
const setDefault = section.getByRole('button', { name: 'Set as new task default: Minimal mode' })
expect(await section.getByRole('switch').count()).toBe(0)
await expect.poll(() => refused.count()).toBeGreaterThan(0)
await expect.poll(() => setDefault.isEnabled()).toBe(true)
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 scaffold.ctx.settings.update('ui-settings', { enabled: false })
await expect.poll(() => refused.count()).toBeGreaterThan(0)
await expect.poll(() => setDefault.isEnabled()).toBe(true)
expect(await section.getByRole('switch').count()).toBe(0)
})
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: b3ad7867f23dad1c
/deepseek-ai-dsh-client-ui-agent-preset/use-this-package:
en: cf381c4f00b42c3f
zh: 1b226687dcbd4875
en: 0a8671f8f7f0ea15
zh: daaf1e8e44ebba15
/deepseek-ai-dsh-client-ui-agent-preset/understand-the-implementation:
en: 7813f0063c15e241
zh: 6eabc242edfc45ec
+1 -1
View File
@@ -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.
Coding Tools in General Settings decide whether a mode can be chosen at all: with them off the new-session picker disappears and Creator is unavailable, while the settings cards still choose the saved default that 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.
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.
+1 -1
View File
@@ -27,7 +27,7 @@ kind: "package-reference"
设置页显示内置与自定义卡片分组、默认项高亮和点击卡片选择;没有 preset 的分组不显示,但自定义分组会保留其创造入口。每张卡片都提供「查看配置」,以只读 YAML 打开该 preset 声明的插件列表,使用 Loader 自己的方言(含 `!!js` 条件);加载失败的 preset 同样可读,因为其诊断信息正指向这份 YAML。Escape 只关闭查看器并将焦点还给卡片;离开设置页会清空查看器,迟到的读取结果不会重新打开它。本页不编辑任何内容:创造入口启动一个创造模式任务,以 bundle 形式创建或覆盖 preset;当 `cordis` preset 在列表中且存在会话流程时提供。
通用设置中的代码工作工具决定能否选择模式:关闭后新会话选择器消失、卡片拒绝选择,而已保存的默认值继续用于新任务。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。
通用设置中的代码工作工具决定能否选择模式:关闭后新会话选择器消失、Creator 入口不可用,而设置卡片仍可选择继续用于新任务的已保存默认值。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。
已知的内置预设提供只读的模式说明与使用示例对话框。各页签保留各自的滚动位置;关闭后焦点回到打开它的操作。帮助不会改变新任务默认值。默认徽标取代卡片的分组徽标,预设 id 显示在标题旁。指南文案与示例归本包所有。
@@ -61,7 +61,7 @@
}
.card:hover:not(.cardActive):not(.cardSelectionDisabled) {
.card:hover:not(.cardActive) {
background: var(--dsw-alias-interactive-bg-hover);
}
@@ -75,14 +75,6 @@
/* Mode selection is off, not the preset itself: mute only the selectable
body while the independent view/copy/location actions stay fully usable. */
.cardSelectionDisabled {
background: var(--dsw-alias-bg-module-platform);
}
.cardSelectionDisabled .cardMain {
opacity: 0.48;
}
/* A broken preset reads as damaged before anything else: the card cannot be
picked, so its border carries the warning the disabled body cannot. */
.cardBroken {
@@ -15,7 +15,7 @@ import css from './AgentPresetSection.module.css'
export interface AgentPresetSectionInjected {
hooks: {
agentPresetSection: SnapshotStore<AgentPresetSectionState>
/** Shared Developer tools preference; off hides every selection action. */
/** Shared Developer tools preference; off disables the Creator entry. */
developerTools: ObservableSnapshot<boolean>
}
/** Stage the `cordis` preset and start a Creator-mode task; absent without a conversation flow. */
@@ -113,15 +113,13 @@ export function AgentPresetSection({
const display = presetDisplayText(row, t)
const help = presetGuide(row.id, builtIn ? 'system' : 'user')
const selectionAction = row.broken !== undefined ? t('brokenBadge')
: row.isDefault ? t('inUse')
: t(developerTools ? 'setDefault' : 'enableDevToolsToSetDefault')
: t(row.isDefault ? 'inUse' : 'setDefault')
return <li key={row.id} data-agent-preset-id={row.id} className={[
css.card, row.broken === undefined ? undefined : css.cardBroken,
row.isDefault ? css.cardActive : undefined,
!developerTools && row.broken === undefined && !row.isDefault ? css.cardSelectionDisabled : undefined,
].filter(Boolean).join(' ')}>
<button type="button" className={css.cardMain} aria-pressed={row.isDefault}
disabled={row.isDefault || (row.broken === undefined && (!developerTools || state.saving))}
disabled={row.isDefault || (row.broken === undefined && state.saving)}
aria-disabled={row.broken !== undefined} aria-label={`${selectionAction}: ${display.name}`} title={selectionAction}
onClick={() => { if (row.broken === undefined) void makeDefault(row.id) }}>
<span className={css.cardHead}>
@@ -27,7 +27,6 @@ export type AgentPresetSettingsKey =
| 'switchRefused'
| 'close'
| 'creatorDraft'
| 'enableDevToolsToSetDefault'
| 'enableDevToolsToCreate'
/** English copy. */
@@ -67,7 +66,6 @@ export const en: Record<AgentPresetSettingsKey, string> = {
creatorDraft: 'Let the agent help me create a preset',
enableDevToolsToSetDefault: 'Turn on Coding Tools in General settings to choose a default',
enableDevToolsToCreate: 'Turn on Coding Tools in General settings to start Creator mode',
}
@@ -104,7 +102,6 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
creatorDraft: '让 Agent 帮我创建预设模式',
enableDevToolsToSetDefault: '请先在通用设置中开启代码工作工具,再设置默认值',
enableDevToolsToCreate: '请先在通用设置中开启代码工作工具,再启动创造模式',
}
@@ -85,7 +85,7 @@ export class AgentPresetSeatController {
this.staged.introduce = false
}
/** Developer tools are the single gate over preset selection. */
/** Developer tools gate chip selection; Settings defaults bypass it. */
private selectionAvailable(): boolean {
return this.ctx.configForms.developerTools.enabled.getSnapshot()
}
@@ -181,7 +181,8 @@ export class AgentPresetSeatController {
const session = this.currentSession()
if (session === undefined || !session.blank || session.id !== expectedSessionId) return undefined
this.stage(id)
return await this.apply()
// A Settings default reaches the blank Session whether or not Developer tools expose the chip.
return await this.applyStage(true)
}
/** Acknowledge the introduction cue once the chip has played it. */
@@ -200,8 +201,11 @@ export class AgentPresetSeatController {
* @returns this attempt's Host refusal, or undefined when successful or no switch starts.
*/
async apply(): Promise<string | undefined> {
return await this.applyStage(this.selectionAvailable())
}
private async applyStage(available: boolean): Promise<string | undefined> {
if (this.store.getSnapshot().busy) return
const available = this.selectionAvailable()
// A stage made while Developer tools were on must not outlive them.
if (!available) this.clearStage()
const staged = this.staged.id
@@ -1038,6 +1038,21 @@ describe('AgentPresetSeatController reconciliation', () => {
expect(controller.blankSessionId()).toBeUndefined()
})
it('synchronizes the blank Session with a Settings default while Developer tools are off', async () => {
const select = vi.fn(() => Promise.resolve({ ok: true as const, value: 'minimal' }))
const current = {
id: SessionId('blank'), blank: true, projectionValues: { agentPreset: 'standard' },
}
const controller = new AgentPresetSeatController({
...developerTools(false),
remote: { agentPresets: { select } },
} as never, () => current)
await controller.syncBlankSession(current.id, 'minimal')
expect(select).toHaveBeenCalledWith(current.id, 'minimal')
})
it('does not retarget a different blank Session after a Settings write', async () => {
const select = vi.fn(() => Promise.resolve({ ok: true as const, value: 'minimal' }))
let current = {
@@ -35,11 +35,12 @@ function rowFor(id: string): HTMLElement {
if (row === null) throw new Error(`no card for ${id}`)
return row
}
it('offers no selection switch and disables the card actions while Developer tools are off', () => {
view({}, undefined, false)
it('offers no selection switch and keeps the card actions while Developer tools are off', () => {
const actions = view({}, undefined, false)
expect(screen.queryByRole('switch')).toBeNull()
expect(screen.getByRole<HTMLButtonElement>('button', { name: `${en.enableDevToolsToSetDefault}: Mine` }).disabled).toBe(true)
fireEvent.click(screen.getByRole('button', { name: `${en.setDefault}: Mine` }))
expect(actions.makeDefault).toHaveBeenCalledWith('mine')
})
it('reads the roster once and sets a default from the card body', async () => {
const actions = view()