fix(mcp-server): review fixes for the shell page

- a download button in the header opens the web app's export dialog
  (.drawio, .png, .svg, .drawio.svg), which the classic page had and the
  shell lacked when it became the default
- the shell asks draw.io for the custom library menu (libraries=1), as
  the classic page did; the web app keeps libraries=0
- the newest card no longer shows "Rendering preview" for good: the sync
  takes the thumbnail of a diagram the server recovered from its file
  (saved without pictures) while the canvas kept it, and of a write whose
  picture was skipped because an edit came first, once the canvas shows
  the write again
- e2e: the get_selection test covers a shape in a container the user
  entered; a download test saves a .drawio file
This commit is contained in:
dayuan.jiang
2026-10-11 20:56:07 +09:00
parent 467fd25fd1
commit f257d8ee99
7 changed files with 164 additions and 8 deletions
+33 -4
View File
@@ -1,10 +1,12 @@
import { History, Moon, Sun } from "lucide-react"
import { Download, History, Moon, Sun } from "lucide-react"
import { useMemo, useState } from "react"
import { CanvasStage } from "@/components/canvas/canvas-stage"
import { CompareDialog } from "@/components/canvas/compare-dialog"
import { VersionStrip } from "@/components/canvas/version-strip"
import { VersionsProvider } from "@/components/canvas/versions-context"
import { SaveDialog } from "@/components/save-dialog"
import { AppToaster } from "@/components/workspace/app-toaster"
import { useDiagram } from "@/contexts/diagram-context"
import { useDictionary } from "@/hooks/use-dictionary"
import type { DrawioSource } from "@/lib/drawio/drawio-config"
import { cn } from "@/lib/utils"
@@ -21,20 +23,24 @@ const iconButton =
/**
* The MCP preview: the web app's canvas without the chat. A slim bar names
* the session and the sync state; the versions strip, the version cards
* (from the server's History) and Compare come from the shared canvas
* components.
* (from the server's History), Compare and the export dialog come from the
* shared canvas components.
*/
export function CanvasShell({ config }: { config: ShellConfig }) {
const dict = useDictionary()
const isDark = useSettingsStore((s) => s.isDark)
const { saveDiagramToFile } = useDiagram()
const { status, sync } = useMcpSync(config)
const versions = useNodeVersions(sync)
const [panelOpen, setPanelOpen] = useState(true)
const [saveOpen, setSaveOpen] = useState(false)
const drawioSource = useMemo<DrawioSource>(
() => ({
editorUrl: config.drawioBaseUrl,
ui: config.drawioUi || undefined,
lang: config.drawioLang || undefined,
// As the classic preview page: the user's own shape libraries
libraries: true,
}),
[config],
)
@@ -89,12 +95,22 @@ export function CanvasShell({ config }: { config: ShellConfig }) {
<History />
</button>
)}
<button
type="button"
onClick={() => setSaveOpen(true)}
aria-label={dict.workspace.export}
title={dict.workspace.export}
className={cn(iconButton, !hasVersions && "ml-auto")}
data-testid="download"
>
<Download />
</button>
<button
type="button"
onClick={() => setDark(!isDark)}
aria-label={dict.shell.toggleTheme}
title={dict.shell.toggleTheme}
className={cn(iconButton, !hasVersions && "ml-auto")}
className={iconButton}
data-testid="theme-toggle"
>
{isDark ? <Sun /> : <Moon />}
@@ -117,6 +133,19 @@ export function CanvasShell({ config }: { config: ShellConfig }) {
)}
</div>
<CompareDialog />
<SaveDialog
open={saveOpen}
onOpenChange={setSaveOpen}
onSave={(filename, format) =>
saveDiagramToFile(
filename,
format,
undefined,
dict.save.savedSuccessfully,
)
}
defaultFilename={`diagram-${new Date().toLocaleDateString("sv-SE")}`}
/>
<AppToaster />
</div>
</VersionsProvider>
+26 -1
View File
@@ -300,6 +300,9 @@ export function createMcpSync(options: SyncOptions): McpSync {
version: number
xml: string
} | null = null
// Its picture was not taken because the user had changed the canvas by
// then: taken when the canvas shows the write again (an undo)
let thumbMissed = false
let pendingMcpExport: ExportFormat | null = null
let mcpExportSeq = 0
let mcpExportId: string | null = null
@@ -409,6 +412,7 @@ export function createMcpSync(options: SyncOptions): McpSync {
decision.mode === "commit" && !sameDiagram(before, xml)
// currentVersion is the write's version
thumbExport = { stateId, version: currentVersion, xml }
thumbMissed = false
setTimeout(captureThumbnail, 500)
}
@@ -420,8 +424,12 @@ export function createMcpSync(options: SyncOptions): McpSync {
if (!t || !isReady) return
canvas.export({ format: "svg" }, 5000).then((result) => {
if (!result?.data || thumbExport !== t) return
if (!sameDiagram(latestXml, t.xml)) return
if (!sameDiagram(latestXml, t.xml)) {
thumbMissed = true
return
}
thumbExport = null
thumbMissed = false
postJson("/history-svg", {
sessionId,
svg: svgDataUrl(result.data),
@@ -552,6 +560,13 @@ export function createMcpSync(options: SyncOptions): McpSync {
}
if (mine && !sameDiagram(mine, s.xml)) {
pushState(mine, "", s.version)
} else if (s.xml && !s.blank && mine) {
// The canvas still shows the diagram the server recovered:
// its History entry came back without a picture (the file
// keeps none), take one now
thumbExport = { stateId, version: currentVersion, xml: s.xml }
thumbMissed = false
captureThumbnail()
}
} else if (s.xml) {
// The server has a diagram this tab never showed (an AI write it
@@ -828,6 +843,16 @@ export function createMcpSync(options: SyncOptions): McpSync {
latestXml = xml
const commitCopy = awaitingCommitCopy
awaitingCommitCopy = false
// The canvas is back at a write whose picture was not taken (an
// edit came first): take it now
if (
thumbMissed &&
thumbExport &&
sameDiagram(xml, thumbExport.xml)
) {
thumbMissed = false
captureThumbnail()
}
if (xml === lastXml || (commitCopy && sameDiagram(xml, lastXml))) {
return
}