@refraction-ui/astro 0.8.1 → 0.10.0

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 (93) hide show
  1. package/dist/analytics/index.ts +5 -0
  2. package/dist/astro-accordion/Accordion.stories.ts +12 -0
  3. package/dist/astro-ai/Ai.stories.ts +12 -0
  4. package/dist/astro-analytics/Analytics.stories.ts +12 -0
  5. package/dist/astro-animated-text/AnimatedText.stories.ts +12 -0
  6. package/dist/astro-app-shell/AppShell.stories.ts +12 -0
  7. package/dist/astro-auth/Auth.stories.ts +12 -0
  8. package/dist/astro-avatar/Avatar.stories.ts +12 -0
  9. package/dist/astro-avatar-group/AvatarGroup.stories.ts +12 -0
  10. package/dist/astro-badge/Badge.stories.ts +16 -0
  11. package/dist/astro-bottom-nav/BottomNav.stories.ts +12 -0
  12. package/dist/astro-breadcrumbs/Breadcrumbs.stories.ts +12 -0
  13. package/dist/astro-button/Button.stories.ts +12 -0
  14. package/dist/astro-calendar/Calendar.stories.ts +12 -0
  15. package/dist/astro-callout/Callout.stories.ts +12 -0
  16. package/dist/astro-card/Card.stories.ts +12 -0
  17. package/dist/astro-card-grid/CardGrid.stories.ts +12 -0
  18. package/dist/astro-carousel/Carousel.stories.ts +12 -0
  19. package/dist/astro-charts/Charts.stories.ts +12 -0
  20. package/dist/astro-checkbox/Checkbox.stories.ts +12 -0
  21. package/dist/astro-code-block/CodeBlock.stories.ts +12 -0
  22. package/dist/astro-code-editor/CodeEditor.stories.ts +12 -0
  23. package/dist/astro-collapsible/Collapsible.stories.ts +12 -0
  24. package/dist/astro-command/Command.stories.ts +12 -0
  25. package/dist/astro-command-input/CommandInput.stories.ts +12 -0
  26. package/dist/astro-content-protection/ContentProtection.stories.ts +12 -0
  27. package/dist/astro-conversation/Chat.astro +208 -0
  28. package/dist/astro-conversation/Conversation.stories.ts +12 -0
  29. package/dist/astro-conversation/index.ts +29 -0
  30. package/dist/astro-cookie-consent/CookieConsent.astro +141 -0
  31. package/dist/astro-cookie-consent/index.ts +12 -0
  32. package/dist/astro-data-table/DataTable.stories.ts +12 -0
  33. package/dist/astro-date-picker/DatePicker.stories.ts +12 -0
  34. package/dist/astro-device-frame/DeviceFrame.stories.ts +12 -0
  35. package/dist/astro-dialog/Dialog.stories.ts +12 -0
  36. package/dist/astro-diff-viewer/DiffViewer.stories.ts +12 -0
  37. package/dist/astro-dropdown-menu/DropdownMenu.stories.ts +12 -0
  38. package/dist/astro-emoji-picker/EmojiPicker.stories.ts +12 -0
  39. package/dist/astro-feedback-dialog/FeedbackDialog.stories.ts +12 -0
  40. package/dist/astro-file-tree/FileTree.stories.ts +12 -0
  41. package/dist/astro-file-upload/FileUpload.stories.ts +12 -0
  42. package/dist/astro-footer/Footer.stories.ts +12 -0
  43. package/dist/astro-icon-system/IconSystem.stories.ts +12 -0
  44. package/dist/astro-inline-editor/InlineEditor.stories.ts +12 -0
  45. package/dist/astro-input/Input.stories.ts +12 -0
  46. package/dist/astro-input-group/InputGroup.stories.ts +12 -0
  47. package/dist/astro-install-prompt/InstallPrompt.stories.ts +12 -0
  48. package/dist/astro-keyboard-shortcut/KeyboardShortcut.stories.ts +12 -0
  49. package/dist/astro-language-selector/LanguageSelector.stories.ts +12 -0
  50. package/dist/astro-link-card/LinkCard.stories.ts +12 -0
  51. package/dist/astro-logger/Logger.stories.ts +12 -0
  52. package/dist/astro-markdown-renderer/MarkdownRenderer.stories.ts +12 -0
  53. package/dist/astro-mobile-nav/MobileNav.stories.ts +12 -0
  54. package/dist/astro-navbar/Navbar.stories.ts +12 -0
  55. package/dist/astro-otp-input/OtpInput.stories.ts +12 -0
  56. package/dist/astro-pagination/Pagination.stories.ts +12 -0
  57. package/dist/astro-payment/Payment.stories.ts +12 -0
  58. package/dist/astro-popover/Popover.stories.ts +12 -0
  59. package/dist/astro-presence-indicator/PresenceIndicator.stories.ts +12 -0
  60. package/dist/astro-progress-display/ProgressDisplay.stories.ts +12 -0
  61. package/dist/astro-radio/Radio.stories.ts +12 -0
  62. package/dist/astro-reaction-bar/ReactionBar.stories.ts +12 -0
  63. package/dist/astro-resizable-layout/ResizableLayout.stories.ts +12 -0
  64. package/dist/astro-rich-editor/RichEditor.stories.ts +12 -0
  65. package/dist/astro-search-bar/SearchBar.stories.ts +12 -0
  66. package/dist/astro-select/Select.stories.ts +12 -0
  67. package/dist/astro-sidebar/Sidebar.stories.ts +12 -0
  68. package/dist/astro-skeleton/Skeleton.stories.ts +12 -0
  69. package/dist/astro-skip-to-content/SkipToContent.stories.ts +12 -0
  70. package/dist/astro-slide-viewer/SlideViewer.stories.ts +12 -0
  71. package/dist/astro-status-indicator/StatusIndicator.stories.ts +12 -0
  72. package/dist/astro-steps/Steps.stories.ts +12 -0
  73. package/dist/astro-switch/Switch.stories.ts +12 -0
  74. package/dist/astro-table-of-contents/TableOfContents.stories.ts +12 -0
  75. package/dist/astro-tabs/Tabs.stories.ts +12 -0
  76. package/dist/astro-textarea/Textarea.stories.ts +12 -0
  77. package/dist/astro-theme/Theme.stories.ts +12 -0
  78. package/dist/astro-thread-view/ThreadView.stories.ts +12 -0
  79. package/dist/astro-toast/Toast.stories.ts +12 -0
  80. package/dist/astro-tooltip/Tooltip.stories.ts +12 -0
  81. package/dist/astro-version-selector/VersionSelector.stories.ts +12 -0
  82. package/dist/astro-video-player/VideoPlayer.stories.ts +12 -0
  83. package/dist/astro-voice-pill/VoicePill.stories.ts +12 -0
  84. package/dist/astro-waveform/Waveform.stories.ts +12 -0
  85. package/dist/conversation/conversation.ts +399 -0
  86. package/dist/conversation/index.ts +30 -0
  87. package/dist/conversation/selectors.ts +58 -0
  88. package/dist/conversation/types.ts +232 -0
  89. package/dist/cookie-consent/cookie-consent.ts +132 -0
  90. package/dist/cookie-consent/index.ts +10 -0
  91. package/dist/cookie-consent/types.ts +72 -0
  92. package/dist/index.ts +2 -0
  93. package/package.json +3 -1
@@ -0,0 +1,12 @@
1
+ import Component from './SkipToContent.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/SkipToContent',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './SlideViewer.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/SlideViewer',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './StatusIndicator.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/StatusIndicator',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './Steps.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Steps',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './Switch.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Switch',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './TableOfContents.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/TableOfContents',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './Tabs.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Tabs',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './Textarea.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Textarea',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './ThemeScript.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Theme',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './ThreadView.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/ThreadView',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './Toast.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Toast',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './Tooltip.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Tooltip',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './VersionSelector.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/VersionSelector',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './VideoPlayer.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/VideoPlayer',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './VoicePill.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/VoicePill',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,12 @@
1
+ import Component from './Waveform.astro'
2
+
3
+ // Auto-generated baseline story. Renders the component with empty args;
4
+ // components that require props show a render error (enrich by hand as needed).
5
+ const meta = {
6
+ title: 'Astro/Waveform',
7
+ component: Component,
8
+ }
9
+
10
+ export default meta
11
+
12
+ export const Default = { args: {} }
@@ -0,0 +1,399 @@
1
+ import { generateId } from '../shared/index.ts'
2
+ import { rootIdOf, selectRoots, selectThreadMessages } from './selectors.js'
3
+ import type {
4
+ ChatAuthor,
5
+ ChatMessage,
6
+ Conversation,
7
+ ConversationAPI,
8
+ ConversationConfig,
9
+ ConversationState,
10
+ SendOptions,
11
+ ThreadingMode,
12
+ } from './types.js'
13
+
14
+ const DEFAULT_ASSISTANT: ChatAuthor = { id: 'assistant', name: 'Assistant' }
15
+ const DEFAULT_USER: ChatAuthor = { id: 'user', name: 'You' }
16
+ const DEFAULT_TITLE = 'New conversation'
17
+
18
+ /** Default title: first line, trimmed to ~48 chars. */
19
+ function defaultGenerateTitle(firstMessage: string): string {
20
+ const firstLine = firstMessage.split('\n', 1)[0]!.trim()
21
+ if (firstLine.length <= 48) return firstLine || DEFAULT_TITLE
22
+ return `${firstLine.slice(0, 47).trimEnd()}…`
23
+ }
24
+
25
+ /**
26
+ * createConversation — headless multi-conversation chat store with reply-threads,
27
+ * reactions, edit/delete, and streaming. Backend-agnostic: provide a
28
+ * {@link ChatTransport} and `sendMessage` will optimistically append the user
29
+ * message, stream the reply, and expose retry/stop. Without a transport it is a
30
+ * pure local message log.
31
+ */
32
+ export function createConversation(config: ConversationConfig = {}): ConversationAPI {
33
+ const assistant = config.assistant ?? DEFAULT_ASSISTANT
34
+ const currentUser = config.currentUser ?? DEFAULT_USER
35
+ const generateTitle = config.generateTitle ?? defaultGenerateTitle
36
+
37
+ let transport = config.transport
38
+ let threadingMode: ThreadingMode = config.threadingMode ?? 'inline'
39
+ const conversations = new Map<string, Conversation>()
40
+ const messagesByConv = new Map<string, ChatMessage[]>()
41
+ let activeConversationId: string | null = config.activeConversationId ?? null
42
+ let openThreadRootId: string | null = null
43
+ let replyTarget: string | null = null
44
+ let status: ConversationState['status'] = 'idle'
45
+ let error: string | null = null
46
+ let abortController: AbortController | null = null
47
+
48
+ // Seed
49
+ for (const c of config.conversations ?? []) {
50
+ conversations.set(c.id, c)
51
+ messagesByConv.set(c.id, [])
52
+ }
53
+ for (const [conversationId, msgs] of Object.entries(config.messages ?? {})) {
54
+ messagesByConv.set(conversationId, [...msgs])
55
+ }
56
+ if (activeConversationId === null && config.conversations?.length) {
57
+ activeConversationId = config.conversations[0]!.id
58
+ }
59
+
60
+ // — subscription —
61
+ const listeners = new Set<() => void>()
62
+ let snapshot: ConversationState = buildSnapshot()
63
+
64
+ function buildSnapshot(): ConversationState {
65
+ return {
66
+ conversations: orderedConversations(),
67
+ activeConversationId,
68
+ messages: activeConversationId ? (messagesByConv.get(activeConversationId) ?? []) : [],
69
+ openThreadRootId,
70
+ replyTarget,
71
+ threadingMode,
72
+ status,
73
+ error,
74
+ }
75
+ }
76
+
77
+ function emit(): void {
78
+ snapshot = buildSnapshot()
79
+ for (const l of listeners) l()
80
+ }
81
+
82
+ function orderedConversations(): Conversation[] {
83
+ return [...conversations.values()].sort(
84
+ (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
85
+ )
86
+ }
87
+
88
+ function touch(conversationId: string): void {
89
+ const c = conversations.get(conversationId)
90
+ if (c) conversations.set(conversationId, { ...c, updatedAt: new Date() })
91
+ }
92
+
93
+ function ensureActiveConversation(opts?: SendOptions): string {
94
+ if (opts?.conversationId) {
95
+ if (!conversations.has(opts.conversationId)) createConversationInternal({}, opts.conversationId)
96
+ return opts.conversationId
97
+ }
98
+ if (activeConversationId && conversations.has(activeConversationId)) return activeConversationId
99
+ return createConversationInternal({}).id
100
+ }
101
+
102
+ function createConversationInternal(
103
+ opts: { title?: string; metadata?: Record<string, unknown> },
104
+ id?: string,
105
+ ): Conversation {
106
+ const now = new Date()
107
+ const conversation: Conversation = {
108
+ id: id ?? generateId('rfr-conv'),
109
+ title: opts.title ?? DEFAULT_TITLE,
110
+ createdAt: now,
111
+ updatedAt: now,
112
+ metadata: opts.metadata,
113
+ }
114
+ conversations.set(conversation.id, conversation)
115
+ messagesByConv.set(conversation.id, [])
116
+ activeConversationId = conversation.id
117
+ return conversation
118
+ }
119
+
120
+ /** Run a transport stream into the given assistant message. */
121
+ async function streamReply(
122
+ conversationId: string,
123
+ assistantMsg: ChatMessage,
124
+ userMsg: ChatMessage,
125
+ history: ChatMessage[],
126
+ ): Promise<void> {
127
+ abortController = new AbortController()
128
+ status = 'streaming'
129
+ emit()
130
+
131
+ try {
132
+ const stream = transport!.send({
133
+ conversationId,
134
+ message: userMsg,
135
+ history,
136
+ parentId: assistantMsg.parentId,
137
+ signal: abortController.signal,
138
+ })
139
+ for await (const chunk of stream) {
140
+ // Apply the delivered chunk before honoring an abort, so a `stop()` mid
141
+ // stream keeps the partial reply rather than discarding the last token.
142
+ if (chunk.content !== undefined) assistantMsg.content = chunk.content
143
+ if (chunk.delta) assistantMsg.content += chunk.delta
144
+ if (chunk.metadata) {
145
+ assistantMsg.metadata = { ...assistantMsg.metadata, ...chunk.metadata }
146
+ }
147
+ emit()
148
+ if (abortController.signal.aborted) break
149
+ }
150
+ assistantMsg.status = 'sent'
151
+ status = 'idle'
152
+ error = null
153
+ } catch (err) {
154
+ if (abortController.signal.aborted) {
155
+ assistantMsg.status = 'sent'
156
+ status = 'idle'
157
+ } else {
158
+ const message = err instanceof Error ? err.message : String(err)
159
+ assistantMsg.status = 'error'
160
+ assistantMsg.error = message
161
+ status = 'error'
162
+ error = message
163
+ }
164
+ } finally {
165
+ abortController = null
166
+ touch(conversationId)
167
+ emit()
168
+ }
169
+ }
170
+
171
+ /** History the transport sees: the thread when replying, else the main timeline. */
172
+ function historyFor(list: ChatMessage[], parentId: string | undefined): ChatMessage[] {
173
+ const scope = parentId
174
+ ? selectThreadMessages(list, parentId)
175
+ : selectRoots(list)
176
+ return scope.filter((m) => m.status !== 'error')
177
+ }
178
+
179
+ const api: ConversationAPI = {
180
+ getState() {
181
+ return snapshot
182
+ },
183
+
184
+ subscribe(listener) {
185
+ listeners.add(listener)
186
+ return () => listeners.delete(listener)
187
+ },
188
+
189
+ newConversation(opts) {
190
+ const conversation = createConversationInternal(opts ?? {})
191
+ openThreadRootId = null
192
+ replyTarget = null
193
+ emit()
194
+ return conversation
195
+ },
196
+
197
+ selectConversation(conversationId) {
198
+ if (!conversations.has(conversationId) || activeConversationId === conversationId) return
199
+ activeConversationId = conversationId
200
+ openThreadRootId = null
201
+ replyTarget = null
202
+ emit()
203
+ },
204
+
205
+ deleteConversation(conversationId) {
206
+ if (!conversations.has(conversationId)) return
207
+ conversations.delete(conversationId)
208
+ messagesByConv.delete(conversationId)
209
+ if (activeConversationId === conversationId) {
210
+ activeConversationId = orderedConversations()[0]?.id ?? null
211
+ openThreadRootId = null
212
+ replyTarget = null
213
+ }
214
+ emit()
215
+ },
216
+
217
+ renameConversation(conversationId, title) {
218
+ const c = conversations.get(conversationId)
219
+ if (!c) return
220
+ conversations.set(conversationId, { ...c, title })
221
+ emit()
222
+ },
223
+
224
+ appendMessage(message) {
225
+ const list = messagesByConv.get(message.conversationId)
226
+ if (!list) return
227
+ list.push(message)
228
+ touch(message.conversationId)
229
+ emit()
230
+ },
231
+
232
+ editMessage(messageId, content) {
233
+ if (!activeConversationId) return
234
+ const list = messagesByConv.get(activeConversationId)!
235
+ const msg = list.find((m) => m.id === messageId)
236
+ if (!msg) return
237
+ msg.content = content
238
+ msg.edited = true
239
+ emit()
240
+ },
241
+
242
+ deleteMessage(messageId) {
243
+ if (!activeConversationId) return
244
+ const list = messagesByConv.get(activeConversationId)!
245
+ const msg = list.find((m) => m.id === messageId)
246
+ if (!msg) return
247
+ const removeIds = new Set<string>([messageId])
248
+ // Deleting a thread root removes its replies too.
249
+ if (!msg.parentId) for (const r of list) if (r.parentId === messageId) removeIds.add(r.id)
250
+ const next = list.filter((m) => !removeIds.has(m.id))
251
+ messagesByConv.set(activeConversationId, next)
252
+ if (openThreadRootId && removeIds.has(openThreadRootId)) openThreadRootId = null
253
+ if (replyTarget && removeIds.has(replyTarget)) replyTarget = openThreadRootId
254
+ emit()
255
+ },
256
+
257
+ react(messageId, emoji) {
258
+ if (!activeConversationId) return
259
+ const list = messagesByConv.get(activeConversationId)!
260
+ const msg = list.find((m) => m.id === messageId)
261
+ if (!msg) return
262
+ const reactions = [...(msg.reactions ?? [])]
263
+ const idx = reactions.findIndex((r) => r.emoji === emoji)
264
+ if (idx === -1) {
265
+ reactions.push({ emoji, count: 1, userReacted: true })
266
+ } else {
267
+ const r = reactions[idx]!
268
+ if (r.userReacted) {
269
+ const count = r.count - 1
270
+ if (count <= 0) reactions.splice(idx, 1)
271
+ else reactions[idx] = { ...r, count, userReacted: false }
272
+ } else {
273
+ reactions[idx] = { ...r, count: r.count + 1, userReacted: true }
274
+ }
275
+ }
276
+ msg.reactions = reactions
277
+ emit()
278
+ },
279
+
280
+ async sendMessage(content, opts) {
281
+ const trimmed = content.trim()
282
+ if (!trimmed && !opts?.attachments?.length) return
283
+
284
+ const conversationId = ensureActiveConversation(opts)
285
+ const list = messagesByConv.get(conversationId)!
286
+ // Group under the originating root, but remember the specific message replied to.
287
+ const parentId = opts?.replyTo ? rootIdOf(list, opts.replyTo) : undefined
288
+ const replyToId = opts?.replyTo
289
+ const isFirstRoot = !parentId && selectRoots(list).length === 0
290
+
291
+ const userMsg: ChatMessage = {
292
+ id: generateId('rfr-msg'),
293
+ conversationId,
294
+ role: 'user',
295
+ author: currentUser,
296
+ content: trimmed,
297
+ timestamp: new Date(),
298
+ status: 'sent',
299
+ parentId,
300
+ replyToId,
301
+ attachments: opts?.attachments,
302
+ metadata: opts?.metadata,
303
+ }
304
+ list.push(userMsg)
305
+
306
+ const conversation = conversations.get(conversationId)!
307
+ if (isFirstRoot && conversation.title === DEFAULT_TITLE) {
308
+ conversations.set(conversationId, { ...conversation, title: generateTitle(trimmed) })
309
+ }
310
+ touch(conversationId)
311
+
312
+ if (!transport) {
313
+ status = 'idle'
314
+ emit()
315
+ return
316
+ }
317
+
318
+ // history excludes the just-pushed user message
319
+ const history = historyFor(list, parentId).filter((m) => m.id !== userMsg.id)
320
+ const assistantMsg: ChatMessage = {
321
+ id: generateId('rfr-msg'),
322
+ conversationId,
323
+ role: 'assistant',
324
+ author: assistant,
325
+ content: '',
326
+ timestamp: new Date(),
327
+ status: 'streaming',
328
+ parentId,
329
+ }
330
+ list.push(assistantMsg)
331
+ await streamReply(conversationId, assistantMsg, userMsg, history)
332
+ },
333
+
334
+ async retryLast() {
335
+ if (!activeConversationId || !transport) return
336
+ const list = messagesByConv.get(activeConversationId)!
337
+ const last = list[list.length - 1]
338
+ const parentId = last?.parentId
339
+ // Drop a trailing failed assistant message, if any.
340
+ if (last && last.role === 'assistant' && last.status === 'error') list.pop()
341
+
342
+ const scope = parentId ? selectThreadMessages(list, parentId) : selectRoots(list)
343
+ const lastUser = [...scope].reverse().find((m) => m.role === 'user')
344
+ if (!lastUser) return
345
+
346
+ const history = historyFor(list, parentId)
347
+ const assistantMsg: ChatMessage = {
348
+ id: generateId('rfr-msg'),
349
+ conversationId: activeConversationId,
350
+ role: 'assistant',
351
+ author: assistant,
352
+ content: '',
353
+ timestamp: new Date(),
354
+ status: 'streaming',
355
+ parentId,
356
+ }
357
+ list.push(assistantMsg)
358
+ await streamReply(activeConversationId, assistantMsg, lastUser, history)
359
+ },
360
+
361
+ stop() {
362
+ abortController?.abort()
363
+ },
364
+
365
+ openThread(rootId) {
366
+ openThreadRootId = rootId
367
+ replyTarget = rootId // reply to the root by default
368
+ emit()
369
+ },
370
+
371
+ replyTo(messageId) {
372
+ if (!activeConversationId) return
373
+ const list = messagesByConv.get(activeConversationId)!
374
+ // Open the originating thread, but target the specific message replied to.
375
+ openThreadRootId = rootIdOf(list, messageId)
376
+ replyTarget = messageId
377
+ emit()
378
+ },
379
+
380
+ closeThread() {
381
+ if (openThreadRootId === null && replyTarget === null) return
382
+ openThreadRootId = null
383
+ replyTarget = null
384
+ emit()
385
+ },
386
+
387
+ setThreadingMode(mode) {
388
+ if (threadingMode === mode) return
389
+ threadingMode = mode
390
+ emit()
391
+ },
392
+
393
+ setTransport(next) {
394
+ transport = next
395
+ },
396
+ }
397
+
398
+ return api
399
+ }
@@ -0,0 +1,30 @@
1
+ export { createConversation } from './conversation.js'
2
+
3
+ export {
4
+ findMessage,
5
+ rootIdOf,
6
+ selectRoots,
7
+ selectReplies,
8
+ selectThreadMessages,
9
+ getReplyCount,
10
+ selectMainTimeline,
11
+ selectTimelineFromState,
12
+ } from './selectors.js'
13
+
14
+ export type {
15
+ ChatAuthor,
16
+ ChatRole,
17
+ MessageStatus,
18
+ ThreadingMode,
19
+ MessageReaction,
20
+ MessageAttachment,
21
+ ChatMessage,
22
+ Conversation,
23
+ SendContext,
24
+ TransportChunk,
25
+ ChatTransport,
26
+ ConversationState,
27
+ ConversationConfig,
28
+ SendOptions,
29
+ ConversationAPI,
30
+ } from './types.js'