opencode icon indicating copy to clipboard operation
opencode copied to clipboard

[FEATURE]: Plugin Hook for Instant TUI Commands

Open malhashemi opened this issue 1 month ago • 16 comments

Feature hasn't been suggested before.

  • [x] 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

malhashemi avatar Dec 10 '25 01:12 malhashemi

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

  • #2185: Hooks for commands (Plugin Commands) - Originally requested hooks to capture commands and allow plugins to define commands instead of requiring markdown files
  • #5148: Comprehensive Plugin Pipeline - Middleware-Style Data Flow Control - A broader feature request for plugin system extensibility that could encompass plugin commands as part of a larger pipeline architecture

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

github-actions[bot] avatar Dec 10 '25 01:12 github-actions[bot]

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

malhashemi avatar Dec 10 '25 01:12 malhashemi