@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,76 @@
1
+ <script lang="ts">
2
+ import type { ContextUsage, Cost, TokenUsage } from '../ports'
3
+ import { getKitContext } from '../context'
4
+ import UsageControl from './UsageControl.svelte'
5
+ interface Props {
6
+ context: ContextUsage | null
7
+ usage: TokenUsage | null
8
+ total: TokenUsage
9
+ /** Last turn's cost; null when its model has no pricing or a round lacked token counts. */
10
+ cost: Cost | null
11
+ /** Session total per currency. */
12
+ totalCost: Cost[]
13
+ }
14
+ let {
15
+ context,
16
+ usage,
17
+ total,
18
+ cost,
19
+ totalCost,
20
+ modelName = '',
21
+ }: Props & { modelName?: string } = $props()
22
+ const { i18n } = getKitContext()
23
+ const { t } = i18n
24
+ const fields = [
25
+ 'inputTokens',
26
+ 'outputTokens',
27
+ 'cacheReadTokens',
28
+ 'cacheWriteTokens',
29
+ 'reasoningTokens',
30
+ ] as const
31
+ const money = ({ amount, currency }: Cost) =>
32
+ `${currency === 'CNY' ? '¥' : '$'}${amount.toFixed(4)}`
33
+ </script>
34
+
35
+ <UsageControl context={context ? { ...context, modelName } : null} {i18n}>
36
+ {#snippet details()}
37
+ <table>
38
+ <thead
39
+ ><tr
40
+ ><th scope="col">{t('chat.usage')}</th><th scope="col">{t('chat.currentUsage')}</th><th
41
+ scope="col">{t('chat.totalUsage')}</th
42
+ ></tr
43
+ ></thead
44
+ >
45
+ <tbody
46
+ >{#each fields as field (field)}<tr
47
+ ><th scope="row">{t(`chat.usage.${field}`)}</th><td
48
+ >{usage?.[field] ?? t('chat.unknown')}</td
49
+ ><td>{total[field] ?? t('chat.unknown')}</td></tr
50
+ >{/each}<tr
51
+ ><th scope="row">{t('chat.cost')}</th><td>{cost ? money(cost) : t('chat.unknown')}</td><td
52
+ >{totalCost.length ? totalCost.map(money).join(' + ') : t('chat.unknown')}</td
53
+ ></tr
54
+ ></tbody
55
+ >
56
+ </table>
57
+ <p>{t('chat.usageHint')}</p>
58
+ {/snippet}
59
+ </UsageControl>
60
+
61
+ <style>
62
+ p {
63
+ margin: 0;
64
+ overflow-wrap: anywhere;
65
+ }
66
+ table {
67
+ width: 100%;
68
+ border-collapse: collapse;
69
+ }
70
+ th,
71
+ td {
72
+ padding: 0.25rem;
73
+ text-align: left;
74
+ overflow-wrap: anywhere;
75
+ }
76
+ </style>
@@ -0,0 +1,74 @@
1
+ import type { KitI18n } from '../i18n'
2
+ import type { KitNotify } from '../context'
3
+ import type { ImagePart } from '../ports'
4
+
5
+ export type AttachedImage = ImagePart & { preview: string }
6
+
7
+ export function hasImages(files: FileList | null | undefined): boolean {
8
+ return [...(files ?? [])].some((file) => file.type.startsWith('image/'))
9
+ }
10
+
11
+ function readBase64(file: File): Promise<string> {
12
+ const { promise, resolve, reject } = Promise.withResolvers<string>()
13
+ const reader = new FileReader()
14
+ reader.onload = () => resolve(String(reader.result).slice(String(reader.result).indexOf(',') + 1))
15
+ reader.onerror = () => reject(reader.error ?? new Error('read failed'))
16
+ reader.readAsDataURL(file)
17
+ return promise
18
+ }
19
+
20
+ /** Owns attachment reads and previews; the panel transfers images to run/draft recovery. */
21
+ export class ChatAttachments {
22
+ images = $state<AttachedImage[]>([])
23
+ reading = $state(0)
24
+
25
+ constructor(
26
+ private readonly canVision: () => boolean,
27
+ private readonly notify: KitNotify,
28
+ private readonly i18n: KitI18n,
29
+ ) {}
30
+
31
+ remove = (index: number): void => {
32
+ const [image] = this.images.splice(index, 1)
33
+ if (image) URL.revokeObjectURL(image.preview)
34
+ }
35
+
36
+ attach = async (files: FileList | File[] | null | undefined): Promise<void> => {
37
+ const picked = [...(files ?? [])].filter((file) => file.type.startsWith('image/'))
38
+ if (picked.length === 0) return
39
+ const { t } = this.i18n
40
+ if (!this.canVision()) this.notify.toast('error', t('chat.visionUnsupported'))
41
+ else if (
42
+ picked.some(
43
+ (file) => !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(file.type),
44
+ )
45
+ )
46
+ this.notify.toast('error', t('chat.imageType'))
47
+ else if (picked.some((file) => file.size > 4 * 1024 * 1024))
48
+ this.notify.toast('error', t('chat.imageSize'))
49
+ else if (this.images.length + picked.length > 4)
50
+ this.notify.toast('error', t('chat.imageLimit', { max: 4 }))
51
+ else {
52
+ this.reading++
53
+ try {
54
+ const read = await Promise.all(
55
+ picked.map(async (file) => ({ mime: file.type, data: await readBase64(file), file })),
56
+ )
57
+ if (this.images.length + read.length > 4)
58
+ this.notify.toast('error', t('chat.imageLimit', { max: 4 }))
59
+ else
60
+ this.images.push(
61
+ ...read.map(({ mime, data, file }) => ({
62
+ mime,
63
+ data,
64
+ preview: URL.createObjectURL(file),
65
+ })),
66
+ )
67
+ } catch {
68
+ this.notify.toast('error', t('errors.internal'))
69
+ } finally {
70
+ this.reading--
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,430 @@
1
+ import { SvelteMap } from 'svelte/reactivity'
2
+ import type { KitNotify } from '../context'
3
+ import type {
4
+ Conversation,
5
+ ConversationClient,
6
+ ConversationSummary,
7
+ ModelOption,
8
+ ModelRef,
9
+ } from '../ports'
10
+ import type { ChatTurn, RunSession } from './run-session.svelte'
11
+
12
+ type Translate = (key: string, values?: Record<string, string | number>) => string
13
+
14
+ type HistoryDependencies = {
15
+ history: ConversationClient | undefined
16
+ getSession: () => RunSession | null
17
+ getModelOptions: () => ModelOption[] | null
18
+ getModelError: () => string | null
19
+ setDraft: (value: string) => void
20
+ bumpDraftRevision: () => void
21
+ setFullAccess: (value: boolean) => void
22
+ getScope: () => string | undefined
23
+ getLocale: () => string
24
+ onConversationChange?: ((id: string | null) => void) | undefined
25
+ bumpScrollRevision: () => void
26
+ notify: KitNotify
27
+ t: Translate
28
+ errorText: (error: unknown) => string
29
+ consumeBase: () => Parameters<RunSession['translate']>[0]
30
+ discardRecovery: () => void
31
+ }
32
+
33
+ export class ChatPanelHistoryState {
34
+ readonly history: ConversationClient | undefined
35
+ conversations = $state<ConversationSummary[]>([])
36
+ shownSummary = $state<ConversationSummary | null>(null)
37
+ historyOpen = $state(false)
38
+ historyBusy = $state(false)
39
+ historyLoading = $state(false)
40
+ historyError = $state<string | null>(null)
41
+ retryHistory = $state<() => Promise<void>>(async () => {
42
+ await this.refresh()
43
+ })
44
+ renameTitle = $state('')
45
+ renaming = $state(false)
46
+ restored = $state(false)
47
+ restoreBlocked = $state(false)
48
+ replyDialog = $state<'edit' | 'translate' | null>(null)
49
+ replyOpen = $state(false)
50
+ sourceReply = $state<ChatTurn | null>(null)
51
+ editDraft = $state('')
52
+ expectedText = ''
53
+ targetLanguage = $state('')
54
+ replyModel = $state<ModelRef | null>(null)
55
+ replyError = $state<string | null>(null)
56
+ viewGeneration = $state(0)
57
+ disposed = $state(false)
58
+ #listGeneration = 0
59
+ #reported: string | null
60
+ readonly #deps: HistoryDependencies
61
+
62
+ constructor(deps: HistoryDependencies, initialConversationId: string | null) {
63
+ this.#deps = deps
64
+ this.history = deps.history
65
+ this.#reported = initialConversationId
66
+ this.retryHistory = this.refresh
67
+ }
68
+
69
+ get title() {
70
+ const session = this.#deps.getSession()
71
+ return (
72
+ (this.shownSummary?.id === session?.conversationId ? this.shownSummary?.title : null) ??
73
+ this.conversations.find((conversation) => conversation.id === session?.conversationId)
74
+ ?.title ??
75
+ this.#deps.t('chat.newConversation')
76
+ )
77
+ }
78
+
79
+ get groups() {
80
+ const grouped = new SvelteMap<string, ChatTurn[]>()
81
+ for (const turn of this.#deps.getSession()?.turns ?? []) {
82
+ const replies = grouped.get(turn.questionId)
83
+ if (replies) replies.push(turn)
84
+ else grouped.set(turn.questionId, [turn])
85
+ }
86
+ return [...grouped.entries()]
87
+ }
88
+
89
+ get actionBusy() {
90
+ return this.historyBusy || this.#deps.getSession()?.running === true || this.restoreBlocked
91
+ }
92
+
93
+ get validReplyModel() {
94
+ const model = this.replyModel
95
+ return (
96
+ this.#deps
97
+ .getModelOptions()
98
+ ?.some(
99
+ (option) => option.endpointId === model?.endpointId && option.modelId === model?.modelId,
100
+ ) === true
101
+ )
102
+ }
103
+
104
+ get isRestored() {
105
+ return this.restored
106
+ }
107
+
108
+ dispose = () => {
109
+ this.disposed = true
110
+ this.viewGeneration++
111
+ this.#listGeneration++
112
+ this.#deps.setFullAccess(false)
113
+ this.#deps.getSession()?.dispose()
114
+ }
115
+
116
+ syncConversation = () => {
117
+ const session = this.#deps.getSession()
118
+ const id = session?.conversationId ?? null
119
+ if (!this.restored || this.restoreBlocked || id === this.#reported) return
120
+ this.#reported = id
121
+ this.#deps.onConversationChange?.(id)
122
+ }
123
+
124
+ refresh = async () => {
125
+ if (!this.history) return
126
+ const generation = ++this.#listGeneration
127
+ this.historyLoading = true
128
+ try {
129
+ const next = await this.history.list(this.#deps.getScope())
130
+ if (this.disposed || generation !== this.#listGeneration) return
131
+ this.conversations = next
132
+ if (this.retryHistory === this.refresh) this.historyError = null
133
+ } catch (error) {
134
+ if (!this.disposed && generation === this.#listGeneration && !this.restoreBlocked) {
135
+ this.historyError = this.#deps.errorText(error)
136
+ this.retryHistory = this.refresh
137
+ }
138
+ } finally {
139
+ if (!this.disposed && generation === this.#listGeneration) this.historyLoading = false
140
+ }
141
+ }
142
+
143
+ restore = async (id: string | null) => {
144
+ if (id !== null && this.history && this.#deps.getSession()) {
145
+ this.restoreBlocked = true
146
+ await this.open(id)
147
+ }
148
+ this.restored = true
149
+ }
150
+
151
+ open = async (id: string) => {
152
+ const session = this.#deps.getSession()
153
+ if (!this.history || !session || session.running || this.historyBusy) return
154
+ const generation = ++this.viewGeneration
155
+ this.#deps.setFullAccess(false)
156
+ this.historyBusy = true
157
+ try {
158
+ const conversation = await this.history.get(id)
159
+ if (this.disposed || generation !== this.viewGeneration) return
160
+ session.show(id, conversation.turns, conversation.summary.selectedTurnId)
161
+ this.shownSummary = conversation.summary
162
+ this.restoreBlocked = false
163
+ this.restored = true
164
+ this.historyError = null
165
+ this.retryHistory = this.refresh
166
+ this.historyOpen = false
167
+ this.renaming = false
168
+ this.#deps.bumpScrollRevision()
169
+ } catch (error) {
170
+ if (this.disposed || generation !== this.viewGeneration) return
171
+ this.historyError = this.#deps.errorText(error)
172
+ this.retryHistory = () => this.open(id)
173
+ } finally {
174
+ if (!this.disposed && generation === this.viewGeneration) this.historyBusy = false
175
+ }
176
+ }
177
+
178
+ newConversation = () => {
179
+ const session = this.#deps.getSession()
180
+ if (!session || session.running || this.historyBusy) return
181
+ this.viewGeneration++
182
+ this.#deps.setFullAccess(false)
183
+ session.show(null)
184
+ this.shownSummary = null
185
+ this.restoreBlocked = false
186
+ this.restored = true
187
+ this.historyError = null
188
+ this.historyOpen = false
189
+ this.renaming = false
190
+ this.#deps.bumpScrollRevision()
191
+ }
192
+
193
+ rename = async () => {
194
+ const session = this.#deps.getSession()
195
+ const id = session?.conversationId
196
+ const next = this.renameTitle.trim()
197
+ if (!this.history || !id || !next || session?.running || this.historyBusy) return
198
+ const generation = this.viewGeneration
199
+ this.historyBusy = true
200
+ try {
201
+ const summary = await this.history.rename(id, next)
202
+ if (this.disposed || generation !== this.viewGeneration) return
203
+ this.shownSummary = summary
204
+ this.conversations = this.conversations.map((item) => (item.id === id ? summary : item))
205
+ this.renaming = false
206
+ this.historyError = null
207
+ } catch (error) {
208
+ if (this.disposed || generation !== this.viewGeneration) return
209
+ this.historyError = this.#deps.errorText(error)
210
+ this.retryHistory = this.rename
211
+ } finally {
212
+ if (!this.disposed && generation === this.viewGeneration) this.historyBusy = false
213
+ }
214
+ }
215
+
216
+ remove = async () => {
217
+ const session = this.#deps.getSession()
218
+ const id = session?.conversationId
219
+ if (!this.history || !session || !id || session.running || this.historyBusy) return
220
+ const generation = this.viewGeneration
221
+ this.historyBusy = true
222
+ try {
223
+ const ok = await this.#deps.notify.confirm({
224
+ title: this.#deps.t('chat.deleteTitle'),
225
+ body: this.#deps.t('chat.deleteBody'),
226
+ danger: true,
227
+ })
228
+ if (!ok || this.disposed || generation !== this.viewGeneration) return
229
+ await this.history.remove(id)
230
+ if (this.disposed || generation !== this.viewGeneration) return
231
+ if (session.conversationId === id) {
232
+ session.show(null)
233
+ this.shownSummary = null
234
+ this.historyOpen = false
235
+ this.renaming = false
236
+ this.#deps.bumpScrollRevision()
237
+ }
238
+ this.historyError = null
239
+ void this.refresh()
240
+ } catch (error) {
241
+ if (this.disposed || generation !== this.viewGeneration) return
242
+ this.historyError = this.#deps.errorText(error)
243
+ this.retryHistory = this.remove
244
+ } finally {
245
+ if (!this.disposed && generation === this.viewGeneration) this.historyBusy = false
246
+ }
247
+ }
248
+
249
+ sourceModel = (turn: ChatTurn): ModelRef | null =>
250
+ turn.endpointId && turn.modelId ? { endpointId: turn.endpointId, modelId: turn.modelId } : null
251
+
252
+ sourceLabel = (turn: ChatTurn) => {
253
+ const option = this.#deps
254
+ .getModelOptions()
255
+ ?.find((item) => item.endpointId === turn.endpointId && item.modelId === turn.modelId)
256
+ return option
257
+ ? (option.name ?? option.modelId)
258
+ : turn.modelId
259
+ ? `${turn.endpointId ?? this.#deps.t('chat.unknown')} · ${turn.modelId}`
260
+ : this.#deps.t('chat.noSourceModel')
261
+ }
262
+
263
+ #hasChildren(turn: ChatTurn) {
264
+ return (
265
+ this.#deps
266
+ .getSession()
267
+ ?.turns.some((child) => child.parentId === turn.id || child.supersedesId === turn.id) ===
268
+ true
269
+ )
270
+ }
271
+
272
+ canEdit = (turn: ChatTurn) => turn.status !== 'running' && !this.#hasChildren(turn)
273
+
274
+ editReply = (turn: ChatTurn) => {
275
+ this.sourceReply = turn
276
+ this.editDraft = turn.text
277
+ this.expectedText = turn.text
278
+ this.replyDialog = 'edit'
279
+ this.replyOpen = true
280
+ }
281
+
282
+ translateDialog = (turn: ChatTurn) => {
283
+ this.sourceReply = turn
284
+ this.replyModel = this.sourceModel(turn)
285
+ this.targetLanguage = this.#deps.getLocale()
286
+ this.replyError = null
287
+ this.replyDialog = 'translate'
288
+ this.replyOpen = true
289
+ }
290
+
291
+ #showConversation(conversation: Conversation) {
292
+ this.viewGeneration++
293
+ this.#deps.setFullAccess(false)
294
+ this.#deps
295
+ .getSession()
296
+ ?.show(conversation.summary.id, conversation.turns, conversation.summary.selectedTurnId)
297
+ this.shownSummary = conversation.summary
298
+ this.#deps.bumpScrollRevision()
299
+ }
300
+
301
+ mutateReply = async (turn: ChatTurn, action: 'select' | 'remove' | 'branch' | 'copy') => {
302
+ const session = this.#deps.getSession()
303
+ const id = session?.conversationId
304
+ if (!this.history || !session || !id || this.actionBusy) return
305
+ let generation = this.viewGeneration
306
+ this.historyBusy = true
307
+ try {
308
+ if (action === 'remove') {
309
+ if (turn.status === 'running') return
310
+ const reparent = session.turns.some((child) => child.parentId === turn.id)
311
+ if (
312
+ !(await this.#deps.notify.confirm({
313
+ title: this.#deps.t('chat.deleteReplyTitle'),
314
+ body: reparent ? this.#deps.t('chat.deleteReparentBody') : '',
315
+ danger: true,
316
+ }))
317
+ )
318
+ return
319
+ }
320
+ if (this.disposed || generation !== this.viewGeneration) return
321
+ const conversation =
322
+ action === 'select' || action === 'branch'
323
+ ? await this.history.selectTurn(id, turn.id)
324
+ : action === 'remove'
325
+ ? await this.history.removeTurn(id, turn.id)
326
+ : await this.history.fork(id, turn.id)
327
+ if (this.disposed || generation !== this.viewGeneration) return
328
+ this.#showConversation(conversation)
329
+ generation = this.viewGeneration
330
+ if (action === 'branch') {
331
+ this.#deps.setDraft('')
332
+ this.#deps.bumpDraftRevision()
333
+ this.#deps.discardRecovery()
334
+ }
335
+ this.historyError = null
336
+ void this.refresh()
337
+ } catch (error) {
338
+ if (this.disposed || generation !== this.viewGeneration) return
339
+ this.#deps.notify.toast('error', this.#deps.errorText(error))
340
+ } finally {
341
+ if (!this.disposed && generation === this.viewGeneration) this.historyBusy = false
342
+ }
343
+ }
344
+
345
+ retryReply = async (turn: ChatTurn) => {
346
+ const session = this.#deps.getSession()
347
+ if (!session || this.actionBusy) return
348
+ const source = this.sourceModel(turn)
349
+ const options = this.#deps.getModelOptions()
350
+ if (this.history && turn.persisted) {
351
+ if (options === null || this.#deps.getModelError()) {
352
+ this.#deps.notify.toast(
353
+ 'error',
354
+ this.#deps.getModelError() ?? this.#deps.t('chat.modelsLoading'),
355
+ )
356
+ return
357
+ }
358
+ if (
359
+ !source ||
360
+ !options.some(
361
+ (option) => option.endpointId === source.endpointId && option.modelId === source.modelId,
362
+ )
363
+ ) {
364
+ this.#deps.notify.toast('error', this.#deps.t('chat.sourceModelUnavailable'))
365
+ return
366
+ }
367
+ }
368
+ this.#deps.bumpScrollRevision()
369
+ await session.retry(this.#deps.consumeBase(), turn.id)
370
+ }
371
+
372
+ switchModel = async (turn: ChatTurn, model: ModelRef) => {
373
+ const session = this.#deps.getSession()
374
+ if (!session || this.actionBusy) return
375
+ this.#deps.bumpScrollRevision()
376
+ if (!(await session.retry(this.#deps.consumeBase(), turn.id, model)))
377
+ this.#deps.notify.toast('error', this.#deps.t(`errors.${session.actionError ?? 'internal'}`))
378
+ }
379
+
380
+ submitReply = async () => {
381
+ const session = this.#deps.getSession()
382
+ const turn = this.sourceReply
383
+ if (this.disposed || !session || !turn || this.actionBusy) return
384
+ let generation = this.viewGeneration
385
+ this.replyError = null
386
+ if (this.replyDialog === 'edit') {
387
+ const id = session.conversationId
388
+ if (
389
+ !this.history ||
390
+ !id ||
391
+ !this.editDraft.trim() ||
392
+ this.editDraft.length > 24_000 ||
393
+ !this.canEdit(turn)
394
+ )
395
+ return
396
+ this.historyBusy = true
397
+ try {
398
+ const conversation = await this.history.editTurn(
399
+ id,
400
+ turn.id,
401
+ this.editDraft,
402
+ this.expectedText,
403
+ )
404
+ if (this.disposed || generation !== this.viewGeneration) return
405
+ this.#showConversation(conversation)
406
+ generation = this.viewGeneration
407
+ this.replyOpen = false
408
+ } catch (error) {
409
+ if (this.disposed || generation !== this.viewGeneration) return
410
+ this.replyError = this.#deps.errorText(error)
411
+ } finally {
412
+ if (!this.disposed && generation === this.viewGeneration) this.historyBusy = false
413
+ }
414
+ } else {
415
+ if (!this.validReplyModel || !this.replyModel) return
416
+ if (!this.targetLanguage.trim() || this.targetLanguage.trim().length > 80) return
417
+ const accepted = await session.translate(
418
+ { ...this.#deps.consumeBase(), model: this.replyModel },
419
+ turn.id,
420
+ this.targetLanguage.trim(),
421
+ )
422
+ if (this.disposed || generation !== this.viewGeneration) return
423
+ if (accepted) this.replyOpen = false
424
+ else
425
+ this.replyError = session.actionError
426
+ ? this.#deps.t(`errors.${session.actionError}`)
427
+ : this.#deps.t('errors.internal')
428
+ }
429
+ }
430
+ }
@@ -0,0 +1,32 @@
1
+ import type { ImageRef, ModelRef, PermissionMode, ReasoningEffort, SkillRef } from '../ports'
2
+
3
+ /** Host inputs for the chat container, independent of its private view composition. */
4
+ export interface ChatPanelProps {
5
+ purpose?: string
6
+ model?: ModelRef | null
7
+ scope?: string
8
+ /** Host context can be removed for the next message without changing conversation ownership. */
9
+ context?: { kind: string; id: string; label: string } | null
10
+ skills?: SkillRef[]
11
+ onSkillsChange?: (refs: SkillRef[]) => void
12
+ skillsBudget?: number
13
+ onInsert?: (markdown: string) => void
14
+ onSaveNote?: (markdown: string, images: ImageRef[]) => Promise<void> | void
15
+ onExportMarkdown?: (markdown: string) => Promise<void> | void
16
+ onOpenSettings?: () => void
17
+ /** A failed restore stays visible until retried or replaced with a new conversation. */
18
+ conversationId?: string | null
19
+ onConversationChange?: (id: string | null) => void
20
+ onClose?: () => void
21
+ /** A host painting model gates image generation. */
22
+ paintingModel?: boolean
23
+ /** Ordered pinned tool ids; omitted means the default toolbar. */
24
+ toolbar?: string[]
25
+ onToolbarChange?: (ids: string[]) => void
26
+ /** Saved preference mapped to the selected model's nearest supported effort. */
27
+ reasoningEffort?: ReasoningEffort
28
+ onReasoningEffortChange?: (effort: ReasoningEffort) => void
29
+ permissionModes?: PermissionMode[]
30
+ /** Full access stays private and is consumed once, never saved as a preference. */
31
+ permissionMode?: PermissionMode
32
+ }
@@ -0,0 +1,6 @@
1
+ export { default as ChatPanel } from './ChatPanel.svelte'
2
+ export { default as ChatSurface } from './ChatSurface.svelte'
3
+ export { default as ChatComposer } from './ChatComposer.svelte'
4
+ export { default as ChatMessage } from './ChatMessage.svelte'
5
+ export { default as ReasoningControl } from './ReasoningControl.svelte'
6
+ export { default as UsageControl } from './UsageControl.svelte'
@@ -0,0 +1,56 @@
1
+ import type { Cost, TokenUsage, Turn } from '../ports'
2
+ import type { ChatTurn } from './run-session-types'
3
+
4
+ export function fromHistory(turn: Turn): ChatTurn {
5
+ return {
6
+ id: turn.id,
7
+ persisted: true,
8
+ parentId: turn.parentId,
9
+ questionId: turn.questionId,
10
+ supersedesId: turn.supersedesId,
11
+ kind: turn.kind,
12
+ endpointId: turn.endpointId,
13
+ modelId: turn.modelId,
14
+ input: turn.input,
15
+ images: turn.images,
16
+ generated: turn.generated,
17
+ text: turn.text,
18
+ reasoningText: turn.reasoningText ?? '',
19
+ reasoningEffort: turn.reasoningEffort ?? 'default',
20
+ usage: turn.usage ?? null,
21
+ cost: turn.cost ?? null,
22
+ contextUsage: turn.contextUsage ?? null,
23
+ status: turn.status,
24
+ error: turn.errorCode,
25
+ tools: [],
26
+ exhausted: false,
27
+ search: { status: 'idle', query: null, provider: null, hits: [], error: null },
28
+ }
29
+ }
30
+
31
+ export function cumulativeUsage(turns: readonly ChatTurn[]): TokenUsage {
32
+ const total: TokenUsage = {
33
+ inputTokens: null,
34
+ outputTokens: null,
35
+ cacheReadTokens: null,
36
+ cacheWriteTokens: null,
37
+ reasoningTokens: null,
38
+ }
39
+ for (const turn of turns)
40
+ for (const key of Object.keys(total) as (keyof TokenUsage)[]) {
41
+ const value = turn.usage?.[key]
42
+ if (value != null) total[key] = (total[key] ?? 0) + value
43
+ }
44
+ return total
45
+ }
46
+
47
+ export function cumulativeCost(turns: readonly ChatTurn[]): Cost[] {
48
+ const totals: Cost[] = []
49
+ for (const { cost } of turns) {
50
+ if (!cost) continue
51
+ const found = totals.find((item) => item.currency === cost.currency)
52
+ if (found) found.amount += cost.amount
53
+ else totals.push({ ...cost })
54
+ }
55
+ return totals
56
+ }