@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,326 @@
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 { inject, injectable, named } from '@theia/core/shared/inversify';
18
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
19
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
20
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
21
+ import { StorageService } from '@theia/core/lib/browser';
22
+ import { URI } from '@theia/core';
23
+ import { BinaryBuffer } from '@theia/core/lib/common/buffer';
24
+ import { ILogger } from '@theia/core/lib/common/logger';
25
+ import { ChatModel } from '../common/chat-model';
26
+ import { ChatSessionIndex, ChatSessionStore, ChatModelWithMetadata, ChatSessionMetadata } from '../common/chat-session-store';
27
+ import { SerializedChatData, CHAT_DATA_VERSION } from '../common/chat-model-serialization';
28
+
29
+ const MAX_SESSIONS = 25;
30
+ const INDEX_FILE = 'index.json';
31
+
32
+ @injectable()
33
+ export class ChatSessionStoreImpl implements ChatSessionStore {
34
+ @inject(FileService)
35
+ protected readonly fileService: FileService;
36
+
37
+ @inject(WorkspaceService)
38
+ protected readonly workspaceService: WorkspaceService;
39
+
40
+ @inject(EnvVariablesServer)
41
+ protected readonly envServer: EnvVariablesServer;
42
+
43
+ @inject(StorageService)
44
+ protected readonly storageService: StorageService;
45
+
46
+ @inject(ILogger) @named('ChatSessionStore')
47
+ protected readonly logger: ILogger;
48
+
49
+ protected storageRoot?: URI;
50
+ protected indexCache?: ChatSessionIndex;
51
+ protected storePromise: Promise<void> = Promise.resolve();
52
+
53
+ async storeSessions(...sessions: Array<ChatModel | ChatModelWithMetadata>): Promise<void> {
54
+ this.storePromise = this.storePromise.then(async () => {
55
+ const root = await this.getStorageRoot();
56
+ this.logger.debug('Starting to store sessions', { totalSessions: sessions.length, storageRoot: root.toString() });
57
+
58
+ // Normalize to SessionWithTitle and filter empty sessions
59
+ const nonEmptySessions = sessions
60
+ .map(s => this.isChatModelWithMetadata(s) ? { ...s, saveDate: Date.now() } : { model: s, saveDate: Date.now() })
61
+ .filter(s => !s.model.isEmpty());
62
+ this.logger.debug('Filtered empty sessions', { nonEmptySessions: nonEmptySessions.length });
63
+
64
+ // Write each session as JSON file
65
+ for (const session of nonEmptySessions) {
66
+ const sessionFile = root.resolve(`${session.model.id}.json`);
67
+ const modelData = session.model.toSerializable();
68
+ // Wrap model data with persistence metadata
69
+ const data: SerializedChatData = {
70
+ version: CHAT_DATA_VERSION,
71
+ title: session.title,
72
+ pinnedAgentId: session.pinnedAgentId,
73
+ saveDate: session.saveDate,
74
+ model: modelData
75
+ };
76
+ this.logger.debug('Writing session to file', {
77
+ sessionId: session.model.id,
78
+ title: data.title,
79
+ filePath: sessionFile.toString(),
80
+ requestCount: modelData.requests.length,
81
+ responseCount: modelData.responses.length,
82
+ pinnedAgentId: data.pinnedAgentId,
83
+ version: data.version
84
+ });
85
+ await this.fileService.writeFile(
86
+ sessionFile,
87
+ BinaryBuffer.fromString(JSON.stringify(data, undefined, 2))
88
+ );
89
+ }
90
+
91
+ // Update index with metadata
92
+ await this.updateIndex(nonEmptySessions);
93
+
94
+ // Trim to max sessions
95
+ await this.trimSessions();
96
+ this.logger.debug('Finished storing sessions');
97
+ });
98
+ return this.storePromise;
99
+ }
100
+
101
+ private isChatModelWithMetadata(session: ChatModel | ChatModelWithMetadata): session is ChatModelWithMetadata {
102
+ return 'model' in session;
103
+ }
104
+
105
+ async readSession(sessionId: string): Promise<SerializedChatData | undefined> {
106
+ const root = await this.getStorageRoot();
107
+ const sessionFile = root.resolve(`${sessionId}.json`);
108
+ this.logger.debug('Reading session from file', { sessionId, filePath: sessionFile.toString() });
109
+
110
+ try {
111
+ const content = await this.fileService.readFile(sessionFile);
112
+ const parsedData = JSON.parse(content.value.toString());
113
+ const data = this.migrateData(parsedData);
114
+ this.logger.debug('Successfully read session', {
115
+ sessionId,
116
+ requestCount: data.model.requests.length,
117
+ responseCount: data.model.responses.length,
118
+ version: data.version
119
+ });
120
+ return data;
121
+ } catch (e) {
122
+ this.logger.debug('Failed to read session', { sessionId, error: e });
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ async deleteSession(sessionId: string): Promise<void> {
128
+ this.storePromise = this.storePromise.then(async () => {
129
+ const root = await this.getStorageRoot();
130
+ const sessionFile = root.resolve(`${sessionId}.json`);
131
+ this.logger.debug('Deleting session', { sessionId, filePath: sessionFile.toString() });
132
+
133
+ try {
134
+ await this.fileService.delete(sessionFile);
135
+ this.logger.debug('Session file deleted', { sessionId });
136
+ } catch (e) {
137
+ this.logger.debug('Failed to delete session file (may not exist)', { sessionId, error: e });
138
+ }
139
+
140
+ // Update index
141
+ const index = await this.loadIndex();
142
+ delete index[sessionId];
143
+ await this.saveIndex(index);
144
+ this.logger.debug('Session removed from index', { sessionId });
145
+ });
146
+ return this.storePromise;
147
+ }
148
+
149
+ async clearAllSessions(): Promise<void> {
150
+ this.storePromise = this.storePromise.then(async () => {
151
+ const root = await this.getStorageRoot();
152
+
153
+ try {
154
+ await this.fileService.delete(root, { recursive: true });
155
+ await this.fileService.createFolder(root);
156
+ } catch (e) {
157
+ // Ignore errors
158
+ }
159
+
160
+ this.indexCache = {};
161
+ await this.saveIndex({});
162
+ });
163
+ return this.storePromise;
164
+ }
165
+
166
+ async getSessionIndex(): Promise<ChatSessionIndex> {
167
+ const index = await this.loadIndex();
168
+ this.logger.debug('Retrieved session index', { sessionCount: Object.keys(index).length });
169
+ return index;
170
+ }
171
+
172
+ async setSessionTitle(sessionId: string, title: string): Promise<void> {
173
+ this.storePromise = this.storePromise.then(async () => {
174
+ const index = await this.loadIndex();
175
+ if (index[sessionId]) {
176
+ index[sessionId].title = title;
177
+ await this.saveIndex(index);
178
+ }
179
+ });
180
+ return this.storePromise;
181
+ }
182
+
183
+ protected async getStorageRoot(): Promise<URI> {
184
+ if (this.storageRoot) {
185
+ return this.storageRoot;
186
+ }
187
+
188
+ const configDir = await this.envServer.getConfigDirUri();
189
+ this.storageRoot = new URI(configDir).resolve('chatSessions');
190
+
191
+ try {
192
+ await this.fileService.createFolder(this.storageRoot);
193
+ } catch (e) {
194
+ // Folder may already exist
195
+ }
196
+
197
+ return this.storageRoot;
198
+ }
199
+
200
+ protected async updateIndex(sessions: ((ChatModelWithMetadata & { saveDate: number })[])): Promise<void> {
201
+ const index = await this.loadIndex();
202
+
203
+ for (const session of sessions) {
204
+ const data = session.model.toSerializable();
205
+ const { model, ...metadata } = session;
206
+ const previousData = index[model.id];
207
+ index[model.id] = {
208
+ ...previousData,
209
+ sessionId: model.id,
210
+ location: data.location,
211
+ ...metadata
212
+ };
213
+ }
214
+
215
+ await this.saveIndex(index);
216
+ }
217
+
218
+ protected async trimSessions(): Promise<void> {
219
+ const index = await this.loadIndex();
220
+ const sessions = Object.values(index);
221
+
222
+ if (sessions.length <= MAX_SESSIONS) {
223
+ return;
224
+ }
225
+
226
+ this.logger.debug('Trimming sessions', { currentCount: sessions.length, maxSessions: MAX_SESSIONS });
227
+
228
+ // Sort by save date
229
+ sessions.sort((a, b) => a.saveDate - b.saveDate);
230
+
231
+ // Delete oldest sessions
232
+ const sessionsToDelete = sessions.slice(0, sessions.length - MAX_SESSIONS);
233
+ this.logger.debug('Deleting oldest sessions', { deleteCount: sessionsToDelete.length, sessionIds: sessionsToDelete.map(s => s.sessionId) });
234
+ for (const session of sessionsToDelete) {
235
+ await this.deleteSession(session.sessionId);
236
+ }
237
+ }
238
+
239
+ protected async loadIndex(): Promise<ChatSessionIndex> {
240
+ if (this.indexCache) {
241
+ return this.indexCache;
242
+ }
243
+
244
+ const root = await this.getStorageRoot();
245
+ const indexFile = root.resolve(INDEX_FILE);
246
+
247
+ try {
248
+ const content = await this.fileService.readFile(indexFile);
249
+ const rawIndex = JSON.parse(content.value.toString());
250
+
251
+ // Validate and clean up index entries
252
+ const validatedIndex: ChatSessionIndex = {};
253
+ let hasInvalidEntries = false;
254
+
255
+ for (const [sessionId, metadata] of Object.entries(rawIndex)) {
256
+ // Check if entry has required fields and valid values
257
+ if (this.isValidMetadata(metadata)) {
258
+ validatedIndex[sessionId] = metadata as ChatSessionMetadata;
259
+ } else {
260
+ hasInvalidEntries = true;
261
+ this.logger.warn('Removing invalid session metadata from index', {
262
+ sessionId,
263
+ metadata
264
+ });
265
+ }
266
+ }
267
+
268
+ // If we removed any entries, persist the cleaned index
269
+ if (hasInvalidEntries) {
270
+ this.logger.info('Index cleaned up, removing invalid entries');
271
+ await this.fileService.writeFile(
272
+ indexFile,
273
+ BinaryBuffer.fromString(JSON.stringify(validatedIndex, undefined, 2))
274
+ );
275
+ }
276
+
277
+ this.indexCache = validatedIndex;
278
+ return this.indexCache;
279
+ } catch (e) {
280
+ this.indexCache = {};
281
+ return this.indexCache;
282
+ }
283
+ }
284
+
285
+ protected isValidMetadata(metadata: unknown): metadata is ChatSessionMetadata {
286
+ if (!metadata || typeof metadata !== 'object') {
287
+ return false;
288
+ }
289
+
290
+ const m = metadata as Record<string, unknown>;
291
+
292
+ // Check required fields exist and have correct types
293
+ return typeof m.sessionId === 'string' &&
294
+ typeof m.title === 'string' &&
295
+ typeof m.saveDate === 'number' &&
296
+ typeof m.location === 'string' &&
297
+ // Ensure saveDate is a valid timestamp
298
+ !isNaN(m.saveDate) &&
299
+ m.saveDate > 0;
300
+ }
301
+
302
+ protected async saveIndex(index: ChatSessionIndex): Promise<void> {
303
+ this.indexCache = index;
304
+ const root = await this.getStorageRoot();
305
+ const indexFile = root.resolve(INDEX_FILE);
306
+
307
+ await this.fileService.writeFile(
308
+ indexFile,
309
+ BinaryBuffer.fromString(JSON.stringify(index, undefined, 2))
310
+ );
311
+ }
312
+
313
+ protected migrateData(data: unknown): SerializedChatData {
314
+ const parsed = data as SerializedChatData;
315
+
316
+ // Defensive check for unexpected future versions
317
+ if (parsed.version && parsed.version > CHAT_DATA_VERSION) {
318
+ this.logger.warn(
319
+ `Session data version ${parsed.version} is newer than supported ${CHAT_DATA_VERSION}. ` +
320
+ 'Data may not load correctly.'
321
+ );
322
+ }
323
+
324
+ return parsed;
325
+ }
326
+ }
@@ -0,0 +1,90 @@
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 { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
18
+ import { ContributionProvider, MaybePromise, URI } from '@theia/core';
19
+ import { ChangeSetElement } from './change-set';
20
+ import { SerializableChangeSetElement } from './chat-model-serialization';
21
+
22
+ export const ChangeSetElementDeserializer = Symbol('ChangeSetElementDeserializer');
23
+
24
+ export interface ChangeSetElementDeserializer<T = unknown> {
25
+ readonly kind: string;
26
+ deserialize(serialized: SerializableChangeSetElement, context: ChangeSetDeserializationContext): ChangeSetElement | Promise<ChangeSetElement>;
27
+ }
28
+
29
+ export interface ChangeSetDeserializationContext {
30
+ chatSessionId: string;
31
+ requestId: string;
32
+ }
33
+
34
+ export interface ChangeSetElementDeserializerContribution {
35
+ registerDeserializers(registry: ChangeSetElementDeserializerRegistry): void;
36
+ }
37
+ export const ChangeSetElementDeserializerContribution = Symbol('ChangeSetElementDeserializerContribution');
38
+ export interface ChangeSetElementDeserializerRegistry {
39
+ register(deserializer: ChangeSetElementDeserializer): void;
40
+ deserialize(serialized: SerializableChangeSetElement, context: ChangeSetDeserializationContext): MaybePromise<ChangeSetElement>;
41
+ }
42
+ export const ChangeSetElementDeserializerRegistry = Symbol('ChangeSetElementDeserializerRegistry');
43
+
44
+ @injectable()
45
+ export class ChangeSetElementDeserializerRegistryImpl implements ChangeSetElementDeserializerRegistry {
46
+ protected deserializers = new Map<string, ChangeSetElementDeserializer>();
47
+
48
+ @inject(ContributionProvider) @named(ChangeSetElementDeserializerContribution)
49
+ protected readonly changeSetElementDeserializerContributions: ContributionProvider<ChangeSetElementDeserializerContribution>;
50
+
51
+ @postConstruct() init(): void {
52
+ for (const contribution of this.changeSetElementDeserializerContributions.getContributions()) {
53
+ contribution.registerDeserializers(this);
54
+ }
55
+ }
56
+
57
+ register(deserializer: ChangeSetElementDeserializer): void {
58
+ this.deserializers.set(deserializer.kind, deserializer);
59
+ }
60
+
61
+ deserialize(serialized: SerializableChangeSetElement, context: ChangeSetDeserializationContext): MaybePromise<ChangeSetElement> {
62
+ const deserializer = this.deserializers.get(serialized.kind || 'generic');
63
+ if (!deserializer) {
64
+ return this.createFallbackElement(serialized);
65
+ }
66
+ return deserializer.deserialize(serialized, context);
67
+ }
68
+
69
+ private createFallbackElement(serialized: SerializableChangeSetElement): ChangeSetElement {
70
+ return {
71
+ uri: new URI(serialized.uri),
72
+ name: serialized.name,
73
+ icon: serialized.icon,
74
+ additionalInfo: serialized.additionalInfo,
75
+ state: serialized.state,
76
+ type: serialized.type,
77
+ data: serialized.data,
78
+ toSerializable: (): SerializableChangeSetElement => ({
79
+ kind: serialized.kind || 'generic',
80
+ uri: serialized.uri,
81
+ name: serialized.name,
82
+ icon: serialized.icon,
83
+ additionalInfo: serialized.additionalInfo,
84
+ state: serialized.state,
85
+ type: serialized.type,
86
+ data: serialized.data
87
+ })
88
+ };
89
+ }
90
+ }
@@ -15,6 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ArrayUtils, Disposable, Emitter, Event, nls, URI } from '@theia/core';
18
+ import { SerializableChangeSetElement } from './chat-model-serialization';
18
19
 
19
20
  export interface ChangeSetElement {
20
21
  readonly uri: URI;
@@ -37,6 +38,13 @@ export interface ChangeSetElement {
37
38
  apply?(): Promise<void>;
38
39
  revert?(): Promise<void>;
39
40
  dispose?(): void;
41
+
42
+ /**
43
+ * Serializes this element to a format suitable for persistence.
44
+ * Each element type is responsible for serializing its own data.
45
+ * Optional - elements without this method will be excluded from serialization.
46
+ */
47
+ toSerializable?(): SerializableChangeSetElement;
40
48
  }
41
49
 
42
50
  export interface ChatUpdateChangeSetEvent {