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

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
@@ -0,0 +1,269 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { Container } from '@theia/core/shared/inversify';
19
+ import { ChatServiceImpl } from './chat-service';
20
+ import { ChatSessionStore, ChatSessionIndex, ChatModelWithMetadata } from './chat-session-store';
21
+ import { ChatAgentService } from './chat-agent-service';
22
+ import { ChatRequestParser } from './chat-request-parser';
23
+ import { AIVariableService } from '@theia/ai-core';
24
+ import { ILogger } from '@theia/core';
25
+ import { ChatContentDeserializerRegistry, ChatContentDeserializerRegistryImpl, DefaultChatContentDeserializerContribution } from './chat-content-deserializer';
26
+ import { ChangeSetElementDeserializerRegistry, ChangeSetElementDeserializerRegistryImpl } from './change-set-element-deserializer';
27
+ import { ChatAgentLocation } from './chat-agents';
28
+ import { ChatModel } from './chat-model';
29
+ import { SerializedChatData } from './chat-model-serialization';
30
+
31
+ describe('ChatService Session Deletion', () => {
32
+ let chatService: ChatServiceImpl;
33
+ let sessionStore: MockChatSessionStore;
34
+ let container: Container;
35
+
36
+ class MockChatSessionStore implements ChatSessionStore {
37
+ public deletedSessions: string[] = [];
38
+ public storedSessions: Array<ChatModel | ChatModelWithMetadata> = [];
39
+
40
+ async storeSessions(...sessions: Array<ChatModel | ChatModelWithMetadata>): Promise<void> {
41
+ this.storedSessions = sessions;
42
+ }
43
+
44
+ async readSession(sessionId: string): Promise<SerializedChatData | undefined> {
45
+ return undefined;
46
+ }
47
+
48
+ async deleteSession(sessionId: string): Promise<void> {
49
+ this.deletedSessions.push(sessionId);
50
+ }
51
+
52
+ async clearAllSessions(): Promise<void> {
53
+ this.deletedSessions = [];
54
+ this.storedSessions = [];
55
+ }
56
+
57
+ async getSessionIndex(): Promise<ChatSessionIndex> {
58
+ return {};
59
+ }
60
+
61
+ async setSessionTitle(sessionId: string, title: string): Promise<void> {
62
+ }
63
+ }
64
+
65
+ class MockChatAgentService {
66
+ getAgent(): undefined {
67
+ return undefined;
68
+ }
69
+ getAgents(): never[] {
70
+ return [];
71
+ }
72
+ }
73
+
74
+ class MockChatRequestParser {
75
+ parseChatRequest(): { parts: never[]; text: string } {
76
+ return { parts: [], text: '' };
77
+ }
78
+ }
79
+
80
+ class MockAIVariableService {
81
+ resolveVariables(): Promise<unknown[]> {
82
+ return Promise.resolve([]);
83
+ }
84
+ }
85
+
86
+ class MockLogger {
87
+ error(): void { }
88
+ warn(): void { }
89
+ info(): void { }
90
+ debug(): void { }
91
+ }
92
+
93
+ beforeEach(() => {
94
+ container = new Container();
95
+ sessionStore = new MockChatSessionStore();
96
+
97
+ container.bind(ChatSessionStore).toConstantValue(sessionStore);
98
+ container.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
99
+ container.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
100
+ container.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
101
+ container.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
102
+
103
+ // Bind deserializer registries
104
+ const contentRegistry = new ChatContentDeserializerRegistryImpl();
105
+ new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
106
+ container.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
107
+ container.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
108
+ container.bind(ChatServiceImpl).toSelf().inSingletonScope();
109
+
110
+ chatService = container.get(ChatServiceImpl);
111
+ });
112
+
113
+ describe('deleteSession', () => {
114
+ it('should delete session from memory and persistent storage', async () => {
115
+ // Create a session
116
+ const session = chatService.createSession(ChatAgentLocation.Panel);
117
+ expect(chatService.getSessions()).to.have.lengthOf(1);
118
+
119
+ // Delete the session (now returns a Promise)
120
+ await chatService.deleteSession(session.id);
121
+
122
+ // Verify it's removed from memory
123
+ expect(chatService.getSessions()).to.have.lengthOf(0);
124
+
125
+ // Verify it's deleted from persistent storage
126
+ expect(sessionStore.deletedSessions).to.include(session.id);
127
+ });
128
+
129
+ it('should emit SessionDeletedEvent when session is deleted', done => {
130
+ // Create a session
131
+ const session = chatService.createSession(ChatAgentLocation.Panel);
132
+
133
+ // Listen for deletion event
134
+ chatService.onSessionEvent(event => {
135
+ if (event.type === 'deleted') {
136
+ expect(event.sessionId).to.equal(session.id);
137
+ done();
138
+ }
139
+ });
140
+
141
+ // Delete the session
142
+ chatService.deleteSession(session.id);
143
+ });
144
+
145
+ it('should handle deletion when session store is not available', async () => {
146
+ // Create a new service without session store
147
+ const containerWithoutStore = new Container();
148
+ containerWithoutStore.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
149
+ containerWithoutStore.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
150
+ containerWithoutStore.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
151
+ containerWithoutStore.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
152
+
153
+ // Bind deserializer registries
154
+ const contentRegistry = new ChatContentDeserializerRegistryImpl();
155
+ new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
156
+ containerWithoutStore.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
157
+ containerWithoutStore.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
158
+
159
+ containerWithoutStore.bind(ChatServiceImpl).toSelf().inSingletonScope();
160
+
161
+ const serviceWithoutStore = containerWithoutStore.get(ChatServiceImpl);
162
+
163
+ // Create and delete a session - should not throw
164
+ const session = serviceWithoutStore.createSession(ChatAgentLocation.Panel);
165
+ await serviceWithoutStore.deleteSession(session.id);
166
+
167
+ // Verify session is still removed from memory
168
+ expect(serviceWithoutStore.getSessions()).to.have.lengthOf(0);
169
+ });
170
+
171
+ it('should attempt storage deletion even for non-existent in-memory sessions', async () => {
172
+ const initialDeletedCount = sessionStore.deletedSessions.length;
173
+ const nonExistentId = 'non-existent-id';
174
+
175
+ // Try to delete non-existent session (could be a persisted-only session)
176
+ await chatService.deleteSession(nonExistentId);
177
+
178
+ // Verify storage deletion was attempted even though session not in memory
179
+ expect(sessionStore.deletedSessions).to.include(nonExistentId);
180
+ expect(sessionStore.deletedSessions).to.have.lengthOf(initialDeletedCount + 1);
181
+ });
182
+
183
+ it('should handle deleting active session', () => {
184
+ // Create two sessions
185
+ const session1 = chatService.createSession(ChatAgentLocation.Panel);
186
+ const session2 = chatService.createSession(ChatAgentLocation.Panel);
187
+
188
+ // Ensure session2 is active (it should be by default as the latest)
189
+ expect(chatService.getActiveSession()?.id).to.equal(session2.id);
190
+
191
+ // Delete session1 (not active)
192
+ chatService.deleteSession(session1.id);
193
+
194
+ // Verify session2 is still active
195
+ const activeSession = chatService.getActiveSession();
196
+ expect(activeSession).to.not.be.undefined;
197
+ expect(activeSession?.id).to.equal(session2.id);
198
+ });
199
+
200
+ it('should handle storage deletion errors gracefully', async () => {
201
+ // Create a session store that throws errors
202
+ const errorStore = new MockChatSessionStore();
203
+ errorStore.deleteSession = async () => {
204
+ throw new Error('Storage error');
205
+ };
206
+
207
+ const errorContainer = new Container();
208
+ errorContainer.bind(ChatSessionStore).toConstantValue(errorStore);
209
+ errorContainer.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
210
+ errorContainer.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
211
+ errorContainer.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
212
+ errorContainer.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
213
+
214
+ // Bind deserializer registries
215
+ const contentRegistry = new ChatContentDeserializerRegistryImpl();
216
+ new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
217
+ errorContainer.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
218
+ errorContainer.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
219
+
220
+ errorContainer.bind(ChatServiceImpl).toSelf().inSingletonScope();
221
+
222
+ const errorService = errorContainer.get(ChatServiceImpl);
223
+
224
+ // Create and delete a session - should not throw even with storage error
225
+ const session = errorService.createSession(ChatAgentLocation.Panel);
226
+ await errorService.deleteSession(session.id);
227
+
228
+ // Verify session is still removed from memory despite storage error
229
+ expect(errorService.getSessions()).to.have.lengthOf(0);
230
+ });
231
+
232
+ it('should delete persisted-only sessions (not loaded in memory)', async () => {
233
+ // This simulates deleting a session from the "Show Chats..." dialog
234
+ // when the session is persisted but not currently loaded into memory
235
+ const persistedSessionId = 'persisted-session-123';
236
+
237
+ // Verify session is not in memory
238
+ expect(chatService.getSessions().find(s => s.id === persistedSessionId)).to.be.undefined;
239
+
240
+ // Delete the persisted-only session
241
+ await chatService.deleteSession(persistedSessionId);
242
+
243
+ // Verify it was still deleted from storage (even though not in memory)
244
+ expect(sessionStore.deletedSessions).to.include(persistedSessionId);
245
+ });
246
+
247
+ it('should not fire SessionDeletedEvent for persisted-only sessions', async () => {
248
+ // When deleting a persisted-only session (not in memory),
249
+ // we shouldn't fire the event since no in-memory state changed
250
+ const persistedSessionId = 'persisted-session-456';
251
+ let eventFired = false;
252
+
253
+ // Listen for deletion event
254
+ chatService.onSessionEvent(event => {
255
+ if (event.type === 'deleted' && event.sessionId === persistedSessionId) {
256
+ eventFired = true;
257
+ }
258
+ });
259
+
260
+ // Delete the persisted-only session
261
+ await chatService.deleteSession(persistedSessionId);
262
+
263
+ // Event should not have been fired since session wasn't in memory
264
+ expect(eventFired).to.be.false;
265
+ // But storage deletion should still have happened
266
+ expect(sessionStore.deletedSessions).to.include(persistedSessionId);
267
+ });
268
+ });
269
+ });
@@ -27,6 +27,8 @@ import { Event } from '@theia/core/shared/vscode-languageserver-protocol';
27
27
  import { ChatAgentService } from './chat-agent-service';
28
28
  import { ChatAgent, ChatAgentLocation, ChatSessionContext } from './chat-agents';
29
29
  import {
30
+ ChangeSetElement,
31
+ ChangeSetImpl,
30
32
  ChatContext,
31
33
  ChatModel,
32
34
  ChatRequest,
@@ -39,6 +41,11 @@ import {
39
41
  import { ChatRequestParser } from './chat-request-parser';
40
42
  import { ChatSessionNamingService } from './chat-session-naming-service';
41
43
  import { ParsedChatRequest, ParsedChatRequestAgentPart } from './parsed-chat-request';
44
+ import { ChatSessionIndex, ChatSessionStore } from './chat-session-store';
45
+ import { ChatContentDeserializerRegistry } from './chat-content-deserializer';
46
+ import { ChangeSetDeserializationContext, ChangeSetElementDeserializerRegistry } from './change-set-element-deserializer';
47
+ import { SerializableChangeSetElement, SerializedChatModel } from './chat-model-serialization';
48
+ import debounce = require('@theia/core/shared/lodash.debounce');
42
49
 
43
50
  export interface ChatRequestInvocation {
44
51
  /**
@@ -126,7 +133,7 @@ export interface ChatService {
126
133
  getSession(id: string): ChatSession | undefined;
127
134
  getSessions(): ChatSession[];
128
135
  createSession(location?: ChatAgentLocation, options?: SessionOptions, pinnedAgent?: ChatAgent): ChatSession;
129
- deleteSession(sessionId: string): void;
136
+ deleteSession(sessionId: string): Promise<void>;
130
137
  getActiveSession(): ChatSession | undefined;
131
138
  setActiveSession(sessionId: string, options?: SessionOptions): void;
132
139
 
@@ -141,6 +148,15 @@ export interface ChatService {
141
148
  cancelRequest(sessionId: string, requestId: string): Promise<void>;
142
149
 
143
150
  getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined;
151
+
152
+ /**
153
+ * Get an existing session or restore from storage
154
+ */
155
+ getOrRestoreSession(sessionId: string): Promise<ChatSession | undefined>;
156
+ /**
157
+ * Get all persisted session metadata
158
+ */
159
+ getPersistedSessions(): Promise<ChatSessionIndex>;
144
160
  }
145
161
 
146
162
  interface ChatSessionInternal extends ChatSession {
@@ -176,6 +192,15 @@ export class ChatServiceImpl implements ChatService {
176
192
  @inject(ILogger)
177
193
  protected logger: ILogger;
178
194
 
195
+ @inject(ChatSessionStore) @optional()
196
+ protected sessionStore: ChatSessionStore | undefined;
197
+
198
+ @inject(ChatContentDeserializerRegistry)
199
+ protected deserializerRegistry: ChatContentDeserializerRegistry;
200
+
201
+ @inject(ChangeSetElementDeserializerRegistry)
202
+ protected changeSetElementDeserializerRegistry: ChangeSetElementDeserializerRegistry;
203
+
179
204
  protected _sessions: ChatSessionInternal[] = [];
180
205
 
181
206
  getSessions(): ChatSessionInternal[] {
@@ -190,27 +215,41 @@ export class ChatServiceImpl implements ChatService {
190
215
  const model = new MutableChatModel(location);
191
216
  const session: ChatSessionInternal = {
192
217
  id: model.id,
218
+ lastInteraction: new Date(),
193
219
  model,
194
220
  isActive: true,
195
221
  pinnedAgent
196
222
  };
197
223
  this._sessions.push(session);
224
+ this.setupAutoSaveForSession(session);
198
225
  this.setActiveSession(session.id, options);
199
226
  this.onSessionEventEmitter.fire({ type: 'created', sessionId: session.id });
200
227
  return session;
201
228
  }
202
229
 
203
- deleteSession(sessionId: string): void {
230
+ async deleteSession(sessionId: string): Promise<void> {
204
231
  const sessionIndex = this._sessions.findIndex(candidate => candidate.id === sessionId);
205
- if (sessionIndex === -1) { return; }
206
- const session = this._sessions[sessionIndex];
207
- // If the removed session is the active one, set the newest one as active
208
- if (session.isActive) {
209
- this.setActiveSession(this._sessions[this._sessions.length - 1]?.id);
232
+
233
+ // If session is in memory, remove it
234
+ if (sessionIndex !== -1) {
235
+ const session = this._sessions[sessionIndex];
236
+ // If the removed session is the active one, set the newest one as active
237
+ if (session.isActive) {
238
+ this.setActiveSession(this._sessions[this._sessions.length - 1]?.id);
239
+ }
240
+ session.model.dispose();
241
+ this._sessions.splice(sessionIndex, 1);
242
+ this.onSessionEventEmitter.fire({ type: 'deleted', sessionId: sessionId });
243
+ }
244
+
245
+ // Always delete from persistent storage
246
+ if (this.sessionStore) {
247
+ try {
248
+ await this.sessionStore.deleteSession(sessionId);
249
+ } catch (error) {
250
+ this.logger.error('Failed to delete session from storage', { sessionId, error });
251
+ }
210
252
  }
211
- session.model.dispose();
212
- this._sessions.splice(sessionIndex, 1);
213
- this.onSessionEventEmitter.fire({ type: 'deleted', sessionId: sessionId });
214
253
  }
215
254
 
216
255
  getActiveSession(): ChatSession | undefined {
@@ -303,6 +342,8 @@ export class ChatServiceImpl implements ChatService {
303
342
  namingService.generateChatSessionName(session, otherSessionNames).then(name => {
304
343
  if (name && session.title === requestText) {
305
344
  session.title = name;
345
+ // Trigger persistence when title changes
346
+ this.saveSession(session.id);
306
347
  }
307
348
  didGenerateName = true;
308
349
  }).catch(error => this.logger.error('Failed to generate chat session name', error));
@@ -392,4 +433,180 @@ export class ChatServiceImpl implements ChatService {
392
433
  deleteChangeSetElement(sessionId: string, uri: URI): void {
393
434
  this.getSession(sessionId)?.model.changeSet.removeElements(uri);
394
435
  }
436
+
437
+ protected saveSession(sessionId: string): void {
438
+ if (!this.sessionStore) {
439
+ this.logger.debug('Session store not available, skipping save');
440
+ return;
441
+ }
442
+
443
+ const session = this.getSession(sessionId);
444
+ if (!session) {
445
+ this.logger.debug('Session not found, skipping save', { sessionId });
446
+ return;
447
+ }
448
+
449
+ // Don't save empty sessions
450
+ if (session.model.isEmpty()) {
451
+ this.logger.debug('Session is empty, skipping save', { sessionId });
452
+ return;
453
+ }
454
+
455
+ // Store session with title and pinned agent info
456
+ this.sessionStore.storeSessions(
457
+ { model: session.model, title: session.title, pinnedAgentId: session.pinnedAgent?.id }
458
+ ).catch(error => {
459
+ this.logger.error('Failed to store chat sessions', error);
460
+ });
461
+ }
462
+
463
+ /**
464
+ * Set up auto-save for a session by listening to model changes.
465
+ */
466
+ protected setupAutoSaveForSession(session: ChatSessionInternal): void {
467
+ const debouncedSave = debounce(() => this.saveSession(session.id), 500, { maxWait: 5000 });
468
+ session.model.onDidChange(_event => {
469
+ debouncedSave();
470
+ });
471
+ }
472
+
473
+ async getOrRestoreSession(sessionId: string): Promise<ChatSession | undefined> {
474
+ const existing = this.getSession(sessionId);
475
+ if (existing) {
476
+ this.logger.debug('Session already loaded', { sessionId });
477
+ return existing;
478
+ }
479
+
480
+ if (!this.sessionStore) {
481
+ this.logger.debug('Session store not available, cannot restore', { sessionId });
482
+ return undefined;
483
+ }
484
+
485
+ this.logger.debug('Restoring session from storage', { sessionId });
486
+
487
+ const serialized = await this.sessionStore.readSession(sessionId);
488
+ if (!serialized) {
489
+ this.logger.warn('Session not found in storage', { sessionId });
490
+ return undefined;
491
+ }
492
+
493
+ this.logger.debug('Session loaded from storage', {
494
+ sessionId,
495
+ requestCount: serialized.model.requests.length,
496
+ responseCount: serialized.model.responses.length,
497
+ version: serialized.version
498
+ });
499
+
500
+ const model = new MutableChatModel(serialized.model);
501
+ await this.restoreSessionData(model, serialized.model);
502
+
503
+ // Determine pinned agent
504
+ const pinnedAgent = serialized.pinnedAgentId
505
+ ? this.chatAgentService.getAgent(serialized.pinnedAgentId)
506
+ : undefined;
507
+
508
+ // Register as session
509
+ const session: ChatSessionInternal = {
510
+ id: sessionId,
511
+ title: serialized.title,
512
+ lastInteraction: new Date(serialized.saveDate),
513
+ model,
514
+ isActive: false,
515
+ pinnedAgent
516
+ };
517
+ this._sessions.push(session);
518
+ this.setupAutoSaveForSession(session);
519
+ this.onSessionEventEmitter.fire({ type: 'created', sessionId: session.id });
520
+
521
+ this.logger.debug('Session successfully restored and registered', { sessionId, title: session.title });
522
+
523
+ return session;
524
+ }
525
+
526
+ async getPersistedSessions(): Promise<ChatSessionIndex> {
527
+ if (!this.sessionStore) {
528
+ return {};
529
+ }
530
+ return this.sessionStore.getSessionIndex();
531
+ }
532
+
533
+ /**
534
+ * Deserialize response content and restore changesets.
535
+ * Called after basic chat model structure was created.
536
+ */
537
+ protected async restoreSessionData(model: MutableChatModel, data: SerializedChatModel): Promise<void> {
538
+ this.logger.debug('Restoring dynamic session data', { sessionId: data.sessionId, requestCount: data.requests.length });
539
+
540
+ // Process each request for response content and changeset restoration
541
+ // IMPORTANT: Use getAllRequests() to include alternatives, not just active requests
542
+ const requests = model.getAllRequests();
543
+ for (let i = 0; i < requests.length; i++) {
544
+ const requestModel = requests[i];
545
+
546
+ this.logger.debug('Restore response content', { requestId: requestModel.id, index: i });
547
+
548
+ // Restore response content using deserializer registry
549
+ const respData = data.responses.find(r => r.requestId === requestModel.id);
550
+ if (respData && respData.content.length > 0) {
551
+ const restoredContent = await Promise.all(respData.content.map(contentData =>
552
+ this.deserializerRegistry.deserialize(contentData)
553
+ ));
554
+ restoredContent.forEach(content => requestModel.response.response.addContent(content));
555
+ this.logger.debug('Restored response content', {
556
+ requestId: requestModel.id,
557
+ contentCount: restoredContent.length
558
+ });
559
+ }
560
+
561
+ // Restore changeset elements
562
+ const serializedChangeSet = data.requests.find(r => r.id === requestModel.id)?.changeSet;
563
+ if (serializedChangeSet && serializedChangeSet.elements && serializedChangeSet.elements.length > 0) {
564
+ // Create a changeset if one doesn't exist
565
+ if (!requestModel.changeSet) {
566
+ requestModel.changeSet = new ChangeSetImpl();
567
+ }
568
+ await this.restoreChangeSetElements(requestModel, serializedChangeSet.elements, data.sessionId);
569
+
570
+ // Restore changeset title
571
+ if (serializedChangeSet.title) {
572
+ requestModel.changeSet.setTitle(serializedChangeSet.title);
573
+ }
574
+
575
+ this.logger.debug('Restored changeset', {
576
+ requestId: requestModel.id,
577
+ elementCount: serializedChangeSet.elements.length
578
+ });
579
+ }
580
+ }
581
+
582
+ this.logger.debug('Restoring dynamic session data complete', { sessionId: data.sessionId });
583
+ }
584
+
585
+ protected async restoreChangeSetElements(
586
+ requestModel: MutableChatRequestModel,
587
+ elements: SerializableChangeSetElement[],
588
+ sessionId: string
589
+ ): Promise<void> {
590
+ this.logger.debug('Restoring changeset elements', { requestId: requestModel.id, elementCount: elements.length });
591
+
592
+ const context: ChangeSetDeserializationContext = {
593
+ chatSessionId: sessionId,
594
+ requestId: requestModel.id
595
+ };
596
+
597
+ const restoredElements: ChangeSetElement[] = [];
598
+
599
+ for (const elem of elements) {
600
+ const restoredElement = await this.changeSetElementDeserializerRegistry.deserialize(elem, context);
601
+ restoredElements.push(restoredElement);
602
+ }
603
+
604
+ // Add elements to the request's changeset
605
+ if (requestModel.changeSet) {
606
+ requestModel.changeSet.addElements(...restoredElements);
607
+ this.logger.debug('Changeset elements restored', { requestId: requestModel.id, elementCount: restoredElements.length });
608
+ } else {
609
+ this.logger.warn('Request has no changeset, cannot restore elements', { requestId: requestModel.id });
610
+ }
611
+ }
395
612
  }
@@ -0,0 +1,63 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { ChatModel } from './chat-model';
18
+ import { SerializedChatData } from './chat-model-serialization';
19
+ import { ChatAgentLocation } from './chat-agents';
20
+
21
+ export const ChatSessionStore = Symbol('ChatSessionStore');
22
+
23
+ export interface ChatModelWithMetadata {
24
+ model: ChatModel;
25
+ title?: string;
26
+ pinnedAgentId?: string;
27
+ }
28
+
29
+ export interface ChatSessionStore {
30
+ /**
31
+ * Stores the handed over sessions.
32
+ *
33
+ * Might overwrite existing sessions when maximum storage capacity is exceeded.
34
+ */
35
+ storeSessions(...sessions: Array<ChatModel | ChatModelWithMetadata>): Promise<void>;
36
+ /**
37
+ * Read specified session
38
+ */
39
+ readSession(sessionId: string): Promise<SerializedChatData | undefined>;
40
+ /**
41
+ * Delete specified session
42
+ */
43
+ deleteSession(sessionId: string): Promise<void>;
44
+ /**
45
+ * Deletes all sessions
46
+ */
47
+ clearAllSessions(): Promise<void>;
48
+ /**
49
+ * Get index of all stored sessions
50
+ */
51
+ getSessionIndex(): Promise<ChatSessionIndex>;
52
+ }
53
+
54
+ export interface ChatSessionIndex {
55
+ [sessionId: string]: ChatSessionMetadata;
56
+ }
57
+
58
+ export interface ChatSessionMetadata {
59
+ sessionId: string;
60
+ title: string;
61
+ saveDate: number;
62
+ location: ChatAgentLocation;
63
+ }
@@ -16,9 +16,12 @@
16
16
  export * from './chat-agents';
17
17
  export * from './chat-agent-service';
18
18
  export * from './chat-model';
19
+ export * from './chat-model-serialization';
20
+ export * from './chat-content-deserializer';
19
21
  export * from './chat-model-util';
20
22
  export * from './chat-request-parser';
21
23
  export * from './chat-service';
24
+ export * from './chat-session-store';
22
25
  export * from './custom-chat-agent';
23
26
  export * from './parsed-chat-request';
24
27
  export * from './context-variables';