From 0a07944b46a13958ac1199b1490ace6167e6cc94 Mon Sep 17 00:00:00 2001 From: Max Kroner Date: Thu, 12 Feb 2026 16:50:37 +0100 Subject: [PATCH] feat: mount editor inline when fullscreen unavailable (VS Code support) When the host doesn't report fullscreen in availableDisplayModes, mount the Excalidraw editor directly as the primary view instead of SVG-only preview. One code path, no overlay or mode toggling. Changes: - Detect canFullscreen from host context availableDisplayModes - Size inline editor via containerDimensions.maxHeight (fallback 500px) - Deduplicate ontoolinput/ontoolresult replays (VS Code scroll behavior) - Show toolbar only when fullscreen is available - Rename .fullscreen-btn to .toolbar-btn, add flex layout to .toolbar --- src/global.css | 17 ++++++++++++----- src/mcp-app.tsx | 36 +++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/global.css b/src/global.css index 320db7d..ece7da1 100644 --- a/src/global.css +++ b/src/global.css @@ -85,6 +85,11 @@ body { height: 100%; } +/* Inline editor mode (no fullscreen available) */ +.main.inline-editor { + overflow: hidden; +} + /* Hide library button in fullscreen editor */ .main.fullscreen .default-sidebar-trigger { display: none !important; @@ -101,6 +106,8 @@ body { top: 8px; right: 8px; z-index: 100; + display: flex; + gap: 4px; opacity: 0; transition: opacity 0.2s ease; } @@ -109,8 +116,8 @@ body { opacity: 1; } -/* Fullscreen button */ -.fullscreen-btn { +/* Toolbar button */ +.toolbar-btn { display: flex; align-items: center; justify-content: center; @@ -129,14 +136,14 @@ body { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } -.fullscreen-btn:hover { +.toolbar-btn:hover { color: rgba(0, 0, 0, 0.7); background: rgba(255, 255, 255, 0.8); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); } -/* Hide fullscreen button in fullscreen mode (host provides exit UI) */ -.main.fullscreen .fullscreen-btn { +/* Hide toolbar button in fullscreen mode (host provides exit UI) */ +.main.fullscreen .toolbar-btn { display: none !important; } diff --git a/src/mcp-app.tsx b/src/mcp-app.tsx index ccd686d..72d0ba0 100644 --- a/src/mcp-app.tsx +++ b/src/mcp-app.tsx @@ -565,6 +565,8 @@ function ExcalidrawApp() { const [elements, setElements] = useState([]); const [userEdits, setUserEdits] = useState(null); const [containerHeight, setContainerHeight] = useState(null); + const [canFullscreen, setCanFullscreen] = useState(false); + const [containerMaxHeight, setContainerMaxHeight] = useState(null); const [editorReady, setEditorReady] = useState(false); const [excalidrawApi, setExcalidrawApi] = useState(null); const [editorSettled, setEditorSettled] = useState(false); @@ -572,6 +574,7 @@ function ExcalidrawApp() { const svgViewportRef = useRef(null); const elementsRef = useRef([]); const checkpointIdRef = useRef(null); + const lastInputRef = useRef(null); const toggleFullscreen = useCallback(async () => { if (!appRef.current) return; @@ -627,9 +630,9 @@ function ExcalidrawApp() { } }, [displayMode, containerHeight]); - // Mount editor when entering fullscreen + // Mount editor when entering fullscreen or when host doesn't support fullscreen useEffect(() => { - if (displayMode !== "fullscreen") { + if (displayMode !== "fullscreen" && canFullscreen) { setEditorReady(false); setExcalidrawApi(null); setEditorSettled(false); @@ -639,10 +642,10 @@ function ExcalidrawApp() { await document.fonts.ready; setTimeout(() => setEditorReady(true), 200); })(); - }, [displayMode]); + }, [displayMode, canFullscreen]); // After editor mounts: refresh text dimensions, then reveal - const mountEditor = displayMode === "fullscreen" && inputIsFinal && elements.length > 0 && editorReady; + const mountEditor = (displayMode === "fullscreen" || !canFullscreen) && inputIsFinal && elements.length > 0 && editorReady; useEffect(() => { if (!mountEditor || !excalidrawApi) return; if (editorSettled) return; // already revealed, don't redo @@ -681,14 +684,25 @@ function ExcalidrawApp() { appRef.current = app; _logFn = (msg) => { try { app.sendLog({ level: "info", logger: "FS", data: msg }); } catch {} }; - // Capture initial container dimensions - const initDims = app.getHostContext()?.containerDimensions as any; + // Capture initial container dimensions and host capabilities + const initCtx = app.getHostContext() as any; + const initDims = initCtx?.containerDimensions; if (initDims?.height) setContainerHeight(initDims.height); + if (initDims?.maxHeight) setContainerMaxHeight(initDims.maxHeight); + if (initCtx?.availableDisplayModes) { + setCanFullscreen(initCtx.availableDisplayModes.includes("fullscreen")); + } app.onhostcontextchanged = (ctx: any) => { if (ctx.containerDimensions?.height) { setContainerHeight(ctx.containerDimensions.height); } + if (ctx.containerDimensions?.maxHeight) { + setContainerMaxHeight(ctx.containerDimensions.maxHeight); + } + if (ctx.availableDisplayModes) { + setCanFullscreen(ctx.availableDisplayModes.includes("fullscreen")); + } if (ctx.displayMode) { fsLog(`hostContextChanged: displayMode=${ctx.displayMode}`); // Sync edited elements when host exits fullscreen @@ -711,6 +725,9 @@ function ExcalidrawApp() { app.ontoolinput = async (input) => { const args = (input as any)?.arguments || input; + const sig = JSON.stringify(args); + if (lastInputRef.current === sig) return; + lastInputRef.current = sig; setInputIsFinal(true); setToolInput(args); }; @@ -718,6 +735,7 @@ function ExcalidrawApp() { app.ontoolresult = (result: any) => { const cpId = (result.structuredContent as { checkpointId?: string })?.checkpointId; if (cpId) { + if (cpId === checkpointIdRef.current) return; checkpointIdRef.current = cpId; setCheckpointId(cpId); // Use checkpointId as localStorage key for persisting user edits @@ -741,11 +759,11 @@ function ExcalidrawApp() { if (!app) return
Connecting...
; return ( -
- {displayMode === "inline" && ( +
+ {displayMode === "inline" && canFullscreen && (