Export diagrams as interactive HTML from the app and an agent tool

This commit is contained in:
Ian Gulliver
2026-10-23 08:12:23 -07:00
parent caf5b123d2
commit f18dd88465
7 changed files with 269 additions and 19 deletions
+45 -2
View File
@@ -44,7 +44,50 @@ func (m *Manager) renderDiagramTool() mcp.Tool {
}
}
func renderDiagram(ctx context.Context, source string) ([]byte, error) {
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: func(ctx context.Context, raw json.RawMessage) (string, error) {
args := struct {
Source string `json:"source"`
Path string `json:"path"`
}{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed export_diagram call: %w", err)
}
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
@@ -52,7 +95,7 @@ func renderDiagram(ctx context.Context, source string) ([]byte, error) {
app := filepath.Join(filepath.Dir(exe), "..", "MacOS", "Byre")
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, app, "render-diagram")
cmd := exec.CommandContext(ctx, app, append([]string{"render-diagram"}, flags...)...)
cmd.Stdin = strings.NewReader(source)
stderr := bytes.Buffer{}
cmd.Stderr = &stderr
+2
View File
@@ -33,3 +33,5 @@ highlight "Event loop" store server coord
Keep each box to a title and one or two short detail lines, and draw only the arrows the reader needs; a picture with every dependency in it reads worse than prose.
Check a diagram before you send it: `render_diagram` takes the block's body and returns a PNG of what the app draws, or the reason it can't. Look for crossing or overlapping arrows and labels sitting on top of each other, and adjust the rows, `aside` or arrows until it reads cleanly.
To hand a diagram over as a file, `export_diagram` writes it as one self-contained HTML page with the app's exact layout and its full-size view's interactivity (click to highlight, highlight buttons, zoom and pan, light and dark mode); show it with `show_page`. The full-size view in the app has the same export as its Export HTML button.
+2 -2
View File
@@ -867,7 +867,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
if err != nil {
return nil, err
}
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool(), m.renderDiagramTool()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l)}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
lead := true
if sess.Role == api.Implementer {
g += longTitleGuidance(task)
@@ -937,7 +937,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
return nil, err
}
l.notes = "\n\n" + notes
tools = append(append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool()), m.clientShellTools()...)
tools = append(append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l)), m.clientShellTools()...)
}
env := git.IdentityEnv(l.gitName, l.gitEmail)
servers := []*mcp.Server{{Name: "byre", Version: "0.0.0", Tools: tools}}