Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"agent"
],
"scripts": {
"postinstall": "patch-package && node scripts/install-brand-assets.mjs && install-electron --no",
"postinstall": "patch-package && node scripts/install-plugin-compatibility.mjs && node scripts/install-brand-assets.mjs && install-electron --no",
"dev": "electron-vite dev",
"build": "electron-vite build",
"icons:generate": "node scripts/generate-app-icons.mjs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
diff --git a/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js
index aa98760..6967420 100644
--- a/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js
+++ b/node_modules/@deepseek-ai/dsh-client-ui-settings-plugins/lib/client.js
@@ -982,7 +982,10 @@ window.__ModuleLoader__.load({
}
publish() {
const served = new Set(this.served);
- const namespaces = this.entries().flatMap((entry) => entry.options.key !== void 0 && served.has(entry.options.key) ? [entry.options.key] : []);
+ // rc.6 list registrations gain key=id in the shell compatibility shim.
+ // Keep those self-contained cards visible; modern key-only cards still
+ // require a matching Host settings namespace.
+ const namespaces = this.entries().flatMap((entry) => entry.options.key !== void 0 && (entry.options.id === entry.options.key || served.has(entry.options.key)) ? [entry.options.key] : []);
const previous = this.store.getSnapshot();
if (previous.loaded === this.loaded && previous.namespaces.length === namespaces.length && previous.namespaces.every((ns, index) => ns === namespaces[index])) return;
this.store.set({
20 changes: 20 additions & 0 deletions patches/@deepseek-ai+dsh-client-ui-slots+0.1.0-rc.7.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
diff --git a/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js b/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js
index c653bea..f8e6358 100644
--- a/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js
+++ b/node_modules/@deepseek-ai/dsh-client-ui-slots/lib/index.js
@@ -65,6 +65,15 @@ var SlotCore = class {
const rec = this.records.get(options.name);
if (!rec?.spec) throw new Error(`slot "${options.name}" is not declared (a parent entry's children table must declare it)`);
const spec = rec.spec;
+ // rc.6 exposed the plugin settings surface as a list slot, so existing
+ // third-party bundles register an id. rc.7 made this one slot keyed;
+ // preserve the old identity while leaving explicit keys authoritative.
+ if (options.name === "settings.plugin.item" && spec.kind === "keyed" && options.key === void 0 && options.id !== void 0) {
+ options = {
+ ...options,
+ key: options.id
+ };
+ }
const priority = options.priority ?? 0;
const occupantHint = (occupant) => `at priority ${priority}${occupant.registrant !== void 0 ? ` (registered by ${occupant.registrant})` : ""} — register at a different priority to shadow it (lowest renders)`;
switch (spec.kind) {
39 changes: 39 additions & 0 deletions scripts/install-plugin-compatibility.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { readFile, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'

const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const frontendDirectory = path.join(
projectRoot,
'node_modules',
'@deepseek-ai',
'dsh-web-frontend',
'dist'
)
const indexPath = path.join(frontendDirectory, 'index.html')

const index = await readFile(indexPath, 'utf8')
const assetUrl = index.match(/src="\/(assets\/index-[^"]+\.js)"/)?.[1]
if (assetUrl === undefined) {
throw new Error('Could not install plugin compatibility: DSH frontend asset was not found')
}

const assetPath = path.join(frontendDirectory, assetUrl)
const original = 'const u=s.spec,c=r.priority??0'
const compatible =
'const u=s.spec;r.name==="settings.plugin.item"&&u.kind==="keyed"&&r.key===void 0&&r.id!==void 0&&(r={...r,key:r.id});const c=r.priority??0'
const asset = await readFile(assetPath, 'utf8')

if (!asset.includes(compatible)) {
const occurrences = asset.split(original).length - 1
if (occurrences !== 1) {
throw new Error(
`Could not install plugin compatibility in ${path.relative(projectRoot, assetPath)}: expected one SlotCore registration site, found ${occurrences}`
)
}
await writeFile(assetPath, asset.replace(original, compatible))
}

console.log(
`Installed legacy plugin compatibility: ${path.relative(projectRoot, assetPath)}`
)
137 changes: 137 additions & 0 deletions test/plugin-slot-compatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
import { describe, expect, it } from 'vitest'

const projectRoot = path.resolve(import.meta.dirname, '..')

type RuntimeRegister = (
options: Record<string, unknown>,
component: () => null
) => () => void

function keyedSlotCore(name = 'settings.plugin.item'): {
core: SlotCore
register: RuntimeRegister
} {
const core = new SlotCore()
const register = core.register.bind(core) as unknown as RuntimeRegister
register(
{
name: 'root',
children: { [name]: { kind: 'keyed', scope: 'root' } }
},
() => null
)
return { core, register }
}

describe('legacy plugin settings slot compatibility', () => {
it('uses an rc.6 list id as the rc.7 key and preserves disposal', () => {
const { core, register } = keyedSlotCore()

const dispose = register(
{ name: 'settings.plugin.item', id: 'github', order: 30 },
() => null
)

expect(core.entriesOfSlot('settings.plugin.item')).toHaveLength(1)
expect(core.entriesOfSlot('settings.plugin.item')[0]?.options).toMatchObject({
id: 'github',
key: 'github',
order: 30
})

dispose()
expect(core.entriesOfSlot('settings.plugin.item')).toHaveLength(0)
})

it('keeps explicit rc.7 keys authoritative and rejects collisions', () => {
const { core, register } = keyedSlotCore()

register(
{ name: 'settings.plugin.item', id: 'legacy-id', key: 'explicit-key' },
() => null
)

expect(core.entriesOfSlot('settings.plugin.item')[0]?.options.key).toBe(
'explicit-key'
)
expect(() =>
register({ name: 'settings.plugin.item', key: 'explicit-key' }, () => null)
).toThrow('already has an entry for key "explicit-key"')
})

it('does not relax keyed registration for unrelated slots', () => {
const { register } = keyedSlotCore('unrelated.keyed')

expect(() =>
register({ name: 'unrelated.keyed', id: 'legacy-id' }, () => null)
).toThrow('keyed slot "unrelated.keyed" requires options.key')
})

it('ships the matching legacy dispatch patch for the settings controller', async () => {
const settingsPatch = await readFile(
path.join(
projectRoot,
'patches',
'@deepseek-ai+dsh-client-ui-settings-plugins+0.1.0-rc.7.patch'
),
'utf8'
)

expect(settingsPatch).toContain(
'entry.options.id === entry.options.key || served.has(entry.options.key)'
)
})

it('patches the SlotCore bundle actually served by the desktop app', async () => {
const indexHtml = await readFile(
path.join(
projectRoot,
'node_modules',
'@deepseek-ai',
'dsh-web-frontend',
'dist',
'index.html'
),
'utf8'
)
const asset = indexHtml.match(/src="\/(assets\/index-[^"]+\.js)"/)?.[1]
expect(asset).toBeDefined()

const compatibilityInstaller = await readFile(
path.join(
projectRoot,
'scripts',
'install-plugin-compatibility.mjs'
),
'utf8'
)
const packageJson = JSON.parse(
await readFile(path.join(projectRoot, 'package.json'), 'utf8')
) as { scripts: { postinstall: string } }

expect(compatibilityInstaller).toContain(
'r.name==="settings.plugin.item"&&u.kind==="keyed"&&r.key===void 0&&r.id!==void 0&&(r={...r,key:r.id})'
)
expect(packageJson.scripts.postinstall).toContain(
'node scripts/install-plugin-compatibility.mjs'
)

const servedAsset = await readFile(
path.join(
projectRoot,
'node_modules',
'@deepseek-ai',
'dsh-web-frontend',
'dist',
asset ?? ''
),
'utf8'
)
expect(servedAsset).toContain(
'r.name==="settings.plugin.item"&&u.kind==="keyed"&&r.key===void 0&&r.id!==void 0&&(r={...r,key:r.id})'
)
})
})
Loading