@lota-sdk/ui 0.2.3 → 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 +3 -3
- package/src/chat/attachments.ts +8 -0
- package/src/chat/index.ts +0 -1
- package/src/runtime/active-thread-helpers.ts +3 -3
- package/src/runtime/index.ts +0 -1
- package/src/runtime/use-active-thread-session.ts +4 -4
- package/src/runtime/use-thread-chat.ts +56 -5
- package/src/runtime/use-thread-management.ts +12 -15
- package/src/tools/execution-plan.ts +6 -8
- package/src/chat/transport.ts +0 -8
- package/src/runtime/use-lota-chat.ts +0 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/ui",
|
|
3
|
-
"version": "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
|
|
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.
|
|
28
|
+
"@lota-sdk/shared": "0.3.1",
|
|
29
29
|
"ai": "^6.0.145"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
package/src/chat/attachments.ts
CHANGED
|
@@ -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,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UIMessage } from 'ai'
|
|
2
2
|
|
|
3
|
-
export type ComposerMessage<TFile
|
|
3
|
+
export type ComposerMessage<TFile = File> = { text: string; files?: TFile[] }
|
|
4
4
|
|
|
5
|
-
export function validateComposerMessage<TFile
|
|
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()
|
package/src/runtime/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
119
|
-
|
|
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,9 +5,8 @@ const PLACEHOLDER_THREAD_PREFIX = '__placeholder_'
|
|
|
5
5
|
export interface ThreadManagementItem<TAgentId extends string = string> {
|
|
6
6
|
threadId: string
|
|
7
7
|
title: string
|
|
8
|
-
status: '
|
|
9
|
-
|
|
10
|
-
core: boolean
|
|
8
|
+
status: 'active' | 'archived'
|
|
9
|
+
type: string
|
|
11
10
|
isMutable: boolean
|
|
12
11
|
agentId?: TAgentId | null
|
|
13
12
|
}
|
|
@@ -63,11 +62,7 @@ export function findReusableRegularGroupThreadId<TThread extends ThreadManagemen
|
|
|
63
62
|
defaultThreadTitle: string,
|
|
64
63
|
): string | null {
|
|
65
64
|
const draft = threads.find(
|
|
66
|
-
(thread) =>
|
|
67
|
-
thread.mode === 'group' &&
|
|
68
|
-
!thread.core &&
|
|
69
|
-
thread.status === 'regular' &&
|
|
70
|
-
thread.title.trim() === defaultThreadTitle,
|
|
65
|
+
(thread) => thread.type === 'group' && thread.status === 'active' && thread.title.trim() === defaultThreadTitle,
|
|
71
66
|
)
|
|
72
67
|
|
|
73
68
|
return draft?.threadId ?? null
|
|
@@ -151,7 +146,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
151
146
|
? (() => {
|
|
152
147
|
const agentId = threadId.slice(placeholderThreadPrefix.length) as TAgentId
|
|
153
148
|
const directThread = allThreadsRef.current.find(
|
|
154
|
-
(thread) => thread.
|
|
149
|
+
(thread) => thread.type === 'default' && thread.agentId === agentId,
|
|
155
150
|
)
|
|
156
151
|
return directThread?.threadId
|
|
157
152
|
})()
|
|
@@ -160,7 +155,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
160
155
|
if (!resolvedThreadId) return
|
|
161
156
|
|
|
162
157
|
const target = allThreadsRef.current.find((thread) => thread.threadId === resolvedThreadId)
|
|
163
|
-
const nextAgentId = target?.
|
|
158
|
+
const nextAgentId = target?.type === 'default' && target.agentId ? target.agentId : null
|
|
164
159
|
setCurrentThreadId(resolvedThreadId)
|
|
165
160
|
setSelectedAgentId(nextAgentId)
|
|
166
161
|
},
|
|
@@ -243,13 +238,14 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
243
238
|
const preferredThread =
|
|
244
239
|
primaryDirectAgentId === undefined
|
|
245
240
|
? threads[0]
|
|
246
|
-
: (threads.find((thread) => thread.
|
|
241
|
+
: (threads.find((thread) => thread.type === 'default' && thread.agentId === primaryDirectAgentId) ??
|
|
247
242
|
threads[0])
|
|
248
243
|
setCurrentThreadId(preferredThread.threadId)
|
|
249
|
-
setSelectedAgentId(preferredThread.
|
|
244
|
+
setSelectedAgentId(preferredThread.type === 'default' && preferredThread.agentId ? preferredThread.agentId : null)
|
|
250
245
|
return
|
|
251
246
|
}
|
|
252
247
|
|
|
248
|
+
if (!enabled) return
|
|
253
249
|
if (isCreatingThreadRef.current) return
|
|
254
250
|
if (isCreatingThread) return
|
|
255
251
|
isCreatingThreadRef.current = true
|
|
@@ -262,6 +258,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
262
258
|
isCreatingThreadRef.current = false
|
|
263
259
|
})
|
|
264
260
|
}, [
|
|
261
|
+
enabled,
|
|
265
262
|
currentThreadId,
|
|
266
263
|
handleCreateNewThread,
|
|
267
264
|
isCreatingThread,
|
|
@@ -276,7 +273,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
276
273
|
if (!currentThreadId?.startsWith(placeholderThreadPrefix)) return
|
|
277
274
|
|
|
278
275
|
const agentId = currentThreadId.slice(placeholderThreadPrefix.length) as TAgentId
|
|
279
|
-
const directThread = allThreads.find((thread) => thread.
|
|
276
|
+
const directThread = allThreads.find((thread) => thread.type === 'default' && thread.agentId === agentId)
|
|
280
277
|
if (!directThread) return
|
|
281
278
|
|
|
282
279
|
setCurrentThreadId(directThread.threadId)
|
|
@@ -286,7 +283,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
286
283
|
useEffect(() => {
|
|
287
284
|
if (currentThreadId !== null || selectedAgentId === null) return
|
|
288
285
|
|
|
289
|
-
const directThread = allThreads.find((thread) => thread.
|
|
286
|
+
const directThread = allThreads.find((thread) => thread.type === 'default' && thread.agentId === selectedAgentId)
|
|
290
287
|
if (!directThread) return
|
|
291
288
|
|
|
292
289
|
setCurrentThreadId(directThread.threadId)
|
|
@@ -303,7 +300,7 @@ export function useThreadManagement<TThread extends ThreadManagementItem<TAgentI
|
|
|
303
300
|
const activeThread = allThreads.find((thread) => thread.threadId === currentThreadId)
|
|
304
301
|
if (!activeThread) return
|
|
305
302
|
|
|
306
|
-
const expectedAgentId = activeThread.
|
|
303
|
+
const expectedAgentId = activeThread.type === 'default' && activeThread.agentId ? activeThread.agentId : null
|
|
307
304
|
if (selectedAgentId !== expectedAgentId) {
|
|
308
305
|
setSelectedAgentId(expectedAgentId)
|
|
309
306
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExecutionPlanNodeSummary } from '@lota-sdk/shared'
|
|
2
2
|
import { getLatestExecutionPlanResult } from '@lota-sdk/shared'
|
|
3
3
|
|
|
4
4
|
export { getLatestExecutionPlanResult }
|
|
@@ -33,14 +33,13 @@ export function getExecutionPlanStatusLabel(status: string | null | undefined):
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function getExecutionPlanOwnerLabel(
|
|
36
|
-
node:
|
|
36
|
+
node: ExecutionPlanNodeSummary,
|
|
37
37
|
displayNameByOwnerRef: Record<string, string> = {},
|
|
38
38
|
): string {
|
|
39
|
-
if (node.
|
|
40
|
-
return displayNameByOwnerRef[node.
|
|
39
|
+
if (node.ownerRef in displayNameByOwnerRef) {
|
|
40
|
+
return displayNameByOwnerRef[node.ownerRef]
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
return node.owner.ref
|
|
42
|
+
return node.ownerRef
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
export function buildExecutionPlanToolViewModel(params: { input: unknown; isRunning: boolean; output: unknown }) {
|
|
@@ -54,12 +53,11 @@ export function buildExecutionPlanToolViewModel(params: { input: unknown; isRunn
|
|
|
54
53
|
? params.input.title.trim()
|
|
55
54
|
: ''
|
|
56
55
|
const title = plan && plan.title.trim() ? plan.title.trim() : inputTitle || 'Execution run'
|
|
57
|
-
const completedCount = plan ? plan.progress.completed
|
|
56
|
+
const completedCount = plan ? plan.progress.completed : 0
|
|
58
57
|
|
|
59
58
|
return {
|
|
60
59
|
actionLabel: getExecutionPlanActionLabel(result?.action ?? 'none', params.isRunning),
|
|
61
60
|
hasPlan: plan !== null,
|
|
62
|
-
latestEvent: plan ? plan.recentEvents.at(-1)?.message.trim() : undefined,
|
|
63
61
|
plan,
|
|
64
62
|
progressSummary: plan ? `${completedCount}/${plan.progress.total} complete` : 'No active execution run',
|
|
65
63
|
result,
|
package/src/chat/transport.ts
DELETED
|
@@ -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
|
-
}
|