@theia/ai-chat-ui 1.60.2 → 1.61.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 (79) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +5 -1
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +102 -4
  4. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  5. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.js +30 -1
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/chat-input-agent-suggestions.d.ts +11 -0
  9. package/lib/browser/chat-input-agent-suggestions.d.ts.map +1 -0
  10. package/lib/browser/chat-input-agent-suggestions.js +76 -0
  11. package/lib/browser/chat-input-agent-suggestions.js.map +1 -0
  12. package/lib/browser/chat-input-widget.d.ts +17 -6
  13. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  14. package/lib/browser/chat-input-widget.js +72 -22
  15. package/lib/browser/chat-input-widget.js.map +1 -1
  16. package/lib/browser/chat-node-toolbar-action-contribution.d.ts +8 -0
  17. package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
  18. package/lib/browser/chat-node-toolbar-action-contribution.js +55 -1
  19. package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
  20. package/lib/browser/chat-progress-message.d.ts +7 -0
  21. package/lib/browser/chat-progress-message.d.ts.map +1 -0
  22. package/lib/browser/chat-progress-message.js +33 -0
  23. package/lib/browser/chat-progress-message.js.map +1 -0
  24. package/lib/browser/chat-response-renderer/code-part-renderer.js +1 -1
  25. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  26. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  27. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  28. package/lib/browser/chat-response-renderer/index.js +1 -0
  29. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  30. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +7 -1
  31. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
  32. package/lib/browser/chat-response-renderer/markdown-part-renderer.js +14 -3
  33. package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
  34. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts +9 -0
  35. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts.map +1 -0
  36. package/lib/browser/chat-response-renderer/progress-part-renderer.js +39 -0
  37. package/lib/browser/chat-response-renderer/progress-part-renderer.js.map +1 -0
  38. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +33 -0
  39. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -0
  40. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +79 -0
  41. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -0
  42. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +20 -4
  43. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  44. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +184 -48
  45. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  46. package/lib/browser/chat-view-commands.d.ts +3 -0
  47. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  48. package/lib/browser/chat-view-commands.js +15 -0
  49. package/lib/browser/chat-view-commands.js.map +1 -1
  50. package/lib/browser/chat-view-contribution.d.ts +1 -0
  51. package/lib/browser/chat-view-contribution.d.ts.map +1 -1
  52. package/lib/browser/chat-view-contribution.js +16 -14
  53. package/lib/browser/chat-view-contribution.js.map +1 -1
  54. package/lib/browser/chat-view-language-contribution.d.ts +3 -3
  55. package/lib/browser/chat-view-language-contribution.d.ts.map +1 -1
  56. package/lib/browser/chat-view-language-contribution.js +9 -22
  57. package/lib/browser/chat-view-language-contribution.js.map +1 -1
  58. package/lib/browser/chat-view-widget.d.ts +6 -2
  59. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  60. package/lib/browser/chat-view-widget.js +36 -19
  61. package/lib/browser/chat-view-widget.js.map +1 -1
  62. package/package.json +12 -12
  63. package/src/browser/ai-chat-ui-contribution.ts +93 -6
  64. package/src/browser/ai-chat-ui-frontend-module.ts +33 -3
  65. package/src/browser/chat-input-agent-suggestions.tsx +85 -0
  66. package/src/browser/chat-input-widget.tsx +122 -32
  67. package/src/browser/chat-node-toolbar-action-contribution.ts +40 -1
  68. package/src/browser/chat-progress-message.tsx +40 -0
  69. package/src/browser/chat-response-renderer/code-part-renderer.tsx +3 -3
  70. package/src/browser/chat-response-renderer/index.ts +1 -0
  71. package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +19 -2
  72. package/src/browser/chat-response-renderer/progress-part-renderer.tsx +40 -0
  73. package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +89 -0
  74. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +200 -37
  75. package/src/browser/chat-view-commands.ts +18 -0
  76. package/src/browser/chat-view-contribution.ts +20 -16
  77. package/src/browser/chat-view-language-contribution.ts +10 -24
  78. package/src/browser/chat-view-widget.tsx +18 -5
  79. package/src/browser/style/index.css +58 -4
@@ -0,0 +1,85 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH 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 * as React from '@theia/core/shared/react';
18
+ import { DeclaredEventsEventListenerObject, useMarkdownRendering } from './chat-response-renderer/markdown-part-renderer';
19
+ import { OpenerService } from '@theia/core/lib/browser';
20
+ import { ChatSuggestion, ChatSuggestionCallback } from '@theia/ai-chat';
21
+ import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
22
+
23
+ interface ChatInputAgentSuggestionsProps {
24
+ suggestions: readonly ChatSuggestion[];
25
+ opener: OpenerService;
26
+ }
27
+
28
+ function getText(suggestion: ChatSuggestion): string {
29
+ if (typeof suggestion === 'string') { return suggestion; }
30
+ if ('value' in suggestion) { return suggestion.value; }
31
+ if (typeof suggestion.content === 'string') { return suggestion.content; }
32
+ return suggestion.content.value;
33
+ }
34
+
35
+ function getContent(suggestion: ChatSuggestion): string | MarkdownString {
36
+ if (typeof suggestion === 'string') { return suggestion; }
37
+ if ('value' in suggestion) { return suggestion; }
38
+ return suggestion.content;
39
+ }
40
+
41
+ export const ChatInputAgentSuggestions: React.FC<ChatInputAgentSuggestionsProps> = ({ suggestions, opener }) => (
42
+ !!suggestions?.length && <div className="chat-agent-suggestions">
43
+ {suggestions.map(suggestion => <ChatInputAgentSuggestion
44
+ key={getText(suggestion)}
45
+ suggestion={suggestion}
46
+ opener={opener}
47
+ handler={ChatSuggestionCallback.is(suggestion) ? new ChatSuggestionClickHandler(suggestion) : undefined}
48
+ />)}
49
+ </div>
50
+ );
51
+
52
+ interface ChatInputAgestSuggestionProps {
53
+ suggestion: ChatSuggestion;
54
+ opener: OpenerService;
55
+ handler?: DeclaredEventsEventListenerObject;
56
+ }
57
+
58
+ const ChatInputAgentSuggestion: React.FC<ChatInputAgestSuggestionProps> = ({ suggestion, opener, handler }) => {
59
+ const ref = useMarkdownRendering(getContent(suggestion), opener, true, handler);
60
+ return <div className="chat-agent-suggestion" style={(!handler || ChatSuggestionCallback.containsCallbackLink(suggestion)) ? undefined : { cursor: 'pointer' }} ref={ref} />;
61
+ };
62
+
63
+ class ChatSuggestionClickHandler implements DeclaredEventsEventListenerObject {
64
+ constructor(protected readonly suggestion: ChatSuggestionCallback) { }
65
+ handleEvent(event: Event): boolean {
66
+ const { target, currentTarget } = event;
67
+ if (event.type !== 'click' || !(target instanceof Element)) { return false; }
68
+ const link = target.closest('a[href^="_callback"]');
69
+ if (link) {
70
+ this.suggestion.callback();
71
+ return true;
72
+ }
73
+ if (!(currentTarget instanceof Element)) {
74
+ this.suggestion.callback();
75
+ return true;
76
+ }
77
+ const containedLink = currentTarget.querySelector('a[href^="_callback"]');
78
+ // Whole body should count.
79
+ if (!containedLink) {
80
+ this.suggestion.callback();
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ }
@@ -13,31 +13,35 @@
13
13
  //
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
- import { ChangeSet, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat';
16
+ import { ChangeSet, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel, ChatService, ChatSuggestion } from '@theia/ai-chat';
17
17
  import { Disposable, DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
18
- import { ContextMenuRenderer, LabelProvider, Message, ReactWidget } from '@theia/core/lib/browser';
18
+ import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
19
19
  import { Deferred } from '@theia/core/lib/common/promise-util';
20
20
  import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
21
21
  import * as React from '@theia/core/shared/react';
22
22
  import { IMouseEvent } from '@theia/monaco-editor-core';
23
- import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
23
+ import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
24
24
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
25
25
  import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
26
26
  import { AIVariableResolutionRequest } from '@theia/ai-core';
27
27
  import { FrontendVariableService } from '@theia/ai-core/lib/browser';
28
28
  import { ContextVariablePicker } from './context-variable-picker';
29
29
  import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
30
+ import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
31
+ import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
30
32
 
31
33
  type Query = (query: string) => Promise<void>;
32
34
  type Unpin = () => void;
33
35
  type Cancel = (requestModel: ChatRequestModel) => void;
34
36
  type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
35
37
  type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
38
+ type OpenContextElement = (request: AIVariableResolutionRequest) => unknown;
36
39
 
37
40
  export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
38
41
  export interface AIChatInputConfiguration {
39
42
  showContext?: boolean;
40
43
  showPinnedAgent?: boolean;
44
+ showChangeSet?: boolean;
41
45
  }
42
46
 
43
47
  @injectable()
@@ -69,8 +73,17 @@ export class AIChatInputWidget extends ReactWidget {
69
73
  @inject(ChangeSetActionService)
70
74
  protected readonly changeSetActionService: ChangeSetActionService;
71
75
 
72
- protected editorRef: MonacoEditor | undefined = undefined;
73
- private editorReady = new Deferred<void>();
76
+ @inject(ChangeSetDecoratorService)
77
+ protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
78
+
79
+ @inject(OpenerService)
80
+ protected readonly openerService: OpenerService;
81
+
82
+ @inject(ChatService)
83
+ protected readonly chatService: ChatService;
84
+
85
+ protected editorRef: SimpleMonacoEditor | undefined = undefined;
86
+ protected readonly editorReady = new Deferred<void>();
74
87
 
75
88
  protected isEnabled = false;
76
89
 
@@ -95,6 +108,11 @@ export class AIChatInputWidget extends ReactWidget {
95
108
  this._onDeleteChangeSetElement = deleteChangeSetElement;
96
109
  }
97
110
 
111
+ private _initialValue?: string;
112
+ set initialValue(value: string | undefined) {
113
+ this._initialValue = value;
114
+ }
115
+
98
116
  protected onDisposeForChatModel = new DisposableCollection();
99
117
  private _chatModel: ChatModel;
100
118
  set chatModel(chatModel: ChatModel) {
@@ -130,6 +148,10 @@ export class AIChatInputWidget extends ReactWidget {
130
148
  });
131
149
  }
132
150
 
151
+ protected getResourceUri(): URI {
152
+ return new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
153
+ }
154
+
133
155
  protected render(): React.ReactNode {
134
156
  return (
135
157
  <ChatInput
@@ -142,11 +164,13 @@ export class AIChatInputWidget extends ReactWidget {
142
164
  onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
143
165
  onAddContextElement={this.addContextElement.bind(this)}
144
166
  onDeleteContextElement={this.deleteContextElement.bind(this)}
145
- context={this._chatModel.context.getVariables()}
167
+ context={this.getContext()}
168
+ onOpenContextElement={this.openContextElement.bind(this)}
146
169
  chatModel={this._chatModel}
147
170
  pinnedAgent={this._pinnedAgent}
148
171
  editorProvider={this.editorProvider}
149
172
  resources={this.resources}
173
+ resourceUriProvider={this.getResourceUri.bind(this)}
150
174
  contextMenuCallback={this.handleContextMenu.bind(this)}
151
175
  isEnabled={this.isEnabled}
152
176
  setEditorRef={editor => {
@@ -155,8 +179,13 @@ export class AIChatInputWidget extends ReactWidget {
155
179
  }}
156
180
  showContext={this.configuration?.showContext}
157
181
  showPinnedAgent={this.configuration?.showPinnedAgent}
182
+ showChangeSet={this.configuration?.showChangeSet}
158
183
  labelProvider={this.labelProvider}
159
184
  actionService={this.changeSetActionService}
185
+ decoratorService={this.changeSetDecoratorService}
186
+ initialValue={this._initialValue}
187
+ openerService={this.openerService}
188
+ suggestions={this._chatModel.suggestions}
160
189
  />
161
190
  );
162
191
  }
@@ -195,6 +224,12 @@ export class AIChatInputWidget extends ReactWidget {
195
224
  });
196
225
  }
197
226
 
227
+ protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
228
+ const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
229
+ const context = { session };
230
+ await this.variableService.open(request, context);
231
+ }
232
+
198
233
  public setEnabled(enabled: boolean): void {
199
234
  this.isEnabled = enabled;
200
235
  this.update();
@@ -203,7 +238,7 @@ export class AIChatInputWidget extends ReactWidget {
203
238
  protected addContextElement(): void {
204
239
  this.contextVariablePicker.pickContextVariable().then(contextElement => {
205
240
  if (contextElement) {
206
- this._chatModel.context.addVariables(contextElement);
241
+ this.addContext(contextElement);
207
242
  }
208
243
  });
209
244
  }
@@ -225,6 +260,10 @@ export class AIChatInputWidget extends ReactWidget {
225
260
  addContext(variable: AIVariableResolutionRequest): void {
226
261
  this._chatModel.context.addVariables(variable);
227
262
  }
263
+
264
+ protected getContext(): readonly AIVariableResolutionRequest[] {
265
+ return this._chatModel.context.getVariables();
266
+ }
228
267
  }
229
268
 
230
269
  interface ChatInputProperties {
@@ -237,18 +276,25 @@ interface ChatInputProperties {
237
276
  onDeleteChangeSetElement: (sessionId: string, index: number) => void;
238
277
  onAddContextElement: () => void;
239
278
  onDeleteContextElement: (index: number) => void;
279
+ onOpenContextElement: OpenContextElement;
240
280
  context?: readonly AIVariableResolutionRequest[];
241
281
  isEnabled?: boolean;
242
282
  chatModel: ChatModel;
243
283
  pinnedAgent?: ChatAgent;
244
284
  editorProvider: MonacoEditorProvider;
245
285
  resources: InMemoryResources;
286
+ resourceUriProvider: () => URI;
246
287
  contextMenuCallback: (event: IMouseEvent) => void;
247
- setEditorRef: (editor: MonacoEditor | undefined) => void;
288
+ setEditorRef: (editor: SimpleMonacoEditor | undefined) => void;
248
289
  showContext?: boolean;
249
290
  showPinnedAgent?: boolean;
291
+ showChangeSet?: boolean;
250
292
  labelProvider: LabelProvider;
251
293
  actionService: ChangeSetActionService;
294
+ decoratorService: ChangeSetDecoratorService;
295
+ initialValue?: string;
296
+ openerService: OpenerService;
297
+ suggestions: readonly ChatSuggestion[]
252
298
  }
253
299
 
254
300
  const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
@@ -262,6 +308,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
262
308
  ? buildChangeSetUI(
263
309
  props.chatModel.changeSet,
264
310
  props.labelProvider,
311
+ props.decoratorService,
265
312
  props.actionService.getActionsForChangeset(props.chatModel.changeSet),
266
313
  onDeleteChangeSet,
267
314
  onDeleteChangeSetElement
@@ -273,16 +320,16 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
273
320
  const editorContainerRef = React.useRef<HTMLDivElement | null>(null);
274
321
  // eslint-disable-next-line no-null/no-null
275
322
  const placeholderRef = React.useRef<HTMLDivElement | null>(null);
276
- const editorRef = React.useRef<MonacoEditor | undefined>(undefined);
323
+ const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
277
324
 
278
325
  React.useEffect(() => {
279
- const uri = new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
326
+ const uri = props.resourceUriProvider();
280
327
  const resource = props.resources.add(uri, '');
281
328
  const createInputElement = async () => {
282
329
  const paddingTop = 6;
283
330
  const lineHeight = 20;
284
331
  const maxHeight = 240;
285
- const editor = await props.editorProvider.createInline(uri, editorContainerRef.current!, {
332
+ const editor = await props.editorProvider.createSimpleInline(uri, editorContainerRef.current!, {
286
333
  language: CHAT_VIEW_LANGUAGE_EXTENSION,
287
334
  // Disable code lens, inlay hints and hover support to avoid console errors from other contributions
288
335
  codeLens: false,
@@ -323,6 +370,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
323
370
  editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeight)}px`;
324
371
  }
325
372
  };
373
+
326
374
  editor.getControl().onDidChangeModelContent(() => {
327
375
  const value = editor.getControl().getValue();
328
376
  setIsInputEmpty(!value || value.length === 0);
@@ -343,6 +391,10 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
343
391
 
344
392
  editorRef.current = editor;
345
393
  props.setEditorRef(editor);
394
+
395
+ if (props.initialValue) {
396
+ setValue(props.initialValue);
397
+ }
346
398
  };
347
399
  createInputElement();
348
400
 
@@ -376,6 +428,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
376
428
  setChangeSetUI(buildChangeSetUI(
377
429
  event.changeSet,
378
430
  props.labelProvider,
431
+ props.decoratorService,
379
432
  props.actionService.getActionsForChangeset(event.changeSet),
380
433
  onDeleteChangeSet,
381
434
  onDeleteChangeSetElement
@@ -387,6 +440,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
387
440
  ? buildChangeSetUI(
388
441
  props.chatModel.changeSet,
389
442
  props.labelProvider,
443
+ props.decoratorService,
390
444
  props.actionService.getActionsForChangeset(props.chatModel.changeSet),
391
445
  onDeleteChangeSet,
392
446
  onDeleteChangeSetElement
@@ -408,16 +462,37 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
408
462
  return () => disposable.dispose();
409
463
  });
410
464
 
465
+ React.useEffect(() => {
466
+ const disposable = props.decoratorService.onDidChangeDecorations(() => {
467
+ if (!props.chatModel.changeSet) {
468
+ return;
469
+ }
470
+ setChangeSetUI(buildChangeSetUI(
471
+ props.chatModel.changeSet,
472
+ props.labelProvider,
473
+ props.decoratorService,
474
+ props.actionService.getActionsForChangeset(props.chatModel.changeSet),
475
+ onDeleteChangeSet,
476
+ onDeleteChangeSetElement
477
+ ));
478
+ });
479
+ return () => disposable.dispose();
480
+ });
481
+
482
+ const setValue = React.useCallback((value: string) => {
483
+ if (editorRef.current && !editorRef.current.document.isDisposed()) {
484
+ editorRef.current.document.textEditorModel.setValue(value);
485
+ }
486
+ }, [editorRef]);
487
+
411
488
  const submit = React.useCallback(function submit(value: string): void {
412
489
  if (!value || value.trim().length === 0) {
413
490
  return;
414
491
  }
415
492
  setInProgress(true);
416
493
  props.onQuery(value);
417
- if (editorRef.current) {
418
- editorRef.current.document.textEditorModel.setValue('');
419
- }
420
- }, [props.context, props.onQuery, editorRef]);
494
+ setValue('');
495
+ }, [props.context, props.onQuery, setValue]);
421
496
 
422
497
  const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
423
498
  if (!props.isEnabled) {
@@ -515,10 +590,11 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
515
590
  disabled: isInputEmpty || !props.isEnabled
516
591
  }];
517
592
 
518
- const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement);
593
+ const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
519
594
 
520
595
  return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
521
- {changeSetUI?.elements &&
596
+ {<ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
597
+ {props.showChangeSet && changeSetUI?.elements &&
522
598
  <ChangeSetBox changeSet={changeSetUI} />
523
599
  }
524
600
  <div className='theia-ChatInput-Editor-Box'>
@@ -541,6 +617,7 @@ const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
541
617
  const buildChangeSetUI = (
542
618
  changeSet: ChangeSet,
543
619
  labelProvider: LabelProvider,
620
+ decoratorService: ChangeSetDecoratorService,
544
621
  actions: ChangeSetActionRenderer[],
545
622
  onDeleteChangeSet: () => void,
546
623
  onDeleteChangeSetElement: (index: number) => void
@@ -554,11 +631,12 @@ const buildChangeSetUI = (
554
631
  nameClass: `${element.type} ${element.state}`,
555
632
  name: element.name ?? labelProvider.getName(element.uri),
556
633
  additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
634
+ additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
557
635
  openChange: element?.openChange?.bind(element),
558
636
  apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
559
637
  revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
560
638
  delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
561
- })),
639
+ } satisfies ChangeSetUIElement)),
562
640
  actions
563
641
  });
564
642
 
@@ -567,6 +645,7 @@ interface ChangeSetUIElement {
567
645
  iconClass: string;
568
646
  nameClass: string;
569
647
  additionalInfo: string;
648
+ additionalInfoSuffixIcon?: string[];
570
649
  open?: () => void;
571
650
  openChange?: () => void;
572
651
  apply?: () => void;
@@ -596,15 +675,18 @@ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.
596
675
  <ul>
597
676
  {elements.map((element, index) => (
598
677
  <li key={index} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
599
- <div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`} />
600
- <span className='theia-ChatInput-ChangeSet-labelParts'>
678
+ <div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
679
+ </div>
680
+ <div className='theia-ChatInput-ChangeSet-labelParts'>
601
681
  <span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
602
682
  {element.name}
603
683
  </span>
604
- <span className='theia-ChatInput-ChangeSet-additionalInfo'>
605
- {element.additionalInfo}
606
- </span>
607
- </span>
684
+ <div className='theia-ChatInput-ChangeSet-additionalInfo'>
685
+ {element.additionalInfo && <span>{element.additionalInfo}</span>}
686
+ {element.additionalInfoSuffixIcon
687
+ && <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
688
+ </div>
689
+ </div>
608
690
  <div className='theia-ChatInput-ChangeSet-Actions'>
609
691
  {element.open && (
610
692
  <span
@@ -685,7 +767,12 @@ function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
685
767
  return requests.length > 0 ? requests[requests.length - 1] : undefined;
686
768
  }
687
769
 
688
- function buildContextUI(context: readonly AIVariableResolutionRequest[] | undefined, labelProvider: LabelProvider, onDeleteContextElement: (index: number) => void): ChatContextUI {
770
+ function buildContextUI(
771
+ context: readonly AIVariableResolutionRequest[] | undefined,
772
+ labelProvider: LabelProvider,
773
+ onDeleteContextElement: (index: number) => void,
774
+ onOpen: OpenContextElement
775
+ ): ChatContextUI {
689
776
  if (!context) {
690
777
  return { context: [] };
691
778
  }
@@ -697,6 +784,7 @@ function buildContextUI(context: readonly AIVariableResolutionRequest[] | undefi
697
784
  additionalInfo: labelProvider.getDetails(element),
698
785
  details: labelProvider.getLongName(element),
699
786
  delete: () => onDeleteContextElement(index),
787
+ open: () => onOpen(element)
700
788
  }))
701
789
  };
702
790
  }
@@ -719,13 +807,15 @@ const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
719
807
  {context.map((element, index) => (
720
808
  <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
721
809
  <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
722
- <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
723
- {element.name}
724
- </span>
725
- <span className='theia-ChatInput-ChatContext-additionalInfo'>
726
- {element.additionalInfo}
727
- </span>
728
- <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={() => element.delete()} />
810
+ <div className="theia-ChatInput-ChatContext-labelParts">
811
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
812
+ {element.name}
813
+ </span>
814
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
815
+ {element.additionalInfo}
816
+ </span>
817
+ </div>
818
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
729
819
  </li>
730
820
  ))}
731
821
  </ul>
@@ -13,7 +13,10 @@
13
13
  //
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
- import { RequestNode, ResponseNode } from './chat-tree-view';
16
+ import { Command, nls } from '@theia/core';
17
+ import { codicon } from '@theia/core/lib/browser';
18
+ import { isRequestNode, RequestNode, ResponseNode } from './chat-tree-view';
19
+ import { EditableChatRequestModel } from '@theia/ai-chat';
17
20
 
18
21
  export interface ChatNodeToolbarAction {
19
22
  /**
@@ -61,3 +64,39 @@ export interface ChatNodeToolbarActionContribution {
61
64
  */
62
65
  getToolbarActions(node: RequestNode | ResponseNode): ChatNodeToolbarAction[];
63
66
  }
67
+
68
+ export namespace ChatNodeToolbarCommands {
69
+ const CHAT_NODE_TOOLBAR_CATEGORY = 'ChatNodeToolbar';
70
+ const CHAT_NODE_TOOLBAR_CATEGORY_KEY = nls.getDefaultKey(CHAT_NODE_TOOLBAR_CATEGORY);
71
+
72
+ export const EDIT = Command.toLocalizedCommand({
73
+ id: 'chat:node:toolbar:edit-request',
74
+ category: CHAT_NODE_TOOLBAR_CATEGORY,
75
+ }, '', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
76
+
77
+ export const CANCEL = Command.toLocalizedCommand({
78
+ id: 'chat:node:toolbar:cancel-request',
79
+ category: CHAT_NODE_TOOLBAR_CATEGORY,
80
+ }, '', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
81
+ }
82
+
83
+ export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbarActionContribution {
84
+ getToolbarActions(node: RequestNode | ResponseNode): ChatNodeToolbarAction[] {
85
+ if (isRequestNode(node)) {
86
+ if (EditableChatRequestModel.isEditing(node.request)) {
87
+ return [{
88
+ commandId: ChatNodeToolbarCommands.CANCEL.id,
89
+ icon: codicon('close'),
90
+ tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/cancel', 'Cancel'),
91
+ }];
92
+ }
93
+ return [{
94
+ commandId: ChatNodeToolbarCommands.EDIT.id,
95
+ icon: codicon('edit'),
96
+ tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/edit', 'Edit'),
97
+ }];
98
+ } else {
99
+ return [];
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,40 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 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 { ChatProgressMessage } from '@theia/ai-chat';
18
+ import * as React from '@theia/core/shared/react';
19
+
20
+ export type ProgressMessageProps = Omit<ChatProgressMessage, 'kind' | 'id' | 'show'>;
21
+
22
+ export const ProgressMessage = (c: ProgressMessageProps) => (
23
+ <div className='theia-ResponseNode-ProgressMessage'>
24
+ <Indicator {...c} /> {c.content}
25
+ </div>
26
+ );
27
+
28
+ export const Indicator = (progressMessage: ProgressMessageProps) => (
29
+ <span className='theia-ResponseNode-ProgressMessage-Indicator'>
30
+ {progressMessage.status === 'inProgress' &&
31
+ <i className={'fa fa-spinner fa-spin ' + progressMessage.status}></i>
32
+ }
33
+ {progressMessage.status === 'completed' &&
34
+ <i className={'fa fa-check ' + progressMessage.status}></i>
35
+ }
36
+ {progressMessage.status === 'failed' &&
37
+ <i className={'fa fa-warning ' + progressMessage.status}></i>
38
+ }
39
+ </span>
40
+ );
@@ -26,7 +26,7 @@ import { ReactNode } from '@theia/core/shared/react';
26
26
  import { nls } from '@theia/core/lib/common/nls';
27
27
  import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
28
28
  import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
29
- import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
29
+ import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
30
30
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
31
31
  import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
32
32
  import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
@@ -206,11 +206,11 @@ export const CodeWrapper = (props: {
206
206
  }) => {
207
207
  // eslint-disable-next-line no-null/no-null
208
208
  const ref = React.useRef<HTMLDivElement | null>(null);
209
- const editorRef = React.useRef<MonacoEditor | undefined>(undefined);
209
+ const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
210
210
 
211
211
  const createInputElement = async () => {
212
212
  const resource = await props.untitledResourceResolver.createUntitledResource(undefined, props.language);
213
- const editor = await props.editorProvider.createInline(resource.uri, ref.current!, {
213
+ const editor = await props.editorProvider.createSimpleInline(resource.uri, ref.current!, {
214
214
  readOnly: true,
215
215
  autoSizing: true,
216
216
  scrollBeyondLastLine: false,
@@ -22,3 +22,4 @@ export * from './markdown-part-renderer';
22
22
  export * from './text-part-renderer';
23
23
  export * from './toolcall-part-renderer';
24
24
  export * from './thinking-part-renderer';
25
+ export * from './progress-part-renderer';
@@ -60,6 +60,10 @@ const MarkdownRender = ({ response, openerService }: { response: MarkdownChatRes
60
60
  return <div ref={ref}></div>;
61
61
  };
62
62
 
63
+ export interface DeclaredEventsEventListenerObject extends EventListenerObject {
64
+ handledEvents?: (keyof HTMLElementEventMap)[];
65
+ }
66
+
63
67
  /**
64
68
  * This hook uses markdown-it directly to render markdown.
65
69
  * The reason to use markdown-it directly is that the MarkdownRenderer is
@@ -72,9 +76,17 @@ const MarkdownRender = ({ response, openerService }: { response: MarkdownChatRes
72
76
  * @param markdown the string to render as markdown
73
77
  * @param skipSurroundingParagraph whether to remove a surrounding paragraph element (default: false)
74
78
  * @param openerService the service to handle link opening
79
+ * @param eventHandler `handleEvent` will be called by default for `click` events and additionally
80
+ * for all events enumerated in {@link DeclaredEventsEventListenerObject.handledEvents}. If `handleEvent` returns `true`,
81
+ * no additional handlers will be run for the event.
75
82
  * @returns the ref to use in an element to render the markdown
76
83
  */
77
- export const useMarkdownRendering = (markdown: string | MarkdownString, openerService: OpenerService, skipSurroundingParagraph: boolean = false) => {
84
+ export const useMarkdownRendering = (
85
+ markdown: string | MarkdownString,
86
+ openerService: OpenerService,
87
+ skipSurroundingParagraph: boolean = false,
88
+ eventHandler?: DeclaredEventsEventListenerObject
89
+ ) => {
78
90
  // null is valid in React
79
91
  // eslint-disable-next-line no-null/no-null
80
92
  const ref = useRef<HTMLDivElement | null>(null);
@@ -98,6 +110,7 @@ export const useMarkdownRendering = (markdown: string | MarkdownString, openerSe
98
110
 
99
111
  // intercept link clicks to use the Theia OpenerService instead of the default browser behavior
100
112
  const handleClick = (event: MouseEvent) => {
113
+ if ((eventHandler?.handleEvent(event) as unknown) === true) {return; }
101
114
  let target = event.target as HTMLElement;
102
115
  while (target && target.tagName !== 'A') {
103
116
  target = target.parentElement as HTMLElement;
@@ -112,7 +125,11 @@ export const useMarkdownRendering = (markdown: string | MarkdownString, openerSe
112
125
  };
113
126
 
114
127
  ref?.current?.addEventListener('click', handleClick);
115
- return () => ref.current?.removeEventListener('click', handleClick);
128
+ eventHandler?.handledEvents?.forEach(eventType => eventType !== 'click' && ref?.current?.addEventListener(eventType, eventHandler));
129
+ return () => {
130
+ ref.current?.removeEventListener('click', handleClick);
131
+ eventHandler?.handledEvents?.forEach(eventType => eventType !== 'click' && ref?.current?.removeEventListener(eventType, eventHandler));
132
+ };
116
133
  }, [markdownString, skipSurroundingParagraph, openerService]);
117
134
 
118
135
  return ref;