Introducing Maple: Visual Review Written by People, Fixed by Agents
A coding agent can fix almost anything you hand it — as long as what you handed it is clear and actionable. A visual review comment usually isn’t.
Here’s the loop I got tired of.
Someone opens a preview deployment — a designer, a product manager, whoever has the link — and something is wrong. They screenshot it, drop it in Slack, and write “the spacing under the chart looks off on the dashboard”. Now the work starts: I read the message, open the preview, guess which of four nearly-identical cards they meant, grep for the component, and only then make a two-line change.
The fix was never the expensive part. The translation was. And it is exactly the part a coding agent cannot do for me: it has the words and none of the pointing.
So I built Maple.

What it is
Maple is an open-source overlay that mounts into your app on preview builds. A reviewer clicks the thing that is wrong — an element, a region, a run of text — and says what is wrong with it. Maple records where they pointed, what they were looking at, and who they are, then:
- hands it to a coding agent over MCP, anchored to
file:line; - holds the merge until every comment is resolved.
It ships as six packages, all Apache-2.0, all on npm at 0.6.0, with no hosted service anywhere in the path:
@maple-kit/core— the server SDK, the overlay and the connector contracts.@maple-kit/mcp— the MCP server an agent talks to.@maple-kit/cli— themaplecommand.@maple-kit/react— bindings for the reviewer controller.@maple-kit/ui— the composed reviewer parts: the marks, the island, the composer.@maple-kit/classifier— model-backed scoring for the assist tier.
Why I built this myself
There is good work in this space already, and I went looking for it first.
Agentation is the closest thing in spirit and genuinely nice to use — you point at an element and your agent hears about it. It runs against localhost, which is the catch: the only person who can leave a comment is the person running the build. That is the engineer — and Maple is a collaboration experience, like most good work is.
Then there is the commercial tier: Vercel Toolbar puts comments on a deployment, and Pincushion, Chromatic, BugHerd and Marker.io all collect visual feedback well. Each of them does some of what I wanted. None of them does all four:
- Deployed previews, not localhost. Maple runs on the preview URL your CI already builds, so anyone holding the link can comment.
- A merge gate. CI blocks while a visual comment is open, and the check is real and named:
maple/visual-review. - An agent loop. The agent reads the comments, makes the change, and resolves them — the gate clears because the work is actually done.
- Open source. Apache-2.0, no account in the loop, nothing to migrate off later.
What the agent actually receives
Not a screenshot and a sentence. A comment carries the anchor, the viewport width at pick time, the theme, which disclosure elements were open, who wrote it and how strongly that identity is attested, plus a screenshot taken automatically when the pick commits.

The agent’s half of the loop is four MCP tools:
| Tool | What it is for |
|---|---|
list_comments | Everything on a branch, newest first. |
wait_for_comments | Block until something new arrives. |
get_comment_context | Everything needed to act on one comment. |
resolve_comment | Mark one addressed, naming the commit. |
The gate is the wedge
Every other tool in this space collects visual feedback and then trusts a human to remember it. Maple posts a maple/visual-review check and blocks the merge while a comment is unaddressed.
The decision is vendor-neutral and lives in core/gate; saying it to a forge is a connector’s job, because GitHub has check runs, GitLab has external status checks, and Bitbucket’s enforcement is Premium-only — three APIs over one decision.
import { decideGate } from "@maple-kit/core/gate";
const { comments } = await store.list({ branch });
const verdict = decideGate(comments, { statusTracked: supports(store, "setStatus") });
Maple stores nothing
No database, no bucket, no account. A connector is one file implementing plain Promise-returning methods — no registration step, no base class:
import type { StoreConnector } from "@maple-kit/core/connectors";
export function myStore(options: MyOptions): StoreConnector {
return {
name: "my-store",
async list(query) {
/* … */
},
async append(comment) {
/* … */
},
};
}
There are connectors for the comment store, media, identity, observability and the forge’s status check. A connector’s capabilities are exactly the methods it defines — there is no second field declaring what it supports, because two sources of truth eventually disagree and then you are debugging a lie. capabilitiesOf reads the object. A store with no setStatus keeps status client-side and the gate reports neutral instead of blocking.
The default store is a GitHub pull request comment, so a fresh install needs nothing you do not already have.
Where it is
In beta, and already usable. Maple is published and still being built, in the open.
- maple-kit.org — what it does, in about a minute.
- github.com/maple-kit/maple — the code, the docs and the design decisions behind every choice above.
If you try it, I would love to hear what broke and what you wished it did instead. Contributions are very welcome — issues, connectors, or an argument about a decision I got wrong.