refactor(acp): dynamic command list based on model

Amolith created

- Remove impossible commands (new_session, switch_session)
- Remove client-handled commands (switch_model, help)
- Remove system: prefix from command names
- Add toggle_thinking only for Anthropic/Hyper models
- Add set_reasoning_effort only for models with ReasoningLevels

Assisted-by: Claude Sonnet 4 via Crush

Change summary

internal/acp/sink_commands.go | 74 +++++++++++++++++++++++-------------
1 file changed, 47 insertions(+), 27 deletions(-)

Detailed changes

internal/acp/sink_commands.go 🔗

@@ -3,6 +3,8 @@ package acp
 import (
 	"log/slog"
 
+	"github.com/charmbracelet/catwalk/pkg/catwalk"
+	"github.com/charmbracelet/crush/internal/agent/hyper"
 	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/pubsub"
@@ -10,10 +12,7 @@ import (
 	"github.com/coder/acp-go-sdk"
 )
 
-const (
-	systemCommandPrefix = "system:"
-	mcpCommandPrefix    = "mcp:"
-)
+const mcpCommandPrefix = "mcp:"
 
 // HandleMCPEvent processes MCP events and republishes commands when prompts
 // change.
@@ -54,37 +53,58 @@ func (s *Sink) PublishCommands() {
 }
 
 // builtinCommands returns ACP-compatible built-in commands.
+// Commands are dynamically generated based on current model capabilities.
 func (s *Sink) builtinCommands() []acp.AvailableCommand {
-	return []acp.AvailableCommand{
-		{
-			Name:        systemCommandPrefix + "new_session",
-			Description: "Start a new session",
-		},
-		{
-			Name:        systemCommandPrefix + "switch_session",
-			Description: "Switch to a different session",
-		},
-		{
-			Name:        systemCommandPrefix + "switch_model",
-			Description: "Switch to a different model",
-		},
+	commands := []acp.AvailableCommand{
 		{
-			Name:        systemCommandPrefix + "summarize",
+			Name:        "summarize",
 			Description: "Summarize the current session and create a new one with the summary",
 		},
 		{
-			Name:        systemCommandPrefix + "toggle_thinking",
-			Description: "Toggle model thinking for reasoning-capable models",
-		},
-		{
-			Name:        systemCommandPrefix + "toggle_yolo",
+			Name:        "toggle_yolo",
 			Description: "Toggle yolo mode (auto-approve tool calls)",
 		},
-		{
-			Name:        systemCommandPrefix + "help",
-			Description: "Show available commands and shortcuts",
-		},
 	}
+
+	// Add reasoning commands based on current model capabilities.
+	cfg := config.Get()
+	if cfg == nil {
+		return commands
+	}
+
+	agentCfg, ok := cfg.Agents[config.AgentCoder]
+	if !ok {
+		return commands
+	}
+
+	providerCfg := cfg.GetProviderForModel(agentCfg.Model)
+	model := cfg.GetModelByType(agentCfg.Model)
+	if providerCfg == nil || model == nil || !model.CanReason {
+		return commands
+	}
+
+	// Anthropic/Hyper models: thinking toggle.
+	if providerCfg.Type == catwalk.TypeAnthropic || providerCfg.Type == catwalk.Type(hyper.Name) {
+		commands = append(commands, acp.AvailableCommand{
+			Name:        "toggle_thinking",
+			Description: "Toggle extended thinking for reasoning-capable models",
+		})
+	}
+
+	// OpenAI-style models: reasoning effort selection.
+	if len(model.ReasoningLevels) > 0 {
+		commands = append(commands, acp.AvailableCommand{
+			Name:        "set_reasoning_effort",
+			Description: "Set reasoning effort level (low, medium, high)",
+			Input: &acp.AvailableCommandInput{
+				UnstructuredCommandInput: &acp.AvailableCommandUnstructuredCommandInput{
+					Hint: "low | medium | high",
+				},
+			},
+		})
+	}
+
+	return commands
 }
 
 // translateCommands converts uicmd.Command slice to acp.AvailableCommand