@@ -19,18 +19,23 @@ import { inject, injectable } from '@theia/core/shared/inversify';
1919import { ChatResponseContent , ToolCallChatResponseContent } from '@theia/ai-chat/lib/common' ;
2020import { ReactNode } from '@theia/core/shared/react' ;
2121import { nls } from '@theia/core/lib/common/nls' ;
22- import { codicon } from '@theia/core/lib/browser' ;
22+ import { codicon , OpenerService } from '@theia/core/lib/browser' ;
2323import * as React from '@theia/core/shared/react' ;
2424import { ToolConfirmation , ToolConfirmationState } from './tool-confirmation' ;
2525import { ToolConfirmationManager , ToolConfirmationMode } from '@theia/ai-chat/lib/browser/chat-tool-preferences' ;
2626import { ResponseNode } from '../chat-tree-view' ;
27+ import { useMarkdownRendering } from './markdown-part-renderer' ;
28+ import { ToolCallResult } from '@theia/ai-core' ;
2729
2830@injectable ( )
2931export class ToolCallPartRenderer implements ChatResponsePartRenderer < ToolCallChatResponseContent > {
3032
3133 @inject ( ToolConfirmationManager )
3234 protected toolConfirmationManager : ToolConfirmationManager ;
3335
36+ @inject ( OpenerService )
37+ protected openerService : OpenerService ;
38+
3439 canHandle ( response : ChatResponseContent ) : number {
3540 if ( ToolCallChatResponseContent . is ( response ) ) {
3641 return 10 ;
@@ -47,7 +52,52 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
4752 toolConfirmationManager = { this . toolConfirmationManager }
4853 chatId = { chatId }
4954 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+ }
51101 }
52102
53103 protected getToolConfirmationSettings ( responseId : string , chatId : string ) : ToolConfirmationMode {
@@ -75,29 +125,6 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
75125 return args ;
76126 }
77127 }
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- }
101128}
102129
103130const Spinner = ( ) => (
@@ -110,13 +137,13 @@ interface ToolCallContentProps {
110137 toolConfirmationManager : ToolConfirmationManager ;
111138 chatId : string ;
112139 renderCollapsibleArguments : ( args : string | undefined ) => ReactNode ;
113- tryPrettyPrintJson : ( response : ToolCallChatResponseContent ) => string | undefined ;
140+ responseRenderer : ( response : ToolCallChatResponseContent ) => ReactNode | undefined ;
114141}
115142
116143/**
117144 * A function component to handle tool call rendering and confirmation
118145 */
119- const ToolCallContent : React . FC < ToolCallContentProps > = ( { response, confirmationMode, toolConfirmationManager, chatId, tryPrettyPrintJson , renderCollapsibleArguments } ) => {
146+ const ToolCallContent : React . FC < ToolCallContentProps > = ( { response, confirmationMode, toolConfirmationManager, chatId, responseRenderer , renderCollapsibleArguments } ) => {
120147 const [ confirmationState , setConfirmationState ] = React . useState < ToolConfirmationState > ( 'waiting' ) ;
121148
122149 React . useEffect ( ( ) => {
@@ -163,35 +190,43 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
163190
164191 return (
165192 < 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 }
170211 </ 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+ ) }
186214
187215 { /* Show confirmation UI when waiting for allow */ }
188216 { 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 >
194224 ) }
195225 </ div >
196226 ) ;
197227} ;
228+
229+ const MarkdownRender = ( { text, openerService } : { text : string ; openerService : OpenerService } ) => {
230+ const ref = useMarkdownRendering ( text , openerService ) ;
231+ return < div ref = { ref } > </ div > ;
232+ } ;
0 commit comments