@theia/ai-chat 1.63.0-next.24 → 1.63.0-next.52

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 (80) hide show
  1. package/lib/browser/agent-delegation-tool.d.ts +25 -0
  2. package/lib/browser/agent-delegation-tool.d.ts.map +1 -0
  3. package/lib/browser/agent-delegation-tool.js +171 -0
  4. package/lib/browser/agent-delegation-tool.js.map +1 -0
  5. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-frontend-module.js +8 -0
  7. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  8. package/lib/browser/change-set-file-element.d.ts +19 -1
  9. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  10. package/lib/browser/change-set-file-element.js +63 -8
  11. package/lib/browser/change-set-file-element.js.map +1 -1
  12. package/lib/browser/chat-tool-preferences.d.ts +1 -1
  13. package/lib/browser/chat-tool-preferences.d.ts.map +1 -1
  14. package/lib/browser/chat-tool-preferences.js +4 -4
  15. package/lib/browser/chat-tool-preferences.js.map +1 -1
  16. package/lib/browser/chat-tool-request-service.js +1 -1
  17. package/lib/browser/chat-tool-request-service.js.map +1 -1
  18. package/lib/browser/delegation-response-content.d.ts +20 -0
  19. package/lib/browser/delegation-response-content.d.ts.map +1 -0
  20. package/lib/browser/delegation-response-content.js +51 -0
  21. package/lib/browser/delegation-response-content.js.map +1 -0
  22. package/lib/browser/file-chat-variable-contribution.d.ts +15 -1
  23. package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
  24. package/lib/browser/file-chat-variable-contribution.js +111 -5
  25. package/lib/browser/file-chat-variable-contribution.js.map +1 -1
  26. package/lib/browser/image-context-variable-contribution.d.ts +27 -0
  27. package/lib/browser/image-context-variable-contribution.d.ts.map +1 -0
  28. package/lib/browser/image-context-variable-contribution.js +149 -0
  29. package/lib/browser/image-context-variable-contribution.js.map +1 -0
  30. package/lib/browser/task-context-service.d.ts +9 -3
  31. package/lib/browser/task-context-service.d.ts.map +1 -1
  32. package/lib/browser/task-context-service.js +111 -9
  33. package/lib/browser/task-context-service.js.map +1 -1
  34. package/lib/browser/task-context-storage-service.d.ts +1 -0
  35. package/lib/browser/task-context-storage-service.d.ts.map +1 -1
  36. package/lib/browser/task-context-storage-service.js +4 -1
  37. package/lib/browser/task-context-storage-service.js.map +1 -1
  38. package/lib/common/change-set.js +1 -1
  39. package/lib/common/change-set.js.map +1 -1
  40. package/lib/common/chat-agent-service.d.ts +1 -0
  41. package/lib/common/chat-agent-service.d.ts.map +1 -1
  42. package/lib/common/chat-agent-service.js +2 -1
  43. package/lib/common/chat-agent-service.js.map +1 -1
  44. package/lib/common/chat-agents.d.ts +2 -2
  45. package/lib/common/chat-agents.d.ts.map +1 -1
  46. package/lib/common/chat-agents.js +21 -5
  47. package/lib/common/chat-agents.js.map +1 -1
  48. package/lib/common/chat-model.d.ts +2 -2
  49. package/lib/common/chat-model.d.ts.map +1 -1
  50. package/lib/common/chat-model.js +1 -1
  51. package/lib/common/chat-model.js.map +1 -1
  52. package/lib/common/chat-request-parser.d.ts.map +1 -1
  53. package/lib/common/chat-request-parser.js +3 -0
  54. package/lib/common/chat-request-parser.js.map +1 -1
  55. package/lib/common/chat-service.d.ts +3 -2
  56. package/lib/common/chat-service.d.ts.map +1 -1
  57. package/lib/common/chat-service.js +4 -3
  58. package/lib/common/chat-service.js.map +1 -1
  59. package/lib/common/image-context-variable.d.ts +29 -0
  60. package/lib/common/image-context-variable.d.ts.map +1 -0
  61. package/lib/common/image-context-variable.js +99 -0
  62. package/lib/common/image-context-variable.js.map +1 -0
  63. package/package.json +10 -9
  64. package/src/browser/agent-delegation-tool.ts +207 -0
  65. package/src/browser/ai-chat-frontend-module.ts +20 -2
  66. package/src/browser/change-set-file-element.ts +69 -9
  67. package/src/browser/chat-tool-preferences.ts +4 -4
  68. package/src/browser/chat-tool-request-service.ts +1 -1
  69. package/src/browser/delegation-response-content.ts +55 -0
  70. package/src/browser/file-chat-variable-contribution.ts +120 -6
  71. package/src/browser/image-context-variable-contribution.ts +153 -0
  72. package/src/browser/task-context-service.ts +115 -9
  73. package/src/browser/task-context-storage-service.ts +5 -1
  74. package/src/common/change-set.ts +1 -1
  75. package/src/common/chat-agent-service.ts +1 -0
  76. package/src/common/chat-agents.ts +26 -9
  77. package/src/common/chat-model.ts +11 -3
  78. package/src/common/chat-request-parser.ts +3 -0
  79. package/src/common/chat-service.ts +5 -4
  80. package/src/common/image-context-variable.ts +116 -0
@@ -17,13 +17,14 @@
17
17
  import { AIVariableContext, AIVariableResolutionRequest, PromptText } from '@theia/ai-core';
18
18
  import { AIVariableCompletionContext, AIVariableDropResult, FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
19
19
  import { FILE_VARIABLE } from '@theia/ai-core/lib/browser/file-variable-contribution';
20
- import { CancellationToken, QuickInputService, URI } from '@theia/core';
20
+ import { CancellationToken, ILogger, QuickInputService, URI } from '@theia/core';
21
21
  import { inject, injectable } from '@theia/core/shared/inversify';
22
22
  import * as monaco from '@theia/monaco-editor-core';
23
23
  import { FileQuickPickItem, QuickFileSelectService } from '@theia/file-search/lib/browser/quick-file-select-service';
24
24
  import { WorkspaceService } from '@theia/workspace/lib/browser';
25
25
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
26
26
  import { VARIABLE_ADD_CONTEXT_COMMAND } from './ai-chat-frontend-contribution';
27
+ import { IMAGE_CONTEXT_VARIABLE, ImageContextVariable } from '../common/image-context-variable';
27
28
 
28
29
  @injectable()
29
30
  export class FileChatVariableContribution implements FrontendVariableContribution {
@@ -39,8 +40,12 @@ export class FileChatVariableContribution implements FrontendVariableContributio
39
40
  @inject(QuickFileSelectService)
40
41
  protected readonly quickFileSelectService: QuickFileSelectService;
41
42
 
43
+ @inject(ILogger)
44
+ protected readonly logger: ILogger;
45
+
42
46
  registerVariables(service: FrontendVariableService): void {
43
47
  service.registerArgumentPicker(FILE_VARIABLE, this.triggerArgumentPicker.bind(this));
48
+ service.registerArgumentPicker(IMAGE_CONTEXT_VARIABLE, this.imageArgumentPicker.bind(this));
44
49
  service.registerArgumentCompletionProvider(FILE_VARIABLE, this.provideArgumentCompletionItems.bind(this));
45
50
  service.registerDropHandler(this.handleDrop.bind(this));
46
51
  }
@@ -68,6 +73,57 @@ export class FileChatVariableContribution implements FrontendVariableContributio
68
73
  });
69
74
  }
70
75
 
76
+ protected async imageArgumentPicker(): Promise<string | undefined> {
77
+ const quickPick = this.quickInputService.createQuickPick();
78
+ quickPick.title = 'Select an image file';
79
+
80
+ // Get all files and filter only image files
81
+ const allPicks = await this.quickFileSelectService.getPicks();
82
+ quickPick.items = allPicks.filter(item => {
83
+ if (FileQuickPickItem.is(item)) {
84
+ return this.isImageFile(item.uri.path.toString());
85
+ }
86
+ return false;
87
+ });
88
+
89
+ const updateItems = async (value: string) => {
90
+ const filteredPicks = await this.quickFileSelectService.getPicks(value, CancellationToken.None);
91
+ quickPick.items = filteredPicks.filter(item => {
92
+ if (FileQuickPickItem.is(item)) {
93
+ return this.isImageFile(item.uri.path.toString());
94
+ }
95
+ return false;
96
+ });
97
+ };
98
+
99
+ const onChangeListener = quickPick.onDidChangeValue(updateItems);
100
+ quickPick.show();
101
+
102
+ return new Promise(resolve => {
103
+ quickPick.onDispose(onChangeListener.dispose);
104
+ quickPick.onDidAccept(async () => {
105
+ const selectedItem = quickPick.selectedItems[0];
106
+ if (selectedItem && FileQuickPickItem.is(selectedItem)) {
107
+ quickPick.dispose();
108
+ const filePath = await this.wsService.getWorkspaceRelativePath(selectedItem.uri);
109
+ const fileName = selectedItem.uri.displayName;
110
+ const base64Data = await this.fileToBase64(selectedItem.uri);
111
+ const mimeType = this.getMimeTypeFromExtension(selectedItem.uri.path.toString());
112
+
113
+ // Create the argument string in the required format
114
+ const imageVarArgs: ImageContextVariable = {
115
+ name: fileName,
116
+ wsRelativePath: filePath,
117
+ data: base64Data,
118
+ mimeType: mimeType
119
+ };
120
+
121
+ resolve(ImageContextVariable.createArgString(imageVarArgs));
122
+ }
123
+ });
124
+ });
125
+ }
126
+
71
127
  protected async provideArgumentCompletionItems(
72
128
  model: monaco.editor.ITextModel,
73
129
  position: monaco.Position,
@@ -106,6 +162,50 @@ export class FileChatVariableContribution implements FrontendVariableContributio
106
162
  );
107
163
  }
108
164
 
165
+ /**
166
+ * Checks if a file is an image based on its extension.
167
+ */
168
+ protected isImageFile(filePath: string): boolean {
169
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'];
170
+ const extension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
171
+ return imageExtensions.includes(extension);
172
+ }
173
+
174
+ /**
175
+ * Determines the MIME type based on file extension.
176
+ */
177
+ protected getMimeTypeFromExtension(filePath: string): string {
178
+ const extension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
179
+ const mimeTypes: { [key: string]: string } = {
180
+ '.jpg': 'image/jpeg',
181
+ '.jpeg': 'image/jpeg',
182
+ '.png': 'image/png',
183
+ '.gif': 'image/gif',
184
+ '.bmp': 'image/bmp',
185
+ '.svg': 'image/svg+xml',
186
+ '.webp': 'image/webp'
187
+ };
188
+ return mimeTypes[extension] || 'application/octet-stream';
189
+ }
190
+
191
+ /**
192
+ * Converts a file to base64 data URL.
193
+ */
194
+ protected async fileToBase64(uri: URI): Promise<string> {
195
+ try {
196
+ const fileContent = await this.fileService.readFile(uri);
197
+ const uint8Array = new Uint8Array(fileContent.value.buffer);
198
+ let binary = '';
199
+ for (let i = 0; i < uint8Array.length; i++) {
200
+ binary += String.fromCharCode(uint8Array[i]);
201
+ }
202
+ return btoa(binary);
203
+ } catch (error) {
204
+ this.logger.error('Error reading file content:', error);
205
+ return '';
206
+ }
207
+ }
208
+
109
209
  protected async handleDrop(event: DragEvent, _: AIVariableContext): Promise<AIVariableDropResult | undefined> {
110
210
  const data = event.dataTransfer?.getData('selected-tree-nodes');
111
211
  if (!data) {
@@ -126,11 +226,25 @@ export class FileChatVariableContribution implements FrontendVariableContributio
126
226
  const uri = URI.fromFilePath(filePath);
127
227
  if (await this.fileService.exists(uri)) {
128
228
  const wsRelativePath = await this.wsService.getWorkspaceRelativePath(uri);
129
- variables.push({
130
- variable: FILE_VARIABLE,
131
- arg: wsRelativePath
132
- });
133
- texts.push(`${PromptText.VARIABLE_CHAR}${FILE_VARIABLE.name}${PromptText.VARIABLE_SEPARATOR_CHAR}${wsRelativePath}`);
229
+ const fileName = uri.displayName;
230
+
231
+ if (this.isImageFile(filePath)) {
232
+ const base64Data = await this.fileToBase64(uri);
233
+ const mimeType = this.getMimeTypeFromExtension(filePath);
234
+ variables.push(ImageContextVariable.createRequest({
235
+ [ImageContextVariable.name]: fileName,
236
+ [ImageContextVariable.wsRelativePath]: wsRelativePath,
237
+ [ImageContextVariable.data]: base64Data,
238
+ [ImageContextVariable.mimeType]: mimeType
239
+ }));
240
+ // we do not want to push a text for image variables
241
+ } else {
242
+ variables.push({
243
+ variable: FILE_VARIABLE,
244
+ arg: wsRelativePath
245
+ });
246
+ texts.push(`${PromptText.VARIABLE_CHAR}${FILE_VARIABLE.name}${PromptText.VARIABLE_SEPARATOR_CHAR}${wsRelativePath}`);
247
+ }
134
248
  }
135
249
  }
136
250
 
@@ -0,0 +1,153 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import {
18
+ AIVariableContext, AIVariableContribution,
19
+ AIVariableOpener, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIContextVariable
20
+ } from '@theia/ai-core';
21
+ import { FrontendVariableService, AIVariablePasteResult } from '@theia/ai-core/lib/browser';
22
+ import { Path, URI } from '@theia/core';
23
+ import { LabelProvider, LabelProviderContribution, open, OpenerService } from '@theia/core/lib/browser';
24
+ import { inject, injectable } from '@theia/core/shared/inversify';
25
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
26
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
27
+ import { IMAGE_CONTEXT_VARIABLE, ImageContextVariable, ImageContextVariableRequest } from '../common/image-context-variable';
28
+
29
+ @injectable()
30
+ export class ImageContextVariableContribution implements AIVariableContribution, AIVariableResolver, AIVariableOpener, LabelProviderContribution {
31
+ @inject(FileService)
32
+ protected readonly fileService: FileService;
33
+
34
+ @inject(WorkspaceService)
35
+ protected readonly wsService: WorkspaceService;
36
+
37
+ @inject(OpenerService)
38
+ protected readonly openerService: OpenerService;
39
+
40
+ @inject(LabelProvider)
41
+ protected readonly labelProvider: LabelProvider;
42
+
43
+ registerVariables(service: FrontendVariableService): void {
44
+ service.registerResolver(IMAGE_CONTEXT_VARIABLE, this);
45
+ service.registerOpener(IMAGE_CONTEXT_VARIABLE, this);
46
+ service.registerPasteHandler(this.handlePaste.bind(this));
47
+ }
48
+
49
+ async canResolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<number> {
50
+ return ImageContextVariable.isImageContextRequest(request) ? 1 : 0;
51
+ }
52
+
53
+ async resolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<ResolvedAIContextVariable | undefined> {
54
+ return ImageContextVariable.resolve(request as ImageContextVariableRequest);
55
+ }
56
+
57
+ async canOpen(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<number> {
58
+ return ImageContextVariable.isImageContextRequest(request) && !!ImageContextVariable.parseRequest(request)?.wsRelativePath ? 1 : 0;
59
+ }
60
+
61
+ async open(request: ImageContextVariableRequest, context: AIVariableContext): Promise<void> {
62
+ const uri = await this.toUri(request);
63
+ if (!uri) {
64
+ throw new Error('Unable to resolve URI for request.');
65
+ }
66
+ await open(this.openerService, uri);
67
+ }
68
+
69
+ protected async toUri(request: ImageContextVariableRequest): Promise<URI | undefined> {
70
+ const variable = ImageContextVariable.parseRequest(request);
71
+ return variable?.wsRelativePath ? this.makeAbsolute(variable.wsRelativePath) : undefined;
72
+ }
73
+
74
+ async handlePaste(event: ClipboardEvent, context: AIVariableContext): Promise<AIVariablePasteResult | undefined> {
75
+ if (!event.clipboardData?.items) { return undefined; }
76
+
77
+ const variables: AIVariableResolutionRequest[] = [];
78
+
79
+ for (const item of event.clipboardData.items) {
80
+ if (item.type.startsWith('image/')) {
81
+ const blob = item.getAsFile();
82
+ if (blob) {
83
+ try {
84
+ const dataUrl = await this.readFileAsDataURL(blob);
85
+ // Extract the base64 data by removing the data URL prefix
86
+ // Format is like: data:image/png;base64,BASE64DATA
87
+ const imageData = dataUrl.substring(dataUrl.indexOf(',') + 1);
88
+ variables.push(ImageContextVariable.createRequest({
89
+ data: imageData,
90
+ name: blob.name || `pasted-image-${Date.now()}.png`,
91
+ mimeType: blob.type
92
+ }));
93
+ } catch (error) {
94
+ console.error('Failed to process pasted image:', error);
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return variables.length > 0 ? { variables } : undefined;
101
+ }
102
+
103
+ private readFileAsDataURL(blob: Blob): Promise<string> {
104
+ return new Promise((resolve, reject) => {
105
+ const reader = new FileReader();
106
+ reader.onload = e => {
107
+ if (!e.target?.result) {
108
+ reject(new Error('Failed to read file as data URL'));
109
+ return;
110
+ }
111
+ resolve(e.target.result as string);
112
+ };
113
+ reader.onerror = () => reject(reader.error);
114
+ reader.readAsDataURL(blob);
115
+ });
116
+ }
117
+
118
+ protected async makeAbsolute(pathStr: string): Promise<URI | undefined> {
119
+ const path = new Path(Path.normalizePathSeparator(pathStr));
120
+ if (!path.isAbsolute) {
121
+ const workspaceRoots = this.wsService.tryGetRoots();
122
+ const wsUris = workspaceRoots.map(root => root.resource.resolve(path));
123
+ for (const uri of wsUris) {
124
+ if (await this.fileService.exists(uri)) {
125
+ return uri;
126
+ }
127
+ }
128
+ }
129
+ const argUri = new URI(pathStr);
130
+ if (await this.fileService.exists(argUri)) {
131
+ return argUri;
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ canHandle(element: object): number {
137
+ return ImageContextVariable.isImageContextRequest(element) ? 10 : -1;
138
+ }
139
+
140
+ getIcon(element: ImageContextVariableRequest): string | undefined {
141
+ const path = ImageContextVariable.parseArg(element.arg).wsRelativePath;
142
+ return path ? this.labelProvider.getIcon(new URI(path)) : undefined;
143
+ }
144
+
145
+ getName(element: ImageContextVariableRequest): string | undefined {
146
+ return ImageContextVariable.parseArg(element.arg).name;
147
+ }
148
+
149
+ getDetails(element: ImageContextVariableRequest): string | undefined {
150
+ const path = ImageContextVariable.parseArg(element.arg).wsRelativePath;
151
+ return path ? this.labelProvider.getDetails(new URI(path)) : '[pasted]';
152
+ }
153
+ }
@@ -15,12 +15,15 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { inject, injectable } from '@theia/core/shared/inversify';
18
- import { MaybePromise, ProgressService, URI, generateUuid, Event } from '@theia/core';
18
+ import { MaybePromise, ProgressService, URI, generateUuid, Event, EOL } from '@theia/core';
19
19
  import { ChatAgent, ChatAgentLocation, ChatService, ChatSession, MutableChatModel, MutableChatRequestModel, ParsedChatRequestTextPart } from '../common';
20
+ import { PreferenceService } from '@theia/core/lib/browser';
20
21
  import { ChatSessionSummaryAgent } from '../common/chat-session-summary-agent';
21
22
  import { Deferred } from '@theia/core/lib/common/promise-util';
22
- import { AgentService, PromptService } from '@theia/ai-core';
23
+ import { AgentService, PromptService, ResolvedPromptFragment } from '@theia/ai-core';
23
24
  import { CHAT_SESSION_SUMMARY_PROMPT } from '../common/chat-session-summary-agent-prompt';
25
+ import { ChangeSetFileElementFactory } from './change-set-file-element';
26
+ import * as yaml from 'js-yaml';
24
27
 
25
28
  export interface SummaryMetadata {
26
29
  label: string;
@@ -33,7 +36,7 @@ export interface Summary extends SummaryMetadata {
33
36
  id: string;
34
37
  }
35
38
 
36
- export const TaskContextStorageService = Symbol('TextContextStorageService');
39
+ export const TaskContextStorageService = Symbol('TaskContextStorageService');
37
40
  export interface TaskContextStorageService {
38
41
  onDidChange: Event<void>;
39
42
  store(summary: Summary): MaybePromise<void>;
@@ -53,6 +56,9 @@ export class TaskContextService {
53
56
  @inject(PromptService) protected readonly promptService: PromptService;
54
57
  @inject(TaskContextStorageService) protected readonly storageService: TaskContextStorageService;
55
58
  @inject(ProgressService) protected readonly progressService: ProgressService;
59
+ @inject(PreferenceService) protected readonly preferenceService: PreferenceService;
60
+ @inject(ChangeSetFileElementFactory)
61
+ protected readonly fileChangeFactory: ChangeSetFileElementFactory;
56
62
 
57
63
  get onDidChange(): Event<void> {
58
64
  return this.storageService.onDidChange;
@@ -77,18 +83,19 @@ export class TaskContextService {
77
83
  }
78
84
 
79
85
  /** Returns an ID that can be used to refer to the summary in the future. */
80
- async summarize(session: ChatSession, promptId?: string, agent?: ChatAgent): Promise<string> {
86
+ async summarize(session: ChatSession, promptId?: string, agent?: ChatAgent, override = true): Promise<string> {
81
87
  const pending = this.pendingSummaries.get(session.id);
82
88
  if (pending) { return pending.then(({ id }) => id); }
83
89
  const existing = this.getSummaryForSession(session);
84
- if (existing) { return existing.id; }
90
+ if (existing && !override) { return existing.id; }
85
91
  const summaryId = generateUuid();
86
92
  const summaryDeferred = new Deferred<Summary>();
87
93
  const progress = await this.progressService.showProgress({ text: `Summarize: ${session.title || session.id}`, options: { location: 'ai-chat' } });
88
94
  this.pendingSummaries.set(session.id, summaryDeferred.promise);
89
95
  try {
96
+ const prompt = await this.getSystemPrompt(session, promptId);
90
97
  const newSummary: Summary = {
91
- summary: await this.getLlmSummary(session, promptId, agent),
98
+ summary: await this.getLlmSummary(session, prompt, agent),
92
99
  label: session.title || session.id,
93
100
  sessionId: session.id,
94
101
  id: summaryId
@@ -97,6 +104,13 @@ export class TaskContextService {
97
104
  return summaryId;
98
105
  } catch (err) {
99
106
  summaryDeferred.reject(err);
107
+ const errorSummary: Summary = {
108
+ summary: `Summary creation failed: ${err instanceof Error ? err.message : typeof err === 'string' ? err : 'Unknown error'}`,
109
+ label: session.title || session.id,
110
+ sessionId: session.id,
111
+ id: summaryId
112
+ };
113
+ await this.storageService.store(errorSummary);
100
114
  throw err;
101
115
  } finally {
102
116
  progress.cancel();
@@ -104,7 +118,95 @@ export class TaskContextService {
104
118
  }
105
119
  }
106
120
 
107
- protected async getLlmSummary(session: ChatSession, promptId: string = CHAT_SESSION_SUMMARY_PROMPT.id, agent?: ChatAgent): Promise<string> {
121
+ async update(session: ChatSession, promptId?: string, agent?: ChatAgent, override = true): Promise<string> {
122
+ // Get the existing summary for the session
123
+ const existingSummary = this.getSummaryForSession(session);
124
+ if (!existingSummary) {
125
+ // If no summary exists, create one instead
126
+ // TODO: Maybe we could also look into the task context folder and ask for the existing ones with an additional menu to create a new one?
127
+ return this.summarize(session, promptId, agent, override);
128
+ }
129
+
130
+ const progress = await this.progressService.showProgress({ text: `Updating: ${session.title || session.id}`, options: { location: 'ai-chat' } });
131
+ try {
132
+ const prompt = await this.getSystemPrompt(session, promptId);
133
+ if (!prompt) {
134
+ return '';
135
+ }
136
+
137
+ // Get the task context file path
138
+ const taskContextStorageDirectory = this.preferenceService.get(
139
+ // preference key is defined in TASK_CONTEXT_STORAGE_DIRECTORY_PREF in @theia/ai-ide
140
+ 'ai-features.promptTemplates.taskContextStorageDirectory',
141
+ '.prompts/task-contexts'
142
+ );
143
+ const taskContextFileVariable = session.model.context.getVariables().find(variableReq => variableReq.variable.id === 'file-provider' &&
144
+ typeof variableReq.arg === 'string' &&
145
+ (variableReq.arg.startsWith(taskContextStorageDirectory)));
146
+
147
+ // Check if we have a document path to update
148
+ if (taskContextFileVariable && typeof taskContextFileVariable.arg === 'string') {
149
+ // Set document path in prompt template
150
+ const documentPath = taskContextFileVariable.arg;
151
+
152
+ // Modify prompt to include the document path and content
153
+ prompt.text = prompt.text + '\nThe document to update is: ' + documentPath + '\n\n## Current Document Content\n\n' + existingSummary.summary;
154
+
155
+ // Get updated document content from LLM
156
+ const updatedDocumentContent = await this.getLlmSummary(session, prompt, agent);
157
+
158
+ if (existingSummary.uri) {
159
+ // updated document metadata shall be updated.
160
+ // otherwise, frontmatter won't be set
161
+ const frontmatter = {
162
+ sessionId: existingSummary.sessionId,
163
+ date: new Date().toISOString(),
164
+ label: existingSummary.label,
165
+ };
166
+ const content = yaml.dump(frontmatter).trim() + `${EOL}---${EOL}` + updatedDocumentContent;
167
+
168
+ session.model.changeSet.addElements(this.fileChangeFactory({
169
+ uri: existingSummary.uri,
170
+ type: 'modify',
171
+ state: 'pending',
172
+ targetState: content,
173
+ requestId: session.model.id, // not a request id, as no changeRequest made yet.
174
+ chatSessionId: session.id
175
+ }));
176
+ } else {
177
+ const updatedSummary: Summary = {
178
+ ...existingSummary,
179
+ summary: updatedDocumentContent
180
+ };
181
+
182
+ // Store the updated summary
183
+ await this.storageService.store(updatedSummary);
184
+ }
185
+ return existingSummary.id;
186
+ } else {
187
+ // Fall back to standard update if no document path is found
188
+ const updatedSummaryText = await this.getLlmSummary(session, prompt, agent);
189
+ const updatedSummary: Summary = {
190
+ ...existingSummary,
191
+ summary: updatedSummaryText
192
+ };
193
+ await this.storageService.store(updatedSummary);
194
+ return updatedSummary.id;
195
+ }
196
+ } catch (err) {
197
+ const errorSummary: Summary = {
198
+ ...existingSummary,
199
+ summary: `Summary update failed: ${err instanceof Error ? err.message : typeof err === 'string' ? err : 'Unknown error'}`
200
+ };
201
+ await this.storageService.store(errorSummary);
202
+ throw err;
203
+ } finally {
204
+ progress.cancel();
205
+ }
206
+ }
207
+
208
+ protected async getLlmSummary(session: ChatSession, prompt: ResolvedPromptFragment | undefined, agent?: ChatAgent): Promise<string> {
209
+ if (!prompt) { return ''; }
108
210
  agent = agent || this.agentService.getAgents().find<ChatAgent>((candidate): candidate is ChatAgent =>
109
211
  'invoke' in candidate
110
212
  && typeof candidate.invoke === 'function'
@@ -112,8 +214,7 @@ export class TaskContextService {
112
214
  );
113
215
  if (!agent) { throw new Error('Unable to identify agent for summary.'); }
114
216
  const model = new MutableChatModel(ChatAgentLocation.Panel);
115
- const prompt = await this.promptService.getResolvedPromptFragment(promptId || CHAT_SESSION_SUMMARY_PROMPT.id, undefined, { model: session.model });
116
- if (!prompt) { return ''; }
217
+
117
218
  const messages = session.model.getRequests().filter((candidate): candidate is MutableChatRequestModel => candidate instanceof MutableChatRequestModel);
118
219
  messages.forEach(message => model['_hierarchy'].append(message));
119
220
  const summaryRequest = model.addRequest({
@@ -126,6 +227,11 @@ export class TaskContextService {
126
227
  return summaryRequest.response.response.asDisplayString();
127
228
  }
128
229
 
230
+ protected async getSystemPrompt(session: ChatSession, promptId: string = CHAT_SESSION_SUMMARY_PROMPT.id): Promise<ResolvedPromptFragment | undefined> {
231
+ const prompt = await this.promptService.getResolvedPromptFragment(promptId || CHAT_SESSION_SUMMARY_PROMPT.id, undefined, { model: session.model });
232
+ return prompt;
233
+ }
234
+
129
235
  hasSummary(chatSession: ChatSession): boolean {
130
236
  return !!this.getSummaryForSession(chatSession);
131
237
  }
@@ -28,6 +28,10 @@ export class InMemoryTaskContextStorage implements TaskContextStorageService {
28
28
  protected readonly onDidChangeEmitter = new Emitter<void>();
29
29
  readonly onDidChange = this.onDidChangeEmitter.event;
30
30
 
31
+ protected sanitizeLabel(label: string): string {
32
+ return label.replace(/^[^\p{L}\p{N}]+/vg, '');
33
+ }
34
+
31
35
  @inject(AIVariableResourceResolver)
32
36
  protected readonly variableResourceResolver: AIVariableResourceResolver;
33
37
 
@@ -35,7 +39,7 @@ export class InMemoryTaskContextStorage implements TaskContextStorageService {
35
39
  protected readonly openerService: OpenerService;
36
40
 
37
41
  store(summary: Summary): void {
38
- this.summaries.set(summary.id, summary);
42
+ this.summaries.set(summary.id, { ...summary, label: this.sanitizeLabel(summary.label) });
39
43
  this.onDidChangeEmitter.fire();
40
44
  }
41
45
 
@@ -142,7 +142,7 @@ export class ChangeSetImpl implements ChangeSet {
142
142
  const added = [];
143
143
  const modified = [];
144
144
  const removed = [];
145
- const toHandle = new Set(...this._elements.keys());
145
+ const toHandle = new Set(this._elements.keys());
146
146
  for (const element of elements) {
147
147
  toHandle.delete(element.uri.toString());
148
148
  if (this.doAdd(element)) {
@@ -25,6 +25,7 @@ import { ChatAgent } from './chat-agents';
25
25
  import { AgentService } from '@theia/ai-core';
26
26
 
27
27
  export const ChatAgentService = Symbol('ChatAgentService');
28
+ export const ChatAgentServiceFactory = Symbol('ChatAgentServiceFactory');
28
29
  /**
29
30
  * The ChatAgentService provides access to the available chat agents.
30
31
  */
@@ -54,18 +54,19 @@ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inv
54
54
  import { ChatAgentService } from './chat-agent-service';
55
55
  import {
56
56
  ChatModel,
57
- MutableChatRequestModel,
57
+ ChatRequestModel,
58
58
  ChatResponseContent,
59
59
  ErrorChatResponseContentImpl,
60
60
  MarkdownChatResponseContentImpl,
61
- ToolCallChatResponseContentImpl,
62
- ChatRequestModel,
61
+ MutableChatRequestModel,
63
62
  ThinkingChatResponseContentImpl,
63
+ ToolCallChatResponseContentImpl,
64
64
  ErrorChatResponseContent,
65
65
  } from './chat-model';
66
+ import { ChatToolRequest, ChatToolRequestService } from './chat-tool-request-service';
66
67
  import { parseContents } from './parse-contents';
67
68
  import { DefaultResponseContentFactory, ResponseContentMatcher, ResponseContentMatcherProvider } from './response-content-matcher';
68
- import { ChatToolRequest, ChatToolRequestService } from './chat-tool-request-service';
69
+ import { ImageContextVariable } from './image-context-variable';
69
70
 
70
71
  /**
71
72
  * System message content, enriched with function descriptions.
@@ -254,11 +255,27 @@ export abstract class AbstractChatAgent implements ChatAgent {
254
255
  const requestMessages = model.getRequests().flatMap(request => {
255
256
  const messages: LanguageModelMessage[] = [];
256
257
  const text = request.message.parts.map(part => part.promptText).join('');
257
- messages.push({
258
- actor: 'user',
259
- type: 'text',
260
- text: text,
261
- });
258
+ if (text.length > 0) {
259
+ messages.push({
260
+ actor: 'user',
261
+ type: 'text',
262
+ text: text,
263
+ });
264
+ }
265
+ const imageMessages = request.context.variables
266
+ .filter(variable => ImageContextVariable.isResolvedImageContext(variable))
267
+ .map(variable => ImageContextVariable.parseResolved(variable))
268
+ .filter(content => content !== undefined)
269
+ .map(content => ({
270
+ actor: 'user' as const,
271
+ type: 'image' as const,
272
+ image: {
273
+ base64data: content!.data,
274
+ mimeType: content!.mimeType
275
+ }
276
+ }));
277
+ messages.push(...imageMessages);
278
+
262
279
  if (request.response.isComplete || includeResponseInProgress) {
263
280
  const responseMessages: LanguageModelMessage[] = request.response.response.content
264
281
  .filter(c => !ErrorChatResponseContent.is(c))
@@ -19,15 +19,23 @@
19
19
  *--------------------------------------------------------------------------------------------*/
20
20
  // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts
21
21
 
22
- import { AIVariableResolutionRequest, LanguageModelMessage, ResolvedAIContextVariable, TextMessage, ThinkingMessage, ToolResultMessage, ToolUseMessage } from '@theia/ai-core';
22
+ import {
23
+ AIVariableResolutionRequest,
24
+ LanguageModelMessage,
25
+ ResolvedAIContextVariable,
26
+ TextMessage,
27
+ ThinkingMessage,
28
+ ToolResultMessage,
29
+ ToolUseMessage
30
+ } from '@theia/ai-core';
23
31
  import { ArrayUtils, CancellationToken, CancellationTokenSource, Command, Disposable, DisposableCollection, Emitter, Event, generateUuid, URI } from '@theia/core';
24
32
  import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
25
33
  import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
34
+ import { ChangeSet, ChangeSetElement, ChangeSetImpl, ChatUpdateChangeSetEvent } from './change-set';
26
35
  import { ChatAgentLocation } from './chat-agents';
27
36
  import { ParsedChatRequest } from './parsed-chat-request';
28
- import { ChangeSet, ChangeSetImpl, ChangeSetElement, ChatUpdateChangeSetEvent } from './change-set';
29
37
  import debounce = require('@theia/core/shared/lodash.debounce');
30
- export { ChangeSet, ChangeSetImpl, ChangeSetElement };
38
+ export { ChangeSet, ChangeSetElement, ChangeSetImpl };
31
39
 
32
40
  /**********************
33
41
  * INTERFACES AND TYPE GUARDS
@@ -104,6 +104,9 @@ export class ChatRequestParserImpl implements ChatRequestParser {
104
104
  const parts: ParsedChatRequestPart[] = [];
105
105
  const variables = new Map<string, AIVariable>();
106
106
  const toolRequests = new Map<string, ToolRequest>();
107
+ if (!request.text) {
108
+ return { parts, toolRequests, variables };
109
+ }
107
110
  const message = request.text;
108
111
  for (let i = 0; i < message.length; i++) {
109
112
  const previousChar = message.charAt(i - 1);