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
1 change: 1 addition & 0 deletions .github/instructions/sessions.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ When working on files under `src/vs/sessions/`, use these skills for detailed gu
The Agents window can run on touch-capable platforms (notably iOS). Follow these rules for all DOM interaction code:

- Do not use `EventType.MOUSE_DOWN`, `EventType.MOUSE_UP`, or `EventType.MOUSE_MOVE` with `addDisposableListener` directly — on iOS, these events don't fire because the platform uses pointer events. Use `addDisposableGenericMouseDownListener`, `addDisposableGenericMouseUpListener`, or `addDisposableGenericMouseMoveListener` instead, which automatically select the correct event type per platform.
- For custom clickable elements (e.g. picker triggers, title bar pills, or other `<div>`/`<span>` elements styled as buttons) that open pickers or menus on click, listen to **both** `EventType.CLICK` and `TouchEventType.Tap` and call `Gesture.addTarget` on the element. On touch devices, including iOS, VS Code relies on the gesture system to emit `TouchEventType.Tap`, and `EventType.CLICK` alone may not reliably fire there. The base `Button` class already does this correctly, so this rule applies to custom non-`<button>` trigger elements.
- Add `touch-action: manipulation` in CSS on custom clickable elements (e.g. picker triggers, title bar pills, or other `<div>`/`<span>` elements styled as buttons) to eliminate the 300ms tap delay on touch devices. This is not needed for native `<button>` elements or standard VS Code widgets (quick picks, context menus, action bar items) which already handle touch behavior.
66 changes: 65 additions & 1 deletion extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2890,6 +2890,34 @@
"enablement": "!chatSessionRequestInProgress",
"icon": "$(repo)",
"category": "GitHub Copilot"
},
{
"command": "github.copilot.claude.sessions.commit",
"title": "%github.copilot.command.claude.sessions.commit%",
"enablement": "!chatSessionRequestInProgress",
"icon": "$(git-commit)",
"category": "GitHub Copilot"
},
{
"command": "github.copilot.claude.sessions.commitAndSync",
"title": "%github.copilot.command.claude.sessions.commitAndSync%",
"enablement": "!chatSessionRequestInProgress",
"icon": "$(sync)",
"category": "GitHub Copilot"
},
{
"command": "github.copilot.claude.sessions.sync",
"title": "%github.copilot.command.claude.sessions.sync%",
"enablement": "!chatSessionRequestInProgress",
"icon": "$(sync)",
"category": "GitHub Copilot"
},
{
"command": "github.copilot.claude.sessions.initializeRepository",
"title": "%github.copilot.command.claude.sessions.initializeRepository%",
"enablement": "!chatSessionRequestInProgress",
"icon": "$(repo)",
"category": "GitHub Copilot"
}
],
"configuration": [
Expand Down Expand Up @@ -4657,7 +4685,7 @@
},
"github.copilot.chat.cli.sessionController.enabled": {
"type": "boolean",
"default": true,
"default": false,
"markdownDescription": "%github.copilot.config.cli.sessionController.enabled%",
"tags": [
"advanced"
Expand Down Expand Up @@ -5014,6 +5042,26 @@
"command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR",
"when": "chatSessionType == copilotcli && isSessionsWindow && sessions.isolationMode == worktree && sessions.hasGitRepository && sessions.hasGitHubRemote && sessions.hasPullRequest && sessions.hasOpenPullRequest",
"group": "pull_request@1"
},
{
"command": "github.copilot.claude.sessions.initializeRepository",
"when": "chatSessionType == claude-code && isSessionsWindow && !sessions.hasGitRepository",
"group": "init@1"
},
{
"command": "github.copilot.claude.sessions.commit",
"when": "chatSessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges",
"group": "commit@1"
},
{
"command": "github.copilot.claude.sessions.commitAndSync",
"when": "chatSessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && sessions.hasUncommittedChanges && sessions.hasUpstream",
"group": "commit@2"
},
{
"command": "github.copilot.claude.sessions.sync",
"when": "chatSessionType == claude-code && isSessionsWindow && sessions.hasGitRepository && !sessions.hasUncommittedChanges && sessions.hasUpstream",
"group": "sync@1"
}
],
"chat/contextUsage/actions": [
Expand Down Expand Up @@ -5373,6 +5421,22 @@
{
"command": "github.copilot.sessions.initializeRepository",
"when": "false"
},
{
"command": "github.copilot.claude.sessions.commit",
"when": "false"
},
{
"command": "github.copilot.claude.sessions.commitAndSync",
"when": "false"
},
{
"command": "github.copilot.claude.sessions.sync",
"when": "false"
},
{
"command": "github.copilot.claude.sessions.initializeRepository",
"when": "false"
}
],
"view/title": [
Expand Down
4 changes: 4 additions & 0 deletions extensions/copilot/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@
"github.copilot.command.cli.fleet.description": "Enable fleet mode for parallel subagent execution",
"github.copilot.command.cli.remote.description": "Enable remote control for this session",
"github.copilot.command.claude.sessions.rename": "Rename...",
"github.copilot.command.claude.sessions.commit": "Commit",
"github.copilot.command.claude.sessions.commitAndSync": "Commit and Sync",
"github.copilot.command.claude.sessions.sync": "Sync Changes",
"github.copilot.command.claude.sessions.initializeRepository": "Initialize Repository",
"github.copilot.command.cli.sessions.openRepository": "Open Repository",
"github.copilot.command.cli.sessions.openWorktreeInNewWindow": "Open Session in New Window",
"github.copilot.command.cli.sessions.openWorktreeInTerminal": "Open Session in Terminal",
Expand Down
53 changes: 52 additions & 1 deletion extensions/copilot/src/extension/chatSessions/claude/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ All interactions are displayed through VS Code's native chat UI, providing a sea
- Used to resume previous Claude Code conversations
- See `node/sessionParser/README.md` for detailed documentation

### `node/claudeSkills.ts`

**IClaudePluginService / ClaudePluginService**
- Resolves plugin root directories for the Claude SDK's `plugins` option
- Combines three sources of plugin locations:
1. **Config skill locations** — from `chat.agentSkillsLocations` setting, resolved via the shared `resolveSkillConfigLocations()` utility. These point to skills directories (e.g. `.../skills/`), so the service walks **one level up** to reach the plugin root expected by the SDK.
2. **Discovered skills** — from `IPromptsService.getSkills()`. Each skill has a `SKILL.md` at `<plugin-root>/skills/<skill-name>/SKILL.md`, so the service walks **three levels up** (`dirname(dirname(dirname(uri)))`) to reach the plugin root.
3. **Direct plugins** — from `IPromptsService.getPlugins()`, returned as-is since they already point to plugin root directories.
- Filters out `.claude` directories (the Claude SDK loads these automatically)
- Deduplicates results using `ResourceSet`
- Plugin roots are passed to the SDK as `SdkPluginConfig[]` with `{ type: 'local', path }` in `ClaudeCodeSession._doStartSession()`

**Shared utility:** `../../common/skillConfigLocations.ts``resolveSkillConfigLocations()` handles `~/` expansion, absolute paths, and relative paths joined to workspace folders. Used by both `ClaudePluginService` and `CopilotCLISkills`.

### `common/claudeTools.ts`

Defines Claude Code's tool interface:
Expand Down Expand Up @@ -216,19 +230,56 @@ In multi-root and empty workspaces, a folder picker option appears in the chat s
- **`common/claudeFolderInfo.ts`**: `ClaudeFolderInfo` interface
- **`../../chatSessions/common/claudeWorkspaceFolderService.ts`**: `IClaudeWorkspaceFolderService` interface — computes git diff changes for session items
- **`../../chatSessions/vscode-node/claudeWorkspaceFolderServiceImpl.ts`**: Implementation — diffs the session's branch against its base branch, caches results, and maps changes to `ChatSessionChangedFile[]` for display in the Sessions view
- **`../../chatSessions/vscode-node/claudeChatSessionContentProvider.ts`**: Folder resolution, picker options, and handler integration
- **`../../chatSessions/vscode-node/claudeChatSessionContentProvider.ts`**: Folder resolution, picker options, session metadata enrichment, and git command handlers
- **`../../chatSessions/common/builtinSlashCommands.ts`**: Shared constants for built-in slash commands (`/commit`, `/sync`, `/merge`, etc.) used by both Claude and CopilotCLI sessions
- **`../../chatSessions/vscode-node/folderRepositoryManagerImpl.ts`**: `FolderRepositoryManager` (abstract base) with `ClaudeFolderRepositoryManager` subclass — the Claude subclass does not depend on `ICopilotCLISessionService` (CopilotCLI has its own subclass `CopilotCLIFolderRepositoryManager`)
- **`node/claudeCodeAgent.ts`**: Consumes `ClaudeFolderInfo` in `ClaudeCodeSession._startSession()`
- **`node/sessionParser/claudeCodeSessionService.ts`**: `_getProjectSlugs()` generates slugs for all folders

## Session Metadata and Git Commands

### Session Metadata Enrichment

Each Claude session item carries metadata that drives the Sessions view UI (button visibility, status indicators). The `ClaudeChatSessionItemController._buildSessionMetadata()` method enriches session items with git repository state:

| Field | Type | Description |
|-------|------|-------------|
| `workingDirectoryPath` | `string` | Session's working directory (always present) |
| `repositoryPath` | `string?` | Git repository root path |
| `branchName` | `string?` | Current HEAD branch name |
| `upstreamBranchName` | `string?` | Upstream tracking ref (e.g., `origin/main`) |
| `hasGitHubRemote` | `boolean?` | Whether any remote points to GitHub |
| `incomingChanges` | `number?` | Commits behind upstream |
| `outgoingChanges` | `number?` | Commits ahead of upstream |
| `uncommittedChanges` | `number?` | Total uncommitted changes (merge + index + working tree + untracked) |

These metadata fields map to `when`-clause context keys in `package.json` (e.g., `sessions.hasGitRepository`, `sessions.hasUncommittedChanges`, `sessions.hasUpstream`) that control which action buttons appear in the Changes view.

### Git Action Commands

The `ClaudeChatSessionItemController` registers four git-related commands that appear as action buttons in the Sessions/Changes view:

| Command | When Visible | Action |
|---------|-------------|--------|
| `github.copilot.claude.sessions.commit` | Has git repo + uncommitted changes | Sends `/commit` prompt to the session |
| `github.copilot.claude.sessions.commitAndSync` | Has git repo + uncommitted changes + upstream | Sends `/commit and /sync` prompt |
| `github.copilot.claude.sessions.sync` | Has git repo + no uncommitted changes + upstream | Sends `/sync` prompt |
| `github.copilot.claude.sessions.initializeRepository` | No git repo | Calls `IGitService.initRepository()` on the session's workspace folder |

The commit, commitAndSync, and sync commands use a shared `_registerPromptCommand()` helper that extracts the session resource and dispatches via `workbench.action.chat.openSessionWithPrompt.claude-code`. The slash command strings come from the shared `builtinSlashCommands` module (`../../common/builtinSlashCommands.ts`).

## Testing

Unit tests are located in `node/test/`:
- `claudeCodeAgent.spec.ts`: Tests for agent and session logic
- `claudeCodeSessionService.spec.ts`: Tests for session loading and persistence
- `claudePluginService.spec.ts`: Tests for plugin location resolution
- `mockClaudeCodeSdkService.ts`: Mock SDK service for testing
- `fixtures/`: Sample `.jsonl` session files for testing

Additional tests for the session item controller and content provider:
- `../../chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts`: Tests for session metadata enrichment, git command handlers, session lifecycle, and content provider behavior

## Extension Registries

The Claude integration uses several registries to organize and manage extensibility points:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ This guide covers the **Claude** session target in VS Code Copilot Chat: what it
- [Tools Available to Claude](#tools-available-to-claude)
- [Memory Files (CLAUDE.md)](#memory-files-claudemd)
- [Custom Subagents](#custom-subagents)
- [Skills and Plugins](#skills-and-plugins)
- [Hooks](#hooks)
- [Settings Reference](#settings-reference)
- [How It Differs from Other Session Targets](#how-it-differs-from-other-session-targets)
Expand Down Expand Up @@ -229,6 +230,19 @@ Each session in the list displays:

Sessions are sorted by recency — the most recent session appears at the top. In the dedicated sidebar, they're also grouped by time period.

#### Git Action Buttons

When a session has a git repository, action buttons appear in the Changes view based on the repository state:

| Button | When It Appears | What It Does |
|--------|----------------|-------------|
| **Commit** | Uncommitted changes exist | Sends `/commit` to the session — Claude stages and commits your changes |
| **Commit and Sync** | Uncommitted changes exist + upstream branch configured | Sends `/commit and /sync` — Claude commits and pushes/pulls |
| **Sync Changes** | No uncommitted changes + upstream branch configured | Sends `/sync` — Claude pushes/pulls with the remote |
| **Initialize Repository** | No git repository in the session's working directory | Creates a new git repository in the session's folder |

These buttons let you manage git operations without leaving the Sessions view or typing commands manually.

#### Searching and Filtering Sessions

The Sessions toolbar (above the session list) provides three tools:
Expand Down Expand Up @@ -565,6 +579,68 @@ Use the [`/agents`](#agents--create-and-manage-subagents) slash command to creat

---

## Skills and Plugins

Claude sessions automatically discover and load **skills** and **plugins** from your workspace and configuration. These extend Claude's capabilities with reusable, packaged functionality — such as custom slash commands, tools, or domain-specific instructions.

### How Skills and Plugins Are Discovered

Claude finds plugin directories from three sources:

| Source | How It Works |
|--------|-------------|
| **`chat.agentSkillsLocations` setting** | Paths to directories containing skills. Claude walks one level up from each to find the plugin root. Supports `~/`, absolute, and relative paths. |
| **Discovered `SKILL.md` files** | The prompts service finds `SKILL.md` files in your workspace. Claude derives the plugin root from each skill's file path. |
| **Plugin directories** | The prompts service returns plugin root directories directly. |

> **Note:** Directories under `.claude/` are automatically excluded since the Claude SDK loads those on its own.

### Configuring Additional Skill Locations

Use the `chat.agentSkillsLocations` setting to point Claude at additional skill directories:

```json
{
"chat.agentSkillsLocations": {
"~/my-skills": true,
"/absolute/path/to/skills": true,
"relative/skills": true
}
}
```

- **`~/` paths** are expanded relative to your home directory
- **Absolute paths** are used as-is
- **Relative paths** are resolved against each workspace folder

Set a path's value to `false` to disable it without removing the entry.

### Skill File Format

Each skill lives in its own directory with a `SKILL.md` file:

```
my-plugin/
└── skills/
├── my-skill/
│ └── SKILL.md
└── another-skill/
└── SKILL.md
```

`SKILL.md` files use YAML frontmatter for metadata:

```markdown
---
name: my-skill
description: "A brief description of what this skill does"
---

Instructions for Claude when this skill is invoked...
```

---

## Hooks

Hooks let you run custom scripts at key moments in Claude's execution. They're configured via the [`/hooks`](#hooks--configure-lifecycle-hooks) slash command and stored in VS Code settings.
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 { EffortLevel, McpServerConfig, Options, PermissionMode, Query, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
import { EffortLevel, McpServerConfig, Options, PermissionMode, Query, SDKUserMessage, SdkPluginConfig } from '@anthropic-ai/claude-agent-sdk';
import Anthropic from '@anthropic-ai/sdk';
import * as l10n from '@vscode/l10n';
import type * as vscode from 'vscode';
Expand All @@ -20,6 +20,7 @@ import { isWindows } from '../../../../util/vs/base/common/platform';
import { URI } from '../../../../util/vs/base/common/uri';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { LanguageModelToolMCPSource } from '../../../../vscodeTypes';
import { IClaudePluginService } from './claudeSkills';
import { ExternalEditTracker } from '../../common/externalEditTracker';
import { buildMcpServersFromRegistry } from '../common/claudeMcpServerRegistry';
import { dispatchMessage, KnownClaudeError } from '../common/claudeMessageDispatch';
Expand Down Expand Up @@ -214,6 +215,7 @@ export class ClaudeCodeSession extends Disposable {
@IClaudeSessionStateService private readonly sessionStateService: IClaudeSessionStateService,
@IClaudeRuntimeDataService private readonly runtimeDataService: IClaudeRuntimeDataService,
@IMcpService private readonly mcpService: IMcpService,
@IClaudePluginService private readonly claudePluginService: IClaudePluginService,
@IOTelService private readonly _otelService: IOTelService,
@IChatDebugFileLoggerService private readonly _debugFileLogger: IChatDebugFileLoggerService,
) {
Expand Down Expand Up @@ -429,6 +431,22 @@ export class ClaudeCodeSession extends Disposable {
const errorMessage = error instanceof Error ? (error.stack ?? error.message) : String(error);
this.logService.warn(`[ClaudeCodeSession] Failed to start MCP gateway: ${errorMessage}`);
}

// Build plugins from skill directories
const plugins: SdkPluginConfig[] = [];
try {
const pluginLocations = await this.claudePluginService.getPluginLocations(token);
for (const pluginLocation of pluginLocations) {
plugins.push({ type: 'local', path: pluginLocation.fsPath });
}
if (plugins.length > 0) {
this.logService.info(`[ClaudeCodeSession] Passing ${plugins.length} plugin(s) from skill locations`);
}
} catch (error) {
const errorMessage = error instanceof Error ? (error.stack ?? error.message) : String(error);
this.logService.warn(`[ClaudeCodeSession] Failed to resolve skill locations for plugins: ${errorMessage}`);
}

const options: Options = {
cwd,
additionalDirectories,
Expand All @@ -451,6 +469,7 @@ export class ClaudeCodeSession extends Disposable {
permissionMode: this._currentPermissionMode,
includeHookEvents: true,
mcpServers,
plugins,
settings: {
env: {
ANTHROPIC_BASE_URL: `http://localhost:${this.serverConfig.port}`,
Expand Down
Loading
Loading