Files
byre/internal/agents/diagram.go
T

98 lines
3.0 KiB
Go

package agents
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"g.fc.run/ai/byre/internal/mcp"
)
func (m *Manager) renderDiagramTool() mcp.Tool {
return mcp.Tool{
Name: "render_diagram",
Description: "See a diagram as byre's app draws it in the transcript: pass the body of a ```diagram block and get back a PNG of the result, drawn by the app's own renderer in light mode. " +
"A source the app can't draw comes back as the reason instead.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"source": {"type": "string", "description": "The block's body, without the fence lines."}
},
"required": ["source"]
}`),
Rich: mcp.TypedRich(func(ctx context.Context, args struct {
Source string `json:"source"`
}) mcp.Result {
png, err := renderDiagram(ctx, args.Source)
if err != nil {
return mcp.TextResult(err.Error(), true)
}
return mcp.Result{Content: []mcp.Content{{Type: "image", Data: base64.StdEncoding.EncodeToString(png), MimeType: "image/png"}}}
}),
}
}
func (m *Manager) exportDiagramTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "export_diagram",
Description: "Export a diagram as a self-contained HTML page, drawn exactly as byre's app lays it out and as interactive as its full-size view: clicking a box highlights it, its arrows and its neighbors, each highlight line is a button, and it zooms and pans. " +
"Pass the body of a ```diagram block and the file to write; show it with show_page.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"source": {"type": "string", "description": "The block's body, without the fence lines."},
"path": {"type": "string", "description": "The HTML file to write, relative to your worktree, absolute, or starting with ~/."}
},
"required": ["source", "path"]
}`),
Handler: mcp.Typed(func(ctx context.Context, args struct {
Source string `json:"source"`
Path string `json:"path"`
}) (string, error) {
full, err := l.resolve(args.Path)
if err != nil {
return "", err
}
html, err := renderDiagram(ctx, args.Source, "--html")
if err != nil {
return "", err
}
err = os.MkdirAll(filepath.Dir(full), 0o755)
if err != nil {
return "", err
}
err = os.WriteFile(full, html, 0o644)
if err != nil {
return "", err
}
return fmt.Sprintf("Wrote the diagram to %s.", args.Path), nil
}),
}
}
func renderDiagram(ctx context.Context, source string, flags ...string) ([]byte, error) {
exe, err := os.Executable()
if err != nil {
return nil, err
}
app := filepath.Join(filepath.Dir(exe), "..", "MacOS", "Byre")
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, app, append([]string{"render-diagram"}, flags...)...)
cmd.Stdin = strings.NewReader(source)
stderr := bytes.Buffer{}
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("render diagram: %w: %s", err, strings.TrimSpace(stderr.String()))
}
return out, nil
}