diff --git a/cli/azd/cmd/auto_install_integration_test.go b/cli/azd/cmd/auto_install_integration_test.go index d72d79cc384..ef16a0cb9d9 100644 --- a/cli/azd/cmd/auto_install_integration_test.go +++ b/cli/azd/cmd/auto_install_integration_test.go @@ -211,6 +211,13 @@ func TestAgentDetectionIntegration(t *testing.T) { expectedNoPrompt: true, description: "When running under GitHub Copilot CLI (COPILOT_CLI), --no-prompt should be auto-enabled", }, + { + name: "GitHub Copilot VSCode enables no-prompt automatically", + args: []string{"deploy"}, + envVars: map[string]string{"AI_AGENT": "github_copilot_vscode_agent"}, + expectedNoPrompt: true, + description: "When running under GitHub Copilot VSCode, --no-prompt should be auto-enabled", + }, { name: "Gemini agent enables no-prompt automatically", args: []string{"init"}, @@ -303,7 +310,7 @@ func containsNoPromptFalse(args []string) bool { // runs inside a CI/CD provider. func clearAgentEnvVarsForTest(t *testing.T) { envVarsToUnset := []string{ - // GitHub Copilot App + // GitHub Copilot hosts "AI_AGENT", // Claude Code "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", diff --git a/cli/azd/internal/runcontext/agentdetect/detect_env.go b/cli/azd/internal/runcontext/agentdetect/detect_env.go index 5b562d9262a..fd8b6810a7e 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_env.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_env.go @@ -20,12 +20,17 @@ type envVarPattern struct { // knownEnvVarPatterns defines environment variables that indicate known AI agents. // These are checked in order, so more specific patterns should come first. var knownEnvVarPatterns = []envVarPattern{ - // GitHub Copilot App can expose Copilot CLI markers, so check its host-specific marker first. + // GitHub Copilot hosts can expose Copilot CLI markers, so check their host-specific markers first. { envVar: "AI_AGENT", expectedValue: "github_copilot_app_agent", agentType: AgentTypeGitHubCopilotApp, }, + { + envVar: "AI_AGENT", + expectedValue: "github_copilot_vscode_agent", + agentType: AgentTypeGitHubCopilotVSCode, + }, // Claude Code - Anthropic's coding agent {envVar: "CLAUDE_CODE", agentType: AgentTypeClaudeCode}, @@ -70,7 +75,7 @@ var userAgentPatterns = []struct { substring string agentType AgentType }{ - // VS Code GitHub Copilot extension + // GitHub Copilot for Azure extension in VS Code {substring: internal.VsCodeAzureCopilotAgentPrefix, agentType: AgentTypeVSCodeCopilot}, {substring: "github-copilot", agentType: AgentTypeGitHubCopilotCLI}, {substring: "copilot-cli", agentType: AgentTypeGitHubCopilotCLI}, diff --git a/cli/azd/internal/runcontext/agentdetect/detect_test.go b/cli/azd/internal/runcontext/agentdetect/detect_test.go index 27a4648cc0f..d6f05384297 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_test.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_test.go @@ -20,6 +20,7 @@ func TestAgentType_DisplayName(t *testing.T) { {AgentTypeClaudeCode, "Claude Code"}, {AgentTypeGitHubCopilotCLI, "GitHub Copilot CLI"}, {AgentTypeGitHubCopilotApp, "GitHub Copilot App"}, + {AgentTypeGitHubCopilotVSCode, "GitHub Copilot VSCode"}, {AgentTypeVSCodeCopilot, "VS Code GitHub Copilot"}, {AgentTypeGemini, "Gemini"}, {AgentTypeOpenCode, "OpenCode"}, @@ -63,12 +64,46 @@ func TestDetectFromEnvVars(t *testing.T) { expectedAgent: AgentTypeGitHubCopilotApp, detected: true, }, + { + name: "GitHub Copilot VSCode takes precedence over Copilot CLI", + envVars: map[string]string{ + "AI_AGENT": "github_copilot_vscode_agent", + "GITHUB_COPILOT_CLI": "true", + "GH_COPILOT": "1", + "COPILOT_CLI": "1", + }, + expectedAgent: AgentTypeGitHubCopilotVSCode, + detected: true, + }, { name: "Unrecognized AI_AGENT value", envVars: map[string]string{"AI_AGENT": "another_agent"}, expectedAgent: AgentTypeUnknown, detected: false, }, + { + name: "AI_AGENT requires an exact value", + envVars: map[string]string{ + "AI_AGENT": "github_copilot_vscode_agent_extra", + }, + expectedAgent: AgentTypeUnknown, + detected: false, + }, + { + name: "Wrong AI_AGENT value falls back to Copilot CLI", + envVars: map[string]string{ + "AI_AGENT": "github_copilot_vscode", + "COPILOT_CLI": "1", + }, + expectedAgent: AgentTypeGitHubCopilotCLI, + detected: true, + }, + { + name: "COPILOT_AGENT alone is not attributed", + envVars: map[string]string{"COPILOT_AGENT": "1"}, + expectedAgent: AgentTypeUnknown, + detected: false, + }, { name: "Claude Code via CLAUDE_CODE", envVars: map[string]string{"CLAUDE_CODE": "1"}, @@ -343,7 +378,7 @@ func TestDisableAgentDetect(t *testing.T) { // This list must be kept in sync with knownEnvVarPatterns in detect_env.go. func clearAgentEnvVars(t *testing.T) { envVarsToUnset := []string{ - // GitHub Copilot App + // GitHub Copilot hosts "AI_AGENT", // Claude Code "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", diff --git a/cli/azd/internal/runcontext/agentdetect/types.go b/cli/azd/internal/runcontext/agentdetect/types.go index 07f6b7ab5ba..d2a5a7795d9 100644 --- a/cli/azd/internal/runcontext/agentdetect/types.go +++ b/cli/azd/internal/runcontext/agentdetect/types.go @@ -18,7 +18,9 @@ const ( AgentTypeGitHubCopilotCLI AgentType = "github-copilot-cli" // AgentTypeGitHubCopilotApp is GitHub's Copilot App agent. AgentTypeGitHubCopilotApp AgentType = "github-copilot-app" - // AgentTypeVSCodeCopilot is VS Code GitHub Copilot extension. + // AgentTypeGitHubCopilotVSCode is GitHub Copilot agent mode in VS Code. + AgentTypeGitHubCopilotVSCode AgentType = "github-copilot-vscode" + // AgentTypeVSCodeCopilot is the GitHub Copilot for Azure extension in VS Code. AgentTypeVSCodeCopilot AgentType = "vscode-copilot" // AgentTypeGemini is Google's Gemini CLI. AgentTypeGemini AgentType = "gemini" @@ -40,6 +42,8 @@ func (a AgentType) DisplayName() string { return "GitHub Copilot CLI" case AgentTypeGitHubCopilotApp: return "GitHub Copilot App" + case AgentTypeGitHubCopilotVSCode: + return "GitHub Copilot VSCode" case AgentTypeVSCodeCopilot: return "VS Code GitHub Copilot" case AgentTypeGemini: diff --git a/cli/azd/internal/terminal/terminal_test.go b/cli/azd/internal/terminal/terminal_test.go index 6c588832fbd..b826da6cd03 100644 --- a/cli/azd/internal/terminal/terminal_test.go +++ b/cli/azd/internal/terminal/terminal_test.go @@ -45,6 +45,11 @@ func TestIsTerminal_AgentDetection(t *testing.T) { envVars: map[string]string{"COPILOT_CLI": "true"}, expected: false, }, + { + name: "GitHub Copilot VSCode disables TTY", + envVars: map[string]string{"AI_AGENT": "github_copilot_vscode_agent"}, + expected: false, + }, { name: "Gemini CLI disables TTY", envVars: map[string]string{"GEMINI_CLI": "1"}, @@ -92,7 +97,7 @@ func clearTestEnvVars(t *testing.T) { envVarsToUnset := []string{ "AZD_FORCE_TTY", // Agent env vars - "AI_AGENT", + "AI_AGENT", // GitHub Copilot hosts "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", "GITHUB_COPILOT_CLI", "GH_COPILOT", "COPILOT_CLI", "GEMINI_CLI", "GEMINI_CLI_NO_RELAUNCH", diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index f59a0bf8cd0..ab94da9b765 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -295,11 +295,12 @@ const ( EnvCloudShell = "Azure CloudShell" // AI Coding Agent environments - EnvClaudeCode = "Claude Code" - EnvGitHubCopilotCLI = "GitHub Copilot CLI" - EnvGitHubCopilotApp = "GitHub Copilot App" - EnvGemini = "Gemini" - EnvOpenCode = "OpenCode" + EnvClaudeCode = "Claude Code" + EnvGitHubCopilotCLI = "GitHub Copilot CLI" + EnvGitHubCopilotApp = "GitHub Copilot App" + EnvGitHubCopilotVSCode = "GitHub Copilot VSCode" + EnvGemini = "Gemini" + EnvOpenCode = "OpenCode" // Continuous Integration environments diff --git a/cli/azd/internal/tracing/resource/exec_environment.go b/cli/azd/internal/tracing/resource/exec_environment.go index c452942e9ec..e95f5c834ef 100644 --- a/cli/azd/internal/tracing/resource/exec_environment.go +++ b/cli/azd/internal/tracing/resource/exec_environment.go @@ -77,6 +77,8 @@ func execEnvFromAgent() string { return fields.EnvGitHubCopilotCLI case agentdetect.AgentTypeGitHubCopilotApp: return fields.EnvGitHubCopilotApp + case agentdetect.AgentTypeGitHubCopilotVSCode: + return fields.EnvGitHubCopilotVSCode case agentdetect.AgentTypeVSCodeCopilot: return fields.EnvVSCodeAzureCopilot case agentdetect.AgentTypeGemini: diff --git a/cli/azd/internal/tracing/resource/resource_test.go b/cli/azd/internal/tracing/resource/resource_test.go index 6f8a9ee2c2a..84b4b18a1ce 100644 --- a/cli/azd/internal/tracing/resource/resource_test.go +++ b/cli/azd/internal/tracing/resource/resource_test.go @@ -8,6 +8,8 @@ import ( "slices" "testing" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/internal/runcontext/agentdetect" "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" ) @@ -257,6 +259,46 @@ func TestExecEnvForHosts_no_host(t *testing.T) { } } +func TestGetExecutionEnvironment_GitHubCopilotHosts(t *testing.T) { + tests := []struct { + name string + aiAgent string + userAgent string + want string + }{ + { + name: "GitHub Copilot App", + aiAgent: "github_copilot_app_agent", + want: fields.EnvGitHubCopilotApp, + }, + { + name: "GitHub Copilot VSCode", + aiAgent: "github_copilot_vscode_agent", + want: fields.EnvGitHubCopilotVSCode, + }, + { + name: "VS Code Azure GitHub Copilot", + userAgent: internal.VsCodeAzureCopilotAgentPrefix + "/1.0.0", + want: fields.EnvVSCodeAzureCopilot, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("AI_AGENT", tt.aiAgent) + t.Setenv(internal.AzdUserAgentEnvVar, tt.userAgent) + t.Setenv(agentdetect.DisableAgentDetectEnvVar, "") + os.Unsetenv(agentdetect.DisableAgentDetectEnvVar) + agentdetect.ResetDetection() + t.Cleanup(agentdetect.ResetDetection) + + if got := getExecutionEnvironment(); got != tt.want { + t.Fatalf("getExecutionEnvironment() = %q, want %q", got, tt.want) + } + }) + } +} + func TestExecEnvModifiers(t *testing.T) { tests := []struct { name string diff --git a/docs/reference/telemetry-data.md b/docs/reference/telemetry-data.md index 202e8cce7d5..ab49f3acf07 100644 --- a/docs/reference/telemetry-data.md +++ b/docs/reference/telemetry-data.md @@ -579,6 +579,7 @@ The `execution.environment` field identifies where azd is running. Format: `