diff --git a/packages/coding-agent/examples/sdk/03-custom-prompt.ts b/packages/coding-agent/examples/sdk/03-custom-prompt.ts index f4bd521e..7a4444d7 100644 --- a/packages/coding-agent/examples/sdk/03-custom-prompt.ts +++ b/packages/coding-agent/examples/sdk/03-custom-prompt.ts @@ -10,6 +10,7 @@ import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mari const loader1 = new DefaultResourceLoader({ systemPromptOverride: () => `You are a helpful assistant that speaks like a pirate. Always end responses with "Arrr!"`, + // Needed to avoid DefaultResourceLoader appending APPEND_SYSTEM.md from ~/.pi/agent or /.pi. appendSystemPromptOverride: () => [], }); await loader1.reload(); diff --git a/packages/coding-agent/examples/sdk/04-skills.ts b/packages/coding-agent/examples/sdk/04-skills.ts index 4bffa5fb..e7e92a78 100644 --- a/packages/coding-agent/examples/sdk/04-skills.ts +++ b/packages/coding-agent/examples/sdk/04-skills.ts @@ -13,7 +13,7 @@ const customSkill: Skill = { description: "Custom project instructions", filePath: "/virtual/SKILL.md", baseDir: "/virtual", - source: "custom", + source: "path", }; const loader = new DefaultResourceLoader({ @@ -28,13 +28,13 @@ const loader = new DefaultResourceLoader({ await loader.reload(); // Discover all skills from cwd/.pi/skills, ~/.pi/agent/skills, etc. -const discovered = loader.getSkills(); +const { skills: allSkills, diagnostics } = loader.getSkills(); console.log( "Discovered skills:", - discovered.skills.map((s) => s.name), + allSkills.map((s) => s.name), ); -if (discovered.diagnostics.length > 0) { - console.log("Warnings:", discovered.diagnostics); +if (diagnostics.length > 0) { + console.log("Warnings:", diagnostics); } await createAgentSession({ diff --git a/packages/coding-agent/examples/sdk/07-context-files.ts b/packages/coding-agent/examples/sdk/07-context-files.ts index 50390b50..a0240659 100644 --- a/packages/coding-agent/examples/sdk/07-context-files.ts +++ b/packages/coding-agent/examples/sdk/07-context-files.ts @@ -6,6 +6,7 @@ import { createAgentSession, DefaultResourceLoader, SessionManager } from "@mariozechner/pi-coding-agent"; +// Disable context files entirely by returning an empty list in agentsFilesOverride. const loader = new DefaultResourceLoader({ agentsFilesOverride: (current) => ({ agentsFiles: [ diff --git a/packages/coding-agent/examples/sdk/08-prompt-templates.ts b/packages/coding-agent/examples/sdk/08-prompt-templates.ts index 76f916dd..1926846b 100644 --- a/packages/coding-agent/examples/sdk/08-prompt-templates.ts +++ b/packages/coding-agent/examples/sdk/08-prompt-templates.ts @@ -15,8 +15,8 @@ import { const deployTemplate: PromptTemplate = { name: "deploy", description: "Deploy the application", - source: "(custom)", - filePath: "", + source: "path", + filePath: "/virtual/prompts/deploy.md", content: `# Deploy Instructions 1. Build: npm run build diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 41021532..d666c1d4 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -43,7 +43,7 @@ export interface PackageManager { options?: { local?: boolean; temporary?: boolean }, ): Promise; setProgressCallback(callback: ProgressCallback | undefined): void; - getInstalledPath(source: string, scope: "global" | "project"): string | undefined; + getInstalledPath(source: string, scope: "user" | "project"): string | undefined; } interface PackageManagerOptions { @@ -52,7 +52,7 @@ interface PackageManagerOptions { settingsManager: SettingsManager; } -type SourceScope = "global" | "project" | "temporary"; +type SourceScope = "user" | "project" | "temporary"; type NpmSource = { type: "npm"; @@ -266,7 +266,7 @@ export class DefaultPackageManager implements PackageManager { this.progressCallback = callback; } - getInstalledPath(source: string, scope: "global" | "project"): string | undefined { + getInstalledPath(source: string, scope: "user" | "project"): string | undefined { const parsed = this.parseSource(source); if (parsed.type === "npm") { const path = this.getNpmInstallPath(parsed, scope); @@ -312,7 +312,7 @@ export class DefaultPackageManager implements PackageManager { // Collect all packages with scope const allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = []; for (const pkg of globalSettings.packages ?? []) { - allPackages.push({ pkg, scope: "global" }); + allPackages.push({ pkg, scope: "user" }); } for (const pkg of projectSettings.packages ?? []) { allPackages.push({ pkg, scope: "project" }); @@ -330,7 +330,7 @@ export class DefaultPackageManager implements PackageManager { globalEntries, resourceType, target, - { source: "local", scope: "global", origin: "top-level" }, + { source: "local", scope: "user", origin: "top-level" }, accumulator, ); this.resolveLocalEntries( @@ -350,7 +350,7 @@ export class DefaultPackageManager implements PackageManager { options?: { local?: boolean; temporary?: boolean }, ): Promise { const accumulator = this.createAccumulator(); - const scope: SourceScope = options?.temporary ? "temporary" : options?.local ? "project" : "global"; + const scope: SourceScope = options?.temporary ? "temporary" : options?.local ? "project" : "user"; const packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope })); await this.resolvePackageSources(packageSources, accumulator); return this.toResolvedPaths(accumulator); @@ -358,7 +358,7 @@ export class DefaultPackageManager implements PackageManager { async install(source: string, options?: { local?: boolean }): Promise { const parsed = this.parseSource(source); - const scope: SourceScope = options?.local ? "project" : "global"; + const scope: SourceScope = options?.local ? "project" : "user"; await this.withProgress("install", source, `Installing ${source}...`, async () => { if (parsed.type === "npm") { await this.installNpm(parsed, scope, false); @@ -374,7 +374,7 @@ export class DefaultPackageManager implements PackageManager { async remove(source: string, options?: { local?: boolean }): Promise { const parsed = this.parseSource(source); - const scope: SourceScope = options?.local ? "project" : "global"; + const scope: SourceScope = options?.local ? "project" : "user"; await this.withProgress("remove", source, `Removing ${source}...`, async () => { if (parsed.type === "npm") { await this.uninstallNpm(parsed, scope); @@ -390,7 +390,7 @@ export class DefaultPackageManager implements PackageManager { async update(source?: string): Promise { if (source) { - await this.updateSourceForScope(source, "global"); + await this.updateSourceForScope(source, "user"); await this.updateSourceForScope(source, "project"); return; } @@ -398,7 +398,7 @@ export class DefaultPackageManager implements PackageManager { const globalSettings = this.settingsManager.getGlobalSettings(); const projectSettings = this.settingsManager.getProjectSettings(); for (const extension of globalSettings.extensions ?? []) { - await this.updateSourceForScope(extension, "global"); + await this.updateSourceForScope(extension, "user"); } for (const extension of projectSettings.extensions ?? []) { await this.updateSourceForScope(extension, "project"); @@ -575,8 +575,8 @@ export class DefaultPackageManager implements PackageManager { const existing = seen.get(identity); if (!existing) { seen.set(identity, entry); - } else if (entry.scope === "project" && existing.scope === "global") { - // Project wins over global + } else if (entry.scope === "project" && existing.scope === "user") { + // Project wins over user seen.set(identity, entry); } // If existing is project and new is global, keep existing (project) @@ -597,7 +597,7 @@ export class DefaultPackageManager implements PackageManager { } private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise { - if (scope === "global" && !temporary) { + if (scope === "user" && !temporary) { await this.runCommand("npm", ["install", "-g", source.spec]); return; } @@ -607,7 +607,7 @@ export class DefaultPackageManager implements PackageManager { } private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise { - if (scope === "global") { + if (scope === "user") { await this.runCommand("npm", ["uninstall", "-g", source.name]); return; } diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 0edd3288..262d2319 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -11,7 +11,7 @@ export interface PromptTemplate { name: string; description: string; content: string; - source: string; // e.g., "(user)", "(project)", "(custom:my-dir)" + source: string; // e.g., "user", "project", "path", "inline" filePath: string; // Absolute path to the template file } @@ -99,7 +99,7 @@ export function substituteArgs(content: string, args: string[]): string { return result; } -function loadTemplateFromFile(filePath: string, sourceLabel: string): PromptTemplate | null { +function loadTemplateFromFile(filePath: string, source: string, sourceLabel: string): PromptTemplate | null { try { const rawContent = readFileSync(filePath, "utf-8"); const { frontmatter, body } = parseFrontmatter>(rawContent); @@ -124,7 +124,7 @@ function loadTemplateFromFile(filePath: string, sourceLabel: string): PromptTemp name, description, content: body, - source: sourceLabel, + source, filePath, }; } catch { @@ -135,7 +135,7 @@ function loadTemplateFromFile(filePath: string, sourceLabel: string): PromptTemp /** * Scan a directory for .md files (non-recursive) and load them as prompt templates. */ -function loadTemplatesFromDir(dir: string, sourceLabel: string): PromptTemplate[] { +function loadTemplatesFromDir(dir: string, source: string, sourceLabel: string): PromptTemplate[] { const templates: PromptTemplate[] = []; if (!existsSync(dir)) { @@ -161,7 +161,7 @@ function loadTemplatesFromDir(dir: string, sourceLabel: string): PromptTemplate[ } if (isFile && entry.name.endsWith(".md")) { - const template = loadTemplateFromFile(fullPath, sourceLabel); + const template = loadTemplateFromFile(fullPath, source, sourceLabel); if (template) { templates.push(template); } @@ -196,9 +196,9 @@ function resolvePromptPath(p: string, cwd: string): string { return isAbsolute(normalized) ? normalized : resolve(cwd, normalized); } -function buildCustomSourceLabel(p: string): string { - const base = basename(p).replace(/\.md$/, "") || "custom"; - return `(custom:${base})`; +function buildPathSourceLabel(p: string): string { + const base = basename(p).replace(/\.md$/, "") || "path"; + return `(path:${base})`; } /** @@ -217,11 +217,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P // 1. Load global templates from agentDir/prompts/ // Note: if agentDir is provided, it should be the agent dir, not the prompts dir const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir; - templates.push(...loadTemplatesFromDir(globalPromptsDir, "(user)")); + templates.push(...loadTemplatesFromDir(globalPromptsDir, "user", "(user)")); // 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/ const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts"); - templates.push(...loadTemplatesFromDir(projectPromptsDir, "(project)")); + templates.push(...loadTemplatesFromDir(projectPromptsDir, "project", "(project)")); // 3. Load explicit prompt paths for (const rawPath of promptPaths) { @@ -233,9 +233,9 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P try { const stats = statSync(resolvedPath); if (stats.isDirectory()) { - templates.push(...loadTemplatesFromDir(resolvedPath, buildCustomSourceLabel(resolvedPath))); + templates.push(...loadTemplatesFromDir(resolvedPath, "path", buildPathSourceLabel(resolvedPath))); } else if (stats.isFile() && resolvedPath.endsWith(".md")) { - const template = loadTemplateFromFile(resolvedPath, buildCustomSourceLabel(resolvedPath)); + const template = loadTemplateFromFile(resolvedPath, "path", buildPathSourceLabel(resolvedPath)); if (template) { templates.push(template); } diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 1fd8b642..56a1bd6a 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -1,6 +1,6 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; -import { join, resolve } from "node:path"; +import { join, resolve, sep } from "node:path"; import chalk from "chalk"; import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.js"; @@ -329,6 +329,9 @@ export class DefaultResourceLoader implements ResourceLoader { const resolvedSkills = this.skillsOverride ? this.skillsOverride(skillsResult) : skillsResult; this.skills = resolvedSkills.skills; this.skillDiagnostics = resolvedSkills.diagnostics; + for (const skill of this.skills) { + this.addDefaultMetadataForPath(skill.filePath); + } const promptPaths = this.noPromptTemplates ? this.mergePaths(cliExtensionPaths.prompts, this.additionalPromptTemplatePaths) @@ -351,6 +354,9 @@ export class DefaultResourceLoader implements ResourceLoader { const resolvedPrompts = this.promptsOverride ? this.promptsOverride(promptsResult) : promptsResult; this.prompts = resolvedPrompts.prompts; this.promptDiagnostics = resolvedPrompts.diagnostics; + for (const prompt of this.prompts) { + this.addDefaultMetadataForPath(prompt.filePath); + } const themePaths = this.noThemes ? this.mergePaths(cliExtensionPaths.themes, this.additionalThemePaths) @@ -367,6 +373,15 @@ export class DefaultResourceLoader implements ResourceLoader { const resolvedThemes = this.themesOverride ? this.themesOverride(themesResult) : themesResult; this.themes = resolvedThemes.themes; this.themeDiagnostics = resolvedThemes.diagnostics; + for (const theme of this.themes) { + if (theme.sourcePath) { + this.addDefaultMetadataForPath(theme.sourcePath); + } + } + + for (const extension of this.extensionsResult.extensions) { + this.addDefaultMetadataForPath(extension.path); + } const agentsFiles = { agentsFiles: loadProjectContextFiles({ cwd: this.cwd, agentDir: this.agentDir }) }; const resolvedAgentsFiles = this.agentsFilesOverride ? this.agentsFilesOverride(agentsFiles) : agentsFiles; @@ -588,6 +603,53 @@ export class DefaultResourceLoader implements ResourceLoader { return undefined; } + private addDefaultMetadataForPath(filePath: string): void { + if (!filePath || filePath.startsWith("<")) { + return; + } + + const normalizedPath = resolve(filePath); + if (this.pathMetadata.has(normalizedPath) || this.pathMetadata.has(filePath)) { + return; + } + + const agentRoots = [ + join(this.agentDir, "skills"), + join(this.agentDir, "prompts"), + join(this.agentDir, "themes"), + join(this.agentDir, "extensions"), + ]; + const projectRoots = [ + join(this.cwd, CONFIG_DIR_NAME, "skills"), + join(this.cwd, CONFIG_DIR_NAME, "prompts"), + join(this.cwd, CONFIG_DIR_NAME, "themes"), + join(this.cwd, CONFIG_DIR_NAME, "extensions"), + ]; + + for (const root of agentRoots) { + if (this.isUnderPath(normalizedPath, root)) { + this.pathMetadata.set(normalizedPath, { source: "local", scope: "user", origin: "top-level" }); + return; + } + } + + for (const root of projectRoots) { + if (this.isUnderPath(normalizedPath, root)) { + this.pathMetadata.set(normalizedPath, { source: "local", scope: "project", origin: "top-level" }); + return; + } + } + } + + private isUnderPath(target: string, root: string): boolean { + const normalizedRoot = resolve(root); + if (target === normalizedRoot) { + return true; + } + const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return target.startsWith(prefix); + } + private detectExtensionConflicts(extensions: Extension[]): Array<{ path: string; message: string }> { const conflicts: Array<{ path: string; message: string }> = []; diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 5d4bd160..b0d1247f 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -366,9 +366,9 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult { try { const stats = statSync(resolvedPath); if (stats.isDirectory()) { - addSkills(loadSkillsFromDirInternal(resolvedPath, "custom", true)); + addSkills(loadSkillsFromDirInternal(resolvedPath, "path", true)); } else if (stats.isFile() && resolvedPath.endsWith(".md")) { - const result = loadSkillFromFile(resolvedPath, "custom"); + const result = loadSkillFromFile(resolvedPath, "path"); if (result.skill) { addSkills({ skills: [result.skill], diagnostics: result.diagnostics }); } else { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 695c4cf0..932106bf 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -190,7 +190,7 @@ async function handlePackageCommand(args: string[]): Promise { return true; } - const formatPackage = (pkg: (typeof globalPackages)[number], scope: "global" | "project") => { + const formatPackage = (pkg: (typeof globalPackages)[number], scope: "user" | "project") => { const source = typeof pkg === "string" ? pkg : pkg.source; const filtered = typeof pkg === "object"; const display = filtered ? `${source} (filtered)` : source; @@ -203,9 +203,9 @@ async function handlePackageCommand(args: string[]): Promise { }; if (globalPackages.length > 0) { - console.log(chalk.bold("Global packages:")); + console.log(chalk.bold("User packages:")); for (const pkg of globalPackages) { - formatPackage(pkg, "global"); + formatPackage(pkg, "user"); } } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 19df0b74..5df57cc8 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -657,66 +657,101 @@ export class InteractiveMode { return this.formatDisplayPath(fullPath); } - /** - * Group paths by source and scope using metadata. - * Returns sorted: local first, then global packages, then project packages. - */ - private groupPathsBySource( + private getDisplaySourceInfo( + source: string, + scope: string, + ): { label: string; scopeLabel?: string; color: "accent" | "muted" } { + if (source === "local") { + if (scope === "user") { + return { label: "user", color: "muted" }; + } + if (scope === "project") { + return { label: "project", color: "muted" }; + } + if (scope === "temporary") { + return { label: "path", scopeLabel: "temp", color: "muted" }; + } + return { label: "path", color: "muted" }; + } + + if (source === "cli") { + return { label: "path", scopeLabel: scope === "temporary" ? "temp" : undefined, color: "muted" }; + } + + const scopeLabel = + scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined; + return { label: source, scopeLabel, color: "accent" }; + } + + private getScopeGroup(source: string, scope: string): "user" | "project" | "path" { + if (source === "cli" || scope === "temporary") return "path"; + if (scope === "user") return "user"; + if (scope === "project") return "project"; + return "path"; + } + + private isPackageSource(source: string): boolean { + return source.startsWith("npm:") || source.startsWith("git:"); + } + + private buildScopeGroups( paths: string[], metadata: Map, - ): Map { - const groups = new Map(); + ): Array<{ scope: "user" | "project" | "path"; paths: string[]; packages: Map }> { + const groups: Record< + "user" | "project" | "path", + { scope: "user" | "project" | "path"; paths: string[]; packages: Map } + > = { + user: { scope: "user", paths: [], packages: new Map() }, + project: { scope: "project", paths: [], packages: new Map() }, + path: { scope: "path", paths: [], packages: new Map() }, + }; for (const p of paths) { const meta = this.findMetadata(p, metadata); const source = meta?.source ?? "local"; const scope = meta?.scope ?? "project"; + const groupKey = this.getScopeGroup(source, scope); + const group = groups[groupKey]; - if (!groups.has(source)) { - groups.set(source, { scope, paths: [] }); + if (this.isPackageSource(source)) { + const list = group.packages.get(source) ?? []; + list.push(p); + group.packages.set(source, list); + } else { + group.paths.push(p); } - groups.get(source)!.paths.push(p); } - // Sort: local first, then global packages, then project packages - const sorted = new Map(); - const entries = Array.from(groups.entries()); - - // Local entries first - for (const [source, data] of entries) { - if (source === "local") sorted.set(source, data); - } - // Global packages - for (const [source, data] of entries) { - if (source !== "local" && data.scope === "global") sorted.set(source, data); - } - // Project packages - for (const [source, data] of entries) { - if (source !== "local" && data.scope === "project") sorted.set(source, data); - } - - return sorted; + return [groups.user, groups.project, groups.path].filter( + (group) => group.paths.length > 0 || group.packages.size > 0, + ); } - /** - * Format grouped paths for display with colors. - */ - private formatGroupedPaths( - groups: Map, - formatPath: (p: string, source: string) => string, + private formatScopeGroups( + groups: Array<{ scope: "user" | "project" | "path"; paths: string[]; packages: Map }>, + options: { + formatPath: (p: string) => string; + formatPackagePath: (p: string, source: string) => string; + }, ): string { const lines: string[] = []; - for (const [source, { scope, paths }] of groups) { - const scopeLabel = scope === "global" ? "global" : scope === "project" ? "project" : ""; - // Source name in accent, scope in muted - const sourceColor = source === "local" ? "muted" : "accent"; - const header = scopeLabel - ? `${theme.fg(sourceColor, source)} ${theme.fg("dim", `(${scopeLabel})`)}` - : theme.fg(sourceColor, source); - lines.push(` ${header}`); - for (const p of paths) { - lines.push(theme.fg("dim", ` ${formatPath(p, source)}`)); + for (const group of groups) { + lines.push(` ${theme.fg("muted", group.scope)}`); + + const sortedPaths = [...group.paths].sort((a, b) => a.localeCompare(b)); + for (const p of sortedPaths) { + lines.push(theme.fg("dim", ` ${options.formatPath(p)}`)); + } + + const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b)); + for (const [source, paths] of sortedPackages) { + lines.push(` ${theme.fg("accent", source)}`); + const sortedPackagePaths = [...paths].sort((a, b) => a.localeCompare(b)); + for (const p of sortedPackagePaths) { + lines.push(theme.fg("dim", ` ${options.formatPackagePath(p, source)}`)); + } } } @@ -756,8 +791,9 @@ export class InteractiveMode { const meta = this.findMetadata(p, metadata); if (meta) { const shortPath = this.getShortPath(p, meta.source); - const scopeLabel = meta.scope === "global" ? "global" : meta.scope === "project" ? "project" : "temp"; - return `${meta.source} (${scopeLabel}) ${shortPath}`; + const { label, scopeLabel } = this.getDisplaySourceInfo(meta.source, meta.scope); + const labelText = scopeLabel ? `${label} (${scopeLabel})` : label; + return `${labelText} ${shortPath}`; } return this.formatDisplayPath(p); } @@ -842,8 +878,11 @@ export class InteractiveMode { const skills = this.session.resourceLoader.getSkills().skills; if (skills.length > 0) { const skillPaths = skills.map((s) => s.filePath); - const groups = this.groupPathsBySource(skillPaths, metadata); - const skillList = this.formatGroupedPaths(groups, (p, source) => this.getShortPath(p, source)); + const groups = this.buildScopeGroups(skillPaths, metadata); + const skillList = this.formatScopeGroups(groups, { + formatPath: (p) => this.formatDisplayPath(p), + formatPackagePath: (p, source) => this.getShortPath(p, source), + }); this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0)); this.chatContainer.addChild(new Spacer(1)); } @@ -857,25 +896,20 @@ export class InteractiveMode { const templates = this.session.promptTemplates; if (templates.length > 0) { - // Group templates by source using metadata const templatePaths = templates.map((t) => t.filePath); - const groups = this.groupPathsBySource(templatePaths, metadata); - const templateLines: string[] = []; - for (const [source, { scope, paths }] of groups) { - const scopeLabel = scope === "global" ? "global" : scope === "project" ? "project" : ""; - const sourceColor = source === "local" ? "muted" : "accent"; - const header = scopeLabel - ? `${theme.fg(sourceColor, source)} ${theme.fg("dim", `(${scopeLabel})`)}` - : theme.fg(sourceColor, source); - templateLines.push(` ${header}`); - for (const p of paths) { - const template = templates.find((t) => t.filePath === p); - if (template) { - templateLines.push(theme.fg("dim", ` /${template.name}`)); - } - } - } - this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateLines.join("\n")}`, 0, 0)); + const groups = this.buildScopeGroups(templatePaths, metadata); + const templateByPath = new Map(templates.map((t) => [t.filePath, t])); + const templateList = this.formatScopeGroups(groups, { + formatPath: (p) => { + const template = templateByPath.get(p); + return template ? `/${template.name}` : this.formatDisplayPath(p); + }, + formatPackagePath: (p) => { + const template = templateByPath.get(p); + return template ? `/${template.name}` : this.formatDisplayPath(p); + }, + }); + this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0)); this.chatContainer.addChild(new Spacer(1)); } @@ -888,8 +922,11 @@ export class InteractiveMode { const extensionPaths = options?.extensionPaths ?? []; if (extensionPaths.length > 0) { - const groups = this.groupPathsBySource(extensionPaths, metadata); - const extList = this.formatGroupedPaths(groups, (p, source) => this.getShortPath(p, source)); + const groups = this.buildScopeGroups(extensionPaths, metadata); + const extList = this.formatScopeGroups(groups, { + formatPath: (p) => this.formatDisplayPath(p), + formatPackagePath: (p, source) => this.getShortPath(p, source), + }); this.chatContainer.addChild(new Text(`${sectionHeader("Extensions")}\n${extList}`, 0, 0)); this.chatContainer.addChild(new Spacer(1)); } @@ -899,8 +936,11 @@ export class InteractiveMode { const customThemes = loadedThemes.filter((t) => t.sourcePath); if (customThemes.length > 0) { const themePaths = customThemes.map((t) => t.sourcePath!); - const groups = this.groupPathsBySource(themePaths, metadata); - const themeList = this.formatGroupedPaths(groups, (p, source) => this.getShortPath(p, source)); + const groups = this.buildScopeGroups(themePaths, metadata); + const themeList = this.formatScopeGroups(groups, { + formatPath: (p) => this.formatDisplayPath(p), + formatPackagePath: (p, source) => this.getShortPath(p, source), + }); this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0)); this.chatContainer.addChild(new Spacer(1)); }