> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mindset.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Examples

> Four complete starter integrations. Pick the tab that matches your stack: the built-in chat UI, or headless mode with vanilla HTML, React, or Angular.

Four complete starter integrations. The Built-in chat UI tab is the fastest path to a working agent. The three headless tabs show how to render your own UI on top of the `<mindset-agent>` element.

<Tabs>
  <Tab title="Built-in chat UI">
    This is the minimum integration. The element renders Mindset AI's built-in chat UI inside a shadow DOM. Your users see a chat panel; you write no UI code.

    ### Complete page

    ```html theme={null}
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Mindset AI agent</title>
      <style>
        body { margin: 0; font-family: system-ui, sans-serif; }
        main { max-width: 800px; margin: 0 auto; padding: 2rem; }
        mindset-agent { display: block; height: 600px; border: 1px solid #ddd; border-radius: 8px; }
      </style>
    </head>
    <body>
      <main>
        <h1>Support agent</h1>
        <p>Ask anything about your account, orders, or our products.</p>

        <mindset-agent
          agent-uid="agt-your-agent-uid"
        ></mindset-agent>
      </main>

      <script src="MINDSET-SERVER-URL/mindset-sdk3.umd.js"></script>
      <script>
        window.mindset.init({
          appUid: 'your-app-uid',
          fetchAuthentication: async () => {
            // Your application's API - validates current user and then calls Mindset AI's API to create
            const r = await fetch('/api/mindset-token', { credentials: 'include' });
            const { authToken } = await r.json();
            return authToken;
          },
        });
      </script>
    </body>
    </html>
    ```

    Replace the agent and app UIDs with yours, set up the token endpoint, and you have a working chat panel.

    ### What you get

    * Chat input and message history
    * Thread switcher (the user can start, switch, rename, and delete threads)
    * Streaming responses with typing indicator
    * Tool widgets rendered automatically (cards, carousels, custom UIs)
    * Quick reply chips and follow-up question buttons
    * Citation display when the agent uses retrieval
    * Theme that matches the page's color scheme (configurable)

    ### Sizing the element

    `<mindset-agent>` is `display: block` by default. Size it like any other block element:

    ```css theme={null}
    mindset-agent {
      display: block;
      height: 600px;
      width: 100%;
      border: 1px solid #ddd;
      border-radius: 8px;
    }
    ```

    Common patterns:

    * For full-page chat, use `height: 100vh; width: 100%`.
    * For a sidebar, use `height: 100%; width: 360px`.
    * For an embedded panel, use a fixed height like the example above.

    The chat UI is responsive within the element. It works at sizes from \~320px wide up to full screen.

    ### Reacting to events

    Even with the built-in UI, you can hook into events to integrate the agent with the rest of your page.

    ```js theme={null}
    const agent = document.querySelector('mindset-agent');

    // Sync the active thread to the URL
    agent.addEventListener('mindset:thread-changed', (e) => {
      if (e.detail.threadUid) {
        history.replaceState({}, '', `?thread=${e.detail.threadUid}`);
      }
    });

    // Track turn completions in your analytics
    agent.addEventListener('mindset:complete', (e) => {
      analytics.track('agent_turn_complete', {
        threadId: e.detail.threadId,
        responseLength: e.detail.response.length,
      });
    });

    // Show your own loading state outside the chat panel
    agent.addEventListener('mindset:agent-busy', () => updateStatusIndicator('thinking'));
    agent.addEventListener('mindset:agent-idle', () => updateStatusIndicator('ready'));
    ```

    ### Driving the agent from outside

    You can call methods on the element from anywhere on your page. Useful for:

    Quick-action buttons that send a pre-written message:

    ```html theme={null}
    <button id="ask-status">Where's my order?</button>

    <script>
      document.getElementById('ask-status').addEventListener('click', () => {
        document.querySelector('mindset-agent').sendMessage("Where's my order?");
      });
    </script>
    ```

    Deep-linking to a specific thread:

    ```js theme={null}
    const params = new URLSearchParams(location.search);
    const threadUid = params.get('thread');

    document.querySelector('mindset-agent').addEventListener('mindset:agent-idle', async function once() {
      if (threadUid) {
        await this.switchThread(threadUid);
      }
      this.removeEventListener('mindset:agent-idle', once);
    }, { once: false });
    ```

    Programmatic priming. Silently prime the agent with context when the user does something on the page:

    ```js theme={null}
    document.querySelector('.cart-button').addEventListener('click', () => {
      const agent = document.querySelector('mindset-agent');
      agent.sendMessage('the user just opened the cart panel', { silent: true });
    });
    ```

    The agent receives the message and can respond, but no user-side bubble appears in the chat.

    ### Adding page tools

    Page tools let the agent call functions on your page during a turn. Configure them with `agent.setPageTools()`:

    ```js theme={null}
    const agent = document.querySelector('mindset-agent');

    agent.setPageTools([
      {
        name: 'lookup_order',
        description: 'Look up the current user\'s order by ID',
        parameters: {
          type: 'object',
          properties: {
            orderId: { type: 'string', description: 'The order ID' },
          },
          required: ['orderId'],
        },
        handler: async ({ orderId }) => {
          const r = await fetch(`/api/orders/${orderId}`);
          return await r.json();
        },
      },
    ]);
    ```

    Call `setPageTools` once the element has fired `mindset:agent-idle`. See [When the element is ready to call](./methods.mdx#when-the-element-is-ready-to-call).

    ### Configuration via attributes

    For static setup that's known at render time, you can pass values as attributes on the element instead of calling JS methods. See the [`<mindset-agent>` reference](./mindset-agent-element.mdx#configuration-alternative-to-js-methods) for the full list.

    ```html theme={null}
    <mindset-agent
      agent-uid="agt-..."
      initial-question="What can I help you with today?"
      situational-awareness='{"currentPage":"/products","userPlan":"enterprise"}'
    ></mindset-agent>
    ```

    Attributes are read once at render time. Use the JS method if you need to update values dynamically.

    ### Switching to headless later

    If you outgrow the built-in UI and want to render your own, add the `headless` attribute and start listening for events:

    ```html theme={null}
    <mindset-agent agent-uid="agt-..." headless></mindset-agent>
    ```

    The runtime, state machine, auth flow, and DOM events stay the same. The only thing that changes is whether the element renders its own UI. Switch to the **Headless: vanilla HTML**, **Headless: React**, or **Headless: Angular** tab above for what that looks like end-to-end.
  </Tab>

  <Tab title="Headless: vanilla HTML">
    This example builds a complete chat interface in plain HTML and JavaScript. The `<mindset-agent>` element runs the agent and dispatches DOM events; you handle rendering. No framework, no build step.

    ### What this example covers

    * Streaming text rendering as it arrives
    * Sending messages and managing busy state
    * Rendering tool widgets when the agent calls tools
    * Switching threads and showing thread history
    * Quick reply chips and follow-up question buttons

    ### Complete page

    ```html theme={null}
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Custom agent UI</title>
      <style>
        body { margin: 0; font-family: system-ui, sans-serif; display: grid; grid-template-columns: 240px 1fr; height: 100vh; }
        aside { background: #f5f5f5; padding: 1rem; overflow-y: auto; }
        aside h2 { font-size: 0.9rem; text-transform: uppercase; color: #666; }
        aside ul { list-style: none; padding: 0; }
        aside button { display: block; width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; cursor: pointer; border-radius: 4px; }
        aside button:hover { background: #e0e0e0; }
        aside button.active { background: #d0d0ff; }
        main { display: flex; flex-direction: column; padding: 1rem; }
        #messages { flex: 1; overflow-y: auto; }
        .message { margin-bottom: 1rem; padding: 0.75rem; border-radius: 8px; }
        .message.user { background: #e0e8ff; margin-left: 2rem; }
        .message.agent { background: #f0f0f0; margin-right: 2rem; }
        .quick-replies { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.5rem 0; }
        .quick-replies button { padding: 0.4rem 0.8rem; border: 1px solid #ccc; background: white; border-radius: 16px; cursor: pointer; }
        form { display: flex; gap: 0.5rem; padding-top: 1rem; border-top: 1px solid #ddd; }
        form textarea { flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; resize: vertical; min-height: 2.5rem; }
        form button { padding: 0.5rem 1rem; }
        form button:disabled { opacity: 0.5; cursor: not-allowed; }
      </style>
    </head>
    <body>
      <aside>
        <button id="new-thread">+ New thread</button>
        <h2>History</h2>
        <ul id="thread-list"></ul>
      </aside>

      <main>
        <div id="messages"></div>
        <form id="form">
          <textarea id="input" placeholder="Ask anything…" required></textarea>
          <button type="submit" id="send">Send</button>
        </form>
      </main>

      <!-- Headless: no UI rendered by the element itself -->
      <mindset-agent
        id="agent"
        agent-uid="agt-your-agent-uid"
        headless
      ></mindset-agent>

      <script src="MINDSET-SERVER-URL/mindset-sdk3.umd.js"></script>
      <script>
        const agent = document.getElementById('agent');
        const messagesEl = document.getElementById('messages');
        const inputEl = document.getElementById('input');
        const sendBtn = document.getElementById('send');
        const formEl = document.getElementById('form');
        const threadListEl = document.getElementById('thread-list');
        const newThreadBtn = document.getElementById('new-thread');

        // Render state
        let currentReplyEl = null;
        let currentReplyText = '';

        // ---- Initialize the SDK ----
        window.mindset.init({
          appUid: 'your-app-uid',
          fetchAuthentication: async () => {
            const r = await fetch('/api/mindset-token', { credentials: 'include' });
            const { authToken } = await r.json();
            return authToken;
          },
        });

        // ---- Listen for lifecycle events ----
        agent.addEventListener('mindset:agent-idle', () => {
          sendBtn.disabled = false;
          refreshThreadList();
        });

        agent.addEventListener('mindset:agent-busy', () => {
          sendBtn.disabled = true;
          // Start a new agent message bubble
          currentReplyText = '';
          currentReplyEl = appendMessage('agent', '');
        });

        agent.addEventListener('mindset:agent-error', (e) => {
          appendMessage('agent', `[error: ${e.detail.message}]`);
          sendBtn.disabled = false;
        });

        // ---- Listen for streaming output ----
        agent.addEventListener('mindset:text-delta', (e) => {
          currentReplyText += e.detail.content;
          if (currentReplyEl) currentReplyEl.textContent = currentReplyText;
        });

        // ---- Tool widgets ----
        agent.addEventListener('mindset:tool-end', (e) => {
          if (e.detail.widget && currentReplyEl) {
            const widgetEl = renderWidget(e.detail.widget);
            currentReplyEl.appendChild(widgetEl);
          }
        });

        // ---- Quick replies ----
        agent.addEventListener('mindset:quick-replies', (e) => {
          if (!currentReplyEl) return;
          const wrapper = document.createElement('div');
          wrapper.className = 'quick-replies';
          // Optional: render the framing question above the chips
          if (e.detail.question) {
            const q = document.createElement('div');
            q.textContent = e.detail.question;
            q.style.fontSize = '0.85rem';
            q.style.color = '#666';
            currentReplyEl.appendChild(q);
          }
          e.detail.options.forEach((option) => {
            const btn = document.createElement('button');
            btn.textContent = option;
            btn.addEventListener('click', () => {
              inputEl.value = option;
              formEl.requestSubmit();
            });
            wrapper.appendChild(btn);
          });
          currentReplyEl.appendChild(wrapper);
        });

        // ---- Thread management ----
        // mindset:thread-changed fires when a thread persists server-side
        // (after the first user message of a new thread, or when switching to
        // an existing one). It does NOT fire immediately on agent.newThread().
        // A fresh thread has no uid until the user engages with it.
        agent.addEventListener('mindset:thread-changed', () => {
          refreshThreadList();
        });

        newThreadBtn.addEventListener('click', async () => {
          await agent.newThread();
          // newThread() resets the agent's local state but doesn't persist a thread
          // server-side. That happens after the user's first message. Clear the
          // message list now so the UI matches; the sidebar will pick up the new
          // thread once it persists (via mindset:thread-changed).
          messagesEl.innerHTML = '';
          refreshThreadList();  // refresh anyway in case a previous thread should now appear
        });

        async function refreshThreadList() {
          const { threads } = await agent.listThreads({ pageSize: 20 });
          threadListEl.innerHTML = '';
          threads.forEach((t) => {
            const li = document.createElement('li');
            const btn = document.createElement('button');
            btn.textContent = t.title || '(untitled)';
            if (t.uid === agent.threadUid) btn.classList.add('active');
            btn.addEventListener('click', () => agent.switchThread(t.uid));
            li.appendChild(btn);
            threadListEl.appendChild(li);
          });
        }

        // ---- Send a message ----
        // sendWhenIdle handles the case where the agent is still busy with its
        // icebreaker turn (an automatic first turn some agents are configured to
        // fire on startup). If the agent is idle right now, it sends straight
        // away; if it's busy, it waits for the next mindset:agent-idle and
        // recurses. Safe to call at any time.
        function sendWhenIdle(text) {
          if (!agent.isAgentBusy()) {
            agent.sendMessage(text);
          } else {
            agent.addEventListener('mindset:agent-idle', () => sendWhenIdle(text), { once: true });
          }
        }

        formEl.addEventListener('submit', (e) => {
          e.preventDefault();
          const text = inputEl.value.trim();
          if (!text) return;

          appendMessage('user', text);
          sendWhenIdle(text);
          inputEl.value = '';
        });

        // ---- Helpers ----
        function appendMessage(role, text) {
          const el = document.createElement('div');
          el.className = `message ${role}`;
          el.textContent = text;
          messagesEl.appendChild(el);
          el.scrollIntoView({ block: 'end' });
          return el;
        }

        function renderWidget(widget) {
          // Replace this with your own widget rendering (cards, carousels, custom components).
          const el = document.createElement('pre');
          el.style.background = '#fafafa';
          el.style.padding = '0.5rem';
          el.style.borderRadius = '4px';
          el.style.fontSize = '0.85rem';
          el.textContent = JSON.stringify(widget, null, 2);
          return el;
        }
      </script>
    </body>
    </html>
    ```

    ### How it works

    The element runs the agent. You write the rest. Three event handlers do most of the work:

    1. `mindset:agent-busy` starts a new agent message bubble in the UI
    2. `mindset:text-delta` appends streaming text to the current bubble
    3. `mindset:agent-idle` re-enables the send button. The turn is done.

    Everything else (tool widgets, quick replies, thread changes) is layered on top with additional event listeners.

    The `sendWhenIdle` helper covers a subtle case: if your agent is configured with an icebreaker (a turn that fires automatically as the agent settles after init), the agent goes idle once after init completes, then immediately busy again while the icebreaker streams, then idle a second time when the icebreaker finishes. A user clicking "Send" during that first window would otherwise hit a busy throw. `sendWhenIdle` re-checks busy state on each `mindset:agent-idle`, so it queues automatically until the agent is actually free.

    ### Common adjustments

    **Show a typing indicator.** Replace the empty agent message with a "..." while waiting for the first text delta:

    ```js theme={null}
    agent.addEventListener('mindset:agent-busy', () => {
      currentReplyText = '';
      currentReplyEl = appendMessage('agent', '…'); // visible while streaming starts
    });

    agent.addEventListener('mindset:text-delta', (e) => {
      currentReplyText += e.detail.content;
      if (currentReplyEl) currentReplyEl.textContent = currentReplyText; // overwrite the …
    });
    ```

    **Render markdown in agent replies.** Pipe `currentReplyText` through your favorite markdown library before setting it on the element. Re-render on every delta:

    ```js theme={null}
    import { marked } from 'https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js';

    agent.addEventListener('mindset:text-delta', (e) => {
      currentReplyText += e.detail.content;
      if (currentReplyEl) currentReplyEl.innerHTML = marked.parse(currentReplyText);
    });
    ```

    **Handle interrupts.** Some turns pause for input. The agent calls a UI-interrupt tool (e.g. `present_choices`) and waits. Listen for `mindset:interrupt`, render UI based on `e.detail.args`, then drive the next turn with `agent.sendMessage()` once the user selects:

    ```js theme={null}
    agent.addEventListener('mindset:interrupt', (e) => {
      // e.detail.interruptType identifies the tool that paused the turn.
      // e.detail.args carries the data your UI needs to render.
      const userSelection = await promptUserForChoice(e.detail.args);
      agent.sendMessage(userSelection);
    });
    ```

    <Note>
      A dedicated programmatic-resume API on the element is on the roadmap. For now, treat interrupts as turn-ending and drive the next turn with `sendMessage()`.
    </Note>

    **Persist the active thread in the URL.** Sync `agent.threadUid` to the URL so reloads land on the same thread. Because uids only surface after a thread persists server-side, the URL won't get a value until the user's first message. That's the correct behavior, since a local-only thread can't be reloaded anyway.

    ```js theme={null}
    agent.addEventListener('mindset:thread-changed', (e) => {
      if (e.detail.threadUid) {
        history.replaceState({}, '', `?thread=${e.detail.threadUid}`);
      } else {
        // threadUid went null (between threads or after newThread() reset).
        history.replaceState({}, '', location.pathname);
      }
    });

    // On page load, after the agent reaches idle:
    agent.addEventListener('mindset:agent-idle', async function once() {
      const threadUid = new URLSearchParams(location.search).get('thread');
      if (threadUid) {
        try {
          await agent.switchThread(threadUid);
        } catch (err) {
          // The uid in the URL no longer exists (deleted, expired, or stale).
          // Clear the URL and continue with a fresh thread.
          console.warn('Stored thread no longer exists, starting fresh:', err.message);
          history.replaceState({}, '', location.pathname);
        }
      }
      agent.removeEventListener('mindset:agent-idle', once);
    }, { once: false });
    ```
  </Tab>

  <Tab title="Headless: React">
    `<mindset-agent>` is a custom element, which means it works directly as a JSX tag with no wrapper component. This example shows the patterns React developers want: refs, `useEffect` for subscription, cleanup on unmount, and TypeScript types for the events.

    ### Initialization

    Call `mindset.init()` once at app startup. Don't put it inside a component's render. Call it once in your entry file or a top-level effect:

    ```ts theme={null}
    // src/lib/mindset-init.ts
    declare global {
      interface Window {
        mindset: {
          init(config: {
            appUid: string;
            fetchAuthentication: () => Promise<string>;
          }): Promise<void>;
        };
      }
    }

    let initialized = false;

    export function initializeMindset() {
      if (initialized) return;
      initialized = true;

      window.mindset.init({
        appUid: import.meta.env.VITE_MINDSET_APP_UID,
        fetchAuthentication: async () => {
          const r = await fetch('/api/mindset-token', { credentials: 'include' });
          const { authToken } = await r.json();
          return authToken;
        },
      });
    }
    ```

    Call `initializeMindset()` in your app's entry point (e.g. `main.tsx`):

    ```tsx theme={null}
    import { initializeMindset } from './lib/mindset-init';

    initializeMindset();

    createRoot(document.getElementById('root')!).render(<App />);
    ```

    Make sure the SDK script is loaded in your `index.html` before the app boots:

    ```html theme={null}
    <script src="MINDSET-SERVER-URL/mindset-sdk3.umd.js"></script>
    ```

    ### TypeScript types for the element and events

    There are no shipped TypeScript declarations yet (planned). Declare the types you need locally:

    ```ts theme={null}
    // src/types/mindset.d.ts
    declare namespace JSX {
      interface IntrinsicElements {
        'mindset-agent': React.DetailedHTMLProps<
          React.HTMLAttributes<HTMLElement> & {
            'agent-uid': string;
            headless?: boolean | '';
            'initial-question'?: string;
            'thread-uid'?: string;
            'situational-awareness'?: string;
            'passthrough-params'?: string;
            'theme'?: string;
            'show-thread-list'?: 'true' | 'false';
          },
          HTMLElement
        >;
      }
    }

    interface MindsetAgentElement extends HTMLElement {
      setPageTools(tools: PageToolDefinition[]): void;
      setSituationalAwareness(sa: Record<string, string>): void;
      setPassthroughParams(params: Record<string, string>): void;
      sendMessage(text: string, options?: { silent?: boolean }): void;
      isAgentBusy(): boolean;
      newThread(): Promise<void>;
      switchThread(threadUid: string): Promise<void>;
      deleteThread(threadUid: string): Promise<boolean>;
      renameThread(threadUid: string, title: string): Promise<boolean>;
      listThreads(options?: { pageSize?: number; pageToken?: string }): Promise<{
        threads: Array<{
          uid: string;
          title: string;
          createdAt: string;
          updatedAt: string;
          messageCount: number;
        }>;
        nextPageToken: string | null;
        hasMore: boolean;
      }>;
      readonly threadUid: string | null;
    }

    // Event detail shapes. See events reference for the full list.
    interface MindsetEventMap {
      'mindset:agent-registered': CustomEvent<{ headless: boolean }>;
      'mindset:agent-initializing': CustomEvent<{}>;
      'mindset:agent-idle': CustomEvent<{}>;
      'mindset:agent-busy': CustomEvent<{}>;
      'mindset:agent-error': CustomEvent<{ code: string; message: string }>;
      'mindset:thread-changed': CustomEvent<{ threadUid: string | null; previous: string | null }>;
      'mindset:text-delta': CustomEvent<{ content: string }>;
      'mindset:tool-start': CustomEvent<{
        toolName: string;
        toolCallId: string;
        runId: string;
        silent: boolean;
        args?: Record<string, unknown>;
      }>;
      'mindset:tool-end': CustomEvent<{
        toolName: string;
        toolCallId: string;
        runId: string;
        durationMs: number;
        silent: boolean;
        widget?: unknown;
        canvas?: unknown;
        llmToolCallId?: string;
        output?: string;
      }>;
      'mindset:complete': CustomEvent<{ response: string; threadId: string }>;
      // …add more as you use them
    }

    interface PageToolDefinition {
      name: string;
      description: string;
      parameters: Record<string, unknown>;
      handler: (args: any) => Promise<unknown>;
    }
    ```

    ### A reusable hook

    A small custom hook handles the common pattern: get a typed ref, subscribe to events, clean up on unmount.

    ```tsx theme={null}
    // src/hooks/useMindsetAgent.ts
    import { useEffect, useRef } from 'react';

    export function useMindsetAgent() {
      const ref = useRef<MindsetAgentElement>(null);

      function on<K extends keyof MindsetEventMap>(
        type: K,
        handler: (e: MindsetEventMap[K]) => void,
      ) {
        useEffect(() => {
          const el = ref.current;
          if (!el) return;
          el.addEventListener(type as string, handler as EventListener);
          return () => el.removeEventListener(type as string, handler as EventListener);
        }, [handler]);
      }

      return { ref, on };
    }
    ```

    ### A chat component

    Putting it together:

    ```tsx theme={null}
    // src/components/AgentChat.tsx
    import { useState, useCallback } from 'react';
    import { useMindsetAgent } from '../hooks/useMindsetAgent';

    interface Message {
      role: 'user' | 'agent';
      text: string;
    }

    interface Props {
      agentUid: string;
    }

    export function AgentChat({ agentUid }: Props) {
      const { ref, on } = useMindsetAgent();
      const [messages, setMessages] = useState<Message[]>([]);
      const [busy, setBusy] = useState(false);
      const [input, setInput] = useState('');

      const onBusy = useCallback(() => {
        setBusy(true);
        setMessages((prev) => [...prev, { role: 'agent', text: '' }]);
      }, []);

      const onIdle = useCallback(() => {
        setBusy(false);
      }, []);

      const onDelta = useCallback((e: MindsetEventMap['mindset:text-delta']) => {
        setMessages((prev) => {
          const next = [...prev];
          const last = next[next.length - 1];
          if (last && last.role === 'agent') {
            next[next.length - 1] = { ...last, text: last.text + e.detail.content };
          }
          return next;
        });
      }, []);

      const onComplete = useCallback((e: MindsetEventMap['mindset:complete']) => {
        console.log('Turn done in thread', e.detail.threadId);
      }, []);

      on('mindset:agent-busy', onBusy);
      on('mindset:agent-idle', onIdle);
      on('mindset:text-delta', onDelta);
      on('mindset:complete', onComplete);

      // sendWhenIdle handles the case where the agent is still busy with its
      // icebreaker turn (an automatic first turn some agents are configured to
      // fire on startup). If the agent is idle, it sends straight away; if it's
      // busy, it waits for the next mindset:agent-idle and recurses. Safe to call
      // at any time.
      function sendWhenIdle(agent: MindsetAgentElement, text: string) {
        if (!agent.isAgentBusy()) {
          agent.sendMessage(text);
        } else {
          agent.addEventListener(
            'mindset:agent-idle',
            () => sendWhenIdle(agent, text),
            { once: true },
          );
        }
      }

      function send(e: React.FormEvent) {
        e.preventDefault();
        const text = input.trim();
        if (!text || !ref.current) return;

        setMessages((prev) => [...prev, { role: 'user', text }]);
        sendWhenIdle(ref.current, text);
        setInput('');
      }

      return (
        <>
          <mindset-agent
            ref={ref}
            agent-uid={agentUid}
            headless
          />

          <div className="messages">
            {messages.map((m, i) => (
              <div key={i} className={`message message-${m.role}`}>
                {m.text}
              </div>
            ))}
          </div>

          <form onSubmit={send}>
            <textarea
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder="Ask anything…"
            />
            <button type="submit" disabled={busy}>
              Send
            </button>
          </form>
        </>
      );
    }
    ```

    ### How it works

    A few things make this React pattern straightforward:

    * `<mindset-agent>` is a real custom element, so React recognizes it as a JSX tag and forwards the ref to the underlying DOM node. No wrapper component needed.
    * Events come from the element, not from React. Subscribe in `useEffect`, return a cleanup function that calls `removeEventListener`. The custom hook does this in one line.
    * State setters and `sendMessage` are sync, so call them directly from event handlers and form submit handlers. No `await` needed.
    * Thread CRUD is async. `newThread`, `switchThread`, and the rest return Promises. Handle them like any other API call.
    * `sendWhenIdle` covers the icebreaker race. If your agent is configured with an icebreaker (a turn that fires automatically as the agent settles after init), the lifecycle is `agent-idle` (init done), then `agent-busy` (icebreaker streaming), then `agent-idle` (icebreaker done). A user clicking Send during that first window would otherwise hit a busy throw. The recursive `sendWhenIdle` re-checks busy state on each idle event, so it queues automatically until the agent is actually free.

    ### Configuring page tools

    Set page tools when the component mounts. Use `useEffect` to wait for the element ref:

    ```tsx theme={null}
    useEffect(() => {
      const el = ref.current;
      if (!el) return;

      el.setPageTools([
        {
          name: 'lookup_order',
          description: 'Look up the current user\'s order by ID',
          parameters: {
            type: 'object',
            properties: { orderId: { type: 'string' } },
            required: ['orderId'],
          },
          handler: async ({ orderId }: { orderId: string }) => {
            const r = await fetch(`/api/orders/${orderId}`);
            return await r.json();
          },
        },
      ]);
    }, []);
    ```

    If this effect runs before `mindset.init()` has completed (or before the SDK script has loaded), `setPageTools` won't exist on the element yet. Gate the call on a `mindset:agent-idle` listener (or wait for it inside the effect). See [When the element is ready to call](./methods.mdx#when-the-element-is-ready-to-call) for the pattern.

    ### Updating situational awareness when state changes

    Pass page state to the agent so it can personalize responses without the user having to repeat themselves:

    ```tsx theme={null}
    const { user, currentPage, cart } = useAppState();

    useEffect(() => {
      ref.current?.setSituationalAwareness({
        currentPage,
        userPlan: user.plan,
        cartItems: String(cart.items.length),
      });
    }, [currentPage, user.plan, cart.items.length]);
    ```

    The agent reads these values on the next turn.
  </Tab>

  <Tab title="Headless: Angular">
    `<mindset-agent>` is a custom element, so it drops straight into an Angular template. Angular has three quirks that aren't obvious if you're coming from the **Headless: React** tab, and getting them wrong produces confusing failures:

    1. Angular rejects unknown elements unless you tell it `<mindset-agent>` is a custom element (`CUSTOM_ELEMENTS_SCHEMA`).
    2. The `mindset:*` event names contain a colon, which Angular's template event binding parses as a target/event pair, so `(mindset:text-delta)="…"` does not work. You subscribe with `addEventListener` instead.
    3. Events fire from the element, not from Angular, so change detection may not run unless you re-enter the Angular zone.

    This example targets Angular 17+ with standalone components and signals. The same approach works with NgModules and older versions, and the notes call out where.

    ### Allow the custom element

    Angular's template compiler errors on unknown tags (`NG8001`). Add `CUSTOM_ELEMENTS_SCHEMA` to any standalone component that renders `<mindset-agent>` (or to the `NgModule` that declares it):

    ```ts theme={null}
    import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

    @Component({
      selector: 'app-agent-chat',
      standalone: true,
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      templateUrl: './agent-chat.component.html',
    })
    export class AgentChatComponent {}
    ```

    `CUSTOM_ELEMENTS_SCHEMA` tells Angular to leave any unrecognized tag and its attributes alone instead of validating them. It's scoped to the component, so it doesn't loosen checking anywhere else.

    ### Initialization

    Call `mindset.init()` once, before the app renders the element. The cleanest place is an app initializer so it runs during bootstrap:

    ```ts theme={null}
    // src/app/mindset.service.ts
    import { Injectable } from '@angular/core';
    import { environment } from '../environments/environment';

    declare global {
      interface Window {
        mindset: {
          init(config: {
            appUid: string;
            fetchAuthentication: () => Promise<string>;
          }): Promise<void>;
        };
      }
    }

    @Injectable({ providedIn: 'root' })
    export class MindsetService {
      private initialized = false;

      async init(): Promise<void> {
        if (this.initialized) return;
        this.initialized = true;

        await window.mindset.init({
          appUid: environment.mindsetAppUid,
          fetchAuthentication: async () => {
            const r = await fetch('/api/mindset-token', { credentials: 'include' });
            const { authToken } = await r.json();
            return authToken;
          },
        });
      }
    }
    ```

    Wire it into bootstrap with `provideAppInitializer` (Angular 19+), or the `APP_INITIALIZER` token on older versions:

    ```ts theme={null}
    // src/app/app.config.ts
    import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';
    import { MindsetService } from './mindset.service';

    export const appConfig: ApplicationConfig = {
      providers: [
        provideAppInitializer(() => inject(MindsetService).init()),
      ],
    };
    ```

    <Note>
      On Angular 17 and 18, use the classic token instead: `{ provide: APP_INITIALIZER, useFactory: (m: MindsetService) => () => m.init(), deps: [MindsetService], multi: true }`.
    </Note>

    Make sure the SDK script is loaded in your `index.html` before Angular boots:

    ```html theme={null}
    <script src="MINDSET-SERVER-URL/mindset-sdk3.umd.js"></script>
    ```

    `init()` is safe to call before the `<mindset-agent>` element exists in the DOM. The SDK uses a `MutationObserver` to bind elements Angular mounts later.

    ### TypeScript types for the element and events

    The SDK doesn't ship TypeScript declarations, so you declare the types you use locally. Unlike React, Angular doesn't use JSX, so you don't declare a JSX namespace, because `CUSTOM_ELEMENTS_SCHEMA` already lets the template accept the tag. What you want is a type for the element instance you get from `@ViewChild`, plus the event payloads:

    ```ts theme={null}
    // src/types/mindset.d.ts
    export interface PageToolDefinition {
      name: string;
      description: string;
      parameters: Record<string, unknown>;
      handler: (args: any) => Promise<unknown>;
    }

    export interface MindsetAgentElement extends HTMLElement {
      setPageTools(tools: PageToolDefinition[]): void;
      setSituationalAwareness(sa: Record<string, string>): void;
      setPassthroughParams(params: Record<string, string>): void;
      sendMessage(text: string, options?: { silent?: boolean }): void;
      isAgentBusy(): boolean;
      newThread(): Promise<void>;
      switchThread(threadUid: string): Promise<void>;
      deleteThread(threadUid: string): Promise<boolean>;
      renameThread(threadUid: string, title: string): Promise<boolean>;
      listThreads(options?: { pageSize?: number; pageToken?: string }): Promise<{
        threads: Array<{
          uid: string;
          title: string;
          createdAt: string;
          updatedAt: string;
          messageCount: number;
        }>;
        nextPageToken: string | null;
        hasMore: boolean;
      }>;
      readonly threadUid: string | null;
    }

    // Event detail shapes. See events reference for the full list.
    export interface MindsetEventMap {
      'mindset:agent-registered': CustomEvent<{ headless: boolean }>;
      'mindset:agent-initializing': CustomEvent<{}>;
      'mindset:agent-idle': CustomEvent<{}>;
      'mindset:agent-busy': CustomEvent<{}>;
      'mindset:agent-error': CustomEvent<{ code: string; message: string }>;
      'mindset:thread-changed': CustomEvent<{ threadUid: string | null; previous: string | null }>;
      'mindset:text-delta': CustomEvent<{ content: string }>;
      'mindset:complete': CustomEvent<{ response: string; threadId: string }>;
      // …add more as you use them
    }
    ```

    ### A reusable subscription helper

    The recurring pattern is: get the element, add a listener, re-enter the Angular zone so change detection runs, and remove the listener on destroy. A small helper captures all of it. It uses `NgZone` and `DestroyRef`, both available from `inject()`:

    ```ts theme={null}
    // src/app/agent-events.ts
    import { DestroyRef, NgZone } from '@angular/core';
    import type { MindsetEventMap } from '../types/mindset';

    /**
     * Subscribe to a mindset:* event on an element, with change detection and
     * automatic teardown. Call from a component's injection context (constructor
     * or a field initializer) so inject(DestroyRef) resolves.
     */
    export function onAgentEvent<K extends keyof MindsetEventMap>(
      el: HTMLElement,
      type: K,
      handler: (e: MindsetEventMap[K]) => void,
      zone: NgZone,
      destroyRef: DestroyRef,
    ): void {
      const listener = (e: Event) =>
        // Re-enter Angular's zone: the SDK dispatches some events from async
        // callbacks that may have escaped the zone, which would skip change
        // detection. zone.run() guarantees the view updates.
        zone.run(() => handler(e as MindsetEventMap[K]));

      el.addEventListener(type, listener);
      destroyRef.onDestroy(() => el.removeEventListener(type, listener));
    }
    ```

    <Note>
      Zone.js patches `addEventListener`, so listeners usually run inside Angular's zone already. The explicit `zone.run()` is cheap insurance for events the SDK fires from promise callbacks, and it's what you'll need if you've moved to zoneless change detection (`provideExperimentalZonelessChangeDetection`). There, signal writes inside the handler drive the view and `zone.run()` is a harmless no-op.
    </Note>

    ### A chat component

    Putting it together. The element is referenced with `@ViewChild` and a template reference variable; subscriptions are wired in `ngAfterViewInit`, once the view exists. State lives in signals so the template stays declarative:

    ```ts theme={null}
    // src/app/agent-chat.component.ts
    import {
      AfterViewInit,
      ChangeDetectionStrategy,
      Component,
      CUSTOM_ELEMENTS_SCHEMA,
      DestroyRef,
      ElementRef,
      NgZone,
      ViewChild,
      inject,
      input,
      signal,
    } from '@angular/core';
    import { onAgentEvent } from './agent-events';
    import type { MindsetAgentElement } from '../types/mindset';

    interface Message {
      role: 'user' | 'agent';
      text: string;
    }

    @Component({
      selector: 'app-agent-chat',
      standalone: true,
      changeDetection: ChangeDetectionStrategy.OnPush,
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      template: `
        <mindset-agent #agent [attr.agent-uid]="agentUid()" headless></mindset-agent>

        <div class="messages">
          @for (m of messages(); track $index) {
            <div class="message message-{{ m.role }}">{{ m.text }}</div>
          }
        </div>

        <form (submit)="send($event)">
          <textarea
            [value]="input()"
            (input)="input.set($any($event.target).value)"
            placeholder="Ask anything…"
          ></textarea>
          <button type="submit" [disabled]="busy()">Send</button>
        </form>
      `,
    })
    export class AgentChatComponent implements AfterViewInit {
      // agentUid is a required input: <app-agent-chat [agentUid]="…" />
      readonly agentUid = input.required<string>();

      @ViewChild('agent') private agentRef!: ElementRef<MindsetAgentElement>;

      protected readonly messages = signal<Message[]>([]);
      protected readonly busy = signal(false);
      protected readonly input = signal('');

      private readonly zone = inject(NgZone);
      private readonly destroyRef = inject(DestroyRef);

      ngAfterViewInit(): void {
        const el = this.agentRef.nativeElement;

        onAgentEvent(el, 'mindset:agent-busy', () => {
          this.busy.set(true);
          this.messages.update((prev) => [...prev, { role: 'agent', text: '' }]);
        }, this.zone, this.destroyRef);

        onAgentEvent(el, 'mindset:agent-idle', () => {
          this.busy.set(false);
        }, this.zone, this.destroyRef);

        onAgentEvent(el, 'mindset:text-delta', (e) => {
          this.messages.update((prev) => {
            const next = [...prev];
            const last = next[next.length - 1];
            if (last && last.role === 'agent') {
              next[next.length - 1] = { ...last, text: last.text + e.detail.content };
            }
            return next;
          });
        }, this.zone, this.destroyRef);

        onAgentEvent(el, 'mindset:complete', (e) => {
          console.log('Turn done in thread', e.detail.threadId);
        }, this.zone, this.destroyRef);
      }

      send(e: Event): void {
        e.preventDefault();
        const text = this.input().trim();
        const el = this.agentRef?.nativeElement;
        if (!text || !el) return;

        this.messages.update((prev) => [...prev, { role: 'user', text }]);
        this.sendWhenIdle(el, text);
        this.input.set('');
      }

      // sendWhenIdle handles the case where the agent is still busy with its
      // icebreaker turn (an automatic first turn some agents are configured to
      // fire on startup). If the agent is idle, it sends straight away; if it's
      // busy, it waits for the next mindset:agent-idle and recurses. Safe to call
      // at any time.
      private sendWhenIdle(el: MindsetAgentElement, text: string): void {
        if (!el.isAgentBusy()) {
          el.sendMessage(text);
        } else {
          el.addEventListener(
            'mindset:agent-idle',
            () => this.sendWhenIdle(el, text),
            { once: true },
          );
        }
      }
    }
    ```

    ### How it works

    The Angular-specific pieces, and why each one matters:

    * `CUSTOM_ELEMENTS_SCHEMA` is required. Without it the template compiler throws `NG8001: 'mindset-agent' is not a known element`. With it, Angular passes the tag and its attributes straight to the DOM.
    * You can't bind `mindset:*` events in the template. Angular reads the colon in `(mindset:text-delta)` as a `target:event` pair (the same syntax as `(window:resize)`), so the binding silently targets a non-existent `mindset` global. Subscribe with `addEventListener` via `@ViewChild` instead. That's what `onAgentEvent` does. Plain DOM events without a colon would bind fine, but every Mindset AI event uses the colon.
    * Re-enter the Angular zone for change detection. Events come from the element, not from Angular. `zone.run()` (or a signal write under zoneless) makes sure the view updates when a `text-delta` lands.
    * `@ViewChild` resolves in `ngAfterViewInit`, not the constructor. The element doesn't exist until the view renders, so wire subscriptions there. `DestroyRef.onDestroy` removes them when the component is torn down, with no manual `OnDestroy` boilerplate.
    * Attributes go through `[attr.agent-uid]`. Custom elements read configuration from attributes, not Angular `@Input` properties. Use attribute binding (or a static `agent-uid="…"`) so the value lands in the DOM where the element reads it. `headless` is a static boolean attribute.
    * `sendMessage` and signal writes are sync, so call them directly from the submit handler and event callbacks. Thread CRUD is async (`newThread`, `switchThread`, and the rest), so handle the returned Promises like any other API call.
    * `sendWhenIdle` covers the icebreaker race. If your agent is configured with an icebreaker (a turn that fires automatically as the agent settles after init), the lifecycle is `agent-idle` (init done), then `agent-busy` (icebreaker streaming), then `agent-idle` (icebreaker done). A user clicking Send during that first window would otherwise hit a busy throw. The recursive `sendWhenIdle` re-checks busy state on each idle event, so it queues until the agent is actually free.

    ### Configuring page tools

    Set page tools after the view initializes, once you have the element ref:

    ```ts theme={null}
    ngAfterViewInit(): void {
      const el = this.agentRef.nativeElement;

      el.setPageTools([
        {
          name: 'lookup_order',
          description: "Look up the current user's order by ID",
          parameters: {
            type: 'object',
            properties: { orderId: { type: 'string' } },
            required: ['orderId'],
          },
          handler: async ({ orderId }: { orderId: string }) => {
            const r = await fetch(`/api/orders/${orderId}`);
            return await r.json();
          },
        },
      ]);

      // …event subscriptions
    }
    ```

    If `ngAfterViewInit` runs before `mindset.init()` has completed (or before the SDK script has loaded), `setPageTools` won't exist on the element yet. Gate the call on a `mindset:agent-idle` listener. See [When the element is ready to call](./methods.mdx#when-the-element-is-ready-to-call) for the pattern.

    ### Updating situational awareness when state changes

    Push page state to the agent so it can personalize responses without the user repeating themselves. With signals, an `effect` runs the update whenever the source state changes:

    ```ts theme={null}
    import { effect, inject } from '@angular/core';
    import { AppStateService } from './app-state.service';

    private readonly appState = inject(AppStateService);

    constructor() {
      // appState exposes signals: currentPage(), userPlan(), cartItemCount()
      effect(() => {
        this.agentRef?.nativeElement.setSituationalAwareness({
          currentPage: this.appState.currentPage(),
          userPlan: this.appState.userPlan(),
          cartItems: String(this.appState.cartItemCount()),
        });
      });
    }
    ```

    If your app state lives in RxJS observables instead, subscribe and call `setSituationalAwareness` in the `next` handler, and use `takeUntilDestroyed()` for cleanup. The agent reads the latest values on its next turn either way.
  </Tab>
</Tabs>

## Related

<CardGroup cols={2}>
  <Card title="Methods reference" icon="terminal" href="/deploy/sdk3-client-apis/methods">
    Every method on the element with signatures and behaviors.
  </Card>

  <Card title="Events reference" icon="bell" href="/deploy/sdk3-client-apis/events">
    Every event the element fires, with payload shapes.
  </Card>

  <Card title="Integration walkthrough" icon="compass" href="/deploy/sdk3-client-apis/integration-guide">
    The end-to-end setup, from token endpoint to working agent.
  </Card>

  <Card title="Data channels overview" icon="diagram-project" href="/deploy/sdk3-client-apis/data-channels-overview">
    How your application talks to the agent.
  </Card>
</CardGroup>
