# How ChatInput Handles Streaming and Tool Selection in pi-web

> Discover how ChatInput in pi-web manages streaming and tool selection. Learn about UI controls for steering and follow-up interactions, and backend tool allow-lists.

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

---

**The ChatInput component in pi-web prevents new messages and image uploads during active streaming responses while enabling "steering" and follow-up interactions, and exposes a tool preset dropdown that maps UI selections ("off", "default", "full") to backend tool allow-lists.**

The **ChatInput** component serves as the interactive text area powering the chat interface in the **pi-web** repository. It manages the complex interplay between asynchronous LLM streaming states and user input controls, implementing strict guards against conflicting actions while providing granular control over agent tool access through a configurable preset system.

## Streaming-Aware Interaction Guards

The component receives an **`isStreaming`** prop that indicates when an assistant response is in progress. This flag drives the majority of the streaming-sensitive UI logic, ensuring that users cannot interrupt or corrupt an ongoing inference session.

### Blocking Actions During Active Streams

When `isStreaming` evaluates to `true`, the component disables input mechanisms that would otherwise initiate new network requests or state changes.

**Image upload blocking** occurs in the `processImageFiles` callback at line 120 of [`components/ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/components/ChatInput.tsx):

```typescript
const processImageFiles = useCallback(async (files: File[]) => {
  if (isStreaming) return;       // Early exit during active stream
  // ... handle image reading logic
}, [isStreaming]);

```

**Message sending prevention** is enforced inside the `handleSend` function (lines 998–1000):

```typescript
const handleSend = useCallback(async () => {
  if (isStreaming) return;       // Prevent new sends while streaming
  // ... message dispatch logic
}, [isStreaming]);

```

### Steering and Follow-Up While Streaming

Rather than completely freezing the interface, the component allows **steering** (corrective guidance) and **follow-up** messages during active streams. These are handled via the `sendQueued` function (lines 603–617) rather than the standard send path.

When the user presses **Enter** during streaming, the `handleKeyDown` event handler (lines 744–751) detects the active stream and checks for `onSteer` or `onFollowUp` callbacks:

```typescript
if (isStreaming && (onSteer || onFollowUp)) {
  sendQueued(onSteer ? "steer" : "followup");
}

```

Pressing **Escape** while streaming triggers the abort mechanism (lines 738–742):

```typescript
if (e.key === "Escape" && !isComposing && isStreaming && onAbort) {
  onAbort();
}

```

## Tool Preset Selection System

The component exposes a dropdown interface that allows users to toggle between three distinct tool permission levels, directly influencing the agent's capabilities for the current session.

### Defining Tool Presets

The available presets are defined as constants at lines 70–71 of [`ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/ChatInput.tsx):

```typescript
const TOOL_PRESETS = ["off", "default", "full"] as const;
const TOOL_PRESET_MAP: Record<"off" | "default" | "full", "none" | "default" | "full"> = {
  off: "none",
  default: "default",
  full: "full"
};

```

The component maintains local state for dropdown visibility using `toolDropdownOpen` and `toolDropdownRef` to handle outside-click detection (lines 6–9).

### Mapping UI Selection to Backend Configuration

When a user selects a preset, the component resolves the human-readable label back to the API value. The `toolPresetLabel` derivation at lines 444–445 performs the reverse lookup:

```typescript
const toolPresetLabel = Object.entries(TOOL_PRESET_MAP)
  .find(([, v]) => v === (toolPreset ?? "default"))?.[0] ?? "default";

```

Selection changes invoke the **`onToolPresetChange`** callback, which bubbles up to the session manager in [`hooks/useAgentSession.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useAgentSession.ts). The preset value then propagates to [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), specifically the `startRpcSession` function (line 108), which constructs the `toolNames` array passed to the agent initialization. This creates a direct link between the UI dropdown and the backend's tool execution permissions.

## Practical Implementation Examples

### Guarding File Processing and Sends

```typescript
// components/ChatInput.tsx
const processImageFiles = useCallback(async (files: File[]) => {
  if (isStreaming) return;       // Streaming guard
  // Process file reading...
}, [isStreaming]);

const handleSend = useCallback(async () => {
  if (isStreaming) return;       // Streaming guard
  // Dispatch message...
}, [isStreaming]);

```

### Implementing Queued Actions

```typescript
const sendQueued = useCallback((mode: "steer" | "followup") => {
  if (!isStreaming) return;      // Only active during streams
  const msg = value.trim();
  if (mode === "steer" && onSteer) onSteer(msg);
  if (mode === "followup" && onFollowUp) onFollowUp(msg);
  clearInput();
}, [isStreaming, onSteer, onFollowUp, value]);

```

### Tool Preset Dropdown Component

```typescript
const TOOL_PRESETS = ["off", "default", "full"] as const;
const TOOL_PRESET_MAP = { off: "none", default: "default", full: "full" };

const toolPresetLabel = Object.entries(TOOL_PRESET_MAP)
  .find(([, v]) => v === (toolPreset ?? "default"))?.[0] ?? "default";

return (
  <div ref={toolDropdownRef}>
    <button onClick={() => setToolDropdownOpen(!toolDropdownOpen)}>
      Tools: {toolPresetLabel}
    </button>
    {toolDropdownOpen && (
      <ul>
        {TOOL_PRESETS.map(p => (
          <li key={p}>
            <button onClick={() => {
              onToolPresetChange(TOOL_PRESET_MAP[p]);
              setToolDropdownOpen(false);
            }}>
              {p}
            </button>
          </li>
        ))}
      </ul>
    )}
  </div>
);

```

## Summary

- **Streaming Guards**: The `isStreaming` flag blocks image uploads (`processImageFiles`, line 120) and new message sends (`handleSend`, lines 998–1000) to prevent interfering with active assistant responses.
- **Keyboard Control**: Pressing **Escape** triggers `onAbort` (lines 738–742), while **Enter** during streaming initiates steering or follow-up via `sendQueued` (lines 744–751, 603–617).
- **Tool Presets**: Constants `TOOL_PRESETS` and `TOOL_PRESET_MAP` (lines 70–71) define the UI options "off", "default", and "full", mapping to API values "none", "default", and "full".
- **Backend Integration**: Selections propagate through `onToolPresetChange` to [`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts), where `startRpcSession` (line 108) configures the agent's allowed tool list.

## Frequently Asked Questions

### How does ChatInput prevent users from sending messages during streaming?

The component checks the `isStreaming` prop at the beginning of the `handleSend` function (lines 998–1000) and returns early if a stream is active. This guard ensures that new messages cannot be dispatched until the current assistant response completes, preventing race conditions and state corruption.

### What is the difference between steering and follow-up in pi-web?

**Steering** allows users to send corrective guidance while the model is actively generating a response, while **follow-up** enables auxiliary messages that don't interrupt the stream. Both are handled by the `sendQueued` function (lines 603–617) when `Enter` is pressed during streaming, utilizing the `onSteer` and `onFollowUp` callbacks respectively instead of the standard message flow.

### How do tool preset selections affect the backend agent?

When a user selects a tool preset from the dropdown, the `onToolPresetChange` callback propagates the selection through the hook hierarchy to [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts). The `startRpcSession` function uses this value to filter which tools are included in the `toolNames` array passed during agent session initialization, directly constraining or expanding the agent's available capabilities.

### Can users upload images while waiting for a streaming response?

No. The `processImageFiles` function at line 120 of [`ChatInput.tsx`](https://github.com/agegr/pi-web/blob/main/ChatInput.tsx) contains an explicit guard that returns immediately if `isStreaming` is true. This prevents image processing and upload operations from competing with or interrupting the active text generation stream.