Available
← all posts

Typing While the Agent Thinks

The dead time while a coding agent works adds up to a real part of the day. I built wordwright, a terminal typing trainer that teaches grammar and writing in the gap, and this is how its interface is put together.

A day of agent runs, with the gaps between them marked out

I asked Claude Code to refactor a module and then I sat there.

Not for long. Long enough to open a browser tab, read half a thread about something I did not care about, and lose the thing I had been holding in my head. When the run finished I had to read my own prompt again to remember what I had asked for.

That happens many times a day now. I never measured it properly and I am not going to pretend I did, but the shape is obvious: a run is rarely under a minute, it is sometimes five, and the gap is exactly the wrong length. Too long to stare at. Too short to start anything.

So I went looking for something to put in the hole, and everything I tried was wrong in the same way. A typing trainer feeds you scrambled words and you get faster at hitting keys. That is it. You spend twenty minutes a day at it and the only thing that improves is the muscle. Meanwhile the actual bottleneck in my day is not how fast my fingers move. It is how long it takes me to write a clear pull request description, a ticket, a paragraph like this one.

The best two minutes to practise writing are the two you spend waiting for a machine to finish writing.

The constraint

It has to be over the moment the agent is. That is the whole design problem in one sentence.

Everything falls out of it. No login, because a login costs more than the gap. No network, because a spinner in a two minute window is the same as a failure. No session you have to finish, because the thing you were actually doing comes back at an unpredictable moment and you need to be able to walk away mid sentence with nothing lost. And the unit of work has to be one sentence, not one paragraph, because one sentence is what fits.

One sentence turns out to be enough to teach something, as long as you pick the sentence on purpose.

What it does

It is called wordwright. You type a sentence, it measures you, and then it tells you why the sentence is built the way it is.

  wordwright                                        intermediate  ·  drill 3/8
  ────────────────────────────────────────────────────────────────────────────
 
  ▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░   58 wpm   97% accurate   3.0s
 
  The deploy finished at two; nobody noticed until the morning.
 
  semicolon · clause-joining
 
  esc quit    ctrl+r restart    tab skip

That semicolon is the point of the exercise. You type the sentence, you see your speed, and then you get two or three sentences on why a semicolon can join those two halves and a comma cannot. Every sentence in the corpus is there to carry one pattern: an appositive that defines a noun in passing, a dangling modifier, the difference between fewer and less.

Wrong characters stay on the screen in red rather than being rejected. A trainer that refuses the wrong key teaches you to hammer a key until it gives, and it quietly hides the mistake from the report. Here the miss lands, you can see it, and you decide whether to go back for it. Correcting it still costs you accuracy, because it happened.

Then there is the other mode, which is the one I actually built the tool for. It gives you a brief and you write the sentence yourself.

  score      ▓▓▓▓▓▓▓▓▓▓▓░░░░░  2/3 checks
 
  yours      the outage was caused by an expired certificate
 
  checks     ✗ Still passive. Put the actor in front of the verb: something
               did something.
             ✓ 12 words or fewer
             ✓ used "certificate"
 
  mechanics  Open with a capital letter.
 
  one way    An expired certificate caused the outage.

There is no model in the loop. Not one call, not one token. Each check is a rule sitting in a JSON file that you can open and read: this pattern must appear, this one must not, stay under twelve words, use this word. That is a long way short of a human editor and it buys three things I was not willing to give up. The verdict arrives in the same frame as the keypress. It is identical every time, so a rule you failed yesterday fails the same way today. And it works on a train.

Grading is split in two on purpose. Mechanics are the rules that hold for anything you write, so a capital at the front and a stop at the end. The checks are the actual lesson, and each one fails with a hint that tells you what to do rather than what you did. "Still passive, put the actor in front of the verb" is useful. "Incorrect" is not.

Getting it

npm install -g wordwright
wordwright

Node 20.11 or newer, no runtime dependencies, nothing to configure. If you would rather not install anything, npx wordwright runs it once and leaves nothing behind.

wordwright                     pick a level and a mode, then practise
wordwright drill               type sentences, measured in words per minute
wordwright compose             write to a brief, graded on grammar and intent
wordwright stats               speed, accuracy, streak, and your weakest patterns
wordwright packs               every pack loaded, and anything that failed to load
wordwright add my-pack.json    install a pack of your own

Bare wordwright asks you for a level and a mode and then runs eight exercises, which lands somewhere around two minutes. --count 3 makes it shorter. --level advanced skips the first menu. Escape leaves at any point and keeps everything you finished.

There are three levels. Beginner is the ground you stand on, so its and it's, subject and verb agreement, the comma before and. Intermediate is the joins: semicolons, comma splices, active voice, parallel structure. Advanced is rhythm and precision, which means appositives, the short sentence that lands after a long one, cutting the hedges, and the words that compress a whole clause into one.

Your history is a single file at ~/.wordwright/progress.json. Every number the tool shows is derived from it when you ask, so nothing goes stale, and nothing is sent anywhere because there is nowhere to send it to.

What it picks next

A trainer that serves random sentences trains what you are already good at.

Every exercise carries focus tags, 84 of them across the corpus, so semicolon, active-voice, its-vs-its, rhythm. Each attempt you finish scores the tags it touched, weighted so a recent attempt counts for more than an old one, and the next session leans towards the tags you are worst at and the exercises you have not seen in a while.

Two guards stop it becoming a punishment loop. An exercise you did in the last hour gets pushed down hard, and a little jitter goes into every priority, so your three worst sentences do not come back on a loop until you resent them. Practice, not detention.

How the interface is put together

This is where the interesting part is, and it is not the terminal drawing.

The shape is one rule: exactly one file is allowed to know that a terminal exists. Everything else is a pure function.

The typing engine holds an immutable state and never reads the clock. The current time comes in as an argument to every function that needs it, which is the only reason the timing tests are not flaky. A test hands it three keystrokes and a fake stopwatch and asserts on the words per minute, with no waiting and no tolerance window.

The key parser takes a string and returns key presses, and it exists because typing fast is messier than it looks. Several characters arrive in a single chunk when your hands are moving. An arrow key arrives as three bytes and must not be read as somebody typing a bracket. Because it is pure, a test can drive an entire session by feeding it a string, with no terminal attached anywhere.

The renderers take state and return an array of lines. They do not write to standard output, they do not read the clock, and they know the width of your window only as a number somebody passed them. So the tests assert on what you would see, which for a typing trainer is most of what is worth asserting on. Every colour goes through one paint function that takes a plain flag, so every screen has a colourless twin. That is what --plain gives you, and it is also what the tests read.

Two details in the impure file earned their place.

The first is that a frame is drawn by moving the cursor home and repainting, erasing each line as it goes and then erasing whatever is left below it. The obvious version clears the screen and then writes, and the obvious version flickers, because for one frame there is genuinely nothing there. Nobody notices this until they type fast.

The second is that the restore is registered before the screen is entered, not after. Raw mode and the alternate screen are things you borrow from somebody's terminal. If the process falls over between borrowing and giving back, they are left with a terminal that does not echo what they type, and their only move is to close the tab. So the handlers for exit, interrupt and terminate go on first, they all run the same restore, and the restore is guarded so that calling it twice is harmless.

There is one timer in the whole thing. It repaints every 120 milliseconds while a drill is running, and it is there for one reason: the elapsed seconds have to keep moving when your hands are not. Keystrokes paint their own frame.

The result is 3,476 lines of TypeScript, of which 1,134 are tests. There are 127 of them, on Node's own test runner, with no framework and no dependencies, and they run in about 130 milliseconds. One of those tests is worth stealing for any project that ships content: every model answer in every shipped pack has to pass its own checks. An exercise whose own answer would be marked wrong is a bug, and it gets caught long before it reaches you.

The one that bit me

The repository runs straight off TypeScript with no build step, because Node strips the types for you now. That is a genuinely nice way to work, and it stops working the moment somebody installs your package: Node refuses to strip types for anything under node_modules. The first version I packed would have failed on every machine that installed it.

So the build compiles to JavaScript, and continuous integration installs the packed tarball on Node 20, 22 and 24, on Linux and macOS, and runs the command for real before anything is published. Type checking proves the code compiles. Only installing it the way a stranger would proves that it runs.

What it does not do

It does not know whether your sentence is good. It knows whether it is passive, whether it used the word, whether it stayed under twelve words, and whether the mechanics hold. It will not teach you to type if you have never touched a keyboard, so learn the home row somewhere else and come back.

And the corpus is 81 exercises. That is weeks of daily practice and it is nowhere near enough forever, which is exactly why the pack format is the first thing documented in the README. A pack is a JSON file you drop in ~/.wordwright/packs/, wordwright template prints the skeleton, and wordwright add validates one before it goes in. If you write a pack for the mistakes you actually repeat, I would like to see it.

The code is at github.com/juliantellez/wordwright, it is MIT, and it is on npm.

Now go and give your agent something slow to do.

ShareXLinkedIn

Questions about the build, or building something similar? Say hello. I read every message.

Comments

Sign in with GitHub to join the conversation.

Built from Scratch

A newsletter on video, streaming and building reliable systems. No spam, unsubscribe anytime.