Merge pull request 'release: v0.7.2 stable — promote develop to main' (#8) from develop into main
All checks were successful
Stable Release / stable (push) Successful in 1m4s
All checks were successful
Stable Release / stable (push) Successful in 1m4s
Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@@ -4,6 +4,24 @@ 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.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
|
||||
|
||||
@@ -213,7 +213,17 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
orb.SetSystemPrompt(studioPrompt.String())
|
||||
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 {
|
||||
enrichedMessage = enrichedMessage + "\n\n[RAPPORT PRÉALABLE — produit par un autre modèle, à valider]\n" + report + "\n[/RAPPORT PRÉALABLE]"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty/v2"
|
||||
"github.com/gorilla/websocket"
|
||||
"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")
|
||||
|
||||
ptmx, err := pty.Start(cmd)
|
||||
session, err := startTermSession(cmd)
|
||||
if err != nil {
|
||||
conn.WriteJSON(wsMessage{Type: "error", Data: err.Error()})
|
||||
return
|
||||
@@ -163,11 +162,8 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
||||
var once sync.Once
|
||||
cleanup := func() {
|
||||
once.Do(func() {
|
||||
ptmx.Close()
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
}
|
||||
session.Close()
|
||||
session.Wait()
|
||||
})
|
||||
}
|
||||
defer cleanup()
|
||||
@@ -175,11 +171,8 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := ptmx.Read(buf)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
n, err := session.Read(buf)
|
||||
if n > 0 {
|
||||
if err := conn.WriteJSON(wsMessage{
|
||||
Type: "output",
|
||||
Data: string(buf[:n]),
|
||||
@@ -188,6 +181,11 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
conn.SetReadLimit(1 << 20)
|
||||
@@ -207,16 +205,13 @@ func (s *Server) handleTerminalWS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch msg.Type {
|
||||
case "input":
|
||||
if _, err := ptmx.Write([]byte(msg.Data)); err != nil {
|
||||
if _, err := session.Write([]byte(msg.Data)); err != nil {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
case "resize":
|
||||
if msg.Rows > 0 && msg.Cols > 0 {
|
||||
pty.Setsize(ptmx, &pty.Winsize{
|
||||
Rows: msg.Rows,
|
||||
Cols: msg.Cols,
|
||||
})
|
||||
session.Resize(msg.Rows, msg.Cols)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
198
internal/api/terminal_session.go
Normal file
198
internal/api/terminal_session.go
Normal 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
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
const (
|
||||
Name = "muyue"
|
||||
Version = "0.7.0"
|
||||
Version = "0.7.2"
|
||||
Author = "La Légion de Muyue"
|
||||
)
|
||||
|
||||
|
||||
@@ -148,6 +148,9 @@ lesquels déclenchent une erreur console.`}
|
||||
<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>.
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user