# How Tool Presets Are Applied and Persisted for New Sessions in Pi Web

> Learn how Pi Web applies and persists tool presets for new sessions. Discover how user-selected presets are read from localStorage, converted to tool names, and sent to the server for AgentSession creation.

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

---

**Pi Web applies tool presets to new sessions by reading a user-selected preset from browser `localStorage`, converting it to an explicit list of tool names, and sending that list to the server when creating an AgentSession.**

Pi Web implements a lightweight, browser-first persistence layer for tool presets that ensures every new coding session starts with the exact set of tools the user expects. This article explains the complete flow—from preset definitions to server-side session creation—based on the actual source code in the agegr/pi-web repository.

---

## What Are Tool Presets in Pi Web?

Tool presets are predefined bundles of capabilities that control what an AI agent can do during a session. In [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts), Pi Web declares four built-in presets as enum values:

- **`PRESET_NONE`** — No tools are enabled; the agent operates as a plain text model.
- **`PRESET_READ_ONLY`** — Only read-only tools (file reading, directory listing) are available.
- **`PRESET_DEFAULT`** — The standard tool set that Pi Web ships with (balanced read/write capabilities).
- **`PRESET_FULL`** — Every available tool is enabled, including destructive operations.

The preset system provides a quick way to switch between security levels and capability sets without toggling individual tools.

---

## How Presets Are Persisted in the Browser

Pi Web stores the user's preset preference using the browser's `localStorage` API. The module [`lib/tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-preset-preference.ts) handles all read and write operations:

```typescript
// lib/tool-preset-preference.ts
const LOCAL_STORAGE_KEY = "pi-tool-preset";

export function getStoredPreset(): ToolPreset {
  const raw = localStorage.getItem(LOCAL_STORAGE_KEY);
  return raw ? (raw as ToolPreset) : TOOL_PRESET_VALUES.DEFAULT;
}

export function setStoredPreset(preset: ToolPreset): void {
  localStorage.setItem(LOCAL_STORAGE_KEY, preset);
}

```

This approach ensures **immediate persistence**—no server round-trip or database write is required. The preset survives browser restarts, tab closures, and even cross-tab synchronization (localStorage is origin-scoped).

---

## Converting Presets to Tool Names for Session Creation

A preset is an abstract label; the server requires concrete tool identifiers. The `getToolNamesForPreset()` helper in [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts) performs this mapping, returning an array of tool name strings that correspond to the preset's definition.

When a user initiates a new session—typically by submitting a message in the chat interface—the [`useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/useAgentSession.ts) hook orchestrates the preset-to-tools conversion:

```typescript
// hooks/useAgentSession.ts (excerpt)
import { getStoredPreset, setStoredPreset } from "@/lib/tool-preset-preference";
import { getToolNamesForPreset } from "@/lib/tool-presets";

async function startNewSession() {
  const preset = getStoredPreset();                     // ← read from localStorage
  const toolNames = getToolNamesForPreset(preset);     // convert preset → tool list
  const payload = { cwd, message, toolNames /* … */ };

  const res = await fetch("/api/agent/new", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload)
  });
  // … handle response
}

```

Key observation: **the preset enum itself is never transmitted to the server**. Only the resolved `toolNames` array travels across the wire. This decouples the client-side preset abstraction from the server's tool authorization logic.

---

## Server-Side Session Construction

The backend receives the tool list in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) (via the `/api/agent/new` endpoint handler). The RPC manager constructs an `AgentSession` instance using the provided `toolNames` as an allow-list:

- Tools not in the list are excluded from the session's tool registry.
- The session object's capabilities are locked at creation time—runtime tool changes for that session require explicit re-initialization.

This design ensures that the **preset's security boundary is enforced server-side**, not merely suggested by client-side UI state.

---

## UI Integration: Selecting and Updating Presets

The preset selector in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) provides the user-facing control:

```tsx
// components/ChatInput.tsx (excerpt)
import type { ToolPreset } from "@/lib/tool-presets";
import { setStoredPreset } from "@/lib/tool-preset-preference";

function PresetSelector({ current }: { current: ToolPreset }) {
  return (
    <select
      value={current}
      onChange={e => {
        const chosen = e.target.value as ToolPreset;
        setStoredPreset(chosen);               // persist immediately
        // Parent component re-renders with new preset
      }}
    >
      <option value="PRESET_NONE">None</option>
      <option value="PRESET_READ_ONLY">Read‑only</option>
      <option value="PRESET_DEFAULT">Default</option>
      <option value="PRESET_FULL">Full</option>
    </select>
  );
}

```

The `onChange` handler calls `setStoredPreset()` **before** any session creation occurs, guaranteeing that the next session (even in a new tab) uses the updated selection.

---

## Runtime Tool Changes vs. Preset Persistence

While a session is active, users can toggle individual tools on or off for specific requests. These changes:

- Affect only the **current session's** tool allow-list.
- **Do not** call `setStoredPreset()` or modify `localStorage`.
- Are lost when the session ends; the next new session reverts to the stored preset.

This distinction preserves the **preset as a global default** while allowing **per-session flexibility**.

---

## Summary

- **Preset definitions** live in [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts) with four standard security/capability tiers.
- **Browser persistence** is handled by [`lib/tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-preset-preference.ts) using `localStorage` under the key `"pi-tool-preset"`.
- **Session creation** in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) reads the stored preset, converts it to tool names via `getToolNamesForPreset()`, and includes them in the POST payload to `/api/agent/new`.
- **Server enforcement** occurs in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), which builds the `AgentSession` with the exact tool allow-list provided.
- **UI updates** in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) persist user choices immediately for future sessions.

---

## Frequently Asked Questions

### What happens if no preset is stored in localStorage?

Pi Web defaults to `TOOL_PRESET_VALUES.DEFAULT` (the standard tool set). The `getStoredPreset()` function returns this fallback when `localStorage.getItem("pi-tool-preset")` yields `null`.

### Can users have different presets for different projects?

Not natively. The current implementation uses a single global key in `localStorage`. Per-project presets would require extending [`tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/tool-preset-preference.ts) to scope storage keys by project path or adding a project-aware abstraction layer.

### Why doesn't the server receive the preset enum directly?

The server only needs the concrete tool allow-list to enforce permissions. Sending the resolved `toolNames` array decouples the client-side preset abstraction from server logic, allowing presets to evolve without requiring backend changes.

### Is the preset synchronized across browser tabs?

Yes. Since `localStorage` is origin-scoped, all tabs from the same origin share the stored value. However, tabs already open will not auto-refresh their UI; they read `localStorage` only at session creation time or on explicit reload.