Skip to content
Star on GitHub
Review and approveDesign tuner
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Design tuner

Tweak a component by hand or let the agent tune it. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add design-tuner
Pro
$24per editor, monthly
Unlimited framesAgent reviewsShared tokens
Pricing cardEdited by you
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add design-tuner

React

$npx designme add design-tuner --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Design tuner puts a live preview next to a few sliders, such as radius, padding, spacing and shadow, so a person can tweak a component by hand. A button hands the same sliders to the agent, which can move them while people watch and then explain what it changed.

  • When people and the agent should share control over a component's look.
  • When you want the agent's styling choices to be visible as slider moves, not hidden in code.
  • When designers want to fine-tune spacing or radius after the agent has made a first pass.
  • When you need a short note from the agent about why it picked the new values.
03

Usage

Import it and drive it from your own state.

Import

import { DesignTuner } from '@/components/designme/design-tuner/DesignTuner';

Example

The demo above as a React component, 41 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useEffect, useRef, useState } from 'react';
import { DesignTuner } from './DesignTuner';
import { useScript, useTakeover } from '../lib/useScript';

/* ---------- Design tuner: drag the sliders, or let the agent tune each one in turn ---------- */
const START = { rad: 16, pad: 18, gap: 12, sh: .08 };
const KEYS = ['rad', 'pad', 'gap', 'sh'] as const;
export default function DesignTunerDemo() {
  const [values, setValues] = useState<Record<string, number>>(START);
  const [status, setStatus] = useState('Edited by you');
  const [active, setActive] = useState<string>();
  const [note, setNote] = useState<string>();
  const [runs, setRuns] = useState(0);
  const live = useRef(values), ref = useRef<HTMLDivElement>(null), mine = useTakeover(ref);
  useEffect(() => { live.current = values; });
  // the agent tunes once on its own, unless you take over first
  useScript(async ({ wait }) => { await wait(1600); if (!mine()) setRuns(r => r || 1); });
  // each run aims at the other set of values, so Tune again always has something to do
  useScript(async ({ wait }) => {
    if (runs <= 0) return;
    const target = runs % 2 ? { rad: 20, pad: 24, gap: 14, sh: .05 } : { rad: 12, pad: 20, gap: 10, sh: .1 };
    setStatus('Agent is tuning'); setNote(undefined);
    for (const k of KEYS) {
      setActive(k);
      const from = live.current[k], to = target[k], t0 = performance.now();
      for (let p = 0; p < 1;) { await wait(16); p = Math.min(1, (performance.now() - t0) / 650); setValues(v => ({ ...v, [k]: from + (to - from) * (1 - Math.pow(1 - p, 3)) })); }
      await wait(120); setActive(undefined);
    }
    setNote('Matched the 4px grid and your card tokens. Softer shadow so the CTA leads.');
    setStatus('Tuned by agent'); setRuns(r => -r);
  }, [runs > 0 ? runs : 0]);
  return (
    <div ref={ref} style={{ width: '100%' }}>
      <DesignTuner values={values} onValuesChange={v => { setValues(v); setStatus('Edited by you'); }} status={status} active={active}
        tuning={runs > 0} note={note} actionLabel={runs ? 'Tune again' : 'Tune with agent'} onTune={() => setRuns(r => Math.abs(r) + 1)} />
    </div>
  );
}
04

Props

Everything DesignTuner takes.

PropTypeWhat it does
paramsTunerParam[]The sliders. Defaults to radius, padding, spacing and shadow for the sample card
valuesRecord<string, number>The value of each slider by key. Leave it out to let the sliders keep their own
defaultValuesRecord<string, number>
onValuesChange(values: Record<string, number>) => void
titlestringWhat is being tuned, like "Pricing card"
statusstringQuiet line beside the title, like "Edited by you" or "Agent is tuning"
activestringKey of the slider the agent is moving now; its value turns the accent colour
tuningbooleanThe agent is tuning: the button thinks and stops taking clicks
noteReactNodeWhat the agent changed and why, shown above the button
actionLabelstringThe button's label, like "Tune with agent" or "Tune again"
onTune() => void
preview(values: Record<string, number>, vars: CSSProperties) => ReactNodeWhat the sliders change. Gets the values and a style with a `--dm-<key>` variable for each one. Defaults to the sample pricing card from designme.dev
classNamestring
05

Code

The full source, as the CLI installs it.

DesignTuner.tsxReact · 123 lines
'use client';
import { useRef, useState, type CSSProperties, type KeyboardEvent, type PointerEvent, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { enter, Swap, Think } from '../lib/motion';
import '../designme.css';
import './design-tuner.css';

export interface TunerParam {
  /** Names the value and the CSS variable it sets on the preview, `--dm-<key>` */
  key: string;
  /** Shown beside the slider, like "Radius" */
  label: string;
  min: number;
  max: number;
  /** Where the slider starts when you don't pass values */
  value: number;
  /** Added after whole numbers, like "px". Leave it out for plain numbers */
  unit?: string;
  /** Shows and sets the value with this many decimals instead of rounding it, like 2 for a shadow strength */
  decimals?: number;
}

export interface DesignTunerProps {
  /** The sliders. Defaults to radius, padding, spacing and shadow for the sample card */
  params?: TunerParam[];
  /** The value of each slider by key. Leave it out to let the sliders keep their own */
  values?: Record<string, number>;
  defaultValues?: Record<string, number>;
  onValuesChange?: (values: Record<string, number>) => void;
  /** What is being tuned, like "Pricing card" */
  title?: string;
  /** Quiet line beside the title, like "Edited by you" or "Agent is tuning" */
  status?: string;
  /** Key of the slider the agent is moving now; its value turns the accent colour */
  active?: string;
  /** The agent is tuning: the button thinks and stops taking clicks */
  tuning?: boolean;
  /** What the agent changed and why, shown above the button */
  note?: ReactNode;
  /** The button's label, like "Tune with agent" or "Tune again" */
  actionLabel?: string;
  onTune?: () => void;
  /** What the sliders change. Gets the values and a style with a `--dm-<key>` variable for each one.
      Defaults to the sample pricing card from designme.dev */
  preview?: (values: Record<string, number>, vars: CSSProperties) => ReactNode;
  className?: string;
}

const PARAMS: TunerParam[] = [
  { key: 'rad', label: 'Radius', min: 0, max: 28, value: 16, unit: 'px' },
  { key: 'pad', label: 'Padding', min: 8, max: 32, value: 18, unit: 'px' },
  { key: 'gap', label: 'Spacing', min: 4, max: 24, value: 12, unit: 'px' },
  { key: 'sh', label: 'Shadow', min: 0, max: .2, value: .08, decimals: 2 }
];

const clamp = (p: TunerParam, v: number) => Math.max(p.min, Math.min(p.max, v));
const shown = (p: TunerParam, v: number) => p.decimals ? v.toFixed(p.decimals) : Math.round(v) + (p.unit ?? '');
const cssValue = (p: TunerParam, v: number) => p.decimals ? String(+v.toFixed(p.decimals)) : Math.round(v) + (p.unit ?? '');

/** The sample the site tunes: a pricing card that reads --dm-rad, --dm-pad, --dm-gap and --dm-sh */
function SampleCard({ vars }: { vars: CSSProperties }) {
  return (
    <div className="dm-dtx-card" style={vars}>
      <span className="dm-tag dm-b dm-dtx-tg">Pro</span>
      <div className="dm-dtx-pr"><b className="dm-num">$24</b><span className="dm-muted">per editor, monthly</span></div>
      <div className="dm-dtx-li">{['Unlimited frames', 'Agent reviews', 'Shared tokens'].map(t => <span key={t}><Icon name="check" size="xs" />{t}</span>)}</div>
      <button type="button" className="dm-pill dm-accent dm-dtx-cta">Start free trial</button>
    </div>
  );
}

function Slider({ param, value, hot, onChange }: { param: TunerParam; value: number; hot: boolean; onChange: (v: number) => void }) {
  const [drag, setDrag] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
  const pct = (value - param.min) / (param.max - param.min) * 100;
  const at = (e: PointerEvent) => { const b = ref.current!.getBoundingClientRect(); onChange(clamp(param, param.min + (param.max - param.min) * Math.max(0, Math.min(1, (e.clientX - b.left) / b.width)))); };
  const key = (e: KeyboardEvent) => {
    const dir = ({ ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 } as Record<string, number>)[e.key]; if (!dir) return;
    e.preventDefault(); onChange(clamp(param, value + dir * (param.decimals ? Math.pow(10, -param.decimals) : 1)));
  };
  return (
    <div className={cx('dm-dtx-r', hot && 'dm-hot')}>
      <span>{param.label}</span>
      <div ref={ref} className={cx('dm-dtx-sl', drag && 'dm-drag')} role="slider" tabIndex={0} aria-label={param.label}
        aria-valuemin={param.min} aria-valuemax={param.max} aria-valuenow={param.decimals ? +value.toFixed(param.decimals) : Math.round(value)} aria-valuetext={shown(param, value)}
        onPointerDown={e => { e.currentTarget.setPointerCapture(e.pointerId); setDrag(true); at(e); }}
        onPointerMove={e => { if (drag) at(e); }}
        onPointerUp={() => setDrag(false)} onPointerCancel={() => setDrag(false)} onKeyDown={key}>
        <span className="dm-f" style={{ width: pct + '%' }} /><span className="dm-k" style={{ left: pct + '%' }} />
      </div>
      <span className="dm-v dm-num">{shown(param, value)}</span>
    </div>
  );
}

/** A live preview beside a few sliders, with a button that hands the tuning to the agent. */
export function DesignTuner({ params = PARAMS, values, defaultValues, onValuesChange, title = 'Pricing card', status = 'Edited by you', active, tuning = false, note,
  actionLabel = 'Tune with agent', onTune, preview, className }: DesignTunerProps) {
  const [vals, setVals] = useControllable(values, defaultValues ?? Object.fromEntries(params.map(p => [p.key, p.value])), onValuesChange);
  const valueOf = (p: TunerParam) => clamp(p, vals[p.key] ?? p.value);
  const vars = Object.fromEntries(params.map(p => ['--dm-' + p.key, cssValue(p, valueOf(p))])) as CSSProperties;
  return (
    // the site lays this out by the width of its card, so the box around it is the container its query measures
    <div data-dm="" className={cx('dm-dtx-box', className)} style={{ containerType: 'inline-size', width: '100%' }}>
      <div className="dm-dtx">
        <div className="dm-dtx-prev">{preview ? preview(Object.fromEntries(params.map(p => [p.key, valueOf(p)])), vars) : <SampleCard vars={vars} />}</div>
        <div className="dm-dtx-insp dm-panel">
          <div className="dm-dtx-ih"><b>{title}</b><Swap className="dm-muted dm-dtx-st">{status}</Swap></div>
          <div className="dm-dtx-rows">
            {params.map(p => <Slider key={p.key} param={p} value={valueOf(p)} hot={active === p.key} onChange={v => setVals({ ...vals, [p.key]: v })} />)}
          </div>
          {note && <div className="dm-dtx-note dm-enter" style={enter().style}>{note}</div>}
          <button type="button" className={cx('dm-pill dm-primary dm-sm dm-dtx-go', tuning && 'dm-busy')} aria-busy={tuning} onClick={() => { if (!tuning) onTune?.(); }}>
            {tuning ? <><Think />Tuning</> : <><Icon name="wand" size="xs" />{actionLabel}</>}
          </button>
        </div>
      </div>
    </div>
  );
}
design-tuner.cssReact · 34 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Design tuner ---------- */
.dm-dtx{display:grid;grid-template-columns:1fr 280px;gap:14px;align-items:stretch}
@media (max-width:720px){.dm-dtx{grid-template-columns:1fr}}
@container (max-width:600px){.dm-dtx{grid-template-columns:1fr}.dm-dtx-prev{min-height:240px}}
.dm-dtx-prev{border-radius:18px;background:var(--dm-chip);display:grid;place-items:center;padding:var(--dm-s-6);min-height:300px;min-width:0}
.dm-dtx-card{--dm-rad:16px;--dm-pad:18px;--dm-gap:12px;--dm-sh:.08;width:min(250px,100%);background:var(--dm-surface);border-radius:var(--dm-rad);padding:var(--dm-pad);display:flex;flex-direction:column;gap:var(--dm-gap);box-shadow:0 1px 2px rgba(0,0,0,var(--dm-sh)),0 8px 24px rgba(0,0,0,calc(var(--dm-sh) * .8))}
.dm-dtx-tg{align-self:flex-start}
.dm-dtx-pr{display:flex;align-items:baseline;gap:6px}
.dm-dtx-pr b{font-size:14px;font-weight:500;letter-spacing:-.02em}
.dm-dtx-pr span{font-size:12px}
.dm-dtx-li{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--dm-fg2)}
.dm-dtx-li span{display:flex;align-items:center;gap:7px}
.dm-dtx-li svg{color:var(--dm-accent-fg,var(--dm-accent))}
.dm-dtx-cta{width:100%;border-radius:calc(var(--dm-rad) * .6)}
.dm-dtx-insp{padding:var(--dm-s-4);display:flex;flex-direction:column;gap:var(--dm-s-3);min-width:0}
.dm-dtx-ih{display:flex;align-items:baseline;justify-content:space-between;font-size:13.5px}
.dm-dtx-ih b{font-weight:500}
.dm-dtx-st{font-size:12px}
.dm-dtx-rows{display:flex;flex-direction:column;gap:10px}
.dm-dtx-r{display:grid;grid-template-columns:62px 1fr 40px;align-items:center;gap:10px;font-size:12.5px;color:var(--dm-fg2)}
.dm-dtx-r .dm-v{text-align:right;color:var(--dm-fg);font-size:12px}
.dm-dtx-r.dm-hot .dm-v{color:var(--dm-accent-fg,var(--dm-accent))}
.dm-dtx-sl{position:relative;height:22px;display:flex;align-items:center;cursor:pointer;touch-action:none}
.dm-dtx-sl::before{content:"";position:absolute;left:0;right:0;height:4px;border-radius:2px;background:var(--dm-line)}
.dm-dtx-sl .dm-f{position:absolute;left:0;height:4px;border-radius:2px;background:var(--dm-accent-mark,var(--dm-accent))}
.dm-dtx-sl .dm-k{position:absolute;width:14px;height:14px;margin-left:-7px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px color-mix(in srgb,var(--dm-line) 50%,transparent),0 1px 3px rgba(0,0,0,.18);transition:transform .2s}
.dm-dtx-sl.dm-drag .dm-k,.dm-dtx-sl:focus-visible .dm-k{transform:scale(1.15)}
@media (hover:hover) and (pointer:fine){.dm-dtx-sl:hover .dm-k{transform:scale(1.15)}}
.dm-dtx-sl:focus-visible{outline-offset:4px}
.dm-dtx-go{align-self:stretch;margin-top:auto}
.dm-dtx-go.dm-busy{pointer-events:none}
.dm-dtx-note{font-size:12px;color:var(--dm-fg2);padding:var(--dm-s-2) 10px;border-radius:10px;background:var(--dm-accent-bg);line-height:1.45}
design-tuner.htmlHTML · 18 lines
<div class="card wide" data-c="design-tuner">
  <div class="stage-in">
    <div class="dtx">
      <div class="dtx-prev"><div class="dtx-card">
        <span class="tag b dtx-tg">Pro</span>
        <div class="dtx-pr"><b class="num">$24</b><span class="muted">per editor, monthly</span></div>
        <div class="dtx-li"><span><svg class="i xs"><use href="#i-check"/></svg>Unlimited frames</span><span><svg class="i xs"><use href="#i-check"/></svg>Agent reviews</span><span><svg class="i xs"><use href="#i-check"/></svg>Shared tokens</span></div>
        <button class="pill accent dtx-cta">Start free trial</button>
      </div></div>
      <div class="dtx-insp panel">
        <div class="dtx-ih"><b>Pricing card</b><span class="muted dtx-st">Edited by you</span></div>
        <div class="dtx-rows"></div>
        <button class="pill primary sm dtx-go"><svg class="i xs"><use href="#i-wand"/></svg>Tune with agent</button>
      </div>
    </div>
  </div>
</div>
design-tuner.cssHTML · 33 lines
/* ---------- Design tuner ---------- */
.dtx{display:grid;grid-template-columns:1fr 280px;gap:14px;align-items:stretch}
@media (max-width:720px){.dtx{grid-template-columns:1fr}}
@container (max-width:600px){.dtx{grid-template-columns:1fr}.dtx-prev{min-height:240px}}
.dtx-prev{border-radius:18px;background:var(--chip);display:grid;place-items:center;padding:var(--s-6);min-height:300px;min-width:0}
.dtx-card{--rad:16px;--pad:18px;--gap:12px;--sh:.08;width:min(250px,100%);background:var(--surface);border-radius:var(--rad);padding:var(--pad);display:flex;flex-direction:column;gap:var(--gap);box-shadow:0 1px 2px rgba(0,0,0,var(--sh)),0 8px 24px rgba(0,0,0,calc(var(--sh) * .8))}
.dtx-tg{align-self:flex-start}
.dtx-pr{display:flex;align-items:baseline;gap:6px}
.dtx-pr b{font-size:14px;font-weight:500;letter-spacing:-.02em}
.dtx-pr span{font-size:12px}
.dtx-li{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--fg2)}
.dtx-li span{display:flex;align-items:center;gap:7px}
.dtx-li svg{color:var(--accent-fg,var(--accent))}
.dtx-cta{width:100%;border-radius:calc(var(--rad) * .6)}
.dtx-insp{padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-3);min-width:0}
.dtx-ih{display:flex;align-items:baseline;justify-content:space-between;font-size:13.5px}
.dtx-ih b{font-weight:500}
.dtx-st{font-size:12px}
.dtx-rows{display:flex;flex-direction:column;gap:10px}
.dtx-r{display:grid;grid-template-columns:62px 1fr 40px;align-items:center;gap:10px;font-size:12.5px;color:var(--fg2)}
.dtx-r .v{text-align:right;color:var(--fg);font-size:12px}
.dtx-r.hot .v{color:var(--accent-fg,var(--accent))}
.dtx-sl{position:relative;height:22px;display:flex;align-items:center;cursor:pointer;touch-action:none}
.dtx-sl::before{content:"";position:absolute;left:0;right:0;height:4px;border-radius:2px;background:var(--line)}
.dtx-sl .f{position:absolute;left:0;height:4px;border-radius:2px;background:var(--accent-mark,var(--accent))}
.dtx-sl .k{position:absolute;width:14px;height:14px;margin-left:-7px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px color-mix(in srgb,var(--line) 50%,transparent),0 1px 3px rgba(0,0,0,.18);transition:transform .2s}
.dtx-sl.drag .k,.dtx-sl:focus-visible .k{transform:scale(1.15)}
@media (hover:hover) and (pointer:fine){.dtx-sl:hover .k{transform:scale(1.15)}}
.dtx-sl:focus-visible{outline-offset:4px}
.dtx-go{align-self:stretch;margin-top:auto}
.dtx-go.busy{pointer-events:none}
.dtx-note{font-size:12px;color:var(--fg2);padding:var(--s-2) 10px;border-radius:10px;background:var(--accent-bg);line-height:1.45}
design-tuner.jsHTML · 32 lines
DM.def('design-tuner', (r, c) => {
  r.innerHTML = r.innerHTML.replace(/\{c\}/g, I('check','xs')).replace('{wand}', I('wand','xs'));
  const card = $('.dtx-card', r), rows = $('.dtx-rows', r), st = $('.dtx-st', r), go = $('.dtx-go', r);
  const P = [
    { k:'rad', n:'Radius', min:0, max:28, v:16, u:'px', t:20 },
    { k:'pad', n:'Padding', min:8, max:32, v:18, u:'px', t:24 },
    { k:'gap', n:'Spacing', min:4, max:24, v:12, u:'px', t:14 },
    { k:'sh', n:'Shadow', min:0, max:.2, v:.08, u:'', t:.05, f:2 }
  ];
  rows.innerHTML = P.map(p => `<div class="dtx-r" data-k="${p.k}"><span>${p.n}</span><div class="dtx-sl" role="slider" tabindex="0" aria-label="${p.n}" aria-valuemin="${p.min}" aria-valuemax="${p.max}"><span class="f"></span><span class="k"></span></div><span class="v num"></span></div>`).join('');
  function set(p, v){ p.v = Math.max(p.min, Math.min(p.max, v)); const pc = (p.v - p.min) / (p.max - p.min) * 100; const row = $(`[data-k="${p.k}"]`, rows);
    $('.f', row).style.width = pc + '%'; $('.k', row).style.left = pc + '%'; $('.v', row).textContent = p.f ? p.v.toFixed(p.f) : Math.round(p.v) + p.u;
    card.style.setProperty('--' + p.k, p.f ? p.v : Math.round(p.v) + 'px'); $('.dtx-sl', row).setAttribute('aria-valuenow', p.f ? p.v.toFixed(p.f) : Math.round(p.v)); }
  P.forEach(p => {
    set(p, p.v); const sl = $(`[data-k="${p.k}"] .dtx-sl`, rows);
    const at = e => { const b = sl.getBoundingClientRect(); set(p, p.min + (p.max - p.min) * Math.max(0, Math.min(1, (e.clientX - b.left) / b.width))); };
    sl.onpointerdown = e => { sl.setPointerCapture(e.pointerId); sl.classList.add('drag'); at(e); swap(st, 'Edited by you'); sl.onpointermove = at; };
    sl.onpointerup = sl.onpointercancel = () => { sl.classList.remove('drag'); sl.onpointermove = null; };
    sl.onkeydown = e => { const k = { ArrowRight:1, ArrowUp:1, ArrowLeft:-1, ArrowDown:-1 }[e.key]; if (!k) return; e.preventDefault(); set(p, p.v + k * (p.f ? .01 : 1)); swap(st, 'Edited by you'); };
  });
  let note;
  go.onclick = () => c.run(async () => {
    go.classList.add('busy'); go.innerHTML = '<span class="think on"><i></i><i></i><i></i><i></i></span>Tuning'; swap(st, 'Agent is tuning'); if (note) note.remove();
    for (const p of P){ const row = $(`[data-k="${p.k}"]`, rows); row.classList.add('hot'); await tween(p.v, p.t, 650, x => set(p, x)); await c.wait(120); row.classList.remove('hot'); }
    note = html('<div class="dtx-note">Matched the 4px grid and your card tokens. Softer shadow so the CTA leads.</div>'); go.before(note); enter(note);
    go.classList.remove('busy'); go.innerHTML = I('wand','xs') + 'Tune again'; swap(st, 'Tuned by agent');
    P.forEach(p => { const o = p.t; p.t = p.k === 'rad' ? (o === 20 ? 12 : 20) : p.k === 'pad' ? (o === 24 ? 20 : 24) : p.k === 'gap' ? (o === 14 ? 10 : 14) : (o === .05 ? .1 : .05); });
  });
  // the preview lets the agent tune once on its own, unless you reach for a slider or the button first
  c.auto(1600, () => go.click());
});
06

Questions

What people ask about it.

How do I let the agent move the sliders?
Control values and update them from your agent as it works. Set tuning and active while it runs so people can see which slider it is on.
How do I tune my own component instead of the sample card?
Pass preview, a function that gets the values and a style object. Spread the style onto your element and read the --dm-<key> variables in its CSS.
Can a slider hold decimal values?
Yes. Set decimals on the param, for example 2 for a shadow strength. The value is shown and set with that many decimals instead of being rounded.
07

More in Review and approve

Moments where a person checks, steers or signs off on the agent's work.

Change reviewAccept or reject each change the agent made, file by file.
Plan reviewCheck the agent's plan, edit steps, then let it run.
Permission gateThe agent asks before it touches something new.
Hand-off to a personThe agent hands a conversation to a person, with what they need to pick it up.
Decision promptThe agent asks a few quick questions before it starts.
Proposal cardA suggested change with confidence, alternatives and undo.
Rewrite toolbarSelect a sentence and let the agent rewrite it in place.
Launch checklistThe agent checks a design before it ships, and fixes what fails.
All 14 in Review and approve →