@theia/ai-core 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-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +16 -0
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/ai-variable-uri-label-provider.d.ts +17 -0
- package/lib/browser/ai-variable-uri-label-provider.d.ts.map +1 -0
- package/lib/browser/ai-variable-uri-label-provider.js +85 -0
- package/lib/browser/ai-variable-uri-label-provider.js.map +1 -0
- package/lib/browser/file-variable-contribution.d.ts +9 -3
- package/lib/browser/file-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-variable-contribution.js +26 -8
- package/lib/browser/file-variable-contribution.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.d.ts +22 -1
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +63 -25
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/frontend-variable-service.d.ts +28 -4
- package/lib/browser/frontend-variable-service.d.ts.map +1 -1
- package/lib/browser/frontend-variable-service.js +84 -1
- package/lib/browser/frontend-variable-service.js.map +1 -1
- package/lib/browser/prompttemplate-contribution.d.ts +0 -3
- package/lib/browser/prompttemplate-contribution.d.ts.map +1 -1
- package/lib/browser/prompttemplate-contribution.js +5 -24
- package/lib/browser/prompttemplate-contribution.js.map +1 -1
- package/lib/browser/token-usage-frontend-service-impl.d.ts +25 -0
- package/lib/browser/token-usage-frontend-service-impl.d.ts.map +1 -0
- package/lib/browser/token-usage-frontend-service-impl.js +120 -0
- package/lib/browser/token-usage-frontend-service-impl.js.map +1 -0
- package/lib/browser/token-usage-frontend-service.d.ts +29 -0
- package/lib/browser/token-usage-frontend-service.d.ts.map +1 -0
- package/lib/browser/token-usage-frontend-service.js +23 -0
- package/lib/browser/token-usage-frontend-service.js.map +1 -0
- package/lib/common/ai-variable-resource.d.ts +18 -0
- package/lib/common/ai-variable-resource.d.ts.map +1 -0
- package/lib/common/ai-variable-resource.js +103 -0
- package/lib/common/ai-variable-resource.js.map +1 -0
- package/lib/common/configurable-in-memory-resources.d.ts +45 -0
- package/lib/common/configurable-in-memory-resources.d.ts.map +1 -0
- package/lib/common/configurable-in-memory-resources.js +147 -0
- package/lib/common/configurable-in-memory-resources.js.map +1 -0
- package/lib/common/index.d.ts +3 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +3 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model.d.ts +7 -2
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +5 -1
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/prompt-service.d.ts +16 -4
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +1 -1
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-variable-contribution.d.ts +2 -1
- package/lib/common/prompt-variable-contribution.d.ts.map +1 -1
- package/lib/common/prompt-variable-contribution.js +10 -1
- package/lib/common/prompt-variable-contribution.js.map +1 -1
- package/lib/common/protocol.d.ts +14 -0
- package/lib/common/protocol.d.ts.map +1 -1
- package/lib/common/protocol.js +3 -1
- package/lib/common/protocol.js.map +1 -1
- package/lib/common/token-usage-service.d.ts +35 -0
- package/lib/common/token-usage-service.d.ts.map +1 -0
- package/lib/common/token-usage-service.js +20 -0
- package/lib/common/token-usage-service.js.map +1 -0
- package/lib/common/variable-service.d.ts +17 -2
- package/lib/common/variable-service.d.ts.map +1 -1
- package/lib/common/variable-service.js +43 -32
- package/lib/common/variable-service.js.map +1 -1
- package/lib/node/ai-core-backend-module.d.ts.map +1 -1
- package/lib/node/ai-core-backend-module.js +9 -0
- package/lib/node/ai-core-backend-module.js.map +1 -1
- package/lib/node/language-model-frontend-delegate.d.ts +2 -2
- package/lib/node/language-model-frontend-delegate.d.ts.map +1 -1
- package/lib/node/language-model-frontend-delegate.js.map +1 -1
- package/lib/node/token-usage-service-impl.d.ts +23 -0
- package/lib/node/token-usage-service-impl.d.ts.map +1 -0
- package/lib/node/token-usage-service-impl.js +65 -0
- package/lib/node/token-usage-service-impl.js.map +1 -0
- package/package.json +11 -10
- package/src/browser/ai-core-frontend-module.ts +26 -5
- package/src/browser/ai-variable-uri-label-provider.ts +66 -0
- package/src/browser/file-variable-contribution.ts +34 -14
- package/src/browser/frontend-prompt-customization-service.ts +72 -25
- package/src/browser/frontend-variable-service.ts +115 -5
- package/src/browser/prompttemplate-contribution.ts +5 -26
- package/src/browser/token-usage-frontend-service-impl.ts +117 -0
- package/src/browser/token-usage-frontend-service.ts +47 -0
- package/src/common/ai-variable-resource.ts +86 -0
- package/src/common/configurable-in-memory-resources.ts +156 -0
- package/src/common/index.ts +3 -0
- package/src/common/language-model.ts +10 -2
- package/src/common/prompt-service.ts +14 -4
- package/src/common/prompt-variable-contribution.ts +10 -2
- package/src/common/protocol.ts +18 -0
- package/src/common/token-usage-service.ts +56 -0
- package/src/common/variable-service.ts +58 -44
- package/src/node/ai-core-backend-module.ts +21 -1
- package/src/node/language-model-frontend-delegate.ts +2 -2
- package/src/node/token-usage-service-impl.ts +65 -0
|
@@ -0,0 +1,66 @@
|
|
|
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 { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { URI } from '@theia/core';
|
|
19
|
+
import { LabelProvider, LabelProviderContribution } from '@theia/core/lib/browser';
|
|
20
|
+
import { AI_VARIABLE_RESOURCE_SCHEME, AIVariableResourceResolver } from '../common/ai-variable-resource';
|
|
21
|
+
import { AIVariableResolutionRequest, AIVariableService } from '../common/variable-service';
|
|
22
|
+
|
|
23
|
+
@injectable()
|
|
24
|
+
export class AIVariableUriLabelProvider implements LabelProviderContribution {
|
|
25
|
+
|
|
26
|
+
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
|
|
27
|
+
@inject(AIVariableResourceResolver) protected variableResourceResolver: AIVariableResourceResolver;
|
|
28
|
+
@inject(AIVariableService) protected readonly variableService: AIVariableService;
|
|
29
|
+
|
|
30
|
+
protected isMine(element: object): element is URI {
|
|
31
|
+
return element instanceof URI && element.scheme === AI_VARIABLE_RESOURCE_SCHEME;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
canHandle(element: object): number {
|
|
35
|
+
return this.isMine(element) ? 150 : -1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getIcon(element: object): string | undefined {
|
|
39
|
+
if (!this.isMine(element)) { return undefined; }
|
|
40
|
+
return this.labelProvider.getIcon(this.getResolutionRequest(element)!);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getName(element: object): string | undefined {
|
|
44
|
+
if (!this.isMine(element)) { return undefined; }
|
|
45
|
+
return this.labelProvider.getName(this.getResolutionRequest(element)!);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getLongName(element: object): string | undefined {
|
|
49
|
+
if (!this.isMine(element)) { return undefined; }
|
|
50
|
+
return this.labelProvider.getLongName(this.getResolutionRequest(element)!);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getDetails(element: object): string | undefined {
|
|
54
|
+
if (!this.isMine(element)) { return undefined; }
|
|
55
|
+
return this.labelProvider.getDetails(this.getResolutionRequest(element)!);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected getResolutionRequest(element: object): AIVariableResolutionRequest | undefined {
|
|
59
|
+
if (!this.isMine(element)) { return undefined; }
|
|
60
|
+
const metadata = this.variableResourceResolver.fromUri(element);
|
|
61
|
+
if (!metadata) { return undefined; }
|
|
62
|
+
const { variableName, arg } = metadata;
|
|
63
|
+
const variable = this.variableService.getVariable(variableName);
|
|
64
|
+
return variable && { variable, arg };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { Path, URI } from '@theia/core';
|
|
18
|
-
import { codiconArray } from '@theia/core/lib/browser';
|
|
18
|
+
import { OpenerService, codiconArray, open } from '@theia/core/lib/browser';
|
|
19
19
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
20
20
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
21
21
|
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
|
@@ -23,11 +23,12 @@ import {
|
|
|
23
23
|
AIVariable,
|
|
24
24
|
AIVariableContext,
|
|
25
25
|
AIVariableContribution,
|
|
26
|
+
AIVariableOpener,
|
|
26
27
|
AIVariableResolutionRequest,
|
|
27
28
|
AIVariableResolver,
|
|
28
|
-
AIVariableService,
|
|
29
29
|
ResolvedAIContextVariable,
|
|
30
30
|
} from '../common/variable-service';
|
|
31
|
+
import { FrontendVariableService } from './frontend-variable-service';
|
|
31
32
|
|
|
32
33
|
export namespace FileVariableArgs {
|
|
33
34
|
export const uri = 'uri';
|
|
@@ -44,15 +45,19 @@ export const FILE_VARIABLE: AIVariable = {
|
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
@injectable()
|
|
47
|
-
export class FileVariableContribution implements AIVariableContribution, AIVariableResolver {
|
|
48
|
+
export class FileVariableContribution implements AIVariableContribution, AIVariableResolver, AIVariableOpener {
|
|
48
49
|
@inject(FileService)
|
|
49
50
|
protected readonly fileService: FileService;
|
|
50
51
|
|
|
51
52
|
@inject(WorkspaceService)
|
|
52
53
|
protected readonly wsService: WorkspaceService;
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
@inject(OpenerService)
|
|
56
|
+
protected readonly openerService: OpenerService;
|
|
57
|
+
|
|
58
|
+
registerVariables(service: FrontendVariableService): void {
|
|
55
59
|
service.registerResolver(FILE_VARIABLE, this);
|
|
60
|
+
service.registerOpener(FILE_VARIABLE, this);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
async canResolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<number> {
|
|
@@ -60,21 +65,15 @@ export class FileVariableContribution implements AIVariableContribution, AIVaria
|
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
async resolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<ResolvedAIContextVariable | undefined> {
|
|
63
|
-
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
68
|
+
const uri = await this.toUri(request);
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
const absoluteUri = await this.makeAbsolute(path);
|
|
69
|
-
if (!absoluteUri) {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
70
|
+
if (!uri) { return undefined; }
|
|
72
71
|
|
|
73
72
|
try {
|
|
74
|
-
const content = await this.fileService.readFile(
|
|
73
|
+
const content = await this.fileService.readFile(uri);
|
|
75
74
|
return {
|
|
76
75
|
variable: request.variable,
|
|
77
|
-
value: await this.wsService.getWorkspaceRelativePath(
|
|
76
|
+
value: await this.wsService.getWorkspaceRelativePath(uri),
|
|
78
77
|
contextValue: content.value.toString(),
|
|
79
78
|
};
|
|
80
79
|
} catch (error) {
|
|
@@ -82,6 +81,27 @@ export class FileVariableContribution implements AIVariableContribution, AIVaria
|
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
84
|
+
protected async toUri(request: AIVariableResolutionRequest): Promise<URI | undefined> {
|
|
85
|
+
if (request.variable.name !== FILE_VARIABLE.name || request.arg === undefined) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const path = request.arg;
|
|
90
|
+
return this.makeAbsolute(path);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
canOpen(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<number> {
|
|
94
|
+
return this.canResolve(request, context);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async open(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<void> {
|
|
98
|
+
const uri = await this.toUri(request);
|
|
99
|
+
if (!uri) {
|
|
100
|
+
throw new Error('Unable to resolve URI for request.');
|
|
101
|
+
}
|
|
102
|
+
await open(this.openerService, uri);
|
|
103
|
+
}
|
|
104
|
+
|
|
85
105
|
protected async makeAbsolute(pathStr: string): Promise<URI | undefined> {
|
|
86
106
|
const path = new Path(Path.normalizePathSeparator(pathStr));
|
|
87
107
|
if (!path.isAbsolute) {
|
|
@@ -519,46 +519,93 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
async getCustomAgents(): Promise<CustomAgentDescription[]> {
|
|
522
|
-
const
|
|
522
|
+
const agentsById = new Map<string, CustomAgentDescription>();
|
|
523
|
+
// First, process additional (workspace) template directories to give them precedence
|
|
524
|
+
for (const dirPath of this.additionalTemplateDirs) {
|
|
525
|
+
const dirURI = URI.fromFilePath(dirPath);
|
|
526
|
+
await this.loadCustomAgentsFromDirectory(dirURI, agentsById);
|
|
527
|
+
}
|
|
528
|
+
// Then process global template directory (only adding agents that don't conflict)
|
|
529
|
+
const globalTemplateDir = await this.getTemplatesDirectoryURI();
|
|
530
|
+
await this.loadCustomAgentsFromDirectory(globalTemplateDir, agentsById);
|
|
531
|
+
// Return the merged list of agents
|
|
532
|
+
return Array.from(agentsById.values());
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Load custom agents from a specific directory
|
|
537
|
+
* @param directoryURI The URI of the directory to load from
|
|
538
|
+
* @param agentsById Map to store the loaded agents by ID
|
|
539
|
+
*/
|
|
540
|
+
protected async loadCustomAgentsFromDirectory(
|
|
541
|
+
directoryURI: URI,
|
|
542
|
+
agentsById: Map<string, CustomAgentDescription>
|
|
543
|
+
): Promise<void> {
|
|
544
|
+
const customAgentYamlUri = directoryURI.resolve('customAgents.yml');
|
|
523
545
|
const yamlExists = await this.fileService.exists(customAgentYamlUri);
|
|
524
546
|
if (!yamlExists) {
|
|
525
|
-
return
|
|
547
|
+
return;
|
|
526
548
|
}
|
|
527
|
-
|
|
549
|
+
|
|
528
550
|
try {
|
|
551
|
+
const fileContent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' });
|
|
529
552
|
const doc = load(fileContent.value);
|
|
553
|
+
|
|
530
554
|
if (!Array.isArray(doc) || !doc.every(entry => CustomAgentDescription.is(entry))) {
|
|
531
|
-
console.debug(
|
|
532
|
-
return
|
|
555
|
+
console.debug(`Invalid customAgents.yml file content in ${directoryURI.toString()}`);
|
|
556
|
+
return;
|
|
533
557
|
}
|
|
558
|
+
|
|
534
559
|
const readAgents = doc as CustomAgentDescription[];
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return;
|
|
560
|
+
|
|
561
|
+
// Add agents to the map if they don't already exist
|
|
562
|
+
for (const agent of readAgents) {
|
|
563
|
+
if (!agentsById.has(agent.id)) {
|
|
564
|
+
agentsById.set(agent.id, agent);
|
|
541
565
|
}
|
|
542
|
-
|
|
543
|
-
uniqueAgents.push(agent);
|
|
544
|
-
});
|
|
545
|
-
return uniqueAgents;
|
|
566
|
+
}
|
|
546
567
|
} catch (e) {
|
|
547
|
-
console.debug(e.message
|
|
548
|
-
return [];
|
|
568
|
+
console.debug(`Error loading customAgents.yml from ${directoryURI.toString()}: ${e.message}`, e);
|
|
549
569
|
}
|
|
550
570
|
}
|
|
551
571
|
|
|
552
|
-
|
|
553
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Returns all locations of existing customAgents.yml files and potential locations where
|
|
574
|
+
* new customAgents.yml files could be created.
|
|
575
|
+
*
|
|
576
|
+
* @returns An array of objects containing the URI and whether the file exists
|
|
577
|
+
*/
|
|
578
|
+
async getCustomAgentsLocations(): Promise<{ uri: URI, exists: boolean }[]> {
|
|
579
|
+
const locations: { uri: URI, exists: boolean }[] = [];
|
|
580
|
+
// Check global template directory
|
|
581
|
+
const globalTemplateDir = await this.getTemplatesDirectoryURI();
|
|
582
|
+
const globalAgentsUri = globalTemplateDir.resolve('customAgents.yml');
|
|
583
|
+
const globalExists = await this.fileService.exists(globalAgentsUri);
|
|
584
|
+
locations.push({ uri: globalAgentsUri, exists: globalExists });
|
|
585
|
+
// Check additional (workspace) template directories
|
|
586
|
+
for (const dirPath of this.additionalTemplateDirs) {
|
|
587
|
+
const dirURI = URI.fromFilePath(dirPath);
|
|
588
|
+
const agentsUri = dirURI.resolve('customAgents.yml');
|
|
589
|
+
const exists = await this.fileService.exists(agentsUri);
|
|
590
|
+
locations.push({ uri: agentsUri, exists: exists });
|
|
591
|
+
}
|
|
592
|
+
return locations;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Opens an existing customAgents.yml file at the given URI, or creates a new one if it doesn't exist.
|
|
597
|
+
*
|
|
598
|
+
* @param uri The URI of the customAgents.yml file to open or create
|
|
599
|
+
*/
|
|
600
|
+
async openCustomAgentYaml(uri: URI): Promise<void> {
|
|
554
601
|
const content = dump([templateEntry]);
|
|
555
|
-
if (! await this.fileService.exists(
|
|
556
|
-
await this.fileService.createFile(
|
|
602
|
+
if (! await this.fileService.exists(uri)) {
|
|
603
|
+
await this.fileService.createFile(uri, BinaryBuffer.fromString(content));
|
|
557
604
|
} else {
|
|
558
|
-
const fileContent = (await this.fileService.readFile(
|
|
559
|
-
await this.fileService.writeFile(
|
|
605
|
+
const fileContent = (await this.fileService.readFile(uri)).value;
|
|
606
|
+
await this.fileService.writeFile(uri, BinaryBuffer.concat([fileContent, BinaryBuffer.fromString(content)]));
|
|
560
607
|
}
|
|
561
|
-
const openHandler = await this.openerService.getOpener(
|
|
562
|
-
openHandler.open(
|
|
608
|
+
const openHandler = await this.openerService.getOpener(uri);
|
|
609
|
+
openHandler.open(uri);
|
|
563
610
|
}
|
|
564
611
|
}
|
|
@@ -14,10 +14,21 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { Disposable } from '@theia/core';
|
|
18
|
-
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
|
19
|
-
import { injectable } from '@theia/core/shared/inversify';
|
|
20
|
-
import {
|
|
17
|
+
import { Disposable, MessageService, Prioritizeable } from '@theia/core';
|
|
18
|
+
import { FrontendApplicationContribution, OpenerService, open } from '@theia/core/lib/browser';
|
|
19
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
20
|
+
import {
|
|
21
|
+
AIVariable,
|
|
22
|
+
AIVariableArg,
|
|
23
|
+
AIVariableContext,
|
|
24
|
+
AIVariableOpener,
|
|
25
|
+
AIVariableResolutionRequest,
|
|
26
|
+
AIVariableResourceResolver,
|
|
27
|
+
AIVariableService,
|
|
28
|
+
DefaultAIVariableService,
|
|
29
|
+
PromptText
|
|
30
|
+
} from '../common';
|
|
31
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
21
32
|
|
|
22
33
|
export type AIVariableDropHandler = (event: DragEvent, context: AIVariableContext) => Promise<AIVariableDropResult | undefined>;
|
|
23
34
|
|
|
@@ -26,11 +37,52 @@ export interface AIVariableDropResult {
|
|
|
26
37
|
text?: string
|
|
27
38
|
};
|
|
28
39
|
|
|
40
|
+
export interface AIVariableCompletionContext {
|
|
41
|
+
/** Portion of user input to be used for filtering completion candidates. */
|
|
42
|
+
userInput: string;
|
|
43
|
+
/** The range of suggestion completions. */
|
|
44
|
+
range: monaco.Range
|
|
45
|
+
/** A prefix to be applied to each completion item's text */
|
|
46
|
+
prefix: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export namespace AIVariableCompletionContext {
|
|
50
|
+
export function get(
|
|
51
|
+
variableName: string,
|
|
52
|
+
model: monaco.editor.ITextModel,
|
|
53
|
+
position: monaco.Position,
|
|
54
|
+
matchString?: string
|
|
55
|
+
): AIVariableCompletionContext | undefined {
|
|
56
|
+
const lineContent = model.getLineContent(position.lineNumber);
|
|
57
|
+
const indexOfVariableTrigger = lineContent.lastIndexOf(matchString ?? PromptText.VARIABLE_CHAR, position.column - 1);
|
|
58
|
+
|
|
59
|
+
// check if there is a variable trigger and no space typed between the variable trigger and the cursor
|
|
60
|
+
if (indexOfVariableTrigger === -1 || lineContent.substring(indexOfVariableTrigger).includes(' ')) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// determine whether we are providing completions before or after the variable argument separator
|
|
65
|
+
const indexOfVariableArgSeparator = lineContent.lastIndexOf(PromptText.VARIABLE_SEPARATOR_CHAR, position.column - 1);
|
|
66
|
+
const triggerCharIndex = Math.max(indexOfVariableTrigger, indexOfVariableArgSeparator);
|
|
67
|
+
|
|
68
|
+
const userInput = lineContent.substring(triggerCharIndex + 1, position.column - 1);
|
|
69
|
+
const range = new monaco.Range(position.lineNumber, triggerCharIndex + 2, position.lineNumber, position.column);
|
|
70
|
+
const matchVariableChar = lineContent[triggerCharIndex] === (matchString ? matchString : PromptText.VARIABLE_CHAR);
|
|
71
|
+
const prefix = matchVariableChar ? variableName + PromptText.VARIABLE_SEPARATOR_CHAR : '';
|
|
72
|
+
return { range, userInput, prefix };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
29
76
|
export const FrontendVariableService = Symbol('FrontendVariableService');
|
|
30
77
|
export interface FrontendVariableService extends AIVariableService {
|
|
31
78
|
registerDropHandler(handler: AIVariableDropHandler): Disposable;
|
|
32
79
|
unregisterDropHandler(handler: AIVariableDropHandler): void;
|
|
33
80
|
getDropResult(event: DragEvent, context: AIVariableContext): Promise<AIVariableDropResult>;
|
|
81
|
+
|
|
82
|
+
registerOpener(variable: AIVariable, opener: AIVariableOpener): Disposable;
|
|
83
|
+
unregisterOpener(variable: AIVariable, opener: AIVariableOpener): void;
|
|
84
|
+
getOpener(name: string, arg: string | undefined, context: AIVariableContext): Promise<AIVariableOpener | undefined>;
|
|
85
|
+
open(variable: AIVariableArg, context?: AIVariableContext): Promise<void>
|
|
34
86
|
}
|
|
35
87
|
|
|
36
88
|
export interface FrontendVariableContribution {
|
|
@@ -38,9 +90,13 @@ export interface FrontendVariableContribution {
|
|
|
38
90
|
}
|
|
39
91
|
|
|
40
92
|
@injectable()
|
|
41
|
-
export class DefaultFrontendVariableService extends DefaultAIVariableService implements FrontendApplicationContribution {
|
|
93
|
+
export class DefaultFrontendVariableService extends DefaultAIVariableService implements FrontendApplicationContribution, FrontendVariableService {
|
|
42
94
|
protected dropHandlers = new Set<AIVariableDropHandler>();
|
|
43
95
|
|
|
96
|
+
@inject(MessageService) protected readonly messageService: MessageService;
|
|
97
|
+
@inject(AIVariableResourceResolver) protected readonly aiResourceResolver: AIVariableResourceResolver;
|
|
98
|
+
@inject(OpenerService) protected readonly openerService: OpenerService;
|
|
99
|
+
|
|
44
100
|
onStart(): void {
|
|
45
101
|
this.initContributions();
|
|
46
102
|
}
|
|
@@ -68,4 +124,58 @@ export class DefaultFrontendVariableService extends DefaultAIVariableService imp
|
|
|
68
124
|
}
|
|
69
125
|
return { variables, text };
|
|
70
126
|
}
|
|
127
|
+
|
|
128
|
+
registerOpener(variable: AIVariable, opener: AIVariableOpener): Disposable {
|
|
129
|
+
const key = this.getKey(variable.name);
|
|
130
|
+
if (!this.variables.get(key)) {
|
|
131
|
+
this.variables.set(key, variable);
|
|
132
|
+
this.onDidChangeVariablesEmitter.fire();
|
|
133
|
+
}
|
|
134
|
+
const openers = this.openers.get(key) ?? [];
|
|
135
|
+
openers.push(opener);
|
|
136
|
+
this.openers.set(key, openers);
|
|
137
|
+
return Disposable.create(() => this.unregisterOpener(variable, opener));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
unregisterOpener(variable: AIVariable, opener: AIVariableOpener): void {
|
|
141
|
+
const key = this.getKey(variable.name);
|
|
142
|
+
const registeredOpeners = this.openers.get(key);
|
|
143
|
+
registeredOpeners?.splice(registeredOpeners.indexOf(opener), 1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getOpener(name: string, arg: string | undefined, context: AIVariableContext = {}): Promise<AIVariableOpener | undefined> {
|
|
147
|
+
const variable = this.getVariable(name);
|
|
148
|
+
return variable && Prioritizeable.prioritizeAll(
|
|
149
|
+
this.openers.get(this.getKey(name)) ?? [],
|
|
150
|
+
opener => (async () => opener.canOpen({ variable, arg }, context))().catch(() => 0)
|
|
151
|
+
)
|
|
152
|
+
.then(prioritized => prioritized.at(0)?.value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async open(request: AIVariableArg, context?: AIVariableContext | undefined): Promise<void> {
|
|
156
|
+
const { variableName, arg } = this.parseRequest(request);
|
|
157
|
+
const variable = this.getVariable(variableName);
|
|
158
|
+
if (!variable) {
|
|
159
|
+
this.messageService.warn('No variable found for open request.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const opener = await this.getOpener(variableName, arg, context);
|
|
163
|
+
try {
|
|
164
|
+
return opener ? opener.open({ variable, arg }, context ?? {}) : this.openReadonly({ variable, arg }, context);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error('Unable to open variable:', err);
|
|
167
|
+
this.messageService.error('Unable to display variable value.');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected async openReadonly(request: AIVariableResolutionRequest, context: AIVariableContext = {}): Promise<void> {
|
|
172
|
+
const resolved = await this.resolveVariable(request, context);
|
|
173
|
+
if (resolved === undefined) {
|
|
174
|
+
this.messageService.warn('Unable to resolve variable.');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const resource = this.aiResourceResolver.getOrCreate(request, context, resolved.value);
|
|
178
|
+
await open(this.openerService, resource.uri);
|
|
179
|
+
resource.dispose();
|
|
180
|
+
}
|
|
71
181
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
18
|
import { GrammarDefinition, GrammarDefinitionProvider, LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate';
|
|
19
19
|
import * as monaco from '@theia/monaco-editor-core';
|
|
20
|
-
import { Command, CommandContribution, CommandRegistry,
|
|
20
|
+
import { Command, CommandContribution, CommandRegistry, nls } from '@theia/core';
|
|
21
21
|
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
|
22
22
|
|
|
23
23
|
import { codicon, Widget } from '@theia/core/lib/browser';
|
|
@@ -33,17 +33,10 @@ export const PROMPT_TEMPLATE_EXTENSION = '.prompttemplate';
|
|
|
33
33
|
|
|
34
34
|
export const DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS: Command = Command.toLocalizedCommand({
|
|
35
35
|
id: 'theia-ai-prompt-template:discard',
|
|
36
|
+
label: 'Discard AI Prompt Template',
|
|
36
37
|
iconClass: codicon('discard'),
|
|
37
|
-
category: '
|
|
38
|
-
}, '', 'theia/ai/core/prompts/category');
|
|
39
|
-
|
|
40
|
-
// TODO this command is mainly for testing purposes
|
|
41
|
-
export const SHOW_ALL_PROMPTS_COMMAND: Command = Command.toLocalizedCommand({
|
|
42
|
-
id: 'theia-ai-prompt-template:show-prompts-command',
|
|
43
|
-
label: 'Show all prompts',
|
|
44
|
-
iconClass: codicon('beaker'),
|
|
45
|
-
category: 'Theia AI Prompt Templates',
|
|
46
|
-
}, 'theia/ai/core/showAllPrompts/label', 'theia/ai/core/prompts/category');
|
|
38
|
+
category: 'AI Prompt Templates'
|
|
39
|
+
}, 'theia/ai/core/discard/label', 'theia/ai/core/prompts/category');
|
|
47
40
|
|
|
48
41
|
@injectable()
|
|
49
42
|
export class PromptTemplateContribution implements LanguageGrammarDefinitionContribution, CommandContribution, TabBarToolbarContribution {
|
|
@@ -51,9 +44,6 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont
|
|
|
51
44
|
@inject(PromptService)
|
|
52
45
|
private readonly promptService: PromptService;
|
|
53
46
|
|
|
54
|
-
@inject(MessageService)
|
|
55
|
-
private readonly messageService: MessageService;
|
|
56
|
-
|
|
57
47
|
@inject(PromptCustomizationService)
|
|
58
48
|
protected readonly customizationService: PromptCustomizationService;
|
|
59
49
|
|
|
@@ -89,7 +79,7 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont
|
|
|
89
79
|
monaco.languages.register({
|
|
90
80
|
id: PROMPT_TEMPLATE_LANGUAGE_ID,
|
|
91
81
|
'aliases': [
|
|
92
|
-
'
|
|
82
|
+
'AI Prompt Template'
|
|
93
83
|
],
|
|
94
84
|
'extensions': [
|
|
95
85
|
PROMPT_TEMPLATE_EXTENSION,
|
|
@@ -253,10 +243,6 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont
|
|
|
253
243
|
isEnabled: (widget: EditorWidget) => this.canDiscard(widget),
|
|
254
244
|
execute: (widget: EditorWidget) => this.discard(widget)
|
|
255
245
|
});
|
|
256
|
-
|
|
257
|
-
commands.registerCommand(SHOW_ALL_PROMPTS_COMMAND, {
|
|
258
|
-
execute: () => this.showAllPrompts()
|
|
259
|
-
});
|
|
260
246
|
}
|
|
261
247
|
|
|
262
248
|
protected isPromptTemplateWidget(widget: Widget): boolean {
|
|
@@ -311,13 +297,6 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont
|
|
|
311
297
|
});
|
|
312
298
|
}
|
|
313
299
|
|
|
314
|
-
private showAllPrompts(): void {
|
|
315
|
-
const allPrompts = this.promptService.getAllPrompts();
|
|
316
|
-
Object.keys(allPrompts).forEach(id => {
|
|
317
|
-
this.messageService.info(`Prompt Template ID: ${id}\n${allPrompts[id].template}`, 'Got it');
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
300
|
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
|
322
301
|
registry.registerItem({
|
|
323
302
|
id: DISCARD_PROMPT_TEMPLATE_CUSTOMIZATIONS.id,
|
|
@@ -0,0 +1,117 @@
|
|
|
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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
18
|
+
import { Emitter } from '@theia/core';
|
|
19
|
+
import { ModelTokenUsageData, TokenUsageFrontendService } from './token-usage-frontend-service';
|
|
20
|
+
import { TokenUsage, TokenUsageService } from '../common/token-usage-service';
|
|
21
|
+
import { TokenUsageServiceClient } from '../common/protocol';
|
|
22
|
+
|
|
23
|
+
@injectable()
|
|
24
|
+
export class TokenUsageServiceClientImpl implements TokenUsageServiceClient {
|
|
25
|
+
private readonly _onTokenUsageUpdated = new Emitter<TokenUsage>();
|
|
26
|
+
readonly onTokenUsageUpdated = this._onTokenUsageUpdated.event;
|
|
27
|
+
|
|
28
|
+
notifyTokenUsage(usage: TokenUsage): void {
|
|
29
|
+
this._onTokenUsageUpdated.fire(usage);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@injectable()
|
|
35
|
+
export class TokenUsageFrontendServiceImpl implements TokenUsageFrontendService {
|
|
36
|
+
|
|
37
|
+
@inject(TokenUsageServiceClient)
|
|
38
|
+
protected readonly tokenUsageServiceClient: TokenUsageServiceClient;
|
|
39
|
+
|
|
40
|
+
@inject(TokenUsageService)
|
|
41
|
+
protected readonly tokenUsageService: TokenUsageService;
|
|
42
|
+
|
|
43
|
+
private readonly _onTokenUsageUpdated = new Emitter<ModelTokenUsageData[]>();
|
|
44
|
+
readonly onTokenUsageUpdated = this._onTokenUsageUpdated.event;
|
|
45
|
+
|
|
46
|
+
private cachedUsageData: ModelTokenUsageData[] = [];
|
|
47
|
+
|
|
48
|
+
@postConstruct()
|
|
49
|
+
protected init(): void {
|
|
50
|
+
this.tokenUsageServiceClient.onTokenUsageUpdated(() => {
|
|
51
|
+
this.getTokenUsageData().then(data => {
|
|
52
|
+
this._onTokenUsageUpdated.fire(data);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gets the current token usage data for all models
|
|
59
|
+
*/
|
|
60
|
+
async getTokenUsageData(): Promise<ModelTokenUsageData[]> {
|
|
61
|
+
try {
|
|
62
|
+
const usages = await this.tokenUsageService.getTokenUsages();
|
|
63
|
+
this.cachedUsageData = this.aggregateTokenUsages(usages);
|
|
64
|
+
return this.cachedUsageData;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Failed to get token usage data:', error);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Aggregates token usages by model
|
|
73
|
+
*/
|
|
74
|
+
private aggregateTokenUsages(usages: TokenUsage[]): ModelTokenUsageData[] {
|
|
75
|
+
// Group by model
|
|
76
|
+
const modelMap = new Map<string, {
|
|
77
|
+
inputTokens: number;
|
|
78
|
+
outputTokens: number;
|
|
79
|
+
lastUsed?: Date;
|
|
80
|
+
}>();
|
|
81
|
+
|
|
82
|
+
// Process each usage record
|
|
83
|
+
for (const usage of usages) {
|
|
84
|
+
const existing = modelMap.get(usage.model);
|
|
85
|
+
|
|
86
|
+
if (existing) {
|
|
87
|
+
existing.inputTokens += usage.inputTokens;
|
|
88
|
+
existing.outputTokens += usage.outputTokens;
|
|
89
|
+
|
|
90
|
+
// Update last used if this usage is more recent
|
|
91
|
+
if (!existing.lastUsed || (usage.timestamp && usage.timestamp > existing.lastUsed)) {
|
|
92
|
+
existing.lastUsed = usage.timestamp;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
modelMap.set(usage.model, {
|
|
96
|
+
inputTokens: usage.inputTokens,
|
|
97
|
+
outputTokens: usage.outputTokens,
|
|
98
|
+
lastUsed: usage.timestamp
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Convert map to array of model usage data
|
|
104
|
+
const result: ModelTokenUsageData[] = [];
|
|
105
|
+
|
|
106
|
+
for (const [modelId, data] of modelMap.entries()) {
|
|
107
|
+
result.push({
|
|
108
|
+
modelId,
|
|
109
|
+
inputTokens: data.inputTokens,
|
|
110
|
+
outputTokens: data.outputTokens,
|
|
111
|
+
lastUsed: data.lastUsed
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { Event } from '@theia/core';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Data structure for token usage data specific to a model.
|
|
21
|
+
*/
|
|
22
|
+
export interface ModelTokenUsageData {
|
|
23
|
+
/** The model identifier */
|
|
24
|
+
modelId: string;
|
|
25
|
+
/** Number of input tokens used */
|
|
26
|
+
inputTokens: number;
|
|
27
|
+
/** Number of output tokens used */
|
|
28
|
+
outputTokens: number;
|
|
29
|
+
/** Date when the model was last used */
|
|
30
|
+
lastUsed?: Date;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Service for managing token usage data on the frontend.
|
|
35
|
+
*/
|
|
36
|
+
export const TokenUsageFrontendService = Symbol('TokenUsageFrontendService');
|
|
37
|
+
export interface TokenUsageFrontendService {
|
|
38
|
+
/**
|
|
39
|
+
* Event emitted when token usage data is updated
|
|
40
|
+
*/
|
|
41
|
+
readonly onTokenUsageUpdated: Event<ModelTokenUsageData[]>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets the current token usage data for all models
|
|
45
|
+
*/
|
|
46
|
+
getTokenUsageData(): Promise<ModelTokenUsageData[]>;
|
|
47
|
+
}
|