npx skillsauth add laststance/skills qa-tuiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
When running this skill in Codex, translate Claude Code-only primitives before acting: AskUserQuestion -> chat/request_user_input, TodoWrite -> update_plan, Task/TaskCreate/TeamCreate/SendMessage -> spawn_agent/send_input/wait_agent when available and allowed, and EnterPlanMode/ExitPlanMode -> a concise chat plan plus explicit approval.
Resolve Read/Write/Edit/Bash/WebSearch/WebFetch to Codex file/shell/web tools, and map ~/.claude/... paths to ~/.agents/... or ~/.codex/... unless the task explicitly targets Claude Code.
When running this skill in Cursor Agent, translate Claude Code-only primitives before acting: AskUserQuestion -> AskQuestion; TodoWrite -> Cursor TodoWrite or an equivalent checklist; Task/TaskCreate/TeamCreate/SendMessage/multi-agent flows -> Cursor Task (subagents), parallel Tasks, or run_in_background when allowed (TeamCreate/SendMessage may have no exact match); EnterPlanMode/ExitPlanMode -> Plan mode (SwitchMode / CreatePlan) plus explicit user approval.
Resolve Read/Write/Edit/StrReplace/Bash/web/search/MCP via Cursor Composer or Agent equivalents. MCP names written as mcp__server__tool typically map to call_mcp_tool with configured server identifiers. Map ~/.claude/... to ~/.cursor/skills/, .cursor/skills/, and .cursor/rules/ unless the task explicitly targets Claude Code.
Drive a TUI program inside a shellwright-managed PTY, collect evidence (raw bytes + rendered screenshots + recordings), grade issues, and produce a report. Fixing is out of scope — this skill reports. If the user wants to fix bugs, hand the report to a session that has access to the TUI's source.
A TUI is a program that draws its UI with escape sequences on a PTY. That makes it harder to test than a web or desktop app for three reasons:
C-x C-c). The skill has to know how to drive these.Shellwright solves the driving problem by giving us a managed PTY with
shell_send (send keystrokes) + shell_read (read the byte stream) +
shell_screenshot (render the current terminal state to an image) +
shell_record_start/stop (capture a session replay). This skill codifies
the systematic path that catches the most common classes of TUI bugs.
In scope: Black-box testing of a TUI program driven through a shellwright PTY on the current host — sending keystrokes, reading the rendered buffer, capturing screenshots, noting what breaks under resize / non-standard TERM / NO_COLOR / narrow widths.
Out of scope:
grep, curl, ls) — use /qa patterns for those,
or spot-check manually. The skill assumes the program draws an
interactive UI, not just prints output and exits.flamegraph, samply, perf — out of scope)If any of these are unknown, use AskUserQuestion to collect them:
nvim, lazygit, htop -d 10, k9s --context prod,
./target/release/my-tui --config test.toml.:q, q, Ctrl+C, Esc, Ctrl+X Ctrl+C. Knowing this up front prevents leaving orphan processes.echo $TERM, echo $COLORTERM, emulator name
(iTerm2, Kitty, etc.), PTY dimensions (tput cols, tput lines). These
go in the report.stty sane or reset.Do not guess. Launching the wrong command or not knowing the quit key causes the skill to get stuck or kill the wrong process.
Goal: clean PTY, TUI launched, baseline screenshot + byte log captured, recording started for the full session.
Record the pre-launch context (for the report's environment section):
echo "TERM=$TERM"
echo "COLORTERM=$COLORTERM"
tput cols; tput lines
uname -srm
Start the shellwright session:
mcp__shellwright__shell_start
Note the session ID — all subsequent commands target it.
Start recording the full session (for the report's appendix):
mcp__shellwright__shell_record_start
Save path goes in /tmp/qa-tui-session/recording.cast (asciicast) or
similar — whatever shellwright returns. Capture it.
Launch the TUI by sending the launch command + Enter:
mcp__shellwright__shell_send({ keys: "<launch-command>\r" })
Then wait ~1 second for the TUI to initialize its first frame. Some heavier TUIs (k9s, lazygit with large repos) take 2–5 seconds.
Baseline screenshot + byte read:
mcp__shellwright__shell_screenshot → /tmp/qa-tui-session/00-baseline.png
mcp__shellwright__shell_read → /tmp/qa-tui-session/00-baseline.txt
The screenshot is what the user sees. The byte read is what the TUI emitted. Both are useful — keep both per surface.
Confirm the TUI is interactive: send a no-op key the TUI should swallow
(e.g., h for help in many TUIs) and verify the buffer changed with
another shell_read. If nothing changes, the launch may have failed
silently — check the buffer for a shell prompt or error.
If the launch fails, STOP and report. Common causes: binary not in PATH, required env var missing, TUI requires a minimum terminal size the PTY doesn't meet (typical: 80×24).
Goal: inventory every screen / pane / mode before testing any of them.
A TUI's surfaces:
/ in many TUIs), fuzzy picker (fzf-style)Walk them by:
? or h or F1).
Screenshot it — this is the TUI's own documentation of what the user
can do. It also doubles as the checklist for Phase 3 (exercise every
keybind listed).q, some only with a specific confirmation. If a popup has no
documented dismiss key, that's a finding.Budget: 2–5 minutes. If the help screen lists >50 keybinds, ask the user which feature sets matter for this run.
For each surface from Phase 1:
references/terminal-conventions.md — minimum 80×24
support? Graceful degradation below that? Consistent color semantics
(red for error, green for success, yellow for warning)?shell_read returns the raw stream the TUI emitted. Look for:
^[[31m
where \x1b[31m should have gone — indicates the TUI emitted an
escape with a control-char that the emulator didn't recognize^[[M, ^[[<0;...)
when the TUI doesn't support mouse but also doesn't disable mouse
reporting\x1b[2J) on every
keystroke is usually a bug; incremental draws (\x1b[H + deltas) are
correct\x1b[38;2;r;g;bm) breaks in 256-color terminalsEnumerate every keybind from the help screen (or the TUI's man page /
README) and fire each:
mcp__shellwright__shell_send({ keys: "<key-sequence>" })
mcp__shellwright__shell_screenshot → <Nth>-<action>.png
mcp__shellwright__shell_read → check for error output
For each keybind, verify:
j moves down, k moves up
the same distanceSpecial keys to always test:
fg, does the TUI redraw
cleanly? Many don't — they leak the pre-suspend screen.echo hi and confirm it
echoes normally).Shellwright's shell_send takes a keys string. Typical conventions:
"abc""\r" (or sometimes "\n" — check shellwright behavior on first
use)"\u0003" for Ctrl+C, "\u001b" for Esc, "\u0009"
for Tab"\u001b[A" Up, "\u001b[B" Down,
"\u001b[C" Right, "\u001b[D" LeftSee references/shellwright-tui-reference.md for the full key-code table
and quirk list.
Force the conditions the terminal emulator / shell controls:
| State | How to force | What to check |
|-------|--------------|---------------|
| Narrow width | Resize PTY to 60 cols (if shellwright supports) or launch with COLUMNS=60 stty cols 60 in the session | Layout adapts, no overflow, minimum-size message if below floor |
| Tall / wide | Start a large PTY (120×40) | No wasted gaps, tables expand to fill |
| Resize mid-session | Change PTY size after launch | TUI handles SIGWINCH — redraws cleanly, no lingering ghost content |
| TERM=dumb | TERM=dumb <launch-command> | Program degrades gracefully or prints a readable error — doesn't crash |
| TERM=xterm vs xterm-256color | Set TERM to each, relaunch | Colors adapt; 16-color fallback works if 256 isn't available |
| NO_COLOR=1 | NO_COLOR=1 <launch-command> | All color disabled, output still legible (spec: https://no-color.org/) |
| LANG=C / ASCII-only | LANG=C <launch-command> | Box-drawing chars fall back to ASCII, no mojibake |
| Piped stdin | echo foo \| <launch-command> | TUI refuses politely ("not a TTY") or adapts — doesn't segfault |
| Background / SIGSTOP | Send Ctrl+Z, then fg | Full redraw on resume |
| Long idle | Wait 5 minutes, then send any key | No memory growth, no reconnection delay (for network TUIs), no screen corruption |
At minimum: narrow width + resize mid-session + NO_COLOR + TERM=dumb.
These catch the vast majority of terminal-compat bugs.
shell_send, then shell_read. Is the output
coherent or does it show redraw artifacts?See references/issue-taxonomy-tui.md for the full category list.
Deliberately stress the exit paths:
echo ok at the prompt and confirm it echoes
normally. If characters don't echo, raw mode was left on — critical.kill <pid>. Same questions.kill -9 <pid>. The TUI can't catch SIGKILL, so this
simulates a crash. Afterward, use stty -a or try typing into the
shell. If line discipline is broken, the program should have used
atexit or equivalent to restore — and didn't. Note as critical.After each, if the terminal is in a bad state, restore with stty sane or
reset and note what was needed. (The user may need to do the same after
a real crash.)
Take every issue collected across phases and classify:
references/issue-taxonomy-tui.md)rendering / keybind / terminal-compat /
resize / signal-exit / state-config / unicode / performance
/ accessibility / crashDe-duplicate aggressively. If the same rendering glitch appears in every pane because a shared component misrenders wide chars, that's one issue with a list of affected panes — not five.
Use templates/qa-report-template-tui.md as the skeleton. Fill in:
<cmd> --version works, language/runtime
if known)$TERM, $COLORTERM, PTY size)Save the report to ./qa-reports/tui-<date>-<app>.md relative to the
user's current directory. Save all screenshots, byte dumps, and the
asciicast recording to ./qa-reports/tui-<date>-<app>/.
Don't embed full byte dumps in the report — they can contain long escape sequences and blow up the file. Save them alongside and link. Only embed the specific snippet relevant to an issue.
Always, even if the report isn't finished:
Stop recording:
mcp__shellwright__shell_record_stop
Quit the TUI via its documented quit key (safer than killing the session from outside):
mcp__shellwright__shell_send({ keys: "<quit-key>" })
If the TUI didn't quit cleanly, send Ctrl+C, then exit to the shell:
mcp__shellwright__shell_send({ keys: "\u0003" })
mcp__shellwright__shell_send({ keys: "exit\r" })
Stop the shellwright session:
mcp__shellwright__shell_stop
If the QA run changed terminal state (NO_COLOR, custom TERM, narrow width), those only lived inside the PTY — no host cleanup needed. But if you touched real env for some reason, unset here.
Before telling the user "done":
qa-reports/tui-<date>-<app>.md exists and opens cleanly{SCORE} placeholders left)$TERM + PTY size recorded (defines coverage)references/issue-taxonomy-tui.md — severity + category definitionsreferences/shellwright-tui-reference.md — shellwright MCP tool cheat
sheet + key-code tablereferences/terminal-conventions.md — what terminals / TUIs conventionally
do, for comparison during Phase 2 + Phase 4templates/qa-report-template-tui.md — fill-in report skeletonLoad the references on demand (they're not in context by default). When you hit a category question during triage, read the taxonomy. When you need a key-code you don't remember, read the shellwright reference.
shell_read to see
what's actually in the buffer — maybe it's drawing with cursor
positioning into a single row and the image capture missed it.dumb.\u0003 (Ctrl+C)
then stty sane\r then reset\r to the session. If the session is
itself wedged, shell_stop + shell_start again.shell_send keystrokes don't seem to register: some TUIs poll stdin
slowly. Add a small wait after each send before the next shell_read /
shell_screenshot. Find the minimum that works, note it in the report.locale should show LANG=en_US.UTF-8 or similar). If not, the issue
may be the PTY's encoding, not the TUI.tools
Inspect video frame-by-frame and capture-then-verify UI motion. Extract frames from any clip (handed to you, screen-recorded, or self-captured) with ffmpeg and read them as images; record an interaction (Playwright / computer-use / iOS simulator) and verify animations, transitions, and motion that static screenshots and getComputedStyle cannot reveal. Use when verifying animations/transitions/motion, analyzing a video or .webm/.mp4, extracting frames, checking how something "looks" in motion, or recording a UI flow to inspect.
testing
Cited research briefs
development
Daily coding habit prompts JP
development
React core deep-dive JP