How Pi-Web Tool Presets (NONE, READ_ONLY, DEFAULT, FULL) Work and How They're Passed at Session Creation

Pi-Web uses four built-in tool presets that determine which coding tools are available in a chat session, translating a preset name into a concrete array of tool names that gets passed to the backend during session creation.

Understanding how tool presets function in the Pi-Web codebase requires tracing the flow from UI selection through the API layer to the RPC session manager. This article examines the complete lifecycle of a tool preset, from the TypeScript definitions in the core library to the moment tools are activated for a new agent session.

Tool Preset Definitions in lib/tool-presets.ts

The foundation of Pi-Web's preset system lives in lib/tool-presets.ts, which exports four constants that enumerate allowed tools for each preset mode:

Preset Available Tools
NONE [] (empty — all tools disabled)
READ_ONLY ["read", "grep", "find", "ls"]
DEFAULT ["read", "bash", "edit", "write"]
FULL ["bash", "read", "edit", "write", "grep", "find", "ls"]

This file also provides two critical helper functions. getToolNamesForPreset(preset) converts a preset enum value into its corresponding string array. getPresetFromTools(tools) performs the reverse operation: it accepts an array of tool objects (each with an active boolean) and returns the matching preset name, with a fallback to "default" when no exact match exists.

These helpers ensure the system can both apply presets during session creation and reconstruct the current preset when examining an existing session's state.

How the UI Translates Presets to Tool Names

User interaction with tool presets occurs through hooks/useAgentSession.ts, a React hook that manages the preset selector in the chat interface. When a user selects a new preset, the hook executes a three-step process:

  1. Calls getToolNamesForPreset(preset) to retrieve the concrete tool array
  2. Persists the selection via setPreferredToolPreset to localStorage
  3. Dispatches a "set_tools" command to the running session
// From hooks/useAgentSession.ts (lines 1727-1732)
const toolNames = getToolNamesForPreset(preset);
setPreferredToolPreset(preset);
await sendAgentCommand(sid, { type: "set_tools", toolNames });

The sendAgentCommand function bridges to the backend via WebSocket or HTTP, ensuring the tool configuration change propagates immediately to the active RPC session.

Passing Tool Presets When Creating a New Session

The critical handoff for session creation happens in app/api/agent/new/route.ts. This route handler receives a POST request that may include a toolNames array derived from the selected preset, then forwards it to the RPC manager.

// From app/api/agent/new/route.ts (line 48)
const { toolNames, cwd, message } = await request.json();
const session = await startRpcSession({ toolNames, cwd, message });

The backend does not receive the preset string directly — the preset is always resolved to tool names before transmission. This design keeps the API contract explicit about which tools are available and prevents ambiguity in the session initialization protocol.

Activating Tools in the RPC Session Manager

Once toolNames reaches lib/rpc-manager.ts, the RpcSessionStartOptions type governs how the configuration is applied. The session manager performs three key operations:

// From lib/rpc-manager.ts (lines 698-701)
if (toolNames !== undefined) {
  // Empty list disables all tools and clears the system prompt
  this.setForceEmptySystemPrompt(toolNames.length === 0);
  // Merge with extension tools, then activate
  this.inner.setActiveToolsByName(withExtensionTools(this.inner, toolNames));
}

The setForceEmptySystemPrompt call handles a special case: when toolNames is an empty array (the NONE preset), the manager strips the system prompt entirely to indicate no tools are available. This distinguishes a deliberately empty toolset from an undefined one.

The withExtensionTools helper (lines 147-156) ensures extension-provided tools remain available regardless of preset. It appends any non-coding tools from installed extensions to the user-selected list, preserving functionality like custom integrations even under restrictive presets.

Reading and Reconstructing Presets from Active Sessions

For existing sessions, the UI determines the current preset through a reverse lookup. The flow begins with a "get_tools" command handled in lib/rpc-manager.ts (lines 58-66), which returns the full tool list with active flags.

// UI-side preset reconstruction
const tools = await sendAgentCommand(sessionId, { type: 'get_tools' });
// tools: [{ name: 'read', active: true }, { name: 'bash', active: false }, ...]
const preset = getPresetFromTools(tools); // "read-only" | "default" | "full" | "none"

This mechanism ensures the preset selector always reflects ground truth from the backend rather than cached UI state, preventing synchronization issues when tools are modified through other channels.

Persisting User Preferences Across Sessions

The last preset selected by a user survives page reloads through lib/tool-preset-preference.ts, which manages the pi-tool-preset key in browser localStorage:

// From lib/tool-preset-preference.ts (lines 32-37)
export function setPreferredToolPreset(preset: ToolPreset): void {
  if (typeof window !== 'undefined') {
    localStorage.setItem('pi-tool-preset', preset);
  }
}

export function getPreferredToolPreset(): ToolPreset {
  return (localStorage.getItem('pi-tool-preset') as ToolPreset) || 'default';
}

This preference seeds the preset selector for new sessions only. Existing sessions always derive their preset from actual active tools via getPresetFromTools, ensuring the UI never misrepresents a session's capabilities.

Complete Code Example: Session Creation with a Preset

import { getToolNamesForPreset, ToolPreset } from '@/lib/tool-presets';

async function createReadOnlySession(cwd: string, message: string) {
  const preset: ToolPreset = 'read-only';
  const toolNames = getToolNamesForPreset(preset);
  
  const response = await fetch('/api/agent/new', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ cwd, message, toolNames }),
  });
  
  return response.json(); // { sessionId: string }
}

Summary

  • Preset definitions in lib/tool-presets.ts provide four fixed tool configurations as string arrays, with bidirectional conversion helpers.
  • UI translation in hooks/useAgentSession.ts converts preset selections to toolNames arrays before any backend communication.
  • Session creation at app/api/agent/new/route.ts accepts toolNames in the POST body and forwards it to the RPC manager.
  • Tool activation in lib/rpc-manager.ts applies the tool list, handles the special NONE case by clearing the system prompt, and merges extension tools via withExtensionTools.
  • Preset reconstruction uses getPresetFromTools to derive the preset name from a session's actual active tools, ensuring UI accuracy.
  • Preference persistence stores the last selected preset in localStorage under pi-tool-preset for initialization of future new sessions.

Frequently Asked Questions

What happens when I select the NONE preset?

The NONE preset translates to an empty tool array []. When passed to lib/rpc-manager.ts, this triggers setForceEmptySystemPrompt(true), which disables all tools and clears the system prompt entirely. According to the source code, this special handling distinguishes intentional tool disabling from an undefined configuration.

Can extensions override preset restrictions?

Extensions cannot override preset restrictions, but they are preserved across all presets. The withExtensionTools helper in lib/rpc-manager.ts (lines 147-156) automatically merges any extension-provided tools with the user-selected list, ensuring custom integrations remain functional even when all standard coding tools are disabled.

How does Pi-Web remember my last preset choice?

The last preset is stored in browser localStorage under the key pi-tool-preset by lib/tool-preset-preference.ts. This value initializes the preset selector for new sessions. However, for existing sessions, the UI always queries the backend via "get_tools" and uses getPresetFromTools to display the accurate current state.

Why does the API use toolNames instead of the preset string?

The API uses concrete toolNames arrays rather than preset identifiers to maintain an explicit contract about session capabilities. This design prevents ambiguity — the backend knows exactly which tools to activate without needing to import preset definitions, and the UI remains the single source of truth for preset-to-tool mapping logic.

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 →