- name
- tui-verify-change
- description
- Verify a change to Warp's headless TUI front-end (crates/warp_tui) by running it — locally via ./script/run-tui, or in a headless cloud runner via a WARP_API_KEY dogfood build — and reading the rendered screen back (under tmux when it's installed, otherwise directly). Use whenever you change TUI UI, rendering, input, or behavior and need to confirm the real on-screen result.
# tui-verify-change
Verify a change to Warp's **headless TUI** front-end (`crates/warp_tui`, and the
cell-grid element library in `crates/warpui_core/src/elements/tui`) by running it
and reading back the actual rendered screen.
The whole point: **the TUI is a console program, so you can run it in a real
terminal and read the actual rendered screen straight back.** When `tmux` is
available it's the ideal driver — you run the TUI in a tmux pane, drive it with
`tmux send-keys`, and read the frame back with `tmux capture-pane` — but tmux is
**not required**: if it isn't installed you can still run and observe the TUI
directly (see **If tmux isn't installed** under Step 2). Either way you (the
agent) see the actual screen text — no `computer_use`, no real display, no cloud
screenshot agent, and no relying on a separate watcher's description of what it
saw. (Contrast the GUI-only `gui-onboarding-verification-skill`,
`gui-integration-test`, `gui-integration-test-video`, and the `computer_use` /
`verify-ui-change-in-cloud` flow.) This is the fast, preferred way to confirm a
TUI change end-to-end.
This skill covers the **manual live-run** verification. For a durable regression
that runs in CI, also add a render-to-lines unit test per `tui-testing` — the two
are complementary: the live run confirms real behavior; the snapshot test locks
it in.
## When to use
- You changed anything a TUI user sees or interacts with: an element/layout in
`crates/warpui_core/src/elements/tui`, a TUI view/screen in `crates/warp_tui`
(transcript, input, zero state, login placeholder, etc.), TUI keybindings, or
TUI rendering/behavior.
- You want to confirm the *actual* rendered result, not just that it compiles or
a unit test passes.
If your change is GUI-only (`app/`, WarpUI pixel `Element`/`View`), this is the
wrong skill — use the GUI verification path instead.
## Local vs cloud verification (pick your context first)
*How* you build, run, and log in depends on **where you're running**. Decide this
up front — it determines whether you use `./script/run-tui` or the `WARP_API_KEY`
path below.
- **Local context** — you're working in a local dev checkout, typically
alongside the user. Just run **`./script/run-tui`** directly: it builds
`warp_tui` and runs it, selecting the internal `local` channel when the
`warp-channel-config` generator is available and falling back to
`warp-tui-oss` otherwise. **Do not reach for `WARP_API_KEY`** here — it's
generally **not set** in a local environment, and you don't need it: a local
build reaches the authenticated state through the normal interactive
device-auth login flow (or you're already signed in on this machine). The
non-interactive `WARP_API_KEY` login below is a *cloud-runner* affordance, not
a local one, so don't let a missing key block you locally.
- **Cloud context** — you're a headless cloud agent (e.g. the factory-client
runner) with no browser for device-auth, so reaching a **signed-in** surface
relies on the non-interactive `WARP_API_KEY` already in the environment. That
does **not** mean bypassing `./script/run-tui`: when the `warp-channel-config`
generator is available, `./script/run-tui` selects the internal `local`
binary; otherwise it falls back to `warp-tui-oss`. Both accept the inherited
`WARP_API_KEY`, so prefer the maintained runner unless you need a specific
binary or profile. In the cloud, the inherited key signs you in without the
browser device-auth flow.
The logged-**out** surface (the `Sign in to continue` placeholder and any pure
element/layout) needs neither login path — a plain OSS build is enough in either
context.
## Step 1 — Build the TUI
Always build with a small job count so the large `warp` dependency tree doesn't
OOM the machine:
```bash
cd <warp-repo-root>
CARGO_BUILD_JOBS=2 cargo build -p warp_tui --bin warp-tui-oss
```
- `warp-tui-oss` is the OSS channel binary and the safest default (no internal
`warp-channel-config` generator required). `./script/run-tui` does the
equivalent, selecting the internal `local` binary when the generator is
available and falling back to `warp-tui-oss` otherwise.
- Fix all compile errors before running (see `fix-errors`). The first build of
this tree takes a while; **incremental rebuilds after a one-line change are
fast (~10s)**, so the edit → rebuild → re-capture loop below is quick.
### Logged-in vs logged-out (important)
The OSS build starts **logged out** and stops at a `Sign in to continue`
placeholder (it drives a device-authorization login flow that needs a browser).
The login-gated root has three pre-session states you may see:
- `AwaitingLogin` → a centered placeholder that reads `Sign in to continue`,
then `Opening your browser…` (or, once the device code is known, `Open <uri> in
your browser` and `and enter code: <code>`). It does **not** show a Ctrl-C hint.
- `LoggedIn` → briefly `Starting terminal…`, then the **zero state** (`Warp
Agent` + version, a "What's new" list, and the project context section) with the
input view.
- `Failed` → `Login failed: <message>` followed by `Press Ctrl-C to exit.`
(Exact strings live in `crates/warp_tui/src/ui.rs` — verify against it if you're
asserting on placeholder text.)
So: if your change is on the **login placeholder** or a pure element/layout, the
logged-out OSS build is enough. If your change is in the **live terminal /
transcript / input** surface, you must reach the authenticated state (see the
next section), or your change will sit behind the login gate and you'll only ever
see `Sign in to continue`.
### Logging in non-interactively (`WARP_API_KEY`) — cloud context
**This is the cloud path.** In a local checkout, skip it: run `./script/run-tui`
and log in interactively (see **Local vs cloud verification** above), since
`WARP_API_KEY` usually isn't set locally. Use the flow here when you're a
headless cloud runner where the key *is* set and there's no browser for
device-auth.
You can reach the authenticated (`LoggedIn`) state headlessly — no browser, no
device-auth flow — by launching any TUI channel binary with a `WARP_API_KEY` in
the environment. This is the fast way to verify live terminal/transcript/input
changes.
Key constraints:
- **All channels are supported.** API-key login works with `warp-tui-oss` as
well as the Preview, Dev, Local, and Stable binaries.
- **The key must already be in the environment.** In a sandbox where
`WARP_API_KEY` is set, a freshly started `tmux` server inherits it. Never echo,
print, or inline the secret value in a command — just rely on the inherited
environment variable. (`--api-key <key>` on the command line also works but
would expose the secret, so prefer the env var.)
```bash
cd <warp-repo-root>
CARGO_BUILD_JOBS=2 cargo build -p warp_tui --bin warp-tui-oss
tmux kill-session -t tuicheck 2>/dev/null
# WARP_API_KEY is inherited from the environment by the new tmux server.
tmux new-session -d -s tuicheck -x 120 -y 40 './target/debug/warp-tui-oss'
sleep 20 # login + session start
tmux capture-pane -t tuicheck -p # expect the logged-in zero state
```
When it works you'll see the **zero state** (`Warp Agent` + input view + model
selector) instead of `Sign in to continue`, and you can `send-keys` a real prompt
and read the agent's reply back with `capture-pane`. (This login path was added
in warpdotdev/warp#13583.)
## Step 2 — Run under tmux and read the frame back
The TUI needs a **real interactive PTY** — a tmux pane is exactly that, and it
lets you both send input and read the rendered screen back as text. Start the TUI
in a detached session with an explicit size (don't skip `-x`/`-y`; a degenerate
1-row pane renders nothing useful):
```bash
cd <warp-repo-root>
tmux kill-session -t tuicheck 2>/dev/null # clear any prior run
tmux new-session -d -s tuicheck -x 120 -y 40 './target/debug/warp-tui-oss'
sleep 1 # let it draw + probe the terminal
tmux capture-pane -t tuicheck -p # <-- the rendered screen, as text
```
`tmux capture-pane -p` prints the pane's current contents (the TUI's alternate
screen) to stdout, so **you read the real frame directly** and assert on it. Add
`-e` to include ANSI escape sequences when you need to check colors/styles:
```bash
tmux capture-pane -t tuicheck -p -e # includes color/style escapes
```
Drive interactions with `tmux send-keys`, sleeping to let the UI settle, then
capture again. For example (type a line, submit it, wait, then read the screen):
```bash
tmux send-keys -t tuicheck "What is 2+2? Answer in one short sentence." && \
sleep 1 && tmux send-keys -t tuicheck Enter && sleep 5 && \
tmux capture-pane -t tuicheck -p -e
```
Send special keys by name (`Enter`, `Escape`, `C-c` for Ctrl-C, `Up`/`Down`).
When done, tear the session down: `tmux kill-session -t tuicheck`.
### If tmux isn't installed
`tmux` is the preferred driver because it gives you programmatic `send-keys` +
`capture-pane`, but it is **not** a hard requirement — never block verification
just because tmux is missing. Check with `command -v tmux`; if it's absent, fall
back:
- **Local context:** the simplest path is to run `./script/run-tui` directly in a
real terminal and read the rendered output yourself — you already have a PTY.
When you're working alongside the user, you can also have them run it and report
what renders. Installing tmux is optional, not a prerequisite.
- **Cloud / no-tmux context:** run the built binary inside another PTY wrapper so
you can still capture output — e.g. `script` (util-linux):
`script -qe -c './target/debug/warp-tui-oss' /tmp/tui.log`, then read
`/tmp/tui.log`. If tmux is installable in your environment
(`apt-get install -y tmux`) and that's cheaper, do that and use the flow above
instead. If none of these work, run the binary directly, capture whatever
output you can, and **say so** in the PR/thread rather than implying a
tmux-driven capture.
Everything else in this skill (what to look for, the snapshot test, the evidence)
is identical whether or not tmux drove the run.
### Iterate loop
Because incremental rebuilds are ~10s, iterate tightly: edit the TUI code →
`cargo build -p warp_tui --bin warp-tui-oss` → `tmux kill-session` + restart the
session → `capture-pane` and compare. Verified before/after example: changing the
login placeholder string and rebuilding flips the captured line from
`Sign in to continue` to the new text, visible directly in `capture-pane` output.
## Step 3 — Check the captured frame against your change
You have the real screen text, so verify it yourself: grep/scan the
`capture-pane` output for the **exact string or layout** your change should
produce, and diff the before/after captures. No watcher interpretation needed —
if the expected text isn't in the capture, the change isn't rendering.
## Pitfalls (learned hands-on)
- **Run it in a real terminal / tmux pane** (a PTY), and drive + capture it via
tmux as above.
- **If it exits (code 101) right after the first frame instead of staying up:**
don't assume it's a terminal/stdin problem — **check the TUI log first**:
`tail -40 ~/.local/state/warp-terminal-tui/oz/warp-tui.log`. One cause seen in
the headless OSS/logged-out sandbox build is a debug-only binding-validation
panic: `crates/warpui_core/src/keymap/matcher.rs` (`validate_bindings`, gated on
`#[cfg(debug_assertions)]`) panics with `Bindings failed validation` when a
*keystroke* binding matches a TUI keymap context without being TUI-owned (it was
`app:reopen_closed_session`, Ctrl+Alt+T). It does **not** reproduce in every
setup — it depends on which keystroke bindings the running config loads, and the
validator exempts non-keystroke (palette/custom) triggers — so treat this as one
thing to check, not a guarantee. If you hit it, two ways to still verify a
change:
- **Build `--release`** — the validator is compiled out, so the TUI stays up
and you can `send-keys`/`capture-pane` freely:
`cargo build --release -p warp_tui --bin warp-tui-oss` then run
`./target/release/warp-tui-oss`.
- **Or poll `capture-pane` right after launch** on the debug build to grab the
first frame before the panic:
```bash
tmux new-session -d -s tuicheck -x 120 -y 40 './target/debug/warp-tui-oss'
for i in $(seq 1 15); do
frame=$(tmux capture-pane -t tuicheck -p | sed 's/[[:space:]]*$//' | grep .)
[ -n "$frame" ] && { echo "$frame"; break; }
sleep 0.2
done
tmux kill-session -t tuicheck 2>/dev/null
```
If the debug build stays up on your machine, you can `send-keys`/`capture-pane`
repeatedly without racing.
- **Alt screen is handled for you.** `capture-pane` reads the alternate screen,
so you don't need to fight escape-sequence soup the way piping stdout would.
- **Startup probe.** On launch the TUI emits terminal probes (OSC `10`/`11` +
a device-attributes query) to pick a theme; a normal tmux pane answers them.
Give it ~0.5–1s (a `sleep`) before the first capture.
- **Quitting.** Ctrl-C is `tmux send-keys -t tuicheck C-c`. On the login
placeholder one Ctrl-C exits; in a live session it's press-again-to-exit (first
press cancels/clears input and arms a ~1s window; a second within the window
exits). Always `tmux kill-session` at the end so a stray session doesn't linger.
## Step 4 — Capture screenshots and video (asciinema + agg)
`capture-pane` text is the fast inner-loop check (Step 3) and enough to *assert*
on a change. But for PR evidence — and for attaching durable image/video
artifacts — you often want an actual **screenshot** or a short **video** of the
rendered TUI. Because the TUI is a console program, capture it by recording its
PTY session with **asciinema**, rendering that recording with **agg**, and
transcoding it to an **MP4** (the same format Warp's `computer_use` screen
recording produces) with `ffmpeg`; pull a still frame out with `ffmpeg` too.
**Install the tooling (cloud runner — one-time).** asciinema and ffmpeg are
packaged; `agg` ships as a prebuilt binary rather than in apt:
```bash
sudo apt-get update && sudo apt-get install -y asciinema ffmpeg tmux
# agg is not in apt — install a PINNED release binary and verify its checksum before
# installing as root (don't pull an unpinned `latest`). The checksum below is for the
# x86_64 build; on another arch use that asset's published checksum from the release.
AGG_VERSION=v1.9.0
AGG_SHA256=f111e315cd71056b116302342553dd765b7297579ed511f111d0cedb442aeda6
curl -fsSL -o /tmp/agg \
"https://github.com/asciinema/agg/releases/download/${AGG_VERSION}/agg-$(uname -m)-unknown-linux-gnu"
echo "${AGG_SHA256} /tmp/agg" | sha256sum -c - # aborts on mismatch
sudo install -m 0755 /tmp/agg /usr/local/bin/agg
```
**Record the session.** asciinema needs a real PTY, so run it **inside tmux** (a
bare `asciinema rec` in a non-interactive runner shell fails with "not a
terminal"). Drive the TUI with `tmux send-keys` exactly as in Step 2 — the keys
reach the binary running under asciinema:
```bash
cd <warp-repo-root>
tmux kill-session -t tuicap 2>/dev/null # clear only THIS capture session (not kill-server)
# asciinema records the TUI's PTY; -c runs the binary; --overwrite replaces a prior cast.
tmux new-session -d -s tuicap -x 120 -y 40 \
'asciinema rec --overwrite -c "./target/debug/warp-tui-oss" /tmp/tui.cast'
sleep 1 # let it draw + answer the theme probe
# ...drive the interaction you want to show, e.g.:
# tmux send-keys -t tuicap "hello" Enter && sleep 3
# Quit the TUI so asciinema finalizes the cast. The logged-out placeholder exits on one
# Ctrl-C, but a live/logged-in session needs a SECOND press within its ~1s window (see
# "Quitting" under Pitfalls) — so send two; the extra press is a harmless no-op if it
# already exited (the session is gone, hence 2>/dev/null).
tmux send-keys -t tuicap C-c && sleep 0.5 && tmux send-keys -t tuicap C-c 2>/dev/null
sleep 1
```
For a **logged-in** capture, build/run `warp-tui-dev` with `WARP_API_KEY` per
Step 1 instead of `warp-tui-oss`.
**Render the video (MP4).** Match the format Warp's `computer_use` screen
recording uses — an **H.264 / yuv420p MP4** with `+faststart` (see
`crates/computer_use/src/linux/recording.rs`) — so TUI captures are consistent
with GUI/computer-use recordings. `agg` only emits GIF, so render to GIF and then
transcode to MP4 with those settings. libx264 + yuv420p require **even**
dimensions, so pad up by a pixel when the terminal render is odd-sized:
```bash
agg --cols 120 --rows 40 /tmp/tui.cast /tmp/tui.gif
ffmpeg -y -i /tmp/tui.gif -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" \
-c:v libx264 -pix_fmt yuv420p -movflags +faststart /tmp/tui.mp4
عرض على GitHub