Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guidelines for writing Warp headless TUI (crates/warp_tui) UI code with the cell-grid TuiElement library. Read before any TUI UI work.
.claude/skills/warpdotdev-tui-ui-guidelines/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 14% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -7% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 2% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 36% | 0% |
Guidelines for writing UI code in Warp's headless TUI front-end. This is the TUI counterpart to gui-ui-guidelines (which covers the pixel-based GUI desktop app). Read this once at the start of any TUI UI task, then keep it in mind while implementing.
The TUI is a distinct front-end from the GUI desktop app. Do not carry over GUI assumptions (pixels, mouse-pixel hit-testing, GPU/WGSL, .app bundles, design-system button pixel themes, launch modals). If a GUI guideline is about pixel layout or GPU rendering, it does not apply here.
crates/warp_tui — per-channel console binaries (e.g. crates/warp_tui/src/bin). Run/observe the TUI with ./script/run-tui. There is no .app bundle, no GPU/WGSL, and no mouse-pixel model.crates/warpui_core/src/elements/tui, behind the tui cargo feature. This is a parallel cell-grid element vocabulary, separate from the GUI Element/View library.Shared with the GUI (do reuse): the Entity/model core in warp_core/warpui — App/Entity/AppContext/ViewContext, the actions system, Appearance/theming, FeatureFlag runtime checks (FeatureFlag::X.is_enabled() works in both front-ends), telemetry, and logging.
Different from the GUI (do NOT use here): the GUI Element/View types, pixel geometry, and GPU/WGSL rendering or pixel-drawn button themes. The TUI has its own crates/warp_tui/Cargo.toml; the compile-time Cargo-feature bridge in app/Cargo.toml + app/src/lib.rs enabled_features() is GUI-app-specific and does not gate TUI code. (The TUI does have hover/click: TuiHoverable and tui_collapsible reuse the shared MouseStateHandle, so own that handle outside render just like the GUI — only pixel-based hit-testing is GUI-only.)
TuiElement traitDefined in crates/warpui_core/src/elements/tui/mod.rs. An element measures itself, then paints into a sub-rectangle of a cell buffer:
layout(&mut self, constraint: TuiConstraint, ctx: &mut TuiLayoutContext, app: &AppContext) -> TuiSize — measure against a constraint and return a size within it. app gives shared read access to the core (mirrors the GUI's Element::layout).render(&self, area: TuiRect, buffer: &mut TuiBuffer, ctx: &mut TuiPaintContext) — paint into area of buffer. area's size is what layout returned, clamped to what was available.cursor_position(&self, area, ctx) -> Option<(u16, u16)> — where the terminal cursor should sit within area, if this element owns it (default: None).present(&mut self, ctx) — participate in the child-view recursion so the presenter records parent/child view relationships (default: nothing; only container/child-view elements override it).dispatch_event(&mut self, event, area, event_ctx, ctx, app) -> bool — offer an event to this element, returning whether it was handled (default: false)..finish() — boxing convenience that returns Box<dyn TuiElement>, mirroring the GUI Element::finish. Always terminate an element with .finish(); never hand-wrap an element in Box::new. It's what the child-taking APIs (TuiFlex::child/with_child, TuiChildView, etc.) expect, and it keeps element trees consistent and readable.Re-exported from crates/warpui_core/src/elements/tui/mod.rs:
TuiFlex (TuiFlex::row() / TuiFlex::column(), with .child(...), .flex_child(...), .with_cross_axis_alignment(...)), TuiContainer, and TuiConstrainedBox (e.g. .with_max_cols(N)).TuiText (.with_style(style), .truncate(), TuiText::from_spans([...])).TuiChildView for embedding another view's rendered element; TuiEventHandler (e.g. .on_key("x", |_, _, _| ...)) to attach handlers to a subtree.TuiParentElement provides with_child / with_children / add_child / add_children.TuiSize, TuiRect, TuiConstraint (TuiConstraint::loose(size) / TuiConstraint::tight(size); TuiConstraint::clamp). Also TuiPoint.Styles are TuiStyle values (Color, Modifier — e.g. Modifier::BOLD, Modifier::DIM) painted into a TuiBuffer of Cells. Terminal cells have no alpha, so styles are solid.
Prefer the semantic style helpers on TuiUiBuilder (crates/warp_tui/src/tui_builder.rs) over hardcoding colors — this mirrors the GUI guideline about reusing themes. Construct it per render with TuiUiBuilder::from_app(app), then ask for semantic styles: primary_text_style(), muted_text_style(), dim_text_style(), error_text_style(), success_glyph_style(), accent_border_style(), input_text_style(), etc. The builder owns the theme→style recipes so views ask for "primary text" / "muted text" instead of deriving colors from the theme by hand. Do not reach for raw ANSI slots (e.g. Color::White) directly — those are tuned for dark backgrounds and wash out on light themes.
Crossterm input is converted (in crate::runtime) to TuiEvent and dispatched through the element tree via dispatch_event; text-cursor placement flows through cursor_position.
Keybindings follow the GUI convention: each TUI view module exposes a top-level init(app) that registers its bindings, aggregated in crates/warp_tui/src/keybindings.rs and called once at TUI startup. Fixed/reserved bindings (e.g. ctrl-c) are tagged with the tui group (TUI_BINDING_GROUP); editable, user-remappable bindings are named with a tui: prefix. GUI bindings never fire in the TUI — predicate-scoped bindings never match TUI keymap contexts, and predicate-less ones dispatch action types no TUI view handles — and debug-time validators (register_binding_validators) enforce that any keystroke binding matching a TUI view's context is TUI-owned.
A TuiFlex::column() of styled TuiText children, wrapped in a width cap (illustrative):
rust path=null start=nulllet builder = TuiUiBuilder::from_app(app); let title_style = builder.accent_border_style().add_modifier(Modifier::BOLD); let muted = builder.muted_text_style(); let column = TuiFlex::column() .child( TuiText::new("Warp Agent") .with_style(title_style) .truncate() .finish(), ) .child(TuiText::new(version).with_style(muted).truncate().finish()); TuiConstrainedBox::new(column.finish()) .with_max_cols(48) .finish()
Verify API names against the element library (crates/warpui_core/src/elements/tui/mod.rs) and TuiUiBuilder (crates/warp_tui/src/tui_builder.rs); don't invent methods. Don't treat existing crates/warp_tui view code as canonical examples — much of it is early prototyping and isn't the pattern to copy going forward.
./script/run-tui../script/run-tui) and observing the output in an interactive terminal; the tui-verify-change skill covers this end to end.tui-testing skill.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 17,136 | 10,392 | -39% | 1 | 1 | 0% | 3,489 | 3,977 | +14% | 0 | 0 | — |
case-02 | fail→pass | 29,194 | 18,377 | -37% | 1 | 1 | 0% | 5,829 | 5,445 | -7% | 0 | 0 | — |
case-03 | fail→pass | 19,726 | 10,171 | -48% | 1 | 1 | 0% | 3,926 | 4,022 | +2% | 0 | 0 | — |
case-04 | pass→fail | 24,266 | 15,873 | -35% | 1 | 1 | 0% | 4,596 | 4,648 | +1% | 0 | 0 | — |
case-05 | fail→fail | 21,283 | 12,379 | -42% | 1 | 1 | 0% | 3,621 | 4,165 | +15% | 0 | 0 | — |
case-06 | pass→pass | 18,629 | 9,433 | -49% | 1 | 1 | 0% | 3,234 | 3,526 | +9% | 0 | 0 | — |
case-07 | fail→pass | 7,868 | 5,696 | -28% | 1 | 1 | 0% | 1,301 | 2,728 | +110% | 0 | 0 | — |
case-08 | fail→pass | 11,594 | 3,408 | -71% | 1 | 1 | 0% | 1,862 | 2,540 | +36% | 0 | 0 | — |
case-09 | fail→pass | 16,326 | 3,311 | -80% | 1 | 1 | 0% | 2,465 | 2,502 | +2% | 0 | 0 | — |
case-10 | fail→pass | 11,855 | 5,144 | -57% | 1 | 1 | 0% | 1,947 | 2,844 | +46% | 0 | 0 | — |
case-11 | fail→pass | 14,605 | 12,225 | -16% | 1 | 1 | 0% | 1,961 | 2,966 | +51% | 0 | 0 | — |
case-12 | fail→pass | 16,446 | 6,509 | -60% | 1 | 1 | 0% | 2,562 | 3,125 | +22% | 0 | 0 | — |
case-13 | fail→pass | 14,347 | 4,130 | -71% | 1 | 1 | 0% | 2,058 | 2,398 | +17% | 0 | 0 | — |
case-14 | fail→pass | 18,095 | 2,876 | -84% | 1 | 1 | 0% | 2,792 | 2,381 | -15% | 0 | 0 | — |
case-15 | fail→pass | 15,838 | 2,117 | -87% | 1 | 1 | 0% | 2,621 | 2,271 | -13% | 0 | 0 | — |
case-16 | fail→pass | 14,276 | 4,559 | -68% | 1 | 1 | 0% | 2,387 | 2,926 | +23% | 0 | 0 | — |
case-17 | fail→pass | 14,637 | 4,740 | -68% | 1 | 1 | 0% | 2,261 | 2,731 | +21% | 0 | 0 | — |
case-18 | pass→pass | 13,990 | 3,599 | -74% | 1 | 1 | 0% | 2,138 | 2,513 | +18% | 0 | 0 | — |
case-19 | fail→pass | 16,195 | 3,671 | -77% | 1 | 1 | 0% | 2,419 | 2,486 | +3% | 0 | 0 | — |
case-20 | fail→pass | 16,168 | 5,906 | -63% | 1 | 1 | 0% | 2,576 | 2,986 | +16% | 0 | 0 | — |
case-21 | fail→pass | 15,012 | 2,295 | -85% | 1 | 1 | 0% | 2,378 | 2,293 | -4% | 0 | 0 | — |
case-22 | fail→pass | 22,397 | 2,434 | -89% | 1 | 1 | 0% | 2,333 | 2,312 | -1% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +77 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.