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