@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,556 @@
1
+ // One conversation in the panel: turns, the active run and its abort (docs/16 §4.2, §7 trap 4).
2
+ import { SvelteMap } from 'svelte/reactivity'
3
+ import { isKitClientError } from '../ports'
4
+ import type {
5
+ RunClient,
6
+ ConversationClient,
7
+ ContextRef,
8
+ ImagePart,
9
+ ImageRef,
10
+ KitError,
11
+ ModelRef,
12
+ RunEvent,
13
+ RunRequest,
14
+ SearchHit,
15
+ SkillRef,
16
+ ToolOutcome,
17
+ Turn,
18
+ TurnStatus,
19
+ } from '../ports'
20
+
21
+ /** One tool call of a turn, as shown by ToolCallItem / ApprovalCard. */
22
+ export interface ToolItem {
23
+ callId: string
24
+ name: string
25
+ /** MCP server name; absent for host tools. */
26
+ server: string | null
27
+ /** Arguments preview while the call waits for the user. */
28
+ pending: string | null
29
+ outcome: ToolOutcome | null
30
+ summary: string
31
+ }
32
+
33
+ import type { ContextUsage, Cost, TokenUsage, ReasoningEffort, PermissionMode } from '../ports'
34
+
35
+ export interface ChatTurn {
36
+ id: string
37
+ persisted: boolean
38
+ parentId: string | null
39
+ questionId: string
40
+ supersedesId: string | null
41
+ kind: 'reply' | 'translation'
42
+ endpointId: string | null
43
+ modelId: string | null
44
+ input: string
45
+ images: ImageRef[]
46
+ /** Images `generate_image` produced in this reply (docs/20). */
47
+ generated: ImageRef[]
48
+ text: string
49
+ reasoningText: string
50
+ reasoningEffort: ReasoningEffort
51
+ usage: TokenUsage | null
52
+ /** Priced at run time; later price changes never recompute it (docs/41 §3.7). */
53
+ cost: Cost | null
54
+ contextUsage: ContextUsage | null
55
+ status: TurnStatus
56
+ error: string | null
57
+ tools: ToolItem[]
58
+ exhausted: boolean
59
+ search: SearchState
60
+ }
61
+
62
+ export interface SearchState {
63
+ status: 'idle' | 'searching' | 'complete' | 'failed'
64
+ query: string | null
65
+ provider: string | null
66
+ hits: SearchHit[]
67
+ error: string | null
68
+ }
69
+
70
+ export interface RunBase {
71
+ purpose: string
72
+ scope: string | null
73
+ language: string
74
+ model: ModelRef | null
75
+ skills: SkillRef[]
76
+ context: ContextRef | null
77
+ reasoningEffort?: ReasoningEffort
78
+ /** Offer the `generate_image` tool (docs/20). */
79
+ generateImage?: boolean
80
+ /** Permission mode of the run (docs/16 §4.2); omitted is `default`. */
81
+ permissionMode?: PermissionMode
82
+ }
83
+
84
+ export class RunSession {
85
+ turns = $state<ChatTurn[]>([])
86
+ conversationId = $state<string | null>(null)
87
+ selectedTurnId = $state<string | null>(null)
88
+ actionError = $state<string | null>(null)
89
+ historyRefreshError = $state<string | null>(null)
90
+ running = $state(false)
91
+ contextUsage = $state<ContextUsage | null>(null)
92
+ search = $state<SearchState>({ status: 'idle', query: null, provider: null, hits: [], error: null })
93
+ /** Id of a selected skill the backend rejected as `skill_changed`; the user has to confirm it again. */
94
+ skillChanged = $state<string | null>(null)
95
+ #abort: AbortController | null = null
96
+ /** Id of the active run, known once `chat_run` resolves. */
97
+ #runId: string | null = null
98
+ readonly #port: RunClient
99
+ readonly #conversations: ConversationClient | undefined
100
+ #ended = false
101
+
102
+ constructor(port: RunClient, conversations?: ConversationClient) {
103
+ this.#port = port
104
+ this.#conversations = conversations
105
+ }
106
+ resetContext() {
107
+ this.contextUsage = null
108
+ }
109
+
110
+ get cumulativeUsage(): TokenUsage {
111
+ const total: TokenUsage = {
112
+ inputTokens: null,
113
+ outputTokens: null,
114
+ cacheReadTokens: null,
115
+ cacheWriteTokens: null,
116
+ reasoningTokens: null,
117
+ }
118
+ for (const turn of this.turns)
119
+ for (const key of Object.keys(total) as (keyof TokenUsage)[]) {
120
+ const value = turn.usage?.[key]
121
+ if (value != null) total[key] = (total[key] ?? 0) + value
122
+ }
123
+ return total
124
+ }
125
+
126
+ /** Known turn costs summed per currency, same scope as `cumulativeUsage` (no exchange rates). */
127
+ get cumulativeCost(): Cost[] {
128
+ const totals: Cost[] = []
129
+ for (const { cost } of this.turns) {
130
+ if (!cost) continue
131
+ const found = totals.find((item) => item.currency === cost.currency)
132
+ if (found) found.amount += cost.amount
133
+ else totals.push({ ...cost })
134
+ }
135
+ return totals
136
+ }
137
+
138
+ send(base: RunBase, input: string, webSearch = false, images: ImagePart[] = []): Promise<boolean> {
139
+ if (this.running || input.trim() === '') return Promise.resolve(false)
140
+ return this.#append(base, input, this.selectedTurnId, null, 'reply', images.length ? { images } : {}, webSearch)
141
+ }
142
+
143
+ retry(base: RunBase, turnId: string, model?: ModelRef | null): Promise<boolean> {
144
+ if (base.purpose !== 'chat') {
145
+ this.actionError = 'bad_request'
146
+ return Promise.resolve(false)
147
+ }
148
+ const source = this.#source(turnId)
149
+ if (!source) return Promise.resolve(false)
150
+ const snapshot = {
151
+ ...base,
152
+ reasoningEffort: source.reasoningEffort,
153
+ model:
154
+ model ??
155
+ (source.endpointId && source.modelId
156
+ ? { endpointId: source.endpointId, modelId: source.modelId }
157
+ : source.persisted
158
+ ? null
159
+ : base.model),
160
+ }
161
+ const storedSource = this.#stored && source.persisted
162
+ // Without stored history the host cannot read the source images back, so resend the local data URLs.
163
+ const images = source.images.flatMap((image) =>
164
+ image.url?.startsWith('data:') ? [{ mime: image.mime, data: image.url.slice(image.url.indexOf(',') + 1) }] : [],
165
+ )
166
+ return this.#append(
167
+ snapshot,
168
+ source.input,
169
+ source.parentId,
170
+ this.#stored && !source.persisted ? null : source,
171
+ source.kind,
172
+ storedSource ? { retryTurnId: source.id } : images.length ? { images } : {},
173
+ )
174
+ }
175
+
176
+ translate(base: RunBase, turnId: string, language: string): Promise<boolean> {
177
+ if (base.purpose !== 'chat') {
178
+ this.actionError = 'bad_request'
179
+ return Promise.resolve(false)
180
+ }
181
+ const source = this.#source(turnId)
182
+ if (!source) return Promise.resolve(false)
183
+ if (!language.trim() || language.trim().length > 80) {
184
+ this.actionError = 'bad_request'
185
+ return Promise.resolve(false)
186
+ }
187
+ const input = `Translate the following reply into ${language.trim()}. Return only the translation, preserving Markdown formatting.\n\n${source.text}`
188
+ const snapshot = {
189
+ ...base,
190
+ reasoningEffort: source.reasoningEffort,
191
+ model:
192
+ base.model ??
193
+ (source.endpointId && source.modelId ? { endpointId: source.endpointId, modelId: source.modelId } : null),
194
+ }
195
+ return this.#append(
196
+ snapshot,
197
+ input,
198
+ source.id,
199
+ null,
200
+ 'translation',
201
+ this.#stored && source.persisted ? { translateTurnId: source.id, targetLanguage: language.trim() } : {},
202
+ )
203
+ }
204
+
205
+ get #stored() {
206
+ return this.#conversations !== undefined && this.conversationId !== null
207
+ }
208
+
209
+ #source(id: string) {
210
+ if (this.running) return null
211
+ const source = this.turns.find((turn) => turn.id === id)
212
+ if (!source) this.actionError = 'not_found'
213
+ return source ?? null
214
+ }
215
+
216
+ #append(
217
+ base: RunBase,
218
+ input: string,
219
+ parentId: string | null,
220
+ source: ChatTurn | null,
221
+ kind: ChatTurn['kind'],
222
+ action: Partial<RunRequest>,
223
+ webSearch = false,
224
+ ): Promise<boolean> {
225
+ const id = crypto.randomUUID()
226
+ this.actionError = null
227
+ this.historyRefreshError = null
228
+ this.turns.push({
229
+ id,
230
+ persisted: false,
231
+ parentId,
232
+ questionId: source?.questionId ?? id,
233
+ supersedesId: source?.id ?? null,
234
+ kind,
235
+ endpointId: base.model?.endpointId ?? null,
236
+ modelId: base.model?.modelId ?? null,
237
+ input,
238
+ images:
239
+ action.images?.map((image) => ({ mime: image.mime, url: `data:${image.mime};base64,${image.data}` })) ??
240
+ source?.images ??
241
+ [],
242
+ generated: [],
243
+ text: '',
244
+ reasoningText: '',
245
+ reasoningEffort: base.reasoningEffort ?? 'default',
246
+ usage: null,
247
+ cost: null,
248
+ contextUsage: null,
249
+ status: 'running',
250
+ error: null,
251
+ tools: [],
252
+ exhausted: false,
253
+ search: { status: 'idle', query: null, provider: null, hits: [], error: null },
254
+ })
255
+ return this.#start(base, input, action, webSearch)
256
+ }
257
+
258
+ /** Cancels the active run; its `end` arrives as `cancelled`. */
259
+ stop() {
260
+ this.#abort?.abort()
261
+ for (const turn of this.turns) for (const tool of turn.tools) tool.pending = null
262
+ if (this.#runId && !this.#ended) {
263
+ const abort = this.#abort
264
+ void this.#port.cancel(this.#runId).catch((error: unknown) => {
265
+ if (this.#abort === abort && this.running) this.actionError = isKitClientError(error) ? error.code : 'internal'
266
+ })
267
+ }
268
+ }
269
+
270
+ /** Invalidates callbacks before the owning view is destroyed. */
271
+ dispose() {
272
+ this.stop()
273
+ this.#abort = null
274
+ this.#runId = null
275
+ this.running = false
276
+ }
277
+
278
+ /** Answers an `approval_required` call of the active run. */
279
+ approve(tool: ToolItem, allow: boolean) {
280
+ const runId = this.#runId
281
+ const abort = this.#abort
282
+ if (tool.pending === null || runId === null || this.#ended || abort?.signal.aborted) return
283
+ const preview = tool.pending
284
+ tool.pending = null
285
+ this.#port.approve(runId, tool.callId, allow).catch((error: unknown) => {
286
+ if (this.#abort !== abort || this.#ended || abort?.signal.aborted || !this.running) return
287
+ this.actionError = isKitClientError(error) ? error.code : 'internal'
288
+ if (tool.outcome === null) tool.pending = preview
289
+ })
290
+ }
291
+
292
+ show(conversationId: string | null, turns: readonly Turn[] = [], selectedTurnId?: string | null) {
293
+ const previous =
294
+ conversationId === this.conversationId ? new SvelteMap(this.turns.map((turn) => [turn.id, turn])) : null
295
+ this.stop()
296
+ this.#abort = null
297
+ this.#runId = null
298
+ this.running = false
299
+ this.actionError = null
300
+ this.historyRefreshError = null
301
+ this.conversationId = conversationId
302
+ this.selectedTurnId =
303
+ selectedTurnId === undefined
304
+ ? (turns.findLast((turn) => turn.status === 'completed')?.id ?? null)
305
+ : selectedTurnId
306
+ this.contextUsage = turns.find((turn) => turn.id === this.selectedTurnId)?.contextUsage ?? null
307
+ this.search = { status: 'idle', query: null, provider: null, hits: [], error: null }
308
+ this.turns = turns.map((turn) => {
309
+ const restored = this.#fromHistory(turn)
310
+ const current = previous?.get(turn.id)
311
+ if (
312
+ current &&
313
+ current.text === restored.text &&
314
+ current.reasoningText === restored.reasoningText &&
315
+ (current.contextUsage === null) === (restored.contextUsage === null)
316
+ ) {
317
+ restored.tools = current.tools
318
+ restored.search = current.search
319
+ restored.exhausted = current.exhausted
320
+ }
321
+ return restored
322
+ })
323
+ }
324
+
325
+ #fromHistory(turn: Turn): ChatTurn {
326
+ return {
327
+ id: turn.id,
328
+ persisted: true,
329
+ parentId: turn.parentId,
330
+ questionId: turn.questionId,
331
+ supersedesId: turn.supersedesId,
332
+ kind: turn.kind,
333
+ endpointId: turn.endpointId,
334
+ modelId: turn.modelId,
335
+ input: turn.input,
336
+ images: turn.images,
337
+ generated: turn.generated,
338
+ text: turn.text,
339
+ reasoningText: turn.reasoningText ?? '',
340
+ reasoningEffort: turn.reasoningEffort ?? 'default',
341
+ usage: turn.usage ?? null,
342
+ cost: turn.cost ?? null,
343
+ contextUsage: turn.contextUsage ?? null,
344
+ status: turn.status,
345
+ error: turn.errorCode,
346
+ tools: [],
347
+ exhausted: false,
348
+ search: { status: 'idle', query: null, provider: null, hits: [], error: null },
349
+ }
350
+ }
351
+
352
+ async #start(base: RunBase, input: string, action: Partial<RunRequest>, webSearch: boolean): Promise<boolean> {
353
+ // The proxied element, so streamed writes are reactive.
354
+ const turn = this.turns[this.turns.length - 1]
355
+ if (!turn) return Promise.resolve(false)
356
+ const abort = new AbortController()
357
+ this.#abort = abort
358
+ this.#ended = false
359
+ this.contextUsage = null
360
+ this.search = {
361
+ status: webSearch ? 'searching' : 'idle',
362
+ query: webSearch ? input : null,
363
+ provider: null,
364
+ hits: [],
365
+ error: null,
366
+ }
367
+ turn.search = this.search
368
+ this.running = true
369
+ let accepted = false
370
+ let ended = false
371
+ let refreshing = false
372
+ const settle = async () => {
373
+ if (!accepted || !ended || refreshing || this.#abort !== abort) return
374
+ refreshing = true
375
+ try {
376
+ if (this.#conversations && this.conversationId) {
377
+ const conversation = await this.#conversations.get(this.conversationId)
378
+ if (this.#abort !== abort) return
379
+ const live = new SvelteMap(this.turns.map((item) => [item.id, item]))
380
+ this.turns = conversation.turns.map((stored) => {
381
+ const current = live.get(stored.id)
382
+ live.delete(stored.id)
383
+ return {
384
+ ...this.#fromHistory(stored),
385
+ tools: current?.tools ?? [],
386
+ search: current?.search ?? { status: 'idle', query: null, provider: null, hits: [], error: null },
387
+ exhausted: current?.exhausted ?? false,
388
+ }
389
+ })
390
+ this.turns.push(...live.values())
391
+ this.selectedTurnId = conversation.summary.selectedTurnId
392
+ this.contextUsage = this.turns.find((item) => item.id === this.selectedTurnId)?.contextUsage ?? null
393
+ }
394
+ } catch (error: unknown) {
395
+ if (this.#abort !== abort) return
396
+ this.historyRefreshError = error instanceof Error ? error.message : 'internal'
397
+ } finally {
398
+ if (this.#abort === abort) {
399
+ this.#abort = null
400
+ this.#runId = null
401
+ this.running = false
402
+ }
403
+ }
404
+ }
405
+ const finish = (status: TurnStatus, error: string | null) => {
406
+ if (this.#abort !== abort) return
407
+ ended = true
408
+ this.#ended = true
409
+ turn.status = status
410
+ turn.error = error
411
+ if (status !== 'completed') {
412
+ turn.contextUsage = null
413
+ this.contextUsage = null
414
+ } else if (turn.supersedesId === null) this.selectedTurnId = turn.id
415
+ if (this.search.status === 'searching') {
416
+ this.search = { ...this.search, status: status === 'failed' ? 'failed' : 'idle', error }
417
+ turn.search = this.search
418
+ }
419
+ void settle()
420
+ }
421
+ const onEvent = (event: RunEvent) => {
422
+ if (this.#abort !== abort || ended) return
423
+ const tool = (callId: string) => turn.tools.find((x) => x.callId === callId)
424
+ switch (event.kind) {
425
+ case 'search_start':
426
+ this.search.status = 'searching'
427
+ break
428
+ case 'search_result':
429
+ this.search = {
430
+ status: 'complete',
431
+ query: event.query,
432
+ provider: event.provider,
433
+ hits: event.hits,
434
+ error: null,
435
+ }
436
+ turn.search = this.search
437
+ break
438
+ case 'text_delta':
439
+ turn.text += event.text
440
+ break
441
+ case 'reasoning_delta':
442
+ turn.reasoningText += event.text
443
+ break
444
+ case 'context':
445
+ turn.contextUsage = event.usage
446
+ this.contextUsage = event.usage
447
+ break
448
+ case 'usage':
449
+ turn.usage = {
450
+ inputTokens: event.inputTokens,
451
+ outputTokens: event.outputTokens,
452
+ cacheReadTokens: event.cacheReadTokens,
453
+ cacheWriteTokens: event.cacheWriteTokens,
454
+ reasoningTokens: event.reasoningTokens,
455
+ }
456
+ turn.cost = event.cost
457
+ break
458
+ case 'tool_call':
459
+ // A repeated call id is fed back without running again (docs/12 §5): one row.
460
+ if (!tool(event.callId)) {
461
+ const server = event.server ?? null
462
+ turn.tools.push({
463
+ callId: event.callId,
464
+ name: event.name,
465
+ server,
466
+ pending: null,
467
+ outcome: null,
468
+ summary: '',
469
+ })
470
+ }
471
+ break
472
+ case 'approval_required': {
473
+ const item = tool(event.callId)
474
+ if (item) item.pending = event.argsPreview
475
+ break
476
+ }
477
+ case 'tool_result': {
478
+ const item = tool(event.callId)
479
+ if (item) Object.assign(item, { pending: null, outcome: event.outcome, summary: event.summary })
480
+ const painted = (event.payload as { images?: ImageRef[] } | undefined)?.images
481
+ if (event.outcome === 'ok' && Array.isArray(painted)) turn.generated.push(...painted)
482
+ break
483
+ }
484
+ case 'end':
485
+ turn.exhausted = event.exhausted === true
486
+ // Calls still waiting when the run stops never ran.
487
+ for (const item of turn.tools) item.pending = null
488
+ this.#checkSkill(event.error)
489
+ finish(event.status, event.error?.code ?? null)
490
+ break
491
+ case 'round_start':
492
+ turn.contextUsage = null
493
+ this.contextUsage = null
494
+ break
495
+ }
496
+ }
497
+ const request: RunRequest = {
498
+ purpose: base.purpose,
499
+ model: base.model,
500
+ skills: base.skills,
501
+ context: base.context,
502
+ input,
503
+ conversationId: this.conversationId,
504
+ scope: base.scope,
505
+ regenerate: false,
506
+ language: base.language,
507
+ reasoningEffort: base.reasoningEffort ?? 'default',
508
+ permissionMode: base.permissionMode ?? 'default',
509
+ ...(base.generateImage && !action.translateTurnId ? { generateImage: true } : {}),
510
+ ...(base.purpose === 'chat' && this.#stored && !action.retryTurnId && !action.translateTurnId
511
+ ? { replyToId: this.selectedTurnId }
512
+ : {}),
513
+ ...action,
514
+ ...(base.purpose === 'chat' && webSearch ? { webSearch: true } : {}),
515
+ }
516
+ this.#runId = null
517
+ try {
518
+ const result = await this.#port.start(request, onEvent, { signal: abort.signal })
519
+ if (this.#abort !== abort) {
520
+ if (!ended) void this.#port.cancel(result.runId).catch(() => undefined)
521
+ return true
522
+ }
523
+ if (abort.signal.aborted && !ended) void this.#port.cancel(result.runId).catch(() => undefined)
524
+ accepted = true
525
+ this.#runId = result.runId
526
+ if (result.conversationId) this.conversationId = result.conversationId
527
+ if (result.turnId) {
528
+ const temporaryId = turn.id
529
+ turn.id = result.turnId
530
+ turn.persisted = true
531
+ if (turn.questionId === temporaryId) turn.questionId = result.turnId
532
+ if (this.selectedTurnId === temporaryId) this.selectedTurnId = result.turnId
533
+ }
534
+ void settle()
535
+ return true
536
+ } catch (error: unknown) {
537
+ if (this.#abort !== abort) return true
538
+ const code = isKitClientError(error) ? error.code : 'internal'
539
+ if (isKitClientError(error)) this.#checkSkill(error)
540
+ finish(code === 'cancelled' ? 'cancelled' : 'failed', code === 'cancelled' ? null : code)
541
+ if ((turn.supersedesId !== null || turn.kind === 'translation') && code !== 'cancelled') this.actionError = code
542
+ if (turn.supersedesId !== null || turn.kind === 'translation')
543
+ this.turns = this.turns.filter((item) => item !== turn)
544
+ this.#abort = null
545
+ this.#runId = null
546
+ this.running = false
547
+ return code === 'cancelled'
548
+ }
549
+ }
550
+
551
+ #checkSkill(error: Pick<KitError, 'code' | 'details'> | null | undefined) {
552
+ if (error?.code === 'conflict' && error.details?.reason === 'skill_changed') {
553
+ this.skillChanged = error.details.entity ?? ''
554
+ }
555
+ }
556
+ }
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "#lib/utils.js";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="alert-action"
16
+ class={cn("absolute top-2 right-2", className)}
17
+ {...restProps}
18
+ >
19
+ {@render children?.()}
20
+ </div>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "#lib/utils.js";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="alert-description"
16
+ class={cn(
17
+ "text-muted-foreground text-kit-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
18
+ className
19
+ )}
20
+ {...restProps}
21
+ >
22
+ {@render children?.()}
23
+ </div>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from "#lib/utils.js";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ let {
6
+ ref = $bindable(null),
7
+ class: className,
8
+ children,
9
+ ...restProps
10
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
11
+ </script>
12
+
13
+ <div
14
+ bind:this={ref}
15
+ data-slot="alert-title"
16
+ class={cn(
17
+ "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
18
+ className
19
+ )}
20
+ {...restProps}
21
+ >
22
+ {@render children?.()}
23
+ </div>
@@ -0,0 +1,43 @@
1
+ <script lang="ts" module>
2
+ import { type VariantProps, tv } from "#lib/utils.js";
3
+
4
+ export const alertVariants = tv({
5
+ base: "grid gap-0.5 rounded-kit border px-2.5 py-2 text-left text-kit-base has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 group/alert relative w-full",
6
+ variants: {
7
+ variant: {
8
+ default: "bg-card text-card-foreground",
9
+ destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
10
+ },
11
+ },
12
+ defaultVariants: {
13
+ variant: "default",
14
+ },
15
+ });
16
+
17
+ export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ import { cn, type WithElementRef } from "#lib/utils.js";
22
+ import type { HTMLAttributes } from "svelte/elements";
23
+
24
+ let {
25
+ ref = $bindable(null),
26
+ class: className,
27
+ variant = "default",
28
+ children,
29
+ ...restProps
30
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
31
+ variant?: AlertVariant;
32
+ } = $props();
33
+ </script>
34
+
35
+ <div
36
+ bind:this={ref}
37
+ data-slot="alert"
38
+ role="alert"
39
+ class={cn(alertVariants({ variant }), className)}
40
+ {...restProps}
41
+ >
42
+ {@render children?.()}
43
+ </div>
@@ -0,0 +1,17 @@
1
+ import Action from "./alert-action.svelte";
2
+ import Description from "./alert-description.svelte";
3
+ import Title from "./alert-title.svelte";
4
+ import Root from "./alert.svelte";
5
+ export { alertVariants, type AlertVariant } from "./alert.svelte";
6
+
7
+ export {
8
+ Root,
9
+ Description,
10
+ Title,
11
+ Action,
12
+ //
13
+ Root as Alert,
14
+ Description as AlertDescription,
15
+ Title as AlertTitle,
16
+ Action as AlertAction,
17
+ };