@theia/ai-chat 1.60.2 → 1.61.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.
- package/lib/browser/ai-chat-frontend-contribution.d.ts +11 -0
- package/lib/browser/ai-chat-frontend-contribution.d.ts.map +1 -0
- package/lib/browser/ai-chat-frontend-contribution.js +56 -0
- package/lib/browser/ai-chat-frontend-contribution.js.map +1 -0
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +21 -3
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-decorator-service.d.ts +24 -0
- package/lib/browser/change-set-decorator-service.d.ts.map +1 -0
- package/lib/browser/change-set-decorator-service.js +66 -0
- package/lib/browser/change-set-decorator-service.js.map +1 -0
- package/lib/browser/change-set-file-element.d.ts +7 -4
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +20 -12
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/change-set-file-resource.d.ts +1 -42
- package/lib/browser/change-set-file-resource.d.ts.map +1 -1
- package/lib/browser/change-set-file-resource.js +1 -136
- package/lib/browser/change-set-file-resource.js.map +1 -1
- package/lib/browser/change-set-variable.d.ts.map +1 -1
- package/lib/browser/change-set-variable.js +13 -4
- package/lib/browser/change-set-variable.js.map +1 -1
- package/lib/browser/context-file-variable-label-provider.js +1 -1
- package/lib/browser/context-file-variable-label-provider.js.map +1 -1
- package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-chat-variable-contribution.js +29 -27
- package/lib/browser/file-chat-variable-contribution.js.map +1 -1
- package/lib/browser/task-context-service.d.ts +40 -0
- package/lib/browser/task-context-service.d.ts.map +1 -0
- package/lib/browser/task-context-service.js +148 -0
- package/lib/browser/task-context-service.js.map +1 -0
- package/lib/browser/task-context-storage-service.d.ts +18 -0
- package/lib/browser/task-context-storage-service.d.ts.map +1 -0
- package/lib/browser/task-context-storage-service.js +77 -0
- package/lib/browser/task-context-storage-service.js.map +1 -0
- package/lib/browser/task-context-variable-contribution.d.ts +20 -0
- package/lib/browser/task-context-variable-contribution.d.ts.map +1 -0
- package/lib/browser/task-context-variable-contribution.js +101 -0
- package/lib/browser/task-context-variable-contribution.js.map +1 -0
- package/lib/browser/task-context-variable-label-provider.d.ts +21 -0
- package/lib/browser/task-context-variable-label-provider.d.ts.map +1 -0
- package/lib/browser/task-context-variable-label-provider.js +83 -0
- package/lib/browser/task-context-variable-label-provider.js.map +1 -0
- package/lib/browser/task-context-variable.d.ts +3 -0
- package/lib/browser/task-context-variable.d.ts.map +1 -0
- package/lib/browser/task-context-variable.js +29 -0
- package/lib/browser/task-context-variable.js.map +1 -0
- package/lib/common/chat-agents.d.ts +2 -1
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +32 -20
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-model.d.ts +191 -8
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +369 -12
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-request-parser.d.ts +1 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +1 -1
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-service.d.ts +2 -0
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +18 -25
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-session-naming-service.d.ts.map +1 -1
- package/lib/common/chat-session-naming-service.js +9 -11
- package/lib/common/chat-session-naming-service.js.map +1 -1
- package/lib/common/chat-session-summary-agent-prompt.d.ts +5 -0
- package/lib/common/chat-session-summary-agent-prompt.d.ts.map +1 -0
- package/lib/common/chat-session-summary-agent-prompt.js +30 -0
- package/lib/common/chat-session-summary-agent-prompt.js.map +1 -0
- package/lib/common/chat-session-summary-agent.d.ts +17 -0
- package/lib/common/chat-session-summary-agent.d.ts.map +1 -0
- package/lib/common/chat-session-summary-agent.js +48 -0
- package/lib/common/chat-session-summary-agent.js.map +1 -0
- package/lib/common/context-summary-variable.js +1 -1
- package/lib/common/context-summary-variable.js.map +1 -1
- package/lib/common/parse-contents-with-incomplete-parts.spec.d.ts +2 -0
- package/lib/common/parse-contents-with-incomplete-parts.spec.d.ts.map +1 -0
- package/lib/common/parse-contents-with-incomplete-parts.spec.js +103 -0
- package/lib/common/parse-contents-with-incomplete-parts.spec.js.map +1 -0
- package/lib/common/parse-contents.d.ts +1 -0
- package/lib/common/parse-contents.d.ts.map +1 -1
- package/lib/common/parse-contents.js +45 -5
- package/lib/common/parse-contents.js.map +1 -1
- package/lib/common/parse-contents.spec.d.ts +1 -0
- package/lib/common/parse-contents.spec.d.ts.map +1 -1
- package/lib/common/parse-contents.spec.js +25 -13
- package/lib/common/parse-contents.spec.js.map +1 -1
- package/lib/common/response-content-matcher.d.ts +6 -0
- package/lib/common/response-content-matcher.d.ts.map +1 -1
- package/lib/common/response-content-matcher.js +14 -3
- package/lib/common/response-content-matcher.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/ai-chat-frontend-contribution.ts +49 -0
- package/src/browser/ai-chat-frontend-module.ts +25 -4
- package/src/browser/change-set-decorator-service.ts +72 -0
- package/src/browser/change-set-file-element.ts +18 -13
- package/src/browser/change-set-file-resource.ts +1 -138
- package/src/browser/change-set-variable.ts +14 -6
- package/src/browser/context-file-variable-label-provider.ts +1 -1
- package/src/browser/file-chat-variable-contribution.ts +26 -29
- package/src/browser/task-context-service.ts +144 -0
- package/src/browser/task-context-storage-service.ts +75 -0
- package/src/browser/task-context-variable-contribution.ts +93 -0
- package/src/browser/task-context-variable-label-provider.ts +67 -0
- package/src/browser/task-context-variable.ts +28 -0
- package/src/common/chat-agents.ts +38 -22
- package/src/common/chat-model.ts +566 -17
- package/src/common/chat-request-parser.ts +2 -2
- package/src/common/chat-service.ts +17 -26
- package/src/common/chat-session-naming-service.ts +13 -15
- package/src/common/chat-session-summary-agent-prompt.ts +28 -0
- package/src/common/chat-session-summary-agent.ts +42 -0
- package/src/common/context-summary-variable.ts +1 -1
- package/src/common/parse-contents-with-incomplete-parts.spec.ts +114 -0
- package/src/common/parse-contents.spec.ts +24 -12
- package/src/common/parse-contents.ts +52 -6
- package/src/common/response-content-matcher.ts +21 -3
|
@@ -14,147 +14,10 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
import { injectable } from '@theia/core/shared/inversify';
|
|
17
|
+
import { URI } from '@theia/core';
|
|
19
18
|
|
|
20
19
|
export const CHANGE_SET_FILE_RESOURCE_SCHEME = 'changeset-file';
|
|
21
|
-
export type ResourceInitializationOptions = Pick<Resource, 'autosaveable' | 'initiallyDirty' | 'readOnly'> & { contents?: string, onSave?: Resource['saveContents'] };
|
|
22
|
-
export type ResourceUpdateOptions = Pick<ResourceInitializationOptions, 'contents' | 'onSave'>;
|
|
23
20
|
|
|
24
21
|
export function createChangeSetFileUri(chatSessionId: string, elementUri: URI): URI {
|
|
25
22
|
return elementUri.withScheme(CHANGE_SET_FILE_RESOURCE_SCHEME).withAuthority(chatSessionId);
|
|
26
23
|
}
|
|
27
|
-
|
|
28
|
-
export class UpdatableReferenceResource extends ReferenceMutableResource {
|
|
29
|
-
static acquire(resource: UpdatableReferenceResource): UpdatableReferenceResource {
|
|
30
|
-
DisposableRefCounter.acquire(resource.reference);
|
|
31
|
-
return resource;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
constructor(protected override reference: DisposableRefCounter<DisposableMutableResource>) {
|
|
35
|
-
super(reference);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
update(options: ResourceUpdateOptions): void {
|
|
39
|
-
this.reference.object.update(options);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
get readOnly(): Resource['readOnly'] {
|
|
43
|
-
return this.reference.object.readOnly;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get initiallyDirty(): boolean {
|
|
47
|
-
return this.reference.object.initiallyDirty;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
get autosaveable(): boolean {
|
|
51
|
-
return this.reference.object.autosaveable;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export class DisposableMutableResource extends MutableResource {
|
|
56
|
-
onSave: Resource['saveContents'] | undefined;
|
|
57
|
-
constructor(uri: URI, protected readonly options?: ResourceInitializationOptions) {
|
|
58
|
-
super(uri);
|
|
59
|
-
this.onSave = options?.onSave;
|
|
60
|
-
this.contents = options?.contents ?? '';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
get readOnly(): Resource['readOnly'] {
|
|
64
|
-
return this.options?.readOnly || !this.onSave;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
get autosaveable(): boolean {
|
|
68
|
-
return this.options?.autosaveable !== false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
get initiallyDirty(): boolean {
|
|
72
|
-
return !!this.options?.initiallyDirty;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
override async saveContents(contents: string): Promise<void> {
|
|
76
|
-
if (this.options?.onSave) {
|
|
77
|
-
await this.options.onSave(contents);
|
|
78
|
-
this.update({ contents });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
update(options: ResourceUpdateOptions): void {
|
|
83
|
-
if (options.contents !== undefined && options.contents !== this.contents) {
|
|
84
|
-
this.contents = options.contents;
|
|
85
|
-
this.fireDidChangeContents();
|
|
86
|
-
}
|
|
87
|
-
if ('onSave' in options && options.onSave !== this.onSave) {
|
|
88
|
-
this.onSave = options.onSave;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
override dispose(): void {
|
|
93
|
-
this.onDidChangeContentsEmitter.dispose();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export class DisposableRefCounter<V = unknown> implements Reference<V> {
|
|
98
|
-
static acquire<V>(item: DisposableRefCounter<V>): DisposableRefCounter<V> {
|
|
99
|
-
item.refs++;
|
|
100
|
-
return item;
|
|
101
|
-
}
|
|
102
|
-
static create<V>(value: V, onDispose: () => void): DisposableRefCounter<V> {
|
|
103
|
-
return this.acquire(new this(value, onDispose));
|
|
104
|
-
}
|
|
105
|
-
readonly object: V;
|
|
106
|
-
protected refs = 0;
|
|
107
|
-
protected constructor(value: V, protected readonly onDispose: () => void) {
|
|
108
|
-
this.object = value;
|
|
109
|
-
}
|
|
110
|
-
dispose(): void {
|
|
111
|
-
this.refs--;
|
|
112
|
-
if (this.refs === 0) {
|
|
113
|
-
this.onDispose();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
@injectable()
|
|
119
|
-
export class ChangeSetFileResourceResolver implements ResourceResolver {
|
|
120
|
-
protected readonly cache = new Map<string, UpdatableReferenceResource>();
|
|
121
|
-
|
|
122
|
-
add(uri: URI, options?: ResourceInitializationOptions): UpdatableReferenceResource {
|
|
123
|
-
const key = uri.toString();
|
|
124
|
-
if (this.cache.has(key)) {
|
|
125
|
-
throw new Error(`Resource ${key} already exists.`);
|
|
126
|
-
}
|
|
127
|
-
const underlyingResource = new DisposableMutableResource(uri, options);
|
|
128
|
-
const ref = DisposableRefCounter.create(underlyingResource, () => {
|
|
129
|
-
underlyingResource.dispose();
|
|
130
|
-
this.cache.delete(key);
|
|
131
|
-
});
|
|
132
|
-
const refResource = new UpdatableReferenceResource(ref);
|
|
133
|
-
this.cache.set(key, refResource);
|
|
134
|
-
return refResource;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
tryGet(uri: URI): UpdatableReferenceResource | undefined {
|
|
138
|
-
try {
|
|
139
|
-
return this.resolve(uri);
|
|
140
|
-
} catch {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
update(uri: URI, contents: string): void {
|
|
146
|
-
const key = uri.toString();
|
|
147
|
-
const resource = this.cache.get(key);
|
|
148
|
-
if (!resource) {
|
|
149
|
-
throw new Error(`No resource for ${key}.`);
|
|
150
|
-
}
|
|
151
|
-
resource.update({ contents });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
resolve(uri: URI): UpdatableReferenceResource {
|
|
155
|
-
const key = uri.toString();
|
|
156
|
-
const ref = this.cache.get(key);
|
|
157
|
-
if (!ref) { throw new Error(`No resource for ${key}.`); }
|
|
158
|
-
return UpdatableReferenceResource.acquire(ref);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
@@ -18,13 +18,13 @@ import { MaybePromise, nls } from '@theia/core';
|
|
|
18
18
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
19
19
|
import { AIVariable, ResolvedAIVariable, AIVariableContribution, AIVariableResolver, AIVariableService, AIVariableResolutionRequest, AIVariableContext } from '@theia/ai-core';
|
|
20
20
|
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
21
|
-
import { ChatSessionContext } from '../common';
|
|
21
|
+
import { CHANGE_SET_SUMMARY_VARIABLE_ID, ChatSessionContext } from '../common';
|
|
22
22
|
|
|
23
23
|
export const CHANGE_SET_SUMMARY_VARIABLE: AIVariable = {
|
|
24
|
-
id:
|
|
24
|
+
id: CHANGE_SET_SUMMARY_VARIABLE_ID,
|
|
25
25
|
description: nls.localize('theia/ai/core/changeSetSummaryVariable/description', 'Provides a summary of the files in a change set and their contents.'),
|
|
26
26
|
|
|
27
|
-
name:
|
|
27
|
+
name: CHANGE_SET_SUMMARY_VARIABLE_ID,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
@injectable()
|
|
@@ -41,14 +41,22 @@ export class ChangeSetVariableContribution implements AIVariableContribution, AI
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
|
|
44
|
-
if (!ChatSessionContext.is(context) || request.variable.name !== CHANGE_SET_SUMMARY_VARIABLE.name
|
|
44
|
+
if (!ChatSessionContext.is(context) || request.variable.name !== CHANGE_SET_SUMMARY_VARIABLE.name) { return undefined; }
|
|
45
|
+
if (!context.model.changeSet?.getElements().length) {
|
|
46
|
+
return {
|
|
47
|
+
variable: CHANGE_SET_SUMMARY_VARIABLE,
|
|
48
|
+
value: ''
|
|
49
|
+
};
|
|
50
|
+
}
|
|
45
51
|
const entries = await Promise.all(
|
|
46
52
|
context.model.changeSet.getElements().map(async element => `- file: ${await this.workspaceService.getWorkspaceRelativePath(element.uri)}, status: ${element.state}`)
|
|
47
53
|
);
|
|
48
54
|
return {
|
|
49
55
|
variable: CHANGE_SET_SUMMARY_VARIABLE,
|
|
50
|
-
value:
|
|
56
|
+
value: `## Previously Proposed Changes
|
|
57
|
+
You have previously proposed changes for the following files. Some suggestions may have been accepted by the user, while others may still be pending.
|
|
58
|
+
${entries.join('\n')}
|
|
59
|
+
`
|
|
51
60
|
};
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
|
-
|
|
@@ -49,7 +49,7 @@ export class ContextFileVariableLabelProvider implements LabelProviderContributi
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
getDetails(element: object): string | undefined {
|
|
52
|
-
return this.
|
|
52
|
+
return this.labelProvider.getDetails(this.getUri(element)!);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
protected getUri(element: object): URI | undefined {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { AIVariableContext, AIVariableResolutionRequest, PromptText } from '@theia/ai-core';
|
|
18
|
-
import { AIVariableDropResult, FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
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
20
|
import { CancellationToken, QuickInputService, URI } from '@theia/core';
|
|
21
21
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
@@ -23,6 +23,7 @@ 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
|
+
import { VARIABLE_ADD_CONTEXT_COMMAND } from './ai-chat-frontend-contribution';
|
|
26
27
|
|
|
27
28
|
@injectable()
|
|
28
29
|
export class FileChatVariableContribution implements FrontendVariableContribution {
|
|
@@ -72,40 +73,36 @@ export class FileChatVariableContribution implements FrontendVariableContributio
|
|
|
72
73
|
position: monaco.Position,
|
|
73
74
|
matchString?: string
|
|
74
75
|
): Promise<monaco.languages.CompletionItem[] | undefined> {
|
|
75
|
-
const
|
|
76
|
-
|
|
76
|
+
const context = AIVariableCompletionContext.get(FILE_VARIABLE.name, model, position, matchString);
|
|
77
|
+
if (!context) { return undefined; }
|
|
78
|
+
const { userInput, range, prefix } = context;
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
if (indexOfVariableTrigger === -1 || lineContent.substring(indexOfVariableTrigger).includes(' ')) {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// determine whether we are providing completions before or after the variable argument separator
|
|
84
|
-
const indexOfVariableArgSeparator = lineContent.lastIndexOf(PromptText.VARIABLE_SEPARATOR_CHAR, position.column - 1);
|
|
85
|
-
const triggerCharIndex = Math.max(indexOfVariableTrigger, indexOfVariableArgSeparator);
|
|
86
|
-
|
|
87
|
-
const typedWord = lineContent.substring(triggerCharIndex + 1, position.column - 1);
|
|
88
|
-
const range = new monaco.Range(position.lineNumber, triggerCharIndex + 2, position.lineNumber, position.column);
|
|
89
|
-
const picks = await this.quickFileSelectService.getPicks(typedWord, CancellationToken.None);
|
|
90
|
-
const matchVariableChar = lineContent[triggerCharIndex] === (matchString ? matchString : PromptText.VARIABLE_CHAR);
|
|
91
|
-
const prefix = matchVariableChar ? FILE_VARIABLE.name + PromptText.VARIABLE_SEPARATOR_CHAR : '';
|
|
80
|
+
const picks = await this.quickFileSelectService.getPicks(userInput, CancellationToken.None);
|
|
92
81
|
|
|
93
82
|
return Promise.all(
|
|
94
83
|
picks
|
|
95
84
|
.filter(FileQuickPickItem.is)
|
|
96
85
|
// only show files with highlights, if the user started typing to filter down the results
|
|
97
|
-
.filter(p => !
|
|
98
|
-
.map(async (pick, index) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
86
|
+
.filter(p => !userInput || p.highlights?.label)
|
|
87
|
+
.map(async (pick, index) => {
|
|
88
|
+
const relativePath = await this.wsService.getWorkspaceRelativePath(pick.uri);
|
|
89
|
+
return {
|
|
90
|
+
label: pick.label,
|
|
91
|
+
kind: monaco.languages.CompletionItemKind.File,
|
|
92
|
+
range,
|
|
93
|
+
insertText: `${prefix}${relativePath}`,
|
|
94
|
+
detail: await this.wsService.getWorkspaceRelativePath(pick.uri.parent),
|
|
95
|
+
// don't let monaco filter the items, as we only return picks that are filtered
|
|
96
|
+
filterText: userInput,
|
|
97
|
+
// keep the order of the items, but move them to the end of the list
|
|
98
|
+
sortText: `ZZ${index.toString().padStart(4, '0')}_${pick.label}`,
|
|
99
|
+
command: {
|
|
100
|
+
title: VARIABLE_ADD_CONTEXT_COMMAND.label!,
|
|
101
|
+
id: VARIABLE_ADD_CONTEXT_COMMAND.id,
|
|
102
|
+
arguments: [FILE_VARIABLE.name, relativePath]
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
})
|
|
109
106
|
);
|
|
110
107
|
}
|
|
111
108
|
|
|
@@ -0,0 +1,144 @@
|
|
|
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 { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { MaybePromise, ProgressService, URI, generateUuid, Event } from '@theia/core';
|
|
19
|
+
import { ChatAgent, ChatAgentLocation, ChatService, ChatSession, MutableChatModel, MutableChatRequestModel, ParsedChatRequestTextPart } from '../common';
|
|
20
|
+
import { ChatSessionSummaryAgent } from '../common/chat-session-summary-agent';
|
|
21
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
22
|
+
import { AgentService, PromptService } from '@theia/ai-core';
|
|
23
|
+
import { CHAT_SESSION_SUMMARY_PROMPT } from '../common/chat-session-summary-agent-prompt';
|
|
24
|
+
|
|
25
|
+
export interface SummaryMetadata {
|
|
26
|
+
label: string;
|
|
27
|
+
uri?: URI;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Summary extends SummaryMetadata {
|
|
32
|
+
summary: string;
|
|
33
|
+
id: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const TaskContextStorageService = Symbol('TextContextStorageService');
|
|
37
|
+
export interface TaskContextStorageService {
|
|
38
|
+
onDidChange: Event<void>;
|
|
39
|
+
store(summary: Summary): MaybePromise<void>;
|
|
40
|
+
getAll(): Summary[];
|
|
41
|
+
get(identifier: string): Summary | undefined;
|
|
42
|
+
delete(identifier: string): MaybePromise<boolean>;
|
|
43
|
+
open(identifier: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@injectable()
|
|
47
|
+
export class TaskContextService {
|
|
48
|
+
|
|
49
|
+
protected pendingSummaries = new Map<string, Promise<Summary>>();
|
|
50
|
+
|
|
51
|
+
@inject(ChatService) protected readonly chatService: ChatService;
|
|
52
|
+
@inject(AgentService) protected readonly agentService: AgentService;
|
|
53
|
+
@inject(PromptService) protected readonly promptService: PromptService;
|
|
54
|
+
@inject(TaskContextStorageService) protected readonly storageService: TaskContextStorageService;
|
|
55
|
+
@inject(ProgressService) protected readonly progressService: ProgressService;
|
|
56
|
+
|
|
57
|
+
get onDidChange(): Event<void> {
|
|
58
|
+
return this.storageService.onDidChange;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getAll(): Array<Summary> {
|
|
62
|
+
return this.storageService.getAll();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getSummary(sessionIdOrFilePath: string): Promise<string> {
|
|
66
|
+
const existing = this.storageService.get(sessionIdOrFilePath);
|
|
67
|
+
if (existing) { return existing.summary; }
|
|
68
|
+
const pending = this.pendingSummaries.get(sessionIdOrFilePath);
|
|
69
|
+
if (pending) {
|
|
70
|
+
return pending.then(({ summary }) => summary);
|
|
71
|
+
}
|
|
72
|
+
const session = this.chatService.getSession(sessionIdOrFilePath);
|
|
73
|
+
if (session) {
|
|
74
|
+
return this.summarize(session);
|
|
75
|
+
}
|
|
76
|
+
throw new Error('Unable to resolve summary request.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 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> {
|
|
81
|
+
const pending = this.pendingSummaries.get(session.id);
|
|
82
|
+
if (pending) { return pending.then(({ id }) => id); }
|
|
83
|
+
const existing = this.getSummaryForSession(session);
|
|
84
|
+
if (existing) { return existing.id; }
|
|
85
|
+
const summaryId = generateUuid();
|
|
86
|
+
const summaryDeferred = new Deferred<Summary>();
|
|
87
|
+
const progress = await this.progressService.showProgress({ text: `Summarize: ${session.title || session.id}`, options: { location: 'ai-chat' } });
|
|
88
|
+
this.pendingSummaries.set(session.id, summaryDeferred.promise);
|
|
89
|
+
try {
|
|
90
|
+
const newSummary: Summary = {
|
|
91
|
+
summary: await this.getLlmSummary(session, promptId, agent),
|
|
92
|
+
label: session.title || session.id,
|
|
93
|
+
sessionId: session.id,
|
|
94
|
+
id: summaryId
|
|
95
|
+
};
|
|
96
|
+
await this.storageService.store(newSummary);
|
|
97
|
+
return summaryId;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
summaryDeferred.reject(err);
|
|
100
|
+
throw err;
|
|
101
|
+
} finally {
|
|
102
|
+
progress.cancel();
|
|
103
|
+
this.pendingSummaries.delete(session.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected async getLlmSummary(session: ChatSession, promptId: string = CHAT_SESSION_SUMMARY_PROMPT.id, agent?: ChatAgent): Promise<string> {
|
|
108
|
+
agent = agent || this.agentService.getAgents().find<ChatAgent>((candidate): candidate is ChatAgent =>
|
|
109
|
+
'invoke' in candidate
|
|
110
|
+
&& typeof candidate.invoke === 'function'
|
|
111
|
+
&& candidate.id === ChatSessionSummaryAgent.ID
|
|
112
|
+
);
|
|
113
|
+
if (!agent) { throw new Error('Unable to identify agent for summary.'); }
|
|
114
|
+
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
115
|
+
const prompt = await this.promptService.getPrompt(promptId || CHAT_SESSION_SUMMARY_PROMPT.id, undefined, { model: session.model });
|
|
116
|
+
if (!prompt) { return ''; }
|
|
117
|
+
const messages = session.model.getRequests().filter((candidate): candidate is MutableChatRequestModel => candidate instanceof MutableChatRequestModel);
|
|
118
|
+
messages.forEach(message => model['_hierarchy'].append(message));
|
|
119
|
+
const summaryRequest = model.addRequest({
|
|
120
|
+
variables: prompt.variables ?? [],
|
|
121
|
+
request: { text: prompt.text },
|
|
122
|
+
parts: [new ParsedChatRequestTextPart({ start: 0, endExclusive: prompt.text.length }, prompt.text)],
|
|
123
|
+
toolRequests: prompt.functionDescriptions ?? new Map()
|
|
124
|
+
}, agent.id);
|
|
125
|
+
await agent.invoke(summaryRequest);
|
|
126
|
+
return summaryRequest.response.response.asDisplayString();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
hasSummary(chatSession: ChatSession): boolean {
|
|
130
|
+
return !!this.getSummaryForSession(chatSession);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
protected getSummaryForSession(chatSession: ChatSession): Summary | undefined {
|
|
134
|
+
return this.storageService.getAll().find(candidate => candidate.sessionId === chatSession.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getLabel(id: string): string | undefined {
|
|
138
|
+
return this.storageService.get(id)?.label;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
open(id: string): Promise<void> {
|
|
142
|
+
return this.storageService.open(id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { Summary, TaskContextStorageService } from './task-context-service';
|
|
19
|
+
import { Emitter } from '@theia/core';
|
|
20
|
+
import { AIVariableResourceResolver } from '@theia/ai-core';
|
|
21
|
+
import { TASK_CONTEXT_VARIABLE } from './task-context-variable';
|
|
22
|
+
import { open, OpenerService } from '@theia/core/lib/browser';
|
|
23
|
+
|
|
24
|
+
@injectable()
|
|
25
|
+
export class InMemoryTaskContextStorage implements TaskContextStorageService {
|
|
26
|
+
protected summaries = new Map<string, Summary>();
|
|
27
|
+
|
|
28
|
+
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
29
|
+
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
30
|
+
|
|
31
|
+
@inject(AIVariableResourceResolver)
|
|
32
|
+
protected readonly variableResourceResolver: AIVariableResourceResolver;
|
|
33
|
+
|
|
34
|
+
@inject(OpenerService)
|
|
35
|
+
protected readonly openerService: OpenerService;
|
|
36
|
+
|
|
37
|
+
store(summary: Summary): void {
|
|
38
|
+
this.summaries.set(summary.id, summary);
|
|
39
|
+
this.onDidChangeEmitter.fire();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getAll(): Summary[] {
|
|
43
|
+
return Array.from(this.summaries.values());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get(identifier: string): Summary | undefined {
|
|
47
|
+
return this.summaries.get(identifier);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
delete(identifier: string): boolean {
|
|
51
|
+
const didDelete = this.summaries.delete(identifier);
|
|
52
|
+
if (didDelete) {
|
|
53
|
+
this.onDidChangeEmitter.fire();
|
|
54
|
+
}
|
|
55
|
+
return didDelete;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clear(): void {
|
|
59
|
+
if (this.summaries.size) {
|
|
60
|
+
this.summaries.clear();
|
|
61
|
+
this.onDidChangeEmitter.fire();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async open(identifier: string): Promise<void> {
|
|
66
|
+
const summary = this.get(identifier);
|
|
67
|
+
if (!summary) {
|
|
68
|
+
throw new Error('Unable to upon requested task context: none found.');
|
|
69
|
+
}
|
|
70
|
+
const resource = this.variableResourceResolver.getOrCreate({ variable: TASK_CONTEXT_VARIABLE, arg: identifier }, {}, summary.summary);
|
|
71
|
+
resource.update({ onSave: async content => { summary.summary = content; }, readOnly: false });
|
|
72
|
+
await open(this.openerService, resource.uri);
|
|
73
|
+
resource.dispose();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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 { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { AIVariableContext, AIVariableOpener, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIContextVariable } from '@theia/ai-core';
|
|
19
|
+
import { AIVariableCompletionContext, FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
20
|
+
import { MaybePromise, QuickInputService, QuickPickItem } from '@theia/core';
|
|
21
|
+
import { ChatService } from '../common';
|
|
22
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
23
|
+
import { TaskContextService } from './task-context-service';
|
|
24
|
+
import { TASK_CONTEXT_VARIABLE } from './task-context-variable';
|
|
25
|
+
import { VARIABLE_ADD_CONTEXT_COMMAND } from './ai-chat-frontend-contribution';
|
|
26
|
+
|
|
27
|
+
@injectable()
|
|
28
|
+
export class TaskContextVariableContribution implements FrontendVariableContribution, AIVariableResolver, AIVariableOpener {
|
|
29
|
+
@inject(QuickInputService) protected readonly quickInputService: QuickInputService;
|
|
30
|
+
@inject(ChatService) protected readonly chatService: ChatService;
|
|
31
|
+
@inject(TaskContextService) protected readonly taskContextService: TaskContextService;
|
|
32
|
+
|
|
33
|
+
registerVariables(service: FrontendVariableService): void {
|
|
34
|
+
service.registerResolver(TASK_CONTEXT_VARIABLE, this);
|
|
35
|
+
service.registerArgumentPicker(TASK_CONTEXT_VARIABLE, this.pickSession.bind(this));
|
|
36
|
+
service.registerArgumentCompletionProvider(TASK_CONTEXT_VARIABLE, this.provideCompletionItems.bind(this));
|
|
37
|
+
service.registerOpener(TASK_CONTEXT_VARIABLE, this);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected async pickSession(): Promise<string | undefined> {
|
|
41
|
+
const items = this.getItems();
|
|
42
|
+
const selection = await this.quickInputService.showQuickPick(items);
|
|
43
|
+
return selection?.id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected async provideCompletionItems(
|
|
47
|
+
model: monaco.editor.ITextModel,
|
|
48
|
+
position: monaco.Position,
|
|
49
|
+
matchString?: string
|
|
50
|
+
): Promise<monaco.languages.CompletionItem[] | undefined> {
|
|
51
|
+
const context = AIVariableCompletionContext.get(TASK_CONTEXT_VARIABLE.name, model, position, matchString);
|
|
52
|
+
if (!context) { return undefined; }
|
|
53
|
+
const { userInput, range, prefix } = context;
|
|
54
|
+
return this.getItems().filter(candidate => QuickPickItem.is(candidate) && candidate.label.startsWith(userInput)).map(({ label, id }: QuickPickItem) => ({
|
|
55
|
+
label,
|
|
56
|
+
kind: monaco.languages.CompletionItemKind.Class,
|
|
57
|
+
range,
|
|
58
|
+
insertText: `${prefix}${id}`,
|
|
59
|
+
detail: id,
|
|
60
|
+
filterText: userInput,
|
|
61
|
+
command: {
|
|
62
|
+
title: VARIABLE_ADD_CONTEXT_COMMAND.label!,
|
|
63
|
+
id: VARIABLE_ADD_CONTEXT_COMMAND.id,
|
|
64
|
+
arguments: [TASK_CONTEXT_VARIABLE.name, id]
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected getItems(): QuickPickItem[] {
|
|
70
|
+
const currentSession = this.chatService.getSessions().find(candidate => candidate.isActive);
|
|
71
|
+
const existingSummaries = this.taskContextService.getAll().filter(candidate => !currentSession || currentSession.id !== candidate.sessionId);
|
|
72
|
+
return existingSummaries;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise<number> {
|
|
76
|
+
return request.variable.id === TASK_CONTEXT_VARIABLE.id ? 10000 : -5;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async resolve(request: AIVariableResolutionRequest, _context: AIVariableContext): Promise<ResolvedAIContextVariable | undefined> {
|
|
80
|
+
if (request.variable.id !== TASK_CONTEXT_VARIABLE.id || !request.arg) { return; }
|
|
81
|
+
const value = await this.taskContextService.getSummary(request.arg).catch(() => undefined);
|
|
82
|
+
return value ? { ...request, value, contextValue: value } : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
canOpen(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise<number> {
|
|
86
|
+
return this.canResolve(request, context);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async open(request: AIVariableResolutionRequest, _context: AIVariableContext): Promise<void> {
|
|
90
|
+
if (request.variable.id !== TASK_CONTEXT_VARIABLE.id || !request.arg) { throw new Error('Unable to service open request.'); }
|
|
91
|
+
return this.taskContextService.open(request.arg);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 Eclipse 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 { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
18
|
+
import { URI } from '@theia/core';
|
|
19
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
20
|
+
import { codicon, LabelProviderContribution } from '@theia/core/lib/browser';
|
|
21
|
+
import { TaskContextVariableContribution } from './task-context-variable-contribution';
|
|
22
|
+
import { ChatService } from '../common';
|
|
23
|
+
import { TaskContextService } from './task-context-service';
|
|
24
|
+
import { TASK_CONTEXT_VARIABLE } from './task-context-variable';
|
|
25
|
+
|
|
26
|
+
@injectable()
|
|
27
|
+
export class TaskContextVariableLabelProvider implements LabelProviderContribution {
|
|
28
|
+
@inject(ChatService) protected readonly chatService: ChatService;
|
|
29
|
+
@inject(TaskContextVariableContribution) protected readonly chatVariableContribution: TaskContextVariableContribution;
|
|
30
|
+
@inject(TaskContextService) protected readonly taskContextService: TaskContextService;
|
|
31
|
+
protected isMine(element: object): element is AIVariableResolutionRequest & { arg: string } {
|
|
32
|
+
return AIVariableResolutionRequest.is(element) && element.variable.id === TASK_CONTEXT_VARIABLE.id && !!element.arg;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
canHandle(element: object): number {
|
|
36
|
+
return this.isMine(element) ? 10 : -1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getIcon(element: object): string | undefined {
|
|
40
|
+
if (!this.isMine(element)) { return undefined; }
|
|
41
|
+
return codicon('clippy');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getName(element: object): string | undefined {
|
|
45
|
+
if (!this.isMine(element)) { return undefined; }
|
|
46
|
+
const session = this.chatService.getSession(element.arg);
|
|
47
|
+
return session?.title ?? this.taskContextService.getLabel(element.arg) ?? session?.id ?? element.arg;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getLongName(element: object): string | undefined {
|
|
51
|
+
const short = this.getName(element);
|
|
52
|
+
const details = this.getDetails(element);
|
|
53
|
+
return `'${short}' (${details})`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getDetails(element: object): string | undefined {
|
|
57
|
+
if (!this.isMine(element)) { return undefined; }
|
|
58
|
+
return `id: ${element.arg}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected getUri(element: object): URI | undefined {
|
|
62
|
+
if (!AIVariableResolutionRequest.is(element)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return new URI(element.arg);
|
|
66
|
+
}
|
|
67
|
+
}
|