diff --git a/CHANGELOG.md b/CHANGELOG.md index f7497cb..4a3ab58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,473 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## v0.6.0 + +### Audit & corrections (sécurité, concurrence, stabilité) + +- fix(api): empty `resp.Choices[0]` panic in chat engine — bounded check +- fix(api): `defer release()` accumulating inside tool-call loop — release immediately after each tool call +- fix(api): race in `ConversationStoreMulti.Add` (fire-and-forget save under released lock) — synchronous save under existing lock +- fix(workflow): infinite busy-wait in `engine.Execute` when a dependency fails — propagate `StatusFailed`/`StatusSkipped` and short-circuit +- fix(workflow): UTF-8-unsafe slicing of plan goal — rune-aware truncate +- fix(security): CORS `Access-Control-Allow-Origin: *` — restricted to localhost origins +- fix(security): API key disclosure in `/api/providers` — masked as `"***"`; saving handler ignores `"***"` placeholder +- fix(security): SSH password disclosure in `/api/terminal/sessions` — masked; update handler preserves stored password if `"***"` is sent +- fix(security): sshpass `-p` + `-e` mutually-exclusive flags — use only `-e` with `SSHPASS` env var +- fix(security): unbounded chat request body — `MaxBytesReader` 50 MB +- fix(security): unbounded image upload — 10 MB cap in `saveImage` +- fix(security): font size unbounded — capped at 72 +- fix(security): `LSP /auto-install` accepted arbitrary `project_dir` — restricted to user home subtree +- fix(api): silent `json.Unmarshal` errors in profile save — propagated +- fix(ui): operator-precedence bug in `Shell.jsx` resize check — parenthesized + +### Nouvelles fonctionnalités + +- feat(ai): inject OS name (e.g. `Debian 12`, `Windows 11`, `macOS 14.5`) alongside date in Studio system prompt +- feat(agents): default timeout raised to 30 minutes for `crush_run` and `claude_run`; max also 30 min +- feat(agents): new optional params `cwd`, `wsl_distro`, `wsl_user` — agents can be launched in a specific directory, and on Windows hosts inside a specific WSL distribution under a specific user +- feat(agents): new `claude_run` tool (mirrors `crush_run` for the Claude Code CLI) +- feat(terminal): WSL distros listed individually as quick-launch entries in the new-tab menu (Windows hosts only) +- feat(studio): system prompt rewritten around the BMAD-METHOD (Analyst/PM/Architect/SM/Dev/QA personas + mandatory `[OBJECTIF]/[CONTEXTE]/[CONTRAINTES]/[LIVRABLE]/[CRITÈRE D'ACCEPTATION]` template for any agent delegation) +- feat(studio): "Réflexion avancée" toggle — when enabled, the inactive AI provider produces a preliminary report that is injected as `[RAPPORT PRÉALABLE]` context into the active provider's prompt +- feat(studio): "Historique compressé" toggle — collapses past tool calls and keeps only the last visible action per assistant message, with `Tout afficher` to expand + +### Bug fix CI + +- fix(test): `cleanAIResponse` → `CleanAIResponse` in `orchestrator_test.go` (was failing `go vet`) + +## v0.4.0 + +### Changes since v0.3.5 + +- fix: token persistence, context windows, CSS tables/bullets/hr, image attachments (12000e5) +- feat: terminal sudo blocking, token tracking, mermaid & consumption UI (cb3d357) +- fix(shell,config): terminal font size, AI tools, provider keys (0830e64) +- chore: update CHANGELOG for v0.3.5 (b43e335) +- fix(shell): set default terminal fontSize to 6px (a60435d) +- fix(shell): default fontSize 10px and init new tabs immediately (6b0fcfb) +- feat(shell): add Ctrl+/- zoom and display all shortcuts in footer (df46b5c) +- fix(deps): upgrade @xterm/xterm to 6.1.0-beta.203 for addon compatibility (7240813) +- fix(shell): enable allowProposedApi for Unicode11 addon (97bfb80) +- fix(ci): add .npmrc with legacy-peer-deps for xterm addon resolution (3104179) +- feat(shell): integrate Hyper-like terminal technologies (WebGL, search, unicode11, image) (e21b47a) +- fix(shell): restore all missing imports, constants, and utility functions (2e98701) +- fix(shell): add missing Monitor import from lucide-react (f9d56de) +- fix(shell): restore missing MAX_TABS, TABS_STORAGE_KEY, TERMINAL_BUFFER_KEY constants (0e73408) +- fix(shell): add missing useI18n import (3b819be) +- fix(shell): remove stray 'impo' typo causing ReferenceError (c607943) +- fix(terminal): improve dimensions handling and add system theme for xterm (3312005) +- fix(shell): resolve savedTabs undefined ReferenceError in activeTab init (6cc86b7) +- fix(terminal): improve dimension calculation and tab init reliability (1885616) +- fix(dashboard): show MiMo quota instead of ZAI on dashboard (c8506d4) +- feat(ai): add Xiaomi MiMo provider, ZAI as last-resort fallback (68acabd) +- fix(terminal): use absolute positioning for content panels (b80562a) +- feat(terminal): add Ctrl+Shift+C/V copy/paste shortcuts (c562972) +- fix(shell): prevent Enter in AI chat from leaking to terminal (3651f62) +- fix(terminal): improve terminal dimensions and fit timing (18e8347) +- fix(terminal): detect shell tab visibility via MutationObserver (6596d86) +- fix(terminal): init all tabs on load, fix excessive zoom (9fb5aa8) +- fix(terminal): improve tab visibility checks and positioning (ab3641d) +- fix(ui): adjust global CSS styles (5dac191) +- fix(terminal): use display:none instead of visibility for tab hiding (e6da61f) +- feat(ui): refactor copy state to Set and add helper functions (a994749) +- feat(ui): add recentUnique to deduplicate recent commands in Dashboard (b394ef9) +- feat(ui): redesign recent commands display and fix terminal visibility (fca5344) +- fix(shell): initialize activeTabRef with activeTab and move useEffect (0a3123e) +- fix(config): remove unused import, reorder hooks, and improve variable naming (e6447f2) +- fix(studio): add tool results serialization and improve message handling (16c5ed6) +- fix(shell): improve tab reference stability and command queueing (e8924be) +- fix(shell): add debug logging for tab tracking and WebSocket state (a905f22) +- fix(terminal): refactor WebSocket cleanup, buffer management, and disposal (183dd27) +- fix(terminal): refactor WS cleanup, improve clear detection, fix sendToTerminal (203f57f) +- fix: restore buffer after WebSocket init, fix clear detection, fix streaming chunks (a1046da) +- refactor: remove locale panel, improve provider validation and terminal buffer persistence (02ee41c) +- bump: v0.3.5 (06810be) +- fix: display all quota models, center card content vertically (8db3bd7) +- fix: AI terminal init, Shift+Tab nav, code block rendering, command filtering (20237c0) +- fix(shell): set default terminal fontSize to 6px (9a218b1) +- fix(shell): default fontSize 10px and init new tabs immediately (399b845) +- feat(shell): add Ctrl+/- zoom and display all shortcuts in footer (436d5c6) +- fix(deps): upgrade @xterm/xterm to 6.1.0-beta.203 for addon compatibility (5a9edc0) +- fix(shell): enable allowProposedApi for Unicode11 addon (5bdc7a6) +- fix(ci): add .npmrc with legacy-peer-deps for xterm addon resolution (5a0480b) +- feat(shell): integrate Hyper-like terminal technologies (WebGL, search, unicode11, image) (80de4dd) +- fix(shell): restore all missing imports, constants, and utility functions (de52f4e) +- fix(shell): add missing Monitor import from lucide-react (98ff0dd) +- fix(shell): restore missing MAX_TABS, TABS_STORAGE_KEY, TERMINAL_BUFFER_KEY constants (9a1ff6e) +- fix(shell): add missing useI18n import (034b9ee) +- fix(shell): remove stray 'impo' typo causing ReferenceError (c1b1fc6) +- fix(terminal): improve dimensions handling and add system theme for xterm (50ca751) +- fix(shell): resolve savedTabs undefined ReferenceError in activeTab init (b8aa935) +- fix(terminal): improve dimension calculation and tab init reliability (5627ddd) +- fix(dashboard): show MiMo quota instead of ZAI on dashboard (d278725) +- feat(ai): add Xiaomi MiMo provider, ZAI as last-resort fallback (7d0f807) +- fix(terminal): use absolute positioning for content panels (cbf623b) +- feat(terminal): add Ctrl+Shift+C/V copy/paste shortcuts (b85ebb8) +- fix(shell): prevent Enter in AI chat from leaking to terminal (7cc206d) +- fix(terminal): improve terminal dimensions and fit timing (bf8c0fd) +- fix(terminal): detect shell tab visibility via MutationObserver (08dc1fd) +- fix(terminal): init all tabs on load, fix excessive zoom (13e937a) +- fix(terminal): improve tab visibility checks and positioning (3cf701b) +- fix(ui): adjust global CSS styles (3a09e0e) +- fix(terminal): use display:none instead of visibility for tab hiding (47fa2e0) +- feat(ui): refactor copy state to Set and add helper functions (401292e) +- feat(ui): add recentUnique to deduplicate recent commands in Dashboard (199a7e4) +- feat(ui): redesign recent commands display and fix terminal visibility (c91931f) +- fix(shell): initialize activeTabRef with activeTab and move useEffect (cbbb224) +- fix(config): remove unused import, reorder hooks, and improve variable naming (8d10d21) +- fix(studio): add tool results serialization and improve message handling (e9696ef) +- fix(shell): improve tab reference stability and command queueing (1edd4f0) +- fix(shell): add debug logging for tab tracking and WebSocket state (92f943c) +- fix(terminal): refactor WebSocket cleanup, buffer management, and disposal (1704b19) +- fix(terminal): refactor WS cleanup, improve clear detection, fix sendToTerminal (40ec493) +- fix: restore buffer after WebSocket init, fix clear detection, fix streaming chunks (233368c) +- refactor: remove locale panel, improve provider validation and terminal buffer persistence (00118f0) +- chore: update CHANGELOG for v0.3.4 (c39203c) +- feat(dashboard): single-view grid with live CPU/RAM/Net graphs, API quota, processes, and sudo indicator (328e9e6) +- feat(dashboard): add quota monitoring, process list, and command history (c81ebb4) +- refactor(chat): deduplicate streaming code, add multi-conv, and XSS protection (b0865bc) +- fix(studio): improve chat context, thinking tags, streaming, and tool results (0d8e1b1) +- feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard (485e085) +- feat(agent): refactor AI chat with streaming, agent registry, and tool execution (61da803) +- feat(onboarding): add minimax api key step and AI-powered editor scan (65df154) +- fix(onboarding): require fields before advancing steps (b6147dd) +- fix: register missing /api/config/reset and /api/starship/apply-theme routes (275a9a4) +- fix(config): per-provider form state to avoid field cross-talk (e92a2f0) +- fix(onboarding): auto-save on done step, keyboard nav, error feedback (1f12b8a) +- feat(config): add system panel with reset and starship theme, add onboarding wizard (9188231) +- chore: update CHANGELOG for v0.3.2 (28e5113) +- chore: update CHANGELOG for v0.3.2-beta.1 (51a599f) +- chore: update CHANGELOG for v0.3.1 (5b4a70e) + +### Downloads + +| Platform | File | +|----------|------| +| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-linux-amd64.tar.gz) | +| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-linux-arm64.tar.gz) | +| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-darwin-amd64.tar.gz) | +| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-darwin-arm64.tar.gz) | +| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-windows-amd64.zip) | +| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-windows-arm64.zip) | + +The binary includes both CLI and Desktop modes. +Run `muyue` for TUI, `muyue desktop` for web UI. + +### Install + +**Linux (x86_64)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-linux-amd64.tar.gz | tar xz +chmod +x muyue-linux-amd64 +sudo mv muyue-linux-amd64 /usr/local/bin/muyue +``` + +**macOS (Apple Silicon)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-darwin-arm64.tar.gz | tar xz +chmod +x muyue-darwin-arm64 +sudo mv muyue-darwin-arm64 /usr/local/bin/muyue +``` + +**Windows (x86_64)** +```powershell +Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.4.0/muyue-windows-amd64.zip" -OutFile "muyue.zip" +Expand-Archive -Path "muyue.zip" -DestinationPath "." +Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe +``` + + +## v0.3.5 + +### Changes since v0.3.5 + +- fix(shell): set default terminal fontSize to 6px (a60435d) +- fix(shell): default fontSize 10px and init new tabs immediately (6b0fcfb) +- feat(shell): add Ctrl+/- zoom and display all shortcuts in footer (df46b5c) +- fix(deps): upgrade @xterm/xterm to 6.1.0-beta.203 for addon compatibility (7240813) +- fix(shell): enable allowProposedApi for Unicode11 addon (97bfb80) +- fix(ci): add .npmrc with legacy-peer-deps for xterm addon resolution (3104179) +- feat(shell): integrate Hyper-like terminal technologies (WebGL, search, unicode11, image) (e21b47a) +- fix(shell): restore all missing imports, constants, and utility functions (2e98701) +- fix(shell): add missing Monitor import from lucide-react (f9d56de) +- fix(shell): restore missing MAX_TABS, TABS_STORAGE_KEY, TERMINAL_BUFFER_KEY constants (0e73408) +- fix(shell): add missing useI18n import (3b819be) +- fix(shell): remove stray 'impo' typo causing ReferenceError (c607943) +- fix(terminal): improve dimensions handling and add system theme for xterm (3312005) +- fix(shell): resolve savedTabs undefined ReferenceError in activeTab init (6cc86b7) +- fix(terminal): improve dimension calculation and tab init reliability (1885616) +- fix(dashboard): show MiMo quota instead of ZAI on dashboard (c8506d4) +- feat(ai): add Xiaomi MiMo provider, ZAI as last-resort fallback (68acabd) +- fix(terminal): use absolute positioning for content panels (b80562a) +- feat(terminal): add Ctrl+Shift+C/V copy/paste shortcuts (c562972) +- fix(shell): prevent Enter in AI chat from leaking to terminal (3651f62) +- fix(terminal): improve terminal dimensions and fit timing (18e8347) +- fix(terminal): detect shell tab visibility via MutationObserver (6596d86) +- fix(terminal): init all tabs on load, fix excessive zoom (9fb5aa8) +- fix(terminal): improve tab visibility checks and positioning (ab3641d) +- fix(ui): adjust global CSS styles (5dac191) +- fix(terminal): use display:none instead of visibility for tab hiding (e6da61f) +- feat(ui): refactor copy state to Set and add helper functions (a994749) +- feat(ui): add recentUnique to deduplicate recent commands in Dashboard (b394ef9) +- feat(ui): redesign recent commands display and fix terminal visibility (fca5344) +- fix(shell): initialize activeTabRef with activeTab and move useEffect (0a3123e) +- fix(config): remove unused import, reorder hooks, and improve variable naming (e6447f2) +- fix(studio): add tool results serialization and improve message handling (16c5ed6) +- fix(shell): improve tab reference stability and command queueing (e8924be) +- fix(shell): add debug logging for tab tracking and WebSocket state (a905f22) +- fix(terminal): refactor WebSocket cleanup, buffer management, and disposal (183dd27) +- fix(terminal): refactor WS cleanup, improve clear detection, fix sendToTerminal (203f57f) +- fix: restore buffer after WebSocket init, fix clear detection, fix streaming chunks (a1046da) +- refactor: remove locale panel, improve provider validation and terminal buffer persistence (02ee41c) +- bump: v0.3.5 (06810be) +- fix: display all quota models, center card content vertically (8db3bd7) +- fix: AI terminal init, Shift+Tab nav, code block rendering, command filtering (20237c0) +- chore: update CHANGELOG for v0.3.4 (c39203c) +- feat(dashboard): single-view grid with live CPU/RAM/Net graphs, API quota, processes, and sudo indicator (328e9e6) +- feat(dashboard): add quota monitoring, process list, and command history (c81ebb4) +- refactor(chat): deduplicate streaming code, add multi-conv, and XSS protection (b0865bc) +- fix(studio): improve chat context, thinking tags, streaming, and tool results (0d8e1b1) +- feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard (485e085) +- feat(agent): refactor AI chat with streaming, agent registry, and tool execution (61da803) +- feat(onboarding): add minimax api key step and AI-powered editor scan (65df154) +- fix(onboarding): require fields before advancing steps (b6147dd) +- fix: register missing /api/config/reset and /api/starship/apply-theme routes (275a9a4) +- fix(config): per-provider form state to avoid field cross-talk (e92a2f0) +- fix(onboarding): auto-save on done step, keyboard nav, error feedback (1f12b8a) +- feat(config): add system panel with reset and starship theme, add onboarding wizard (9188231) +- chore: update CHANGELOG for v0.3.2 (28e5113) +- chore: update CHANGELOG for v0.3.2-beta.1 (51a599f) +- chore: update CHANGELOG for v0.3.1 (5b4a70e) + +### Downloads + +| Platform | File | +|----------|------| +| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-linux-amd64.tar.gz) | +| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-linux-arm64.tar.gz) | +| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-darwin-amd64.tar.gz) | +| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-darwin-arm64.tar.gz) | +| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-windows-amd64.zip) | +| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-windows-arm64.zip) | + +The binary includes both CLI and Desktop modes. +Run `muyue` for TUI, `muyue desktop` for web UI. + +### Install + +**Linux (x86_64)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-linux-amd64.tar.gz | tar xz +chmod +x muyue-linux-amd64 +sudo mv muyue-linux-amd64 /usr/local/bin/muyue +``` + +**macOS (Apple Silicon)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-darwin-arm64.tar.gz | tar xz +chmod +x muyue-darwin-arm64 +sudo mv muyue-darwin-arm64 /usr/local/bin/muyue +``` + +**Windows (x86_64)** +```powershell +Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.5/muyue-windows-amd64.zip" -OutFile "muyue.zip" +Expand-Archive -Path "muyue.zip" -DestinationPath "." +Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe +``` + + +## v0.3.4 + +### Changes since v0.3.3 + +- fix(ci): replace jq with python3 in release step, add debug output (7ae4017) +- feat: AI terminal, Z.AI quota, /model change, formatting fixes, update redirects (8c540eb) +- feat(studio): Tab focuses textarea, autocomplete commands (1074b01) +- fix(studio): convert newlines to
in AI message rendering (2da0cf9) +- fix(config): replace hardcoded model list with free text input (9987a58) +- feat(config): providers panel shows only MINIMAX/ZAI with model selector (2827acf) +- feat(dashboard): show top 5 most used commands as clickable chips (afb6e77) +- fix: tab containers height, dashboard 2-row grid, studio scroll buttons (84be226) +- feat(shell): dedicated System Analyst AI, no code execution, analyze system (f9c4cf1) +- fix: keep all tabs mounted, switch via CSS display instead of unmount (eda7293) +- refactor(config): locale panel with edit/save flow like profile (b55feae) +- feat(config): split profile into Personal Info + Preferences sections, centered (54621bd) +- feat(studio): improve context compression UI and provider display (6bad294) +- fix(config): locale panel show active language/keyboard, add save button (92eb783) +- feat(config): dynamic profile panel, generic save, tabs margin fix (8005e97) +- fix(dashboard): remove bg graphs, add scrollable lists, show used/total quota (6e76e7d) +- feat(chat): add auto-summarization with token tracking UI (e8f6dc4) +- feat(dashboard): add background graphs to cards and improve layout (bb03c9f) +- feat(dashboard): single-view grid with live CPU/RAM/Net graphs, API quota, processes, and sudo indicator (79d0821) +- feat(dashboard): add quota monitoring, process list, and command history (7682717) +- refactor(chat): deduplicate streaming code, add multi-conv, and XSS protection (3948a4c) +- fix(studio): improve chat context, thinking tags, streaming, and tool results (65804aa) +- feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard (2e50366) +- feat(agent): refactor AI chat with streaming, agent registry, and tool execution (66b773f) +- feat(onboarding): add minimax api key step and AI-powered editor scan (bc5c295) +- fix(onboarding): require fields before advancing steps (e19122d) +- fix: register missing /api/config/reset and /api/starship/apply-theme routes (8b6a7e8) +- fix(config): per-provider form state to avoid field cross-talk (58f8cb0) +- fix(onboarding): auto-save on done step, keyboard nav, error feedback (b52fecc) +- feat(config): add system panel with reset and starship theme, add onboarding wizard (5bbac49) + +### Downloads + +| Platform | File | +|----------|------| +| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-linux-amd64.tar.gz) | +| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-linux-arm64.tar.gz) | +| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-darwin-amd64.tar.gz) | +| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-darwin-arm64.tar.gz) | +| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-windows-amd64.zip) | +| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-windows-arm64.zip) | + +The binary includes both CLI and Desktop modes. +Run `muyue` for TUI, `muyue desktop` for web UI. + +### Install + +**Linux (x86_64)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-linux-amd64.tar.gz | tar xz +chmod +x muyue-linux-amd64 +sudo mv muyue-linux-amd64 /usr/local/bin/muyue +``` + +**macOS (Apple Silicon)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-darwin-arm64.tar.gz | tar xz +chmod +x muyue-darwin-arm64 +sudo mv muyue-darwin-arm64 /usr/local/bin/muyue +``` + +**Windows (x86_64)** +```powershell +Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.4/muyue-windows-amd64.zip" -OutFile "muyue.zip" +Expand-Archive -Path "muyue.zip" -DestinationPath "." +Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe +``` + + +## v0.3.2 + +### Changes since v0.3.1 + +- chore: update CHANGELOG for v0.3.2-beta.1 (51a599f) +- fix: correct version from 3.2 to 0.3.2 (83d7a57) +- chore: bump version to 3.2 (0fe82f6) +- refactor(config): remove Terminal sub-tab from Configuration page (3b6cc38) +- fix(terminal): init payload never sent due to ws.onopen being overwritten (93a22d4) +- fix(terminal): improve shell resolution with better error handling and ws proxy support (e0e1e73) +- feat(studio): parse AI thinking and tool launch messages in terminal panel (0496ca7) +- fix(studio): forward AI thinking chunks to frontend instead of dropping them (b407ab8) +- feat(studio): add tool execution and hide AI thinking tags (12df184) +- fix(terminal): ignore invalid shell config from race condition (8af6d25) +- feat(shell): restore AI assistant panel (4fd599a) +- fix(terminal): restore terminal input and cursor visibility (bcba593) +- refactor(api): split monolithic handlers.go into focused modules (04b0fff) + +### Downloads + +| Platform | File | +|----------|------| +| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-linux-amd64.tar.gz) | +| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-linux-arm64.tar.gz) | +| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-darwin-amd64.tar.gz) | +| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-darwin-arm64.tar.gz) | +| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-windows-amd64.zip) | +| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-windows-arm64.zip) | + +The binary includes both CLI and Desktop modes. +Run `muyue` for TUI, `muyue desktop` for web UI. + +### Install + +**Linux (x86_64)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-linux-amd64.tar.gz | tar xz +chmod +x muyue-linux-amd64 +sudo mv muyue-linux-amd64 /usr/local/bin/muyue +``` + +**macOS (Apple Silicon)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-darwin-arm64.tar.gz | tar xz +chmod +x muyue-darwin-arm64 +sudo mv muyue-darwin-arm64 /usr/local/bin/muyue +``` + +**Windows (x86_64)** +```powershell +Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.2/muyue-windows-amd64.zip" -OutFile "muyue.zip" +Expand-Archive -Path "muyue.zip" -DestinationPath "." +Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe +``` + + +## v0.3.2-beta.1 (Beta) + +### Commits since v0.3.1 + +- fix: correct version from 3.2 to 0.3.2 (83d7a57) + +> This is a **beta** release. Use at your own risk. + +## v0.3.1 + +### Changes since v0.3.0 + +- refactor(config): remove Terminal sub-tab from Configuration page (95bd824) +- fix(terminal): init payload never sent due to ws.onopen being overwritten (252f178) +- fix(terminal): improve shell resolution with better error handling and ws proxy support (7dcf505) +- feat(studio): parse AI thinking and tool launch messages in terminal panel (8fb93fa) +- fix(studio): forward AI thinking chunks to frontend instead of dropping them (5ec373c) +- feat(studio): add tool execution and hide AI thinking tags (1eb5a6d) +- fix(terminal): ignore invalid shell config from race condition (cd5ebe0) +- feat(shell): restore AI assistant panel (2004c15) +- fix(terminal): restore terminal input and cursor visibility (9306152) +- refactor(api): split monolithic handlers.go into focused modules (e15a034) + +### Downloads + +| Platform | File | +|----------|------| +| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-linux-amd64.tar.gz) | +| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-linux-arm64.tar.gz) | +| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-darwin-amd64.tar.gz) | +| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-darwin-arm64.tar.gz) | +| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-windows-amd64.zip) | +| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-windows-arm64.zip) | + +The binary includes both CLI and Desktop modes. +Run `muyue` for TUI, `muyue desktop` for web UI. + +### Install + +**Linux (x86_64)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-linux-amd64.tar.gz | tar xz +chmod +x muyue-linux-amd64 +sudo mv muyue-linux-amd64 /usr/local/bin/muyue +``` + +**macOS (Apple Silicon)** +```bash +curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-darwin-arm64.tar.gz | tar xz +chmod +x muyue-darwin-arm64 +sudo mv muyue-darwin-arm64 /usr/local/bin/muyue +``` + +**Windows (x86_64)** +```powershell +Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.1/muyue-windows-amd64.zip" -OutFile "muyue.zip" +Expand-Archive -Path "muyue.zip" -DestinationPath "." +Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe +``` + + ## v0.3.0 ### Changes since v0.2.1 diff --git a/internal/agent/definitions.go b/internal/agent/definitions.go index 037ae56..4d01058 100644 --- a/internal/agent/definitions.go +++ b/internal/agent/definitions.go @@ -124,13 +124,16 @@ func NewTerminalTool() (*ToolDefinition, error) { } type CrushRunParams struct { - Task string `json:"task" description:"The task description for Crush to execute"` - Timeout int `json:"timeout,omitempty" description:"Maximum execution time in seconds (default 600, max 900)"` + Task string `json:"task" description:"The task description for Crush to execute"` + Timeout int `json:"timeout,omitempty" description:"Maximum execution time in seconds (default 1800, max 1800)"` + Cwd string `json:"cwd,omitempty" description:"Working directory in which to launch the agent (absolute path; falls back to user home)"` + WSLDistro string `json:"wsl_distro,omitempty" description:"On Windows host: WSL distribution to launch the agent in (e.g. 'Ubuntu')"` + WSLUser string `json:"wsl_user,omitempty" description:"On Windows host: WSL user to run the agent as"` } func NewCrushRunTool() (*ToolDefinition, error) { return NewTool("crush_run", - "Delegate a complex coding task to the Crush AI agent. Crush has access to file editing, code search, bash execution, and other development tools. Use this for multi-step coding tasks like refactoring, debugging, implementing features, or code review. Returns the agent's final output.", + "Delegate a complex coding task to the Crush AI agent. Crush has access to file editing, code search, bash execution, and other development tools. Use this for multi-step coding tasks like refactoring, debugging, implementing features, or code review. Optionally pass cwd to run in a specific directory, or wsl_distro/wsl_user to launch inside a WSL distribution under a specific user (Windows hosts only). Returns the agent's final output.", func(ctx context.Context, p CrushRunParams) (ToolResponse, error) { if p.Task == "" { return TextErrorResponse("task is required"), nil @@ -138,15 +141,18 @@ func NewCrushRunTool() (*ToolDefinition, error) { timeout := time.Duration(p.Timeout) * time.Second if timeout == 0 { - timeout = 600 * time.Second + timeout = 1800 * time.Second } - if timeout > 900*time.Second { - timeout = 900 * time.Second + if timeout > 1800*time.Second { + timeout = 1800 * time.Second } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - cmd := exec.CommandContext(ctx, "crush", "run", p.Task) + cmd, prepErr := buildAgentCommand(ctx, "crush", []string{"run", p.Task}, p.Cwd, p.WSLDistro, p.WSLUser) + if prepErr != nil { + return TextErrorResponse(prepErr.Error()), nil + } output, err := cmd.CombinedOutput() result := string(output) @@ -169,6 +175,58 @@ func NewCrushRunTool() (*ToolDefinition, error) { }) } +type ClaudeRunParams struct { + Task string `json:"task" description:"The task description for Claude Code to execute"` + Timeout int `json:"timeout,omitempty" description:"Maximum execution time in seconds (default 1800, max 1800)"` + Cwd string `json:"cwd,omitempty" description:"Working directory in which to launch the agent (absolute path; falls back to user home)"` + WSLDistro string `json:"wsl_distro,omitempty" description:"On Windows host: WSL distribution to launch the agent in (e.g. 'Ubuntu')"` + WSLUser string `json:"wsl_user,omitempty" description:"On Windows host: WSL user to run the agent as"` +} + +func NewClaudeRunTool() (*ToolDefinition, error) { + return NewTool("claude_run", + "Delegate a complex coding task to the Claude Code CLI agent. Claude has access to file editing, code search, bash execution. Use for multi-step coding tasks. Same cwd/wsl_distro/wsl_user options as crush_run.", + func(ctx context.Context, p ClaudeRunParams) (ToolResponse, error) { + if p.Task == "" { + return TextErrorResponse("task is required"), nil + } + + timeout := time.Duration(p.Timeout) * time.Second + if timeout == 0 { + timeout = 1800 * time.Second + } + if timeout > 1800*time.Second { + timeout = 1800 * time.Second + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + cmd, prepErr := buildAgentCommand(ctx, "claude", []string{"-p", p.Task}, p.Cwd, p.WSLDistro, p.WSLUser) + if prepErr != nil { + return TextErrorResponse(prepErr.Error()), nil + } + output, err := cmd.CombinedOutput() + + result := string(output) + if len(result) > 15000 { + result = result[:15000] + "\n... [truncated]" + } + + if err != nil { + errMsg := fmt.Sprintf("Claude error: %v", err) + if ctx.Err() == context.DeadlineExceeded { + errMsg = fmt.Sprintf("Claude timed out after %d seconds. Try splitting the task into smaller parts.", int(timeout.Seconds())) + } + if result != "" { + errMsg += "\n\n" + result + } + return TextErrorResponse(errMsg), nil + } + + return TextResponse(result), nil + }) +} + type ReadFileParams struct { Path string `json:"path" description:"Absolute or relative path to the file to read"` Offset int `json:"offset,omitempty" description:"Line number to start reading from (0-based, default 0)"` @@ -371,6 +429,7 @@ func DefaultRegistry() *Registry { tools := []*ToolDefinition{ must(NewTerminalTool()), must(NewCrushRunTool()), + must(NewClaudeRunTool()), must(NewReadFileTool()), must(NewListFilesTool()), must(NewSearchFilesTool()), diff --git a/internal/agent/impl.go b/internal/agent/impl.go index 53090cb..6934773 100644 --- a/internal/agent/impl.go +++ b/internal/agent/impl.go @@ -26,6 +26,43 @@ func detectShell() string { return "/bin/sh" } +var validIdentifier = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + +// buildAgentCommand assembles an agent execution command, optionally launching it +// inside a WSL distribution (Windows host only) and applying a working directory. +// On non-Windows hosts, wsl_* parameters are ignored. +func buildAgentCommand(ctx context.Context, bin string, args []string, cwd, wslDistro, wslUser string) (*exec.Cmd, error) { + if wslDistro != "" && runtime.GOOS == "windows" { + if !validIdentifier.MatchString(wslDistro) { + return nil, fmt.Errorf("invalid wsl_distro: %q", wslDistro) + } + if wslUser != "" && !validIdentifier.MatchString(wslUser) { + return nil, fmt.Errorf("invalid wsl_user: %q", wslUser) + } + wslArgs := []string{"-d", wslDistro} + if wslUser != "" { + wslArgs = append(wslArgs, "-u", wslUser) + } + if cwd != "" { + wslArgs = append(wslArgs, "--cd", cwd) + } + wslArgs = append(wslArgs, "--") + wslArgs = append(wslArgs, bin) + wslArgs = append(wslArgs, args...) + return exec.CommandContext(ctx, "wsl", wslArgs...), nil + } + + cmd := exec.CommandContext(ctx, bin, args...) + if cwd != "" { + dir := expandHome(cwd) + if info, err := os.Stat(dir); err != nil || !info.IsDir() { + return nil, fmt.Errorf("cwd does not exist or is not a directory: %s", cwd) + } + cmd.Dir = dir + } + return cmd, nil +} + func expandHome(path string) string { if path == "" { return "" diff --git a/internal/agent/prompts/studio_system.md b/internal/agent/prompts/studio_system.md index fc16ae4..d3b114d 100644 --- a/internal/agent/prompts/studio_system.md +++ b/internal/agent/prompts/studio_system.md @@ -1,6 +1,33 @@ -Tu es l'assistant IA de **Muyue Studio**, le centre de commandement de l'environnement de développement de l'utilisateur. +Tu es l'assistant IA de **Muyue Studio**, le centre de commandement de l'environnement de développement de l'utilisateur, et tu es spécialisé dans la **construction de prompts** selon la **méthode BMAD** (Breakthrough Method for Agile AI-Driven Development — https://github.com/bmad-code-org/BMAD-METHOD). -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. +Tu es intégré dans Muyue, un gestionnaire d'environnement de développement de bureau. Ton rôle est double : +1. Aider l'utilisateur à configurer, gérer et optimiser son environnement dev (avec les outils ci-dessous). +2. Construire pour lui des prompts structurés et actionnables avant d'exécuter une tâche complexe ou de la déléguer à un agent (`crush_run`, `claude_run`). + +## Méthode BMAD — principes appliqués à chaque réponse + +BMAD organise le travail IA comme une équipe agile : chaque demande est traitée avec une persona spécifique (Analyst, PM, Architect, SM, Dev, QA) puis exécutée. Tu n'as pas besoin de jouer toutes les personas — applique simplement leurs réflexes : + +- **Analyst** : reformule l'objectif réel derrière la demande en 1 phrase. S'il est ambigu, choisis l'interprétation la plus probable et indique-la au début. +- **PM** : découpe en livrables concrets (épopée → stories). Pas plus de 3-5 stories pour une demande, chaque story doit être indépendamment livrable. +- **Architect** : pour toute story qui touche au code, identifie les fichiers concernés, les contraintes (compat, style, perf, sécurité) et les risques avant d'écrire. +- **SM (Scrum Master)** : si tu délègues à `crush_run`/`claude_run`, fournis un prompt **autonome** : objectif, contraintes, fichiers cibles, critère d'acceptation. Pas de référence à la conversation parente — l'agent ne la voit pas. +- **Dev** : exécute story par story. Vérifie chaque livraison avant de passer à la suivante. +- **QA** : avant de répondre "fini", relis l'objectif initial et confirme qu'il est atteint. + +## Format d'un prompt BMAD délégué + +Quand tu construis un prompt pour `crush_run`/`claude_run`, suis ce gabarit : + +``` +[OBJECTIF] +[CONTEXTE] +[CONTRAINTES] +[LIVRABLE] +[CRITÈRE D'ACCEPTATION] +``` + +Ce gabarit est **obligatoire** pour toute délégation à un agent. Il évite que l'agent erre, suppose, ou produise du code hors-périmètre. 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. diff --git a/internal/api/chat_engine.go b/internal/api/chat_engine.go index e4146e9..93b8d89 100644 --- a/internal/api/chat_engine.go +++ b/internal/api/chat_engine.go @@ -3,6 +3,7 @@ package api import ( "context" "encoding/json" + "fmt" "net/http" "github.com/muyue/muyue/internal/agent" @@ -85,6 +86,9 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. ce.TotalTokens += resp.Usage.TotalTokens } + if len(resp.Choices) == 0 { + return finalContent, allToolCalls, allToolResults, fmt.Errorf("empty response from provider") + } choice := resp.Choices[0] content := orchestrator.CleanAIResponse(cleanThinkingTags(choice.Message.Content)) @@ -124,8 +128,9 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. Arguments: json.RawMessage(tc.Function.Arguments), } + var release func() if ce.limiter != nil { - release, limitErr := ce.limiter(tc.Function.Name) + rel, limitErr := ce.limiter(tc.Function.Name) if limitErr != nil { limResultData := map[string]interface{}{ "tool_call_id": tc.ID, @@ -150,10 +155,13 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator. }) continue } - defer release() + release = rel } result, execErr := ce.registry.Execute(ctx, call) + if release != nil { + release() + } if execErr != nil { result = agent.ToolResponse{ Content: execErr.Error(), @@ -216,6 +224,9 @@ func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator. ce.TotalTokens += resp.Usage.TotalTokens } + if len(resp.Choices) == 0 { + return finalContent, fmt.Errorf("empty response from provider") + } choice := resp.Choices[0] content := orchestrator.CleanAIResponse(cleanThinkingTags(choice.Message.Content)) @@ -241,8 +252,9 @@ func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator. Arguments: json.RawMessage(tc.Function.Arguments), } + var release func() if ce.limiter != nil { - release, limitErr := ce.limiter(tc.Function.Name) + rel, limitErr := ce.limiter(tc.Function.Name) if limitErr != nil { messages = append(messages, orchestrator.Message{ Role: "tool", @@ -252,10 +264,13 @@ func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator. }) continue } - defer release() + release = rel } result, execErr := ce.registry.Execute(ctx, call) + if release != nil { + release() + } if execErr != nil { result = agent.ToolResponse{ Content: execErr.Error(), @@ -310,6 +325,5 @@ func SetupSSEHeaders(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) } \ No newline at end of file diff --git a/internal/api/conversation_multi.go b/internal/api/conversation_multi.go index bb488e6..43df8e3 100644 --- a/internal/api/conversation_multi.go +++ b/internal/api/conversation_multi.go @@ -222,9 +222,9 @@ func (cs *ConversationStoreMulti) Add(role, content string) FeedMessage { Time: time.Now().Format(time.RFC3339), } conv.Messages = append(conv.Messages, msg) - - go cs.saveCurrent() // Fire and forget - + + cs.saveCurrent() + return msg } diff --git a/internal/api/handlers_chat.go b/internal/api/handlers_chat.go index 8e2d920..641263c 100644 --- a/internal/api/handlers_chat.go +++ b/internal/api/handlers_chat.go @@ -16,6 +16,7 @@ import ( "github.com/muyue/muyue/internal/agent" "github.com/muyue/muyue/internal/orchestrator" + "github.com/muyue/muyue/internal/platform" ) var thinkingTagRegex = regexp.MustCompile(`(?s)<[Tt]hink[^>]*>.*?`) @@ -131,10 +132,12 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { writeError(w, "POST only", http.StatusMethodNotAllowed) return } + r.Body = http.MaxBytesReader(w, r.Body, 50*1024*1024) var body struct { - Message string `json:"message"` - Stream bool `json:"stream"` - Images []ImageAttachment `json:"images"` + Message string `json:"message"` + Stream bool `json:"stream"` + Images []ImageAttachment `json:"images"` + AdvancedReflection bool `json:"advanced_reflection"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, err.Error(), http.StatusBadRequest) @@ -194,7 +197,12 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { } 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"))) + sysInfo := platform.Detect() + osName := sysInfo.OSName + if osName == "" { + osName = string(sysInfo.OS) + } + studioPrompt.WriteString(fmt.Sprintf("\nDate: %s\nHeure: %s\nSystème: %s\n", time.Now().Format("02/01/2006"), time.Now().Format("15:04:05"), osName)) canSudo := !agent.NeedsSudoPassword() studioPrompt.WriteString(fmt.Sprintf("Root: %t\n", !canSudo)) if !canSudo { @@ -205,6 +213,12 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { orb.SetSystemPrompt(studioPrompt.String()) orb.SetTools(s.agentToolsJSON) + if body.AdvancedReflection { + if report, ok := s.runReflectionReport(enrichedMessage); ok { + enrichedMessage = enrichedMessage + "\n\n[RAPPORT PRÉALABLE — produit par un autre modèle, à valider]\n" + report + "\n[/RAPPORT PRÉALABLE]" + } + } + if body.Stream { s.handleStreamChat(w, orb, enrichedMessage) } else { @@ -281,6 +295,23 @@ func cleanThinkingTags(content string) string { return strings.TrimSpace(thinkingTagRegex.ReplaceAllString(content, "")) } +// runReflectionReport runs the inactive AI provider on the user message to +// produce a preliminary analysis report that the active provider will then +// use as additional context. Returns ("", false) if no inactive provider is +// configured or on error — the caller falls back to a normal chat flow. +func (s *Server) runReflectionReport(userMessage string) (string, bool) { + orb, err := orchestrator.NewForInactiveProvider(s.config) + if err != nil { + return "", false + } + orb.SetSystemPrompt("Tu es un analyste. Pour la question ci-dessous, produis un rapport bref (max 15 lignes) qui : (1) reformule l'objectif de l'utilisateur, (2) liste les points à clarifier ou les risques, (3) suggère une approche structurée. Pas de code, pas d'action — uniquement de l'analyse.") + resp, err := orb.SendNoTools(userMessage) + if err != nil { + return "", false + } + return strings.TrimSpace(resp), true +} + func (s *Server) buildContextMessages(userMessage string) []orchestrator.Message { history := s.convStore.Get() diff --git a/internal/api/handlers_config.go b/internal/api/handlers_config.go index 044037a..18c154f 100644 --- a/internal/api/handlers_config.go +++ b/internal/api/handlers_config.go @@ -60,10 +60,17 @@ func (s *Server) handleSaveProfile(w http.ResponseWriter, r *http.Request) { return } var currentMap map[string]interface{} - json.Unmarshal(currentJSON, ¤tMap) + if err := json.Unmarshal(currentJSON, ¤tMap); err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } var updates map[string]interface{} - body, _ := io.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) + if err != nil { + writeError(w, err.Error(), http.StatusBadRequest) + return + } if err := json.Unmarshal(body, &updates); err != nil { writeError(w, err.Error(), http.StatusBadRequest) return @@ -71,8 +78,15 @@ func (s *Server) handleSaveProfile(w http.ResponseWriter, r *http.Request) { deepMerge(currentMap, updates) - mergedJSON, _ := json.Marshal(currentMap) - json.Unmarshal(mergedJSON, &s.config.Profile) + mergedJSON, err := json.Marshal(currentMap) + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + if err := json.Unmarshal(mergedJSON, &s.config.Profile); err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } if err := config.Save(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) @@ -122,7 +136,7 @@ func (s *Server) handleSaveProvider(w http.ResponseWriter, r *http.Request) { found := false for i := range s.config.AI.Providers { if s.config.AI.Providers[i].Name == body.Name { - if body.APIKey != "" { + if body.APIKey != "" && body.APIKey != "***" { s.config.AI.Providers[i].APIKey = body.APIKey } if body.Model != "" { @@ -173,6 +187,14 @@ func (s *Server) handleValidateProvider(w http.ResponseWriter, r *http.Request) writeError(w, "api_key required", http.StatusBadRequest) return } + if body.APIKey == "***" { + for _, p := range s.config.AI.Providers { + if p.Name == body.Name { + body.APIKey = p.APIKey + break + } + } + } baseURL := body.BaseURL if baseURL == "" { @@ -266,7 +288,7 @@ func (s *Server) handleSaveTerminalSettings(w http.ResponseWriter, r *http.Reque writeError(w, err.Error(), http.StatusBadRequest) return } - if body.FontSize > 0 { + if body.FontSize > 0 && body.FontSize <= 72 { s.config.Terminal.FontSize = body.FontSize } if body.FontFamily != "" { diff --git a/internal/api/handlers_info.go b/internal/api/handlers_info.go index ae027b7..0fa1519 100644 --- a/internal/api/handlers_info.go +++ b/internal/api/handlers_info.go @@ -80,8 +80,23 @@ func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) { writeError(w, "no config", http.StatusNotFound) return } + masked := make([]map[string]interface{}, 0, len(s.config.AI.Providers)) + for _, p := range s.config.AI.Providers { + entry := map[string]interface{}{ + "name": p.Name, + "model": p.Model, + "base_url": p.BaseURL, + "active": p.Active, + } + if p.APIKey != "" { + entry["api_key"] = "***" + } else { + entry["api_key"] = "" + } + masked = append(masked, entry) + } writeJSON(w, map[string]interface{}{ - "providers": s.config.AI.Providers, + "providers": masked, }) } @@ -203,9 +218,20 @@ func (s *Server) handleLSPAutoInstall(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&body) } + home, _ := os.UserHomeDir() if body.ProjectDir == "" { - home, _ := os.UserHomeDir() body.ProjectDir = home + } else { + abs, err := filepath.Abs(body.ProjectDir) + if err != nil { + writeError(w, "invalid project_dir", http.StatusBadRequest) + return + } + body.ProjectDir = abs + if home != "" && !strings.HasPrefix(abs, home+string(filepath.Separator)) && abs != home { + writeError(w, "project_dir must be within user home", http.StatusBadRequest) + return + } } results, err := lsp.AutoInstallForProject(body.ProjectDir) diff --git a/internal/api/handlers_workflow.go b/internal/api/handlers_workflow.go index 9e268c4..e7958e0 100644 --- a/internal/api/handlers_workflow.go +++ b/internal/api/handlers_workflow.go @@ -144,7 +144,7 @@ func (s *Server) handleWorkflowPlan(w http.ResponseWriter, r *http.Request) { engine, _ = workflow.NewEngine(s.agentRegistry) } - wf := engine.Create("Plan: "+body.Goal[:min(len(body.Goal), 30)], body.Goal, "plan_execute", steps) + wf := engine.Create("Plan: "+truncateString(body.Goal, 30), body.Goal, "plan_execute", steps) writeJSON(w, wf) } @@ -188,7 +188,6 @@ func (s *Server) handleWorkflowExecuteStream(w http.ResponseWriter, engine *work w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) flusher, canFlush := w.(http.Flusher) @@ -250,9 +249,10 @@ func (s *Server) handleWorkflowApprove(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]string{"status": "approved"}) } -func min(a, b int) int { - if a < b { - return a +func truncateString(s string, max int) string { + runes := []rune(s) + if len(runes) <= max { + return s } - return b + return string(runes[:max]) } \ No newline at end of file diff --git a/internal/api/image_cache.go b/internal/api/image_cache.go index bcbfe86..b6dfd48 100644 --- a/internal/api/image_cache.go +++ b/internal/api/image_cache.go @@ -37,6 +37,9 @@ func saveImage(dataURI, filename, mimeType string) (string, error) { if err != nil { return "", fmt.Errorf("base64 decode: %w", err) } + if len(decoded) > 10*1024*1024 { + return "", fmt.Errorf("image too large (max 10MB)") + } id := fmt.Sprintf("%d-%d", time.Now().UnixMilli(), atomic.AddUint64(&imageCounter, 1)) ext := ".png" diff --git a/internal/api/server.go b/internal/api/server.go index 229b278..fe661c2 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -154,8 +154,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS") + if origin := r.Header.Get("Origin"); isAllowedOrigin(origin) { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Vary", "Origin") + } + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) @@ -164,6 +167,22 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } +func isAllowedOrigin(origin string) bool { + if origin == "" { + return false + } + switch { + case strings.HasPrefix(origin, "http://127.0.0.1"), + strings.HasPrefix(origin, "http://localhost"), + strings.HasPrefix(origin, "http://[::1]"), + strings.HasPrefix(origin, "https://127.0.0.1"), + strings.HasPrefix(origin, "https://localhost"), + strings.HasPrefix(origin, "https://[::1]"): + return true + } + return false +} + const maxCrushAgents = 2 const maxClaudeAgents = 2 diff --git a/internal/api/terminal.go b/internal/api/terminal.go index f2eed64..566eb64 100644 --- a/internal/api/terminal.go +++ b/internal/api/terminal.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "runtime" "strings" "sync" @@ -96,7 +97,10 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) { if sshConf.Password != "" { sshpassPath, err := exec.LookPath("sshpass") if err == nil { - cmd = exec.Command(sshpassPath, append([]string{"-p", sshConf.Password}, append([]string{"-e"}, sshArgs...)...)...) + args := append([]string{"-e"}, "ssh") + args = append(args, sshArgs...) + cmd = exec.Command(sshpassPath, args...) + cmd.Env = append(os.Environ(), "SSHPASS="+sshConf.Password) } else { cmd = exec.Command("ssh", sshArgs...) } @@ -113,29 +117,42 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) { shell = "/bin/sh" } - if path, err := exec.LookPath(shell); err == nil { - shell = path - } + // Support "wsl -d " shell strings sent from the UI quick-access. + if extra, ok := parseWSLShell(shell); ok { + wslPath, err := exec.LookPath("wsl") + if err != nil { + conn.WriteJSON(wsMessage{Type: "error", Data: "wsl not found on this host"}) + return + } + cmd = exec.Command(wslPath, extra...) + } else { + if path, err := exec.LookPath(shell); err == nil { + shell = path + } - if _, err := os.Stat(shell); err != nil { - conn.WriteJSON(wsMessage{Type: "error", Data: fmt.Sprintf("shell not found: %s (resolved from: %q)", shell, initMsg.Data)}) - return - } + if _, err := os.Stat(shell); err != nil { + conn.WriteJSON(wsMessage{Type: "error", Data: fmt.Sprintf("shell not found: %s (resolved from: %q)", shell, initMsg.Data)}) + return + } - shellName := filepath.Base(shell) - switch shellName { - case "wsl": - cmd = exec.Command(shell, "--shell-type", "login") - case "powershell", "pwsh": - cmd = exec.Command(shell, "-NoLogo", "-NoProfile") - case "fish": - cmd = exec.Command(shell, "--login") - default: - cmd = exec.Command(shell) + shellName := filepath.Base(shell) + switch shellName { + case "wsl": + cmd = exec.Command(shell, "--shell-type", "login") + case "powershell", "pwsh": + cmd = exec.Command(shell, "-NoLogo", "-NoProfile") + case "fish": + cmd = exec.Command(shell, "--login") + default: + cmd = exec.Command(shell) + } } } - cmd.Env = append(os.Environ(), "TERM=xterm-256color") + if cmd.Env == nil { + cmd.Env = os.Environ() + } + cmd.Env = append(cmd.Env, "TERM=xterm-256color") ptmx, err := pty.Start(cmd) if err != nil { @@ -207,8 +224,15 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) { func (s *Server) handleTerminalSessions(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { + masked := make([]config.SSHConnection, len(s.config.Terminal.SSH)) + for i, c := range s.config.Terminal.SSH { + masked[i] = c + if masked[i].Password != "" { + masked[i].Password = "***" + } + } writeJSON(w, map[string]interface{}{ - "ssh": s.config.Terminal.SSH, + "ssh": masked, "system": detectSystemTerminals(), }) return @@ -239,13 +263,17 @@ func (s *Server) handleTerminalSessions(w http.ResponseWriter, r *http.Request) for i, c := range s.config.Terminal.SSH { if c.Name == body.Name { + password := body.Password + if password == "***" { + password = c.Password + } s.config.Terminal.SSH[i] = config.SSHConnection{ Name: body.Name, Host: body.Host, Port: body.Port, User: body.User, KeyPath: body.KeyPath, - Password: body.Password, + Password: password, } if err := config.Save(s.config); err != nil { writeError(w, err.Error(), http.StatusInternalServerError) @@ -314,6 +342,87 @@ func detectShell() string { return "/bin/sh" } +// listWSLDistros returns the list of installed WSL distribution names. +// Windows hosts only — returns nil on other platforms or if WSL is unavailable. +func listWSLDistros() []string { + if runtime.GOOS != "windows" { + return nil + } + out, err := exec.Command("wsl", "--list", "--quiet").Output() + if err != nil { + return nil + } + // `wsl --list --quiet` outputs UTF-16LE on Windows. Strip BOM and decode best-effort. + raw := stripUTF16ToASCII(out) + var distros []string + seen := make(map[string]bool) + for _, line := range strings.Split(raw, "\n") { + name := strings.TrimSpace(line) + if name == "" || seen[name] { + continue + } + // Skip default-marker arrows or annotations. + name = strings.TrimSpace(strings.TrimPrefix(name, "*")) + if name == "" || !validWSLName.MatchString(name) { + continue + } + seen[name] = true + distros = append(distros, name) + } + return distros +} + +var validWSLName = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + +// parseWSLShell recognises strings of the form "wsl -d " (and optionally +// "-u ") emitted by the Shell tab quick-access menu, returning the args +// to pass to the wsl binary. Returns ok=false otherwise. +func parseWSLShell(shell string) ([]string, bool) { + parts := strings.Fields(shell) + if len(parts) < 3 || parts[0] != "wsl" { + return nil, false + } + args := []string{} + i := 1 + for i < len(parts) { + switch parts[i] { + case "-d", "--distribution": + if i+1 >= len(parts) || !validWSLName.MatchString(parts[i+1]) { + return nil, false + } + args = append(args, "-d", parts[i+1]) + i += 2 + case "-u", "--user": + if i+1 >= len(parts) || !validWSLName.MatchString(parts[i+1]) { + return nil, false + } + args = append(args, "-u", parts[i+1]) + i += 2 + default: + return nil, false + } + } + if len(args) == 0 { + return nil, false + } + return args, true +} + +func stripUTF16ToASCII(b []byte) string { + // Best-effort: keep only printable bytes (drop high bytes from UTF-16LE pairs). + var out []byte + for i := 0; i < len(b); i++ { + c := b[i] + if c == 0 { + continue + } + if c >= 32 && c < 127 || c == '\n' || c == '\r' || c == '\t' { + out = append(out, c) + } + } + return string(out) +} + func detectSystemTerminals() []map[string]string { var terminals []map[string]string @@ -326,10 +435,17 @@ func detectSystemTerminals() []map[string]string { if runtime.GOOS == "windows" { if _, err := exec.LookPath("wsl"); err == nil { terminals = append(terminals, map[string]string{ - "type": "local", - "name": "WSL", + "type": "local", + "name": "WSL (default)", "shell": "wsl", }) + for _, distro := range listWSLDistros() { + terminals = append(terminals, map[string]string{ + "type": "local", + "name": "WSL: " + distro, + "shell": "wsl -d " + distro, + }) + } } if _, err := exec.LookPath("powershell"); err == nil { terminals = append(terminals, map[string]string{ diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 4725011..c66160c 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -142,6 +142,37 @@ func New(cfg *config.MuyueConfig) (*Orchestrator, error) { }, nil } +// NewForProvider builds an orchestrator using a specific (non-active) provider, +// for the Advanced Reflection feature where the inactive provider produces a +// preliminary report before the active provider answers. Excludes the currently +// active provider from selection — picks the first other configured provider +// with a non-empty API key. +func NewForInactiveProvider(cfg *config.MuyueConfig) (*Orchestrator, error) { + var activeName string + for _, p := range cfg.AI.Providers { + if p.Active { + activeName = p.Name + break + } + } + for i := range cfg.AI.Providers { + p := &cfg.AI.Providers[i] + if p.Name == activeName { + continue + } + if p.APIKey == "" { + continue + } + return &Orchestrator{ + config: cfg, + provider: p, + client: sharedHTTPClient, + history: []Message{}, + }, nil + } + return nil, fmt.Errorf("no inactive provider with API key configured") +} + func (o *Orchestrator) SetSystemPrompt(prompt string) { o.systemPrompt = prompt } @@ -174,6 +205,33 @@ func (o *Orchestrator) GetHistory() []Message { return out } +// SendNoTools issues a one-shot, history-less request to this orchestrator's +// provider. Used by the Advanced Reflection feature so the inactive provider +// can produce a preliminary report without contaminating the active +// orchestrator's history or invoking tools. +func (o *Orchestrator) SendNoTools(userMessage string) (string, error) { + messages := make([]Message, 0, 2) + if o.systemPrompt != "" { + messages = append(messages, Message{Role: "system", Content: TextContent(o.systemPrompt)}) + } + messages = append(messages, Message{Role: "user", Content: TextContent(userMessage)}) + + reqBody := ChatRequest{ + Model: o.provider.Model, + Messages: messages, + Stream: false, + } + + chatResp, _, err := o.sendWithFallback(reqBody, "") + if err != nil { + return "", err + } + if len(chatResp.Choices) == 0 { + return "", fmt.Errorf("empty response from provider") + } + return CleanAIResponse(chatResp.Choices[0].Message.Content), nil +} + func (o *Orchestrator) Send(userMessage string) (string, error) { o.histMu.Lock() o.history = append(o.history, Message{ diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 5d59de3..aeeef60 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -65,11 +65,11 @@ func TestCleanAIResponse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := cleanAIResponse(tt.input) + result := CleanAIResponse(tt.input) result = strings.TrimSpace(result) expected := strings.TrimSpace(tt.expected) if result != expected { - t.Errorf("cleanAIResponse(%q) = %q, want %q", tt.input, result, expected) + t.Errorf("CleanAIResponse(%q) = %q, want %q", tt.input, result, expected) } }) } @@ -77,34 +77,34 @@ func TestCleanAIResponse(t *testing.T) { func TestCleanAIResponseThinkRegex(t *testing.T) { input2 := "some reasoningactual response" - result2 := cleanAIResponse(input2) + result2 := CleanAIResponse(input2) if result2 != "actual response" { t.Errorf("Valid Think tags should be removed: %q", result2) } input3 := " on opening tag, so won't match regex if result3 != ", so won't match if result4 != "reasoning after it, so won't match if result_real != "prefix request('/shell/chat/clear', { method: 'POST' }), analyzeSystem: () => request('/shell/analyze', { method: 'POST' }), getShellAnalysis: () => request('/shell/analysis'), - sendChat: (message, stream = true, onChunk, signal, images = []) => { + sendChat: (message, stream = true, onChunk, signal, images = [], advancedReflection = false) => { if (!stream) { - return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false, images }) }) + return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false, images, advanced_reflection: advancedReflection }) }) } return new Promise((resolve, reject) => { fetch(`${API_BASE}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message, stream: true, images }), + body: JSON.stringify({ message, stream: true, images, advanced_reflection: advancedReflection }), signal, }).then(async (res) => { if (!res.ok) { diff --git a/web/src/components/Shell.jsx b/web/src/components/Shell.jsx index 7c5778f..206dce6 100644 --- a/web/src/components/Shell.jsx +++ b/web/src/components/Shell.jsx @@ -785,7 +785,7 @@ export default function Shell({ api, isSudo }) { if (!container) return const rect = container.getBoundingClientRect() const dims = entry.fitAddon.proposeDimensions() - if (dims && entry.term.cols !== dims.cols || entry.term.rows !== dims.rows) { + if (dims && (entry.term.cols !== dims.cols || entry.term.rows !== dims.rows)) { entry.fitAddon.fit() } } diff --git a/web/src/components/Studio.jsx b/web/src/components/Studio.jsx index 45d5547..420cd41 100644 --- a/web/src/components/Studio.jsx +++ b/web/src/components/Studio.jsx @@ -270,11 +270,12 @@ function CodeBlockWithCopy({ part, index, copiedIdx, setCopiedIdx }) { ) } -function FeedItem({ msg, activeAgents, onModeChange }) { +function FeedItem({ msg, activeAgents, onModeChange, collapseHistory }) { const isUser = msg.role === 'user' const isSystem = msg.role === 'system' const rank = getRank(msg.role) const [copiedIdx, setCopiedIdx] = useState(null) + const [forceExpand, setForceExpand] = useState(false) const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '' @@ -330,43 +331,81 @@ function FeedItem({ msg, activeAgents, onModeChange }) { )} {parsedSegments && parsedSegments.some(s => s.type === 'tool') ? ( - parsedSegments.map((seg, i) => { - if (seg.type === 'text') { - if (!seg.content) return null - const c = seg.content.replace(/]*>[\s\S]*?<\/think>/gi, '') - if (!c) return null - return ( -
- {renderContent(c).map((part, j) => - part.type === 'code' ? ( - - ) : ( - + (() => { + const toolSegs = parsedSegments.filter(s => s.type === 'tool') + const compress = collapseHistory && !forceExpand && toolSegs.length > 1 + const lastTool = toolSegs.length > 0 ? toolSegs[toolSegs.length - 1] : null + return ( + <> + {compress && ( +
+ … {toolSegs.length - 1} action{toolSegs.length - 1 > 1 ? 's' : ''} précédente{toolSegs.length - 1 > 1 ? 's' : ''} masquée{toolSegs.length - 1 > 1 ? 's' : ''} + +
+ )} + {parsedSegments.map((seg, i) => { + if (seg.type === 'text') { + if (!seg.content) return null + const c = seg.content.replace(/]*>[\s\S]*?<\/think>/gi, '') + if (!c) return null + return ( +
+ {renderContent(c).map((part, j) => + part.type === 'code' ? ( + + ) : ( + + ) + )} +
) - )} -
- ) - } - if (seg.type === 'tool') { - const r = seg.result - const result = r && (r.content !== undefined || r.is_error !== undefined) - ? { content: r.content, is_error: r.is_error } - : null - return - } - return null - }) + } + if (seg.type === 'tool') { + if (compress && seg !== lastTool) return null + const r = seg.result + const result = r && (r.content !== undefined || r.is_error !== undefined) + ? { content: r.content, is_error: r.is_error } + : null + return + } + return null + })} + + ) + })() ) : ( <> - {parsedToolCalls && parsedToolCalls.map((tc, i) => { - const resultData = parsedToolResults - ? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id) - : null - const result = resultData - ? { content: resultData.result, is_error: resultData.is_error } - : null - return - })} + {parsedToolCalls && (() => { + const compress = collapseHistory && !forceExpand && parsedToolCalls.length > 1 + const items = compress ? parsedToolCalls.slice(-1) : parsedToolCalls + return ( + <> + {compress && ( +
+ … {parsedToolCalls.length - 1} action{parsedToolCalls.length - 1 > 1 ? 's' : ''} précédente{parsedToolCalls.length - 1 > 1 ? 's' : ''} masquée{parsedToolCalls.length - 1 > 1 ? 's' : ''} + +
+ )} + {items.map((tc, i) => { + const resultData = parsedToolResults + ? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id) + : null + const result = resultData + ? { content: resultData.result, is_error: resultData.is_error } + : null + return + })} + + ) + })()} {cleanContent && (
{renderContent(cleanContent).map((part, i) => @@ -385,11 +424,12 @@ function FeedItem({ msg, activeAgents, onModeChange }) { ) } -function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, onModeChange }) { +function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, onModeChange, collapseHistory }) { const rank = RANKS.general const cleanContent = content.replace(/]*>[\s\S]*?<\/think>/gi, '') const hasToolCalls = toolCalls && toolCalls.length > 0 const [copiedIdx, setCopiedIdx] = useState(null) + const [forceExpand, setForceExpand] = useState(false) const renderedContent = useMemo(() => { if (!cleanContent) return [] @@ -402,6 +442,8 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o }, [thinking]) const hasOrderedSegments = segments && segments.some(s => s.type === 'tool') + const toolSegments = (segments || []).filter(s => s.type === 'tool') + const compress = collapseHistory && !forceExpand && toolSegments.length > 1 return (
@@ -417,32 +459,51 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
{thinking && } {hasOrderedSegments ? ( - segments.map((seg, i) => { - if (seg.type === 'text') { - if (!seg.content) return null - const parts = renderContent(seg.content) - return ( -
- {parts.map((part, j) => - part.type === 'code' ? ( - - ) : ( - - ) - )} -
- ) - } - if (seg.type === 'tool') { - return - } - return null - }) + <> + {compress && ( +
+ … {toolSegments.length - 1} action{toolSegments.length - 1 > 1 ? 's' : ''} précédente{toolSegments.length - 1 > 1 ? 's' : ''} masquée{toolSegments.length - 1 > 1 ? 's' : ''} (mode compressé) + +
+ )} + {(() => { + const lastToolId = toolSegments.length > 0 ? toolSegments[toolSegments.length - 1] : null + return segments.map((seg, i) => { + if (seg.type === 'text') { + if (!seg.content) return null + const parts = renderContent(seg.content) + return ( +
+ {parts.map((part, j) => + part.type === 'code' ? ( + + ) : ( + + ) + )} +
+ ) + } + if (seg.type === 'tool') { + if (compress && seg !== lastToolId) return null + return + } + return null + }) + })()} + ) : ( <> - {hasToolCalls && toolCalls.map((tc, i) => ( - - ))} + {hasToolCalls && (compress + ? [] + : toolCalls.map((tc, i) => ( + + )) + )} {cleanContent && (
{renderedContent.map((part, i) => @@ -487,6 +548,12 @@ export default function Studio({ api }) { const [attachedImages, setAttachedImages] = useState([]) const [activeAgents, setActiveAgents] = useState({ crush: 0, claude: 0 }) const [toolModes, setToolModes] = useState({}) + const [advancedReflection, setAdvancedReflection] = useState(() => { + try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false } + }) + const [collapseHistory, setCollapseHistory] = useState(() => { + try { return localStorage.getItem('muyue.collapseHistory') !== 'false' } catch { return true } + }) const MAX_CRUSH_AGENTS = 2 const MAX_CLAUDE_AGENTS = 2 const messagesEnd = useRef(null) @@ -747,7 +814,7 @@ export default function Studio({ api }) { setStreaming(partial) const snap = segments.map(s => ({ ...s })) setStreamSegments(snap) - }, controller.signal, images) + }, controller.signal, images, advancedReflection) const allText = segments.filter(s => s.type === 'text').map(s => s.content).join('') const toolSegs = segments.filter(s => s.type === 'tool') @@ -882,7 +949,7 @@ export default function Studio({ api }) { {summarizedExpanded ? 'masquer' : 'voir'}
{summarizedExpanded && summarizedMsgs.map(msg => ( - + ))}
) @@ -894,7 +961,7 @@ export default function Studio({ api }) { <> {renderSummaryBlock()} {activeMsgs.slice(0, visibleCount).map(msg => ( - + ))}
@@ -911,7 +978,7 @@ export default function Studio({ api }) { <> {renderSummaryBlock()} {activeMsgs.map(msg => ( - + ))} ) @@ -935,7 +1002,7 @@ export default function Studio({ api }) {
{renderMessages()} {(streaming || streamThinking || loading || streamToolCalls.length > 0) && ( - + )}
@@ -997,6 +1064,36 @@ export default function Studio({ api }) { + +