# How pi-web Implements Chat Input Controls for Models, Tools, Thinking, and Compaction

> Discover how pi-web implements chat input controls. Learn about its React-powered dropdowns for models, tools, and thinking, plus conditional buttons for compaction, all managed by typed callbacks.

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

---

**pi-web implements chat input controls through the `ChatInput` component in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx), using React state-managed dropdowns for model selection, tool presets, and thinking levels, plus conditional buttons for compaction actions that communicate with the parent `ChatWindow` via typed callbacks.**

The π-Web repository provides a React-based chat interface with sophisticated input controls for managing AI model behavior. These **pi-web chat input controls** reside in the `ChatInput` component and handle four distinct interaction patterns: dynamic model selection, tool-preset toggling, thinking-level adjustment, and manual context compaction. Each control maintains local UI state while delegating persistent changes to the parent component and underlying RPC manager.

## Model Selection Controls

### State Management for Model Dropdowns

The model selector uses local React state to manage dropdown visibility and positioning. Two state variables control the interaction: `modelDropdownOpen` tracks whether the dropdown is visible, while `modelDropdownRect` stores the screen coordinates for absolute positioning.

The component constructs available options by prioritizing the `modelList` prop over the legacy `modelNames` mapping. According to the source code in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx), the option generation logic handles both data structures:

```tsx
// lines 48-58
const modelOptions: ModelOption[] = (() => {
  if (modelList && modelList.length > 0) {
    return modelList.map(m => ({provider: m.provider, modelId: m.id, name: m.name}))
                     .sort(compareModelOptions);
  }
  return Object.entries(modelNames ?? {})
                .map(([modelId, name]) => ({
                  provider: model?.provider ?? "unknown",
                  modelId,
                  name,
                }))
                .sort(compareModelOptions);
})();

```

### Rendering and Selection

The UI renders a button displaying the current `displayModelName` (lines 1700–1740). When clicked, it reveals a grouped dropdown organized by provider. Selecting an option triggers the `onModelChange(provider, modelId)` callback passed from `ChatWindow` and immediately closes the dropdown panel.

## Tool-Preset Selector Implementation

Tool presets are defined as static constants in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx). The `TOOL_PRESETS` array provides the available options, while `TOOL_PRESET_MAP` translates user-facing labels to internal values (`none`, `default`, `full`) on lines 77–79.

The selector appears when users click the "tools" icon, toggling the `toolDropdownOpen` state. Each preset button executes a handler that validates the selection before notifying the parent:

```tsx
// lines 20-26 (excerpt)
onClick={() => {
  setToolDropdownOpen(false);
  if (!isActive) onToolPresetChange(preset);
}}

```

The UI reflects the active state through the `isActive` boolean, displaying both the preset label (`lvl`) and description (`desc`) to provide context for the current tool configuration.

## Thinking-Level Configuration

Thinking levels are declared as a constant array with an accompanying description map (lines 88–98). The component receives three relevant props: `thinkingLevel` (current value), `thinkingLevelMap` (optional translation for UI labels), and `onThinkingLevelChange` (callback).

Similar to other dropdowns, this control uses `thinkingDropdownOpen` state to manage visibility. When a user selects a level, the handler invokes `onThinkingLevelChange(level)` with the selected internal key and closes the dropdown. The optional `thinkingLevelMap` allows the interface to display user-friendly names while maintaining consistent internal identifiers.

## Context Compaction Controls

### Start and Abort Compaction

Compaction functionality appears only when the `onCompact` callback prop is provided and the stream is inactive (`!isStreaming`). The implementation supports two distinct operational modes through a single button interface.

When `isCompacting` is false, clicking the button initiates compaction via `onCompact`. During active compaction (`isCompacting` is true), the same button transforms into an abort control that calls `onAbortCompaction`. The button includes dynamic accessibility attributes:

- **ARIA labels** switch between "Compact context" and "Stop compaction"
- **Tooltip text** (`title` attribute) updates to reflect current action
- **Error display** renders above the button when `compactError` is present (lines 40–49)

## Integration with ChatWindow and Server

`ChatWindow` instantiates `ChatInput` with session-aware callbacks that bridge UI interactions to server-side effects. When users trigger any control change, the corresponding callback (`onModelChange`, `onToolPresetChange`, `onThinkingLevelChange`, `onCompact`, or `onAbortCompaction`) executes, updating session state through [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts).

The RPC manager ([`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts)) persists these changes to the agent session, with updated values flowing back down to the UI via prop updates. This unidirectional data flow ensures the `ChatInput` component remains pure and predictable while supporting complex stateful operations like model switching and context compaction.

## Summary

- **pi-web chat input controls** are centralized in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) as a React functional component with local state management for dropdown visibility.
- Model selection supports dynamic option lists via `modelList` or legacy `modelNames` props, sorted using `compareModelOptions`.
- Tool presets use static constants (`TOOL_PRESETS`, `TOOL_PRESET_MAP`) with visual feedback through the `isActive` state.
- Thinking levels accept an optional mapping prop (`thinkingLevelMap`) to decouple internal keys from display labels.
- Compaction exposes dual callbacks (`onCompact`, `onAbortCompaction`) and error handling through the `compactError` prop.
- All control callbacks are forwarded from `ChatWindow` and processed server-side by [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts).

## Frequently Asked Questions

### How does pi-web handle model selection state?

pi-web manages model selection through React state variables `modelDropdownOpen` and `modelDropdownRect` in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx). The component calculates available models by checking the `modelList` prop first, falling back to `modelNames` if undefined, and sorting results with a custom comparator. Selection changes trigger `onModelChange(provider, modelId)`, which `ChatWindow` forwards to the RPC manager.

### What props does the compaction button require?

The compaction button requires two optional callback props: `onCompact` to initiate compression and `onAbortCompaction` to cancel ongoing operations. It also consumes `isStreaming` (to disable during generation), `isCompacting` (to toggle button state), and `compactError` (to display failure messages). These props are defined in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) lines 38–88.

### Where are tool presets defined in pi-web?

Tool presets are defined as static constants in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx). The `TOOL_PRESETS` array contains the available configurations, while `TOOL_PRESET_MAP` translates these into internal values (`none`, `default`, `full`). This mapping occurs on lines 77–79 of the source file.

### How is the thinking level mapped to UI labels?

pi-web supports thinking level localization through the optional `thinkingLevelMap` prop. This allows internal keys (defined in the constant array on lines 88–98) to display as user-friendly strings. When rendering the dropdown, the component looks up each level in this map, defaulting to the raw key if no translation exists.