revert: restore /show /list /switch as slash commands, hide from :help

The colon-prefix unification was not practical for these commands.
Restore original /show, /list, /switch behavior and Telegram handlers.
Remove them from :help output to keep the help text focused on
colon commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seagochen 2026-02-26 17:07:04 +09:00
parent cd0ecff2e2
commit cc3b58d9da
4 changed files with 217 additions and 116 deletions

View file

@ -1151,7 +1151,12 @@ func (al *AgentLoop) estimateTokens(messages []providers.Message) int {
func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage) (string, bool) { func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage) (string, bool) {
content := strings.TrimSpace(msg.Content) content := strings.TrimSpace(msg.Content)
if !strings.HasPrefix(content, ":") { // Handle : prefixed extension commands (work across all channels)
if strings.HasPrefix(content, ":") {
return al.handleExtensionCommand(content)
}
if !strings.HasPrefix(content, "/") {
return "", false return "", false
} }
@ -1163,6 +1168,91 @@ func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage)
cmd := parts[0] cmd := parts[0]
args := parts[1:] args := parts[1:]
switch cmd {
case "/show":
if len(args) < 1 {
return "Usage: /show [model|channel|agents]", true
}
switch args[0] {
case "model":
defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent == nil {
return "No default agent configured", true
}
return fmt.Sprintf("Current model: %s", defaultAgent.Model), true
case "channel":
return fmt.Sprintf("Current channel: %s", msg.Channel), true
case "agents":
agentIDs := al.registry.ListAgentIDs()
return fmt.Sprintf("Registered agents: %s", strings.Join(agentIDs, ", ")), true
default:
return fmt.Sprintf("Unknown show target: %s", args[0]), true
}
case "/list":
if len(args) < 1 {
return "Usage: /list [models|channels|agents]", true
}
switch args[0] {
case "models":
return "Available models: configured in config.json per agent", true
case "channels":
if al.channelManager == nil {
return "Channel manager not initialized", true
}
channels := al.channelManager.GetEnabledChannels()
if len(channels) == 0 {
return "No channels enabled", true
}
return fmt.Sprintf("Enabled channels: %s", strings.Join(channels, ", ")), true
case "agents":
agentIDs := al.registry.ListAgentIDs()
return fmt.Sprintf("Registered agents: %s", strings.Join(agentIDs, ", ")), true
default:
return fmt.Sprintf("Unknown list target: %s", args[0]), true
}
case "/switch":
if len(args) < 3 || args[1] != "to" {
return "Usage: /switch [model|channel] to <name>", true
}
target := args[0]
value := args[2]
switch target {
case "model":
defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent == nil {
return "No default agent configured", true
}
oldModel := defaultAgent.Model
defaultAgent.Model = value
return fmt.Sprintf("Switched model from %s to %s", oldModel, value), true
case "channel":
if al.channelManager == nil {
return "Channel manager not initialized", true
}
if _, exists := al.channelManager.GetChannel(value); !exists && value != "cli" {
return fmt.Sprintf("Channel '%s' not found or not enabled", value), true
}
return fmt.Sprintf("Switched target channel to %s", value), true
default:
return fmt.Sprintf("Unknown switch target: %s", target), true
}
}
return "", false
}
// handleExtensionCommand handles : prefixed commands that work across all channels.
func (al *AgentLoop) handleExtensionCommand(content string) (string, bool) {
parts := strings.Fields(content)
if len(parts) == 0 {
return "", false
}
cmd := parts[0]
switch cmd { switch cmd {
case ":cmd", ":pico", ":hipico", ":edit": case ":cmd", ":pico", ":hipico", ":edit":
// Pass through to processMessage for mode handling (needs sessionKey from routing) // Pass through to processMessage for mode handling (needs sessionKey from routing)
@ -1174,10 +1264,7 @@ func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage)
:cmd - Switch to command mode (execute shell commands) :cmd - Switch to command mode (execute shell commands)
:pico - Switch to chat mode (default, AI conversation) :pico - Switch to chat mode (default, AI conversation)
:hipico <msg> - Ask AI for help (from command mode, one-shot) :hipico <msg> - Ask AI for help (from command mode, one-shot)
:edit <file> - View/edit files (cmd mode) :edit <file> - View/edit files (cmd mode)`, true
:show [model|channel|agents] - Show current configuration
:list [models|channels|agents] - List available options
:switch [model|channel] to <name> - Switch model or channel`, true
case ":usage": case ":usage":
agent := al.registry.GetDefaultAgent() agent := al.registry.GetDefaultAgent()
@ -1204,77 +1291,6 @@ Token usage (this session):
agent.TotalRequests.Load(), agent.TotalRequests.Load(),
), true ), true
case ":show":
if len(args) < 1 {
return "Usage: :show [model|channel|agents]", true
}
switch args[0] {
case "model":
defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent == nil {
return "No default agent configured", true
}
return fmt.Sprintf("Current model: %s", defaultAgent.Model), true
case "channel":
return fmt.Sprintf("Current channel: %s", msg.Channel), true
case "agents":
agentIDs := al.registry.ListAgentIDs()
return fmt.Sprintf("Registered agents: %s", strings.Join(agentIDs, ", ")), true
default:
return fmt.Sprintf("Unknown show target: %s", args[0]), true
}
case ":list":
if len(args) < 1 {
return "Usage: :list [models|channels|agents]", true
}
switch args[0] {
case "models":
return "Available models: configured in config.json per agent", true
case "channels":
if al.channelManager == nil {
return "Channel manager not initialized", true
}
channels := al.channelManager.GetEnabledChannels()
if len(channels) == 0 {
return "No channels enabled", true
}
return fmt.Sprintf("Enabled channels: %s", strings.Join(channels, ", ")), true
case "agents":
agentIDs := al.registry.ListAgentIDs()
return fmt.Sprintf("Registered agents: %s", strings.Join(agentIDs, ", ")), true
default:
return fmt.Sprintf("Unknown list target: %s", args[0]), true
}
case ":switch":
if len(args) < 3 || args[1] != "to" {
return "Usage: :switch [model|channel] to <name>", true
}
target := args[0]
value := args[2]
switch target {
case "model":
defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent == nil {
return "No default agent configured", true
}
oldModel := defaultAgent.Model
defaultAgent.Model = value
return fmt.Sprintf("Switched model from %s to %s", oldModel, value), true
case "channel":
if al.channelManager == nil {
return "Channel manager not initialized", true
}
if _, exists := al.channelManager.GetChannel(value); !exists && value != "cli" {
return fmt.Sprintf("Channel '%s' not found or not enabled", value), true
}
return fmt.Sprintf("Switched target channel to %s", value), true
default:
return fmt.Sprintf("Unknown switch target: %s", target), true
}
default: default:
// Don't intercept unrecognized : prefixed messages (e.g. :) :D :thinking:) // Don't intercept unrecognized : prefixed messages (e.g. :) :D :thinking:)
// Let them pass through as normal chat messages // Let them pass through as normal chat messages

View file

@ -632,9 +632,9 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
} }
} }
// TestHandleCommand_EmojiPassthrough verifies that emoji-like // TestHandleExtensionCommand_EmojiPassthrough verifies that emoji-like
// messages starting with : are not intercepted as commands. // messages starting with : are not intercepted as commands.
func TestHandleCommand_EmojiPassthrough(t *testing.T) { func TestHandleExtensionCommand_EmojiPassthrough(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
Agents: config.AgentsConfig{ Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{ Defaults: config.AgentDefaults{
@ -647,12 +647,10 @@ func TestHandleCommand_EmojiPassthrough(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider)
ctx := context.Background()
emojiInputs := []string{":)", ":D", ":heart:", ":thinking:", ":-)", ":100:"} emojiInputs := []string{":)", ":D", ":heart:", ":thinking:", ":-)", ":100:"}
for _, input := range emojiInputs { for _, input := range emojiInputs {
msg := bus.InboundMessage{Content: input} _, handled := al.handleExtensionCommand(input)
_, handled := al.handleCommand(ctx, msg)
if handled { if handled {
t.Errorf("Expected %q to pass through (not handled), but it was handled", input) t.Errorf("Expected %q to pass through (not handled), but it was handled", input)
} }
@ -661,36 +659,9 @@ func TestHandleCommand_EmojiPassthrough(t *testing.T) {
// Known commands should still be handled // Known commands should still be handled
knownCommands := []string{":help", ":usage"} knownCommands := []string{":help", ":usage"}
for _, cmd := range knownCommands { for _, cmd := range knownCommands {
msg := bus.InboundMessage{Content: cmd} _, handled := al.handleExtensionCommand(cmd)
_, handled := al.handleCommand(ctx, msg)
if !handled { if !handled {
t.Errorf("Expected %q to be handled, but it was not", cmd) t.Errorf("Expected %q to be handled, but it was not", cmd)
} }
} }
// :show, :list, :switch should be handled
showMsg := bus.InboundMessage{Content: ":show model", Channel: "cli"}
resp, handled := al.handleCommand(ctx, showMsg)
if !handled {
t.Error("Expected :show model to be handled")
}
if resp != "Current model: test-model" {
t.Errorf("Unexpected :show model response: %s", resp)
}
listMsg := bus.InboundMessage{Content: ":list agents"}
_, handled = al.handleCommand(ctx, listMsg)
if !handled {
t.Error("Expected :list agents to be handled")
}
// Non-: messages should not be handled
plainInputs := []string{"hello", "/show model", "ls -la"}
for _, input := range plainInputs {
msg := bus.InboundMessage{Content: input}
_, handled := al.handleCommand(ctx, msg)
if handled {
t.Errorf("Expected %q to not be handled, but it was", input)
}
}
} }

View file

@ -76,7 +76,7 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
return &TelegramChannel{ return &TelegramChannel{
BaseChannel: base, BaseChannel: base,
commands: NewTelegramCommands(bot), commands: NewTelegramCommands(bot, cfg),
bot: bot, bot: bot,
config: cfg, config: cfg,
chatIDs: make(map[string]int64), chatIDs: make(map[string]int64),
@ -113,6 +113,14 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
return c.commands.Start(ctx, message) return c.commands.Start(ctx, message)
}, th.CommandEqual("start")) }, th.CommandEqual("start"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.commands.Show(ctx, message)
}, th.CommandEqual("show"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.commands.List(ctx, message)
}, th.CommandEqual("list"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.handleMessage(ctx, &message) return c.handleMessage(ctx, &message)
}, th.AnyMessage()) }, th.AnyMessage())

View file

@ -2,31 +2,46 @@ package channels
import ( import (
"context" "context"
"fmt"
"strings"
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
"github.com/sipeed/picoclaw/pkg/config"
) )
type TelegramCommander interface { type TelegramCommander interface {
Help(ctx context.Context, message telego.Message) error Help(ctx context.Context, message telego.Message) error
Start(ctx context.Context, message telego.Message) error Start(ctx context.Context, message telego.Message) error
Show(ctx context.Context, message telego.Message) error
List(ctx context.Context, message telego.Message) error
} }
type cmd struct { type cmd struct {
bot *telego.Bot bot *telego.Bot
config *config.Config
} }
func NewTelegramCommands(bot *telego.Bot) TelegramCommander { func NewTelegramCommands(bot *telego.Bot, cfg *config.Config) TelegramCommander {
return &cmd{ return &cmd{
bot: bot, bot: bot,
config: cfg,
} }
} }
func commandArgs(text string) string {
parts := strings.SplitN(text, " ", 2)
if len(parts) < 2 {
return ""
}
return strings.TrimSpace(parts[1])
}
func (c *cmd) Help(ctx context.Context, message telego.Message) error { func (c *cmd) Help(ctx context.Context, message telego.Message) error {
msg := `/start - Start the bot msg := `/start - Start the bot
/help - Show this help message /help - Show this help message
:show [model|channel|agents] - Show current configuration /show [model|channel] - Show current configuration
:list [models|channels|agents] - List available options /list [models|channels] - List available options
:switch [model|channel] to <name> - Switch model or channel
` `
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID}, ChatID: telego.ChatID{ID: message.Chat.ID},
@ -48,3 +63,94 @@ func (c *cmd) Start(ctx context.Context, message telego.Message) error {
}) })
return err return err
} }
func (c *cmd) Show(ctx context.Context, message telego.Message) error {
args := commandArgs(message.Text)
if args == "" {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: "Usage: /show [model|channel]",
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
var response string
switch args {
case "model":
response = fmt.Sprintf("Current Model: %s (Provider: %s)",
c.config.Agents.Defaults.GetModelName(),
c.config.Agents.Defaults.Provider)
case "channel":
response = "Current Channel: telegram"
default:
response = fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args)
}
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: response,
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
func (c *cmd) List(ctx context.Context, message telego.Message) error {
args := commandArgs(message.Text)
if args == "" {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: "Usage: /list [models|channels]",
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
var response string
switch args {
case "models":
provider := c.config.Agents.Defaults.Provider
if provider == "" {
provider = "configured default"
}
response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.yaml",
c.config.Agents.Defaults.GetModelName(), provider)
case "channels":
var enabled []string
if c.config.Channels.Telegram.Enabled {
enabled = append(enabled, "telegram")
}
if c.config.Channels.WhatsApp.Enabled {
enabled = append(enabled, "whatsapp")
}
if c.config.Channels.Feishu.Enabled {
enabled = append(enabled, "feishu")
}
if c.config.Channels.Discord.Enabled {
enabled = append(enabled, "discord")
}
if c.config.Channels.Slack.Enabled {
enabled = append(enabled, "slack")
}
response = fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- "))
default:
response = fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args)
}
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: response,
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}