Skip to content

Conversation

@shuv1337
Copy link
Collaborator

@shuv1337 shuv1337 commented Dec 27, 2025

Summary

  • Enable server-side plugin slash commands without client changes
  • Enforce session-only commands in the command execution path
  • Add alias resolution and error reporting for plugin commands
  • Regenerate SDK command types and document the rollout plan

Plugin Command Example

Here's a complete example plugin that registers custom slash commands:

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

export const TestCommandsPlugin: Plugin = async (ctx) => {
  return {
    "plugin.command": {
      hello: {
        description: "Say hello from plugin",
        aliases: ["hi"],
        sessionOnly: false,
        async execute({ arguments: args, client }) {
          await client.tui.publish({
            body: {
              type: "tui.toast.show",
              properties: {
                title: "Hello!",
                message: `You said: ${args || "(nothing)"}`,
                variant: "success",
              },
            },
          });
        },
      },
      global: {
        description: "A global command (works without session)",
        aliases: ["glob"],
        sessionOnly: false,
        async execute({ client }) {
          await client.tui.publish({
            body: {
              type: "tui.toast.show",
              properties: {
                title: "Global Command",
                message: "This command works globally!",
                variant: "info",
              },
            },
          });
        },
      },
      error: {
        description: "A command that throws an error",
        aliases: ["err"],
        sessionOnly: false,
        async execute() {
          throw new Error("This is a test error from the plugin command");
        },
      },
      toast: {
        description: "Show a toast notification",
        aliases: ["notify"],
        sessionOnly: false,
        async execute({ client }) {
          await client.tui.publish({
            body: {
              type: "tui.toast.show",
              properties: {
                title: "Plugin Toast",
                message: "Hello from the test plugin!",
                variant: "success",
              },
            },
          });
        },
      },
    },
  };
};

Features

Feature Description
description Shows in command autocomplete
aliases Alternative names (e.g., /hi/hello)
sessionOnly When true, command requires an existing session
execute Handler receives { sessionID, arguments, client }

Test Coverage

  • Alias resolution: /hi resolves to hello via Command.get
  • Execution: /hello world calls the plugin execute handler with arguments
  • Error handling: Failed commands publish Session.Event.Error
  • Session-only guard: Commands with sessionOnly: true reject when session doesn't exist

Greptile Summary

This PR implements server-side plugin slash command execution without requiring client changes. Plugin commands are loaded into the command registry with support for aliases and sessionOnly enforcement.

Key changes:

  • Added plugin.command hook type to enable custom slash commands via plugins
  • Extended command schema to support plugin commands, aliases, and sessionOnly fields
  • Implemented alias resolution in Command.get() to resolve command aliases server-side
  • Added plugin command execution path in SessionPrompt.command() with error handling via Session.Event.Error
  • Added sessionOnly enforcement to block commands when session doesn't exist
  • Comprehensive test coverage for alias resolution, execution, error handling, and sessionOnly guard

Issues found:

  • Event emission logic has a bug that may incorrectly fire Command.Event.Executed when the plugin didn't create a message (see inline comment)

Confidence Score: 4/5

  • This PR is generally safe to merge with one logical bug that needs attention
  • The implementation is well-structured with comprehensive tests, proper error handling, and follows the codebase patterns. However, there's a logical bug in the event emission detection that could cause incorrect behavior in production
  • Pay close attention to packages/opencode/src/session/prompt.ts - the event emission logic needs to be fixed before merge

Important Files Changed

Filename Overview
packages/plugin/src/index.ts Added plugin.command hook type definition to enable custom slash commands
packages/opencode/src/command/index.ts Extended command schema with plugin type, aliases, and sessionOnly fields; added plugin command loading and alias resolution
packages/opencode/src/plugin/index.ts Added client() export, type guard for plugin functions, and excluded plugin.command from trigger mechanism
packages/opencode/src/session/prompt.ts Implemented plugin command execution with sessionOnly enforcement, error handling, and conditional event emission; potential issue with message detection timing
packages/opencode/test/command/plugin-commands.test.ts Added comprehensive test coverage for alias resolution, command execution, error handling, and sessionOnly enforcement

Sequence Diagram

sequenceDiagram
    participant Client as Client (TUI/Web)
    participant API as session.command API
    participant Command as Command.get()
    participant Plugin as Plugin.list()
    participant Hook as plugin.command.execute()
    participant Session as Session
    participant Bus as Event Bus

    Client->>API: /hello world
    API->>Command: Command.get("hello")
    Command->>Plugin: Load plugin commands
    Plugin-->>Command: Plugin commands + aliases
    Command-->>API: Resolve alias → canonical name
    
    alt sessionOnly command
        API->>Session: Session.get(sessionID)
        alt session not found
            Session-->>API: Error
            API->>Bus: Publish Session.Event.Error
            API-->>Client: Throw error
        end
    end
    
    alt plugin command
        API->>Plugin: Plugin.list()
        Plugin-->>API: Plugin hooks
        API->>Hook: execute({ sessionID, arguments, client })
        
        alt command succeeds
            Hook->>Session: Create message (optional)
            Hook-->>API: Success
            API->>Session: Session.messages(limit: 1)
            Session-->>API: Last message
            alt message was created
                API->>Bus: Publish Command.Event.Executed
            end
            API-->>Client: Return message
        else command fails
            Hook-->>API: Throw error
            API->>Bus: Publish Session.Event.Error
            API-->>Client: Throw error
        end
    else template command
        API->>Session: Execute template command
        Session-->>API: Result
        API-->>Client: Return result
    end
Loading

@shuv1337 shuv1337 changed the base branch from integration to dev December 27, 2025 17:26
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 1337 to 1347
// Emit event if plugin created a message
const last = await Session.messages({ sessionID: input.sessionID, limit: 1 })
if (last.length > 0) {
Bus.publish(Command.Event.Executed, {
name: command.name,
sessionID: input.sessionID,
arguments: input.arguments,
messageID: last[0].info.id,
})
return last[0]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: event emission may incorrectly trigger if session already has messages

the logic Session.messages({ sessionID, limit: 1 }) returns the most recent message in the session, but doesn't verify it was created by this plugin command execution. if the session already had messages before this command ran, the check last.length > 0 will be true even if the plugin didn't create any new message.

to fix, capture the message count or last message ID before execution, then compare after:

// Before execution
const messagesBefore = await Session.messages({ sessionID: input.sessionID, limit: 1 })
const lastMessageIDBefore = messagesBefore[0]?.info.id

// After execution
const messagesAfter = await Session.messages({ sessionID: input.sessionID, limit: 1 })
if (messagesAfter.length > 0 && messagesAfter[0].info.id !== lastMessageIDBefore) {
  // New message was created
  Bus.publish(Command.Event.Executed, { ... })
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/opencode/src/session/prompt.ts
Line: 1337:1347

Comment:
**logic:** event emission may incorrectly trigger if session already has messages

the logic `Session.messages({ sessionID, limit: 1 })` returns the most recent message in the session, but doesn't verify it was created by this plugin command execution. if the session already had messages before this command ran, the check `last.length > 0` will be true even if the plugin didn't create any new message.

to fix, capture the message count or last message ID before execution, then compare after:
```typescript
// Before execution
const messagesBefore = await Session.messages({ sessionID: input.sessionID, limit: 1 })
const lastMessageIDBefore = messagesBefore[0]?.info.id

// After execution
const messagesAfter = await Session.messages({ sessionID: input.sessionID, limit: 1 })
if (messagesAfter.length > 0 && messagesAfter[0].info.id !== lastMessageIDBefore) {
  // New message was created
  Bus.publish(Command.Event.Executed, { ... })
}
```

How can I resolve this? If you propose a fix, please make it concise.

@shuv1337 shuv1337 closed this Dec 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants