@klarkxy/dsh-web-search-manager 0.1.10 → 0.2.0

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.
@@ -375,6 +375,49 @@ ${scope} {
375
375
  `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
376
376
  }
377
377
  //#endregion
378
+ //#region ../dsh-plugin-kit/src/client-utils.ts
379
+ /** Fixed pause for failing background reads; manual work is never replayed. */
380
+ function createPluginReadGate(now = Date.now) {
381
+ let pausedUntil = 0;
382
+ let epoch = 0;
383
+ return {
384
+ canRead: () => now() >= pausedUntil,
385
+ reset: () => {
386
+ epoch++;
387
+ pausedUntil = 0;
388
+ },
389
+ async run(read) {
390
+ const current = epoch;
391
+ try {
392
+ const value = await read();
393
+ if (current === epoch) pausedUntil = 0;
394
+ return value;
395
+ } catch (error) {
396
+ const message = error instanceof Error ? error.message : "";
397
+ if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) pausedUntil = now() + 3e4;
398
+ throw error;
399
+ }
400
+ }
401
+ };
402
+ }
403
+ //#endregion
404
+ //#region src/contracts.ts
405
+ const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
406
+ function pickActiveSearch(order, configured) {
407
+ return order.find(configured) ?? "";
408
+ }
409
+ function safeConfigurationUrl(value) {
410
+ if (typeof value !== "string" || !value || /[\u0000-\u0020\\]/.test(value)) return;
411
+ try {
412
+ const url = new URL(value, "https://dsh.invalid");
413
+ if (url.username || url.password) return;
414
+ if (value.startsWith("/") && !value.startsWith("//") || value.startsWith("#")) return url.origin === "https://dsh.invalid" ? value : void 0;
415
+ return /^https:\/\//i.test(value) && url.protocol === "https:" ? value : void 0;
416
+ } catch {
417
+ return;
418
+ }
419
+ }
420
+ //#endregion
378
421
  //#region src/provider-info.ts
379
422
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
380
423
  const providerInfo = {
@@ -419,22 +462,37 @@ const providerInfo = {
419
462
  pricingUrl: "https://docs.tavily.com/documentation/api-credits"
420
463
  }
421
464
  };
422
- function searchProviderInfo(provider) {
423
- const known = Object.hasOwn(providerInfo, provider.id) ? providerInfo[provider.id] : void 0;
424
- return {
425
- description: known?.description ?? provider.description,
426
- pricing: provider.pricing ?? known?.pricing ?? (provider.billing === "none" ? "该后端不收取搜索费用。" : provider.billing === "model-and-tools" ? "按模型用量与工具调用计费;额度与单价见供应商说明。" : "按 API 调用计费;额度与单价见供应商说明。"),
427
- pricingUrl: provider.pricingUrl ?? known?.pricingUrl ?? provider.signupUrl
428
- };
465
+ function bundledCatalogId(provider) {
466
+ if (!(typeof provider.configurationOwned === "boolean" ? provider.configurationOwned : !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager")) return;
467
+ return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
429
468
  }
430
- //#endregion
431
- //#region src/contracts.ts
432
- const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
433
- function resolveSearchOrder(knownIds, settings, _keyed) {
434
- return [...new Set(settings.searchOrder.filter((id) => knownIds.includes(id)))];
469
+ function missingPricing(locale) {
470
+ return locale === "zh" ? "未提供费用信息" : "No pricing information provided";
435
471
  }
436
- function pickActiveSearch(order, configured) {
437
- return order.find(configured) ?? "";
472
+ function usesBundledPricing(provider) {
473
+ if (provider.pricing?.trim()) return false;
474
+ const catalogId = bundledCatalogId(provider);
475
+ return Boolean(catalogId && Object.hasOwn(providerInfo, catalogId));
476
+ }
477
+ function searchProviderInfo(provider, locale = "zh") {
478
+ const catalogId = bundledCatalogId(provider);
479
+ const known = catalogId && Object.hasOwn(providerInfo, catalogId) ? providerInfo[catalogId] : void 0;
480
+ const description = provider.description.trim() ? provider.description : known?.description ?? provider.description;
481
+ if (provider.pricing?.trim()) return {
482
+ description,
483
+ pricing: provider.pricing,
484
+ pricingUrl: provider.pricingUrl ?? known?.pricingUrl ?? provider.signupUrl
485
+ };
486
+ if (known) return {
487
+ description,
488
+ pricing: known.pricing,
489
+ pricingUrl: provider.pricingUrl ?? known.pricingUrl ?? provider.signupUrl
490
+ };
491
+ return {
492
+ description,
493
+ pricing: missingPricing(locale),
494
+ pricingUrl: provider.pricingUrl ?? provider.signupUrl
495
+ };
438
496
  }
439
497
  //#endregion
440
498
  //#region src/client.tsx
@@ -454,32 +512,129 @@ function editable(settings) {
454
512
  const { revision: _revision, ...rest } = settings;
455
513
  return rest;
456
514
  }
515
+ const STATUS_REFRESH_MS = 3e3;
516
+ function isPageHidden(hidden) {
517
+ if (hidden) return true;
518
+ return typeof document !== "undefined" && document.visibilityState === "hidden";
519
+ }
520
+ function canStartStatusRead(input) {
521
+ if (!input.mounted) return false;
522
+ if (input.source === "poll") return !input.hidden && !input.busy && !input.reordering && !input.reading;
523
+ return true;
524
+ }
525
+ function isExternalConfigurationUrl(url) {
526
+ return /^https:\/\//i.test(url);
527
+ }
528
+ function shouldApplyPolledStatus(input) {
529
+ if (input.hidden || input.busy || input.reordering) return false;
530
+ if (input.requestId !== input.latestRequestId) return false;
531
+ if (input.currentRevision !== void 0 && input.incomingRevision < input.currentRevision) return false;
532
+ return true;
533
+ }
534
+ function bindStatusRefresh(options) {
535
+ const intervalMs = options.intervalMs ?? 3e3;
536
+ let inFlight = false;
537
+ let stopped = false;
538
+ const run = async () => {
539
+ if (stopped || inFlight || options.isHidden()) return;
540
+ inFlight = true;
541
+ try {
542
+ await options.tick();
543
+ } finally {
544
+ inFlight = false;
545
+ }
546
+ };
547
+ const timer = (options.setIntervalFn ?? setInterval)(() => {
548
+ run();
549
+ }, intervalMs);
550
+ const onVisible = () => {
551
+ if (!options.isHidden()) run();
552
+ };
553
+ const add = options.addListener ?? ((type, listener) => {
554
+ if (typeof document !== "undefined") document.addEventListener(type, listener);
555
+ });
556
+ const remove = options.removeListener ?? ((type, listener) => {
557
+ if (typeof document !== "undefined") document.removeEventListener(type, listener);
558
+ });
559
+ add("visibilitychange", onVisible);
560
+ return () => {
561
+ stopped = true;
562
+ (options.clearIntervalFn ?? clearInterval)(timer);
563
+ remove("visibilitychange", onVisible);
564
+ };
565
+ }
457
566
  function isProviderOn(status, provider) {
458
- const enabled = provider.kind === "search" ? status.searchActive : status.fetchActive;
459
- const selected = status.settings[provider.kind === "search" ? "searchProvider" : "fetchProvider"];
460
- return enabled && selected === provider.id;
567
+ if (provider.kind === "fetch") return status.fetchActive && status.settings.fetchProvider === provider.id && provider.configured;
568
+ return status.searchActive && selectedSearchBackend(status)?.id === provider.id;
461
569
  }
462
- function searchBackends(status) {
463
- const all = status.providers.filter((provider) => provider.kind === "search");
464
- return resolveSearchOrder(all.map((provider) => provider.id), status.settings, (id) => Boolean(all.find((provider) => provider.id === id)?.credentialRef)).flatMap((id) => all.find((provider) => provider.id === id) ?? []);
570
+ function dormantSearchPlaceholder(id) {
571
+ return {
572
+ id,
573
+ label: id,
574
+ description: "",
575
+ billing: "unknown",
576
+ kind: "search",
577
+ configured: false,
578
+ calls: 0,
579
+ failures: 0,
580
+ configurationOwned: false
581
+ };
582
+ }
583
+ function isRegisteredSearch(status, id) {
584
+ return status.providers.some((provider) => provider.kind === "search" && provider.id === id);
465
585
  }
466
586
  function catalogSearchBackends(status) {
467
- const enabled = searchBackends(status);
468
- const rest = status.providers.filter((provider) => provider.kind === "search" && !enabled.some((row) => row.id === provider.id));
469
- return [...enabled, ...rest];
587
+ const byId = new Map(status.providers.filter((provider) => provider.kind === "search").map((provider) => [provider.id, provider]));
588
+ const rows = [];
589
+ const seen = /* @__PURE__ */ new Set();
590
+ for (const id of status.settings.searchOrder) {
591
+ if (!id || seen.has(id)) continue;
592
+ seen.add(id);
593
+ rows.push(byId.get(id) ?? dormantSearchPlaceholder(id));
594
+ }
595
+ for (const provider of status.providers) {
596
+ if (provider.kind !== "search" || seen.has(provider.id)) continue;
597
+ rows.push(provider);
598
+ }
599
+ return rows;
600
+ }
601
+ function searchBackends(status) {
602
+ const participating = new Set(status.settings.searchOrder);
603
+ return catalogSearchBackends(status).filter((provider) => participating.has(provider.id));
470
604
  }
471
605
  function selectedSearchBackend(status) {
472
- const backends = searchBackends(status);
606
+ const backends = searchBackends(status).filter((provider) => isRegisteredSearch(status, provider.id));
473
607
  const id = pickActiveSearch(backends.map((provider) => provider.id), (id) => Boolean(backends.find((provider) => provider.id === id)?.configured));
474
608
  return backends.find((provider) => provider.id === id);
475
609
  }
476
- function ExternalLink({ url, label, children }) {
610
+ function droppedDormantIds(settings, order, providers) {
611
+ const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
612
+ return [...new Set(settings.searchOrder.filter((id) => id && !known.has(id) && !order.includes(id)))];
613
+ }
614
+ function configurationAccess(provider) {
615
+ if (ownsProviderConfiguration(provider)) return { kind: "owned" };
616
+ const url = safeConfigurationUrl(provider.configurationUrl);
617
+ if (url) return {
618
+ kind: "link",
619
+ url
620
+ };
621
+ if (provider.configurationOwner?.trim()) return {
622
+ kind: "owner",
623
+ name: provider.configurationOwner
624
+ };
625
+ return { kind: "unknown" };
626
+ }
627
+ function AppLink({ url, label, children }) {
628
+ const external = isExternalConfigurationUrl(url);
477
629
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
478
630
  href: url,
479
- target: "_blank",
480
- rel: "noreferrer noopener",
631
+ ...external ? {
632
+ target: "_blank",
633
+ rel: "noreferrer noopener"
634
+ } : {},
481
635
  "aria-label": label,
482
636
  onClick: (event) => {
637
+ if (!external) return;
483
638
  const bridge = globalThis.dshWindow;
484
639
  if (bridge?.openExternal) {
485
640
  event.preventDefault();
@@ -495,9 +650,13 @@ function canEnableSearch(order, providers, keys, writable) {
495
650
  if (!provider) return false;
496
651
  if (provider.configured) return true;
497
652
  const typed = (keys[provider.id] ?? "").trim();
498
- return Boolean(provider.credentialRef && typed && writable[provider.credentialRef] !== false);
653
+ return Boolean(ownsProviderConfiguration(provider) && provider.credentialRef && typed && writable[provider.credentialRef] !== false);
499
654
  });
500
655
  }
656
+ function ownsProviderConfiguration(provider) {
657
+ if (typeof provider.configurationOwned === "boolean") return provider.configurationOwned;
658
+ return !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager";
659
+ }
501
660
  function nextSearchEnabled(currentlyEnabled, nextOrder, providers, keys, writable) {
502
661
  return currentlyEnabled && canEnableSearch(nextOrder, providers, keys, writable);
503
662
  }
@@ -549,11 +708,14 @@ const copy = {
549
708
  dragPrefix: "拖动排序 ",
550
709
  dragHint: "拖动排序,或用上下方向键",
551
710
  inUse: "当前使用",
711
+ available: "可用",
712
+ unavailable: "暂不可用",
713
+ dormant: "未加载,保留选择",
552
714
  signup: "去注册",
553
715
  pricing: "费用说明",
554
716
  website: "官网",
717
+ openConfig: "打开配置",
555
718
  needsKey: "待配置 Key",
556
- unavailable: "暂不可用",
557
719
  sharedKey: "与其他设置共用凭据",
558
720
  keyConfigured: "已配置,留空不替换",
559
721
  keyPlaceholder: "输入 API Key",
@@ -567,7 +729,11 @@ const copy = {
567
729
  maxResults: "每条查询结果上限",
568
730
  maxQueries: "每次调用查询上限",
569
731
  timeoutMs: "请求超时(毫秒)",
570
- maxFetchChars: "正文字符上限"
732
+ maxFetchChars: "正文字符上限",
733
+ fetchTitle: "网页读取",
734
+ fetchProvider: "读取服务",
735
+ ownerSettings: "配置入口:",
736
+ unknownOwner: "请在服务所属插件中配置"
571
737
  },
572
738
  en: {
573
739
  tabsLabel: "Web search settings",
@@ -589,11 +755,14 @@ const copy = {
589
755
  dragPrefix: "Reorder ",
590
756
  dragHint: "Drag to reorder, or use arrow keys",
591
757
  inUse: "In use",
758
+ available: "Available",
759
+ unavailable: "Unavailable",
760
+ dormant: "Not loaded; selection kept",
592
761
  signup: "Sign up",
593
762
  pricing: "Pricing",
594
763
  website: "Website",
764
+ openConfig: "Open configuration",
595
765
  needsKey: "Key required",
596
- unavailable: "Unavailable",
597
766
  sharedKey: "Shares a credential with other settings",
598
767
  keyConfigured: "Configured; empty keeps it",
599
768
  keyPlaceholder: "Enter API key",
@@ -607,14 +776,18 @@ const copy = {
607
776
  maxResults: "Max results/query",
608
777
  maxQueries: "Max queries/call",
609
778
  timeoutMs: "Request timeout (ms)",
610
- maxFetchChars: "Max text chars"
779
+ maxFetchChars: "Max text chars",
780
+ fetchTitle: "Read web pages",
781
+ fetchProvider: "Fetch service",
782
+ ownerSettings: "Configure in: ",
783
+ unknownOwner: "Configure this service in its plugin"
611
784
  }
612
785
  };
613
786
  function movedText(locale, label, position) {
614
787
  return locale === "zh" ? `${label} 已移至第 ${position} 位` : `${label} moved to position ${position}`;
615
788
  }
616
789
  function pricingCheckedText(locale, date) {
617
- return locale === "zh" ? `价格核对于 ${date},额度与费用以供应商账户为准。` : `Prices checked on ${date}; your provider account is authoritative.`;
790
+ return locale === "zh" ? `内置适配器价格核对于 ${date},额度与费用以供应商账户为准。` : `Bundled adapter prices checked on ${date}; your provider account is authoritative.`;
618
791
  }
619
792
  function testOkText(locale, sources) {
620
793
  return locale === "zh" ? `连接正常,返回 ${sources} 条来源。` : `Connection OK: ${sources} sources returned.`;
@@ -655,13 +828,25 @@ function parseLimit(raw, min, max) {
655
828
  * Settings written by an immediate action (a switch or a reorder). It keeps the
656
829
  * saved limits and never includes typed-but-unsaved keys or limit drafts.
657
830
  */
658
- function immediateUpdate(settings, on, order) {
831
+ function immediateUpdate(settings, on, order, providers, remove = []) {
832
+ let searchOrder = order;
833
+ if (providers) {
834
+ const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
835
+ const removed = new Set(remove);
836
+ const following = order.filter((id) => !removed.has(id));
837
+ if (following.some((id) => !known.has(id))) searchOrder = following;
838
+ else {
839
+ searchOrder = settings.searchOrder.flatMap((id) => {
840
+ if (removed.has(id)) return [];
841
+ return known.has(id) ? following.splice(0, 1) : [id];
842
+ });
843
+ searchOrder.push(...following);
844
+ }
845
+ }
659
846
  return {
660
847
  ...editable(settings),
661
- searchOrder: order,
662
- searchEnabled: on,
663
- fetchProvider: "http",
664
- fetchEnabled: on
848
+ searchOrder,
849
+ searchEnabled: on
665
850
  };
666
851
  }
667
852
  function limitTexts(settings) {
@@ -679,7 +864,7 @@ function useHostLocale(client) {
679
864
  }
680
865
  /** Keyboard reorders are batched: persist once the arrows stop for this long. */
681
866
  const REORDER_DEBOUNCE_MS = 500;
682
- function NetworkSearchSettings({ client }) {
867
+ function NetworkSearchSettings({ client, hidden }) {
683
868
  const locale = useHostLocale(client);
684
869
  const text = copy[locale];
685
870
  const tabsId = (0, react.useId)();
@@ -703,6 +888,17 @@ function NetworkSearchSettings({ client }) {
703
888
  const timerRef = (0, react.useRef)(void 0);
704
889
  const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
705
890
  const refocusRef = (0, react.useRef)(null);
891
+ const busyRef = (0, react.useRef)(false);
892
+ const hiddenRef = (0, react.useRef)(hidden);
893
+ hiddenRef.current = hidden;
894
+ const requestSeq = (0, react.useRef)(0);
895
+ const mountedRef = (0, react.useRef)(true);
896
+ const readingRef = (0, react.useRef)(false);
897
+ const readingRequest = (0, react.useRef)(0);
898
+ const statusRead = (0, react.useRef)(Promise.resolve());
899
+ const readGateRef = (0, react.useRef)(void 0);
900
+ if (!readGateRef.current) readGateRef.current = createPluginReadGate();
901
+ const readGate = readGateRef.current;
706
902
  async function call(endpoint, payload = {}) {
707
903
  return unwrap(await client.connection.rpc.call(WEB_SEARCH_RPC_CHANNEL, endpoint, payload));
708
904
  }
@@ -718,30 +914,109 @@ function NetworkSearchSettings({ client }) {
718
914
  });
719
915
  if (resetDraft) setInvalid({});
720
916
  }
721
- async function load(resetDraft = false) {
722
- const next = await call("status");
723
- adopt(next, resetDraft);
724
- setLoadError("");
725
- const refs = next.providers.flatMap((provider) => provider.credentialRef ? [provider.credentialRef] : []);
726
- if (refs.length) {
727
- const credentials = unwrap(await client.remote.credentials.describe(refs));
728
- setWritable(Object.fromEntries(Object.entries(credentials).map(([ref, value]) => [ref, value.writable])));
917
+ function pollBlocked() {
918
+ return !canStartStatusRead({
919
+ source: "poll",
920
+ mounted: mountedRef.current,
921
+ hidden: isPageHidden(hiddenRef.current),
922
+ busy: busyRef.current,
923
+ reordering: pendingRef.current !== null,
924
+ reading: readingRef.current
925
+ });
926
+ }
927
+ async function describeOwned(next, mine) {
928
+ const refs = next.providers.flatMap((provider) => ownsProviderConfiguration(provider) && provider.credentialRef ? [provider.credentialRef] : []);
929
+ if (!refs.length) return;
930
+ const credentials = unwrap(await readGate.run(() => client.remote.credentials.describe(refs)));
931
+ if (!mountedRef.current || mine !== requestSeq.current) return;
932
+ setWritable(Object.fromEntries(Object.entries(credentials).map(([ref, value]) => [ref, value.writable])));
933
+ }
934
+ async function load(resetDraft = false, source = "user") {
935
+ if (source === "poll" && !readGate.canRead()) return;
936
+ if (source === "user") {
937
+ readGate.reset();
938
+ requestSeq.current += 1;
939
+ }
940
+ while (readingRef.current) {
941
+ if (source === "poll" || !mountedRef.current) return;
942
+ await statusRead.current;
943
+ }
944
+ if (!mountedRef.current) return;
945
+ if (source === "poll" && pollBlocked()) return;
946
+ const mine = ++requestSeq.current;
947
+ readingRef.current = true;
948
+ readingRequest.current = mine;
949
+ let release = () => {};
950
+ statusRead.current = new Promise((resolve) => {
951
+ release = resolve;
952
+ });
953
+ try {
954
+ const next = unwrap(await readGate.run(() => client.connection.rpc.call(WEB_SEARCH_RPC_CHANNEL, "status", {})));
955
+ if (!mountedRef.current || mine !== requestSeq.current) return next;
956
+ if (source === "poll" && !shouldApplyPolledStatus({
957
+ requestId: mine,
958
+ latestRequestId: requestSeq.current,
959
+ busy: busyRef.current,
960
+ reordering: pendingRef.current !== null,
961
+ hidden: isPageHidden(hiddenRef.current),
962
+ currentRevision: statusRef.current?.settings.revision,
963
+ incomingRevision: next.settings.revision
964
+ })) return next;
965
+ adopt(next, resetDraft);
966
+ setLoadError("");
967
+ await describeOwned(next, mine);
968
+ return next;
969
+ } catch (cause) {
970
+ if (!mountedRef.current || mine !== requestSeq.current) return;
971
+ setLoadError(cause instanceof Error && cause.message ? cause.message : text.failed);
972
+ if (source === "poll") return;
973
+ throw cause;
974
+ } finally {
975
+ if (readingRequest.current === mine) readingRef.current = false;
976
+ release();
729
977
  }
730
- return next;
731
978
  }
732
979
  (0, react.useEffect)(() => {
980
+ mountedRef.current = true;
981
+ readGate.reset();
733
982
  load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : text.failed));
983
+ const bind = () => bindStatusRefresh({
984
+ isHidden: () => isPageHidden(hiddenRef.current),
985
+ tick: async () => {
986
+ await load(false, "poll").catch(() => {});
987
+ }
988
+ });
989
+ let stop = bind();
990
+ const stopConnection = client.connection.generation?.subscribe(() => {
991
+ readGate.reset();
992
+ requestSeq.current += 1;
993
+ readingRef.current = false;
994
+ statusRead.current = Promise.resolve();
995
+ stop();
996
+ stop = bind();
997
+ load(false, "poll");
998
+ });
999
+ return () => {
1000
+ mountedRef.current = false;
1001
+ readGate.reset();
1002
+ requestSeq.current += 1;
1003
+ stop();
1004
+ stopConnection?.();
1005
+ if (timerRef.current === void 0) return;
1006
+ clearTimeout(timerRef.current);
1007
+ const current = statusRef.current;
1008
+ const next = pendingRef.current;
1009
+ if (current && next) call("update", {
1010
+ settings: immediateUpdate(current.settings, current.settings.searchEnabled, next, current.providers),
1011
+ expectedRevision: current.settings.revision
1012
+ }).catch(() => {});
1013
+ };
734
1014
  }, [client]);
735
- (0, react.useEffect)(() => () => {
736
- if (timerRef.current === void 0) return;
737
- clearTimeout(timerRef.current);
738
- const current = statusRef.current;
739
- const next = pendingRef.current;
740
- if (current && next) call("update", {
741
- settings: immediateUpdate(current.settings, current.settings.searchEnabled, next),
742
- expectedRevision: current.settings.revision
743
- }).catch(() => {});
744
- }, []);
1015
+ (0, react.useEffect)(() => {
1016
+ if (hidden) return;
1017
+ if (!statusRef.current) return;
1018
+ load(false, "poll").catch(() => {});
1019
+ }, [hidden]);
745
1020
  (0, react.useLayoutEffect)(() => {
746
1021
  const id = refocusRef.current;
747
1022
  if (!id) return;
@@ -750,6 +1025,9 @@ function NetworkSearchSettings({ client }) {
750
1025
  if (handle && document.activeElement !== handle) handle.focus();
751
1026
  });
752
1027
  async function action(run) {
1028
+ readGate.reset();
1029
+ requestSeq.current += 1;
1030
+ busyRef.current = true;
753
1031
  setBusy(true);
754
1032
  setNote("");
755
1033
  setError("");
@@ -759,11 +1037,12 @@ function NetworkSearchSettings({ client }) {
759
1037
  setError(cause instanceof Error && cause.message ? cause.message : text.failed);
760
1038
  await load().catch(() => {});
761
1039
  } finally {
1040
+ busyRef.current = false;
762
1041
  setBusy(false);
763
1042
  }
764
1043
  }
765
1044
  /** Switches and reorders: settings only, applied at once. */
766
- function runImmediate(on, order) {
1045
+ function runImmediate(on, order, remove = []) {
767
1046
  clearTimeout(timerRef.current);
768
1047
  timerRef.current = void 0;
769
1048
  pendingRef.current = null;
@@ -771,7 +1050,7 @@ function NetworkSearchSettings({ client }) {
771
1050
  const current = statusRef.current;
772
1051
  if (!current) return;
773
1052
  const next = await call("update", {
774
- settings: immediateUpdate(current.settings, on, order),
1053
+ settings: immediateUpdate(current.settings, on, order, current.providers, remove),
775
1054
  expectedRevision: current.settings.revision
776
1055
  });
777
1056
  setStatus(next);
@@ -787,7 +1066,7 @@ function NetworkSearchSettings({ client }) {
787
1066
  if (provider.kind !== "search" || only && provider.id !== only.id) continue;
788
1067
  const snapshot = keys[provider.id] ?? "";
789
1068
  const typed = snapshot.trim();
790
- if (!provider.credentialRef || !typed) continue;
1069
+ if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
791
1070
  if (writable[provider.credentialRef] === false) throw new Error(text.readOnly);
792
1071
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
793
1072
  setKeys((current) => current[provider.id] === snapshot ? {
@@ -797,7 +1076,7 @@ function NetworkSearchSettings({ client }) {
797
1076
  }
798
1077
  }
799
1078
  async function removeKey(provider) {
800
- if (!provider.credentialRef || provider.credentialShared || !status) return;
1079
+ if (!ownsProviderConfiguration(provider) || !provider.credentialRef || provider.credentialShared || !status) return;
801
1080
  unwrap(await client.remote.credentials.unset(provider.credentialRef));
802
1081
  setKeys((current) => ({
803
1082
  ...current,
@@ -886,9 +1165,7 @@ function NetworkSearchSettings({ client }) {
886
1165
  adopt(await call("update", {
887
1166
  settings: {
888
1167
  ...editable(current.settings),
889
- ...parsed,
890
- fetchProvider: "http",
891
- fetchEnabled: current.settings.searchEnabled
1168
+ ...parsed
892
1169
  },
893
1170
  expectedRevision: current.settings.revision
894
1171
  }), true);
@@ -927,6 +1204,22 @@ function NetworkSearchSettings({ client }) {
927
1204
  className: "dsh-ui-error",
928
1205
  children: error
929
1206
  }),
1207
+ loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1208
+ className: "dsh-ui-stack",
1209
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1210
+ role: "alert",
1211
+ className: "dsh-ui-error",
1212
+ children: [text.loadFailed, loadError]
1213
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1214
+ variant: "outline",
1215
+ size: "sm",
1216
+ disabled: busy,
1217
+ onClick: () => void action(async () => {
1218
+ await load();
1219
+ }),
1220
+ children: text.reconnect
1221
+ })]
1222
+ }) : null,
930
1223
  note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
931
1224
  role: "status",
932
1225
  className: "dsh-ui-notice",
@@ -937,13 +1230,13 @@ function NetworkSearchSettings({ client }) {
937
1230
  className: "dsh-ui-banner dsh-ui-banner--danger",
938
1231
  children: text.storageFailed
939
1232
  }),
940
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
941
1234
  role: "tabpanel",
942
1235
  id: tabsId + "-providers-panel",
943
1236
  "aria-labelledby": tabsId + "-providers-tab",
944
1237
  hidden: tab !== "providers",
945
1238
  tabIndex: 0,
946
- children: backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1239
+ children: [backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
947
1240
  className: "dsh-ui-empty",
948
1241
  children: text.empty
949
1242
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
@@ -972,17 +1265,23 @@ function NetworkSearchSettings({ client }) {
972
1265
  children: backends.map((provider) => {
973
1266
  const participating = order.includes(provider.id);
974
1267
  const rank = order.indexOf(provider.id);
975
- const active = saved.searchActive && selected?.id === provider.id;
976
- const info = searchProviderInfo(provider);
977
- const needsKey = Boolean(provider.credentialRef);
1268
+ const dormant = !isRegisteredSearch(saved, provider.id);
1269
+ const active = !dormant && saved.searchActive && selected?.id === provider.id;
1270
+ const info = searchProviderInfo(provider, locale);
1271
+ const owned = ownsProviderConfiguration(provider);
1272
+ const access = configurationAccess(provider);
1273
+ const needsKey = owned && Boolean(provider.credentialRef);
978
1274
  const shared = Boolean(provider.credentialShared);
979
- const showKey = needsKey && participating && (!shared || !provider.configured);
1275
+ const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
980
1276
  const canSort = participating && order.length > 1;
981
1277
  const readOnly = writable[provider.credentialRef ?? ""] === false;
982
1278
  const typedKey = (keys[provider.id] ?? "").trim();
1279
+ const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
983
1280
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
984
1281
  className: active ? "is-active" : void 0,
985
1282
  "data-testid": `web-search-rank-${provider.id}`,
1283
+ "data-availability": availability,
1284
+ "data-in-use": active || void 0,
986
1285
  "data-dragging": dragSource === provider.id || void 0,
987
1286
  "data-drop": dropTarget === provider.id && dragSource ? order.indexOf(dragSource) < rank ? "after" : "before" : void 0,
988
1287
  onDragOver: (event) => {
@@ -1084,11 +1383,15 @@ function NetworkSearchSettings({ client }) {
1084
1383
  className: "dsh-ui-heading",
1085
1384
  children: provider.label
1086
1385
  }),
1386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1387
+ className: "ws-provider-note",
1388
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dormant ? "idle" : provider.configured ? "done" : "warning" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: dormant ? text.dormant : provider.configured ? text.available : text.unavailable })]
1389
+ }),
1087
1390
  active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1088
1391
  tone: "success",
1089
1392
  children: text.inUse
1090
1393
  }),
1091
- provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
1394
+ provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1092
1395
  url: provider.signupUrl,
1093
1396
  label: provider.label + " " + text.signup,
1094
1397
  children: text.signup
@@ -1097,20 +1400,30 @@ function NetworkSearchSettings({ client }) {
1097
1400
  }),
1098
1401
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1099
1402
  className: "web-search-provider-info",
1100
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [info.pricing, info.pricingUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
1403
+ children: [info.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [info.pricing, info.pricingUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1101
1404
  url: info.pricingUrl,
1102
1405
  label: provider.label + " " + text.pricing,
1103
1406
  children: provider.id === "ddg" ? text.website : text.pricing
1104
1407
  })] })] })]
1105
1408
  }),
1106
- participating && !provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1107
- className: "ws-provider-note",
1108
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: needsKey ? text.needsKey : text.unavailable })]
1409
+ access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1410
+ className: "dsh-ui-hint",
1411
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1412
+ url: access.url,
1413
+ label: text.openConfig,
1414
+ children: text.openConfig
1415
+ })
1416
+ }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1417
+ className: "dsh-ui-hint",
1418
+ children: [text.ownerSettings, access.name]
1419
+ }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1420
+ className: "dsh-ui-hint",
1421
+ children: text.unknownOwner
1109
1422
  }) : null,
1110
- shared && participating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1423
+ shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1111
1424
  className: "dsh-ui-meta",
1112
1425
  children: provider.credentialHint ?? text.sharedKey
1113
- }),
1426
+ }) : null,
1114
1427
  showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1115
1428
  className: "dsh-ui-field",
1116
1429
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1159,7 +1472,7 @@ function NetworkSearchSettings({ client }) {
1159
1472
  disabled: busy,
1160
1473
  onChange: (next) => {
1161
1474
  const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1162
- runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following);
1475
+ runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following, droppedDormantIds(saved.settings, following, saved.providers));
1163
1476
  }
1164
1477
  })
1165
1478
  })
@@ -1167,10 +1480,10 @@ function NetworkSearchSettings({ client }) {
1167
1480
  }, provider.id);
1168
1481
  })
1169
1482
  }),
1170
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1483
+ backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1171
1484
  className: "dsh-ui-hint",
1172
1485
  children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1173
- }),
1486
+ }) : null,
1174
1487
  on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1175
1488
  className: "dsh-ui-actions",
1176
1489
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
@@ -1187,7 +1500,104 @@ function NetworkSearchSettings({ client }) {
1187
1500
  })
1188
1501
  }) : null
1189
1502
  ]
1190
- })
1503
+ }), (saved.providers.some((provider) => provider.kind === "fetch") || saved.settings.fetchProvider) && (() => {
1504
+ const fetchId = saved.settings.fetchProvider;
1505
+ const fetchSelected = saved.providers.find((provider) => provider.kind === "fetch" && provider.id === fetchId);
1506
+ const fetchDormant = Boolean(fetchId) && !fetchSelected;
1507
+ const fetchActive = Boolean(fetchSelected && saved.fetchActive && fetchSelected.configured);
1508
+ const fetchAccess = fetchSelected ? configurationAccess(fetchSelected) : fetchDormant ? { kind: "unknown" } : void 0;
1509
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1510
+ className: "dsh-ui-card dsh-ui-card--flat",
1511
+ "data-testid": "web-search-fetch",
1512
+ "data-availability": fetchDormant ? "dormant" : fetchSelected?.configured ? "available" : "unavailable",
1513
+ "data-in-use": fetchActive || void 0,
1514
+ children: [
1515
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1516
+ className: "ws-card-head",
1517
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1518
+ className: "dsh-ui-title",
1519
+ children: text.fetchTitle
1520
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1521
+ className: "ws-provider-note",
1522
+ children: [
1523
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: fetchDormant ? "idle" : fetchSelected?.configured ? "done" : "warning" }),
1524
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fetchDormant ? text.dormant : fetchSelected?.configured ? text.available : text.unavailable }),
1525
+ fetchActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1526
+ tone: "success",
1527
+ children: text.inUse
1528
+ }) : null
1529
+ ]
1530
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1531
+ checked: saved.settings.fetchEnabled,
1532
+ label: text.fetchTitle,
1533
+ disabled: busy || reordering,
1534
+ onChange: (next) => void action(async () => {
1535
+ const current = statusRef.current;
1536
+ adopt(await call("update", {
1537
+ settings: {
1538
+ ...editable(current.settings),
1539
+ fetchEnabled: next
1540
+ },
1541
+ expectedRevision: current.settings.revision
1542
+ }));
1543
+ await load();
1544
+ })
1545
+ })]
1546
+ }),
1547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1548
+ className: "dsh-ui-field",
1549
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1550
+ className: "dsh-ui-label",
1551
+ children: text.fetchProvider
1552
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1553
+ className: "dsh-ui-select",
1554
+ "aria-label": text.fetchProvider,
1555
+ value: fetchId,
1556
+ disabled: busy || reordering,
1557
+ onChange: (event) => {
1558
+ const id = event.target.value;
1559
+ action(async () => {
1560
+ const current = statusRef.current;
1561
+ adopt(await call("update", {
1562
+ settings: {
1563
+ ...editable(current.settings),
1564
+ fetchProvider: id
1565
+ },
1566
+ expectedRevision: current.settings.revision
1567
+ }));
1568
+ await load();
1569
+ });
1570
+ },
1571
+ children: [fetchDormant && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1572
+ value: fetchId,
1573
+ children: [
1574
+ fetchId,
1575
+ " · ",
1576
+ text.dormant
1577
+ ]
1578
+ }), saved.providers.filter((provider) => provider.kind === "fetch").map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1579
+ value: provider.id,
1580
+ children: [provider.label, provider.configured ? "" : " · " + text.unavailable]
1581
+ }, provider.id))]
1582
+ })]
1583
+ }),
1584
+ fetchAccess?.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1585
+ className: "dsh-ui-hint",
1586
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1587
+ url: fetchAccess.url,
1588
+ label: text.openConfig,
1589
+ children: text.openConfig
1590
+ })
1591
+ }) : fetchAccess?.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1592
+ className: "dsh-ui-hint",
1593
+ children: [text.ownerSettings, fetchAccess.name]
1594
+ }) : fetchAccess?.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1595
+ className: "dsh-ui-hint",
1596
+ children: text.unknownOwner
1597
+ }) : null
1598
+ ]
1599
+ });
1600
+ })()]
1191
1601
  }),
1192
1602
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1193
1603
  role: "tabpanel",
@@ -1329,20 +1739,34 @@ function apply(ctx) {
1329
1739
  ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1330
1740
  name: "plugins.bundle.config",
1331
1741
  key: "@klarkxy/dsh-web-search-manager"
1332
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NetworkSearchSettings, { client }))), "web-search-manager.settings");
1742
+ }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NetworkSearchSettings, {
1743
+ client,
1744
+ hidden: props?.hidden
1745
+ }))), "web-search-manager.settings");
1333
1746
  }
1334
1747
  //#endregion
1335
1748
  exports.NetworkSearchSettings = NetworkSearchSettings;
1749
+ exports.STATUS_REFRESH_MS = STATUS_REFRESH_MS;
1336
1750
  exports.apply = apply;
1751
+ exports.bindStatusRefresh = bindStatusRefresh;
1337
1752
  exports.canEnableSearch = canEnableSearch;
1753
+ exports.canStartStatusRead = canStartStatusRead;
1338
1754
  exports.catalogSearchBackends = catalogSearchBackends;
1755
+ exports.configurationAccess = configurationAccess;
1756
+ exports.dormantSearchPlaceholder = dormantSearchPlaceholder;
1757
+ exports.droppedDormantIds = droppedDormantIds;
1339
1758
  exports.immediateUpdate = immediateUpdate;
1340
1759
  exports.inject = inject;
1760
+ exports.isExternalConfigurationUrl = isExternalConfigurationUrl;
1761
+ exports.isPageHidden = isPageHidden;
1341
1762
  exports.isProviderOn = isProviderOn;
1763
+ exports.isRegisteredSearch = isRegisteredSearch;
1342
1764
  exports.name = name;
1343
1765
  exports.nextSearchEnabled = nextSearchEnabled;
1766
+ exports.ownsProviderConfiguration = ownsProviderConfiguration;
1344
1767
  exports.parseLimit = parseLimit;
1345
1768
  exports.searchBackends = searchBackends;
1346
1769
  exports.selectedSearchBackend = selectedSearchBackend;
1770
+ exports.shouldApplyPolledStatus = shouldApplyPolledStatus;
1347
1771
 
1348
1772
  //# sourceMappingURL=client.inner.cjs.map