@theia/ai-chat 1.66.0-next.67 → 1.66.0-next.80

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 (83) hide show
  1. package/lib/browser/agent-delegation-tool.d.ts.map +1 -1
  2. package/lib/browser/agent-delegation-tool.js +4 -2
  3. package/lib/browser/agent-delegation-tool.js.map +1 -1
  4. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  5. package/lib/browser/ai-chat-frontend-module.js +15 -0
  6. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  7. package/lib/browser/change-set-file-element-deserializer.d.ts +7 -0
  8. package/lib/browser/change-set-file-element-deserializer.d.ts.map +1 -0
  9. package/lib/browser/change-set-file-element-deserializer.js +61 -0
  10. package/lib/browser/change-set-file-element-deserializer.js.map +1 -0
  11. package/lib/browser/change-set-file-element.d.ts +2 -0
  12. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  13. package/lib/browser/change-set-file-element.js +17 -0
  14. package/lib/browser/change-set-file-element.js.map +1 -1
  15. package/lib/browser/chat-session-store-impl.d.ts +36 -0
  16. package/lib/browser/chat-session-store-impl.d.ts.map +1 -0
  17. package/lib/browser/chat-session-store-impl.js +287 -0
  18. package/lib/browser/chat-session-store-impl.js.map +1 -0
  19. package/lib/common/change-set-element-deserializer.d.ts +30 -0
  20. package/lib/common/change-set-element-deserializer.d.ts.map +1 -0
  21. package/lib/common/change-set-element-deserializer.js +81 -0
  22. package/lib/common/change-set-element-deserializer.js.map +1 -0
  23. package/lib/common/change-set.d.ts +7 -0
  24. package/lib/common/change-set.d.ts.map +1 -1
  25. package/lib/common/change-set.js.map +1 -1
  26. package/lib/common/chat-auto-save.spec.d.ts +2 -0
  27. package/lib/common/chat-auto-save.spec.d.ts.map +1 -0
  28. package/lib/common/chat-auto-save.spec.js +304 -0
  29. package/lib/common/chat-auto-save.spec.js.map +1 -0
  30. package/lib/common/chat-content-deserializer.d.ts +161 -0
  31. package/lib/common/chat-content-deserializer.d.ts.map +1 -0
  32. package/lib/common/chat-content-deserializer.js +166 -0
  33. package/lib/common/chat-content-deserializer.js.map +1 -0
  34. package/lib/common/chat-content-deserializer.spec.d.ts +2 -0
  35. package/lib/common/chat-content-deserializer.spec.d.ts.map +1 -0
  36. package/lib/common/chat-content-deserializer.spec.js +307 -0
  37. package/lib/common/chat-content-deserializer.spec.js.map +1 -0
  38. package/lib/common/chat-model-serialization.d.ts +110 -0
  39. package/lib/common/chat-model-serialization.d.ts.map +1 -0
  40. package/lib/common/chat-model-serialization.js +20 -0
  41. package/lib/common/chat-model-serialization.js.map +1 -0
  42. package/lib/common/chat-model-serialization.spec.d.ts +2 -0
  43. package/lib/common/chat-model-serialization.spec.d.ts.map +1 -0
  44. package/lib/common/chat-model-serialization.spec.js +278 -0
  45. package/lib/common/chat-model-serialization.spec.js.map +1 -0
  46. package/lib/common/chat-model.d.ts +163 -14
  47. package/lib/common/chat-model.d.ts.map +1 -1
  48. package/lib/common/chat-model.js +444 -36
  49. package/lib/common/chat-model.js.map +1 -1
  50. package/lib/common/chat-service-deletion.spec.d.ts +2 -0
  51. package/lib/common/chat-service-deletion.spec.d.ts.map +1 -0
  52. package/lib/common/chat-service-deletion.spec.js +221 -0
  53. package/lib/common/chat-service-deletion.spec.js.map +1 -0
  54. package/lib/common/chat-service.d.ts +30 -2
  55. package/lib/common/chat-service.d.ts.map +1 -1
  56. package/lib/common/chat-service.js +182 -10
  57. package/lib/common/chat-service.js.map +1 -1
  58. package/lib/common/chat-session-store.d.ts +43 -0
  59. package/lib/common/chat-session-store.d.ts.map +1 -0
  60. package/lib/common/chat-session-store.js +20 -0
  61. package/lib/common/chat-session-store.js.map +1 -0
  62. package/lib/common/index.d.ts +3 -0
  63. package/lib/common/index.d.ts.map +1 -1
  64. package/lib/common/index.js +3 -0
  65. package/lib/common/index.js.map +1 -1
  66. package/package.json +9 -9
  67. package/src/browser/agent-delegation-tool.ts +4 -2
  68. package/src/browser/ai-chat-frontend-module.ts +27 -0
  69. package/src/browser/change-set-file-element-deserializer.ts +62 -0
  70. package/src/browser/change-set-file-element.ts +19 -0
  71. package/src/browser/chat-session-store-impl.ts +326 -0
  72. package/src/common/change-set-element-deserializer.ts +90 -0
  73. package/src/common/change-set.ts +8 -0
  74. package/src/common/chat-auto-save.spec.ts +372 -0
  75. package/src/common/chat-content-deserializer.spec.ts +375 -0
  76. package/src/common/chat-content-deserializer.ts +327 -0
  77. package/src/common/chat-model-serialization.spec.ts +343 -0
  78. package/src/common/chat-model-serialization.ts +133 -0
  79. package/src/common/chat-model.ts +644 -41
  80. package/src/common/chat-service-deletion.spec.ts +269 -0
  81. package/src/common/chat-service.ts +227 -10
  82. package/src/common/chat-session-store.ts +63 -0
  83. package/src/common/index.ts +3 -0
@@ -34,6 +34,16 @@ import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markd
34
34
  import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
35
35
  import { ChangeSet, ChangeSetElement, ChangeSetImpl, ChatUpdateChangeSetEvent } from './change-set';
36
36
  import { ChatAgentLocation } from './chat-agents';
37
+ import {
38
+ SerializedChatModel,
39
+ SerializableChatRequestData,
40
+ SerializableChatResponseContentData,
41
+ SerializableChatResponseData,
42
+ SerializableHierarchy,
43
+ SerializableHierarchyBranch,
44
+ SerializableHierarchyBranchItem,
45
+ SerializableChangeSetElement
46
+ } from './chat-model-serialization';
37
47
  import { ParsedChatRequest } from './parsed-chat-request';
38
48
  import debounce = require('@theia/core/shared/lodash.debounce');
39
49
  export { ChangeSet, ChangeSetElement, ChangeSetImpl };
@@ -54,6 +64,7 @@ export type ChatChangeEvent =
54
64
  | ChatEditRequestEvent
55
65
  | ChatEditCancelEvent
56
66
  | ChatEditSubmitEvent
67
+ | ChatResponseChangedEvent
57
68
  | ChatChangeHierarchyBranchEvent;
58
69
 
59
70
  export interface ChatAddRequestEvent {
@@ -107,6 +118,10 @@ export interface ChatSuggestionsChangedEvent {
107
118
  suggestions: ChatSuggestion[];
108
119
  }
109
120
 
121
+ export interface ChatResponseChangedEvent {
122
+ kind: 'responseChanged';
123
+ }
124
+
110
125
  export namespace ChatChangeEvent {
111
126
  export function isChangeSetEvent(event: ChatChangeEvent): event is ChatUpdateChangeSetEvent {
112
127
  return event.kind === 'updateChangeSet';
@@ -141,7 +156,8 @@ export interface ChatRequestHierarchy<TRequest extends ChatRequestModel = ChatRe
141
156
  findRequest(requestId: string): TRequest | undefined;
142
157
  findBranch(requestId: string): ChatHierarchyBranch<TRequest> | undefined;
143
158
 
144
- notifyChange(event: ChangeActiveBranchEvent<TRequest>): void
159
+ notifyChange(event: ChangeActiveBranchEvent<TRequest>): void;
160
+ toSerializable(): SerializableHierarchy;
145
161
  }
146
162
 
147
163
  export interface ChangeActiveBranchEvent<TRequest extends ChatRequestModel = ChatRequestModel> {
@@ -193,6 +209,7 @@ export interface ChatModel {
193
209
  getRequests(): ChatRequestModel[];
194
210
  getBranches(): ChatHierarchyBranch<ChatRequestModel>[];
195
211
  isEmpty(): boolean;
212
+ toSerializable(): SerializedChatModel;
196
213
  }
197
214
 
198
215
  export interface ChatSuggestionCallback {
@@ -251,6 +268,7 @@ export interface ChatRequestModel {
251
268
  readonly context: ChatContext;
252
269
  readonly agentId?: string;
253
270
  readonly data?: { [key: string]: unknown };
271
+ toSerializable(): SerializableChatRequestData;
254
272
  }
255
273
 
256
274
  export namespace ChatRequestModel {
@@ -321,6 +339,7 @@ export interface ChatResponseContent {
321
339
  asDisplayString?(): string | undefined;
322
340
  merge?(nextChatResponseContent: ChatResponseContent): boolean;
323
341
  toLanguageModelMessage?(): LanguageModelMessage | LanguageModelMessage[];
342
+ toSerializable?(): SerializableChatResponseContentData;
324
343
  }
325
344
 
326
345
  export namespace ChatResponseContent {
@@ -354,6 +373,71 @@ export namespace ChatResponseContent {
354
373
  }
355
374
  }
356
375
 
376
+ /**
377
+ * Data interfaces for chat response content serialization.
378
+ * These define the structure of the data property in SerializableChatResponseContentData.
379
+ */
380
+
381
+ export interface TextContentData {
382
+ content: string;
383
+ }
384
+
385
+ export interface ThinkingContentData {
386
+ content: string;
387
+ signature: string;
388
+ }
389
+
390
+ export interface MarkdownContentData {
391
+ content: string;
392
+ }
393
+
394
+ export interface InformationalContentData {
395
+ content: string;
396
+ }
397
+
398
+ export interface CodeContentData {
399
+ code: string;
400
+ language?: string;
401
+ location?: Location;
402
+ }
403
+
404
+ export interface ToolCallContentData {
405
+ id?: string;
406
+ name?: string;
407
+ arguments?: string;
408
+ finished?: boolean;
409
+ result?: ToolCallResult;
410
+ }
411
+
412
+ export interface CommandContentData {
413
+ commandId?: string;
414
+ commandLabel?: string;
415
+ arguments?: unknown[];
416
+ }
417
+
418
+ export interface HorizontalLayoutContentData {
419
+ content: SerializableChatResponseContentData[];
420
+ }
421
+
422
+ export interface ProgressContentData {
423
+ message: string;
424
+ }
425
+
426
+ export interface ErrorContentData {
427
+ message: string;
428
+ stack?: string;
429
+ }
430
+
431
+ /**
432
+ * Restored questions display the question, options, and any previously selected answer,
433
+ * but do not allow new selections.
434
+ */
435
+ export interface QuestionContentData {
436
+ question: string;
437
+ options: { text: string; value?: string }[];
438
+ selectedOption?: { text: string; value?: string };
439
+ }
440
+
357
441
  export interface TextChatResponseContent
358
442
  extends Required<ChatResponseContent> {
359
443
  kind: 'text';
@@ -566,8 +650,13 @@ export interface QuestionResponseContent extends ChatResponseContent {
566
650
  question: string;
567
651
  options: { text: string, value?: string }[];
568
652
  selectedOption?: { text: string, value?: string };
569
- handler: QuestionResponseHandler;
570
- request: MutableChatRequestModel;
653
+ handler?: QuestionResponseHandler;
654
+ request?: MutableChatRequestModel;
655
+ /**
656
+ * Whether this question is read-only (restored from persistence without handler).
657
+ * When true, the UI should disable option selection.
658
+ */
659
+ readonly isReadOnly: boolean;
571
660
  }
572
661
 
573
662
  export namespace QuestionResponseContent {
@@ -585,10 +674,9 @@ export namespace QuestionResponseContent {
585
674
  typeof (option as { text: unknown }).text === 'string' &&
586
675
  ('value' in option ? typeof (option as { value: unknown }).value === 'string' || typeof (option as { value: unknown }).value === 'undefined' : true)
587
676
  ) &&
588
- 'handler' in obj &&
589
- typeof (obj as { handler: unknown }).handler === 'function' &&
590
- 'request' in obj &&
591
- obj.request instanceof MutableChatRequestModel
677
+ // handler and request are optional (undefined for restored/read-only questions)
678
+ ('handler' in obj ? (obj as { handler: unknown }).handler === undefined || typeof (obj as { handler: unknown }).handler === 'function' : true) &&
679
+ ('request' in obj ? (obj as { request: unknown }).request === undefined || (obj as { request: unknown }).request instanceof MutableChatRequestModel : true)
592
680
  );
593
681
  }
594
682
  }
@@ -652,6 +740,7 @@ export interface ChatResponseModel {
652
740
  * This can be used to store and retrieve such data.
653
741
  */
654
742
  readonly data: { [key: string]: unknown };
743
+ toSerializable(): SerializableChatResponseData;
655
744
  }
656
745
 
657
746
  /**********************
@@ -668,16 +757,29 @@ export class MutableChatModel implements ChatModel, Disposable {
668
757
  protected _id: string;
669
758
  protected _suggestions: readonly ChatSuggestion[] = [];
670
759
  protected readonly _contextManager = new ChatContextManagerImpl();
671
- protected readonly _changeSet: ChatTreeChangeSet;
760
+ protected _changeSet: ChatTreeChangeSet;
672
761
  protected _settings: { [key: string]: unknown };
762
+ protected _location: ChatAgentLocation;
673
763
 
674
- constructor(public readonly location = ChatAgentLocation.Panel) {
675
- // TODO accept serialized data as a parameter to restore a previously saved ChatModel
676
- this._hierarchy = new ChatRequestHierarchyImpl<MutableChatRequestModel>();
677
- this._changeSet = new ChatTreeChangeSet(this._hierarchy);
678
- this.toDispose.push(this._changeSet);
679
- this._changeSet.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
680
- this._id = generateUuid();
764
+ get location(): ChatAgentLocation {
765
+ return this._location;
766
+ }
767
+
768
+ constructor(
769
+ locationOrSerializedData: ChatAgentLocation | SerializedChatModel = ChatAgentLocation.Panel
770
+ ) {
771
+ // Check if we're restoring from serialized data
772
+ if (this.isSerializedChatModel(locationOrSerializedData)) {
773
+ this.restoreFromSerializedData(locationOrSerializedData);
774
+ } else {
775
+ // Normal creation path
776
+ this._location = locationOrSerializedData;
777
+ this._id = generateUuid();
778
+ this._hierarchy = new ChatRequestHierarchyImpl<MutableChatRequestModel>();
779
+ this._changeSet = new ChatTreeChangeSet(this._hierarchy);
780
+ this.toDispose.push(this._changeSet);
781
+ this._changeSet.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
782
+ }
681
783
 
682
784
  this.toDispose.pushAll([
683
785
  this._onDidChangeEmitter,
@@ -691,6 +793,44 @@ export class MutableChatModel implements ChatModel, Disposable {
691
793
  ]);
692
794
  }
693
795
 
796
+ /**
797
+ * Type guard to determine if we're receiving serialized data
798
+ */
799
+ protected isSerializedChatModel(data: ChatAgentLocation | SerializedChatModel): data is SerializedChatModel {
800
+ return typeof data === 'object' && 'sessionId' in data && 'requests' in data && 'responses' in data;
801
+ }
802
+
803
+ /**
804
+ * Restore this chat model from serialized data
805
+ *
806
+ * Does not restore response content or changesets.
807
+ * This handled by the chat service using the deserializer registries.
808
+ */
809
+ protected restoreFromSerializedData(data: SerializedChatModel): void {
810
+ this._id = data.sessionId;
811
+ this._location = data.location;
812
+
813
+ // First, create all request models and build a map
814
+ const requestMap = new Map<string, MutableChatRequestModel>();
815
+ for (const reqData of data.requests) {
816
+ const respData = data.responses.find(r => r.requestId === reqData.id);
817
+ const requestModel = new MutableChatRequestModel(this, reqData, respData);
818
+ requestMap.set(requestModel.id, requestModel);
819
+ }
820
+
821
+ // Restore the hierarchy structure with all alternatives
822
+ this._hierarchy = new ChatRequestHierarchyImpl<MutableChatRequestModel>(data.hierarchy, requestMap);
823
+
824
+ // Register all requests with changeset
825
+ this._changeSet = new ChatTreeChangeSet(this._hierarchy);
826
+ this.toDispose.push(this._changeSet);
827
+ this._changeSet.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
828
+
829
+ for (const requestModel of requestMap.values()) {
830
+ this._changeSet.registerRequest(requestModel);
831
+ }
832
+ }
833
+
694
834
  get id(): string {
695
835
  return this._id;
696
836
  }
@@ -769,6 +909,52 @@ export class MutableChatModel implements ChatModel, Disposable {
769
909
  return this.getRequests().length === 0;
770
910
  }
771
911
 
912
+ toSerializable(): SerializedChatModel {
913
+ const hierarchy = this._hierarchy.toSerializable();
914
+
915
+ const allRequests = this.getAllRequests();
916
+
917
+ const serializedRequests: SerializableChatRequestData[] = allRequests.map(req => req.toSerializable());
918
+ const serializedResponses: SerializableChatResponseData[] = allRequests
919
+ .filter(req => req.response)
920
+ .map(req => req.response.toSerializable());
921
+
922
+ return {
923
+ sessionId: this._id,
924
+ location: this.location,
925
+ hierarchy,
926
+ requests: serializedRequests,
927
+ responses: serializedResponses
928
+ };
929
+ }
930
+
931
+ /**
932
+ * Get all requests from the hierarchy.
933
+ * This is used for operations that need to process all requests, such as serialization.
934
+ */
935
+ getAllRequests(): MutableChatRequestModel[] {
936
+ const allRequests: MutableChatRequestModel[] = [];
937
+ const visited = new Set<string>();
938
+
939
+ const collectFromBranch = (branch: ChatHierarchyBranch<MutableChatRequestModel>): void => {
940
+ for (const item of branch.items) {
941
+ // Avoid duplicates
942
+ if (!visited.has(item.element.id)) {
943
+ visited.add(item.element.id);
944
+ allRequests.push(item.element);
945
+ }
946
+
947
+ // Recursively collect from next branches
948
+ if (item.next) {
949
+ collectFromBranch(item.next);
950
+ }
951
+ }
952
+ };
953
+
954
+ collectFromBranch(this._hierarchy.branch);
955
+ return allRequests;
956
+ }
957
+
772
958
  dispose(): void {
773
959
  this.toDispose.dispose();
774
960
  }
@@ -902,7 +1088,89 @@ export class ChatRequestHierarchyImpl<TRequest extends ChatRequestModel = ChatRe
902
1088
  protected readonly onDidChangeActiveBranchEmitter = new Emitter<ChangeActiveBranchEvent<TRequest>>();
903
1089
  readonly onDidChange = this.onDidChangeActiveBranchEmitter.event;
904
1090
 
905
- readonly branch: ChatHierarchyBranch<TRequest> = new ChatRequestHierarchyBranchImpl<TRequest>(this);
1091
+ readonly branch: ChatHierarchyBranch<TRequest>;
1092
+
1093
+ constructor(
1094
+ serializedHierarchy?: SerializableHierarchy,
1095
+ requestMap?: Map<string, TRequest>) {
1096
+ this.branch = new ChatRequestHierarchyBranchImpl<TRequest>(this);
1097
+ if (serializedHierarchy && requestMap) {
1098
+ this.restoreFromSerialized(serializedHierarchy, requestMap);
1099
+ }
1100
+ }
1101
+
1102
+ /**
1103
+ * Restore the hierarchy from serialized data.
1104
+ */
1105
+ protected restoreFromSerialized(
1106
+ serializedHierarchy: SerializableHierarchy,
1107
+ requestMap: Map<string, TRequest>
1108
+ ): void {
1109
+ // Build a map of branch IDs to restored branch objects
1110
+ const branchMap = new Map<string, ChatHierarchyBranch<TRequest>>();
1111
+
1112
+ // Function to restore a branch and its descendants
1113
+ const restoreBranch = (branchId: string): ChatHierarchyBranch<TRequest> => {
1114
+ // Check if already restored
1115
+ if (branchMap.has(branchId)) {
1116
+ return branchMap.get(branchId)!;
1117
+ }
1118
+
1119
+ const serializedBranch = serializedHierarchy.branches[branchId];
1120
+ if (!serializedBranch) {
1121
+ throw new Error(`Cannot find serialized branch with id: ${branchId}`);
1122
+ }
1123
+
1124
+ // Restore items in this branch
1125
+ const items: ChatHierarchyBranchItem<TRequest>[] = serializedBranch.items.map(serializedItem => {
1126
+ const request = requestMap.get(serializedItem.requestId);
1127
+ if (!request) {
1128
+ throw new Error(`Cannot find request with id: ${serializedItem.requestId}`);
1129
+ }
1130
+
1131
+ // Restore next branch if present
1132
+ const next = serializedItem.nextBranchId
1133
+ ? restoreBranch(serializedItem.nextBranchId)
1134
+ : undefined;
1135
+
1136
+ return {
1137
+ element: request,
1138
+ next
1139
+ };
1140
+ });
1141
+
1142
+ // Determine if this is the root branch
1143
+ const isRoot = branchId === serializedHierarchy.rootBranchId;
1144
+
1145
+ if (isRoot) {
1146
+ // For root branch, we need to replace the existing branch's internals
1147
+ // Cast is safe here as we know this.branch is a ChatRequestHierarchyBranchImpl
1148
+ const rootBranch = this.branch as ChatRequestHierarchyBranchImpl<TRequest>;
1149
+ // Use Object.assign to update the readonly properties
1150
+ Object.assign(rootBranch, {
1151
+ id: branchId,
1152
+ items,
1153
+ _activeIndex: serializedBranch.activeBranchIndex
1154
+ });
1155
+ branchMap.set(branchId, rootBranch);
1156
+ return rootBranch;
1157
+ } else {
1158
+ // For non-root branches, use constructor-based deserialization
1159
+ const restoredBranch = new ChatRequestHierarchyBranchImpl<TRequest>(
1160
+ this,
1161
+ undefined, // previous will be set by parent
1162
+ items,
1163
+ serializedBranch.activeBranchIndex,
1164
+ branchId
1165
+ );
1166
+ branchMap.set(branchId, restoredBranch);
1167
+ return restoredBranch;
1168
+ }
1169
+ };
1170
+
1171
+ // Start restoration from the root branch
1172
+ restoreBranch(serializedHierarchy.rootBranchId);
1173
+ }
906
1174
 
907
1175
  append(request: TRequest): ChatHierarchyBranch<TRequest> {
908
1176
  const branches = this.activeBranches();
@@ -965,6 +1233,39 @@ export class ChatRequestHierarchyImpl<TRequest extends ChatRequestModel = ChatRe
965
1233
  this.onDidChangeActiveBranchEmitter.fire(event);
966
1234
  }
967
1235
 
1236
+ toSerializable(): SerializableHierarchy {
1237
+ const branches: { [branchId: string]: SerializableHierarchyBranch } = {};
1238
+
1239
+ // Recursively serialize all branches starting from the root
1240
+ this.serializeBranch(this.branch, branches);
1241
+
1242
+ return {
1243
+ rootBranchId: this.branch.id,
1244
+ branches
1245
+ };
1246
+ }
1247
+
1248
+ protected serializeBranch(
1249
+ branch: ChatHierarchyBranch<TRequest>,
1250
+ branches: { [branchId: string]: SerializableHierarchyBranch }
1251
+ ): void {
1252
+ const items: SerializableHierarchyBranchItem[] = branch.items.map(item => {
1253
+ if (item.next) {
1254
+ this.serializeBranch(item.next, branches);
1255
+ }
1256
+ return {
1257
+ requestId: item.element.id,
1258
+ nextBranchId: item.next?.id
1259
+ };
1260
+ });
1261
+
1262
+ branches[branch.id] = {
1263
+ id: branch.id,
1264
+ items,
1265
+ activeBranchIndex: branch.activeBranchIndex
1266
+ };
1267
+ }
1268
+
968
1269
  dispose(): void {
969
1270
  this.onDidChangeActiveBranchEmitter.dispose();
970
1271
  this.branch.dispose();
@@ -972,14 +1273,17 @@ export class ChatRequestHierarchyImpl<TRequest extends ChatRequestModel = ChatRe
972
1273
  }
973
1274
 
974
1275
  export class ChatRequestHierarchyBranchImpl<TRequest extends ChatRequestModel> implements ChatHierarchyBranch<TRequest> {
975
- readonly id = generateUuid();
1276
+ readonly id: string;
976
1277
 
977
1278
  constructor(
978
1279
  readonly hierarchy: ChatRequestHierarchy<TRequest>,
979
1280
  readonly previous?: ChatHierarchyBranch<TRequest>,
980
1281
  readonly items: ChatHierarchyBranchItem<TRequest>[] = [],
981
- protected _activeIndex = -1
982
- ) { }
1282
+ protected _activeIndex = -1,
1283
+ id?: string
1284
+ ) {
1285
+ this.id = id ?? generateUuid();
1286
+ }
983
1287
 
984
1288
  get activeBranchIndex(): number {
985
1289
  return this._activeIndex;
@@ -1147,7 +1451,7 @@ export class ChatContextManagerImpl implements ChatContextManager {
1147
1451
  export class MutableChatRequestModel implements ChatRequestModel, EditableChatRequestModel, Disposable {
1148
1452
  protected readonly _onDidChangeEmitter = new Emitter<ChatChangeEvent>();
1149
1453
  onDidChange: Event<ChatChangeEvent> = this._onDidChangeEmitter.event;
1150
- protected readonly _id: string;
1454
+ protected _id: string;
1151
1455
  protected _session: MutableChatModel;
1152
1456
  protected _request: ChatRequest;
1153
1457
  protected _response: MutableChatResponseModel;
@@ -1156,26 +1460,93 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1156
1460
  protected _agentId?: string;
1157
1461
  protected _data: { [key: string]: unknown };
1158
1462
  protected _isEditing = false;
1463
+ readonly message: ParsedChatRequest;
1159
1464
 
1160
1465
  protected readonly toDispose = new DisposableCollection();
1161
1466
  readonly editContextManager: ChatContextManagerImpl;
1162
1467
 
1163
- constructor(session: MutableChatModel, public readonly message: ParsedChatRequest, agentId?: string,
1164
- context: ChatContext = { variables: [] }, data: { [key: string]: unknown } = {}) {
1165
- // TODO accept serialized data as a parameter to restore a previously saved ChatRequestModel
1166
- this._request = message.request;
1167
- this._id = generateUuid();
1468
+ constructor(
1469
+ session: MutableChatModel,
1470
+ messageOrData: ParsedChatRequest | SerializableChatRequestData,
1471
+ agentIdOrResponseData?: string | SerializableChatResponseData,
1472
+ context: ChatContext = { variables: [] },
1473
+ data: { [key: string]: unknown } = {}
1474
+ ) {
1168
1475
  this._session = session;
1169
- this._response = new MutableChatResponseModel(this._id, agentId);
1170
- this._context = context;
1171
- this._agentId = agentId;
1172
- this._data = data;
1173
1476
 
1174
- this.editContextManager = new ChatContextManagerImpl(context);
1477
+ // Check if we're restoring from serialized data
1478
+ if (this.isSerializedRequestData(messageOrData)) {
1479
+ this.restoreFromSerializedData(messageOrData, agentIdOrResponseData as SerializableChatResponseData | undefined);
1480
+ } else {
1481
+ // Normal creation path
1482
+ this._request = messageOrData.request;
1483
+ this._id = generateUuid();
1484
+ this._response = new MutableChatResponseModel(this._id, agentIdOrResponseData as string | undefined);
1485
+ this._context = context;
1486
+ this._agentId = agentIdOrResponseData as string | undefined;
1487
+ this._data = data;
1488
+ // Store the parsed message
1489
+ this.message = messageOrData;
1490
+ }
1491
+
1492
+ this.editContextManager = new ChatContextManagerImpl(this._context);
1175
1493
  this.editContextManager.onDidChange(this._onDidChangeEmitter.fire, this._onDidChangeEmitter, this.toDispose);
1494
+
1495
+ // Wire response changes to propagate through request to session
1496
+ this._response.onDidChange(() => {
1497
+ // Fire a generic addVariable event to propagate response changes
1498
+ this._onDidChangeEmitter.fire({ kind: 'responseChanged' });
1499
+ }, this, this.toDispose);
1500
+
1176
1501
  this.toDispose.push(this._onDidChangeEmitter);
1177
1502
  }
1178
1503
 
1504
+ /**
1505
+ * Type guard to determine if we're receiving serialized data
1506
+ */
1507
+ protected isSerializedRequestData(data: ParsedChatRequest | SerializableChatRequestData): data is SerializableChatRequestData {
1508
+ return 'id' in data && 'text' in data && !('request' in data);
1509
+ }
1510
+
1511
+ /**
1512
+ * Restore this request model from serialized data
1513
+ */
1514
+ protected restoreFromSerializedData(
1515
+ reqData: SerializableChatRequestData,
1516
+ respData?: SerializableChatResponseData
1517
+ ): void {
1518
+ this._id = reqData.id;
1519
+ this._request = { text: reqData.text };
1520
+ this._agentId = reqData.agentId;
1521
+ this._data = {};
1522
+
1523
+ // Create minimal context
1524
+ this._context = { variables: [] };
1525
+
1526
+ // TODO: More sophisticated restoration?
1527
+ // Casting required because 'message' is readonly
1528
+ (this as { message: ParsedChatRequest }).message = {
1529
+ request: this._request,
1530
+ parts: [{
1531
+ kind: 'text',
1532
+ text: reqData.text,
1533
+ promptText: reqData.text,
1534
+ range: { start: 0, endExclusive: reqData.text.length }
1535
+ }],
1536
+ toolRequests: new Map(),
1537
+ variables: []
1538
+ };
1539
+
1540
+ // Restore response if present
1541
+ if (respData) {
1542
+ this._response = new MutableChatResponseModel(this._id, this._agentId, respData);
1543
+ } else {
1544
+ this._response = new MutableChatResponseModel(this._id, this._agentId);
1545
+ }
1546
+
1547
+ // Note: ChangeSet restoration will be handled by ChatService using deserializer registry
1548
+ }
1549
+
1179
1550
  get changeSet(): ChangeSetImpl | undefined {
1180
1551
  return this._changeSet;
1181
1552
  }
@@ -1265,6 +1636,18 @@ export class MutableChatRequestModel implements ChatRequestModel, EditableChatRe
1265
1636
  this.response.cancel();
1266
1637
  }
1267
1638
 
1639
+ toSerializable(): SerializableChatRequestData {
1640
+ return {
1641
+ id: this.id,
1642
+ text: this.request.text,
1643
+ agentId: this.agentId,
1644
+ changeSet: this._changeSet ? {
1645
+ title: this._changeSet.title,
1646
+ elements: this._changeSet.getElements().map(elem => elem.toSerializable?.()).filter((elem): elem is SerializableChangeSetElement => elem !== undefined)
1647
+ } : undefined
1648
+ };
1649
+ }
1650
+
1268
1651
  dispose(): void {
1269
1652
  this.toDispose.dispose();
1270
1653
  }
@@ -1323,6 +1706,15 @@ export class ErrorChatResponseContentImpl implements ErrorChatResponseContent {
1323
1706
  asString(): string | undefined {
1324
1707
  return undefined;
1325
1708
  }
1709
+ toSerializable(): SerializableChatResponseContentData<ErrorContentData> {
1710
+ return {
1711
+ kind: 'error',
1712
+ data: {
1713
+ message: this._error.message,
1714
+ stack: this._error.stack
1715
+ }
1716
+ };
1717
+ }
1326
1718
  }
1327
1719
 
1328
1720
  export class TextChatResponseContentImpl implements TextChatResponseContent {
@@ -1349,6 +1741,7 @@ export class TextChatResponseContentImpl implements TextChatResponseContent {
1349
1741
  this._content += nextChatResponseContent.content;
1350
1742
  return true;
1351
1743
  }
1744
+
1352
1745
  toLanguageModelMessage(): TextMessage {
1353
1746
  return {
1354
1747
  actor: 'ai',
@@ -1356,6 +1749,13 @@ export class TextChatResponseContentImpl implements TextChatResponseContent {
1356
1749
  text: this.content
1357
1750
  };
1358
1751
  }
1752
+
1753
+ toSerializable(): SerializableChatResponseContentData<TextContentData> {
1754
+ return {
1755
+ kind: 'text',
1756
+ data: { content: this._content }
1757
+ };
1758
+ }
1359
1759
  }
1360
1760
 
1361
1761
  export class ThinkingChatResponseContentImpl implements ThinkingChatResponseContent {
@@ -1401,6 +1801,16 @@ export class ThinkingChatResponseContentImpl implements ThinkingChatResponseCont
1401
1801
  signature: this.signature
1402
1802
  };
1403
1803
  }
1804
+
1805
+ toSerializable(): SerializableChatResponseContentData<ThinkingContentData> {
1806
+ return {
1807
+ kind: 'thinking',
1808
+ data: {
1809
+ content: this._content,
1810
+ signature: this._signature
1811
+ }
1812
+ };
1813
+ }
1404
1814
  }
1405
1815
 
1406
1816
  export class MarkdownChatResponseContentImpl implements MarkdownChatResponseContent {
@@ -1435,6 +1845,13 @@ export class MarkdownChatResponseContentImpl implements MarkdownChatResponseCont
1435
1845
  text: this.content.value
1436
1846
  };
1437
1847
  }
1848
+
1849
+ toSerializable(): SerializableChatResponseContentData<MarkdownContentData> {
1850
+ return {
1851
+ kind: 'markdownContent',
1852
+ data: { content: this._content.value }
1853
+ };
1854
+ }
1438
1855
  }
1439
1856
 
1440
1857
  export class InformationalChatResponseContentImpl implements InformationalChatResponseContent {
@@ -1457,6 +1874,13 @@ export class InformationalChatResponseContentImpl implements InformationalChatRe
1457
1874
  this._content.appendMarkdown(nextChatResponseContent.content.value);
1458
1875
  return true;
1459
1876
  }
1877
+
1878
+ toSerializable(): SerializableChatResponseContentData<InformationalContentData> {
1879
+ return {
1880
+ kind: 'informational',
1881
+ data: { content: this._content.value }
1882
+ };
1883
+ }
1460
1884
  }
1461
1885
 
1462
1886
  export class CodeChatResponseContentImpl implements CodeChatResponseContent {
@@ -1491,6 +1915,17 @@ export class CodeChatResponseContentImpl implements CodeChatResponseContent {
1491
1915
  this._code += `${nextChatResponseContent.code}`;
1492
1916
  return true;
1493
1917
  }
1918
+
1919
+ toSerializable(): SerializableChatResponseContentData<CodeContentData> {
1920
+ return {
1921
+ kind: 'code',
1922
+ data: {
1923
+ code: this._code,
1924
+ language: this._language,
1925
+ location: this._location
1926
+ }
1927
+ };
1928
+ }
1494
1929
  }
1495
1930
 
1496
1931
  export class ToolCallChatResponseContentImpl implements ToolCallChatResponseContent {
@@ -1622,6 +2057,19 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
1622
2057
  name: this.name ?? ''
1623
2058
  }];
1624
2059
  }
2060
+
2061
+ toSerializable(): SerializableChatResponseContentData<ToolCallContentData> {
2062
+ return {
2063
+ kind: 'toolCall',
2064
+ data: {
2065
+ id: this._id,
2066
+ name: this._name,
2067
+ arguments: this._arguments,
2068
+ finished: this._finished,
2069
+ result: this._result
2070
+ }
2071
+ };
2072
+ }
1625
2073
  }
1626
2074
 
1627
2075
  export const COMMAND_CHAT_RESPONSE_COMMAND: Command = {
@@ -1639,6 +2087,17 @@ export class CommandChatResponseContentImpl implements CommandChatResponseConten
1639
2087
  asString(): string {
1640
2088
  return this.command?.id || this.customCallback?.label || 'command';
1641
2089
  }
2090
+
2091
+ toSerializable(): SerializableChatResponseContentData<CommandContentData> {
2092
+ return {
2093
+ kind: 'command',
2094
+ data: {
2095
+ commandId: this.command?.id,
2096
+ commandLabel: this.customCallback?.label,
2097
+ arguments: this.args
2098
+ }
2099
+ };
2100
+ }
1642
2101
  }
1643
2102
 
1644
2103
  export class HorizontalLayoutChatResponseContentImpl implements HorizontalLayoutChatResponseContent {
@@ -1669,20 +2128,58 @@ export class HorizontalLayoutChatResponseContentImpl implements HorizontalLayout
1669
2128
  }
1670
2129
  return true;
1671
2130
  }
2131
+
2132
+ toSerializable(): SerializableChatResponseContentData<HorizontalLayoutContentData> {
2133
+ return {
2134
+ kind: 'horizontal',
2135
+ data: {
2136
+ content: this._content.map(child => {
2137
+ const serialized = child.toSerializable?.();
2138
+ if (!serialized) {
2139
+ return {
2140
+ kind: child.kind,
2141
+ fallbackMessage: child.asString?.(),
2142
+ data: undefined
2143
+ };
2144
+ }
2145
+ return {
2146
+ ...serialized,
2147
+ fallbackMessage: child.asString?.()
2148
+ };
2149
+ })
2150
+ }
2151
+ };
2152
+ }
1672
2153
  }
1673
2154
 
1674
2155
  /**
1675
2156
  * Default implementation for the QuestionResponseContent.
2157
+ * Can be created with or without handler/request for read-only (restored) mode.
1676
2158
  */
1677
2159
  export class QuestionResponseContentImpl implements QuestionResponseContent {
1678
2160
  readonly kind = 'question';
1679
2161
  protected _selectedOption: { text: string; value?: string } | undefined;
1680
- constructor(public question: string, public options: { text: string, value?: string }[],
1681
- public request: MutableChatRequestModel, public handler: QuestionResponseHandler) {
2162
+
2163
+ constructor(
2164
+ public question: string,
2165
+ public options: { text: string, value?: string }[],
2166
+ public request: MutableChatRequestModel | undefined,
2167
+ public handler: QuestionResponseHandler | undefined,
2168
+ selectedOption?: { text: string; value?: string }
2169
+ ) {
2170
+ this._selectedOption = selectedOption;
2171
+ }
2172
+
2173
+ get isReadOnly(): boolean {
2174
+ return !this.handler || !this.request;
1682
2175
  }
2176
+
1683
2177
  set selectedOption(option: { text: string; value?: string; } | undefined) {
1684
2178
  this._selectedOption = option;
1685
- this.request.response.response.responseContentChanged();
2179
+ // Only trigger change notification if request is available (not in read-only mode)
2180
+ if (this.request) {
2181
+ this.request.response.response.responseContentChanged();
2182
+ }
1686
2183
  }
1687
2184
  get selectedOption(): { text: string; value?: string; } | undefined {
1688
2185
  return this._selectedOption;
@@ -1694,6 +2191,16 @@ ${this.selectedOption ? `Answer: ${this.selectedOption?.text}` : 'No answer'}`;
1694
2191
  merge?(): boolean {
1695
2192
  return false;
1696
2193
  }
2194
+ toSerializable(): SerializableChatResponseContentData<QuestionContentData> {
2195
+ return {
2196
+ kind: 'question',
2197
+ data: {
2198
+ question: this.question,
2199
+ options: this.options,
2200
+ selectedOption: this._selectedOption
2201
+ }
2202
+ };
2203
+ }
1697
2204
  }
1698
2205
 
1699
2206
  class ChatResponseImpl implements ChatResponse {
@@ -1704,7 +2211,6 @@ class ChatResponseImpl implements ChatResponse {
1704
2211
  protected _responseRepresentationForDisplay: string;
1705
2212
 
1706
2213
  constructor() {
1707
- // TODO accept serialized data as a parameter to restore a previously saved ChatResponse
1708
2214
  this._content = [];
1709
2215
  }
1710
2216
 
@@ -1815,18 +2321,52 @@ export class MutableChatResponseModel implements ChatResponseModel {
1815
2321
  protected _errorObject: Error | undefined;
1816
2322
  protected _cancellationToken: CancellationTokenSource;
1817
2323
 
1818
- constructor(requestId: string, agentId?: string) {
1819
- // TODO accept serialized data as a parameter to restore a previously saved ChatResponseModel
2324
+ constructor(
2325
+ requestId: string,
2326
+ agentId?: string,
2327
+ serializedData?: SerializableChatResponseData
2328
+ ) {
1820
2329
  this._requestId = requestId;
1821
- this._id = generateUuid();
1822
- this._progressMessages = [];
2330
+ this._agentId = agentId;
2331
+ this._cancellationToken = new CancellationTokenSource();
2332
+
2333
+ // Check if we're restoring from serialized data
2334
+ if (serializedData) {
2335
+ this.restoreFromSerializedData(serializedData);
2336
+ } else {
2337
+ // Normal creation path
2338
+ this._id = generateUuid();
2339
+ this._progressMessages = [];
2340
+ this._isComplete = false;
2341
+ this._isWaitingForInput = false;
2342
+ this._isError = false;
2343
+ }
2344
+
1823
2345
  const response = new ChatResponseImpl();
1824
2346
  response.onDidChange(() => this._onDidChangeEmitter.fire());
1825
2347
  this._response = response;
1826
- this._isComplete = false;
2348
+ }
2349
+
2350
+ /**
2351
+ * Restore this response model from serialized data
2352
+ */
2353
+ protected restoreFromSerializedData(data: SerializableChatResponseData): void {
2354
+ this._id = data.id;
2355
+ // Always mark restored responses as complete since there's no active agent
2356
+ this._isComplete = true;
2357
+ this._isError = data.isError;
2358
+
2359
+ // Do not restore waitingForInput state - when a session is restored,
2360
+ // the agent that was waiting for input is no longer running
1827
2361
  this._isWaitingForInput = false;
1828
- this._agentId = agentId;
1829
- this._cancellationToken = new CancellationTokenSource();
2362
+ // TODO: Restore progressMessages?
2363
+ this._progressMessages = [];
2364
+
2365
+ if (data.errorMessage) {
2366
+ this._errorObject = new Error(data.errorMessage);
2367
+ }
2368
+
2369
+ // Note: Content restoration will be handled by ChatService using deserializer registry
1830
2370
  }
1831
2371
 
1832
2372
  get id(): string {
@@ -1949,6 +2489,31 @@ export class MutableChatResponseModel implements ChatResponseModel {
1949
2489
  get isError(): boolean {
1950
2490
  return this._isError;
1951
2491
  }
2492
+
2493
+ toSerializable(): SerializableChatResponseData {
2494
+ return {
2495
+ id: this.id,
2496
+ requestId: this.requestId,
2497
+ isComplete: this.isComplete,
2498
+ isError: this.isError,
2499
+ errorMessage: this.errorObject?.message,
2500
+ content: this.response.content.map(c => {
2501
+ const serialized = c.toSerializable?.();
2502
+ if (!serialized) {
2503
+ // Fallback if toSerializable not implemented
2504
+ return {
2505
+ kind: c.kind,
2506
+ fallbackMessage: c.asString?.(),
2507
+ data: undefined
2508
+ };
2509
+ }
2510
+ return {
2511
+ ...serialized,
2512
+ fallbackMessage: c.asString?.()
2513
+ };
2514
+ })
2515
+ };
2516
+ }
1952
2517
  }
1953
2518
 
1954
2519
  export class ErrorChatResponseModel extends MutableChatResponseModel {
@@ -1993,4 +2558,42 @@ export class ProgressChatResponseContentImpl implements ProgressChatResponseCont
1993
2558
  text: this.message
1994
2559
  };
1995
2560
  }
2561
+ toSerializable(): SerializableChatResponseContentData<ProgressContentData> {
2562
+ return {
2563
+ kind: 'progress',
2564
+ data: { message: this._message }
2565
+ };
2566
+ }
2567
+ }
2568
+
2569
+ /**
2570
+ * Fallback content for unknown content types.
2571
+ * Used when a deserializer is not available (e.g., content from removed extension).
2572
+ */
2573
+ export interface UnknownChatResponseContent extends ChatResponseContent {
2574
+ kind: 'unknown';
2575
+ originalKind: string;
2576
+ fallbackMessage?: string;
2577
+ data: unknown;
2578
+ }
2579
+
2580
+ export class UnknownChatResponseContentImpl implements UnknownChatResponseContent {
2581
+ readonly kind = 'unknown';
2582
+
2583
+ constructor(
2584
+ public readonly originalKind: string,
2585
+ public readonly fallbackMessage: string | undefined,
2586
+ public readonly data: unknown
2587
+ ) { }
2588
+
2589
+ asString(): string | undefined {
2590
+ return this.fallbackMessage;
2591
+ }
2592
+
2593
+ toSerializable(): SerializableChatResponseContentData {
2594
+ return {
2595
+ kind: 'unknown',
2596
+ data: this.data
2597
+ };
2598
+ }
1996
2599
  }