@lobehub/lobehub 2.0.0-next.31 → 2.0.0-next.33

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 (83) hide show
  1. package/.github/workflows/test.yml +1 -0
  2. package/CHANGELOG.md +58 -0
  3. package/apps/desktop/package.json +1 -1
  4. package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +23 -2
  5. package/changelog/v1.json +21 -0
  6. package/docker-compose/local/.env.example +3 -0
  7. package/docs/self-hosting/server-database/docker-compose.mdx +29 -0
  8. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +29 -0
  9. package/package.json +1 -1
  10. package/packages/const/src/hotkeys.ts +3 -3
  11. package/packages/const/src/models.ts +2 -2
  12. package/packages/const/src/utils/merge.ts +3 -3
  13. package/packages/conversation-flow/package.json +13 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/index.ts +48 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +56 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-with-tools.json +144 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +131 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +96 -0
  19. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +123 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +128 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +14 -0
  22. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +179 -0
  23. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/index.ts +8 -0
  24. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +85 -0
  25. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +169 -0
  26. package/packages/conversation-flow/src/__tests__/fixtures/inputs/complex-scenario.json +107 -0
  27. package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +14 -0
  28. package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +59 -0
  29. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +135 -0
  30. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-with-tools.json +340 -0
  31. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +242 -0
  32. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +208 -0
  33. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +254 -0
  34. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +260 -0
  35. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +14 -0
  36. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +389 -0
  37. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/index.ts +8 -0
  38. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +224 -0
  39. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +418 -0
  40. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +239 -0
  41. package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +138 -0
  42. package/packages/conversation-flow/src/__tests__/parse.test.ts +97 -0
  43. package/packages/conversation-flow/src/index.ts +17 -0
  44. package/packages/conversation-flow/src/indexing.ts +58 -0
  45. package/packages/conversation-flow/src/parse.ts +53 -0
  46. package/packages/conversation-flow/src/structuring.ts +38 -0
  47. package/packages/conversation-flow/src/transformation/BranchResolver.ts +66 -0
  48. package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +292 -0
  49. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +421 -0
  50. package/packages/conversation-flow/src/transformation/MessageCollector.ts +166 -0
  51. package/packages/conversation-flow/src/transformation/MessageTransformer.ts +177 -0
  52. package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +151 -0
  53. package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +385 -0
  54. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +511 -0
  55. package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +220 -0
  56. package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +287 -0
  57. package/packages/conversation-flow/src/transformation/index.ts +78 -0
  58. package/packages/conversation-flow/src/types/contextTree.ts +65 -0
  59. package/packages/conversation-flow/src/types/flatMessageList.ts +66 -0
  60. package/packages/conversation-flow/src/types/shared.ts +63 -0
  61. package/packages/conversation-flow/src/types.ts +36 -0
  62. package/packages/conversation-flow/vitest.config.mts +10 -0
  63. package/packages/types/src/message/common/metadata.ts +5 -1
  64. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +3 -4
  65. package/src/envs/__tests__/app.test.ts +47 -13
  66. package/src/envs/app.ts +6 -0
  67. package/src/server/modules/S3/index.test.ts +379 -0
  68. package/src/server/routers/async/__tests__/caller.test.ts +333 -0
  69. package/src/server/routers/async/caller.ts +2 -1
  70. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +57 -57
  71. package/src/server/routers/lambda/message.ts +2 -2
  72. package/src/server/services/message/__tests__/index.test.ts +4 -4
  73. package/src/server/services/message/index.ts +1 -1
  74. package/src/services/message/index.ts +2 -3
  75. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +8 -8
  76. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +8 -8
  77. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +1 -1
  78. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +1 -1
  79. package/src/store/chat/slices/message/action.test.ts +7 -7
  80. package/src/store/chat/slices/message/action.ts +2 -2
  81. package/src/store/chat/slices/plugin/action.test.ts +7 -7
  82. package/src/store/chat/slices/plugin/action.ts +1 -1
  83. package/packages/context-engine/ARCHITECTURE.md +0 -425
@@ -0,0 +1,220 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { IdNode, Message } from '../../types';
4
+ import { MessageCollector } from '../MessageCollector';
5
+
6
+ describe('MessageCollector', () => {
7
+ describe('collectGroupMembers', () => {
8
+ it('should collect messages with matching groupId', () => {
9
+ const messageMap = new Map<string, Message>();
10
+ const childrenMap = new Map<string | null, string[]>();
11
+ const collector = new MessageCollector(messageMap, childrenMap);
12
+
13
+ const messages: Message[] = [
14
+ {
15
+ content: '1',
16
+ createdAt: 0,
17
+ groupId: 'group-1',
18
+ id: 'msg-1',
19
+ meta: {},
20
+ role: 'assistant',
21
+ updatedAt: 0,
22
+ },
23
+ {
24
+ content: '2',
25
+ createdAt: 0,
26
+ groupId: 'group-1',
27
+ id: 'msg-2',
28
+ meta: {},
29
+ role: 'assistant',
30
+ updatedAt: 0,
31
+ },
32
+ {
33
+ content: '3',
34
+ createdAt: 0,
35
+ groupId: 'group-2',
36
+ id: 'msg-3',
37
+ meta: {},
38
+ role: 'assistant',
39
+ updatedAt: 0,
40
+ },
41
+ ];
42
+
43
+ const result = collector.collectGroupMembers('group-1', messages);
44
+
45
+ expect(result).toHaveLength(2);
46
+ expect(result.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
47
+ });
48
+ });
49
+
50
+ describe('collectToolMessages', () => {
51
+ it('should collect tool messages matching assistant tool call IDs', () => {
52
+ const messageMap = new Map<string, Message>();
53
+ const childrenMap = new Map<string | null, string[]>();
54
+ const collector = new MessageCollector(messageMap, childrenMap);
55
+
56
+ const assistant: Message = {
57
+ content: 'test',
58
+ createdAt: 0,
59
+ id: 'msg-1',
60
+ meta: {},
61
+ role: 'assistant',
62
+ tools: [
63
+ { apiName: 'tool1', arguments: '{}', id: 'tool-1', identifier: 'test', type: 'default' },
64
+ { apiName: 'tool2', arguments: '{}', id: 'tool-2', identifier: 'test', type: 'default' },
65
+ ],
66
+ updatedAt: 0,
67
+ };
68
+
69
+ const messages: Message[] = [
70
+ {
71
+ content: 'result1',
72
+ createdAt: 0,
73
+ id: 'msg-2',
74
+ meta: {},
75
+ parentId: 'msg-1',
76
+ role: 'tool',
77
+ tool_call_id: 'tool-1',
78
+ updatedAt: 0,
79
+ },
80
+ {
81
+ content: 'result2',
82
+ createdAt: 0,
83
+ id: 'msg-3',
84
+ meta: {},
85
+ parentId: 'msg-1',
86
+ role: 'tool',
87
+ tool_call_id: 'tool-2',
88
+ updatedAt: 0,
89
+ },
90
+ {
91
+ content: 'other',
92
+ createdAt: 0,
93
+ id: 'msg-4',
94
+ meta: {},
95
+ parentId: 'msg-1',
96
+ role: 'tool',
97
+ tool_call_id: 'tool-3',
98
+ updatedAt: 0,
99
+ },
100
+ ];
101
+
102
+ const result = collector.collectToolMessages(assistant, messages);
103
+
104
+ expect(result).toHaveLength(2);
105
+ expect(result.map((m) => m.id)).toEqual(['msg-2', 'msg-3']);
106
+ });
107
+ });
108
+
109
+ describe('findLastNodeInAssistantGroup', () => {
110
+ it('should return the node itself if no tool children', () => {
111
+ const messageMap = new Map<string, Message>();
112
+ const childrenMap = new Map<string | null, string[]>();
113
+ const collector = new MessageCollector(messageMap, childrenMap);
114
+
115
+ const idNode: IdNode = {
116
+ children: [],
117
+ id: 'msg-1',
118
+ };
119
+
120
+ const result = collector.findLastNodeInAssistantGroup(idNode);
121
+
122
+ expect(result).toEqual(idNode);
123
+ });
124
+
125
+ it('should return last tool node if no assistant children', () => {
126
+ const messageMap = new Map<string, Message>([
127
+ [
128
+ 'msg-1',
129
+ {
130
+ content: 'test',
131
+ createdAt: 0,
132
+ id: 'msg-1',
133
+ meta: {},
134
+ role: 'assistant',
135
+ updatedAt: 0,
136
+ },
137
+ ],
138
+ [
139
+ 'tool-1',
140
+ {
141
+ content: 'result',
142
+ createdAt: 0,
143
+ id: 'tool-1',
144
+ meta: {},
145
+ parentId: 'msg-1',
146
+ role: 'tool',
147
+ updatedAt: 0,
148
+ },
149
+ ],
150
+ ]);
151
+ const childrenMap = new Map<string | null, string[]>();
152
+ const collector = new MessageCollector(messageMap, childrenMap);
153
+
154
+ const idNode: IdNode = {
155
+ children: [{ children: [], id: 'tool-1' }],
156
+ id: 'msg-1',
157
+ };
158
+
159
+ const result = collector.findLastNodeInAssistantGroup(idNode);
160
+
161
+ expect(result?.id).toBe('tool-1');
162
+ });
163
+
164
+ it('should follow assistant chain recursively', () => {
165
+ const messageMap = new Map<string, Message>([
166
+ [
167
+ 'msg-1',
168
+ {
169
+ content: 'test1',
170
+ createdAt: 0,
171
+ id: 'msg-1',
172
+ meta: {},
173
+ role: 'assistant',
174
+ updatedAt: 0,
175
+ },
176
+ ],
177
+ [
178
+ 'tool-1',
179
+ {
180
+ content: 'result1',
181
+ createdAt: 0,
182
+ id: 'tool-1',
183
+ meta: {},
184
+ parentId: 'msg-1',
185
+ role: 'tool',
186
+ updatedAt: 0,
187
+ },
188
+ ],
189
+ [
190
+ 'msg-2',
191
+ {
192
+ content: 'test2',
193
+ createdAt: 0,
194
+ id: 'msg-2',
195
+ meta: {},
196
+ parentId: 'tool-1',
197
+ role: 'assistant',
198
+ updatedAt: 0,
199
+ },
200
+ ],
201
+ ]);
202
+ const childrenMap = new Map<string | null, string[]>();
203
+ const collector = new MessageCollector(messageMap, childrenMap);
204
+
205
+ const idNode: IdNode = {
206
+ children: [
207
+ {
208
+ children: [{ children: [], id: 'msg-2' }],
209
+ id: 'tool-1',
210
+ },
211
+ ],
212
+ id: 'msg-1',
213
+ };
214
+
215
+ const result = collector.findLastNodeInAssistantGroup(idNode);
216
+
217
+ expect(result?.id).toBe('msg-2');
218
+ });
219
+ });
220
+ });
@@ -0,0 +1,287 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { AssistantContentBlock } from '@/types/index';
4
+
5
+ import type { Message } from '../../types';
6
+ import { MessageTransformer } from '../MessageTransformer';
7
+
8
+ describe('MessageTransformer', () => {
9
+ const transformer = new MessageTransformer();
10
+
11
+ describe('messageToContentBlock', () => {
12
+ it('should convert message to content block', () => {
13
+ const message: Message = {
14
+ content: 'Hello',
15
+ createdAt: 0,
16
+ id: 'msg-1',
17
+ meta: {},
18
+ metadata: {
19
+ cost: 0.001,
20
+ duration: 1000,
21
+ totalInputTokens: 10,
22
+ totalOutputTokens: 20,
23
+ totalTokens: 30,
24
+ tps: 20,
25
+ },
26
+ role: 'assistant',
27
+ updatedAt: 0,
28
+ };
29
+
30
+ const result = transformer.messageToContentBlock(message);
31
+
32
+ expect(result).toEqual({
33
+ content: 'Hello',
34
+ error: undefined,
35
+ id: 'msg-1',
36
+ imageList: undefined,
37
+ performance: {
38
+ duration: 1000,
39
+ tps: 20,
40
+ },
41
+ reasoning: undefined,
42
+ tools: undefined,
43
+ usage: {
44
+ cost: 0.001,
45
+ totalInputTokens: 10,
46
+ totalOutputTokens: 20,
47
+ totalTokens: 30,
48
+ },
49
+ });
50
+ });
51
+
52
+ it('should handle message without metadata', () => {
53
+ const message: Message = {
54
+ content: 'Hello',
55
+ createdAt: 0,
56
+ id: 'msg-1',
57
+ meta: {},
58
+ role: 'assistant',
59
+ updatedAt: 0,
60
+ };
61
+
62
+ const result = transformer.messageToContentBlock(message);
63
+
64
+ expect(result.usage).toBeUndefined();
65
+ expect(result.performance).toBeUndefined();
66
+ });
67
+ });
68
+
69
+ describe('splitMetadata', () => {
70
+ it('should split metadata into usage and performance', () => {
71
+ const metadata = {
72
+ cost: 0.001,
73
+ duration: 1000,
74
+ latency: 1100,
75
+ totalInputTokens: 10,
76
+ totalOutputTokens: 20,
77
+ totalTokens: 30,
78
+ tps: 20,
79
+ ttft: 100,
80
+ };
81
+
82
+ const result = transformer.splitMetadata(metadata);
83
+
84
+ expect(result.usage).toEqual({
85
+ cost: 0.001,
86
+ totalInputTokens: 10,
87
+ totalOutputTokens: 20,
88
+ totalTokens: 30,
89
+ });
90
+
91
+ expect(result.performance).toEqual({
92
+ duration: 1000,
93
+ latency: 1100,
94
+ tps: 20,
95
+ ttft: 100,
96
+ });
97
+ });
98
+
99
+ it('should handle undefined metadata', () => {
100
+ const result = transformer.splitMetadata(undefined);
101
+
102
+ expect(result).toEqual({});
103
+ });
104
+
105
+ it('should handle empty metadata', () => {
106
+ const result = transformer.splitMetadata({});
107
+
108
+ expect(result).toEqual({
109
+ performance: undefined,
110
+ usage: undefined,
111
+ });
112
+ });
113
+
114
+ it('should handle partial metadata', () => {
115
+ const result = transformer.splitMetadata({
116
+ cost: 0.001,
117
+ duration: 1000,
118
+ });
119
+
120
+ expect(result.usage).toEqual({
121
+ cost: 0.001,
122
+ });
123
+
124
+ expect(result.performance).toEqual({
125
+ duration: 1000,
126
+ });
127
+ });
128
+ });
129
+
130
+ describe('aggregateMetadata', () => {
131
+ it('should aggregate usage and performance from multiple children', () => {
132
+ const children: AssistantContentBlock[] = [
133
+ {
134
+ content: 'First',
135
+ id: 'msg-1',
136
+ performance: {
137
+ duration: 1000,
138
+ latency: 1100,
139
+ tps: 20,
140
+ ttft: 100,
141
+ },
142
+ usage: {
143
+ cost: 0.001,
144
+ totalInputTokens: 10,
145
+ totalOutputTokens: 20,
146
+ totalTokens: 30,
147
+ },
148
+ },
149
+ {
150
+ content: 'Second',
151
+ id: 'msg-2',
152
+ performance: {
153
+ duration: 2000,
154
+ latency: 2100,
155
+ tps: 30,
156
+ },
157
+ usage: {
158
+ cost: 0.002,
159
+ totalInputTokens: 15,
160
+ totalOutputTokens: 25,
161
+ totalTokens: 40,
162
+ },
163
+ },
164
+ ];
165
+
166
+ const result = transformer.aggregateMetadata(children);
167
+
168
+ expect(result.usage).toEqual({
169
+ cost: 0.003,
170
+ totalInputTokens: 25,
171
+ totalOutputTokens: 45,
172
+ totalTokens: 70,
173
+ });
174
+
175
+ expect(result.performance).toEqual({
176
+ duration: 3000,
177
+ latency: 3200,
178
+ tps: 25, // average of 20 and 30
179
+ ttft: 100, // first value
180
+ });
181
+ });
182
+
183
+ it('should handle empty children array', () => {
184
+ const result = transformer.aggregateMetadata([]);
185
+
186
+ expect(result).toEqual({
187
+ performance: undefined,
188
+ usage: undefined,
189
+ });
190
+ });
191
+
192
+ it('should handle children without metadata', () => {
193
+ const children: AssistantContentBlock[] = [
194
+ {
195
+ content: 'First',
196
+ id: 'msg-1',
197
+ },
198
+ {
199
+ content: 'Second',
200
+ id: 'msg-2',
201
+ },
202
+ ];
203
+
204
+ const result = transformer.aggregateMetadata(children);
205
+
206
+ expect(result).toEqual({
207
+ performance: undefined,
208
+ usage: undefined,
209
+ });
210
+ });
211
+
212
+ it('should handle mixed children (some with metadata, some without)', () => {
213
+ const children: AssistantContentBlock[] = [
214
+ {
215
+ content: 'First',
216
+ id: 'msg-1',
217
+ usage: {
218
+ cost: 0.001,
219
+ totalTokens: 30,
220
+ },
221
+ },
222
+ {
223
+ content: 'Second',
224
+ id: 'msg-2',
225
+ },
226
+ {
227
+ content: 'Third',
228
+ id: 'msg-3',
229
+ usage: {
230
+ cost: 0.002,
231
+ totalTokens: 40,
232
+ },
233
+ },
234
+ ];
235
+
236
+ const result = transformer.aggregateMetadata(children);
237
+
238
+ expect(result.usage).toEqual({
239
+ cost: 0.003,
240
+ totalTokens: 70,
241
+ });
242
+ });
243
+
244
+ it('should average tps correctly', () => {
245
+ const children: AssistantContentBlock[] = [
246
+ {
247
+ content: 'First',
248
+ id: 'msg-1',
249
+ performance: { tps: 10 },
250
+ },
251
+ {
252
+ content: 'Second',
253
+ id: 'msg-2',
254
+ performance: { tps: 20 },
255
+ },
256
+ {
257
+ content: 'Third',
258
+ id: 'msg-3',
259
+ performance: { tps: 30 },
260
+ },
261
+ ];
262
+
263
+ const result = transformer.aggregateMetadata(children);
264
+
265
+ expect(result.performance?.tps).toBe(20); // average of 10, 20, 30
266
+ });
267
+
268
+ it('should take first ttft value only', () => {
269
+ const children: AssistantContentBlock[] = [
270
+ {
271
+ content: 'First',
272
+ id: 'msg-1',
273
+ performance: { ttft: 100 },
274
+ },
275
+ {
276
+ content: 'Second',
277
+ id: 'msg-2',
278
+ performance: { ttft: 200 },
279
+ },
280
+ ];
281
+
282
+ const result = transformer.aggregateMetadata(children);
283
+
284
+ expect(result.performance?.ttft).toBe(100); // first value only
285
+ });
286
+ });
287
+ });
@@ -0,0 +1,78 @@
1
+ import type { ContextNode, HelperMaps, IdNode, Message } from '../types';
2
+ import { BranchResolver } from './BranchResolver';
3
+ import { ContextTreeBuilder } from './ContextTreeBuilder';
4
+ import { FlatListBuilder } from './FlatListBuilder';
5
+ import { MessageCollector } from './MessageCollector';
6
+ import { MessageTransformer } from './MessageTransformer';
7
+
8
+ /**
9
+ * Phase 3: Transformation
10
+ * Converts structural idTree into semantic contextTree and flatList
11
+ *
12
+ * This is the main coordinator that delegates to specialized builders:
13
+ * - ContextTreeBuilder: Builds the tree structure for UI rendering
14
+ * - FlatListBuilder: Builds the flat list for API consumption
15
+ */
16
+ export class Transformer {
17
+ private messageMap: Map<string, Message>;
18
+ private nodeIdCounter = 0;
19
+
20
+ // Utility classes
21
+ private branchResolver: BranchResolver;
22
+ private messageCollector: MessageCollector;
23
+ private messageTransformer: MessageTransformer;
24
+
25
+ // Builder classes
26
+ private contextTreeBuilder: ContextTreeBuilder;
27
+ private flatListBuilder: FlatListBuilder;
28
+
29
+ constructor(private helperMaps: HelperMaps) {
30
+ this.messageMap = helperMaps.messageMap;
31
+
32
+ // Initialize utility classes
33
+ this.branchResolver = new BranchResolver();
34
+ this.messageCollector = new MessageCollector(this.messageMap, helperMaps.childrenMap);
35
+ this.messageTransformer = new MessageTransformer();
36
+
37
+ // Initialize builder classes
38
+ this.contextTreeBuilder = new ContextTreeBuilder(
39
+ helperMaps.messageMap,
40
+ helperMaps.messageGroupMap,
41
+ this.branchResolver,
42
+ this.messageCollector,
43
+ this.generateNodeId.bind(this),
44
+ );
45
+
46
+ this.flatListBuilder = new FlatListBuilder(
47
+ helperMaps.messageMap,
48
+ helperMaps.messageGroupMap,
49
+ helperMaps.childrenMap,
50
+ this.branchResolver,
51
+ this.messageCollector,
52
+ this.messageTransformer,
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Generates a unique node ID
58
+ */
59
+ private generateNodeId(prefix: string, messageId: string): string {
60
+ return `${prefix}-${messageId}-${this.nodeIdCounter++}`;
61
+ }
62
+
63
+ /**
64
+ * Transform all root nodes to contextTree
65
+ * Returns a linear array of context nodes for UI rendering
66
+ */
67
+ transformAll(idNodes: IdNode[]): ContextNode[] {
68
+ return this.contextTreeBuilder.transformAll(idNodes);
69
+ }
70
+
71
+ /**
72
+ * Generate flatList from messages array
73
+ * Only includes messages in the active path for API consumption
74
+ */
75
+ flatten(messages: Message[]): Message[] {
76
+ return this.flatListBuilder.flatten(messages);
77
+ }
78
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Context Tree Types
3
+ *
4
+ * Tree structure for understanding conversation flow and navigation.
5
+ * Used for complex operations like branch switching and context understanding.
6
+ */
7
+
8
+ /**
9
+ * Base interface for all display nodes
10
+ */
11
+ interface BaseNode {
12
+ /** Unique identifier for this node */
13
+ id: string;
14
+ /** Type discriminator */
15
+ type: 'message' | 'assistantGroup' | 'compare' | 'branch';
16
+ }
17
+
18
+ /**
19
+ * Basic message node - leaf node representing a single message
20
+ */
21
+ export interface MessageNode extends BaseNode {
22
+ /** Tool message IDs (for assistant messages with tool calls) */
23
+ tools?: string[];
24
+ type: 'message';
25
+ }
26
+
27
+ /**
28
+ * Assistant group node - aggregates an assistant message with its tool calls
29
+ */
30
+ export interface AssistantGroupNode extends BaseNode {
31
+ /** Child nodes (assistant and tool messages) */
32
+ children: ContextNode[];
33
+ type: 'assistantGroup';
34
+ }
35
+
36
+ /**
37
+ * Compare node - renders multiple parallel outputs side by side
38
+ */
39
+ export interface CompareNode extends BaseNode {
40
+ /** ID of the active column that enters LLM context */
41
+ activeColumnId?: string;
42
+ /** Each column represents a parallel output tree */
43
+ columns: ContextNode[][];
44
+ /** The message that triggered the comparison */
45
+ messageId: string;
46
+ type: 'compare';
47
+ }
48
+
49
+ /**
50
+ * Branch node - represents multiple alternate conversation paths
51
+ */
52
+ export interface BranchNode extends BaseNode {
53
+ /** Index of the currently active branch */
54
+ activeBranchIndex: number;
55
+ /** Each branch is a separate conversation tree */
56
+ branches: ContextNode[][];
57
+ /** The parent message that has multiple branches */
58
+ parentMessageId: string;
59
+ type: 'branch';
60
+ }
61
+
62
+ /**
63
+ * Union type of all display nodes
64
+ */
65
+ export type ContextNode = MessageNode | AssistantGroupNode | CompareNode | BranchNode;
@@ -0,0 +1,66 @@
1
+ import type { UIChatMessage } from '@lobechat/types';
2
+
3
+ /**
4
+ * Flat Message List Types
5
+ *
6
+ * Flattened array optimized for virtual list rendering.
7
+ * Contains virtual messages with extended role types for grouped display.
8
+ */
9
+
10
+ /**
11
+ * Extended message role types for flat list rendering
12
+ *
13
+ * Standard roles from UIChatMessage:
14
+ * - 'user': User message
15
+ * - 'assistant': Assistant message (standalone, without tools)
16
+ * - 'tool': Tool execution result
17
+ * - 'system': System message
18
+ * - 'supervisor': Supervisor message in multi-agent
19
+ *
20
+ * Virtual roles created by parse():
21
+ * - 'assistantGroup': Assistant message + tool calls aggregation
22
+ * - 'messageGroup': Generic message group (manual/summary)
23
+ * - 'compare': Compare mode for parallel model outputs
24
+ */
25
+ export type FlatMessageRole =
26
+ | 'user'
27
+ | 'assistant'
28
+ | 'tool'
29
+ | 'system'
30
+ | 'supervisor'
31
+ | 'assistantGroup'
32
+ | 'messageGroup'
33
+ | 'compare';
34
+
35
+ /**
36
+ * Message in flat list
37
+ *
38
+ * Can be either:
39
+ * 1. Original message from database
40
+ * 2. Virtual message created by parse() with extended role and children
41
+ */
42
+ export type FlatMessage = UIChatMessage;
43
+
44
+ /**
45
+ * Branch metadata attached to user messages
46
+ */
47
+ export interface BranchMetadata {
48
+ /** Active branch message ID */
49
+ activeId: string;
50
+ /** All branch message IDs */
51
+ branchIds: string[];
52
+ }
53
+
54
+ /**
55
+ * Virtual message extra fields for flat list
56
+ */
57
+ export interface FlatMessageExtra {
58
+ /** Branch information for user messages with multiple children */
59
+ branches?: BranchMetadata;
60
+ /** Optional description for groups */
61
+ description?: string;
62
+ /** Group mode for messageGroup and compare virtual messages */
63
+ groupMode?: 'compare' | 'manual' | 'summary';
64
+ /** Parent message ID that triggered this group */
65
+ parentMessageId?: string;
66
+ }