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 +6 -0
- package/client.js +34 -3
- package/index.js +39 -0
- package/package.json +1 -1
- package/scripts/client-smoke.mjs +15 -0
- package/scripts/selftest.mjs +32 -0
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
|
-
|
|
480
|
-
}, [
|
|
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:
|
|
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.
|
|
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",
|
package/scripts/client-smoke.mjs
CHANGED
|
@@ -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} · 编辑`)
|
package/scripts/selftest.mjs
CHANGED
|
@@ -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 })
|