@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.362](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.361...v2.0.0-next.362)
6
+
7
+ <sup>Released on **2026-01-24**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix page selection not display correctly.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix page selection not display correctly, closes [#11765](https://github.com/lobehub/lobe-chat/issues/11765) ([7ae5f68](https://github.com/lobehub/lobe-chat/commit/7ae5f68))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.361](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.360...v2.0.0-next.361)
6
31
 
7
32
  <sup>Released on **2026-01-24**</sup>
package/Dockerfile CHANGED
@@ -47,7 +47,8 @@ ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}" \
47
47
  ENV APP_URL="http://app.com" \
48
48
  DATABASE_DRIVER="node" \
49
49
  DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" \
50
- KEY_VAULTS_SECRET="use-for-build"
50
+ KEY_VAULTS_SECRET="use-for-build" \
51
+ AUTH_SECRET="use-for-build"
51
52
 
52
53
  # Sentry
53
54
  ENV NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix page selection not display correctly."
6
+ ]
7
+ },
8
+ "date": "2026-01-24",
9
+ "version": "2.0.0-next.362"
10
+ },
2
11
  {
3
12
  "children": {},
4
13
  "date": "2026-01-24",
@@ -208,7 +208,9 @@
208
208
  "operation.sendMessage": "Sending message",
209
209
  "owner": "Group owner",
210
210
  "pageCopilot.title": "Page Agent",
211
- "pageCopilot.welcome": "**Clearer, sharper writing**\n\nDraft, rewrite, or polish—tell me your intent and Ill refine the rest.",
211
+ "pageCopilot.welcome": "**Clearer, sharper writing**\n\nDraft, rewrite, or polish—tell me your intent and I'll refine the rest.",
212
+ "pageSelection.lines": "Lines {{start}}-{{end}}",
213
+ "pageSelection.reference": "Selected Text",
212
214
  "pin": "Pin",
213
215
  "pinOff": "Unpin",
214
216
  "prompts.summaryExpert": "As a summary expert, please summarize the following content based on the system prompts above:",
@@ -209,6 +209,8 @@
209
209
  "owner": "群主",
210
210
  "pageCopilot.title": "文稿助理",
211
211
  "pageCopilot.welcome": "**让文字更清晰、更到位**\n\n起草、改写、润色都可以。你把意图说清楚,其余交给我打磨",
212
+ "pageSelection.lines": "第 {{start}}-{{end}} 行",
213
+ "pageSelection.reference": "选中文本",
212
214
  "pin": "置顶",
213
215
  "pinOff": "取消置顶",
214
216
  "prompts.summaryExpert": "作为一名总结专家,请结合以上系统提示词,将以下内容进行总结:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.361",
3
+ "version": "2.0.0-next.362",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1,204 @@
1
+ import type { Message, PipelineContext, ProcessorOptions } from '../types';
2
+ import { BaseProcessor } from './BaseProcessor';
3
+ import { CONTEXT_INSTRUCTION, SYSTEM_CONTEXT_END, SYSTEM_CONTEXT_START } from './constants';
4
+
5
+ /**
6
+ * Base Provider for appending content to every user message
7
+ * Used for injecting context that should be attached to each user message individually
8
+ * (e.g., page selections that are specific to each message)
9
+ *
10
+ * Features:
11
+ * - Iterates through all user messages
12
+ * - For each message, calls buildContentForMessage to get content to inject
13
+ * - Wraps content with SYSTEM CONTEXT markers (or reuses existing wrapper)
14
+ * - Runs BEFORE BaseLastUserContentProvider so that the last user message
15
+ * can reuse the SYSTEM CONTEXT wrapper created here
16
+ */
17
+ export abstract class BaseEveryUserContentProvider extends BaseProcessor {
18
+ constructor(options: ProcessorOptions = {}) {
19
+ super(options);
20
+ }
21
+
22
+ /**
23
+ * Build the content to inject for a specific user message
24
+ * Subclasses must implement this method
25
+ * @param message - The user message to build content for
26
+ * @param index - The index of the message in the messages array
27
+ * @param isLastUser - Whether this is the last user message
28
+ * @returns Object with content and contextType, or null to skip injection for this message
29
+ */
30
+ protected abstract buildContentForMessage(
31
+ message: Message,
32
+ index: number,
33
+ isLastUser: boolean,
34
+ ): { content: string; contextType: string } | null;
35
+
36
+ /**
37
+ * Get the text content from a message (handles both string and array content)
38
+ */
39
+ private getTextContent(content: string | any[]): string {
40
+ if (typeof content === 'string') {
41
+ return content;
42
+ }
43
+ if (Array.isArray(content)) {
44
+ const lastTextPart = content.findLast((part: any) => part.type === 'text');
45
+ return lastTextPart?.text || '';
46
+ }
47
+ return '';
48
+ }
49
+
50
+ /**
51
+ * Check if the content already has a system context wrapper
52
+ */
53
+ protected hasSystemContextWrapper(content: string | any[]): boolean {
54
+ const textContent = this.getTextContent(content);
55
+ return textContent.includes(SYSTEM_CONTEXT_START) && textContent.includes(SYSTEM_CONTEXT_END);
56
+ }
57
+
58
+ /**
59
+ * Wrap content with system context markers
60
+ */
61
+ protected wrapWithSystemContext(content: string, contextType: string): string {
62
+ return `${SYSTEM_CONTEXT_START}
63
+ ${CONTEXT_INSTRUCTION}
64
+ <${contextType}>
65
+ ${content}
66
+ </${contextType}>
67
+ ${SYSTEM_CONTEXT_END}`;
68
+ }
69
+
70
+ /**
71
+ * Insert content into existing system context wrapper (before the END marker)
72
+ */
73
+ private insertIntoExistingWrapper(existingContent: string, newContextBlock: string): string {
74
+ const endMarkerIndex = existingContent.lastIndexOf(SYSTEM_CONTEXT_END);
75
+ if (endMarkerIndex === -1) {
76
+ return existingContent + '\n\n' + newContextBlock;
77
+ }
78
+
79
+ const beforeEnd = existingContent.slice(0, endMarkerIndex);
80
+ const afterEnd = existingContent.slice(endMarkerIndex);
81
+
82
+ return beforeEnd + newContextBlock + '\n' + afterEnd;
83
+ }
84
+
85
+ /**
86
+ * Create a context block without the full wrapper (for inserting into existing wrapper)
87
+ */
88
+ protected createContextBlock(content: string, contextType: string): string {
89
+ return `<${contextType}>
90
+ ${content}
91
+ </${contextType}>`;
92
+ }
93
+
94
+ /**
95
+ * Append content to a message with SYSTEM CONTEXT wrapper
96
+ */
97
+ protected appendToMessage(message: Message, content: string, contextType: string): Message {
98
+ const currentContent = message.content;
99
+
100
+ // Handle string content
101
+ if (typeof currentContent === 'string') {
102
+ let newContent: string;
103
+
104
+ if (this.hasSystemContextWrapper(currentContent)) {
105
+ // Insert into existing wrapper
106
+ const contextBlock = this.createContextBlock(content, contextType);
107
+ newContent = this.insertIntoExistingWrapper(currentContent, contextBlock);
108
+ } else {
109
+ // Create new wrapper
110
+ newContent = currentContent + '\n\n' + this.wrapWithSystemContext(content, contextType);
111
+ }
112
+
113
+ return {
114
+ ...message,
115
+ content: newContent,
116
+ };
117
+ }
118
+
119
+ // Handle array content (multimodal messages)
120
+ if (Array.isArray(currentContent)) {
121
+ const lastTextIndex = currentContent.findLastIndex((part: any) => part.type === 'text');
122
+
123
+ if (lastTextIndex !== -1) {
124
+ const newContent = [...currentContent];
125
+ const existingText = newContent[lastTextIndex].text;
126
+ let updatedText: string;
127
+
128
+ if (this.hasSystemContextWrapper(existingText)) {
129
+ // Insert into existing wrapper
130
+ const contextBlock = this.createContextBlock(content, contextType);
131
+ updatedText = this.insertIntoExistingWrapper(existingText, contextBlock);
132
+ } else {
133
+ // Create new wrapper
134
+ updatedText = existingText + '\n\n' + this.wrapWithSystemContext(content, contextType);
135
+ }
136
+
137
+ newContent[lastTextIndex] = {
138
+ ...newContent[lastTextIndex],
139
+ text: updatedText,
140
+ };
141
+ return {
142
+ ...message,
143
+ content: newContent,
144
+ };
145
+ } else {
146
+ // No text part found, add a new one with wrapper
147
+ return {
148
+ ...message,
149
+ content: [
150
+ ...currentContent,
151
+ { text: this.wrapWithSystemContext(content, contextType), type: 'text' },
152
+ ],
153
+ };
154
+ }
155
+ }
156
+
157
+ return message;
158
+ }
159
+
160
+ /**
161
+ * Find the index of the last user message
162
+ */
163
+ protected findLastUserMessageIndex(messages: Message[]): number {
164
+ for (let i = messages.length - 1; i >= 0; i--) {
165
+ if (messages[i].role === 'user') {
166
+ return i;
167
+ }
168
+ }
169
+ return -1;
170
+ }
171
+
172
+ /**
173
+ * Process the context by injecting content to every user message
174
+ */
175
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
176
+ const clonedContext = this.cloneContext(context);
177
+ const lastUserIndex = this.findLastUserMessageIndex(clonedContext.messages);
178
+ let injectCount = 0;
179
+
180
+ // Iterate through all messages
181
+ for (let i = 0; i < clonedContext.messages.length; i++) {
182
+ const message = clonedContext.messages[i];
183
+
184
+ // Only process user messages
185
+ if (message.role !== 'user') continue;
186
+
187
+ const isLastUser = i === lastUserIndex;
188
+ const result = this.buildContentForMessage(message, i, isLastUser);
189
+
190
+ if (!result) continue;
191
+
192
+ // Append to this user message with SYSTEM CONTEXT wrapper
193
+ clonedContext.messages[i] = this.appendToMessage(message, result.content, result.contextType);
194
+ injectCount++;
195
+ }
196
+
197
+ // Update metadata with injection count
198
+ if (injectCount > 0) {
199
+ clonedContext.metadata[`${this.name}InjectedCount`] = injectCount;
200
+ }
201
+
202
+ return this.markAsExecuted(clonedContext);
203
+ }
204
+ }
@@ -1,13 +1,6 @@
1
1
  import type { Message, PipelineContext, ProcessorOptions } from '../types';
2
2
  import { BaseProcessor } from './BaseProcessor';
3
-
4
- const SYSTEM_CONTEXT_START = '<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->';
5
- const SYSTEM_CONTEXT_END = '<!-- END SYSTEM CONTEXT -->';
6
- const CONTEXT_INSTRUCTION = `<context.instruction>following part contains context information injected by the system. Please follow these instructions:
7
-
8
- 1. Always prioritize handling user-visible content.
9
- 2. the context is only required when user's queries rely on it.
10
- </context.instruction>`;
3
+ import { CONTEXT_INSTRUCTION, SYSTEM_CONTEXT_END, SYSTEM_CONTEXT_START } from './constants';
11
4
 
12
5
  /**
13
6
  * Base Provider for appending content to the last user message
@@ -0,0 +1,354 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { Message, PipelineContext } from '../../types';
4
+ import { BaseEveryUserContentProvider } from '../BaseEveryUserContentProvider';
5
+
6
+ class TestEveryUserContentProvider extends BaseEveryUserContentProvider {
7
+ readonly name = 'TestEveryUserContentProvider';
8
+
9
+ constructor(
10
+ private contentBuilder?: (
11
+ message: Message,
12
+ index: number,
13
+ isLastUser: boolean,
14
+ ) => { content: string; contextType: string } | null,
15
+ ) {
16
+ super();
17
+ }
18
+
19
+ protected buildContentForMessage(
20
+ message: Message,
21
+ index: number,
22
+ isLastUser: boolean,
23
+ ): { content: string; contextType: string } | null {
24
+ if (this.contentBuilder) {
25
+ return this.contentBuilder(message, index, isLastUser);
26
+ }
27
+ // Default: inject content for every user message
28
+ return {
29
+ content: `Content for message ${index}`,
30
+ contextType: 'test_context',
31
+ };
32
+ }
33
+
34
+ // Expose protected methods for testing
35
+ testHasSystemContextWrapper(content: string | any[]) {
36
+ return this.hasSystemContextWrapper(content);
37
+ }
38
+
39
+ testWrapWithSystemContext(content: string, contextType: string) {
40
+ return this.wrapWithSystemContext(content, contextType);
41
+ }
42
+
43
+ testCreateContextBlock(content: string, contextType: string) {
44
+ return this.createContextBlock(content, contextType);
45
+ }
46
+
47
+ testAppendToMessage(message: Message, content: string, contextType: string) {
48
+ return this.appendToMessage(message, content, contextType);
49
+ }
50
+
51
+ testFindLastUserMessageIndex(messages: Message[]) {
52
+ return this.findLastUserMessageIndex(messages);
53
+ }
54
+ }
55
+
56
+ describe('BaseEveryUserContentProvider', () => {
57
+ const createContext = (messages: any[] = []): PipelineContext => ({
58
+ initialState: {
59
+ messages: [],
60
+ model: 'test-model',
61
+ provider: 'test-provider',
62
+ },
63
+ isAborted: false,
64
+ messages,
65
+ metadata: {
66
+ maxTokens: 4000,
67
+ model: 'test-model',
68
+ },
69
+ });
70
+
71
+ describe('findLastUserMessageIndex', () => {
72
+ it('should find the last user message', () => {
73
+ const provider = new TestEveryUserContentProvider();
74
+ const messages = [
75
+ { content: 'Hello', role: 'user' },
76
+ { content: 'Hi', role: 'assistant' },
77
+ { content: 'Question', role: 'user' },
78
+ { content: 'Answer', role: 'assistant' },
79
+ ];
80
+
81
+ expect(provider.testFindLastUserMessageIndex(messages)).toBe(2);
82
+ });
83
+
84
+ it('should return -1 when no user messages exist', () => {
85
+ const provider = new TestEveryUserContentProvider();
86
+ const messages = [{ content: 'System', role: 'system' }];
87
+
88
+ expect(provider.testFindLastUserMessageIndex(messages)).toBe(-1);
89
+ });
90
+ });
91
+
92
+ describe('hasSystemContextWrapper', () => {
93
+ it('should detect existing system context wrapper in string content', () => {
94
+ const provider = new TestEveryUserContentProvider();
95
+
96
+ const withWrapper = `Question
97
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
98
+ <test>content</test>
99
+ <!-- END SYSTEM CONTEXT -->`;
100
+
101
+ const withoutWrapper = 'Simple question';
102
+
103
+ expect(provider.testHasSystemContextWrapper(withWrapper)).toBe(true);
104
+ expect(provider.testHasSystemContextWrapper(withoutWrapper)).toBe(false);
105
+ });
106
+
107
+ it('should detect existing system context wrapper in array content', () => {
108
+ const provider = new TestEveryUserContentProvider();
109
+
110
+ const withWrapper = [
111
+ {
112
+ text: `Question
113
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
114
+ <test>content</test>
115
+ <!-- END SYSTEM CONTEXT -->`,
116
+ type: 'text',
117
+ },
118
+ ];
119
+
120
+ const withoutWrapper = [{ text: 'Simple question', type: 'text' }];
121
+
122
+ expect(provider.testHasSystemContextWrapper(withWrapper)).toBe(true);
123
+ expect(provider.testHasSystemContextWrapper(withoutWrapper)).toBe(false);
124
+ });
125
+ });
126
+
127
+ describe('wrapWithSystemContext', () => {
128
+ it('should wrap content with system context markers', () => {
129
+ const provider = new TestEveryUserContentProvider();
130
+ const result = provider.testWrapWithSystemContext('Test content', 'test_type');
131
+
132
+ expect(result).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
133
+ expect(result).toContain('<context.instruction>');
134
+ expect(result).toContain('<test_type>');
135
+ expect(result).toContain('Test content');
136
+ expect(result).toContain('</test_type>');
137
+ expect(result).toContain('<!-- END SYSTEM CONTEXT -->');
138
+ });
139
+ });
140
+
141
+ describe('createContextBlock', () => {
142
+ it('should create context block without wrapper', () => {
143
+ const provider = new TestEveryUserContentProvider();
144
+ const result = provider.testCreateContextBlock('Block content', 'block_type');
145
+
146
+ expect(result).toBe(`<block_type>
147
+ Block content
148
+ </block_type>`);
149
+ });
150
+ });
151
+
152
+ describe('appendToMessage', () => {
153
+ it('should append with new wrapper to string content without existing wrapper', () => {
154
+ const provider = new TestEveryUserContentProvider();
155
+ const message: Message = { content: 'Original question', role: 'user' };
156
+
157
+ const result = provider.testAppendToMessage(message, 'New content', 'new_type');
158
+
159
+ expect(result.content).toContain('Original question');
160
+ expect(result.content).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
161
+ expect(result.content).toContain('<new_type>');
162
+ expect(result.content).toContain('New content');
163
+ expect(result.content).toContain('<!-- END SYSTEM CONTEXT -->');
164
+ });
165
+
166
+ it('should insert into existing wrapper in string content', () => {
167
+ const provider = new TestEveryUserContentProvider();
168
+ const message: Message = {
169
+ content: `Original question
170
+
171
+ <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
172
+ <context.instruction>...</context.instruction>
173
+ <existing_type>
174
+ Existing content
175
+ </existing_type>
176
+ <!-- END SYSTEM CONTEXT -->`,
177
+ role: 'user',
178
+ };
179
+
180
+ const result = provider.testAppendToMessage(message, 'New content', 'new_type');
181
+
182
+ // Should have only one SYSTEM CONTEXT wrapper
183
+ const content = result.content as string;
184
+ const startCount = (content.match(/<!-- SYSTEM CONTEXT/g) || []).length;
185
+ const endCount = (content.match(/<!-- END SYSTEM CONTEXT/g) || []).length;
186
+
187
+ expect(startCount).toBe(1);
188
+ expect(endCount).toBe(1);
189
+ expect(content).toContain('<existing_type>');
190
+ expect(content).toContain('<new_type>');
191
+ expect(content).toContain('New content');
192
+ });
193
+
194
+ it('should handle array content without existing wrapper', () => {
195
+ const provider = new TestEveryUserContentProvider();
196
+ const message: Message = {
197
+ content: [
198
+ { text: 'Original question', type: 'text' },
199
+ { image_url: { url: 'http://example.com/img.png' }, type: 'image_url' },
200
+ ],
201
+ role: 'user',
202
+ };
203
+
204
+ const result = provider.testAppendToMessage(message, 'New content', 'new_type');
205
+
206
+ expect(result.content[0].text).toContain('Original question');
207
+ expect(result.content[0].text).toContain('<!-- SYSTEM CONTEXT');
208
+ expect(result.content[0].text).toContain('<new_type>');
209
+ expect(result.content[1].type).toBe('image_url');
210
+ });
211
+
212
+ it('should add new text part when array content has no text part', () => {
213
+ const provider = new TestEveryUserContentProvider();
214
+ const message: Message = {
215
+ content: [{ image_url: { url: 'http://example.com/img.png' }, type: 'image_url' }],
216
+ role: 'user',
217
+ };
218
+
219
+ const result = provider.testAppendToMessage(message, 'New content', 'new_type');
220
+
221
+ expect(result.content).toHaveLength(2);
222
+ expect(result.content[1].type).toBe('text');
223
+ expect(result.content[1].text).toContain('<!-- SYSTEM CONTEXT');
224
+ expect(result.content[1].text).toContain('New content');
225
+ });
226
+ });
227
+
228
+ describe('process integration', () => {
229
+ it('should inject content to all user messages', async () => {
230
+ const provider = new TestEveryUserContentProvider();
231
+ const context = createContext([
232
+ { content: 'First question', role: 'user' },
233
+ { content: 'First answer', role: 'assistant' },
234
+ { content: 'Second question', role: 'user' },
235
+ { content: 'Second answer', role: 'assistant' },
236
+ { content: 'Third question', role: 'user' },
237
+ ]);
238
+
239
+ const result = await provider.process(context);
240
+
241
+ // All user messages should have content injected
242
+ expect(result.messages[0].content).toContain('First question');
243
+ expect(result.messages[0].content).toContain('<test_context>');
244
+ expect(result.messages[0].content).toContain('Content for message 0');
245
+
246
+ expect(result.messages[2].content).toContain('Second question');
247
+ expect(result.messages[2].content).toContain('<test_context>');
248
+ expect(result.messages[2].content).toContain('Content for message 2');
249
+
250
+ expect(result.messages[4].content).toContain('Third question');
251
+ expect(result.messages[4].content).toContain('<test_context>');
252
+ expect(result.messages[4].content).toContain('Content for message 4');
253
+
254
+ // Assistant messages should be unchanged
255
+ expect(result.messages[1].content).toBe('First answer');
256
+ expect(result.messages[3].content).toBe('Second answer');
257
+ });
258
+
259
+ it('should correctly identify isLastUser parameter', async () => {
260
+ const isLastUserCalls: boolean[] = [];
261
+
262
+ const provider = new TestEveryUserContentProvider((message, index, isLastUser) => {
263
+ isLastUserCalls.push(isLastUser);
264
+ return { content: `Content ${index}`, contextType: 'test' };
265
+ });
266
+
267
+ const context = createContext([
268
+ { content: 'First', role: 'user' },
269
+ { content: 'Answer', role: 'assistant' },
270
+ { content: 'Second', role: 'user' },
271
+ { content: 'Answer', role: 'assistant' },
272
+ { content: 'Third (last)', role: 'user' },
273
+ ]);
274
+
275
+ await provider.process(context);
276
+
277
+ expect(isLastUserCalls).toEqual([false, false, true]);
278
+ });
279
+
280
+ it('should skip injection when buildContentForMessage returns null', async () => {
281
+ const provider = new TestEveryUserContentProvider((message, index) => {
282
+ // Only inject for first user message
283
+ if (index === 0) {
284
+ return { content: 'First only', contextType: 'test' };
285
+ }
286
+ return null;
287
+ });
288
+
289
+ const context = createContext([
290
+ { content: 'First question', role: 'user' },
291
+ { content: 'Answer', role: 'assistant' },
292
+ { content: 'Second question', role: 'user' },
293
+ ]);
294
+
295
+ const result = await provider.process(context);
296
+
297
+ expect(result.messages[0].content).toContain('<test>');
298
+ expect(result.messages[0].content).toContain('First only');
299
+ expect(result.messages[2].content).toBe('Second question');
300
+ });
301
+
302
+ it('should update metadata with injection count', async () => {
303
+ const provider = new TestEveryUserContentProvider();
304
+ const context = createContext([
305
+ { content: 'First', role: 'user' },
306
+ { content: 'Second', role: 'user' },
307
+ { content: 'Third', role: 'user' },
308
+ ]);
309
+
310
+ const result = await provider.process(context);
311
+
312
+ expect(result.metadata.TestEveryUserContentProviderInjectedCount).toBe(3);
313
+ });
314
+
315
+ it('should not set metadata when no injections made', async () => {
316
+ const provider = new TestEveryUserContentProvider(() => null);
317
+ const context = createContext([{ content: 'Question', role: 'user' }]);
318
+
319
+ const result = await provider.process(context);
320
+
321
+ expect(result.metadata.TestEveryUserContentProviderInjectedCount).toBeUndefined();
322
+ });
323
+ });
324
+
325
+ describe('integration with BaseLastUserContentProvider', () => {
326
+ it('should allow BaseLastUserContentProvider to reuse wrapper created by BaseEveryUserContentProvider', async () => {
327
+ // First: BaseEveryUserContentProvider injects to last user message
328
+ const everyProvider = new TestEveryUserContentProvider((message, index, isLastUser) => {
329
+ if (isLastUser) {
330
+ return { content: 'Selection content', contextType: 'user_selections' };
331
+ }
332
+ return null;
333
+ });
334
+
335
+ const context = createContext([
336
+ { content: 'First question', role: 'user' },
337
+ { content: 'Answer', role: 'assistant' },
338
+ { content: 'Last question', role: 'user' },
339
+ ]);
340
+
341
+ const result = await everyProvider.process(context);
342
+
343
+ // The last user message should have a SYSTEM CONTEXT wrapper
344
+ const lastUserContent = result.messages[2].content as string;
345
+ expect(lastUserContent).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
346
+ expect(lastUserContent).toContain('<user_selections>');
347
+ expect(lastUserContent).toContain('Selection content');
348
+ expect(lastUserContent).toContain('<!-- END SYSTEM CONTEXT -->');
349
+
350
+ // Now BaseLastUserContentProvider can detect and reuse this wrapper
351
+ expect(everyProvider.testHasSystemContextWrapper(lastUserContent)).toBe(true);
352
+ });
353
+ });
354
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared constants for context injection
3
+ */
4
+
5
+ /**
6
+ * System context wrapper markers
7
+ * Used to wrap injected context content so models can distinguish it from user content
8
+ */
9
+ export const SYSTEM_CONTEXT_START = '<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->';
10
+ export const SYSTEM_CONTEXT_END = '<!-- END SYSTEM CONTEXT -->';
11
+
12
+ /**
13
+ * Context instruction text
14
+ * Provides guidance to the model on how to handle injected context
15
+ */
16
+ export const CONTEXT_INSTRUCTION = `<context.instruction>following part contains context information injected by the system. Please follow these instructions:
17
+
18
+ 1. Always prioritize handling user-visible content.
19
+ 2. the context is only required when user's queries rely on it.
20
+ </context.instruction>`;