@ouraihub/aether-ui 0.1.0 → 0.1.2

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.
Files changed (90) hide show
  1. package/package.json +3 -3
  2. package/src/chat/ApprovalCard.svelte +8 -2
  3. package/src/chat/ChatComposer.svelte +319 -0
  4. package/src/chat/ChatMessage.svelte +262 -0
  5. package/src/chat/ChatPanel.css +386 -0
  6. package/src/chat/ChatPanel.svelte +276 -1880
  7. package/src/chat/ChatPanelApprovals.svelte +28 -0
  8. package/src/chat/ChatPanelComposer.svelte +336 -0
  9. package/src/chat/ChatPanelDialogs.svelte +242 -0
  10. package/src/chat/ChatPanelHeader.svelte +108 -0
  11. package/src/chat/ChatPanelMessages.svelte +166 -0
  12. package/src/chat/ChatPanelMode.svelte +190 -0
  13. package/src/chat/ChatPanelState.svelte.ts +1 -0
  14. package/src/chat/ChatPanelToolbar.svelte +436 -0
  15. package/src/chat/ChatSurface.svelte +168 -0
  16. package/src/chat/ContextUsageStatus.svelte +97 -0
  17. package/src/chat/MessageItem.svelte +111 -214
  18. package/src/chat/ModelEffortPicker.svelte +326 -0
  19. package/src/chat/PickerEffort.svelte +71 -0
  20. package/src/chat/ReasoningControl.svelte +184 -0
  21. package/src/chat/ReasoningSlider.svelte +174 -0
  22. package/src/chat/SkillPicker.svelte +14 -6
  23. package/src/chat/UsageControl.svelte +223 -0
  24. package/src/chat/UsageDetails.svelte +44 -90
  25. package/src/chat/chat-attachments.svelte.ts +78 -0
  26. package/src/chat/chat-panel-history.svelte.ts +430 -0
  27. package/src/chat/chat-panel-models.svelte.ts +97 -0
  28. package/src/chat/chat-panel-props.ts +38 -0
  29. package/src/chat/context-status.ts +101 -0
  30. package/src/chat/icons/LICENSE-REMIX +186 -0
  31. package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
  32. package/src/chat/icons/PathIcon.svelte +8 -0
  33. package/src/chat/icons/effort-icons.ts +65 -0
  34. package/src/chat/icons/status-icons.ts +105 -0
  35. package/src/chat/index.ts +25 -1
  36. package/src/chat/model-effort.ts +177 -0
  37. package/src/chat/run-session-history.ts +56 -0
  38. package/src/chat/run-session-types.ts +69 -0
  39. package/src/chat/run-session.svelte.ts +73 -135
  40. package/src/chat/test-support/chat-fixture.ts +194 -0
  41. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  42. package/src/chat/test-support/run-session-fixture.ts +112 -0
  43. package/src/dev/Demo.svelte +3 -1
  44. package/src/i18n/en-chat.d.ts +1 -0
  45. package/src/i18n/en-chat.js +201 -0
  46. package/src/i18n/en.ts +58 -180
  47. package/src/i18n/index.ts +7 -2
  48. package/src/i18n/zh-CN-chat.d.ts +1 -0
  49. package/src/i18n/zh-CN-chat.js +190 -0
  50. package/src/i18n/zh-CN.ts +8 -153
  51. package/src/logs/LogPanel.svelte +9 -3
  52. package/src/logs/LogSettings.svelte +3 -1
  53. package/src/mcp/McpClientEditor.svelte +204 -0
  54. package/src/mcp/McpClientSettings.svelte +57 -130
  55. package/src/mcp/McpServerSettings.svelte +47 -11
  56. package/src/mcp/input.ts +2 -1
  57. package/src/ports/contract.ts +11 -2
  58. package/src/ports/fake-optional.ts +81 -0
  59. package/src/ports/fake.ts +90 -86
  60. package/src/ports/search.ts +11 -2
  61. package/src/ports/test-support/fake-fixture.ts +63 -0
  62. package/src/providers/EndpointKeysDialog.svelte +27 -13
  63. package/src/providers/EndpointModels.svelte +311 -0
  64. package/src/providers/EndpointPanel.svelte +33 -258
  65. package/src/providers/ModelPickerDialog.svelte +59 -16
  66. package/src/providers/ModelPricing.svelte +125 -0
  67. package/src/providers/ModelSelect.svelte +22 -8
  68. package/src/providers/ModelSettingsDialog.svelte +61 -126
  69. package/src/providers/ProviderDialogs.svelte +120 -0
  70. package/src/providers/ProviderList.svelte +159 -0
  71. package/src/providers/ProvidersSettings.svelte +67 -207
  72. package/src/providers/RequestConfigDialog.svelte +40 -12
  73. package/src/providers/SecretInput.svelte +11 -3
  74. package/src/providers/draft.ts +39 -13
  75. package/src/providers/edit-session.ts +31 -7
  76. package/src/providers/reasoning.ts +18 -4
  77. package/src/providers/revision.svelte.ts +3 -1
  78. package/src/providers/test-support/providers-fixture.ts +120 -0
  79. package/src/search/SearchAdvancedTools.svelte +182 -0
  80. package/src/search/SearchProviderCard.svelte +195 -0
  81. package/src/search/SearchSettings.svelte +62 -265
  82. package/src/search/settings-flow.ts +8 -3
  83. package/src/skills/SkillAdd.svelte +51 -18
  84. package/src/skills/SkillAuthoring.svelte +69 -20
  85. package/src/skills/SkillEditor.svelte +13 -3
  86. package/src/skills/SkillPreviewDialog.svelte +19 -5
  87. package/src/skills/SkillsSettings.svelte +11 -4
  88. package/src/skills/test-support/skills-fixture.ts +139 -0
  89. package/src/tauri/index.ts +23 -7
  90. package/src/web/index.ts +90 -25
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ import ActivityIcon from '@lucide/svelte/icons/activity'
3
+ import ListIcon from '@lucide/svelte/icons/list'
4
+ import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
5
+ import ExternalLinkIcon from '@lucide/svelte/icons/external-link'
6
+ import { Button } from '#lib/components/ui/button/index.js'
7
+ import { Input } from '#lib/components/ui/input/index.js'
8
+ import CopyInput from '../CopyInput.svelte'
9
+ import * as Alert from '#lib/components/ui/alert/index.js'
10
+ import * as Field from '#lib/components/ui/field/index.js'
11
+ import { getKitContext } from '../context'
12
+ import type { SearchCap, SearchProvider, SearchSettingsDto } from '../ports'
13
+
14
+ type Draft = { baseUrl?: string; basicAuthUsername?: string }
15
+ let {
16
+ cap,
17
+ provider,
18
+ settings,
19
+ pending,
20
+ error,
21
+ draft,
22
+ keyDraft,
23
+ check,
24
+ onChoose,
25
+ onOpenKeys,
26
+ onKeyInput,
27
+ onPatchDraft,
28
+ onSave,
29
+ }: {
30
+ cap: SearchCap
31
+ provider: SearchProvider
32
+ settings: SearchSettingsDto
33
+ pending: boolean
34
+ error: string
35
+ draft: Draft
36
+ keyDraft: string
37
+ check?: number | undefined
38
+ onChoose: (provider: string) => void
39
+ onOpenKeys: () => void
40
+ onKeyInput: (value: string) => void
41
+ onPatchDraft: (patch: Draft) => void
42
+ onSave: (check?: boolean) => void
43
+ } = $props()
44
+
45
+ const { i18n } = getKitContext()
46
+ const { t } = i18n
47
+ const safeUrl = (raw?: string) => {
48
+ if (!raw) return null
49
+ try {
50
+ const url = new URL(raw)
51
+ return ['https:', 'http:'].includes(url.protocol) ? url.href : null
52
+ } catch {
53
+ return null
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <section
59
+ class="flex min-w-0 flex-col gap-3 rounded-kit border p-4"
60
+ aria-labelledby={`search-${cap}`}
61
+ >
62
+ <header class="flex flex-wrap items-start justify-between gap-3">
63
+ <div class="min-w-0 flex-1">
64
+ <h2 id={`search-${cap}`} class="text-kit-base font-semibold">{t(`search.${cap}`)}</h2>
65
+ <p class="text-muted-foreground text-kit-sm">
66
+ {provider.description}
67
+ {#if safeUrl(provider.officialWebsite)}<a
68
+ class="text-primary inline-flex"
69
+ href={safeUrl(provider.officialWebsite) ?? ''}
70
+ target="_blank"
71
+ rel="noopener noreferrer"
72
+ aria-label={t('search.website')}
73
+ title={t('search.website')}><ExternalLinkIcon size={13} /></a
74
+ >{/if}
75
+ </p>
76
+ </div>
77
+ <select
78
+ class="bg-background border-input h-kit-control max-w-full rounded-kit-sm border px-2 text-kit-sm"
79
+ aria-label={t(`search.${cap}`)}
80
+ disabled={pending}
81
+ value={settings.defaultProvider}
82
+ onchange={(event) => onChoose(event.currentTarget.value)}
83
+ >
84
+ {#each settings.providers as option (option.id)}<option value={option.id}
85
+ >{option.name}</option
86
+ >{/each}
87
+ </select>
88
+ </header>
89
+ {#if error}<Alert.Root variant="destructive"
90
+ ><Alert.Description>{error}</Alert.Description></Alert.Root
91
+ >{/if}
92
+ <Field.Group class="gap-3 border-t pt-3">
93
+ {#if provider.keyMode !== 'none' || provider.basicAuth}
94
+ <Field.Field data-disabled={pending}>
95
+ <div class="flex flex-wrap items-baseline gap-2">
96
+ <Field.Label for={`search-key-${cap}`}
97
+ >{provider.basicAuth ? t('search.password') : t('search.apiKey')}</Field.Label
98
+ >
99
+ {#if safeUrl(provider.apiKeyWebsite)}<a
100
+ class="text-primary text-kit-sm hover:underline"
101
+ href={safeUrl(provider.apiKeyWebsite) ?? ''}
102
+ target="_blank"
103
+ rel="noopener noreferrer">{t('search.apiKeyWebsite')}</a
104
+ >{/if}
105
+ </div>
106
+ <div class="flex min-w-0 items-center gap-2">
107
+ <Input
108
+ id={`search-key-${cap}`}
109
+ class="min-w-0 flex-1"
110
+ type="password"
111
+ autocomplete="off"
112
+ spellcheck={false}
113
+ maxlength={4096}
114
+ disabled={pending}
115
+ placeholder={t('search.newKey')}
116
+ value={keyDraft}
117
+ oninput={(event) => onKeyInput(event.currentTarget.value)}
118
+ />
119
+ <Button
120
+ type="button"
121
+ variant="outline"
122
+ size="icon-sm"
123
+ disabled={pending}
124
+ aria-label={t('search.manageKeys')}
125
+ title={t('search.manageKeys')}
126
+ onclick={onOpenKeys}><ListIcon /></Button
127
+ >
128
+ <Button
129
+ type="button"
130
+ variant="outline"
131
+ size="icon-sm"
132
+ disabled={pending}
133
+ aria-label={t('search.check')}
134
+ title={t('search.check')}
135
+ onclick={() => onSave(true)}
136
+ >
137
+ {#if pending}<LoaderCircleIcon class="animate-spin" />{:else}<ActivityIcon />{/if}
138
+ </Button>
139
+ </div>
140
+ <Field.Description
141
+ >{provider.keys.length
142
+ ? provider.keys.map((item) => item.masked).join(' · ')
143
+ : t('search.keyNotSet')}</Field.Description
144
+ >
145
+ </Field.Field>
146
+ {/if}
147
+ {#if provider.defaultBaseUrl !== undefined}
148
+ <Field.Field data-disabled={pending}>
149
+ <Field.Label for={`search-url-${cap}`}>{t('search.baseUrl')}</Field.Label>
150
+ <div class="flex min-w-0 items-center gap-2">
151
+ <CopyInput
152
+ id={`search-url-${cap}`}
153
+ class="min-w-0 flex-1"
154
+ disabled={pending}
155
+ value={draft.baseUrl ?? provider.baseUrl ?? provider.defaultBaseUrl}
156
+ oninput={(event) => onPatchDraft({ baseUrl: event.currentTarget.value })}
157
+ />
158
+ {#if provider.keyMode === 'none' && !provider.basicAuth}<Button
159
+ type="button"
160
+ variant="outline"
161
+ size="icon-sm"
162
+ disabled={pending}
163
+ aria-label={t('search.check')}
164
+ title={t('search.check')}
165
+ onclick={() => onSave(true)}><ActivityIcon /></Button
166
+ >{/if}
167
+ </div>
168
+ </Field.Field>
169
+ {/if}
170
+ {#if provider.basicAuth}
171
+ <Field.Field
172
+ ><Field.Label for={`search-user-${cap}`}>{t('search.username')}</Field.Label><Input
173
+ id={`search-user-${cap}`}
174
+ disabled={pending}
175
+ value={draft.basicAuthUsername ?? provider.basicAuthUsername ?? ''}
176
+ oninput={(event) => onPatchDraft({ basicAuthUsername: event.currentTarget.value })}
177
+ /></Field.Field
178
+ >
179
+ {/if}
180
+ </Field.Group>
181
+ <div class="flex flex-wrap items-center gap-2">
182
+ {#if provider.defaultBaseUrl !== undefined || provider.keyMode !== 'none'}<Button
183
+ type="button"
184
+ size="sm"
185
+ disabled={pending}
186
+ onclick={() => onSave()}>{t('search.save')}</Button
187
+ >{/if}
188
+ {#if pending}<span class="text-muted-foreground text-kit-sm" role="status"
189
+ >{t('search.checking')}</span
190
+ >
191
+ {:else if check !== undefined}<span class="text-muted-foreground text-kit-sm" role="status"
192
+ >{t('search.resultCount', { count: check })}</span
193
+ >{/if}
194
+ </div>
195
+ </section>
@@ -1,16 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte'
3
- import ActivityIcon from '@lucide/svelte/icons/activity'
4
- import ListIcon from '@lucide/svelte/icons/list'
5
- import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
6
- import ExternalLinkIcon from '@lucide/svelte/icons/external-link'
7
3
  import { Button } from '#lib/components/ui/button/index.js'
8
4
  import { Input } from '#lib/components/ui/input/index.js'
9
- import CopyInput from '../CopyInput.svelte'
10
- import { Textarea } from '#lib/components/ui/textarea/index.js'
11
5
  import * as Alert from '#lib/components/ui/alert/index.js'
12
6
  import * as Empty from '#lib/components/ui/empty/index.js'
13
7
  import * as Field from '#lib/components/ui/field/index.js'
8
+ import SearchProviderCard from './SearchProviderCard.svelte'
9
+ import SearchAdvancedTools from './SearchAdvancedTools.svelte'
14
10
  import { getKitContext } from '../context'
15
11
  import { isKitClientError } from '../ports'
16
12
  import { createClientSession } from '../session'
@@ -67,7 +63,8 @@
67
63
  let passwordRevision = 0
68
64
 
69
65
  const keyOf = (cap: SearchCap, provider: string) => `${cap}:${provider}`
70
- const errorText = (error: unknown) => (isKitClientError(error) ? t(`errors.${error.code}`) : t('errors.internal'))
66
+ const errorText = (error: unknown) =>
67
+ isKitClientError(error) ? t(`errors.${error.code}`) : t('errors.internal')
71
68
  const activeProvider = (cap: SearchCap) =>
72
69
  settings[cap]?.providers.find((p) => p.id === settings[cap]?.defaultProvider)
73
70
  const dialogProvider = $derived.by(() => {
@@ -85,20 +82,12 @@
85
82
  function setError(key: string, value: string) {
86
83
  errors = { ...errors, [key]: value }
87
84
  }
88
- function safeUrl(raw?: string) {
89
- if (!raw) return null
90
- try {
91
- const url = new URL(raw)
92
- return ['https:', 'http:'].includes(url.protocol) ? url.href : null
93
- } catch {
94
- return null
95
- }
96
- }
97
85
  function patchDraft(cap: SearchCap, provider: string, patch: Draft) {
98
86
  const key = keyOf(cap, provider)
99
87
  drafts = { ...drafts, [key]: { ...drafts[key], ...patch } }
100
88
  const revisions = draftRevisions[key] ?? {}
101
- for (const field of Object.keys(patch) as (keyof Draft)[]) revisions[field] = (revisions[field] ?? 0) + 1
89
+ for (const field of Object.keys(patch) as (keyof Draft)[])
90
+ revisions[field] = (revisions[field] ?? 0) + 1
102
91
  draftRevisions[key] = revisions
103
92
  checks = { ...checks, [key]: undefined }
104
93
  }
@@ -168,7 +157,8 @@
168
157
  onConfigSaved(sent) {
169
158
  const next = { ...drafts[key] }
170
159
  for (const field of ['baseUrl', 'basicAuthUsername'] as const) {
171
- if (sent[field] !== undefined && draftRevisions[key]?.[field] === revisions[field]) delete next[field]
160
+ if (sent[field] !== undefined && draftRevisions[key]?.[field] === revisions[field])
161
+ delete next[field]
172
162
  }
173
163
  drafts = { ...drafts, [key]: next }
174
164
  },
@@ -276,7 +266,8 @@
276
266
  if (isCurrent()) notify.toast('success', t('search.copied'))
277
267
  } else revealed = { ...revealed, [key.id]: value }
278
268
  } catch (error) {
279
- if (isCurrent()) dialogError = copy && !isKitClientError(error) ? t('search.copyFailed') : errorText(error)
269
+ if (isCurrent())
270
+ dialogError = copy && !isKitClientError(error) ? t('search.copyFailed') : errorText(error)
280
271
  } finally {
281
272
  if (isCurrent() && passwordRevision === revision) {
282
273
  password = ''
@@ -362,250 +353,47 @@
362
353
  <div class="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-4">
363
354
  {#each caps as cap (cap)}
364
355
  {@const provider = activeProvider(cap)}
365
- {#if provider}
356
+ {@const current = settings[cap]}
357
+ {#if provider && current}
366
358
  {@const key = keyOf(cap, provider.id)}
367
- <section class="flex min-w-0 flex-col gap-3 rounded-kit border p-4" aria-labelledby={`search-${cap}`}>
368
- <header class="flex flex-wrap items-start justify-between gap-3">
369
- <div class="min-w-0 flex-1">
370
- <h2 id={`search-${cap}`} class="text-kit-base font-semibold">{t(`search.${cap}`)}</h2>
371
- <p class="text-muted-foreground text-kit-sm">
372
- {provider.description}
373
- {#if safeUrl(provider.officialWebsite)}<a
374
- class="text-primary inline-flex"
375
- href={safeUrl(provider.officialWebsite) ?? ''}
376
- target="_blank"
377
- rel="noopener noreferrer"
378
- aria-label={t('search.website')}
379
- title={t('search.website')}><ExternalLinkIcon size={13} /></a
380
- >{/if}
381
- </p>
382
- </div>
383
- <select
384
- class="bg-background border-input h-kit-control max-w-full rounded-kit-sm border px-2 text-kit-sm"
385
- aria-label={t(`search.${cap}`)}
386
- disabled={pending[cap]}
387
- value={settings[cap]?.defaultProvider}
388
- onchange={(event) => void choose(cap, event.currentTarget.value)}
389
- >
390
- {#each settings[cap]?.providers ?? [] as option (option.id)}<option value={option.id}
391
- >{option.name}</option
392
- >{/each}
393
- </select>
394
- </header>
395
- {#if errors[cap]}<Alert.Root variant="destructive"
396
- ><Alert.Description>{errors[cap]}</Alert.Description></Alert.Root
397
- >{/if}
398
- <Field.Group class="gap-3 border-t pt-3">
399
- {#if provider.keyMode !== 'none' || provider.basicAuth}
400
- <Field.Field data-disabled={pending[cap]}>
401
- <div class="flex flex-wrap items-baseline gap-2">
402
- <Field.Label for={`search-key-${cap}`}
403
- >{provider.basicAuth ? t('search.password') : t('search.apiKey')}</Field.Label
404
- >
405
- {#if safeUrl(provider.apiKeyWebsite)}<a
406
- class="text-primary text-kit-sm hover:underline"
407
- href={safeUrl(provider.apiKeyWebsite) ?? ''}
408
- target="_blank"
409
- rel="noopener noreferrer">{t('search.apiKeyWebsite')}</a
410
- >{/if}
411
- </div>
412
- <div class="flex min-w-0 items-center gap-2">
413
- <Input
414
- id={`search-key-${cap}`}
415
- class="min-w-0 flex-1"
416
- type="password"
417
- autocomplete="off"
418
- spellcheck={false}
419
- maxlength={4096}
420
- disabled={pending[cap]}
421
- placeholder={t('search.newKey')}
422
- value={keyDrafts[key] ?? ''}
423
- oninput={(event) => {
424
- keyDrafts = { ...keyDrafts, [key]: event.currentTarget.value }
425
- keyRevisions[key] = (keyRevisions[key] ?? 0) + 1
426
- checks = { ...checks, [key]: undefined }
427
- }}
428
- />
429
- <Button
430
- type="button"
431
- variant="outline"
432
- size="icon-sm"
433
- disabled={pending[cap]}
434
- aria-label={t('search.manageKeys')}
435
- title={t('search.manageKeys')}
436
- onclick={() => openKeys(cap, provider.id)}><ListIcon /></Button
437
- >
438
- <Button
439
- type="button"
440
- variant="outline"
441
- size="icon-sm"
442
- disabled={pending[cap]}
443
- aria-label={t('search.check')}
444
- title={t('search.check')}
445
- onclick={() => void saveAndCheck(cap, provider, true)}
446
- >{#if pending[cap]}<LoaderCircleIcon class="animate-spin" />{:else}<ActivityIcon />{/if}</Button
447
- >
448
- </div>
449
- <Field.Description
450
- >{provider.keys.length
451
- ? provider.keys.map((item) => item.masked).join(' · ')
452
- : t('search.keyNotSet')}</Field.Description
453
- >
454
- </Field.Field>
455
- {/if}
456
- {#if provider.defaultBaseUrl !== undefined}
457
- <Field.Field data-disabled={pending[cap]}>
458
- <Field.Label for={`search-url-${cap}`}>{t('search.baseUrl')}</Field.Label>
459
- <div class="flex min-w-0 items-center gap-2">
460
- <CopyInput
461
- id={`search-url-${cap}`}
462
- class="min-w-0 flex-1"
463
- disabled={pending[cap]}
464
- value={draftOf(cap, provider.id).baseUrl ?? provider.baseUrl ?? provider.defaultBaseUrl}
465
- oninput={(event) => patchDraft(cap, provider.id, { baseUrl: event.currentTarget.value })}
466
- />
467
- {#if provider.keyMode === 'none' && !provider.basicAuth}<Button
468
- type="button"
469
- variant="outline"
470
- size="icon-sm"
471
- disabled={pending[cap]}
472
- aria-label={t('search.check')}
473
- title={t('search.check')}
474
- onclick={() => void saveAndCheck(cap, provider, true)}><ActivityIcon /></Button
475
- >{/if}
476
- </div>
477
- </Field.Field>
478
- {/if}
479
- {#if provider.basicAuth}
480
- <Field.Field
481
- ><Field.Label for={`search-user-${cap}`}>{t('search.username')}</Field.Label><Input
482
- id={`search-user-${cap}`}
483
- disabled={pending[cap]}
484
- value={draftOf(cap, provider.id).basicAuthUsername ?? provider.basicAuthUsername ?? ''}
485
- oninput={(event) => patchDraft(cap, provider.id, { basicAuthUsername: event.currentTarget.value })}
486
- /></Field.Field
487
- >
488
- {/if}
489
- </Field.Group>
490
- <div class="flex flex-wrap items-center gap-2">
491
- {#if provider.defaultBaseUrl !== undefined || provider.keyMode !== 'none'}<Button
492
- type="button"
493
- size="sm"
494
- disabled={pending[cap]}
495
- onclick={() => void saveAndCheck(cap, provider)}>{t('search.save')}</Button
496
- >{/if}
497
- {#if pending[cap]}<span class="text-muted-foreground text-kit-sm" role="status">{t('search.checking')}</span
498
- >{:else if checks[key] !== undefined}<span class="text-muted-foreground text-kit-sm" role="status"
499
- >{t('search.resultCount', { count: checks[key] ?? 0 })}</span
500
- >{/if}
501
- </div>
502
- </section>
359
+ <SearchProviderCard
360
+ {cap}
361
+ {provider}
362
+ settings={current}
363
+ pending={pending[cap] === true}
364
+ error={errors[cap] ?? ''}
365
+ draft={draftOf(cap, provider.id)}
366
+ keyDraft={keyDrafts[key] ?? ''}
367
+ check={checks[key]}
368
+ onChoose={(next) => void choose(cap, next)}
369
+ onOpenKeys={() => openKeys(cap, provider.id)}
370
+ onKeyInput={(value) => {
371
+ keyDrafts = { ...keyDrafts, [key]: value }
372
+ keyRevisions[key] = (keyRevisions[key] ?? 0) + 1
373
+ checks = { ...checks, [key]: undefined }
374
+ }}
375
+ onPatchDraft={(patch) => patchDraft(cap, provider.id, patch)}
376
+ onSave={(check = false) => void saveAndCheck(cap, provider, check)}
377
+ />
503
378
  {/if}
504
379
  {/each}
505
- <section class="rounded-kit border p-4">
506
- <details>
507
- <summary class="cursor-pointer text-kit-base font-semibold">{t('search.advanced')}</summary>
508
- <Field.Group class="mt-3 gap-3">
509
- <Field.Field orientation="horizontal"
510
- ><Field.Label for="search-max-results">{t('search.maxResults')}</Field.Label><Input
511
- id="search-max-results"
512
- class="ml-auto w-20"
513
- type="number"
514
- min="1"
515
- max="50"
516
- disabled={pending.preferences}
517
- bind:value={preferences.maxResults}
518
- /></Field.Field
519
- >
520
- <Field.Field orientation="horizontal"
521
- ><Field.Label for="search-compression">{t('search.compression')}</Field.Label><select
522
- id="search-compression"
523
- class="bg-background border-input h-kit-control ml-auto rounded-kit-sm border px-2 text-kit-sm"
524
- disabled={pending.preferences}
525
- bind:value={preferences.compression}
526
- ><option value="none">{t('search.compressionNone')}</option><option value="cutoff"
527
- >{t('search.compressionCutoff')}</option
528
- ></select
529
- ></Field.Field
530
- >
531
- {#if preferences.compression === 'cutoff'}<Field.Field orientation="horizontal"
532
- ><Field.Label for="search-cutoff">{t('search.cutoffLimit')}</Field.Label><Input
533
- id="search-cutoff"
534
- class="ml-auto w-24"
535
- type="number"
536
- min="100"
537
- max="20000"
538
- disabled={pending.preferences}
539
- bind:value={preferences.cutoffLimit}
540
- /></Field.Field
541
- >{/if}
542
- <Field.Field
543
- ><Field.Label for="search-blacklist">{t('search.blacklist')}</Field.Label><Textarea
544
- id="search-blacklist"
545
- rows={3}
546
- disabled={pending.preferences}
547
- placeholder="example.com\n*://*.example.org/*"
548
- bind:value={blacklist}
549
- /></Field.Field
550
- >
551
- </Field.Group>
552
- </details>
553
- <Field.Field class="mt-3 border-t pt-3"
554
- ><label class="flex items-center gap-2 text-kit-sm"
555
- ><input type="checkbox" disabled={pending.preferences} bind:checked={preferences.modelToolsPreferred} />{t(
556
- 'search.nativePreferred',
557
- )}</label
558
- ><Field.Description>{t('search.nativePrivacy')}</Field.Description></Field.Field
559
- >
560
- {#if errors.preferences}<p role="alert" class="text-destructive text-kit-sm">{errors.preferences}</p>{/if}
561
- <Button class="mt-3" type="button" size="sm" disabled={pending.preferences} onclick={() => void savePreferences()}
562
- >{t('search.save')}</Button
563
- >
564
- </section>
565
- <section class="flex min-w-0 flex-col gap-3 rounded-kit border p-4" aria-label={t('search.tools')}>
566
- <Field.Field
567
- ><Field.Label for="search-query">{t('search.query')}</Field.Label>
568
- <div class="flex min-w-0 gap-2">
569
- <Input id="search-query" class="min-w-0 flex-1" disabled={pending.query} bind:value={query} /><Button
570
- type="button"
571
- disabled={pending.query || !query.trim()}
572
- onclick={() => void search()}>{t('search.runSearch')}</Button
573
- >
574
- </div></Field.Field
575
- >
576
- {#if errors.query}<p class="text-destructive text-kit-sm" role="alert">{errors.query}</p>{/if}
577
- {#if result}<p class="text-muted-foreground text-kit-sm">
578
- {t('search.resultCount', { count: result.hits.length })}
579
- </p>
580
- {#each result.hits as hit (hit.url)}<article class="min-w-0 text-kit-sm">
581
- <a
582
- class="text-primary break-words hover:underline"
583
- href={safeUrl(hit.url) ?? ''}
584
- target="_blank"
585
- rel="noopener noreferrer">{hit.title}</a
586
- >
587
- <p class="whitespace-pre-wrap break-words">{hit.snippet}</p>
588
- </article>{/each}{/if}
589
- <Field.Field
590
- ><Field.Label for="search-page-url">{t('search.pageUrl')}</Field.Label>
591
- <div class="flex min-w-0 gap-2">
592
- <Input id="search-page-url" class="min-w-0 flex-1" disabled={pending.page} bind:value={pageUrl} /><Button
593
- type="button"
594
- disabled={pending.page || !pageUrl.trim()}
595
- onclick={() => void readPage()}>{t('search.read')}</Button
596
- >
597
- </div></Field.Field
598
- >
599
- {#if errors.page}<p class="text-destructive text-kit-sm" role="alert">{errors.page}</p>{/if}
600
- {#if page}<h3 class="text-kit-base font-medium">{page.title}</h3>
601
- <a
602
- class="text-primary break-words text-kit-sm"
603
- href={safeUrl(page.url) ?? ''}
604
- target="_blank"
605
- rel="noopener noreferrer">{page.url}</a
606
- >
607
- <pre class="max-h-96 overflow-auto whitespace-pre-wrap break-words text-kit-sm">{page.content}</pre>{/if}
608
- </section>
380
+ <SearchAdvancedTools
381
+ bind:preferences
382
+ bind:blacklist
383
+ bind:query
384
+ bind:pageUrl
385
+ preferencesPending={pending.preferences === true}
386
+ preferencesError={errors.preferences ?? ''}
387
+ {result}
388
+ {page}
389
+ queryPending={pending.query === true}
390
+ pagePending={pending.page === true}
391
+ queryError={errors.query ?? ''}
392
+ pageError={errors.page ?? ''}
393
+ onSavePreferences={() => void savePreferences()}
394
+ onSearch={() => void search()}
395
+ onReadPage={() => void readPage()}
396
+ />
609
397
  </div>
610
398
  {/if}
611
399
 
@@ -665,15 +453,21 @@
665
453
  disabled={dialogBusy || (port?.revealNeedsPassword && !password)}
666
454
  onclick={() => void revealKey(key, true)}>{t('search.copy')}</Button
667
455
  >
668
- <Button type="button" variant="ghost" size="sm" disabled={dialogBusy} onclick={() => void removeKey(key)}
669
- >{t('search.delete')}</Button
456
+ <Button
457
+ type="button"
458
+ variant="ghost"
459
+ size="sm"
460
+ disabled={dialogBusy}
461
+ onclick={() => void removeKey(key)}>{t('search.delete')}</Button
670
462
  >
671
463
  </div>
672
464
  </li>
673
465
  {/each}
674
466
  </ul>
675
467
  <Field.Field
676
- ><Field.Label for="search-edit-key">{t(editingId ? 'search.editKey' : 'search.newKey')}</Field.Label><Input
468
+ ><Field.Label for="search-edit-key"
469
+ >{t(editingId ? 'search.editKey' : 'search.newKey')}</Field.Label
470
+ ><Input
677
471
  id="search-edit-key"
678
472
  type="password"
679
473
  autocomplete="off"
@@ -692,8 +486,11 @@
692
486
  <Button
693
487
  type="button"
694
488
  size="sm"
695
- disabled={dialogBusy || !editValue.trim() || (!editingId && dialogProvider.keys.length >= 16)}
696
- onclick={() => void saveEditedKey()}>{t(editingId ? 'search.save' : 'search.addKey')}</Button
489
+ disabled={dialogBusy ||
490
+ !editValue.trim() ||
491
+ (!editingId && dialogProvider.keys.length >= 16)}
492
+ onclick={() => void saveEditedKey()}
493
+ >{t(editingId ? 'search.save' : 'search.addKey')}</Button
697
494
  ><Button type="button" variant="outline" size="sm" onclick={() => keyDialog.close()}
698
495
  >{t('common.cancel')}</Button
699
496
  >
@@ -17,7 +17,8 @@ export async function saveSearchSettings(
17
17
  const config = { ...input.config }
18
18
  const key = input.key ? { ...input.key } : undefined
19
19
  const check = input.check
20
- const cancelled = () => new KitClientError({ code: 'cancelled', message: 'Search settings cancelled', details: {} })
20
+ const cancelled = () =>
21
+ new KitClientError({ code: 'cancelled', message: 'Search settings cancelled', details: {} })
21
22
  function assertCurrent() {
22
23
  if (options?.signal?.aborted || !session.isCurrent()) throw cancelled()
23
24
  }
@@ -51,12 +52,16 @@ export async function saveSearchSettings(
51
52
  assertCurrent()
52
53
  session.onConfigSaved(config)
53
54
  if (key?.value.trim()) {
54
- settings = await step(() => port.saveKey(config.capability, config.provider, key.value, key.keyId, options))
55
+ settings = await step(() =>
56
+ port.saveKey(config.capability, config.provider, key.value, key.keyId, options),
57
+ )
55
58
  session.onSettings(settings)
56
59
  assertCurrent()
57
60
  session.onKeySaved()
58
61
  }
59
- const checked = check ? await step(() => port.check(config.capability, config.provider, options)) : undefined
62
+ const checked = check
63
+ ? await step(() => port.check(config.capability, config.provider, options))
64
+ : undefined
60
65
  assertCurrent()
61
66
  return checked ? { settings, checked } : { settings }
62
67
  }