mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 17:00:59 +00:00
feat(coding-agent): refine resource metadata and display
This commit is contained in:
parent
79ab767beb
commit
725d6bbf35
10 changed files with 213 additions and 109 deletions
|
|
@ -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 <cwd>/.pi.
|
||||
appendSystemPromptOverride: () => [],
|
||||
});
|
||||
await loader1.reload();
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import {
|
|||
const deployTemplate: PromptTemplate = {
|
||||
name: "deploy",
|
||||
description: "Deploy the application",
|
||||
source: "(custom)",
|
||||
filePath: "<inline>",
|
||||
source: "path",
|
||||
filePath: "/virtual/prompts/deploy.md",
|
||||
content: `# Deploy Instructions
|
||||
|
||||
1. Build: npm run build
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export interface PackageManager {
|
|||
options?: { local?: boolean; temporary?: boolean },
|
||||
): Promise<ResolvedPaths>;
|
||||
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<ResolvedPaths> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
if (scope === "global") {
|
||||
if (scope === "user") {
|
||||
await this.runCommand("npm", ["uninstall", "-g", source.name]);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string>>(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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }> = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
|
|||
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<boolean> {
|
|||
};
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, { source: string; scope: string; origin: string }>,
|
||||
): Map<string, { scope: string; paths: string[] }> {
|
||||
const groups = new Map<string, { scope: string; paths: string[] }>();
|
||||
): Array<{ scope: "user" | "project" | "path"; paths: string[]; packages: Map<string, string[]> }> {
|
||||
const groups: Record<
|
||||
"user" | "project" | "path",
|
||||
{ scope: "user" | "project" | "path"; paths: string[]; packages: Map<string, string[]> }
|
||||
> = {
|
||||
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<string, { scope: string; paths: string[] }>();
|
||||
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<string, { scope: string; paths: string[] }>,
|
||||
formatPath: (p: string, source: string) => string,
|
||||
private formatScopeGroups(
|
||||
groups: Array<{ scope: "user" | "project" | "path"; paths: string[]; packages: Map<string, string[]> }>,
|
||||
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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue