fix(coding-agent): add force exclude pattern and fix config toggle persistence

- Add `-path` force-exclude pattern (exact path match, highest precedence)
- Change `+path` force-include to exact path match (no glob)
- Manifest-excluded resources are now hidden from config (not toggleable)
- Config toggle uses `+` to enable and `-` to disable
- Settings paths resolve relative to settings.json location:
  - Global: relative to ~/.pi/agent
  - Project: relative to .pi
- Add baseDir to PathMetadata for proper relative path computation
- Update tests for new base directory and pattern behavior

fixes #951
This commit is contained in:
Mario Zechner 2026-01-26 12:47:07 +01:00
parent 7a0b435923
commit ea93e2f3da
5 changed files with 259 additions and 102 deletions

View file

@ -2,7 +2,7 @@
* TUI component for managing package resources (enable/disable)
*/
import { basename, relative } from "node:path";
import { basename, dirname, join, relative } from "node:path";
import {
type Component,
Container,
@ -14,6 +14,7 @@ import {
truncateToWidth,
visibleWidth,
} from "@mariozechner/pi-tui";
import { CONFIG_DIR_NAME } from "../../../config.js";
import type { PathMetadata, ResolvedPaths, ResolvedResource } from "../../../core/package-manager.js";
import type { PackageSource, SettingsManager } from "../../../core/settings-manager.js";
import { theme } from "../theme/theme.js";
@ -170,6 +171,7 @@ class ResourceList implements Component, Focusable {
private maxVisible = 15;
private settingsManager: SettingsManager;
private cwd: string;
private agentDir: string;
public onCancel?: () => void;
public onExit?: () => void;
@ -184,10 +186,11 @@ class ResourceList implements Component, Focusable {
this.searchInput.focused = value;
}
constructor(groups: ResourceGroup[], settingsManager: SettingsManager, cwd: string) {
constructor(groups: ResourceGroup[], settingsManager: SettingsManager, cwd: string, agentDir: string) {
this.groups = groups;
this.settingsManager = settingsManager;
this.cwd = cwd;
this.agentDir = agentDir;
this.searchInput = new Input();
this.buildFlatList();
this.filteredItems = [...this.flatItems];
@ -416,19 +419,20 @@ class ResourceList implements Component, Focusable {
// Generate pattern for this resource
const pattern = this.getResourcePattern(item);
const disablePattern = `!${pattern}`;
const disablePattern = `-${pattern}`;
const enablePattern = `+${pattern}`;
// Filter out existing patterns for this resource
const updated = current.filter((p) => {
const stripped = p.startsWith("!") || p.startsWith("+") ? p.slice(1) : p;
const stripped = p.startsWith("!") || p.startsWith("+") || p.startsWith("-") ? p.slice(1) : p;
return stripped !== pattern;
});
if (!enabled) {
// Add !pattern to disable
if (enabled) {
updated.push(enablePattern);
} else {
updated.push(disablePattern);
}
// For enabling, just remove the !pattern (done above)
if (scope === "project") {
if (arrayKey === "extensions") {
@ -480,18 +484,20 @@ class ResourceList implements Component, Focusable {
// Generate pattern relative to package root
const pattern = this.getPackageResourcePattern(item);
const disablePattern = `!${pattern}`;
const disablePattern = `-${pattern}`;
const enablePattern = `+${pattern}`;
// Filter out existing patterns for this resource
const updated = current.filter((p) => {
const stripped = p.startsWith("!") || p.startsWith("+") ? p.slice(1) : p;
const stripped = p.startsWith("!") || p.startsWith("+") || p.startsWith("-") ? p.slice(1) : p;
return stripped !== pattern;
});
if (!enabled) {
if (enabled) {
updated.push(enablePattern);
} else {
updated.push(disablePattern);
}
// For enabling, just remove the !pattern (done above)
(pkg as Record<string, unknown>)[arrayKey] = updated.length > 0 ? updated : undefined;
@ -510,19 +516,19 @@ class ResourceList implements Component, Focusable {
}
}
private getTopLevelBaseDir(scope: "user" | "project"): string {
return scope === "project" ? join(this.cwd, CONFIG_DIR_NAME) : this.agentDir;
}
private getResourcePattern(item: ResourceItem): string {
// Try relative path from cwd first
const rel = relative(this.cwd, item.path);
if (!rel.startsWith("..") && !rel.startsWith("/")) {
return rel;
}
// Fall back to basename
return basename(item.path);
const scope = item.metadata.scope as "user" | "project";
const baseDir = this.getTopLevelBaseDir(scope);
return relative(baseDir, item.path);
}
private getPackageResourcePattern(item: ResourceItem): string {
// For package resources, use just the filename
return basename(item.path);
const baseDir = item.metadata.baseDir ?? dirname(item.path);
return relative(baseDir, item.path);
}
}
@ -542,6 +548,7 @@ export class ConfigSelectorComponent extends Container implements Focusable {
resolvedPaths: ResolvedPaths,
settingsManager: SettingsManager,
cwd: string,
agentDir: string,
onClose: () => void,
onExit: () => void,
requestRender: () => void,
@ -558,7 +565,7 @@ export class ConfigSelectorComponent extends Container implements Focusable {
this.addChild(new Spacer(1));
// Resource list
this.resourceList = new ResourceList(groups, settingsManager, cwd);
this.resourceList = new ResourceList(groups, settingsManager, cwd, agentDir);
this.resourceList.onCancel = onClose;
this.resourceList.onExit = onExit;
this.resourceList.onToggle = () => requestRender();