Skip to content
RU
Scale
Density
Theme
Accent

Agent layer

Approval request

JS Sending the decision and moving the block into a decided state belong to the application layer

The defining interaction of agent systems: the agent stops and asks permission. Everything else on the screen can be scrolled past — this cannot be, so the block is the only one in the library entitled to stop the eye.

The component
Confirmation is required
Write 4 files into terrain/
  • heightmap.ts — overwrite
  • chunks.bin — deletion
Markup
<div class="inst-approval" data-state="pending" role="group" aria-labelledby="ap1">
  <div class="inst-approval-head" id="ap1">Confirmation is required</div>
  <div class="inst-approval-what">Write 4 files into <code>terrain/</code></div>
  <ul class="inst-approval-effects">
    <li>heightmap.ts — overwrite</li>
    <li data-tone="error">chunks.bin — deletion</li>
  </ul>
  <div class="inst-approval-actions">
    <button class="inst-btn inst-btn--sm inst-btn--primary" type="button">Allow</button>
    <button class="inst-btn inst-btn--sm" type="button">Deny</button>
  </div>
</div>

Contract

Without any one of them the component does not do its work and turns into a decorated "are you sure?".

What The class Why it is required
What exactly will be done inst-approval-what "Allow the action?" is not a question: the user does not know what they are answering
What it will touch inst-approval-effects The list of consequences. The irreversible ones are marked with the error tone
A decision in one press inst-approval-actions Allow and deny side by side. The absence of "deny" turns a request into a notification

Accessibility

The group A role="group" + an aria-labelledby pointing at the heading. Otherwise the block falls apart into unrelated paragraphs
Appearing A request arises on the machine's initiative, so it has to reach a live region: an aria-live="assertive" or a move of the focus to the block. A request that appeared quietly will wait for ever
The order of the actions "Allow" first. The row is aligned to the start, and the principal action stands at the outer edge of the row — the one signal of which of the two is the principal one
Denial is always available The "Deny" button cannot be hidden in a menu. A refusal has to cost as many presses as consent
Not colour alone An irreversible consequence is marked by a tone and a word ("deletion") rather than by redness alone
The focus after an answer The actions disappear along with the answer — the application has to move the focus, otherwise it falls into the <body>

Anatomy

<ul class="inst-approval-effects">
  <li>heightmap.ts — overwrite</li>
  <li data-tone="warn">config.json — a change of settings</li>
  <li data-tone="error">chunks.bin — deletion</li>
</ul>

A tone here means irreversibility. A deletion is red because it cannot be undone — rather than because it is "bad".

States

Example
Confirmation is required
Write 4 files
Confirmation is required
Write 4 files
Allowed at 14:32
Markup
<div class="inst-approval" data-state="pending" role="group" aria-label="Waiting">
  <div class="inst-approval-head">Confirmation is required</div>
  <div class="inst-approval-what">Write 4 files</div>
  <div class="inst-approval-actions">
    <button class="inst-btn inst-btn--sm inst-btn--primary" type="button">Allow</button>
    <button class="inst-btn inst-btn--sm" type="button">Deny</button>
  </div>
</div>
<div class="inst-approval" data-state="approved" role="group" aria-label="Allowed">
  <div class="inst-approval-head">Confirmation is required</div>
  <div class="inst-approval-what">Write 4 files</div>
  <div class="inst-approval-verdict">Allowed at 14:32</div>
</div>
data-state What happens
pending Waiting for an answer. The only one where the actions are shown
approved Allowed. The block steps back, the actions are hidden
denied Denied. The description of the action is struck through

After an answer the block neither disappears nor goes out — it steps back. The history of decisions stays readable: a user has to be able to come back and see what exactly they allowed an hour ago. A request that vanished cannot be checked.

JS

Include the module once per page — there is no need to initialise the components one by one, instrument.js works by delegation and sees nodes that arrived later.

<script type="module" src="instrument.js"></script>

What instrument.js does

Nothing. Here it honestly has no part: consent to an agent's action is a decision rather than the behaviour of a widget, and it cannot be faked by styling.

What the application has to do

  1. Send the decision to where it is carried out.
  2. Move the block into a decided state — otherwise the buttons stay live and a person presses a second time.
  3. Leave visible what was decided: a block of consent is a trace in the history rather than a dialog that closed.
block.addEventListener('click', async (e) => {
  const btn = e.target.closest('[data-decision]');
  if (!btn) return;

  for (const b of block.querySelectorAll('button')) b.disabled = true;
  await api.decide(block.dataset.id, btn.dataset.decision);

  block.dataset.decided = btn.dataset.decision;
});

The buttons are disabled before the request. The network takes seconds, and in that time "Allow" gets pressed twice.

Patterns

A request from a real run — with the command, the list of consequences and the mark of the irreversible.

Note the order: what will be done, what it will touch, and only then the decision. A request beginning with the buttons demands an answer before it states the question.

In context
The agent asks permission
rm -rf build/ && npm run build
  • It will delete the build/ directory entire — 1 284 files
  • Irreversible: the contents do not go to a recycle bin
  • The rebuild will take about 40 s
Markup
<div class="inst-approval" data-state="pending" role="group" aria-labelledby="ap2">
  <div class="inst-approval-head" id="ap2">The agent asks permission</div>
  <div class="inst-approval-what">rm -rf build/ &amp;&amp; npm run build</div>
  <ul class="inst-approval-effects">
    <li>It will delete the <code>build/</code> directory entire — 1 284 files</li>
    <li data-tone="warn">Irreversible: the contents do not go to a recycle bin</li>
    <li>The rebuild will take about 40 s</li>
  </ul>
  <div class="inst-approval-actions">
    <button class="inst-btn inst-btn--sm inst-btn--primary" type="button">Allow</button>
    <button class="inst-btn inst-btn--sm" type="button">Deny</button>
    <button class="inst-btn inst-btn--ghost inst-btn--sm" type="button">Always allow</button>
  </div>
</div>

API

Classes
NameWhat it does
class
inst-approvalThe block
inst-approval-headThe heading with an icon
inst-approval-whatWhat will be done
inst-approval-effectsThe list of consequences, on a <ul>
inst-approval-actionsThe decision
inst-approval-verdictWhat was decided and when. Shown after the answer
Attributes
NameValueWhat it does
attribute
data-statepending · approved · deniedon inst-approval
data-tonewarn · erroron an <li> of a consequence
Tokens
NameValueWhere from
token
--pad-cardvar(--space-6)base +14: density · scale
--radius-lg12pxbase +4: scale
--space-36pxone across every cell
--gap-inlinevar(--space-4)base +14: density · scale
--text-sm0.875rembase +4: scale
--text-xs0.78125rembase +4: scale
--warn-bglight-dark(var(--warn-1), color-mix(in oklab, var(--warn-4) 16%, transparent))one across every cell
--warn-textlight-dark(var(--warn-5), var(--warn-3))base +1: theme
--err-textlight-dark(var(--err-5), var(--err-3))base +1: theme
--text-mutedlight-dark(var(--n-8), var(--n-6))one across every cell

Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument