diff --git a/internal/api/handlers_info.go b/internal/api/handlers_info.go index 4d02f76..5519412 100644 --- a/internal/api/handlers_info.go +++ b/internal/api/handlers_info.go @@ -117,3 +117,8 @@ func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) { s.scanResult = scanner.ScanSystem() writeJSON(w, map[string]string{"status": "ok"}) } + +func (s *Server) handleEditors(w http.ResponseWriter, r *http.Request) { + editors := scanner.ScanEditors() + writeJSON(w, map[string]interface{}{"editors": editors}) +} diff --git a/internal/api/server.go b/internal/api/server.go index bfa7300..f7041d0 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -1,18 +1,22 @@ package api import ( + "encoding/json" "net/http" "strings" + "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/scanner" ) type Server struct { - config *config.MuyueConfig - scanResult *scanner.ScanResult - mux *http.ServeMux - convStore *ConversationStore + config *config.MuyueConfig + scanResult *scanner.ScanResult + mux *http.ServeMux + convStore *ConversationStore + agentRegistry *agent.Registry + agentToolsJSON json.RawMessage } func NewServer(cfg *config.MuyueConfig) *Server { @@ -22,6 +26,10 @@ func NewServer(cfg *config.MuyueConfig) *Server { } s.scanResult = scanner.ScanSystem() s.convStore = NewConversationStore() + s.agentRegistry = agent.DefaultRegistry() + tools := s.agentRegistry.OpenAITools() + toolsJSON, _ := json.Marshal(tools) + s.agentToolsJSON = json.RawMessage(toolsJSON) s.routes() return s } @@ -38,6 +46,7 @@ func (s *Server) routes() { s.mux.HandleFunc("/api/updates", s.handleUpdates) s.mux.HandleFunc("/api/install", s.handleInstall) s.mux.HandleFunc("/api/scan", s.handleScan) + s.mux.HandleFunc("/api/editors", s.handleEditors) s.mux.HandleFunc("/api/preferences", s.handleUpdatePreferences) s.mux.HandleFunc("/api/terminal", s.handleTerminal) s.mux.HandleFunc("/api/ws/terminal", s.handleTerminalWS) diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 40d58a8..9721c7b 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -14,13 +14,13 @@ 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"` + 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"` } type RuntimeStatus struct { @@ -30,15 +30,15 @@ type RuntimeStatus struct { } 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"` + Tools []ToolStatus `yaml:"tools"` + Runtimes []RuntimeStatus `yaml:"runtimes"` + ShellSetup bool `yaml:"shell_setup"` + GitConfigured bool `yaml:"git_configured"` } var ( - cacheMu sync.RWMutex + cacheMu sync.RWMutex cacheResult *ScanResult cacheTime time.Time cacheTTL = 5 * time.Minute @@ -193,6 +193,43 @@ func checkGitConfig() bool { return true } +var editorsList = []struct { + name string + cmd []string + version []string +}{ + {"vim", []string{"vim"}, []string{"--version"}}, + {"nvim", []string{"nvim"}, []string{"--version"}}, + {"code", []string{"code"}, []string{"--version"}}, + {"emacs", []string{"emacs"}, []string{"--version"}}, + {"nano", []string{"nano"}, []string{"--version"}}, + {"helix", []string{"hx"}, []string{"--version"}}, + {"subl", []string{"subl"}, []string{"--version"}}, + {"zed", []string{"zed"}, []string{"--version"}}, +} + +func ScanEditors() []ToolStatus { + var results []ToolStatus + for _, e := range editorsList { + status := ToolStatus{Name: e.name} + path, err := exec.LookPath(e.name) + if err != nil { + continue + } + status.Installed = true + status.Path = path + if len(e.version) > 0 { + cmd := exec.Command(e.cmd[0], e.version...) + out, err := cmd.Output() + if err == nil { + status.Version = strings.TrimSpace(string(out)) + } + } + results = append(results, status) + } + return results +} + var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`) func (s *ScanResult) Summary() string { diff --git a/web/src/api/client.js b/web/src/api/client.js index 24a3601..d86dd89 100644 --- a/web/src/api/client.js +++ b/web/src/api/client.js @@ -22,6 +22,7 @@ const api = { getLSP: () => request('/lsp'), getMCP: () => request('/mcp'), getUpdates: () => request('/updates'), + getEditors: () => request('/editors'), runScan: () => request('/scan', { method: 'POST' }), installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }), configureMCP: () => request('/mcp/configure', { method: 'POST' }), @@ -73,6 +74,8 @@ const api = { if (onChunk) onChunk(full, data) } else if (data.thinking !== undefined || data.thinking_end) { if (onChunk) onChunk(full, data) + } else if (data.tool_call || data.tool_result) { + if (onChunk) onChunk(full, data) } } catch {} } diff --git a/web/src/components/OnboardingWizard.jsx b/web/src/components/OnboardingWizard.jsx index 6f589a0..0434c74 100644 --- a/web/src/components/OnboardingWizard.jsx +++ b/web/src/components/OnboardingWizard.jsx @@ -1,18 +1,19 @@ import { useState, useEffect } from 'react' -import { Sparkles, ArrowRight, ArrowLeft } from 'lucide-react' +import { Sparkles, ArrowRight, ArrowLeft, Search, Loader } from 'lucide-react' import { useI18n, LANGUAGES } from '../i18n' import { getLayoutList } from '../i18n/keyboards' const STEPS = [ - { key: 'welcome', title: 'welcome', field: null }, - { key: 'name', title: 'name', field: 'name' }, - { key: 'language', title: 'language', field: 'language' }, - { key: 'keyboard', title: 'keyboard', field: 'keyboard' }, - { key: 'editor', title: 'editor', field: 'editor' }, - { key: 'done', title: 'done', field: null }, + { key: 'welcome', title: 'welcome' }, + { key: 'name', title: 'name' }, + { key: 'language', title: 'language' }, + { key: 'keyboard', title: 'keyboard' }, + { key: 'apikey', title: 'apikey' }, + { key: 'editor', title: 'editor' }, + { key: 'done', title: 'done' }, ] -const EDITOR_SUGGESTIONS = ['vim', 'nvim', 'vscode', 'emacs', 'nano', 'helix'] +const BASE_EDITORS = ['vim', 'nvim', 'vscode', 'emacs', 'nano', 'helix'] export default function OnboardingWizard({ api, onComplete }) { const { t, language, keyboard, setLanguage, setKeyboard } = useI18n() @@ -21,11 +22,16 @@ export default function OnboardingWizard({ api, onComplete }) { name: '', language: 'fr', keyboard: 'azerty', + apikey: '', editor: '', }) + const [editorList, setEditorList] = useState(BASE_EDITORS) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [requiredError, setRequiredError] = useState(false) + const [validating, setValidating] = useState(false) + const [keyValid, setKeyValid] = useState(false) + const [scanning, setScanning] = useState(false) const current = STEPS[step] const layouts = getLayoutList() @@ -44,6 +50,7 @@ export default function OnboardingWizard({ api, onComplete }) { case 'name': return answers.name.trim().length > 0 case 'language': return !!answers.language case 'keyboard': return !!answers.keyboard + case 'apikey': return true case 'editor': return true case 'done': return true default: return true @@ -57,7 +64,7 @@ export default function OnboardingWizard({ api, onComplete }) { useEffect(() => { const handler = (e) => { if (e.key === 'Escape') { goPrev(); return } - if (e.key === 'Enter' && current.key !== 'done') { e.preventDefault(); goNext() } + if (e.key === 'Enter' && current.key !== 'done' && current.key !== 'editor') { e.preventDefault(); goNext() } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) @@ -69,19 +76,68 @@ export default function OnboardingWizard({ api, onComplete }) { } }, [step]) + const handleValidateKey = async () => { + if (!answers.apikey.trim()) return + setValidating(true) + setError(null) + try { + await api.validateProvider({ + name: 'minimax', + api_key: answers.apikey, + model: 'MiniMax-M2.7', + base_url: 'https://api.minimax.io/v1', + }) + setKeyValid(true) + } catch (err) { + setError(err.message || 'Clé invalide') + setKeyValid(false) + } + setValidating(false) + } + + const handleScanEditors = async () => { + setScanning(true) + setError(null) + try { + const data = await api.getEditors() + const detected = (data.editors || []).map(e => e.name) + const merged = [...new Set([...detected, ...BASE_EDITORS])] + setEditorList(merged) + if (detected.length === 0) { + setError('Aucun éditeur détecté') + } + } catch (err) { + setError(err.message || 'Erreur lors du scan') + } + setScanning(false) + } + const handleSave = async () => { setSaving(true) setError(null) try { - await api.saveProfile({ + const profile = { name: answers.name, pseudo: answers.name.split(' ')[0] || 'user', editor: answers.editor, - }) + } + if (answers.apikey.trim()) { + profile.apikey = answers.apikey + } + await api.saveProfile(profile) await api.savePreferences({ language: answers.language, keyboard_layout: answers.keyboard, }) + if (answers.apikey.trim()) { + await api.saveProvider({ + name: 'minimax', + api_key: answers.apikey, + model: 'MiniMax-M2.7', + base_url: 'https://api.minimax.io/v1', + active: true, + }) + } onComplete() } catch (err) { setError(err.message || 'Erreur lors de la sauvegarde') @@ -129,7 +185,7 @@ export default function OnboardingWizard({ api, onComplete }) { {current.key === 'language' && (
-
Quelle langue pr\u00e9f\u00e9rez-vous ?
+
Quelle langue préférez-vous ?
{LANGUAGES.map(lang => (
)} + {current.key === 'apikey' && ( +
+
Clé API MiniMax
+
+ Entrez votre clé API MiniMax pour activer l'assistant IA. Vous pouvez passer cette étape et la configurer plus tard. +
+ { setAnswers(a => ({ ...a, apikey: e.target.value })); setKeyValid(false); setError(null) }} + autoFocus + /> + {error && !keyValid &&
{error}
} + {keyValid &&
Clé valide ✓
} +
+ + +
+ {answers.apikey.trim() && !keyValid && !error && ( +
Cliquez "Valider la clé" ou "Passer"
+ )} +
+ )} + {current.key === 'editor' && (
-
Quel \u00e9diteur utilisez-vous ?
-
- {EDITOR_SUGGESTIONS.map(ed => ( -
setAnswers(a => ({ ...a, editor: ed }))} - > - {ed} -
- ))} +
Quel éditeur utilisez-vous ?
+
+
+ {editorList.map(ed => ( +
setAnswers(a => ({ ...a, editor: ed }))} + > + {ed} +
+ ))} +
+
setAnswers(a => ({ ...a, editor: e.target.value }))} autoFocus /> + {error &&
{error}
}
)} @@ -282,6 +388,19 @@ export default function OnboardingWizard({ api, onComplete }) { .onboarding-required { font-size: 12px; color: var(--error); margin-top: 4px; } + .onboarding-valid { + font-size: 12px; color: var(--success); margin-top: 4px; + } + .onboarding-hint { + font-size: 12px; color: var(--text-tertiary); margin-top: 4px; + } + .spin-icon { + animation: spin 1s linear infinite; + } + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } `}
)