/** * Native Pi command handler for config viewing. * * Provides a structured read-only panel of the resolved eforge * configuration (/eforge:config). Falls back to skill forwarding * when the Pi UI is not available. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { API_ROUTES } from "@eforge-build/client"; import { piDaemonRequest, DAEMON_NOT_RUNNING_GUIDANCE } from "./daemon-requests.js"; import { showInfoOverlay, withLoader, type UIContext } from "./ui-helpers"; import { getPresetRegistryData } from "./toolbelt-presets"; // --------------------------------------------------------------------------- // /eforge:config - structured config viewer // --------------------------------------------------------------------------- export async function handleConfigCommand( pi: ExtensionAPI, ctx: UIContext | null, args: string, ): Promise { if (!ctx || !ctx.hasUI) { pi.sendUserMessage(`/skill:eforge-config${args ? " " + args : ""}`); return; } let config: Record; try { const result = await withLoader(ctx, "Loading config...", () => piDaemonRequest>(ctx.cwd, "GET", API_ROUTES.configShow), ); if (result === null) { await showInfoOverlay( ctx, "eforge - Daemon Not Running", `Cannot load config: ${DAEMON_NOT_RUNNING_GUIDANCE}`, ); return; } config = (result.data ?? {}) as Record; } catch (err) { await showInfoOverlay( ctx, "eforge - Configuration Error", `Failed to load config:\n\n${err instanceof Error ? err.message : String(err)}`, ); return; } const sections: string[] = []; // Post-merge commands const build = config.build as Record | undefined; if (build?.postMergeCommands) { sections.push("## Post-Merge Commands\n"); for (const cmd of build.postMergeCommands as string[]) { sections.push(`- \`${cmd}\``); } sections.push(""); } // Hooks const hooks = config.hooks as Array> | undefined; if (hooks && hooks.length > 0) { sections.push("## Hooks\n"); for (const hook of hooks) { sections.push(`- **${hook.event}**: \`${hook.command}\``); } sections.push(""); } // Daemon settings const daemon = config.daemon as Record | undefined; if (daemon) { sections.push("## Daemon\n"); if (daemon.idleShutdownMs !== undefined) { const ms = daemon.idleShutdownMs as number; const display = ms === 0 ? "run forever" : `${Math.round(ms / 60000)} min idle timeout`; sections.push(`- ${display}`); } sections.push(""); } // Agents defaults const agents = config.agents as Record | undefined; if (agents) { sections.push("## Agents\n"); if (agents.maxTurns) sections.push(`- Max turns: ${agents.maxTurns}`); if (agents.maxContinuations) sections.push(`- Max continuations: ${agents.maxContinuations}`); if (agents.effort) sections.push(`- Effort: ${agents.effort}`); if (agents.permissionMode) sections.push(`- Permission mode: ${agents.permissionMode}`); const models = agents.models as Record | undefined; if (models) { sections.push("- Model classes:"); for (const [cls, ref] of Object.entries(models)) { const modelRef = ref as Record; const display = modelRef.provider ? `${modelRef.provider}/${modelRef.id}` : modelRef.id; sections.push(` - **${cls}**: ${display}`); } } sections.push(""); } // Native extensions const extensions = config.extensions as Record | undefined; if (extensions) { sections.push("## Extensions\n"); if (extensions.enabled !== undefined) sections.push(`- Enabled: ${extensions.enabled}`); if (Array.isArray(extensions.include) && extensions.include.length > 0) { sections.push(`- Include: ${extensions.include.map((v) => `\`${v}\``).join(", ")}`); } if (Array.isArray(extensions.exclude) && extensions.exclude.length > 0) { sections.push(`- Exclude: ${extensions.exclude.map((v) => `\`${v}\``).join(", ")}`); } if (Array.isArray(extensions.paths) && extensions.paths.length > 0) { sections.push("- Paths:"); for (const path of extensions.paths) { sections.push(` - \`${path}\``); } } sections.push(""); } // Toolbelts — show configured toolbelts from daemon config, then available presets const tools = config.tools as Record | undefined; const configuredToolbelts = tools?.toolbelts as Record | undefined; const presetRegistry = getPresetRegistryData(); if ((configuredToolbelts && Object.keys(configuredToolbelts).length > 0) || presetRegistry.length > 0) { sections.push("## Toolbelts\n"); if (configuredToolbelts && Object.keys(configuredToolbelts).length > 0) { sections.push("**Configured:**\n"); for (const [tbName, tbEntry] of Object.entries(configuredToolbelts)) { const tb = tbEntry as Record; const servers = Array.isArray(tb.mcpServers) ? (tb.mcpServers as string[]).join(', ') : ''; const desc = typeof tb.description === 'string' ? tb.description : ''; sections.push(`- **${tbName}**${desc ? `: ${desc}` : ''}${servers ? ` (${servers})` : ''}`); } sections.push(""); } sections.push("**Available presets** (use `/eforge:profile:new` to apply):\n"); for (const preset of presetRegistry) { sections.push(`- **${preset.label}** (\`${preset.id}\`): ${preset.description}`); } sections.push(""); } // Queue const prdQueue = config.prdQueue as Record | undefined; if (prdQueue) { sections.push("## Queue\n"); if (prdQueue.autoBuild !== undefined) sections.push(`- Auto-build: ${prdQueue.autoBuild}`); if (prdQueue.dir) sections.push(`- Directory: \`${prdQueue.dir}\``); sections.push(""); } // Max concurrent builds if (config.maxConcurrentBuilds !== undefined) { sections.push("## Concurrency\n"); sections.push(`- Max concurrent builds: ${config.maxConcurrentBuilds}`); sections.push(""); } if (sections.length === 0) { sections.push("*No custom configuration. Using defaults.*\n"); } sections.push("---\n"); sections.push("Edit `eforge/config.yaml` directly to change settings.\n"); sections.push("Use `/eforge:profile` to manage profiles."); sections.push("Use `/eforge:workflow` to configure landing action, stacking, and PR settings."); await showInfoOverlay(ctx, "eforge - Configuration", sections.join("\n")); }