@theia/ai-chat 1.62.1 → 1.63.0-next.24

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 (46) hide show
  1. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-frontend-module.js +7 -1
  3. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  4. package/lib/browser/change-set-file-element.d.ts +9 -6
  5. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  6. package/lib/browser/change-set-file-element.js +27 -14
  7. package/lib/browser/change-set-file-element.js.map +1 -1
  8. package/lib/browser/change-set-variable.js +1 -2
  9. package/lib/browser/change-set-variable.js.map +1 -1
  10. package/lib/browser/chat-tool-preferences.d.ts +54 -0
  11. package/lib/browser/chat-tool-preferences.d.ts.map +1 -0
  12. package/lib/browser/chat-tool-preferences.js +170 -0
  13. package/lib/browser/chat-tool-preferences.js.map +1 -0
  14. package/lib/browser/chat-tool-request-service.d.ts +20 -0
  15. package/lib/browser/chat-tool-request-service.d.ts.map +1 -0
  16. package/lib/browser/chat-tool-request-service.js +89 -0
  17. package/lib/browser/chat-tool-request-service.js.map +1 -0
  18. package/lib/browser/frontend-chat-service.d.ts.map +1 -1
  19. package/lib/browser/frontend-chat-service.js +2 -6
  20. package/lib/browser/frontend-chat-service.js.map +1 -1
  21. package/lib/common/change-set.d.ts +78 -0
  22. package/lib/common/change-set.d.ts.map +1 -0
  23. package/lib/common/change-set.js +133 -0
  24. package/lib/common/change-set.js.map +1 -0
  25. package/lib/common/chat-agents.d.ts.map +1 -1
  26. package/lib/common/chat-agents.js +4 -1
  27. package/lib/common/chat-agents.js.map +1 -1
  28. package/lib/common/chat-model.d.ts +68 -80
  29. package/lib/common/chat-model.d.ts.map +1 -1
  30. package/lib/common/chat-model.js +224 -136
  31. package/lib/common/chat-model.js.map +1 -1
  32. package/lib/common/chat-service.d.ts +3 -3
  33. package/lib/common/chat-service.d.ts.map +1 -1
  34. package/lib/common/chat-service.js +5 -8
  35. package/lib/common/chat-service.js.map +1 -1
  36. package/package.json +10 -10
  37. package/src/browser/ai-chat-frontend-module.ts +8 -1
  38. package/src/browser/change-set-file-element.ts +33 -21
  39. package/src/browser/change-set-variable.ts +1 -1
  40. package/src/browser/chat-tool-preferences.ts +178 -0
  41. package/src/browser/chat-tool-request-service.ts +93 -0
  42. package/src/browser/frontend-chat-service.ts +3 -6
  43. package/src/common/change-set.ts +197 -0
  44. package/src/common/chat-agents.ts +15 -11
  45. package/src/common/chat-model.ts +249 -207
  46. package/src/common/chat-service.ts +6 -9
@@ -20,11 +20,14 @@
20
20
  // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts
21
21
 
22
22
  import { AIVariableResolutionRequest, LanguageModelMessage, ResolvedAIContextVariable, TextMessage, ThinkingMessage, ToolResultMessage, ToolUseMessage } from '@theia/ai-core';
23
- import { CancellationToken, CancellationTokenSource, Command, Disposable, DisposableCollection, Emitter, Event, generateUuid, URI } from '@theia/core';
23
+ import { ArrayUtils, CancellationToken, CancellationTokenSource, Command, Disposable, DisposableCollection, Emitter, Event, generateUuid, URI } from '@theia/core';
24
24
  import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
25
25
  import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
26
26
  import { ChatAgentLocation } from './chat-agents';
27
27
  import { ParsedChatRequest } from './parsed-chat-request';
28
+ import { ChangeSet, ChangeSetImpl, ChangeSetElement, ChatUpdateChangeSetEvent } from './change-set';
29
+ import debounce = require('@theia/core/shared/lodash.debounce');
30
+ export { ChangeSet, ChangeSetImpl, ChangeSetElement };
28
31
 
29
32
  /**********************
30
33
  * INTERFACES AND TYPE GUARDS
@@ -37,10 +40,8 @@ export type ChatChangeEvent =
37
40
  | ChatRemoveVariableEvent
38
41
  | ChatSetVariablesEvent
39
42
  | ChatRemoveRequestEvent
40
- | ChatSetChangeSetEvent
41
43
  | ChatSuggestionsChangedEvent
42
44
  | ChatUpdateChangeSetEvent
43
- | ChatRemoveChangeSetEvent
44
45
  | ChatEditRequestEvent
45
46
  | ChatEditCancelEvent
46
47
  | ChatEditSubmitEvent
@@ -80,22 +81,6 @@ export interface ChatAddResponseEvent {
80
81
  response: ChatResponseModel;
81
82
  }
82
83
 
83
- export interface ChatSetChangeSetEvent {
84
- kind: 'setChangeSet';
85
- changeSet: ChangeSet;
86
- oldChangeSet?: ChangeSet;
87
- }
88
-
89
- export interface ChatUpdateChangeSetEvent {
90
- kind: 'updateChangeSet';
91
- changeSet: ChangeSet;
92
- }
93
-
94
- export interface ChatRemoveChangeSetEvent {
95
- kind: 'removeChangeSet';
96
- changeSet: ChangeSet;
97
- }
98
-
99
84
  export interface ChatAddVariableEvent {
100
85
  kind: 'addVariable';
101
86
  }
@@ -114,8 +99,8 @@ export interface ChatSuggestionsChangedEvent {
114
99
  }
115
100
 
116
101
  export namespace ChatChangeEvent {
117
- export function isChangeSetEvent(event: ChatChangeEvent): event is ChatSetChangeSetEvent | ChatUpdateChangeSetEvent | ChatRemoveChangeSetEvent {
118
- return event.kind === 'setChangeSet' || event.kind === 'removeChangeSet' || event.kind === 'updateChangeSet';
102
+ export function isChangeSetEvent(event: ChatChangeEvent): event is ChatUpdateChangeSetEvent {
103
+ return event.kind === 'updateChangeSet';
119
104
  }
120
105
  }
121
106
 
@@ -136,7 +121,7 @@ export interface ChatRemoveRequestEvent {
136
121
  * - Within each branch, the requests are stored in a list. Those requests are the alternatives to the original request.
137
122
  * Each of those items can have a next branch, which is the next request in the hierarchy.
138
123
  */
139
- export interface ChatRequestHierarchy<TRequest extends ChatRequestModel = ChatRequestModel> {
124
+ export interface ChatRequestHierarchy<TRequest extends ChatRequestModel = ChatRequestModel> extends Disposable {
140
125
  readonly branch: ChatHierarchyBranch<TRequest>
141
126
 
142
127
  onDidChange: Event<ChangeActiveBranchEvent<TRequest>>;
@@ -160,7 +145,7 @@ export interface ChangeActiveBranchEvent<TRequest extends ChatRequestModel = Cha
160
145
  * It contains a list of items, each representing a request.
161
146
  * Those items can have a next branch, which is the next request in the hierarchy.
162
147
  */
163
- export interface ChatHierarchyBranch<TRequest extends ChatRequestModel = ChatRequestModel> {
148
+ export interface ChatHierarchyBranch<TRequest extends ChatRequestModel = ChatRequestModel> extends Disposable {
164
149
  readonly id: string;
165
150
  readonly hierarchy: ChatRequestHierarchy<TRequest>;
166
151
  readonly previous?: ChatHierarchyBranch<TRequest>;
@@ -192,28 +177,15 @@ export interface ChatModel {
192
177
  readonly onDidChange: Event<ChatChangeEvent>;
193
178
  readonly id: string;
194
179
  readonly location: ChatAgentLocation;
195
- readonly changeSet?: ChangeSet;
196
180
  readonly context: ChatContextManager;
197
181
  readonly suggestions: readonly ChatSuggestion[];
198
182
  readonly settings?: { [key: string]: unknown };
183
+ readonly changeSet: ChangeSet;
199
184
  getRequests(): ChatRequestModel[];
200
185
  getBranches(): ChatHierarchyBranch<ChatRequestModel>[];
201
186
  isEmpty(): boolean;
202
187
  }
203
188
 
204
- export interface ChangeSet extends Disposable {
205
- onDidChange: Event<ChangeSetChangeEvent>;
206
- readonly title: string;
207
- getElements(): ChangeSetElement[];
208
- /**
209
- * Find an element by URI.
210
- * @param uri The URI to look for.
211
- * @returns The element with the given URI, or undefined if not found.
212
- */
213
- getElementByURI(uri: URI): ChangeSetElement | undefined;
214
- dispose(): void;
215
- }
216
-
217
189
  export interface ChatSuggestionCallback {
218
190
  kind: 'callback',
219
191
  callback: () => unknown;
@@ -240,25 +212,6 @@ export interface ChatContextManager {
240
212
  clear(): void;
241
213
  }
242
214
 
243
- export interface ChangeSetElement {
244
- readonly uri: URI;
245
-
246
- onDidChange?: Event<void>
247
- readonly name?: string;
248
- readonly icon?: string;
249
- readonly additionalInfo?: string;
250
-
251
- readonly state?: 'pending' | 'applied' | 'stale';
252
- readonly type?: 'add' | 'modify' | 'delete';
253
- readonly data?: { [key: string]: unknown };
254
-
255
- open?(): Promise<void>;
256
- openChange?(): Promise<void>;
257
- apply?(): Promise<void>;
258
- revert?(): Promise<void>;
259
- dispose?(): void;
260
- }
261
-
262
215
  export interface ChangeSetDecoration {
263
216
  readonly priority?: number;
264
217
  readonly additionalInfoSuffixIcon?: string[];
@@ -429,6 +382,9 @@ export interface ToolCallChatResponseContent extends Required<ChatResponseConten
429
382
  arguments?: string;
430
383
  finished: boolean;
431
384
  result?: string;
385
+ confirmed: Promise<boolean>;
386
+ confirm(): void;
387
+ deny(): void;
432
388
  }
433
389
 
434
390
  export interface ThinkingChatResponseContent
@@ -700,26 +656,39 @@ export class MutableChatModel implements ChatModel, Disposable {
700
656
 
701
657
  protected _hierarchy: ChatRequestHierarchy<MutableChatRequestModel>;
702
658
  protected _id: string;
703
- protected _changeSet?: ChangeSetImpl;
704
659
  protected _suggestions: readonly ChatSuggestion[] = [];
705
660
  protected readonly _contextManager = new ChatContextManagerImpl();
661
+ protected readonly _changeSet: ChatTreeChangeSet;
706
662
  protected _settings: { [key: string]: unknown };
707
663
 
708
664
  constructor(public readonly location = ChatAgentLocation.Panel) {
709
665
  // TODO accept serialized data as a parameter to restore a previously saved ChatModel
710
666
  this._hierarchy = new ChatRequestHierarchyImpl<MutableChatRequestModel>();
667
+ this._changeSet = new ChatTreeChangeSet(this._hierarchy);
668
+ this.toDispose.push(this._changeSet);
669
+ this._changeSet.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
711
670
  this._id = generateUuid();
712
671
 
713
672
  this.toDispose.pushAll([
714
673
  this._onDidChangeEmitter,
715
- this._contextManager.onDidChange(e => this._onDidChangeEmitter.fire(e)),
716
- this._hierarchy.onDidChange(event => this._onDidChangeEmitter.fire({
717
- kind: 'changeHierarchyBranch',
718
- branch: event.branch,
719
- })),
674
+ this._contextManager.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter),
675
+ this._hierarchy.onDidChange(event => {
676
+ this._onDidChangeEmitter.fire({
677
+ kind: 'changeHierarchyBranch',
678
+ branch: event.branch,
679
+ });
680
+ }),
720
681
  ]);
721
682
  }
722
683
 
684
+ get id(): string {
685
+ return this._id;
686
+ }
687
+
688
+ get changeSet(): ChangeSet {
689
+ return this._changeSet;
690
+ }
691
+
723
692
  getBranches(): ChatHierarchyBranch<ChatRequestModel>[] {
724
693
  return this._hierarchy.activeBranches();
725
694
  }
@@ -736,14 +705,6 @@ export class MutableChatModel implements ChatModel, Disposable {
736
705
  return this.getRequests().find(request => request.id === id);
737
706
  }
738
707
 
739
- get id(): string {
740
- return this._id;
741
- }
742
-
743
- get changeSet(): ChangeSetImpl | undefined {
744
- return this._changeSet;
745
- }
746
-
747
708
  get suggestions(): readonly ChatSuggestion[] {
748
709
  return this._suggestions;
749
710
  }
@@ -760,46 +721,18 @@ export class MutableChatModel implements ChatModel, Disposable {
760
721
  this._settings = settings;
761
722
  }
762
723
 
763
- setChangeSet(changeSet: ChangeSetImpl | undefined): void {
764
- if (!changeSet) {
765
- return this.removeChangeSet();
766
- }
767
- const oldChangeSet = this._changeSet;
768
- oldChangeSet?.dispose();
769
- this._changeSet = changeSet;
770
- this._onDidChangeEmitter.fire({
771
- kind: 'setChangeSet',
772
- changeSet,
773
- oldChangeSet,
774
- });
775
- changeSet.onDidChange(() => {
776
- this._onDidChangeEmitter.fire({
777
- kind: 'updateChangeSet',
778
- changeSet,
779
- });
780
- });
781
- }
782
-
783
- removeChangeSet(): void {
784
- if (this._changeSet) {
785
- const oldChangeSet = this._changeSet;
786
- this._changeSet = undefined;
787
- oldChangeSet.dispose();
788
- this._onDidChangeEmitter.fire({
789
- kind: 'removeChangeSet',
790
- changeSet: oldChangeSet,
791
- });
792
- }
793
- }
794
-
795
724
  addRequest(parsedChatRequest: ParsedChatRequest, agentId?: string, context: ChatContext = { variables: [] }): MutableChatRequestModel {
796
- if (parsedChatRequest.request.referencedRequestId) {
797
- return this.applyEdit(parsedChatRequest, agentId, context);
798
- }
799
-
725
+ const add = this.getTargetForRequestAddition(parsedChatRequest);
800
726
  const requestModel = new MutableChatRequestModel(this, parsedChatRequest, agentId, context);
801
- this.toDispose.push(requestModel);
802
- this._hierarchy.append(requestModel);
727
+ requestModel.onDidChange(event => {
728
+ if (!ChatChangeEvent.isChangeSetEvent(event)) {
729
+ this._onDidChangeEmitter.fire(event);
730
+ }
731
+ }, this, this.toDispose);
732
+
733
+ add(requestModel);
734
+ this._changeSet.registerRequest(requestModel);
735
+
803
736
  this._onDidChangeEmitter.fire({
804
737
  kind: 'addRequest',
805
738
  request: requestModel,
@@ -807,6 +740,13 @@ export class MutableChatModel implements ChatModel, Disposable {
807
740
  return requestModel;
808
741
  }
809
742
 
743
+ protected getTargetForRequestAddition(request: ParsedChatRequest): (addendum: MutableChatRequestModel) => void {
744
+ const requestId = request.request.referencedRequestId;
745
+ const branch = requestId !== undefined && this._hierarchy.findBranch(requestId);
746
+ if (requestId !== undefined && !branch) { throw new Error(`Cannot find branch for requestId: ${requestId}`); }
747
+ return branch ? branch.add.bind(branch) : this._hierarchy.append.bind(this._hierarchy);
748
+ }
749
+
810
750
  setSuggestions(suggestions: ChatSuggestion[]): void {
811
751
  this._suggestions = Object.freeze(suggestions);
812
752
  this._onDidChangeEmitter.fire({
@@ -819,33 +759,132 @@ export class MutableChatModel implements ChatModel, Disposable {
819
759
  return this.getRequests().length === 0;
820
760
  }
821
761
 
822
- applyEdit(parsedChatRequest: ParsedChatRequest, agentId?: string, context: ChatContext = { variables: [] }): MutableChatRequestModel {
823
- const requestId = parsedChatRequest.request.referencedRequestId!;
824
- const branch = this._hierarchy.findBranch(requestId);
825
- if (!branch) {
826
- throw new Error(`Cannot find branch for requestId: ${requestId}`);
762
+ dispose(): void {
763
+ this.toDispose.dispose();
764
+ }
765
+ }
766
+
767
+ export class ChatTreeChangeSet implements Omit<ChangeSet, 'onDidChange'> {
768
+ protected readonly onDidChangeEmitter = new Emitter<ChatUpdateChangeSetEvent>();
769
+ get onDidChange(): Event<ChatUpdateChangeSetEvent> {
770
+ return this.onDidChangeEmitter.event;
771
+ }
772
+
773
+ protected readonly toDispose = new DisposableCollection();
774
+
775
+ constructor(protected readonly hierarchy: ChatRequestHierarchy<MutableChatRequestModel>) {
776
+ hierarchy.onDidChange(this.handleChangeSetChange, this, this.toDispose);
777
+ }
778
+
779
+ get title(): string {
780
+ return this.getCurrentChangeSet()?.title ?? '';
781
+ }
782
+
783
+ removeElements(...uris: URI[]): boolean {
784
+ return this.getMutableChangeSet().removeElements(...uris);
785
+ }
786
+
787
+ addElements(...elements: ChangeSetElement[]): boolean {
788
+ return this.getMutableChangeSet().addElements(...elements);
789
+ }
790
+
791
+ setElements(...elements: ChangeSetElement[]): void {
792
+ this.getMutableChangeSet().setElements(...elements);
793
+ }
794
+
795
+ setTitle(title: string): void {
796
+ this.getMutableChangeSet().setTitle(title);
797
+ }
798
+
799
+ getElementByURI(uri: URI): ChangeSetElement | undefined {
800
+ return this.currentElements.find(candidate => candidate.uri.isEqual(uri));
801
+ }
802
+
803
+ protected currentElements: ChangeSetElement[] = [];
804
+ protected handleChangeSetChange = debounce(this.doHandleChangeSetChange.bind(this), 100, { leading: false, trailing: true });
805
+ protected doHandleChangeSetChange(): void {
806
+ const newElements = this.computeChangeSetElements();
807
+ this.handleElementChange(newElements);
808
+ this.currentElements = newElements;
809
+ this.onDidChangeEmitter.fire({ kind: 'updateChangeSet', elements: this.currentElements, title: this.getCurrentChangeSet()?.title });
810
+ }
811
+
812
+ getElements(): ChangeSetElement[] {
813
+ return this.currentElements;
814
+ }
815
+
816
+ protected computeChangeSetElements(): ChangeSetElement[] {
817
+ const allElements = ChangeSetImpl.combine((function* (requests: MutableChatRequestModel[]): IterableIterator<ChangeSetImpl> {
818
+ for (let i = requests.length - 1; i >= 0; i--) {
819
+ const changeSet = requests[i].changeSet;
820
+ if (changeSet) { yield changeSet; }
821
+ }
822
+ })(this.hierarchy.activeRequests()));
823
+ return ArrayUtils.coalesce(Array.from(allElements.values()));
824
+ }
825
+
826
+ protected handleElementChange(newElements: ChangeSetElement[]): void {
827
+ const old = new Set(this.currentElements);
828
+ for (const element of newElements) {
829
+ if (!old.delete(element)) {
830
+ element.onShow?.();
831
+ }
832
+ }
833
+ for (const element of old) {
834
+ element.onHide?.();
827
835
  }
836
+ }
828
837
 
829
- const requestModel = new MutableChatRequestModel(this, parsedChatRequest, agentId, context);
830
- this.toDispose.push(requestModel);
831
- branch.add(requestModel);
832
- this.removeChangeSet();
838
+ protected toDisposeOnRequestAdded = new DisposableCollection();
839
+ registerRequest(request: MutableChatRequestModel): void {
840
+ request.onDidChange(event => event.kind === 'updateChangeSet' && this.handleChangeSetChange(), this, this.toDispose);
841
+ if (this.localChangeSet) {
842
+ request.changeSet = this.localChangeSet;
843
+ this.localChangeSet = undefined;
844
+ }
845
+ this.toDisposeOnRequestAdded.dispose();
846
+ }
833
847
 
834
- this._onDidChangeEmitter.fire({
835
- kind: 'addRequest',
836
- request: requestModel,
837
- });
848
+ protected localChangeSet?: ChangeSetImpl;
849
+ protected getMutableChangeSet(): ChangeSetImpl {
850
+ const tipRequest = this.hierarchy.activeRequests().at(-1);
851
+ const existingChangeSet = tipRequest?.changeSet;
852
+ if (existingChangeSet) {
853
+ return existingChangeSet;
854
+ }
855
+ if (this.localChangeSet && tipRequest) {
856
+ throw new Error('Non-empty chat model retained reference to own change set. This is unexpected!');
857
+ }
858
+ if (this.localChangeSet) {
859
+ return this.localChangeSet;
860
+ }
861
+ const newChangeSet = new ChangeSetImpl();
862
+ if (tipRequest) {
863
+ tipRequest.changeSet = newChangeSet;
864
+ } else {
865
+ this.localChangeSet = newChangeSet;
866
+ newChangeSet.onDidChange(this.handleChangeSetChange, this, this.toDisposeOnRequestAdded);
867
+ }
868
+ return newChangeSet;
869
+ }
838
870
 
839
- return requestModel;
871
+ protected getCurrentChangeSet(): ChangeSet | undefined {
872
+ const holder = this.getBranchParent(candidate => !!candidate.get().changeSet);
873
+ return holder?.get().changeSet ?? this.localChangeSet;
840
874
  }
841
875
 
842
- dispose(): void {
843
- this.removeChangeSet(); // Signal disposal of last change set.
844
- this.toDispose.dispose();
876
+ /** Returns the lowest node among active nodes that satisfies {@link criterion} */
877
+ getBranchParent(criterion: (branch: ChatHierarchyBranch<MutableChatRequestModel>) => boolean): ChatHierarchyBranch<MutableChatRequestModel> | undefined {
878
+ const branches = this.hierarchy.activeBranches();
879
+ for (let i = branches.length - 1; i >= 0; i--) {
880
+ const branch = branches[i];
881
+ if (criterion?.(branch)) { return branch; }
882
+ }
883
+ return branches.at(0);
845
884
  }
846
885
 
847
- emit(event: ChatChangeEvent): void {
848
- this._onDidChangeEmitter.fire(event);
886
+ dispose(): void {
887
+ this.toDispose.dispose();
849
888
  }
850
889
  }
851
890
 
@@ -915,6 +954,11 @@ export class ChatRequestHierarchyImpl<TRequest extends ChatRequestModel = ChatRe
915
954
  notifyChange(event: ChangeActiveBranchEvent<TRequest>): void {
916
955
  this.onDidChangeActiveBranchEmitter.fire(event);
917
956
  }
957
+
958
+ dispose(): void {
959
+ this.onDidChangeActiveBranchEmitter.dispose();
960
+ this.branch.dispose();
961
+ }
918
962
  }
919
963
 
920
964
  export class ChatRequestHierarchyBranchImpl<TRequest extends ChatRequestModel> implements ChatHierarchyBranch<TRequest> {
@@ -1019,82 +1063,12 @@ export class ChatRequestHierarchyBranchImpl<TRequest extends ChatRequestModel> i
1019
1063
 
1020
1064
  return branches;
1021
1065
  }
1022
- }
1023
-
1024
- interface ChangeSetChangeEvent {
1025
- added?: URI[],
1026
- removed?: URI[],
1027
- modified?: URI[],
1028
- /** Fired when only the state of a given element changes, not its contents */
1029
- state?: URI[],
1030
- }
1031
-
1032
- export class ChangeSetImpl implements ChangeSet {
1033
- protected readonly _onDidChangeEmitter = new Emitter<ChangeSetChangeEvent>();
1034
- onDidChange: Event<ChangeSetChangeEvent> = this._onDidChangeEmitter.event;
1035
-
1036
- protected _elements: ChangeSetElement[] = [];
1037
-
1038
- constructor(public readonly title: string, elements: ChangeSetElement[] = []) {
1039
- this.addElements(...elements);
1040
- }
1041
-
1042
- getElements(): ChangeSetElement[] {
1043
- return this._elements;
1044
- }
1045
-
1046
- /**
1047
- * Find an element by URI.
1048
- * @param uri The URI to look for.
1049
- * @returns The element with the given URI, or undefined if not found.
1050
- */
1051
- getElementByURI(uri: URI): ChangeSetElement | undefined {
1052
- const uriString = uri.toString();
1053
- for (const element of this._elements) {
1054
- if (element.uri.toString() === uriString) {
1055
- return element;
1056
- }
1057
- }
1058
- return undefined;
1059
- }
1060
-
1061
- /** Will replace any element that is already present, using URI as identity criterion. */
1062
- addElements(...elements: ChangeSetElement[]): void {
1063
- const added: URI[] = [];
1064
- const modified: URI[] = [];
1065
- const toDispose: ChangeSetElement[] = [];
1066
- const current = new Map(this.getElements().map((element, index) => [element.uri.toString(), index]));
1067
- elements.forEach(element => {
1068
- const existingIndex = current.get(element.uri.toString());
1069
- if (existingIndex !== undefined) {
1070
- modified.push(element.uri);
1071
- toDispose.push(this._elements[existingIndex]);
1072
- this._elements[existingIndex] = element;
1073
- } else {
1074
- added.push(element.uri);
1075
- this._elements.push(element);
1076
- }
1077
- element.onDidChange?.(() => this.notifyChange({ state: [element.uri] }));
1078
- });
1079
- toDispose.forEach(element => element.dispose?.());
1080
- this.notifyChange({ added, modified });
1081
- }
1082
-
1083
- removeElements(...indices: number[]): void {
1084
- // From highest to lowest so that we don't affect lower indices with our splicing.
1085
- const sorted = indices.slice().sort((left, right) => left - right);
1086
- const deletions = sorted.flatMap(index => this._elements.splice(index, 1));
1087
- deletions.forEach(deleted => deleted.dispose?.());
1088
- this.notifyChange({ removed: deletions.map(element => element.uri) });
1089
- }
1090
-
1091
- protected notifyChange(change: ChangeSetChangeEvent): void {
1092
- this._onDidChangeEmitter.fire(change);
1093
- }
1094
1066
 
1095
1067
  dispose(): void {
1096
- this._onDidChangeEmitter.dispose();
1097
- this._elements.forEach(element => element.dispose?.());
1068
+ if (Disposable.is(this.get())) {
1069
+ this.items.forEach(({ element }) => Disposable.is(element) && element.dispose());
1070
+ }
1071
+ this.items.length = 0;
1098
1072
  }
1099
1073
  }
1100
1074
 
@@ -1161,10 +1135,13 @@ export class ChatContextManagerImpl implements ChatContextManager {
1161
1135
  }
1162
1136
 
1163
1137
  export class MutableChatRequestModel implements ChatRequestModel, EditableChatRequestModel, Disposable {
1138
+ protected readonly _onDidChangeEmitter = new Emitter<ChatChangeEvent>();
1139
+ onDidChange: Event<ChatChangeEvent> = this._onDidChangeEmitter.event;
1164
1140
  protected readonly _id: string;
1165
1141
  protected _session: MutableChatModel;
1166
1142
  protected _request: ChatRequest;
1167
1143
  protected _response: MutableChatResponseModel;
1144
+ protected _changeSet?: ChangeSetImpl;
1168
1145
  protected _context: ChatContext;
1169
1146
  protected _agentId?: string;
1170
1147
  protected _data: { [key: string]: unknown };
@@ -1185,9 +1162,20 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1185
1162
  this._data = data;
1186
1163
 
1187
1164
  this.editContextManager = new ChatContextManagerImpl(context);
1188
- this.toDispose.pushAll([
1189
- this.editContextManager.onDidChange(e => this.session.emit(e))
1190
- ]);
1165
+ this.editContextManager.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
1166
+ this.toDispose.push(this._onDidChangeEmitter);
1167
+ }
1168
+
1169
+ get changeSet(): ChangeSetImpl | undefined {
1170
+ return this._changeSet;
1171
+ }
1172
+
1173
+ set changeSet(changeSet: ChangeSetImpl) {
1174
+ this._changeSet?.dispose();
1175
+ this._changeSet = changeSet;
1176
+ this.toDispose.push(changeSet);
1177
+ changeSet.onDidChange(() => this._onDidChangeEmitter.fire({ kind: 'updateChangeSet', elements: changeSet.getElements(), title: changeSet.title }), this, this.toDispose);
1178
+ this._onDidChangeEmitter.fire({ kind: 'updateChangeSet', elements: changeSet.getElements(), title: changeSet.title });
1191
1179
  }
1192
1180
 
1193
1181
  get isEditing(): boolean {
@@ -1280,7 +1268,7 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1280
1268
  if (!branch) {
1281
1269
  throw new Error(`Cannot find hierarchy for requestId: ${request.id}`);
1282
1270
  }
1283
- this.session.emit({
1271
+ this._onDidChangeEmitter.fire({
1284
1272
  kind: 'enableEdit',
1285
1273
  request,
1286
1274
  branch,
@@ -1292,7 +1280,7 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1292
1280
  if (!branch) {
1293
1281
  throw new Error(`Cannot find branch for requestId: ${request.id}`);
1294
1282
  }
1295
- this.session.emit({
1283
+ this._onDidChangeEmitter.fire({
1296
1284
  kind: 'cancelEdit',
1297
1285
  request,
1298
1286
  branch,
@@ -1304,7 +1292,7 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1304
1292
  if (!branch) {
1305
1293
  throw new Error(`Cannot find branch for requestId: ${request.id}`);
1306
1294
  }
1307
- this.session.emit({
1295
+ this._onDidChangeEmitter.fire({
1308
1296
  kind: 'submitEdit',
1309
1297
  request,
1310
1298
  branch,
@@ -1359,6 +1347,7 @@ export class TextChatResponseContentImpl implements TextChatResponseContent {
1359
1347
  };
1360
1348
  }
1361
1349
  }
1350
+
1362
1351
  export class ThinkingChatResponseContentImpl implements ThinkingChatResponseContent {
1363
1352
  readonly kind = 'thinking';
1364
1353
  protected _content: string;
@@ -1501,6 +1490,9 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1501
1490
  protected _arguments?: string;
1502
1491
  protected _finished?: boolean;
1503
1492
  protected _result?: string;
1493
+ protected _confirmed: Promise<boolean>;
1494
+ protected _confirmationResolver?: (value: boolean) => void;
1495
+ protected _confirmationRejecter?: (reason?: unknown) => void;
1504
1496
 
1505
1497
  constructor(id?: string, name?: string, arg_string?: string, finished?: boolean, result?: string) {
1506
1498
  this._id = id;
@@ -1508,6 +1500,8 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1508
1500
  this._arguments = arg_string;
1509
1501
  this._finished = finished;
1510
1502
  this._result = result;
1503
+ // Initialize the confirmation promise immediately
1504
+ this._confirmed = this.createConfirmationPromise();
1511
1505
  }
1512
1506
 
1513
1507
  get id(): string | undefined {
@@ -1529,6 +1523,53 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1529
1523
  return this._result;
1530
1524
  }
1531
1525
 
1526
+ get confirmed(): Promise<boolean> {
1527
+ return this._confirmed;
1528
+ }
1529
+
1530
+ /**
1531
+ * Create a confirmation promise that can be resolved/rejected later
1532
+ */
1533
+ createConfirmationPromise(): Promise<boolean> {
1534
+ // The promise is always created, just ensure we have resolution handlers
1535
+ if (!this._confirmationResolver) {
1536
+ this._confirmed = new Promise<boolean>((resolve, reject) => {
1537
+ this._confirmationResolver = resolve;
1538
+ this._confirmationRejecter = reject;
1539
+ });
1540
+ }
1541
+ return this._confirmed;
1542
+ }
1543
+
1544
+ /**
1545
+ * Confirm the tool execution
1546
+ */
1547
+ confirm(): void {
1548
+ if (this._confirmationResolver) {
1549
+ this._confirmationResolver(true);
1550
+ }
1551
+ }
1552
+
1553
+ /**
1554
+ * Deny the tool execution
1555
+ */
1556
+ deny(): void {
1557
+ if (this._confirmationResolver) {
1558
+ this._confirmationResolver(false);
1559
+ this._finished = true;
1560
+ this._result = 'Tool execution denied by user';
1561
+ }
1562
+ }
1563
+
1564
+ /**
1565
+ * Cancel the confirmation (reject the promise)
1566
+ */
1567
+ cancelConfirmation(reason?: unknown): void {
1568
+ if (this._confirmationRejecter) {
1569
+ this._confirmationRejecter(reason);
1570
+ }
1571
+ }
1572
+
1532
1573
  asString(): string {
1533
1574
  return '';
1534
1575
  }
@@ -1543,6 +1584,7 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1543
1584
  this._result = nextChatResponseContent.result;
1544
1585
  const args = nextChatResponseContent.arguments;
1545
1586
  this._arguments = (args && args.length > 0) ? args : this._arguments;
1587
+ // Don't merge confirmation promises - they should be managed separately
1546
1588
  return true;
1547
1589
  }
1548
1590
  if (nextChatResponseContent.name !== undefined) {