@theia/ai-chat 1.74.0-next.8 → 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.
Files changed (94) hide show
  1. package/lib/browser/agent-delegation-tool.js +6 -7
  2. package/lib/browser/agent-delegation-tool.js.map +1 -1
  3. package/lib/browser/agent-delegation-tool.spec.js +13 -0
  4. package/lib/browser/agent-delegation-tool.spec.js.map +1 -1
  5. package/lib/browser/chat-session-store-impl.d.ts.map +1 -1
  6. package/lib/browser/chat-session-store-impl.js +2 -0
  7. package/lib/browser/chat-session-store-impl.js.map +1 -1
  8. package/lib/browser/chat-session-store-impl.spec.js +51 -2
  9. package/lib/browser/chat-session-store-impl.spec.js.map +1 -1
  10. package/lib/browser/chat-tool-preference-bindings.d.ts +18 -8
  11. package/lib/browser/chat-tool-preference-bindings.d.ts.map +1 -1
  12. package/lib/browser/chat-tool-preference-bindings.js +27 -28
  13. package/lib/browser/chat-tool-preference-bindings.js.map +1 -1
  14. package/lib/browser/chat-tool-preference-bindings.spec.js +43 -46
  15. package/lib/browser/chat-tool-preference-bindings.spec.js.map +1 -1
  16. package/lib/common/chat-agents.d.ts +8 -0
  17. package/lib/common/chat-agents.d.ts.map +1 -1
  18. package/lib/common/chat-agents.js +35 -1
  19. package/lib/common/chat-agents.js.map +1 -1
  20. package/lib/common/chat-agents.spec.js +94 -0
  21. package/lib/common/chat-agents.spec.js.map +1 -1
  22. package/lib/common/chat-auto-save.spec.js +13 -0
  23. package/lib/common/chat-auto-save.spec.js.map +1 -1
  24. package/lib/common/chat-content-deserializer.d.ts.map +1 -1
  25. package/lib/common/chat-content-deserializer.js +4 -0
  26. package/lib/common/chat-content-deserializer.js.map +1 -1
  27. package/lib/common/chat-model-serialization.d.ts +12 -1
  28. package/lib/common/chat-model-serialization.d.ts.map +1 -1
  29. package/lib/common/chat-model-serialization.js.map +1 -1
  30. package/lib/common/chat-model-serialization.spec.js +29 -0
  31. package/lib/common/chat-model-serialization.spec.js.map +1 -1
  32. package/lib/common/chat-model.d.ts +104 -2
  33. package/lib/common/chat-model.d.ts.map +1 -1
  34. package/lib/common/chat-model.js +151 -2
  35. package/lib/common/chat-model.js.map +1 -1
  36. package/lib/common/chat-model.spec.d.ts +2 -0
  37. package/lib/common/chat-model.spec.d.ts.map +1 -0
  38. package/lib/common/chat-model.spec.js +42 -0
  39. package/lib/common/chat-model.spec.js.map +1 -0
  40. package/lib/common/chat-request-parser.d.ts +4 -1
  41. package/lib/common/chat-request-parser.d.ts.map +1 -1
  42. package/lib/common/chat-request-parser.js +43 -3
  43. package/lib/common/chat-request-parser.js.map +1 -1
  44. package/lib/common/chat-request-parser.spec.js +59 -0
  45. package/lib/common/chat-request-parser.spec.js.map +1 -1
  46. package/lib/common/chat-service-deletion.spec.js +43 -0
  47. package/lib/common/chat-service-deletion.spec.js.map +1 -1
  48. package/lib/common/chat-service-pinned-agent.spec.js +52 -2
  49. package/lib/common/chat-service-pinned-agent.spec.js.map +1 -1
  50. package/lib/common/chat-service-session-events.spec.d.ts +2 -0
  51. package/lib/common/chat-service-session-events.spec.d.ts.map +1 -0
  52. package/lib/common/chat-service-session-events.spec.js +130 -0
  53. package/lib/common/chat-service-session-events.spec.js.map +1 -0
  54. package/lib/common/chat-service.d.ts +3 -0
  55. package/lib/common/chat-service.d.ts.map +1 -1
  56. package/lib/common/chat-service.js +60 -6
  57. package/lib/common/chat-service.js.map +1 -1
  58. package/lib/common/chat-service.spec.d.ts +2 -0
  59. package/lib/common/chat-service.spec.d.ts.map +1 -0
  60. package/lib/common/chat-service.spec.js +177 -0
  61. package/lib/common/chat-service.spec.js.map +1 -0
  62. package/lib/common/chat-session-status.spec.d.ts +2 -0
  63. package/lib/common/chat-session-status.spec.d.ts.map +1 -0
  64. package/lib/common/chat-session-status.spec.js +213 -0
  65. package/lib/common/chat-session-status.spec.js.map +1 -0
  66. package/lib/common/chat-session-store.d.ts +8 -0
  67. package/lib/common/chat-session-store.d.ts.map +1 -1
  68. package/lib/common/tool-call-response-content.spec.js +36 -0
  69. package/lib/common/tool-call-response-content.spec.js.map +1 -1
  70. package/package.json +11 -11
  71. package/src/browser/agent-delegation-tool.spec.ts +16 -0
  72. package/src/browser/agent-delegation-tool.ts +7 -7
  73. package/src/browser/chat-session-store-impl.spec.ts +62 -2
  74. package/src/browser/chat-session-store-impl.ts +2 -0
  75. package/src/browser/chat-tool-preference-bindings.spec.ts +43 -48
  76. package/src/browser/chat-tool-preference-bindings.ts +42 -30
  77. package/src/common/chat-agents.spec.ts +112 -2
  78. package/src/common/chat-agents.ts +44 -1
  79. package/src/common/chat-auto-save.spec.ts +18 -1
  80. package/src/common/chat-content-deserializer.ts +11 -0
  81. package/src/common/chat-model-serialization.spec.ts +38 -0
  82. package/src/common/chat-model-serialization.ts +12 -1
  83. package/src/common/chat-model.spec.ts +42 -0
  84. package/src/common/chat-model.ts +233 -2
  85. package/src/common/chat-request-parser.spec.ts +69 -1
  86. package/src/common/chat-request-parser.ts +51 -6
  87. package/src/common/chat-service-deletion.spec.ts +52 -0
  88. package/src/common/chat-service-pinned-agent.spec.ts +58 -2
  89. package/src/common/chat-service-session-events.spec.ts +161 -0
  90. package/src/common/chat-service.spec.ts +211 -0
  91. package/src/common/chat-service.ts +64 -8
  92. package/src/common/chat-session-status.spec.ts +280 -0
  93. package/src/common/chat-session-store.ts +8 -0
  94. 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 preferenceServiceMock: sinon.SinonStubbedInstance<PreferenceService>;
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
- preferenceServiceMock = {
62
- updateValue: sinon.stub().callsFake((key: string, value: unknown) => {
63
- if (key === TOOL_CONFIRMATION_PREFERENCE) {
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<TrustAwarePreferenceReader>;
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(PreferenceService).toConstantValue(preferenceServiceMock as unknown as PreferenceService);
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
128
- expect(preferenceServiceMock.updateValue.firstCall.args[0]).to.equal(DEFAULT_TOOL_CONFIRMATION_PREFERENCE);
129
- expect(preferenceServiceMock.updateValue.firstCall.args[1]).to.equal(ToolConfirmationMode.ALWAYS_ALLOW);
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.called).to.be.false;
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(preferenceServiceMock.updateValue.called).to.be.false;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.called).to.be.false;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.called).to.be.false;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
366
- expect(preferenceServiceMock.updateValue.firstCall.args[0]).to.equal(TOOL_CONFIRMATION_PREFERENCE);
367
- expect(preferenceServiceMock.updateValue.firstCall.args[1]).to.deep.equal({});
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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(preferenceServiceMock.updateValue.calledOnce).to.be.true;
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
- import { TrustAwarePreferenceReader } from '@theia/ai-core/lib/browser/trust-aware-preference-reader';
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(PreferenceService)
35
- protected readonly preferenceService: PreferenceService;
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.trustAwareReader.get<ToolConfirmationMode>(DEFAULT_TOOL_CONFIRMATION_PREFERENCE);
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.preferenceService.updateValue(DEFAULT_TOOL_CONFIRMATION_PREFERENCE, mode);
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.trustAwareReader.get<Record<string, ToolConfirmationMode>>(
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.trustAwareReader.get<Record<string, ToolConfirmationMode>>(
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.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, rest);
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.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, updated);
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.trustAwareReader.get<Record<string, ToolConfirmationMode>>(
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.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, next);
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.trustAwareReader.get<Record<string, ToolConfirmationMode>>(
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.preferenceService.updateValue(TOOL_CONFIRMATION_PREFERENCE, {});
187
+ return this.aiConfigurationService.update(TOOL_CONFIRMATION_PREFERENCE, {});
183
188
  }
184
189
 
185
190
  /**
186
- * Compute the effective default for a given tool, taking the schema-level default,
187
- * any product-shipped per-tool default, and the confirmAlwaysAllow flag into account.
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 computeEffectiveDefaultForTool(toolId: string, toolRequest?: ToolRequest): ToolConfirmationMode {
190
- const perToolDefaults = this.preferenceService.inspect(TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as
194
+ protected readConfirmationDefaults(): ToolConfirmationDefaults {
195
+ const perToolDefaults = this.aiConfigurationService.inspect(TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as
191
196
  | { [toolId: string]: ToolConfirmationMode }
192
197
  | undefined;
193
- const perToolDefault = perToolDefaults?.[toolId];
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
- const globalDefault = this.getDefaultConfirmationMode();
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.preferenceService.inspect(DEFAULT_TOOL_CONFIRMATION_PREFERENCE)?.defaultValue as
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.getLanguageModel(this.defaultLanguageModelPurpose);
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);