feat(coding): add PI-105 product file host API
This commit is contained in:
171
electron/api/coding-product-services.ts
Normal file
171
electron/api/coding-product-services.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type {
|
||||
CodingProjectFileContent,
|
||||
CodingProjectFileEntry,
|
||||
CodingTextSearchResult,
|
||||
ConversationChangesSnapshot,
|
||||
ProductCodingCommand,
|
||||
ProductCodingSkill,
|
||||
ProductPiCommandInput,
|
||||
} from '../../shared/coding-product-tools';
|
||||
import { createCodingConversationStore } from '../coding-projects/conversation-store';
|
||||
import type { CodingAttachmentStore } from '../coding-projects/attachment-store';
|
||||
import { readCodingProjectConfigV2 } from '../coding-projects/project-config';
|
||||
import { CodingProjectFileService } from '../coding-projects/project-files';
|
||||
import type { PiProductTools } from '../coding-runtime/pi/product-tools';
|
||||
|
||||
export interface ActiveCodingProject {
|
||||
id: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface CodingProductHost {
|
||||
fileStatus(): Promise<CodingProjectFileEntry[]>;
|
||||
findFiles(query: string, limit?: number): Promise<CodingProjectFileEntry[]>;
|
||||
fileContent(path: string): Promise<CodingProjectFileContent>;
|
||||
searchText(pattern: string): Promise<CodingTextSearchResult[]>;
|
||||
listSkills(agentId?: string): Promise<ProductCodingSkill[]>;
|
||||
listCommands(conversationId: string): Promise<ProductCodingCommand[]>;
|
||||
getChanges(conversationId: string): Promise<ConversationChangesSnapshot | null>;
|
||||
}
|
||||
|
||||
export interface CodingProductComposition {
|
||||
attachments: CodingAttachmentStore;
|
||||
productTools: PiProductTools;
|
||||
host: CodingProductHost;
|
||||
}
|
||||
|
||||
export class CodingProductHostError extends Error {
|
||||
constructor(
|
||||
readonly status: 404 | 409,
|
||||
readonly code: string,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodingProductHostOptions {
|
||||
getActiveProject(): Promise<ActiveCodingProject | null>;
|
||||
productTools: PiProductTools;
|
||||
files?: CodingProjectFileService;
|
||||
listPiCommands?(conversationId: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
function normalizePiCommands(value: unknown): ProductPiCommandInput[] {
|
||||
const record = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
const candidates = Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(record?.commands)
|
||||
? record.commands
|
||||
: Array.isArray(record?.data)
|
||||
? record.data
|
||||
: [];
|
||||
return candidates.flatMap((candidate) => {
|
||||
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return [];
|
||||
const command = candidate as Record<string, unknown>;
|
||||
if (typeof command.name !== 'string') return [];
|
||||
return [{
|
||||
name: command.name,
|
||||
...(typeof command.description === 'string' ? { description: command.description } : {}),
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
export function createCodingProductHost(options: CodingProductHostOptions): CodingProductHost {
|
||||
const files = options.files ?? new CodingProjectFileService();
|
||||
|
||||
async function activeProject(): Promise<ActiveCodingProject> {
|
||||
const project = await options.getActiveProject();
|
||||
if (!project) {
|
||||
throw new CodingProductHostError(
|
||||
409,
|
||||
'CODING_ACTIVE_PROJECT_REQUIRED',
|
||||
'No active coding project is selected',
|
||||
);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
async function selectedSkillIds(
|
||||
projectPath: string,
|
||||
agentId?: string,
|
||||
): Promise<readonly string[]> {
|
||||
if (!agentId) return [];
|
||||
const config = await readCodingProjectConfigV2(projectPath);
|
||||
if (config.status !== 'valid') {
|
||||
throw new CodingProductHostError(
|
||||
409,
|
||||
'CODING_PROJECT_CONFIG_INVALID',
|
||||
'Coding project configuration is unavailable',
|
||||
);
|
||||
}
|
||||
const agent = config.config.agents.find((candidate) => (
|
||||
candidate.id === agentId && candidate.enabled && !candidate.archivedAt
|
||||
));
|
||||
if (!agent) {
|
||||
throw new CodingProductHostError(
|
||||
404,
|
||||
'CODING_AGENT_NOT_FOUND',
|
||||
'Coding project Agent does not exist',
|
||||
);
|
||||
}
|
||||
return agent.skillIds;
|
||||
}
|
||||
|
||||
async function conversationContext(conversationId: string): Promise<{
|
||||
project: ActiveCodingProject;
|
||||
skillIds: readonly string[];
|
||||
}> {
|
||||
const project = await activeProject();
|
||||
const conversation = await createCodingConversationStore(project.path).get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new CodingProductHostError(
|
||||
404,
|
||||
'CODING_CONVERSATION_NOT_FOUND',
|
||||
'Coding Conversation does not exist',
|
||||
);
|
||||
}
|
||||
return {
|
||||
project,
|
||||
skillIds: await selectedSkillIds(project.path, conversation.agentId),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
async fileStatus() {
|
||||
const project = await activeProject();
|
||||
return await files.status(project.path);
|
||||
},
|
||||
async findFiles(query, limit) {
|
||||
const project = await activeProject();
|
||||
return await files.find(project.path, query, limit);
|
||||
},
|
||||
async fileContent(filePath) {
|
||||
const project = await activeProject();
|
||||
return await files.content(project.path, filePath);
|
||||
},
|
||||
async searchText(pattern) {
|
||||
const project = await activeProject();
|
||||
return await files.search(project.path, pattern);
|
||||
},
|
||||
async listSkills(agentId) {
|
||||
const project = await activeProject();
|
||||
return await options.productTools.listSkills(
|
||||
await selectedSkillIds(project.path, agentId),
|
||||
);
|
||||
},
|
||||
async listCommands(conversationId) {
|
||||
const context = await conversationContext(conversationId);
|
||||
const piCommands = options.listPiCommands
|
||||
? normalizePiCommands(await options.listPiCommands(conversationId))
|
||||
: [];
|
||||
return await options.productTools.listCommands(context.skillIds, piCommands);
|
||||
},
|
||||
async getChanges(conversationId) {
|
||||
await conversationContext(conversationId);
|
||||
return options.productTools.getChanges(conversationId);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user