From e8f6dc4b4d475eb75c29efec0c8933489b3af0d8 Mon Sep 17 00:00:00 2001 From: Augustin Date: Thu, 23 Apr 2026 20:42:43 +0200 Subject: [PATCH] feat(chat): add auto-summarization with token tracking UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add /summarize command, token usage bar, and summary endpoint. Add JSON tags to config/platform/scanner structs for API serialization. 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush --- internal/api/handlers_chat.go | 20 +++++++- internal/api/server.go | 1 + internal/config/config.go | 88 +++++++++++++++++------------------ internal/platform/platform.go | 12 ++--- internal/scanner/scanner.go | 30 ++++++------ web/src/api/client.js | 1 + web/src/components/Studio.jsx | 51 +++++++++++++++++++- web/src/styles/global.css | 5 ++ 8 files changed, 139 insertions(+), 69 deletions(-) diff --git a/internal/api/handlers_chat.go b/internal/api/handlers_chat.go index aef47ba..58cb135 100644 --- a/internal/api/handlers_chat.go +++ b/internal/api/handlers_chat.go @@ -206,8 +206,11 @@ func (s *Server) handleChatHistory(w http.ResponseWriter, r *http.Request) { } messages := s.convStore.Get() writeJSON(w, map[string]interface{}{ - "messages": messages, - "tokens": s.convStore.ApproxTokenCount(), + "messages": messages, + "tokens": s.convStore.ApproxTokenCount(), + "max_tokens": maxTokensApprox, + "summarize_at": summarizeThreshold, + "summary": s.convStore.GetSummary(), }) } @@ -219,3 +222,16 @@ func (s *Server) handleChatClear(w http.ResponseWriter, r *http.Request) { s.convStore.Clear() writeJSON(w, map[string]string{"status": "ok"}) } + +func (s *Server) handleChatSummarize(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + writeError(w, "POST only", http.StatusMethodNotAllowed) + return + } + s.autoSummarize() + writeJSON(w, map[string]interface{}{ + "status": "ok", + "tokens": s.convStore.ApproxTokenCount(), + "summary": s.convStore.GetSummary(), + }) +} diff --git a/internal/api/server.go b/internal/api/server.go index 57e4dfb..2e6f9fb 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -85,6 +85,7 @@ func (s *Server) routes() { s.mux.HandleFunc("/api/chat", s.handleChat) s.mux.HandleFunc("/api/chat/history", s.handleChatHistory) s.mux.HandleFunc("/api/chat/clear", s.handleChatClear) + s.mux.HandleFunc("/api/chat/summarize", s.handleChatSummarize) s.mux.HandleFunc("/api/tool/call", s.handleToolCall) s.mux.HandleFunc("/api/tools/list", s.handleToolList) s.mux.HandleFunc("/api/shell/chat", s.handleShellChat) diff --git a/internal/config/config.go b/internal/config/config.go index a8b5036..cb26ec0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,66 +12,66 @@ import ( ) type Profile struct { - Name string `yaml:"name"` - Pseudo string `yaml:"pseudo"` - Email string `yaml:"email"` - Languages []string `yaml:"languages"` + Name string `yaml:"name" json:"name"` + Pseudo string `yaml:"pseudo" json:"pseudo"` + Email string `yaml:"email" json:"email"` + Languages []string `yaml:"languages" json:"languages"` Preferences struct { - Editor string `yaml:"editor"` - Shell string `yaml:"shell"` - Theme string `yaml:"theme"` - DefaultAI string `yaml:"default_ai"` - AutoUpdate bool `yaml:"auto_update"` - CheckOnStart bool `yaml:"check_on_start"` - Language string `yaml:"language"` - KeyboardLayout string `yaml:"keyboard_layout"` - } `yaml:"preferences"` + Editor string `yaml:"editor" json:"editor"` + Shell string `yaml:"shell" json:"shell"` + Theme string `yaml:"theme" json:"theme"` + DefaultAI string `yaml:"default_ai" json:"default_ai"` + AutoUpdate bool `yaml:"auto_update" json:"auto_update"` + CheckOnStart bool `yaml:"check_on_start" json:"check_on_start"` + Language string `yaml:"language" json:"language"` + KeyboardLayout string `yaml:"keyboard_layout" json:"keyboard_layout"` + } `yaml:"preferences" json:"preferences"` } type AIProvider struct { - Name string `yaml:"name"` - APIKey string `yaml:"api_key,omitempty"` - BaseURL string `yaml:"base_url,omitempty"` - Model string `yaml:"model"` - Active bool `yaml:"active"` + Name string `yaml:"name" json:"name"` + APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"` + BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"` + Model string `yaml:"model" json:"model"` + Active bool `yaml:"active" json:"active"` } type ToolConfig struct { - Name string `yaml:"name"` - Installed bool `yaml:"installed"` - Version string `yaml:"version"` - AutoUpdate bool `yaml:"auto_update"` + Name string `yaml:"name" json:"name"` + Installed bool `yaml:"installed" json:"installed"` + Version string `yaml:"version" json:"version"` + AutoUpdate bool `yaml:"auto_update" json:"auto_update"` } type SSHConnection struct { - Name string `yaml:"name"` - Host string `yaml:"host"` - Port int `yaml:"port"` - User string `yaml:"user"` - Password string `yaml:"password,omitempty"` - KeyPath string `yaml:"key_path,omitempty"` + Name string `yaml:"name" json:"name"` + Host string `yaml:"host" json:"host"` + Port int `yaml:"port" json:"port"` + User string `yaml:"user" json:"user"` + Password string `yaml:"password,omitempty" json:"password,omitempty"` + KeyPath string `yaml:"key_path,omitempty" json:"key_path,omitempty"` } type MuyueConfig struct { - Version string `yaml:"version"` - Profile Profile `yaml:"profile"` + Version string `yaml:"version" json:"version"` + Profile Profile `yaml:"profile" json:"profile"` AI struct { - Providers []AIProvider `yaml:"providers"` - } `yaml:"ai"` - Tools []ToolConfig `yaml:"tools"` + Providers []AIProvider `yaml:"providers" json:"providers"` + } `yaml:"ai" json:"ai"` + Tools []ToolConfig `yaml:"tools" json:"tools"` BMAD struct { - Installed bool `yaml:"installed"` - Version string `yaml:"version"` - Global bool `yaml:"global"` - } `yaml:"bmad"` + Installed bool `yaml:"installed" json:"installed"` + Version string `yaml:"version" json:"version"` + Global bool `yaml:"global" json:"global"` + } `yaml:"bmad" json:"bmad"` Terminal struct { - CustomPrompt bool `yaml:"custom_prompt"` - PromptTheme string `yaml:"prompt_theme"` - SSH []SSHConnection `yaml:"ssh"` - FontSize int `yaml:"font_size"` - FontFamily string `yaml:"font_family"` - Theme string `yaml:"theme"` - } `yaml:"terminal"` + CustomPrompt bool `yaml:"custom_prompt" json:"custom_prompt"` + PromptTheme string `yaml:"prompt_theme" json:"prompt_theme"` + SSH []SSHConnection `yaml:"ssh" json:"ssh"` + FontSize int `yaml:"font_size" json:"font_size"` + FontFamily string `yaml:"font_family" json:"font_family"` + Theme string `yaml:"theme" json:"theme"` + } `yaml:"terminal" json:"terminal"` } type TerminalTheme struct { diff --git a/internal/platform/platform.go b/internal/platform/platform.go index c34c488..6aa8cb5 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -24,12 +24,12 @@ const ( ) type SystemInfo struct { - OS OS - Arch Arch - IsWSL bool - Shell string - Terminal string - PackageManager string + OS OS `json:"os"` + Arch Arch `json:"arch"` + IsWSL bool `json:"is_wsl"` + Shell string `json:"shell"` + Terminal string `json:"terminal"` + PackageManager string `json:"package_manager"` } func Detect() SystemInfo { diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 9721c7b..9898def 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -14,27 +14,27 @@ import ( ) type ToolStatus struct { - Name string `yaml:"name"` - Installed bool `yaml:"installed"` - Version string `yaml:"version"` - Path string `yaml:"path"` - Latest string `yaml:"latest"` - NeedsUpdate bool `yaml:"needs_update"` - Category string `yaml:"category"` + Name string `yaml:"name" json:"name"` + Installed bool `yaml:"installed" json:"installed"` + Version string `yaml:"version" json:"version"` + Path string `yaml:"path" json:"path"` + Latest string `yaml:"latest" json:"latest"` + NeedsUpdate bool `yaml:"needs_update" json:"needs_update"` + Category string `yaml:"category" json:"category"` } type RuntimeStatus struct { - Name string `yaml:"name"` - Installed bool `yaml:"installed"` - Version string `yaml:"version"` + Name string `yaml:"name" json:"name"` + Installed bool `yaml:"installed" json:"installed"` + Version string `yaml:"version" json:"version"` } type ScanResult struct { - System platform.SystemInfo `yaml:"system"` - Tools []ToolStatus `yaml:"tools"` - Runtimes []RuntimeStatus `yaml:"runtimes"` - ShellSetup bool `yaml:"shell_setup"` - GitConfigured bool `yaml:"git_configured"` + System platform.SystemInfo `yaml:"system" json:"system"` + Tools []ToolStatus `yaml:"tools" json:"tools"` + Runtimes []RuntimeStatus `yaml:"runtimes" json:"runtimes"` + ShellSetup bool `yaml:"shell_setup" json:"shell_setup"` + GitConfigured bool `yaml:"git_configured" json:"git_configured"` } var ( diff --git a/web/src/api/client.js b/web/src/api/client.js index 9affabf..6536001 100644 --- a/web/src/api/client.js +++ b/web/src/api/client.js @@ -56,6 +56,7 @@ const api = { saveTerminalSettings: (settings) => request('/terminal/settings', { method: 'PUT', body: JSON.stringify(settings) }), getChatHistory: () => request('/chat/history'), clearChat: () => request('/chat/clear', { method: 'POST' }), + summarizeChat: () => request('/chat/summarize', { method: 'POST' }), sendChat: (message, stream = true, onChunk, signal) => { if (!stream) { return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false }) }) diff --git a/web/src/components/Studio.jsx b/web/src/components/Studio.jsx index fd4899d..63cff2a 100644 --- a/web/src/components/Studio.jsx +++ b/web/src/components/Studio.jsx @@ -284,6 +284,7 @@ export default function Studio({ api }) { const [streamThinking, setStreamThinking] = useState('') const [streamToolCalls, setStreamToolCalls] = useState([]) const [loaded, setLoaded] = useState(false) + const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 }) const messagesEnd = useRef(null) const textareaRef = useRef(null) const abortRef = useRef(null) @@ -297,6 +298,11 @@ export default function Studio({ api }) { { id: 'welcome', role: 'system', content: t('studio.welcomeNew'), time: new Date().toISOString() }, ]) } + setTokenInfo({ + used: data.tokens || 0, + max: data.max_tokens || 100000, + summarizeAt: data.summarize_at || 80000, + }) setLoaded(true) }).catch(() => { setMessages([ @@ -317,6 +323,28 @@ export default function Studio({ api }) { } }, [input]) + const refreshTokens = useCallback(async () => { + try { + const data = await api.getChatHistory() + setTokenInfo({ + used: data.tokens || 0, + max: data.max_tokens || 100000, + summarizeAt: data.summarize_at || 80000, + }) + } catch {} + }, [api]) + + const handleSummarize = useCallback(async () => { + setMessages(prev => [...prev, { id: Date.now().toString(), role: 'system', content: 'RĂ©sumĂ© de la conversation en cours...', time: new Date().toISOString() }]) + try { + const data = await api.summarizeChat() + setTokenInfo(prev => ({ ...prev, used: data.tokens || 0 })) + setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: '✓ Conversation rĂ©sumĂ©e automatiquement. Le contexte a Ă©tĂ© compressĂ©.', time: new Date().toISOString() }]) + } catch (err) { + setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: `Erreur de rĂ©sumĂ©: ${err.message}`, time: new Date().toISOString() }]) + } + }, [api]) + const handleClear = useCallback(async () => { try { await api.clearChat() @@ -341,6 +369,7 @@ export default function Studio({ api }) { '## Commandes Studio', '', '- `/clear` - Effacer la conversation', + '- `/summarize` - RĂ©sumer la conversation prĂ©cĂ©dente', '- `/help` - Afficher cette aide', '- `/plan ` - Demander un plan structurĂ©', '- `/export` - Exporter la conversation en Markdown', @@ -359,6 +388,11 @@ export default function Studio({ api }) { return } + if (text === '/summarize') { + handleSummarize() + return + } + if (text === '/model') { api.getProviders().then(data => { const active = data.providers?.find(p => p.active) @@ -474,8 +508,9 @@ export default function Studio({ api }) { setStreamThinking('') setStreamToolCalls([]) abortRef.current = null + refreshTokens() } - }, [input, loading, api, t, handleClear, streaming]) + }, [input, loading, api, t, handleClear, streaming, refreshTokens, handleSummarize]) const handleStop = useCallback(() => { if (abortRef.current) { @@ -515,6 +550,18 @@ export default function Studio({ api }) {
+
+
+
= tokenInfo.summarizeAt ? 'warn' : ''}`} + style={{ width: `${Math.min(100, (tokenInfo.used / tokenInfo.max) * 100)}%` }} + /> +
+ + {(tokenInfo.used / 1000).toFixed(1)}k / {(tokenInfo.max / 1000).toFixed(0)}k tokens + {tokenInfo.used >= tokenInfo.summarizeAt && ' · résumé automatique déclenché'} + +