Skip to content

[FEATURE]: Plugin Hook for Instant TUI Commands #5305

Closed as not planned
Closed as not planned

Description

@malhashemi

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Summary

Add a new plugin hook that allows plugins to register instant TUI commands that execute without agent involvement. This enables plugins to provide quick-action commands (like toggling modes, showing status, etc.) that respond immediately rather than going through the LLM.

Problem Statement

Current Behavior

OpenCode has two types of commands:

  1. TUI Commands (instant) - Internal commands like session.new, session.share, agent.cycle that execute immediately via keyboard shortcuts or command palette
  2. Slash Commands (agent-driven) - Custom commands defined in .opencode/command/ that become prompts sent to an LLM agent

Plugins can only create slash commands (via command markdown files), which means every plugin command must go through an agent, adding:

  • 1-3 seconds of latency
  • Context window usage
  • Unnecessary LLM inference for simple operations

Use Case: AFK Mode Toggle

I'm building an opencode-afk plugin that monitors sessions and sends notifications to the user's phone when agents need input. The plugin needs a simple toggle command:

/afk  →  Toggle AFK mode on/off

This is a boolean state toggle that should:

  • Execute instantly (< 100ms)
  • Show a toast confirmation
  • Not involve any agent reasoning

But with current architecture, /afk must:

  1. Be defined as a slash command
  2. Go through an agent that processes the template
  3. Agent calls a tool the plugin provides
  4. Tool toggles the state

This adds unnecessary latency and complexity for what should be an instant operation.

Current Architecture

Based on my research of the codebase:

TUI Command Registration (packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx)

// TUI commands are registered via React hooks in components
const command = useCommandDialog()
command.register(() => [
  {
    title: "Share session",
    value: "session.share",
    keybind: "session_share",
    category: "Session",
    onSelect: async (dialog) => {
      // Executes immediately - no agent involved
      await sdk.client.session.share({ path: { id: sessionID } })
      dialog.clear()
    },
  },
])

Plugin Hooks (packages/plugin/src/index.ts)

export interface Hooks {
  event?: (input: { event: Event }) => Promise<void>
  config?: (input: Config) => Promise<void>
  tool?: { [key: string]: ToolDefinition }
  auth?: AuthHook
  "chat.message"?: (...) => Promise<void>
  "chat.params"?: (...) => Promise<void>
  "permission.ask"?: (...) => Promise<void>
  "tool.execute.before"?: (...) => Promise<void>
  "tool.execute.after"?: (...) => Promise<void>
  // No hook for TUI commands!
}

The Gap

  • TUI commands are registered in the React component tree (client-side)
  • Plugins run on the server and communicate via events/SDK
  • There's no bridge for plugins to register instant TUI commands

Proposed Solution

New Plugin Hook: command

Add a new hook that allows plugins to register instant commands:

// In packages/plugin/src/index.ts
export interface Hooks {
  // ... existing hooks ...
  
  /**
   * Register instant TUI commands that execute without agent involvement
   */
  command?: {
    [name: string]: {
      /** Display title in command palette */
      title: string
      /** Optional description */
      description?: string
      /** Category for grouping in palette */
      category?: string
      /** Keyboard shortcut key (references keybinds config) */
      keybind?: string
      /** Callback when command is executed */
      execute: (context: CommandContext) => Promise<CommandResult>
    }
  }
}

export interface CommandContext {
  /** Current session ID if in a session */
  sessionID?: string
  /** SDK client for API calls */
  client: ReturnType<typeof createOpencodeClient>
  /** Show a toast notification */
  toast: (options: { message: string; variant: "info" | "success" | "warning" | "error" }) => void
}

export interface CommandResult {
  /** Optional message to display */
  message?: string
  /** Prevent default command palette close */
  keepOpen?: boolean
}

Example Plugin Usage

import type { Plugin } from "@opencode-ai/plugin"

export const AFKPlugin: Plugin = async (ctx) => {
  let afkEnabled = false
  
  return {
    command: {
      afk: {
        title: "Toggle AFK Mode",
        description: "Enable/disable away-from-keyboard notifications",
        category: "AFK",
        keybind: "afk_toggle", // User can configure in keybinds
        async execute({ toast }) {
          afkEnabled = !afkEnabled
          toast({
            message: afkEnabled 
              ? "AFK mode enabled - notifications will go to your phone"
              : "AFK mode disabled - welcome back!",
            variant: afkEnabled ? "success" : "info",
          })
          return {}
        },
      },
      "afk-status": {
        title: "AFK Status",
        description: "Show current AFK mode status",
        category: "AFK",
        async execute({ toast }) {
          toast({
            message: afkEnabled ? "AFK mode is ON" : "AFK mode is OFF",
            variant: "info",
          })
          return {}
        },
      },
    },
    // ... other hooks
  }
}

Implementation Approach

  1. Plugin Loader (packages/opencode/src/plugin/index.ts):

    • Collect command hooks from all plugins
    • Expose via a new API endpoint or event
  2. New Event (packages/opencode/src/cli/cmd/tui/event.ts):

    PluginCommandsUpdated: Bus.event(
      "plugin.commands.updated",
      z.object({
        commands: z.array(z.object({
          name: z.string(),
          title: z.string(),
          description: z.string().optional(),
          category: z.string().optional(),
          keybind: z.string().optional(),
        })),
      }),
    )
  3. TUI Integration (packages/opencode/src/cli/cmd/tui/context/sync.tsx or similar):

    • Subscribe to plugin.commands.updated event
    • Register commands via useCommandDialog().register()
    • On command select, call back to server to execute plugin callback
  4. Server Endpoint for command execution:

    POST /plugin/command/:name
    // Triggers the plugin's execute callback
    // Returns result for toast/feedback

Alternatives Considered

Alternative 1: Use existing tools + minimal slash command

# /afk command
Call the afk-toggle tool.

Pros: Works today
Cons: Still goes through agent (1-3s latency), wastes context

Alternative 2: Shell escape with external binary

User types !afk-toggle which runs a shell script.

Pros: Instant execution
Cons: Requires separate binary installation, fragile, no toast feedback

Alternative 3: Keybind-only (no command)

Plugin could potentially hook into a keybind directly.

Pros: Truly instant
Cons: Not discoverable, no command palette integration, limited UX

Request

I'd love to work on implementing this feature if the maintainers are open to the approach. Before I start a PR, I wanted to:

  1. Validate the use case - Is this something other plugin authors would benefit from?
  2. Get feedback on the API design - Does the proposed command hook interface make sense?
  3. Understand any constraints - Are there architectural concerns I'm missing?

Happy to iterate on the design or explore alternatives. Thanks for considering!


References

  • Plugin hook types: packages/plugin/src/index.ts
  • TUI command registration: packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx
  • Plugin loader: packages/opencode/src/plugin/index.ts
  • TUI events: packages/opencode/src/cli/cmd/tui/event.ts

Activity

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on Dec 10, 2025
  2. github-actions commented on Dec 10, 2025

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Feel free to ignore if none of these address your specific case.

  3. malhashemi commented on Dec 10, 2025

    @malhashemi
    ContributorAuthor

    This issue might be a duplicate of existing issues. Please check:

    * [Hooks for commands (Plugin Commands) #2185](https://github.com/sst/opencode/issues/2185): Hooks for commands (Plugin Commands) - Originally requested hooks to capture commands and allow plugins to define commands instead of requiring markdown files
    

    This one talks about adding commands which is something we can already do now, I believe could be closed, this issue is about TUI commands, not meant to be passed to LLMs at all

  4. denisdifazio commented on Dec 11, 2025

    @denisdifazio

    +1

    I want to create custom slash commands that don’t trigger any interaction with the LLM. For example, I built a /usage command to fetch my current Claude plan usage, but the LLM attempts to interpret and respond to the output. In this case, I want the command to run and display its result without the LLM engaging with it.

  5. malhashemi commented on Dec 11, 2025

    @malhashemi
    ContributorAuthor

    @rekram1-node

    Permission to make a PR once you are happy with the technical proposal

  6. malhashemi commented on Dec 18, 2025

    @malhashemi
    ContributorAuthor

    @rekram1-node really appreciate any feedback on this one, I believe this would be a nice addition to the plugin eco system, unlocking new capabilities

  7. rekram1-node commented on Dec 18, 2025

    @rekram1-node
    Collaborator

    Sorry didn't see messages reading this now

  8. rekram1-node commented on Dec 18, 2025

    @rekram1-node
    Collaborator

    So to me this sounds like:

    Slash commands should optionally have the ability to run actual code as part of their execution

    Would that be correct?

    Good example here is like:

    /status (doesn't fire a prompt triggers action)

    But you could create arbitrary ones like (this is totally hypothetical ignore the absurdity):

    /send-reminder and it could literally just send an email to someone (no prompt triggered)

    And maybe even the ability to run typescript code and inject it into a prompt so you could do ncie shortcuts like normal commands but with more ease of use

  9. rekram1-node commented on Dec 18, 2025

    @rekram1-node
    Collaborator

    Lmk if im following or if i misunderstood

  10. malhashemi commented on Dec 18, 2025

    @malhashemi
    ContributorAuthor

    Correct, exactly as you described, however these slash commands have to be registered as part of plugins. But now that you are framing it like this, maybe they can be registered by just adding *.ts files to the command folder, similar to how custom tools work. That would be infinitely cooler honestly, but I have not thought about where to start to implement this. If you like the proposal let me know so I can start planning something that include that as well. Could be too core of an implementation so I would ask Dax about his opinion here.

  11. rekram1-node commented on Dec 18, 2025

    @rekram1-node
    Collaborator

    Yeah I think as plugins is prolly easiest idk tho

  12. rekram1-node commented on Dec 18, 2025

    @rekram1-node
    Collaborator

    I think someone actually started this work

  13. rekram1-node commented on Dec 18, 2025

    @rekram1-node
    Collaborator

    @spoons-and-mirrors started some work here:
    #4411

  14. malhashemi commented on Dec 18, 2025

    @malhashemi
    ContributorAuthor

    Great! I will jump in on that draft one!

  15. spoons-and-mirrors commented on Dec 18, 2025

    @spoons-and-mirrors
    Contributor

    I'm on opencode's discord @spoon if you want to discuss that PR @malhashemi

  16. PhilippPolterauer commented on Jan 11, 2026

    @PhilippPolterauer

    i would really love seeing one of these plugin command PRs merged!

  17. rgodha24 commented on Jan 14, 2026

    @rgodha24
    Contributor

    +1

    I can def work on an implementation too based on the existing PRs

  18. m10rten commented on Mar 20, 2026

    @m10rten

    +1

    Much wanted feature, together with plugin loaded agents, skills and already possible: tools, this makes the suite complete.

  19. removed
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on May 3, 2026
  20. Ar9av commented on Jun 14, 2026

    @Ar9av

    For reference on how other agents draw this line: agent-manual (https://github.com/Ar9av/agent-manual) cross-documents slash commands vs. hooks across 17 agents.

    The clearest separation is Claude Code's model: slash commands (.claude/commands/<name>.md) are agent-facing — they inject a prompt into the conversation. Hooks (settings.json under hooks) are programmatic — they fire on lifecycle events and can block, transform, or observe without the agent knowing. The two systems never overlap.

    Gemini CLI follows the same split: Agent Skills (on-demand Markdown procedures) vs. Hooks (event-driven scripts). Codex doesn't have a slash command system yet, so hooks handle both roles.

    If OpenCode goes the same route, the key design question from the almanac data is whether slash commands can trigger hooks or vice versa — Claude Code keeps them strictly separate, which makes both simpler to reason about individually.

  21. rusak47 commented on Jul 17, 2026

    @rusak47

    My 5 cents here, if allowed. Recently, I found a powerful plugin that provides a command similar to Claude Code's /context — and maybe @denisdifazio. However, I noticed that the plugin's tool output isn't shown at all. For example, the grep tool produces no visible output. While its output may not always be interesting, having the option to see it would be handy.

    After some research, I found a working hack: the experimental.text.complete hook allows prepending tool output to the agent's response. However, this isn't an ideal solution, as longer outputs would add unnecessary noise. A tool output preview (#5358)— without even passing it to the agent for interpretation — could be really useful.

    Image
  22. github-actions commented on Sep 16, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

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