@lobehub/lobehub 2.0.0-next.286 → 2.0.0-next.288

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 (72) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/apps/desktop/src/main/const/theme.ts +0 -3
  3. package/apps/desktop/src/main/core/browser/Browser.ts +1 -1
  4. package/apps/desktop/src/main/core/browser/WindowThemeManager.ts +3 -2
  5. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +0 -1
  6. package/apps/desktop/src/main/core/browser/__tests__/WindowThemeManager.test.ts +8 -5
  7. package/changelog/v1.json +14 -0
  8. package/locales/en-US/plugin.json +3 -5
  9. package/locales/zh-CN/plugin.json +3 -5
  10. package/locales/zh-CN/tool.json +2 -0
  11. package/package.json +1 -1
  12. package/packages/builtin-agents/src/agents/group-supervisor/index.ts +12 -1
  13. package/packages/builtin-agents/src/agents/group-supervisor/systemRole.ts +0 -7
  14. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/EditLocalFile/index.tsx +93 -0
  15. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GlobLocalFiles/index.tsx +73 -0
  16. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GrepContent/index.tsx +69 -0
  17. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ListLocalFiles/index.tsx +68 -0
  18. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ReadLocalFile/index.tsx +74 -0
  19. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/SearchLocalFiles/index.tsx +70 -0
  20. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/WriteLocalFile/index.tsx +57 -0
  21. package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/index.ts +14 -0
  22. package/packages/builtin-tool-cloud-sandbox/src/client/Render/WriteFile/index.tsx +54 -35
  23. package/packages/builtin-tool-cloud-sandbox/src/client/components/FilePathDisplay.tsx +52 -0
  24. package/packages/builtin-tool-group-management/src/client/Inspector/ExecuteTasks/index.tsx +90 -0
  25. package/packages/builtin-tool-group-management/src/client/Inspector/index.ts +2 -0
  26. package/packages/builtin-tool-group-management/src/client/Intervention/ExecuteTasks.tsx +237 -0
  27. package/packages/builtin-tool-group-management/src/client/Intervention/index.ts +4 -1
  28. package/packages/builtin-tool-group-management/src/client/Render/index.ts +1 -1
  29. package/packages/builtin-tool-group-management/src/client/Streaming/ExecuteTask/index.tsx +69 -0
  30. package/packages/builtin-tool-group-management/src/client/Streaming/ExecuteTasks/index.tsx +87 -0
  31. package/packages/builtin-tool-group-management/src/client/Streaming/index.ts +4 -0
  32. package/packages/builtin-tool-group-management/src/executor.test.ts +8 -311
  33. package/packages/builtin-tool-group-management/src/executor.ts +5 -160
  34. package/packages/builtin-tool-group-management/src/manifest.ts +50 -94
  35. package/packages/builtin-tool-group-management/src/systemRole.ts +251 -172
  36. package/packages/builtin-tool-group-management/src/types.ts +29 -40
  37. package/packages/context-engine/src/engine/messages/MessagesEngine.ts +6 -4
  38. package/packages/context-engine/src/engine/messages/types.ts +4 -4
  39. package/packages/context-engine/src/processors/GroupRoleTransform.ts +261 -0
  40. package/packages/context-engine/src/processors/__tests__/GroupRoleTransform.test.ts +553 -0
  41. package/packages/context-engine/src/processors/index.ts +2 -2
  42. package/packages/context-engine/src/providers/__tests__/GroupContextInjector.test.ts +4 -16
  43. package/packages/context-engine/src/providers/__tests__/__snapshots__/GroupContextInjector.test.ts.snap +23 -28
  44. package/packages/desktop-bridge/src/index.ts +3 -0
  45. package/packages/prompts/src/prompts/agentGroup/__snapshots__/index.test.ts.snap +0 -7
  46. package/packages/prompts/src/prompts/agentGroup/groupContext.ts +0 -7
  47. package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/OpeningQuestions.tsx +4 -8
  48. package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/GroupChat.tsx +0 -3
  49. package/src/app/[variants]/(main)/group/features/Conversation/useGroupContext.ts +3 -0
  50. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +15 -2
  51. package/src/features/ChatInput/Desktop/index.tsx +1 -3
  52. package/src/features/Conversation/store/slices/message/action/crud.ts +2 -2
  53. package/src/features/ElectronTitlebar/Connection/ConnectionMode.tsx +2 -2
  54. package/src/features/ElectronTitlebar/SimpleTitleBar.tsx +1 -2
  55. package/src/features/ElectronTitlebar/index.tsx +2 -2
  56. package/src/hooks/useUserAvatar.test.ts +23 -4
  57. package/src/locales/default/plugin.ts +3 -5
  58. package/src/locales/default/tool.ts +3 -0
  59. package/src/services/chat/mecha/agentConfigResolver.test.ts +160 -0
  60. package/src/services/chat/mecha/agentConfigResolver.ts +15 -3
  61. package/src/services/chat/mecha/contextEngineering.ts +2 -1
  62. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +4 -2
  63. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +2 -0
  64. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -18
  65. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +24 -0
  66. package/src/store/chat/slices/message/selectors/displayMessage.ts +6 -1
  67. package/src/store/chat/slices/topic/action.test.ts +10 -4
  68. package/src/store/chat/slices/topic/action.ts +3 -2
  69. package/src/store/electron/selectors/sync.ts +17 -1
  70. package/packages/context-engine/src/processors/GroupMessageSender.ts +0 -138
  71. package/packages/context-engine/src/processors/__tests__/GroupMessageSender.test.ts +0 -274
  72. package/src/features/ElectronTitlebar/const.ts +0 -1
@@ -190,12 +190,13 @@ export const createGroupOrchestrationExecutors = (
190
190
 
191
191
  // If instruction is provided, inject it as a virtual User Message
192
192
  // This virtual message is not persisted to database, only used for model context
193
+ // Mark with <speaker> tag so the agent knows this instruction is from the Supervisor
193
194
  const now = Date.now();
194
195
  const messagesWithInstruction: UIChatMessage[] = agentInstruction
195
196
  ? [
196
197
  ...messages,
197
198
  {
198
- content: agentInstruction,
199
+ content: `<speaker name="Supervisor" />\n${agentInstruction}`,
199
200
  createdAt: now,
200
201
  id: `virtual_speak_instruction_${now}`,
201
202
  role: 'user',
@@ -266,12 +267,13 @@ export const createGroupOrchestrationExecutors = (
266
267
 
267
268
  // If instruction is provided, inject it as a virtual User Message
268
269
  // This virtual message is not persisted to database, only used for model context
270
+ // Mark with <speaker> tag so the agent knows this instruction is from the Supervisor
269
271
  const now = Date.now();
270
272
  const messagesWithInstruction: UIChatMessage[] = agentInstruction
271
273
  ? [
272
274
  ...messages,
273
275
  {
274
- content: agentInstruction,
276
+ content: `<speaker name="Supervisor" />\n${agentInstruction}`,
275
277
  createdAt: now,
276
278
  id: `virtual_broadcast_instruction_${now}`,
277
279
  role: 'user',
@@ -197,6 +197,8 @@ export const conversationLifecycle: StateCreator<
197
197
  // if there is topicId,then add topicId to message
198
198
  topicId: operationContext.topicId ?? undefined,
199
199
  threadId: operationContext.threadId ?? undefined,
200
+ // Pass isSupervisor metadata for group orchestration (consistent with server)
201
+ metadata: operationContext.isSupervisor ? { isSupervisor: true } : undefined,
200
202
  },
201
203
  { operationId, tempMessageId: tempAssistantId },
202
204
  );
@@ -592,7 +592,7 @@ export const streamingExecutor: StateCreator<
592
592
  // resolveAgentConfig handles:
593
593
  // - Builtin agent runtime config merging
594
594
  // - max_tokens/reasoning_effort based on chatConfig settings
595
- const { agentConfig: agentConfigData, chatConfig } = resolveAgentConfig({
595
+ const { agentConfig: agentConfigData } = resolveAgentConfig({
596
596
  agentId: effectiveAgentId || '',
597
597
  scope: context.scope, // Pass scope from context parameter (available at line 883)
598
598
  });
@@ -863,22 +863,5 @@ export const streamingExecutor: StateCreator<
863
863
  console.error('Desktop notification error:', error);
864
864
  }
865
865
  }
866
-
867
- // Summary history if context messages is larger than historyCount
868
- const historyCount = chatConfig.historyCount ?? 0;
869
-
870
- if (
871
- chatConfig.enableHistoryCount &&
872
- chatConfig.enableCompressHistory &&
873
- messages.length > historyCount
874
- ) {
875
- // after generation: [u1,a1,u2,a2,u3,a3]
876
- // but the `messages` is still: [u1,a1,u2,a2,u3]
877
- // So if historyCount=2, we need to summary [u1,a1,u2,a2]
878
- // because user find UI is [u1,a1,u2,a2 | u3,a3]
879
- const historyMessages = messages.slice(0, -historyCount + 1);
880
-
881
- await get().internal_summaryHistory(historyMessages);
882
- }
883
866
  },
884
867
  });
@@ -323,6 +323,30 @@ describe('displayMessageSelectors', () => {
323
323
  const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
324
324
  expect(result).toBe(messageMapKey({ agentId: '', topicId: undefined }));
325
325
  });
326
+
327
+ it('should generate correct key with activeGroupId for group conversations', () => {
328
+ const state: Partial<ChatStore> = {
329
+ activeAgentId: 'testId',
330
+ activeGroupId: 'groupId',
331
+ activeTopicId: undefined,
332
+ };
333
+ const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
334
+ expect(result).toBe(
335
+ messageMapKey({ agentId: 'testId', groupId: 'groupId', topicId: undefined }),
336
+ );
337
+ });
338
+
339
+ it('should generate correct key with activeGroupId and activeTopicId', () => {
340
+ const state: Partial<ChatStore> = {
341
+ activeAgentId: 'testId',
342
+ activeGroupId: 'groupId',
343
+ activeTopicId: 'topicId',
344
+ };
345
+ const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
346
+ expect(result).toBe(
347
+ messageMapKey({ agentId: 'testId', groupId: 'groupId', topicId: 'topicId' }),
348
+ );
349
+ });
326
350
  });
327
351
 
328
352
  describe('activeDisplayMessages with group chat messages', () => {
@@ -24,9 +24,14 @@ import { messageMapKey } from '../../../utils/messageMapKey';
24
24
 
25
25
  /**
26
26
  * Get the current chat key for accessing messagesMap
27
+ * For group conversations, uses groupId to generate the correct key
27
28
  */
28
29
  export const currentDisplayChatKey = (s: ChatStoreState) =>
29
- messageMapKey({ agentId: s.activeAgentId, topicId: s.activeTopicId });
30
+ messageMapKey({
31
+ agentId: s.activeAgentId,
32
+ groupId: s.activeGroupId,
33
+ topicId: s.activeTopicId,
34
+ });
30
35
 
31
36
  /**
32
37
  * Get display messages by key
@@ -462,7 +462,7 @@ describe('topic action', () => {
462
462
  });
463
463
  });
464
464
 
465
- it('should NOT clear new key data when switching with undefined (backward compatibility)', async () => {
465
+ it('should clear new key data when switching with undefined (same as null)', async () => {
466
466
  const { result } = renderHook(() => useChatStore());
467
467
  const activeAgentId = 'test-agent-id';
468
468
 
@@ -475,13 +475,19 @@ describe('topic action', () => {
475
475
 
476
476
  const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
477
477
 
478
- // Switch with undefined (should NOT clear because id !== null)
478
+ // Switch with undefined (should clear because id == null matches both null and undefined)
479
479
  await act(async () => {
480
480
  await result.current.switchTopic(undefined, { skipRefreshMessage: true });
481
481
  });
482
482
 
483
- // replaceMessages should NOT be called when switching with undefined
484
- expect(replaceMessagesSpy).not.toHaveBeenCalled();
483
+ // replaceMessages SHOULD be called when switching with undefined
484
+ expect(replaceMessagesSpy).toHaveBeenCalledWith([], {
485
+ context: expect.objectContaining({
486
+ agentId: activeAgentId,
487
+ topicId: null,
488
+ }),
489
+ action: expect.any(String),
490
+ });
485
491
  });
486
492
 
487
493
  it('should not clear new key data when switching to an existing topic', async () => {
@@ -532,10 +532,11 @@ export const chatTopic: StateCreator<
532
532
  const { activeAgentId, activeGroupId } = get();
533
533
 
534
534
  // Clear the _new key data in the following cases:
535
- // 1. When id is explicitly null (switching to empty topic state)
535
+ // 1. When id is null or undefined (switching to empty topic state)
536
536
  // 2. When clearNewKey option is explicitly true
537
537
  // This prevents stale data from previous conversations showing up
538
- const shouldClearNewKey = id === null || opts.clearNewKey;
538
+ // Note: Use == null to match both null and undefined
539
+ const shouldClearNewKey = !id || opts.clearNewKey;
539
540
 
540
541
  if (shouldClearNewKey && activeAgentId) {
541
542
  // Determine scope: use explicit scope from options, or infer from activeGroupId
@@ -1,12 +1,28 @@
1
+ import { OFFICIAL_URL } from '@lobechat/const';
2
+
1
3
  import { type ElectronState } from '../initialState';
2
4
 
3
5
  const isSyncActive = (s: ElectronState) => s.dataSyncConfig.active;
4
6
 
5
7
  const storageMode = (s: ElectronState) => s.dataSyncConfig.storageMode;
6
- const remoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
8
+
9
+ /**
10
+ * Returns the effective remote server URL based on storage mode:
11
+ * - Cloud mode: returns OFFICIAL_URL
12
+ * - SelfHost mode: returns the configured remoteServerUrl
13
+ */
14
+ const remoteServerUrl = (s: ElectronState) =>
15
+ s.dataSyncConfig.storageMode === 'cloud' ? OFFICIAL_URL : s.dataSyncConfig.remoteServerUrl || '';
16
+
17
+ /**
18
+ * Returns the raw remoteServerUrl from config without transformation.
19
+ * Use this when you need the original configured value (e.g., for editing forms).
20
+ */
21
+ const rawRemoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
7
22
 
8
23
  export const electronSyncSelectors = {
9
24
  isSyncActive,
25
+ rawRemoteServerUrl,
10
26
  remoteServerUrl,
11
27
  storageMode,
12
28
  };
@@ -1,138 +0,0 @@
1
- import debug from 'debug';
2
-
3
- import { BaseProcessor } from '../base/BaseProcessor';
4
- import type { Message, PipelineContext, ProcessorOptions } from '../types';
5
-
6
- const log = debug('context-engine:processor:GroupMessageSenderProcessor');
7
-
8
- /**
9
- * Agent info for message sender identification
10
- */
11
- export interface AgentInfo {
12
- name: string;
13
- role: 'supervisor' | 'participant';
14
- }
15
-
16
- /**
17
- * Configuration for GroupMessageSenderProcessor
18
- */
19
- export interface GroupMessageSenderConfig {
20
- /**
21
- * Mapping from agentId to agent info
22
- * Used to look up agent name and role for each message
23
- */
24
- agentMap: Record<string, AgentInfo>;
25
- }
26
-
27
- /**
28
- * Group Message Sender Processor
29
- *
30
- * Responsible for injecting sender identity information into assistant messages
31
- * in group chat scenarios. This helps the model understand which agent sent
32
- * each message in a multi-agent conversation.
33
- *
34
- * The processor appends a system context block at the end of each assistant
35
- * message that has an agentId, containing:
36
- * - Agent name
37
- * - Agent role (supervisor or agent)
38
- * - Agent ID (with instruction not to expose it in responses)
39
- *
40
- * @example
41
- * ```typescript
42
- * const processor = new GroupMessageSenderProcessor({
43
- * agentMap: {
44
- * 'agt_xxx': { name: 'Weather Expert', role: 'agent' },
45
- * 'agt_yyy': { name: 'Supervisor', role: 'supervisor' },
46
- * }
47
- * });
48
- * ```
49
- */
50
- export class GroupMessageSenderProcessor extends BaseProcessor {
51
- readonly name = 'GroupMessageSenderProcessor';
52
-
53
- constructor(
54
- private config: GroupMessageSenderConfig,
55
- options: ProcessorOptions = {},
56
- ) {
57
- super(options);
58
- }
59
-
60
- protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
61
- const clonedContext = this.cloneContext(context);
62
-
63
- // Skip if no agentMap provided
64
- if (!this.config.agentMap || Object.keys(this.config.agentMap).length === 0) {
65
- log('No agentMap provided, skipping processing');
66
- return this.markAsExecuted(clonedContext);
67
- }
68
-
69
- let processedCount = 0;
70
-
71
- clonedContext.messages = clonedContext.messages.map((msg: Message) => {
72
- // Only process assistant messages with agentId
73
- if (msg.role === 'assistant' && msg.agentId) {
74
- const agentInfo = this.config.agentMap[msg.agentId];
75
-
76
- if (agentInfo) {
77
- // Build the sender tag
78
- const senderTag = this.buildSenderContext(msg.agentId, agentInfo);
79
-
80
- // Prepend to message content (at the beginning)
81
- if (typeof msg.content === 'string') {
82
- processedCount++;
83
- log(
84
- `Injecting sender info for message from agent: ${agentInfo.name} (${agentInfo.role})`,
85
- );
86
-
87
- return {
88
- ...msg,
89
- content: senderTag + msg.content,
90
- };
91
- }
92
- // Handle array content (multimodal messages)
93
- else if (Array.isArray(msg.content)) {
94
- const firstTextIndex = msg.content.findIndex((part: any) => part.type === 'text');
95
-
96
- if (firstTextIndex !== -1) {
97
- processedCount++;
98
- log(
99
- `Injecting sender info for multimodal message from agent: ${agentInfo.name} (${agentInfo.role})`,
100
- );
101
-
102
- const newContent = [...msg.content];
103
- newContent[firstTextIndex] = {
104
- ...newContent[firstTextIndex],
105
- text: senderTag + newContent[firstTextIndex].text,
106
- };
107
-
108
- return {
109
- ...msg,
110
- content: newContent,
111
- };
112
- }
113
- }
114
- }
115
- }
116
-
117
- return msg;
118
- });
119
-
120
- // Update metadata
121
- clonedContext.metadata.groupMessageSenderProcessed = processedCount;
122
-
123
- log(`Group message sender processing completed: ${processedCount} messages processed`);
124
-
125
- return this.markAsExecuted(clonedContext);
126
- }
127
-
128
- /**
129
- * Build the sender tag to prepend to assistant messages.
130
- *
131
- * Uses a self-closing XML tag at the beginning:
132
- * - Placed at start so model sees "who is speaking" before the content
133
- * - Self-closing tag is less likely to be reproduced as it's not a "wrapper"
134
- */
135
- private buildSenderContext(_agentId: string, agentInfo: AgentInfo): string {
136
- return `<speaker name="${agentInfo.name}" />\n`;
137
- }
138
- }
@@ -1,274 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { PipelineContext } from '../../types';
4
- import { GroupMessageSenderProcessor } from '../GroupMessageSender';
5
-
6
- describe('GroupMessageSenderProcessor', () => {
7
- const createContext = (messages: any[]): PipelineContext => ({
8
- initialState: { messages: [] },
9
- isAborted: false,
10
- messages,
11
- metadata: {},
12
- });
13
-
14
- describe('Basic Scenarios', () => {
15
- it('should inject sender info into assistant message with agentId', async () => {
16
- const processor = new GroupMessageSenderProcessor({
17
- agentMap: {
18
- agt_weather: { name: 'Weather Expert', role: 'participant' },
19
- },
20
- });
21
-
22
- const input: any[] = [
23
- { role: 'user', content: 'What is the weather?' },
24
- { role: 'assistant', content: 'The weather is sunny.', agentId: 'agt_weather' },
25
- ];
26
-
27
- const context = createContext(input);
28
- const result = await processor.process(context);
29
-
30
- expect(result.messages).toHaveLength(2);
31
-
32
- // User message should be unchanged
33
- expect(result.messages[0].content).toBe('What is the weather?');
34
-
35
- // Assistant message should have speaker tag prepended
36
- const assistantContent = result.messages[1].content;
37
- expect(assistantContent).toMatch(/^<speaker name="Weather Expert" \/>/);
38
- expect(assistantContent).toContain('The weather is sunny.');
39
-
40
- // Check metadata
41
- expect(result.metadata.groupMessageSenderProcessed).toBe(1);
42
- });
43
-
44
- it('should inject sender info for supervisor role', async () => {
45
- const processor = new GroupMessageSenderProcessor({
46
- agentMap: {
47
- agt_supervisor: { name: 'Group Supervisor', role: 'supervisor' },
48
- },
49
- });
50
-
51
- const input: any[] = [
52
- { role: 'assistant', content: 'I will coordinate the agents.', agentId: 'agt_supervisor' },
53
- ];
54
-
55
- const context = createContext(input);
56
- const result = await processor.process(context);
57
-
58
- const assistantContent = result.messages[0].content;
59
- expect(assistantContent).toMatch(/^<speaker name="Group Supervisor" \/>/);
60
- });
61
-
62
- it('should not modify assistant message without agentId', async () => {
63
- const processor = new GroupMessageSenderProcessor({
64
- agentMap: {
65
- agt_weather: { name: 'Weather Expert', role: 'participant' },
66
- },
67
- });
68
-
69
- const input: any[] = [{ role: 'assistant', content: 'Hello from assistant.' }];
70
-
71
- const context = createContext(input);
72
- const result = await processor.process(context);
73
-
74
- // Should be unchanged
75
- expect(result.messages[0].content).toBe('Hello from assistant.');
76
- expect(result.metadata.groupMessageSenderProcessed).toBe(0);
77
- });
78
-
79
- it('should not modify assistant message with unknown agentId', async () => {
80
- const processor = new GroupMessageSenderProcessor({
81
- agentMap: {
82
- agt_weather: { name: 'Weather Expert', role: 'participant' },
83
- },
84
- });
85
-
86
- const input: any[] = [{ role: 'assistant', content: 'Hello.', agentId: 'agt_unknown' }];
87
-
88
- const context = createContext(input);
89
- const result = await processor.process(context);
90
-
91
- // Should be unchanged because agentId not in map
92
- expect(result.messages[0].content).toBe('Hello.');
93
- expect(result.metadata.groupMessageSenderProcessed).toBe(0);
94
- });
95
-
96
- it('should not modify user or tool messages', async () => {
97
- const processor = new GroupMessageSenderProcessor({
98
- agentMap: {
99
- agt_weather: { name: 'Weather Expert', role: 'participant' },
100
- },
101
- });
102
-
103
- const input: any[] = [
104
- { role: 'user', content: 'Hello', agentId: 'agt_weather' },
105
- { role: 'tool', content: 'Tool result', agentId: 'agt_weather' },
106
- ];
107
-
108
- const context = createContext(input);
109
- const result = await processor.process(context);
110
-
111
- expect(result.messages[0].content).toBe('Hello');
112
- expect(result.messages[1].content).toBe('Tool result');
113
- expect(result.metadata.groupMessageSenderProcessed).toBe(0);
114
- });
115
- });
116
-
117
- describe('Multiple Messages', () => {
118
- it('should process multiple assistant messages from different agents', async () => {
119
- const processor = new GroupMessageSenderProcessor({
120
- agentMap: {
121
- agt_weather: { name: 'Weather Expert', role: 'participant' },
122
- agt_news: { name: 'News Reporter', role: 'participant' },
123
- agt_supervisor: { name: 'Supervisor', role: 'supervisor' },
124
- },
125
- });
126
-
127
- const input: any[] = [
128
- { role: 'user', content: 'Give me updates' },
129
- { role: 'assistant', content: 'Weather is sunny.', agentId: 'agt_weather' },
130
- { role: 'assistant', content: 'Top news today.', agentId: 'agt_news' },
131
- { role: 'assistant', content: 'Summary complete.', agentId: 'agt_supervisor' },
132
- ];
133
-
134
- const context = createContext(input);
135
- const result = await processor.process(context);
136
-
137
- expect(result.messages).toHaveLength(4);
138
-
139
- // Check each assistant message has correct speaker tag prepended
140
- expect(result.messages[1].content).toMatch(/^<speaker name="Weather Expert" \/>/);
141
- expect(result.messages[2].content).toMatch(/^<speaker name="News Reporter" \/>/);
142
- expect(result.messages[3].content).toMatch(/^<speaker name="Supervisor" \/>/);
143
-
144
- expect(result.metadata.groupMessageSenderProcessed).toBe(3);
145
- });
146
- });
147
-
148
- describe('Multimodal Messages', () => {
149
- it('should handle array content (multimodal messages)', async () => {
150
- const processor = new GroupMessageSenderProcessor({
151
- agentMap: {
152
- agt_weather: { name: 'Weather Expert', role: 'participant' },
153
- },
154
- });
155
-
156
- const input: any[] = [
157
- {
158
- role: 'assistant',
159
- content: [
160
- { type: 'text', text: 'Here is the weather chart.' },
161
- { type: 'image_url', image_url: { url: 'https://example.com/chart.png' } },
162
- ],
163
- agentId: 'agt_weather',
164
- },
165
- ];
166
-
167
- const context = createContext(input);
168
- const result = await processor.process(context);
169
-
170
- const content = result.messages[0].content as any[];
171
- expect(Array.isArray(content)).toBe(true);
172
-
173
- // The first text part should have speaker tag prepended
174
- const textPart = content.find((p: any) => p.type === 'text');
175
- expect(textPart.text).toMatch(/^<speaker name="Weather Expert" \/>/);
176
- expect(textPart.text).toContain('Here is the weather chart.');
177
-
178
- // Image part should be unchanged
179
- const imagePart = content.find((p: any) => p.type === 'image_url');
180
- expect(imagePart.image_url.url).toBe('https://example.com/chart.png');
181
-
182
- expect(result.metadata.groupMessageSenderProcessed).toBe(1);
183
- });
184
-
185
- it('should append to the last text part in multimodal content', async () => {
186
- const processor = new GroupMessageSenderProcessor({
187
- agentMap: {
188
- agt_weather: { name: 'Weather Expert', role: 'participant' },
189
- },
190
- });
191
-
192
- const input: any[] = [
193
- {
194
- role: 'assistant',
195
- content: [
196
- { type: 'text', text: 'First text.' },
197
- { type: 'image_url', image_url: { url: 'https://example.com/img.png' } },
198
- { type: 'text', text: 'Last text.' },
199
- ],
200
- agentId: 'agt_weather',
201
- },
202
- ];
203
-
204
- const context = createContext(input);
205
- const result = await processor.process(context);
206
-
207
- const content = result.messages[0].content;
208
-
209
- // First text part should have speaker tag prepended
210
- expect(content[0].text).toMatch(/^<speaker name="Weather Expert" \/>/);
211
- expect(content[0].text).toContain('First text.');
212
-
213
- // Last text should be unchanged
214
- expect(content[2].text).toBe('Last text.');
215
- });
216
- });
217
-
218
- describe('Edge Cases', () => {
219
- it('should skip processing with empty agentMap', async () => {
220
- const processor = new GroupMessageSenderProcessor({
221
- agentMap: {},
222
- });
223
-
224
- const input: any[] = [{ role: 'assistant', content: 'Hello', agentId: 'agt_weather' }];
225
-
226
- const context = createContext(input);
227
- const result = await processor.process(context);
228
-
229
- expect(result.messages[0].content).toBe('Hello');
230
- });
231
-
232
- it('should handle empty messages array', async () => {
233
- const processor = new GroupMessageSenderProcessor({
234
- agentMap: {
235
- agt_weather: { name: 'Weather Expert', role: 'participant' },
236
- },
237
- });
238
-
239
- const context = createContext([]);
240
- const result = await processor.process(context);
241
-
242
- expect(result.messages).toHaveLength(0);
243
- expect(result.metadata.groupMessageSenderProcessed).toBe(0);
244
- });
245
-
246
- it('should preserve original message properties', async () => {
247
- const processor = new GroupMessageSenderProcessor({
248
- agentMap: {
249
- agt_weather: { name: 'Weather Expert', role: 'participant' },
250
- },
251
- });
252
-
253
- const input: any[] = [
254
- {
255
- role: 'assistant',
256
- content: 'Weather info.',
257
- agentId: 'agt_weather',
258
- id: 'msg-123',
259
- createdAt: '2025-01-01T00:00:00Z',
260
- customField: 'custom value',
261
- },
262
- ];
263
-
264
- const context = createContext(input);
265
- const result = await processor.process(context);
266
-
267
- const msg = result.messages[0];
268
- expect(msg.id).toBe('msg-123');
269
- expect(msg.createdAt).toBe('2025-01-01T00:00:00Z');
270
- expect(msg.customField).toBe('custom value');
271
- expect(msg.agentId).toBe('agt_weather');
272
- });
273
- });
274
- });
@@ -1 +0,0 @@
1
- export const TITLE_BAR_HEIGHT = 30;