test terminal typing

This commit is contained in:
Ian Gulliver
2026-10-18 16:26:02 -07:00
parent d0bfd4f657
commit 2374b0f7a8
3 changed files with 67 additions and 14 deletions
+50 -7
View File
@@ -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...))
}
}
}