@@ -17,6 +17,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js';
1717import { Iterable } from '../../../../base/common/iterator.js' ;
1818import { KeyCode } from '../../../../base/common/keyCodes.js' ;
1919import { Disposable , DisposableStore , IDisposable } from '../../../../base/common/lifecycle.js' ;
20+ import { Schemas } from '../../../../base/common/network.js' ;
2021import { basename , dirname } from '../../../../base/common/path.js' ;
2122import { ThemeIcon } from '../../../../base/common/themables.js' ;
2223import { URI } from '../../../../base/common/uri.js' ;
@@ -52,8 +53,9 @@ import { revealInSideBarCommand } from '../../files/browser/fileActions.contribu
5253import { CellUri } from '../../notebook/common/notebookCommon.js' ;
5354import { INotebookService } from '../../notebook/common/notebookService.js' ;
5455import { getHistoryItemEditorTitle } from '../../scm/browser/util.js' ;
56+ import { ITerminalService } from '../../terminal/browser/terminal.js' ;
5557import { IChatContentReference } from '../common/chatService.js' ;
56- import { IChatRequestPasteVariableEntry , IChatRequestVariableEntry , IElementVariableEntry , INotebookOutputVariableEntry , IPromptFileVariableEntry , IPromptTextVariableEntry , ISCMHistoryItemVariableEntry , OmittedState , PromptFileVariableKind , ChatRequestToolReferenceEntry , ISCMHistoryItemChangeVariableEntry , ISCMHistoryItemChangeRangeVariableEntry } from '../common/chatVariableEntries.js' ;
58+ import { IChatRequestPasteVariableEntry , IChatRequestVariableEntry , IElementVariableEntry , INotebookOutputVariableEntry , IPromptFileVariableEntry , IPromptTextVariableEntry , ISCMHistoryItemVariableEntry , OmittedState , PromptFileVariableKind , ChatRequestToolReferenceEntry , ISCMHistoryItemChangeVariableEntry , ISCMHistoryItemChangeRangeVariableEntry , ITerminalVariableEntry } from '../common/chatVariableEntries.js' ;
5759import { ILanguageModelChatMetadataAndIdentifier , ILanguageModelsService } from '../common/languageModels.js' ;
5860import { ILanguageModelToolsService , ToolSet } from '../common/languageModelToolsService.js' ;
5961import { getCleanPromptName } from '../common/promptSyntax/config/promptFileLocations.js' ;
@@ -91,6 +93,7 @@ abstract class AbstractChatAttachmentWidget extends Disposable {
9193 protected readonly currentLanguageModel : ILanguageModelChatMetadataAndIdentifier | undefined ,
9294 @ICommandService protected readonly commandService : ICommandService ,
9395 @IOpenerService protected readonly openerService : IOpenerService ,
96+ @ITerminalService protected readonly terminalService ?: ITerminalService ,
9497 ) {
9598 super ( ) ;
9699 this . element = dom . append ( container , $ ( '.chat-attached-context-attachment.show-file-icons' ) ) ;
@@ -160,6 +163,11 @@ abstract class AbstractChatAttachmentWidget extends Disposable {
160163 return ;
161164 }
162165
166+ if ( resource . scheme === Schemas . vscodeTerminal ) {
167+ this . terminalService ?. openResource ( resource ) ;
168+ return ;
169+ }
170+
163171 // Open file in editor
164172 const openTextEditorOptions : ITextEditorOptions | undefined = range ? { selection : range } : undefined ;
165173 const options : OpenInternalOptions = {
@@ -170,6 +178,7 @@ abstract class AbstractChatAttachmentWidget extends Disposable {
170178 ...openOptions . editorOptions
171179 } ,
172180 } ;
181+
173182 await this . openerService . open ( resource , options ) ;
174183 this . _onDidOpen . fire ( ) ;
175184 this . element . focus ( ) ;
@@ -249,6 +258,118 @@ export class FileAttachmentWidget extends AbstractChatAttachmentWidget {
249258 }
250259}
251260
261+
262+ export class TerminalCommandAttachmentWidget extends AbstractChatAttachmentWidget {
263+
264+ constructor (
265+ attachment : ITerminalVariableEntry ,
266+ currentLanguageModel : ILanguageModelChatMetadataAndIdentifier | undefined ,
267+ options : { shouldFocusClearButton : boolean ; supportsDeletion : boolean } ,
268+ container : HTMLElement ,
269+ contextResourceLabels : ResourceLabels ,
270+ @ICommandService commandService : ICommandService ,
271+ @IOpenerService openerService : IOpenerService ,
272+ @IHoverService private readonly hoverService : IHoverService ,
273+ @ITerminalService protected override readonly terminalService : ITerminalService ,
274+ ) {
275+ super ( attachment , options , container , contextResourceLabels , currentLanguageModel , commandService , openerService , terminalService ) ;
276+
277+ const ariaLabel = localize ( 'chat.terminalCommand' , "Terminal command, {0}" , attachment . command ) ;
278+ const clickHandler = ( ) => this . openResource ( attachment . resource , { editorOptions : { preserveFocus : true } } , false , undefined ) ;
279+
280+ this . _register ( createTerminalCommandElements ( this . element , attachment , ariaLabel , this . hoverService , clickHandler ) ) ;
281+
282+ this . _register ( dom . addDisposableListener ( this . element , dom . EventType . KEY_DOWN , async ( e : KeyboardEvent ) => {
283+ if ( ( e . target as HTMLElement | null ) ?. closest ( '.monaco-button' ) ) {
284+ return ;
285+ }
286+ const event = new StandardKeyboardEvent ( e ) ;
287+ if ( event . equals ( KeyCode . Enter ) || event . equals ( KeyCode . Space ) ) {
288+ dom . EventHelper . stop ( e , true ) ;
289+ await clickHandler ( ) ;
290+ }
291+ } ) ) ;
292+
293+ this . attachClearButton ( ) ;
294+ }
295+ }
296+
297+ const MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH = 2000 ;
298+
299+ function createTerminalCommandElements (
300+ element : HTMLElement ,
301+ attachment : ITerminalVariableEntry ,
302+ ariaLabel : string ,
303+ hoverService : IHoverService ,
304+ clickHandler : ( ) => Promise < void >
305+ ) : IDisposable {
306+ const disposable = new DisposableStore ( ) ;
307+ element . ariaLabel = ariaLabel ;
308+ element . style . cursor = 'pointer' ;
309+
310+ const pillIcon = dom . $ ( 'div.chat-attached-context-pill' , { } , dom . $ ( 'span.codicon.codicon-terminal' ) ) ;
311+ const textLabel = dom . $ ( 'span.chat-attached-context-custom-text' , { } , attachment . command ) ;
312+ element . appendChild ( pillIcon ) ;
313+ element . appendChild ( textLabel ) ;
314+
315+ disposable . add ( dom . addDisposableListener ( element , dom . EventType . CLICK , e => {
316+ if ( ( e . target as HTMLElement | null ) ?. closest ( '.monaco-button' ) ) {
317+ return ;
318+ }
319+ void clickHandler ( ) ;
320+ } ) ) ;
321+
322+ const hoverElement = dom . $ ( 'div.chat-attached-context-hover' ) ;
323+ hoverElement . setAttribute ( 'aria-label' , ariaLabel ) ;
324+
325+ const commandTitle = dom . $ ( 'div' , { } , typeof attachment . exitCode === 'number'
326+ ? localize ( 'chat.terminalCommandHoverCommandTitleExit' , "Command: {0}, exit code: {1}" , attachment . command , attachment . exitCode )
327+ : localize ( 'chat.terminalCommandHoverCommandTitle' , "Command" ) ) ;
328+ commandTitle . classList . add ( 'attachment-additional-info' ) ;
329+ const commandBlock = dom . $ ( 'pre.chat-terminal-command-block' ) ;
330+ hoverElement . append ( commandTitle , commandBlock ) ;
331+
332+ if ( attachment . output && attachment . output . trim ( ) . length > 0 ) {
333+ const outputTitle = dom . $ ( 'div' , { } , localize ( 'chat.terminalCommandHoverOutputTitle' , "Output" ) ) ;
334+ outputTitle . classList . add ( 'attachment-additional-info' ) ;
335+ const outputBlock = dom . $ ( 'pre.chat-terminal-command-output' ) ;
336+ let outputText = attachment . output ;
337+ let truncated = false ;
338+ if ( outputText . length > MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH ) {
339+ outputText = `${ outputText . slice ( 0 , MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH ) } ...` ;
340+ truncated = true ;
341+ }
342+ outputBlock . textContent = outputText ;
343+ hoverElement . append ( outputTitle , outputBlock ) ;
344+
345+ if ( truncated ) {
346+ const truncatedInfo = dom . $ ( 'div' , { } , localize ( 'chat.terminalCommandHoverOutputTruncated' , "Output truncated to first {0} characters." , MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH ) ) ;
347+ truncatedInfo . classList . add ( 'attachment-additional-info' ) ;
348+ hoverElement . appendChild ( truncatedInfo ) ;
349+ }
350+ }
351+
352+ const hint = dom . $ ( 'div' , { } , localize ( 'chat.terminalCommandHoverHint' , "Click to focus this command in the terminal." ) ) ;
353+ hint . classList . add ( 'attachment-additional-info' ) ;
354+ hoverElement . appendChild ( hint ) ;
355+
356+ const separator = dom . $ ( 'div.chat-attached-context-url-separator' ) ;
357+ const openLink = dom . $ ( 'a.chat-attached-context-url' , { } , localize ( 'chat.terminalCommandHoverOpen' , "Open in terminal" ) ) ;
358+ disposable . add ( dom . addDisposableListener ( openLink , 'click' , e => {
359+ e . preventDefault ( ) ;
360+ e . stopPropagation ( ) ;
361+ void clickHandler ( ) ;
362+ } ) ) ;
363+ hoverElement . append ( separator , openLink ) ;
364+
365+ disposable . add ( hoverService . setupDelayedHover ( element , {
366+ ...commonHoverOptions ,
367+ content : hoverElement ,
368+ } , commonHoverLifecycleOptions ) ) ;
369+
370+ return disposable ;
371+ }
372+
252373export class ImageAttachmentWidget extends AbstractChatAttachmentWidget {
253374
254375 constructor (
0 commit comments