From 056c3d202a8c778b7235eb67f652e5a944f12132 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 21 Oct 2026 15:41:51 -0700 Subject: [PATCH] Let agents see rendered diagrams and drop Mermaid support --- api/openapi.yaml | 2 +- docs/macos.md | 2 +- internal/agents/diagram.go | 64 +++ internal/agents/inbox.go | 2 +- internal/agents/kb/diagrams.md | 4 +- internal/agents/lead.go | 2 +- internal/agents/manager.go | 4 +- internal/agents/review.go | 2 +- internal/api/api.gen.go | 648 +++++++++++++------------- mac/Sources/Byre/ByreApp.swift | 11 + mac/Sources/Byre/Diagram.swift | 594 ----------------------- mac/Sources/Byre/Fonts.swift | 5 +- mac/Sources/Byre/Markdown.swift | 7 +- mac/Sources/Byre/MarkdownView.swift | 2 - mac/Sources/Byre/Questions.swift | 2 +- mac/Sources/Byre/Render.swift | 33 ++ mac/Sources/Byre/Workflow.swift | 42 +- mac/Sources/Byre/WorkflowEditor.swift | 60 ++- 18 files changed, 506 insertions(+), 980 deletions(-) create mode 100644 internal/agents/diagram.go create mode 100644 mac/Sources/Byre/Render.swift diff --git a/api/openapi.yaml b/api/openapi.yaml index 714882d..8bd43ca 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1994,7 +1994,7 @@ components: end_line: { type: integer } ref: { type: string, description: "diff: git revision range." } url: { type: string } - mermaid: { type: string, description: "diagram: Mermaid source." } + diagram: { type: string, description: "diagram: source of a ```diagram block." } image: { type: string, description: "image: data URL." } task_id: { type: string } snapshot_id: { type: string } diff --git a/docs/macos.md b/docs/macos.md index b8a6285..dbfadc4 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -4,7 +4,7 @@ Platform constraints, build requirements and SwiftUI behavior observed live. ## Decision -**No web view, under any circumstances**: no `WKWebView`, no `WebView`, no HTML rendering of any kind, in the window, in a dialog, for Markdown, for diagrams, for the terminal or for previews. A link to a web page opens in the default browser. Mermaid needs a browser DOM, so the app renders diagrams natively. Mermaid's automatic layout can't give the banded, nested look of a hand-drawn architecture picture, so byre also has its own ```` ```diagram ```` block (rows, nested boxes, a side column, routed arrows, named highlights), laid out on a grid by the app; its syntax is the `diagrams` knowledge-base article. byre's Markdown dialect (choice markers, `
` sections, `#/task/` links, worktree image paths, `NO_REPLY`) matches no library, so the app parses it itself. +**No web view, under any circumstances**: no `WKWebView`, no `WebView`, no HTML rendering of any kind, in the window, in a dialog, for Markdown, for diagrams, for the terminal or for previews. A link to a web page opens in the default browser. Mermaid needs a browser DOM, and its automatic layout can't give the banded, nested look of a hand-drawn architecture picture, so byre has its own ```` ```diagram ```` block (rows, nested boxes, a side column, routed arrows, named highlights), laid out on a grid by the app; its syntax is the `diagrams` knowledge-base article. It is byre's only diagram format: a ```` ```mermaid ```` block shows as code, and the workflow stage graphs are drawn by generating its source. Agents see their diagrams through the `render_diagram` tool, which runs the installed app's own binary as `Byre render-diagram` (source on stdin, PNG on stdout), so the picture is exactly what the transcript draws; that mode opens no window and never touches the app's iCloud-synced store, so it uses the default text size. byre's Markdown dialect (choice markers, `
` sections, `#/task/` links, worktree image paths, `NO_REPLY`) matches no library, so the app parses it itself. The app joins the tailnet as its own node with libtailscale and reaches byre's HTTPS origin on byre's node, so byre knows who is calling from the node itself. It never sends identity headers, and certificate validation is never relaxed. diff --git a/internal/agents/diagram.go b/internal/agents/diagram.go new file mode 100644 index 0000000..4989c52 --- /dev/null +++ b/internal/agents/diagram.go @@ -0,0 +1,64 @@ +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: func(ctx context.Context, raw json.RawMessage) mcp.Result { + args := struct { + Source string `json:"source"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return mcp.TextResult(fmt.Sprintf("malformed render_diagram call: %v", err), true) + } + 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 renderDiagram(ctx context.Context, source 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, "render-diagram") + 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 +} diff --git a/internal/agents/inbox.go b/internal/agents/inbox.go index da9968d..acceb8a 100644 --- a/internal/agents/inbox.go +++ b/internal/agents/inbox.go @@ -65,7 +65,7 @@ func (m *Manager) askTool(l *live) mcp.Tool { "end_line": {"type": "integer"}, "ref": {"type": "string"}, "url": {"type": "string"}, - "mermaid": {"type": "string"}, + "diagram": {"type": "string", "description": "For kind diagram: the body of a ` + "```diagram" + ` block, whose format is the diagrams knowledge-base article."}, "task_id": {"type": "string"}, "snapshot_id": {"type": "string"}, "caption": {"type": "string"} diff --git a/internal/agents/kb/diagrams.md b/internal/agents/kb/diagrams.md index a93a633..c71fd33 100644 --- a/internal/agents/kb/diagrams.md +++ b/internal/agents/kb/diagrams.md @@ -2,7 +2,7 @@ How to draw a box diagram (architecture, components, data flow) that byre's app # Box diagrams -A fenced block with the language `diagram` draws boxes in rows with arrows between them. The app lays it out itself, so you never place coordinates. Clicking it opens a full-size view: pinch or scroll to zoom and pan; clicking a box highlights it, its arrows and its neighbors; each `highlight` line becomes a button there. Mermaid flowcharts (```` ```mermaid ````) still work for graphs whose shape the layout should find on its own. +A fenced block with the language `diagram` draws boxes in rows with arrows between them. The app lays it out itself, so you never place coordinates. Clicking it opens a full-size view: pinch or scroll to zoom and pan; clicking a box highlights it, its arrows and its neighbors; each `highlight` line becomes a button there. It is the only diagram format byre draws; a ```` ```mermaid ```` block shows as plain code. ```diagram row @@ -31,3 +31,5 @@ highlight "Event loop" store server coord - Lines starting with `#` are comments. A mistake shows the source with the reason it wasn't drawn. 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. diff --git a/internal/agents/lead.go b/internal/agents/lead.go index 2da0837..2eca39b 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -33,7 +33,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left - Dig into context with list_tasks, read_transcript, read_inbox and read_status (vendor rate limits and agent health), and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message. - You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it. - Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine. -- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix". +- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a fenced diagram block (its format is the diagrams knowledge-base article) of its stage names only, with no models: one row per step, a parallel's stages side by side in one row, and an arrow into each stage from the stages before it. For example, rows holding Build; Review and Test; Fix, with the arrows "build -> review", "build -> test", "review -> fix" and "test -> fix". - Schedules: when the human asks for work to run on a timer (every Monday, nightly), you manage it with list_schedules, save_schedule and delete_schedule; the human sees and edits the same schedules in the Project tab. On each run, byre creates a task from a schedule's prompt or workflow as create_task would, created by byre, and you get its created signal; a run that fails to start reaches you as a signal. - Whenever you name a task in a message, link it as a Markdown link to #/task/, for example [Fix login redirect](#/task/tskDa7e4K7305e). byre draws it in that task's color, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task//. When you point the human at a question to answer or a finished turn, link to #/task//transcript. - Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line
Title, the Markdown content, then a line
. diff --git a/internal/agents/manager.go b/internal/agents/manager.go index 42e4fa6..e4fb846 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -847,7 +847,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()}, 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()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...) lead := true if sess.Role == api.Implementer { g += longTitleGuidance(task) @@ -918,7 +918,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api return nil, err } l.notes = "\n\n" + notes - tools = append(m.leadTools(l), m.readKnowledgeTool()) + tools = append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool()) env = nil } env = append(git.IdentityEnv(l.gitName, l.gitEmail), env...) diff --git a/internal/agents/review.go b/internal/agents/review.go index a69e6dd..d316705 100644 --- a/internal/agents/review.go +++ b/internal/agents/review.go @@ -30,7 +30,7 @@ const reviewGuidance = `You are a reviewer running inside byre. Another agent is - Start the review with ` + "`change`" + `: what the change does, in a few sentences, for someone who hasn't read it. - Each finding says what is wrong, where (path and line), why it matters and what to do about it. Leave out praise and nits. No findings is a fine result. - With no findings, the summary is exactly "No significant findings." and nothing else. With findings, the summary is a bulleted Markdown list, one line per finding, and nothing else. -- Show rather than quote. Give a finding's file and lines and byre shows that part of the diff, so don't paste code. When a picture would explain something better than prose, such as a data flow, call graph, state machine or schema the change alters, draw it in a ` + "```mermaid" + ` block. When the change alters something you can see, you can run it (on any free port on 127.0.0.1) and attach screenshots taken with the Playwright tools. Most reviews need neither; leave out a diagram or screenshot that adds nothing to the text.` +- Show rather than quote. Give a finding's file and lines and byre shows that part of the diff, so don't paste code. When a picture would explain something better than prose, such as a data flow, call graph, state machine or schema the change alters, draw it in a ` + "```diagram" + ` block (its format is the diagrams knowledge-base article) and check it with render_diagram. When the change alters something you can see, you can run it (on any free port on 127.0.0.1) and attach screenshots taken with the Playwright tools. Most reviews need neither; leave out a diagram or screenshot that adds nothing to the text.` func reviewSchema() json.RawMessage { images := map[string]any{ diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index fde7513..631a629 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -735,15 +735,15 @@ type AgentSessionVendor string // Attachment defines model for Attachment. type Attachment struct { Caption *string `json:"caption,omitempty"` + + // Diagram diagram: source of a ```diagram block. + Diagram *string `json:"diagram,omitempty"` EndLine *int `json:"end_line,omitempty"` // Image image: data URL. Image *string `json:"image,omitempty"` Kind AttachmentKind `json:"kind"` - // Mermaid diagram: Mermaid source. - Mermaid *string `json:"mermaid,omitempty"` - // Path file: repo-relative path. image: an image file in the task's worktree, absolute or relative to it. Path *string `json:"path,omitempty"` @@ -4764,327 +4764,327 @@ var swaggerSpec = []string{ "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/mac/Sources/Byre/ByreApp.swift b/mac/Sources/Byre/ByreApp.swift index f12ac2a..46eb7a3 100644 --- a/mac/Sources/Byre/ByreApp.swift +++ b/mac/Sources/Byre/ByreApp.swift @@ -52,6 +52,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } @main +@MainActor +enum Main { + static func main() { + let args = CommandLine.arguments + if args.count == 2 && args[1] == "render-diagram" { + exit(renderDiagram()) + } + ByreApp.main() + } +} + struct ByreApp: App { @NSApplicationDelegateAdaptor private var delegate: AppDelegate diff --git a/mac/Sources/Byre/Diagram.swift b/mac/Sources/Byre/Diagram.swift index 4af916d..840f82a 100644 --- a/mac/Sources/Byre/Diagram.swift +++ b/mac/Sources/Byre/Diagram.swift @@ -2,296 +2,10 @@ import AppKit import CoreText import SwiftUI -enum NodeShape { - case box, rounded, diamond, circle -} - -enum EdgeStyle { - case solid, dotted, thick -} - -enum FlowDirection { - case lr, rl, td, bt -} - -struct NodeColors { - var fill: Color? - var stroke: Color? - var text: Color? - - mutating func merge(_ other: NodeColors) { - fill = other.fill ?? fill - stroke = other.stroke ?? stroke - text = other.text ?? text - } -} - -struct FlowNode { - let id: String - var label: String - var shape: NodeShape = .box - var classes: [String] = [] - var style = NodeColors() - var members: [FlowNode] = [] -} - -struct FlowEdge { - let from: String - let to: String - let label: String? - let style: EdgeStyle - let arrow: Bool -} - -struct Flowchart { - var direction: FlowDirection = .td - var order: [String] = [] - var nodes: [String: FlowNode] = [:] - var edges: [FlowEdge] = [] - var classDefs: [String: NodeColors] = [:] - - mutating func node(_ id: String, label: String? = nil, shape: NodeShape? = nil) { - if nodes[id] == nil { - order.append(id) - nodes[id] = FlowNode(id: id, label: id) - } - if let label { - nodes[id]!.label = label - } - if let shape { - nodes[id]!.shape = shape - } - } - - func colors(_ id: String) -> NodeColors { - let n = nodes[id]! - var c = NodeColors() - for cls in n.classes { - if let d = classDefs[cls] { - c.merge(d) - } - } - c.merge(n.style) - return c - } -} - struct DiagramError: Error { let reason: String } -private let header = Re(#"^(flowchart|graph)(?:\s+(LR|RL|TD|TB|BT))?$"#, caseless: true) -private let nodeId = Re(#"^[A-Za-z0-9_]+"#) -private let plainEdge = Re(#"^(-{2,}>|-{3,}|-\.+->|-\.+-|={2,}>|={3,})(?:\|([^|]*)\|)?"#) -private let dashLabel = Re(#"^--\s*([^->].*?)\s*(-{2,}>|-{3,})"#) -private let dotLabel = Re(#"^-\.\s*(.*?)\s*(\.+->|\.+-)"#) -private let thickLabel = Re(#"^==\s*([^=>].*?)\s*(={2,}>|={3,})"#) -private let classDefLine = Re(#"^classDef\s+([A-Za-z0-9_,]+)\s+(.+)$"#) -private let classLine = Re(#"^class\s+([A-Za-z0-9_,\s]+?)\s+([A-Za-z0-9_]+)$"#) -private let styleLine = Re(#"^style\s+([A-Za-z0-9_]+)\s+(.+)$"#) -private let hexColor = Re(#"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"#) - -private let shapes: [(open: String, close: String, shape: NodeShape)] = [ - ("((", "))", .circle), ("([", "])", .rounded), ("[[", "]]", .box), ("[(", ")]", .rounded), ("{{", "}}", .diamond), - ("[", "]", .box), ("(", ")", .rounded), ("{", "}", .diamond), -] - -func parseFlowchart(_ src: String) throws(DiagramError) -> Flowchart { - var lines: [String] = [] - for raw in src.components(separatedBy: "\n") { - let line = trimmed(raw) - if line.isEmpty || line.hasPrefix("%%") { - continue - } - for part in line.split(separator: ";") { - let s = trimmed(String(part)) - if !s.isEmpty { - lines.append(s) - } - } - } - guard let first = lines.first else { - throw DiagramError(reason: "empty diagram") - } - guard let h = header.exec(first) else { - let kind = first.split(separator: " ").first.map(String.init) ?? first - throw DiagramError(reason: "\(kind) diagrams aren't drawn here, only flowcharts") - } - var chart = Flowchart() - switch h[2]?.uppercased() { - case "LR": chart.direction = .lr - case "RL": chart.direction = .rl - case "BT": chart.direction = .bt - default: chart.direction = .td - } - for line in lines.dropFirst() { - if line.hasPrefix("linkStyle ") { - continue - } - if let m = classDefLine.exec(line) { - let colors = parseStyle(m[2]!) - for name in m[1]!.split(separator: ",") { - chart.classDefs[String(name)] = colors - } - continue - } - if let m = classLine.exec(line) { - for id in m[1]!.split(whereSeparator: { $0 == "," || $0 == " " }) { - chart.node(String(id)) - chart.nodes[String(id)]!.classes.append(m[2]!) - } - continue - } - if let m = styleLine.exec(line) { - chart.node(m[1]!) - chart.nodes[m[1]!]!.style.merge(parseStyle(m[2]!)) - continue - } - try parseChain(line, &chart) - } - return chart -} - -private func parseStyle(_ text: String) -> NodeColors { - var c = NodeColors() - for prop in text.split(separator: ",") { - let kv = prop.split(separator: ":", maxSplits: 1).map { trimmed(String($0)) } - guard kv.count == 2, let color = parseColor(kv[1]) else { - continue - } - switch kv[0] { - case "fill": c.fill = color - case "stroke": c.stroke = color - case "color": c.text = color - default: break - } - } - return c -} - -private func parseColor(_ text: String) -> Color? { - guard let m = hexColor.exec(text) else { - return nil - } - var hex = m[1]! - if hex.count == 3 { - hex = hex.map { "\($0)\($0)" }.joined() - } - let v = UInt32(hex, radix: 16)! - return Color(.sRGB, red: Double(v >> 16 & 0xff) / 255, green: Double(v >> 8 & 0xff) / 255, blue: Double(v & 0xff) / 255) -} - -private func unquote(_ s: String) -> String { - var t = trimmed(s) - if t.count >= 2 && t.hasPrefix("\"") && t.hasSuffix("\"") { - t = String(t.dropFirst().dropLast()) - } - return t.replacingOccurrences(of: "
", with: "\n").replacingOccurrences(of: "
", with: "\n").replacingOccurrences(of: "
", with: "\n") -} - -private func parseChain(_ line: String, _ chart: inout Flowchart) throws(DiagramError) { - var rest = Substring(line) - let fail = DiagramError(reason: "can't parse \"\(line)\"") - func skipSpaces() { - rest = rest.drop { $0 == " " || $0 == "\t" } - } - func node() throws(DiagramError) -> String { - skipSpaces() - guard let m = nodeId.match(String(rest)) else { - throw fail - } - let id = m.groups[0]! - rest = rest.dropFirst(id.count) - var label: String? - var shape: NodeShape? - for s in shapes where rest.hasPrefix(s.open) { - let inner = rest.dropFirst(s.open.count) - var after: Substring? - if inner.first == "\"", let q = inner.dropFirst().firstIndex(of: "\"") { - let tail = inner[inner.index(after: q)...] - if tail.hasPrefix(s.close) { - label = unquote(String(inner[...q])) - after = tail.dropFirst(s.close.count) - } - } - if after == nil, let r = inner.range(of: s.close) { - label = unquote(String(inner[.. (label: String?, style: EdgeStyle, arrow: Bool) { - skipSpaces() - let s = String(rest) - if let m = plainEdge.match(s) { - rest = rest.dropFirst(s.distance(from: s.startIndex, to: m.range.upperBound)) - let arrow = m.groups[1]! - return (m.groups[2].map(unquote), arrow.hasPrefix("=") ? .thick : arrow.contains(".") ? .dotted : .solid, arrow.hasSuffix(">")) - } - for (re, style) in [(dashLabel, EdgeStyle.solid), (dotLabel, .dotted), (thickLabel, .thick)] { - if let m = re.match(s) { - rest = rest.dropFirst(s.distance(from: s.startIndex, to: m.range.upperBound)) - return (unquote(m.groups[1]!), style, m.groups[2]!.hasSuffix(">")) - } - } - throw fail - } - var from = try node() - while true { - skipSpaces() - if rest.isEmpty { - return - } - let e = try edge() - let to = try node() - chart.edges.append(FlowEdge(from: from, to: to, label: e.label?.isEmpty == true ? nil : e.label, style: e.style, arrow: e.arrow)) - from = to - } -} - -struct PlacedNode { - let node: FlowNode - let center: CGPoint - let size: CGSize - let colors: NodeColors - var members: [PlacedNode] = [] -} - -private let groupPad: CGFloat = 10 -private let memberGap: CGFloat = 10 - -struct DiagramLayout { - let nodes: [String: PlacedNode] - let edges: [FlowEdge] - let size: CGSize -} - -@MainActor -private var labelFont: CTFont { - Fonts.ns(13) as CTFont -} - -@MainActor -private var edgeFont: CTFont { - Fonts.ns(12) as CTFont -} - func textSize(_ text: String, _ font: CTFont) -> CGSize { let lines = text.components(separatedBy: "\n") var width: CGFloat = 0 @@ -303,314 +17,6 @@ func textSize(_ text: String, _ font: CTFont) -> CGSize { return CGSize(width: ceil(width), height: ceil(lineHeight * CGFloat(lines.count))) } -@MainActor -func layoutFlowchart(_ chart: Flowchart) -> DiagramLayout { - let ids = chart.order - var succ: [String: [String]] = [:] - for e in chart.edges where e.from != e.to { - succ[e.from, default: []].append(e.to) - } - - var state: [String: Int] = [:] - var dag: [String: [String]] = [:] - func visit(_ n: String) { - state[n] = 1 - for m in succ[n] ?? [] { - if state[m] == 1 { - dag[m, default: []].append(n) - continue - } - dag[n, default: []].append(m) - if state[m] == nil { - visit(m) - } - } - state[n] = 2 - } - for n in ids where state[n] == nil { - visit(n) - } - - var indegree: [String: Int] = [:] - for (_, ms) in dag { - for m in ms { - indegree[m, default: 0] += 1 - } - } - var layer: [String: Int] = [:] - var queue = ids.filter { (indegree[$0] ?? 0) == 0 } - for n in queue { - layer[n] = 0 - } - var qi = 0 - while qi < queue.count { - let n = queue[qi] - qi += 1 - for m in dag[n] ?? [] { - layer[m] = max(layer[m] ?? 0, layer[n]! + 1) - indegree[m]! -= 1 - if indegree[m] == 0 { - queue.append(m) - } - } - } - let depth = (layer.values.max() ?? 0) + 1 - var layers = Array(repeating: [String](), count: depth) - for n in ids { - layers[layer[n] ?? 0].append(n) - } - - var preds: [String: [String]] = [:] - for (n, ms) in dag { - for m in ms { - preds[m, default: []].append(n) - } - } - func position(_ l: Int) -> [String: Double] { - Dictionary(uniqueKeysWithValues: layers[l].enumerated().map { ($0.element, Double($0.offset)) }) - } - func reorder(_ l: Int, _ ref: Int, _ links: [String: [String]]) { - let pos = position(ref) - let current = position(l) - layers[l].sort { a, b in - func bary(_ n: String) -> Double { - let ps = (links[n] ?? []).compactMap { pos[$0] } - return ps.isEmpty ? current[n]! : ps.reduce(0, +) / Double(ps.count) - } - let (x, y) = (bary(a), bary(b)) - return x == y ? current[a]! < current[b]! : x < y - } - } - for _ in 0..<4 { - for l in layers.indices.dropFirst() { - reorder(l, l - 1, preds) - } - for l in layers.indices.dropLast().reversed() { - reorder(l, l + 1, dag) - } - } - - let horizontal = chart.direction == .lr || chart.direction == .rl - let rankGap: CGFloat = horizontal ? 70 : 56 - let nodeGap: CGFloat = horizontal ? 24 : 32 - func along(_ s: CGSize) -> CGFloat { - horizontal ? s.width : s.height - } - func across(_ s: CGSize) -> CGFloat { - horizontal ? s.height : s.width - } - - func nodeSize(_ node: FlowNode) -> CGSize { - let t = textSize(node.label, labelFont) - if !node.members.isEmpty { - let ms = node.members.map(nodeSize) - let band = groupPad + t.height + groupPad - let gaps = memberGap * CGFloat(ms.count - 1) - let inner = horizontal - ? CGSize(width: ms.map(\.width).max()!, height: ms.map(\.height).reduce(0, +) + gaps) - : CGSize(width: ms.map(\.width).reduce(0, +) + gaps, height: ms.map(\.height).max()!) - return CGSize(width: max(t.width + 30, inner.width + groupPad * 2), height: band + inner.height + groupPad) - } - switch node.shape { - case .box, .rounded: - return CGSize(width: t.width + 30, height: t.height + 18) - case .diamond: - return CGSize(width: (t.width * 0.75 + 14) * 2, height: (t.height * 1.5 + 8) * 2) - case .circle: - let d = (t.width * t.width + t.height * t.height).squareRoot() + 16 - return CGSize(width: d, height: d) - } - } - func placeMembers(_ group: FlowNode, _ center: CGPoint, _ size: CGSize) -> [PlacedNode] { - let ms = group.members.map(nodeSize) - let top = center.y - size.height / 2 + groupPad + textSize(group.label, labelFont).height + groupPad - let gaps = memberGap * CGFloat(ms.count - 1) - var offset = horizontal ? top : center.x - (ms.map(\.width).reduce(0, +) + gaps) / 2 - let rowHeight = ms.map(\.height).max() ?? 0 - return zip(group.members, ms).map { m, s in - let c = horizontal - ? CGPoint(x: center.x, y: offset + s.height / 2) - : CGPoint(x: offset + s.width / 2, y: top + rowHeight / 2) - offset += (horizontal ? s.height : s.width) + memberGap - return PlacedNode(node: m, center: c, size: s, colors: m.style) - } - } - - var sizes: [String: CGSize] = [:] - for n in ids { - sizes[n] = nodeSize(chart.nodes[n]!) - } - let spans = layers.map { l in l.map { across(sizes[$0]!) }.reduce(0, +) + nodeGap * CGFloat(max(0, l.count - 1)) } - let widest = spans.max() ?? 0 - var centers: [String: CGPoint] = [:] - var rank: CGFloat = 0 - for (l, nodes) in layers.enumerated() { - let thick = nodes.map { along(sizes[$0]!) }.max() ?? 0 - var offset = (widest - spans[l]) / 2 - for n in nodes { - let a = across(sizes[n]!) - let c1 = rank + thick / 2 - let c2 = offset + a / 2 - centers[n] = horizontal ? CGPoint(x: c1, y: c2) : CGPoint(x: c2, y: c1) - offset += a + nodeGap - } - rank += thick + rankGap - } - let length = max(0, rank - rankGap) - let margin: CGFloat = 8 - let total = horizontal ? CGSize(width: length, height: widest) : CGSize(width: widest, height: length) - var placed: [String: PlacedNode] = [:] - for n in ids { - var c = centers[n]! - if chart.direction == .rl { - c.x = total.width - c.x - } - if chart.direction == .bt { - c.y = total.height - c.y - } - let center = CGPoint(x: c.x + margin, y: c.y + margin) - let node = chart.nodes[n]! - placed[n] = PlacedNode(node: node, center: center, size: sizes[n]!, colors: chart.colors(n), members: placeMembers(node, center, sizes[n]!)) - } - return DiagramLayout(nodes: placed, edges: chart.edges, size: CGSize(width: total.width + margin * 2, height: total.height + margin * 2)) -} - -private func boundary(_ n: PlacedNode, toward p: CGPoint) -> CGPoint { - let dx = p.x - n.center.x - let dy = p.y - n.center.y - if dx == 0 && dy == 0 { - return n.center - } - let a = n.size.width / 2 - let b = n.size.height / 2 - let t: CGFloat - switch n.node.shape { - case .box, .rounded: - t = min(dx == 0 ? .infinity : a / abs(dx), dy == 0 ? .infinity : b / abs(dy)) - case .diamond: - t = 1 / (abs(dx) / a + abs(dy) / b) - case .circle: - t = a / (dx * dx + dy * dy).squareRoot() - } - return CGPoint(x: n.center.x + dx * t, y: n.center.y + dy * t) -} - -private func shapePath(_ n: PlacedNode) -> Path { - let r = CGRect(x: n.center.x - n.size.width / 2, y: n.center.y - n.size.height / 2, width: n.size.width, height: n.size.height) - switch n.node.shape { - case .box: - return Path(roundedRect: r, cornerRadius: 3) - case .rounded: - return Path(roundedRect: r, cornerRadius: min(r.height / 2, 14)) - case .circle: - return Path(ellipseIn: r) - case .diamond: - var p = Path() - p.move(to: CGPoint(x: r.midX, y: r.minY)) - p.addLine(to: CGPoint(x: r.maxX, y: r.midY)) - p.addLine(to: CGPoint(x: r.midX, y: r.maxY)) - p.addLine(to: CGPoint(x: r.minX, y: r.midY)) - p.closeSubpath() - return p - } -} - -struct FlowchartCanvas: View { - let layout: DiagramLayout - - var body: some View { - FitWidth(natural: layout.size) { - Canvas { context, size in - let scale = layout.size.width > 0 ? size.width / layout.size.width : 1 - context.scaleBy(x: scale, y: scale) - draw(&context) - } - } - } - - private func draw(_ context: inout GraphicsContext) { - for e in layout.edges { - guard let a = layout.nodes[e.from], let b = layout.nodes[e.to] else { - continue - } - let stroke = StrokeStyle(lineWidth: e.style == .thick ? 3 : 1.5, lineCap: .round, dash: e.style == .dotted ? [3, 4] : []) - var mid: CGPoint - if e.from == e.to { - let top = CGPoint(x: a.center.x, y: a.center.y - a.size.height / 2) - var loop = Path() - loop.move(to: CGPoint(x: top.x - 8, y: top.y)) - loop.addCurve(to: CGPoint(x: top.x + 8, y: top.y), control1: CGPoint(x: top.x - 24, y: top.y - 30), control2: CGPoint(x: top.x + 24, y: top.y - 30)) - context.stroke(loop, with: .color(Palette.muted), style: stroke) - if e.arrow { - arrowHead(&context, from: CGPoint(x: top.x + 20, y: top.y - 20), to: CGPoint(x: top.x + 8, y: top.y)) - } - mid = CGPoint(x: top.x, y: top.y - 26) - } else { - let start = boundary(a, toward: b.center) - let end = boundary(b, toward: a.center) - var line = Path() - line.move(to: start) - line.addLine(to: end) - context.stroke(line, with: .color(Palette.muted), style: stroke) - if e.arrow { - arrowHead(&context, from: start, to: end) - } - mid = CGPoint(x: (start.x + end.x) / 2, y: (start.y + end.y) / 2) - } - if let label = e.label { - let size = textSize(label, edgeFont) - let box = CGRect(x: mid.x - size.width / 2 - 4, y: mid.y - size.height / 2 - 2, width: size.width + 8, height: size.height + 4) - context.fill(Path(roundedRect: box, cornerRadius: 3), with: .color(Palette.bg)) - context.draw(Text(label).font(Font(edgeFont)).foregroundColor(Palette.text), at: mid, anchor: .center) - } - } - for (_, n) in layout.nodes { - drawNode(&context, n) - for m in n.members { - drawNode(&context, m) - } - } - } - - private func drawNode(_ context: inout GraphicsContext, _ n: PlacedNode) { - let path = shapePath(n) - context.fill(path, with: .color(n.colors.fill ?? Palette.surface)) - context.stroke(path, with: .color(n.colors.stroke ?? Palette.border), lineWidth: 1) - let lines = n.node.label.components(separatedBy: "\n") - let lineHeight = textSize("X", labelFont).height - let middle = n.members.isEmpty ? n.center.y : n.center.y - n.size.height / 2 + groupPad + lineHeight * CGFloat(lines.count) / 2 - for (i, line) in lines.enumerated() { - let y = middle + (CGFloat(i) - CGFloat(lines.count - 1) / 2) * lineHeight - context.draw(Text(line).font(Font(labelFont)).foregroundColor(n.colors.text ?? Palette.text), at: CGPoint(x: n.center.x, y: y), anchor: .center) - } - } - - private func arrowHead(_ context: inout GraphicsContext, from: CGPoint, to: CGPoint) { - let angle = atan2(to.y - from.y, to.x - from.x) - let size: CGFloat = 9 - var head = Path() - head.move(to: to) - head.addLine(to: CGPoint(x: to.x - size * cos(angle - .pi / 7), y: to.y - size * sin(angle - .pi / 7))) - head.addLine(to: CGPoint(x: to.x - size * cos(angle + .pi / 7), y: to.y - size * sin(angle + .pi / 7))) - head.closeSubpath() - context.fill(head, with: .color(Palette.muted)) - } -} - -struct DiagramView: View { - let source: String - - var body: some View { - switch Result(catching: { () throws(DiagramError) -> Flowchart in try parseFlowchart(source) }) { - case .success(let chart): - FlowchartCanvas(layout: layoutFlowchart(chart)) - .frame(maxWidth: .infinity, alignment: .leading) - case .failure(let error): - DiagramFailure(source: source, reason: error.reason) - } - } -} - struct DiagramFailure: View { let source: String let reason: String diff --git a/mac/Sources/Byre/Fonts.swift b/mac/Sources/Byre/Fonts.swift index 6f859e5..5704afa 100644 --- a/mac/Sources/Byre/Fonts.swift +++ b/mac/Sources/Byre/Fonts.swift @@ -9,12 +9,15 @@ import SwiftUI final class FontSize { static let shared = FontSize() static let range = 9...20 + static var stored = true private(set) var body = PrefsRecord.defaultFontSize private(set) var code = PrefsRecord.defaultFontSize private init() { - load() + if FontSize.stored { + load() + } } func load() { diff --git a/mac/Sources/Byre/Markdown.swift b/mac/Sources/Byre/Markdown.swift index 18c763a..267f2d5 100644 --- a/mac/Sources/Byre/Markdown.swift +++ b/mac/Sources/Byre/Markdown.swift @@ -43,7 +43,6 @@ indirect enum MDBlock: Sendable { case diff([String]) case details(summary: [MDInline], body: [MDBlock]) case diagram(String) - case boxDiagram(String) case table(head: [[MDInline]], rows: [[[MDInline]]]) } @@ -123,12 +122,8 @@ private func blocks(_ lines: [String], _ choices: Bool) -> [MDBlock] { } i += 1 let lang = f[2]!.lowercased() - if lang == "mermaid" { - out.append(.diagram(body.joined(separator: "\n"))) - continue - } if lang == "diagram" { - out.append(.boxDiagram(body.joined(separator: "\n"))) + out.append(.diagram(body.joined(separator: "\n"))) continue } if lang == "diff" || lang == "patch" { diff --git a/mac/Sources/Byre/MarkdownView.swift b/mac/Sources/Byre/MarkdownView.swift index ab22de0..9574efe 100644 --- a/mac/Sources/Byre/MarkdownView.swift +++ b/mac/Sources/Byre/MarkdownView.swift @@ -814,8 +814,6 @@ private struct BlockView: View { case .details(let summary, let body): DetailsBlock(summary: summary, blocks: body, ctx: ctx) case .diagram(let source): - DiagramView(source: source) - case .boxDiagram(let source): BoxDiagramView(source: source) case .table(let head, let rows): TableBlock(head: head, rows: rows, ctx: ctx) diff --git a/mac/Sources/Byre/Questions.swift b/mac/Sources/Byre/Questions.swift index cf07c65..fa4d608 100644 --- a/mac/Sources/Byre/Questions.swift +++ b/mac/Sources/Byre/Questions.swift @@ -542,7 +542,7 @@ private struct Attachments: View { InlineView(inlines: [.link(href: src, [.text(caption.isEmpty ? src : caption)])], ctx: MDContext(taskId: taskId)) } case .diagram: - DiagramView(source: a.mermaid ?? "") + BoxDiagramView(source: a.diagram ?? "") case .task: let id = a.taskId ?? "" Button("task \(id)") { diff --git a/mac/Sources/Byre/Render.swift b/mac/Sources/Byre/Render.swift new file mode 100644 index 0000000..3c4252c --- /dev/null +++ b/mac/Sources/Byre/Render.swift @@ -0,0 +1,33 @@ +import AppKit +import SwiftUI + +@MainActor +func renderDiagram() -> Int32 { + FontSize.stored = false + let source = String(decoding: FileHandle.standardInput.readDataToEndOfFile(), as: UTF8.self) + let layout: BoxLayout + switch cachedBoxLayout(source) { + case .success(let l): + layout = l + case .failure(let error): + FileHandle.standardError.write(Data(error.reason.utf8)) + return 1 + } + let content = BoxCanvas(layout: layout) + .frame(width: layout.size.width, height: layout.size.height) + .padding(16) + .background(Palette.bg) + .environment(\.colorScheme, .light) + let renderer = ImageRenderer(content: content) + renderer.scale = 2 + var png: Data? + NSAppearance(named: .aqua)!.performAsCurrentDrawingAppearance { + png = renderer.cgImage.flatMap { NSBitmapImageRep(cgImage: $0).representation(using: .png, properties: [:]) } + } + guard let png else { + FileHandle.standardError.write(Data("diagram didn't render".utf8)) + return 1 + } + FileHandle.standardOutput.write(png) + return 0 +} diff --git a/mac/Sources/Byre/Workflow.swift b/mac/Sources/Byre/Workflow.swift index c7de36c..d96ead4 100644 --- a/mac/Sources/Byre/Workflow.swift +++ b/mac/Sources/Byre/Workflow.swift @@ -5,39 +5,26 @@ typealias WorkflowRun = Components.Schemas.WorkflowRun typealias RunStage = Components.Schemas.WorkflowRunStage typealias StageAttempt = Components.Schemas.StageAttempt -private let stateColors: [RunStage.StatePayload: NodeColors] = [ - .waiting: NodeColors(fill: Palette.surface, stroke: Palette.border, text: Palette.muted), - .running: NodeColors(fill: Palette.accentTint, stroke: Palette.accent, text: Palette.text), - .idle: NodeColors(fill: Palette.surface, stroke: Palette.border, text: Palette.muted), - .finished: NodeColors(fill: Palette.okTint, stroke: Palette.ok, text: Palette.text), - .merged: NodeColors(fill: Palette.okTint, stroke: Palette.ok, text: Palette.text), - .cleanedUp: NodeColors(fill: Palette.okTint, stroke: Palette.ok, text: Palette.text), - .discarded: NodeColors(fill: Palette.dangerTint, stroke: Palette.danger, text: Palette.text), +private let stateTints: [RunStage.StatePayload: BoxTint] = [ + .waiting: .plain, + .running: .blue, + .idle: .plain, + .finished: .green, + .merged: .green, + .cleanedUp: .green, + .discarded: .red, ] private func stageSteps(_ steps: [WorkflowStep]) -> [WorkflowStep] { steps.flatMap { s in s.order.map(stageSteps) ?? s.parallel.map(stageSteps) ?? [s] } } -private func stageGraph(_ run: WorkflowRun) -> Flowchart { - var chart = Flowchart(direction: .td) +private func runDiagram(_ run: WorkflowRun) -> String { let steps = Dictionary(stageSteps(run.definition.steps).map { ($0.stage ?? "", $0) }, uniquingKeysWith: { a, _ in a }) - for (i, s) in run.stages.enumerated() { - let id = "s\(i)" - let colors = stateColors[s.state]! - chart.node(id, label: s.name, shape: .box) - chart.nodes[id]!.style = colors - chart.nodes[id]!.members = teamNodes(id, steps[s.name]?.agents, stroke: colors.stroke) - } - let ids = Dictionary(run.stages.enumerated().map { ($0.element.name, "s\($0.offset)") }, uniquingKeysWith: { a, _ in a }) - for s in run.stages { - for a in s.after { - if let from = ids[a], let to = ids[s.name] { - chart.edges.append(FlowEdge(from: from, to: to, label: nil, style: .solid, arrow: true)) - } - } - } - return chart + let index = Dictionary(run.stages.enumerated().map { ($0.element.name, $0.offset) }, uniquingKeysWith: { a, _ in a }) + return stageDiagram(run.stages.map { s in + GraphStage(name: s.name, tint: stateTints[s.state]!, agents: steps[s.name]?.agents, after: s.after.compactMap { index[$0] }) + }) } private func attemptLabel(_ a: StageAttempt) -> String { @@ -103,8 +90,7 @@ struct WorkflowView: View { .modifier(HeaderBar()) ScrollView { VStack(alignment: .leading, spacing: 12) { - FlowchartCanvas(layout: layoutFlowchart(stageGraph(run))) - .frame(maxWidth: .infinity, alignment: .leading) + BoxDiagramView(source: runDiagram(run)) if !error.isEmpty { EmptyText(text: error, error: true) } else { diff --git a/mac/Sources/Byre/WorkflowEditor.swift b/mac/Sources/Byre/WorkflowEditor.swift index 26fcdc4..7770184 100644 --- a/mac/Sources/Byre/WorkflowEditor.swift +++ b/mac/Sources/Byre/WorkflowEditor.swift @@ -92,33 +92,62 @@ private func newStage(_ root: StepNode, _ model: String) -> StepNode { return s } -func teamNodes(_ id: String, _ agents: [TeamMember]?, stroke: Color?) -> [FlowNode] { - (agents ?? []).enumerated().map { i, a in - FlowNode(id: "\(id)m\(i)", label: "\(a.name.isEmpty ? " " : a.name)\n\(a.model)", style: NodeColors(fill: Palette.bg, stroke: stroke, text: Palette.text)) +struct GraphStage { + let name: String + var tint = BoxTint.plain + let agents: [TeamMember]? + let after: [Int] +} + +private func quoted(_ s: String) -> String { + "\"" + (s.isEmpty ? " " : s).replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\"" +} + +func stageDiagram(_ stages: [GraphStage]) -> String { + var depth: [Int: Int] = [:] + func level(_ i: Int) -> Int { + if let d = depth[i] { + return d + } + let d = stages[i].after.map { level($0) + 1 }.max() ?? 0 + depth[i] = d + return d } + let levels = stages.indices.map(level) + var lines: [String] = [] + for row in 0...(levels.max() ?? 0) { + lines.append("row") + for (i, s) in stages.enumerated() where levels[i] == row { + lines.append(" s\(i) \(quoted(s.name)) \(s.tint.rawValue)") + for (j, a) in (s.agents ?? []).enumerated() { + lines.append(" s\(i)m\(j) \(quoted(a.name)) \(quoted(a.model))") + } + } + } + for (i, s) in stages.enumerated() { + for b in s.after { + lines.append("s\(b) -> s\(i)") + } + } + return lines.joined(separator: "\n") } @MainActor -private func stepGraph(_ root: StepNode) -> Flowchart { - var chart = Flowchart(direction: .td) - func walk(_ node: StepNode, _ before: [String]) -> [String] { +private func stepDiagram(_ root: StepNode) -> String { + var stages: [GraphStage] = [] + func walk(_ node: StepNode, _ before: [Int]) -> [Int] { switch node.kind { case .order: return node.children.reduce(before) { walk($1, $0) } case .parallel: return node.children.flatMap { walk($0, before) } case .stage: - let id = "n\(chart.order.count)" - chart.node(id, label: node.name.isEmpty ? " " : node.name, shape: .box) - chart.nodes[id]!.members = teamNodes(id, node.agents, stroke: nil) - for b in before { - chart.edges.append(FlowEdge(from: b, to: id, label: nil, style: .solid, arrow: true)) - } - return [id] + stages.append(GraphStage(name: node.name, agents: node.agents, after: before)) + return [stages.count - 1] } } _ = walk(root, []) - return chart + return stageDiagram(stages) } private struct StepLine: Identifiable { @@ -178,8 +207,7 @@ struct WorkflowEditor: View { .modifier(FieldStyle()) .padding(.top, 8) if !root.stageNames().isEmpty { - FlowchartCanvas(layout: layoutFlowchart(stepGraph(root))) - .frame(maxWidth: .infinity, alignment: .leading) + BoxDiagramView(source: stepDiagram(root)) .padding(.top, 12) } if !failure.isEmpty {