dsh-loadout 0.12.2 → 0.12.3

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,6 +2,12 @@
2
2
 
3
3
  「装备栏」是一个 DeepSeek Harness 插件:给每个对话选一套工具(套装),只把用得上的工具投给模型。
4
4
 
5
+ ## 0.12.3
6
+
7
+ - **标题栏按钮不再「点一下才对」**:芯片只在菜单被打开时才去读状态,所以进对话先看到「装备栏」,点开一次才换成套装名。现在挂载就读一次,并监听 dsh-loadout:changed 与窗口 ocus,别处切了套装也会立刻跟上。
8
+ - **设置页「预设套装」默认折叠**:与「我的套装」一致——折叠时只显示当前被设为默认的那一套,展开才列出五套。
9
+ - **同名撞车**:0.12 之前手建的套装如果叫 chat / plan 这类预置名,
10
+ - 回归断言: 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。
5
11
  ## 0.12.2
6
12
 
7
13
  - **修「绑定到已不存在的套装」**:如果某个对话绑的套装在应用关着的时候被删掉(旧版本、手改 loadout.json、或别的机器同步过来的文件),启动后它会被当成「空套装」套用——等于把这对话的工具连同内置工具一起裁光,只剩 kit_*。现在这种绑定在第一次被读到时回退到「全部」,并把 ssignments 里那个失效名字改成
package/client.js CHANGED
@@ -475,9 +475,24 @@ window.__ModuleLoader__.load({
475
475
  }
476
476
  }, [sessionId])
477
477
 
478
+ // The label is this conversation's persisted binding, so read it as soon as
479
+ // the chip mounts: waiting for the first click is what made a bound
480
+ // conversation show 装备栏 until the menu was opened once.
478
481
  useEffect(() => {
479
- if (open) refresh()
480
- }, [open, refresh])
482
+ refresh()
483
+ }, [refresh])
484
+
485
+ // Another surface (the settings page, the /kit command) may have changed
486
+ // this conversation's binding while the chip sat idle.
487
+ useEffect(() => {
488
+ const sync = () => { refresh() }
489
+ document.addEventListener('dsh-loadout:changed', sync)
490
+ window.addEventListener('focus', sync)
491
+ return () => {
492
+ document.removeEventListener('dsh-loadout:changed', sync)
493
+ window.removeEventListener('focus', sync)
494
+ }
495
+ }, [refresh])
481
496
 
482
497
  useEffect(() => {
483
498
  if (!open) return undefined
@@ -946,7 +961,7 @@ window.__ModuleLoader__.load({
946
961
  // 我的套装 starts collapsed, like the chip: the page's job is "which set does
947
962
  // a new conversation start with", and a list of five kits does not answer it.
948
963
  // The test seam can seed it open so the card assertions never depend on a click.
949
- const [openSections, setOpenSections] = useState(preview?.openSections ?? { presets: true, kits: false })
964
+ const [openSections, setOpenSections] = useState(preview?.openSections ?? { presets: false, kits: false })
950
965
 
951
966
  const refresh = useCallback(async () => {
952
967
  try {
@@ -1007,6 +1022,7 @@ window.__ModuleLoader__.load({
1007
1022
  // A collapsed 我的套装 still answers "which kit is the default", the same way
1008
1023
  // the chip does.
1009
1024
  const defaultKitRow = kits.find((kit) => kit.name === data.defaultKit)
1025
+ const defaultPresetRow = presets.find((preset) => preset.name === data.defaultKit)
1010
1026
 
1011
1027
  const saveDraft = async () => {
1012
1028
  if (draft === null) return
@@ -1195,6 +1211,21 @@ window.__ModuleLoader__.load({
1195
1211
  onDelete: () => {},
1196
1212
  }))))
1197
1213
  : null,
1214
+ SHOW_PRESETS === true && openSections.presets === false
1215
+ // Collapsed, exactly like 我的套装: only the preset that is the
1216
+ // default stays visible, so the group still answers "new conversations
1217
+ // start with this".
1218
+ ? (defaultPresetRow === undefined
1219
+ ? null
1220
+ : h(MenuRow, {
1221
+ key: `default-preset-${defaultPresetRow.name}`,
1222
+ label: kitLabel(defaultPresetRow),
1223
+ hint: '当前默认',
1224
+ indent: true,
1225
+ active: true,
1226
+ onClick: () => act({ action: 'default', name: defaultPresetRow.name }),
1227
+ }))
1228
+ : null,
1198
1229
  h(GroupHead, {
1199
1230
  title: '我的套装',
1200
1231
  count: kits.length,
package/index.js CHANGED
@@ -418,7 +418,46 @@ export function apply(ctx, config = {}) {
418
418
  }
419
419
  }
420
420
 
421
+ /**
422
+ * A kit saved before the preset list existed may carry a preset's name or
423
+ * title (`chat`, `plan`, `写作`…). `resolveKit` prefers user kits, so a
424
+ * default naming such a preset would actually answer the user's kit while the
425
+ * settings page marked the preset — two rows claiming 「默认」. Renaming is the
426
+ * only honest fix: keep the user's kit (with its tools) under a free name and
427
+ * repoint every binding that used the old one.
428
+ */
429
+ function disambiguatePresetClashes() {
430
+ const taken = new Set(presets.flatMap((preset) => [preset.name.toLowerCase(), preset.title.toLowerCase()]))
431
+ const changed = []
432
+ for (const kit of book.kits) {
433
+ const nameClash = taken.has(kit.name.toLowerCase())
434
+ const titleClash = taken.has(kit.title.toLowerCase())
435
+ if (!nameClash && !titleClash) continue
436
+ const before = kit.name
437
+ if (nameClash) {
438
+ let suffix = 2
439
+ let candidate = `${before}-${suffix}`
440
+ while (taken.has(candidate.toLowerCase()) || findKit(book.kits, candidate) !== undefined) {
441
+ suffix += 1
442
+ candidate = `${before}-${suffix}`
443
+ }
444
+ kit.name = candidate
445
+ taken.add(candidate.toLowerCase())
446
+ }
447
+ if (titleClash) kit.title = `${kit.title}(自建)`
448
+ for (const [sessionId, value] of Object.entries(book.assignments)) {
449
+ if (value === before) book.assignments[sessionId] = kit.name
450
+ }
451
+ if (book.defaultKit === before) book.defaultKit = kit.name
452
+ changed.push(`${before} → ${kit.name}`)
453
+ }
454
+ if (changed.length === 0) return
455
+ save()
456
+ ctx.logger?.warn(`dsh-loadout: 自建套装与预置同名,已改名(没有删除):${changed.join(',')}`)
457
+ }
458
+
421
459
  migrateBook()
460
+ disambiguatePresetClashes()
422
461
 
423
462
  /** agent -> { selection: {base,kit,extras}, label, dispose } */
424
463
  const state = new Map()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-loadout",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
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",
@@ -344,6 +344,21 @@ try {
344
344
  check('kit cards show their own estimate', panelText.includes('≈860 tokens') && panelText.includes('≈420 tokens'), panelText.slice(0, 400))
345
345
  const deleteButtons = nodesIn(panel, (node) => node.type === 'button' && textOf(node) === '删除')
346
346
  check('only my kits can be deleted', deleteButtons.length === 2, `${deleteButtons.length} · 删除@${panelText.indexOf('删除')} · ${panelText.slice(0, 200)}`)
347
+
348
+ // Presets start collapsed, and while collapsed they behave like 我的套装: the
349
+ // one that is the default stays visible so the page still answers "a new
350
+ // conversation starts with this".
351
+ const presetDefault = { ...fixture, defaultKit: 'chat', selection: { base: 'kit', kit: 'chat', extras: [] } }
352
+ const collapsed = expand(registration.component({ preview: { data: presetDefault } }))
353
+ const collapsedText = textOf(collapsed)
354
+ check('presets start collapsed',
355
+ collapsedText.includes('预设套装 · 只读(5)') && !collapsedText.includes('写作'), collapsedText.slice(0, 400))
356
+ check('a collapsed preset group keeps the default visible',
357
+ collapsedText.includes('仅聊天') && collapsedText.includes('当前默认'), collapsedText.slice(0, 400))
358
+ const presetExpanded = expand(registration.component({ preview: { data: presetDefault, openSections: { presets: true, kits: false } } }))
359
+ const presetExpandedText = textOf(presetExpanded)
360
+ check('only the default preset carries the default tag',
361
+ presetExpandedText.includes('仅聊天') && presetExpandedText.includes('默认'), presetExpandedText.slice(0, 400))
347
362
  const renameButtons = nodesIn(panel, (node) => node.type === 'button' && textOf(node) === '重命名')
348
363
  // Renaming moved into 编辑: one editor for name, note and tools on the card.
349
364
  check('renaming lives inside 编辑 rather than its own button', renameButtons.length === 0 && editButtons.length === 2, `${renameButtons.length} · 重命名 / ${editButtons.length} · 编辑`)
@@ -922,6 +922,38 @@ process.env.USERPROFILE = savedHomeV2
922
922
  process.env.HOME = savedHomeV2b
923
923
  rmSync(v2Home, { recursive: true, force: true })
924
924
 
925
+ // ---- a user kit that shadows a preset is renamed, never deleted ------------
926
+ // Stores written before the preset list existed can carry `chat` / `plan`, which
927
+ // `resolveKit` prefers over the preset of the same name — the settings page then
928
+ // showed two rows as 默认. The upgrade renames the user's kit and repoints every
929
+ // binding that used it.
930
+ const clashHome = join(tmpdir(), `loadout-clash-${process.pid}`)
931
+ rmSync(clashHome, { recursive: true, force: true })
932
+ mkdirSync(join(clashHome, '.dsh', 'sessions', 'ws', 'session-clash'), { recursive: true })
933
+ process.env.USERPROFILE = clashHome
934
+ process.env.HOME = clashHome
935
+ writeFileSync(join(clashHome, '.dsh', 'loadout.json'), JSON.stringify({
936
+ schema: 2,
937
+ kits: [{ name: 'chat', title: '聊天', builtins: ['read'], plugins: [] }],
938
+ defaultKit: 'chat',
939
+ assignments: { 'session-clash': 'chat' },
940
+ migratedAt: new Date().toISOString(),
941
+ }), 'utf8')
942
+ const clash = await import('../index.js?clash=1')
943
+ const ctxClash = makeCtx()
944
+ clash.apply(ctxClash, { defaultKit: '', kits: config.kits })
945
+ const clashStore = JSON.parse(readFileSync(join(clashHome, '.dsh', 'loadout.json'), 'utf8'))
946
+ check('a user kit shadowing a preset is renamed',
947
+ clashStore.kits.length === 1 && clashStore.kits[0].name === 'chat-2'
948
+ && clashStore.kits[0].title === '聊天' && clashStore.kits[0].builtins.join(',') === 'read',
949
+ JSON.stringify(clashStore.kits))
950
+ check('the renamed kit keeps its default and its bindings',
951
+ clashStore.defaultKit === 'chat-2' && clashStore.assignments['session-clash'] === 'chat-2',
952
+ JSON.stringify({ defaultKit: clashStore.defaultKit, assignments: clashStore.assignments }))
953
+ process.env.USERPROFILE = savedHomeV2
954
+ process.env.HOME = savedHomeV2b
955
+ rmSync(clashHome, { recursive: true, force: true })
956
+
925
957
  // ---- legacy store: plugin-kits.json is read once ---------------------------
926
958
  const legacyHome = join(tmpdir(), `loadout-legacy-${process.pid}`)
927
959
  rmSync(legacyHome, { recursive: true, force: true })