How SkillsConfig Manages Skill Installation and Enable/Disable Toggles in pi-web
SkillsConfig orchestrates the complete lifecycle of pi-web skills by loading metadata from JSON lock files, synchronizing toggle states via PATCH requests, and managing installation scope through dedicated API endpoints.
The SkillsConfig component in the agegr/pi-web repository serves as the central UI for managing AI agent capabilities. It handles everything from discovering locally installed skills to fetching new packages from skills.sh, while respecting both global and project-level configuration boundaries.
Loading and Annotating Skill Metadata
Before any toggles can be flipped or installations started, SkillsConfig must reconcile raw skill definitions with their installation status. This happens in two coordinated phases involving the Pi SDK's resource loader and lock file parsers.
Trust-Aware Resource Loading
In lib/skills-service.ts, the loadSkillsWithInstallInfo() function initializes a DefaultResourceLoader to reload trust-aware resources for the current working directory. This ensures that only vetted skills are presented in the UI.
// lib/skills-service.ts
export async function loadSkillsWithInstallInfo(cwd: string): Promise<SkillsResponse> {
const agentDir = getAgentDir();
const loader = new DefaultResourceLoader({ cwd, agentDir });
await loader.reload(projectTrustReloadOptions(cwd, agentDir));
const { skills, diagnostics } = loader.getSkills();
return {
skills: annotateSkillsWithInstallInfo(skills as SkillInfo[], { cwd, agentDir }),
diagnostics,
projectResourcesLoaded: getProjectTrustStatus(cwd, agentDir).trusted,
};
}
Lock File Annotation
The annotateSkillsWithInstallInfo() function in lib/skill-lock.ts reads both global (~/.agents/.skill-lock.json) and project-level (<cwd>/skills-lock.json) lock files. It determines whether a skill resides in the global directory (~/.agents/skills/) or project directory (<cwd>/.pi/skills/), then attaches a SkillInstallInfo object containing the package name, scope, source URL, and version hash.
// lib/skill-lock.ts
export function annotateSkillsWithInstallInfo(
skills: SkillInfo[],
{ cwd, agentDir, globalLockPath = getGlobalSkillsLockPath(), projectLockPath = join(cwd, "skills-lock.json") }: AnnotateSkillOptions,
): SkillInfo[] {
const globalEntries = readSkillLock(globalLockPath);
const projectEntries = readSkillLock(projectLockPath);
const globalSkillsRoot = join(agentDir, "skills");
const projectSkillsRoot = join(cwd, ".pi", "skills");
return skills.map((skill) => {
if (!existsSync(skill.filePath)) return skill;
const install = isWithin(skill.filePath, globalSkillsRoot)
? getInstallInfo(globalEntries, skill.name, "global")
: isWithin(skill.filePath, projectSkillsRoot)
? getInstallInfo(projectEntries, skill.name, "project")
: undefined;
return install ? { ...skill, install } : skill;
});
}
Enabling and Disabling Skills
Toggling a skill's visibility in model prompts happens through a PATCH request to /api/skills. In components/SkillsConfig.tsx, the toggle() callback constructs a request body containing the filePath and the new disableModelInvocation boolean, then updates local React state optimistically upon success.
// components/SkillsConfig.tsx
const toggle = useCallback(async (skill: Skill) => {
const next = !skill.disableModelInvocation;
setToggling((s) => new Set(s).add(skill.filePath));
try {
const res = await fetch("/api/skills", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filePath: skill.filePath, disableModelInvocation: next }),
});
const d = await res.json();
if (!res.ok) throw new Error(d.error ?? `HTTP ${res.status}`);
setSkills((prev) =>
prev.map((s) => (s.filePath === skill.filePath ? { ...s, disableModelInvocation: next } : s)),
);
} finally {
setToggling((s) => {
const n = new Set(s);
n.delete(skill.filePath);
return n;
});
}
}, []);
The server persists this flag to the appropriate lock file, ensuring the disabled state survives across sessions.
Installing New Skills from skills.sh
When users add skills through the AddSkillPanel, the component posts to /api/skills/search to query candidates, then calls install() to execute the installation. The function sends a POST request to /api/skills/install with the package name, target scope (global or project), and current working directory.
// AddSkillPanel – install()
const install = useCallback(async (pkg: string) => {
setInstalling(pkg);
try {
const res = await fetch("/api/skills/install", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ package: pkg, scope, cwd }),
});
const d = await res.json();
if (!res.ok) throw new Error(d.error ?? `HTTP ${res.status}`);
setNewlyInstalledPkgs((prev) => new Set(prev).add(`${scope}:${pkg}`));
onInstalled();
} finally {
setInstalling(null);
}
}, [scope, cwd]);
After successful installation, the component triggers a refresh of the skill list to reflect the new entry in the lock file.
Checking and Applying Updates
SkillsConfig monitors version drift through two async operations. First, checkForUpdates() POSTs to /api/skills/check with an array of skill keys (scope\0package). The server compares local version hashes against remote GitHub tags and returns an array of SkillUpdateResult objects.
// components/SkillsConfig.tsx – checkForUpdates()
const checkForUpdates = useCallback(async (skill?: Skill) => {
const targets = skill ? [skill] : skills.filter((i) => Boolean(i.install));
const keys = targets.map(updateKey).filter(Boolean);
setCheckingUpdates((c) => new Set([...c, ...keys]));
try {
const res = await fetch("/api/skills/check", { /* … */ });
const data = await res.json();
setUpdateStatuses((cur) => {
const next = { ...cur };
data.updates?.forEach((u) => (next[`${u.scope}\0${u.package}`] = u));
return next;
});
} finally {
setCheckingUpdates((c) => {
const n = new Set(c);
keys.forEach((k) => n.delete(k));
return n;
});
}
}, [skills]);
When an update is available, clicking the Update button invokes updateInstalledSkill(), which POSTs to /api/skills/update. The server pulls the latest commit, rewrites the lock file entry with the new version hash, and returns success. The UI then clears the update indicator and reloads the skill list.
Global vs Project Scope Handling
SkillsConfig respects two distinct installation contexts:
- Global scope: Skills installed under
~/.agents/skills/(or~/.pi/agent/skills/) are available across all projects. The global lock file resides at~/.agents/.skill-lock.json. - Project scope: Skills isolated to the current working directory live under
<cwd>/.pi/skills/with metadata stored in<cwd>/skills-lock.json.
The UI disables project-scoped toggles until projectResourcesLoaded returns true, preventing interaction with untrusted project configurations. When installing, the user selects the target scope, and the API routes handle the corresponding file system paths accordingly.
Summary
- SkillsConfig in
components/SkillsConfig.tsxprovides the React UI for skill lifecycle management in pi-web. - Metadata loading combines
DefaultResourceLoaderwithannotateSkillsWithInstallInfo()fromlib/skill-lock.tsto reconcile skill definitions with global and project lock files. - Enable/disable toggles synchronize via PATCH requests to
/api/skills, updating thedisableModelInvocationflag in real-time. - Installation flow queries skills.sh through
/api/skills/searchand executes via POST to/api/skills/install, supporting both global and project scopes. - Update mechanism checks version hashes against remote sources via
/api/skills/checkand applies changes through/api/skills/update.
Frequently Asked Questions
How does SkillsConfig determine if a skill is installed globally or locally?
According to the pi-web source code in lib/skill-lock.ts, the system compares each skill's filePath against the globalSkillsRoot (~/.agents/skills/) and projectSkillsRoot (<cwd>/.pi/skills/). If the path falls within the global directory, it reads metadata from ~/.agents/.skill-lock.json; otherwise, it checks the project-specific skills-lock.json in the current working directory.
What happens when I disable a skill in the SkillsConfig UI?
When you toggle the switch off, the toggle() function in components/SkillsConfig.tsx sends a PATCH request to /api/skills with disableModelInvocation: true. The server persists this state to the appropriate lock file, and the UI immediately updates its local React state to hide the skill from model prompts without requiring a page reload.
Can SkillsConfig check for updates to skills installed from GitHub?
Yes. The checkForUpdates() function constructs unique keys for each installed skill and POSTs them to /api/skills/check. According to the implementation in the pi-web API routes, the server compares the stored version hash from the lock file against the latest GitHub tag or commit, returning status indicators that the UI displays as "up-to-date", "update-available", or error states.
Why is the project scope toggle sometimes disabled in the UI?
The project scope controls remain locked until the projectResourcesLoaded flag evaluates to true. This safety mechanism, tracked in loadSkillsWithInstallInfo(), ensures that SkillsConfig only interacts with project-local skills after the DefaultResourceLoader has successfully loaded and validated the project's trust-aware resources, preventing accidental modifications to untrusted configurations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →