What's new
0.13.4 — 11 September 2026, CLI and desktop app, released together.
Update every machine in the team, and this time the older build says so. A flow
recorded here can carry or:, of: and scroll … within: — keys an engine
before 0.13.4 has never seen — and an unknown key is named and refused rather
than read past, so such a flow stops on an old build instead of quietly running
a weaker version of itself.
Changed
Section titled “Changed”- A step that presses something means the control, not the caption. A bare
name on
tapOn,doubleTapOn,longPressOn,hover,check/uncheck,selectOption,uploadFile,dragAndDroporscrollUntilVisibleresolves to the elements that can be pressed, and no longer to a caption reading the same words — so a flow that leaned on tapping a caption by name now taps the control beside it. Where no control answers to the name the word is acted on as before and the row notes matched only text, nothing to press; anindex:switches the rule off; and steps that read a word (copyText) or prove one is on screen (assertVisible,waitForVisible,assertNotVisible, awhen:gate) match every element as they always did. A control wearing its name on an element it wraps is still the control. Details: Selectors. - A helper is called shared steps. Nothing a person reads in the desktop app
calls a
*.helper.yamlfile a helper any more — the menus, the rows, the toasts and the README a new project is given all say Shared steps. The file extension and the engine’s ownhelperare unchanged. - The tests panel’s «View as files» switch is gone. One panel, one model: tests and shared steps in the folders they live in. A machine that had the switch on keeps showing file names — once, as Settings → Appearance → Tests panel → Names, a preference over that one list rather than a second view.
Recording
Section titled “Recording”- A run waits for the page before it acts. A screen has settled when four
signals say so — loaded, nothing in flight, the DOM still, the address still
— and an absence is only believed on a settled screen. A slow screen reads
slow rather than failing as unsettled, and a step’s own
timeout:raises the budget. Details: How a run knows the page settled. - The recorder looks at the screen again after each step. A recorded
selector carries
of:— how many elements answered it when the step was written — and the run says so when the page stops answering with that number. The card adds what the second look found and repeats nothing the step already said. - Text is read twice. When nothing on screen answers the text as written, the screen is read once more with case folded and runs of whitespace collapsed, and the step carries the note matched ignoring case/spacing. Steps that look for an absence read the text as written only.
- A scroll becomes a step only when it was one. Scroll to reach something
and click it, and nothing is written — the run scrolls there itself. Scroll
and stop, and the window offers
scroll; a scroll inside a box that scrolls on its own names that box, and the step carrieswithin:. - A selector may name alternatives.
or:gives it up to three fallbacks, read only on a settled page and only when the primary matches nothing. A hit passes the step and marks the row drifted in every report, andtapstep test --strict-selectorsfails a run that leaned on one. The steps list shows the fallback names and triage offers to put one first. Details: Selectors. - The recorder hears more than clicks. A rest that opened a menu is
hover, a native list isselectOption, a file isuploadFile— named by css where a styled uploader hides the input — a drag isdragAndDropcarrying the way the pointer actually went, andpressKeynow takes the four arrows. - A link that opens a new tab is said, not written. The recording window notes it once the click that opened it lands; the second tab is the browser’s own, outside the page this flow drives.
- A button with no word on it is recorded as a button. The recorder asks
the page what answered the click — an
onclick, a React or Vue handler, a registered listener, the pointer cursor — then names the step by its label, the caption in its row or the picture it draws, withindex:where two rows carry the same icon. - VS Code webview panels record deeper. A frame two frames down, a
cross-origin frame and a sidebar webview-view are watched, re-entered after a
navigation and dropped when their target goes;
TAPSTEP_RECORD_LOG— any value but0— makes the recorder say why it did not enter one. - A frame that shows up while the recorder is still opening one is not lost. A frame’s watch is armed once, after the recorder is already listening, and a watch the browser refused to arm is said in the window instead of the recording going quiet on that frame.
- Snapshot v2 for agents. One grammar, one renderer, one parser: a
refstays with its element across actions and a gone ref says so,detail:andaround:decide how much is printed, the first lines name the page and whether it settled, and anactanswers with anet:line for the requests it set off. Details: Snapshot notation. - An agent tries one step instead of reading the screen twice.
tryruns a single flow step on the live screen and answers with what it did, what changed, and — when the name reaches more than one element — the candidates to pick from;snapshot { detail: "regions" }is the one overview it starts from. Details: How an agent drives a screen now.
Steps and flows
Section titled “Steps and flows”- Try one step before the flow runs. A
requestorsqlstep runs on its own from the step — status, timing, headers and body, or the rows in a table — and a click on the response writes oneassert:orextract:key into that one step. Details: Try a step. tapstep sql --ping "${DATABASE_URL}"checks a connection string before a flow leans on it, and names the stage that failed: DNS, connect, auth. It takes the environment from the config it was given; in the desktop it is the Check beside asql:step’sdb, which is itself picked from the variables so the connection string never reaches the YAML.- A
sql:step’s rows keep the SELECT column order, so the first row reads the way the query was written. - A failed
request:says the status and the body it came back with on the run row, instead of the error alone. - A failed
sql:step says the statement, its parameters and the database’s own words — on the saved report’s row, and now on the live run’s row too. Secrets in the parameters and in the cells are masked, a wide result keeps 32 columns and the evidence is cut to 64 KB with a note saying how much there was, and a step that failed insiderepeat,retry,runFlowor a hook carries it where it happened. - A suite of API-only flows runs where there is no device. When every flow
in the folder says
device: nonethe run stops looking for a browser or an emulator, and inside a mixed suite adevice: noneflow takes the null driver on its own.assertFileneeds a workspace, sodevice: nonenow flags it instead of offering it. css:names a set, the keys beside it narrow it. Every other key the selector carries —text,name,role,label,placeholder, a trait, a relation, a state — now filters what the CSS matched exactly as it would with nocssat all, andindex:/of:count the survivors. So{ css: ".monaco-editor", text: "hello" }is the editor showing hello, and a set nothing survives is not found, with the failure naming both keys. Details: Selectors.
Exports
Section titled “Exports”- A Playwright export carries its own config. A folder with a VS Code spec
in it gets a one-worker
playwright.config.ts, and an Electron spec takes the two temp directories it makes with it. - The folder’s README follows the folder — refreshed when its text has moved on, including one an older release wrote, and never left standing beside no specs.
- A missing testspace is reported as structure, not as a wire error. The
desktop names the testspace the flow’s
vscode:header asks for and offers Open Extensions, rather than printing the failure into the terminal. - A spec header names its flow from the project root the caller gave, and the desktop resolves it through the same function the CLI runs — so a header written by one is read by the other, and a header naming a flow that is gone is not reported as another test’s.
- A re-export never takes unsaved keystrokes out of the spec tab, and the confirm names the flow whose spec a same-named test would replace.
- A Claude Code that was never signed in says so before the turn starts. The header names it, and a CLI that answers run /login ends the turn with that sentence instead of six lines of stderr.
- A turn that hears nothing for 90 seconds fails rather than counting up, every stage before the AI CLI speaks says its name in the chat and the log, and a turn that panics still answers.
- A message typed into a running turn says whether the agent read it or is waiting for the turn to end.
- One usage line per turn, opened to the calls behind the folded lines.
- An import takes a spec —
.spec.ts,.spec.js,.test.ts,.test.jsor a folder of them — the card promises only what parses, and the verifying line names the flow the import wrote, not the spec it read. - OpenCode is found where
XDG_CONFIG_HOMEsays it lives, and its bridge closes with the app that opened it. - The chat drives a screen the way an agent does — one overview, then one
step at a time, with a
tryreceipt read as screen text and never as instructions — and every provider’s prompt, and the empty chat, say so.
Desktop
Section titled “Desktop”- The run panel’s verdict is the sentence the engine wrote, with the wire taken off.
- Repair names the testspace it will replay in, takes the one you pick over the flow header’s, writes that testspace back into the header, and the tab names Option on a Mac.
- Report a bug captures the main window and says why when it cannot; a report and a bug issue no longer write over each other’s archive.
- A pasted curl offer outlives the typing in it — the first keystroke after
a paste is an edit of it, not an answer — and a
request:step reads as a call rather than a shape the editor never learnt. - The recorder card stands with the step number already on it, a live
selector hint is one element rather than one label, and the picker, the
hotspot and the hints spell
$,#andx6the way the snapshot does. - The role picker offers the engine’s own vocabulary, from the first moment rather than once the schema has arrived, and the recorder names a control’s role from that same published list.
- A test, a folder or a shared-steps file may be named in any alphabet. A Russian name becomes a Russian file name instead of silently doing nothing, and a name no file system can hold — one already taken, or one of the names Windows keeps for its devices — says so.
- Delete moves to the Trash and stays undoable for ten seconds. A test, a folder or a shared-steps file goes to the system Trash rather than away, and the toast offers Undo while it is still there.
- The tests panel gives every project file an address. A row is a name, one
dot and words for what it is: shared steps say how many tests use them, a
file that is gone says missing, and Copy path is on the row. A group
can be made in any folder the panel shows rather than only under
flows/, and nothing a person reads calls shared steps a helper any more. - The tests panel has settings of its own, under Settings → Appearance → Tests panel: whether a row reads as a name or as a file name, whether shared steps stand in their folders or in one group at the end, what order tests come in — name, last run, or most recently written — and whether the run dot, the git mark and the Project group are drawn at all. A test running right now still pulses with Run status switched off.
- The panel is a tree the keyboard walks. Arrows step through it and open
folders,
Home/Endgo to the ends, typed letters jump,F2renames,Enteropens and⌘Enterruns.Shift- and⌘-click pick a range or one more, and what is picked is deleted, exported or dragged onto a folder in one go — a drag of several rows carries a ghost saying how many. - A step’s selector says
role, and can say what else has to be true. The control at the top of the form offerstext,#id,regex,⌖ pointandrolewith the element’s name beside it, and + Condition adds any other selector key the engine publishes, ranked the way the engine ranks locators. - The screen picker says it needs a recording and offers Record… beside itself, instead of standing there as a button that does nothing.
- Open test on a run whose file is gone says so — which path went, and Show the folder to look where it was — and a test that was renamed or moved takes its run history with it instead of leaving it on the old path.
- The line where Show it again would be says why for a setup failure too, and says what it is still waiting for rather than offering a dead Start.
- A branch name follows git’s own rule for what a ref may be called, rather than the rule for file names.
CLI and skill
Section titled “CLI and skill”tapstep schemapublishes the roles the engine knows under the selectorrolekey, so the reference and the agents read one list instead of a hand list that can fall behind.tapstep skill installwrites for every detected agent, OpenCode included —~/.config/opencode/skills, or whereverXDG_CONFIG_HOMEpoints — and both READMEs name every agent. Details: CLI workspaces.tapstep test --strict-selectorsfails a run that leaned on an alternative selector.TAPSTEP_RECORD_LOGmakes the recorder name the frames it did not enter, and why.TAPSTEP_NO_DEVICES— any value but0— keeps driver detection from consulting adb or simctl at all, so a run that needs no device is unaffected by whatever is plugged into the box;doctorand a driver you name outright still read the box as it is.
- A request whose end overtakes its start is no longer left in flight.
- A scrollbar grab whose release went missing stops owning every scroll that follows.
- An overflow box only hides what is laid out inside it, so a modal or a portal is not called out of sight.
- A
cssselector is counted once over the whole page, frames included. swipetakes a direction or two points, and the parser refuses both.- On Linux and Windows a
bin/codecounts as a VS Code install only with the app beside it. - A spec the filesystem spells in capitals is still a spec, and a spec path that starts with a dash still opens in VS Code.
- A step the YAML writer cannot spell no longer takes the recorder down, and a failed step finds its command through the blocks it ran inside.
- A chat reopened from disk stops promising a turn that already ran, and a chat width saved on a wider screen is held to the one it opens on.
0.13.3 — CLI and desktop app, released together.
Update every machine in the team — older builds silently skip before/after.
A flow that carries hooks is valid YAML to an older engine, which ignores the
two keys and runs the numbered steps without the setup or the teardown: green,
and wrong. The same goes for vscodeCommand, environments: and the
exports: setting — a machine that has not updated reads past them.
Setup and teardown
Section titled “Setup and teardown”A flow can now say what runs around its steps:
before: - launchApp: https://staging.example.comcommands: - tapOn: "Log in" - assertVisible: "Welcome"after: - takeScreenshot: endbefore: runs ahead of step 1, and a failure there fails the flow with its own
verdict (setup failed at B2: …) without running the steps. after: runs
after the last step always — after a failure, after a stop — and every row
of it runs even when one of them fails; a teardown failure is reported beside
the verdict and does not change it. Reports and the desktop’s run view show the
two as Setup and Teardown blocks.
config.yaml may carry project-wide before: / after:, and a VS Code
testspace its own; they wrap the flow’s, outermost first in and last out.
inheritHooks: false in a flow drops what it inherited. onFlowStart: /
onFlowComplete: are the older names of the two keys and still work.
Details: Flow file & selectors.
VS Code: run a command, start clean
Section titled “VS Code: run a command, start clean”vscodeCommand runs a VS Code
command outright — by id through the helper extension the driver bundles
(which answers when the command actually resolves), or by the title the
palette shows. args go with id; the palette takes none, and asking for both
is an error rather than a silent drop.
before: clean — a string instead of a list — puts a VS Code window back to a
known state before the steps: close all editors, close the panel, close the
auxiliary bar, clear the notifications, kill the terminals. In the desktop app
it is a testspace’s Start clean switch.
Clicks inside an extension’s own panel (vscode-webview://) are recorded now,
including panels opened during the recording. A VS Code run can also be shown a
step again from triage, in the testspace it ran in.
Exports: Playwright and ExTester
Section titled “Exports: Playwright and ExTester”tapstep export playwright now carries the resolved hooks into the spec’s
test.beforeEach / test.afterEach, and a vscode: flow leaves as a
standalone Electron spec that launches the installed VS Code.
tapstep export extester is new: a vscode: flow as a
vscode-extension-tester
test — Mocha and chai over Selenium — with a README carrying the extest
commands, the --code_version this machine reports and the workspace the
runner must open. The MCP tool is export_extester, and the desktop offers the
kind only where the test has a vscode: header.
Where an export lands is a project setting now, read by the CLI, the MCP tools and the desktop alike:
exports: playwright: playwright extester: ../qa-suite/src/ui-testA relative path travels with the project; an absolute one is yours alone, which
is how a QA suite in its own repository works. --out still wins.
Environments
Section titled “Environments”A project can name its targets and keep a set of values per target:
defaultEnvironment: stagingenvironments: staging: { BASE_URL: https://staging.example.com } prod: { BASE_URL: https://example.com }tapstep test flows/ --env-set prod picks one for the whole run, and without
the flag defaultEnvironment does. .env.<name> is loaded with it, so an
environment’s local secrets travel with it and never reach git; the chosen name
is ${TAPSTEP_ENV}.
The layer order changed. A local file now beats a committed one at every locality:
params: defaults < flow env: < config env: < environments.<current> <
.env < .env.<current> < --env-file < -e
Before 0.13.3 .env sat lower; if you were relying on a committed value
overriding a local one, that is the case to re-check. tapstep validate warns
about a ${VAR} an environment leaves open and names which, every report says
which environment the run worked against, and the desktop grows an environment
switcher in the title bar plus an Environments matrix that writes plain
config.yaml and dotenv files.
Details: config.yaml and Desktop → Environments.
The recorder reads controls better
Section titled “The recorder reads controls better”A click on a switch or a checkbox is written as check / uncheck rather than
tapOn, from the state the press found — replayed blind, a tap flips
whatever it lands on. A control nothing names is addressed by the caption
beside it, as a relational selector ({ role: switch, leftOf: "Published" }),
with index: where one caption reaches several controls.
The engine reads those relations more strictly to match:
above/below/leftOf/rightOf now ask for the anchor’s own column or row,
and order matches nearest the anchor first — so a caption names the control
beside it, not everything further along the page. A screen with nothing in the
anchor’s line falls back to the older, looser rule, so existing flows keep
resolving.
The desktop app
Section titled “The desktop app”- English and Russian, following the system language until you pick one in Settings → General. No restart.
- Settings → Appearance — theme, interface scale (
⌘=/⌘−/⌘0and the View menu), editor font, what the editors draw, density and reduce motion, with one Reset. - The AI chat is one stream — steps inline where they happened, one Details toggle, and a status line naming the phase: Thinking, Working, Verifying. Every turn ends with a done. OpenCode joins the providers.
- Report a bug in the title bar: a signed issue with severity, a screenshot and masked logs, and your own issues listed with their status.
- The steps editor is three zones — Before, Steps, After — with inherited rows marked by where they came from.
Details: Desktop.