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

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
@@ -166,8 +166,382 @@ window.__ModuleLoader__.load({
166
166
  var module = { exports: {} };
167
167
  var exports = module.exports;
168
168
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
169
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
169
170
  let react = require("react");
170
171
  let react_jsx_runtime = require("react/jsx-runtime");
172
+ Object.values({
173
+ /** Surfaces, from the page up to the topmost nested card. */
174
+ surface: [
175
+ "--dsw-alias-bg-base",
176
+ "--dsw-alias-bg-layer-1",
177
+ "--dsw-alias-bg-layer-2",
178
+ "--dsw-alias-bg-layer-3",
179
+ "--dsw-alias-bg-layer-4",
180
+ "--dsw-alias-bg-module-platform",
181
+ "--dsw-alias-bg-overlay"
182
+ ],
183
+ /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
184
+ border: [
185
+ "--dsw-alias-border-l1",
186
+ "--dsw-alias-border-l2",
187
+ "--dsw-alias-border-l3",
188
+ "--dsw-alias-border-l4"
189
+ ],
190
+ /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
191
+ label: [
192
+ "--dsw-alias-label-primary",
193
+ "--dsw-alias-label-secondary",
194
+ "--dsw-alias-label-tertiary",
195
+ "--dsw-alias-label-caption",
196
+ "--dsw-alias-label-dimmed",
197
+ "--dsw-alias-label-error",
198
+ "--dsw-alias-label-primary-foreground"
199
+ ],
200
+ /** Interactive fills. The `-danger` hover belongs to destructive rows. */
201
+ interactive: [
202
+ "--dsw-alias-interactive-bg-hover",
203
+ "--dsw-alias-interactive-bg-active",
204
+ "--dsw-alias-interactive-bg-hover-danger"
205
+ ],
206
+ /** Accent and status. `business` is the focus ring's default colour. */
207
+ accent: [
208
+ "--dsw-alias-brand-primary",
209
+ "--dsw-alias-button-primary-fill",
210
+ "--dsw-alias-button-primary-hover",
211
+ "--dsw-alias-button-tool-bar-fill",
212
+ "--dsw-alias-button-tool-bar-hover",
213
+ "--dsw-alias-button-ghost-active-fill",
214
+ "--dsw-alias-button-ghost-active-border"
215
+ ],
216
+ state: [
217
+ "--dsw-alias-state-business-primary",
218
+ "--dsw-alias-state-success-primary",
219
+ "--dsw-alias-state-warn-primary",
220
+ "--dsw-alias-state-error-primary",
221
+ "--dsw-alias-state-idle-primary"
222
+ ],
223
+ /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
224
+ radius: [
225
+ "--dsw-radius-sm",
226
+ "--dsw-radius-md",
227
+ "--dsw-radius-lg",
228
+ "--dsw-radius-panel"
229
+ ],
230
+ /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
231
+ elevation: [
232
+ "--dsw-elevation-soft",
233
+ "--dsw-elevation-stroke",
234
+ "--dsw-elevation-stroke-color",
235
+ "--dsw-elevation-panel",
236
+ "--dsw-elevation-prominent"
237
+ ],
238
+ /** Floats, masks and scrollbars. */
239
+ overlay: [
240
+ "--dsw-menu-surface-fill",
241
+ "--dsw-menu-backdrop-filter",
242
+ "--dsw-mask-blur",
243
+ "--dsw-alias-bg-mask-1",
244
+ "--dsw-alias-menu-icon",
245
+ "--dsw-alias-scrollbar-bg-l2",
246
+ "--dsw-alias-scrollbar-hover-l2",
247
+ "--dsw-shadow-lv3"
248
+ ],
249
+ /** Motion, focus and type axis, published by the host rather than by a component. */
250
+ system: [
251
+ "--dsw-focus-ring-width",
252
+ "--dsw-focus-ring-color",
253
+ "--dsw-font-markdown-code-font-family",
254
+ "--ds-transition-duration",
255
+ "--ds-ease-in-out",
256
+ "--ds-font-family-code",
257
+ "--dsh-scrollbar-thumb",
258
+ "--dsh-scrollbar-thumb-hover",
259
+ "--dsh-content-font-size-secondary",
260
+ "--dsh-content-font-delta",
261
+ "--dsh-frame-top-clearance"
262
+ ]
263
+ }).flat();
264
+ /**
265
+ * The host's focus ring shorthand, verbatim. Writing this per control is how
266
+ * rings drift apart; a stylesheet that draws its own focus style should use
267
+ * {@link officialFocus} instead. The colour fallback is the host's own default.
268
+ */
269
+ const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
270
+ /**
271
+ * A complete `outline` declaration at the official geometry. `offset` defaults
272
+ * to the 2px the native controls use; the host's inline field rules sit at 1px
273
+ * and a segmented tab pulls the ring inside itself at -2px.
274
+ */
275
+ function officialFocus(offset = "2px") {
276
+ return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
277
+ }
278
+ /**
279
+ * An elevated surface. The host draws a raised surface's edge inside its
280
+ * shadow rather than as a border, so a card sets `border: 0` and takes the edge
281
+ * from the elevation — which is also why its border stays off the alias-border
282
+ * set that a flat, non-elevated rule would use.
283
+ */
284
+ function officialElevation(level) {
285
+ return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
286
+ }
287
+ /**
288
+ * The shared stylesheet for a plugin UI, scoped under that plugin's root class.
289
+ *
290
+ * Scoping is what keeps this safe to always-bundle into every client: the rules
291
+ * can only match inside the plugin's own subtree, so two plugins can both mount
292
+ * `dsh-ui-card` without either styling the other's markup.
293
+ *
294
+ * Each recipe matches the root element *and* its descendants, because the
295
+ * commonest arrangement is one element that is both — a surface that carries
296
+ * the root class and `dsh-ui-card`, or a portaled dialog that is the root and
297
+ * the `dsh-ui-surface`. A descendant-only scope would silently leave those
298
+ * unstyled, with no error anywhere.
299
+ *
300
+ * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
301
+ * content leaves the plugin's subtree entirely. A portaled surface therefore
302
+ * needs its own root class on an element inside the portal.
303
+ *
304
+ * Only patterns that primitives do not provide live here. If a control appears
305
+ * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
306
+ * bug in this file: use the primitive and delete the recipe.
307
+ */
308
+ function officialUiCss(roots) {
309
+ const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
310
+ return `
311
+ /* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
312
+ * Do not hand-edit the copy inside a plugin: change this module so every plugin
313
+ * moves together. Scope: ${scope} */
314
+ /* The scope rule below stays open for the whole sheet: every rule after it
315
+ * nests with the parent selector, so one stylesheet can cover several unrelated
316
+ * roots without leaking into a neighbouring plugin. Its closing brace is last. */
317
+ ${scope} {
318
+ color: var(--dsw-alias-label-primary);
319
+ font-family: inherit;
320
+ font-size: 14px;
321
+ line-height: 22px;
322
+ min-width: 0;
323
+
324
+ & *, & *::before, & *::after { box-sizing: border-box; }
325
+ & h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
326
+ /* Keyboard focus only: pointer input keeps the native resting appearance. The
327
+ * ring needs no radius of its own — the outline already follows the control's
328
+ * own border-radius, and overriding it here would flatten every control. */
329
+ & :focus-visible { ${officialFocus()}; }
330
+ & [hidden] { display: none !important; }
331
+
332
+ /* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
333
+ & .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
334
+ & .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
335
+ & .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
336
+ & .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
337
+ & .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
338
+ & .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
339
+ & .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
340
+ & .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
341
+ & .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
342
+ & .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
343
+ & .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
344
+
345
+ /* --- Layout scaffolding. --- */
346
+ & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
347
+ & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
348
+ & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
349
+ & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
350
+ & .dsh-ui-spacer { flex: 1; min-width: 0; }
351
+ /* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
352
+ & .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
353
+ & .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
354
+
355
+ /* --- Card: a raised surface whose edge lives in the shadow. --- */
356
+ & .dsh-ui-card {
357
+ ${officialElevation("stroke")}
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 12px;
361
+ padding: 16px;
362
+ border-radius: var(--dsw-radius-lg);
363
+ background: var(--dsw-alias-bg-layer-1);
364
+ color: var(--dsw-alias-label-primary);
365
+ min-width: 0;
366
+ }
367
+ & .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
368
+ & .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
369
+ & .dsh-ui-card--plain { background: none; }
370
+
371
+ /* --- Field: label, control, help, error. The control itself is an
372
+ * official primitive, so this only owns the vertical rhythm. --- */
373
+ & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
374
+ & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
375
+ & .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); }
376
+ & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
377
+ & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
378
+ & .dsh-ui-help p { margin: 0; }
379
+ & .dsh-ui-help p + p { margin-top: 8px; }
380
+ & .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
381
+ & .dsh-ui-control { width: 100%; min-width: 0; }
382
+ /* An invalid control is marked on the control, not only in the message. */
383
+ & .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
384
+ & .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
385
+ & .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
386
+ & .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
387
+
388
+ /* --- Buttons row. A no-op container: it exists so actions line up on the
389
+ * trailing edge the way every native panel does. --- */
390
+ & .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
391
+ & .dsh-ui-actions-end { justify-content: flex-end; }
392
+
393
+ /* --- Banners. A banner is a left-rail notice, the shape used for warnings,
394
+ * deprecations and blocking instructions. --- */
395
+ & .dsh-ui-banner {
396
+ display: flex;
397
+ flex-direction: column;
398
+ gap: 8px;
399
+ padding: 10px 14px;
400
+ border: 0.5px solid var(--dsw-alias-border-l3);
401
+ border-left: 3px solid var(--dsw-alias-state-warn-primary);
402
+ border-radius: var(--dsw-radius-md);
403
+ background: var(--dsw-alias-bg-layer-1);
404
+ font-size: 13px;
405
+ line-height: 20px;
406
+ color: var(--dsw-alias-label-secondary);
407
+ }
408
+ & .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
409
+ & .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
410
+ & .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
411
+ & .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
412
+ & .dsh-ui-banner p { margin: 0; }
413
+
414
+ /* --- Inline notice: a transient confirmation with no chrome of its own. --- */
415
+ & .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
416
+ & .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
417
+
418
+ /* --- Empty and loading states. Both sit in a dashed well so an empty panel
419
+ * reads as "nothing here yet" rather than as a broken layout. --- */
420
+ & .dsh-ui-empty {
421
+ display: flex;
422
+ flex-direction: column;
423
+ align-items: flex-start;
424
+ gap: 6px;
425
+ padding: 20px 16px;
426
+ border: 0.5px dashed var(--dsw-alias-border-l3);
427
+ border-radius: var(--dsw-radius-md);
428
+ font-size: 13px;
429
+ line-height: 20px;
430
+ color: var(--dsw-alias-label-secondary);
431
+ }
432
+ & .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
433
+
434
+ /* --- Selectable list. A row is a button-sized target with a hover fill and
435
+ * a selected fill; the name carries the weight, the description does not. --- */
436
+ & .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
437
+ & .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
438
+ & .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
439
+ & .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
440
+ & .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
441
+ & .dsh-ui-list-item {
442
+ display: flex;
443
+ flex-direction: column;
444
+ align-items: flex-start;
445
+ gap: 4px;
446
+ width: 100%;
447
+ min-width: 0;
448
+ padding: 10px 12px;
449
+ border: 0;
450
+ border-radius: var(--dsw-radius-md);
451
+ background: none;
452
+ font: inherit;
453
+ color: inherit;
454
+ text-align: start;
455
+ cursor: pointer;
456
+ }
457
+ & .dsh-ui-list-item:disabled { cursor: default; }
458
+ & .dsh-ui-list-item:disabled:hover { background: none; }
459
+ & .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
460
+ & .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
461
+
462
+ /* --- Code and structured output. Wraps rather than scrolls, because a
463
+ * feature panel is read rather than scanned. --- */
464
+ & .dsh-ui-code {
465
+ padding: 10px 12px;
466
+ border: 0.5px solid var(--dsw-alias-border-l3);
467
+ border-radius: var(--dsw-radius-md);
468
+ background: var(--dsw-alias-bg-layer-1);
469
+ font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
470
+ font-size: 12px;
471
+ line-height: 20px;
472
+ color: var(--dsw-alias-label-secondary);
473
+ overflow-wrap: anywhere;
474
+ white-space: pre-wrap;
475
+ }
476
+ & .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
477
+
478
+ /* --- A toggle row: label on the left, switch on the trailing edge. This is
479
+ * layout only; the switch itself is the official primitive. --- */
480
+ & .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
481
+ & .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
482
+ & .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
483
+ & .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
484
+
485
+ /* --- Floating surface: a panel anchored to a trigger. This is the material
486
+ * a menu card is made of — the translucent fill plus its backdrop blur, with
487
+ * the edge riding inside the prominent elevation. A plugin that hand-rolls this
488
+ * is how tokens like --dsw-specific-menu get invented; use this instead. --- */
489
+ & .dsh-ui-surface {
490
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
491
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
492
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
493
+ position: relative;
494
+ display: flex;
495
+ flex-direction: column;
496
+ border: 0;
497
+ border-radius: var(--dsw-radius-lg);
498
+ background: var(--dsw-menu-surface-fill);
499
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
500
+ box-shadow: var(--dsw-elevation-prominent);
501
+ color: var(--dsw-alias-label-primary);
502
+ font-size: 13px;
503
+ line-height: 20px;
504
+ min-width: 0;
505
+ }
506
+ & .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
507
+ & .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
508
+ & .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
509
+
510
+ /* --- Native select. The primitives ship no select, so a plugin that offers a
511
+ * closed list of choices keeps the platform control and matches the official
512
+ * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
513
+ * SegmentedControl is the primitive to reach for when the choices are few
514
+ * enough to show at once. --- */
515
+ & .dsh-ui-select {
516
+ box-sizing: border-box;
517
+ width: 100%;
518
+ min-width: 0;
519
+ height: 34px;
520
+ padding: 0 12px;
521
+ border: 0.5px solid var(--dsw-alias-border-l4);
522
+ border-radius: var(--dsw-radius-md);
523
+ background: var(--dsw-alias-bg-layer-3);
524
+ font: inherit;
525
+ font-size: 13px;
526
+ line-height: 1.5;
527
+ color: var(--dsw-alias-label-primary);
528
+ cursor: pointer;
529
+ }
530
+ & .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
531
+ & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
532
+ & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
533
+
534
+ /* Long panels scroll inside themselves instead of growing the host page. */
535
+ & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
536
+ & .dsh-ui-panel-wide { max-width: 100%; }
537
+
538
+ @media (prefers-reduced-motion: reduce) {
539
+ & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
540
+ }
541
+ }
542
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
543
+ }
544
+ //#endregion
171
545
  //#region src/provider-info.ts
172
546
  const PROVIDER_PRICING_CHECKED = "2026-09-21";
173
547
  const providerInfo = {
@@ -188,12 +562,12 @@ const providerInfo = {
188
562
  },
189
563
  "deepseek-official": {
190
564
  description: "由 DeepSeek 模型调用内置搜索,复用模型 API Key。",
191
- pricing: "默认 Flash 按 tokens 计费:每百万输入 ¥1–2(缓存命中 ¥0.02–0.04)、输出 ¥4–8,随峰谷时段变化;赠送余额和搜索工具费用以平台为准。",
565
+ pricing: "默认 Flash 按 tokens 计费:每百万输入 ¥1–2(缓存命中 ¥0.02–0.04)、输出 ¥4–8,随峰谷时段变化;赠送余额与工具费用以平台为准。",
192
566
  pricingUrl: "https://api-docs.deepseek.com/zh-cn/quick_start/pricing/"
193
567
  },
194
568
  exa: {
195
569
  description: "面向 AI 的网页搜索,提供相关网页与内容片段。",
196
- pricing: "注册赠 $20,此后每月赠 $10;$7/千次(每次最多 10 条),超过 10 条每多一条加 $1/千次。",
570
+ pricing: "注册赠 $20,之后每月赠 $10;$7/千次(每次最多 10 条),超出 10 条每条加 $1/千次。",
197
571
  pricingUrl: "https://exa.ai/pricing"
198
572
  },
199
573
  firecrawl: {
@@ -216,7 +590,7 @@ function searchProviderInfo(provider) {
216
590
  const known = Object.hasOwn(providerInfo, provider.id) ? providerInfo[provider.id] : void 0;
217
591
  return {
218
592
  description: known?.description ?? provider.description,
219
- pricing: provider.pricing ?? known?.pricing ?? (provider.billing === "none" ? "此后端不收取搜索费用。" : provider.billing === "model-and-tools" ? "按模型用量和工具调用计费;免费额度及单价请查看供应商说明。" : "按 API 调用计费;免费额度及单价请查看供应商说明。"),
593
+ pricing: provider.pricing ?? known?.pricing ?? (provider.billing === "none" ? "该后端不收取搜索费用。" : provider.billing === "model-and-tools" ? "按模型用量与工具调用计费;额度与单价见供应商说明。" : "按 API 调用计费;额度与单价见供应商说明。"),
220
594
  pricingUrl: provider.pricingUrl ?? known?.pricingUrl ?? provider.signupUrl
221
595
  };
222
596
  }
@@ -236,7 +610,8 @@ const inject = [
236
610
  "slots",
237
611
  "connection",
238
612
  "remote",
239
- "remote.credentials"
613
+ "remote.credentials",
614
+ "locale"
240
615
  ];
241
616
  function unwrap(result) {
242
617
  if (!result.ok) throw new Error(result.error.message);
@@ -301,10 +676,12 @@ function ConfirmButton(props) {
301
676
  clearTimeout(timer.current);
302
677
  setArmed(false);
303
678
  }
304
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
305
- type: "button",
306
- className: "web-search-danger",
679
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
680
+ variant: "outline",
681
+ size: "sm",
682
+ className: "ws-danger",
307
683
  disabled: props.disabled,
684
+ "data-armed": armed || void 0,
308
685
  onClick: () => {
309
686
  if (!armed) {
310
687
  setArmed(true);
@@ -318,26 +695,200 @@ function ConfirmButton(props) {
318
695
  children: armed ? props.confirmLabel : props.label
319
696
  });
320
697
  }
698
+ const copy = {
699
+ zh: {
700
+ tabsLabel: "网络搜索设置",
701
+ tabProviders: "搜索服务",
702
+ tabLimits: "请求限制",
703
+ loading: "正在读取设置…",
704
+ loadFailed: "读取设置失败:",
705
+ reconnect: "重新连接",
706
+ failed: "操作失败。",
707
+ readOnly: "凭据只读,无法保存。",
708
+ keyDeleted: "已删除 Key。",
709
+ keySaved: "已保存 Key。",
710
+ saved: "已保存。",
711
+ storageFailed: "保存失败,重新保存前网络访问已暂停。",
712
+ empty: "暂无可用搜索后端,请启用一个供应商。",
713
+ toolTitle: "联网搜索",
714
+ needBackend: "需选择搜索服务并配置 Key。",
715
+ backends: "搜索后端",
716
+ dragPrefix: "拖动排序 ",
717
+ dragHint: "拖动排序,或用上下方向键",
718
+ inUse: "当前使用",
719
+ signup: "去注册",
720
+ pricing: "费用说明",
721
+ website: "官网",
722
+ needsKey: "待配置 Key",
723
+ unavailable: "暂不可用",
724
+ sharedKey: "与其他设置共用凭据",
725
+ keyConfigured: "已配置,留空不替换",
726
+ keyPlaceholder: "输入 API Key",
727
+ saveKey: "保存 Key",
728
+ deleteKey: "删除 Key",
729
+ confirmDelete: "确认删除 Key?",
730
+ test: "测试连接(可能计费)",
731
+ unsaved: "有未保存的修改",
732
+ save: "保存",
733
+ saveHint: "开关与排序立即生效;Key 与限制需保存。",
734
+ maxResults: "每条查询结果上限",
735
+ maxQueries: "每次调用查询上限",
736
+ timeoutMs: "请求超时(毫秒)",
737
+ maxFetchChars: "正文字符上限"
738
+ },
739
+ en: {
740
+ tabsLabel: "Web search settings",
741
+ tabProviders: "Search services",
742
+ tabLimits: "Request limits",
743
+ loading: "Loading settings…",
744
+ loadFailed: "Could not load settings: ",
745
+ reconnect: "Reconnect",
746
+ failed: "Operation failed.",
747
+ readOnly: "Read-only credential; cannot save.",
748
+ keyDeleted: "Key deleted.",
749
+ keySaved: "Key saved.",
750
+ saved: "Saved.",
751
+ storageFailed: "Save failed; network access is paused until you save again.",
752
+ empty: "No search backends yet; enable a provider.",
753
+ toolTitle: "Web search",
754
+ needBackend: "Needs a search service with a key.",
755
+ backends: "Search backends",
756
+ dragPrefix: "Reorder ",
757
+ dragHint: "Drag to reorder, or use arrow keys",
758
+ inUse: "In use",
759
+ signup: "Sign up",
760
+ pricing: "Pricing",
761
+ website: "Website",
762
+ needsKey: "Key required",
763
+ unavailable: "Unavailable",
764
+ sharedKey: "Shares a credential with other settings",
765
+ keyConfigured: "Configured; empty keeps it",
766
+ keyPlaceholder: "Enter API key",
767
+ saveKey: "Save key",
768
+ deleteKey: "Delete key",
769
+ confirmDelete: "Delete this key?",
770
+ test: "Test connection (may be billed)",
771
+ unsaved: "Unsaved changes",
772
+ save: "Save",
773
+ saveHint: "Switches and order apply now; keys and limits need saving.",
774
+ maxResults: "Max results/query",
775
+ maxQueries: "Max queries/call",
776
+ timeoutMs: "Request timeout (ms)",
777
+ maxFetchChars: "Max text chars"
778
+ }
779
+ };
780
+ function movedText(locale, label, position) {
781
+ return locale === "zh" ? `${label} 已移至第 ${position} 位` : `${label} moved to position ${position}`;
782
+ }
783
+ function pricingCheckedText(locale, date) {
784
+ return locale === "zh" ? `价格核对于 ${date},额度与费用以供应商账户为准。` : `Prices checked on ${date}; your provider account is authoritative.`;
785
+ }
786
+ function testOkText(locale, sources) {
787
+ return locale === "zh" ? `连接正常,返回 ${sources} 条来源。` : `Connection OK: ${sources} sources returned.`;
788
+ }
789
+ function limitInvalidText(locale, label, min, max) {
790
+ return locale === "zh" ? `${label}需为 ${min}–${max} 之间的整数。` : `${label} must be a whole number from ${min} to ${max}.`;
791
+ }
792
+ const LIMITS = [
793
+ [
794
+ "maxResults",
795
+ 1,
796
+ 20
797
+ ],
798
+ [
799
+ "maxQueries",
800
+ 1,
801
+ 5
802
+ ],
803
+ [
804
+ "timeoutMs",
805
+ 1e3,
806
+ 12e4
807
+ ],
808
+ [
809
+ "maxFetchChars",
810
+ 1e3,
811
+ 2e5
812
+ ]
813
+ ];
814
+ /** A limit field's raw text, or `undefined` when it is not a whole number in range. */
815
+ function parseLimit(raw, min, max) {
816
+ const text = raw.trim();
817
+ if (!/^\d+$/u.test(text)) return void 0;
818
+ const value = Number(text);
819
+ return value >= min && value <= max ? value : void 0;
820
+ }
821
+ /**
822
+ * Settings written by an immediate action (a switch or a reorder). It keeps the
823
+ * saved limits and never includes typed-but-unsaved keys or limit drafts.
824
+ */
825
+ function immediateUpdate(settings, on, order) {
826
+ return {
827
+ ...editable(settings),
828
+ searchOrder: order,
829
+ searchEnabled: on,
830
+ fetchProvider: "http",
831
+ fetchEnabled: on
832
+ };
833
+ }
834
+ function limitTexts(settings) {
835
+ return {
836
+ maxResults: String(settings.maxResults),
837
+ maxQueries: String(settings.maxQueries),
838
+ timeoutMs: String(settings.timeoutMs),
839
+ maxFetchChars: String(settings.maxFetchChars)
840
+ };
841
+ }
842
+ function useHostLocale(client) {
843
+ const subscribe = (0, react.useCallback)((listener) => client.locale?.subscribe(listener) ?? (() => {}), [client]);
844
+ const snapshot = (0, react.useCallback)(() => client.locale?.getSnapshot().active ?? "zh", [client]);
845
+ return String((0, react.useSyncExternalStore)(subscribe, snapshot, snapshot)).startsWith("zh") ? "zh" : "en";
846
+ }
847
+ /** Keyboard reorders are batched: persist once the arrows stop for this long. */
848
+ const REORDER_DEBOUNCE_MS = 500;
321
849
  function NetworkSearchSettings({ client }) {
850
+ const locale = useHostLocale(client);
851
+ const text = copy[locale];
322
852
  const tabsId = (0, react.useId)();
323
853
  const [tab, setTab] = (0, react.useState)("providers");
324
854
  const [status, setStatus] = (0, react.useState)();
325
- const [draft, setDraft] = (0, react.useState)();
855
+ const [limitDraft, setLimitDraft] = (0, react.useState)();
856
+ const [invalid, setInvalid] = (0, react.useState)({});
326
857
  const [keys, setKeys] = (0, react.useState)({});
327
858
  const [busy, setBusy] = (0, react.useState)(false);
859
+ const [pendingOrder, setPendingOrder] = (0, react.useState)(null);
328
860
  const [dragSource, setDragSource] = (0, react.useState)(null);
329
861
  const [dropTarget, setDropTarget] = (0, react.useState)(null);
330
862
  const [sortAnnouncement, setSortAnnouncement] = (0, react.useState)("");
331
863
  const [note, setNote] = (0, react.useState)("");
332
864
  const [error, setError] = (0, react.useState)("");
865
+ const [loadError, setLoadError] = (0, react.useState)("");
333
866
  const [writable, setWritable] = (0, react.useState)({});
867
+ const statusRef = (0, react.useRef)(status);
868
+ statusRef.current = status;
869
+ const pendingRef = (0, react.useRef)(null);
870
+ const timerRef = (0, react.useRef)(void 0);
871
+ const handleRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
872
+ const refocusRef = (0, react.useRef)(null);
334
873
  async function call(endpoint, payload = {}) {
335
874
  return unwrap(await client.connection.rpc.call(WEB_SEARCH_RPC_CHANNEL, endpoint, payload));
336
875
  }
337
- async function load() {
338
- const next = await call("status");
876
+ function adopt(next, resetDraft = false) {
877
+ const previous = statusRef.current;
878
+ statusRef.current = next;
339
879
  setStatus(next);
340
- setDraft(next.settings);
880
+ setLimitDraft((current) => {
881
+ const incoming = limitTexts(next.settings);
882
+ if (!current || !previous || resetDraft) return incoming;
883
+ const baseline = limitTexts(previous.settings);
884
+ return Object.fromEntries(LIMITS.map(([key]) => [key, current[key] === baseline[key] ? incoming[key] : current[key]]));
885
+ });
886
+ if (resetDraft) setInvalid({});
887
+ }
888
+ async function load(resetDraft = false) {
889
+ const next = await call("status");
890
+ adopt(next, resetDraft);
891
+ setLoadError("");
341
892
  const refs = next.providers.flatMap((provider) => provider.credentialRef ? [provider.credentialRef] : []);
342
893
  if (refs.length) {
343
894
  const credentials = unwrap(await client.remote.credentials.describe(refs));
@@ -346,8 +897,25 @@ function NetworkSearchSettings({ client }) {
346
897
  return next;
347
898
  }
348
899
  (0, react.useEffect)(() => {
349
- load().catch(() => setError("无法读取网络搜索设置,请检查 Host 连接后重试。"));
900
+ load().catch((cause) => setLoadError(cause instanceof Error && cause.message ? cause.message : text.failed));
350
901
  }, [client]);
902
+ (0, react.useEffect)(() => () => {
903
+ if (timerRef.current === void 0) return;
904
+ clearTimeout(timerRef.current);
905
+ const current = statusRef.current;
906
+ const next = pendingRef.current;
907
+ if (current && next) call("update", {
908
+ settings: immediateUpdate(current.settings, current.settings.searchEnabled, next),
909
+ expectedRevision: current.settings.revision
910
+ }).catch(() => {});
911
+ }, []);
912
+ (0, react.useLayoutEffect)(() => {
913
+ const id = refocusRef.current;
914
+ if (!id) return;
915
+ refocusRef.current = null;
916
+ const handle = handleRefs.current.get(id);
917
+ if (handle && document.activeElement !== handle) handle.focus();
918
+ });
351
919
  async function action(run) {
352
920
  setBusy(true);
353
921
  setNote("");
@@ -355,52 +923,45 @@ function NetworkSearchSettings({ client }) {
355
923
  try {
356
924
  await run();
357
925
  } catch (cause) {
358
- setError(cause instanceof Error ? cause.message : "操作失败。");
926
+ setError(cause instanceof Error && cause.message ? cause.message : text.failed);
359
927
  await load().catch(() => {});
360
928
  } finally {
361
929
  setBusy(false);
362
930
  }
363
931
  }
364
- async function update(settings, expectedRevision) {
365
- const next = await call("update", {
366
- settings,
367
- expectedRevision
932
+ /** Switches and reorders: settings only, applied at once. */
933
+ function runImmediate(on, order) {
934
+ clearTimeout(timerRef.current);
935
+ timerRef.current = void 0;
936
+ pendingRef.current = null;
937
+ action(async () => {
938
+ const current = statusRef.current;
939
+ if (!current) return;
940
+ const next = await call("update", {
941
+ settings: immediateUpdate(current.settings, on, order),
942
+ expectedRevision: current.settings.revision
943
+ });
944
+ setStatus(next);
945
+ await load();
946
+ }).then(() => {
947
+ if (pendingRef.current === null) setPendingOrder(null);
368
948
  });
369
- setStatus(next);
370
- setDraft(next.settings);
371
- return next;
372
949
  }
373
- async function persist(on, order) {
374
- if (!status || !draft) return;
950
+ /** Writes every typed key. Explicit actions only (Save, Save key, Test). */
951
+ async function saveKeys(only) {
952
+ if (!status) return;
375
953
  for (const provider of status.providers) {
376
- if (provider.kind !== "search") continue;
954
+ if (provider.kind !== "search" || only && provider.id !== only.id) continue;
377
955
  const snapshot = keys[provider.id] ?? "";
378
956
  const typed = snapshot.trim();
379
957
  if (!provider.credentialRef || !typed) continue;
380
- if (writable[provider.credentialRef] === false) throw new Error("当前凭据为只读,无法写入。");
958
+ if (writable[provider.credentialRef] === false) throw new Error(text.readOnly);
381
959
  unwrap(await client.remote.credentials.set(provider.credentialRef, typed));
382
960
  setKeys((current) => current[provider.id] === snapshot ? {
383
961
  ...current,
384
962
  [provider.id]: ""
385
963
  } : current);
386
964
  }
387
- const bounded = (key) => {
388
- const row = limits.find((item) => item[0] === key);
389
- return Math.min(row[3], Math.max(row[2], Number.isFinite(draft[key]) ? draft[key] : row[2]));
390
- };
391
- await update({
392
- ...editable(status.settings),
393
- maxResults: bounded("maxResults"),
394
- maxQueries: bounded("maxQueries"),
395
- timeoutMs: bounded("timeoutMs"),
396
- maxFetchChars: bounded("maxFetchChars"),
397
- endpoints: draft.endpoints,
398
- searchOrder: order,
399
- searchEnabled: on,
400
- fetchProvider: "http",
401
- fetchEnabled: on
402
- }, status.settings.revision);
403
- await load();
404
965
  }
405
966
  async function removeKey(provider) {
406
967
  if (!provider.credentialRef || provider.credentialShared || !status) return;
@@ -410,120 +971,138 @@ function NetworkSearchSettings({ client }) {
410
971
  [provider.id]: ""
411
972
  }));
412
973
  await load();
413
- setNote("已删除 Key。");
974
+ setNote(text.keyDeleted);
414
975
  }
415
- if (!status || !draft) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
416
- className: "web-search-settings",
417
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
976
+ if (!status || !limitDraft) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
977
+ className: "web-search-settings ws-root dsh-ui-panel",
978
+ children: [loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
979
+ role: "alert",
980
+ className: "dsh-ui-error",
981
+ children: [text.loadFailed, loadError]
982
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
418
983
  role: "status",
419
- children: error || "正在读取设置…"
420
- }), error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
421
- type: "button",
422
- onClick: () => void action(async () => {
423
- await load();
424
- }),
425
- children: "重新连接"
984
+ className: "dsh-ui-hint",
985
+ children: text.loading
986
+ }), loadError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
987
+ className: "dsh-ui-actions",
988
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
989
+ variant: "outline",
990
+ size: "sm",
991
+ disabled: busy,
992
+ onClick: () => void action(async () => {
993
+ await load();
994
+ }),
995
+ children: text.reconnect
996
+ })
426
997
  }) : null]
427
998
  });
428
- const enabled = searchBackends(status);
429
- const backends = catalogSearchBackends(status);
430
- const selected = selectedSearchBackend(status);
431
- const on = status.settings.searchEnabled;
432
- const order = enabled.map((provider) => provider.id);
433
- const canEnable = canEnableSearch(order, status.providers, keys, writable);
434
- const limits = [
435
- [
436
- "maxResults",
437
- "每条查询的结果上限",
438
- 1,
439
- 20
440
- ],
441
- [
442
- "maxQueries",
443
- "每次工具调用的查询上限",
444
- 1,
445
- 5
446
- ],
447
- [
448
- "timeoutMs",
449
- "请求超时(毫秒)",
450
- 1e3,
451
- 12e4
452
- ],
453
- [
454
- "maxFetchChars",
455
- "网页正文字符上限",
456
- 1e3,
457
- 2e5
458
- ]
459
- ];
460
- function move(id, nextIndex) {
999
+ const saved = status;
1000
+ const savedOrder = searchBackends(saved).map((provider) => provider.id);
1001
+ const order = pendingOrder ?? savedOrder;
1002
+ const catalog = catalogSearchBackends(saved);
1003
+ const backends = [...order.flatMap((id) => catalog.find((provider) => provider.id === id) ?? []), ...catalog.filter((provider) => !order.includes(provider.id))];
1004
+ const selected = selectedSearchBackend(saved);
1005
+ const on = saved.settings.searchEnabled;
1006
+ const canEnable = canEnableSearch(order, saved.providers, {}, writable);
1007
+ const savedLimits = limitTexts(saved.settings);
1008
+ const limitsDirty = LIMITS.some(([key]) => limitDraft[key] !== savedLimits[key]);
1009
+ const keysDirty = Object.values(keys).some((value) => value.trim());
1010
+ const reordering = pendingOrder !== null;
1011
+ function move(id, nextIndex, keyboard = false) {
461
1012
  const index = order.indexOf(id);
462
1013
  if (busy || index < 0 || nextIndex < 0 || nextIndex >= order.length || index === nextIndex) return;
463
1014
  const next = [...order];
464
1015
  const [item] = next.splice(index, 1);
465
1016
  next.splice(nextIndex, 0, item);
466
- action(async () => {
467
- await persist(on, next);
468
- setSortAnnouncement((backends.find((provider) => provider.id === id)?.label ?? id) + " 已移至第 " + (nextIndex + 1) + " 位");
469
- });
1017
+ pendingRef.current = next;
1018
+ setPendingOrder(next);
1019
+ if (keyboard) refocusRef.current = id;
1020
+ setSortAnnouncement(movedText(locale, backends.find((provider) => provider.id === id)?.label ?? id, nextIndex + 1));
1021
+ clearTimeout(timerRef.current);
1022
+ timerRef.current = setTimeout(() => {
1023
+ timerRef.current = void 0;
1024
+ const batched = pendingRef.current;
1025
+ const current = statusRef.current;
1026
+ if (batched && current) runImmediate(current.settings.searchEnabled, batched);
1027
+ }, keyboard ? REORDER_DEBOUNCE_MS : 0);
470
1028
  }
471
1029
  function endDrag() {
472
1030
  setDragSource(null);
473
1031
  setDropTarget(null);
474
1032
  }
1033
+ function saveAll() {
1034
+ const parsed = {};
1035
+ const nextInvalid = {};
1036
+ const messages = [];
1037
+ for (const [key, min, max] of LIMITS) {
1038
+ const value = parseLimit(limitDraft[key], min, max);
1039
+ if (value === void 0) {
1040
+ nextInvalid[key] = true;
1041
+ messages.push(limitInvalidText(locale, text[key], min, max));
1042
+ } else parsed[key] = value;
1043
+ }
1044
+ setInvalid(nextInvalid);
1045
+ if (messages.length) {
1046
+ setNote("");
1047
+ setError(messages.join(" "));
1048
+ return;
1049
+ }
1050
+ action(async () => {
1051
+ await saveKeys();
1052
+ const current = statusRef.current;
1053
+ adopt(await call("update", {
1054
+ settings: {
1055
+ ...editable(current.settings),
1056
+ ...parsed,
1057
+ fetchProvider: "http",
1058
+ fetchEnabled: current.settings.searchEnabled
1059
+ },
1060
+ expectedRevision: current.settings.revision
1061
+ }), true);
1062
+ await load();
1063
+ setNote(text.saved);
1064
+ });
1065
+ }
475
1066
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
476
- className: "web-search-settings",
1067
+ className: "web-search-settings ws-root dsh-ui-panel",
477
1068
  "data-testid": "web-search-settings",
478
1069
  children: [
479
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
480
- className: "web-search-tabs",
481
- role: "tablist",
482
- "aria-label": "网络搜索设置",
483
- onKeyDown: (event) => {
484
- if (![
485
- "ArrowLeft",
486
- "ArrowRight",
487
- "Home",
488
- "End"
489
- ].includes(event.key)) return;
490
- const buttons = [...event.currentTarget.querySelectorAll("[role=\"tab\"]")];
491
- const index = buttons.indexOf(event.target);
492
- if (index < 0) return;
493
- event.preventDefault();
494
- const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowRight" ? 1 : -1) + buttons.length) % buttons.length;
495
- buttons[next]?.focus();
496
- buttons[next]?.click();
497
- },
498
- children: [["providers", "搜索服务"], ["limits", "请求限制"]].map(([key, label]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
499
- type: "button",
500
- role: "tab",
501
- id: tabsId + "-" + key + "-tab",
502
- "aria-controls": tabsId + "-" + key + "-panel",
503
- "aria-selected": tab === key,
504
- tabIndex: tab === key ? 0 : -1,
505
- onClick: () => setTab(key),
506
- children: label
507
- }, key))
1070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
1071
+ value: tab,
1072
+ onChange: setTab,
1073
+ label: text.tabsLabel,
1074
+ items: [{
1075
+ value: "providers",
1076
+ label: text.tabProviders,
1077
+ id: tabsId + "-providers-tab",
1078
+ panelId: tabsId + "-providers-panel"
1079
+ }, {
1080
+ value: "limits",
1081
+ label: text.tabLimits,
1082
+ id: tabsId + "-limits-tab",
1083
+ panelId: tabsId + "-limits-panel"
1084
+ }]
508
1085
  }),
509
1086
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
510
- className: "web-search-sr-only",
1087
+ className: "ws-sr-only",
511
1088
  role: "status",
512
1089
  "aria-live": "polite",
513
1090
  children: sortAnnouncement
514
1091
  }),
515
1092
  error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
516
1093
  role: "alert",
517
- className: "web-search-error",
1094
+ className: "dsh-ui-error",
518
1095
  children: error
519
1096
  }),
520
1097
  note && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
521
1098
  role: "status",
1099
+ className: "dsh-ui-notice",
522
1100
  children: note
523
1101
  }),
524
- status.storageFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1102
+ saved.storageFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
525
1103
  role: "alert",
526
- children: "配置保存异常,当前运行已暂停网络访问。请重新保存。"
1104
+ className: "dsh-ui-banner dsh-ui-banner--danger",
1105
+ children: text.storageFailed
527
1106
  }),
528
1107
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
529
1108
  role: "tabpanel",
@@ -532,41 +1111,42 @@ function NetworkSearchSettings({ client }) {
532
1111
  hidden: tab !== "providers",
533
1112
  tabIndex: 0,
534
1113
  children: backends.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
535
- className: "web-search-empty",
536
- children: "还没有可用的搜索后端。"
1114
+ className: "dsh-ui-empty",
1115
+ children: text.empty
537
1116
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
538
- className: "web-search-card",
1117
+ className: "web-search-card dsh-ui-card dsh-ui-card--flat",
539
1118
  "data-testid": "web-search-tool",
540
1119
  "data-on": on ? "true" : "false",
541
1120
  children: [
542
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "联网搜索" }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
543
- className: "web-search-meta",
544
- children: "请选择搜索服务并配置 Key。"
545
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
546
- type: "button",
547
- role: "switch",
548
- className: `web-search-switch${on ? " is-on" : ""}`,
549
- "aria-checked": on,
550
- "aria-label": on ? "关闭联网搜索" : "启用联网搜索",
551
- disabled: busy || !on && !canEnable,
552
- onClick: () => void action(() => persist(!on, order)),
553
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
554
- className: "web-search-switch-thumb",
555
- "aria-hidden": "true"
556
- })
557
- })] }),
1121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1122
+ className: "ws-card-head",
1123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1124
+ className: "dsh-ui-title",
1125
+ children: text.toolTitle
1126
+ }), !canEnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1127
+ className: "dsh-ui-hint",
1128
+ children: text.needBackend
1129
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1130
+ checked: on,
1131
+ label: text.toolTitle,
1132
+ disabled: busy || !on && !canEnable,
1133
+ onChange: (next) => runImmediate(next, order)
1134
+ })]
1135
+ }),
558
1136
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
559
1137
  className: "web-search-rank",
560
- "aria-label": "搜索后端",
1138
+ "aria-label": text.backends,
561
1139
  children: backends.map((provider) => {
562
1140
  const participating = order.includes(provider.id);
563
1141
  const rank = order.indexOf(provider.id);
564
- const active = status.searchActive && selected?.id === provider.id;
1142
+ const active = saved.searchActive && selected?.id === provider.id;
565
1143
  const info = searchProviderInfo(provider);
566
1144
  const needsKey = Boolean(provider.credentialRef);
567
1145
  const shared = Boolean(provider.credentialShared);
568
1146
  const showKey = needsKey && participating && (!shared || !provider.configured);
569
1147
  const canSort = participating && order.length > 1;
1148
+ const readOnly = writable[provider.credentialRef ?? ""] === false;
1149
+ const typedKey = (keys[provider.id] ?? "").trim();
570
1150
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
571
1151
  className: active ? "is-active" : void 0,
572
1152
  "data-testid": `web-search-rank-${provider.id}`,
@@ -588,32 +1168,15 @@ function NetworkSearchSettings({ client }) {
588
1168
  endDrag();
589
1169
  },
590
1170
  children: [
591
- canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
592
- type: "button",
1171
+ canSort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1172
+ variant: "ghost",
1173
+ size: "sm",
593
1174
  className: "web-search-drag-handle",
594
- draggable: !busy,
595
- "aria-disabled": busy,
596
- "aria-label": "拖动排序 " + provider.label,
597
- title: "拖动排序,或用上下方向键调整",
598
- onDragStart: (event) => {
599
- if (busy) {
600
- event.preventDefault();
601
- return;
602
- }
603
- event.dataTransfer.effectAllowed = "move";
604
- event.dataTransfer.setData("application/x-dsh-search-provider", provider.id);
605
- const row = event.currentTarget.closest("li");
606
- if (row) event.dataTransfer.setDragImage(row, 16, 16);
607
- setDragSource(provider.id);
608
- setSortAnnouncement("");
1175
+ ref: (element) => {
1176
+ if (element) handleRefs.current.set(provider.id, element);
1177
+ else handleRefs.current.delete(provider.id);
609
1178
  },
610
- onDragEnd: endDrag,
611
- onKeyDown: (event) => {
612
- if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
613
- event.preventDefault();
614
- move(provider.id, rank + (event.key === "ArrowUp" ? -1 : 1));
615
- },
616
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1179
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
617
1180
  width: "16",
618
1181
  height: "20",
619
1182
  viewBox: "0 0 16 20",
@@ -651,7 +1214,29 @@ function NetworkSearchSettings({ client }) {
651
1214
  r: "1.5"
652
1215
  })
653
1216
  ]
654
- })
1217
+ }),
1218
+ draggable: !busy,
1219
+ "aria-disabled": busy,
1220
+ "aria-label": text.dragPrefix + provider.label,
1221
+ title: text.dragHint,
1222
+ onDragStart: (event) => {
1223
+ if (busy) {
1224
+ event.preventDefault();
1225
+ return;
1226
+ }
1227
+ event.dataTransfer.effectAllowed = "move";
1228
+ event.dataTransfer.setData("application/x-dsh-search-provider", provider.id);
1229
+ const row = event.currentTarget.closest("li");
1230
+ if (row) event.dataTransfer.setDragImage(row, 16, 16);
1231
+ setDragSource(provider.id);
1232
+ setSortAnnouncement("");
1233
+ },
1234
+ onDragEnd: endDrag,
1235
+ onKeyDown: (event) => {
1236
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
1237
+ event.preventDefault();
1238
+ move(provider.id, rank + (event.key === "ArrowUp" ? -1 : 1), true);
1239
+ }
655
1240
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
656
1241
  className: "web-search-rank-index",
657
1242
  children: participating ? rank + 1 : "—"
@@ -662,94 +1247,110 @@ function NetworkSearchSettings({ client }) {
662
1247
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
663
1248
  className: "web-search-provider-heading",
664
1249
  children: [
665
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: provider.label }),
666
- active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
667
- className: "web-search-meta",
668
- children: "当前使用"
1250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1251
+ className: "dsh-ui-heading",
1252
+ children: provider.label
1253
+ }),
1254
+ active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1255
+ tone: "success",
1256
+ children: text.inUse
669
1257
  }),
670
1258
  provider.signupUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
671
1259
  url: provider.signupUrl,
672
- label: provider.label + " 去注册",
673
- children: "去注册"
1260
+ label: provider.label + " " + text.signup,
1261
+ children: text.signup
674
1262
  })
675
1263
  ]
676
1264
  }),
677
1265
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
678
1266
  className: "web-search-provider-info",
679
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: "费用说明" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [info.pricing, info.pricingUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
1267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: info.description }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [info.pricing, info.pricingUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalLink, {
680
1268
  url: info.pricingUrl,
681
- label: provider.label + " 费用说明",
682
- children: provider.id === "ddg" ? "官网" : "费用说明"
683
- })] })] })] })]
1269
+ label: provider.label + " " + text.pricing,
1270
+ children: provider.id === "ddg" ? text.website : text.pricing
1271
+ })] })] })]
684
1272
  }),
685
- participating && !provider.configured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
686
- className: "web-search-meta",
687
- children: needsKey ? "待配置 Key" : "暂不可用"
1273
+ participating && !provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1274
+ className: "ws-provider-note",
1275
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: needsKey ? text.needsKey : text.unavailable })]
1276
+ }) : null,
1277
+ shared && participating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
1278
+ className: "dsh-ui-meta",
1279
+ children: provider.credentialHint ?? text.sharedKey
688
1280
  }),
689
- shared && participating && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: provider.credentialHint ?? "与其他设置共用凭据" }),
690
- showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["API Key", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
691
- type: "password",
692
- autoComplete: "off",
693
- spellCheck: false,
694
- disabled: busy || writable[provider.credentialRef ?? ""] === false,
695
- value: keys[provider.id] ?? "",
696
- placeholder: provider.configured ? "已配置,留空不替换" : "请输入 API Key",
697
- onChange: (event) => setKeys((current) => ({
698
- ...current,
699
- [provider.id]: event.target.value
700
- }))
701
- })] }) : null,
702
- needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
703
- disabled: busy || writable[provider.credentialRef ?? ""] === false,
704
- label: "删除 Key",
705
- confirmLabel: "确认删除 Key?",
706
- onConfirm: () => void action(() => removeKey(provider))
1281
+ showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1282
+ className: "dsh-ui-field",
1283
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1284
+ className: "dsh-ui-label",
1285
+ children: "API Key"
1286
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1287
+ className: "ws-input",
1288
+ type: "password",
1289
+ autoComplete: "off",
1290
+ spellCheck: false,
1291
+ disabled: busy || readOnly,
1292
+ value: keys[provider.id] ?? "",
1293
+ placeholder: provider.configured ? text.keyConfigured : text.keyPlaceholder,
1294
+ onChange: (event) => setKeys((current) => ({
1295
+ ...current,
1296
+ [provider.id]: event.target.value
1297
+ }))
1298
+ })]
1299
+ }) : null,
1300
+ showKey || needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1301
+ className: "dsh-ui-actions",
1302
+ children: [showKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1303
+ variant: "outline",
1304
+ size: "sm",
1305
+ disabled: busy || readOnly || !typedKey,
1306
+ onClick: () => void action(async () => {
1307
+ await saveKeys(provider);
1308
+ await load();
1309
+ setNote(text.keySaved);
1310
+ }),
1311
+ children: text.saveKey
1312
+ }) : null, needsKey && !shared && provider.configured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmButton, {
1313
+ disabled: busy || readOnly,
1314
+ label: text.deleteKey,
1315
+ confirmLabel: text.confirmDelete,
1316
+ onConfirm: () => void action(() => removeKey(provider))
1317
+ }) : null]
707
1318
  }) : null
708
1319
  ]
709
1320
  }),
710
1321
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
711
1322
  className: "web-search-rank-move",
712
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
713
- type: "button",
714
- role: "switch",
715
- className: `web-search-switch${participating ? " is-on" : ""}`,
716
- "aria-checked": participating,
717
- "aria-label": `${participating ? "关闭" : "开启"} ${provider.label}`,
1323
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1324
+ checked: participating,
1325
+ label: provider.label,
718
1326
  disabled: busy,
719
- onClick: () => {
720
- const next = participating ? order.filter((id) => id !== provider.id) : [...order, provider.id];
721
- action(() => persist(nextSearchEnabled(status.settings.searchEnabled, next, status.providers, keys, writable), next));
722
- },
723
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
724
- className: "web-search-switch-thumb",
725
- "aria-hidden": "true"
726
- })
1327
+ onChange: (next) => {
1328
+ const following = next ? [...order, provider.id] : order.filter((id) => id !== provider.id);
1329
+ runImmediate(nextSearchEnabled(saved.settings.searchEnabled, following, saved.providers, {}, writable), following);
1330
+ }
727
1331
  })
728
1332
  })
729
1333
  ]
730
1334
  }, provider.id);
731
1335
  })
732
1336
  }),
733
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
734
- className: "web-search-meta",
735
- children: [
736
- "价格核对于 ",
737
- PROVIDER_PRICING_CHECKED,
738
- ",实际额度与费用以供应商账户为准。"
739
- ]
1337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1338
+ className: "dsh-ui-hint",
1339
+ children: pricingCheckedText(locale, PROVIDER_PRICING_CHECKED)
740
1340
  }),
741
1341
  on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
742
- className: "web-search-card-actions",
743
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
744
- type: "button",
745
- disabled: busy,
1342
+ className: "dsh-ui-actions",
1343
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1344
+ variant: "outline",
1345
+ size: "md",
1346
+ disabled: busy || reordering,
746
1347
  onClick: () => void action(async () => {
747
- await persist(status.settings.searchEnabled, order);
1348
+ await saveKeys();
748
1349
  const result = await call("test");
749
1350
  await load();
750
- setNote(`连接正常,返回 ${result.sources} 条来源。`);
1351
+ setNote(testOkText(locale, result.sources));
751
1352
  }),
752
- children: "测试连接(可能计费)"
1353
+ children: text.test
753
1354
  })
754
1355
  }) : null
755
1356
  ]
@@ -763,83 +1364,123 @@ function NetworkSearchSettings({ client }) {
763
1364
  tabIndex: 0,
764
1365
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
765
1366
  className: "web-search-limits",
766
- disabled: busy,
767
- "aria-label": "请求限制",
768
- children: limits.map(([key, label, min, max]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
769
- type: "number",
770
- min,
771
- max,
772
- step: 1,
773
- value: draft[key],
774
- onChange: (event) => setDraft({
775
- ...draft,
776
- [key]: Number(event.target.value)
777
- })
778
- })] }, key))
1367
+ disabled: busy || reordering,
1368
+ "aria-label": text.tabLimits,
1369
+ children: LIMITS.map(([key, min, max]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1370
+ className: "ws-limit",
1371
+ children: [
1372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1373
+ className: "dsh-ui-label",
1374
+ children: text[key]
1375
+ }),
1376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1377
+ className: "ws-input",
1378
+ type: "number",
1379
+ min,
1380
+ max,
1381
+ step: 1,
1382
+ value: limitDraft[key],
1383
+ "aria-invalid": invalid[key] || void 0,
1384
+ onChange: (event) => {
1385
+ const raw = event.target.value;
1386
+ setLimitDraft((current) => current ? {
1387
+ ...current,
1388
+ [key]: raw
1389
+ } : current);
1390
+ setInvalid((current) => ({
1391
+ ...current,
1392
+ [key]: false
1393
+ }));
1394
+ },
1395
+ onBlur: () => setInvalid((current) => ({
1396
+ ...current,
1397
+ [key]: parseLimit(limitDraft[key], min, max) === void 0
1398
+ }))
1399
+ }),
1400
+ invalid[key] ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1401
+ className: "dsh-ui-error",
1402
+ children: limitInvalidText(locale, text[key], min, max)
1403
+ }) : null
1404
+ ]
1405
+ }, key))
779
1406
  })
780
1407
  }),
781
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
782
- type: "button",
783
- disabled: busy,
784
- onClick: () => void action(async () => {
785
- await persist(status.settings.searchEnabled, order);
786
- setNote("已保存。");
787
- }),
788
- children: "保存"
1408
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1409
+ className: "dsh-ui-actions dsh-ui-actions-end",
1410
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1411
+ className: "dsh-ui-meta",
1412
+ children: limitsDirty || keysDirty ? text.unsaved : text.saveHint
1413
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1414
+ variant: "primary",
1415
+ size: "md",
1416
+ disabled: busy || reordering,
1417
+ onClick: saveAll,
1418
+ children: text.save
1419
+ })]
789
1420
  })
790
1421
  ]
791
1422
  });
792
1423
  }
793
- const styles = `
794
- .web-search-settings{max-width:760px;display:grid;gap:24px;color:inherit;font:400 var(--font-size-2,14px)/1.5 var(--default-font-family,system-ui,sans-serif)}
795
- .web-search-settings p{margin:0;line-height:1.5}
796
- .web-search-settings small,.web-search-meta,.web-search-empty{font-size:var(--font-size-1,13px);color:var(--gray-11,inherit)}
797
- .web-search-error{color:var(--red-11,#b42318)}
798
- .web-search-card{display:grid;gap:12px;padding:16px;border:1px solid var(--gray-6,color-mix(in srgb,currentColor 15%,transparent));border-radius:12px}
799
- .web-search-card header{display:flex;justify-content:space-between;gap:12px;align-items:center;padding-bottom:8px}
800
- .web-search-card h3{margin:0;font-size:var(--font-size-3,16px);font-weight:600}
801
- .web-search-provider{display:grid;gap:8px;min-width:0}
802
- .web-search-provider-info{display:grid;gap:3px;font-size:var(--font-size-1,13px);color:var(--gray-11,inherit);overflow-wrap:anywhere}
803
- .web-search-provider-heading{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;min-height:24px}
804
- .web-search-provider-heading strong{font-weight:500}
805
- .web-search-card label{display:grid;gap:6px}
806
- .web-search-settings input{box-sizing:border-box;width:100%;min-width:0;padding:8px 10px;border:1px solid var(--gray-6,color-mix(in srgb,currentColor 22%,transparent));border-radius:8px;background:var(--color-surface,transparent);color:inherit;font:inherit;transition:border-color 150ms ease,box-shadow 150ms ease}
807
- .web-search-settings input:focus{border-color:var(--accent-9,#3b82f6);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent-9,#3b82f6) 25%,transparent)}
808
- .web-search-settings button:not([role="switch"]){min-height:34px;padding:6px 12px;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:8px;background:transparent;color:inherit;cursor:pointer;font:inherit;justify-self:start;transition:background-color 150ms ease,color 150ms ease,border-color 150ms ease,box-shadow 150ms ease,transform 150ms ease}
809
- .web-search-settings button:not([role="switch"]):not([role="tab"]):not(.web-search-drag-handle):hover:not(:disabled){background:var(--gray-3,color-mix(in srgb,currentColor 6%,transparent));border-color:color-mix(in srgb,currentColor 35%,transparent)}
810
- .web-search-settings button:not([role="switch"]):not([role="tab"]):not(.web-search-drag-handle):active:not(:disabled){transform:scale(.97)}
811
- .web-search-danger:hover:not(:disabled){border-color:var(--red-11,#b42318);color:var(--red-11,#b42318);background:color-mix(in srgb,var(--red-11,#b42318) 8%,transparent)}
812
- .web-search-settings button:disabled{opacity:.45;cursor:not-allowed}
813
- .web-search-tabs{display:flex;gap:20px;border-bottom:1px solid var(--gray-6,color-mix(in srgb,currentColor 15%,transparent))}
814
- .web-search-tabs button[role="tab"]{padding:8px 0;border:0;border-bottom:2px solid transparent;border-radius:0;color:var(--gray-11,inherit)}
815
- .web-search-tabs button[role="tab"]:hover{color:inherit}
816
- .web-search-tabs button[aria-selected="true"]{border-bottom-color:var(--accent-9,#3b82f6);color:var(--accent-11,inherit);font-weight:600}
817
- .web-search-settings [role="tabpanel"]{border-radius:12px}
818
- .web-search-settings .web-search-switch{all:unset;box-sizing:border-box;position:relative;display:inline-block;width:36px;height:20px;flex:none;border-radius:999px;background:var(--gray-7,color-mix(in srgb,currentColor 28%,transparent));cursor:pointer}
819
- .web-search-switch.is-on{background:var(--accent-9,#3b82f6)}
820
- .web-search-switch:disabled{cursor:not-allowed;opacity:.7}
821
- .web-search-switch-thumb{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:999px;background:#fff;transition:transform 150ms ease}
822
- .web-search-switch.is-on .web-search-switch-thumb{transform:translateX(16px)}
823
- .web-search-settings :focus-visible{outline:2px solid var(--accent-9,currentColor);outline-offset:3px}
824
- .web-search-card-actions{display:flex;flex-wrap:wrap;gap:8px}
825
- .web-search-rank{margin:0;padding:0;list-style:none}
826
- .web-search-rank li{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto;gap:10px;align-items:start;padding:14px 0;border-top:1px solid var(--gray-6,color-mix(in srgb,currentColor 15%,transparent))}
827
- .web-search-settings button.web-search-drag-handle{display:grid;place-items:center;width:24px;height:24px;min-height:24px;padding:0;border:0;cursor:grab;color:inherit;opacity:.65}
828
- .web-search-settings button.web-search-drag-handle:hover{opacity:1;background:var(--gray-3,color-mix(in srgb,currentColor 8%,transparent))}
829
- .web-search-settings button.web-search-drag-handle:active{cursor:grabbing}
830
- .web-search-settings button.web-search-drag-handle[aria-disabled="true"]{cursor:wait;opacity:.4}
831
- .web-search-rank li[data-dragging]{opacity:.45}
832
- .web-search-rank li[data-drop]::after{content:'';position:absolute;left:0;right:0;height:2px;background:var(--accent-9,#3b82f6);pointer-events:none}
833
- .web-search-rank li[data-drop="before"]::after{top:-1px}
834
- .web-search-rank li[data-drop="after"]::after{bottom:-1px}
835
- .web-search-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
836
- .web-search-rank-index{opacity:.6;font-variant-numeric:tabular-nums;line-height:24px}
837
- .web-search-rank-move{display:flex;flex-wrap:wrap;gap:6px;align-items:center;justify-content:flex-end;padding-top:2px}
838
- .web-search-settings a{color:var(--accent-11,currentColor);font-size:var(--font-size-1,13px);text-underline-offset:3px}
839
- .web-search-limits{margin:0;border:0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 20px}
840
- .web-search-limits label{display:grid;gap:6px;font-size:var(--font-size-2,14px)}
841
- @media(max-width:560px){.web-search-rank-move{flex-direction:column;align-items:flex-end}.web-search-limits{grid-template-columns:minmax(0,1fr)}}
842
- @media(prefers-reduced-motion:reduce){.web-search-switch-thumb{transition:none}.web-search-settings button:not([role="switch"]),.web-search-settings input{transition:none}}
1424
+ const styles = `${officialUiCss("ws-root")}
1425
+ .ws-root { max-width: 760px; }
1426
+ .ws-card-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding-bottom: 8px; }
1427
+ .ws-sr-only {
1428
+ position: absolute;
1429
+ width: 1px;
1430
+ height: 1px;
1431
+ padding: 0;
1432
+ margin: -1px;
1433
+ overflow: hidden;
1434
+ clip-path: inset(50%);
1435
+ white-space: nowrap;
1436
+ border: 0;
1437
+ }
1438
+ .ws-provider-note { display: flex; align-items: center; gap: 6px; }
1439
+ .web-search-provider { display: grid; gap: 8px; min-width: 0; }
1440
+ .web-search-provider-info { display: grid; gap: 3px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
1441
+ .web-search-provider-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; min-height: 24px; }
1442
+ .web-search-settings a { color: var(--dsw-alias-state-business-primary); font-size: 12px; line-height: 18px; text-underline-offset: 3px; }
1443
+ /* The primitive renders a 32px control in an inline-flex wrapper; a settings
1444
+ * field needs it to fill the column it sits in. */
1445
+ .ws-input { width: 100%; }
1446
+ .web-search-rank { margin: 0; padding: 0; list-style: none; }
1447
+ .web-search-rank li {
1448
+ position: relative;
1449
+ display: grid;
1450
+ grid-template-columns: 24px minmax(0, 1fr) auto;
1451
+ gap: 10px;
1452
+ align-items: start;
1453
+ padding: 14px 0;
1454
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
1455
+ }
1456
+ .web-search-settings button.web-search-drag-handle {
1457
+ display: grid;
1458
+ place-items: center;
1459
+ width: 24px;
1460
+ height: 24px;
1461
+ min-height: 24px;
1462
+ padding: 0;
1463
+ cursor: grab;
1464
+ opacity: 0.65;
1465
+ }
1466
+ .web-search-settings button.web-search-drag-handle:hover { opacity: 1; }
1467
+ .web-search-settings button.web-search-drag-handle:active { cursor: grabbing; }
1468
+ .web-search-settings button.web-search-drag-handle[aria-disabled='true'] { cursor: wait; opacity: 0.4; }
1469
+ .web-search-rank li[data-dragging] { opacity: 0.45; }
1470
+ .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; }
1471
+ .web-search-rank li[data-drop='before']::after { top: -1px; }
1472
+ .web-search-rank li[data-drop='after']::after { bottom: -1px; }
1473
+ .web-search-rank-index { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; line-height: 24px; }
1474
+ .web-search-rank-move { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; justify-content: flex-end; padding-top: 2px; }
1475
+ .web-search-limits { margin: 0; border: 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; }
1476
+ /* A two-column grid, so these fields keep the contract's caption and the
1477
+ * primitive's control and only own their vertical rhythm — a stacked list is
1478
+ * what draws the hairline between fields, and there is nothing to separate. */
1479
+ .ws-limit { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
1480
+ .ws-danger:hover:not(:disabled),
1481
+ .ws-danger[data-armed] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); }
1482
+ .ws-danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger); }
1483
+ @media (max-width: 560px) { .web-search-rank-move { flex-direction: column; align-items: flex-end; } .web-search-limits { grid-template-columns: minmax(0, 1fr); } }
843
1484
  `;
844
1485
  function apply(ctx) {
845
1486
  const client = ctx;
@@ -862,10 +1503,12 @@ exports.NetworkSearchSettings = NetworkSearchSettings;
862
1503
  exports.apply = apply;
863
1504
  exports.canEnableSearch = canEnableSearch;
864
1505
  exports.catalogSearchBackends = catalogSearchBackends;
1506
+ exports.immediateUpdate = immediateUpdate;
865
1507
  exports.inject = inject;
866
1508
  exports.isProviderOn = isProviderOn;
867
1509
  exports.name = name;
868
1510
  exports.nextSearchEnabled = nextSearchEnabled;
1511
+ exports.parseLimit = parseLimit;
869
1512
  exports.searchBackends = searchBackends;
870
1513
  exports.selectedSearchBackend = selectedSearchBackend;
871
1514