'Ask Leo to..' Buttons (prefill Leo's chat from your app)
⚠️ Cookbook example — not live code. (KEEP THIS CALLOUT.) Every code block below is an example snippet, not part of the llamapress.ai codebase, and not running on this server. This is a reference recipe for a Leo instance (an AI coding agent) to implement in its own app — read it to understand the pattern, then recreate it there.
Your app's pages render inside an iframe in the Leo workspace, with Leo's chat sitting
in the parent window. That parent listens for a specific postMessage shape — send it
and Leo's chat input fills with your text, focused and ready to send. An "Ask Leo
to.." button packages a whole request ("reset this password", "export this report",
"fix this page") into one click: the user reviews the pre-filled command, edits any
placeholder, and hits send.
The proven origin: a sign-in page with an "Ask Leo to Reset" button next to each
registered email. Clicking it pre-fills Leo's chat with
Ask Leo to reset the password for jane@example.com to [new_password] so I can sign into the application. — the user swaps in a password and sends. No admin panel, no
console, and Leo (not hand-built form code) performs the action.
When to use: actions that are really requests for the AI agent — resets, fixes, data changes, "do X for me" — where writing the prompt is the user's hurdle. The user stays in the loop: they see the command before it runs. When not to: actions your app should just do (a normal form/controller is faster and doesn't burn an agent turn), or anything on a page where visitors aren't Leo users — outside the workspace the buttons are silent no-ops.
The 80/20 in one breath
- Put the full command text in a
data-commandattribute on a button with a shared class (e.g..ask-leo-btn). Use[bracketed_placeholders]for anything the user must fill in. - On click, post
{ source: 'launchpad', type: 'prefill-chat', command }towindow.parent— that exactsource/typepair is what Leo's chat listens for. - Guard with
window.parent !== windowso the button no-ops on the standalone site. - Done — the receiving side ships with every Leo workspace. It fills the chat input,
focuses it, and dispatches an
inputevent so the send button activates. - Optional: add
auto_send: trueto send immediately instead of pre-filling.
Layer 1 — The View (button + inline script, the proven minimal version)
<%# app/views/devise/sessions/new.html.erb (or any page in your app) %>
<div class="flex items-center justify-between py-1.5 px-3 rounded hover:bg-slate-50 group">
<span class="text-sm text-slate-700 truncate min-w-0">jane@example.com</span>
<button 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"
data-command="Ask Leo to reset the password for jane@example.com to [new_password] so I can sign into the application.">
Ask Leo to Reset
</button>
</div>
<script>
(function() {
'use strict';
document.querySelectorAll('.ask-leo-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var command = this.dataset.command;
if (window.parent !== window) {
window.parent.postMessage({
source: 'launchpad', // exact value required
type: 'prefill-chat', // exact value required
command: command // lands in Leo's chat input verbatim
}, '*');
}
});
});
})();
</script>
The message contract, in full:
| Field | Value | Notes |
|---|---|---|
source |
'launchpad' |
Required, exact. Anything else is ignored by the listener. |
type |
'prefill-chat' |
Required, exact. |
command |
your text | Placed into the chat input verbatim. |
auto_send |
true (optional) |
Sends immediately instead of waiting for the user. Use sparingly. |
Target origin is '*' because Leo's chat interface runs on a different origin than
your app's pages. That's acceptable here — the message only pre-fills text and carries
nothing sensitive (see Gotchas).
Layer 2 — Stimulus variant (house style, plays nicely with Turbo)
The inline script above re-binds fine on full page loads, but a Stimulus controller is the idiomatic Rails/Hotwire shape and survives Turbo navigation without a re-run:
// app/javascript/controllers/ask_leo_controller.js
import { Controller } from "@hotwired/stimulus"
// Sends a pre-written command into Leo's chat input (the parent workspace frame).
// Usage: <button data-controller="ask-leo"
// data-action="ask-leo#send"
// data-ask-leo-command-value="Ask Leo to ...">Ask Leo</button>
export default class extends Controller {
static values = { command: String, autoSend: { type: Boolean, default: false } }
send(event) {
event.stopPropagation()
if (window.parent === window) return // standalone site: no-op
window.parent.postMessage({
source: "launchpad",
type: "prefill-chat",
command: this.commandValue,
auto_send: this.autoSendValue
}, "*")
}
}
<%# anywhere in a view %>
<button data-controller="ask-leo"
data-action="ask-leo#send"
data-ask-leo-command-value="Ask Leo to export this month's report as a CSV.">
Ask Leo to Export
</button>
Layer 3 — What the receiving side does (context only — do NOT build this)
Every Leo workspace already ships the listener in its chat frontend. For understanding (so you can debug, not so you can implement):
// (ships with the Leo chat interface — reference only)
window.addEventListener('message', (event) => {
if (event.data && event.data.source === 'launchpad' && event.data.type === 'prefill-chat') {
const command = event.data.command;
if (command && this.elements.messageInput) {
this.elements.messageInput.value = command;
this.elements.messageInput.focus();
this.elements.messageInput.dispatchEvent(new Event('input', { bubbles: true }));
if (event.data.auto_send) {
this.sendMessageWithDebugInfo();
}
}
}
});
Gotchas (the hard-won stuff)
- The
source/typestrings are an exact contract.'launchpad'+'prefill-chat'— a typo (orprefill_chat) fails silently. There is no error channel; the message just doesn't match and nothing happens. - Silent no-op outside the workspace is a feature AND a trap. The
window.parent !== windowguard means the button does nothing on the app's public URL. Keep the guard (never remove it), but if the page is public-facing, consider hiding the buttons or showing a hint when the page isn't embedded — otherwise real visitors click a dead button. - Never put secrets in
data-command. The attribute is plain page source, visible to anyone who can load the page. That's exactly why the password-reset command uses a[new_password]placeholder the user types into the chat, instead of generating a password into the button. - Prefer pre-fill over
auto_send. The pattern's strength is the human review step — the user sees and can edit the command before it runs. Reserveauto_send: truefor commands with no placeholders and no surprises; a command containing[brackets]must never auto-send. - Write commands self-contained. Leo reads the command with no other context from the button — bake the specifics (email, record, page) into the text and state the goal ("…so I can sign into the application"). Vague commands get vague agent turns.
e.stopPropagation()matters when the button sits inside a clickable row/card — without it the row's own click handler (navigation, selection) fires too.- Cross-origin is normal here. Your page and the chat interface are on different
origins, so you cannot reach the chat DOM directly —
postMessagewith'*'is the supported channel. Don't try to tighten the target origin to your own host; the parent is not on your host.
Files this pattern touches
app/views/<any_page>.html.erb # button(s) + inline script (Layer 1)
app/javascript/controllers/ask_leo_controller.js # optional Stimulus variant (Layer 2)
How to adapt to your schema
- Decide the action and write the command sentence first — complete, specific,
self-contained, with
[placeholders]for user-supplied values. - Drop the button where the action lives (a table row, a card, a settings page) and
put the command in
data-command(ordata-ask-leo-command-valuefor the Stimulus variant). Loop over records to generate one button per row (data-command="Ask Leo to ... <%= user.email %> ..."). - Pick inline script (one page, zero setup) or the Stimulus controller (used on several pages / Turbo-heavy app).
- Style the button to match your app — the pattern has no styling requirements.
- Safe to drop:
auto_send(default false), the Stimulus variant, the per-row loop — the minimal version is one button and the five-line script.