@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.
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
@@ -19,6 +19,8 @@ import {
19
19
  ChatResponseContent,
20
20
  CodeChatResponseContentImpl,
21
21
  CommandChatResponseContentImpl,
22
+ CompactionChatResponseContentImpl,
23
+ CompactionContentData,
22
24
  ErrorChatResponseContentImpl,
23
25
  HorizontalLayoutChatResponseContentImpl,
24
26
  InformationalChatResponseContentImpl,
@@ -250,6 +252,15 @@ export class DefaultChatContentDeserializerContribution implements ChatContentDe
250
252
  )
251
253
  });
252
254
 
255
+ registry.register({
256
+ kind: 'compaction',
257
+ deserialize: (data: CompactionContentData) => new CompactionChatResponseContentImpl(
258
+ data.provider,
259
+ data.data,
260
+ data.summary
261
+ )
262
+ });
263
+
253
264
  registry.register({
254
265
  kind: 'markdownContent',
255
266
  deserialize: (data: MarkdownContentData) => new MarkdownChatResponseContentImpl(data.content)
@@ -700,4 +700,42 @@ describe('ChatModel Serialization and Restoration', () => {
700
700
  expect(restoredPart.resolution?.variable.description).to.equal('File variable');
701
701
  });
702
702
  });
703
+
704
+ describe('Session settings and response model serialization', () => {
705
+ it('should serialize and restore the per-session model override', () => {
706
+ const model = new MutableChatModel(ChatAgentLocation.Panel);
707
+ model.addRequest(createParsedRequest('Hello'));
708
+ model.setSettings({ commonSettings: { modelId: 'anthropic/claude-opus-4-8' } });
709
+
710
+ const serialized = model.toSerializable();
711
+ expect(serialized.settings?.commonSettings?.modelId).to.equal('anthropic/claude-opus-4-8');
712
+
713
+ const restored = new MutableChatModel(serialized);
714
+ expect(restored.settings?.commonSettings?.modelId).to.equal('anthropic/claude-opus-4-8');
715
+ });
716
+
717
+ it('should leave settings undefined when none were set', () => {
718
+ const model = new MutableChatModel(ChatAgentLocation.Panel);
719
+ model.addRequest(createParsedRequest('Hello'));
720
+
721
+ const serialized = model.toSerializable();
722
+ expect(serialized.settings).to.be.undefined;
723
+
724
+ const restored = new MutableChatModel(serialized);
725
+ expect(restored.settings).to.be.undefined;
726
+ });
727
+
728
+ it('should serialize and restore the language model recorded on a response', () => {
729
+ const model = new MutableChatModel(ChatAgentLocation.Panel);
730
+ const request = model.addRequest(createParsedRequest('Hello'));
731
+ request.response.setLanguageModel('anthropic/claude-opus-4-8');
732
+ request.response.complete();
733
+
734
+ const serialized = model.toSerializable();
735
+ expect(serialized.responses[0].languageModel).to.equal('anthropic/claude-opus-4-8');
736
+
737
+ const restored = new MutableChatModel(serialized);
738
+ expect(restored.getAllRequests()[0].response.languageModel).to.equal('anthropic/claude-opus-4-8');
739
+ });
740
+ });
703
741
  });
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { GenericCapabilitySelections } from '@theia/ai-core';
18
18
  import { ChatAgentLocation } from './chat-agents';
19
- import { ResponseTokenUsage } from './chat-model';
19
+ import { ChatSessionSettings, ResponseTokenUsage } from './chat-model';
20
20
 
21
21
  export interface SerializableChangeSetElement {
22
22
  kind?: string;
@@ -138,6 +138,8 @@ export interface SerializableChatResponseData {
138
138
  errorMessage?: string;
139
139
  promptVariantId?: string;
140
140
  isPromptVariantEdited?: boolean;
141
+ /** Identifier of the language model that produced this response, if recorded. */
142
+ languageModel?: string;
141
143
  tokenUsage?: ResponseTokenUsage;
142
144
  content: SerializableChatResponseContentData[];
143
145
  }
@@ -189,6 +191,11 @@ export interface SerializedChatModel {
189
191
  requests: SerializableChatRequestData[];
190
192
  /** All responses for the requests */
191
193
  responses: SerializableChatResponseData[];
194
+ /**
195
+ * Per-session settings (e.g. the per-session model override) so they survive a reload and the
196
+ * chat input can restore the correct selection.
197
+ */
198
+ settings?: ChatSessionSettings;
192
199
  }
193
200
 
194
201
  /**
@@ -201,6 +208,10 @@ export interface SerializedChatData {
201
208
  title?: string;
202
209
  model: SerializedChatModel;
203
210
  saveDate: number;
211
+ /** ID of the root session in the delegation chain. */
212
+ rootSessionId?: string;
213
+ /** ID of the immediate parent session that delegated this one. */
214
+ parentSessionId?: string;
204
215
  }
205
216
 
206
217
  export interface SerializableChatsData {
@@ -0,0 +1,42 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { CompactionChatResponseContentImpl, CompactionChatResponseContent } from './chat-model';
19
+
20
+ describe('CompactionChatResponseContent', () => {
21
+ it('round-trips through serialization', () => {
22
+ const original = new CompactionChatResponseContentImpl('anthropic', { block: 'opaque' }, 'summary text');
23
+ const serialized = original.toSerializable();
24
+ expect(serialized.kind).to.equal('compaction');
25
+ const restored = new CompactionChatResponseContentImpl(
26
+ serialized.data.provider, serialized.data.data, serialized.data.summary);
27
+ expect(restored.provider).to.equal('anthropic');
28
+ expect(restored.summary).to.equal('summary text');
29
+ expect(restored.data).to.deep.equal({ block: 'opaque' });
30
+ });
31
+ it('produces a compaction language model message', () => {
32
+ const content = new CompactionChatResponseContentImpl('openai-responses', { item: 'x' });
33
+ const message = content.toLanguageModelMessage();
34
+ expect(message).to.deep.equal({ actor: 'ai', type: 'compaction', provider: 'openai-responses', data: { item: 'x' }, summary: undefined });
35
+ });
36
+ it('is excluded from plain-text history but offers a display string', () => {
37
+ const content = new CompactionChatResponseContentImpl('anthropic', {}, 'sum');
38
+ expect(content.asString()).to.equal(undefined);
39
+ expect(content.asDisplayString()).to.equal('sum');
40
+ expect(CompactionChatResponseContent.is(content)).to.equal(true);
41
+ });
42
+ });
@@ -21,6 +21,8 @@
21
21
 
22
22
  import {
23
23
  AIVariableResolutionRequest,
24
+ CompactionMessage,
25
+ CompactionSettings,
24
26
  GenericCapabilitySelections,
25
27
  LanguageModelMessage,
26
28
  ReasoningSettings,
@@ -80,7 +82,9 @@ export type ChatChangeEvent =
80
82
  | ChatEditSubmitEvent
81
83
  | ChatResponseChangedEvent
82
84
  | ChatChangeHierarchyBranchEvent
83
- | ChatInteractionNeededEvent;
85
+ | ChatInteractionNeededEvent
86
+ | ChatSessionStatusChangedEvent
87
+ | ChatSettingsChangedEvent;
84
88
 
85
89
  export interface ChatAddRequestEvent {
86
90
  kind: 'addRequest';
@@ -142,6 +146,16 @@ export interface ChatInteractionNeededEvent {
142
146
  contentPart: InteractiveContent & ChatResponseContent;
143
147
  }
144
148
 
149
+ export interface ChatSessionStatusChangedEvent {
150
+ kind: 'statusChanged';
151
+ status: ChatSessionStatus;
152
+ }
153
+
154
+ export interface ChatSettingsChangedEvent {
155
+ kind: 'settingsChanged';
156
+ settings: ChatSessionSettings;
157
+ }
158
+
145
159
  export namespace ChatChangeEvent {
146
160
  export function isChangeSetEvent(event: ChatChangeEvent): event is ChatUpdateChangeSetEvent {
147
161
  return event.kind === 'updateChangeSet';
@@ -149,6 +163,9 @@ export namespace ChatChangeEvent {
149
163
  export function isInteractionNeededEvent(event: ChatChangeEvent): event is ChatInteractionNeededEvent {
150
164
  return event.kind === 'interactionNeeded';
151
165
  }
166
+ export function isStatusChangedEvent(event: ChatChangeEvent): event is ChatSessionStatusChangedEvent {
167
+ return event.kind === 'statusChanged';
168
+ }
152
169
  }
153
170
 
154
171
  export type ChatRequestRemovalReason = 'removal' | 'resend' | 'adoption';
@@ -222,10 +239,18 @@ export interface ChatHierarchyBranchItem<TRequest extends ChatRequestModel = Cha
222
239
  }
223
240
 
224
241
  export interface CommonChatSessionSettings {
242
+ /**
243
+ * Language model (or alias) id to use for this session only, overriding the agent's default.
244
+ * New sessions start without an override and use the agent's configured model. Cleared to
245
+ * revert to the default.
246
+ */
247
+ modelId?: string;
225
248
  /** Reasoning configuration for this session; applied to reasoning-capable models. */
226
249
  reasoning?: ReasoningSettings;
227
250
  /** Per-session tool confirmation timeout in seconds. Overrides the global preference when set. */
228
251
  confirmationTimeout?: number;
252
+ /** Per-session server-side compaction settings; set values win over the per-provider and global settings. */
253
+ compaction?: CompactionSettings;
229
254
  }
230
255
 
231
256
  export interface ChatSessionSettings {
@@ -243,6 +268,69 @@ export interface ChatSessionSettings {
243
268
  [key: string]: unknown;
244
269
  }
245
270
 
271
+ /**
272
+ * Aggregated, observable status of a chat session, derived from the state of the session's last request.
273
+ */
274
+ export type ChatSessionStatus =
275
+ /** No request is in progress. The last request, if any, completed successfully or was canceled. */
276
+ | 'idle'
277
+ /** A request is in progress. */
278
+ | 'running'
279
+ /** A tool call requires user confirmation before it can be executed. */
280
+ | 'awaitingApproval'
281
+ /** An approved or confirmation-free tool call is still executing. */
282
+ | 'awaitingToolCall'
283
+ /** The agent is waiting for user input, e.g. an answer to a structured question. */
284
+ | 'awaitingInput'
285
+ /** The last request ended in an error. */
286
+ | 'failed';
287
+
288
+ export namespace ChatSessionStatus {
289
+ /**
290
+ * Whether a request is in progress in this status, including the states waiting on the user or a tool.
291
+ */
292
+ export function isInProgress(status: ChatSessionStatus): boolean {
293
+ return status !== 'idle' && status !== 'failed';
294
+ }
295
+
296
+ /**
297
+ * Whether the session is blocked on the user, i.e. a tool approval or another input is required to proceed.
298
+ */
299
+ export function requiresUserAction(status: ChatSessionStatus): boolean {
300
+ return status === 'awaitingApproval' || status === 'awaitingInput';
301
+ }
302
+
303
+ /**
304
+ * Derives the session status from the state of the given request (usually the session's last request).
305
+ */
306
+ export function fromRequest(request: ChatRequestModel | undefined): ChatSessionStatus {
307
+ if (!request) {
308
+ return 'idle';
309
+ }
310
+ const response = request.response;
311
+ if (response.isError) {
312
+ return 'failed';
313
+ }
314
+ if (!ChatRequestModel.isInProgress(request)) {
315
+ return 'idle';
316
+ }
317
+ const content = response.response.content;
318
+ if (content.some(part => ToolCallChatResponseContent.is(part) && part.isAwaitingUserConfirmation)) {
319
+ return 'awaitingApproval';
320
+ }
321
+ // Unresolved non-tool-call interactive parts are structured questions (tool calls are
322
+ // "unresolved" while merely executing). The waiting-for-input flag additionally covers
323
+ // inputs without a dedicated content part, e.g. the user-interaction tool's wizard.
324
+ if (response.isWaitingForInput || content.some(part => !ToolCallChatResponseContent.is(part) && InteractiveContent.is(part) && !part.isResolved)) {
325
+ return 'awaitingInput';
326
+ }
327
+ if (content.some(part => ToolCallChatResponseContent.is(part) && !part.finished)) {
328
+ return 'awaitingToolCall';
329
+ }
330
+ return 'running';
331
+ }
332
+ }
333
+
246
334
  export interface ChatModel {
247
335
  readonly onDidChange: Event<ChatChangeEvent>;
248
336
  readonly id: string;
@@ -251,8 +339,15 @@ export interface ChatModel {
251
339
  readonly suggestions: readonly ChatSuggestion[];
252
340
  readonly settings?: ChatSessionSettings;
253
341
  readonly changeSet: ChangeSet;
342
+ /**
343
+ * Aggregated status of this session, derived from the state of its last request.
344
+ * Changes are announced via {@link onDidChange} with a {@link ChatSessionStatusChangedEvent}.
345
+ */
346
+ readonly status: ChatSessionStatus;
254
347
  /** ID of the root session in the delegation chain. For delegated sessions, this points to the topmost session where task contexts are stored. */
255
348
  rootSessionId?: string;
349
+ /** ID of the immediate parent session that delegated this one. Undefined for top-level sessions. */
350
+ parentSessionId?: string;
256
351
  getRequests(): ChatRequestModel[];
257
352
  getBranches(): ChatHierarchyBranch<ChatRequestModel>[];
258
353
  isEmpty(): boolean;
@@ -477,6 +572,12 @@ export interface ThinkingContentData {
477
572
  signature: string;
478
573
  }
479
574
 
575
+ export interface CompactionContentData {
576
+ provider: string;
577
+ data: unknown;
578
+ summary?: string;
579
+ }
580
+
480
581
  export interface MarkdownContentData {
481
582
  content: string;
482
583
  }
@@ -581,6 +682,11 @@ export interface ToolCallChatResponseContent extends Required<ChatResponseConten
581
682
  confirmed: Promise<boolean>;
582
683
  /** Resolves when the tool call requires user confirmation (show Allow/Deny UI). */
583
684
  needsUserConfirmation: Promise<void>;
685
+ /**
686
+ * Whether the tool call is currently awaiting the user's confirmation decision, i.e.
687
+ * {@link requestUserConfirmation} was called and the user has neither confirmed nor denied it yet.
688
+ */
689
+ readonly isAwaitingUserConfirmation: boolean;
584
690
  whenFinished: Promise<void>;
585
691
  /**
586
692
  * Provider-specific metadata about the tool call that the language model needs back on
@@ -869,6 +975,18 @@ export namespace ThinkingChatResponseContent {
869
975
  }
870
976
  }
871
977
 
978
+ export interface CompactionChatResponseContent extends ChatResponseContent {
979
+ kind: 'compaction';
980
+ provider: string;
981
+ data: unknown;
982
+ summary?: string;
983
+ }
984
+ export namespace CompactionChatResponseContent {
985
+ export function is(obj: unknown): obj is CompactionChatResponseContent {
986
+ return ChatResponseContent.is(obj) && obj.kind === 'compaction';
987
+ }
988
+ }
989
+
872
990
  export namespace ProgressChatResponseContent {
873
991
  export function is(obj: unknown): obj is ProgressChatResponseContent {
874
992
  return (
@@ -1008,6 +1126,10 @@ export interface ChatResponseModel {
1008
1126
  * Indicates whether the prompt variant was customized/edited
1009
1127
  */
1010
1128
  readonly isPromptVariantEdited?: boolean;
1129
+ /**
1130
+ * The identifier of the language model that produced this response, if recorded.
1131
+ */
1132
+ readonly languageModel?: string;
1011
1133
  readonly tokenUsage?: ResponseTokenUsage;
1012
1134
  toSerializable(): SerializableChatResponseData;
1013
1135
  }
@@ -1029,7 +1151,9 @@ export class MutableChatModel implements ChatModel, Disposable {
1029
1151
  protected _changeSet: ChatTreeChangeSet;
1030
1152
  protected _settings: ChatSessionSettings;
1031
1153
  protected _location: ChatAgentLocation;
1154
+ protected _status: ChatSessionStatus = 'idle';
1032
1155
  rootSessionId?: string;
1156
+ parentSessionId?: string;
1033
1157
 
1034
1158
  get location(): ChatAgentLocation {
1035
1159
  return this._location;
@@ -1060,7 +1184,14 @@ export class MutableChatModel implements ChatModel, Disposable {
1060
1184
  branch: event.branch,
1061
1185
  });
1062
1186
  }),
1187
+ // Re-derive the aggregated session status whenever anything in the model changes.
1188
+ this.onDidChange(event => {
1189
+ if (!ChatChangeEvent.isStatusChangedEvent(event)) {
1190
+ this.updateStatus();
1191
+ }
1192
+ }),
1063
1193
  ]);
1194
+ this._status = this.computeStatus();
1064
1195
  }
1065
1196
 
1066
1197
  /**
@@ -1097,6 +1228,12 @@ export class MutableChatModel implements ChatModel, Disposable {
1097
1228
  }, this, this.toDispose);
1098
1229
  }
1099
1230
 
1231
+ // Restore per-session settings (e.g. the per-session model override) so the chat input can
1232
+ // reflect the previous selection.
1233
+ if (data.settings) {
1234
+ this._settings = data.settings;
1235
+ }
1236
+
1100
1237
  // Restore the hierarchy structure with all alternatives
1101
1238
  this._hierarchy = new ChatRequestHierarchyImpl<MutableChatRequestModel>(data.hierarchy, requestMap);
1102
1239
 
@@ -1138,6 +1275,22 @@ export class MutableChatModel implements ChatModel, Disposable {
1138
1275
  return this._suggestions;
1139
1276
  }
1140
1277
 
1278
+ get status(): ChatSessionStatus {
1279
+ return this._status;
1280
+ }
1281
+
1282
+ protected computeStatus(): ChatSessionStatus {
1283
+ return ChatSessionStatus.fromRequest(this.getRequests().at(-1));
1284
+ }
1285
+
1286
+ protected updateStatus(): void {
1287
+ const status = this.computeStatus();
1288
+ if (status !== this._status) {
1289
+ this._status = status;
1290
+ this._onDidChangeEmitter.fire({ kind: 'statusChanged', status });
1291
+ }
1292
+ }
1293
+
1141
1294
  get context(): ChatContextManager {
1142
1295
  return this._contextManager;
1143
1296
  }
@@ -1148,6 +1301,9 @@ export class MutableChatModel implements ChatModel, Disposable {
1148
1301
 
1149
1302
  setSettings(settings: ChatSessionSettings): void {
1150
1303
  this._settings = settings;
1304
+ // Emit a change so listeners (e.g. session auto-save) persist selector-only or dialog-only
1305
+ // settings updates that are not accompanied by another model change.
1306
+ this._onDidChangeEmitter.fire({ kind: 'settingsChanged', settings });
1151
1307
  }
1152
1308
 
1153
1309
  addChildModel(child: MutableChatModel): Disposable {
@@ -1219,7 +1375,8 @@ export class MutableChatModel implements ChatModel, Disposable {
1219
1375
  location: this.location,
1220
1376
  hierarchy,
1221
1377
  requests: serializedRequests,
1222
- responses: serializedResponses
1378
+ responses: serializedResponses,
1379
+ settings: this._settings
1223
1380
  };
1224
1381
  }
1225
1382
 
@@ -2242,6 +2399,58 @@ export class ThinkingChatResponseContentImpl implements ThinkingChatResponseCont
2242
2399
  }
2243
2400
  }
2244
2401
 
2402
+ export class CompactionChatResponseContentImpl implements CompactionChatResponseContent {
2403
+ readonly kind = 'compaction';
2404
+ protected _provider: string;
2405
+ protected _data: unknown;
2406
+ protected _summary?: string;
2407
+
2408
+ constructor(provider: string, data: unknown, summary?: string) {
2409
+ this._provider = provider;
2410
+ this._data = data;
2411
+ this._summary = summary;
2412
+ }
2413
+
2414
+ get provider(): string {
2415
+ return this._provider;
2416
+ }
2417
+ get data(): unknown {
2418
+ return this._data;
2419
+ }
2420
+ get summary(): string | undefined {
2421
+ return this._summary;
2422
+ }
2423
+
2424
+ asString(): string | undefined {
2425
+ return undefined;
2426
+ }
2427
+
2428
+ asDisplayString(): string | undefined {
2429
+ return this._summary;
2430
+ }
2431
+
2432
+ toLanguageModelMessage(): CompactionMessage {
2433
+ return {
2434
+ actor: 'ai',
2435
+ type: 'compaction',
2436
+ provider: this._provider,
2437
+ data: this._data,
2438
+ summary: this._summary
2439
+ };
2440
+ }
2441
+
2442
+ toSerializable(): SerializableChatResponseContentData<CompactionContentData> {
2443
+ return {
2444
+ kind: 'compaction',
2445
+ data: {
2446
+ provider: this._provider,
2447
+ data: this._data,
2448
+ summary: this._summary
2449
+ }
2450
+ };
2451
+ }
2452
+ }
2453
+
2245
2454
  export class MarkdownChatResponseContentImpl implements MarkdownChatResponseContent {
2246
2455
  readonly kind = 'markdownContent';
2247
2456
  protected _content: MarkdownStringImpl = new MarkdownStringImpl();
@@ -2368,6 +2577,7 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
2368
2577
  protected _confirmationTimeout?: number;
2369
2578
  protected _needsUserConfirmation: Promise<void>;
2370
2579
  protected _needsUserConfirmationResolver?: () => void;
2580
+ protected _isAwaitingUserConfirmation = false;
2371
2581
  protected _confirmed: Promise<boolean>;
2372
2582
  protected _confirmationResolver?: (value: boolean) => void;
2373
2583
  protected _confirmationRejecter?: (reason?: unknown) => void;
@@ -2445,6 +2655,10 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
2445
2655
  return this._needsUserConfirmation;
2446
2656
  }
2447
2657
 
2658
+ get isAwaitingUserConfirmation(): boolean {
2659
+ return this._isAwaitingUserConfirmation && !this.finished;
2660
+ }
2661
+
2448
2662
  get whenFinished(): Promise<void> {
2449
2663
  return this._whenFinished;
2450
2664
  }
@@ -2479,12 +2693,14 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
2479
2693
  * Confirm the tool execution
2480
2694
  */
2481
2695
  confirm(): void {
2696
+ this._isAwaitingUserConfirmation = false;
2482
2697
  if (this._confirmationResolver) {
2483
2698
  this._confirmationResolver(true);
2484
2699
  }
2485
2700
  }
2486
2701
 
2487
2702
  deny(reason?: string): void {
2703
+ this._isAwaitingUserConfirmation = false;
2488
2704
  if (this._confirmationResolver) {
2489
2705
  this._finished = true;
2490
2706
  this._result = { denied: true, reason };
@@ -2494,6 +2710,7 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
2494
2710
  }
2495
2711
 
2496
2712
  requestUserConfirmation(): void {
2713
+ this._isAwaitingUserConfirmation = true;
2497
2714
  if (this._needsUserConfirmationResolver) {
2498
2715
  this._needsUserConfirmationResolver();
2499
2716
  this._needsUserConfirmationResolver = undefined;
@@ -2506,6 +2723,7 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
2506
2723
  }
2507
2724
 
2508
2725
  cancelConfirmation(reason?: unknown): void {
2726
+ this._isAwaitingUserConfirmation = false;
2509
2727
  if (this._confirmationRejecter) {
2510
2728
  this._confirmationRejecter(reason);
2511
2729
  }
@@ -3084,6 +3302,7 @@ export class MutableChatResponseModel implements ChatResponseModel {
3084
3302
  protected _cancellationToken: CancellationTokenSource;
3085
3303
  protected _promptVariantId?: string;
3086
3304
  protected _isPromptVariantEdited?: boolean;
3305
+ protected _languageModel?: string;
3087
3306
  protected _tokenUsage?: ResponseTokenUsage;
3088
3307
  protected _tokenUsageEntries: ResponseTokenUsage[] = [];
3089
3308
 
@@ -3129,6 +3348,7 @@ export class MutableChatResponseModel implements ChatResponseModel {
3129
3348
  this._progressMessages = [];
3130
3349
  this._promptVariantId = data.promptVariantId;
3131
3350
  this._isPromptVariantEdited = data.isPromptVariantEdited ?? false;
3351
+ this._languageModel = data.languageModel;
3132
3352
  this._tokenUsage = data.tokenUsage;
3133
3353
 
3134
3354
  if (data.errorMessage) {
@@ -3209,6 +3429,10 @@ export class MutableChatResponseModel implements ChatResponseModel {
3209
3429
  return this._isPromptVariantEdited ?? false;
3210
3430
  }
3211
3431
 
3432
+ get languageModel(): string | undefined {
3433
+ return this._languageModel;
3434
+ }
3435
+
3212
3436
  get tokenUsage(): ResponseTokenUsage | undefined {
3213
3437
  return this._tokenUsage;
3214
3438
  }
@@ -3233,6 +3457,12 @@ export class MutableChatResponseModel implements ChatResponseModel {
3233
3457
  this._onDidChangeEmitter.fire();
3234
3458
  }
3235
3459
 
3460
+ /** Records the identifier of the language model that produced this response. */
3461
+ setLanguageModel(languageModel: string | undefined): void {
3462
+ this._languageModel = languageModel;
3463
+ this._onDidChangeEmitter.fire();
3464
+ }
3465
+
3236
3466
  overrideAgentId(agentId: string): void {
3237
3467
  this._agentId = agentId;
3238
3468
  }
@@ -3310,6 +3540,7 @@ export class MutableChatResponseModel implements ChatResponseModel {
3310
3540
  errorMessage: this.errorObject?.message,
3311
3541
  promptVariantId: this._promptVariantId,
3312
3542
  isPromptVariantEdited: this._isPromptVariantEdited,
3543
+ languageModel: this._languageModel,
3313
3544
  tokenUsage: this._tokenUsage,
3314
3545
  content: this.response.content.map(c => {
3315
3546
  const serialized = c.toSerializable?.();
@@ -20,7 +20,7 @@ import { ChatRequestParserImpl } from './chat-request-parser';
20
20
  import { ChatAgent, ChatAgentLocation } from './chat-agents';
21
21
  import { ChatContext, ChatRequest } from './chat-model';
22
22
  import { expect } from 'chai';
23
- import { AIVariable, DefaultAIVariableService, ResolvedAIVariable, ToolInvocationRegistryImpl, ToolRequest } from '@theia/ai-core';
23
+ import { AIVariable, DefaultAIVariableService, PromptService, ResolvedAIVariable, ToolInvocationRegistryImpl, ToolRequest } from '@theia/ai-core';
24
24
  import { ILogger, Logger } from '@theia/core';
25
25
  import { ParsedChatRequestAgentPart, ParsedChatRequestFunctionPart, ParsedChatRequestTextPart, ParsedChatRequestVariablePart } from './parsed-chat-request';
26
26
  import { AgentDelegationTool } from '../browser/agent-delegation-tool';
@@ -276,6 +276,74 @@ describe('ChatRequestParserImpl', () => {
276
276
  expect(varPart.variableArg).to.equal('cmd|"arg with \\"quote\\"" other');
277
277
  });
278
278
 
279
+ it('parses multiple commands in one message', async () => {
280
+ const req: ChatRequest = {
281
+ text: '/skill-one /skill-two'
282
+ };
283
+ const context: ChatContext = { variables: [] };
284
+ const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
285
+
286
+ expect(result.parts.length).to.equal(3);
287
+ const firstCommand = result.parts[0] as ParsedChatRequestVariablePart;
288
+ const separator = result.parts[1] as ParsedChatRequestTextPart;
289
+ const secondCommand = result.parts[2] as ParsedChatRequestVariablePart;
290
+ expect(firstCommand.variableName).to.equal('prompt');
291
+ expect(firstCommand.variableArg).to.equal('skill-one');
292
+ expect(separator.text).to.equal(' ');
293
+ expect(secondCommand.variableName).to.equal('prompt');
294
+ expect(secondCommand.variableArg).to.equal('skill-two');
295
+ });
296
+
297
+ it('inserts a separator between two argument-taking commands', async () => {
298
+ const req: ChatRequest = {
299
+ text: '/summarize foo /hello bar'
300
+ };
301
+ const context: ChatContext = { variables: [] };
302
+ const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
303
+
304
+ expect(result.parts.length).to.equal(3);
305
+ const firstCommand = result.parts[0] as ParsedChatRequestVariablePart;
306
+ const separator = result.parts[1] as ParsedChatRequestTextPart;
307
+ const secondCommand = result.parts[2] as ParsedChatRequestVariablePart;
308
+ expect(firstCommand.variableArg).to.equal('summarize|foo');
309
+ expect(separator.kind).to.equal('text');
310
+ expect(separator.text).to.equal(' ');
311
+ expect(secondCommand.variableArg).to.equal('hello|bar');
312
+ });
313
+
314
+ it('keeps path-like slash arguments as command arguments', async () => {
315
+ const req: ChatRequest = {
316
+ text: '/explain /path/to/file'
317
+ };
318
+ const context: ChatContext = { variables: [] };
319
+ const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
320
+
321
+ expect(result.parts.length).to.equal(1);
322
+ const varPart = result.parts[0] as ParsedChatRequestVariablePart;
323
+ expect(varPart.variableName).to.equal('prompt');
324
+ expect(varPart.variableArg).to.equal('explain|/path/to/file');
325
+ });
326
+
327
+ it('uses known command names to disambiguate slash command arguments', async () => {
328
+ const promptService = {
329
+ getCommands: () => [
330
+ { id: 'command-skill-one', template: '', isCommand: true, commandName: 'skill-one' },
331
+ { id: 'command-skill-two', template: '', isCommand: true, commandName: 'skill-two' },
332
+ ]
333
+ } as unknown as PromptService;
334
+ const parserWithPromptService = new ChatRequestParserImpl(chatAgentService, variableService, toolInvocationRegistry, logger);
335
+ (parserWithPromptService as unknown as { promptService: PromptService }).promptService = promptService;
336
+ const context: ChatContext = { variables: [] };
337
+
338
+ const multipleCommands = await parserWithPromptService.parseChatRequest({ text: '/skill-one /skill-two' }, ChatAgentLocation.Panel, context);
339
+ expect((multipleCommands.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('skill-one');
340
+ expect((multipleCommands.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('skill-two');
341
+
342
+ const pathArgument = await parserWithPromptService.parseChatRequest({ text: '/skill-one /tmp' }, ChatAgentLocation.Panel, context);
343
+ expect(pathArgument.parts.length).to.equal(1);
344
+ expect((pathArgument.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('skill-one|/tmp');
345
+ });
346
+
279
347
  it('treats the first @agent mention as the selector and does not allow later mentions to override it', async () => {
280
348
  const createAgent = (id: string): ChatAgent => ({
281
349
  id,