{"slug":"ask-leo-to-buttons","meta":{"title":"'Ask Leo to..' Buttons (prefill Leo's chat from your app)","slug":"ask-leo-to-buttons","category":"Integrations","summary":"Buttons on your app's pages that drop a pre-written command straight into Leo's chat input via postMessage — one click instead of the user typing the request out.","tags":["postmessage","iframe","leo-chat","launchpad","javascript","stimulus","ux-default"],"status":"stable","visibility":"public","source_project":"mbc-preceptors.leo.llamapress.ai sign-in page","layers":["view","stimulus_js"],"related":[{"title":"'Ask Leo to..' Buttons (customer guide)","url":"/wiki/ask-leo-to-buttons","summary":"The customer-facing explanation of this pattern on the wiki — what the buttons do and how to ask Leo for one."},{"title":"Leo Tools \u0026 Features","url":"/wiki/leo-tools-and-features","summary":"A tour of the chat interface these buttons talk to."}]},"body":"# 'Ask Leo to..' Buttons (prefill Leo's chat from your app)\n\n\u003e ⚠️ **Cookbook example — not live code.** (KEEP THIS CALLOUT.) Every code block below\n\u003e is an **example snippet**, **not part of the llamapress.ai codebase**, and **not\n\u003e running on this server**. This is a reference recipe for a **Leo instance (an AI coding\n\u003e agent) to implement in its own app** — read it to understand the pattern, then recreate\n\u003e it there.\n\nYour app's pages render inside an iframe in the Leo workspace, with Leo's chat sitting\nin the parent window. That parent listens for a specific `postMessage` shape — send it\nand **Leo's chat input fills with your text, focused and ready to send**. An \"Ask Leo\nto..\" button packages a whole request (\"reset this password\", \"export this report\",\n\"fix this page\") into one click: the user reviews the pre-filled command, edits any\nplaceholder, and hits send.\n\nThe proven origin: a sign-in page with an **\"Ask Leo to Reset\"** button next to each\nregistered email. Clicking it pre-fills Leo's chat with\n`Ask Leo to reset the password for jane@example.com to [new_password] so I can sign\ninto the application.` — the user swaps in a password and sends. No admin panel, no\nconsole, and Leo (not hand-built form code) performs the action.\n\n\u003e **When to use:** actions that are really *requests for the AI agent* — resets, fixes,\n\u003e data changes, \"do X for me\" — where writing the prompt is the user's hurdle. The user\n\u003e stays in the loop: they see the command before it runs.\n\u003e **When not to:** actions your app should just *do* (a normal form/controller is\n\u003e faster and doesn't burn an agent turn), or anything on a page where visitors aren't\n\u003e Leo users — outside the workspace the buttons are silent no-ops.\n\n\u003c!-- ▼ LIVE DEMO block — renders interactively on the llamapress.ai/cookbook page\n     (raw HTML passes through the markdown renderer). Agents: IGNORE this block;\n     recreate the pattern from the code layers below. The real pattern posts to\n     window.parent (the Leo workspace); this demo simulates the chat input inline\n     because this page has no parent frame. Inline handlers only — an\n     innerHTML-injected \u003cscript\u003e never executes. --\u003e\n\u003cdiv class=\"not-prose my-6 rounded-2xl border border-gray-200 overflow-hidden shadow-sm\"\u003e\n  \u003cdiv class=\"bg-gray-50 border-b border-gray-200 px-4 py-2 flex items-center gap-2\"\u003e\n    \u003cspan class=\"w-2 h-2 rounded-full bg-emerald-400\"\u003e\u003c/span\u003e\n    \u003cspan class=\"text-[10px] font-bold uppercase tracking-widest text-gray-500\"\u003eLive demo — simulated (the real one fills Leo's actual chat)\u003c/span\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"p-5 bg-white space-y-4\"\u003e\n    \u003cdiv class=\"flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2\"\u003e\n      \u003cspan class=\"text-sm text-gray-700 truncate\"\u003ejane@example.com\u003c/span\u003e\n      \u003cbutton type=\"button\" class=\"shrink-0 ml-2 text-xs bg-indigo-600 text-white px-2.5 py-1 rounded font-medium hover:bg-indigo-700 transition-colors\" onclick=\"var d=document.getElementById('demo-leo-chat-input');d.value=this.dataset.command;d.focus();\" data-command=\"Ask Leo to reset the password for jane@example.com to [new_password] so I can sign into the application.\"\u003eAsk Leo to Reset\u003c/button\u003e\n    \u003c/div\u003e\n    \u003cdiv\u003e\n      \u003cp class=\"text-xs font-semibold text-gray-500 mb-1\"\u003eLeo's chat input (simulated)\u003c/p\u003e\n      \u003ctextarea id=\"demo-leo-chat-input\" rows=\"2\" placeholder=\"Click the button above…\" class=\"w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:ring-indigo-500\"\u003e\u003c/textarea\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n---\n\n## The 80/20 in one breath\n\n1. Put the full command text in a `data-command` attribute on a button with a shared\n   class (e.g. `.ask-leo-btn`). Use `[bracketed_placeholders]` for anything the user\n   must fill in.\n2. On click, post `{ source: 'launchpad', type: 'prefill-chat', command }` to\n   `window.parent` — that exact `source`/`type` pair is what Leo's chat listens for.\n3. Guard with `window.parent !== window` so the button no-ops on the standalone site.\n4. Done — the receiving side ships with every Leo workspace. It fills the chat input,\n   focuses it, and dispatches an `input` event so the send button activates.\n5. Optional: add `auto_send: true` to send immediately instead of pre-filling.\n\n---\n\n## Layer 1 — The View (button + inline script, the proven minimal version)\n\n```erb\n\u003c%# app/views/devise/sessions/new.html.erb (or any page in your app) %\u003e\n\u003cdiv class=\"flex items-center justify-between py-1.5 px-3 rounded hover:bg-slate-50 group\"\u003e\n  \u003cspan class=\"text-sm text-slate-700 truncate min-w-0\"\u003ejane@example.com\u003c/span\u003e\n  \u003cbutton class=\"ask-leo-btn shrink-0 ml-2 text-xs bg-indigo-600 text-white px-2.5 py-1 rounded font-medium hover:bg-indigo-700 transition-colors\"\n          data-command=\"Ask Leo to reset the password for jane@example.com to [new_password] so I can sign into the application.\"\u003e\n    Ask Leo to Reset\n  \u003c/button\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\n  (function() {\n    'use strict';\n    document.querySelectorAll('.ask-leo-btn').forEach(function(btn) {\n      btn.addEventListener('click', function(e) {\n        e.stopPropagation();\n        var command = this.dataset.command;\n        if (window.parent !== window) {\n          window.parent.postMessage({\n            source: 'launchpad',      // exact value required\n            type: 'prefill-chat',     // exact value required\n            command: command          // lands in Leo's chat input verbatim\n          }, '*');\n        }\n      });\n    });\n  })();\n\u003c/script\u003e\n```\n\nThe message contract, in full:\n\n| Field | Value | Notes |\n|---|---|---|\n| `source` | `'launchpad'` | Required, exact. Anything else is ignored by the listener. |\n| `type` | `'prefill-chat'` | Required, exact. |\n| `command` | your text | Placed into the chat input verbatim. |\n| `auto_send` | `true` (optional) | Sends immediately instead of waiting for the user. Use sparingly. |\n\nTarget origin is `'*'` because Leo's chat interface runs on a different origin than\nyour app's pages. That's acceptable here — the message only pre-fills text and carries\nnothing sensitive (see Gotchas).\n\n## Layer 2 — Stimulus variant (house style, plays nicely with Turbo)\n\nThe inline script above re-binds fine on full page loads, but a Stimulus controller is\nthe idiomatic Rails/Hotwire shape and survives Turbo navigation without a re-run:\n\n```javascript\n// app/javascript/controllers/ask_leo_controller.js\nimport { Controller } from \"@hotwired/stimulus\"\n\n// Sends a pre-written command into Leo's chat input (the parent workspace frame).\n// Usage: \u003cbutton data-controller=\"ask-leo\"\n//                data-action=\"ask-leo#send\"\n//                data-ask-leo-command-value=\"Ask Leo to ...\"\u003eAsk Leo\u003c/button\u003e\nexport default class extends Controller {\n  static values = { command: String, autoSend: { type: Boolean, default: false } }\n\n  send(event) {\n    event.stopPropagation()\n    if (window.parent === window) return   // standalone site: no-op\n    window.parent.postMessage({\n      source: \"launchpad\",\n      type: \"prefill-chat\",\n      command: this.commandValue,\n      auto_send: this.autoSendValue\n    }, \"*\")\n  }\n}\n```\n\n```erb\n\u003c%# anywhere in a view %\u003e\n\u003cbutton data-controller=\"ask-leo\"\n        data-action=\"ask-leo#send\"\n        data-ask-leo-command-value=\"Ask Leo to export this month's report as a CSV.\"\u003e\n  Ask Leo to Export\n\u003c/button\u003e\n```\n\n## Layer 3 — What the receiving side does (context only — do NOT build this)\n\nEvery Leo workspace already ships the listener in its chat frontend. For understanding\n(so you can debug, not so you can implement):\n\n```javascript\n// (ships with the Leo chat interface — reference only)\nwindow.addEventListener('message', (event) =\u003e {\n  if (event.data \u0026\u0026 event.data.source === 'launchpad' \u0026\u0026 event.data.type === 'prefill-chat') {\n    const command = event.data.command;\n    if (command \u0026\u0026 this.elements.messageInput) {\n      this.elements.messageInput.value = command;\n      this.elements.messageInput.focus();\n      this.elements.messageInput.dispatchEvent(new Event('input', { bubbles: true }));\n      if (event.data.auto_send) {\n        this.sendMessageWithDebugInfo();\n      }\n    }\n  }\n});\n```\n\n---\n\n## Gotchas (the hard-won stuff)\n\n- **The `source`/`type` strings are an exact contract.** `'launchpad'` +\n  `'prefill-chat'` — a typo (or `prefill_chat`) fails silently. There is no error\n  channel; the message just doesn't match and nothing happens.\n- **Silent no-op outside the workspace is a feature AND a trap.** The\n  `window.parent !== window` guard means the button does nothing on the app's public\n  URL. Keep the guard (never remove it), but if the page is public-facing, consider\n  hiding the buttons or showing a hint when the page isn't embedded — otherwise real\n  visitors click a dead button.\n- **Never put secrets in `data-command`.** The attribute is plain page source, visible\n  to anyone who can load the page. That's exactly why the password-reset command uses a\n  `[new_password]` placeholder the user types into the chat, instead of generating a\n  password into the button.\n- **Prefer pre-fill over `auto_send`.** The pattern's strength is the human review step\n  — the user sees and can edit the command before it runs. Reserve `auto_send: true`\n  for commands with no placeholders and no surprises; a command containing\n  `[brackets]` must never auto-send.\n- **Write commands self-contained.** Leo reads the command with no other context from\n  the button — bake the specifics (email, record, page) into the text and state the\n  goal (\"…so I can sign into the application\"). Vague commands get vague agent turns.\n- **`e.stopPropagation()` matters** when the button sits inside a clickable row/card —\n  without it the row's own click handler (navigation, selection) fires too.\n- **Cross-origin is normal here.** Your page and the chat interface are on different\n  origins, so you cannot reach the chat DOM directly — `postMessage` with `'*'` is the\n  supported channel. Don't try to tighten the target origin to your own host; the\n  parent is *not* on your host.\n\n---\n\n## Files this pattern touches\n\n```\napp/views/\u003cany_page\u003e.html.erb                      # button(s) + inline script (Layer 1)\napp/javascript/controllers/ask_leo_controller.js   # optional Stimulus variant (Layer 2)\n```\n\n## How to adapt to your schema\n\n1. Decide the action and write the command sentence first — complete, specific,\n   self-contained, with `[placeholders]` for user-supplied values.\n2. Drop the button where the action lives (a table row, a card, a settings page) and\n   put the command in `data-command` (or `data-ask-leo-command-value` for the Stimulus\n   variant). Loop over records to generate one button per row\n   (`data-command=\"Ask Leo to ... \u003c%= user.email %\u003e ...\"`).\n3. Pick inline script (one page, zero setup) or the Stimulus controller (used on\n   several pages / Turbo-heavy app).\n4. Style the button to match your app — the pattern has no styling requirements.\n5. Safe to drop: `auto_send` (default false), the Stimulus variant, the per-row loop —\n   the minimal version is one button and the five-line script.\n"}