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

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 (62) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +29 -1
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +156 -0
  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 +8 -0
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/chat-input-widget.d.ts +7 -6
  9. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  10. package/lib/browser/chat-input-widget.js +81 -22
  11. package/lib/browser/chat-input-widget.js.map +1 -1
  12. package/lib/browser/chat-node-toolbar-action-contribution.d.ts +1 -0
  13. package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
  14. package/lib/browser/chat-node-toolbar-action-contribution.js +13 -0
  15. package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
  16. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +14 -0
  17. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -0
  18. package/lib/browser/chat-response-renderer/delegation-response-renderer.js +144 -0
  19. package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -0
  20. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  21. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  22. package/lib/browser/chat-response-renderer/index.js +1 -0
  23. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  24. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +2 -2
  25. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  26. package/lib/browser/chat-response-renderer/tool-confirmation.js +23 -23
  27. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  28. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +9 -9
  29. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  30. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +3 -1
  31. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  32. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +50 -8
  33. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  34. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
  35. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
  36. package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
  37. package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
  38. package/lib/browser/chat-view-commands.d.ts +1 -0
  39. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  40. package/lib/browser/chat-view-commands.js +5 -0
  41. package/lib/browser/chat-view-commands.js.map +1 -1
  42. package/lib/browser/chat-view-contribution.js +2 -1
  43. package/lib/browser/chat-view-contribution.js.map +1 -1
  44. package/lib/browser/chat-view-widget.d.ts +1 -1
  45. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  46. package/lib/browser/chat-view-widget.js +1 -4
  47. package/lib/browser/chat-view-widget.js.map +1 -1
  48. package/package.json +10 -10
  49. package/src/browser/ai-chat-ui-contribution.ts +164 -3
  50. package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
  51. package/src/browser/chat-input-widget.tsx +111 -35
  52. package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
  53. package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
  54. package/src/browser/chat-response-renderer/index.ts +1 -0
  55. package/src/browser/chat-response-renderer/tool-confirmation.tsx +30 -30
  56. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +10 -10
  57. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +58 -8
  58. package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
  59. package/src/browser/chat-view-commands.ts +6 -0
  60. package/src/browser/chat-view-contribution.ts +1 -1
  61. package/src/browser/chat-view-widget.tsx +2 -5
  62. package/src/browser/style/index.css +178 -3
@@ -14,24 +14,25 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
  import {
17
- ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel,
18
- ChatService, ChatSuggestion, EditableChatRequestModel, ChatHierarchyBranch
17
+ ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatHierarchyBranch,
18
+ ChatModel, ChatRequestModel, ChatService, ChatSuggestion, EditableChatRequestModel
19
19
  } from '@theia/ai-chat';
20
+ import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
21
+ import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
22
+ import { AIVariableResolutionRequest } from '@theia/ai-core';
23
+ import { FrontendVariableService } from '@theia/ai-core/lib/browser';
20
24
  import { DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
21
25
  import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
22
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
23
27
  import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
24
28
  import * as React from '@theia/core/shared/react';
25
29
  import { IMouseEvent } from '@theia/monaco-editor-core';
26
- import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
27
30
  import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
28
- import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
29
- import { AIVariableResolutionRequest } from '@theia/ai-core';
30
- import { FrontendVariableService } from '@theia/ai-core/lib/browser';
31
- import { ContextVariablePicker } from './context-variable-picker';
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
- import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
34
33
  import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
34
+ import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
35
+ import { ContextVariablePicker } from './context-variable-picker';
35
36
 
36
37
  type Query = (query: string) => Promise<void>;
37
38
  type Unpin = () => void;
@@ -185,6 +186,7 @@ export class AIChatInputWidget extends ReactWidget {
185
186
  onCancel={this._onCancel.bind(this)}
186
187
  onDragOver={this.onDragOver.bind(this)}
187
188
  onDrop={this.onDrop.bind(this)}
189
+ onPaste={this.onPaste.bind(this)}
188
190
  onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
189
191
  onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
190
192
  onAddContextElement={this.addContextElement.bind(this)}
@@ -257,6 +259,26 @@ export class AIChatInputWidget extends ReactWidget {
257
259
  });
258
260
  }
259
261
 
262
+ protected onPaste(event: ClipboardEvent): void {
263
+ this.variableService.getPasteResult(event, { type: 'ai-chat-input-widget' }).then(result => {
264
+ result.variables.forEach(variable => this.addContext(variable));
265
+ if (result.text) {
266
+ const position = this.editorRef?.getControl().getPosition();
267
+ if (position && result.text) {
268
+ this.editorRef?.getControl().executeEdits('paste', [{
269
+ range: {
270
+ startLineNumber: position.lineNumber,
271
+ startColumn: position.column,
272
+ endLineNumber: position.lineNumber,
273
+ endColumn: position.column
274
+ },
275
+ text: result.text
276
+ }]);
277
+ }
278
+ }
279
+ });
280
+ }
281
+
260
282
  protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
261
283
  const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
262
284
  const context = { session };
@@ -306,6 +328,7 @@ interface ChatInputProperties {
306
328
  onUnpin: () => void;
307
329
  onDragOver: (event: React.DragEvent) => void;
308
330
  onDrop: (event: React.DragEvent) => void;
331
+ onPaste: (event: ClipboardEvent) => void;
309
332
  onDeleteChangeSet: (sessionId: string) => void;
310
333
  onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
311
334
  onAddContextElement: () => void;
@@ -355,6 +378,25 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
355
378
  // eslint-disable-next-line no-null/no-null
356
379
  const placeholderRef = React.useRef<HTMLDivElement | null>(null);
357
380
  const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
381
+ // eslint-disable-next-line no-null/no-null
382
+ const containerRef = React.useRef<HTMLDivElement>(null);
383
+
384
+ // Handle paste events on the container
385
+ const handlePaste = React.useCallback((event: ClipboardEvent) => {
386
+ props.onPaste(event);
387
+ }, [props.onPaste]);
388
+
389
+ // Set up paste handler on the container div
390
+ React.useEffect(() => {
391
+ const container = containerRef.current;
392
+ if (container) {
393
+ container.addEventListener('paste', handlePaste, true);
394
+ return () => {
395
+ container.removeEventListener('paste', handlePaste, true);
396
+ };
397
+ }
398
+ return undefined;
399
+ }, [handlePaste]);
358
400
 
359
401
  React.useEffect(() => {
360
402
  const uri = props.uri;
@@ -472,7 +514,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
472
514
  setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
473
515
  });
474
516
  return () => disposable.dispose();
475
- });
517
+ }, [props.actionService, props.chatModel.changeSet]);
476
518
 
477
519
  React.useEffect(() => {
478
520
  const disposable = props.decoratorService.onDidChangeDecorations(() => {
@@ -498,8 +540,13 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
498
540
  if (!value || value.trim().length === 0) {
499
541
  return;
500
542
  }
543
+
501
544
  props.onQuery(value);
502
545
  setValue('');
546
+
547
+ if (editorRef.current) {
548
+ editorRef.current.document.textEditorModel.setValue('');
549
+ }
503
550
  }, [props.context, props.onQuery, setValue]);
504
551
 
505
552
  const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
@@ -620,21 +667,23 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
620
667
 
621
668
  const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
622
669
 
623
- return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
624
- {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
625
- {props.showChangeSet && changeSetUI?.elements &&
626
- <ChangeSetBox changeSet={changeSetUI} />
627
- }
628
- <div className='theia-ChatInput-Editor-Box'>
629
- <div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
630
- <div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
631
- </div>
632
- {props.context && props.context.length > 0 &&
633
- <ChatContext context={contextUI.context} />
670
+ return (
671
+ <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
672
+ {props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
673
+ {props.showChangeSet && changeSetUI?.elements &&
674
+ <ChangeSetBox changeSet={changeSetUI} />
634
675
  }
635
- <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
676
+ <div className='theia-ChatInput-Editor-Box'>
677
+ <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>
679
+ </div>
680
+ {props.context && props.context.length > 0 &&
681
+ <ChatContext context={contextUI.context} />
682
+ }
683
+ <ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
684
+ </div>
636
685
  </div>
637
- </div>;
686
+ );
638
687
  };
639
688
 
640
689
  const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
@@ -816,6 +865,7 @@ function buildContextUI(
816
865
  }
817
866
  return {
818
867
  context: context.map((element, index) => ({
868
+ variable: element,
819
869
  name: labelProvider.getName(element),
820
870
  iconClass: labelProvider.getIcon(element),
821
871
  nameClass: element.variable.name,
@@ -829,6 +879,7 @@ function buildContextUI(
829
879
 
830
880
  interface ChatContextUI {
831
881
  context: {
882
+ variable: AIVariableResolutionRequest,
832
883
  name: string;
833
884
  iconClass: string;
834
885
  nameClass: string;
@@ -842,20 +893,45 @@ interface ChatContextUI {
842
893
  const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
843
894
  <div className="theia-ChatInput-ChatContext">
844
895
  <ul>
845
- {context.map((element, index) => (
846
- <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
847
- <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
848
- <div className="theia-ChatInput-ChatContext-labelParts">
849
- <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
850
- {element.name}
851
- </span>
852
- <span className='theia-ChatInput-ChatContext-additionalInfo'>
853
- {element.additionalInfo}
854
- </span>
896
+ {context.map((element, index) => {
897
+ if (ImageContextVariable.isImageContextRequest(element.variable)) {
898
+ const variable = ImageContextVariable.parseRequest(element.variable)!;
899
+ return <li key={index} className="theia-ChatInput-ChatContext-Element theia-ChatInput-ImageContext-Element"
900
+ title={variable.name ?? variable.wsRelativePath} onClick={() => element.open?.()}>
901
+ <div className="theia-ChatInput-ChatContext-Row">
902
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
903
+ <div className="theia-ChatInput-ChatContext-labelParts">
904
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
905
+ {variable.name ?? variable.wsRelativePath?.split('/').pop()}
906
+ </span>
907
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
908
+ {element.additionalInfo}
909
+ </span>
910
+ </div>
911
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
912
+ </div>
913
+ <div className="theia-ChatInput-ChatContext-ImageRow">
914
+ <div className='theia-ChatInput-ImagePreview-Item'>
915
+ <img src={`data:${variable.mimeType};base64,${variable.data}`} alt={variable.name} />
916
+ </div>
917
+ </div>
918
+ </li>;
919
+ }
920
+ return <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
921
+ <div className="theia-ChatInput-ChatContext-Row">
922
+ <div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
923
+ <div className="theia-ChatInput-ChatContext-labelParts">
924
+ <span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
925
+ {element.name}
926
+ </span>
927
+ <span className='theia-ChatInput-ChatContext-additionalInfo'>
928
+ {element.additionalInfo}
929
+ </span>
930
+ </div>
931
+ <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
855
932
  </div>
856
- <span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
857
- </li>
858
- ))}
933
+ </li>;
934
+ })}
859
935
  </ul>
860
936
  </div>
861
937
  );
@@ -78,6 +78,11 @@ export namespace ChatNodeToolbarCommands {
78
78
  id: 'chat:node:toolbar:cancel-request',
79
79
  category: CHAT_NODE_TOOLBAR_CATEGORY,
80
80
  }, '', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
81
+
82
+ export const RETRY = Command.toLocalizedCommand({
83
+ id: 'chat:node:toolbar:retry-message',
84
+ category: CHAT_NODE_TOOLBAR_CATEGORY,
85
+ }, 'Retry', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
81
86
  }
82
87
 
83
88
  export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbarActionContribution {
@@ -96,6 +101,15 @@ export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbar
96
101
  tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/edit', 'Edit'),
97
102
  }];
98
103
  } else {
104
+ const shouldShowRetry = node.response.isError || node.response.isCanceled;
105
+ if (shouldShowRetry) {
106
+ return [{
107
+ commandId: ChatNodeToolbarCommands.RETRY.id,
108
+ icon: codicon('refresh'),
109
+ tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/retry', 'Retry'),
110
+ priority: -1 // Higher priority to show it first
111
+ }];
112
+ }
99
113
  return [];
100
114
  }
101
115
  }
@@ -0,0 +1,177 @@
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
+ import { inject, injectable } from '@theia/core/shared/inversify';
17
+ import { ChatRequestInvocation, ChatResponseContent, ChatResponseModel } from '@theia/ai-chat';
18
+ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
19
+ import * as React from '@theia/core/shared/react';
20
+ import { DelegationResponseContent, isDelegationResponseContent } from '@theia/ai-chat/lib/browser/delegation-response-content';
21
+ import { ResponseNode } from '../chat-tree-view';
22
+ import { CompositeTreeNode } from '@theia/core/lib/browser';
23
+ import { SubChatWidgetFactory } from '../chat-tree-view/sub-chat-widget';
24
+ import { DisposableCollection } from '@theia/core';
25
+
26
+ @injectable()
27
+ export class DelegationResponseRenderer implements ChatResponsePartRenderer<DelegationResponseContent> {
28
+
29
+ @inject(SubChatWidgetFactory)
30
+ subChatWidgetFactory: SubChatWidgetFactory;
31
+
32
+ canHandle(response: ChatResponseContent): number {
33
+ if (isDelegationResponseContent(response)) {
34
+ return 10;
35
+ }
36
+ return -1;
37
+ }
38
+ render(response: DelegationResponseContent, parentNode: ResponseNode): React.ReactNode {
39
+ return this.renderExpandableNode(response, parentNode);
40
+ }
41
+
42
+ private renderExpandableNode(response: DelegationResponseContent, parentNode: ResponseNode): React.ReactNode {
43
+ return <DelegatedChat
44
+ response={response.response}
45
+ agentId={response.agentId}
46
+ prompt={response.prompt}
47
+ parentNode={parentNode}
48
+ subChatWidgetFactory={this.subChatWidgetFactory} />;
49
+ }
50
+ }
51
+
52
+ interface DelegatedChatProps {
53
+ response: ChatRequestInvocation;
54
+ agentId: string;
55
+ prompt: string;
56
+ parentNode: ResponseNode;
57
+ subChatWidgetFactory: SubChatWidgetFactory;
58
+ }
59
+
60
+ interface DelegatedChatState {
61
+ node?: ResponseNode;
62
+ }
63
+
64
+ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatState> {
65
+ private widget: ReturnType<SubChatWidgetFactory>;
66
+ private readonly toDispose = new DisposableCollection();
67
+
68
+ constructor(props: DelegatedChatProps) {
69
+ super(props);
70
+ this.state = {
71
+ node: undefined
72
+ };
73
+ this.widget = props.subChatWidgetFactory();
74
+ }
75
+
76
+ override componentDidMount(): void {
77
+ // Start rendering as soon as the response is created (streaming mode)
78
+ this.props.response.responseCreated.then(chatModel => {
79
+ const node = mapResponseToNode(chatModel, this.props.parentNode);
80
+ this.setState({ node });
81
+
82
+ // Listen for changes to update the rendering as the response streams in
83
+ const changeListener = () => {
84
+ // Force re-render when the response content changes
85
+ this.forceUpdate();
86
+ };
87
+ this.toDispose.push(chatModel.onDidChange(changeListener));
88
+ }).catch(error => {
89
+ console.error('Failed to create delegated chat response:', error);
90
+ // Still try to handle completion in case of partial success
91
+ });
92
+
93
+ // Keep the completion handling for final cleanup if needed
94
+ this.props.response.responseCompleted.then(() => {
95
+ // Final update when response is complete
96
+ this.forceUpdate();
97
+ }).catch(error => {
98
+ console.error('Error in delegated chat response completion:', error);
99
+ // Force update anyway to show any partial content or error state
100
+ this.forceUpdate();
101
+ });
102
+ }
103
+
104
+ override componentWillUnmount(): void {
105
+ this.toDispose.dispose();
106
+ }
107
+
108
+ override render(): React.ReactNode {
109
+ const { agentId, prompt } = this.props;
110
+ const hasNode = !!this.state.node;
111
+ const isComplete = this.state.node?.response.isComplete ?? false;
112
+ const isCanceled = this.state.node?.response.isCanceled ?? false;
113
+ const isError = this.state.node?.response.isError ?? false;
114
+
115
+ let statusIcon = '';
116
+ let statusText = '';
117
+ if (hasNode) {
118
+ if (isComplete) {
119
+ statusIcon = 'codicon-check';
120
+ statusText = 'completed';
121
+ } else if (isCanceled) {
122
+ statusIcon = 'codicon-cancel';
123
+ statusText = 'canceled';
124
+ } else if (isError) {
125
+ statusIcon = 'codicon-error';
126
+ statusText = 'error';
127
+ } else {
128
+ statusIcon = 'codicon-loading';
129
+ statusText = 'generating...';
130
+ }
131
+ } else {
132
+ statusIcon = 'codicon-loading';
133
+ statusText = 'starting...';
134
+ }
135
+
136
+ return (
137
+ <div className="theia-delegation-container">
138
+ <details className="delegation-response-details">
139
+ <summary className="delegation-summary">
140
+ <div className="delegation-header">
141
+ <span className="delegation-agent">
142
+ <strong>Agent:</strong> {agentId}
143
+ </span>
144
+ <span className="delegation-status">
145
+ <span className={`codicon ${statusIcon} delegation-status-icon`}></span>
146
+ <span className="delegation-status-text">{statusText}</span>
147
+ </span>
148
+ </div>
149
+ </summary>
150
+ <div className="delegation-content">
151
+ <div className="delegation-prompt-section">
152
+ <strong>Delegated prompt:</strong>
153
+ <div className="delegation-prompt">{prompt}</div>
154
+ </div>
155
+ <div className="delegation-response-section">
156
+ <strong>Response:</strong>
157
+ <div className='delegation-response-placeholder'>
158
+ {hasNode && this.state.node ? this.widget.renderChatResponse(this.state.node) :
159
+ <div className="theia-ChatContentInProgress">Starting delegation...</div>
160
+ }
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </details>
165
+ </div>
166
+ );
167
+ }
168
+ }
169
+
170
+ function mapResponseToNode(response: ChatResponseModel, parentNode: ResponseNode): ResponseNode {
171
+ return {
172
+ id: response.id,
173
+ parent: parentNode as unknown as CompositeTreeNode,
174
+ response,
175
+ sessionId: parentNode.sessionId
176
+ };
177
+ }
@@ -24,3 +24,4 @@ export * from './toolcall-part-renderer';
24
24
  export * from './thinking-part-renderer';
25
25
  export * from './progress-part-renderer';
26
26
  export * from './tool-confirmation';
27
+ export * from './delegation-response-renderer';
@@ -22,38 +22,38 @@ import { ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
22
22
  /**
23
23
  * States the tool confirmation component can be in
24
24
  */
25
- export type ToolConfirmationState = 'waiting' | 'approved' | 'denied';
25
+ export type ToolConfirmationState = 'waiting' | 'allowed' | 'denied';
26
26
 
27
27
  export interface ToolConfirmationProps {
28
28
  response: ToolCallChatResponseContent;
29
- onApprove: (mode?: 'once' | 'session' | 'forever') => void;
29
+ onAllow: (mode?: 'once' | 'session' | 'forever') => void;
30
30
  onDeny: (mode?: 'once' | 'session' | 'forever') => void;
31
31
  }
32
32
 
33
33
  /**
34
34
  * Component that displays approval/denial buttons for tool execution
35
35
  */
36
- export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, onApprove, onDeny }) => {
36
+ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, onAllow, onDeny }) => {
37
37
  const [state, setState] = React.useState<ToolConfirmationState>('waiting');
38
38
  // Track selected mode for each action
39
- const [approveMode, setApproveMode] = React.useState<'once' | 'session' | 'forever'>('once');
39
+ const [allowMode, setAllowMode] = React.useState<'once' | 'session' | 'forever'>('once');
40
40
  const [denyMode, setDenyMode] = React.useState<'once' | 'session' | 'forever'>('once');
41
- const [dropdownOpen, setDropdownOpen] = React.useState<'approve' | 'deny' | undefined>(undefined);
41
+ const [dropdownOpen, setDropdownOpen] = React.useState<'allow' | 'deny' | undefined>(undefined);
42
42
 
43
- const handleApprove = React.useCallback(() => {
44
- setState('approved');
45
- onApprove(approveMode);
46
- }, [onApprove, approveMode]);
43
+ const handleAllow = React.useCallback(() => {
44
+ setState('allowed');
45
+ onAllow(allowMode);
46
+ }, [onAllow, allowMode]);
47
47
 
48
48
  const handleDeny = React.useCallback(() => {
49
49
  setState('denied');
50
50
  onDeny(denyMode);
51
51
  }, [onDeny, denyMode]);
52
52
 
53
- if (state === 'approved') {
53
+ if (state === 'allowed') {
54
54
  return (
55
- <div className="theia-tool-confirmation-status approved">
56
- <span className={codicon('check')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/approved', 'Tool execution approved')}
55
+ <div className="theia-tool-confirmation-status allowed">
56
+ <span className={codicon('check')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/allowed', 'Tool execution allowed')}
57
57
  </div>
58
58
  );
59
59
  }
@@ -69,12 +69,12 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
69
69
  // Helper for dropdown options
70
70
  const MODES: Array<'once' | 'session' | 'forever'> = ['once', 'session', 'forever'];
71
71
  // Unified labels for both main button and dropdown, as requested
72
- const modeLabel = (type: 'approve' | 'deny', mode: 'once' | 'session' | 'forever') => {
73
- if (type === 'approve') {
72
+ const modeLabel = (type: 'allow' | 'deny', mode: 'once' | 'session' | 'forever') => {
73
+ if (type === 'allow') {
74
74
  switch (mode) {
75
- case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve', 'Approve');
76
- case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-session', 'Approve for this Chat');
77
- case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-forever', 'Always Approve');
75
+ case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow', 'Allow');
76
+ case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-session', 'Allow for this Chat');
77
+ case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-forever', 'Always Allow');
78
78
  }
79
79
  } else {
80
80
  switch (mode) {
@@ -88,12 +88,12 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
88
88
  const mainButtonLabel = modeLabel; // Use the same function for both
89
89
 
90
90
  // Tooltips for dropdown options
91
- const modeTooltip = (type: 'approve' | 'deny', mode: 'once' | 'session' | 'forever') => {
92
- if (type === 'approve') {
91
+ const modeTooltip = (type: 'allow' | 'deny', mode: 'once' | 'session' | 'forever') => {
92
+ if (type === 'allow') {
93
93
  switch (mode) {
94
- case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-tooltip', 'Approve this tool call once');
95
- case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-session-tooltip', 'Approve all calls of this tool for this chat session');
96
- case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-forever-tooltip', 'Always approve this tool');
94
+ case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-tooltip', 'Allow this tool call once');
95
+ case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-session-tooltip', 'Allow all calls of this tool for this chat session');
96
+ case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-forever-tooltip', 'Always allow this tool');
97
97
  }
98
98
  } else {
99
99
  switch (mode) {
@@ -105,27 +105,27 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
105
105
  };
106
106
 
107
107
  // Split button for approve/deny
108
- const renderSplitButton = (type: 'approve' | 'deny') => {
109
- const selectedMode = type === 'approve' ? approveMode : denyMode;
110
- const setMode = type === 'approve' ? setApproveMode : setDenyMode;
111
- const handleMain = type === 'approve' ? handleApprove : handleDeny;
108
+ const renderSplitButton = (type: 'allow' | 'deny') => {
109
+ const selectedMode = type === 'allow' ? allowMode : denyMode;
110
+ const setMode = type === 'allow' ? setAllowMode : setDenyMode;
111
+ const handleMain = type === 'allow' ? handleAllow : handleDeny;
112
112
  const otherModes = MODES.filter(m => m !== selectedMode);
113
113
  return (
114
114
  <div className={`theia-tool-confirmation-split-button ${type}`}
115
115
  style={{ display: 'inline-flex', position: 'relative' }}>
116
116
  <button
117
- className={`theia-button ${type === 'approve' ? 'primary' : 'secondary'} theia-tool-confirmation-main-btn`}
117
+ className={`theia-button ${type === 'allow' ? 'primary' : 'secondary'} theia-tool-confirmation-main-btn`}
118
118
  onClick={handleMain}
119
119
  >
120
120
  {mainButtonLabel(type, selectedMode)}
121
121
  </button>
122
122
  <button
123
- className={`theia-button ${type === 'approve' ? 'primary' : 'secondary'} theia-tool-confirmation-chevron-btn`}
123
+ className={`theia-button ${type === 'allow' ? 'primary' : 'secondary'} theia-tool-confirmation-chevron-btn`}
124
124
  onClick={() => setDropdownOpen(dropdownOpen === type ? undefined : type)}
125
125
  aria-haspopup="true"
126
126
  aria-expanded={dropdownOpen === type}
127
127
  tabIndex={0}
128
- title={type === 'approve' ? 'More Approve Options' : 'More Deny Options'}
128
+ title={type === 'allow' ? 'More Allow Options' : 'More Deny Options'}
129
129
  >
130
130
  <span className={codicon('chevron-down')}></span>
131
131
  </button>
@@ -166,7 +166,7 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
166
166
  </div>
167
167
  <div className="theia-tool-confirmation-actions">
168
168
  {renderSplitButton('deny')}
169
- {renderSplitButton('approve')}
169
+ {renderSplitButton('allow')}
170
170
  </div>
171
171
  </div>
172
172
  );
@@ -101,7 +101,7 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
101
101
  }
102
102
 
103
103
  const Spinner = () => (
104
- <span className={codicon('loading')}></span>
104
+ <span className={`${codicon('loading')} theia-animation-spin`}></span>
105
105
  );
106
106
 
107
107
  interface ToolCallContentProps {
@@ -120,9 +120,9 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
120
120
  const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
121
121
 
122
122
  React.useEffect(() => {
123
- if (confirmationMode === ToolConfirmationMode.YOLO) {
123
+ if (confirmationMode === ToolConfirmationMode.ALWAYS_ALLOW) {
124
124
  response.confirm();
125
- setConfirmationState('approved');
125
+ setConfirmationState('allowed');
126
126
  return;
127
127
  } else if (confirmationMode === ToolConfirmationMode.DISABLED) {
128
128
  response.deny();
@@ -132,7 +132,7 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
132
132
  response.confirmed.then(
133
133
  confirmed => {
134
134
  if (confirmed === true) {
135
- setConfirmationState('approved');
135
+ setConfirmationState('allowed');
136
136
  } else {
137
137
  setConfirmationState('denied');
138
138
  }
@@ -143,11 +143,11 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
143
143
  });
144
144
  }, [response, confirmationMode]);
145
145
 
146
- const handleApprove = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
146
+ const handleAllow = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
147
147
  if (mode === 'forever' && response.name) {
148
- toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.YOLO);
148
+ toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW);
149
149
  } else if (mode === 'session' && response.name) {
150
- toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.YOLO, chatId);
150
+ toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, chatId);
151
151
  }
152
152
  response.confirm();
153
153
  }, [response, toolConfirmationManager, chatId]);
@@ -176,7 +176,7 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
176
176
  <pre>{tryPrettyPrintJson(response)}</pre>
177
177
  </details>
178
178
  ) : (
179
- confirmationState === 'approved' && (
179
+ confirmationState === 'allowed' && (
180
180
  <span>
181
181
  <Spinner /> {nls.localizeByDefault('Running')} {response.name}
182
182
  </span>
@@ -184,11 +184,11 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
184
184
  )}
185
185
  </h4>
186
186
 
187
- {/* Show confirmation UI when waiting for approval */}
187
+ {/* Show confirmation UI when waiting for allow */}
188
188
  {confirmationState === 'waiting' && (
189
189
  <ToolConfirmation
190
190
  response={response}
191
- onApprove={handleApprove}
191
+ onAllow={handleAllow}
192
192
  onDeny={handleDeny}
193
193
  />
194
194
  )}