@ouraihub/aether-ui 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/chat/ApprovalCard.svelte +8 -2
- package/src/chat/ChatComposer.svelte +319 -0
- package/src/chat/ChatMessage.svelte +262 -0
- package/src/chat/ChatPanel.css +386 -0
- package/src/chat/ChatPanel.svelte +276 -1880
- package/src/chat/ChatPanelApprovals.svelte +28 -0
- package/src/chat/ChatPanelComposer.svelte +336 -0
- package/src/chat/ChatPanelDialogs.svelte +242 -0
- package/src/chat/ChatPanelHeader.svelte +108 -0
- package/src/chat/ChatPanelMessages.svelte +166 -0
- package/src/chat/ChatPanelMode.svelte +190 -0
- package/src/chat/ChatPanelState.svelte.ts +1 -0
- package/src/chat/ChatPanelToolbar.svelte +436 -0
- package/src/chat/ChatSurface.svelte +168 -0
- package/src/chat/ContextUsageStatus.svelte +97 -0
- package/src/chat/MessageItem.svelte +111 -214
- package/src/chat/ModelEffortPicker.svelte +326 -0
- package/src/chat/PickerEffort.svelte +71 -0
- package/src/chat/ReasoningControl.svelte +184 -0
- package/src/chat/ReasoningSlider.svelte +174 -0
- package/src/chat/SkillPicker.svelte +14 -6
- package/src/chat/UsageControl.svelte +223 -0
- package/src/chat/UsageDetails.svelte +44 -90
- package/src/chat/chat-attachments.svelte.ts +78 -0
- package/src/chat/chat-panel-history.svelte.ts +430 -0
- package/src/chat/chat-panel-models.svelte.ts +97 -0
- package/src/chat/chat-panel-props.ts +38 -0
- package/src/chat/context-status.ts +101 -0
- package/src/chat/icons/LICENSE-REMIX +186 -0
- package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
- package/src/chat/icons/PathIcon.svelte +8 -0
- package/src/chat/icons/effort-icons.ts +65 -0
- package/src/chat/icons/status-icons.ts +105 -0
- package/src/chat/index.ts +25 -1
- package/src/chat/model-effort.ts +177 -0
- package/src/chat/run-session-history.ts +56 -0
- package/src/chat/run-session-types.ts +69 -0
- package/src/chat/run-session.svelte.ts +73 -135
- package/src/chat/test-support/chat-fixture.ts +194 -0
- package/src/chat/test-support/conversation-fixtures.ts +32 -0
- package/src/chat/test-support/run-session-fixture.ts +112 -0
- package/src/dev/Demo.svelte +3 -1
- package/src/i18n/en-chat.d.ts +1 -0
- package/src/i18n/en-chat.js +201 -0
- package/src/i18n/en.ts +58 -180
- package/src/i18n/index.ts +7 -2
- package/src/i18n/zh-CN-chat.d.ts +1 -0
- package/src/i18n/zh-CN-chat.js +190 -0
- package/src/i18n/zh-CN.ts +8 -153
- package/src/logs/LogPanel.svelte +9 -3
- package/src/logs/LogSettings.svelte +3 -1
- package/src/mcp/McpClientEditor.svelte +204 -0
- package/src/mcp/McpClientSettings.svelte +57 -130
- package/src/mcp/McpServerSettings.svelte +47 -11
- package/src/mcp/input.ts +2 -1
- package/src/ports/contract.ts +11 -2
- package/src/ports/fake-optional.ts +81 -0
- package/src/ports/fake.ts +90 -86
- package/src/ports/search.ts +11 -2
- package/src/ports/test-support/fake-fixture.ts +63 -0
- package/src/providers/EndpointKeysDialog.svelte +27 -13
- package/src/providers/EndpointModels.svelte +311 -0
- package/src/providers/EndpointPanel.svelte +33 -258
- package/src/providers/ModelPickerDialog.svelte +59 -16
- package/src/providers/ModelPricing.svelte +125 -0
- package/src/providers/ModelSelect.svelte +22 -8
- package/src/providers/ModelSettingsDialog.svelte +61 -126
- package/src/providers/ProviderDialogs.svelte +120 -0
- package/src/providers/ProviderList.svelte +159 -0
- package/src/providers/ProvidersSettings.svelte +67 -207
- package/src/providers/RequestConfigDialog.svelte +40 -12
- package/src/providers/SecretInput.svelte +11 -3
- package/src/providers/draft.ts +39 -13
- package/src/providers/edit-session.ts +31 -7
- package/src/providers/reasoning.ts +18 -4
- package/src/providers/revision.svelte.ts +3 -1
- package/src/providers/test-support/providers-fixture.ts +120 -0
- package/src/search/SearchAdvancedTools.svelte +182 -0
- package/src/search/SearchProviderCard.svelte +195 -0
- package/src/search/SearchSettings.svelte +62 -265
- package/src/search/settings-flow.ts +8 -3
- package/src/skills/SkillAdd.svelte +51 -18
- package/src/skills/SkillAuthoring.svelte +69 -20
- package/src/skills/SkillEditor.svelte +13 -3
- package/src/skills/SkillPreviewDialog.svelte +19 -5
- package/src/skills/SkillsSettings.svelte +11 -4
- package/src/skills/test-support/skills-fixture.ts +139 -0
- package/src/tauri/index.ts +23 -7
- package/src/web/index.ts +90 -25
|
@@ -68,7 +68,9 @@
|
|
|
68
68
|
invalidate()
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
async function act(
|
|
71
|
+
async function act(
|
|
72
|
+
work: (signal: AbortSignal, current: () => boolean) => Promise<void>,
|
|
73
|
+
): Promise<void> {
|
|
72
74
|
invalidate()
|
|
73
75
|
const id = generation
|
|
74
76
|
const controller = new AbortController()
|
|
@@ -130,7 +132,9 @@
|
|
|
130
132
|
function scan(pick: boolean): Promise<void> {
|
|
131
133
|
return act(async (signal, current) => {
|
|
132
134
|
if (!sources) return
|
|
133
|
-
const found = await (pick
|
|
135
|
+
const found = await (pick
|
|
136
|
+
? sources.pickSearchDirectory({ signal })
|
|
137
|
+
: sources.discover({ signal }))
|
|
134
138
|
if (current() && found !== null) {
|
|
135
139
|
system = found
|
|
136
140
|
searched = true
|
|
@@ -155,7 +159,9 @@
|
|
|
155
159
|
input.value = ''
|
|
156
160
|
if (!picked) return
|
|
157
161
|
void act(async (_signal, current) => {
|
|
158
|
-
const next: Source = {
|
|
162
|
+
const next: Source = {
|
|
163
|
+
bytes: { fileName: picked.name, bytes: [...new Uint8Array(await picked.arrayBuffer())] },
|
|
164
|
+
}
|
|
159
165
|
await showPreview(next, picked.name, current)
|
|
160
166
|
})
|
|
161
167
|
}
|
|
@@ -195,7 +201,9 @@
|
|
|
195
201
|
<DropdownMenu.Trigger>
|
|
196
202
|
{#snippet child({ props })}
|
|
197
203
|
<Button {...props} {disabled}
|
|
198
|
-
><Plus data-icon="inline-start" />{t('skills.add.title')}<ChevronDown
|
|
204
|
+
><Plus data-icon="inline-start" />{t('skills.add.title')}<ChevronDown
|
|
205
|
+
data-icon="inline-end"
|
|
206
|
+
/></Button
|
|
199
207
|
>
|
|
200
208
|
{/snippet}
|
|
201
209
|
</DropdownMenu.Trigger>
|
|
@@ -203,13 +211,20 @@
|
|
|
203
211
|
<DropdownMenu.Group>
|
|
204
212
|
{#if sources}
|
|
205
213
|
{#if sources.authoring && ports.providers}
|
|
206
|
-
<DropdownMenu.Item onSelect={() => open('agent')}
|
|
214
|
+
<DropdownMenu.Item onSelect={() => open('agent')}
|
|
215
|
+
><Bot />{t('skills.add.agent')}</DropdownMenu.Item
|
|
216
|
+
>
|
|
207
217
|
{/if}
|
|
208
|
-
<DropdownMenu.Item onSelect={() => open('online')}
|
|
209
|
-
|
|
218
|
+
<DropdownMenu.Item onSelect={() => open('online')}
|
|
219
|
+
><Search />{t('skills.add.online')}</DropdownMenu.Item
|
|
220
|
+
>
|
|
221
|
+
<DropdownMenu.Item onSelect={() => open('system')}
|
|
222
|
+
><FolderSearch />{t('skills.add.system')}</DropdownMenu.Item
|
|
210
223
|
>
|
|
211
224
|
{/if}
|
|
212
|
-
<DropdownMenu.Item onSelect={() => open('local')}
|
|
225
|
+
<DropdownMenu.Item onSelect={() => open('local')}
|
|
226
|
+
><Import />{t('skills.add.local')}</DropdownMenu.Item
|
|
227
|
+
>
|
|
213
228
|
</DropdownMenu.Group>
|
|
214
229
|
</DropdownMenu.Content>
|
|
215
230
|
</DropdownMenu.Root>
|
|
@@ -265,10 +280,15 @@
|
|
|
265
280
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
266
281
|
<strong class="text-sm">{candidate.name}</strong>
|
|
267
282
|
<p class="text-muted-foreground text-xs">{candidate.description}</p>
|
|
268
|
-
<p class="text-muted-foreground break-all text-xs">
|
|
283
|
+
<p class="text-muted-foreground break-all text-xs">
|
|
284
|
+
{candidate.origin} · {candidate.url}
|
|
285
|
+
</p>
|
|
269
286
|
</div>
|
|
270
|
-
<Button
|
|
271
|
-
|
|
287
|
+
<Button
|
|
288
|
+
size="sm"
|
|
289
|
+
variant="outline"
|
|
290
|
+
disabled={busy}
|
|
291
|
+
onclick={() => void prepare(candidate)}>{t('skills.add.preview')}</Button
|
|
272
292
|
>
|
|
273
293
|
</li>
|
|
274
294
|
{/each}
|
|
@@ -276,9 +296,13 @@
|
|
|
276
296
|
{:else if mode === 'system'}
|
|
277
297
|
<p class="text-muted-foreground text-sm">{t('skills.add.system_hint')}</p>
|
|
278
298
|
<div class="flex flex-wrap gap-2">
|
|
279
|
-
<Button variant="outline" disabled={busy} onclick={() => void scan(false)}
|
|
299
|
+
<Button variant="outline" disabled={busy} onclick={() => void scan(false)}
|
|
300
|
+
>{t('skills.add.rescan')}</Button
|
|
301
|
+
>
|
|
280
302
|
<Button variant="outline" disabled={busy} onclick={() => void scan(true)}
|
|
281
|
-
><FolderSearch data-icon="inline-start" />{t(
|
|
303
|
+
><FolderSearch data-icon="inline-start" />{t(
|
|
304
|
+
'skills.add.pick_search_directory',
|
|
305
|
+
)}</Button
|
|
282
306
|
>
|
|
283
307
|
</div>
|
|
284
308
|
<ul class="flex flex-col gap-2" aria-label={t('skills.add.results')}>
|
|
@@ -286,10 +310,15 @@
|
|
|
286
310
|
<li class="flex items-start gap-3 rounded-md border p-3">
|
|
287
311
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
288
312
|
<strong class="text-sm">{candidate.name}</strong>
|
|
289
|
-
<p class="text-muted-foreground break-all text-xs">
|
|
313
|
+
<p class="text-muted-foreground break-all text-xs">
|
|
314
|
+
{candidate.origin} · {candidate.path}
|
|
315
|
+
</p>
|
|
290
316
|
</div>
|
|
291
|
-
<Button
|
|
292
|
-
|
|
317
|
+
<Button
|
|
318
|
+
size="sm"
|
|
319
|
+
variant="outline"
|
|
320
|
+
disabled={busy}
|
|
321
|
+
onclick={() => void prepareSystem(candidate)}>{t('skills.add.preview')}</Button
|
|
293
322
|
>
|
|
294
323
|
</li>
|
|
295
324
|
{/each}
|
|
@@ -305,12 +334,16 @@
|
|
|
305
334
|
>
|
|
306
335
|
</div>
|
|
307
336
|
{/if}
|
|
308
|
-
{#if busy}<p role="status" class="text-muted-foreground text-sm">
|
|
337
|
+
{#if busy}<p role="status" class="text-muted-foreground text-sm">
|
|
338
|
+
{t('skills.add.working')}
|
|
339
|
+
</p>{/if}
|
|
309
340
|
{#if !busy && searched && ((mode === 'online' && candidates.length === 0) || (mode === 'system' && system.length === 0))}
|
|
310
341
|
<p role="status" class="text-muted-foreground text-sm">{t('skills.add.no_results')}</p>
|
|
311
342
|
{/if}
|
|
312
343
|
</div>
|
|
313
|
-
<Dialog.Footer
|
|
344
|
+
<Dialog.Footer
|
|
345
|
+
><Button variant="outline" onclick={close}>{t('common.cancel')}</Button></Dialog.Footer
|
|
346
|
+
>
|
|
314
347
|
</Dialog.Content>
|
|
315
348
|
</Dialog.Root>
|
|
316
349
|
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
import * as Field from '#lib/components/ui/field/index.js'
|
|
7
7
|
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
8
8
|
import { getKitContext } from '../context'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
KitClientError,
|
|
11
|
+
type ModelRef,
|
|
12
|
+
type PreparedSkill,
|
|
13
|
+
type RunEvent,
|
|
14
|
+
type SkillDto,
|
|
15
|
+
} from '../ports'
|
|
10
16
|
import ModelSelect from '../providers/ModelSelect.svelte'
|
|
11
17
|
import ApprovalCard from '../chat/ApprovalCard.svelte'
|
|
12
18
|
import ToolCallItem from '../chat/ToolCallItem.svelte'
|
|
@@ -94,7 +100,9 @@
|
|
|
94
100
|
await cleanup
|
|
95
101
|
if (!alive || epoch !== currentEpoch || !active) return
|
|
96
102
|
const skills = await ports.skills?.list()
|
|
97
|
-
const builtin = skills?.find(
|
|
103
|
+
const builtin = skills?.find(
|
|
104
|
+
(skill) => skill.origin === 'builtin' && skill.name === 'skill-creator',
|
|
105
|
+
)
|
|
98
106
|
if (!builtin || !ports.skills)
|
|
99
107
|
throw new KitClientError({ code: 'needs_config', message: 'skill-creator unavailable' })
|
|
100
108
|
const full = await ports.skills.get(builtin.id)
|
|
@@ -158,7 +166,8 @@
|
|
|
158
166
|
const currentEpoch = epoch
|
|
159
167
|
const controller = new AbortController()
|
|
160
168
|
operation = controller
|
|
161
|
-
const current = (): boolean =>
|
|
169
|
+
const current = (): boolean =>
|
|
170
|
+
alive && epoch === currentEpoch && session === id && !controller.signal.aborted
|
|
162
171
|
const turnId = ++nextTurn
|
|
163
172
|
turns.push({ id: turnId, input: input.trim(), text: '', tools: [], status: 'running' })
|
|
164
173
|
// The pushed literal is wrapped by the $state proxy; mutate the proxied entry.
|
|
@@ -170,7 +179,8 @@
|
|
|
170
179
|
let ended = false
|
|
171
180
|
const receive = (event: RunEvent): void => {
|
|
172
181
|
if (!current() || ended) return
|
|
173
|
-
const tool = (callId: string): ToolItem | undefined =>
|
|
182
|
+
const tool = (callId: string): ToolItem | undefined =>
|
|
183
|
+
turn.tools.find((item) => item.callId === callId)
|
|
174
184
|
switch (event.kind) {
|
|
175
185
|
case 'text_delta':
|
|
176
186
|
turn.text += event.text
|
|
@@ -202,19 +212,25 @@
|
|
|
202
212
|
}
|
|
203
213
|
case 'tool_result': {
|
|
204
214
|
const item = tool(event.callId)
|
|
205
|
-
if (item)
|
|
215
|
+
if (item)
|
|
216
|
+
Object.assign(item, { pending: null, outcome: event.outcome, summary: event.summary })
|
|
206
217
|
break
|
|
207
218
|
}
|
|
208
219
|
case 'end':
|
|
209
220
|
ended = true
|
|
210
221
|
finish(turn, event.status)
|
|
211
222
|
if (event.status === 'failed')
|
|
212
|
-
error = errorText(
|
|
223
|
+
error = errorText(
|
|
224
|
+
t,
|
|
225
|
+
new KitClientError(event.error ?? { code: 'internal', message: '' }),
|
|
226
|
+
)
|
|
213
227
|
break
|
|
214
228
|
}
|
|
215
229
|
}
|
|
216
230
|
try {
|
|
217
|
-
const accepted = await authoring.send(id, turn.input, model, receive, {
|
|
231
|
+
const accepted = await authoring.send(id, turn.input, model, receive, {
|
|
232
|
+
signal: controller.signal,
|
|
233
|
+
})
|
|
218
234
|
if (!current()) {
|
|
219
235
|
if (!ended) await authoring.cancel(id, accepted.runId).catch(report)
|
|
220
236
|
if (alive && epoch === currentEpoch && session === id && controller.signal.aborted) {
|
|
@@ -233,7 +249,15 @@
|
|
|
233
249
|
}
|
|
234
250
|
}
|
|
235
251
|
async function approve(tool: ToolItem, allow: boolean): Promise<void> {
|
|
236
|
-
if (
|
|
252
|
+
if (
|
|
253
|
+
!authoring ||
|
|
254
|
+
!session ||
|
|
255
|
+
!runId ||
|
|
256
|
+
!running ||
|
|
257
|
+
tool.pending === null ||
|
|
258
|
+
answering.includes(tool.callId)
|
|
259
|
+
)
|
|
260
|
+
return
|
|
237
261
|
const id = session
|
|
238
262
|
const run = runId
|
|
239
263
|
const currentEpoch = epoch
|
|
@@ -244,7 +268,8 @@
|
|
|
244
268
|
} catch (cause) {
|
|
245
269
|
if (alive && epoch === currentEpoch && runId === run) error = errorText(t, cause)
|
|
246
270
|
} finally {
|
|
247
|
-
if (alive && epoch === currentEpoch)
|
|
271
|
+
if (alive && epoch === currentEpoch)
|
|
272
|
+
answering = answering.filter((callId) => callId !== tool.callId)
|
|
248
273
|
}
|
|
249
274
|
}
|
|
250
275
|
async function stop(): Promise<void> {
|
|
@@ -277,7 +302,8 @@
|
|
|
277
302
|
error = null
|
|
278
303
|
try {
|
|
279
304
|
const prepared = await authoring.prepare(id, { signal: controller.signal })
|
|
280
|
-
if (alive && epoch === currentEpoch && session === id && !controller.signal.aborted)
|
|
305
|
+
if (alive && epoch === currentEpoch && session === id && !controller.signal.aborted)
|
|
306
|
+
await onprepared(prepared)
|
|
281
307
|
} catch (cause) {
|
|
282
308
|
if (alive && epoch === currentEpoch) error = errorText(t, cause)
|
|
283
309
|
} finally {
|
|
@@ -305,7 +331,9 @@
|
|
|
305
331
|
{#if !session}
|
|
306
332
|
<p class="text-sm">{t('skills.add.creator_review')}</p>
|
|
307
333
|
{#if creator}
|
|
308
|
-
<Button disabled={loading} onclick={() => (creatorReview = true)}
|
|
334
|
+
<Button disabled={loading} onclick={() => (creatorReview = true)}
|
|
335
|
+
>{t('skills.add.review_creator')}</Button
|
|
336
|
+
>
|
|
309
337
|
{/if}
|
|
310
338
|
{:else}
|
|
311
339
|
<Field.Group>
|
|
@@ -332,33 +360,54 @@
|
|
|
332
360
|
</fieldset>
|
|
333
361
|
{:else}<ToolCallItem {tool} />{/if}
|
|
334
362
|
{/each}
|
|
335
|
-
{#if turn.status === 'running'}<p
|
|
363
|
+
{#if turn.status === 'running'}<p
|
|
364
|
+
role="status"
|
|
365
|
+
class="text-muted-foreground text-xs"
|
|
366
|
+
>
|
|
336
367
|
{t('skills.add.working')}
|
|
337
368
|
</p>{/if}
|
|
338
|
-
{#if turn.status === 'cancelled'}<p
|
|
369
|
+
{#if turn.status === 'cancelled'}<p
|
|
370
|
+
role="status"
|
|
371
|
+
class="text-muted-foreground text-xs"
|
|
372
|
+
>
|
|
339
373
|
{t('chat.stopped')}
|
|
340
374
|
</p>{/if}
|
|
341
375
|
</li>
|
|
342
376
|
{/each}
|
|
343
377
|
</ol>
|
|
344
378
|
<Field.Field>
|
|
345
|
-
<Field.Label for="skill-authoring-message"
|
|
346
|
-
|
|
379
|
+
<Field.Label for="skill-authoring-message"
|
|
380
|
+
>{t('skills.add.authoring_input')}</Field.Label
|
|
381
|
+
>
|
|
382
|
+
<Textarea
|
|
383
|
+
id="skill-authoring-message"
|
|
384
|
+
bind:value={input}
|
|
385
|
+
disabled={running || loading}
|
|
386
|
+
rows={3}
|
|
387
|
+
/>
|
|
347
388
|
</Field.Field>
|
|
348
389
|
<div class="flex flex-wrap gap-2">
|
|
349
390
|
<Button disabled={running || loading || !input.trim()} onclick={() => void send()}
|
|
350
391
|
><Bot data-icon="inline-start" />{t('chat.send')}</Button
|
|
351
392
|
>
|
|
352
|
-
{#if running}<Button variant="outline" onclick={() => void stop()}
|
|
353
|
-
|
|
354
|
-
>{
|
|
393
|
+
{#if running}<Button variant="outline" onclick={() => void stop()}
|
|
394
|
+
>{t('chat.stop')}</Button
|
|
395
|
+
>{/if}
|
|
396
|
+
<Button
|
|
397
|
+
variant="outline"
|
|
398
|
+
disabled={running || loading || turns.length === 0}
|
|
399
|
+
onclick={() => void prepare()}>{t('skills.add.preview_artifact')}</Button
|
|
355
400
|
>
|
|
356
401
|
</div>
|
|
357
402
|
</Field.Group>
|
|
358
403
|
{/if}
|
|
359
|
-
{#if loading}<p role="status" class="text-muted-foreground text-sm">
|
|
404
|
+
{#if loading}<p role="status" class="text-muted-foreground text-sm">
|
|
405
|
+
{t('skills.add.working')}
|
|
406
|
+
</p>{/if}
|
|
360
407
|
</div>
|
|
361
|
-
<Dialog.Footer
|
|
408
|
+
<Dialog.Footer
|
|
409
|
+
><Button variant="outline" onclick={onclose}>{t('common.cancel')}</Button></Dialog.Footer
|
|
410
|
+
>
|
|
362
411
|
</Dialog.Content>
|
|
363
412
|
</Dialog.Root>
|
|
364
413
|
|
|
@@ -62,7 +62,9 @@
|
|
|
62
62
|
<Dialog.Root bind:open>
|
|
63
63
|
<Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
|
|
64
64
|
<Dialog.Header>
|
|
65
|
-
<Dialog.Title
|
|
65
|
+
<Dialog.Title
|
|
66
|
+
>{skill ? t('skills.edit_title', { name: skill.name }) : t('skills.new')}</Dialog.Title
|
|
67
|
+
>
|
|
66
68
|
</Dialog.Header>
|
|
67
69
|
<Field.Group class="min-h-0 overflow-y-auto">
|
|
68
70
|
<div class="flex gap-3">
|
|
@@ -72,7 +74,12 @@
|
|
|
72
74
|
</Field.Field>
|
|
73
75
|
<Field.Field class="w-32">
|
|
74
76
|
<Field.Label for="kit-skill-version">{t('skills.version')}</Field.Label>
|
|
75
|
-
<Input
|
|
77
|
+
<Input
|
|
78
|
+
id="kit-skill-version"
|
|
79
|
+
maxlength={32}
|
|
80
|
+
placeholder={t('skills.unversioned')}
|
|
81
|
+
bind:value={version}
|
|
82
|
+
/>
|
|
76
83
|
</Field.Field>
|
|
77
84
|
</div>
|
|
78
85
|
<Field.Field>
|
|
@@ -86,7 +93,10 @@
|
|
|
86
93
|
</Field.Group>
|
|
87
94
|
<Dialog.Footer>
|
|
88
95
|
<Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
|
|
89
|
-
<Button
|
|
96
|
+
<Button
|
|
97
|
+
disabled={saving || !name.trim() || !description.trim() || !body.trim()}
|
|
98
|
+
onclick={() => void save()}
|
|
99
|
+
>
|
|
90
100
|
{t('skills.save')}
|
|
91
101
|
</Button>
|
|
92
102
|
</Dialog.Footer>
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import type { Preview, PreviewCurrent } from '../ports'
|
|
9
9
|
|
|
10
10
|
/** What both a Preview and a stored SkillDto carry. */
|
|
11
|
-
type SkillText = Pick<
|
|
11
|
+
type SkillText = Pick<
|
|
12
|
+
Preview,
|
|
13
|
+
'name' | 'description' | 'version' | 'body' | 'license' | 'extra' | 'files'
|
|
14
|
+
>
|
|
12
15
|
|
|
13
16
|
let {
|
|
14
17
|
open = $bindable(false),
|
|
@@ -82,14 +85,19 @@
|
|
|
82
85
|
</Alert.Root>
|
|
83
86
|
{/if}
|
|
84
87
|
{#if files.length > 0}
|
|
85
|
-
<section
|
|
88
|
+
<section
|
|
89
|
+
class="flex flex-col gap-1 text-xs"
|
|
90
|
+
aria-label={t('skills.files', { count: files.length })}
|
|
91
|
+
>
|
|
86
92
|
<h3 class="font-medium">{t('skills.files', { count: files.length })}</h3>
|
|
87
93
|
<ul class="max-h-40 overflow-y-auto font-mono">
|
|
88
94
|
{#each files as file (file.path)}
|
|
89
95
|
<li class="flex gap-3">
|
|
90
96
|
<span class="min-w-0 flex-1 truncate">{file.path}</span>
|
|
91
97
|
<span class="text-muted-foreground">{t(`skills.file_kind.${file.kind}`)}</span>
|
|
92
|
-
<span class="text-muted-foreground"
|
|
98
|
+
<span class="text-muted-foreground"
|
|
99
|
+
>{t('skills.file_size', { size: file.size })}</span
|
|
100
|
+
>
|
|
93
101
|
</li>
|
|
94
102
|
{/each}
|
|
95
103
|
</ul>
|
|
@@ -106,14 +114,20 @@
|
|
|
106
114
|
{#if onconfirm && current}
|
|
107
115
|
<label class="flex items-center gap-2 text-sm">
|
|
108
116
|
<input type="checkbox" class="accent-primary" bind:checked={replace} />
|
|
109
|
-
{t('skills.replace', {
|
|
117
|
+
{t('skills.replace', {
|
|
118
|
+
version: current.version ?? t('skills.unversioned'),
|
|
119
|
+
revision: current.revision,
|
|
120
|
+
})}
|
|
110
121
|
</label>
|
|
111
122
|
{/if}
|
|
112
123
|
{/if}
|
|
113
124
|
<Dialog.Footer>
|
|
114
125
|
<Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
|
|
115
126
|
{#if onconfirm}
|
|
116
|
-
<Button
|
|
127
|
+
<Button
|
|
128
|
+
disabled={busy || !skill || (current !== null && !replace)}
|
|
129
|
+
onclick={() => onconfirm(replace)}
|
|
130
|
+
>
|
|
117
131
|
{confirmLabel ?? t('skills.install')}
|
|
118
132
|
</Button>
|
|
119
133
|
{/if}
|
|
@@ -92,12 +92,16 @@
|
|
|
92
92
|
<span class="flex min-w-0 items-center gap-2">
|
|
93
93
|
<span class="truncate font-mono text-sm">{skill.name}</span>
|
|
94
94
|
{#if skill.hasScripts}
|
|
95
|
-
<span class="shrink-0 rounded border px-1.5 text-xs"
|
|
95
|
+
<span class="shrink-0 rounded border px-1.5 text-xs"
|
|
96
|
+
>{t('skills.has_scripts')}</span
|
|
97
|
+
>
|
|
96
98
|
{/if}
|
|
97
99
|
</span>
|
|
98
100
|
<span class="text-muted-foreground truncate text-xs">
|
|
99
101
|
{skill.version ?? t('skills.unversioned')} · {t(`skills.origin.${skill.origin}`)} ·
|
|
100
|
-
{t('skills.revision', { revision: skill.revision })} · {t('skills.chars', {
|
|
102
|
+
{t('skills.revision', { revision: skill.revision })} · {t('skills.chars', {
|
|
103
|
+
count: skill.bodyChars,
|
|
104
|
+
})}
|
|
101
105
|
{#if skill.fileCount > 0}· {t('skills.file_count', { count: skill.fileCount })}{/if}
|
|
102
106
|
</span>
|
|
103
107
|
</div>
|
|
@@ -126,8 +130,11 @@
|
|
|
126
130
|
<Button size="sm" variant="outline" disabled={busy} onclick={() => edit(skill.id)}
|
|
127
131
|
>{t('skills.edit')}</Button
|
|
128
132
|
>
|
|
129
|
-
<Button
|
|
130
|
-
|
|
133
|
+
<Button
|
|
134
|
+
size="sm"
|
|
135
|
+
variant="destructive"
|
|
136
|
+
disabled={busy}
|
|
137
|
+
onclick={() => void remove(skill)}>{t('skills.delete')}</Button
|
|
131
138
|
>
|
|
132
139
|
{/if}
|
|
133
140
|
</li>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/svelte'
|
|
2
|
+
import { createI18n } from '../../i18n'
|
|
3
|
+
import {
|
|
4
|
+
createFakePorts,
|
|
5
|
+
type FakePortsOptions,
|
|
6
|
+
type Preview,
|
|
7
|
+
type RunEvent,
|
|
8
|
+
type SkillAuthoringPort,
|
|
9
|
+
type SkillDto,
|
|
10
|
+
type SkillMeta,
|
|
11
|
+
type Source,
|
|
12
|
+
} from '../../ports'
|
|
13
|
+
import { vi } from 'vitest'
|
|
14
|
+
import Harness from '../Harness.test.svelte'
|
|
15
|
+
|
|
16
|
+
export const meta = (
|
|
17
|
+
revision: number,
|
|
18
|
+
files: Pick<SkillMeta, 'hasScripts' | 'fileCount'> = { hasScripts: false, fileCount: 0 },
|
|
19
|
+
): SkillMeta => ({
|
|
20
|
+
id: 's1',
|
|
21
|
+
name: 'writer',
|
|
22
|
+
description: 'Writes',
|
|
23
|
+
version: null,
|
|
24
|
+
origin: 'imported',
|
|
25
|
+
revision,
|
|
26
|
+
bodyChars: 4,
|
|
27
|
+
updatedAt: 0,
|
|
28
|
+
...files,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export const preview = (body: string): Preview => ({
|
|
32
|
+
digest: `d-${body}`,
|
|
33
|
+
name: 'writer',
|
|
34
|
+
description: 'Writes',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
body,
|
|
37
|
+
license: null,
|
|
38
|
+
extra: {},
|
|
39
|
+
files: [],
|
|
40
|
+
action: 'install',
|
|
41
|
+
current: null,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export function mount(options: FakePortsOptions, skills?: { id: string; revision: number }[]) {
|
|
45
|
+
const ports = createFakePorts({
|
|
46
|
+
providers: {
|
|
47
|
+
modelOptions: () =>
|
|
48
|
+
Promise.resolve([
|
|
49
|
+
{
|
|
50
|
+
endpointId: 'e1',
|
|
51
|
+
modelId: 'm1',
|
|
52
|
+
label: 'm1',
|
|
53
|
+
name: null,
|
|
54
|
+
modelType: 'chat',
|
|
55
|
+
supportsTools: null,
|
|
56
|
+
reasoning: null,
|
|
57
|
+
nativeWebSearch: false,
|
|
58
|
+
canVision: false,
|
|
59
|
+
},
|
|
60
|
+
]),
|
|
61
|
+
},
|
|
62
|
+
...options,
|
|
63
|
+
})
|
|
64
|
+
const notify = { toast: vi.fn(), confirm: () => Promise.resolve(true) }
|
|
65
|
+
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }
|
|
66
|
+
const view = render(Harness, {
|
|
67
|
+
kit: { ports, i18n: createI18n('en'), notify, logger },
|
|
68
|
+
...(skills ? { skills } : {}),
|
|
69
|
+
})
|
|
70
|
+
return { ...notify, unmount: view.unmount }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function addFrom(name: string) {
|
|
74
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Add skill' }))
|
|
75
|
+
await fireEvent.click(await screen.findByRole('menuitem', { name }))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function selectAuthoringModel() {
|
|
79
|
+
await fireEvent.click(await screen.findByLabelText('Select a model'))
|
|
80
|
+
await fireEvent.click(await screen.findByRole('option', { name: /m1/ }))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const creator: SkillDto = {
|
|
84
|
+
...meta(1),
|
|
85
|
+
id: 'builtin-creator',
|
|
86
|
+
name: 'skill-creator',
|
|
87
|
+
origin: 'builtin',
|
|
88
|
+
body: 'Complete creator instructions: capture intent, draft, evaluate, improve, package.',
|
|
89
|
+
license: null,
|
|
90
|
+
extra: {},
|
|
91
|
+
files: [{ path: 'scripts/quick_validate.py', kind: 'script', size: 10 }],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function authoring(overrides: Partial<SkillAuthoringPort> = {}): SkillAuthoringPort {
|
|
95
|
+
return {
|
|
96
|
+
create: vi.fn(async () => 'authoring-1'),
|
|
97
|
+
send: vi.fn<SkillAuthoringPort['send']>(async (_session, _input, _model, emit) => {
|
|
98
|
+
emit({ kind: 'end', status: 'completed' })
|
|
99
|
+
return { runId: 'authoring-run', conversationId: null, turnId: null }
|
|
100
|
+
}),
|
|
101
|
+
approve: vi.fn(async () => {}),
|
|
102
|
+
cancel: vi.fn(async () => {}),
|
|
103
|
+
prepare: vi.fn(async () => ({
|
|
104
|
+
source: { bytes: { fileName: 'writer.zip', bytes: [1] } },
|
|
105
|
+
origin: 'Authoring workspace',
|
|
106
|
+
})),
|
|
107
|
+
close: vi.fn(async () => {}),
|
|
108
|
+
...overrides,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function mountAuthoring(api: SkillAuthoringPort, options: FakePortsOptions = {}) {
|
|
113
|
+
return mount({
|
|
114
|
+
...options,
|
|
115
|
+
skillSources: { ...options.skillSources, authoring: api },
|
|
116
|
+
skills: {
|
|
117
|
+
list: async () => [creator],
|
|
118
|
+
get: async () => creator,
|
|
119
|
+
...options.skills,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function startAuthoring() {
|
|
125
|
+
await addFrom('Create with Agent')
|
|
126
|
+
await fireEvent.click(await screen.findByRole('button', { name: 'Review skill-creator' }))
|
|
127
|
+
await screen.findByText(creator.body)
|
|
128
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Confirm skill-creator and start' }))
|
|
129
|
+
await screen.findByLabelText('Message to the skill creation assistant')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function authorMessage(text: string) {
|
|
133
|
+
await fireEvent.input(screen.getByLabelText('Message to the skill creation assistant'), {
|
|
134
|
+
target: { value: text },
|
|
135
|
+
})
|
|
136
|
+
await fireEvent.click(screen.getByRole('button', { name: /^Send$/ }))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type { Preview, RunEvent, SkillAuthoringPort, Source }
|
package/src/tauri/index.ts
CHANGED
|
@@ -39,7 +39,9 @@ async function call<T>(command: string, args: Record<string, unknown> = {}): Pro
|
|
|
39
39
|
error !== null &&
|
|
40
40
|
typeof (error as Partial<KitError>).code === 'string' &&
|
|
41
41
|
typeof (error as Partial<KitError>).message === 'string'
|
|
42
|
-
throw new KitClientError(
|
|
42
|
+
throw new KitClientError(
|
|
43
|
+
isKitError ? (error as KitError) : { code: 'internal', message: String(error) },
|
|
44
|
+
)
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -103,14 +105,19 @@ export function createTauriPorts(
|
|
|
103
105
|
const search: SearchPort = {
|
|
104
106
|
settings: (capability, o) => searchCall('search_settings', { capability }, o),
|
|
105
107
|
save: (input, o) => searchCall('search_save', { input }, o),
|
|
106
|
-
setDefault: (capability, provider, o) =>
|
|
108
|
+
setDefault: (capability, provider, o) =>
|
|
109
|
+
searchCall('search_set_default', { capability, provider }, o),
|
|
107
110
|
savePreferences: (input, o) => searchCall('search_save_preferences', { input }, o),
|
|
108
111
|
saveKey: (capability, provider, value, keyId, o) =>
|
|
109
112
|
searchCall('search_save_key', { capability, provider, value, keyId: keyId ?? null }, o),
|
|
110
113
|
removeKey: (capability, provider, keyId, o) =>
|
|
111
114
|
searchCall('search_remove_key', { capability, provider, keyId }, o),
|
|
112
115
|
reveal: (capability, provider, keyId, currentPassword, o) =>
|
|
113
|
-
searchCall(
|
|
116
|
+
searchCall(
|
|
117
|
+
'search_reveal',
|
|
118
|
+
{ capability, provider, keyId, currentPassword: currentPassword ?? null },
|
|
119
|
+
o,
|
|
120
|
+
),
|
|
114
121
|
check: (capability, provider, o) => searchCall('search_check', { capability, provider }, o),
|
|
115
122
|
search: (query, o) => searchCall('search_query', { query }, o),
|
|
116
123
|
fetch: (url, o) => searchCall('search_fetch', { url }, o),
|
|
@@ -183,7 +190,8 @@ export function createTauriPorts(
|
|
|
183
190
|
get: (id) => call('skills_get', { id }),
|
|
184
191
|
save: (input) => call('skills_save', { input }),
|
|
185
192
|
preview: (source) => call('skills_preview', { source }),
|
|
186
|
-
install: (source, confirmedDigest, replace) =>
|
|
193
|
+
install: (source, confirmedDigest, replace) =>
|
|
194
|
+
call('skills_install', { source, confirmedDigest, replace }),
|
|
187
195
|
duplicate: (id, newName) => call('skills_duplicate', { id, newName }),
|
|
188
196
|
reorder: (ids) => call('skills_reorder', { ids }),
|
|
189
197
|
remove: (id) => call('skills_delete', { id }),
|
|
@@ -218,7 +226,9 @@ export function createTauriPorts(
|
|
|
218
226
|
let ended = false
|
|
219
227
|
const cancel = () => {
|
|
220
228
|
if (accepted && !ended)
|
|
221
|
-
void call('skills_authoring_cancel', { sessionId, runId: accepted.runId }).catch(
|
|
229
|
+
void call('skills_authoring_cancel', { sessionId, runId: accepted.runId }).catch(
|
|
230
|
+
() => undefined,
|
|
231
|
+
)
|
|
222
232
|
}
|
|
223
233
|
signal?.addEventListener('abort', cancel, { once: true })
|
|
224
234
|
const channel = new Channel<RunEvent>((event) => {
|
|
@@ -230,7 +240,12 @@ export function createTauriPorts(
|
|
|
230
240
|
onEvent(event)
|
|
231
241
|
})
|
|
232
242
|
try {
|
|
233
|
-
accepted = await call<RunAccepted>('skills_authoring_send', {
|
|
243
|
+
accepted = await call<RunAccepted>('skills_authoring_send', {
|
|
244
|
+
sessionId,
|
|
245
|
+
input,
|
|
246
|
+
model,
|
|
247
|
+
onEvent: channel,
|
|
248
|
+
})
|
|
234
249
|
if (signal?.aborted) cancel()
|
|
235
250
|
return accepted
|
|
236
251
|
} catch (error) {
|
|
@@ -245,7 +260,8 @@ export function createTauriPorts(
|
|
|
245
260
|
cancel: async (sessionId, runId) => {
|
|
246
261
|
await call('skills_authoring_cancel', { sessionId, runId })
|
|
247
262
|
},
|
|
248
|
-
prepare: (sessionId, o) =>
|
|
263
|
+
prepare: (sessionId, o) =>
|
|
264
|
+
sourceCall<PreparedSkill>('skills_authoring_prepare', { sessionId }, o),
|
|
249
265
|
close: async (sessionId) => {
|
|
250
266
|
await call('skills_authoring_close', { sessionId })
|
|
251
267
|
},
|