package tools import ( "context" "strings" ) // Diagnoser is what edit_file and write_file ask about a file they have just // changed: whatever this machine's language server or linter has to say about // it, formatted for the model, or "" for nothing. *diag.Runner implements it. // // It is an interface here rather than a *diag.Runner so the tools can be // tested against a stub, and so the tools package says nothing about how a // file gets checked — only that it may be. type Diagnoser interface { Report(ctx context.Context, root, path string) string } // Option configures the file-changing tools. There is one so far; it is an // option rather than a third positional argument so that every existing // NewEdit(root, state) call, in main.go and the tests, goes on compiling. type Option func(*options) type options struct { diag Diagnoser procs *Processes } // WithDiagnostics has the tool append diagnostics for the changed file to its // result. A nil d, or a Diagnoser whose Report returns "", means none: the // result is exactly what it was before this feature existed. func WithDiagnostics(d Diagnoser) Option { return func(o *options) { o.diag = d } } func applyOptions(opts []Option) options { var o options for _, opt := range opts { if opt != nil { opt(&o) } } return o } // describe finishes a tool description. The sentence about diagnostics is // only added when a Diagnoser is wired in: a tool that promises the model // diagnostics and never delivers any teaches it that silence means nothing, // which is the opposite of what silence is meant to mean. func (o options) describe(desc string) string { desc = strings.TrimSpace(desc) if o.diag == nil { return desc } // Its own paragraph. Run onto the end of the last sentence it inherited // that sentence's line breaks and came out starting mid-line, which reads // like a bug in whatever assembled the description. return desc + "\n\n" + `If a language server or linter has something to say about the file afterwards it is appended under "diagnostics"; no such block means the file was clean.` } // withDiagnostics appends the diagnostics block, if any, to a successful tool // result. It is only ever called on the success path: a failed edit already // has the model's attention, and diagnostics about a file that did not change // would be about the previous edit. func (o options) withDiagnostics(ctx context.Context, root, path, result string) string { if o.diag == nil { return result } report := o.diag.Report(ctx, root, path) if report == "" { return result } return strings.TrimRight(result, "\n") + "\n\n" + report }