feat: add command mode with /cmd, /pico, /hipico, /byepico commands

Add three interactive modes to the CLI agent:
- /cmd: switch to command mode for direct shell command execution
- /pico: switch back to AI chat mode
- /hipico <msg>: invoke AI assistance from within command mode
- /byepico: end AI assistance and return to command mode

Command mode supports cd with directory tracking, cross-platform
shell execution (sh on Unix, PowerShell on Windows), and contextual
AI assistance that knows the current working directory.

https://claude.ai/code/session_01PoHBfe7eKmhHvVF12W3xZ2
This commit is contained in:
Claude 2026-02-24 03:31:09 +00:00
parent 7cbfa89a96
commit 49237b3b59
No known key found for this signature in database

View file

@ -5,11 +5,14 @@ package main
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"github.com/chzyer/readline" "github.com/chzyer/readline"
@ -20,6 +23,20 @@ import (
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
) )
// Interactive mode identifiers
const (
modePico = "pico" // Chat mode (default) - input goes to AI agent
modeCmd = "cmd" // Command mode - input executed as shell commands
modeHiPico = "hipico" // AI-assisted mode within cmd - multi-turn AI conversation
)
// cmdWorkingDir tracks the current working directory for command mode.
var cmdWorkingDir string
func init() {
cmdWorkingDir, _ = os.Getwd()
}
func agentCmd() { func agentCmd() {
message := "" message := ""
sessionKey := "cli:default" sessionKey := "cli:default"
@ -30,7 +47,7 @@ func agentCmd() {
switch args[i] { switch args[i] {
case "--debug", "-d": case "--debug", "-d":
logger.SetLevel(logger.DEBUG) logger.SetLevel(logger.DEBUG)
fmt.Println("🔍 Debug mode enabled") fmt.Println("Debug mode enabled")
case "-m", "--message": case "-m", "--message":
if i+1 < len(args) { if i+1 < len(args) {
message = args[i+1] message = args[i+1]
@ -90,16 +107,25 @@ func agentCmd() {
} }
fmt.Printf("\n%s %s\n", logo, response) fmt.Printf("\n%s %s\n", logo, response)
} else { } else {
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo) fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n", logo)
fmt.Println(" /cmd - switch to command mode")
fmt.Println(" /pico - switch to chat mode")
fmt.Println(" /hipico - AI assistance in command mode")
fmt.Println(" /byepico - end AI assistance")
fmt.Println()
interactiveMode(agentLoop, sessionKey) interactiveMode(agentLoop, sessionKey)
} }
} }
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
prompt := fmt.Sprintf("%s You: ", logo) chatPrompt := fmt.Sprintf("%s You: ", logo)
cmdPrompt := "$ "
hipicoPrompt := fmt.Sprintf("%s> ", logo)
mode := modePico
rl, err := readline.NewEx(&readline.Config{ rl, err := readline.NewEx(&readline.Config{
Prompt: prompt, Prompt: chatPrompt,
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"), HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
HistoryLimit: 100, HistoryLimit: 100,
InterruptPrompt: "^C", InterruptPrompt: "^C",
@ -113,6 +139,8 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
} }
defer rl.Close() defer rl.Close()
hipicoSessionKey := "cli:hipico"
for { for {
line, err := rl.Readline() line, err := rl.Readline()
if err != nil { if err != nil {
@ -134,21 +162,101 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
return return
} }
switch mode {
case modePico:
if input == "/cmd" {
mode = modeCmd
rl.SetPrompt(cmdPrompt)
fmt.Println("Switched to command mode. Type /pico to return to chat.")
continue
}
ctx := context.Background() ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey) response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil { if err != nil {
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
continue continue
} }
fmt.Printf("\n%s %s\n\n", logo, response) fmt.Printf("\n%s %s\n\n", logo, response)
case modeCmd:
if input == "/pico" {
mode = modePico
rl.SetPrompt(chatPrompt)
fmt.Println("Switched to chat mode. Type /cmd to return to command mode.")
continue
}
if strings.HasPrefix(input, "/hipico") {
initialMsg := strings.TrimSpace(strings.TrimPrefix(input, "/hipico"))
if initialMsg == "" {
fmt.Println("Usage: /hipico <message>")
fmt.Println("Example: /hipico check the log files for error messages")
continue
}
mode = modeHiPico
rl.SetPrompt(hipicoPrompt)
contextPrefix := fmt.Sprintf("[Command mode context: working directory is %s]\n\n", cmdWorkingDir)
fmt.Printf("\n%s AI assistance started. Type /byepico to end.\n\n", logo)
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, contextPrefix+initialMsg, hipicoSessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
mode = modeCmd
rl.SetPrompt(cmdPrompt)
continue
}
fmt.Printf("%s %s\n\n", logo, response)
continue
}
executeShellCommand(input)
case modeHiPico:
if input == "/byepico" {
mode = modeCmd
rl.SetPrompt(cmdPrompt)
fmt.Println("AI assistance ended. Back to command mode.")
continue
}
if input == "/pico" {
mode = modePico
rl.SetPrompt(chatPrompt)
fmt.Println("AI assistance ended. Switched to chat mode.")
continue
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, hipicoSessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", logo, response)
}
} }
} }
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
mode := modePico
hipicoSessionKey := "cli:hipico"
for { for {
switch mode {
case modePico:
fmt.Printf("%s You: ", logo) fmt.Printf("%s You: ", logo)
case modeCmd:
fmt.Print("$ ")
case modeHiPico:
fmt.Printf("%s> ", logo)
}
line, err := reader.ReadString('\n') line, err := reader.ReadString('\n')
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
@ -169,13 +277,169 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
return return
} }
switch mode {
case modePico:
if input == "/cmd" {
mode = modeCmd
fmt.Println("Switched to command mode. Type /pico to return to chat.")
continue
}
ctx := context.Background() ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey) response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil { if err != nil {
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
continue continue
} }
fmt.Printf("\n%s %s\n\n", logo, response)
case modeCmd:
if input == "/pico" {
mode = modePico
fmt.Println("Switched to chat mode. Type /cmd to return to command mode.")
continue
}
if strings.HasPrefix(input, "/hipico") {
initialMsg := strings.TrimSpace(strings.TrimPrefix(input, "/hipico"))
if initialMsg == "" {
fmt.Println("Usage: /hipico <message>")
fmt.Println("Example: /hipico check the log files for error messages")
continue
}
mode = modeHiPico
contextPrefix := fmt.Sprintf("[Command mode context: working directory is %s]\n\n", cmdWorkingDir)
fmt.Printf("\n%s AI assistance started. Type /byepico to end.\n\n", logo)
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, contextPrefix+initialMsg, hipicoSessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
mode = modeCmd
continue
}
fmt.Printf("%s %s\n\n", logo, response)
continue
}
executeShellCommand(input)
case modeHiPico:
if input == "/byepico" {
mode = modeCmd
fmt.Println("AI assistance ended. Back to command mode.")
continue
}
if input == "/pico" {
mode = modePico
fmt.Println("AI assistance ended. Switched to chat mode.")
continue
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, hipicoSessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", logo, response) fmt.Printf("\n%s %s\n\n", logo, response)
} }
} }
}
// executeShellCommand runs a shell command in the current working directory
// and prints the output. It also handles the cd command to change directories.
func executeShellCommand(input string) {
// Handle cd command specially to update working directory
if strings.HasPrefix(input, "cd ") || input == "cd" {
handleCd(input)
return
}
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", input)
} else {
cmd = exec.Command("sh", "-c", input)
}
cmd.Dir = cmdWorkingDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if stdout.Len() > 0 {
fmt.Print(stdout.String())
if !strings.HasSuffix(stdout.String(), "\n") {
fmt.Println()
}
}
if stderr.Len() > 0 {
fmt.Print(stderr.String())
if !strings.HasSuffix(stderr.String(), "\n") {
fmt.Println()
}
}
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
fmt.Printf("Exit code: %d\n", exitErr.ExitCode())
} else {
fmt.Printf("Error: %v\n", err)
}
}
}
// handleCd handles the cd command to change the working directory for command mode.
func handleCd(input string) {
parts := strings.Fields(input)
var target string
if len(parts) < 2 {
home, err := os.UserHomeDir()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
target = home
} else {
target = parts[1]
}
// Handle ~ expansion
if strings.HasPrefix(target, "~") {
home, err := os.UserHomeDir()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if target == "~" {
target = home
} else if len(target) > 1 && target[1] == '/' {
target = filepath.Join(home, target[2:])
}
}
// Handle relative paths
if !filepath.IsAbs(target) {
target = filepath.Join(cmdWorkingDir, target)
}
target = filepath.Clean(target)
info, err := os.Stat(target)
if err != nil {
fmt.Printf("cd: %v\n", err)
return
}
if !info.IsDir() {
fmt.Printf("cd: %s: Not a directory\n", target)
return
}
cmdWorkingDir = target
}