@theia/ai-chat 1.61.0-next.8 → 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.
Files changed (98) hide show
  1. package/lib/browser/ai-chat-frontend-contribution.d.ts +11 -0
  2. package/lib/browser/ai-chat-frontend-contribution.d.ts.map +1 -0
  3. package/lib/browser/ai-chat-frontend-contribution.js +56 -0
  4. package/lib/browser/ai-chat-frontend-contribution.js.map +1 -0
  5. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-frontend-module.js +21 -3
  7. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  8. package/lib/browser/change-set-decorator-service.d.ts +24 -0
  9. package/lib/browser/change-set-decorator-service.d.ts.map +1 -0
  10. package/lib/browser/change-set-decorator-service.js +66 -0
  11. package/lib/browser/change-set-decorator-service.js.map +1 -0
  12. package/lib/browser/change-set-file-element.d.ts +7 -4
  13. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  14. package/lib/browser/change-set-file-element.js +20 -12
  15. package/lib/browser/change-set-file-element.js.map +1 -1
  16. package/lib/browser/change-set-file-resource.d.ts +1 -42
  17. package/lib/browser/change-set-file-resource.d.ts.map +1 -1
  18. package/lib/browser/change-set-file-resource.js +1 -136
  19. package/lib/browser/change-set-file-resource.js.map +1 -1
  20. package/lib/browser/change-set-variable.d.ts.map +1 -1
  21. package/lib/browser/change-set-variable.js +13 -4
  22. package/lib/browser/change-set-variable.js.map +1 -1
  23. package/lib/browser/context-file-variable-label-provider.js +1 -1
  24. package/lib/browser/context-file-variable-label-provider.js.map +1 -1
  25. package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
  26. package/lib/browser/file-chat-variable-contribution.js +29 -27
  27. package/lib/browser/file-chat-variable-contribution.js.map +1 -1
  28. package/lib/browser/task-context-service.d.ts +40 -0
  29. package/lib/browser/task-context-service.d.ts.map +1 -0
  30. package/lib/browser/task-context-service.js +148 -0
  31. package/lib/browser/task-context-service.js.map +1 -0
  32. package/lib/browser/task-context-storage-service.d.ts +18 -0
  33. package/lib/browser/task-context-storage-service.d.ts.map +1 -0
  34. package/lib/browser/task-context-storage-service.js +77 -0
  35. package/lib/browser/task-context-storage-service.js.map +1 -0
  36. package/lib/browser/task-context-variable-contribution.d.ts +20 -0
  37. package/lib/browser/task-context-variable-contribution.d.ts.map +1 -0
  38. package/lib/browser/task-context-variable-contribution.js +101 -0
  39. package/lib/browser/task-context-variable-contribution.js.map +1 -0
  40. package/lib/browser/task-context-variable-label-provider.d.ts +21 -0
  41. package/lib/browser/task-context-variable-label-provider.d.ts.map +1 -0
  42. package/lib/browser/task-context-variable-label-provider.js +83 -0
  43. package/lib/browser/task-context-variable-label-provider.js.map +1 -0
  44. package/lib/browser/task-context-variable.d.ts +3 -0
  45. package/lib/browser/task-context-variable.d.ts.map +1 -0
  46. package/lib/browser/task-context-variable.js +29 -0
  47. package/lib/browser/task-context-variable.js.map +1 -0
  48. package/lib/common/chat-agents.d.ts +2 -1
  49. package/lib/common/chat-agents.d.ts.map +1 -1
  50. package/lib/common/chat-agents.js +5 -0
  51. package/lib/common/chat-agents.js.map +1 -1
  52. package/lib/common/chat-model.d.ts +173 -8
  53. package/lib/common/chat-model.d.ts.map +1 -1
  54. package/lib/common/chat-model.js +329 -17
  55. package/lib/common/chat-model.js.map +1 -1
  56. package/lib/common/chat-request-parser.d.ts +1 -1
  57. package/lib/common/chat-request-parser.d.ts.map +1 -1
  58. package/lib/common/chat-request-parser.js +1 -1
  59. package/lib/common/chat-request-parser.js.map +1 -1
  60. package/lib/common/chat-service.d.ts +2 -0
  61. package/lib/common/chat-service.d.ts.map +1 -1
  62. package/lib/common/chat-service.js +18 -25
  63. package/lib/common/chat-service.js.map +1 -1
  64. package/lib/common/chat-session-naming-service.d.ts.map +1 -1
  65. package/lib/common/chat-session-naming-service.js +3 -3
  66. package/lib/common/chat-session-naming-service.js.map +1 -1
  67. package/lib/common/chat-session-summary-agent-prompt.d.ts +5 -0
  68. package/lib/common/chat-session-summary-agent-prompt.d.ts.map +1 -0
  69. package/lib/common/chat-session-summary-agent-prompt.js +30 -0
  70. package/lib/common/chat-session-summary-agent-prompt.js.map +1 -0
  71. package/lib/common/chat-session-summary-agent.d.ts +17 -0
  72. package/lib/common/chat-session-summary-agent.d.ts.map +1 -0
  73. package/lib/common/chat-session-summary-agent.js +48 -0
  74. package/lib/common/chat-session-summary-agent.js.map +1 -0
  75. package/lib/common/context-summary-variable.js +1 -1
  76. package/lib/common/context-summary-variable.js.map +1 -1
  77. package/package.json +11 -11
  78. package/src/browser/ai-chat-frontend-contribution.ts +49 -0
  79. package/src/browser/ai-chat-frontend-module.ts +25 -4
  80. package/src/browser/change-set-decorator-service.ts +72 -0
  81. package/src/browser/change-set-file-element.ts +18 -13
  82. package/src/browser/change-set-file-resource.ts +1 -138
  83. package/src/browser/change-set-variable.ts +14 -6
  84. package/src/browser/context-file-variable-label-provider.ts +1 -1
  85. package/src/browser/file-chat-variable-contribution.ts +26 -29
  86. package/src/browser/task-context-service.ts +144 -0
  87. package/src/browser/task-context-storage-service.ts +75 -0
  88. package/src/browser/task-context-variable-contribution.ts +93 -0
  89. package/src/browser/task-context-variable-label-provider.ts +67 -0
  90. package/src/browser/task-context-variable.ts +28 -0
  91. package/src/common/chat-agents.ts +6 -1
  92. package/src/common/chat-model.ts +507 -18
  93. package/src/common/chat-request-parser.ts +2 -2
  94. package/src/common/chat-service.ts +17 -26
  95. package/src/common/chat-session-naming-service.ts +4 -3
  96. package/src/common/chat-session-summary-agent-prompt.ts +28 -0
  97. package/src/common/chat-session-summary-agent.ts +42 -0
  98. package/src/common/context-summary-variable.ts +1 -1
@@ -0,0 +1,72 @@
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 { ContributionProvider, Emitter, type Event } from '@theia/core';
18
+ import { type FrontendApplicationContribution } from '@theia/core/lib/browser';
19
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
20
+ import debounce = require('@theia/core/shared/lodash.debounce');
21
+ import type { ChangeSetDecoration, ChangeSetElement } from '../common';
22
+
23
+ /**
24
+ * A decorator for a change set element.
25
+ * It allows to add additional information to the element, such as icons.
26
+ */
27
+ export const ChangeSetDecorator = Symbol('ChangeSetDecorator');
28
+ export interface ChangeSetDecorator {
29
+ readonly id: string;
30
+
31
+ readonly onDidChangeDecorations: Event<void>;
32
+
33
+ decorate(element: ChangeSetElement): ChangeSetDecoration | undefined;
34
+ }
35
+
36
+ @injectable()
37
+ export class ChangeSetDecoratorService implements FrontendApplicationContribution {
38
+
39
+ protected readonly onDidChangeDecorationsEmitter = new Emitter<void>();
40
+ readonly onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event;
41
+
42
+ @inject(ContributionProvider) @named(ChangeSetDecorator)
43
+ protected readonly contributions: ContributionProvider<ChangeSetDecorator>;
44
+
45
+ initialize(): void {
46
+ this.contributions.getContributions().map(decorator => decorator.onDidChangeDecorations(this.fireDidChangeDecorations));
47
+ }
48
+
49
+ protected readonly fireDidChangeDecorations = debounce(() => {
50
+ this.onDidChangeDecorationsEmitter.fire(undefined);
51
+ }, 150);
52
+
53
+ getDecorations(element: ChangeSetElement): ChangeSetDecoration[] {
54
+ const decorators = this.contributions.getContributions();
55
+ const decorations: ChangeSetDecoration[] = [];
56
+ for (const decorator of decorators) {
57
+ const decoration = decorator.decorate(element);
58
+ if (decoration) {
59
+ decorations.push(decoration);
60
+ }
61
+ }
62
+
63
+ decorations.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
64
+
65
+ return decorations;
66
+ }
67
+
68
+ getAdditionalInfoSuffixIcon(element: ChangeSetElement): string[] | undefined {
69
+ const decorations = this.getDecorations(element);
70
+ return decorations.find(d => d.additionalInfoSuffixIcon)?.additionalInfoSuffixIcon;
71
+ }
72
+ }
@@ -17,11 +17,13 @@
17
17
  import { DisposableCollection, Emitter, URI } from '@theia/core';
18
18
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
19
19
  import { Replacement } from '@theia/core/lib/common/content-replacer';
20
+ import { ConfigurableInMemoryResources, ConfigurableMutableReferenceResource } from '@theia/ai-core';
20
21
  import { ChangeSetElement, ChangeSetImpl } from '../common';
21
- import { ChangeSetFileResourceResolver, createChangeSetFileUri, UpdatableReferenceResource } from './change-set-file-resource';
22
+ import { createChangeSetFileUri } from './change-set-file-resource';
22
23
  import { ChangeSetFileService } from './change-set-file-service';
23
24
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
24
25
  import { ConfirmDialog } from '@theia/core/lib/browser';
26
+ import { ChangeSetDecoratorService } from './change-set-decorator-service';
25
27
 
26
28
  export const ChangeSetFileElementFactory = Symbol('ChangeSetFileElementFactory');
27
29
  export type ChangeSetFileElementFactory = (elementProps: ChangeSetElementArgs) => ChangeSetFileElement;
@@ -60,11 +62,14 @@ export class ChangeSetFileElement implements ChangeSetElement {
60
62
  @inject(ChangeSetFileService)
61
63
  protected readonly changeSetFileService: ChangeSetFileService;
62
64
 
65
+ @inject(ChangeSetDecoratorService)
66
+ protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
67
+
63
68
  @inject(FileService)
64
69
  protected readonly fileService: FileService;
65
70
 
66
- @inject(ChangeSetFileResourceResolver)
67
- protected readonly resourceResolver: ChangeSetFileResourceResolver;
71
+ @inject(ConfigurableInMemoryResources)
72
+ protected readonly inMemoryResources: ConfigurableInMemoryResources;
68
73
 
69
74
  protected readonly toDispose = new DisposableCollection();
70
75
  protected _state: ChangeSetElementState;
@@ -73,8 +78,8 @@ export class ChangeSetFileElement implements ChangeSetElement {
73
78
 
74
79
  protected readonly onDidChangeEmitter = new Emitter<void>();
75
80
  readonly onDidChange = this.onDidChangeEmitter.event;
76
- protected readOnlyResource: UpdatableReferenceResource;
77
- protected changeResource: UpdatableReferenceResource;
81
+ protected readOnlyResource: ConfigurableMutableReferenceResource;
82
+ protected changeResource: ConfigurableMutableReferenceResource;
78
83
 
79
84
  @postConstruct()
80
85
  init(): void {
@@ -93,17 +98,17 @@ export class ChangeSetFileElement implements ChangeSetElement {
93
98
  }
94
99
 
95
100
  protected getResources(): void {
96
- this.readOnlyResource = this.resourceResolver.tryGet(this.readOnlyUri) ?? this.resourceResolver.add(this.readOnlyUri, { autosaveable: false, readOnly: true });
97
- let changed = this.resourceResolver.tryGet(this.changedUri);
98
- if (changed) {
99
- changed.update({ contents: this.targetState, onSave: content => this.writeChanges(content) });
100
- } else {
101
- changed = this.resourceResolver.add(this.changedUri, { contents: this.targetState, onSave: content => this.writeChanges(content), autosaveable: false });
102
- }
103
- this.changeResource = changed;
101
+ this.readOnlyResource = this.getInMemoryUri(this.readOnlyUri);
102
+ this.readOnlyResource.update({ autosaveable: false, readOnly: true });
103
+ this.changeResource = this.getInMemoryUri(this.changedUri);
104
+ this.changeResource.update({ contents: this.targetState, onSave: content => this.writeChanges(content), autosaveable: false });
104
105
  this.toDispose.pushAll([this.readOnlyResource, this.changeResource]);
105
106
  }
106
107
 
108
+ protected getInMemoryUri(uri: URI): ConfigurableMutableReferenceResource {
109
+ try { return this.inMemoryResources.resolve(uri); } catch { return this.inMemoryResources.add(uri, { contents: '' }); }
110
+ }
111
+
107
112
  protected listenForOriginalFileChanges(): void {
108
113
  this.toDispose.push(this.fileService.onDidFilesChange(async event => {
109
114
  if (!event.contains(this.uri)) { return; }
@@ -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 { MutableResource, Reference, ReferenceMutableResource, Resource, ResourceResolver, URI } from '@theia/core';
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: 'changeSetSummary',
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: 'changeSetSummary',
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 || !context.model.changeSet?.getElements().length) { return undefined; }
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: entries.join('\n')
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.changeSetFileService.getAdditionalInfo(this.getUri(element)!);
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 lineContent = model.getLineContent(position.lineNumber);
76
- const indexOfVariableTrigger = lineContent.lastIndexOf(matchString ?? PromptText.VARIABLE_CHAR, position.column - 1);
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
- // check if there is a variable trigger and no space typed between the variable trigger and the cursor
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 => !typedWord || p.highlights?.label)
98
- .map(async (pick, index) => ({
99
- label: pick.label,
100
- kind: monaco.languages.CompletionItemKind.File,
101
- range,
102
- insertText: `${prefix}${await this.wsService.getWorkspaceRelativePath(pick.uri)}`,
103
- detail: await this.wsService.getWorkspaceRelativePath(pick.uri.parent),
104
- // don't let monaco filter the items, as we only return picks that are filtered
105
- filterText: typedWord,
106
- // keep the order of the items, but move them to the end of the list
107
- sortText: `ZZ${index.toString().padStart(4, '0')}_${pick.label}`,
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
+ }