@theia/ai-chat 1.56.0 → 1.57.0-next.112

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 (49) hide show
  1. package/README.md +2 -1
  2. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-frontend-module.js +14 -4
  4. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  5. package/lib/browser/change-set-file-element.d.ts +44 -0
  6. package/lib/browser/change-set-file-element.d.ts.map +1 -0
  7. package/lib/browser/change-set-file-element.js +113 -0
  8. package/lib/browser/change-set-file-element.js.map +1 -0
  9. package/lib/browser/change-set-file-resource.d.ts +13 -0
  10. package/lib/browser/change-set-file-resource.d.ts.map +1 -0
  11. package/lib/browser/change-set-file-resource.js +73 -0
  12. package/lib/browser/change-set-file-resource.js.map +1 -0
  13. package/lib/browser/change-set-file-service.d.ts +26 -0
  14. package/lib/browser/change-set-file-service.d.ts.map +1 -0
  15. package/lib/browser/change-set-file-service.js +139 -0
  16. package/lib/browser/change-set-file-service.js.map +1 -0
  17. package/lib/common/chat-agents.d.ts +4 -5
  18. package/lib/common/chat-agents.d.ts.map +1 -1
  19. package/lib/common/chat-agents.js +14 -21
  20. package/lib/common/chat-agents.js.map +1 -1
  21. package/lib/common/chat-model.d.ts +117 -7
  22. package/lib/common/chat-model.d.ts.map +1 -1
  23. package/lib/common/chat-model.js +124 -8
  24. package/lib/common/chat-model.js.map +1 -1
  25. package/lib/common/chat-service.d.ts +6 -0
  26. package/lib/common/chat-service.d.ts.map +1 -1
  27. package/lib/common/chat-service.js +12 -0
  28. package/lib/common/chat-service.js.map +1 -1
  29. package/lib/common/chat-tool-request-service.d.ts +17 -0
  30. package/lib/common/chat-tool-request-service.d.ts.map +1 -0
  31. package/lib/common/chat-tool-request-service.js +52 -0
  32. package/lib/common/chat-tool-request-service.js.map +1 -0
  33. package/lib/common/command-chat-agents.js +1 -1
  34. package/lib/common/command-chat-agents.js.map +1 -1
  35. package/package.json +10 -8
  36. package/src/browser/ai-chat-frontend-module.ts +17 -6
  37. package/src/browser/change-set-file-element.ts +137 -0
  38. package/src/browser/change-set-file-resource.ts +74 -0
  39. package/src/browser/change-set-file-service.ts +136 -0
  40. package/src/common/chat-agents.ts +12 -24
  41. package/src/common/chat-model.ts +236 -14
  42. package/src/common/chat-service.ts +17 -0
  43. package/src/common/chat-tool-request-service.ts +59 -0
  44. package/src/common/command-chat-agents.ts +1 -1
  45. package/lib/common/o1-chat-agent.d.ts +0 -13
  46. package/lib/common/o1-chat-agent.d.ts.map +0 -1
  47. package/lib/common/o1-chat-agent.js +0 -45
  48. package/lib/common/o1-chat-agent.js.map +0 -1
  49. package/src/common/o1-chat-agent.ts +0 -51
@@ -0,0 +1,136 @@
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 { ILogger, UNTITLED_SCHEME, URI } from '@theia/core';
18
+ import { DiffUris, LabelProvider, OpenerService, open } from '@theia/core/lib/browser';
19
+ import { inject, injectable } from '@theia/core/shared/inversify';
20
+ import { EditorManager } from '@theia/editor/lib/browser';
21
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
22
+ import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
23
+ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
24
+ import { ChangeSetFileElement } from './change-set-file-element';
25
+
26
+ @injectable()
27
+ export class ChangeSetFileService {
28
+ @inject(ILogger)
29
+ protected readonly logger: ILogger;
30
+
31
+ @inject(WorkspaceService)
32
+ protected readonly wsService: WorkspaceService;
33
+
34
+ @inject(LabelProvider)
35
+ protected readonly labelProvider: LabelProvider;
36
+
37
+ @inject(OpenerService)
38
+ protected readonly openerService: OpenerService;
39
+
40
+ @inject(EditorManager)
41
+ protected readonly editorManager: EditorManager;
42
+
43
+ @inject(MonacoWorkspace)
44
+ protected readonly monacoWorkspace: MonacoWorkspace;
45
+
46
+ @inject(FileService)
47
+ protected readonly fileService: FileService;
48
+
49
+ async read(uri: URI): Promise<string | undefined> {
50
+ const exists = await this.fileService.exists(uri);
51
+ if (!exists) {
52
+ return undefined;
53
+ }
54
+ try {
55
+ const document = this.monacoWorkspace.getTextDocument(uri.toString());
56
+ if (document) {
57
+ return document.getText();
58
+ }
59
+ return (await this.fileService.readFile(uri)).value.toString();
60
+ } catch (error) {
61
+ this.logger.error('Failed to read original content of change set file element.', error);
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ getName(uri: URI): string {
67
+ return this.labelProvider.getName(uri);
68
+ }
69
+
70
+ getIcon(uri: URI): string | undefined {
71
+ return this.labelProvider.getIcon(uri);
72
+ }
73
+
74
+ getAdditionalInfo(uri: URI): string | undefined {
75
+ const wsUri = this.wsService.getWorkspaceRootUri(uri);
76
+ if (wsUri) {
77
+ const wsRelative = wsUri.relative(uri);
78
+ if (wsRelative?.hasDir) {
79
+ return `${wsRelative.dir.toString()}`;
80
+ }
81
+ return '';
82
+ }
83
+ return this.labelProvider.getLongName(uri.parent);
84
+ }
85
+
86
+ async open(element: ChangeSetFileElement): Promise<void> {
87
+ const exists = await this.fileService.exists(element.uri);
88
+ if (exists) {
89
+ await open(this.openerService, element.uri);
90
+ return;
91
+ }
92
+ await this.editorManager.open(element.changedUri, {
93
+ mode: 'reveal'
94
+ });
95
+ }
96
+
97
+ async openDiff(originalUri: URI, suggestedUri: URI): Promise<void> {
98
+ const exists = await this.fileService.exists(originalUri);
99
+ const openedUri = exists ? originalUri : originalUri.withScheme(UNTITLED_SCHEME);
100
+ // Currently we don't have a great way to show the suggestions in a diff editor with accept/reject buttons
101
+ // So we just use plain diffs with the suggestions as original and the current state as modified, so users can apply changes in their current state
102
+ // But this leads to wrong colors and wrong label (revert change instead of accept change)
103
+ const diffUri = DiffUris.encode(openedUri, suggestedUri,
104
+ `AI Changes: ${this.labelProvider.getName(originalUri)}`,
105
+ );
106
+ open(this.openerService, diffUri);
107
+ }
108
+
109
+ async delete(uri: URI): Promise<void> {
110
+ const exists = await this.fileService.exists(uri);
111
+ if (exists) {
112
+ await this.fileService.delete(uri);
113
+ }
114
+ }
115
+
116
+ async write(uri: URI, targetState: string): Promise<void> {
117
+ const exists = await this.fileService.exists(uri);
118
+ if (!exists) {
119
+ await this.fileService.create(uri, targetState);
120
+ }
121
+ await this.doWrite(uri, targetState);
122
+ }
123
+
124
+ protected async doWrite(uri: URI, text: string): Promise<void> {
125
+ const document = this.monacoWorkspace.getTextDocument(uri.toString());
126
+ if (document) {
127
+ await this.monacoWorkspace.applyBackgroundEdit(document, [{
128
+ range: document.textEditorModel.getFullModelRange(),
129
+ text
130
+ }], (editor, wasDirty) => editor === undefined || !wasDirty);
131
+ } else {
132
+ await this.fileService.write(uri, text);
133
+ }
134
+ }
135
+
136
+ }
@@ -38,12 +38,11 @@ import {
38
38
  LanguageModelStreamResponsePart,
39
39
  MessageActor,
40
40
  } from '@theia/ai-core/lib/common';
41
- import { CancellationToken, CancellationTokenSource, ContributionProvider, ILogger, isArray } from '@theia/core';
41
+ import { CancellationToken, ContributionProvider, ILogger, isArray } from '@theia/core';
42
42
  import { inject, injectable, named, postConstruct, unmanaged } from '@theia/core/shared/inversify';
43
43
  import { ChatAgentService } from './chat-agent-service';
44
44
  import {
45
45
  ChatModel,
46
- ChatRequestModel,
47
46
  ChatRequestModelImpl,
48
47
  ChatResponseContent,
49
48
  ErrorChatResponseContentImpl,
@@ -53,6 +52,7 @@ import {
53
52
  import { findFirstMatch, parseContents } from './parse-contents';
54
53
  import { DefaultResponseContentFactory, ResponseContentMatcher, ResponseContentMatcherProvider } from './response-content-matcher';
55
54
  import { ChatHistoryEntry } from './chat-history-entry';
55
+ import { ChatToolRequestService } from './chat-tool-request-service';
56
56
 
57
57
  /**
58
58
  * A conversation consists of a sequence of ChatMessages.
@@ -123,10 +123,12 @@ export abstract class AbstractChatAgent {
123
123
  @inject(LanguageModelRegistry) protected languageModelRegistry: LanguageModelRegistry;
124
124
  @inject(ILogger) protected logger: ILogger;
125
125
  @inject(CommunicationRecordingService) protected recordingService: CommunicationRecordingService;
126
+ @inject(ChatToolRequestService) protected chatToolRequestService: ChatToolRequestService;
126
127
  @inject(PromptService) protected promptService: PromptService;
127
128
 
128
129
  @inject(ContributionProvider) @named(ResponseContentMatcherProvider)
129
130
  protected contentMatcherProviders: ContributionProvider<ResponseContentMatcherProvider>;
131
+ protected additionalToolRequests: ToolRequest[] = [];
130
132
  protected contentMatchers: ResponseContentMatcher[] = [];
131
133
 
132
134
  @inject(DefaultResponseContentFactory)
@@ -171,7 +173,6 @@ export abstract class AbstractChatAgent {
171
173
  );
172
174
  }
173
175
 
174
- const tools: Map<string, ToolRequest> = new Map();
175
176
  if (systemMessageDescription) {
176
177
  const systemMsg: ChatMessage = {
177
178
  actor: 'system',
@@ -180,24 +181,20 @@ export abstract class AbstractChatAgent {
180
181
  };
181
182
  // insert system message at the beginning of the request messages
182
183
  messages.unshift(systemMsg);
183
- systemMessageDescription.functionDescriptions?.forEach((tool, id) => {
184
- tools.set(id, tool);
185
- });
186
184
  }
187
- this.getTools(request)?.forEach(tool => tools.set(tool.id, tool));
188
185
 
189
- const cancellationToken = new CancellationTokenSource();
190
- request.response.onDidChange(() => {
191
- if (request.response.isCanceled) {
192
- cancellationToken.cancel();
193
- }
194
- });
186
+ const systemMessageToolRequests = systemMessageDescription?.functionDescriptions?.values();
187
+ const tools = [
188
+ ...this.chatToolRequestService.getChatToolRequests(request),
189
+ ...this.chatToolRequestService.toChatToolRequests(systemMessageToolRequests ? Array.from(systemMessageToolRequests) : [], request),
190
+ ...this.chatToolRequestService.toChatToolRequests(this.additionalToolRequests, request)
191
+ ];
195
192
 
196
193
  const languageModelResponse = await this.callLlm(
197
194
  languageModel,
198
195
  messages,
199
- tools.size > 0 ? Array.from(tools.values()) : undefined,
200
- cancellationToken.token
196
+ tools.length > 0 ? tools : undefined,
197
+ request.response.cancellationToken
201
198
  );
202
199
  await this.addContentsToResponse(languageModelResponse, request);
203
200
  await this.onResponseComplete(request);
@@ -265,15 +262,6 @@ export abstract class AbstractChatAgent {
265
262
  return requestMessages;
266
263
  }
267
264
 
268
- /**
269
- * @returns the list of tools used by this agent, or undefined if none is needed.
270
- */
271
- protected getTools(request: ChatRequestModel): ToolRequest[] | undefined {
272
- return request.message.toolRequests.size > 0
273
- ? [...request.message.toolRequests.values()]
274
- : undefined;
275
- }
276
-
277
265
  protected async callLlm(
278
266
  languageModel: LanguageModel,
279
267
  messages: ChatMessage[],
@@ -19,11 +19,12 @@
19
19
  *--------------------------------------------------------------------------------------------*/
20
20
  // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts
21
21
 
22
- import { Command, Emitter, Event, generateUuid, URI } from '@theia/core';
22
+ import { CancellationToken, CancellationTokenSource, Command, Disposable, Emitter, Event, generateUuid, URI } from '@theia/core';
23
23
  import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
24
24
  import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
25
25
  import { ChatAgentLocation } from './chat-agents';
26
- import { ParsedChatRequest } from './parsed-chat-request';
26
+ import { ParsedChatRequest, ParsedChatRequestVariablePart } from './parsed-chat-request';
27
+ import { ResolvedAIVariable } from '@theia/ai-core';
27
28
 
28
29
  /**********************
29
30
  * INTERFACES AND TYPE GUARDS
@@ -32,7 +33,11 @@ import { ParsedChatRequest } from './parsed-chat-request';
32
33
  export type ChatChangeEvent =
33
34
  | ChatAddRequestEvent
34
35
  | ChatAddResponseEvent
35
- | ChatRemoveRequestEvent;
36
+ | ChatRemoveRequestEvent
37
+ | ChatSetChangeSetEvent
38
+ | ChatSetChangeDeleteEvent
39
+ | ChatUpdateChangeSetEvent
40
+ | ChatRemoveChangeSetEvent;
36
41
 
37
42
  export interface ChatAddRequestEvent {
38
43
  kind: 'addRequest';
@@ -44,6 +49,31 @@ export interface ChatAddResponseEvent {
44
49
  response: ChatResponseModel;
45
50
  }
46
51
 
52
+ export interface ChatSetChangeSetEvent {
53
+ kind: 'setChangeSet';
54
+ changeSet: ChangeSet;
55
+ }
56
+
57
+ export interface ChatSetChangeDeleteEvent {
58
+ kind: 'deleteChangeSet';
59
+ }
60
+
61
+ export interface ChatUpdateChangeSetEvent {
62
+ kind: 'updateChangeSet';
63
+ changeSet: ChangeSet;
64
+ }
65
+
66
+ export interface ChatRemoveChangeSetEvent {
67
+ kind: 'removeChangeSet';
68
+ changeSet: ChangeSet;
69
+ }
70
+
71
+ export namespace ChatChangeEvent {
72
+ export function isChangeSetEvent(event: ChatChangeEvent): event is ChatSetChangeSetEvent | ChatUpdateChangeSetEvent | ChatRemoveChangeSetEvent {
73
+ return event.kind === 'setChangeSet' || event.kind === 'deleteChangeSet' || event.kind === 'removeChangeSet' || event.kind === 'updateChangeSet';
74
+ }
75
+ }
76
+
47
77
  export type ChatRequestRemovalReason = 'removal' | 'resend' | 'adoption';
48
78
 
49
79
  export interface ChatRemoveRequestEvent {
@@ -57,10 +87,33 @@ export interface ChatModel {
57
87
  readonly onDidChange: Event<ChatChangeEvent>;
58
88
  readonly id: string;
59
89
  readonly location: ChatAgentLocation;
90
+ readonly changeSet?: ChangeSet;
60
91
  getRequests(): ChatRequestModel[];
61
92
  isEmpty(): boolean;
62
93
  }
63
94
 
95
+ export interface ChangeSet {
96
+ readonly title: string;
97
+ getElements(): ChangeSetElement[];
98
+ }
99
+
100
+ export interface ChangeSetElement {
101
+ readonly uri: URI;
102
+
103
+ readonly name?: string;
104
+ readonly icon?: string;
105
+ readonly additionalInfo?: string;
106
+
107
+ readonly state?: 'pending' | 'applied' | 'discarded';
108
+ readonly type?: 'add' | 'modify' | 'delete';
109
+ readonly data?: { [key: string]: unknown };
110
+
111
+ open?(): Promise<void>;
112
+ openChange?(): Promise<void>;
113
+ accept?(): Promise<void>;
114
+ discard?(): Promise<void>;
115
+ }
116
+
64
117
  export interface ChatRequest {
65
118
  readonly text: string;
66
119
  readonly displayText?: string;
@@ -76,6 +129,32 @@ export interface ChatRequestModel {
76
129
  readonly data?: { [key: string]: unknown };
77
130
  }
78
131
 
132
+ export namespace ChatRequestModel {
133
+ export function is(request: unknown): request is ChatRequestModel {
134
+ return !!(
135
+ request &&
136
+ typeof request === 'object' &&
137
+ 'id' in request &&
138
+ typeof (request as { id: unknown }).id === 'string' &&
139
+ 'session' in request &&
140
+ 'request' in request &&
141
+ 'response' in request &&
142
+ 'message' in request
143
+ );
144
+ }
145
+ export function isInProgress(request: ChatRequestModel | undefined): boolean {
146
+ if (!request) {
147
+ return false;
148
+ }
149
+ const response = request.response;
150
+ return !(
151
+ response.isComplete ||
152
+ response.isCanceled ||
153
+ response.isError
154
+ );
155
+ }
156
+ }
157
+
79
158
  export interface ChatProgressMessage {
80
159
  kind: 'progressMessage';
81
160
  id: string;
@@ -321,18 +400,59 @@ export interface ChatResponse {
321
400
  asString(): string;
322
401
  }
323
402
 
403
+ /**
404
+ * The ChatResponseModel wraps the actual ChatResponse with additional information like the current state, progress messages, a unique id etc.
405
+ */
324
406
  export interface ChatResponseModel {
407
+ /**
408
+ * Use this to be notified for any change in the response model
409
+ */
325
410
  readonly onDidChange: Event<void>;
411
+ /**
412
+ * The unique identifier of the response model
413
+ */
326
414
  readonly id: string;
415
+ /**
416
+ * The unique identifier of the request model this response is associated with
417
+ */
327
418
  readonly requestId: string;
419
+ /**
420
+ * In case there are progress messages, then they will be stored here
421
+ */
328
422
  readonly progressMessages: ChatProgressMessage[];
423
+ /**
424
+ * The actual response content
425
+ */
329
426
  readonly response: ChatResponse;
427
+ /**
428
+ * Indicates whether this response is complete. No further changes are expected if 'true'.
429
+ */
330
430
  readonly isComplete: boolean;
431
+ /**
432
+ * Indicates whether this response is canceled. No further changes are expected if 'true'.
433
+ */
331
434
  readonly isCanceled: boolean;
435
+ /**
436
+ * Some agents might need to wait for user input to continue. This flag indicates that.
437
+ */
332
438
  readonly isWaitingForInput: boolean;
439
+ /**
440
+ * Indicates whether an error occurred when processing the response. No further changes are expected if 'true'.
441
+ */
333
442
  readonly isError: boolean;
443
+ /**
444
+ * The agent who produced the response content, if there is one.
445
+ */
334
446
  readonly agentId?: string
447
+ /**
448
+ * An optional error object that caused the response to be in an error state.
449
+ */
335
450
  readonly errorObject?: Error;
451
+ /**
452
+ * Some functionality might want to store some data associated with the response.
453
+ * This can be used to store and retrieve such data.
454
+ */
455
+ readonly data: { [key: string]: unknown };
336
456
  }
337
457
 
338
458
  /**********************
@@ -345,6 +465,8 @@ export class ChatModelImpl implements ChatModel {
345
465
 
346
466
  protected _requests: ChatRequestModelImpl[];
347
467
  protected _id: string;
468
+ protected _changeSetListener?: Disposable;
469
+ protected _changeSet?: ChangeSetImpl;
348
470
 
349
471
  constructor(public readonly location = ChatAgentLocation.Panel) {
350
472
  // TODO accept serialized data as a parameter to restore a previously saved ChatModel
@@ -356,12 +478,52 @@ export class ChatModelImpl implements ChatModel {
356
478
  return this._requests;
357
479
  }
358
480
 
481
+ getRequest(id: string): ChatRequestModelImpl | undefined {
482
+ return this._requests.find(request => request.id === id);
483
+ }
484
+
359
485
  get id(): string {
360
486
  return this._id;
361
487
  }
362
488
 
363
- addRequest(parsedChatRequest: ParsedChatRequest, agentId?: string): ChatRequestModelImpl {
364
- const requestModel = new ChatRequestModelImpl(this, parsedChatRequest, agentId);
489
+ get changeSet(): ChangeSetImpl | undefined {
490
+ return this._changeSet;
491
+ }
492
+
493
+ setChangeSet(changeSet: ChangeSetImpl | undefined): void {
494
+ this._changeSet = changeSet;
495
+ if (this._changeSet === undefined) {
496
+ this._changeSetListener?.dispose();
497
+ this._onDidChangeEmitter.fire({
498
+ kind: 'deleteChangeSet',
499
+ });
500
+ return;
501
+ }
502
+ this._onDidChangeEmitter.fire({
503
+ kind: 'setChangeSet',
504
+ changeSet: this._changeSet,
505
+ });
506
+ this._changeSetListener = this._changeSet.onDidChange(() => {
507
+ this._onDidChangeEmitter.fire({
508
+ kind: 'updateChangeSet',
509
+ changeSet: this._changeSet!,
510
+ });
511
+ });
512
+ }
513
+
514
+ removeChangeSet(): void {
515
+ if (this._changeSet) {
516
+ const oldChangeSet = this._changeSet;
517
+ this._changeSet = undefined;
518
+ this._onDidChangeEmitter.fire({
519
+ kind: 'removeChangeSet',
520
+ changeSet: oldChangeSet,
521
+ });
522
+ }
523
+ }
524
+
525
+ addRequest(parsedChatRequest: ParsedChatRequest, agentId?: string, context: ResolvedAIVariable[] = []): ChatRequestModelImpl {
526
+ const requestModel = new ChatRequestModelImpl(this, parsedChatRequest, agentId, context);
365
527
  this._requests.push(requestModel);
366
528
  this._onDidChangeEmitter.fire({
367
529
  kind: 'addRequest',
@@ -375,21 +537,72 @@ export class ChatModelImpl implements ChatModel {
375
537
  }
376
538
  }
377
539
 
540
+ export class ChangeSetImpl implements ChangeSet {
541
+ protected readonly _onDidChangeEmitter = new Emitter<void>();
542
+ onDidChange: Event<void> = this._onDidChangeEmitter.event;
543
+
544
+ protected _elements: ChangeSetElement[] = [];
545
+
546
+ constructor(public readonly title: string, elements: ChangeSetElement[] = []) {
547
+ this.addElements(elements);
548
+ }
549
+
550
+ getElements(): ChangeSetElement[] {
551
+ return this._elements;
552
+ }
553
+
554
+ addElement(element: ChangeSetElement): void {
555
+ this.addElements([element]);
556
+ }
557
+
558
+ addElements(elements: ChangeSetElement[]): void {
559
+ this._elements.push(...elements);
560
+ this.notifyChange();
561
+ }
562
+
563
+ replaceElement(element: ChangeSetElement): boolean {
564
+ const index = this._elements.findIndex(e => e.uri.toString() === element.uri.toString());
565
+ if (index < 0) {
566
+ return false;
567
+ }
568
+ this._elements[index] = element;
569
+ this.notifyChange();
570
+ return true;
571
+ }
572
+
573
+ addOrReplaceElement(element: ChangeSetElement): void {
574
+ if (!this.replaceElement(element)) {
575
+ this.addElement(element);
576
+ }
577
+ }
578
+
579
+ removeElement(index: number): void {
580
+ this._elements.splice(index, 1);
581
+ this.notifyChange();
582
+ }
583
+
584
+ notifyChange(): void {
585
+ this._onDidChangeEmitter.fire();
586
+ }
587
+ }
588
+
378
589
  export class ChatRequestModelImpl implements ChatRequestModel {
379
590
  protected readonly _id: string;
380
- protected _session: ChatModel;
591
+ protected _session: ChatModelImpl;
381
592
  protected _request: ChatRequest;
382
593
  protected _response: ChatResponseModelImpl;
594
+ protected _context: ResolvedAIVariable[];
383
595
  protected _agentId?: string;
384
596
  protected _data: { [key: string]: unknown };
385
597
 
386
- constructor(session: ChatModel, public readonly message: ParsedChatRequest, agentId?: string,
387
- data: { [key: string]: unknown } = {}) {
598
+ constructor(session: ChatModelImpl, public readonly message: ParsedChatRequest, agentId?: string,
599
+ context: ResolvedAIVariable[] = [], data: { [key: string]: unknown } = {}) {
388
600
  // TODO accept serialized data as a parameter to restore a previously saved ChatRequestModel
389
601
  this._request = message.request;
390
602
  this._id = generateUuid();
391
603
  this._session = session;
392
604
  this._response = new ChatResponseModelImpl(this._id, agentId);
605
+ this._context = context.concat(message.parts.filter(part => part.kind === 'var').map(part => (part as ParsedChatRequestVariablePart).resolution));
393
606
  this._agentId = agentId;
394
607
  this._data = data;
395
608
  }
@@ -410,7 +623,7 @@ export class ChatRequestModelImpl implements ChatRequestModel {
410
623
  return this._id;
411
624
  }
412
625
 
413
- get session(): ChatModel {
626
+ get session(): ChatModelImpl {
414
627
  return this._session;
415
628
  }
416
629
 
@@ -425,6 +638,10 @@ export class ChatRequestModelImpl implements ChatRequestModel {
425
638
  get agentId(): string | undefined {
426
639
  return this._agentId;
427
640
  }
641
+
642
+ cancel(): void {
643
+ this.response.cancel();
644
+ }
428
645
  }
429
646
 
430
647
  export class ErrorChatResponseContentImpl implements ErrorChatResponseContent {
@@ -750,16 +967,18 @@ class ChatResponseModelImpl implements ChatResponseModel {
750
967
  protected readonly _onDidChangeEmitter = new Emitter<void>();
751
968
  onDidChange: Event<void> = this._onDidChangeEmitter.event;
752
969
 
970
+ data = {};
971
+
753
972
  protected _id: string;
754
973
  protected _requestId: string;
755
974
  protected _progressMessages: ChatProgressMessage[];
756
975
  protected _response: ChatResponseImpl;
757
976
  protected _isComplete: boolean;
758
- protected _isCanceled: boolean;
759
977
  protected _isWaitingForInput: boolean;
760
978
  protected _agentId?: string;
761
979
  protected _isError: boolean;
762
980
  protected _errorObject: Error | undefined;
981
+ protected _cancellationToken: CancellationTokenSource;
763
982
 
764
983
  constructor(requestId: string, agentId?: string) {
765
984
  // TODO accept serialized data as a parameter to restore a previously saved ChatResponseModel
@@ -770,9 +989,9 @@ class ChatResponseModelImpl implements ChatResponseModel {
770
989
  response.onDidChange(() => this._onDidChangeEmitter.fire());
771
990
  this._response = response;
772
991
  this._isComplete = false;
773
- this._isCanceled = false;
774
992
  this._isWaitingForInput = false;
775
993
  this._agentId = agentId;
994
+ this._cancellationToken = new CancellationTokenSource();
776
995
  }
777
996
 
778
997
  get id(): string {
@@ -827,7 +1046,7 @@ class ChatResponseModelImpl implements ChatResponseModel {
827
1046
  }
828
1047
 
829
1048
  get isCanceled(): boolean {
830
- return this._isCanceled;
1049
+ return this._cancellationToken.token.isCancellationRequested;
831
1050
  }
832
1051
 
833
1052
  get isWaitingForInput(): boolean {
@@ -849,12 +1068,16 @@ class ChatResponseModelImpl implements ChatResponseModel {
849
1068
  }
850
1069
 
851
1070
  cancel(): void {
1071
+ this._cancellationToken.cancel();
852
1072
  this._isComplete = true;
853
- this._isCanceled = true;
854
1073
  this._isWaitingForInput = false;
855
1074
  this._onDidChangeEmitter.fire();
856
1075
  }
857
1076
 
1077
+ get cancellationToken(): CancellationToken {
1078
+ return this._cancellationToken.token;
1079
+ }
1080
+
858
1081
  waitForInput(): void {
859
1082
  this._isWaitingForInput = true;
860
1083
  this._onDidChangeEmitter.fire();
@@ -867,7 +1090,6 @@ class ChatResponseModelImpl implements ChatResponseModel {
867
1090
 
868
1091
  error(error: Error): void {
869
1092
  this._isComplete = true;
870
- this._isCanceled = false;
871
1093
  this._isWaitingForInput = false;
872
1094
  this._isError = true;
873
1095
  this._errorObject = error;
@@ -86,6 +86,11 @@ export interface ChatService {
86
86
  sessionId: string,
87
87
  request: ChatRequest
88
88
  ): Promise<ChatRequestInvocation | undefined>;
89
+
90
+ deleteChangeSet(sessionId: string): void;
91
+ deleteChangeSetElement(sessionId: string, index: number): void;
92
+
93
+ cancelRequest(sessionId: string, requestId: string): Promise<void>;
89
94
  }
90
95
 
91
96
  interface ChatSessionInternal extends ChatSession {
@@ -219,6 +224,10 @@ export class ChatServiceImpl implements ChatService {
219
224
  return invocation;
220
225
  }
221
226
 
227
+ async cancelRequest(sessionId: string, requestId: string): Promise<void> {
228
+ return this.getSession(sessionId)?.model.getRequest(requestId)?.response.cancel();
229
+ }
230
+
222
231
  protected getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined {
223
232
  const agentPart = this.getMentionedAgent(parsedRequest);
224
233
  if (agentPart) {
@@ -233,4 +242,12 @@ export class ChatServiceImpl implements ChatService {
233
242
  protected getMentionedAgent(parsedRequest: ParsedChatRequest): ParsedChatRequestAgentPart | undefined {
234
243
  return parsedRequest.parts.find(p => p instanceof ParsedChatRequestAgentPart) as ParsedChatRequestAgentPart | undefined;
235
244
  }
245
+
246
+ deleteChangeSet(sessionId: string): void {
247
+ this.getSession(sessionId)?.model.setChangeSet(undefined);
248
+ }
249
+
250
+ deleteChangeSetElement(sessionId: string, index: number): void {
251
+ this.getSession(sessionId)?.model.changeSet?.removeElement(index);
252
+ }
236
253
  }