@moguiyu/dsh-tavily 0.1.3 → 0.1.5

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
@@ -144,10 +144,6 @@ window.__ModuleLoader__.load({
144
144
  }
145
145
  }
146
146
 
147
- const hasPending = Object.values(removing).some(Boolean)
148
- || Object.values(replacing).some(Boolean)
149
- || adds.some((item) => item.value.trim().length > 0)
150
-
151
147
  const saveStrategy = async (next) => {
152
148
  setBusy(true)
153
149
  setNotice(null)
@@ -168,29 +164,69 @@ window.__ModuleLoader__.load({
168
164
  }
169
165
  }
170
166
 
171
- const save = async () => {
167
+ const saveAdd = async (item) => {
168
+ const value = item.value.trim()
169
+ if (!value) return
172
170
  setBusy(true)
173
171
  setNotice(null)
174
172
  try {
175
- const addValues = adds.map((item) => item.value.trim()).filter((value) => value.length > 0)
176
- const removeMasked = [
177
- ...Object.keys(removing).filter((masked) => removing[masked] === true),
178
- ...Object.keys(replacing).filter((masked) => replacing[masked] === true)
179
- ]
180
173
  const response = await fetch('/api/tavily-manager', {
181
174
  method: 'POST',
182
175
  headers: { 'content-type': 'application/json' },
183
- body: JSON.stringify({ add: addValues, remove: removeMasked, strategy })
176
+ body: JSON.stringify({ add: [value], remove: [], strategy })
184
177
  })
185
178
  const data = await response.json()
186
179
  if (!data.ok) { setNotice({ error: data.error || 'Save failed' }); return }
187
- setNotice({ ok: 'Saved — effective on the next search.' })
188
- setRemoving({})
189
- setReplacing({})
190
- setReplaceDrafts({})
191
- setAdds([])
192
- setRevealed({})
193
- setConfirm({})
180
+ setNotice({ ok: 'Key saved.' })
181
+ setAdds((current) => current.filter((entry) => entry.id !== item.id))
182
+ await refresh()
183
+ } catch (error) {
184
+ setNotice({ error: String(error && error.message ? error.message : error) })
185
+ } finally {
186
+ setBusy(false)
187
+ }
188
+ }
189
+
190
+ const saveRemove = async (masked) => {
191
+ setBusy(true)
192
+ setNotice(null)
193
+ try {
194
+ const response = await fetch('/api/tavily-manager', {
195
+ method: 'POST',
196
+ headers: { 'content-type': 'application/json' },
197
+ body: JSON.stringify({ add: [], remove: [masked], strategy })
198
+ })
199
+ const data = await response.json()
200
+ if (!data.ok) { setNotice({ error: data.error || 'Delete failed' }); return }
201
+ setNotice({ ok: 'Key removed.' })
202
+ setConfirm((current) => Object.assign({}, current, { [masked]: false }))
203
+ setRemoving((current) => Object.assign({}, current, { [masked]: false }))
204
+ setRevealed((current) => Object.assign({}, current, { [masked]: false }))
205
+ await refresh()
206
+ } catch (error) {
207
+ setNotice({ error: String(error && error.message ? error.message : error) })
208
+ } finally {
209
+ setBusy(false)
210
+ }
211
+ }
212
+
213
+ const saveReplace = async (masked) => {
214
+ const value = typeof replaceDrafts[masked] === 'string' ? replaceDrafts[masked].trim() : ''
215
+ if (!value) return
216
+ setBusy(true)
217
+ setNotice(null)
218
+ try {
219
+ const response = await fetch('/api/tavily-manager', {
220
+ method: 'POST',
221
+ headers: { 'content-type': 'application/json' },
222
+ body: JSON.stringify({ add: [value], remove: [masked], strategy })
223
+ })
224
+ const data = await response.json()
225
+ if (!data.ok) { setNotice({ error: data.error || 'Update failed' }); return }
226
+ setNotice({ ok: 'Key updated.' })
227
+ setReplacing((current) => Object.assign({}, current, { [masked]: false }))
228
+ setReplaceDrafts((current) => Object.assign({}, current, { [masked]: '' }))
229
+ setRevealed((current) => Object.assign({}, current, { [masked]: false }))
194
230
  await refresh()
195
231
  } catch (error) {
196
232
  setNotice({ error: String(error && error.message ? error.message : error) })
@@ -226,6 +262,7 @@ window.__ModuleLoader__.load({
226
262
  setConfirm((current) => Object.assign({}, current, { [masked]: false }))
227
263
  setRemoving((current) => Object.assign({}, current, { [masked]: true }))
228
264
  setRevealed((current) => Object.assign({}, current, { [masked]: false }))
265
+ saveRemove(masked)
229
266
  }
230
267
 
231
268
  const restore = (masked) => {
@@ -291,16 +328,21 @@ window.__ModuleLoader__.load({
291
328
  react.createElement('thead', null,
292
329
  react.createElement('tr', null,
293
330
  react.createElement('th', { style: headStyle }, 'Key'),
331
+ react.createElement('th', { style: headStyle }, 'Usage'),
294
332
  react.createElement('th', { style: headStyle }, 'Saved'),
295
333
  react.createElement('th', { style: headStyle }, 'Actions')
296
334
  )
297
335
  ),
298
336
  react.createElement('tbody', null,
299
- (server !== null ? server.keys : []).map((key) => {
337
+ (server !== null ? server.keys : []).map((key, index) => {
300
338
  const masked = key.masked
301
339
  const isRemoved = removing[masked] === true
302
340
  const isReplacing = replacing[masked] === true
303
341
  const isRevealed = typeof revealed[masked] === 'string'
342
+ const usageRow = usage !== null && usage.ok === true ? usage.perKey[index] : null
343
+ const pct = usageRow && usageRow.ok && usageRow.planLimit != null && usageRow.planLimit > 0 && usageRow.planUsage != null
344
+ ? Math.min(100, Math.round((usageRow.planUsage / usageRow.planLimit) * 100))
345
+ : null
304
346
  return react.createElement('tr', { key: masked, style: { borderBottom: '1px solid var(--dsw-alias-border-l2)', opacity: isRemoved ? 0.45 : 1 } },
305
347
  react.createElement('td', { style: Object.assign({}, cellStyle, { fontFamily: 'var(--ds-font-family-code, monospace)', fontSize: 12, wordBreak: 'break-all' }) },
306
348
  isReplacing
@@ -319,11 +361,14 @@ window.__ModuleLoader__.load({
319
361
  })
320
362
  )
321
363
  ),
364
+ react.createElement('td', { style: cellStyle },
365
+ react.createElement(UsageCircle, { percent: pct, label: pct != null ? pct + '%' : (usageRow && usageRow.ok && usageRow.planUsage != null ? String(usageRow.planUsage) : '—') })
366
+ ),
322
367
  react.createElement('td', { style: Object.assign({}, cellStyle, { color: 'var(--dsw-alias-label-tertiary)', whiteSpace: 'nowrap' }) }, formatDate(key.savedAt)),
323
368
  react.createElement('td', { style: cellStyle },
324
369
  isReplacing
325
370
  ? react.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
326
- react.createElement(IconButton, { icon: 'check', title: 'Keep', onClick: () => { setReplacing((current) => Object.assign({}, current, { [masked]: false })) }, disabled: busy }),
371
+ react.createElement(IconButton, { icon: 'check', title: 'Save key', onClick: () => saveReplace(masked), disabled: busy || (typeof replaceDrafts[masked] === 'string' ? replaceDrafts[masked].trim().length === 0 : true) }),
327
372
  react.createElement(IconButton, { icon: 'close', title: 'Cancel', onClick: () => cancelReplace(masked), disabled: busy })
328
373
  )
329
374
  : react.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
@@ -348,73 +393,42 @@ window.__ModuleLoader__.load({
348
393
  placeholder: 'New key value',
349
394
  value: item.value,
350
395
  onChange: (event) => setAdds((current) => current.map((entry) => entry.id === item.id ? Object.assign({}, entry, { value: event.target.value }) : entry)),
396
+ onKeyDown: (event) => { if (event.key === 'Enter') saveAdd(item) },
351
397
  disabled: busy
352
398
  })
353
399
  ),
354
400
  react.createElement('td', { style: cellStyle }, '—'),
401
+ react.createElement('td', { style: cellStyle }, '—'),
355
402
  react.createElement('td', { style: cellStyle },
356
- react.createElement(IconButton, { icon: 'trash', title: 'Remove', danger: true, onClick: () => setAdds((current) => current.filter((entry) => entry.id !== item.id)), disabled: busy })
403
+ react.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
404
+ react.createElement(IconButton, { icon: 'check', title: 'Save key', onClick: () => saveAdd(item), disabled: busy || item.value.trim().length === 0 }),
405
+ react.createElement(IconButton, { icon: 'trash', title: 'Remove', danger: true, onClick: () => setAdds((current) => current.filter((entry) => entry.id !== item.id)), disabled: busy })
406
+ )
357
407
  )
358
408
  ))
359
409
  )
360
410
  ),
361
- react.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
362
- react.createElement('button', { type: 'button', style: btn, onClick: () => setAdds((current) => [...current, { id: 'add-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), value: '' }]), disabled: busy }, '+ Add key')
363
- ),
364
- react.createElement('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12, padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 8 } },
365
- react.createElement('label', { style: { fontWeight: 500, fontSize: 13 } }, 'Key usage strategy'),
366
- react.createElement('select', {
367
- style: { maxWidth: 280, height: 32, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', padding: '0 10px', fontSize: 13 },
368
- value: strategy,
369
- onChange: (event) => saveStrategy(event.target.value),
370
- disabled: busy
371
- }, STRATEGIES.map((option) => react.createElement('option', { key: option.id, value: option.id }, option.label))),
372
- react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
373
- (STRATEGIES.find((option) => option.id === strategy) || STRATEGIES[0]).hint + ' The strategy is saved immediately when you select it; the first key then becomes primary for web_search.'
374
- )
375
- ),
376
- react.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
377
- react.createElement('button', { type: 'button', style: Object.assign({}, btn, { height: 32, background: 'var(--dsw-alias-button-primary-fill)', color: 'var(--dsw-alias-label-primary-foreground)' }), onClick: save, disabled: busy || !hasPending }, 'Save key changes'),
378
- notice !== null && react.createElement('span', { style: { fontSize: 13, color: notice.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } }, notice.ok ? notice.ok : String(notice.error))
411
+ react.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
412
+ react.createElement('button', { type: 'button', style: btn, onClick: () => setAdds((current) => [...current, { id: 'add-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), value: '' }]), disabled: busy }, '+ Add key'),
413
+ react.createElement('button', { type: 'button', style: btn, onClick: refresh, disabled: busy }, 'Refresh usage')
379
414
  ),
380
- react.createElement('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12, padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 8 } },
381
- react.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'space-between' } },
382
- react.createElement('span', { style: { fontWeight: 500, fontSize: 13 } }, 'Tavily Usage'),
383
- react.createElement('button', { type: 'button', style: btn, onClick: refresh, disabled: busy }, 'Refresh')
415
+ react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
416
+ react.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
417
+ react.createElement('label', { style: { fontWeight: 500, fontSize: 13 } }, 'Key usage strategy'),
418
+ react.createElement('select', {
419
+ style: { maxWidth: 280, height: 32, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', padding: '0 10px', fontSize: 13 },
420
+ value: strategy,
421
+ onChange: (event) => saveStrategy(event.target.value),
422
+ disabled: busy
423
+ }, STRATEGIES.map((option) => react.createElement('option', { key: option.id, value: option.id }, option.label))),
424
+ notice !== null && react.createElement('span', { style: { fontSize: 13, color: notice.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } }, notice.ok ? notice.ok : String(notice.error))
384
425
  ),
385
- usageError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usageError)),
386
- usage !== null && usage.ok === false && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usage.error)),
387
- usage !== null && usage.ok === true && react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
388
- react.createElement('div', { style: { fontSize: 13 } },
389
- usage.totals.keys + ' key(s) (' + usage.totals.okKeys + ' readable)' + (usage.totals.planLimit !== null ? ' · plan usage ' + usage.totals.planUsage + ' / ' + usage.totals.planLimit : '')
390
- ),
391
- react.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } },
392
- react.createElement('thead', null,
393
- react.createElement('tr', null,
394
- react.createElement('th', { style: headStyle }, 'API Key'),
395
- react.createElement('th', { style: headStyle }, 'Usage')
396
- )
397
- ),
398
- react.createElement('tbody', null,
399
- (server !== null ? server.keys : []).map((key, index) => {
400
- const row = usage.perKey[index]
401
- const pct = row && row.ok && row.planLimit != null && row.planLimit > 0 && row.planUsage != null
402
- ? Math.min(100, Math.round((row.planUsage / row.planLimit) * 100))
403
- : null
404
- return react.createElement('tr', { key: key.masked, style: { borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
405
- react.createElement('td', { style: Object.assign({}, cellStyle, { fontFamily: 'var(--ds-font-family-code, monospace)', fontSize: 12, wordBreak: 'break-all' }) }, key.masked),
406
- react.createElement('td', { style: cellStyle },
407
- react.createElement(UsageCircle, { percent: pct, label: pct != null ? pct + '%' : (row && row.ok && row.planUsage != null ? String(row.planUsage) : '—') })
408
- )
409
- )
410
- })
411
- )
412
- )
426
+ react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
427
+ (STRATEGIES.find((option) => option.id === strategy) || STRATEGIES[0]).hint + ' Saved immediately when selected; the first key then becomes primary for web_search.'
413
428
  )
414
429
  ),
415
- react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
416
- 'Usage data comes from the Tavily /usage endpoint (fetched locally; no keys are exposed).'
417
- )
430
+ usageError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usageError)),
431
+ usage !== null && usage.ok === false && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usage.error))
418
432
  )
419
433
  )
420
434
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moguiyu/dsh-tavily",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Tavily Search for DSH: multiple API keys, rotation/failover, usage gauge, settings card, and web_search toggle in one plugin.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/client.js CHANGED
@@ -149,10 +149,6 @@ export function factory(require) {
149
149
  }
150
150
  }
151
151
 
152
- const hasPending = Object.values(removing).some(Boolean)
153
- || Object.values(replacing).some(Boolean)
154
- || adds.some((item) => item.value.trim().length > 0)
155
-
156
152
  const saveStrategy = async (next) => {
157
153
  setBusy(true)
158
154
  setNotice(null)
@@ -173,29 +169,69 @@ export function factory(require) {
173
169
  }
174
170
  }
175
171
 
176
- const save = async () => {
172
+ const saveAdd = async (item) => {
173
+ const value = item.value.trim()
174
+ if (!value) return
177
175
  setBusy(true)
178
176
  setNotice(null)
179
177
  try {
180
- const addValues = adds.map((item) => item.value.trim()).filter((value) => value.length > 0)
181
- const removeMasked = [
182
- ...Object.keys(removing).filter((masked) => removing[masked] === true),
183
- ...Object.keys(replacing).filter((masked) => replacing[masked] === true)
184
- ]
185
178
  const response = await fetch('/api/tavily-manager', {
186
179
  method: 'POST',
187
180
  headers: { 'content-type': 'application/json' },
188
- body: JSON.stringify({ add: addValues, remove: removeMasked, strategy })
181
+ body: JSON.stringify({ add: [value], remove: [], strategy })
189
182
  })
190
183
  const data = await response.json()
191
184
  if (!data.ok) { setNotice({ error: data.error || 'Save failed' }); return }
192
- setNotice({ ok: 'Saved — effective on the next search.' })
193
- setRemoving({})
194
- setReplacing({})
195
- setReplaceDrafts({})
196
- setAdds([])
197
- setRevealed({})
198
- setConfirm({})
185
+ setNotice({ ok: 'Key saved.' })
186
+ setAdds((current) => current.filter((entry) => entry.id !== item.id))
187
+ await refresh()
188
+ } catch (error) {
189
+ setNotice({ error: String(error && error.message ? error.message : error) })
190
+ } finally {
191
+ setBusy(false)
192
+ }
193
+ }
194
+
195
+ const saveRemove = async (masked) => {
196
+ setBusy(true)
197
+ setNotice(null)
198
+ try {
199
+ const response = await fetch('/api/tavily-manager', {
200
+ method: 'POST',
201
+ headers: { 'content-type': 'application/json' },
202
+ body: JSON.stringify({ add: [], remove: [masked], strategy })
203
+ })
204
+ const data = await response.json()
205
+ if (!data.ok) { setNotice({ error: data.error || 'Delete failed' }); return }
206
+ setNotice({ ok: 'Key removed.' })
207
+ setConfirm((current) => Object.assign({}, current, { [masked]: false }))
208
+ setRemoving((current) => Object.assign({}, current, { [masked]: false }))
209
+ setRevealed((current) => Object.assign({}, current, { [masked]: false }))
210
+ await refresh()
211
+ } catch (error) {
212
+ setNotice({ error: String(error && error.message ? error.message : error) })
213
+ } finally {
214
+ setBusy(false)
215
+ }
216
+ }
217
+
218
+ const saveReplace = async (masked) => {
219
+ const value = typeof replaceDrafts[masked] === 'string' ? replaceDrafts[masked].trim() : ''
220
+ if (!value) return
221
+ setBusy(true)
222
+ setNotice(null)
223
+ try {
224
+ const response = await fetch('/api/tavily-manager', {
225
+ method: 'POST',
226
+ headers: { 'content-type': 'application/json' },
227
+ body: JSON.stringify({ add: [value], remove: [masked], strategy })
228
+ })
229
+ const data = await response.json()
230
+ if (!data.ok) { setNotice({ error: data.error || 'Update failed' }); return }
231
+ setNotice({ ok: 'Key updated.' })
232
+ setReplacing((current) => Object.assign({}, current, { [masked]: false }))
233
+ setReplaceDrafts((current) => Object.assign({}, current, { [masked]: '' }))
234
+ setRevealed((current) => Object.assign({}, current, { [masked]: false }))
199
235
  await refresh()
200
236
  } catch (error) {
201
237
  setNotice({ error: String(error && error.message ? error.message : error) })
@@ -231,6 +267,7 @@ export function factory(require) {
231
267
  setConfirm((current) => Object.assign({}, current, { [masked]: false }))
232
268
  setRemoving((current) => Object.assign({}, current, { [masked]: true }))
233
269
  setRevealed((current) => Object.assign({}, current, { [masked]: false }))
270
+ saveRemove(masked)
234
271
  }
235
272
 
236
273
  const restore = (masked) => {
@@ -296,16 +333,21 @@ export function factory(require) {
296
333
  react.createElement('thead', null,
297
334
  react.createElement('tr', null,
298
335
  react.createElement('th', { style: headStyle }, 'Key'),
336
+ react.createElement('th', { style: headStyle }, 'Usage'),
299
337
  react.createElement('th', { style: headStyle }, 'Saved'),
300
338
  react.createElement('th', { style: headStyle }, 'Actions')
301
339
  )
302
340
  ),
303
341
  react.createElement('tbody', null,
304
- (server !== null ? server.keys : []).map((key) => {
342
+ (server !== null ? server.keys : []).map((key, index) => {
305
343
  const masked = key.masked
306
344
  const isRemoved = removing[masked] === true
307
345
  const isReplacing = replacing[masked] === true
308
346
  const isRevealed = typeof revealed[masked] === 'string'
347
+ const usageRow = usage !== null && usage.ok === true ? usage.perKey[index] : null
348
+ const pct = usageRow && usageRow.ok && usageRow.planLimit != null && usageRow.planLimit > 0 && usageRow.planUsage != null
349
+ ? Math.min(100, Math.round((usageRow.planUsage / usageRow.planLimit) * 100))
350
+ : null
309
351
  return react.createElement('tr', { key: masked, style: { borderBottom: '1px solid var(--dsw-alias-border-l2)', opacity: isRemoved ? 0.45 : 1 } },
310
352
  react.createElement('td', { style: Object.assign({}, cellStyle, { fontFamily: 'var(--ds-font-family-code, monospace)', fontSize: 12, wordBreak: 'break-all' }) },
311
353
  isReplacing
@@ -324,11 +366,14 @@ export function factory(require) {
324
366
  })
325
367
  )
326
368
  ),
369
+ react.createElement('td', { style: cellStyle },
370
+ react.createElement(UsageCircle, { percent: pct, label: pct != null ? pct + '%' : (usageRow && usageRow.ok && usageRow.planUsage != null ? String(usageRow.planUsage) : '—') })
371
+ ),
327
372
  react.createElement('td', { style: Object.assign({}, cellStyle, { color: 'var(--dsw-alias-label-tertiary)', whiteSpace: 'nowrap' }) }, formatDate(key.savedAt)),
328
373
  react.createElement('td', { style: cellStyle },
329
374
  isReplacing
330
375
  ? react.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
331
- react.createElement(IconButton, { icon: 'check', title: 'Keep', onClick: () => { setReplacing((current) => Object.assign({}, current, { [masked]: false })) }, disabled: busy }),
376
+ react.createElement(IconButton, { icon: 'check', title: 'Save key', onClick: () => saveReplace(masked), disabled: busy || (typeof replaceDrafts[masked] === 'string' ? replaceDrafts[masked].trim().length === 0 : true) }),
332
377
  react.createElement(IconButton, { icon: 'close', title: 'Cancel', onClick: () => cancelReplace(masked), disabled: busy })
333
378
  )
334
379
  : react.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
@@ -353,73 +398,42 @@ export function factory(require) {
353
398
  placeholder: 'New key value',
354
399
  value: item.value,
355
400
  onChange: (event) => setAdds((current) => current.map((entry) => entry.id === item.id ? Object.assign({}, entry, { value: event.target.value }) : entry)),
401
+ onKeyDown: (event) => { if (event.key === 'Enter') saveAdd(item) },
356
402
  disabled: busy
357
403
  })
358
404
  ),
359
405
  react.createElement('td', { style: cellStyle }, '—'),
406
+ react.createElement('td', { style: cellStyle }, '—'),
360
407
  react.createElement('td', { style: cellStyle },
361
- react.createElement(IconButton, { icon: 'trash', title: 'Remove', danger: true, onClick: () => setAdds((current) => current.filter((entry) => entry.id !== item.id)), disabled: busy })
408
+ react.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
409
+ react.createElement(IconButton, { icon: 'check', title: 'Save key', onClick: () => saveAdd(item), disabled: busy || item.value.trim().length === 0 }),
410
+ react.createElement(IconButton, { icon: 'trash', title: 'Remove', danger: true, onClick: () => setAdds((current) => current.filter((entry) => entry.id !== item.id)), disabled: busy })
411
+ )
362
412
  )
363
413
  ))
364
414
  )
365
415
  ),
366
- react.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
367
- react.createElement('button', { type: 'button', style: btn, onClick: () => setAdds((current) => [...current, { id: 'add-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), value: '' }]), disabled: busy }, '+ Add key')
368
- ),
369
- react.createElement('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12, padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 8 } },
370
- react.createElement('label', { style: { fontWeight: 500, fontSize: 13 } }, 'Key usage strategy'),
371
- react.createElement('select', {
372
- style: { maxWidth: 280, height: 32, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', padding: '0 10px', fontSize: 13 },
373
- value: strategy,
374
- onChange: (event) => saveStrategy(event.target.value),
375
- disabled: busy
376
- }, STRATEGIES.map((option) => react.createElement('option', { key: option.id, value: option.id }, option.label))),
377
- react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
378
- (STRATEGIES.find((option) => option.id === strategy) || STRATEGIES[0]).hint + ' The strategy is saved immediately when you select it; the first key then becomes primary for web_search.'
379
- )
380
- ),
381
- react.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
382
- react.createElement('button', { type: 'button', style: Object.assign({}, btn, { height: 32, background: 'var(--dsw-alias-button-primary-fill)', color: 'var(--dsw-alias-label-primary-foreground)' }), onClick: save, disabled: busy || !hasPending }, 'Save key changes'),
383
- notice !== null && react.createElement('span', { style: { fontSize: 13, color: notice.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } }, notice.ok ? notice.ok : String(notice.error))
416
+ react.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
417
+ react.createElement('button', { type: 'button', style: btn, onClick: () => setAdds((current) => [...current, { id: 'add-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), value: '' }]), disabled: busy }, '+ Add key'),
418
+ react.createElement('button', { type: 'button', style: btn, onClick: refresh, disabled: busy }, 'Refresh usage')
384
419
  ),
385
- react.createElement('div', { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12, padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 8 } },
386
- react.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'space-between' } },
387
- react.createElement('span', { style: { fontWeight: 500, fontSize: 13 } }, 'Tavily Usage'),
388
- react.createElement('button', { type: 'button', style: btn, onClick: refresh, disabled: busy }, 'Refresh')
420
+ react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
421
+ react.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
422
+ react.createElement('label', { style: { fontWeight: 500, fontSize: 13 } }, 'Key usage strategy'),
423
+ react.createElement('select', {
424
+ style: { maxWidth: 280, height: 32, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', padding: '0 10px', fontSize: 13 },
425
+ value: strategy,
426
+ onChange: (event) => saveStrategy(event.target.value),
427
+ disabled: busy
428
+ }, STRATEGIES.map((option) => react.createElement('option', { key: option.id, value: option.id }, option.label))),
429
+ notice !== null && react.createElement('span', { style: { fontSize: 13, color: notice.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } }, notice.ok ? notice.ok : String(notice.error))
389
430
  ),
390
- usageError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usageError)),
391
- usage !== null && usage.ok === false && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usage.error)),
392
- usage !== null && usage.ok === true && react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
393
- react.createElement('div', { style: { fontSize: 13 } },
394
- usage.totals.keys + ' key(s) (' + usage.totals.okKeys + ' readable)' + (usage.totals.planLimit !== null ? ' · plan usage ' + usage.totals.planUsage + ' / ' + usage.totals.planLimit : '')
395
- ),
396
- react.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } },
397
- react.createElement('thead', null,
398
- react.createElement('tr', null,
399
- react.createElement('th', { style: headStyle }, 'API Key'),
400
- react.createElement('th', { style: headStyle }, 'Usage')
401
- )
402
- ),
403
- react.createElement('tbody', null,
404
- (server !== null ? server.keys : []).map((key, index) => {
405
- const row = usage.perKey[index]
406
- const pct = row && row.ok && row.planLimit != null && row.planLimit > 0 && row.planUsage != null
407
- ? Math.min(100, Math.round((row.planUsage / row.planLimit) * 100))
408
- : null
409
- return react.createElement('tr', { key: key.masked, style: { borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
410
- react.createElement('td', { style: Object.assign({}, cellStyle, { fontFamily: 'var(--ds-font-family-code, monospace)', fontSize: 12, wordBreak: 'break-all' }) }, key.masked),
411
- react.createElement('td', { style: cellStyle },
412
- react.createElement(UsageCircle, { percent: pct, label: pct != null ? pct + '%' : (row && row.ok && row.planUsage != null ? String(row.planUsage) : '—') })
413
- )
414
- )
415
- })
416
- )
417
- )
431
+ react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
432
+ (STRATEGIES.find((option) => option.id === strategy) || STRATEGIES[0]).hint + ' Saved immediately when selected; the first key then becomes primary for web_search.'
418
433
  )
419
434
  ),
420
- react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
421
- 'Usage data comes from the Tavily /usage endpoint (fetched locally; no keys are exposed).'
422
- )
435
+ usageError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usageError)),
436
+ usage !== null && usage.ok === false && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usage.error))
423
437
  )
424
438
  )
425
439
  )
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { readFileSync, writeFileSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
7
  import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
8
8
  import { defineTool } from '@deepseek-ai/dsh-tools'
9
- import { STRATEGIES, maskValue, parseKeyList, orderKeys, readJsonFile } from './lib.js'
9
+ import { STRATEGIES, isValidStrategy, maskValue, parseKeyList, orderKeys, readJsonFile } from './lib.js'
10
10
 
11
11
  export const name = 'dsh-tavily'
12
12