agent.go

145 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
package controllers

import (
	"encoding/json"
	"fmt"
	"net/http"

	"congo.gg/pkg/application"
	"congo.gg/dev/internal"
	"congo.gg/dev/models"
)

func Agent() (string, *AgentController) {
	return "agent", &AgentController{}
}

type AgentController struct {
	application.BaseController
}

func (c *AgentController) Setup(app *application.App) {
	c.BaseController.Setup(app)

	http.Handle("POST /agent/chat", app.Method(c, "Chat", RequireAuth()))
	http.Handle("GET /agent/conversations", app.Method(c, "ListConversations", RequireAuth()))
	http.Handle("GET /agent/conversations/{id}", app.Method(c, "LoadConversation", RequireAuth()))
	http.Handle("POST /agent/conversations/new", app.Method(c, "NewConversation", RequireAuth()))
}

func (c AgentController) Handle(r *http.Request) application.Controller {
	c.Request = r
	return &c
}

// Template methods

func (c *AgentController) Conversations() []*models.Conversation {
	convs, _ := models.Conversations.Search("ORDER BY UpdatedAt DESC LIMIT 20")
	return convs
}

func (c *AgentController) HasAgent() bool {
	return internal.IsAgentAvailable()
}

// Handlers

func (c *AgentController) Chat(w http.ResponseWriter, r *http.Request) {
	message := r.FormValue("message")
	convID := r.FormValue("conversation_id")

	if message == "" {
		c.RenderError(w, r, fmt.Errorf("message is required"))
		return
	}

	if !internal.IsAgentAvailable() {
		c.RenderError(w, r, fmt.Errorf("Claude Code not authenticated — run 'claude' in the IDE terminal to sign in"))
		return
	}

	// Load or create conversation
	var conv *models.Conversation
	if convID != "" {
		conv, _ = models.Conversations.Get(convID)
	}
	if conv == nil {
		conv = &models.Conversation{
			Title: truncate(message, 50),
		}
		id, _ := models.Conversations.Insert(conv)
		conv, _ = models.Conversations.Get(id)
	}

	// Run claude CLI
	response, err := internal.RunAgentChat(message)
	if err != nil {
		c.RenderError(w, r, fmt.Errorf("agent error: %v", err))
		return
	}

	// Save conversation history
	type chatMsg struct {
		Role    string `json:"role"`
		Content string `json:"content"`
	}
	var history []chatMsg
	if conv.Messages != "" {
		json.Unmarshal([]byte(conv.Messages), &history)
	}
	history = append(history, chatMsg{Role: "user", Content: message})
	history = append(history, chatMsg{Role: "assistant", Content: response})
	historyJSON, _ := json.Marshal(history)
	conv.Messages = string(historyJSON)
	models.Conversations.Update(conv)

	// Render response
	type chatResponse struct {
		Response       string
		ConversationID string
	}
	c.Render(w, r, "agent-message.html", chatResponse{
		Response:       response,
		ConversationID: conv.ID,
	})
}

func (c *AgentController) ListConversations(w http.ResponseWriter, r *http.Request) {
	convs, _ := models.Conversations.Search("ORDER BY UpdatedAt DESC LIMIT 20")
	c.Render(w, r, "agent-conversations.html", convs)
}

func (c *AgentController) LoadConversation(w http.ResponseWriter, r *http.Request) {
	conv, err := models.Conversations.Get(r.PathValue("id"))
	if err != nil {
		c.RenderError(w, r, err)
		return
	}

	type chatMsg struct {
		Role    string `json:"role"`
		Content string `json:"content"`
	}
	var messages []chatMsg
	if conv.Messages != "" {
		json.Unmarshal([]byte(conv.Messages), &messages)
	}

	type convData struct {
		Conversation *models.Conversation
		Messages     []chatMsg
	}
	c.Render(w, r, "agent-history.html", convData{conv, messages})
}

func (c *AgentController) NewConversation(w http.ResponseWriter, r *http.Request) {
	c.Redirect(w, r, "/")
}

func truncate(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen-3] + "..."
}