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

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 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.

// 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 detects the unsaved state and hydrates the component state from localStorage. This runs only when isNew is true and no sessionIdRef exists.

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

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

// 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. 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".

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

// 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, 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, hooks/useAgentSession.ts, and 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. 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 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, which coordinates between lib/tool-preset-preference.ts (for localStorage operations) and 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →