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

@ -24,10 +24,80 @@ window.__ModuleLoader__.load({
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let react_jsx_runtime = require("react/jsx-runtime");
let react = require("react");
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
// 0.1.2+: createSnapshotStore lives on the platform seed @deepseek-ai/dsh-client-store
// (old @deepseek-ai/dsh-client-runtime/client is gone from the module table).
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-store");
// Desktop 0.2 may not seed ui-primitives / dsh-client-store in the ModuleLoader
// table. Soft-require + local fallbacks so import never fails web boot.
let _deepseek_ai_dsh_client_ui_primitives = {};
try {
_deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
} catch {
_deepseek_ai_dsh_client_ui_primitives = {};
}
if (
_deepseek_ai_dsh_client_ui_primitives
&& _deepseek_ai_dsh_client_ui_primitives.default
&& typeof _deepseek_ai_dsh_client_ui_primitives.default === "object"
) {
_deepseek_ai_dsh_client_ui_primitives = {
..._deepseek_ai_dsh_client_ui_primitives.default,
..._deepseek_ai_dsh_client_ui_primitives,
};
}
if (typeof _deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14 !== "function") {
_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14 = function IconChevronDownOutline14(props) {
return (0, react_jsx_runtime.jsx)("svg", {
viewBox: "0 0 14 14",
width: "14",
height: "14",
"aria-hidden": "true",
className: props && props.className,
children: (0, react_jsx_runtime.jsx)("path", {
d: "M3.5 5.25L7 8.75L10.5 5.25",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.5",
strokeLinecap: "round",
strokeLinejoin: "round",
}),
});
};
}
function createSnapshotStoreFallback(initial) {
let state = initial;
const listeners = new Set();
return {
getSnapshot() {
return state;
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
set(next) {
state = next;
for (const listener of listeners) {
try { listener(); } catch { /* ignore */ }
}
},
};
}
let createSnapshotStore = createSnapshotStoreFallback;
try {
const storeMod = require("@deepseek-ai/dsh-client-store");
if (typeof storeMod?.createSnapshotStore === "function") {
createSnapshotStore = storeMod.createSnapshotStore;
}
} catch {
try {
const runtimeMod = require("@deepseek-ai/dsh-client-runtime");
const nested = runtimeMod?.client || runtimeMod;
if (typeof nested?.createSnapshotStore === "function") {
createSnapshotStore = nested.createSnapshotStore;
}
} catch {
// keep fallback
}
}
const _deepseek_ai_dsh_client_runtime_client = { createSnapshotStore };
//#region styles
const css = [
@ -1043,30 +1113,113 @@ window.__ModuleLoader__.load({
//#endregion
//#region apply
/** Required client services (resolved by the client kernel). */
const inject = ["slots", "locale", "settingsScope", "remote", "remote.credentials", "remote.settings"];
/**
* Hard-inject only services present on DSH ≤0.1.5 and ≥0.1.7 / 0.2.0.
* Do NOT hard-inject settingsScope — Desktop 0.2 removed it (configForms).
*/
const inject = ["slots", "locale"];
function noopDescribeFace() {
return {
load: async () => {},
ensure: async () => {},
acceptView: () => {},
};
}
function apply(ctx) {
const t = ctx.locale.bind(NS);
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-search-mcp: section dictionaries");
const describeFace = ctx.settingsScope.describe();
const controller = new SearchMcpCardController(ctx.settingsScope.bind({ namespace: NS }), ctx.remote, describeFace);
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
function resolveRemote(ctx) {
return ctx.remote || ctx.get?.("remote") || {
credentials: { set: async () => ({ ok: false }), describe: async () => ({ ok: false }) },
settings: { mutate: async () => ({ ok: false }), describe: async () => ({ ok: false }) },
$on: () => () => {},
};
}
function mountSearchCard(ctx, scope, describeFace) {
const remote = resolveRemote(ctx);
const controller = new SearchMcpCardController(scope, remote, describeFace || noopDescribeFace());
try {
ctx.effect(() => {
if (typeof remote.$on !== "function") return () => {};
return remote.$on("credentials/reference-updated", (ref) => {
const used = controller.baseDraft().servers.some((row) => row.apiKeyEnv.trim() === ref);
if (used) controller.readSecretState();
}), "dsh-search-mcp: credential status invalidation");
});
}, "dsh-search-mcp: credential status invalidation");
} catch { /* optional */ }
try {
ctx.slots.inject("settings.plugin.item", function* () {
yield ctx.slots.register({
name: "settings.plugin.item",
key: NS,
order: 30,
locale: NS,
inject: () => controller.inject(),
}, SearchMcpCard);
});
} catch (error) {
ctx.logger?.warn?.("dsh-search-mcp: settings.plugin.item unavailable: %s", error);
}
}
locale: NS,
inject: () => controller.inject()
}, SearchMcpCard);
function apply(ctx) {
const t = ctx.locale.bind(NS);
ctx.effect(() => {
try {
return ctx.locale.register(NS, { zh, en });
} catch {
const offZh = ctx.locale.register(NS, "zh", zh);
const offEn = ctx.locale.register(NS, "en", en);
return () => { offZh?.(); offEn?.(); };
}
}, "dsh-search-mcp: section dictionaries");
let mounted = false;
const tryMount = (scope, describeFace, source) => {
if (mounted || !scope || typeof scope.getSnapshot !== "function") return;
mounted = true;
ctx.logger?.info?.("dsh-search-mcp: settings card via %s", source);
mountSearchCard(ctx, scope, describeFace);
};
// DSH ≥0.1.7 / 0.2.0 — configForms (settingsScope removed).
ctx.inject(["configForms"], (formsCtx) => {
const forms = formsCtx.configForms || formsCtx.get?.("configForms");
if (!forms || typeof forms.get !== "function") {
formsCtx.logger?.warn?.("dsh-search-mcp: configForms present but .get missing");
return;
}
const candidates = [NS, "dsh-search-mcp", "search-mcp"];
const hit = candidates.map((id) => {
try { return { id, scope: forms.get(id) }; } catch { return null; }
}).find((row) => row?.scope && typeof row.scope.getSnapshot === "function");
if (hit) tryMount(hit.scope, noopDescribeFace(), `configForms:${hit.id}`);
if (typeof forms.whileServed === "function") {
formsCtx.effect(() => forms.whileServed(candidates, (served) => {
const id = candidates.find((c) => served.has(c));
if (!id) return () => {};
try {
const scope = forms.get(id);
tryMount(scope, noopDescribeFace(), `whileServed:${id}`);
} catch { /* ignore */ }
return () => {};
}), "dsh-search-mcp: configForms whileServed");
}
});
// DSH ≤0.1.5 — settingsScope binder.
ctx.inject(["settingsScope"], (scopeCtx) => {
if (mounted) return;
const binder = scopeCtx.settingsScope || scopeCtx.get?.("settingsScope");
if (!binder || typeof binder.bind !== "function") {
scopeCtx.logger?.warn?.("dsh-search-mcp: settingsScope present but .bind missing");
return;
}
const describeFace = typeof binder.describe === "function" ? binder.describe() : noopDescribeFace();
tryMount(binder.bind({ namespace: NS }), describeFace, "settingsScope");
});
void t;
}
//#endregion

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())));

View file

@ -1,6 +1,6 @@
{
"name": "dsh-search-mcp",
"version": "0.2.0",
"version": "0.2.2",
"description": "Replace dsh's built-in web search with search MCP servers (Tavily / Brave / Exa / Perplexity / DuckDuckGo / custom), configured from the web Settings page. When this plugin is enabled the built-in DeepSeek search provider is disabled.",
"type": "module",
"main": "lib/index.js",
@ -42,7 +42,7 @@
"platform": "web",
"immediately": true,
"inject": [
"@deepseek-ai/dsh-client-ui-settings-plugins",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-locale"
]
}