Loom


A read-only terminal dashboard for Beads projects. The agent manages the work, I just need to see it.

Started 2026 · Active Go · Bubble Tea · TUI

Beads is an issue tracker that lives in your repo instead of a web app. Issues are files under version control, and everything happens through the bd CLI, which makes it a natural fit for coding agents: an agent can create, claim and close work without anyone building it an API.

That works beautifully right up until you have fifty issues with a dependency graph and want to know what’s actually going on. There’s no UI, because not having a UI was the point. Loom is the window.

The Loom dependency tree, an ASCII forest of issues with status glyphs and colour-coded priorities

Read-only is the whole idea

Loom never writes to the Beads database, and never will.

The division is that bd is the agent’s tool for managing work and Loom is the human’s tool for understanding it. Once you accept that, an enormous amount of difficulty evaporates. No write path means no lock contention with the agent that’s actively mutating the database, no merge semantics to reimplement, no schema coupling, and no chance that my dashboard corrupts my tracker while I’m looking at it.

It also means I never had to answer “should Loom let you close an issue?”, which is the question that would have turned a weekend into a project.

Shelling out on purpose

Loom talks to Beads by running bd list --all --json, bd show <id> --json and bd ready --json, then parsing stdout. It does not link the database.

That’s slower than reading Dolt directly, and it’s the decision I’d defend hardest. There was an earlier third-party Beads viewer that linked the storage layer, and it broke when Beads migrated from SQLite to Dolt. The CLI is the stable, documented, versioned interface. The storage engine is an implementation detail that its authors are entitled to change.

So there’s exactly one package that knows what bd output looks like. If the JSON shape changes, that package changes and nothing else does.

The concurrency model falls out of the same choice. The agent is writing to an embedded database while I’m reading from it, so the reader is the one that has to yield. There’s a retry layer that backs off exponentially, but only on a database lock error, and fails fast on everything else. Retrying an actual error is just a slower way to show the wrong thing.

Errors get translated into sentences a person can act on rather than passed through. “No beads project found. Run ‘bd init’ to initialize.” beats an exit status.

What it shows

A dashboard for the shape of the project: status counts, priority distribution, what’s ready to pick up, what’s blocked.

The Loom dashboard with status counts, a priority bar chart, and the ready queue

A sortable, filterable issue table. The filter is a small DSL that took about twenty five lines: any key:value token is a field predicate, anything else is free text matched against title and ID. So status:open priority:1 auth does what you’d hope.

The Loom issues table, sorted by priority with a filter applied

And a dependency tree, which is the view I actually built the thing for. Everything is keyboard-driven and Vim-flavoured.

Building it, then deleting half of it

The original spec had six views. Two of them were the clever ones: a critical path view showing the longest blocking chains, and a focus view ranking the ready queue by how much work each item transitively unblocks, with a scoring formula I was quite pleased with.

I built both, used them for two days, and deleted them. Four files gone. A little later the dashboard’s stats block went too, to reclaim the space.

They weren’t broken. I just never looked at them. The tree view answered the same questions faster because I could see the structure instead of reading a ranked list about the structure, and every extra tab was one more thing to skip past. Cutting features you built and still like is harder than not building them, and the repo is better for it.

Under it

Bubble Tea, Bubbles and Lipgloss, and nothing else. No CLI framework, no JSON library, no database driver. The dependency graph is hand-written: dual adjacency maps, Kahn’s algorithm for the topological sort, and three-colour DFS for cycle detection that reconstructs the actual cycle rather than just reporting that one exists.

Views opt into shared behaviour by satisfying small interfaces. A view that implements InputCapturer suppresses global hotkeys while its filter prompt has focus, which structurally solves the classic TUI bug where typing “q” in a search box quits the program. Jumper gets you gg and G. The root model type-asserts and degrades gracefully when a view doesn’t care.

Every layer is an interface, the cache takes a clock function, and the retry layer takes a sleep function, so the tests run instantly and don’t need bd installed. Statement coverage sits around 98%, and the only gap is the main.go path that starts the program.

Loom tracks its own backlog in Beads, so the screenshots on this page are Loom rendering Loom’s issues.

Getting it

1
brew install grantlucas/tap/loom

You’ll need bd installed and a project initialized. Run loom --watch if you want it to poll while you work.

It went from first commit to a tagged 1.0 in four days, which says more about Bubble Tea and a clear spec than it does about me. It’s had dependency bumps and nothing else since April, and that’s the right amount of activity for a tool that does one thing. I keep it open in a split while agents work, which was the entire brief.