Skip to content

Commit a830ffa

Browse files
authored
Allow workspace specific custom agents (#15457)
fixed #15456
1 parent 0b3f538 commit a830ffa

File tree

3 files changed

+118
-30
lines changed

3 files changed

+118
-30
lines changed

packages/ai-core/src/browser/frontend-prompt-customization-service.ts

+72-25
Original file line numberDiff line numberDiff line change
@@ -519,46 +519,93 @@ export class FrontendPromptCustomizationServiceImpl implements PromptCustomizati
519519
}
520520

521521
async getCustomAgents(): Promise<CustomAgentDescription[]> {
522-
const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml');
522+
const agentsById = new Map<string, CustomAgentDescription>();
523+
// First, process additional (workspace) template directories to give them precedence
524+
for (const dirPath of this.additionalTemplateDirs) {
525+
const dirURI = URI.fromFilePath(dirPath);
526+
await this.loadCustomAgentsFromDirectory(dirURI, agentsById);
527+
}
528+
// Then process global template directory (only adding agents that don't conflict)
529+
const globalTemplateDir = await this.getTemplatesDirectoryURI();
530+
await this.loadCustomAgentsFromDirectory(globalTemplateDir, agentsById);
531+
// Return the merged list of agents
532+
return Array.from(agentsById.values());
533+
}
534+
535+
/**
536+
* Load custom agents from a specific directory
537+
* @param directoryURI The URI of the directory to load from
538+
* @param agentsById Map to store the loaded agents by ID
539+
*/
540+
protected async loadCustomAgentsFromDirectory(
541+
directoryURI: URI,
542+
agentsById: Map<string, CustomAgentDescription>
543+
): Promise<void> {
544+
const customAgentYamlUri = directoryURI.resolve('customAgents.yml');
523545
const yamlExists = await this.fileService.exists(customAgentYamlUri);
524546
if (!yamlExists) {
525-
return [];
547+
return;
526548
}
527-
const fileContent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' });
549+
528550
try {
551+
const fileContent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' });
529552
const doc = load(fileContent.value);
553+
530554
if (!Array.isArray(doc) || !doc.every(entry => CustomAgentDescription.is(entry))) {
531-
console.debug('Invalid customAgents.yml file content');
532-
return [];
555+
console.debug(`Invalid customAgents.yml file content in ${directoryURI.toString()}`);
556+
return;
533557
}
558+
534559
const readAgents = doc as CustomAgentDescription[];
535-
// make sure all agents are unique (id and name)
536-
const uniqueAgentIds = new Set<string>();
537-
const uniqueAgents: CustomAgentDescription[] = [];
538-
readAgents.forEach(agent => {
539-
if (uniqueAgentIds.has(agent.id)) {
540-
return;
560+
561+
// Add agents to the map if they don't already exist
562+
for (const agent of readAgents) {
563+
if (!agentsById.has(agent.id)) {
564+
agentsById.set(agent.id, agent);
541565
}
542-
uniqueAgentIds.add(agent.id);
543-
uniqueAgents.push(agent);
544-
});
545-
return uniqueAgents;
566+
}
546567
} catch (e) {
547-
console.debug(e.message, e);
548-
return [];
568+
console.debug(`Error loading customAgents.yml from ${directoryURI.toString()}: ${e.message}`, e);
549569
}
550570
}
551571

552-
async openCustomAgentYaml(): Promise<void> {
553-
const customAgentYamlUri = (await this.getTemplatesDirectoryURI()).resolve('customAgents.yml');
572+
/**
573+
* Returns all locations of existing customAgents.yml files and potential locations where
574+
* new customAgents.yml files could be created.
575+
*
576+
* @returns An array of objects containing the URI and whether the file exists
577+
*/
578+
async getCustomAgentsLocations(): Promise<{ uri: URI, exists: boolean }[]> {
579+
const locations: { uri: URI, exists: boolean }[] = [];
580+
// Check global template directory
581+
const globalTemplateDir = await this.getTemplatesDirectoryURI();
582+
const globalAgentsUri = globalTemplateDir.resolve('customAgents.yml');
583+
const globalExists = await this.fileService.exists(globalAgentsUri);
584+
locations.push({ uri: globalAgentsUri, exists: globalExists });
585+
// Check additional (workspace) template directories
586+
for (const dirPath of this.additionalTemplateDirs) {
587+
const dirURI = URI.fromFilePath(dirPath);
588+
const agentsUri = dirURI.resolve('customAgents.yml');
589+
const exists = await this.fileService.exists(agentsUri);
590+
locations.push({ uri: agentsUri, exists: exists });
591+
}
592+
return locations;
593+
}
594+
595+
/**
596+
* Opens an existing customAgents.yml file at the given URI, or creates a new one if it doesn't exist.
597+
*
598+
* @param uri The URI of the customAgents.yml file to open or create
599+
*/
600+
async openCustomAgentYaml(uri: URI): Promise<void> {
554601
const content = dump([templateEntry]);
555-
if (! await this.fileService.exists(customAgentYamlUri)) {
556-
await this.fileService.createFile(customAgentYamlUri, BinaryBuffer.fromString(content));
602+
if (! await this.fileService.exists(uri)) {
603+
await this.fileService.createFile(uri, BinaryBuffer.fromString(content));
557604
} else {
558-
const fileContent = (await this.fileService.readFile(customAgentYamlUri)).value;
559-
await this.fileService.writeFile(customAgentYamlUri, BinaryBuffer.concat([fileContent, BinaryBuffer.fromString(content)]));
605+
const fileContent = (await this.fileService.readFile(uri)).value;
606+
await this.fileService.writeFile(uri, BinaryBuffer.concat([fileContent, BinaryBuffer.fromString(content)]));
560607
}
561-
const openHandler = await this.openerService.getOpener(customAgentYamlUri);
562-
openHandler.open(customAgentYamlUri);
608+
const openHandler = await this.openerService.getOpener(uri);
609+
openHandler.open(uri);
563610
}
564611
}

packages/ai-core/src/common/prompt-service.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,19 @@ export interface PromptCustomizationService {
202202
readonly onDidChangeCustomAgents: Event<void>;
203203

204204
/**
205-
* Open the custom agent yaml file.
205+
* Returns all locations of existing customAgents.yml files and potential locations where
206+
* new customAgents.yml files could be created.
207+
*
208+
* @returns An array of objects containing the URI and whether the file exists
209+
*/
210+
getCustomAgentsLocations(): Promise<{ uri: URI, exists: boolean }[]>;
211+
212+
/**
213+
* Opens an existing customAgents.yml file at the given URI, or creates a new one if it doesn't exist.
214+
*
215+
* @param uri The URI of the customAgents.yml file to open or create
206216
*/
207-
openCustomAgentYaml(): void;
217+
openCustomAgentYaml(uri: URI): Promise<void>;
208218
}
209219

210220
@injectable()

packages/ai-ide/src/browser/ai-configuration/agent-configuration-widget.tsx

+34-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import {
2626
PromptCustomizationService,
2727
PromptService,
2828
} from '@theia/ai-core/lib/common';
29-
import { codicon, ReactWidget } from '@theia/core/lib/browser';
29+
import { codicon, QuickInputService, ReactWidget } from '@theia/core/lib/browser';
30+
import { URI } from '@theia/core/lib/common';
3031
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
3132
import * as React from '@theia/core/shared/react';
3233
import { AIConfigurationSelectionService } from './ai-configuration-service';
@@ -68,6 +69,9 @@ export class AIAgentConfigurationWidget extends ReactWidget {
6869
@inject(PromptService)
6970
protected promptService: PromptService;
7071

72+
@inject(QuickInputService)
73+
protected readonly quickInputService: QuickInputService;
74+
7175
protected languageModels: LanguageModel[] | undefined;
7276

7377
@postConstruct()
@@ -237,8 +241,35 @@ export class AIAgentConfigurationWidget extends ReactWidget {
237241
this.aiConfigurationSelectionService.selectConfigurationTab(AIVariableConfigurationWidget.ID);
238242
}
239243

240-
protected addCustomAgent(): void {
241-
this.promptCustomizationService.openCustomAgentYaml();
244+
protected async addCustomAgent(): Promise<void> {
245+
const locations = await this.promptCustomizationService.getCustomAgentsLocations();
246+
247+
// If only one location is available, use the direct approach
248+
if (locations.length === 1) {
249+
this.promptCustomizationService.openCustomAgentYaml(locations[0].uri);
250+
return;
251+
}
252+
253+
// Multiple locations - show quick picker
254+
const quickPick = this.quickInputService.createQuickPick();
255+
quickPick.title = 'Select Location for Custom Agents File';
256+
quickPick.placeholder = 'Choose where to create or open a custom agents file';
257+
258+
quickPick.items = locations.map(location => ({
259+
label: location.uri.path.toString(),
260+
description: location.exists ? 'Open existing file' : 'Create new file',
261+
location
262+
}));
263+
264+
quickPick.onDidAccept(async () => {
265+
const selectedItem = quickPick.selectedItems[0] as unknown as { location: { uri: URI, exists: boolean } };
266+
if (selectedItem && selectedItem.location) {
267+
quickPick.dispose();
268+
this.promptCustomizationService.openCustomAgentYaml(selectedItem.location.uri);
269+
}
270+
});
271+
272+
quickPick.show();
242273
}
243274

244275
protected setActiveAgent(agent: Agent): void {

0 commit comments

Comments
 (0)