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
48 changes: 41 additions & 7 deletions src/vs/sessions/AI_CUSTOMIZATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ src/vs/workbench/contrib/chat/browser/aiCustomization/
├── aiCustomizationManagement.ts # IDs + context keys
├── aiCustomizationManagementEditor.ts # SplitView list/editor
├── aiCustomizationManagementEditorInput.ts # Singleton input
├── aiCustomizationListWidget.ts # Search + grouped list
├── aiCustomizationListWidget.ts # Search + grouped list + harness toggle
├── aiCustomizationDebugPanel.ts # Debug diagnostics panel
├── aiCustomizationWorkspaceService.ts # Core VS Code workspace service impl
├── customizationHarnessService.ts # Core harness service impl (VS Code harness)
├── customizationCreatorService.ts # AI-guided creation flow
├── mcpListWidget.ts # MCP servers section
├── aiCustomizationIcons.ts # Icons
└── media/
└── aiCustomizationManagement.css

src/vs/workbench/contrib/chat/common/
└── aiCustomizationWorkspaceService.ts # IAICustomizationWorkspaceService + IStorageSourceFilter
├── aiCustomizationWorkspaceService.ts # IAICustomizationWorkspaceService + IStorageSourceFilter
└── customizationHarnessService.ts # ICustomizationHarnessService + CustomizationHarness enum
```

The tree view and overview live in `vs/sessions` (sessions window only):
Expand All @@ -44,6 +46,7 @@ Sessions-specific overrides:
```
src/vs/sessions/contrib/chat/browser/
├── aiCustomizationWorkspaceService.ts # Sessions workspace service override
├── customizationHarnessService.ts # Sessions harness service (CLI + Claude harnesses)
└── promptsService.ts # AgenticPromptsService (CLI user roots)
src/vs/sessions/contrib/sessions/browser/
├── customizationCounts.ts # Source count utilities (type-aware)
Expand All @@ -57,10 +60,31 @@ The `IAICustomizationWorkspaceService` interface controls per-window behavior:
| Property / Method | Core VS Code | Sessions Window |
|----------|-------------|----------|
| `managementSections` | All sections except Models | Same minus MCP |
| `getStorageSourceFilter(type)` | All sources, no user root filter | Per-type (see below) |
| `getStorageSourceFilter(type)` | Delegates to `ICustomizationHarnessService` | Delegates to `ICustomizationHarnessService` |
| `isSessionsWindow` | `false` | `true` |
| `activeProjectRoot` | First workspace folder | Active session worktree |

### ICustomizationHarnessService

A harness represents the AI execution environment that consumes customizations.
Storage answers "where did this come from?"; harness answers "who consumes it?".

The service is defined in `common/customizationHarnessService.ts` which also provides:
- **`CustomizationHarnessServiceBase`** — reusable base class handling active-harness state, the observable list, and `getStorageSourceFilter` dispatch.
- **Factory functions** — `createVSCodeHarnessDescriptor`, `createCliHarnessDescriptor`, `createClaudeHarnessDescriptor` — parameterized by an `extras` array (the additional storage sources beyond `local`, `user`, `plugin`). Core passes `[PromptsStorage.extension]`; sessions passes `[BUILTIN_STORAGE]`.
- **Well-known root helpers** — `getCliUserRoots(userHome)` and `getClaudeUserRoots(userHome)` centralize the `~/.copilot`, `~/.claude`, `~/.agents` path knowledge so it isn't duplicated.

Available harnesses:

| Harness | Label | Description |
|---------|-------|-------------|
| `vscode` | VS Code | Shows all storage sources (default in core) |
| `cli` | Copilot CLI | Restricts user roots to `~/.copilot`, `~/.claude`, `~/.agents` |
| `claude` | Claude | Restricts user roots to `~/.claude` |

In core VS Code, all three harnesses are registered; VS Code is the default.
In sessions, `cli` and `claude` harnesses are registered with a toggle bar above the list.

### IStorageSourceFilter

A unified per-type filter controlling which storage sources and user file roots are visible.
Expand All @@ -75,13 +99,23 @@ interface IStorageSourceFilter {

The shared `applyStorageSourceFilter()` helper applies this filter to any `{uri, storage}` array.

**Sessions filter behavior by type:**
**Sessions filter behavior by harness and type:**

CLI harness:

| Type | sources | includedUserFileRoots |
|------|---------|----------------------|
| Hooks | `[local, plugin]` | N/A |
| Prompts | `[local, user, plugin, builtin]` | `undefined` (all roots) |
| Agents, Skills, Instructions | `[local, user, plugin, builtin]` | `[~/.copilot, ~/.claude, ~/.agents]` |

Claude harness:

| Type | sources | includedUserFileRoots |
|------|---------|----------------------|
| Hooks | `[local]` | N/A |
| Prompts | `[local, user]` | `undefined` (all roots) |
| Agents, Skills, Instructions | `[local, user]` | `[~/.copilot, ~/.claude, ~/.agents]` |
| Hooks | `[local, plugin]` | N/A |
| Prompts | `[local, user, plugin, builtin]` | `undefined` (all roots) |
| Agents, Skills, Instructions | `[local, user, plugin, builtin]` | `[~/.claude]` |

**Core VS Code:** All types use `[local, user, extension, plugin]` with no user root filter.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@
*--------------------------------------------------------------------------------------------*/

import { derived, IObservable, observableValue, ISettableObservable } from '../../../../base/common/observable.js';
import { joinPath, relativePath } from '../../../../base/common/resources.js';
import { relativePath } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter, applyStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IChatPromptSlashCommand, IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { BUILTIN_STORAGE } from '../../chat/common/builtinPromptsStorage.js';
import { IChatPromptSlashCommand, IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js';
import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
import { IPathService } from '../../../../workbench/services/path/common/pathService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IFileService } from '../../../../platform/files/common/files.js';
Expand Down Expand Up @@ -43,37 +42,16 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
*/
private readonly _overrideRoot: ISettableObservable<URI | undefined>;

/**
* CLI-accessible user directories for customization file filtering and creation.
*/
private readonly _cliUserRoots: readonly URI[];

/**
* Pre-built filter for types that should only show CLI-accessible user roots.
*/
private readonly _cliUserFilter: IStorageSourceFilter;

constructor(
@ISessionsManagementService private readonly sessionsService: ISessionsManagementService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IPromptsService private readonly promptsService: IPromptsService,
@IPathService pathService: IPathService,
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
@ICommandService private readonly commandService: ICommandService,
@ILogService private readonly logService: ILogService,
@IFileService private readonly fileService: IFileService,
@INotificationService private readonly notificationService: INotificationService,
) {
const userHome = pathService.userHome({ preferLocal: true });
this._cliUserRoots = [
joinPath(userHome, '.copilot'),
joinPath(userHome, '.claude'),
joinPath(userHome, '.agents'),
];
this._cliUserFilter = {
sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin, BUILTIN_STORAGE],
includedUserFileRoots: this._cliUserRoots,
};

this._overrideRoot = observableValue(this, undefined);

this.activeProjectRoot = derived(reader => {
Expand Down Expand Up @@ -117,24 +95,8 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
AICustomizationManagementSection.Plugins,
];

private static readonly _hooksFilter: IStorageSourceFilter = {
sources: [PromptsStorage.local, PromptsStorage.plugin],
};

private static readonly _allUserRootsFilter: IStorageSourceFilter = {
sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin, BUILTIN_STORAGE],
};

getStorageSourceFilter(type: PromptsType): IStorageSourceFilter {
if (type === PromptsType.hook) {
return SessionsAICustomizationWorkspaceService._hooksFilter;
}
if (type === PromptsType.prompt) {
// Prompts are shown from all user roots (including VS Code profile)
return SessionsAICustomizationWorkspaceService._allUserRootsFilter;
}
// Other types only show user files from CLI-accessible roots (~/.copilot, ~/.claude, ~/.agents)
return this._cliUserFilter;
return this.harnessService.getStorageSourceFilter(type);
}

readonly isSessionsWindow = true;
Expand Down
3 changes: 3 additions & 0 deletions src/vs/sessions/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import { AgenticPromptsService } from './promptsService.js';
import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { ISessionsConfigurationService, SessionsConfigurationService } from './sessionsConfigurationService.js';
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { SessionsAICustomizationWorkspaceService } from './aiCustomizationWorkspaceService.js';
import { SessionsCustomizationHarnessService } from './customizationHarnessService.js';
import { ChatViewContainerId, ChatViewId } from '../../../../workbench/contrib/chat/browser/chat.js';
import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
import { NewChatViewPane, SessionsViewId } from './newChatViewPane.js';
Expand Down Expand Up @@ -195,3 +197,4 @@ registerWorkbenchContribution2(RunScriptContribution.ID, RunScriptContribution,
registerSingleton(IPromptsService, AgenticPromptsService, InstantiationType.Delayed);
registerSingleton(ISessionsConfigurationService, SessionsConfigurationService, InstantiationType.Delayed);
registerSingleton(IAICustomizationWorkspaceService, SessionsAICustomizationWorkspaceService, InstantiationType.Delayed);
registerSingleton(ICustomizationHarnessService, SessionsCustomizationHarnessService, InstantiationType.Delayed);
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import {
CustomizationHarness,
CustomizationHarnessServiceBase,
createCliHarnessDescriptor,
getCliUserRoots,
} from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { IPathService } from '../../../../workbench/services/path/common/pathService.js';
import { BUILTIN_STORAGE } from '../common/builtinPromptsStorage.js';

/**
* Sessions-window override of the customization harness service.
*
* Only the CLI harness is registered because sessions always run via
* the Copilot CLI. With a single harness the toggle bar is hidden.
*/
export class SessionsCustomizationHarnessService extends CustomizationHarnessServiceBase {
constructor(
@IPathService pathService: IPathService,
) {
const userHome = pathService.userHome({ preferLocal: true });
const extras = [BUILTIN_STORAGE];
super(
[createCliHarnessDescriptor(getCliUserRoots(userHome), extras)],
CustomizationHarness.CLI,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { parse as parseJSONC } from '../../../../../base/common/json.js';
import { Schemas } from '../../../../../base/common/network.js';
import { OS } from '../../../../../base/common/platform.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js';

export { truncateToFirstSentence } from './aiCustomizationListWidgetUtils.js';

Expand Down Expand Up @@ -461,6 +462,7 @@ export class AICustomizationListWidget extends Disposable {
@IFileService private readonly fileService: IFileService,
@IPathService private readonly pathService: IPathService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
) {
super();
this.element = $('.ai-customization-list-widget');
Expand All @@ -473,6 +475,13 @@ export class AICustomizationListWidget extends Disposable {
this.refresh();
}));

// Re-filter when the active harness changes
this._register(autorun(reader => {
this.harnessService.activeHarness.read(reader);
this.updateAddButton();
this.refresh();
}));

}

private create(): void {
Expand Down Expand Up @@ -766,6 +775,8 @@ export class AICustomizationListWidget extends Disposable {

/**
* Gets the dropdown actions for the add button.
* Respects the active harness filter — user-scoped creation is only
* offered when the harness shows all user roots (i.e. "Local").
*/
private getDropdownActions(): Action[] {
this.dropdownActionDisposables.clear();
Expand All @@ -788,11 +799,22 @@ export class AICustomizationListWidget extends Disposable {
return actions;
}

// User-scoped creation: in core VS Code, only when the harness shows
// all user roots (no includedUserFileRoots restriction). Restricted
// harnesses like CLI/Claude filter user roots, so creating in the
// VS Code profile directory wouldn't be visible in the current view.
// In sessions, user creation always targets CLI-accessible paths
// (AgenticPromptsService routes to ~/.copilot/...) so it's always valid.
const filter = this.harnessService.getStorageSourceFilter(promptType);
const showUserCreate = this.workspaceService.isSessionsWindow || !filter.includedUserFileRoots;

if (this.workspaceService.isSessionsWindow) {
// Sessions: primary is workspace, dropdown has user
actions.push(this.dropdownActionDisposables.add(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => {
this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' });
})));
if (showUserCreate) {
actions.push(this.dropdownActionDisposables.add(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => {
this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' });
})));
}
// For instructions: offer AGENTS.md at workspace root
if (promptType === PromptsType.instructions && this.hasActiveWorkspace()) {
actions.push(this.dropdownActionDisposables.add(new Action('createAgentsMd', `$(${Codicon.file.id}) New ${AGENT_MD_FILENAME}`, undefined, true, () => {
Expand All @@ -806,9 +828,11 @@ export class AICustomizationListWidget extends Disposable {
this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' });
})));
}
actions.push(this.dropdownActionDisposables.add(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => {
this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' });
})));
if (showUserCreate) {
actions.push(this.dropdownActionDisposables.add(new Action('createUser', `$(${Codicon.account.id}) New ${typeLabel} (User)`, undefined, true, () => {
this._onDidRequestCreateManual.fire({ type: promptType, target: 'user' });
})));
}
}

return actions;
Expand Down
Loading
Loading