@starwish-ai/xingyuan-dsh 0.6.2 → 0.6.4

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 CHANGED
@@ -5,6 +5,69 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.4] - 2026-09-11
9
+
10
+ ### Changed
11
+ - **dsh 0.1.5-rc.2 compatibility**: peer and dev dependencies moved from
12
+ `0.1.2-rc.1` to `^0.1.5-rc.2`. Every host/client API this plugin consumes was
13
+ re-verified against the installed 0.1.5-rc.2 build — tool registration,
14
+ settings namespaces, storage backend/domain, user questions, system prompt,
15
+ conversation cards and session tabs are unchanged; the client half needs no
16
+ code changes. XingYuan's prompt sections (orders 5–115) now naturally precede
17
+ the host's relocated sections (10000+).
18
+
19
+ ### Fixed
20
+ - **Session log self-heal now covers format v3**: dsh 0.1.5 names session
21
+ artifacts `session.v{N}.jsonl[.zstd]`; the repair module now selects the
22
+ highest stored generation per session instead of the fixed `session.jsonl`
23
+ names, so `xingyuan/*` events in new sessions keep receiving the `ignorable`
24
+ marker before a cold load.
25
+ - **Legacy sessions with XingYuan cards open again**: dsh 0.1.5's v0/v1/v2 → v3
26
+ migration chain rejects every unknown historical event, `ignorable` included,
27
+ which previously made a session containing `xingyuan/*` events fail to load as
28
+ a whole. Legacy artifacts are now de-poisoned ahead of migration: those event
29
+ rows are replaced with the official inert `hook/invoked` event (seq/time
30
+ preserved, payload valid across every migration edge), so the session migrates
31
+ and opens; other rows stay byte-identical and files without XingYuan events
32
+ are never written. Trade-off: historical cards in those old sessions no longer
33
+ replay (business data lives in SQLite; the pre-repair file is backed up under
34
+ `~/.dsh/xingyuan/session-backups/`).
35
+
36
+ ## [0.6.3] - 2026-09-06
37
+
38
+ ### Changed
39
+ - **Settings page information architecture**: the former single "chat preferences"
40
+ card is split into two focused cards — "Confirmation before writes" (master
41
+ switch + six category rows + an always-on badge for deletes) and "Chat
42
+ preferences" (memory injection limit + confirmation-card language). Toggle and
43
+ enum settings now follow the settings-row convention used by OS settings pages
44
+ (label + description left, control right, hairline dividers); form fields keep
45
+ the label-above layout. Both themes and narrow viewports verified against the
46
+ static style mock.
47
+ - Confirmation-card language uses a segmented control (consistent with coach
48
+ style / tab visibility) instead of a dropdown; tab visibility chips gain a ✓
49
+ prefix to distinguish multi-select chips from single-select segmented controls.
50
+ - The locked delete category shows an "Always on" badge instead of a disabled
51
+ checkbox: a disabled control read as "broken", the badge states the fact.
52
+ - Coach style hint is static guidance (the segmented buttons already carry the
53
+ current selection).
54
+
55
+ ### Fixed
56
+ - Disabled controls now look disabled: custom toggles and segmented buttons get
57
+ explicit grey-out styling when the settings namespace is read-only or a write
58
+ is in flight (they ignore native disabled rendering).
59
+ - With the write-confirmation master switch off, the locked delete row is no
60
+ longer greyed out — it is not governed by the master switch, and the dimmed
61
+ label contradicted its "Always on" badge.
62
+ - Entering an out-of-range memory injection limit (e.g. 300) shows an
63
+ informational note that the nearest bound was saved, instead of an "enter an
64
+ integer between 5 and 200" error while the clamped value had in fact been
65
+ saved. Genuinely invalid input (non-integers) keeps the inline error.
66
+ - Free-standing hint lines (namespace notice, trailing data note) align with the
67
+ card text column instead of hugging the container's left edge.
68
+ - Memory-limit input: `autocomplete="off"` and tabular numerals; corrected a
69
+ stale style comment.
70
+
8
71
  ## [0.6.2] - 2026-09-06
9
72
 
10
73
  ### Added
package/README.en-US.md CHANGED
@@ -25,6 +25,9 @@ Tools are only mounted on sessions using the XingYuan preset; other sessions are
25
25
 
26
26
  ## Install
27
27
 
28
+ > Requires DeepSeek Harness `0.1.5-rc.2` (peer dependencies are pinned to that
29
+ > release; check the upgrade notes in [AGENTS.md](./AGENTS.md) before bumping dsh).
30
+
28
31
  ```sh
29
32
  dsh plugin --profile web add @starwish-ai/xingyuan-dsh
30
33
  ```
package/README.md CHANGED
@@ -27,6 +27,9 @@
27
27
 
28
28
  ## 安装
29
29
 
30
+ > 需要 DeepSeek Harness `0.1.5-rc.2`(peer 依赖按该版本锁定;升级 dsh 前先核对
31
+ > [AGENTS.md](./AGENTS.md) 的升级要点)。
32
+
30
33
  ```sh
31
34
  dsh plugin --profile web add @starwish-ai/xingyuan-dsh
32
35
  ```
@@ -255,27 +255,28 @@ export const ZH_DICT = {
255
255
  'settings.coach.gentle': '温柔型',
256
256
  'settings.coach.strict': '严格型',
257
257
  'settings.coach.humorous': '幽默型',
258
- 'settings.coach.current': '当前:{label}。决定对话语气与人设,也可在对话中说「对我严格一点」。',
258
+ 'settings.coach.hint': '决定对话语气与人设,也可在对话中说「对我严格一点」。',
259
259
  'settings.coach.saved': '教练风格已更新,对后续对话生效',
260
260
  'settings.coach.loadFailed': '加载失败:{error}',
261
261
  'settings.profile.title': '昵称与画像',
262
262
  'settings.profile.nickname': '昵称',
263
263
  'settings.profile.occupation': '职业',
264
264
  'settings.profile.interests': '兴趣',
265
- 'settings.profile.nicknamePlaceholder': '昵称:希望被怎么称呼(留空清除)',
266
- 'settings.profile.occupationPlaceholder': '职业(留空清除)',
267
- 'settings.profile.interestsPlaceholder': '兴趣:用顿号或逗号分隔(如 阅读、跑步)',
265
+ 'settings.profile.nicknamePlaceholder': '希望被怎么称呼',
266
+ 'settings.profile.occupationPlaceholder': '如:教师、产品经理',
267
+ 'settings.profile.interestsPlaceholder': '如:阅读、跑步',
268
268
  'settings.profile.save': '保存画像',
269
269
  'settings.profile.saving': '保存中…',
270
270
  'settings.profile.saved': '画像已保存',
271
- 'settings.profile.sharedHint': '与对话侧共享同一份档案;对话里说「叫我小星」也会更新。',
271
+ 'settings.profile.sharedHint': '留空的字段会被清除;画像与对话侧共享,对话里说「叫我小星」也会更新。',
272
272
  'settings.pref.title': '对话偏好',
273
273
  'settings.pref.unavailable': '当前连接不支持持久化偏好(远程或临时模式),以下选项不可用。',
274
274
  'settings.pref.loading': '偏好加载中…',
275
275
  'settings.pref.notRegistered': '偏好暂不可用(组件未就绪),请稍后重试。',
276
276
  'settings.pref.readOnly': '当前设置为只读,无法保存修改。',
277
- 'settings.pref.confirmWrites': '写操作二次确认',
278
- 'settings.pref.confirmWritesHint': '总开关:开启后按下方类目决定是否弹确认卡,关闭后除删除(始终确认)外一律直接执行。',
277
+ 'settings.confirm.title': '写操作确认',
278
+ 'settings.confirm.master': '总开关',
279
+ 'settings.pref.confirmWritesHint': '开启后按下方类目决定是否弹确认卡;关闭后除删除(始终确认)外一律直接执行。',
279
280
  'settings.pref.confirmOps.group': '确认类目',
280
281
  'settings.pref.confirmOps.create': '创建愿望/任务(含微行动拆解)',
281
282
  'settings.pref.confirmOps.checkin': '打卡',
@@ -284,11 +285,13 @@ export const ZH_DICT = {
284
285
  'settings.pref.confirmOps.update': '修改愿望/任务/分类',
285
286
  'settings.pref.confirmOps.memorySave': '保存记忆',
286
287
  'settings.pref.confirmOps.delete': '删除(含批量、微行动重开)',
288
+ 'settings.pref.confirmOps.locked': '始终开启',
287
289
  'settings.pref.confirmOps.deleteHint': '始终确认,不可关闭:删除不可恢复。',
288
290
  'settings.pref.confirmOps.hint': '类目明细在总开关开启时生效;默认与之前版本一致。',
289
291
  'settings.pref.memoryLimit': '记忆注入上限',
290
292
  'settings.pref.memoryLimitHint': '每次对话自动注入上下文的记忆条数上限(5-200,默认 40);失焦后保存。',
291
293
  'settings.pref.limitInvalid': '请输入 5-200 的整数',
294
+ 'settings.pref.limitClamped': '超出 5-200 范围,已按最近的边界值保存',
292
295
  'settings.pref.writeFailed': '保存未生效,请重试。',
293
296
  'settings.pref.confirmLang': '确认卡语言',
294
297
  'settings.pref.confirmLangHint': '对话中写操作确认卡与问题的显示语言。确认卡无法自动跟随界面语言,需在此选择;默认中文。',
@@ -684,27 +687,28 @@ const EN_DICT = {
684
687
  'settings.coach.gentle': 'Gentle',
685
688
  'settings.coach.strict': 'Strict',
686
689
  'settings.coach.humorous': 'Humorous',
687
- 'settings.coach.current': 'Current: {label}. Shapes the coaching tone; you can also say “be tougher on me” in chat.',
690
+ 'settings.coach.hint': 'Shapes the coaching tone; you can also say “be tougher on me” in chat.',
688
691
  'settings.coach.saved': 'Coach style updated; applies to future chats',
689
692
  'settings.coach.loadFailed': 'Failed to load: {error}',
690
693
  'settings.profile.title': 'Nickname & profile',
691
694
  'settings.profile.nickname': 'Nickname',
692
695
  'settings.profile.occupation': 'Occupation',
693
696
  'settings.profile.interests': 'Interests',
694
- 'settings.profile.nicknamePlaceholder': 'Nickname: how should I call you? (empty clears)',
695
- 'settings.profile.occupationPlaceholder': 'Occupation (empty clears)',
696
- 'settings.profile.interestsPlaceholder': 'Interests: separate with commas (e.g. reading, running)',
697
+ 'settings.profile.nicknamePlaceholder': 'How should I call you?',
698
+ 'settings.profile.occupationPlaceholder': 'e.g. teacher, product manager',
699
+ 'settings.profile.interestsPlaceholder': 'e.g. reading, running',
697
700
  'settings.profile.save': 'Save profile',
698
701
  'settings.profile.saving': 'Saving…',
699
702
  'settings.profile.saved': 'Profile saved',
700
- 'settings.profile.sharedHint': 'Shared with the chat side; saying “call me Star” in chat updates it too.',
703
+ 'settings.profile.sharedHint': 'Empty fields are cleared. The profile is shared with chat; saying “call me Star” updates it too.',
701
704
  'settings.pref.title': 'Chat preferences',
702
705
  'settings.pref.unavailable': 'This connection cannot persist preferences (remote or temporary mode); the options below are disabled.',
703
706
  'settings.pref.loading': 'Loading preferences…',
704
707
  'settings.pref.notRegistered': 'Preferences are unavailable right now (component not ready); please retry later.',
705
708
  'settings.pref.readOnly': 'Settings are read-only; changes cannot be saved.',
706
- 'settings.pref.confirmWrites': 'Confirm write actions',
707
- 'settings.pref.confirmWritesHint': 'Master switch: when on, confirmations follow the categories below; when off, everything except deletes (always confirmed) runs directly.',
709
+ 'settings.confirm.title': 'Confirmation before writes',
710
+ 'settings.confirm.master': 'Master switch',
711
+ 'settings.pref.confirmWritesHint': 'When on, confirmations follow the categories below; when off, everything except deletes (always confirmed) runs directly.',
708
712
  'settings.pref.confirmOps.group': 'Confirmation categories',
709
713
  'settings.pref.confirmOps.create': 'Create wishes/tasks (incl. micro-action plans)',
710
714
  'settings.pref.confirmOps.checkin': 'Check-ins',
@@ -713,11 +717,13 @@ const EN_DICT = {
713
717
  'settings.pref.confirmOps.update': 'Editing wishes/tasks/categories',
714
718
  'settings.pref.confirmOps.memorySave': 'Saving memories',
715
719
  'settings.pref.confirmOps.delete': 'Deletes (incl. batch & restarts)',
720
+ 'settings.pref.confirmOps.locked': 'Always on',
716
721
  'settings.pref.confirmOps.deleteHint': 'Always confirms and cannot be turned off; deletes are irreversible.',
717
722
  'settings.pref.confirmOps.hint': 'Category switches apply while the master switch is on; defaults match previous versions.',
718
723
  'settings.pref.memoryLimit': 'Memory injection limit',
719
724
  'settings.pref.memoryLimitHint': 'Max memories auto-injected per chat (5-200, default 40); saved when you leave the field.',
720
725
  'settings.pref.limitInvalid': 'Enter an integer between 5 and 200',
726
+ 'settings.pref.limitClamped': 'Out of the 5-200 range; saved as the nearest allowed value.',
721
727
  'settings.pref.writeFailed': 'The change did not save; please try again.',
722
728
  'settings.pref.confirmLang': 'Confirm card language',
723
729
  'settings.pref.confirmLangHint': 'Language of the in-chat confirmation card and its question. The card cannot follow the interface language automatically, so pick it here; defaults to Chinese.',
@@ -49,9 +49,10 @@ export function SettingsSection(props) {
49
49
  const [nicknameDraft, setNicknameDraft] = useState('');
50
50
  const [occupationDraft, setOccupationDraft] = useState('');
51
51
  const [interestsDraft, setInterestsDraft] = useState('');
52
- // 记忆注入上限:草稿态编辑(null=显示快照值),失焦才校验提交——避免逐键持久化与非法中间值死锁
52
+ // 记忆注入上限:草稿态编辑(null=显示快照值),失焦才校验提交——避免逐键持久化与非法中间值死锁。
53
+ // 错误态分两档:invalid=未保存的就地报错;clamped=越界已按边界值保存的说明性提示(不是错误)
53
54
  const [limitDraft, setLimitDraft] = useState(null);
54
- const [limitError, setLimitError] = useState(false);
55
+ const [limitError, setLimitError] = useState(undefined);
55
56
  // 记忆注入上限:写入在途的乐观值(与开关的 pendingToggle 同款,用于禁用控件并顶住回显,
56
57
  // 否则提交后到快照回折前会闪回旧值)
57
58
  const [pendingLimit, setPendingLimit] = useState(undefined);
@@ -237,7 +238,7 @@ export function SettingsSection(props) {
237
238
  const commitLimit = () => {
238
239
  const raw = limitDraft;
239
240
  setLimitDraft(null);
240
- setLimitError(false);
241
+ setLimitError(undefined);
241
242
  if (raw === null || !writable)
242
243
  return;
243
244
  // 空串视为放弃编辑:静默回显保存值。若在此报错,会同时出现「已填回旧值」与
@@ -247,11 +248,12 @@ export function SettingsSection(props) {
247
248
  const parsed = parseMemoryLimit(raw);
248
249
  // 非法输入就地报错(不再静默回弹):用户明确知道哪里错、该怎么改
249
250
  if (parsed === undefined) {
250
- setLimitError(true);
251
+ setLimitError('invalid');
251
252
  return;
252
253
  }
253
254
  if (parsed.clamped) {
254
- // 越界但可夹取:按夹取值提交并回显,同时给出行内提示
255
+ // 越界但可夹取:按夹取值提交并回显,给说明性提示而非报错——值已成功保存,
256
+ // 「请输入合法整数」的报错口径与实际行为矛盾
255
257
  const seq = ++writeSeqRef.current;
256
258
  setPendingLimit(parsed.value);
257
259
  void scope.set('memoryInjectLimit', parsed.value)
@@ -261,7 +263,7 @@ export function SettingsSection(props) {
261
263
  verifyWritten(seq, 'memoryInjectLimit', parsed.value);
262
264
  })
263
265
  .catch((err) => { setPendingLimit(undefined); toastError(err); });
264
- setLimitError(true);
266
+ setLimitError('clamped');
265
267
  return;
266
268
  }
267
269
  if (parsed.value === snap.value?.memoryInjectLimit)
@@ -275,10 +277,6 @@ export function SettingsSection(props) {
275
277
  })
276
278
  .catch((err) => { setPendingLimit(undefined); toastError(err); });
277
279
  };
278
- const coachLabel = (id) => {
279
- const index = COACH_IDS.indexOf(id);
280
- return index >= 0 ? t(COACH_KEYS[index]) : (id ?? '');
281
- };
282
280
  const saveCoach = (style) => {
283
281
  if (saving || style === profile?.coachStyle)
284
282
  return;
@@ -304,16 +302,19 @@ export function SettingsSection(props) {
304
302
  flashSaved(t('settings.profile.saved'));
305
303
  }));
306
304
  };
307
- // 三个分节各自成面板卡(与全站卡片语言一致),字段不再裸堆叠;卡内节奏由样式层统一
305
+ // 分节各自成面板卡(与全站卡片语言一致);开关类设置用 row 语法(标签+说明在左、
306
+ // 控件在右),表单类字段保持 label 在上的纵排——两类各有惯例,不混用
308
307
  return createElement('div', { className: 'xy-settings' }, createElement('section', { className: 'xy-panel' }, createElement('h3', { className: 'xy-panel-head' }, t('settings.coach.title')), createElement('div', { className: 'xy-seg', role: 'group', 'aria-label': t('settings.coach.title') }, ...COACH_IDS.map((id, i) => createElement('button', {
309
308
  key: id,
310
309
  className: `xy-seg-btn${profile?.coachStyle === id ? ' xy-on' : ''}`,
311
310
  'aria-pressed': profile?.coachStyle === id,
312
311
  disabled: saving,
313
312
  onClick: () => saveCoach(id),
314
- }, t(COACH_KEYS[i])))), createElement('p', { className: 'xy-hint' }, profileError !== undefined ? t('settings.coach.loadFailed', { error: profileError })
313
+ }, t(COACH_KEYS[i])))),
314
+ // 分段按钮已表达当前风格,说明行只讲作用不报状态;错误态保留重试入口
315
+ createElement('p', { className: 'xy-hint' }, profileError !== undefined ? t('settings.coach.loadFailed', { error: profileError })
315
316
  : profile === undefined ? t('common.loading') + '…'
316
- : t('settings.coach.current', { label: coachLabel(profile.coachStyle) })), profileError !== undefined
317
+ : t('settings.coach.hint')), profileError !== undefined
317
318
  ? createElement('button', { className: 'xy-btn', onClick: loadProfile }, t('common.retry'))
318
319
  : null),
319
320
  // 画像字段:label 包裹关联控件(可点击标签聚焦输入),小标题在上、示例 placeholder 在下,
@@ -334,11 +335,14 @@ export function SettingsSection(props) {
334
335
  className: 'xy-btn xy-btn-primary', disabled: saving, onClick: saveProfile,
335
336
  }, saving ? t('settings.profile.saving') : t('settings.profile.save')), savedMsg !== '' ? createElement('span', { className: 'xy-saved', role: 'status' },
336
337
  // 勾形装饰对读屏隐藏(语义由文案承担),与卡片完成态同一语法
337
- createElement('span', { 'aria-hidden': 'true' }, '✓ '), savedMsg) : null), createElement('p', { className: 'xy-hint' }, t('settings.profile.sharedHint'))), createElement('section', { className: 'xy-panel' }, createElement('h3', { className: 'xy-panel-head' }, t('settings.pref.title')),
338
- // 整页 section 没有官方卡片那套「按命名空间自动显隐」的保护,失败呈现归注册方
338
+ createElement('span', { 'aria-hidden': 'true' }, '✓ '), savedMsg) : null), createElement('p', { className: 'xy-hint' }, t('settings.profile.sharedHint'))),
339
+ // 整页 section 没有官方卡片那套「按命名空间自动显隐」的保护,失败呈现归注册方。
340
+ // 命名空间级通知作用于下方两卡(写操作确认/对话偏好),页级一次说清不逐卡重复
339
341
  prefNoticeKey !== undefined
340
342
  ? createElement('p', { className: 'xy-hint' }, t(prefNoticeKey))
341
- : null, createElement('label', { className: 'xy-field' }, createElement('span', { className: 'xy-field-head' }, createElement('input', {
343
+ : null,
344
+ // 写操作确认独立成卡:安全类设置自成一组,与一般行为参数分离
345
+ createElement('section', { className: 'xy-panel' }, createElement('h3', { className: 'xy-panel-head' }, t('settings.confirm.title')), createElement('label', { className: 'xy-setrow' }, createElement('span', { className: 'xy-setrow-main' }, createElement('span', { className: 'xy-setrow-label' }, t('settings.confirm.master')), createElement('span', { className: 'xy-setrow-desc' }, t('settings.pref.confirmWritesHint'))), createElement('input', {
342
346
  type: 'checkbox',
343
347
  className: 'xy-toggle',
344
348
  name: 'confirmWrites',
@@ -358,60 +362,57 @@ export function SettingsSection(props) {
358
362
  toastError(err);
359
363
  });
360
364
  },
361
- }), t('settings.pref.confirmWrites')), createElement('p', { className: 'xy-hint' }, t('settings.pref.confirmWritesHint'))),
365
+ })),
362
366
  // 确认类目明细(锁定删除行恒展示):总开关关闭时整组禁用置灰——不弹卡由总开关
363
367
  // 决定;禁用原因由组尾 hint 承担(aria-describedby 关联),不在每行重复
364
368
  createElement('div', {
365
- className: 'xy-confirm-ops',
369
+ className: `xy-confirm-ops${confirmWrites ? '' : ' xy-confirm-ops-off'}`,
366
370
  role: 'group',
367
371
  'aria-labelledby': 'xy-confirm-ops-head',
368
372
  'aria-describedby': 'xy-confirm-ops-hint',
369
- }, createElement('span', { className: 'xy-op-group-head', id: 'xy-confirm-ops-head' }, t('settings.pref.confirmOps.group')), ...CONFIRM_OPS.map((op) => createElement('label', { className: 'xy-op-row', key: op }, createElement('input', {
373
+ }, createElement('span', { className: 'xy-op-group-head', id: 'xy-confirm-ops-head' }, t('settings.pref.confirmOps.group')), createElement('div', { className: 'xy-setrows' }, ...CONFIRM_OPS.map((op) => createElement('label', { className: 'xy-setrow', key: op }, createElement('span', { className: 'xy-setrow-main' }, createElement('span', { className: 'xy-setrow-label' }, t(CONFIRM_OP_LABEL_KEYS[op]))), createElement('input', {
370
374
  type: 'checkbox',
371
375
  className: 'xy-toggle',
372
376
  name: `confirmOps.${op}`,
373
377
  checked: confirmOps[op],
374
378
  disabled: !writable || pendingOps !== undefined || !confirmWrites,
375
379
  onChange: (e) => toggleConfirmOp(op, e.target.checked),
376
- }), t(CONFIRM_OP_LABEL_KEYS[op]))),
377
- // 锁定类目:div + 只读开关(不可交互故不用 label),说明单独一行与文字对齐
378
- createElement('div', { className: 'xy-op-row xy-op-row-locked' }, createElement('input', {
379
- type: 'checkbox',
380
- className: 'xy-toggle',
381
- name: 'confirmOps.deleteLocked',
382
- checked: true,
383
- readOnly: true,
384
- disabled: true,
385
- 'aria-label': t('settings.pref.confirmOps.delete'),
386
- }), createElement('span', null, t('settings.pref.confirmOps.delete'))), createElement('span', { className: 'xy-hint xy-op-locked-hint' }, t('settings.pref.confirmOps.deleteHint')), createElement('span', { className: 'xy-hint', id: 'xy-confirm-ops-hint' }, t('settings.pref.confirmOps.hint'))), createElement('label', { className: 'xy-field' }, createElement('span', { className: 'xy-field-head' }, t('settings.pref.memoryLimit')), createElement('input', {
380
+ }))),
381
+ // 锁定类目:div + 「始终开启」徽章(不可交互,不用禁用开关表达恒开语义)
382
+ createElement('div', { className: 'xy-setrow' }, createElement('span', { className: 'xy-setrow-main' }, createElement('span', { className: 'xy-setrow-label' }, t('settings.pref.confirmOps.delete')), createElement('span', { className: 'xy-setrow-desc' }, t('settings.pref.confirmOps.deleteHint'))), createElement('span', { className: 'xy-locked' }, t('settings.pref.confirmOps.locked'))))), createElement('span', { className: 'xy-hint', id: 'xy-confirm-ops-hint' }, t('settings.pref.confirmOps.hint'))),
383
+ // 一般对话行为参数:记忆注入上限 + 确认卡语言
384
+ createElement('section', { className: 'xy-panel' }, createElement('h3', { className: 'xy-panel-head' }, t('settings.pref.title')), createElement('div', { className: 'xy-setrows' }, createElement('label', { className: 'xy-setrow' }, createElement('span', { className: 'xy-setrow-main' }, createElement('span', { className: 'xy-setrow-label' }, t('settings.pref.memoryLimit')), createElement('span', { className: 'xy-setrow-desc' }, t('settings.pref.memoryLimitHint')), limitError === 'invalid'
385
+ ? createElement('span', { id: 'xy-limit-error', className: 'xy-field-err', role: 'alert' }, t('settings.pref.limitInvalid'))
386
+ : limitError === 'clamped'
387
+ ? createElement('span', { id: 'xy-limit-error', className: 'xy-hint', role: 'status' }, t('settings.pref.limitClamped'))
388
+ : null), createElement('input', {
387
389
  type: 'number', min: MEMORY_LIMIT_MIN, max: MEMORY_LIMIT_MAX,
388
390
  className: 'xy-input xy-input-num', name: 'memoryInjectLimit', inputMode: 'numeric',
391
+ autoComplete: 'off',
389
392
  'aria-label': t('settings.pref.memoryLimit'),
390
- 'aria-invalid': limitError || undefined,
393
+ 'aria-invalid': limitError === 'invalid' || undefined,
391
394
  ...(limitError ? { 'aria-describedby': 'xy-limit-error' } : {}),
392
395
  // 写入在途时顶住乐观值并禁用(与开关同款),避免闪回旧值与并发写交错
393
396
  value: limitDraft ?? (pendingLimit !== undefined ? String(pendingLimit) : limit),
394
397
  disabled: !writable || pendingLimit !== undefined,
395
- onChange: (e) => { setLimitDraft(e.target.value); setLimitError(false); },
398
+ onChange: (e) => { setLimitDraft(e.target.value); setLimitError(undefined); },
396
399
  onBlur: commitLimit,
397
400
  onKeyDown: (e) => {
398
401
  if (e.key === 'Enter')
399
402
  e.currentTarget.blur();
400
403
  },
401
- }), createElement('span', { className: 'xy-hint' }, t('settings.pref.memoryLimitHint')), limitError
402
- ? createElement('span', { id: 'xy-limit-error', className: 'xy-field-err', role: 'alert' }, t('settings.pref.limitInvalid'))
403
- : null),
404
+ })),
404
405
  // 确认卡语言:平台不向 host 侧暴露界面语言(rc.2 实测),对话侧确认卡文案无法
405
- // 自动跟随界面语言——由此处显式选择,即时热生效(hitl/tools 每次执行读 thunk)
406
- createElement('label', { className: 'xy-field' }, createElement('span', { className: 'xy-field-head' }, t('settings.pref.confirmLang')), createElement('select', {
407
- className: 'xy-input', value: confirmLang, name: 'confirmLang',
408
- 'aria-label': t('settings.pref.confirmLang'),
406
+ // 自动跟随界面语言——由此处显式选择,即时热生效(hitl/tools 每次执行读 thunk)。
407
+ // 两档枚举与教练风格共用分段控件(下拉对小枚举过重);行内是按钮组故不用 label
408
+ createElement('div', { className: 'xy-setrow' }, createElement('span', { className: 'xy-setrow-main' }, createElement('span', { className: 'xy-setrow-label' }, t('settings.pref.confirmLang')), createElement('span', { className: 'xy-setrow-desc' }, t('settings.pref.confirmLangHint'))), createElement('span', { className: 'xy-seg', role: 'group', 'aria-label': t('settings.pref.confirmLang') }, ...['zh', 'en'].map((lang) => createElement('button', {
409
+ key: lang,
410
+ type: 'button',
411
+ className: `xy-seg-btn${confirmLang === lang ? ' xy-on' : ''}`,
412
+ 'aria-pressed': confirmLang === lang,
409
413
  disabled: !writable || pendingLang !== undefined,
410
- onChange: (e) => {
411
- if (e.target.value === 'zh' || e.target.value === 'en')
412
- switchConfirmLang(e.target.value);
413
- },
414
- }, createElement('option', { value: 'zh' }, t('settings.pref.confirmLang.zh')), createElement('option', { value: 'en' }, t('settings.pref.confirmLang.en'))), createElement('span', { className: 'xy-hint' }, t('settings.pref.confirmLangHint')))),
414
+ onClick: () => switchConfirmLang(lang),
415
+ }, t(lang === 'zh' ? 'settings.pref.confirmLang.zh' : 'settings.pref.confirmLang.en'))))))),
415
416
  // 标签页显示:模式三态(跟随会话/始终显示/始终隐藏)+ 六个标签勾选 chips。
416
417
  // 与教练风格卡同一 xy-seg 视觉语法;「始终隐藏」时勾选区整组禁用置灰。
417
418
  // 命名空间常驻于 bundle 层(未选星愿预设也可调),见 src/ui-settings.ts 头注。
@@ -429,12 +430,14 @@ export function SettingsSection(props) {
429
430
  'aria-label': t('settings.tabs.chooseTab'),
430
431
  }, ...TAB_IDS.map((id) => {
431
432
  const shown = !hiddenTabs.includes(id);
433
+ // 多选勾选态加勾形装饰(对读屏隐藏,状态由 aria-pressed 承担):
434
+ // 与上方单选的模式分段在视觉上区分「可多选」
432
435
  return createElement('button', {
433
436
  key: id,
434
437
  className: `xy-seg-btn${shown ? ' xy-on' : ''}`,
435
438
  'aria-pressed': shown,
436
439
  disabled: !uiWritable || pendingHidden !== undefined || tabMode === 'hide',
437
440
  onClick: () => toggleTab(id, !shown),
438
- }, t(TAB_LABEL_KEYS[id]));
441
+ }, shown ? createElement('span', { 'aria-hidden': 'true' }, '✓ ') : null, t(TAB_LABEL_KEYS[id]));
439
442
  })), createElement('p', { className: 'xy-hint' }, t('settings.tabs.hint'))), createElement('p', { className: 'xy-hint' }, t('settings.dataHint')));
440
443
  }
@@ -264,7 +264,7 @@ body[data-ds-dark-theme] .xy-badge-cat{background:hsl(var(--cat-h,275) calc(var(
264
264
  .xy-input:disabled{opacity:.55;cursor:not-allowed}
265
265
  .xy-field-err{color:var(--xyd-danger);font-size:12px;margin-top:4px}
266
266
  /* 数字微输入(设置页注入上限等):按内容长度定宽,不随浏览器默认 size 拉成宽条 */
267
- .xy-input-num{width:96px}
267
+ .xy-input-num{width:96px;font-variant-numeric:tabular-nums}
268
268
 
269
269
  /* 列表行 */
270
270
  .xy-rowmain{display:flex;flex-direction:column;gap:2px;min-width:0}
@@ -506,35 +506,54 @@ button.xy-growth-col.xy-hover{box-shadow:inset 0 0 0 2px var(--xyd-accent-ring)}
506
506
 
507
507
  /* ===== 设置页 ===== */
508
508
  .xy-settings{display:flex;flex-direction:column;gap:16px;max-width:560px;color:var(--dsw-alias-label-primary);font-size:14px}
509
- /* 设置三个分节各自成面板卡;间距交给 xy-settings 的 gap,卡内子元素统一 10px 纵向节奏 */
509
+ /* 游离于卡片外的说明行(页级命名空间通知/页尾数据说明)随卡内文字对齐
510
+ * (xy-panel 横向内边距 14px),不得贴容器左缘悬在列外 */
511
+ .xy-settings>.xy-hint{padding:0 14px}
512
+ /* 设置各分节自成面板卡;间距交给 xy-settings 的 gap,卡内子元素统一 10px 纵向节奏 */
510
513
  .xy-settings .xy-panel{margin-top:0}
511
514
  .xy-settings .xy-panel>*+*{margin-top:10px}
512
515
  .xy-field{display:flex;flex-direction:column;gap:6px}
513
- .xy-field-head{display:flex;align-items:center;gap:8px;font-weight:600}
516
+ /* 设置行(业界设置页 row 惯例):标签+说明在左、控件在右,扫描线对齐;说明文字
517
+ * flex:1 收缩换行、控件 flex:none 不被挤压,窄面板下控件换行左对齐。
518
+ * label 行整行可点(label 包裹控件);div 行(分段按钮组/锁定行)不可点。 */
519
+ .xy-setrow{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
520
+ label.xy-setrow{cursor:pointer}
521
+ .xy-setrow-main{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 220px}
522
+ .xy-setrow-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}
523
+ .xy-setrow-desc{color:var(--dsw-alias-label-secondary);font-size:12px;margin:0;overflow-wrap:anywhere}
524
+ /* 行列表:行间 hairline 分隔(与分组卡 xy-grouprow 同一语法),首行不画线 */
525
+ .xy-setrows{display:flex;flex-direction:column}
526
+ .xy-setrows>.xy-setrow{padding:8px 0}
527
+ .xy-setrows>.xy-setrow+.xy-setrow{border-top:1px solid var(--dsw-alias-border-l1)}
528
+ .xy-setrow>.xy-toggle,.xy-setrow>.xy-input-num,.xy-setrow>.xy-seg{flex:none}
514
529
  .xy-input-wide{width:100%;max-width:360px;box-sizing:border-box}
515
530
  .xy-seg{display:flex;gap:6px;flex-wrap:wrap}
516
531
  .xy-seg-btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:999px;padding:5px 14px;font-size:13px;cursor:pointer;transition:border-color .15s ease,background-color .15s ease,color .15s ease}
532
+ .xy-seg-btn:disabled{opacity:.55;cursor:not-allowed}
517
533
  @media (hover:hover) and (pointer:fine){
518
- .xy-seg-btn:hover{border-color:var(--xyd-accent)}
534
+ .xy-seg-btn:hover:not(:disabled){border-color:var(--xyd-accent)}
519
535
  }
520
536
  /* 选中态双选择器:类名与 aria-pressed 属性等价(组件侧用属性表达状态语义) */
521
537
  .xy-seg-btn.xy-on,.xy-seg-btn[aria-pressed='true']{background:var(--xyd-accent);border-color:transparent;color:var(--xyd-on-accent)}
522
538
  .xy-save-row{display:flex;align-items:center;gap:10px;margin-top:2px}
523
539
  .xy-toggle{width:38px;height:22px;appearance:none;border-radius:999px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);position:relative;cursor:pointer;transition:background .15s}
540
+ /* 禁用即置灰(与 xy-btn/xy-input 同语法):只读命名空间/写入在途的控件不得看起来仍可交互 */
541
+ .xy-toggle:disabled{opacity:.4;cursor:not-allowed}
524
542
  .xy-toggle:checked{background:var(--xyd-accent);border-color:transparent}
525
543
  .xy-toggle::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary);transition:transform .15s}
526
544
  .xy-toggle:checked::after{transform:translateX(16px);background:var(--xyd-on-accent)}
527
545
  .xy-hint{color:var(--dsw-alias-label-secondary);font-size:12px;margin:0}
528
- /* 确认类目明细:缩进嵌在总开关下(与「总开关 + 明细」分层同构),行 = 开关 + 名称;
529
- * 锁定行(删除)为只读展示,其说明行缩进与文字对齐(开关 38px + 间距 8px) */
530
- .xy-confirm-ops{display:flex;flex-direction:column;gap:8px;margin-left:22px}
531
- /* 禁用弱化必须显式写:自定义外观的 toggle 不吃原生禁用变灰,总开关关闭时明细行
532
- * 否则与可用态无法区分;锁定的删除行恒禁用,常态弱化即「冻结在开启态」的语义 */
533
- .xy-confirm-ops .xy-toggle:disabled{opacity:.4;cursor:default}
534
- .xy-op-group-head{font-weight:600;font-size:13px}
535
- .xy-op-row{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px}
536
- .xy-op-row-locked{color:var(--dsw-alias-label-secondary);cursor:default}
537
- .xy-op-locked-hint{padding-left:46px}
546
+ /* 确认类目明细:总开关行下平铺(row 语法 + 组头),不再缩进嵌套;
547
+ * 总开关关闭时整组禁用弱化必须显式写(自定义 toggle 不吃原生禁用变灰),
548
+ * 行文字同步降权;禁用原因由组尾 hint 承担(aria-describedby 关联) */
549
+ .xy-confirm-ops{display:flex;flex-direction:column;gap:4px}
550
+ /* 关闭态只降权可切换的类目行(label 行):锁定删除行不受总开关支配(始终确认),
551
+ * 置灰会让「始终开启」徽章与灰字自相矛盾 */
552
+ .xy-confirm-ops-off label.xy-setrow .xy-setrow-label{color:var(--dsw-alias-label-secondary)}
553
+ .xy-op-group-head{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-secondary)}
554
+ /* 锁定类目(删除恒确认):「始终开启」徽章表达冻结语义——禁用开关看着像坏了,
555
+ * 徽章即状态;胶囊 999 符合形状纪律 */
556
+ .xy-locked{flex:none;font-size:12px;line-height:1.4;padding:3px 9px;border-radius:999px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--xyd-label-on-2)}
538
557
 
539
558
  /* ===== 详情聚合视图 ===== */
540
559
  /* 段间 10px 呼吸;每段内部「标签 → 内容」固定 5px 纵向节奏(label 不再与内容文字挤行)。