dsh-loadout 0.12.3 → 0.12.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
@@ -2,9 +2,17 @@
2
2
 
3
3
  「装备栏」是一个 DeepSeek Harness 插件:给每个对话选一套工具(套装),只把用得上的工具投给模型。
4
4
 
5
+ ## 0.12.4(本地测试版,未发布)
6
+
7
+ - **重名当场说清楚**:新建/编辑套装时,输入框下方直接给出原因——「「仅聊天」是预置套装(标识 chat),不能用这个名字」,或是「已经有叫「我的套」的套装了」。占用时保存按钮置灰,不用等提交后才失败。
8
+ - 说明重名判定:名字先 trim + NFKC + 转小写,然后同时比对**自建套装的 name 与 title**、**预置套装的 name(稳定标识)与 title(界面显示)**、以及保留字(全部 / 仅内置 / all / default…)。所以预置显示「仅聊天」时,chat 依然被占用——chat 是它的标识。
9
+ - 宿主错误文案同步:「仅聊天」是预置套装(标识 chat),不能占用这个名字。
10
+ - **「+ 新建套装」改成动作色**:蓝色文字 + 浅蓝底 + 悬停加深,右侧补一句「从零配一套」,跟下面那些「点一下就切换」的套装行区分开。
11
+ - **「我的套装」折叠时也留着「+ 新建套装」**:网格收起只显示当前在用的那一套,创建入口始终在最后一行,不用先点开分组。
12
+
5
13
  ## 0.12.3
6
14
 
7
- - **标题栏按钮不再「点一下才对」**:芯片只在菜单被打开时才去读状态,所以进对话先看到「装备栏」,点开一次才换成套装名。现在挂载就读一次,并监听 dsh-loadout:changed 与窗口 ocus,别处切了套装也会立刻跟上。
15
+ - **标题栏按钮不再「点一下才对」**:芯片只在菜单被打开时才去读状态,所以进对话先看到「装备栏」,点开一次才换成套装名。现在挂载就读一次,并监听 dsh-loadout:changed 与窗口 focus,别处切了套装也会立刻跟上。
8
16
  - **设置页「预设套装」默认折叠**:与「我的套装」一致——折叠时只显示当前被设为默认的那一套,展开才列出五套。
9
17
  - **同名撞车**:0.12 之前手建的套装如果叫 chat / plan 这类预置名,
10
18
  - 回归断言: user kit shadowing a preset is renamed、 he renamed kit keeps its default and its bindings、presets start collapsed、 collapsed preset group keeps the default visible。
package/client.js CHANGED
@@ -46,6 +46,28 @@ window.__ModuleLoader__.load({
46
46
  return kit.note ? `${kit.title}(${kit.note})` : kit.title
47
47
  }
48
48
 
49
+ /**
50
+ * Why a name cannot be saved, in the user's terms. The host compares a kit
51
+ * against preset *ids* as well as titles (a preset's `name` is a stable id —
52
+ * `chat` — while the UI shows its title, 仅聊天), so "chat" is refused even
53
+ * though it "looks different". Return '' when the name is free.
54
+ */
55
+ function nameClash(raw, presets, kits, ignore) {
56
+ const value = String(raw ?? '').trim().toLowerCase()
57
+ if (value === '') return ''
58
+ const preset = (presets ?? []).find((item) =>
59
+ String(item.name ?? '').toLowerCase() === value || String(item.title ?? '').toLowerCase() === value)
60
+ if (preset !== undefined) {
61
+ return `「${preset.title}」是预置套装(标识 ${preset.name}),不能用这个名字,换一个吧`
62
+ }
63
+ const kit = (kits ?? []).find((item) =>
64
+ String(item.name ?? '').toLowerCase() === value || String(item.title ?? '').toLowerCase() === value)
65
+ if (kit !== undefined && kit.name !== ignore) {
66
+ return `已经有叫「${kit.title ?? kit.name}」的套装了,换一个名字,或者进去编辑它`
67
+ }
68
+ return ''
69
+ }
70
+
49
71
  /**
50
72
  * Presets (仅聊天 / 编程用 / 查找资料 / 项目梳理 / 写作) are shown again from
51
73
  * 0.12: they are the fastest way to start a conversation of a known kind,
@@ -133,6 +155,10 @@ window.__ModuleLoader__.load({
133
155
  },
134
156
  rowHover: { background: 'rgba(59,130,246,.08)' },
135
157
  rowActive: { background: 'rgba(59,130,246,.10)' },
158
+ // The one row that creates something, not selects something: keep it
159
+ // blue and tinted so it reads as an action at a glance.
160
+ rowPrimary: { color: '#2563eb', background: 'rgba(37,99,235,.07)', fontWeight: 600 },
161
+ rowPrimaryHover: { background: 'rgba(37,99,235,.14)' },
136
162
  rowDisabled: { opacity: 0.45, cursor: 'default' },
137
163
  indent: { paddingLeft: 26 },
138
164
  mark: { width: 12, flex: 'none', color: '#2563eb', fontSize: 12, textAlign: 'center' },
@@ -273,7 +299,8 @@ window.__ModuleLoader__.load({
273
299
  ...styles.row,
274
300
  ...(props.indent === true ? styles.indent : null),
275
301
  ...(props.active === true ? styles.rowActive : null),
276
- ...(hover && !disabled ? styles.rowHover : null),
302
+ ...(props.tone === 'primary' ? styles.rowPrimary : null),
303
+ ...(hover && !disabled ? (props.tone === 'primary' ? styles.rowPrimaryHover : styles.rowHover) : null),
277
304
  ...(disabled ? styles.rowDisabled : null),
278
305
  }
279
306
  const activate = () => {
@@ -295,7 +322,7 @@ window.__ModuleLoader__.load({
295
322
  },
296
323
  },
297
324
  h('span', { style: styles.mark }, props.active === true ? '✓' : ''),
298
- h('span', { style: styles.rowTitle },
325
+ h('span', { style: { ...styles.rowTitle, ...(props.tone === 'primary' ? { color: '#2563eb' } : null) } },
299
326
  props.tip === undefined
300
327
  ? h('span', null, props.label)
301
328
  : h(Tip, { text: props.tip }, h('span', null, props.label))),
@@ -337,7 +364,7 @@ window.__ModuleLoader__.load({
337
364
  function Builder(props) {
338
365
  const {
339
366
  catalog, draft, onChange, busy, onCancel, onSave, saveLabel,
340
- title, onSaveAs, saveAsLabel, nameField, namePlaceholder,
367
+ title, onSaveAs, saveAsLabel, nameField, namePlaceholder, clash,
341
368
  } = props
342
369
  // Both lists start collapsed: the builder opens with a name field and a
343
370
  // summary, not with forty checkboxes.
@@ -368,6 +395,12 @@ window.__ModuleLoader__.load({
368
395
  value: draft[nameField ?? 'name'] ?? '',
369
396
  onChange: (event) => onChange({ ...draft, [nameField ?? 'name']: event.target.value }),
370
397
  }),
398
+ // The host rejects a name that a preset or another kit already uses;
399
+ // saying so *here* keeps the user from staring at a save button that
400
+ // silently does nothing.
401
+ typeof clash === 'string' && clash !== ''
402
+ ? h('div', { style: { ...styles.error, marginTop: 4 } }, clash)
403
+ : null,
371
404
  h('input', {
372
405
  style: { ...styles.input, marginTop: 6 },
373
406
  placeholder: `备注(可选,最多 ${NOTE_MAX} 字),例如 用于视频制作`,
@@ -413,7 +446,12 @@ window.__ModuleLoader__.load({
413
446
  onSaveAs === undefined
414
447
  ? null
415
448
  : h('button', { type: 'button', style: styles.ghost, disabled: busy, onClick: onSaveAs }, saveAsLabel ?? '另存为新套装'),
416
- h('button', { type: 'button', style: styles.primary, disabled: busy, onClick: onSave }, saveLabel ?? '保存')))
449
+ h('button', {
450
+ type: 'button',
451
+ style: { ...styles.primary, ...(typeof clash === 'string' && clash !== '' ? { opacity: 0.5 } : null) },
452
+ disabled: busy || (typeof clash === 'string' && clash !== ''),
453
+ onClick: onSave,
454
+ }, saveLabel ?? '保存')))
417
455
  }
418
456
 
419
457
  // ---- the header control -------------------------------------------------
@@ -429,7 +467,7 @@ window.__ModuleLoader__.load({
429
467
  // collapsed group still shows the one active row, and clicking the head is
430
468
  // how you go browsing.
431
469
  const [groups, setGroups] = useState(preview?.groups ?? { kits: false, presets: false, extras: false, applied: false })
432
- const [builder, setBuilder] = useState(null)
470
+ const [builder, setBuilder] = useState(preview?.builder ?? null)
433
471
  const [saveAs, setSaveAs] = useState(false)
434
472
  const [saveName, setSaveName] = useState('')
435
473
  const [saveNote, setSaveNote] = useState('')
@@ -696,9 +734,10 @@ window.__ModuleLoader__.load({
696
734
  tip: '你自己保存的套装。点一下即可切换。',
697
735
  onToggle: () => setGroups({ ...groups, kits: !groups.kits }),
698
736
  }),
699
- // A collapsed group shows exactly one row: the set that is on. Everything
700
- // else waits for a click on the head.
737
+ // A collapsed group shows the set that is on, plus the one action that
738
+ // creates a set — so "new kit" never needs a click on the head first.
701
739
  activeUserKit === undefined ? null : h(MenuRow, {
740
+ key: 'active-kit',
702
741
  label: kitLabel(activeUserKit),
703
742
  hint: activeUserKit.isDefault ? '默认' : '',
704
743
  active: true,
@@ -706,27 +745,31 @@ window.__ModuleLoader__.load({
706
745
  onClick: () => run({ action: 'applyBase', base: 'kit', kit: activeUserKit.name }),
707
746
  }),
708
747
  groups.kits === true
709
- ? h('div', null,
710
- ...kits
711
- .filter((kit) => activeUserKit === undefined || kit.name !== activeUserKit.name)
712
- .map((kit) => h(MenuRow, {
713
- key: kit.name,
714
- label: kitLabel(kit),
715
- hint: kit.isDefault ? '默认' : '',
716
- indent: true,
717
- onClick: () => {
718
- run({ action: 'applyBase', base: 'kit', kit: kit.name })
719
- // Picking one puts the list away again; the row that stays is the
720
- // one just chosen, right under the group head.
721
- setGroups({ ...groups, kits: false })
722
- },
723
- })),
724
- h(MenuRow, {
725
- label: '+ 新建套装',
748
+ ? kits
749
+ .filter((kit) => activeUserKit === undefined || kit.name !== activeUserKit.name)
750
+ .map((kit) => h(MenuRow, {
751
+ key: kit.name,
752
+ label: kitLabel(kit),
753
+ hint: kit.isDefault ? '默认' : '',
726
754
  indent: true,
727
- onClick: () => openBuilder({ builtins: allBuiltins, plugins: [] }),
755
+ onClick: () => {
756
+ run({ action: 'applyBase', base: 'kit', kit: kit.name })
757
+ // Picking one puts the list away again; the row that stays is the
758
+ // one just chosen, right under the group head.
759
+ setGroups({ ...groups, kits: false })
760
+ },
728
761
  }))
729
762
  : null,
763
+ // Always last inside 我的套装, open or collapsed: the create action sits
764
+ // where the eye ends up after scanning the kits.
765
+ h(MenuRow, {
766
+ key: 'new-kit',
767
+ label: '+ 新建套装',
768
+ tone: 'primary',
769
+ hint: '从零配一套',
770
+ indent: true,
771
+ onClick: () => openBuilder({ builtins: allBuiltins, plugins: [] }),
772
+ }),
730
773
  SHOW_PRESETS === true
731
774
  ? h(GroupHead, {
732
775
  title: '预设套装',
@@ -850,6 +893,7 @@ window.__ModuleLoader__.load({
850
893
  onCancel: () => setBuilder(null),
851
894
  onSave: saveBuilder,
852
895
  saveLabel: '保存并启用',
896
+ clash: nameClash(builder.name, presets, kits, null),
853
897
  })
854
898
 
855
899
  return h('div', { style: styles.wrap, ref: box },
@@ -954,7 +998,7 @@ window.__ModuleLoader__.load({
954
998
  const [data, setData] = useState(preview?.data ?? null)
955
999
  const [busy, setBusy] = useState(false)
956
1000
  const [message, setMessage] = useState('')
957
- const [draft, setDraft] = useState(null)
1001
+ const [draft, setDraft] = useState(preview?.draft ?? null)
958
1002
  // Editing a saved kit reuses the same builder as creating one; only the
959
1003
  // action buttons differ (覆盖保存 keeps the name, 另存为新套装 mints a new one).
960
1004
  const [editingKit, setEditingKit] = useState(null)
@@ -1146,6 +1190,7 @@ window.__ModuleLoader__.load({
1146
1190
  onCancel: () => setDraft(null),
1147
1191
  onSave: saveDraft,
1148
1192
  saveLabel: '保存',
1193
+ clash: nameClash(draft.name, presets, kits, null),
1149
1194
  })),
1150
1195
  message === '' ? null : h('div', { style: styles.error }, message),
1151
1196
  h('div', { style: styles.ledger },
@@ -1296,6 +1341,7 @@ window.__ModuleLoader__.load({
1296
1341
  },
1297
1342
  onSave: saveOverwrite,
1298
1343
  saveLabel: '覆盖保存',
1344
+ clash: nameClash(draft.title, presets, kits, editingKit),
1299
1345
  onSaveAs: saveEditingAsNew,
1300
1346
  saveAsLabel: '另存为新套装',
1301
1347
  }))
package/index.js CHANGED
@@ -98,7 +98,8 @@ function assertKitName(raw, presets) {
98
98
  if (NAME_FORBIDDEN.test(value)) throw new Error('名字里不能有 / \\ : * ? " < > | 或控制字符')
99
99
  const lower = value.toLowerCase()
100
100
  if (RESERVED_NAMES.includes(lower)) throw new Error(`"${value}" 是保留名字,请换一个名字`)
101
- if (presets !== undefined && findKit(presets, value) !== undefined) throw new Error(`"${value}" 与预置套装同名,请换一个名字`)
101
+ const preset = presets === undefined ? undefined : findKit(presets, value)
102
+ if (preset !== undefined) throw new Error(`「${preset.title}」是预置套装(标识 ${preset.name}),不能占用这个名字`)
102
103
  return value
103
104
  }
104
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-loadout",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "description": "Loadout for DeepSeek Harness: group tools into kits, keep only the current kit's tools visible in each conversation, and manage everything from a settings panel.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -233,6 +233,8 @@ if (menuDefault !== undefined) {
233
233
  check('presets start collapsed', !flat.includes('写作'), flat.slice(0, 400))
234
234
  check('user kits start collapsed', !flat.includes('另一套'), flat.slice(0, 400))
235
235
  check('the active kit is the only kit row while collapsed', flat.includes('我的套(用于视频制作)'), flat.slice(0, 400))
236
+ // Creating a kit must not need a click on the group head first.
237
+ check('the new-kit row is visible while my kits are collapsed', flat.includes('+ 新建套装'), flat.slice(0, 400))
236
238
  check('applied plugins start collapsed', !flat.includes('alpha 插件'), flat.slice(0, 400))
237
239
  check('applied plugins collapse into one summary row', flat.includes('已应用 1 个插件'), flat.slice(0, 400))
238
240
  check('menu opens with a status line', flat.includes('当前:我的套') && flat.includes('加挂 1'), flat.slice(0, 200))
@@ -359,9 +361,28 @@ try {
359
361
  const presetExpandedText = textOf(presetExpanded)
360
362
  check('only the default preset carries the default tag',
361
363
  presetExpandedText.includes('仅聊天') && presetExpandedText.includes('默认'), presetExpandedText.slice(0, 400))
364
+ // 「+ 新建套装」 is the only row that creates something; it must read as an
365
+ // action (blue, tinted), not as another selectable kit.
366
+ const menuWithKits = expand(header.component({ sessionId: 'session-1', preview: { data: fixture, open: true, groups: { kits: true, presets: false, extras: false } } }))
367
+ const newKit = nodesIn(menuWithKits, (node) => node.props?.role === 'menuitem' && node.props?.style?.color === '#2563eb' && textOf(node).includes('+ 新建套装'))
368
+ check('the new-kit row is highlighted as an action', newKit.length === 1, String(newKit.length))
362
369
  const renameButtons = nodesIn(panel, (node) => node.type === 'button' && textOf(node) === '重命名')
363
370
  // Renaming moved into 编辑: one editor for name, note and tools on the card.
364
371
  check('renaming lives inside 编辑 rather than its own button', renameButtons.length === 0 && editButtons.length === 2, `${renameButtons.length} · 重命名 / ${editButtons.length} · 编辑`)
372
+
373
+ // A name the host would refuse is explained where the user typed it: a preset's
374
+ // `name` is a private id (`chat`), so "chat" looks free and is not.
375
+ const clashPanel = expand(registration.component({
376
+ preview: { data: fixture, draft: { name: 'chat', note: '', builtins: ['read'], plugins: [] } },
377
+ }))
378
+ const clashText = textOf(clashPanel)
379
+ check('a name taken by a preset is explained inline',
380
+ clashText.includes('是预置套装') && clashText.includes('仅聊天'), clashText.slice(0, 600))
381
+ const clashKitPanel = expand(registration.component({
382
+ preview: { data: fixture, draft: { name: '我的套', note: '', builtins: ['read'], plugins: [] } },
383
+ }))
384
+ check('a name taken by my own kit is explained inline',
385
+ textOf(clashKitPanel).includes('已经有叫「我的套」的套装了'), textOf(clashKitPanel).slice(0, 600))
365
386
  } catch (error) {
366
387
  failures.push(`panel render threw: ${error instanceof Error ? error.message : String(error)}`)
367
388
  }