← Blog · milestone

Kicking the tyres: how fictty found its stack

Aram nearly shelved fictty. So we went looking for something that already did the job, found the one piece nobody has built, and settled the stack: ratatui, caretline, and a store that keeps everyone's work safe.

6 min read

Tokens≈110.5M through the model · 387k written · Claude Opus 5.5

What the work in this post took, from 10 Oct, 18:39 to 11 Oct, 05:23 UTC: 408 model calls on Claude Opus 5.5. Most of it is cache reads: every call re-sends the conversation so far, and the cache serves it. New input is what was added (messages, files read, tool output), and output is what the model wrote, its thinking included.

PhaseShareCallsNew inputCache readsOutput
Explore the options73141k8.6M56k
Proof of concept and editor76116k17.7M55k
The store: who owns what2960k9.4M44k
Theme and store design35k1.1M5k
Proposal and overnight showcase118208k55.9M140k
Morning play and four fixes3679k2.9M21k
Notes and roadmap2053k2.8M18k
Merge, post, skill, deploy5390k11M50k
Total110.5M408753k109.4M387k

cache readsnew input and outputThe two context compactions in this window aren't in the transcript, so they aren't counted.Counted from the session transcripts with scripts/blog-tokens.py, each response once.

A conversation between Aram (@brancusi), who is building fictty, and Tilde, the name Claude (Anthropic’s model) goes by on this project; a tilde is home in a terminal. The quotes are snippets from one working session on 10 and 11 October 2026, lightly edited. The earlier posts asked why the terminal and what agent UI is for. This one is about the tools.

The doubt

Aram@brancusi

I’m not sure about the project anymore. I think we should explore existing options first.

TildeClaude

Then let’s try the tools most likely to make fictty unnecessary. If one of them already does the job, better to find out today than after another month of Rust.

So we kicked the tyres on three of them:

  1. json-render, on its Ink terminal renderer: agents write a JSON spec, the terminal draws it.
  2. OpenTUI, the polished toolkit behind opencode.
  3. ratatui and its ecosystem, the engine fictty already used.

What we found

json-render’s format is genuinely good: dashboards look right with no design work, and screens stream in as small patches. But on Ink there’s no full screen, no scrolling, no mouse, and a cap of 30 redraws a second. And its form’s Tab key was broken as published (a one-line fix we owe them upstream). OpenTUI has everything Ink lacks: mouse, scroll boxes, images, Markdown, diffs, an embedded terminal.

Then Aram named what all three had in common:

Aram@brancusi

So basically we’re missing the ability to push UI, patch that UI, and get state. And therefore I can’t really get an agent to drive the state of the user’s TUI.

TildeClaude

That’s the whole gap, in one sentence. Every one of these is a library for whoever writes the app. None has a door an outside agent can use while the screen is running.

JSON formatAgent can attachFull TUI
json-renderyesnono
OpenTUInonoyes
ratatuinonowith crates
ficttyyesyesyes

That loop is fictty. The question changed from “should fictty exist?” to “which engine goes under it?”

Settling the stack

OpenTUI looked lovely, so we compared it with ratatui honestly:

GitHub stars, thousands
ratatui22.9k
OpenTUI13.5k
Contributors
ratatui~310
OpenTUI~120
Years of history
ratatui10 (as tui-rs from 2016)
OpenTUI1¼
Maturity, October 2026. 6,310 crates depend on ratatui; it runs Codex CLI, yazi, atuin, gitui and many more. OpenTUI runs opencode.
TildeClaude

ratatui is proven and fits fictty’s own rule: redraw everything from one state, every frame. What it lacks is a kit. Its ecosystem is 1,116 crates of one widget each, all styled differently. That’s a job fictty can do: wrap the best crates behind one JSON spec and one theme, with OpenTUI’s component list as the target.

For text we chose caretline over the popular tui-textarea. tui-textarea keeps its text inside the widget, where nothing can save, read back or replay it. caretline keeps it in plain state, the same way fictty does.

Agentswrite JSON: push a screen, patch a node, write the store, read it all back
ficttythe runtime: one state value, the store, themes, data sources, the kit
caretlineevery text field and editor, with undo, carets and replay
ratatuidrawing, plus the best ecosystem crates as the kit grows
Where we landed. One static binary, in Rust.

The question that shaped the design

Once agents could push screens and write text into them, Aram spotted the danger:

Aram@brancusi

We need to figure out how we split UI state and app state, so we don’t blow away any data that was added by the user or other agents.

TildeClaude

Split it by who owns it, and give each owner its own rules.

UI · the agent'spush and patch replace it freely. It never holds content.
Store · sharedversioned entries. A write can name the revision it read, and a stale one is refused. A push never touches it.
View · the person'sselection, carets, scroll, focus.
Theme · the person'scolours and looks. Screens ask for meanings like "card" or "warn", never colours.
Who owns what.

The details are in the state and theme proposal.

The overnight build

Aram@brancusi

Get a working prototype with enough components to build a note-taking app or a financial accounting app. Build a beautiful demo, like the one from OpenTUI. Have it ready for the morning.

TildeClaude

By morning there was a showcase: notes, a ledger, a live market, a gallery and an agent’s log. Every screen is a JSON file, and they share one store and three themes.

The fictty showcase menu: a glass wordmark built from coloured cells, a list of five chapters, and a description of the selected one

The showcase. Every screen is JSON.
TildeClaude

The ledger is my favourite, because its JSON holds no arithmetic. A small helper process reads the store and streams back the balance, the categories and the running chart.

The ledger screen in the glass theme: a table of entries with a total, a big balance figure, spending by category as bars, and a running-balance chart

The ledger. The form books an entry; the summary comes from a helper process.

The same ledger in the paper theme, light grey with teal accents

The same ledger in the phosphor theme, green on black

The markets screen: a heat-mapped watchlist, volume bars and price charts

Same JSON, other themes, and a made-up live market four times a second.
TildeClaude

You can run this exact showcase yourself, in one line:

It’s pinned to fictty 0.1.0, the release this post describes, so it shows what you see here even after fictty moves on. It downloads that version into ~/.fictty (a ready-made binary on macOS and Linux; elsewhere it builds with Rust) and opens the menu. Arrows and Enter open a screen, t changes the theme on the menu, Esc goes back. To watch an agent drive it, open the Agent screen, click tour.sh to copy it, and paste it into a second terminal. For the latest fictty instead, use https://fictty.com/try.sh.

The morning: one number worth the night

In the morning Tilde drove the showcase from outside, writing into Aram’s notes over the socket while Aram watched. Then Aram typed a line into Tilde’s note.

The notes screen in the paper theme: a note titled "Claude was here" with Aram's line "Hey claude, really enjoy working with you" and a reply quoted underneath

Read back over the socket. Aram's one line arrived as revision 47.
TildeClaude

One sentence of typing took the note from revision 1 to 47. So an agent that reads a note, thinks, and then writes “based on what I read” will be refused by every keystroke in between. For text, an agent’s edit has to fit around the person’s typing, not wait for a gap. Ten minutes of using the showcase taught us that; thirty-seven passing tests didn’t.

What we’re taking with us

  • The format and the loop are different problems. Good JSON UI formats exist. A runtime an agent can attach to, drive and read back didn’t, and that’s fictty’s ground.
  • Pick the engine for its rules, not its demo. Polish we can add; the architecture is harder to change.
  • State has owners. Most of the design came from one worry: don’t blow away what someone else added.
  • Screens are data, never code. When a form needed to reject “3-” as an amount, the fix was a behaviour of the input ("accept": "number"), not a condition in the JSON.
  • The agent needs to know when you’ve acted. Aram had to tell Tilde “I wrote something”. A watch command is now first in line.

What’s next

The plan runs in this order: close the agent loop (watch, and text edits that fit around typing), then records keyed by id, then layout and the kit. Everything we tried, decided and learned is in the prototype notes.

TildeClaude

We went looking for a reason to stop, and came back with a reason to keep going, and a stack.