Skip to content

TUI: client.tui.showToast() from command.execute.before corrupts input box #50962

Description

@dchristal

Bug: /good's toast notification corrupts the input box, using the officially documented client.tui.showToast() API

opencode version: 1.18.32
OS: Arch Linux (7.1.9-arch1-2)
TERM: xterm-256color, COLORTERM=truecolor

Description

A local plugin implements a custom slash command (/good) via the command.execute.before hook. On invocation, the hook:

  1. Sets output.parts = [] (correctly suppresses the LLM turn per the documented Part[] contract for this hook)
  2. Runs some local bookkeeping (shell/sqlite calls)
  3. Calls client.tui.showToast({ body: { title, message, variant: "success" } }) — the documented SDK method for /tui/show-toast

Every single invocation of this command visually corrupts/"butchers" the TUI's input box immediately after the toast fires. This reproduces 100% of the time, across fresh sessions (confirmed by fully killing and restarting the opencode process between attempts), ruling out stale plugin state.

What we ruled out

  • Not caused by hot-reload/stale-plugin-state: reproduced on fully fresh process restarts.
  • Not caused by weird bytes/control characters in the toast message: verified plain ASCII via od -c, no embedded escape sequences.
  • Not caused by using output.parts to fake a synthetic assistant message: rewrote the plugin to use output.parts = [] (no synthetic message at all) plus the sanctioned client.tui.showToast() API instead — corruption still occurs identically.
  • Not caused by the plugin's shell subprocess calls per se — the toast fires only after those complete, and the visual corruption appears specifically once the toast renders.

This points to the toast rendering/dismissal path in the TUI conflicting with the input box's own render cycle, specifically when the toast is triggered from within a command.execute.before hook (as opposed to a normal event hook context, e.g. session.idle, which is the only toast-trigger example shown in the docs).

Repro plugin

export const GoodPlugin = async ({ client }) => {
  return {
    "command.execute.before": async (input, output) => {
      if (input.command !== "good") return;
      output.parts = [];
      await client.tui.showToast({
        body: { title: "test", message: "hello", variant: "success" },
      }).catch(() => {});
    },
  };
};

Paired with a minimal command file:

---
description: test
---
This command is handled entirely by the plugin. Do not execute anything.

Expected

Toast renders without disturbing the input box's layout/redraw state.

Actual

Input box becomes visually corrupted/garbled immediately after the toast is shown, every time, on this terminal setup.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions