/** * agent-editor.ts — Inline agent editing interface. * * Provides a menu of editable fields for custom/project agents, * delegating to model-picker, tool-picker, and ctx.ui.* primitives. */ import { readFileSync, writeFileSync } from "node:fs"; import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent"; import { getAgentConfig } from "../agent-types.js"; import { loadCustomAgents } from "../custom-agents.js"; import { showModelPicker } from "./model-picker.js"; import { showToolPicker } from "./tool-picker.js"; /** * Show the agent editor for a given agent name. * The agent must have an editable .md file. */ export async function showAgentEditor( ctx: ExtensionCommandContext, name: string, ): Promise { const cfg = getAgentConfig(name); if (!cfg) { ctx.ui.notify(`Agent config not found for "${name}".`, "warning"); return; } const choice = await ctx.ui.select(`Edit: ${name}`, [ "Open in editor", "Edit tools", "Edit model", "Edit description", "Edit system prompt", "Back", ]); if (!choice || choice === "Back") return; switch (choice) { case "Open in editor": await editInEditor(ctx, name); break; case "Edit tools": await editTools(ctx, name, cfg); break; case "Edit model": await editModel(ctx, name, cfg); break; case "Edit description": await editDescription(ctx, name, cfg); break; case "Edit system prompt": await editSystemPrompt(ctx, name, cfg); break; } } /** * Reload custom agents (re-registers from disk). */ function reloadAgents(): void { const { registerAgents } = require("../agent-types.js"); const agents = loadCustomAgents(process.cwd()); registerAgents(agents); } /** * Find the file path for an agent. */ function findAgentFile(name: string): string | undefined { const { existsSync } = require("node:fs"); const { join } = require("node:path"); const { getAgentDir } = require("@mariozechner/pi-coding-agent"); const projectPath = join(process.cwd(), ".pi", "agents", `${name}.md`); if (existsSync(projectPath)) return projectPath; const personalPath = join(getAgentDir(), "agents", `${name}.md`); if (existsSync(personalPath)) return personalPath; return undefined; } /** * Open the entire .md file in ctx.ui.editor(). */ async function editInEditor(ctx: ExtensionCommandContext, name: string): Promise { const filePath = findAgentFile(name); if (!filePath) { ctx.ui.notify(`No .md file found for ${name}`, "warning"); return; } const content = readFileSync(filePath, "utf-8"); const edited = await ctx.ui.editor(`Edit ${name}`, content); if (edited !== undefined && edited !== content) { writeFileSync(filePath, edited, "utf-8"); reloadAgents(); ctx.ui.notify(`Updated ${name}`, "info"); } } /** * Edit the tools field using the tool picker. */ async function editTools(ctx: ExtensionCommandContext, name: string, cfg: import("../types.js").AgentConfig): Promise { const filePath = findAgentFile(name); if (!filePath) { ctx.ui.notify(`No .md file found for ${name}`, "warning"); return; } const currentTools = cfg.builtinToolNames; const newTools = await showToolPicker(ctx, currentTools); if (newTools === null) return; // Update the frontmatter tools field const content = readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n"); const toolsLine = newTools === undefined ? "tools: all" : `tools: ${newTools.join(", ")}`; // Find frontmatter boundaries and operate only within them const fmEnd = content.indexOf("\n---\n", 4); if (content.startsWith("---\n") && fmEnd !== -1) { const frontmatter = content.slice(0, fmEnd + 5); const body = content.slice(fmEnd + 5); const updatedFm = /^tools:.*$/m.test(frontmatter) ? frontmatter.replace(/^tools:.*$/m, toolsLine) : frontmatter.replace(/^---\n/, `---\n${toolsLine}\n`); writeFileSync(filePath, updatedFm + (body ? `\n${body}` : ""), "utf-8"); } else { // No frontmatter — create one writeFileSync(filePath, `---\n${toolsLine}\n---\n\n${content}`, "utf-8"); } reloadAgents(); ctx.ui.notify(`Tools updated for ${name}`, "info"); } /** * Edit the model field using the model picker. */ async function editModel(ctx: ExtensionCommandContext, name: string, cfg: import("../types.js").AgentConfig): Promise { const filePath = findAgentFile(name); if (!filePath) { ctx.ui.notify(`No .md file found for ${name}`, "warning"); return; } const newModel = await showModelPicker(ctx, cfg.model); if (!newModel) return; const content = readFileSync(filePath, "utf-8"); if (newModel === "inherit") { // Remove model line entirely — collapse only within frontmatter const normalized = content.replace(/\r\n/g, "\n"); const fmEnd = normalized.indexOf("\n---\n", 4); if (normalized.startsWith("---\n") && fmEnd !== -1) { const frontmatter = normalized.slice(0, fmEnd + 5); const body = normalized.slice(fmEnd + 5); const cleaned = frontmatter.replace(/^model:.*$/m, "").replace(/\n{2,}/g, "\n"); writeFileSync(filePath, cleaned + "\n" + body, "utf-8"); } else { const withoutModel = normalized.replace(/^model:.*$/m, "").replace(/\n{2,}/g, "\n"); writeFileSync(filePath, withoutModel, "utf-8"); } } else if (content.includes("model:")) { const updated = content.replace(/^model:.*$/m, `model: ${newModel}`); writeFileSync(filePath, updated, "utf-8"); } else { const updated = content.replace(/^---\n/, `---\nmodel: ${newModel}\n`); writeFileSync(filePath, updated, "utf-8"); } reloadAgents(); ctx.ui.notify(`Model updated for ${name}`, "info"); } /** * Edit the description field via ctx.ui.input(). */ async function editDescription(ctx: ExtensionCommandContext, name: string, cfg: import("../types.js").AgentConfig): Promise { const filePath = findAgentFile(name); if (!filePath) { ctx.ui.notify(`No .md file found for ${name}`, "warning"); return; } const newDesc = await ctx.ui.input("Description (one line)", cfg.description ?? ""); if (!newDesc) return; const content = readFileSync(filePath, "utf-8"); const updated = content.replace(/^description:.*$/m, `description: ${newDesc}`); if (updated === content) { // description: not found, add it const patched = content.replace(/^---\n/, `---\ndescription: ${newDesc}\n`); writeFileSync(filePath, patched, "utf-8"); } else { writeFileSync(filePath, updated, "utf-8"); } reloadAgents(); ctx.ui.notify(`Description updated for ${name}`, "info"); } /** * Edit the system prompt body via ctx.ui.editor(). */ async function editSystemPrompt(ctx: ExtensionCommandContext, name: string, cfg: import("../types.js").AgentConfig): Promise { const filePath = findAgentFile(name); if (!filePath) { ctx.ui.notify(`No .md file found for ${name}`, "warning"); return; } const content = readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n"); // Split frontmatter and body at the closing "---\n" const fmEnd = content.indexOf("\n---\n", 4); const hasFm = content.startsWith("---\n") && fmEnd !== -1; const currentBody = hasFm ? content.slice(fmEnd + 5) : cfg.systemPrompt ?? ""; const newBody = await ctx.ui.editor(`System prompt for ${name}`, currentBody); if (newBody === undefined || newBody === currentBody) return; if (hasFm) { // Keep everything up to and including the closing ---\n, then append new body const frontmatter = content.slice(0, fmEnd + 5); writeFileSync(filePath, frontmatter + "\n" + newBody, "utf-8"); } else { // Fallback: try regex for edge-case frontmatter or use defaults const frontmatter = content.match(/^---\n[\s\S]*?\n---\n/)?.[0] ?? "---\n---\n"; writeFileSync(filePath, frontmatter + "\n" + newBody, "utf-8"); } reloadAgents(); ctx.ui.notify(`System prompt updated for ${name}`, "info"); }