feat: surface template README to agent template tools (#26334)

Fixes CODAGT-447.

Alternative implementation of https://github.com/coder/coder/pull/26212
and https://github.com/coder/coder/pull/25978

- Adds up to the first 1000 characters of `README.md` (with leading
frontmatter stripped) to `chattool.list_templates` output
- Adds up to 800 characters of `README.md` to `chattool.read_template`.

**Note:** skipping `toolsdk` versions to keep scope small.

> 🤖 Generated by Coder Agents
This commit is contained in:
Cian Johnston
2026-06-24 12:32:46 +01:00
committed by GitHub
parent 85652554f9
commit 2d28c1b396
11 changed files with 712 additions and 8 deletions
+66
View File
@@ -9,9 +9,22 @@ import (
gomarkdown "github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
goldmarkhtml "github.com/yuin/goldmark/renderer/html"
xhtml "golang.org/x/net/html"
"golang.org/x/xerrors"
)
// innerTextMarkdown converts Markdown to HTML for InnerTextFromMarkdown. Table
// renders cells as text (not pipe-delimited lines); WithUnsafe lets embedded raw
// HTML through so its inner text survives. Safe to share: goldmark inits the
// parser once via sync.Once, then only reads it.
var innerTextMarkdown = goldmark.New(
goldmark.WithExtensions(extension.Table),
goldmark.WithRendererOptions(goldmarkhtml.WithUnsafe()),
)
var plaintextStyle = ansi.StyleConfig{
Document: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
@@ -108,3 +121,56 @@ func HTMLFromMarkdown(markdown string) string {
})
return string(bytes.TrimSpace(gomarkdown.Render(doc, renderer)))
}
// InnerTextFromMarkdown renders Markdown (including embedded raw HTML) to HTML
// and returns its visible text ("innerText"). Block, code-line, and table-cell
// boundaries become newlines and intra-line whitespace is collapsed; link text
// is kept but URLs, images, and badges are dropped.
//
// Input is untrusted: a parser panic is recovered and returned as an error.
func InnerTextFromMarkdown(markdown string) (out string, err error) {
defer func() {
if r := recover(); r != nil {
out, err = "", xerrors.Errorf("render markdown to innertext: %v", r)
}
}()
var rendered bytes.Buffer
if convErr := innerTextMarkdown.Convert([]byte(markdown), &rendered); convErr != nil {
return "", xerrors.Errorf("convert markdown to html: %w", convErr)
}
z := xhtml.NewTokenizer(&rendered)
var b strings.Builder
// script and style are raw-text elements: their body is the single text token
// after the start tag. Skip just that token (not a running depth) so a stray
// </script> or unterminated tag can't swallow the rest of the document.
skipNextText := false
for {
if z.Next() == xhtml.ErrorToken {
break // includes io.EOF
}
switch tok := z.Token(); tok.Type {
case xhtml.StartTagToken:
skipNextText = tok.Data == "script" || tok.Data == "style"
case xhtml.TextToken:
if skipNextText {
skipNextText = false
continue
}
_, _ = b.WriteString(tok.Data)
default:
skipNextText = false
}
}
// Collapse intra-line whitespace but keep newlines so code lines, table
// cells, and block boundaries stay on separate lines; drop blank lines.
var lines []string
for _, line := range strings.Split(b.String(), "\n") {
if f := strings.Join(strings.Fields(line), " "); f != "" {
lines = append(lines, f)
}
}
return strings.Join(lines, "\n"), nil
}