dsh-structured 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +27 -10
- package/assets/screenshots/09-theme-light.png +0 -0
- package/assets/screenshots/10-theme-dark.png +0 -0
- package/lib/client.js +138 -2
- package/package.json +1 -1
- package/screenshots.json +3 -1
- package/src/client.js +138 -2
- package/test/client-config-page.test.mjs +273 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.2 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
- 插件详情页配置区:client 注册 `plugins.bundle.config`(键 `dsh-structured`),`theme`/`visual`/`defaultEnabled`/`tailReminder`/`includeSubagents`/`genui` 六字段在 插件板块 → dsh-structured 详情页直接编辑;写入走 configForms 的 `set`(落到 active profile 的 Cordis patch),`writable=false` 时控件只读并提示。
|
|
6
|
+
- 背景:宿主不会从 Config schema 自动生成表单——配置页必须由插件自己的浏览器半侧注册(`dsh-client-ui-plugin-manager` slot 合同的 `plugins.bundle.config`/`plugins.row.config`)。0.8.2 引入 Config 起、直到 0.9.1,插件从未注册配置页,所以详情页一直没有设置项;本次按官方伴生包模式(`whileServed` + `slots.inject`)补齐。
|
|
7
|
+
- 防御:宿主 describe 未服务本命名空间、`react/jsx-runtime`/`dsh-client-ui-primitives` 缺失时静默不出配置页;配置页注入与主题/视觉联动分开挂 child inject,互不影响;headless/CLI 不加载 client 半区。
|
|
8
|
+
- 文档修复:README 安装节三处 `0.7.4.tgz` 过时引用改为通用写法,移除三个「(发布后)」括号,实装实测记录更新为 0.9.1;`.gitignore` 增加 `.v2c/`、`out/`。
|
|
9
|
+
|
|
3
10
|
## 0.9.1 — 2026-10-23
|
|
4
11
|
|
|
5
12
|
- 代码/diff 隔离:` ```diff `/`patch` 代码块、`[data-diff]`/`.md-diff`/GenUI diff 区域不套插件的 code/table chrome;diff 行色与宿主渲染保持原样。代码/diff 回复允许代码块或 diff 卡直接作首块,不强制标题卡。
|
package/README.md
CHANGED
|
@@ -27,11 +27,11 @@ node scripts/desktop-check.mjs
|
|
|
27
27
|
|
|
28
28
|
`desktop` 由 Electron 独占只影响 **CLI 的 `--profile desktop` 子命令**,不影响插件本身;推荐走插件管理入口,或退出 Desktop 后手工 pnpm(方式二)。
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### 方式一:插件市场
|
|
31
31
|
|
|
32
32
|
在 DSH 插件管理里搜索 `dsh-structured` 安装;市场收录后可用。桌面端由应用侧加载 client,不需要浏览器刷新。
|
|
33
33
|
|
|
34
|
-
### 方式二:npm
|
|
34
|
+
### 方式二:npm
|
|
35
35
|
|
|
36
36
|
```powershell
|
|
37
37
|
dsh plugin --profile <profile> add dsh-structured
|
|
@@ -39,13 +39,13 @@ dsh plugin --profile <profile> add dsh-structured
|
|
|
39
39
|
|
|
40
40
|
desktop profile 由 Electron 独占,使用插件管理搜索/安装,不要跑 `--profile desktop`。
|
|
41
41
|
|
|
42
|
-
### 方式三:GitHub Release
|
|
42
|
+
### 方式三:GitHub Release
|
|
43
43
|
|
|
44
|
-
从 GitHub Releases
|
|
44
|
+
从 GitHub Releases 下载最新版的 `dsh-structured-<version>.tgz`(当前 0.9.1),在插件管理里选择本地包;不使用 npm 时,市场 YAML 的 `tarball` 指向该 release。
|
|
45
45
|
|
|
46
46
|
### 开发方式:本地 tgz / 手工 pnpm(desktop profile)
|
|
47
47
|
|
|
48
|
-
本地包只用于开发验证:在 DSH
|
|
48
|
+
本地包只用于开发验证:在 DSH 插件管理里选择仓库根的 `dsh-structured-<version>.tgz`,或 `plugin_manager.install_bundle` 指向该文件。手工 pnpm 方式如下。
|
|
49
49
|
|
|
50
50
|
`desktop` profile 由 Electron 独占:**全局 `dsh plugin --profile desktop ...` 会被 CLI 直接拒绝**(`profile "desktop" is managed exclusively by the Electron application`)。不要用那条命令。要先**完全退出 Desktop**,再在 profile 目录里手工 pnpm:
|
|
51
51
|
|
|
@@ -56,7 +56,7 @@ Copy-Item "$dir\pnpm-lock.yaml" "$dir\pnpm-lock.yaml.bak-before-dsh-structured"
|
|
|
56
56
|
Copy-Item "$dir\cordis.patch.yml" "$dir\cordis.patch.yml.bak-before-dsh-structured"
|
|
57
57
|
|
|
58
58
|
Set-Location $dir
|
|
59
|
-
$tgz = "dsh-structured-0.
|
|
59
|
+
$tgz = "dsh-structured-0.9.1.tgz" # 把 tgz 放到当前目录,或写成实际路径
|
|
60
60
|
pnpm add $tgz
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -76,7 +76,7 @@ pnpm add $tgz
|
|
|
76
76
|
|
|
77
77
|
- 预检:`node scripts/desktop-check.mjs` 全部 PASS —— desktop profile 已初始化、bundle 栈含 `dsh-base`/`dsh-web-app`、`files` 含 `cordis.patch.yml`、`main`/`exports` 指向存在文件、`dsh.client.platform: web` 与 `exports["./client"]` 就位、`engines.dsh`/`dsh.compatibility.dshReleases` 已声明、运行时只 import `node:`、无 Electron 专有 API、无 profile 分支、状态走 `DSH_HOME`。
|
|
78
78
|
- **desktop 是主目标,Web 只做兼容。** 本会话本身运行在 desktop profile,`systemPrompt` 与 `commands` 由 live host 的 Service 查询确认;`dsh.client.platform: web` 是 DSH 客户端模块的格式(desktop 的 Web UI 也加载它),不代表 Web 优先。
|
|
79
|
-
- desktop
|
|
79
|
+
- desktop 实装实测:desktop profile 已装 0.9.1(`dependencies`/`bundles` 各一处,见 `docs/MAINTENANCE-LOG.md` 2026-10-23 条目);desktop 会话最新 `system/message` 命中注入(可用 `scripts/session-inspect.mjs` 复跑)。
|
|
80
80
|
- 视觉层由 desktop 应用侧加载,验收以应用内实际渲染为准;不要求用户手动刷新。
|
|
81
81
|
|
|
82
82
|
### 兼容性门禁
|
|
@@ -125,11 +125,28 @@ Phase 2 已落地 2.1–2.4:卡片类型(结论/判断/指标/风险/下一
|
|
|
125
125
|
|
|
126
126
|

|
|
127
127
|
|
|
128
|
-
|
|
128
|
+

|
|
129
|
+
|
|
130
|
+

|
|
131
|
+
|
|
132
|
+
`screenshots.json` 列出全部 6 张:前 4 张是实机裁图,覆盖标题卡、表格 + 表格结论、状态列、代码块与 800px 窄窗;后 2 张是按 0.9.1 实际 token 渲染的主题对照图,用于看 accent/surface/status,**不是桌面实机截图**。GenUI 截图因避免暴露工作区未收录。
|
|
129
133
|
|
|
130
134
|
### 配置
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
**首选:插件板块 → dsh-structured → 详情页的配置区**(0.9.2 起)。插件自己的浏览器半侧把六个字段注册进宿主 `plugins.bundle.config` slot,改完立即写入 active profile 的 Cordis patch:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
主题 / Theme deepseek | codex | anthropic
|
|
140
|
+
视觉层 / Visual layer 开关
|
|
141
|
+
新会话默认开启 开关
|
|
142
|
+
尾部提醒 / Tail reminder 开关
|
|
143
|
+
子代理注入 / Subagents 开关
|
|
144
|
+
GenUI 联动 auto | on | off
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
宿主 describe 未服务本插件、或部署缺少前端 primitives 时,配置区自动隐藏(此时用下面的 patch 方式)。会话级开关不在这里:用 `/structured on | off`。
|
|
148
|
+
|
|
149
|
+
底层方式:在 profile 的 `cordis.patch.yml` 里覆盖同一行(注意:config 是整份替换,不是深合并):
|
|
133
150
|
|
|
134
151
|
```yaml
|
|
135
152
|
- id: dsh-structured
|
|
@@ -142,7 +159,7 @@ Phase 2 已落地 2.1–2.4:卡片类型(结论/判断/指标/风险/下一
|
|
|
142
159
|
genui: auto # auto | on | off;auto 检测到 GenUI 才追加 dsh-ui 路由
|
|
143
160
|
```
|
|
144
161
|
|
|
145
|
-
`theme`、`visual`、`defaultEnabled`、`tailReminder`、`includeSubagents`、`genui` 都是 `.volatile()`
|
|
162
|
+
`theme`、`visual`、`defaultEnabled`、`tailReminder`、`includeSubagents`、`genui` 都是 `.volatile()` 配置:插件板块详情页的配置区(0.9.2 起,见上)与标准 bundle 表单都可编辑,写入走 active profile 的 Cordis patch,home patch / CLI overlay 优先。若快照的 `writable` 为 false,说明当前配置被上层覆盖,配置区控件只读并提示,不会静默写入成功;请改上层配置或接受只读。`/structured on|off` 只影响 `state.json` 里的按会话提示词开关,不影响 `theme`/`visual`。
|
|
146
163
|
|
|
147
164
|
## 验证注入生效
|
|
148
165
|
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
window.__ModuleLoader__.load({
|
|
14
14
|
id: 'dsh-structured',
|
|
15
|
-
factory() {
|
|
15
|
+
factory(require) {
|
|
16
16
|
const CARD_ATTR = 'data-dsh-structured-card'
|
|
17
17
|
const CARD_TYPE_ATTR = 'data-dsh-structured-card-type'
|
|
18
18
|
const ICON_ATTR = 'data-dsh-structured-icon'
|
|
@@ -30,6 +30,20 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
const STYLE_ID = 'dsh-structured-skin-style'
|
|
31
31
|
const LEGACY_STYLE_ID = 'dsh-structured-card-style'
|
|
32
32
|
|
|
33
|
+
/** 配置页常量:宿主插件详情页的 bundle 配置 slot(键 = 包名)。 */
|
|
34
|
+
const CONFIG_NS = 'dsh-structured'
|
|
35
|
+
const BUNDLE_CONFIG_SLOT = 'plugins.bundle.config'
|
|
36
|
+
|
|
37
|
+
/** 六个配置字段的界面文案;field 与 host Config schema 对齐,fallback 为 schema 默认值。 */
|
|
38
|
+
const CONFIG_FIELDS = [
|
|
39
|
+
{ field: 'theme', kind: 'segmented', fallback: 'deepseek', label: '主题 / Theme', hint: '回复区配色,改完立即生效', options: [['deepseek', 'DeepSeek'], ['codex', 'Codex'], ['anthropic', 'Anthropic']] },
|
|
40
|
+
{ field: 'visual', kind: 'switch', fallback: true, label: '视觉层 / Visual layer', hint: '关闭后不加载回复区样式(标题卡、表格、代码块皮肤)' },
|
|
41
|
+
{ field: 'defaultEnabled', kind: 'switch', fallback: true, label: '新会话默认开启 / Default on', hint: '新会话默认注入结构化提示词' },
|
|
42
|
+
{ field: 'tailReminder', kind: 'switch', fallback: true, label: '尾部提醒 / Tail reminder', hint: '在 system prompt 末尾追加一句结构化提醒' },
|
|
43
|
+
{ field: 'includeSubagents', kind: 'switch', fallback: false, label: '子代理注入 / Subagents', hint: '子代理会话也注入(其产出不直接面向用户)' },
|
|
44
|
+
{ field: 'genui', kind: 'segmented', fallback: 'auto', label: 'GenUI 联动 / GenUI routing', hint: 'auto:检测到 dsh-genui 才追加 dsh-ui 路由规则', options: [['auto', 'auto'], ['on', 'on'], ['off', 'off']] },
|
|
45
|
+
]
|
|
46
|
+
|
|
33
47
|
const CARD_TYPES = new Map([
|
|
34
48
|
['结论', 'conclusion'], ['总结', 'conclusion'], ['答案', 'conclusion'],
|
|
35
49
|
['判断', 'decision'], ['决策', 'decision'],
|
|
@@ -454,6 +468,115 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
454
468
|
}
|
|
455
469
|
}
|
|
456
470
|
|
|
471
|
+
/**
|
|
472
|
+
* 在插件板块的插件详情页注册配置区(plugins.bundle.config,键 = 包名)。
|
|
473
|
+
* 宿主不会从 Config schema 自动生成表单——配置页必须由插件自己的浏览器半侧注册;
|
|
474
|
+
* 宿主 describe 未服务本命名空间(whileServed 不触发)或 primitives 缺失时不出配置页,
|
|
475
|
+
* 且任何失败都不影响视觉层与消息渲染。
|
|
476
|
+
*/
|
|
477
|
+
function setupConfigPage(child, form) {
|
|
478
|
+
if (typeof require !== 'function') return
|
|
479
|
+
if (typeof child?.configForms?.whileServed !== 'function') return
|
|
480
|
+
if (typeof child?.slots?.inject !== 'function' || typeof child?.slots?.register !== 'function') return
|
|
481
|
+
if (typeof form?.set !== 'function') return
|
|
482
|
+
let jsx
|
|
483
|
+
let jsxs
|
|
484
|
+
let ui
|
|
485
|
+
try {
|
|
486
|
+
const runtime = require('react/jsx-runtime')
|
|
487
|
+
jsx = runtime.jsx
|
|
488
|
+
jsxs = runtime.jsxs
|
|
489
|
+
ui = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
490
|
+
if (typeof ui?.Switch !== 'function' || typeof ui?.SegmentedControl !== 'function') return
|
|
491
|
+
} catch {
|
|
492
|
+
// primitives 不可用(非 desktop 表层):不出配置页。
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const labelStyle = { fontSize: '14px', lineHeight: '20px', color: 'var(--dsw-alias-label-primary, inherit)' }
|
|
497
|
+
const hintStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', marginTop: '2px' }
|
|
498
|
+
const rowStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', padding: '10px 0', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))' }
|
|
499
|
+
const noteStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', margin: '6px 0 2px' }
|
|
500
|
+
|
|
501
|
+
const fieldValue = (value, spec) => {
|
|
502
|
+
const raw = value?.[spec.field]
|
|
503
|
+
if (raw && typeof raw.get === 'function') return raw.get()
|
|
504
|
+
return raw === undefined || raw === null ? spec.fallback : raw
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const renderRow = (state, setField, spec) => {
|
|
508
|
+
const writable = state.writable !== false
|
|
509
|
+
const control = spec.kind === 'switch'
|
|
510
|
+
? jsx(ui.Switch, {
|
|
511
|
+
checked: fieldValue(state.value, spec) === true,
|
|
512
|
+
disabled: !writable,
|
|
513
|
+
label: spec.label,
|
|
514
|
+
onChange: (next) => setField(spec.field, next === true),
|
|
515
|
+
})
|
|
516
|
+
: jsx(ui.SegmentedControl, {
|
|
517
|
+
id: 'dsh-structured-' + spec.field,
|
|
518
|
+
value: String(fieldValue(state.value, spec)),
|
|
519
|
+
options: spec.options.map(([value, text]) => ({ value, label: text })),
|
|
520
|
+
disabled: !writable,
|
|
521
|
+
label: spec.label,
|
|
522
|
+
onChange: (next) => setField(spec.field, next),
|
|
523
|
+
})
|
|
524
|
+
return jsxs('div', {
|
|
525
|
+
style: rowStyle,
|
|
526
|
+
children: [
|
|
527
|
+
jsxs('div', { style: { minWidth: 0 }, children: [
|
|
528
|
+
jsx('div', { style: labelStyle, children: spec.label }),
|
|
529
|
+
jsx('div', { style: hintStyle, children: spec.hint }),
|
|
530
|
+
] }),
|
|
531
|
+
control,
|
|
532
|
+
],
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const StructuredConfigCard = (props) => {
|
|
537
|
+
if (props.view !== undefined && props.view !== 'page') return null
|
|
538
|
+
const read = props.useStructuredConfig ?? props.structuredConfig
|
|
539
|
+
const state = typeof read === 'function' ? read((snapshot) => snapshot) : null
|
|
540
|
+
if (!state || state.status !== 'ready' || typeof state.value !== 'object' || state.value === null) {
|
|
541
|
+
return jsx('p', { style: noteStyle, children: '插件未加载或配置暂不可用。' })
|
|
542
|
+
}
|
|
543
|
+
const setField = typeof props.setField === 'function' ? props.setField : () => {}
|
|
544
|
+
return jsxs('div', {
|
|
545
|
+
'data-dsh-structured-config': '1',
|
|
546
|
+
children: [
|
|
547
|
+
state.writable === false
|
|
548
|
+
? jsx('p', { style: noteStyle, children: '当前配置被更高层覆盖(home patch / CLI overlay),此处只读。' })
|
|
549
|
+
: null,
|
|
550
|
+
jsx('div', { children: CONFIG_FIELDS.map((spec) => renderRow(state, setField, spec)) }),
|
|
551
|
+
jsx('p', { style: noteStyle, children: '会话级开关请用 /structured on | off(按会话生效,不写入这里)。' }),
|
|
552
|
+
],
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
child.effect?.(
|
|
557
|
+
() =>
|
|
558
|
+
child.configForms.whileServed([CONFIG_NS], () =>
|
|
559
|
+
child.slots.inject(BUNDLE_CONFIG_SLOT, () =>
|
|
560
|
+
child.slots.register({
|
|
561
|
+
name: BUNDLE_CONFIG_SLOT,
|
|
562
|
+
key: 'dsh-structured',
|
|
563
|
+
inject: () => ({
|
|
564
|
+
hooks: { structuredConfig: form },
|
|
565
|
+
setField: (field, value) => {
|
|
566
|
+
try {
|
|
567
|
+
form.set(field, value)
|
|
568
|
+
} catch {
|
|
569
|
+
// 写入失败不打断页面;快照未变化即用户可见的反馈。
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
}),
|
|
573
|
+
}, StructuredConfigCard),
|
|
574
|
+
),
|
|
575
|
+
),
|
|
576
|
+
'dsh-structured: bundle config page',
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
457
580
|
let active = false
|
|
458
581
|
let disposeLayer = null
|
|
459
582
|
let currentTheme = 'deepseek'
|
|
@@ -548,7 +671,7 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
548
671
|
|
|
549
672
|
return {
|
|
550
673
|
inject: [],
|
|
551
|
-
__test: { inDiff, isDiffCode, decorate },
|
|
674
|
+
__test: { inDiff, isDiffCode, decorate, setupConfigPage },
|
|
552
675
|
apply(ctx) {
|
|
553
676
|
// 默认主题/视觉先启用;随后由 profile Config 的 theme/visual 覆盖。
|
|
554
677
|
enable()
|
|
@@ -578,6 +701,19 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
578
701
|
} catch {
|
|
579
702
|
// 无 ctx.inject 的宿主保持默认开启。
|
|
580
703
|
}
|
|
704
|
+
try {
|
|
705
|
+
// 插件详情页配置区与视觉层分开注入:配置页失败不影响主题/视觉联动。
|
|
706
|
+
ctx?.inject?.(['configForms', 'slots'], (child) => {
|
|
707
|
+
try {
|
|
708
|
+
const form = child.configForms?.get('dsh-structured')
|
|
709
|
+
if (form) setupConfigPage(child, form)
|
|
710
|
+
} catch {
|
|
711
|
+
// 配置页不可用不影响视觉层与注入。
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
} catch {
|
|
715
|
+
// 无 ctx.inject 的宿主没有配置页,视觉层照常。
|
|
716
|
+
}
|
|
581
717
|
try {
|
|
582
718
|
ctx?.effect?.(() => disable, 'dsh-structured: visual layer')
|
|
583
719
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-structured",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Structured output for DSH: no-label title card first, then tables/lists/steps; per-session switch and style.md override. 让 DSH 的每条实质回复默认结构化。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/screenshots.json
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
"assets/screenshots/01-title-card.png",
|
|
3
3
|
"assets/screenshots/06-status-table.png",
|
|
4
4
|
"assets/screenshots/07-code-block.png",
|
|
5
|
-
"assets/screenshots/08-narrow.png"
|
|
5
|
+
"assets/screenshots/08-narrow.png",
|
|
6
|
+
"assets/screenshots/09-theme-light.png",
|
|
7
|
+
"assets/screenshots/10-theme-dark.png"
|
|
6
8
|
]
|
package/src/client.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
window.__ModuleLoader__.load({
|
|
12
12
|
id: 'dsh-structured',
|
|
13
|
-
factory() {
|
|
13
|
+
factory(require) {
|
|
14
14
|
const CARD_ATTR = 'data-dsh-structured-card'
|
|
15
15
|
const CARD_TYPE_ATTR = 'data-dsh-structured-card-type'
|
|
16
16
|
const ICON_ATTR = 'data-dsh-structured-icon'
|
|
@@ -28,6 +28,20 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
const STYLE_ID = 'dsh-structured-skin-style'
|
|
29
29
|
const LEGACY_STYLE_ID = 'dsh-structured-card-style'
|
|
30
30
|
|
|
31
|
+
/** 配置页常量:宿主插件详情页的 bundle 配置 slot(键 = 包名)。 */
|
|
32
|
+
const CONFIG_NS = 'dsh-structured'
|
|
33
|
+
const BUNDLE_CONFIG_SLOT = 'plugins.bundle.config'
|
|
34
|
+
|
|
35
|
+
/** 六个配置字段的界面文案;field 与 host Config schema 对齐,fallback 为 schema 默认值。 */
|
|
36
|
+
const CONFIG_FIELDS = [
|
|
37
|
+
{ field: 'theme', kind: 'segmented', fallback: 'deepseek', label: '主题 / Theme', hint: '回复区配色,改完立即生效', options: [['deepseek', 'DeepSeek'], ['codex', 'Codex'], ['anthropic', 'Anthropic']] },
|
|
38
|
+
{ field: 'visual', kind: 'switch', fallback: true, label: '视觉层 / Visual layer', hint: '关闭后不加载回复区样式(标题卡、表格、代码块皮肤)' },
|
|
39
|
+
{ field: 'defaultEnabled', kind: 'switch', fallback: true, label: '新会话默认开启 / Default on', hint: '新会话默认注入结构化提示词' },
|
|
40
|
+
{ field: 'tailReminder', kind: 'switch', fallback: true, label: '尾部提醒 / Tail reminder', hint: '在 system prompt 末尾追加一句结构化提醒' },
|
|
41
|
+
{ field: 'includeSubagents', kind: 'switch', fallback: false, label: '子代理注入 / Subagents', hint: '子代理会话也注入(其产出不直接面向用户)' },
|
|
42
|
+
{ field: 'genui', kind: 'segmented', fallback: 'auto', label: 'GenUI 联动 / GenUI routing', hint: 'auto:检测到 dsh-genui 才追加 dsh-ui 路由规则', options: [['auto', 'auto'], ['on', 'on'], ['off', 'off']] },
|
|
43
|
+
]
|
|
44
|
+
|
|
31
45
|
const CARD_TYPES = new Map([
|
|
32
46
|
['结论', 'conclusion'], ['总结', 'conclusion'], ['答案', 'conclusion'],
|
|
33
47
|
['判断', 'decision'], ['决策', 'decision'],
|
|
@@ -452,6 +466,115 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
452
466
|
}
|
|
453
467
|
}
|
|
454
468
|
|
|
469
|
+
/**
|
|
470
|
+
* 在插件板块的插件详情页注册配置区(plugins.bundle.config,键 = 包名)。
|
|
471
|
+
* 宿主不会从 Config schema 自动生成表单——配置页必须由插件自己的浏览器半侧注册;
|
|
472
|
+
* 宿主 describe 未服务本命名空间(whileServed 不触发)或 primitives 缺失时不出配置页,
|
|
473
|
+
* 且任何失败都不影响视觉层与消息渲染。
|
|
474
|
+
*/
|
|
475
|
+
function setupConfigPage(child, form) {
|
|
476
|
+
if (typeof require !== 'function') return
|
|
477
|
+
if (typeof child?.configForms?.whileServed !== 'function') return
|
|
478
|
+
if (typeof child?.slots?.inject !== 'function' || typeof child?.slots?.register !== 'function') return
|
|
479
|
+
if (typeof form?.set !== 'function') return
|
|
480
|
+
let jsx
|
|
481
|
+
let jsxs
|
|
482
|
+
let ui
|
|
483
|
+
try {
|
|
484
|
+
const runtime = require('react/jsx-runtime')
|
|
485
|
+
jsx = runtime.jsx
|
|
486
|
+
jsxs = runtime.jsxs
|
|
487
|
+
ui = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
488
|
+
if (typeof ui?.Switch !== 'function' || typeof ui?.SegmentedControl !== 'function') return
|
|
489
|
+
} catch {
|
|
490
|
+
// primitives 不可用(非 desktop 表层):不出配置页。
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const labelStyle = { fontSize: '14px', lineHeight: '20px', color: 'var(--dsw-alias-label-primary, inherit)' }
|
|
495
|
+
const hintStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', marginTop: '2px' }
|
|
496
|
+
const rowStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', padding: '10px 0', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))' }
|
|
497
|
+
const noteStyle = { fontSize: '12.5px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)', margin: '6px 0 2px' }
|
|
498
|
+
|
|
499
|
+
const fieldValue = (value, spec) => {
|
|
500
|
+
const raw = value?.[spec.field]
|
|
501
|
+
if (raw && typeof raw.get === 'function') return raw.get()
|
|
502
|
+
return raw === undefined || raw === null ? spec.fallback : raw
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const renderRow = (state, setField, spec) => {
|
|
506
|
+
const writable = state.writable !== false
|
|
507
|
+
const control = spec.kind === 'switch'
|
|
508
|
+
? jsx(ui.Switch, {
|
|
509
|
+
checked: fieldValue(state.value, spec) === true,
|
|
510
|
+
disabled: !writable,
|
|
511
|
+
label: spec.label,
|
|
512
|
+
onChange: (next) => setField(spec.field, next === true),
|
|
513
|
+
})
|
|
514
|
+
: jsx(ui.SegmentedControl, {
|
|
515
|
+
id: 'dsh-structured-' + spec.field,
|
|
516
|
+
value: String(fieldValue(state.value, spec)),
|
|
517
|
+
options: spec.options.map(([value, text]) => ({ value, label: text })),
|
|
518
|
+
disabled: !writable,
|
|
519
|
+
label: spec.label,
|
|
520
|
+
onChange: (next) => setField(spec.field, next),
|
|
521
|
+
})
|
|
522
|
+
return jsxs('div', {
|
|
523
|
+
style: rowStyle,
|
|
524
|
+
children: [
|
|
525
|
+
jsxs('div', { style: { minWidth: 0 }, children: [
|
|
526
|
+
jsx('div', { style: labelStyle, children: spec.label }),
|
|
527
|
+
jsx('div', { style: hintStyle, children: spec.hint }),
|
|
528
|
+
] }),
|
|
529
|
+
control,
|
|
530
|
+
],
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const StructuredConfigCard = (props) => {
|
|
535
|
+
if (props.view !== undefined && props.view !== 'page') return null
|
|
536
|
+
const read = props.useStructuredConfig ?? props.structuredConfig
|
|
537
|
+
const state = typeof read === 'function' ? read((snapshot) => snapshot) : null
|
|
538
|
+
if (!state || state.status !== 'ready' || typeof state.value !== 'object' || state.value === null) {
|
|
539
|
+
return jsx('p', { style: noteStyle, children: '插件未加载或配置暂不可用。' })
|
|
540
|
+
}
|
|
541
|
+
const setField = typeof props.setField === 'function' ? props.setField : () => {}
|
|
542
|
+
return jsxs('div', {
|
|
543
|
+
'data-dsh-structured-config': '1',
|
|
544
|
+
children: [
|
|
545
|
+
state.writable === false
|
|
546
|
+
? jsx('p', { style: noteStyle, children: '当前配置被更高层覆盖(home patch / CLI overlay),此处只读。' })
|
|
547
|
+
: null,
|
|
548
|
+
jsx('div', { children: CONFIG_FIELDS.map((spec) => renderRow(state, setField, spec)) }),
|
|
549
|
+
jsx('p', { style: noteStyle, children: '会话级开关请用 /structured on | off(按会话生效,不写入这里)。' }),
|
|
550
|
+
],
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
child.effect?.(
|
|
555
|
+
() =>
|
|
556
|
+
child.configForms.whileServed([CONFIG_NS], () =>
|
|
557
|
+
child.slots.inject(BUNDLE_CONFIG_SLOT, () =>
|
|
558
|
+
child.slots.register({
|
|
559
|
+
name: BUNDLE_CONFIG_SLOT,
|
|
560
|
+
key: 'dsh-structured',
|
|
561
|
+
inject: () => ({
|
|
562
|
+
hooks: { structuredConfig: form },
|
|
563
|
+
setField: (field, value) => {
|
|
564
|
+
try {
|
|
565
|
+
form.set(field, value)
|
|
566
|
+
} catch {
|
|
567
|
+
// 写入失败不打断页面;快照未变化即用户可见的反馈。
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
}),
|
|
571
|
+
}, StructuredConfigCard),
|
|
572
|
+
),
|
|
573
|
+
),
|
|
574
|
+
'dsh-structured: bundle config page',
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
455
578
|
let active = false
|
|
456
579
|
let disposeLayer = null
|
|
457
580
|
let currentTheme = 'deepseek'
|
|
@@ -546,7 +669,7 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
546
669
|
|
|
547
670
|
return {
|
|
548
671
|
inject: [],
|
|
549
|
-
__test: { inDiff, isDiffCode, decorate },
|
|
672
|
+
__test: { inDiff, isDiffCode, decorate, setupConfigPage },
|
|
550
673
|
apply(ctx) {
|
|
551
674
|
// 默认主题/视觉先启用;随后由 profile Config 的 theme/visual 覆盖。
|
|
552
675
|
enable()
|
|
@@ -576,6 +699,19 @@ body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] [class*="_markdo
|
|
|
576
699
|
} catch {
|
|
577
700
|
// 无 ctx.inject 的宿主保持默认开启。
|
|
578
701
|
}
|
|
702
|
+
try {
|
|
703
|
+
// 插件详情页配置区与视觉层分开注入:配置页失败不影响主题/视觉联动。
|
|
704
|
+
ctx?.inject?.(['configForms', 'slots'], (child) => {
|
|
705
|
+
try {
|
|
706
|
+
const form = child.configForms?.get('dsh-structured')
|
|
707
|
+
if (form) setupConfigPage(child, form)
|
|
708
|
+
} catch {
|
|
709
|
+
// 配置页不可用不影响视觉层与注入。
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
} catch {
|
|
713
|
+
// 无 ctx.inject 的宿主没有配置页,视觉层照常。
|
|
714
|
+
}
|
|
579
715
|
try {
|
|
580
716
|
ctx?.effect?.(() => disable, 'dsh-structured: visual layer')
|
|
581
717
|
} catch {
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import test from 'node:test'
|
|
4
|
+
import vm from 'node:vm'
|
|
5
|
+
|
|
6
|
+
// 插件板块详情页配置区:宿主不会从 Config schema 自动生成表单,
|
|
7
|
+
// 配置页必须由插件 client 注册进 plugins.bundle.config(键 = 包名)。
|
|
8
|
+
// 本文件验证:注册条件、slot 键、注入面、卡片渲染与字段控件。
|
|
9
|
+
|
|
10
|
+
function makeDom() {
|
|
11
|
+
const element = () => ({
|
|
12
|
+
children: [],
|
|
13
|
+
appendChild() {},
|
|
14
|
+
remove() {},
|
|
15
|
+
setAttribute() {},
|
|
16
|
+
removeAttribute() {},
|
|
17
|
+
querySelectorAll() {
|
|
18
|
+
return []
|
|
19
|
+
},
|
|
20
|
+
closest() {
|
|
21
|
+
return null
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
return {
|
|
25
|
+
body: element(),
|
|
26
|
+
documentElement: { setAttribute() {}, removeAttribute() {} },
|
|
27
|
+
head: { children: [], appendChild() {} },
|
|
28
|
+
createElement: () => element(),
|
|
29
|
+
getElementById: () => null,
|
|
30
|
+
querySelectorAll: () => [],
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fakeRuntime() {
|
|
35
|
+
const node = (type, props) => ({ type, props })
|
|
36
|
+
return {
|
|
37
|
+
require(spec) {
|
|
38
|
+
if (spec === 'react/jsx-runtime') {
|
|
39
|
+
return { jsx: (type, props) => node(type, props), jsxs: (type, props) => node(type, props) }
|
|
40
|
+
}
|
|
41
|
+
if (spec === '@deepseek-ai/dsh-client-ui-primitives') {
|
|
42
|
+
return {
|
|
43
|
+
Switch: function Switch(props) {
|
|
44
|
+
return { type: Switch, props }
|
|
45
|
+
},
|
|
46
|
+
SegmentedControl: function SegmentedControl(props) {
|
|
47
|
+
return { type: SegmentedControl, props }
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new Error('unexpected require: ' + spec)
|
|
52
|
+
},
|
|
53
|
+
node,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadClient(dom, fakeRequire) {
|
|
58
|
+
const code = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
|
|
59
|
+
let module = null
|
|
60
|
+
const window = {
|
|
61
|
+
__ModuleLoader__: {
|
|
62
|
+
load(definition) {
|
|
63
|
+
module = definition.factory(fakeRequire)
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
vm.runInNewContext(code, {
|
|
68
|
+
window,
|
|
69
|
+
document: dom,
|
|
70
|
+
MutationObserver: class {
|
|
71
|
+
observe() {}
|
|
72
|
+
disconnect() {}
|
|
73
|
+
},
|
|
74
|
+
requestAnimationFrame: (fn) => fn(),
|
|
75
|
+
cancelAnimationFrame() {},
|
|
76
|
+
setTimeout: (fn) => fn(),
|
|
77
|
+
clearTimeout() {},
|
|
78
|
+
console: { info() {}, warn() {}, error() {} },
|
|
79
|
+
}, { filename: 'src/client.js' })
|
|
80
|
+
assert.ok(module, 'client module should register')
|
|
81
|
+
return module
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makeChild(form, slots) {
|
|
85
|
+
return {
|
|
86
|
+
configForms: {
|
|
87
|
+
get(id) {
|
|
88
|
+
return id === 'dsh-structured' ? form : undefined
|
|
89
|
+
},
|
|
90
|
+
whileServed(namespaces, register) {
|
|
91
|
+
if (served) register()
|
|
92
|
+
return () => {}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
slots,
|
|
96
|
+
effect(fn) {
|
|
97
|
+
return fn()
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let served = true
|
|
103
|
+
|
|
104
|
+
function collectRegistration() {
|
|
105
|
+
let registered = null
|
|
106
|
+
const slots = {
|
|
107
|
+
inject(name, factory) {
|
|
108
|
+
assert.equal(name, 'plugins.bundle.config')
|
|
109
|
+
factory()
|
|
110
|
+
},
|
|
111
|
+
register(options, component) {
|
|
112
|
+
registered = { options, component }
|
|
113
|
+
return () => {}
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
return { slots, get: () => registered }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test('config page:宿主服务 describe 命名空间时注册 bundle 配置页', () => {
|
|
120
|
+
served = true
|
|
121
|
+
const { require: fakeRequire } = fakeRuntime()
|
|
122
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
123
|
+
const snapshot = { status: 'ready', writable: true, value: { theme: 'deepseek' } }
|
|
124
|
+
const form = {
|
|
125
|
+
setCalls: [],
|
|
126
|
+
set(field, value) {
|
|
127
|
+
this.setCalls.push([field, value])
|
|
128
|
+
return Promise.resolve(true)
|
|
129
|
+
},
|
|
130
|
+
getSnapshot() {
|
|
131
|
+
return snapshot
|
|
132
|
+
},
|
|
133
|
+
subscribe() {
|
|
134
|
+
return () => {}
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
const { slots, get } = collectRegistration()
|
|
138
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
139
|
+
|
|
140
|
+
const registration = get()
|
|
141
|
+
assert.ok(registration, 'should register into plugins.bundle.config')
|
|
142
|
+
assert.equal(registration.options.name, 'plugins.bundle.config')
|
|
143
|
+
assert.equal(registration.options.key, 'dsh-structured')
|
|
144
|
+
|
|
145
|
+
const face = registration.options.inject()
|
|
146
|
+
assert.equal(face.hooks.structuredConfig, form)
|
|
147
|
+
assert.equal(typeof face.setField, 'function')
|
|
148
|
+
face.setField('visual', false)
|
|
149
|
+
assert.deepEqual(form.setCalls[0], ['visual', false])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('config page:describe 未服务命名空间时不注册', () => {
|
|
153
|
+
served = false
|
|
154
|
+
const { require: fakeRequire } = fakeRuntime()
|
|
155
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
156
|
+
const form = { getSnapshot() { return null }, subscribe() { return () => {} } }
|
|
157
|
+
const { slots, get } = collectRegistration()
|
|
158
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
159
|
+
assert.equal(get(), null)
|
|
160
|
+
served = true
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('config page:primitives 或 require 缺失时静默跳过', () => {
|
|
164
|
+
const module = loadClient(makeDom(), undefined)
|
|
165
|
+
const form = { getSnapshot() { return null }, subscribe() { return () => {} } }
|
|
166
|
+
const { slots, get } = collectRegistration()
|
|
167
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
168
|
+
assert.equal(get(), null)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('config page:卡片渲染六字段,switch/segmented 值来自快照', () => {
|
|
172
|
+
served = true
|
|
173
|
+
const { require: fakeRequire, node } = fakeRuntime()
|
|
174
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
175
|
+
const snapshot = {
|
|
176
|
+
status: 'ready',
|
|
177
|
+
writable: true,
|
|
178
|
+
value: {
|
|
179
|
+
theme: 'codex',
|
|
180
|
+
visual: false,
|
|
181
|
+
defaultEnabled: true,
|
|
182
|
+
tailReminder: true,
|
|
183
|
+
includeSubagents: false,
|
|
184
|
+
genui: 'auto',
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
const form = {
|
|
188
|
+
set() {
|
|
189
|
+
return Promise.resolve(true)
|
|
190
|
+
},
|
|
191
|
+
getSnapshot() {
|
|
192
|
+
return snapshot
|
|
193
|
+
},
|
|
194
|
+
subscribe() {
|
|
195
|
+
return () => {}
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
const { slots, get } = collectRegistration()
|
|
199
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
200
|
+
const { component } = get()
|
|
201
|
+
|
|
202
|
+
const tree = component({
|
|
203
|
+
view: 'page',
|
|
204
|
+
useStructuredConfig: (selector) => selector(snapshot),
|
|
205
|
+
setField: () => {},
|
|
206
|
+
})
|
|
207
|
+
assert.equal(tree.props['data-dsh-structured-config'], '1')
|
|
208
|
+
|
|
209
|
+
const flat = []
|
|
210
|
+
const walk = (value) => {
|
|
211
|
+
if (!value || typeof value !== 'object') return
|
|
212
|
+
flat.push(value)
|
|
213
|
+
if (Array.isArray(value.props?.children)) value.props.children.forEach((child) => walk(child))
|
|
214
|
+
else walk(value.props?.children)
|
|
215
|
+
}
|
|
216
|
+
walk(tree)
|
|
217
|
+
|
|
218
|
+
const switches = flat.filter((element) => element.type?.name === 'Switch')
|
|
219
|
+
const segmented = flat.filter((element) => element.type?.name === 'SegmentedControl')
|
|
220
|
+
assert.equal(switches.length, 4)
|
|
221
|
+
assert.equal(segmented.length, 2)
|
|
222
|
+
|
|
223
|
+
const visual = switches.find((element) => element.props.label.includes('视觉层'))
|
|
224
|
+
assert.equal(visual.props.checked, false)
|
|
225
|
+
const includeSubagents = switches.find((element) => element.props.label.includes('子代理'))
|
|
226
|
+
assert.equal(includeSubagents.props.checked, false)
|
|
227
|
+
const theme = segmented.find((element) => element.props.id === 'dsh-structured-theme')
|
|
228
|
+
assert.equal(theme.props.value, 'codex')
|
|
229
|
+
// 卡片在 vm 沙箱里构建:数组跨 realm,序列化后比较。
|
|
230
|
+
assert.equal(JSON.stringify(theme.props.options.map((option) => option.value)), JSON.stringify(['deepseek', 'codex', 'anthropic']))
|
|
231
|
+
const genui = segmented.find((element) => element.props.id === 'dsh-structured-genui')
|
|
232
|
+
assert.equal(genui.props.value, 'auto')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('config page:插件未就绪与只读态的降级渲染', () => {
|
|
236
|
+
served = true
|
|
237
|
+
const { require: fakeRequire } = fakeRuntime()
|
|
238
|
+
const module = loadClient(makeDom(), fakeRequire)
|
|
239
|
+
const form = {
|
|
240
|
+
set() {
|
|
241
|
+
return Promise.resolve(true)
|
|
242
|
+
},
|
|
243
|
+
getSnapshot() {
|
|
244
|
+
return null
|
|
245
|
+
},
|
|
246
|
+
subscribe() {
|
|
247
|
+
return () => {}
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
const { slots, get } = collectRegistration()
|
|
251
|
+
module.__test.setupConfigPage(makeChild(form, slots), form)
|
|
252
|
+
const { component } = get()
|
|
253
|
+
|
|
254
|
+
const unavailable = component({ view: 'page', useStructuredConfig: (selector) => selector({ status: 'unavailable' }) })
|
|
255
|
+
assert.match(String(unavailable.props.children), /暂不可用/)
|
|
256
|
+
|
|
257
|
+
const readonly = component({
|
|
258
|
+
view: 'page',
|
|
259
|
+
useStructuredConfig: (selector) => selector({ status: 'ready', writable: false, value: { theme: 'deepseek' } }),
|
|
260
|
+
setField: () => {},
|
|
261
|
+
})
|
|
262
|
+
const flat = []
|
|
263
|
+
const walk = (value) => {
|
|
264
|
+
if (!value || typeof value !== 'object') return
|
|
265
|
+
flat.push(value)
|
|
266
|
+
if (Array.isArray(value.props?.children)) value.props.children.forEach((child) => walk(child))
|
|
267
|
+
else walk(value.props?.children)
|
|
268
|
+
}
|
|
269
|
+
walk(readonly)
|
|
270
|
+
const switches = flat.filter((element) => element.type?.name === 'Switch')
|
|
271
|
+
assert.equal(switches.length, 4)
|
|
272
|
+
for (const element of switches) assert.equal(element.props.disabled, true)
|
|
273
|
+
})
|