Skip to content

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.

  • A step that presses something means the control, not the caption. A bare name on tapOn, doubleTapOn, longPressOn, hover, check/uncheck, selectOption, uploadFile, dragAndDrop or scrollUntilVisible resolves 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; an index: switches the rule off; and steps that read a word (copyText) or prove one is on screen (assertVisible, waitForVisible, assertNotVisible, a when: 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.yaml file 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 own helper are 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.
  • 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 carries within:.
  • 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, and tapstep test --strict-selectors fails 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 is selectOption, a file is uploadFile — named by css where a styled uploader hides the input — a drag is dragAndDrop carrying the way the pointer actually went, and pressKey now 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, with index: 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 but 0 — 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 ref stays with its element across actions and a gone ref says so, detail: and around: decide how much is printed, the first lines name the page and whether it settled, and an act answers with a net: line for the requests it set off. Details: Snapshot notation.
  • An agent tries one step instead of reading the screen twice. try runs 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.
  • Try one step before the flow runs. A request or sql step 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 one assert: or extract: 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 a sql: step’s db, 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 inside repeat, retry, runFlow or 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: none the run stops looking for a browser or an emulator, and inside a mixed suite a device: none flow takes the null driver on its own. assertFile needs a workspace, so device: none now 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 no css at all, and index:/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.
  • 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.js or 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_HOME says 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 try receipt read as screen text and never as instructions — and every provider’s prompt, and the empty chat, say so.
  • 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 $, # and x6 the 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 / End go to the ends, typed letters jump, F2 renames, Enter opens and ⌘Enter runs. 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 offers text, #id, regex, ⌖ point and role with 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.
  • tapstep schema publishes the roles the engine knows under the selector role key, so the reference and the agents read one list instead of a hand list that can fall behind.
  • tapstep skill install writes for every detected agent, OpenCode included — ~/.config/opencode/skills, or wherever XDG_CONFIG_HOME points — and both READMEs name every agent. Details: CLI workspaces.
  • tapstep test --strict-selectors fails a run that leaned on an alternative selector.
  • TAPSTEP_RECORD_LOG makes the recorder name the frames it did not enter, and why.
  • TAPSTEP_NO_DEVICES — any value but 0 — 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; doctor and 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 css selector is counted once over the whole page, frames included.
  • swipe takes a direction or two points, and the parser refuses both.
  • On Linux and Windows a bin/code counts 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.

A flow can now say what runs around its steps:

before:
- launchApp: https://staging.example.com
commands:
- tapOn: "Log in"
- assertVisible: "Welcome"
after:
- takeScreenshot: end

before: 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.

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.

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-test

A 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.

A project can name its targets and keep a set of values per target:

defaultEnvironment: staging
environments:
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.

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.

  • English and Russian, following the system language until you pick one in Settings → General. No restart.
  • Settings → Appearance — theme, interface scale (⌘= / ⌘− / ⌘0 and 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.