Refactor edit and scaffold commands to support invocation from chat --agent mode by using temporary global variables for passing instructions/descriptions. Add interactive prompts for normal usage, preview diffs, and confirmation before writing files. Update chat handler to set these variables and reset after execution.
226 lines
6.2 KiB
Go
226 lines
6.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"gmgauthier.com/grokkit/config"
|
|
"gmgauthier.com/grokkit/internal/git"
|
|
"gmgauthier.com/grokkit/internal/grok"
|
|
)
|
|
|
|
type ChatHistory struct {
|
|
Messages []map[string]string `json:"messages"`
|
|
}
|
|
|
|
type ToolCall struct {
|
|
Tool string `json:"tool"`
|
|
File string `json:"file,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Instruction string `json:"instruction,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
func loadChatHistory() []map[string]string {
|
|
histFile := getChatHistoryFile()
|
|
data, err := os.ReadFile(histFile)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var hist ChatHistory
|
|
if err := json.Unmarshal(data, &hist); err != nil {
|
|
return nil
|
|
}
|
|
return hist.Messages
|
|
}
|
|
|
|
func saveChatHistory(messages []map[string]string) error {
|
|
histFile := getChatHistoryFile()
|
|
hist := ChatHistory{Messages: messages}
|
|
data, err := json.MarshalIndent(hist, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(histFile, data, 0644)
|
|
}
|
|
|
|
func getChatHistoryFile() string {
|
|
configFile := viper.GetString("chat.history_file")
|
|
if configFile != "" {
|
|
return configFile
|
|
}
|
|
home, _ := os.UserHomeDir()
|
|
if home == "" {
|
|
home = "."
|
|
}
|
|
histDir := filepath.Join(home, ".config", "grokkit")
|
|
_ = os.MkdirAll(histDir, 0755)
|
|
return filepath.Join(histDir, "chat_history.json")
|
|
}
|
|
|
|
var chatCmd = &cobra.Command{
|
|
Use: "chat",
|
|
Short: "Interactive chat with Grok (use --agent for tool-enabled mode)",
|
|
Long: `Start a persistent conversation with Grok.
|
|
|
|
Normal mode: coherent, reasoning-focused chat (uses full model).
|
|
Agent mode (--agent): Grok can call tools with safety previews (uses fast model).`,
|
|
Run: runChat,
|
|
}
|
|
|
|
func init() {
|
|
chatCmd.Flags().Bool("agent", false, "Enable agent mode with tool calling (uses fast non-reasoning model)")
|
|
chatCmd.Flags().String("model", "", "Override model")
|
|
rootCmd.AddCommand(chatCmd)
|
|
}
|
|
|
|
func runChat(cmd *cobra.Command, args []string) {
|
|
agentMode, _ := cmd.Flags().GetBool("agent")
|
|
modelFlag, _ := cmd.Flags().GetString("model")
|
|
|
|
var model string
|
|
if modelFlag != "" {
|
|
model = modelFlag
|
|
} else if agentMode {
|
|
model = config.GetModel("chat-agent", "")
|
|
} else {
|
|
model = config.GetModel("chat", "")
|
|
}
|
|
|
|
client := grok.NewClient()
|
|
|
|
systemPrompt := map[string]string{
|
|
"role": "system",
|
|
"content": fmt.Sprintf("You are Grok 4, the latest and most powerful model from xAI (2026). You are currently running as `%s`. Be helpful, truthful, and a little irreverent.", model),
|
|
}
|
|
|
|
if agentMode {
|
|
systemPrompt["content"] = "You are Grok in Agent Mode.\n" +
|
|
"You can call tools using this exact JSON format inside ```tool blocks:\n\n" +
|
|
"```tool\n" +
|
|
"{\"tool\": \"edit\", \"file\": \"main.go\", \"instruction\": \"Add error handling\"}\n" +
|
|
"```\n\n" +
|
|
"Available tools: edit, scaffold, testgen, lint, commit.\n\n" +
|
|
"Always use tools when the user asks you to change code, generate tests, lint, or commit.\n" +
|
|
"After every tool call, wait for the result before continuing.\n" +
|
|
"Be concise and action-oriented."
|
|
}
|
|
|
|
history := loadChatHistory()
|
|
if len(history) == 0 {
|
|
history = []map[string]string{systemPrompt}
|
|
} else if history[0]["role"] != "system" {
|
|
history = append([]map[string]string{systemPrompt}, history...)
|
|
} else {
|
|
history[0] = systemPrompt
|
|
}
|
|
|
|
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
|
|
color.Cyan("│ Grokkit Chat %s — Model: %s │", map[bool]string{true: "(Agent Mode)", false: ""}[agentMode], model)
|
|
color.Cyan("│ Type /quit to exit │")
|
|
color.Cyan("└──────────────────────────────────────────────────────────────┘\n")
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
for {
|
|
color.Yellow("You > ")
|
|
if !scanner.Scan() {
|
|
break
|
|
}
|
|
|
|
input := strings.TrimSpace(scanner.Text())
|
|
if input == "" {
|
|
continue
|
|
}
|
|
if input == "/quit" || input == "/q" || input == "exit" {
|
|
color.Cyan("\nGoodbye 👋\n")
|
|
break
|
|
}
|
|
|
|
history = append(history, map[string]string{"role": "user", "content": input})
|
|
|
|
color.Green("Grok > ")
|
|
reply := client.Stream(history, model)
|
|
|
|
if agentMode && strings.Contains(reply, "```tool") {
|
|
handleToolCall(reply, &history)
|
|
continue
|
|
}
|
|
|
|
history = append(history, map[string]string{"role": "assistant", "content": reply})
|
|
_ = saveChatHistory(history)
|
|
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func handleToolCall(reply string, history *[]map[string]string) {
|
|
start := strings.Index(reply, "```tool")
|
|
if start == -1 {
|
|
return
|
|
}
|
|
end := strings.Index(reply[start+7:], "```")
|
|
if end == -1 {
|
|
return
|
|
}
|
|
block := strings.TrimSpace(reply[start+7 : start+7+end])
|
|
|
|
var tc ToolCall
|
|
if err := json.Unmarshal([]byte(block), &tc); err != nil {
|
|
color.Red("Failed to parse tool call: %v", err)
|
|
return
|
|
}
|
|
|
|
color.Yellow("\n[Agent] Grok wants to call tool: %s", tc.Tool)
|
|
|
|
switch tc.Tool {
|
|
case "edit":
|
|
if tc.File != "" && tc.Instruction != "" {
|
|
// Pass instruction directly to edit command
|
|
editInstruction = tc.Instruction
|
|
editCmd.SetArgs([]string{tc.File})
|
|
_ = editCmd.Execute()
|
|
editInstruction = "" // reset
|
|
}
|
|
case "scaffold":
|
|
if tc.Path != "" && tc.Description != "" {
|
|
scaffoldDescription = tc.Description
|
|
scaffoldCmd.SetArgs([]string{tc.Path})
|
|
_ = scaffoldCmd.Execute()
|
|
scaffoldDescription = ""
|
|
}
|
|
case "testgen":
|
|
if tc.File != "" {
|
|
testgenCmd.SetArgs([]string{tc.File})
|
|
_ = testgenCmd.Execute()
|
|
}
|
|
case "lint":
|
|
if tc.File != "" {
|
|
lintCmd.SetArgs([]string{tc.File})
|
|
_ = lintCmd.Execute()
|
|
}
|
|
case "commit":
|
|
if tc.Message != "" {
|
|
_, _ = git.Run([]string{"commit", "-m", tc.Message})
|
|
}
|
|
default:
|
|
color.Red("Unknown tool: %s", tc.Tool)
|
|
}
|
|
|
|
*history = append(*history, map[string]string{"role": "assistant", "content": reply})
|
|
}
|
|
|
|
// Temporary variables used by agent mode to pass instructions to existing commands
|
|
var (
|
|
editInstruction string
|
|
scaffoldDescription string
|
|
)
|