@theia/ai-chat-ui 1.76.0-next.7 → 1.76.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 (134) hide show
  1. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-ui-frontend-module.js +6 -0
  3. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  4. package/lib/browser/chat-find/chat-find-contribution.d.ts +18 -0
  5. package/lib/browser/chat-find/chat-find-contribution.d.ts.map +1 -0
  6. package/lib/browser/chat-find/chat-find-contribution.js +76 -0
  7. package/lib/browser/chat-find/chat-find-contribution.js.map +1 -0
  8. package/lib/browser/chat-find/chat-find-highlighter.d.ts +114 -0
  9. package/lib/browser/chat-find/chat-find-highlighter.d.ts.map +1 -0
  10. package/lib/browser/chat-find/chat-find-highlighter.js +353 -0
  11. package/lib/browser/chat-find/chat-find-highlighter.js.map +1 -0
  12. package/lib/browser/chat-find/chat-find-highlighter.spec.d.ts +2 -0
  13. package/lib/browser/chat-find/chat-find-highlighter.spec.d.ts.map +1 -0
  14. package/lib/browser/chat-find/chat-find-highlighter.spec.js +283 -0
  15. package/lib/browser/chat-find/chat-find-highlighter.spec.js.map +1 -0
  16. package/lib/browser/chat-find/chat-find-matcher.d.ts +72 -0
  17. package/lib/browser/chat-find/chat-find-matcher.d.ts.map +1 -0
  18. package/lib/browser/chat-find/chat-find-matcher.js +168 -0
  19. package/lib/browser/chat-find/chat-find-matcher.js.map +1 -0
  20. package/lib/browser/chat-find/chat-find-matcher.spec.d.ts +2 -0
  21. package/lib/browser/chat-find/chat-find-matcher.spec.d.ts.map +1 -0
  22. package/lib/browser/chat-find/chat-find-matcher.spec.js +145 -0
  23. package/lib/browser/chat-find/chat-find-matcher.spec.js.map +1 -0
  24. package/lib/browser/chat-find/chat-find-text.d.ts +20 -0
  25. package/lib/browser/chat-find/chat-find-text.d.ts.map +1 -0
  26. package/lib/browser/chat-find/chat-find-text.js +56 -0
  27. package/lib/browser/chat-find/chat-find-text.js.map +1 -0
  28. package/lib/browser/chat-find/chat-find-widget.d.ts +96 -0
  29. package/lib/browser/chat-find/chat-find-widget.d.ts.map +1 -0
  30. package/lib/browser/chat-find/chat-find-widget.js +332 -0
  31. package/lib/browser/chat-find/chat-find-widget.js.map +1 -0
  32. package/lib/browser/chat-find/chat-find-widget.spec.d.ts +2 -0
  33. package/lib/browser/chat-find/chat-find-widget.spec.d.ts.map +1 -0
  34. package/lib/browser/chat-find/chat-find-widget.spec.js +236 -0
  35. package/lib/browser/chat-find/chat-find-widget.spec.js.map +1 -0
  36. package/lib/browser/chat-input-scroll-util.d.ts +7 -0
  37. package/lib/browser/chat-input-scroll-util.d.ts.map +1 -0
  38. package/lib/browser/chat-input-scroll-util.js +33 -0
  39. package/lib/browser/chat-input-scroll-util.js.map +1 -0
  40. package/lib/browser/chat-input-scroll-util.spec.d.ts +2 -0
  41. package/lib/browser/chat-input-scroll-util.spec.d.ts.map +1 -0
  42. package/lib/browser/chat-input-scroll-util.spec.js +38 -0
  43. package/lib/browser/chat-input-scroll-util.spec.js.map +1 -0
  44. package/lib/browser/chat-input-widget.d.ts +21 -3
  45. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  46. package/lib/browser/chat-input-widget.js +109 -50
  47. package/lib/browser/chat-input-widget.js.map +1 -1
  48. package/lib/browser/chat-input-widget.spec.js +21 -0
  49. package/lib/browser/chat-input-widget.spec.js.map +1 -1
  50. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
  51. package/lib/browser/chat-response-renderer/code-part-renderer.js +9 -1
  52. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  53. package/lib/browser/chat-response-renderer/code-wrapper-editors.d.ts +13 -0
  54. package/lib/browser/chat-response-renderer/code-wrapper-editors.d.ts.map +1 -0
  55. package/lib/browser/chat-response-renderer/code-wrapper-editors.js +41 -0
  56. package/lib/browser/chat-response-renderer/code-wrapper-editors.js.map +1 -0
  57. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js +74 -43
  58. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js.map +1 -1
  59. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  60. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  61. package/lib/browser/chat-response-renderer/index.js +1 -0
  62. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  63. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +9 -0
  64. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
  65. package/lib/browser/chat-response-renderer/markdown-part-renderer.js +34 -16
  66. package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
  67. package/lib/browser/chat-response-renderer/server-toolcall-part-renderer.d.ts.map +1 -1
  68. package/lib/browser/chat-response-renderer/server-toolcall-part-renderer.js +2 -2
  69. package/lib/browser/chat-response-renderer/server-toolcall-part-renderer.js.map +1 -1
  70. package/lib/browser/chat-response-renderer/spinner.d.ts +3 -0
  71. package/lib/browser/chat-response-renderer/spinner.d.ts.map +1 -0
  72. package/lib/browser/chat-response-renderer/spinner.js +24 -0
  73. package/lib/browser/chat-response-renderer/spinner.js.map +1 -0
  74. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts +9 -1
  75. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts.map +1 -1
  76. package/lib/browser/chat-response-renderer/thinking-part-renderer.js +93 -2
  77. package/lib/browser/chat-response-renderer/thinking-part-renderer.js.map +1 -1
  78. package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.d.ts +2 -0
  79. package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.d.ts.map +1 -0
  80. package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.js +84 -0
  81. package/lib/browser/chat-response-renderer/thinking-part-renderer.spec.js.map +1 -0
  82. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  83. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +2 -2
  84. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  85. package/lib/browser/chat-response-renderer/toolcall-part-renderer.spec.js +16 -0
  86. package/lib/browser/chat-response-renderer/toolcall-part-renderer.spec.js.map +1 -1
  87. package/lib/browser/chat-response-renderer/toolcall-utils.d.ts.map +1 -1
  88. package/lib/browser/chat-response-renderer/toolcall-utils.js +7 -2
  89. package/lib/browser/chat-response-renderer/toolcall-utils.js.map +1 -1
  90. package/lib/browser/chat-tree-view/chat-view-tree-container.d.ts.map +1 -1
  91. package/lib/browser/chat-tree-view/chat-view-tree-container.js +4 -0
  92. package/lib/browser/chat-tree-view/chat-view-tree-container.js.map +1 -1
  93. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +20 -1
  94. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  95. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +88 -7
  96. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  97. package/lib/common/toolcall-utils.d.ts +11 -1
  98. package/lib/common/toolcall-utils.d.ts.map +1 -1
  99. package/lib/common/toolcall-utils.js +191 -9
  100. package/lib/common/toolcall-utils.js.map +1 -1
  101. package/lib/common/toolcall-utils.spec.js +92 -4
  102. package/lib/common/toolcall-utils.spec.js.map +1 -1
  103. package/package.json +13 -13
  104. package/src/browser/ai-chat-ui-frontend-module.ts +7 -0
  105. package/src/browser/chat-find/chat-find-contribution.ts +74 -0
  106. package/src/browser/chat-find/chat-find-highlighter.spec.ts +313 -0
  107. package/src/browser/chat-find/chat-find-highlighter.ts +385 -0
  108. package/src/browser/chat-find/chat-find-matcher.spec.ts +173 -0
  109. package/src/browser/chat-find/chat-find-matcher.ts +197 -0
  110. package/src/browser/chat-find/chat-find-text.ts +65 -0
  111. package/src/browser/chat-find/chat-find-widget.spec.tsx +261 -0
  112. package/src/browser/chat-find/chat-find-widget.tsx +418 -0
  113. package/src/browser/chat-input-scroll-util.spec.ts +42 -0
  114. package/src/browser/chat-input-scroll-util.ts +30 -0
  115. package/src/browser/chat-input-widget.spec.ts +29 -0
  116. package/src/browser/chat-input-widget.tsx +118 -56
  117. package/src/browser/chat-response-renderer/code-part-renderer.tsx +9 -1
  118. package/src/browser/chat-response-renderer/code-wrapper-editors.ts +41 -0
  119. package/src/browser/chat-response-renderer/delegation-tool-renderer.tsx +88 -62
  120. package/src/browser/chat-response-renderer/index.ts +1 -0
  121. package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +34 -17
  122. package/src/browser/chat-response-renderer/server-toolcall-part-renderer.tsx +2 -5
  123. package/src/browser/chat-response-renderer/spinner.tsx +22 -0
  124. package/src/browser/chat-response-renderer/thinking-part-renderer.spec.tsx +104 -0
  125. package/src/browser/chat-response-renderer/thinking-part-renderer.tsx +129 -9
  126. package/src/browser/chat-response-renderer/toolcall-part-renderer.spec.ts +16 -0
  127. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +1 -4
  128. package/src/browser/chat-response-renderer/toolcall-utils.ts +5 -0
  129. package/src/browser/chat-tree-view/chat-view-tree-container.ts +4 -0
  130. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +99 -6
  131. package/src/browser/style/chat-find.css +107 -0
  132. package/src/browser/style/index.css +75 -8
  133. package/src/common/toolcall-utils.spec.ts +113 -4
  134. package/src/common/toolcall-utils.ts +207 -9
@@ -28,8 +28,8 @@ import {
28
28
  import { mergeReasoningSettings } from '@theia/ai-core/lib/browser/frontend-language-model-service';
29
29
  import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
30
30
  import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
31
- import { AI_SHOW_SETTINGS_COMMAND, FrontendVariableService, AIActivationService } from '@theia/ai-core/lib/browser';
32
- import { AISettingsService, PromptService } from '@theia/ai-core/lib/common';
31
+ import { AI_SHOW_SETTINGS_COMMAND, AIActivationService, FavoriteModelsService, FrontendVariableService } from '@theia/ai-core/lib/browser';
32
+ import { AISettingsService, groupModelsByProvider, LanguageModelAliasRegistry, PromptService } from '@theia/ai-core/lib/common';
33
33
  import { CommandService, DisposableCollection, Emitter, InMemoryResources, MessageService, URI, nls, Disposable, ILogger } from '@theia/core';
34
34
  import { ContextMenuRenderer, HoverService, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
35
35
  import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
@@ -39,15 +39,15 @@ import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
39
39
  import { Deferred } from '@theia/core/lib/common/promise-util';
40
40
  import { inject, injectable, optional, postConstruct, named } from '@theia/core/shared/inversify';
41
41
  import * as React from '@theia/core/shared/react';
42
- import { IMouseEvent, Range } from '@theia/monaco-editor-core';
42
+ import { IMouseEvent, IPosition } from '@theia/monaco-editor-core';
43
43
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
44
44
  import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
45
45
  import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
46
46
  import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
47
+ import { computeRevealScrollDelta } from './chat-input-scroll-util';
47
48
  import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
48
49
  import { ContextVariablePicker } from './context-variable-picker';
49
50
  import { TASK_CONTEXT_VARIABLE } from '@theia/ai-chat/lib/browser/task-context-variable';
50
- import { IModelDeltaDecoration } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
51
51
  import { EditorOption } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
52
52
  import { SuggestController } from '@theia/monaco-editor-core/esm/vs/editor/contrib/suggest/browser/suggestController';
53
53
  import { ChatInputHistoryService, ChatInputNavigationState } from './chat-input-history';
@@ -84,6 +84,13 @@ type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
84
84
  type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
85
85
  type OpenContextElement = (request: AIVariableResolutionRequest) => unknown;
86
86
 
87
+ /**
88
+ * Id of the Models category of the AI Configuration view (`AiConfigurationCategoryId.MODELS`), used to
89
+ * open it from the model selector. Not imported: that id is declared in `@theia/ai-core-ui`, which this
90
+ * package does not depend on.
91
+ */
92
+ const AI_CONFIGURATION_MODELS_CATEGORY_ID = 'models';
93
+
87
94
  export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
88
95
  export interface AIChatInputConfiguration {
89
96
  showContext?: boolean;
@@ -182,6 +189,12 @@ export class AIChatInputWidget extends ReactWidget {
182
189
  @inject(FrontendLanguageModelRegistry)
183
190
  protected readonly languageModelRegistry: FrontendLanguageModelRegistry;
184
191
 
192
+ @inject(FavoriteModelsService)
193
+ protected readonly favoriteModels: FavoriteModelsService;
194
+
195
+ @inject(LanguageModelAliasRegistry)
196
+ protected readonly aliasRegistry: LanguageModelAliasRegistry;
197
+
185
198
  @inject(PreferenceService) @optional()
186
199
  protected readonly preferenceService: PreferenceService | undefined;
187
200
 
@@ -313,21 +326,20 @@ export class AIChatInputWidget extends ReactWidget {
313
326
  * Resolves the reasoning level to display in the selector. Priority: session override →
314
327
  * persisted per-agent selection (from {@link AISettingsService}) →
315
328
  * `ai-features.reasoning.defaults` preference entry matching the current model/agent →
316
- * model's declared default → `'off'`.
329
+ * model's declared default → `'off'`. The result is clamped to the model's supported levels, matching
330
+ * what the frontend language model service sends.
317
331
  */
318
332
  protected getCurrentReasoningLevel(): ReasoningLevel | undefined {
319
333
  if (!this.currentReasoningSupport) {
320
334
  return undefined;
321
335
  }
322
336
  const session = this.chatService.getSessions().find(s => s.model.id === this._chatModel?.id);
323
- const sessionLevel = session?.model.settings?.commonSettings?.reasoning?.level;
324
- if (sessionLevel) {
325
- return sessionLevel;
326
- }
327
- if (this.savedReasoning?.level) {
328
- return this.savedReasoning.level;
329
- }
330
- return this.resolvePreferenceReasoningLevel() ?? this.currentReasoningSupport.defaultLevel ?? 'off';
337
+ const level = session?.model.settings?.commonSettings?.reasoning?.level
338
+ ?? this.savedReasoning?.level
339
+ ?? this.resolvePreferenceReasoningLevel()
340
+ ?? this.currentReasoningSupport.defaultLevel
341
+ ?? 'off';
342
+ return ReasoningSupport.clampLevel(this.currentReasoningSupport, level);
331
343
  }
332
344
 
333
345
  protected resolvePreferenceReasoningLevel(): ReasoningLevel | undefined {
@@ -586,6 +598,17 @@ export class AIChatInputWidget extends ReactWidget {
586
598
  return session?.model.settings?.commonSettings?.modelId;
587
599
  }
588
600
 
601
+ /**
602
+ * Opens the Models page of the AI Configuration view, which is where the models this list shows are
603
+ * chosen. The target is the id of that view's Models category (`AiConfigurationCategoryId.MODELS`),
604
+ * passed as a string because this package does not depend on the package that declares it.
605
+ */
606
+ protected openModelConfiguration = (): void => {
607
+ this.commandService.executeCommand(AI_SHOW_SETTINGS_COMMAND.id, AI_CONFIGURATION_MODELS_CATEGORY_ID).catch(error => {
608
+ this.logger.error(`Failed to execute '${AI_SHOW_SETTINGS_COMMAND.id}' from the model selector`, error);
609
+ });
610
+ };
611
+
589
612
  /** Sets (or clears, with `undefined`) the per-session model override for the active session. */
590
613
  protected handleSessionModelChange = (modelId: string | undefined): void => {
591
614
  const session = this.chatService.getSessions().find(s => s.model.id === this._chatModel?.id);
@@ -609,16 +632,27 @@ export class AIChatInputWidget extends ReactWidget {
609
632
  this.update();
610
633
  };
611
634
 
635
+ /**
636
+ * The models the selector offers: the favorites, i.e. the newest models of each provider plus
637
+ * whatever the user starred, and the session's own model even when it is neither. Discovery
638
+ * registers every release a provider offers, which is a list far too long to pick from here; the
639
+ * provider's page in the AI Configuration view is where the rest can be browsed and starred.
640
+ */
641
+ protected getSelectableModels(currentModelId: string | undefined): LanguageModel[] {
642
+ return this.availableModels.filter(model => this.favoriteModels.isFavorite(model.id) || model.id === currentModelId);
643
+ }
644
+
612
645
  /** Builds the props for the per-session model selector. */
613
646
  protected getModelSelectorProps(): ModelSelectorWidgetProps {
614
647
  const currentModelId = this.getSessionModelOverride();
615
648
  const defaultLabel = this.resolvedDefaultLabel
616
649
  ?? nls.localize('theia/ai/chat-ui/agentDefaultModel', 'agent default');
617
650
  return {
618
- models: this.availableModels,
651
+ models: this.getSelectableModels(currentModelId),
619
652
  currentModelId,
620
653
  defaultLabel,
621
654
  onModelChange: this.handleSessionModelChange,
655
+ onConfigure: this.openModelConfiguration,
622
656
  };
623
657
  }
624
658
 
@@ -1113,6 +1147,14 @@ export class AIChatInputWidget extends ReactWidget {
1113
1147
  this.updateReasoningSupport(this.receivingAgent.agentId);
1114
1148
  }
1115
1149
  }));
1150
+ this.toDispose.push(this.favoriteModels.onDidChange(() => this.update()));
1151
+ // An agent's default is usually an alias, so editing which model the alias points at changes what
1152
+ // "Default" resolves to and with it the model-dependent state. The registry's own change event
1153
+ // does not cover this: the models it holds are the same ones, only the alias moved.
1154
+ this.toDispose.push(this.aliasRegistry.onDidChange(() => {
1155
+ this.updateResolvedDefaultModel();
1156
+ this.updateReasoningSupport(this.receivingAgent?.agentId);
1157
+ }));
1116
1158
  // When the agent's model is changed in the AI configuration, refresh the selector's resolved
1117
1159
  // default and the model-dependent state (reasoning support, context size, server tools, vendor).
1118
1160
  this.toDispose.push(this.aiSettingsService.onDidChange(() => {
@@ -1849,6 +1891,8 @@ interface ModelSelectorWidgetProps {
1849
1891
  defaultLabel: string;
1850
1892
  /** Set the session override (or clear it with `undefined`). */
1851
1893
  onModelChange: (modelId: string | undefined) => void;
1894
+ /** Opens the page that decides which models this list shows. */
1895
+ onConfigure: () => void;
1852
1896
  }
1853
1897
 
1854
1898
  interface ChatInputProperties {
@@ -2074,37 +2118,28 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
2074
2118
  props.contextMenuCallback(e.event)
2075
2119
  );
2076
2120
 
2077
- const updateLineCounts = () => {
2078
- // We need the line numbers to allow scrolling by using the keyboard
2079
- const model = editor.getControl().getModel()!;
2080
- const lineCount = model.getLineCount();
2081
- const decorations: IModelDeltaDecoration[] = [];
2082
-
2083
- for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
2084
- decorations.push({
2085
- range: new Range(lineNumber, 1, lineNumber, 1),
2086
- options: {
2087
- description: `line-number-${lineNumber}`,
2088
- isWholeLine: false,
2089
- className: `line-number-${lineNumber}`,
2090
- }
2091
- });
2121
+ // The editor is laid out at its full content height and the surrounding container
2122
+ // scrolls, so Monaco cannot reveal the cursor itself. Scroll the container to the
2123
+ // visual row of the cursor; for wrapped lines this differs from the start of the model line.
2124
+ // `getTopForPosition` is content-absolute, so subtract the editor's own scroll offset,
2125
+ // which is non-zero while `automaticLayout` has the editor laid out at a clamped height.
2126
+ const revealCursor = (position: IPosition) => {
2127
+ const container = editorContainerRef.current;
2128
+ const control = editor.getControl();
2129
+ const editorNode = control.getDomNode();
2130
+ if (!container || !editorNode) {
2131
+ return;
2132
+ }
2133
+ const rowTop = editorNode.getBoundingClientRect().top - container.getBoundingClientRect().top
2134
+ + control.getTopForPosition(position.lineNumber, position.column) - control.getScrollTop();
2135
+ const rowBottom = rowTop + control.getOption(EditorOption.lineHeight);
2136
+ const delta = computeRevealScrollDelta(rowTop, rowBottom, container.clientHeight);
2137
+ if (delta !== 0) {
2138
+ container.scrollTop += delta;
2092
2139
  }
2093
-
2094
- const lineNumbers = model.getAllDecorations().filter(predicate => predicate.options.description?.startsWith('line-number-'));
2095
- editor.getControl().removeDecorations(lineNumbers.map(d => d.id));
2096
- editor.getControl().createDecorationsCollection(decorations);
2097
2140
  };
2098
2141
 
2099
- editor.getControl().getModel()?.onDidChangeContent(() => {
2100
- updateLineCounts();
2101
- });
2102
-
2103
- editor.getControl().onDidChangeCursorPosition(e => {
2104
- const lineNumber = e.position.lineNumber;
2105
- const line = editor.getControl().getDomNode()?.querySelector(`.line-number-${lineNumber}`);
2106
- line?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
2107
- });
2142
+ editor.getControl().onDidChangeCursorPosition(e => revealCursor(e.position));
2108
2143
 
2109
2144
  editorRef.current = editor;
2110
2145
  props.setEditorRef(editor);
@@ -2112,8 +2147,6 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
2112
2147
  if (props.initialValue) {
2113
2148
  setValue(props.initialValue);
2114
2149
  }
2115
-
2116
- updateLineCounts();
2117
2150
  };
2118
2151
  createInputElement();
2119
2152
 
@@ -2609,6 +2642,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({
2609
2642
  currentModelId={modelSelectorProps.currentModelId}
2610
2643
  defaultLabel={modelSelectorProps.defaultLabel}
2611
2644
  onModelChange={modelSelectorProps.onModelChange}
2645
+ onConfigure={modelSelectorProps.onConfigure}
2612
2646
  disabled={!isEnabled}
2613
2647
  hoverService={hoverService}
2614
2648
  />
@@ -2777,6 +2811,8 @@ interface ChatModelSelectorProps {
2777
2811
  currentModelId?: string;
2778
2812
  defaultLabel: string;
2779
2813
  onModelChange: (modelId: string | undefined) => void;
2814
+ /** Opens the page that decides which models this list shows. */
2815
+ onConfigure: () => void;
2780
2816
  disabled?: boolean;
2781
2817
  hoverService: HoverService;
2782
2818
  }
@@ -2784,33 +2820,51 @@ interface ChatModelSelectorProps {
2784
2820
  /**
2785
2821
  * Per-session model selector. The first option ("Default") reverts to the agent's configured
2786
2822
  * model that new sessions use; picking any other model overrides it for the current session only.
2823
+ *
2824
+ * It lists the models marked for it rather than every model a provider offers, and ends with the way
2825
+ * to change that, so a list this short is not mistaken for all there is.
2787
2826
  */
2788
2827
  const ChatModelSelector: React.FunctionComponent<ChatModelSelectorProps> = React.memo(({
2789
- models, currentModelId, defaultLabel, onModelChange, disabled, hoverService
2828
+ models, currentModelId, defaultLabel, onModelChange, onConfigure, disabled, hoverService
2790
2829
  }) => {
2791
2830
  // Sentinel value for the "use the agent default" option (SelectComponent needs a non-empty value).
2792
2831
  const defaultValueId = '__default__';
2832
+ // …and for the entry that opens the configuration instead of selecting anything.
2833
+ const configureValueId = '__configure__';
2834
+ // Picking the configure entry leaves the model as it was, but the select has already taken it as its
2835
+ // selection; remounting it puts the session's own model back in the field.
2836
+ const [selectorGeneration, setSelectorGeneration] = React.useState(0);
2793
2837
  const isOverridden = !!currentModelId;
2794
2838
  // The override points at a model that is no longer ready/available. Guard on a loaded model list so
2795
2839
  // the override is not flagged as unavailable during the initial (still empty) load.
2796
2840
  const isUnavailable = isOverridden && models.length > 0
2797
2841
  && !models.some(model => model.id === currentModelId && model.status.status === 'ready');
2798
2842
  const options: SelectOption[] = React.useMemo(() => {
2799
- const readyModels = models.filter(model => model.status.status === 'ready')
2800
- // Stable, predictable order: the registry adds models as their async metadata resolves.
2801
- .sort((left, right) => left.id.localeCompare(right.id));
2802
2843
  const opts: SelectOption[] = [
2803
2844
  {
2804
2845
  value: defaultValueId,
2805
2846
  label: nls.localizeByDefault('Default'),
2806
2847
  detail: defaultLabel
2807
- },
2808
- ...readyModels.map(model => ({
2848
+ }
2849
+ ];
2850
+ // A rule between the providers, their models newest first: the models of one provider belong
2851
+ // together, and the registry adds them in whatever order their metadata resolves. Models whose
2852
+ // provider reports no release date fall back to alphabetical.
2853
+ groupModelsByProvider(models.filter(model => model.status.status === 'ready')).forEach(({ models: providerModels }) => {
2854
+ opts.push({ separator: true });
2855
+ opts.push(...providerModels.map(model => ({
2809
2856
  value: model.id,
2810
2857
  label: model.id,
2811
2858
  detail: model.name && model.name !== model.id ? model.name : undefined
2812
- }))
2813
- ];
2859
+ })));
2860
+ });
2861
+ // The list is deliberately short — it holds the models marked for it, not every model a provider
2862
+ // offers — so it says where the rest are rather than leaving the absence to be puzzled over.
2863
+ opts.push({ separator: true });
2864
+ opts.push({
2865
+ value: configureValueId,
2866
+ label: nls.localize('theia/ai/chat-ui/manageModels', 'Manage models…')
2867
+ });
2814
2868
  // Keep the active override visible even if its model is no longer ready/available, so the
2815
2869
  // selector reflects the stored modelId instead of silently falling back to "Default". Once we
2816
2870
  // know it is unavailable, show it as a disabled (struck-through) entry that cannot be reselected.
@@ -2826,8 +2880,15 @@ const ChatModelSelector: React.FunctionComponent<ChatModelSelectorProps> = React
2826
2880
  }, [models, defaultLabel, currentModelId, isUnavailable]);
2827
2881
 
2828
2882
  const handleChange = React.useCallback(
2829
- (option: SelectOption) => onModelChange(!option.value || option.value === defaultValueId ? undefined : option.value),
2830
- [onModelChange]
2883
+ (option: SelectOption) => {
2884
+ if (option.value === configureValueId) {
2885
+ setSelectorGeneration(generation => generation + 1);
2886
+ onConfigure();
2887
+ return;
2888
+ }
2889
+ onModelChange(!option.value || option.value === defaultValueId ? undefined : option.value);
2890
+ },
2891
+ [onModelChange, onConfigure]
2831
2892
  );
2832
2893
 
2833
2894
  const title = isUnavailable
@@ -2839,6 +2900,7 @@ const ChatModelSelector: React.FunctionComponent<ChatModelSelectorProps> = React
2839
2900
  return (
2840
2901
  <span className='theia-ChatInput-ModelSelector-container' onMouseEnter={hoverHandler(hoverService, title)}>
2841
2902
  <SelectComponent
2903
+ key={selectorGeneration}
2842
2904
  className={`theia-ChatInput-ModelSelector${isOverridden ? ' session-override' : ''}${disabled ? ' disabled' : ''}`}
2843
2905
  options={options}
2844
2906
  defaultValue={currentModelId ?? defaultValueId}
@@ -2885,7 +2947,7 @@ const ReasoningSelector: React.FunctionComponent<ReasoningSelectorProps> = React
2885
2947
  );
2886
2948
 
2887
2949
  const title = nls.localizeByDefault('Reasoning');
2888
- const effectiveLevel = currentLevel ?? reasoningSupport.defaultLevel ?? reasoningSupport.supportedLevels[0] ?? 'off';
2950
+ const effectiveLevel = ReasoningSupport.clampLevel(reasoningSupport, currentLevel ?? reasoningSupport.defaultLevel ?? 'off');
2889
2951
 
2890
2952
  return (
2891
2953
  <span onMouseEnter={hoverHandler(hoverService, title)}>
@@ -3030,7 +3092,7 @@ function toUiElement(element: ChangeSetElement,
3030
3092
  }
3031
3093
 
3032
3094
  const ChangeSetElement: React.FC<ChangeSetUIElement> = element => (
3033
- <li title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
3095
+ <li title={nls.localizeByDefault('Open Diff')} onClick={() => element.openChange?.()}>
3034
3096
  <div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
3035
3097
  </div>
3036
3098
  <div className='theia-ChatInput-ChangeSet-labelParts'>
@@ -32,6 +32,7 @@ import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
32
32
  import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
33
33
  import { ChatViewTreeWidget, ResponseNode } from '../chat-tree-view/chat-view-tree-widget';
34
34
  import { IMouseEvent } from '@theia/monaco-editor-core';
35
+ import { CodeWrapperEditors } from './code-wrapper-editors';
35
36
 
36
37
  export const CodePartRendererAction = Symbol('CodePartRendererAction');
37
38
  /**
@@ -249,11 +250,18 @@ export const CodeWrapper = (props: {
249
250
  editor.document.textEditorModel.setValue(props.content);
250
251
  editor.getControl().onContextMenu(e => props.contextMenuCallback(e.event));
251
252
  editorRef.current = editor;
253
+ if (ref.current) {
254
+ CodeWrapperEditors.set(ref.current, editor);
255
+ }
252
256
  };
253
257
 
254
258
  React.useEffect(() => {
255
259
  createInputElement();
260
+ const element = ref.current;
256
261
  return () => {
262
+ if (element) {
263
+ CodeWrapperEditors.remove(element);
264
+ }
257
265
  if (editorRef.current) {
258
266
  editorRef.current.dispose();
259
267
  }
@@ -268,5 +276,5 @@ export const CodeWrapper = (props: {
268
276
 
269
277
  editorRef.current?.resizeToFit();
270
278
 
271
- return <div className='theia-CodeWrapper' ref={ref}></div>;
279
+ return <div className={CodeWrapperEditors.CLASS} ref={ref}></div>;
272
280
  };
@@ -0,0 +1,41 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
18
+
19
+ const editors = new WeakMap<Element, SimpleMonacoEditor>();
20
+
21
+ /**
22
+ * The Monaco editors that code parts render into, keyed by their `.theia-CodeWrapper` element, so features that
23
+ * work on the chat DOM (e.g. find in chat) can reach the editor that renders a code block.
24
+ */
25
+ export namespace CodeWrapperEditors {
26
+
27
+ export const CLASS = 'theia-CodeWrapper';
28
+
29
+ /** The editor rendered into `element`, once it has been created. */
30
+ export function get(element: Element): SimpleMonacoEditor | undefined {
31
+ return editors.get(element);
32
+ }
33
+
34
+ export function set(element: Element, editor: SimpleMonacoEditor): void {
35
+ editors.set(element, editor);
36
+ }
37
+
38
+ export function remove(element: Element): void {
39
+ editors.delete(element);
40
+ }
41
+ }
@@ -148,11 +148,6 @@ export class DelegationToolRenderer implements ChatResponsePartRenderer<ToolCall
148
148
  }
149
149
  }
150
150
 
151
- interface PendingInteraction {
152
- contentPart: InteractiveContent & ChatResponseContent;
153
- id: string;
154
- }
155
-
156
151
  interface DelegatedChatProps {
157
152
  invocation?: ChatRequestInvocation;
158
153
  agentName: string;
@@ -167,20 +162,18 @@ interface DelegatedChatProps {
167
162
  interface DelegatedChatState {
168
163
  node?: ResponseNode;
169
164
  isOpen: boolean;
170
- pendingInteractions: PendingInteraction[];
171
165
  }
172
166
 
173
167
  class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatState> {
174
168
  private widget: ReturnType<SubChatWidgetFactory>;
175
169
  private toDispose = new DisposableCollection();
176
- private trackedInteractionIds = new Set<string>();
170
+ private unmounted = false;
177
171
 
178
172
  constructor(props: DelegatedChatProps) {
179
173
  super(props);
180
174
  this.state = {
181
175
  node: undefined,
182
- isOpen: false,
183
- pendingInteractions: []
176
+ isOpen: false
184
177
  };
185
178
  this.widget = props.subChatWidgetFactory();
186
179
  }
@@ -198,72 +191,64 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
198
191
  private subscribeToInvocation(invocation?: ChatRequestInvocation): void {
199
192
  this.toDispose.dispose();
200
193
  this.toDispose = new DisposableCollection();
201
- this.trackedInteractionIds.clear();
202
194
  if (!invocation) {
203
195
  return;
204
196
  }
205
197
 
206
198
  invocation.responseCreated.then(chatModel => {
199
+ // Guard against continuations of a superseded invocation or an unmounted
200
+ // component: they would show the old model's node and register listeners
201
+ // into an already-disposed collection (which silently keeps them alive).
202
+ if (this.unmounted || invocation !== this.props.invocation) {
203
+ return;
204
+ }
207
205
  const node = mapResponseToNode(chatModel, this.props.parentNode);
208
206
  this.setState({ node });
209
207
 
210
- const changeListener = () => {
211
- this.removeResolvedInteractions();
212
- this.forceUpdate();
213
- };
214
- this.toDispose.push(chatModel.onDidChange(changeListener));
215
-
216
- // Subscribe to interactionNeeded for push-based interaction tracking
217
- this.toDispose.push(chatModel.onInteractionNeeded(contentPart => {
218
- const id = contentPart.interactionId;
219
- if (id && !this.trackedInteractionIds.has(id)
220
- && this.findConfirmationRenderer(contentPart)) {
221
- this.trackedInteractionIds.add(id);
222
- this.setState(prevState => ({
223
- pendingInteractions: [
224
- ...prevState.pendingInteractions,
225
- { contentPart, id }
226
- ]
227
- }));
228
- }
229
- }));
208
+ this.toDispose.push(chatModel.onDidChange(() => this.forceUpdate()));
209
+ // Pending interactions are derived from chatModel.pendingInteractions in
210
+ // render(); interactionNeeded does not imply onDidChange, so re-render
211
+ // explicitly when a new interaction is announced (#17952).
212
+ this.toDispose.push(chatModel.onInteractionNeeded(() => this.forceUpdate()));
230
213
  }).catch(error => {
231
214
  console.error('Failed to create delegated chat response:', error);
232
215
  });
233
216
 
234
217
  invocation.responseCompleted.then(() => {
235
- this.forceUpdate();
218
+ if (!this.unmounted) {
219
+ this.forceUpdate();
220
+ }
236
221
  }).catch(error => {
237
222
  console.error('Error in delegated chat response completion:', error);
238
- this.forceUpdate();
223
+ if (!this.unmounted) {
224
+ this.forceUpdate();
225
+ }
239
226
  });
240
227
  }
241
228
 
242
229
  override componentWillUnmount(): void {
230
+ this.unmounted = true;
243
231
  this.toDispose.dispose();
244
- this.trackedInteractionIds.clear();
245
- }
246
-
247
- private removeResolvedInteractions(): void {
248
- this.setState(prevState => ({
249
- pendingInteractions: prevState.pendingInteractions.filter(p => !p.contentPart.isResolved)
250
- }));
251
232
  }
252
233
 
234
+ /**
235
+ * The highest-priority renderer that implements renderConfirmation. Unlike plain
236
+ * rendering, this must not stop at the overall highest-priority renderer: a
237
+ * specialized renderer without renderConfirmation (e.g. the delegation renderer
238
+ * itself for nested delegations) would otherwise silently drop the interaction.
239
+ */
253
240
  private findConfirmationRenderer(contentPart: ChatResponseContent): ChatResponsePartRenderer<ChatResponseContent> | undefined {
254
- const renderer = this.props.chatResponsePartRenderers.getContributions().reduce<[number, ChatResponsePartRenderer<ChatResponseContent> | undefined]>(
255
- (prev, current) => {
256
- const prio = current.canHandle(contentPart);
257
- if (prio > prev[0]) {
258
- return [prio, current];
259
- }
260
- return prev;
261
- },
262
- [-1, undefined])[1];
263
- if (renderer && renderer.renderConfirmation) {
264
- return renderer;
265
- }
266
- return undefined;
241
+ return this.props.chatResponsePartRenderers.getContributions()
242
+ .filter(renderer => renderer.renderConfirmation)
243
+ .reduce<[number, ChatResponsePartRenderer<ChatResponseContent> | undefined]>(
244
+ (prev, current) => {
245
+ const prio = current.canHandle(contentPart);
246
+ if (prio > prev[0]) {
247
+ return [prio, current];
248
+ }
249
+ return prev;
250
+ },
251
+ [-1, undefined])[1];
267
252
  }
268
253
 
269
254
  private handleToggle = (event: React.SyntheticEvent<HTMLDetailsElement>): void => {
@@ -271,10 +256,43 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
271
256
  this.setState({ isOpen: details.open });
272
257
  };
273
258
 
274
- private renderInteractionConfirmation(contentPart: ChatResponseContent, id: string): React.ReactNode {
259
+ /**
260
+ * Clicking anywhere inside a <summary> toggles the <details> element by default,
261
+ * which would collapse/expand the block while the user operates the inline
262
+ * interaction UI. Suppress the toggle for such clicks — but keep nested
263
+ * <summary> elements working (e.g. the argument expanders of a confirmation
264
+ * card), whose own toggle is also the click's default action.
265
+ */
266
+ private preventSummaryToggle = (event: React.MouseEvent): void => {
267
+ const target = event.target instanceof HTMLElement ? event.target : undefined;
268
+ const closestSummary = target?.closest('summary');
269
+ if (closestSummary && event.currentTarget.contains(closestSummary)) {
270
+ return;
271
+ }
272
+ event.preventDefault();
273
+ };
274
+
275
+ private renderPendingInteractions(pendingInteractions: ReadonlyArray<InteractiveContent & ChatResponseContent>): React.ReactNode[] {
276
+ // Key by interactionId so a sibling interaction resolving does not remount the
277
+ // remaining ones (losing in-progress wizard state); disambiguate the rare
278
+ // duplicate ids (e.g. two pending questions with identical text) by occurrence.
279
+ const seenIds = new Map<string, number>();
280
+ return pendingInteractions.map(contentPart => {
281
+ const baseKey = contentPart.interactionId ?? 'interaction';
282
+ const occurrence = seenIds.get(baseKey) ?? 0;
283
+ seenIds.set(baseKey, occurrence + 1);
284
+ return this.renderInteractionConfirmation(contentPart, occurrence === 0 ? baseKey : `${baseKey}-${occurrence}`);
285
+ });
286
+ }
287
+
288
+ private renderInteractionConfirmation(contentPart: InteractiveContent & ChatResponseContent, key: string): React.ReactNode {
275
289
  const renderer = this.findConfirmationRenderer(contentPart);
276
290
  if (renderer && this.state.node) {
277
- return <React.Fragment key={id}>{renderer.renderConfirmation!(contentPart, this.state.node)}</React.Fragment>;
291
+ return (
292
+ <React.Fragment key={key}>
293
+ {renderer.renderConfirmation!(contentPart, this.state.node)}
294
+ </React.Fragment>
295
+ );
278
296
  }
279
297
  return undefined;
280
298
  }
@@ -285,6 +303,12 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
285
303
  const isComplete = this.state.node?.response.isComplete ?? false;
286
304
  const isCanceled = this.state.node?.response.isCanceled ?? false;
287
305
  const isError = this.state.node?.response.isError ?? false;
306
+ // Derived from the model on every render so pending interactions survive
307
+ // remounts and never outlive their resolution or the response (#17952). The
308
+ // list also covers interactions bubbled up from nested delegations, whose
309
+ // waiting state is not reflected in this response's isWaitingForInput.
310
+ const pendingInteractions = this.state.node?.response.pendingInteractions ?? [];
311
+ const isWaitingForInput = (this.state.node?.response.isWaitingForInput ?? false) || pendingInteractions.length > 0;
288
312
 
289
313
  let statusIcon = '';
290
314
  let statusText = '';
@@ -292,12 +316,16 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
292
316
  if (isCanceled) {
293
317
  statusIcon = 'codicon-close';
294
318
  statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/canceled', 'canceled');
295
- } else if (isComplete) {
296
- statusIcon = 'codicon-check';
297
- statusText = nls.localizeByDefault('completed');
298
319
  } else if (isError) {
320
+ // error() also marks the response complete, so check isError first
299
321
  statusIcon = 'codicon-error';
300
322
  statusText = nls.localizeByDefault('error');
323
+ } else if (isComplete) {
324
+ statusIcon = 'codicon-check';
325
+ statusText = nls.localizeByDefault('completed');
326
+ } else if (isWaitingForInput) {
327
+ statusIcon = 'codicon-loading';
328
+ statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/waitingForInput', 'waiting for input');
301
329
  } else {
302
330
  statusIcon = 'codicon-loading';
303
331
  statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/generating', 'generating...');
@@ -310,7 +338,7 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
310
338
  statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/starting', 'starting...');
311
339
  }
312
340
 
313
- const { isOpen, pendingInteractions } = this.state;
341
+ const { isOpen } = this.state;
314
342
  const showInteractionsInSummary = !isOpen && pendingInteractions.length > 0;
315
343
 
316
344
  return (
@@ -336,10 +364,8 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
336
364
  <span className={`delegation-toggle-arrow${isOpen ? ' open' : ''}`} />
337
365
  </div>
338
366
  {showInteractionsInSummary && (
339
- <div className='delegation-pending-confirmations'>
340
- {pendingInteractions.map(({ contentPart, id }) =>
341
- this.renderInteractionConfirmation(contentPart, id)
342
- )}
367
+ <div className='delegation-pending-confirmations' onClick={this.preventSummaryToggle}>
368
+ {this.renderPendingInteractions(pendingInteractions)}
343
369
  </div>
344
370
  )}
345
371
  </summary>