Coding agents made writing code cheap. Reviewing what they build did not get any faster.
You work on a new, big feature. You want feedback. You share a preview URL with your team and, best case, nobody has notes. (A myth, in my experience.)
Usually the notes arrive over the day, across three threads and a DM ("the spacing under the chart looks off", a screenshot, no other context) while you're deep in three other things. Then you spend the evening working out which note belongs to which task and translating each one into something your agent can act on. You come back, and it changed the wrong button.
The fix was never the expensive part. The translation was.
So I built Maple: comments are written directly on the app, UI edge cases are simulated in plain language, your agent gets all the context it needs to fix them, and CI enforces it. No middleman. A reviewer can point at a component, drag a box over part of the page, or select the words that are wrong.
What it is
Maple is an open-source frontend review toolkit: one route in your application, one script in the preview build. A reviewer points at what is wrong and says why. 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 also puts the page into whatever state someone wants to review, and checks the rules a machine can check without anyone looking.
It ships as eight packages, all Apache-2.0, all on npm with provenance, with no hosted service anywhere in the path:
-
@maple-kit/core: the server SDK, the overlay controller and the connector contracts. -
@maple-kit/uiand@maple-kit/react: the marks, the island and the composer, and the hooks underneath them. -
@maple-kit/mcpand@maple-kit/cli: the MCP server an agent talks to, and themaplecommand. -
@maple-kit/mock: rewrites a page's API responses into a named state. -
@maple-kit/lint: design-system rules, read off the rendered page. -
@maple-kit/classifier: scores a comment as it is written.
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 review is a team sport, 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
A build-time tagger marks every JSX element with the file, line and column it was written at, so a comment arrives pointing at the source, not at a CSS selector that breaks on the next deploy. Alongside it: the viewport width at pick time, the theme, which disclosure elements were open, who wrote it and how strongly that identity is attested, and a screenshot taken automatically when the pick commits.
Once the comments are written, the agent kicks in over MCP:
| 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, including the mock it was written under, with a link that reopens the page in that state. |
resolve_comment |
Mark one addressed, naming the commit, and tell the gate, so it clears without another push. |
There is also start_solo, which pairs a deployed preview with a bridge on your machine so you can review a preview alone. Wait, read, fix, resolve. Nobody translated anything.
Any state, as anyone
The states that most need a visual review are the hardest to reach on a preview: empty, error, forbidden, loading, one item, a thousand items. Nobody seeds a staging account with zero projects just to see whether the empty state is embarrassing. So nobody sees it until a customer does.
With Maple Mock, the reviewer types the state they want in natural language ("mock this page with an empty state") and the page refreshes with the matching API calls mocked.
It goes past data, too. Say "as a member, with the merge forecast on" and Maple reads the sentence into a role, permissions and feature flags. OpenFeature and LaunchDarkly flags are answered on the wire. The server still acts as the real reviewer, so a mutation sent under a mock really happens; the mock changes what the page is shown, not who you are.
A comment written under a mock remembers the mock. So when the agent picks it up, it can reopen the exact page the reviewer was complaining about (the empty one, as a billing manager, with the flag off) instead of staring at the happy path and saying "looks fine to me".
The gate is the wedge
Maple posts a maple/visual-review check and blocks the merge while a comment is unaddressed, and, if you want it to, until somebody actually approves the preview.
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") });
The rules nobody should have to point at
Some review comments shouldn't need a person at all. "That grey isn't one of our tokens." "This button is 20px tall." "This animates width." A machine can check those on every pull request, so @maple-kit/lint does.
It runs Chromium against the deployed preview and reads what the cascade, the theme and the media queries finally produced: off-token colours, sizes off the type scale, touch targets under 24px, text under WCAG AA contrast, motion on anything but opacity and transform, and motion that ignores prefers-reduced-motion. That last one is checked the only way you can from the outside: each viewport is read twice, once normally and once with reduced motion emulated, and whatever still moves in the second pass hard-coded its motion.
Maple stores nothing
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 six kinds: store, media, identity, observability, gate and classifier. A connector's capabilities are exactly the methods it defines. The default store is the pull request itself (one Maple comment per PR, reposted rather than edited), so a fresh install needs nothing you do not already have.
And if you want a second opinion while you type, plug in a classifier and the comment is scored as you write it, against five pillars: specific, actionable, concise, standalone and placed.
Where it is
At 0.18, built in the open, and already usable. 0.x makes no compatibility promise: when breaking an interface is the right shape I break it, and the changeset says what broke. The MCP server is listed in the official MCP Registry as io.github.maple-kit/maple.
What is not built yet: a hosted shared store (a file store in a preview pod loses its comments when the pod goes, so use GitHub or write a connector for now), a session replay link on every comment, accepting the agent's fix in the preview, and the whole overlay as terminal commands. Next up is letting the agent post its fix on the comment, with a before and after, so the reviewer accepts it right where they wrote it and the gate clears without a push.
To try it on an app you already run, with no accounts and nothing wired in: npx @maple-kit/cli review --port 5173, with your dev server's port. Comments are kept as files under .maple/.
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.
Top comments (6)
"the translation was the expensive part" — this is the most important sentence in the post.
everyone is optimizing the agent's speed, but nobody talks about the context gap. a comment like "the chart looks wrong" is useless to an agent. but a comment anchored to
chart.tsx:142with viewport width, theme, and the exact mock state (empty data, billing manager role, feature flag off)? that's actionable.the file:line anchoring via build-time jsx tagging is brilliant. css selectors break on every deploy, but source coordinates are stable. and the mock persistence — where a comment remembers the state it was written under — means the agent doesn't reopen the happy path and say "looks fine to me."
curious about one edge case: if a reviewer comments on a specific mock state (say, empty projects list), but the underlying data changes between review sessions, does the mock persist as a snapshot, or does the agent see the current state? how do you handle drift between "what the reviewer saw" and "what the agent sees now"?
massive respect for the apache-2.0, no-hosted-service approach. the merge gate enforcement is the wedge that makes this stick. 🐯
Thank you!! It means a lot.
As for your question - Maple saves the instructions for the mock, not a copy of the data.
When someone leaves a comment while a mock is on, Maple writes down which state they asked for, like "make the projects list empty." Later, the agent opens
the page with that same mock applied, using whatever data the server has now.
Whether that drifts depends on the state:
To cover the drift, every comment comes with a screenshot taken when the reviewer left it. The screenshot shows what they saw; the replayed page shows
what's there now. We don't save the actual data on purpose as it would make comments bigger.
that makes perfect sense. saving the mock instructions + a screenshot is the exact right trade-off. it preserves the visual context for the agent without bloating the database with redundant data payloads.
the distinction between stateful drift ("one item") and stateless drift ("empty", "error") is a really clean way to think about it. thanks for the detailed breakdown, nitzan. maple is shaping up to be a fantastic tool for real teams. 🐯
Ý tưởng kết hợp MCP để coding agent đọc trực tiếp comment ngay trên preview rất tiềm năng. Trước đây team mình hay gặp tình trạng feedback từ design hoặc QA bị trôi mất trong Slack hoặc Jira, dẫn đến việc dev phải nhảy qua nhảy lại giữa các tab để hiểu ý người dùng. Nếu Maple có thể tự động hóa việc đọc hiểu context này để cập nhật ticket hoặc sửa code ngay trong CI thì sẽ tiết kiệm được cực nhiều thời gian review thủ công. Mình cũng đang thử nghiệm các workflow tương tự để tối ưu hóa việc sử dụng các model AI mạnh cho công việc qua LabAgent, site: labagent .tech giúp việc truy cập các account pro ổn định hơn khi chạy agent liên tục.
One of the best open-source projects for truly bridging the gap between developers and PMs. I’ve already used it and really love it!
Quy trình review trên preview deploy này giải quyết được nỗi đau lớn nhất của team mình: feedback rơi vào Slack/GitHub thì dev phải context-switch liên tục, dễ bỏ sót khi merge. Cho comment trực tiếp lên app (overlay DOM) + agent đọc qua MCP để gom thành actionable task là tư duy đúng hướng — gần giống pattern "living spec" mà team mình tự build internal tool năm trước.
Điểm quan trọng: CI gate merge dựa trên review status. Cần đảm bảo flow này không thành bottleneck khi có hotfix hoặc emergency deploy. Team mình thường dùng label
skip-review+ audit trail để balance. Maple có support escape-hatch kiểu này không? Cũng tò mò phần agent parsing comment qua MCP — nó handle được nested thread / resolve-unresolve flow hay chỉ flat list? (site: labagent .tech)