@theia/ai-chat 1.74.0-next.9 → 1.74.1

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 (102) 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-request-command-resolution.spec.d.ts +2 -0
  6. package/lib/browser/chat-request-command-resolution.spec.d.ts.map +1 -0
  7. package/lib/browser/chat-request-command-resolution.spec.js +94 -0
  8. package/lib/browser/chat-request-command-resolution.spec.js.map +1 -0
  9. package/lib/browser/chat-session-store-impl.d.ts.map +1 -1
  10. package/lib/browser/chat-session-store-impl.js +2 -0
  11. package/lib/browser/chat-session-store-impl.js.map +1 -1
  12. package/lib/browser/chat-session-store-impl.spec.js +51 -2
  13. package/lib/browser/chat-session-store-impl.spec.js.map +1 -1
  14. package/lib/browser/chat-tool-preference-bindings.d.ts +18 -8
  15. package/lib/browser/chat-tool-preference-bindings.d.ts.map +1 -1
  16. package/lib/browser/chat-tool-preference-bindings.js +27 -28
  17. package/lib/browser/chat-tool-preference-bindings.js.map +1 -1
  18. package/lib/browser/chat-tool-preference-bindings.spec.js +43 -46
  19. package/lib/browser/chat-tool-preference-bindings.spec.js.map +1 -1
  20. package/lib/common/chat-agents.d.ts +8 -0
  21. package/lib/common/chat-agents.d.ts.map +1 -1
  22. package/lib/common/chat-agents.js +35 -1
  23. package/lib/common/chat-agents.js.map +1 -1
  24. package/lib/common/chat-agents.spec.js +94 -0
  25. package/lib/common/chat-agents.spec.js.map +1 -1
  26. package/lib/common/chat-auto-save.spec.js +13 -0
  27. package/lib/common/chat-auto-save.spec.js.map +1 -1
  28. package/lib/common/chat-content-deserializer.d.ts.map +1 -1
  29. package/lib/common/chat-content-deserializer.js +4 -0
  30. package/lib/common/chat-content-deserializer.js.map +1 -1
  31. package/lib/common/chat-model-serialization.d.ts +12 -1
  32. package/lib/common/chat-model-serialization.d.ts.map +1 -1
  33. package/lib/common/chat-model-serialization.js.map +1 -1
  34. package/lib/common/chat-model-serialization.spec.js +29 -0
  35. package/lib/common/chat-model-serialization.spec.js.map +1 -1
  36. package/lib/common/chat-model.d.ts +105 -2
  37. package/lib/common/chat-model.d.ts.map +1 -1
  38. package/lib/common/chat-model.js +159 -3
  39. package/lib/common/chat-model.js.map +1 -1
  40. package/lib/common/chat-model.spec.d.ts +2 -0
  41. package/lib/common/chat-model.spec.d.ts.map +1 -0
  42. package/lib/common/chat-model.spec.js +42 -0
  43. package/lib/common/chat-model.spec.js.map +1 -0
  44. package/lib/common/chat-request-parser.d.ts +9 -1
  45. package/lib/common/chat-request-parser.d.ts.map +1 -1
  46. package/lib/common/chat-request-parser.js +54 -3
  47. package/lib/common/chat-request-parser.js.map +1 -1
  48. package/lib/common/chat-request-parser.spec.js +333 -0
  49. package/lib/common/chat-request-parser.spec.js.map +1 -1
  50. package/lib/common/chat-response-model.spec.js +31 -0
  51. package/lib/common/chat-response-model.spec.js.map +1 -1
  52. package/lib/common/chat-service-deletion.spec.js +43 -0
  53. package/lib/common/chat-service-deletion.spec.js.map +1 -1
  54. package/lib/common/chat-service-pinned-agent.spec.js +52 -2
  55. package/lib/common/chat-service-pinned-agent.spec.js.map +1 -1
  56. package/lib/common/chat-service-session-events.spec.d.ts +2 -0
  57. package/lib/common/chat-service-session-events.spec.d.ts.map +1 -0
  58. package/lib/common/chat-service-session-events.spec.js +130 -0
  59. package/lib/common/chat-service-session-events.spec.js.map +1 -0
  60. package/lib/common/chat-service.d.ts +3 -0
  61. package/lib/common/chat-service.d.ts.map +1 -1
  62. package/lib/common/chat-service.js +60 -6
  63. package/lib/common/chat-service.js.map +1 -1
  64. package/lib/common/chat-service.spec.d.ts +2 -0
  65. package/lib/common/chat-service.spec.d.ts.map +1 -0
  66. package/lib/common/chat-service.spec.js +177 -0
  67. package/lib/common/chat-service.spec.js.map +1 -0
  68. package/lib/common/chat-session-status.spec.d.ts +2 -0
  69. package/lib/common/chat-session-status.spec.d.ts.map +1 -0
  70. package/lib/common/chat-session-status.spec.js +213 -0
  71. package/lib/common/chat-session-status.spec.js.map +1 -0
  72. package/lib/common/chat-session-store.d.ts +8 -0
  73. package/lib/common/chat-session-store.d.ts.map +1 -1
  74. package/lib/common/tool-call-response-content.spec.js +36 -0
  75. package/lib/common/tool-call-response-content.spec.js.map +1 -1
  76. package/package.json +11 -11
  77. package/src/browser/agent-delegation-tool.spec.ts +16 -0
  78. package/src/browser/agent-delegation-tool.ts +7 -7
  79. package/src/browser/chat-request-command-resolution.spec.ts +117 -0
  80. package/src/browser/chat-session-store-impl.spec.ts +62 -2
  81. package/src/browser/chat-session-store-impl.ts +2 -0
  82. package/src/browser/chat-tool-preference-bindings.spec.ts +43 -48
  83. package/src/browser/chat-tool-preference-bindings.ts +42 -30
  84. package/src/common/chat-agents.spec.ts +112 -2
  85. package/src/common/chat-agents.ts +44 -1
  86. package/src/common/chat-auto-save.spec.ts +18 -1
  87. package/src/common/chat-content-deserializer.ts +11 -0
  88. package/src/common/chat-model-serialization.spec.ts +38 -0
  89. package/src/common/chat-model-serialization.ts +12 -1
  90. package/src/common/chat-model.spec.ts +42 -0
  91. package/src/common/chat-model.ts +241 -3
  92. package/src/common/chat-request-parser.spec.ts +401 -1
  93. package/src/common/chat-request-parser.ts +60 -5
  94. package/src/common/chat-response-model.spec.ts +41 -1
  95. package/src/common/chat-service-deletion.spec.ts +52 -0
  96. package/src/common/chat-service-pinned-agent.spec.ts +58 -2
  97. package/src/common/chat-service-session-events.spec.ts +161 -0
  98. package/src/common/chat-service.spec.ts +211 -0
  99. package/src/common/chat-service.ts +64 -8
  100. package/src/common/chat-session-status.spec.ts +280 -0
  101. package/src/common/chat-session-store.ts +8 -0
  102. package/src/common/tool-call-response-content.spec.ts +42 -0
@@ -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
  }
@@ -2946,6 +3164,7 @@ class ChatResponseImpl implements ChatResponse {
2946
3164
  protected _content: ChatResponseContent[];
2947
3165
  protected _responseRepresentation: string;
2948
3166
  protected _responseRepresentationForDisplay: string;
3167
+ protected readonly contentChangeListeners = new Map<ChatResponseContent, Disposable>();
2949
3168
 
2950
3169
  constructor() {
2951
3170
  this._content = [];
@@ -2956,6 +3175,8 @@ class ChatResponseImpl implements ChatResponse {
2956
3175
  }
2957
3176
 
2958
3177
  clearContent(): void {
3178
+ this.contentChangeListeners.forEach(listener => listener.dispose());
3179
+ this.contentChangeListeners.clear();
2959
3180
  this._content = [];
2960
3181
  this._updateResponseRepresentation();
2961
3182
  this._onDidChangeEmitter.fire();
@@ -2991,7 +3212,11 @@ class ChatResponseImpl implements ChatResponse {
2991
3212
  // Forward content-level change events (e.g. partial-result updates from a
2992
3213
  // renderer) so auto-save can persist them. Without this, mutations that
2993
3214
  // don't go through addContent/merge are invisible to listeners.
2994
- nextContent.onDidChange(() => this._onDidChangeEmitter.fire());
3215
+ // The subscription is tracked so that clearContent() can dispose it: the stream
3216
+ // parser clears and re-adds the content per token, which would otherwise stack
3217
+ // up one listener per token on the same content object (#17858).
3218
+ this.contentChangeListeners.get(nextContent)?.dispose();
3219
+ this.contentChangeListeners.set(nextContent, nextContent.onDidChange(() => this._onDidChangeEmitter.fire()));
2995
3220
  }
2996
3221
  } else if (ServerToolCallChatResponseContent.is(nextContent) && nextContent.id !== undefined) {
2997
3222
  // Server tool calls are matched by id (the start and result blocks arrive as separate stream parts).
@@ -3084,6 +3309,7 @@ export class MutableChatResponseModel implements ChatResponseModel {
3084
3309
  protected _cancellationToken: CancellationTokenSource;
3085
3310
  protected _promptVariantId?: string;
3086
3311
  protected _isPromptVariantEdited?: boolean;
3312
+ protected _languageModel?: string;
3087
3313
  protected _tokenUsage?: ResponseTokenUsage;
3088
3314
  protected _tokenUsageEntries: ResponseTokenUsage[] = [];
3089
3315
 
@@ -3129,6 +3355,7 @@ export class MutableChatResponseModel implements ChatResponseModel {
3129
3355
  this._progressMessages = [];
3130
3356
  this._promptVariantId = data.promptVariantId;
3131
3357
  this._isPromptVariantEdited = data.isPromptVariantEdited ?? false;
3358
+ this._languageModel = data.languageModel;
3132
3359
  this._tokenUsage = data.tokenUsage;
3133
3360
 
3134
3361
  if (data.errorMessage) {
@@ -3209,6 +3436,10 @@ export class MutableChatResponseModel implements ChatResponseModel {
3209
3436
  return this._isPromptVariantEdited ?? false;
3210
3437
  }
3211
3438
 
3439
+ get languageModel(): string | undefined {
3440
+ return this._languageModel;
3441
+ }
3442
+
3212
3443
  get tokenUsage(): ResponseTokenUsage | undefined {
3213
3444
  return this._tokenUsage;
3214
3445
  }
@@ -3233,6 +3464,12 @@ export class MutableChatResponseModel implements ChatResponseModel {
3233
3464
  this._onDidChangeEmitter.fire();
3234
3465
  }
3235
3466
 
3467
+ /** Records the identifier of the language model that produced this response. */
3468
+ setLanguageModel(languageModel: string | undefined): void {
3469
+ this._languageModel = languageModel;
3470
+ this._onDidChangeEmitter.fire();
3471
+ }
3472
+
3236
3473
  overrideAgentId(agentId: string): void {
3237
3474
  this._agentId = agentId;
3238
3475
  }
@@ -3310,6 +3547,7 @@ export class MutableChatResponseModel implements ChatResponseModel {
3310
3547
  errorMessage: this.errorObject?.message,
3311
3548
  promptVariantId: this._promptVariantId,
3312
3549
  isPromptVariantEdited: this._isPromptVariantEdited,
3550
+ languageModel: this._languageModel,
3313
3551
  tokenUsage: this._tokenUsage,
3314
3552
  content: this.response.content.map(c => {
3315
3553
  const serialized = c.toSerializable?.();