From cb3d35756a70e5c40ebe94404b47ebcb42fdd1a0 Mon Sep 17 00:00:00 2001 From: Augustin Date: Sun, 26 Apr 2026 12:43:15 +0200 Subject: [PATCH] feat: terminal sudo blocking, token tracking, mermaid & consumption UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Block sudo/doas commands when not running as root - Add real token counting from API responses - Track and display consumption by provider/day - Add Mermaid diagram rendering in Shell and Studio - Add copy-to-clipboard buttons for code blocks - Support tables in AI message rendering - Update system prompt with context (date, time, root status) 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush --- internal/agent/definitions.go | 20 + internal/agent/prompts/studio_system.md | 94 +- internal/api/chat_engine.go | 19 + internal/api/consumption.go | 127 +++ internal/api/conversation.go | 21 +- internal/api/handlers_chat.go | 19 +- internal/api/handlers_common.go | 16 +- internal/api/handlers_info.go | 42 + internal/api/handlers_shell_chat.go | 77 +- internal/api/server.go | 3 + internal/api/shell_conversation.go | 82 +- internal/config/config.go | 2 +- internal/workflow/planner.go | 20 +- web/package-lock.json | 1253 +++++++++++++++++++++++ web/package.json | 1 + web/src/api/client.js | 1 + web/src/components/App.jsx | 6 +- web/src/components/Dashboard.jsx | 91 +- web/src/components/Shell.jsx | 262 ++++- web/src/components/Studio.jsx | 156 ++- web/src/styles/global.css | 62 +- 21 files changed, 2166 insertions(+), 208 deletions(-) create mode 100644 internal/api/consumption.go diff --git a/internal/agent/definitions.go b/internal/agent/definitions.go index 0c0b534..898b421 100644 --- a/internal/agent/definitions.go +++ b/internal/agent/definitions.go @@ -3,6 +3,7 @@ package agent import ( "context" "fmt" + "os" "os/exec" "path/filepath" "strings" @@ -14,6 +15,13 @@ type TerminalParams struct { Timeout int `json:"timeout,omitempty" description:"Timeout in seconds (default 60, max 300)"` } +type TerminalResponse struct { + Content string `json:"content"` + IsError bool `json:"is_error"` + SudoBlocked bool `json:"sudo_blocked,omitempty"` + Command string `json:"command,omitempty"` +} + func NewTerminalTool() (*ToolDefinition, error) { return NewTool("terminal", "Execute a shell command on the local system and return the output. Use for running builds, tests, git operations, package management, system info, or any CLI task. Commands run in the user's home directory by default. Long-running commands are auto-terminated.", @@ -22,6 +30,18 @@ func NewTerminalTool() (*ToolDefinition, error) { return TextErrorResponse("command is required"), nil } + if os.Geteuid() != 0 { + trimmed := strings.TrimSpace(p.Command) + lower := strings.ToLower(trimmed) + if strings.HasPrefix(lower, "sudo ") || strings.HasPrefix(lower, "doas ") || strings.HasPrefix(lower, "run0 ") || strings.HasPrefix(lower, "pkexec ") { + return ToolResponse{ + Content: fmt.Sprintf("BLOCKED: Command '%s' requires elevated privileges (%s). The current user is not root. Do NOT retry with sudo. Explain to the user that this command needs admin privileges and suggest an alternative, or tell them to run it manually in their terminal.", trimmed, strings.Fields(trimmed)[0]), + IsError: true, + Meta: map[string]string{"sudo_blocked": "true", "command": trimmed}, + }, nil + } + } + timeout := time.Duration(p.Timeout) * time.Second if timeout == 0 { timeout = 60 * time.Second diff --git a/internal/agent/prompts/studio_system.md b/internal/agent/prompts/studio_system.md index d32560a..d81b6bb 100644 --- a/internal/agent/prompts/studio_system.md +++ b/internal/agent/prompts/studio_system.md @@ -2,6 +2,16 @@ Tu es l'assistant IA de **Muyue Studio**, le centre de commandement de l'environ Tu es intĂ©grĂ© dans Muyue, un gestionnaire d'environnement de dĂ©veloppement de bureau. Ton rĂŽle est d'aider l'utilisateur Ă  configurer, gĂ©rer et optimiser son environnement dev. + +1. **AGIS, ne dĂ©cris pas** — Si l'utilisateur demande de faire quelque chose, utilise les outils immĂ©diatement. Ne dis pas "je pourrais faire X" — fais-le. +2. **SOIS AUTONOME** — Ne pose pas de questions si tu peux chercher, lire, dĂ©duire. Essaie plusieurs approches avant de bloquer. Ne t'arrĂȘte que pour les erreurs bloquantes rĂ©elles (credentials manquants, permissions, etc.). +3. **SOIS CONCIS** — Max 4 lignes par dĂ©faut. Pas de prĂ©ambule ("Voici...", "Je vais..."), pas de postambule ("N'hĂ©sitez pas...", "J'espĂšre que..."). RĂ©ponse directe. Un mot quand c'est suffisant. +4. **GÈRE LES ERREURS** — Si un outil Ă©choue, essaie 2-3 approches alternatives avant de rapporter l'Ă©chec. Lis le message d'erreur complet, isole la cause racine. +5. **NE DEVINE PAS** — Lis les fichiers avant d'Ă©diter. Utilise les outils pour obtenir les informations manquantes (lire, chercher, grep). +6. **CONFIDENTIALITÉ** — Ne rĂ©vĂšle jamais les clĂ©s API, mots de passe, tokens ou informations sensibles. +7. **LANGUE** — RĂ©ponds dans la mĂȘme langue que l'utilisateur. + + ## Environnement Muyue gĂšre : @@ -13,32 +23,70 @@ Muyue gĂšre : ## Outils disponibles -Tu as accĂšs Ă  des outils. Utilise-les concrĂštement, ne dĂ©cris pas ce que tu ferais — fais-le. +| Outil | Usage | +|-------|-------| +| **terminal** | ExĂ©cuter des commandes shell (builds, tests, git, etc.) | +| **crush_run** | DĂ©lĂ©guer une tĂąche complexe Ă  Crush (Ă©dition de fichiers, refactoring, debug) — prĂ©fĂšre cet outil pour les tĂąches multi-fichiers ou l'Ă©criture de code | +| **read_file** | Lire le contenu d'un fichier | +| **list_files** | Lister les fichiers d'un rĂ©pertoire | +| **search_files** | Chercher des fichiers par motif (glob) | +| **grep_content** | Chercher du texte dans les fichiers | +| **get_config** | Lire la configuration Muyue | +| **set_provider** | Configurer un fournisseur IA | +| **manage_ssh** | GĂ©rer les connexions SSH | +| **web_fetch** | RĂ©cupĂ©rer le contenu d'une URL | -- **terminal** : ExĂ©cuter des commandes shell (builds, tests, git, etc.) -- **crush_run** : DĂ©lĂ©guer une tĂąche complexe Ă  l'agent Crush (Ă©dition de fichiers, refactoring, debug) -- **read_file** : Lire le contenu d'un fichier -- **list_files** : Lister les fichiers d'un rĂ©pertoire -- **search_files** : Chercher des fichiers par motif (glob) -- **grep_content** : Chercher du texte dans le contenu des fichiers -- **get_config** : Lire la configuration Muyue -- **set_provider** : Configurer un fournisseur IA -- **manage_ssh** : GĂ©rer les connexions SSH -- **web_fetch** : RĂ©cupĂ©rer le contenu d'une URL + +- **Recherche avant action** — Utilise `search_files`, `grep_content`, `read_file` avant de supposer quoi que ce soit sur l'Ă©tat du systĂšme +- **DĂ©lĂ©gation intelligente** — Pour les tĂąches complexes (refactoring, crĂ©ation de fichiers, debug multi-fichiers), utilise `crush_run` au lieu d'enchaĂźner des commandes terminal +- **ParallĂ©lisme** — Lance plusieurs appels d'outils en parallĂšle quand les opĂ©rations sont indĂ©pendantes +- **Troncature** — Si un rĂ©sultat d'outil dĂ©passe 2000 caractĂšres, rĂ©sume les points clĂ©s au lieu de tout afficher +- **Une chose Ă  la fois** — Sauf si les opĂ©rations sont indĂ©pendantes, exĂ©cute sĂ©quentiellement + -## RĂšgles + +- DĂ©cide par toi-mĂȘme : cherche, lis, dĂ©duis, agis +- Ne demande confirmation que pour : actions destructrices (suppression, overwrite), plusieurs approches valides avec des trade-offs importants +- Si bloquĂ© : documente (a) ce que tu as essayĂ©, (b) pourquoi tu es bloquĂ©, (c) l'action minimale requise +- Ne t'arrĂȘte jamais pour : tĂąche trop grosse (dĂ©coupe), trop de fichiers (change-les), complexitĂ© (gĂšre-la) + -1. **AGIS, ne dĂ©cris pas** — Si l'utilisateur demande de faire quelque chose, utilise les outils pour le faire. Ne dis pas "je pourrais faire X" — fais-le. -2. **Sois concis** — Pas de prĂ©ambule, pas de blabla. RĂ©ponse directe. -3. **Une chose Ă  la fois** — N'appelle pas plusieurs outils simultanĂ©ment sauf si c'est nĂ©cessaire. -4. **GĂšre les erreurs** — Si un outil Ă©choue, essaie une approche diffĂ©rente avant de le dire Ă  l'utilisateur. -5. **Ne devine pas** — Si tu n'as pas assez d'informations, utilise les outils pour les obtenir (lire un fichier, chercher, etc.) -6. **ConfidentialitĂ©** — Ne rĂ©vĂšle jamais les clĂ©s API, mots de passe ou informations sensibles dans tes rĂ©ponses. -7. **Langue** — RĂ©ponds dans la mĂȘme langue que l'utilisateur. + +1. Lis le message d'erreur complet +2. Comprends la cause racine +3. Essaie une approche diffĂ©rente (pas la mĂȘme) +4. Cherche du code similaire qui fonctionne +5. Applique un correctif ciblĂ© +6. VĂ©rifie que ça marche +7. Pour chaque erreur, essaie au moins 2-3 stratĂ©gies avant de conclure que c'est bloquant + ## Format des rĂ©ponses -- Code : utilise des blocs markdown -- RĂ©sultats d'outils : rĂ©sume les points clĂ©s, ne colle pas des milliers de lignes -- Erreurs : explique clairement et propose une solution -- SuccĂšs : confirme briĂšvement ce qui a Ă©tĂ© fait +- **Code** : blocs markdown avec le langage spĂ©cifiĂ© +- **RĂ©sultats d'outils** : rĂ©sume les points clĂ©s, max 2000 caractĂšres, ne copie pas des milliers de lignes +- **Erreurs** : explique clairement la cause et propose une solution concrĂšte +- **SuccĂšs** : confirme briĂšvement ce qui a Ă©tĂ© fait (1 ligne) +- **Multi-fichiers** : liste les fichiers modifiĂ©s avec `fichier:ligne` pour les rĂ©fĂ©rences + +## Diagrammes Mermaid + +Tu peux utiliser des diagrammes Mermaid pour visualiser des architectures, flux, sĂ©quences, etc. +Utilise un bloc code avec le langage `mermaid` : + +```mermaid +graph TD + A[DĂ©but] --> B{DĂ©cision} + B -->|Oui| C[Action] + B -->|Non| D[Fin] +``` + +Types utiles : +- `graph TD/LR` — Architecture, flux de donnĂ©es +- `sequenceDiagram` — Interactions entre composants +- `flowchart` — Processus et dĂ©cisions +- `classDiagram` — Structures de donnĂ©es +- `erDiagram` — SchĂ©mas de base de donnĂ©es +- `gantt` — Planning et timelines + +Utilise Mermaid quand ça apporte de la clartĂ© : architecture complexe, flux multi-Ă©tapes, relations entre entitĂ©s. Ne l'utilise pas pour du texte simple. diff --git a/internal/api/chat_engine.go b/internal/api/chat_engine.go index 79193a0..65d01f6 100644 --- a/internal/api/chat_engine.go +++ b/internal/api/chat_engine.go @@ -21,6 +21,7 @@ type ChatEngine struct { tools json.RawMessage onChunk func(map[string]interface{}) stream bool + TotalTokens int } // NewChatEngine creates a new ChatEngine instance. @@ -71,6 +72,10 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. return finalContent, allToolCalls, allToolResults, err } + if resp.Usage.TotalTokens > 0 { + ce.TotalTokens += resp.Usage.TotalTokens + } + choice := resp.Choices[0] content := cleanThinkingTags(choice.Message.Content) @@ -123,6 +128,11 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. "content": result.Content, "is_error": result.IsError, } + if result.Meta != nil { + for k, v := range result.Meta { + resultData[k] = v + } + } allToolResults = append(allToolResults, map[string]interface{}{ "tool_call_id": tc.ID, "name": tc.Function.Name, @@ -149,6 +159,11 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. return finalContent, allToolCalls, allToolResults, nil } +// ProviderName returns the name of the active provider used by the engine. +func (ce *ChatEngine) ProviderName() string { + return ce.orchestrator.ProviderName() +} + // RunNonStream executes chat without streaming content to client. func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator.Message) (string, error) { var finalContent string @@ -159,6 +174,10 @@ func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator. return finalContent, err } + if resp.Usage.TotalTokens > 0 { + ce.TotalTokens += resp.Usage.TotalTokens + } + choice := resp.Choices[0] content := cleanThinkingTags(choice.Message.Content) diff --git a/internal/api/consumption.go b/internal/api/consumption.go new file mode 100644 index 0000000..b618c1d --- /dev/null +++ b/internal/api/consumption.go @@ -0,0 +1,127 @@ +package api + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" + "time" + + "github.com/muyue/muyue/internal/config" +) + +type consumptionEntry struct { + Date string `json:"date"` + Tokens int `json:"tokens"` + Requests int `json:"requests"` +} + +type providerConsumption struct { + Name string `json:"name"` + Daily []consumptionEntry `json:"daily"` + Total int `json:"total_tokens"` + Requests int `json:"total_requests"` +} + +type consumptionStore struct { + mu sync.Mutex + providers map[string]*providerConsumption +} + +func newConsumptionStore() *consumptionStore { + cs := &consumptionStore{ + providers: make(map[string]*providerConsumption), + } + cs.load() + cs.prune() + return cs +} + +func (cs *consumptionStore) Record(providerName string, tokens int) { + if tokens <= 0 || providerName == "" { + return + } + cs.mu.Lock() + defer cs.mu.Unlock() + + today := time.Now().UTC().Format("2006-01-02") + + p, ok := cs.providers[providerName] + if !ok { + p = &providerConsumption{Name: providerName} + cs.providers[providerName] = p + } + + p.Total += tokens + p.Requests++ + + if len(p.Daily) > 0 && p.Daily[len(p.Daily)-1].Date == today { + p.Daily[len(p.Daily)-1].Tokens += tokens + p.Daily[len(p.Daily)-1].Requests++ + } else { + p.Daily = append(p.Daily, consumptionEntry{ + Date: today, + Tokens: tokens, + Requests: 1, + }) + } + + cs.save() +} + +func (cs *consumptionStore) GetAll() map[string]*providerConsumption { + cs.mu.Lock() + defer cs.mu.Unlock() + + result := make(map[string]*providerConsumption) + for k, v := range cs.providers { + pc := *v + daily := make([]consumptionEntry, len(v.Daily)) + copy(daily, v.Daily) + pc.Daily = daily + result[k] = &pc + } + return result +} + +func (cs *consumptionStore) prune() { + cutoff := time.Now().UTC().AddDate(0, 0, -7).Format("2006-01-02") + for _, p := range cs.providers { + filtered := make([]consumptionEntry, 0) + for _, d := range p.Daily { + if d.Date >= cutoff { + filtered = append(filtered, d) + } + } + p.Daily = filtered + } +} + +func (cs *consumptionStore) filePath() string { + dir, err := config.ConfigDir() + if err != nil { + return "" + } + return filepath.Join(dir, "consumption.json") +} + +func (cs *consumptionStore) load() { + fp := cs.filePath() + if fp == "" { + return + } + data, err := os.ReadFile(fp) + if err != nil { + return + } + json.Unmarshal(data, &cs.providers) +} + +func (cs *consumptionStore) save() { + fp := cs.filePath() + if fp == "" { + return + } + data, _ := json.Marshal(cs.providers) + os.WriteFile(fp, data, 0644) +} diff --git a/internal/api/conversation.go b/internal/api/conversation.go index 505d7cb..195c846 100644 --- a/internal/api/conversation.go +++ b/internal/api/conversation.go @@ -32,9 +32,10 @@ type Conversation struct { } type ConversationStore struct { - mu sync.RWMutex - path string - conv *Conversation + mu sync.RWMutex + path string + conv *Conversation + realTokens int } type TokenCount struct { @@ -133,6 +134,7 @@ func (cs *ConversationStore) Clear() { cs.conv.Summary = "" cs.conv.CreatedAt = time.Now().Format(time.RFC3339) cs.conv.UpdatedAt = time.Now().Format(time.RFC3339) + cs.realTokens = 0 cs.save() } @@ -154,9 +156,22 @@ func (cs *ConversationStore) TrimOld(keepCount int) { } func (cs *ConversationStore) ApproxTokenCount() int { + if cs.realTokens > 0 { + return cs.realTokens + } return cs.ApproxTokenCountDetailed().total } +// AddRealTokens accumulates actual token counts from the API response. +func (cs *ConversationStore) AddRealTokens(tokens int) { + if tokens <= 0 { + return + } + cs.mu.Lock() + cs.realTokens += tokens + cs.mu.Unlock() +} + func (cs *ConversationStore) ApproxTokenCountDetailed() TokenCount { cs.mu.RLock() defer cs.mu.RUnlock() diff --git a/internal/api/handlers_chat.go b/internal/api/handlers_chat.go index 58cb135..673f3a3 100644 --- a/internal/api/handlers_chat.go +++ b/internal/api/handlers_chat.go @@ -3,9 +3,12 @@ package api import ( "context" "encoding/json" + "fmt" "net/http" + "os" "regexp" "strings" + "time" "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/orchestrator" @@ -42,7 +45,14 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { writeError(w, err.Error(), http.StatusServiceUnavailable) return } - orb.SetSystemPrompt(agent.StudioSystemPrompt()) + var studioPrompt strings.Builder + studioPrompt.WriteString(agent.StudioSystemPrompt()) + studioPrompt.WriteString(fmt.Sprintf("\nDate: %s\nHeure: %s\n", time.Now().Format("02/01/2006"), time.Now().Format("15:04:05"))) + studioPrompt.WriteString(fmt.Sprintf("Root: %t\n", os.Geteuid() == 0)) + if os.Geteuid() != 0 { + studioPrompt.WriteString("⚠ Session utilisateur standard — les commandes sudo/doas nĂ©cessitent une autorisation. N'utilise PAS sudo ou doas sans demander.\n") + } + orb.SetSystemPrompt(studioPrompt.String()) orb.SetTools(s.agentToolsJSON) if body.Stream { @@ -91,6 +101,9 @@ func (s *Server) handleStreamChat(w http.ResponseWriter, orb *orchestrator.Orche storeContent = string(storeJSON) } s.convStore.Add("assistant", storeContent) + s.convStore.AddRealTokens(engine.TotalTokens) + + s.consumption.Record(engine.ProviderName(), engine.TotalTokens) sseWriter.Write(map[string]interface{}{"done": "true"}) } @@ -107,6 +120,10 @@ func (s *Server) handleNonStreamChat(w http.ResponseWriter, orb *orchestrator.Or } s.convStore.Add("assistant", finalContent) + s.convStore.AddRealTokens(engine.TotalTokens) + + s.consumption.Record(engine.ProviderName(), engine.TotalTokens) + writeJSON(w, map[string]string{"content": finalContent}) } diff --git a/internal/api/handlers_common.go b/internal/api/handlers_common.go index 50ae5f8..75d0560 100644 --- a/internal/api/handlers_common.go +++ b/internal/api/handlers_common.go @@ -5,7 +5,21 @@ import ( "net/http" ) -const summarizePrompt = `RĂ©sume la conversation suivante de maniĂšre concise et structurĂ©e. Garde les points clĂ©s, les dĂ©cisions prises, le contexte technique important. Le rĂ©sumĂ© doit permettre de continuer la conversation sans perte de contexte. RĂ©ponds uniquement avec le rĂ©sumĂ©, sans meta-commentaire.` +const summarizePrompt = `RĂ©sume cette conversation de maniĂšre ultra-concise et structurĂ©e. + +CONSERVE : +- Les dĂ©cisions techniques prises et leur rationale +- Les configurations modifiĂ©es (noms exacts, valeurs) +- Les fichiers/chemins manipulĂ©s +- Les erreurs rencontrĂ©es et leurs rĂ©solutions +- Le contexte nĂ©cessaire pour continuer + +ÉLIMINE : +- Les Ă©changes de politesse +- Les tentatives infructueuses (sauf si la solution n'a pas Ă©tĂ© trouvĂ©e) +- Les sorties d'outils brutes (garde seulement les conclusions) + +FORMAT : Markdown structurĂ© avec sections. Max 500 mots. Pas de mĂ©ta-commentaire.` func writeJSON(w http.ResponseWriter, data interface{}) { json.NewEncoder(w).Encode(data) diff --git a/internal/api/handlers_info.go b/internal/api/handlers_info.go index 921481a..33850c4 100644 --- a/internal/api/handlers_info.go +++ b/internal/api/handlers_info.go @@ -534,6 +534,39 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) { q.Healthy = p.APIKey != "" if p.APIKey == "" { q.Error = "no API key" + results = append(results, q) + continue + } + mimoBase := p.BaseURL + if mimoBase == "" { + mimoBase = "https://token-plan-ams.xiaomimimo.com/v1" + } + req, _ := http.NewRequest("GET", strings.TrimRight(mimoBase, "/")+"/models", nil) + req.Header.Set("Authorization", "Bearer "+p.APIKey) + resp, err := client.Do(req) + if err != nil { + q.Error = err.Error() + } else { + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + var data map[string]interface{} + if json.Unmarshal(body, &data) == nil { + if modelList, ok := data["data"].([]interface{}); ok { + models := make([]map[string]interface{}, 0) + for _, m := range modelList { + if mm, ok := m.(map[string]interface{}); ok { + id, _ := mm["id"].(string) + if id != "" { + models = append(models, map[string]interface{}{ + "model": id, + }) + } + } + } + q.Data = map[string]interface{}{"models": models, "available": len(models)} + q.Healthy = true + } + } } case "claude", "anthropic": // Claude Code n'a pas d'API externe, vĂ©rifier l'installation @@ -551,6 +584,15 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]interface{}{"providers": results}) } +func (s *Server) handleProvidersConsumption(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + writeError(w, "GET only", http.StatusMethodNotAllowed) + return + } + data := s.consumption.GetAll() + writeJSON(w, map[string]interface{}{"providers": data}) +} + func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) { home, _ := os.UserHomeDir() type cmdEntry struct { diff --git a/internal/api/handlers_shell_chat.go b/internal/api/handlers_shell_chat.go index 7eebaab..d1cd38a 100644 --- a/internal/api/handlers_shell_chat.go +++ b/internal/api/handlers_shell_chat.go @@ -9,6 +9,7 @@ import ( "os/exec" "runtime" "strings" + "time" "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/orchestrator" @@ -62,35 +63,36 @@ func (s *Server) handleShellChat(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) buildShellSystemPrompt(_ ShellChatRequest) string { +func (s *Server) buildShellSystemPrompt(req ShellChatRequest) string { var sb strings.Builder - sb.WriteString(`Tu es l'Analyste SystĂšme de Muyue. Tu es un expert en administration systĂšme et dĂ©veloppement. -Tu aides l'utilisateur Ă  comprendre son systĂšme, diagnostiquer des problĂšmes, et optimiser son environnement. - -OUTILS DISPONIBLES: -- terminal: ExĂ©cute des commandes shell sur le systĂšme local et retourne le rĂ©sultat - -RÈGLES: -- Utilise l'outil terminal pour exĂ©cuter des commandes quand c'est nĂ©cessaire -- Analyse les rĂ©sultats et explique-les clairement -- Formate tes rĂ©ponses en markdown avec des blocs de code quand appropriĂ© -- Sois concis et technique -- Quand tu proposes des commandes alternatives, utilise des blocs de code markdown - -`) + sb.WriteString(shellSystemPromptBase) analysis := LoadSystemAnalysis() if analysis != "" { - sb.WriteString("=== ANALYSE SYSTÈME ACTUELLE ===\n") + sb.WriteString("\n") sb.WriteString(analysis) - sb.WriteString("\n=== FIN DE L'ANALYSE ===\n\n") + sb.WriteString("\n\n\n") } sb.WriteString(fmt.Sprintf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH)) if hostname, err := os.Hostname(); err == nil { sb.WriteString("Hostname: " + hostname + "\n") } + if user := os.Getenv("USER"); user != "" { + sb.WriteString("User: " + user + "\n") + } + + isRoot := os.Geteuid() == 0 + sb.WriteString(fmt.Sprintf("Root: %t\n", isRoot)) + if isRoot { + sb.WriteString("⚠ Session en root — toutes les commandes ont les privilĂšges administrateur.\n") + } else { + sb.WriteString("⚠ Session utilisateur standard — les commandes sudo/doas nĂ©cessitent une autorisation. N'utilise PAS sudo ou doas sans demander.\n") + } + + now := time.Now() + sb.WriteString(fmt.Sprintf("Date: %s\nHeure: %s\n", now.Format("02/01/2006"), now.Format("15:04:05"))) return sb.String() } @@ -131,6 +133,9 @@ func (s *Server) handleShellChatStream(w http.ResponseWriter, orb *orchestrator. storeContent = string(storeJSON) } s.shellConvStore.Add("assistant", storeContent) + s.shellConvStore.AddRealTokens(engine.TotalTokens) + + s.consumption.Record(engine.ProviderName(), engine.TotalTokens) sseWriter.Write(map[string]interface{}{ "done": "true", @@ -150,6 +155,10 @@ func (s *Server) handleShellChatNonStream(w http.ResponseWriter, orb *orchestrat } s.shellConvStore.Add("assistant", finalContent) + s.shellConvStore.AddRealTokens(engine.TotalTokens) + + s.consumption.Record(engine.ProviderName(), engine.TotalTokens) + writeJSON(w, map[string]interface{}{ "content": finalContent, "tokens": s.shellConvStore.ApproxTokens(), @@ -289,15 +298,33 @@ func (s *Server) handleShellAnalyze(w http.ResponseWriter, r *http.Request) { } orb.SetSystemPrompt(agent.StudioSystemPrompt()) - analysisPrompt := `Tu es un expert en administration systĂšme. Analyse les informations suivantes sur le systĂšme de l'utilisateur. -GĂ©nĂšre un rapport d'analyse concis et structurĂ© en markdown qui inclut: -1. Un rĂ©sumĂ© de l'Ă©tat du systĂšme -2. Les points d'attention (performance, sĂ©curitĂ©, configuration) -3. Des recommandations spĂ©cifiques d'optimisation -4. Les outils manquants qui pourraient ĂȘtre utiles -5. L'Ă©tat du rĂ©seau et des connexions + analysisPrompt := `Tu es un expert en administration systĂšme. Analyse les informations suivantes et gĂ©nĂšre un rapport structurĂ© en markdown. -Sois concret et technique. Le rapport sera utilisĂ© comme contexte pour un assistant terminal. +STRUCTURE REQUISE : + +## État du systĂšme +- RĂ©sumĂ© en 2-3 phrases de l'Ă©tat gĂ©nĂ©ral (OK/Attention/Critique) + +## Points d'attention +Liste les problĂšmes dĂ©tectĂ©s par prioritĂ© : +- **CRITIQUE** : problĂšmes de sĂ©curitĂ©, espace disque < 10%, mĂ©moire < 10% +- **ATTENTION** : CPU Ă©levĂ©, services en Ă©chec, config non-optimale +- **INFO** : amĂ©liorations possibles, mises Ă  jour disponibles + +## Recommandations +Pour chaque point d'attention, donne UNE commande ou action corrective concrĂšte. + +## Outils manquants +Liste les outils utiles non installĂ©s avec la commande d'installation. + +## RĂ©seau +- Interfaces actives, ports en Ă©coute, connectivitĂ© + +RÈGLES : +- Pas de blabla gĂ©nĂ©rique — sois spĂ©cifique Ă  CE systĂšme +- Inclus les valeurs numĂ©riques rĂ©elles (%, Go, MHz) +- Max 1500 mots +- Le rapport sert de contexte persistant pour un assistant terminal ` + sysInfo.String() diff --git a/internal/api/server.go b/internal/api/server.go index e07e75c..1b37478 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -18,6 +18,7 @@ type Server struct { mux *http.ServeMux convStore *ConversationStore shellConvStore *ShellConvStore + consumption *consumptionStore agentRegistry *agent.Registry agentToolsJSON json.RawMessage shellAgentRegistry *agent.Registry @@ -50,6 +51,7 @@ func NewServer(cfg *config.MuyueConfig) *Server { s.scanResult = scanner.ScanSystem() s.convStore = NewConversationStore() s.shellConvStore = NewShellConvStore() + s.consumption = newConsumptionStore() s.agentRegistry = agent.DefaultRegistry() tools := s.agentRegistry.OpenAITools() toolsJSON, _ := json.Marshal(tools) @@ -131,6 +133,7 @@ func (s *Server) routes() { s.mux.HandleFunc("/api/skills/import", s.handleSkillImport) s.mux.HandleFunc("/api/dashboard/status", s.handleDashboardStatus) s.mux.HandleFunc("/api/providers/quota", s.handleProvidersQuota) + s.mux.HandleFunc("/api/providers/consumption", s.handleProvidersConsumption) s.mux.HandleFunc("/api/recent-commands", s.handleRecentCommands) s.mux.HandleFunc("/api/running-processes", s.handleRunningProcesses) s.mux.HandleFunc("/api/system/metrics", s.handleSystemMetrics) diff --git a/internal/api/shell_conversation.go b/internal/api/shell_conversation.go index 657cfee..141ca0a 100644 --- a/internal/api/shell_conversation.go +++ b/internal/api/shell_conversation.go @@ -14,6 +14,63 @@ import ( const shellMaxTokens = 100000 const shellCharsPerToken = 4 +const shellSystemPromptBase = `Tu es l'**Analyste SystĂšme** de Muyue. Tu es un expert en administration systĂšme, DevOps et dĂ©veloppement. + + +1. **AGIS, ne dĂ©cris pas** — Utilise l'outil terminal pour exĂ©cuter, ne te contente pas de proposer des commandes. +2. **SOIS AUTONOME** — Cherche les infos manquantes via des commandes avant de demander Ă  l'utilisateur. Essaie plusieurs approches avant de bloquer. +3. **SOIS CONCIS** — Max 4 lignes par dĂ©faut. Pas de prĂ©ambule. RĂ©ponse directe et technique. +4. **GÈRE LES ERREURS** — Si une commande Ă©choue, lis l'erreur, comprends la cause, essaie une approche alternative. 2-3 tentatives avant de rapporter. +5. **SÉCURITÉ** — Ne rĂ©vĂšle jamais de credentials. Demande confirmation avant les commandes destructrices (rm -rf, format, etc.). +6. **LANGUE** — RĂ©ponds dans la mĂȘme langue que l'utilisateur. + + + +Outil disponible : **terminal** — ExĂ©cute des commandes shell sur le systĂšme local. + +StratĂ©gies : +- **Diagnostique** — EnchaĂźne les commandes de diagnostic (ps, df, free, top, journalctl, dmesg, netstat, ss, etc.) +- **ParallĂ©lisme** — Combine les commandes avec && ou ; quand elles sont indĂ©pendantes +- **Filtrage** — Utilise grep, awk, sort, head pour extraire l'essentiel des sorties volumineuses +- **Non-interactif** — PrĂ©fĂšre les commandes non-interactives (apt install -y, non pas apt install) +- **Troncature** — Si le rĂ©sultat dĂ©passe 2000 caractĂšres, rĂ©sume les points clĂ©s au lieu de tout afficher + + + +- DĂ©cide par toi-mĂȘme : exĂ©cute des commandes pour comprendre l'Ă©tat du systĂšme +- Ne demande confirmation que pour les actions destructrices +- Si tu ne connais pas la commande exacte, exĂ©cute la commande avec --help pour la trouver +- Si bloquĂ© : documente ce que tu as essayĂ©, pourquoi, et l'action minimale requise +- Ne t'arrĂȘte jamais pour une tĂąche complexe — dĂ©coupe en Ă©tapes et exĂ©cute-les + + + +1. Lis le message d'erreur complet (stderr + stdout) +2. Identifie la cause racine (permissions, paquet manquant, config, service) +3. Essaie : vĂ©rifier le service, vĂ©rifier les logs, chercher le paquet, tester la connexion +4. Propose une solution concrĂšte, pas gĂ©nĂ©rique + + + +- **Commandes** : blocs markdown avec le langage (bash, sh, etc.) +- **RĂ©sultats** : rĂ©sume les mĂ©triques clĂ©s, pas de dump complet +- **Erreurs** : cause + solution en 1-2 lignes +- **SuccĂšs** : confirmation en 1 ligne +- **Analyses** : markdown structurĂ© avec sections si nĂ©cessaire + + + +Tu peux utiliser des diagrammes Mermaid pour visualiser : +- Architecture systĂšme (graph TD/LR) +- Flux rĂ©seau (sequenceDiagram) +- Processus (flowchart) +- Timeline (gantt) + +Utilise un bloc de code avec le langage mermaid quand ça clarifie l'explication. Pas pour du texte simple. + + +` + type ShellMessage struct { ID string `json:"id"` Role string `json:"role"` @@ -22,9 +79,10 @@ type ShellMessage struct { } type ShellConvStore struct { - mu sync.RWMutex - path string - msgs []ShellMessage + mu sync.RWMutex + path string + msgs []ShellMessage + realTokens int } func NewShellConvStore() *ShellConvStore { @@ -82,19 +140,37 @@ func (s *ShellConvStore) Clear() { s.mu.Lock() defer s.mu.Unlock() s.msgs = []ShellMessage{} + s.realTokens = 0 s.save() } func (s *ShellConvStore) ApproxTokens() int { + if s.realTokens > 0 { + return s.realTokens + } s.mu.RLock() defer s.mu.RUnlock() total := 0 for _, m := range s.msgs { total += utf8.RuneCountInString(m.Content) / shellCharsPerToken } + total += utf8.RuneCountInString(shellSystemPromptBase) / shellCharsPerToken + if analysis := LoadSystemAnalysis(); analysis != "" { + total += utf8.RuneCountInString(analysis) / shellCharsPerToken + } return total } +// AddRealTokens accumulates actual token counts from the API response. +func (s *ShellConvStore) AddRealTokens(tokens int) { + if tokens <= 0 { + return + } + s.mu.Lock() + s.realTokens += tokens + s.mu.Unlock() +} + func (s *ShellConvStore) AtLimit() bool { return s.ApproxTokens() >= shellMaxTokens } diff --git a/internal/config/config.go b/internal/config/config.go index d0e143e..b2941d9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -289,7 +289,7 @@ func Default() *MuyueConfig { }, { Name: "mimo", - Model: "MiMo-V2.5-Pro", + Model: "mimo-v2.5-pro", BaseURL: "https://token-plan-ams.xiaomimimo.com/v1", Active: false, }, diff --git a/internal/workflow/planner.go b/internal/workflow/planner.go index ccec148..3beeba0 100644 --- a/internal/workflow/planner.go +++ b/internal/workflow/planner.go @@ -159,14 +159,18 @@ func parsePlanResponse(content string) ([]Step, error) { return steps, nil } -const plannerSystemPrompt = `Tu es un assistant de planification de workflows pour Muyue. Tu gĂ©nĂšres des plans d'exĂ©cution sous forme de JSON. Chaque plan est une sĂ©quence d'Ă©tapes (steps) reprĂ©sentant des appels d'outils. +const plannerSystemPrompt = `Tu es un planificateur de workflows pour Muyue. Tu gĂ©nĂšres des plans d'exĂ©cution sous forme de tableaux JSON. -Pour gĂ©nĂ©rer un plan: -1. Comprends l'objectif de l'utilisateur -2. Identifie les outils nĂ©cessaires -3. DĂ©compose en Ă©tapes logiques -4. SpĂ©cifie les paramĂštres de chaque outil +RÈGLES : +1. Analyse l'objectif → identifie les outils → dĂ©compose en Ă©tapes +2. Chaque Ă©tape : {"name": string, "tool": string, "args": object} +3. Max 10 Ă©tapes par plan +4. Ordonne par dĂ©pendances (les lectures avant les Ă©critures) +5. PrĂ©fĂšre les commandes non-interactives +6. Utilise crush_run pour les tĂąches complexes multi-fichiers -RĂ©ponds toujours en JSON valide, sans texte additionnel.` +Outils : terminal, crush_run, read_file, list_files, search_files, grep_content, get_config, set_provider, manage_ssh, web_fetch -var _ = plannerSystemPrompt \ No newline at end of file +RĂ©ponds UNIQUEMENT en JSON valide, sans texte avant/aprĂšs.` + +const _ = plannerSystemPrompt \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index cc97aa7..22b7341 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,6 +14,7 @@ "@xterm/addon-webgl": "^0.20.0-beta.202", "@xterm/xterm": "^6.1.0-beta.203", "lucide-react": "^1.8.0", + "mermaid": "^11.14.0", "react": "^19.2.5", "react-dom": "^19.2.5" }, @@ -22,6 +23,62 @@ "vite": "^8.0.9" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", + "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "12.0.0", + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", + "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", + "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", + "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", + "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", @@ -56,6 +113,32 @@ "tslib": "^2.4.0" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.2" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", + "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", + "license": "MIT", + "dependencies": { + "langium": "^4.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -378,6 +461,282 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -461,6 +820,584 @@ "addons/*" ] }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/chevrotain": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", + "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "12.0.0", + "@chevrotain/gast": "12.0.0", + "@chevrotain/regexp-to-ast": "12.0.0", + "@chevrotain/types": "12.0.0", + "@chevrotain/utils": "12.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", + "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^12.0.0" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cytoscape": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", + "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -471,6 +1408,15 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -504,6 +1450,87 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/katex": { + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/langium": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz", + "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==", + "license": "MIT", + "dependencies": { + "@chevrotain/regexp-to-ast": "~12.0.0", + "chevrotain": "~12.0.0", + "chevrotain-allstar": "~0.4.1", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -777,6 +1804,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lucide-react": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", @@ -786,6 +1819,59 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", + "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "lodash-es": "^4.17.23", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -805,6 +1891,24 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -825,6 +1929,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", @@ -875,6 +2006,12 @@ "react": "^19.2.5" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rolldown": { "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", @@ -916,6 +2053,30 @@ "dev": true, "license": "MIT" }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -932,6 +2093,21 @@ "node": ">=0.10.0" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -949,6 +2125,15 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -957,6 +2142,25 @@ "license": "0BSD", "optional": true }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "8.0.9", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", @@ -1034,6 +2238,55 @@ "optional": true } } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" } } } diff --git a/web/package.json b/web/package.json index 5f53677..73add98 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,7 @@ "@xterm/addon-webgl": "^0.20.0-beta.202", "@xterm/xterm": "^6.1.0-beta.203", "lucide-react": "^1.8.0", + "mermaid": "^11.14.0", "react": "^19.2.5", "react-dom": "^19.2.5" }, diff --git a/web/src/api/client.js b/web/src/api/client.js index 5148a10..0c1defa 100644 --- a/web/src/api/client.js +++ b/web/src/api/client.js @@ -38,6 +38,7 @@ const api = { importSkill: (path) => request('/skills/import', { method: 'POST', body: JSON.stringify({ import_path: path }) }), getDashboardStatus: () => request('/dashboard/status'), getProvidersQuota: () => request('/providers/quota'), + getProvidersConsumption: () => request('/providers/consumption'), getRecentCommands: () => request('/recent-commands'), getRunningProcesses: () => request('/running-processes'), getSystemMetrics: () => request('/system/metrics'), diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx index 563a7ba..6d707b4 100644 --- a/web/src/components/App.jsx +++ b/web/src/components/App.jsx @@ -94,10 +94,8 @@ export default function App() { shell: [ { keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') }, { keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') }, - { keys: `${layout.keys.ctrl}+F`, desc: t('statusbar.search') }, - { keys: `${layout.keys.ctrl}+/Ctrl−`, desc: t('statusbar.zoom') }, - { keys: `Alt+1-7`, desc: t('statusbar.switchTab') }, - { keys: `${layout.keys.shift}+Tab`, desc: t('statusbar.nextTab') }, + { keys: `${layout.keys.ctrl}+${layout.keys.shift}+F`, desc: t('statusbar.search') }, + { keys: `${layout.keys.ctrl}++/${layout.keys.ctrl}+−`, desc: t('statusbar.zoom') }, { keys: layout.keys.enter, desc: t('statusbar.runCommand') }, { keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') }, ], diff --git a/web/src/components/Dashboard.jsx b/web/src/components/Dashboard.jsx index 44111b5..bbadee7 100644 --- a/web/src/components/Dashboard.jsx +++ b/web/src/components/Dashboard.jsx @@ -6,6 +6,12 @@ const MAX_POINTS = 30 const POLL_INTERVAL = 5000 const MAX_IDLE_POLLS = 3 +function formatTokens(n) { + if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M' + if (n >= 1000) return (n / 1000).toFixed(1) + 'K' + return String(n) +} + function MiniGraph({ data, max, color, label, unit }) { if (!data || data.length < 2) return
collecting...
const m = max || Math.max(...data, 1) @@ -37,9 +43,28 @@ function MiniGraph({ data, max, color, label, unit }) { ) } +function BarChart({ data, max, color }) { + if (!data || data.length === 0) return null + const barW = 100 / 7 + const m = max || Math.max(...data.map(d => d.tokens), 1) + return ( + + {data.map((d, i) => { + const h = Math.max(1, (d.tokens / m) * 36) + const x = i * barW + barW * 0.15 + const w = barW * 0.7 + return ( + + ) + })} + + ) +} + export default function Dashboard({ api, refreshRef }) { const { t } = useI18n() const [quota, setQuota] = useState(null) + const [consumption, setConsumption] = useState(null) const [recentCmds, setRecentCmds] = useState([]) const [processes, setProcesses] = useState([]) const [metrics, setMetrics] = useState(null) @@ -51,13 +76,15 @@ export default function Dashboard({ api, refreshRef }) { const loadData = useCallback(async () => { try { - const [quotaData, cmdData, procData, metricsData] = await Promise.all([ + const [quotaData, consumData, cmdData, procData, metricsData] = await Promise.all([ api.getProvidersQuota().catch(() => null), + api.getProvidersConsumption().catch(() => null), api.getRecentCommands().catch(() => ({ commands: [] })), api.getRunningProcesses().catch(() => ({ processes: [] })), api.getSystemMetrics().catch(() => null), ]) setQuota(quotaData?.providers || []) + setConsumption(consumData?.providers || {}) setRecentCmds(cmdData.commands || []) setProcesses(procData.processes || []) if (metricsData) { @@ -91,7 +118,6 @@ export default function Dashboard({ api, refreshRef }) { }, [loadData, refreshRef]) const minimax = (quota || []).find(p => p.name === 'minimax') - const mimo = (quota || []).find(p => p.name === 'mimo') const EXCLUDE_CMDS = ['ls', 'cd', 'pwd', 'clear', 'exit', 'history', 'cat', 'echo', 'grep', 'export', 'alias', 'unalias', 'set', 'unset', 'source', '.', 'fg', 'bg', 'jobs', 'wait', 'true', 'false', 'yes', 'sleep', 'date', 'whoami', 'id', 'uname', 'hostname', 'uptime', 'df', 'free', 'top', 'htop', 'nano', 'vi', 'vim', 'less', 'more', 'tail', 'head', 'man', 'info', 'which', 'whereis', 'type', 'command', 'hash', 'builtin', 'help'] @@ -135,6 +161,12 @@ export default function Dashboard({ api, refreshRef }) { }) })() + const providerEntries = consumption ? Object.entries(consumption) : [] + const colors = ['var(--accent)', '#34d399', '#a78bfa', '#f59e0b', '#f472b6'] + const maxDaily = providerEntries.length > 0 + ? Math.max(...providerEntries.map(([, p]) => Math.max(...(p.daily || []).map(d => d.tokens), 0)), 1) + : 1 + return (
{/* CPU */} @@ -165,43 +197,36 @@ export default function Dashboard({ api, refreshRef }) {
- {/* API Quota */} + {/* Consommation */}
- API Quota + Consommation + 7j
-
- {minimax && minimax.data?.models?.map((m, i) => ( -
- {String(m.model).replace('MiniMax-', '')} -
-
+
+ {providerEntries.length === 0 && ( + Aucune donnée + )} + {providerEntries.map(([name, p], pi) => ( +
+
+ + {name.toUpperCase()} + + + {formatTokens(p.total_tokens)} tokens · {p.total_requests} req + +
+ +
+ {(p.daily || []).map((d, i) => ( + + {d.date.slice(5)} {formatTokens(d.tokens)} + + ))}
- {m.used}/{m.total}
))} - {minimax && minimax.data?.models?.length === 0 && ( -
- MiniMax - {minimax.error || 'no data'} -
- )} - {mimo && mimo.data?.models?.map((m, i) => ( -
- {String(m.model).replace('MiMo-', '')} -
-
-
- {m.used}/{m.total} -
- ))} - {mimo && !mimo.data?.models?.length && ( -
- MiMo - {mimo.error || (mimo.healthy ? '✓ configured' : 'no key')} -
- )} - {!minimax && !mimo && No providers}
diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index 2d14767..3b6f346 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -9,10 +9,14 @@ import { ImageAddon } from '@xterm/addon-image' import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot } from 'lucide-react' import '@xterm/xterm/css/xterm.css' import { useI18n } from '../i18n' +import mermaid from 'mermaid' + +mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' }) const AI_TAB_ID = 0 const MAX_TABS = 7 const SHELL_MAX_TOKENS = 100000 +const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change'] const TABS_STORAGE_KEY = 'muyue_shell_tabs' const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers' @@ -51,6 +55,15 @@ function formatText(text) { let html = text .replace(/&/g, '&').replace(//g, '>') + html = html.replace(/^(\|.+\|)\n(\|[\s\-:|]+\|)\n((?:\|.+\|\n?)+)/gm, (match, headerRow, sepRow, bodyRows) => { + const headers = headerRow.split('|').filter(c => c.trim() !== '').map(c => `${c.trim()}`).join('') + const rows = bodyRows.trim().split('\n').map(row => { + const cells = row.split('|').filter(c => c.trim() !== '').map(c => `${c.trim()}`).join('') + return `${cells}` + }).join('') + return `${headers}${rows}
` + }) + html = html .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/`([^`]+)`/g, '$1') @@ -63,8 +76,8 @@ function formatText(text) { html = html .replace(/\s*/g, '
') - .replace(/\s*(|<\/table>)\s*/g, '$1') .replace(/\s+on\w+=["'][^"']*["']/gi, '') .replace(/javascript:/gi, '') .replace(/data:/gi, '') @@ -200,11 +213,13 @@ function getTheme(themeName) { } function createTerminal(container, settings = {}) { + console.log('[Shell] createTerminal called with settings:', JSON.stringify({ fontSize: settings.fontSize, fontFamily: settings.fontFamily?.slice(0, 30), theme: settings.theme })) const theme = getTheme(settings.theme || 'system') + const actualFontSize = settings.fontSize || 14 const term = new XTerm({ cursorBlink: true, allowProposedApi: true, - fontSize: settings.fontSize || 14, + fontSize: actualFontSize, fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace", theme, allowTransparency: false, @@ -404,12 +419,13 @@ export default function Shell({ api }) { theme: 'system', }) + const [configLoaded, setConfigLoaded] = useState(false) const [showSearch, setShowSearch] = useState(false) const [searchText, setSearchText] = useState('') const searchInputRef = useRef(null) const searchDecorationsRef = useRef(null) const [zoomLevel, setZoomLevel] = useState(0) - const baseFontSizeRef = useRef(12) + const baseFontSizeRef = useRef(14) useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings]) @@ -450,6 +466,7 @@ export default function Shell({ api }) { const [analyzing, setAnalyzing] = useState(false) const [showAnalysis, setShowAnalysis] = useState(false) const [analysisContent, setAnalysisContent] = useState('') + const [sudoModal, setSudoModal] = useState(null) const aiMessagesRef = useRef(null) const aiLoadedRef = useRef(false) const aiLoadingRef = useRef(false) @@ -473,14 +490,10 @@ export default function Shell({ api }) { api.getShellChatHistory().then(d => { if (d.messages && d.messages.length > 0) { setAiMessages(d.messages) - } else { - setAiMessages([{ role: 'assistant', content: t('shell.aiWelcome') || 'SystĂšme Analyste prĂȘt. Tapez /help pour les commandes.' }]) } setAiTokens(d.tokens || 0) setAiAtLimit(d.at_limit || false) - }).catch(() => { - setAiMessages([{ role: 'assistant', content: 'SystĂšme Analyste prĂȘt.' }]) - }) + }).catch(() => {}) }, []) useEffect(() => { @@ -494,14 +507,21 @@ export default function Shell({ api }) { setSystemTerminals(d.system || []) }).catch(() => {}) api.getConfig().then(d => { + console.log('[Shell] config response terminal:', JSON.stringify(d?.terminal)) if (d.terminal) { - setTerminalSettings({ - fontSize: d.terminal.font_size || 14, - fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace", - theme: d.terminal.theme || 'system', - }) + const fontSize = d.terminal.font_size || 14 + const fontFamily = d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace" + const theme = d.terminal.theme || 'system' + console.log('[Shell] setting fontSize to:', fontSize, 'from config') + setTerminalSettings({ fontSize, fontFamily, theme }) + settingsRef.current = { fontSize, fontFamily, theme } + baseFontSizeRef.current = fontSize + } else { + console.log('[Shell] no terminal config in response, using defaults') } - }).catch(() => {}) + setConfigLoaded(true) + console.log('[Shell] configLoaded = true, settingsRef:', JSON.stringify(settingsRef.current)) + }).catch((err) => { console.warn('[Shell] getConfig failed:', err); setConfigLoaded(true) }) }, []) const initTerminal = useCallback((tabId, tab) => { @@ -512,6 +532,7 @@ export default function Shell({ api }) { const s = settingsRef.current const effectiveFontSize = s.fontSize + zoomLevel * 2 + console.log(`[Shell] initTerminal tab=${tabId}: settingsRef.fontSize=${s.fontSize}, zoomLevel=${zoomLevel}, effectiveFontSize=${effectiveFontSize}`) const { term, fitAddon, searchAddon } = createTerminal(container, { fontSize: effectiveFontSize, fontFamily: s.fontFamily, @@ -621,6 +642,7 @@ export default function Shell({ api }) { }, []) const initPendingTabs = useCallback(() => { + if (!configLoaded) return for (const tab of tabsRef.current._tabList || []) { if (!tabsRef.current[tab.id]) { const container = document.getElementById(`terminal-${tab.id}`) @@ -641,7 +663,7 @@ export default function Shell({ api }) { } }, 150) }) - }, [initTerminal]) + }, [initTerminal, configLoaded]) useEffect(() => { tabsRef.current._tabList = tabs @@ -689,6 +711,7 @@ export default function Shell({ api }) { } if (!tabsRef.current[tab.id]) { + console.log(`[Shell] tryInitTab: calling initTerminal for tab ${tab.id}, configLoaded=${configLoaded}`) initTerminal(tab.id, tab) } @@ -707,8 +730,9 @@ export default function Shell({ api }) { }) } + console.log(`[Shell] init effect: tabs=${tabs.length}, configLoaded=${configLoaded}`) for (const tab of tabs) { - if (!tabsRef.current[tab.id]) { + if (configLoaded && !tabsRef.current[tab.id]) { tryInitTab(tab, 0) } } @@ -729,7 +753,7 @@ export default function Shell({ api }) { pending.forEach(clearTimeout) observer?.disconnect() } - }, [tabs, initTerminal, initPendingTabs]) + }, [tabs, initTerminal, initPendingTabs, configLoaded]) useEffect(() => { const entry = tabsRef.current[activeTab] @@ -976,9 +1000,10 @@ export default function Shell({ api }) { if (entry) entry.term.focus() }, []) - const _sendAiMessage = useCallback(async (text, fromEvent = false) => { - if (!text || !text.trim() || aiLoadingRef.current || aiAtLimit) return + const _sendAiMessage = useCallback(async (text, fromEvent = false, isAnalysis = false) => { + if (!text || !text.trim() || aiLoadingRef.current) return const trimmed = text.trim() + if (aiAtLimit && !trimmed.startsWith('/')) return aiLoadingRef.current = true if (!fromEvent) { @@ -986,10 +1011,21 @@ export default function Shell({ api }) { setTimeout(() => focusAiTerminal(), 0) } + const isSlashCommand = (t) => /^\/(clear|help|model(?:\s+\S+)?)$/.test(t) + + if (trimmed.startsWith('/') && !isSlashCommand(trimmed)) { + setAiMessages(prev => [...prev, + { role: 'user', content: trimmed }, + { role: 'assistant', content: 'Commande inconnue. Tapez `/help` pour la liste des commandes.' } + ]) + aiLoadingRef.current = false + return + } + if (trimmed === '/clear') { try { await api.clearShellChat() - setAiMessages([{ role: 'assistant', content: t('shell.aiWelcome') || 'Contexte effacĂ©. PrĂȘt.' }]) + setAiMessages([]) setAiTokens(0) setAiAtLimit(false) } catch {} @@ -1000,15 +1036,50 @@ export default function Shell({ api }) { if (trimmed === '/help') { setAiMessages(prev => [...prev, { role: 'user', content: trimmed }, - { role: 'assistant', content: 'Commandes disponibles:\n‱ /clear — Effacer la conversation\n‱ /help — Afficher l\'aide\n\nJe peux exĂ©cuter des commandes via l\'outil terminal. Les blocs de code proposĂ©s peuvent aussi ĂȘtre copiĂ©s ou envoyĂ©s directement au terminal actif.' } + { role: 'assistant', content: '## Commandes Terminal\n\n- `/clear` — Effacer la conversation\n- `/help` — Afficher cette aide\n- `/model` — Afficher le provider et modĂšle actifs\n- `/model change` — Basculer entre les providers disponibles' } ]) aiLoadingRef.current = false return } + if (trimmed === '/model' || trimmed === '/model change') { + if (trimmed === '/model change') { + api.getProviders().then(data => { + const providers = data.providers || [] + const minimax = providers.find(p => p.name.toUpperCase() === 'MINIMAX') + const mimo = providers.find(p => p.name.toUpperCase() === 'MIMO') + if (!minimax || !mimo) { + setAiMessages(prev => [...prev, { role: 'assistant', content: 'MiniMax et MiMo doivent ĂȘtre configurĂ©s pour utiliser `/model change`.' }]) + return + } + const active = providers.find(p => p.active) + const activeName = active ? active.name.toUpperCase() : '' + const switchTo = activeName === 'MINIMAX' ? 'MIMO' : 'MINIMAX' + const target = switchTo === 'MINIMAX' ? minimax : mimo + api.saveProvider({ name: target.name, active: true }).then(() => { + setAiMessages(prev => [...prev, { role: 'assistant', content: `✓ Provider changĂ©: **${target.name}** (${target.model})` }]) + }).catch(() => { + setAiMessages(prev => [...prev, { role: 'assistant', content: 'Erreur lors du changement de provider.' }]) + }) + }).catch(() => { + setAiMessages(prev => [...prev, { role: 'assistant', content: 'Erreur: impossible de rĂ©cupĂ©rer les providers' }]) + }) + } else { + api.getProviders().then(data => { + const active = data.providers?.find(p => p.active) + const modelMsg = active ? `**${active.name}** — ${active.model}` : 'Aucun provider actif configurĂ©' + setAiMessages(prev => [...prev, { role: 'assistant', content: modelMsg }]) + }).catch(() => { + setAiMessages(prev => [...prev, { role: 'assistant', content: 'Erreur: impossible de rĂ©cupĂ©rer les providers' }]) + }) + } + aiLoadingRef.current = false + return + } + const currentTab = activeTabRef.current console.log(`[Shell] _sendAiMessage: activeTab=${currentTab}, fromEvent=${fromEvent}, text="${trimmed.slice(0, 50)}"`) - setAiMessages(prev => [...prev, { role: 'user', content: trimmed, _tabId: currentTab }]) + setAiMessages(prev => [...prev, { role: 'user', content: trimmed, _tabId: currentTab, _analysis: isAnalysis || undefined }]) setAiLoading(true) try { @@ -1026,6 +1097,9 @@ export default function Shell({ api }) { return } if (event && event.tool_result) { + if (event.tool_result.sudo_blocked) { + setSudoModal({ command: event.tool_result.command || event.tool_result.content }) + } const idx = toolCalls.findIndex(tc => tc.call && tc.call.tool_call_id === event.tool_result.tool_call_id) if (idx >= 0) { toolCalls[idx] = { ...toolCalls[idx], result: event.tool_result } @@ -1091,23 +1165,20 @@ export default function Shell({ api }) { return () => window.removeEventListener('ask-ai-terminal', handler) }, [_sendAiMessage]) - const handleAnalyze = async () => { - setAnalyzing(true) - setAiMessages(prev => [...prev, { role: 'system', content: 'Analyse du systĂšme en cours...' }]) - try { - const d = await api.analyzeSystem() - if (d.analysis) { - setAnalysisContent(d.analysis) - localStorage.setItem('shell_analysis', d.analysis) - } - setAiMessages(prev => [...prev.filter(m => m.content !== 'Analyse du systĂšme en cours...'), { - role: 'system', - content: 'Analyse systĂšme terminĂ©e et sauvegardĂ©e. Le contexte systĂšme est maintenant disponible.' - }]) - } catch (err) { - setAiMessages(prev => prev.filter(m => m.content !== 'Analyse du systĂšme en cours...')) - } - setAnalyzing(false) + const handleAnalyze = () => { + _sendAiMessage(`Fais une analyse complĂšte du systĂšme. Utilise l'outil terminal pour explorer et rĂ©dige un rapport structurĂ© en markdown. Couvre: + +1. **OS & MatĂ©riel** — distrib, kernel, CPU, RAM, GPU, hostname +2. **Disque & Partitions** — occupation, points de montage, inodes +3. **RĂ©seau** — interfaces, IP, ports en Ă©coute, DNS, pare-feu, SSH actif ? +4. **Utilisateurs & SĂ©curitĂ©** — utilisateurs, groupes sudo, last logins, fail2ban, rĂšgles sudo +5. **Services & Processus** — systemd actifs, top processus par CPU/RAM, load average, uptime +6. **Outils installĂ©s** — IDE (VSCode, JetBrains...), langages (go, python, node, rust...), gestionnaire de paquets, conteneurs (docker, podman), git, outils CLI +7. **Environnement utilisateur** — shell, $HOME (arborescence top-level, nombre de fichiers/projects), dotfiles, config notable (.bashrc, .zshrc, starship) +8. **Paquets & Mises Ă  jour** — paquets obsolĂštes, kernel Ă  jour, security patches +9. **Recommandations** — outils manquants, optimisations, points d'attention + +Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. Le rapport fera office de carte d'identitĂ© du systĂšme (max ~40k tokens).`, true, true) } return ( @@ -1291,7 +1362,37 @@ export default function Shell({ api }) { setAiInput(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleAiSend() } }} + onKeyDown={e => { + if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleAiSend(); return } + if (e.key === 'Tab') { + e.preventDefault() + const val = aiInput + const pos = e.target.selectionStart + const before = val.slice(0, pos) + const afterSlash = before.match(/\/[\w ]*$/) + if (afterSlash) { + const partial = afterSlash[0] + const matches = SHELL_AI_COMMANDS.filter(c => c.startsWith(partial) && c !== partial) + if (matches.length >= 1) { + let completed = matches[0] + for (const m of matches) { + while (!m.startsWith(completed)) completed = completed.slice(0, -1) + } + if (completed === partial && matches.length === 1) completed = matches[0] + if (completed.length > partial.length) { + const suffix = completed[completed.length - 1] === ' ' ? '' : (matches.length === 1 ? ' ' : '') + completed += suffix + const newText = val.slice(0, pos - afterSlash[0].length) + completed + val.slice(pos) + setAiInput(newText) + requestAnimationFrame(() => { + e.target.selectionStart = e.target.selectionEnd = pos - afterSlash[0].length + completed.length + }) + } + } + } + return + } + }} placeholder={aiAtLimit ? '/clear pour continuer' : t('shell.askAi')} disabled={aiAtLimit && aiInput !== '/clear'} /> @@ -1308,7 +1409,12 @@ export default function Shell({ api }) {
{renderContent(analysisContent).map((part, i) => - part.type === 'code' ? ( + part.type === 'code' && part.lang === 'mermaid' ? ( +
+
mermaid
+ +
+ ) : part.type === 'code' ? (
{part.lang &&
{part.lang}
}
{part.content}
@@ -1371,6 +1477,22 @@ export default function Shell({ api }) {
)} + + {sudoModal && ( +
setSudoModal(null)}> +
e.stopPropagation()}> +
Commande bloquée
+
+

L'IA a tenté d'exécuter une commande nécessitant des privilÚges administrateur :

+
{sudoModal.command}
+

La commande a été bloquée. L'IA en a été informée et cherchera une alternative.

+
+
+ +
+
+
+ )}
) } @@ -1409,12 +1531,36 @@ function ShellToolBlock({ call, result }) { ) } +let mermaidIdCounter = 0 + +function MermaidBlock({ code }) { + const ref = useRef(null) + const [svg, setSvg] = useState('') + const [error, setError] = useState(false) + + useEffect(() => { + let cancelled = false + const id = `mermaid-${++mermaidIdCounter}` + mermaid.render(id, code).then(({ svg }) => { + if (!cancelled) setSvg(svg) + }).catch(() => { + if (!cancelled) setError(true) + }) + return () => { cancelled = true } + }, [code]) + + if (error) return
{code}
+ if (!svg) return
Chargement du diagramme...
+ return
+} + function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) { const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant' const content = msg.content || '' + const [copiedIdx, setCopiedIdx] = useState(null) if (role === 'user') { - return
{content}
+ return
} if (role === 'system') { @@ -1426,16 +1572,16 @@ function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) { let displayContent = content let streamingToolCalls = msg._toolCalls || null - if (!streamingToolCalls) { - try { - const parsed = JSON.parse(content) - if (parsed && Array.isArray(parsed.tool_calls)) { + try { + const parsed = JSON.parse(content) + if (parsed && Array.isArray(parsed.tool_calls)) { + if (!streamingToolCalls) { parsedToolCalls = parsed.tool_calls parsedToolResults = parsed.tool_results || null - displayContent = parsed.content || '' } - } catch {} - } + displayContent = parsed.content || '' + } + } catch {} const parts = renderContent(displayContent) @@ -1454,14 +1600,26 @@ function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) { return })} {parts.map((part, i) => { + if (part.type === 'code' && part.lang === 'mermaid') { + return ( +
+
mermaid
+ +
+ ) + } if (part.type === 'code') { return (
{part.lang &&
{part.lang}
}
{part.content}
- +
+ +
+ ) + } + return ( +
+
+ {part.lang && {part.lang}} + +
+
{part.content}
+
+ ) +} + function FeedItem({ msg }) { const isUser = msg.role === 'user' const isSystem = msg.role === 'system' const rank = getRank(msg.role) + const [copiedIdx, setCopiedIdx] = useState(null) const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '' @@ -226,10 +297,7 @@ function FeedItem({ msg }) {
{renderContent(cleanContent).map((part, i) => part.type === 'code' ? ( -
- {part.lang &&
{part.lang}
} -
{part.content}
-
+ ) : ( ) @@ -245,6 +313,7 @@ function StreamingItem({ content, thinking, toolCalls }) { const rank = RANKS.general const cleanContent = content.replace(/]*>[\s\S]*?<\/think>/gi, '') const hasToolCalls = toolCalls && toolCalls.length > 0 + const [copiedIdx, setCopiedIdx] = useState(null) const renderedContent = useMemo(() => { if (!cleanContent) return [] @@ -281,10 +350,7 @@ function StreamingItem({ content, thinking, toolCalls }) {
{renderedContent.map((part, i) => part.type === 'code' ? ( -
- {part.lang &&
{part.lang}
} -
{part.content}
-
+ ) : ( ) @@ -309,6 +375,7 @@ export default function Studio({ api }) { const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 }) const [contextCollapsed, setContextCollapsed] = useState(false) const [messagesCollapsed, setMessagesCollapsed] = useState(false) + const [sudoModal, setSudoModal] = useState(null) const messagesEnd = useRef(null) const feedRef = useRef(null) const textareaRef = useRef(null) @@ -404,7 +471,7 @@ export default function Studio({ api }) { const text = input.trim() setInput('') - const isSlashCommand = (t) => /^\/(clear|help|summarize|export|model(?:\s+\S+)?|plan\s+.+)$/.test(t) + const isSlashCommand = (t) => /^\/(clear|help|summarize|model(?:\s+\S+)?)$/.test(t) if (text.startsWith('/') && !isSlashCommand(text)) { setMessages(prev => [...prev, { id: Date.now().toString(), role: 'user', content: text, time: new Date().toISOString() }]) @@ -424,19 +491,8 @@ export default function Studio({ api }) { '- `/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', '- `/model` - Afficher le provider et modĂšle actifs', - '- `/model change` - Basculer entre MiniMax et ZAI', - '', - '## Tools disponibles', - '- Terminal - ExĂ©cuter des commandes', - '- read_file - Lire des fichiers', - '- list_files - Lister des fichiers', - '- search_files - Rechercher des fichiers', - '- grep_content - Rechercher dans le contenu', - '- get_config - Lire la configuration', - '- web_fetch - RĂ©cupĂ©rer une page web', + '- `/model change` - Basculer entre MiniMax et MiMo', ].join('\n') setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: helpMsg, time: new Date().toISOString() }]) return @@ -481,31 +537,6 @@ export default function Studio({ api }) { return } - if (text.startsWith('/plan ')) { - const objective = text.slice(6).trim() - if (!objective) { - setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'Usage: `/plan `\nEx: `/plan crĂ©er un fichier de test`', time: new Date().toISOString() }]) - return - } - setInput(`CrĂ©e un plan structurĂ© en Ă©tapes numĂ©rotĂ©es pour: ${objective}. Chaque Ă©tape devrait avoir une estimation de complexitĂ© et de temps.`) - handleSend() - return - } - - if (text === '/export') { - api.getChatHistory().then(data => { - let markdown = '# Conversation Export\n\n' - data.messages?.forEach((msg, i) => { - const roleLabel = msg.role === 'user' ? 'đŸ‘€' : (msg.role === 'assistant' ? 'đŸ€–' : '⚙') - markdown += `## [${i + 1}] ${roleLabel} ${msg.role}\n${msg.content}\n\n---\n\n` - }) - setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'Conversation exportĂ©e:\n```markdown\n' + markdown + '```', time: new Date().toISOString() }]) - }).catch(() => { - setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'Erreur: impossible d\'exporter la conversation', time: new Date().toISOString() }]) - }) - return - } - const userMsg = { id: Date.now().toString(), role: 'user', content: text, time: new Date().toISOString() } setMessages(prev => [...prev, userMsg]) setLoading(true) @@ -537,6 +568,9 @@ export default function Studio({ api }) { return } if (event && event.tool_result) { + if (event.tool_result.sudo_blocked) { + setSudoModal({ command: event.tool_result.command || event.tool_result.content }) + } const idx = toolCalls.findIndex(tc => tc.call && tc.call.tool_call_id === event.tool_result.tool_call_id) if (idx >= 0) { toolCalls[idx] = { ...toolCalls[idx], result: event.tool_result } @@ -602,7 +636,7 @@ export default function Studio({ api }) { } }, []) - const COMMANDS = ['/clear', '/summarize', '/help', '/plan', '/export', '/model', '/model change'] + const COMMANDS = ['/clear', '/summarize', '/help', '/model', '/model change'] const handleKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -744,9 +778,25 @@ export default function Studio({ api }) { )}
- {t('studio.inputHint')} · /clear /summarize /help /plan /export /model /model change + {t('studio.inputHint')} · /clear /summarize /help /model
+ + {sudoModal && ( +
setSudoModal(null)}> +
e.stopPropagation()}> +
Commande bloquée
+
+

L'IA a tenté d'exécuter une commande nécessitant des privilÚges administrateur :

+
{sudoModal.command}
+

La commande a été bloquée. L'IA en a été informée et cherchera une alternative.

+
+
+ +
+
+
+ )}
) } diff --git a/web/src/styles/global.css b/web/src/styles/global.css index f168472..50eae3b 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -458,6 +458,8 @@ input::placeholder { color: var(--text-disabled); } .shell-ai-token-text { font-size: 10px; font-family: var(--font-mono); color: var(--text-tertiary); white-space: nowrap; } .ai-panel-messages { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; } .ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; } +.ai-message.user { background: var(--bg-elevated); border-left: 3px solid var(--accent-bright); color: var(--text-primary); } +.ai-message.user.analysis { border-left-color: var(--info); background: color-mix(in srgb, var(--info) 8%, var(--bg-elevated)); } .ai-message.assistant { background: var(--bg-card); border-left: 3px solid var(--accent); } .ai-message.system { background: var(--bg-elevated); border-left: 3px solid var(--info); font-style: italic; color: var(--text-tertiary); font-size: 12px; } .ai-message.assistant { background: var(--bg-card); border-left: 3px solid var(--accent); } @@ -492,6 +494,23 @@ input::placeholder { color: var(--text-disabled); } } .shell-code-actions button:last-child { border-right: none; } .shell-code-actions button:hover { background: var(--accent-bg); color: var(--accent); } +.shell-code-actions button.copied { background: var(--accent-bg); color: var(--accent); animation: copy-flash 0.3s ease; } + +.shell-mermaid-container { padding: 12px; background: var(--bg); overflow-x: auto; display: flex; justify-content: center; } +.shell-mermaid-container svg { max-width: 100%; height: auto; } +.shell-mermaid-loading { padding: 16px; text-align: center; color: var(--text-tertiary); font-size: 12px; } +.shell-mermaid-error { padding: 10px 12px; color: var(--accent-bright); font-family: var(--font-mono); font-size: 12px; white-space: pre-wrap; } + +.ai-message table { width: 100%; border-collapse: collapse; margin: 6px 0; font-size: 12px; } +.ai-message th { background: var(--bg-surface); padding: 6px 10px; text-align: left; font-weight: 600; border: 1px solid var(--border); color: var(--text-secondary); } +.ai-message td { padding: 5px 10px; border: 1px solid var(--border); color: var(--text-primary); } +.ai-message tr:nth-child(even) td { background: var(--bg-surface); } + +@keyframes copy-flash { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); background: color-mix(in srgb, var(--accent) 20%, transparent); } + 100% { transform: scale(1); } +} .shell-analysis-modal { background: var(--bg-elevated); border: 1px solid var(--border); @@ -720,6 +739,25 @@ input::placeholder { color: var(--text-disabled); } white-space: nowrap; } +/* Consumption */ +.dash-consumption-list { display: flex; flex-direction: column; gap: 10px; max-height: 270px; overflow-y: auto; } +.dash-consumption-provider { display: flex; flex-direction: column; gap: 4px; } +.dash-consumption-head { display: flex; align-items: center; justify-content: space-between; } +.dash-consumption-name { + font-size: 11px; font-weight: 700; letter-spacing: 0.5px; +} +.dash-consumption-total { + font-size: 10px; font-family: var(--font-mono); color: var(--text-tertiary); +} +.dash-consumption-days { + display: flex; gap: 4px; flex-wrap: wrap; +} +.dash-consumption-day { + font-size: 9px; font-family: var(--font-mono); color: var(--text-tertiary); + background: var(--bg-input); padding: 1px 5px; border-radius: 4px; +} +.dash-consumption-day strong { color: var(--text-secondary); } + /* Processes */ .dash-proc-list { display: flex; flex-direction: column; gap: 4px; max-height: 270px; overflow-y: auto; } .dash-proc-row { @@ -933,11 +971,33 @@ input::placeholder { color: var(--text-disabled); } background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin: 8px 0; } +.studio-code-header { + display: flex; align-items: center; justify-content: space-between; + background: var(--bg-surface); border-bottom: 1px solid var(--border); +} .studio-code-block pre { padding: 12px 16px; font-family: var(--font-mono); font-size: 13px; line-height: 1.5; overflow-x: auto; color: var(--text-primary); margin: 0; } .studio-code-lang { padding: 4px 12px; font-size: 11px; font-weight: 600; color: var(--text-tertiary); - background: var(--bg-surface); border-bottom: 1px solid var(--border); text-transform: uppercase; letter-spacing: 0.5px; + background: var(--bg-surface); text-transform: uppercase; letter-spacing: 0.5px; } +.studio-copy-btn { + padding: 3px 10px; font-size: 10px; font-weight: 600; color: var(--text-tertiary); + background: transparent; border: none; border-left: 1px solid var(--border); + cursor: pointer; transition: all 0.15s; font-family: var(--font-sans); + white-space: nowrap; +} +.studio-copy-btn:hover { background: var(--accent-bg); color: var(--accent); } +.studio-copy-btn.copied { background: var(--accent-bg); color: var(--accent); } + +.studio-mermaid-container { padding: 12px; background: var(--bg); overflow-x: auto; display: flex; justify-content: center; } +.studio-mermaid-container svg { max-width: 100%; height: auto; } +.studio-mermaid-loading { padding: 12px; text-align: center; color: var(--text-tertiary); font-size: 12px; } +.studio-mermaid-error { padding: 10px 12px; color: var(--accent-bright); font-family: var(--font-mono); font-size: 12px; white-space: pre-wrap; } + +.feed-content table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 13px; } +.feed-content th { background: var(--bg-surface); padding: 6px 12px; text-align: left; font-weight: 600; border: 1px solid var(--border); color: var(--text-secondary); } +.feed-content td { padding: 5px 12px; border: 1px solid var(--border); color: var(--text-primary); } +.feed-content tr:nth-child(even) td { background: var(--bg-surface); } .inline-code { background: var(--bg-input); padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); font-size: 13px; color: var(--accent-muted); } .msg-h3 { font-size: 16px; font-weight: 700; color: var(--text-primary); margin: 10px 0 4px; display: block; } .msg-h4 { font-size: 14px; font-weight: 700; color: var(--text-secondary); margin: 8px 0 3px; display: block; }