@lobehub/lobehub 2.0.0-next.327 → 2.0.0-next.329
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/.env.example +0 -3
- package/.env.example.development +0 -3
- package/CHANGELOG.md +58 -0
- package/Dockerfile +1 -2
- package/changelog/v1.json +18 -0
- package/docs/self-hosting/advanced/auth.mdx +5 -6
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
- package/docs/self-hosting/environment-variables/auth.mdx +0 -7
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
- package/locales/en-US/chat.json +6 -1
- package/locales/en-US/discover.json +1 -0
- package/locales/zh-CN/chat.json +5 -0
- package/locales/zh-CN/discover.json +1 -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/scripts/prebuild.mts +2 -2
- package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
- package/src/components/StreamingMarkdown/index.tsx +10 -43
- package/src/envs/__tests__/app.test.ts +81 -0
- package/src/envs/app.ts +14 -2
- package/src/envs/auth.test.ts +0 -13
- package/src/envs/auth.ts +0 -41
- 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/libs/better-auth/auth-client.ts +0 -9
- package/src/libs/better-auth/define-config.ts +13 -12
- package/src/libs/better-auth/sso/index.ts +2 -1
- package/src/libs/better-auth/utils/config.ts +2 -2
- package/src/libs/next/proxy/define-config.ts +4 -6
- package/src/locales/default/chat.ts +6 -1
- package/src/locales/default/discover.ts +2 -0
- package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
- package/src/server/routers/lambda/aiAgent.ts +239 -1
- package/src/server/routers/lambda/thread.ts +2 -0
- package/src/server/routers/lambda/topic.ts +6 -0
- package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
- package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
- 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/services/topic/index.ts +4 -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
- package/src/store/chat/slices/topic/action.test.ts +2 -1
- package/src/store/chat/slices/topic/action.ts +1 -1
|
@@ -407,18 +407,20 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
|
|
407
407
|
* Execute a single async task
|
|
408
408
|
*
|
|
409
409
|
* This method triggers async task execution by returning a special state.
|
|
410
|
-
* The AgentRuntime's executor will recognize this state and trigger the
|
|
410
|
+
* The AgentRuntime's executor will recognize this state and trigger the appropriate instruction.
|
|
411
411
|
*
|
|
412
412
|
* Flow:
|
|
413
|
-
* 1. GTD tool returns stop: true with state.type = 'execTask'
|
|
414
|
-
* 2. AgentRuntime executor recognizes the state and triggers exec_task instruction
|
|
415
|
-
* 3.
|
|
413
|
+
* 1. GTD tool returns stop: true with state.type = 'execTask' or 'execClientTask'
|
|
414
|
+
* 2. AgentRuntime executor recognizes the state and triggers exec_task or exec_client_task instruction
|
|
415
|
+
* 3. The executor creates task message and handles execution
|
|
416
|
+
*
|
|
417
|
+
* @param params.runInClient - If true, returns 'execClientTask' state for client-side execution
|
|
416
418
|
*/
|
|
417
419
|
execTask = async (
|
|
418
420
|
params: ExecTaskParams,
|
|
419
421
|
ctx: BuiltinToolContext,
|
|
420
422
|
): Promise<BuiltinToolResult> => {
|
|
421
|
-
const { description, instruction, inheritMessages, timeout } = params;
|
|
423
|
+
const { description, instruction, inheritMessages, timeout, runInClient } = params;
|
|
422
424
|
|
|
423
425
|
if (!description || !instruction) {
|
|
424
426
|
return {
|
|
@@ -427,19 +429,25 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
|
|
427
429
|
};
|
|
428
430
|
}
|
|
429
431
|
|
|
432
|
+
const task = {
|
|
433
|
+
description,
|
|
434
|
+
inheritMessages,
|
|
435
|
+
instruction,
|
|
436
|
+
runInClient,
|
|
437
|
+
timeout,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Determine state type based on runInClient
|
|
441
|
+
// If runInClient is true, return 'execClientTask' to trigger client-side executor
|
|
442
|
+
const stateType = runInClient ? 'execClientTask' : 'execTask';
|
|
443
|
+
|
|
430
444
|
// Return stop: true with special state that AgentRuntime will recognize
|
|
431
|
-
// The exec_task executor will be triggered by the runtime when it sees this state
|
|
432
445
|
return {
|
|
433
|
-
content: `🚀 Triggered async task for execution:\n- ${description}`,
|
|
446
|
+
content: `🚀 Triggered async task for ${runInClient ? 'client-side' : ''} execution:\n- ${description}`,
|
|
434
447
|
state: {
|
|
435
448
|
parentMessageId: ctx.messageId,
|
|
436
|
-
task
|
|
437
|
-
|
|
438
|
-
inheritMessages,
|
|
439
|
-
instruction,
|
|
440
|
-
timeout,
|
|
441
|
-
},
|
|
442
|
-
type: 'execTask',
|
|
449
|
+
task,
|
|
450
|
+
type: stateType,
|
|
443
451
|
},
|
|
444
452
|
stop: true,
|
|
445
453
|
success: true,
|
|
@@ -450,12 +458,15 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
|
|
450
458
|
* Execute one or more async tasks
|
|
451
459
|
*
|
|
452
460
|
* This method triggers async task execution by returning a special state.
|
|
453
|
-
* The AgentRuntime's executor will recognize this state and trigger the
|
|
461
|
+
* The AgentRuntime's executor will recognize this state and trigger the appropriate instruction.
|
|
454
462
|
*
|
|
455
463
|
* Flow:
|
|
456
|
-
* 1. GTD tool returns stop: true with state.type = 'execTasks'
|
|
457
|
-
* 2. AgentRuntime executor recognizes the state and triggers exec_tasks instruction
|
|
458
|
-
* 3.
|
|
464
|
+
* 1. GTD tool returns stop: true with state.type = 'execTasks' or 'execClientTasks'
|
|
465
|
+
* 2. AgentRuntime executor recognizes the state and triggers exec_tasks or exec_client_tasks instruction
|
|
466
|
+
* 3. The executor creates task messages and handles execution
|
|
467
|
+
*
|
|
468
|
+
* Note: If any task has runInClient=true, all tasks will be routed to 'execClientTasks'.
|
|
469
|
+
* This is because client-side execution is the "special" case requiring local tool access.
|
|
459
470
|
*/
|
|
460
471
|
execTasks = async (
|
|
461
472
|
params: ExecTasksParams,
|
|
@@ -473,14 +484,20 @@ class GTDExecutor extends BaseExecutor<typeof GTDApiNameEnum> {
|
|
|
473
484
|
const taskCount = tasks.length;
|
|
474
485
|
const taskList = tasks.map((t, i) => `${i + 1}. ${t.description}`).join('\n');
|
|
475
486
|
|
|
487
|
+
// Check if any task requires client-side execution
|
|
488
|
+
const hasClientTasks = tasks.some((t) => t.runInClient);
|
|
489
|
+
|
|
490
|
+
// Determine state type: if any task needs client-side, route all to client executor
|
|
491
|
+
const stateType = hasClientTasks ? 'execClientTasks' : 'execTasks';
|
|
492
|
+
const executionMode = hasClientTasks ? 'client-side' : '';
|
|
493
|
+
|
|
476
494
|
// Return stop: true with special state that AgentRuntime will recognize
|
|
477
|
-
// The exec_tasks executor will be triggered by the runtime when it sees this state
|
|
478
495
|
return {
|
|
479
|
-
content: `🚀 Triggered ${taskCount} async task${taskCount > 1 ? 's' : ''} for execution:\n${taskList}`,
|
|
496
|
+
content: `🚀 Triggered ${taskCount} async task${taskCount > 1 ? 's' : ''} for ${executionMode} execution:\n${taskList}`,
|
|
480
497
|
state: {
|
|
481
498
|
parentMessageId: ctx.messageId,
|
|
482
499
|
tasks,
|
|
483
|
-
type:
|
|
500
|
+
type: stateType,
|
|
484
501
|
},
|
|
485
502
|
stop: true,
|
|
486
503
|
success: true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BuiltinToolManifest } from '@lobechat/types';
|
|
2
2
|
|
|
3
|
+
import { isDesktop } from './const';
|
|
3
4
|
import { systemPrompt } from './systemRole';
|
|
4
5
|
import { GTDApiName } from './types';
|
|
5
6
|
|
|
@@ -171,6 +172,13 @@ export const GTDManifest: BuiltinToolManifest = {
|
|
|
171
172
|
'Whether to inherit context messages from the parent conversation. Default is false.',
|
|
172
173
|
type: 'boolean',
|
|
173
174
|
},
|
|
175
|
+
...(isDesktop && {
|
|
176
|
+
runInClient: {
|
|
177
|
+
description:
|
|
178
|
+
'Whether to run on the desktop client (for local file/shell access). MUST be true when task requires local-system tools. Default is false (server execution).',
|
|
179
|
+
type: 'boolean',
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
174
182
|
timeout: {
|
|
175
183
|
description: 'Optional timeout in milliseconds. Default is 30 minutes.',
|
|
176
184
|
type: 'number',
|
|
@@ -203,6 +211,13 @@ export const GTDManifest: BuiltinToolManifest = {
|
|
|
203
211
|
'Whether to inherit context messages from the parent conversation. Default is false.',
|
|
204
212
|
type: 'boolean',
|
|
205
213
|
},
|
|
214
|
+
...(isDesktop && {
|
|
215
|
+
runInClient: {
|
|
216
|
+
description:
|
|
217
|
+
'Whether to run on the desktop client (for local file/shell access). MUST be true when task requires local-system tools. Default is false (server execution).',
|
|
218
|
+
type: 'boolean',
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
206
221
|
timeout: {
|
|
207
222
|
description: 'Optional timeout in milliseconds. Default is 30 minutes.',
|
|
208
223
|
type: 'number',
|
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
import { isDesktop } from './const';
|
|
2
|
+
|
|
3
|
+
const runInClientSection = `
|
|
4
|
+
<run_in_client>
|
|
5
|
+
**IMPORTANT: When to use \`runInClient: true\` for async tasks**
|
|
6
|
+
|
|
7
|
+
The \`runInClient\` parameter controls WHERE the async task executes:
|
|
8
|
+
- \`runInClient: false\` (default): Task runs on the **server** - suitable for web searches, API calls, general research
|
|
9
|
+
- \`runInClient: true\`: Task runs on the **desktop client** - required for local system access
|
|
10
|
+
|
|
11
|
+
**MUST set \`runInClient: true\` when the task involves:**
|
|
12
|
+
- Reading or writing local files (via \`local-system\` tool)
|
|
13
|
+
- Executing shell commands on the user's machine
|
|
14
|
+
- Accessing local directories or file system
|
|
15
|
+
- Any operation that requires desktop-only local tools
|
|
16
|
+
|
|
17
|
+
**Keep \`runInClient: false\` (or omit) when:**
|
|
18
|
+
- Task only needs web searches or API calls
|
|
19
|
+
- Task processes data that doesn't require local file access
|
|
20
|
+
- Task can be fully completed with server-side capabilities
|
|
21
|
+
|
|
22
|
+
**Note:** \`runInClient\` only has effect on the **desktop app**. On web platform, tasks always run on the server regardless of this setting.
|
|
23
|
+
|
|
24
|
+
**Examples:**
|
|
25
|
+
- "Research Python best practices" → \`runInClient: false\` (web search only)
|
|
26
|
+
- "Organize files in my Downloads folder" → \`runInClient: true\` (local file access required)
|
|
27
|
+
- "Read the project README and summarize it" → \`runInClient: true\` (local file read required)
|
|
28
|
+
- "Find trending tech news" → \`runInClient: false\` (web search only)
|
|
29
|
+
- "Create a new directory structure for my project" → \`runInClient: true\` (local shell/file required)
|
|
30
|
+
</run_in_client>
|
|
31
|
+
`;
|
|
32
|
+
|
|
1
33
|
export const systemPrompt = `You have GTD (Getting Things Done) tools to help manage plans, todos and tasks effectively. These tools support three levels of task management:
|
|
2
34
|
|
|
3
35
|
- **Plan**: A high-level strategic document describing goals, context, and overall direction. Plans do NOT contain actionable steps - they define the "what" and "why". **Plans should be stable once created** - they represent the overarching objective that rarely changes.
|
|
@@ -85,7 +117,7 @@ Use \`execTask\` for a single task, \`execTasks\` for multiple parallel tasks.
|
|
|
85
117
|
- User asks a factual question you know → Just answer directly
|
|
86
118
|
- User wants multiple independent analyses → execTasks (parallel execution)
|
|
87
119
|
</when_to_use>
|
|
88
|
-
|
|
120
|
+
${isDesktop ? runInClientSection : ''}
|
|
89
121
|
<best_practices>
|
|
90
122
|
- **Plan first, then todos**: Always start with a plan unless explicitly told otherwise
|
|
91
123
|
- **Separate concerns**: Plans describe goals; Todos list actions
|
|
@@ -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({
|