Compare commits

...

6 Commits

Author SHA1 Message Date
Augustin ROUX
a1da9da3db Merge pull request 'feat(studio): auto-réflexion avancée pendant les tests (v0.7.2)' (#7) from release/v0.7.2 into develop
All checks were successful
Beta Release / beta (push) Successful in 1m7s
PR Check / check (pull_request) Successful in 52s
Reviewed-on: #7
2026-04-27 10:04:47 +00:00
Muyue
a7d4b31a0d feat(studio): force advanced reflection during browser-test sessions (v0.7.2)
All checks were successful
PR Check / check (pull_request) Successful in 55s
When at least one browser_test session is connected, every chat
message in Studio now auto-enables advanced reflection regardless of
the user toggle. The intent: during AI-driven UI testing, having a
second model produce a preliminary [RAPPORT PRÉALABLE] materially
improves which clicks the active model decides to perform and the
quality of the final ✓/✗ report.

- handlers_chat: derive wantReflection from body.AdvancedReflection
  OR (browserTestStore has any active session). The user toggle still
  works for normal conversations; tests just override it.
- Silent fallback when no inactive provider is configured (no error,
  no behaviour change for single-provider setups).
- Tests.jsx: add a hint explaining the auto-on behaviour so the user
  understands why the Studio toggle appears bypassed.
- Version 0.7.1 → 0.7.2 + CHANGELOG entry.
2026-04-27 12:02:04 +02:00
Augustin ROUX
0ee006f71f Merge pull request 'fix(terminal/windows): pipes fallback for v0.7.1' (#6) from release/v0.7.1 into develop
All checks were successful
Beta Release / beta (push) Successful in 1m7s
Reviewed-on: #6
2026-04-27 09:59:15 +00:00
Muyue
fc7a5b9d87 fix(terminal/windows): fallback to pipes when PTY unsupported (v0.7.1)
All checks were successful
PR Check / check (pull_request) Successful in 57s
The terminal tab was unusable on Windows: creack/pty has no native
Windows ConPTY support, so pty.Start() returned "operating system not
supported" and the WebSocket closed immediately on any tab click —
even though the menu detection (wsl --list --quiet, pwsh, cmd) worked.

Introduce a termSession interface with two implementations selected at
runtime:
- ptySession (unix): unchanged behaviour, real PTY via creack/pty,
  resize works, vim/top behave normally.
- pipeSession (windows): plain stdin + merged stdout/stderr pipes,
  forwarded to the WebSocket. Resize is a no-op (no SIGWINCH without a
  TTY), so full-screen TUIs misbehave in this mode — but launching
  wsl.exe, pwsh, or cmd works for line-based interaction, which is
  what the menu shortcuts target.

handleTerminalWS now goes through startTermSession(cmd); the unix path
is unchanged, the windows fallback kicks in only when pty.Start would
have failed.

Bump v0.7.0 → v0.7.1; CHANGELOG entry added.
2026-04-27 11:56:40 +02:00
CI Bot
654444ccc8 chore: update CHANGELOG for v0.7.0 2026-04-27 09:25:31 +00:00
Augustin ROUX
991878939b Merge pull request 'release: v0.7.0 stable — promote develop to main' (#5) from develop into main
All checks were successful
Stable Release / stable (push) Successful in 1m1s
Reviewed-on: #5
2026-04-27 09:24:20 +00:00
6 changed files with 297 additions and 22 deletions

View File

@@ -4,6 +4,75 @@ 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.7.2
### Amélioration
- **feat(studio): réflexion avancée forcée automatiquement pendant les tests** — quand au moins une session `browser_test` est connectée, chaque message à Studio active automatiquement la réflexion avancée (un second modèle, si configuré, produit un rapport préalable injecté dans le prompt actif). Le toggle UI est ignoré tant qu'une session de test est active. Justification : pendant un test piloté par l'IA, avoir une analyse complémentaire d'un autre modèle améliore matériellement la qualité des décisions de clic et la couverture du rapport final.
- Si aucun second provider n'est configuré, le comportement reste silencieux (fallback chat normal — pas d'erreur visible côté utilisateur).
- Hint UI ajouté dans l'onglet Tests pour expliquer le comportement.
## v0.7.1
### Fix
- **fix(terminal/windows): "unsupported" / connection closed** — `creack/pty` n'a pas de support Windows natif et `pty.Start()` retourne immédiatement une erreur ("operating system not supported"), fermant le WebSocket avant même la bannière. L'utilisateur voyait le menu des terminaux peuplé (détection OK : `wsl --list --quiet` fonctionne) mais chaque clic se soldait par "unsupported" ou une connexion fermée.
- Introduction de l'abstraction `termSession` (`internal/api/terminal_session.go`) avec deux implémentations sélectionnées au runtime :
- **`ptySession`** (Linux / macOS / BSDs) : conserve le comportement existant (TTY complet via `creack/pty`, resize, apps interactives type vim/top).
- **`pipeSession`** (Windows) : pipes natifs `stdin` + `stdout` + `stderr` mergés, lus en goroutines, forwardés au WebSocket. Suffisant pour `wsl.exe`, `pwsh`, `cmd` en mode ligne — la plupart des cas d'usage (lancer une commande, voir la sortie, taper la suivante). Resize est un no-op (pas de SIGWINCH sans TTY) ; les TUIs en plein écran ne fonctionnent pas dans ce mode.
- Refactor minimal de `handleTerminalWS` : utilise `startTermSession(cmd)` au lieu de `pty.Start(cmd)` direct ; même chemin code pour les deux OS.
## v0.7.0
### Changes since v0.4.0
- fix(ci): rename browser_test.go → browsertest.go (6d2f174)
- feat: AI-driven browser tests — Tests tab + browser_test agent tool (c820d55)
- release: v0.6.0 — security audit fixes + 7 new features (6a7b4d8)
- chore: bump version to 0.5.0 (2a6647b)
- feat: agent concurrency, conversation summaries, AI tools config, UI polish (3740454)
- feat: AI task API, token-based context windows, SSH password auth, sudo bypass detection (d98110c)
- feat: agent concurrency, conversation summaries, AI tools config, UI polish (d2bb42b)
- feat: AI task API, token-based context windows, SSH password auth, sudo bypass detection (e8a289c)
### Downloads
| Platform | File |
|----------|------|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-linux-amd64.tar.gz) |
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-linux-arm64.tar.gz) |
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-darwin-amd64.tar.gz) |
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-darwin-arm64.tar.gz) |
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-windows-amd64.zip) |
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-windows-arm64.zip) |
The binary includes both CLI and Desktop modes.
Run `muyue` for TUI, `muyue desktop` for web UI.
### Install
**Linux (x86_64)**
```bash
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-linux-amd64.tar.gz | tar xz
chmod +x muyue-linux-amd64
sudo mv muyue-linux-amd64 /usr/local/bin/muyue
```
**macOS (Apple Silicon)**
```bash
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-darwin-arm64.tar.gz | tar xz
chmod +x muyue-darwin-arm64
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
```
**Windows (x86_64)**
```powershell
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.7.0/muyue-windows-amd64.zip" -OutFile "muyue.zip"
Expand-Archive -Path "muyue.zip" -DestinationPath "."
Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe
```
## v0.7.0 ## v0.7.0
### Nouvelle fonctionnalité majeure : Tests pilotés par l'IA ### Nouvelle fonctionnalité majeure : Tests pilotés par l'IA

View File

@@ -213,7 +213,17 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
orb.SetSystemPrompt(studioPrompt.String()) orb.SetSystemPrompt(studioPrompt.String())
orb.SetTools(s.agentToolsJSON) orb.SetTools(s.agentToolsJSON)
if body.AdvancedReflection { // Auto-force advanced reflection while a browser-test session is active:
// the user is doing AI-driven UI testing, where having a second model
// produce a preliminary report (when one is configured) materially
// improves which clicks the active model decides to perform. The toggle
// remains user-controllable for non-test conversations.
wantReflection := body.AdvancedReflection
if !wantReflection && s.browserTestStore != nil && len(s.browserTestStore.List()) > 0 {
wantReflection = true
}
if wantReflection {
if report, ok := s.runReflectionReport(enrichedMessage); ok { if report, ok := s.runReflectionReport(enrichedMessage); ok {
enrichedMessage = enrichedMessage + "\n\n[RAPPORT PRÉALABLE — produit par un autre modèle, à valider]\n" + report + "\n[/RAPPORT PRÉALABLE]" enrichedMessage = enrichedMessage + "\n\n[RAPPORT PRÉALABLE — produit par un autre modèle, à valider]\n" + report + "\n[/RAPPORT PRÉALABLE]"
} }

View File

@@ -13,7 +13,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/creack/pty/v2"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/muyue/muyue/internal/config" "github.com/muyue/muyue/internal/config"
) )
@@ -154,7 +153,7 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
} }
cmd.Env = append(cmd.Env, "TERM=xterm-256color") cmd.Env = append(cmd.Env, "TERM=xterm-256color")
ptmx, err := pty.Start(cmd) session, err := startTermSession(cmd)
if err != nil { if err != nil {
conn.WriteJSON(wsMessage{Type: "error", Data: err.Error()}) conn.WriteJSON(wsMessage{Type: "error", Data: err.Error()})
return return
@@ -163,11 +162,8 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
var once sync.Once var once sync.Once
cleanup := func() { cleanup := func() {
once.Do(func() { once.Do(func() {
ptmx.Close() session.Close()
if cmd.Process != nil { session.Wait()
cmd.Process.Kill()
cmd.Wait()
}
}) })
} }
defer cleanup() defer cleanup()
@@ -175,11 +171,8 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
go func() { go func() {
buf := make([]byte, 4096) buf := make([]byte, 4096)
for { for {
n, err := ptmx.Read(buf) n, err := session.Read(buf)
if err != nil { if n > 0 {
cleanup()
return
}
if err := conn.WriteJSON(wsMessage{ if err := conn.WriteJSON(wsMessage{
Type: "output", Type: "output",
Data: string(buf[:n]), Data: string(buf[:n]),
@@ -188,6 +181,11 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
if err != nil {
cleanup()
return
}
}
}() }()
conn.SetReadLimit(1 << 20) conn.SetReadLimit(1 << 20)
@@ -207,16 +205,13 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
switch msg.Type { switch msg.Type {
case "input": case "input":
if _, err := ptmx.Write([]byte(msg.Data)); err != nil { if _, err := session.Write([]byte(msg.Data)); err != nil {
cleanup() cleanup()
return return
} }
case "resize": case "resize":
if msg.Rows > 0 && msg.Cols > 0 { if msg.Rows > 0 && msg.Cols > 0 {
pty.Setsize(ptmx, &pty.Winsize{ session.Resize(msg.Rows, msg.Cols)
Rows: msg.Rows,
Cols: msg.Cols,
})
} }
} }
} }

View File

@@ -0,0 +1,198 @@
package api
// Cross-platform terminal session abstraction.
//
// On Linux / macOS we have a real PTY via creack/pty: full TTY semantics,
// resize support, interactive apps (vim, top…) work. On Windows the same
// package returns "operating system not supported" at pty.Start time, so we
// fall back to plain pipes (stdin / stdout merged with stderr). Pipes don't
// give a real TTY — interactive TUIs misbehave — but `wsl`, `pwsh`, `cmd`,
// and most CLI tools emit usable line-buffered output, which is what the
// user actually clicks for.
import (
"io"
"os"
"os/exec"
"runtime"
"sync"
"github.com/creack/pty/v2"
)
// termSession is the read/write/resize/close surface used by handleTerminalWS.
type termSession interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Resize(rows, cols uint16) error
Close() error
Wait() error
Pid() int
}
// startTermSession tries a real PTY first; on Windows or any pty.Start failure
// it falls back to a pipe-based session.
func startTermSession(cmd *exec.Cmd) (termSession, error) {
if runtime.GOOS != "windows" {
ptmx, err := pty.Start(cmd)
if err == nil {
return &ptySession{ptmx: ptmx, cmd: cmd}, nil
}
// On unix, a pty.Start error is fatal — pipes won't help interactive
// shells without a TTY, and the unix build is the supported path.
return nil, err
}
return startPipeSession(cmd)
}
// ptySession wraps creack/pty's *os.File-backed PTY.
type ptySession struct {
ptmx *os.File
cmd *exec.Cmd
}
func (s *ptySession) Read(p []byte) (int, error) { return s.ptmx.Read(p) }
func (s *ptySession) Write(p []byte) (int, error) { return s.ptmx.Write(p) }
func (s *ptySession) Resize(rows, cols uint16) error {
return pty.Setsize(s.ptmx, &pty.Winsize{Rows: rows, Cols: cols})
}
func (s *ptySession) Close() error {
err := s.ptmx.Close()
if s.cmd.Process != nil {
s.cmd.Process.Kill()
}
return err
}
func (s *ptySession) Wait() error {
if s.cmd.Process == nil {
return nil
}
return s.cmd.Wait()
}
func (s *ptySession) Pid() int {
if s.cmd.Process == nil {
return 0
}
return s.cmd.Process.Pid
}
// pipeSession is the Windows fallback: stdin pipe + merged stdout/stderr pipe,
// running concurrently. Resize is a no-op (no TTY to send TIOCSWINSZ to).
type pipeSession struct {
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
stderr io.ReadCloser
mu sync.Mutex
merged chan []byte
closed bool
closeCh chan struct{}
}
func startPipeSession(cmd *exec.Cmd) (termSession, error) {
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
stdin.Close()
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
stdin.Close()
stdout.Close()
return nil, err
}
if err := cmd.Start(); err != nil {
stdin.Close()
stdout.Close()
stderr.Close()
return nil, err
}
s := &pipeSession{
cmd: cmd,
stdin: stdin,
stdout: stdout,
stderr: stderr,
merged: make(chan []byte, 32),
closeCh: make(chan struct{}),
}
go s.pump(stdout)
go s.pump(stderr)
return s, nil
}
func (s *pipeSession) pump(r io.ReadCloser) {
buf := make([]byte, 4096)
for {
n, err := r.Read(buf)
if n > 0 {
chunk := make([]byte, n)
copy(chunk, buf[:n])
select {
case s.merged <- chunk:
case <-s.closeCh:
return
}
}
if err != nil {
return
}
}
}
func (s *pipeSession) Read(p []byte) (int, error) {
select {
case chunk, ok := <-s.merged:
if !ok {
return 0, io.EOF
}
n := copy(p, chunk)
return n, nil
case <-s.closeCh:
return 0, io.EOF
}
}
func (s *pipeSession) Write(p []byte) (int, error) {
return s.stdin.Write(p)
}
func (s *pipeSession) Resize(rows, cols uint16) error {
// No real TTY → resize is a no-op; the child won't get SIGWINCH.
return nil
}
func (s *pipeSession) Close() error {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil
}
s.closed = true
close(s.closeCh)
s.mu.Unlock()
s.stdin.Close()
s.stdout.Close()
s.stderr.Close()
if s.cmd.Process != nil {
s.cmd.Process.Kill()
}
return nil
}
func (s *pipeSession) Wait() error {
if s.cmd.Process == nil {
return nil
}
return s.cmd.Wait()
}
func (s *pipeSession) Pid() int {
if s.cmd.Process == nil {
return 0
}
return s.cmd.Process.Pid
}

View File

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

View File

@@ -148,6 +148,9 @@ lesquels déclenchent une erreur console.`}
<p style={{ margin: '8px 0 0', opacity: 0.75, fontSize: '0.85em' }}> <p style={{ margin: '8px 0 0', opacity: 0.75, fontSize: '0.85em' }}>
L'IA dispose de l'outil <code>browser_test</code> avec les actions <code>list_clickables</code>, <code>click</code>, <code>console</code>, <code>eval</code>, <code>type</code>, <code>current_url</code>, <code>wait</code>, <code>summary</code>. L'IA dispose de l'outil <code>browser_test</code> avec les actions <code>list_clickables</code>, <code>click</code>, <code>console</code>, <code>eval</code>, <code>type</code>, <code>current_url</code>, <code>wait</code>, <code>summary</code>.
</p> </p>
<p style={{ margin: '8px 0 0', padding: 8, fontSize: '0.85em', background: 'var(--accent-bg, rgba(108,92,231,0.1))', border: '1px solid var(--accent, #6c5ce7)', borderRadius: 4 }}>
<strong>Réflexion avancée auto :</strong> tant qu'au moins une session de test est connectée, chaque message dans Studio utilise automatiquement la réflexion avancée — un second modèle (s'il est configuré) produit un rapport d'analyse préalable injecté dans le prompt actif. Le toggle Studio est ignoré pendant la session.
</p>
</div> </div>
</section> </section>