@theia/ai-chat 1.58.2 → 1.59.0-next.62

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 (103) hide show
  1. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-frontend-module.js +10 -13
  3. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  4. package/lib/browser/ai-chat-preferences.d.ts +1 -0
  5. package/lib/browser/ai-chat-preferences.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-preferences.js +12 -3
  7. package/lib/browser/ai-chat-preferences.js.map +1 -1
  8. package/lib/browser/change-set-file-element.d.ts +24 -6
  9. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  10. package/lib/browser/change-set-file-element.js +105 -15
  11. package/lib/browser/change-set-file-element.js.map +1 -1
  12. package/lib/browser/change-set-file-resource.d.ts +40 -8
  13. package/lib/browser/change-set-file-resource.d.ts.map +1 -1
  14. package/lib/browser/change-set-file-resource.js +123 -37
  15. package/lib/browser/change-set-file-resource.js.map +1 -1
  16. package/lib/browser/change-set-file-service.d.ts +9 -3
  17. package/lib/browser/change-set-file-service.d.ts.map +1 -1
  18. package/lib/browser/change-set-file-service.js +35 -13
  19. package/lib/browser/change-set-file-service.js.map +1 -1
  20. package/lib/browser/context-file-variable-label-provider.d.ts +14 -0
  21. package/lib/browser/context-file-variable-label-provider.d.ts.map +1 -0
  22. package/lib/browser/context-file-variable-label-provider.js +63 -0
  23. package/lib/browser/context-file-variable-label-provider.js.map +1 -0
  24. package/lib/browser/context-variable-label-provider.d.ts +9 -0
  25. package/lib/browser/context-variable-label-provider.d.ts.map +1 -0
  26. package/lib/browser/context-variable-label-provider.js +55 -0
  27. package/lib/browser/context-variable-label-provider.js.map +1 -0
  28. package/lib/browser/file-chat-variable-contribution.d.ts +18 -0
  29. package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -0
  30. package/lib/browser/file-chat-variable-contribution.js +135 -0
  31. package/lib/browser/file-chat-variable-contribution.js.map +1 -0
  32. package/lib/browser/frontend-chat-service.d.ts +10 -3
  33. package/lib/browser/frontend-chat-service.d.ts.map +1 -1
  34. package/lib/browser/frontend-chat-service.js +39 -19
  35. package/lib/browser/frontend-chat-service.js.map +1 -1
  36. package/lib/common/chat-agents.d.ts +47 -24
  37. package/lib/common/chat-agents.d.ts.map +1 -1
  38. package/lib/common/chat-agents.js +50 -19
  39. package/lib/common/chat-agents.js.map +1 -1
  40. package/lib/common/chat-history-entry.js +1 -1
  41. package/lib/common/chat-history-entry.js.map +1 -1
  42. package/lib/common/chat-model.d.ts +58 -35
  43. package/lib/common/chat-model.d.ts.map +1 -1
  44. package/lib/common/chat-model.js +96 -53
  45. package/lib/common/chat-model.js.map +1 -1
  46. package/lib/common/chat-service.d.ts +32 -12
  47. package/lib/common/chat-service.d.ts.map +1 -1
  48. package/lib/common/chat-service.js +77 -19
  49. package/lib/common/chat-service.js.map +1 -1
  50. package/lib/common/chat-tool-request-service.d.ts +5 -5
  51. package/lib/common/chat-tool-request-service.d.ts.map +1 -1
  52. package/lib/common/chat-tool-request-service.js.map +1 -1
  53. package/lib/common/custom-chat-agent.d.ts +7 -10
  54. package/lib/common/custom-chat-agent.d.ts.map +1 -1
  55. package/lib/common/custom-chat-agent.js +7 -11
  56. package/lib/common/custom-chat-agent.js.map +1 -1
  57. package/lib/common/index.d.ts +0 -3
  58. package/lib/common/index.d.ts.map +1 -1
  59. package/lib/common/index.js +0 -3
  60. package/lib/common/index.js.map +1 -1
  61. package/lib/common/parse-contents.d.ts +2 -2
  62. package/lib/common/parse-contents.d.ts.map +1 -1
  63. package/lib/common/parse-contents.js.map +1 -1
  64. package/lib/common/parse-contents.spec.d.ts.map +1 -1
  65. package/lib/common/parse-contents.spec.js.map +1 -1
  66. package/lib/common/response-content-matcher.d.ts +3 -3
  67. package/lib/common/response-content-matcher.d.ts.map +1 -1
  68. package/lib/common/response-content-matcher.js.map +1 -1
  69. package/package.json +12 -10
  70. package/src/browser/ai-chat-frontend-module.ts +14 -18
  71. package/src/browser/ai-chat-preferences.ts +13 -2
  72. package/src/browser/change-set-file-element.ts +99 -20
  73. package/src/browser/change-set-file-resource.ts +125 -39
  74. package/src/browser/change-set-file-service.ts +38 -16
  75. package/src/browser/context-file-variable-label-provider.ts +62 -0
  76. package/src/browser/context-variable-label-provider.ts +56 -0
  77. package/src/browser/file-chat-variable-contribution.ts +143 -0
  78. package/src/browser/frontend-chat-service.ts +40 -26
  79. package/src/common/chat-agents.ts +72 -27
  80. package/src/common/chat-history-entry.ts +1 -1
  81. package/src/common/chat-model.ts +138 -74
  82. package/src/common/chat-service.ts +96 -23
  83. package/src/common/chat-tool-request-service.ts +5 -5
  84. package/src/common/custom-chat-agent.ts +8 -20
  85. package/src/common/index.ts +0 -3
  86. package/src/common/parse-contents.spec.ts +2 -2
  87. package/src/common/parse-contents.ts +2 -2
  88. package/src/common/response-content-matcher.ts +3 -3
  89. package/lib/common/command-chat-agents.d.ts +0 -33
  90. package/lib/common/command-chat-agents.d.ts.map +0 -1
  91. package/lib/common/command-chat-agents.js +0 -329
  92. package/lib/common/command-chat-agents.js.map +0 -1
  93. package/lib/common/orchestrator-chat-agent.d.ts +0 -22
  94. package/lib/common/orchestrator-chat-agent.d.ts.map +0 -1
  95. package/lib/common/orchestrator-chat-agent.js +0 -167
  96. package/lib/common/orchestrator-chat-agent.js.map +0 -1
  97. package/lib/common/universal-chat-agent.d.ts +0 -16
  98. package/lib/common/universal-chat-agent.d.ts.map +0 -1
  99. package/lib/common/universal-chat-agent.js +0 -109
  100. package/lib/common/universal-chat-agent.js.map +0 -1
  101. package/src/common/command-chat-agents.ts +0 -354
  102. package/src/common/orchestrator-chat-agent.ts +0 -179
  103. package/src/common/universal-chat-agent.ts +0 -117
@@ -14,8 +14,8 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { ILogger, UNTITLED_SCHEME, URI } from '@theia/core';
18
- import { DiffUris, LabelProvider, OpenerService, open } from '@theia/core/lib/browser';
17
+ import { ILogger, URI } from '@theia/core';
18
+ import { ApplicationShell, DiffUris, LabelProvider, NavigatableWidget, OpenerService, open } from '@theia/core/lib/browser';
19
19
  import { inject, injectable } from '@theia/core/shared/inversify';
20
20
  import { EditorManager } from '@theia/editor/lib/browser';
21
21
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
@@ -40,6 +40,9 @@ export class ChangeSetFileService {
40
40
  @inject(EditorManager)
41
41
  protected readonly editorManager: EditorManager;
42
42
 
43
+ @inject(ApplicationShell)
44
+ protected readonly shell: ApplicationShell;
45
+
43
46
  @inject(MonacoWorkspace)
44
47
  protected readonly monacoWorkspace: MonacoWorkspace;
45
48
 
@@ -95,15 +98,14 @@ export class ChangeSetFileService {
95
98
  }
96
99
 
97
100
  async openDiff(originalUri: URI, suggestedUri: URI): Promise<void> {
98
- const exists = await this.fileService.exists(originalUri);
99
- const openedUri = exists ? originalUri : originalUri.withScheme(UNTITLED_SCHEME);
100
- // Currently we don't have a great way to show the suggestions in a diff editor with accept/reject buttons
101
- // So we just use plain diffs with the suggestions as original and the current state as modified, so users can apply changes in their current state
102
- // But this leads to wrong colors and wrong label (revert change instead of accept change)
103
- const diffUri = DiffUris.encode(openedUri, suggestedUri,
101
+ const diffUri = this.getDiffUri(originalUri, suggestedUri);
102
+ open(this.openerService, diffUri);
103
+ }
104
+
105
+ protected getDiffUri(originalUri: URI, suggestedUri: URI): URI {
106
+ return DiffUris.encode(originalUri, suggestedUri,
104
107
  `AI Changes: ${this.labelProvider.getName(originalUri)}`,
105
108
  );
106
- open(this.openerService, diffUri);
107
109
  }
108
110
 
109
111
  async delete(uri: URI): Promise<void> {
@@ -113,24 +115,44 @@ export class ChangeSetFileService {
113
115
  }
114
116
  }
115
117
 
116
- async write(uri: URI, targetState: string): Promise<void> {
117
- const exists = await this.fileService.exists(uri);
118
- if (!exists) {
119
- await this.fileService.create(uri, targetState);
118
+ /** Returns true if there was a document available to save for the specified URI. */
119
+ async trySave(suggestedUri: URI): Promise<boolean> {
120
+ const openModel = this.monacoWorkspace.getTextDocument(suggestedUri.toString());
121
+ if (openModel) {
122
+ await openModel.save();
123
+ return true;
124
+ } else {
125
+ return false;
120
126
  }
121
- await this.doWrite(uri, targetState);
122
127
  }
123
128
 
124
- protected async doWrite(uri: URI, text: string): Promise<void> {
129
+ async writeFrom(from: URI, to: URI, fallbackContent: string): Promise<void> {
130
+ const authoritativeContent = this.monacoWorkspace.getTextDocument(from.toString())?.getText() ?? fallbackContent;
131
+ await this.write(to, authoritativeContent);
132
+ }
133
+
134
+ async write(uri: URI, text: string): Promise<void> {
125
135
  const document = this.monacoWorkspace.getTextDocument(uri.toString());
126
136
  if (document) {
127
137
  await this.monacoWorkspace.applyBackgroundEdit(document, [{
128
138
  range: document.textEditorModel.getFullModelRange(),
129
139
  text
130
- }], (editor, wasDirty) => editor === undefined || !wasDirty);
140
+ }], () => true);
131
141
  } else {
132
142
  await this.fileService.write(uri, text);
133
143
  }
134
144
  }
135
145
 
146
+ closeDiffsForSession(sessionId: string, except?: URI[]): void {
147
+ const openEditors = this.shell.widgets.filter(widget => {
148
+ const uri = NavigatableWidget.getUri(widget);
149
+ return uri && uri.authority === sessionId && !except?.some(candidate => candidate.path.toString() === uri.path.toString());
150
+ });
151
+ openEditors.forEach(editor => editor.close());
152
+ }
153
+
154
+ closeDiff(uri: URI): void {
155
+ const openEditors = this.shell.widgets.filter(widget => NavigatableWidget.getUri(widget)?.isEqual(uri));
156
+ openEditors.forEach(editor => editor.close());
157
+ }
136
158
  }
@@ -0,0 +1,62 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
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 { AIVariableResolutionRequest } from '@theia/ai-core';
18
+ import { URI } from '@theia/core';
19
+ import { inject, injectable } from '@theia/core/shared/inversify';
20
+ import { LabelProvider, LabelProviderContribution } from '@theia/core/lib/browser';
21
+ import { ChangeSetFileService } from './change-set-file-service';
22
+
23
+ @injectable()
24
+ export class ContextFileVariableLabelProvider implements LabelProviderContribution {
25
+
26
+ @inject(LabelProvider)
27
+ protected readonly labelProvider: LabelProvider;
28
+
29
+ @inject(ChangeSetFileService)
30
+ protected readonly changeSetFileService: ChangeSetFileService;
31
+
32
+ canHandle(element: object): number {
33
+ if (AIVariableResolutionRequest.is(element) && element.variable.name === 'file') {
34
+ return 10;
35
+ }
36
+ return -1;
37
+ }
38
+
39
+ getIcon(element: object): string | undefined {
40
+ return this.labelProvider.getIcon(this.getUri(element)!);
41
+ }
42
+
43
+ getName(element: object): string | undefined {
44
+ return this.labelProvider.getName(this.getUri(element)!);
45
+ }
46
+
47
+ getLongName(element: object): string | undefined {
48
+ return this.labelProvider.getLongName(this.getUri(element)!);
49
+ }
50
+
51
+ getDetails(element: object): string | undefined {
52
+ return this.changeSetFileService.getAdditionalInfo(this.getUri(element)!);
53
+ }
54
+
55
+ protected getUri(element: object): URI | undefined {
56
+ if (!AIVariableResolutionRequest.is(element)) {
57
+ return undefined;
58
+ }
59
+ return new URI(element.arg);
60
+ }
61
+
62
+ }
@@ -0,0 +1,56 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
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 { AIVariableResolutionRequest } from '@theia/ai-core';
18
+ import { LabelProviderContribution } from '@theia/core/lib/browser';
19
+ import { injectable } from '@theia/core/shared/inversify';
20
+
21
+ @injectable()
22
+ export class ContextVariableLabelProvider implements LabelProviderContribution {
23
+
24
+ canHandle(element: object): number {
25
+ if (AIVariableResolutionRequest.is(element)) {
26
+ return 1;
27
+ }
28
+ return -1;
29
+ }
30
+
31
+ getIcon(element: object): string | undefined {
32
+ return 'codicon codicon-variable';
33
+ }
34
+
35
+ getName(element: object): string | undefined {
36
+ if (!AIVariableResolutionRequest.is(element)) {
37
+ return undefined;
38
+ }
39
+ return element.variable.name;
40
+ }
41
+
42
+ getLongName(element: object): string | undefined {
43
+ if (!AIVariableResolutionRequest.is(element)) {
44
+ return undefined;
45
+ }
46
+ return element.variable.name + (element.arg ? ':' + element.arg : '');
47
+ }
48
+
49
+ getDetails(element: object): string | undefined {
50
+ if (!AIVariableResolutionRequest.is(element)) {
51
+ return undefined;
52
+ }
53
+ return element.arg;
54
+ }
55
+
56
+ }
@@ -0,0 +1,143 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
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 { AIVariableContext, AIVariableResolutionRequest, PromptText } from '@theia/ai-core';
18
+ import { AIVariableDropResult, FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
19
+ import { FILE_VARIABLE } from '@theia/ai-core/lib/browser/file-variable-contribution';
20
+ import { CancellationToken, QuickInputService, URI } from '@theia/core';
21
+ import { inject, injectable } from '@theia/core/shared/inversify';
22
+ import * as monaco from '@theia/monaco-editor-core';
23
+ import { FileQuickPickItem, QuickFileSelectService } from '@theia/file-search/lib/browser/quick-file-select-service';
24
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
25
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
26
+
27
+ @injectable()
28
+ export class FileChatVariableContribution implements FrontendVariableContribution {
29
+ @inject(FileService)
30
+ protected readonly fileService: FileService;
31
+
32
+ @inject(WorkspaceService)
33
+ protected readonly wsService: WorkspaceService;
34
+
35
+ @inject(QuickInputService)
36
+ protected readonly quickInputService: QuickInputService;
37
+
38
+ @inject(QuickFileSelectService)
39
+ protected readonly quickFileSelectService: QuickFileSelectService;
40
+
41
+ registerVariables(service: FrontendVariableService): void {
42
+ service.registerArgumentPicker(FILE_VARIABLE, this.triggerArgumentPicker.bind(this));
43
+ service.registerArgumentCompletionProvider(FILE_VARIABLE, this.provideArgumentCompletionItems.bind(this));
44
+ service.registerDropHandler(this.handleDrop.bind(this));
45
+ }
46
+
47
+ protected async triggerArgumentPicker(): Promise<string | undefined> {
48
+ const quickPick = this.quickInputService.createQuickPick();
49
+ quickPick.items = await this.quickFileSelectService.getPicks();
50
+
51
+ const updateItems = async (value: string) => {
52
+ quickPick.items = await this.quickFileSelectService.getPicks(value, CancellationToken.None);
53
+ };
54
+
55
+ const onChangeListener = quickPick.onDidChangeValue(updateItems);
56
+ quickPick.show();
57
+
58
+ return new Promise(resolve => {
59
+ quickPick.onDispose(onChangeListener.dispose);
60
+ quickPick.onDidAccept(async () => {
61
+ const selectedItem = quickPick.selectedItems[0];
62
+ if (selectedItem && FileQuickPickItem.is(selectedItem)) {
63
+ quickPick.dispose();
64
+ resolve(await this.wsService.getWorkspaceRelativePath(selectedItem.uri));
65
+ }
66
+ });
67
+ });
68
+ }
69
+
70
+ protected async provideArgumentCompletionItems(
71
+ model: monaco.editor.ITextModel,
72
+ position: monaco.Position
73
+ ): Promise<monaco.languages.CompletionItem[] | undefined> {
74
+ const lineContent = model.getLineContent(position.lineNumber);
75
+ const indexOfVariableTrigger = lineContent.lastIndexOf(PromptText.VARIABLE_CHAR, position.column - 1);
76
+
77
+ // check if there is a variable trigger and no space typed between the variable trigger and the cursor
78
+ if (indexOfVariableTrigger === -1 || lineContent.substring(indexOfVariableTrigger).includes(' ')) {
79
+ return undefined;
80
+ }
81
+
82
+ // determine whether we are providing completions before or after the variable argument separator
83
+ const indexOfVariableArgSeparator = lineContent.lastIndexOf(PromptText.VARIABLE_SEPARATOR_CHAR, position.column - 1);
84
+ const triggerCharIndex = Math.max(indexOfVariableTrigger, indexOfVariableArgSeparator);
85
+
86
+ const typedWord = lineContent.substring(triggerCharIndex + 1, position.column - 1);
87
+ const range = new monaco.Range(position.lineNumber, triggerCharIndex + 2, position.lineNumber, position.column);
88
+ const picks = await this.quickFileSelectService.getPicks(typedWord, CancellationToken.None);
89
+ const prefix = lineContent[triggerCharIndex] === PromptText.VARIABLE_CHAR ? FILE_VARIABLE.name + PromptText.VARIABLE_SEPARATOR_CHAR : '';
90
+
91
+ return Promise.all(
92
+ picks
93
+ .filter(FileQuickPickItem.is)
94
+ // only show files with highlights, if the user started typing to filter down the results
95
+ .filter(p => !typedWord || p.highlights?.label)
96
+ .map(async (pick, index) => ({
97
+ label: pick.label,
98
+ kind: monaco.languages.CompletionItemKind.File,
99
+ range,
100
+ insertText: `${prefix}${await this.wsService.getWorkspaceRelativePath(pick.uri)}`,
101
+ detail: await this.wsService.getWorkspaceRelativePath(pick.uri.parent),
102
+ // don't let monaco filter the items, as we only return picks that are filtered
103
+ filterText: typedWord,
104
+ // keep the order of the items, but move them to the end of the list
105
+ sortText: `ZZ${index.toString().padStart(4, '0')}_${pick.label}`,
106
+ }))
107
+ );
108
+ }
109
+
110
+ protected async handleDrop(event: DragEvent, _: AIVariableContext): Promise<AIVariableDropResult | undefined> {
111
+ const data = event.dataTransfer?.getData('selected-tree-nodes');
112
+ if (!data) {
113
+ return undefined;
114
+ }
115
+
116
+ try {
117
+ const nodes: string[] = JSON.parse(data);
118
+ const variables: AIVariableResolutionRequest[] = [];
119
+ const texts: string[] = [];
120
+
121
+ for (const node of nodes) {
122
+ const [, filePath] = node.split(':');
123
+ if (!filePath) {
124
+ continue;
125
+ }
126
+
127
+ const uri = URI.fromFilePath(filePath);
128
+ if (await this.fileService.exists(uri)) {
129
+ const wsRelativePath = await this.wsService.getWorkspaceRelativePath(uri);
130
+ variables.push({
131
+ variable: FILE_VARIABLE,
132
+ arg: wsRelativePath
133
+ });
134
+ texts.push(`${PromptText.VARIABLE_CHAR}${FILE_VARIABLE.name}${PromptText.VARIABLE_SEPARATOR_CHAR}${wsRelativePath}`);
135
+ }
136
+ }
137
+
138
+ return { variables, text: texts.length ? texts.join(' ') : undefined };
139
+ } catch {
140
+ return undefined;
141
+ }
142
+ }
143
+ }
@@ -15,44 +15,45 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { inject, injectable } from '@theia/core/shared/inversify';
18
- import { ChatAgent, ChatServiceImpl, ParsedChatRequest } from '../common';
18
+ import { ChangeSet, ChatAgent, ChatAgentLocation, ChatServiceImpl, ChatSession, ParsedChatRequest, SessionOptions } from '../common';
19
19
  import { PreferenceService } from '@theia/core/lib/browser';
20
- import { DEFAULT_CHAT_AGENT_PREF } from './ai-chat-preferences';
20
+ import { DEFAULT_CHAT_AGENT_PREF, PIN_CHAT_AGENT_PREF } from './ai-chat-preferences';
21
+ import { ChangeSetFileService } from './change-set-file-service';
21
22
 
23
+ /**
24
+ * Customizes the ChatServiceImpl to consider preference based default chat agent
25
+ */
22
26
  @injectable()
23
27
  export class FrontendChatServiceImpl extends ChatServiceImpl {
24
28
 
25
29
  @inject(PreferenceService)
26
- protected preferenceService: PreferenceService;
30
+ protected readonly preferenceService: PreferenceService;
27
31
 
28
- protected override getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined {
29
- const agentPart = this.getMentionedAgent(parsedRequest);
30
- if (agentPart) {
31
- return this.chatAgentService.getAgent(agentPart.agentId);
32
- }
32
+ @inject(ChangeSetFileService)
33
+ protected readonly changeSetFileService: ChangeSetFileService;
33
34
 
34
- const configuredDefaultChatAgent = this.getConfiguredDefaultChatAgent();
35
- if (configuredDefaultChatAgent) {
36
- return configuredDefaultChatAgent;
35
+ protected override getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined {
36
+ let agent = this.initialAgentSelection(parsedRequest);
37
+ if (!this.preferenceService.get<boolean>(PIN_CHAT_AGENT_PREF)) {
38
+ return agent;
37
39
  }
38
-
39
- if (this.defaultChatAgentId) {
40
- const defaultAgent = this.chatAgentService.getAgent(this.defaultChatAgentId.id);
41
- // the default agent could be disabled
42
- if (defaultAgent) {
43
- return defaultAgent;
44
- }
40
+ if (!session.pinnedAgent && agent && agent.id !== this.defaultChatAgentId?.id) {
41
+ session.pinnedAgent = agent;
42
+ } else if (session.pinnedAgent && this.getMentionedAgent(parsedRequest) === undefined) {
43
+ agent = session.pinnedAgent;
45
44
  }
45
+ return agent;
46
+ }
46
47
 
47
- // check whether "Universal" is available
48
- const universalAgent = this.chatAgentService.getAgent('Universal');
49
- if (universalAgent) {
50
- return universalAgent;
48
+ protected override initialAgentSelection(parsedRequest: ParsedChatRequest): ChatAgent | undefined {
49
+ const agentPart = this.getMentionedAgent(parsedRequest);
50
+ if (!agentPart) {
51
+ const configuredDefaultChatAgent = this.getConfiguredDefaultChatAgent();
52
+ if (configuredDefaultChatAgent) {
53
+ return configuredDefaultChatAgent;
54
+ }
51
55
  }
52
-
53
- this.logger.warn('No default chat agent is configured or available and the "Universal" Chat Agent is unavailable too. Falling back to first registered agent.');
54
-
55
- return this.chatAgentService.getAgents()[0] ?? undefined;
56
+ return super.initialAgentSelection(parsedRequest);
56
57
  }
57
58
 
58
59
  protected getConfiguredDefaultChatAgent(): ChatAgent | undefined {
@@ -63,4 +64,17 @@ export class FrontendChatServiceImpl extends ChatServiceImpl {
63
64
  }
64
65
  return configuredDefaultChatAgent;
65
66
  }
67
+
68
+ override createSession(location?: ChatAgentLocation, options?: SessionOptions): ChatSession {
69
+ const session = super.createSession(location, options);
70
+ session.model.onDidChange(event => {
71
+ const changeSet = (event as { changeSet?: ChangeSet }).changeSet;
72
+ if (event.kind === 'removeChangeSet') {
73
+ this.changeSetFileService.closeDiffsForSession(session.id);
74
+ } else if (changeSet) {
75
+ this.changeSetFileService.closeDiffsForSession(session.id, changeSet.getElements().map(({ uri }) => uri));
76
+ }
77
+ });
78
+ return session;
79
+ }
66
80
  }
@@ -20,6 +20,7 @@
20
20
  // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatAgents.ts
21
21
 
22
22
  import {
23
+ AgentSpecificVariables,
23
24
  CommunicationRecordingService,
24
25
  getTextOfResponse,
25
26
  LanguageModel,
@@ -27,7 +28,9 @@ import {
27
28
  LanguageModelResponse,
28
29
  LanguageModelStreamResponse,
29
30
  PromptService,
31
+ PromptTemplate,
30
32
  ResolvedPromptTemplate,
33
+ ToolCall,
31
34
  ToolRequest,
32
35
  } from '@theia/ai-core';
33
36
  import {
@@ -39,11 +42,11 @@ import {
39
42
  MessageActor,
40
43
  } from '@theia/ai-core/lib/common';
41
44
  import { CancellationToken, ContributionProvider, ILogger, isArray } from '@theia/core';
42
- import { inject, injectable, named, postConstruct, unmanaged } from '@theia/core/shared/inversify';
45
+ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
43
46
  import { ChatAgentService } from './chat-agent-service';
44
47
  import {
45
48
  ChatModel,
46
- ChatRequestModelImpl,
49
+ MutableChatRequestModel,
47
50
  ChatResponseContent,
48
51
  ErrorChatResponseContentImpl,
49
52
  MarkdownChatResponseContentImpl,
@@ -115,11 +118,11 @@ export const ChatAgent = Symbol('ChatAgent');
115
118
  export interface ChatAgent extends Agent {
116
119
  locations: ChatAgentLocation[];
117
120
  iconClass?: string;
118
- invoke(request: ChatRequestModelImpl, chatAgentService?: ChatAgentService): Promise<void>;
121
+ invoke(request: MutableChatRequestModel, chatAgentService?: ChatAgentService): Promise<void>;
119
122
  }
120
123
 
121
124
  @injectable()
122
- export abstract class AbstractChatAgent {
125
+ export abstract class AbstractChatAgent implements ChatAgent {
123
126
  @inject(LanguageModelRegistry) protected languageModelRegistry: LanguageModelRegistry;
124
127
  @inject(ILogger) protected logger: ILogger;
125
128
  @inject(CommunicationRecordingService) protected recordingService: CommunicationRecordingService;
@@ -128,21 +131,26 @@ export abstract class AbstractChatAgent {
128
131
 
129
132
  @inject(ContributionProvider) @named(ResponseContentMatcherProvider)
130
133
  protected contentMatcherProviders: ContributionProvider<ResponseContentMatcherProvider>;
131
- protected additionalToolRequests: ToolRequest[] = [];
132
- protected contentMatchers: ResponseContentMatcher[] = [];
133
134
 
134
135
  @inject(DefaultResponseContentFactory)
135
136
  protected defaultContentFactory: DefaultResponseContentFactory;
136
137
 
137
- constructor(
138
- @unmanaged() public id: string,
139
- @unmanaged() public languageModelRequirements: LanguageModelRequirement[],
140
- @unmanaged() protected defaultLanguageModelPurpose: string,
141
- @unmanaged() public iconClass: string = 'codicon codicon-copilot',
142
- @unmanaged() public locations: ChatAgentLocation[] = ChatAgentLocation.ALL,
143
- @unmanaged() public tags: string[] = ['Chat'],
144
- @unmanaged() public defaultLogging: boolean = true) {
145
- }
138
+ readonly abstract id: string;
139
+ readonly abstract name: string;
140
+ readonly abstract languageModelRequirements: LanguageModelRequirement[];
141
+ iconClass: string = 'codicon codicon-copilot';
142
+ locations: ChatAgentLocation[] = ChatAgentLocation.ALL;
143
+ tags: string[] = ['Chat'];
144
+ description: string = '';
145
+ variables: string[] = [];
146
+ promptTemplates: PromptTemplate[] = [];
147
+ agentSpecificVariables: AgentSpecificVariables[] = [];
148
+ functions: string[] = [];
149
+ protected readonly abstract defaultLanguageModelPurpose: string;
150
+ protected defaultLogging: boolean = true;
151
+ protected systemPromptId: string | undefined = undefined;
152
+ protected additionalToolRequests: ToolRequest[] = [];
153
+ protected contentMatchers: ResponseContentMatcher[] = [];
146
154
 
147
155
  @postConstruct()
148
156
  init(): void {
@@ -154,7 +162,7 @@ export abstract class AbstractChatAgent {
154
162
  this.contentMatchers.push(...contributedContentMatchers);
155
163
  }
156
164
 
157
- async invoke(request: ChatRequestModelImpl): Promise<void> {
165
+ async invoke(request: MutableChatRequestModel): Promise<void> {
158
166
  try {
159
167
  const languageModel = await this.getLanguageModel(this.defaultLanguageModelPurpose);
160
168
  if (!languageModel) {
@@ -206,7 +214,7 @@ export abstract class AbstractChatAgent {
206
214
  }
207
215
  }
208
216
 
209
- protected parseContents(text: string, request: ChatRequestModelImpl): ChatResponseContent[] {
217
+ protected parseContents(text: string, request: MutableChatRequestModel): ChatResponseContent[] {
210
218
  return parseContents(
211
219
  text,
212
220
  request,
@@ -215,7 +223,7 @@ export abstract class AbstractChatAgent {
215
223
  );
216
224
  };
217
225
 
218
- protected handleError(request: ChatRequestModelImpl, error: Error): void {
226
+ protected handleError(request: MutableChatRequestModel, error: Error): void {
219
227
  request.response.response.addContent(new ErrorChatResponseContentImpl(error));
220
228
  request.response.error(error);
221
229
  }
@@ -236,7 +244,13 @@ export abstract class AbstractChatAgent {
236
244
  return languageModel;
237
245
  }
238
246
 
239
- protected abstract getSystemMessageDescription(): Promise<SystemMessageDescription | undefined>;
247
+ protected async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
248
+ if (this.systemPromptId === undefined) {
249
+ return undefined;
250
+ }
251
+ const resolvedPrompt = await this.promptService.getPrompt(this.systemPromptId);
252
+ return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptTemplate(resolvedPrompt) : undefined;
253
+ }
240
254
 
241
255
  protected async getMessages(
242
256
  model: ChatModel, includeResponseInProgress = false
@@ -290,17 +304,17 @@ export abstract class AbstractChatAgent {
290
304
  * The default implementation sets the state of the response to `complete`.
291
305
  * Subclasses may override this method to perform additional actions or keep the response open for processing further requests.
292
306
  */
293
- protected async onResponseComplete(request: ChatRequestModelImpl): Promise<void> {
307
+ protected async onResponseComplete(request: MutableChatRequestModel): Promise<void> {
294
308
  return request.response.complete();
295
309
  }
296
310
 
297
- protected abstract addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void>;
311
+ protected abstract addContentsToResponse(languageModelResponse: LanguageModelResponse, request: MutableChatRequestModel): Promise<void>;
298
312
  }
299
313
 
300
314
  @injectable()
301
315
  export abstract class AbstractTextToModelParsingChatAgent<T> extends AbstractChatAgent {
302
316
 
303
- protected async addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void> {
317
+ protected async addContentsToResponse(languageModelResponse: LanguageModelResponse, request: MutableChatRequestModel): Promise<void> {
304
318
  const responseAsText = await getTextOfResponse(languageModelResponse);
305
319
  const parsedCommand = await this.parseTextResponse(responseAsText);
306
320
  const content = this.createResponseContent(parsedCommand, request);
@@ -309,13 +323,31 @@ export abstract class AbstractTextToModelParsingChatAgent<T> extends AbstractCha
309
323
 
310
324
  protected abstract parseTextResponse(text: string): Promise<T>;
311
325
 
312
- protected abstract createResponseContent(parsedModel: T, request: ChatRequestModelImpl): ChatResponseContent;
326
+ protected abstract createResponseContent(parsedModel: T, request: MutableChatRequestModel): ChatResponseContent;
327
+ }
328
+
329
+ /**
330
+ * Factory for creating ToolCallChatResponseContent instances.
331
+ */
332
+ @injectable()
333
+ export class ToolCallChatResponseContentFactory {
334
+ create(toolCall: ToolCall): ChatResponseContent {
335
+ return new ToolCallChatResponseContentImpl(
336
+ toolCall.id,
337
+ toolCall.function?.name,
338
+ toolCall.function?.arguments,
339
+ toolCall.finished,
340
+ toolCall.result
341
+ );
342
+ }
313
343
  }
314
344
 
315
345
  @injectable()
316
346
  export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
347
+ @inject(ToolCallChatResponseContentFactory)
348
+ protected toolCallResponseContentFactory: ToolCallChatResponseContentFactory;
317
349
 
318
- protected override async addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void> {
350
+ protected override async addContentsToResponse(languageModelResponse: LanguageModelResponse, request: MutableChatRequestModel): Promise<void> {
319
351
  if (isLanguageModelTextResponse(languageModelResponse)) {
320
352
  const contents = this.parseContents(languageModelResponse.text, request);
321
353
  request.response.response.addContents(contents);
@@ -335,7 +367,7 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
335
367
  );
336
368
  }
337
369
 
338
- protected async addStreamResponse(languageModelResponse: LanguageModelStreamResponse, request: ChatRequestModelImpl): Promise<void> {
370
+ protected async addStreamResponse(languageModelResponse: LanguageModelStreamResponse, request: MutableChatRequestModel): Promise<void> {
339
371
  for await (const token of languageModelResponse.stream) {
340
372
  const newContents = this.parse(token, request);
341
373
  if (isArray(newContents)) {
@@ -362,7 +394,7 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
362
394
  }
363
395
  }
364
396
 
365
- protected parse(token: LanguageModelStreamResponsePart, request: ChatRequestModelImpl): ChatResponseContent | ChatResponseContent[] {
397
+ protected parse(token: LanguageModelStreamResponsePart, request: MutableChatRequestModel): ChatResponseContent | ChatResponseContent[] {
366
398
  const content = token.content;
367
399
  // eslint-disable-next-line no-null/no-null
368
400
  if (content !== undefined && content !== null) {
@@ -371,10 +403,23 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
371
403
  const toolCalls = token.tool_calls;
372
404
  if (toolCalls !== undefined) {
373
405
  const toolCallContents = toolCalls.map(toolCall =>
374
- new ToolCallChatResponseContentImpl(toolCall.id, toolCall.function?.name, toolCall.function?.arguments, toolCall.finished, toolCall.result));
406
+ this.createToolCallResponseContent(toolCall)
407
+ );
375
408
  return toolCallContents;
376
409
  }
377
410
  return this.defaultContentFactory.create('', request);
378
411
  }
379
412
 
413
+ /**
414
+ * Creates a ToolCallChatResponseContent instance from the provided tool call data.
415
+ *
416
+ * This method is called when parsing stream response tokens that contain tool call data.
417
+ * Subclasses can override this method to customize the creation of tool call response contents.
418
+ *
419
+ * @param toolCall The ToolCall.
420
+ * @returns A ChatResponseContent representing the tool call.
421
+ */
422
+ protected createToolCallResponseContent(toolCall: ToolCall): ChatResponseContent {
423
+ return this.toolCallResponseContentFactory.create(toolCall);
424
+ }
380
425
  }