Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
30c3b10
fixes #259261
meganrogge Oct 20, 2025
7c335a2
Merge branch 'main' into merogge/terminal-last
meganrogge Oct 20, 2025
b5e7b3e
include output
meganrogge Oct 23, 2025
27a3917
do not encode commandLine and outpu t in uri
meganrogge Oct 23, 2025
919b8f7
Merge branch 'main' into merogge/terminal-last
meganrogge Oct 23, 2025
44580cf
re-add something
meganrogge Oct 23, 2025
01a9423
format
meganrogge Oct 23, 2025
82dca7c
revert a bunch of changes
meganrogge Oct 23, 2025
cc4ad9a
revert more changes
meganrogge Oct 23, 2025
e5a1fc6
revert more changes
meganrogge Oct 23, 2025
e7894c6
get it mostly working
meganrogge Oct 23, 2025
581dab5
handle disposal properly
meganrogge Oct 23, 2025
45fea3d
add exit code
meganrogge Oct 23, 2025
d0f7d31
slight tweaks
meganrogge Oct 23, 2025
fe1e01e
add terminalService openResource
meganrogge Oct 24, 2025
0652e4f
pass in URI to xtermTerminal instead of instanceId
meganrogge Oct 24, 2025
28d4442
get hover and click working, add custom widget
meganrogge Oct 24, 2025
cc79afd
get it to work!
meganrogge Oct 24, 2025
7aa7432
rm resource check from input part
meganrogge Oct 24, 2025
d95cd6a
part of #271388
meganrogge Oct 24, 2025
3c7521c
Update src/vs/workbench/contrib/chat/browser/actions/chatContext.ts
meganrogge Oct 24, 2025
44fcff8
use html
meganrogge Oct 24, 2025
1756c51
Merge branch 'main' into merogge/inline-output
meganrogge Oct 24, 2025
026842a
scroll down by default
meganrogge Oct 24, 2025
e35e8fb
Merge branch 'main' into merogge/inline-output
meganrogge Oct 24, 2025
9e040ad
rename to align
meganrogge Oct 24, 2025
4d24d8a
rename
meganrogge Oct 24, 2025
e2e8410
fix issue with context menu action not showing up
meganrogge Oct 24, 2025
3ccc52f
fix issue with context menu action not showing up
meganrogge Oct 24, 2025
c085718
followups
meganrogge Oct 24, 2025
d3ebafd
Merge branch 'merogge/cleanup' into merogge/inline-output
meganrogge Oct 24, 2025
3990af9
add stuff
meganrogge Oct 24, 2025
ec55245
fix bug
meganrogge Oct 24, 2025
c892ad4
Merge branch 'main' into merogge/inline-output
meganrogge Oct 24, 2025
d5c0cf5
don't fallback
meganrogge Oct 24, 2025
fdad452
go back to text
meganrogge Oct 24, 2025
b4673ba
keep last max lines
meganrogge Oct 24, 2025
104b442
Merge branch 'main' into merogge/inline-output
meganrogge Oct 27, 2025
64be1c5
rm `chatSessionId` form `ITerminalCommand`, add `terminalCommandUri` …
meganrogge Oct 27, 2025
2066294
chars, not lines
meganrogge Oct 27, 2025
90984d6
rm space between elements, style
meganrogge Oct 27, 2025
1c9cdcc
try to get scrollbar working
meganrogge Oct 27, 2025
b0f1afd
fix layout issues
meganrogge Oct 27, 2025
cf9fce6
set scrollbars to auto
meganrogge Oct 27, 2025
f4e6d01
@xterm/[email protected]
meganrogge Oct 28, 2025
ec7aca0
get it mostly working
meganrogge Oct 28, 2025
bafb618
get it to work on reload
meganrogge Oct 28, 2025
49de5ce
fix start/end
meganrogge Oct 28, 2025
4060a42
get reload working for real
meganrogge Oct 28, 2025
36388f0
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Oct 28, 2025
d402df0
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Oct 28, 2025
702e8d0
cleanup
meganrogge Oct 28, 2025
ffc125a
fix issue
meganrogge Oct 28, 2025
211e912
add action on command finished
meganrogge Oct 28, 2025
21336f5
fix action order
meganrogge Oct 28, 2025
5f72467
polish style of title
meganrogge Oct 29, 2025
a086976
get rid of height 100%
meganrogge Oct 29, 2025
a2aa259
rm inline width
meganrogge Oct 29, 2025
0df968f
add getHtmlForCommand, extract ToggleChatTerminalOutputAction
meganrogge Oct 29, 2025
c1dc885
use URI
meganrogge Oct 29, 2025
40005ea
use URI
meganrogge Oct 29, 2025
8b14e57
rm overkill function
meganrogge Oct 29, 2025
a7feba3
trim empty lines
meganrogge Oct 29, 2025
215d10a
revert a commit
meganrogge Oct 29, 2025
9c6c85c
don't set command uri too early
meganrogge Oct 29, 2025
86f2d33
Register actions as disposable
meganrogge Oct 29, 2025
c0174d1
rm width
meganrogge Oct 29, 2025
16738cb
wip, not working well
meganrogge Oct 29, 2025
b5ec5b0
Apply suggestion from @meganrogge
meganrogge Oct 29, 2025
8fa79c9
Apply suggestion from @meganrogge
meganrogge Oct 29, 2025
7860bc5
Apply suggestion from @meganrogge
meganrogge Oct 29, 2025
47e5709
cleanup
meganrogge Oct 29, 2025
ee28e69
clean up
meganrogge Oct 29, 2025
866dab6
fix double border
meganrogge Oct 29, 2025
354dca5
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Oct 30, 2025
9b2e7b4
Update src/vs/workbench/contrib/chat/browser/chatContentParts/toolInv…
meganrogge Oct 30, 2025
d404e7c
revert changes
meganrogge Oct 30, 2025
ca3c6ae
Merge branch 'main' into merogge/inline-output
meganrogge Oct 30, 2025
b60f110
add todo
meganrogge Oct 30, 2025
158c137
fix bug
meganrogge Oct 30, 2025
f2d7c98
rename function
meganrogge Oct 30, 2025
dd4924f
don't override startCol
meganrogge Oct 30, 2025
c81d9b1
rm lines
meganrogge Oct 30, 2025
f557a9d
use const enums
meganrogge Oct 30, 2025
fa99301
get output to show on reload
meganrogge Oct 30, 2025
07f235c
fix issue
meganrogge Oct 30, 2025
194dc79
rename
meganrogge Oct 30, 2025
21fa01d
cleanup
meganrogge Oct 30, 2025
4d54439
still have focus terminal action even if no command
meganrogge Oct 30, 2025
754d216
Merge branch 'main' into merogge/inline-output
meganrogge Oct 30, 2025
5767f4f
enable modifying for now
meganrogge Oct 30, 2025
341bad3
Merge branch 'main' into merogge/inline-output
meganrogge Oct 30, 2025
47bafa8
fix error, use UriComponents
meganrogge Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/vs/platform/terminal/common/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ interface IBaseTerminalCommand {
isTrusted: boolean;
timestamp: number;
duration: number;
id: string;

// Optional serializable
cwd: string | undefined;
Expand All @@ -309,6 +310,7 @@ export interface ITerminalCommand extends IBaseTerminalCommand {
readonly executedMarker?: IMarker;
readonly aliases?: string[][];
readonly wasReplayed?: boolean;
readonly chatSessionId?: string;

extractCommandLine(): string;
getOutput(): string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import { IMarkProperties, ISerializedTerminalCommand, ITerminalCommand } from '../capabilities.js';
import { ITerminalOutputMatcher, ITerminalOutputMatch } from '../../terminal.js';
import type { IBuffer, IBufferLine, IMarker, Terminal } from '@xterm/headless';
import { generateUuid } from '../../../../../base/common/uuid.js';

export interface ITerminalCommandProperties {
command: string;
commandLineConfidence: 'low' | 'medium' | 'high';
isTrusted: boolean;
timestamp: number;
duration: number;
id: string;
marker: IMarker | undefined;
cwd: string | undefined;
exitCode: number | undefined;
Expand All @@ -26,6 +28,7 @@ export interface ITerminalCommandProperties {
executedMarker?: IMarker | undefined;
aliases?: string[][] | undefined;
wasReplayed?: boolean | undefined;
sessionId?: string | undefined;
}

export class TerminalCommand implements ITerminalCommand {
Expand All @@ -42,12 +45,14 @@ export class TerminalCommand implements ITerminalCommand {
get executedMarker() { return this._properties.executedMarker; }
get aliases() { return this._properties.aliases; }
get wasReplayed() { return this._properties.wasReplayed; }
get chatSessionId() { return this._properties.sessionId; }
get cwd() { return this._properties.cwd; }
get exitCode() { return this._properties.exitCode; }
get commandStartLineContent() { return this._properties.commandStartLineContent; }
get markProperties() { return this._properties.markProperties; }
get executedX() { return this._properties.executedX; }
get startX() { return this._properties.startX; }
get id() { return this._properties.id; }

constructor(
private readonly _xterm: Terminal,
Expand All @@ -72,6 +77,7 @@ export class TerminalCommand implements ITerminalCommand {
command: isCommandStorageDisabled ? '' : serialized.command,
commandLineConfidence: serialized.commandLineConfidence ?? 'low',
isTrusted: serialized.isTrusted,
id: serialized.id,
promptStartMarker,
marker,
startX: serialized.startX,
Expand All @@ -85,7 +91,8 @@ export class TerminalCommand implements ITerminalCommand {
exitCode: serialized.exitCode,
markProperties: serialized.markProperties,
aliases: undefined,
wasReplayed: true
wasReplayed: true,
sessionId: undefined
});
return newCommand;
}
Expand All @@ -107,6 +114,7 @@ export class TerminalCommand implements ITerminalCommand {
timestamp: this.timestamp,
duration: this.duration,
markProperties: this.markProperties,
id: this.id,
};
}

Expand Down Expand Up @@ -271,13 +279,16 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
cwd?: string;
command?: string;
commandLineConfidence?: 'low' | 'medium' | 'high';
id: string;
sessionId?: string | undefined;

isTrusted?: boolean;
isInvalid?: boolean;

constructor(
private readonly _xterm: Terminal,
) {
this.id = generateUuid();
}

serialize(cwd: string | undefined): ISerializedTerminalCommand | undefined {
Expand All @@ -300,7 +311,8 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
commandStartLineContent: undefined,
timestamp: 0,
duration: 0,
markProperties: undefined
markProperties: undefined,
id: this.id
};
}

Expand All @@ -315,6 +327,7 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
command: ignoreCommandLine ? '' : (this.command || ''),
commandLineConfidence: ignoreCommandLine ? 'low' : (this.commandLineConfidence || 'low'),
isTrusted: !!this.isTrusted,
id: this.id,
promptStartMarker: this.promptStartMarker,
marker: this.commandStartMarker,
startX: this.commandStartX,
Expand All @@ -326,7 +339,8 @@ export class PartialTerminalCommand implements ICurrentPartialCommand {
cwd,
exitCode,
commandStartLineContent: this.commandStartLineContent,
markProperties
markProperties,
sessionId: this.sessionId
});
}

Expand Down
72 changes: 71 additions & 1 deletion src/vs/workbench/contrib/chat/browser/actions/chatContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
import { isElectron } from '../../../../../base/common/platform.js';
import { dirname } from '../../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
Expand All @@ -29,6 +29,9 @@ import { IChatWidget } from '../chat.js';
import { imageToHash, isImage } from '../chatPasteProviders.js';
import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';
import { ChatInstructionsPickerPick } from '../promptSyntax/attachInstructionsAction.js';
import { ITerminalService } from '../../../terminal/browser/terminal.js';
import { URI } from '../../../../../base/common/uri.js';
import { ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';


export class ChatContextContributions extends Disposable implements IWorkbenchContribution {
Expand Down Expand Up @@ -258,6 +261,73 @@ class ClipboardImageContextValuePick implements IChatContextValueItem {
}
}

export class TerminalContext implements IChatContextValueItem {

readonly type = 'valuePick';
readonly icon = Codicon.terminal;
readonly label = localize('terminal', 'Terminal');
constructor(private readonly _resource: URI, @ITerminalService private readonly _terminalService: ITerminalService) {

}
async isEnabled(widget: IChatWidget) {
const terminal = this._terminalService.getInstanceFromResource(this._resource);
return !!widget.attachmentCapabilities.supportsTerminalAttachments && terminal?.isDisposed === false;
}
async asAttachment(widget: IChatWidget): Promise<IChatRequestVariableEntry | undefined> {
const terminal = this._terminalService.getInstanceFromResource(this._resource);
if (!terminal) {
return;
}

const command = terminal.capabilities.get(TerminalCapability.CommandDetection)?.commands.find(cmd => cmd.id === this._resource.query);
if (!command) {
return;
}
const attachment: IChatRequestVariableEntry = {
kind: 'terminalCommand',
id: `terminalCommand:${Date.now()}}`,
value: this.asValue(command),
name: command.command,
command: command.command,
output: command.getOutput(),
exitCode: command.exitCode,
resource: this._resource
};
const cleanup = new DisposableStore();
let disposed = false;
const disposeCleanup = () => {
if (disposed) {
return;
}
disposed = true;
cleanup.dispose();
};
cleanup.add(widget.attachmentModel.onDidChange(e => {
if (e.deleted.includes(attachment.id)) {
disposeCleanup();
}
}));
cleanup.add(terminal.onDisposed(() => {
widget.attachmentModel.delete(attachment.id);
widget.refreshParsedInput();
disposeCleanup();
}));
return attachment;
}

private asValue(command: ITerminalCommand): string {
let value = `Command: ${command.command}`;
const output = command.getOutput();
if (output) {
value += `\nOutput:\n${output}`;
}
if (typeof command.exitCode === 'number') {
value += `\nExit Code: ${command.exitCode}`;
}
return value;
}
}

class ScreenshotContextValuePick implements IChatContextValueItem {

readonly type = 'valuePick';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class ChatAttachmentModel extends Disposable {
constructor(
@IFileService private readonly fileService: IFileService,
@ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService,
@IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService
@IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService,
) {
super();
}
Expand Down
123 changes: 122 additions & 1 deletion src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../base/common/network.js';
import { basename, dirname } from '../../../../base/common/path.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
Expand Down Expand Up @@ -52,8 +53,9 @@ import { revealInSideBarCommand } from '../../files/browser/fileActions.contribu
import { CellUri } from '../../notebook/common/notebookCommon.js';
import { INotebookService } from '../../notebook/common/notebookService.js';
import { getHistoryItemEditorTitle } from '../../scm/browser/util.js';
import { ITerminalService } from '../../terminal/browser/terminal.js';
import { IChatContentReference } from '../common/chatService.js';
import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry } from '../common/chatVariableEntries.js';
import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry } from '../common/chatVariableEntries.js';
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js';
import { ILanguageModelToolsService, ToolSet } from '../common/languageModelToolsService.js';
import { getCleanPromptName } from '../common/promptSyntax/config/promptFileLocations.js';
Expand Down Expand Up @@ -91,6 +93,7 @@ abstract class AbstractChatAttachmentWidget extends Disposable {
protected readonly currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
@ICommandService protected readonly commandService: ICommandService,
@IOpenerService protected readonly openerService: IOpenerService,
@ITerminalService protected readonly terminalService?: ITerminalService,
) {
super();
this.element = dom.append(container, $('.chat-attached-context-attachment.show-file-icons'));
Expand Down Expand Up @@ -160,6 +163,11 @@ abstract class AbstractChatAttachmentWidget extends Disposable {
return;
}

if (resource.scheme === Schemas.vscodeTerminal) {
this.terminalService?.openResource(resource);
return;
}

// Open file in editor
const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined;
const options: OpenInternalOptions = {
Expand All @@ -170,6 +178,7 @@ abstract class AbstractChatAttachmentWidget extends Disposable {
...openOptions.editorOptions
},
};

await this.openerService.open(resource, options);
this._onDidOpen.fire();
this.element.focus();
Expand Down Expand Up @@ -249,6 +258,118 @@ export class FileAttachmentWidget extends AbstractChatAttachmentWidget {
}
}


export class TerminalCommandAttachmentWidget extends AbstractChatAttachmentWidget {

constructor(
attachment: ITerminalVariableEntry,
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
container: HTMLElement,
contextResourceLabels: ResourceLabels,
@ICommandService commandService: ICommandService,
@IOpenerService openerService: IOpenerService,
@IHoverService private readonly hoverService: IHoverService,
@ITerminalService protected override readonly terminalService: ITerminalService,
) {
super(attachment, options, container, contextResourceLabels, currentLanguageModel, commandService, openerService, terminalService);

const ariaLabel = localize('chat.terminalCommand', "Terminal command, {0}", attachment.command);
const clickHandler = () => this.openResource(attachment.resource, { editorOptions: { preserveFocus: true } }, false, undefined);

this._register(createTerminalCommandElements(this.element, attachment, ariaLabel, this.hoverService, clickHandler));

this._register(dom.addDisposableListener(this.element, dom.EventType.KEY_DOWN, async (e: KeyboardEvent) => {
if ((e.target as HTMLElement | null)?.closest('.monaco-button')) {
return;
}
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
dom.EventHelper.stop(e, true);
await clickHandler();
}
}));

this.attachClearButton();
}
}

const MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH = 2000;

function createTerminalCommandElements(
element: HTMLElement,
attachment: ITerminalVariableEntry,
ariaLabel: string,
hoverService: IHoverService,
clickHandler: () => Promise<void>
): IDisposable {
const disposable = new DisposableStore();
element.ariaLabel = ariaLabel;
element.style.cursor = 'pointer';

const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-terminal'));
const textLabel = dom.$('span.chat-attached-context-custom-text', {}, attachment.command);
element.appendChild(pillIcon);
element.appendChild(textLabel);

disposable.add(dom.addDisposableListener(element, dom.EventType.CLICK, e => {
if ((e.target as HTMLElement | null)?.closest('.monaco-button')) {
return;
}
void clickHandler();
}));

const hoverElement = dom.$('div.chat-attached-context-hover');
hoverElement.setAttribute('aria-label', ariaLabel);

const commandTitle = dom.$('div', {}, typeof attachment.exitCode === 'number'
? localize('chat.terminalCommandHoverCommandTitleExit', "Command: {0}, exit code: {1}", attachment.command, attachment.exitCode)
: localize('chat.terminalCommandHoverCommandTitle', "Command"));
commandTitle.classList.add('attachment-additional-info');
const commandBlock = dom.$('pre.chat-terminal-command-block');
hoverElement.append(commandTitle, commandBlock);

if (attachment.output && attachment.output.trim().length > 0) {
const outputTitle = dom.$('div', {}, localize('chat.terminalCommandHoverOutputTitle', "Output"));
outputTitle.classList.add('attachment-additional-info');
const outputBlock = dom.$('pre.chat-terminal-command-output');
let outputText = attachment.output;
let truncated = false;
if (outputText.length > MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH) {
outputText = `${outputText.slice(0, MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH)}...`;
truncated = true;
}
outputBlock.textContent = outputText;
hoverElement.append(outputTitle, outputBlock);

if (truncated) {
const truncatedInfo = dom.$('div', {}, localize('chat.terminalCommandHoverOutputTruncated', "Output truncated to first {0} characters.", MAX_TERMINAL_ATTACHMENT_OUTPUT_LENGTH));
truncatedInfo.classList.add('attachment-additional-info');
hoverElement.appendChild(truncatedInfo);
}
}

const hint = dom.$('div', {}, localize('chat.terminalCommandHoverHint', "Click to focus this command in the terminal."));
hint.classList.add('attachment-additional-info');
hoverElement.appendChild(hint);

const separator = dom.$('div.chat-attached-context-url-separator');
const openLink = dom.$('a.chat-attached-context-url', {}, localize('chat.terminalCommandHoverOpen', "Open in terminal"));
disposable.add(dom.addDisposableListener(openLink, 'click', e => {
e.preventDefault();
e.stopPropagation();
void clickHandler();
}));
hoverElement.append(separator, openLink);

disposable.add(hoverService.setupDelayedHover(element, {
...commonHoverOptions,
content: hoverElement,
}, commonHoverLifecycleOptions));

return disposable;
}

export class ImageAttachmentWidget extends AbstractChatAttachmentWidget {

constructor(
Expand Down
Loading