Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633df84168 | ||
|
|
8e4fdfecf3 | ||
|
|
e1fe0881cf | ||
|
|
053cbca779 | ||
|
|
fd03423062 | ||
|
|
db6e7a1bf8 | ||
|
|
face2637da | ||
|
|
5a39a3a804 | ||
|
|
b7b66634ea | ||
|
|
591dc5adcd | ||
|
|
be40fa278f | ||
|
|
3f4432d88a |
311
CHANGELOG.md
311
CHANGELOG.md
@@ -4,6 +4,317 @@ 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.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
|
||||
|
||||
### Changes since v0.9.1
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
const (
|
||||
Name = "muyue"
|
||||
Version = "0.9.2"
|
||||
Version = "0.9.5"
|
||||
Author = "La Légion de Muyue"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,21 +6,18 @@ import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import { SearchAddon } from '@xterm/addon-search'
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||
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 { useI18n } from '../i18n'
|
||||
import mermaid from 'mermaid'
|
||||
import FileEditor from './FileEditor'
|
||||
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
|
||||
|
||||
const AI_TAB_ID = 0
|
||||
const MAX_TABS = 7
|
||||
const MAX_PANES = 4
|
||||
const SHELL_MAX_TOKENS = 100000
|
||||
const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change']
|
||||
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
|
||||
const LAYOUT_STORAGE_KEY = 'muyue_shell_layout'
|
||||
const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers'
|
||||
|
||||
function renderContent(text) {
|
||||
@@ -480,107 +477,6 @@ export default function Shell({ api, isSudo }) {
|
||||
const _streamRafRef = 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(() => {
|
||||
_streamRafRef.current = null
|
||||
const pending = _streamPendingRef.current
|
||||
@@ -922,22 +818,6 @@ export default function Shell({ api, isSudo }) {
|
||||
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.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
|
||||
</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 && (
|
||||
<div className="shell-new-tab-wrapper">
|
||||
<button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}>
|
||||
@@ -1525,59 +1384,34 @@ Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. L
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`shell-xterm-wrapper ${splitLayout ? 'has-splits' : ''}`}>
|
||||
{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}
|
||||
<div className="shell-xterm-wrapper">
|
||||
{showSearch && (
|
||||
<div className="shell-search-bar">
|
||||
<Search size={14} className="shell-search-icon" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
className="shell-search-input"
|
||||
value={searchText}
|
||||
onChange={e => handleSearchChange(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') { e.shiftKey ? handleSearchPrev() : handleSearchNext() }
|
||||
if (e.key === 'Escape') handleCloseSearch()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
placeholder="Rechercher..."
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{showSearch && (
|
||||
<div className="shell-search-bar">
|
||||
<Search size={14} className="shell-search-icon" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
className="shell-search-input"
|
||||
value={searchText}
|
||||
onChange={e => handleSearchChange(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') { e.shiftKey ? handleSearchPrev() : handleSearchNext() }
|
||||
if (e.key === 'Escape') handleCloseSearch()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
placeholder="Rechercher..."
|
||||
/>
|
||||
<button className="shell-search-nav" onClick={handleSearchPrev} title="Précédent (Shift+Entrée)">↑</button>
|
||||
<button className="shell-search-nav" onClick={handleSearchNext} title="Suivant (Entrée)">↓</button>
|
||||
<button className="shell-search-close" onClick={handleCloseSearch}><X size={14} /></button>
|
||||
</div>
|
||||
)}
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
id={`terminal-${tab.id}`}
|
||||
className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
<button className="shell-search-nav" onClick={handleSearchPrev} title="Précédent (Shift+Entrée)">↑</button>
|
||||
<button className="shell-search-nav" onClick={handleSearchNext} title="Suivant (Entrée)">↓</button>
|
||||
<button className="shell-search-close" onClick={handleCloseSearch}><X size={14} /></button>
|
||||
</div>
|
||||
)}
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
id={`terminal-${tab.id}`}
|
||||
className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1941,158 +1775,3 @@ const ShellAIMessage = memo(function ShellAIMessage({ msg, sendToTerminal, termi
|
||||
</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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
import mermaid from 'mermaid'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
@@ -9,8 +8,6 @@ import rehypeHighlight from 'rehype-highlight'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import 'highlight.js/styles/github-dark.css'
|
||||
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })
|
||||
|
||||
const RANKS = {
|
||||
commandant: { label: 'Commandant', short: 'CDT', color: '#FFD740' },
|
||||
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
html = html.replace(/^(\|.+\|)\n(\|[\s\-:|]+\|)\n((?:\|.+\|\n?)+)/gm, (match, headerRow, sepRow, bodyRows) => {
|
||||
const headers = headerRow.split('|').filter(c => c.trim() !== '').map(c => `<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 }) {
|
||||
return (
|
||||
<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 }) {
|
||||
if (raw) {
|
||||
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 isSystem = msg.role === 'system'
|
||||
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' }) : ''
|
||||
|
||||
@@ -365,21 +238,8 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
|
||||
)}
|
||||
{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 (
|
||||
<>
|
||||
{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) => {
|
||||
if (seg.type === 'text') {
|
||||
if (!seg.content) return null
|
||||
@@ -406,21 +266,9 @@ function FeedItem({ msg, activeAgents, onModeChange }) {
|
||||
) : (
|
||||
<>
|
||||
{parsedToolCalls && (() => {
|
||||
const compress = collapseHistory && !forceExpand && parsedToolCalls.length > 1
|
||||
const items = compress ? parsedToolCalls.slice(-1) : parsedToolCalls
|
||||
return (
|
||||
<>
|
||||
{compress && (
|
||||
<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) => {
|
||||
{parsedToolCalls.map((tc, i) => {
|
||||
const resultData = parsedToolResults
|
||||
? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
|
||||
: null
|
||||
@@ -448,17 +296,6 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
|
||||
const rank = RANKS.general
|
||||
const cleanContent = content.replace(/<think[^>]*>[\s\S]*?<\/think>/gi, '')
|
||||
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')
|
||||
|
||||
@@ -477,43 +314,27 @@ function StreamingItem({ content, thinking, toolCalls, segments, activeAgents, o
|
||||
{thinking && <ThinkingBlock content={thinking} raw done={false} />}
|
||||
{hasOrderedSegments ? (
|
||||
<>
|
||||
{compress && (
|
||||
<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.content) return null
|
||||
return (
|
||||
<div key={`t${i}`} className="feed-content">
|
||||
<MarkdownContent content={seg.content} raw={false} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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 null
|
||||
})
|
||||
})()}
|
||||
{segments.map((seg, i) => {
|
||||
if (seg.type === 'text') {
|
||||
if (!seg.content) return null
|
||||
return (
|
||||
<div key={`t${i}`} className="feed-content">
|
||||
<MarkdownContent content={seg.content} raw={false} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (seg.type === 'tool') {
|
||||
return <ToolCallBlock key={`tc${i}`} call={seg.call} result={seg.result} activeAgents={activeAgents} onModeChange={onModeChange} />
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{hasToolCalls && (compress
|
||||
? [<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) => (
|
||||
{hasToolCalls && toolCalls.map((tc, i) => (
|
||||
<ToolCallBlock key={tc.call?.tool_call_id || i} call={tc.call} result={tc.result} activeAgents={activeAgents} onModeChange={onModeChange} />
|
||||
))
|
||||
)}
|
||||
}
|
||||
{cleanContent && (
|
||||
<div className="feed-content">
|
||||
<MarkdownContent content={cleanContent} raw={false} />
|
||||
@@ -551,7 +372,6 @@ export default function Studio({ api }) {
|
||||
const [sudoModal, setSudoModal] = useState(null)
|
||||
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 }
|
||||
})
|
||||
|
||||
@@ -1313,8 +1313,6 @@ input::placeholder { color: var(--text-disabled); }
|
||||
color: var(--accent-muted) !important;
|
||||
}
|
||||
|
||||
.config-ai-tools-grid {
|
||||
|
||||
/* ── KaTeX overrides ── */
|
||||
.katex { font-size: 1em; color: var(--text-primary); }
|
||||
.katex-display { margin: 12px 0; overflow-x: auto; }
|
||||
@@ -1376,25 +1374,352 @@ input::placeholder { color: var(--text-disabled); }
|
||||
.hljs-number { color: var(--warning) !important; }
|
||||
|
||||
/* ── Responsive / Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
:root { --sidebar-w: 100%; --header-h: 46px; }
|
||||
.header { padding: 0 12px; gap: 8px; }
|
||||
.header-nav { margin-left: 12px; gap: 2px; }
|
||||
.nav-tab { padding: 6px 10px; font-size: 12px; }
|
||||
.header-brand { gap: 6px; }
|
||||
.header-logo { font-size: 15px; letter-spacing: 2px; }
|
||||
.studio-feed { padding: 12px 8px; }
|
||||
.studio-input-area { padding: 8px 8px 4px; }
|
||||
.feed-item { padding: 6px 8px; }
|
||||
.feed-avatar { width: 24px; height: 24px; }
|
||||
.dash-grid { grid-template-columns: 1fr; grid-template-rows: auto; height: auto; overflow: auto; }
|
||||
.dash-span-2 { grid-column: span 1; }
|
||||
|
||||
/* ── Large tablets / small laptops ── */
|
||||
@media (max-width: 1200px) {
|
||||
.dash-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: auto;
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
.dash-span-2 { grid-column: span 2; }
|
||||
.shell-ai-col { width: 280px; max-width: 280px; }
|
||||
.config-profile-center { max-width: 100%; }
|
||||
.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; }
|
||||
.split-horizontal { flex-direction: column; }
|
||||
.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 {
|
||||
|
||||
Reference in New Issue
Block a user