Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { encodeBase64 } from '../../../base/common/buffer.js';
import { ILoadEstimator, LoadEstimator } from '../../../base/parts/ipc/common/ipc.net.js';
import { TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from '../../telemetry/common/telemetry.js';
import { getTelemetryLevel } from '../../telemetry/common/telemetryUtils.js';
import { AgentHostTelemetryLevelConfigKey, AgentHostCodexEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostPreferLongContextEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, getAgentHostTerminalAutoApproveRulesConfig, SESSION_SYNC_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, GLOBAL_AUTO_APPROVE_SETTING_ID, AUTO_REPLY_SETTING_ID, PREFER_LONG_CONTEXT_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';
import { AgentHostTelemetryLevelConfigKey, AgentHostCodexEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostPreferLongContextEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, getAgentHostTerminalAutoApproveRulesConfig, SESSION_SYNC_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, GLOBAL_AUTO_APPROVE_SETTING_ID, AUTO_REPLY_SETTING_ID, PREFER_LONG_CONTEXT_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, DISABLE_REPO_INFO_TELEMETRY_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';
import type { OtlpExportLogsParams } from '../common/state/protocol/channels-otlp/notifications.js';
import type { TelemetryCapabilities } from '../common/state/protocol/channels-otlp/state.js';
import type { InitializeResult } from '../common/state/protocol/common/commands.js';
Expand Down Expand Up @@ -382,6 +382,12 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
}
this._updateCodexEnabled();
}
if (e.affectsConfiguration(DISABLE_REPO_INFO_TELEMETRY_SETTING_ID)) {
if (this._state.kind !== AgentHostClientState.Connected) {
return;
}
this._updateDisableRepoInfoTelemetry();
}
}));

// Detect silently-dead transports — see {@link _resetLivenessTimers}.
Expand Down Expand Up @@ -477,6 +483,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
this._updateSystemProxyEnabled();
this._updateTerminalAutoApproveRules();
this._updateCodexEnabled();
this._updateDisableRepoInfoTelemetry();
this._transitionTo({ kind: AgentHostClientState.Connected });
}

Expand Down Expand Up @@ -1351,6 +1358,14 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
}, this._clientId, 0);
}

private _updateDisableRepoInfoTelemetry(): void {
const disabled = this._configurationService.getValue<boolean>(DISABLE_REPO_INFO_TELEMETRY_SETTING_ID) === true;
this.dispatchAction(ROOT_STATE_URI, {
type: ActionType.RootConfigChanged,
config: { [AgentHostDisableRepoInfoTelemetryConfigKey]: disabled },
}, this._clientId, 0);
}

private _updateSessionSyncEnabled(): void {
const enabled = !!this._configurationService.getValue<boolean>(SESSION_SYNC_ENABLED_SETTING_ID);
this.dispatchAction(ROOT_STATE_URI, {
Expand Down
22 changes: 22 additions & 0 deletions src/vs/platform/agentHost/common/agentHostGitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ export interface IComputeSessionFileDiffsOptions {
readonly baseBranch?: string;
}

/** Cheap repository facts used to decide whether a branch diff is safe to compute. */
export interface IBranchDiffSafetyInfo {
readonly hasVirtualFileSystem: boolean;
readonly baselineCommitTimestamp: number | undefined;
readonly commitCount: number | undefined;
readonly workspaceFileCount: number;
}

/** A bounded unified-diff result. */
export interface IDiffPatchResult {
readonly patch: string | undefined;
readonly tooLarge: boolean;
}

/** Options for {@link IAgentHostGitService.push}. */
export interface IPushOptions {
/** The branch or refspec to push. Defaults to the current branch. */
Expand Down Expand Up @@ -156,6 +170,10 @@ export interface IAgentHostGitService {
* so the UI always reflects current branch/remote/change state.
*/
getSessionGitState(workingDirectory: URI): Promise<ISessionGitState | undefined>;
/** Returns fetch remote URLs with the preferred remote, then `origin`, first. */
getFetchRemoteUrls(workingDirectory: URI, preferredRemote?: string): Promise<readonly string[] | undefined>;
/** Returns repo-relative untracked file paths. */
getUntrackedPaths(workingDirectory: URI): Promise<readonly string[] | undefined>;

/**
* Computes per-file diffs for the session by shelling out to `git
Expand Down Expand Up @@ -264,6 +282,10 @@ export interface IAgentHostGitService {
* terminal-tool edits the FileEditTracker pipeline misses.
*/
computeFileDiffsBetweenRefs(workingDirectory: URI, options: { readonly sessionUri: string; readonly fromRef: string; readonly toRef: string }): Promise<readonly ISessionFileDiff[] | undefined>;
/** Reads bounded facts needed before computing an expensive branch diff. */
getBranchDiffSafetyInfo(workingDirectory: URI, baselineCommit: string): Promise<IBranchDiffSafetyInfo | undefined>;
/** Computes a unified patch for paths between immutable tree-ish values. */
getDiffPatchBetweenRefs(workingDirectory: URI, options: { readonly fromRef: string; readonly toRef: string; readonly paths: readonly string[]; readonly maxBuffer: number }): Promise<IDiffPatchResult | undefined>;
}

function getCommonBranchPriority(branch: string): number {
Expand Down
12 changes: 12 additions & 0 deletions src/vs/platform/agentHost/common/agentHostSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@ export function migrateLegacyAutopilotConfig<T extends Record<string, unknown> |
*/
export const AgentHostTelemetryLevelConfigKey = 'telemetryLevel';

/** Legacy Copilot Chat debug switch that disables `request.repoInfo` collection. */
export const AgentHostDisableRepoInfoTelemetryConfigKey = 'disableRepoInfoTelemetry';

/** VS Code setting forwarded into {@link AgentHostDisableRepoInfoTelemetryConfigKey}. */
export const DISABLE_REPO_INFO_TELEMETRY_SETTING_ID = 'chat.advanced.debug.disableRepoInfoTelemetry';

/**
* Root config key forwarded from the renderer when VS Code's
* `chat.sessionSync.enabled` setting changes. Controls the `remote` flag
Expand Down Expand Up @@ -655,6 +661,12 @@ const mcpServersValueProperties: Record<string, SessionConfigPropertySchema> = {

export const platformRootSchema = createSchema({
[SessionConfigKey.Permissions]: permissionsProperty,
[AgentHostDisableRepoInfoTelemetryConfigKey]: schemaProperty<boolean>({
type: 'boolean',
title: localize('agentHost.config.disableRepoInfoTelemetry.title', "Disable Repository Information Telemetry"),
description: localize('agentHost.config.disableRepoInfoTelemetry.description', "Whether repository information telemetry is disabled for Agent Host sessions."),
default: false,
}),
[AgentHostTelemetryLevelConfigKey]: schemaProperty<TelemetryConfiguration>({
type: 'string',
title: localize('agentHost.config.telemetryLevel.title', "Telemetry Level"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { AGENT_HOST_CLIENT_BYOK_LM_CHANNEL, AgentHostClientByokLmChannel } from
import { AGENT_HOST_CLIENT_PROXY_CHANNEL, AgentHostClientProxyChannel } from '../common/agentHostClientProxyChannel.js';
import { TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from '../../telemetry/common/telemetry.js';
import { getTelemetryLevel } from '../../telemetry/common/telemetryUtils.js';
import { AgentHostTelemetryLevelConfigKey, AgentHostCodexEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostPreferLongContextEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, getAgentHostTerminalAutoApproveRulesConfig, SESSION_SYNC_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, GLOBAL_AUTO_APPROVE_SETTING_ID, AUTO_REPLY_SETTING_ID, PREFER_LONG_CONTEXT_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';
import { AgentHostTelemetryLevelConfigKey, AgentHostCodexEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyEnabledConfigKey, AgentHostPreferLongContextEnabledConfigKey, AgentHostSystemProxyEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, getAgentHostTerminalAutoApproveRulesConfig, SESSION_SYNC_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, GLOBAL_AUTO_APPROVE_SETTING_ID, AUTO_REPLY_SETTING_ID, PREFER_LONG_CONTEXT_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, DISABLE_REPO_INFO_TELEMETRY_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';

/**
* Renderer-side implementation of {@link IAgentHostService} that connects
Expand Down Expand Up @@ -161,6 +161,9 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
if (e.affectsConfiguration(AgentHostCodexAgentEnabledSettingId)) {
this._updateCodexEnabled();
}
if (e.affectsConfiguration(DISABLE_REPO_INFO_TELEMETRY_SETTING_ID)) {
this._updateDisableRepoInfoTelemetry();
}
}));

if (agentHostEnablementService.enabled) {
Expand Down Expand Up @@ -206,6 +209,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
this._updateSystemProxyEnabled();
this._updateTerminalAutoApproveRules();
this._updateCodexEnabled();
this._updateDisableRepoInfoTelemetry();

store.add(this._proxy.onDidAction(e => {
const revived = revive(e) as ActionEnvelope;
Expand Down Expand Up @@ -245,6 +249,14 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
}, this.clientId, 0);
}

private _updateDisableRepoInfoTelemetry(): void {
const disabled = this._configurationService.getValue<boolean>(DISABLE_REPO_INFO_TELEMETRY_SETTING_ID) === true;
this.dispatchAction(ROOT_STATE_URI, {
type: ActionType.RootConfigChanged,
config: { [AgentHostDisableRepoInfoTelemetryConfigKey]: disabled },
}, this.clientId, 0);
}

private _updateSessionSyncEnabled(): void {
const enabled = !!this._configurationService.getValue<boolean>(SESSION_SYNC_ENABLED_SETTING_ID);
this.dispatchAction(ROOT_STATE_URI, {
Expand Down
112 changes: 91 additions & 21 deletions src/vs/platform/agentHost/node/agentHostGitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { IFileService } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
import { FileEditKind, type ISessionFileDiff, type ISessionGitState } from '../common/state/sessionState.js';
import { buildGitBlobUri } from './gitDiffContent.js';
import { EMPTY_TREE_OBJECT, getBranchCompletions, IAgentHostGitService, IComputeSessionFileDiffsOptions, IPullOptions, IPushOptions } from '../common/agentHostGitService.js';
import { EMPTY_TREE_OBJECT, getBranchCompletions, IAgentHostGitService, IBranchDiffSafetyInfo, IComputeSessionFileDiffsOptions, IPullOptions, IPushOptions } from '../common/agentHostGitService.js';
import { LRUCache } from '../../../base/common/map.js';
import { Limiter, SequencerByKey } from '../../../base/common/async.js';

Expand Down Expand Up @@ -482,6 +482,23 @@ export class AgentHostGitService implements IAgentHostGitService {
return this._computeSessionGitState(workingDirectory);
}

async getFetchRemoteUrls(workingDirectory: URI, preferredRemote?: string): Promise<readonly string[] | undefined> {
const repositoryRoot = await this.getRepositoryRoot(workingDirectory);
if (!repositoryRoot) {
return undefined;
}
return parseFetchRemoteUrls(await this._runGit(repositoryRoot, ['remote', '-v']), preferredRemote);
}

async getUntrackedPaths(workingDirectory: URI): Promise<readonly string[] | undefined> {
const repositoryRoot = await this.getRepositoryRoot(workingDirectory);
if (!repositoryRoot) {
return undefined;
}
const status = await this._runGit(repositoryRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
return status === undefined ? undefined : parseUntrackedPaths(status);
}

async captureWorkingTreeAsTree(workingDirectory: URI): Promise<string | undefined> {
const repositoryRoot = await this.getRepositoryRoot(workingDirectory);
if (!repositoryRoot) {
Expand Down Expand Up @@ -620,6 +637,50 @@ export class AgentHostGitService implements IAgentHostGitService {
}
}

async getBranchDiffSafetyInfo(workingDirectory: URI, baselineCommit: string): Promise<IBranchDiffSafetyInfo | undefined> {
const repositoryRoot = await this.getRepositoryRoot(workingDirectory);
if (!repositoryRoot) {
return undefined;
}

const [virtualFileSystem, sparseCheckout, timestamp, commitCount, workspaceFiles] = await Promise.all([
this._runGit(repositoryRoot, ['config', '--get', 'core.virtualfilesystem']),
this._runGit(repositoryRoot, ['config', '--get', 'core.sparsecheckout']),
this._runGit(repositoryRoot, ['show', '-s', '--format=%ct', baselineCommit]),
this._runGit(repositoryRoot, ['rev-list', '--count', `${baselineCommit}..HEAD`]),
this._runGit(repositoryRoot, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']),
]);
const sparseCheckoutEnabled = new Set(['true', 'yes', 'on', '1']).has(sparseCheckout?.trim().toLowerCase() ?? '');
const timestampSeconds = Number(timestamp?.trim());
const parsedCommitCount = Number(commitCount?.trim());
return {
hasVirtualFileSystem: Boolean(virtualFileSystem?.trim()) || sparseCheckoutEnabled,
baselineCommitTimestamp: Number.isFinite(timestampSeconds) ? timestampSeconds * 1000 : undefined,
commitCount: Number.isFinite(parsedCommitCount) ? parsedCommitCount : undefined,
workspaceFileCount: workspaceFiles?.split('\x00').filter(Boolean).length ?? 0,
};
}

async getDiffPatchBetweenRefs(workingDirectory: URI, options: { readonly fromRef: string; readonly toRef: string; readonly paths: readonly string[]; readonly maxBuffer: number }): Promise<{ readonly patch: string | undefined; readonly tooLarge: boolean } | undefined> {
const repositoryRoot = await this.getRepositoryRoot(workingDirectory);
if (!repositoryRoot) {
return undefined;
}
const paths = [...new Set(options.paths)];
if (paths.length === 0) {
return { patch: '', tooLarge: false };
}
try {
const patch = await this._runGit(repositoryRoot, ['diff', '--patch', '--no-ext-diff', '--find-renames', '--diff-filter=ADMR', options.fromRef, options.toRef, '--', ...paths], { maxBuffer: options.maxBuffer, throwOnError: true });
return patch === undefined ? undefined : { patch, tooLarge: false };
} catch (error) {
if (isMaxBufferError(error)) {
return { patch: undefined, tooLarge: true };
}
throw error;
}
}

private async _computeSessionGitState(workingDirectory: URI): Promise<ISessionGitState | undefined> {
const repositoryRoot = await this.getRepositoryRoot(workingDirectory);
if (!repositoryRoot) {
Expand Down Expand Up @@ -1044,6 +1105,27 @@ export function parseHasGitHubRemote(remotesOutput: string | undefined): boolean
return /github\.com[:\/]/i.test(remotesOutput);
}

/** Returns fetch remote URLs with the preferred remote, then `origin`, first. */
export function parseFetchRemoteUrls(remotesOutput: string | undefined, preferredRemote?: string): string[] | undefined {
if (remotesOutput === undefined) {
return undefined;
}
const candidates: { name: string; url: string }[] = [];
for (const rawLine of remotesOutput.split(/\r?\n/)) {
const match = /^(\S+)\s+(\S+)\s+\(fetch\)$/.exec(rawLine.trim());
if (match) {
candidates.push({ name: match[1], url: match[2] });
}
}
const preferredNames = new Set([preferredRemote, 'origin'].filter((name): name is string => Boolean(name)));
const ordered = [
...candidates.filter(candidate => candidate.name === preferredRemote),
...candidates.filter(candidate => candidate.name === 'origin' && candidate.name !== preferredRemote),
...candidates.filter(candidate => !preferredNames.has(candidate.name)),
];
return [...new Set(ordered.map(candidate => candidate.url))];
}

/**
* Parse `owner` and `repo` from `git remote -v` output. Prefers the `origin`
* remote; falls back to the first GitHub remote so worktrees that renamed
Expand All @@ -1053,28 +1135,11 @@ export function parseHasGitHubRemote(remotesOutput: string | undefined): boolean
* Exported for tests.
*/
export function parseGitHubRepoFromRemote(remotesOutput: string | undefined): { owner: string; repo: string } | undefined {
if (!remotesOutput) {
const candidates = parseFetchRemoteUrls(remotesOutput);
if (!candidates) {
return undefined;
}
// Each line: `<name>\t<url> (<fetch|push>)`. Take fetch URLs only so we
// don't double-count the same remote.
const candidates: { name: string; url: string }[] = [];
for (const rawLine of remotesOutput.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) { continue; }
const m = /^(\S+)\s+(\S+)\s+\(fetch\)$/.exec(line);
if (!m) { continue; }
candidates.push({ name: m[1], url: m[2] });
}
if (candidates.length === 0) {
return undefined;
}
// Prefer `origin`, otherwise first matching remote.
const ordered = [
...candidates.filter(c => c.name === 'origin'),
...candidates.filter(c => c.name !== 'origin'),
];
for (const { url } of ordered) {
for (const url of candidates) {
const parsed = parseGitHubOwnerRepoFromUrl(url);
if (parsed) {
return parsed;
Expand Down Expand Up @@ -1117,3 +1182,8 @@ function stripUndefined<T extends object>(obj: T): T {
}
return out as T;
}

function isMaxBufferError(error: unknown): boolean {
const cause = error instanceof Error ? error.cause : undefined;
return cause instanceof Error && (cause as cp.ExecFileException).code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
}
Loading
Loading