Compare commits

...

16 Commits
v0.9.2 ... main

Author SHA1 Message Date
CI Bot
4e7c086cec chore: update CHANGELOG for v0.9.6 2026-04-28 17:11:04 +00:00
Augustin
b40bc291ba fix(ext): use <all_urls> host_permissions to allow injection on any page
All checks were successful
Beta Release / beta (push) Successful in 1m41s
Stable Release / stable (push) Successful in 1m39s
💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 19:08:02 +02:00
Augustin
1216c80118 chore: bump version to v0.9.6
Some checks failed
Beta Release / beta (push) Successful in 1m50s
Stable Release / stable (push) Has been cancelled
💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 19:04:58 +02:00
Augustin
eda493856a feat(ext): add session inject button, auto-inject toggle, and live Studio feed in sidepanel, bump v0.9.6
Some checks failed
Beta Release / beta (push) Successful in 1m40s
Stable Release / stable (push) Has been cancelled
- Configuration tab: inject session script button + auto-inject toggle
- Chat tab replaced by read-only live Studio feed mirror (polls /chat/history)
- content.js now checks auto-inject flag before initializing

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 19:02:04 +02:00
CI Bot
633df84168 chore: update CHANGELOG for v0.9.5
All checks were successful
Beta Release / beta (push) Successful in 1m37s
2026-04-28 14:53:17 +00:00
Augustin
8e4fdfecf3 fix(ui): remove orphaned unclosed CSS brace breaking all post-line-1316 styles including xterm, bump v0.9.5
All checks were successful
Stable Release / stable (push) Successful in 1m54s
The root cause of the tiny terminal was an unclosed .config-ai-tools-grid {
selector at line 1316 that swallowed ALL subsequent CSS rules (xterm, responsive,
KaTeX, highlight.js, split panes) into an invalid selector block.

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 16:50:50 +02:00
CI Bot
e1fe0881cf chore: update CHANGELOG for v0.9.4 2026-04-28 14:22:45 +00:00
Augustin
053cbca779 fix(ui): remove aggressive shell responsive rules breaking terminal sizing, bump v0.9.4
All checks were successful
Stable Release / stable (push) Successful in 1m53s
Terminal tabs, search bar, xterm wrapper were being resized by media
queries causing the xterm to render as a tiny box. Now only the AI
panel column adapts on narrow screens — the terminal stays full size.

🤗 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 16:20:37 +02:00
CI Bot
fd03423062 chore: update CHANGELOG for v0.9.3 2026-04-28 14:04:41 +00:00
Augustin
db6e7a1bf8 feat(ui): comprehensive responsive CSS for all breakpoints
All checks were successful
Stable Release / stable (push) Successful in 1m42s
Add 4 responsive breakpoints (1200px, 1024px, 768px, 480px) covering
every component: header nav scrollable on mobile, dashboard grid
adapts from 3→2→1 columns, studio feed/input/tool blocks scale down,
shell AI panel stacks vertically, config cards stack labels, and all
typography/buttons/padding scale progressively for small screens.

🤗 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 16:02:36 +02:00
CI Bot
face2637da chore: update CHANGELOG for v0.9.3 2026-04-28 13:56:14 +00:00
Augustin
5a39a3a804 fix(ui): restore Shell.jsx to v0.9.0-beta.1 state
All checks were successful
Stable Release / stable (push) Successful in 1m38s
Revert terminal tab to its exact state at v0.9.0-beta.1, removing
split panes, file editor integration, and agent sessions added in
the v0.9.0 UI overhaul.

🤗 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 15:54:38 +02:00
CI Bot
b7b66634ea chore: update CHANGELOG for v0.9.3 2026-04-28 13:43:57 +00:00
Augustin
591dc5adcd fix(ui): restore useI18n import accidentally removed in cleanup
All checks were successful
Stable Release / stable (push) Successful in 1m40s
🤗 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 15:41:58 +02:00
CI Bot
be40fa278f chore: update CHANGELOG for v0.9.3 2026-04-28 12:52:47 +00:00
Augustin
3f4432d88a fix(ui): add missing copiedMsg state, remove dead code, bump v0.9.3
All checks were successful
Stable Release / stable (push) Successful in 1m58s
The copiedMsg state was referenced in the Copy MD button but never declared,
causing a ReferenceError crash at runtime. Also removed unused compress
logic (collapseHistory/forceExpand), dead functions (renderContent, formatText,
CodeBlockWithCopy, MermaidBlock), and unused imports (useMemo, mermaid).

🤗 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-28 14:51:01 +02:00
13 changed files with 1009 additions and 772 deletions

View File

@@ -4,6 +4,376 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## v0.9.6
### Changes since v0.9.5
- fix(ext): use <all_urls> host_permissions to allow injection on any page (b40bc29)
- chore: bump version to v0.9.6 (1216c80)
- feat(ext): add session inject button, auto-inject toggle, and live Studio feed in sidepanel, bump v0.9.6 (eda4938)
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/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.9.6/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.9.6/muyue-darwin-arm64.tar.gz | tar xz
chmod +x muyue-darwin-arm64
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
```
**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
```powershell
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.6/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
& "$dest\muyue-windows-amd64.exe" install-shortcuts
$env:Path += ";$dest"
```
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
## v0.9.6
### Changes since v0.9.5
- feat(ext): add session inject button and auto-inject toggle in sidepanel Configuration tab
- feat(ext): replace interactive Chat tab with live read-only Studio feed mirror
- chore(ext): bump extension to v0.9.6
## v0.9.5
### Changes since v0.9.4
- fix(ui): remove orphaned unclosed CSS brace breaking all post-line-1316 styles including xterm, bump v0.9.5 (8e4fdfe)
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.5/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.5/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.5/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.5/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.5/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.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.9.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.9.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)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
```powershell
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.5/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
& "$dest\muyue-windows-amd64.exe" install-shortcuts
$env:Path += ";$dest"
```
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
## v0.9.5
### Changes since v0.9.4
- fix(ui): remove orphaned unclosed CSS brace that swallowed all xterm/responsive/highlight.js CSS rules
## v0.9.4
### Changes since v0.9.3
- fix(ui): remove aggressive shell responsive rules breaking terminal sizing, bump v0.9.4 (053cbca)
- chore: update CHANGELOG for v0.9.3 (fd03423)
- feat(ui): comprehensive responsive CSS for all breakpoints (db6e7a1)
- chore: update CHANGELOG for v0.9.3 (face263)
- fix(ui): restore Shell.jsx to v0.9.0-beta.1 state (5a39a3a)
- chore: update CHANGELOG for v0.9.3 (b7b6633)
- fix(ui): restore useI18n import accidentally removed in cleanup (591dc5a)
### Downloads
| Platform | File |
|----------|------|| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.4/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.4/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.4/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.4/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.4/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.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.9.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.9.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)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
```powershell
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.4/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
& "$dest\muyue-windows-amd64.exe" install-shortcuts
$env:Path += ";$dest"
```
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
## v0.9.3
### Changes since v0.9.3
- feat(ui): comprehensive responsive CSS for all breakpoints (db6e7a1)
- chore: update CHANGELOG for v0.9.3 (face263)
- fix(ui): restore Shell.jsx to v0.9.0-beta.1 state (5a39a3a)
- chore: update CHANGELOG for v0.9.3 (b7b6663)
- fix(ui): restore useI18n import accidentally removed in cleanup (591dc5a)
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/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.9.3/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.9.3/muyue-darwin-arm64.tar.gz | tar xz
chmod +x muyue-darwin-arm64
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
```
**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
```powershell
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
& "$dest\muyue-windows-amd64.exe" install-shortcuts
$env:Path += ";$dest"
```
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
## v0.9.3
### Changes since v0.9.3
- fix(ui): restore Shell.jsx to v0.9.0-beta.1 state (5a39a3a)
- chore: update CHANGELOG for v0.9.3 (b7b6663)
- fix(ui): restore useI18n import accidentally removed in cleanup (591dc5a)
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/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.9.3/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.9.3/muyue-darwin-arm64.tar.gz | tar xz
chmod +x muyue-darwin-arm64
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
```
**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
```powershell
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
& "$dest\muyue-windows-amd64.exe" install-shortcuts
$env:Path += ";$dest"
```
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
## v0.9.3
### Changes since v0.9.3
- fix(ui): restore useI18n import accidentally removed in cleanup (591dc5a)
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/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.9.3/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.9.3/muyue-darwin-arm64.tar.gz | tar xz
chmod +x muyue-darwin-arm64
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
```
**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
```powershell
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
& "$dest\muyue-windows-amd64.exe" install-shortcuts
$env:Path += ";$dest"
```
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
## v0.9.3
### Changes since v0.9.2
- fix(ui): add missing copiedMsg state, remove dead code, bump v0.9.3 (3f4432d)
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/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.9.3/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.9.3/muyue-darwin-arm64.tar.gz | tar xz
chmod +x muyue-darwin-arm64
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
```
**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer + commande `muyue` dans la session courante :
```powershell
Get-Process muyue, muyue-windows-amd64 -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Milliseconds 500
$dest = "$env:LOCALAPPDATA\Muyue"; New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.9.3/muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
& "$dest\muyue-windows-amd64.exe" install-shortcuts
$env:Path += ";$dest"
```
Le 1ʳᵉ ligne tue toute instance Muyue déjà lancée (sinon Windows refuse d'écraser le `.exe` verrouillé et l'install échoue silencieusement). Si vous mettez à jour depuis une version précédente, c'est obligatoire.
## v0.9.2 ## v0.9.2
### Changes since v0.9.1 ### Changes since v0.9.1

View File

@@ -1,12 +1,12 @@
{ {
"name": "muyue-extension", "name": "muyue-extension",
"version": "0.1.0", "version": "0.9.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "muyue-extension", "name": "muyue-extension",
"version": "0.1.0", "version": "0.9.0",
"dependencies": { "dependencies": {
"wxt": "^0.20" "wxt": "^0.20"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "muyue-extension", "name": "muyue-extension",
"version": "0.9.0", "version": "0.9.6",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -3,7 +3,9 @@ import { dispatch } from '../lib/page-rpc';
export default defineContentScript({ export default defineContentScript({
matches: ['http://*/*', 'https://*/*'], matches: ['http://*/*', 'https://*/*'],
runAt: 'document_idle', runAt: 'document_idle',
main() { async main() {
const autoInjectResult = await chrome.storage.local.get('muyue_auto_inject');
if (!autoInjectResult.muyue_auto_inject) return;
if (window.__muyueExtension) return; if (window.__muyueExtension) return;
window.__muyueExtension = true; window.__muyueExtension = true;

View File

@@ -46,7 +46,7 @@
</div> </div>
<div class="footer"> <div class="footer">
<span>Muyue</span> extension v0.9.0 <span>Muyue</span> extension v0.9.6
</div> </div>
</div> </div>

View File

@@ -40,6 +40,26 @@
<a id="btn-dashboard" href="#" class="btn btn-primary" target="_blank"> <a id="btn-dashboard" href="#" class="btn btn-primary" target="_blank">
Open Dashboard Open Dashboard
</a> </a>
<button id="btn-inject-session" class="btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
</svg>
Injecter le script de session
</button>
<div class="inject-status" id="inject-status" style="display:none"></div>
</div>
<div class="toggle-section">
<div class="toggle-row">
<span class="toggle-label">Auto-injection</span>
<label class="toggle-switch">
<input type="checkbox" id="toggle-auto-inject" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="toggle-desc">
Injecte automatiquement le script de session dans chaque page visitée
</div>
</div> </div>
<div class="settings-section"> <div class="settings-section">
@@ -59,28 +79,16 @@
<span>Server offline</span> <span>Server offline</span>
</div> </div>
<div id="chat-area" class="studio-feed-layout" style="display:none"> <div id="chat-area" class="studio-feed-layout" style="display:none">
<div class="chat-live-header">
<svg width="12" height="12" viewBox="0 0 24 24" fill="var(--accent)"><circle cx="12" cy="12" r="5"/></svg>
<span>Live — Studio</span>
</div>
<div id="chat-feed" class="studio-feed"></div> <div id="chat-feed" class="studio-feed"></div>
<div class="studio-input-area">
<div class="studio-input-row">
<textarea id="chat-input" placeholder="Envoyer un message…" rows="1"></textarea>
<button id="chat-send" class="studio-send-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
<button id="chat-stop" class="studio-stop-btn" style="display:none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<rect x="4" y="4" width="16" height="16" rx="2"/>
</svg>
</button>
</div>
<div class="studio-input-hint">/clear /help</div>
</div>
</div> </div>
</section> </section>
<div class="footer"> <div class="footer">
<span>Muyue</span> extension v0.9.0 <span>Muyue</span> extension v0.9.6
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import '../../styles/panel.css'; import '../../styles/panel.css';
import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config'; import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config';
import { getChatHistory, sendChat, clearChat } from '../../lib/api'; import { getChatHistory } from '../../lib/api';
const $ = (s) => document.querySelector(s); const $ = (s) => document.querySelector(s);
const $$ = (s) => document.querySelectorAll(s); const $$ = (s) => document.querySelectorAll(s);
@@ -15,16 +15,13 @@ const $btnSaveUrl = $('#btn-save-url');
const $chatOffline = $('#chat-offline'); const $chatOffline = $('#chat-offline');
const $chatArea = $('#chat-area'); const $chatArea = $('#chat-area');
const $chatFeed = $('#chat-feed'); const $chatFeed = $('#chat-feed');
const $chatStreaming = $('#chat-streaming'); const $btnInject = $('#btn-inject-session');
const $chatInput = $('#chat-input'); const $injectStatus = $('#inject-status');
const $chatSend = $('#chat-send'); const $toggleAutoInject = $('#toggle-auto-inject');
const $chatStop = $('#chat-stop');
let serverOnline = false; let serverOnline = false;
let messages = []; let lastMessageCount = -1;
let loading = false; let pollInterval = null;
let abortController = null;
let currentStreamingEl = null;
function dot(color) { function dot(color) {
return `<span class="dot dot-${color}"></span>`; return `<span class="dot dot-${color}"></span>`;
@@ -54,6 +51,10 @@ function renderSessions(sessions) {
`; `;
} }
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function formatText(text) { function formatText(text) {
let html = text let html = text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -95,10 +96,6 @@ function renderContent(text) {
return parts; return parts;
} }
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function createMessageEl(msg) { function createMessageEl(msg) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = `chat-msg ${msg.role}`; el.className = `chat-msg ${msg.role}`;
@@ -188,14 +185,6 @@ function createMessageEl(msg) {
return el; return el;
} }
function renderMessages() {
$chatFeed.innerHTML = '';
messages.forEach((msg) => {
$chatFeed.appendChild(createMessageEl(msg));
});
scrollToBottom();
}
function scrollToBottom() { function scrollToBottom() {
requestAnimationFrame(() => { requestAnimationFrame(() => {
$chatFeed.scrollTop = $chatFeed.scrollHeight; $chatFeed.scrollTop = $chatFeed.scrollHeight;
@@ -205,6 +194,9 @@ function scrollToBottom() {
function switchTab(tabName) { function switchTab(tabName) {
$$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabName)); $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabName));
$$('.tab-content').forEach((s) => s.classList.toggle('active', s.id === `tab-${tabName}`)); $$('.tab-content').forEach((s) => s.classList.toggle('active', s.id === `tab-${tabName}`));
if (tabName === 'chat' && serverOnline) {
pollStudio();
}
} }
function updateChatVisibility() { function updateChatVisibility() {
@@ -217,187 +209,82 @@ function updateChatVisibility() {
} }
} }
async function loadChatHistory() { async function pollStudio() {
try { try {
const data = await getChatHistory(); const data = await getChatHistory();
if (data.messages && data.messages.length > 0) { const msgs = data.messages || [];
messages = data.messages; if (msgs.length !== lastMessageCount) {
} else { lastMessageCount = msgs.length;
messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }]; $chatFeed.innerHTML = '';
} msgs.forEach((msg) => {
renderMessages(); $chatFeed.appendChild(createMessageEl(msg));
} catch { });
messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }]; scrollToBottom();
renderMessages();
} }
} catch {}
} }
async function handleSend() { function injectSessionScript() {
const text = $chatInput.value.trim(); $injectStatus.style.display = 'block';
if (!text || loading) return; $injectStatus.className = 'inject-status';
$injectStatus.textContent = 'Injection en cours…';
if (text === '/clear') { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
try { await clearChat(); } catch {} if (!tabs || !tabs[0]) {
messages = [{ id: 'clear-' + Date.now(), role: 'system', content: 'Conversation cleared.' }]; $injectStatus.textContent = 'Erreur: aucun onglet actif';
renderMessages(); $injectStatus.classList.add('inject-error');
$chatInput.value = ''; return;
}
const tabId = tabs[0].id;
chrome.scripting.executeScript({
target: { tabId },
func: () => {
if (window.__muyueExtension) {
return 'already_injected';
}
window.__muyueExtension = true;
return 'fresh_inject';
},
}, (results) => {
if (chrome.runtime.lastError) {
$injectStatus.textContent = 'Erreur: ' + chrome.runtime.lastError.message;
$injectStatus.classList.add('inject-error');
return; return;
} }
$chatInput.value = ''; const status = results?.[0]?.result;
$chatInput.style.height = 'auto';
const userMsg = { id: Date.now().toString(), role: 'user', content: text }; if (status === 'already_injected') {
messages.push(userMsg); $injectStatus.textContent = '✓ Script déjà injecté dans cette page';
$chatFeed.appendChild(createMessageEl(userMsg)); $injectStatus.classList.add('inject-success');
scrollToBottom();
loading = true;
$chatSend.style.display = 'none';
$chatStop.style.display = 'flex';
const controller = new AbortController();
abortController = controller;
let segments = [];
let thinking = '';
let textStartIdx = 0;
let streamText = '';
const updateLastText = (text) => {
if (!text) return;
const last = segments.length > 0 ? segments[segments.length - 1] : null;
if (last && last.type === 'text') {
last.content = text;
} else { } else {
segments.push({ type: 'text', content: text }); chrome.tabs.reload(tabId, {}, () => {
} $injectStatus.textContent = '✓ Page rechargée avec le script de session';
}; $injectStatus.classList.add('inject-success');
currentStreamingEl = document.createElement('div');
currentStreamingEl.className = 'chat-msg assistant streaming';
$chatFeed.appendChild(currentStreamingEl);
scrollToBottom();
try {
const finalContent = await sendChat(text, true, (partial, event) => {
if (event && (event.thinking !== undefined || event.thinking_start || event.thinking_end)) {
if (event.thinking !== undefined) thinking += event.thinking;
return;
}
if (event && event.tool_call) {
updateLastText(partial.slice(textStartIdx));
textStartIdx = partial.length;
segments.push({ type: 'tool', call: event.tool_call, result: null });
} else if (event && event.tool_result) {
const segIdx = segments.findIndex((s) => s.type === 'tool' && s.call && s.call.tool_call_id === event.tool_result.tool_call_id);
if (segIdx >= 0) segments[segIdx].result = event.tool_result;
} else {
updateLastText(partial.slice(textStartIdx));
}
streamText = partial;
const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join('');
const toolSegs = segments.filter((s) => s.type === 'tool');
let html = '';
if (thinking) {
html += `<div class="chat-thinking"><span class="chat-thinking-icon">⏱</span> Thinking…</div>`;
}
segments.forEach((seg) => {
if (seg.type === 'text' && seg.content) {
const c = seg.content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '');
if (c) html += `<div class="chat-content">${formatText(c)}</div>`;
}
if (seg.type === 'tool') {
const name = seg.call?.name || 'tool';
const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', web_fetch: '🌐' }[name] || '🔧';
const done = seg.result;
const isErr = done && done.is_error;
const preview = (() => {
try {
const args = typeof seg.call.args === 'string' ? JSON.parse(seg.call.args) : seg.call.args;
return args.command || args.task || args.path || JSON.stringify(args).slice(0, 60);
} catch { return ''; }
})();
html += `<div class="chat-tool ${done ? 'done' : 'running'} ${isErr ? 'error' : ''}"><div class="chat-tool-header"><span class="chat-tool-icon">${icon}</span><span>${name}</span>${done ? `<span class="chat-tool-status ${isErr ? 'err' : 'ok'}">${isErr ? '✗' : '✓'}</span>` : '<span class="chat-dots"><span></span><span></span><span></span></span>'}</div>${preview ? `<div class="chat-tool-args">${escapeHtml(preview)}</div>` : ''}</div>`;
}
}); });
if (!html) {
html = '<span class="chat-dots"><span></span><span></span><span></span></span>';
} }
currentStreamingEl.innerHTML = ` setTimeout(() => {
<div class="chat-avatar ai">◆</div> $injectStatus.style.display = 'none';
<div class="chat-body"> }, 3000);
<div class="chat-header"><span class="chat-badge" style="color:#FF9100;border-color:#FF9100">GEN</span></div> });
${html} });
<span class="chat-cursor"></span> }
</div>
`;
scrollToBottom();
}, controller.signal);
if (currentStreamingEl && currentStreamingEl.parentNode) { async function loadAutoInjectSetting() {
currentStreamingEl.remove(); const result = await chrome.storage.local.get('muyue_auto_inject');
} $toggleAutoInject.checked = !!result.muyue_auto_inject;
}
const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join(''); async function saveAutoInjectSetting(enabled) {
const toolSegs = segments.filter((s) => s.type === 'tool'); await chrome.storage.local.set({ muyue_auto_inject: enabled });
const aiMsg = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: toolSegs.length > 0 ? JSON.stringify({
segments: segments.map((s) => s.type === 'text'
? { type: 'text', content: s.content }
: { type: 'tool', call: s.call, result: s.result ? { content: s.result.content || '', is_error: s.result.is_error || false, tool_call_id: s.call?.tool_call_id } : null }),
content: allText,
}) : (allText || finalContent),
};
messages.push(aiMsg);
$chatFeed.appendChild(createMessageEl(aiMsg));
scrollToBottom();
} catch (err) {
if (currentStreamingEl && currentStreamingEl.parentNode) {
currentStreamingEl.remove();
}
if (err.name !== 'AbortError') {
const errMsg = { id: (Date.now() + 1).toString(), role: 'system', content: `Error: ${err.message}` };
messages.push(errMsg);
$chatFeed.appendChild(createMessageEl(errMsg));
scrollToBottom();
}
} finally {
loading = false;
abortController = null;
currentStreamingEl = null;
$chatSend.style.display = 'flex';
$chatStop.style.display = 'none';
}
} }
$$('.tab').forEach((tab) => { $$('.tab').forEach((tab) => {
tab.addEventListener('click', () => switchTab(tab.dataset.tab)); tab.addEventListener('click', () => switchTab(tab.dataset.tab));
}); });
$chatInput.addEventListener('input', () => {
$chatInput.style.height = 'auto';
$chatInput.style.height = Math.min($chatInput.scrollHeight, 100) + 'px';
});
$chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
$chatSend.addEventListener('click', handleSend);
$chatStop.addEventListener('click', () => {
if (abortController) abortController.abort();
});
$chatFeed.addEventListener('click', (e) => { $chatFeed.addEventListener('click', (e) => {
const btn = e.target.closest('.chat-copy-btn'); const btn = e.target.closest('.chat-copy-btn');
if (btn) { if (btn) {
@@ -408,6 +295,12 @@ $chatFeed.addEventListener('click', (e) => {
} }
}); });
$btnInject.addEventListener('click', injectSessionScript);
$toggleAutoInject.addEventListener('change', () => {
saveAutoInjectSetting($toggleAutoInject.checked);
});
$btnSaveUrl.addEventListener('click', async () => { $btnSaveUrl.addEventListener('click', async () => {
const url = $serverUrl.value.trim().replace(/\/$/, ''); const url = $serverUrl.value.trim().replace(/\/$/, '');
if (url) { if (url) {
@@ -442,6 +335,11 @@ async function refresh() {
}); });
} }
loadAutoInjectSetting();
refresh(); refresh();
loadChatHistory();
setInterval(refresh, 10000); if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(() => {
refresh();
if (serverOnline) pollStudio();
}, 3000);

View File

@@ -704,3 +704,138 @@ header h1 { font-size: 16px; font-weight: 600; letter-spacing: -0.3px; color: va
text-align: center; text-align: center;
margin-top: 6px; margin-top: 6px;
} }
/* ── Inject Session ── */
#btn-inject-session {
background: var(--bg-card);
border-color: var(--accent-dim);
color: var(--text-primary);
}
#btn-inject-session:hover {
background: var(--accent-bg);
border-color: var(--accent);
}
#btn-inject-session svg {
flex-shrink: 0;
}
.inject-status {
font-size: 11px;
text-align: center;
padding: 6px 8px;
border-radius: var(--radius-sm);
background: var(--bg-card);
color: var(--text-tertiary);
animation: fadeIn 0.2s ease-out;
}
.inject-status.inject-success {
color: var(--success);
background: rgba(0, 230, 118, 0.08);
}
.inject-status.inject-error {
color: var(--error);
background: rgba(255, 23, 68, 0.08);
}
/* ── Toggle Switch ── */
.toggle-section {
margin-top: 12px;
padding: 12px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.toggle-label {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
}
.toggle-desc {
font-size: 11px;
color: var(--text-tertiary);
margin-top: 6px;
line-height: 1.4;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 22px;
transition: all 0.2s;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background: var(--text-tertiary);
border-radius: 50%;
transition: all 0.2s;
}
.toggle-switch input:checked + .toggle-slider {
background: var(--accent-bg);
border-color: var(--accent);
}
.toggle-switch input:checked + .toggle-slider::before {
background: var(--accent);
transform: translateX(18px);
}
/* ── Live Chat Header ── */
.chat-live-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 11px;
font-weight: 700;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.chat-live-header svg {
animation: pulse-live 2s ease-in-out infinite;
}
@keyframes pulse-live {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}

View File

@@ -14,7 +14,7 @@ export default defineConfig({
'notifications', 'notifications',
'alarms', 'alarms',
], ],
host_permissions: ['http://127.0.0.1:*/*', 'http://localhost:*/*'], host_permissions: ['<all_urls>'],
action: { action: {
default_icon: { default_icon: {
16: 'icon/16.png', 16: 'icon/16.png',

View File

@@ -7,7 +7,7 @@ import (
const ( const (
Name = "muyue" Name = "muyue"
Version = "0.9.2" Version = "0.9.6"
Author = "La Légion de Muyue" Author = "La Légion de Muyue"
) )

View File

@@ -6,21 +6,18 @@ import { WebglAddon } from '@xterm/addon-webgl'
import { SearchAddon } from '@xterm/addon-search' import { SearchAddon } from '@xterm/addon-search'
import { Unicode11Addon } from '@xterm/addon-unicode11' import { Unicode11Addon } from '@xterm/addon-unicode11'
import { ImageAddon } from '@xterm/addon-image' import { ImageAddon } from '@xterm/addon-image'
import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot, Columns, Rows, Maximize2 } from 'lucide-react' import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot } from 'lucide-react'
import '@xterm/xterm/css/xterm.css' import '@xterm/xterm/css/xterm.css'
import { useI18n } from '../i18n' import { useI18n } from '../i18n'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import FileEditor from './FileEditor'
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' }) mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
const AI_TAB_ID = 0 const AI_TAB_ID = 0
const MAX_TABS = 7 const MAX_TABS = 7
const MAX_PANES = 4
const SHELL_MAX_TOKENS = 100000 const SHELL_MAX_TOKENS = 100000
const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change'] const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change']
const TABS_STORAGE_KEY = 'muyue_shell_tabs' const TABS_STORAGE_KEY = 'muyue_shell_tabs'
const LAYOUT_STORAGE_KEY = 'muyue_shell_layout'
const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers' const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers'
function renderContent(text) { function renderContent(text) {
@@ -480,107 +477,6 @@ export default function Shell({ api, isSudo }) {
const _streamRafRef = useRef(null) const _streamRafRef = useRef(null)
const _streamPendingRef = useRef(null) const _streamPendingRef = useRef(null)
const [splitLayout, setSplitLayout] = useState(() => {
try {
const raw = localStorage.getItem(LAYOUT_STORAGE_KEY)
if (raw) return JSON.parse(raw)
} catch {}
return null
})
const [editingFile, setEditingFile] = useState(null)
const [agentSessions, setAgentSessions] = useState([])
const paneCount = useMemo(() => {
const count = (node) => {
if (!node) return 1
if (node.type === 'leaf') return 1
return count(node.children?.[0]) + count(node.children?.[1])
}
return count(splitLayout)
}, [splitLayout])
const splitPane = useCallback((direction) => {
if (paneCount >= MAX_PANES) return
const activeId = activeTabRef.current
setSplitLayout(prev => {
if (!prev) {
return { type: 'split', direction, ratio: 0.5, activePane: activeId, children: [
{ type: 'leaf', tabId: activeId },
{ type: 'leaf', tabId: null },
]}
}
const clone = JSON.parse(JSON.stringify(prev))
const findAndSplit = (node) => {
if (node.type === 'leaf' && node.tabId === activeId) {
return { type: 'split', direction, ratio: 0.5, activePane: activeId, children: [
{ type: 'leaf', tabId: activeId },
{ type: 'leaf', tabId: null },
]}
}
if (node.children) {
return { ...node, children: node.children.map(findAndSplit) }
}
return node
}
return findAndSplit(clone)
})
}, [paneCount])
const removePane = useCallback((tabId) => {
setSplitLayout(prev => {
if (!prev) return null
if (prev.type === 'leaf') return null
const clone = JSON.parse(JSON.stringify(prev))
const removeFromTree = (node) => {
if (node.type !== 'split') return node
const left = node.children[0]
const right = node.children[1]
const leftIsTarget = left.type === 'leaf' && left.tabId === tabId
const rightIsTarget = right.type === 'leaf' && right.tabId === tabId
if (leftIsTarget) return removeFromTree(right)
if (rightIsTarget) return removeFromTree(left)
return { ...node, children: [removeFromTree(left), removeFromTree(right)] }
}
const result = removeFromTree(clone)
if (result.type === 'leaf' && result.tabId === null) return null
if (result.type === 'split' && (!result.children || result.children.length < 2)) return result.children?.[0] || null
return result
})
}, [])
const assignPaneTab = useCallback((paneLeaf, tabId) => {
setSplitLayout(prev => {
if (!prev) return prev
const clone = JSON.parse(JSON.stringify(prev))
const assign = (node) => {
if (node === paneLeaf) return { ...node, tabId }
if (node.children) return { ...node, children: node.children.map(assign) }
return node
}
return assign(clone)
})
}, [])
useEffect(() => {
if (splitLayout) {
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(splitLayout))
} else {
localStorage.removeItem(LAYOUT_STORAGE_KEY)
}
}, [splitLayout])
useEffect(() => {
api.getAgentSessions?.().then(d => {
setAgentSessions(d?.sessions || [])
}).catch(() => {})
const iv = setInterval(() => {
api.getAgentSessions?.().then(d => {
setAgentSessions(d?.sessions || [])
}).catch(() => {})
}, 5000)
return () => clearInterval(iv)
}, [])
const _flushStreamUpdate = useCallback(() => { const _flushStreamUpdate = useCallback(() => {
_streamRafRef.current = null _streamRafRef.current = null
const pending = _streamPendingRef.current const pending = _streamPendingRef.current
@@ -922,22 +818,6 @@ export default function Shell({ api, isSudo }) {
return return
} }
if (ctrl && e.shiftKey && e.key === 'D') {
const shellTab = document.querySelector('.shell-layout')
if (!shellTab || shellTab.closest('.tab-hidden')) return
e.preventDefault()
splitPane('vertical')
return
}
if (ctrl && e.shiftKey && e.key === 'H') {
const shellTab = document.querySelector('.shell-layout')
if (!shellTab || shellTab.closest('.tab-hidden')) return
e.preventDefault()
splitPane('horizontal')
return
}
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return
@@ -1438,27 +1318,6 @@ Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. L
{zoomLevel > 0 ? '+' : ''}{zoomLevel > 0 ? zoomLevel * 2 : zoomLevel * 2}px {zoomLevel > 0 ? '+' : ''}{zoomLevel > 0 ? zoomLevel * 2 : zoomLevel * 2}px
</span> </span>
)} )}
{paneCount < MAX_PANES && (
<>
<button className="shell-split-btn" onClick={() => splitPane('vertical')} title="Split Vertical (Ctrl+Shift+D)">
<Columns size={14} />
</button>
<button className="shell-split-btn" onClick={() => splitPane('horizontal')} title="Split Horizontal (Ctrl+Shift+H)">
<Rows size={14} />
</button>
</>
)}
{splitLayout && (
<button className="shell-split-btn" onClick={() => setSplitLayout(null)} title="Unsplit">
<Maximize2 size={14} />
</button>
)}
{agentSessions.length > 0 && (
<span className="shell-agent-indicator" title={`${agentSessions.length} agent(s) actif(s)`}>
<Bot size={12} />
<span className="shell-agent-count">{agentSessions.length}</span>
</span>
)}
{tabs.length < MAX_TABS && ( {tabs.length < MAX_TABS && (
<div className="shell-new-tab-wrapper"> <div className="shell-new-tab-wrapper">
<button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}> <button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}>
@@ -1525,29 +1384,7 @@ Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. L
</div> </div>
</div> </div>
<div className={`shell-xterm-wrapper ${splitLayout ? 'has-splits' : ''}`}> <div className="shell-xterm-wrapper">
{editingFile && (
<FileEditor api={api} filePath={editingFile} onClose={() => setEditingFile(null)} />
)}
{!editingFile && (
splitLayout ? (
<SplitPaneRenderer
node={splitLayout}
tabs={tabs}
activeTab={activeTab}
setActiveTab={setActiveTab}
showSearch={showSearch}
searchText={searchText}
searchInputRef={searchInputRef}
handleSearchChange={handleSearchChange}
handleSearchNext={handleSearchNext}
handleSearchPrev={handleSearchPrev}
handleCloseSearch={handleCloseSearch}
removePane={removePane}
onLayoutChange={setSplitLayout}
/>
) : (
<>
{showSearch && ( {showSearch && (
<div className="shell-search-bar"> <div className="shell-search-bar">
<Search size={14} className="shell-search-icon" /> <Search size={14} className="shell-search-icon" />
@@ -1575,9 +1412,6 @@ Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. L
className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`} className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`}
/> />
))} ))}
</>
)
)}
</div> </div>
</div> </div>
@@ -1941,158 +1775,3 @@ const ShellAIMessage = memo(function ShellAIMessage({ msg, sendToTerminal, termi
</div> </div>
) )
}) })
function SplitPaneRenderer({ node, tabs, activeTab, setActiveTab, showSearch, searchText, searchInputRef, handleSearchChange, handleSearchNext, handleSearchPrev, handleCloseSearch, removePane, onLayoutChange }) {
if (!node) return null
if (node.type === 'leaf') {
const tabId = node.tabId
const tab = tabId ? tabs.find(t => t.id === tabId) : null
const isActive = activeTab === tabId
if (!tab && tabId !== null) {
const fallbackTab = tabs[0]
if (fallbackTab) {
return (
<div className="split-pane-leaf" onClick={() => setActiveTab(fallbackTab.id)}>
<div id={`terminal-${fallbackTab.id}`} className={`shell-xterm-instance${activeTab === fallbackTab.id ? ' active' : ''}`} />
</div>
)
}
}
if (!tab) {
return (
<div className="split-pane-leaf empty">
<div className="split-pane-empty">
<Monitor size={24} style={{ opacity: 0.3 }} />
<span style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 8 }}>
Select a tab for this pane
</span>
<div style={{ display: 'flex', gap: 4, marginTop: 8 }}>
{tabs.slice(0, 4).map(t => (
<button key={t.id} className="sm ghost" onClick={() => { onLayoutChange(prev => assignLeafTab(prev, node, t.id)) }}>
{t.name}
</button>
))}
</div>
</div>
</div>
)
}
return (
<div className={`split-pane-leaf ${isActive ? 'active' : ''}`} onClick={() => setActiveTab(tabId)}>
<div className="split-pane-header">
<span className="split-pane-title">{tab.name}</span>
<button className="split-pane-close" onClick={(e) => { e.stopPropagation(); removePane(tabId) }}>
<X size={10} />
</button>
</div>
<div className="split-pane-content">
<div id={`terminal-${tabId}`} className="shell-xterm-instance active" />
</div>
</div>
)
}
if (node.type === 'split') {
const dir = node.direction === 'horizontal' ? 'row' : 'column'
return (
<div className={`split-pane-split ${dir}`} style={{ flex: 1 }}>
<div className="split-pane-child" style={{ flex: node.ratio || 0.5, overflow: 'hidden' }}>
<SplitPaneRenderer
node={node.children?.[0]}
tabs={tabs}
activeTab={activeTab}
setActiveTab={setActiveTab}
showSearch={showSearch}
searchText={searchText}
searchInputRef={searchInputRef}
handleSearchChange={handleSearchChange}
handleSearchNext={handleSearchNext}
handleSearchPrev={handleSearchPrev}
handleCloseSearch={handleCloseSearch}
removePane={removePane}
onLayoutChange={onLayoutChange}
/>
</div>
<div
className="split-pane-resizer"
onMouseDown={(e) => {
e.preventDefault()
const parent = e.target.parentElement
const startX = e.clientX
const startY = e.clientY
const startRatio = node.ratio || 0.5
const isVertical = node.direction === 'vertical'
const parentSize = isVertical ? parent.offsetWidth : parent.offsetHeight
const onMouseMove = (me) => {
const delta = isVertical ? (me.clientX - startX) : (me.clientY - startY)
const newRatio = Math.max(0.15, Math.min(0.85, startRatio + delta / parentSize))
onLayoutChange(prev => updateSplitRatio(prev, node, newRatio))
}
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.body.style.cursor = isVertical ? 'col-resize' : 'row-resize'
document.body.style.userSelect = 'none'
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}}
/>
<div className="split-pane-child" style={{ flex: 1 - (node.ratio || 0.5), overflow: 'hidden' }}>
<SplitPaneRenderer
node={node.children?.[1]}
tabs={tabs}
activeTab={activeTab}
setActiveTab={setActiveTab}
showSearch={showSearch}
searchText={searchText}
searchInputRef={searchInputRef}
handleSearchChange={handleSearchChange}
handleSearchNext={handleSearchNext}
handleSearchPrev={handleSearchPrev}
handleCloseSearch={handleCloseSearch}
removePane={removePane}
onLayoutChange={onLayoutChange}
/>
</div>
</div>
)
}
return null
}
function assignLeafTab(layout, leaf, tabId) {
if (!layout) return layout
if (layout === leaf) return { ...layout, tabId }
if (layout.children) {
return {
...layout,
children: layout.children.map(c => assignLeafTab(c, leaf, tabId)),
}
}
return layout
}
function updateSplitRatio(layout, targetNode, ratio) {
if (layout === targetNode) {
return { ...layout, ratio }
}
if (layout.children) {
return {
...layout,
children: layout.children.map(c => updateSplitRatio(c, targetNode, ratio)),
}
}
return layout
}

View File

@@ -1,6 +1,5 @@
import { useState, useRef, useEffect, useCallback, useMemo } from 'react' import { useState, useRef, useEffect, useCallback } from 'react'
import { useI18n } from '../i18n' import { useI18n } from '../i18n'
import mermaid from 'mermaid'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math' import remarkMath from 'remark-math'
@@ -9,8 +8,6 @@ import rehypeHighlight from 'rehype-highlight'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import 'highlight.js/styles/github-dark.css' import 'highlight.js/styles/github-dark.css'
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
const RANKS = { const RANKS = {
commandant: { label: 'Commandant', short: 'CDT', color: '#FFD740' }, commandant: { label: 'Commandant', short: 'CDT', color: '#FFD740' },
general: { label: 'General', short: 'GEN', color: '#FF9100' }, general: { label: 'General', short: 'GEN', color: '#FF9100' },
@@ -40,72 +37,6 @@ function RankIcon({ rank }) {
) )
} }
function renderContent(text) {
const parts = []
const codeBlockRegex = /(```[\s\S]*?```)/g
let match
let lastIndex = 0
while ((match = codeBlockRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push({ type: 'text', content: text.slice(lastIndex, match.index) })
}
const full = match[1]
const firstNewline = full.indexOf('\n')
const lang = firstNewline > -1 ? full.slice(3, firstNewline).trim() : ''
const code = firstNewline > -1 ? full.slice(firstNewline + 1, -3) : full.slice(3, -3)
parts.push({ type: 'code', lang, content: code })
lastIndex = match.index + full.length
}
if (lastIndex < text.length) {
const remaining = text.slice(lastIndex)
const openBlock = remaining.match(/```(\w*)\n?([\s\S]*)$/)
if (openBlock) {
if (openBlock.index > 0) {
parts.push({ type: 'text', content: remaining.slice(0, openBlock.index) })
}
parts.push({ type: 'code', lang: openBlock[1] || '', content: openBlock[2] || '' })
} else {
parts.push({ type: 'text', content: remaining })
}
}
return parts
}
function formatText(text) {
let html = text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
html = html.replace(/^(\|.+\|)\n(\|[\s\-:|]+\|)\n((?:\|.+\|\n?)+)/gm, (match, headerRow, sepRow, bodyRows) => {
const headers = headerRow.split('|').filter(c => c.trim() !== '').map(c => `<th>${c.trim()}</th>`).join('')
const rows = bodyRows.trim().split('\n').map(row => {
const cells = row.split('|').filter(c => c.trim() !== '').map(c => `<td>${c.trim()}</td>`).join('')
return `<tr>${cells}</tr>`
}).join('')
return `<table><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`
})
html = html
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
.replace(/^### (.+)$/gm, '<h4 class="msg-h4">$1</h4>')
.replace(/^## (.+)$/gm, '<h3 class="msg-h3">$1</h3>')
.replace(/^# (.+)$/gm, '<h2 class="msg-h2">$1</h2>')
.replace(/^---+$/gm, '<hr>')
.replace(/^\s*[-*] (.+)$/gm, '<div class="msg-bullet">\u2022 $1</div>')
.replace(/^\s*(\d+)[.)] (.+)$/gm, '<div class="msg-step"><span class="msg-step-num">$1</span> $2</div>')
.replace(/\n/g, '<br/>')
html = html
.replace(/<br\/>\s*<br\/>/g, '<br/>')
.replace(/<br\/>\s*(<h[234]|<div class="msg-|<table|<hr)/g, '$1')
.replace(/(<\/h[234]|<\/div>|<\/table>|<hr>)\s*<br\/>/g, '$1')
.replace(/\s+on\w+=["'][^"']*["']/gi, '')
.replace(/javascript:/gi, '')
.replace(/data:/gi, '')
return html
}
function ThinkingBlock({ content, done, raw }) { function ThinkingBlock({ content, done, raw }) {
return ( return (
<div className={`feed-thinking-block ${done ? 'done' : 'active'}`}> <div className={`feed-thinking-block ${done ? 'done' : 'active'}`}>
@@ -219,64 +150,6 @@ function ToolCallBlock({ call, result, activeAgents, onModeChange }) {
) )
} }
let mermaidIdCounter = 0
function MermaidBlock({ code }) {
const ref = useRef(null)
const [svg, setSvg] = useState('')
const [error, setError] = useState(false)
useEffect(() => {
let cancelled = false
const id = `studio-mermaid-${++mermaidIdCounter}`
mermaid.render(id, code).then(({ svg }) => {
if (!cancelled) setSvg(svg)
}).catch(() => {
if (!cancelled) setError(true)
})
return () => { cancelled = true }
}, [code])
if (error) return <pre className="studio-mermaid-error">{code}</pre>
if (!svg) return <div className="studio-mermaid-loading">Chargement...</div>
return <div className="studio-mermaid-container" ref={ref} dangerouslySetInnerHTML={{ __html: svg }} />
}
function CodeBlockWithCopy({ part, index, copiedIdx, setCopiedIdx }) {
if (part.lang === 'mermaid') {
return (
<div className="studio-code-block">
<div className="studio-code-header">
<span className="studio-code-lang">mermaid</span>
<button className={`studio-copy-btn ${copiedIdx === index ? 'copied' : ''}`} onClick={() => {
navigator.clipboard.writeText(part.content)
setCopiedIdx(index)
setTimeout(() => setCopiedIdx(null), 1500)
}}>
{copiedIdx === index ? 'Copie!' : 'Copier'}
</button>
</div>
<MermaidBlock code={part.content} />
</div>
)
}
return (
<div className="studio-code-block">
<div className="studio-code-header">
{part.lang && <span className="studio-code-lang">{part.lang}</span>}
<button className={`studio-copy-btn ${copiedIdx === index ? 'copied' : ''}`} onClick={() => {
navigator.clipboard.writeText(part.content)
setCopiedIdx(index)
setTimeout(() => setCopiedIdx(null), 1500)
}}>
{copiedIdx === index ? 'Copie!' : 'Copier'}
</button>
</div>
<pre><code>{part.content}</code></pre>
</div>
)
}
function MarkdownContent({ content, raw }) { function MarkdownContent({ content, raw }) {
if (raw) { if (raw) {
return <pre className="feed-content" style={{ whiteSpace: 'pre-wrap', fontFamily: 'var(--font-mono)', fontSize: '0.9em' }}>{content}</pre> return <pre className="feed-content" style={{ whiteSpace: 'pre-wrap', fontFamily: 'var(--font-mono)', fontSize: '0.9em' }}>{content}</pre>
@@ -292,7 +165,7 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
const isUser = msg.role === 'user' const isUser = msg.role === 'user'
const isSystem = msg.role === 'system' const isSystem = msg.role === 'system'
const rank = getRank(msg.role) const rank = getRank(msg.role)
const [copiedIdx, setCopiedIdx] = useState(null) const [copiedMsg, setCopiedMsg] = useState(false)
const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '' const timeStr = msg.time ? new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''
@@ -365,21 +238,8 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
)} )}
{parsedSegments && parsedSegments.some(s => s.type === 'tool') ? ( {parsedSegments && parsedSegments.some(s => s.type === 'tool') ? (
(() => { (() => {
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 ( return (
<> <>
{compress && (
<div className="feed-content" style={{ opacity: 0.7, fontSize: '0.85em', display: 'flex', justifyContent: 'space-between' }}>
<span> {toolSegs.length - 1} action{toolSegs.length - 1 > 1 ? 's' : ''} précédente{toolSegs.length - 1 > 1 ? 's' : ''} masquée{toolSegs.length - 1 > 1 ? 's' : ''}</span>
<button
type="button"
onClick={() => setForceExpand(true)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent, #6c5ce7)', cursor: 'pointer', fontSize: 'inherit' }}
>Tout afficher</button>
</div>
)}
{parsedSegments.map((seg, i) => { {parsedSegments.map((seg, i) => {
if (seg.type === 'text') { if (seg.type === 'text') {
if (!seg.content) return null if (!seg.content) return null
@@ -406,21 +266,9 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
) : ( ) : (
<> <>
{parsedToolCalls && (() => { {parsedToolCalls && (() => {
const compress = collapseHistory && !forceExpand && parsedToolCalls.length > 1
const items = compress ? parsedToolCalls.slice(-1) : parsedToolCalls
return ( return (
<> <>
{compress && ( {parsedToolCalls.map((tc, i) => {
<div className="feed-content" style={{ opacity: 0.7, fontSize: '0.85em', display: 'flex', justifyContent: 'space-between' }}>
<span> {parsedToolCalls.length - 1} action{parsedToolCalls.length - 1 > 1 ? 's' : ''} précédente{parsedToolCalls.length - 1 > 1 ? 's' : ''} masquée{parsedToolCalls.length - 1 > 1 ? 's' : ''}</span>
<button
type="button"
onClick={() => setForceExpand(true)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent, #6c5ce7)', cursor: 'pointer', fontSize: 'inherit' }}
>Tout afficher</button>
</div>
)}
{items.map((tc, i) => {
const resultData = parsedToolResults const resultData = parsedToolResults
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id) ? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
: null : null
@@ -448,17 +296,6 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
const rank = RANKS.general const rank = RANKS.general
const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '') const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
const hasToolCalls = toolCalls && toolCalls.length > 0 const hasToolCalls = toolCalls && toolCalls.length > 0
const [copiedIdx, setCopiedIdx] = useState(null)
const renderedContent = useMemo(() => {
if (!cleanContent) return null
return null
}, [cleanContent])
const formattedThinking = useMemo(() => {
if (!thinking) return ''
return thinking
}, [thinking])
const hasOrderedSegments = segments && segments.some(s => s.type === 'tool') const hasOrderedSegments = segments && segments.some(s => s.type === 'tool')
@@ -477,19 +314,7 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
{thinking && <ThinkingBlock content={thinking} raw done={false} />} {thinking && <ThinkingBlock content={thinking} raw done={false} />}
{hasOrderedSegments ? ( {hasOrderedSegments ? (
<> <>
{compress && ( {segments.map((seg, i) => {
<div className="feed-content" style={{ opacity: 0.7, fontSize: '0.85em', display: 'flex', justifyContent: 'space-between' }}>
<span> {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é)</span>
<button
type="button"
onClick={() => setForceExpand(true)}
style={{ background: 'transparent', border: 'none', color: 'var(--accent, #6c5ce7)', cursor: 'pointer', fontSize: 'inherit' }}
>Tout afficher</button>
</div>
)}
{(() => {
const lastToolId = toolSegments.length > 0 ? toolSegments[toolSegments.length - 1] : null
return segments.map((seg, i) => {
if (seg.type === 'text') { if (seg.type === 'text') {
if (!seg.content) return null if (!seg.content) return null
return ( return (
@@ -499,21 +324,17 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
) )
} }
if (seg.type === 'tool') { if (seg.type === 'tool') {
if (compress && seg !== lastToolId) return null
return <ToolCallBlock key={`tc${i}`} call={seg.call} result={seg.result} activeAgents={activeAgents} onModeChange={onModeChange} /> return <ToolCallBlock key={`tc${i}`} call={seg.call} result={seg.result} activeAgents={activeAgents} onModeChange={onModeChange} />
} }
return null return null
}) })}
})()}
</> </>
) : ( ) : (
<> <>
{hasToolCalls && (compress {hasToolCalls && toolCalls.map((tc, i) => (
? [<ToolCallBlock key={toolCalls[toolCalls.length - 1].call?.tool_call_id || 'last'} call={toolCalls[toolCalls.length - 1].call} result={toolCalls[toolCalls.length - 1].result} activeAgents={activeAgents} onModeChange={onModeChange} />]
: toolCalls.map((tc, i) => (
<ToolCallBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} activeAgents={activeAgents} onModeChange={onModeChange} /> <ToolCallBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} activeAgents={activeAgents} onModeChange={onModeChange} />
)) ))
)} }
{cleanContent && ( {cleanContent && (
<div className="feed-content"> <div className="feed-content">
<MarkdownContent content={cleanContent} raw={false} /> <MarkdownContent content={cleanContent} raw={false} />
@@ -551,7 +372,6 @@ export default function Studio({ api }) {
const [sudoModal, setSudoModal] = useState(null) const [sudoModal, setSudoModal] = useState(null)
const [attachedImages, setAttachedImages] = useState([]) const [attachedImages, setAttachedImages] = useState([])
const [activeAgents, setActiveAgents] = useState({ crush: 0, claude: 0 }) const [activeAgents, setActiveAgents] = useState({ crush: 0, claude: 0 })
const [toolModes, setToolModes] = useState({})
const [advancedReflection, setAdvancedReflection] = useState(() => { const [advancedReflection, setAdvancedReflection] = useState(() => {
try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false } try { return localStorage.getItem('muyue.advancedReflection') === 'true' } catch { return false }
}) })

View File

@@ -1313,8 +1313,6 @@ input::placeholder { color: var(--text-disabled); }
color: var(--accent-muted) !important; color: var(--accent-muted) !important;
} }
.config-ai-tools-grid {
/* ── KaTeX overrides ── */ /* ── KaTeX overrides ── */
.katex { font-size: 1em; color: var(--text-primary); } .katex { font-size: 1em; color: var(--text-primary); }
.katex-display { margin: 12px 0; overflow-x: auto; } .katex-display { margin: 12px 0; overflow-x: auto; }
@@ -1376,25 +1374,352 @@ input::placeholder { color: var(--text-disabled); }
.hljs-number { color: var(--warning) !important; } .hljs-number { color: var(--warning) !important; }
/* ── Responsive / Mobile ── */ /* ── Responsive / Mobile ── */
@media (max-width: 768px) {
:root { --sidebar-w: 100%; --header-h: 46px; } /* ── Large tablets / small laptops ── */
.header { padding: 0 12px; gap: 8px; } @media (max-width: 1200px) {
.header-nav { margin-left: 12px; gap: 2px; } .dash-grid {
.nav-tab { padding: 6px 10px; font-size: 12px; } grid-template-columns: repeat(2, 1fr);
.header-brand { gap: 6px; } grid-template-rows: auto;
.header-logo { font-size: 15px; letter-spacing: 2px; } height: auto;
.studio-feed { padding: 12px 8px; } overflow: auto;
.studio-input-area { padding: 8px 8px 4px; } }
.feed-item { padding: 6px 8px; } .dash-span-2 { grid-column: span 2; }
.feed-avatar { width: 24px; height: 24px; } .shell-ai-col { width: 280px; max-width: 280px; }
.dash-grid { grid-template-columns: 1fr; grid-template-rows: auto; height: auto; overflow: auto; } .config-profile-center { max-width: 100%; }
.dash-span-2 { grid-column: span 1; } .config-ai-tools-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
.skill-tiles { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
}
/* ── Tablets portrait ── */
@media (max-width: 1024px) {
:root { --sidebar-w: 240px; }
.header { padding: 0 16px; gap: 8px; }
.header-nav { margin-left: 16px; }
.nav-tab { padding: 6px 12px; font-size: 12px; }
.nav-tab .tab-icon { display: none; }
.dash-grid {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
height: auto;
overflow: auto;
gap: 10px;
padding: 12px;
}
.dash-span-2 { grid-column: span 2; }
.studio-feed { padding: 16px 12px; }
.studio-input-area { padding: 10px 12px 6px; }
.config-panel-body { padding: 12px 16px 16px; }
.config-card-row { gap: 10px; }
.config-card-label { width: 110px; }
.shell-ai-col { width: 240px; max-width: 240px; }
.shell-analysis-modal { width: 90vw; }
.shell-modal { min-width: 320px; }
.grid-2 { grid-template-columns: 1fr; } .grid-2 { grid-template-columns: 1fr; }
.split-horizontal { flex-direction: column; } .split-horizontal { flex-direction: column; }
.split-right { width: 100%; border-left: none; border-top: 1px solid var(--border); max-height: 300px; } .split-right { width: 100%; border-left: none; border-top: 1px solid var(--border); max-height: 300px; }
.shell-ai-col { width: 100%; max-width: 100%; border-left: none; border-top: 1px solid var(--border); max-height: 50vh; } }
.config-card-row { flex-wrap: wrap; gap: 8px; }
.config-card-label { width: 100%; } /* ── Mobile landscape / small tablets ── */
@media (max-width: 768px) {
:root { --sidebar-w: 100%; --header-h: 46px; }
/* Header */
.header {
padding: 0 10px;
gap: 6px;
flex-wrap: nowrap;
overflow: hidden;
}
.header-brand { gap: 4px; flex-shrink: 0; }
.header-logo { font-size: 14px; letter-spacing: 2px; }
.header-logo-img { width: 18px !important; height: 18px !important; }
.header-version { display: none; }
.header-nav {
margin-left: 8px;
gap: 2px;
overflow-x: auto;
scrollbar-width: none;
flex: 1;
min-width: 0;
}
.header-nav::-webkit-scrollbar { display: none; }
.nav-tab {
padding: 5px 8px;
font-size: 11px;
white-space: nowrap;
flex-shrink: 0;
}
.header-spacer { display: none; }
.header-clock { font-size: 10px; flex-shrink: 0; }
.header-indicators { gap: 6px; flex-shrink: 0; }
/* Statusbar */
.statusbar { padding: 0 8px; font-size: 10px; }
.statusbar-sudo { font-size: 9px; padding: 1px 4px; }
.statusbar-shortcut { display: none; }
.statusbar-right { display: none; }
/* Dashboard */
.dash-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
height: auto;
overflow: auto;
gap: 8px;
padding: 10px;
}
.dash-span-2 { grid-column: span 1; }
.dash-card { padding: 10px 12px; }
.dash-quota-list,
.dash-consumption-list,
.dash-proc-list { max-height: 200px; }
.dash-cmd-card .dash-cmd-list { max-height: 160px; }
/* Studio */
.studio-feed { padding: 10px 6px; gap: 2px; }
.studio-feed-scroll-wrap { }
.studio-input-area { padding: 8px 6px 4px; }
.studio-input-row textarea { min-height: 36px; padding: 8px 10px; font-size: 13px; }
.studio-send-btn, .studio-stop-btn, .studio-attach-btn { width: 36px; height: 36px; }
.studio-token-bar { gap: 6px; margin-bottom: 6px; }
.studio-image-previews { gap: 6px; padding: 6px 4px; }
.studio-image-preview { width: 80px; height: 80px; }
.studio-scroll-btns { right: 8px; bottom: 8px; }
/* Feed items */
.feed-item { padding: 4px 6px; gap: 6px; }
.feed-avatar { width: 22px; height: 22px; font-size: 12px; }
.feed-header { gap: 4px; flex-wrap: wrap; }
.feed-rank-badge { font-size: 8px; padding: 0 4px; }
.feed-role { font-size: 10px; }
.feed-time { font-size: 9px; }
.feed-content { font-size: 13px; }
.feed-content table { font-size: 11px; }
.feed-content th, .feed-content td { padding: 3px 6px; }
.feed-images { gap: 4px; }
.feed-image { max-width: 160px; max-height: 120px; }
/* Thinking blocks */
.feed-thinking-block { max-height: 120px; }
.feed-thinking-content { max-height: 60px; font-size: 11px; }
/* Tool blocks */
.studio-tool-args { font-size: 11px; }
.studio-tool-result { max-height: 150px; }
.studio-tool-result pre { font-size: 11px; }
/* Code blocks */
.studio-code-block pre { padding: 8px 10px; font-size: 12px; }
.studio-code-header { }
.studio-code-lang { font-size: 10px; padding: 3px 8px; }
/* Messages */
.message { max-width: 95%; padding: 10px 12px; font-size: 13px; }
/* Shell / Terminal — AI panel stacks below on narrow screens */
.shell-ai-col {
width: 100%;
max-width: 100%;
border-left: none;
border-top: 1px solid var(--border);
max-height: 45vh;
flex-shrink: 1;
}
.ai-panel-messages { padding: 8px; }
.ai-message { padding: 6px 8px; font-size: 12px; }
.ai-panel-input input { font-size: 12px; padding: 5px 8px; }
.shell-modal-row { grid-template-columns: 1fr; gap: 6px; }
/* Config */
.config-tabs-bar { padding: 8px 12px; gap: 2px; overflow-x: auto; scrollbar-width: none; }
.config-tabs-bar::-webkit-scrollbar { display: none; }
.config-panel-body { padding: 10px 12px 12px; }
.config-card { padding: 14px 16px; }
.config-card-row { flex-wrap: wrap; gap: 6px; }
.config-card-label { width: 100%; margin-bottom: 2px; }
.config-card-value { width: 100%; }
.config-form-input { font-size: 12px; padding: 6px 10px; }
.provider-card-top { flex-wrap: wrap; }
.provider-card-meta { flex-wrap: wrap; gap: 8px; }
.provider-card-actions { width: 100%; justify-content: flex-end; }
.provider-setup-token-row { flex-direction: column; gap: 8px; }
.provider-setup-token-actions { width: 100%; }
.config-update-controls { flex-direction: column; gap: 8px; }
.config-update-info { flex-wrap: wrap; gap: 6px; }
.config-ai-tools-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.config-ai-tool-card { padding: 10px; min-height: 100px; }
/* Skills */
.skill-tiles { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.skill-tile { padding: 10px; }
.skill-detail-panel { width: 95%; max-height: 90vh; }
/* Grids */
.grid-2 { grid-template-columns: 1fr; padding: 10px; }
.split-horizontal { flex-direction: column; }
.split-right { width: 100%; border-left: none; border-top: 1px solid var(--border); max-height: 300px; }
/* Onboarding */
.onboarding-overlay { padding: 12px; }
}
/* ── Mobile portrait ── */
@media (max-width: 480px) {
:root { --header-h: 42px; }
/* Header */
.header { padding: 0 6px; gap: 4px; }
.header-brand { gap: 2px; }
.header-logo { font-size: 12px; letter-spacing: 1px; }
.header-logo-img { width: 16px !important; height: 16px !important; }
.header-nav { margin-left: 4px; gap: 1px; }
.nav-tab { padding: 4px 6px; font-size: 10px; gap: 0; }
.nav-tab .tab-icon { display: none; }
.header-clock { font-size: 9px; }
.header-indicators { gap: 4px; }
.indicator { width: 6px; height: 6px; }
/* Statusbar */
.statusbar { height: 24px; padding: 0 6px; }
.statusbar-sudo { font-size: 8px; padding: 0 3px; }
/* Dashboard */
.dash-grid { padding: 6px; gap: 6px; }
.dash-card { padding: 8px 10px; }
.dash-label { font-size: 10px; }
.dash-count { font-size: 9px; }
.dash-tool-tag { font-size: 10px; padding: 2px 5px; }
.dash-quota-name { font-size: 10px; min-width: 60px; }
.dash-consumption-name { font-size: 10px; }
.dash-consumption-day { font-size: 8px; }
.dash-proc-name { font-size: 10px; }
.dash-cmd-text { font-size: 10px; }
.dash-cmd-freq-name { font-size: 10px; width: 70px; }
.dash-svc-name { font-size: 10px; }
/* Studio */
.studio-feed { padding: 6px 4px; }
.studio-input-area { padding: 6px 4px 3px; }
.studio-input-row { gap: 4px; }
.studio-input-row textarea { min-height: 32px; padding: 6px 8px; font-size: 12px; }
.studio-send-btn, .studio-stop-btn, .studio-attach-btn { width: 32px; height: 32px; }
.studio-send-btn svg, .studio-stop-btn svg, .studio-attach-btn svg { width: 14px; height: 14px; }
.studio-image-preview { width: 60px; height: 60px; }
.studio-image-remove { width: 18px; height: 18px; font-size: 11px; top: 2px; right: 2px; }
.studio-input-hint { font-size: 9px; }
.studio-scroll-btn { width: 28px; height: 28px; }
.studio-scroll-btn svg { width: 12px; height: 12px; }
.studio-copy-btn { font-size: 9px; padding: 2px 6px; }
/* Feed */
.feed-item { padding: 3px 4px; gap: 4px; }
.feed-avatar { width: 20px; height: 20px; font-size: 11px; }
.feed-body { min-width: 0; }
.feed-header { gap: 3px; margin-bottom: 1px; }
.feed-rank-badge { font-size: 7px; padding: 0 3px; }
.feed-role { font-size: 9px; }
.feed-time { font-size: 8px; }
.feed-content { font-size: 12px; line-height: 1.4; }
.feed-content h1 { font-size: 16px; }
.feed-content h2 { font-size: 14px; }
.feed-content h3 { font-size: 13px; }
.feed-content h4 { font-size: 12px; }
.feed-content table { font-size: 10px; }
.feed-content th, .feed-content td { padding: 2px 4px; }
.feed-image { max-width: 120px; max-height: 90px; }
.feed-system-text { font-size: 11px; }
/* Thinking */
.feed-thinking-header { padding: 4px 6px; font-size: 9px; }
.feed-thinking-content { padding: 4px 6px; font-size: 10px; max-height: 50px; }
/* Tool blocks */
.studio-tool-header { padding: 4px 6px; gap: 4px; font-size: 11px; }
.studio-tool-name { font-size: 11px; }
.studio-tool-args { padding: 4px 6px; font-size: 10px; }
.studio-tool-result { max-height: 120px; }
.studio-tool-result pre { padding: 4px 6px; font-size: 10px; }
/* Code */
.studio-code-block pre { padding: 6px 8px; font-size: 11px; }
.studio-code-lang { font-size: 9px; padding: 2px 6px; }
/* Markdown content */
.feed-content blockquote { padding: 3px 8px; }
.feed-content ul, .feed-content ol { padding-left: 14px; }
.msg-bullet, .msg-step { font-size: 12px; }
.msg-h1 { font-size: 16px; }
.msg-h2 { font-size: 14px; }
.msg-h3 { font-size: 13px; }
.msg-h4 { font-size: 12px; }
.inline-code { font-size: 11px; padding: 1px 4px; }
/* Messages */
.message { max-width: 98%; padding: 8px 10px; font-size: 12px; line-height: 1.4; }
.chat-input-bar { padding: 10px 8px; gap: 6px; }
.chat-input-bar input { font-size: 13px; }
/* Shell — only AI panel shrinks, terminal stays full size */
.shell-ai-col { max-height: 40vh; }
.ai-panel-header { padding: 8px 10px; font-size: 12px; }
.ai-panel-messages { padding: 6px; gap: 4px; }
.ai-message { padding: 5px 6px; font-size: 11px; }
.ai-panel-input { padding: 6px 8px; gap: 4px; }
.ai-panel-input input { font-size: 11px; padding: 4px 6px; }
/* Config */
.config-tabs-bar { padding: 6px 8px; gap: 1px; }
.nav-tab { padding: 4px 6px; font-size: 10px; }
.config-panel-body { padding: 8px 8px 8px; }
.config-card { padding: 10px 12px; margin-bottom: 10px; border-radius: var(--radius); }
.config-card-row { padding: 6px 0; gap: 4px; }
.config-card-label { width: 100%; font-size: 11px; }
.config-card-value { font-size: 12px; }
.config-form-label { font-size: 10px; }
.config-form-input { font-size: 11px; padding: 5px 8px; }
.provider-card-v2 { padding: 10px 12px; }
.provider-card-name { font-size: 12px; }
.provider-card-meta { font-size: 10px; gap: 6px; flex-wrap: wrap; }
.provider-card-model { margin-top: 8px; padding-top: 8px; gap: 4px; }
.provider-card-model-value { font-size: 12px; }
.provider-setup-hint { font-size: 11px; padding: 8px 10px; }
.config-update-row { padding: 6px 10px; flex-wrap: wrap; gap: 4px; }
.config-update-name { font-size: 11px; min-width: auto; }
.config-update-versions { font-size: 10px; }
.config-ai-tools-grid { grid-template-columns: 1fr 1fr; gap: 6px; }
.config-ai-tool-card { padding: 8px; min-height: 80px; }
.config-ai-tool-name { font-size: 11px; }
.config-ai-tool-desc { font-size: 10px; }
.config-toast { padding: 8px 16px; font-size: 12px; bottom: 30px; }
/* Skills */
.skill-tiles { grid-template-columns: 1fr 1fr; gap: 6px; }
.skill-tile { padding: 8px; }
.skill-tile-name { font-size: 12px; }
.skill-tile-desc { font-size: 10px; -webkit-line-clamp: 2; }
.skill-detail-panel { width: 98%; max-height: 95vh; }
.skill-detail-header { padding: 12px 14px; }
.skill-detail-name { font-size: 14px; }
.skill-detail-body { padding: 14px; }
.skill-detail-content { font-size: 10px; padding: 8px; max-height: 200px; }
/* Buttons */
button { font-size: 12px; padding: 6px 12px; }
button.sm { font-size: 11px; padding: 3px 8px; }
input, textarea { font-size: 12px; padding: 6px 10px; }
/* Cards */
.card { padding: 12px; border-radius: var(--radius); }
.card-header { font-size: 10px; margin-bottom: 10px; }
.badge { font-size: 10px; padding: 1px 6px; }
.chip { font-size: 11px; padding: 4px 8px; }
/* Empty states */
.empty-state { padding: 24px 12px; font-size: 12px; }
/* KaTeX */
.katex { font-size: 0.85em; }
.katex-display { margin: 8px 0; overflow-x: auto; }
} }
.config-ai-tools-grid { .config-ai-tools-grid {