Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
106 changes: 57 additions & 49 deletions src/vs/workbench/common/contextkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,7 @@ export function getVisbileViewContextKey(viewId: string): string { return `view.

//#region < --- Resources --- >

export class ResourceContextKey {

// NOTE: DO NOT CHANGE THE DEFAULT VALUE TO ANYTHING BUT
// UNDEFINED! IT IS IMPORTANT THAT DEFAULTS ARE INHERITED
// FROM THE PARENT CONTEXT AND ONLY UNDEFINED DOES THIS

abstract class AbstractResourceContextKey {
static readonly Scheme = new RawContextKey<string>('resourceScheme', undefined, { type: 'string', description: localize('resourceScheme', "The scheme of the resource") });
static readonly Filename = new RawContextKey<string>('resourceFilename', undefined, { type: 'string', description: localize('resourceFilename', "The file name of the resource") });
static readonly Dirname = new RawContextKey<string>('resourceDirname', undefined, { type: 'string', description: localize('resourceDirname', "The folder name the resource is contained in") });
Expand All @@ -191,57 +186,41 @@ export class ResourceContextKey {
static readonly HasResource = new RawContextKey<boolean>('resourceSet', undefined, { type: 'boolean', description: localize('resourceSet', "Whether a resource is present or not") });
static readonly IsFileSystemResource = new RawContextKey<boolean>('isFileSystemResource', undefined, { type: 'boolean', description: localize('isFileSystemResource', "Whether the resource is backed by a file system provider") });

private readonly _disposables = new DisposableStore();
protected readonly _disposables = new DisposableStore();

private _value: URI | undefined;
private readonly _resourceKey: IContextKey<string | null>;
private readonly _schemeKey: IContextKey<string | null>;
private readonly _filenameKey: IContextKey<string | null>;
private readonly _dirnameKey: IContextKey<string | null>;
private readonly _pathKey: IContextKey<string | null>;
private readonly _langIdKey: IContextKey<string | null>;
private readonly _extensionKey: IContextKey<string | null>;
private readonly _hasResource: IContextKey<boolean>;
private readonly _isFileSystemResource: IContextKey<boolean>;
protected _value: URI | undefined;
protected readonly _resourceKey: IContextKey<string | null>;
protected readonly _schemeKey: IContextKey<string | null>;
protected readonly _filenameKey: IContextKey<string | null>;
protected readonly _dirnameKey: IContextKey<string | null>;
protected readonly _pathKey: IContextKey<string | null>;
protected readonly _langIdKey: IContextKey<string | null>;
protected readonly _extensionKey: IContextKey<string | null>;
protected readonly _hasResource: IContextKey<boolean>;
protected readonly _isFileSystemResource: IContextKey<boolean>;

constructor(
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IFileService private readonly _fileService: IFileService,
@ILanguageService private readonly _languageService: ILanguageService,
@IModelService private readonly _modelService: IModelService
@IContextKeyService protected readonly _contextKeyService: IContextKeyService,
@IFileService protected readonly _fileService: IFileService,
@ILanguageService protected readonly _languageService: ILanguageService,
@IModelService protected readonly _modelService: IModelService
) {
this._schemeKey = ResourceContextKey.Scheme.bindTo(this._contextKeyService);
this._filenameKey = ResourceContextKey.Filename.bindTo(this._contextKeyService);
this._dirnameKey = ResourceContextKey.Dirname.bindTo(this._contextKeyService);
this._pathKey = ResourceContextKey.Path.bindTo(this._contextKeyService);
this._langIdKey = ResourceContextKey.LangId.bindTo(this._contextKeyService);
this._resourceKey = ResourceContextKey.Resource.bindTo(this._contextKeyService);
this._extensionKey = ResourceContextKey.Extension.bindTo(this._contextKeyService);
this._hasResource = ResourceContextKey.HasResource.bindTo(this._contextKeyService);
this._isFileSystemResource = ResourceContextKey.IsFileSystemResource.bindTo(this._contextKeyService);

this._disposables.add(_fileService.onDidChangeFileSystemProviderRegistrations(() => {
const resource = this.get();
this._isFileSystemResource.set(Boolean(resource && _fileService.hasProvider(resource)));
}));

this._disposables.add(_modelService.onModelAdded(model => {
if (isEqual(model.uri, this.get())) {
this._setLangId();
}
}));
this._disposables.add(_modelService.onModelLanguageChanged(e => {
if (isEqual(e.model.uri, this.get())) {
this._setLangId();
}
}));
this._schemeKey = AbstractResourceContextKey.Scheme.bindTo(this._contextKeyService);
this._filenameKey = AbstractResourceContextKey.Filename.bindTo(this._contextKeyService);
this._dirnameKey = AbstractResourceContextKey.Dirname.bindTo(this._contextKeyService);
this._pathKey = AbstractResourceContextKey.Path.bindTo(this._contextKeyService);
this._langIdKey = AbstractResourceContextKey.LangId.bindTo(this._contextKeyService);
this._resourceKey = AbstractResourceContextKey.Resource.bindTo(this._contextKeyService);
this._extensionKey = AbstractResourceContextKey.Extension.bindTo(this._contextKeyService);
this._hasResource = AbstractResourceContextKey.HasResource.bindTo(this._contextKeyService);
this._isFileSystemResource = AbstractResourceContextKey.IsFileSystemResource.bindTo(this._contextKeyService);
}

dispose(): void {
this._disposables.dispose();
}

private _setLangId(): void {
protected _setLangId(): void {
const value = this.get();
if (!value) {
this._langIdKey.set(null);
Expand Down Expand Up @@ -270,11 +249,10 @@ export class ResourceContextKey {
});
}

private uriToPath(uri: URI): string {
protected uriToPath(uri: URI): string {
if (uri.scheme === Schemas.file) {
return uri.fsPath;
}

return uri.path;
}

Expand All @@ -298,6 +276,36 @@ export class ResourceContextKey {
}
}

export class ResourceContextKey extends AbstractResourceContextKey {
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IFileService fileService: IFileService,
@ILanguageService languageService: ILanguageService,
@IModelService modelService: IModelService
) {
super(contextKeyService, fileService, languageService, modelService);
this._disposables.add(fileService.onDidChangeFileSystemProviderRegistrations(() => {
const resource = this.get();
this._isFileSystemResource.set(Boolean(resource && fileService.hasProvider(resource)));
}));
this._disposables.add(modelService.onModelAdded(model => {
if (isEqual(model.uri, this.get())) {
this._setLangId();
}
}));
this._disposables.add(modelService.onModelLanguageChanged(e => {
if (isEqual(e.model.uri, this.get())) {
this._setLangId();
}
}));
}
}

export class StaticResourceContextKey extends AbstractResourceContextKey {
// No event listeners
}


//#endregion

export function applyAvailableEditorIds(contextKey: IContextKey<string>, editor: EditorInput | undefined | null, editorResolverService: IEditorResolverService): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation {
private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up GitHub Copilot and be signed in to use Chat."));
private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat."));

private static CHAT_REPORT_ISSUE_WITH_OUTPUT_ID = 'workbench.action.chat.reportIssueWithOutput';
private static readonly CHAT_RETRY_COMMAND_ID = 'workbench.action.chat.retrySetup';
private static readonly CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID = 'workbench.action.chat.reportIssueWithOutput';

private readonly _onUnresolvableError = this._register(new Emitter<void>());
readonly onUnresolvableError = this._onUnresolvableError.event;
Expand All @@ -192,7 +193,9 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation {
}

private registerCommands(): void {
this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_ID, async accessor => {

// Report issue with output command
this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID, async accessor => {
const outputService = accessor.get(IOutputService);
const textModelService = accessor.get(ITextModelService);
const issueService = accessor.get(IWorkbenchIssueService);
Expand Down Expand Up @@ -234,6 +237,22 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation {
data: outputData || localize('chatOutputChannelUnavailable', "GitHub Copilot Chat output channel not available. Please ensure the GitHub Copilot Chat extension is active and try again. If the issue persists, you can manually include relevant information from the Output panel (View > Output > GitHub Copilot Chat).")
});
}));

// Retry chat command
this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_RETRY_COMMAND_ID, async (accessor, sessionResource: URI) => {
const chatService = accessor.get(IChatService);
const chatWidgetService = accessor.get(IChatWidgetService);

const widget = chatWidgetService.getWidgetBySessionResource(sessionResource);
const lastRequest = widget?.viewModel?.model.getRequests().at(-1);
if (lastRequest) {
await chatService.resendRequest(lastRequest, {
...widget?.getModeRequestOptions(),
modeInfo: widget?.input.currentModeInfo,
userSelectedModelId: widget?.input.currentLanguageModel
});
}
}));
}

async invoke(request: IChatAgentRequest, progress: (parts: IChatProgress[]) => void): Promise<IChatAgentResult> {
Expand Down Expand Up @@ -397,9 +416,14 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation {
progress({
kind: 'command',
command: {
id: SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_ID,
id: SetupAgent.CHAT_RETRY_COMMAND_ID,
title: localize('retryChat', "Retry"),
arguments: [requestModel.session.sessionResource]
},
additionalCommands: [{
id: SetupAgent.CHAT_REPORT_ISSUE_WITH_OUTPUT_COMMAND_ID,
title: localize('reportChatIssue', "Report Issue"),
}
}]
});

// This means Chat is unhealthy and we cannot retry the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IChatContentPart, IChatContentPartRenderContext } from './chatContentPa
import { IChatProgressRenderableResponseContent } from '../../../common/model/chatModel.js';
import { IChatCommandButton } from '../../../common/chatService/chatService.js';
import { isResponseVM } from '../../../common/model/chatViewModel.js';
import { Command } from '../../../../../../editor/common/languages.js';

const $ = dom.$;

Expand All @@ -28,15 +29,28 @@ export class ChatCommandButtonContentPart extends Disposable implements IChatCon

this.domNode = $('.chat-command-button');
const enabled = !isResponseVM(context.element) || !context.element.isStale;

// Render the primary button
this.renderButton(this.domNode, commandButton.command, enabled);

// Render additional buttons if any
if (commandButton.additionalCommands) {
for (const command of commandButton.additionalCommands) {
this.renderButton(this.domNode, command, enabled, true);
}
}
}

private renderButton(container: HTMLElement, command: Command, enabled: boolean, secondary?: boolean): void {
const tooltip = enabled ?
commandButton.command.tooltip :
command.tooltip :
localize('commandButtonDisabled', "Button not available in restored chat");
const button = this._register(new Button(this.domNode, { ...defaultButtonStyles, supportIcons: true, title: tooltip }));
button.label = commandButton.command.title;
const button = this._register(new Button(container, { ...defaultButtonStyles, supportIcons: true, title: tooltip, secondary }));
button.label = command.title;
button.enabled = enabled;

// TODO still need telemetry for command buttons
this._register(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? []))));
this._register(button.onDidClick(() => this.commandService.executeCommand(command.id, ...(command.arguments ?? []))));
}

hasSameContent(other: IChatProgressRenderableResponseContent): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { ILabelService } from '../../../../../../platform/label/common/label.js'
import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js';
import { FolderThemeIcon, IThemeService } from '../../../../../../platform/theme/common/themeService.js';
import { fillEditorsDragData } from '../../../../../browser/dnd.js';
import { ResourceContextKey } from '../../../../../common/contextkeys.js';
import { StaticResourceContextKey } from '../../../../../common/contextkeys.js';
import { IEditorService, SIDE_GROUP } from '../../../../../services/editor/common/editorService.js';
import { INotebookDocumentService } from '../../../../../services/notebook/common/notebookDocumentService.js';
import { ExplorerFolderContext } from '../../../../files/common/files.js';
Expand Down Expand Up @@ -236,7 +236,7 @@ export class InlineAnchorWidget extends Disposable {
}
}

const resourceContextKey = this._register(new ResourceContextKey(contextKeyService, fileService, languageService, modelService));
const resourceContextKey = this._register(new StaticResourceContextKey(contextKeyService, fileService, languageService, modelService));
resourceContextKey.set(location.uri);
this._chatResourceContext.set(location.uri.toString());

Expand Down
12 changes: 9 additions & 3 deletions src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch

private _currentLayoutWidth = observableValue(this, 0);
private _isVisible = true;
private _elementBeingRendered: ChatTreeItem | undefined;
private _onDidChangeVisibility = this._register(new Emitter<boolean>());

/**
Expand Down Expand Up @@ -525,7 +526,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}

renderElement(node: ITreeNode<ChatTreeItem, FuzzyScore>, index: number, templateData: IChatListItemTemplate): void {
this.renderChatTreeItem(node.element, index, templateData);
this._elementBeingRendered = node.element;
try {
this.renderChatTreeItem(node.element, index, templateData);
} finally {
this._elementBeingRendered = undefined;
}
}

private clearRenderedParts(templateData: IChatListItemTemplate): void {
Expand All @@ -536,7 +542,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}
}

renderChatTreeItem(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate): void {
private renderChatTreeItem(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate): void {
if (templateData.currentElement && templateData.currentElement.id !== element.id) {
this.traceLayout('renderChatTreeItem', `Rendering a different element into the template, index=${index}`);
this.clearRenderedParts(templateData);
Expand Down Expand Up @@ -948,7 +954,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}

private updateItemHeight(templateData: IChatListItemTemplate): void {
if (!templateData.currentElement) {
if (!templateData.currentElement || templateData.currentElement === this._elementBeingRendered) {
return;
}

Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/contrib/chat/browser/widget/media/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -2239,6 +2239,8 @@ have to be updated for changes to the rules above, or to support more deeply nes

.interactive-item-container .chat-command-button {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export interface IChatAgentMarkdownContentWithVulnerability {
export interface IChatCommandButton {
command: Command;
kind: 'command';
additionalCommands?: Command[]; // rendered as secondary buttons
}

export interface IChatMoveMessage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm
}

xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void {
// Don't show is the terminal was launched by an extension or a feature like debug
if (hasKey(this._ctx.instance, { shellLaunchConfig: true }) && (this._ctx.instance.shellLaunchConfig.isExtensionOwnedTerminal || this._ctx.instance.shellLaunchConfig.isFeatureTerminal)) {
// Don't show if the terminal was launched by an extension or a feature like debug
if (hasKey(this._ctx.instance, { shellLaunchConfig: true }) && (this._ctx.instance.shellLaunchConfig.isExtensionOwnedTerminal || this._ctx.instance.shellLaunchConfig.isFeatureTerminal || this._ctx.instance.shellLaunchConfig.hideFromUser)) {
return;
}
// Don't show if disabled
Expand All @@ -124,7 +124,7 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm
private _createHint(): void {
const instance = this._ctx.instance instanceof TerminalInstance ? this._ctx.instance : undefined;
const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection);
if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || !!instance.shellLaunchConfig.attachPersistentProcess) {
if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || !!instance.shellLaunchConfig.attachPersistentProcess || commandDetectionCapability.commands.length > 0) {
return;
}

Expand Down
Loading