@theia/ai-chat 1.63.0-next.0 → 1.63.0-next.52

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 (91) hide show
  1. package/lib/browser/agent-delegation-tool.d.ts +25 -0
  2. package/lib/browser/agent-delegation-tool.d.ts.map +1 -0
  3. package/lib/browser/agent-delegation-tool.js +171 -0
  4. package/lib/browser/agent-delegation-tool.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 +15 -1
  7. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  8. package/lib/browser/change-set-file-element.d.ts +28 -7
  9. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  10. package/lib/browser/change-set-file-element.js +86 -18
  11. package/lib/browser/change-set-file-element.js.map +1 -1
  12. package/lib/browser/change-set-variable.js +1 -2
  13. package/lib/browser/change-set-variable.js.map +1 -1
  14. package/lib/browser/chat-tool-preferences.d.ts +54 -0
  15. package/lib/browser/chat-tool-preferences.d.ts.map +1 -0
  16. package/lib/browser/chat-tool-preferences.js +170 -0
  17. package/lib/browser/chat-tool-preferences.js.map +1 -0
  18. package/lib/browser/chat-tool-request-service.d.ts +20 -0
  19. package/lib/browser/chat-tool-request-service.d.ts.map +1 -0
  20. package/lib/browser/chat-tool-request-service.js +89 -0
  21. package/lib/browser/chat-tool-request-service.js.map +1 -0
  22. package/lib/browser/delegation-response-content.d.ts +20 -0
  23. package/lib/browser/delegation-response-content.d.ts.map +1 -0
  24. package/lib/browser/delegation-response-content.js +51 -0
  25. package/lib/browser/delegation-response-content.js.map +1 -0
  26. package/lib/browser/file-chat-variable-contribution.d.ts +15 -1
  27. package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
  28. package/lib/browser/file-chat-variable-contribution.js +111 -5
  29. package/lib/browser/file-chat-variable-contribution.js.map +1 -1
  30. package/lib/browser/frontend-chat-service.d.ts.map +1 -1
  31. package/lib/browser/frontend-chat-service.js +2 -6
  32. package/lib/browser/frontend-chat-service.js.map +1 -1
  33. package/lib/browser/image-context-variable-contribution.d.ts +27 -0
  34. package/lib/browser/image-context-variable-contribution.d.ts.map +1 -0
  35. package/lib/browser/image-context-variable-contribution.js +149 -0
  36. package/lib/browser/image-context-variable-contribution.js.map +1 -0
  37. package/lib/browser/task-context-service.d.ts +9 -3
  38. package/lib/browser/task-context-service.d.ts.map +1 -1
  39. package/lib/browser/task-context-service.js +111 -9
  40. package/lib/browser/task-context-service.js.map +1 -1
  41. package/lib/browser/task-context-storage-service.d.ts +1 -0
  42. package/lib/browser/task-context-storage-service.d.ts.map +1 -1
  43. package/lib/browser/task-context-storage-service.js +4 -1
  44. package/lib/browser/task-context-storage-service.js.map +1 -1
  45. package/lib/common/change-set.d.ts +78 -0
  46. package/lib/common/change-set.d.ts.map +1 -0
  47. package/lib/common/change-set.js +133 -0
  48. package/lib/common/change-set.js.map +1 -0
  49. package/lib/common/chat-agent-service.d.ts +1 -0
  50. package/lib/common/chat-agent-service.d.ts.map +1 -1
  51. package/lib/common/chat-agent-service.js +2 -1
  52. package/lib/common/chat-agent-service.js.map +1 -1
  53. package/lib/common/chat-agents.d.ts +2 -2
  54. package/lib/common/chat-agents.d.ts.map +1 -1
  55. package/lib/common/chat-agents.js +25 -6
  56. package/lib/common/chat-agents.js.map +1 -1
  57. package/lib/common/chat-model.d.ts +68 -80
  58. package/lib/common/chat-model.d.ts.map +1 -1
  59. package/lib/common/chat-model.js +224 -136
  60. package/lib/common/chat-model.js.map +1 -1
  61. package/lib/common/chat-request-parser.d.ts.map +1 -1
  62. package/lib/common/chat-request-parser.js +3 -0
  63. package/lib/common/chat-request-parser.js.map +1 -1
  64. package/lib/common/chat-service.d.ts +6 -5
  65. package/lib/common/chat-service.d.ts.map +1 -1
  66. package/lib/common/chat-service.js +9 -11
  67. package/lib/common/chat-service.js.map +1 -1
  68. package/lib/common/image-context-variable.d.ts +29 -0
  69. package/lib/common/image-context-variable.d.ts.map +1 -0
  70. package/lib/common/image-context-variable.js +99 -0
  71. package/lib/common/image-context-variable.js.map +1 -0
  72. package/package.json +10 -9
  73. package/src/browser/agent-delegation-tool.ts +207 -0
  74. package/src/browser/ai-chat-frontend-module.ts +28 -3
  75. package/src/browser/change-set-file-element.ts +97 -25
  76. package/src/browser/change-set-variable.ts +1 -1
  77. package/src/browser/chat-tool-preferences.ts +178 -0
  78. package/src/browser/chat-tool-request-service.ts +93 -0
  79. package/src/browser/delegation-response-content.ts +55 -0
  80. package/src/browser/file-chat-variable-contribution.ts +120 -6
  81. package/src/browser/frontend-chat-service.ts +3 -6
  82. package/src/browser/image-context-variable-contribution.ts +153 -0
  83. package/src/browser/task-context-service.ts +115 -9
  84. package/src/browser/task-context-storage-service.ts +5 -1
  85. package/src/common/change-set.ts +197 -0
  86. package/src/common/chat-agent-service.ts +1 -0
  87. package/src/common/chat-agents.ts +40 -19
  88. package/src/common/chat-model.ts +258 -208
  89. package/src/common/chat-request-parser.ts +3 -0
  90. package/src/common/chat-service.ts +11 -13
  91. package/src/common/image-context-variable.ts +116 -0
@@ -19,12 +19,23 @@
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 { 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';
22
+ import {
23
+ AIVariableResolutionRequest,
24
+ LanguageModelMessage,
25
+ ResolvedAIContextVariable,
26
+ TextMessage,
27
+ ThinkingMessage,
28
+ ToolResultMessage,
29
+ ToolUseMessage
30
+ } from '@theia/ai-core';
31
+ import { ArrayUtils, CancellationToken, CancellationTokenSource, Command, Disposable, DisposableCollection, Emitter, Event, generateUuid, URI } from '@theia/core';
24
32
  import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
25
33
  import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
34
+ import { ChangeSet, ChangeSetElement, ChangeSetImpl, ChatUpdateChangeSetEvent } from './change-set';
26
35
  import { ChatAgentLocation } from './chat-agents';
27
36
  import { ParsedChatRequest } from './parsed-chat-request';
37
+ import debounce = require('@theia/core/shared/lodash.debounce');
38
+ export { ChangeSet, ChangeSetElement, ChangeSetImpl };
28
39
 
29
40
  /**********************
30
41
  * INTERFACES AND TYPE GUARDS
@@ -37,10 +48,8 @@ export type ChatChangeEvent =
37
48
  | ChatRemoveVariableEvent
38
49
  | ChatSetVariablesEvent
39
50
  | ChatRemoveRequestEvent
40
- | ChatSetChangeSetEvent
41
51
  | ChatSuggestionsChangedEvent
42
52
  | ChatUpdateChangeSetEvent
43
- | ChatRemoveChangeSetEvent
44
53
  | ChatEditRequestEvent
45
54
  | ChatEditCancelEvent
46
55
  | ChatEditSubmitEvent
@@ -80,22 +89,6 @@ export interface ChatAddResponseEvent {
80
89
  response: ChatResponseModel;
81
90
  }
82
91
 
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
92
  export interface ChatAddVariableEvent {
100
93
  kind: 'addVariable';
101
94
  }
@@ -114,8 +107,8 @@ export interface ChatSuggestionsChangedEvent {
114
107
  }
115
108
 
116
109
  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';
110
+ export function isChangeSetEvent(event: ChatChangeEvent): event is ChatUpdateChangeSetEvent {
111
+ return event.kind === 'updateChangeSet';
119
112
  }
120
113
  }
121
114
 
@@ -136,7 +129,7 @@ export interface ChatRemoveRequestEvent {
136
129
  * - Within each branch, the requests are stored in a list. Those requests are the alternatives to the original request.
137
130
  * Each of those items can have a next branch, which is the next request in the hierarchy.
138
131
  */
139
- export interface ChatRequestHierarchy<TRequest extends ChatRequestModel = ChatRequestModel> {
132
+ export interface ChatRequestHierarchy<TRequest extends ChatRequestModel = ChatRequestModel> extends Disposable {
140
133
  readonly branch: ChatHierarchyBranch<TRequest>
141
134
 
142
135
  onDidChange: Event<ChangeActiveBranchEvent<TRequest>>;
@@ -160,7 +153,7 @@ export interface ChangeActiveBranchEvent<TRequest extends ChatRequestModel = Cha
160
153
  * It contains a list of items, each representing a request.
161
154
  * Those items can have a next branch, which is the next request in the hierarchy.
162
155
  */
163
- export interface ChatHierarchyBranch<TRequest extends ChatRequestModel = ChatRequestModel> {
156
+ export interface ChatHierarchyBranch<TRequest extends ChatRequestModel = ChatRequestModel> extends Disposable {
164
157
  readonly id: string;
165
158
  readonly hierarchy: ChatRequestHierarchy<TRequest>;
166
159
  readonly previous?: ChatHierarchyBranch<TRequest>;
@@ -192,28 +185,15 @@ export interface ChatModel {
192
185
  readonly onDidChange: Event<ChatChangeEvent>;
193
186
  readonly id: string;
194
187
  readonly location: ChatAgentLocation;
195
- readonly changeSet?: ChangeSet;
196
188
  readonly context: ChatContextManager;
197
189
  readonly suggestions: readonly ChatSuggestion[];
198
190
  readonly settings?: { [key: string]: unknown };
191
+ readonly changeSet: ChangeSet;
199
192
  getRequests(): ChatRequestModel[];
200
193
  getBranches(): ChatHierarchyBranch<ChatRequestModel>[];
201
194
  isEmpty(): boolean;
202
195
  }
203
196
 
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
197
  export interface ChatSuggestionCallback {
218
198
  kind: 'callback',
219
199
  callback: () => unknown;
@@ -240,25 +220,6 @@ export interface ChatContextManager {
240
220
  clear(): void;
241
221
  }
242
222
 
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
223
  export interface ChangeSetDecoration {
263
224
  readonly priority?: number;
264
225
  readonly additionalInfoSuffixIcon?: string[];
@@ -429,6 +390,9 @@ export interface ToolCallChatResponseContent extends Required<ChatResponseConten
429
390
  arguments?: string;
430
391
  finished: boolean;
431
392
  result?: string;
393
+ confirmed: Promise<boolean>;
394
+ confirm(): void;
395
+ deny(): void;
432
396
  }
433
397
 
434
398
  export interface ThinkingChatResponseContent
@@ -700,26 +664,39 @@ export class MutableChatModel implements ChatModel, Disposable {
700
664
 
701
665
  protected _hierarchy: ChatRequestHierarchy<MutableChatRequestModel>;
702
666
  protected _id: string;
703
- protected _changeSet?: ChangeSetImpl;
704
667
  protected _suggestions: readonly ChatSuggestion[] = [];
705
668
  protected readonly _contextManager = new ChatContextManagerImpl();
669
+ protected readonly _changeSet: ChatTreeChangeSet;
706
670
  protected _settings: { [key: string]: unknown };
707
671
 
708
672
  constructor(public readonly location = ChatAgentLocation.Panel) {
709
673
  // TODO accept serialized data as a parameter to restore a previously saved ChatModel
710
674
  this._hierarchy = new ChatRequestHierarchyImpl<MutableChatRequestModel>();
675
+ this._changeSet = new ChatTreeChangeSet(this._hierarchy);
676
+ this.toDispose.push(this._changeSet);
677
+ this._changeSet.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
711
678
  this._id = generateUuid();
712
679
 
713
680
  this.toDispose.pushAll([
714
681
  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
- })),
682
+ this._contextManager.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter),
683
+ this._hierarchy.onDidChange(event => {
684
+ this._onDidChangeEmitter.fire({
685
+ kind: 'changeHierarchyBranch',
686
+ branch: event.branch,
687
+ });
688
+ }),
720
689
  ]);
721
690
  }
722
691
 
692
+ get id(): string {
693
+ return this._id;
694
+ }
695
+
696
+ get changeSet(): ChangeSet {
697
+ return this._changeSet;
698
+ }
699
+
723
700
  getBranches(): ChatHierarchyBranch<ChatRequestModel>[] {
724
701
  return this._hierarchy.activeBranches();
725
702
  }
@@ -736,14 +713,6 @@ export class MutableChatModel implements ChatModel, Disposable {
736
713
  return this.getRequests().find(request => request.id === id);
737
714
  }
738
715
 
739
- get id(): string {
740
- return this._id;
741
- }
742
-
743
- get changeSet(): ChangeSetImpl | undefined {
744
- return this._changeSet;
745
- }
746
-
747
716
  get suggestions(): readonly ChatSuggestion[] {
748
717
  return this._suggestions;
749
718
  }
@@ -760,46 +729,18 @@ export class MutableChatModel implements ChatModel, Disposable {
760
729
  this._settings = settings;
761
730
  }
762
731
 
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
732
  addRequest(parsedChatRequest: ParsedChatRequest, agentId?: string, context: ChatContext = { variables: [] }): MutableChatRequestModel {
796
- if (parsedChatRequest.request.referencedRequestId) {
797
- return this.applyEdit(parsedChatRequest, agentId, context);
798
- }
799
-
733
+ const add = this.getTargetForRequestAddition(parsedChatRequest);
800
734
  const requestModel = new MutableChatRequestModel(this, parsedChatRequest, agentId, context);
801
- this.toDispose.push(requestModel);
802
- this._hierarchy.append(requestModel);
735
+ requestModel.onDidChange(event => {
736
+ if (!ChatChangeEvent.isChangeSetEvent(event)) {
737
+ this._onDidChangeEmitter.fire(event);
738
+ }
739
+ }, this, this.toDispose);
740
+
741
+ add(requestModel);
742
+ this._changeSet.registerRequest(requestModel);
743
+
803
744
  this._onDidChangeEmitter.fire({
804
745
  kind: 'addRequest',
805
746
  request: requestModel,
@@ -807,6 +748,13 @@ export class MutableChatModel implements ChatModel, Disposable {
807
748
  return requestModel;
808
749
  }
809
750
 
751
+ protected getTargetForRequestAddition(request: ParsedChatRequest): (addendum: MutableChatRequestModel) => void {
752
+ const requestId = request.request.referencedRequestId;
753
+ const branch = requestId !== undefined && this._hierarchy.findBranch(requestId);
754
+ if (requestId !== undefined && !branch) { throw new Error(`Cannot find branch for requestId: ${requestId}`); }
755
+ return branch ? branch.add.bind(branch) : this._hierarchy.append.bind(this._hierarchy);
756
+ }
757
+
810
758
  setSuggestions(suggestions: ChatSuggestion[]): void {
811
759
  this._suggestions = Object.freeze(suggestions);
812
760
  this._onDidChangeEmitter.fire({
@@ -819,33 +767,132 @@ export class MutableChatModel implements ChatModel, Disposable {
819
767
  return this.getRequests().length === 0;
820
768
  }
821
769
 
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}`);
770
+ dispose(): void {
771
+ this.toDispose.dispose();
772
+ }
773
+ }
774
+
775
+ export class ChatTreeChangeSet implements Omit<ChangeSet, 'onDidChange'> {
776
+ protected readonly onDidChangeEmitter = new Emitter<ChatUpdateChangeSetEvent>();
777
+ get onDidChange(): Event<ChatUpdateChangeSetEvent> {
778
+ return this.onDidChangeEmitter.event;
779
+ }
780
+
781
+ protected readonly toDispose = new DisposableCollection();
782
+
783
+ constructor(protected readonly hierarchy: ChatRequestHierarchy<MutableChatRequestModel>) {
784
+ hierarchy.onDidChange(this.handleChangeSetChange, this, this.toDispose);
785
+ }
786
+
787
+ get title(): string {
788
+ return this.getCurrentChangeSet()?.title ?? '';
789
+ }
790
+
791
+ removeElements(...uris: URI[]): boolean {
792
+ return this.getMutableChangeSet().removeElements(...uris);
793
+ }
794
+
795
+ addElements(...elements: ChangeSetElement[]): boolean {
796
+ return this.getMutableChangeSet().addElements(...elements);
797
+ }
798
+
799
+ setElements(...elements: ChangeSetElement[]): void {
800
+ this.getMutableChangeSet().setElements(...elements);
801
+ }
802
+
803
+ setTitle(title: string): void {
804
+ this.getMutableChangeSet().setTitle(title);
805
+ }
806
+
807
+ getElementByURI(uri: URI): ChangeSetElement | undefined {
808
+ return this.currentElements.find(candidate => candidate.uri.isEqual(uri));
809
+ }
810
+
811
+ protected currentElements: ChangeSetElement[] = [];
812
+ protected handleChangeSetChange = debounce(this.doHandleChangeSetChange.bind(this), 100, { leading: false, trailing: true });
813
+ protected doHandleChangeSetChange(): void {
814
+ const newElements = this.computeChangeSetElements();
815
+ this.handleElementChange(newElements);
816
+ this.currentElements = newElements;
817
+ this.onDidChangeEmitter.fire({ kind: 'updateChangeSet', elements: this.currentElements, title: this.getCurrentChangeSet()?.title });
818
+ }
819
+
820
+ getElements(): ChangeSetElement[] {
821
+ return this.currentElements;
822
+ }
823
+
824
+ protected computeChangeSetElements(): ChangeSetElement[] {
825
+ const allElements = ChangeSetImpl.combine((function* (requests: MutableChatRequestModel[]): IterableIterator<ChangeSetImpl> {
826
+ for (let i = requests.length - 1; i >= 0; i--) {
827
+ const changeSet = requests[i].changeSet;
828
+ if (changeSet) { yield changeSet; }
829
+ }
830
+ })(this.hierarchy.activeRequests()));
831
+ return ArrayUtils.coalesce(Array.from(allElements.values()));
832
+ }
833
+
834
+ protected handleElementChange(newElements: ChangeSetElement[]): void {
835
+ const old = new Set(this.currentElements);
836
+ for (const element of newElements) {
837
+ if (!old.delete(element)) {
838
+ element.onShow?.();
839
+ }
840
+ }
841
+ for (const element of old) {
842
+ element.onHide?.();
827
843
  }
844
+ }
828
845
 
829
- const requestModel = new MutableChatRequestModel(this, parsedChatRequest, agentId, context);
830
- this.toDispose.push(requestModel);
831
- branch.add(requestModel);
832
- this.removeChangeSet();
846
+ protected toDisposeOnRequestAdded = new DisposableCollection();
847
+ registerRequest(request: MutableChatRequestModel): void {
848
+ request.onDidChange(event => event.kind === 'updateChangeSet' && this.handleChangeSetChange(), this, this.toDispose);
849
+ if (this.localChangeSet) {
850
+ request.changeSet = this.localChangeSet;
851
+ this.localChangeSet = undefined;
852
+ }
853
+ this.toDisposeOnRequestAdded.dispose();
854
+ }
833
855
 
834
- this._onDidChangeEmitter.fire({
835
- kind: 'addRequest',
836
- request: requestModel,
837
- });
856
+ protected localChangeSet?: ChangeSetImpl;
857
+ protected getMutableChangeSet(): ChangeSetImpl {
858
+ const tipRequest = this.hierarchy.activeRequests().at(-1);
859
+ const existingChangeSet = tipRequest?.changeSet;
860
+ if (existingChangeSet) {
861
+ return existingChangeSet;
862
+ }
863
+ if (this.localChangeSet && tipRequest) {
864
+ throw new Error('Non-empty chat model retained reference to own change set. This is unexpected!');
865
+ }
866
+ if (this.localChangeSet) {
867
+ return this.localChangeSet;
868
+ }
869
+ const newChangeSet = new ChangeSetImpl();
870
+ if (tipRequest) {
871
+ tipRequest.changeSet = newChangeSet;
872
+ } else {
873
+ this.localChangeSet = newChangeSet;
874
+ newChangeSet.onDidChange(this.handleChangeSetChange, this, this.toDisposeOnRequestAdded);
875
+ }
876
+ return newChangeSet;
877
+ }
838
878
 
839
- return requestModel;
879
+ protected getCurrentChangeSet(): ChangeSet | undefined {
880
+ const holder = this.getBranchParent(candidate => !!candidate.get().changeSet);
881
+ return holder?.get().changeSet ?? this.localChangeSet;
840
882
  }
841
883
 
842
- dispose(): void {
843
- this.removeChangeSet(); // Signal disposal of last change set.
844
- this.toDispose.dispose();
884
+ /** Returns the lowest node among active nodes that satisfies {@link criterion} */
885
+ getBranchParent(criterion: (branch: ChatHierarchyBranch<MutableChatRequestModel>) => boolean): ChatHierarchyBranch<MutableChatRequestModel> | undefined {
886
+ const branches = this.hierarchy.activeBranches();
887
+ for (let i = branches.length - 1; i >= 0; i--) {
888
+ const branch = branches[i];
889
+ if (criterion?.(branch)) { return branch; }
890
+ }
891
+ return branches.at(0);
845
892
  }
846
893
 
847
- emit(event: ChatChangeEvent): void {
848
- this._onDidChangeEmitter.fire(event);
894
+ dispose(): void {
895
+ this.toDispose.dispose();
849
896
  }
850
897
  }
851
898
 
@@ -915,6 +962,11 @@ export class ChatRequestHierarchyImpl<TRequest extends ChatRequestModel = ChatRe
915
962
  notifyChange(event: ChangeActiveBranchEvent<TRequest>): void {
916
963
  this.onDidChangeActiveBranchEmitter.fire(event);
917
964
  }
965
+
966
+ dispose(): void {
967
+ this.onDidChangeActiveBranchEmitter.dispose();
968
+ this.branch.dispose();
969
+ }
918
970
  }
919
971
 
920
972
  export class ChatRequestHierarchyBranchImpl<TRequest extends ChatRequestModel> implements ChatHierarchyBranch<TRequest> {
@@ -1019,82 +1071,12 @@ export class ChatRequestHierarchyBranchImpl<TRequest extends ChatRequestModel> i
1019
1071
 
1020
1072
  return branches;
1021
1073
  }
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
1074
 
1095
1075
  dispose(): void {
1096
- this._onDidChangeEmitter.dispose();
1097
- this._elements.forEach(element => element.dispose?.());
1076
+ if (Disposable.is(this.get())) {
1077
+ this.items.forEach(({ element }) => Disposable.is(element) && element.dispose());
1078
+ }
1079
+ this.items.length = 0;
1098
1080
  }
1099
1081
  }
1100
1082
 
@@ -1161,10 +1143,13 @@ export class ChatContextManagerImpl implements ChatContextManager {
1161
1143
  }
1162
1144
 
1163
1145
  export class MutableChatRequestModel implements ChatRequestModel, EditableChatRequestModel, Disposable {
1146
+ protected readonly _onDidChangeEmitter = new Emitter<ChatChangeEvent>();
1147
+ onDidChange: Event<ChatChangeEvent> = this._onDidChangeEmitter.event;
1164
1148
  protected readonly _id: string;
1165
1149
  protected _session: MutableChatModel;
1166
1150
  protected _request: ChatRequest;
1167
1151
  protected _response: MutableChatResponseModel;
1152
+ protected _changeSet?: ChangeSetImpl;
1168
1153
  protected _context: ChatContext;
1169
1154
  protected _agentId?: string;
1170
1155
  protected _data: { [key: string]: unknown };
@@ -1185,9 +1170,20 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1185
1170
  this._data = data;
1186
1171
 
1187
1172
  this.editContextManager = new ChatContextManagerImpl(context);
1188
- this.toDispose.pushAll([
1189
- this.editContextManager.onDidChange(e => this.session.emit(e))
1190
- ]);
1173
+ this.editContextManager.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
1174
+ this.toDispose.push(this._onDidChangeEmitter);
1175
+ }
1176
+
1177
+ get changeSet(): ChangeSetImpl | undefined {
1178
+ return this._changeSet;
1179
+ }
1180
+
1181
+ set changeSet(changeSet: ChangeSetImpl) {
1182
+ this._changeSet?.dispose();
1183
+ this._changeSet = changeSet;
1184
+ this.toDispose.push(changeSet);
1185
+ changeSet.onDidChange(() => this._onDidChangeEmitter.fire({ kind: 'updateChangeSet', elements: changeSet.getElements(), title: changeSet.title }), this, this.toDispose);
1186
+ this._onDidChangeEmitter.fire({ kind: 'updateChangeSet', elements: changeSet.getElements(), title: changeSet.title });
1191
1187
  }
1192
1188
 
1193
1189
  get isEditing(): boolean {
@@ -1280,7 +1276,7 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1280
1276
  if (!branch) {
1281
1277
  throw new Error(`Cannot find hierarchy for requestId: ${request.id}`);
1282
1278
  }
1283
- this.session.emit({
1279
+ this._onDidChangeEmitter.fire({
1284
1280
  kind: 'enableEdit',
1285
1281
  request,
1286
1282
  branch,
@@ -1292,7 +1288,7 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1292
1288
  if (!branch) {
1293
1289
  throw new Error(`Cannot find branch for requestId: ${request.id}`);
1294
1290
  }
1295
- this.session.emit({
1291
+ this._onDidChangeEmitter.fire({
1296
1292
  kind: 'cancelEdit',
1297
1293
  request,
1298
1294
  branch,
@@ -1304,7 +1300,7 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1304
1300
  if (!branch) {
1305
1301
  throw new Error(`Cannot find branch for requestId: ${request.id}`);
1306
1302
  }
1307
- this.session.emit({
1303
+ this._onDidChangeEmitter.fire({
1308
1304
  kind: 'submitEdit',
1309
1305
  request,
1310
1306
  branch,
@@ -1359,6 +1355,7 @@ export class TextChatResponseContentImpl implements TextChatResponseContent {
1359
1355
  };
1360
1356
  }
1361
1357
  }
1358
+
1362
1359
  export class ThinkingChatResponseContentImpl implements ThinkingChatResponseContent {
1363
1360
  readonly kind = 'thinking';
1364
1361
  protected _content: string;
@@ -1501,6 +1498,9 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1501
1498
  protected _arguments?: string;
1502
1499
  protected _finished?: boolean;
1503
1500
  protected _result?: string;
1501
+ protected _confirmed: Promise<boolean>;
1502
+ protected _confirmationResolver?: (value: boolean) => void;
1503
+ protected _confirmationRejecter?: (reason?: unknown) => void;
1504
1504
 
1505
1505
  constructor(id?: string, name?: string, arg_string?: string, finished?: boolean, result?: string) {
1506
1506
  this._id = id;
@@ -1508,6 +1508,8 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1508
1508
  this._arguments = arg_string;
1509
1509
  this._finished = finished;
1510
1510
  this._result = result;
1511
+ // Initialize the confirmation promise immediately
1512
+ this._confirmed = this.createConfirmationPromise();
1511
1513
  }
1512
1514
 
1513
1515
  get id(): string | undefined {
@@ -1529,6 +1531,53 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1529
1531
  return this._result;
1530
1532
  }
1531
1533
 
1534
+ get confirmed(): Promise<boolean> {
1535
+ return this._confirmed;
1536
+ }
1537
+
1538
+ /**
1539
+ * Create a confirmation promise that can be resolved/rejected later
1540
+ */
1541
+ createConfirmationPromise(): Promise<boolean> {
1542
+ // The promise is always created, just ensure we have resolution handlers
1543
+ if (!this._confirmationResolver) {
1544
+ this._confirmed = new Promise<boolean>((resolve, reject) => {
1545
+ this._confirmationResolver = resolve;
1546
+ this._confirmationRejecter = reject;
1547
+ });
1548
+ }
1549
+ return this._confirmed;
1550
+ }
1551
+
1552
+ /**
1553
+ * Confirm the tool execution
1554
+ */
1555
+ confirm(): void {
1556
+ if (this._confirmationResolver) {
1557
+ this._confirmationResolver(true);
1558
+ }
1559
+ }
1560
+
1561
+ /**
1562
+ * Deny the tool execution
1563
+ */
1564
+ deny(): void {
1565
+ if (this._confirmationResolver) {
1566
+ this._confirmationResolver(false);
1567
+ this._finished = true;
1568
+ this._result = 'Tool execution denied by user';
1569
+ }
1570
+ }
1571
+
1572
+ /**
1573
+ * Cancel the confirmation (reject the promise)
1574
+ */
1575
+ cancelConfirmation(reason?: unknown): void {
1576
+ if (this._confirmationRejecter) {
1577
+ this._confirmationRejecter(reason);
1578
+ }
1579
+ }
1580
+
1532
1581
  asString(): string {
1533
1582
  return '';
1534
1583
  }
@@ -1543,6 +1592,7 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1543
1592
  this._result = nextChatResponseContent.result;
1544
1593
  const args = nextChatResponseContent.arguments;
1545
1594
  this._arguments = (args && args.length > 0) ? args : this._arguments;
1595
+ // Don't merge confirmation promises - they should be managed separately
1546
1596
  return true;
1547
1597
  }
1548
1598
  if (nextChatResponseContent.name !== undefined) {
@@ -104,6 +104,9 @@ export class ChatRequestParserImpl implements ChatRequestParser {
104
104
  const parts: ParsedChatRequestPart[] = [];
105
105
  const variables = new Map<string, AIVariable>();
106
106
  const toolRequests = new Map<string, ToolRequest>();
107
+ if (!request.text) {
108
+ return { parts, toolRequests, variables };
109
+ }
107
110
  const message = request.text;
108
111
  for (let i = 0; i < message.length; i++) {
109
112
  const previousChar = message.charAt(i - 1);