@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,496 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte'
3
+ import { getKitContext } from '../context'
4
+ import {
5
+ isKitClientError,
6
+ type ModelRef,
7
+ type SkillRef,
8
+ type ModelOption,
9
+ type ReasoningEffort,
10
+ type PermissionMode,
11
+ } from '../ports'
12
+ import { providersRevision } from '../providers/revision.svelte'
13
+ import { RunSession } from './run-session.svelte'
14
+ import { ChatPanelHistoryState } from './chat-panel-history.svelte'
15
+ import { ChatAttachments, hasImages, type AttachedImage } from './chat-attachments.svelte'
16
+ import ChatSurface from './ChatSurface.svelte'
17
+ import ChatPanelApprovals from './ChatPanelApprovals.svelte'
18
+ import { nearestEffort, REASONING_EFFORTS } from '../providers/reasoning'
19
+ import ChatPanelHeader from './ChatPanelHeader.svelte'
20
+ import ChatPanelMessages from './ChatPanelMessages.svelte'
21
+ import ChatPanelComposer from './ChatPanelComposer.svelte'
22
+ import ChatPanelDialogs from './ChatPanelDialogs.svelte'
23
+ import type { ChatPanelProps } from './chat-panel-props'
24
+
25
+ let {
26
+ purpose = 'chat',
27
+ model = null,
28
+ scope,
29
+ context = null,
30
+ skills,
31
+ onSkillsChange,
32
+ skillsBudget = 48_000,
33
+ onInsert,
34
+ onSaveNote,
35
+ onExportMarkdown,
36
+ onOpenSettings,
37
+ conversationId = null,
38
+ onConversationChange,
39
+ onClose,
40
+ paintingModel = false,
41
+ toolbar: toolbarProp,
42
+ onToolbarChange,
43
+ reasoningEffort: reasoningEffortProp,
44
+ onReasoningEffortChange,
45
+ permissionModes = ['default', 'plan', 'accept_edits'],
46
+ permissionMode = $bindable('default'),
47
+ }: ChatPanelProps = $props()
48
+
49
+ const { ports, i18n, notify } = getKitContext()
50
+ const { t } = i18n
51
+ const history = ports.conversations
52
+ const session = ports.chat ? new RunSession(ports.chat, history) : null
53
+ // svelte-ignore state_referenced_locally
54
+ let picked = $state<ModelRef | null>(model)
55
+ let modelOptions = $state<ModelOption[] | null>(null)
56
+ let modelError = $state<string | null>(null)
57
+ let modelReload = $state(0)
58
+ const capability = $derived(
59
+ modelOptions?.find(
60
+ (item) => item.endpointId === picked?.endpointId && item.modelId === picked?.modelId,
61
+ ) ?? null,
62
+ )
63
+ // The saved choice survives model switches; each model sends its nearest allowed effort (docs/28 RT3).
64
+ // svelte-ignore state_referenced_locally
65
+ let chosenEffort = $state<ReasoningEffort>(reasoningEffortProp ?? 'default')
66
+ const allowedEfforts = $derived(
67
+ REASONING_EFFORTS.filter((effort) => capability?.reasoning?.efforts.includes(effort)),
68
+ )
69
+ const reasoningEffort = $derived(nearestEffort(chosenEffort, allowedEfforts))
70
+ function chooseEffort(effort: ReasoningEffort) {
71
+ if (session?.running || effort === chosenEffort) return
72
+ chosenEffort = effort
73
+ onReasoningEffortChange?.(effort)
74
+ }
75
+ const MODES: PermissionMode[] = ['default', 'plan', 'accept_edits', 'auto', 'full_access']
76
+ const isAgent = $derived(purpose === 'agent')
77
+ const allowedModes: PermissionMode[] = $derived(
78
+ isAgent ? MODES.filter((mode) => permissionModes.includes(mode)) : ['default'],
79
+ )
80
+ let fullAccess = $state(false)
81
+ const mode: PermissionMode = $derived(
82
+ fullAccess && allowedModes.includes('full_access')
83
+ ? 'full_access'
84
+ : permissionMode !== 'full_access' && allowedModes.includes(permissionMode)
85
+ ? permissionMode
86
+ : (allowedModes.find((option) => option !== 'full_access') ?? 'default'),
87
+ )
88
+ async function chooseMode(next: PermissionMode) {
89
+ if (session?.running || next === mode || !allowedModes.includes(next)) return
90
+ const generation = historyState.viewGeneration
91
+ if (next === 'full_access') {
92
+ if (
93
+ !(await notify.confirm({
94
+ title: t('chat.mode.fullAccessTitle'),
95
+ body: t('chat.mode.fullAccessBody'),
96
+ danger: true,
97
+ }))
98
+ )
99
+ return
100
+ if (
101
+ !historyState.disposed &&
102
+ generation === historyState.viewGeneration &&
103
+ !session?.running &&
104
+ allowedModes.includes(next)
105
+ )
106
+ fullAccess = true
107
+ } else {
108
+ fullAccess = false
109
+ permissionMode = next
110
+ }
111
+ }
112
+ let reasoningOpen = $state(false)
113
+ const noModels = $derived(modelOptions !== null && modelOptions.length === 0)
114
+ const errorText = (error: unknown) =>
115
+ t(`errors.${isKitClientError(error) ? error.code : 'internal'}`)
116
+ $effect(() => {
117
+ void modelReload
118
+ void providersRevision.value
119
+ let alive = true
120
+ // A provider save swaps options in place: nulling them would reset the chosen effort (docs/41 §3.5).
121
+ modelError = null
122
+ if (!ports.providers) {
123
+ modelOptions = []
124
+ return
125
+ }
126
+ void ports.providers.modelOptions({ modelType: 'chat' }).then(
127
+ (list) => {
128
+ if (alive) modelOptions = list
129
+ },
130
+ (error: unknown) => {
131
+ if (alive) modelError = errorText(error)
132
+ },
133
+ )
134
+ return () => {
135
+ alive = false
136
+ }
137
+ })
138
+ $effect(() => {
139
+ // A model that turns out not to take tools cannot keep a pending generate_image request.
140
+ if (capability?.supportsTools === false) generateImage = false
141
+ })
142
+ function pickModel(next: ModelRef) {
143
+ picked = next
144
+ session?.resetContext()
145
+ }
146
+
147
+ let draft = $state('')
148
+ let draftRevision = 0
149
+ let contextRevision = 0
150
+ // svelte-ignore state_referenced_locally
151
+ let ownSkills = $state<SkillRef[]>(skills ?? [])
152
+ const selected = $derived(skills ?? ownSkills)
153
+ function setSkills(refs: SkillRef[]) {
154
+ ownSkills = refs
155
+ onSkillsChange?.(refs)
156
+ }
157
+ let droppedKey = $state<string | null>(null)
158
+ const contextKey = $derived(context ? `${context.kind}:${context.id}` : null)
159
+ const activeContext = $derived(context && contextKey !== droppedKey ? context : null)
160
+ let webSearch = $state(false)
161
+ let generateImage = $state(false)
162
+ const base = $derived({
163
+ purpose,
164
+ scope: scope ?? null,
165
+ language: i18n.locale,
166
+ model: picked,
167
+ skills: isAgent ? selected : [],
168
+ reasoningEffort,
169
+ permissionMode: mode,
170
+ generateImage: purpose === 'chat' && generateImage && paintingModel,
171
+ context: activeContext ? { kind: activeContext.kind, id: activeContext.id } : null,
172
+ })
173
+ // Consume before awaiting run acceptance, including retries and translations. Never persist full access.
174
+ function consumeBase() {
175
+ const request = { ...base }
176
+ fullAccess = false
177
+ return request
178
+ }
179
+
180
+ let scrollRevision = $state(0)
181
+ const historyState = new ChatPanelHistoryState(
182
+ {
183
+ history,
184
+ getSession: () => session,
185
+ getModelOptions: () => modelOptions,
186
+ getModelError: () => modelError,
187
+ setDraft: (value) => {
188
+ draft = value
189
+ },
190
+ bumpDraftRevision: () => draftRevision++,
191
+ setFullAccess: (value) => {
192
+ fullAccess = value
193
+ },
194
+ getScope: () => scope,
195
+ getLocale: () => i18n.locale,
196
+ onConversationChange: (id) => onConversationChange?.(id),
197
+ bumpScrollRevision: () => scrollRevision++,
198
+ notify,
199
+ t,
200
+ errorText,
201
+ consumeBase,
202
+ discardRecovery: () => discardRecovery(),
203
+ },
204
+ untrack(() => conversationId),
205
+ )
206
+ $effect(() => () => historyState.dispose())
207
+ $effect(() => {
208
+ if (session && !session.running) void historyState.refresh()
209
+ })
210
+ $effect(() => historyState.syncConversation())
211
+ void historyState.restore(untrack(() => conversationId))
212
+
213
+ let followKey = $state(0)
214
+
215
+ let contentRevision = 0
216
+ const pendingApprovals = $derived(
217
+ session?.running
218
+ ? session.turns.flatMap((turn) => turn.tools.filter((tool) => tool.pending !== null))
219
+ : [],
220
+ )
221
+ $effect.pre(() => {
222
+ for (const turn of session?.turns ?? []) {
223
+ void turn.text
224
+ void turn.reasoningText
225
+ void turn.status
226
+ void turn.error
227
+ void turn.usage
228
+ void turn.search
229
+ void turn.generated.length
230
+ for (const tool of turn.tools) {
231
+ void tool.pending
232
+ void tool.summary
233
+ void tool.outcome
234
+ }
235
+ }
236
+ followKey = ++contentRevision
237
+ })
238
+ function onkeydown(e: KeyboardEvent) {
239
+ if (e.key !== 'Escape' || e.defaultPrevented || e.isComposing) return
240
+ // Popover content renders inline, so Escape inside it bubbles here; let the layer close itself.
241
+ if (
242
+ reasoningOpen ||
243
+ historyState.historyOpen ||
244
+ document.querySelector(
245
+ '[data-popover-content], [data-slot="select-content"], [role="dialog"]',
246
+ )
247
+ )
248
+ return
249
+ if (onClose && !session?.running) onClose()
250
+ }
251
+ const attachments = new ChatAttachments(() => capability?.canVision === true, notify, i18n)
252
+ type Attached = AttachedImage
253
+ const currentContext = $derived(
254
+ draft.trim() === '' &&
255
+ attachments.images.length === 0 &&
256
+ session?.contextUsage &&
257
+ session.contextUsage.endpointId === picked?.endpointId &&
258
+ session.contextUsage.modelId === picked?.modelId
259
+ ? session.contextUsage
260
+ : null,
261
+ )
262
+ const reading = $derived(attachments.reading)
263
+ const vision = $derived(capability?.canVision === true)
264
+ const canSend = $derived(
265
+ session !== null &&
266
+ !session.running &&
267
+ historyState.restored &&
268
+ !historyState.restoreBlocked &&
269
+ !historyState.historyBusy &&
270
+ capability !== null &&
271
+ modelError === null &&
272
+ draft.trim() !== '' &&
273
+ (attachments.images.length === 0 || vision) &&
274
+ reading === 0,
275
+ )
276
+ let recovery = $state<{
277
+ draft: string
278
+ images: Attached[]
279
+ droppedKey: string | null
280
+ contextKey: string | null
281
+ } | null>(null)
282
+ function discardRecovery() {
283
+ for (const image of recovery?.images ?? []) URL.revokeObjectURL(image.preview)
284
+ recovery = null
285
+ }
286
+ function recoverDraft() {
287
+ if (!recovery || draft !== '' || attachments.images.length > 0) return
288
+ draft = recovery.draft
289
+ attachments.images = recovery.images
290
+ draftRevision++
291
+ if (contextKey === recovery.contextKey) droppedKey = recovery.droppedKey
292
+ contextRevision++
293
+ recovery = null
294
+ }
295
+ async function send() {
296
+ if (!session || !canSend) return
297
+ const generation = historyState.viewGeneration
298
+ const original = draft
299
+ const attached = attachments.images
300
+ const revision = draftRevision
301
+ const contextVersion = contextRevision
302
+ const removed = droppedKey
303
+ const key = contextKey
304
+ draft = ''
305
+ attachments.images = []
306
+ discardRecovery()
307
+ scrollRevision++
308
+ const accepted = await session.send(
309
+ consumeBase(),
310
+ original.trim(),
311
+ webSearch,
312
+ attached.map(({ mime, data }) => ({ mime, data })),
313
+ )
314
+ if (historyState.disposed || generation !== historyState.viewGeneration) {
315
+ for (const image of attached) URL.revokeObjectURL(image.preview)
316
+ return
317
+ }
318
+ if (accepted) {
319
+ for (const image of attached) URL.revokeObjectURL(image.preview)
320
+ if (contextRevision === contextVersion && contextKey === key) droppedKey = null
321
+ } else if (draftRevision === revision && attachments.images.length === 0) {
322
+ draft = original
323
+ attachments.images = attached
324
+ if (contextRevision === contextVersion && contextKey === key) droppedKey = removed
325
+ } else {
326
+ recovery = { draft: original, images: attached, droppedKey: removed, contextKey: key }
327
+ }
328
+ }
329
+
330
+ onDestroy(() => {
331
+ for (const image of attachments.images) URL.revokeObjectURL(image.preview)
332
+ discardRecovery()
333
+ })
334
+ const removeImage = attachments.remove
335
+ const attach = attachments.attach
336
+ </script>
337
+
338
+ {#if session}
339
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions (Esc bubbles from controls) -->
340
+ <section class="chat-panel" aria-label={t('chat.title')} {onkeydown}>
341
+ <ChatSurface
342
+ label={t('chat.title')}
343
+ messagesLabel={t('chat.messages')}
344
+ latestLabel={t('chat.backToBottom')}
345
+ {followKey}
346
+ scrollKey={`${session.conversationId ?? ''}:${scrollRevision}`}
347
+ >
348
+ {#snippet header()}
349
+ <ChatPanelHeader
350
+ {i18n}
351
+ title={historyState.title}
352
+ {session}
353
+ {history}
354
+ historyBusy={historyState.historyBusy}
355
+ historyOpen={historyState.historyOpen}
356
+ historyError={historyState.historyError}
357
+ {modelOptions}
358
+ {modelError}
359
+ onHistory={() => {
360
+ historyState.historyOpen = true
361
+ void historyState.refresh()
362
+ }}
363
+ {onClose}
364
+ onRetryHistory={() => void historyState.retryHistory()}
365
+ {picked}
366
+ onPickModel={pickModel}
367
+ {onOpenSettings}
368
+ />
369
+ {/snippet}
370
+ <ChatPanelMessages
371
+ {i18n}
372
+ {session}
373
+ groups={historyState.groups}
374
+ {purpose}
375
+ {history}
376
+ restoreBlocked={historyState.restoreBlocked}
377
+ actionBusy={historyState.actionBusy}
378
+ {modelOptions}
379
+ {modelError}
380
+ {capability}
381
+ {noModels}
382
+ onRetryModels={() => modelReload++}
383
+ {onOpenSettings}
384
+ {onInsert}
385
+ {onSaveNote}
386
+ {onExportMarkdown}
387
+ sourceLabel={historyState.sourceLabel}
388
+ sourceModel={historyState.sourceModel}
389
+ canEdit={historyState.canEdit}
390
+ onRetry={(turn) => void historyState.retryReply(turn)}
391
+ onEdit={historyState.editReply}
392
+ onBranch={(turn) => void historyState.mutateReply(turn, 'branch')}
393
+ onCopyConversation={(turn) => void historyState.mutateReply(turn, 'copy')}
394
+ onSwitchModel={(turn, next) => void historyState.switchModel(turn, next)}
395
+ onTranslate={historyState.translateDialog}
396
+ onDelete={(turn) => void historyState.mutateReply(turn, 'remove')}
397
+ onUseContext={(turn) => void historyState.mutateReply(turn, 'select')}
398
+ />
399
+ {#snippet composer()}
400
+ <ChatPanelComposer
401
+ {i18n}
402
+ {session}
403
+ {purpose}
404
+ bind:draft
405
+ {canSend}
406
+ {activeContext}
407
+ images={attachments.images}
408
+ {vision}
409
+ {recovery}
410
+ bind:webSearch
411
+ bind:generateImage
412
+ toolbar={toolbarProp}
413
+ {onToolbarChange}
414
+ {paintingModel}
415
+ {capability}
416
+ historyBusy={historyState.historyBusy}
417
+ onNewConversation={historyState.newConversation}
418
+ {attach}
419
+ onDraftInput={() => draftRevision++}
420
+ onRemoveImage={removeImage}
421
+ onRecoverDraft={recoverDraft}
422
+ onDropContext={() => {
423
+ droppedKey = contextKey
424
+ contextRevision++
425
+ }}
426
+ hasSearch={!!ports.search}
427
+ {selected}
428
+ {skillsBudget}
429
+ skillsPort={isAgent ? ports.skills : undefined}
430
+ onSkillsChange={setSkills}
431
+ onSkillResolved={() => (session.skillChanged = null)}
432
+ {mode}
433
+ {allowedModes}
434
+ {chooseMode}
435
+ {reasoningEffort}
436
+ {allowedEfforts}
437
+ bind:reasoningOpen
438
+ {chooseEffort}
439
+ {currentContext}
440
+ {picked}
441
+ {onOpenSettings}
442
+ onSend={() => void send()}
443
+ onStop={() => session.stop()}
444
+ {hasImages}
445
+ override={pendingApprovals.length ? approvals : undefined}
446
+ />
447
+ {/snippet}
448
+ </ChatSurface>
449
+ {#snippet approvals()}
450
+ <ChatPanelApprovals {session} tools={pendingApprovals} {i18n} />
451
+ {/snippet}
452
+ <ChatPanelDialogs
453
+ {i18n}
454
+ {history}
455
+ {session}
456
+ conversations={historyState.conversations}
457
+ title={historyState.title}
458
+ historyLoading={historyState.historyLoading}
459
+ historyBusy={historyState.historyBusy}
460
+ historyError={historyState.historyError}
461
+ retryHistory={historyState.retryHistory}
462
+ open={historyState.open}
463
+ rename={historyState.rename}
464
+ remove={historyState.remove}
465
+ actionBusy={historyState.actionBusy}
466
+ replyDialog={historyState.replyDialog}
467
+ {modelOptions}
468
+ {modelError}
469
+ {noModels}
470
+ validReplyModel={historyState.validReplyModel}
471
+ submitReply={historyState.submitReply}
472
+ reloadModels={() => modelReload++}
473
+ bind:historyOpen={historyState.historyOpen}
474
+ bind:renaming={historyState.renaming}
475
+ bind:renameTitle={historyState.renameTitle}
476
+ bind:replyOpen={historyState.replyOpen}
477
+ bind:editDraft={historyState.editDraft}
478
+ bind:replyModel={historyState.replyModel}
479
+ bind:targetLanguage={historyState.targetLanguage}
480
+ replyError={historyState.replyError}
481
+ />
482
+ </section>
483
+ {/if}
484
+
485
+ <style>
486
+ .chat-panel {
487
+ display: flex;
488
+ flex: 1;
489
+ flex-direction: column;
490
+ gap: 0.75rem;
491
+ min-width: 0;
492
+ min-height: 0;
493
+ height: 100%;
494
+ container-type: size;
495
+ }
496
+ </style>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import { Button } from '#lib/components/ui/button/index.js'
3
+ import ApprovalCard from './ApprovalCard.svelte'
4
+ import type { RunSession, ToolItem } from './run-session.svelte'
5
+ import type { KitI18n } from '../i18n'
6
+ let { session, tools, i18n }: { session: RunSession; tools: ToolItem[]; i18n: KitI18n } = $props()
7
+ </script>
8
+
9
+ <div class="approval-override">
10
+ {#each tools as tool (tool.callId)}
11
+ <ApprovalCard {tool} onAnswer={(allow) => session.approve(tool, allow)} />
12
+ {/each}
13
+ <Button type="button" variant="outline" onclick={() => session.stop()}
14
+ >{i18n.t('chat.stop')}</Button
15
+ >
16
+ </div>
17
+
18
+ <style>
19
+ .approval-override {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 0.5rem;
23
+ min-width: 0;
24
+ }
25
+ .approval-override > :global(button) {
26
+ align-self: flex-end;
27
+ }
28
+ </style>