Ask Leo to.. Buttons That Prefill the Chat | LlamaPress Cookbook
LlamaPress
Cookbook
Integrations Stable

'Ask Leo to..' Buttons (prefill Leo's chat from your app)

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.

Proven on mbc-preceptors.leo.llamapress.ai sign-in page view · stimulus_js
Live demo — simulated (the real one fills Leo's actual chat)
jane@example.com

Leo's chat input (simulated)

'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

  1. Put the full command text in a data-command attribute on a button with a shared class (e.g. .ask-leo-btn). Use [bracketed_placeholders] for anything the user must fill in.
  2. On click, post { source: 'launchpad', type: 'prefill-chat', command } to window.parent — that exact source/type pair is what Leo's chat listens for.
  3. Guard with window.parent !== window so the button no-ops on the standalone site.
  4. Done — the receiving side ships with every Leo workspace. It fills the chat input, focuses it, and dispatches an input event so the send button activates.
  5. Optional: add auto_send: true to 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/type strings are an exact contract. 'launchpad' + 'prefill-chat' — a typo (or prefill_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 !== window guard 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. Reserve auto_send: true for 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 — postMessage with '*' 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

  1. Decide the action and write the command sentence first — complete, specific, self-contained, with [placeholders] for user-supplied values.
  2. Drop the button where the action lives (a table row, a card, a settings page) and put the command in data-command (or data-ask-leo-command-value for the Stimulus variant). Loop over records to generate one button per row (data-command="Ask Leo to ... <%= user.email %> ...").
  3. Pick inline script (one page, zero setup) or the Stimulus controller (used on several pages / Turbo-heavy app).
  4. Style the button to match your app — the pattern has no styling requirements.
  5. 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.

Related