-
Notifications
You must be signed in to change notification settings - Fork 6.3k
Description
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:
- TUI Commands (instant) - Internal commands like
session.new,session.share,agent.cyclethat execute immediately via keyboard shortcuts or command palette - 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:
- Be defined as a slash command
- Go through an agent that processes the template
- Agent calls a tool the plugin provides
- 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
-
Plugin Loader (
packages/opencode/src/plugin/index.ts):- Collect
commandhooks from all plugins - Expose via a new API endpoint or event
- Collect
-
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(), })), }), )
-
TUI Integration (
packages/opencode/src/cli/cmd/tui/context/sync.tsxor similar):- Subscribe to
plugin.commands.updatedevent - Register commands via
useCommandDialog().register() - On command select, call back to server to execute plugin callback
- Subscribe to
-
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:
- Validate the use case - Is this something other plugin authors would benefit from?
- Get feedback on the API design - Does the proposed
commandhook interface make sense? - 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