feat(diagram): named styles the model defines once and uses by name

The model may now write <mxStyle name="step" value="..."/> before its cells
and refer to the name inside a cell's style, the way draw.io resolves its
own named styles. style-classes.ts expands the names after validation (so
repaired cells get them too) and adds draw.io's defaults html=1 and
whiteSpace=wrap where they belong, so the prompts can tell the model never
to write them. Wired into display_diagram, append_diagram, create_new_diagram,
add_page, edit_diagram (defaults only) and the streaming preview.

Prompts (system prompt, tool description, MCP drawing guide) teach the
notation with a shared example, drop the draw.io defaults from every
example and tell the model how labels, line breaks and reserved names work.

Measured on the five start-screen examples, same model and prompt, old vs
new prompt: gpt-6-luna 19% fewer output tokens, Opus 5.5 19% fewer; flow
and architecture diagrams 15% to 46%, creative drawings about equal.
This commit is contained in:
dayuan.jiang
2026-10-10 16:28:42 +09:00
parent 4cba863fd9
commit 98f2a4fda6
13 changed files with 995 additions and 51 deletions
+22 -3
View File
@@ -12,6 +12,12 @@ import {
} from "@/lib/utils"
import { applyDiagramOperations } from "@/packages/mcp-server/src/diagram-operations.ts"
import { BLANK_MXFILE } from "@/packages/mcp-server/src/pages.ts"
import {
addDefaultStyles,
applyStyleClasses,
edgeIdsOf,
readStyleClasses,
} from "@/packages/mcp-server/src/style-classes.ts"
// Helper to extract complete operations from streaming input
function getCompleteOperations(
@@ -71,9 +77,14 @@ export function useDiagramStreaming({
// so far. The tool handler validates and loads the final diagram.
const handleDisplayChart = useCallback(
(xml: string) => {
const completeCells = extractCompleteMxCells(xml || "")
// Named styles come before the cells, so the ones written so
// far are known by the time their cells stream in
const { classes, xml: cellsXml } = readStyleClasses(xml || "")
const completeCells = extractCompleteMxCells(cellsXml)
if (!completeCells) return
const convertedXml = convertToLegalXml(completeCells)
const convertedXml = addDefaultStyles(
applyStyleClasses(convertToLegalXml(completeCells), classes),
)
if (convertedXml === previousXML.current) return
// Skip this update while the cells written so far don't parse
@@ -202,9 +213,17 @@ export function useDiagramStreaming({
if (lastProcessedXmlRef.current.get(opsKey) === ops) continue
lastProcessedXmlRef.current.set(opsKey, ops)
try {
const edges = edgeIdsOf(originalXml)
const { result } = applyDiagramOperations(
originalXml,
completeOps,
completeOps.map((op) =>
op.new_xml
? {
...op,
new_xml: addDefaultStyles(op.new_xml, edges),
}
: op,
),
)
if (lastProcessedXmlRef.current.get(resultKey) === result) {
continue