@ouraihub/aether-ui 0.1.0 → 0.1.2

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 (90) hide show
  1. package/package.json +3 -3
  2. package/src/chat/ApprovalCard.svelte +8 -2
  3. package/src/chat/ChatComposer.svelte +319 -0
  4. package/src/chat/ChatMessage.svelte +262 -0
  5. package/src/chat/ChatPanel.css +386 -0
  6. package/src/chat/ChatPanel.svelte +276 -1880
  7. package/src/chat/ChatPanelApprovals.svelte +28 -0
  8. package/src/chat/ChatPanelComposer.svelte +336 -0
  9. package/src/chat/ChatPanelDialogs.svelte +242 -0
  10. package/src/chat/ChatPanelHeader.svelte +108 -0
  11. package/src/chat/ChatPanelMessages.svelte +166 -0
  12. package/src/chat/ChatPanelMode.svelte +190 -0
  13. package/src/chat/ChatPanelState.svelte.ts +1 -0
  14. package/src/chat/ChatPanelToolbar.svelte +436 -0
  15. package/src/chat/ChatSurface.svelte +168 -0
  16. package/src/chat/ContextUsageStatus.svelte +97 -0
  17. package/src/chat/MessageItem.svelte +111 -214
  18. package/src/chat/ModelEffortPicker.svelte +326 -0
  19. package/src/chat/PickerEffort.svelte +71 -0
  20. package/src/chat/ReasoningControl.svelte +184 -0
  21. package/src/chat/ReasoningSlider.svelte +174 -0
  22. package/src/chat/SkillPicker.svelte +14 -6
  23. package/src/chat/UsageControl.svelte +223 -0
  24. package/src/chat/UsageDetails.svelte +44 -90
  25. package/src/chat/chat-attachments.svelte.ts +78 -0
  26. package/src/chat/chat-panel-history.svelte.ts +430 -0
  27. package/src/chat/chat-panel-models.svelte.ts +97 -0
  28. package/src/chat/chat-panel-props.ts +38 -0
  29. package/src/chat/context-status.ts +101 -0
  30. package/src/chat/icons/LICENSE-REMIX +186 -0
  31. package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
  32. package/src/chat/icons/PathIcon.svelte +8 -0
  33. package/src/chat/icons/effort-icons.ts +65 -0
  34. package/src/chat/icons/status-icons.ts +105 -0
  35. package/src/chat/index.ts +25 -1
  36. package/src/chat/model-effort.ts +177 -0
  37. package/src/chat/run-session-history.ts +56 -0
  38. package/src/chat/run-session-types.ts +69 -0
  39. package/src/chat/run-session.svelte.ts +73 -135
  40. package/src/chat/test-support/chat-fixture.ts +194 -0
  41. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  42. package/src/chat/test-support/run-session-fixture.ts +112 -0
  43. package/src/dev/Demo.svelte +3 -1
  44. package/src/i18n/en-chat.d.ts +1 -0
  45. package/src/i18n/en-chat.js +201 -0
  46. package/src/i18n/en.ts +58 -180
  47. package/src/i18n/index.ts +7 -2
  48. package/src/i18n/zh-CN-chat.d.ts +1 -0
  49. package/src/i18n/zh-CN-chat.js +190 -0
  50. package/src/i18n/zh-CN.ts +8 -153
  51. package/src/logs/LogPanel.svelte +9 -3
  52. package/src/logs/LogSettings.svelte +3 -1
  53. package/src/mcp/McpClientEditor.svelte +204 -0
  54. package/src/mcp/McpClientSettings.svelte +57 -130
  55. package/src/mcp/McpServerSettings.svelte +47 -11
  56. package/src/mcp/input.ts +2 -1
  57. package/src/ports/contract.ts +11 -2
  58. package/src/ports/fake-optional.ts +81 -0
  59. package/src/ports/fake.ts +90 -86
  60. package/src/ports/search.ts +11 -2
  61. package/src/ports/test-support/fake-fixture.ts +63 -0
  62. package/src/providers/EndpointKeysDialog.svelte +27 -13
  63. package/src/providers/EndpointModels.svelte +311 -0
  64. package/src/providers/EndpointPanel.svelte +33 -258
  65. package/src/providers/ModelPickerDialog.svelte +59 -16
  66. package/src/providers/ModelPricing.svelte +125 -0
  67. package/src/providers/ModelSelect.svelte +22 -8
  68. package/src/providers/ModelSettingsDialog.svelte +61 -126
  69. package/src/providers/ProviderDialogs.svelte +120 -0
  70. package/src/providers/ProviderList.svelte +159 -0
  71. package/src/providers/ProvidersSettings.svelte +67 -207
  72. package/src/providers/RequestConfigDialog.svelte +40 -12
  73. package/src/providers/SecretInput.svelte +11 -3
  74. package/src/providers/draft.ts +39 -13
  75. package/src/providers/edit-session.ts +31 -7
  76. package/src/providers/reasoning.ts +18 -4
  77. package/src/providers/revision.svelte.ts +3 -1
  78. package/src/providers/test-support/providers-fixture.ts +120 -0
  79. package/src/search/SearchAdvancedTools.svelte +182 -0
  80. package/src/search/SearchProviderCard.svelte +195 -0
  81. package/src/search/SearchSettings.svelte +62 -265
  82. package/src/search/settings-flow.ts +8 -3
  83. package/src/skills/SkillAdd.svelte +51 -18
  84. package/src/skills/SkillAuthoring.svelte +69 -20
  85. package/src/skills/SkillEditor.svelte +13 -3
  86. package/src/skills/SkillPreviewDialog.svelte +19 -5
  87. package/src/skills/SkillsSettings.svelte +11 -4
  88. package/src/skills/test-support/skills-fixture.ts +139 -0
  89. package/src/tauri/index.ts +23 -7
  90. package/src/web/index.ts +90 -25
@@ -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>
@@ -0,0 +1,336 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import XIcon from '@lucide/svelte/icons/x'
4
+ import { Button } from '#lib/components/ui/button/index.js'
5
+ import type { KitI18n } from '../i18n'
6
+ import type {
7
+ ContextUsage,
8
+ ImagePart,
9
+ ModelOption,
10
+ ModelRef,
11
+ PermissionMode,
12
+ SkillRef,
13
+ SkillsPort,
14
+ } from '../ports'
15
+ import ChatComposer from './ChatComposer.svelte'
16
+ import ChatPanelMode from './ChatPanelMode.svelte'
17
+ import ChatPanelToolbar from './ChatPanelToolbar.svelte'
18
+ import ModelEffortPicker from './ModelEffortPicker.svelte'
19
+ import SkillPicker from './SkillPicker.svelte'
20
+ import UsageDetails from './UsageDetails.svelte'
21
+ import type { PickerModel, PickerValue } from './model-effort'
22
+ import { settingsTarget } from '../providers/revision.svelte'
23
+ import type { RunSession } from './run-session.svelte'
24
+
25
+ type Attached = ImagePart & { preview: string }
26
+ type Recovery = {
27
+ draft: string
28
+ images: Attached[]
29
+ droppedKey: string | null
30
+ contextKey: string | null
31
+ }
32
+
33
+ let {
34
+ i18n,
35
+ session,
36
+ purpose,
37
+ draft = $bindable(''),
38
+ canSend,
39
+ activeContext,
40
+ images,
41
+ vision,
42
+ recovery,
43
+ webSearch = $bindable(false),
44
+ generateImage = $bindable(false),
45
+ toolbar,
46
+ onToolbarChange,
47
+ paintingModel,
48
+ capability,
49
+ historyBusy,
50
+ onNewConversation,
51
+ attach,
52
+ onDraftInput,
53
+ onRemoveImage,
54
+ onRecoverDraft,
55
+ onDropContext,
56
+ hasSearch,
57
+ selected,
58
+ skillsBudget,
59
+ skillsPort,
60
+ onSkillsChange,
61
+ onSkillResolved,
62
+ mode,
63
+ allowedModes,
64
+ chooseMode,
65
+ reasoningOpen = $bindable(false),
66
+ pickerModels,
67
+ pickerValue,
68
+ pickerDefault,
69
+ pickerRefreshing,
70
+ pickerNotice,
71
+ pickerDisabled,
72
+ onPick,
73
+ onSetDefault,
74
+ onRefresh,
75
+ currentContext,
76
+ picked,
77
+ onOpenSettings,
78
+ onSend,
79
+ onStop,
80
+ hasImages,
81
+ override,
82
+ }: {
83
+ i18n: KitI18n
84
+ session: RunSession
85
+ purpose: string
86
+ draft?: string
87
+ canSend: boolean
88
+ activeContext: { kind: string; id: string; label: string } | null
89
+ images: Attached[]
90
+ vision: boolean
91
+ recovery: Recovery | null
92
+ webSearch?: boolean
93
+ generateImage?: boolean
94
+ toolbar?: string[] | undefined
95
+ onToolbarChange?: ((ids: string[]) => void) | undefined
96
+ paintingModel: boolean
97
+ capability: ModelOption | null
98
+ historyBusy: boolean
99
+ onNewConversation: () => void
100
+ attach: (files: FileList | File[] | null | undefined) => Promise<void>
101
+ onDraftInput: () => void
102
+ onRemoveImage: (index: number) => void
103
+ onRecoverDraft: () => void
104
+ onDropContext: () => void
105
+ hasSearch: boolean
106
+ selected: SkillRef[]
107
+ skillsBudget: number
108
+ skillsPort?: SkillsPort | undefined
109
+ onSkillsChange: (refs: SkillRef[]) => void
110
+ onSkillResolved: () => void
111
+ mode: PermissionMode
112
+ allowedModes: PermissionMode[]
113
+ chooseMode: (mode: PermissionMode) => Promise<void>
114
+ reasoningOpen?: boolean
115
+ pickerModels: PickerModel[]
116
+ pickerValue: PickerValue | null
117
+ pickerDefault: PickerValue | null
118
+ pickerRefreshing: boolean
119
+ pickerNotice: string
120
+ pickerDisabled: boolean
121
+ onPick: (value: PickerValue) => void
122
+ onSetDefault?: ((value: PickerValue) => void) | undefined
123
+ onRefresh: (force: boolean) => void
124
+ currentContext: ContextUsage | null
125
+ picked: ModelRef | null
126
+ onOpenSettings?: (() => void) | undefined
127
+ onSend: () => void
128
+ onStop: () => void
129
+ hasImages: (files: FileList | null | undefined) => boolean
130
+ override?: Snippet | undefined
131
+ } = $props()
132
+ const t = $derived(i18n.t)
133
+ </script>
134
+
135
+ <ChatComposer
136
+ bind:value={draft}
137
+ label={t('chat.input')}
138
+ placeholder={t('chat.placeholder')}
139
+ submitLabel={t('chat.send')}
140
+ stopLabel={t('chat.stop')}
141
+ disabled={!canSend}
142
+ running={session.running}
143
+ expandLabel={t('chat.expandInput')}
144
+ collapseLabel={t('chat.collapseInput')}
145
+ onsubmit={onSend}
146
+ onstop={onStop}
147
+ oninput={onDraftInput}
148
+ onpaste={(event) => {
149
+ const files = event.clipboardData?.files
150
+ if (!hasImages(files)) return
151
+ event.preventDefault()
152
+ void attach(files)
153
+ }}
154
+ onfiles={(files) => void attach(files)}
155
+ {override}
156
+ >
157
+ {#snippet top()}
158
+ {#if activeContext}
159
+ <div class="context-chip">
160
+ <span title={activeContext.label}>{activeContext.label}</span><Button
161
+ type="button"
162
+ variant="ghost"
163
+ size="icon-sm"
164
+ aria-label={t('chat.context.remove', { label: activeContext.label })}
165
+ title={t('chat.context.remove', { label: activeContext.label })}
166
+ onclick={onDropContext}><XIcon /></Button
167
+ >
168
+ </div>
169
+ {/if}
170
+ {#if images.length > 0}
171
+ <ul class="image-tray">
172
+ {#each images as image, index (image.preview)}
173
+ <li>
174
+ <img src={image.preview} alt="" /><Button
175
+ type="button"
176
+ variant="secondary"
177
+ size="icon-xs"
178
+ aria-label={t('chat.removeImage', { index: index + 1 })}
179
+ title={t('chat.removeImage', { index: index + 1 })}
180
+ onclick={() => onRemoveImage(index)}><XIcon /></Button
181
+ >
182
+ </li>
183
+ {/each}
184
+ </ul>
185
+ {#if !vision}<p class="search-status" role="status">{t('chat.visionUnsupported')}</p>{/if}
186
+ {/if}
187
+ {#if recovery}
188
+ <div class="draft-recovery" role="status">
189
+ <span>{t('chat.draftRecoverable')}</span><Button
190
+ type="button"
191
+ variant="outline"
192
+ size="sm"
193
+ disabled={draft !== '' || images.length > 0}
194
+ onclick={onRecoverDraft}>{t('chat.restoreDraft')}</Button
195
+ >
196
+ </div>
197
+ {/if}
198
+ {#if hasSearch && purpose === 'chat' && session.search.status === 'searching'}<p
199
+ class="search-status"
200
+ role="status"
201
+ >
202
+ {t('chat.searching')}
203
+ </p>{/if}
204
+ {/snippet}
205
+ {#snippet tools()}
206
+ <ChatPanelToolbar
207
+ {i18n}
208
+ running={session.running}
209
+ {historyBusy}
210
+ {vision}
211
+ {paintingModel}
212
+ {capability}
213
+ {purpose}
214
+ {toolbar}
215
+ {onToolbarChange}
216
+ {hasSearch}
217
+ newConversation={onNewConversation}
218
+ {attach}
219
+ bind:webSearch
220
+ bind:generateImage
221
+ />
222
+ {#if skillsPort}<div class="skills-control">
223
+ <SkillPicker
224
+ port={skillsPort}
225
+ value={selected}
226
+ budget={skillsBudget}
227
+ changed={session.skillChanged}
228
+ onchange={onSkillsChange}
229
+ onresolved={onSkillResolved}
230
+ />
231
+ </div>{/if}
232
+ <ModelEffortPicker
233
+ models={pickerModels}
234
+ value={pickerValue}
235
+ defaultValue={pickerDefault}
236
+ disabled={session.running || pickerDisabled}
237
+ refreshing={pickerRefreshing}
238
+ notice={pickerNotice}
239
+ {i18n}
240
+ bind:open={reasoningOpen}
241
+ onselect={onPick}
242
+ onsetdefault={onSetDefault}
243
+ onrefresh={onRefresh}
244
+ onopensettings={onOpenSettings && picked
245
+ ? () => {
246
+ settingsTarget.value = { endpointId: picked.endpointId, modelId: picked.modelId }
247
+ onOpenSettings()
248
+ }
249
+ : undefined}
250
+ />
251
+ {/snippet}
252
+ {#snippet actions()}
253
+ {#if purpose !== 'chat'}
254
+ <ChatPanelMode {i18n} {mode} {allowedModes} running={session.running} {chooseMode} />
255
+ {/if}
256
+ <UsageDetails
257
+ context={currentContext}
258
+ contextWindow={pickerModels.find((model) => model.key === pickerValue?.key)?.contextWindow}
259
+ modelName={capability?.name ?? capability?.modelId ?? ''}
260
+ turns={session.turns}
261
+ usage={session.turns.at(-1)?.usage ?? null}
262
+ total={session.cumulativeUsage}
263
+ cost={session.turns.at(-1)?.cost ?? null}
264
+ totalCost={session.cumulativeCost}
265
+ />
266
+ {/snippet}
267
+ </ChatComposer>
268
+
269
+ <style>
270
+ .context-chip {
271
+ display: flex;
272
+ align-items: center;
273
+ align-self: flex-start;
274
+ gap: 0.25rem;
275
+ max-width: calc(100% - 2rem);
276
+ padding-left: 0.5rem;
277
+ background: var(--color-muted);
278
+ border-radius: var(--kit-radius, var(--radius));
279
+ }
280
+ .context-chip span {
281
+ min-width: 0;
282
+ overflow: hidden;
283
+ text-overflow: ellipsis;
284
+ white-space: nowrap;
285
+ }
286
+ .context-chip :global(button) {
287
+ flex-shrink: 0;
288
+ }
289
+ .image-tray {
290
+ display: flex;
291
+ flex-wrap: wrap;
292
+ gap: 0.375rem;
293
+ margin: 0;
294
+ padding: 0;
295
+ list-style: none;
296
+ }
297
+ .image-tray li {
298
+ position: relative;
299
+ }
300
+ .image-tray img {
301
+ width: 3.5rem;
302
+ height: 3.5rem;
303
+ object-fit: cover;
304
+ border: 1px solid var(--color-border);
305
+ border-radius: var(--kit-radius, var(--radius));
306
+ }
307
+ .image-tray :global(button) {
308
+ position: absolute;
309
+ top: -0.375rem;
310
+ right: -0.375rem;
311
+ }
312
+ .skills-control {
313
+ min-width: 0;
314
+ max-width: 100%;
315
+ }
316
+ .skills-control :global(div:first-child) {
317
+ flex-wrap: wrap;
318
+ min-width: 0;
319
+ }
320
+ .skills-control :global([data-slot='select-trigger']) {
321
+ width: auto;
322
+ max-width: 100%;
323
+ }
324
+ .search-status,
325
+ .draft-recovery {
326
+ margin: 0;
327
+ color: var(--color-muted-foreground);
328
+ font-size: var(--kit-font-size-sm, 0.8125rem);
329
+ }
330
+ .draft-recovery {
331
+ display: flex;
332
+ flex-wrap: wrap;
333
+ align-items: center;
334
+ gap: 0.5rem;
335
+ }
336
+ </style>
@@ -0,0 +1,242 @@
1
+ <script lang="ts">
2
+ import { Button } from '#lib/components/ui/button/index.js'
3
+ import { Input } from '#lib/components/ui/input/index.js'
4
+ import { Textarea } from '#lib/components/ui/textarea/index.js'
5
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
6
+ import * as Field from '#lib/components/ui/field/index.js'
7
+ import ModelSelect from '../providers/ModelSelect.svelte'
8
+ import type { KitI18n } from '../i18n'
9
+ import type { ConversationClient, ConversationSummary, ModelOption, ModelRef } from '../ports'
10
+ let {
11
+ i18n,
12
+ history,
13
+ session,
14
+ conversations,
15
+ title,
16
+ historyLoading,
17
+ historyBusy,
18
+ historyError,
19
+ retryHistory,
20
+ open,
21
+ rename,
22
+ remove,
23
+ actionBusy,
24
+ replyDialog,
25
+ modelOptions,
26
+ modelError,
27
+ noModels,
28
+ validReplyModel,
29
+ submitReply,
30
+ reloadModels,
31
+ historyOpen = $bindable(false),
32
+ renaming = $bindable(false),
33
+ renameTitle = $bindable(''),
34
+ replyOpen = $bindable(false),
35
+ editDraft = $bindable(''),
36
+ replyModel = $bindable<ModelRef | null>(null),
37
+ targetLanguage = $bindable(''),
38
+ replyError,
39
+ }: {
40
+ i18n: KitI18n
41
+ history: ConversationClient | undefined
42
+ session: { running: boolean; conversationId: string | null }
43
+ conversations: ConversationSummary[]
44
+ title: string
45
+ historyLoading: boolean
46
+ historyBusy: boolean
47
+ historyError: string | null
48
+ retryHistory: () => Promise<void>
49
+ open: (id: string) => Promise<void>
50
+ rename: () => Promise<void>
51
+ remove: () => Promise<void>
52
+ actionBusy: boolean
53
+ replyDialog: 'edit' | 'translate' | null
54
+ modelOptions: ModelOption[] | null
55
+ modelError: string | null
56
+ noModels: boolean
57
+ validReplyModel: boolean
58
+ submitReply: () => Promise<void>
59
+ reloadModels: () => void
60
+ historyOpen?: boolean
61
+ renaming?: boolean
62
+ renameTitle?: string
63
+ replyOpen?: boolean
64
+ editDraft?: string
65
+ replyModel?: ModelRef | null
66
+ targetLanguage?: string
67
+ replyError: string | null
68
+ } = $props()
69
+ const t = $derived(i18n.t)
70
+ </script>
71
+
72
+ {#if history}
73
+ <Dialog.Root bind:open={historyOpen}>
74
+ <Dialog.Content showCloseButton={false}>
75
+ <Dialog.Header><Dialog.Title>{t('chat.history')}</Dialog.Title></Dialog.Header>
76
+ {#if historyLoading}<p role="status">{t('chat.historyLoading')}</p>{/if}
77
+ {#if historyError}<p role="alert">
78
+ {t('chat.historyFailed')}: {historyError}<Button
79
+ variant="outline"
80
+ size="sm"
81
+ disabled={historyBusy}
82
+ onclick={() => void retryHistory()}>{t('chat.retryHistory')}</Button
83
+ >
84
+ </p>{/if}
85
+ <div class="history-list">
86
+ {#each conversations as c (c.id)}<Button
87
+ variant="ghost"
88
+ title={c.title}
89
+ disabled={historyBusy || session.running}
90
+ onclick={() => void open(c.id)}><span>{c.title}</span></Button
91
+ >{/each}
92
+ </div>
93
+ {#if session.conversationId}
94
+ {#if renaming}
95
+ <form
96
+ onsubmit={(e) => {
97
+ e.preventDefault()
98
+ void rename()
99
+ }}
100
+ >
101
+ <Field.FieldGroup
102
+ ><Field.Field
103
+ ><Field.FieldLabel for="chat-conversation-title"
104
+ >{t('chat.conversationTitle')}</Field.FieldLabel
105
+ ><Input
106
+ id="chat-conversation-title"
107
+ bind:value={renameTitle}
108
+ disabled={historyBusy}
109
+ /></Field.Field
110
+ ><Button type="submit" disabled={historyBusy || renameTitle.trim() === ''}
111
+ >{t('common.confirm')}</Button
112
+ ></Field.FieldGroup
113
+ >
114
+ </form>
115
+ {:else}<Button
116
+ variant="outline"
117
+ disabled={historyBusy || session.running}
118
+ onclick={() => {
119
+ renameTitle = title
120
+ renaming = true
121
+ }}>{t('chat.rename')}</Button
122
+ >{/if}
123
+ <Button
124
+ variant="destructive"
125
+ disabled={historyBusy || session.running}
126
+ onclick={() => void remove()}>{t('chat.delete')}</Button
127
+ >
128
+ {/if}
129
+ <Dialog.Footer
130
+ ><Button variant="ghost" onclick={() => (historyOpen = false)}>{t('chat.close')}</Button
131
+ ></Dialog.Footer
132
+ >
133
+ </Dialog.Content>
134
+ </Dialog.Root>
135
+ {/if}
136
+ <Dialog.Root bind:open={replyOpen}>
137
+ <Dialog.Content class="reply-dialog" showCloseButton={false}>
138
+ <Dialog.Header>
139
+ <Dialog.Title>{t(replyDialog === 'edit' ? 'chat.editReply' : 'chat.translate')}</Dialog.Title>
140
+ <Dialog.Description
141
+ >{t(
142
+ replyDialog === 'edit' ? 'chat.editReplyHint' : 'chat.candidateHint',
143
+ )}</Dialog.Description
144
+ >
145
+ </Dialog.Header>
146
+ <form
147
+ onsubmit={(event) => {
148
+ event.preventDefault()
149
+ void submitReply()
150
+ }}
151
+ >
152
+ <Field.FieldGroup>
153
+ {#if replyDialog === 'edit'}
154
+ <Field.Field
155
+ ><Field.FieldLabel for="chat-reply-edit">{t('chat.replyText')}</Field.FieldLabel
156
+ ><Textarea
157
+ id="chat-reply-edit"
158
+ bind:value={editDraft}
159
+ rows={8}
160
+ maxlength={24_000}
161
+ disabled={actionBusy}
162
+ /></Field.Field
163
+ >
164
+ {:else}
165
+ <Field.Field
166
+ ><Field.FieldLabel>{t('chat.replyModel')}</Field.FieldLabel><ModelSelect
167
+ value={replyModel}
168
+ options={modelOptions}
169
+ disabled={actionBusy || modelError !== null}
170
+ onchange={(value) => (replyModel = value)}
171
+ /></Field.Field
172
+ >
173
+ {#if noModels}<p role="status">{t('chat.noModels')}</p>{/if}
174
+ {#if modelError}<p role="alert">
175
+ {modelError}<Button type="button" variant="outline" size="sm" onclick={reloadModels}
176
+ >{t('chat.retryModels')}</Button
177
+ >
178
+ </p>{/if}
179
+ <Field.Field
180
+ ><Field.FieldLabel for="chat-target-language"
181
+ >{t('chat.targetLanguage')}</Field.FieldLabel
182
+ ><Input
183
+ id="chat-target-language"
184
+ bind:value={targetLanguage}
185
+ maxlength={80}
186
+ disabled={actionBusy}
187
+ /></Field.Field
188
+ >
189
+ {/if}
190
+ {#if replyError}<p role="alert" class="text-destructive">{replyError}</p>{/if}
191
+ <Dialog.Footer>
192
+ <Button
193
+ type="button"
194
+ variant="ghost"
195
+ disabled={actionBusy}
196
+ onclick={() => (replyOpen = false)}>{t('common.cancel')}</Button
197
+ >
198
+ <Button
199
+ type="submit"
200
+ disabled={actionBusy ||
201
+ (replyDialog === 'edit'
202
+ ? !editDraft.trim() || editDraft.length > 24_000
203
+ : !validReplyModel || !targetLanguage.trim())}>{t('common.confirm')}</Button
204
+ >
205
+ </Dialog.Footer>
206
+ </Field.FieldGroup>
207
+ </form>
208
+ </Dialog.Content>
209
+ </Dialog.Root>
210
+
211
+ <style>
212
+ .history-list {
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: 0.25rem;
216
+ max-height: 40vh;
217
+ overflow-y: auto;
218
+ min-width: 0;
219
+ }
220
+ .history-list :global(button) {
221
+ justify-content: flex-start;
222
+ min-width: 0;
223
+ }
224
+ .history-list span {
225
+ min-width: 0;
226
+ overflow: hidden;
227
+ text-overflow: ellipsis;
228
+ white-space: nowrap;
229
+ }
230
+ :global(.reply-dialog) {
231
+ max-height: calc(100dvh - 2rem);
232
+ overflow-y: auto;
233
+ }
234
+ :global(.reply-dialog form),
235
+ :global(.reply-dialog [data-slot='field-group']) {
236
+ min-width: 0;
237
+ }
238
+ :global(.reply-dialog textarea) {
239
+ max-height: 45dvh;
240
+ overflow-y: auto;
241
+ }
242
+ </style>
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import XIcon from '@lucide/svelte/icons/x'
3
+ import HistoryIcon from '@lucide/svelte/icons/history'
4
+ import { Button } from '#lib/components/ui/button/index.js'
5
+ import * as Alert from '#lib/components/ui/alert/index.js'
6
+ import type { KitI18n } from '../i18n'
7
+ import type { ConversationClient } from '../ports'
8
+ import type { RunSession } from './run-session.svelte'
9
+
10
+ let {
11
+ i18n,
12
+ title,
13
+ session,
14
+ history,
15
+ historyBusy,
16
+ historyOpen,
17
+ historyError,
18
+ onHistory,
19
+ onClose,
20
+ onRetryHistory,
21
+ }: {
22
+ i18n: KitI18n
23
+ title: string
24
+ session: RunSession
25
+ history: ConversationClient | undefined
26
+ historyBusy: boolean
27
+ historyOpen: boolean
28
+ historyError: string | null
29
+ onHistory: () => void
30
+ onClose?: (() => void) | undefined
31
+ onRetryHistory: () => void
32
+ } = $props()
33
+ const t = $derived(i18n.t)
34
+ </script>
35
+
36
+ <header class="chat-header">
37
+ <div class="title-row">
38
+ <h2>{title}</h2>
39
+ {#if history}<Button
40
+ variant="ghost"
41
+ size="icon-sm"
42
+ disabled={session.running || historyBusy}
43
+ aria-label={t('chat.history')}
44
+ title={t('chat.history')}
45
+ onclick={onHistory}><HistoryIcon /></Button
46
+ >{/if}
47
+ {#if onClose}<Button
48
+ variant="ghost"
49
+ size="icon-sm"
50
+ aria-label={t('chat.close')}
51
+ title={t('chat.close')}
52
+ onclick={onClose}><XIcon /></Button
53
+ >{/if}
54
+ </div>
55
+ </header>
56
+ {#if historyError && !historyOpen}
57
+ <Alert.Root variant="destructive">
58
+ <Alert.Title>{t('chat.historyFailed')}</Alert.Title>
59
+ <Alert.Description
60
+ >{historyError}<Button
61
+ variant="outline"
62
+ size="sm"
63
+ disabled={historyBusy || session.running}
64
+ onclick={onRetryHistory}>{t('chat.retryHistory')}</Button
65
+ ></Alert.Description
66
+ >
67
+ </Alert.Root>
68
+ {/if}
69
+ {#if session.actionError}<p role="alert" class="text-destructive text-kit-sm">
70
+ {t(`errors.${session.actionError}`)}
71
+ </p>{/if}
72
+ {#if session.historyRefreshError}<p role="alert" class="text-destructive text-kit-sm">
73
+ {t('chat.replyRefreshFailed')}<Button
74
+ variant="outline"
75
+ size="sm"
76
+ disabled={session.running || historyBusy}
77
+ onclick={() => session.conversationId && onRetryHistory()}>{t('chat.retryHistory')}</Button
78
+ >
79
+ </p>{/if}
80
+
81
+ <style>
82
+ .chat-header {
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 0.5rem;
86
+ flex: none;
87
+ min-width: 0;
88
+ }
89
+ .title-row {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 0.25rem;
93
+ min-width: 0;
94
+ }
95
+ .title-row h2 {
96
+ flex: 1;
97
+ min-width: 0;
98
+ margin: 0;
99
+ font: inherit;
100
+ font-weight: 600;
101
+ overflow: hidden;
102
+ text-overflow: ellipsis;
103
+ white-space: nowrap;
104
+ }
105
+ .title-row :global(button) {
106
+ flex-shrink: 0;
107
+ }
108
+ </style>