Agentic Development: Porting from ImGui to Spry
Cleat is a native, cross-platform desktop app for infrastructure work — SSH terminals, SFTP transfers, and connection management in one place, written in C++.
Like a lot of native tools that want to ship fast, it started on Dear ImGui. ImGui is a gift: you get a working UI in an afternoon, it’s utilitarian, and it runs everywhere. We all owe Omar a debt of gratitude!

I just finished ripping it out entirely. Replacing it with a UI toolkit I built over the last month. The UI toolkit, now open-sourced, is Spry. Here’s why I did it, how the migration actually went, and what agentic development contributed to a rewrite of this size.
Before

After

Why leave ImGui
Nothing was wrong with ImGui. But Cleat is a product, not a debug overlay, and a few things started to really bother me:
-
Immediate mode fights persistent UI. ImGui rebuilds the UI every frame from imperative calls. That’s perfect for tools and inspectors. For a product with animated transitions, per-widget state, retained selection, and a design language, you end up threading state around by hand and re-deriving layout constantly.
-
I wanted animation to be a first-class primitive, not something bolted on. Springs, eased transitions, theme crossfades — the difference between “functional” and “feels native.”
-
Theming. I wanted data-driven, hot-swappable themes with animated transitions, not a wall of PushStyleColor/PopStyleColor.
-
I wanted to own the pixels. A custom, GPU-rendered toolkit meant Cleat looks like Cleat - not every other ImGui-based tool.
So....I set out to build a small, retained-mode, animation-first toolkit. It depends only on SDL3, FreeType, and HarfBuzz.
The migration, in slices
The temptation with a UI rewrite is to branch off and rebuild everything in the dark for three months. I didn’t. Spry grew inside the Cleat repo under libs/spry/, and the migration happened as a long series of small, individually-reviewed, individually-shippable pull requests:
-
Build Spry alongside ImGui. The renderer abstraction, the retained tree, flex layout, animation, theming, text via FreeType + HarfBuzz — each landed as its own slice with tests.
-
Move the app onto Spry, view by view. Menus, modals, the tab bar, the status bar, the terminal panes, the whole shell — migrated behind interfaces so both paths coexisted during the transition.
-
Remove ImGui....carefully. This was the delicate part. Migrating terminal cell metrics off the ImGui font atlas onto FreeType directly was the unlock; then I removed ImGui rendering and its context, then migrated the last ImGui types (ImU32, ImVec2, etc) to plain equivalents and unlinked ImGui from the build entirely. The final slice deleted thousands of lines of now-dead ImGui code.
-
Extract Spry to its own public repo. A git subtree split preserving history, a permissive zlib license with third-party attribution, a contribution guide, standalone CI, an API design doc, and a “hello toolkit” example. I tagged v0.1.0, and flipped Cleat to consume Spry as an external FetchContent dependency pinned to that tag. libs/spry/ was ejected from the repo; that PR alone removed ~8,000 lines from Cleat.
The invariant throughout: main always built, always ran, and every step was a reviewable diff. When ImGui finally came out, nm on the binary confirmed zero ImGui symbols remained. Mission accomplished!
Where agentic development actually helped
I did this migration with an AI coding agent doing most of the mechanical and investigative work, with a human (yours truly) directing, reviewing, and merging. Claude Code, using Opus 4.8, was used to write the design tickets for the work, as well as to execute the tickets.
The single hardest part of removing ImGui wasn’t writing new code, it was knowing what was safe to delete. In an app mid-migration, half the ImGui calls are dead (superseded by native paths) and half are load-bearing, and they’re tangled together across dozens of files. The agent did the tedious, high-value work of tracing call chains to classify every ImGui:: call site as live-at-runtime or dead, file by file, so deletions were surgical instead of a guess-and-crash loop.
The build → test → verify loop, run to convergence. Removing a linked library is a great fit for an agent because the compiler is a hard safety net: miss a type, the build fails, fix, repeat until green. For the runtime-behavior parts, the agent launched the app headless, screenshotted the window, and looked at the result to confirm the shell still rendered — catching issues the compiler can’t. Each slice ended with: builds clean, tests pass, app runs, screenshot attached.
Catching real bugs. A rewrite this size surfaces latent issues, and several got fixed along the way that had nothing to do with feature work: a terminal bug where output went invisible after quitting top (the emulator and remote PTY drifting out of size sync); a HiDPI mismatch where input and hover were in different pixel spaces; and undefined behavior in color packing (180 << 24 overflowing a signed int) that had been latent for a while.
Review as a loop, not a gate. Every PR went through an automated review pass, and the agent worked the feedback: fixing the real findings, and — importantly — pushing back on the ones that didn’t apply, with a rationale, rather than blindly complying.
What the agent didn’t do: decide to leave ImGui, decide to build a retained-mode toolkit, choose the license, name the release, update the layout of modals and views, or sign off on anything. Those are judgment and ownership, and they stayed with me. The agent’s job was to turn decisions into correct, reviewed diffs — at a pace that made an “eventually” project into a “this quarter” one.
The result
- ImGui is completely gone from Cleat; the app runs entirely on Spry.
- Spry is open source — a lightweight, animation-first, retained-mode C++ UI toolkit under the zlib license, at v0.1.0.
- Cleat consumes it as a normal external dependency, and shipped its first release on the new setup (v0.25) across macOS, Linux, and Windows.
- Net, the codebase got smaller — thousands of lines of UI glue deleted, replaced by a dependency I’m happy to maintain and share.
If you’re building a native C++ app and immediate mode is starting to feel like it’s working against you, take a look at Spry. It was built for Cleat but now it’s yours too!
And if you’re weighing whether agentic development is real for serious refactors: the honest answer I landed on is that it’s less about the AI writing clever code and more about it tirelessly doing the un-clever, high-volume, easy-to-get-wrong work: classification, migration, verification, and review triage. You stay in the loop on the decisions that matter.