Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
78905ec
fix: enable copilot-local ESLint rules in PR CI (#316121)
benvillalobos May 13, 2026
5c43f5d
Agents web: hide harness picker and host suffix in web when single ha…
osortega May 13, 2026
4136011
Terminal output compression: argv-aware filters, dedup cache, structu…
meganrogge May 13, 2026
cfef5c2
Agents web: config picker order on mobile-aware subclass (#316151)
osortega May 13, 2026
a9aeb3d
fix: defer marking old chat model as read to prevent blocking new cha…
DonJayamanne May 13, 2026
c302942
Optimize agent mode instructions handling for efficiency (#316152)
DonJayamanne May 13, 2026
420d10c
Refactor getSDKAgents to avoid using SDK for finding agents temporari…
DonJayamanne May 13, 2026
ba911a6
send telemetry for CLI tool calls (#316135)
amunger May 13, 2026
6a0d8bc
Update manage models heading detection (#316028)
pwang347 May 13, 2026
57fa97f
Revert "run_in_terminal: promote sync command to background after idl…
meganrogge May 13, 2026
7e7b3b2
feat: add session history language model handling to ChatInputPart (#…
DonJayamanne May 13, 2026
d2ce926
Update to chat status dashboard contributed sections (#315134)
pwang347 May 13, 2026
bddbcf2
Bump @vscode/gulp-electron to 1.41.3 (retry transient network errors)…
dmitrivMS May 13, 2026
a3c6210
fix: update setModelId to accept undefined and adjust related session…
DonJayamanne May 13, 2026
efa9345
CI: kill lingering Windows smoke-test processes before Publish Log Fi…
dmitrivMS May 13, 2026
c305abc
Anthropic Messages API: add extended (1h) prompt cache TTL behind exp…
bhavyaus May 13, 2026
fd7f7ce
Disabling readFile tool for experimentation for gpt-5.5 (#316188)
dileepyavan May 13, 2026
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
16 changes: 16 additions & 0 deletions build/azure-pipelines/win32/steps/product-build-win32-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,19 @@ steps:
testResultsFiles: "*-results.xml"
searchFolder: "$(Build.ArtifactStagingDirectory)/test-results"
condition: succeededOrFailed()

# Force-kill any lingering test processes that still hold smoke-test log
# files open and would otherwise break the 1ES "Publish Log Files" output.
- powershell: |
$ErrorActionPreference = "Continue"
$testRoot = "$(agent.builddirectory)\test"
Get-CimInstance Win32_Process -Filter "ExecutablePath IS NOT NULL" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -like "$testRoot\*" } |
ForEach-Object {
Write-Host "Killing lingering test process: pid=$($_.ProcessId), name=$($_.Name), path=$($_.ExecutablePath)"
try { Stop-Process -Id $_.ProcessId -Force -ErrorAction Stop }
catch { Write-Host " failed: $($_.Exception.Message)" }
}
displayName: Kill lingering test processes
continueOnError: true
condition: succeededOrFailed()
6 changes: 3 additions & 3 deletions extensions/copilot/.eslintplugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { LooseRuleDefinition } from '@typescript-eslint/utils/ts-eslint';
import * as glob from 'glob';
import fs from 'fs';
import path from 'path';

// Re-export all .ts files as rules
const rules: Record<string, LooseRuleDefinition> = {};
await Promise.all(
glob.sync('*.ts', { cwd: import.meta.dirname })
.filter(file => !file.endsWith('index.ts') && !file.endsWith('utils.ts'))
fs.readdirSync(import.meta.dirname)
.filter(file => file.endsWith('.ts') && !file.endsWith('index.ts') && !file.endsWith('utils.ts'))
.map(async file => {
rules[path.basename(file, '.ts')] = (await import('./' + file)).default;
})
Expand Down
10 changes: 9 additions & 1 deletion extensions/copilot/.eslintplugin/no-unlayered-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ export default new class NoUnlayeredFiles implements eslint.Rule.RuleModule {

create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {

const filenameParts = context.filename.split(path.sep);
// Use only the path relative to extensions/copilot/ to avoid false positives
// from the repo directory name (e.g., "vscode" is both a layer name and the
// checkout directory, so absolute paths always contain it).
const copilotPrefix = `extensions${path.sep}copilot${path.sep}`;
const idx = context.filename.indexOf(copilotPrefix);
const relativePath = idx >= 0
? context.filename.slice(idx + copilotPrefix.length)
: context.filename;
const filenameParts = relativePath.split(path.sep);

if (!filenameParts.find(part => layers.has(part))) {
context.report({
Expand Down
10 changes: 10 additions & 0 deletions extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4094,6 +4094,16 @@
"advanced"
]
},
"github.copilot.chat.anthropic.promptCaching.extendedTtl": {
"type": "boolean",
"default": false,
"tags": [
"advanced",
"experimental",
"onExp"
],
"description": "%github.copilot.config.anthropic.promptCaching.extendedTtl%"
},
"github.copilot.chat.installExtensionSkill.enabled": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions extensions/copilot/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@
"copilot.toolSet.web.description": "Fetch information from the web",
"github.copilot.config.useMessagesApi": "Use the Messages API instead of the Chat Completions API when supported.",
"github.copilot.config.anthropic.contextEditing.mode": "Select the context editing mode for Anthropic models. Automatically manages conversation context as it grows, helping optimize costs and stay within context window limits.\n\n- `off`: Context editing is disabled.\n- `clear-thinking`: Clears thinking blocks while preserving tool uses.\n- `clear-tooluse`: Clears tool uses while preserving thinking blocks.\n- `clear-both`: Clears both thinking blocks and tool uses.\n\n**Note**: This is an experimental feature. Context editing may cause additional cache rewrites. Enable with caution.",
"github.copilot.config.anthropic.promptCaching.extendedTtl": "Use the extended (1 hour) prompt cache TTL on tools and system blocks for the Anthropic Messages API. Only applied to 1M context Claude variants; other models keep the default 5 minute TTL even when this setting is enabled.\n\n**Note**: This is an experimental feature. Only the main agent conversation is eligible — inline chat, terminal chat, notebook chat, and subagent requests are excluded.",
"github.copilot.config.useResponsesApi": "Use the Responses API instead of the Chat Completions API when supported. Enables reasoning and reasoning summaries.\n\n**Note**: This is an experimental feature that is not yet activated for all users.\n\n**Important**: URL API path resolution for custom OpenAI-compatible and Azure models is independent of this setting and fully determined by `url` property of `#github.copilot.chat.customOAIModels#` or `#github.copilot.chat.azureModels#` respectively.",
"github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
"github.copilot.config.responsesApiContextManagement.enabled": "Enables context management for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { ResourceSet } from '../../../../util/vs/base/common/map';
import { basename } from '../../../../util/vs/base/common/resources';
import { URI } from '../../../../util/vs/base/common/uri';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { getCopilotLogger } from './logger';
import { ensureNodePtyShim } from './nodePtyShim';
import { ensureRipgrepShim } from './ripgrepShim';
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
Expand Down Expand Up @@ -307,7 +306,6 @@ export class CopilotCLIAgents extends Disposable implements ICopilotCLIAgents {
readonly onDidChangeAgents: Event<void> = this._onDidChangeAgents.event;
constructor(
@IPromptsService private readonly promptsService: IPromptsService,
@ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK,
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
@ILogService private readonly logService: ILogService,
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
Expand Down Expand Up @@ -386,7 +384,7 @@ export class CopilotCLIAgents extends Disposable implements ICopilotCLIAgents {
async getAgentsImpl(): Promise<readonly CLIAgentInfo[]> {
const merged = new Map<string, CLIAgentInfo>();
const knownAgents = new ResourceSet();
const [sdkAgents, customAgents] = await Promise.all([this.getSDKAgents(), this.promptsService.getCustomAgents(CancellationToken.None)]);
const customAgents = await this.promptsService.getCustomAgents(CancellationToken.None);
const hiddenOrInvalidAgentUris = new ResourceSet();
const validCustomAgents = customAgents.filter(customAgent => {
if (!customAgent.enabled || !isEnabledForCopilotCLI(customAgent)) {
Expand All @@ -402,17 +400,6 @@ export class CopilotCLIAgents extends Disposable implements ICopilotCLIAgents {
return true;
});

for (const agent of sdkAgents) {
const sourceUri = agent.path ? URI.file(agent.path) : URI.from({ scheme: 'copilotcli', path: `/agents/${agent.name}` });
if (hiddenOrInvalidAgentUris.has(sourceUri)) {
continue;
}
knownAgents.add(sourceUri);
merged.set(agent.name.toLowerCase(), {
agent: this.cloneAgent(agent),
sourceUri,
});
}
for (const customAgent of validCustomAgents) {
if (knownAgents.has(customAgent.uri)) {
continue;
Expand All @@ -427,18 +414,6 @@ export class CopilotCLIAgents extends Disposable implements ICopilotCLIAgents {
return [...merged.values()];
}

private async getSDKAgents(): Promise<Readonly<SweCustomAgent>[]> {
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
if (workspaceFolders.length === 0) {
return [];
}

const [auth, { getCustomAgents }] = await Promise.all([this.copilotCLISDK.getAuthInfo(), this.copilotCLISDK.getPackage()]);
const workingDirectory = workspaceFolders[0];
const agents = await getCustomAgents(auth, workingDirectory.fsPath, undefined, getCopilotLogger(this.logService));
return agents.map(agent => this.cloneAgent(agent));
}

private toCustomAgent(customAgent: vscode.ChatCustomAgent): CLIAgentInfo | undefined {
const agentName = getAgentFileNameFromFilePath(customAgent.uri);
const headerName = customAgent.name;
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 type { Attachment, LocalSession, SendOptions, Session, SessionOptions } from '@github/copilot/sdk';
import type { Attachment, LocalSession, SendOptions, Session, SessionOptions, ToolExecutionCompleteEvent } from '@github/copilot/sdk';
import * as l10n from '@vscode/l10n';
import * as cp from 'child_process';
import * as crypto from 'crypto';
Expand All @@ -18,6 +18,7 @@ import { GenAiMetrics } from '../../../../platform/otel/common/genAiMetrics';
import { CopilotChatAttr, GenAiAttr, GenAiOperationName, GenAiProviderName, IOTelService, ISpanHandle, SpanKind, SpanStatusCode, truncateForOTel, resolveWorkspaceOTelMetadata, workspaceMetadataToOTelAttributes } from '../../../../platform/otel/common/index';
import { CapturingToken } from '../../../../platform/requestLogger/common/capturingToken';
import { IRequestLogger, LoggedRequestKind } from '../../../../platform/requestLogger/common/requestLogger';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
import { PromptTokenCategory, PromptTokenLabel } from '../../../../platform/tokenizer/node/promptTokenDetails';
import { IGitService } from '../../../../platform/git/common/gitService';
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
Expand Down Expand Up @@ -815,6 +816,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
@IGitService private readonly _gitService: IGitService,
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
@IChatQuotaService private readonly _chatQuotaService: IChatQuotaService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
) {
super();
this.sessionId = _sdkSession.sessionId;
Expand Down Expand Up @@ -955,7 +957,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
}

private async _handleRequestImpl(
request: { id: string; toolInvocationToken: ChatParticipantToolToken },
request: { id: string; toolInvocationToken: ChatParticipantToolToken; sessionResource?: vscode.Uri },
input: CopilotCLISessionInput,
attachments: Attachment[],
model: { model: string; reasoningEffort?: string } | undefined,
Expand Down Expand Up @@ -1010,7 +1012,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes

private async _handleRequestImplInner(
invokeAgentSpan: ISpanHandle,
request: { id: string; toolInvocationToken: ChatParticipantToolToken },
request: { id: string; toolInvocationToken: ChatParticipantToolToken; sessionResource?: vscode.Uri },
input: CopilotCLISessionInput,
attachments: Attachment[],
modelId: string | undefined,
Expand Down Expand Up @@ -1062,6 +1064,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes

const editToolIds = new Set<string>();
const toolCalls = new Map<string, ToolCall>();
const toolStartTimes = new Map<string, number>();
const editTracker = new ExternalEditTracker();
let sdkRequestId: string | undefined;
let isQuotaError = false;
Expand Down Expand Up @@ -1332,6 +1335,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
})));
disposables.add(toDisposable(this._sdkSession.on('tool.execution_start', (event) => {
toolCalls.set(event.data.toolCallId, event.data as unknown as ToolCall);
toolStartTimes.set(event.data.toolCallId, Date.now());

if (isCopilotCliEditToolCall(event.data)) {
flushPendingInvocationMessages();
Expand Down Expand Up @@ -1359,18 +1363,24 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
}
})));
disposables.add(toDisposable(this._sdkSession.on('tool.execution_complete', (event) => {
const toolName = toolCalls.get(event.data.toolCallId)?.toolName || '<unknown>';
const toolCall = toolCalls.get(event.data.toolCallId);
const toolName = toolCall?.toolName || '<unknown>';
if (toolName.endsWith('create_pull_request') && event.data.success) {
const pullRequestUrl = extractPullRequestUrlFromToolResult(event.data.result);
if (pullRequestUrl) {
this._createdPullRequestUrl = pullRequestUrl;
GenAiMetrics.incrementPullRequestCount(this._otelService);
}
}
// Emit `languageModelToolInvoked` to mirror the workbench LanguageModelToolsService event
// for the Copilot CLI agent. CLI tools execute inside the SDK and never reach
// LanguageModelToolsService, so the workbench-side emission does not fire for them.
this._sendToolInvokedTelemetry(event, toolCall, toolStartTimes, request.sessionResource);

// Log tool call to request logger
const eventError = event.data.error ? { ...event.data.error, code: event.data.error.code || '' } : undefined;
const eventData = { ...event.data, error: eventError };
this._logToolCall(event.data.toolCallId, toolName, toolCalls.get(event.data.toolCallId)?.arguments, eventData);
this._logToolCall(event.data.toolCallId, toolName, toolCall?.arguments, eventData);

// Mark the end of the edit if this was an edit tool.
toolIdEditMap.set(event.data.toolCallId, editTracker.completeEdit(event.data.toolCallId));
Expand All @@ -1392,9 +1402,8 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
// When a sql tool execution completes that modifies the todos table,
// query the session database and update the todo list widget.
if (toolName === 'sql' && event.data.success) {
const toolCallData = toolCalls.get(event.data.toolCallId);
try {
const query = (toolCallData?.arguments as { query?: string } | undefined)?.query ?? '';
const query = (toolCall?.arguments as { query?: string } | undefined)?.query ?? '';
if (isTodoRelatedSqlQuery(query)) {
const sessionDir = getCopilotCLISessionDir(this.sessionId);
this._todoSqlQuery.queryTodos(sessionDir).then(items => {
Expand Down Expand Up @@ -2614,6 +2623,53 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
isConversationRequest: true
});
}

private _sendToolInvokedTelemetry(
event: ToolExecutionCompleteEvent,
toolCall: ToolCall | undefined,
toolStartTimes: Map<string, number>,
sessionResource: vscode.Uri | undefined,
): void {
const { toolCallId, success, error } = event.data;
const eventToolName = 'toolName' in event.data && typeof event.data.toolName === 'string' ? event.data.toolName : undefined;
const toolName = toolCall?.toolName ?? eventToolName ?? '<unknown>';
const startTime = toolStartTimes.get(toolCallId);
toolStartTimes.delete(toolCallId);
const invocationTimeMs = startTime !== undefined ? Date.now() - startTime : undefined;

let result: 'success' | 'error' | 'userCancelled';
if (success) {
result = 'success';
} else if (error?.code === 'rejected' || error?.code === 'denied' || error?.code === 'cancelled') {
// `rejected`/`denied` come from the user denying a permission prompt; `cancelled` comes
// from request cancellation.
result = 'userCancelled';
} else {
result = 'error';
}

const toolSourceKind = toolCall?.mcpServerName ? 'mcp' : 'copilotCli';

/* __GDPR__
"languageModelToolInvoked" : {
"owner": "zhichli",
"comment": "Provides insight into the usage of language model tools (Copilot CLI agent).",
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "success | error | userCancelled" },
"chatSessionId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The chat session resource id." },
"toolId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The CLI/SDK tool name (e.g. bash, str_replace_editor, apply_patch)." },
"toolExtensionId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Always undefined for CLI." },
"toolSourceKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "copilotCli | mcp" },
"invocationTimeMs": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Time between tool.execution_start and tool.execution_complete (includes any permission wait)." }
}
*/
this._telemetryService.sendMSFTTelemetryEvent('languageModelToolInvoked', {
result,
chatSessionId: sessionResource?.toString(),
toolId: toolName,
toolExtensionId: undefined,
toolSourceKind,
}, invocationTimeMs !== undefined ? { invocationTimeMs } : undefined);
}
}

function extractPullRequestUrlFromToolResult(result: unknown): string | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1048,13 +1048,14 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
const detailsByCopilotId = new Map<string, RequestIdDetails>();
const defaultModeInstructions = agentId ? await this.resolveAgentModeInstructions(agentId) : undefined;

await Promise.all(storedDetails.map(async d => {
for (const d of storedDetails) {
if (d.copilotRequestId) {
const turnAgentId = d.modeInstructions?.uri || d.agentId;
const modeInstructions = (d.modeInstructions ?? (turnAgentId ? await this.resolveAgentModeInstructions(turnAgentId) : defaultModeInstructions)) ?? defaultModeInstructions;
// Agents from older requests isn't useful, hence to save time.
// Re-use the same custom agent from last request for all previous requests.
const modeInstructions = defaultModeInstructions;
detailsByCopilotId.set(d.copilotRequestId, { requestId: d.vscodeRequestId, toolIdEditMap: d.toolIdEditMap, modeInstructions, responseModelId: d.responseModelId, creditsUsed: d.creditsUsed });
}
}));
}
const getVSCodeRequestId = (sdkRequestId: string) => {
const stored = detailsByCopilotId.get(sdkRequestId);
if (stored) {
Expand Down
Loading
Loading