@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,286 @@
1
+ <script lang="ts">
2
+ import GaugeIcon from '@lucide/svelte/icons/gauge'
3
+ import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
4
+ import { Popover } from 'bits-ui'
5
+ import { Button } from '#lib/components/ui/button/index.js'
6
+ import * as Field from '#lib/components/ui/field/index.js'
7
+ import type { KitI18n } from '../i18n'
8
+ import type { ReasoningEffort } from '../ports'
9
+ import { REASONING_EFFORTS } from '../providers/reasoning'
10
+
11
+ let {
12
+ value = 'default',
13
+ efforts,
14
+ disabled = false,
15
+ open = $bindable(false),
16
+ i18n,
17
+ onchange,
18
+ onOpenSettings,
19
+ }: {
20
+ value?: ReasoningEffort
21
+ efforts: readonly ReasoningEffort[]
22
+ disabled?: boolean
23
+ open?: boolean
24
+ i18n: KitI18n
25
+ onchange: (effort: ReasoningEffort) => void
26
+ onOpenSettings?: (() => void) | undefined
27
+ } = $props()
28
+ const id = $props.id()
29
+ let trigger = $state<HTMLButtonElement | null>(null)
30
+ const t = (key: string) => i18n.t(key)
31
+ const allowedEfforts = $derived(REASONING_EFFORTS.filter((effort) => efforts.includes(effort)))
32
+ const effectiveEffort = $derived(
33
+ value === 'default' || allowedEfforts.includes(value) ? value : 'default',
34
+ )
35
+ const effortLabel = $derived(t(`chat.effort.${effectiveEffort}`))
36
+ const effortIndex = $derived(
37
+ effectiveEffort === 'default'
38
+ ? Math.floor(allowedEfforts.length / 2)
39
+ : allowedEfforts.indexOf(effectiveEffort),
40
+ )
41
+ function choose(effort: ReasoningEffort) {
42
+ if (!disabled && (effort === 'default' || allowedEfforts.includes(effort))) onchange(effort)
43
+ }
44
+ function commit(event: Event & { currentTarget: HTMLInputElement }) {
45
+ const effort = allowedEfforts[Number(event.currentTarget.value)]
46
+ if (effort) choose(effort)
47
+ }
48
+ </script>
49
+
50
+ <Popover.Root bind:open>
51
+ <Popover.Trigger {disabled}>
52
+ {#snippet child({ props })}
53
+ <Button
54
+ {...props}
55
+ bind:ref={trigger}
56
+ variant="ghost"
57
+ size="sm"
58
+ aria-label={`${t('chat.reasoningEffort')}: ${effortLabel}`}
59
+ >
60
+ <GaugeIcon data-icon="inline-start" />
61
+ {effortLabel}
62
+ <ChevronDownIcon data-icon="inline-end" />
63
+ </Button>
64
+ {/snippet}
65
+ </Popover.Trigger>
66
+ <Popover.Portal>
67
+ <Popover.Content
68
+ class="reasoning-popover"
69
+ side="top"
70
+ align="end"
71
+ sideOffset={8}
72
+ collisionPadding={8}
73
+ trapFocus={false}
74
+ preventScroll={false}
75
+ aria-label={t('chat.reasoningEffort')}
76
+ onEscapeKeydown={(event) => {
77
+ if (event.isComposing) event.preventDefault()
78
+ event.stopPropagation()
79
+ }}
80
+ onCloseAutoFocus={(event) => {
81
+ event.preventDefault()
82
+ trigger?.focus({ preventScroll: true })
83
+ }}
84
+ >
85
+ <div class="reasoning-heading">
86
+ <p>{t('chat.reasoningStrength')}: <strong>{effortLabel}</strong></p>
87
+ <Button
88
+ variant="secondary"
89
+ size="xs"
90
+ disabled={disabled || value === 'default'}
91
+ onclick={() => choose('default')}>{t('chat.resetReasoning')}</Button
92
+ >
93
+ </div>
94
+ {#if allowedEfforts.length === 0}
95
+ <p class="reasoning-hint">{t('chat.reasoningUnconfigured')}</p>
96
+ {#if onOpenSettings}<Button
97
+ variant="outline"
98
+ size="sm"
99
+ onclick={() => {
100
+ open = false
101
+ onOpenSettings()
102
+ }}>{t('chat.openSettings')}</Button
103
+ >{/if}
104
+ {:else if allowedEfforts.length > 1}
105
+ <div class="reasoning-scale" aria-hidden="true">
106
+ <span>{t('chat.reasoningFaster')}</span><span>{t('chat.reasoningSmarter')}</span>
107
+ </div>
108
+ <div
109
+ class="reasoning-slider"
110
+ style:--reasoning-progress={`${(effortIndex / (allowedEfforts.length - 1)) * 100}%`}
111
+ >
112
+ <input
113
+ type="range"
114
+ min="0"
115
+ max={allowedEfforts.length - 1}
116
+ step="1"
117
+ value={effortIndex}
118
+ aria-label={t('chat.reasoningEffort')}
119
+ aria-valuetext={effortLabel}
120
+ {disabled}
121
+ oninput={commit}
122
+ onpointerup={(event) => {
123
+ if (event.button === 0) commit(event)
124
+ }}
125
+ onkeyup={(event) => {
126
+ if (
127
+ !event.isComposing &&
128
+ ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(
129
+ event.key,
130
+ )
131
+ ) {
132
+ commit(event)
133
+ }
134
+ }}
135
+ />
136
+ <div class="reasoning-ticks" aria-hidden="true">
137
+ {#each allowedEfforts as effort, index (effort)}
138
+ <span
139
+ class="reasoning-tick"
140
+ data-effort={effort}
141
+ class:current={index === effortIndex}
142
+ style:left={`${(index / (allowedEfforts.length - 1)) * 100}%`}
143
+ ></span>
144
+ {/each}
145
+ </div>
146
+ </div>
147
+ {:else}
148
+ <Field.FieldSet {disabled}>
149
+ <Field.FieldLegend class="sr-only">{t('chat.reasoningEffort')}</Field.FieldLegend>
150
+ {#each ['default' as const, ...allowedEfforts] as effort (effort)}
151
+ <label class="reasoning-option">
152
+ <input
153
+ type="radio"
154
+ name={id}
155
+ value={effort}
156
+ checked={effectiveEffort === effort}
157
+ onchange={() => choose(effort)}
158
+ />
159
+ {t(`chat.effort.${effort}`)}
160
+ </label>
161
+ {/each}
162
+ </Field.FieldSet>
163
+ {/if}
164
+ {#if effectiveEffort === 'default'}<p class="reasoning-hint">
165
+ {t('chat.reasoningDefaultHint')}
166
+ </p>{/if}
167
+ </Popover.Content>
168
+ </Popover.Portal>
169
+ </Popover.Root>
170
+
171
+ <style>
172
+ :global(.reasoning-popover) {
173
+ box-sizing: border-box;
174
+ z-index: 210;
175
+ width: min(18rem, calc(100vw - 1rem));
176
+ max-height: calc(100dvh - 1rem);
177
+ overflow-y: auto;
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 0.75rem;
181
+ padding: 0.75rem;
182
+ border: 1px solid var(--color-border);
183
+ border-radius: var(--kit-radius, var(--radius));
184
+ background: var(--color-popover);
185
+ color: var(--color-popover-foreground);
186
+ box-shadow: 0 4px 12px color-mix(in oklch, var(--color-foreground) 12%, transparent);
187
+ font-size: var(--kit-font-size-sm, 0.8125rem);
188
+ }
189
+ .reasoning-heading,
190
+ .reasoning-scale {
191
+ display: flex;
192
+ justify-content: space-between;
193
+ align-items: center;
194
+ gap: 0.5rem;
195
+ }
196
+ .reasoning-heading p,
197
+ .reasoning-hint {
198
+ margin: 0;
199
+ overflow-wrap: anywhere;
200
+ }
201
+ .reasoning-scale,
202
+ .reasoning-hint {
203
+ color: var(--color-muted-foreground);
204
+ }
205
+ :global(.reasoning-popover input) {
206
+ accent-color: var(--color-primary);
207
+ }
208
+ .reasoning-slider {
209
+ position: relative;
210
+ height: 1.75rem;
211
+ display: flex;
212
+ align-items: center;
213
+ }
214
+ :global(.reasoning-slider input[type='range']) {
215
+ appearance: none;
216
+ width: 100%;
217
+ height: 1.75rem;
218
+ margin: 0;
219
+ background: transparent;
220
+ cursor: pointer;
221
+ }
222
+ :global(.reasoning-slider input::-webkit-slider-runnable-track) {
223
+ height: 0.375rem;
224
+ border-radius: 999px;
225
+ background: linear-gradient(
226
+ to right,
227
+ var(--color-primary) var(--reasoning-progress),
228
+ var(--color-muted) var(--reasoning-progress)
229
+ );
230
+ }
231
+ :global(.reasoning-slider input::-moz-range-track) {
232
+ height: 0.375rem;
233
+ border-radius: 999px;
234
+ background: linear-gradient(
235
+ to right,
236
+ var(--color-primary) var(--reasoning-progress),
237
+ var(--color-muted) var(--reasoning-progress)
238
+ );
239
+ }
240
+ :global(.reasoning-slider input::-webkit-slider-thumb) {
241
+ appearance: none;
242
+ width: 1.125rem;
243
+ height: 1.125rem;
244
+ margin-top: -0.375rem;
245
+ border: 1px solid var(--color-border);
246
+ border-radius: 50%;
247
+ background: var(--color-popover);
248
+ }
249
+ :global(.reasoning-slider input::-moz-range-thumb) {
250
+ width: 1.125rem;
251
+ height: 1.125rem;
252
+ box-sizing: border-box;
253
+ border: 1px solid var(--color-border);
254
+ border-radius: 50%;
255
+ background: var(--color-popover);
256
+ }
257
+ .reasoning-ticks {
258
+ position: absolute;
259
+ inset: 50% 0.5625rem auto;
260
+ pointer-events: none;
261
+ }
262
+ .reasoning-tick {
263
+ position: absolute;
264
+ width: 0.25rem;
265
+ height: 0.25rem;
266
+ border-radius: 50%;
267
+ background: var(--color-popover);
268
+ transform: translate(-50%, -50%);
269
+ }
270
+ .reasoning-tick.current {
271
+ visibility: hidden;
272
+ }
273
+ :global(.reasoning-slider input:disabled) {
274
+ cursor: not-allowed;
275
+ opacity: 0.5;
276
+ }
277
+ :global(.reasoning-popover input:focus-visible) {
278
+ outline: 2px solid var(--color-ring);
279
+ outline-offset: 2px;
280
+ }
281
+ .reasoning-option {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 0.5rem;
285
+ }
286
+ </style>
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ // Per-run skill selection above the composer (docs/16 §4.2): metadata only, selected chars / budget.
3
+ // `changed` (a `skill_changed` rejection) opens the current text for re-confirmation (docs/14 §5).
4
+ import * as Select from '#lib/components/ui/select/index.js'
5
+ import { cn } from '#lib/utils.js'
6
+ import { getKitContext } from '../context'
7
+ import { isKitClientError } from '../ports'
8
+ import type { SkillDto, SkillMeta, SkillRef, SkillsPort } from '../ports'
9
+ import SkillPreviewDialog from '../skills/SkillPreviewDialog.svelte'
10
+
11
+ let {
12
+ port,
13
+ value,
14
+ budget,
15
+ changed,
16
+ onchange,
17
+ onresolved,
18
+ }: {
19
+ port: SkillsPort
20
+ value: SkillRef[]
21
+ /** `Budget.skills_chars` of the backend. */
22
+ budget: number
23
+ /** Skill id from a `skill_changed` rejection, or null. */
24
+ changed: string | null
25
+ onchange: (refs: SkillRef[]) => void
26
+ onresolved: () => void
27
+ } = $props()
28
+
29
+ const { i18n, notify } = getKitContext()
30
+ const { t } = i18n
31
+ let skills = $state<SkillMeta[]>([])
32
+ let reconfirm = $state<SkillDto | null>(null)
33
+ let reconfirmOpen = $state(false)
34
+
35
+ const ids = $derived(value.map((r) => r.id))
36
+ const chars = $derived(
37
+ skills.filter((s) => ids.includes(s.id)).reduce((sum, s) => sum + s.bodyChars, 0),
38
+ )
39
+
40
+ async function refresh() {
41
+ try {
42
+ skills = await port.list()
43
+ } catch (error) {
44
+ notify.toast('error', t(`errors.${isKitClientError(error) ? error.code : 'internal'}`))
45
+ }
46
+ }
47
+ void refresh()
48
+
49
+ // Kept refs keep their pinned revision: an edited skill is caught by `snapshot`, not silently re-pinned.
50
+ function pick(next: string[]) {
51
+ onchange(
52
+ next.flatMap((id) => {
53
+ const kept = value.find((r) => r.id === id)
54
+ const meta = skills.find((s) => s.id === id)
55
+ return kept ? [kept] : meta ? [{ id, revision: meta.revision }] : []
56
+ }),
57
+ )
58
+ }
59
+
60
+ $effect(() => {
61
+ if (changed !== null) void openChanged(changed)
62
+ })
63
+
64
+ async function openChanged(id: string) {
65
+ void refresh()
66
+ try {
67
+ reconfirm = await port.get(id)
68
+ reconfirmOpen = true
69
+ } catch {
70
+ // Deleted since it was selected: drop it.
71
+ onchange(value.filter((r) => r.id !== id))
72
+ notify.toast('info', t('skills.removed'))
73
+ onresolved()
74
+ }
75
+ }
76
+
77
+ function confirmChanged() {
78
+ const skill = reconfirm
79
+ if (skill)
80
+ onchange(value.map((r) => (r.id === skill.id ? { id: r.id, revision: skill.revision } : r)))
81
+ reconfirm = null
82
+ reconfirmOpen = false
83
+ onresolved()
84
+ }
85
+
86
+ // Closing the dialog without confirming deselects the changed skill.
87
+ $effect(() => {
88
+ if (!reconfirmOpen && reconfirm) {
89
+ const id = reconfirm.id
90
+ reconfirm = null
91
+ onchange(value.filter((r) => r.id !== id))
92
+ onresolved()
93
+ }
94
+ })
95
+ </script>
96
+
97
+ <div class="flex min-w-0 max-w-full flex-wrap items-center gap-2 text-kit-sm">
98
+ <Select.Root
99
+ type="multiple"
100
+ value={ids}
101
+ onValueChange={pick}
102
+ onOpenChange={(open) => {
103
+ if (open) void refresh()
104
+ }}
105
+ >
106
+ <Select.Trigger size="sm" class="max-w-full shrink-0" aria-label={t('skills.picker')}>
107
+ {value.length === 0
108
+ ? t('skills.picker_none')
109
+ : t('skills.picker_count', { count: value.length })}
110
+ </Select.Trigger>
111
+ <Select.Content>
112
+ <Select.Group>
113
+ {#each skills as skill (skill.id)}
114
+ <Select.Item value={skill.id} label={skill.name} />
115
+ {:else}
116
+ <p class="text-muted-foreground px-2 py-1">{t('skills.empty')}</p>
117
+ {/each}
118
+ </Select.Group>
119
+ </Select.Content>
120
+ </Select.Root>
121
+ <span
122
+ class={cn('max-w-full shrink-0 text-muted-foreground', chars > budget && 'text-destructive')}
123
+ >
124
+ {t('skills.budget', { chars, budget })}
125
+ </span>
126
+ </div>
127
+ <SkillPreviewDialog
128
+ bind:open={reconfirmOpen}
129
+ skill={reconfirm}
130
+ title={t('skills.changed_title')}
131
+ confirmLabel={t('skills.use')}
132
+ onconfirm={confirmChanged}
133
+ />
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ // One tool call collapsed to a line (docs/16 §4.2): calling, then its outcome and summary.
3
+ import { getKitContext } from '../context'
4
+ import type { ToolItem } from './run-session.svelte'
5
+
6
+ let { tool }: { tool: ToolItem } = $props()
7
+ const { t } = getKitContext().i18n
8
+ const name = $derived(tool.server ? `${tool.name} · ${tool.server}` : tool.name)
9
+ </script>
10
+
11
+ <p class="text-muted-foreground truncate text-kit-sm">
12
+ {#if tool.outcome === null}
13
+ {t('chat.tool.calling', { name })}
14
+ {:else if tool.outcome === 'denied'}
15
+ {t('chat.tool.denied', { name })}
16
+ {:else if tool.outcome === 'unknown'}
17
+ {t('chat.tool.unknown', { name })}
18
+ {:else if tool.outcome === 'error'}
19
+ {t('chat.tool.error', { name, summary: tool.summary })}
20
+ {:else}
21
+ {t('chat.tool.ok', { name, summary: tool.summary })}
22
+ {/if}
23
+ </p>
@@ -0,0 +1,216 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { Popover } from 'bits-ui'
4
+ import Settings2Icon from '@lucide/svelte/icons/settings-2'
5
+ import { Button } from '#lib/components/ui/button/index.js'
6
+ import type { KitI18n } from '../i18n'
7
+
8
+ let {
9
+ context = null,
10
+ i18n,
11
+ settings,
12
+ details,
13
+ open = $bindable(false),
14
+ }: {
15
+ context?: {
16
+ tokens: number | null
17
+ contextWindow: number | null
18
+ modelName: string
19
+ source: 'reported' | 'estimated' | 'unknown'
20
+ } | null
21
+ i18n: KitI18n
22
+ settings?: Snippet | undefined
23
+ details?: Snippet | undefined
24
+ open?: boolean
25
+ } = $props()
26
+ const t = (key: string) => i18n.t(key)
27
+ const percent = $derived(
28
+ context?.tokens != null && context.contextWindow != null && context.contextWindow > 0
29
+ ? Math.min(100, Math.max(0, (context.tokens / context.contextWindow) * 100))
30
+ : null,
31
+ )
32
+ const format = (value: number | null | undefined) =>
33
+ value == null ? t('chat.unknown') : value.toLocaleString(i18n.locale)
34
+ let trigger = $state<HTMLButtonElement | null>(null)
35
+ </script>
36
+
37
+ <Popover.Root bind:open>
38
+ <Popover.Trigger>
39
+ {#snippet child({ props })}
40
+ <Button
41
+ {...props}
42
+ bind:ref={trigger}
43
+ variant="ghost"
44
+ size="icon-sm"
45
+ aria-label={t('chat.usageSettings')}
46
+ title={t('chat.usageSettings')}
47
+ >
48
+ <Settings2Icon aria-hidden="true" />
49
+ </Button>
50
+ {/snippet}
51
+ </Popover.Trigger>
52
+ <Popover.Portal>
53
+ <Popover.Content
54
+ class="usage-popover"
55
+ side="top"
56
+ align="end"
57
+ sideOffset={8}
58
+ collisionPadding={12}
59
+ aria-label={t('chat.usageSettings')}
60
+ trapFocus={false}
61
+ preventScroll={false}
62
+ onEscapeKeydown={(event) => {
63
+ if (event.isComposing) event.preventDefault()
64
+ event.stopPropagation()
65
+ }}
66
+ onCloseAutoFocus={(event) => {
67
+ event.preventDefault()
68
+ trigger?.focus({ preventScroll: true })
69
+ }}
70
+ >
71
+ <section class="context-summary" aria-label={t('chat.contextUsage')}>
72
+ <h3>{t('chat.contextUsage')}</h3>
73
+ {#if percent !== null}
74
+ <meter min="0" max="100" value={percent} aria-label={t('chat.contextUsage')}></meter>
75
+ {/if}
76
+ <div class="context-values">
77
+ <span
78
+ >{format(context?.tokens)} / {format(context?.contextWindow)}{#if percent !== null}
79
+ ({Math.round(percent)}%){/if}</span
80
+ >
81
+ {#if context?.modelName}<span class="context-model" title={context.modelName}
82
+ >{context.modelName}</span
83
+ >{/if}
84
+ </div>
85
+ <small
86
+ >{t(
87
+ context?.source === 'reported'
88
+ ? 'chat.reported'
89
+ : context?.source === 'estimated'
90
+ ? 'chat.estimated'
91
+ : 'chat.unknown',
92
+ )}</small
93
+ >
94
+ </section>
95
+ {#if settings}<section class="usage-settings" aria-label={t('chat.runSettings')}>
96
+ <h3>{t('chat.runSettings')}</h3>
97
+ {@render settings()}
98
+ </section>{/if}
99
+ {#if details}<details class="usage-more">
100
+ <summary>{t('chat.usage')}</summary>
101
+ <div class="usage-more-content">{@render details()}</div>
102
+ </details>{/if}
103
+ </Popover.Content>
104
+ </Popover.Portal>
105
+ </Popover.Root>
106
+
107
+ <style>
108
+ /* Hallmark · component: usage-settings · theme: inherited Aether
109
+ * Pre-emit critique: Philosophy 5 · Hierarchy 5 · Execution 4 · Specificity 5 · Restraint 5 · Variety 4 */
110
+ :global(.usage-popover) {
111
+ box-sizing: border-box;
112
+ z-index: 210;
113
+ width: min(360px, calc(100vw - 24px));
114
+ max-height: min(70dvh, var(--bits-popover-content-available-height));
115
+ overflow-y: auto;
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 16px;
119
+ padding: 16px;
120
+ border: 1px solid var(--color-border);
121
+ border-radius: var(--kit-radius, var(--radius));
122
+ background: var(--color-popover);
123
+ color: var(--color-popover-foreground);
124
+ font-size: var(--kit-font-size-sm, 0.8125rem);
125
+ }
126
+ h3 {
127
+ margin: 0;
128
+ font: inherit;
129
+ font-weight: 500;
130
+ }
131
+ .context-summary {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 8px;
135
+ min-width: 0;
136
+ }
137
+ .context-values {
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: space-between;
141
+ gap: 12px;
142
+ color: var(--color-muted-foreground);
143
+ font-variant-numeric: tabular-nums;
144
+ }
145
+ .context-values > span:first-child {
146
+ flex: none;
147
+ }
148
+ .context-model {
149
+ min-width: 0;
150
+ overflow: hidden;
151
+ text-overflow: ellipsis;
152
+ white-space: nowrap;
153
+ }
154
+ small {
155
+ color: var(--color-muted-foreground);
156
+ font-size: 11px;
157
+ }
158
+ meter {
159
+ appearance: none;
160
+ width: 100%;
161
+ height: 6px;
162
+ border: 0;
163
+ border-radius: 999px;
164
+ background: var(--color-muted);
165
+ }
166
+ meter::-webkit-meter-bar {
167
+ border: 0;
168
+ border-radius: 999px;
169
+ background: var(--color-muted);
170
+ }
171
+ meter::-webkit-meter-optimum-value {
172
+ background: var(--color-primary);
173
+ border-radius: 999px;
174
+ }
175
+ meter::-moz-meter-bar {
176
+ background: var(--color-primary);
177
+ border-radius: 999px;
178
+ }
179
+ .usage-settings {
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 12px;
183
+ border-top: 1px solid var(--color-border);
184
+ padding-top: 12px;
185
+ }
186
+ .usage-more {
187
+ border-top: 1px solid var(--color-border);
188
+ padding-top: 12px;
189
+ }
190
+ summary {
191
+ cursor: pointer;
192
+ color: var(--color-muted-foreground);
193
+ }
194
+ summary:hover {
195
+ color: var(--color-foreground);
196
+ }
197
+ summary:focus-visible {
198
+ outline: 2px solid var(--color-ring);
199
+ outline-offset: 2px;
200
+ }
201
+ .usage-more-content {
202
+ padding-top: 12px;
203
+ min-width: 0;
204
+ overflow-wrap: anywhere;
205
+ }
206
+ @media (max-width: 375px) {
207
+ .context-values {
208
+ align-items: flex-start;
209
+ flex-direction: column;
210
+ gap: 4px;
211
+ }
212
+ .context-model {
213
+ max-width: 100%;
214
+ }
215
+ }
216
+ </style>