sink_commands.go

  1package acp
  2
  3import (
  4	"log/slog"
  5
  6	"github.com/charmbracelet/catwalk/pkg/catwalk"
  7	"github.com/charmbracelet/crush/internal/agent/hyper"
  8	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/pubsub"
 11	"github.com/charmbracelet/crush/internal/uicmd"
 12	"github.com/coder/acp-go-sdk"
 13)
 14
 15const mcpCommandPrefix = "mcp:"
 16
 17// HandleMCPEvent processes MCP events and republishes commands when prompts
 18// change.
 19func (s *Sink) HandleMCPEvent(event pubsub.Event[mcp.Event]) {
 20	switch event.Payload.Type {
 21	case mcp.EventPromptsListChanged, mcp.EventStateChanged:
 22		s.PublishCommands()
 23	}
 24}
 25
 26// PublishCommands aggregates commands from all sources and sends an
 27// AvailableCommandsUpdate to the ACP client.
 28func (s *Sink) PublishCommands() {
 29	var commands []acp.AvailableCommand
 30
 31	// System/built-in commands.
 32	commands = append(commands, s.builtinCommands()...)
 33
 34	// User and project commands (already prefixed by uicmd).
 35	if userCmds, err := uicmd.LoadCustomCommandsFromConfig(config.Get()); err == nil {
 36		commands = append(commands, translateCommands(userCmds, "")...)
 37	}
 38
 39	// MCP prompts.
 40	mcpCmds := uicmd.LoadMCPPrompts()
 41	commands = append(commands, translateCommands(mcpCmds, mcpCommandPrefix)...)
 42
 43	if err := s.conn.SessionUpdate(s.ctx, acp.SessionNotification{
 44		SessionId: acp.SessionId(s.sessionID),
 45		Update: acp.SessionUpdate{
 46			AvailableCommandsUpdate: &acp.SessionAvailableCommandsUpdate{
 47				AvailableCommands: commands,
 48			},
 49		},
 50	}); err != nil {
 51		slog.Error("Failed to send available commands update", "error", err)
 52	}
 53}
 54
 55// builtinCommands returns ACP-compatible built-in commands.
 56// Commands are dynamically generated based on current model capabilities.
 57func (s *Sink) builtinCommands() []acp.AvailableCommand {
 58	commands := []acp.AvailableCommand{
 59		{
 60			Name:        "summarize",
 61			Description: "Summarize the current session and create a new one with the summary",
 62		},
 63		{
 64			Name:        "toggle_yolo",
 65			Description: "Toggle yolo mode (auto-approve tool calls)",
 66		},
 67	}
 68
 69	// Add reasoning commands based on current model capabilities.
 70	cfg := config.Get()
 71	if cfg == nil {
 72		return commands
 73	}
 74
 75	agentCfg, ok := cfg.Agents[config.AgentCoder]
 76	if !ok {
 77		return commands
 78	}
 79
 80	providerCfg := cfg.GetProviderForModel(agentCfg.Model)
 81	model := cfg.GetModelByType(agentCfg.Model)
 82	if providerCfg == nil || model == nil || !model.CanReason {
 83		return commands
 84	}
 85
 86	// Anthropic/Hyper models: thinking toggle.
 87	if providerCfg.Type == catwalk.TypeAnthropic || providerCfg.Type == catwalk.Type(hyper.Name) {
 88		commands = append(commands, acp.AvailableCommand{
 89			Name:        "toggle_thinking",
 90			Description: "Toggle extended thinking for reasoning-capable models",
 91		})
 92	}
 93
 94	// OpenAI-style models: reasoning effort selection.
 95	if len(model.ReasoningLevels) > 0 {
 96		commands = append(commands, acp.AvailableCommand{
 97			Name:        "set_reasoning_effort",
 98			Description: "Set reasoning effort level (low, medium, high)",
 99			Input: &acp.AvailableCommandInput{
100				UnstructuredCommandInput: &acp.AvailableCommandUnstructuredCommandInput{
101					Hint: "low | medium | high",
102				},
103			},
104		})
105	}
106
107	return commands
108}
109
110// translateCommands converts uicmd.Command slice to acp.AvailableCommand
111// slice, optionally adding a prefix.
112func translateCommands(cmds []uicmd.Command, prefix string) []acp.AvailableCommand {
113	result := make([]acp.AvailableCommand, 0, len(cmds))
114	for _, cmd := range cmds {
115		acpCmd := acp.AvailableCommand{
116			Name:        prefix + cmd.ID,
117			Description: cmd.Description,
118		}
119
120		// If the command has a title different from ID, use it as a hint.
121		if cmd.Title != "" && cmd.Title != cmd.ID {
122			acpCmd.Input = &acp.AvailableCommandInput{
123				UnstructuredCommandInput: &acp.AvailableCommandUnstructuredCommandInput{
124					Hint: cmd.Title,
125				},
126			}
127		}
128
129		result = append(result, acpCmd)
130	}
131	return result
132}