@theia/ai-chat 1.74.0-next.9 → 1.74.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/agent-delegation-tool.js +6 -7
- package/lib/browser/agent-delegation-tool.js.map +1 -1
- package/lib/browser/agent-delegation-tool.spec.js +13 -0
- package/lib/browser/agent-delegation-tool.spec.js.map +1 -1
- package/lib/browser/chat-session-store-impl.d.ts.map +1 -1
- package/lib/browser/chat-session-store-impl.js +2 -0
- package/lib/browser/chat-session-store-impl.js.map +1 -1
- package/lib/browser/chat-session-store-impl.spec.js +51 -2
- package/lib/browser/chat-session-store-impl.spec.js.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.d.ts +18 -8
- package/lib/browser/chat-tool-preference-bindings.d.ts.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.js +27 -28
- package/lib/browser/chat-tool-preference-bindings.js.map +1 -1
- package/lib/browser/chat-tool-preference-bindings.spec.js +43 -46
- package/lib/browser/chat-tool-preference-bindings.spec.js.map +1 -1
- package/lib/common/chat-agents.d.ts +8 -0
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +35 -1
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-agents.spec.js +94 -0
- package/lib/common/chat-agents.spec.js.map +1 -1
- package/lib/common/chat-auto-save.spec.js +13 -0
- package/lib/common/chat-auto-save.spec.js.map +1 -1
- package/lib/common/chat-content-deserializer.d.ts.map +1 -1
- package/lib/common/chat-content-deserializer.js +4 -0
- package/lib/common/chat-content-deserializer.js.map +1 -1
- package/lib/common/chat-model-serialization.d.ts +12 -1
- package/lib/common/chat-model-serialization.d.ts.map +1 -1
- package/lib/common/chat-model-serialization.js.map +1 -1
- package/lib/common/chat-model-serialization.spec.js +29 -0
- package/lib/common/chat-model-serialization.spec.js.map +1 -1
- package/lib/common/chat-model.d.ts +104 -2
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +151 -2
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-model.spec.d.ts +2 -0
- package/lib/common/chat-model.spec.d.ts.map +1 -0
- package/lib/common/chat-model.spec.js +42 -0
- package/lib/common/chat-model.spec.js.map +1 -0
- package/lib/common/chat-request-parser.d.ts +4 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +43 -3
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-request-parser.spec.js +59 -0
- package/lib/common/chat-request-parser.spec.js.map +1 -1
- package/lib/common/chat-service-deletion.spec.js +43 -0
- package/lib/common/chat-service-deletion.spec.js.map +1 -1
- package/lib/common/chat-service-pinned-agent.spec.js +52 -2
- package/lib/common/chat-service-pinned-agent.spec.js.map +1 -1
- package/lib/common/chat-service-session-events.spec.d.ts +2 -0
- package/lib/common/chat-service-session-events.spec.d.ts.map +1 -0
- package/lib/common/chat-service-session-events.spec.js +130 -0
- package/lib/common/chat-service-session-events.spec.js.map +1 -0
- package/lib/common/chat-service.d.ts +3 -0
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +60 -6
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-service.spec.d.ts +2 -0
- package/lib/common/chat-service.spec.d.ts.map +1 -0
- package/lib/common/chat-service.spec.js +177 -0
- package/lib/common/chat-service.spec.js.map +1 -0
- package/lib/common/chat-session-status.spec.d.ts +2 -0
- package/lib/common/chat-session-status.spec.d.ts.map +1 -0
- package/lib/common/chat-session-status.spec.js +213 -0
- package/lib/common/chat-session-status.spec.js.map +1 -0
- package/lib/common/chat-session-store.d.ts +8 -0
- package/lib/common/chat-session-store.d.ts.map +1 -1
- package/lib/common/tool-call-response-content.spec.js +36 -0
- package/lib/common/tool-call-response-content.spec.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/agent-delegation-tool.spec.ts +16 -0
- package/src/browser/agent-delegation-tool.ts +7 -7
- package/src/browser/chat-session-store-impl.spec.ts +62 -2
- package/src/browser/chat-session-store-impl.ts +2 -0
- package/src/browser/chat-tool-preference-bindings.spec.ts +43 -48
- package/src/browser/chat-tool-preference-bindings.ts +42 -30
- package/src/common/chat-agents.spec.ts +112 -2
- package/src/common/chat-agents.ts +44 -1
- package/src/common/chat-auto-save.spec.ts +18 -1
- package/src/common/chat-content-deserializer.ts +11 -0
- package/src/common/chat-model-serialization.spec.ts +38 -0
- package/src/common/chat-model-serialization.ts +12 -1
- package/src/common/chat-model.spec.ts +42 -0
- package/src/common/chat-model.ts +233 -2
- package/src/common/chat-request-parser.spec.ts +69 -1
- package/src/common/chat-request-parser.ts +51 -6
- package/src/common/chat-service-deletion.spec.ts +52 -0
- package/src/common/chat-service-pinned-agent.spec.ts +58 -2
- package/src/common/chat-service-session-events.spec.ts +161 -0
- package/src/common/chat-service.spec.ts +211 -0
- package/src/common/chat-service.ts +64 -8
- package/src/common/chat-session-status.spec.ts +280 -0
- package/src/common/chat-session-store.ts +8 -0
- package/src/common/tool-call-response-content.spec.ts +42 -0
|
@@ -23,9 +23,7 @@ import {
|
|
|
23
23
|
TOOL_CONFIRMATION_PREFERENCE,
|
|
24
24
|
ToolConfirmationMode
|
|
25
25
|
} from '../common/chat-tool-preferences';
|
|
26
|
-
import { ToolRequest } from '@theia/ai-core';
|
|
27
|
-
import { PreferenceService } from '@theia/core/lib/common/preferences';
|
|
28
|
-
import { TrustAwarePreferenceReader } from '@theia/ai-core/lib/browser/trust-aware-preference-reader';
|
|
26
|
+
import { AiConfigurationService, ToolRequest } from '@theia/ai-core';
|
|
29
27
|
|
|
30
28
|
interface InspectResult<T> {
|
|
31
29
|
defaultValue?: T;
|
|
@@ -35,8 +33,7 @@ interface InspectResult<T> {
|
|
|
35
33
|
|
|
36
34
|
describe('ToolConfirmationManager', () => {
|
|
37
35
|
let manager: ToolConfirmationManager;
|
|
38
|
-
let
|
|
39
|
-
let trustAwareReaderMock: sinon.SinonStubbedInstance<TrustAwarePreferenceReader>;
|
|
36
|
+
let aiConfigurationServiceMock: sinon.SinonStubbedInstance<AiConfigurationService>;
|
|
40
37
|
let storedPerToolPreferences: { [toolId: string]: ToolConfirmationMode };
|
|
41
38
|
let storedDefaultMode: ToolConfirmationMode | undefined;
|
|
42
39
|
let trusted: boolean;
|
|
@@ -58,27 +55,9 @@ describe('ToolConfirmationManager', () => {
|
|
|
58
55
|
perToolInspectResult = undefined;
|
|
59
56
|
defaultInspectResult = undefined;
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
storedPerToolPreferences = value as { [toolId: string]: ToolConfirmationMode };
|
|
65
|
-
} else if (key === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
|
|
66
|
-
storedDefaultMode = value as ToolConfirmationMode;
|
|
67
|
-
}
|
|
68
|
-
return Promise.resolve();
|
|
69
|
-
}),
|
|
70
|
-
inspect: sinon.stub().callsFake((name: string) => {
|
|
71
|
-
if (name === TOOL_CONFIRMATION_PREFERENCE) {
|
|
72
|
-
return perToolInspectResult;
|
|
73
|
-
}
|
|
74
|
-
if (name === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
|
|
75
|
-
return defaultInspectResult;
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
})
|
|
79
|
-
} as unknown as sinon.SinonStubbedInstance<PreferenceService>;
|
|
80
|
-
|
|
81
|
-
trustAwareReaderMock = {
|
|
58
|
+
// The manager talks only to AiConfigurationService. `get` mirrors the trust-aware read
|
|
59
|
+
// semantics, `update` the smart write, and `inspect` exposes schema defaults.
|
|
60
|
+
aiConfigurationServiceMock = {
|
|
82
61
|
get: sinon.stub().callsFake(<T>(name: string, fallback?: T): T | undefined => {
|
|
83
62
|
if (name === TOOL_CONFIRMATION_PREFERENCE) {
|
|
84
63
|
if (trusted) {
|
|
@@ -95,13 +74,29 @@ describe('ToolConfirmationManager', () => {
|
|
|
95
74
|
return ((value as unknown as T) ?? fallback);
|
|
96
75
|
}
|
|
97
76
|
return fallback;
|
|
77
|
+
}),
|
|
78
|
+
update: sinon.stub().callsFake((key: string, value: unknown) => {
|
|
79
|
+
if (key === TOOL_CONFIRMATION_PREFERENCE) {
|
|
80
|
+
storedPerToolPreferences = value as { [toolId: string]: ToolConfirmationMode };
|
|
81
|
+
} else if (key === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
|
|
82
|
+
storedDefaultMode = value as ToolConfirmationMode;
|
|
83
|
+
}
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
}),
|
|
86
|
+
inspect: sinon.stub().callsFake((name: string) => {
|
|
87
|
+
if (name === TOOL_CONFIRMATION_PREFERENCE) {
|
|
88
|
+
return perToolInspectResult;
|
|
89
|
+
}
|
|
90
|
+
if (name === DEFAULT_TOOL_CONFIRMATION_PREFERENCE) {
|
|
91
|
+
return defaultInspectResult;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
98
94
|
})
|
|
99
|
-
} as unknown as sinon.SinonStubbedInstance<
|
|
95
|
+
} as unknown as sinon.SinonStubbedInstance<AiConfigurationService>;
|
|
100
96
|
|
|
101
97
|
const container = new Container();
|
|
102
98
|
container.bind(ToolConfirmationManager).toSelf().inSingletonScope();
|
|
103
|
-
container.bind(
|
|
104
|
-
container.bind(TrustAwarePreferenceReader).toConstantValue(trustAwareReaderMock as unknown as TrustAwarePreferenceReader);
|
|
99
|
+
container.bind(AiConfigurationService).toConstantValue(aiConfigurationServiceMock as unknown as AiConfigurationService);
|
|
105
100
|
manager = container.get(ToolConfirmationManager);
|
|
106
101
|
});
|
|
107
102
|
|
|
@@ -124,9 +119,9 @@ describe('ToolConfirmationManager', () => {
|
|
|
124
119
|
describe('setDefaultConfirmationMode', () => {
|
|
125
120
|
it('persists the new default through the preference service', () => {
|
|
126
121
|
manager.setDefaultConfirmationMode(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
127
|
-
expect(
|
|
128
|
-
expect(
|
|
129
|
-
expect(
|
|
122
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
123
|
+
expect(aiConfigurationServiceMock.update.firstCall.args[0]).to.equal(DEFAULT_TOOL_CONFIRMATION_PREFERENCE);
|
|
124
|
+
expect(aiConfigurationServiceMock.update.firstCall.args[1]).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
130
125
|
expect(storedDefaultMode).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
131
126
|
});
|
|
132
127
|
});
|
|
@@ -223,31 +218,31 @@ describe('ToolConfirmationManager', () => {
|
|
|
223
218
|
describe('setConfirmationMode', () => {
|
|
224
219
|
it('persists ALWAYS_ALLOW for a regular tool when default is CONFIRM', () => {
|
|
225
220
|
manager.setConfirmationMode('regularTool', ToolConfirmationMode.ALWAYS_ALLOW);
|
|
226
|
-
expect(
|
|
221
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
227
222
|
expect(storedPerToolPreferences['regularTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
228
223
|
});
|
|
229
224
|
|
|
230
225
|
it('persists ALWAYS_ALLOW for confirmAlwaysAllow tools', () => {
|
|
231
226
|
const toolRequest = createToolRequest('dangerousTool', true);
|
|
232
227
|
manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
|
|
233
|
-
expect(
|
|
228
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
234
229
|
expect(storedPerToolPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
235
230
|
});
|
|
236
231
|
|
|
237
232
|
it('does not persist when mode matches the global default', () => {
|
|
238
233
|
storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
|
|
239
234
|
manager.setConfirmationMode('regularTool', ToolConfirmationMode.ALWAYS_ALLOW);
|
|
240
|
-
expect(
|
|
235
|
+
expect(aiConfigurationServiceMock.update.called).to.be.false;
|
|
241
236
|
});
|
|
242
237
|
|
|
243
238
|
it('does not persist CONFIRM for a regular tool when default is CONFIRM', () => {
|
|
244
239
|
manager.setConfirmationMode('regularTool', ToolConfirmationMode.CONFIRM);
|
|
245
|
-
expect(
|
|
240
|
+
expect(aiConfigurationServiceMock.update.called).to.be.false;
|
|
246
241
|
});
|
|
247
242
|
|
|
248
243
|
it('persists DISABLED when default is CONFIRM', () => {
|
|
249
244
|
manager.setConfirmationMode('regularTool', ToolConfirmationMode.DISABLED);
|
|
250
|
-
expect(
|
|
245
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
251
246
|
expect(storedPerToolPreferences['regularTool']).to.equal(ToolConfirmationMode.DISABLED);
|
|
252
247
|
});
|
|
253
248
|
|
|
@@ -256,7 +251,7 @@ describe('ToolConfirmationManager', () => {
|
|
|
256
251
|
defaultValue: { 'myTool': ToolConfirmationMode.DISABLED }
|
|
257
252
|
};
|
|
258
253
|
manager.setConfirmationMode('myTool', ToolConfirmationMode.DISABLED);
|
|
259
|
-
expect(
|
|
254
|
+
expect(aiConfigurationServiceMock.update.called).to.be.false;
|
|
260
255
|
});
|
|
261
256
|
|
|
262
257
|
it('removes an existing entry when mode matches the tool-specific schema default', () => {
|
|
@@ -265,14 +260,14 @@ describe('ToolConfirmationManager', () => {
|
|
|
265
260
|
};
|
|
266
261
|
storedPerToolPreferences['myTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
|
|
267
262
|
manager.setConfirmationMode('myTool', ToolConfirmationMode.DISABLED);
|
|
268
|
-
expect(
|
|
263
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
269
264
|
expect(storedPerToolPreferences['myTool']).to.be.undefined;
|
|
270
265
|
});
|
|
271
266
|
|
|
272
267
|
it('does not persist CONFIRM for confirmAlwaysAllow tools (matches effective default)', () => {
|
|
273
268
|
const toolRequest = createToolRequest('dangerousTool', true);
|
|
274
269
|
manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.CONFIRM, toolRequest);
|
|
275
|
-
expect(
|
|
270
|
+
expect(aiConfigurationServiceMock.update.called).to.be.false;
|
|
276
271
|
});
|
|
277
272
|
|
|
278
273
|
it('persists ALWAYS_ALLOW for a confirmAlwaysAllow tool when global default is ALWAYS_ALLOW', () => {
|
|
@@ -281,7 +276,7 @@ describe('ToolConfirmationManager', () => {
|
|
|
281
276
|
storedDefaultMode = ToolConfirmationMode.ALWAYS_ALLOW;
|
|
282
277
|
const toolRequest = createToolRequest('dangerousTool', true);
|
|
283
278
|
manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
|
|
284
|
-
expect(
|
|
279
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
285
280
|
expect(storedPerToolPreferences['dangerousTool']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
286
281
|
});
|
|
287
282
|
|
|
@@ -289,13 +284,13 @@ describe('ToolConfirmationManager', () => {
|
|
|
289
284
|
const toolRequest = createToolRequest('dangerousTool', true);
|
|
290
285
|
storedPerToolPreferences['dangerousTool'] = ToolConfirmationMode.ALWAYS_ALLOW;
|
|
291
286
|
manager.setConfirmationMode('dangerousTool', ToolConfirmationMode.CONFIRM, toolRequest);
|
|
292
|
-
expect(
|
|
287
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
293
288
|
expect(storedPerToolPreferences['dangerousTool']).to.be.undefined;
|
|
294
289
|
});
|
|
295
290
|
|
|
296
291
|
it('persists DISABLED for any tool when default is CONFIRM', () => {
|
|
297
292
|
manager.setConfirmationMode('anyTool', ToolConfirmationMode.DISABLED);
|
|
298
|
-
expect(
|
|
293
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
299
294
|
expect(storedPerToolPreferences['anyTool']).to.equal(ToolConfirmationMode.DISABLED);
|
|
300
295
|
});
|
|
301
296
|
});
|
|
@@ -362,9 +357,9 @@ describe('ToolConfirmationManager', () => {
|
|
|
362
357
|
|
|
363
358
|
manager.resetAllConfirmationModeSettings();
|
|
364
359
|
|
|
365
|
-
expect(
|
|
366
|
-
expect(
|
|
367
|
-
expect(
|
|
360
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
361
|
+
expect(aiConfigurationServiceMock.update.firstCall.args[0]).to.equal(TOOL_CONFIRMATION_PREFERENCE);
|
|
362
|
+
expect(aiConfigurationServiceMock.update.firstCall.args[1]).to.deep.equal({});
|
|
368
363
|
});
|
|
369
364
|
|
|
370
365
|
it('does not modify the default-confirmation preference', () => {
|
|
@@ -386,7 +381,7 @@ describe('ToolConfirmationManager', () => {
|
|
|
386
381
|
|
|
387
382
|
manager.setConfirmationMode('shellExecute', ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
|
|
388
383
|
|
|
389
|
-
expect(
|
|
384
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
390
385
|
expect(storedPerToolPreferences['shellExecute']).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
|
|
391
386
|
|
|
392
387
|
mode = manager.getConfirmationMode('shellExecute', 'chat-1', toolRequest);
|
|
@@ -398,7 +393,7 @@ describe('ToolConfirmationManager', () => {
|
|
|
398
393
|
|
|
399
394
|
manager.setConfirmationMode('shellExecute', ToolConfirmationMode.DISABLED, toolRequest);
|
|
400
395
|
|
|
401
|
-
expect(
|
|
396
|
+
expect(aiConfigurationServiceMock.update.calledOnce).to.be.true;
|
|
402
397
|
expect(storedPerToolPreferences['shellExecute']).to.equal(ToolConfirmationMode.DISABLED);
|
|
403
398
|
|
|
404
399
|
const mode = manager.getConfirmationMode('shellExecute', 'chat-1', toolRequest);
|
|
@@ -15,27 +15,30 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
18
|
-
import {
|
|
19
|
-
PreferenceService,
|
|
20
|
-
} from '@theia/core/lib/common/preferences';
|
|
21
18
|
import {
|
|
22
19
|
ToolConfirmationMode,
|
|
23
20
|
TOOL_CONFIRMATION_PREFERENCE,
|
|
24
21
|
DEFAULT_TOOL_CONFIRMATION_PREFERENCE
|
|
25
22
|
} from '../common/chat-tool-preferences';
|
|
26
|
-
import { ToolRequest } from '@theia/ai-core';
|
|
27
|
-
|
|
23
|
+
import { AiConfigurationService, ToolRequest } from '@theia/ai-core';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The loop-invariant inputs to {@link ToolConfirmationManager.computeEffectiveDefaultForTool}:
|
|
27
|
+
* the product-shipped per-tool schema defaults and the effective global default. Reading these
|
|
28
|
+
* once lets bulk operations avoid re-inspecting the preference schema per tool.
|
|
29
|
+
*/
|
|
30
|
+
interface ToolConfirmationDefaults {
|
|
31
|
+
perToolDefaults?: { [toolId: string]: ToolConfirmationMode };
|
|
32
|
+
globalDefault: ToolConfirmationMode;
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* Utility class to manage tool confirmation settings
|
|
31
37
|
*/
|
|
32
38
|
@injectable()
|
|
33
39
|
export class ToolConfirmationManager {
|
|
34
|
-
@inject(
|
|
35
|
-
protected readonly
|
|
36
|
-
|
|
37
|
-
@inject(TrustAwarePreferenceReader)
|
|
38
|
-
protected readonly trustAwareReader: TrustAwarePreferenceReader;
|
|
40
|
+
@inject(AiConfigurationService)
|
|
41
|
+
protected readonly aiConfigurationService: AiConfigurationService;
|
|
39
42
|
|
|
40
43
|
// In-memory session overrides (not persisted), per chat
|
|
41
44
|
protected sessionOverrides: Map<string, Map<string, ToolConfirmationMode>> = new Map();
|
|
@@ -47,7 +50,7 @@ export class ToolConfirmationManager {
|
|
|
47
50
|
* the default to a more permissive value.
|
|
48
51
|
*/
|
|
49
52
|
getDefaultConfirmationMode(): ToolConfirmationMode {
|
|
50
|
-
const value = this.
|
|
53
|
+
const value = this.aiConfigurationService.get<ToolConfirmationMode>(DEFAULT_TOOL_CONFIRMATION_PREFERENCE);
|
|
51
54
|
return value ?? this.getDefaultPreferenceSchemaDefault();
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -58,7 +61,7 @@ export class ToolConfirmationManager {
|
|
|
58
61
|
* `await` completion and react to errors (e.g. show a notification on failure).
|
|
59
62
|
*/
|
|
60
63
|
setDefaultConfirmationMode(mode: ToolConfirmationMode): Promise<void> {
|
|
61
|
-
return this.
|
|
64
|
+
return this.aiConfigurationService.update(DEFAULT_TOOL_CONFIRMATION_PREFERENCE, mode);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/**
|
|
@@ -77,7 +80,7 @@ export class ToolConfirmationManager {
|
|
|
77
80
|
if (chatMap && chatMap.has(toolId)) {
|
|
78
81
|
return chatMap.get(toolId)!;
|
|
79
82
|
}
|
|
80
|
-
const toolConfirmation = this.
|
|
83
|
+
const toolConfirmation = this.aiConfigurationService.get<Record<string, ToolConfirmationMode>>(
|
|
81
84
|
TOOL_CONFIRMATION_PREFERENCE, {}
|
|
82
85
|
) ?? {};
|
|
83
86
|
if (toolId in toolConfirmation) {
|
|
@@ -99,19 +102,19 @@ export class ToolConfirmationManager {
|
|
|
99
102
|
* @param toolRequest - Optional ToolRequest to check for confirmAlwaysAllow flag
|
|
100
103
|
*/
|
|
101
104
|
setConfirmationMode(toolId: string, mode: ToolConfirmationMode, toolRequest?: ToolRequest): Promise<void> {
|
|
102
|
-
const current = this.
|
|
105
|
+
const current = this.aiConfigurationService.get<Record<string, ToolConfirmationMode>>(
|
|
103
106
|
TOOL_CONFIRMATION_PREFERENCE, {}
|
|
104
107
|
) ?? {};
|
|
105
108
|
const effectiveDefault = this.computeEffectiveDefaultForTool(toolId, toolRequest);
|
|
106
109
|
if (mode === effectiveDefault) {
|
|
107
110
|
if (toolId in current) {
|
|
108
111
|
const { [toolId]: _, ...rest } = current;
|
|
109
|
-
return this.
|
|
112
|
+
return this.aiConfigurationService.update(TOOL_CONFIRMATION_PREFERENCE, rest);
|
|
110
113
|
}
|
|
111
114
|
return Promise.resolve();
|
|
112
115
|
}
|
|
113
116
|
const updated = { ...current, [toolId]: mode };
|
|
114
|
-
return this.
|
|
117
|
+
return this.aiConfigurationService.update(TOOL_CONFIRMATION_PREFERENCE, updated);
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
@@ -122,13 +125,15 @@ export class ToolConfirmationManager {
|
|
|
122
125
|
* round-trip per tool.
|
|
123
126
|
*/
|
|
124
127
|
setConfirmationModes(updates: Iterable<{ toolId: string; mode: ToolConfirmationMode; toolRequest?: ToolRequest }>): Promise<void> {
|
|
125
|
-
const current = this.
|
|
128
|
+
const current = this.aiConfigurationService.get<Record<string, ToolConfirmationMode>>(
|
|
126
129
|
TOOL_CONFIRMATION_PREFERENCE, {}
|
|
127
130
|
) ?? {};
|
|
128
131
|
const next: Record<string, ToolConfirmationMode> = { ...current };
|
|
132
|
+
// Read the loop-invariant schema/global defaults once, not once per tool.
|
|
133
|
+
const defaults = this.readConfirmationDefaults();
|
|
129
134
|
let changed = false;
|
|
130
135
|
for (const { toolId, mode, toolRequest } of updates) {
|
|
131
|
-
const effectiveDefault = this.computeEffectiveDefaultForTool(toolId, toolRequest);
|
|
136
|
+
const effectiveDefault = this.computeEffectiveDefaultForTool(toolId, toolRequest, defaults);
|
|
132
137
|
if (mode === effectiveDefault) {
|
|
133
138
|
if (toolId in next) {
|
|
134
139
|
delete next[toolId];
|
|
@@ -143,7 +148,7 @@ export class ToolConfirmationManager {
|
|
|
143
148
|
if (!changed) {
|
|
144
149
|
return Promise.resolve();
|
|
145
150
|
}
|
|
146
|
-
return this.
|
|
151
|
+
return this.aiConfigurationService.update(TOOL_CONFIRMATION_PREFERENCE, next);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
/**
|
|
@@ -173,32 +178,39 @@ export class ToolConfirmationManager {
|
|
|
173
178
|
* Get all tool confirmation settings
|
|
174
179
|
*/
|
|
175
180
|
getAllConfirmationSettings(): { [toolId: string]: ToolConfirmationMode } {
|
|
176
|
-
return this.
|
|
181
|
+
return this.aiConfigurationService.get<Record<string, ToolConfirmationMode>>(
|
|
177
182
|
TOOL_CONFIRMATION_PREFERENCE, {}
|
|
178
183
|
) ?? {};
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
resetAllConfirmationModeSettings(): Promise<void> {
|
|
182
|
-
return this.
|
|
187
|
+
return this.aiConfigurationService.update(TOOL_CONFIRMATION_PREFERENCE, {});
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
191
|
+
* Read the loop-invariant inputs for {@link computeEffectiveDefaultForTool}: the product-shipped
|
|
192
|
+
* per-tool schema defaults and the effective global default.
|
|
188
193
|
*/
|
|
189
|
-
protected
|
|
190
|
-
const perToolDefaults = this.
|
|
194
|
+
protected readConfirmationDefaults(): ToolConfirmationDefaults {
|
|
195
|
+
const perToolDefaults = this.aiConfigurationService.inspect(TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as
|
|
191
196
|
| { [toolId: string]: ToolConfirmationMode }
|
|
192
197
|
| undefined;
|
|
193
|
-
|
|
198
|
+
return { perToolDefaults, globalDefault: this.getDefaultConfirmationMode() };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
protected computeEffectiveDefaultForTool(
|
|
202
|
+
toolId: string,
|
|
203
|
+
toolRequest?: ToolRequest,
|
|
204
|
+
defaults: ToolConfirmationDefaults = this.readConfirmationDefaults()
|
|
205
|
+
): ToolConfirmationMode {
|
|
206
|
+
const perToolDefault = defaults.perToolDefaults?.[toolId];
|
|
194
207
|
if (perToolDefault) {
|
|
195
208
|
return perToolDefault;
|
|
196
209
|
}
|
|
197
|
-
|
|
198
|
-
if (toolRequest?.confirmAlwaysAllow && globalDefault === ToolConfirmationMode.ALWAYS_ALLOW) {
|
|
210
|
+
if (toolRequest?.confirmAlwaysAllow && defaults.globalDefault === ToolConfirmationMode.ALWAYS_ALLOW) {
|
|
199
211
|
return ToolConfirmationMode.CONFIRM;
|
|
200
212
|
}
|
|
201
|
-
return globalDefault;
|
|
213
|
+
return defaults.globalDefault;
|
|
202
214
|
}
|
|
203
215
|
|
|
204
216
|
/**
|
|
@@ -206,7 +218,7 @@ export class ToolConfirmationManager {
|
|
|
206
218
|
* Falls back to CONFIRM if the preference service has not registered the schema yet.
|
|
207
219
|
*/
|
|
208
220
|
protected getDefaultPreferenceSchemaDefault(): ToolConfirmationMode {
|
|
209
|
-
const schemaDefault = this.
|
|
221
|
+
const schemaDefault = this.aiConfigurationService.inspect(DEFAULT_TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as
|
|
210
222
|
| ToolConfirmationMode
|
|
211
223
|
| undefined;
|
|
212
224
|
return schemaDefault ?? ToolConfirmationMode.CONFIRM;
|
|
@@ -17,9 +17,14 @@
|
|
|
17
17
|
import 'reflect-metadata';
|
|
18
18
|
|
|
19
19
|
import { expect } from 'chai';
|
|
20
|
-
import { LanguageModel, LanguageModelMessage, LanguageModelRequirement, LanguageModelResponse, LanguageModelService, ServerToolDescriptor, UserRequest } from '@theia/ai-core';
|
|
21
|
-
import { AbstractChatAgent, ChatAgentLocation } from './chat-agents';
|
|
22
20
|
import {
|
|
21
|
+
LanguageModel, LanguageModelMessage, LanguageModelRegistry, LanguageModelRequirement, LanguageModelResponse,
|
|
22
|
+
LanguageModelSelector, LanguageModelService, LanguageModelStreamResponsePart, ServerToolDescriptor, UserRequest
|
|
23
|
+
} from '@theia/ai-core';
|
|
24
|
+
import { AbstractChatAgent, AbstractStreamParsingChatAgent, ChatAgentLocation } from './chat-agents';
|
|
25
|
+
import {
|
|
26
|
+
ChatResponseContent,
|
|
27
|
+
CompactionChatResponseContent,
|
|
23
28
|
MutableChatModel,
|
|
24
29
|
MutableChatRequestModel,
|
|
25
30
|
ChatModel,
|
|
@@ -46,6 +51,14 @@ class TestChatAgent extends AbstractChatAgent {
|
|
|
46
51
|
public exposeSendLlmRequest(request: MutableChatRequestModel, languageModel: LanguageModel): Promise<LanguageModelResponse> {
|
|
47
52
|
return this.sendLlmRequest(request, [], [], undefined, languageModel);
|
|
48
53
|
}
|
|
54
|
+
|
|
55
|
+
public exposeGetLanguageModelForRequest(request: MutableChatRequestModel, purpose = 'chat'): Promise<LanguageModel> {
|
|
56
|
+
return this.getLanguageModelForRequest(request, purpose);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public setLanguageModelRegistry(registry: LanguageModelRegistry): void {
|
|
60
|
+
this.languageModelRegistry = registry;
|
|
61
|
+
}
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
function createParsedRequest(text: string, request?: Partial<ChatRequest>): ParsedChatRequest {
|
|
@@ -182,3 +195,100 @@ describe('AbstractChatAgent.sendLlmRequest server tools', () => {
|
|
|
182
195
|
expect(captured()!.serverTools).to.equal(undefined);
|
|
183
196
|
});
|
|
184
197
|
});
|
|
198
|
+
|
|
199
|
+
class StreamParsingTestChatAgent extends AbstractStreamParsingChatAgent {
|
|
200
|
+
readonly id = 'stream-test-agent';
|
|
201
|
+
readonly name = 'Stream Test Agent';
|
|
202
|
+
readonly languageModelRequirements: LanguageModelRequirement[] = [];
|
|
203
|
+
protected readonly defaultLanguageModelPurpose = 'chat';
|
|
204
|
+
|
|
205
|
+
exposeParse(token: LanguageModelStreamResponsePart): ChatResponseContent | ChatResponseContent[] {
|
|
206
|
+
return this.parse(token, undefined as never);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
describe('AbstractChatAgent.parse compaction', () => {
|
|
211
|
+
it('creates compaction content from a compaction response part', () => {
|
|
212
|
+
const agent = new StreamParsingTestChatAgent();
|
|
213
|
+
const content = agent.exposeParse({ compaction: { provider: 'anthropic', data: { b: 1 }, summary: 's' } });
|
|
214
|
+
expect(ChatResponseContent.is(content)).to.equal(true);
|
|
215
|
+
expect(CompactionChatResponseContent.is(content)).to.equal(true);
|
|
216
|
+
const compaction = content as CompactionChatResponseContent;
|
|
217
|
+
expect(compaction.provider).to.equal('anthropic');
|
|
218
|
+
expect(compaction.data).to.deep.equal({ b: 1 });
|
|
219
|
+
expect(compaction.summary).to.equal('s');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('AbstractChatAgent.getLanguageModelForRequest', () => {
|
|
224
|
+
|
|
225
|
+
const DEFAULT_MODEL = 'default-model';
|
|
226
|
+
const OVERRIDE_MODEL = 'override-model';
|
|
227
|
+
|
|
228
|
+
let agent: TestChatAgent;
|
|
229
|
+
let requestedIdentifiers: (string | undefined)[];
|
|
230
|
+
|
|
231
|
+
function fakeModel(id: string): LanguageModel {
|
|
232
|
+
return { id } as LanguageModel;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
beforeEach(() => {
|
|
236
|
+
agent = new TestChatAgent();
|
|
237
|
+
agent.languageModelRequirements.push({ purpose: 'chat', identifier: DEFAULT_MODEL });
|
|
238
|
+
requestedIdentifiers = [];
|
|
239
|
+
// Resolve any of the known model ids; anything else (e.g. an unavailable override) resolves to undefined.
|
|
240
|
+
const known = new Set([DEFAULT_MODEL, OVERRIDE_MODEL]);
|
|
241
|
+
agent.setLanguageModelRegistry({
|
|
242
|
+
// Settings-aware selection used for the agent default (fallback) path.
|
|
243
|
+
async selectLanguageModel(request: LanguageModelSelector): Promise<LanguageModel | undefined> {
|
|
244
|
+
requestedIdentifiers.push(request.identifier);
|
|
245
|
+
return request.identifier && known.has(request.identifier) ? fakeModel(request.identifier) : undefined;
|
|
246
|
+
},
|
|
247
|
+
// Direct resolution used for the per-session override path (bypasses agent settings).
|
|
248
|
+
async getReadyLanguageModel(idOrAlias: string): Promise<LanguageModel | undefined> {
|
|
249
|
+
requestedIdentifiers.push(idOrAlias);
|
|
250
|
+
return known.has(idOrAlias) ? fakeModel(idOrAlias) : undefined;
|
|
251
|
+
}
|
|
252
|
+
} as unknown as LanguageModelRegistry);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
function createRequest(modelOverride?: string): MutableChatRequestModel {
|
|
256
|
+
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
257
|
+
const request = model.addRequest(createParsedRequest('Hello'));
|
|
258
|
+
if (modelOverride !== undefined) {
|
|
259
|
+
model.setSettings({ commonSettings: { modelId: modelOverride } });
|
|
260
|
+
}
|
|
261
|
+
return request;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
it('uses the agent default when no session override is set', async () => {
|
|
265
|
+
const resolved = await agent.exposeGetLanguageModelForRequest(createRequest());
|
|
266
|
+
expect(resolved.id).to.equal(DEFAULT_MODEL);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('honors the session model override when it resolves', async () => {
|
|
270
|
+
const resolved = await agent.exposeGetLanguageModelForRequest(createRequest(OVERRIDE_MODEL));
|
|
271
|
+
expect(resolved.id).to.equal(OVERRIDE_MODEL);
|
|
272
|
+
// The override id must be the first thing tried.
|
|
273
|
+
expect(requestedIdentifiers[0]).to.equal(OVERRIDE_MODEL);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('falls back to the agent default when the session override does not resolve', async () => {
|
|
277
|
+
const resolved = await agent.exposeGetLanguageModelForRequest(createRequest('no-such-model'));
|
|
278
|
+
expect(resolved.id).to.equal(DEFAULT_MODEL);
|
|
279
|
+
// First the unavailable override is attempted, then the agent default.
|
|
280
|
+
expect(requestedIdentifiers).to.deep.equal(['no-such-model', DEFAULT_MODEL]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('throws when neither the override nor the default resolves', async () => {
|
|
284
|
+
agent.languageModelRequirements.length = 0;
|
|
285
|
+
agent.languageModelRequirements.push({ purpose: 'chat', identifier: 'missing-default' });
|
|
286
|
+
let error: Error | undefined;
|
|
287
|
+
try {
|
|
288
|
+
await agent.exposeGetLanguageModelForRequest(createRequest('also-missing'));
|
|
289
|
+
} catch (e) {
|
|
290
|
+
error = e as Error;
|
|
291
|
+
}
|
|
292
|
+
expect(error).to.be.an('error');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
CapabilityAwareContext,
|
|
28
28
|
GenericCapabilitySelections,
|
|
29
29
|
getTextOfResponse,
|
|
30
|
+
isCompactionResponsePart,
|
|
30
31
|
isLanguageModelStreamResponsePart,
|
|
31
32
|
isServerToolCallResponsePart,
|
|
32
33
|
isTextResponsePart,
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
} from '@theia/ai-core';
|
|
52
53
|
import {
|
|
53
54
|
Agent,
|
|
55
|
+
FrontendLanguageModelRegistry,
|
|
54
56
|
isLanguageModelParsedResponse,
|
|
55
57
|
isLanguageModelStreamResponse,
|
|
56
58
|
isLanguageModelTextResponse,
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
ChatRequestModel,
|
|
66
68
|
ChatResponseContent,
|
|
67
69
|
CommonChatSessionSettings,
|
|
70
|
+
CompactionChatResponseContentImpl,
|
|
68
71
|
ErrorChatResponseContentImpl,
|
|
69
72
|
MarkdownChatResponseContentImpl,
|
|
70
73
|
MutableChatRequestModel,
|
|
@@ -225,10 +228,12 @@ export abstract class AbstractChatAgent implements ChatAgent {
|
|
|
225
228
|
|
|
226
229
|
async invoke(request: MutableChatRequestModel): Promise<void> {
|
|
227
230
|
try {
|
|
228
|
-
const languageModel = await this.
|
|
231
|
+
const languageModel = await this.getLanguageModelForRequest(request, this.defaultLanguageModelPurpose);
|
|
229
232
|
if (!languageModel) {
|
|
230
233
|
throw new Error(nls.localize('theia/ai/chat/couldNotFindMatchingLM', 'Couldn\'t find a matching language model. Please check your setup!'));
|
|
231
234
|
}
|
|
235
|
+
// Record the model that actually handled this request so the chat thread can show it.
|
|
236
|
+
request.response.setLanguageModel(languageModel.id);
|
|
232
237
|
const context: ChatSessionContext = {
|
|
233
238
|
model: request.session,
|
|
234
239
|
request,
|
|
@@ -313,6 +318,36 @@ export abstract class AbstractChatAgent implements ChatAgent {
|
|
|
313
318
|
return this.selectLanguageModel(this.getLanguageModelSelector(languageModelPurpose));
|
|
314
319
|
}
|
|
315
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Resolves the language model for a request, honoring a per-session model override
|
|
323
|
+
* ({@link CommonChatSessionSettings.modelId}) when set and resolvable, and otherwise falling
|
|
324
|
+
* back to the agent's configured model for the given purpose.
|
|
325
|
+
*/
|
|
326
|
+
protected async getLanguageModelForRequest(request: MutableChatRequestModel, languageModelPurpose: string): Promise<LanguageModel> {
|
|
327
|
+
const overrideId = request.session.settings?.commonSettings?.modelId;
|
|
328
|
+
if (overrideId) {
|
|
329
|
+
// Resolve the override directly. We must not go through `selectLanguageModel` here, because
|
|
330
|
+
// that honors the per-agent model configured in the AI settings, which would take
|
|
331
|
+
// precedence over and thus ignore the session override.
|
|
332
|
+
const overridden = await this.resolveModelById(overrideId);
|
|
333
|
+
if (overridden) {
|
|
334
|
+
return overridden;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return this.getLanguageModel(languageModelPurpose);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Resolves a concrete language model (or alias) id to a ready model, or `undefined` if none is ready. */
|
|
341
|
+
protected async resolveModelById(modelId: string): Promise<LanguageModel | undefined> {
|
|
342
|
+
const registry = this.languageModelRegistry as LanguageModelRegistry & Partial<FrontendLanguageModelRegistry>;
|
|
343
|
+
if (typeof registry.getReadyLanguageModel === 'function') {
|
|
344
|
+
// Frontend registry: resolves aliases and only returns the model if it is ready.
|
|
345
|
+
return registry.getReadyLanguageModel(modelId);
|
|
346
|
+
}
|
|
347
|
+
const model = await registry.getLanguageModel(modelId);
|
|
348
|
+
return model?.status.status === 'ready' ? model : undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
316
351
|
protected async selectLanguageModel(selector: LanguageModelRequirement): Promise<LanguageModel> {
|
|
317
352
|
const languageModel = await this.languageModelRegistry.selectLanguageModel({ agent: this.id, ...selector });
|
|
318
353
|
if (!languageModel) {
|
|
@@ -547,6 +582,7 @@ export abstract class AbstractChatAgent implements ChatAgent {
|
|
|
547
582
|
serverTools: enabledServerTools.length > 0 ? enabledServerTools : undefined,
|
|
548
583
|
settings,
|
|
549
584
|
reasoning: commonSettings?.reasoning,
|
|
585
|
+
compaction: commonSettings?.compaction,
|
|
550
586
|
agentId: this.id,
|
|
551
587
|
sessionId: request.session.id,
|
|
552
588
|
requestId: request.id,
|
|
@@ -776,6 +812,13 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
|
|
|
776
812
|
request.response.setTokenUsage(this.mapUsageResponsePart(token));
|
|
777
813
|
return [];
|
|
778
814
|
}
|
|
815
|
+
if (isCompactionResponsePart(token)) {
|
|
816
|
+
return new CompactionChatResponseContentImpl(
|
|
817
|
+
token.compaction.provider,
|
|
818
|
+
token.compaction.data,
|
|
819
|
+
token.compaction.summary
|
|
820
|
+
);
|
|
821
|
+
}
|
|
779
822
|
return this.defaultContentFactory.create('', request);
|
|
780
823
|
}
|
|
781
824
|
|
|
@@ -25,7 +25,7 @@ import { ILogger } from '@theia/core';
|
|
|
25
25
|
import { ChatAgentLocation } from './chat-agents';
|
|
26
26
|
import { ChatContentDeserializerRegistry, ChatContentDeserializerRegistryImpl, DefaultChatContentDeserializerContribution } from './chat-content-deserializer';
|
|
27
27
|
import { ChangeSetElementDeserializerRegistry, ChangeSetElementDeserializerRegistryImpl } from './change-set-element-deserializer';
|
|
28
|
-
import { ChatModel } from './chat-model';
|
|
28
|
+
import { ChatModel, MutableChatModel } from './chat-model';
|
|
29
29
|
import { SerializedChatData } from './chat-model-serialization';
|
|
30
30
|
import { ParsedChatRequest, ParsedChatRequestTextPart } from './parsed-chat-request';
|
|
31
31
|
|
|
@@ -242,6 +242,23 @@ describe('Chat Auto-Save Mechanism', () => {
|
|
|
242
242
|
});
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
+
describe('Auto-save on settings changes', () => {
|
|
246
|
+
it('should auto-save when per-session settings change', async () => {
|
|
247
|
+
const session = chatService.createSession(ChatAgentLocation.Panel);
|
|
248
|
+
// Non-empty session so saveSession does not skip it.
|
|
249
|
+
await chatService.sendRequest(session.id, { text: 'Test request' });
|
|
250
|
+
sessionStore.reset();
|
|
251
|
+
|
|
252
|
+
// A selector-only / dialog-only settings change must trigger persistence.
|
|
253
|
+
(session.model as MutableChatModel).setSettings({ commonSettings: { modelId: 'test/model' } });
|
|
254
|
+
|
|
255
|
+
// Wait for auto-save to complete (debounce is 500ms + execution time)
|
|
256
|
+
await new Promise(resolve => setTimeout(resolve, 700));
|
|
257
|
+
|
|
258
|
+
expect(sessionStore.saveCount).to.be.greaterThan(0);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
245
262
|
describe('Auto-save for all sessions', () => {
|
|
246
263
|
it('should save all non-empty sessions', async () => {
|
|
247
264
|
const session1 = chatService.createSession(ChatAgentLocation.Panel);
|