@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,480 @@
1
+ <script lang="ts">
2
+ // Full reply candidate; the panel groups shared user input by questionId.
3
+ import { Button } from '#lib/components/ui/button/index.js'
4
+ import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'
5
+ import { getKitContext } from '../context'
6
+ import ChatMessage from './ChatMessage.svelte'
7
+ import type { ChatTurn } from './run-session.svelte'
8
+ import type { ImageRef, ModelOption, ModelRef } from '../ports'
9
+ import ModelSelect from '../providers/ModelSelect.svelte'
10
+ import ExternalLinkIcon from '@lucide/svelte/icons/external-link'
11
+ import { onDestroy } from 'svelte'
12
+ import CopyIcon from '@lucide/svelte/icons/copy'
13
+ import CheckIcon from '@lucide/svelte/icons/check'
14
+ import FileInputIcon from '@lucide/svelte/icons/file-input'
15
+ import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw'
16
+ import ToolCallItem from './ToolCallItem.svelte'
17
+ import MenuIcon from '@lucide/svelte/icons/menu'
18
+ import LanguagesIcon from '@lucide/svelte/icons/languages'
19
+ import TrashIcon from '@lucide/svelte/icons/trash-2'
20
+ import FilePlusIcon from '@lucide/svelte/icons/file-plus'
21
+ import AtSignIcon from '@lucide/svelte/icons/at-sign'
22
+ import ThumbsUpIcon from '@lucide/svelte/icons/thumbs-up'
23
+ import FilePenLineIcon from '@lucide/svelte/icons/file-pen-line'
24
+ import SplitIcon from '@lucide/svelte/icons/split'
25
+ import CopyPlusIcon from '@lucide/svelte/icons/copy-plus'
26
+ import SaveIcon from '@lucide/svelte/icons/save'
27
+ import UploadIcon from '@lucide/svelte/icons/upload'
28
+
29
+ let {
30
+ turn,
31
+ latest = false,
32
+ onInsert,
33
+ onRetry,
34
+ modelLabel,
35
+ chosen = false,
36
+ showInput = true,
37
+ busy = false,
38
+ editable = false,
39
+ onEdit,
40
+ onBranch,
41
+ onCopyConversation,
42
+ onSwitchModel,
43
+ modelOptions = null,
44
+ sourceModel = null,
45
+ onTranslate,
46
+ onDelete,
47
+ onUseContext,
48
+ onSaveNote,
49
+ onExportMarkdown,
50
+ }: {
51
+ turn: ChatTurn
52
+ latest?: boolean
53
+ onInsert?: ((markdown: string) => void) | undefined
54
+ onRetry?: (() => void) | undefined
55
+ modelLabel?: string
56
+ chosen?: boolean
57
+ showInput?: boolean
58
+ busy?: boolean
59
+ editable?: boolean
60
+ onEdit?: (() => void) | undefined
61
+ onBranch?: (() => void) | undefined
62
+ onCopyConversation?: (() => void) | undefined
63
+ /** Regenerates this reply with the picked model. */
64
+ onSwitchModel?: ((model: ModelRef) => void) | undefined
65
+ modelOptions?: ModelOption[] | null
66
+ sourceModel?: ModelRef | null
67
+ onTranslate?: (() => void) | undefined
68
+ onDelete?: (() => void) | undefined
69
+ onUseContext?: (() => void) | undefined
70
+ onSaveNote?: ((markdown: string, images: ImageRef[]) => Promise<void> | void) | undefined
71
+ onExportMarkdown?: ((markdown: string) => Promise<void> | void) | undefined
72
+ } = $props()
73
+ const { i18n, notify } = getKitContext()
74
+ const { t } = i18n
75
+ const savable = $derived(turn.text !== '' || turn.generated.length > 0)
76
+ function safeUrl(value: string): string | null {
77
+ try {
78
+ const protocol = new URL(value).protocol
79
+ return protocol === 'http:' || protocol === 'https:' ? value : null
80
+ } catch {
81
+ return null
82
+ }
83
+ }
84
+ let copied = $state(false)
85
+ let copyTimer: ReturnType<typeof setTimeout> | undefined
86
+ let alive = true
87
+ onDestroy(() => {
88
+ alive = false
89
+ clearTimeout(copyTimer)
90
+ })
91
+
92
+ function insert() {
93
+ try {
94
+ onInsert?.(turn.text)
95
+ } catch {
96
+ notify.toast('error', t('errors.internal'))
97
+ }
98
+ }
99
+
100
+ async function copy() {
101
+ try {
102
+ await navigator.clipboard.writeText(turn.text)
103
+ } catch {
104
+ notify.toast('error', t('chat.copyFailed'))
105
+ return
106
+ }
107
+ if (!alive) return
108
+ copied = true
109
+ clearTimeout(copyTimer)
110
+ copyTimer = setTimeout(() => (copied = false), 1500)
111
+ }
112
+
113
+ async function saveNote() {
114
+ try {
115
+ await onSaveNote?.(turn.text, turn.generated)
116
+ } catch {
117
+ notify.toast('error', t('chat.saveNoteFailed'))
118
+ }
119
+ }
120
+ async function download() {
121
+ let url: string | null = null
122
+ try {
123
+ if (onExportMarkdown) {
124
+ await onExportMarkdown(turn.text)
125
+ return
126
+ }
127
+ url = URL.createObjectURL(new Blob([turn.text], { type: 'text/markdown;charset=utf-8' }))
128
+ const link = document.createElement('a')
129
+ link.href = url
130
+ link.download = `reply-${turn.id}.md`
131
+ document.body.append(link)
132
+ try {
133
+ link.click()
134
+ } finally {
135
+ link.remove()
136
+ }
137
+ } catch {
138
+ notify.toast('error', t('chat.exportFailed'))
139
+ } finally {
140
+ if (url) URL.revokeObjectURL(url)
141
+ }
142
+ }
143
+ // Browsers refuse to navigate a new tab to a `data:` URL, and a strict CSP connect-src
144
+ // blocks fetch(data:); decode locally and hand it over as a blob.
145
+ function openImage(event: MouseEvent, url: string | null) {
146
+ if (!url?.startsWith('data:')) return
147
+ event.preventDefault()
148
+ const comma = url.indexOf(',')
149
+ const mime = url.slice(5, url.indexOf(';'))
150
+ const bytes = Uint8Array.from(atob(url.slice(comma + 1)), (c) => c.charCodeAt(0))
151
+ const blobUrl = URL.createObjectURL(new Blob([bytes], { type: mime }))
152
+ window.open(blobUrl, '_blank', 'noopener')
153
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
154
+ }
155
+ </script>
156
+
157
+ <li class="message-turn flex min-w-0 flex-col gap-2">
158
+ {#if showInput}
159
+ <ChatMessage
160
+ role="user"
161
+ text={turn.input}
162
+ showMoreLabel={t('chat.showMore')}
163
+ showLessLabel={t('chat.showLess')}
164
+ >
165
+ {#snippet attachments()}
166
+ {#if turn.images.length > 0}
167
+ <ul class="user-images">
168
+ {#each turn.images as image, index (index)}
169
+ <li>
170
+ {#if image.url}<a
171
+ href={image.url}
172
+ target="_blank"
173
+ rel="noopener"
174
+ onclick={(event) => openImage(event, image.url)}
175
+ aria-label={t('chat.openImage', { index: index + 1 })}
176
+ title={t('chat.openImage', { index: index + 1 })}
177
+ ><img src={image.url} alt="" /></a
178
+ >{:else}<span class="image-expired">{t('chat.imageExpired')}</span>{/if}
179
+ </li>
180
+ {/each}
181
+ </ul>
182
+ {/if}
183
+ {/snippet}
184
+ </ChatMessage>
185
+ {/if}
186
+ <ChatMessage
187
+ role="assistant"
188
+ text={turn.text}
189
+ label={modelLabel}
190
+ status={turn.status}
191
+ reasoning={turn.reasoningText}
192
+ reasoningLabel={t('chat.reasoning')}
193
+ selected={chosen}
194
+ {latest}
195
+ >
196
+ {#snippet process()}
197
+ {#if turn.tools.some((tool) => tool.pending === null)}
198
+ <details class="tool-process" open={turn.status === 'running'}>
199
+ <summary>{t('chat.toolProcess')}</summary>
200
+ {#each turn.tools.filter((tool) => tool.pending === null) as tool (tool.callId)}<ToolCallItem
201
+ {tool}
202
+ />{/each}
203
+ </details>
204
+ {/if}
205
+ {/snippet}
206
+ {#snippet attachments()}
207
+ {#if turn.generated.length > 0}
208
+ <ul class="generated-images" aria-label={t('chat.generatedImages')}>
209
+ {#each turn.generated as image, index (index)}
210
+ <li>
211
+ {#if image.url}<a
212
+ href={image.url}
213
+ target="_blank"
214
+ rel="noopener"
215
+ onclick={(event) => openImage(event, image.url)}
216
+ aria-label={t('chat.openGeneratedImage', { index: index + 1 })}
217
+ title={t('chat.openGeneratedImage', { index: index + 1 })}
218
+ ><img src={image.url} alt="" /></a
219
+ >{:else}<span class="image-expired">{t('chat.imageExpired')}</span>{/if}
220
+ </li>
221
+ {/each}
222
+ </ul>
223
+ {/if}
224
+ {/snippet}
225
+ {#snippet actions()}
226
+ {#if savable || (onRetry && turn.status !== 'running') || onUseContext || onDelete}
227
+ {#if turn.text !== ''}
228
+ <Button
229
+ variant="ghost"
230
+ size="icon-xs"
231
+ title={t('chat.copy')}
232
+ aria-label={t('chat.copy')}
233
+ onclick={() => void copy()}
234
+ >
235
+ {#if copied}<CheckIcon />{:else}<CopyIcon />{/if}
236
+ </Button>
237
+ {/if}
238
+ {#if onRetry && turn.status !== 'running'}
239
+ {@const label = t(turn.status === 'failed' ? 'chat.retry' : 'chat.regenerate')}
240
+ <Button
241
+ variant="ghost"
242
+ size="icon-xs"
243
+ title={label}
244
+ aria-label={label}
245
+ disabled={busy}
246
+ onclick={onRetry}><RotateCcwIcon /></Button
247
+ >
248
+ {/if}
249
+ {#if onSwitchModel}<ModelSelect
250
+ value={sourceModel}
251
+ options={modelOptions}
252
+ disabled={busy}
253
+ onchange={(model) => onSwitchModel(model)}
254
+ >
255
+ {#snippet trigger({ props })}<Button
256
+ {...props}
257
+ variant="ghost"
258
+ size="icon-xs"
259
+ title={t('chat.switchModel')}
260
+ aria-label={t('chat.switchModel')}><AtSignIcon /></Button
261
+ >{/snippet}
262
+ </ModelSelect>{/if}
263
+ {#if onTranslate}<Button
264
+ variant="ghost"
265
+ size="icon-xs"
266
+ title={t('chat.translate')}
267
+ aria-label={t('chat.translate')}
268
+ disabled={busy || !turn.text}
269
+ onclick={onTranslate}><LanguagesIcon /></Button
270
+ >{/if}
271
+ {#if onUseContext}<Button
272
+ variant="ghost"
273
+ size="icon-xs"
274
+ title={t('chat.useContext')}
275
+ aria-label={t('chat.useContext')}
276
+ aria-pressed={chosen}
277
+ disabled={busy || turn.status !== 'completed'}
278
+ onclick={onUseContext}
279
+ ><ThumbsUpIcon class={chosen ? 'fill-primary text-primary' : ''} /></Button
280
+ >{/if}
281
+ {#if onSaveNote && savable}<Button
282
+ variant="ghost"
283
+ size="icon-xs"
284
+ title={t('chat.saveNote')}
285
+ aria-label={t('chat.saveNote')}
286
+ disabled={busy || turn.status === 'running'}
287
+ onclick={() => void saveNote()}><FilePlusIcon /></Button
288
+ >{/if}
289
+ {#if onDelete}<Button
290
+ variant="ghost"
291
+ size="icon-xs"
292
+ title={t('chat.deleteReply')}
293
+ aria-label={t('chat.deleteReply')}
294
+ disabled={busy || turn.status === 'running'}
295
+ onclick={onDelete}><TrashIcon /></Button
296
+ >{/if}
297
+ {#if onInsert}
298
+ <Button
299
+ variant="ghost"
300
+ size="icon-xs"
301
+ title={t('chat.insert')}
302
+ aria-label={t('chat.insert')}
303
+ disabled={busy || turn.status === 'running'}
304
+ onclick={insert}
305
+ >
306
+ <FileInputIcon />
307
+ </Button>
308
+ {/if}
309
+ {#if savable || onEdit || onBranch || onCopyConversation}
310
+ <DropdownMenu.Root>
311
+ <DropdownMenu.Trigger>
312
+ {#snippet child({ props })}<Button
313
+ {...props}
314
+ variant="ghost"
315
+ size="icon-xs"
316
+ title={t('chat.more')}
317
+ aria-label={t('chat.more')}><MenuIcon /></Button
318
+ >{/snippet}
319
+ </DropdownMenu.Trigger>
320
+ <DropdownMenu.Content class="w-auto">
321
+ {#if onEdit || onBranch || onCopyConversation}
322
+ <DropdownMenu.Group>
323
+ {#if onEdit}<DropdownMenu.Item
324
+ disabled={busy || !editable}
325
+ onSelect={() => onEdit?.()}
326
+ ><FilePenLineIcon />{t('chat.editReply')}</DropdownMenu.Item
327
+ >{/if}
328
+ {#if onBranch}<DropdownMenu.Item disabled={busy} onSelect={() => onBranch?.()}
329
+ ><SplitIcon />{t('chat.branch')}</DropdownMenu.Item
330
+ >{/if}
331
+ {#if onCopyConversation}<DropdownMenu.Item
332
+ disabled={busy}
333
+ onSelect={() => onCopyConversation?.()}
334
+ ><CopyPlusIcon />{t('chat.copyConversation')}</DropdownMenu.Item
335
+ >{/if}
336
+ </DropdownMenu.Group>
337
+ <DropdownMenu.Separator />
338
+ {/if}
339
+ <DropdownMenu.Sub>
340
+ <DropdownMenu.SubTrigger disabled={!savable}
341
+ ><SaveIcon />{t('chat.save')}</DropdownMenu.SubTrigger
342
+ >
343
+ <DropdownMenu.SubContent>
344
+ <DropdownMenu.Item disabled={!turn.text} onSelect={() => void download()}
345
+ >{t('chat.saveFile')}</DropdownMenu.Item
346
+ >
347
+ {#if onSaveNote}<DropdownMenu.Item
348
+ disabled={busy || turn.status === 'running' || !savable}
349
+ onSelect={() => void saveNote()}>{t('chat.saveNote')}</DropdownMenu.Item
350
+ >{/if}
351
+ </DropdownMenu.SubContent>
352
+ </DropdownMenu.Sub>
353
+ <DropdownMenu.Separator />
354
+ <DropdownMenu.Sub>
355
+ <DropdownMenu.SubTrigger disabled={!turn.text}
356
+ ><UploadIcon />{t('chat.export')}</DropdownMenu.SubTrigger
357
+ >
358
+ <DropdownMenu.SubContent>
359
+ <DropdownMenu.Item disabled={!turn.text} onSelect={() => void download()}
360
+ >{t('chat.exportMarkdown')}</DropdownMenu.Item
361
+ >
362
+ </DropdownMenu.SubContent>
363
+ </DropdownMenu.Sub>
364
+ </DropdownMenu.Content>
365
+ </DropdownMenu.Root>
366
+ {/if}
367
+ <span class="text-muted-foreground text-kit-sm" role="status"
368
+ >{copied ? t('chat.copied') : ''}</span
369
+ >
370
+ {/if}
371
+ {/snippet}
372
+ {#if turn.search.status === 'complete'}
373
+ <section class="flex flex-col gap-1 text-kit-sm" aria-label={t('chat.sources')}>
374
+ <p class="text-muted-foreground">{t('chat.sources')}</p>
375
+ {#each turn.search.hits as hit (hit.url)}
376
+ {@const url = safeUrl(hit.url)}
377
+ {#if url}<a
378
+ class="text-primary flex min-w-0 items-center gap-1 underline"
379
+ href={url}
380
+ target="_blank"
381
+ rel="noopener noreferrer"
382
+ ><ExternalLinkIcon data-icon="inline-start" /><span class="truncate"
383
+ >{hit.title || url}</span
384
+ ></a
385
+ >{/if}
386
+ {/each}
387
+ </section>
388
+ {/if}
389
+ {#if turn.status === 'cancelled'}
390
+ <p class="text-muted-foreground text-kit-sm" role="status">{t('chat.stopped')}</p>
391
+ {:else if turn.status === 'failed'}
392
+ <div class="flex items-center gap-2">
393
+ <p class="text-destructive text-kit-sm" role="alert">
394
+ {turn.error === 'interrupted'
395
+ ? t('chat.interrupted')
396
+ : t(`errors.${turn.error ?? 'internal'}`)}
397
+ </p>
398
+ </div>
399
+ {:else if turn.exhausted}
400
+ <p class="text-muted-foreground text-kit-sm">{t('chat.exhausted')}</p>
401
+ {/if}
402
+ </ChatMessage>
403
+ </li>
404
+
405
+ <style>
406
+ .message-turn {
407
+ min-width: 0;
408
+ max-width: 100%;
409
+ overflow-wrap: anywhere;
410
+ }
411
+ .user-images {
412
+ display: flex;
413
+ flex-wrap: wrap;
414
+ justify-content: flex-end;
415
+ gap: 0.375rem;
416
+ margin: 0;
417
+ padding: 0;
418
+ list-style: none;
419
+ }
420
+ .generated-images {
421
+ display: grid;
422
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
423
+ gap: 0.5rem;
424
+ margin: 0;
425
+ padding: 0;
426
+ list-style: none;
427
+ }
428
+ .generated-images a {
429
+ display: block;
430
+ overflow: hidden;
431
+ border: 1px solid var(--color-border);
432
+ border-radius: var(--kit-radius, var(--radius));
433
+ }
434
+ .generated-images a:focus-visible {
435
+ outline: 2px solid var(--color-ring);
436
+ outline-offset: 2px;
437
+ }
438
+ .generated-images img {
439
+ display: block;
440
+ width: 100%;
441
+ height: auto;
442
+ }
443
+ .user-images a,
444
+ .image-expired {
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ width: 5rem;
449
+ height: 5rem;
450
+ overflow: hidden;
451
+ border: 1px solid var(--color-border);
452
+ border-radius: var(--kit-radius, var(--radius));
453
+ }
454
+ .user-images a:focus-visible {
455
+ outline: 2px solid var(--color-ring);
456
+ outline-offset: 2px;
457
+ }
458
+ .user-images img {
459
+ width: 100%;
460
+ height: 100%;
461
+ object-fit: cover;
462
+ }
463
+ .image-expired {
464
+ padding: 0.25rem;
465
+ color: var(--color-muted-foreground);
466
+ font-size: var(--kit-font-size-sm, 0.8125rem);
467
+ text-align: center;
468
+ }
469
+ .tool-process {
470
+ color: var(--color-muted-foreground);
471
+ font-size: var(--kit-text-sm);
472
+ }
473
+ .tool-process summary {
474
+ cursor: pointer;
475
+ }
476
+ .tool-process summary:focus-visible {
477
+ outline: 2px solid var(--color-ring);
478
+ outline-offset: 2px;
479
+ }
480
+ </style>