Soft-fail Desktop 0.2 settings primitives for search-mcp.

Avoid hard settingsScope/store requires and inject ui-slots so the plugin loads on Desktop without breaking boot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-09 22:46:35 +08:00
parent d5c4d2006c
commit 743e894598
3 changed files with 213 additions and 22 deletions

View file

@ -57,19 +57,57 @@ export const Config = z.object({
/** Settings namespace owning this plugin's section (Settings → Plugins card). */
export const SEARCH_MCP_SETTINGS_NAMESPACE = 'search-mcp';
/** Normalize settings.describe() across DSH generations. */
function describeRows(describe) {
if (typeof describe !== 'function') return [];
try {
const raw = describe();
if (Array.isArray(raw)) return raw;
if (raw && typeof raw === 'object' && Array.isArray(raw.namespaces)) return raw.namespaces;
} catch {
// describe can throw while the provider is settling
}
return [];
}
/** Register the search provider and the live settings section. */
export function apply(ctx, config) {
let current = () => config;
// Optional settings seam: fall back to the composition entry when settings
// is absent (same pattern as @deepseek-ai/dsh-web-search-deepseek).
ctx.inject(['settings'], (settingsCtx) => {
settingsCtx.settings.installSection(ctx, SEARCH_MCP_SETTINGS_NAMESPACE, Config, config, {
const settings = settingsCtx.settings;
const hooks = {
setSource: (source) => {
current = source;
},
// Provider projects the section per search; no re-registration needed.
onChange: () => {},
});
};
if (typeof settings?.installSection === 'function') {
settings.installSection(ctx, SEARCH_MCP_SETTINGS_NAMESPACE, Config, config, hooks);
return;
}
// DSH ≥0.1.7 / 0.2.0 — Config projection via describe() (no installSection).
const readLive = () => {
const row = describeRows(settings?.describe).find((item) => item.ns === SEARCH_MCP_SETTINGS_NAMESPACE);
if (row?.value !== null && typeof row?.value === 'object' && !Array.isArray(row.value)) {
return { ...config, ...row.value };
}
return config;
};
hooks.setSource(readLive);
let last = JSON.stringify(readLive());
const timer = setInterval(() => {
const next = readLive();
const fingerprint = JSON.stringify(next);
if (fingerprint === last) return;
last = fingerprint;
hooks.onChange();
}, 2_000);
settingsCtx.effect(() => () => clearInterval(timer), 'dsh-search-mcp: settings describe poll');
settingsCtx.logger?.info?.(
'dsh-search-mcp: following settings via describe() (no installSection — DSH ≥0.1.7 path)',
);
});
// `registerSearchProvider` owns its cleanup via ctx.effect (HMR/dispose safe).
ctx.web.registerSearchProvider(new SearchMCPProvider(() => resolveOptions(ctx, current())));