The server asks the preview tab for the selection the way it asks for an export (a random request id in GET /api/state, the answer in a POST with that id, 10 s to answer). The shell answers through the editor bridge with each cell's id, label, an edge's ends, a shape's geometry and the page on screen; without a same-origin editor it says so, and the tool names the external draw.io. The classic page cannot answer, so the tool says that at once. screenshot_diagram's description now says a page selector renders that page without changing the page on screen (PNG exports by pageId already did).
In a session of its own: three writes give three cards; undoing the newest
restores the one before on the server too, redo brings it back, a hand edit
turns the undo into a restore, and restoring the first version keeps that
edit as a version of its own, which restore_version also finds.
The shell's VersionsSource (shell/node-versions-source.ts) reads GET
/api/history through the sync and shows one version per distinct content
(a restore's copy folds into the entry it copies; the blank page is none),
numbered as they first appeared, with what changed since the one before.
The entry the server says is on the canvas marks the version; undo and
redo of the newest version restore the one before it, or it again, through
POST /api/restore, so hand edits stay as versions of their own. The cards
sit in a panel beside the canvas (toggle in the header); the strip and
Compare come from the shared canvas components. The sync tells listeners
once per server version, and when History changed without one, so the
list is read again only then.
Each History entry records who made it (a user edit, a recovering tab's
copy, a restored copy; none for the AI's writes), when, and its page
count. GET /api/history now also returns each entry's XML, the first entry
with the same content (a restore adds a copy of an older one), the state
the ids belong to, and the entry whose content the canvas shows, by the
rule restore_version uses. The shell's version cards read these; the
classic page keeps using index, id and svg.
A write taken in place that leaves the canvas as it is gets no autosave from draw.io, so the marker for its copy stayed armed and ate the user's next paper-size edit: arm it only when the write changed the document. The 15 s export timeout now retires the export's sequence number, so a result POST that answers late no longer restores the document a second time.
An autosave that arrives after loadDiagram sent a full load, and before draw.io reports it, is of the canvas being replaced (draw.io reports each load, in order). Until now it overwrote chartXMLRef: in the MCP shell, a late autosave of a one-page projection replaced the cached document, and a remount of an external draw.io then put the projection on the canvas as the whole document.
The e2e job's first shard builds the MCP package, packs it, installs the
tarball in an empty directory and runs packages/mcp-server/tests/e2e
against the installed dist/index.js (MCP_SERVER_ENTRY), so a shell that
builds but fails in the browser, or a tarball missing a runtime
dependency, fails the PR. Its traces go with the Playwright report. The
publish workflow also triggers on the web app code the shell is bundled
from (components, contexts, hooks, lib, stores, globals.css) and the
root lockfile.
tsc -p shell checked nothing: the inherited exclude dropped every shell
file; the shell's tsconfig now excludes node_modules only. Tailwind scans
app-toaster.tsx too, so the sync notices look like the web app's toasts.
The bundle keeps its dependencies' license comments (esbuild appends them
at the end). postcss, which build-shell.mjs loads from the root, is a
declared devDependency.
The stalled-tab note names the page start_session opened (the shell
with DRAWIO_PREVIEW_UI=shell) instead of the classic page. With the
shell not built, start_session opens the classic page and the server
says so at startup, as it does for a missing dist/drawio. BROWSER=none
skips the system browser, as Vite and CRA dev servers do: the e2e tests
set it. Without a fixed DRAWIO_UI, the shell's draw.io URL carries
themes=1 so Extras > Theme is offered, as on the classic page.
A recovery loads the server's diagram in full and waits for draw.io's
load report, as the classic page does: whether the canvas takes a write
in place is decided inside DiagramProvider, and an autosave of the
canvas being replaced went to the server as an edit when the decision
was wrong. A paper size change alone is pushed again: autosaves are
compared as documents only for draw.io's own copy of a committed write.
A projection stays on screen until draw.io reports the restore load
(5 s at most), the whole export ends in 15 s even when its result POST
hangs, and an export in flight is dropped when draw.io starts over. A
refused poll shows the tab offline. With several pages and the page on
screen unknown, a write loads in full. hasLoadOnlySettings and the
highlight of an AI change are shared with the editor bridge.
The shell connects, shows what create_new_diagram draws, marks an
edit_diagram change and takes it back with one Ctrl+Z (which get_diagram
then reflects), pushes a shape inserted in the editor, serves
screenshot_diagram, and shows another page only for its SVG export before
the user's page comes back.
The recovery, thumbnail, size limit and History cases of
tests/unit/mcp-preview-recovery.test.ts, driven against mcp-sync-core.ts
with a stubbed server and canvas, plus the stale 409 recovery, an export
request answered once, a projection that ignores edits and restores the
document, the thumbnail of a write loaded before draw.io was ready, writes
taken in place as commits (draw.io's own copy is not pushed), and the
commit-or-load decision table of decideLoad.
shell/mcp-sync-core.ts ports preview.js's protocol without the page or
React: polling GET /api/state every 2 s with the state id and version,
pushes of the user's edits with their base version and state, 409 and 413
handling, recovery of a recreated session (the tab's copy goes to History),
the token refresh after a 403, sync and export requests (PNG by page id,
SVG of another page through a transient one-page projection with autosave
ignored, the 600 ms wait for icons), thumbnails for History, and History
reads and restores that name the state the list belongs to.
A server write goes on the canvas as one undo step (loadDiagram commit,
with the change marked) when decideLoad finds it changes only the page on
screen, keeps the file variables and needs no load-only setting; the other
pages are compared with the MCP core's contentFingerprint. draw.io's own
re-serialized copy of such a write is recognized with isSameDocument and
not pushed back as an edit.
shell/use-mcp-sync.ts mounts the sync once inside DiagramProvider, reads
draw.io's autosave and load messages, and reports the connection state,
which the shell's status bar shows; notices come as toasts from the
dictionaries.
requestExport(request, timeoutMs) runs one draw.io export with the
request's own parameters (a PNG's pageId and width) and resolves with its
answer (data, or xml for format "xml"); null when draw.io does not answer.
showTransient(xml) puts a document on the canvas for an export only: it is
not recorded, its autosaves are ignored, and the next loadDiagram brings
the user back to the page they were on. The MCP's canvas shell answers the
server's export requests with both.
npm run test:e2e starts dist/index.js over the MCP SDK with
DRAWIO_PREVIEW_UI=shell, opens the URL start_session returns in headless
Chromium, and checks that the bundled draw.io loads without console
errors and that the theme toggle switches draw.io in place.
GET /shell/ fills the page template's {{CONFIG_JSON}} with the session,
the API token, where draw.io comes from and the host's editor settings,
with the classic page's security headers; /shell/<file> serves the built
files like the draw.io copy. start_session opens the shell when
DRAWIO_PREVIEW_UI=shell; the classic page stays the default.
The shell (packages/mcp-server/shell) renders DrawioFrame inside the
shared providers without the chat: its config comes from
window.__MCP_CONFIG__, the four dictionaries ship in the bundle, the
theme is kept under an mcp: localStorage key, and system fonts stand in
for the web fonts. scripts/build-shell.mjs bundles it into dist/shell
with the root's esbuild and Tailwind; check-package caps shell.js at
1.5 MB and shell.css at 300 KB.
- Mermaid's converter is in js/extensions.min.js, which the editor loads at
startup, so the README no longer lists it as left out; the org chart
layout (js/orgchart.min.js) is the example of a feature that is.
- Note that the bundled copy, like the web app's, has no /drawio/proxy:
images from other websites, including those in some templates, are left
out of exports and thumbnails. The guard's comment names this as a known
limitation it does not cover.
- Export requests carried a per-process counter. The preview page retries a
result refused with 403 against the process that took over the port, so a
late result of the old process's export could be taken for the new
process's export with the same number. The id is a random UUID now.
- The ETag of a bundled draw.io file now starts with the version the fetch
script stamps into dist/drawio/.version. An install that keeps the
archive's dates (npm does not) would otherwise answer 304 for a changed
file of the same size after an upgrade.
- busyReason had no reader: the chat engine sets isBusy through the store's
generic set, like every other flag.
- The isSameDocument comment says the two "same document" rules disagree
in both directions, so neither is a subset of the other.
- The import boundary test bundles minified with one pako (the MCP core
resolves its own copy) and caps the canvas core at 160 KB (134 KB now).
- The offline deployment guides point at packages/mcp-server/src/
drawio-version.json for the draw.war version; scripts/fetch-drawio.mjs
no longer names it.
- The MCP README says what the bundled draw.io copy includes, and that the
preview page cannot be shown inside an editor's built-in browser since it
is served with frame-ancestors 'self'.
- The list now ships the templates of Insert > Template, the PlantUML
parser of Insert > Advanced and the template dialog's icon: the menu
items were shown but failed with 404s. 15.1 MB packed; the tarball cap
goes from 40 MB to 20 MB, where it still catches a list that grew by a
whole js/ directory.
- The guard drives both features, fails when the copy it runs against
lacks files the editor asked for or when an export does not answer,
checks that the copy is the pinned draw.io version, and runs in CI
(npm run check-drawio, one E2E shard) so a list regression cannot reach
a release.
- The preview page fetches a fresh copy of itself and retries once when an
API request is refused with 403: another MCP process, with its own token,
now answers on this port, and the recovery logic (recoverState) needs its
polls to go through. The page is sent with Cache-Control: no-store.
- draw.io files are served with an ETag and Cache-Control: no-cache instead
of a 24 hour max-age: their names do not change between versions, so a
package upgrade must reach the browser on the next preview. HEAD and
If-None-Match (304) are answered.
- The file read stream goes through stream.pipeline, so a read error no
longer ends the MCP process and a client that leaves mid-download no
longer leaks the file handle.
GET /drawio/<path> serves dist/drawio with a MIME table, a day of
caching and nosniff; paths are normalized and never reach WEB-INF or
META-INF. The preview embeds /drawio/index.html when the copy exists and
DRAWIO_BASE_URL is unset, else the external draw.io as before (and
start_session says so). Every /api request must carry the per-process
X-Drawio-Token the page gets in its HTML; pages send
frame-ancestors 'self' and nosniff.
scripts/fetch-drawio.mjs downloads the pinned draw.war into a cache
(DRAWIO_WAR_CACHE, or DRAWIO_WAR for a local file), checks its SHA-256
and extracts the files named in drawio-files.txt plus a LICENSE with the
Apache-2.0 text. scripts/check-drawio-files.mjs drives the embedded
editor in headless Chromium from a full copy, records every requested
file and checks or rewrites (--update) the list.
The boundary test scans the canvas modules' imports and bundles the canvas components with esbuild; both fail on anything from components/chat, the tool handlers, next/navigation, next/font, next/script, next/headers or a server-only module. The card test renders VersionCard and VersionStrip on a fake VersionsSource.
The canvas components will be reused by the MCP server's browser shell,
which has no chat and no Next.js. They now read everything they need
from small contexts and the canvas store instead of the chat engine:
- components/canvas/versions-context.tsx: VersionsProvider and
useVersionsContext give the version cards, the strip and the compare
dialog a VersionsSource (versions, onCanvasId, undoneId, isBusy,
canUndo, canRedo, restore, undo, redo). The chat fills it from the
versions store and the engine in components/chat/chat-versions.tsx.
- stores/canvas-store.ts: isBusy and busyReason; the chat engine sets
them while a turn runs, SelectionAsk and the version UI read them.
- components/canvas/version-card.tsx: the visual version card and the
thumbnail, out of tool-activity.tsx; the chat's card composes it and
adds its "Show XML" link and code panel.
- compare-dialog.tsx and version-strip.tsx move to components/canvas;
the strip takes the minimum number of versions to show as a prop, the
chat panel computes it from the cards it has.
- components/canvas/locale-context.tsx: LocaleProvider and useLocale,
fed by the [lang] layout; CanvasStage no longer uses next/navigation.
The overlay (SelectionAsk) is a slot and the wait for the saved
language is a prop, so the shell can leave both out.
- lib/version-text.ts: describeChanges is now describeChangeSummary, so
it can be imported next to the MCP core's describeChanges.
* feat(mcp-server): optional screenshot attached to create_new_diagram and edit_diagram results
Both write tools take an optional boolean `screenshot`; the default comes
from DRAWIO_AUTO_SCREENSHOT. The body of screenshot_diagram moved into
captureScreenshot so the write tools can append the PNG and checklist to
their result, or a "Screenshot skipped" note when the preview tab is not
available. The preview page delays the PNG export by 600 ms right after
loading a new version so icon images finish loading.
* fix(mcp-server): review fixes for screenshot attached to create and edit results
* fix(mcp-server): Codex review fixes for the screenshot on write results
- A cleared canvas also gets the screenshot note when one was asked for
- The note for a tab that never polled says it may not have connected yet
* docs: list undo and custom drawing rules among the MCP features
* test(mcp-server): truncation check with named styles and compact cells after the merge
* feat(mcp-server): load .drawio.svg (Editable SVG) files with load_diagram
* feat(mcp-server): summarize the user's manual changes per cell in stale rejections and get_diagram
* feat(mcp-server): XML reference for tables, layers and groups via get_drawing_guide topic
* feat(mcp-server): DRAWIO_LANG, DRAWIO_UI and DRAWIO_DARK for the preview page
Three host-config environment variables fix the language, theme and
dark mode of the draw.io editor embedded in the preview page. The new
drawio-themes module holds the theme list and draw.io's locale names
(zh-hant becomes zh-tw); drawioEmbedParams() in http-server.ts builds
the variable tail of the iframe query and getHtmlPage fills the new
{{DRAWIO_PARAMS}} placeholder. Without the variables the page keeps
sending dark=auto as before. Both READMEs document the variables.
* feat(mcp-server): report XML cut off inside a cell and explain drawing in parts
create_new_diagram and add_page now detect bare-cell XML that ends inside an
unfinished mxCell (XML comments stripped first) and return an error with the
last 300 characters of the input, asking the model to resend from that cell or
continue with edit_diagram add operations. isMxCellXmlComplete moves from
lib/utils.ts into packages/mcp-server/src/new-diagram.ts and is re-exported
from lib/utils.ts for the web app. The drawing guide gains a "Large diagrams"
paragraph and the INSTRUCTIONS edit_diagram line mentions drawing in parts.
* fix(mcp-server): review fixes for load .drawio.svg, change summary, XML reference, draw.io embed options, truncation message
* fix(mcp-server): Codex review fixes for .drawio.svg loading, change summary, references, embed options and truncation
- Truncation check: a closing tag such as </mxCell/> that the auto-fix
repairs is no cut, and input without any cell keeps the validator's
message
- Guide: a call rejected as cut off drew nothing, so all of its cells are
sent again; the topic pointer is its own paragraph
- Change summary: decoded labels (no or merged words from <br>),
a fast path for equal XML, and "the order of the cells changed" as
the fallback
- DRAWIO_UI in any case, DRAWIO_LANG=zh-Hans maps to draw.io's zh
- load_diagram and export_diagram describe which files load again
* feat: redraw the app icon on a white tile with shorter bottom boxes
Same org chart shape as before, redrawn as a vector so edges are sharp.
The white tile keeps the icon visible on dark docks, taskbars and tabs,
so dark mode now uses the same image and favicon-white.svg is removed.
The desktop icon is now 1024x1024.
* chore: add SVG sources for the app icon and favicons
resources/icon.svg renders resources/icon.png; resources/favicon.svg renders
the favicon .ico and .png files.
* fix: add a title to the icon SVG sources for the Biome a11y check
* feat(mcp-server): list and resume saved diagram sessions
start_session accepts an optional session_id to continue a saved diagram
under the same preview URL and auto-save file, reporting the restored
pages. New read-only tool list_saved_diagrams prints every auto-saved
file newest first with its pages. Autosaver gains list() and dataDir().
INSTRUCTIONS and both READMEs mention the new tool.
* feat(mcp-server): add restore_version tool for undo and redo from History
Add the restore_version write tool so the model can put an earlier
version from History back on the canvas (steps_back, default 1). The
current canvas is kept in History unless it is the blank page, so a redo
is possible. http-server.ts exports restoreHistoryEntry, shared by the
HTTP restore endpoint and the tool; history.ts exports HistoryEntry and
otherVersions (distinct versions other than the canvas, newest first).
The drawing guide tells the model to call restore_version when the user
asks to undo, and both READMEs document the tool.
* feat(mcp-server): save the diagram History next to the auto-saved file
The Autosaver writes each session's History (the XML of its last 20 versions, without thumbnails) to <session-id>.history.json beside the .drawio file, after the same 1 second delay and only when the entries changed. When a saved session is resumed after an idle timeout or a process restart, the saved History is loaded back before the diagram, so the History panel and restore_version keep working. Removing the oldest files also removes their History files.
* fix(mcp-server): review fixes for saved sessions, undo tool, history on disk
* fix(mcp-server): Codex review fixes for saved sessions, undo and History on disk
- start_session resumes every saved file, also a cleared one or one of
empty named pages, and decompresses pages draw.io compressed
- History kept by a lost user edit or a recovering tab is saved too
- A diagram cleared before its first save keeps its History on disk
- The History file is written again after the 50-file cap removed it
- restore_version compares versions like the edit gate (a bare
mxGraphModel from the browser has no page name), checks the restore
result, and says "none" when no version is left
- The preview closes an open History list when the server state was
recreated, since the entries got new ids
- start_session's description mentions session_id
* fix(mcp-server): compare History versions pairwise and tie restores to the server state
Second Codex review of the History fixes:
- One bare mxGraphModel in History no longer hides page renames in every
comparison: names are left out only when one of the two is bare
- A History list asked for before the server recreated the session is
dropped, and a restore names the state its list belongs to; the server
refuses one from a lost state (History ids change after a restart)
* fix(mcp-server): restore from a History list with the state it was taken in
Third Codex review: a restore sent the tab's current state, so an old
list shown after a failed refresh passed the server's check. The list now
keeps the state it came from, and the server refuses a missing (null)
state too; only a tab of an older version, which sends none, is let
through.
* feat(mcp-server): add source-reading and clear-canvas hints to the drawing guide
Carry the missing web-prompt sentences into the MCP drawing guide: draw from
a document, image or web page by reading it yourself first, compare image
replications with screenshot_diagram, keep replies short after a successful
draw, compose artistic requests from standard shapes, and clear the canvas by
sending only the two root cells. Replace the "If the diagram is large" bullet
with guidance on choosing create_new_diagram vs edit_diagram.
The server INSTRUCTIONS mention reading files yourself and opening .drawio
files with load_diagram; the create_new_diagram description documents the
clear-canvas call, and its handler reports "Canvas cleared" when the prepared
XML holds only the root cells. Add tests for both and the matching README
lines in the mcp-server and claude-plugin packages.
* feat(mcp-server): append the user's instructions.md to the drawing guide
Read <DRAWIO_DATA_DIR>/instructions.md (default ~/.next-ai-drawio) on
every call and append its first 5000 characters to the drawing guide
under a "## Custom Instructions" heading, for start_session,
get_drawing_guide and the diagram-workflow prompt. start_session now
tells the model where the file lives. Document the feature in both
READMEs and cover it with unit and server-wiring tests.
* fix(mcp-server): review fixes for drawing guide text and custom instructions file
* fix(mcp-server): Codex review fixes for the guide and custom instructions
- Report "Canvas cleared" only for a one-page document; several empty
pages get the page summary
- Build the instructions.md path with path.join (Windows separators)
- Keep the 15000-character guide budget in the wiring test
- Plugin README: DRAWIO_DATA_DIR also holds instructions.md
Agents frequently hold .drawio content in memory (another tool's output,
a repository read, an API response) and previously had to write it to a
temporary file just so load_diagram could read it back. Add a mutually
exclusive 'xml' argument that goes through the same parser as the 'path'
branch, so both plain XML and draw.io's compressed save format work.
Edit-gate semantics by source:
- 'path': unchanged — the model has not seen the content, one
get_diagram round-trip is still required before editing.
- plain 'xml': the model supplied the exact content (same rationale as
create_new_diagram), so it is recorded as seen and can be edited
immediately.
- compressed 'xml': the session stores the decompressed form, which the
model cannot derive from the compressed input — the gate is kept.
Argument validation (mutual exclusion / presence) fires before the
session check so callers get useful errors regardless of session state.
parseDrawioFileContent now reports whether any page was decompressed,
which drives the gate decision. The diagram-workflow prompt is updated
to document the new argument.
Co-authored-by: caoxiaole07 <[email protected]>
Issues labelled "wait for confrim" get a reminder and the "no response"
label after 15 days without activity, and are closed as not planned after
15 more. A reply from the reporter removes both labels.
The test clicked the canvas at (10, 10) to deselect. When the click on the
shape before it needed retries, Playwright scrolled the shape into the
container's top-left corner, so the next click landed on the shape and the
selection stayed (seen on the main run of cd5352c and on #973). The new
clickEmptyCanvas helper clicks the corner farthest from the shape.
Reproduced locally by scrolling the shape into the corner: the old click
keeps the selection, the helper clears it.
* fix(chat): the AI reads and draws the page the user is viewing
The model always got the first page and display_diagram replaced the whole
file with one page, so drawing on page 3 of a multi-page file deleted the
other pages (#241), and edits went to page 1 while the user looked at
another page.
Now a turn records the page the user is viewing when the message is sent.
That page's model goes to the model (also as previousXml, on regenerate and
in edit_diagram error messages), selected shapes are sent on any page,
display_diagram and append_diagram replace only that page (a drawing that
brings several pages still replaces the file), edit_diagram targets it,
and the streaming preview draws on it. Compact cells and default styles
take the layer and the edges of that page, in the preview and in
editDiagram. The editor bridge replaces the current page in place when the
other pages are unchanged, so Ctrl+Z undoes an AI change on a multi-page
file in one step; when a full load is needed, the user's page is shown
again afterwards. Version summaries and the compare dialog count the page
the change was made on.
* test: read the failed edit's output without unsafe optional chaining
The model defines reused styles once as <mxStyle name="..." value="..."/> and
refers to them by name, and writes shapes as one self-closing mxCell with
x, y, w, h and edges with source and target. style-classes.ts and
compact-cells.ts expand both back into standard draw.io XML after validation
and add draw.io's html=1 / whiteSpace=wrap defaults; the diagram shown to the
model is folded into the same notation. Prompts, tool descriptions and the MCP
drawing guide teach the notation with shared examples.
Measured on the five start-screen examples against main: gpt-6-luna 35% fewer
output tokens, Claude Opus 5.5 41% fewer. Two five-model review rounds fixed
quote-aware cell matching, attribute escaping, edge/vertex inference, root-id
and layer handling, and several auto-fix gaps.
electron-builder.yml hard-coded electronVersion: 39.2.7 since #448, so every
desktop release, including v0.5.0, shipped Electron 39.2.7 no matter which
version package.json asked for. Removing the line makes electron-builder use
the installed version (checked: the packaged Electron Framework reports
41.10.7 and the app starts and serves its page). Electron 41.10.7 fixes the
four open Electron advisories (popup sandbox, webview Node integration,
protocol handler cross-origin reads, sandboxed window inheritance) and its
@electron/get 5 drops extract-zip, which clears two more alerts.
npm audit fix in the root and in packages/mcp-server. proxy-addr 2.0.8 fixes
the IP spoofing through IPv4-mapped IPv6 trust subnets (GHSA-jqcg-44mw-7w3h),
shell-quote 1.12.0 fixes the command injection through a line terminator, and
wrangler 4.149.0 brings a miniflare that no longer bundles the vulnerable
sharp 0.35.4. No package is added; 56 lock entries for miniflare's sharp
binaries go away.
A Request's signal follows the signal it was created with through a weak
link inside undici: once the Request object is garbage collected, the link is
gone and request.signal never aborts (nodejs/undici#3644). The chat route
passes req.signal to streamText and holds nothing else from the request, so a
GC pause during a stream could leave the model call running after the client
stopped or disconnected, with no onAbort accounting.
The route now keeps each request in a WeakMap keyed by its response, which
Next holds while the body is piped.
This is also the cause of the unit test chat-route-abort hanging on CI (three
times this week, 60 ms locally): the test's Request is dropped as soon as the
route returns, and a GC in that window lost the abort. With a forced GC the
test hangs on Node 20 and 24 without this change and passes with it.