Approval
A tool call held until a person decides — the call and its arguments asked at the composer, approve, reject, or a word on what to do instead.
<!-- In your app, AparteClient asks this for any tool registered with needsApproval: true.
Here the request is made by hand: import { requestUserInput } from '@aparte/core'. -->
<aparte-chat style="height: 26rem; --aparte-elic-max-height: 20rem"></aparte-chat>
<script>
const { requestUserInput } = window.aparte;
const chat = document.querySelector('aparte-chat');
chat.viewport.appendMessage({ id: 'u1', role: 'user', content: 'Clean up the old client.', timestamp: Date.now() });
chat.viewport.appendMessage({
id: 'a1', role: 'assistant', timestamp: Date.now(),
segments: [{
id: 's1', type: 'tool_call', status: 'awaiting-approval',
toolCall: { id: 't1', name: 'delete_file', input: { path: 'src/legacy/old-client.ts' } },
}],
});
// The decision is asked at the composer, with the arguments it is about.
requestUserInput({
kind: 'approval',
message: 'Run delete_file?',
details: JSON.stringify({ path: 'src/legacy/old-client.ts' }, null, 2),
options: [
{ value: 'approve', label: 'Approve' },
{ value: 'reject', label: 'Reject', tone: 'deny' },
],
}).catch(() => {});
</script>Some tools should not run without a yes: deleting a file, sending an email, running a command.
Mark such a tool needsApproval and the turn stops before its handler runs. The tool’s row
in the transcript says it is waiting, and the question is asked where the person is already
looking, at the composer: which tool, with which arguments, approve or reject.
The person can also type an answer instead of clicking: “no, archive it instead”. That refuses the call, and the words reach the model as the tool’s result, so the next step can follow them.
Variants
Section titled “Variants”A choice that can be remembered
Section titled “A choice that can be remembered”<!-- The panel takes at most --aparte-elic-max-height (half the window by default). -->
<aparte-chat style="height: 26rem; --aparte-elic-max-height: 22rem"></aparte-chat>
<script>
const { requestUserInput } = window.aparte;
// Offer "always" only when your app has somewhere to remember it, and say what it reaches.
requestUserInput({
kind: 'approval',
message: 'Run run_command?',
details: 'git status',
options: [
{ value: 'approve', label: 'Allow once' },
{ value: 'approve-always', label: 'Always allow', description: 'git status, in this workspace' },
{ value: 'reject', label: 'Reject', tone: 'deny' },
],
}).catch(() => {});
</script>The options come with the request, so an app that can remember a grant offers a third one, and says what it reaches. Offer “always” only where your app keeps it.
With AparteClient, marking the tool is all it takes:
import { aparteGlobalConfig } from '@aparte/core';
aparteGlobalConfig.registerTool(
{
name: 'delete_file',
description: 'Delete a file from the workspace.',
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
needsApproval: true,
},
async (call) => ({ toolCallId: call.id, content: `Deleted ${String(call.input['path'])}.` }),
);To ask on your own terms, call requestUserInput() with kind: 'approval'. It resolves with
the chosen option’s value, or with the typed instruction:
import { requestUserInput } from '@aparte/core';
import type { AparteApprovalAnswer } from '@aparte/core';
const result = await requestUserInput({
kind: 'approval',
message: 'Run run_command?',
details: 'git status',
options: [
{ value: 'once', label: 'Allow once' },
{ value: 'always', label: 'Always allow', description: 'git status, in this workspace' },
{ value: 'reject', label: 'Reject', tone: 'deny' },
],
});
if (result.action === 'accept') {
const { option, instruction } = result.content as AparteApprovalAnswer;
}An AparteApprovalOption has a value, a label, an optional description (what choosing it
commits to) and a tone, affirm or deny. A label can be a function, re-read when the
language changes while the question is open.
Customize
Section titled “Customize”- Which calls ask is a policy: the approval mode switch decides by mode (plan, ask, auto-edit, auto) from your tool names.
AparteClient’sapprovalResolveroption takes the whole decision instead, for a CLI, a webhook or a rule written in code.- The panel is capped at
--aparte-elic-max-height, half the window by default.
Accessibility
Section titled “Accessibility”- The arguments are shown as text, in a focusable group named “Arguments”, never as markup: they are written by the model.
- The options are buttons in a group named from the
approvalOptionsLabelkey (“Your decision”); a decision takes one click or one key press. - The instruction field is named by its placeholder (“Or tell it what to do instead…”).
- The panel takes the focus and gives it back when it closes (to the composer when there is nothing to go back to). From the chat’s own editor it focuses the panel itself, named by the question, so the next key does not pick an option. A reader writing in a field elsewhere on the page, or in another chat, keeps the focus, and a polite status says a question waits. Taken from nowhere, it does not scroll the page.
Related
Section titled “Related”- Approval mode — when to ask, by mode.
- Tool call — the row that waits in the transcript.
- Question — the same panel collecting an answer instead of a decision.