@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.
Files changed (179) hide show
  1. package/LICENSE +789 -0
  2. package/fixtures/model-classify.json +42 -0
  3. package/fixtures/pricing-cost.json +56 -0
  4. package/fixtures/reasoning-infer.json +44 -0
  5. package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
  6. package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
  7. package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
  8. package/fixtures/sse/anthropic-messages/text.sse +27 -0
  9. package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
  10. package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
  11. package/fixtures/sse/expect.json +20 -0
  12. package/fixtures/sse/openai-chat/chinese.sse +14 -0
  13. package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
  14. package/fixtures/sse/openai-chat/single-tool.sse +22 -0
  15. package/fixtures/sse/openai-chat/text.sse +14 -0
  16. package/fixtures/sse/openai-chat/truncated.sse +12 -0
  17. package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
  18. package/fixtures/sse/openai-responses/chinese.sse +33 -0
  19. package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
  20. package/fixtures/sse/openai-responses/single-tool.sse +27 -0
  21. package/fixtures/sse/openai-responses/text.sse +33 -0
  22. package/fixtures/sse/openai-responses/truncated.sse +30 -0
  23. package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
  24. package/package.json +71 -3
  25. package/src/CopyInput.svelte +33 -0
  26. package/src/KitNotifier.svelte +33 -0
  27. package/src/chat/ApprovalCard.svelte +23 -0
  28. package/src/chat/ChatPanel.svelte +2097 -0
  29. package/src/chat/MessageItem.svelte +583 -0
  30. package/src/chat/SkillPicker.svelte +126 -0
  31. package/src/chat/ToolCallItem.svelte +23 -0
  32. package/src/chat/UsageDetails.svelte +138 -0
  33. package/src/chat/index.ts +2 -0
  34. package/src/chat/run-session.svelte.ts +556 -0
  35. package/src/components/ui/alert/alert-action.svelte +20 -0
  36. package/src/components/ui/alert/alert-description.svelte +23 -0
  37. package/src/components/ui/alert/alert-title.svelte +23 -0
  38. package/src/components/ui/alert/alert.svelte +43 -0
  39. package/src/components/ui/alert/index.ts +17 -0
  40. package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
  41. package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
  42. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
  43. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
  44. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
  45. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
  46. package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
  47. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
  48. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
  49. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
  50. package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
  51. package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
  52. package/src/components/ui/alert-dialog/index.ts +40 -0
  53. package/src/components/ui/button/button.svelte +81 -0
  54. package/src/components/ui/button/index.ts +17 -0
  55. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  56. package/src/components/ui/dialog/dialog-content.svelte +48 -0
  57. package/src/components/ui/dialog/dialog-description.svelte +17 -0
  58. package/src/components/ui/dialog/dialog-footer.svelte +32 -0
  59. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  60. package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
  61. package/src/components/ui/dialog/dialog-portal.svelte +7 -0
  62. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  63. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  64. package/src/components/ui/dialog/dialog.svelte +7 -0
  65. package/src/components/ui/dialog/index.ts +34 -0
  66. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  67. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
  68. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
  69. package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  70. package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  71. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  72. package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  73. package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  74. package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
  75. package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
  76. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  77. package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  78. package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
  79. package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  80. package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  81. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  82. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  83. package/src/components/ui/dropdown-menu/index.ts +54 -0
  84. package/src/components/ui/empty/empty-content.svelte +23 -0
  85. package/src/components/ui/empty/empty-description.svelte +23 -0
  86. package/src/components/ui/empty/empty-header.svelte +20 -0
  87. package/src/components/ui/empty/empty-media.svelte +41 -0
  88. package/src/components/ui/empty/empty-title.svelte +20 -0
  89. package/src/components/ui/empty/empty.svelte +23 -0
  90. package/src/components/ui/empty/index.ts +22 -0
  91. package/src/components/ui/field/field-content.svelte +20 -0
  92. package/src/components/ui/field/field-description.svelte +25 -0
  93. package/src/components/ui/field/field-error.svelte +58 -0
  94. package/src/components/ui/field/field-group.svelte +23 -0
  95. package/src/components/ui/field/field-label.svelte +25 -0
  96. package/src/components/ui/field/field-legend.svelte +24 -0
  97. package/src/components/ui/field/field-separator.svelte +35 -0
  98. package/src/components/ui/field/field-set.svelte +20 -0
  99. package/src/components/ui/field/field-title.svelte +20 -0
  100. package/src/components/ui/field/field.svelte +47 -0
  101. package/src/components/ui/field/index.ts +33 -0
  102. package/src/components/ui/input/index.ts +7 -0
  103. package/src/components/ui/input/input.svelte +48 -0
  104. package/src/components/ui/label/index.ts +7 -0
  105. package/src/components/ui/label/label.svelte +20 -0
  106. package/src/components/ui/select/index.ts +40 -0
  107. package/src/components/ui/select/select-content.svelte +45 -0
  108. package/src/components/ui/select/select-group-heading.svelte +21 -0
  109. package/src/components/ui/select/select-group.svelte +17 -0
  110. package/src/components/ui/select/select-item.svelte +43 -0
  111. package/src/components/ui/select/select-label.svelte +20 -0
  112. package/src/components/ui/select/select-portal.svelte +7 -0
  113. package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
  114. package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
  115. package/src/components/ui/select/select-separator.svelte +18 -0
  116. package/src/components/ui/select/select-trigger.svelte +29 -0
  117. package/src/components/ui/select/select-value.svelte +7 -0
  118. package/src/components/ui/select/select.svelte +11 -0
  119. package/src/components/ui/separator/index.ts +7 -0
  120. package/src/components/ui/separator/separator.svelte +21 -0
  121. package/src/components/ui/sonner/index.ts +1 -0
  122. package/src/components/ui/sonner/sonner.svelte +40 -0
  123. package/src/components/ui/switch/index.ts +7 -0
  124. package/src/components/ui/switch/switch.svelte +34 -0
  125. package/src/components/ui/textarea/index.ts +7 -0
  126. package/src/components/ui/textarea/textarea.svelte +22 -0
  127. package/src/context.ts +34 -0
  128. package/src/dev/Demo.svelte +45 -0
  129. package/src/dev/main.ts +7 -0
  130. package/src/i18n/en.ts +617 -0
  131. package/src/i18n/index.ts +23 -0
  132. package/src/i18n/zh-CN.ts +599 -0
  133. package/src/index.ts +10 -0
  134. package/src/logs/LogPanel.svelte +97 -0
  135. package/src/logs/LogSettings.svelte +56 -0
  136. package/src/logs/index.ts +5 -0
  137. package/src/logs/logger.ts +96 -0
  138. package/src/markdown.ts +8 -0
  139. package/src/mcp/McpClientSettings.svelte +491 -0
  140. package/src/mcp/McpServerSettings.svelte +327 -0
  141. package/src/mcp/config.ts +26 -0
  142. package/src/mcp/index.ts +3 -0
  143. package/src/mcp/input.ts +73 -0
  144. package/src/notify.svelte.ts +43 -0
  145. package/src/ports/contract.ts +165 -0
  146. package/src/ports/fake.ts +484 -0
  147. package/src/ports/index.ts +7 -0
  148. package/src/ports/search.ts +47 -0
  149. package/src/ports/skill-sources.ts +45 -0
  150. package/src/providers/EndpointKeysDialog.svelte +343 -0
  151. package/src/providers/EndpointPanel.svelte +491 -0
  152. package/src/providers/ModelPickerDialog.svelte +187 -0
  153. package/src/providers/ModelSelect.svelte +434 -0
  154. package/src/providers/ModelSettingsDialog.svelte +453 -0
  155. package/src/providers/ProvidersSettings.svelte +631 -0
  156. package/src/providers/RequestConfigDialog.svelte +250 -0
  157. package/src/providers/SecretInput.svelte +276 -0
  158. package/src/providers/draft.ts +329 -0
  159. package/src/providers/edit-session.ts +146 -0
  160. package/src/providers/index.ts +5 -0
  161. package/src/providers/reasoning.ts +41 -0
  162. package/src/providers/revision.svelte.ts +4 -0
  163. package/src/search/SearchSettings.svelte +703 -0
  164. package/src/search/index.ts +13 -0
  165. package/src/search/settings-flow.ts +62 -0
  166. package/src/session.ts +20 -0
  167. package/src/skills/SkillAdd.svelte +347 -0
  168. package/src/skills/SkillAuthoring.svelte +381 -0
  169. package/src/skills/SkillEditor.svelte +94 -0
  170. package/src/skills/SkillPreviewDialog.svelte +122 -0
  171. package/src/skills/SkillsSettings.svelte +140 -0
  172. package/src/skills/error.ts +32 -0
  173. package/src/skills/index.ts +4 -0
  174. package/src/styles/kit.css +137 -0
  175. package/src/styles/tokens.css +22 -0
  176. package/src/tauri/index.ts +282 -0
  177. package/src/utils.ts +22 -0
  178. package/src/web/index.ts +376 -0
  179. package/README.md +0 -3
@@ -0,0 +1,13 @@
1
+ export { default as SearchSettings } from './SearchSettings.svelte'
2
+ export type {
3
+ SearchCap,
4
+ SearchKey,
5
+ SearchPreferences,
6
+ SearchHit,
7
+ SearchPage,
8
+ SearchPort,
9
+ SearchProvider,
10
+ SearchResult,
11
+ SearchSave,
12
+ SearchSettingsDto,
13
+ } from '../ports/search'
@@ -0,0 +1,62 @@
1
+ import { KitClientError } from '../ports/contract'
2
+ import type { CallOptions } from '../ports/contract'
3
+ import type { SearchPort, SearchSave, SearchSettingsDto } from '../ports/search'
4
+
5
+ /** Applies each persisted step before attempting the next (docs/30 §4). */
6
+ export async function saveSearchSettings(
7
+ port: SearchPort,
8
+ input: { config: SearchSave; key?: { value: string; keyId?: string }; check: boolean },
9
+ session: {
10
+ isCurrent(): boolean
11
+ onSettings(next: SearchSettingsDto): void
12
+ onConfigSaved(sent: SearchSave): void
13
+ onKeySaved(): void
14
+ },
15
+ options?: CallOptions,
16
+ ): Promise<{ settings: SearchSettingsDto; checked?: { count: number } }> {
17
+ const config = { ...input.config }
18
+ const key = input.key ? { ...input.key } : undefined
19
+ const check = input.check
20
+ const cancelled = () => new KitClientError({ code: 'cancelled', message: 'Search settings cancelled', details: {} })
21
+ function assertCurrent() {
22
+ if (options?.signal?.aborted || !session.isCurrent()) throw cancelled()
23
+ }
24
+ // Consume an in-flight response even if its transport does not honor abort.
25
+ async function step<T>(call: () => Promise<T>): Promise<T> {
26
+ assertCurrent()
27
+ const signal = options?.signal
28
+ let onAbort: (() => void) | undefined
29
+ try {
30
+ const operation = call()
31
+ let value: T
32
+ if (signal) {
33
+ const abort = Promise.withResolvers<never>()
34
+ onAbort = () => abort.reject(cancelled())
35
+ signal.addEventListener('abort', onAbort, { once: true })
36
+ if (signal.aborted) onAbort()
37
+ value = await Promise.race([operation, abort.promise])
38
+ } else value = await operation
39
+ assertCurrent()
40
+ return value
41
+ } catch (error) {
42
+ assertCurrent()
43
+ throw error
44
+ } finally {
45
+ if (signal && onAbort) signal.removeEventListener('abort', onAbort)
46
+ }
47
+ }
48
+
49
+ let settings = await step(() => port.save(config, options))
50
+ session.onSettings(settings)
51
+ assertCurrent()
52
+ session.onConfigSaved(config)
53
+ if (key?.value.trim()) {
54
+ settings = await step(() => port.saveKey(config.capability, config.provider, key.value, key.keyId, options))
55
+ session.onSettings(settings)
56
+ assertCurrent()
57
+ session.onKeySaved()
58
+ }
59
+ const checked = check ? await step(() => port.check(config.capability, config.provider, options)) : undefined
60
+ assertCurrent()
61
+ return checked ? { settings, checked } : { settings }
62
+ }
package/src/session.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** A token belongs to exactly one session instance and one target generation. */
2
+ export interface SessionToken {
3
+ readonly generation: number
4
+ }
5
+ export interface ClientSession {
6
+ capture(): SessionToken
7
+ isCurrent(token: SessionToken): boolean
8
+ invalidate(): void
9
+ }
10
+ export function createClientSession(): ClientSession {
11
+ let generation = 0
12
+ let current: SessionToken = { generation }
13
+ return {
14
+ capture: () => current,
15
+ isCurrent: (token) => token === current,
16
+ invalidate: () => {
17
+ current = { generation: ++generation }
18
+ },
19
+ }
20
+ }
@@ -0,0 +1,347 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte'
3
+ import { Bot, ChevronDown, FolderSearch, Import, Plus, Search } from '@lucide/svelte'
4
+ import { Button } from '#lib/components/ui/button/index.js'
5
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
6
+ import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'
7
+ import * as Field from '#lib/components/ui/field/index.js'
8
+ import { Input } from '#lib/components/ui/input/index.js'
9
+ import { getKitContext } from '../context'
10
+ import {
11
+ isKitClientError,
12
+ type Preview,
13
+ type PreparedSkill,
14
+ type SkillMeta,
15
+ type Source,
16
+ type SkillCandidate,
17
+ type SystemSkillCandidate,
18
+ } from '../ports'
19
+ import { errorText } from './error'
20
+ import SkillPreviewDialog from './SkillPreviewDialog.svelte'
21
+ import SkillAuthoring from './SkillAuthoring.svelte'
22
+
23
+ let {
24
+ oninstalled,
25
+ onmanual,
26
+ disabled = false,
27
+ }: {
28
+ oninstalled: (skills: SkillMeta[]) => void
29
+ onmanual: () => void
30
+ disabled?: boolean
31
+ } = $props()
32
+ const { ports, i18n, notify } = getKitContext()
33
+ const { t } = i18n
34
+ const port = ports.skills
35
+ const sources = ports.skillSources
36
+ type Mode = 'agent' | 'online' | 'system' | 'local'
37
+ let mode = $state<Mode | null>(null)
38
+ let busy = $state(false)
39
+ let error = $state<string | null>(null)
40
+ let query = $state('')
41
+ let candidates = $state<SkillCandidate[]>([])
42
+ let system = $state<SystemSkillCandidate[]>([])
43
+ let searched = $state(false)
44
+ let file = $state<HTMLInputElement | null>(null)
45
+ let source: Source | null = null
46
+ let preview = $state<Preview | null>(null)
47
+ let origin = $state('')
48
+ let ignored = $state<string[]>([])
49
+ let operation: AbortController | null = null
50
+ let generation = 0
51
+ let alive = true
52
+
53
+ function invalidate(): void {
54
+ generation += 1
55
+ operation?.abort()
56
+ operation = null
57
+ busy = false
58
+ }
59
+ function close(): void {
60
+ invalidate()
61
+ mode = null
62
+ preview = null
63
+ source = null
64
+ error = null
65
+ }
66
+ onDestroy(() => {
67
+ alive = false
68
+ invalidate()
69
+ })
70
+
71
+ async function act(work: (signal: AbortSignal, current: () => boolean) => Promise<void>): Promise<void> {
72
+ invalidate()
73
+ const id = generation
74
+ const controller = new AbortController()
75
+ operation = controller
76
+ const current = (): boolean => alive && generation === id && !controller.signal.aborted
77
+ busy = true
78
+ error = null
79
+ try {
80
+ await work(controller.signal, current)
81
+ } catch (cause) {
82
+ if (current()) error = errorText(t, cause)
83
+ } finally {
84
+ if (current()) {
85
+ busy = false
86
+ operation = null
87
+ }
88
+ }
89
+ }
90
+ function open(next: Mode): void {
91
+ close()
92
+ query = ''
93
+ candidates = []
94
+ system = []
95
+ searched = false
96
+ mode = next
97
+ if (next === 'system') void scan(false)
98
+ }
99
+ async function showPreview(
100
+ next: Source,
101
+ from: string,
102
+ current: () => boolean,
103
+ excluded: string[] = [],
104
+ ): Promise<void> {
105
+ if (!port || !current()) return
106
+ const result = await port.preview(next)
107
+ if (!current()) return
108
+ source = next
109
+ origin = from
110
+ ignored = excluded
111
+ preview = result
112
+ }
113
+ function search(): Promise<void> {
114
+ return act(async (signal, current) => {
115
+ if (!sources) return
116
+ const result = await sources.search(query.trim(), { signal })
117
+ if (current()) {
118
+ candidates = result
119
+ searched = true
120
+ }
121
+ })
122
+ }
123
+ function prepare(candidate: SkillCandidate): Promise<void> {
124
+ return act(async (signal, current) => {
125
+ if (!sources) return
126
+ const prepared = await sources.prepare(candidate.url, { signal })
127
+ await showPreview(prepared.source, prepared.origin, current, prepared.ignored)
128
+ })
129
+ }
130
+ function scan(pick: boolean): Promise<void> {
131
+ return act(async (signal, current) => {
132
+ if (!sources) return
133
+ const found = await (pick ? sources.pickSearchDirectory({ signal }) : sources.discover({ signal }))
134
+ if (current() && found !== null) {
135
+ system = found
136
+ searched = true
137
+ }
138
+ })
139
+ }
140
+ function prepareSystem(candidate: SystemSkillCandidate): Promise<void> {
141
+ return act(async (signal, current) => {
142
+ if (!sources) return
143
+ const prepared = await sources.prepareSystem(candidate.path, { signal })
144
+ await showPreview(prepared.source, prepared.origin, current, prepared.ignored)
145
+ })
146
+ }
147
+ function pickDirectory(): Promise<void> {
148
+ return act(async (signal, current) => {
149
+ const prepared = await sources?.pickDirectory({ signal })
150
+ if (prepared) await showPreview(prepared.source, prepared.origin, current, prepared.ignored)
151
+ })
152
+ }
153
+ function pickFile(input: HTMLInputElement): void {
154
+ const picked = input.files?.[0]
155
+ input.value = ''
156
+ if (!picked) return
157
+ void act(async (_signal, current) => {
158
+ const next: Source = { bytes: { fileName: picked.name, bytes: [...new Uint8Array(await picked.arrayBuffer())] } }
159
+ await showPreview(next, picked.name, current)
160
+ })
161
+ }
162
+ function closePreview(): void {
163
+ invalidate()
164
+ preview = null
165
+ source = null
166
+ }
167
+ function install(replace: boolean): void {
168
+ const confirmed = preview
169
+ const from = source
170
+ if (!port || !confirmed || !from || busy) return
171
+ void act(async (_signal, current) => {
172
+ try {
173
+ const installed = await port.install(from, confirmed.digest, replace)
174
+ if (!current()) return
175
+ oninstalled(installed)
176
+ close()
177
+ notify.toast('success', t('skills.installed', { name: confirmed.name }))
178
+ } catch (cause) {
179
+ if (isKitClientError(cause) && cause.details.reason === 'content_changed') {
180
+ const refreshed = await port.preview(from)
181
+ if (current()) {
182
+ preview = refreshed
183
+ notify.toast('info', t('skills.content_changed'))
184
+ }
185
+ return
186
+ }
187
+ throw cause
188
+ }
189
+ })
190
+ }
191
+ </script>
192
+
193
+ <div class="flex gap-2">
194
+ <DropdownMenu.Root>
195
+ <DropdownMenu.Trigger>
196
+ {#snippet child({ props })}
197
+ <Button {...props} {disabled}
198
+ ><Plus data-icon="inline-start" />{t('skills.add.title')}<ChevronDown data-icon="inline-end" /></Button
199
+ >
200
+ {/snippet}
201
+ </DropdownMenu.Trigger>
202
+ <DropdownMenu.Content align="start">
203
+ <DropdownMenu.Group>
204
+ {#if sources}
205
+ {#if sources.authoring && ports.providers}
206
+ <DropdownMenu.Item onSelect={() => open('agent')}><Bot />{t('skills.add.agent')}</DropdownMenu.Item>
207
+ {/if}
208
+ <DropdownMenu.Item onSelect={() => open('online')}><Search />{t('skills.add.online')}</DropdownMenu.Item>
209
+ <DropdownMenu.Item onSelect={() => open('system')}><FolderSearch />{t('skills.add.system')}</DropdownMenu.Item
210
+ >
211
+ {/if}
212
+ <DropdownMenu.Item onSelect={() => open('local')}><Import />{t('skills.add.local')}</DropdownMenu.Item>
213
+ </DropdownMenu.Group>
214
+ </DropdownMenu.Content>
215
+ </DropdownMenu.Root>
216
+ <Button variant="outline" {disabled} onclick={onmanual}>{t('skills.new')}</Button>
217
+ </div>
218
+ <input
219
+ bind:this={file}
220
+ type="file"
221
+ accept=".md,.zip"
222
+ class="hidden"
223
+ aria-label={t('skills.import')}
224
+ onchange={(e) => pickFile(e.currentTarget)}
225
+ />
226
+
227
+ <Dialog.Root
228
+ open={mode !== null && mode !== 'agent' && preview === null}
229
+ onOpenChange={(value) => {
230
+ if (!value) close()
231
+ }}
232
+ >
233
+ <Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
234
+ <Dialog.Header>
235
+ <Dialog.Title>{mode ? t(`skills.add.${mode}`) : t('skills.add.title')}</Dialog.Title>
236
+ <Dialog.Description>{t('skills.add.review_notice')}</Dialog.Description>
237
+ </Dialog.Header>
238
+ <div class="flex min-h-0 flex-col gap-4 overflow-y-auto">
239
+ {#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
240
+ {#if mode === 'online'}
241
+ <form
242
+ onsubmit={(event) => {
243
+ event.preventDefault()
244
+ if (!busy && query.trim()) void search()
245
+ }}
246
+ >
247
+ <Field.Group>
248
+ <Field.Field>
249
+ <Field.Label for="skill-source-query">{t('skills.add.query')}</Field.Label>
250
+ <Input
251
+ id="skill-source-query"
252
+ bind:value={query}
253
+ placeholder={t('skills.add.query_hint')}
254
+ disabled={busy}
255
+ />
256
+ </Field.Field>
257
+ <Button type="submit" disabled={busy || !query.trim()}
258
+ ><Search data-icon="inline-start" />{t('skills.add.search')}</Button
259
+ >
260
+ </Field.Group>
261
+ </form>
262
+ <ul class="flex flex-col gap-2" aria-label={t('skills.add.results')}>
263
+ {#each candidates as candidate (candidate.url)}
264
+ <li class="flex items-start gap-3 rounded-md border p-3">
265
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
266
+ <strong class="text-sm">{candidate.name}</strong>
267
+ <p class="text-muted-foreground text-xs">{candidate.description}</p>
268
+ <p class="text-muted-foreground break-all text-xs">{candidate.origin} · {candidate.url}</p>
269
+ </div>
270
+ <Button size="sm" variant="outline" disabled={busy} onclick={() => void prepare(candidate)}
271
+ >{t('skills.add.preview')}</Button
272
+ >
273
+ </li>
274
+ {/each}
275
+ </ul>
276
+ {:else if mode === 'system'}
277
+ <p class="text-muted-foreground text-sm">{t('skills.add.system_hint')}</p>
278
+ <div class="flex flex-wrap gap-2">
279
+ <Button variant="outline" disabled={busy} onclick={() => void scan(false)}>{t('skills.add.rescan')}</Button>
280
+ <Button variant="outline" disabled={busy} onclick={() => void scan(true)}
281
+ ><FolderSearch data-icon="inline-start" />{t('skills.add.pick_search_directory')}</Button
282
+ >
283
+ </div>
284
+ <ul class="flex flex-col gap-2" aria-label={t('skills.add.results')}>
285
+ {#each system as candidate (candidate.path)}
286
+ <li class="flex items-start gap-3 rounded-md border p-3">
287
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
288
+ <strong class="text-sm">{candidate.name}</strong>
289
+ <p class="text-muted-foreground break-all text-xs">{candidate.origin} · {candidate.path}</p>
290
+ </div>
291
+ <Button size="sm" variant="outline" disabled={busy} onclick={() => void prepareSystem(candidate)}
292
+ >{t('skills.add.preview')}</Button
293
+ >
294
+ </li>
295
+ {/each}
296
+ </ul>
297
+ {:else if mode === 'local'}
298
+ <p class="text-muted-foreground text-sm">{t('skills.add.local_hint')}</p>
299
+ <div class="flex flex-wrap gap-2">
300
+ <Button disabled={busy} onclick={() => file?.click()}
301
+ ><Import data-icon="inline-start" />{t('skills.import')}</Button
302
+ >
303
+ <Button variant="outline" disabled={busy || !sources} onclick={() => void pickDirectory()}
304
+ ><FolderSearch data-icon="inline-start" />{t('skills.add.pick_directory')}</Button
305
+ >
306
+ </div>
307
+ {/if}
308
+ {#if busy}<p role="status" class="text-muted-foreground text-sm">{t('skills.add.working')}</p>{/if}
309
+ {#if !busy && searched && ((mode === 'online' && candidates.length === 0) || (mode === 'system' && system.length === 0))}
310
+ <p role="status" class="text-muted-foreground text-sm">{t('skills.add.no_results')}</p>
311
+ {/if}
312
+ </div>
313
+ <Dialog.Footer><Button variant="outline" onclick={close}>{t('common.cancel')}</Button></Dialog.Footer>
314
+ </Dialog.Content>
315
+ </Dialog.Root>
316
+
317
+ <SkillAuthoring
318
+ active={mode === 'agent'}
319
+ open={mode === 'agent' && preview === null}
320
+ onclose={close}
321
+ onprepared={async (prepared: PreparedSkill) => {
322
+ const id = generation
323
+ await showPreview(
324
+ prepared.source,
325
+ prepared.origin,
326
+ () => alive && generation === id && mode === 'agent',
327
+ prepared.ignored,
328
+ )
329
+ }}
330
+ />
331
+
332
+ <SkillPreviewDialog
333
+ bind:open={
334
+ () => preview !== null,
335
+ (value) => {
336
+ if (!value) closePreview()
337
+ }
338
+ }
339
+ skill={preview}
340
+ title={t('skills.preview_title')}
341
+ current={preview?.current ?? null}
342
+ {busy}
343
+ {error}
344
+ {origin}
345
+ {ignored}
346
+ onconfirm={install}
347
+ />