@ouraihub/aether-ui 0.0.0-stage → 0.1.1

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 (212) 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 +29 -0
  28. package/src/chat/ChatComposer.svelte +277 -0
  29. package/src/chat/ChatMessage.svelte +262 -0
  30. package/src/chat/ChatPanel.css +386 -0
  31. package/src/chat/ChatPanel.svelte +496 -0
  32. package/src/chat/ChatPanelApprovals.svelte +28 -0
  33. package/src/chat/ChatPanelComposer.svelte +317 -0
  34. package/src/chat/ChatPanelDialogs.svelte +242 -0
  35. package/src/chat/ChatPanelHeader.svelte +132 -0
  36. package/src/chat/ChatPanelMessages.svelte +166 -0
  37. package/src/chat/ChatPanelMode.svelte +190 -0
  38. package/src/chat/ChatPanelState.svelte.ts +1 -0
  39. package/src/chat/ChatPanelToolbar.svelte +425 -0
  40. package/src/chat/ChatSurface.svelte +168 -0
  41. package/src/chat/MessageItem.svelte +480 -0
  42. package/src/chat/ReasoningControl.svelte +286 -0
  43. package/src/chat/SkillPicker.svelte +133 -0
  44. package/src/chat/ToolCallItem.svelte +23 -0
  45. package/src/chat/UsageControl.svelte +216 -0
  46. package/src/chat/UsageDetails.svelte +76 -0
  47. package/src/chat/chat-attachments.svelte.ts +74 -0
  48. package/src/chat/chat-panel-history.svelte.ts +430 -0
  49. package/src/chat/chat-panel-props.ts +32 -0
  50. package/src/chat/index.ts +6 -0
  51. package/src/chat/run-session-history.ts +56 -0
  52. package/src/chat/run-session-types.ts +69 -0
  53. package/src/chat/run-session.svelte.ts +494 -0
  54. package/src/chat/test-support/chat-fixture.ts +178 -0
  55. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  56. package/src/chat/test-support/run-session-fixture.ts +112 -0
  57. package/src/components/ui/alert/alert-action.svelte +20 -0
  58. package/src/components/ui/alert/alert-description.svelte +23 -0
  59. package/src/components/ui/alert/alert-title.svelte +23 -0
  60. package/src/components/ui/alert/alert.svelte +43 -0
  61. package/src/components/ui/alert/index.ts +17 -0
  62. package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
  63. package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
  64. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
  65. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
  66. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
  67. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
  68. package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
  69. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
  70. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
  71. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
  72. package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
  73. package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
  74. package/src/components/ui/alert-dialog/index.ts +40 -0
  75. package/src/components/ui/button/button.svelte +81 -0
  76. package/src/components/ui/button/index.ts +17 -0
  77. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  78. package/src/components/ui/dialog/dialog-content.svelte +48 -0
  79. package/src/components/ui/dialog/dialog-description.svelte +17 -0
  80. package/src/components/ui/dialog/dialog-footer.svelte +32 -0
  81. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  82. package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
  83. package/src/components/ui/dialog/dialog-portal.svelte +7 -0
  84. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  85. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  86. package/src/components/ui/dialog/dialog.svelte +7 -0
  87. package/src/components/ui/dialog/index.ts +34 -0
  88. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  89. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
  90. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
  91. package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  92. package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  93. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  94. package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  95. package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  96. package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
  97. package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
  98. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  99. package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  100. package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
  101. package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  102. package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  103. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  104. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  105. package/src/components/ui/dropdown-menu/index.ts +54 -0
  106. package/src/components/ui/empty/empty-content.svelte +23 -0
  107. package/src/components/ui/empty/empty-description.svelte +23 -0
  108. package/src/components/ui/empty/empty-header.svelte +20 -0
  109. package/src/components/ui/empty/empty-media.svelte +41 -0
  110. package/src/components/ui/empty/empty-title.svelte +20 -0
  111. package/src/components/ui/empty/empty.svelte +23 -0
  112. package/src/components/ui/empty/index.ts +22 -0
  113. package/src/components/ui/field/field-content.svelte +20 -0
  114. package/src/components/ui/field/field-description.svelte +25 -0
  115. package/src/components/ui/field/field-error.svelte +58 -0
  116. package/src/components/ui/field/field-group.svelte +23 -0
  117. package/src/components/ui/field/field-label.svelte +25 -0
  118. package/src/components/ui/field/field-legend.svelte +24 -0
  119. package/src/components/ui/field/field-separator.svelte +35 -0
  120. package/src/components/ui/field/field-set.svelte +20 -0
  121. package/src/components/ui/field/field-title.svelte +20 -0
  122. package/src/components/ui/field/field.svelte +47 -0
  123. package/src/components/ui/field/index.ts +33 -0
  124. package/src/components/ui/input/index.ts +7 -0
  125. package/src/components/ui/input/input.svelte +48 -0
  126. package/src/components/ui/label/index.ts +7 -0
  127. package/src/components/ui/label/label.svelte +20 -0
  128. package/src/components/ui/select/index.ts +40 -0
  129. package/src/components/ui/select/select-content.svelte +45 -0
  130. package/src/components/ui/select/select-group-heading.svelte +21 -0
  131. package/src/components/ui/select/select-group.svelte +17 -0
  132. package/src/components/ui/select/select-item.svelte +43 -0
  133. package/src/components/ui/select/select-label.svelte +20 -0
  134. package/src/components/ui/select/select-portal.svelte +7 -0
  135. package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
  136. package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
  137. package/src/components/ui/select/select-separator.svelte +18 -0
  138. package/src/components/ui/select/select-trigger.svelte +29 -0
  139. package/src/components/ui/select/select-value.svelte +7 -0
  140. package/src/components/ui/select/select.svelte +11 -0
  141. package/src/components/ui/separator/index.ts +7 -0
  142. package/src/components/ui/separator/separator.svelte +21 -0
  143. package/src/components/ui/sonner/index.ts +1 -0
  144. package/src/components/ui/sonner/sonner.svelte +40 -0
  145. package/src/components/ui/switch/index.ts +7 -0
  146. package/src/components/ui/switch/switch.svelte +34 -0
  147. package/src/components/ui/textarea/index.ts +7 -0
  148. package/src/components/ui/textarea/textarea.svelte +22 -0
  149. package/src/context.ts +34 -0
  150. package/src/dev/Demo.svelte +47 -0
  151. package/src/dev/main.ts +7 -0
  152. package/src/i18n/en.ts +645 -0
  153. package/src/i18n/index.ts +28 -0
  154. package/src/i18n/zh-CN.ts +610 -0
  155. package/src/index.ts +10 -0
  156. package/src/logs/LogPanel.svelte +103 -0
  157. package/src/logs/LogSettings.svelte +58 -0
  158. package/src/logs/index.ts +5 -0
  159. package/src/logs/logger.ts +96 -0
  160. package/src/markdown.ts +8 -0
  161. package/src/mcp/McpClientEditor.svelte +204 -0
  162. package/src/mcp/McpClientSettings.svelte +418 -0
  163. package/src/mcp/McpServerSettings.svelte +363 -0
  164. package/src/mcp/config.ts +26 -0
  165. package/src/mcp/index.ts +3 -0
  166. package/src/mcp/input.ts +74 -0
  167. package/src/notify.svelte.ts +43 -0
  168. package/src/ports/contract.ts +174 -0
  169. package/src/ports/fake-optional.ts +81 -0
  170. package/src/ports/fake.ts +488 -0
  171. package/src/ports/index.ts +7 -0
  172. package/src/ports/search.ts +56 -0
  173. package/src/ports/skill-sources.ts +45 -0
  174. package/src/ports/test-support/fake-fixture.ts +63 -0
  175. package/src/providers/EndpointKeysDialog.svelte +357 -0
  176. package/src/providers/EndpointModels.svelte +311 -0
  177. package/src/providers/EndpointPanel.svelte +266 -0
  178. package/src/providers/ModelPickerDialog.svelte +230 -0
  179. package/src/providers/ModelPricing.svelte +125 -0
  180. package/src/providers/ModelSelect.svelte +448 -0
  181. package/src/providers/ModelSettingsDialog.svelte +388 -0
  182. package/src/providers/ProviderDialogs.svelte +120 -0
  183. package/src/providers/ProviderList.svelte +159 -0
  184. package/src/providers/ProvidersSettings.svelte +491 -0
  185. package/src/providers/RequestConfigDialog.svelte +278 -0
  186. package/src/providers/SecretInput.svelte +284 -0
  187. package/src/providers/draft.ts +355 -0
  188. package/src/providers/edit-session.ts +170 -0
  189. package/src/providers/index.ts +5 -0
  190. package/src/providers/reasoning.ts +55 -0
  191. package/src/providers/revision.svelte.ts +6 -0
  192. package/src/providers/test-support/providers-fixture.ts +120 -0
  193. package/src/search/SearchAdvancedTools.svelte +182 -0
  194. package/src/search/SearchProviderCard.svelte +195 -0
  195. package/src/search/SearchSettings.svelte +500 -0
  196. package/src/search/index.ts +13 -0
  197. package/src/search/settings-flow.ts +67 -0
  198. package/src/session.ts +20 -0
  199. package/src/skills/SkillAdd.svelte +380 -0
  200. package/src/skills/SkillAuthoring.svelte +430 -0
  201. package/src/skills/SkillEditor.svelte +104 -0
  202. package/src/skills/SkillPreviewDialog.svelte +136 -0
  203. package/src/skills/SkillsSettings.svelte +147 -0
  204. package/src/skills/error.ts +32 -0
  205. package/src/skills/index.ts +4 -0
  206. package/src/skills/test-support/skills-fixture.ts +139 -0
  207. package/src/styles/kit.css +137 -0
  208. package/src/styles/tokens.css +22 -0
  209. package/src/tauri/index.ts +298 -0
  210. package/src/utils.ts +22 -0
  211. package/src/web/index.ts +441 -0
  212. package/README.md +0 -3
@@ -0,0 +1,69 @@
1
+ import type {
2
+ ContextRef,
3
+ ContextUsage,
4
+ Cost,
5
+ ImageRef,
6
+ ModelRef,
7
+ PermissionMode,
8
+ ReasoningEffort,
9
+ SearchHit,
10
+ SkillRef,
11
+ TokenUsage,
12
+ ToolOutcome,
13
+ TurnStatus,
14
+ } from '../ports'
15
+
16
+ /** One tool call of a turn, as shown by ToolCallItem / ApprovalCard. */
17
+ export interface ToolItem {
18
+ callId: string
19
+ name: string
20
+ server: string | null
21
+ pending: string | null
22
+ outcome: ToolOutcome | null
23
+ summary: string
24
+ }
25
+
26
+ export interface ChatTurn {
27
+ id: string
28
+ persisted: boolean
29
+ parentId: string | null
30
+ questionId: string
31
+ supersedesId: string | null
32
+ kind: 'reply' | 'translation'
33
+ endpointId: string | null
34
+ modelId: string | null
35
+ input: string
36
+ images: ImageRef[]
37
+ generated: ImageRef[]
38
+ text: string
39
+ reasoningText: string
40
+ reasoningEffort: ReasoningEffort
41
+ usage: TokenUsage | null
42
+ cost: Cost | null
43
+ contextUsage: ContextUsage | null
44
+ status: TurnStatus
45
+ error: string | null
46
+ tools: ToolItem[]
47
+ exhausted: boolean
48
+ search: SearchState
49
+ }
50
+
51
+ export interface SearchState {
52
+ status: 'idle' | 'searching' | 'complete' | 'failed'
53
+ query: string | null
54
+ provider: string | null
55
+ hits: SearchHit[]
56
+ error: string | null
57
+ }
58
+
59
+ export interface RunBase {
60
+ purpose: string
61
+ scope: string | null
62
+ language: string
63
+ model: ModelRef | null
64
+ skills: SkillRef[]
65
+ context: ContextRef | null
66
+ reasoningEffort?: ReasoningEffort
67
+ generateImage?: boolean
68
+ permissionMode?: PermissionMode
69
+ }
@@ -0,0 +1,494 @@
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 type { ChatTurn, RunBase, SearchState, ToolItem } from './run-session-types'
4
+ import { cumulativeCost, cumulativeUsage, fromHistory } from './run-session-history'
5
+ import type {
6
+ ConversationClient,
7
+ ContextUsage,
8
+ Cost,
9
+ ImagePart,
10
+ ImageRef,
11
+ KitError,
12
+ ModelRef,
13
+ RunClient,
14
+ RunEvent,
15
+ RunRequest,
16
+ TokenUsage,
17
+ Turn,
18
+ TurnStatus,
19
+ } from '../ports'
20
+ import { isKitClientError } from '../ports'
21
+ export type { ChatTurn, RunBase, SearchState, ToolItem } from './run-session-types'
22
+ export class RunSession {
23
+ turns = $state<ChatTurn[]>([])
24
+ conversationId = $state<string | null>(null)
25
+ selectedTurnId = $state<string | null>(null)
26
+ actionError = $state<string | null>(null)
27
+ historyRefreshError = $state<string | null>(null)
28
+ running = $state(false)
29
+ contextUsage = $state<ContextUsage | null>(null)
30
+ search = $state<SearchState>({
31
+ status: 'idle',
32
+ query: null,
33
+ provider: null,
34
+ hits: [],
35
+ error: null,
36
+ })
37
+ /** Id of a selected skill the backend rejected as `skill_changed`; the user has to confirm it again. */
38
+ skillChanged = $state<string | null>(null)
39
+ #abort: AbortController | null = null
40
+ /** Id of the active run, known once `chat_run` resolves. */
41
+ #runId: string | null = null
42
+ readonly #port: RunClient
43
+ readonly #conversations: ConversationClient | undefined
44
+ #ended = false
45
+
46
+ constructor(port: RunClient, conversations?: ConversationClient) {
47
+ this.#port = port
48
+ this.#conversations = conversations
49
+ }
50
+ resetContext() {
51
+ this.contextUsage = null
52
+ }
53
+
54
+ get cumulativeUsage(): TokenUsage {
55
+ return cumulativeUsage(this.turns)
56
+ }
57
+
58
+ /** Known turn costs summed per currency, same scope as `cumulativeUsage` (no exchange rates). */
59
+ get cumulativeCost(): Cost[] {
60
+ return cumulativeCost(this.turns)
61
+ }
62
+
63
+ send(
64
+ base: RunBase,
65
+ input: string,
66
+ webSearch = false,
67
+ images: ImagePart[] = [],
68
+ ): Promise<boolean> {
69
+ if (this.running || input.trim() === '') return Promise.resolve(false)
70
+ return this.#append(
71
+ base,
72
+ input,
73
+ this.selectedTurnId,
74
+ null,
75
+ 'reply',
76
+ images.length ? { images } : {},
77
+ webSearch,
78
+ )
79
+ }
80
+
81
+ retry(base: RunBase, turnId: string, model?: ModelRef | null): Promise<boolean> {
82
+ if (base.purpose !== 'chat') {
83
+ this.actionError = 'bad_request'
84
+ return Promise.resolve(false)
85
+ }
86
+ const source = this.#source(turnId)
87
+ if (!source) return Promise.resolve(false)
88
+ const snapshot = {
89
+ ...base,
90
+ reasoningEffort: source.reasoningEffort,
91
+ model:
92
+ model ??
93
+ (source.endpointId && source.modelId
94
+ ? { endpointId: source.endpointId, modelId: source.modelId }
95
+ : source.persisted
96
+ ? null
97
+ : base.model),
98
+ }
99
+ const storedSource = this.#stored && source.persisted
100
+ // Without stored history the host cannot read the source images back, so resend the local data URLs.
101
+ const images = source.images.flatMap((image) =>
102
+ image.url?.startsWith('data:')
103
+ ? [{ mime: image.mime, data: image.url.slice(image.url.indexOf(',') + 1) }]
104
+ : [],
105
+ )
106
+ return this.#append(
107
+ snapshot,
108
+ source.input,
109
+ source.parentId,
110
+ this.#stored && !source.persisted ? null : source,
111
+ source.kind,
112
+ storedSource ? { retryTurnId: source.id } : images.length ? { images } : {},
113
+ )
114
+ }
115
+
116
+ translate(base: RunBase, turnId: string, language: string): Promise<boolean> {
117
+ if (base.purpose !== 'chat') {
118
+ this.actionError = 'bad_request'
119
+ return Promise.resolve(false)
120
+ }
121
+ const source = this.#source(turnId)
122
+ if (!source) return Promise.resolve(false)
123
+ if (!language.trim() || language.trim().length > 80) {
124
+ this.actionError = 'bad_request'
125
+ return Promise.resolve(false)
126
+ }
127
+ const input = `Translate the following reply into ${language.trim()}. Return only the translation, preserving Markdown formatting.\n\n${source.text}`
128
+ const snapshot = {
129
+ ...base,
130
+ reasoningEffort: source.reasoningEffort,
131
+ model:
132
+ base.model ??
133
+ (source.endpointId && source.modelId
134
+ ? { endpointId: source.endpointId, modelId: source.modelId }
135
+ : null),
136
+ }
137
+ return this.#append(
138
+ snapshot,
139
+ input,
140
+ source.id,
141
+ null,
142
+ 'translation',
143
+ this.#stored && source.persisted
144
+ ? { translateTurnId: source.id, targetLanguage: language.trim() }
145
+ : {},
146
+ )
147
+ }
148
+
149
+ get #stored() {
150
+ return this.#conversations !== undefined && this.conversationId !== null
151
+ }
152
+
153
+ #source(id: string) {
154
+ if (this.running) return null
155
+ const source = this.turns.find((turn) => turn.id === id)
156
+ if (!source) this.actionError = 'not_found'
157
+ return source ?? null
158
+ }
159
+
160
+ #append(
161
+ base: RunBase,
162
+ input: string,
163
+ parentId: string | null,
164
+ source: ChatTurn | null,
165
+ kind: ChatTurn['kind'],
166
+ action: Partial<RunRequest>,
167
+ webSearch = false,
168
+ ): Promise<boolean> {
169
+ const id = crypto.randomUUID()
170
+ this.actionError = null
171
+ this.historyRefreshError = null
172
+ this.turns.push({
173
+ id,
174
+ persisted: false,
175
+ parentId,
176
+ questionId: source?.questionId ?? id,
177
+ supersedesId: source?.id ?? null,
178
+ kind,
179
+ endpointId: base.model?.endpointId ?? null,
180
+ modelId: base.model?.modelId ?? null,
181
+ input,
182
+ images:
183
+ action.images?.map((image) => ({
184
+ mime: image.mime,
185
+ url: `data:${image.mime};base64,${image.data}`,
186
+ })) ??
187
+ source?.images ??
188
+ [],
189
+ generated: [],
190
+ text: '',
191
+ reasoningText: '',
192
+ reasoningEffort: base.reasoningEffort ?? 'default',
193
+ usage: null,
194
+ cost: null,
195
+ contextUsage: null,
196
+ status: 'running',
197
+ error: null,
198
+ tools: [],
199
+ exhausted: false,
200
+ search: { status: 'idle', query: null, provider: null, hits: [], error: null },
201
+ })
202
+ return this.#start(base, input, action, webSearch)
203
+ }
204
+
205
+ /** Cancels the active run; its `end` arrives as `cancelled`. */
206
+ stop() {
207
+ this.#abort?.abort()
208
+ for (const turn of this.turns) for (const tool of turn.tools) tool.pending = null
209
+ if (this.#runId && !this.#ended) {
210
+ const abort = this.#abort
211
+ void this.#port.cancel(this.#runId).catch((error: unknown) => {
212
+ if (this.#abort === abort && this.running)
213
+ this.actionError = isKitClientError(error) ? error.code : 'internal'
214
+ })
215
+ }
216
+ }
217
+
218
+ /** Invalidates callbacks before the owning view is destroyed. */
219
+ dispose() {
220
+ this.stop()
221
+ this.#abort = null
222
+ this.#runId = null
223
+ this.running = false
224
+ }
225
+
226
+ /** Answers an `approval_required` call of the active run. */
227
+ approve(tool: ToolItem, allow: boolean) {
228
+ const runId = this.#runId
229
+ const abort = this.#abort
230
+ if (tool.pending === null || runId === null || this.#ended || abort?.signal.aborted) return
231
+ const preview = tool.pending
232
+ tool.pending = null
233
+ this.#port.approve(runId, tool.callId, allow).catch((error: unknown) => {
234
+ if (this.#abort !== abort || this.#ended || abort?.signal.aborted || !this.running) return
235
+ this.actionError = isKitClientError(error) ? error.code : 'internal'
236
+ if (tool.outcome === null) tool.pending = preview
237
+ })
238
+ }
239
+
240
+ show(conversationId: string | null, turns: readonly Turn[] = [], selectedTurnId?: string | null) {
241
+ const previous =
242
+ conversationId === this.conversationId
243
+ ? new SvelteMap(this.turns.map((turn) => [turn.id, turn]))
244
+ : null
245
+ this.stop()
246
+ this.#abort = null
247
+ this.#runId = null
248
+ this.running = false
249
+ this.actionError = null
250
+ this.historyRefreshError = null
251
+ this.conversationId = conversationId
252
+ this.selectedTurnId =
253
+ selectedTurnId === undefined
254
+ ? (turns.findLast((turn) => turn.status === 'completed')?.id ?? null)
255
+ : selectedTurnId
256
+ this.contextUsage = turns.find((turn) => turn.id === this.selectedTurnId)?.contextUsage ?? null
257
+ this.search = { status: 'idle', query: null, provider: null, hits: [], error: null }
258
+ this.turns = turns.map((turn) => {
259
+ const restored = fromHistory(turn)
260
+ const current = previous?.get(turn.id)
261
+ if (
262
+ current &&
263
+ current.text === restored.text &&
264
+ current.reasoningText === restored.reasoningText &&
265
+ (current.contextUsage === null) === (restored.contextUsage === null)
266
+ ) {
267
+ restored.tools = current.tools
268
+ restored.search = current.search
269
+ restored.exhausted = current.exhausted
270
+ }
271
+ return restored
272
+ })
273
+ }
274
+
275
+ async #start(
276
+ base: RunBase,
277
+ input: string,
278
+ action: Partial<RunRequest>,
279
+ webSearch: boolean,
280
+ ): Promise<boolean> {
281
+ // The proxied element, so streamed writes are reactive.
282
+ const turn = this.turns[this.turns.length - 1]
283
+ if (!turn) return Promise.resolve(false)
284
+ const abort = new AbortController()
285
+ this.#abort = abort
286
+ this.#ended = false
287
+ this.contextUsage = null
288
+ this.search = {
289
+ status: webSearch ? 'searching' : 'idle',
290
+ query: webSearch ? input : null,
291
+ provider: null,
292
+ hits: [],
293
+ error: null,
294
+ }
295
+ turn.search = this.search
296
+ this.running = true
297
+ let accepted = false
298
+ let ended = false
299
+ let refreshing = false
300
+ const settle = async () => {
301
+ if (!accepted || !ended || refreshing || this.#abort !== abort) return
302
+ refreshing = true
303
+ try {
304
+ if (this.#conversations && this.conversationId) {
305
+ const conversation = await this.#conversations.get(this.conversationId)
306
+ if (this.#abort !== abort) return
307
+ const live = new SvelteMap(this.turns.map((item) => [item.id, item]))
308
+ this.turns = conversation.turns.map((stored) => {
309
+ const current = live.get(stored.id)
310
+ live.delete(stored.id)
311
+ return {
312
+ ...fromHistory(stored),
313
+ tools: current?.tools ?? [],
314
+ search: current?.search ?? {
315
+ status: 'idle',
316
+ query: null,
317
+ provider: null,
318
+ hits: [],
319
+ error: null,
320
+ },
321
+ exhausted: current?.exhausted ?? false,
322
+ }
323
+ })
324
+ this.turns.push(...live.values())
325
+ this.selectedTurnId = conversation.summary.selectedTurnId
326
+ this.contextUsage =
327
+ this.turns.find((item) => item.id === this.selectedTurnId)?.contextUsage ?? null
328
+ }
329
+ } catch (error: unknown) {
330
+ if (this.#abort !== abort) return
331
+ this.historyRefreshError = error instanceof Error ? error.message : 'internal'
332
+ } finally {
333
+ if (this.#abort === abort) {
334
+ this.#abort = null
335
+ this.#runId = null
336
+ this.running = false
337
+ }
338
+ }
339
+ }
340
+ const finish = (status: TurnStatus, error: string | null) => {
341
+ if (this.#abort !== abort) return
342
+ ended = true
343
+ this.#ended = true
344
+ turn.status = status
345
+ turn.error = error
346
+ if (status !== 'completed') {
347
+ turn.contextUsage = null
348
+ this.contextUsage = null
349
+ } else if (turn.supersedesId === null) this.selectedTurnId = turn.id
350
+ if (this.search.status === 'searching') {
351
+ this.search = { ...this.search, status: status === 'failed' ? 'failed' : 'idle', error }
352
+ turn.search = this.search
353
+ }
354
+ void settle()
355
+ }
356
+ const onEvent = (event: RunEvent) => {
357
+ if (this.#abort !== abort || ended) return
358
+ const tool = (callId: string) => turn.tools.find((x) => x.callId === callId)
359
+ switch (event.kind) {
360
+ case 'search_start':
361
+ this.search.status = 'searching'
362
+ break
363
+ case 'search_result':
364
+ this.search = {
365
+ status: 'complete',
366
+ query: event.query,
367
+ provider: event.provider,
368
+ hits: event.hits,
369
+ error: null,
370
+ }
371
+ turn.search = this.search
372
+ break
373
+ case 'text_delta':
374
+ turn.text += event.text
375
+ break
376
+ case 'reasoning_delta':
377
+ turn.reasoningText += event.text
378
+ break
379
+ case 'context':
380
+ turn.contextUsage = event.usage
381
+ this.contextUsage = event.usage
382
+ break
383
+ case 'usage':
384
+ turn.usage = {
385
+ inputTokens: event.inputTokens,
386
+ outputTokens: event.outputTokens,
387
+ cacheReadTokens: event.cacheReadTokens,
388
+ cacheWriteTokens: event.cacheWriteTokens,
389
+ reasoningTokens: event.reasoningTokens,
390
+ }
391
+ turn.cost = event.cost
392
+ break
393
+ case 'tool_call':
394
+ // A repeated call id is fed back without running again (docs/12 §5): one row.
395
+ if (!tool(event.callId)) {
396
+ const server = event.server ?? null
397
+ turn.tools.push({
398
+ callId: event.callId,
399
+ name: event.name,
400
+ server,
401
+ pending: null,
402
+ outcome: null,
403
+ summary: '',
404
+ })
405
+ }
406
+ break
407
+ case 'approval_required': {
408
+ const item = tool(event.callId)
409
+ if (item) item.pending = event.argsPreview
410
+ break
411
+ }
412
+ case 'tool_result': {
413
+ const item = tool(event.callId)
414
+ if (item)
415
+ Object.assign(item, { pending: null, outcome: event.outcome, summary: event.summary })
416
+ const painted = (event.payload as { images?: ImageRef[] } | undefined)?.images
417
+ if (event.outcome === 'ok' && Array.isArray(painted)) turn.generated.push(...painted)
418
+ break
419
+ }
420
+ case 'end':
421
+ turn.exhausted = event.exhausted === true
422
+ // Calls still waiting when the run stops never ran.
423
+ for (const item of turn.tools) item.pending = null
424
+ this.#checkSkill(event.error)
425
+ finish(event.status, event.error?.code ?? null)
426
+ break
427
+ case 'round_start':
428
+ turn.contextUsage = null
429
+ this.contextUsage = null
430
+ break
431
+ }
432
+ }
433
+ const request: RunRequest = {
434
+ purpose: base.purpose,
435
+ model: base.model,
436
+ skills: base.skills,
437
+ context: base.context,
438
+ input,
439
+ conversationId: this.conversationId,
440
+ scope: base.scope,
441
+ regenerate: false,
442
+ language: base.language,
443
+ reasoningEffort: base.reasoningEffort ?? 'default',
444
+ permissionMode: base.permissionMode ?? 'default',
445
+ ...(base.generateImage && !action.translateTurnId ? { generateImage: true } : {}),
446
+ ...(base.purpose === 'chat' && this.#stored && !action.retryTurnId && !action.translateTurnId
447
+ ? { replyToId: this.selectedTurnId }
448
+ : {}),
449
+ ...action,
450
+ ...(base.purpose === 'chat' && webSearch ? { webSearch: true } : {}),
451
+ }
452
+ this.#runId = null
453
+ try {
454
+ const result = await this.#port.start(request, onEvent, { signal: abort.signal })
455
+ if (this.#abort !== abort) {
456
+ if (!ended) void this.#port.cancel(result.runId).catch(() => undefined)
457
+ return true
458
+ }
459
+ if (abort.signal.aborted && !ended)
460
+ void this.#port.cancel(result.runId).catch(() => undefined)
461
+ accepted = true
462
+ this.#runId = result.runId
463
+ if (result.conversationId) this.conversationId = result.conversationId
464
+ if (result.turnId) {
465
+ const temporaryId = turn.id
466
+ turn.id = result.turnId
467
+ turn.persisted = true
468
+ if (turn.questionId === temporaryId) turn.questionId = result.turnId
469
+ if (this.selectedTurnId === temporaryId) this.selectedTurnId = result.turnId
470
+ }
471
+ void settle()
472
+ return true
473
+ } catch (error: unknown) {
474
+ if (this.#abort !== abort) return true
475
+ const code = isKitClientError(error) ? error.code : 'internal'
476
+ if (isKitClientError(error)) this.#checkSkill(error)
477
+ finish(code === 'cancelled' ? 'cancelled' : 'failed', code === 'cancelled' ? null : code)
478
+ if ((turn.supersedesId !== null || turn.kind === 'translation') && code !== 'cancelled')
479
+ this.actionError = code
480
+ if (turn.supersedesId !== null || turn.kind === 'translation')
481
+ this.turns = this.turns.filter((item) => item !== turn)
482
+ this.#abort = null
483
+ this.#runId = null
484
+ this.running = false
485
+ return code === 'cancelled'
486
+ }
487
+ }
488
+
489
+ #checkSkill(error: Pick<KitError, 'code' | 'details'> | null | undefined) {
490
+ if (error?.code === 'conflict' && error.details?.reason === 'skill_changed') {
491
+ this.skillChanged = error.details.entity ?? ''
492
+ }
493
+ }
494
+ }