Introduction
gpui-form turns an application-owned Rust model into typed GPUI form state.
Derive GpuiForm, give each field one form intent, and use the generated
holder to validate and reconstruct the model.
This guide is for Rust application developers who use GPUI Kit. The workspace supports Rust 1.98. Use the published facade version shown in Getting started.
Mental model
A derived form separates the source model from the state owned by the view:
- The source struct defines the values the application accepts.
- Field intents decide which values have components, remain hidden, or stay under application control.
- Component shapes define construction, rendering, value binding, and holder storage.
- The generated holder collects edits, exposes validation, and reconstructs the source model.
For a source struct named UserProfile, the derive generates:
| Type | Purpose |
|---|---|
UserProfileFormField | Typed identity for component-backed fields |
UserProfileFormFields | GPUI entities owned by the form view |
UserProfileFormComponents | Constructors for those entities |
UserProfileFormValueHolder | Editable values, defaults, validation, and model conversion |
Choose a path
Start with Getting started to derive and wire a form. Use Field intents and generated types for hidden, skipped, or converted values. Continue to Component shapes to select widgets and Validation and conversion before submission.
MCP form tools describes structured submit and headless editing. Prototyping covers generating complete GPUI wiring from inventory metadata.
Getting started
This tutorial adds a typed form model to an existing GPUI application. At the
end, cargo check recognizes the generated UserProfileForm* types and the
application owns the holder and component entities needed to render the form.
Prerequisites
- Rust 1.98 or newer.
- A GPUI application that calls
gpui_kit::init(cx). - A
gpui_kit::component::Rootaround each first-level window view. gpui-kit0.6.1.
Add the dependencies
Use gpui-form for the derive and generated runtime paths. This example uses
ready-made collection shapes and the collection select derive:
[dependencies]
gpui-kit = "0.6.1"
gpui-form = "0.7"
gpui-form-collection = "0.7"
gpui-form-collection-derive = "0.7"
strum = { version = "0.28", features = ["derive"] }
Derive the form
Give every field one component(...), hidden, or skip intent:
use gpui_form::GpuiForm;
use gpui_form_collection_derive::SelectItem;
use strum::EnumIter;
#[derive(Clone, Debug, Default, EnumIter, PartialEq, SelectItem)]
enum Country {
#[default]
UnitedStates,
France,
Japan,
}
#[derive(Clone, Debug, Default, GpuiForm)]
struct UserProfile {
#[gpui_form(component(gpui_form_collection::input::Input::<_>))]
username: Option<String>,
#[gpui_form(component(gpui_form_collection::input::Input::<_>))]
age: Option<u32>,
#[gpui_form(component(
gpui_form_collection::select::Select::<_>,
default = Country::France
))]
country: Country,
#[gpui_form(component(gpui_form_collection::checkbox::Checkbox))]
subscribe: bool,
}
The default belongs to the component intent and seeds the generated holder.
Optional fields start as None. Direct-storage fields without an explicit
default use the form-side type’s Default implementation.
Own the generated state
Create the holder and component entities in the GPUI entity that renders the form:
let holder = UserProfileFormValueHolder::default();
let username = cx.new(|cx| UserProfileFormComponents::username(window, cx));
let age = cx.new(|cx| UserProfileFormComponents::age(window, cx));
let country = cx.new(|cx| UserProfileFormComponents::country(window, cx));
let subscribe = cx.new(|cx| UserProfileFormComponents::subscribe(window, cx));
let fields = UserProfileFormFields {
username,
age,
country,
subscribe,
};
The constructors create widget state. A complete view must also:
- Retain each
gpui_kit::Subscriptionon the owning GPUI entity. - Map component events through
gpui_form::runtime::shape::value_changeinto the holder. - Seed widget state with
gpui_form::runtime::shape::seed_value_binding_state. - Render each shape’s generated render component.
- Validate and convert the holder before invoking application submission code.
The inventory workflow in Prototyping generates this wiring from the same shape metadata.
Choose optional features
| Need | Configuration |
|---|---|
| Inventory metadata | gpui-form = { version = "0.7", features = ["inventory"] } |
| MCP tools in a GPUI application | gpui-form = { version = "0.7", features = ["mcp"] } |
| Headless MCP forms | gpui-form = { version = "0.7", default-features = false, features = ["derive", "mcp"] } |
| MCP schemas for Chrono or decimal values | Add chrono or rust_decimal beside mcp |
| Localized date, file, or infinite-select shapes | Add gpui-form-component with component-shape and, for the derive, derive |
Check the result
Run:
cargo check
A successful check confirms that the dependencies resolve, each field has an intent, and every selected shape supports its form-side value type.
Troubleshooting
| Symptom | Action |
|---|---|
field ... must choose a gpui_form field intent | Add component(...), hidden, or skip to the named field. |
A gpui_form_component shape is unavailable | Enable that crate’s component-shape feature; infinite-select enums also need its derive feature. |
| GPUI types from two dependencies do not match | Depend on gpui-kit alone, as shown above. |
| The widget renders but the holder never changes | Retain the subscription and map its event into the holder, or generate the wiring through the prototyping workflow. |
Field intents and generated types
Field intents decide which values render, which values stay in the generated holder, and which values the application must supply during reconstruction. Every non-empty-form field must choose exactly one intent.
| Intent | Generated component | Stored in holder | Reconstruction |
|---|---|---|---|
component(...) | Yes | Yes | Uses the holder value |
hidden | No | Yes | Uses the holder value |
skip | No | No | Caller supplies the source value |
Component fields
Use component(...) for fields edited by a GPUI widget:
#[gpui_form(component(gpui_form_collection::input::Input::<_>))]
name: String,
The shape owns construction, rendering, value binding, and the storage policy used by the generated holder. A configured shape expression can customize construction:
#[gpui_form(
component(gpui_form_collection::select::Select::<_>.searchable(true))
)]
country: Country,
Put default = ... inside component(...) when the holder should start with a
field-specific value. A component shape also defines how a non-optional field
stores an empty value; see Component shapes.
Hidden fields
Use hidden for values that participate in the holder and conversion without a
component:
#[gpui_form(hidden(default = request_context()))]
context: RequestContext,
Hidden fields are useful for non-visual values that may still come from a
structured client or application default. They do not create a
FormFields member or a FormComponents constructor.
Skipped fields
Use skip for source fields the form does not own:
#[gpui_form(skip)]
created_by: UserId,
Reconstruct a skipped-field model by passing those values to
holder.into_original(created_by, ...). If another field can fail conversion,
the same method returns Result<Model, Error>. holder.present_fields()
provides a typed snapshot of the editable fields for preview or diagnostic UI.
Do not combine skip with component or hidden on the same field.
Form-side value types
Use value(...) when a widget edits a type different from the source field:
#[gpui_form(component(
gpui_form_collection::input::Input::<_>,
value(
type = String,
from_source = format_account_id,
try_into_source = parse_account_id,
)
))]
account_id: AccountId,
Write the base form-side type in type = ...; source optionality determines
whether the holder stores an optional value. Both from_source and one reverse
conversion are required. Use into_source for an infallible reverse conversion
or try_into_source for a function returning Result<Source, Error> where the
error implements Debug.
For a struct-level #[koruma(newtype)], value(koruma_newtype) edits the inner
value and reconstructs the validated wrapper through Koruma’s public newtype
traits.
Generated types
For UserProfile, the generated types have distinct jobs:
| Type | Use |
|---|---|
UserProfileFormField | Identify component-backed fields; each variant’s name() returns the exact source field name. |
UserProfileFormFields | Store the GPUI entities owned by the form view. |
UserProfileFormComponents | Construct component state with methods named after source fields. |
UserProfileFormValueHolder | Store editable values and defaults, convert back to the model, and expose validation when Koruma integration is enabled. |
Hidden and skipped fields do not produce UserProfileFormField variants.
Validation and conversion
Validate the generated holder, then use the conversion method emitted for the form’s field contracts. Validation reports invalid editable values; conversion reconstructs the source model and can separately report a missing required value or a failed reverse conversion.
| Form contract | Generated conversion |
|---|---|
| No skipped fields; reconstruction is statically infallible | holder.into_original() returns the model. |
| No skipped fields; the derive emits a checked path | holder.try_into_original() returns Result<Model, Error>. This includes fallible conversions and shape-policy component fields without a declared field default. |
| One or more skipped fields | holder.into_original(skipped_value, ...) accepts those values and returns either the model or Result<Model, Error>, depending on the remaining fields. |
A non-optional shape-backed field can still start empty when its shape uses
required storage and no default = ... is declared. Fallible holder conversion
reports that missing value. When the form enables Koruma integration, generated
validate() reports it too.
The derive emits the checked conversion method for any non-defaulted shape-policy component because the policy is resolved through the shape type. A direct-storage policy always supplies a value, so that checked conversion cannot fail at runtime unless another field has a fallible conversion.
Validate with Koruma
Koruma validation can run directly against form-side values:
use gpui_form::GpuiForm;
use koruma::Koruma;
use koruma_collection::{collection::NonEmptyValidation, numeric::RangeValidation};
#[derive(Clone, Debug, GpuiForm)]
#[gpui_form(koruma)]
struct Registration {
#[gpui_form(component(gpui_form_collection::input::Input::<_>))]
#[koruma(NonEmptyValidation::<_>)]
username: String,
#[gpui_form(component(gpui_form_collection::number_input::NumberInput::<_>))]
#[koruma(RangeValidation::<_>.min(18).max(120))]
age: u8,
}
GpuiForm copies the field validators to RegistrationFormValueHolder and
derives its Koruma implementation. The Koruma import brings the validate()
method into scope; the source model only needs its own Koruma derive when the
application also validates that model directly. All configured validators run
in attribute order:
let holder = RegistrationFormValueHolder::default();
if let Err(errors) = holder.validate() {
if errors.username().non_empty_validation().is_some() {
eprintln!("username must not be empty");
}
}
Use #[gpui_form(koruma(fluent))] when the application has enabled the Koruma
Fluent features and installed its application-owned localizer. The generated
holder derives KorumaAllFluent and exposes localized failed-validator values
through all(); the source model does not need that derive unless it is also
validated directly.
Convert validated newtypes
For #[koruma(newtype)] source fields, use value(koruma_newtype) inside the
field intent. The generated holder edits the inner value and reconstructs the
validated wrapper through Koruma’s public newtype traits.
Call the fallible conversion method after validate(). Validation is still
useful even when conversion is checked because it can expose field-specific
validator errors instead of only the first reconstruction failure.
Troubleshooting
| Symptom | Cause and action |
|---|---|
validate() reports a required field with no Koruma attribute | The component shape uses required storage. Supply a value or add an intent-scoped default = .... |
try_into_original() fails after Koruma validation succeeds | A reverse conversion failed or a required value was still absent. Inspect the conversion error’s field name. |
| Fluent validator types do not compile or messages cannot be resolved | Enable koruma/fluent and the matching koruma-collection Fluent feature, then initialize the application’s Fluent resources. |
Component shapes
A component shape connects a form-side value to GPUI state, rendering, and events. Prefer a ready-made collection or component-owned shape. Define an application-owned shape when the existing contracts cannot represent the widget or construction behavior you need.
Choose the package
- Use
gpui-form-collectionfor common GPUI Kit controls. - Use
gpui-form-componentfor localized date and file pickers or cascading infinite selects. - Use
component-shape-gpuiandgpui-form-runtimewhen a crate defines its own reusable shape.
Applications that only consume existing shapes use the
gpui_form::runtime::shape facade and do not need a direct
gpui-form-runtime dependency.
Collection shapes
gpui-form-collection provides these form shapes:
| Need | Shape |
|---|---|
A FromStr + ToString + 'static value | input::Input::<T> |
| Application-defined text parsing and formatting | input::ParsedInput::<T, Config> |
| One enum-like value | select::Select::<T> |
| Several enum-like values | combobox::Combobox::<Item> |
| Boolean value | checkbox::Checkbox or switch::Switch |
| Numeric text input | number_input::NumberInput::<T> |
f32 or gpui_kit::component::slider::SliderValue | slider::Slider |
gpui_kit::Hsla | color_picker::ColorPicker |
chrono::NaiveDate or a date pair | date_picker::DatePicker or DateRangePicker |
| One-time-password value | otp_input::OtpInput::<T> |
ParsedInput<T, Config> uses a ParsedInputConfig<T> implementation for
parsing, formatting, placeholder text, empty-as-clear behavior, and optional
widget validation.
Select values normally derive SelectItem from
gpui-form-collection-derive and EnumIter from strum. The generic
parameter of Combobox<Item> is the item type, so a Vec<Country> field uses
Combobox::<Country>.
Configure one field
Built-in shapes expose builder expressions inside component(...):
#[gpui_form(component(
gpui_form_collection::select::Select::<_>.searchable(true)
))]
country: Country,
Use Shape.from(options) when the shape publishes a completed options type.
Configuration changes construction for that field while preserving the base
shape’s value compatibility, rendering, storage, and metadata.
Component-owned shapes
Enable the form-shape implementations and derives you use:
[dependencies]
gpui-form-component = { version = "0.7", features = ["component-shape", "derive"] }
| Need | Shape | Requirement |
|---|---|---|
| Localized date or date range | gpui_form_component::date_picker::DatePicker or DateRangePicker | Initialize application gpui-es-fluent resources |
| Native file or directory selection | gpui_form_component::file_picker::FilePicker | Initialize application gpui-es-fluent resources |
| Cascading enum choices | gpui_form_component::infinite_select::InfiniteSelect::<T> | Derive InfiniteSelect and implement Clone + Default + PartialEq + 'static throughout the enum tree |
Nested infinite-select payload types must implement Default. Use
InfiniteSelect::<_>.searchable(true) for search or
InfiniteSelect::<_>.from(InfiniteSelectOptions::new(true, Some(3))) for
search plus a maximum depth.
Define an application-owned shape
Declare an owned rendered component with
#[derive(component_shape_gpui::GpuiComponentShape)], or wrap an external
state/component pair with component_shape_gpui::component_shape!. Then attach
the form storage policy:
impl gpui_form_runtime::shape::GpuiFormComponentShapePolicy for EmailInputShape {
type ValueStoragePolicy =
gpui_form_runtime::shape::DirectValueStorage;
}
A reusable shape defines:
- backing entity state and construction
- a render component
- supported value types
- event-to-value binding when the form should synchronize automatically
- form holder storage policy
- a stable prototyping field suffix
- MCP input metadata when the wire contract needs shape-specific guidance
A shape declared only through a hand-written GpuiComponentShape
implementation lacks the declaration marker required by GpuiForm. Use the
derive or macro so the generated contract includes that marker and its
metadata.
Storage policy
DirectValueStorage stores T for a non-optional field. Initialization comes
from an intent-scoped default or the form-side type’s Default implementation.
RequiredValueStorage stores Option<T> so the form can represent missing
input. Holder conversion reports an absent required value. Generated
validation reports the same condition when Koruma integration is enabled.
Troubleshooting
| Symptom | Action |
|---|---|
A component-owned type cannot be used in component(...) | Enable gpui-form-component’s component-shape feature. |
| The derive rejects an application-owned shape as undeclared | Declare it with the GpuiComponentShape derive or component_shape! macro. |
| A configured shape expression fails its bound | Use a builder produced for the same base shape, or pass its completed options through Shape.from(...). |
| Prototyping reports missing capabilities | Add the missing render, value-binding, shape-path, or storage metadata to the owning shape and regenerate. |
MCP form tools
The mcp feature exposes concrete generated forms as structured submit tools
and headless edit sessions. The application still owns submission behavior;
gpui-form supplies typed decoding, validation, metadata, registration, and
session management.
Add MCP support
For a crate that also renders GPUI forms:
[dependencies]
gpui-form = { version = "0.7", features = ["mcp"] }
serde = { version = "1", features = ["derive"] }
For a headless form server, disable the default runtime and enable the derive explicitly:
gpui-form = { version = "0.7", default-features = false, features = ["derive", "mcp"] }
Add chrono or rust_decimal beside mcp when exposed fields or responses
use those values.
Define a form and handler
MCP forms must be concrete and inventory-backed. Opt in on the model and register a free submit function with one owned parameter:
use gpui_form::GpuiForm;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, GpuiForm, Serialize)]
#[gpui_form(mcp(name = "submit_contact", title = "Submit contact request"))]
struct ContactRequest {
#[gpui_form(hidden)]
email: String,
}
#[derive(Debug, gpui_form::mcp::McpJsonSchema, Serialize)]
struct ContactResponse {
accepted: bool,
}
#[gpui_form::mcp_submit]
async fn submit_contact(
request: ContactRequest,
) -> Result<ContactResponse, String> {
Ok(ContactResponse {
accepted: !request.email.is_empty(),
})
}
fn main() -> gpui_form::mcp::ServeStdioResult {
gpui_form::mcp::serve_stdio_blocking()
}
Handlers can be synchronous or asynchronous and return Result<T, E>, where
the response is serializable with an MCP schema and the error implements
Display. Prefer a response struct or newtype with an object-root schema.
Use the source model parameter for normal submission. When a form has skipped
fields, accept its generated *FormValueHolder so application code can supply
the skipped context.
Share application context
For several forms that submit through the same state:
- Add
context(MyContext)to each form’smcp(...)options. - Implement
McpContextSubmit<MyContext>for the model, or addsubmit(path::to::async_fn)so the derive emits the implementation. - Add
response(MyResponse)for a precise output schema. - Register all matching forms with
register_context_submitters(&mut server, context).
Use map_response(path::to::fn) when a raw application response needs a
fallible mapping into the published response. Use
submitter(path::to::Trait) when an application trait owns the associated
context, response, error, and submit method.
Choose registration behavior
| Need | API |
|---|---|
| Default inventory server over stdio | serve_stdio_blocking() |
| Application-owned server metadata | server_named(name, version) |
| Add generated forms to an existing server | register(&mut server) |
| Register shared-context submitters | register_context_submitters(...) |
| Select submit, editor, or metadata behavior | register_with_options(...) |
| Add generated prompt templates | register_prompt_templates(&mut server) |
Use per-form registration helpers when one form needs a manual handler or editor policy.
The MCP server retains editor sessions across calls until EOF, cancellation, idle expiry, or another application-owned shutdown signal. Tool completion does not request shutdown.
Edit a holder through MCP
Generated editor tools use an optimistic revision:
- Call
*_edit_openand retainsession_idandrevision. - Call
*_edit_patchwithvaluesand optionalclear. - Pass
expected_revisionwhen stale writes must fail. - Call
*_edit_validateand inspect form-level and field-level errors. - Call
*_edit_submitwhen the form has a submit handler. - Close abandoned sessions with
*_edit_close.
Bulk patches are atomic. Sessions have a default count limit and idle timeout;
configure them with McpFormEditorOptions and the corresponding
*_with_editor_options registration helper.
Editor sessions hold generated values on the MCP server. They do not mutate live GPUI entities; an application that shows remote edits must provide that bridge.
Use generated metadata
Registered forms publish descriptor, schema, and examples resources at:
gpui-form://forms/{tool_name}/descriptorgpui-form://forms/{tool_name}/schemagpui-form://forms/{tool_name}/examples
Prompt templates are opt-in and produce fill_*, repair_*, and submit_*
prompts that point clients to those resources and the applicable editor tools.
Field input schemas come from McpToolValue. Custom Serde types normally
derive gpui_form::mcp::McpJsonSchema. Component-backed fields also attach
value-specific shape MCP metadata when the shape publishes it. Koruma-backed
forms expose validation metadata and structured validation issues.
Troubleshooting
| Symptom | Action |
|---|---|
#[gpui_form(mcp)] rejects the form | Use a concrete form and keep inventory enabled; remove no_inventory. |
| Editor tools exist but the submit tool is absent | Add a #[gpui_form::mcp_submit] handler or register a manual/context submitter. |
| Registration reports a duplicate name or URI | Give each exposed form a unique mcp(name = "...") value. |
| A field fails schema generation | Derive McpJsonSchema or implement McpToolValue for the custom value type. |
| An editor patch reports a stale revision | Read the session again and retry against its current revision. |
Prototyping
Inventory prototyping turns concrete GpuiForm registrations into GPUI form
scaffolds. A generator should discover the expected form types, write the
configured output files, and format them with rustfmt; the workspace example
performs all three steps.
Enable inventory
Enable the inventory feature in the dependency graph that contains the form
models:
[dependencies]
gpui-form = { version = "0.7", features = ["inventory"] }
gpui-form-prototyping-core = "0.7"
The registry describes each concrete form, its field intents, component shapes, storage policy, rendering capability, value binding, holder conversion, and stable prototyping suffix.
The generator binary must link the crate that owns the forms so its inventory
registrations are present. In this example, replace app_models with that
crate’s Rust import name. Inspect the registry before writing files:
use gpui_form::schema::registry::{GpuiFormShape, inventory};
extern crate app_models;
for shape in inventory::iter::<GpuiFormShape>() {
println!("{}: {} fields", shape.struct_name, shape.fields.len());
}
Seeing the expected struct names is the first verification checkpoint. An empty
iterator usually means the generator does not link the model crate or the
inventory feature is absent from that build graph.
Generate scaffolds
Implement gpui_form_prototyping_core::FormLayout for the application’s target
view, then pass each registration to
gpui_form_prototyping_core::FormShapeAdapter::new(shape).generate_file(&layout).
The adapter produces the component construction, subscriptions, holder seeding,
rendering, validation, and conversion fragments used by the layout.
The workspace example is a complete generator. From this repository root, run:
cargo run -p prototyping
The command rewrites Rust scaffolds in
examples/some-lib-forms/src/forms and mirrors them under
examples/prototyping/output, then runs rustfmt. Success ends with
Form generation complete. for each output directory.
Keep registrations concrete
Generic form structs should use #[gpui_form(no_inventory)]; inventory entries
must describe a concrete form type.
Generated scaffolds are consumers of component-shape metadata. When generation reports missing render, value-binding, shape-path, or storage information, update the owning shape declaration and regenerate.
Troubleshooting
| Symptom | Action |
|---|---|
| No forms are discovered | Link the form-owning crate in the generator and enable gpui-form/inventory in the same dependency graph. |
| A generic form fails inventory registration | Add #[gpui_form(no_inventory)] to that generic form and register concrete form types instead. |
| Generation reports incomplete component capabilities | Add the missing render, value-binding, shape path, or storage metadata to the owning shape. The derive uses a generic shape suffix when the shape does not publish one. |
| Generation fails while formatting | Install the rustfmt component and rerun the generator. |