Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 13 additions & 2 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface IAgentSessionMetadata {
readonly session: URI;
readonly startTime: number;
readonly modifiedTime: number;
readonly project?: IAgentSessionProjectInfo;
readonly summary?: string;
readonly status?: SessionStatus;
readonly workingDirectory?: URI;
Expand All @@ -49,6 +50,16 @@ export interface IAgentSessionMetadata {
readonly diffs?: readonly { readonly uri: string; readonly added?: number; readonly removed?: number }[];
}

export interface IAgentSessionProjectInfo {
readonly uri: URI;
readonly displayName: string;
}

export interface IAgentCreateSessionResult {
readonly session: URI;
readonly project?: IAgentSessionProjectInfo;
}

export type AgentProvider = string;

/** Metadata describing an agent backend, discovered over IPC. */
Expand Down Expand Up @@ -321,8 +332,8 @@ export interface IAgent {
/** Fires when the provider streams progress for a session. */
readonly onDidSessionProgress: Event<IAgentProgressEvent>;

/** Create a new session. Returns the session URI. */
createSession(config?: IAgentCreateSessionConfig): Promise<URI>;
/** Create a new session. Returns server-owned session metadata. */
createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult>;

/** Send a user message into an existing session. */
sendMessage(session: URI, prompt: string, attachments?: IAgentAttachment[], turnId?: string): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4e0303d
8a85cec
14 changes: 14 additions & 0 deletions src/vs/platform/agentHost/common/state/protocol/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,18 @@ export interface ISessionFileDiff {
removed?: number;
}

/**
* Server-owned project metadata for a session.
*
* @category Session State
*/
export interface IProjectInfo {
/** Project URI */
uri: URI;
/** Human-readable project name */
displayName: string;
}

/**
* @category Session State
*/
Expand All @@ -352,6 +364,8 @@ export interface ISessionSummary {
createdAt: number;
/** Last modification timestamp */
modifiedAt: number;
/** Server-owned project for this session */
project?: IProjectInfo;
/** Currently selected model */
model?: string;
/** The working directory URI for this session */
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/agentHost/common/state/sessionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export {
type IAgentInfo,
type IContentRef,
type IErrorInfo,
type IProjectInfo,
type IMarkdownResponsePart,
type IMessageAttachment,
type IReasoningResponsePart,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { DeferredPromise } from '../../../base/common/async.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable, IReference } from '../../../base/common/lifecycle.js';
import { Schemas } from '../../../base/common/network.js';
import { hasKey } from '../../../base/common/types.js';
import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js';
Expand Down Expand Up @@ -235,6 +236,12 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
session: URI.parse(s.resource),
startTime: s.createdAt,
modifiedTime: s.modifiedAt,
...(s.project ? {
project: {
uri: this._toLocalProjectUri(URI.parse(s.project.uri)),
displayName: s.project.displayName,
}
} : {}),
summary: s.title,
status: s.status,
workingDirectory: typeof s.workingDirectory === 'string' ? toAgentHostUri(URI.parse(s.workingDirectory), this._connectionAuthority) : undefined,
Expand All @@ -243,6 +250,10 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
}));
}

private _toLocalProjectUri(uri: URI): URI {
return uri.scheme === Schemas.file ? toAgentHostUri(uri, this._connectionAuthority) : uri;
}

/**
* List the contents of a directory on the remote host's filesystem.
*/
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/agentHost/node/agentHostStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export class AgentHostStateManager extends Disposable {
if (current.title !== lastNotified.title) { changes.title = current.title; }
if (current.status !== lastNotified.status) { changes.status = current.status; }
if (current.modifiedAt !== lastNotified.modifiedAt) { changes.modifiedAt = current.modifiedAt; }
if (current.project !== lastNotified.project) { changes.project = current.project; }
if (current.model !== lastNotified.model) { changes.model = current.model; }
if (current.workingDirectory !== lastNotified.workingDirectory) { changes.workingDirectory = current.workingDirectory; }
if (current.isRead !== lastNotified.isRead) { changes.isRead = current.isRead; }
Expand Down
7 changes: 6 additions & 1 deletion src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,11 @@ export class AgentService extends Disposable implements IAgentService {
// Safe to run in parallel with createSession since no events flow until
// sendMessage() is called.
this._logService.trace(`[AgentService] createSession: initializing auto-approver and creating session...`);
const [, session] = await Promise.all([
const [, created] = await Promise.all([
this._sideEffects.initialize(),
provider.createSession(config),
]);
const session = created.session;
this._logService.trace(`[AgentService] createSession: initialization complete`);

this._logService.trace(`[AgentService] createSession: provider=${provider.id} model=${config?.model ?? '(default)'}`);
Expand All @@ -228,6 +229,7 @@ export class AgentService extends Disposable implements IAgentService {
status: SessionStatus.Idle,
createdAt: Date.now(),
modifiedAt: Date.now(),
...(created.project ? { project: { uri: created.project.uri.toString(), displayName: created.project.displayName } } : {}),
workingDirectory: config.workingDirectory?.toString(),
};
const state = this._stateManager.createSession(summary);
Expand All @@ -241,6 +243,7 @@ export class AgentService extends Disposable implements IAgentService {
status: SessionStatus.Idle,
createdAt: Date.now(),
modifiedAt: Date.now(),
...(created.project ? { project: { uri: created.project.uri.toString(), displayName: created.project.displayName } } : {}),
workingDirectory: config?.workingDirectory?.toString(),
};
this._stateManager.createSession(summary);
Expand Down Expand Up @@ -419,6 +422,7 @@ export class AgentService extends Disposable implements IAgentService {
status: SessionStatus.Idle,
createdAt: meta.startTime,
modifiedAt: meta.modifiedTime,
...(meta.project ? { project: { uri: meta.project.uri.toString(), displayName: meta.project.displayName } } : {}),
workingDirectory: meta.workingDirectory?.toString(),
isRead,
isDone,
Expand Down Expand Up @@ -878,6 +882,7 @@ export class AgentService extends Disposable implements IAgentService {
status: SessionStatus.Idle,
createdAt: Date.now(),
modifiedAt: Date.now(),
...(parentState?.summary.project ? { project: parentState.summary.project } : {}),
},
childTurns,
);
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/agentHost/node/agentSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ export class AgentSideEffects extends Disposable {
}

this._logService.info(`[AgentSideEffects] Creating subagent session: ${subagentSessionUri} (parent=${parentSession}, toolCallId=${toolCallId})`);
const parentState = this._stateManager.getSessionState(parentSession);

// Create the subagent session silently (restoreSession skips notification)
this._stateManager.restoreSession(
Expand All @@ -365,6 +366,7 @@ export class AgentSideEffects extends Disposable {
status: SessionStatus.Idle,
createdAt: Date.now(),
modifiedAt: Date.now(),
...(parentState?.summary.project ? { project: parentState.summary.project } : {}),
},
[],
);
Expand Down
27 changes: 17 additions & 10 deletions src/vs/platform/agentHost/node/copilot/copilotAgent.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 { IInstantiationService } from '../../../instantiation/common/instantiation.js';
import { ILogService } from '../../../log/common/log.js';
import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js';
import { AgentSession, IAgent, IAgentAttachment, IAgentCreateSessionConfig, IAgentDescriptor, IAgentMessageEvent, IAgentModelInfo, IAgentProgressEvent, IAgentSessionMetadata, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
import { AgentSession, IAgent, IAgentAttachment, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMessageEvent, IAgentModelInfo, IAgentProgressEvent, IAgentSessionMetadata, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
import { ISessionDataService } from '../../common/sessionDataService.js';
import { CustomizationStatus, ICustomizationRef, SessionInputResponseKind, type ISessionInputAnswer, type IPendingMessage, type PolicyState } from '../../common/state/sessionState.js';
import { CopilotAgentSession, SessionWrapperFactory } from './copilotAgentSession.js';
Expand All @@ -26,6 +26,7 @@ import { CopilotSessionWrapper } from './copilotSessionWrapper.js';
import { forkCopilotSessionOnDisk, getCopilotDataDir, truncateCopilotSessionOnDisk } from './copilotAgentForking.js';
import { IProtectedResourceMetadata } from '../../common/state/protocol/state.js';
import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
import { projectFromCopilotContext } from './copilotGitProject.js';
import { createShellTools, ShellManager } from './copilotShellTools.js';

/**
Expand Down Expand Up @@ -161,12 +162,16 @@ export class CopilotAgent extends Disposable implements IAgent {
this._logService.info('[Copilot] Listing sessions...');
const client = await this._ensureClient();
const sessions = await client.listSessions();
const result: IAgentSessionMetadata[] = sessions.map(s => ({
session: AgentSession.uri(this.id, s.sessionId),
startTime: s.startTime.getTime(),
modifiedTime: s.modifiedTime.getTime(),
summary: s.summary,
workingDirectory: typeof s.context?.cwd === 'string' ? URI.file(s.context.cwd) : undefined,
const result: IAgentSessionMetadata[] = await Promise.all(sessions.map(async s => {
const project = await projectFromCopilotContext(s.context);
return {
session: AgentSession.uri(this.id, s.sessionId),
startTime: s.startTime.getTime(),
modifiedTime: s.modifiedTime.getTime(),
...(project ? { project } : {}),
summary: s.summary,
workingDirectory: typeof s.context?.cwd === 'string' ? URI.file(s.context.cwd) : undefined,
};
}));
Comment thread
roblourens marked this conversation as resolved.
Resolved
this._logService.info(`[Copilot] Found ${result.length} sessions`);
return result;
Expand All @@ -192,7 +197,7 @@ export class CopilotAgent extends Disposable implements IAgent {
return result;
}

async createSession(config?: IAgentCreateSessionConfig): Promise<URI> {
async createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult> {
this._logService.info(`[Copilot] Creating session... ${config?.model ? `model=${config.model}` : ''}`);
const client = await this._ensureClient();
const parsedPlugins = await this._plugins.getAppliedPlugins();
Expand Down Expand Up @@ -220,7 +225,8 @@ export class CopilotAgent extends Disposable implements IAgent {
const agentSession = await this._resumeSession(newSessionId);
const session = agentSession.sessionUri;
this._logService.info(`[Copilot] Forked session created: ${session.toString()}`);
return session;
const project = await projectFromCopilotContext({ cwd: config.workingDirectory?.fsPath });
return { session, ...(project ? { project } : {}) };
});
}

Expand Down Expand Up @@ -250,7 +256,8 @@ export class CopilotAgent extends Disposable implements IAgent {

const session = agentSession.sessionUri;
this._logService.info(`[Copilot] Session created: ${session.toString()}`);
return session;
const project = await projectFromCopilotContext({ cwd: config?.workingDirectory?.fsPath });
return { session, ...(project ? { project } : {}) };
}

async setClientCustomizations(clientId: string, customizations: ICustomizationRef[], progress?: (results: ISyncedCustomization[]) => void): Promise<ISyncedCustomization[]> {
Expand Down
87 changes: 87 additions & 0 deletions src/vs/platform/agentHost/node/copilot/copilotGitProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as cp from 'child_process';
import { Schemas } from '../../../../base/common/network.js';
import { basename } from '../../../../base/common/path.js';
import { URI } from '../../../../base/common/uri.js';
import type { IAgentSessionProjectInfo } from '../../common/agentService.js';

export interface ICopilotSessionContext {
readonly cwd?: string;
readonly gitRoot?: string;
readonly repository?: string;
}

function execGit(cwd: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
cp.execFile('git', args, { cwd, encoding: 'utf8' }, (error, stdout) => {
if (error) {
reject(error);
return;
}
resolve(stdout.trim());
});
});
}

export async function resolveGitProject(workingDirectory: URI | undefined): Promise<IAgentSessionProjectInfo | undefined> {
if (!workingDirectory || workingDirectory.scheme !== Schemas.file) {
return undefined;
}

const cwd = workingDirectory.fsPath;
try {
if ((await execGit(cwd, ['rev-parse', '--is-inside-work-tree'])) !== 'true') {
return undefined;
}
} catch {
return undefined;
}

let projectPath: string | undefined;
try {
const worktreeList = await execGit(cwd, ['worktree', 'list', '--porcelain']);
projectPath = worktreeList.split(/\r?\n/).find(line => line.startsWith('worktree '))?.substring('worktree '.length);
} catch {
// Fall back to the current worktree root below.
}

if (!projectPath) {
try {
projectPath = await execGit(cwd, ['rev-parse', '--show-toplevel']);
} catch {
return undefined;
}
}

const uri = URI.file(projectPath);
return { uri, displayName: basename(uri.fsPath) || uri.toString() };
}

export function projectFromRepository(repository: string): IAgentSessionProjectInfo | undefined {
const uri = repository.includes('://') ? URI.parse(repository) : URI.parse(`https://github.com/${repository}`);
const rawDisplayName = basename(uri.path) || repository.split('/').filter(Boolean).pop() || repository;
const displayName = rawDisplayName.endsWith('.git') ? rawDisplayName.slice(0, -'.git'.length) : rawDisplayName;
return { uri, displayName };
}

export async function projectFromCopilotContext(context: ICopilotSessionContext | undefined): Promise<IAgentSessionProjectInfo | undefined> {
const workingDirectory = typeof context?.cwd === 'string'
? URI.file(context.cwd)
: typeof context?.gitRoot === 'string'
? URI.file(context.gitRoot)
: undefined;
const gitProject = await resolveGitProject(workingDirectory);
if (gitProject) {
return gitProject;
}

if (context?.repository) {
return projectFromRepository(context.repository);
}

return undefined;
}
1 change: 1 addition & 0 deletions src/vs/platform/agentHost/node/protocolServerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ export class ProtocolServerHandler extends Disposable {
status: s.status ?? SessionStatus.Idle,
createdAt: s.startTime,
modifiedAt: s.modifiedTime,
...(s.project ? { project: { uri: s.project.uri.toString(), displayName: s.project.displayName } } : {}),
workingDirectory: s.workingDirectory?.toString(),
isRead: s.isRead,
isDone: s.isDone,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function makeSessionState(sessionUri: string, overrides?: Partial<ISessionState>
status: SessionStatus.Idle,
createdAt: 1,
modifiedAt: 1,
project: { uri: 'file:///test-project', displayName: 'Test Project' },
},
lifecycle: SessionLifecycle.Ready,
turns: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ suite('AgentHostStateManager', () => {
status: SessionStatus.Idle,
createdAt: Date.now(),
modifiedAt: Date.now(),
project: { uri: 'file:///test-project', displayName: 'Test Project' },
};
}

Expand Down
Loading
Loading