# How the ChatInput Component in pi-web Handles Message Composition with Model and Thinking Controls

> Discover how the ChatInput component in pi-web manages message composition, model selection, and thinking controls. Learn about React state, backend API integration, and real-time sync with useAgentSession.

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

---

**The ChatInput component orchestrates message composition by managing React state for text input, model selection, and thinking levels, then submits structured payloads to the backend Agent API while maintaining real-time synchronization via the `useAgentSession` hook.**

The ChatInput component in pi-web serves as the primary user interface for composing messages in this open-source chat application by agegr. Located at [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx), it coordinates the complex interaction between user input, LLM model selection, and adjustable "thinking" depth controls before dispatching structured data to the backend RPC manager.

## State Management and Session Integration

The component initializes three critical state variables using React hooks to capture the composition context:

- **inputValue**: Stores the current textarea content
- **modelId**: Tracks the selected language model, defaulting to the session's current model  
- **thinkingLevel**: Maintains the reasoning depth as a numeric pin from 0 to 3

All three states integrate with the `useAgentSession` hook, which provides the active session context and real-time update capabilities. As implemented in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx), the initialization logic references the session defaults while allowing per-message overrides:

```tsx
const [inputValue, setInputValue] = useState('');
const [modelId, setModelId] = useState<string | undefined>(session?.modelId);
const [thinkingLevel, setThinkingLevel] = useState<number>(session?.thinkingLevel ?? 0);

```

The `useAgentSession` hook, defined in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts), subscribes to the SSE stream at `/api/agent/[id]/events`, ensuring that any external changes to model configuration or thinking levels immediately reflect in the component's UI state.

## UI Controls for Model Selection and Thinking Configuration

The component renders a composite input interface that separates message content from behavioral configuration. The implementation in [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx) specifically handles keyboard interactions, model dropdowns, and reasoning sliders.

### Textarea Input with Keyboard Shortcuts

The main text capture area distinguishes between submission and line breaks using modifier key detection:

```tsx
const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    submitMessage();
  }
};

```

This allows users to press **Enter** to send or **Shift+Enter** to insert a newline, maintaining standard chat application UX patterns.

### Dynamic Model Selector

The model dropdown populates from the session's available models or the `/api/models` endpoint. The component renders a native select element that updates the `modelId` state on change:

```tsx
<select
  className="model-select"
  value={modelId ?? ''}
  onChange={e => setModelId(e.target.value)}
>
  {availableModels.map(m => (
    <option key={m.id} value={m.id}>
      {m.name}
    </option>
  ))}
</select>

```

### Thinking Level Slider

The thinking control implements a ranged input mapping to pi-web's 0-3 pin system, where higher values instruct the model to perform deeper reasoning chains:

```tsx
<input
  type="range"
  min={0}
  max={3}
  value={thinkingLevel}
  onChange={e => setThinkingLevel(Number(e.target.value))}
/>

```

## Message Construction and Submission Flow

When the user triggers submission, the `submitMessage` function assembles a conditional payload that only includes non-default values, minimizing request size while preserving explicit user choices:

```tsx
const submitMessage = async () => {
  if (!inputValue.trim()) return;
  
  const body: any = { message: inputValue };
  if (modelId) body.modelId = modelId;
  if (thinkingLevel) body.thinkingLevel = thinkingLevel;
  
  await fetch(`/api/agent/${sessionId}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  
  setInputValue('');
};

```

This implementation clears the textarea immediately after dispatching the request, providing instant UI feedback while the `useAgentSession` hook handles the asynchronous response stream separately.

## Backend Integration and Data Flow

The payload travels through a specific pipeline defined in the pi-web architecture.

### Validation Layer

According to [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), the backend receives the POST request at `/api/agent/[id]` and validates the `modelId` against the session's **enabledModels** scope, ensuring users cannot access unauthorized model endpoints.

### Scope Translation

The [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts) file translates the numeric `thinkingLevel` into model-specific configuration hints. For compatible models, this converts to a `:thinkingLevel=N` instruction appended to the system prompt, directly influencing the LLM's reasoning depth as implemented in the pi-web SDK integration.

### Real-time Synchronization

Because `useAgentSession` maintains an SSE connection to `/api/agent/[id]/events`, any backend-side changes to model parameters propagate back to the ChatInput component, keeping the UI selectors synchronized with the actual session state.

## Summary

- **ChatInput.tsx** implements a controlled React form with separate state management for message content, model selection, and thinking depth
- The component integrates with **useAgentSession** from [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts) to read initial values and subscribe to real-time session updates via Server-Sent Events
- **Model selection** populates from session-available models or the `/api/models` endpoint, with the chosen `modelId` conditionally included in POST payloads to `/api/agent/[id]`
- **Thinking controls** utilize a 0-3 range slider that translates to pin values processed by [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts) into LLM-specific reasoning instructions
- The submission flow clears input immediately for UX responsiveness while the session hook manages asynchronous response streaming separately from the input component's lifecycle

## Frequently Asked Questions

### How does the ChatInput component prevent empty message submissions?

The `submitMessage` function explicitly checks `inputValue.trim()` before constructing the fetch payload. If the trimmed value is empty, the function returns early without dispatching the API call, preventing empty messages from entering the conversation stream.

### What happens when a user changes the model mid-conversation?

The `modelId` state updates immediately via the `onChange` handler, but the change only persists to the backend when the user sends the next message. The component includes the current `modelId` in the POST body only if it has a value, allowing per-message model overrides while the `useAgentSession` hook ensures the UI remains synchronized with the authoritative session state.

### Why does the thinking level use a 0-3 range instead of continuous values?

According to the implementation in [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts), the numeric pins map to discrete reasoning modes within the pi-web SDK. Values 0 through 3 represent specific prompt modifications (such as `:thinkingLevel=0` for minimal reasoning up to `:thinkingLevel=3` for extended chain-of-thought), ensuring consistent behavior across different LLM providers that support structured thinking parameters.

### How does the component maintain synchronization with backend session state?

The `useAgentSession` hook establishes an EventSource connection to `/api/agent/[id]/events`. When the backend updates session parameters, the SSE pushes these changes to the hook, which updates the session object passed to ChatInput. The component's state initializers reference this session object, causing the model selector and thinking slider to reflect the current authoritative state on subsequent renders.