How ChatInput Handles Streaming and Tool Selection in pi-web
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:
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):
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:
if (isStreaming && (onSteer || onFollowUp)) {
sendQueued(onSteer ? "steer" : "followup");
}
Pressing Escape while streaming triggers the abort mechanism (lines 738–742):
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:
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:
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. The preset value then propagates to 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
// 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
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
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
isStreamingflag 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 viasendQueued(lines 744–751, 603–617). - Tool Presets: Constants
TOOL_PRESETSandTOOL_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
onToolPresetChangetorpc-manager.ts, wherestartRpcSession(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. 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →