feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard
All checks were successful
Beta Release / beta (push) Successful in 2m24s
All checks were successful
Beta Release / beta (push) Successful in 2m24s
Major changes: - Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version) - Add LSP registry with health checks, auto-install, and editor config generation - Add MCP registry with editor detection, status tracking, and per-editor configuration - Add workflow engine with planner and step execution for automated task chains - Add conversation search, export (Markdown/JSON), and detailed token counting - Add streaming shell chat handler with tool call/result events - Add skill validation, dry-run testing, and export endpoints - Enrich dashboard with Tools/Activity/Status tabs and tool cards grid - Add PRD documentation - Complete i18n for both EN and FR 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -36,6 +37,19 @@ type ConversationStore struct {
|
||||
conv *Conversation
|
||||
}
|
||||
|
||||
type TokenCount struct {
|
||||
total int
|
||||
byRole map[string]int
|
||||
byMessage int
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
ID string `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
func NewConversationStore() *ConversationStore {
|
||||
dir, err := config.ConfigDir()
|
||||
if err != nil {
|
||||
@@ -140,19 +154,109 @@ func (cs *ConversationStore) TrimOld(keepCount int) {
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) ApproxTokenCount() int {
|
||||
return cs.ApproxTokenCountDetailed().total
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) ApproxTokenCountDetailed() TokenCount {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
total := utf8.RuneCountInString(cs.conv.Summary)
|
||||
for _, m := range cs.conv.Messages {
|
||||
total += utf8.RuneCountInString(m.Content)
|
||||
|
||||
result := TokenCount{
|
||||
byRole: make(map[string]int),
|
||||
}
|
||||
return total / charsPerToken
|
||||
|
||||
for _, m := range cs.conv.Messages {
|
||||
count := utf8.RuneCountInString(m.Content) / charsPerToken
|
||||
result.byMessage += count
|
||||
result.byRole[m.Role] += count
|
||||
}
|
||||
|
||||
if cs.conv.Summary != "" {
|
||||
result.total = result.byMessage + utf8.RuneCountInString(cs.conv.Summary)/charsPerToken
|
||||
} else {
|
||||
result.total = result.byMessage
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) NeedsSummarization() bool {
|
||||
return cs.ApproxTokenCount() > summarizeThreshold
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) Search(query string) []SearchResult {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
|
||||
var results []SearchResult
|
||||
queryLower := strings.ToLower(query)
|
||||
|
||||
for _, msg := range cs.conv.Messages {
|
||||
if strings.Contains(strings.ToLower(msg.Content), queryLower) {
|
||||
results = append(results, SearchResult{
|
||||
ID: msg.ID,
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
Time: msg.Time,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) ExportMarkdown() string {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# Conversation Export\n\n")
|
||||
sb.WriteString(fmt.Sprintf("Exporté le: %s\n\n", time.Now().Format(time.RFC3339)))
|
||||
|
||||
if cs.conv.Summary != "" {
|
||||
sb.WriteString("## Résumé\n\n")
|
||||
sb.WriteString(cs.conv.Summary)
|
||||
sb.WriteString("\n\n---\n\n")
|
||||
}
|
||||
|
||||
sb.WriteString("## Messages\n\n")
|
||||
|
||||
for i, msg := range cs.conv.Messages {
|
||||
roleLabel := msg.Role
|
||||
if roleLabel == "user" {
|
||||
roleLabel = "👤 Utilisateur"
|
||||
} else if roleLabel == "assistant" {
|
||||
roleLabel = "🤖 Assistant"
|
||||
} else if roleLabel == "system" {
|
||||
roleLabel = "⚙️ Système"
|
||||
}
|
||||
|
||||
timestamp := ""
|
||||
if msg.Time != "" {
|
||||
if t, err := time.Parse(time.RFC3339, msg.Time); err == nil {
|
||||
timestamp = t.Format("2006-01-02 15:04")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("### [%d] %s (%s)\n\n", i+1, roleLabel, timestamp))
|
||||
sb.WriteString(msg.Content)
|
||||
sb.WriteString("\n\n---\n\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) ExportJSON() string {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
|
||||
data, err := json.MarshalIndent(cs.conv, "", " ")
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func generateMsgID() string {
|
||||
return time.Now().Format("20060102150405.000") + "-" + fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user