# How Skills Are Listed in Pi Web: Discovery, Annotation, and Runtime Visibility

> Discover how skills are listed in Pi Web. Learn about skill discovery, annotation, and runtime visibility with the Pi SDK and interactive UI components.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-07-30

---

**Pi Web discovers skills through the Pi SDK's `DefaultResourceLoader`, enriches them with installation metadata from lock files, and exposes them at runtime via an API endpoint and React components that render the live skill list in the UI.**

The `agegr/pi-web` repository implements a multi-layered system for listing available agent skills. Understanding how skills are listed in Pi Web—including their runtime visibility—requires examining the interaction between the Pi SDK's resource loading mechanism, lock-file based annotation, and the frontend components that consume this data.

## Skill Discovery via the Pi SDK

Pi Web relies on the **Pi SDK's** `DefaultResourceLoader` to discover available skills across multiple filesystem roots. The loader is instantiated with the current working directory (`cwd`) and the agent's global directory (`agentDir`), then initialized to scan skill directories.

```typescript
const loader = new DefaultResourceLoader({ cwd, agentDir });
await loader.reload(projectTrustReloadOptions(cwd, agentDir));
const { skills } = loader.getSkills();

```

The `loader.getSkills()` method walks several roots and returns an array of `SkillInfo` objects sourced from:

- The global skills folder at `~/.agents/skills` (symlinked into the agent's `skills` directory)
- The project-specific folder `.pi/skills` inside the repository
- Package-installed skill directories defined by the SDK's extensions system

## Annotating Installation Metadata

After discovery, raw skill data requires enrichment to reflect installation state. The `annotateSkillsWithInstallInfo` function in [`lib/skill-lock.ts`](https://github.com/agegr/pi-web/blob/main/lib/skill-lock.ts) (lines 21-46) merges each skill with metadata from two lock files:

- **Global lock**: `$XDG_STATE_HOME/skills/.skill-lock.json` for globally installed skills
- **Project lock**: `<cwd>/skills-lock.json` for project-scoped installations

The function determines whether a skill's `filePath` lives under the global root (`~/.agents/skills`) or project root (`.pi/skills`), then attaches the corresponding lock entry.

```typescript
export function annotateSkillsWithInstallInfo(
  skills: SkillInfo[],
  { cwd, agentDir }: AnnotateSkillOptions,
): SkillInfo[] {
  const globalEntries = readSkillLock(getGlobalSkillsLockPath());
  const projectEntries = readSkillLock(join(cwd, 'skills-lock.json'));
  const globalRoot = join(agentDir, 'skills');
  const projectRoot = join(cwd, '.pi', 'skills');

  return skills.map(skill => {
    if (!existsSync(skill.filePath)) return skill;
    const install = isWithin(skill.filePath, globalRoot)
      ? getInstallInfo(globalEntries, skill.name, 'global')
      : isWithin(skill.filePath, projectRoot)
      ? getInstallInfo(projectEntries, skill.name, 'project')
      : undefined;
    return install ? { ...skill, install } : skill;
  });
}

```

This produces a final `SkillInfo` object that may include an `install` property containing scope, package name, version hash, and URL.

## The Skills API Endpoint

The server-side route at [`app/api/skills/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/skills/route.ts) exposes the annotated skill list via HTTP. The endpoint calls `loadSkillsWithInstallInfo(cwd)` and returns a JSON payload containing:

- `skills`: The annotated array of `SkillInfo` objects
- `projectResourcesLoaded`: A boolean indicating whether project trust status allowed loading of local skills

```typescript
import { loadSkillsWithInstallInfo } from '@/lib/skills-service';

export async function GET(req: Request) {
  const url = new URL(req.url);
  const cwd = url.searchParams.get('cwd') ?? process.cwd();
  const data = await loadSkillsWithInstallInfo(cwd);
  return new Response(JSON.stringify(data), { status: 200 });
}

```

## Runtime Visibility in the UI

Runtime visibility operates through two mechanisms: the **RPC manager** for active sessions and the **React frontend** for user interaction.

In [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) (line 537), the RPC manager accesses `inner.resourceLoader.getSkills().skills` when an `AgentSession` is active, ensuring the current session can query the live skill list.

The UI component [`components/SkillsConfig.tsx`](https://github.com/agegr/pi-web/blob/main/components/SkillsConfig.tsx) fetches the `/api/skills` endpoint on mount and stores the results in component state:

```typescript
const loadSkills = useCallback(async () => {
  const res = await fetch(`/api/skills?cwd=${encodeURIComponent(cwd)}`);
  const { skills, projectResourcesLoaded } = await res.json();
  setSkills(skills);
  setProjectResourcesLoaded(projectResourcesLoaded);
}, [cwd]);

```

The UI renders each skill with:

- **Name** and **description** from `SkillInfo`
- **Install count** and **skills.sh link** (for public registry skills)
- **Action buttons** for install, update, and model-invocation toggling based on the `install` metadata

The component also respects the `projectResourcesLoaded` flag to display warnings when the project is not trusted.

## Summary

- **Discovery**: The `DefaultResourceLoader` scans global (`~/.agents/skills`) and project (`.pi/skills`) directories to collect `SkillInfo` objects
- **Annotation**: `annotateSkillsWithInstallInfo` in [`lib/skill-lock.ts`](https://github.com/agegr/pi-web/blob/main/lib/skill-lock.ts) enriches skills with installation metadata from global and project lock files
- **API Exposure**: The `/api/skills` endpoint in [`app/api/skills/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/skills/route.ts) serves annotated skills with trust status
- **Runtime Visibility**: The RPC manager exposes skills to active sessions, while [`SkillsConfig.tsx`](https://github.com/agegr/pi-web/blob/main/SkillsConfig.tsx) fetches and renders the live list with installation controls

## Frequently Asked Questions

### How does Pi Web distinguish between global and project-specific skills?

Pi Web checks each skill's `filePath` against the global root (`join(agentDir, 'skills')`) and project root (`join(cwd, '.pi', 'skills')`). Skills existing under the global directory receive metadata from `$XDG_STATE_HOME/skills/.skill-lock.json`, while project-scoped skills pull from `<cwd>/skills-lock.json`.

### What happens when a project is not trusted?

When project trust checks fail, the `projectResourcesLoaded` flag returns `false` from the `/api/skills` endpoint. The UI ([`components/SkillsConfig.tsx`](https://github.com/agegr/pi-web/blob/main/components/SkillsConfig.tsx)) detects this state and displays a warning that project-local skills are unavailable, while still showing globally installed skills.

### How does the runtime UI stay synchronized with the skill list?

The `SkillsConfig` component fetches fresh data from `/api/skills?cwd=...` on component mount and during refresh operations. Additionally, the RPC manager in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) maintains access to `inner.resourceLoader.getSkills().skills` for active AgentSessions, ensuring the backend state remains consistent with the filesystem.

### Where does Pi Web store skill installation metadata?

Installation metadata resides in two JSON lock files: the global lock at `$XDG_STATE_HOME/skills/.skill-lock.json` and the project lock at `<cwd>/skills-lock.json`. The [`lib/skill-lock.ts`](https://github.com/agegr/pi-web/blob/main/lib/skill-lock.ts) utility reads these files to determine installation scope, version hashes, and package URLs for each discovered skill.