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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { derived, IObservable } from '../../../../base/common/observable.js';
import { derived, IObservable, observableValue, ISettableObservable } from '../../../../base/common/observable.js';
import { joinPath } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
Expand All @@ -23,6 +23,13 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
declare readonly _serviceBrand: undefined;

readonly activeProjectRoot: IObservable<URI | undefined>;
readonly hasOverrideProjectRoot: IObservable<boolean>;

/**
* Transient override for the project root. When set, `activeProjectRoot`
* returns this value instead of the session-derived root.
*/
private readonly _overrideRoot: ISettableObservable<URI | undefined>;

/**
* CLI-accessible user directories for customization file filtering and creation.
Expand Down Expand Up @@ -50,17 +57,39 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
includedUserFileRoots: this._cliUserRoots,
};

this._overrideRoot = observableValue(this, undefined);

this.activeProjectRoot = derived(reader => {
const override = this._overrideRoot.read(reader);
if (override) {
return override;
}
const session = this.sessionsService.activeSession.read(reader);
return session?.worktree ?? session?.repository;
});

this.hasOverrideProjectRoot = derived(reader => {
return this._overrideRoot.read(reader) !== undefined;
});
}

getActiveProjectRoot(): URI | undefined {
const override = this._overrideRoot.get();
if (override) {
return override;
}
const session = this.sessionsService.getActiveSession();
return session?.worktree ?? session?.repository;
}

setOverrideProjectRoot(root: URI): void {
this._overrideRoot.set(root, undefined);
}

clearOverrideProjectRoot(): void {
this._overrideRoot.set(undefined, undefined);
}

readonly managementSections: readonly AICustomizationManagementSection[] = [
AICustomizationManagementSection.Agents,
AICustomizationManagementSection.Skills,
Expand Down
9 changes: 4 additions & 5 deletions src/vs/sessions/contrib/chat/browser/promptsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { IWorkbenchEnvironmentService } from '../../../../workbench/services/env
import { IPathService } from '../../../../workbench/services/path/common/pathService.js';
import { ISearchService } from '../../../../workbench/services/search/common/search.js';
import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';

export class AgenticPromptsService extends PromptsService {
private _copilotRoot: URI | undefined;
Expand Down Expand Up @@ -67,7 +67,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator {
@IUserDataProfileService userDataService: IUserDataProfileService,
@ILogService logService: ILogService,
@IPathService pathService: IPathService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
@IAICustomizationWorkspaceService private readonly customizationWorkspaceService: IAICustomizationWorkspaceService,
) {
super(
fileService,
Expand Down Expand Up @@ -95,7 +95,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator {
}

protected override onDidChangeWorkspaceFolders(): Event<void> {
return Event.fromObservableLight(this.activeSessionService.activeSession);
return Event.fromObservableLight(this.customizationWorkspaceService.activeProjectRoot);
}

public override async getHookSourceFolders(): Promise<readonly URI[]> {
Expand All @@ -108,8 +108,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator {
}

private getActiveWorkspaceFolder(): IWorkspaceFolder | undefined {
const session = this.activeSessionService.getActiveSession();
const root = session?.worktree ?? session?.repository;
const root = this.customizationWorkspaceService.getActiveProjectRoot();
if (!root) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ class GroupHeaderRenderer implements IListRenderer<IGroupHeaderEntry, IGroupHead
class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICustomizationItemTemplateData> {
readonly templateId = 'aiCustomizationItem';

constructor(
@IHoverService private readonly hoverService: IHoverService,
@ILabelService private readonly labelService: ILabelService,
) { }

renderTemplate(container: HTMLElement): IAICustomizationItemTemplateData {
const disposables = new DisposableStore();
const elementDisposables = new DisposableStore();
Expand Down Expand Up @@ -236,6 +241,18 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
templateData.elementDisposables.clear();
const element = entry.item;

// Hover tooltip: name + full path
templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.container, () => {
const uriLabel = this.labelService.getUriLabel(element.uri, { relative: false });
return {
content: `${element.name}\n${uriLabel}`,
appearance: {
compact: true,
skipFadeInAnimation: true,
}
};
}));

// Name with highlights
templateData.nameLabel.set(element.name, element.nameMatches);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,14 @@ import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditor
import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js';
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { HOOKS_SOURCE_FOLDER } from '../../common/promptSyntax/config/promptFileLocations.js';
import { COPILOT_CLI_HOOK_TYPE_MAP } from '../../common/promptSyntax/hookSchema.js';
import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput.js';
import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js';
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { IWorkbenchMcpServer } from '../../../mcp/common/mcpTypes.js';

const $ = DOM.$;
Expand Down Expand Up @@ -168,6 +171,11 @@ export class AICustomizationManagementEditor extends EditorPane {
private readonly editorDisposables = this._register(new DisposableStore());
private _editorContentChanged = false;

// Folder picker (sessions window only)
private folderPickerContainer: HTMLElement | undefined;
private folderPickerLabel: HTMLElement | undefined;
private folderPickerClearButton: HTMLElement | undefined;

private readonly inEditorContextKey: IContextKey<boolean>;
private readonly sectionContextKey: IContextKey<string>;

Expand All @@ -187,6 +195,8 @@ export class AICustomizationManagementEditor extends EditorPane {
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@ITextFileService private readonly textFileService: ITextFileService,
@IFileService private readonly fileService: IFileService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IHoverService private readonly hoverService: IHoverService,
) {
super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService);

Expand Down Expand Up @@ -264,7 +274,8 @@ export class AICustomizationManagementEditor extends EditorPane {
layout: (width, _, height) => {
this.sidebarContainer.style.width = `${width}px`;
if (height !== undefined) {
const listHeight = height - 8;
const footerHeight = this.folderPickerContainer?.offsetHeight ?? 0;
const listHeight = height - 8 - footerHeight;
this.sectionsList.layout(listHeight, width);
}
},
Expand Down Expand Up @@ -350,6 +361,72 @@ export class AICustomizationManagementEditor extends EditorPane {
this.selectSection(e.elements[0].id);
}
}));

// Folder picker (sessions window only)
if (this.workspaceService.isSessionsWindow) {
this.createFolderPicker(sidebarContent);
}
}

private createFolderPicker(sidebarContent: HTMLElement): void {
const footer = this.folderPickerContainer = DOM.append(sidebarContent, $('.sidebar-folder-picker'));

Comment thread
joshspicer marked this conversation as resolved.
const button = DOM.append(footer, $('button.folder-picker-button'));
button.setAttribute('aria-label', localize('browseFolder', "Browse folder"));

const folderIcon = DOM.append(button, $(`.codicon.codicon-${Codicon.folder.id}`));
folderIcon.classList.add('folder-picker-icon');

this.folderPickerLabel = DOM.append(button, $('span.folder-picker-label'));

this.folderPickerClearButton = DOM.append(footer, $('button.folder-picker-clear'));
this.folderPickerClearButton.setAttribute('aria-label', localize('clearFolderOverride', "Reset to session folder"));
DOM.append(this.folderPickerClearButton, $(`.codicon.codicon-${Codicon.close.id}`));

// Clicking the main button opens the folder dialog
this.editorDisposables.add(DOM.addDisposableListener(button, 'click', () => {
this.browseForFolder();
}));

// Clear button resets to session default
this.editorDisposables.add(DOM.addDisposableListener(this.folderPickerClearButton, 'click', () => {
this.workspaceService.clearOverrideProjectRoot();
}));

// Hover showing full path
this.editorDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), button, () => {
const root = this.workspaceService.getActiveProjectRoot();
return root?.fsPath ?? '';
}));

// Keep label and clear button in sync with the active root
this.editorDisposables.add(autorun(reader => {
const root = this.workspaceService.activeProjectRoot.read(reader);
const hasOverride = this.workspaceService.hasOverrideProjectRoot.read(reader);
this.updateFolderPickerLabel(root, hasOverride);
}));
}

private updateFolderPickerLabel(root: URI | undefined, hasOverride: boolean): void {
if (this.folderPickerLabel) {
this.folderPickerLabel.textContent = root ? basename(root) : localize('noFolder', "No folder");
}
if (this.folderPickerClearButton) {
this.folderPickerClearButton.style.display = hasOverride ? '' : 'none';
}
}

private async browseForFolder(): Promise<void> {
const result = await this.fileDialogService.showOpenDialog({
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
title: localize('selectFolder', "Select Folder to Explore"),
defaultUri: this.workspaceService.getActiveProjectRoot(),
});
if (result?.[0]) {
this.workspaceService.setOverrideProjectRoot(result[0]);
}
}

private createContent(): void {
Expand Down Expand Up @@ -585,6 +662,9 @@ export class AICustomizationManagementEditor extends EditorPane {
}

override async setInput(input: AICustomizationManagementEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
// On (re)open, clear any override so the root comes from the default source
this.workspaceService.clearOverrideProjectRoot();

this.inEditorContextKey.set(true);
this.sectionContextKey.set(this.selectedSection);

Expand All @@ -603,6 +683,8 @@ export class AICustomizationManagementEditor extends EditorPane {
if (this.viewMode === 'mcpDetail') {
this.goBackFromMcpDetail();
}
// Clear transient folder override on close
this.workspaceService.clearOverrideProjectRoot();
super.clearInput();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js';
import { constObservable, derived, IObservable, observableFromEventOpts } from '../../../../../base/common/observable.js';
import { URI } from '../../../../../base/common/uri.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js';
Expand Down Expand Up @@ -63,6 +63,10 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic

readonly isSessionsWindow = false;

readonly hasOverrideProjectRoot = constObservable(false);
setOverrideProjectRoot(_root: URI): void { }
clearOverrideProjectRoot(): void { }

async commitFiles(_projectRoot: URI, _fileUris: URI[]): Promise<void> {
// No-op in core VS Code.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,65 @@
overflow: hidden;
}

/* Folder picker footer (sessions window only) */
.ai-customization-management-editor .sidebar-folder-picker {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 2px;
padding: 6px 4px;
border-top: 1px solid var(--vscode-sideBarSectionHeader-border, transparent);
}

.ai-customization-management-editor .folder-picker-button {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
padding: 4px 6px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--vscode-descriptionForeground);
cursor: pointer;
font-size: 11px;
}

.ai-customization-management-editor .folder-picker-button:hover {
background-color: var(--vscode-list-hoverBackground);
}

.ai-customization-management-editor .folder-picker-icon {
flex-shrink: 0;
font-size: 14px;
}

.ai-customization-management-editor .folder-picker-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.ai-customization-management-editor .folder-picker-clear {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--vscode-descriptionForeground);
cursor: pointer;
font-size: 12px;
}

.ai-customization-management-editor .folder-picker-clear:hover {
background-color: var(--vscode-list-hoverBackground);
}

/* Section list items */
.ai-customization-management-editor .section-list-item {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,22 @@ export interface IAICustomizationWorkspaceService {
* Launches the AI-guided creation flow for the given customization type.
*/
generateCustomization(type: PromptsType): Promise<void>;

/**
* Whether a transient project root override is currently active.
*/
readonly hasOverrideProjectRoot: IObservable<boolean>;

/**
* Sets a transient override for the active project root.
* While set, `activeProjectRoot` returns this value instead of the
* session- or workspace-derived root. Call `clearOverrideProjectRoot()` to revert.
*/
setOverrideProjectRoot(root: URI): void;

/**
* Clears the transient project root override, reverting to the
* session-derived (or workspace-derived) root.
*/
clearOverrideProjectRoot(): void;
}
Loading