@theia/ai-chat 1.75.0-next.8 → 1.75.0

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 (100) hide show
  1. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-frontend-module.js +4 -0
  3. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  4. package/lib/browser/change-set-file-element.d.ts +2 -0
  5. package/lib/browser/change-set-file-element.d.ts.map +1 -1
  6. package/lib/browser/change-set-file-element.js +10 -4
  7. package/lib/browser/change-set-file-element.js.map +1 -1
  8. package/lib/browser/change-set-file-service.js +1 -0
  9. package/lib/browser/change-set-file-service.js.map +1 -1
  10. package/lib/browser/chat-session-store-impl.js +1 -1
  11. package/lib/browser/chat-session-store-impl.js.map +1 -1
  12. package/lib/browser/chat-session-store-impl.spec.js +1 -2
  13. package/lib/browser/chat-session-store-impl.spec.js.map +1 -1
  14. package/lib/browser/chat-tool-request-service.js +1 -1
  15. package/lib/browser/chat-tool-request-service.js.map +1 -1
  16. package/lib/browser/file-chat-variable-contribution.js +1 -0
  17. package/lib/browser/file-chat-variable-contribution.js.map +1 -1
  18. package/lib/browser/file-read-tracker-impl.d.ts +55 -0
  19. package/lib/browser/file-read-tracker-impl.d.ts.map +1 -0
  20. package/lib/browser/file-read-tracker-impl.js +200 -0
  21. package/lib/browser/file-read-tracker-impl.js.map +1 -0
  22. package/lib/browser/file-read-tracker-impl.spec.d.ts +2 -0
  23. package/lib/browser/file-read-tracker-impl.spec.d.ts.map +1 -0
  24. package/lib/browser/file-read-tracker-impl.spec.js +251 -0
  25. package/lib/browser/file-read-tracker-impl.spec.js.map +1 -0
  26. package/lib/common/ai-chat-preferences.d.ts.map +1 -1
  27. package/lib/common/ai-chat-preferences.js +4 -0
  28. package/lib/common/ai-chat-preferences.js.map +1 -1
  29. package/lib/common/chat-agent-service.js +1 -0
  30. package/lib/common/chat-agent-service.js.map +1 -1
  31. package/lib/common/chat-agents.d.ts +8 -1
  32. package/lib/common/chat-agents.d.ts.map +1 -1
  33. package/lib/common/chat-agents.js +31 -2
  34. package/lib/common/chat-agents.js.map +1 -1
  35. package/lib/common/chat-agents.spec.js +51 -0
  36. package/lib/common/chat-agents.spec.js.map +1 -1
  37. package/lib/common/chat-content-deserializer.js +1 -1
  38. package/lib/common/chat-content-deserializer.js.map +1 -1
  39. package/lib/common/chat-request-parser.d.ts +1 -1
  40. package/lib/common/chat-request-parser.d.ts.map +1 -1
  41. package/lib/common/chat-request-parser.js +5 -2
  42. package/lib/common/chat-request-parser.js.map +1 -1
  43. package/lib/common/chat-request-parser.spec.js +32 -1
  44. package/lib/common/chat-request-parser.spec.js.map +1 -1
  45. package/lib/common/chat-service-session-restore.spec.d.ts +2 -0
  46. package/lib/common/chat-service-session-restore.spec.d.ts.map +1 -0
  47. package/lib/common/chat-service-session-restore.spec.js +165 -0
  48. package/lib/common/chat-service-session-restore.spec.js.map +1 -0
  49. package/lib/common/chat-service.d.ts +4 -1
  50. package/lib/common/chat-service.d.ts.map +1 -1
  51. package/lib/common/chat-service.js +17 -0
  52. package/lib/common/chat-service.js.map +1 -1
  53. package/lib/common/chat-session-summary-agent.d.ts +2 -0
  54. package/lib/common/chat-session-summary-agent.d.ts.map +1 -1
  55. package/lib/common/chat-session-summary-agent.js +5 -0
  56. package/lib/common/chat-session-summary-agent.js.map +1 -1
  57. package/lib/common/chat-tool-preferences.d.ts +2 -0
  58. package/lib/common/chat-tool-preferences.d.ts.map +1 -1
  59. package/lib/common/chat-tool-preferences.js +7 -7
  60. package/lib/common/chat-tool-preferences.js.map +1 -1
  61. package/lib/common/context-summary-variable.d.ts.map +1 -1
  62. package/lib/common/context-summary-variable.js +1 -1
  63. package/lib/common/context-summary-variable.js.map +1 -1
  64. package/lib/common/custom-chat-agent.d.ts +2 -0
  65. package/lib/common/custom-chat-agent.d.ts.map +1 -1
  66. package/lib/common/custom-chat-agent.js +6 -0
  67. package/lib/common/custom-chat-agent.js.map +1 -1
  68. package/lib/common/file-read-tracker.d.ts +12 -0
  69. package/lib/common/file-read-tracker.d.ts.map +1 -0
  70. package/lib/common/file-read-tracker.js +20 -0
  71. package/lib/common/file-read-tracker.js.map +1 -0
  72. package/lib/common/index.d.ts +1 -0
  73. package/lib/common/index.d.ts.map +1 -1
  74. package/lib/common/index.js +1 -0
  75. package/lib/common/index.js.map +1 -1
  76. package/package.json +11 -11
  77. package/src/browser/ai-chat-frontend-module.ts +5 -0
  78. package/src/browser/change-set-file-element.ts +8 -5
  79. package/src/browser/change-set-file-service.ts +2 -2
  80. package/src/browser/chat-session-store-impl.spec.ts +2 -3
  81. package/src/browser/chat-session-store-impl.ts +1 -1
  82. package/src/browser/chat-tool-request-service.ts +1 -1
  83. package/src/browser/file-chat-variable-contribution.ts +2 -2
  84. package/src/browser/file-read-tracker-impl.spec.ts +318 -0
  85. package/src/browser/file-read-tracker-impl.ts +208 -0
  86. package/src/common/ai-chat-preferences.ts +4 -0
  87. package/src/common/chat-agent-service.ts +1 -1
  88. package/src/common/chat-agents.spec.ts +72 -0
  89. package/src/common/chat-agents.ts +31 -3
  90. package/src/common/chat-content-deserializer.ts +1 -1
  91. package/src/common/chat-request-parser.spec.ts +39 -1
  92. package/src/common/chat-request-parser.ts +7 -4
  93. package/src/common/chat-service-session-restore.spec.ts +190 -0
  94. package/src/common/chat-service.ts +20 -3
  95. package/src/common/chat-session-summary-agent.ts +6 -2
  96. package/src/common/chat-tool-preferences.ts +2 -2
  97. package/src/common/context-summary-variable.ts +2 -1
  98. package/src/common/custom-chat-agent.ts +5 -1
  99. package/src/common/file-read-tracker.ts +29 -0
  100. package/src/common/index.ts +1 -0
@@ -62,6 +62,7 @@ import {
62
62
  import { ContributionProvider, ILogger, isArray, nls } from '@theia/core';
63
63
  import { inject, injectable, named, optional, postConstruct } from '@theia/core/shared/inversify';
64
64
  import { ChatAgentService } from './chat-agent-service';
65
+ import { FileReadTracker } from './file-read-tracker';
65
66
  import {
66
67
  ChatModel,
67
68
  ChatRequestModel,
@@ -187,7 +188,10 @@ export function isChatAgent(agent: Agent): agent is ChatAgent {
187
188
  @injectable()
188
189
  export abstract class AbstractChatAgent implements ChatAgent {
189
190
  @inject(LanguageModelRegistry) protected languageModelRegistry: LanguageModelRegistry;
190
- @inject(ILogger) protected logger: ILogger;
191
+
192
+ @inject(ILogger) @named('ai-chat:AbstractChatAgent')
193
+ protected readonly logger: ILogger;
194
+
191
195
  @inject(ChatToolRequestService) protected chatToolRequestService: ChatToolRequestService;
192
196
  @inject(LanguageModelService) protected languageModelService: LanguageModelService;
193
197
  @inject(PromptService) protected promptService: PromptService;
@@ -200,6 +204,8 @@ export abstract class AbstractChatAgent implements ChatAgent {
200
204
 
201
205
  @inject(TokenUsageService) @optional() protected tokenUsageService: TokenUsageService | undefined;
202
206
 
207
+ @inject(FileReadTracker) @optional() protected fileReadTracker: FileReadTracker | undefined;
208
+
203
209
  readonly abstract id: string;
204
210
  readonly abstract name: string;
205
211
  readonly abstract languageModelRequirements: LanguageModelRequirement[];
@@ -255,6 +261,7 @@ export abstract class AbstractChatAgent implements ChatAgent {
255
261
  }
256
262
 
257
263
  const messages = await this.getMessages(request.session);
264
+ await this.appendExternalFileChangeNotice(request, messages);
258
265
 
259
266
  if (systemMessageDescription) {
260
267
  const systemMsg: LanguageModelMessage = {
@@ -295,6 +302,27 @@ export abstract class AbstractChatAgent implements ChatAgent {
295
302
  }
296
303
  }
297
304
 
305
+ /**
306
+ * Tells the agent which files it read were meanwhile changed by somebody else. A trailing user message
307
+ * rather than the cached system message; providers requiring alternating roles merge same-role runs.
308
+ */
309
+ protected async appendExternalFileChangeNotice(request: MutableChatRequestModel, messages: LanguageModelMessage[]): Promise<void> {
310
+ try {
311
+ const changedFiles = await this.fileReadTracker?.getChangedFiles(request.session.id);
312
+ if (changedFiles?.length) {
313
+ messages.push({
314
+ actor: 'user',
315
+ type: 'text',
316
+ text: `The following files changed since you last read them: ${changedFiles.join(', ')}. ` +
317
+ 'Read them again before relying on their content or overwriting them.'
318
+ });
319
+ }
320
+ } catch (error) {
321
+ // Advisory, so failing to determine it must not fail the request.
322
+ this.logger.warn('Could not determine externally changed files.', error);
323
+ }
324
+ }
325
+
298
326
  protected parseContents(text: string, request: MutableChatRequestModel): ChatResponseContent[] {
299
327
  return parseContents(
300
328
  text,
@@ -305,7 +333,7 @@ export abstract class AbstractChatAgent implements ChatAgent {
305
333
  };
306
334
 
307
335
  protected handleError(request: MutableChatRequestModel, error: Error): void {
308
- console.error('Error handling chat interaction:', error);
336
+ this.logger.error('Error handling chat interaction:', error);
309
337
  request.response.response.addContent(new ErrorChatResponseContentImpl(error));
310
338
  request.response.error(error);
311
339
  }
@@ -749,7 +777,7 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
749
777
  for await (const token of languageModelResponse.stream) {
750
778
  // Skip unknown tokens. For example OpenAI sends empty tokens around tool calls
751
779
  if (!isLanguageModelStreamResponsePart(token)) {
752
- console.debug(`Unknown token: '${JSON.stringify(token)}'. Skipping`);
780
+ this.logger.debug(`Unknown token: '${JSON.stringify(token)}'. Skipping`);
753
781
  continue;
754
782
  }
755
783
  const newContent = this.parse(token, request);
@@ -194,7 +194,7 @@ export class ChatContentDeserializerRegistryImpl implements ChatContentDeseriali
194
194
  @inject(ContributionProvider) @named(ChatContentDeserializerContribution)
195
195
  protected readonly deserializerContributions: ContributionProvider<ChatContentDeserializerContribution>;
196
196
 
197
- @inject(ILogger) @named('ChatContentDeserializerRegistry')
197
+ @inject(ILogger) @named('ai-chat:ChatContentDeserializerRegistryImpl')
198
198
  protected readonly logger: ILogger;
199
199
 
200
200
  /**
@@ -27,7 +27,7 @@ import { AgentDelegationTool } from '../browser/agent-delegation-tool';
27
27
 
28
28
  describe('ChatRequestParserImpl', () => {
29
29
  /** Command names the stubbed `PromptService` knows about. Anything else is not a command. */
30
- const KNOWN_COMMANDS = ['hello', 'explain', 'compare', 'cmd', 'summarize', 'skill-one', 'skill-two'];
30
+ const KNOWN_COMMANDS = ['hello', 'explain', 'compare', 'cmd', 'summarize', 'skill-one', 'skill-two', 'bigquery:query-builder', 'io.github.acme_tools:my-skill'];
31
31
  /** Prompt fragments that are not marked as commands, but can still be invoked by their id. */
32
32
  const KNOWN_FRAGMENT_IDS = ['coder-system'];
33
33
 
@@ -710,6 +710,44 @@ describe('ChatRequestParserImpl', () => {
710
710
  expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('coder-system');
711
711
  });
712
712
 
713
+ it('parses a qualified command name containing a colon as one command', async () => {
714
+ // A skill supplied by an installed artifact is addressed as `<qualifier>:<skill>`, so
715
+ // the colon has to be part of the command name rather than terminate it.
716
+ const text = '/bigquery:query-builder';
717
+ const result = await parse(text);
718
+
719
+ expect(result.parts.length).to.equal(1);
720
+ expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('bigquery:query-builder');
721
+ expectFullCoverage(result.parts, text);
722
+ });
723
+
724
+ it('parses a qualified command name with arguments', async () => {
725
+ const text = '/bigquery:query-builder count the rows';
726
+ const result = await parse(text);
727
+
728
+ expect(result.parts.length).to.equal(1);
729
+ expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('bigquery:query-builder|count the rows');
730
+ expectFullCoverage(result.parts, text);
731
+ });
732
+
733
+ it('parses a qualified command name whose qualifier contains periods', async () => {
734
+ // The qualifier is the plugin's directory name, which is derived from its identifier -
735
+ // `io.github.acme/tools` becomes `io.github.acme_tools`, periods and all. Without them
736
+ // in the charset the token falls through to the model as plain text, with no error.
737
+ const text = '/io.github.acme_tools:my-skill';
738
+ const result = await parse(text);
739
+
740
+ expect(result.parts.length).to.equal(1);
741
+ expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('io.github.acme_tools:my-skill');
742
+ expectFullCoverage(result.parts, text);
743
+ });
744
+
745
+ it('leaves an unknown colon-separated slash token as plain text', async () => {
746
+ // Widening the command charset must not turn arbitrary text into a command.
747
+ await expectPlainText('/unknown:thing');
748
+ await expectPlainText('/io.github.other_tools:my-skill');
749
+ });
750
+
713
751
  it('keeps trailing whitespace out of the command part', async () => {
714
752
  const text = '/summarize ';
715
753
  const result = await parse(text);
@@ -19,7 +19,7 @@
19
19
  *--------------------------------------------------------------------------------------------*/
20
20
  // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatRequestParser.ts
21
21
 
22
- import { inject, injectable } from '@theia/core/shared/inversify';
22
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
23
23
  import { ChatAgentService } from './chat-agent-service';
24
24
  import { ChatAgentLocation } from './chat-agents';
25
25
  import { ChatContext, ChatRequest } from './chat-model';
@@ -50,8 +50,10 @@ const variableReg = /^#([\w_\-]+)(?::([\w_\-_\/\\.:]+))?(?=(\s|$|\b))/i; // A #-
50
50
  // A /-command (/commandname) with optional arguments parsed separately. The command name must be
51
51
  // terminated by whitespace or the end of the input, so that path segments such as `/home/user` are
52
52
  // not mistaken for a command.
53
- const commandReg = /^\/([\w_\-]+)(?=\s|$)/;
54
- const nextCommandReg = /\s+\/([\w_\-]+)(?=\s|$)/g;
53
+ // The colon and the period are in the charset so a qualified skill is invocable as
54
+ // `/<qualifier>:<skill>`; the qualifier comes from a plugin identifier, so it usually has periods.
55
+ const commandReg = /^\/([\w_\-.:]+)(?=\s|$)/;
56
+ const nextCommandReg = /\s+\/([\w_\-.:]+)(?=\s|$)/g;
55
57
 
56
58
  export const ChatRequestParser = Symbol('ChatRequestParser');
57
59
  export interface ChatRequestParser {
@@ -74,7 +76,8 @@ export class ChatRequestParserImpl implements ChatRequestParser {
74
76
  @inject(ChatAgentService) private readonly agentService: ChatAgentService,
75
77
  @inject(AIVariableService) private readonly variableService: AIVariableService,
76
78
  @inject(ToolInvocationRegistry) private readonly toolInvocationRegistry: ToolInvocationRegistry,
77
- @inject(ILogger) private readonly logger: ILogger,
79
+ @inject(ILogger) @named('ai-chat:ChatRequestParserImpl')
80
+ protected readonly logger: ILogger
78
81
  ) { }
79
82
 
80
83
  async parseChatRequest(request: ChatRequest, location: ChatAgentLocation, context: ChatContext): Promise<ParsedChatRequest> {
@@ -0,0 +1,190 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
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, ToolInvocationRegistry } 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 Restore', () => {
32
+
33
+ class MockChatSessionStore implements ChatSessionStore {
34
+ public readCount = 0;
35
+ public readDelayMs = 50;
36
+ public serialized?: SerializedChatData;
37
+ public failReads = false;
38
+
39
+ async storeSessions(...sessions: Array<ChatModel | ChatModelWithMetadata>): Promise<void> { }
40
+
41
+ async readSession(sessionId: string): Promise<SerializedChatData | undefined> {
42
+ this.readCount++;
43
+ if (this.failReads) {
44
+ throw new Error('storage unavailable');
45
+ }
46
+ // Simulate the load time of a large persisted session: this is the window in
47
+ // which a second restore request for the same session can arrive.
48
+ await new Promise(resolve => setTimeout(resolve, this.readDelayMs));
49
+ return this.serialized;
50
+ }
51
+
52
+ async deleteSession(sessionId: string): Promise<void> { }
53
+
54
+ async clearAllSessions(): Promise<void> { }
55
+
56
+ async getSessionIndex(): Promise<ChatSessionIndex> {
57
+ return {};
58
+ }
59
+
60
+ async hasPersistedSessions(): Promise<boolean> {
61
+ return this.serialized !== undefined;
62
+ }
63
+
64
+ async setSessionTitle(sessionId: string, title: string): Promise<void> { }
65
+ }
66
+
67
+ class MockChatAgentService {
68
+ readonly onDidChangeAgents = { dispose: () => { } };
69
+ readonly onDefaultAgentChanged = { dispose: () => { } };
70
+
71
+ getAgent(): undefined {
72
+ return undefined;
73
+ }
74
+ getAgents(): never[] {
75
+ return [];
76
+ }
77
+ resolveAgent(): undefined {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ class MockChatRequestParser {
83
+ parseChatRequest(): { parts: never[]; text: string } {
84
+ return { parts: [], text: '' };
85
+ }
86
+ }
87
+
88
+ class MockAIVariableService {
89
+ resolveVariables(): Promise<unknown[]> {
90
+ return Promise.resolve([]);
91
+ }
92
+ }
93
+
94
+ class MockLogger {
95
+ error(): void { }
96
+ warn(): void { }
97
+ info(): void { }
98
+ debug(): void { }
99
+ }
100
+
101
+ const mockToolInvocationRegistry: ToolInvocationRegistry = {
102
+ registerTool: () => { },
103
+ unregisterTool: () => { },
104
+ getFunction: () => undefined,
105
+ getFunctions: () => [],
106
+ getAllFunctions: () => [],
107
+ unregisterAllTools: () => { },
108
+ onDidChange: () => ({ dispose: () => { } })
109
+ };
110
+
111
+ function createChatService(sessionStore: ChatSessionStore): ChatServiceImpl {
112
+ const container = new Container();
113
+ container.bind(ChatSessionStore).toConstantValue(sessionStore);
114
+ container.bind(ChatAgentService).toConstantValue(new MockChatAgentService() as unknown as ChatAgentService);
115
+ container.bind(ChatRequestParser).toConstantValue(new MockChatRequestParser() as unknown as ChatRequestParser);
116
+ container.bind(AIVariableService).toConstantValue(new MockAIVariableService() as unknown as AIVariableService);
117
+ container.bind(ILogger).toConstantValue(new MockLogger() as unknown as ILogger);
118
+ container.bind(ToolInvocationRegistry).toConstantValue(mockToolInvocationRegistry);
119
+ const contentRegistry = new ChatContentDeserializerRegistryImpl();
120
+ new DefaultChatContentDeserializerContribution().registerDeserializers(contentRegistry);
121
+ container.bind(ChatContentDeserializerRegistry).toConstantValue(contentRegistry);
122
+ container.bind(ChangeSetElementDeserializerRegistry).toConstantValue(new ChangeSetElementDeserializerRegistryImpl());
123
+ container.bind(ChatServiceImpl).toSelf().inSingletonScope();
124
+ return container.get(ChatServiceImpl);
125
+ }
126
+
127
+ let store: MockChatSessionStore;
128
+ let chatService: ChatServiceImpl;
129
+ let restoredId: string;
130
+
131
+ beforeEach(async () => {
132
+ // Build valid serialized session data by serializing a real session.
133
+ const sourceStore = new MockChatSessionStore();
134
+ const sourceService = createChatService(sourceStore);
135
+ const sourceSession = sourceService.createSession(ChatAgentLocation.Panel);
136
+ await sourceService.sendRequest(sourceSession.id, { text: 'Test' });
137
+ const serialized: SerializedChatData = {
138
+ version: 1,
139
+ model: sourceSession.model.toSerializable(),
140
+ pinnedAgentId: undefined,
141
+ saveDate: Date.now()
142
+ };
143
+ restoredId = sourceSession.id;
144
+
145
+ store = new MockChatSessionStore();
146
+ store.serialized = serialized;
147
+ chatService = createChatService(store);
148
+ });
149
+
150
+ it('should register a session only once when restored concurrently', async () => {
151
+ const [first, second] = await Promise.all([
152
+ chatService.getOrRestoreSession(restoredId),
153
+ chatService.getOrRestoreSession(restoredId)
154
+ ]);
155
+
156
+ expect(first).to.not.be.undefined;
157
+ expect(second).to.equal(first);
158
+ expect(chatService.getSessions().filter(s => s.id === restoredId)).to.have.lengthOf(1);
159
+ expect(store.readCount).to.equal(1);
160
+ });
161
+
162
+ it('should keep getActiveSession working when concurrent restores are followed by activation', async () => {
163
+ // Opening a session from the session list runs restore + activate. A double-click on a
164
+ // large (slow to load) session runs it twice concurrently. Without a guard this
165
+ // registers the session twice, setActiveSession(id) marks both copies active, and every
166
+ // getActiveSession() call throws from then on — disabling the chat Home button and
167
+ // other session commands until the page is reloaded.
168
+ await Promise.all([
169
+ chatService.getOrRestoreSession(restoredId).then(() => chatService.setActiveSession(restoredId, { focus: true })),
170
+ chatService.getOrRestoreSession(restoredId).then(() => chatService.setActiveSession(restoredId, { focus: true }))
171
+ ]);
172
+
173
+ const active = chatService.getActiveSession();
174
+ expect(active).to.not.be.undefined;
175
+ expect(active!.id).to.equal(restoredId);
176
+ });
177
+
178
+ it('should allow restoring again after a failed attempt', async () => {
179
+ store.failReads = true;
180
+ await chatService.getOrRestoreSession(restoredId).then(
181
+ () => { throw new Error('expected the restore to fail'); },
182
+ () => undefined
183
+ );
184
+
185
+ store.failReads = false;
186
+ const session = await chatService.getOrRestoreSession(restoredId);
187
+ expect(session).to.not.be.undefined;
188
+ expect(session!.id).to.equal(restoredId);
189
+ });
190
+ });
@@ -22,7 +22,7 @@
22
22
  import { AIVariableResolutionRequest, AIVariableService, ResolvedAIContextVariable, ToolInvocationRegistry, ToolRequest } from '@theia/ai-core';
23
23
  import { Emitter, Event, ILogger, URI, generateUuid } from '@theia/core';
24
24
  import { Deferred } from '@theia/core/lib/common/promise-util';
25
- import { inject, injectable, optional } from '@theia/core/shared/inversify';
25
+ import { inject, injectable, optional, named } from '@theia/core/shared/inversify';
26
26
  import { ChatAgentService, DefaultChatAgentId, FallbackChatAgentId } from './chat-agent-service';
27
27
  import { ChatAgent, ChatAgentLocation, ChatSessionContext } from './chat-agents';
28
28
  import {
@@ -184,8 +184,8 @@ export class ChatServiceImpl implements ChatService {
184
184
  @inject(AIVariableService)
185
185
  protected variableService: AIVariableService;
186
186
 
187
- @inject(ILogger)
188
- protected logger: ILogger;
187
+ @inject(ILogger) @named('ai-chat:ChatServiceImpl')
188
+ protected readonly logger: ILogger;
189
189
 
190
190
  @inject(ChatSessionStore) @optional()
191
191
  protected sessionStore: ChatSessionStore | undefined;
@@ -201,6 +201,9 @@ export class ChatServiceImpl implements ChatService {
201
201
 
202
202
  protected _sessions: ChatSessionInternal[] = [];
203
203
 
204
+ /** In-flight session restores by id, so concurrent requests for the same session share one restore. */
205
+ protected readonly pendingSessionRestores = new Map<string, Promise<ChatSession | undefined>>();
206
+
204
207
  getSessions(): ChatSessionInternal[] {
205
208
  return [...this._sessions];
206
209
  }
@@ -532,7 +535,21 @@ export class ChatServiceImpl implements ChatService {
532
535
  this.logger.debug('Session already loaded', { sessionId });
533
536
  return existing;
534
537
  }
538
+ // Share one in-flight restore per session id: restoring a large session takes long
539
+ // enough that a second request (e.g. a double-click in the session list) can arrive
540
+ // mid-restore. Without this guard both restores register the session, and
541
+ // setActiveSession then marks both copies active, breaking getActiveSession()
542
+ // (and with it e.g. the chat Home button) until the page is reloaded.
543
+ let pending = this.pendingSessionRestores.get(sessionId);
544
+ if (!pending) {
545
+ pending = this.restoreSession(sessionId)
546
+ .finally(() => this.pendingSessionRestores.delete(sessionId));
547
+ this.pendingSessionRestores.set(sessionId, pending);
548
+ }
549
+ return pending;
550
+ }
535
551
 
552
+ protected async restoreSession(sessionId: string): Promise<ChatSession | undefined> {
536
553
  if (!this.sessionStore) {
537
554
  this.logger.debug('Session store not available, cannot restore', { sessionId });
538
555
  return undefined;
@@ -18,14 +18,18 @@ import {
18
18
  LanguageModelRequirement,
19
19
  PromptVariantSet
20
20
  } from '@theia/ai-core';
21
- import { injectable } from '@theia/core/shared/inversify';
21
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
22
22
  import { AbstractStreamParsingChatAgent, ChatAgent } from './chat-agents';
23
23
  import { CHAT_SESSION_SUMMARY_PROMPT } from './chat-session-summary-agent-prompt';
24
- import { nls } from '@theia/core';
24
+ import { ILogger, nls } from '@theia/core';
25
25
 
26
26
  @injectable()
27
27
  export class ChatSessionSummaryAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
28
28
  static ID = 'chat-session-summary-agent';
29
+
30
+ @inject(ILogger) @named('ai-chat:ChatSessionSummaryAgent')
31
+ protected override readonly logger: ILogger;
32
+
29
33
  id = ChatSessionSummaryAgent.ID;
30
34
  name = 'Chat Session Summary';
31
35
  override description = nls.localize('theia/ai/chat/chatSessionSummaryAgent/description', 'Agent for generating chat session summaries.');
@@ -57,13 +57,13 @@ export const TOOL_CONFIRMATION_PREFERENCE = 'ai-features.chat.toolConfirmation';
57
57
  export const DEFAULT_TOOL_CONFIRMATION_PREFERENCE = 'ai-features.chat.defaultToolConfirmation';
58
58
  export const TOOL_CONFIRMATION_TIMEOUT_PREFERENCE = 'ai-features.chat.toolConfirmationTimeout';
59
59
 
60
- const TOOL_CONFIRMATION_MODE_VALUES = [
60
+ export const TOOL_CONFIRMATION_MODE_VALUES = [
61
61
  ToolConfirmationMode.ALWAYS_ALLOW,
62
62
  ToolConfirmationMode.CONFIRM,
63
63
  ToolConfirmationMode.DISABLED
64
64
  ];
65
65
 
66
- const TOOL_CONFIRMATION_MODE_DESCRIPTIONS = [
66
+ export const TOOL_CONFIRMATION_MODE_DESCRIPTIONS = [
67
67
  nls.localize('theia/ai/chat/toolConfirmation/alwaysAllow/description', 'Execute tools automatically without confirmation'),
68
68
  nls.localize('theia/ai/chat/toolConfirmation/confirm/description', 'Ask for confirmation before executing tools'),
69
69
  nls.localize('theia/ai/chat/toolConfirmation/disabled/description', 'Disable tool execution')
@@ -22,7 +22,8 @@ import { ChatSessionContext } from './chat-agents';
22
22
 
23
23
  export const CONTEXT_SUMMARY_VARIABLE: AIVariable = {
24
24
  id: 'contextSummary',
25
- description: nls.localize('theia/ai/core/contextSummaryVariable/description', 'Describes files in the context for a given session.'),
25
+ description: nls.localize('theia/ai/chat/contextSummaryVariable/description',
26
+ 'A JSON summary of every element in the current chat session context, listing each element\'s type and identifiers.'),
26
27
  name: 'contextSummary',
27
28
  };
28
29
 
@@ -16,10 +16,14 @@
16
16
 
17
17
  import { CustomAgentPromptVariant, LanguageModelRequirement } from '@theia/ai-core';
18
18
  import { AbstractStreamParsingChatAgent } from './chat-agents';
19
- import { injectable } from '@theia/core/shared/inversify';
19
+ import { ILogger } from '@theia/core';
20
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
20
21
 
21
22
  @injectable()
22
23
  export class CustomChatAgent extends AbstractStreamParsingChatAgent {
24
+ @inject(ILogger) @named('ai-chat:CustomChatAgent')
25
+ protected override readonly logger: ILogger;
26
+
23
27
  id: string = 'CustomChatAgent';
24
28
  name: string = 'CustomChatAgent';
25
29
  languageModelRequirements: LanguageModelRequirement[] = [{ purpose: 'chat' }];
@@ -0,0 +1,29 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Ehab Younes.
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 { URI } from '@theia/core';
18
+
19
+ export const FileReadTracker = Symbol('FileReadTracker');
20
+
21
+ /** Tracks the content an agent has seen per chat session, so that changes by anyone else can be reported back to it. */
22
+ export interface FileReadTracker {
23
+ /** Snapshots `uri` as the session's agent just saw it, or forgets it if it is gone. Pass `content` when at hand to save a read. */
24
+ recordRead(sessionId: string, uri: URI, content?: string): Promise<void>;
25
+ /** Whether `uri` differs from what the session's agent last saw. `false` if never read or gone: neither holds content a write could discard. */
26
+ isStale(sessionId: string, uri: URI): Promise<boolean>;
27
+ /** Files changed since the session's agent read them, reported again until it reads them anew so an ignored notice cannot get lost. */
28
+ getChangedFiles(sessionId: string): Promise<string[]>;
29
+ }
@@ -27,5 +27,6 @@ export * from './custom-chat-agent';
27
27
  export * from './parsed-chat-request';
28
28
  export * from './context-variables';
29
29
  export * from './chat-tool-request-service';
30
+ export * from './file-read-tracker';
30
31
  export * from './chat-tool-confirmation-timeout';
31
32
  export * from './provider-error-formatter';