@lobehub/lobehub 2.0.0-next.361 → 2.0.0-next.362

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 (32) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/Dockerfile +2 -1
  3. package/changelog/v1.json +9 -0
  4. package/locales/en-US/chat.json +3 -1
  5. package/locales/zh-CN/chat.json +2 -0
  6. package/package.json +1 -1
  7. package/packages/context-engine/src/base/BaseEveryUserContentProvider.ts +204 -0
  8. package/packages/context-engine/src/base/BaseLastUserContentProvider.ts +1 -8
  9. package/packages/context-engine/src/base/__tests__/BaseEveryUserContentProvider.test.ts +354 -0
  10. package/packages/context-engine/src/base/constants.ts +20 -0
  11. package/packages/context-engine/src/engine/messages/MessagesEngine.ts +27 -23
  12. package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +364 -0
  13. package/packages/context-engine/src/providers/PageEditorContextInjector.ts +17 -13
  14. package/packages/context-engine/src/providers/PageSelectionsInjector.ts +65 -0
  15. package/packages/context-engine/src/providers/__tests__/PageSelectionsInjector.test.ts +333 -0
  16. package/packages/context-engine/src/providers/index.ts +3 -1
  17. package/packages/prompts/src/agents/index.ts +1 -0
  18. package/packages/prompts/src/agents/pageSelectionContext.ts +28 -0
  19. package/packages/types/src/aiChat.ts +4 -0
  20. package/packages/types/src/message/common/index.ts +1 -0
  21. package/packages/types/src/message/common/metadata.ts +8 -0
  22. package/packages/types/src/message/common/pageSelection.ts +36 -0
  23. package/packages/types/src/message/ui/params.ts +16 -0
  24. package/scripts/prebuild.mts +1 -0
  25. package/src/features/ChatInput/Desktop/ContextContainer/ContextList.tsx +1 -1
  26. package/src/features/Conversation/ChatInput/index.tsx +9 -1
  27. package/src/features/Conversation/Messages/User/components/MessageContent.tsx +7 -1
  28. package/src/features/Conversation/Messages/User/components/PageSelections.tsx +62 -0
  29. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +5 -1
  30. package/src/locales/default/chat.ts +3 -2
  31. package/src/server/routers/lambda/aiChat.ts +7 -0
  32. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +5 -19
@@ -28,6 +28,7 @@ import {
28
28
  HistorySummaryProvider,
29
29
  KnowledgeInjector,
30
30
  PageEditorContextInjector,
31
+ PageSelectionsInjector,
31
32
  SystemRoleInjector,
32
33
  ToolSystemRoleProvider,
33
34
  UserMemoryInjector,
@@ -143,19 +144,19 @@ export class MessagesEngine {
143
144
 
144
145
  return [
145
146
  // =============================================
146
- // Phase 2: System Role Injection
147
+ // Phase 1: System Role Injection
147
148
  // =============================================
148
149
 
149
- // 2. System role injection (agent's system role)
150
+ // 1. System role injection (agent's system role)
150
151
  new SystemRoleInjector({ systemRole }),
151
152
 
152
153
  // =============================================
153
- // Phase 2.5: First User Message Context Injection
154
+ // Phase 2: First User Message Context Injection
154
155
  // These providers inject content before the first user message
155
156
  // Order matters: first executed = first in content
156
157
  // =============================================
157
158
 
158
- // 4. User memory injection (conditionally added, injected first)
159
+ // 2. User memory injection (conditionally added, injected first)
159
160
  ...(isUserMemoryEnabled ? [new UserMemoryInjector(userMemory)] : []),
160
161
 
161
162
  // 3. Group context injection (agent identity and group info for multi-agent chat)
@@ -169,7 +170,7 @@ export class MessagesEngine {
169
170
  systemPrompt: agentGroup?.systemPrompt,
170
171
  }),
171
172
 
172
- // 4.5. GTD Plan injection (conditionally added, after user memory, before knowledge)
173
+ // 4. GTD Plan injection (conditionally added, after user memory, before knowledge)
173
174
  ...(isGTDPlanEnabled ? [new GTDPlanInjector({ enabled: true, plan: gtd.plan })] : []),
174
175
 
175
176
  // 5. Knowledge injection (full content for agent files + metadata for knowledge bases)
@@ -179,7 +180,7 @@ export class MessagesEngine {
179
180
  }),
180
181
 
181
182
  // =============================================
182
- // Phase 2.6: Additional System Context
183
+ // Phase 3: Additional System Context
183
184
  // =============================================
184
185
 
185
186
  // 6. Agent Builder context injection (current agent config/meta for editing)
@@ -212,7 +213,10 @@ export class MessagesEngine {
212
213
  historySummary,
213
214
  }),
214
215
 
215
- // 10. Page Editor context injection
216
+ // 12. Page Selections injection (inject user-selected text into each user message that has them)
217
+ new PageSelectionsInjector({ enabled: isPageEditorEnabled }),
218
+
219
+ // 10. Page Editor context injection (inject current page content to last user message)
216
220
  new PageEditorContextInjector({
217
221
  enabled: isPageEditorEnabled,
218
222
  // Use direct pageContentContext if provided (server-side), otherwise build from initialContext + stepContext (frontend)
@@ -232,37 +236,37 @@ export class MessagesEngine {
232
236
  : undefined,
233
237
  }),
234
238
 
235
- // 10.5. GTD Todo injection (conditionally added, at end of last user message)
239
+ // 11. GTD Todo injection (conditionally added, at end of last user message)
236
240
  ...(isGTDTodoEnabled ? [new GTDTodoInjector({ enabled: true, todos: gtd.todos })] : []),
237
241
 
238
242
  // =============================================
239
- // Phase 3: Message Transformation
243
+ // Phase 4: Message Transformation
240
244
  // =============================================
241
245
 
242
- // 11. Input template processing
246
+ // 13. Input template processing
243
247
  new InputTemplateProcessor({ inputTemplate }),
244
248
 
245
- // 11. Placeholder variables processing
249
+ // 14. Placeholder variables processing
246
250
  new PlaceholderVariablesProcessor({
247
251
  variableGenerators: variableGenerators || {},
248
252
  }),
249
253
 
250
- // 12. AgentCouncil message flatten (convert role=agentCouncil to standard assistant + tool messages)
254
+ // 15. AgentCouncil message flatten (convert role=agentCouncil to standard assistant + tool messages)
251
255
  new AgentCouncilFlattenProcessor(),
252
256
 
253
- // 13. Group message flatten (convert role=assistantGroup to standard assistant + tool messages)
257
+ // 16. Group message flatten (convert role=assistantGroup to standard assistant + tool messages)
254
258
  new GroupMessageFlattenProcessor(),
255
259
 
256
- // 14. Tasks message flatten (convert role=tasks to individual task messages)
260
+ // 17. Tasks message flatten (convert role=tasks to individual task messages)
257
261
  new TasksFlattenProcessor(),
258
262
 
259
- // 15. Task message processing (convert role=task to assistant with instruction + content)
263
+ // 18. Task message processing (convert role=task to assistant with instruction + content)
260
264
  new TaskMessageProcessor(),
261
265
 
262
- // 15. Supervisor role restore (convert role=supervisor back to role=assistant for model)
266
+ // 19. Supervisor role restore (convert role=supervisor back to role=assistant for model)
263
267
  new SupervisorRoleRestoreProcessor(),
264
268
 
265
- // 15.5. Group orchestration filter (remove supervisor's orchestration messages like broadcast/speak)
269
+ // 20. Group orchestration filter (remove supervisor's orchestration messages like broadcast/speak)
266
270
  // This must be BEFORE GroupRoleTransformProcessor so we filter based on original agentId/tools
267
271
  ...(isAgentGroupEnabled && agentGroup.agentMap && agentGroup.currentAgentId
268
272
  ? [
@@ -277,7 +281,7 @@ export class MessagesEngine {
277
281
  ]
278
282
  : []),
279
283
 
280
- // 16. Group role transform (convert other agents' messages to user role with speaker tags)
284
+ // 21. Group role transform (convert other agents' messages to user role with speaker tags)
281
285
  // This must be BEFORE ToolCallProcessor so other agents' tool messages are converted first
282
286
  ...(isAgentGroupEnabled && agentGroup.currentAgentId
283
287
  ? [
@@ -289,10 +293,10 @@ export class MessagesEngine {
289
293
  : []),
290
294
 
291
295
  // =============================================
292
- // Phase 4: Content Processing
296
+ // Phase 5: Content Processing
293
297
  // =============================================
294
298
 
295
- // 17. Message content processing (image encoding, etc.)
299
+ // 22. Message content processing (image encoding, etc.)
296
300
  new MessageContentProcessor({
297
301
  fileContext: fileContext || { enabled: true, includeFileUrl: true },
298
302
  isCanUseVideo: capabilities?.isCanUseVideo || (() => false),
@@ -301,7 +305,7 @@ export class MessagesEngine {
301
305
  provider,
302
306
  }),
303
307
 
304
- // 18. Tool call processing
308
+ // 23. Tool call processing
305
309
  new ToolCallProcessor({
306
310
  genToolCallingName: this.toolNameResolver.generate.bind(this.toolNameResolver),
307
311
  isCanUseFC: capabilities?.isCanUseFC || (() => true),
@@ -309,10 +313,10 @@ export class MessagesEngine {
309
313
  provider,
310
314
  }),
311
315
 
312
- // 19. Tool message reordering
316
+ // 24. Tool message reordering
313
317
  new ToolMessageReorder(),
314
318
 
315
- // 20. Message cleanup (final step, keep only necessary fields)
319
+ // 25. Message cleanup (final step, keep only necessary fields)
316
320
  new MessageCleanupProcessor(),
317
321
  ];
318
322
  }
@@ -407,4 +407,368 @@ describe('MessagesEngine', () => {
407
407
  expect(userMessage?.content).toBe('Please respond to: user input');
408
408
  });
409
409
  });
410
+
411
+ describe('Page Editor context', () => {
412
+ it('should inject page content to the last user message when pageContentContext is provided', async () => {
413
+ const messages: UIChatMessage[] = [
414
+ {
415
+ content: 'First question',
416
+ createdAt: Date.now(),
417
+ id: 'msg-1',
418
+ role: 'user',
419
+ updatedAt: Date.now(),
420
+ } as UIChatMessage,
421
+ {
422
+ content: 'Answer',
423
+ createdAt: Date.now(),
424
+ id: 'msg-2',
425
+ role: 'assistant',
426
+ updatedAt: Date.now(),
427
+ } as UIChatMessage,
428
+ {
429
+ content: 'Second question about the page',
430
+ createdAt: Date.now(),
431
+ id: 'msg-3',
432
+ role: 'user',
433
+ updatedAt: Date.now(),
434
+ } as UIChatMessage,
435
+ ];
436
+
437
+ const params = createBasicParams({
438
+ messages,
439
+ pageContentContext: {
440
+ markdown: '# Document Title\n\nDocument content here.',
441
+ metadata: {
442
+ charCount: 40,
443
+ lineCount: 3,
444
+ title: 'Test Document',
445
+ },
446
+ },
447
+ });
448
+ const engine = new MessagesEngine(params);
449
+
450
+ const result = await engine.process();
451
+
452
+ expect(result.messages).toEqual([
453
+ { content: 'First question', role: 'user' },
454
+ { content: 'Answer', role: 'assistant' },
455
+ {
456
+ content: `Second question about the page
457
+
458
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
459
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
460
+
461
+ 1. Always prioritize handling user-visible content.
462
+ 2. the context is only required when user's queries rely on it.
463
+ </context.instruction>
464
+ <current_page_context>
465
+ <current_page title="Test Document">
466
+ <markdown chars="40" lines="3">
467
+ # Document Title
468
+
469
+ Document content here.
470
+ </markdown>
471
+ </current_page>
472
+ </current_page_context>
473
+ <!-- END SYSTEM CONTEXT -->`,
474
+ role: 'user',
475
+ },
476
+ ]);
477
+
478
+ expect(result.metadata.pageEditorContextInjected).toBe(true);
479
+ });
480
+
481
+ it('should not inject page content when not enabled', async () => {
482
+ const messages: UIChatMessage[] = [
483
+ {
484
+ content: 'Question',
485
+ createdAt: Date.now(),
486
+ id: 'msg-1',
487
+ role: 'user',
488
+ updatedAt: Date.now(),
489
+ } as UIChatMessage,
490
+ ];
491
+
492
+ const params = createBasicParams({ messages });
493
+ const engine = new MessagesEngine(params);
494
+
495
+ const result = await engine.process();
496
+
497
+ expect(result.messages).toEqual([{ content: 'Question', role: 'user' }]);
498
+ expect(result.metadata.pageEditorContextInjected).toBeUndefined();
499
+ });
500
+ });
501
+
502
+ describe('Page Selections', () => {
503
+ it('should inject page selections to each user message that has them', async () => {
504
+ const messages: UIChatMessage[] = [
505
+ {
506
+ content: 'First question with selection',
507
+ createdAt: Date.now(),
508
+ id: 'msg-1',
509
+ metadata: {
510
+ pageSelections: [
511
+ {
512
+ content: 'Selected paragraph 1',
513
+ id: 'sel-1',
514
+ pageId: 'page-1',
515
+ xml: '<p>Selected paragraph 1</p>',
516
+ },
517
+ ],
518
+ },
519
+ role: 'user',
520
+ updatedAt: Date.now(),
521
+ } as UIChatMessage,
522
+ {
523
+ content: 'Answer to first',
524
+ createdAt: Date.now(),
525
+ id: 'msg-2',
526
+ role: 'assistant',
527
+ updatedAt: Date.now(),
528
+ } as UIChatMessage,
529
+ {
530
+ content: 'Second question with different selection',
531
+ createdAt: Date.now(),
532
+ id: 'msg-3',
533
+ metadata: {
534
+ pageSelections: [
535
+ {
536
+ content: 'Selected paragraph 2',
537
+ id: 'sel-2',
538
+ pageId: 'page-1',
539
+ xml: '<p>Selected paragraph 2</p>',
540
+ },
541
+ ],
542
+ },
543
+ role: 'user',
544
+ updatedAt: Date.now(),
545
+ } as UIChatMessage,
546
+ ];
547
+
548
+ const params = createBasicParams({
549
+ messages,
550
+ pageContentContext: {
551
+ markdown: '# Doc',
552
+ metadata: { title: 'Doc' },
553
+ },
554
+ });
555
+ const engine = new MessagesEngine(params);
556
+
557
+ const result = await engine.process();
558
+
559
+ expect(result.messages).toEqual([
560
+ {
561
+ content: `First question with selection
562
+
563
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
564
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
565
+
566
+ 1. Always prioritize handling user-visible content.
567
+ 2. the context is only required when user's queries rely on it.
568
+ </context.instruction>
569
+ <user_page_selections>
570
+ <user_selections count="1">
571
+ <selection >
572
+ <p>Selected paragraph 1</p>
573
+ </selection>
574
+ </user_selections>
575
+ </user_page_selections>
576
+ <!-- END SYSTEM CONTEXT -->`,
577
+ role: 'user',
578
+ },
579
+ { content: 'Answer to first', role: 'assistant' },
580
+ {
581
+ content: `Second question with different selection
582
+
583
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
584
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
585
+
586
+ 1. Always prioritize handling user-visible content.
587
+ 2. the context is only required when user's queries rely on it.
588
+ </context.instruction>
589
+ <user_page_selections>
590
+ <user_selections count="1">
591
+ <selection >
592
+ <p>Selected paragraph 2</p>
593
+ </selection>
594
+ </user_selections>
595
+ </user_page_selections>
596
+ <current_page_context>
597
+ <current_page title="Doc">
598
+ <markdown chars="5" lines="1">
599
+ # Doc
600
+ </markdown>
601
+ </current_page>
602
+ </current_page_context>
603
+ <!-- END SYSTEM CONTEXT -->`,
604
+ role: 'user',
605
+ },
606
+ ]);
607
+ });
608
+
609
+ it('should skip user messages without pageSelections', async () => {
610
+ const messages: UIChatMessage[] = [
611
+ {
612
+ content: 'No selection here',
613
+ createdAt: Date.now(),
614
+ id: 'msg-1',
615
+ role: 'user',
616
+ updatedAt: Date.now(),
617
+ } as UIChatMessage,
618
+ {
619
+ content: 'Answer',
620
+ createdAt: Date.now(),
621
+ id: 'msg-2',
622
+ role: 'assistant',
623
+ updatedAt: Date.now(),
624
+ } as UIChatMessage,
625
+ {
626
+ content: 'With selection',
627
+ createdAt: Date.now(),
628
+ id: 'msg-3',
629
+ metadata: {
630
+ pageSelections: [
631
+ {
632
+ content: 'Selected text',
633
+ id: 'sel-1',
634
+ pageId: 'page-1',
635
+ xml: '<span>Selected text</span>',
636
+ },
637
+ ],
638
+ },
639
+ role: 'user',
640
+ updatedAt: Date.now(),
641
+ } as UIChatMessage,
642
+ ];
643
+
644
+ const params = createBasicParams({
645
+ messages,
646
+ pageContentContext: {
647
+ markdown: '# Doc',
648
+ metadata: { title: 'Doc' },
649
+ },
650
+ });
651
+ const engine = new MessagesEngine(params);
652
+
653
+ const result = await engine.process();
654
+
655
+ expect(result.messages).toEqual([
656
+ { content: 'No selection here', role: 'user' },
657
+ { content: 'Answer', role: 'assistant' },
658
+ {
659
+ content: `With selection
660
+
661
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
662
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
663
+
664
+ 1. Always prioritize handling user-visible content.
665
+ 2. the context is only required when user's queries rely on it.
666
+ </context.instruction>
667
+ <user_page_selections>
668
+ <user_selections count="1">
669
+ <selection >
670
+ <span>Selected text</span>
671
+ </selection>
672
+ </user_selections>
673
+ </user_page_selections>
674
+ <current_page_context>
675
+ <current_page title="Doc">
676
+ <markdown chars="5" lines="1">
677
+ # Doc
678
+ </markdown>
679
+ </current_page>
680
+ </current_page_context>
681
+ <!-- END SYSTEM CONTEXT -->`,
682
+ role: 'user',
683
+ },
684
+ ]);
685
+ });
686
+
687
+ it('should have only one SYSTEM CONTEXT wrapper when both selections and page content are injected', async () => {
688
+ const messages: UIChatMessage[] = [
689
+ {
690
+ content: 'Question about selection',
691
+ createdAt: Date.now(),
692
+ id: 'msg-1',
693
+ metadata: {
694
+ pageSelections: [
695
+ {
696
+ content: 'Selected text',
697
+ id: 'sel-1',
698
+ pageId: 'page-1',
699
+ xml: '<p>Selected text</p>',
700
+ },
701
+ ],
702
+ },
703
+ role: 'user',
704
+ updatedAt: Date.now(),
705
+ } as UIChatMessage,
706
+ ];
707
+
708
+ const params = createBasicParams({
709
+ messages,
710
+ pageContentContext: {
711
+ markdown: '# Full Document',
712
+ metadata: { title: 'Full Doc' },
713
+ },
714
+ });
715
+ const engine = new MessagesEngine(params);
716
+
717
+ const result = await engine.process();
718
+
719
+ expect(result.messages).toEqual([
720
+ {
721
+ content: `Question about selection
722
+
723
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
724
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
725
+
726
+ 1. Always prioritize handling user-visible content.
727
+ 2. the context is only required when user's queries rely on it.
728
+ </context.instruction>
729
+ <user_page_selections>
730
+ <user_selections count="1">
731
+ <selection >
732
+ <p>Selected text</p>
733
+ </selection>
734
+ </user_selections>
735
+ </user_page_selections>
736
+ <current_page_context>
737
+ <current_page title="Full Doc">
738
+ <markdown chars="15" lines="1">
739
+ # Full Document
740
+ </markdown>
741
+ </current_page>
742
+ </current_page_context>
743
+ <!-- END SYSTEM CONTEXT -->`,
744
+ role: 'user',
745
+ },
746
+ ]);
747
+ });
748
+
749
+ it('should not inject selections when page editor is not enabled', async () => {
750
+ const messages: UIChatMessage[] = [
751
+ {
752
+ content: 'Question',
753
+ createdAt: Date.now(),
754
+ id: 'msg-1',
755
+ metadata: {
756
+ pageSelections: [
757
+ { content: 'Selected', id: 'sel-1', pageId: 'page-1', xml: '<p>Selected</p>' },
758
+ ],
759
+ },
760
+ role: 'user',
761
+ updatedAt: Date.now(),
762
+ } as UIChatMessage,
763
+ ];
764
+
765
+ // No pageContentContext or initialContext.pageEditor means not enabled
766
+ const params = createBasicParams({ messages });
767
+ const engine = new MessagesEngine(params);
768
+
769
+ const result = await engine.process();
770
+
771
+ expect(result.messages).toEqual([{ content: 'Question', role: 'user' }]);
772
+ });
773
+ });
410
774
  });
@@ -20,6 +20,9 @@ export interface PageEditorContextInjectorConfig {
20
20
  * Page Editor Context Injector
21
21
  * Responsible for injecting current page context at the end of the last user message
22
22
  * This ensures the model receives the most up-to-date page/document state
23
+ *
24
+ * Note: Page selections (user-selected text regions) are handled separately by
25
+ * PageSelectionsInjector, which injects selections into each user message that has them
23
26
  */
24
27
  export class PageEditorContextInjector extends BaseLastUserContentProvider {
25
28
  readonly name = 'PageEditorContextInjector';
@@ -37,20 +40,11 @@ export class PageEditorContextInjector extends BaseLastUserContentProvider {
37
40
 
38
41
  const clonedContext = this.cloneContext(context);
39
42
 
40
- // Skip if Page Editor is not enabled or no page content context
41
- if (!this.config.enabled || !this.config.pageContentContext) {
42
- log('Page Editor not enabled or no pageContentContext, skipping injection');
43
- return this.markAsExecuted(clonedContext);
44
- }
45
-
46
- // Format page content context
47
- const formattedContent = formatPageContentContext(this.config.pageContentContext);
48
-
49
- log('Formatted content length:', formattedContent.length);
43
+ // Check if we have page content to inject
44
+ const hasPageContent = this.config.enabled && this.config.pageContentContext;
50
45
 
51
- // Skip if no content to inject
52
- if (!formattedContent) {
53
- log('No content to inject after formatting');
46
+ if (!hasPageContent) {
47
+ log('No pageContentContext, skipping injection');
54
48
  return this.markAsExecuted(clonedContext);
55
49
  }
56
50
 
@@ -64,6 +58,16 @@ export class PageEditorContextInjector extends BaseLastUserContentProvider {
64
58
  return this.markAsExecuted(clonedContext);
65
59
  }
66
60
 
61
+ // Format page content
62
+ const formattedContent = formatPageContentContext(this.config.pageContentContext!);
63
+
64
+ if (!formattedContent) {
65
+ log('No content to inject after formatting');
66
+ return this.markAsExecuted(clonedContext);
67
+ }
68
+
69
+ log('Page content formatted, length:', formattedContent.length);
70
+
67
71
  // Check if system context wrapper already exists
68
72
  // If yes, only insert context block; if no, use full wrapper
69
73
  const hasExistingWrapper = this.hasExistingSystemContext(clonedContext);
@@ -0,0 +1,65 @@
1
+ import { formatPageSelections } from '@lobechat/prompts';
2
+ import type { PageSelection } from '@lobechat/types';
3
+ import debug from 'debug';
4
+
5
+ import { BaseEveryUserContentProvider } from '../base/BaseEveryUserContentProvider';
6
+ import type { Message, ProcessorOptions } from '../types';
7
+
8
+ const log = debug('context-engine:provider:PageSelectionsInjector');
9
+
10
+ export interface PageSelectionsInjectorConfig {
11
+ /** Whether Page Selections injection is enabled */
12
+ enabled?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Page Selections Injector
17
+ * Responsible for injecting page selections into each user message that has them
18
+ * Unlike PageEditorContextInjector which only injects to the last user message,
19
+ * this processor handles selections attached to any user message in the conversation
20
+ *
21
+ * This injector runs BEFORE PageEditorContextInjector so that:
22
+ * - Each user message with selections gets a SYSTEM CONTEXT wrapper
23
+ * - PageEditorContextInjector can then reuse the wrapper for the last user message
24
+ */
25
+ export class PageSelectionsInjector extends BaseEveryUserContentProvider {
26
+ readonly name = 'PageSelectionsInjector';
27
+
28
+ constructor(
29
+ private config: PageSelectionsInjectorConfig = {},
30
+ options: ProcessorOptions = {},
31
+ ) {
32
+ super(options);
33
+ }
34
+
35
+ protected buildContentForMessage(
36
+ message: Message,
37
+ index: number,
38
+ ): { content: string; contextType: string } | null {
39
+ // Skip if not enabled
40
+ if (!this.config.enabled) {
41
+ return null;
42
+ }
43
+
44
+ // Check if message has pageSelections in metadata
45
+ const pageSelections = message.metadata?.pageSelections as PageSelection[] | undefined;
46
+
47
+ if (!pageSelections || pageSelections.length === 0) {
48
+ return null;
49
+ }
50
+
51
+ // Format the selections
52
+ const formattedSelections = formatPageSelections(pageSelections);
53
+
54
+ if (!formattedSelections) {
55
+ return null;
56
+ }
57
+
58
+ log(`Building content for message at index ${index} with ${pageSelections.length} selections`);
59
+
60
+ return {
61
+ content: formattedSelections,
62
+ contextType: 'user_page_selections',
63
+ };
64
+ }
65
+ }