@lobehub/lobehub 2.0.0-next.327 → 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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/chat.json +6 -1
- package/locales/zh-CN/chat.json +5 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
- package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
- package/packages/agent-runtime/src/types/instruction.ts +46 -2
- package/packages/builtin-tool-gtd/src/const.ts +1 -0
- package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
- package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
- package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
- package/packages/builtin-tool-gtd/src/types.ts +55 -33
- package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
- package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
- package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
- package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
- package/packages/database/src/models/message.ts +8 -1
- package/packages/database/src/models/thread.ts +1 -1
- package/packages/types/src/message/ui/chat.ts +2 -0
- package/packages/types/src/topic/thread.ts +20 -0
- package/src/components/StreamingMarkdown/index.tsx +10 -43
- package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
- package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
- package/src/features/Conversation/Messages/Task/index.tsx +11 -6
- package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
- package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
- package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
- package/src/features/Conversation/components/Thinking/index.tsx +9 -30
- package/src/features/Conversation/store/slices/data/action.ts +2 -3
- package/src/features/NavPanel/components/BackButton.tsx +10 -13
- package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
- package/src/hooks/useAutoScroll.ts +117 -0
- package/src/locales/default/chat.ts +6 -1
- package/src/server/routers/lambda/aiAgent.ts +239 -1
- package/src/server/routers/lambda/thread.ts +2 -0
- package/src/server/services/message/__tests__/index.test.ts +37 -0
- package/src/server/services/message/index.ts +6 -1
- package/src/services/aiAgent.ts +51 -0
- package/src/store/chat/agents/createAgentExecutors.ts +714 -12
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
- package/src/store/chat/slices/message/actions/query.ts +33 -1
- package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
- package/src/store/chat/slices/operation/types.ts +4 -0
|
@@ -14,43 +14,25 @@ export const GTDApiName = {
|
|
|
14
14
|
/** Clear completed or all todos */
|
|
15
15
|
clearTodos: 'clearTodos',
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
// ==================== Planning ====================
|
|
19
|
-
/** Create a structured plan by breaking down a goal into actionable steps */
|
|
20
|
-
createPlan: 'createPlan',
|
|
18
|
+
/** Create a structured plan by breaking down a goal into actionable steps */
|
|
19
|
+
createPlan: 'createPlan',
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/** Create new todo items */
|
|
22
|
+
createTodos: 'createTodos',
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// ==================== Async Tasks ====================
|
|
25
|
+
/** Execute a single async task */
|
|
26
|
+
execTask: 'execTask',
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// ==================== Async Tasks ====================
|
|
32
|
-
/** Execute a single async task */
|
|
33
|
-
execTask: 'execTask',
|
|
28
|
+
/** Execute one or more async tasks */
|
|
29
|
+
execTasks: 'execTasks',
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
/** Update an existing plan */
|
|
32
|
+
updatePlan: 'updatePlan',
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
execTasks: 'execTasks',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/** Update an existing plan */
|
|
48
|
-
updatePlan: 'updatePlan',
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** Update todo items with batch operations (add, update, remove, complete, processing) */
|
|
53
|
-
updateTodos: 'updateTodos',
|
|
34
|
+
/** Update todo items with batch operations (add, update, remove, complete, processing) */
|
|
35
|
+
updateTodos: 'updateTodos',
|
|
54
36
|
} as const;
|
|
55
37
|
|
|
56
38
|
export type GTDApiNameType = (typeof GTDApiName)[keyof typeof GTDApiName];
|
|
@@ -258,6 +240,14 @@ export interface ExecTaskItem {
|
|
|
258
240
|
inheritMessages?: boolean;
|
|
259
241
|
/** Detailed instruction/prompt for the task execution */
|
|
260
242
|
instruction: string;
|
|
243
|
+
/**
|
|
244
|
+
* Whether to execute the task on the client side (desktop only).
|
|
245
|
+
* When true and running on desktop, the task will be executed locally
|
|
246
|
+
* with access to local tools (file system, shell commands, etc.).
|
|
247
|
+
*
|
|
248
|
+
* MUST be true when task requires local-system tools.
|
|
249
|
+
*/
|
|
250
|
+
runInClient?: boolean;
|
|
261
251
|
/** Timeout in milliseconds (optional, default 30 minutes) */
|
|
262
252
|
timeout?: number;
|
|
263
253
|
}
|
|
@@ -273,6 +263,14 @@ export interface ExecTaskParams {
|
|
|
273
263
|
inheritMessages?: boolean;
|
|
274
264
|
/** Detailed instruction/prompt for the task execution */
|
|
275
265
|
instruction: string;
|
|
266
|
+
/**
|
|
267
|
+
* Whether to execute the task on the client side (desktop only).
|
|
268
|
+
* When true and running on desktop, the task will be executed locally
|
|
269
|
+
* with access to local tools (file system, shell commands, etc.).
|
|
270
|
+
*
|
|
271
|
+
* MUST be true when task requires local-system tools.
|
|
272
|
+
*/
|
|
273
|
+
runInClient?: boolean;
|
|
276
274
|
/** Timeout in milliseconds (optional, default 30 minutes) */
|
|
277
275
|
timeout?: number;
|
|
278
276
|
}
|
|
@@ -287,7 +285,7 @@ export interface ExecTasksParams {
|
|
|
287
285
|
}
|
|
288
286
|
|
|
289
287
|
/**
|
|
290
|
-
* State returned after triggering exec_task
|
|
288
|
+
* State returned after triggering exec_task (server-side)
|
|
291
289
|
*/
|
|
292
290
|
export interface ExecTaskState {
|
|
293
291
|
/** Parent message ID (tool message) */
|
|
@@ -299,7 +297,7 @@ export interface ExecTaskState {
|
|
|
299
297
|
}
|
|
300
298
|
|
|
301
299
|
/**
|
|
302
|
-
* State returned after triggering exec_tasks
|
|
300
|
+
* State returned after triggering exec_tasks (server-side)
|
|
303
301
|
*/
|
|
304
302
|
export interface ExecTasksState {
|
|
305
303
|
/** Parent message ID (tool message) */
|
|
@@ -309,3 +307,27 @@ export interface ExecTasksState {
|
|
|
309
307
|
/** Type identifier for render component */
|
|
310
308
|
type: 'execTasks';
|
|
311
309
|
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* State returned after triggering exec_client_task (client-side, desktop only)
|
|
313
|
+
*/
|
|
314
|
+
export interface ExecClientTaskState {
|
|
315
|
+
/** Parent message ID (tool message) */
|
|
316
|
+
parentMessageId: string;
|
|
317
|
+
/** The task definition that was triggered */
|
|
318
|
+
task: ExecTaskItem;
|
|
319
|
+
/** Type identifier for render component */
|
|
320
|
+
type: 'execClientTask';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* State returned after triggering exec_client_tasks (client-side, desktop only)
|
|
325
|
+
*/
|
|
326
|
+
export interface ExecClientTasksState {
|
|
327
|
+
/** Parent message ID (tool message) */
|
|
328
|
+
parentMessageId: string;
|
|
329
|
+
/** Array of task definitions that were triggered */
|
|
330
|
+
tasks: ExecTaskItem[];
|
|
331
|
+
/** Type identifier for render component */
|
|
332
|
+
type: 'execClientTasks';
|
|
333
|
+
}
|
|
@@ -47,7 +47,7 @@ export const RunCommandInspector = memo<BuiltinInspectorProps<RunCommandParams,
|
|
|
47
47
|
|
|
48
48
|
// Get execution result from pluginState
|
|
49
49
|
const result = pluginState?.result;
|
|
50
|
-
const isSuccess = result?.success
|
|
50
|
+
const isSuccess = result?.success || result?.exit_code === 0;
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
53
|
<div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
|
|
@@ -32,7 +32,7 @@ const WriteFile = memo<BuiltinRenderProps<WriteLocalFileParams>>(({ args }) => {
|
|
|
32
32
|
|
|
33
33
|
if (ext === 'md' || ext === 'mdx') {
|
|
34
34
|
return (
|
|
35
|
-
<Markdown style={{ maxHeight: 240, overflow: 'auto', padding: '0 8px' }}>
|
|
35
|
+
<Markdown style={{ maxHeight: 240, overflow: 'auto', padding: '0 8px' }} variant={'chat'}>
|
|
36
36
|
{args.content}
|
|
37
37
|
</Markdown>
|
|
38
38
|
);
|
|
@@ -19,7 +19,11 @@ export const WriteFileStreaming = memo<BuiltinStreamingProps<WriteLocalFileParam
|
|
|
19
19
|
|
|
20
20
|
// Use Markdown for .md files, Highlighter for others
|
|
21
21
|
if (ext === 'md' || ext === 'mdx') {
|
|
22
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
<Markdown animated style={{ overflow: 'auto' }} variant={'chat'}>
|
|
24
|
+
{content}
|
|
25
|
+
</Markdown>
|
|
26
|
+
);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
return (
|
|
@@ -12,11 +12,17 @@ Note: The list of existing documents is automatically provided in the context, s
|
|
|
12
12
|
|
|
13
13
|
<when_to_use>
|
|
14
14
|
**Save to Notebook when**:
|
|
15
|
-
- Creating reports, analyses, or summaries that should persist
|
|
16
15
|
- User explicitly asks to "save", "write down", or "document" something
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
16
|
+
- Creating substantial content meant to persist (reports, articles, analyses)
|
|
17
|
+
- Generating structured deliverables the user will likely reference later
|
|
18
|
+
- Web browsing results worth keeping for future reference
|
|
19
|
+
|
|
20
|
+
**Do NOT save to Notebook when**:
|
|
21
|
+
- User asks a simple question (just answer directly)
|
|
22
|
+
- Providing explanations, tutorials, or how-to responses
|
|
23
|
+
- Having casual conversations or discussions
|
|
24
|
+
- Content is short, temporary, or doesn't need persistence
|
|
25
|
+
- User didn't request saving and the content isn't a clear deliverable
|
|
20
26
|
|
|
21
27
|
**Document Types**:
|
|
22
28
|
- markdown: General formatted text (default)
|
|
@@ -44,8 +50,22 @@ Note: The list of existing documents is automatically provided in the context, s
|
|
|
44
50
|
|
|
45
51
|
<response_format>
|
|
46
52
|
After creating/updating documents:
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
53
|
+
- Confirm the action briefly: "Saved to Notebook: [title]"
|
|
54
|
+
- Provide a short summary (2-4 bullet points max) highlighting only the key takeaways
|
|
55
|
+
- NEVER repeat or rephrase the full document content - the user just saw it being created
|
|
56
|
+
- Optionally mention they can view/edit in the sidebar
|
|
57
|
+
|
|
58
|
+
❌ Bad (repeating content):
|
|
59
|
+
"I've saved 'Project Plan' which covers the three-phase implementation approach. Phase 1 focuses on user research and requirements gathering from March to April. Phase 2 involves design and prototyping from May to June. Phase 3 covers development and testing from July to September..."
|
|
60
|
+
|
|
61
|
+
✅ Good (brief summary):
|
|
62
|
+
"Saved to Notebook: Project Plan
|
|
63
|
+
|
|
64
|
+
Key points:
|
|
65
|
+
- 3-phase approach: Research → Design → Development
|
|
66
|
+
- Timeline: March - September
|
|
67
|
+
- 5 team members involved
|
|
68
|
+
|
|
69
|
+
You can edit it in the sidebar."
|
|
50
70
|
</response_format>
|
|
51
71
|
`;
|
|
@@ -31,8 +31,20 @@ export class FlatListBuilder {
|
|
|
31
31
|
const flatList: Message[] = [];
|
|
32
32
|
const processedIds = new Set<string>();
|
|
33
33
|
|
|
34
|
+
// Determine the root parentId
|
|
35
|
+
// Normal case: start from null (messages with no parentId)
|
|
36
|
+
// Orphan case: if all messages have parentId (thread mode), use first message as root
|
|
37
|
+
let rootParentId: string | null = null;
|
|
38
|
+
|
|
39
|
+
const hasRootMessages = this.childrenMap.has(null) && this.childrenMap.get(null)!.length > 0;
|
|
40
|
+
if (!hasRootMessages && messages.length > 0) {
|
|
41
|
+
// All messages have parentId - this is orphan/thread mode
|
|
42
|
+
// Use the first message's parentId as the virtual root
|
|
43
|
+
rootParentId = messages[0].parentId ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
// Build the active path by traversing from root
|
|
35
|
-
this.buildFlatListRecursive(
|
|
47
|
+
this.buildFlatListRecursive(rootParentId, flatList, processedIds, messages);
|
|
36
48
|
|
|
37
49
|
return flatList;
|
|
38
50
|
}
|
|
@@ -517,5 +517,45 @@ describe('FlatListBuilder', () => {
|
|
|
517
517
|
// User message should not have branch info since we're in optimistic update mode
|
|
518
518
|
expect((result[0] as any).siblingCount).toBeUndefined();
|
|
519
519
|
});
|
|
520
|
+
|
|
521
|
+
it('should handle orphan messages where all have parentId (thread mode)', () => {
|
|
522
|
+
// Scenario: Thread messages where the parent (source message) is not in the query result
|
|
523
|
+
// All messages have parentId pointing to a message not in the array
|
|
524
|
+
const messages: Message[] = [
|
|
525
|
+
{
|
|
526
|
+
content: 'Thread user message',
|
|
527
|
+
createdAt: 0,
|
|
528
|
+
id: 'msg-1',
|
|
529
|
+
parentId: 'source-msg-not-in-array',
|
|
530
|
+
role: 'user',
|
|
531
|
+
updatedAt: 0,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
content: 'Thread assistant reply',
|
|
535
|
+
createdAt: 0,
|
|
536
|
+
id: 'msg-2',
|
|
537
|
+
parentId: 'msg-1',
|
|
538
|
+
role: 'assistant',
|
|
539
|
+
updatedAt: 0,
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
content: 'Thread user follow-up',
|
|
543
|
+
createdAt: 0,
|
|
544
|
+
id: 'msg-3',
|
|
545
|
+
parentId: 'msg-2',
|
|
546
|
+
role: 'user',
|
|
547
|
+
updatedAt: 0,
|
|
548
|
+
},
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
const builder = createBuilder(messages);
|
|
552
|
+
const result = builder.flatten(messages);
|
|
553
|
+
|
|
554
|
+
// Should flatten all messages correctly using first message's parentId as virtual root
|
|
555
|
+
expect(result).toHaveLength(3);
|
|
556
|
+
expect(result[0].id).toBe('msg-1');
|
|
557
|
+
expect(result[1].id).toBe('msg-2');
|
|
558
|
+
expect(result[2].id).toBe('msg-3');
|
|
559
|
+
});
|
|
520
560
|
});
|
|
521
561
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { eq, sql } from 'drizzle-orm';
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
3
|
|
|
4
|
+
import { getTestDB } from '../../../core/getTestDB';
|
|
4
5
|
import { agents, messages, sessions, threads, topics, users } from '../../../schemas';
|
|
5
6
|
import { LobeChatDatabase } from '../../../type';
|
|
6
7
|
import { MessageModel } from '../../message';
|
|
7
|
-
import { getTestDB } from '../../../core/getTestDB';
|
|
8
8
|
|
|
9
9
|
const serverDB: LobeChatDatabase = await getTestDB();
|
|
10
10
|
|
|
@@ -337,6 +337,90 @@ describe('MessageModel thread query', () => {
|
|
|
337
337
|
expect(result.map((m) => m.id)).toEqual(['msg2', 'thread-msg1', 'thread-msg2']);
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
it('should return only thread messages for Isolation type (no parent messages)', async () => {
|
|
341
|
+
await serverDB.transaction(async (trx) => {
|
|
342
|
+
await trx.insert(agents).values([{ id: 'agent1', userId }]);
|
|
343
|
+
await trx.insert(topics).values([{ id: 'topic1', userId }]);
|
|
344
|
+
|
|
345
|
+
// Create thread with Isolation type
|
|
346
|
+
await trx.insert(threads).values([
|
|
347
|
+
{
|
|
348
|
+
id: 'thread1',
|
|
349
|
+
userId,
|
|
350
|
+
topicId: 'topic1',
|
|
351
|
+
sourceMessageId: 'msg2',
|
|
352
|
+
type: 'isolation',
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
// Create main conversation messages
|
|
357
|
+
await trx.insert(messages).values([
|
|
358
|
+
{
|
|
359
|
+
id: 'msg1',
|
|
360
|
+
userId,
|
|
361
|
+
agentId: 'agent1',
|
|
362
|
+
topicId: 'topic1',
|
|
363
|
+
threadId: null,
|
|
364
|
+
role: 'user',
|
|
365
|
+
content: 'first message',
|
|
366
|
+
createdAt: new Date('2023-01-01'),
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: 'msg2',
|
|
370
|
+
userId,
|
|
371
|
+
agentId: 'agent1',
|
|
372
|
+
topicId: 'topic1',
|
|
373
|
+
threadId: null,
|
|
374
|
+
role: 'assistant',
|
|
375
|
+
content: 'second message - source',
|
|
376
|
+
createdAt: new Date('2023-01-02'),
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: 'msg3',
|
|
380
|
+
userId,
|
|
381
|
+
agentId: 'agent1',
|
|
382
|
+
topicId: 'topic1',
|
|
383
|
+
threadId: null,
|
|
384
|
+
role: 'user',
|
|
385
|
+
content: 'third message',
|
|
386
|
+
createdAt: new Date('2023-01-03'),
|
|
387
|
+
},
|
|
388
|
+
// Thread messages
|
|
389
|
+
{
|
|
390
|
+
id: 'thread-msg1',
|
|
391
|
+
userId,
|
|
392
|
+
agentId: 'agent1',
|
|
393
|
+
topicId: 'topic1',
|
|
394
|
+
threadId: 'thread1',
|
|
395
|
+
role: 'user',
|
|
396
|
+
content: 'thread message 1',
|
|
397
|
+
createdAt: new Date('2023-01-02T10:00:00'),
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
id: 'thread-msg2',
|
|
401
|
+
userId,
|
|
402
|
+
agentId: 'agent1',
|
|
403
|
+
topicId: 'topic1',
|
|
404
|
+
threadId: 'thread1',
|
|
405
|
+
role: 'assistant',
|
|
406
|
+
content: 'thread message 2',
|
|
407
|
+
createdAt: new Date('2023-01-02T11:00:00'),
|
|
408
|
+
},
|
|
409
|
+
]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const result = await messageModel.query({
|
|
413
|
+
agentId: 'agent1',
|
|
414
|
+
topicId: 'topic1',
|
|
415
|
+
threadId: 'thread1',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// For Isolation: should include ONLY thread messages, NO parent messages at all
|
|
419
|
+
// Should NOT include msg1, msg2 (source), or msg3
|
|
420
|
+
expect(result).toHaveLength(2);
|
|
421
|
+
expect(result.map((m) => m.id)).toEqual(['thread-msg1', 'thread-msg2']);
|
|
422
|
+
});
|
|
423
|
+
|
|
340
424
|
it('should return only thread messages when thread has no sourceMessageId', async () => {
|
|
341
425
|
await serverDB.transaction(async (trx) => {
|
|
342
426
|
await trx.insert(sessions).values([{ id: 'session1', userId }]);
|
|
@@ -572,6 +656,55 @@ describe('MessageModel thread query', () => {
|
|
|
572
656
|
expect(result[0].id).toBe('msg2');
|
|
573
657
|
});
|
|
574
658
|
|
|
659
|
+
it('should return empty array for Isolation thread type', async () => {
|
|
660
|
+
await serverDB.transaction(async (trx) => {
|
|
661
|
+
await trx.insert(agents).values([{ id: 'agent1', userId }]);
|
|
662
|
+
await trx.insert(topics).values([{ id: 'topic1', userId }]);
|
|
663
|
+
|
|
664
|
+
await trx.insert(messages).values([
|
|
665
|
+
{
|
|
666
|
+
id: 'msg1',
|
|
667
|
+
userId,
|
|
668
|
+
agentId: 'agent1',
|
|
669
|
+
topicId: 'topic1',
|
|
670
|
+
threadId: null,
|
|
671
|
+
role: 'user',
|
|
672
|
+
content: 'first',
|
|
673
|
+
createdAt: new Date('2023-01-01'),
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
id: 'msg2',
|
|
677
|
+
userId,
|
|
678
|
+
agentId: 'agent1',
|
|
679
|
+
topicId: 'topic1',
|
|
680
|
+
threadId: null,
|
|
681
|
+
role: 'assistant',
|
|
682
|
+
content: 'second - source',
|
|
683
|
+
createdAt: new Date('2023-01-02'),
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: 'msg3',
|
|
687
|
+
userId,
|
|
688
|
+
agentId: 'agent1',
|
|
689
|
+
topicId: 'topic1',
|
|
690
|
+
threadId: null,
|
|
691
|
+
role: 'user',
|
|
692
|
+
content: 'third',
|
|
693
|
+
createdAt: new Date('2023-01-03'),
|
|
694
|
+
},
|
|
695
|
+
]);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const result = await messageModel.getThreadParentMessages({
|
|
699
|
+
sourceMessageId: 'msg2',
|
|
700
|
+
topicId: 'topic1',
|
|
701
|
+
threadType: 'isolation' as any,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Isolation should return empty array (no parent messages)
|
|
705
|
+
expect(result).toHaveLength(0);
|
|
706
|
+
});
|
|
707
|
+
|
|
575
708
|
it('should return all messages up to source message for Continuation thread type', async () => {
|
|
576
709
|
await serverDB.transaction(async (trx) => {
|
|
577
710
|
await trx.insert(sessions).values([{ id: 'session1', userId }]);
|
|
@@ -431,6 +431,7 @@ export class MessageModel {
|
|
|
431
431
|
return [
|
|
432
432
|
t.sourceMessageId!,
|
|
433
433
|
{
|
|
434
|
+
clientMode: metadata?.clientMode as boolean | undefined,
|
|
434
435
|
duration: metadata?.duration as number | undefined,
|
|
435
436
|
status: t.status as ThreadStatus,
|
|
436
437
|
threadId: t.threadId,
|
|
@@ -678,10 +679,11 @@ export class MessageModel {
|
|
|
678
679
|
* @param params - Parameters for getting parent messages
|
|
679
680
|
* @param params.sourceMessageId - The ID of the source message that started the thread
|
|
680
681
|
* @param params.topicId - The topic ID the thread belongs to
|
|
681
|
-
* @param params.threadType - The type of thread (Continuation or
|
|
682
|
+
* @param params.threadType - The type of thread (Continuation, Standalone, or Isolation)
|
|
682
683
|
* @returns Parent messages based on thread type:
|
|
683
684
|
* - Continuation: All messages from the topic up to and including the source message
|
|
684
685
|
* - Standalone: Only the source message itself
|
|
686
|
+
* - Isolation: No parent messages (completely isolated thread)
|
|
685
687
|
*/
|
|
686
688
|
getThreadParentMessages = async (params: {
|
|
687
689
|
sourceMessageId: string;
|
|
@@ -690,6 +692,11 @@ export class MessageModel {
|
|
|
690
692
|
}): Promise<DBMessageItem[]> => {
|
|
691
693
|
const { sourceMessageId, topicId, threadType } = params;
|
|
692
694
|
|
|
695
|
+
// For Isolation type, return empty array (no parent messages)
|
|
696
|
+
if (threadType === ThreadType.Isolation) {
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
|
|
693
700
|
// For Standalone type, only return the source message
|
|
694
701
|
if (threadType === ThreadType.Standalone) {
|
|
695
702
|
const sourceMessage = await this.db.query.messages.findFirst({
|
|
@@ -32,7 +32,7 @@ export class ThreadModel {
|
|
|
32
32
|
// @ts-ignore
|
|
33
33
|
const [result] = await this.db
|
|
34
34
|
.insert(threads)
|
|
35
|
-
.values({
|
|
35
|
+
.values({ status: ThreadStatus.Active, ...params, userId: this.userId })
|
|
36
36
|
.onConflictDoNothing()
|
|
37
37
|
.returning();
|
|
38
38
|
|
|
@@ -64,6 +64,8 @@ interface UIMessageBranch {
|
|
|
64
64
|
* Retrieved from the associated Thread via sourceMessageId
|
|
65
65
|
*/
|
|
66
66
|
export interface TaskDetail {
|
|
67
|
+
/** Whether this task runs in client mode (local execution) */
|
|
68
|
+
clientMode?: boolean;
|
|
67
69
|
/** Task completion time (ISO string) */
|
|
68
70
|
completedAt?: string;
|
|
69
71
|
/** Execution duration in milliseconds */
|
|
@@ -23,6 +23,8 @@ export enum ThreadStatus {
|
|
|
23
23
|
* Metadata for Thread, used for agent task execution
|
|
24
24
|
*/
|
|
25
25
|
export interface ThreadMetadata {
|
|
26
|
+
/** Whether this thread runs in client mode (local execution) */
|
|
27
|
+
clientMode?: boolean;
|
|
26
28
|
/** Task completion time */
|
|
27
29
|
completedAt?: string;
|
|
28
30
|
/** Execution duration in milliseconds */
|
|
@@ -68,16 +70,34 @@ export interface CreateThreadParams {
|
|
|
68
70
|
agentId?: string;
|
|
69
71
|
/** Group ID for group chat context */
|
|
70
72
|
groupId?: string;
|
|
73
|
+
/** Initial metadata for the thread */
|
|
74
|
+
metadata?: ThreadMetadata;
|
|
71
75
|
parentThreadId?: string;
|
|
72
76
|
sourceMessageId?: string;
|
|
77
|
+
/** Initial status (defaults to Active) */
|
|
78
|
+
status?: ThreadStatus;
|
|
73
79
|
title?: string;
|
|
74
80
|
topicId: string;
|
|
75
81
|
type: IThreadType;
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
export const threadMetadataSchema = z.object({
|
|
85
|
+
clientMode: z.boolean().optional(),
|
|
86
|
+
completedAt: z.string().optional(),
|
|
87
|
+
duration: z.number().optional(),
|
|
88
|
+
error: z.any().optional(),
|
|
89
|
+
operationId: z.string().optional(),
|
|
90
|
+
startedAt: z.string().optional(),
|
|
91
|
+
totalCost: z.number().optional(),
|
|
92
|
+
totalMessages: z.number().optional(),
|
|
93
|
+
totalTokens: z.number().optional(),
|
|
94
|
+
totalToolCalls: z.number().optional(),
|
|
95
|
+
});
|
|
96
|
+
|
|
78
97
|
export const createThreadSchema = z.object({
|
|
79
98
|
agentId: z.string().optional(),
|
|
80
99
|
groupId: z.string().optional(),
|
|
100
|
+
metadata: threadMetadataSchema.optional(),
|
|
81
101
|
parentThreadId: z.string().optional(),
|
|
82
102
|
sourceMessageId: z.string().optional(),
|
|
83
103
|
title: z.string().optional(),
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { Markdown, ScrollShadow } from '@lobehub/ui';
|
|
4
4
|
import { createStaticStyles } from 'antd-style';
|
|
5
|
-
import {
|
|
5
|
+
import { type RefObject, memo, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useAutoScroll } from '@/hooks/useAutoScroll';
|
|
6
8
|
|
|
7
9
|
const styles = createStaticStyles(({ css }) => ({
|
|
8
10
|
container: css`
|
|
@@ -19,51 +21,16 @@ interface StreamingMarkdownProps {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const StreamingMarkdown = memo<StreamingMarkdownProps>(({ children, maxHeight = 400 }) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Handle user scroll detection
|
|
27
|
-
const handleScroll = useCallback(() => {
|
|
28
|
-
// Ignore scroll events triggered by auto-scroll
|
|
29
|
-
if (isAutoScrollingRef.current) return;
|
|
30
|
-
|
|
31
|
-
const container = containerRef.current;
|
|
32
|
-
if (!container) return;
|
|
33
|
-
|
|
34
|
-
// Check if user scrolled away from bottom
|
|
35
|
-
const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
36
|
-
const isAtBottom = distanceToBottom < 20;
|
|
37
|
-
|
|
38
|
-
// If user scrolled up, stop auto-scrolling
|
|
39
|
-
if (!isAtBottom) {
|
|
40
|
-
setUserHasScrolled(true);
|
|
41
|
-
}
|
|
42
|
-
}, []);
|
|
43
|
-
|
|
44
|
-
// Auto scroll to bottom when content changes (unless user has scrolled)
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (userHasScrolled) return;
|
|
47
|
-
|
|
48
|
-
const container = containerRef.current;
|
|
49
|
-
if (!container) return;
|
|
50
|
-
|
|
51
|
-
isAutoScrollingRef.current = true;
|
|
52
|
-
requestAnimationFrame(() => {
|
|
53
|
-
container.scrollTop = container.scrollHeight;
|
|
54
|
-
// Reset the flag after scroll completes
|
|
55
|
-
requestAnimationFrame(() => {
|
|
56
|
-
isAutoScrollingRef.current = false;
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}, [children, userHasScrolled]);
|
|
24
|
+
const { ref, handleScroll, resetScrollLock } = useAutoScroll<HTMLDivElement>({
|
|
25
|
+
deps: [children],
|
|
26
|
+
});
|
|
60
27
|
|
|
61
|
-
// Reset
|
|
28
|
+
// Reset scroll lock when content is cleared (new stream starts)
|
|
62
29
|
useEffect(() => {
|
|
63
30
|
if (!children) {
|
|
64
|
-
|
|
31
|
+
resetScrollLock();
|
|
65
32
|
}
|
|
66
|
-
}, [children]);
|
|
33
|
+
}, [children, resetScrollLock]);
|
|
67
34
|
|
|
68
35
|
if (!children) return null;
|
|
69
36
|
|
|
@@ -72,7 +39,7 @@ const StreamingMarkdown = memo<StreamingMarkdownProps>(({ children, maxHeight =
|
|
|
72
39
|
className={styles.container}
|
|
73
40
|
offset={12}
|
|
74
41
|
onScroll={handleScroll}
|
|
75
|
-
ref={
|
|
42
|
+
ref={ref as RefObject<HTMLDivElement>}
|
|
76
43
|
size={12}
|
|
77
44
|
style={{ maxHeight }}
|
|
78
45
|
>
|
|
@@ -28,8 +28,6 @@ const MessageContent = memo<ContentBlockProps>(({ content, hasTools, id }) => {
|
|
|
28
28
|
if (!content && !hasTools) return <ContentLoading id={id} />;
|
|
29
29
|
|
|
30
30
|
if (content === LOADING_FLAT) {
|
|
31
|
-
if (hasTools) return null;
|
|
32
|
-
|
|
33
31
|
return <ContentLoading id={id} />;
|
|
34
32
|
}
|
|
35
33
|
|