@lota-sdk/ui 0.3.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/ui",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -18,14 +18,14 @@
18
18
  "lint": "bunx oxlint --fix -c ../oxlint.config.ts src",
19
19
  "format": "bunx oxfmt src",
20
20
  "typecheck": "bunx tsgo --noEmit",
21
- "test:unit": "bun test ../tests/unit/ui"
21
+ "test:unit": "bun run scripts/test-unit.ts"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public",
25
25
  "registry": "https://registry.npmjs.org/"
26
26
  },
27
27
  "dependencies": {
28
- "@lota-sdk/shared": "0.3.0",
28
+ "@lota-sdk/shared": "0.3.1",
29
29
  "ai": "^6.0.145"
30
30
  },
31
31
  "peerDependencies": {
@@ -14,7 +14,15 @@ type UploadedAttachmentPayload = {
14
14
  url: string
15
15
  }
16
16
 
17
+ function hasLocalFile(value: FileUIPart): value is FileUIPart & { file: File } {
18
+ return 'file' in value && value.file instanceof File
19
+ }
20
+
17
21
  async function toUploadFile(filePart: FileUIPart): Promise<File> {
22
+ if (hasLocalFile(filePart)) {
23
+ return filePart.file
24
+ }
25
+
18
26
  if (!filePart.url) {
19
27
  throw new Error('Attachment payload is missing file data.')
20
28
  }
package/src/chat/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './attachments'
2
2
  export * from './message-parts'
3
3
  export * from './messages'
4
- export * from './transport'
@@ -1,8 +1,8 @@
1
- import type { FileUIPart, UIMessage } from 'ai'
1
+ import type { UIMessage } from 'ai'
2
2
 
3
- export type ComposerMessage<TFile extends FileUIPart = FileUIPart> = { text: string; files?: TFile[] }
3
+ export type ComposerMessage<TFile = File> = { text: string; files?: TFile[] }
4
4
 
5
- export function validateComposerMessage<TFile extends FileUIPart = FileUIPart>(
5
+ export function validateComposerMessage<TFile = File>(
6
6
  message: ComposerMessage<TFile>,
7
7
  ): { textContent: string; files: TFile[] } | null {
8
8
  const textContent = message.text.trim()
@@ -1,7 +1,6 @@
1
1
  export * from './active-thread-helpers'
2
2
  export * from './session-common'
3
3
  export * from './use-active-thread-session'
4
- export * from './use-lota-chat'
5
4
  export * from './use-session-messages'
6
5
  export * from './use-thread-chat'
7
6
  export * from './use-thread-management'
@@ -1,5 +1,5 @@
1
1
  import { getMessageCreatedAt } from '@lota-sdk/shared'
2
- import type { FileUIPart, UIMessage } from 'ai'
2
+ import type { UIMessage } from 'ai'
3
3
  import { useCallback, useEffect, useMemo, useRef } from 'react'
4
4
 
5
5
  import { mergeChatMessages } from '../chat/messages'
@@ -21,7 +21,7 @@ interface ActiveThreadLike {
21
21
  export interface UseActiveThreadSessionOptions<
22
22
  TMessage extends UIMessage & { metadata?: { createdAt?: unknown } },
23
23
  TThread extends ActiveThreadLike,
24
- TFile extends FileUIPart = FileUIPart,
24
+ TFile = File,
25
25
  TTrackerState = never,
26
26
  > {
27
27
  threadId: string | null
@@ -49,7 +49,7 @@ export interface UseActiveThreadSessionOptions<
49
49
  isThreadBusy?: boolean
50
50
  }
51
51
 
52
- export interface UseActiveThreadSessionReturn<TMessage extends UIMessage, TFile extends FileUIPart = FileUIPart> {
52
+ export interface UseActiveThreadSessionReturn<TMessage extends UIMessage, TFile = File> {
53
53
  messages: TMessage[]
54
54
  status: ChatStatus
55
55
  error: Error | undefined
@@ -67,7 +67,7 @@ export interface UseActiveThreadSessionReturn<TMessage extends UIMessage, TFile
67
67
  export function useActiveThreadSession<
68
68
  TMessage extends UIMessage & { metadata?: { createdAt?: unknown } },
69
69
  TThread extends ActiveThreadLike,
70
- TFile extends FileUIPart = FileUIPart,
70
+ TFile = File,
71
71
  TTrackerState = never,
72
72
  >({
73
73
  threadId,
@@ -58,6 +58,9 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
58
58
  const onDataRef = useRef(onData)
59
59
  const onMessagesSettledRef = useRef(onMessagesSettled)
60
60
  const setThreadRunningStateRef = useRef(setThreadRunningState)
61
+ const attemptedResumeForChatRef = useRef<string | null>(null)
62
+ const skipNextResumeForChatRef = useRef<string | null>(null)
63
+ const locallyStartedChatRef = useRef<string | null>(null)
61
64
 
62
65
  useEffect(() => {
63
66
  onDataRef.current = onData
@@ -78,8 +81,8 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
78
81
  status,
79
82
  error,
80
83
  clearError,
81
- sendMessage,
82
- regenerate,
84
+ sendMessage: sendChatMessage,
85
+ regenerate: regenerateChat,
83
86
  stop: stopChatStream,
84
87
  setMessages,
85
88
  addToolApprovalResponse,
@@ -98,10 +101,17 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
98
101
  experimental_throttle: experimentalThrottle,
99
102
  sendAutomaticallyWhen,
100
103
  onFinish: () => {
104
+ if (locallyStartedChatRef.current === chatId) {
105
+ skipNextResumeForChatRef.current = chatId
106
+ locallyStartedChatRef.current = null
107
+ }
101
108
  setThreadRunningStateRef.current?.(false)
102
109
  onMessagesSettledRef.current?.()
103
110
  },
104
111
  onError: (error) => {
112
+ if (locallyStartedChatRef.current === chatId) {
113
+ locallyStartedChatRef.current = null
114
+ }
105
115
  if (!(error instanceof Error && error.name === 'AbortError')) {
106
116
  notifyError?.('Chat stream failed.', toError(error))
107
117
  }
@@ -113,13 +123,54 @@ export function useThreadChat<UI_MESSAGE extends UIMessage>({
113
123
  // Manual resume with ref guard — prevents React StrictMode double-fire.
114
124
  // We do NOT pass `resume` to useChat because the AI SDK's built-in effect
115
125
  // has no StrictMode guard, causing two concurrent GET streams.
116
- const resumedForChatRef = useRef<string | null>(null)
117
126
  useEffect(() => {
118
- if (!resume || status !== 'ready' || resumedForChatRef.current === chatId) return
119
- resumedForChatRef.current = chatId
127
+ if (!resume || status !== 'ready') return
128
+ if (skipNextResumeForChatRef.current === chatId) {
129
+ skipNextResumeForChatRef.current = null
130
+ return
131
+ }
132
+ if (attemptedResumeForChatRef.current === chatId) return
133
+ attemptedResumeForChatRef.current = chatId
120
134
  void resumeStream()
121
135
  }, [resume, status, chatId, resumeStream])
122
136
 
137
+ useEffect(() => {
138
+ if (status === 'ready' && resume) return
139
+ if (attemptedResumeForChatRef.current === chatId) {
140
+ attemptedResumeForChatRef.current = null
141
+ }
142
+ }, [chatId, resume, status])
143
+
144
+ const sendMessage = useCallback<UseChatHelpers<UI_MESSAGE>['sendMessage']>(
145
+ async (message, options) => {
146
+ locallyStartedChatRef.current = chatId
147
+ try {
148
+ return await sendChatMessage(message, options)
149
+ } catch (error) {
150
+ if (locallyStartedChatRef.current === chatId) {
151
+ locallyStartedChatRef.current = null
152
+ }
153
+ throw error
154
+ }
155
+ },
156
+ [chatId, sendChatMessage],
157
+ )
158
+
159
+ const regenerate = useCallback<UseChatHelpers<UI_MESSAGE>['regenerate']>(
160
+ async (options) => {
161
+ locallyStartedChatRef.current = chatId
162
+ try {
163
+ return await regenerateChat(options)
164
+ } catch (error) {
165
+ if (locallyStartedChatRef.current === chatId) {
166
+ locallyStartedChatRef.current = null
167
+ }
168
+ throw error
169
+ }
170
+ },
171
+ [chatId, regenerateChat],
172
+ )
173
+
123
174
  const stop = useCallback(async () => {
124
175
  const stopRequest = threadId && stopRemoteRun ? stopRemoteRun(threadId) : Promise.resolve(null)
125
176
  const [stopResponse] = await Promise.allSettled([stopRequest, stopChatStream()])
@@ -5,7 +5,7 @@ const PLACEHOLDER_THREAD_PREFIX = '__placeholder_'
5
5
  export interface ThreadManagementItem<TAgentId extends string = string> {
6
6
  threadId: string
7
7
  title: string
8
- status: 'regular' | 'archived'
8
+ status: 'active' | 'archived'
9
9
  type: string
10
10
  isMutable: boolean
11
11
  agentId?: TAgentId | null
@@ -62,7 +62,7 @@ export function findReusableRegularGroupThreadId<TThread extends ThreadManagemen
62
62
  defaultThreadTitle: string,
63
63
  ): string | null {
64
64
  const draft = threads.find(
65
- (thread) => thread.type === 'group' && thread.status === 'regular' && thread.title.trim() === defaultThreadTitle,
65
+ (thread) => thread.type === 'group' && thread.status === 'active' && thread.title.trim() === defaultThreadTitle,
66
66
  )
67
67
 
68
68
  return draft?.threadId ?? null
@@ -245,6 +245,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
245
245
  return
246
246
  }
247
247
 
248
+ if (!enabled) return
248
249
  if (isCreatingThreadRef.current) return
249
250
  if (isCreatingThread) return
250
251
  isCreatingThreadRef.current = true
@@ -257,6 +258,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
257
258
  isCreatingThreadRef.current = false
258
259
  })
259
260
  }, [
261
+ enabled,
260
262
  currentThreadId,
261
263
  handleCreateNewThread,
262
264
  isCreatingThread,
@@ -1,8 +0,0 @@
1
- import { DefaultChatTransport } from 'ai'
2
- import type { HttpChatTransportInitOptions, UIMessage } from 'ai'
3
-
4
- export type ChatTransportOptions<UI_MESSAGE extends UIMessage> = HttpChatTransportInitOptions<UI_MESSAGE>
5
-
6
- export function createChatTransport<UI_MESSAGE extends UIMessage>(options: ChatTransportOptions<UI_MESSAGE>) {
7
- return new DefaultChatTransport<UI_MESSAGE>(options)
8
- }
@@ -1,13 +0,0 @@
1
- import type { UIMessage } from 'ai'
2
-
3
- import type { UseThreadChatOptions, UseThreadChatReturn } from './use-thread-chat'
4
- import { useThreadChat } from './use-thread-chat'
5
-
6
- export type UseLotaChatOptions<UI_MESSAGE extends UIMessage> = UseThreadChatOptions<UI_MESSAGE>
7
- export type UseLotaChatReturn<UI_MESSAGE extends UIMessage> = UseThreadChatReturn<UI_MESSAGE>
8
-
9
- export function useLotaChat<UI_MESSAGE extends UIMessage>(
10
- options: UseLotaChatOptions<UI_MESSAGE>,
11
- ): UseLotaChatReturn<UI_MESSAGE> {
12
- return useThreadChat(options)
13
- }