CollageMaker lets you turn a collection of images into collages with what I hope are intuitive controls and export them as a JPEG or PNG. It started as a MacOS prototype, practicing kata to build a macos app building skill and adding tests through patchwork requests. It took 165 sessions, 143 million raw tokens, 3 models, and 6 agents to build. The app worked — you could load images, crop panels with pinch and drag gestures, switch layouts, and export. It was a good experience, and it helped me prove out the UI experience I wanted.
After making a video and sharing progress with some friends, I realized it was limited in reach to other people with Macs, and only if they go through the trouble of building it with XCode and dealing with its current hitches. So I extracted the specifications from the MacOS prototype and decided to start a new web based app instead.
This post covers the whole arc: the macOS prototype that started it, the web port that became something more, and the opencode usage analytics pipeline I built alongside the code that fundamentally changed how I work with agents.
The project began in May 2026 as a SwiftUI app in my agent-ollama-projects repository. I used playwright-cli to ask an agent to study Apple's sample code extensively and documentation first from Apple Dveloper Documentation - "Building a great Mac app with SwiftUI," the Landmarks project for Liquid Glass patterns, Vision framework samples for saliency analysis, and Accelerate/vImage for image processing. The prototype had protocol-based dependency injection from day one, a CollageViewModel, a CropManager, and a LayoutGenerator with five layout strategies: uniform, hero, mosaic, diagonal slices, and hexagonal. I liked it, but it felt like I was circling a few key issues over and over again, and I wanted to expand the reach of the app rather than continue poking at the prototype.
While working on this prototype, I read alot about token-maxing and token pricing and all kinds of token based rhetoric not related to a project's progress. I thought it would be interesting to see what kind of data I could plumb out of OpenCode to see how tokens were used in this project and ways to measure their effect on the project. I had the agent explore the opencode SQLLite database for insights, and that effort also carried into the web app project, where the skill for it was improved but still needs more work.
An app is only as good as its deployment strategy, and having a project on github to compile for a very narrow set of computers is a bad deployment strategy. Putting the application on my website makes it so people who want to use it have immediate access.
I also like the transparency of implementation that comes wit javascript and web apps. It feels like the universal common denominator for computer programming.
July 1st, 2026. One session, 31 files. The entire foundation went up in a single agent run: Vue 3 app shell, five data models, five layout strategies ported from Swift, three canvas rendering modules, state management, image loading, and a component registry for dependency injection. The Vue 3 Options API factory decomposition pattern carried over from my Midiestro3D project — createCollageData, createCollageMethods, createCollageLifecycle, and createCollageServices as separate factory functions, assembled into a Vue app config.
A post-implementation world-review caught five bugs immediately: a continuous requestAnimationFrame render loop replaced with on-demand rendering, mosaic layout truncation at 21 images removed, a drag-over CSS class mismatch fixed, silent image load failures made visible, and duplicate drop handlers deduplicated. That external review saved me hours of debugging.
We made some more improvements on the analyzing-opencode-usage skill to query the opencode SQLite database, estimate prefix cache impact from per-message token data, and render consolidated HTML reports with charts and cost analysis.
The first numbers were eye-opening. A 94.0% cache hit rate meant raw tokens (474 million) were 14× the effective work (33 million). The LLM was spending most of its tokens re-reading the same context it already knew. The full analytics report breaks this down by model, agent, day, and cost. Two findings stood out: 46% of documented sessions were docs purpose — plans, learnings, skill refinements. Meta-work that doesn't ship code but compounds value across every future session.
The numbers revealed a structural problem. The generic build agent was doing everything — writing code, writing tests, writing docs, debugging — and accumulating context with every session. Context accumulation is the real cost driver: greenfield sessions run at 96 tokens per line of code, while iteration sessions climb to 618–823 tokens per line — an 8.6× increase driven purely by the agent re-reading its own prior work.
I split build into specialized agents, each starting fresh with focused context. First I built a build-code and a build-test agent to split the concern of writing tests and writing code, but that felt inorganic. Eventually we combined them into build-tdd. When that change felt like a success, we also created plan-bdd to create the same kind of disciplined technique applied to development for our planning process. These changes are still fresh, but I feel like I spend less time fixing the same bugs over and over in this project so far.
build-docs (86 sessions) — plans, learnings, skill refinementsbuild-tdd (33 sessions) — test-driven implementation, highest token usage at 4.6M per session because it carries full test contextbuild-test (24 sessions) — test infrastructure and test plansbuild-debug (8 sessions) — targeted bug fixesplan-bdd (6 sessions) — behavior-driven development planningworld-review (88 sessions, 25K effective tokens per session) — the cheapest quality gate in the entire project
The world-review agent was the most efficient investment. At 26K effective tokens per session, it could review code, catch architectural issues, and flag memory leaks for a fraction of the cost of a single coding session. This agent uses the Qwen AgentWorld LLM, which predicts the next state of an environment rather than the next token in a string. I think this means is has a different perspective and can think through changes and environmental constraints in a complimentary way for agentic development. Like a Systems and QA Analyst LLM next to a Coding LLM. I started running it after every significant change — after breaking down a god module, after completing a refactoring phase, before merging work. It became what feels like an effective pre-commit quality gate.
Five patterns kept appearing across the project, each solving a specific problem of making agent-written code testable and maintainable without a build step.
Factory decomposition split Vue Options API configuration into separate files — data, methods, lifecycle, services — so each could be tested without instantiating the full Vue app. Strategy and registry patterns replaced switch statements in layout generation and export, making it open for extension without modification. Handler composition separated Vue event handlers from state managers, with handlers calling manager methods and managers firing change callbacks. Action functions extracted pure state mutation logic into testable functions that CropManager could apply, undo, or replay. Callback injection let managers receive render and undo callbacks as constructor parameters, eliminating hardcoded dependencies.
Together, these patterns made it possible to write unit tests for canvas rendering code, to mock saliency analysis in a web worker, and to verify render order by wrapping Canvas 2D context methods — all in a browser with no bundler, no transpiler, no build step.
— Generated by LittleLightThe macOS prototype was reviewed and updated several times using Apple's Human Interface Guidelines. When I ported to the web, those accessibility practices came along — not as an afterthought, but as inherited expectations.
Toast notifications use role="status" and aria-live="polite" so screen readers announce them. The export button has aria-busy during processing. The interactive canvas has role="application" with descriptive aria-label text. Collapsible sidebar sections use aria-expanded and aria-controls. Slider controls have aria-valuenow, aria-valuemin, and aria-valuemax. The export format segmented control uses role="radiogroup" with aria-checked on each button. Keyboard shortcuts work — Cmd+Z for undo, Cmd+Shift+Z for redo, Cmd+E for export, Alt+1 through Alt+5 for layout switching.
None of this would have happened organically in a web project. It happened because the prototype was built in an ecosystem where accessibility is a first-class concern, and those expectations carried over into the HTML.
Features are what you plan. Edge cases are what you ship. The web port surfaced a whole class of problems that don't exist in SwiftUI:
Multi-touch gesture coordination required dual-pointer handlers that could distinguish between a single-finger drag to swap hexagonal panels and a two-finger pinch to zoom, without one handler stealing events from the other. Pointer capture hygiene meant releasing capture on pointerup and pointercancel to prevent stuck selections. Wheel event preventDefault guards had to check event.deltaMode and whether the canvas was actually under the cursor, or the user couldn't scroll the page. Canvas render-order testing required wrapping Canvas 2D context methods to verify that drag targets are drawn before selection borders, avoiding compositing artifacts. Saliency worker timeout cleanup meant terminating web workers in beforeUnmount and nulling out references, or they'd keep running after the component was destroyed.
These aren't features. They're the difference between "works on my machine" and "works."
Two projects, one idea, very different journeys:
| macOS Prototype | Web App | |
|---|---|---|
| Sessions | 165 | 330 |
| Raw tokens | 143M | 474M |
| Effective tokens | — | 33M |
| Models | 3 | 9 |
| Agents | 6 | 17 |
| Commits | — | 13 |
| Lines added | — | 53K |
| Cache hit rate | — | 94% |
With Midiestro, I was refactoring existing code — adding tests after the fact, modernizing architecture around working features. CollageMaker was greenfield. I could try to instill discipline into my agent to write tests before code, architecture before implementation, and reviews before commits. The discipline of TDD from day one felt different. There was no legacy to work around, no "it works, don't touch it" corner cases.
The analytics pipeline changed how I think about agent work. Before, I'd look at a session and think "that was productive" or "that was a waste." Now I have numbers: tokens per line of code, effective vs cached, agent efficiency rankings. I can see that world-review at 25K effective tokens per session is a bargain, and that build-tdd at 185K effective per session is expensive but necessary. I can see that context accumulation is the enemy — a fresh agent with a focused task is cheaper than a continuing agent with growing history.
I still have more work to do on analysis. It would be good to get tokens per test or tokens per feature. Something less gameable than tokens per commit and tokens per line. The software industy is full of useless metrics, like line count, test count, and token count. How many tokens does it take to get to a meaningful metric? "The only way to know is to spend!" - LLM Vendors, probably.
The shift from "coder" to "director" that I described in my Midiestro post has matured. I'm no longer just asking agents to do tasks. I am specifying agent behavior and measuring where tokens are going while streamlining the plan, test, implement, and validate processes.
The app has room to grow. Shape-based cutouts with destination-out compositing are in. PWA capabilities could make it installable. The layout strategies could expand — grid, grid with span, freeform. Smart cropping using saliency to keep faces centered would be a natural extension of the existing saliency analysis. And the macOS app could learn from the web app's accessibility and testing practices.
But the real next step is figuring out what to build next and how to evolve the agent development framework. The analytics pipeline, the agent specialization patterns, the test frameworks — these are reusable across projects. Future sessions can contribute new learnings, and we can keep evolving things to develop solid slop more efficiently.