feat(tools): stream shell_run output live into the TUI
This commit is contained in:
+40
-5
@@ -1,6 +1,7 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"agentu/pkg/llm"
|
||||
)
|
||||
@@ -46,6 +48,10 @@ type shellRunArgs struct {
|
||||
}
|
||||
|
||||
func (t *ShellRunTool) Execute(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return t.ExecuteStream(ctx, raw, nil)
|
||||
}
|
||||
|
||||
func (t *ShellRunTool) ExecuteStream(ctx context.Context, raw json.RawMessage, emit func(string) error) (string, error) {
|
||||
var args shellRunArgs
|
||||
if err := json.Unmarshal(raw, &args); err != nil {
|
||||
return "", fmt.Errorf("parse arguments: %w", err)
|
||||
@@ -66,16 +72,45 @@ func (t *ShellRunTool) Execute(ctx context.Context, raw json.RawMessage) (string
|
||||
cmd := exec.CommandContext(ctx, shell, "-lc", args.Command)
|
||||
cmd.Dir = workdir
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
text := FormatResult(string(output))
|
||||
var mu sync.Mutex
|
||||
var combined bytes.Buffer
|
||||
writer := &shellStreamWriter{mu: &mu, buf: &combined, emit: emit}
|
||||
cmd.Stdout = writer
|
||||
cmd.Stderr = writer
|
||||
|
||||
runErr := cmd.Run()
|
||||
text := FormatResult(combined.String())
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return text, fmt.Errorf("command timed out: %w", ctx.Err())
|
||||
}
|
||||
if err != nil {
|
||||
if runErr != nil {
|
||||
if text != "" {
|
||||
return text, fmt.Errorf("command failed: %w", err)
|
||||
return text, fmt.Errorf("command failed: %w", runErr)
|
||||
}
|
||||
return "", fmt.Errorf("command failed: %w", err)
|
||||
return "", fmt.Errorf("command failed: %w", runErr)
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// shellStreamWriter captures command output for the final result while
|
||||
// forwarding every write to the streaming callback as it happens.
|
||||
type shellStreamWriter struct {
|
||||
mu *sync.Mutex
|
||||
buf *bytes.Buffer
|
||||
emit func(string) error
|
||||
}
|
||||
|
||||
func (w *shellStreamWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
n, writeErr := w.buf.Write(p)
|
||||
w.mu.Unlock()
|
||||
if writeErr != nil {
|
||||
return n, writeErr
|
||||
}
|
||||
if w.emit != nil {
|
||||
if err := w.emit(string(p)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user