# How SkillsConfig Manages Skill Installation and Enable/Disable Toggles in pi-web

> Discover how SkillsConfig manages pi-web skill installation and enable/disable toggles. Learn about metadata loading, toggle synchronization, and scope management.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-07-31

---

**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](https://github.com/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`](https://github.com/agegr/pi-web/blob/main/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.

```typescript
// 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`](https://github.com/agegr/pi-web/blob/main/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.

```typescript
// 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`](https://github.com/agegr/pi-web/blob/main/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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.tsx`](https://github.com/agegr/pi-web/blob/main/components/SkillsConfig.tsx) provides the React UI for skill lifecycle management in pi-web.
- **Metadata loading** combines `DefaultResourceLoader` with `annotateSkillsWithInstallInfo()` from [`lib/skill-lock.ts`](https://github.com/agegr/pi-web/blob/main/lib/skill-lock.ts) to reconcile skill definitions with global and project lock files.
- **Enable/disable toggles** synchronize via PATCH requests to `/api/skills`, updating the `disableModelInvocation` flag in real-time.
- **Installation flow** queries skills.sh through `/api/skills/search` and executes via POST to `/api/skills/install`, supporting both global and project scopes.
- **Update mechanism** checks version hashes against remote sources via `/api/skills/check` and 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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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.