From 743e8945989465b3b24de7da339a0c76ecf4e440 Mon Sep 17 00:00:00 2001 From: oliver Date: Fri, 9 Oct 2026 22:46:35 +0800 Subject: [PATCH] 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 --- lib/client.browser.js | 187 ++++++++++++++++++++++++++++++++++++++---- lib/index.js | 44 +++++++++- package.json | 4 +- 3 files changed, 213 insertions(+), 22 deletions(-) diff --git a/lib/client.browser.js b/lib/client.browser.js index 56aa9d1..0713ca2 100644 --- a/lib/client.browser.js +++ b/lib/client.browser.js @@ -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 diff --git a/lib/index.js b/lib/index.js index 725208b..58c3e91 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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()))); diff --git a/package.json b/package.json index ae984fc..ef17343 100644 --- a/package.json +++ b/package.json @@ -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" ] }