@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.
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +4 -0
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-file-element.d.ts +2 -0
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +10 -4
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/change-set-file-service.js +1 -0
- package/lib/browser/change-set-file-service.js.map +1 -1
- package/lib/browser/chat-session-store-impl.js +1 -1
- package/lib/browser/chat-session-store-impl.js.map +1 -1
- package/lib/browser/chat-session-store-impl.spec.js +1 -2
- package/lib/browser/chat-session-store-impl.spec.js.map +1 -1
- package/lib/browser/chat-tool-request-service.js +1 -1
- package/lib/browser/chat-tool-request-service.js.map +1 -1
- package/lib/browser/file-chat-variable-contribution.js +1 -0
- package/lib/browser/file-chat-variable-contribution.js.map +1 -1
- package/lib/browser/file-read-tracker-impl.d.ts +55 -0
- package/lib/browser/file-read-tracker-impl.d.ts.map +1 -0
- package/lib/browser/file-read-tracker-impl.js +200 -0
- package/lib/browser/file-read-tracker-impl.js.map +1 -0
- package/lib/browser/file-read-tracker-impl.spec.d.ts +2 -0
- package/lib/browser/file-read-tracker-impl.spec.d.ts.map +1 -0
- package/lib/browser/file-read-tracker-impl.spec.js +251 -0
- package/lib/browser/file-read-tracker-impl.spec.js.map +1 -0
- package/lib/common/ai-chat-preferences.d.ts.map +1 -1
- package/lib/common/ai-chat-preferences.js +4 -0
- package/lib/common/ai-chat-preferences.js.map +1 -1
- package/lib/common/chat-agent-service.js +1 -0
- package/lib/common/chat-agent-service.js.map +1 -1
- package/lib/common/chat-agents.d.ts +8 -1
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +31 -2
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-agents.spec.js +51 -0
- package/lib/common/chat-agents.spec.js.map +1 -1
- package/lib/common/chat-content-deserializer.js +1 -1
- package/lib/common/chat-content-deserializer.js.map +1 -1
- package/lib/common/chat-request-parser.d.ts +1 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +5 -2
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-request-parser.spec.js +32 -1
- package/lib/common/chat-request-parser.spec.js.map +1 -1
- package/lib/common/chat-service-session-restore.spec.d.ts +2 -0
- package/lib/common/chat-service-session-restore.spec.d.ts.map +1 -0
- package/lib/common/chat-service-session-restore.spec.js +165 -0
- package/lib/common/chat-service-session-restore.spec.js.map +1 -0
- package/lib/common/chat-service.d.ts +4 -1
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +17 -0
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-session-summary-agent.d.ts +2 -0
- package/lib/common/chat-session-summary-agent.d.ts.map +1 -1
- package/lib/common/chat-session-summary-agent.js +5 -0
- package/lib/common/chat-session-summary-agent.js.map +1 -1
- package/lib/common/chat-tool-preferences.d.ts +2 -0
- package/lib/common/chat-tool-preferences.d.ts.map +1 -1
- package/lib/common/chat-tool-preferences.js +7 -7
- package/lib/common/chat-tool-preferences.js.map +1 -1
- package/lib/common/context-summary-variable.d.ts.map +1 -1
- package/lib/common/context-summary-variable.js +1 -1
- package/lib/common/context-summary-variable.js.map +1 -1
- package/lib/common/custom-chat-agent.d.ts +2 -0
- package/lib/common/custom-chat-agent.d.ts.map +1 -1
- package/lib/common/custom-chat-agent.js +6 -0
- package/lib/common/custom-chat-agent.js.map +1 -1
- package/lib/common/file-read-tracker.d.ts +12 -0
- package/lib/common/file-read-tracker.d.ts.map +1 -0
- package/lib/common/file-read-tracker.js +20 -0
- package/lib/common/file-read-tracker.js.map +1 -0
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/ai-chat-frontend-module.ts +5 -0
- package/src/browser/change-set-file-element.ts +8 -5
- package/src/browser/change-set-file-service.ts +2 -2
- package/src/browser/chat-session-store-impl.spec.ts +2 -3
- package/src/browser/chat-session-store-impl.ts +1 -1
- package/src/browser/chat-tool-request-service.ts +1 -1
- package/src/browser/file-chat-variable-contribution.ts +2 -2
- package/src/browser/file-read-tracker-impl.spec.ts +318 -0
- package/src/browser/file-read-tracker-impl.ts +208 -0
- package/src/common/ai-chat-preferences.ts +4 -0
- package/src/common/chat-agent-service.ts +1 -1
- package/src/common/chat-agents.spec.ts +72 -0
- package/src/common/chat-agents.ts +31 -3
- package/src/common/chat-content-deserializer.ts +1 -1
- package/src/common/chat-request-parser.spec.ts +39 -1
- package/src/common/chat-request-parser.ts +7 -4
- package/src/common/chat-service-session-restore.spec.ts +190 -0
- package/src/common/chat-service.ts +20 -3
- package/src/common/chat-session-summary-agent.ts +6 -2
- package/src/common/chat-tool-preferences.ts +2 -2
- package/src/common/context-summary-variable.ts +2 -1
- package/src/common/custom-chat-agent.ts +5 -1
- package/src/common/file-read-tracker.ts +29 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
54
|
-
|
|
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)
|
|
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/
|
|
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 {
|
|
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
|
+
}
|
package/src/common/index.ts
CHANGED
|
@@ -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';
|