@theia/ai-chat-ui 1.63.0-next.24 → 1.63.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 (71) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +31 -1
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +192 -8
  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 +24 -16
  9. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  10. package/lib/browser/chat-input-widget.js +199 -37
  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.d.ts +4 -1
  29. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  30. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +65 -32
  31. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  32. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +3 -1
  33. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  34. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +50 -8
  35. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  36. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
  37. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
  38. package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
  39. package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
  40. package/lib/browser/chat-view-commands.d.ts +1 -0
  41. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  42. package/lib/browser/chat-view-commands.js +5 -0
  43. package/lib/browser/chat-view-commands.js.map +1 -1
  44. package/lib/browser/chat-view-contribution.d.ts +2 -0
  45. package/lib/browser/chat-view-contribution.d.ts.map +1 -1
  46. package/lib/browser/chat-view-contribution.js +31 -18
  47. package/lib/browser/chat-view-contribution.js.map +1 -1
  48. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts +2 -0
  49. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
  50. package/lib/browser/chat-view-widget-toolbar-contribution.js +13 -5
  51. package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
  52. package/lib/browser/chat-view-widget.d.ts +1 -1
  53. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  54. package/lib/browser/chat-view-widget.js +1 -4
  55. package/lib/browser/chat-view-widget.js.map +1 -1
  56. package/package.json +11 -11
  57. package/src/browser/ai-chat-ui-contribution.ts +191 -12
  58. package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
  59. package/src/browser/chat-input-widget.tsx +253 -58
  60. package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
  61. package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
  62. package/src/browser/chat-response-renderer/index.ts +1 -0
  63. package/src/browser/chat-response-renderer/tool-confirmation.tsx +30 -30
  64. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +95 -60
  65. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +58 -8
  66. package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
  67. package/src/browser/chat-view-commands.ts +6 -0
  68. package/src/browser/chat-view-contribution.ts +29 -18
  69. package/src/browser/chat-view-widget-toolbar-contribution.tsx +12 -5
  70. package/src/browser/chat-view-widget.tsx +2 -5
  71. package/src/browser/style/index.css +209 -5
@@ -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
  );
@@ -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,33 +125,10 @@ 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 = () => (
104
- <span className={codicon('loading')}></span>
131
+ <span className={`${codicon('loading')} theia-animation-spin`}></span>
105
132
  );
106
133
 
107
134
  interface ToolCallContentProps {
@@ -110,19 +137,19 @@ 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(() => {
123
- if (confirmationMode === ToolConfirmationMode.YOLO) {
150
+ if (confirmationMode === ToolConfirmationMode.ALWAYS_ALLOW) {
124
151
  response.confirm();
125
- setConfirmationState('approved');
152
+ setConfirmationState('allowed');
126
153
  return;
127
154
  } else if (confirmationMode === ToolConfirmationMode.DISABLED) {
128
155
  response.deny();
@@ -132,7 +159,7 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
132
159
  response.confirmed.then(
133
160
  confirmed => {
134
161
  if (confirmed === true) {
135
- setConfirmationState('approved');
162
+ setConfirmationState('allowed');
136
163
  } else {
137
164
  setConfirmationState('denied');
138
165
  }
@@ -143,11 +170,11 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
143
170
  });
144
171
  }, [response, confirmationMode]);
145
172
 
146
- const handleApprove = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
173
+ const handleAllow = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
147
174
  if (mode === 'forever' && response.name) {
148
- toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.YOLO);
175
+ toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW);
149
176
  } else if (mode === 'session' && response.name) {
150
- toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.YOLO, chatId);
177
+ toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, chatId);
151
178
  }
152
179
  response.confirm();
153
180
  }, [response, toolConfirmationManager, chatId]);
@@ -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 === 'approved' && (
180
- <span>
181
- <Spinner /> {nls.localizeByDefault('Running')} {response.name}
182
- </span>
183
- )
184
- )}
185
- </h4>
186
-
187
- {/* Show confirmation UI when waiting for approval */}
212
+ )
213
+ )}
214
+
215
+ {/* Show confirmation UI when waiting for allow */}
188
216
  {confirmationState === 'waiting' && (
189
- <ToolConfirmation
190
- response={response}
191
- onApprove={handleApprove}
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
+ };
@@ -142,6 +142,8 @@ export class ChatViewTreeWidget extends TreeWidget {
142
142
 
143
143
  onScrollLockChange?: (temporaryLocked: boolean) => void;
144
144
 
145
+ protected lastScrollTop = 0;
146
+
145
147
  set shouldScrollToEnd(shouldScrollToEnd: boolean) {
146
148
  this._shouldScrollToEnd = shouldScrollToEnd;
147
149
  this.shouldScrollToRow = this._shouldScrollToEnd;
@@ -189,6 +191,9 @@ export class ChatViewTreeWidget extends TreeWidget {
189
191
  this.handleScrollEvent(scrollEvent);
190
192
  })
191
193
  ]);
194
+
195
+ // Initialize lastScrollTop with current scroll position
196
+ this.lastScrollTop = this.getCurrentScrollTop(undefined);
192
197
  }
193
198
 
194
199
  public setEnabled(enabled: boolean): void {
@@ -196,20 +201,39 @@ export class ChatViewTreeWidget extends TreeWidget {
196
201
  this.update();
197
202
  }
198
203
 
199
- protected handleScrollEvent(_scrollEvent: unknown): void {
200
- // Check if we're at the bottom of the view
204
+ protected handleScrollEvent(scrollEvent: unknown): void {
205
+ // Get current scroll position
206
+ const currentScrollTop = this.getCurrentScrollTop(scrollEvent);
201
207
  const isAtBottom = this.isScrolledToBottom();
202
208
 
203
- // Only handle temporary scroll lock if auto-scroll is currently enabled
209
+ // Determine scroll direction
210
+ const isScrollingUp = currentScrollTop < this.lastScrollTop;
211
+ const isScrollingDown = currentScrollTop > this.lastScrollTop;
212
+
213
+ // Handle scroll lock logic based on direction and position
214
+ // The key insight is that we only enable temporary lock when scrolling UP,
215
+ // and only disable it when scrolling DOWN to the bottom. This prevents
216
+ // the jitter when users try to scroll up by just a few pixels from the bottom.
204
217
  if (this.shouldScrollToEnd) {
205
- if (!isAtBottom) {
206
- // User scrolled away from bottom, enable temporary lock
218
+ // Auto-scroll is enabled, check if we need to enable temporary lock
219
+ if (isScrollingUp) {
220
+ // User is scrolling up and not at bottom - enable temporary lock
207
221
  this.setTemporaryScrollLock(true);
208
222
  }
209
- } else if (isAtBottom) {
210
- // User scrolled back to bottom, disable temporary lock
211
- this.setTemporaryScrollLock(false);
223
+ // Note: We don't disable temporary lock when scrolling down while shouldScrollToEnd is true
224
+ // because that would cause jitter. The lock will be disabled when user reaches bottom.
225
+ } else {
226
+ // Temporary lock is active, check if we should disable it
227
+ if (isScrollingDown && isAtBottom) {
228
+ // User scrolled back to bottom - disable temporary lock
229
+ this.setTemporaryScrollLock(false);
230
+ }
231
+ // Note: We don't change the lock state when scrolling up while locked,
232
+ // as the user is intentionally scrolling away from auto-scroll behavior.
212
233
  }
234
+
235
+ // Update last scroll position for next comparison
236
+ this.lastScrollTop = currentScrollTop;
213
237
  }
214
238
 
215
239
  protected setTemporaryScrollLock(enabled: boolean): void {
@@ -217,6 +241,32 @@ export class ChatViewTreeWidget extends TreeWidget {
217
241
  this.onScrollLockChange?.(enabled);
218
242
  }
219
243
 
244
+ protected getCurrentScrollTop(scrollEvent: unknown): number {
245
+ // For virtualized trees, try to get scroll position from the virtualized view
246
+ if (this.props.virtualized !== false && this.view) {
247
+ const scrollState = this.getVirtualizedScrollState();
248
+ if (scrollState !== undefined) {
249
+ return scrollState.scrollTop;
250
+ }
251
+ }
252
+
253
+ // Try to extract scroll position from the scroll event
254
+ if (scrollEvent && typeof scrollEvent === 'object' && 'scrollTop' in scrollEvent) {
255
+ const scrollEventWithScrollTop = scrollEvent as { scrollTop: unknown };
256
+ const scrollTop = scrollEventWithScrollTop.scrollTop;
257
+ if (typeof scrollTop === 'number' && !isNaN(scrollTop)) {
258
+ return scrollTop;
259
+ }
260
+ }
261
+
262
+ // Last resort: use DOM scroll position
263
+ if (this.node && typeof this.node.scrollTop === 'number') {
264
+ return this.node.scrollTop;
265
+ }
266
+
267
+ return 0;
268
+ }
269
+
220
270
  protected override renderTree(model: TreeModel): React.ReactNode {
221
271
  if (!this.isEnabled) {
222
272
  return this.renderDisabledMessage();