mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
fix(mcp-server): review fixes for get_selection
Server (src/http-server.ts, src/index.ts, src/selection.ts, src/new-diagram.ts): - overlapping get_selection calls take turns (readSelection, one slot per session as the export slot) instead of replacing each other's request, which left one of them with a false "tab not in front" timeout - an answer is taken only while a request is pending (both ids undefined compared equal) - the tool sees the page start_session actually opens: with dist/shell missing the classic page is in use, which never answers, so the tool says so instead of timing out - the result lists at most 100 cells and counts the rest: a whole large diagram selected would fill the model's context - every <diagram> the model sends without an id gets one, so the page id the shell reports exists in the server's document Shell (shell/mcp-sync-core.ts, shell/use-mcp-sync.ts, contexts/diagram-context.tsx, lib/drawio/editor-bridge.ts): - an answer whose POST failed is sent again at the next poll - no answer while a full load has yet to reach the editor: it still shows the previous document, whose cells and pages the answer would name - a hidden tab (the same session open twice) answers a poll later, so the tab in front answers first; alone, it still answers within the timeout - a cell's container is reported by the model's isLayer, not by comparing with the default parent, which is the group the user entered
This commit is contained in:
@@ -97,6 +97,9 @@ export interface SyncCanvas {
|
||||
currentXml(): string
|
||||
/** The page on screen; null when unknown (external draw.io) */
|
||||
currentPageId(): string | null
|
||||
/** A full load was sent and draw.io has not reported it yet: the
|
||||
* editor still shows the previous document */
|
||||
loadPending(): boolean
|
||||
/** The cells selected in the editor, for get_selection; `unavailable`
|
||||
* when the page cannot reach the editor (external draw.io) */
|
||||
readSelection(): SelectionAnswer
|
||||
@@ -122,6 +125,8 @@ export interface SyncOptions {
|
||||
canvas: SyncCanvas
|
||||
onNotice: (notice: SyncNotice) => void
|
||||
onStatus?: (status: SyncStatus) => void
|
||||
/** The tab is hidden; another tab of this session may be in front */
|
||||
isHidden?: () => boolean
|
||||
/** The server made a new state for the session: History entries got
|
||||
* new ids, so a list on screen is stale */
|
||||
onStateRecreated?: () => void
|
||||
@@ -736,20 +741,42 @@ export function createMcpSync(options: SyncOptions): McpSync {
|
||||
}
|
||||
// A selection request (get_selection): the editor's selection
|
||||
// on the page the user is viewing, once draw.io is up (the
|
||||
// bridge attaches to it at its first load), and not while a
|
||||
// projection replaces that page
|
||||
// bridge attaches to it at its first load), not while a
|
||||
// projection replaces that page, and not while a full load (the
|
||||
// one above, or one still on its way) has yet to reach the
|
||||
// editor: it still shows the previous document
|
||||
if (
|
||||
s.selectionId &&
|
||||
s.selectionId !== answeredSelectionId &&
|
||||
isReady &&
|
||||
!projectionExportActive
|
||||
!projectionExportActive &&
|
||||
!canvas.loadPending()
|
||||
) {
|
||||
answeredSelectionId = s.selectionId
|
||||
postJson("/state", {
|
||||
sessionId,
|
||||
selectionId: s.selectionId,
|
||||
selection: canvas.readSelection(),
|
||||
}).catch(() => {})
|
||||
const id = s.selectionId
|
||||
answeredSelectionId = id
|
||||
const answer = () =>
|
||||
postJson("/state", {
|
||||
sessionId,
|
||||
selectionId: id,
|
||||
selection: canvas.readSelection(),
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(String(r.status))
|
||||
})
|
||||
.catch(() => {
|
||||
// Not delivered: the next poll answers it again
|
||||
if (answeredSelectionId === id) {
|
||||
answeredSelectionId = null
|
||||
}
|
||||
})
|
||||
// A hidden tab lets a tab in front (the same session open
|
||||
// twice) answer first; the server takes the first answer
|
||||
// and ignores the later one
|
||||
if (options.isHidden?.()) {
|
||||
setTimeout(answer, POLL_INTERVAL_MS + 500)
|
||||
} else {
|
||||
answer()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setStatus("offline")
|
||||
|
||||
@@ -56,6 +56,7 @@ export function useMcpSync(config: ShellConfig): {
|
||||
diagramRef.current.requestExport(request, timeoutMs),
|
||||
currentXml: () => diagramRef.current.chartXMLRef.current,
|
||||
currentPageId: () => useCanvasStore.getState().currentPageId,
|
||||
loadPending: () => diagramRef.current.hasPendingLoad(),
|
||||
// The bridge reads the editor directly; without it (an
|
||||
// external draw.io) the selection cannot be read
|
||||
readSelection: () =>
|
||||
@@ -69,6 +70,7 @@ export function useMcpSync(config: ShellConfig): {
|
||||
onNotice: (notice) =>
|
||||
toast(dictRef.current.shell[notice], { duration: 8000 }),
|
||||
onStatus: setStatus,
|
||||
isHidden: () => document.visibilityState === "hidden",
|
||||
})
|
||||
syncRef.current = sync
|
||||
setSync(sync)
|
||||
|
||||
Reference in New Issue
Block a user