@lobehub/lobehub 2.0.0-next.326 → 2.0.0-next.328

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 (56) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +14 -0
  3. package/locales/en-US/chat.json +6 -1
  4. package/locales/zh-CN/chat.json +5 -0
  5. package/package.json +1 -1
  6. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
  7. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
  8. package/packages/agent-runtime/src/types/instruction.ts +46 -2
  9. package/packages/builtin-tool-gtd/src/const.ts +1 -0
  10. package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
  11. package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
  12. package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
  13. package/packages/builtin-tool-gtd/src/types.ts +55 -33
  14. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
  15. package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
  16. package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
  17. package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
  18. package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
  19. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
  20. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
  21. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
  22. package/packages/database/src/models/message.ts +8 -1
  23. package/packages/database/src/models/thread.ts +1 -1
  24. package/packages/types/src/message/ui/chat.ts +2 -0
  25. package/packages/types/src/topic/thread.ts +20 -0
  26. package/src/components/StreamingMarkdown/index.tsx +10 -43
  27. package/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx +6 -6
  28. package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
  29. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
  30. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
  31. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
  32. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
  33. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
  34. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
  35. package/src/features/Conversation/Messages/Task/index.tsx +11 -6
  36. package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
  37. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
  38. package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
  39. package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
  40. package/src/features/Conversation/components/Thinking/index.tsx +9 -30
  41. package/src/features/Conversation/store/slices/data/action.ts +2 -3
  42. package/src/features/NavPanel/components/BackButton.tsx +10 -13
  43. package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
  44. package/src/hooks/useAutoScroll.ts +117 -0
  45. package/src/locales/default/chat.ts +6 -1
  46. package/src/server/routers/lambda/aiAgent.ts +239 -1
  47. package/src/server/routers/lambda/thread.ts +2 -0
  48. package/src/server/services/message/__tests__/index.test.ts +37 -0
  49. package/src/server/services/message/index.ts +6 -1
  50. package/src/services/aiAgent.ts +51 -0
  51. package/src/store/chat/agents/createAgentExecutors.ts +714 -12
  52. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
  53. package/src/store/chat/slices/message/actions/query.ts +33 -1
  54. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
  55. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
  56. package/src/store/chat/slices/operation/types.ts +4 -0
@@ -1,26 +1,23 @@
1
1
  import { ActionIcon, type ActionIconProps } from '@lobehub/ui';
2
2
  import { ChevronLeftIcon } from 'lucide-react';
3
3
  import { memo } from 'react';
4
- import { useNavigate } from 'react-router-dom';
4
+ import { Link } from 'react-router-dom';
5
5
 
6
6
  import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
7
7
 
8
8
  export const BACK_BUTTON_ID = 'lobe-back-button';
9
9
 
10
10
  const BackButton = memo<ActionIconProps & { to?: string }>(({ to = '/', onClick, ...rest }) => {
11
- const navigate = useNavigate();
12
-
13
11
  return (
14
- <ActionIcon
15
- icon={ChevronLeftIcon}
16
- id={BACK_BUTTON_ID}
17
- onClick={(e) => {
18
- navigate(to);
19
- onClick?.(e);
20
- }}
21
- size={DESKTOP_HEADER_ICON_SIZE}
22
- {...rest}
23
- />
12
+ // @ts-expect-error
13
+ <Link onClick={onClick} to={to}>
14
+ <ActionIcon
15
+ icon={ChevronLeftIcon}
16
+ id={BACK_BUTTON_ID}
17
+ size={DESKTOP_HEADER_ICON_SIZE}
18
+ {...rest}
19
+ />
20
+ </Link>
24
21
  );
25
22
  });
26
23
 
@@ -13,6 +13,7 @@ import { systemStatusSelectors } from '@/store/global/selectors';
13
13
  import { isMacOS } from '@/utils/platform';
14
14
 
15
15
  import { useNavPanelSizeChangeHandler } from '../hooks/useNavPanel';
16
+ import { BACK_BUTTON_ID } from './BackButton';
16
17
 
17
18
  const motionVariants = {
18
19
  animate: { opacity: 1, x: 0 },
@@ -76,6 +77,9 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
76
77
  opacity,
77
78
  width 0.2s ${cssVar.motionEaseOut};
78
79
  }
80
+ #${BACK_BUTTON_ID} {
81
+ width: 24px !important;
82
+ }
79
83
 
80
84
  &:hover {
81
85
  #${TOGGLE_BUTTON_ID} {
@@ -0,0 +1,117 @@
1
+ import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ interface UseAutoScrollOptions {
4
+ /**
5
+ * Dependencies that trigger auto-scroll when changed
6
+ */
7
+ deps?: unknown[];
8
+ /**
9
+ * Whether auto-scroll is enabled (e.g., only when streaming/executing)
10
+ * @default true
11
+ */
12
+ enabled?: boolean;
13
+ /**
14
+ * Distance threshold from bottom to consider "near bottom" (in pixels)
15
+ * @default 20
16
+ */
17
+ threshold?: number;
18
+ }
19
+
20
+ interface UseAutoScrollReturn<T extends HTMLElement> {
21
+ /**
22
+ * Callback to handle scroll events, attach to onScroll
23
+ */
24
+ handleScroll: () => void;
25
+ /**
26
+ * Ref to attach to the scrollable container
27
+ */
28
+ ref: RefObject<T | null>;
29
+ /**
30
+ * Reset the scroll lock state (e.g., when new content starts)
31
+ */
32
+ resetScrollLock: () => void;
33
+ /**
34
+ * Whether user has scrolled away from bottom (scroll lock active)
35
+ */
36
+ userHasScrolled: boolean;
37
+ }
38
+
39
+ /**
40
+ * Hook for auto-scrolling content with user scroll detection
41
+ *
42
+ * Features:
43
+ * - Auto-scrolls to bottom when dependencies change
44
+ * - Detects when user scrolls away from bottom and stops auto-scrolling
45
+ * - Provides reset function for when new content starts
46
+ * - Ignores scroll events triggered by auto-scroll itself
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
51
+ * deps: [content],
52
+ * enabled: isStreaming,
53
+ * });
54
+ *
55
+ * return (
56
+ * <ScrollShadow ref={ref} onScroll={handleScroll}>
57
+ * {content}
58
+ * </ScrollShadow>
59
+ * );
60
+ * ```
61
+ */
62
+ export function useAutoScroll<T extends HTMLElement = HTMLDivElement>(
63
+ options: UseAutoScrollOptions = {},
64
+ ): UseAutoScrollReturn<T> {
65
+ const { deps = [], enabled = true, threshold = 20 } = options;
66
+
67
+ const ref = useRef<T | null>(null);
68
+ const [userHasScrolled, setUserHasScrolled] = useState(false);
69
+ const isAutoScrollingRef = useRef(false);
70
+
71
+ // Handle user scroll detection
72
+ const handleScroll = useCallback(() => {
73
+ // Ignore scroll events triggered by auto-scroll
74
+ if (isAutoScrollingRef.current) return;
75
+
76
+ const container = ref.current;
77
+ if (!container) return;
78
+
79
+ // Check if user scrolled away from bottom
80
+ const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
81
+ const isAtBottom = distanceToBottom < threshold;
82
+
83
+ // If user scrolled up, stop auto-scrolling
84
+ if (!isAtBottom) {
85
+ setUserHasScrolled(true);
86
+ }
87
+ }, [threshold]);
88
+
89
+ // Reset scroll lock state
90
+ const resetScrollLock = useCallback(() => {
91
+ setUserHasScrolled(false);
92
+ }, []);
93
+
94
+ // Auto scroll to bottom when deps change (unless user has scrolled or disabled)
95
+ useEffect(() => {
96
+ if (!enabled || userHasScrolled) return;
97
+
98
+ const container = ref.current;
99
+ if (!container) return;
100
+
101
+ isAutoScrollingRef.current = true;
102
+ requestAnimationFrame(() => {
103
+ container.scrollTop = container.scrollHeight;
104
+ // Reset the flag after scroll completes
105
+ requestAnimationFrame(() => {
106
+ isAutoScrollingRef.current = false;
107
+ });
108
+ });
109
+ }, [enabled, userHasScrolled, ...deps]);
110
+
111
+ return {
112
+ handleScroll,
113
+ ref,
114
+ resetScrollLock,
115
+ userHasScrolled,
116
+ };
117
+ }
@@ -230,6 +230,7 @@ export default {
230
230
  'noSelectedAgents': 'No members selected yet',
231
231
  'openInNewWindow': 'Open in New Window',
232
232
  'operation.execAgentRuntime': 'Preparing response',
233
+ 'operation.execClientTask': 'Executing task',
233
234
  'operation.sendMessage': 'Sending message',
234
235
  'owner': 'Group owner',
235
236
  'pageCopilot.title': 'Page Agent',
@@ -354,13 +355,17 @@ export default {
354
355
  'tab.profile': 'Agent Profile',
355
356
  'tab.search': 'Search',
356
357
  'task.activity.calling': 'Calling Skill...',
358
+ 'task.activity.clientExecuting': 'Executing locally...',
357
359
  'task.activity.generating': 'Generating response...',
358
360
  'task.activity.gotResult': 'Tool result received',
359
361
  'task.activity.toolCalling': 'Calling {{toolName}}...',
360
362
  'task.activity.toolResult': '{{toolName}} result received',
361
363
  'task.batchTasks': '{{count}} Batch Subtasks',
364
+ 'task.instruction': 'Task Instruction',
365
+ 'task.intermediateSteps': '{{count}} intermediate steps',
366
+ 'task.metrics.duration': '(took {{duration}})',
362
367
  'task.metrics.stepsShort': 'steps',
363
- 'task.metrics.toolCallsShort': 'tool uses',
368
+ 'task.metrics.toolCallsShort': 'skill uses',
364
369
  'task.status.cancelled': 'Task Cancelled',
365
370
  'task.status.failed': 'Task Failed',
366
371
  'task.status.initializing': 'Initializing task...',
@@ -1,5 +1,10 @@
1
1
  import { type AgentRuntimeContext } from '@lobechat/agent-runtime';
2
- import { type TaskCurrentActivity, type TaskStatusResult, ThreadStatus } from '@lobechat/types';
2
+ import {
3
+ type TaskCurrentActivity,
4
+ type TaskStatusResult,
5
+ ThreadStatus,
6
+ ThreadType,
7
+ } from '@lobechat/types';
3
8
  import { TRPCError } from '@trpc/server';
4
9
  import debug from 'debug';
5
10
  import pMap from 'p-map';
@@ -154,6 +159,49 @@ const ExecSubAgentTaskSchema = z.object({
154
159
  topicId: z.string(),
155
160
  });
156
161
 
162
+ /**
163
+ * Schema for createClientTaskThread - create Thread for client-side task execution
164
+ * This is used when runInClient=true on desktop client
165
+ */
166
+ const CreateClientTaskThreadSchema = z.object({
167
+ /** The Agent ID to execute the task */
168
+ agentId: z.string(),
169
+ /** The Group ID (optional, only for Group mode) */
170
+ groupId: z.string().optional(),
171
+ /** Initial user message content (task instruction) */
172
+ instruction: z.string(),
173
+ /** The parent message ID (task message) */
174
+ parentMessageId: z.string(),
175
+ /** Task title (shown in UI, used as thread title) */
176
+ title: z.string().optional(),
177
+ /** The Topic ID */
178
+ topicId: z.string(),
179
+ });
180
+
181
+ /**
182
+ * Schema for updateClientTaskThreadStatus - update Thread status after client-side execution
183
+ */
184
+ const UpdateClientTaskThreadStatusSchema = z.object({
185
+ /** Completion reason */
186
+ completionReason: z.enum(['done', 'error', 'interrupted']),
187
+ /** Error message if failed */
188
+ error: z.string().optional(),
189
+ /** Thread metadata to update */
190
+ metadata: z
191
+ .object({
192
+ totalCost: z.number().optional(),
193
+ totalMessages: z.number().optional(),
194
+ totalSteps: z.number().optional(),
195
+ totalTokens: z.number().optional(),
196
+ totalToolCalls: z.number().optional(),
197
+ })
198
+ .optional(),
199
+ /** Result content (last assistant message) */
200
+ resultContent: z.string().optional(),
201
+ /** The Thread ID */
202
+ threadId: z.string(),
203
+ });
204
+
157
205
  /**
158
206
  * Schema for interruptTask - interrupt a running task
159
207
  */
@@ -184,6 +232,100 @@ const aiAgentProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
184
232
  });
185
233
 
186
234
  export const aiAgentRouter = router({
235
+ /**
236
+ * Create Thread for client-side task execution
237
+ *
238
+ * This endpoint is called by desktop client when runInClient=true.
239
+ * It creates the Thread but does NOT execute the task - execution happens on client side.
240
+ */
241
+ createClientTaskThread: aiAgentProcedure
242
+ .input(CreateClientTaskThreadSchema)
243
+ .mutation(async ({ input, ctx }) => {
244
+ const { agentId, groupId, instruction, parentMessageId, title, topicId } = input;
245
+
246
+ log('createClientTaskThread: agentId=%s, groupId=%s', agentId, groupId);
247
+
248
+ try {
249
+ // 1. Create Thread for isolated task execution
250
+ const startedAt = new Date().toISOString();
251
+ const thread = await ctx.threadModel.create({
252
+ agentId,
253
+ groupId,
254
+ metadata: { clientMode: true, startedAt },
255
+ sourceMessageId: parentMessageId,
256
+ status: ThreadStatus.Processing,
257
+ title,
258
+ topicId,
259
+ type: ThreadType.Isolation,
260
+ });
261
+
262
+ if (!thread) {
263
+ throw new TRPCError({
264
+ code: 'INTERNAL_SERVER_ERROR',
265
+ message: 'Failed to create thread for task execution',
266
+ });
267
+ }
268
+
269
+ log('createClientTaskThread: created thread %s', thread.id);
270
+
271
+ // 2. Create initial user message (persisted to database)
272
+ const userMessage = await ctx.messageModel.create({
273
+ agentId,
274
+ content: instruction,
275
+ parentId: parentMessageId,
276
+ role: 'user',
277
+ threadId: thread.id,
278
+ topicId,
279
+ });
280
+
281
+ log('createClientTaskThread: created user message %s', userMessage.id);
282
+
283
+ // 3. Query thread messages and main chat messages in parallel
284
+ const [threadMessages, messages] = await Promise.all([
285
+ // Thread messages (messages within this thread)
286
+ ctx.messageModel.query({
287
+ agentId,
288
+ threadId: thread.id,
289
+ topicId,
290
+ }),
291
+ // Main chat messages (messages without threadId, includes updated taskDetail)
292
+ ctx.messageModel.query({
293
+ agentId,
294
+ topicId,
295
+ // No threadId - matchThread will filter for threadId IS NULL (main chat)
296
+ }),
297
+ ]);
298
+
299
+ log(
300
+ 'createClientTaskThread: queried %d thread messages, %d main messages',
301
+ threadMessages.length,
302
+ messages.length,
303
+ );
304
+
305
+ // 4. Return Thread, userMessageId, threadMessages and messages
306
+ return {
307
+ messages,
308
+ startedAt,
309
+ success: true,
310
+ threadId: thread.id,
311
+ threadMessages,
312
+ userMessageId: userMessage.id,
313
+ };
314
+ } catch (error: any) {
315
+ log('createClientTaskThread failed: %O', error);
316
+
317
+ if (error instanceof TRPCError) {
318
+ throw error;
319
+ }
320
+
321
+ throw new TRPCError({
322
+ cause: error,
323
+ code: 'INTERNAL_SERVER_ERROR',
324
+ message: `Failed to create client task thread: ${error.message}`,
325
+ });
326
+ }
327
+ }),
328
+
187
329
  createOperation: aiAgentProcedure
188
330
  .input(CreateAgentOperationSchema)
189
331
  .mutation(async ({ input, ctx }) => {
@@ -846,4 +988,100 @@ export const aiAgentRouter = router({
846
988
  timestamp: new Date().toISOString(),
847
989
  };
848
990
  }),
991
+
992
+ /**
993
+ * Update Thread status after client-side task execution completes
994
+ *
995
+ * This endpoint is called by desktop client after task execution finishes.
996
+ * It updates the Thread status and metadata similar to server-side completion.
997
+ */
998
+ updateClientTaskThreadStatus: aiAgentProcedure
999
+ .input(UpdateClientTaskThreadStatusSchema)
1000
+ .mutation(async ({ input, ctx }) => {
1001
+ const { threadId, completionReason, error, resultContent, metadata } = input;
1002
+
1003
+ log('updateClientTaskThreadStatus: threadId=%s, reason=%s', threadId, completionReason);
1004
+
1005
+ try {
1006
+ // Find thread
1007
+ const thread = await ctx.threadModel.findById(threadId);
1008
+ if (!thread) {
1009
+ throw new TRPCError({
1010
+ code: 'NOT_FOUND',
1011
+ message: 'Thread not found',
1012
+ });
1013
+ }
1014
+
1015
+ const completedAt = new Date().toISOString();
1016
+ const startedAt = thread.metadata?.startedAt;
1017
+ const duration = startedAt ? Date.now() - new Date(startedAt).getTime() : undefined;
1018
+
1019
+ // Determine thread status based on completion reason
1020
+ let status: ThreadStatus;
1021
+ switch (completionReason) {
1022
+ case 'done': {
1023
+ status = ThreadStatus.Completed;
1024
+ break;
1025
+ }
1026
+ case 'error': {
1027
+ status = ThreadStatus.Failed;
1028
+ break;
1029
+ }
1030
+ case 'interrupted': {
1031
+ status = ThreadStatus.Cancel;
1032
+ break;
1033
+ }
1034
+ default: {
1035
+ status = ThreadStatus.Completed;
1036
+ }
1037
+ }
1038
+
1039
+ // Update Thread metadata and status
1040
+ await ctx.threadModel.update(threadId, {
1041
+ metadata: {
1042
+ ...thread.metadata,
1043
+ completedAt,
1044
+ duration,
1045
+ error: error || undefined,
1046
+ totalCost: metadata?.totalCost,
1047
+ totalMessages: metadata?.totalMessages,
1048
+ totalSteps: metadata?.totalSteps,
1049
+ totalTokens: metadata?.totalTokens,
1050
+ totalToolCalls: metadata?.totalToolCalls,
1051
+ },
1052
+ status,
1053
+ });
1054
+
1055
+ // Update task message (sourceMessageId) with result content if provided
1056
+ if (resultContent && thread.sourceMessageId) {
1057
+ await ctx.messageModel.update(thread.sourceMessageId, {
1058
+ content: resultContent,
1059
+ });
1060
+ log(
1061
+ 'updateClientTaskThreadStatus: updated task message %s with result',
1062
+ thread.sourceMessageId,
1063
+ );
1064
+ }
1065
+
1066
+ log('updateClientTaskThreadStatus: thread %s completed with status %s', threadId, status);
1067
+
1068
+ return {
1069
+ status,
1070
+ success: true,
1071
+ threadId,
1072
+ };
1073
+ } catch (error: any) {
1074
+ log('updateClientTaskThreadStatus failed: %O', error);
1075
+
1076
+ if (error instanceof TRPCError) {
1077
+ throw error;
1078
+ }
1079
+
1080
+ throw new TRPCError({
1081
+ cause: error,
1082
+ code: 'INTERNAL_SERVER_ERROR',
1083
+ message: `Failed to update client task thread status: ${error.message}`,
1084
+ });
1085
+ }
1086
+ }),
849
1087
  });
@@ -21,6 +21,7 @@ const threadProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
21
21
  export const threadRouter = router({
22
22
  createThread: threadProcedure.input(createThreadSchema).mutation(async ({ input, ctx }) => {
23
23
  const thread = await ctx.threadModel.create({
24
+ metadata: input.metadata,
24
25
  parentThreadId: input.parentThreadId,
25
26
  sourceMessageId: input.sourceMessageId,
26
27
  title: input.title,
@@ -38,6 +39,7 @@ export const threadRouter = router({
38
39
  )
39
40
  .mutation(async ({ input, ctx }) => {
40
41
  const thread = await ctx.threadModel.create({
42
+ metadata: input.metadata,
41
43
  parentThreadId: input.parentThreadId,
42
44
  sourceMessageId: input.sourceMessageId,
43
45
  title: input.message.content.slice(0, 20),
@@ -293,6 +293,7 @@ describe('MessageService', () => {
293
293
  current: 0,
294
294
  groupId: undefined,
295
295
  pageSize: 9999,
296
+ threadId: undefined,
296
297
  topicId: undefined,
297
298
  },
298
299
  expect.objectContaining({
@@ -327,6 +328,42 @@ describe('MessageService', () => {
327
328
  current: 0,
328
329
  groupId: 'group-1',
329
330
  pageSize: 9999,
331
+ threadId: undefined,
332
+ topicId: 'topic-1',
333
+ },
334
+ expect.objectContaining({
335
+ postProcessUrl: expect.any(Function),
336
+ }),
337
+ );
338
+ expect(result.id).toBe('msg-1');
339
+ expect(result.messages).toEqual(mockMessages);
340
+ });
341
+
342
+ it('should create message with threadId and query thread messages', async () => {
343
+ const params = {
344
+ agentId: 'agent-1',
345
+ content: 'Hello in thread',
346
+ groupId: 'group-1',
347
+ role: 'user' as const,
348
+ threadId: 'thread-1',
349
+ topicId: 'topic-1',
350
+ };
351
+ const createdMessage = { id: 'msg-1', ...params };
352
+ const mockMessages = [createdMessage];
353
+
354
+ vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
355
+ vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
356
+
357
+ const result = await messageService.createMessage(params as any);
358
+
359
+ expect(mockMessageModel.create).toHaveBeenCalledWith(params);
360
+ expect(mockMessageModel.query).toHaveBeenCalledWith(
361
+ {
362
+ agentId: 'agent-1',
363
+ current: 0,
364
+ groupId: 'group-1',
365
+ pageSize: 9999,
366
+ threadId: 'thread-1',
330
367
  topicId: 'topic-1',
331
368
  },
332
369
  expect.objectContaining({
@@ -1,5 +1,9 @@
1
1
  import { type LobeChatDatabase } from '@lobechat/database';
2
- import { type CreateMessageParams, type UIChatMessage, type UpdateMessageParams } from '@lobechat/types';
2
+ import {
3
+ type CreateMessageParams,
4
+ type UIChatMessage,
5
+ type UpdateMessageParams,
6
+ } from '@lobechat/types';
3
7
 
4
8
  import { MessageModel } from '@/database/models/message';
5
9
 
@@ -95,6 +99,7 @@ export class MessageService {
95
99
  current: 0,
96
100
  groupId: params.groupId,
97
101
  pageSize: 9999,
102
+ threadId: params.threadId,
98
103
  topicId: params.topicId,
99
104
  },
100
105
  {
@@ -40,6 +40,38 @@ export interface InterruptTaskParams {
40
40
  threadId?: string;
41
41
  }
42
42
 
43
+ /**
44
+ * Parameters for createClientTaskThread
45
+ * Creates a Thread for client-side task execution (desktop only)
46
+ */
47
+ export interface CreateClientTaskThreadParams {
48
+ agentId: string;
49
+ groupId?: string;
50
+ /** Initial user message content (task instruction) */
51
+ instruction: string;
52
+ parentMessageId: string;
53
+ title?: string;
54
+ topicId: string;
55
+ }
56
+
57
+ /**
58
+ * Parameters for updateClientTaskThreadStatus
59
+ * Updates Thread status after client-side execution completes
60
+ */
61
+ export interface UpdateClientTaskThreadStatusParams {
62
+ completionReason: 'done' | 'error' | 'interrupted';
63
+ error?: string;
64
+ metadata?: {
65
+ totalCost?: number;
66
+ totalMessages?: number;
67
+ totalSteps?: number;
68
+ totalTokens?: number;
69
+ totalToolCalls?: number;
70
+ };
71
+ resultContent?: string;
72
+ threadId: string;
73
+ }
74
+
43
75
  class AiAgentService {
44
76
  /**
45
77
  * Execute a single Agent task
@@ -72,6 +104,25 @@ class AiAgentService {
72
104
  async interruptTask(params: InterruptTaskParams) {
73
105
  return await lambdaClient.aiAgent.interruptTask.mutate(params);
74
106
  }
107
+
108
+ /**
109
+ * Create Thread for client-side task execution (desktop only)
110
+ *
111
+ * This method is called when runInClient=true on desktop client.
112
+ * It creates the Thread but does NOT execute the task - execution happens locally.
113
+ */
114
+ async createClientTaskThread(params: CreateClientTaskThreadParams) {
115
+ return await lambdaClient.aiAgent.createClientTaskThread.mutate(params);
116
+ }
117
+
118
+ /**
119
+ * Update Thread status after client-side task execution completes
120
+ *
121
+ * This method is called by desktop client after task execution finishes.
122
+ */
123
+ async updateClientTaskThreadStatus(params: UpdateClientTaskThreadStatusParams) {
124
+ return await lambdaClient.aiAgent.updateClientTaskThreadStatus.mutate(params);
125
+ }
75
126
  }
76
127
 
77
128
  export const aiAgentService = new AiAgentService();