@opensumi/ide-ai-native 3.8.3-next-1741747748.0 → 3.8.3-next-1741763229.0

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 (40) hide show
  1. package/lib/browser/chat/chat-agent.service.d.ts +1 -1
  2. package/lib/browser/chat/chat-agent.service.d.ts.map +1 -1
  3. package/lib/browser/chat/chat-agent.service.js +4 -4
  4. package/lib/browser/chat/chat-agent.service.js.map +1 -1
  5. package/lib/browser/chat/chat.view.d.ts.map +1 -1
  6. package/lib/browser/chat/chat.view.js +5 -4
  7. package/lib/browser/chat/chat.view.js.map +1 -1
  8. package/lib/browser/components/ChatEditor.d.ts +5 -2
  9. package/lib/browser/components/ChatEditor.d.ts.map +1 -1
  10. package/lib/browser/components/ChatEditor.js +45 -6
  11. package/lib/browser/components/ChatEditor.js.map +1 -1
  12. package/lib/browser/components/ChatMentionInput.d.ts.map +1 -1
  13. package/lib/browser/components/ChatMentionInput.js +1 -1
  14. package/lib/browser/components/ChatMentionInput.js.map +1 -1
  15. package/lib/browser/components/components.module.less +18 -0
  16. package/lib/browser/components/mention-input/mention-input.d.ts.map +1 -1
  17. package/lib/browser/components/mention-input/mention-input.js +22 -3
  18. package/lib/browser/components/mention-input/mention-input.js.map +1 -1
  19. package/lib/browser/components/mention-input/mention-input.module.less +19 -1
  20. package/lib/browser/components/mention-input/types.d.ts +2 -0
  21. package/lib/browser/components/mention-input/types.d.ts.map +1 -1
  22. package/lib/browser/components/mention-input/types.js.map +1 -1
  23. package/lib/browser/context/llm-context.service.d.ts.map +1 -1
  24. package/lib/browser/context/llm-context.service.js +1 -1
  25. package/lib/browser/context/llm-context.service.js.map +1 -1
  26. package/lib/common/prompts/context-prompt-provider.d.ts +12 -2
  27. package/lib/common/prompts/context-prompt-provider.d.ts.map +1 -1
  28. package/lib/common/prompts/context-prompt-provider.js +90 -29
  29. package/lib/common/prompts/context-prompt-provider.js.map +1 -1
  30. package/package.json +23 -23
  31. package/src/browser/chat/chat-agent.service.ts +4 -4
  32. package/src/browser/chat/chat.view.tsx +19 -4
  33. package/src/browser/components/ChatEditor.tsx +72 -9
  34. package/src/browser/components/ChatMentionInput.tsx +1 -0
  35. package/src/browser/components/components.module.less +18 -0
  36. package/src/browser/components/mention-input/mention-input.module.less +19 -1
  37. package/src/browser/components/mention-input/mention-input.tsx +32 -2
  38. package/src/browser/components/mention-input/types.ts +3 -0
  39. package/src/browser/context/llm-context.service.ts +1 -3
  40. package/src/common/prompts/context-prompt-provider.ts +122 -35
@@ -1,7 +1,14 @@
1
1
  import * as React from 'react';
2
2
  import { MessageList } from 'react-chat-elements';
3
3
 
4
- import { AINativeConfigService, AppConfig, getIcon, useInjectable, useUpdateOnEvent } from '@opensumi/ide-core-browser';
4
+ import {
5
+ AINativeConfigService,
6
+ AppConfig,
7
+ LabelService,
8
+ getIcon,
9
+ useInjectable,
10
+ useUpdateOnEvent,
11
+ } from '@opensumi/ide-core-browser';
5
12
  import { Popover, PopoverPosition } from '@opensumi/ide-core-browser/lib/components';
6
13
  import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
7
14
  import {
@@ -117,6 +124,7 @@ export const AIChatView = () => {
117
124
  const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
118
125
  const appConfig = useInjectable<AppConfig>(AppConfig);
119
126
  const applyService = useInjectable<BaseApplyService>(BaseApplyService);
127
+ const labelService = useInjectable<LabelService>(LabelService);
120
128
  const [shortcutCommands, setShortcutCommands] = React.useState<ChatSlashCommandItemModel[]>([]);
121
129
 
122
130
  const [changeList, setChangeList] = React.useState<FileChange[]>(getFileChanges(applyService.getSessionCodeBlocks()));
@@ -355,6 +363,7 @@ export const AIChatView = () => {
355
363
  text={message}
356
364
  agentId={visibleAgentId}
357
365
  command={command}
366
+ labelService={labelService}
358
367
  />
359
368
  ),
360
369
  },
@@ -460,7 +469,13 @@ export const AIChatView = () => {
460
469
  text: ChatUserRoleRender ? (
461
470
  <ChatUserRoleRender content={message} agentId={visibleAgentId} command={command} />
462
471
  ) : (
463
- <CodeBlockWrapperInput relationId={relationId} text={message} agentId={visibleAgentId} command={command} />
472
+ <CodeBlockWrapperInput
473
+ labelService={labelService}
474
+ relationId={relationId}
475
+ text={message}
476
+ agentId={visibleAgentId}
477
+ command={command}
478
+ />
464
479
  ),
465
480
  },
466
481
  styles.chat_message_code,
@@ -666,7 +681,7 @@ export const AIChatView = () => {
666
681
  llmContextService.addFileToContext(fileUri, undefined, true);
667
682
  // 获取文件内容
668
683
  // 替换占位符,后续支持自定义渲染时可替换为自定义渲染标签
669
- processedContent = processedContent.replace(match, `\`File:${fileUri.displayName}\``);
684
+ processedContent = processedContent.replace(match, `\`<attached_file>${fileUri.displayName}\``);
670
685
  }
671
686
  }
672
687
 
@@ -678,7 +693,7 @@ export const AIChatView = () => {
678
693
  const folderUri = new URI(folderPath);
679
694
  llmContextService.addFolderToContext(folderUri);
680
695
  // 替换占位符,后续支持自定义渲染时可替换为自定义渲染标签
681
- processedContent = processedContent.replace(match, `\`Folder:${folderUri.displayName}\``);
696
+ processedContent = processedContent.replace(match, `\`<attached_folder>${folderUri.displayName}\``);
682
697
  }
683
698
  }
684
699
  return handleAgentReply({ message: processedContent, agentId, command, reportExtra });
@@ -2,14 +2,15 @@ import capitalize from 'lodash/capitalize';
2
2
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import Highlight from 'react-highlight';
4
4
 
5
- import { IClipboardService, getIcon, useInjectable, uuid } from '@opensumi/ide-core-browser';
6
- import { Popover } from '@opensumi/ide-core-browser/lib/components';
5
+ import { IClipboardService, LabelService, getIcon, useInjectable, uuid } from '@opensumi/ide-core-browser';
6
+ import { Icon, Popover } from '@opensumi/ide-core-browser/lib/components';
7
7
  import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
8
8
  import {
9
9
  ActionSourceEnum,
10
10
  ActionTypeEnum,
11
11
  ChatFeatureRegistryToken,
12
12
  IAIReporter,
13
+ URI,
13
14
  localize,
14
15
  runWhenIdle,
15
16
  } from '@opensumi/ide-core-common';
@@ -139,16 +140,26 @@ const CodeBlock = ({
139
140
  renderText,
140
141
  agentId = '',
141
142
  command = '',
143
+ labelService,
142
144
  }: {
143
145
  content?: string;
144
146
  relationId: string;
145
147
  renderText?: (t: string) => React.ReactNode;
146
148
  agentId?: string;
147
149
  command?: string;
150
+ labelService?: LabelService;
148
151
  }) => {
149
152
  const rgInlineCode = /`([^`]+)`/g;
150
153
  const rgBlockCode = /```([^]+?)```/g;
151
154
  const rgBlockCodeBefore = /```([^]+)?/g;
155
+ const rgAttachedFile = /<attached_file>(.*)/g;
156
+ const rgAttachedFolder = /<attached_folder>(.*)/g;
157
+ const renderAttachment = (text: string, isFolder = false, key: string) => (
158
+ <span className={styles.attachment} key={key}>
159
+ <Icon iconClass={isFolder ? getIcon('folder') : labelService?.getIcon(new URI(text || 'file'))} />
160
+ <span className={styles.attachment_text}>{text}</span>
161
+ </span>
162
+ );
152
163
 
153
164
  const renderCodeEditor = (content: string) => {
154
165
  const language = content.split('\n')[0].trim().toLowerCase();
@@ -193,11 +204,47 @@ const CodeBlock = ({
193
204
  renderedContent.push(text);
194
205
  }
195
206
  } else {
196
- renderedContent.push(
197
- <span className={styles.code_inline} key={index}>
198
- {text}
199
- </span>,
200
- );
207
+ // 处理文件和文件夹标记
208
+ const processedText = text;
209
+ const fileMatches = [...text.matchAll(rgAttachedFile)];
210
+ const folderMatches = [...text.matchAll(rgAttachedFolder)];
211
+ if (fileMatches.length || folderMatches.length) {
212
+ let lastIndex = 0;
213
+ const fragments: (string | React.ReactNode)[] = [];
214
+
215
+ // 处理文件标记
216
+ fileMatches.forEach((match, matchIndex) => {
217
+ if (match.index !== undefined) {
218
+ const spanText = processedText.slice(lastIndex, match.index);
219
+ if (spanText) {
220
+ fragments.push(<span key={`${index}-${matchIndex}`}>{spanText}</span>);
221
+ }
222
+ fragments.push(renderAttachment(match[1], false, `${index}-tag-${matchIndex}`));
223
+ lastIndex = match.index + match[0].length;
224
+ }
225
+ });
226
+
227
+ // 处理文件夹标记
228
+ folderMatches.forEach((match, matchIndex) => {
229
+ if (match.index !== undefined) {
230
+ const spanText = processedText.slice(lastIndex, match.index);
231
+ if (spanText) {
232
+ fragments.push(<span key={`${index}-${matchIndex}`}>{spanText}</span>);
233
+ }
234
+ fragments.push(renderAttachment(match[1], true, `${index}-tag-${matchIndex}`));
235
+ lastIndex = match.index + match[0].length;
236
+ }
237
+ });
238
+
239
+ fragments.push(processedText.slice(lastIndex));
240
+ renderedContent.push(...fragments);
241
+ } else {
242
+ renderedContent.push(
243
+ <span className={styles.code_inline} key={index}>
244
+ {text}
245
+ </span>,
246
+ );
247
+ }
201
248
  }
202
249
  });
203
250
  } else {
@@ -216,15 +263,23 @@ export const CodeBlockWrapper = ({
216
263
  renderText,
217
264
  relationId,
218
265
  agentId,
266
+ labelService,
219
267
  }: {
220
268
  text?: string;
221
269
  relationId: string;
222
270
  renderText?: (t: string) => React.ReactNode;
223
271
  agentId?: string;
272
+ labelService?: LabelService;
224
273
  }) => (
225
274
  <div className={styles.ai_chat_code_wrapper}>
226
275
  <div className={styles.render_text}>
227
- <CodeBlock content={text} renderText={renderText} relationId={relationId} agentId={agentId} />
276
+ <CodeBlock
277
+ content={text}
278
+ labelService={labelService}
279
+ renderText={renderText}
280
+ relationId={relationId}
281
+ agentId={agentId}
282
+ />
228
283
  </div>
229
284
  </div>
230
285
  );
@@ -234,11 +289,13 @@ export const CodeBlockWrapperInput = ({
234
289
  relationId,
235
290
  agentId,
236
291
  command,
292
+ labelService,
237
293
  }: {
238
294
  text: string;
239
295
  relationId: string;
240
296
  agentId?: string;
241
297
  command?: string;
298
+ labelService?: LabelService;
242
299
  }) => {
243
300
  const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
244
301
  const [tag, setTag] = useState<string>('');
@@ -271,7 +328,13 @@ export const CodeBlockWrapperInput = ({
271
328
  </div>
272
329
  )}
273
330
  {command && <div className={styles.tag}>/ {command}</div>}
274
- <CodeBlock content={txt} relationId={relationId} agentId={agentId} command={command} />
331
+ <CodeBlock
332
+ content={txt}
333
+ labelService={labelService}
334
+ relationId={relationId}
335
+ agentId={agentId}
336
+ command={command}
337
+ />
275
338
  </div>
276
339
  </div>
277
340
  );
@@ -259,6 +259,7 @@ export const ChatMentionInput = React.forwardRef((props: IChatMentionInputProps)
259
259
  onSend={handleSend}
260
260
  onStop={handleStop}
261
261
  loading={disabled}
262
+ labelService={labelService}
262
263
  placeholder={localize('aiNative.chat.input.placeholder.default')}
263
264
  footerConfig={defaultMentionInputFooterOptions}
264
265
  />
@@ -566,3 +566,21 @@
566
566
  color: var(--descriptionForeground);
567
567
  }
568
568
  }
569
+
570
+ .attachment {
571
+ display: inline-flex;
572
+ align-items: center;
573
+ padding: 0 4px;
574
+ margin: 0 2px;
575
+ background: var(--badge-background);
576
+ color: var(--badge-foreground);
577
+ border-radius: 3px;
578
+ vertical-align: middle;
579
+ font-size: 12px;
580
+ }
581
+
582
+ .attachment_text {
583
+ line-height: 20px;
584
+ vertical-align: middle;
585
+ font-size: 12px;
586
+ }
@@ -3,6 +3,7 @@
3
3
  width: 100%;
4
4
  margin: 0 auto;
5
5
  border-radius: 4px;
6
+
6
7
  .model_selector {
7
8
  margin-right: 5px;
8
9
  }
@@ -55,8 +56,10 @@
55
56
  span {
56
57
  color: var(--design-text-foreground);
57
58
  }
59
+
58
60
  &:hover {
59
61
  background-color: var(--badge-background);
62
+
60
63
  span {
61
64
  color: var(--badge-foreground);
62
65
  }
@@ -75,6 +78,7 @@
75
78
  justify-content: flex-start;
76
79
  align-items: center;
77
80
  flex-direction: row;
81
+
78
82
  .left_control {
79
83
  display: flex;
80
84
  align-items: center;
@@ -82,17 +86,21 @@
82
86
  flex-direction: row;
83
87
  flex: 1;
84
88
  }
89
+
85
90
  .right_control {
86
91
  display: flex;
87
92
  align-items: center;
88
93
  justify-content: flex-end;
89
94
  flex-direction: row;
95
+
90
96
  .send_logo,
91
97
  .stop_logo {
92
98
  background-color: var(--badge-background);
93
99
  color: var(--badge-foreground);
100
+
94
101
  &:hover {
95
102
  background-color: var(--kt-primaryButton-background);
103
+
96
104
  .send_logo_icon,
97
105
  .stop_logo_icon {
98
106
  color: var(--kt-primaryButton-foreground);
@@ -155,6 +163,7 @@
155
163
  color: var(--foreground);
156
164
  border-radius: 4px;
157
165
  margin-bottom: 5px;
166
+
158
167
  &:last-child {
159
168
  margin-bottom: 0;
160
169
  }
@@ -242,9 +251,17 @@
242
251
  border-radius: 4px;
243
252
  padding: 0 4px;
244
253
  margin: 0 3px;
245
- display: inline-block;
246
254
  user-select: all;
247
255
  cursor: default;
256
+ display: inline-flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ }
260
+
261
+ .mention_icon {
262
+ display: inline-flex;
263
+ align-items: center;
264
+ justify-content: center;
248
265
  }
249
266
 
250
267
  .empty_state {
@@ -296,6 +313,7 @@
296
313
  0% {
297
314
  background-position: 100% 0;
298
315
  }
316
+
299
317
  100% {
300
318
  background-position: -100% 0;
301
319
  }
@@ -3,6 +3,7 @@ import * as React from 'react';
3
3
 
4
4
  import { Popover, PopoverPosition, Select, getIcon } from '@opensumi/ide-core-browser/lib/components';
5
5
  import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
6
+ import { URI } from '@opensumi/ide-utils';
6
7
 
7
8
  import styles from './mention-input.module.less';
8
9
  import { MentionPanel } from './mention-panel';
@@ -17,6 +18,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
17
18
  loading = false,
18
19
  mentionKeyword = MENTION_KEYWORD,
19
20
  onSelectionChange,
21
+ labelService,
20
22
  placeholder = 'Ask anything, @ to mention',
21
23
  footerConfig = {
22
24
  buttons: [],
@@ -577,7 +579,21 @@ export const MentionInput: React.FC<MentionInputProps> = ({
577
579
  mentionTag.dataset.type = item.type;
578
580
  mentionTag.dataset.contextId = item.contextId || '';
579
581
  mentionTag.contentEditable = 'false';
580
- mentionTag.textContent = mentionKeyword + item.text;
582
+
583
+ // 为 file 和 folder 类型添加图标
584
+ if (item.type === 'file' || item.type === 'folder') {
585
+ // 创建图标容器
586
+ const iconSpan = document.createElement('span');
587
+ iconSpan.className = cls(
588
+ styles.mention_icon,
589
+ item.type === 'file' ? labelService?.getIcon(new URI(item.text)) : getIcon('folder'),
590
+ );
591
+ mentionTag.appendChild(iconSpan);
592
+ }
593
+
594
+ // 创建文本内容容器
595
+ const textSpan = document.createTextNode(item.text);
596
+ mentionTag.appendChild(textSpan);
581
597
 
582
598
  // 创建一个范围从 @type: 开始到当前光标
583
599
  const tempRange = document.createRange();
@@ -666,7 +682,21 @@ export const MentionInput: React.FC<MentionInputProps> = ({
666
682
  mentionTag.dataset.type = item.type;
667
683
  mentionTag.dataset.contextId = item.contextId || '';
668
684
  mentionTag.contentEditable = 'false';
669
- mentionTag.textContent = mentionKeyword + item.text;
685
+
686
+ // 为 file 和 folder 类型添加图标
687
+ if (item.type === 'file' || item.type === 'folder') {
688
+ // 创建图标容器
689
+ const iconSpan = document.createElement('span');
690
+ iconSpan.className = cls(
691
+ styles.mention_icon,
692
+ item.type === 'file' ? labelService?.getIcon(new URI(item.text)) : getIcon('folder'),
693
+ );
694
+ mentionTag.appendChild(iconSpan);
695
+ }
696
+
697
+ // 创建文本内容容器
698
+ const textSpan = document.createTextNode(item.text);
699
+ mentionTag.appendChild(textSpan);
670
700
 
671
701
  // 定位到 @ 符号的位置
672
702
  let charIndex = 0;
@@ -1,3 +1,5 @@
1
+ import type { LabelService } from '@opensumi/ide-core-browser';
2
+
1
3
  export interface MentionItem {
2
4
  id: string;
3
5
  type: string;
@@ -74,6 +76,7 @@ export interface MentionInputProps {
74
76
  onSelectionChange?: (value: string) => void;
75
77
  footerConfig?: FooterConfig; // 新增配置项
76
78
  mentionKeyword?: string;
79
+ labelService?: LabelService;
77
80
  }
78
81
 
79
82
  export const MENTION_KEYWORD = '@';
@@ -209,9 +209,7 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
209
209
  folderPath.map(async (folder) => {
210
210
  const folderUri = new URI(folder);
211
211
  const root = workspaceRoot.relative(folderUri)?.toString() || '/';
212
- return `# Partial Folder Structure\n\`\`\`\n${root}\n${(
213
- await this.getPartiaFolderStructure(folderUri.codeUri.fsPath)
214
- )
212
+ return `\`\`\`\n${root}\n${(await this.getPartiaFolderStructure(folderUri.codeUri.fsPath))
215
213
  .map((line) => `- ${line}`)
216
214
  .join('\n')}\n\`\`\`\n`;
217
215
  }),
@@ -1,5 +1,6 @@
1
1
  import { Autowired, Injectable } from '@opensumi/di';
2
2
  import { WorkbenchEditorService } from '@opensumi/ide-editor/lib/common/editor';
3
+ import { IWorkspaceService } from '@opensumi/ide-workspace';
3
4
 
4
5
  import { SerializedContext } from '../llm-context';
5
6
 
@@ -10,7 +11,7 @@ export interface ChatAgentPromptProvider {
10
11
  * 提供上下文提示
11
12
  * @param context 上下文
12
13
  */
13
- provideContextPrompt(context: SerializedContext, userMessage: string): string;
14
+ provideContextPrompt(context: SerializedContext, userMessage: string): Promise<string>;
14
15
  }
15
16
 
16
17
  @Injectable()
@@ -18,45 +19,131 @@ export class DefaultChatAgentPromptProvider implements ChatAgentPromptProvider {
18
19
  @Autowired(WorkbenchEditorService)
19
20
  protected readonly workbenchEditorService: WorkbenchEditorService;
20
21
 
21
- provideContextPrompt(context: SerializedContext, userMessage: string): string {
22
+ @Autowired(IWorkspaceService)
23
+ protected readonly workspaceService: IWorkspaceService;
24
+
25
+ async provideContextPrompt(context: SerializedContext, userMessage: string) {
26
+ const currentFileInfo = await this.getCurrentFileInfo();
27
+
28
+ return this.buildPromptTemplate({
29
+ recentFiles: this.buildRecentFilesSection(context.recentlyViewFiles),
30
+ attachedFiles: this.buildAttachedFilesSection(context.attachedFiles),
31
+ attachedFolders: this.buildAttachedFoldersSection(context.attachedFolders),
32
+ currentFile: currentFileInfo,
33
+ userMessage,
34
+ });
35
+ }
36
+
37
+ private async getCurrentFileInfo() {
22
38
  const editor = this.workbenchEditorService.currentEditor;
23
39
  const currentModel = editor?.currentDocumentModel;
24
- return `<additional_data>
25
- Below are some potentially helpful/relevant pieces of information for figuring out to respond
26
- <recently_viewed_files>
27
- ${context.recentlyViewFiles.map((file, idx) => ` ${idx + 1}: ${file}`).join('\n')}
28
- </recently_viewed_files>
29
- <attached_files>
30
- ${context.attachedFiles.map(
31
- (file) =>
32
- `
33
- <file_contents>
40
+
41
+ if (!currentModel?.uri) {
42
+ return null;
43
+ }
44
+
45
+ const currentPath =
46
+ (await this.workspaceService.asRelativePath(currentModel.uri))?.path || currentModel.uri.codeUri.fsPath;
47
+
48
+ return {
49
+ path: currentPath,
50
+ languageId: currentModel.languageId,
51
+ content: currentModel.getText(),
52
+ };
53
+ }
54
+
55
+ private buildPromptTemplate({
56
+ recentFiles,
57
+ attachedFiles,
58
+ attachedFolders,
59
+ currentFile,
60
+ userMessage,
61
+ }: {
62
+ recentFiles: string;
63
+ attachedFiles: string;
64
+ attachedFolders: string;
65
+ currentFile: { path: string; languageId: string; content: string } | null;
66
+ userMessage: string;
67
+ }) {
68
+ const sections = [
69
+ '<additional_data>',
70
+ 'Below are some potentially helpful/relevant pieces of information for figuring out to respond',
71
+ recentFiles,
72
+ attachedFiles,
73
+ attachedFolders,
74
+ this.buildCurrentFileSection(currentFile),
75
+ '</additional_data>',
76
+ '<user_query>',
77
+ userMessage,
78
+ '</user_query>',
79
+ ].filter(Boolean);
80
+
81
+ return sections.join('\n');
82
+ }
83
+
84
+ private buildRecentFilesSection(files: string[]): string {
85
+ if (!files.length) {
86
+ return '';
87
+ }
88
+
89
+ return `<recently_viewed_files>
90
+ ${files.map((file, idx) => ` ${idx + 1}: ${file}`).join('\n')}
91
+ </recently_viewed_files>`;
92
+ }
93
+
94
+ private buildAttachedFilesSection(files: { path: string; content: string; lineErrors: string[] }[]): string {
95
+ if (!files.length) {
96
+ return '';
97
+ }
98
+
99
+ const fileContents = files
100
+ .map((file) => {
101
+ const sections = [
102
+ this.buildFileContentSection(file),
103
+ file.lineErrors.length ? this.buildLineErrorsSection(file.lineErrors) : '',
104
+ ].filter(Boolean);
105
+
106
+ return sections.join('\n');
107
+ })
108
+ .filter(Boolean)
109
+ .join('\n');
110
+
111
+ return `<attached_files>\n${fileContents}\n</attached_files>`;
112
+ }
113
+
114
+ private buildFileContentSection(file: { path: string; content: string }): string {
115
+ return `<file_contents>
34
116
  \`\`\`${file.path}
35
117
  ${file.content}
36
118
  \`\`\`
37
- </file_contents>
38
- <linter_errors>
39
- ${file.lineErrors.join('\n')}
40
- </linter_errors>
41
- `,
42
- )}
43
- </attached_files>
44
- <attached_folders>
45
-
46
- ${context.attachedFolders.join('\n')}
47
- </attached_folders>
48
- ${
49
- currentModel
50
- ? `<current_opened_file>
51
- \`\`\`${currentModel.languageId} ${currentModel.uri.toString()}
52
- ${currentModel.getText()}
119
+ </file_contents>`;
120
+ }
121
+
122
+ private buildLineErrorsSection(errors: string[]): string {
123
+ if (!errors.length) {
124
+ return '';
125
+ }
126
+
127
+ return `<linter_errors>\n${errors.join('\n')}\n</linter_errors>`;
128
+ }
129
+
130
+ private buildAttachedFoldersSection(folders: string[]): string {
131
+ if (!folders.length) {
132
+ return '';
133
+ }
134
+
135
+ return `<attached_folders>\n${folders.join('\n')}</attached_folders>`;
136
+ }
137
+
138
+ private buildCurrentFileSection(fileInfo: { path: string; languageId: string; content: string } | null): string {
139
+ if (!fileInfo) {
140
+ return '';
141
+ }
142
+
143
+ return `<current_opened_file>
144
+ \`\`\`${fileInfo.languageId} ${fileInfo.path}
145
+ ${fileInfo.content}
53
146
  \`\`\`
54
- </current_opened_file>`
55
- : ''
56
- }
57
- </additional_data>
58
- <user_query>
59
- ${userMessage}
60
- </user_query>`;
147
+ </current_opened_file>`;
61
148
  }
62
149
  }