@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,381 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, untrack } from 'svelte'
|
|
3
|
+
import { Bot } 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 Field from '#lib/components/ui/field/index.js'
|
|
7
|
+
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
8
|
+
import { getKitContext } from '../context'
|
|
9
|
+
import { KitClientError, type ModelRef, type PreparedSkill, type RunEvent, type SkillDto } from '../ports'
|
|
10
|
+
import ModelSelect from '../providers/ModelSelect.svelte'
|
|
11
|
+
import ApprovalCard from '../chat/ApprovalCard.svelte'
|
|
12
|
+
import ToolCallItem from '../chat/ToolCallItem.svelte'
|
|
13
|
+
import type { ToolItem } from '../chat/run-session.svelte'
|
|
14
|
+
import { errorText } from './error'
|
|
15
|
+
import SkillPreviewDialog from './SkillPreviewDialog.svelte'
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
active,
|
|
19
|
+
open,
|
|
20
|
+
onclose,
|
|
21
|
+
onprepared,
|
|
22
|
+
}: {
|
|
23
|
+
active: boolean
|
|
24
|
+
open: boolean
|
|
25
|
+
onclose: () => void
|
|
26
|
+
onprepared: (prepared: PreparedSkill) => Promise<void>
|
|
27
|
+
} = $props()
|
|
28
|
+
const { ports, i18n, notify } = getKitContext()
|
|
29
|
+
const { t } = i18n
|
|
30
|
+
const authoring = ports.skillSources?.authoring
|
|
31
|
+
type Turn = {
|
|
32
|
+
id: number
|
|
33
|
+
input: string
|
|
34
|
+
text: string
|
|
35
|
+
tools: ToolItem[]
|
|
36
|
+
status: 'running' | 'completed' | 'failed' | 'cancelled'
|
|
37
|
+
}
|
|
38
|
+
let creator = $state<SkillDto | null>(null)
|
|
39
|
+
let session = $state<string | null>(null)
|
|
40
|
+
let model = $state<ModelRef | null>(null)
|
|
41
|
+
let input = $state('')
|
|
42
|
+
let creatorReview = $state(false)
|
|
43
|
+
let turns = $state<Turn[]>([])
|
|
44
|
+
let loading = $state(false)
|
|
45
|
+
let running = $state(false)
|
|
46
|
+
let error = $state<string | null>(null)
|
|
47
|
+
let runId = $state<string | null>(null)
|
|
48
|
+
let answering = $state<string[]>([])
|
|
49
|
+
let operation: AbortController | null = null
|
|
50
|
+
let epoch = 0
|
|
51
|
+
let alive = true
|
|
52
|
+
let nextTurn = 0
|
|
53
|
+
let cleanup: Promise<void> = Promise.resolve()
|
|
54
|
+
|
|
55
|
+
function report(cause: unknown): void {
|
|
56
|
+
notify.toast('error', errorText(t, cause))
|
|
57
|
+
}
|
|
58
|
+
async function release(id: string, run: string | null): Promise<void> {
|
|
59
|
+
if (!authoring) return
|
|
60
|
+
try {
|
|
61
|
+
if (run) await authoring.cancel(id, run)
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
report(cause)
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await authoring.close(id)
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
report(cause)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function reset(): void {
|
|
72
|
+
epoch += 1
|
|
73
|
+
operation?.abort()
|
|
74
|
+
operation = null
|
|
75
|
+
const previous = session
|
|
76
|
+
const run = runId
|
|
77
|
+
session = null
|
|
78
|
+
runId = null
|
|
79
|
+
running = false
|
|
80
|
+
loading = false
|
|
81
|
+
creator = null
|
|
82
|
+
turns = []
|
|
83
|
+
input = ''
|
|
84
|
+
error = null
|
|
85
|
+
answering = []
|
|
86
|
+
creatorReview = false
|
|
87
|
+
if (previous) cleanup = cleanup.then(() => release(previous, run))
|
|
88
|
+
}
|
|
89
|
+
async function loadCreator(): Promise<void> {
|
|
90
|
+
reset()
|
|
91
|
+
const currentEpoch = epoch
|
|
92
|
+
loading = true
|
|
93
|
+
try {
|
|
94
|
+
await cleanup
|
|
95
|
+
if (!alive || epoch !== currentEpoch || !active) return
|
|
96
|
+
const skills = await ports.skills?.list()
|
|
97
|
+
const builtin = skills?.find((skill) => skill.origin === 'builtin' && skill.name === 'skill-creator')
|
|
98
|
+
if (!builtin || !ports.skills)
|
|
99
|
+
throw new KitClientError({ code: 'needs_config', message: 'skill-creator unavailable' })
|
|
100
|
+
const full = await ports.skills.get(builtin.id)
|
|
101
|
+
if (alive && epoch === currentEpoch) creator = full
|
|
102
|
+
} catch (cause) {
|
|
103
|
+
if (alive && epoch === currentEpoch) error = errorText(t, cause)
|
|
104
|
+
} finally {
|
|
105
|
+
if (alive && epoch === currentEpoch) loading = false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
$effect(() => {
|
|
109
|
+
if (active)
|
|
110
|
+
untrack(() => {
|
|
111
|
+
void loadCreator()
|
|
112
|
+
})
|
|
113
|
+
else untrack(reset)
|
|
114
|
+
})
|
|
115
|
+
onDestroy(() => {
|
|
116
|
+
alive = false
|
|
117
|
+
reset()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
async function confirmCreator(): Promise<void> {
|
|
121
|
+
if (!authoring || !creator || loading || session) return
|
|
122
|
+
const currentEpoch = epoch
|
|
123
|
+
const controller = new AbortController()
|
|
124
|
+
operation = controller
|
|
125
|
+
loading = true
|
|
126
|
+
error = null
|
|
127
|
+
try {
|
|
128
|
+
const id = await authoring.create({ signal: controller.signal })
|
|
129
|
+
if (!alive || epoch !== currentEpoch || controller.signal.aborted) {
|
|
130
|
+
await release(id, null)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
session = id
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
if (alive && epoch === currentEpoch && !controller.signal.aborted) error = errorText(t, cause)
|
|
136
|
+
} finally {
|
|
137
|
+
if (alive && epoch === currentEpoch) {
|
|
138
|
+
loading = false
|
|
139
|
+
operation = null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function finish(turn: Turn, status: Turn['status']): void {
|
|
144
|
+
turn.status = status
|
|
145
|
+
for (const tool of turn.tools) tool.pending = null
|
|
146
|
+
running = false
|
|
147
|
+
runId = null
|
|
148
|
+
operation = null
|
|
149
|
+
answering = []
|
|
150
|
+
}
|
|
151
|
+
async function send(): Promise<void> {
|
|
152
|
+
if (!authoring || !session || running || loading || !input.trim()) return
|
|
153
|
+
if (!model) {
|
|
154
|
+
error = t('skills.add.choose_model')
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const id = session
|
|
158
|
+
const currentEpoch = epoch
|
|
159
|
+
const controller = new AbortController()
|
|
160
|
+
operation = controller
|
|
161
|
+
const current = (): boolean => alive && epoch === currentEpoch && session === id && !controller.signal.aborted
|
|
162
|
+
const turnId = ++nextTurn
|
|
163
|
+
turns.push({ id: turnId, input: input.trim(), text: '', tools: [], status: 'running' })
|
|
164
|
+
// The pushed literal is wrapped by the $state proxy; mutate the proxied entry.
|
|
165
|
+
const turn = turns.at(-1)
|
|
166
|
+
if (!turn) return
|
|
167
|
+
input = ''
|
|
168
|
+
error = null
|
|
169
|
+
running = true
|
|
170
|
+
let ended = false
|
|
171
|
+
const receive = (event: RunEvent): void => {
|
|
172
|
+
if (!current() || ended) return
|
|
173
|
+
const tool = (callId: string): ToolItem | undefined => turn.tools.find((item) => item.callId === callId)
|
|
174
|
+
switch (event.kind) {
|
|
175
|
+
case 'text_delta':
|
|
176
|
+
turn.text += event.text
|
|
177
|
+
break
|
|
178
|
+
case 'tool_call':
|
|
179
|
+
if (!tool(event.callId))
|
|
180
|
+
turn.tools.push({
|
|
181
|
+
callId: event.callId,
|
|
182
|
+
name: event.name,
|
|
183
|
+
server: event.server ?? null,
|
|
184
|
+
pending: null,
|
|
185
|
+
outcome: null,
|
|
186
|
+
summary: '',
|
|
187
|
+
})
|
|
188
|
+
break
|
|
189
|
+
case 'approval_required': {
|
|
190
|
+
if (!tool(event.callId))
|
|
191
|
+
turn.tools.push({
|
|
192
|
+
callId: event.callId,
|
|
193
|
+
name: event.name,
|
|
194
|
+
server: null,
|
|
195
|
+
pending: null,
|
|
196
|
+
outcome: null,
|
|
197
|
+
summary: '',
|
|
198
|
+
})
|
|
199
|
+
const item = tool(event.callId)
|
|
200
|
+
if (item) item.pending = event.argsPreview
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
case 'tool_result': {
|
|
204
|
+
const item = tool(event.callId)
|
|
205
|
+
if (item) Object.assign(item, { pending: null, outcome: event.outcome, summary: event.summary })
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
case 'end':
|
|
209
|
+
ended = true
|
|
210
|
+
finish(turn, event.status)
|
|
211
|
+
if (event.status === 'failed')
|
|
212
|
+
error = errorText(t, new KitClientError(event.error ?? { code: 'internal', message: '' }))
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const accepted = await authoring.send(id, turn.input, model, receive, { signal: controller.signal })
|
|
218
|
+
if (!current()) {
|
|
219
|
+
if (!ended) await authoring.cancel(id, accepted.runId).catch(report)
|
|
220
|
+
if (alive && epoch === currentEpoch && session === id && controller.signal.aborted) {
|
|
221
|
+
finish(turn, 'cancelled')
|
|
222
|
+
loading = false
|
|
223
|
+
}
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (!ended) runId = accepted.runId
|
|
227
|
+
} catch (cause) {
|
|
228
|
+
if (alive && epoch === currentEpoch && session === id && !ended) {
|
|
229
|
+
finish(turn, controller.signal.aborted ? 'cancelled' : 'failed')
|
|
230
|
+
loading = false
|
|
231
|
+
if (!controller.signal.aborted) error = errorText(t, cause)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function approve(tool: ToolItem, allow: boolean): Promise<void> {
|
|
236
|
+
if (!authoring || !session || !runId || !running || tool.pending === null || answering.includes(tool.callId)) return
|
|
237
|
+
const id = session
|
|
238
|
+
const run = runId
|
|
239
|
+
const currentEpoch = epoch
|
|
240
|
+
answering = [...answering, tool.callId]
|
|
241
|
+
try {
|
|
242
|
+
await authoring.approve(id, run, tool.callId, allow)
|
|
243
|
+
if (alive && epoch === currentEpoch && runId === run) tool.pending = null
|
|
244
|
+
} catch (cause) {
|
|
245
|
+
if (alive && epoch === currentEpoch && runId === run) error = errorText(t, cause)
|
|
246
|
+
} finally {
|
|
247
|
+
if (alive && epoch === currentEpoch) answering = answering.filter((callId) => callId !== tool.callId)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function stop(): Promise<void> {
|
|
251
|
+
if (!authoring || !session || !running) return
|
|
252
|
+
const id = session
|
|
253
|
+
const currentEpoch = epoch
|
|
254
|
+
const run = runId
|
|
255
|
+
operation?.abort()
|
|
256
|
+
loading = true
|
|
257
|
+
if (!run) return
|
|
258
|
+
try {
|
|
259
|
+
await authoring.cancel(id, run)
|
|
260
|
+
} catch (cause) {
|
|
261
|
+
if (alive && epoch === currentEpoch) error = errorText(t, cause)
|
|
262
|
+
} finally {
|
|
263
|
+
if (alive && epoch === currentEpoch) {
|
|
264
|
+
const turn = turns.findLast((item) => item.status === 'running')
|
|
265
|
+
if (turn) finish(turn, 'cancelled')
|
|
266
|
+
loading = false
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function prepare(): Promise<void> {
|
|
271
|
+
if (!authoring || !session || running || loading) return
|
|
272
|
+
const id = session
|
|
273
|
+
const currentEpoch = epoch
|
|
274
|
+
const controller = new AbortController()
|
|
275
|
+
operation = controller
|
|
276
|
+
loading = true
|
|
277
|
+
error = null
|
|
278
|
+
try {
|
|
279
|
+
const prepared = await authoring.prepare(id, { signal: controller.signal })
|
|
280
|
+
if (alive && epoch === currentEpoch && session === id && !controller.signal.aborted) await onprepared(prepared)
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
if (alive && epoch === currentEpoch) error = errorText(t, cause)
|
|
283
|
+
} finally {
|
|
284
|
+
if (alive && epoch === currentEpoch) {
|
|
285
|
+
loading = false
|
|
286
|
+
operation = null
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
<Dialog.Root
|
|
293
|
+
open={open && !creatorReview}
|
|
294
|
+
onOpenChange={(value) => {
|
|
295
|
+
if (!value) onclose()
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
<Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
|
|
299
|
+
<Dialog.Header>
|
|
300
|
+
<Dialog.Title>{t('skills.add.agent')}</Dialog.Title>
|
|
301
|
+
<Dialog.Description>{t('skills.add.authoring_notice')}</Dialog.Description>
|
|
302
|
+
</Dialog.Header>
|
|
303
|
+
<div class="flex min-h-0 flex-col gap-4 overflow-y-auto">
|
|
304
|
+
{#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
|
|
305
|
+
{#if !session}
|
|
306
|
+
<p class="text-sm">{t('skills.add.creator_review')}</p>
|
|
307
|
+
{#if creator}
|
|
308
|
+
<Button disabled={loading} onclick={() => (creatorReview = true)}>{t('skills.add.review_creator')}</Button>
|
|
309
|
+
{/if}
|
|
310
|
+
{:else}
|
|
311
|
+
<Field.Group>
|
|
312
|
+
<Field.Field>
|
|
313
|
+
<Field.Label>{t('skills.add.model')}</Field.Label>
|
|
314
|
+
<ModelSelect
|
|
315
|
+
value={model}
|
|
316
|
+
filter={{ modelType: 'chat' }}
|
|
317
|
+
onchange={(value) => (model = value)}
|
|
318
|
+
disabled={running || loading}
|
|
319
|
+
placeholder={t('skills.add.choose_model')}
|
|
320
|
+
/>
|
|
321
|
+
</Field.Field>
|
|
322
|
+
<ol class="flex flex-col gap-4" aria-label={t('skills.add.conversation')}>
|
|
323
|
+
{#each turns as turn (turn.id)}
|
|
324
|
+
<li class="flex flex-col gap-2 rounded-md border p-3">
|
|
325
|
+
<p class="whitespace-pre-wrap text-sm">{turn.input}</p>
|
|
326
|
+
<p class="whitespace-pre-wrap text-sm">{turn.text}</p>
|
|
327
|
+
{#each turn.tools as tool (tool.callId)}
|
|
328
|
+
{#if tool.pending !== null}
|
|
329
|
+
<p class="text-destructive text-xs">{t('skills.add.tool_warning')}</p>
|
|
330
|
+
<fieldset disabled={!runId || answering.includes(tool.callId)}>
|
|
331
|
+
<ApprovalCard {tool} onAnswer={(allow) => void approve(tool, allow)} />
|
|
332
|
+
</fieldset>
|
|
333
|
+
{:else}<ToolCallItem {tool} />{/if}
|
|
334
|
+
{/each}
|
|
335
|
+
{#if turn.status === 'running'}<p role="status" class="text-muted-foreground text-xs">
|
|
336
|
+
{t('skills.add.working')}
|
|
337
|
+
</p>{/if}
|
|
338
|
+
{#if turn.status === 'cancelled'}<p role="status" class="text-muted-foreground text-xs">
|
|
339
|
+
{t('chat.stopped')}
|
|
340
|
+
</p>{/if}
|
|
341
|
+
</li>
|
|
342
|
+
{/each}
|
|
343
|
+
</ol>
|
|
344
|
+
<Field.Field>
|
|
345
|
+
<Field.Label for="skill-authoring-message">{t('skills.add.authoring_input')}</Field.Label>
|
|
346
|
+
<Textarea id="skill-authoring-message" bind:value={input} disabled={running || loading} rows={3} />
|
|
347
|
+
</Field.Field>
|
|
348
|
+
<div class="flex flex-wrap gap-2">
|
|
349
|
+
<Button disabled={running || loading || !input.trim()} onclick={() => void send()}
|
|
350
|
+
><Bot data-icon="inline-start" />{t('chat.send')}</Button
|
|
351
|
+
>
|
|
352
|
+
{#if running}<Button variant="outline" onclick={() => void stop()}>{t('chat.stop')}</Button>{/if}
|
|
353
|
+
<Button variant="outline" disabled={running || loading || turns.length === 0} onclick={() => void prepare()}
|
|
354
|
+
>{t('skills.add.preview_artifact')}</Button
|
|
355
|
+
>
|
|
356
|
+
</div>
|
|
357
|
+
</Field.Group>
|
|
358
|
+
{/if}
|
|
359
|
+
{#if loading}<p role="status" class="text-muted-foreground text-sm">{t('skills.add.working')}</p>{/if}
|
|
360
|
+
</div>
|
|
361
|
+
<Dialog.Footer><Button variant="outline" onclick={onclose}>{t('common.cancel')}</Button></Dialog.Footer>
|
|
362
|
+
</Dialog.Content>
|
|
363
|
+
</Dialog.Root>
|
|
364
|
+
|
|
365
|
+
<SkillPreviewDialog
|
|
366
|
+
skill={creator}
|
|
367
|
+
title={t('skills.add.creator_review')}
|
|
368
|
+
confirmLabel={t('skills.add.confirm_creator')}
|
|
369
|
+
busy={loading}
|
|
370
|
+
{error}
|
|
371
|
+
onconfirm={() => {
|
|
372
|
+
creatorReview = false
|
|
373
|
+
void confirmCreator()
|
|
374
|
+
}}
|
|
375
|
+
bind:open={
|
|
376
|
+
() => creatorReview && open,
|
|
377
|
+
(value) => {
|
|
378
|
+
if (!value) creatorReview = false
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Create or edit a hand-written skill (docs/14 §6). Editing sends `expectedRevision`, so a
|
|
3
|
+
// concurrent edit elsewhere fails with `conflict{revision_changed}` instead of being overwritten.
|
|
4
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
5
|
+
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
6
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
7
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
8
|
+
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
9
|
+
import { getKitContext } from '../context'
|
|
10
|
+
import type { ManualSave, SkillDto, SkillMeta, SkillsPort } from '../ports'
|
|
11
|
+
import { errorText } from './error'
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
open = $bindable(false),
|
|
15
|
+
port,
|
|
16
|
+
skill,
|
|
17
|
+
onsaved,
|
|
18
|
+
}: {
|
|
19
|
+
open: boolean
|
|
20
|
+
port: SkillsPort
|
|
21
|
+
/** The skill being edited; null creates a new one. */
|
|
22
|
+
skill: SkillDto | null
|
|
23
|
+
onsaved: (skills: SkillMeta[]) => void
|
|
24
|
+
} = $props()
|
|
25
|
+
|
|
26
|
+
const { i18n, notify } = getKitContext()
|
|
27
|
+
const { t } = i18n
|
|
28
|
+
let name = $state('')
|
|
29
|
+
let description = $state('')
|
|
30
|
+
let version = $state('')
|
|
31
|
+
let body = $state('')
|
|
32
|
+
let saving = $state(false)
|
|
33
|
+
|
|
34
|
+
$effect.pre(() => {
|
|
35
|
+
if (!open) return
|
|
36
|
+
name = skill?.name ?? ''
|
|
37
|
+
description = skill?.description ?? ''
|
|
38
|
+
version = skill?.version ?? ''
|
|
39
|
+
body = skill?.body ?? ''
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
async function save() {
|
|
43
|
+
const input: ManualSave = {
|
|
44
|
+
name: name.trim(),
|
|
45
|
+
description: description.trim(),
|
|
46
|
+
version: version.trim() || null,
|
|
47
|
+
body,
|
|
48
|
+
}
|
|
49
|
+
if (skill) Object.assign(input, { id: skill.id, expectedRevision: skill.revision })
|
|
50
|
+
saving = true
|
|
51
|
+
try {
|
|
52
|
+
onsaved(await port.save(input))
|
|
53
|
+
open = false
|
|
54
|
+
} catch (error) {
|
|
55
|
+
notify.toast('error', errorText(t, error))
|
|
56
|
+
} finally {
|
|
57
|
+
saving = false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<Dialog.Root bind:open>
|
|
63
|
+
<Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
|
|
64
|
+
<Dialog.Header>
|
|
65
|
+
<Dialog.Title>{skill ? t('skills.edit_title', { name: skill.name }) : t('skills.new')}</Dialog.Title>
|
|
66
|
+
</Dialog.Header>
|
|
67
|
+
<Field.Group class="min-h-0 overflow-y-auto">
|
|
68
|
+
<div class="flex gap-3">
|
|
69
|
+
<Field.Field class="flex-1">
|
|
70
|
+
<Field.Label for="kit-skill-name">{t('skills.name')}</Field.Label>
|
|
71
|
+
<Input id="kit-skill-name" class="font-mono" maxlength={64} bind:value={name} />
|
|
72
|
+
</Field.Field>
|
|
73
|
+
<Field.Field class="w-32">
|
|
74
|
+
<Field.Label for="kit-skill-version">{t('skills.version')}</Field.Label>
|
|
75
|
+
<Input id="kit-skill-version" maxlength={32} placeholder={t('skills.unversioned')} bind:value={version} />
|
|
76
|
+
</Field.Field>
|
|
77
|
+
</div>
|
|
78
|
+
<Field.Field>
|
|
79
|
+
<Field.Label for="kit-skill-description">{t('skills.description')}</Field.Label>
|
|
80
|
+
<Input id="kit-skill-description" maxlength={500} bind:value={description} />
|
|
81
|
+
</Field.Field>
|
|
82
|
+
<Field.Field>
|
|
83
|
+
<Field.Label for="kit-skill-body">{t('skills.body')}</Field.Label>
|
|
84
|
+
<Textarea id="kit-skill-body" class="min-h-64 font-mono text-xs" bind:value={body} />
|
|
85
|
+
</Field.Field>
|
|
86
|
+
</Field.Group>
|
|
87
|
+
<Dialog.Footer>
|
|
88
|
+
<Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
|
|
89
|
+
<Button disabled={saving || !name.trim() || !description.trim() || !body.trim()} onclick={() => void save()}>
|
|
90
|
+
{t('skills.save')}
|
|
91
|
+
</Button>
|
|
92
|
+
</Dialog.Footer>
|
|
93
|
+
</Dialog.Content>
|
|
94
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Full text and packaged files of a skill before it is installed or used (docs/16 §4.3, docs/14 §4).
|
|
3
|
+
// Without `onconfirm` it is a read-only viewer.
|
|
4
|
+
import * as Alert from '#lib/components/ui/alert/index.js'
|
|
5
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
6
|
+
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
7
|
+
import { getKitContext } from '../context'
|
|
8
|
+
import type { Preview, PreviewCurrent } from '../ports'
|
|
9
|
+
|
|
10
|
+
/** What both a Preview and a stored SkillDto carry. */
|
|
11
|
+
type SkillText = Pick<Preview, 'name' | 'description' | 'version' | 'body' | 'license' | 'extra' | 'files'>
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
open = $bindable(false),
|
|
15
|
+
skill,
|
|
16
|
+
title,
|
|
17
|
+
current = null,
|
|
18
|
+
confirmLabel,
|
|
19
|
+
busy = false,
|
|
20
|
+
error = null,
|
|
21
|
+
origin,
|
|
22
|
+
ignored = [],
|
|
23
|
+
onconfirm,
|
|
24
|
+
}: {
|
|
25
|
+
open: boolean
|
|
26
|
+
skill: SkillText | null
|
|
27
|
+
title: string
|
|
28
|
+
/** The installed skill a confirm would replace; confirming then needs the "replace" box. */
|
|
29
|
+
current?: PreviewCurrent | null
|
|
30
|
+
confirmLabel?: string
|
|
31
|
+
busy?: boolean
|
|
32
|
+
error?: string | null
|
|
33
|
+
origin?: string
|
|
34
|
+
ignored?: readonly string[]
|
|
35
|
+
onconfirm?: (replace: boolean) => void
|
|
36
|
+
} = $props()
|
|
37
|
+
|
|
38
|
+
const { t } = getKitContext().i18n
|
|
39
|
+
let replace = $state(false)
|
|
40
|
+
$effect.pre(() => {
|
|
41
|
+
void skill
|
|
42
|
+
if (open) replace = false
|
|
43
|
+
})
|
|
44
|
+
const extra = $derived(Object.entries(skill?.extra ?? {}))
|
|
45
|
+
const files = $derived(skill?.files ?? [])
|
|
46
|
+
const hasScripts = $derived(files.some((f) => f.kind === 'script'))
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<Dialog.Root bind:open>
|
|
50
|
+
<Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
|
|
51
|
+
<Dialog.Header>
|
|
52
|
+
<Dialog.Title>{title}</Dialog.Title>
|
|
53
|
+
<Dialog.Description>{t('skills.preview_desc')}</Dialog.Description>
|
|
54
|
+
</Dialog.Header>
|
|
55
|
+
{#if origin}<p class="text-muted-foreground break-all text-xs">{origin}</p>{/if}
|
|
56
|
+
{#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
|
|
57
|
+
{#if skill}
|
|
58
|
+
<dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
|
|
59
|
+
<dt class="text-muted-foreground">{t('skills.name')}</dt>
|
|
60
|
+
<dd class="font-mono">{skill.name}</dd>
|
|
61
|
+
<dt class="text-muted-foreground">{t('skills.version')}</dt>
|
|
62
|
+
<dd>{skill.version ?? t('skills.unversioned')}</dd>
|
|
63
|
+
<dt class="text-muted-foreground">{t('skills.description')}</dt>
|
|
64
|
+
<dd>{skill.description}</dd>
|
|
65
|
+
{#each extra as [key, value] (key)}
|
|
66
|
+
<dt class="text-muted-foreground font-mono">{key}</dt>
|
|
67
|
+
<dd>{value}</dd>
|
|
68
|
+
{/each}
|
|
69
|
+
</dl>
|
|
70
|
+
<pre
|
|
71
|
+
class="bg-muted min-h-0 flex-1 overflow-y-auto rounded-md p-3 text-xs whitespace-pre-wrap"
|
|
72
|
+
aria-label={t('skills.body')}>{skill.body}</pre>
|
|
73
|
+
{#if skill.license}
|
|
74
|
+
<details class="text-xs">
|
|
75
|
+
<summary class="cursor-pointer">{t('skills.license')}</summary>
|
|
76
|
+
<pre class="max-h-40 overflow-y-auto whitespace-pre-wrap">{skill.license}</pre>
|
|
77
|
+
</details>
|
|
78
|
+
{/if}
|
|
79
|
+
{#if hasScripts}
|
|
80
|
+
<Alert.Root>
|
|
81
|
+
<Alert.Description>{t('skills.scripts_notice')}</Alert.Description>
|
|
82
|
+
</Alert.Root>
|
|
83
|
+
{/if}
|
|
84
|
+
{#if files.length > 0}
|
|
85
|
+
<section class="flex flex-col gap-1 text-xs" aria-label={t('skills.files', { count: files.length })}>
|
|
86
|
+
<h3 class="font-medium">{t('skills.files', { count: files.length })}</h3>
|
|
87
|
+
<ul class="max-h-40 overflow-y-auto font-mono">
|
|
88
|
+
{#each files as file (file.path)}
|
|
89
|
+
<li class="flex gap-3">
|
|
90
|
+
<span class="min-w-0 flex-1 truncate">{file.path}</span>
|
|
91
|
+
<span class="text-muted-foreground">{t(`skills.file_kind.${file.kind}`)}</span>
|
|
92
|
+
<span class="text-muted-foreground">{t('skills.file_size', { size: file.size })}</span>
|
|
93
|
+
</li>
|
|
94
|
+
{/each}
|
|
95
|
+
</ul>
|
|
96
|
+
</section>
|
|
97
|
+
{/if}
|
|
98
|
+
{#if ignored.length > 0}
|
|
99
|
+
<section class="flex flex-col gap-1 text-xs" aria-label={t('skills.add.ignored')}>
|
|
100
|
+
<h3 class="font-medium">{t('skills.add.ignored')}</h3>
|
|
101
|
+
<ul class="max-h-32 overflow-y-auto break-all font-mono">
|
|
102
|
+
{#each ignored as path (path)}<li>{path}</li>{/each}
|
|
103
|
+
</ul>
|
|
104
|
+
</section>
|
|
105
|
+
{/if}
|
|
106
|
+
{#if onconfirm && current}
|
|
107
|
+
<label class="flex items-center gap-2 text-sm">
|
|
108
|
+
<input type="checkbox" class="accent-primary" bind:checked={replace} />
|
|
109
|
+
{t('skills.replace', { version: current.version ?? t('skills.unversioned'), revision: current.revision })}
|
|
110
|
+
</label>
|
|
111
|
+
{/if}
|
|
112
|
+
{/if}
|
|
113
|
+
<Dialog.Footer>
|
|
114
|
+
<Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
|
|
115
|
+
{#if onconfirm}
|
|
116
|
+
<Button disabled={busy || !skill || (current !== null && !replace)} onclick={() => onconfirm(replace)}>
|
|
117
|
+
{confirmLabel ?? t('skills.install')}
|
|
118
|
+
</Button>
|
|
119
|
+
{/if}
|
|
120
|
+
</Dialog.Footer>
|
|
121
|
+
</Dialog.Content>
|
|
122
|
+
</Dialog.Root>
|