@theia/ai-core 1.61.0-next.8 → 1.61.1

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 (70) hide show
  1. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-core-frontend-module.js +7 -0
  3. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  4. package/lib/browser/ai-variable-uri-label-provider.d.ts +17 -0
  5. package/lib/browser/ai-variable-uri-label-provider.d.ts.map +1 -0
  6. package/lib/browser/ai-variable-uri-label-provider.js +85 -0
  7. package/lib/browser/ai-variable-uri-label-provider.js.map +1 -0
  8. package/lib/browser/file-variable-contribution.d.ts +9 -3
  9. package/lib/browser/file-variable-contribution.d.ts.map +1 -1
  10. package/lib/browser/file-variable-contribution.js +26 -8
  11. package/lib/browser/file-variable-contribution.js.map +1 -1
  12. package/lib/browser/frontend-prompt-customization-service.d.ts +22 -1
  13. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  14. package/lib/browser/frontend-prompt-customization-service.js +63 -25
  15. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  16. package/lib/browser/frontend-variable-service.d.ts +28 -4
  17. package/lib/browser/frontend-variable-service.d.ts.map +1 -1
  18. package/lib/browser/frontend-variable-service.js +84 -1
  19. package/lib/browser/frontend-variable-service.js.map +1 -1
  20. package/lib/browser/token-usage-frontend-service-impl.d.ts.map +1 -1
  21. package/lib/browser/token-usage-frontend-service-impl.js +20 -2
  22. package/lib/browser/token-usage-frontend-service-impl.js.map +1 -1
  23. package/lib/browser/token-usage-frontend-service.d.ts +4 -0
  24. package/lib/browser/token-usage-frontend-service.d.ts.map +1 -1
  25. package/lib/browser/token-usage-frontend-service.js.map +1 -1
  26. package/lib/common/ai-variable-resource.d.ts +18 -0
  27. package/lib/common/ai-variable-resource.d.ts.map +1 -0
  28. package/lib/common/ai-variable-resource.js +103 -0
  29. package/lib/common/ai-variable-resource.js.map +1 -0
  30. package/lib/common/configurable-in-memory-resources.d.ts +45 -0
  31. package/lib/common/configurable-in-memory-resources.d.ts.map +1 -0
  32. package/lib/common/configurable-in-memory-resources.js +147 -0
  33. package/lib/common/configurable-in-memory-resources.js.map +1 -0
  34. package/lib/common/index.d.ts +2 -0
  35. package/lib/common/index.d.ts.map +1 -1
  36. package/lib/common/index.js +2 -0
  37. package/lib/common/index.js.map +1 -1
  38. package/lib/common/prompt-service.d.ts +16 -4
  39. package/lib/common/prompt-service.d.ts.map +1 -1
  40. package/lib/common/prompt-service.js +1 -1
  41. package/lib/common/prompt-service.js.map +1 -1
  42. package/lib/common/prompt-variable-contribution.d.ts +2 -1
  43. package/lib/common/prompt-variable-contribution.d.ts.map +1 -1
  44. package/lib/common/prompt-variable-contribution.js +10 -1
  45. package/lib/common/prompt-variable-contribution.js.map +1 -1
  46. package/lib/common/token-usage-service.d.ts +8 -0
  47. package/lib/common/token-usage-service.d.ts.map +1 -1
  48. package/lib/common/variable-service.d.ts +17 -2
  49. package/lib/common/variable-service.d.ts.map +1 -1
  50. package/lib/common/variable-service.js +43 -32
  51. package/lib/common/variable-service.js.map +1 -1
  52. package/lib/node/token-usage-service-impl.d.ts.map +1 -1
  53. package/lib/node/token-usage-service-impl.js +14 -1
  54. package/lib/node/token-usage-service-impl.js.map +1 -1
  55. package/package.json +11 -10
  56. package/src/browser/ai-core-frontend-module.ts +12 -5
  57. package/src/browser/ai-variable-uri-label-provider.ts +66 -0
  58. package/src/browser/file-variable-contribution.ts +34 -14
  59. package/src/browser/frontend-prompt-customization-service.ts +72 -25
  60. package/src/browser/frontend-variable-service.ts +115 -5
  61. package/src/browser/token-usage-frontend-service-impl.ts +27 -2
  62. package/src/browser/token-usage-frontend-service.ts +4 -0
  63. package/src/common/ai-variable-resource.ts +86 -0
  64. package/src/common/configurable-in-memory-resources.ts +156 -0
  65. package/src/common/index.ts +2 -0
  66. package/src/common/prompt-service.ts +14 -4
  67. package/src/common/prompt-variable-contribution.ts +10 -2
  68. package/src/common/token-usage-service.ts +8 -0
  69. package/src/common/variable-service.ts +58 -44
  70. package/src/node/token-usage-service-impl.ts +19 -1
@@ -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
- registerVariables(service: AIVariableService): void {
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
- if (request.variable.name !== FILE_VARIABLE.name || request.arg === undefined) {
64
- return undefined;
65
- }
68
+ const uri = await this.toUri(request);
66
69
 
67
- const path = request.arg;
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(absoluteUri);
73
+ const content = await this.fileService.readFile(uri);
75
74
  return {
76
75
  variable: request.variable,
77
- value: await this.wsService.getWorkspaceRelativePath(absoluteUri),
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 customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml');
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
- const fileContent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' });
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('Invalid customAgents.yml file content');
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
- // make sure all agents are unique (id and name)
536
- const uniqueAgentIds = new Set<string>();
537
- const uniqueAgents: CustomAgentDescription[] = [];
538
- readAgents.forEach(agent => {
539
- if (uniqueAgentIds.has(agent.id)) {
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
- uniqueAgentIds.add(agent.id);
543
- uniqueAgents.push(agent);
544
- });
545
- return uniqueAgents;
566
+ }
546
567
  } catch (e) {
547
- console.debug(e.message, e);
548
- return [];
568
+ console.debug(`Error loading customAgents.yml from ${directoryURI.toString()}: ${e.message}`, e);
549
569
  }
550
570
  }
551
571
 
552
- async openCustomAgentYaml(): Promise<void> {
553
- const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml');
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(customAgentYamlUri)) {
556
- await this.fileService.createFile(customAgentYamlUri, BinaryBuffer.fromString(content));
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(customAgentYamlUri)).value;
559
- await this.fileService.writeFile(customAgentYamlUri, BinaryBuffer.concat([fileContent, BinaryBuffer.fromString(content)]));
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(customAgentYamlUri);
562
- openHandler.open(customAgentYamlUri);
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 { AIVariableContext, AIVariableResolutionRequest, AIVariableService, DefaultAIVariableService } from '../common';
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
  }
@@ -76,6 +76,8 @@ export class TokenUsageFrontendServiceImpl implements TokenUsageFrontendService
76
76
  const modelMap = new Map<string, {
77
77
  inputTokens: number;
78
78
  outputTokens: number;
79
+ cachedInputTokens: number;
80
+ readCachedInputTokens: number;
79
81
  lastUsed?: Date;
80
82
  }>();
81
83
 
@@ -87,6 +89,16 @@ export class TokenUsageFrontendServiceImpl implements TokenUsageFrontendService
87
89
  existing.inputTokens += usage.inputTokens;
88
90
  existing.outputTokens += usage.outputTokens;
89
91
 
92
+ // Add cached tokens if they exist
93
+ if (usage.cachedInputTokens !== undefined) {
94
+ existing.cachedInputTokens += usage.cachedInputTokens;
95
+ }
96
+
97
+ // Add read cached tokens if they exist
98
+ if (usage.readCachedInputTokens !== undefined) {
99
+ existing.readCachedInputTokens += usage.readCachedInputTokens;
100
+ }
101
+
90
102
  // Update last used if this usage is more recent
91
103
  if (!existing.lastUsed || (usage.timestamp && usage.timestamp > existing.lastUsed)) {
92
104
  existing.lastUsed = usage.timestamp;
@@ -95,6 +107,8 @@ export class TokenUsageFrontendServiceImpl implements TokenUsageFrontendService
95
107
  modelMap.set(usage.model, {
96
108
  inputTokens: usage.inputTokens,
97
109
  outputTokens: usage.outputTokens,
110
+ cachedInputTokens: usage.cachedInputTokens || 0,
111
+ readCachedInputTokens: usage.readCachedInputTokens || 0,
98
112
  lastUsed: usage.timestamp
99
113
  });
100
114
  }
@@ -104,12 +118,23 @@ export class TokenUsageFrontendServiceImpl implements TokenUsageFrontendService
104
118
  const result: ModelTokenUsageData[] = [];
105
119
 
106
120
  for (const [modelId, data] of modelMap.entries()) {
107
- result.push({
121
+ const modelData: ModelTokenUsageData = {
108
122
  modelId,
109
123
  inputTokens: data.inputTokens,
110
124
  outputTokens: data.outputTokens,
111
125
  lastUsed: data.lastUsed
112
- });
126
+ };
127
+
128
+ // Only include cache-related fields if they have non-zero values
129
+ if (data.cachedInputTokens > 0) {
130
+ modelData.cachedInputTokens = data.cachedInputTokens;
131
+ }
132
+
133
+ if (data.readCachedInputTokens > 0) {
134
+ modelData.readCachedInputTokens = data.readCachedInputTokens;
135
+ }
136
+
137
+ result.push(modelData);
113
138
  }
114
139
 
115
140
  return result;
@@ -26,6 +26,10 @@ export interface ModelTokenUsageData {
26
26
  inputTokens: number;
27
27
  /** Number of output tokens used */
28
28
  outputTokens: number;
29
+ /** Number of input tokens written to cache */
30
+ cachedInputTokens?: number;
31
+ /** Number of input tokens read from cache */
32
+ readCachedInputTokens?: number;
29
33
  /** Date when the model was last used */
30
34
  lastUsed?: Date;
31
35
  }
@@ -0,0 +1,86 @@
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 * as deepEqual from 'fast-deep-equal';
18
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
19
+ import { Resource, URI, generateUuid } from '@theia/core';
20
+ import { AIVariableContext, AIVariableResolutionRequest } from './variable-service';
21
+ import stableJsonStringify = require('fast-json-stable-stringify');
22
+ import { ConfigurableInMemoryResources, ConfigurableMutableReferenceResource } from './configurable-in-memory-resources';
23
+
24
+ export const AI_VARIABLE_RESOURCE_SCHEME = 'ai-variable';
25
+ export const NO_CONTEXT_AUTHORITY = 'context-free';
26
+
27
+ @injectable()
28
+ export class AIVariableResourceResolver {
29
+ @inject(ConfigurableInMemoryResources) protected readonly inMemoryResources: ConfigurableInMemoryResources;
30
+
31
+ @postConstruct()
32
+ protected init(): void {
33
+ this.inMemoryResources.onWillDispose(resource => this.cache.delete(resource.uri.toString()));
34
+ }
35
+
36
+ protected readonly cache = new Map<string, [Resource, AIVariableContext]>();
37
+
38
+ getOrCreate(request: AIVariableResolutionRequest, context: AIVariableContext, value: string): ConfigurableMutableReferenceResource {
39
+ const uri = this.toUri(request, context);
40
+ try {
41
+ const existing = this.inMemoryResources.resolve(uri);
42
+ existing.update({ contents: value });
43
+ return existing;
44
+ } catch { /* No-op */ }
45
+ const fresh = this.inMemoryResources.add(uri, { contents: value, readOnly: true, initiallyDirty: false });
46
+ const key = uri.toString();
47
+ this.cache.set(key, [fresh, context]);
48
+ return fresh;
49
+ }
50
+
51
+ protected toUri(request: AIVariableResolutionRequest, context: AIVariableContext): URI {
52
+ return URI.fromComponents({
53
+ scheme: AI_VARIABLE_RESOURCE_SCHEME,
54
+ query: stableJsonStringify({ arg: request.arg, name: request.variable.name }),
55
+ path: '/',
56
+ authority: this.toAuthority(context),
57
+ fragment: ''
58
+ });
59
+ }
60
+
61
+ protected toAuthority(context: AIVariableContext): string {
62
+ try {
63
+ if (deepEqual(context, {})) { return NO_CONTEXT_AUTHORITY; }
64
+ for (const [resource, cachedContext] of this.cache.values()) {
65
+ if (deepEqual(context, cachedContext)) {
66
+ return resource.uri.authority;
67
+ }
68
+ }
69
+ } catch (err) {
70
+ // Mostly that deep equal could overflow the stack, but it should run into === or inequality before that.
71
+ console.warn('Problem evaluating context in AIVariableResourceResolver', err);
72
+ }
73
+ return generateUuid();
74
+ }
75
+
76
+ fromUri(uri: URI): { variableName: string, arg: string | undefined } | undefined {
77
+ if (uri.scheme !== AI_VARIABLE_RESOURCE_SCHEME) { return undefined; }
78
+ try {
79
+ const { name: variableName, arg } = JSON.parse(uri.query);
80
+ return variableName ? {
81
+ variableName,
82
+ arg,
83
+ } : undefined;
84
+ } catch { return undefined; }
85
+ }
86
+ }