@@ -10,36 +10,21 @@ import { Emitter } from '../../../../../base/common/event.js';
1010import { IMarkdownString } from '../../../../../base/common/htmlContent.js' ;
1111import { Disposable } from '../../../../../base/common/lifecycle.js' ;
1212import { autorun , ISettableObservable , observableValue } from '../../../../../base/common/observable.js' ;
13- import { basename , joinPath } from '../../../../../base/common/resources.js' ;
1413import { ThemeIcon } from '../../../../../base/common/themables.js' ;
1514import { URI } from '../../../../../base/common/uri.js' ;
16- import { generateUuid } from '../../../../../base/common/uuid.js' ;
1715import { ITextModel } from '../../../../../editor/common/model.js' ;
18- import { localize , localize2 } from '../../../../../nls.js' ;
19- import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js' ;
20- import { Action2 , MenuId , registerAction2 } from '../../../../../platform/actions/common/actions.js' ;
21- import { ICommandService } from '../../../../../platform/commands/common/commands.js' ;
16+ import { localize } from '../../../../../nls.js' ;
2217import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js' ;
23- import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js' ;
24- import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js' ;
25- import { IFileService } from '../../../../../platform/files/common/files.js' ;
26- import { IInstantiationService , ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js' ;
27- import { ILabelService } from '../../../../../platform/label/common/label.js' ;
28- import { INotificationService } from '../../../../../platform/notification/common/notification.js' ;
29- import { IProgressService , ProgressLocation } from '../../../../../platform/progress/common/progress.js' ;
30- import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js' ;
31- import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../../files/browser/fileConstants.js' ;
32- import { getAttachableImageExtension } from '../../common/chatModel.js' ;
33- import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js' ;
18+ import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js' ;
3419import { IChatRendererContent } from '../../common/chatViewModel.js' ;
3520import { LanguageModelPartAudience } from '../../common/languageModels.js' ;
3621import { ChatTreeItem , IChatCodeBlockInfo } from '../chat.js' ;
3722import { CodeBlockPart , ICodeBlockData , ICodeBlockRenderOptions } from '../codeBlockPart.js' ;
38- import { ChatAttachmentsContentPart } from './chatAttachmentsContentPart.js' ;
3923import { IDisposableReference } from './chatCollections.js' ;
4024import { ChatQueryTitlePart } from './chatConfirmationWidget.js' ;
4125import { IChatContentPartRenderContext } from './chatContentParts.js' ;
4226import { EditorPool } from './chatMarkdownContentPart.js' ;
27+ import { ChatToolOutputContentSubPart } from './chatToolOutputContentSubPart.js' ;
4328
4429export interface IChatCollapsibleIOCodePart {
4530 kind : 'code' ;
@@ -71,9 +56,17 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
7156 private _currentWidth : number = 0 ;
7257 private readonly _editorReferences : IDisposableReference < CodeBlockPart > [ ] = [ ] ;
7358 private readonly _titlePart : ChatQueryTitlePart ;
59+ private _outputSubPart : ChatToolOutputContentSubPart | undefined ;
7460 public readonly domNode : HTMLElement ;
7561
76- readonly codeblocks : IChatCodeBlockInfo [ ] = [ ] ;
62+ get codeblocks ( ) : IChatCodeBlockInfo [ ] {
63+ const inputCodeblocks = this . _editorReferences . map ( ref => {
64+ const cbi = this . input . codeBlockInfo ;
65+ return cbi ;
66+ } ) ;
67+ const outputCodeblocks = this . _outputSubPart ?. codeblocks ?? [ ] ;
68+ return [ ...inputCodeblocks , ...outputCodeblocks ] ;
69+ }
7770
7871 public set title ( s : string | IMarkdownString ) {
7972 this . _titlePart . title = s ;
@@ -101,8 +94,6 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
10194 width : number ,
10295 @IContextKeyService private readonly contextKeyService : IContextKeyService ,
10396 @IInstantiationService private readonly _instantiationService : IInstantiationService ,
104- @IContextMenuService private readonly _contextMenuService : IContextMenuService ,
105- @IFileService private readonly _fileService : IFileService ,
10697 ) {
10798 super ( ) ;
10899 this . _currentWidth = width ;
@@ -163,8 +154,16 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
163154 . filter ( p => p . kind === 'data' )
164155 . filter ( p => ! p . audience || p . audience . includes ( LanguageModelPartAudience . User ) ) ;
165156 if ( topLevelResources ?. length ) {
166- const group = this . addResourceGroup ( topLevelResources , container . root ) ;
157+ const resourceSubPart = this . _register ( this . _instantiationService . createInstance (
158+ ChatToolOutputContentSubPart ,
159+ this . context ,
160+ this . editorPool ,
161+ topLevelResources ,
162+ this . _currentWidth
163+ ) ) ;
164+ const group = resourceSubPart . domNode ;
167165 group . classList . add ( 'chat-collapsible-top-level-resource-group' ) ;
166+ container . root . appendChild ( group ) ;
168167 this . _register ( autorun ( r => {
169168 group . style . display = expanded . read ( r ) ? 'none' : '' ;
170169 } ) ) ;
@@ -189,89 +188,21 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
189188 contents . outputTitle . remove ( ) ;
190189 } else {
191190 contents . outputTitle . textContent = localize ( 'chat.output' , "Output" ) ;
192- for ( let i = 0 ; i < output . parts . length ; i ++ ) {
193- const part = output . parts [ i ] ;
194- if ( part . kind === 'code' ) {
195- this . addCodeBlock ( part , contents . output ) ;
196- continue ;
197- }
198-
199- const group : IChatCollapsibleIODataPart [ ] = [ ] ;
200- for ( let k = i ; k < output . parts . length ; k ++ ) {
201- const part = output . parts [ k ] ;
202- if ( part . kind !== 'data' ) {
203- break ;
204- }
205- group . push ( part ) ;
206- }
207-
208- this . addResourceGroup ( group , contents . output ) ;
209- i += group . length - 1 ; // Skip the parts we just added
210- }
191+ const outputSubPart = this . _register ( this . _instantiationService . createInstance (
192+ ChatToolOutputContentSubPart ,
193+ this . context ,
194+ this . editorPool ,
195+ output . parts ,
196+ this . _currentWidth
197+ ) ) ;
198+ this . _outputSubPart = outputSubPart ;
199+ this . _register ( outputSubPart . onDidChangeHeight ( ( ) => this . _onDidChangeHeight . fire ( ) ) ) ;
200+ contents . output . appendChild ( outputSubPart . domNode ) ;
211201 }
212202
213203 return contents . root ;
214204 }
215205
216- private addResourceGroup ( parts : IChatCollapsibleIODataPart [ ] , container : HTMLElement ) {
217- const el = dom . h ( '.chat-collapsible-io-resource-group' , [
218- dom . h ( '.chat-collapsible-io-resource-items@items' ) ,
219- dom . h ( '.chat-collapsible-io-resource-actions@actions' ) ,
220- ] ) ;
221-
222- this . fillInResourceGroup ( parts , el . items , el . actions ) . then ( ( ) => this . _onDidChangeHeight . fire ( ) ) ;
223-
224- container . appendChild ( el . root ) ;
225- return el . root ;
226- }
227-
228- private async fillInResourceGroup ( parts : IChatCollapsibleIODataPart [ ] , itemsContainer : HTMLElement , actionsContainer : HTMLElement ) {
229- const entries = await Promise . all ( parts . map ( async ( part ) : Promise < IChatRequestVariableEntry > => {
230- if ( part . mimeType && getAttachableImageExtension ( part . mimeType ) ) {
231- const value = part . value ?? await this . _fileService . readFile ( part . uri ) . then ( f => f . value . buffer , ( ) => undefined ) ;
232- return { kind : 'image' , id : generateUuid ( ) , name : basename ( part . uri ) , value, mimeType : part . mimeType , isURL : false , references : [ { kind : 'reference' , reference : part . uri } ] } ;
233- } else {
234- return { kind : 'file' , id : generateUuid ( ) , name : basename ( part . uri ) , fullName : part . uri . path , value : part . uri } ;
235- }
236- } ) ) ;
237-
238- const attachments = this . _register ( this . _instantiationService . createInstance (
239- ChatAttachmentsContentPart ,
240- {
241- variables : entries ,
242- limit : 5 ,
243- contentReferences : undefined ,
244- domNode : undefined
245- }
246- ) ) ;
247-
248- attachments . contextMenuHandler = ( attachment , event ) => {
249- const index = entries . indexOf ( attachment ) ;
250- const part = parts [ index ] ;
251- if ( part ) {
252- event . preventDefault ( ) ;
253- event . stopPropagation ( ) ;
254-
255- this . _contextMenuService . showContextMenu ( {
256- menuId : MenuId . ChatToolOutputResourceContext ,
257- menuActionOptions : { shouldForwardArgs : true } ,
258- getAnchor : ( ) => ( { x : event . pageX , y : event . pageY } ) ,
259- getActionsContext : ( ) => ( { parts : [ part ] } satisfies IChatToolOutputResourceToolbarContext ) ,
260- } ) ;
261- }
262- } ;
263-
264- itemsContainer . appendChild ( attachments . domNode ! ) ;
265-
266- const toolbar = this . _register ( this . _instantiationService . createInstance ( MenuWorkbenchToolBar , actionsContainer , MenuId . ChatToolOutputResourceToolbar , {
267- menuOptions : {
268- shouldForwardArgs : true ,
269- } ,
270- } ) ) ;
271- toolbar . context = { parts } satisfies IChatToolOutputResourceToolbarContext ;
272- }
273-
274-
275206 private addCodeBlock ( part : IChatCollapsibleIOCodePart , container : HTMLElement ) {
276207 const data : ICodeBlockData = {
277208 languageId : part . languageId ,
@@ -298,97 +229,6 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
298229 layout ( width : number ) : void {
299230 this . _currentWidth = width ;
300231 this . _editorReferences . forEach ( r => r . object . layout ( width ) ) ;
232+ this . _outputSubPart ?. layout ( width ) ;
301233 }
302234}
303-
304- interface IChatToolOutputResourceToolbarContext {
305- parts : IChatCollapsibleIODataPart [ ] ;
306- }
307-
308- class SaveResourcesAction extends Action2 {
309- public static readonly ID = 'chat.toolOutput.save' ;
310- constructor ( ) {
311- super ( {
312- id : SaveResourcesAction . ID ,
313- title : localize2 ( 'chat.saveResources' , "Save As..." ) ,
314- icon : Codicon . cloudDownload ,
315- menu : [ {
316- id : MenuId . ChatToolOutputResourceToolbar ,
317- group : 'navigation' ,
318- order : 1
319- } , {
320- id : MenuId . ChatToolOutputResourceContext ,
321- } ]
322- } ) ;
323- }
324-
325- async run ( accessor : ServicesAccessor , context : IChatToolOutputResourceToolbarContext ) {
326- const fileDialog = accessor . get ( IFileDialogService ) ;
327- const fileService = accessor . get ( IFileService ) ;
328- const notificationService = accessor . get ( INotificationService ) ;
329- const progressService = accessor . get ( IProgressService ) ;
330- const workspaceContextService = accessor . get ( IWorkspaceContextService ) ;
331- const commandService = accessor . get ( ICommandService ) ;
332- const labelService = accessor . get ( ILabelService ) ;
333- const defaultFilepath = await fileDialog . defaultFilePath ( ) ;
334-
335- const savePart = async ( part : IChatCollapsibleIODataPart , isFolder : boolean , uri : URI ) => {
336- const target = isFolder ? joinPath ( uri , basename ( part . uri ) ) : uri ;
337- try {
338- if ( part . kind === 'data' ) {
339- await fileService . copy ( part . uri , target , true ) ;
340- } else {
341- // MCP doesn't support streaming data, so no sense trying
342- const contents = await fileService . readFile ( part . uri ) ;
343- await fileService . writeFile ( target , contents . value ) ;
344- }
345- } catch ( e ) {
346- notificationService . error ( localize ( 'chat.saveResources.error' , "Failed to save {0}: {1}" , basename ( part . uri ) , e ) ) ;
347- }
348- } ;
349-
350- const withProgress = async ( thenReveal : URI , todo : ( ( ) => Promise < void > ) [ ] ) => {
351- await progressService . withProgress ( {
352- location : ProgressLocation . Notification ,
353- delay : 5_000 ,
354- title : localize ( 'chat.saveResources.progress' , "Saving resources..." ) ,
355- } , async report => {
356- for ( const task of todo ) {
357- await task ( ) ;
358- report . report ( { increment : 1 , total : todo . length } ) ;
359- }
360- } ) ;
361-
362- if ( workspaceContextService . isInsideWorkspace ( thenReveal ) ) {
363- commandService . executeCommand ( REVEAL_IN_EXPLORER_COMMAND_ID , thenReveal ) ;
364- } else {
365- notificationService . info ( localize ( 'chat.saveResources.reveal' , "Saved resources to {0}" , labelService . getUriLabel ( thenReveal ) ) ) ;
366- }
367- } ;
368-
369- if ( context . parts . length === 1 ) {
370- const part = context . parts [ 0 ] ;
371- const uri = await fileDialog . pickFileToSave ( joinPath ( defaultFilepath , basename ( part . uri ) ) ) ;
372- if ( ! uri ) {
373- return ;
374- }
375- await withProgress ( uri , [ ( ) => savePart ( part , false , uri ) ] ) ;
376- } else {
377- const uris = await fileDialog . showOpenDialog ( {
378- title : localize ( 'chat.saveResources.title' , "Pick folder to save resources" ) ,
379- canSelectFiles : false ,
380- canSelectFolders : true ,
381- canSelectMany : false ,
382- defaultUri : workspaceContextService . getWorkspace ( ) . folders [ 0 ] ?. uri ,
383- } ) ;
384-
385- if ( ! uris ?. length ) {
386- return ;
387- }
388-
389- await withProgress ( uris [ 0 ] , context . parts . map ( part => ( ) => savePart ( part , true , uris [ 0 ] ) ) ) ;
390- }
391- }
392- }
393-
394- registerAction2 ( SaveResourcesAction ) ;
0 commit comments