Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/workflows/pr-darwin-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ jobs:
timeout-minutes: 40
run: bash test/smoke/scripts/run-agents-window-network-proxy.sh

- name: 🧪 Run Agents Window smoke tests through Kerberos-authenticated macOS PAC proxy
if: ${{ inputs.electron_tests && inputs.smoke_tests }}
timeout-minutes: 40
run: bash test/smoke/scripts/run-agents-window-network-proxy.sh --kerberos

- name: 🧪 Run smoke tests (Browser, Chromium)
if: ${{ inputs.browser_tests && inputs.smoke_tests }}
timeout-minutes: 20
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ steps:
timeoutInMinutes: 40
displayName: 🧪 Run Agents Window smoke tests through macOS PAC proxy

- script: |
set -e
APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)
APP_NAME="`ls $APP_ROOT | head -n 1`"
bash test/smoke/scripts/run-agents-window-network-proxy.sh --kerberos --build "$APP_ROOT/$APP_NAME"
timeoutInMinutes: 40
displayName: 🧪 Run Agents Window smoke tests through Kerberos-authenticated macOS PAC proxy

- ${{ if eq(parameters.VSCODE_RUN_BROWSER_TESTS, true) }}:
- script: npm run smoketest-no-compile -- --web --tracing --headless
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import { IAccessibilityService } from '../../../../../../../platform/accessibili
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
import { IEditorMouseEvent } from '../../../../../../browser/editorBrowser.js';
import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
import { Point } from '../../../../../../common/core/2d/point.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { HoverService } from '../../../../../../../platform/hover/browser/hoverService.js';
import { HoverWidget } from '../../../../../../../platform/hover/browser/hoverWidget.js';
Expand Down Expand Up @@ -138,17 +136,6 @@ export class InlineEditsGutterIndicator extends Disposable {
minContentWidthInPx: constObservable(0),
}));

this._register(this._editorObs.editor.onMouseMove((e: IEditorMouseEvent) => {
const state = this._state.get();
if (state === undefined) { return; }

const el = this._iconRef.element;
const rect = el.getBoundingClientRect();
const rectangularArea = Rect.fromLeftTopWidthHeight(rect.left, rect.top, rect.width, rect.height);
const point = new Point(e.event.posx, e.event.posy);
this._isHoveredOverIcon.set(rectangularArea.containsPoint(point), undefined);
}));

this._register(this._editorObs.editor.onDidScrollChange(() => {
this._isHoveredOverIcon.set(false, undefined);
}));
Expand Down Expand Up @@ -548,9 +535,11 @@ export class InlineEditsGutterIndicator extends Disposable {
},

onmouseenter: () => {
this._isHoveredOverIcon.set(true, undefined);
// TODO show hover when hovering ghost text etc.
this._showHover();
},
onmouseleave: () => this._isHoveredOverIcon.set(false, undefined),
style: {
cursor: 'pointer',
zIndex: '20',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -969,10 +969,12 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
* response.
*/
async subscribe(resource: URI): Promise<IStateSnapshot> {
this._logService.trace(`[RemoteAgentHostProtocol] subscribe start: ${resource.toString()}`);
const result = await this._sendRequest('subscribe', { channel: resource.toString() });
if (!result.snapshot) {
throw new Error(`subscribe to ${resource.toString()} returned no snapshot`);
}
this._logService.trace(`[RemoteAgentHostProtocol] subscribe done: ${resource.toString()}`);
return result.snapshot;
}

Expand Down
13 changes: 11 additions & 2 deletions src/vs/platform/agentHost/common/agentHostGitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ export interface IWorktreeFileProgress {
readonly filesTotal: number;
}

export interface IAddWorktreeOptions {
readonly path: URI;
readonly commitish: string;
readonly newBranchName?: string;
readonly track: boolean;
readonly preferRemoteBranch?: boolean;
readonly onProgress?: (progress: IWorktreeFileProgress) => void;
}

export interface IAgentHostGitService {
readonly _serviceBrand: undefined;
getCurrentBranch(workingDirectory: URI): Promise<string | undefined>;
Expand All @@ -215,13 +224,13 @@ export interface IAgentHostGitService {
/** Returns worktree roots in Git's porcelain order, with the primary worktree first. */
getWorktreeRoots(workingDirectory: URI): Promise<URI[]>;
/**
* Creates a worktree for a new branch. `onProgress` receives every checkout
* Creates a worktree, optionally on a new branch. `onProgress` receives every checkout
* sample git reports, which can be several per second, so consumers are
* expected to round and rate limit for their own presentation. It may also
* never be called (fast checkouts and git versions that stay silent), so it
* MUST be treated as best-effort.
*/
addWorktree(repositoryRoot: URI, worktree: URI, branchName: string, startPoint: string, track: boolean, onProgress?: (progress: IWorktreeFileProgress) => void): Promise<void>;
addWorktree(repositoryRoot: URI, options: IAddWorktreeOptions): Promise<void>;
/**
* Copies the git-ignored files matching `globs` into the worktree.
* `onProgress` counts the individual files covered, but only fires as whole
Expand Down
28 changes: 17 additions & 11 deletions src/vs/platform/agentHost/node/agentHostGitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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, IAgentHostGitService, IBranch, IBranchDiffSafetyInfo, IRefQuery, IComputeSessionFileDiffsOptions, IDefaultBranch, IPullOptions, IPushOptions, GitRefType, IRemoteBranch, GitRef, ITag, Branch, IWorktreeFileProgress } from '../common/agentHostGitService.js';
import { EMPTY_TREE_OBJECT, IAddWorktreeOptions, IAgentHostGitService, IBranch, IBranchDiffSafetyInfo, IRefQuery, IComputeSessionFileDiffsOptions, IDefaultBranch, IPullOptions, IPushOptions, GitRefType, IRemoteBranch, GitRef, ITag, Branch, IWorktreeFileProgress } from '../common/agentHostGitService.js';
import { LRUCache } from '../../../base/common/map.js';
import { firstParallel, Limiter, SequencerByKey, timeout } from '../../../base/common/async.js';

Expand Down Expand Up @@ -151,26 +151,32 @@ export class AgentHostGitService implements IAgentHostGitService {
.map(line => URI.file(line.substring('worktree '.length)));
}

async addWorktree(repositoryRoot: URI, worktree: URI, branchName: string, startPoint: string, track = false, onProgress?: (progress: IWorktreeFileProgress) => void): Promise<void> {
const resolvedStartPoint = await this._resolveRemoteTrackingBranch(repositoryRoot, startPoint, track) ?? startPoint;
async addWorktree(repositoryRoot: URI, options: IAddWorktreeOptions): Promise<void> {
const resolvedCommitish = options.preferRemoteBranch
? await this._resolveRemoteTrackingBranch(repositoryRoot, options.commitish, options.track) ?? options.commitish
: options.commitish;

const args = ['-c', 'checkout.workers=0', 'worktree', 'add'];

if (!track) {
// Pass --no-track so the new agent branch never picks up upstream
// tracking from the start point (e.g. when starting from
// 'origin/main', without --no-track git would set the new branch's
// upstream to origin/main, which would mis-attribute pushes/pulls).
args.push('--no-track');
if (options.newBranchName) {
if (!options.track) {
// Pass --no-track so the new agent branch never picks up upstream
// tracking from the start point (e.g. when starting from
// 'origin/main', without --no-track git would set the new branch's
// upstream to origin/main, which would mis-attribute pushes/pulls).
args.push('--no-track');
}

args.push('-b', options.newBranchName);
}

args.push('-b', branchName, worktree.fsPath, resolvedStartPoint);
args.push(options.path.fsPath, resolvedCommitish);

// `git worktree add` forces progress reporting on its internal checkout
// even when stderr is a pipe, so `Updating files: N% (x/y)` can be
// parsed for live feedback. GIT_PROGRESS_DELAY=0 lifts git's default
// two-second suppression so the first sample arrives immediately.
const progressParser = onProgress ? new GitCheckoutProgressParser(onProgress) : undefined;
const progressParser = options.onProgress ? new GitCheckoutProgressParser(options.onProgress) : undefined;

await this._runGit(repositoryRoot, args, {
timeout: 180_000,
Expand Down
16 changes: 15 additions & 1 deletion src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3066,7 +3066,10 @@ export class AgentService extends Disposable implements IAgentService {
*/
private async _getChatMessages(provider: IAgent, chat: URI, session: URI, origin?: ChatOrigin): Promise<readonly Turn[]> {
const context = { ...this._chatContext(session, chat), ...(origin ? { origin } : {}) };
const turns = await this._applyPersistedTurnUsage(chat, await provider.chats.getMessages(chat, context));
this._logService.trace(`[AgentService] getChatMessages start: chat=${chat.toString()}`);
const providerTurns = await provider.chats.getMessages(chat, context);
this._logService.trace(`[AgentService] getChatMessages: provider returned ${providerTurns.length} turn(s) for chat=${chat.toString()}`);
const turns = await this._applyPersistedTurnUsage(chat, providerTurns);
// Host-owned worktree restore announcement: re-inject the "Created isolated
// worktree" message at the top of the default chat's first turn from
// persisted metadata. No-op for folder sessions and non-default chats (peer
Expand Down Expand Up @@ -3111,6 +3114,7 @@ export class AgentService extends Disposable implements IAgentService {
}
try {
usages = await ref.object.getTurnUsages();
this._logService.trace(`[AgentService] getTurnUsages done: ${usages.size} row(s) for ${storage.toString()}`);
} catch (err) {
this._logService.warn(`[AgentService] Failed to read persisted turn usage for ${storage.toString()}`, err);
return turns;
Expand Down Expand Up @@ -3790,6 +3794,7 @@ export class AgentService extends Disposable implements IAgentService {
}

let snapshot = this._stateManager.getSnapshot(resourceStr);
const servedFromMemory = !!snapshot;
const parsedChangeset = parseChangesetUri(resourceStr);
if (snapshot && parsedChangeset && !this._stateManager.getSessionState(parsedChangeset.sessionUri)) {
await this._changesetCoordinator.restoreSessionIfChangesetSubscription(resource, s => this.restoreSession(s));
Expand Down Expand Up @@ -3876,6 +3881,7 @@ export class AgentService extends Disposable implements IAgentService {
void this._gitStateService.refreshSessionGitState(resourceStr, workingDirectory);
}

this._logService.trace(`[AgentService] subscribe done: ${resourceStr} (servedFromMemory=${servedFromMemory})`);
return snapshot;
} catch (err) {
this.unsubscribe(resource, clientId);
Expand Down Expand Up @@ -4689,17 +4695,20 @@ export class AgentService extends Disposable implements IAgentService {

const inFlight = this._restoreSessionInFlight.get(sessionStr);
if (inFlight) {
this._logService.trace(`[AgentService] restoreSession: joining in-flight restore for ${sessionStr}`);
return inFlight;
}

if (this._stateManager.getSessionState(sessionStr)) {
return;
}

this._logService.trace(`[AgentService] restoreSession start: ${sessionStr}`);
const restore = this._doRestoreSession(session, sessionStr);
this._restoreSessionInFlight.set(sessionStr, restore);
try {
await restore;
this._logService.trace(`[AgentService] restoreSession done: ${sessionStr}`);
} finally {
if (this._restoreSessionInFlight.get(sessionStr) === restore) {
this._restoreSessionInFlight.delete(sessionStr);
Expand Down Expand Up @@ -4762,6 +4771,7 @@ export class AgentService extends Disposable implements IAgentService {
}
const registeredSession = (await this._listRegisteredSessions()).find(entry => entry.session.toString() === sessionStr);
const external = registeredSession?.external ?? false;
this._logService.trace(`[AgentService] restore: catalog and registry resolved for ${sessionStr} (registered=${!!registeredSession}, external=${external})`);

// Adopt-on-open for a surfaced un-adopted legacy Copilot CLI session, strictly gated on the live migrate setting (a no-op for native / already-adopted sessions).
const migrateLegacyEnabled = this._configurationService.getRootValue(platformRootSchema, AgentHostMigrateLegacyCopilotCliEnabledConfigKey) === true;
Expand Down Expand Up @@ -4891,6 +4901,7 @@ export class AgentService extends Disposable implements IAgentService {
* fails so the caller can report the outcome accurately.
*/
private async _restoreSessionState(agent: IAgent, session: URI, sessionStr: string, adopted: boolean, external: boolean, registrationSource: IRegisteredSession['source'], catalogReadable: boolean, sessionKnownToRegistry: boolean): Promise<{ turnCount: number; hasProject: boolean; hasWorktree: boolean; workingDirectoryCount: number }> {
this._logService.trace(`[AgentService] restore: reading provider metadata for ${sessionStr}`);
let meta = await this._getSessionMetadataForRestore(agent, session, external);
if (!meta) {
// Authoritative absence only when the catalog was readable this run and
Expand All @@ -4901,6 +4912,7 @@ export class AgentService extends Disposable implements IAgentService {
? new ProtocolError(AHP_SESSION_NOT_FOUND, `Session not found on backend: ${sessionStr}`)
: new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Provider ${agent.id} could not describe ${sessionStr} yet`);
}
this._logService.trace(`[AgentService] restore: provider metadata resolved for ${sessionStr}`);

// A freshly-adopted legacy session whose working directory is a
// pre-existing git worktree keeps no worktree metadata (adoption seeds
Expand Down Expand Up @@ -5079,6 +5091,7 @@ export class AgentService extends Disposable implements IAgentService {
// Best-effort: fall back to agent-provided metadata
}
}
this._logService.trace(`[AgentService] restore: persisted session metadata read for ${sessionStr}`);

// Encode isRead/isArchived as status bitmask flags
let status: SessionStatus = SessionStatus.Idle;
Expand Down Expand Up @@ -5127,6 +5140,7 @@ export class AgentService extends Disposable implements IAgentService {
}
this._invalidateSessionList();
this._stateManager.restoreSession(summary, mergedTurns, { draft: restoredDraft, defaultChatTitle });
this._logService.trace(`[AgentService] restore: hydrated state for ${sessionStr} with ${mergedTurns.length} turn(s)`);
this._serverToolHost.advertise(sessionStr);

// A freshly-adopted legacy session bridges its git checkpoints into the
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2655,7 +2655,9 @@ export class CopilotAgentSession extends Disposable {
}

private async _computeMappedEvents(): Promise<IMappedSessionEvents> {
this._logService.trace(`[Copilot:${this.sessionId}] Reading persisted session events`);
const events = await this._wrapper.session.getEvents();
this._logService.trace(`[Copilot:${this.sessionId}] Read ${events.length} persisted event(s); reconstructing turns`);
let db: ISessionDatabase | undefined;
try {
db = this._databaseRef.object;
Expand All @@ -2668,6 +2670,7 @@ export class CopilotAgentSession extends Disposable {
? this._launchPlan.model
: this._launchPlan.fallback.model,
});
this._logService.trace(`[Copilot:${this.sessionId}] Reconstructed ${result.turns.length} turn(s) from ${events.length} event(s)`);
return result;
}

Expand Down
13 changes: 8 additions & 5 deletions src/vs/platform/agentHost/node/copilot/toolSearchDeferral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { isGpt56Model } from './modelIdentifiers.js';
import { SEMANTIC_SEARCH_TOOL_NAME } from '../../common/semanticSearchConstants.js';

export { CLIENT_TOOL_SEARCH_REFERENCE_NAME, RUNTIME_TOOL_SEARCH_TOOL_NAME } from '../../common/toolSearchConstants.js';
Expand All @@ -19,16 +18,20 @@ export const NON_DEFERRED_CLIENT_TOOL_NAMES: ReadonlySet<string> = new Set<strin
SEMANTIC_SEARCH_TOOL_NAME,
]);

/** Mirrors the Copilot extension's string-form `modelSupportsToolSearch`. */
/**
* Follows the Copilot extension's string-form `modelSupportsToolSearch`, minus
* the GPT families — see the deliberate divergence below before re-syncing this
* with the extension.
*/
export function agentHostModelSupportsToolSearch(modelId: string | undefined): boolean {
if (!modelId) {
return false;
}
const id = modelId.toLowerCase();
const normalizedId = id.replace(/\./g, '-');
if (normalizedId === 'gpt-5-4' || normalizedId === 'gpt-5-5' || isGpt56Model(id)) {
return true;
}
// GPT-5.4 / 5.5 / 5.6 supported tool search and were turned off pending
// microsoft/vscode-copilot-evaluation#6323; re-enabling them requires restoring
// the GPT-5.4/5.5 exact checks and the `isGpt56Model` import/check.
if (!normalizedId.startsWith('claude')) {
return false;
}
Expand Down
9 changes: 8 additions & 1 deletion src/vs/platform/agentHost/node/shared/worktreeIsolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,14 @@ export class WorktreeIsolation extends Disposable implements IAgentHostWorktreeI

const worktreeBranchTrack = config[SessionConfigKey.WorktreeBranchTrack] === true;
await withPercentProgress(WorktreeCreationPhase.CheckingOut, onProgress, progress =>
this._gitService.addWorktree(repositoryRoot, worktree, branchName, baseBranch, worktreeBranchTrack, progress));
this._gitService.addWorktree(repositoryRoot, {
path: worktree,
commitish: baseBranch,
newBranchName: branchName,
track: worktreeBranchTrack,
preferRemoteBranch: true,
onProgress: progress,
}));
return { branchName, worktree, baseBranch };
});
const worktreeIncludeFiles = Array.isArray(config[SessionConfigKey.WorktreeIncludeFiles])
Expand Down
Loading
Loading