Introducing Maple: The Frontend Review Toolkit Introducing Maple: The Frontend Review Toolkit

Introducing Maple: The Frontend Review Toolkit

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 (by mirrord, of course), 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.

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. More on both below.

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/ui and @maple-kit/react: the marks, the island and the composer, and the hooks underneath them.
  • @maple-kit/mcp and @maple-kit/cli: the MCP server an agent talks to, and the maple command.
  • @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.

Everything captured alongside one comment.

Once the comments are written, the agent kicks in. It has four MCP tools:

ToolWhat it is for
list_commentsEverything on a branch, newest first.
wait_for_commentsBlock until something new arrives.
get_comment_contextEverything needed to act on one comment, including the mock it was written under, with a link that replays it.
resolve_commentMark one addressed, naming the commit, and tell the gate, so it clears without another push.
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.

Real data, then empty, then failing. The banner always says which one you are looking at.

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.

One sentence in, a role and a flag out, and you get to check them before anything reloads.

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.

Two open comments, a red check. Resolve them and the merge button wakes up.

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.

An off-token colour that arrived through three layers of var() is invisible to a static linter, and plain here.

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.11, 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.

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.

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.⁠

#maple #ai #frontend #engineering #open-source