- Remove unnecessary comments and reset lines in chat.go for edit and scaffold tools. - Ignore return values from fmt.Scanln in edit.go and scaffold.go to handle potential errors. - Delete unused edit_helper_test.go file.
223 lines
6.1 KiB
Go
223 lines
6.1 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 != "" {
|
|
editInstruction = tc.Instruction
|
|
editCmd.SetArgs([]string{tc.File})
|
|
_ = editCmd.Execute()
|
|
}
|
|
case "scaffold":
|
|
if tc.Path != "" && tc.Description != "" {
|
|
scaffoldDescription = tc.Description
|
|
scaffoldCmd.SetArgs([]string{tc.Path})
|
|
_ = scaffoldCmd.Execute()
|
|
}
|
|
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
|
|
)
|