dsh-web-search-plugin 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -25,6 +25,25 @@ window.__ModuleLoader__.load({
25
25
 
26
26
  /** Settings namespace of this plugin. */
27
27
  const NS = "dsh-web-search-plugin";
28
+ /** Built-in REST providers (mirrors `lib/providers.js`; browser bundle is self-contained). */
29
+ const BUILTIN_REST_PROVIDERS = [
30
+ { id: "serper", name: "Serper", officialUrl: "https://serper.dev/", keyRequired: true, keyField: "serperApiKey", keyEnvField: "serperApiKeyEnv", baseURLField: "serperBaseURL" },
31
+ { id: "serpapi", name: "SerpApi", officialUrl: "https://serpapi.com/", keyRequired: true, keyField: "serpapiApiKey", keyEnvField: "serpapiApiKeyEnv", baseURLField: "serpapiBaseURL" },
32
+ { id: "exa", name: "Exa", officialUrl: "https://dashboard.exa.ai/", keyRequired: true, keyField: "exaApiKey", keyEnvField: "exaApiKeyEnv", baseURLField: "exaBaseURL" },
33
+ { id: "searxng", name: "SearXNG", officialUrl: "https://docs.searxng.org/", keyRequired: false, keyField: "", keyEnvField: "", baseURLField: "searxngBaseURL" },
34
+ { id: "scavio", name: "Scavio", officialUrl: "https://scavio.dev/", keyRequired: true, keyField: "scavioApiKey", keyEnvField: "scavioApiKeyEnv", baseURLField: "scavioBaseURL" },
35
+ { id: "firecrawl", name: "Firecrawl", officialUrl: "https://firecrawl.dev/", keyRequired: true, keyField: "firecrawlApiKey", keyEnvField: "firecrawlApiKeyEnv", baseURLField: "firecrawlBaseURL" }
36
+ ];
37
+ /** Find a built-in REST provider by id. */
38
+ function builtinRest(id) {
39
+ for (const entry of BUILTIN_REST_PROVIDERS) if (entry.id === id) return entry;
40
+ return void 0;
41
+ }
42
+ /** Find a built-in REST provider by its literal key field name. */
43
+ function builtinRestByKeyField(field) {
44
+ for (const entry of BUILTIN_REST_PROVIDERS) if (entry.keyField === field) return entry;
45
+ return void 0;
46
+ }
28
47
  /** Credential reference Tavily keyed mode resolves when the section names none. */
29
48
  const DEFAULT_TAVILY_KEY_REF = "TAVILY_API_KEY";
30
49
  /** Credential reference Brave resolves when the section names none. */
@@ -37,12 +56,42 @@ window.__ModuleLoader__.load({
37
56
  const BRAVE_API_KEY_FIELD = "braveApiKey";
38
57
  /** Form field the DeepSeek write-only credential control stages under. */
39
58
  const DEEPSEEK_API_KEY_FIELD = "deepseekApiKey";
40
- const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD, DEEPSEEK_API_KEY_FIELD]);
59
+ /** Form fields carrying literal API keys (write-only, staged under the credentials domain). */
60
+ const REST_KEY_FIELDS = BUILTIN_REST_PROVIDERS.map((entry) => entry.keyField).filter((field) => field !== "");
61
+ const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD, DEEPSEEK_API_KEY_FIELD, ...REST_KEY_FIELDS]);
62
+ const DRAFT_KEY = "dsh-web-search-plugin:settings-draft";
63
+
64
+ function readDraft() {
65
+ try {
66
+ const raw = sessionStorage.getItem(DRAFT_KEY);
67
+ if (!raw) return null;
68
+ const parsed = JSON.parse(raw);
69
+ if (parsed === null || typeof parsed !== "object" || parsed.staged === null || typeof parsed.staged !== "object") return null;
70
+ return parsed;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function writeDraft(form) {
76
+ try {
77
+ const staged = {};
78
+ for (const [field, edit] of form.staged) {
79
+ if (SECRET_FIELDS.has(field)) continue;
80
+ staged[field] = { text: String(edit?.text ?? ""), clear: edit?.clear === true };
81
+ }
82
+ if (Object.keys(staged).length === 0 && form.failed !== true) {
83
+ sessionStorage.removeItem(DRAFT_KEY);
84
+ return;
85
+ }
86
+ sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ staged, failed: form.failed === true }));
87
+ } catch {
88
+ /* private mode / quota */
89
+ }
90
+ }
41
91
 
42
92
  /** Locale bundles for the card. */
43
93
  const en = {
44
94
  nav: "Web search",
45
- description: "Tavily (keyless by default) or Brave Search. Switching engines here is enough — web.searchProvider stays dsh-web-search.",
46
95
  provider: "Search engine",
47
96
  providerTavily: "Tavily",
48
97
  providerBrave: "Brave Search",
@@ -53,7 +102,8 @@ window.__ModuleLoader__.load({
53
102
  apiKey: "API key",
54
103
  apiKeyHint: "Stored outside the settings file. Leave blank to keep the current key.",
55
104
  apiKeySet: "A key is configured.",
56
- apiKeyUnset: "No key is configured; keyed / Brave search is unavailable until one is.",
105
+ apiKeyUnset: "No key set",
106
+ apiKeyUnavailableHint: "Search is unavailable until a key is configured.",
57
107
  apiKeyEnv: "Credential reference",
58
108
  apiKeyEnvHint: "Name of the credential/env var resolved for keyed Tavily search.",
59
109
  braveApiKeyEnv: "Credential reference",
@@ -63,7 +113,6 @@ window.__ModuleLoader__.load({
63
113
  braveBaseURL: "Endpoint",
64
114
  braveBaseURLHint: "Leave blank to use the Brave Search API default.",
65
115
  maxResults: "Max results",
66
- maxResultsHint: "Sources returned per search (1-20).",
67
116
  searchDepth: "Search depth",
68
117
  searchDepthBasic: "Basic (faster)",
69
118
  searchDepthAdvanced: "Advanced (slower, deeper)",
@@ -73,9 +122,9 @@ window.__ModuleLoader__.load({
73
122
  topicGeneral: "General",
74
123
  topicNews: "News",
75
124
  country: "Country",
76
- countryHint: "ISO 2-letter code (e.g. cn). Leave blank for Brave's default.",
125
+ countryHint: "ISO 2-letter code (e.g. cn).",
77
126
  searchLang: "Search language",
78
- searchLangHint: "e.g. zh-hans or en. Leave blank for Brave's default.",
127
+ searchLangHint: "e.g. zh-hans or en.",
79
128
  freshness: "Freshness",
80
129
  freshnessAny: "Any time",
81
130
  freshnessDay: "Past day",
@@ -83,7 +132,7 @@ window.__ModuleLoader__.load({
83
132
  freshnessMonth: "Past month",
84
133
  freshnessYear: "Past year",
85
134
  proxy: "Proxy",
86
- proxyHint: "HTTP(S) proxy URL. Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
135
+ proxyPlaceholder: "Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
87
136
  deepseekApiKeyEnv: "Credential reference",
88
137
  deepseekApiKeyEnvHint: "Name of the credential/env var resolved for DeepSeek search.",
89
138
  deepseekBaseURL: "Endpoint",
@@ -98,18 +147,47 @@ window.__ModuleLoader__.load({
98
147
  maxUsesHint: "Maximum web_search server-tool uses per request.",
99
148
  expand: "Show settings",
100
149
  collapse: "Hide settings",
150
+ getKey: "Get API key",
151
+ ddgDisabled: "DuckDuckGo — use via SearXNG",
152
+ ddgHint: "DuckDuckGo has no official search API. Pick SearXNG and point it at an instance that includes DuckDuckGo (most public ones do), or self-host one.",
101
153
  save: "Save",
102
154
  saving: "Saving…",
103
155
  discard: "Discard",
104
156
  unsaved: "Unsaved",
157
+ overridden: "Overridden",
158
+ reset: "Restore default",
105
159
  saveFailed: "The deployment did not accept these values; they were left for you to correct.",
160
+ savedToast: "✓ Settings saved and applied",
161
+ nothingToSave: "No changes to save",
162
+ invalidToast: "Fix the highlighted fields before saving.",
106
163
  readOnly: "This deployment stores settings read-only.",
107
- invalidNumber: "Enter a number, or leave blank to use the default."
164
+ invalidNumber: "Enter a whole number of at least 1, or leave blank for the default.",
165
+ quotaTitle: "Quota",
166
+ quotaPlan: "Plan {plan}",
167
+ quotaUsed: "Used {used} / {limit}",
168
+ quotaUsedOnly: "Used {used}",
169
+ quotaRemaining: "{remaining} left",
170
+ quotaUnlimited: "No published limit",
171
+ quotaResets: "Resets {time}",
172
+ quotaCycle: "Resets with the billing cycle",
173
+ quotaBurst: "Burst {remaining}/{limit}",
174
+ quotaBraveCapacity: "Capacity",
175
+ quotaBraveRps: "{limit} requests per second",
176
+ quotaBraveRpsWindow: "Per-second window",
177
+ braveDefaultPlaceholder: "Leave blank for Brave's default.",
178
+ quotaEmpty: "Usage appears after the next search.",
179
+ quotaKeyedHint: "Keyed Tavily: local deductions vs periodic /usage, whichever is higher.",
180
+ quotaBraveUnlimited: "No monthly request cap",
181
+ quotaRefresh: "Refresh usage",
182
+ quotaRefreshing: "Refreshing…",
183
+ quotaError: "Usage unavailable: {error}",
184
+ quotaWarn: "Remaining quota is low.",
185
+ quotaDanger: "Remaining quota is critical.",
186
+ quotaPaygo: "Pay-as-you-go {used}/{limit}"
108
187
  };
109
188
  /** Simplified Chinese copy. */
110
189
  const zh = {
111
190
  nav: "网页搜索",
112
- description: "Tavily(默认 keyless)或 Brave Search。在此切换引擎即可,不必改 web.searchProvider。",
113
191
  provider: "搜索引擎",
114
192
  providerTavily: "Tavily",
115
193
  providerBrave: "Brave Search",
@@ -120,7 +198,8 @@ window.__ModuleLoader__.load({
120
198
  apiKey: "API Key",
121
199
  apiKeyHint: "不写入设置文件。留空表示保持当前密钥。",
122
200
  apiKeySet: "已配置密钥。",
123
- apiKeyUnset: "未配置密钥;keyed / Brave 模式下搜索不可用。",
201
+ apiKeyUnset: "未配置密钥",
202
+ apiKeyUnavailableHint: "未配置密钥前搜索不可用。",
124
203
  apiKeyEnv: "凭据引用名",
125
204
  apiKeyEnvHint: "keyed 模式解析的凭据/环境变量名。",
126
205
  braveApiKeyEnv: "凭据引用名",
@@ -130,7 +209,6 @@ window.__ModuleLoader__.load({
130
209
  braveBaseURL: "接口地址",
131
210
  braveBaseURLHint: "留空则使用 Brave Search API 默认地址。",
132
211
  maxResults: "结果数量",
133
- maxResultsHint: "每次搜索返回的源数量(1-20)。",
134
212
  searchDepth: "搜索深度",
135
213
  searchDepthBasic: "Basic(较快)",
136
214
  searchDepthAdvanced: "Advanced(较慢、更深)",
@@ -140,9 +218,9 @@ window.__ModuleLoader__.load({
140
218
  topicGeneral: "通用",
141
219
  topicNews: "新闻",
142
220
  country: "国家/地区",
143
- countryHint: "ISO 两位码(如 cn)。留空使用 Brave 默认。",
221
+ countryHint: "ISO 两位码(如 cn)。",
144
222
  searchLang: "搜索语言",
145
- searchLangHint: "如 zh-hans、en。留空使用 Brave 默认。",
223
+ searchLangHint: "如 zh-hans、en。",
146
224
  freshness: "时效",
147
225
  freshnessAny: "不限",
148
226
  freshnessDay: "过去一天",
@@ -150,7 +228,7 @@ window.__ModuleLoader__.load({
150
228
  freshnessMonth: "过去一月",
151
229
  freshnessYear: "过去一年",
152
230
  proxy: "代理",
153
- proxyHint: "HTTP(S) 代理 URL。留空则使用 HTTPS_PROXY / HTTP_PROXY。",
231
+ proxyPlaceholder: "留空使用 HTTPS_PROXY / HTTP_PROXY环境变量",
154
232
  deepseekApiKeyEnv: "凭据引用名",
155
233
  deepseekApiKeyEnvHint: "DeepSeek 搜索解析的凭据/环境变量名。",
156
234
  deepseekBaseURL: "接口地址",
@@ -165,25 +243,60 @@ window.__ModuleLoader__.load({
165
243
  maxUsesHint: "每次请求 web_search 服务端工具的最大调用次数。",
166
244
  expand: "展开设置",
167
245
  collapse: "收起设置",
246
+ getKey: "获取 API key",
247
+ ddgDisabled: "DuckDuckGo — 请经 SearXNG 使用",
248
+ ddgHint: "DuckDuckGo 没有官方搜索 API。请选择 SearXNG,并填入一个聚合了 DuckDuckGo 的实例地址(多数公共实例都有),或自建一个。",
168
249
  save: "保存",
169
250
  saving: "保存中…",
170
251
  discard: "放弃修改",
171
252
  unsaved: "未保存",
253
+ overridden: "已覆盖",
254
+ reset: "恢复默认",
172
255
  saveFailed: "本部署没有接受这些值,已保留供你修改。",
256
+ savedToast: "✓ 设置已保存并立即生效",
257
+ nothingToSave: "没有需要保存的更改",
258
+ invalidToast: "请先修正标红的字段再保存。",
173
259
  readOnly: "本部署的设置为只读。",
174
- invalidNumber: "请填数字;留空表示使用默认值。"
260
+ invalidNumber: "请输入不小于 1 的整数;留空表示使用默认值。",
261
+ quotaTitle: "额度",
262
+ quotaPlan: "计划 {plan}",
263
+ quotaUsed: "已用 {used} / {limit}",
264
+ quotaUsedOnly: "已用 {used}",
265
+ quotaRemaining: "剩余 {remaining}",
266
+ quotaUnlimited: "未公布限额",
267
+ quotaResets: "{time} 重置",
268
+ quotaCycle: "按计费周期重置",
269
+ quotaBurst: "突发 {remaining}/{limit}",
270
+ quotaBraveCapacity: "容量",
271
+ quotaBraveRps: "{limit} 次/秒",
272
+ quotaBraveRpsWindow: "每秒窗口",
273
+ braveDefaultPlaceholder: "留空使用 Brave 默认。",
274
+ quotaEmpty: "下次搜索后显示用量。",
275
+ quotaKeyedHint: "Tavily keyed:本地扣费与定时 /usage 取较高值。",
276
+ quotaBraveUnlimited: "月请求不限额",
277
+ quotaRefresh: "刷新额度",
278
+ quotaRefreshing: "刷新中…",
279
+ quotaError: "额度不可用:{error}",
280
+ quotaWarn: "剩余额度偏低。",
281
+ quotaDanger: "剩余额度告急。",
282
+ quotaPaygo: "按量 {used}/{limit}"
175
283
  };
176
284
 
177
- /** A whole-number field spec: empty draft clears, any other invalid draft blocks save. */
178
- function numberField(field) {
285
+ const MAX_RESULTS_CAP = 20;
286
+ /** A whole-number field spec: empty draft clears; non-digits block save; max is clamped. */
287
+ function numberField(field, bounds) {
179
288
  return {
180
289
  field,
181
290
  format: (value) => typeof value === "number" ? String(value) : "",
182
291
  parse: (text) => {
183
292
  const trimmed = text.trim();
184
293
  if (trimmed === "") return { kind: "clear" };
185
- const parsed = Number(trimmed);
186
- return Number.isFinite(parsed) ? { kind: "set", value: parsed } : void 0;
294
+ if (!/^[0-9]+$/u.test(trimmed)) return void 0;
295
+ let parsed = Number(trimmed);
296
+ if (!Number.isInteger(parsed) || parsed < 1) return void 0;
297
+ if (bounds?.min != null && parsed < bounds.min) parsed = bounds.min;
298
+ if (bounds?.max != null && parsed > bounds.max) parsed = bounds.max;
299
+ return { kind: "set", value: parsed };
187
300
  }
188
301
  };
189
302
  }
@@ -227,7 +340,7 @@ window.__ModuleLoader__.load({
227
340
  this.specs = new Map([
228
341
  choiceField("provider"),
229
342
  choiceField("mode"),
230
- numberField("maxResults"),
343
+ numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }),
231
344
  textField("apiKeyEnv"),
232
345
  textField("baseURL"),
233
346
  choiceField("searchDepth"),
@@ -243,14 +356,31 @@ window.__ModuleLoader__.load({
243
356
  textField("deepseekBaseURL"),
244
357
  textField("model"),
245
358
  textField("apiVersion"),
246
- numberField("maxTokens"),
247
- numberField("maxUses")
359
+ numberField("maxTokens", { min: 1 }),
360
+ numberField("maxUses", { min: 1 }),
361
+ textField("serperApiKeyEnv"),
362
+ textField("serperBaseURL"),
363
+ textField("serpapiApiKeyEnv"),
364
+ textField("serpapiBaseURL"),
365
+ textField("exaApiKeyEnv"),
366
+ textField("exaBaseURL"),
367
+ textField("searxngBaseURL")
248
368
  ].map((spec) => [spec.field, spec]));
249
369
  this.staged = new Map();
250
370
  this.saving = false;
251
371
  this.failed = false;
252
372
  this.credential = { ref: "", configured: false, writable: true };
373
+ this.toastKey = null;
374
+ this.toastTimer = 0;
253
375
  this.listeners = new Set();
376
+ const draft = readDraft();
377
+ if (draft?.staged) {
378
+ for (const [field, edit] of Object.entries(draft.staged)) {
379
+ if (!this.specs.has(field) || edit === null || typeof edit !== "object") continue;
380
+ this.staged.set(field, { text: String(edit.text ?? ""), clear: edit.clear === true });
381
+ }
382
+ if (draft.failed === true) this.failed = true;
383
+ }
254
384
  scope.subscribe(() => {
255
385
  this.publish();
256
386
  });
@@ -272,7 +402,8 @@ window.__ModuleLoader__.load({
272
402
  dirty: plan.length > 0,
273
403
  invalid: plan.some((item) => item.run === void 0),
274
404
  saving: this.saving,
275
- failed: this.failed
405
+ failed: this.failed,
406
+ toastKey: this.toastKey
276
407
  };
277
408
  }
278
409
  field(field) {
@@ -316,19 +447,41 @@ window.__ModuleLoader__.load({
316
447
  }
317
448
  return plan;
318
449
  }
450
+ showToast(key) {
451
+ this.toastKey = key;
452
+ if (this.toastTimer) clearTimeout(this.toastTimer);
453
+ this.toastTimer = setTimeout(() => {
454
+ this.toastKey = null;
455
+ this.toastTimer = 0;
456
+ this.publish();
457
+ }, 2500);
458
+ this.publish();
459
+ }
319
460
  async save() {
320
461
  const plan = this.plan();
321
462
  const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
322
- if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
463
+ if (this.saving) return;
464
+ if (plan.length === 0) {
465
+ this.showToast("nothing");
466
+ return;
467
+ }
468
+ if (writes.length !== plan.length) {
469
+ this.showToast("invalid");
470
+ return;
471
+ }
323
472
  this.saving = true;
324
473
  this.failed = false;
325
474
  this.publish();
326
475
  let landed = true;
327
- for (const write of writes) landed = await write() && landed;
476
+ try {
477
+ for (const write of writes) landed = await write() && landed;
478
+ } catch (_saveFailure) {
479
+ landed = false;
480
+ }
328
481
  if (landed) this.staged.clear();
329
482
  this.saving = false;
330
483
  this.failed = !landed;
331
- this.publish();
484
+ this.showToast(landed ? "saved" : "failed");
332
485
  }
333
486
  actions() {
334
487
  return {
@@ -350,18 +503,26 @@ window.__ModuleLoader__.load({
350
503
  };
351
504
  }
352
505
  async clear(field) {
353
- await this.scope.unset(field);
354
- return !this.stored(field);
506
+ try {
507
+ await this.scope.unset(field);
508
+ return true;
509
+ } catch (_clearFailure) {
510
+ return false;
511
+ }
355
512
  }
356
513
  async store(field, value) {
357
- await this.scope.set(field, value);
358
- return this.userLayer()?.[field] === value;
514
+ try {
515
+ await this.scope.set(field, value);
516
+ return true;
517
+ } catch (_storeFailure) {
518
+ return false;
519
+ }
359
520
  }
360
521
  stage(field, edit) {
361
522
  this.staged.set(field, edit);
362
523
  this.failed = false;
363
524
  this.publish();
364
- if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv") this.readCredential();
525
+ if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv" || field === "deepseekApiKeyEnv" || BUILTIN_REST_PROVIDERS.some((entry) => entry.keyEnvField === field)) this.readCredential();
365
526
  }
366
527
  snapshotOf() {
367
528
  return this.scope.getSnapshot();
@@ -380,6 +541,7 @@ window.__ModuleLoader__.load({
380
541
  return user !== void 0 && Object.hasOwn(user, field);
381
542
  }
382
543
  publish() {
544
+ writeDraft(this);
383
545
  for (const listener of this.listeners) listener();
384
546
  }
385
547
  /** Draft-aware engine: staged `provider` wins over the saved section. */
@@ -387,7 +549,8 @@ window.__ModuleLoader__.load({
387
549
  const staged = this.staged.get("provider");
388
550
  if (staged !== void 0 && !staged.clear && staged.text.trim() !== "") return staged.text.trim();
389
551
  const declared = this.sectionValue("provider");
390
- return declared === "brave" || declared === "deepseek-official" ? declared : "tavily";
552
+ if (declared === "tavily" || declared === "brave" || declared === "deepseek-official" || builtinRest(declared) !== void 0) return declared;
553
+ return "deepseek-official";
391
554
  }
392
555
  tavilyRef() {
393
556
  const declared = this.sectionValue("apiKeyEnv");
@@ -404,7 +567,17 @@ window.__ModuleLoader__.load({
404
567
  /** The credential reference the currently visible engine uses. */
405
568
  refOf() {
406
569
  const provider = this.providerOf();
407
- return provider === "brave" ? this.braveRef() : provider === "deepseek-official" ? this.deepseekRef() : this.tavilyRef();
570
+ if (provider === "brave") return this.braveRef();
571
+ if (provider === "deepseek-official") return this.deepseekRef();
572
+ const rest = builtinRest(provider);
573
+ if (rest !== void 0 && rest.keyRequired) return this.restKeyRef(rest);
574
+ return this.tavilyRef();
575
+ }
576
+ /** Credential reference for a built-in REST provider's key env field. */
577
+ restKeyRef(rest) {
578
+ const declared = this.sectionValue(rest.keyEnvField);
579
+ if (declared !== void 0 && declared.length > 0) return declared;
580
+ return rest.id === "serper" ? "SERPER_API_KEY" : rest.id === "serpapi" ? "SERPAPI_API_KEY" : "EXA_API_KEY";
408
581
  }
409
582
  async readCredential() {
410
583
  const ref = this.refOf();
@@ -434,7 +607,13 @@ window.__ModuleLoader__.load({
434
607
  this.readCredential();
435
608
  }
436
609
  async writeKey(field, value) {
437
- const ref = field === BRAVE_API_KEY_FIELD ? this.braveRef() : field === DEEPSEEK_API_KEY_FIELD ? this.deepseekRef() : this.tavilyRef();
610
+ let ref;
611
+ if (field === BRAVE_API_KEY_FIELD) ref = this.braveRef();
612
+ else if (field === DEEPSEEK_API_KEY_FIELD) ref = this.deepseekRef();
613
+ else {
614
+ const rest = builtinRestByKeyField(field);
615
+ ref = rest !== void 0 ? this.restKeyRef(rest) : this.tavilyRef();
616
+ }
438
617
  try {
439
618
  await this.api.credentials.set({ ref, value });
440
619
  } catch (_credentialWriteFailure) {}
@@ -480,8 +659,19 @@ window.__ModuleLoader__.load({
480
659
  apiVersion: this.form.field("apiVersion"),
481
660
  maxTokens: this.form.field("maxTokens"),
482
661
  maxUses: this.form.field("maxUses"),
662
+ serperApiKey: this.form.field("serperApiKey"),
663
+ serperApiKeyEnv: this.form.field("serperApiKeyEnv"),
664
+ serperBaseURL: this.form.field("serperBaseURL"),
665
+ serpapiApiKey: this.form.field("serpapiApiKey"),
666
+ serpapiApiKeyEnv: this.form.field("serpapiApiKeyEnv"),
667
+ serpapiBaseURL: this.form.field("serpapiBaseURL"),
668
+ exaApiKey: this.form.field("exaApiKey"),
669
+ exaApiKeyEnv: this.form.field("exaApiKeyEnv"),
670
+ exaBaseURL: this.form.field("exaBaseURL"),
671
+ searxngBaseURL: this.form.field("searxngBaseURL"),
483
672
  apiKeyConfigured: this.form.credential.configured,
484
- apiKeyWritable: this.form.credential.writable
673
+ apiKeyWritable: this.form.credential.writable,
674
+ toastKey: this.form.toastKey
485
675
  };
486
676
  }
487
677
  inject() {
@@ -492,79 +682,422 @@ window.__ModuleLoader__.load({
492
682
  }
493
683
  };
494
684
 
495
- /** Row style: one labelled field with label, hint, and staged input. */
496
- const fieldRow = (label, hint, children) => react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px", padding: "12px 0", borderTop: "1px solid var(--dsw-alias-border-l2)" } },
497
- react.createElement("label", { style: { color: "var(--dsw-alias-label-primary)", fontSize: "13px", fontWeight: 500, lineHeight: "1.5" } }, label),
685
+ /** Row style: one labelled field with optional override badge and restore-default. */
686
+ const fieldRow = (label, hint, children, meta) => react.createElement("div", { className: "dshws_field" },
687
+ react.createElement("div", { className: "dshws_field_head" },
688
+ react.createElement("label", { className: "dshws_field_label" }, label),
689
+ meta?.overridden ? react.createElement("span", { className: "dshws_field_badges" },
690
+ react.createElement("span", { className: "dshws_badge" }, meta.t("overridden")),
691
+ react.createElement("button", { type: "button", className: "dshws_reset", disabled: meta.disabled === true, onClick: meta.onReset }, meta.t("reset"))) : null),
498
692
  children,
499
- hint ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: 0, fontSize: "12px", lineHeight: "1.5" } }, hint) : null);
500
- const textInput = (props) => react.createElement("input", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, type: "text", ...props.numeric ? { inputMode: "numeric" } : {}, ...props });
501
- const secretInput = (props) => react.createElement("input", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, type: "password", autoComplete: "off", ...props });
502
- const selectInput = (props) => react.createElement("select", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, ...props });
693
+ meta?.invalid ? react.createElement("p", { className: "dshws_field_error" }, meta.t("invalidNumber")) : hint ? react.createElement("p", { className: "dshws_field_hint" }, hint) : null);
694
+ const pair = (left, right) => {
695
+ if (!left && !right) return null;
696
+ if (!left) return right;
697
+ if (!right) return left;
698
+ return react.createElement("div", { className: "dshws_pair" }, left, right);
699
+ };
700
+ const controlStyle = { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5", width: "100%", boxSizing: "border-box" };
701
+ const textInput = (props) => react.createElement("input", { style: controlStyle, type: "text", ...props });
702
+ const numberInput = (props) => react.createElement("input", {
703
+ style: controlStyle,
704
+ type: "text",
705
+ inputMode: "numeric",
706
+ pattern: "[0-9]*",
707
+ autoComplete: "off",
708
+ ...props,
709
+ onChange: (event) => {
710
+ props.onChange?.({ target: { value: String(event.target.value ?? "").replace(/\D/g, "") } });
711
+ }
712
+ });
713
+ const secretInput = (props) => react.createElement("input", { style: controlStyle, type: "password", autoComplete: "off", ...props });
714
+ const selectInput = (props) => react.createElement("select", { style: controlStyle, ...props });
503
715
  const checkboxInput = (props) => react.createElement("input", { type: "checkbox", style: { width: "16px", height: "16px", accentColor: "var(--dsw-alias-brand-primary)" }, ...props });
504
716
 
717
+ const CSS_ID = "dsh-web-search-plugin/styles.css#maxresults";
718
+ const GLOBE_MASK = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zM2.1 8.75h2.55c.12 1.58.48 3 1 4.12A6.02 6.02 0 0 1 2.1 8.75zm8.25 4.12c.52-1.12.88-2.54 1-4.12H13.9a6.02 6.02 0 0 1-3.55 4.12zM13.9 7.25h-2.55c-.12-1.58-.48-3-1-4.12A6.02 6.02 0 0 1 13.9 7.25zM5.65 3.13c-.52 1.12-.88 2.54-1 4.12H2.1a6.02 6.02 0 0 1 3.55-4.12zM8 2.2c.82 1.12 1.36 2.82 1.5 4.95H6.5C6.64 5.02 7.18 3.32 8 2.2zm0 11.6c-.82-1.12-1.36-2.82-1.5-4.95h3C9.36 10.98 8.82 12.68 8 13.8z'/%3E%3C/svg%3E\") 50%/contain no-repeat";
719
+ if (typeof document !== "undefined") {
720
+ for (const old of document.querySelectorAll('style[data-plugin="dsh-web-search-plugin"]')) old.remove();
721
+ const tag = document.createElement("style");
722
+ tag.dataset.plugin = "dsh-web-search-plugin";
723
+ tag.dataset.pluginCss = CSS_ID;
724
+ tag.textContent = [
725
+ "@keyframes dshws-toast-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}",
726
+ ".dshws_quota{border:1px solid var(--dsw-alias-border-l3,rgba(128,128,128,0.12));background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,0.04));border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;margin:4px 0 8px}",
727
+ ".dshws_quota_head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
728
+ ".dshws_quota_title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
729
+ ".dshws_quota_pct{font-variant-numeric:tabular-nums;font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);background:transparent;border:none;padding:0;font:inherit;font-weight:600;cursor:pointer}",
730
+ ".dshws_quota_pct:disabled{cursor:wait;opacity:.7}",
731
+ ".dshws_quota_track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,0.14));overflow:hidden}",
732
+ ".dshws_quota_fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-success-primary,#10b981);transition:width .2s ease}",
733
+ ".dshws_quota_fill_warning{background:var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warning-primary,#f59e0b))}",
734
+ ".dshws_quota_fill_danger{background:var(--dsw-alias-state-error-primary,#ef4444)}",
735
+ ".dshws_quota_meta{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
736
+ ".dshws_quota_hint{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.45;margin:0}",
737
+ ".dshws_quota_alert{font-size:11.5px;margin:0;line-height:1.45}",
738
+ ".dshws_quota_alert_warning{color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
739
+ ".dshws_quota_alert_danger{color:var(--dsw-alias-state-error-primary,#ef4444)}",
740
+ ".dshws_btn{appearance:none;font:inherit;cursor:pointer;border-radius:6px;padding:8px 16px;font-size:12.5px;font-weight:500;line-height:1.5;display:inline-flex;align-items:center;justify-content:center;border:1px solid transparent;transition:filter .15s ease,opacity .15s ease}",
741
+ ".dshws_btn:disabled{cursor:not-allowed;opacity:.6}",
742
+ ".dshws_btn_primary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#3b82f6));color:var(--dsw-alias-label-primary-foreground,#fff);font-weight:600}",
743
+ ".dshws_btn_primary:hover:not(:disabled){filter:brightness(1.12)}",
744
+ ".dshws_btn_secondary{background:var(--dsw-alias-button-tool-bar-fill,var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1)));color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2,var(--dsw-alias-border-secondary,rgba(128,128,128,.25)))}",
745
+ ".dshws_btn_secondary:hover:not(:disabled){background:var(--dsw-alias-button-tool-bar-hover,var(--dsw-alias-interactive-bg-active,rgba(128,128,128,.16)))}",
746
+ ".dshws_btn_outline{background:transparent;border-color:var(--dsw-alias-border-l2,var(--dsw-alias-border-secondary,rgba(128,128,128,.25)));color:var(--dsw-alias-label-secondary)}",
747
+ ".dshws_btn_outline:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1,rgba(128,128,128,.45));background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}",
748
+ ".dshws_toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--dsw-alias-state-success-primary,#10b981);color:#fff;padding:8px 18px;border-radius:999px;box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.3));font-size:12.5px;font-weight:500;z-index:100000;animation:dshws-toast-in .2s ease-out}",
749
+ ".dshws_toast_error{background:var(--dsw-alias-state-error-primary,#ef4444)}",
750
+ ".dshws_toast_info{background:var(--dsw-alias-label-secondary,#6b7280)}",
751
+ ".dshws_card_header{display:flex;align-items:flex-start;gap:12px;padding:6px 0 2px}",
752
+ ".dshws_card_headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}",
753
+ ".dshws_card_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
754
+ ".dshws_card_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
755
+ ".dshws_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,0.12));color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
756
+ ".dshws_form{display:flex;flex-direction:column;gap:0}",
757
+ ".dshws_field{display:flex;flex-direction:column;gap:4px;padding:10px 0;border-top:1px solid var(--dsw-alias-border-l2);min-width:0}",
758
+ ".dshws_field_head{display:flex;align-items:center;gap:8px}",
759
+ ".dshws_field_label{min-width:0;flex:1;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
760
+ ".dshws_field_badges{display:inline-flex;align-items:center;gap:8px;flex:none}",
761
+ ".dshws_field_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.45}",
762
+ ".dshws_field_error{color:var(--dsw-alias-state-error-primary,#ef4444)!important;margin:0;font-size:12px;line-height:1.45;font-weight:500}",
763
+ ".dshws_pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);column-gap:12px;border-top:1px solid var(--dsw-alias-border-l2)}",
764
+ ".dshws_pair>.dshws_field{border-top:none;padding:10px 0}",
765
+ ".dshws_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,0.12));color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
766
+ ".dshws_reset{appearance:none;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:transparent;border:none;padding:0;font-size:12px;line-height:1.5}",
767
+ ".dshws_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
768
+ ".dshws_reset:disabled{cursor:default;opacity:.45}",
769
+ ".dshws_input_invalid{border-color:var(--dsw-alias-state-error-primary,#ef4444)!important}",
770
+ "[data-dshws-nav]>[class*='_navIcon']{display:none}",
771
+ "[data-dshws-nav]:before{content:'';background:currentColor;flex:none;width:16px;height:16px;-webkit-mask:" + GLOBE_MASK + ";mask:" + GLOBE_MASK + "}"
772
+ ].join("\n");
773
+ document.head.appendChild(tag);
774
+ }
775
+
776
+ function interpolate(template, vars) {
777
+ return String(template ?? "").replace(/\{(\w+)\}/g, (_, key) => vars[key] == null ? "" : String(vars[key]));
778
+ }
779
+ function tFill(t, key, vars) {
780
+ return interpolate(t(key), vars);
781
+ }
782
+ function formatResetTime(ms) {
783
+ if (!Number.isFinite(ms) || ms <= 0) return "—";
784
+ const date = new Date(ms);
785
+ return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
786
+ }
787
+ function usedPercent(used, limit) {
788
+ if (!Number.isFinite(limit) || limit <= 0 || !Number.isFinite(used)) return 0;
789
+ return Math.max(0, Math.min(100, (100 * used) / limit));
790
+ }
791
+ function remainingLevel(used, limit) {
792
+ if (!Number.isFinite(limit) || limit <= 0) return "unknown";
793
+ const remainingPct = Math.max(0, (100 * (limit - used)) / limit);
794
+ if (remainingPct <= 10) return "danger";
795
+ if (remainingPct <= 20) return "warning";
796
+ return "success";
797
+ }
798
+
799
+ const USAGE_POLL_MS = 15000;
800
+ function createUsageClientStore() {
801
+ let snap = { status: "idle", payload: null, refreshing: false };
802
+ const listeners = new Set();
803
+ const publish = () => {
804
+ for (const listener of listeners) listener();
805
+ };
806
+ const load = async (force) => {
807
+ if (force) {
808
+ snap = { ...snap, refreshing: true };
809
+ publish();
810
+ }
811
+ try {
812
+ const response = await fetch(force ? "/dsh-web-search/usage?force=1" : "/dsh-web-search/usage", { cache: "no-store" });
813
+ const payload = await response.json();
814
+ snap = { status: "ok", payload, refreshing: false };
815
+ } catch (error) {
816
+ snap = { status: "error", payload: snap.payload, refreshing: false, error: String(error?.message ?? error) };
817
+ }
818
+ publish();
819
+ return snap;
820
+ };
821
+ return {
822
+ subscribe(listener) {
823
+ listeners.add(listener);
824
+ return () => listeners.delete(listener);
825
+ },
826
+ getSnapshot() {
827
+ return snap;
828
+ },
829
+ refresh: () => load(true),
830
+ start() {
831
+ void load(true);
832
+ const timer = setInterval(() => {
833
+ void load(false);
834
+ }, USAGE_POLL_MS);
835
+ return () => clearInterval(timer);
836
+ }
837
+ };
838
+ }
839
+ const usageStore = createUsageClientStore();
840
+
841
+ function QuotaBar(props) {
842
+ const { name, used, limit, remainingText, resetText, level, onRefresh, refreshing, t, extra } = props;
843
+ const pct = usedPercent(used, limit);
844
+ const hasLimit = Number.isFinite(limit) && limit > 0;
845
+ const fillClass = "dshws_quota_fill" + (level === "danger" ? " dshws_quota_fill_danger" : level === "warning" ? " dshws_quota_fill_warning" : "");
846
+ return react.createElement("div", { className: "dshws_quota" },
847
+ react.createElement("div", { className: "dshws_quota_head" },
848
+ react.createElement("span", { className: "dshws_quota_title" }, name),
849
+ react.createElement("button", {
850
+ type: "button",
851
+ className: "dshws_quota_pct",
852
+ disabled: refreshing,
853
+ title: refreshing ? t("quotaRefreshing") : t("quotaRefresh"),
854
+ onClick: onRefresh
855
+ }, hasLimit ? Math.round(pct) + "%" : (Number.isFinite(used) && used > 0 ? String(used) : "—"))),
856
+ react.createElement("div", { className: "dshws_quota_track" },
857
+ react.createElement("div", { className: fillClass, style: { width: (hasLimit ? pct : 0) + "%" } })),
858
+ react.createElement("div", { className: "dshws_quota_meta" },
859
+ react.createElement("span", null, remainingText),
860
+ react.createElement("span", null, resetText)),
861
+ level === "warning" || level === "danger"
862
+ ? react.createElement("p", { className: "dshws_quota_alert dshws_quota_alert_" + level }, t(level === "danger" ? "quotaDanger" : "quotaWarn"))
863
+ : null,
864
+ extra);
865
+ }
866
+
867
+ function QuotaCard(props) {
868
+ const { t, provider, keyed } = props;
869
+ const usage = react.useSyncExternalStore(usageStore.subscribe, usageStore.getSnapshot, usageStore.getSnapshot);
870
+ const showTavily = provider === "tavily" && keyed;
871
+ const showBrave = provider === "brave";
872
+ if (!showTavily && !showBrave) return null;
873
+ const payload = usage.payload;
874
+ const refreshing = usage.refreshing === true;
875
+ const onRefresh = (event) => {
876
+ event.preventDefault();
877
+ void usageStore.refresh();
878
+ };
879
+ if (showTavily) {
880
+ const info = payload?.tavily;
881
+ const error = info?.error || (usage.status === "error" ? usage.error : null);
882
+ if (!info || info.fetchedAt === 0 && info.used === 0 && !error) {
883
+ return react.createElement("div", { className: "dshws_quota" },
884
+ react.createElement("div", { className: "dshws_quota_head" },
885
+ react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
886
+ react.createElement("button", { type: "button", className: "dshws_quota_pct", disabled: refreshing, onClick: onRefresh }, refreshing ? t("quotaRefreshing") : t("quotaRefresh"))),
887
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")),
888
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaKeyedHint")));
889
+ }
890
+ const used = Number(info.used) || 0;
891
+ const limit = info.limit;
892
+ const remaining = info.remaining;
893
+ const level = remainingLevel(used, limit);
894
+ const name = info.plan ? tFill(t, "quotaPlan", { plan: info.plan }) : t("quotaTitle");
895
+ const remainingText = Number.isFinite(limit) && limit > 0
896
+ ? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
897
+ : used > 0
898
+ ? tFill(t, "quotaUsedOnly", { used }) + " · " + t("quotaUnlimited")
899
+ : t("quotaUnlimited");
900
+ return react.createElement(QuotaBar, {
901
+ name,
902
+ used,
903
+ limit,
904
+ remainingText,
905
+ resetText: t("quotaCycle"),
906
+ level,
907
+ onRefresh,
908
+ refreshing,
909
+ t,
910
+ extra: [
911
+ info.paygoUsed > 0 ? react.createElement("p", { className: "dshws_quota_hint", key: "paygo" }, tFill(t, "quotaPaygo", { used: info.paygoUsed, limit: info.paygoLimit ?? "—" })) : null,
912
+ error ? react.createElement("p", { className: "dshws_quota_hint", key: "err" }, tFill(t, "quotaError", { error })) : react.createElement("p", { className: "dshws_quota_hint", key: "hint" }, t("quotaKeyedHint"))
913
+ ]
914
+ });
915
+ }
916
+ const brave = payload?.brave;
917
+ const monthly = brave?.monthly;
918
+ const burst = brave?.burst;
919
+ if (!monthly && !burst) {
920
+ return react.createElement("div", { className: "dshws_quota" },
921
+ react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
922
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")));
923
+ }
924
+ const monthlyCapped = Number.isFinite(monthly?.limit) && monthly.limit > 0;
925
+ const primary = monthlyCapped ? monthly : (burst ?? monthly);
926
+ const used = Number(primary.used) || 0;
927
+ const limit = primary.limit;
928
+ const remaining = primary.remaining;
929
+ const level = remainingLevel(used, limit);
930
+ return react.createElement(QuotaBar, {
931
+ name: monthlyCapped ? t("quotaTitle") : t("quotaBraveCapacity"),
932
+ used,
933
+ limit,
934
+ remainingText: monthlyCapped
935
+ ? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
936
+ : tFill(t, "quotaBraveRps", { limit: limit ?? 0 }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? 0 }),
937
+ resetText: monthlyCapped
938
+ ? tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) })
939
+ : t("quotaBraveUnlimited") + (monthly?.resetAt ? " · " + tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) }) : ""),
940
+ level,
941
+ onRefresh,
942
+ refreshing,
943
+ t,
944
+ extra: monthlyCapped && burst
945
+ ? react.createElement("p", { className: "dshws_quota_hint", key: "burst" }, tFill(t, "quotaBurst", { remaining: burst.remaining, limit: burst.limit }))
946
+ : null
947
+ });
948
+ }
949
+
950
+ function toastCopy(t, key) {
951
+ if (key === "saved") return t("savedToast");
952
+ if (key === "nothing") return t("nothingToSave");
953
+ if (key === "invalid") return t("invalidToast");
954
+ if (key === "failed") return t("saveFailed");
955
+ return "";
956
+ }
957
+
505
958
  /** Render the provider selector plus the backend-specific staged form. */
506
959
  function WebSearchCard(props) {
507
960
  const { t, state, actions } = props;
508
961
  if (!state.available) return null;
509
962
  const disabled = !state.writable;
510
- const blocked = !state.dirty || state.invalid || state.saving;
511
- const provider = state.provider.text === "brave" ? "brave" : state.provider.text === "deepseek-official" ? "deepseek-official" : "tavily";
963
+ const provider = state.provider.text === "tavily" || state.provider.text === "brave" || state.provider.text === "deepseek-official" || builtinRest(state.provider.text) !== void 0 ? state.provider.text : "deepseek-official";
512
964
  const brave = provider === "brave";
513
965
  const deepseek = provider === "deepseek-official";
966
+ const tavily = provider === "tavily";
967
+ const restMeta = builtinRest(provider);
514
968
  const keyed = state.mode.text === "keyed";
515
- return react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px" } },
516
- !state.writable ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: "0", fontSize: "12px", lineHeight: "1.5" } }, t("readOnly")) : null,
517
- fieldRow(t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
518
- react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
519
- react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
520
- react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek"))
521
- ] })),
522
- deepseek ? null : brave ? null : fieldRow(t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
523
- react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
524
- react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
525
- ] })),
526
- // API key + credential reference for the visible backend.
527
- deepseek ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })) : null,
528
- deepseek ? fieldRow(t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value), onReset: () => actions.resetField("deepseekApiKeyEnv") })) : null,
529
- brave ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })) : null,
530
- brave ? fieldRow(t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value), onReset: () => actions.resetField("braveApiKeyEnv") })) : null,
531
- (!brave && !deepseek && keyed) ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })) : null,
532
- (!brave && !deepseek && keyed) ? fieldRow(t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value), onReset: () => actions.resetField("apiKeyEnv") })) : null,
533
- // Endpoint.
534
- deepseek ? fieldRow(t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value), onReset: () => actions.resetField("deepseekBaseURL") })) : null,
535
- brave ? fieldRow(t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value), onReset: () => actions.resetField("braveBaseURL") })) : null,
536
- (!brave && !deepseek) ? fieldRow(t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value), onReset: () => actions.resetField("baseURL") })) : null,
537
- // DeepSeek options.
538
- deepseek ? fieldRow(t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value), onReset: () => actions.resetField("model") })) : null,
539
- deepseek ? fieldRow(t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value), onReset: () => actions.resetField("apiVersion") })) : null,
540
- deepseek ? fieldRow(t("maxTokens"), t("maxTokensHint"), textInput({ id: "ws-maxtokens", numeric: true, value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), onReset: () => actions.resetField("maxTokens") })) : null,
541
- deepseek ? fieldRow(t("maxUses"), t("maxUsesHint"), textInput({ id: "ws-maxuses", numeric: true, value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), onReset: () => actions.resetField("maxUses") })) : null,
542
- // Tavily options.
543
- (!brave && !deepseek) ? fieldRow(t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
544
- react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
545
- react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
546
- ] })) : null,
547
- (!brave && !deepseek) ? fieldRow(t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
548
- react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
549
- react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
550
- ] })) : null,
551
- (!brave && !deepseek) ? fieldRow(t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })) : null,
552
- // Brave options.
553
- brave ? fieldRow(t("country"), t("countryHint"), textInput({ id: "ws-country", value: state.country.text, disabled, onChange: (e) => actions.edit("country", e.target.value), onReset: () => actions.resetField("country") })) : null,
554
- brave ? fieldRow(t("searchLang"), t("searchLangHint"), textInput({ id: "ws-lang", value: state.searchLang.text, disabled, onChange: (e) => actions.edit("searchLang", e.target.value), onReset: () => actions.resetField("searchLang") })) : null,
555
- brave ? fieldRow(t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
556
- react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
557
- react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
558
- react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
559
- react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
560
- react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
561
- ] })) : null,
562
- brave ? fieldRow(t("proxy"), t("proxyHint"), textInput({ id: "ws-proxy", value: state.proxy.text, disabled, onChange: (e) => actions.edit("proxy", e.target.value), onReset: () => actions.resetField("proxy") })) : null,
563
- fieldRow(t("maxResults"), t("maxResultsHint"), textInput({ id: "ws-max", numeric: true, value: state.maxResults.text, disabled, onChange: (e) => actions.edit("maxResults", e.target.value), onReset: () => actions.resetField("maxResults") })),
969
+ const keyHint = state.apiKeyConfigured ? t("apiKeyHint") : t("apiKeyHint") + " " + t("apiKeyUnavailableHint");
970
+ const toastText = toastCopy(t, state.toastKey);
971
+ const toastTone = state.toastKey === "failed" || state.toastKey === "invalid" ? "error" : state.toastKey === "nothing" ? "info" : "success";
972
+ const row = (field, label, hint, children) => fieldRow(label, hint, children, {
973
+ t,
974
+ overridden: state[field]?.overridden === true,
975
+ invalid: state[field]?.invalid === true,
976
+ disabled,
977
+ onReset: () => actions.resetField(field)
978
+ });
979
+ const maxResultsWrite = numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }).parse(String(state.maxResults.text ?? "").trim());
980
+ const maxResultsValue = maxResultsWrite?.kind === "set" ? String(maxResultsWrite.value) : "8";
981
+ const maxResultsRow = row("maxResults", t("maxResults"), null, selectInput({
982
+ id: "ws-max",
983
+ value: maxResultsValue,
984
+ disabled,
985
+ className: state.maxResults.invalid ? "dshws_input_invalid" : void 0,
986
+ onChange: (e) => actions.edit("maxResults", e.target.value),
987
+ children: Array.from({ length: MAX_RESULTS_CAP }, (_, index) => {
988
+ const n = String(index + 1);
989
+ return react.createElement("option", { value: n, key: n }, n);
990
+ })
991
+ }));
992
+ return react.createElement("div", { className: "dshws_form" },
993
+ react.createElement("div", { className: "dshws_card_header" },
994
+ react.createElement("div", { className: "dshws_card_headText" },
995
+ react.createElement("span", { className: "dshws_card_name" }, t("nav"))),
996
+ state.dirty || state.failed ? react.createElement("span", { className: "dshws_pending" }, t("unsaved")) : null),
997
+ !state.writable ? react.createElement("p", { className: "dshws_field_hint", style: { padding: "8px 0 0" } }, t("readOnly")) : null,
998
+ pair(
999
+ row("provider", t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
1000
+ react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
1001
+ react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
1002
+ react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek")),
1003
+ ...BUILTIN_REST_PROVIDERS.map((entry) => react.createElement("option", { value: entry.id, key: entry.id }, entry.name)),
1004
+ react.createElement("option", { value: "__ddg__", key: "ddg", disabled: true }, t("ddgDisabled"))
1005
+ ] }),
1006
+ react.createElement("p", { className: "dshws_field_hint", style: { padding: "4px 0 0" } }, t("ddgHint"))),
1007
+ tavily ? row("mode", t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
1008
+ react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
1009
+ react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
1010
+ ] })) : null
1011
+ ),
1012
+ react.createElement(QuotaCard, { t, provider, keyed }),
1013
+ deepseek ? pair(
1014
+ fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })),
1015
+ row("deepseekApiKeyEnv", t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value) }))
1016
+ ) : null,
1017
+ brave ? pair(
1018
+ fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })),
1019
+ row("braveApiKeyEnv", t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value) }))
1020
+ ) : null,
1021
+ (tavily && keyed) ? pair(
1022
+ fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })),
1023
+ row("apiKeyEnv", t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value) }))
1024
+ ) : null,
1025
+ deepseek ? row("deepseekBaseURL", t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value) })) : null,
1026
+ brave ? row("braveBaseURL", t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value) })) : null,
1027
+ tavily ? row("baseURL", t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value) })) : null,
1028
+ deepseek ? pair(
1029
+ row("model", t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value) })),
1030
+ row("apiVersion", t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value) }))
1031
+ ) : null,
1032
+ deepseek ? pair(
1033
+ row("maxTokens", t("maxTokens"), t("maxTokensHint"), numberInput({ id: "ws-maxtokens", value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), className: state.maxTokens.invalid ? "dshws_input_invalid" : void 0 })),
1034
+ row("maxUses", t("maxUses"), t("maxUsesHint"), numberInput({ id: "ws-maxuses", value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), className: state.maxUses.invalid ? "dshws_input_invalid" : void 0 }))
1035
+ ) : null,
1036
+ tavily ? pair(
1037
+ row("searchDepth", t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
1038
+ react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
1039
+ react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
1040
+ ] })),
1041
+ row("topic", t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
1042
+ react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
1043
+ react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
1044
+ ] }))
1045
+ ) : null,
1046
+ tavily ? pair(
1047
+ row("includeAnswer", t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })),
1048
+ maxResultsRow
1049
+ ) : null,
1050
+ brave ? pair(
1051
+ row("country", t("country"), t("countryHint"), textInput({ id: "ws-country", value: state.country.text, disabled, placeholder: t("braveDefaultPlaceholder"), onChange: (e) => actions.edit("country", e.target.value) })),
1052
+ row("searchLang", t("searchLang"), t("searchLangHint"), textInput({ id: "ws-lang", value: state.searchLang.text, disabled, placeholder: t("braveDefaultPlaceholder"), onChange: (e) => actions.edit("searchLang", e.target.value) }))
1053
+ ) : null,
1054
+ brave ? pair(
1055
+ row("freshness", t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
1056
+ react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
1057
+ react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
1058
+ react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
1059
+ react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
1060
+ react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
1061
+ ] })),
1062
+ maxResultsRow
1063
+ ) : null,
1064
+ brave ? row("proxy", t("proxy"), null, textInput({ id: "ws-proxy", value: state.proxy.text, disabled, placeholder: t("proxyPlaceholder"), onChange: (e) => actions.edit("proxy", e.target.value) })) : null,
1065
+ deepseek ? maxResultsRow : null,
1066
+ restMeta ? react.createElement("div", { className: "dshws_form" },
1067
+ restMeta.keyRequired && restMeta.officialUrl ? react.createElement("a", {
1068
+ href: restMeta.officialUrl,
1069
+ target: "_blank",
1070
+ rel: "noopener noreferrer",
1071
+ className: "dshws_field_hint",
1072
+ style: { color: "var(--dsw-alias-brand-primary,#3b82f6)", textDecoration: "none", padding: "8px 0", display: "inline-block" }
1073
+ }, t("getKey") + " ↗") : null,
1074
+ restMeta.keyRequired ? pair(
1075
+ fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-" + restMeta.id + "-key", value: state[restMeta.keyField]?.text ?? "", disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(restMeta.keyField, e.target.value) })),
1076
+ row(restMeta.keyEnvField, t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-" + restMeta.id + "-keyref", value: state[restMeta.keyEnvField]?.text ?? "", disabled, onChange: (e) => actions.edit(restMeta.keyEnvField, e.target.value) }))
1077
+ ) : null,
1078
+ row(restMeta.baseURLField, t("baseURL"), t("baseURLHint"), textInput({ id: "ws-" + restMeta.id + "-base", value: state[restMeta.baseURLField]?.text ?? "", disabled, onChange: (e) => actions.edit(restMeta.baseURLField, e.target.value) })),
1079
+ maxResultsRow
1080
+ ) : null,
564
1081
  react.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", justifyContent: "flex-end", alignItems: "center", gap: "8px", padding: "12px 0 4px", display: "flex" } },
565
- state.failed ? react.createElement("p", { role: "status", style: { minWidth: 0, color: "var(--dsw-alias-label-error)", flex: 1, margin: 0, fontSize: "12px", lineHeight: "1.5" } }, t("saveFailed")) : null,
566
- react.createElement("button", { type: "button", disabled: !state.dirty || state.saving, onClick: actions.discard, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-secondary)", background: "0 0" } }, t("discard")),
567
- react.createElement("button", { type: "button", disabled: blocked, onClick: actions.save, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid transparent", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-on-brand)", background: "var(--dsw-alias-bg-brand-solid)" } }, t(state.saving ? "saving" : "save"))));
1082
+ react.createElement("button", {
1083
+ type: "button",
1084
+ className: "dshws_btn dshws_btn_secondary",
1085
+ disabled: (!state.dirty && !state.failed) || state.saving,
1086
+ onClick: (event) => {
1087
+ event.preventDefault();
1088
+ actions.discard();
1089
+ }
1090
+ }, t("discard")),
1091
+ react.createElement("button", {
1092
+ type: "button",
1093
+ className: "dshws_btn dshws_btn_primary",
1094
+ disabled: state.saving || !state.writable,
1095
+ onClick: (event) => {
1096
+ event.preventDefault();
1097
+ void actions.save();
1098
+ }
1099
+ }, t(state.saving ? "saving" : "save"))),
1100
+ toastText ? react.createElement("div", { className: "dshws_toast" + (toastTone === "error" ? " dshws_toast_error" : toastTone === "info" ? " dshws_toast_info" : ""), role: "status" }, toastText) : null);
568
1101
  }
569
1102
 
570
1103
  /** Section component: the "Web search" page body, driven by useSyncExternalStore. */
@@ -579,6 +1112,32 @@ window.__ModuleLoader__.load({
579
1112
  };
580
1113
  }
581
1114
 
1115
+ function syncSettingsNavIcon() {
1116
+ if (typeof document === "undefined" || typeof document.querySelectorAll !== "function") return;
1117
+ const lists = document.querySelectorAll("[class*='_navList']");
1118
+ for (const list of lists) {
1119
+ const cells = list.querySelectorAll(":scope > [class*='_navCell']");
1120
+ for (const cell of cells) {
1121
+ const text = String(cell.textContent ?? "").replace(/\s+/g, " ").trim();
1122
+ if (text === "网页搜索" || text === "Web search") cell.setAttribute("data-dshws-nav", "web-search");
1123
+ else cell.removeAttribute("data-dshws-nav");
1124
+ }
1125
+ }
1126
+ }
1127
+ function startSettingsNavIconSync() {
1128
+ if (typeof document === "undefined") return () => {};
1129
+ const run = () => {
1130
+ try { syncSettingsNavIcon(); } catch { /* settings sheet not mounted yet */ }
1131
+ };
1132
+ run();
1133
+ if (typeof MutationObserver !== "function") return () => {};
1134
+ const root = document.body || document.documentElement;
1135
+ if (!root) return () => {};
1136
+ const observer = new MutationObserver(run);
1137
+ observer.observe(root, { childList: true, subtree: true, characterData: true });
1138
+ return () => observer.disconnect();
1139
+ }
1140
+
582
1141
  /** Mount the top-level "Web search" settings section. */
583
1142
  function apply(ctx) {
584
1143
  const { api } = ctx.get("connection");
@@ -588,6 +1147,8 @@ window.__ModuleLoader__.load({
588
1147
  ctx.effect(() => ctx.remote.$on("credentials/updated", (ref) => {
589
1148
  controller.form.refreshCredential(ref);
590
1149
  }), "dsh-web-search-plugin: credential invalidations");
1150
+ ctx.effect(() => usageStore.start(), "dsh-web-search-plugin: usage poll");
1151
+ ctx.effect(() => startSettingsNavIconSync(), "dsh-web-search-plugin: settings nav icon");
591
1152
  ctx.slots.inject("settings.section", () => ctx.slots.register({
592
1153
  name: "settings.section",
593
1154
  id: "web-search",