mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-13 04:59:51 +08:00
feat(mcp-server): saved sessions, restore_version and History on disk (#981)
* 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.
This commit is contained in:
@@ -434,3 +434,80 @@ describe("MCP preview with a diagram over the size limit", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("MCP preview History and a recreated session", () => {
|
||||
/** The pending request to a URL that starts with prefix */
|
||||
const take = (t: Awaited<ReturnType<typeof inStep>>, prefix: string) => {
|
||||
const call = t.calls.find((c) => c.url.startsWith(prefix))
|
||||
if (!call) throw new Error(`no pending request to ${prefix}`)
|
||||
t.calls.splice(t.calls.indexOf(call), 1)
|
||||
return call
|
||||
}
|
||||
const historyList = {
|
||||
status: 200,
|
||||
body: { entries: [{ id: 5, index: 0, svg: "" }], count: 1 },
|
||||
}
|
||||
const modalOpen = () =>
|
||||
document.getElementById("history-modal")?.classList.contains("open")
|
||||
|
||||
it("drops a History list asked for before the session was recreated", async () => {
|
||||
const t = await inStep()
|
||||
;(document.getElementById("history-btn") as HTMLButtonElement).click()
|
||||
await t.settle()
|
||||
const list = take(t, "/api/history")
|
||||
// The server restarts before the list arrives: its ids are old
|
||||
const poll = t.page.poll()
|
||||
take(t, "/api/state").answer(state("S2", 1, "<mxfile>A</mxfile>"))
|
||||
await poll
|
||||
list.answer(historyList)
|
||||
await t.settle()
|
||||
await t.settle()
|
||||
expect(modalOpen()).toBe(false)
|
||||
})
|
||||
|
||||
it("restores from an old list with that list's state", async () => {
|
||||
const t = await inStep()
|
||||
const history = document.getElementById(
|
||||
"history-btn",
|
||||
) as HTMLButtonElement
|
||||
history.click()
|
||||
await t.settle()
|
||||
take(t, "/api/history").answer(historyList)
|
||||
await t.settle()
|
||||
await t.settle()
|
||||
// The server restarts; the open list closes
|
||||
const poll = t.page.poll()
|
||||
take(t, "/api/state").answer(state("S2", 1, "<mxfile>A</mxfile>"))
|
||||
await poll
|
||||
expect(modalOpen()).toBe(false)
|
||||
// Asking again fails, so the old list shows
|
||||
history.click()
|
||||
await t.settle()
|
||||
take(t, "/api/history").fail()
|
||||
await t.settle()
|
||||
expect(modalOpen()).toBe(true)
|
||||
;(document.querySelector(".history-item") as HTMLElement).click()
|
||||
;(document.getElementById("restore-btn") as HTMLButtonElement).click()
|
||||
await t.settle()
|
||||
// The server refuses it: the ids are S1's
|
||||
expect(take(t, "/api/restore").body.stateId).toBe("S1")
|
||||
})
|
||||
|
||||
it("names the state its History list belongs to when restoring", async () => {
|
||||
const t = await inStep()
|
||||
;(document.getElementById("history-btn") as HTMLButtonElement).click()
|
||||
await t.settle()
|
||||
take(t, "/api/history").answer(historyList)
|
||||
await t.settle()
|
||||
await t.settle()
|
||||
expect(modalOpen()).toBe(true)
|
||||
;(document.querySelector(".history-item") as HTMLElement).click()
|
||||
;(document.getElementById("restore-btn") as HTMLButtonElement).click()
|
||||
await t.settle()
|
||||
expect(take(t, "/api/restore").body).toMatchObject({
|
||||
sessionId: "mcp-test",
|
||||
id: 5,
|
||||
stateId: "S1",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user