Skip to content

Commit addcf47

Browse files
committed
wip
1 parent 302ffe6 commit addcf47

File tree

5 files changed

+263
-17
lines changed

5 files changed

+263
-17
lines changed

product.json

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,13 @@
116116
},
117117
"providerUriSetting": "github-enterprise.uri",
118118
"providerScopes": [
119-
[
120-
"user:email"
121-
],
122-
[
123-
"read:user"
124-
],
125-
[
126-
"read:user",
127-
"user:email",
128-
"repo",
129-
"workflow"
130-
]
119+
["user:email"],
120+
["read:user"],
121+
["read:user", "user:email", "repo", "workflow"]
131122
],
132123
"entitlementUrl": "https://api.github.com/copilot_internal/user",
133124
"entitlementSignupLimitedUrl": "https://api.github.com/copilot_internal/subscribe_limited_user",
125+
"customAgentsUrl": "https://api.githubcopilot.com/agents/swe/custom-agents",
134126
"chatQuotaExceededContext": "github.copilot.chat.quotaExceeded",
135127
"completionsQuotaExceededContext": "github.copilot.completions.quotaExceeded",
136128
"walkthroughCommand": "github.copilot.open.walkthrough",

src/vs/base/common/product.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ export interface IDefaultChatAgent {
371371

372372
readonly entitlementUrl: string;
373373
readonly entitlementSignupLimitedUrl: string;
374+
readonly customAgentsUrl?: string;
374375

375376
readonly chatQuotaExceededContext: string;
376377
readonly completionsQuotaExceededContext: string;

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { IChatEditingService } from '../common/chatEditingService.js';
3838
import { IChatLayoutService } from '../common/chatLayoutService.js';
3939
import { ChatModeService, IChatModeService } from '../common/chatModes.js';
4040
import { ChatResponseResourceFileSystemProvider } from '../common/chatResponseResourceFileSystemProvider.js';
41+
import { CustomAgentsService, ICustomAgentsService } from '../common/customAgents.js';
4142
import { IChatService } from '../common/chatService.js';
4243
import { ChatService } from '../common/chatServiceImpl.js';
4344
import { IChatSessionsService } from '../common/chatSessionsService.js';
@@ -1078,6 +1079,7 @@ registerSingleton(IChatMarkdownAnchorService, ChatMarkdownAnchorService, Instant
10781079
registerSingleton(ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService, InstantiationType.Delayed);
10791080
registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed);
10801081
registerSingleton(IChatContextPickService, ChatContextPickService, InstantiationType.Delayed);
1082+
registerSingleton(ICustomAgentsService, CustomAgentsService, InstantiationType.Delayed);
10811083
registerSingleton(IChatModeService, ChatModeService, InstantiationType.Delayed);
10821084
registerSingleton(IChatAttachmentResolveService, ChatAttachmentResolveService, InstantiationType.Delayed);
10831085
registerSingleton(IChatTodoListService, ChatTodoListService, InstantiationType.Delayed);

src/vs/workbench/contrib/chat/common/chatModes.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
1818
import { IChatAgentService } from './chatAgents.js';
1919
import { ChatContextKeys } from './chatContextKeys.js';
2020
import { ChatModeKind } from './constants.js';
21+
import { ICustomAgentData, ICustomAgentsService } from './customAgents.js';
2122
import { IHandOff } from './promptSyntax/promptFileParser.js';
2223
import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js';
2324

@@ -39,6 +40,7 @@ export class ChatModeService extends Disposable implements IChatModeService {
3940

4041
private readonly hasCustomModes: IContextKey<boolean>;
4142
private readonly _customModeInstances = new Map<string, CustomChatMode>();
43+
private readonly _apiCustomModeInstances = new Map<string, CustomChatMode>();
4244

4345
private readonly _onDidChangeChatModes = new Emitter<void>();
4446
public readonly onDidChangeChatModes = this._onDidChangeChatModes.event;
@@ -48,7 +50,8 @@ export class ChatModeService extends Disposable implements IChatModeService {
4850
@IChatAgentService private readonly chatAgentService: IChatAgentService,
4951
@IContextKeyService contextKeyService: IContextKeyService,
5052
@ILogService private readonly logService: ILogService,
51-
@IStorageService private readonly storageService: IStorageService
53+
@IStorageService private readonly storageService: IStorageService,
54+
@ICustomAgentsService private readonly customAgentsService: ICustomAgentsService
5255
) {
5356
super();
5457

@@ -63,6 +66,9 @@ export class ChatModeService extends Disposable implements IChatModeService {
6366
}));
6467
this._register(this.storageService.onWillSaveState(() => this.saveCachedModes()));
6568

69+
// Fetch custom agents from API
70+
void this.refreshApiCustomAgents(true);
71+
6672
// Ideally we can get rid of the setting to disable agent mode?
6773
let didHaveToolsAgent = this.chatAgentService.hasToolsAgent;
6874
this._register(this.chatAgentService.onDidChangeAgents(() => {
@@ -155,11 +161,90 @@ export class ChatModeService extends Disposable implements IChatModeService {
155161
}
156162
}
157163

158-
this.hasCustomModes.set(this._customModeInstances.size > 0);
164+
this.hasCustomModes.set(this._customModeInstances.size > 0 || this._apiCustomModeInstances.size > 0);
159165
} catch (error) {
160166
this.logService.error(error, 'Failed to load custom agents');
161167
this._customModeInstances.clear();
162-
this.hasCustomModes.set(false);
168+
this.hasCustomModes.set(this._apiCustomModeInstances.size > 0);
169+
}
170+
if (fireChangeEvent) {
171+
this._onDidChangeChatModes.fire();
172+
}
173+
}
174+
175+
private async refreshApiCustomAgents(fireChangeEvent?: boolean): Promise<void> {
176+
try {
177+
const apiAgents = await this.customAgentsService.fetchCustomAgents({
178+
exclude_invalid_config: true,
179+
dedupe: true
180+
}, CancellationToken.None);
181+
182+
// Convert API agents to custom modes
183+
const seenIds = new Set<string>();
184+
185+
// Add a test entry to verify the dropdown is working
186+
const testAgent: ICustomAgent = {
187+
uri: URI.parse('api:test/test/test-agent'),
188+
name: 'test-agent',
189+
description: 'Test agent from API',
190+
tools: ['*'],
191+
model: undefined,
192+
argumentHint: undefined,
193+
agentInstructions: {
194+
content: 'This is a test agent to verify API integration is working',
195+
toolReferences: []
196+
},
197+
handOffs: undefined,
198+
target: undefined,
199+
source: { storage: PromptsStorage.local }
200+
};
201+
const testModeInstance = new CustomChatMode(testAgent);
202+
this._apiCustomModeInstances.set('api:test/test/test-agent', testModeInstance);
203+
seenIds.add('api:test/test/test-agent');
204+
205+
for (const apiAgent of apiAgents) {
206+
const agentId = `api:${apiAgent.repo_owner}/${apiAgent.repo_name}/${apiAgent.name}`;
207+
seenIds.add(agentId);
208+
209+
// Convert API agent to ICustomAgent format
210+
const customAgent: ICustomAgent = {
211+
uri: URI.parse(agentId),
212+
name: apiAgent.name,
213+
description: apiAgent.description,
214+
tools: apiAgent.tools,
215+
model: undefined,
216+
argumentHint: apiAgent.argument_hint,
217+
agentInstructions: {
218+
content: apiAgent.description,
219+
toolReferences: [],
220+
metadata: apiAgent.metadata
221+
},
222+
handOffs: undefined,
223+
target: apiAgent.target,
224+
source: { storage: PromptsStorage.local }
225+
};
226+
227+
let modeInstance = this._apiCustomModeInstances.get(agentId);
228+
if (modeInstance) {
229+
modeInstance.updateData(customAgent);
230+
} else {
231+
modeInstance = new CustomChatMode(customAgent);
232+
this._apiCustomModeInstances.set(agentId, modeInstance);
233+
}
234+
}
235+
236+
// Clean up instances for modes that no longer exist
237+
for (const [agentId] of this._apiCustomModeInstances.entries()) {
238+
if (!seenIds.has(agentId)) {
239+
this._apiCustomModeInstances.delete(agentId);
240+
}
241+
}
242+
243+
this.hasCustomModes.set(this._customModeInstances.size > 0 || this._apiCustomModeInstances.size > 0);
244+
} catch (error) {
245+
this.logService.error(error, 'Failed to load API custom agents');
246+
this._apiCustomModeInstances.clear();
247+
this.hasCustomModes.set(this._customModeInstances.size > 0);
163248
}
164249
if (fireChangeEvent) {
165250
this._onDidChangeChatModes.fire();
@@ -174,7 +259,9 @@ export class ChatModeService extends Disposable implements IChatModeService {
174259
}
175260

176261
findModeById(id: string | ChatModeKind): IChatMode | undefined {
177-
return this.getBuiltinModes().find(mode => mode.id === id) ?? this._customModeInstances.get(id);
262+
return this.getBuiltinModes().find(mode => mode.id === id) ??
263+
this._customModeInstances.get(id) ??
264+
this._apiCustomModeInstances.get(id);
178265
}
179266

180267
findModeByName(name: string): IChatMode | undefined {
@@ -194,7 +281,14 @@ export class ChatModeService extends Disposable implements IChatModeService {
194281
}
195282

196283
private getCustomModes(): IChatMode[] {
197-
return this.chatAgentService.hasToolsAgent ? Array.from(this._customModeInstances.values()) : [];
284+
if (!this.chatAgentService.hasToolsAgent) {
285+
return [];
286+
}
287+
// Combine both local file-based custom modes and API-fetched custom agents
288+
return [
289+
...Array.from(this._customModeInstances.values()),
290+
...Array.from(this._apiCustomModeInstances.values())
291+
];
198292
}
199293
}
200294

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { CancellationToken } from '../../../../base/common/cancellation.js';
7+
import { Disposable } from '../../../../base/common/lifecycle.js';
8+
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
9+
import { IRequestService, asJson } from '../../../../platform/request/common/request.js';
10+
import { ILogService } from '../../../../platform/log/common/log.js';
11+
import { IProductService } from '../../../../platform/product/common/productService.js';
12+
import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';
13+
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
14+
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
15+
16+
export interface ICustomAgentData {
17+
readonly name: string;
18+
readonly repo_owner_id: number;
19+
readonly repo_owner: string;
20+
readonly repo_id: number;
21+
readonly repo_name: string;
22+
readonly display_name: string;
23+
readonly description: string;
24+
readonly tools: string[];
25+
readonly argument_hint?: string;
26+
readonly metadata?: Record<string, string | number>;
27+
readonly version: string;
28+
readonly 'mcp-servers'?: Record<string, unknown>;
29+
readonly target?: string;
30+
readonly config_error?: string;
31+
}
32+
33+
export interface ICustomAgentsResponse {
34+
readonly agents: ICustomAgentData[];
35+
}
36+
37+
export interface ICustomAgentsQueryOptions {
38+
readonly target?: 'github-copilot' | 'vscode';
39+
readonly exclude_invalid_config?: boolean;
40+
readonly dedupe?: boolean;
41+
readonly include_sources?: string;
42+
}
43+
44+
export const ICustomAgentsService = createDecorator<ICustomAgentsService>('customAgentsService');
45+
46+
export interface ICustomAgentsService {
47+
readonly _serviceBrand: undefined;
48+
49+
/**
50+
* Fetch custom agents for the current repository
51+
*/
52+
fetchCustomAgents(options?: ICustomAgentsQueryOptions, token?: CancellationToken): Promise<ICustomAgentData[]>;
53+
54+
/**
55+
* Fetch custom agents for a specific repository
56+
*/
57+
fetchCustomAgentsForRepo(repoOwner: string, repoName: string, options?: ICustomAgentsQueryOptions, token?: CancellationToken): Promise<ICustomAgentData[]>;
58+
}
59+
60+
export class CustomAgentsService extends Disposable implements ICustomAgentsService {
61+
declare readonly _serviceBrand: undefined;
62+
63+
private readonly customAgentsBaseUrl: string;
64+
65+
constructor(
66+
@IRequestService private readonly requestService: IRequestService,
67+
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
68+
@IProductService private readonly productService: IProductService,
69+
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
70+
@ILogService private readonly logService: ILogService,
71+
@IExtensionService private readonly extensionService: IExtensionService
72+
) {
73+
super();
74+
// Use the custom agents API URL from product configuration
75+
this.customAgentsBaseUrl = this.productService.defaultChatAgent?.customAgentsUrl ?? 'https://api.githubcopilot.com/agents/swe/custom-agents';
76+
}
77+
78+
async fetchCustomAgents(options?: ICustomAgentsQueryOptions, token: CancellationToken = CancellationToken.None): Promise<ICustomAgentData[]> {
79+
// Try to detect the repository from the workspace
80+
const repoInfo = await this.getRepositoryInfo();
81+
if (!repoInfo) {
82+
this.logService.warn('CustomAgentsService: No repository information found in workspace');
83+
return [];
84+
}
85+
86+
return this.fetchCustomAgentsForRepo(repoInfo.owner, repoInfo.name, options, token);
87+
}
88+
89+
async fetchCustomAgentsForRepo(repoOwner: string, repoName: string, options?: ICustomAgentsQueryOptions, token: CancellationToken = CancellationToken.None): Promise<ICustomAgentData[]> {
90+
try {
91+
// Get GitHub authentication session
92+
const sessions = await this.authenticationService.getSessions('github');
93+
if (!sessions || sessions.length === 0) {
94+
this.logService.warn('CustomAgentsService: No GitHub authentication session found');
95+
return [];
96+
}
97+
98+
const accessToken = sessions[0].accessToken;
99+
100+
// Build query parameters
101+
const queryParams = new URLSearchParams();
102+
if (options?.target) {
103+
queryParams.append('target', options.target);
104+
}
105+
if (options?.exclude_invalid_config !== undefined) {
106+
queryParams.append('exclude_invalid_config', String(options.exclude_invalid_config));
107+
}
108+
if (options?.dedupe !== undefined) {
109+
queryParams.append('dedupe', String(options.dedupe));
110+
}
111+
if (options?.include_sources) {
112+
queryParams.append('include_sources', options.include_sources);
113+
}
114+
115+
const url = `${this.customAgentsBaseUrl}/${repoOwner}/${repoName}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
116+
117+
this.logService.debug('CustomAgentsService: Fetching custom agents from', url);
118+
119+
const response = await this.requestService.request({
120+
type: 'GET',
121+
url,
122+
headers: {
123+
'Authorization': `Bearer ${accessToken}`,
124+
'Accept': 'application/json'
125+
}
126+
}, token);
127+
128+
if (response.res.statusCode !== 200) {
129+
this.logService.error('CustomAgentsService: Failed to fetch custom agents', response.res.statusCode);
130+
return [];
131+
}
132+
133+
const result = await asJson<ICustomAgentsResponse>(response);
134+
if (!result || !result.agents) {
135+
this.logService.warn('CustomAgentsService: Invalid response format');
136+
return [];
137+
}
138+
139+
this.logService.info(`CustomAgentsService: Fetched ${result.agents.length} custom agents`);
140+
return result.agents;
141+
} catch (error) {
142+
this.logService.error('CustomAgentsService: Error fetching custom agents', error);
143+
return [];
144+
}
145+
}
146+
147+
private async getRepositoryInfo(): Promise<{ owner: string; name: string } | undefined> {
148+
try {
149+
// For now, hardcode vscode repo for testing
150+
// TODO: Properly implement git extension API access
151+
return { owner: 'microsoft', name: 'vscode' };
152+
} catch (error) {
153+
this.logService.error('CustomAgentsService: Error getting repository info', error);
154+
return undefined;
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)