Skip to content
Closed
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1519,6 +1519,10 @@
"name": "compact",
"description": "%copilot.agent.compact.description%"
},
{
"name": "clearAndImplement",
"description": "%copilot.agent.clearAndImplement.description%"
},
{
"name": "explain",
"description": "%copilot.workspace.explain.description%"
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
"copilot.edits.description": "Edit files in your workspace",
"copilot.agent.description": "Edit files in your workspace in agent mode",
"copilot.agent.compact.description": "Free up context by compacting the conversation history. Optionally include extra instructions for compaction.",
"copilot.agent.clearAndImplement.description": "Open a fresh agent session with the plan from session memory, avoiding context bloat from planning.",
"copilot.workspace.explain.description": "Explain how the code in your active editor works",
"copilot.workspace.edit.description": "Edit files in your workspace",
"copilot.workspace.review.description": "Review the selected code in your active editor",
Expand Down
94 changes: 91 additions & 3 deletions src/extension/agents/vscode-node/planAgentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { AGENT_FILE_EXTENSION } from '../../../platform/customInstructions/common/promptTypes';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { createDirectoryIfNotExists, IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { ILogService } from '../../../platform/log/common/logService';
import { raceTimeout } from '../../../util/vs/base/common/async';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { URI } from '../../../util/vs/base/common/uri';
import { extractSessionId, MEMORY_BASE_DIR } from '../../tools/node/memoryTool';
import { AgentConfig, AgentHandoff, buildAgentMarkdown, DEFAULT_READ_TOOLS } from './agentTypes';

/** Timeout for waiting on the new session resource event after creating a chat. */
const NEW_SESSION_RESOURCE_TIMEOUT_MS = 2000;

/**
* Base Plan agent configuration - embedded from Plan.agent.md
* This avoids runtime file loading and YAML parsing dependencies.
Expand Down Expand Up @@ -52,9 +59,16 @@ export class PlanAgentProvider extends Disposable implements vscode.ChatCustomAg
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
@IFileSystemService private readonly fileSystemService: IFileSystemService,
@ILogService private readonly logService: ILogService,
@IRunCommandExecutionService private readonly runCommandService: IRunCommandExecutionService,
) {
super();

// Register clearAndImplement command
this._register(vscode.commands.registerCommand(
'github.copilot.chat.clearAndImplement',
(sessionResource: vscode.Uri) => this._clearAndImplement(sessionResource)
));

// Listen for settings changes to refresh agents
// Note: When settings change, we fire onDidChangeCustomAgents which causes VS Code to re-fetch
// the agent definition. However, handoff buttons already rendered may not work as
Expand Down Expand Up @@ -174,7 +188,7 @@ On user input after showing the plan:
- Changes requested → revise and present updated plan. Update \`/memories/session/plan.md\` to keep the documented plan in sync
- Questions asked → clarify${askQuestionsEnabled ? ', or use #tool:vscode/askQuestions for follow-ups' : ' and update "Further Considerations" as needed'}
- Alternatives wanted → loop back to **Discovery** with new subagent
- Approval given → acknowledge, the user can now use handoff buttons
- Approval given → acknowledge, and inform the user they can also use the \`/clearAndImplement\` slash command to open a **fresh** agent session with the plan loaded into session memory, avoiding context bloat from the planning conversation

Keep iterating until explicit approval or handoff.
</workflow>
Expand Down Expand Up @@ -227,7 +241,7 @@ ${askQuestionsEnabled ? '- NO blocking questions at the end — ask during workf
const startImplementationHandoff: AgentHandoff = {
label: 'Start Implementation',
agent: 'agent',
prompt: 'Start implementation',
prompt: '/clearAndImplement',
send: true,
...(implementAgentModelOverride ? { model: implementAgentModelOverride } : {})
};
Expand Down Expand Up @@ -266,4 +280,78 @@ ${askQuestionsEnabled ? '- NO blocking questions at the end — ask during workf
...(modelOverride ? { model: modelOverride } : {}),
};
}

private async _sendChatQuery(text: string): Promise<void> {
await this.runCommandService.executeCommand('workbench.panel.chat.view.copilot.focus');
await this.runCommandService.executeCommand('type', { text });
await this.runCommandService.executeCommand('workbench.action.chat.submit');
}

private async _clearAndImplement(currentSessionResource: vscode.Uri): Promise<void> {
const storageUri = this.extensionContext.storageUri;
if (!storageUri) {
this.logService.warn('[PlanAgentProvider] No workspace storage available for clearAndImplement');
return;
}

const currentSessionId = extractSessionId(currentSessionResource.toString());
const planUri = vscode.Uri.joinPath(
URI.from(storageUri), MEMORY_BASE_DIR, currentSessionId, 'plan.md'
);

let planContent: Uint8Array;
try {
planContent = await this.fileSystemService.readFile(planUri);
} catch {
this.logService.info('[PlanAgentProvider] No plan.md found in session memory, falling back to in-session handoff');
await this._sendChatQuery('Start implementation');
return;
}

// Listen for the new session resource BEFORE creating the chat
let eventDisposable: vscode.Disposable | undefined;
const eventPromise = new Promise<vscode.Uri | undefined>(resolve => {
eventDisposable = vscode.window.onDidChangeActiveChatPanelSessionResource(newResource => {
resolve(newResource);
});
});
const newSessionPromise = raceTimeout(eventPromise, NEW_SESSION_RESOURCE_TIMEOUT_MS).finally(() => {
eventDisposable?.dispose();
});

// Create a new chat session
await this.runCommandService.executeCommand('workbench.action.chat.newChat');

// Wait for the new session resource
let newSessionResource = vscode.window.activeChatPanelSessionResource;
if (newSessionResource && newSessionResource.toString() !== currentSessionResource.toString()) {
// Fast path: resource already changed, dispose listener immediately
eventDisposable?.dispose();
} else {
newSessionResource = await newSessionPromise;
}

if (!newSessionResource) {
this.logService.warn('[PlanAgentProvider] Failed to get new session resource');
await this._sendChatQuery('Start implementation');
return;
}

// Copy plan.md to the new session's memory
const newSessionId = extractSessionId(newSessionResource.toString());
const newSessionDir = vscode.Uri.joinPath(
URI.from(storageUri), MEMORY_BASE_DIR, newSessionId
);
await createDirectoryIfNotExists(this.fileSystemService, newSessionDir);

const newPlanUri = vscode.Uri.joinPath(newSessionDir, 'plan.md');
await this.fileSystemService.writeFile(newPlanUri, planContent);
this.logService.trace(`[PlanAgentProvider] Copied plan.md from session ${currentSessionId} to ${newSessionId}`);

// Open in agent mode with "Start implementation"
await this.runCommandService.executeCommand('workbench.action.chat.open', {
mode: 'agent',
query: 'Start implementation',
});
}
}
10 changes: 6 additions & 4 deletions src/extension/agents/vscode-node/test/planAgentProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,10 @@ suite('buildAgentMarkdown', () => {
model: 'Claude Haiku 4.5 (copilot)',
handoffs: [
{
label: 'Start Implementation',
label: 'Open in Editor',
agent: 'agent',
prompt: 'Start implementation',
prompt: 'Open plan in editor',
showContinueOn: false,
send: true
}
],
Expand All @@ -423,10 +424,11 @@ argument-hint: Outline the goal or problem to research
model: Claude Haiku 4.5 (copilot)
tools: ['github/issue_read', 'agent', 'search', 'memory']
handoffs:
- label: Start Implementation
- label: Open in Editor
agent: agent
prompt: 'Start implementation'
prompt: 'Open plan in editor'
send: true
showContinueOn: false
---
You are a PLANNING AGENT.`);
});
Expand Down
1 change: 1 addition & 0 deletions src/extension/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const agentsToCommands: Partial<Record<Intent, Record<string, Intent>>> =
'semanticSearch': Intent.SemanticSearch,
'setupTests': Intent.SetupTests,
'compact': Intent.Agent,
'clearAndImplement': Intent.Agent,
},
[Intent.VSCode]: {
'search': Intent.Search,
Expand Down
17 changes: 16 additions & 1 deletion src/extension/intents/node/agentIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BudgetExceededError } from '@vscode/prompt-tsx/dist/base/materialized';
import type * as vscode from 'vscode';
import { IChatSessionService } from '../../../platform/chat/common/chatSessionService';
import { ChatLocation, ChatResponse } from '../../../platform/chat/common/commonTypes';
import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { isAnthropicFamily, isGptFamily, modelCanUseApplyPatchExclusively, modelCanUseReplaceStringExclusively, modelSupportsApplyPatch, modelSupportsMultiReplaceString, modelSupportsReplaceString, modelSupportsSimplifiedApplyPatchInstructions } from '../../../platform/endpoint/common/chatModelCapabilities';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
Expand All @@ -24,12 +25,12 @@ import { IExperimentationService } from '../../../platform/telemetry/common/null
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { ITestProvider } from '../../../platform/testing/common/testProvider';
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
import { ChatResponseProgressPart2 } from '../../../vscodeTypes';

import { isCancellationError } from '../../../util/vs/base/common/errors';
import { Iterable } from '../../../util/vs/base/common/iterator';
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';

import { ChatResponseProgressPart2 } from '../../../vscodeTypes';
import { ICommandService } from '../../commands/node/commandService';
import { Intent } from '../../common/constants';
import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';
Expand Down Expand Up @@ -204,6 +205,10 @@ export class AgentIntent extends EditCodeIntent {
return this.handleSummarizeCommand(conversation, request, stream, token);
}

if (request.command === 'clearAndImplement') {
return this.handleClearContextAndStartImplementation(request);
}

return super.handleRequest(conversation, request, stream, token, documentContext, agentName, location, chatTelemetry, yieldRequested);
}

Expand Down Expand Up @@ -312,6 +317,16 @@ export class AgentIntent extends EditCodeIntent {
return {};
}
}

private async handleClearContextAndStartImplementation(
request: vscode.ChatRequest,
): Promise<vscode.ChatResult> {
const runCommandService = this.instantiationService.invokeFunction(accessor =>
accessor.get(IRunCommandExecutionService)
);
await runCommandService.executeCommand('github.copilot.chat.clearAndImplement', request.sessionResource);
return {};
}
}

export class AgentIntentInvocation extends EditCodeIntentInvocation implements IIntentInvocation {
Expand Down
2 changes: 1 addition & 1 deletion src/extension/tools/node/memoryTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ToolName } from '../common/toolNames';
import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry';
import { formatUriForFileWidget } from '../common/toolUtils';

const MEMORY_BASE_DIR = 'memory-tool/memories';
export const MEMORY_BASE_DIR = 'memory-tool/memories';
const REPO_PATH_PREFIX = '/memories/repo';
const SESSION_PATH_PREFIX = '/memories/session';

Expand Down
8 changes: 8 additions & 0 deletions src/util/common/test/shims/vscodeTypesShim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ const shim: typeof vscodeTypes = {
authentication: {
getSession: async () => { throw new Error('authentication.getSession not mocked in test'); }
},
commands: {
registerCommand: () => ({ dispose: () => { } }),
executeCommand: async () => { },
},
window: {
get activeChatPanelSessionResource() { return undefined; },
onDidChangeActiveChatPanelSessionResource: () => ({ dispose: () => { } }),
},
McpHttpServerDefinition,
McpStdioServerDefinition,
ThemeIcon
Expand Down
10 changes: 10 additions & 0 deletions src/vscodeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,13 @@ export const l10n = {
export const authentication = {
getSession: vscode.authentication.getSession,
};

export const commands = {
registerCommand: vscode.commands.registerCommand,
executeCommand: vscode.commands.executeCommand,
};

export const window = {
get activeChatPanelSessionResource() { return vscode.window.activeChatPanelSessionResource; },
onDidChangeActiveChatPanelSessionResource: vscode.window.onDidChangeActiveChatPanelSessionResource,
};
Loading