From 0e884bb69707df0815cfa88ceec9e08b056b9d4c Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 22 May 2026 16:23:47 +0200 Subject: [PATCH 1/3] Detect and log terminal/IDE host in telemetry Adds env-based host detection (TERM_PROGRAM, TERMINAL_EMULATOR, CURSOR_TRACE_ID, __CFBundleIdentifier) returning an enum value (vscode, cursor, jetbrains, terminal, etc.). The result is added to the user agent as host/ and to the CLI telemetry ExecutionContext as a new Host field. Includes a best-effort vscode-copilot sentinel that lights up when Copilot agent env vars are seen alongside VSCode. The exact env vars Copilot sets in agent-mode terminals are not stable yet; this is a coarse signal to be refined once we see real telemetry. Co-authored-by: Isaac --- cmd/root/root.go | 2 + cmd/root/user_agent_host.go | 24 ++++ cmd/root/user_agent_host_test.go | 46 +++++++ libs/cmdio/host.go | 94 ++++++++++++++ libs/cmdio/host_test.go | 128 ++++++++++++++++++++ libs/telemetry/protos/databricks_cli_log.go | 5 + 6 files changed, 299 insertions(+) create mode 100644 cmd/root/user_agent_host.go create mode 100644 cmd/root/user_agent_host_test.go create mode 100644 libs/cmdio/host.go create mode 100644 libs/cmdio/host_test.go diff --git a/cmd/root/root.go b/cmd/root/root.go index 6b6de2a9baa..fc7b583c3c2 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -79,6 +79,7 @@ func New(ctx context.Context) *cobra.Command { ctx = withCommandExecIdInUserAgent(ctx) ctx = withUpstreamInUserAgent(ctx) ctx = withInteractiveModeInUserAgent(ctx) + ctx = withHostInUserAgent(ctx) ctx = InjectTestPidToUserAgent(ctx) cmd.SetContext(ctx) return nil @@ -185,6 +186,7 @@ Stack Trace: Command: commandStr, OperatingSystem: runtime.GOOS, DbrVersion: dbr.RuntimeVersion(ctx).String(), + Host: string(cmdio.DetectHost(ctx)), ExecutionTimeMs: time.Since(startTime).Milliseconds(), ExitCode: int64(exitCode), }) diff --git a/cmd/root/user_agent_host.go b/cmd/root/user_agent_host.go new file mode 100644 index 00000000000..2024dffc7c8 --- /dev/null +++ b/cmd/root/user_agent_host.go @@ -0,0 +1,24 @@ +// This file integrates terminal/IDE host detection with the user agent string. +// +// The detection logic is in libs/cmdio. This file retrieves the host from +// the context and adds it to the user agent. +// +// Example user agent strings: +// - "cli/X.Y.Z ... host/vscode ..." +// - "cli/X.Y.Z ... host/cursor ..." +// - "cli/X.Y.Z ... host/unknown ..." +package root + +import ( + "context" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/databricks-sdk-go/useragent" +) + +// Key in the user agent. +const hostKey = "host" + +func withHostInUserAgent(ctx context.Context) context.Context { + return useragent.InContext(ctx, hostKey, string(cmdio.DetectHost(ctx))) +} diff --git a/cmd/root/user_agent_host_test.go b/cmd/root/user_agent_host_test.go new file mode 100644 index 00000000000..8603d3fdfcb --- /dev/null +++ b/cmd/root/user_agent_host_test.go @@ -0,0 +1,46 @@ +package root + +import ( + "testing" + + "github.com/databricks/databricks-sdk-go/useragent" + "github.com/stretchr/testify/assert" +) + +// hostEnvKeys mirrors the env vars read by cmdio.DetectHost. Tests clear them +// so the developer's shell environment cannot bleed into assertions. +var hostEnvKeys = []string{ + "TERM_PROGRAM", + "TERMINAL_EMULATOR", + "CURSOR_TRACE_ID", + "__CFBundleIdentifier", + "GITHUB_COPILOT_AGENT_VERSION", + "COPILOT_AGENT_INTEGRATION_ID", +} + +func clearHostEnv(t *testing.T) { + for _, k := range hostEnvKeys { + t.Setenv(k, "") + } +} + +func TestHostInUserAgent_Unknown(t *testing.T) { + clearHostEnv(t) + ctx := withHostInUserAgent(t.Context()) + assert.Contains(t, useragent.FromContext(ctx), "host/unknown") +} + +func TestHostInUserAgent_VSCode(t *testing.T) { + clearHostEnv(t) + t.Setenv("TERM_PROGRAM", "vscode") + ctx := withHostInUserAgent(t.Context()) + assert.Contains(t, useragent.FromContext(ctx), "host/vscode") +} + +func TestHostInUserAgent_Cursor(t *testing.T) { + clearHostEnv(t) + t.Setenv("TERM_PROGRAM", "vscode") + t.Setenv("CURSOR_TRACE_ID", "abc") + ctx := withHostInUserAgent(t.Context()) + assert.Contains(t, useragent.FromContext(ctx), "host/cursor") +} diff --git a/libs/cmdio/host.go b/libs/cmdio/host.go new file mode 100644 index 00000000000..03a8cf08aad --- /dev/null +++ b/libs/cmdio/host.go @@ -0,0 +1,94 @@ +package cmdio + +import ( + "context" + + "github.com/databricks/cli/libs/env" +) + +// Host describes the terminal or IDE the CLI is being invoked from. +// Values are an enum, never raw env values, so they are safe to log. +type Host string + +const ( + HostVSCode Host = "vscode" + HostVSCodeCopilot Host = "vscode-copilot" + HostCursor Host = "cursor" + HostWindsurf Host = "windsurf" + HostJetBrains Host = "jetbrains" + HostZed Host = "zed" + HostWarp Host = "warp" + HostITerm Host = "iterm" + HostAppleTerminal Host = "apple-terminal" + HostGhostty Host = "ghostty" + HostWezTerm Host = "wezterm" + HostHyper Host = "hyper" + HostTabby Host = "tabby" + HostUnknown Host = "unknown" +) + +// Environment variables we inspect. Sources: +// - VSCode: https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +// - Cursor: VSCode fork; sets CURSOR_TRACE_ID in addition to VSCode vars +// - JetBrains: https://www.jetbrains.com/help/idea/terminal.html +// - Apple Terminal / iTerm / Warp / Ghostty / WezTerm / Hyper / Tabby: set TERM_PROGRAM directly +const ( + envTermProgram = "TERM_PROGRAM" + envTerminalEmulator = "TERMINAL_EMULATOR" + envCursorTraceID = "CURSOR_TRACE_ID" + envCFBundleID = "__CFBundleIdentifier" + envCopilotAgent = "GITHUB_COPILOT_AGENT_VERSION" + envCopilotIntegrator = "COPILOT_AGENT_INTEGRATION_ID" +) + +// DetectHost returns the terminal or IDE host the CLI is being run from, +// derived from environment variables only. Returns HostUnknown if no +// signals match (the common case for raw shells without TERM_PROGRAM set). +func DetectHost(ctx context.Context) Host { + // Cursor and Windsurf are VSCode forks: they inherit TERM_PROGRAM=vscode, + // so check their discriminators before falling through to plain VSCode. + if env.Get(ctx, envCursorTraceID) != "" { + return HostCursor + } + if env.Get(ctx, envCFBundleID) == "com.exafunction.windsurf" { + return HostWindsurf + } + + switch env.Get(ctx, envTermProgram) { + case "vscode": + // Best-effort sentinel for invocations driven by VSCode's Copilot + // coding agent. The exact env vars Copilot sets in agent-mode + // terminals are not stable yet; treat this as a coarse signal to + // be refined once we see real telemetry. + if isCopilotAgent(ctx) { + return HostVSCodeCopilot + } + return HostVSCode + case "Apple_Terminal": + return HostAppleTerminal + case "iTerm.app": + return HostITerm + case "WarpTerminal": + return HostWarp + case "ghostty": + return HostGhostty + case "WezTerm": + return HostWezTerm + case "Hyper": + return HostHyper + case "Tabby": + return HostTabby + case "zed": + return HostZed + } + + if env.Get(ctx, envTerminalEmulator) == "JetBrains-JediTerm" { + return HostJetBrains + } + + return HostUnknown +} + +func isCopilotAgent(ctx context.Context) bool { + return env.Get(ctx, envCopilotAgent) != "" || env.Get(ctx, envCopilotIntegrator) != "" +} diff --git a/libs/cmdio/host_test.go b/libs/cmdio/host_test.go new file mode 100644 index 00000000000..afbe7e19185 --- /dev/null +++ b/libs/cmdio/host_test.go @@ -0,0 +1,128 @@ +package cmdio + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// envKeysToIsolate lists every environment variable read by DetectHost. Tests +// clear all of them at the start so process env from the developer's shell +// (e.g. TERM_PROGRAM=iTerm.app when running locally) cannot leak in. +var envKeysToIsolate = []string{ + envTermProgram, + envTerminalEmulator, + envCursorTraceID, + envCFBundleID, + envCopilotAgent, + envCopilotIntegrator, +} + +func isolateHostEnv(t *testing.T, overrides map[string]string) { + for _, k := range envKeysToIsolate { + t.Setenv(k, "") + } + for k, v := range overrides { + t.Setenv(k, v) + } +} + +func TestDetectHost(t *testing.T) { + tests := []struct { + name string + envs map[string]string + want Host + }{ + { + name: "no env vars", + envs: nil, + want: HostUnknown, + }, + { + name: "vscode", + envs: map[string]string{"TERM_PROGRAM": "vscode"}, + want: HostVSCode, + }, + { + name: "vscode with copilot agent", + envs: map[string]string{ + "TERM_PROGRAM": "vscode", + "GITHUB_COPILOT_AGENT_VERSION": "1.2.3", + }, + want: HostVSCodeCopilot, + }, + { + name: "cursor wins over vscode TERM_PROGRAM", + envs: map[string]string{ + "TERM_PROGRAM": "vscode", + "CURSOR_TRACE_ID": "abc123", + }, + want: HostCursor, + }, + { + name: "windsurf wins over vscode TERM_PROGRAM", + envs: map[string]string{ + "TERM_PROGRAM": "vscode", + "__CFBundleIdentifier": "com.exafunction.windsurf", + }, + want: HostWindsurf, + }, + { + name: "jetbrains", + envs: map[string]string{"TERMINAL_EMULATOR": "JetBrains-JediTerm"}, + want: HostJetBrains, + }, + { + name: "apple terminal", + envs: map[string]string{"TERM_PROGRAM": "Apple_Terminal"}, + want: HostAppleTerminal, + }, + { + name: "iterm", + envs: map[string]string{"TERM_PROGRAM": "iTerm.app"}, + want: HostITerm, + }, + { + name: "warp", + envs: map[string]string{"TERM_PROGRAM": "WarpTerminal"}, + want: HostWarp, + }, + { + name: "ghostty", + envs: map[string]string{"TERM_PROGRAM": "ghostty"}, + want: HostGhostty, + }, + { + name: "wezterm", + envs: map[string]string{"TERM_PROGRAM": "WezTerm"}, + want: HostWezTerm, + }, + { + name: "zed", + envs: map[string]string{"TERM_PROGRAM": "zed"}, + want: HostZed, + }, + { + name: "hyper", + envs: map[string]string{"TERM_PROGRAM": "Hyper"}, + want: HostHyper, + }, + { + name: "tabby", + envs: map[string]string{"TERM_PROGRAM": "Tabby"}, + want: HostTabby, + }, + { + name: "unknown TERM_PROGRAM falls through to unknown", + envs: map[string]string{"TERM_PROGRAM": "somethingnew"}, + want: HostUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isolateHostEnv(t, tt.envs) + assert.Equal(t, tt.want, DetectHost(t.Context())) + }) + } +} diff --git a/libs/telemetry/protos/databricks_cli_log.go b/libs/telemetry/protos/databricks_cli_log.go index 64baa6b384a..0792ee03032 100644 --- a/libs/telemetry/protos/databricks_cli_log.go +++ b/libs/telemetry/protos/databricks_cli_log.go @@ -23,6 +23,11 @@ type ExecutionContext struct { // If true, the CLI is being run from a Databricks notebook / cluster web terminal. FromWebTerminal bool `json:"from_web_terminal,omitempty"` + // Terminal or IDE the CLI is being run from, detected from environment + // variables (TERM_PROGRAM, TERMINAL_EMULATOR, etc.). Enum value, never a + // raw env value. See libs/cmdio/host.go for the full enum. + Host string `json:"host,omitempty"` + // Time taken for the CLI command to execute. // We want to serialize the zero value as well so the omitempty tag is not set. ExecutionTimeMs int64 `json:"execution_time_ms"` From 6743165505013e2fef5f4b1aa8e8da9162afb8d6 Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 22 May 2026 16:41:46 +0200 Subject: [PATCH 2/3] Drop fork-discrimination from host detection Collapse Cursor/Windsurf/code-server into a single 'vscode' label rather than relying on speculative discriminators (ToDesktop IDs aren't stable identifiers; the path-based check is substring-matching a string outside our control). We can split forks back out later if/when we observe a real stable signal. Also drops unverified entries (Hyper, Tabby, Zed) that were guesses. Co-authored-by: Isaac --- cmd/root/user_agent_host_test.go | 8 ++-- libs/cmdio/host.go | 72 +++++++++++++------------------- libs/cmdio/host_test.go | 53 +++++------------------ 3 files changed, 41 insertions(+), 92 deletions(-) diff --git a/cmd/root/user_agent_host_test.go b/cmd/root/user_agent_host_test.go index 8603d3fdfcb..eb47bdd83e3 100644 --- a/cmd/root/user_agent_host_test.go +++ b/cmd/root/user_agent_host_test.go @@ -12,8 +12,6 @@ import ( var hostEnvKeys = []string{ "TERM_PROGRAM", "TERMINAL_EMULATOR", - "CURSOR_TRACE_ID", - "__CFBundleIdentifier", "GITHUB_COPILOT_AGENT_VERSION", "COPILOT_AGENT_INTEGRATION_ID", } @@ -37,10 +35,10 @@ func TestHostInUserAgent_VSCode(t *testing.T) { assert.Contains(t, useragent.FromContext(ctx), "host/vscode") } -func TestHostInUserAgent_Cursor(t *testing.T) { +func TestHostInUserAgent_VSCodeCopilotSentinel(t *testing.T) { clearHostEnv(t) t.Setenv("TERM_PROGRAM", "vscode") - t.Setenv("CURSOR_TRACE_ID", "abc") + t.Setenv("GITHUB_COPILOT_AGENT_VERSION", "1.2.3") ctx := withHostInUserAgent(t.Context()) - assert.Contains(t, useragent.FromContext(ctx), "host/cursor") + assert.Contains(t, useragent.FromContext(ctx), "host/vscode-copilot") } diff --git a/libs/cmdio/host.go b/libs/cmdio/host.go index 03a8cf08aad..9eaa73de891 100644 --- a/libs/cmdio/host.go +++ b/libs/cmdio/host.go @@ -11,55 +11,42 @@ import ( type Host string const ( - HostVSCode Host = "vscode" + // HostVSCode covers TERM_PROGRAM=vscode, which is set by vanilla VSCode + // and every fork that inherits its terminal integration (Cursor, Windsurf, + // code-server, etc.). The forks don't expose a stable, trustworthy + // discriminator in env, so we deliberately don't try to split them apart. + HostVSCode Host = "vscode" + + // HostVSCodeCopilot is a best-effort sentinel for invocations driven by + // VSCode's Copilot coding agent. The env vars Copilot sets are not + // publicly documented; the names checked here are educated guesses and + // may not fire in practice. Treat as a coarse signal to be refined once + // we observe real telemetry. HostVSCodeCopilot Host = "vscode-copilot" - HostCursor Host = "cursor" - HostWindsurf Host = "windsurf" + HostJetBrains Host = "jetbrains" - HostZed Host = "zed" - HostWarp Host = "warp" - HostITerm Host = "iterm" HostAppleTerminal Host = "apple-terminal" - HostGhostty Host = "ghostty" + HostITerm Host = "iterm" + HostWarp Host = "warp" HostWezTerm Host = "wezterm" - HostHyper Host = "hyper" - HostTabby Host = "tabby" + HostGhostty Host = "ghostty" HostUnknown Host = "unknown" ) -// Environment variables we inspect. Sources: -// - VSCode: https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts -// - Cursor: VSCode fork; sets CURSOR_TRACE_ID in addition to VSCode vars -// - JetBrains: https://www.jetbrains.com/help/idea/terminal.html -// - Apple Terminal / iTerm / Warp / Ghostty / WezTerm / Hyper / Tabby: set TERM_PROGRAM directly const ( - envTermProgram = "TERM_PROGRAM" - envTerminalEmulator = "TERMINAL_EMULATOR" - envCursorTraceID = "CURSOR_TRACE_ID" - envCFBundleID = "__CFBundleIdentifier" - envCopilotAgent = "GITHUB_COPILOT_AGENT_VERSION" - envCopilotIntegrator = "COPILOT_AGENT_INTEGRATION_ID" + envTermProgram = "TERM_PROGRAM" + envTerminalEmulator = "TERMINAL_EMULATOR" ) // DetectHost returns the terminal or IDE host the CLI is being run from, -// derived from environment variables only. Returns HostUnknown if no -// signals match (the common case for raw shells without TERM_PROGRAM set). +// derived from environment variables only. +// +// Only detections backed by direct observation or upstream documentation +// are included. Anything we can't verify (Windsurf vs. Cursor split, Zed, +// Hyper, Tabby, etc.) falls into HostUnknown until we see real evidence. func DetectHost(ctx context.Context) Host { - // Cursor and Windsurf are VSCode forks: they inherit TERM_PROGRAM=vscode, - // so check their discriminators before falling through to plain VSCode. - if env.Get(ctx, envCursorTraceID) != "" { - return HostCursor - } - if env.Get(ctx, envCFBundleID) == "com.exafunction.windsurf" { - return HostWindsurf - } - switch env.Get(ctx, envTermProgram) { case "vscode": - // Best-effort sentinel for invocations driven by VSCode's Copilot - // coding agent. The exact env vars Copilot sets in agent-mode - // terminals are not stable yet; treat this as a coarse signal to - // be refined once we see real telemetry. if isCopilotAgent(ctx) { return HostVSCodeCopilot } @@ -70,18 +57,14 @@ func DetectHost(ctx context.Context) Host { return HostITerm case "WarpTerminal": return HostWarp - case "ghostty": - return HostGhostty case "WezTerm": return HostWezTerm - case "Hyper": - return HostHyper - case "Tabby": - return HostTabby - case "zed": - return HostZed + case "ghostty": + return HostGhostty } + // JediTerm is JetBrains' terminal library; sets TERMINAL_EMULATOR + // per https://github.com/JetBrains/jediterm/issues/253. if env.Get(ctx, envTerminalEmulator) == "JetBrains-JediTerm" { return HostJetBrains } @@ -90,5 +73,6 @@ func DetectHost(ctx context.Context) Host { } func isCopilotAgent(ctx context.Context) bool { - return env.Get(ctx, envCopilotAgent) != "" || env.Get(ctx, envCopilotIntegrator) != "" + return env.Get(ctx, "GITHUB_COPILOT_AGENT_VERSION") != "" || + env.Get(ctx, "COPILOT_AGENT_INTEGRATION_ID") != "" } diff --git a/libs/cmdio/host_test.go b/libs/cmdio/host_test.go index afbe7e19185..0859ed37ddb 100644 --- a/libs/cmdio/host_test.go +++ b/libs/cmdio/host_test.go @@ -6,16 +6,14 @@ import ( "github.com/stretchr/testify/assert" ) -// envKeysToIsolate lists every environment variable read by DetectHost. Tests -// clear all of them at the start so process env from the developer's shell -// (e.g. TERM_PROGRAM=iTerm.app when running locally) cannot leak in. +// envKeysToIsolate lists every environment variable read by DetectHost. +// Tests clear all of them at the start so process env from the developer's +// shell (e.g. TERM_PROGRAM=iTerm.app on a macOS dev machine) cannot leak in. var envKeysToIsolate = []string{ envTermProgram, envTerminalEmulator, - envCursorTraceID, - envCFBundleID, - envCopilotAgent, - envCopilotIntegrator, + "GITHUB_COPILOT_AGENT_VERSION", + "COPILOT_AGENT_INTEGRATION_ID", } func isolateHostEnv(t *testing.T, overrides map[string]string) { @@ -39,34 +37,18 @@ func TestDetectHost(t *testing.T) { want: HostUnknown, }, { - name: "vscode", + name: "vscode and forks all classify as vscode", envs: map[string]string{"TERM_PROGRAM": "vscode"}, want: HostVSCode, }, { - name: "vscode with copilot agent", + name: "vscode with copilot agent env", envs: map[string]string{ "TERM_PROGRAM": "vscode", "GITHUB_COPILOT_AGENT_VERSION": "1.2.3", }, want: HostVSCodeCopilot, }, - { - name: "cursor wins over vscode TERM_PROGRAM", - envs: map[string]string{ - "TERM_PROGRAM": "vscode", - "CURSOR_TRACE_ID": "abc123", - }, - want: HostCursor, - }, - { - name: "windsurf wins over vscode TERM_PROGRAM", - envs: map[string]string{ - "TERM_PROGRAM": "vscode", - "__CFBundleIdentifier": "com.exafunction.windsurf", - }, - want: HostWindsurf, - }, { name: "jetbrains", envs: map[string]string{"TERMINAL_EMULATOR": "JetBrains-JediTerm"}, @@ -87,30 +69,15 @@ func TestDetectHost(t *testing.T) { envs: map[string]string{"TERM_PROGRAM": "WarpTerminal"}, want: HostWarp, }, - { - name: "ghostty", - envs: map[string]string{"TERM_PROGRAM": "ghostty"}, - want: HostGhostty, - }, { name: "wezterm", envs: map[string]string{"TERM_PROGRAM": "WezTerm"}, want: HostWezTerm, }, { - name: "zed", - envs: map[string]string{"TERM_PROGRAM": "zed"}, - want: HostZed, - }, - { - name: "hyper", - envs: map[string]string{"TERM_PROGRAM": "Hyper"}, - want: HostHyper, - }, - { - name: "tabby", - envs: map[string]string{"TERM_PROGRAM": "Tabby"}, - want: HostTabby, + name: "ghostty", + envs: map[string]string{"TERM_PROGRAM": "ghostty"}, + want: HostGhostty, }, { name: "unknown TERM_PROGRAM falls through to unknown", From 2744ae925f99052a9f725337addcb8c58e064ecd Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 22 May 2026 16:44:24 +0200 Subject: [PATCH 3/3] Drop Copilot sentinel from host detection Host and 'is Copilot/agent active' are two independent dimensions. Mixing them into one enum value (vscode-copilot) makes it awkward to compute populations like "VSCode users WITHOUT the Copilot extension". Drop the sentinel from this PR; agent/extension detection should land as its own dimension once we've verified the actual env signals. Co-authored-by: Isaac --- cmd/root/user_agent_host_test.go | 10 ---------- libs/cmdio/host.go | 20 +++++--------------- libs/cmdio/host_test.go | 10 ---------- 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/cmd/root/user_agent_host_test.go b/cmd/root/user_agent_host_test.go index eb47bdd83e3..d639baac83b 100644 --- a/cmd/root/user_agent_host_test.go +++ b/cmd/root/user_agent_host_test.go @@ -12,8 +12,6 @@ import ( var hostEnvKeys = []string{ "TERM_PROGRAM", "TERMINAL_EMULATOR", - "GITHUB_COPILOT_AGENT_VERSION", - "COPILOT_AGENT_INTEGRATION_ID", } func clearHostEnv(t *testing.T) { @@ -34,11 +32,3 @@ func TestHostInUserAgent_VSCode(t *testing.T) { ctx := withHostInUserAgent(t.Context()) assert.Contains(t, useragent.FromContext(ctx), "host/vscode") } - -func TestHostInUserAgent_VSCodeCopilotSentinel(t *testing.T) { - clearHostEnv(t) - t.Setenv("TERM_PROGRAM", "vscode") - t.Setenv("GITHUB_COPILOT_AGENT_VERSION", "1.2.3") - ctx := withHostInUserAgent(t.Context()) - assert.Contains(t, useragent.FromContext(ctx), "host/vscode-copilot") -} diff --git a/libs/cmdio/host.go b/libs/cmdio/host.go index 9eaa73de891..b91e90d79f0 100644 --- a/libs/cmdio/host.go +++ b/libs/cmdio/host.go @@ -17,13 +17,6 @@ const ( // discriminator in env, so we deliberately don't try to split them apart. HostVSCode Host = "vscode" - // HostVSCodeCopilot is a best-effort sentinel for invocations driven by - // VSCode's Copilot coding agent. The env vars Copilot sets are not - // publicly documented; the names checked here are educated guesses and - // may not fire in practice. Treat as a coarse signal to be refined once - // we observe real telemetry. - HostVSCodeCopilot Host = "vscode-copilot" - HostJetBrains Host = "jetbrains" HostAppleTerminal Host = "apple-terminal" HostITerm Host = "iterm" @@ -44,12 +37,14 @@ const ( // Only detections backed by direct observation or upstream documentation // are included. Anything we can't verify (Windsurf vs. Cursor split, Zed, // Hyper, Tabby, etc.) falls into HostUnknown until we see real evidence. +// +// Whether a user has a particular extension or AI agent active (Copilot, +// Claude Code, Cursor Agent, etc.) is intentionally not modelled here. +// That's an independent dimension, so a downstream query can ask "vscode +// users without Copilot" by joining the two signals. func DetectHost(ctx context.Context) Host { switch env.Get(ctx, envTermProgram) { case "vscode": - if isCopilotAgent(ctx) { - return HostVSCodeCopilot - } return HostVSCode case "Apple_Terminal": return HostAppleTerminal @@ -71,8 +66,3 @@ func DetectHost(ctx context.Context) Host { return HostUnknown } - -func isCopilotAgent(ctx context.Context) bool { - return env.Get(ctx, "GITHUB_COPILOT_AGENT_VERSION") != "" || - env.Get(ctx, "COPILOT_AGENT_INTEGRATION_ID") != "" -} diff --git a/libs/cmdio/host_test.go b/libs/cmdio/host_test.go index 0859ed37ddb..2c3a05111a7 100644 --- a/libs/cmdio/host_test.go +++ b/libs/cmdio/host_test.go @@ -12,8 +12,6 @@ import ( var envKeysToIsolate = []string{ envTermProgram, envTerminalEmulator, - "GITHUB_COPILOT_AGENT_VERSION", - "COPILOT_AGENT_INTEGRATION_ID", } func isolateHostEnv(t *testing.T, overrides map[string]string) { @@ -41,14 +39,6 @@ func TestDetectHost(t *testing.T) { envs: map[string]string{"TERM_PROGRAM": "vscode"}, want: HostVSCode, }, - { - name: "vscode with copilot agent env", - envs: map[string]string{ - "TERM_PROGRAM": "vscode", - "GITHUB_COPILOT_AGENT_VERSION": "1.2.3", - }, - want: HostVSCodeCopilot, - }, { name: "jetbrains", envs: map[string]string{"TERMINAL_EMULATOR": "JetBrains-JediTerm"},