Vectrum
Sign In
56 lines · 1.7 KB · Go
package tools

import (
	"context"
	"io"
)

// Progress is where a long-running tool reports output it has produced but not
// finished producing. It carries deltas, not the whole output so far: the
// consumer accumulates if it needs to, and a front end that only wants to show
// the last line does not have to diff two strings to find it.
//
// It may be called from more than one goroutine — a command's stdout and
// stderr are copied by separate ones — so an implementation must be safe for
// concurrent use.
type Progress func(chunk string)

type progressKey struct{}

// WithProgress attaches a sink to ctx. The agent loop installs one around each
// tool call so the tool does not have to know who is watching, or whether
// anyone is.
func WithProgress(ctx context.Context, p Progress) context.Context {
	if p == nil {
		return ctx
	}
	return context.WithValue(ctx, progressKey{}, p)
}

// ProgressFrom returns the sink on ctx, or nil when nothing is watching.
// A tool that finds nothing here should do no extra work at all: the cost of
// streaming is only worth paying when it is going somewhere.
func ProgressFrom(ctx context.Context) Progress {
	p, _ := ctx.Value(progressKey{}).(Progress)
	return p
}

// progressWriter feeds an io.Writer chain into a sink.
type progressWriter struct{ p Progress }

func (w progressWriter) Write(b []byte) (int, error) {
	if len(b) > 0 {
		w.p(string(b))
	}
	return len(b), nil
}

// tee returns w, plus a copy into the sink when there is one. A nil sink gives
// back exactly what was passed in, so the no-observer path allocates nothing
// and copies nothing.
func tee(w io.Writer, p Progress) io.Writer {
	if p == nil {
		return w
	}
	return io.MultiWriter(w, progressWriter{p})
}