Streaming Cards
Every conversation turn produces a live-updating Lark card, your primary window for perceiving and controlling the CLI on your phone or in Lark.

- Live screenshots of the terminal refreshed onto the card: xterm renders headlessly into an image that faithfully reproduces the CLI's TUI (borders and colors are all there), instead of converting output to Markdown. One click to "Show / Hide output," "Export text," and "Half-page up / down."
- Live status indicator: the card's header color is the status (a Lark card template, not an emoji dot in the body) — Starting… (yellow) → Working (blue) → Waiting for input (green); when the quota is used up it shows Limit reached (red), turning to Retryable (green) when it can retry.
- Operate directly from the card: open the Web terminal, 🔑 grab an operation link, close the session, and — when the quota is retryable — "🔁 Resend last task."
- A fresh card per turn: the previous card freezes as an archive, keeping conversation history clear and traceable; after a session is moved to another group with
/relay, the original card also automatically freezes as an archive (buttons removed). - A "recoverable" card on close: it carries a "▶️ Resume session" button to click back in anytime; if the CLI supports native resume (the adapter implements
buildResumeCommandand a native session id exists), it also includes the native command (e.g.claude --resume <id>) for manual recovery; when unsupported, only botmux's resume button plus a short note is shown.
Pinning The Current Live Card
When a bot enables pinStreamingCard, Botmux tries to pin the current public live-status card to the top of the chat so the close-session and terminal entry points stay easy to reach.
- This is a per-bot, opt-in, default-off setting.
- Pinning is still chat-wide at the Feishu layer, while each active session keeps its own current/frozen streaming-card lifecycle. If the same chat has multiple active topics or multiple bots, you may therefore see multiple independently managed group-level Pin entries.
- Only the current public live-status real
streamCardIdparticipates. - Repo-picker cards, private
/cardsnapshots, final reply cards, CoT, closed cards, and every other interactive card stay out of scope. - After the switch changes through the dashboard or
/botconfig set pinStreamingCard on/off, Botmux immediately runs a best-effort hot reconciliation across that bot's existing active sessions; the configuration response itself does not wait for Feishu Pin/Unpin completion. /card pin offis the per-chat escape hatch: it stops Botmux from pinning streaming cards in the current chat while keeping live cards themselves enabled./card pin onrestores Pin for that chat, and/card pin statusreports whether Pin is off at the bot level, opted out for this chat, or effectively on.- Failures are fail-open: they never interrupt card publication, transfer, resume, close, or the configuration write. During exceptional periods you may temporarily see zero Pins or multiple Pins.
- The feature keeps no durable retry journal, and startup recovery remains deliberately narrow. On restart, Botmux lists the chat's current Pins as the single ownership authority for persisted cards: a remote Pin counts only when Feishu reports
operator_id_type: "app_id"for the samelarkAppId, and cleanup is further restricted to the strict intersection with the enqueue-time local candidate IDs already known to this process. An already-Pinned current card with human, other-app, mixed, or malformed provenance is left untouched and is not re-pinned. If the current card is absent, Botmux accepts a create only when the returneddata.pinrepeats both the exact message ID and same-app provenance. Botmux never broad-cleans arbitrary remote Pins, and any lookup or Pin API failure stays fail-open. Explicit bot-wide/per-chat off cleans process-owned IDs plus locally known IDs freshly proven same-app; ordinary disable, close, and transfer clean process-owned IDs only.
Open terminal = read-only: the card's main "🖥️ Open Web Terminal" button is read-only viewing; for writable control, tap "🔑 Get operation link" — delivered privately: a flat group prefers an in-chat "visible-to-you" ephemeral card (so you never leave the conversation), falling back to a DM only for topic/thread or p2p chats, or when the ephemeral card fails. Management buttons like "🔄 Restart" and "apply profile" live on the session card, not on each turn's streaming card.
Interrupting / correcting a running turn
To stop or correct it mid-turn, don't wait for it to finish: in screenshot mode the card carries a row of quick keys at the bottom — Esc, ^C, Tab, Space, Enter, arrow keys, ⇞ Half-page up / ⇟ Half-page down. Tapping Esc writes the ESC byte straight into the live terminal (exactly like pressing Esc locally); ^C likewise. After interrupting, just add a new instruction.
This quick-key row only appears when output is shown (screenshot mode) and the backend isn't
riff— "Show output" first, then Esc is available. The default behavior is not to interrupt the current turn; new messages queue (type-ahead) and are fed in after the turn ends. To correct immediately, use Esc to break first.
Messages the CLI proactively sends
The card body is a live screenshot (image) of the terminal, not text rendering. Messages the CLI proactively sends (via botmux send) are separate rich-text / image-and-text messages that can carry images, files, and @mentions; for fully custom display, --card-file / --card-json can send raw interactive card JSON.
⚠️ Raw cards allow display-only elements + open_url buttons only: any callback-firing control — callback buttons (with a
value), dropdown / person selects, date-time pickers, inputs, form submits — is rejected. This prevents custom cards from forging interactive callbacks.
Updating a card after sending (card patch)
A successful botmux send --card-file/--card-json prints {"success":true,"messageId":"om_...",...}. botmux card patch updates that same card in place by its messageId — no new message, same chat/topic — which makes it ideal for progress cards:
- The replacement card JSON goes through the same safety validation as sending (display-only + open_url; callback controls are rejected).
- The
sendexamples pass--no-mention: a progress card doesn't need to @ anyone, and explicitly opting out avoids the mention-policy gate (exit 2). - Bot identity is resolved from the session context (same as
send); errors such as a withdrawn message, missing permission, or a non-card target are surfaced as-is (exit 1). - Success prints
{"success":true,"messageId":"om_xxx","sessionId":"..."}(JSON only on stdout); usage errors exit 2.
