pi-tldraw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/bridge/app-bridge-entry.js +6 -0
- package/mcp-app/LICENSE.md +9 -0
- package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
- package/mcp-app/README.md +129 -0
- package/mcp-app/dev-tunnel.sh +51 -0
- package/mcp-app/dist/editor-api.json +8493 -0
- package/mcp-app/dist/mcp-app.html +643 -0
- package/mcp-app/dist/method-map.json +915 -0
- package/mcp-app/package.json +42 -0
- package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
- package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
- package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
- package/mcp-app/scripts/extract-editor-api.ts +1374 -0
- package/mcp-app/server.json +21 -0
- package/mcp-app/src/logger.ts +45 -0
- package/mcp-app/src/register-tools.ts +368 -0
- package/mcp-app/src/shared/generated-data.ts +160 -0
- package/mcp-app/src/shared/pending-requests.ts +69 -0
- package/mcp-app/src/shared/types.ts +76 -0
- package/mcp-app/src/shared/utils.ts +132 -0
- package/mcp-app/src/tools/exec.ts +120 -0
- package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
- package/mcp-app/src/tools/search.ts +150 -0
- package/mcp-app/src/widget/app-context.tsx +29 -0
- package/mcp-app/src/widget/dev-log.tsx +70 -0
- package/mcp-app/src/widget/exec-helpers.ts +232 -0
- package/mcp-app/src/widget/export-tldr.ts +35 -0
- package/mcp-app/src/widget/focused/defaults.ts +141 -0
- package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
- package/mcp-app/src/widget/focused/format.ts +366 -0
- package/mcp-app/src/widget/focused/to-focused.ts +258 -0
- package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
- package/mcp-app/src/widget/image-guard.tsx +106 -0
- package/mcp-app/src/widget/index.html +33 -0
- package/mcp-app/src/widget/mcp-app.css +113 -0
- package/mcp-app/src/widget/mcp-app.tsx +857 -0
- package/mcp-app/src/widget/persistence.ts +337 -0
- package/mcp-app/src/widget/snapshot.ts +157 -0
- package/mcp-app/src/worker.ts +305 -0
- package/mcp-app/tsconfig.json +23 -0
- package/mcp-app/vite.config.ts +13 -0
- package/mcp-app/wrangler.toml +45 -0
- package/mcp-app-source.json +36 -0
- package/package.json +90 -0
- package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
- package/scripts/assemble-mcp-app.mjs +193 -0
- package/scripts/build-bridge.mjs +74 -0
- package/scripts/e2e-mcp.mjs +69 -0
- package/scripts/e2e-packaged-mcp-app.mjs +79 -0
- package/scripts/run-mcp-app-dev.mjs +44 -0
- package/scripts/verify-bundle.mjs +41 -0
- package/scripts/verify-mcp-app-source.mjs +51 -0
- package/scripts/verify-mcp-app.mjs +38 -0
- package/scripts/verify-package-files.mjs +50 -0
- package/src/canvas/export.ts +164 -0
- package/src/canvas/state.ts +117 -0
- package/src/canvas/workflow.ts +105 -0
- package/src/commands/tldraw-command.ts +48 -0
- package/src/diagram/guidance.ts +44 -0
- package/src/host/local-host.ts +289 -0
- package/src/index.ts +762 -0
- package/src/mcp/client.ts +126 -0
- package/src/mcp/response.ts +74 -0
- package/src/semantic/layer.ts +309 -0
- package/src/server/server-manager.ts +153 -0
- package/src/store/export-store.ts +33 -0
- package/src/store/project-store.ts +251 -0
- package/src/ui/tldraw-status.ts +88 -0
- package/static/app-bridge-bundle.js +18114 -0
- package/static/app-bridge-bundle.meta.json +164 -0
- package/static/host.html +390 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { type App, McpUiDisplayMode, useApp } from '@modelcontextprotocol/ext-apps/react'
|
|
2
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import {
|
|
5
|
+
type TLAsset,
|
|
6
|
+
type TLBindingCreate,
|
|
7
|
+
type TLComponents,
|
|
8
|
+
type TLShapeId,
|
|
9
|
+
type TLUiEventHandler,
|
|
10
|
+
DefaultToolbar,
|
|
11
|
+
DefaultToolbarContent,
|
|
12
|
+
Editor,
|
|
13
|
+
Tldraw,
|
|
14
|
+
TldrawUiIcon,
|
|
15
|
+
useEditor,
|
|
16
|
+
useValue,
|
|
17
|
+
} from 'tldraw'
|
|
18
|
+
import 'tldraw/tldraw.css'
|
|
19
|
+
import './mcp-app.css'
|
|
20
|
+
import tldrawLogoUrl from '../../plugins/tldraw-mcp/assets/logo.svg'
|
|
21
|
+
import { primeEmbeddedMethodMap } from '../shared/generated-data'
|
|
22
|
+
import {
|
|
23
|
+
MCP_SERVER_DESCRIPTION,
|
|
24
|
+
MCP_SERVER_NAME,
|
|
25
|
+
MCP_SERVER_TITLE,
|
|
26
|
+
MCP_SERVER_VERSION,
|
|
27
|
+
MCP_SERVER_WEBSITE_URL,
|
|
28
|
+
} from '../shared/types'
|
|
29
|
+
import type { MCP_APP_HOST_NAMES } from '../shared/types'
|
|
30
|
+
import { isHostCodeEditor, resolveMcpAppHostNameFromClientInfo } from '../shared/utils'
|
|
31
|
+
import { McpAppContext } from './app-context'
|
|
32
|
+
import { DEV_LOG_PANEL_HEIGHT, DevLogPanel, useDevLog } from './dev-log'
|
|
33
|
+
import { executeCode } from './exec-helpers'
|
|
34
|
+
import { exportTldr } from './export-tldr'
|
|
35
|
+
import { ImageDropGuard, uiOverrides } from './image-guard'
|
|
36
|
+
import {
|
|
37
|
+
type CanvasSnapshot,
|
|
38
|
+
getEmbeddedBootstrap,
|
|
39
|
+
getLatestCheckpointSnapshot,
|
|
40
|
+
loadLocalSnapshot,
|
|
41
|
+
parseCheckpointFromToolResult,
|
|
42
|
+
clearCanvasContext,
|
|
43
|
+
pushCanvasContext,
|
|
44
|
+
saveCheckpointToServer,
|
|
45
|
+
saveLocalSnapshot,
|
|
46
|
+
setCurrentCanvasId,
|
|
47
|
+
setCurrentSessionId,
|
|
48
|
+
syncEditorState,
|
|
49
|
+
} from './persistence'
|
|
50
|
+
import { applySnapshot, zoomToFitRequestShapes } from './snapshot'
|
|
51
|
+
|
|
52
|
+
const LICENSE_KEY = import.meta.env.VITE_TLDRAW_LICENSE_KEY as string
|
|
53
|
+
|
|
54
|
+
const SAVE_DEBOUNCE_MS = 500
|
|
55
|
+
|
|
56
|
+
function SharePanelContent() {
|
|
57
|
+
const editor = useEditor()
|
|
58
|
+
const hasShapes = useValue('hasShapes', () => editor.getCurrentPageShapeIds().size > 0, [editor])
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
displayMode,
|
|
62
|
+
toggleFullscreen,
|
|
63
|
+
canFullscreen,
|
|
64
|
+
canDownload,
|
|
65
|
+
app,
|
|
66
|
+
lastEditor,
|
|
67
|
+
hostName,
|
|
68
|
+
isDev,
|
|
69
|
+
isDevLogVisible,
|
|
70
|
+
toggleDevLog,
|
|
71
|
+
} = useContext(McpAppContext)
|
|
72
|
+
|
|
73
|
+
const isCodeEditor = useMemo(() => {
|
|
74
|
+
if (!hostName) return false
|
|
75
|
+
return isHostCodeEditor(hostName)
|
|
76
|
+
}, [hostName])
|
|
77
|
+
|
|
78
|
+
const handleBuildItClick = useCallback(() => {
|
|
79
|
+
if (!app) return
|
|
80
|
+
const messageText =
|
|
81
|
+
lastEditor === 'user'
|
|
82
|
+
? "Hey I've made some edits to the canvas. The new canvas state is attached. Take the changes and implement them in the codebase."
|
|
83
|
+
: 'Use the attached canvas state to implement this in the codebase.'
|
|
84
|
+
app.sendMessage({
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: messageText,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
}, [app, lastEditor])
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="tlui-share-zone mcp-app__share-zone" draggable={false}>
|
|
97
|
+
{canDownload && (
|
|
98
|
+
<button
|
|
99
|
+
className="tlui-button tlui-button__low"
|
|
100
|
+
onClick={() => exportTldr(editor, app ?? undefined)}
|
|
101
|
+
title="Copy to clipboard and download .tldr file"
|
|
102
|
+
>
|
|
103
|
+
<TldrawUiIcon label="Download .tldr file" icon="download" />
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
{toggleFullscreen && canFullscreen && (
|
|
107
|
+
<button
|
|
108
|
+
className="tlui-button tlui-button__low"
|
|
109
|
+
onClick={toggleFullscreen}
|
|
110
|
+
title={displayMode === 'fullscreen' ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
111
|
+
>
|
|
112
|
+
{displayMode === 'fullscreen' ? 'Exit fullscreen' : 'Fullscreen'}
|
|
113
|
+
</button>
|
|
114
|
+
)}
|
|
115
|
+
{isDev && toggleDevLog && (
|
|
116
|
+
<button
|
|
117
|
+
className="tlui-button tlui-button__low"
|
|
118
|
+
onClick={toggleDevLog}
|
|
119
|
+
title={isDevLogVisible ? 'Hide dev log' : 'Show dev log'}
|
|
120
|
+
>
|
|
121
|
+
{isDevLogVisible ? 'Hide dev log' : 'Show dev log'}
|
|
122
|
+
</button>
|
|
123
|
+
)}
|
|
124
|
+
{isCodeEditor && app && (
|
|
125
|
+
<button
|
|
126
|
+
onClick={handleBuildItClick}
|
|
127
|
+
disabled={!hasShapes}
|
|
128
|
+
title="Build it"
|
|
129
|
+
className={`mcp-app__build-button${hasShapes ? ' mcp-app__build-button--enabled' : ''}`}
|
|
130
|
+
>
|
|
131
|
+
Build it
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function DynamicToolbar() {
|
|
139
|
+
const { displayMode } = useContext(McpAppContext)
|
|
140
|
+
return (
|
|
141
|
+
<DefaultToolbar orientation={displayMode === 'fullscreen' ? 'vertical' : 'horizontal'}>
|
|
142
|
+
<DefaultToolbarContent />
|
|
143
|
+
</DefaultToolbar>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tldrawComponents: TLComponents = {
|
|
148
|
+
SharePanel: SharePanelContent,
|
|
149
|
+
Toolbar: DynamicToolbar,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ERROR_BANNER_HEIGHT = 30
|
|
153
|
+
|
|
154
|
+
function parseHostTheme(value: unknown): 'light' | 'dark' | null {
|
|
155
|
+
return value === 'dark' || value === 'light' ? value : null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function TldrawCanvas({ app }: { app: App }) {
|
|
159
|
+
const [displayMode, setDisplayMode] =
|
|
160
|
+
useState<Extract<McpUiDisplayMode, 'inline' | 'fullscreen'>>('inline')
|
|
161
|
+
const [containerHeight, setContainerHeight] = useState<number | null>(null)
|
|
162
|
+
const [lastEditor, setLastEditor] = useState<'user' | 'ai'>('ai')
|
|
163
|
+
const [hostContext, setHostContext] = useState(() => app.getHostContext())
|
|
164
|
+
const [hostTheme, setHostTheme] = useState<'light' | 'dark'>(() => {
|
|
165
|
+
const initialTheme = parseHostTheme(
|
|
166
|
+
(app.getHostContext() as { theme?: string } | undefined)?.theme
|
|
167
|
+
)
|
|
168
|
+
return initialTheme ?? 'light'
|
|
169
|
+
})
|
|
170
|
+
const [canvasTheme, setCanvasTheme] = useState<'light' | 'dark'>(hostTheme)
|
|
171
|
+
const [execError, setExecError] = useState<string | null>(null)
|
|
172
|
+
const editorRef = useRef<Editor | null>(null)
|
|
173
|
+
|
|
174
|
+
const pendingSnapshotRef = useRef<CanvasSnapshot | null>(null)
|
|
175
|
+
const committedSnapshotRef = useRef<CanvasSnapshot>({ shapes: [], assets: [] })
|
|
176
|
+
const checkpointIdRef = useRef<string | null>(null)
|
|
177
|
+
const removeStoreListenerRef = useRef<(() => void) | null>(null)
|
|
178
|
+
const editorReadyResolveRef = useRef<((editor: Editor) => void) | null>(null)
|
|
179
|
+
const editorReadyPromiseRef = useRef<Promise<Editor> | null>(null)
|
|
180
|
+
const saveTimerRef = useRef<number | null>(null)
|
|
181
|
+
const hasUserEditedSinceAiRef = useRef(false)
|
|
182
|
+
const lastEditorRef = useRef<'user' | 'ai'>('ai')
|
|
183
|
+
const execPartialDebounceRef = useRef<number | null>(null)
|
|
184
|
+
const hasExecRunRef = useRef(false)
|
|
185
|
+
|
|
186
|
+
const { isDev, isDevLogVisible, devLogEntries, logIfDevMode, toggleDevLog, enableDevMode } =
|
|
187
|
+
useDevLog()
|
|
188
|
+
|
|
189
|
+
const hostCapabilities = useMemo(() => {
|
|
190
|
+
return app.getHostCapabilities()
|
|
191
|
+
}, [app])
|
|
192
|
+
|
|
193
|
+
const hostInfo = useMemo(() => {
|
|
194
|
+
return app.getHostVersion()
|
|
195
|
+
}, [app])
|
|
196
|
+
|
|
197
|
+
const isMobilePlatform = hostContext?.platform === 'mobile'
|
|
198
|
+
const isDarkTheme = canvasTheme === 'dark'
|
|
199
|
+
|
|
200
|
+
const syncThemeFromEditor = useCallback(() => {
|
|
201
|
+
const editor = editorRef.current
|
|
202
|
+
if (!editor) return
|
|
203
|
+
setCanvasTheme(editor.user.getIsDarkMode() ? 'dark' : 'light')
|
|
204
|
+
}, [])
|
|
205
|
+
|
|
206
|
+
const applyHostThemeToEditor = useCallback((theme: 'light' | 'dark') => {
|
|
207
|
+
setHostTheme(theme)
|
|
208
|
+
setCanvasTheme(theme)
|
|
209
|
+
const editor = editorRef.current
|
|
210
|
+
if (!editor) return
|
|
211
|
+
editor.user.updateUserPreferences({ colorScheme: theme })
|
|
212
|
+
}, [])
|
|
213
|
+
|
|
214
|
+
const handleUiEvent = useCallback<TLUiEventHandler>(
|
|
215
|
+
(name) => {
|
|
216
|
+
const eventName = name as string
|
|
217
|
+
if (eventName !== 'toggle-dark-mode' && eventName !== 'color-scheme') return
|
|
218
|
+
queueMicrotask(() => {
|
|
219
|
+
syncThemeFromEditor()
|
|
220
|
+
})
|
|
221
|
+
},
|
|
222
|
+
[syncThemeFromEditor]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const canFullscreen = useMemo(() => {
|
|
226
|
+
if (isMobilePlatform) return false
|
|
227
|
+
const modes = hostContext?.availableDisplayModes
|
|
228
|
+
if (!modes) return false
|
|
229
|
+
return modes.includes('fullscreen')
|
|
230
|
+
}, [hostContext, isMobilePlatform])
|
|
231
|
+
|
|
232
|
+
const canDownload = useMemo(() => {
|
|
233
|
+
return !!hostCapabilities?.downloadFile
|
|
234
|
+
}, [hostCapabilities])
|
|
235
|
+
|
|
236
|
+
const [hostName, setHostName] = useState<MCP_APP_HOST_NAMES | null>(null)
|
|
237
|
+
const devLogPanelHeight = isDev && isDevLogVisible ? DEV_LOG_PANEL_HEIGHT : 0
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const resolved = resolveMcpAppHostNameFromClientInfo(hostInfo?.name ?? '')
|
|
241
|
+
if (resolved) {
|
|
242
|
+
setHostName(resolved)
|
|
243
|
+
}
|
|
244
|
+
}, [hostInfo])
|
|
245
|
+
|
|
246
|
+
const teardownEditor = useCallback(() => {
|
|
247
|
+
removeStoreListenerRef.current?.()
|
|
248
|
+
removeStoreListenerRef.current = null
|
|
249
|
+
if (saveTimerRef.current !== null) {
|
|
250
|
+
window.clearTimeout(saveTimerRef.current)
|
|
251
|
+
saveTimerRef.current = null
|
|
252
|
+
}
|
|
253
|
+
if (execPartialDebounceRef.current !== null) {
|
|
254
|
+
window.clearTimeout(execPartialDebounceRef.current)
|
|
255
|
+
execPartialDebounceRef.current = null
|
|
256
|
+
}
|
|
257
|
+
editorRef.current?.dispose()
|
|
258
|
+
editorRef.current = null
|
|
259
|
+
// Reset the editor-ready promise so waitForEditor() creates a fresh one
|
|
260
|
+
// that will be resolved by the next handleMount().
|
|
261
|
+
editorReadyResolveRef.current = null
|
|
262
|
+
editorReadyPromiseRef.current = null
|
|
263
|
+
}, [])
|
|
264
|
+
|
|
265
|
+
const markAiActivity = useCallback(() => {
|
|
266
|
+
hasUserEditedSinceAiRef.current = false
|
|
267
|
+
if (lastEditorRef.current !== 'ai') {
|
|
268
|
+
lastEditorRef.current = 'ai'
|
|
269
|
+
setLastEditor('ai')
|
|
270
|
+
}
|
|
271
|
+
}, [])
|
|
272
|
+
|
|
273
|
+
const markUserEdit = useCallback(() => {
|
|
274
|
+
hasUserEditedSinceAiRef.current = true
|
|
275
|
+
if (lastEditorRef.current !== 'user') {
|
|
276
|
+
lastEditorRef.current = 'user'
|
|
277
|
+
setLastEditor('user')
|
|
278
|
+
}
|
|
279
|
+
}, [])
|
|
280
|
+
|
|
281
|
+
const toggleFullscreen = useCallback(async () => {
|
|
282
|
+
const newMode = displayMode === 'fullscreen' ? 'inline' : 'fullscreen'
|
|
283
|
+
if (newMode === 'fullscreen' && (isMobilePlatform || !canFullscreen)) {
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (newMode === 'inline') {
|
|
288
|
+
const editor = editorRef.current
|
|
289
|
+
if (editor) {
|
|
290
|
+
committedSnapshotRef.current = syncEditorState(app, editor, checkpointIdRef.current)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const result = await app.requestDisplayMode({ mode: newMode })
|
|
296
|
+
const actualMode = result.mode === 'fullscreen' ? 'fullscreen' : 'inline'
|
|
297
|
+
setDisplayMode(actualMode)
|
|
298
|
+
} catch {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
}, [app, canFullscreen, displayMode, isMobilePlatform])
|
|
302
|
+
|
|
303
|
+
const mcpAppCtx = useMemo(
|
|
304
|
+
() => ({
|
|
305
|
+
displayMode,
|
|
306
|
+
toggleFullscreen,
|
|
307
|
+
canFullscreen,
|
|
308
|
+
canDownload,
|
|
309
|
+
app,
|
|
310
|
+
lastEditor,
|
|
311
|
+
hostName,
|
|
312
|
+
isDev,
|
|
313
|
+
isDevLogVisible,
|
|
314
|
+
toggleDevLog,
|
|
315
|
+
logIfDevMode,
|
|
316
|
+
}),
|
|
317
|
+
[
|
|
318
|
+
displayMode,
|
|
319
|
+
toggleFullscreen,
|
|
320
|
+
canFullscreen,
|
|
321
|
+
canDownload,
|
|
322
|
+
app,
|
|
323
|
+
lastEditor,
|
|
324
|
+
hostName,
|
|
325
|
+
isDev,
|
|
326
|
+
isDevLogVisible,
|
|
327
|
+
toggleDevLog,
|
|
328
|
+
logIfDevMode,
|
|
329
|
+
]
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
/** Returns the editor, waiting for mount if it hasn't happened yet. */
|
|
333
|
+
const waitForEditor = useCallback((): Promise<Editor> => {
|
|
334
|
+
const editor = editorRef.current
|
|
335
|
+
if (editor) return Promise.resolve(editor)
|
|
336
|
+
|
|
337
|
+
if (editorReadyPromiseRef.current) return editorReadyPromiseRef.current
|
|
338
|
+
|
|
339
|
+
editorReadyPromiseRef.current = new Promise<Editor>((resolve) => {
|
|
340
|
+
editorReadyResolveRef.current = resolve
|
|
341
|
+
})
|
|
342
|
+
return editorReadyPromiseRef.current
|
|
343
|
+
}, [])
|
|
344
|
+
|
|
345
|
+
const scheduleSave = useCallback(() => {
|
|
346
|
+
if (saveTimerRef.current !== null) {
|
|
347
|
+
window.clearTimeout(saveTimerRef.current)
|
|
348
|
+
}
|
|
349
|
+
saveTimerRef.current = window.setTimeout(() => {
|
|
350
|
+
saveTimerRef.current = null
|
|
351
|
+
const editor = editorRef.current
|
|
352
|
+
if (!editor) return
|
|
353
|
+
syncEditorState(app, editor, checkpointIdRef.current)
|
|
354
|
+
}, SAVE_DEBOUNCE_MS)
|
|
355
|
+
}, [app])
|
|
356
|
+
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
setHostContext(app.getHostContext())
|
|
359
|
+
const initialTheme = parseHostTheme(
|
|
360
|
+
(app.getHostContext() as { theme?: string } | undefined)?.theme
|
|
361
|
+
)
|
|
362
|
+
if (initialTheme) {
|
|
363
|
+
applyHostThemeToEditor(initialTheme)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
logIfDevMode('Bootstrap loading...')
|
|
367
|
+
const bootstrap = getEmbeddedBootstrap()
|
|
368
|
+
primeEmbeddedMethodMap()
|
|
369
|
+
|
|
370
|
+
// Delete the bootstrap data from the window object to prevent it from being used again.
|
|
371
|
+
delete window.__TLDRAW_BOOTSTRAP__
|
|
372
|
+
|
|
373
|
+
if (bootstrap) {
|
|
374
|
+
setCurrentSessionId(bootstrap.sessionId)
|
|
375
|
+
if (bootstrap.canvasId) {
|
|
376
|
+
setCurrentCanvasId(bootstrap.canvasId)
|
|
377
|
+
}
|
|
378
|
+
if (bootstrap.isDev) {
|
|
379
|
+
enableDevMode()
|
|
380
|
+
}
|
|
381
|
+
logIfDevMode(
|
|
382
|
+
`Bootstrap loaded for session ${bootstrap.sessionId}, canvas ${bootstrap.canvasId ?? 'none'}${bootstrap.isDev ? ' (dev mode)' : ''}`
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if (bootstrap.snapshot) {
|
|
386
|
+
if (committedSnapshotRef.current.shapes.length === 0) {
|
|
387
|
+
const snapshot: CanvasSnapshot = {
|
|
388
|
+
shapes: bootstrap.snapshot.shapes,
|
|
389
|
+
assets: bootstrap.snapshot.assets,
|
|
390
|
+
bindings: bootstrap.snapshot.bindings,
|
|
391
|
+
}
|
|
392
|
+
committedSnapshotRef.current = snapshot
|
|
393
|
+
if (bootstrap.checkpointId) {
|
|
394
|
+
checkpointIdRef.current = bootstrap.checkpointId
|
|
395
|
+
logIfDevMode(
|
|
396
|
+
`Restored embedded checkpoint ${bootstrap.checkpointId} with ${bootstrap.snapshot.shapes.length} shape(s)`
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
const editor = editorRef.current
|
|
400
|
+
if (editor) {
|
|
401
|
+
applySnapshot(editor, snapshot)
|
|
402
|
+
} else {
|
|
403
|
+
pendingSnapshotRef.current = snapshot
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
const latestSnapshot = getLatestCheckpointSnapshot()
|
|
408
|
+
if (latestSnapshot && committedSnapshotRef.current.shapes.length === 0) {
|
|
409
|
+
logIfDevMode(
|
|
410
|
+
`Restored latest local snapshot with ${latestSnapshot.shapes.length} shape(s)`
|
|
411
|
+
)
|
|
412
|
+
committedSnapshotRef.current = latestSnapshot
|
|
413
|
+
const editor = editorRef.current
|
|
414
|
+
if (editor) {
|
|
415
|
+
applySnapshot(editor, latestSnapshot)
|
|
416
|
+
} else {
|
|
417
|
+
pendingSnapshotRef.current = latestSnapshot
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
app.onhostcontextchanged = (ctx) => {
|
|
424
|
+
const nextContext = app.getHostContext() ?? ctx
|
|
425
|
+
setHostContext(nextContext)
|
|
426
|
+
const nextTheme = parseHostTheme(
|
|
427
|
+
(ctx as { theme?: string } | undefined)?.theme ??
|
|
428
|
+
(nextContext as { theme?: string } | undefined)?.theme
|
|
429
|
+
)
|
|
430
|
+
if (nextTheme) {
|
|
431
|
+
// Host theme changes take precedence over local preference changes.
|
|
432
|
+
applyHostThemeToEditor(nextTheme)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const dims = ctx.containerDimensions
|
|
436
|
+
if (dims && 'height' in dims) {
|
|
437
|
+
setContainerHeight(dims.height)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (ctx.displayMode !== undefined) {
|
|
441
|
+
const newMode = ctx.displayMode === 'fullscreen' ? 'fullscreen' : 'inline'
|
|
442
|
+
|
|
443
|
+
if (newMode !== 'fullscreen') {
|
|
444
|
+
const editor = editorRef.current
|
|
445
|
+
if (editor) {
|
|
446
|
+
committedSnapshotRef.current = syncEditorState(app, editor, checkpointIdRef.current)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
setDisplayMode(newMode)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const runExec = (code: string, source: string, canvasId?: string) => {
|
|
455
|
+
if (hasExecRunRef.current) {
|
|
456
|
+
logIfDevMode(`Exec: skipping duplicate exec from ${source}`)
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
hasExecRunRef.current = true
|
|
460
|
+
|
|
461
|
+
logIfDevMode(`Exec: running from ${source}`)
|
|
462
|
+
markAiActivity()
|
|
463
|
+
|
|
464
|
+
void (async () => {
|
|
465
|
+
logIfDevMode('Exec: waiting for editor...')
|
|
466
|
+
const editor = await waitForEditor()
|
|
467
|
+
|
|
468
|
+
if (canvasId) {
|
|
469
|
+
setCurrentCanvasId(canvasId)
|
|
470
|
+
|
|
471
|
+
if (editor.getCurrentPageShapeIds().size === 0) {
|
|
472
|
+
logIfDevMode(`Exec: canvas empty, fetching state for canvasId=${canvasId}`)
|
|
473
|
+
try {
|
|
474
|
+
const response = await app.callServerTool({
|
|
475
|
+
name: '_get_canvas_state',
|
|
476
|
+
arguments: { canvasId },
|
|
477
|
+
})
|
|
478
|
+
const res = response as any
|
|
479
|
+
let data: any = null
|
|
480
|
+
// Try structuredContent first, fall back to parsing text content
|
|
481
|
+
if (res?.structuredContent) {
|
|
482
|
+
data = res.structuredContent
|
|
483
|
+
} else if (Array.isArray(res?.content)) {
|
|
484
|
+
const textItem = res.content.find(
|
|
485
|
+
(c: any) => c.type === 'text' && typeof c.text === 'string'
|
|
486
|
+
)
|
|
487
|
+
if (textItem) {
|
|
488
|
+
try {
|
|
489
|
+
data = JSON.parse(textItem.text)
|
|
490
|
+
} catch {
|
|
491
|
+
// not JSON
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (data && Array.isArray(data.shapes) && data.shapes.length > 0) {
|
|
496
|
+
const snapshot: CanvasSnapshot = {
|
|
497
|
+
shapes: data.shapes,
|
|
498
|
+
assets: Array.isArray(data.assets) ? data.assets : [],
|
|
499
|
+
bindings: Array.isArray(data.bindings) ? data.bindings : [],
|
|
500
|
+
}
|
|
501
|
+
applySnapshot(editor, snapshot)
|
|
502
|
+
committedSnapshotRef.current = snapshot
|
|
503
|
+
if (typeof data.checkpointId === 'string') {
|
|
504
|
+
checkpointIdRef.current = data.checkpointId
|
|
505
|
+
}
|
|
506
|
+
logIfDevMode(
|
|
507
|
+
`Exec: restored ${data.shapes.length} shape(s) from server for canvasId=${canvasId}`
|
|
508
|
+
)
|
|
509
|
+
} else {
|
|
510
|
+
logIfDevMode(`Exec: no shapes returned from server for canvasId=${canvasId}`)
|
|
511
|
+
}
|
|
512
|
+
} catch (err) {
|
|
513
|
+
logIfDevMode(`Exec: failed to fetch canvas state: ${err}`)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
logIfDevMode('Exec: editor ready, executing code')
|
|
519
|
+
|
|
520
|
+
const execResult = await executeCode(editor, code)
|
|
521
|
+
logIfDevMode(
|
|
522
|
+
`Exec ${execResult.success ? 'succeeded' : 'failed'}: ${JSON.stringify(execResult.result ?? execResult.error)}`
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
// Call _exec_callback FIRST to get the server-assigned canvasId
|
|
526
|
+
const callbackArgs = execResult.success
|
|
527
|
+
? { channel: 'exec', result: { success: true, result: execResult.result } }
|
|
528
|
+
: { channel: 'exec', result: { success: false, error: execResult.error } }
|
|
529
|
+
try {
|
|
530
|
+
const cbResponse = await app.callServerTool({
|
|
531
|
+
name: '_exec_callback',
|
|
532
|
+
arguments: callbackArgs,
|
|
533
|
+
})
|
|
534
|
+
const cbRes = cbResponse as any
|
|
535
|
+
let cbData: any = null
|
|
536
|
+
if (Array.isArray(cbRes?.content)) {
|
|
537
|
+
const textItem = cbRes.content.find(
|
|
538
|
+
(c: any) => c.type === 'text' && typeof c.text === 'string'
|
|
539
|
+
)
|
|
540
|
+
if (textItem) {
|
|
541
|
+
try {
|
|
542
|
+
cbData = JSON.parse(textItem.text)
|
|
543
|
+
} catch {
|
|
544
|
+
// not JSON
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (cbData?.canvasId) {
|
|
549
|
+
setCurrentCanvasId(cbData.canvasId)
|
|
550
|
+
logIfDevMode(`Exec: server canvasId=${cbData.canvasId}`)
|
|
551
|
+
}
|
|
552
|
+
logIfDevMode('Exec: _exec_callback succeeded')
|
|
553
|
+
} catch (err) {
|
|
554
|
+
logIfDevMode(`Exec: _exec_callback failed: ${err}`)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (execResult.success) {
|
|
558
|
+
const cpId = checkpointIdRef.current ?? crypto.randomUUID()
|
|
559
|
+
checkpointIdRef.current = cpId
|
|
560
|
+
|
|
561
|
+
const resultStr =
|
|
562
|
+
execResult.result !== undefined ? JSON.stringify(execResult.result, null, 2) : undefined
|
|
563
|
+
committedSnapshotRef.current = syncEditorState(app, editor, cpId, {
|
|
564
|
+
message: resultStr
|
|
565
|
+
? `Code executed successfully on canvas. Return value:\n${resultStr}`
|
|
566
|
+
: 'Code executed successfully on canvas.',
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
const snapshot = committedSnapshotRef.current
|
|
570
|
+
const allShapeIds = new Set(snapshot.shapes.map((s) => s.id))
|
|
571
|
+
zoomToFitRequestShapes(editor, allShapeIds)
|
|
572
|
+
} else {
|
|
573
|
+
clearCanvasContext(app, {
|
|
574
|
+
message:
|
|
575
|
+
'Canvas context was cleared because code execution failed. Fix the error before using the canvas context again.',
|
|
576
|
+
})
|
|
577
|
+
teardownEditor()
|
|
578
|
+
setExecError(execResult.error ?? 'Unknown error')
|
|
579
|
+
void app.sendSizeChanged({ width: 400, height: ERROR_BANNER_HEIGHT })
|
|
580
|
+
}
|
|
581
|
+
})()
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
app.ontoolinput = (params) => {
|
|
585
|
+
logIfDevMode('Exec: ontoolinput called')
|
|
586
|
+
// Clear any previous exec error so the Tldraw component remounts and
|
|
587
|
+
// the editor becomes available again. Without this, a single runtime
|
|
588
|
+
// error permanently bricks the canvas — the error banner replaces the
|
|
589
|
+
// <Tldraw> component and the editor is never recreated.
|
|
590
|
+
setExecError(null)
|
|
591
|
+
const code = params.arguments?.code
|
|
592
|
+
if (typeof code !== 'string' || !code.trim()) return
|
|
593
|
+
const canvasId =
|
|
594
|
+
typeof params.arguments?.canvasId === 'string' ? params.arguments.canvasId : undefined
|
|
595
|
+
|
|
596
|
+
if (execPartialDebounceRef.current !== null) {
|
|
597
|
+
window.clearTimeout(execPartialDebounceRef.current)
|
|
598
|
+
}
|
|
599
|
+
execPartialDebounceRef.current = window.setTimeout(() => {
|
|
600
|
+
execPartialDebounceRef.current = null
|
|
601
|
+
runExec(code, 'ontoolinput (debounced)', canvasId)
|
|
602
|
+
}, 500)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
app.ontoolinputpartial = (params) => {
|
|
606
|
+
setExecError(null)
|
|
607
|
+
const code = params.arguments?.code
|
|
608
|
+
if (typeof code !== 'string' || !code.trim()) return
|
|
609
|
+
const canvasId =
|
|
610
|
+
typeof params.arguments?.canvasId === 'string' ? params.arguments.canvasId : undefined
|
|
611
|
+
|
|
612
|
+
if (execPartialDebounceRef.current !== null) {
|
|
613
|
+
window.clearTimeout(execPartialDebounceRef.current)
|
|
614
|
+
}
|
|
615
|
+
execPartialDebounceRef.current = window.setTimeout(() => {
|
|
616
|
+
execPartialDebounceRef.current = null
|
|
617
|
+
runExec(code, 'ontoolinputpartial (debounced)', canvasId)
|
|
618
|
+
}, 1000)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
app.onteardown = async () => {
|
|
622
|
+
return {}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
app.ontoolresult = async (result) => {
|
|
626
|
+
logIfDevMode('Exec: ontoolresult called')
|
|
627
|
+
hasExecRunRef.current = false
|
|
628
|
+
markAiActivity()
|
|
629
|
+
|
|
630
|
+
const checkpoint = parseCheckpointFromToolResult(result)
|
|
631
|
+
if (!checkpoint) return
|
|
632
|
+
logIfDevMode(`Received tool result for checkpoint ${checkpoint.checkpointId}`)
|
|
633
|
+
|
|
634
|
+
const {
|
|
635
|
+
checkpointId,
|
|
636
|
+
sessionId,
|
|
637
|
+
shapes: resultShapes,
|
|
638
|
+
assets: resultAssets,
|
|
639
|
+
bindings: resultBindings,
|
|
640
|
+
} = checkpoint
|
|
641
|
+
checkpointIdRef.current = checkpointId
|
|
642
|
+
|
|
643
|
+
if (sessionId) {
|
|
644
|
+
setCurrentSessionId(sessionId)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const localSnapshot = loadLocalSnapshot(checkpointId)
|
|
648
|
+
const finalShapes = localSnapshot ? localSnapshot.shapes : resultShapes
|
|
649
|
+
const finalAssets: TLAsset[] = localSnapshot ? localSnapshot.assets : resultAssets
|
|
650
|
+
const finalBindings: TLBindingCreate[] = localSnapshot
|
|
651
|
+
? localSnapshot.bindings
|
|
652
|
+
: resultBindings
|
|
653
|
+
|
|
654
|
+
const previousCommittedIds = new Set(committedSnapshotRef.current.shapes.map((s) => s.id))
|
|
655
|
+
|
|
656
|
+
const snapshot: CanvasSnapshot = {
|
|
657
|
+
shapes: finalShapes,
|
|
658
|
+
assets: finalAssets,
|
|
659
|
+
bindings: finalBindings,
|
|
660
|
+
}
|
|
661
|
+
committedSnapshotRef.current = snapshot
|
|
662
|
+
|
|
663
|
+
const editor = editorRef.current
|
|
664
|
+
if (!editor) {
|
|
665
|
+
pendingSnapshotRef.current = snapshot
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
applySnapshot(editor, snapshot)
|
|
670
|
+
|
|
671
|
+
if (!localSnapshot) {
|
|
672
|
+
const newIds = new Set<TLShapeId>()
|
|
673
|
+
for (const shape of finalShapes) {
|
|
674
|
+
if (!previousCommittedIds.has(shape.id)) newIds.add(shape.id)
|
|
675
|
+
}
|
|
676
|
+
zoomToFitRequestShapes(editor, newIds)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
saveLocalSnapshot(checkpointId, finalShapes, finalAssets, finalBindings)
|
|
680
|
+
void saveCheckpointToServer(app, checkpointId, editor)
|
|
681
|
+
pushCanvasContext(app, editor)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
app.ontoolcancelled = (_params) => {
|
|
685
|
+
hasExecRunRef.current = false
|
|
686
|
+
if (execPartialDebounceRef.current !== null) {
|
|
687
|
+
window.clearTimeout(execPartialDebounceRef.current)
|
|
688
|
+
execPartialDebounceRef.current = null
|
|
689
|
+
}
|
|
690
|
+
markAiActivity()
|
|
691
|
+
logIfDevMode('Tool invocation cancelled')
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return () => {
|
|
695
|
+
teardownEditor()
|
|
696
|
+
}
|
|
697
|
+
}, [
|
|
698
|
+
app,
|
|
699
|
+
applyHostThemeToEditor,
|
|
700
|
+
enableDevMode,
|
|
701
|
+
logIfDevMode,
|
|
702
|
+
markAiActivity,
|
|
703
|
+
teardownEditor,
|
|
704
|
+
waitForEditor,
|
|
705
|
+
])
|
|
706
|
+
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
if (!isMobilePlatform || displayMode !== 'fullscreen') return
|
|
709
|
+
|
|
710
|
+
void app
|
|
711
|
+
.requestDisplayMode({ mode: 'inline' })
|
|
712
|
+
.then((result) => {
|
|
713
|
+
const actualMode = result.mode === 'fullscreen' ? 'fullscreen' : 'inline'
|
|
714
|
+
setDisplayMode(actualMode)
|
|
715
|
+
})
|
|
716
|
+
.catch(() => {})
|
|
717
|
+
}, [app, displayMode, isMobilePlatform])
|
|
718
|
+
|
|
719
|
+
useEffect(() => {
|
|
720
|
+
if (displayMode === 'fullscreen' && containerHeight) {
|
|
721
|
+
const h = `${containerHeight}px`
|
|
722
|
+
document.documentElement.style.height = h
|
|
723
|
+
document.body.style.height = h
|
|
724
|
+
} else {
|
|
725
|
+
document.documentElement.style.height = ''
|
|
726
|
+
document.body.style.height = ''
|
|
727
|
+
}
|
|
728
|
+
}, [displayMode, containerHeight])
|
|
729
|
+
|
|
730
|
+
const handleMount = useCallback(
|
|
731
|
+
(editor: Editor) => {
|
|
732
|
+
editorRef.current = editor
|
|
733
|
+
editor.user.updateUserPreferences({ colorScheme: canvasTheme })
|
|
734
|
+
|
|
735
|
+
if (editorReadyResolveRef.current) {
|
|
736
|
+
editorReadyResolveRef.current(editor)
|
|
737
|
+
editorReadyResolveRef.current = null
|
|
738
|
+
editorReadyPromiseRef.current = null
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
removeStoreListenerRef.current?.()
|
|
742
|
+
removeStoreListenerRef.current = editor.store.listen(
|
|
743
|
+
() => {
|
|
744
|
+
markUserEdit()
|
|
745
|
+
scheduleSave()
|
|
746
|
+
},
|
|
747
|
+
{ source: 'user', scope: 'document' }
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
editor.sideEffects.registerAfterChangeHandler('instance', (prev, next) => {
|
|
751
|
+
const pb = prev.screenBounds
|
|
752
|
+
const nb = next.screenBounds
|
|
753
|
+
const dw = nb.w - pb.w
|
|
754
|
+
const dh = nb.h - pb.h
|
|
755
|
+
if (dw === 0 && dh === 0) return
|
|
756
|
+
|
|
757
|
+
const cam = editor.getCamera()
|
|
758
|
+
if (!Number.isFinite(cam.z) || cam.z <= 0) return
|
|
759
|
+
const nextX = cam.x + dw / cam.z / 2
|
|
760
|
+
const nextY = cam.y + dh / cam.z / 2
|
|
761
|
+
if (!Number.isFinite(nextX) || !Number.isFinite(nextY)) return
|
|
762
|
+
|
|
763
|
+
editor.setCamera({
|
|
764
|
+
x: nextX,
|
|
765
|
+
y: nextY,
|
|
766
|
+
z: cam.z,
|
|
767
|
+
})
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
const pendingSnapshot = pendingSnapshotRef.current
|
|
771
|
+
if (pendingSnapshot) {
|
|
772
|
+
pendingSnapshotRef.current = null
|
|
773
|
+
applySnapshot(editor, pendingSnapshot)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
pushCanvasContext(app, editor)
|
|
777
|
+
},
|
|
778
|
+
[app, canvasTheme, markUserEdit, scheduleSave]
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
if (execError) {
|
|
782
|
+
return (
|
|
783
|
+
<div className={`mcp-app__error-banner${isDarkTheme ? ' mcp-app__error-banner--dark' : ''}`}>
|
|
784
|
+
<img
|
|
785
|
+
src={tldrawLogoUrl}
|
|
786
|
+
alt="tldraw logo"
|
|
787
|
+
className={`mcp-app__error-logo${isDarkTheme ? ' mcp-app__error-logo--dark' : ''}`}
|
|
788
|
+
/>
|
|
789
|
+
<span className="mcp-app__error-label">Error editing canvas:</span>
|
|
790
|
+
<span
|
|
791
|
+
title={execError}
|
|
792
|
+
className={`mcp-app__error-message${isDarkTheme ? ' mcp-app__error-message--dark' : ''}`}
|
|
793
|
+
>
|
|
794
|
+
{execError}
|
|
795
|
+
</span>
|
|
796
|
+
</div>
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const isFullscreen = displayMode === 'fullscreen'
|
|
801
|
+
|
|
802
|
+
return (
|
|
803
|
+
<McpAppContext.Provider value={mcpAppCtx}>
|
|
804
|
+
<div
|
|
805
|
+
className={`mcp-app__canvas-layout${isFullscreen ? ' mcp-app__canvas-layout--fullscreen' : ''}`}
|
|
806
|
+
>
|
|
807
|
+
<div
|
|
808
|
+
className={`mcp-app__canvas-surface${isFullscreen ? ' mcp-app__canvas-surface--fullscreen' : ''}${devLogPanelHeight > 0 && !isFullscreen ? ' mcp-app__canvas-surface--with-dev-log' : ''}`}
|
|
809
|
+
>
|
|
810
|
+
<Tldraw
|
|
811
|
+
licenseKey={LICENSE_KEY}
|
|
812
|
+
onMount={handleMount}
|
|
813
|
+
onUiEvent={handleUiEvent}
|
|
814
|
+
components={tldrawComponents}
|
|
815
|
+
overrides={uiOverrides}
|
|
816
|
+
>
|
|
817
|
+
<ImageDropGuard />
|
|
818
|
+
</Tldraw>
|
|
819
|
+
</div>
|
|
820
|
+
{isDev && isDevLogVisible && (
|
|
821
|
+
<DevLogPanel entries={devLogEntries} isFullscreen={isFullscreen} />
|
|
822
|
+
)}
|
|
823
|
+
</div>
|
|
824
|
+
</McpAppContext.Provider>
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function McpApp() {
|
|
829
|
+
const { app, isConnected, error } = useApp({
|
|
830
|
+
appInfo: {
|
|
831
|
+
name: MCP_SERVER_NAME,
|
|
832
|
+
version: MCP_SERVER_VERSION,
|
|
833
|
+
title: MCP_SERVER_TITLE,
|
|
834
|
+
description: MCP_SERVER_DESCRIPTION,
|
|
835
|
+
websiteUrl: MCP_SERVER_WEBSITE_URL,
|
|
836
|
+
},
|
|
837
|
+
capabilities: {
|
|
838
|
+
availableDisplayModes: ['fullscreen', 'inline'],
|
|
839
|
+
},
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
const status = isConnected ? 'ready' : 'connecting'
|
|
843
|
+
|
|
844
|
+
return (
|
|
845
|
+
<div>
|
|
846
|
+
{error ? (
|
|
847
|
+
<div className="mcp-app__status mcp-app__status--error">Error: {error.message}</div>
|
|
848
|
+
) : !isConnected || !app ? (
|
|
849
|
+
<div className="mcp-app__status mcp-app__status--connecting">Status: {status}</div>
|
|
850
|
+
) : (
|
|
851
|
+
<TldrawCanvas app={app} />
|
|
852
|
+
)}
|
|
853
|
+
</div>
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
createRoot(document.getElementById('root')!).render(<McpApp />)
|