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