@klarkxy/dsh-web-search-manager 0.2.5 → 0.3.1

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.
package/lib/client.js CHANGED
@@ -225,6 +225,17 @@ ${scope} {
225
225
  /* --- Layout scaffolding. --- */
226
226
  & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
227
227
  & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
228
+ /* Plugin detail pages share one reading column. These are layout recipes;
229
+ * controls keep their own primitive styles. Form spacing follows the host's
230
+ * settings-form/fields.module.css and SettingsForm.module.css. */
231
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
232
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
233
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
234
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
235
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
236
+ @media (max-width: 560px) {
237
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
238
+ }
228
239
  & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
229
240
  & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
230
241
  & .dsh-ui-spacer { flex: 1; min-width: 0; }
@@ -252,6 +263,9 @@ ${scope} {
252
263
  * official primitive, so this only owns the vertical rhythm. --- */
253
264
  & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
254
265
  & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
266
+ /* Grid fields share a row, so the stacked-field divider must not win later in
267
+ * the cascade. Reset it here, after the field recipe. */
268
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
255
269
  & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
256
270
  & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
257
271
  & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
@@ -442,9 +456,87 @@ ${scope} {
442
456
  & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
443
457
  }
444
458
  }
445
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
459
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
446
460
  }
447
461
  //#endregion
462
+ //#region src/client-styles.ts
463
+ /**
464
+ * The shared contract paints this page: the type tiers, the card, the field,
465
+ * the button row, the notice, the focus ring and the state colours. What is
466
+ * left here is only the geometry the contract does not cover — the reading
467
+ * width of the panel, the ranked three-column row, the drag affordance, the
468
+ * drop indicator, the screen-reader-only announcer and the limits grid.
469
+ *
470
+ * Nothing below declares a colour of its own beyond naming a host token, and
471
+ * nothing carries a literal fallback: the host publishes both themes and a
472
+ * literal would pin one of them.
473
+ *
474
+ * Every plugin rule nests under the plugin root, as the contract does, so a
475
+ * .ws-* or .web-search-* class never paints a neighbouring plugin. Selectors
476
+ * that start with an element name keep an explicit `&`, the form every
477
+ * nesting implementation accepts.
478
+ */
479
+ const webSearchClientStyles = `${officialUiCss("ws-root")}
480
+ .ws-root {
481
+
482
+ .ws-card-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding-bottom: 8px; }
483
+ .ws-sr-only {
484
+ position: absolute;
485
+ width: 1px;
486
+ height: 1px;
487
+ padding: 0;
488
+ margin: -1px;
489
+ overflow: hidden;
490
+ clip-path: inset(50%);
491
+ white-space: nowrap;
492
+ border: 0;
493
+ }
494
+ .ws-provider-note { display: flex; align-items: center; gap: 6px; }
495
+ .web-search-provider { display: grid; gap: 8px; min-width: 0; }
496
+ .web-search-provider-info { display: grid; gap: 3px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
497
+ .web-search-provider-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; min-height: 24px; }
498
+ & a { color: var(--dsw-alias-state-business-primary); font-size: 12px; line-height: 18px; text-underline-offset: 3px; }
499
+ /* The primitive renders a 32px control in an inline-flex wrapper; a settings
500
+ * field needs it to fill the column it sits in. */
501
+ .ws-input { width: 100%; }
502
+ .web-search-rank { margin: 0; padding: 0; list-style: none; }
503
+ .web-search-rank li {
504
+ position: relative;
505
+ display: grid;
506
+ grid-template-columns: 24px minmax(0, 1fr) auto;
507
+ gap: 10px;
508
+ align-items: start;
509
+ padding: 14px 0;
510
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
511
+ }
512
+ & button.web-search-drag-handle {
513
+ display: grid;
514
+ place-items: center;
515
+ width: 24px;
516
+ height: 24px;
517
+ min-height: 24px;
518
+ padding: 0;
519
+ cursor: grab;
520
+ opacity: 0.65;
521
+ }
522
+ & button.web-search-drag-handle:hover { opacity: 1; }
523
+ & button.web-search-drag-handle:active { cursor: grabbing; }
524
+ & button.web-search-drag-handle[aria-disabled='true'] { cursor: wait; opacity: 0.4; }
525
+ .web-search-rank li[data-dragging] { opacity: 0.45; }
526
+ .web-search-rank li[data-drop]::after { content: ''; position: absolute; left: 0; right: 0; height: 2px; background: var(--dsw-alias-state-business-primary); pointer-events: none; }
527
+ .web-search-rank li[data-drop='before']::after { top: -1px; }
528
+ .web-search-rank li[data-drop='after']::after { bottom: -1px; }
529
+ .web-search-rank-index { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; line-height: 24px; }
530
+ .web-search-rank-move { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; justify-content: flex-end; padding-top: 2px; }
531
+ .ws-danger:hover:not(:disabled),
532
+ .ws-danger[data-armed] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); }
533
+ .ws-danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger); }
534
+ @media (max-width: 560px) {
535
+ .web-search-rank-move { flex-direction: column; align-items: flex-end; }
536
+ }
537
+ }
538
+ `;
539
+ //#endregion
448
540
  //#region ../dsh-plugin-kit/src/client-utils.ts
449
541
  /** Fixed pause for failing background reads; manual work is never replayed. */
450
542
  function createPluginReadGate(now = Date.now) {
@@ -473,8 +565,8 @@ function createPluginReadGate(now = Date.now) {
473
565
  //#endregion
474
566
  //#region src/contracts.ts
475
567
  const WEB_SEARCH_RPC_CHANNEL = "/web-search-manager";
476
- function pickActiveSearch(order, configured) {
477
- return order.find(configured) ?? "";
568
+ function providerKey(kind, id) {
569
+ return `${kind}:${id}`;
478
570
  }
479
571
  function safeConfigurationUrl(value) {
480
572
  if (typeof value !== "string" || !value || /[\u0000-\u0020\\]/.test(value)) return;
@@ -491,6 +583,21 @@ function safeConfigurationUrl(value) {
491
583
  //#region src/provider-info.ts
492
584
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
493
585
  const providerInfo = {
586
+ perplexity: {
587
+ description: "通过 Perplexity Search API 获取网页来源,不生成回答。",
588
+ pricing: "按 Search API 请求计费;实际价格与额度以官方账户为准。",
589
+ pricingUrl: "https://docs.perplexity.ai/docs/getting-started/pricing"
590
+ },
591
+ you: {
592
+ description: "通过 You.com 获取网页与新闻结果,不请求额外全文提取。",
593
+ pricing: "按 Web Search API 请求计费;实际价格与额度以官方账户为准。",
594
+ pricingUrl: "https://you.com/docs/administration/billing"
595
+ },
596
+ "firecrawl-fetch": {
597
+ description: "读取网页 Markdown;与 Firecrawl 搜索共用 Key。",
598
+ pricing: "网页读取使用 Firecrawl 积分,与搜索分别计费;实际消耗以官方账户为准。",
599
+ pricingUrl: "https://www.firecrawl.dev/pricing"
600
+ },
494
601
  ddg: {
495
602
  description: "搜索 DuckDuckGo 公开网页,无需注册或 API Key。",
496
603
  pricing: "免费使用,不产生搜索 API 费用。",
@@ -533,6 +640,18 @@ const providerInfo = {
533
640
  }
534
641
  };
535
642
  const englishInfo = {
643
+ perplexity: {
644
+ description: "Get web sources through the Perplexity Search API without generating an answer.",
645
+ pricing: "Charged per Search API request; current prices and credits depend on your official account."
646
+ },
647
+ you: {
648
+ description: "Get web and news results through You.com without requesting extra full-page extraction.",
649
+ pricing: "Charged per Web Search API request; current prices and credits depend on your official account."
650
+ },
651
+ "firecrawl-fetch": {
652
+ description: "Read page Markdown using the same key as Firecrawl search.",
653
+ pricing: "Page reads use Firecrawl credits and are charged separately from search; your official account determines usage."
654
+ },
536
655
  ddg: {
537
656
  description: "Search public DuckDuckGo pages without registration or an API key.",
538
657
  pricing: "Free to use; no search API fees."
@@ -567,6 +686,9 @@ const englishInfo = {
567
686
  }
568
687
  };
569
688
  const bundledDescriptions = {
689
+ perplexity: ["Search API;返回网页来源,不生成回答。", "Search API returning web sources without generating an answer."],
690
+ you: ["网页与新闻搜索;不额外提取全文。", "Web and news search without extra full-page extraction."],
691
+ "firecrawl-fetch": ["读取网页 Markdown;与 Firecrawl 搜索共用 Key。", "Read page Markdown using the same key as Firecrawl search."],
570
692
  ddg: ["无需注册。默认后端。", "No registration required. Default backend."],
571
693
  "deepseek-official": ["使用已配置的 DeepSeek API Key,可能产生额外搜索费用。", "Uses the configured DeepSeek API key; additional search fees may apply."],
572
694
  exa: ["独立 Search API。", "Independent Search API."],
@@ -586,6 +708,7 @@ function providerDisplayLabel(provider, locale) {
586
708
  }
587
709
  function bundledCatalogId(provider) {
588
710
  if (!(typeof provider.configurationOwned === "boolean" ? provider.configurationOwned : !provider.configurationOwner || provider.configurationOwner === "@klarkxy/dsh-web-search-manager")) return;
711
+ if (provider.kind === "fetch" && provider.id === "firecrawl") return "firecrawl-fetch";
589
712
  return provider.id === "deepseek-managed" ? "deepseek-official" : provider.id;
590
713
  }
591
714
  function missingPricing(locale) {
@@ -594,7 +717,11 @@ function missingPricing(locale) {
594
717
  function usesBundledPricing(provider) {
595
718
  if (provider.pricing?.trim()) return false;
596
719
  const catalogId = bundledCatalogId(provider);
597
- return Boolean(catalogId && Object.hasOwn(providerInfo, catalogId));
720
+ return Boolean(catalogId && ![
721
+ "perplexity",
722
+ "you",
723
+ "firecrawl-fetch"
724
+ ].includes(catalogId) && Object.hasOwn(providerInfo, catalogId));
598
725
  }
599
726
  function searchProviderInfo(provider, locale = "zh") {
600
727
  const catalogId = bundledCatalogId(provider);
@@ -691,52 +818,61 @@ function bindStatusRefresh(options) {
691
818
  };
692
819
  }
693
820
  function isProviderOn(status, provider) {
694
- if (provider.kind === "fetch") return status.fetchActive && status.settings.fetchProvider === provider.id && provider.configured;
821
+ if (provider.kind === "fetch") return status.fetchActive && selectedBackend(status, "fetch")?.id === provider.id;
695
822
  return status.searchActive && selectedSearchBackend(status)?.id === provider.id;
696
823
  }
697
824
  function dormantSearchPlaceholder(id) {
825
+ return dormantPlaceholder(id, "search");
826
+ }
827
+ function dormantPlaceholder(id, kind) {
698
828
  return {
699
829
  id,
700
830
  label: id,
701
831
  description: "",
702
832
  billing: "unknown",
703
- kind: "search",
833
+ kind,
704
834
  configured: false,
705
835
  calls: 0,
706
836
  failures: 0,
707
837
  configurationOwned: false
708
838
  };
709
839
  }
840
+ function isRegistered(status, id, kind) {
841
+ return status.providers.some((provider) => provider.kind === kind && provider.id === id);
842
+ }
710
843
  function isRegisteredSearch(status, id) {
711
- return status.providers.some((provider) => provider.kind === "search" && provider.id === id);
844
+ return isRegistered(status, id, "search");
712
845
  }
713
- function catalogSearchBackends(status) {
714
- const byId = new Map(status.providers.filter((provider) => provider.kind === "search").map((provider) => [provider.id, provider]));
846
+ function providerOrder(settings, kind) {
847
+ return kind === "search" ? settings.searchOrder : settings.fetchOrder;
848
+ }
849
+ function catalogBackends(status, kind) {
850
+ const byId = new Map(status.providers.filter((provider) => provider.kind === kind).map((provider) => [provider.id, provider]));
715
851
  const rows = [];
716
852
  const seen = /* @__PURE__ */ new Set();
717
- for (const id of status.settings.searchOrder) {
853
+ for (const id of providerOrder(status.settings, kind)) {
718
854
  if (!id || seen.has(id)) continue;
719
855
  seen.add(id);
720
- rows.push(byId.get(id) ?? dormantSearchPlaceholder(id));
721
- }
722
- for (const provider of status.providers) {
723
- if (provider.kind !== "search" || seen.has(provider.id)) continue;
724
- rows.push(provider);
856
+ rows.push(byId.get(id) ?? dormantPlaceholder(id, kind));
725
857
  }
858
+ for (const provider of byId.values()) if (!seen.has(provider.id)) rows.push(provider);
726
859
  return rows;
727
860
  }
861
+ function catalogSearchBackends(status) {
862
+ return catalogBackends(status, "search");
863
+ }
728
864
  function searchBackends(status) {
729
- const participating = new Set(status.settings.searchOrder);
730
- return catalogSearchBackends(status).filter((provider) => participating.has(provider.id));
865
+ return catalogSearchBackends(status).filter((provider) => status.settings.searchOrder.includes(provider.id));
866
+ }
867
+ function selectedBackend(status, kind) {
868
+ return providerOrder(status.settings, kind).flatMap((id) => status.providers.find((provider) => provider.kind === kind && provider.id === id) ?? []).find((provider) => provider.configured);
731
869
  }
732
870
  function selectedSearchBackend(status) {
733
- const backends = searchBackends(status).filter((provider) => isRegisteredSearch(status, provider.id));
734
- const id = pickActiveSearch(backends.map((provider) => provider.id), (id) => Boolean(backends.find((provider) => provider.id === id)?.configured));
735
- return backends.find((provider) => provider.id === id);
871
+ return selectedBackend(status, "search");
736
872
  }
737
- function droppedDormantIds(settings, order, providers) {
738
- const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
739
- return [...new Set(settings.searchOrder.filter((id) => id && !known.has(id) && !order.includes(id)))];
873
+ function droppedDormantIds(settings, order, providers, kind = "search") {
874
+ const known = new Set(providers.filter((provider) => provider.kind === kind).map((provider) => provider.id));
875
+ return [...new Set(providerOrder(settings, kind).filter((id) => id && !known.has(id) && !order.includes(id)))];
740
876
  }
741
877
  function configurationAccess(provider) {
742
878
  if (ownsProviderConfiguration(provider)) return { kind: "owned" };
@@ -817,8 +953,9 @@ function ConfirmButton(props) {
817
953
  const copy = {
818
954
  zh: {
819
955
  tabsLabel: "网络搜索设置",
820
- tabProviders: "搜索服务",
821
- tabLimits: "请求限制",
956
+ tabSearch: "web-search",
957
+ tabFetch: "web-fetch",
958
+ tabLimits: "设置",
822
959
  loading: "正在读取设置…",
823
960
  loadFailed: "读取设置失败:",
824
961
  reconnect: "重新连接",
@@ -859,13 +996,16 @@ const copy = {
859
996
  maxFetchChars: "正文字符上限",
860
997
  fetchTitle: "网页读取",
861
998
  fetchProvider: "读取服务",
999
+ fetchBackends: "读取后端",
1000
+ needFetchBackend: "需选择并配置读取服务。",
862
1001
  ownerSettings: "配置入口:",
863
1002
  unknownOwner: "请在服务所属插件中配置"
864
1003
  },
865
1004
  en: {
866
1005
  tabsLabel: "Web search settings",
867
- tabProviders: "Search services",
868
- tabLimits: "Request limits",
1006
+ tabSearch: "web-search",
1007
+ tabFetch: "web-fetch",
1008
+ tabLimits: "Settings",
869
1009
  loading: "Loading settings…",
870
1010
  loadFailed: "Could not load settings: ",
871
1011
  reconnect: "Reconnect",
@@ -906,6 +1046,8 @@ const copy = {
906
1046
  maxFetchChars: "Max text chars",
907
1047
  fetchTitle: "Read web pages",
908
1048
  fetchProvider: "Fetch service",
1049
+ fetchBackends: "Fetch backends",
1050
+ needFetchBackend: "Choose and configure a fetch service.",
909
1051
  ownerSettings: "Configure in: ",
910
1052
  unknownOwner: "Configure this service in its plugin"
911
1053
  }
@@ -922,7 +1064,7 @@ function uiMessage(message, locale, providers = []) {
922
1064
  if ("key" in message) return copy[locale][message.key];
923
1065
  if ("sources" in message) return testOkText(locale, message.sources);
924
1066
  if ("moved" in message) {
925
- const provider = providers.find((item) => item.id === message.moved);
1067
+ const provider = providers.find((item) => item.id === message.moved && item.kind === message.kind);
926
1068
  return movedText(locale, provider ? providerDisplayLabel(provider, locale) : message.moved, message.position);
927
1069
  }
928
1070
  return message.invalid.map((key) => {
@@ -975,15 +1117,15 @@ function parseLimit(raw, min, max) {
975
1117
  * Settings written by an immediate action (a switch or a reorder). It keeps the
976
1118
  * saved limits and never includes typed-but-unsaved keys or limit drafts.
977
1119
  */
978
- function immediateUpdate(settings, on, order, providers, remove = []) {
1120
+ function immediateUpdate(settings, on, order, providers, remove = [], kind = "search") {
979
1121
  let searchOrder = order;
980
1122
  if (providers) {
981
- const known = new Set(providers.filter((provider) => provider.kind === "search").map((provider) => provider.id));
1123
+ const known = new Set(providers.filter((provider) => provider.kind === kind).map((provider) => provider.id));
982
1124
  const removed = new Set(remove);
983
1125
  const following = order.filter((id) => !removed.has(id));
984
1126
  if (following.some((id) => !known.has(id))) searchOrder = following;
985
1127
  else {
986
- searchOrder = settings.searchOrder.flatMap((id) => {
1128
+ searchOrder = providerOrder(settings, kind).flatMap((id) => {
987
1129
  if (removed.has(id)) return [];
988
1130
  return known.has(id) ? following.splice(0, 1) : [id];
989
1131
  });
@@ -992,8 +1134,13 @@ function immediateUpdate(settings, on, order, providers, remove = []) {
992
1134
  }
993
1135
  return {
994
1136
  ...editable(settings),
995
- searchOrder,
996
- searchEnabled: on
1137
+ ...kind === "search" ? {
1138
+ searchOrder,
1139
+ searchEnabled: on
1140
+ } : {
1141
+ fetchOrder: searchOrder,
1142
+ fetchEnabled: on
1143
+ }
997
1144
  };
998
1145
  }
999
1146
  function limitTexts(settings) {
@@ -1015,7 +1162,7 @@ function NetworkSearchSettings({ client, hidden }) {
1015
1162
  const locale = useHostLocale(client);
1016
1163
  const text = copy[locale];
1017
1164
  const tabsId = (0, react.useId)();
1018
- const [tab, setTab] = (0, react.useState)("providers");
1165
+ const [tab, setTab] = (0, react.useState)("search");
1019
1166
  const [status, setStatus] = (0, react.useState)();
1020
1167
  const [limitDraft, setLimitDraft] = (0, react.useState)();
1021
1168
  const [invalid, setInvalid] = (0, react.useState)({});
@@ -1032,6 +1179,7 @@ function NetworkSearchSettings({ client, hidden }) {
1032
1179
  const statusRef = (0, react.useRef)(status);
1033
1180
  statusRef.current = status;
1034
1181
  const pendingRef = (0, react.useRef)(null);
1182
+ const dragRef = (0, react.useRef)(null);
1035
1183
  const timerRef = (0, react.useRef)(void 0);
1036
1184
  const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
1037
1185
  const refocusRef = (0, react.useRef)(null);
@@ -1067,7 +1215,7 @@ function NetworkSearchSettings({ client, hidden }) {
1067
1215
  mounted: mountedRef.current,
1068
1216
  hidden: isPageHidden(hiddenRef.current),
1069
1217
  busy: busyRef.current,
1070
- reordering: pendingRef.current !== null,
1218
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1071
1219
  reading: readingRef.current
1072
1220
  });
1073
1221
  }
@@ -1104,7 +1252,7 @@ function NetworkSearchSettings({ client, hidden }) {
1104
1252
  requestId: mine,
1105
1253
  latestRequestId: requestSeq.current,
1106
1254
  busy: busyRef.current,
1107
- reordering: pendingRef.current !== null,
1255
+ reordering: pendingRef.current !== null || dragRef.current !== null,
1108
1256
  hidden: isPageHidden(hiddenRef.current),
1109
1257
  currentRevision: statusRef.current?.settings.revision,
1110
1258
  incomingRevision: next.settings.revision
@@ -1154,7 +1302,7 @@ function NetworkSearchSettings({ client, hidden }) {
1154
1302
  const current = statusRef.current;
1155
1303
  const next = pendingRef.current;
1156
1304
  if (current && next) call("update", {
1157
- settings: immediateUpdate(current.settings, current.settings.searchEnabled, next, current.providers),
1305
+ settings: immediateUpdate(current.settings, next.kind === "search" ? current.settings.searchEnabled : current.settings.fetchEnabled, next.order, current.providers, [], next.kind),
1158
1306
  expectedRevision: current.settings.revision
1159
1307
  }).catch(() => {});
1160
1308
  };
@@ -1172,6 +1320,7 @@ function NetworkSearchSettings({ client, hidden }) {
1172
1320
  if (handle && document.activeElement !== handle) handle.focus();
1173
1321
  });
1174
1322
  async function action(run) {
1323
+ if (busyRef.current) return;
1175
1324
  readGate.reset();
1176
1325
  requestSeq.current += 1;
1177
1326
  busyRef.current = true;
@@ -1189,18 +1338,17 @@ function NetworkSearchSettings({ client, hidden }) {
1189
1338
  }
1190
1339
  }
1191
1340
  /** Switches and reorders: settings only, applied at once. */
1192
- function runImmediate(on, order, remove = []) {
1341
+ function runImmediate(on, order, remove = [], kind = "search") {
1193
1342
  clearTimeout(timerRef.current);
1194
1343
  timerRef.current = void 0;
1195
1344
  pendingRef.current = null;
1196
1345
  action(async () => {
1197
1346
  const current = statusRef.current;
1198
1347
  if (!current) return;
1199
- const next = await call("update", {
1200
- settings: immediateUpdate(current.settings, on, order, current.providers, remove),
1348
+ adopt(await call("update", {
1349
+ settings: immediateUpdate(current.settings, on, order, current.providers, remove, kind),
1201
1350
  expectedRevision: current.settings.revision
1202
- });
1203
- setStatus(next);
1351
+ }));
1204
1352
  await load();
1205
1353
  }).then(() => {
1206
1354
  if (pendingRef.current === null) setPendingOrder(null);
@@ -1210,30 +1358,33 @@ function NetworkSearchSettings({ client, hidden }) {
1210
1358
  async function saveKeys(only) {
1211
1359
  if (!status) return;
1212
1360
  for (const provider of status.providers) {
1213
- if (provider.kind !== "search" || only && provider.id !== only.id) continue;
1214
- const snapshot = keys[provider.id] ?? "";
1361
+ if (only && providerKey(provider.kind, provider.id) !== providerKey(only.kind, only.id)) continue;
1362
+ const snapshot = keys[providerKey(provider.kind, provider.id)] ?? "";
1215
1363
  const typed = snapshot.trim();
1216
1364
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || !typed) continue;
1217
1365
  if (writable[provider.credentialRef] === false) throw new UiError("readOnly");
1218
1366
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
1219
- setKeys((current) => current[provider.id] === snapshot ? {
1367
+ setKeys((current) => current[providerKey(provider.kind, provider.id)] === snapshot ? {
1220
1368
  ...current,
1221
- [provider.id]: ""
1369
+ [providerKey(provider.kind, provider.id)]: ""
1222
1370
  } : current);
1223
1371
  }
1224
1372
  }
1373
+ async function saveKeysForKind(kind) {
1374
+ for (const provider of statusRef.current?.providers ?? []) if (provider.kind === kind) await saveKeys(provider);
1375
+ }
1225
1376
  async function removeKey(provider) {
1226
1377
  if (!ownsProviderConfiguration(provider) || !provider.credentialRef || provider.credentialShared || !status) return;
1227
1378
  unwrap(await client.remote.credentials.unset(provider.credentialRef));
1228
1379
  setKeys((current) => ({
1229
1380
  ...current,
1230
- [provider.id]: ""
1381
+ [providerKey(provider.kind, provider.id)]: ""
1231
1382
  }));
1232
1383
  await load();
1233
1384
  setNote({ key: "keyDeleted" });
1234
1385
  }
1235
1386
  if (!status || !limitDraft) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1236
- className: "web-search-settings ws-root dsh-ui-panel",
1387
+ className: "web-search-settings ws-root dsh-ui-page",
1237
1388
  children: [loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1238
1389
  role: "alert",
1239
1390
  className: "dsh-ui-error",
@@ -1256,28 +1407,30 @@ function NetworkSearchSettings({ client, hidden }) {
1256
1407
  }) : null]
1257
1408
  });
1258
1409
  const saved = status;
1259
- const savedOrder = searchBackends(saved).map((provider) => provider.id);
1260
- const order = pendingOrder ?? savedOrder;
1261
- const catalog = catalogSearchBackends(saved);
1262
- const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1263
- const selected = selectedSearchBackend(saved);
1264
- const on = saved.settings.searchEnabled;
1265
- const canEnable = canEnableSearch(order, saved.providers, {}, writable);
1266
1410
  const savedLimits = limitTexts(saved.settings);
1267
1411
  const limitsDirty = LIMITS.some(([key]) => limitDraft[key] !== savedLimits[key]);
1268
1412
  const keysDirty = Object.values(keys).some((value) => value.trim());
1269
- const reordering = pendingOrder !== null;
1270
- function move(id, nextIndex, keyboard = false) {
1413
+ const reordering = pendingOrder !== null || dragSource !== null;
1414
+ function move(kind, id, nextIndex, keyboard = false) {
1415
+ if (pendingRef.current && pendingRef.current.kind !== kind) return;
1416
+ const order = pendingRef.current?.order ?? providerOrder(statusRef.current.settings, kind);
1271
1417
  const index = order.indexOf(id);
1272
1418
  if (busy || index < 0 || nextIndex < 0 || nextIndex >= order.length || index === nextIndex) return;
1273
1419
  const next = [...order];
1274
1420
  const [item] = next.splice(index, 1);
1275
1421
  next.splice(nextIndex, 0, item);
1276
- pendingRef.current = next;
1277
- setPendingOrder(next);
1278
- if (keyboard) refocusRef.current = id;
1422
+ pendingRef.current = {
1423
+ kind,
1424
+ order: next
1425
+ };
1426
+ setPendingOrder({
1427
+ kind,
1428
+ order: next
1429
+ });
1430
+ if (keyboard) refocusRef.current = providerKey(kind, id);
1279
1431
  setSortAnnouncement({
1280
1432
  moved: id,
1433
+ kind,
1281
1434
  position: nextIndex + 1
1282
1435
  });
1283
1436
  clearTimeout(timerRef.current);
@@ -1285,10 +1438,11 @@ function NetworkSearchSettings({ client, hidden }) {
1285
1438
  timerRef.current = void 0;
1286
1439
  const batched = pendingRef.current;
1287
1440
  const current = statusRef.current;
1288
- if (batched && current) runImmediate(current.settings.searchEnabled, batched);
1441
+ if (batched && current) runImmediate(batched.kind === "search" ? current.settings.searchEnabled : current.settings.fetchEnabled, batched.order, [], batched.kind);
1289
1442
  }, keyboard ? REORDER_DEBOUNCE_MS : 0);
1290
1443
  }
1291
1444
  function endDrag() {
1445
+ dragRef.current = null;
1292
1446
  setDragSource(null);
1293
1447
  setDropTarget(null);
1294
1448
  }
@@ -1324,24 +1478,33 @@ function NetworkSearchSettings({ client, hidden }) {
1324
1478
  });
1325
1479
  }
1326
1480
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1327
- className: "web-search-settings ws-root dsh-ui-panel",
1481
+ className: "web-search-settings ws-root dsh-ui-page",
1328
1482
  "data-testid": "web-search-settings",
1329
1483
  children: [
1330
1484
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
1331
1485
  value: tab,
1332
1486
  onChange: setTab,
1333
1487
  label: text.tabsLabel,
1334
- items: [{
1335
- value: "providers",
1336
- label: text.tabProviders,
1337
- id: tabsId + "-providers-tab",
1338
- panelId: tabsId + "-providers-panel"
1339
- }, {
1340
- value: "limits",
1341
- label: text.tabLimits,
1342
- id: tabsId + "-limits-tab",
1343
- panelId: tabsId + "-limits-panel"
1344
- }]
1488
+ items: [
1489
+ {
1490
+ value: "search",
1491
+ label: text.tabSearch,
1492
+ id: tabsId + "-search-tab",
1493
+ panelId: tabsId + "-search-panel"
1494
+ },
1495
+ {
1496
+ value: "fetch",
1497
+ label: text.tabFetch,
1498
+ id: tabsId + "-fetch-tab",
1499
+ panelId: tabsId + "-fetch-panel"
1500
+ },
1501
+ {
1502
+ value: "limits",
1503
+ label: text.tabLimits,
1504
+ id: tabsId + "-limits-tab",
1505
+ panelId: tabsId + "-limits-panel"
1506
+ }
1507
+ ]
1345
1508
  }),
1346
1509
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1347
1510
  className: "ws-sr-only",
@@ -1380,374 +1543,294 @@ function NetworkSearchSettings({ client, hidden }) {
1380
1543
  className: "dsh-ui-banner dsh-ui-banner--danger",
1381
1544
  children: text.storageFailed
1382
1545
  }),
1383
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1384
- role: "tabpanel",
1385
- id: tabsId + "-providers-panel",
1386
- "aria-labelledby": tabsId + "-providers-tab",
1387
- hidden: tab !== "providers",
1388
- tabIndex: 0,
1389
- children: [backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1390
- className: "dsh-ui-empty",
1391
- children: text.empty
1392
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1393
- className: "web-search-card dsh-ui-card dsh-ui-card--flat",
1394
- "data-testid": "web-search-tool",
1395
- "data-on": on ? "true" : "false",
1396
- children: [
1397
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1398
- className: "ws-card-head",
1399
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1400
- className: "dsh-ui-title",
1401
- children: text.toolTitle
1402
- }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1403
- className: "dsh-ui-hint",
1404
- children: text.needBackend
1405
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1406
- checked: on,
1407
- label: text.toolTitle,
1408
- disabled: busy || !on && !canEnable,
1409
- onChange: (next) => runImmediate(next, order)
1410
- })]
1411
- }),
1412
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1413
- className: "web-search-rank",
1414
- "aria-label": text.backends,
1415
- children: backends.map((provider) => {
1416
- const participating = order.includes(provider.id);
1417
- const rank = order.indexOf(provider.id);
1418
- const dormant = !isRegisteredSearch(saved, provider.id);
1419
- const active = !dormant && saved.searchActive && selected?.id === provider.id;
1420
- const info = searchProviderInfo(provider, locale);
1421
- const owned = ownsProviderConfiguration(provider);
1422
- const access = configurationAccess(provider);
1423
- const needsKey = owned && Boolean(provider.credentialRef);
1424
- const shared = Boolean(provider.credentialShared);
1425
- const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1426
- const canSort = participating && order.length > 1;
1427
- const readOnly = writable[provider.credentialRef ?? ""] === false;
1428
- const typedKey = (keys[provider.id] ?? "").trim();
1429
- const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1430
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1431
- className: active ? "is-active" : void 0,
1432
- "data-testid": `web-search-rank-${provider.id}`,
1433
- "data-availability": availability,
1434
- "data-in-use": active || void 0,
1435
- "data-dragging": dragSource === provider.id || void 0,
1436
- "data-drop": dropTarget === provider.id && dragSource ? order.indexOf(dragSource) < rank ? "after" : "before" : void 0,
1437
- onDragOver: (event) => {
1438
- if (busy || !canSort || !dragSource || dragSource === provider.id) return;
1439
- event.preventDefault();
1440
- event.dataTransfer.dropEffect = "move";
1441
- setDropTarget(provider.id);
1442
- },
1443
- onDragLeave: (event) => {
1444
- if (!event.currentTarget.contains(event.relatedTarget)) setDropTarget(null);
1445
- },
1446
- onDrop: (event) => {
1447
- if (busy || !canSort || !dragSource) return;
1448
- event.preventDefault();
1449
- move(dragSource, rank);
1450
- endDrag();
1451
- },
1452
- children: [
1453
- canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1454
- variant: "ghost",
1455
- size: "sm",
1456
- className: "web-search-drag-handle",
1457
- ref: (element) => {
1458
- if (element) handleRefs.current.set(provider.id, element);
1459
- else handleRefs.current.delete(provider.id);
1460
- },
1461
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1462
- width: "16",
1463
- height: "20",
1464
- viewBox: "0 0 16 20",
1465
- fill: "currentColor",
1466
- "aria-hidden": "true",
1467
- children: [
1468
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1469
- cx: "5",
1470
- cy: "5",
1471
- r: "1.5"
1472
- }),
1473
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1474
- cx: "11",
1475
- cy: "5",
1476
- r: "1.5"
1477
- }),
1478
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1479
- cx: "5",
1480
- cy: "10",
1481
- r: "1.5"
1482
- }),
1483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1484
- cx: "11",
1485
- cy: "10",
1486
- r: "1.5"
1487
- }),
1488
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1489
- cx: "5",
1490
- cy: "15",
1491
- r: "1.5"
1492
- }),
1493
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1494
- cx: "11",
1495
- cy: "15",
1496
- r: "1.5"
1497
- })
1498
- ]
1499
- }),
1500
- draggable: !busy,
1501
- "aria-disabled": busy,
1502
- "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1503
- title: text.dragHint,
1504
- onDragStart: (event) => {
1505
- if (busy) {
1506
- event.preventDefault();
1507
- return;
1508
- }
1509
- event.dataTransfer.effectAllowed = "move";
1510
- event.dataTransfer.setData("application/x-dsh-search-provider", provider.id);
1511
- const row = event.currentTarget.closest("li");
1512
- if (row) event.dataTransfer.setDragImage(row, 16, 16);
1513
- setDragSource(provider.id);
1514
- setSortAnnouncement("");
1515
- },
1516
- onDragEnd: endDrag,
1517
- onKeyDown: (event) => {
1518
- if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1519
- event.preventDefault();
1520
- move(provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1521
- }
1522
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1523
- className: "web-search-rank-index",
1524
- children: participating ? rank + 1 : "—"
1525
- }),
1526
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1527
- className: "web-search-provider",
1528
- children: [
1529
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1530
- className: "web-search-provider-heading",
1531
- children: [
1532
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1533
- className: "dsh-ui-heading",
1534
- children: providerDisplayLabel(provider, locale)
1535
- }),
1536
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1537
- className: "ws-provider-note",
1538
- 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 })]
1539
- }),
1540
- active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1541
- tone: "success",
1542
- children: text.inUse
1543
- }),
1544
- provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1545
- url: provider.signupUrl,
1546
- label: providerDisplayLabel(provider, locale) + " " + text.signup,
1547
- children: text.signup
1548
- })
1549
- ]
1550
- }),
1551
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1552
- className: "web-search-provider-info",
1553
- 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, {
1554
- url: info.pricingUrl,
1555
- label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1556
- children: provider.id === "ddg" ? text.website : text.pricing
1557
- })] })] })]
1558
- }),
1559
- access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1560
- className: "dsh-ui-hint",
1561
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1562
- url: access.url,
1563
- label: text.openConfig,
1564
- children: text.openConfig
1565
- })
1566
- }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1567
- className: "dsh-ui-hint",
1568
- children: [text.ownerSettings, access.name]
1569
- }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1570
- className: "dsh-ui-hint",
1571
- children: text.unknownOwner
1572
- }) : null,
1573
- shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1574
- className: "dsh-ui-meta",
1575
- children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : provider.credentialHint ?? text.sharedKey
1576
- }) : null,
1577
- showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1578
- className: "dsh-ui-field",
1579
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1580
- className: "dsh-ui-label",
1581
- children: "API Key"
1582
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1583
- className: "ws-input",
1584
- type: "password",
1585
- autoComplete: "off",
1586
- spellCheck: false,
1587
- disabled: busy || readOnly,
1588
- value: keys[provider.id] ?? "",
1589
- placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1590
- onChange: (event) => setKeys((current) => ({
1591
- ...current,
1592
- [provider.id]: event.target.value
1593
- }))
1594
- })]
1595
- }) : null,
1596
- showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1597
- className: "dsh-ui-actions",
1598
- children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1599
- variant: "outline",
1600
- size: "sm",
1601
- disabled: busy || readOnly || !typedKey,
1602
- onClick: () => void action(async () => {
1603
- await saveKeys(provider);
1604
- await load();
1605
- setNote({ key: "keySaved" });
1606
- }),
1607
- children: text.saveKey
1608
- }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1609
- disabled: busy || readOnly,
1610
- label: text.deleteKey,
1611
- confirmLabel: text.confirmDelete,
1612
- onConfirm: () => void action(() => removeKey(provider))
1613
- }) : null]
1614
- }) : null
1615
- ]
1616
- }),
1617
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1618
- className: "web-search-rank-move",
1619
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1620
- checked: participating,
1621
- label: providerDisplayLabel(provider, locale),
1622
- disabled: busy,
1623
- onChange: (next) => {
1624
- const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1625
- runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following, droppedDormantIds(saved.settings, following, saved.providers));
1626
- }
1627
- })
1628
- })
1629
- ]
1630
- }, provider.id);
1631
- })
1632
- }),
1633
- backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1634
- className: "dsh-ui-hint",
1635
- children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1636
- }) : null,
1637
- on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1638
- className: "dsh-ui-actions",
1639
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1640
- variant: "outline",
1641
- size: "md",
1642
- disabled: busy || reordering,
1643
- onClick: () => void action(async () => {
1644
- await saveKeys();
1645
- const result = await call("test");
1646
- await load();
1647
- setNote({ sources: result.sources });
1648
- }),
1649
- children: text.test
1650
- })
1651
- }) : null
1652
- ]
1653
- }), (saved.providers.some((provider) => provider.kind === "fetch") || saved.settings.fetchProvider) && (() => {
1654
- const fetchId = saved.settings.fetchProvider;
1655
- const fetchSelected = saved.providers.find((provider) => provider.kind === "fetch" && provider.id === fetchId);
1656
- const fetchDormant = Boolean(fetchId) && !fetchSelected;
1657
- const fetchActive = Boolean(fetchSelected && saved.fetchActive && fetchSelected.configured);
1658
- const fetchAccess = fetchSelected ? configurationAccess(fetchSelected) : fetchDormant ? { kind: "unknown" } : void 0;
1659
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1660
- className: "dsh-ui-card dsh-ui-card--flat",
1661
- "data-testid": "web-search-fetch",
1662
- "data-availability": fetchDormant ? "dormant" : fetchSelected?.configured ? "available" : "unavailable",
1663
- "data-in-use": fetchActive || void 0,
1546
+ ["search", "fetch"].map((kind) => {
1547
+ const order = pendingOrder?.kind === kind ? pendingOrder.order : providerOrder(saved.settings, kind);
1548
+ const catalog = catalogBackends(saved, kind);
1549
+ const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1550
+ const selected = selectedBackend(saved, kind);
1551
+ const on = kind === "search" ? saved.settings.searchEnabled : saved.settings.fetchEnabled;
1552
+ const canEnable = order.some((id) => saved.providers.some((provider) => provider.kind === kind && provider.id === id && provider.configured));
1553
+ const sortingOther = pendingOrder !== null && pendingOrder.kind !== kind;
1554
+ const locked = busy || sortingOther || Boolean(dragSource && !dragSource.startsWith(kind + ":"));
1555
+ const title = kind === "search" ? text.toolTitle : text.fetchTitle;
1556
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1557
+ role: "tabpanel",
1558
+ id: tabsId + "-" + kind + "-panel",
1559
+ "aria-labelledby": tabsId + "-" + kind + "-tab",
1560
+ hidden: tab !== kind,
1561
+ tabIndex: 0,
1562
+ children: backends.length === 0 ? kind === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1563
+ className: "dsh-ui-empty",
1564
+ children: text.empty
1565
+ }) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1566
+ className: "web-search-card dsh-ui-card dsh-ui-card--flat",
1567
+ "data-testid": kind === "search" ? "web-search-tool" : "web-search-fetch",
1568
+ "data-on": on ? "true" : "false",
1664
1569
  children: [
1665
1570
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1666
1571
  className: "ws-card-head",
1667
1572
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1668
1573
  className: "dsh-ui-title",
1669
- children: text.fetchTitle
1670
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1671
- className: "ws-provider-note",
1672
- children: [
1673
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: fetchDormant ? "idle" : fetchSelected?.configured ? "done" : "warning" }),
1674
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fetchDormant ? text.dormant : fetchSelected?.configured ? text.available : text.unavailable }),
1675
- fetchActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1676
- tone: "success",
1677
- children: text.inUse
1678
- }) : null
1679
- ]
1574
+ children: title
1575
+ }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1576
+ className: "dsh-ui-hint",
1577
+ children: kind === "search" ? text.needBackend : text.needFetchBackend
1680
1578
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1681
- checked: saved.settings.fetchEnabled,
1682
- label: text.fetchTitle,
1683
- disabled: busy || reordering,
1684
- onChange: (next) => void action(async () => {
1685
- const current = statusRef.current;
1686
- adopt(await call("update", {
1687
- settings: {
1688
- ...editable(current.settings),
1689
- fetchEnabled: next
1690
- },
1691
- expectedRevision: current.settings.revision
1692
- }));
1693
- await load();
1694
- })
1579
+ checked: on,
1580
+ label: title,
1581
+ disabled: locked || dragSource !== null || !on && !canEnable,
1582
+ onChange: (next) => runImmediate(next, order, [], kind)
1695
1583
  })]
1696
1584
  }),
1697
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1698
- className: "dsh-ui-field",
1699
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1700
- className: "dsh-ui-label",
1701
- children: text.fetchProvider
1702
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1703
- className: "dsh-ui-select",
1704
- "aria-label": text.fetchProvider,
1705
- value: fetchId,
1706
- disabled: busy || reordering,
1707
- onChange: (event) => {
1708
- const id = event.target.value;
1709
- action(async () => {
1710
- const current = statusRef.current;
1711
- adopt(await call("update", {
1712
- settings: {
1713
- ...editable(current.settings),
1714
- fetchProvider: id
1715
- },
1716
- expectedRevision: current.settings.revision
1717
- }));
1718
- await load();
1719
- });
1720
- },
1721
- children: [fetchDormant && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1722
- value: fetchId,
1585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1586
+ className: "web-search-rank",
1587
+ "aria-label": kind === "search" ? text.backends : text.fetchBackends,
1588
+ children: backends.map((provider) => {
1589
+ const participating = order.includes(provider.id);
1590
+ const rank = order.indexOf(provider.id);
1591
+ const dormant = !isRegistered(saved, provider.id, kind);
1592
+ const active = !dormant && (kind === "search" ? saved.searchActive : saved.fetchActive) && selected?.id === provider.id;
1593
+ const info = kind === "search" || ownsProviderConfiguration(provider) && provider.id === "firecrawl" ? searchProviderInfo(provider, locale) : {
1594
+ description: ownsProviderConfiguration(provider) && provider.id === "http-managed" && provider.description === "优先匿名 Jina Reader 读取,失败后本机 HTTP 直连;无需 Key。" && locale === "en" ? "Reads with anonymous Jina Reader first, then direct HTTP if needed; no key required." : provider.description,
1595
+ pricing: provider.pricing ?? "",
1596
+ pricingUrl: provider.pricingUrl
1597
+ };
1598
+ const owned = ownsProviderConfiguration(provider);
1599
+ const access = configurationAccess(provider);
1600
+ const needsKey = owned && Boolean(provider.credentialRef);
1601
+ const shared = Boolean(provider.credentialShared);
1602
+ const showKey = !dormant && needsKey && participating && (!shared || !provider.configured);
1603
+ const canSort = participating && order.length > 1;
1604
+ const readOnly = writable[provider.credentialRef ?? ""] === false;
1605
+ const typedKey = (keys[providerKey(provider.kind, provider.id)] ?? "").trim();
1606
+ const availability = dormant ? "dormant" : provider.configured ? "available" : "unavailable";
1607
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1608
+ className: active ? "is-active" : void 0,
1609
+ "data-testid": `web-${kind}-rank-${provider.id}`,
1610
+ "data-availability": availability,
1611
+ "data-in-use": active || void 0,
1612
+ "data-dragging": dragSource === providerKey(kind, provider.id) || void 0,
1613
+ "data-drop": dropTarget === providerKey(kind, provider.id) && dragSource ? order.indexOf(dragSource.slice(kind.length + 1)) < rank ? "after" : "before" : void 0,
1614
+ onDragOver: (event) => {
1615
+ if (locked || !canSort || !dragSource?.startsWith(kind + ":") || dragSource === providerKey(kind, provider.id)) return;
1616
+ event.preventDefault();
1617
+ event.dataTransfer.dropEffect = "move";
1618
+ setDropTarget(providerKey(kind, provider.id));
1619
+ },
1620
+ onDragLeave: (event) => {
1621
+ if (!event.currentTarget.contains(event.relatedTarget)) setDropTarget(null);
1622
+ },
1623
+ onDrop: (event) => {
1624
+ if (locked || !canSort || !dragSource?.startsWith(kind + ":")) return;
1625
+ event.preventDefault();
1626
+ move(kind, dragSource.slice(kind.length + 1), rank);
1627
+ endDrag();
1628
+ },
1723
1629
  children: [
1724
- fetchId,
1725
- " · ",
1726
- text.dormant
1630
+ canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1631
+ variant: "ghost",
1632
+ size: "sm",
1633
+ className: "web-search-drag-handle",
1634
+ ref: (element) => {
1635
+ if (element) handleRefs.current.set(providerKey(kind, provider.id), element);
1636
+ else handleRefs.current.delete(providerKey(kind, provider.id));
1637
+ },
1638
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1639
+ width: "16",
1640
+ height: "20",
1641
+ viewBox: "0 0 16 20",
1642
+ fill: "currentColor",
1643
+ "aria-hidden": "true",
1644
+ children: [
1645
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1646
+ cx: "5",
1647
+ cy: "5",
1648
+ r: "1.5"
1649
+ }),
1650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1651
+ cx: "11",
1652
+ cy: "5",
1653
+ r: "1.5"
1654
+ }),
1655
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1656
+ cx: "5",
1657
+ cy: "10",
1658
+ r: "1.5"
1659
+ }),
1660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1661
+ cx: "11",
1662
+ cy: "10",
1663
+ r: "1.5"
1664
+ }),
1665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1666
+ cx: "5",
1667
+ cy: "15",
1668
+ r: "1.5"
1669
+ }),
1670
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1671
+ cx: "11",
1672
+ cy: "15",
1673
+ r: "1.5"
1674
+ })
1675
+ ]
1676
+ }),
1677
+ draggable: !locked,
1678
+ "aria-disabled": locked,
1679
+ "aria-label": text.dragPrefix + providerDisplayLabel(provider, locale),
1680
+ title: text.dragHint,
1681
+ onDragStart: (event) => {
1682
+ if (locked || pendingRef.current && pendingRef.current.kind !== kind) {
1683
+ event.preventDefault();
1684
+ return;
1685
+ }
1686
+ event.dataTransfer.effectAllowed = "move";
1687
+ event.dataTransfer.setData(`application/x-dsh-${kind}-provider`, provider.id);
1688
+ const row = event.currentTarget.closest("li");
1689
+ if (row) event.dataTransfer.setDragImage(row, 16, 16);
1690
+ dragRef.current = providerKey(kind, provider.id);
1691
+ requestSeq.current += 1;
1692
+ setDragSource(providerKey(kind, provider.id));
1693
+ setSortAnnouncement("");
1694
+ },
1695
+ onDragEnd: endDrag,
1696
+ onKeyDown: (event) => {
1697
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1698
+ event.preventDefault();
1699
+ move(kind, provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1700
+ }
1701
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1702
+ className: "web-search-rank-index",
1703
+ children: participating ? rank + 1 : "—"
1704
+ }),
1705
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1706
+ className: "web-search-provider",
1707
+ children: [
1708
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1709
+ className: "web-search-provider-heading",
1710
+ children: [
1711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1712
+ className: "dsh-ui-heading",
1713
+ children: providerDisplayLabel(provider, locale)
1714
+ }),
1715
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1716
+ className: "ws-provider-note",
1717
+ 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 })]
1718
+ }),
1719
+ active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1720
+ tone: "success",
1721
+ children: text.inUse
1722
+ }),
1723
+ provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1724
+ url: provider.signupUrl,
1725
+ label: providerDisplayLabel(provider, locale) + " " + text.signup,
1726
+ children: text.signup
1727
+ })
1728
+ ]
1729
+ }),
1730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1731
+ className: "web-search-provider-info",
1732
+ children: [info.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }) : null, info.pricing || info.pricingUrl ? /* @__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, {
1733
+ url: info.pricingUrl,
1734
+ label: providerDisplayLabel(provider, locale) + " " + text.pricing,
1735
+ children: provider.id === "ddg" ? text.website : text.pricing
1736
+ })] })] }) : null]
1737
+ }),
1738
+ access.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1739
+ className: "dsh-ui-hint",
1740
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1741
+ url: access.url,
1742
+ label: text.openConfig,
1743
+ children: text.openConfig
1744
+ })
1745
+ }) : access.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1746
+ className: "dsh-ui-hint",
1747
+ children: [text.ownerSettings, access.name]
1748
+ }) : access.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1749
+ className: "dsh-ui-hint",
1750
+ children: text.unknownOwner
1751
+ }) : null,
1752
+ shared && participating && owned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1753
+ className: "dsh-ui-meta",
1754
+ children: provider.credentialHint === "与模型设置共用 Key" ? locale === "zh" ? provider.credentialHint : "Shares a key with model settings" : owned && provider.id === "firecrawl" && locale === "en" ? "Search and page reads share a Firecrawl key" : provider.credentialHint ?? text.sharedKey
1755
+ }) : null,
1756
+ showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1757
+ className: "dsh-ui-field",
1758
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1759
+ className: "dsh-ui-label",
1760
+ children: "API Key"
1761
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1762
+ className: "ws-input",
1763
+ type: "password",
1764
+ autoComplete: "off",
1765
+ spellCheck: false,
1766
+ disabled: busy || reordering || readOnly,
1767
+ value: keys[providerKey(provider.kind, provider.id)] ?? "",
1768
+ placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1769
+ onChange: (event) => setKeys((current) => ({
1770
+ ...current,
1771
+ [providerKey(provider.kind, provider.id)]: event.target.value
1772
+ }))
1773
+ })]
1774
+ }) : null,
1775
+ showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1776
+ className: "dsh-ui-actions",
1777
+ children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1778
+ variant: "outline",
1779
+ size: "sm",
1780
+ disabled: busy || reordering || readOnly || !typedKey,
1781
+ onClick: () => void action(async () => {
1782
+ await saveKeys(provider);
1783
+ await load();
1784
+ setNote({ key: "keySaved" });
1785
+ }),
1786
+ children: text.saveKey
1787
+ }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1788
+ disabled: busy || reordering || readOnly,
1789
+ label: text.deleteKey,
1790
+ confirmLabel: text.confirmDelete,
1791
+ onConfirm: () => void action(() => removeKey(provider))
1792
+ }) : null]
1793
+ }) : null
1794
+ ]
1795
+ }),
1796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1797
+ className: "web-search-rank-move",
1798
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1799
+ checked: participating,
1800
+ label: providerDisplayLabel(provider, locale),
1801
+ disabled: locked || dragSource !== null,
1802
+ onChange: (next) => {
1803
+ const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1804
+ runImmediate(on && following.some((id) => saved.providers.some((item) => item.kind === kind && item.id === id && item.configured)), following, droppedDormantIds(saved.settings, following, saved.providers, kind), kind);
1805
+ }
1806
+ })
1807
+ })
1727
1808
  ]
1728
- }), saved.providers.filter((provider) => provider.kind === "fetch").map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1729
- value: provider.id,
1730
- children: [providerDisplayLabel(provider, locale), provider.configured ? "" : " · " + text.unavailable]
1731
- }, provider.id))]
1732
- })]
1809
+ }, provider.id);
1810
+ })
1733
1811
  }),
1734
- fetchAccess?.kind === "link" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1812
+ kind === "search" && backends.some(usesBundledPricing) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1735
1813
  className: "dsh-ui-hint",
1736
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppLink, {
1737
- url: fetchAccess.url,
1738
- label: text.openConfig,
1739
- children: text.openConfig
1814
+ children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
1815
+ }) : null,
1816
+ on && kind === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1817
+ className: "dsh-ui-actions",
1818
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1819
+ variant: "outline",
1820
+ size: "md",
1821
+ disabled: busy || reordering,
1822
+ onClick: () => void action(async () => {
1823
+ await saveKeysForKind("search");
1824
+ const result = await call("test");
1825
+ await load();
1826
+ setNote({ sources: result.sources });
1827
+ }),
1828
+ children: text.test
1740
1829
  })
1741
- }) : fetchAccess?.kind === "owner" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1742
- className: "dsh-ui-hint",
1743
- children: [text.ownerSettings, fetchAccess.name]
1744
- }) : fetchAccess?.kind === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1745
- className: "dsh-ui-hint",
1746
- children: text.unknownOwner
1747
1830
  }) : null
1748
1831
  ]
1749
- });
1750
- })()]
1832
+ })
1833
+ }, kind);
1751
1834
  }),
1752
1835
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1753
1836
  role: "tabpanel",
@@ -1756,11 +1839,11 @@ function NetworkSearchSettings({ client, hidden }) {
1756
1839
  hidden: tab !== "limits",
1757
1840
  tabIndex: 0,
1758
1841
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
1759
- className: "web-search-limits",
1842
+ className: "dsh-ui-fields-grid",
1760
1843
  disabled: busy || reordering,
1761
1844
  "aria-label": text.tabLimits,
1762
1845
  children: LIMITS.map(([key, min, max]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1763
- className: "ws-limit",
1846
+ className: "dsh-ui-field",
1764
1847
  children: [
1765
1848
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1766
1849
  className: "dsh-ui-label",
@@ -1799,7 +1882,7 @@ function NetworkSearchSettings({ client, hidden }) {
1799
1882
  })
1800
1883
  }),
1801
1884
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1802
- className: "dsh-ui-actions dsh-ui-actions-end",
1885
+ className: "dsh-ui-form-footer dsh-ui-actions-end",
1803
1886
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1804
1887
  className: "dsh-ui-meta",
1805
1888
  children: limitsDirty || keysDirty ? text.unsaved : text.saveHint
@@ -1814,74 +1897,13 @@ function NetworkSearchSettings({ client, hidden }) {
1814
1897
  ]
1815
1898
  });
1816
1899
  }
1817
- const styles = `${officialUiCss("ws-root")}
1818
- .ws-root { max-width: 760px; }
1819
- .ws-card-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding-bottom: 8px; }
1820
- .ws-sr-only {
1821
- position: absolute;
1822
- width: 1px;
1823
- height: 1px;
1824
- padding: 0;
1825
- margin: -1px;
1826
- overflow: hidden;
1827
- clip-path: inset(50%);
1828
- white-space: nowrap;
1829
- border: 0;
1830
- }
1831
- .ws-provider-note { display: flex; align-items: center; gap: 6px; }
1832
- .web-search-provider { display: grid; gap: 8px; min-width: 0; }
1833
- .web-search-provider-info { display: grid; gap: 3px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
1834
- .web-search-provider-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; min-height: 24px; }
1835
- .web-search-settings a { color: var(--dsw-alias-state-business-primary); font-size: 12px; line-height: 18px; text-underline-offset: 3px; }
1836
- /* The primitive renders a 32px control in an inline-flex wrapper; a settings
1837
- * field needs it to fill the column it sits in. */
1838
- .ws-input { width: 100%; }
1839
- .web-search-rank { margin: 0; padding: 0; list-style: none; }
1840
- .web-search-rank li {
1841
- position: relative;
1842
- display: grid;
1843
- grid-template-columns: 24px minmax(0, 1fr) auto;
1844
- gap: 10px;
1845
- align-items: start;
1846
- padding: 14px 0;
1847
- border-top: 0.5px solid var(--dsw-alias-border-l2);
1848
- }
1849
- .web-search-settings button.web-search-drag-handle {
1850
- display: grid;
1851
- place-items: center;
1852
- width: 24px;
1853
- height: 24px;
1854
- min-height: 24px;
1855
- padding: 0;
1856
- cursor: grab;
1857
- opacity: 0.65;
1858
- }
1859
- .web-search-settings button.web-search-drag-handle:hover { opacity: 1; }
1860
- .web-search-settings button.web-search-drag-handle:active { cursor: grabbing; }
1861
- .web-search-settings button.web-search-drag-handle[aria-disabled='true'] { cursor: wait; opacity: 0.4; }
1862
- .web-search-rank li[data-dragging] { opacity: 0.45; }
1863
- .web-search-rank li[data-drop]::after { content: ''; position: absolute; left: 0; right: 0; height: 2px; background: var(--dsw-alias-state-business-primary); pointer-events: none; }
1864
- .web-search-rank li[data-drop='before']::after { top: -1px; }
1865
- .web-search-rank li[data-drop='after']::after { bottom: -1px; }
1866
- .web-search-rank-index { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; line-height: 24px; }
1867
- .web-search-rank-move { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; justify-content: flex-end; padding-top: 2px; }
1868
- .web-search-limits { margin: 0; border: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; }
1869
- /* A two-column grid, so these fields keep the contract's caption and the
1870
- * primitive's control and only own their vertical rhythm — a stacked list is
1871
- * what draws the hairline between fields, and there is nothing to separate. */
1872
- .ws-limit { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
1873
- .ws-danger:hover:not(:disabled),
1874
- .ws-danger[data-armed] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); }
1875
- .ws-danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger); }
1876
- @media (max-width: 560px) { .web-search-rank-move { flex-direction: column; align-items: flex-end; } .web-search-limits { grid-template-columns: minmax(0, 1fr); } }
1877
- `;
1878
1900
  function apply(ctx) {
1879
1901
  const client = ctx;
1880
1902
  ctx.effect(() => {
1881
1903
  if (typeof document === "undefined") return () => {};
1882
1904
  const style = document.createElement("style");
1883
1905
  style.setAttribute("data-plugin", "@klarkxy/dsh-web-search-manager");
1884
- style.textContent = styles;
1906
+ style.textContent = webSearchClientStyles;
1885
1907
  style.dataset.dshWebSearch = "";
1886
1908
  document.head.appendChild(style);
1887
1909
  return () => style.remove();