CODEX // oaoisme wiki

§ Ember — habit & ritual tracker · Telegram — nudges & check-ins

Framework upgrade: inline buttons + callbacks

updated 2026-06-02

To give plugins tappable buttons, the bot framework's IPC contract and router grew three things — all backward-compatible (existing plugins untouched; 98 router + 8 SDK tests green).

1. Richer replies (router ← plugin)

ReplyFrame and AlertFrame gained optional fields: buttons (an inline keyboard as rows of {text,data}), plus edit and answer for callbacks, and style on alerts ("notify" = a clean message with no ⚠ [plugin] prefix). Old plugins that send a plain {text} still work — the fields default to null.

2. A new callback frame (router → plugin)

CallbackFrame {id, data, ctx} carries a button tap to the owning plugin. The supervisor got invoke_callback(data, ctx) (mirrors invoke, collects reply frames); the lifecycle got invoke_callback(plugin, data, ctx) that looks the plugin up by name.

3. Routing the taps (telegram_client)

  • build_markup(plugin, buttons) turns a plugin's buttons into an InlineKeyboardMarkup whose callback_data is namespaced: "<plugin>~<payload>". That namespace is how a tap finds its way home.
  • A CallbackQueryHandler splits the data on ~, allowlist-checks the chat, calls invoke_callback, then either edits the source message (edit=True) or sends a new one, and always answer()s the query to stop the spinner.

SDK additions (plugin author side)

from bot_plugin import command, on_callback, watcher, Reply, Alert, button, run

@on_callback
def cb(ctx, data):                 # data = your payload, e.g. "done:habit:5"
    return Reply("🔥 done", edit=True, answer="Done!")

# a command or watcher can return buttons:
Reply("Pick one", buttons=[button("Yes","y"), button("No","n")](/s/ember-rituals/button-yes-y-button-no-n))
Alert(key=..., text="…", style="notify", buttons=[button("✅","done:habit:5")](/s/ember-rituals/button-done-habit-5))

Design note: callbacks are routed by plugin-name prefix, not pre-registered. A plugin just declares one @on_callback handler and parses its own payload — so adding buttons never touches the router.