diff --git a/CRUSH_ARCHITECTURE_REPORT.md b/CRUSH_ARCHITECTURE_REPORT.md new file mode 100644 index 0000000..5ac3af2 --- /dev/null +++ b/CRUSH_ARCHITECTURE_REPORT.md @@ -0,0 +1,1073 @@ +# Rapport d'Architecture : CharmBracelet Crush + +> Analyse complète de l'application [charmbracelet/crush](https://github.com/charmbracelet/crush.git) — comment elle communique avec les IA, gère les outils, optimise les tokens et les performances. + +--- + +## Table des matières + +1. [Architecture globale](#1-architecture-globale) +2. [Ce qui est envoyé à l'IA](#2-ce-qui-est-envoyé-à-lia) +3. [Système de prompts](#3-système-de-prompts) +4. [Système de résumé / compaction](#4-système-de-résumé--compaction) +5. [Fonctionnement des outils (Tools)](#5-fonctionnement-des-outils-tools) +6. [Optimisation de la consommation de tokens](#6-optimisation-de-la-consommation-de-tokens) +7. [Optimisations de performance](#7-optimisations-de-performance) +8. [Système de permissions](#8-système-de-permissions) +9. [Providers et modèles](#9-providers-et-modèles) +10. [Système de skills](#10-système-de-skills) +11. [Intégration LSP](#11-intégration-lsp) +12. [Intégration MCP](#12-intégration-mcp) +13. [Structure de la base de données](#13-structure-de-la-base-de-données) +14. [Fichiers clés à explorer](#14-fichiers-clés-à-explorer) +15. [Leçons pour notre application](#15-leçons-pour-notre-application) + +--- + +## 1. Architecture globale + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ TUI / CLI │────▶│ Backend │────▶│ Coordinator │ +│ (UI chat) │ │ (workspace) │ │ (orchestrateur) │ +└──────────────┘ └──────────────┘ └────────┬─────────┘ + │ + ┌────────▼─────────┐ + │ SessionAgent │ + │ (moteur IA) │ + └────────┬─────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ + ┌────────▼──────┐ ┌────────▼──────┐ ┌────────▼──────┐ + │ fantasy │ │ SQLite DB │ │ Tools │ + │ (LLM client) │ │ (messages) │ │ (22+ outils) │ + └───────────────┘ └───────────────┘ └───────────────┘ +``` + +**Flow de données complet :** + +``` +User Prompt + → Backend.SendMessage() + → Coordinator.Run() + → UpdateModels() (recharge models + tools) + → mergeCallOptions() (merge JSON des options provider) + → SessionAgent.Run() + → preparePrompt() (construit l'historique, filtre les orphelins) + → fantasy.Agent.Stream() avec : + - System prompt = coder.md.tpl + instructions MCP + - System prompt prefix = prefix provider + - Messages = historique filtré (tronqué au résumé si existe) + - Files = pièces jointes binaires + - Tools = ensemble filtré d'outils + - Provider options = options merged Anthropic/OpenAI/Google/etc. + - Cache control = ephemeral sur dernier system + 2 derniers messages + → Auto-summarize si fenêtre de contexte quasi pleine + → Queue + recurse pour messages en attente +``` + +### Fichiers clés de l'architecture + +| Fichier | Rôle | +|---------|------| +| `internal/agent/agent.go` | Moteur principal — construction des messages, streaming, résumé | +| `internal/agent/coordinator.go` | Orchestration — création agents, assembly tools/models | +| `internal/agent/prompts.go` | Factory de prompts système | +| `internal/agent/templates/coder.md.tpl` | Template du prompt système principal (405 lignes) | +| `internal/backend/backend.go` | Gestion des workspaces | +| `internal/backend/agent.go` | API transport-agnostic vers le coordinator | + +--- + +## 2. Ce qui est envoyé à l'IA + +### 2.1 System Prompt + +Le system prompt est construit à partir du template `coder.md.tpl` et contient : + +``` +System Message: +├── — 13 règles absolues (read before edit, autonomous, etc.) +├── — Style de réponse (concis, <4 lignes) +├── — Séquence de travail (search → read → edit → test) +├── — Décisions autonomes vs. bloquantes +├── — Règles d'édition (exact match, whitespace) +├── — Checklist précision +├── — Complétion exhaustive des tâches +├── — Stratégies de récupération d'erreurs +├── — Gestion des fichiers mémoire +├── — Conventions de code +├── — Règles de test après changements +├── — Utilisation des outils +├── — Proactivité vs. intention utilisateur +├── — Format des réponses finales +├── — Variables dynamiques : +│ ├── Working Directory: {{.WorkingDir}} +│ ├── Is Git Repo: {{.IsGitRepo}} +│ ├── Platform: {{.Platform}} +│ ├── Date: {{.Date}} +│ └── Git Status (branch, git status --short | head -20, git log --oneline -n 3) +├── (optionnel) — État des serveurs LSP +├── — Skills disponibles en XML +├── — Instructions d'utilisation des skills +├── — Fichiers de contexte (crush.md, AGENTS.md, etc.) +└── — Instructions MCP injectées dynamiquement +``` + +### 2.2 Historique des messages + +Chaque appel à l'IA inclut l'historique complet de la session, formaté comme : + +``` +Messages (array de fantasy.Message): +├── [0] User: "This is a reminder that your todo list is currently empty..." +├── [1] User: "Premier prompt de l'utilisateur" +├── [2] Assistant: "Réponse IA + tool_calls" +├── [3] Tool: "Résultat du tool call" +├── [4] Assistant: "Réponse suivante" +├── ... +└── [N] User: "Nouveau prompt" +``` + +**Types de contenu dans les messages :** +- `ReasoningContent` — Chaîne de pensée (pour modèles thinking) +- `TextContent` — Texte brut +- `ImageURLContent` — Images (URL ou base64) +- `BinaryContent` — Fichiers binaires +- `ToolCall` — Appel d'outil (ID, nom, input) +- `ToolResult` — Résultat d'outil (text, error, ou media) +- `Finish` — Métadonnées de fin (end_turn, max_tokens, tool_use, canceled, error) + +### 2.3 Fichiers joints + +Les pièces jointes sont traitées différemment selon leur type : +- **Fichiers texte** : contenus inline dans le prompt utilisateur via `PromptWithTextAttachments()` +- **Fichiers binaires** : envoyés comme `FilePart` séparés +- **Images** : envoyées comme `ImageURLContent` (base64) si le modèle les supporte + +### 2.4 Options provider fusionnées + +Les options sont fusionnées en 3 couches (le plus profond gagne) : + +``` +1. catwalk.Model.Options.ProviderOptions (défauts du catalogue) +2. ProviderConfig.ProviderOptions (config du provider) +3. SelectedModel.ProviderOptions (choix utilisateur) +→ JSON merge avec sémantique "deepest wins" +``` + +Options spécifiques par provider : +- **Anthropic** : thinking, reasoning_effort, beta headers (`interleaved-thinking-2025-05-14`) +- **OpenAI** : responses API, reasoning +- **Google** : thinking_config +- **OpenRouter** : reasoning, suffixe `:exacto` + +--- + +## 3. Système de prompts + +### 3.1 Templates embarqués + +| Template | But | Taille | +|----------|-----|--------| +| `coder.md.tpl` | Prompt système principal de l'agent coder | ~405 lignes | +| `task.md.tpl` | Prompt système des sous-agents | ~15 lignes | +| `initialize.md.tpl` | Prompt d'initialisation du codebase | Analyse + génération AGENTS.md | +| `summary.md` | Prompt de résumé/compaction | Sections structurées obligatoires | +| `title.md` | Génération de titre de session | ≤50 chars | +| `agent_tool.md` | Instructions du tool `agent` | Sous-agent read-only | +| `agentic_fetch.md` | Instructions du tool `agentic_fetch` | Sous-agent web | +| `agentic_fetch_prompt.md.tpl` | Prompt du sous-agent de fetch | Template dynamique | + +### 3.2 Prompt de résumé (`summary.md`) + +Ce prompt est crucial — il définit comment la conversation est compactée : + +**Sections obligatoires du résumé :** +1. **Current State** — État actuel de la tâche +2. **Files & Changes** — Tous les fichiers modifiés et les changements +3. **Technical Context** — Stack technique, dépendances, patterns +4. **Strategy & Approach** — Stratégie adoptée +5. **Exact Next Steps** — Prochaines étapes exactes + +**Philosophie :** *"No limit. Err on the side of too much detail rather than too little."* + +### 3.3 Prompt de sous-agent (`task.md.tpl`) + +Minimal et ciblé : +``` +Rules: +1. Be concise, direct +2. Share file names and paths +3. Use absolute paths only + + +Working Directory: {{.WorkingDir}} +Is Git Repo: {{.IsGitRepo}} +Platform: {{.Platform}} +Date: {{.Date}} + +``` + +### 3.4 Variables dynamiques injectées + +Le système injecte des données en temps réel via le template : + +```go +type PromptData struct { + WorkingDir string + IsGitRepo bool + Platform string + Date string + GitStatus string // branch + git status --short | head -20 + git log --oneline -n 3 + Config Config + AvailSkillXML string // XML des skills disponibles + ContextFiles string // Contenu des fichiers de contexte +} +``` + +### 3.5 Fichiers de contexte (Memory) + +Chemins par défaut explorés et injectés dans `` : +``` +.github/copilot-instructions.md +.cursorrules +.cursor/rules/ +CLAUDE.md +CLAUDE.local.md +GEMINI.md +crush.md +CRUSH.md +AGENTS.md +``` + +--- + +## 4. Système de résumé / compaction + +### 4.1 Déclenchement automatique + +Deux conditions de déclenchement vérifiées après **chaque étape** de l'agent : + +``` +remaining = contextWindow - (completionTokens + promptTokens) + +if contextWindow > 200,000: + threshold = 20,000 tokens (buffer fixe) +else: + threshold = 20% × contextWindow (ratio) + +if remaining ≤ threshold AND !disableAutoSummarize: + → DÉCLENCHE LE RÉSUMÉ + +if contextWindow == 0 (modèle inconnu/local): + → PAS de résumé auto (évite la troncature aveugle) +``` + +**Constantes clés :** +- `largeContextWindowThreshold = 200,000` +- `largeContextWindowBuffer = 20,000` +- `smallContextWindowRatio = 0.2` + +### 4.2 Processus de résumé + +``` +┌─────────────────────────────────────────────┐ +│ 1. Récupérer tous les messages de session │ +│ 2. Convertir en fantasy.Message[] │ +│ 3. Créer message assistant │ +│ avec IsSummaryMessage: true │ +│ 4. Streamer le résumé via summary.md prompt │ +│ + todo list courante │ +│ 5. Update session: │ +│ - SummaryMessageID = summary.ID │ +│ - CompletionTokens = résumé output │ +│ - PromptTokens = 0 │ +└─────────────────────────────────────────────┘ +``` + +### 4.3 Comment la compaction fonctionne au chargement + +```go +func getSessionMessages(session): + msgs = loadAllMessages(sessionID) + + if session.SummaryMessageID != "": + summaryIndex = findIndex(msgs, summaryMessageID) + msgs = msgs[summaryIndex:] // TRONQUE tout avant le résumé + msgs[0].Role = "user" // Change rôle assistant → user + + return msgs +``` + +**Résultat :** Seuls le résumé + les messages après le résumé sont envoyés à l'IA. Tout l'historique précédent est éliminé du contexte. + +### 4.4 Détection de boucle (Loop Detection) + +Mécanisme secondaire de déclenchement du résumé : + +``` +- Fenêtre glissante de 10 dernières étapes +- Signature = SHA-256(ToolName + \x00 + Input + \x00 + Output + \x00) +- Si une signature apparaît > 5 fois dans la fenêtre → BOUCLE DÉTECTÉE +- Déclenche le résumé + arrêt de l'agent +``` + +### 4.5 Reprise après interruption + +Si le résumé est déclenché pendant des appels d'outils en cours : +``` +Re-queue le prompt original avec : +"The previous session was interrupted because it got too long, +the initial user request was: " +``` + +--- + +## 5. Fonctionnement des outils (Tools) + +### 5.1 Liste complète des outils (22+) + +| Outil | Type | Séquentiel/Parallèle | But | +|-------|------|---------------------|-----| +| `bash` | Core | Séquentiel | Exécution de commandes shell | +| `edit` | Core | Séquentiel | Remplacement find-and-replace dans un fichier | +| `multiedit` | Core | Séquentiel | Multiples remplacements séquentiels | +| `write` | Core | Séquentiel | Création/écrasement de fichier | +| `view` | Core | Séquentiel | Lecture de fichier avec line numbers | +| `ls` | Core | Séquentiel | Arborescence de répertoire | +| `glob` | Core | Séquentiel | Recherche de fichiers par pattern | +| `grep` | Core | Séquentiel | Recherche dans le contenu de fichiers | +| `fetch` | Core | Parallèle | Fetch URL brut (text/markdown/html) | +| `agentic_fetch` | Core | Parallèle | Fetch IA avec extraction/résumé | +| `sourcegraph` | Core | Parallèle | Recherche de code sur GitHub | +| `download` | Core | Parallèle | Téléchargement de fichier | +| `agent` | Agent | Parallèle | Sous-agent de recherche/analyse | +| `todos` | Core | Séquentiel | Gestion de la todo list | +| `crush_info` | Core | Séquentiel | État runtime de Crush | +| `crush_logs` | Core | Séquentiel | Logs internes de Crush | +| `job_output` | Core | Séquentiel | Output de processus background | +| `job_kill` | Core | Séquentiel | Terminaison de processus background | +| `lsp_diagnostics` | LSP | Séquentiel | Diagnostics LSP | +| `lsp_references` | LSP | Séquentiel | Références LSP | +| `lsp_restart` | LSP | Séquentiel | Redémarrage LSP | +| `list_mcp_resources` | MCP | Parallèle | Liste ressources MCP | +| `read_mcp_resource` | MCP | Parallèle | Lecture ressource MCP | +| `mcp_{server}_{tool}` | MCP | Parallèle | Outils dynamiques MCP | + +### 5.2 Architecture d'un outil + +Chaque outil suit le pattern : + +```go +fantasy.NewAgentTool(name, description, params, func(ctx context.Context, params Params) (fantasy.ToolResponse, error) { + // 1. Validation des paramètres + // 2. Extraction du contexte (sessionID, messageID, workingDir) + // 3. Vérification de permission (si mutation) + // 4. Exécution de la logique + // 5. Retour de la réponse avec métadonnées +}) +``` + +**Deux constructeurs :** +- `fantasy.NewAgentTool` — Outil séquentiel (bloquant) +- `fantasy.NewParallelAgentTool` — Outil parallèle (peut tourner en //) + +### 5.3 Types de réponses + +```go +fantasy.NewTextResponse(content) // Succès texte +fantasy.NewTextErrorResponse(message) // Erreur (IsError=true) +fantasy.NewImageResponse(data, mimeType) // Image +fantasy.NewMediaResponse(data, mimeType) // Autre média +fantasy.WithResponseMetadata(resp, meta) // Attache métadonnées typées +``` + +### 5.4 Détails des outils clés + +#### `bash` — Exécution de commandes + +``` +Paramètres: {Description, Command, WorkingDir, RunInBackground, AutoBackgroundAfter} + +Sécurité: +├── safeCommands: ls, cat, head, tail, pwd, echo, which, env, git status/diff/log +│ → Pas de permission requise (read-only) +├── Banned: curl, wget, sudo, apt, npm, etc. +│ → Bloqué au niveau shell +├── Background: si RunInBackground=true ou > AutoBackgroundAfter (60s) +│ → Retourne ShellID pour suivi +└── Output: tronqué à 30,000 chars (début + fin + compte lignes tronquées) +``` + +#### `view` — Lecture de fichiers + +``` +Paramètres: {FilePath, Offset, Limit} + +Comportement: +├── crush: prefix → lecture depuis FS embarqué (skills builtins) +├── Chemins relatifs → résolus via SmartJoin(workingDir, ...) +├── Permission requise si hors workingDir +├── Max fichier: 100KB, défaut 2000 lignes +├── Images JPG/PNG/GIF/WebP → NewImageResponse +├── Ajoute numéros de ligne (padding 6 chars) +├── LSP: ouvre fichier, attend 300ms pour diagnostics +├── Enregistre lecture via filetracker.RecordRead() +└── Suggestions si fichier non trouvé +``` + +#### `edit` — Édition de fichiers + +``` +Paramètres: {FilePath, OldString, NewString, ReplaceAll} + +3 modes: +├── OldString="" → Créer nouveau fichier +├── NewString="" → Supprimer contenu +└── Les deux → Remplacer contenu + +Sécurité: +├── Doit avoir lu le fichier avant (filetracker check) +├── Fichier non modifié depuis la lecture (ModTime check) +├── OldString unique (sauf ReplaceAll=true) +├── Permission "write" avec diff affiché +├── Gestion CRLF automatique +└── LSP notifié après écriture +``` + +#### `grep` — Recherche de contenu + +``` +Paramètres: {Pattern, Path, Include, LiteralText} + +Optimisations: +├── ripgrep (rg --json) en priorité, fallback Go regex +├── Respecte .gitignore et .crushignore +├── Cache de regex compilés (csync.Map thread-safe) +├── Résultats triés par date de modification (plus récent d'abord) +├── Max 100 résultats +├── Lignes tronquées à 500 chars +└── Timeout configurable +``` + +### 5.5 Outils spéciaux + +#### `agent` — Sous-agent + +``` +Flow: +1. Valide params.Prompt non vide +2. Crée une session enfant (CreateTaskSession) +3. Lance un SessionAgent séparé (NonInteractive: true) +4. Outils du sous-agent: glob, grep, ls, sourcegraph, view (READ-ONLY) +5. Propage le coût de la session enfant → session parent +``` + +#### `agentic_fetch` — Fetch intelligent + +``` +Flow: +1. Mode URL: fetch + convert (HTML→MD) + → Si contenu > 50KB: sauve en temp file, dit au sous-agent d'utiliser view/grep +2. Mode Search: construit prompt pour web_search + web_fetch +3. Sous-agent avec petit modèle + outils restreints +4. Auto-approve des permissions pour le sous-agent +``` + +--- + +## 6. Optimisation de la consommation de tokens + +### 6.1 Résumé automatique (Auto-Summarization) + +**La plus grande optimisation.** Voir [Section 4](#4-système-de-résumé--compaction). + +Quand la fenêtre de contexte approche sa limite, toute la conversation est remplacée par un résumé détaillé. Ce résumé devient le nouveau point de départ. + +### 6.2 Anthropic Prompt Caching + +Marqueurs `ephemeral` ajoutés automatiquement : + +```go +// Cache control placement: +├── Dernier message système → ephemeral +├── Avant-dernier message → ephemeral +└── Antépénultième message → ephemeral +``` + +Cela permet à Anthropic de mettre en cache ces messages et de réduire les tokens d'input sur les tours suivants. + +### 6.3 Filtrage des orphelins de tool calls + +```go +// Avant d'envoyer à l'IA: +filterOrphanedToolResults() // Supprime tool results sans tool call correspondant +syntheticToolResultsForOrphanedCalls() // Injecte erreurs synthétiques pour tool calls orphelins +``` + +Cela nettoie l'historique des messages inutiles qui consomment des tokens. + +### 6.4 Deux modèles (Large/Small) + +``` +├── Large Model: tâches principales (coder, résumé) +└── Small Model: titre de session, agentic_fetch + → Économise les tokens de haute qualité pour les tâches simples +``` + +### 6.5 Limitations de taille des outils + +| Outil | Limite | Impact token | +|-------|--------|-------------| +| bash output | 30,000 chars | Tronque les longues sorties | +| view | 100KB max, 2000 lignes | Limite les gros fichiers | +| grep | 100 résultats, 500 chars/ligne | Limite les recherches larges | +| glob | 100 fichiers | Limite les résultats | +| ls | 1000 fichiers | Limite les répertoires massifs | +| fetch | 100KB | Limite les pages web | +| sourcegraph | 20 résultats max | Limite les recherches code | +| description tools | FirstLineDescription() | N'envoie que la 1ère ligne de description | + +### 6.6 Short Tool Descriptions + +```go +// Par défaut: seul le premier ligne non-vide de la description est envoyé +FirstLineDescription(fullMarkdownDescription) + +// Désactivable via: CRUSH_SHORT_TOOL_DESCRIPTIONS=0 +``` + +Chaque outil a une description markdown complète, mais seule la première ligne est envoyée à l'IA — économie significative sur 22+ outils. + +### 6.7 Workaround média par provider + +Pour les providers non-Anthropic/Bedrock, les images dans les tool results sont converties en messages utilisateur séparés avec pièces jointes — évitant les erreurs API et les tokens gaspillés. + +### 6.8 Injection system_reminder minimale + +Seul un rappel minimal est injecté comme premier message utilisateur : +``` +"This is a reminder that your todo list is currently empty. +DO NOT mention this to the user explicitly..." +``` + +Ce message est court et sert de garde-fou sans consommer beaucoup de tokens. + +### 6.9 Estimation de tokens + +```go +ApproxTokenCount(text) = len(text) / 4 // Heuristique 4 chars = 1 token +``` + +Utilisé pour le logging et les métriques, pas pour le contrôle strict. + +--- + +## 7. Optimisations de performance + +### 7.1 Concurrence + +``` +├── sync.WaitGroup pour chargement providers (Catwalk + Hyper en //) +├── sync.OnceValue pour cache Hyper (computed once) +├── fastwalk pour découverte des skills (concurent, suit symlinks) +├── errgroup.Group pour readiness des agents (system prompt + tools en //) +├── csync.Map pour maps concurrent-safe (providers, regex cache) +├── sync.RWMutex pour skill tracker +└── Shell mutex sérialise les commandes par instance shell +``` + +### 7.2 File d'attente de messages (Message Queue) + +``` +Si agent occupé → message en queue +↓ +Dans PrepareStep → injecte messages en queue comme user messages supplémentaires +↓ +Pas de perte de messages, traitement séquentiel garanti +``` + +### 7.3 Cache providers + +``` +├── Cache JSON: $XDG_DATA_HOME/crush/providers.json +├── ETag support pour revalidation HTTP +├── Fallback: frais → cache → embarqué +└── Chargement concurrent Catwalk + Hyper +``` + +### 7.4 CGO et GC + +``` +├── CGO_ENABLED=0 (pas de CGO overhead) +└── GOEXPERIMENT=greenteagc (GC optimisé) +``` + +### 7.5 Base de données SQLite + +``` +├── SQLC pour code SQL type-safe généré +├── Transactions avec retry (3 tentatives) pour conflits UNIQUE +├── Atomic SQL increment pour token usage (évite race conditions) +└── Index sur (session_id, created_at) pour requêtes messages rapides +``` + +### 7.6 Pub/Sub + +``` +├── Système d'événements découplé +├── Canal d'événements par workspace +└── Pas de polling — push-based +``` + +--- + +## 8. Système de permissions + +### 8.1 Actions + +| Action | Quand | Outils concernés | +|--------|-------|-----------------| +| `"read"` | Lecture hors workingDir | view, ls | +| `"write"` | Modification de fichiers | edit, multiedit, write | +| `"execute"` | Commande shell non-safe | bash | +| `"fetch"` | Requête réseau | fetch, agentic_fetch | +| `"download"` | Téléchargement | download | +| `"list"` | Liste ressources MCP | list_mcp_resources | +| `"read"` | Lecture ressource MCP | read_mcp_resource | + +### 8.2 Auto-approbations + +- Commandes bash `safeCommands` (ls, cat, pwd, git status, etc.) → **pas de permission** +- Docker MCP tools whitelistés (`mcp_docker_mcp-find`, etc.) → **auto-approuvé** +- Sous-agents (agent, agentic_fetch) → **auto-approuvé** + +### 8.3 Sécurité shell + +``` +Banned commands: alias, aria2c, axel, chrome, curl, curlie, firefox, +http-prompt, httpie, links, lynx, nc, safari, scp, ssh, telnet, w3m, +wget, xh, doas, su, sudo, apk, apt, apt-cache, apt-get, dnf, dpkg, +emerge, home-manager, makepkg, opkg, pacman, paru, pkg, pkg_add, +pkg_delete, portage, rpm, yay, yum, zypper, at, batch, chkconfig, +crontab, fdisk, mkfs, mount, parted, service, systemctl, umount, +firewall-cmd, ifconfig, ip, iptables, netstat, pfctl, route, ufw +``` + +--- + +## 9. Providers et modèles + +### 9.1 Types de providers supportés + +``` +├── OpenAI +├── Anthropic +├── OpenRouter (suffixe :exacto pour modèles supportés) +├── Vercel +├── Azure +├── AWS Bedrock +├── Google (Gemini) +├── Google Vertex AI +├── OpenAI-compatible (générique) +└── Hyper (Charm's meta-provider) +``` + +### 9.2 Hyper Provider + +``` +├── Endpoint: https://hyper.charm.land/api/v1/fantasy +├── Activation: HYPER, HYPERCRUSH, HYPER_ENABLE, HYPER_ENABLED env vars +├── Modèles: GLM-5, GLM-5.1, gpt-oss-120b, Kimi K2.5, Kimi K2.6 +├── Routage: Anthropic/OpenAI/Google/OpenAI-compat selon model ID +└── Header: x-crush-id pour identification +``` + +### 9.3 Construction du provider + +```go +buildProvider(config) → fantasy.Provider: + 1. Parse le type de provider + 2. Configure base URL, API key, headers + 3. Ajoute beta headers si thinking model (Anthropic) + 4. Pour Hyper: route vers le bon provider selon model ID + 5. Pour OpenRouter: ajoute :exacto si supporté +``` + +--- + +## 10. Système de skills + +### 10.1 Standard Agent Skills + +Crush implémente le standard ouvert [agentskills.io](https://agentskills.io). + +### 10.2 Découverte + +``` +Chemins explorés: +├── $CRUSH_SKILLS_DIR +├── ~/.config/agents/skills/ +├── ~/.config/crush/skills/ +├── .agents/skills/ +├── .crush/skills/ +├── .claude/skills/ +├── .cursor/skills/ +└── Custom via options.skills_paths + +Recherche: +├── fastwalk (concurent, suit symlinks) +├── Cherche fichiers SKILL.md +├── Parse YAML frontmatter (entre ---) +├── Validation: nom alphanum-hyphens ≤64 chars, description ≤1024 chars +└── Déduplication: dernier occurence gagne (user > builtin) +``` + +### 10.3 Injection dans le prompt + +```xml + + + skill-name + Description courte + /path/to/SKILL.md + builtin + + + + + When a user task matches a skill's description, read the skill's SKILL.md file... + +``` + +**Important :** Seules les métadonnées (nom, description, chemin) sont injectées dans le system prompt. Les **instructions complètes** ne sont lues que quand l'agent décide d'activer le skill — économie de tokens. + +### 10.4 Skill Tracker + +```go +type Tracker struct { + active map[string]bool // Skills actifs (post-dedup, post-filter) + loaded map[string]bool // Skills dont les instructions ont été lues + mu sync.RWMutex +} + +MarkLoaded(name) // Marque un skill comme lu (uniquement si dans active set) +IsLoaded(name) // Vérifie si déjà chargé +LoadedNames() // Liste des skills chargés +``` + +--- + +## 11. Intégration LSP + +### 11.1 Configuration + +```yaml +lsp: + - command: "gopls" + args: ["serve"] + env: {} + file_types: [".go"] + root_markers: ["go.mod"] + init_options: {} +``` + +### 11.2 Outils LSP + +- `lsp_diagnostics` — Diagnostics par fichier ou projet entier (max 10 fichiers, 5s wait) +- `lsp_references` — Recherche de références symboliques (grep + LSP FindReferences) +- `lsp_restart` — Redémarrage d'un ou tous les clients LSP + +### 11.3 Intégration dans les outils + +``` +view → openInLSPs + wait 300ms pour diagnostics +edit → notifyLSPs + wait 5s pour diagnostics +write → notifyLSPs + wait 5s pour diagnostics +``` + +Les diagnostics sont appendés dans les réponses des outils via des tags XML : +```xml +... +... +errors: N, warnings: M +``` + +--- + +## 12. Intégration MCP + +### 12.1 Configuration + +```yaml +mcp: + - name: "my-server" + command: "npx" # Mode stdio + args: ["-y", "my-mcp"] + env: {} + url: "" # OU mode HTTP/SSE + timeout: 30s + disabled_tools: [] +``` + +### 12.2 Outils MCP dynamiques + +- Outils nommés `mcp_{server}_{tool}` +- Schéma extrait de `InputSchema` MCP +- Permission requise (sauf whitelist Docker) +- Support image/média dans les résultats + +### 12.3 Instructions MCP injectées + +Les instructions des serveurs MCP connectés sont injectées dans le system prompt via ``. + +--- + +## 13. Structure de la base de données + +### 13.1 Tables principales + +```sql +-- Sessions +sessions ( + id, title, prompt_tokens, completion_tokens, + summary_message_id, -- ID du message résumé (NULL si pas de résumé) + cost, todos, -- Coût et todo list + created_at, updated_at +) + +-- Messages +messages ( + id, session_id, role, -- user/assistant/system/tool + parts, -- JSON array de {type, data} + model, provider, + is_summary_message, -- Flag de compaction + created_at, finished_at +) + +-- Fichiers (version history) +files ( + id, session_id, path, + content, version, -- Auto-incrémenté par path + is_new, + created_at +) + +-- Fichiers lus (read tracking) +read_files ( + path, session_id, + last_read_time -- Pour "read before edit" +) +``` + +### 13.2 Requêtes clés + +```sql +-- Messages d'une session (chronologique) +SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC; + +-- Update atomique des tokens (évite race conditions) +UPDATE sessions SET + prompt_tokens = prompt_tokens + ?, + completion_tokens = completion_tokens + ?, + cost = cost + ? +WHERE id = ?; + +-- Derniers fichiers par path (version max) +SELECT f.* FROM files f +INNER JOIN ( + SELECT path, MAX(version) as max_ver + FROM files WHERE session_id = ? + GROUP BY path +) latest ON f.path = latest.path AND f.version = latest.max_ver; + +-- Stats: utilisation des outils via JSON +SELECT json_extract(part.data, '$.name') as tool_name, COUNT(*) +FROM messages, json_each(messages.parts) as part +WHERE json_extract(part.value, '$.type') = 'tool_call' +GROUP BY tool_name; +``` + +--- + +## 14. Fichiers clés à explorer + +### Architecture et flow principal + +| Fichier | Ce qu'il contient | +|---------|------------------| +| `internal/agent/agent.go` | Moteur IA — construction messages, streaming, résumé, queue | +| `internal/agent/coordinator.go` | Orchestration — création agents, tools, models, provider options | +| `internal/agent/prompts.go` | Factory de prompts système | +| `internal/agent/loop_detection.go` | Détection de boucles (SHA-256 signatures) | + +### Templates (ce qui est envoyé à l'IA) + +| Fichier | Ce qu'il contient | +|---------|------------------| +| `internal/agent/templates/coder.md.tpl` | System prompt principal (405 lignes) | +| `internal/agent/templates/task.md.tpl` | Prompt sous-agent | +| `internal/agent/templates/summary.md` | Prompt de résumé/compaction | +| `internal/agent/templates/title.md` | Prompt de génération de titre | +| `internal/agent/templates/agent_tool.md` | Instructions tool agent | +| `internal/agent/templates/agentic_fetch_prompt.md.tpl` | Prompt sous-agent fetch | + +### Outils + +| Fichier | Ce qu'il contient | +|---------|------------------| +| `internal/agent/agent_tool.go` | Tool agent (sous-agent spawner) | +| `internal/agent/agentic_fetch_tool.go` | Tool agentic_fetch | +| `internal/agent/tools/bash.go` | Tool bash | +| `internal/agent/tools/edit.go` | Tool edit | +| `internal/agent/tools/multiedit.go` | Tool multiedit | +| `internal/agent/tools/write.go` | Tool write | +| `internal/agent/tools/view.go` | Tool view | +| `internal/agent/tools/glob.go` | Tool glob | +| `internal/agent/tools/grep.go` | Tool grep | +| `internal/agent/tools/ls.go` | Tool ls | +| `internal/agent/tools/fetch.go` | Tool fetch | +| `internal/agent/tools/sourcegraph.go` | Tool sourcegraph | +| `internal/agent/tools/web_search.go` | Tool web_search (DuckDuckGo) | +| `internal/agent/tools/web_fetch.go` | Tool web_fetch | +| `internal/agent/tools/download.go` | Tool download | +| `internal/agent/tools/todos.go` | Tool todos | +| `internal/agent/tools/diagnostics.go` | Tool LSP diagnostics + helpers | +| `internal/agent/tools/references.go` | Tool LSP references | +| `internal/agent/tools/lsp_restart.go` | Tool LSP restart | +| `internal/agent/tools/crush_info.go` | Tool crush_info | +| `internal/agent/tools/crush_logs.go` | Tool crush_logs | +| `internal/agent/tools/mcp-tools.go` | Outils MCP dynamiques | + +### Messages et données + +| Fichier | Ce qu'il contient | +|---------|------------------| +| `internal/message/message.go` | Service de messages (CRUD + events) | +| `internal/message/content.go` | Types de contenu + conversion fantasy.Message | +| `internal/message/attachment.go` | Pièces jointes | +| `internal/history/file.go` | Historique de versions fichiers | +| `internal/db/sql/sessions.sql` | Requêtes SQL sessions | +| `internal/db/sql/messages.sql` | Requêtes SQL messages | +| `internal/db/sql/files.sql` | Requêtes SQL fichiers | +| `internal/db/sql/stats.sql` | Requêtes SQL statistiques | + +### Configuration et providers + +| Fichier | Ce qu'il contient | +|---------|------------------| +| `internal/config/config.go` | Structures de config (models, providers, tools, agents) | +| `internal/config/provider.go` | Chargement des providers (Catwalk + Hyper + cache) | +| `internal/backend/config.go` | API config backend | +| `internal/agent/hyper/provider.go` | Provider Hyper | +| `internal/agent/hyper/provider.json` | Définition des modèles Hyper | + +### Infrastructure + +| Fichier | Ce qu'il contient | +|---------|------------------| +| `internal/shell/shell.go` | Shell POSIX cross-platform (mvdan/sh) | +| `internal/diff/diff.go` | Génération de diffs unifiés | +| `internal/skills/skills.go` | Système de skills (découverte, injection) | +| `internal/skills/tracker.go` | Suivi des skills chargés | +| `internal/lsp/manager.go` | Gestionnaire LSP | +| `internal/filetracker/service.go` | Suivi des fichiers lus | + +--- + +## 15. Leçons pour notre application + +### 15.1 Patterns à adopter + +| Pattern | Pourquoi | Comment Crush le fait | +|---------|----------|----------------------| +| **Résumé automatique adaptatif** | Gère les longues conversations sans perte | Seuil 20K tokens (fenêtres >200K) ou 20% (fenêtres <200K), skip si fenêtre inconnue | +| **Deux niveaux de modèle** | Économise les tokens coûteux | Large pour coder, Small pour titres et fetch | +| **Skills avec lazy loading** | N'injecte que les métadonnées, charge les instructions à la demande | Métadonnées dans system prompt, instructions lues via view | +| **Short tool descriptions** | Économise des tokens sur 22+ outils | FirstLineDescription() par défaut | +| **Anthropic prompt caching** | Réduit les tokens d'input sur les tours suivants | `ephemeral` sur dernier system + 2 derniers messages | +| **Filetracker read-before-edit** | Sécurité + contexte pour l'IA | Enregistre chaque lecture, vérifie avant édition | +| **Loop detection** | Arrête les boucles infinies coûteuses | SHA-256 signature sur fenêtre de 10 étapes, max 5 répétitions | +| **Orphan filtering** | Nettoie l'historique des messages inutiles | Supprime tool results orphelins, injecte erreurs synthétiques | +| **Atomic SQL increments** | Pas de race conditions sur les compteurs | `prompt_tokens = prompt_tokens + ?` au lieu de read-modify-write | +| **Message queue** | Pas de perte de messages | Queue + injection dans PrepareStep | + +### 15.2 Optimisations token spécifiques + +1. **Troncature de sortie** — bash (30K), grep (500 chars/ligne), view (2000 lignes) +2. **Limites de résultats** — glob (100), grep (100), ls (1000), sourcegraph (20) +3. **Première ligne de description** — Seulement la 1ère ligne des descriptions markdown d'outils +4. **Estimation heuristique** — `len(text) / 4` pour logging sans appel API +5. **Résumé détaillé structuré** — Sections obligatoires assurent pas de perte d'information critique +6. **Reset des compteurs après résumé** — PromptTokens=0, CompletionTokens=résumé seulement + +### 15.3 Points d'attention + +- **Le system prompt fait ~405 lignes** — C'est énorme mais structuré en sections XML claires +- **Pas de troncature du system prompt** — Les skills et context files sont injectés en entier +- **Le résumé n'a pas de limite** — *"Err on the side of too much detail"* +- **La détection de boucle est basique** — SHA-256 sur ToolName+Input+Output, mais ne détecte pas les boucles sémantiques +- **Pas de token counting précis** — Heuristique 4 chars/token, pas de tiktoken +- **Le file history persiste** — Les versions de fichiers survivent au résumé (pas dans le contexte LLM mais disponibles dans l'UI) + +### 15.4 Architecture recommandée + +``` +Pour notre app, inspirons-nous de: + +1. Coordinator Pattern + └── Orchestrateur central qui assemble models + tools + prompts + └── Agents par session avec state isolé + +2. Summary/Compaction Pipeline + └── Seuils adaptatifs (fenêtre large vs small) + └── Résumé structuré avec sections obligatoires + └── Reset des compteurs après compaction + +3. Tool Architecture + └── Interface uniforme (params, execute, response) + └── Permission system par action + └── Output truncation systématique + └── Metadata sur chaque réponse + +4. Provider Layer + └── JSON merge en 3 couches pour options + └── Support multi-provider avec routage + └── Cache avec ETag + fallback + +5. Lazy Skill Loading + └── Métadonnées dans system prompt + └── Instructions complètes à la demande + └── Tracker pour éviter les rechargements +``` + +--- + +## Annexe : Schéma de dépendances + +``` +main.go + └── internal/cmd/ (CLI commands) + └── internal/backend/ (workspace management) + └── internal/app/ (application logic) + └── internal/agent/coordinator.go (orchestration) + ├── internal/agent/agent.go (moteur IA) + │ ├── internal/agent/templates/ (prompts) + │ ├── internal/agent/prompt/ (prompt builder) + │ ├── internal/message/ (message types) + │ └── internal/agent/loop_detection.go + ├── internal/agent/tools/ (22+ outils) + ├── internal/skills/ (skill discovery) + ├── internal/config/ (configuration) + ├── internal/lsp/ (LSP management) + ├── internal/shell/ (shell execution) + ├── internal/db/ (SQLite persistence) + ├── internal/filetracker/ (read tracking) + └── internal/diff/ (diff generation) +``` + +--- + +*Rapport généré le 26 avril 2026 — Basé sur l'analyse du commit `HEAD` de [charmbracelet/crush](https://github.com/charmbracelet/crush)* diff --git a/internal/agent/definitions.go b/internal/agent/definitions.go index 0c0b534..affc551 100644 --- a/internal/agent/definitions.go +++ b/internal/agent/definitions.go @@ -3,17 +3,48 @@ package agent import ( "context" "fmt" + "os" "os/exec" "path/filepath" "strings" + "sync" "time" ) +var ( + sudoCache bool + sudoCacheSet bool + sudoCacheOnce sync.Once +) + +func NeedsSudoPassword() bool { + sudoCacheOnce.Do(func() { + if os.Geteuid() == 0 { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := exec.CommandContext(ctx, "sudo", "-n", "true").Run() + sudoCacheSet = true + sudoCache = err != nil + } else { + sudoCache = true + sudoCacheSet = true + } + }) + return sudoCache +} + type TerminalParams struct { Command string `json:"command" description:"The shell command to execute"` 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 +53,18 @@ func NewTerminalTool() (*ToolDefinition, error) { return TextErrorResponse("command is required"), nil } + if NeedsSudoPassword() { + 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..fc16ae4 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,71 @@ 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 +- **Lecture de fichiers** — Utilise TOUJOURS `read_file` pour lire le contenu d'un fichier. N'utilise PAS `terminal` avec `cat` pour lire des fichiers — `read_file` est plus rapide, plus précis, et consomme moins de tokens +- **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..cc463df 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) @@ -87,7 +92,7 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. assistantMsg := orchestrator.Message{ Role: "assistant", - Content: content, + Content: orchestrator.TextContent(content), ToolCalls: choice.Message.ToolCalls, } messages = append(messages, assistantMsg) @@ -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, @@ -137,7 +147,7 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. messages = append(messages, orchestrator.Message{ Role: "tool", - Content: result.Content, + Content: orchestrator.TextContent(result.Content), ToolCallID: 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) @@ -172,7 +191,7 @@ func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator. assistantMsg := orchestrator.Message{ Role: "assistant", - Content: content, + Content: orchestrator.TextContent(content), ToolCalls: choice.Message.ToolCalls, } messages = append(messages, assistantMsg) @@ -194,7 +213,7 @@ func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator. messages = append(messages, orchestrator.Message{ Role: "tool", - Content: result.Content, + Content: orchestrator.TextContent(result.Content), ToolCallID: tc.ID, Name: tc.Function.Name, }) 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..9382870 100644 --- a/internal/api/conversation.go +++ b/internal/api/conversation.go @@ -13,28 +13,31 @@ import ( "github.com/muyue/muyue/internal/config" ) -const maxTokensApprox = 100000 -const summarizeThreshold = 80000 +const contextWindowTokens = 150000 +const summarizeRatio = 0.80 const charsPerToken = 4 type FeedMessage struct { - ID string `json:"id"` - Role string `json:"role"` - Content string `json:"content"` - Time string `json:"time"` + ID string `json:"id"` + Role string `json:"role"` + Content string `json:"content"` + Time string `json:"time"` + Images []string `json:"images,omitempty"` } type Conversation struct { - Messages []FeedMessage `json:"messages"` - Summary string `json:"summary,omitempty"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + Messages []FeedMessage `json:"messages"` + Summary string `json:"summary,omitempty"` + RealTokens int `json:"real_tokens,omitempty"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } type ConversationStore struct { - mu sync.RWMutex - path string - conv *Conversation + mu sync.RWMutex + path string + conv *Conversation + realTokens int } type TokenCount struct { @@ -84,6 +87,7 @@ func (cs *ConversationStore) load() { conv.Messages = []FeedMessage{} } cs.conv = &conv + cs.realTokens = conv.RealTokens } func (cs *ConversationStore) save() error { @@ -126,14 +130,40 @@ func (cs *ConversationStore) Add(role, content string) FeedMessage { return msg } +func (cs *ConversationStore) AddWithImages(role, content string, imageIDs []string) FeedMessage { + cs.mu.Lock() + defer cs.mu.Unlock() + + msg := FeedMessage{ + ID: generateMsgID(), + Role: role, + Content: content, + Time: time.Now().Format(time.RFC3339), + Images: imageIDs, + } + cs.conv.Messages = append(cs.conv.Messages, msg) + cs.save() + return msg +} + func (cs *ConversationStore) Clear() { cs.mu.Lock() defer cs.mu.Unlock() + + var imageIDs []string + for _, m := range cs.conv.Messages { + imageIDs = append(imageIDs, m.Images...) + } + cs.conv.Messages = []FeedMessage{} cs.conv.Summary = "" + cs.conv.RealTokens = 0 cs.conv.CreatedAt = time.Now().Format(time.RFC3339) cs.conv.UpdatedAt = time.Now().Format(time.RFC3339) + cs.realTokens = 0 cs.save() + + go cleanupImages(imageIDs) } func (cs *ConversationStore) SetSummary(summary string) { @@ -154,9 +184,23 @@ 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.conv.RealTokens = cs.realTokens + cs.mu.Unlock() +} + func (cs *ConversationStore) ApproxTokenCountDetailed() TokenCount { cs.mu.RLock() defer cs.mu.RUnlock() @@ -181,7 +225,7 @@ func (cs *ConversationStore) ApproxTokenCountDetailed() TokenCount { } func (cs *ConversationStore) NeedsSummarization() bool { - return cs.ApproxTokenCount() > summarizeThreshold + return cs.ApproxTokenCount() > int(float64(contextWindowTokens)*summarizeRatio) } func (cs *ConversationStore) Search(query string) []SearchResult { diff --git a/internal/api/handlers_chat.go b/internal/api/handlers_chat.go index 58cb135..b3b7f9f 100644 --- a/internal/api/handlers_chat.go +++ b/internal/api/handlers_chat.go @@ -1,17 +1,132 @@ package api import ( + "bytes" "context" "encoding/json" + "fmt" + "io" + "log" "net/http" + "os" + "path/filepath" "regexp" "strings" + "time" "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/orchestrator" ) var thinkingTagRegex = regexp.MustCompile(`(?s)<[Tt]hink[^>]*>.*?`) +var fileMentionRegex = regexp.MustCompile(`@(\S+\.[a-zA-Z0-9]+)`) + +type ImageAttachment struct { + Data string `json:"data"` + Filename string `json:"filename"` + MimeType string `json:"mime_type"` +} + +func resolveFileMentions(text string) string { + return fileMentionRegex.ReplaceAllStringFunc(text, func(match string) string { + filePath := match[1:] + if strings.HasPrefix(filePath, "~/") { + if home, err := os.UserHomeDir(); err == nil { + filePath = filepath.Join(home, filePath[2:]) + } + } + if !filepath.IsAbs(filePath) { + if home, err := os.UserHomeDir(); err == nil { + filePath = filepath.Join(home, filePath) + } + } + data, err := os.ReadFile(filePath) + if err != nil { + return match + fmt.Sprintf(" (erreur: fichier non trouve)") + } + content := string(data) + if len(content) > 50000 { + content = content[:50000] + "\n... (tronque a 50Ko)" + } + return fmt.Sprintf("[Fichier: %s]\n%s\n[Fin du fichier: %s]", filepath.Base(filePath), content, filepath.Base(filePath)) + }) +} + +var vlmClient = &http.Client{Timeout: 60 * time.Second} + +func (s *Server) describeImages(images []ImageAttachment) []string { + var apiKey string + for i := range s.config.AI.Providers { + if s.config.AI.Providers[i].Active { + apiKey = s.config.AI.Providers[i].APIKey + break + } + } + if apiKey == "" { + log.Printf("[vlm] no API key found for image description") + return nil + } + + descriptions := make([]string, 0, len(images)) + for i, img := range images { + desc, err := s.callVLM(apiKey, img) + if err != nil { + log.Printf("[vlm] image %d (%s) failed: %v", i+1, img.Filename, err) + descriptions = append(descriptions, fmt.Sprintf("(description unavailable: %v)", err)) + } else { + descriptions = append(descriptions, desc) + } + } + return descriptions +} + +func (s *Server) callVLM(apiKey string, img ImageAttachment) (string, error) { + payload := map[string]string{ + "prompt": "Describe this image in detail. Include all text, UI elements, code, diagrams, or data visible. Be thorough and specific.", + "image_url": img.Data, + } + body, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("marshal vlm request: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 55*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "POST", "https://api.minimax.io/v1/coding_plan/vlm", bytes.NewReader(body)) + if err != nil { + return "", fmt.Errorf("create vlm request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + resp, err := vlmClient.Do(req) + if err != nil { + return "", fmt.Errorf("vlm request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read vlm response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("vlm API error (%d): %s", resp.StatusCode, string(respBody)) + } + + var result struct { + Content string `json:"content"` + } + if err := json.Unmarshal(respBody, &result); err != nil { + return "", fmt.Errorf("parse vlm response: %w", err) + } + + if result.Content == "" { + return "(empty description)", nil + } + return result.Content, nil +} func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { @@ -19,8 +134,9 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { return } var body struct { - Message string `json:"message"` - Stream bool `json:"stream"` + Message string `json:"message"` + Stream bool `json:"stream"` + Images []ImageAttachment `json:"images"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) @@ -30,8 +146,44 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { writeError(w, "no message", http.StatusMethodNotAllowed) return } + if len(body.Images) > 3 { + writeError(w, "max 3 images", http.StatusBadRequest) + return + } - s.convStore.Add("user", body.Message) + enrichedMessage := resolveFileMentions(body.Message) + + var imageIDs []string + if len(body.Images) > 0 { + descriptions := s.describeImages(body.Images) + var imgContext strings.Builder + for i, desc := range descriptions { + imgContext.WriteString(fmt.Sprintf("\n[Image %d (%s): %s]\n", i+1, body.Images[i].Filename, desc)) + + id, err := saveImage(body.Images[i].Data, body.Images[i].Filename, body.Images[i].MimeType) + if err != nil { + log.Printf("[images] failed to save %s: %v", body.Images[i].Filename, err) + } else { + imageIDs = append(imageIDs, id) + } + } + enrichedMessage = imgContext.String() + enrichedMessage + } + + displayMsg := body.Message + if len(body.Images) > 0 { + imgNames := make([]string, len(body.Images)) + for i, img := range body.Images { + imgNames[i] = img.Filename + } + displayMsg += " [" + strings.Join(imgNames, ", ") + "]" + } + + if len(imageIDs) > 0 { + s.convStore.AddWithImages("user", displayMsg, imageIDs) + } else { + s.convStore.Add("user", displayMsg) + } if s.convStore.NeedsSummarization() { s.autoSummarize() @@ -42,13 +194,23 @@ 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"))) + canSudo := !agent.NeedsSudoPassword() + studioPrompt.WriteString(fmt.Sprintf("Root: %t\n", !canSudo)) + if !canSudo { + studioPrompt.WriteString("⚠️ Session sans sudo sans mot de passe — les commandes sudo/doas nécessitent une autorisation. N'utilise PAS sudo ou doas sans demander.\n") + } else { + studioPrompt.WriteString("⚠️ Session avec privilèges sudo sans mot de passe — les commandes sudo s'exécuteront directement.\n") + } + orb.SetSystemPrompt(studioPrompt.String()) orb.SetTools(s.agentToolsJSON) if body.Stream { - s.handleStreamChat(w, orb, body.Message) + s.handleStreamChat(w, orb, enrichedMessage) } else { - s.handleNonStreamChat(w, orb, body.Message) + s.handleNonStreamChat(w, orb, enrichedMessage) } } @@ -91,6 +253,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 +272,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}) } @@ -129,7 +298,7 @@ func (s *Server) buildContextMessages(userMessage string) []orchestrator.Message if summary != "" { messages = append(messages, orchestrator.Message{ Role: "system", - Content: "Résumé de la conversation précédente:\n" + summary, + Content: orchestrator.TextContent("Résumé de la conversation précédente:\n" + summary), }) } @@ -154,13 +323,13 @@ func (s *Server) buildContextMessages(userMessage string) []orchestrator.Message } messages = append(messages, orchestrator.Message{ Role: role, - Content: content, + Content: orchestrator.TextContent(content), }) } messages = append(messages, orchestrator.Message{ Role: "user", - Content: userMessage, + Content: orchestrator.TextContent(userMessage), }) return messages @@ -208,8 +377,8 @@ func (s *Server) handleChatHistory(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]interface{}{ "messages": messages, "tokens": s.convStore.ApproxTokenCount(), - "max_tokens": maxTokensApprox, - "summarize_at": summarizeThreshold, + "max_tokens": contextWindowTokens, + "summarize_at": int(float64(contextWindowTokens) * summarizeRatio), "summary": s.convStore.GetSummary(), }) } 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_config.go b/internal/api/handlers_config.go index c0c977b..b2e7896 100644 --- a/internal/api/handlers_config.go +++ b/internal/api/handlers_config.go @@ -187,6 +187,8 @@ func (s *Server) handleValidateProvider(w http.ResponseWriter, r *http.Request) switch body.Name { case "minimax": baseURL = "https://api.minimax.io/v1" + case "mimo": + baseURL = "https://token-plan-ams.xiaomimimo.com/v1" case "openai": baseURL = "https://api.openai.com/v1" case "anthropic": diff --git a/internal/api/handlers_info.go b/internal/api/handlers_info.go index 921481a..cf99f6b 100644 --- a/internal/api/handlers_info.go +++ b/internal/api/handlers_info.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/lsp" "github.com/muyue/muyue/internal/mcp" "github.com/muyue/muyue/internal/scanner" @@ -24,7 +25,7 @@ func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) { "name": version.Name, "version": version.Version, "author": version.Author, - "sudo": os.Geteuid() == 0, + "sudo": !agent.NeedsSudoPassword(), }) } @@ -534,6 +535,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 +585,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 630ca24..bb736a0 100644 --- a/internal/api/handlers_shell_chat.go +++ b/internal/api/handlers_shell_chat.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "fmt" "net/http" @@ -8,6 +9,7 @@ import ( "os/exec" "runtime" "strings" + "time" "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/orchestrator" @@ -51,81 +53,89 @@ func (s *Server) handleShellChat(w http.ResponseWriter, r *http.Request) { return } - orb.SetSystemPrompt(s.buildShellSystemPromptV2(req)) + orb.SetSystemPrompt(s.buildShellSystemPrompt(req)) + orb.SetTools(s.shellAgentToolsJSON) if req.Stream { - s.handleShellChatStreamV2(w, orb) + s.handleShellChatStream(w, orb) } else { - s.handleShellChatNonStreamV2(w, orb) + s.handleShellChatNonStream(w, orb) } } -func (s *Server) buildShellSystemPromptV2(_ 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. - -RÈGLES STRICTES: -- Tu ne peux JAMAIS exécuter de commande ou de code -- Tu ne peux que analyser, expliquer, et proposer des solutions -- Quand tu proposes du code ou des commandes, mets-les dans des blocs de code markdown avec le langage spécifié -- L'utilisateur pourra les copier ou les envoyer directement au terminal depuis les boutons - -`) + 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") + } + + canSudo := !agent.NeedsSudoPassword() + sb.WriteString(fmt.Sprintf("Root: %t\n", !canSudo)) + if canSudo { + sb.WriteString("⚠️ Session avec privilèges sudo sans mot de passe — les commandes sudo s'exécuteront directement.\n") + } else { + sb.WriteString("⚠️ Session sans sudo sans mot de passe — 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() } -func (s *Server) handleShellChatStreamV2(w http.ResponseWriter, orb *orchestrator.Orchestrator) { +func (s *Server) handleShellChatStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) { SetupSSEHeaders(w) flusher, canFlush := w.(http.Flusher) sseWriter := NewSSEWriter(w) - // Rebuild history into orchestrator - history := s.shellConvStore.Get() - for _, m := range history[:len(history)-1] { // all except last user msg - if m.Role == "system" { - continue + ctx := context.Background() + messages := s.buildShellContextMessages() + + engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON) + engine.OnChunk(func(data map[string]interface{}) { + if data == nil { + return } - // Pre-load orchestrator history - orb.AppendHistory(orchestrator.Message{Role: m.Role, Content: m.Content}) - } - - lastUserMsg := history[len(history)-1].Content - - var finalContent string - result, err := orb.SendStream(lastUserMsg, func(chunk string) { - finalContent = chunk - sseWriter.Write(map[string]interface{}{"content": chunk}) + sseWriter.Write(data) if canFlush { flusher.Flush() } }) + finalContent, allToolCalls, allToolResults, err := engine.RunWithTools(ctx, messages) if err != nil { sseWriter.Write(map[string]interface{}{"error": err.Error()}) return } - content := result - if content == "" { - content = finalContent + storeContent := finalContent + if len(allToolCalls) > 0 { + storeObj := map[string]interface{}{ + "content": storeContent, + "tool_calls": allToolCalls, + "tool_results": allToolResults, + } + storeJSON, _ := json.Marshal(storeObj) + storeContent = string(storeJSON) } + s.shellConvStore.Add("assistant", storeContent) + s.shellConvStore.AddRealTokens(engine.TotalTokens) - s.shellConvStore.Add("assistant", cleanThinkingTags(content)) + s.consumption.Record(engine.ProviderName(), engine.TotalTokens) sseWriter.Write(map[string]interface{}{ "done": "true", @@ -133,30 +143,66 @@ func (s *Server) handleShellChatStreamV2(w http.ResponseWriter, orb *orchestrato }) } -func (s *Server) handleShellChatNonStreamV2(w http.ResponseWriter, orb *orchestrator.Orchestrator) { - history := s.shellConvStore.Get() - for _, m := range history[:len(history)-1] { - if m.Role == "system" { - continue - } - orb.AppendHistory(orchestrator.Message{Role: m.Role, Content: m.Content}) - } +func (s *Server) handleShellChatNonStream(w http.ResponseWriter, orb *orchestrator.Orchestrator) { + ctx := context.Background() + messages := s.buildShellContextMessages() - lastUserMsg := history[len(history)-1].Content - - result, err := orb.Send(lastUserMsg) + engine := NewChatEngine(orb, s.shellAgentRegistry, s.shellAgentToolsJSON) + finalContent, err := engine.RunNonStream(ctx, messages) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } - s.shellConvStore.Add("assistant", cleanThinkingTags(result)) + s.shellConvStore.Add("assistant", finalContent) + s.shellConvStore.AddRealTokens(engine.TotalTokens) + + s.consumption.Record(engine.ProviderName(), engine.TotalTokens) + writeJSON(w, map[string]interface{}{ - "content": result, + "content": finalContent, "tokens": s.shellConvStore.ApproxTokens(), }) } +func (s *Server) buildShellContextMessages() []orchestrator.Message { + history := s.shellConvStore.Get() + start := 0 + const shellContextWindow = 20 + if len(history) > shellContextWindow { + start = len(history) - shellContextWindow + } + + messages := make([]orchestrator.Message, 0, len(history[start:])) + + for _, m := range history[start:] { + content := m.Content + if m.Role == "assistant" { + var parsed struct { + Content string `json:"content"` + ToolCalls []struct { + ToolCallID string `json:"tool_call_id"` + Name string `json:"name"` + Args string `json:"args"` + } `json:"tool_calls"` + } + if err := json.Unmarshal([]byte(content), &parsed); err == nil && parsed.Content != "" { + content = parsed.Content + } + } + role := m.Role + if role == "system" { + continue + } + messages = append(messages, orchestrator.Message{ + Role: role, + Content: orchestrator.TextContent(content), + }) + } + + return messages +} + func (s *Server) handleShellChatHistory(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { writeError(w, "GET only", http.StatusMethodNotAllowed) @@ -252,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/image_cache.go b/internal/api/image_cache.go new file mode 100644 index 0000000..f5dd286 --- /dev/null +++ b/internal/api/image_cache.go @@ -0,0 +1,104 @@ +package api + +import ( + "encoding/base64" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "sync/atomic" + "time" + + "github.com/muyue/muyue/internal/config" +) + +var imageDir string + +func init() { + dir, err := config.ConfigDir() + if err != nil { + dir = "/tmp/muyue" + } + imageDir = filepath.Join(dir, "images") + os.MkdirAll(imageDir, 0755) +} + +var imageCounter uint64 + +func saveImage(dataURI, filename, mimeType string) (string, error) { + parts := strings.SplitN(dataURI, ",", 2) + if len(parts) != 2 { + return "", fmt.Errorf("invalid data URI") + } + encoded := parts[1] + + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return "", fmt.Errorf("base64 decode: %w", err) + } + + id := fmt.Sprintf("%d-%d", time.Now().UnixMilli(), atomic.AddUint64(&imageCounter, 1)) + ext := ".png" + switch mimeType { + case "image/jpeg": + ext = ".jpg" + case "image/webp": + ext = ".webp" + } + + filePath := filepath.Join(imageDir, id+ext) + if err := os.WriteFile(filePath, decoded, 0600); err != nil { + return "", fmt.Errorf("write image: %w", err) + } + + return id + ext, nil +} + +func imagePath(id string) string { + return filepath.Join(imageDir, filepath.Base(id)) +} + +func cleanupImages(ids []string) { + for _, id := range ids { + p := imagePath(id) + if err := os.Remove(p); err != nil && !os.IsNotExist(err) { + log.Printf("[images] failed to delete %s: %v", id, err) + } + } +} + +func (s *Server) handleServeImage(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + writeError(w, "GET only", http.StatusMethodNotAllowed) + return + } + + id := strings.TrimPrefix(r.URL.Path, "/api/images/") + if id == "" { + writeError(w, "image id required", http.StatusBadRequest) + return + } + + filePath := imagePath(id) + if _, err := os.Stat(filePath); err != nil { + writeError(w, "image not found", http.StatusNotFound) + return + } + + ext := strings.ToLower(filepath.Ext(id)) + switch ext { + case ".jpg", ".jpeg": + w.Header().Set("Content-Type", "image/jpeg") + case ".png": + w.Header().Set("Content-Type", "image/png") + case ".webp": + w.Header().Set("Content-Type", "image/webp") + default: + w.Header().Set("Content-Type", "application/octet-stream") + } + w.Header().Set("Cache-Control", "public, max-age=86400") + + http.ServeFile(w, r, filePath) +} diff --git a/internal/api/server.go b/internal/api/server.go index 4b89dd7..8ead886 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -13,14 +13,17 @@ import ( ) type Server struct { - config *config.MuyueConfig - scanResult *scanner.ScanResult - mux *http.ServeMux - convStore *ConversationStore - shellConvStore *ShellConvStore - agentRegistry *agent.Registry - agentToolsJSON json.RawMessage - workflowEngine *workflow.Engine + config *config.MuyueConfig + scanResult *scanner.ScanResult + mux *http.ServeMux + convStore *ConversationStore + shellConvStore *ShellConvStore + consumption *consumptionStore + agentRegistry *agent.Registry + agentToolsJSON json.RawMessage + shellAgentRegistry *agent.Registry + shellAgentToolsJSON json.RawMessage + workflowEngine *workflow.Engine } func NewServer(cfg *config.MuyueConfig) *Server { @@ -48,10 +51,19 @@ 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) s.agentToolsJSON = json.RawMessage(toolsJSON) + + s.shellAgentRegistry = agent.NewRegistry() + terminalTool, _ := agent.NewTerminalTool() + s.shellAgentRegistry.Register(terminalTool) + shellTools := s.shellAgentRegistry.OpenAITools() + shellToolsJSON, _ := json.Marshal(shellTools) + s.shellAgentToolsJSON = json.RawMessage(shellToolsJSON) + s.workflowEngine, _ = workflow.NewEngine(s.agentRegistry) s.routes() return s @@ -84,6 +96,7 @@ func (s *Server) routes() { s.mux.HandleFunc("/api/starship/apply-theme", s.handleApplyStarshipTheme) s.mux.HandleFunc("/api/providers/validate", s.handleValidateProvider) s.mux.HandleFunc("/api/update/run", s.handleRunUpdate) + s.mux.HandleFunc("/api/images/", s.handleServeImage) s.mux.HandleFunc("/api/chat", s.handleChat) s.mux.HandleFunc("/api/chat/history", s.handleChatHistory) s.mux.HandleFunc("/api/chat/clear", s.handleChatClear) @@ -121,13 +134,14 @@ 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) } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/api/ws/") { + if strings.HasPrefix(r.URL.Path, "/api/ws/") || strings.HasPrefix(r.URL.Path, "/api/images/") { s.mux.ServeHTTP(w, r) return } 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 ab693b1..b2941d9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -128,6 +128,22 @@ var DEFAULT_TERMINAL_THEMES = map[string]TerminalTheme{ }, } +func migrateProviders(cfg *MuyueConfig) { + defaults := Default().AI.Providers + for _, dp := range defaults { + found := false + for _, p := range cfg.AI.Providers { + if p.Name == dp.Name { + found = true + break + } + } + if !found { + cfg.AI.Providers = append(cfg.AI.Providers, dp) + } + } +} + func GetTerminalTheme(name string) TerminalTheme { if theme, ok := DEFAULT_TERMINAL_THEMES[name]; ok { return theme @@ -206,6 +222,8 @@ func Load() (*MuyueConfig, error) { } } + migrateProviders(&cfg) + return &cfg, nil } @@ -271,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, }, @@ -303,6 +321,7 @@ func Default() *MuyueConfig { cfg.Terminal.CustomPrompt = true cfg.Terminal.PromptTheme = "zerotwo" + cfg.Terminal.FontSize = 14 return cfg } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 235c41e..57e95ae 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -20,14 +20,42 @@ var thinkRegex = regexp.MustCompile(`(?s)<[Tt]hink[^>]*>.*?`) const maxHistorySize = 100 +type ContentPart struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL *ImageURL `json:"image_url,omitempty"` +} + +type ImageURL struct { + URL string `json:"url"` +} + type Message struct { Role string `json:"role"` - Content string `json:"content,omitempty"` + Content json.RawMessage `json:"content,omitempty"` ToolCalls []ToolCallMsg `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` Name string `json:"name,omitempty"` } +func TextContent(s string) json.RawMessage { + b, _ := json.Marshal(s) + return b +} + +func PartsContent(parts []ContentPart) json.RawMessage { + b, _ := json.Marshal(parts) + return b +} + +func (m Message) ContentString() string { + var s string + if json.Unmarshal(m.Content, &s) == nil { + return s + } + return string(m.Content) +} + type ToolCallMsg struct { ID string `json:"id"` Type string `json:"type"` @@ -143,7 +171,7 @@ func (o *Orchestrator) Send(userMessage string) (string, error) { o.histMu.Lock() o.history = append(o.history, Message{ Role: "user", - Content: userMessage, + Content: TextContent(userMessage), }) if len(o.history) > maxHistorySize { @@ -152,7 +180,7 @@ func (o *Orchestrator) Send(userMessage string) (string, error) { messages := make([]Message, 0, len(o.history)+1) if o.systemPrompt != "" { - messages = append(messages, Message{Role: "system", Content: o.systemPrompt}) + messages = append(messages, Message{Role: "system", Content: TextContent(o.systemPrompt)}) } messages = append(messages, o.history...) @@ -173,7 +201,7 @@ func (o *Orchestrator) Send(userMessage string) (string, error) { o.histMu.Lock() o.history = append(o.history, Message{ Role: "assistant", - Content: content, + Content: TextContent(content), }) _ = providerName o.histMu.Unlock() @@ -185,7 +213,7 @@ func (o *Orchestrator) SendStream(userMessage string, onChunk func(string)) (str o.histMu.Lock() o.history = append(o.history, Message{ Role: "user", - Content: userMessage, + Content: TextContent(userMessage), }) if len(o.history) > maxHistorySize { @@ -194,7 +222,7 @@ func (o *Orchestrator) SendStream(userMessage string, onChunk func(string)) (str messages := make([]Message, 0, len(o.history)+1) if o.systemPrompt != "" { - messages = append(messages, Message{Role: "system", Content: o.systemPrompt}) + messages = append(messages, Message{Role: "system", Content: TextContent(o.systemPrompt)}) } messages = append(messages, o.history...) @@ -273,7 +301,7 @@ func (o *Orchestrator) SendStream(userMessage string, onChunk func(string)) (str o.histMu.Lock() o.history = append(o.history, Message{ Role: "assistant", - Content: content, + Content: TextContent(content), }) o.histMu.Unlock() @@ -283,7 +311,7 @@ func (o *Orchestrator) SendStream(userMessage string, onChunk func(string)) (str func (o *Orchestrator) SendWithTools(messages []Message) (*ChatResponse, error) { fullMessages := make([]Message, 0, len(messages)+1) if o.systemPrompt != "" { - fullMessages = append(fullMessages, Message{Role: "system", Content: o.systemPrompt}) + fullMessages = append(fullMessages, Message{Role: "system", Content: TextContent(o.systemPrompt)}) } fullMessages = append(fullMessages, messages...) @@ -314,7 +342,7 @@ type ChunkCallback func(content string, toolCalls []ToolCallMsg) func (o *Orchestrator) SendWithToolsStream(messages []Message, onChunk ChunkCallback) (*ChatResponse, error) { fullMessages := make([]Message, 0, len(messages)+1) if o.systemPrompt != "" { - fullMessages = append(fullMessages, Message{Role: "system", Content: o.systemPrompt}) + fullMessages = append(fullMessages, Message{Role: "system", Content: TextContent(o.systemPrompt)}) } fullMessages = append(fullMessages, messages...) diff --git a/internal/version/version.go b/internal/version/version.go index a7385cd..17df0a3 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -7,7 +7,7 @@ import ( const ( Name = "muyue" - Version = "0.3.5" + Version = "0.4.0" Author = "La Légion de Muyue" ) diff --git a/internal/workflow/planner.go b/internal/workflow/planner.go index ccec148..1794879 100644 --- a/internal/workflow/planner.go +++ b/internal/workflow/planner.go @@ -27,7 +27,7 @@ func (p *Planner) GeneratePlan(ctx context.Context, goal string) ([]Step, error) prompt := buildPlanPrompt(goal) messages := []orchestrator.Message{ - {Role: "user", Content: prompt}, + {Role: "user", Content: orchestrator.TextContent(prompt)}, } resp, err := p.orchestrator.SendWithTools(messages) @@ -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 3845dea..f67b839 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'), @@ -61,15 +62,15 @@ const api = { clearShellChat: () => request('/shell/chat/clear', { method: 'POST' }), analyzeSystem: () => request('/shell/analyze', { method: 'POST' }), getShellAnalysis: () => request('/shell/analysis'), - sendChat: (message, stream = true, onChunk, signal) => { + sendChat: (message, stream = true, onChunk, signal, images = []) => { if (!stream) { - return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false }) }) + return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false, images }) }) } return new Promise((resolve, reject) => { fetch(`${API_BASE}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message, stream: true }), + body: JSON.stringify({ message, stream: true, images }), signal, }).then(async (res) => { if (!res.ok) { @@ -141,7 +142,11 @@ const api = { if (data.error) { reject(new Error(data.error)); return } if (data.done) { resolve({ content: full, tokens: data.tokens }); return } if (data.content) { - full = data.content + full += data.content + if (onChunk) onChunk(full, data) + } else if (data.tool_call || data.tool_result) { + if (onChunk) onChunk(full, data) + } else if (data.thinking !== undefined || data.thinking_end) { if (onChunk) onChunk(full, data) } } catch {} diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx index 563a7ba..13925e6 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') }, ], @@ -154,7 +152,7 @@ export default function App() {