Skip to content
Star on GitHub
WorkspaceAgent settings
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

Agent settings

How much the agent may do alone, with a save bar for changes. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add agent-settings
AutonomyAsks before edits to shared files
Use team memoryBrand rules and past decisions
Post a summary in Slack#design, after each run
Spend cap per runPauses and asks when reached
$5
Instructions
Unsaved changes
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add agent-settings

React

$npx designme add agent-settings --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.

A settings panel for how much an agent may do on its own: an autonomy scale, on/off switches, a spend cap per run and free-text instructions. Any change brings up an "Unsaved changes" bar with Save and Reset, so people can review before the agent's behaviour changes.

  • When people need to decide whether the agent asks first or works alone.
  • When you want to cap what one agent run may spend.
  • When standing instructions for the agent should live next to its other settings.
  • When several settings changes should be reviewed and saved together.
03

Usage

Import it and drive it from your own state.

Import

import { AgentSettings } from '@/components/designme/agent-settings/AgentSettings';

Example

The demo above as a React component, 26 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 { useRef, useState } from 'react';
import { AgentSettings, type AgentSettingsValue } from './AgentSettings';
import { useStory } from '../lib/useScript';

/* ---------- Agent settings: Slack summaries get switched on, and the save bar comes up ---------- */
export default function AgentSettingsDemo() {
  const [value, setValue] = useState<AgentSettingsValue>({
    autonomy: 1, toggles: { memory: true, slack: false }, cap: 5, instructions: 'Use our tokens only. Keep copy short and friendly.'
  });
  const ref = useRef<HTMLDivElement>(null);
  // the preview flips one switch on its own, unless you take over first
  useStory(ref, async ({ wait }) => {
    await wait(1400);
    setValue(v => ({ ...v, toggles: { ...v.toggles, slack: !v.toggles.slack } }));
  });
  return (
    <div ref={ref} style={{ width: '100%' }}>
      <AgentSettings value={value} onValueChange={setValue}
        toggles={[{ key: 'memory', label: 'Use team memory', detail: 'Brand rules and past decisions' }, { key: 'slack', label: 'Post a summary in Slack', detail: '#design, after each run' }]}
        onSave={() => new Promise<void>(res => setTimeout(res, 600))} />
    </div>
  );
}
04

Props

Everything AgentSettings takes.

PropTypeWhat it does
levelsAgentSettingsLevel[]The autonomy scale. Defaults to Ask, Balanced and Autopilot
togglesAgentSettingsToggle[]
capsnumber[]The amounts the spend cap steps through with − and +
formatCap(cap: number) => stringHow the cap reads. Defaults to "$5"
capLabelstring
capDetailstring
valueAgentSettingsValueThe settings. Leave it out to let the panel hold its own
defaultValueAgentSettingsValue
onValueChange(value: AgentSettingsValue) => void
onSave(value: AgentSettingsValue) => void | Promise<void>Called by Save. Return a promise and the button spins until it settles. Until then, the save bar stays up
onReset() => voidCalled by Reset, after the settings go back to the last saved ones
classNamestring
05

Code

The full source, as the CLI installs it.

AgentSettings.tsxReact · 133 lines
'use client';
import { useEffect, useRef, useState } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useIsoLayoutEffect } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { Spin, Swap } from '../lib/motion';
import '../designme.css';
import './agent-settings.css';

/** A step on the autonomy scale */
export interface AgentSettingsLevel {
  /** On the segment, like "Balanced" */
  label: string;
  /** Under "Autonomy" while it is picked, like "Asks before edits to shared files" */
  description: string;
}

/** An on/off setting */
export interface AgentSettingsToggle {
  key: string;
  /** Like "Use team memory" */
  label: string;
  /** The quiet line under it, like "Brand rules and past decisions" */
  detail?: string;
}

/** Everything the panel edits */
export interface AgentSettingsValue {
  /** Index into `levels` */
  autonomy: number;
  /** On or off, by toggle key */
  toggles: Record<string, boolean>;
  /** The spend cap, one of `caps` */
  cap: number;
  instructions: string;
}

export interface AgentSettingsProps {
  /** The autonomy scale. Defaults to Ask, Balanced and Autopilot */
  levels?: AgentSettingsLevel[];
  toggles?: AgentSettingsToggle[];
  /** The amounts the spend cap steps through with − and + */
  caps?: number[];
  /** How the cap reads. Defaults to "$5" */
  formatCap?: (cap: number) => string;
  capLabel?: string;
  capDetail?: string;
  /** The settings. Leave it out to let the panel hold its own */
  value?: AgentSettingsValue;
  defaultValue?: AgentSettingsValue;
  onValueChange?: (value: AgentSettingsValue) => void;
  /** Called by Save. Return a promise and the button spins until it settles. Until then, the save bar stays up */
  onSave?: (value: AgentSettingsValue) => void | Promise<void>;
  /** Called by Reset, after the settings go back to the last saved ones */
  onReset?: () => void;
  className?: string;
}

const LEVELS: AgentSettingsLevel[] = [
  { label: 'Ask', description: 'Asks before every change' },
  { label: 'Balanced', description: 'Asks before edits to shared files' },
  { label: 'Autopilot', description: 'Works alone, asks only before publishing' }
];
const CAPS = [1, 2, 5, 10, 20, 50];
const same = (a: AgentSettingsValue, b: AgentSettingsValue) => JSON.stringify(a) === JSON.stringify(b);

/** How much the agent may do alone. Any change brings up a save bar, which saves or resets them. */
export function AgentSettings({
  levels = LEVELS, toggles = [], caps = CAPS, formatCap = n => '$' + n, capLabel = 'Spend cap per run', capDetail = 'Pauses and asks when reached',
  value, defaultValue, onValueChange, onSave, onReset, className
}: AgentSettingsProps) {
  const [v, setV] = useControllable(value, defaultValue ?? { autonomy: 1, toggles: {}, cap: caps[Math.min(2, caps.length - 1)], instructions: '' }, onValueChange);
  // what the save bar compares against: the settings as they were last saved
  const [saved, setSaved] = useState(v);
  const [phase, setPhase] = useState<'idle' | 'saving' | 'saved'>('idle');
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
  useEffect(() => () => clearTimeout(timer.current), []);
  const dirty = !same(v, saved) || phase !== 'idle';
  // when the bar of unsaved changes hides with the focus in it, the focus goes to the top of the form instead of the page
  const root = useRef<HTMLDivElement>(null), bar = useRef<HTMLDivElement>(null);
  useIsoLayoutEffect(() => { if (!dirty && bar.current?.contains(document.activeElement)) root.current?.querySelector<HTMLElement>('[aria-checked="true"], button')?.focus({ preventScroll: true }); }, [dirty]);

  const n = levels.length, a = Math.max(0, Math.min(n - 1, v.autonomy));
  const ci = Math.max(0, caps.indexOf(v.cap));
  const set = (patch: Partial<AgentSettingsValue>) => setV({ ...v, ...patch });
  const save = async () => {
    if (phase !== 'idle') return;
    setPhase('saving');
    try { await onSave?.(v); } catch { setPhase('idle'); return; }
    setSaved(v); setPhase('saved');
    timer.current = setTimeout(() => setPhase('idle'), 700);
  };
  const reset = () => { setV(saved); onReset?.(); };

  return (
    <div data-dm="" ref={root} className={cx('dm-asx dm-panel', dirty && 'dm-dirty', className)}>
      <div className="dm-asx-r dm-asx-col">
        <div className="dm-asx-l"><b>Autonomy</b><Swap className="dm-muted dm-asx-ad">{levels[a]?.description ?? ''}</Swap></div>
        <div className="dm-asx-steps" role="radiogroup" aria-label="Autonomy" style={n === 3 ? undefined : { gridTemplateColumns: `repeat(${n},1fr)` }}>
          <span className="dm-asx-knob" style={{ left: `calc(3px + ${a} * (100% - 6px) / ${n})`, ...(n === 3 ? {} : { width: `calc((100% - 6px) / ${n})` }) }} />
          {levels.map((l, i) => <button key={l.label} type="button" role="radio" aria-checked={i === a} onClick={() => set({ autonomy: i })}>{l.label}</button>)}
        </div>
      </div>
      {toggles.map(t => (
        <div key={t.key} className="dm-asx-r">
          <div className="dm-asx-l"><b>{t.label}</b>{t.detail && <span className="dm-muted">{t.detail}</span>}</div>
          <button type="button" className="dm-toggle" role="switch" aria-checked={!!v.toggles[t.key]} aria-label={t.label}
            onClick={() => set({ toggles: { ...v.toggles, [t.key]: !v.toggles[t.key] } })} />
        </div>
      ))}
      <div className="dm-asx-r">
        <div className="dm-asx-l"><b>{capLabel}</b>{capDetail && <span className="dm-muted">{capDetail}</span>}</div>
        <div className="dm-asx-cap">
          <button type="button" className="dm-ib dm-fill dm-asx-m" aria-label="Lower cap" onClick={() => set({ cap: caps[Math.max(0, ci - 1)] })}><Icon name="minus" size="xs" /></button>
          <Swap className="dm-num dm-asx-v">{formatCap(v.cap)}</Swap>
          <button type="button" className="dm-ib dm-fill dm-asx-p" aria-label="Raise cap" onClick={() => set({ cap: caps[Math.min(caps.length - 1, ci + 1)] })}><Icon name="plus" size="xs" /></button>
        </div>
      </div>
      <div className="dm-asx-r dm-asx-col">
        <div className="dm-asx-l"><b>Instructions</b></div>
        <textarea className="dm-asx-ta" rows={2} spellCheck={false} aria-label="Instructions" value={v.instructions} onChange={e => set({ instructions: e.target.value })} />
      </div>
      <div ref={bar} className="dm-asx-bar" inert={!dirty} aria-hidden={!dirty}>
        <span>Unsaved changes</span>
        <button type="button" className="dm-pill dm-ghost dm-sm dm-asx-rs" onClick={reset}>Reset</button>
        <button type="button" className="dm-pill dm-accent dm-sm dm-asx-sv" onClick={save}>
          {phase === 'saving' ? <Spin /> : phase === 'saved' ? <><Icon name="check" size="xs" />Saved</> : 'Save'}
        </button>
      </div>
    </div>
  );
}
agent-settings.cssReact · 25 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Agent settings ---------- */
.dm-asx{padding:6px 14px 14px;display:flex;flex-direction:column;position:relative;overflow:hidden}
.dm-asx-r{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-3);padding:11px 0;border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-asx-r:nth-last-child(2){border-bottom:0}
.dm-asx-col{flex-direction:column;align-items:stretch;gap:9px}
.dm-asx-l{display:flex;flex-direction:column;min-width:0;font-size:13px}
.dm-asx-l b{font-weight:500}
.dm-asx-l .dm-muted{font-size:12px}
.dm-asx-steps{position:relative;display:grid;grid-template-columns:repeat(3,1fr);padding:3px;border-radius:var(--dm-r-pill);background:var(--dm-chip)}
.dm-asx-steps button{position:relative;z-index:1;height:30px;font-size:12.5px;color:var(--dm-fg2);transition:color .25s}
.dm-asx-steps button[aria-checked="true"]{color:var(--dm-fg)}
.dm-asx-knob{position:absolute;top:3px;bottom:3px;width:calc((100% - 6px)/3);border-radius:var(--dm-r-pill);background:var(--dm-surface);box-shadow:0 1px 2px rgba(0,0,0,.08);transition:left .45s var(--dm-ease)}
.dm-asx-cap{display:flex;align-items:center;gap:6px}
.dm-asx-cap .dm-ib{width:26px;height:26px}
.dm-asx-v{min-width:38px;text-align:center;font-size:13px}
.dm-asx-ta{display:block;width:100%;resize:none;border:0;outline:0;border-radius:var(--dm-r-field);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);padding:10px var(--dm-s-3);font:inherit;font-size:13.5px;line-height:1.5;color:var(--dm-fg);transition:box-shadow var(--dm-t-fast)}
@supports (corner-shape:squircle){.dm-asx-ta{corner-shape:squircle;border-radius:var(--dm-r-field-sq)}}
.dm-asx-ta:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
.dm-asx-bar{display:flex;align-items:center;gap:6px;padding:6px 6px 6px 14px;border-radius:var(--dm-r-pill);background:var(--dm-ink);color:var(--dm-on-ink);font-size:12.5px;margin-top:10px;transform:translateY(30px);opacity:0;filter:blur(4px);pointer-events:none;transition:transform .45s var(--dm-ease),opacity .3s,filter .3s,height .45s var(--dm-ease),margin-bottom .45s var(--dm-ease);height:0;margin-bottom:-10px}
/* on an ink-filled bar the ring takes the bar's text colour, so it stands out against the fill in every theme */
.dm-asx-bar span{flex:1}
.dm-asx-bar .dm-pill.dm-ghost{color:inherit;opacity:.8}@media (hover:hover) and (pointer:fine){.dm-asx-bar .dm-pill.dm-ghost:hover{background:color-mix(in srgb,var(--dm-on-ink) 14%,transparent)}}
.dm-asx.dm-dirty .dm-asx-bar{transform:none;opacity:1;filter:none;pointer-events:auto;height:40px;margin-bottom:0}
agent-settings.htmlHTML · 14 lines
<div class="card" data-c="agent-settings">
  <div class="stage-in">
    <div class="asx panel">
      <div class="asx-r asx-col"><div class="asx-l"><b>Autonomy</b><span class="muted asx-ad">Asks before edits to shared files</span></div>
        <div class="asx-steps" role="radiogroup" aria-label="Autonomy"><span class="asx-knob"></span><button role="radio" data-v="0">Ask</button><button role="radio" data-v="1" aria-checked="true">Balanced</button><button role="radio" data-v="2">Autopilot</button></div></div>
      <div class="asx-r"><div class="asx-l"><b>Use team memory</b><span class="muted">Brand rules and past decisions</span></div><button class="toggle" role="switch" aria-checked="true" aria-label="Use team memory"></button></div>
      <div class="asx-r"><div class="asx-l"><b>Post a summary in Slack</b><span class="muted">#design, after each run</span></div><button class="toggle" role="switch" aria-checked="false" aria-label="Post a summary in Slack"></button></div>
      <div class="asx-r"><div class="asx-l"><b>Spend cap per run</b><span class="muted">Pauses and asks when reached</span></div><div class="asx-cap"><button class="ib fill asx-m" aria-label="Lower cap"><svg class="i xs"><use href="#i-minus"/></svg></button><span class="num asx-v">$5</span><button class="ib fill asx-p" aria-label="Raise cap"><svg class="i xs"><use href="#i-plus"/></svg></button></div></div>
      <div class="asx-r asx-col"><div class="asx-l"><b>Instructions</b></div><textarea class="asx-ta" rows="2" spellcheck="false">Use our tokens only. Keep copy short and friendly.</textarea></div>
      <div class="asx-bar"><span>Unsaved changes</span><button class="pill ghost sm asx-rs">Reset</button><button class="pill accent sm asx-sv">Save</button></div>
    </div>
  </div>
</div>
agent-settings.cssHTML · 24 lines
/* ---------- Agent settings ---------- */
.asx{padding:6px 14px 14px;display:flex;flex-direction:column;position:relative;overflow:hidden}
.asx-r{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);padding:11px 0;border-bottom:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.asx-r:nth-last-child(2){border-bottom:0}
.asx-col{flex-direction:column;align-items:stretch;gap:9px}
.asx-l{display:flex;flex-direction:column;min-width:0;font-size:13px}
.asx-l b{font-weight:500}
.asx-l .muted{font-size:12px}
.asx-steps{position:relative;display:grid;grid-template-columns:repeat(3,1fr);padding:3px;border-radius:var(--r-pill);background:var(--chip)}
.asx-steps button{position:relative;z-index:1;height:30px;font-size:12.5px;color:var(--fg2);transition:color .25s}
.asx-steps button[aria-checked="true"]{color:var(--fg)}
.asx-knob{position:absolute;top:3px;bottom:3px;width:calc((100% - 6px)/3);border-radius:var(--r-pill);background:var(--surface);box-shadow:0 1px 2px rgba(0,0,0,.08);transition:left .45s var(--ease)}
.asx-cap{display:flex;align-items:center;gap:6px}
.asx-cap .ib{width:26px;height:26px}
.asx-v{min-width:38px;text-align:center;font-size:13px}
.asx-ta{display:block;width:100%;resize:none;border:0;outline:0;border-radius:var(--r-field);background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line) 50%,transparent);padding:10px var(--s-3);font:inherit;font-size:13.5px;line-height:1.5;color:var(--fg);transition:box-shadow var(--t-fast)}
@supports (corner-shape:squircle){.asx-ta{corner-shape:squircle;border-radius:var(--r-field-sq)}}
.asx-ta:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line-strong) 50%,transparent)}
.asx-bar{display:flex;align-items:center;gap:6px;padding:6px 6px 6px 14px;border-radius:var(--r-pill);background:var(--ink);color:var(--on-ink);font-size:12.5px;margin-top:10px;transform:translateY(30px);opacity:0;filter:blur(4px);pointer-events:none;transition:transform .45s var(--ease),opacity .3s,filter .3s,height .45s var(--ease),margin-bottom .45s var(--ease);height:0;margin-bottom:-10px}
/* on an ink-filled bar the ring takes the bar's text colour, so it stands out against the fill in every theme */
.asx-bar span{flex:1}
.asx-bar .pill.ghost{color:inherit;opacity:.8}@media (hover:hover) and (pointer:fine){.asx-bar .pill.ghost:hover{background:color-mix(in srgb,var(--on-ink) 14%,transparent)}}
.asx.dirty .asx-bar{transform:none;opacity:1;filter:none;pointer-events:auto;height:40px;margin-bottom:0}
agent-settings.jsHTML · 24 lines
DM.def('agent-settings', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{minus}', I('minus','xs')).replace('{plus}', I('plus','xs'));
  const root = $('.asx', r), steps = $$('.asx-steps button', r), knob = $('.asx-knob', r), ad = $('.asx-ad', r), tg = $$('.toggle', r), v = $('.asx-v', r), ta = $('.asx-ta', r);
  const AD = ['Asks before every change', 'Asks before edits to shared files', 'Works alone, asks only before publishing'];
  const CAPS = [1, 2, 5, 10, 20, 50];
  let s = { a:1, t:[true,false], cap:2, i:ta.value }, saved = JSON.stringify(s);
  // the bar of unsaved changes is out of reach while it's hidden; when it hides with the focus in it, the focus goes to the
  // top of the form instead of the page
  const dirty = () => { const bar = $('.asx-bar', r), was = root.classList.contains('dirty'), on = root.classList.toggle('dirty', JSON.stringify(s) !== saved); bar.inert = !on;
    if (was && !on && bar.contains(document.activeElement)){ const to = $('[aria-checked="true"]', r) || $('button', r); if (to) to.focus({ preventScroll:true }); } };
  function setA(i, init){ s.a = i; steps.forEach((b, j) => b.setAttribute('aria-checked', j === i)); knob.style.left = `calc(3px + ${i} * (100% - 6px) / 3)`; if (!init) swap(ad, AD[i]); dirty(); }
  steps.forEach((b, i) => b.onclick = () => setA(i));
  tg.forEach((t, i) => t.onclick = () => { s.t[i] = !s.t[i]; t.setAttribute('aria-checked', s.t[i]); dirty(); });
  const cap = d => { s.cap = Math.max(0, Math.min(CAPS.length - 1, s.cap + d)); swap(v, '$' + CAPS[s.cap]); dirty(); };
  $('.asx-m', r).onclick = () => cap(-1); $('.asx-p', r).onclick = () => cap(1);
  ta.oninput = () => { s.i = ta.value; dirty(); };
  function apply(o){ s = JSON.parse(o); setA(s.a, true); swap(ad, AD[s.a]); tg.forEach((t, i) => t.setAttribute('aria-checked', s.t[i])); v.textContent = '$' + CAPS[s.cap]; ta.value = s.i; dirty(); }
  $('.asx-rs', r).onclick = () => apply(saved);
  $('.asx-sv', r).onclick = () => c.run(async () => { const b = $('.asx-sv', r); b.innerHTML = '<span class="spin"></span>'; await c.wait(600); saved = JSON.stringify(s); b.innerHTML = I('check','xs') + 'Saved'; await c.wait(700); dirty(); b.textContent = 'Save'; });
  setA(1, true);
  // the preview flips one switch on its own, unless you take over first
  c.auto(1400, () => tg[1].click());
});
06

Questions

What people ask about it.

What happens if saving fails?
If the promise returned by onSave rejects, the button goes back to Save and the save bar stays up, because the settings still differ from the last saved ones.
Can I change the autonomy levels?
Yes. Pass levels with a label and description for each step. The scale lays out as many steps as you give it, and autonomy in the value is the index of the picked one.
How do I add my own on/off settings?
Pass toggles with a key, label and optional detail for each. Their state lives in value.toggles, keyed by key.
Can the spend cap use other amounts or a currency?
Yes. caps sets the amounts the minus and plus buttons step through, and formatCap sets how the amount reads. capLabel and capDetail change the text beside it.
07

More in Workspace

Navigation, search, settings, models and automations around the agent.

Automation canvasA trigger, a few agent steps, and a run you can watch flow through.
Canvas toolbarA floating toolbar for a workflow canvas: run, pick a tool, comment, fit and zoom.
Command paletteJump anywhere or hand a task to the agent from one box.
Memory panelWhat the agent remembers about your team, editable and removable.
Model compareRun one prompt on three models side by side and keep the best.
Tools and connectionsThe tools the agent can reach, and how much it may do with each.
Knowledge sourcesThe files, sites and folders the agent knows, and how far each is indexed.
Scheduled runsTasks the agent runs on a schedule, with their last result and a switch.
All 11 in Workspace →