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
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
1f72258
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
107 changes: 90 additions & 17 deletions src/vs/platform/agentHost/node/copilot/copilotAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { CopilotClient } from '@github/copilot-sdk';
import { rgPath } from '@vscode/ripgrep';
import { SequencerByKey } from '../../../../base/common/async.js';
import { Limiter, SequencerByKey } from '../../../../base/common/async.js';
import { Emitter } from '../../../../base/common/event.js';
import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';
import { FileAccess } from '../../../../base/common/network.js';
Expand All @@ -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, IAgentSessionProjectInfo, 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 { ICopilotSessionContext, projectFromCopilotContext } from './copilotGitProject.js';
import { createShellTools, ShellManager } from './copilotShellTools.js';

/**
Expand Down Expand Up @@ -161,12 +162,23 @@ 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 projectLimiter = new Limiter<IAgentSessionProjectInfo | undefined>(4);
const projectByContext = new Map<string, Promise<IAgentSessionProjectInfo | undefined>>();
const result: IAgentSessionMetadata[] = await Promise.all(sessions.map(async s => {
const session = AgentSession.uri(this.id, s.sessionId);
let { project, resolved } = await this._readSessionProject(session);
if (!resolved) {
project = await this._resolveSessionProject(s.context, projectLimiter, projectByContext);
this._storeSessionProjectResolution(session, project);
}
return {
session,
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.
this._logService.info(`[Copilot] Found ${result.length} sessions`);
return result;
Expand All @@ -192,7 +204,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 +232,9 @@ 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 });
this._storeSessionMetadata(session, undefined, config.workingDirectory, project, true);
return { session, ...(project ? { project } : {}) };
});
}

Expand All @@ -244,13 +258,13 @@ export class CopilotAgent extends Disposable implements IAgent {
this._plugins.setAppliedPlugins(agentSession, parsedPlugins);
await agentSession.initializeSession();

// Persist model & working directory so we can recreate the session
// if the SDK loses it (e.g. sessions without messages).
this._storeSessionMetadata(agentSession.sessionUri, config?.model, config?.workingDirectory);

const session = agentSession.sessionUri;
this._logService.info(`[Copilot] Session created: ${session.toString()}`);
return session;
const project = await projectFromCopilotContext({ cwd: config?.workingDirectory?.fsPath });
// Persist model, working directory, and project so we can recreate the
// session if the SDK loses it and avoid rediscovering git metadata.
this._storeSessionMetadata(agentSession.sessionUri, config?.model, config?.workingDirectory, project, true);
return { session, ...(project ? { project } : {}) };
}

async setClientCustomizations(clientId: string, customizations: ICustomizationRef[], progress?: (results: ISyncedCustomization[]) => void): Promise<ISyncedCustomization[]> {
Expand Down Expand Up @@ -365,7 +379,7 @@ export class CopilotAgent extends Disposable implements IAgent {
if (entry) {
await entry.setModel(model);
}
this._storeSessionMetadata(session, model, undefined);
this._storeSessionMetadata(session, model, undefined, undefined);
}

async shutdown(): Promise<void> {
Expand Down Expand Up @@ -496,8 +510,11 @@ export class CopilotAgent extends Disposable implements IAgent {

private static readonly _META_MODEL = 'copilot.model';
private static readonly _META_CWD = 'copilot.workingDirectory';
private static readonly _META_PROJECT_RESOLVED = 'copilot.project.resolved';
private static readonly _META_PROJECT_URI = 'copilot.project.uri';
private static readonly _META_PROJECT_DISPLAY_NAME = 'copilot.project.displayName';

private _storeSessionMetadata(session: URI, model: string | undefined, workingDirectory: URI | undefined): void {
private _storeSessionMetadata(session: URI, model: string | undefined, workingDirectory: URI | undefined, project: IAgentSessionProjectInfo | undefined, projectResolved = project !== undefined): void {
const dbRef = this._sessionDataService.tryOpenDatabase(session);
dbRef?.then(ref => {
if (!ref) {
Expand All @@ -511,6 +528,13 @@ export class CopilotAgent extends Disposable implements IAgent {
if (workingDirectory) {
work.push(db.setMetadata(CopilotAgent._META_CWD, workingDirectory.toString()));
}
if (projectResolved) {
work.push(db.setMetadata(CopilotAgent._META_PROJECT_RESOLVED, 'true'));
}
if (project) {
work.push(db.setMetadata(CopilotAgent._META_PROJECT_URI, project.uri.toString()));
work.push(db.setMetadata(CopilotAgent._META_PROJECT_DISPLAY_NAME, project.displayName));
}
Promise.all(work).finally(() => ref.dispose());
});
}
Expand All @@ -534,6 +558,55 @@ export class CopilotAgent extends Disposable implements IAgent {
}
}

private async _readSessionProject(session: URI): Promise<{ project?: IAgentSessionProjectInfo; resolved: boolean }> {
const ref = await this._sessionDataService.tryOpenDatabase(session);
if (!ref) {
return { resolved: false };
}
try {
const [resolved, uri, displayName] = await Promise.all([
ref.object.getMetadata(CopilotAgent._META_PROJECT_RESOLVED),
ref.object.getMetadata(CopilotAgent._META_PROJECT_URI),
ref.object.getMetadata(CopilotAgent._META_PROJECT_DISPLAY_NAME),
]);
const project = uri && displayName ? { uri: URI.parse(uri), displayName } : undefined;
return { project, resolved: resolved === 'true' || project !== undefined };
} finally {
ref.dispose();
}
}

private _storeSessionProjectResolution(session: URI, project: IAgentSessionProjectInfo | undefined): void {
this._storeSessionMetadata(session, undefined, undefined, project, true);
}

private _resolveSessionProject(context: ICopilotSessionContext | undefined, limiter: Limiter<IAgentSessionProjectInfo | undefined>, projectByContext: Map<string, Promise<IAgentSessionProjectInfo | undefined>>): Promise<IAgentSessionProjectInfo | undefined> {
const key = this._projectContextKey(context);
if (!key) {
return Promise.resolve(undefined);
}

let project = projectByContext.get(key);
if (!project) {
project = limiter.queue(() => projectFromCopilotContext(context));
projectByContext.set(key, project);
}
return project;
}

private _projectContextKey(context: ICopilotSessionContext | undefined): string | undefined {
if (context?.cwd) {
return `cwd:${context.cwd}`;
}
if (context?.gitRoot) {
return `gitRoot:${context.gitRoot}`;
}
if (context?.repository) {
return `repository:${context.repository}`;
}
return undefined;
}

override dispose(): void {
this._client?.stop().catch(() => { /* best-effort */ });
super.dispose();
Expand Down
Loading
Loading