# How Thinking Level Selection Works in ChatInput: A Deep Dive into the pi-web Implementation

> Explore how pi-web's ChatInput component manages thinking level selection. Learn about its static list, conditional UI, and backend RPC communication for model reasoning depth.

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

---

**The thinking level selector in pi-web's ChatInput component uses a static list of eight reasoning levels, renders a conditional dropdown UI, and transmits the user's choice through a React ref override to the backend RPC manager, which forwards it to the SDK to control model reasoning depth.**

The **thinking level** feature lets users control how much computational reasoning an AI model applies to each request. In the `agegr/pi-web` repository, this functionality is implemented across the React frontend and Node.js backend, with [`ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/ChatInput.tsx) serving as the primary user interface. Understanding this flow helps developers customize reasoning behavior or extend the level system for their own use cases.

## Static Level Definitions in ChatInput.tsx

The available thinking levels are defined as a TypeScript const array and mapped to internationalization keys. This definition anchors the entire feature.

```tsx
// components/ChatInput.tsx – lines 142-146
const THINKING_LEVELS = ["auto", "off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;

const THINKING_LEVEL_DESC_KEYS: Record<typeof THINKING_LEVELS[number], string> = {
  auto: "chat.thinkingUseDefault",
  off: "chat.thinkingOff",
  minimal: "chat.thinkingMinimal",
  low: "chat.thinkingLow",
  medium: "chat.thinkingMedium",
  high: "chat.thinkingHigh",
  xhigh: "chat.thinkingXhigh",
  max: "chat.thinkingMax",
};

```

The `auto` level delegates to system defaults, while `off` disables reasoning entirely. The graduated scale from `minimal` to `max` provides granular control over token consumption and latency.

## Conditional UI Rendering Based on Streaming State

The thinking level dropdown only appears when two conditions are met: the composer is **not actively streaming** a response, and the parent component has supplied an `onThinkingLevelChange` callback.

```tsx
// components/ChatInput.tsx – lines 2306-2365
{!isStreaming && onThinkingLevelChange && (
  <div ref={thinkingDropdownRef}>
    <button onClick={() => setThinkingDropdownOpen(v => !v)} …>
      {/* icon + current level label */}
    </button>
    {thinkingDropdownOpen && (
      <div className="thinking-dropdown-menu">
        {/* mapped list of THINKING_LEVELS */}
      </div>
    )}
  </div>
)}

```

This conditional rendering prevents users from changing reasoning levels mid-generation, which could create inconsistent behavior. The `thinkingDropdownRef` enables click-outside detection to close the menu.

## Dropdown Selection and Callback Invocation

Each dropdown item displays the level name, an optional custom label from `thinkingLevelMap` (provided by model configuration), and a translated description. Clicking an item triggers the parent's callback with conditional logic to avoid redundant updates.

```tsx
// components/ChatInput.tsx – lines 2365-2369
onClick={() => {
  setThinkingDropdownOpen(false);
  if (!isActive) onThinkingLevelChange(lvl);
}}

```

The `isActive` check prevents re-selecting the already-active level, reducing unnecessary state updates.

## Prop Drilling from ChatWindow to ChatInput

The callback originates in [`ChatWindow.tsx`](https://github.com/agegr/pi-web/blob/main/ChatWindow.tsx), which conditionally passes it based on session state:

```tsx
// components/ChatWindow.tsx – line 556
<ChatInput
  …
  onThinkingLevelChange={session || isNew ? handleThinkingLevelChange : undefined}
/>

```

If no session exists and this isn't a new conversation, the prop remains `undefined` and the UI element is suppressed. This guards against setting thinking levels in invalid application states.

## State Management in useAgentSession Hook

The `useAgentSession` hook in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) manages thinking level persistence and override behavior through three mechanisms:

**Default initialization:**

```tsx
// hooks/useAgentSession.ts – lines 289-297
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevelOption>("auto");

```

**Session restoration:**

```tsx
// hooks/useAgentSession.ts – lines 480-492
if (d.context.thinkingLevel && d.context.thinkingLevel !== "off") {
  setThinkingLevel(d.context.thinkingLevel as ThinkingLevelOption);
}

```

**Temporary override for next request:**

```tsx
// hooks/useAgentSession.ts – lines 302-315
const thinkingLevelOverrideRef = useRef<Exclude<ThinkingLevelOption, "auto"> | null>(null);

const handleThinkingLevelChange = (lvl: ThinkingLevelOption) => {
  thinkingLevelOverrideRef.current = lvl === "auto" ? null : lvl;
};

// Later, when constructing the request:
const extra = selectedThinkingLevel ? { thinkingLevel: selectedThinkingLevel } : {};

```

The `useRef` pattern creates a **temporary override** that applies only to the immediate next request, then clears. This design allows per-message reasoning adjustment without persisting the change to session defaults.

## Backend Transmission via RpcManager

The override value reaches the backend through `RpcManager.startRpcSession`:

```ts
// lib/rpc-manager.ts – lines 1560-1637
const { toolNames, initialModel, thinkingLevel } = options;

const session = await this.createAgentSession({
  …,
  ...(thinkingLevel ? { thinkingLevel } : {}),
});

```

The spread operator conditionally includes the key only when a non-auto level is specified, keeping request payloads minimal.

## SDK Integration and Model-Specific Mapping

The backend SDK applies thinking levels through configuration objects in [`lib/models-cache.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-cache.ts):

```ts
// lib/models-cache.ts (server-side)
interface ModelConfig {
  thinkingLevelPins: Record<string, string>;
  thinkingLevelMaps: Record<string, Record<string, string | null>>;
}

```

- **`thinkingLevelPins`**: Hardcoded mappings from level names to SDK-specific identifiers
- **`thinkingLevelMaps`**: Per-model overrides that rename or disable certain levels for specific architectures

The [`/api/models/route.ts`](https://github.com/agegr/pi-web/blob/main//api/models/route.ts) endpoint surfaces `thinkingLevelMap` to the frontend, enabling `ChatInput` to display custom labels like "Deep Think" instead of raw level names for compatible models.

## Complete Data Flow Summary

| Stage | File | Key Mechanism |
|-------|------|---------------|
| Level definition | `ChatInput.tsx:142-146` | Static const array `THINKING_LEVELS` |
| UI visibility | `ChatInput.tsx:2306-2365` | Conditional on `!isStreaming && onThinkingLevelChange` |
| User selection | `ChatInput.tsx:2365-2369` | `onThinkingLevelChange(lvl)` callback |
| Prop passing | `ChatWindow.tsx:556` | `handleThinkingLevelChange` handler |
| State persistence | `useAgentSession.ts:289-492` | `useState` + `useRef` override pattern |
| Backend transmission | `rpc-manager.ts:1560-1637` | Destructured `thinkingLevel` in session creation |
| SDK application | [`models-cache.ts`](https://github.com/agegr/pi-web/blob/main/models-cache.ts) | `thinkingLevelPins` and `thinkingLevelMaps` config |

## Summary

The **thinking level selection in ChatInput** operates through these interconnected mechanisms:

- Eight static levels from `auto` to `max` defined in `THINKING_LEVELS`
- Conditional dropdown rendering blocked during active streaming
- Callback-driven state updates through `handleThinkingLevelChange` in `useAgentSession`
- Temporary override pattern using `useRef` to apply levels per-request without persisting
- RPC transmission to backend SDK via `RpcManager.startRpcSession`
- Model-specific customization through `thinkingLevelMaps` configuration

This architecture separates UI concerns from state management and backend communication, making the feature extensible for additional levels or dynamic model-specific behavior.

## Frequently Asked Questions

### What happens when "auto" thinking level is selected?

Selecting `auto` sets `thinkingLevelOverrideRef.current` to `null`, causing the system to omit the `thinkingLevel` field from the next request entirely. The backend SDK then applies its default reasoning behavior based on the model's base configuration rather than any user override.

### Why is the thinking level dropdown hidden during streaming?

The dropdown condition `!isStreaming` prevents users from modifying reasoning parameters while a response is being generated. This maintains response consistency—changing reasoning depth mid-generation could produce incoherent outputs or violate SDK constraints on parameter immutability during active sessions.

### Can thinking levels be customized per AI model?

Yes. The `thinkingLevelMaps` field in [`lib/models-cache.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-cache.ts) allows per-model overrides. When the `/api/models` endpoint returns model metadata, any custom `thinkingLevelMap` is passed to `ChatInput`, which displays alternative labels or hides incompatible levels for specific model architectures.

### How does the temporary override differ from persisted session state?

The `useRef`-based `thinkingLevelOverrideRef` only affects the immediate next request, then clears automatically. This enables one-off reasoning adjustments without changing the session's default level stored in `useState`. The persisted state in `d.context.thinkingLevel` loads when sessions resume, providing continuity across page refreshes.