feat(coding-agent): refine resource metadata and display

This commit is contained in:
Mario Zechner 2026-01-24 02:46:08 +01:00
parent 79ab767beb
commit 725d6bbf35
10 changed files with 213 additions and 109 deletions

View file

@ -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();

View file

@ -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({

View file

@ -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: [

View file

@ -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

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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 }> = [];

View file

@ -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 {

View file

@ -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");
}
}

View file

@ -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));
}