@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,491 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// External MCP servers the in-app AI may call (docs/16 §4.4, docs/13 §2): list, edit with
|
|
3
|
+
// write-only credentials, test connection, trust/enable/private-network switches, and each
|
|
4
|
+
// tool's approval default. Only enabled + trusted servers' tools reach a run. A server is
|
|
5
|
+
// reached over Streamable HTTP or as a local stdio command.
|
|
6
|
+
import { onDestroy } from 'svelte'
|
|
7
|
+
import { Alert, AlertDescription } from '#lib/components/ui/alert/index.js'
|
|
8
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
9
|
+
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
10
|
+
import * as Empty from '#lib/components/ui/empty/index.js'
|
|
11
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
12
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
13
|
+
import CopyInput from '../CopyInput.svelte'
|
|
14
|
+
import * as Select from '#lib/components/ui/select/index.js'
|
|
15
|
+
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
16
|
+
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
17
|
+
import { getKitContext } from '../context'
|
|
18
|
+
import { isKitClientError } from '../ports'
|
|
19
|
+
import type { Approval, McpTransport, ServerDto, ServerSave } from '../ports'
|
|
20
|
+
import { createClientSession } from '../session'
|
|
21
|
+
import { mapMcpInput } from './input'
|
|
22
|
+
|
|
23
|
+
const APPROVALS: Approval[] = ['auto', 'ask', 'deny']
|
|
24
|
+
const KINDS: McpTransport['kind'][] = ['http', 'stdio']
|
|
25
|
+
const ARGS_PLACEHOLDER = '-y\n@modelcontextprotocol/server-filesystem'
|
|
26
|
+
|
|
27
|
+
const { ports, i18n, notify, logger } = getKitContext()
|
|
28
|
+
const client = ports.mcpClient
|
|
29
|
+
const t = i18n.t
|
|
30
|
+
|
|
31
|
+
let servers = $state<ServerDto[]>([])
|
|
32
|
+
let busy = $state(false)
|
|
33
|
+
/** Last "test connection" failure per server id. */
|
|
34
|
+
let testErrors = $state<Record<string, string>>({})
|
|
35
|
+
|
|
36
|
+
let editing = $state(false)
|
|
37
|
+
let editId = $state<string | null>(null)
|
|
38
|
+
let name = $state('')
|
|
39
|
+
let slug = $state('')
|
|
40
|
+
let kind = $state<McpTransport['kind']>('http')
|
|
41
|
+
let url = $state('')
|
|
42
|
+
let command = $state('')
|
|
43
|
+
let args = $state('')
|
|
44
|
+
let cwd = $state('')
|
|
45
|
+
let replaceSecret = $state(true)
|
|
46
|
+
let headers = $state('')
|
|
47
|
+
let query = $state('')
|
|
48
|
+
let env = $state('')
|
|
49
|
+
let currentKeys = $state<string[]>([])
|
|
50
|
+
const editSession = createClientSession()
|
|
51
|
+
let editTarget: ServerDto | null = null
|
|
52
|
+
let editorSaving = $state(false)
|
|
53
|
+
let editRevision = 0
|
|
54
|
+
let secretRevision = 0
|
|
55
|
+
|
|
56
|
+
function clearSecrets() {
|
|
57
|
+
headers = ''
|
|
58
|
+
query = ''
|
|
59
|
+
env = ''
|
|
60
|
+
secretRevision++
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function invalidateEditor() {
|
|
64
|
+
editSession.invalidate()
|
|
65
|
+
editorSaving = false
|
|
66
|
+
clearSecrets()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function closeEditor() {
|
|
70
|
+
invalidateEditor()
|
|
71
|
+
editing = false
|
|
72
|
+
editTarget = null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function changeKind(next: McpTransport['kind']) {
|
|
76
|
+
if (next === kind) return
|
|
77
|
+
invalidateEditor()
|
|
78
|
+
editRevision++
|
|
79
|
+
kind = next
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function changeReplacement(next: boolean) {
|
|
83
|
+
invalidateEditor()
|
|
84
|
+
editRevision++
|
|
85
|
+
replaceSecret = next
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onDestroy(closeEditor)
|
|
89
|
+
|
|
90
|
+
// Actions only render when the port exists; the throw marks a host wiring bug.
|
|
91
|
+
function need() {
|
|
92
|
+
if (!client) throw new Error('mcpClient port missing')
|
|
93
|
+
return client
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function errorText(error: unknown): string {
|
|
97
|
+
if (!isKitClientError(error)) return t('errors.internal')
|
|
98
|
+
const { reason, field } = error.details
|
|
99
|
+
if (reason === 'credential_query') return t('mcp.client.err.credentialQuery')
|
|
100
|
+
if (reason === 'duplicate_slug') return t('mcp.client.err.duplicateSlug')
|
|
101
|
+
if (reason === 'credential_arg') return t('mcp.client.err.credentialArg')
|
|
102
|
+
if (reason === 'transport_changed') return t('mcp.client.err.transportChanged')
|
|
103
|
+
return field ? `${t(`errors.${error.code}`)} (${field})` : t(`errors.${error.code}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fail(error: unknown) {
|
|
107
|
+
logger.warn('mcp.client.ui_failed', { code: isKitClientError(error) ? error.code : 'internal' })
|
|
108
|
+
notify.toast('error', errorText(error))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function act(run: () => Promise<void>) {
|
|
112
|
+
busy = true
|
|
113
|
+
try {
|
|
114
|
+
await run()
|
|
115
|
+
} catch (error) {
|
|
116
|
+
fail(error)
|
|
117
|
+
} finally {
|
|
118
|
+
busy = false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$effect(() => {
|
|
123
|
+
if (client) void act(async () => void (servers = await client.list()))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
function base(server: ServerDto): ServerSave {
|
|
127
|
+
const { id, slug, name, transport, enabled, trusted, allowPrivate } = server
|
|
128
|
+
return { id, slug, name, transport, enabled, trusted, allowPrivate }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One-line location shown in the list: the URL, or the command line. */
|
|
132
|
+
function location(transport: McpTransport): string {
|
|
133
|
+
return transport.kind === 'http' ? transport.url : [transport.command, ...transport.args].join(' ')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const update = (next: ServerSave) => act(async () => void (servers = await need().save(next)))
|
|
137
|
+
|
|
138
|
+
async function setTrusted(server: ServerDto, on: boolean) {
|
|
139
|
+
if (on) {
|
|
140
|
+
const ok = await notify.confirm({
|
|
141
|
+
title: t('mcp.client.trustTitle'),
|
|
142
|
+
body: t('mcp.client.trustBody', { name: server.name }),
|
|
143
|
+
})
|
|
144
|
+
if (!ok) return
|
|
145
|
+
}
|
|
146
|
+
await update({ ...base(server), trusted: on })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function setPrivate(server: ServerDto, on: boolean) {
|
|
150
|
+
if (on) {
|
|
151
|
+
const ok = await notify.confirm({
|
|
152
|
+
title: t('mcp.client.privateTitle'),
|
|
153
|
+
body: t('mcp.client.privateBody', { url: location(server.transport) }),
|
|
154
|
+
danger: true,
|
|
155
|
+
})
|
|
156
|
+
if (!ok) return
|
|
157
|
+
}
|
|
158
|
+
await update({ ...base(server), allowPrivate: on })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const test = (server: ServerDto) =>
|
|
162
|
+
act(async () => {
|
|
163
|
+
testErrors = Object.fromEntries(Object.entries(testErrors).filter(([id]) => id !== server.id))
|
|
164
|
+
try {
|
|
165
|
+
await need().refresh(server.id)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
testErrors = { ...testErrors, [server.id]: errorText(error) }
|
|
168
|
+
}
|
|
169
|
+
servers = await need().list()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const setApproval = (server: ServerDto, tool: string, approval: Approval) =>
|
|
173
|
+
act(async () => {
|
|
174
|
+
await need().setToolApproval(server.id, tool, approval)
|
|
175
|
+
servers = await need().list()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const remove = (server: ServerDto) =>
|
|
179
|
+
act(async () => {
|
|
180
|
+
const ok = await notify.confirm({
|
|
181
|
+
title: t('mcp.client.deleteTitle'),
|
|
182
|
+
body: t('mcp.client.deleteBody', { name: server.name }),
|
|
183
|
+
danger: true,
|
|
184
|
+
})
|
|
185
|
+
if (ok) servers = await need().remove(server.id)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
function edit(server: ServerDto | null) {
|
|
189
|
+
invalidateEditor()
|
|
190
|
+
editTarget = server
|
|
191
|
+
editRevision++
|
|
192
|
+
const transport = server?.transport
|
|
193
|
+
editId = server?.id ?? null
|
|
194
|
+
name = server?.name ?? ''
|
|
195
|
+
slug = server?.slug ?? ''
|
|
196
|
+
kind = transport?.kind ?? 'http'
|
|
197
|
+
url = transport?.kind === 'http' ? transport.url : ''
|
|
198
|
+
command = transport?.kind === 'stdio' ? transport.command : ''
|
|
199
|
+
args = transport?.kind === 'stdio' ? transport.args.join('\n') : ''
|
|
200
|
+
cwd = transport?.kind === 'stdio' ? (transport.cwd ?? '') : ''
|
|
201
|
+
currentKeys = server?.secretKeys ?? []
|
|
202
|
+
replaceSecret = server === null
|
|
203
|
+
editing = true
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function save() {
|
|
207
|
+
const mapped = mapMcpInput(editTarget, {
|
|
208
|
+
name,
|
|
209
|
+
slug,
|
|
210
|
+
kind,
|
|
211
|
+
url,
|
|
212
|
+
command,
|
|
213
|
+
args,
|
|
214
|
+
cwd,
|
|
215
|
+
replaceSecret,
|
|
216
|
+
headers,
|
|
217
|
+
query,
|
|
218
|
+
env,
|
|
219
|
+
})
|
|
220
|
+
if (!mapped.ok) {
|
|
221
|
+
notify.toast('error', t(`mcp.client.err.${mapped.error}`))
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
const token = editSession.capture()
|
|
225
|
+
const revision = editRevision
|
|
226
|
+
const sentSecretRevision = secretRevision
|
|
227
|
+
editorSaving = true
|
|
228
|
+
try {
|
|
229
|
+
const next = await need().save(mapped.value)
|
|
230
|
+
if (!editSession.isCurrent(token)) return
|
|
231
|
+
servers = next
|
|
232
|
+
if (editRevision === revision) {
|
|
233
|
+
closeEditor()
|
|
234
|
+
} else {
|
|
235
|
+
// Preserve edits made while saving, but do not retain successfully sent secrets.
|
|
236
|
+
const stored = editTarget
|
|
237
|
+
? next.find((server) => server.id === editTarget?.id)
|
|
238
|
+
: next.find((server) => server.slug === mapped.value.slug)
|
|
239
|
+
if (stored) {
|
|
240
|
+
editTarget = stored
|
|
241
|
+
editId = stored.id
|
|
242
|
+
currentKeys = stored.secretKeys
|
|
243
|
+
}
|
|
244
|
+
if (sentSecretRevision === secretRevision) {
|
|
245
|
+
clearSecrets()
|
|
246
|
+
replaceSecret = false
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (editSession.isCurrent(token)) fail(error)
|
|
251
|
+
} finally {
|
|
252
|
+
if (editSession.isCurrent(token)) editorSaving = false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
{#if client}
|
|
258
|
+
<section class="flex flex-col gap-4" aria-label={t('mcp.client.title')}>
|
|
259
|
+
<div class="flex items-center gap-2">
|
|
260
|
+
<p class="text-muted-foreground flex-1 text-sm">{t('mcp.client.hint')}</p>
|
|
261
|
+
<Button disabled={busy} onclick={() => edit(null)}>{t('mcp.client.add')}</Button>
|
|
262
|
+
</div>
|
|
263
|
+
{#if servers.length === 0}
|
|
264
|
+
<Empty.Root>
|
|
265
|
+
<Empty.Header>
|
|
266
|
+
<Empty.Title>{t('mcp.client.empty')}</Empty.Title>
|
|
267
|
+
<Empty.Description>{t('mcp.client.emptyHint')}</Empty.Description>
|
|
268
|
+
</Empty.Header>
|
|
269
|
+
</Empty.Root>
|
|
270
|
+
{/if}
|
|
271
|
+
<ul class="flex flex-col gap-3">
|
|
272
|
+
{#each servers as server (server.id)}
|
|
273
|
+
<li class="flex flex-col gap-3 rounded-md border p-3" aria-label={server.name}>
|
|
274
|
+
<div class="flex items-start gap-3">
|
|
275
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
276
|
+
<span class="truncate font-medium"
|
|
277
|
+
>{server.name} <span class="text-muted-foreground">({server.slug})</span></span
|
|
278
|
+
>
|
|
279
|
+
<span class="text-muted-foreground truncate font-mono text-xs"
|
|
280
|
+
>{t(`mcp.client.transport.${server.transport.kind}`)} · {location(server.transport)}</span
|
|
281
|
+
>
|
|
282
|
+
{#if server.secretKeys.length}
|
|
283
|
+
<span class="text-muted-foreground text-xs">
|
|
284
|
+
{t('mcp.client.secretKeys', { keys: server.secretKeys.join(', ') })}
|
|
285
|
+
</span>
|
|
286
|
+
{/if}
|
|
287
|
+
</div>
|
|
288
|
+
<Button size="sm" variant="outline" disabled={busy} onclick={() => void test(server)}>
|
|
289
|
+
{t('mcp.client.test')}
|
|
290
|
+
</Button>
|
|
291
|
+
<Button size="sm" variant="outline" disabled={busy} onclick={() => edit(server)}>
|
|
292
|
+
{t('mcp.client.edit')}
|
|
293
|
+
</Button>
|
|
294
|
+
<Button size="sm" variant="destructive" disabled={busy} onclick={() => void remove(server)}>
|
|
295
|
+
{t('mcp.client.delete')}
|
|
296
|
+
</Button>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="flex flex-wrap gap-6">
|
|
299
|
+
<Field.Field orientation="horizontal" class="w-auto">
|
|
300
|
+
<Switch
|
|
301
|
+
id={`kit-mcp-enabled-${server.id}`}
|
|
302
|
+
checked={server.enabled}
|
|
303
|
+
disabled={busy}
|
|
304
|
+
onCheckedChange={(on) => void update({ ...base(server), enabled: on })}
|
|
305
|
+
/>
|
|
306
|
+
<Field.Label for={`kit-mcp-enabled-${server.id}`}>{t('mcp.client.enabled')}</Field.Label>
|
|
307
|
+
</Field.Field>
|
|
308
|
+
<Field.Field orientation="horizontal" class="w-auto">
|
|
309
|
+
<Switch
|
|
310
|
+
id={`kit-mcp-trusted-${server.id}`}
|
|
311
|
+
checked={server.trusted}
|
|
312
|
+
disabled={busy}
|
|
313
|
+
onCheckedChange={(on) => void setTrusted(server, on)}
|
|
314
|
+
/>
|
|
315
|
+
<Field.Label for={`kit-mcp-trusted-${server.id}`}>{t('mcp.client.trusted')}</Field.Label>
|
|
316
|
+
</Field.Field>
|
|
317
|
+
{#if server.transport.kind === 'http'}
|
|
318
|
+
<Field.Field orientation="horizontal" class="w-auto">
|
|
319
|
+
<Switch
|
|
320
|
+
id={`kit-mcp-private-${server.id}`}
|
|
321
|
+
checked={server.allowPrivate}
|
|
322
|
+
disabled={busy}
|
|
323
|
+
onCheckedChange={(on) => void setPrivate(server, on)}
|
|
324
|
+
/>
|
|
325
|
+
<Field.Label for={`kit-mcp-private-${server.id}`}>{t('mcp.client.allowPrivate')}</Field.Label>
|
|
326
|
+
</Field.Field>
|
|
327
|
+
{/if}
|
|
328
|
+
</div>
|
|
329
|
+
{#if testErrors[server.id]}
|
|
330
|
+
<Alert variant="destructive">
|
|
331
|
+
<AlertDescription>{t('mcp.client.testFailed', { error: testErrors[server.id] ?? '' })}</AlertDescription>
|
|
332
|
+
</Alert>
|
|
333
|
+
{/if}
|
|
334
|
+
{#if server.tools.length}
|
|
335
|
+
<ul class="flex flex-col gap-2" aria-label={t('mcp.client.tools')}>
|
|
336
|
+
{#each server.tools as tool (tool.name)}
|
|
337
|
+
<li class="flex items-center gap-3">
|
|
338
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
339
|
+
<span class="truncate font-mono text-sm">{tool.name}</span>
|
|
340
|
+
<span class="text-muted-foreground truncate text-xs">
|
|
341
|
+
{tool.description}{#if tool.readOnly && tool.approval !== 'auto'}
|
|
342
|
+
· {t('mcp.client.readOnlyHint')}{/if}
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
<Select.Root
|
|
346
|
+
type="single"
|
|
347
|
+
value={tool.approval}
|
|
348
|
+
onValueChange={(value) => void setApproval(server, tool.name, value as Approval)}
|
|
349
|
+
>
|
|
350
|
+
<Select.Trigger
|
|
351
|
+
size="sm"
|
|
352
|
+
class="w-28"
|
|
353
|
+
aria-label={t('mcp.client.approvalFor', { tool: tool.name })}
|
|
354
|
+
>
|
|
355
|
+
{t(`mcp.client.approval.${tool.approval}`)}
|
|
356
|
+
</Select.Trigger>
|
|
357
|
+
<Select.Content>
|
|
358
|
+
<Select.Group>
|
|
359
|
+
{#each APPROVALS as approval (approval)}
|
|
360
|
+
<Select.Item value={approval} label={t(`mcp.client.approval.${approval}`)} />
|
|
361
|
+
{/each}
|
|
362
|
+
</Select.Group>
|
|
363
|
+
</Select.Content>
|
|
364
|
+
</Select.Root>
|
|
365
|
+
</li>
|
|
366
|
+
{/each}
|
|
367
|
+
</ul>
|
|
368
|
+
{:else}
|
|
369
|
+
<p class="text-muted-foreground text-sm">{t('mcp.client.noTools')}</p>
|
|
370
|
+
{/if}
|
|
371
|
+
</li>
|
|
372
|
+
{/each}
|
|
373
|
+
</ul>
|
|
374
|
+
</section>
|
|
375
|
+
|
|
376
|
+
<Dialog.Root
|
|
377
|
+
open={editing}
|
|
378
|
+
onOpenChange={(open) => {
|
|
379
|
+
if (!open) closeEditor()
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
<Dialog.Content class="sm:max-w-xl" oninput={() => editRevision++}>
|
|
383
|
+
<Dialog.Header>
|
|
384
|
+
<Dialog.Title>{editId ? t('mcp.client.editTitle') : t('mcp.client.add')}</Dialog.Title>
|
|
385
|
+
</Dialog.Header>
|
|
386
|
+
<Field.Group>
|
|
387
|
+
<div class="flex gap-3">
|
|
388
|
+
<Field.Field class="flex-1">
|
|
389
|
+
<Field.Label for="kit-mcp-name">{t('mcp.client.name')}</Field.Label>
|
|
390
|
+
<Input id="kit-mcp-name" maxlength={200} bind:value={name} />
|
|
391
|
+
</Field.Field>
|
|
392
|
+
<Field.Field class="w-40">
|
|
393
|
+
<Field.Label for="kit-mcp-slug">{t('mcp.client.slug')}</Field.Label>
|
|
394
|
+
<Input id="kit-mcp-slug" class="font-mono" maxlength={24} bind:value={slug} />
|
|
395
|
+
</Field.Field>
|
|
396
|
+
</div>
|
|
397
|
+
<Field.Field>
|
|
398
|
+
<Field.Label for="kit-mcp-transport">{t('mcp.client.transport')}</Field.Label>
|
|
399
|
+
<Select.Root type="single" value={kind} onValueChange={(value) => changeKind(value as McpTransport['kind'])}>
|
|
400
|
+
<Select.Trigger id="kit-mcp-transport" class="w-full">
|
|
401
|
+
{t(`mcp.client.transport.${kind}`)}
|
|
402
|
+
</Select.Trigger>
|
|
403
|
+
<Select.Content>
|
|
404
|
+
<Select.Group>
|
|
405
|
+
{#each KINDS as option (option)}
|
|
406
|
+
<Select.Item value={option} label={t(`mcp.client.transport.${option}`)} />
|
|
407
|
+
{/each}
|
|
408
|
+
</Select.Group>
|
|
409
|
+
</Select.Content>
|
|
410
|
+
</Select.Root>
|
|
411
|
+
</Field.Field>
|
|
412
|
+
{#if kind === 'http'}
|
|
413
|
+
<Field.Field>
|
|
414
|
+
<Field.Label for="kit-mcp-url">{t('mcp.client.url')}</Field.Label>
|
|
415
|
+
<CopyInput id="kit-mcp-url" class="font-mono" placeholder="https://example.com/mcp" bind:value={url} />
|
|
416
|
+
<Field.Description>{t('mcp.client.urlHint')}</Field.Description>
|
|
417
|
+
</Field.Field>
|
|
418
|
+
{:else}
|
|
419
|
+
<Field.Field>
|
|
420
|
+
<Field.Label for="kit-mcp-command">{t('mcp.client.command')}</Field.Label>
|
|
421
|
+
<Input id="kit-mcp-command" class="font-mono" maxlength={1024} placeholder="npx" bind:value={command} />
|
|
422
|
+
</Field.Field>
|
|
423
|
+
<Field.Field>
|
|
424
|
+
<Field.Label for="kit-mcp-args">{t('mcp.client.args')}</Field.Label>
|
|
425
|
+
<Textarea id="kit-mcp-args" class="font-mono text-xs" placeholder={ARGS_PLACEHOLDER} bind:value={args} />
|
|
426
|
+
<Field.Description>{t('mcp.client.argsHint')}</Field.Description>
|
|
427
|
+
</Field.Field>
|
|
428
|
+
<Field.Field>
|
|
429
|
+
<Field.Label for="kit-mcp-cwd">{t('mcp.client.cwd')}</Field.Label>
|
|
430
|
+
<Input id="kit-mcp-cwd" class="font-mono" bind:value={cwd} />
|
|
431
|
+
<Field.Description>{t('mcp.client.cwdHint')}</Field.Description>
|
|
432
|
+
</Field.Field>
|
|
433
|
+
{/if}
|
|
434
|
+
{#if editId}
|
|
435
|
+
<Field.Field orientation="horizontal">
|
|
436
|
+
<Switch id="kit-mcp-replace" checked={replaceSecret} onCheckedChange={changeReplacement} />
|
|
437
|
+
<Field.Label for="kit-mcp-replace">
|
|
438
|
+
{t('mcp.client.replaceSecret', { keys: currentKeys.join(', ') || '—' })}
|
|
439
|
+
</Field.Label>
|
|
440
|
+
</Field.Field>
|
|
441
|
+
{/if}
|
|
442
|
+
{#if replaceSecret && kind === 'http'}
|
|
443
|
+
<Field.Field>
|
|
444
|
+
<Field.Label for="kit-mcp-headers">{t('mcp.client.headers')}</Field.Label>
|
|
445
|
+
<Textarea
|
|
446
|
+
id="kit-mcp-headers"
|
|
447
|
+
class="font-mono text-xs"
|
|
448
|
+
placeholder="Authorization: Bearer …"
|
|
449
|
+
bind:value={headers}
|
|
450
|
+
oninput={() => secretRevision++}
|
|
451
|
+
/>
|
|
452
|
+
</Field.Field>
|
|
453
|
+
<Field.Field>
|
|
454
|
+
<Field.Label for="kit-mcp-query">{t('mcp.client.query')}</Field.Label>
|
|
455
|
+
<Textarea
|
|
456
|
+
id="kit-mcp-query"
|
|
457
|
+
class="font-mono text-xs"
|
|
458
|
+
placeholder="ak=…"
|
|
459
|
+
bind:value={query}
|
|
460
|
+
oninput={() => secretRevision++}
|
|
461
|
+
/>
|
|
462
|
+
<Field.Description>{t('mcp.client.secretHint')}</Field.Description>
|
|
463
|
+
</Field.Field>
|
|
464
|
+
{:else if replaceSecret}
|
|
465
|
+
<Field.Field>
|
|
466
|
+
<Field.Label for="kit-mcp-env">{t('mcp.client.env')}</Field.Label>
|
|
467
|
+
<Textarea
|
|
468
|
+
id="kit-mcp-env"
|
|
469
|
+
class="font-mono text-xs"
|
|
470
|
+
placeholder="GITHUB_TOKEN=…"
|
|
471
|
+
bind:value={env}
|
|
472
|
+
oninput={() => secretRevision++}
|
|
473
|
+
/>
|
|
474
|
+
<Field.Description>{t('mcp.client.envHint')}</Field.Description>
|
|
475
|
+
</Field.Field>
|
|
476
|
+
{/if}
|
|
477
|
+
</Field.Group>
|
|
478
|
+
<Dialog.Footer>
|
|
479
|
+
<Button variant="outline" onclick={closeEditor}>{t('common.cancel')}</Button>
|
|
480
|
+
<Button
|
|
481
|
+
disabled={busy ||
|
|
482
|
+
editorSaving ||
|
|
483
|
+
!name.trim() ||
|
|
484
|
+
!slug.trim() ||
|
|
485
|
+
!(kind === 'http' ? url.trim() : command.trim())}
|
|
486
|
+
onclick={save}>{t('mcp.client.save')}</Button
|
|
487
|
+
>
|
|
488
|
+
</Dialog.Footer>
|
|
489
|
+
</Dialog.Content>
|
|
490
|
+
</Dialog.Root>
|
|
491
|
+
{/if}
|