# How the Tool Preset System Persists for New vs Existing Sessions in Pi Web

> Discover how Pi Web's tool preset system persists across sessions. Learn about localStorage for new sessions and server files for existing ones.

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

---

**Pi Web uses browser `localStorage` to remember your preferred tool preset for new sessions, but relies on server-stored session files to restore exact tool configurations for existing sessions.**

The `agegr/pi-web` repository implements a dual-track persistence system for **tool presets**—named collections of built-in tools ranging from "none" to "full". Understanding how this system handles **new sessions vs existing sessions** is crucial for developers customizing the agent interface. The implementation splits persistence logic between client-side storage for unsaved chats and server-side session files for persisted conversations.

## New Sessions – Browser-based Persistence

For brand-new conversations, the system defers to the browser's `localStorage` until the session is actually created and saved to disk.

### Reading the User's Preference from localStorage

The helper module [`lib/tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-preset-preference.ts) manages a single storage key (`pi-tool-preset`) that survives page refreshes. The `getPreferredToolPreset()` function reads this value and validates it against known preset names, falling back to `"default"` if the storage is empty or invalid.

```typescript
// lib/tool-preset-preference.ts
export function getPreferredToolPreset(
  storage: StorageLike | null = getBrowserStorage(),
): ToolPreset {
  if (!storage) return "default";
  const value = storage.getItem(STORAGE_KEY);
  return isToolPreset(value) ? value : "default";
}

```

This ensures that returning users see their last selected preset (none, read-only, default, or full) immediately when opening a fresh chat.

### Applying the Preset on Component Mount

When a new session component mounts, the `useLayoutEffect` hook in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) detects the unsaved state and hydrates the component state from localStorage. This runs only when `isNew` is true and no `sessionIdRef` exists.

```typescript
// hooks/useAgentSession.ts
useLayoutEffect(() => {
  if (!isNew || sessionIdRef.current) return;
  setToolPresetState(getPreferredToolPreset());
}, [isNew, setToolPresetState]);

```

At this stage, the preset exists only in React state and browser storage—it has not yet been transmitted to the backend.

### Persisting to the Backend on Session Creation

When the UI finally creates the session via `ensureNewSession()`, the preset is translated into an array of specific tool names using `getToolNamesForPreset()`. These names are sent in the `toolNames` field of the `POST /api/agent/new` request body, committing the configuration to the server.

```typescript
// hooks/useAgentSession.ts (ensureNewSession)
const toolNames = getToolNamesForPreset(toolPreset);
body: JSON.stringify({ 
  cwd: newSessionCwd, 
  type: "ensure_session", 
  toolNames, 
  ... 
})

```

Once the server acknowledges the creation, the session file (`.jsonl`) stores the active tool list, and the browser's localStorage preference becomes irrelevant for this specific session.

## Existing Sessions – Server-driven Persistence

For previously saved sessions, the system ignores localStorage entirely and reconstructs the preset from the actual tool state stored on the server.

### Retrieving Active Tools via RPC

When loading an existing session, the client issues a `get_tools` command through the RPC manager. The server returns an array of `ToolEntry` objects indicating which tools are currently active for that session.

```typescript
// hooks/useAgentSession.ts
const tools = await sendAgentCommand<ToolEntry[]>(sid, { type: "get_tools" });

```

This query hits the session file on disk, ensuring the client receives the authoritative tool configuration regardless of which browser or device is used.

### Reverse-engineering the Preset from Tool State

The returned tool list passes through `getPresetFromTools()` in [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts). This function sorts the active built-in tool names and compares them against the canonical preset definitions (`PRESET_READ_ONLY`, `PRESET_DEFAULT`, `PRESET_FULL`), returning the matching preset name or falling back to `"default"`.

```typescript
// lib/tool-presets.ts
export function getPresetFromTools(tools: ToolEntry[]): ToolPreset {
  const activeTools = tools.filter((t) => t.active);
  if (activeTools.length === 0) return "none";
  const active = activeTools.map(t => t.name)
                            .filter(name => BUILTIN_TOOL_NAMES.has(name))
                            .sort()
                            .join(",");
  if (active === [...PRESET_READ_ONLY].sort().join(",")) return "read-only";
  if (active === [...PRESET_DEFAULT].sort().join(","))   return "default";
  if (active === [...PRESET_FULL].sort().join(","))      return "full";
  return "default";
}

```

### Synchronizing UI State

The derived preset is then written to component state via `setToolPresetState()`, aligning the UI controls with the session's actual capabilities.

```typescript
// hooks/useAgentSession.ts
setToolPresetState(getPresetFromTools(tools));

```

Because the session file (`.jsonl`) is the source of truth, reloading the same session weeks later will restore the exact same tool configuration, independent of any changes to the user's browser localStorage preferences.

## Summary

- **New sessions** read the preset preference from `window.localStorage` (key: `pi-tool-preset`) via `getPreferredToolPreset()`, apply it to React state on mount, and transmit the tool list to the server only when creating the session via `POST /api/agent/new`.
- **Existing sessions** fetch the active tool list via the `get_tools` RPC command, derive the preset using `getPresetFromTools()` in [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts), and ignore localStorage entirely.
- The **session file** (`.jsonl`) serves as the persistent source of truth for saved chats, while **localStorage** only affects the initial state of unsaved conversations.
- The three critical files implementing this logic are [`lib/tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-preset-preference.ts), [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), and [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts).

## Frequently Asked Questions

### Where is the tool preset stored for a new session before it is saved?

For new sessions, the preset is stored in the browser's `localStorage` under the key `pi-tool-preset`, managed by [`lib/tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-preset-preference.ts). This value is read once when the session component mounts and is only relevant until the session is created server-side via the `ensure_session` API call.

### Why doesn't changing localStorage affect existing sessions?

Existing sessions load their tool configuration from the session file (`.jsonl`) on disk via the `get_tools` RPC command. The UI derives the preset from the actual active tools using `getPresetFromTools()`, making the browser's localStorage preference irrelevant once a session has been persisted.

### What happens if the active tools don't match any defined preset?

The `getPresetFromTools()` function in [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts) returns `"default"` as a fallback when the sorted list of active built-in tools doesn't exactly match the "none", "read-only", "default", or "full" preset definitions. This ensures the UI always displays a valid preset selection even if tools were modified individually.

### Which source files control the tool preset persistence logic?

The persistence flow is orchestrated by [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), which coordinates between [`lib/tool-preset-preference.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-preset-preference.ts) (for localStorage operations) and [`lib/tool-presets.ts`](https://github.com/agegr/pi-web/blob/main/lib/tool-presets.ts) (for preset definitions and tool-to-preset mapping). The RPC handling for existing sessions is managed through the agent command interface in the same hook.