test terminal typing
This commit is contained in:
@@ -50,7 +50,7 @@ type terminalTypeArgs struct {
|
||||
func (m *Manager) terminalTypeTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "terminal_type",
|
||||
Description: "Type into your task's terminal: the shell in this task's worktree that the human uses from the Terminal tab, starting it when none is running. " +
|
||||
Description: "Type into your task's terminal: the shell in this task's worktree that the human uses from the Terminal tab, starting it when none is running and waiting for its prompt before typing. " +
|
||||
"text goes to the shell exactly as keystrokes, so end a command with \"\\n\" to run it, and send control characters for keys (\"\\u0003\" is Ctrl-C). " +
|
||||
"The human sees everything you type and its output. " +
|
||||
"Returns the output that followed, as terminal_read does, once the terminal has been quiet for half a second or after 10 seconds; read the rest of a longer command's output with terminal_read.",
|
||||
@@ -84,13 +84,22 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool {
|
||||
}
|
||||
|
||||
func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (string, error) {
|
||||
_, out := term.Subscribe()
|
||||
backlog, out := term.Subscribe()
|
||||
if out == nil {
|
||||
exit, _ := term.ExitStatus()
|
||||
return "", fmt.Errorf("the shell ended (%s); type again to start a new one", exit)
|
||||
}
|
||||
defer term.Unsubscribe(out)
|
||||
|
||||
got := []byte{}
|
||||
if promptWait(term) > 0 {
|
||||
var err error
|
||||
got, err = awaitPrompt(ctx, term, out, keepTail(backlog))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
stop := atomic.Bool{}
|
||||
defer stop.Store(true)
|
||||
typed := atomic.Int64{}
|
||||
@@ -108,7 +117,6 @@ func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (str
|
||||
written <- nil
|
||||
}()
|
||||
|
||||
got := []byte{}
|
||||
result := func() string {
|
||||
res := terminal.Text(got)
|
||||
exit, exited := term.ExitStatus()
|
||||
@@ -140,11 +148,46 @@ func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (str
|
||||
if !open {
|
||||
return result(), nil
|
||||
}
|
||||
got = append(got, b...)
|
||||
if len(got) > 2*typeKeep {
|
||||
got = bytes.Clone(got[len(got)-typeKeep:])
|
||||
}
|
||||
got = keepTail(append(got, b...))
|
||||
quiet.Reset(typeQuiet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func keepTail(b []byte) []byte {
|
||||
if len(b) > 2*typeKeep {
|
||||
return bytes.Clone(b[len(b)-typeKeep:])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func promptWait(term *terminal.Terminal) time.Duration {
|
||||
started, last := term.Activity()
|
||||
wait := time.Until(started.Add(typeMaxWait))
|
||||
if !last.IsZero() {
|
||||
wait = min(wait, time.Until(last.Add(typeQuiet)))
|
||||
}
|
||||
return wait
|
||||
}
|
||||
|
||||
func awaitPrompt(ctx context.Context, term *terminal.Terminal, out chan []byte, got []byte) ([]byte, error) {
|
||||
for {
|
||||
wait := promptWait(term)
|
||||
if wait <= 0 {
|
||||
return got, nil
|
||||
}
|
||||
timer := time.NewTimer(wait)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
case b, open := <-out:
|
||||
timer.Stop()
|
||||
if !open {
|
||||
return got, nil
|
||||
}
|
||||
got = keepTail(append(got, b...))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
|
||||
@@ -30,11 +31,13 @@ type Terminal struct {
|
||||
ptmx *os.File
|
||||
cmd *exec.Cmd
|
||||
|
||||
mu sync.Mutex
|
||||
backlog []byte
|
||||
clients map[chan []byte]bool
|
||||
exit string
|
||||
exited bool
|
||||
mu sync.Mutex
|
||||
backlog []byte
|
||||
clients map[chan []byte]bool
|
||||
exit string
|
||||
exited bool
|
||||
started time.Time
|
||||
lastOutput time.Time
|
||||
}
|
||||
|
||||
func New() *Terminals {
|
||||
@@ -68,7 +71,7 @@ func (ts *Terminals) Attach(t api.Task, size api.TerminalSize) (*Terminal, error
|
||||
return nil, fmt.Errorf("start %s: %w", shell, err)
|
||||
}
|
||||
|
||||
term = &Terminal{ptmx: ptmx, cmd: cmd, clients: map[chan []byte]bool{}}
|
||||
term = &Terminal{ptmx: ptmx, cmd: cmd, clients: map[chan []byte]bool{}, started: time.Now()}
|
||||
ts.byTask[t.Id] = term
|
||||
slog.Info("terminal started", "task", t.Id, "shell", shell, "pid", cmd.Process.Pid)
|
||||
go func() {
|
||||
@@ -144,6 +147,7 @@ func (term *Terminal) output(b []byte) {
|
||||
term.mu.Lock()
|
||||
defer term.mu.Unlock()
|
||||
|
||||
term.lastOutput = time.Now()
|
||||
term.backlog = append(term.backlog, b...)
|
||||
if len(term.backlog) > 2*backlogSize {
|
||||
term.backlog = bytes.Clone(term.backlog[len(term.backlog)-backlogSize:])
|
||||
@@ -192,6 +196,12 @@ func (term *Terminal) ExitStatus() (string, bool) {
|
||||
return term.exit, term.exited
|
||||
}
|
||||
|
||||
func (term *Terminal) Activity() (started, lastOutput time.Time) {
|
||||
term.mu.Lock()
|
||||
defer term.mu.Unlock()
|
||||
return term.started, term.lastOutput
|
||||
}
|
||||
|
||||
func (term *Terminal) Write(b []byte) error {
|
||||
_, err := term.ptmx.Write(b)
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user