@ouraihub/aether-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +789 -0
- package/fixtures/model-classify.json +42 -0
- package/fixtures/pricing-cost.json +56 -0
- package/fixtures/reasoning-infer.json +44 -0
- package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
- package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
- package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
- package/fixtures/sse/anthropic-messages/text.sse +27 -0
- package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
- package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
- package/fixtures/sse/expect.json +20 -0
- package/fixtures/sse/openai-chat/chinese.sse +14 -0
- package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
- package/fixtures/sse/openai-chat/single-tool.sse +22 -0
- package/fixtures/sse/openai-chat/text.sse +14 -0
- package/fixtures/sse/openai-chat/truncated.sse +12 -0
- package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
- package/fixtures/sse/openai-responses/chinese.sse +33 -0
- package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
- package/fixtures/sse/openai-responses/single-tool.sse +27 -0
- package/fixtures/sse/openai-responses/text.sse +33 -0
- package/fixtures/sse/openai-responses/truncated.sse +30 -0
- package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
- package/package.json +71 -3
- package/src/CopyInput.svelte +33 -0
- package/src/KitNotifier.svelte +33 -0
- package/src/chat/ApprovalCard.svelte +23 -0
- package/src/chat/ChatPanel.svelte +2097 -0
- package/src/chat/MessageItem.svelte +583 -0
- package/src/chat/SkillPicker.svelte +126 -0
- package/src/chat/ToolCallItem.svelte +23 -0
- package/src/chat/UsageDetails.svelte +138 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/run-session.svelte.ts +556 -0
- package/src/components/ui/alert/alert-action.svelte +20 -0
- package/src/components/ui/alert/alert-description.svelte +23 -0
- package/src/components/ui/alert/alert-title.svelte +23 -0
- package/src/components/ui/alert/alert.svelte +43 -0
- package/src/components/ui/alert/index.ts +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
- package/src/components/ui/alert-dialog/index.ts +40 -0
- package/src/components/ui/button/button.svelte +81 -0
- package/src/components/ui/button/index.ts +17 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +34 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +54 -0
- package/src/components/ui/empty/empty-content.svelte +23 -0
- package/src/components/ui/empty/empty-description.svelte +23 -0
- package/src/components/ui/empty/empty-header.svelte +20 -0
- package/src/components/ui/empty/empty-media.svelte +41 -0
- package/src/components/ui/empty/empty-title.svelte +20 -0
- package/src/components/ui/empty/empty.svelte +23 -0
- package/src/components/ui/empty/index.ts +22 -0
- package/src/components/ui/field/field-content.svelte +20 -0
- package/src/components/ui/field/field-description.svelte +25 -0
- package/src/components/ui/field/field-error.svelte +58 -0
- package/src/components/ui/field/field-group.svelte +23 -0
- package/src/components/ui/field/field-label.svelte +25 -0
- package/src/components/ui/field/field-legend.svelte +24 -0
- package/src/components/ui/field/field-separator.svelte +35 -0
- package/src/components/ui/field/field-set.svelte +20 -0
- package/src/components/ui/field/field-title.svelte +20 -0
- package/src/components/ui/field/field.svelte +47 -0
- package/src/components/ui/field/index.ts +33 -0
- package/src/components/ui/input/index.ts +7 -0
- package/src/components/ui/input/input.svelte +48 -0
- package/src/components/ui/label/index.ts +7 -0
- package/src/components/ui/label/label.svelte +20 -0
- package/src/components/ui/select/index.ts +40 -0
- package/src/components/ui/select/select-content.svelte +45 -0
- package/src/components/ui/select/select-group-heading.svelte +21 -0
- package/src/components/ui/select/select-group.svelte +17 -0
- package/src/components/ui/select/select-item.svelte +43 -0
- package/src/components/ui/select/select-label.svelte +20 -0
- package/src/components/ui/select/select-portal.svelte +7 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/src/components/ui/select/select-separator.svelte +18 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select-value.svelte +7 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/separator/index.ts +7 -0
- package/src/components/ui/separator/separator.svelte +21 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +40 -0
- package/src/components/ui/switch/index.ts +7 -0
- package/src/components/ui/switch/switch.svelte +34 -0
- package/src/components/ui/textarea/index.ts +7 -0
- package/src/components/ui/textarea/textarea.svelte +22 -0
- package/src/context.ts +34 -0
- package/src/dev/Demo.svelte +45 -0
- package/src/dev/main.ts +7 -0
- package/src/i18n/en.ts +617 -0
- package/src/i18n/index.ts +23 -0
- package/src/i18n/zh-CN.ts +599 -0
- package/src/index.ts +10 -0
- package/src/logs/LogPanel.svelte +97 -0
- package/src/logs/LogSettings.svelte +56 -0
- package/src/logs/index.ts +5 -0
- package/src/logs/logger.ts +96 -0
- package/src/markdown.ts +8 -0
- package/src/mcp/McpClientSettings.svelte +491 -0
- package/src/mcp/McpServerSettings.svelte +327 -0
- package/src/mcp/config.ts +26 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/input.ts +73 -0
- package/src/notify.svelte.ts +43 -0
- package/src/ports/contract.ts +165 -0
- package/src/ports/fake.ts +484 -0
- package/src/ports/index.ts +7 -0
- package/src/ports/search.ts +47 -0
- package/src/ports/skill-sources.ts +45 -0
- package/src/providers/EndpointKeysDialog.svelte +343 -0
- package/src/providers/EndpointPanel.svelte +491 -0
- package/src/providers/ModelPickerDialog.svelte +187 -0
- package/src/providers/ModelSelect.svelte +434 -0
- package/src/providers/ModelSettingsDialog.svelte +453 -0
- package/src/providers/ProvidersSettings.svelte +631 -0
- package/src/providers/RequestConfigDialog.svelte +250 -0
- package/src/providers/SecretInput.svelte +276 -0
- package/src/providers/draft.ts +329 -0
- package/src/providers/edit-session.ts +146 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/reasoning.ts +41 -0
- package/src/providers/revision.svelte.ts +4 -0
- package/src/search/SearchSettings.svelte +703 -0
- package/src/search/index.ts +13 -0
- package/src/search/settings-flow.ts +62 -0
- package/src/session.ts +20 -0
- package/src/skills/SkillAdd.svelte +347 -0
- package/src/skills/SkillAuthoring.svelte +381 -0
- package/src/skills/SkillEditor.svelte +94 -0
- package/src/skills/SkillPreviewDialog.svelte +122 -0
- package/src/skills/SkillsSettings.svelte +140 -0
- package/src/skills/error.ts +32 -0
- package/src/skills/index.ts +4 -0
- package/src/styles/kit.css +137 -0
- package/src/styles/tokens.css +22 -0
- package/src/tauri/index.ts +282 -0
- package/src/utils.ts +22 -0
- package/src/web/index.ts +376 -0
- package/README.md +0 -3
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
<script lang="ts">
|
|
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
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
8
|
+
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
|
+
import * as Alert from '#lib/components/ui/alert/index.js'
|
|
12
|
+
import * as Empty from '#lib/components/ui/empty/index.js'
|
|
13
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
14
|
+
import { getKitContext } from '../context'
|
|
15
|
+
import { isKitClientError } from '../ports'
|
|
16
|
+
import { createClientSession } from '../session'
|
|
17
|
+
import { saveSearchSettings } from './settings-flow'
|
|
18
|
+
import type {
|
|
19
|
+
SearchCap,
|
|
20
|
+
SearchKey,
|
|
21
|
+
SearchPage,
|
|
22
|
+
SearchPreferences,
|
|
23
|
+
SearchProvider,
|
|
24
|
+
SearchResult,
|
|
25
|
+
SearchSettingsDto,
|
|
26
|
+
} from '../ports'
|
|
27
|
+
|
|
28
|
+
const { ports, i18n, notify } = getKitContext()
|
|
29
|
+
const { t } = i18n
|
|
30
|
+
const port = ports.search
|
|
31
|
+
const caps: SearchCap[] = ['search', 'fetch']
|
|
32
|
+
type Draft = { baseUrl?: string; basicAuthUsername?: string }
|
|
33
|
+
let settings = $state<Partial<Record<SearchCap, SearchSettingsDto>>>({})
|
|
34
|
+
let drafts = $state<Record<string, Draft>>({})
|
|
35
|
+
let keyDrafts = $state<Record<string, string>>({})
|
|
36
|
+
const draftRevisions: Record<string, Partial<Record<keyof Draft, number>>> = {}
|
|
37
|
+
const keyRevisions: Record<string, number> = {}
|
|
38
|
+
const flowSessions = { search: createClientSession(), fetch: createClientSession() }
|
|
39
|
+
let pending = $state<Record<string, boolean>>({})
|
|
40
|
+
let errors = $state<Record<string, string>>({})
|
|
41
|
+
let checks = $state<Record<string, number | undefined>>({})
|
|
42
|
+
let preferences = $state<SearchPreferences>({
|
|
43
|
+
maxResults: 5,
|
|
44
|
+
compression: 'none',
|
|
45
|
+
cutoffLimit: 2000,
|
|
46
|
+
blacklist: [],
|
|
47
|
+
modelToolsPreferred: false,
|
|
48
|
+
})
|
|
49
|
+
let blacklist = $state('')
|
|
50
|
+
let loading = $state(true)
|
|
51
|
+
let loadError = $state('')
|
|
52
|
+
let query = $state('')
|
|
53
|
+
let pageUrl = $state('')
|
|
54
|
+
let result = $state<SearchResult | null>(null)
|
|
55
|
+
let page = $state<SearchPage | null>(null)
|
|
56
|
+
const controller = new AbortController()
|
|
57
|
+
const options = { signal: controller.signal }
|
|
58
|
+
let keyDialog: HTMLDialogElement
|
|
59
|
+
let keyTarget = $state<{ cap: SearchCap; provider: string } | null>(null)
|
|
60
|
+
let editingId = $state<string | undefined>()
|
|
61
|
+
let editValue = $state('')
|
|
62
|
+
let password = $state('')
|
|
63
|
+
let revealed = $state<Record<string, string>>({})
|
|
64
|
+
let dialogError = $state('')
|
|
65
|
+
const dialogSession = createClientSession()
|
|
66
|
+
let editRevision = 0
|
|
67
|
+
let passwordRevision = 0
|
|
68
|
+
|
|
69
|
+
const keyOf = (cap: SearchCap, provider: string) => `${cap}:${provider}`
|
|
70
|
+
const errorText = (error: unknown) => (isKitClientError(error) ? t(`errors.${error.code}`) : t('errors.internal'))
|
|
71
|
+
const activeProvider = (cap: SearchCap) =>
|
|
72
|
+
settings[cap]?.providers.find((p) => p.id === settings[cap]?.defaultProvider)
|
|
73
|
+
const dialogProvider = $derived.by(() => {
|
|
74
|
+
const target = keyTarget
|
|
75
|
+
return target ? settings[target.cap]?.providers.find((p) => p.id === target.provider) : null
|
|
76
|
+
})
|
|
77
|
+
const dialogBusy = $derived(keyTarget !== null && pending[keyTarget.cap] === true)
|
|
78
|
+
const draftOf = (cap: SearchCap, provider: string) => drafts[keyOf(cap, provider)] ?? {}
|
|
79
|
+
function apply(next: SearchSettingsDto) {
|
|
80
|
+
if (!controller.signal.aborted) settings = { ...settings, [next.capability]: next }
|
|
81
|
+
}
|
|
82
|
+
function setPending(key: string, value: boolean) {
|
|
83
|
+
pending = { ...pending, [key]: value }
|
|
84
|
+
}
|
|
85
|
+
function setError(key: string, value: string) {
|
|
86
|
+
errors = { ...errors, [key]: value }
|
|
87
|
+
}
|
|
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
|
+
function patchDraft(cap: SearchCap, provider: string, patch: Draft) {
|
|
98
|
+
const key = keyOf(cap, provider)
|
|
99
|
+
drafts = { ...drafts, [key]: { ...drafts[key], ...patch } }
|
|
100
|
+
const revisions = draftRevisions[key] ?? {}
|
|
101
|
+
for (const field of Object.keys(patch) as (keyof Draft)[]) revisions[field] = (revisions[field] ?? 0) + 1
|
|
102
|
+
draftRevisions[key] = revisions
|
|
103
|
+
checks = { ...checks, [key]: undefined }
|
|
104
|
+
}
|
|
105
|
+
async function load() {
|
|
106
|
+
if (!port) return
|
|
107
|
+
loading = true
|
|
108
|
+
loadError = ''
|
|
109
|
+
try {
|
|
110
|
+
const list = await Promise.all(caps.map((cap) => port.settings(cap, options)))
|
|
111
|
+
if (controller.signal.aborted) return
|
|
112
|
+
for (const next of list) apply(next)
|
|
113
|
+
const first = list[0]
|
|
114
|
+
if (!first) throw new Error('Search settings missing')
|
|
115
|
+
preferences = first.preferences
|
|
116
|
+
blacklist = preferences.blacklist.join('\n')
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!controller.signal.aborted) loadError = errorText(error)
|
|
119
|
+
} finally {
|
|
120
|
+
if (!controller.signal.aborted) loading = false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function choose(cap: SearchCap, provider: string) {
|
|
124
|
+
if (!port || pending[cap]) return
|
|
125
|
+
setPending(cap, true)
|
|
126
|
+
setError(cap, '')
|
|
127
|
+
try {
|
|
128
|
+
apply(await port.setDefault(cap, provider, options))
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!controller.signal.aborted) setError(cap, errorText(error))
|
|
131
|
+
} finally {
|
|
132
|
+
setPending(cap, false)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function saveAndCheck(cap: SearchCap, provider: SearchProvider, check = false) {
|
|
136
|
+
if (!port || pending[cap]) return
|
|
137
|
+
const key = keyOf(cap, provider.id)
|
|
138
|
+
const draft = { ...draftOf(cap, provider.id) }
|
|
139
|
+
const revisions = { ...draftRevisions[key] }
|
|
140
|
+
const keyRevision = keyRevisions[key] ?? 0
|
|
141
|
+
const value = keyDrafts[key]?.trim()
|
|
142
|
+
const session = flowSessions[cap]
|
|
143
|
+
session.invalidate()
|
|
144
|
+
const token = session.capture()
|
|
145
|
+
const isCurrent = () => !controller.signal.aborted && session.isCurrent(token)
|
|
146
|
+
setPending(cap, true)
|
|
147
|
+
setError(cap, '')
|
|
148
|
+
checks = { ...checks, [key]: undefined }
|
|
149
|
+
try {
|
|
150
|
+
if (value && value.length > 4096) throw new Error('credential too long')
|
|
151
|
+
const saved = await saveSearchSettings(
|
|
152
|
+
port,
|
|
153
|
+
{
|
|
154
|
+
config: {
|
|
155
|
+
capability: cap,
|
|
156
|
+
provider: provider.id,
|
|
157
|
+
...(draft.baseUrl !== undefined ? { baseUrl: draft.baseUrl.trim() } : {}),
|
|
158
|
+
...(provider.basicAuth && draft.basicAuthUsername !== undefined
|
|
159
|
+
? { basicAuthUsername: draft.basicAuthUsername.trim() }
|
|
160
|
+
: {}),
|
|
161
|
+
},
|
|
162
|
+
...(value ? { key: { value } } : {}),
|
|
163
|
+
check,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
isCurrent,
|
|
167
|
+
onSettings: apply,
|
|
168
|
+
onConfigSaved(sent) {
|
|
169
|
+
const next = { ...drafts[key] }
|
|
170
|
+
for (const field of ['baseUrl', 'basicAuthUsername'] as const) {
|
|
171
|
+
if (sent[field] !== undefined && draftRevisions[key]?.[field] === revisions[field]) delete next[field]
|
|
172
|
+
}
|
|
173
|
+
drafts = { ...drafts, [key]: next }
|
|
174
|
+
},
|
|
175
|
+
onKeySaved() {
|
|
176
|
+
if ((keyRevisions[key] ?? 0) === keyRevision) keyDrafts = { ...keyDrafts, [key]: '' }
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
options,
|
|
180
|
+
)
|
|
181
|
+
if (!isCurrent()) return
|
|
182
|
+
if (saved.checked) checks = { ...checks, [key]: saved.checked.count }
|
|
183
|
+
else notify.toast('success', t('search.saved'))
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (isCurrent()) setError(cap, errorText(error))
|
|
186
|
+
} finally {
|
|
187
|
+
if (isCurrent()) setPending(cap, false)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function clearDialog() {
|
|
191
|
+
dialogSession.invalidate()
|
|
192
|
+
if (keyTarget) keyDrafts = { ...keyDrafts, [keyOf(keyTarget.cap, keyTarget.provider)]: '' }
|
|
193
|
+
keyTarget = null
|
|
194
|
+
editingId = undefined
|
|
195
|
+
editRevision++
|
|
196
|
+
editValue = ''
|
|
197
|
+
passwordRevision++
|
|
198
|
+
password = ''
|
|
199
|
+
revealed = {}
|
|
200
|
+
dialogError = ''
|
|
201
|
+
}
|
|
202
|
+
function openKeys(cap: SearchCap, provider: string) {
|
|
203
|
+
clearDialog()
|
|
204
|
+
keyTarget = { cap, provider }
|
|
205
|
+
keyDialog.showModal()
|
|
206
|
+
}
|
|
207
|
+
function invalidateCredentials() {
|
|
208
|
+
dialogSession.invalidate()
|
|
209
|
+
passwordRevision++
|
|
210
|
+
password = ''
|
|
211
|
+
revealed = {}
|
|
212
|
+
dialogError = ''
|
|
213
|
+
}
|
|
214
|
+
async function saveEditedKey() {
|
|
215
|
+
if (!port || !keyTarget || dialogBusy || !editValue.trim()) return
|
|
216
|
+
const target = { ...keyTarget }
|
|
217
|
+
const value = editValue.trim()
|
|
218
|
+
const id = editingId
|
|
219
|
+
const revision = editRevision
|
|
220
|
+
invalidateCredentials()
|
|
221
|
+
const token = dialogSession.capture()
|
|
222
|
+
const isCurrent = () => !controller.signal.aborted && dialogSession.isCurrent(token)
|
|
223
|
+
setPending(target.cap, true)
|
|
224
|
+
try {
|
|
225
|
+
const next = await port.saveKey(target.cap, target.provider, value, id, options)
|
|
226
|
+
if (!isCurrent()) return
|
|
227
|
+
apply(next)
|
|
228
|
+
if (revision === editRevision) {
|
|
229
|
+
editValue = ''
|
|
230
|
+
editingId = undefined
|
|
231
|
+
editRevision++
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (isCurrent()) dialogError = errorText(error)
|
|
235
|
+
} finally {
|
|
236
|
+
if (!controller.signal.aborted) setPending(target.cap, false)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function removeKey(key: SearchKey) {
|
|
240
|
+
if (!port || !keyTarget || dialogBusy) return
|
|
241
|
+
const target = { ...keyTarget }
|
|
242
|
+
const revision = editRevision
|
|
243
|
+
invalidateCredentials()
|
|
244
|
+
const token = dialogSession.capture()
|
|
245
|
+
const isCurrent = () => !controller.signal.aborted && dialogSession.isCurrent(token)
|
|
246
|
+
setPending(target.cap, true)
|
|
247
|
+
try {
|
|
248
|
+
const next = await port.removeKey(target.cap, target.provider, key.id, options)
|
|
249
|
+
if (!isCurrent()) return
|
|
250
|
+
apply(next)
|
|
251
|
+
if (editingId === key.id && revision === editRevision) {
|
|
252
|
+
editingId = undefined
|
|
253
|
+
editValue = ''
|
|
254
|
+
editRevision++
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (isCurrent()) dialogError = errorText(error)
|
|
258
|
+
} finally {
|
|
259
|
+
if (!controller.signal.aborted) setPending(target.cap, false)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function revealKey(key: SearchKey, copy = false) {
|
|
263
|
+
if (!port || !keyTarget || dialogBusy) return
|
|
264
|
+
const target = { ...keyTarget }
|
|
265
|
+
const token = dialogSession.capture()
|
|
266
|
+
const revision = passwordRevision
|
|
267
|
+
const currentPassword = port.revealNeedsPassword ? password : undefined
|
|
268
|
+
const isCurrent = () => !controller.signal.aborted && dialogSession.isCurrent(token)
|
|
269
|
+
setPending(target.cap, true)
|
|
270
|
+
dialogError = ''
|
|
271
|
+
try {
|
|
272
|
+
const value = await port.reveal(target.cap, target.provider, key.id, currentPassword, options)
|
|
273
|
+
if (!isCurrent()) return
|
|
274
|
+
if (copy) {
|
|
275
|
+
await navigator.clipboard.writeText(value)
|
|
276
|
+
if (isCurrent()) notify.toast('success', t('search.copied'))
|
|
277
|
+
} else revealed = { ...revealed, [key.id]: value }
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (isCurrent()) dialogError = copy && !isKitClientError(error) ? t('search.copyFailed') : errorText(error)
|
|
280
|
+
} finally {
|
|
281
|
+
if (isCurrent() && passwordRevision === revision) {
|
|
282
|
+
password = ''
|
|
283
|
+
passwordRevision++
|
|
284
|
+
}
|
|
285
|
+
if (!controller.signal.aborted) setPending(target.cap, false)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function savePreferences() {
|
|
289
|
+
if (!port || pending.preferences) return
|
|
290
|
+
setPending('preferences', true)
|
|
291
|
+
setError('preferences', '')
|
|
292
|
+
try {
|
|
293
|
+
const saved = await port.savePreferences(
|
|
294
|
+
{
|
|
295
|
+
...preferences,
|
|
296
|
+
blacklist: blacklist
|
|
297
|
+
.split('\n')
|
|
298
|
+
.map((rule) => rule.trim())
|
|
299
|
+
.filter(Boolean),
|
|
300
|
+
},
|
|
301
|
+
options,
|
|
302
|
+
)
|
|
303
|
+
if (!controller.signal.aborted) {
|
|
304
|
+
preferences = saved
|
|
305
|
+
blacklist = saved.blacklist.join('\n')
|
|
306
|
+
notify.toast('success', t('search.saved'))
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (!controller.signal.aborted) setError('preferences', errorText(error))
|
|
310
|
+
} finally {
|
|
311
|
+
setPending('preferences', false)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function search() {
|
|
315
|
+
if (!port || pending.query || !query.trim()) return
|
|
316
|
+
setPending('query', true)
|
|
317
|
+
setError('query', '')
|
|
318
|
+
result = null
|
|
319
|
+
try {
|
|
320
|
+
const next = await port.search(query.trim(), options)
|
|
321
|
+
if (!controller.signal.aborted) result = next
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (!controller.signal.aborted) setError('query', errorText(error))
|
|
324
|
+
} finally {
|
|
325
|
+
setPending('query', false)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function readPage() {
|
|
329
|
+
if (!port || pending.page || !pageUrl.trim()) return
|
|
330
|
+
setPending('page', true)
|
|
331
|
+
setError('page', '')
|
|
332
|
+
page = null
|
|
333
|
+
try {
|
|
334
|
+
const next = await port.fetch(pageUrl.trim(), options)
|
|
335
|
+
if (!controller.signal.aborted) page = next
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (!controller.signal.aborted) setError('page', errorText(error))
|
|
338
|
+
} finally {
|
|
339
|
+
setPending('page', false)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
onMount(() => {
|
|
343
|
+
void load()
|
|
344
|
+
return () => {
|
|
345
|
+
controller.abort()
|
|
346
|
+
for (const session of Object.values(flowSessions)) session.invalidate()
|
|
347
|
+
keyDrafts = {}
|
|
348
|
+
keyDialog?.close()
|
|
349
|
+
clearDialog()
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
</script>
|
|
353
|
+
|
|
354
|
+
{#if !port}
|
|
355
|
+
<Empty.Root><Empty.Description>{t('search.unavailable')}</Empty.Description></Empty.Root>
|
|
356
|
+
{:else if loading}
|
|
357
|
+
<p class="text-muted-foreground text-kit-sm">{t('search.loading')}</p>
|
|
358
|
+
{:else if loadError}
|
|
359
|
+
<Alert.Root variant="destructive"><Alert.Description>{loadError}</Alert.Description></Alert.Root>
|
|
360
|
+
<Button variant="outline" onclick={() => void load()}>{t('chat.retry')}</Button>
|
|
361
|
+
{:else}
|
|
362
|
+
<div class="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-4">
|
|
363
|
+
{#each caps as cap (cap)}
|
|
364
|
+
{@const provider = activeProvider(cap)}
|
|
365
|
+
{#if provider}
|
|
366
|
+
{@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>
|
|
503
|
+
{/if}
|
|
504
|
+
{/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>
|
|
609
|
+
</div>
|
|
610
|
+
{/if}
|
|
611
|
+
|
|
612
|
+
<dialog
|
|
613
|
+
bind:this={keyDialog}
|
|
614
|
+
aria-labelledby="search-key-dialog-title"
|
|
615
|
+
onclose={clearDialog}
|
|
616
|
+
class="bg-background text-foreground border-border m-auto max-h-[80dvh] w-[min(560px,calc(100vw-32px))] overflow-y-auto rounded-kit border p-4"
|
|
617
|
+
>
|
|
618
|
+
<h2 id="search-key-dialog-title" class="text-kit-base font-semibold">
|
|
619
|
+
{t('search.manageKeys')}{#if dialogProvider}
|
|
620
|
+
· {dialogProvider.name}{/if}
|
|
621
|
+
</h2>
|
|
622
|
+
{#if dialogProvider}
|
|
623
|
+
<Field.Group class="mt-3 gap-3">
|
|
624
|
+
{#if port?.revealNeedsPassword}<Field.Field
|
|
625
|
+
><Field.Label for="search-key-password">{t('search.currentPassword')}</Field.Label><Input
|
|
626
|
+
id="search-key-password"
|
|
627
|
+
type="password"
|
|
628
|
+
autocomplete="off"
|
|
629
|
+
disabled={dialogBusy}
|
|
630
|
+
value={password}
|
|
631
|
+
oninput={(event) => {
|
|
632
|
+
password = event.currentTarget.value
|
|
633
|
+
passwordRevision++
|
|
634
|
+
}}
|
|
635
|
+
/></Field.Field
|
|
636
|
+
>{/if}
|
|
637
|
+
<ul class="flex flex-col gap-3">
|
|
638
|
+
{#each dialogProvider.keys as key (key.id)}
|
|
639
|
+
<li class="flex min-w-0 flex-col gap-1 border-b pb-2 text-kit-sm">
|
|
640
|
+
<span class="break-all font-mono">{revealed[key.id] ?? key.masked}</span>
|
|
641
|
+
<div class="flex flex-wrap gap-1">
|
|
642
|
+
<Button
|
|
643
|
+
type="button"
|
|
644
|
+
variant="ghost"
|
|
645
|
+
size="sm"
|
|
646
|
+
disabled={dialogBusy}
|
|
647
|
+
onclick={() => {
|
|
648
|
+
invalidateCredentials()
|
|
649
|
+
editingId = key.id
|
|
650
|
+
editValue = ''
|
|
651
|
+
editRevision++
|
|
652
|
+
}}>{t('search.editKey')}</Button
|
|
653
|
+
>
|
|
654
|
+
<Button
|
|
655
|
+
type="button"
|
|
656
|
+
variant="ghost"
|
|
657
|
+
size="sm"
|
|
658
|
+
disabled={dialogBusy || (port?.revealNeedsPassword && !password)}
|
|
659
|
+
onclick={() => void revealKey(key)}>{t('search.reveal')}</Button
|
|
660
|
+
>
|
|
661
|
+
<Button
|
|
662
|
+
type="button"
|
|
663
|
+
variant="ghost"
|
|
664
|
+
size="sm"
|
|
665
|
+
disabled={dialogBusy || (port?.revealNeedsPassword && !password)}
|
|
666
|
+
onclick={() => void revealKey(key, true)}>{t('search.copy')}</Button
|
|
667
|
+
>
|
|
668
|
+
<Button type="button" variant="ghost" size="sm" disabled={dialogBusy} onclick={() => void removeKey(key)}
|
|
669
|
+
>{t('search.delete')}</Button
|
|
670
|
+
>
|
|
671
|
+
</div>
|
|
672
|
+
</li>
|
|
673
|
+
{/each}
|
|
674
|
+
</ul>
|
|
675
|
+
<Field.Field
|
|
676
|
+
><Field.Label for="search-edit-key">{t(editingId ? 'search.editKey' : 'search.newKey')}</Field.Label><Input
|
|
677
|
+
id="search-edit-key"
|
|
678
|
+
type="password"
|
|
679
|
+
autocomplete="off"
|
|
680
|
+
spellcheck={false}
|
|
681
|
+
maxlength={4096}
|
|
682
|
+
disabled={dialogBusy}
|
|
683
|
+
value={editValue}
|
|
684
|
+
oninput={(event) => {
|
|
685
|
+
editValue = event.currentTarget.value
|
|
686
|
+
editRevision++
|
|
687
|
+
}}
|
|
688
|
+
/></Field.Field
|
|
689
|
+
>
|
|
690
|
+
{#if dialogError}<p class="text-destructive text-kit-sm" role="alert">{dialogError}</p>{/if}
|
|
691
|
+
<div class="flex flex-wrap justify-end gap-2">
|
|
692
|
+
<Button
|
|
693
|
+
type="button"
|
|
694
|
+
size="sm"
|
|
695
|
+
disabled={dialogBusy || !editValue.trim() || (!editingId && dialogProvider.keys.length >= 16)}
|
|
696
|
+
onclick={() => void saveEditedKey()}>{t(editingId ? 'search.save' : 'search.addKey')}</Button
|
|
697
|
+
><Button type="button" variant="outline" size="sm" onclick={() => keyDialog.close()}
|
|
698
|
+
>{t('common.cancel')}</Button
|
|
699
|
+
>
|
|
700
|
+
</div>
|
|
701
|
+
</Field.Group>
|
|
702
|
+
{/if}
|
|
703
|
+
</dialog>
|