fix(web): remove tool-call settings placeholder
This commit is contained in:
+2
-2
@@ -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-07-31-permission-default-for-new-sessions.md
|
||||
2026-07-31-permission-default-for-new-sessions.md: 236e0eedd2b3a6ba64a837fa1838d63545f13fb1
|
||||
2026-07-31-permission-default-for-new-sessions.zh.md: 8cdb5e6a0b6ca8a9a878351474728b325fb92528
|
||||
2026-07-31-permission-default-for-new-sessions.md: 78ec7a9b7c690c7b29fa10c1518fb7466971f0ac
|
||||
2026-07-31-permission-default-for-new-sessions.zh.md: f6b29b879112de8b8f1c1f7ab466b7d90f9a142c
|
||||
@@ -14,7 +14,7 @@ The Web General-settings page displayed Permission as a disabled skeleton even t
|
||||
|
||||
The service reads the current Settings value synchronously at `session/created`. A genuinely fresh session receives three explicit events: `permission/preset`, `sandbox/mode`, and `approval/policy`. Those facts pin the permission selected at creation, so a later Settings change affects only later sessions. A seeded or partially initialized session preserves its effective knobs and receives only missing facts; it never adopts the latest user default while resuming.
|
||||
|
||||
The existing `/permission` command and `permissions` projection remain the current-session path. The browser plugin now contributes the Permission row to `settings.general.item`, reads the dynamic enum from the redacted Settings descriptor, and writes only `defaultPreset` through a revision-checked `settings.mutate`. The ownerless General-settings package retains only the Tool Call skeleton.
|
||||
The existing `/permission` command and `permissions` projection remain the current-session path. The browser plugin now contributes the Permission row to `settings.general.item`, reads the dynamic enum from the redacted Settings descriptor, and writes only `defaultPreset` through a revision-checked `settings.mutate`. The ownerless General-settings package contributes no placeholder rows.
|
||||
|
||||
ApiProxy explicitly adds `permission` to its Web settings allowlist beside the configurable-provider namespaces. This is a local boundary decision, not a general registration flag or a `local-client` access model: registering another Settings namespace still does not expose it. Permission changes emit `host/settings-changed` but not `host/models-changed`.
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Web「通用」设置页将「权限」显示为禁用的骨架控件,尽管 `
|
||||
|
||||
服务会在 `session/created` 时同步读取当前 Settings 值。真正的新会话会收到三个显式事件:`permission/preset`、`sandbox/mode` 和 `approval/policy`。这些事实将创建时选中的权限固定下来,因此后续 Settings 变更只影响之后的会话。带 seed 或只完成部分初始化的会话会保留其有效调节项,只补齐缺失的事实;恢复时绝不会采用最新的用户默认值。
|
||||
|
||||
现有 `/permission` 命令和 `permissions` 投影仍是当前会话的操作路径。浏览器插件现在向 `settings.general.item` 贡献「权限」行,从脱敏后的 Settings 描述符读取动态 enum,并只通过经过 revision 校验的 `settings.mutate` 写入 `defaultPreset`。无归属的「通用」设置包只保留「工具调用」骨架。
|
||||
现有 `/permission` 命令和 `permissions` 投影仍是当前会话的操作路径。浏览器插件现在向 `settings.general.item` 贡献「权限」行,从脱敏后的 Settings 描述符读取动态 enum,并只通过经过 revision 校验的 `settings.mutate` 写入 `defaultPreset`。无归属的「通用」设置包不贡献任何占位行。
|
||||
|
||||
ApiProxy 在可配置提供方 namespace 之外,将 `permission` 显式加入 Web Settings allowlist。这是局部的边界决策,而不是通用注册标志或 `local-client` 访问模型:注册其他 Settings namespace 仍不会将其暴露。权限变更会发出 `host/settings-changed`,但不会发出 `host/models-changed`。
|
||||
|
||||
|
||||
@@ -53,8 +53,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('true')
|
||||
// General is active by default; Permission, Language and Appearance are
|
||||
// functional, while Tool Call remains a skeleton.
|
||||
// General is active by default; Permission, Language and Appearance are functional.
|
||||
expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBe('true')
|
||||
await dialog.getByRole('button', { name: 'Danger Full Access' }).waitFor({ timeout: 10_000 })
|
||||
await expect.poll(() => dialog.getByText('语言', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
|
||||
@@ -88,7 +87,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
const selector = dialog.getByRole('button', { name: 'Danger Full Access' })
|
||||
await selector.waitFor({ timeout: 10_000 })
|
||||
expect(await selector.isEnabled()).toBe(true)
|
||||
await expect.poll(() => selector.isEnabled(), { timeout: 5_000 }).toBe(true)
|
||||
await selector.click()
|
||||
await page.getByRole('menuitem', { name: 'Read Only' }).click()
|
||||
await dialog.getByRole('button', { name: 'Read Only' }).waitFor({ timeout: 10_000 })
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- button "Danger Full Access":
|
||||
- text: Danger Full Access
|
||||
- img
|
||||
- text: 工具调用 Schema mode Traditional function calling — invoke tools one at a time Code mode Chain multiple tools with code — multi-step orchestration 语言
|
||||
- text: 语言
|
||||
- button "中文":
|
||||
- text: 中文
|
||||
- img
|
||||
|
||||
@@ -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: 9e12f02fc1e767fb807be4fdd3f506c189662bc7
|
||||
README.zh.md: 225e27f5705f33bc6199615b0fe96e04eaa6a04c
|
||||
README.md: 241678567c4dbc7411ab9e76f595f2f696cc02d6
|
||||
README.zh.md: da4568d109c20bf1860fb8841942d42078b9443a
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Settings ownerless-copy plugin: registers everything on the Settings surface that belongs to no single feature — the shell's trigger/header/close chrome content, the General section (`settings.general.item` slot plus the Tool Call skeleton), and the `settings` dictionaries. Feature-owned rows (Permission, Language, Appearance) and sections (Models) stay with their feature packages.
|
||||
Settings ownerless-copy plugin: registers everything on the Settings surface that belongs to no single feature — the shell's trigger/header/close chrome content, the General section and its `settings.general.item` slot, and the `settings` dictionaries. Feature-owned rows (Permission, Language, Appearance) and sections (Models) stay with their feature packages.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -14,4 +14,4 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Tool Call is a display skeleton** — its backing host setting does not exist yet, so the cubes write nothing. When it gains real backing, the row moves to its owning feature plugin per the self-registration doctrine.
|
||||
- The General section has no built-in rows; each row appears only when its owning feature plugin is mounted.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
设置界面无归属文案插件:在设置界面注册所有不属于单一功能的内容,包括外壳的触发器、标题栏与关闭控件内容,「通用」分区(`settings.general.item` slot 加上「工具调用」骨架行),以及 `settings` 字典。归具体功能所有的行(「权限」、「语言」、「外观」)和分区(「模型」)仍由各自的功能包提供。
|
||||
设置界面无归属文案插件:在设置界面注册所有不属于单一功能的内容,包括外壳的触发器、标题栏与关闭控件内容,「通用」分区及其 `settings.general.item` slot,以及 `settings` 字典。归具体功能所有的行(「权限」、「语言」、「外观」)和分区(「模型」)仍由各自的功能包提供。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **「工具调用」只是展示骨架**:其宿主设置尚不存在,因此控件不会写入任何内容。一旦获得实际支撑,按照自注册原则,该行会移至拥有它的功能插件。
|
||||
- 「通用」分区没有内置行;每一行仅在其所属功能插件挂载时出现。
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-settings-general",
|
||||
"description": "Settings ownerless-copy plugin: the General section and Tool Call skeleton, shell trigger/header chrome content, and settings dictionaries",
|
||||
"description": "Settings ownerless-copy plugin: the General section, shell trigger/header chrome content, and settings dictionaries",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* General section rows (figma 501:29983 'Options'): stacked groups, 16px
|
||||
* vertical padding each, hairline separator under all but the last child
|
||||
* (feature-contributed rows carry their own row chrome and separators; the
|
||||
* :last-child rule strips the trailing one wherever the column ends). */
|
||||
/* Feature-contributed rows own their chrome and separators; the section
|
||||
* strips the trailing separator wherever the column ends. */
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
@@ -12,64 +10,3 @@
|
||||
.section > :last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Title + full-width body group (figma 'Frame 2117131229': column, gap 8). */
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* Tool Call mode cubes share an 8px gap and wrap to one per row when the
|
||||
panel is too narrow. */
|
||||
.cubeRow {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Tool Call mode cube (figma '.Selector Cube' 418w r16, flexed to fit the
|
||||
* 800 panel; horizontal inset = outer pad 4 + inner .Menu_cell pad 10,
|
||||
* vertical = inner pad 8). */
|
||||
.modeCube {
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 276px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 16px;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modeCube:hover:not(.selected) {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
/* Selected cube: #F5F6F7 fill + #ADB2B8 border (static token — the bluish-400
|
||||
* step has no alias-layer name). */
|
||||
.selected {
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
border-color: var(--dsw-static-neutral-bluish-400);
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
/**
|
||||
* The General section (figma 501:29983 'Options'): one column rendering the
|
||||
* `settings.general.item` contributions. Features own their rows; this
|
||||
* package contributes only the ownerless Tool Call skeleton.
|
||||
*/
|
||||
import type { PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
/** The General section: one column rendering feature-owned item contributions. */
|
||||
import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import css from './GeneralSection.module.css'
|
||||
|
||||
/** Full component props: section owner share plus item render share. */
|
||||
@@ -22,30 +18,3 @@ export function GeneralSection({ renderSlot }: GeneralSectionComponentProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Props of the ownerless Tool Call item contribution. */
|
||||
export type ToolCallSkeletonProps =
|
||||
PropsRuntime<'settings.general.item'> & PropsLocale<'settings'>
|
||||
|
||||
/**
|
||||
* Render the static Tool Call mode choice until its host setting exists.
|
||||
* @param props - item runtime and translated copy.
|
||||
* @returns the skeleton row.
|
||||
*/
|
||||
export function ToolCallSkeleton({ t }: ToolCallSkeletonProps) {
|
||||
return (
|
||||
<div className={css.group}>
|
||||
<div className={css.title}>{t('toolcall.title')}</div>
|
||||
<div className={css.cubeRow}>
|
||||
<div className={`${css.modeCube} ${css.selected}`}>
|
||||
<div className={css.title}>{t('toolcall.schema.title')}</div>
|
||||
<div className={css.desc}>{t('toolcall.schema.desc')}</div>
|
||||
</div>
|
||||
<div className={css.modeCube}>
|
||||
<div className={css.title}>{t('toolcall.code.title')}</div>
|
||||
<div className={css.desc}>{t('toolcall.code.desc')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Settings ownerless-copy plugin, browser half: registers everything on the
|
||||
* Settings surface that belongs to no single feature — the trigger/header
|
||||
* chrome content, the General section (`settings.general.item` slot plus the
|
||||
* ownerless Tool Call skeleton), and the `settings` dictionaries.
|
||||
* chrome content, the General section, and the `settings` dictionaries.
|
||||
* Feature-owned rows and sections stay with their features.
|
||||
* Export discipline: packages/client/AGENTS.md.
|
||||
*/
|
||||
@@ -13,14 +12,14 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
// Type-only: pulls ctx.locale and the 'settings.general.item' SlotMap merge.
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { CloseLabel, HeaderContent, TriggerContent } from './chrome.tsx'
|
||||
import { GeneralSection, ToolCallSkeleton } from './GeneralSection.tsx'
|
||||
import { GeneralSection } from './GeneralSection.tsx'
|
||||
import { en, zh, type SettingsKey } from './locales.ts'
|
||||
|
||||
export type {
|
||||
CloseLabelProps, HeaderContentProps, TriggerContentProps,
|
||||
} from './chrome.tsx'
|
||||
export type {
|
||||
GeneralSectionComponentProps, ToolCallSkeletonProps,
|
||||
GeneralSectionComponentProps,
|
||||
} from './GeneralSection.tsx'
|
||||
export type { SettingsKey } from './locales.ts'
|
||||
|
||||
@@ -69,19 +68,11 @@ export function apply(ctx: ClientContext): void {
|
||||
locale: NS,
|
||||
children: { 'settings.general.item': { kind: 'list', scope: 'root' } },
|
||||
}, GeneralSection))
|
||||
const toolCall = deferRegistration(ctx.slots, 'settings.general.item', ToolCallSkeleton, () =>
|
||||
ctx.slots.register({
|
||||
name: 'settings.general.item',
|
||||
id: 'tool-call',
|
||||
order: -10,
|
||||
locale: NS,
|
||||
}, ToolCallSkeleton))
|
||||
return () => {
|
||||
trigger.dispose()
|
||||
header.dispose()
|
||||
close.dispose()
|
||||
general.dispose()
|
||||
toolCall.dispose()
|
||||
}
|
||||
}, 'ui-settings-general: chrome and section registrations')
|
||||
}
|
||||
@@ -1,24 +1,11 @@
|
||||
/**
|
||||
* `settings` namespace dictionaries: shell chrome plus the shell-owned
|
||||
* General section (nav label and ownerless Tool Call skeleton). Technical
|
||||
* mode copy is shared verbatim across locales per the Figma design.
|
||||
* Feature-owned rows ship their copy in their own packages.
|
||||
*/
|
||||
const SHARED = {
|
||||
'toolcall.schema.title': 'Schema mode',
|
||||
'toolcall.schema.desc': 'Traditional function calling — invoke tools one at a time',
|
||||
'toolcall.code.title': 'Code mode',
|
||||
'toolcall.code.desc': 'Chain multiple tools with code — multi-step orchestration',
|
||||
} satisfies Record<string, string>
|
||||
/** Shell chrome and General-nav dictionaries; feature rows own their copy. */
|
||||
|
||||
/** Simplified Chinese dictionary (the key-set source of truth). */
|
||||
export const zh = {
|
||||
...SHARED,
|
||||
'trigger': '设置',
|
||||
'title': '设置',
|
||||
'close': '关闭',
|
||||
'general.nav': '通用设置',
|
||||
'toolcall.title': '工具调用',
|
||||
} satisfies Record<string, string>
|
||||
|
||||
/** The settings namespace key union. */
|
||||
@@ -26,10 +13,8 @@ export type SettingsKey = keyof typeof zh
|
||||
|
||||
/** English dictionary, checked complete against the zh key set. */
|
||||
export const en = {
|
||||
...SHARED,
|
||||
'trigger': 'Settings',
|
||||
'title': 'Settings',
|
||||
'close': 'Close',
|
||||
'general.nav': 'General',
|
||||
'toolcall.title': 'Tool Call',
|
||||
} satisfies Record<SettingsKey, string>
|
||||
@@ -6,7 +6,7 @@ import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
|
||||
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
|
||||
import { GeneralSection, ToolCallSkeleton } from '../src/client/GeneralSection.tsx'
|
||||
import { GeneralSection } from '../src/client/GeneralSection.tsx'
|
||||
|
||||
/** The four seats this plugin fills (slot name → expected component). */
|
||||
const SEATS = [
|
||||
@@ -61,17 +61,11 @@ describe('ui-settings-general apply', () => {
|
||||
// The nav label is a locale-following thunk; owners resolve at read time.
|
||||
expect(resolveSlotLabel(entry.options.label)).toBe('通用设置')
|
||||
expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
||||
const toolEntry = before.slots.entries('settings.general.item')[0]!
|
||||
expect(toolEntry).toMatchObject({
|
||||
component: ToolCallSkeleton,
|
||||
options: { id: 'tool-call', order: -10 },
|
||||
})
|
||||
expect(before.slots.entries('settings.general.item')).toEqual([])
|
||||
// Copy rides the standard locale seat: every seat declares the namespace.
|
||||
for (const [name] of SEATS) {
|
||||
expect(before.slots.entries(name)[0]!.locale).toBe('settings')
|
||||
}
|
||||
expect(toolEntry.locale).toBe('settings')
|
||||
|
||||
const after = await bench()
|
||||
await after.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
for (const [name] of SEATS) expect(after.slots.entries(name)).toHaveLength(0)
|
||||
@@ -83,7 +77,7 @@ describe('ui-settings-general apply', () => {
|
||||
expect(after.slots.entries(name)).toHaveLength(1)
|
||||
}
|
||||
await vi.waitFor(() => {
|
||||
expect(after.slots.entries('settings.general.item')[0]!.component).toBe(ToolCallSkeleton)
|
||||
expect(after.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -133,7 +127,7 @@ describe('ui-settings-general apply', () => {
|
||||
for (const [name, component] of SEATS) {
|
||||
expect(b.slots.entries(name)[0]!.component).toBe(component)
|
||||
}
|
||||
expect(b.slots.entries('settings.general.item')[0]!.component).toBe(ToolCallSkeleton)
|
||||
expect(b.slots.entries('settings.general.item')).toEqual([])
|
||||
expect(b.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
|
||||
// The recovered registrations still ride the locale path.
|
||||
b.locale.setLocale('en')
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import type {
|
||||
GeneralSectionComponentProps, ToolCallSkeletonProps,
|
||||
} from '../src/client/GeneralSection.tsx'
|
||||
import { GeneralSection, ToolCallSkeleton } from '../src/client/GeneralSection.tsx'
|
||||
import type { GeneralSectionComponentProps } from '../src/client/GeneralSection.tsx'
|
||||
import { GeneralSection } from '../src/client/GeneralSection.tsx'
|
||||
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
|
||||
import type { TriggerContentProps } from '../src/client/chrome.tsx'
|
||||
import { en } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
// The seat's key domain is settings ∪ common; the stub answers from the
|
||||
// package dictionary and falls back to the key like the real chain.
|
||||
const t: ToolCallSkeletonProps['t'] = key => (en as Record<string, string>)[key] ?? key
|
||||
const t: TriggerContentProps['t'] = key => (en as Record<string, string>)[key] ?? key
|
||||
|
||||
// Global standard kit stubs: none of these components consume the hooks.
|
||||
const unusedHook = (() => { throw new Error('unused by settings-general components') }) as never
|
||||
@@ -55,16 +54,3 @@ describe('GeneralSection', () => {
|
||||
expect(screen.getByTestId('slot-settings.general.item')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ToolCallSkeleton', () => {
|
||||
it('renders the mode cubes with schema pinned selected', () => {
|
||||
render(<ToolCallSkeleton {...kit} t={t} />)
|
||||
expect(screen.getByText('Tool Call')).toBeTruthy()
|
||||
const schema = screen.getByText('Schema mode')
|
||||
const code = screen.getByText('Code mode')
|
||||
expect(schema.parentElement!.className).toContain('selected')
|
||||
expect(code.parentElement!.className).not.toContain('selected')
|
||||
expect(screen.getByText('Traditional function calling — invoke tools one at a time')).toBeTruthy()
|
||||
expect(screen.getByText('Chain multiple tools with code — multi-step orchestration')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user