Requirement Sheets

Requirement sheets pause tool execution and show an inline form at the top of the conversation. Use them when your extension tool needs something from the user before it can run -- an API key, OAuth login, or configuration setting.

Related:


How It Works

  1. The AI model calls your extension tool
  2. Your tool handler detects a missing prerequisite (no API key, expired token, etc.)
  3. Your handler returns an error with a requirementSheet object containing HTML
  4. Chatons shows the sheet as an iframe at the top of the conversation
  5. The user fills in the form and clicks "Save" (or "Cancel")
  6. The sheet iframe uses postMessage to signal back to the parent
  7. Chatons closes the sheet and returns the tool result to the model

The tool error has already been returned to the model by the time the sheet appears. The model should instruct the user to retry after fixing the issue.

⚠️ Important: The sheet iframe is sandboxed and cannot directly call parent functions. Always use postMessage to communicate with the parent window (see section below).


Returning a Requirement Sheet

When your tool handler detects missing prerequisites, return an error with a requirementSheet property:

window.addEventListener('message', function (event) {
  var data = event && event.data;
  if (!data || data.type !== 'chaton.extension.apiCall') return;
  if (data.apiName !== 'my_tool') return;

  // Check for prerequisite
  var apiKey = getStoredApiKey();
  if (!apiKey) {
    window.parent.postMessage({
      type: 'chaton.extension.apiCallResponse',
      callId: data.callId,
      result: {
        ok: false,
        error: {
          code: 'unauthorized',
          message: 'API key not configured. Please set it in the sheet above.',
          requirementSheet: {
            title: 'Configure API Key',
            html: buildConfigSheetHtml(),
          },
        },
      },
    }, '*');
    return;
  }

  // Normal execution
  window.parent.postMessage({
    type: 'chaton.extension.apiCallResponse',
    callId: data.callId,
    result: { ok: true, data: { result: 'success' } },
  }, '*');
});

requirementSheet object

PropertyTypeRequiredDescription
titlestringNoTitle shown in the sheet header. Defaults to "Action Required".
htmlstringYesFull HTML document rendered in the sheet iframe.

Sheet HTML

The sheet is rendered as a sandboxed iframe (allow-scripts allow-same-origin allow-forms). This iframe runs in an isolated context, separate from the parent window.

⚠️ Critical: Use postMessage for Communication

Because the iframe is sandboxed, you cannot call functions on window.chaton.requirementSheet or other parent objects directly. The iframe has no direct access to parent APIs.

Always use postMessage to communicate:

// ✅ CORRECT - This works in a sandboxed iframe
window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');

// ❌ WRONG - This will fail silently
window.chaton.requirementSheet.confirm();  // undefined

Available communication methods

Using postMessage (recommended):

// Signal that the user completed all required actions
window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');

// Dismiss the sheet without completing
window.parent.postMessage({ type: 'chaton:requirement-sheet:dismiss' }, '*');

// Open Chatons Settings and dismiss the sheet
window.parent.postMessage({ type: 'chaton:requirement-sheet:open-settings' }, '*');

Note: The iframe is sandboxed and runs in a separate context, so direct function calls like window.chaton.requirementSheet.confirm() are not available. Always use postMessage to communicate with the parent window.


Complete Example

Manifest

{
  "id": "@user/chatons-weather",
  "name": "Weather Extension",
  "version": "1.0.0",
  "capabilities": ["llm.tools", "storage.kv"],
  "llm": {
    "tools": [
      {
        "name": "get_weather",
        "description": "Get current weather for a city.",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string" }
          },
          "required": ["city"]
        }
      }
    ]
  },
  "apis": {
    "exposes": [
      { "name": "get_weather", "version": "1.0.0" }
    ]
  }
}

Tool handler with requirement sheet

var EXTENSION_ID = '@user/chatons-weather';

window.addEventListener('message', function (event) {
  var data = event && event.data;
  if (!data || data.type !== 'chaton.extension.apiCall') return;
  if (data.apiName !== 'get_weather') return;

  // Check for stored API key
  window.chaton.extensionStorageKvGet(EXTENSION_ID, 'weather_api_key')
    .then(function (result) {
      var apiKey = result && result.ok && result.data;

      if (!apiKey) {
        // Return requirement sheet
        window.parent.postMessage({
          type: 'chaton.extension.apiCallResponse',
          callId: data.callId,
          result: {
            ok: false,
            error: {
              code: 'unauthorized',
              message: 'Weather API key required. A configuration sheet has been shown.',
              requirementSheet: {
                title: 'Set up Weather API Key',
                html: buildWeatherKeySheet(),
              },
            },
          },
        }, '*');
        return;
      }

      // Normal execution with the API key
      fetch('https://api.weather.example.com/v1/current?city=' + encodeURIComponent(data.payload.city), {
        headers: { 'Authorization': 'Bearer ' + apiKey }
      })
        .then(function (resp) { return resp.json(); })
        .then(function (weather) {
          window.parent.postMessage({
            type: 'chaton.extension.apiCallResponse',
            callId: data.callId,
            result: { ok: true, data: weather },
          }, '*');
        });
    });
});

function buildWeatherKeySheet() {
  return [
    '<!DOCTYPE html>',
    '<html><head><meta charset="utf-8">',
    '<style>',
    '  body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;',
    '         padding: 20px 24px; margin: 0;',
    '         background: var(--chaton-ui-background, #f5f6f8);',
    '         color: var(--chaton-ui-foreground, #1a1b22); font-size: 14px; }',
    '  .field { margin-bottom: 16px; }',
    '  label { display: block; font-weight: 500; font-size: 13px; margin-bottom: 6px; }',
    '  input { width: 100%; box-sizing: border-box; padding: 8px 12px;',
    '          border-radius: 8px; border: 1px solid var(--chaton-ui-border, #d6d9e2);',
    '          background: var(--chaton-ui-card, #fff); color: inherit; font: inherit; }',
    '  input:focus { outline: 2px solid var(--chaton-ui-ring, #6b7280); outline-offset: 2px; }',
    '  .actions { display: flex; gap: 10px; justify-content: flex-end; }',
    '  button { font: inherit; font-size: 13px; font-weight: 500; padding: 8px 18px;',
    '           border-radius: 8px; cursor: pointer;',
    '           border: 1px solid var(--chaton-ui-border, #d6d9e2);',
    '           background: var(--chaton-ui-card, #fff); color: inherit; }',
    '  .btn-primary { background: var(--chaton-ui-primary, hsl(220 7% 32%));',
    '                 color: var(--chaton-ui-primary-foreground, #fff);',
    '                 border-color: var(--chaton-ui-primary, hsl(220 7% 32%)); }',
    '  @media (prefers-color-scheme: dark) {',
    '    body { background: hsl(222 14% 12%); color: hsl(210 20% 90%); }',
    '    input { background: hsl(222 13% 17%); border-color: hsl(222 10% 24%); }',
    '    button { background: hsl(222 13% 17%); border-color: hsl(222 10% 24%); }',
    '  }',
    '</style>',
    '</head><body>',
    '  <div class="field">',
    '    <label for="api-key">Weather API Key</label>',
    '    <input id="api-key" type="password" placeholder="Enter your API key" />',
    '  </div>',
    '  <p style="font-size:12px;color:var(--chaton-ui-muted-foreground,#6b7280);margin-bottom:16px">',
    '    Your key is stored locally and never shared.',
    '  </p>',
    '  <div class="actions">',
    '    <button onclick="dismissSheet()">Cancel</button>',
    '    <button class="btn-primary" onclick="saveKey()">Save</button>',
    '  </div>',
    '  <script>',
    '    function saveKey() {',
    '      var key = document.getElementById("api-key").value.trim();',
    '      if (!key) return;',
    '      window.parent.postMessage({ type: "chaton:requirement-sheet:confirm" }, "*");',
    '    }',
    '    function dismissSheet() {',
    '      window.parent.postMessage({ type: "chaton:requirement-sheet:dismiss" }, "*");',
    '    }',
    '  </script>',
    '</body></html>',
  ].join('\n');
}

Built-in Auth Error Sheets

Chatons automatically shows a requirement sheet when an LLM provider returns an authentication error. This handles:

  • 401 Unauthorized responses
  • Expired OAuth tokens
  • Invalid or missing API keys
  • 403 Forbidden responses

The built-in sheet shows the error message and offers:

  • Dismiss -- Close the sheet
  • Open Settings -- Navigate to Settings > Providers & Models

No extension code is needed. This is built into the conversation runtime.


Styling

Use CSS variables from the Chatons design system for a native look:

VariablePurpose
--chaton-ui-backgroundPage background
--chaton-ui-foregroundText color
--chaton-ui-cardCard/input background
--chaton-ui-borderBorder color
--chaton-ui-primaryPrimary button background
--chaton-ui-primary-foregroundPrimary button text
--chaton-ui-muted-foregroundMuted text color
--chaton-ui-ringFocus ring color

Dark mode: Use @media (prefers-color-scheme: dark) or rely on the CSS variables (they automatically adjust in dark mode).

Max height: Sheets have a max height of approximately 360px. Keep content compact.


User Interaction

ActionResult
Click close icon (X)Sheet dismissed
Click outside the sheetSheet dismissed
Press EscapeSheet dismissed
confirm()Sheet confirmed and closed
dismiss()Sheet dismissed
openSettings()Opens Settings and closes sheet

Common Mistakes

Mistake 1: Trying to call parent functions directly

// ❌ WRONG - Will not work (iframe is sandboxed)
button.onclick = () => {
  window.chaton.requirementSheet.confirm();
};

// ✅ CORRECT - Use postMessage instead
button.onclick = () => {
  window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');
};

Mistake 2: Forgetting to check iframe context

The sheet runs in a separate iframe, so window.chaton is undefined. Always assume you're in a sandboxed context and use postMessage.

Mistake 3: Using event handlers without proper closure

// ❌ Problem: event.data might be stale
window.addEventListener('input', function(event) {
  button.onclick = () => {
    var value = input.value;  // References outer scope
    window.parent.postMessage({ type: 'confirm', value }, '*');
  };
});

// ✅ Better: Capture value directly
button.onclick = function() {
  var value = input.value;
  window.parent.postMessage({ type: 'chaton:requirement-sheet:confirm' }, '*');
};

Constraints

  • Only one sheet can be active per conversation at a time
  • The tool error has already been returned to the model before the sheet appears
  • The sheet iframe is sandboxed (allow-scripts allow-same-origin allow-forms)
  • The sandboxed iframe cannot access parent APIs directly — use postMessage instead
  • The sheet does not have direct access to the parent extension's window.chaton APIs (it runs in a separate iframe context)

On this page