How Pi Web Implements Extension UI Context Bindings (select, confirm, input, editor)
Pi Web implements extension UI context bindings by routing JSON-RPC requests from agents through an RPC manager to a React dialog queue, rendering modality-specific interfaces for select, confirm, input, and editor interactions before returning user responses to resume agent execution.
The agegr/pi-web repository provides a browser-based interface for AI agents that requires seamless user interaction without breaking execution flow. This article examines how the codebase implements extension UI context bindings to handle four core interaction patterns—select, confirm, input, and editor—allowing running skills to pause, collect user input, and resume operation.
Extension UI Request Architecture
The system relies on a bidirectional JSON-RPC channel where agents emit ExtensionUiRequest messages that the frontend renders as modal dialogs. When a skill or plugin needs user input, it dispatches a request with a specific method field—select, confirm, input, or editor—containing UI-specific parameters like title, message, options, placeholder, or prefill.
Request Creation in Agent Code
Agent-side code constructs these requests and awaits responses through an async wrapper. The payload structure varies by interaction type: select requests include an options array, while input and editor requests may specify placeholder text or prefill content.
// lib/skill-example.ts
import { sendExtensionUiRequest } from "./rpc-manager";
export async function askUserForName() {
const request = {
method: "input",
title: "Enter your name",
message: "What should we call you?",
placeholder: "Your name…",
prefill: "",
};
const { value } = await sendExtensionUiRequest(request);
return value; // e.g. "Alice"
}
RPC Routing and Session Management
Incoming RPC payloads arrive at lib/rpc-manager.ts, which forwards them to the active AgentSession. The session handler treats the payload as an ExtensionUiRequest and pushes it onto a dialog queue maintained in the application state. This queue drives the modal rendering logic in components/ChatWindow.tsx.
Rendering Extension Dialogs in ChatWindow
The ExtensionDialog component in components/ChatWindow.tsx (lines 667–720) serves as the primary renderer for all four interaction types. It initializes local state from the request properties—using prefill content for editor mode or empty strings for other modes—and manages the onRespond callback that transmits user actions back to the agent.
// components/ChatWindow.tsx (excerpt)
type ExtensionDialogRequest = Extract<
ExtensionUiRequest,
{ method: "select" | "confirm" | "input" | "editor" }
>;
function ExtensionDialog({
request,
onRespond,
}: {
request: ExtensionDialogRequest;
onRespond: (
req: ExtensionDialogRequest,
resp:
| { value: string }
| { confirmed: boolean }
| { cancelled: true }
) => void;
}) {
const [value, setValue] = useState(
request.method === "editor" ? request.prefill ?? "" : ""
);
const submitValue = () => {
if (request.method === "confirm") {
onRespond(request, { confirmed: true });
} else {
onRespond(request, { value });
}
};
// ...
}
UI Mode Implementations
Each binding type renders distinct controls but shares the same response pipeline through onRespond.
Select Bindings
Select bindings render each option as a full-width button within a CSS grid layout. Clicking an option immediately calls onRespond(request, { value: option }), transmitting the selection back through the RPC channel without additional confirmation steps.
{request.method === "select" && (
<div style={{ display: "grid", gap: 8 }}>
{request.options.map((option) => (
<button
key={option}
onClick={() => onRespond(request, { value: option })}
style={{
width: "100%",
padding: "9px 10px",
borderRadius: 7,
border: "1px solid var(--border)",
background: "var(--bg-panel)",
color: "var(--text)",
cursor: "pointer",
textAlign: "left",
fontSize: 13,
}}
>
{option}
</button>
))}
</div>
)}
Confirm Dialogs
For confirm requests, the component displays the provided message alongside an OK button. When clicked, onRespond fires with { confirmed: true }, allowing the agent to proceed with the approved action. The pattern appears in components/SessionSidebar.tsx (lines 660–678) for destructive actions like worktree removal.
Input Fields
Input requests generate a single-line text field. The component monitors for Enter key presses to trigger submitValue, which returns { value: string } to the awaiting agent. This provides a lightweight mechanism for collecting short text responses like filenames or search queries.
Multi-line Editors
The editor method creates a <textarea> for multi-line content entry. Unlike single-line input, submission requires Ctrl+Enter (or Cmd+Enter on macOS) to prevent accidental newlines. The component initializes the text area with request.prefill content when available, making it ideal for editing code blocks or configuration files.
Response Handling and Promise Resolution
Once onRespond executes, the callback traverses back through the RPC manager to resolve the original agent's Promise. This unblocks the skill's execution thread, passing the user-supplied data as the return value of the initial sendExtensionUiRequest call. The entire flow maintains type safety through TypeScript definitions that shape requests as ExtensionUiRequest and responses as discriminated unions of { value }, { confirmed }, or { cancelled } objects.
Summary
- Extension UI context bindings in Pi Web use JSON-RPC to bridge agent execution and React frontend components.
- The
lib/rpc-manager.tsfile handles request routing and Promise resolution between agents and the UI. components/ChatWindow.tsximplements theExtensionDialogcomponent that renders four distinct interaction modes: select, confirm, input, and editor.- Each method returns a specifically shaped response object—
{ value }for data entry,{ confirmed }for boolean approval, or{ cancelled }for dismissals. - User input triggers
onRespondcallbacks that resolve pending agent Promises, allowing skills to resume with collected data.
Frequently Asked Questions
What file handles the routing of extension UI requests in Pi Web?
The lib/rpc-manager.ts module receives JSON-RPC payloads from agents and forwards them to the active AgentSession. It maintains the Promise registry that connects user interactions back to the awaiting agent code.
How does Pi Web distinguish between single-line input and multi-line editor bindings?
The system checks the method field of the ExtensionUiRequest. When method equals "input", components/ChatWindow.tsx renders a single-line <input> element that submits on Enter. When method equals "editor", it renders a <textarea> that requires Ctrl+Enter (or Cmd+Enter) to submit, preventing accidental line breaks during text entry.
What response format does a select binding return to the agent?
Select bindings return an object shaped as { value: string } where the value property contains the exact string of the selected option. The agent awaits this response through the sendExtensionUiRequest Promise, which resolves immediately upon user selection.
Can agents prefill content in editor bindings?
Yes. Editor requests support an optional prefill field in the request payload. The ExtensionDialog component initializes the textarea state with this value, allowing agents to present existing content for user editing rather than starting with an empty field.
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 →