diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 2129662d7..3723e230d 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -592,6 +592,9 @@ func gatewayCmd() { os.Exit(1) } + // Inject channel manager into agent loop for command handling + agentLoop.SetChannelManager(channelManager) + var transcriber *voice.GroqTranscriber if cfg.Providers.Groq.APIKey != "" { transcriber = voice.NewGroqTranscriber(cfg.Providers.Groq.APIKey) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 73e837119..8c7bcf4f7 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -18,6 +18,7 @@ import ( "time" "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/constants" "github.com/sipeed/picoclaw/pkg/logger" @@ -41,6 +42,7 @@ type AgentLoop struct { tools *tools.ToolRegistry running atomic.Bool summarizing sync.Map // Tracks which sessions are currently being summarized + channelManager *channels.Manager } // processOptions configures how a message is processed @@ -198,6 +200,10 @@ func (al *AgentLoop) RegisterTool(tool tools.Tool) { al.tools.Register(tool) } +func (al *AgentLoop) SetChannelManager(cm *channels.Manager) { + al.channelManager = cm +} + // RecordLastChannel records the last active channel for this workspace. // This uses the atomic state save mechanism to prevent data loss on crash. func (al *AgentLoop) RecordLastChannel(channel string) error { @@ -262,6 +268,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) return al.processSystemMessage(ctx, msg) } + // Check for commands + if response, handled := al.handleCommand(ctx, msg); handled { + return response, nil + } + // Process as user message return al.runAgentLoop(ctx, processOptions{ SessionKey: msg.SessionKey, @@ -775,3 +786,86 @@ func (al *AgentLoop) estimateTokens(messages []providers.Message) int { } return total } + +func (al *AgentLoop) handleCommand(ctx context.Context, msg bus.InboundMessage) (string, bool) { + content := strings.TrimSpace(msg.Content) + if !strings.HasPrefix(content, "/") { + return "", false + } + + parts := strings.Fields(content) + if len(parts) == 0 { + return "", false + } + + cmd := parts[0] + args := parts[1:] + + switch cmd { + case "/show": + if len(args) < 1 { + return "Usage: /show [model|channel]", true + } + switch args[0] { + case "model": + return fmt.Sprintf("Current model: %s", al.model), true + case "channel": + return fmt.Sprintf("Current channel: %s", msg.Channel), true + default: + return fmt.Sprintf("Unknown show target: %s", args[0]), true + } + + case "/list": + if len(args) < 1 { + return "Usage: /list [models|channels]", true + } + switch args[0] { + case "models": + // TODO: Fetch available models dynamically if possible + return "Available models: glm-4.7, claude-3-5-sonnet, gpt-4o (configured in config.json/env)", 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 + 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 ", true + } + target := args[0] + value := args[2] + + switch target { + case "model": + oldModel := al.model + al.model = value + return fmt.Sprintf("Switched model from %s to %s", oldModel, value), true + case "channel": + // This changes the 'default' channel for some operations, or effectively redirects output? + // For now, let's just validate if the channel exists + 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 + } + + // If message came from CLI, maybe we want to redirect CLI output to this channel? + // That would require state persistence about "redirected channel" + // For now, just acknowledged. + return fmt.Sprintf("Switched target channel to %s (Note: this currently only validates existence)", value), true + default: + return fmt.Sprintf("Unknown switch target: %s", target), true + } + } + + return "", false +}