@theia/ai-chat-ui 1.63.0-next.52 → 1.63.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +2 -0
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +37 -9
  4. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  5. package/lib/browser/chat-input-widget.d.ts +18 -11
  6. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  7. package/lib/browser/chat-input-widget.js +121 -18
  8. package/lib/browser/chat-input-widget.js.map +1 -1
  9. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +4 -1
  10. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  11. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +58 -25
  12. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  13. package/lib/browser/chat-view-contribution.d.ts +2 -0
  14. package/lib/browser/chat-view-contribution.d.ts.map +1 -1
  15. package/lib/browser/chat-view-contribution.js +29 -17
  16. package/lib/browser/chat-view-contribution.js.map +1 -1
  17. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts +2 -0
  18. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
  19. package/lib/browser/chat-view-widget-toolbar-contribution.js +13 -5
  20. package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
  21. package/package.json +11 -11
  22. package/src/browser/ai-chat-ui-contribution.ts +28 -10
  23. package/src/browser/chat-input-widget.tsx +148 -29
  24. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +86 -51
  25. package/src/browser/chat-view-contribution.ts +28 -17
  26. package/src/browser/chat-view-widget-toolbar-contribution.tsx +12 -5
  27. package/src/browser/style/index.css +34 -5
@@ -20,19 +20,21 @@ import {
20
20
  import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
21
21
  import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
22
22
  import { AIVariableResolutionRequest } from '@theia/ai-core';
23
- import { FrontendVariableService } from '@theia/ai-core/lib/browser';
24
- import { DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
23
+ import { AgentCompletionNotificationService, FrontendVariableService, AIActivationService } from '@theia/ai-core/lib/browser';
24
+ import { DisposableCollection, Emitter, InMemoryResources, URI, nls } from '@theia/core';
25
25
  import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
26
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
27
27
  import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
28
28
  import * as React from '@theia/core/shared/react';
29
- import { IMouseEvent } from '@theia/monaco-editor-core';
29
+ import { IMouseEvent, Range } from '@theia/monaco-editor-core';
30
30
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
31
31
  import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
32
32
  import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
33
33
  import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
34
34
  import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
35
35
  import { ContextVariablePicker } from './context-variable-picker';
36
+ import { TASK_CONTEXT_VARIABLE } from '@theia/ai-chat/lib/browser/task-context-variable';
37
+ import { IModelDeltaDecoration } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
36
38
 
37
39
  type Query = (query: string) => Promise<void>;
38
40
  type Unpin = () => void;
@@ -78,6 +80,9 @@ export class AIChatInputWidget extends ReactWidget {
78
80
  @inject(ChangeSetActionService)
79
81
  protected readonly changeSetActionService: ChangeSetActionService;
80
82
 
83
+ @inject(AgentCompletionNotificationService)
84
+ protected readonly agentNotificationService: AgentCompletionNotificationService;
85
+
81
86
  @inject(ChangeSetDecoratorService)
82
87
  protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
83
88
 
@@ -87,12 +92,16 @@ export class AIChatInputWidget extends ReactWidget {
87
92
  @inject(ChatService)
88
93
  protected readonly chatService: ChatService;
89
94
 
95
+ @inject(AIActivationService)
96
+ protected readonly aiActivationService: AIActivationService;
97
+
90
98
  protected editorRef: SimpleMonacoEditor | undefined = undefined;
91
99
  protected readonly editorReady = new Deferred<void>();
92
100
 
93
101
  protected isEnabled = false;
102
+ protected heightInLines = 12;
94
103
 
95
- private _branch?: ChatHierarchyBranch;
104
+ protected _branch?: ChatHierarchyBranch;
96
105
  set branch(branch: ChatHierarchyBranch | undefined) {
97
106
  if (this._branch !== branch) {
98
107
  this._branch = branch;
@@ -100,34 +109,34 @@ export class AIChatInputWidget extends ReactWidget {
100
109
  }
101
110
  }
102
111
 
103
- private _onQuery: Query;
112
+ protected _onQuery: Query;
104
113
  set onQuery(query: Query) {
105
114
  this._onQuery = query;
106
115
  }
107
- private _onUnpin: Unpin;
116
+ protected _onUnpin: Unpin;
108
117
  set onUnpin(unpin: Unpin) {
109
118
  this._onUnpin = unpin;
110
119
  }
111
- private _onCancel: Cancel;
120
+ protected _onCancel: Cancel;
112
121
  set onCancel(cancel: Cancel) {
113
122
  this._onCancel = cancel;
114
123
  }
115
- private _onDeleteChangeSet: DeleteChangeSet;
124
+ protected _onDeleteChangeSet: DeleteChangeSet;
116
125
  set onDeleteChangeSet(deleteChangeSet: DeleteChangeSet) {
117
126
  this._onDeleteChangeSet = deleteChangeSet;
118
127
  }
119
- private _onDeleteChangeSetElement: DeleteChangeSetElement;
128
+ protected _onDeleteChangeSetElement: DeleteChangeSetElement;
120
129
  set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
121
130
  this._onDeleteChangeSetElement = deleteChangeSetElement;
122
131
  }
123
132
 
124
- private _initialValue?: string;
133
+ protected _initialValue?: string;
125
134
  set initialValue(value: string | undefined) {
126
135
  this._initialValue = value;
127
136
  }
128
137
 
129
138
  protected onDisposeForChatModel = new DisposableCollection();
130
- private _chatModel: ChatModel;
139
+ protected _chatModel: ChatModel;
131
140
  set chatModel(chatModel: ChatModel) {
132
141
  this.onDisposeForChatModel.dispose();
133
142
  this.onDisposeForChatModel = new DisposableCollection();
@@ -139,17 +148,25 @@ export class AIChatInputWidget extends ReactWidget {
139
148
  this._chatModel = chatModel;
140
149
  this.update();
141
150
  }
142
- private _pinnedAgent: ChatAgent | undefined;
151
+ protected _pinnedAgent: ChatAgent | undefined;
143
152
  set pinnedAgent(pinnedAgent: ChatAgent | undefined) {
144
153
  this._pinnedAgent = pinnedAgent;
145
154
  this.update();
146
155
  }
147
156
 
157
+ protected onDidResizeEmitter = new Emitter<void>();
158
+ readonly onDidResize = this.onDidResizeEmitter.event;
159
+
148
160
  @postConstruct()
149
161
  protected init(): void {
150
162
  this.id = AIChatInputWidget.ID;
151
163
  this.title.closable = false;
152
164
  this.toDispose.push(this.resources.add(this.getResourceUri(), ''));
165
+ this.toDispose.push(this.aiActivationService.onDidChangeActiveStatus(() => {
166
+ this.setEnabled(this.aiActivationService.isActive);
167
+ }));
168
+ this.toDispose.push(this.onDidResizeEmitter);
169
+ this.setEnabled(this.aiActivationService.isActive);
153
170
  this.update();
154
171
  }
155
172
 
@@ -162,6 +179,18 @@ export class AIChatInputWidget extends ReactWidget {
162
179
  });
163
180
  }
164
181
 
182
+ protected async handleAgentCompletion(request: ChatRequestModel): Promise<void> {
183
+ try {
184
+ const agentId = request.agentId;
185
+
186
+ if (agentId) {
187
+ await this.agentNotificationService.showCompletionNotification(agentId);
188
+ }
189
+ } catch (error) {
190
+ console.error('Failed to handle agent completion notification:', error);
191
+ }
192
+ }
193
+
165
194
  protected getResourceUri(): URI {
166
195
  return new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
167
196
  }
@@ -187,12 +216,14 @@ export class AIChatInputWidget extends ReactWidget {
187
216
  onDragOver={this.onDragOver.bind(this)}
188
217
  onDrop={this.onDrop.bind(this)}
189
218
  onPaste={this.onPaste.bind(this)}
219
+ onEscape={this.onEscape.bind(this)}
190
220
  onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
191
221
  onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
192
222
  onAddContextElement={this.addContextElement.bind(this)}
193
223
  onDeleteContextElement={this.deleteContextElement.bind(this)}
194
224
  onOpenContextElement={this.openContextElement.bind(this)}
195
225
  context={this.getContext()}
226
+ onAgentCompletion={this.handleAgentCompletion.bind(this)}
196
227
  chatModel={this._chatModel}
197
228
  pinnedAgent={this._pinnedAgent}
198
229
  editorProvider={this.editorProvider}
@@ -216,11 +247,13 @@ export class AIChatInputWidget extends ReactWidget {
216
247
  currentRequest={currentRequest}
217
248
  isEditing={isEditing}
218
249
  pending={pending}
250
+ heightInLines={this.heightInLines}
219
251
  onResponseChanged={() => {
220
252
  if (isPending() !== pending) {
221
253
  this.update();
222
254
  }
223
255
  }}
256
+ onResize={() => this.onDidResizeEmitter.fire()}
224
257
  />
225
258
  );
226
259
  }
@@ -279,6 +312,10 @@ export class AIChatInputWidget extends ReactWidget {
279
312
  });
280
313
  }
281
314
 
315
+ protected onEscape(): void {
316
+ // No op
317
+ }
318
+
282
319
  protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
283
320
  const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
284
321
  const context = { session };
@@ -333,7 +370,9 @@ interface ChatInputProperties {
333
370
  onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
334
371
  onAddContextElement: () => void;
335
372
  onDeleteContextElement: (index: number) => void;
373
+ onEscape: () => void;
336
374
  onOpenContextElement: OpenContextElement;
375
+ onAgentCompletion: (request: ChatRequestModel) => void;
337
376
  context?: readonly AIVariableResolutionRequest[];
338
377
  isEnabled?: boolean;
339
378
  chatModel: ChatModel;
@@ -355,9 +394,16 @@ interface ChatInputProperties {
355
394
  currentRequest?: ChatRequestModel;
356
395
  isEditing: boolean;
357
396
  pending: boolean;
397
+ heightInLines?: number;
358
398
  onResponseChanged: () => void;
399
+ onResize: () => void;
359
400
  }
360
401
 
402
+ // Utility to check if we have task context in the chat model
403
+ const hasTaskContext = (chatModel: ChatModel): boolean => chatModel.context.getVariables().some(variable =>
404
+ variable.variable?.id === TASK_CONTEXT_VARIABLE.id
405
+ );
406
+
361
407
  const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
362
408
  const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
363
409
  const onDeleteChangeSetElement = (uri: URI) => props.onDeleteChangeSetElement(props.chatModel.id, uri);
@@ -381,6 +427,17 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
381
427
  // eslint-disable-next-line no-null/no-null
382
428
  const containerRef = React.useRef<HTMLDivElement>(null);
383
429
 
430
+ // On the first request of the chat, if the chat has a task context and a pinned
431
+ // agent, show a "Perform this task." placeholder which is the message to send by default
432
+ const isFirstRequest = props.chatModel.getRequests().length === 0;
433
+ const shouldUseTaskPlaceholder = isFirstRequest && props.pinnedAgent && hasTaskContext(props.chatModel);
434
+ const taskPlaceholder = nls.localize('theia/ai/chat-ui/performThisTask', 'Perform this task.');
435
+ const placeholderText = !props.isEnabled
436
+ ? nls.localize('theia/ai/chat-ui/aiDisabled', 'AI features are disabled')
437
+ : shouldUseTaskPlaceholder
438
+ ? taskPlaceholder
439
+ : nls.localizeByDefault('Ask a question');
440
+
384
441
  // Handle paste events on the container
385
442
  const handlePaste = React.useCallback((event: ClipboardEvent) => {
386
443
  props.onPaste(event);
@@ -403,7 +460,8 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
403
460
  const createInputElement = async () => {
404
461
  const paddingTop = 6;
405
462
  const lineHeight = 20;
406
- const maxHeight = 240;
463
+ const maxHeightPx = (props.heightInLines ?? 12) * lineHeight;
464
+
407
465
  const editor = await props.editorProvider.createSimpleInline(uri, editorContainerRef.current!, {
408
466
  language: CHAT_VIEW_LANGUAGE_EXTENSION,
409
467
  // Disable code lens, inlay hints and hover support to avoid console errors from other contributions
@@ -438,12 +496,17 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
438
496
  if (editorContainerRef.current) {
439
497
  editorContainerRef.current.style.overflowY = 'auto'; // ensure vertical scrollbar
440
498
  editorContainerRef.current.style.height = (lineHeight + (2 * paddingTop)) + 'px';
499
+
500
+ editorContainerRef.current.addEventListener('wheel', e => {
501
+ // Prevent parent from scrolling
502
+ e.stopPropagation();
503
+ }, { passive: false });
441
504
  }
442
505
 
443
506
  const updateEditorHeight = () => {
444
507
  if (editorContainerRef.current) {
445
508
  const contentHeight = editor.getControl().getContentHeight() + paddingTop;
446
- editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeight)}px`;
509
+ editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeightPx)}px`;
447
510
  }
448
511
  };
449
512
 
@@ -453,7 +516,10 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
453
516
  updateEditorHeight();
454
517
  handleOnChange();
455
518
  });
456
- const resizeObserver = new ResizeObserver(updateEditorHeight);
519
+ const resizeObserver = new ResizeObserver(() => {
520
+ updateEditorHeight();
521
+ props.onResize();
522
+ });
457
523
  if (editorContainerRef.current) {
458
524
  resizeObserver.observe(editorContainerRef.current);
459
525
  }
@@ -465,12 +531,46 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
465
531
  props.contextMenuCallback(e.event)
466
532
  );
467
533
 
534
+ const updateLineCounts = () => {
535
+ // We need the line numbers to allow scrolling by using the keyboard
536
+ const model = editor.getControl().getModel()!;
537
+ const lineCount = model.getLineCount();
538
+ const decorations: IModelDeltaDecoration[] = [];
539
+
540
+ for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
541
+ decorations.push({
542
+ range: new Range(lineNumber, 1, lineNumber, 1),
543
+ options: {
544
+ description: `line-number-${lineNumber}`,
545
+ isWholeLine: false,
546
+ className: `line-number-${lineNumber}`,
547
+ }
548
+ });
549
+ }
550
+
551
+ const lineNumbers = model.getAllDecorations().filter(predicate => predicate.options.description?.startsWith('line-number-'));
552
+ editor.getControl().removeDecorations(lineNumbers.map(d => d.id));
553
+ editor.getControl().createDecorationsCollection(decorations);
554
+ };
555
+
556
+ editor.getControl().getModel()?.onDidChangeContent(() => {
557
+ updateLineCounts();
558
+ });
559
+
560
+ editor.getControl().onDidChangeCursorPosition(e => {
561
+ const lineNumber = e.position.lineNumber;
562
+ const line = editor.getControl().getDomNode()?.querySelector(`.line-number-${lineNumber}`);
563
+ line?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
564
+ });
565
+
468
566
  editorRef.current = editor;
469
567
  props.setEditorRef(editor);
470
568
 
471
569
  if (props.initialValue) {
472
570
  setValue(props.initialValue);
473
571
  }
572
+
573
+ updateLineCounts();
474
574
  };
475
575
  createInputElement();
476
576
 
@@ -502,6 +602,15 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
502
602
  onDeleteChangeSetElement
503
603
  ));
504
604
  }
605
+ if (event.kind === 'addRequest') {
606
+ // Listen for when this request's response becomes complete
607
+ const responseListener = event.request.response.onDidChange(() => {
608
+ if (event.request.response.isComplete) {
609
+ props.onAgentCompletion(event.request);
610
+ responseListener.dispose(); // Clean up the listener once notification is sent
611
+ }
612
+ });
613
+ }
505
614
  });
506
615
  return () => {
507
616
  listener.dispose();
@@ -536,18 +645,21 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
536
645
  }
537
646
  }, [editorRef]);
538
647
 
648
+ // Without user input, if we can default to "Perform this task.", do so
539
649
  const submit = React.useCallback(function submit(value: string): void {
540
- if (!value || value.trim().length === 0) {
650
+ let effectiveValue = value;
651
+ if ((!value || value.trim().length === 0) && shouldUseTaskPlaceholder) {
652
+ effectiveValue = taskPlaceholder;
653
+ }
654
+ if (!effectiveValue || effectiveValue.trim().length === 0) {
541
655
  return;
542
656
  }
543
-
544
- props.onQuery(value);
657
+ props.onQuery(effectiveValue);
545
658
  setValue('');
546
-
547
- if (editorRef.current) {
659
+ if (editorRef.current && !editorRef.current.document.textEditorModel.isDisposed()) {
548
660
  editorRef.current.document.textEditorModel.setValue('');
549
661
  }
550
- }, [props.context, props.onQuery, setValue]);
662
+ }, [props.context, props.onQuery, setValue, shouldUseTaskPlaceholder, taskPlaceholder]);
551
663
 
552
664
  const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
553
665
  if (!props.isEnabled) {
@@ -555,7 +667,12 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
555
667
  }
556
668
  if (event.key === 'Enter' && !event.shiftKey) {
557
669
  event.preventDefault();
558
- submit(editorRef.current?.document.textEditorModel.getValue() || '');
670
+ // On Enter, read input and submit (handles task context)
671
+ const currentValue = editorRef.current?.document.textEditorModel.getValue() || '';
672
+ submit(currentValue);
673
+ } else if (event.key === 'Escape') {
674
+ event.preventDefault();
675
+ props.onEscape();
559
676
  }
560
677
  }, [props.isEnabled, submit]);
561
678
 
@@ -606,7 +723,8 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
606
723
  ? [{
607
724
  title: nls.localize('theia/ai/chat-ui/attachToContext', 'Attach elements to context'),
608
725
  handler: () => props.onAddContextElement(),
609
- className: 'codicon-add'
726
+ className: 'codicon-add',
727
+ disabled: !props.isEnabled
610
728
  }]
611
729
  : []),
612
730
  ...(props.showPinnedAgent
@@ -614,6 +732,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
614
732
  title: props.pinnedAgent ? nls.localize('theia/ai/chat-ui/unpinAgent', 'Unpin Agent') : nls.localize('theia/ai/chat-ui/pinAgent', 'Pin Agent'),
615
733
  handler: props.pinnedAgent ? props.onUnpin : handlePin,
616
734
  className: 'at-icon',
735
+ disabled: !props.isEnabled,
617
736
  text: {
618
737
  align: 'right',
619
738
  content: props.pinnedAgent && props.pinnedAgent.name
@@ -640,7 +759,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
640
759
  }
641
760
  },
642
761
  className: 'codicon-send',
643
- disabled: isInputEmpty || !props.isEnabled
762
+ disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
644
763
  }];
645
764
  } else if (pending) {
646
765
  rightOptions = [{
@@ -661,21 +780,21 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
661
780
  }
662
781
  },
663
782
  className: 'codicon-send',
664
- disabled: isInputEmpty || !props.isEnabled
783
+ disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
665
784
  }];
666
785
  }
667
786
 
668
787
  const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
669
788
 
670
789
  return (
671
- <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
790
+ <div className='theia-ChatInput' data-ai-disabled={!props.isEnabled} onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
672
791
  {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
673
792
  {props.showChangeSet && changeSetUI?.elements &&
674
793
  <ChangeSetBox changeSet={changeSetUI} />
675
794
  }
676
795
  <div className='theia-ChatInput-Editor-Box'>
677
796
  <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
678
- <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
797
+ <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{placeholderText}</div>
679
798
  </div>
680
799
  {props.context && props.context.length > 0 &&
681
800
  <ChatContext context={contextUI.context} />
@@ -829,7 +948,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
829
948
  {leftOptions.map((option, index) => (
830
949
  <span
831
950
  key={index}
832
- className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
951
+ className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
833
952
  title={option.title}
834
953
  onClick={option.handler}
835
954
  >
@@ -842,7 +961,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
842
961
  {rightOptions.map((option, index) => (
843
962
  <span
844
963
  key={index}
845
- className={`option ${option.disabled ? 'disabled' : ''} ${option.text?.align === 'right' ? 'reverse' : ''}`}
964
+ className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
846
965
  title={option.title}
847
966
  onClick={option.handler}
848
967
  >
@@ -19,11 +19,13 @@ import { inject, injectable } from '@theia/core/shared/inversify';
19
19
  import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
20
20
  import { ReactNode } from '@theia/core/shared/react';
21
21
  import { nls } from '@theia/core/lib/common/nls';
22
- import { codicon } from '@theia/core/lib/browser';
22
+ import { codicon, OpenerService } from '@theia/core/lib/browser';
23
23
  import * as React from '@theia/core/shared/react';
24
24
  import { ToolConfirmation, ToolConfirmationState } from './tool-confirmation';
25
25
  import { ToolConfirmationManager, ToolConfirmationMode } from '@theia/ai-chat/lib/browser/chat-tool-preferences';
26
26
  import { ResponseNode } from '../chat-tree-view';
27
+ import { useMarkdownRendering } from './markdown-part-renderer';
28
+ import { ToolCallResult } from '@theia/ai-core';
27
29
 
28
30
  @injectable()
29
31
  export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {
@@ -31,6 +33,9 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
31
33
  @inject(ToolConfirmationManager)
32
34
  protected toolConfirmationManager: ToolConfirmationManager;
33
35
 
36
+ @inject(OpenerService)
37
+ protected openerService: OpenerService;
38
+
34
39
  canHandle(response: ChatResponseContent): number {
35
40
  if (ToolCallChatResponseContent.is(response)) {
36
41
  return 10;
@@ -47,7 +52,52 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
47
52
  toolConfirmationManager={this.toolConfirmationManager}
48
53
  chatId={chatId}
49
54
  renderCollapsibleArguments={this.renderCollapsibleArguments.bind(this)}
50
- tryPrettyPrintJson={this.tryPrettyPrintJson.bind(this)} />;
55
+ responseRenderer={this.renderResult.bind(this)} />;
56
+ }
57
+
58
+ protected renderResult(response: ToolCallChatResponseContent): ReactNode {
59
+ const result = this.tryParse(response.result);
60
+ if (!result) {
61
+ return undefined;
62
+ }
63
+ if (typeof result === 'string') {
64
+ return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
65
+ }
66
+ if ('content' in result) {
67
+ return <div className='theia-toolCall-response-content'>
68
+ {result.content.map((content, idx) => {
69
+ switch (content.type) {
70
+ case 'image': {
71
+ return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-image-result'>
72
+ <img src={`data:${content.mimeType};base64,${content.base64data}`} />
73
+ </div>;
74
+ }
75
+ case 'text': {
76
+ return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-text-result'>
77
+ <MarkdownRender text={content.text} openerService={this.openerService} />
78
+ </div>;
79
+ }
80
+ case 'audio':
81
+ case 'error':
82
+ default: {
83
+ return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-default-result'><pre>{JSON.stringify(response, undefined, 2)}</pre></div>;
84
+ }
85
+ }
86
+ })}
87
+ </div>;
88
+ }
89
+ return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
90
+ }
91
+
92
+ private tryParse(result: ToolCallResult): ToolCallResult {
93
+ if (!result) {
94
+ return undefined;
95
+ }
96
+ try {
97
+ return typeof result === 'string' ? JSON.parse(result) : result;
98
+ } catch (error) {
99
+ return result;
100
+ }
51
101
  }
52
102
 
53
103
  protected getToolConfirmationSettings(responseId: string, chatId: string): ToolConfirmationMode {
@@ -75,29 +125,6 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
75
125
  return args;
76
126
  }
77
127
  }
78
-
79
- private tryPrettyPrintJson(response: ToolCallChatResponseContent): string | undefined {
80
- let responseContent = response.result;
81
- try {
82
- if (responseContent) {
83
- if (typeof responseContent === 'string') {
84
- responseContent = JSON.parse(responseContent);
85
- }
86
- responseContent = JSON.stringify(responseContent, undefined, 2);
87
- }
88
- } catch (e) {
89
- if (typeof responseContent !== 'string') {
90
- responseContent = nls.localize(
91
- 'theia/ai/chat-ui/toolcall-part-renderer/prettyPrintError',
92
- "The content could not be converted to string: '{0}'. This is the original content: '{1}'.",
93
- e.message,
94
- responseContent
95
- );
96
- }
97
- // fall through
98
- }
99
- return responseContent;
100
- }
101
128
  }
102
129
 
103
130
  const Spinner = () => (
@@ -110,13 +137,13 @@ interface ToolCallContentProps {
110
137
  toolConfirmationManager: ToolConfirmationManager;
111
138
  chatId: string;
112
139
  renderCollapsibleArguments: (args: string | undefined) => ReactNode;
113
- tryPrettyPrintJson: (response: ToolCallChatResponseContent) => string | undefined;
140
+ responseRenderer: (response: ToolCallChatResponseContent) => ReactNode | undefined;
114
141
  }
115
142
 
116
143
  /**
117
144
  * A function component to handle tool call rendering and confirmation
118
145
  */
119
- const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId, tryPrettyPrintJson, renderCollapsibleArguments }) => {
146
+ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId, responseRenderer, renderCollapsibleArguments }) => {
120
147
  const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
121
148
 
122
149
  React.useEffect(() => {
@@ -163,35 +190,43 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
163
190
 
164
191
  return (
165
192
  <div className='theia-toolCall'>
166
- <h4>
167
- {confirmationState === 'denied' ? (
168
- <span className="theia-tool-denied">
169
- <span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolcall-part-renderer/denied', 'Execution denied')}: {response.name}
193
+ {confirmationState === 'denied' ? (
194
+ <span className='theia-toolCall-denied'>
195
+ <span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolcall-part-renderer/denied', 'Execution denied')}: {response.name}
196
+ </span>
197
+ ) : response.finished ? (
198
+ <details className='theia-toolCall-finished'>
199
+ <summary>
200
+ {nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
201
+ ({renderCollapsibleArguments(response.arguments)})
202
+ </summary>
203
+ <div className='theia-toolCall-response-result'>
204
+ {responseRenderer(response)}
205
+ </div>
206
+ </details>
207
+ ) : (
208
+ confirmationState === 'allowed' && (
209
+ <span className='theia-toolCall-allowed'>
210
+ <Spinner /> {nls.localizeByDefault('Running')} {response.name}
170
211
  </span>
171
- ) : response.finished ? (
172
- <details>
173
- <summary>{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
174
- ({renderCollapsibleArguments(response.arguments)})
175
- </summary>
176
- <pre>{tryPrettyPrintJson(response)}</pre>
177
- </details>
178
- ) : (
179
- confirmationState === 'allowed' && (
180
- <span>
181
- <Spinner /> {nls.localizeByDefault('Running')} {response.name}
182
- </span>
183
- )
184
- )}
185
- </h4>
212
+ )
213
+ )}
186
214
 
187
215
  {/* Show confirmation UI when waiting for allow */}
188
216
  {confirmationState === 'waiting' && (
189
- <ToolConfirmation
190
- response={response}
191
- onAllow={handleAllow}
192
- onDeny={handleDeny}
193
- />
217
+ <span className='theia-toolCall-waiting'>
218
+ <ToolConfirmation
219
+ response={response}
220
+ onAllow={handleAllow}
221
+ onDeny={handleDeny}
222
+ />
223
+ </span>
194
224
  )}
195
225
  </div>
196
226
  );
197
227
  };
228
+
229
+ const MarkdownRender = ({ text, openerService }: { text: string; openerService: OpenerService }) => {
230
+ const ref = useMarkdownRendering(text, openerService);
231
+ return <div ref={ref}></div>;
232
+ };