PixelDosa UI

Autonomy Control

How much the agent does without asking, explained through its real actions. Users can make it more careful per action, never less careful than the risk allows.

How much the agent does on its own

What that means

Does 2 of 5 without asking

  • Label and archive emailDoes it, tells you
  • Draft repliesDoes it, tells you
  • Accept meeting invitesAsks first
  • Unsubscribe from listsAsks first
  • Send email for youAsks firstHigh risk · always asks

Installation

bash
npx shadcn@latest add @pixeldosa/autonomy-control

Requires `cn` at `@/lib/utils`. List real actions in the user's words, and put anything irreversible or external at `risk: "high"`. Enforce the same rule server-side: this control shows the policy, it doesn't enforce it.

Usage

"use client";

import { AutonomyControl } from "@/components/ui/autonomy-control";

<AutonomyControl
  level={settings.level}
  onLevelChange={(level) => save({ level })}
  alwaysAsk={settings.alwaysAsk}
  onAlwaysAskChange={(alwaysAsk) => save({ alwaysAsk })}
  actions={[
    { id: "label", label: "Label and archive email", risk: "low" },
    { id: "calendar", label: "Accept meeting invites", risk: "medium" },
    { id: "send", label: "Send email for you", risk: "high" },
  ]}
/>

Why it exists

People ask agents to be less autonomous, and to have an off switch. Most products offer a slider from "cautious" to "autonomous", which tells nobody what will happen to their inbox. This control explains every level through the agent's actual actions.

Features

  • Concrete outcomes. Every action reads "Asks first" or "Does it, tells you" for the chosen level.
  • A floor you can raise, never lower. Pin any action to Always ask. High-risk actions ask at every level. That isn't a setting, on purpose.
  • Announced changes. "Does 2 of 5 without asking" updates politely when the level changes, because every row changed with it.
  • Native radios, so arrow keys and form semantics work without extra code.

The policy

RiskAsk me firstAsk when it mattersAct, then tell me
lowAsks firstDoes it, tells youDoes it, tells you
mediumAsks firstAsks firstDoes it, tells you
highAsks firstAsks firstAsks first

Examples

Deciding whether to show an approval, with the same rule the control displays:

function needsApproval(action, { level, alwaysAsk }) {
  if (action.risk === "high" || alwaysAsk.includes(action.id)) return true;
  if (level === "ask") return true;
  return level === "balanced" && action.risk === "medium";
}

{needsApproval(action, settings) ? <AIApprovalGate {...gate} /> : execute(action)}

Props

PropTypeDescription
level"ask" | "balanced" | "act"Required.
onLevelChange(level) => voidRequired.
actions{ id, label, risk }[]Required. Real actions, in the user's words.
alwaysAskstring[]Action ids pinned to asking.
onAlwaysAskChange(ids) => voidShows the Always ask checkboxes.
labelstringThe fieldset legend.
headingLevel2 | 3 | 4 | 5 | 6Heading level, so the component fits your page's outline. Defaults to 3.

Accessibility

  • Levels are a <fieldset> of native radios with a legend.
  • Each Always ask checkbox is named after its action ("Always ask before: Draft replies").
  • The live count is a polite status region.

When not to use

This isn't the approval itself. That's AI Approval Gate, and this control decides when to show one. It also doesn't enforce anything: apply the same policy on the server.

why it's built this way

Engineering Notes

Research on agent products keeps finding people asking for lower automation and an off switch, while the 'match autonomy to risk' principle has no UI anywhere. Most autonomy settings fail because they are abstract. A slider labelled cautious-to-autonomous doesn't tell anyone what will happen to their inbox. So each level is explained through the agent's actual actions, with the outcome for each in words. The policy is a pure function of level and risk: 'ask' asks for everything, 'balanced' acts only on low risk, 'act' acts on low and medium. High risk asks at every level, and that is deliberately not configurable, because a setting that can turn off the approval for an irreversible send is a trap with a friendly label. User overrides only move in the safe direction: 'Always ask' pins an action to asking, and nothing un-pins the floor. Levels are native radios in a fieldset, so arrow keys, labels and form semantics come for free. The 'Does N of M without asking' count sits in a polite live region, because changing the level silently changes every row below it.

how it moves, and when it doesn't

Motion Notes

Still. This is a settings surface and the user's turn, so nothing animates on its own. Selection and hover change background and border colour at `--pd-duration-instant`. Row outcomes swap text instantly when the level changes, not with a transition, so the new policy can be read the moment it applies. The focus ring follows the radio via `has-[:focus-visible]`. Under prefers-reduced-motion the colour transitions become instant.