How Tool Presets Are Applied and Persisted for New Sessions in Pi Web
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, 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 handles all read and write operations:
// 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 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 hook orchestrates the preset-to-tools conversion:
// 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 (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 provides the user-facing control:
// 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 modifylocalStorage. - 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.tswith four standard security/capability tiers. - Browser persistence is handled by
lib/tool-preset-preference.tsusinglocalStorageunder the key"pi-tool-preset". - Session creation in
hooks/useAgentSession.tsreads the stored preset, converts it to tool names viagetToolNamesForPreset(), and includes them in the POST payload to/api/agent/new. - Server enforcement occurs in
lib/rpc-manager.ts, which builds theAgentSessionwith the exact tool allow-list provided. - UI updates in
components/ChatInput.tsxpersist 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 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.
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 →