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}