@theia/ai-chat-ui 1.68.0-next.8 → 1.68.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 (132) hide show
  1. package/README.md +33 -0
  2. package/lib/browser/ai-chat-ui-contribution.d.ts +2 -1
  3. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  4. package/lib/browser/ai-chat-ui-contribution.js +41 -32
  5. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  7. package/lib/browser/ai-chat-ui-frontend-module.js +15 -6
  8. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  9. package/lib/browser/change-set-actions/change-set-accept-action.d.ts +0 -1
  10. package/lib/browser/change-set-actions/change-set-accept-action.d.ts.map +1 -1
  11. package/lib/browser/change-set-actions/change-set-action-service.d.ts +0 -1
  12. package/lib/browser/change-set-actions/change-set-action-service.d.ts.map +1 -1
  13. package/lib/browser/change-set-actions/change-set-action-service.js +2 -2
  14. package/lib/browser/change-set-actions/change-set-action-service.js.map +1 -1
  15. package/lib/browser/chat-focus-contribution.d.ts +14 -0
  16. package/lib/browser/chat-focus-contribution.d.ts.map +1 -0
  17. package/lib/browser/chat-focus-contribution.js +102 -0
  18. package/lib/browser/chat-focus-contribution.js.map +1 -0
  19. package/lib/browser/chat-input-agent-suggestions.d.ts +0 -1
  20. package/lib/browser/chat-input-agent-suggestions.d.ts.map +1 -1
  21. package/lib/browser/chat-input-agent-suggestions.js +1 -1
  22. package/lib/browser/chat-input-agent-suggestions.js.map +1 -1
  23. package/lib/browser/chat-input-history-contribution.js +6 -10
  24. package/lib/browser/chat-input-history-contribution.js.map +1 -1
  25. package/lib/browser/chat-input-mode-contribution.js +1 -2
  26. package/lib/browser/chat-input-mode-contribution.js.map +1 -1
  27. package/lib/browser/chat-input-widget.d.ts +0 -1
  28. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  29. package/lib/browser/chat-input-widget.js +106 -96
  30. package/lib/browser/chat-input-widget.js.map +1 -1
  31. package/lib/browser/chat-progress-message.d.ts +0 -1
  32. package/lib/browser/chat-progress-message.d.ts.map +1 -1
  33. package/lib/browser/chat-response-part-renderer.d.ts +0 -1
  34. package/lib/browser/chat-response-part-renderer.d.ts.map +1 -1
  35. package/lib/browser/chat-response-renderer/ai-selection-resolver.js +4 -5
  36. package/lib/browser/chat-response-renderer/ai-selection-resolver.js.map +1 -1
  37. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts +0 -1
  38. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
  39. package/lib/browser/chat-response-renderer/code-part-renderer.js +6 -9
  40. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  41. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts +0 -1
  42. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts.map +1 -1
  43. package/lib/browser/chat-response-renderer/command-part-renderer.js +7 -4
  44. package/lib/browser/chat-response-renderer/command-part-renderer.js.map +1 -1
  45. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +0 -1
  46. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -1
  47. package/lib/browser/chat-response-renderer/delegation-response-renderer.js +3 -4
  48. package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -1
  49. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts +0 -1
  50. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts.map +1 -1
  51. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts +0 -1
  52. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts.map +1 -1
  53. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  54. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  55. package/lib/browser/chat-response-renderer/index.js +1 -0
  56. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  57. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +0 -1
  58. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
  59. package/lib/browser/chat-response-renderer/markdown-part-renderer.js +7 -9
  60. package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
  61. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.d.ts +17 -0
  62. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.d.ts.map +1 -0
  63. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.js +56 -0
  64. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.js.map +1 -0
  65. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts +0 -1
  66. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts.map +1 -1
  67. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +0 -1
  68. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -1
  69. package/lib/browser/chat-response-renderer/question-part-renderer.js +6 -9
  70. package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -1
  71. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts +0 -1
  72. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts.map +1 -1
  73. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts +0 -1
  74. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts.map +1 -1
  75. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +29 -10
  76. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  77. package/lib/browser/chat-response-renderer/tool-confirmation.js +219 -65
  78. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  79. package/lib/browser/chat-response-renderer/tool-confirmation.spec.d.ts +2 -0
  80. package/lib/browser/chat-response-renderer/tool-confirmation.spec.d.ts.map +1 -0
  81. package/lib/browser/chat-response-renderer/tool-confirmation.spec.js +100 -0
  82. package/lib/browser/chat-response-renderer/tool-confirmation.spec.js.map +1 -0
  83. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -3
  84. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  85. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +21 -11
  86. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  87. package/lib/browser/chat-response-renderer/unknown-part-renderer.d.ts +0 -1
  88. package/lib/browser/chat-response-renderer/unknown-part-renderer.d.ts.map +1 -1
  89. package/lib/browser/chat-response-renderer/unknown-part-renderer.js +1 -2
  90. package/lib/browser/chat-response-renderer/unknown-part-renderer.js.map +1 -1
  91. package/lib/browser/chat-tree-view/chat-view-tree-container.js +1 -2
  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-input-widget.js +2 -3
  94. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  95. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +3 -1
  96. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  97. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +49 -42
  98. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  99. package/lib/browser/chat-tree-view/prompt-variant-badge.d.ts +0 -1
  100. package/lib/browser/chat-tree-view/prompt-variant-badge.d.ts.map +1 -1
  101. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +0 -1
  102. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -1
  103. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  104. package/lib/browser/chat-view-commands.js +2 -2
  105. package/lib/browser/chat-view-commands.js.map +1 -1
  106. package/lib/browser/chat-view-contribution.js +7 -7
  107. package/lib/browser/chat-view-contribution.js.map +1 -1
  108. package/lib/browser/chat-view-language-contribution.js +3 -4
  109. package/lib/browser/chat-view-language-contribution.js.map +1 -1
  110. package/lib/browser/chat-view-widget.js +9 -10
  111. package/lib/browser/chat-view-widget.js.map +1 -1
  112. package/lib/browser/context-variable-picker.js +9 -13
  113. package/lib/browser/context-variable-picker.js.map +1 -1
  114. package/lib/browser/session-storage-preference-renderer.d.ts +127 -0
  115. package/lib/browser/session-storage-preference-renderer.d.ts.map +1 -0
  116. package/lib/browser/session-storage-preference-renderer.js +490 -0
  117. package/lib/browser/session-storage-preference-renderer.js.map +1 -0
  118. package/package.json +12 -11
  119. package/src/browser/ai-chat-ui-contribution.ts +19 -7
  120. package/src/browser/ai-chat-ui-frontend-module.ts +13 -0
  121. package/src/browser/chat-focus-contribution.ts +102 -0
  122. package/src/browser/chat-input-widget.tsx +99 -27
  123. package/src/browser/chat-response-renderer/index.ts +1 -0
  124. package/src/browser/chat-response-renderer/not-available-toolcall-renderer.tsx +57 -0
  125. package/src/browser/chat-response-renderer/tool-confirmation.spec.ts +108 -0
  126. package/src/browser/chat-response-renderer/tool-confirmation.tsx +379 -95
  127. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +27 -13
  128. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +33 -5
  129. package/src/browser/chat-view-commands.ts +2 -2
  130. package/src/browser/session-storage-preference-renderer.ts +657 -0
  131. package/src/browser/style/index.css +73 -26
  132. package/src/browser/style/session-storage-preference.css +134 -0
@@ -20,6 +20,7 @@ import { bindViewContribution, FrontendApplicationContribution, WidgetFactory, K
20
20
  import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
21
21
  import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
22
22
  import { EditorSelectionResolver } from '@theia/editor/lib/browser/editor-manager';
23
+ import { PreferenceNodeRendererContribution } from '@theia/preferences/lib/browser/views/components/preference-node-renderer-creator';
23
24
  import { AIChatContribution } from './ai-chat-ui-contribution';
24
25
  import { AIChatInputConfiguration, AIChatInputWidget } from './chat-input-widget';
25
26
  import { ChatNodeToolbarActionContribution, DefaultChatNodeToolbarActionContribution } from './chat-node-toolbar-action-contribution';
@@ -34,6 +35,7 @@ import {
34
35
  InsertCodeAtCursorButtonAction,
35
36
  MarkdownPartRenderer,
36
37
  ToolCallPartRenderer,
38
+ NotAvailableToolCallRenderer,
37
39
  ThinkingPartRenderer,
38
40
  ProgressPartRenderer,
39
41
  DelegationResponseRenderer,
@@ -60,6 +62,8 @@ import { SubChatWidget, SubChatWidgetFactory } from './chat-tree-view/sub-chat-w
60
62
  import { ChatInputHistoryService } from './chat-input-history';
61
63
  import { ChatInputHistoryContribution } from './chat-input-history-contribution';
62
64
  import { ChatInputModeContribution } from './chat-input-mode-contribution';
65
+ import { ChatFocusContribution } from './chat-focus-contribution';
66
+ import { SessionStoragePreferenceRenderer, SessionStoragePreferenceRendererContribution } from './session-storage-preference-renderer';
63
67
 
64
68
  export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
65
69
  bindViewContribution(bind, AIChatContribution);
@@ -74,6 +78,10 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
74
78
  bind(CommandContribution).toService(ChatInputModeContribution);
75
79
  bind(KeybindingContribution).toService(ChatInputModeContribution);
76
80
 
81
+ bind(ChatFocusContribution).toSelf().inSingletonScope();
82
+ bind(CommandContribution).toService(ChatFocusContribution);
83
+ bind(KeybindingContribution).toService(ChatFocusContribution);
84
+
77
85
  bindContributionProvider(bind, ChatResponsePartRenderer);
78
86
 
79
87
  bindChatViewWidget(bind);
@@ -132,6 +140,7 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
132
140
  bind(ChatResponsePartRenderer).to(CodePartRenderer).inSingletonScope();
133
141
  bind(ChatResponsePartRenderer).to(CommandPartRenderer).inSingletonScope();
134
142
  bind(ChatResponsePartRenderer).to(ToolCallPartRenderer).inSingletonScope();
143
+ bind(ChatResponsePartRenderer).to(NotAvailableToolCallRenderer).inSingletonScope();
135
144
  bind(ChatResponsePartRenderer).to(ErrorPartRenderer).inSingletonScope();
136
145
  bind(ChatResponsePartRenderer).to(ThinkingPartRenderer).inSingletonScope();
137
146
  bind(ChatResponsePartRenderer).to(QuestionPartRenderer).inSingletonScope();
@@ -173,6 +182,10 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
173
182
  return widget;
174
183
  });
175
184
 
185
+ // Session storage preference renderer
186
+ bind(SessionStoragePreferenceRenderer).toSelf();
187
+ bind(PreferenceNodeRendererContribution).to(SessionStoragePreferenceRendererContribution).inSingletonScope();
188
+
176
189
  });
177
190
 
178
191
  function bindChatViewWidget(bind: interfaces.Bind): void {
@@ -0,0 +1,102 @@
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 { Command, CommandContribution, CommandRegistry } from '@theia/core';
18
+ import { ApplicationShell, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser';
19
+ import { inject, injectable } from '@theia/core/shared/inversify';
20
+ import { ChatViewWidget } from './chat-view-widget';
21
+ import { ChatCommands } from './chat-view-commands';
22
+
23
+ export const CHAT_FOCUS_INPUT_COMMAND = Command.toLocalizedCommand({
24
+ id: 'ai-chat.focus-input',
25
+ category: ChatCommands.CHAT_CATEGORY,
26
+ label: 'Focus Chat Input'
27
+ }, 'theia/ai/chat-ui/focusInput', ChatCommands.CHAT_CATEGORY_KEY);
28
+
29
+ export const CHAT_FOCUS_RESPONSE_COMMAND = Command.toLocalizedCommand({
30
+ id: 'ai-chat.focus-response',
31
+ category: ChatCommands.CHAT_CATEGORY,
32
+ label: 'Focus Chat Response'
33
+ }, 'theia/ai/chat-ui/focusResponse', ChatCommands.CHAT_CATEGORY_KEY);
34
+
35
+ @injectable()
36
+ export class ChatFocusContribution implements CommandContribution, KeybindingContribution {
37
+
38
+ @inject(ApplicationShell)
39
+ protected readonly shell: ApplicationShell;
40
+
41
+ registerCommands(commands: CommandRegistry): void {
42
+ commands.registerCommand(CHAT_FOCUS_INPUT_COMMAND, {
43
+ execute: () => this.focusInput(),
44
+ isEnabled: () => this.findActiveChatViewWidget() !== undefined
45
+ });
46
+ commands.registerCommand(CHAT_FOCUS_RESPONSE_COMMAND, {
47
+ execute: () => this.focusResponse(),
48
+ isEnabled: () => this.findActiveChatViewWidget() !== undefined
49
+ });
50
+ }
51
+
52
+ registerKeybindings(keybindings: KeybindingRegistry): void {
53
+ keybindings.registerKeybinding({
54
+ command: CHAT_FOCUS_RESPONSE_COMMAND.id,
55
+ keybinding: 'ctrlcmd+up',
56
+ when: 'chatInputFocus && !suggestWidgetVisible'
57
+ });
58
+ keybindings.registerKeybinding({
59
+ command: CHAT_FOCUS_INPUT_COMMAND.id,
60
+ keybinding: 'ctrlcmd+down',
61
+ when: 'chatResponseFocus'
62
+ });
63
+ }
64
+
65
+ protected focusInput(): void {
66
+ const chatViewWidget = this.findActiveChatViewWidget();
67
+ if (chatViewWidget) {
68
+ chatViewWidget.inputWidget.activate();
69
+ }
70
+ }
71
+
72
+ protected focusResponse(): void {
73
+ const chatViewWidget = this.findActiveChatViewWidget();
74
+ if (chatViewWidget) {
75
+ chatViewWidget.treeWidget.node.focus();
76
+ }
77
+ }
78
+
79
+ protected findActiveChatViewWidget(): ChatViewWidget | undefined {
80
+ const activeWidget = this.shell.activeWidget;
81
+ if (activeWidget instanceof ChatViewWidget) {
82
+ return activeWidget;
83
+ }
84
+ // Also check if any part of the chat view has focus
85
+ const activeElement = document.activeElement;
86
+ if (activeElement instanceof HTMLElement) {
87
+ const widget = this.shell.findWidgetForElement(activeElement);
88
+ if (widget instanceof ChatViewWidget) {
89
+ return widget;
90
+ }
91
+ // Check parent widgets (e.g., when input widget has focus)
92
+ let parent = widget?.parent;
93
+ while (parent) {
94
+ if (parent instanceof ChatViewWidget) {
95
+ return parent;
96
+ }
97
+ parent = parent.parent;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ }
@@ -364,9 +364,12 @@ export class AIChatInputWidget extends ReactWidget {
364
364
  // Only update and re-render when the agent changes
365
365
  if (agent && agentId !== previousAgentId) {
366
366
  const modes = agent.modes ?? [];
367
+ const defaultMode = modes.find(m => m.isDefault);
368
+ const initialModeId = defaultMode?.id;
367
369
  this.receivingAgent = {
368
370
  agentId: agentId,
369
- modes
371
+ modes,
372
+ currentModeId: initialModeId
370
373
  };
371
374
  this.chatInputHasModesKey.set(modes.length > 1);
372
375
  this.update();
@@ -494,6 +497,7 @@ export class AIChatInputWidget extends ReactWidget {
494
497
  this.editorRef = editor;
495
498
  this.setupEditorEventListeners();
496
499
  this.editorReady.resolve();
500
+ this.scheduleUpdateReceivingAgent();
497
501
  }}
498
502
  showContext={this.configuration?.showContext}
499
503
  showPinnedAgent={this.configuration?.showPinnedAgent}
@@ -785,6 +789,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
785
789
  bracketPairColorization: { enabled: false },
786
790
  wrappingStrategy: 'advanced',
787
791
  stickyScroll: { enabled: false },
792
+ ariaLabel: nls.localize('theia/ai/chat-ui/chatInputAriaLabel', 'Type your message here'),
788
793
  });
789
794
 
790
795
  if (editorContainerRef.current) {
@@ -952,6 +957,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
952
957
  setValue('');
953
958
  if (editorRef.current && !editorRef.current.document.textEditorModel.isDisposed()) {
954
959
  editorRef.current.document.textEditorModel.setValue('');
960
+ editorRef.current.focus();
955
961
  }
956
962
  }, [props.context, props.onQuery, props.modeSelectorProps.currentMode, setValue, shouldUseTaskPlaceholder, taskPlaceholder]);
957
963
 
@@ -1132,40 +1138,60 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({
1132
1138
  isEnabled,
1133
1139
  modeSelectorProps
1134
1140
  }) => (
1141
+ // Right options are rendered first in DOM for tab order (send button first when enabled)
1142
+ // CSS order property positions them visually (left on left, right on right)
1135
1143
  <div className="theia-ChatInputOptions">
1136
- <div className="theia-ChatInputOptions-left">
1137
- {leftOptions.map((option, index) => (
1144
+ <div className="theia-ChatInputOptions-right">
1145
+ {rightOptions.map((option, index) => (
1138
1146
  <span
1139
1147
  key={index}
1140
1148
  className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
1141
1149
  title={option.title}
1150
+ aria-label={option.title}
1151
+ role='button'
1152
+ tabIndex={option.disabled ? -1 : 0}
1142
1153
  onClick={option.handler}
1154
+ onKeyDown={e => {
1155
+ if (e.key === 'Enter' || e.key === ' ') {
1156
+ e.preventDefault();
1157
+ option.handler();
1158
+ }
1159
+ }}
1143
1160
  >
1144
1161
  <span>{option.text?.content}</span>
1145
1162
  <span className={`codicon ${option.className}`} />
1146
1163
  </span>
1147
1164
  ))}
1148
- {modeSelectorProps.show && modeSelectorProps.modes && (
1149
- <ChatModeSelector
1150
- modes={modeSelectorProps.modes}
1151
- currentMode={modeSelectorProps.currentMode}
1152
- onModeChange={modeSelectorProps.onModeChange}
1153
- disabled={!isEnabled}
1154
- />
1155
- )}
1156
1165
  </div>
1157
- <div className="theia-ChatInputOptions-right">
1158
- {rightOptions.map((option, index) => (
1166
+ <div className="theia-ChatInputOptions-left">
1167
+ {leftOptions.map((option, index) => (
1159
1168
  <span
1160
1169
  key={index}
1161
1170
  className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
1162
1171
  title={option.title}
1172
+ aria-label={option.title}
1173
+ role='button'
1174
+ tabIndex={option.disabled ? -1 : 0}
1163
1175
  onClick={option.handler}
1176
+ onKeyDown={e => {
1177
+ if (e.key === 'Enter' || e.key === ' ') {
1178
+ e.preventDefault();
1179
+ option.handler();
1180
+ }
1181
+ }}
1164
1182
  >
1165
1183
  <span>{option.text?.content}</span>
1166
1184
  <span className={`codicon ${option.className}`} />
1167
1185
  </span>
1168
1186
  ))}
1187
+ {modeSelectorProps.show && modeSelectorProps.modes && (
1188
+ <ChatModeSelector
1189
+ modes={modeSelectorProps.modes}
1190
+ currentMode={modeSelectorProps.currentMode}
1191
+ onModeChange={modeSelectorProps.onModeChange}
1192
+ disabled={!isEnabled}
1193
+ />
1194
+ )}
1169
1195
  </div>
1170
1196
  </div>
1171
1197
  );
@@ -1239,22 +1265,68 @@ interface ChangeSetUI {
1239
1265
  }
1240
1266
 
1241
1267
  /** Memo because the parent element rerenders on every key press in the chat widget. */
1242
- const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.memo(({ changeSet: { changeSet, title, deleteChangeSet, elements, actions } }) => (
1243
- <div className='theia-ChatInput-ChangeSet-Box'>
1244
- <div className='theia-ChatInput-ChangeSet-Header'>
1245
- <h3>{title}</h3>
1246
- <div className='theia-ChatInput-ChangeSet-Header-Actions'>
1247
- {actions.map(action => <div key={action.id} className='theia-changeSet-Action'>{action.render(changeSet)}</div>)}
1248
- <span className='codicon codicon-close action' title={nls.localize('theia/ai/chat-ui/deleteChangeSet', 'Delete Change Set')} onClick={() => deleteChangeSet()} />
1268
+ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.memo(({ changeSet: { changeSet, title, deleteChangeSet, elements, actions } }) => {
1269
+ const [isCollapsed, setIsCollapsed] = React.useState(false);
1270
+
1271
+ const toggleCollapse = React.useCallback(() => {
1272
+ setIsCollapsed(prev => !prev);
1273
+ }, []);
1274
+
1275
+ const handleToggleKeyDown = React.useCallback(
1276
+ (e: React.KeyboardEvent) => {
1277
+ if (e.key === 'Enter' || e.key === ' ') {
1278
+ e.preventDefault();
1279
+ toggleCollapse();
1280
+ }
1281
+ },
1282
+ [toggleCollapse]
1283
+ );
1284
+
1285
+ return (
1286
+ <div className='theia-ChatInput-ChangeSet-Box'>
1287
+ <div className='theia-ChatInput-ChangeSet-Header'>
1288
+ <div className='theia-ChatInput-ChangeSet-Header-Title'>
1289
+ <span
1290
+ className={`codicon ${isCollapsed ? 'codicon-chevron-right' : 'codicon-chevron-down'} theia-ChatInput-ChangeSet-Toggle`}
1291
+ onClick={toggleCollapse}
1292
+ onKeyDown={handleToggleKeyDown}
1293
+ role='button'
1294
+ tabIndex={0}
1295
+ aria-expanded={!isCollapsed}
1296
+ aria-label={
1297
+ isCollapsed
1298
+ ? nls.localize('theia/ai/chat-ui/expandChangeSet', 'Expand Change Set')
1299
+ : nls.localize('theia/ai/chat-ui/collapseChangeSet', 'Collapse Change Set')
1300
+ }
1301
+ title={
1302
+ isCollapsed
1303
+ ? nls.localize('theia/ai/chat-ui/expandChangeSet', 'Expand Change Set')
1304
+ : nls.localize('theia/ai/chat-ui/collapseChangeSet', 'Collapse Change Set')
1305
+ }
1306
+ />
1307
+ <h3>{title}</h3>
1308
+ </div>
1309
+ <div className='theia-ChatInput-ChangeSet-Header-Actions'>
1310
+ {actions.map(action => (
1311
+ <div key={action.id} className='theia-changeSet-Action'>
1312
+ {action.render(changeSet)}
1313
+ </div>
1314
+ ))}
1315
+ <span
1316
+ className='codicon codicon-close action'
1317
+ title={nls.localize('theia/ai/chat-ui/deleteChangeSet', 'Delete Change Set')}
1318
+ onClick={() => deleteChangeSet()}
1319
+ />
1320
+ </div>
1321
+ </div>
1322
+ <div className={`theia-ChatInput-ChangeSet-List ${isCollapsed ? 'collapsed' : 'expanded'}`}>
1323
+ <ul>
1324
+ {elements.map(element => ChangeSetElement(element))}
1325
+ </ul>
1249
1326
  </div>
1250
1327
  </div>
1251
- <div className='theia-ChatInput-ChangeSet-List'>
1252
- <ul>
1253
- {elements.map(element => ChangeSetElement(element))}
1254
- </ul>
1255
- </div>
1256
- </div>
1257
- ));
1328
+ );
1329
+ });
1258
1330
 
1259
1331
  function toUiElement(element: ChangeSetElement,
1260
1332
  onDeleteChangeSetElement: (uri: URI) => void,
@@ -21,6 +21,7 @@ export * from './horizontal-layout-part-renderer';
21
21
  export * from './markdown-part-renderer';
22
22
  export * from './text-part-renderer';
23
23
  export * from './toolcall-part-renderer';
24
+ export * from './not-available-toolcall-renderer';
24
25
  export * from './thinking-part-renderer';
25
26
  export * from './progress-part-renderer';
26
27
  export * from './tool-confirmation';
@@ -0,0 +1,57 @@
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 { ChatResponsePartRenderer } from '../chat-response-part-renderer';
18
+ import { injectable } from '@theia/core/shared/inversify';
19
+ import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
20
+ import { ReactNode } from '@theia/core/shared/react';
21
+ import { codicon } from '@theia/core/lib/browser';
22
+ import * as React from '@theia/core/shared/react';
23
+ import { ResponseNode } from '../chat-tree-view';
24
+
25
+ /**
26
+ * High-priority renderer for tool calls that were not available.
27
+ *
28
+ * This handles cases where the LLM attempted to call a tool that was not
29
+ * made available to it in the request. This takes priority over all other
30
+ * tool renderers (including specialized ones like ShellExecutionToolRenderer)
31
+ * since unavailable tools should never be processed by tool-specific renderers.
32
+ */
33
+ @injectable()
34
+ export class NotAvailableToolCallRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {
35
+
36
+ canHandle(response: ChatResponseContent): number {
37
+ if (ToolCallChatResponseContent.is(response) && response.finished) {
38
+ if (ToolCallChatResponseContent.isNotAvailableResult(response.result)) {
39
+ return 100;
40
+ }
41
+ }
42
+ return -1;
43
+ }
44
+
45
+ render(response: ToolCallChatResponseContent, _parentNode: ResponseNode): ReactNode {
46
+ const errorMessage = ToolCallChatResponseContent.getErrorMessage(response.result);
47
+ return (
48
+ <div className='theia-toolCall'>
49
+ <span className='theia-toolCall-unavailable'>
50
+ <span className={codicon('warning')}></span>
51
+ {' '}
52
+ {response.name}: {errorMessage}
53
+ </span>
54
+ </div>
55
+ );
56
+ }
57
+ }
@@ -0,0 +1,108 @@
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 { ContextMenuRenderer } from '@theia/core/lib/browser';
19
+ import { ConfirmationScope, ToolConfirmationCallbacks, ToolConfirmationActionsProps, ToolConfirmationProps } from './tool-confirmation';
20
+
21
+ const mockContextMenuRenderer = {} as ContextMenuRenderer;
22
+
23
+ describe('Tool Confirmation Types', () => {
24
+ describe('ConfirmationScope', () => {
25
+ it('should accept valid scopes', () => {
26
+ const scopes: ConfirmationScope[] = ['once', 'session', 'forever'];
27
+ expect(scopes).to.have.length(3);
28
+ });
29
+ });
30
+
31
+ describe('ToolConfirmationCallbacks', () => {
32
+ it('should define required callback properties', () => {
33
+ const callbacks: ToolConfirmationCallbacks = {
34
+ onAllow: (_scope: ConfirmationScope) => { },
35
+ onDeny: (_scope: ConfirmationScope, _reason?: string) => { }
36
+ };
37
+ expect(callbacks.onAllow).to.be.a('function');
38
+ expect(callbacks.onDeny).to.be.a('function');
39
+ });
40
+
41
+ it('should allow optional toolRequest', () => {
42
+ const callbacks: ToolConfirmationCallbacks = {
43
+ toolRequest: { id: 'test', name: 'test', handler: async () => '', parameters: { type: 'object', properties: {} } },
44
+ onAllow: () => { },
45
+ onDeny: () => { }
46
+ };
47
+ expect(callbacks.toolRequest).to.exist;
48
+ });
49
+ });
50
+
51
+ describe('ToolConfirmationActionsProps', () => {
52
+ it('should extend ToolConfirmationCallbacks with toolName', () => {
53
+ const props: ToolConfirmationActionsProps = {
54
+ toolName: 'testTool',
55
+ onAllow: () => { },
56
+ onDeny: () => { },
57
+ contextMenuRenderer: mockContextMenuRenderer
58
+ };
59
+ expect(props.toolName).to.equal('testTool');
60
+ });
61
+
62
+ it('should support confirmAlwaysAllow string in toolRequest', () => {
63
+ const props: ToolConfirmationActionsProps = {
64
+ toolName: 'dangerousTool',
65
+ toolRequest: {
66
+ id: 'test',
67
+ name: 'test',
68
+ handler: async () => '',
69
+ parameters: { type: 'object', properties: {} },
70
+ confirmAlwaysAllow: 'This tool can modify system files.'
71
+ },
72
+ onAllow: () => { },
73
+ onDeny: () => { },
74
+ contextMenuRenderer: mockContextMenuRenderer
75
+ };
76
+ expect(props.toolRequest?.confirmAlwaysAllow).to.equal('This tool can modify system files.');
77
+ });
78
+
79
+ it('should support confirmAlwaysAllow boolean in toolRequest', () => {
80
+ const props: ToolConfirmationActionsProps = {
81
+ toolName: 'dangerousTool',
82
+ toolRequest: {
83
+ id: 'test',
84
+ name: 'test',
85
+ handler: async () => '',
86
+ parameters: { type: 'object', properties: {} },
87
+ confirmAlwaysAllow: true
88
+ },
89
+ onAllow: () => { },
90
+ onDeny: () => { },
91
+ contextMenuRenderer: mockContextMenuRenderer
92
+ };
93
+ expect(props.toolRequest?.confirmAlwaysAllow).to.be.true;
94
+ });
95
+ });
96
+
97
+ describe('ToolConfirmationProps', () => {
98
+ it('should pick toolRequest from ToolConfirmationCallbacks', () => {
99
+ const props: ToolConfirmationProps = {
100
+ response: { kind: 'toolCall', id: 'test', name: 'test' } as ToolConfirmationProps['response'],
101
+ onAllow: () => { },
102
+ onDeny: () => { },
103
+ contextMenuRenderer: mockContextMenuRenderer
104
+ };
105
+ expect(props.toolRequest).to.be.undefined;
106
+ });
107
+ });
108
+ });