@xpert-ai/plugin-drawio 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.
Files changed (100) hide show
  1. package/.xpertai-plugin/plugin.json +118 -0
  2. package/README.md +5 -0
  3. package/assets/composerIcon.svg +6 -0
  4. package/assets/logo.svg +10 -0
  5. package/dist/docs/drawio-agent-skill.md +36 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +153 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/constants.d.ts +24 -0
  11. package/dist/lib/constants.d.ts.map +1 -0
  12. package/dist/lib/constants.js +43 -0
  13. package/dist/lib/constants.js.map +1 -0
  14. package/dist/lib/drawio-view.provider.d.ts +14 -0
  15. package/dist/lib/drawio-view.provider.d.ts.map +1 -0
  16. package/dist/lib/drawio-view.provider.js +435 -0
  17. package/dist/lib/drawio-view.provider.js.map +1 -0
  18. package/dist/lib/drawio.middleware.d.ts +10 -0
  19. package/dist/lib/drawio.middleware.d.ts.map +1 -0
  20. package/dist/lib/drawio.middleware.js +159 -0
  21. package/dist/lib/drawio.middleware.js.map +1 -0
  22. package/dist/lib/drawio.plugin.d.ts +8 -0
  23. package/dist/lib/drawio.plugin.d.ts.map +1 -0
  24. package/dist/lib/drawio.plugin.js +27 -0
  25. package/dist/lib/drawio.plugin.js.map +1 -0
  26. package/dist/lib/drawio.service.d.ts +176 -0
  27. package/dist/lib/drawio.service.d.ts.map +1 -0
  28. package/dist/lib/drawio.service.js +415 -0
  29. package/dist/lib/drawio.service.js.map +1 -0
  30. package/dist/lib/drawio.templates.d.ts +3 -0
  31. package/dist/lib/drawio.templates.d.ts.map +1 -0
  32. package/dist/lib/drawio.templates.js +78 -0
  33. package/dist/lib/drawio.templates.js.map +1 -0
  34. package/dist/lib/entities/drawio-action-log.entity.d.ts +18 -0
  35. package/dist/lib/entities/drawio-action-log.entity.d.ts.map +1 -0
  36. package/dist/lib/entities/drawio-action-log.entity.js +69 -0
  37. package/dist/lib/entities/drawio-action-log.entity.js.map +1 -0
  38. package/dist/lib/entities/drawio-drawing-version.entity.d.ts +22 -0
  39. package/dist/lib/entities/drawio-drawing-version.entity.d.ts.map +1 -0
  40. package/dist/lib/entities/drawio-drawing-version.entity.js +86 -0
  41. package/dist/lib/entities/drawio-drawing-version.entity.js.map +1 -0
  42. package/dist/lib/entities/drawio-drawing.entity.d.ts +24 -0
  43. package/dist/lib/entities/drawio-drawing.entity.d.ts.map +1 -0
  44. package/dist/lib/entities/drawio-drawing.entity.js +94 -0
  45. package/dist/lib/entities/drawio-drawing.entity.js.map +1 -0
  46. package/dist/lib/entities/index.d.ts +4 -0
  47. package/dist/lib/entities/index.d.ts.map +1 -0
  48. package/dist/lib/entities/index.js +4 -0
  49. package/dist/lib/entities/index.js.map +1 -0
  50. package/dist/lib/remote-components/drawio-workbench/app.css +0 -0
  51. package/dist/lib/remote-components/drawio-workbench/app.js +1151 -0
  52. package/dist/lib/remote-components/drawio-workbench/src/i18n.d.ts +3 -0
  53. package/dist/lib/remote-components/drawio-workbench/src/i18n.d.ts.map +1 -0
  54. package/dist/lib/remote-components/drawio-workbench/src/i18n.js +95 -0
  55. package/dist/lib/remote-components/drawio-workbench/src/i18n.js.map +1 -0
  56. package/dist/lib/remote-components/drawio-workbench/src/i18n.ts +139 -0
  57. package/dist/lib/remote-components/drawio-workbench/src/main.tsx +916 -0
  58. package/dist/lib/remote-components/drawio-workbench/src/react-dom-client-shim.d.ts +3 -0
  59. package/dist/lib/remote-components/drawio-workbench/src/react-dom-client-shim.d.ts.map +1 -0
  60. package/dist/lib/remote-components/drawio-workbench/src/react-dom-client-shim.js +4 -0
  61. package/dist/lib/remote-components/drawio-workbench/src/react-dom-client-shim.js.map +1 -0
  62. package/dist/lib/remote-components/drawio-workbench/src/react-dom-client-shim.ts +4 -0
  63. package/dist/lib/remote-components/drawio-workbench/src/react-dom-shim.d.ts +11 -0
  64. package/dist/lib/remote-components/drawio-workbench/src/react-dom-shim.d.ts.map +1 -0
  65. package/dist/lib/remote-components/drawio-workbench/src/react-dom-shim.js +11 -0
  66. package/dist/lib/remote-components/drawio-workbench/src/react-dom-shim.js.map +1 -0
  67. package/dist/lib/remote-components/drawio-workbench/src/react-dom-shim.ts +11 -0
  68. package/dist/lib/remote-components/drawio-workbench/src/react-jsx-runtime-shim.d.ts +5 -0
  69. package/dist/lib/remote-components/drawio-workbench/src/react-jsx-runtime-shim.d.ts.map +1 -0
  70. package/dist/lib/remote-components/drawio-workbench/src/react-jsx-runtime-shim.js +8 -0
  71. package/dist/lib/remote-components/drawio-workbench/src/react-jsx-runtime-shim.js.map +1 -0
  72. package/dist/lib/remote-components/drawio-workbench/src/react-jsx-runtime-shim.ts +8 -0
  73. package/dist/lib/remote-components/drawio-workbench/src/react-shim.d.ts +36 -0
  74. package/dist/lib/remote-components/drawio-workbench/src/react-shim.d.ts.map +1 -0
  75. package/dist/lib/remote-components/drawio-workbench/src/react-shim.js +36 -0
  76. package/dist/lib/remote-components/drawio-workbench/src/react-shim.js.map +1 -0
  77. package/dist/lib/remote-components/drawio-workbench/src/react-shim.ts +36 -0
  78. package/dist/lib/remote-components/drawio-workbench/src/runtime.d.ts +21 -0
  79. package/dist/lib/remote-components/drawio-workbench/src/runtime.d.ts.map +1 -0
  80. package/dist/lib/remote-components/drawio-workbench/src/runtime.js +198 -0
  81. package/dist/lib/remote-components/drawio-workbench/src/runtime.js.map +1 -0
  82. package/dist/lib/remote-components/drawio-workbench/src/runtime.ts +228 -0
  83. package/dist/lib/remote-components/drawio-workbench/src/styles.d.ts +2 -0
  84. package/dist/lib/remote-components/drawio-workbench/src/styles.d.ts.map +1 -0
  85. package/dist/lib/remote-components/drawio-workbench/src/styles.js +290 -0
  86. package/dist/lib/remote-components/drawio-workbench/src/styles.js.map +1 -0
  87. package/dist/lib/remote-components/drawio-workbench/src/styles.ts +289 -0
  88. package/dist/lib/remote-components/drawio-workbench/src/vendor.d.ts +4 -0
  89. package/dist/lib/remote-components/drawio-workbench/src/vendor.d.ts.map +1 -0
  90. package/dist/lib/remote-components/drawio-workbench/src/vendor.js +4 -0
  91. package/dist/lib/remote-components/drawio-workbench/src/vendor.js.map +1 -0
  92. package/dist/lib/remote-components/drawio-workbench/src/vendor.ts +3 -0
  93. package/dist/lib/types.d.ts +67 -0
  94. package/dist/lib/types.d.ts.map +1 -0
  95. package/dist/lib/types.js +2 -0
  96. package/dist/lib/types.js.map +1 -0
  97. package/dist/xpert-drawio-assistant.yaml +130 -0
  98. package/package.json +85 -0
  99. package/skills/index/SKILL.md +45 -0
  100. package/skills/index/agents/xpertai.yaml +6 -0
@@ -0,0 +1,916 @@
1
+ import {
2
+ Archive,
3
+ Badge,
4
+ Button,
5
+ Check,
6
+ FileJson,
7
+ Input,
8
+ PanelLeftClose,
9
+ PanelLeftOpen,
10
+ PanelRightClose,
11
+ PanelRightOpen,
12
+ Plus,
13
+ RotateCcw,
14
+ Save,
15
+ ScrollArea,
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ Send,
22
+ Sidebar,
23
+ SidebarContent,
24
+ SidebarHeader,
25
+ SidebarMenu,
26
+ SidebarMenuButton,
27
+ SidebarMenuItem,
28
+ SidebarRail,
29
+ SidebarTitle,
30
+ SidebarTrigger,
31
+ Textarea,
32
+ Upload,
33
+ installShadcnThemeVars
34
+ } from '@xpert-ai/plugin-shadcn-ui'
35
+ import { React, ReactDOM, h } from './vendor'
36
+ import { createTranslator, TranslationKey } from './i18n'
37
+ import { injectStyles } from './styles'
38
+ import {
39
+ executeAction,
40
+ executeFileAction,
41
+ getErrorMessage,
42
+ getResponsePayload,
43
+ invokeClientCommand,
44
+ notify,
45
+ post,
46
+ reportResize,
47
+ requestData,
48
+ resolveMessage,
49
+ setRuntimeText,
50
+ startRemoteBridge
51
+ } from './runtime'
52
+
53
+ type StatusFilter = '' | 'draft' | 'reviewed' | 'archived'
54
+ type Drawing = Record<string, any>
55
+ type DrawingVersion = Record<string, any>
56
+ type DetailPayload = {
57
+ item?: Drawing
58
+ currentVersion?: DrawingVersion | null
59
+ versions?: DrawingVersion[]
60
+ logs?: any[]
61
+ }
62
+
63
+ const DRAWIO_ORIGIN = 'https://embed.diagrams.net'
64
+ const DRAWIO_EDITOR_URL = `${DRAWIO_ORIGIN}/?embed=1&proto=json&spin=1&libraries=1&configure=1&noExitBtn=1&saveAndExit=0&modified=0`
65
+ const EMPTY_DRAWIO_XML = '<mxfile host="xpert"><diagram name="Page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>'
66
+ const DEFAULT_MERMAID = `flowchart TD
67
+ A[User Request] --> B[Agent Plans Diagram]
68
+ B --> C{Best Format?}
69
+ C -->|Flow| D[Save Mermaid Draft]
70
+ C -->|Precise Layout| E[Save draw.io XML]
71
+ D --> F[Workbench Loads diagrams.net]
72
+ E --> G[Human Review]
73
+ F --> G`
74
+
75
+ const SAVE_MERMAID_DRAFT_TOOL_NAME = 'drawio_save_mermaid_draft'
76
+ const DRAWIO_TOOL_NAMES = new Set([
77
+ 'drawio_create_diagram',
78
+ 'drawio_save_scene_version',
79
+ 'drawio_patch_scene',
80
+ SAVE_MERMAID_DRAFT_TOOL_NAME,
81
+ 'drawio_search_diagrams',
82
+ 'drawio_get_diagram',
83
+ 'drawio_update_diagram_status',
84
+ 'drawio_report_failure'
85
+ ])
86
+ const DRAWIO_MUTATION_TOOL_NAMES = new Set([
87
+ 'drawio_create_diagram',
88
+ 'drawio_save_scene_version',
89
+ 'drawio_patch_scene',
90
+ SAVE_MERMAID_DRAFT_TOOL_NAME,
91
+ 'drawio_update_diagram_status',
92
+ 'drawio_report_failure'
93
+ ])
94
+
95
+ installShadcnThemeVars({ styleId: 'drawio-workbench-shadcn-ui-vars' })
96
+ injectStyles()
97
+
98
+ function App() {
99
+ const [context, setContext] = React.useState<any>(null)
100
+ const [drawings, setDrawings] = React.useState<Drawing[]>([])
101
+ const [detail, setDetail] = React.useState<DetailPayload | null>(null)
102
+ const [selectedId, setSelectedId] = React.useState('')
103
+ const [search, setSearch] = React.useState('')
104
+ const [status, setStatus] = React.useState<StatusFilter>('')
105
+ const [busy, setBusy] = React.useState(false)
106
+ const [dirty, setDirty] = React.useState(false)
107
+ const [editorReady, setEditorReady] = React.useState(false)
108
+ const [newTitle, setNewTitle] = React.useState('')
109
+ const [newDescription, setNewDescription] = React.useState('')
110
+ const [changeSummary, setChangeSummary] = React.useState('')
111
+ const [assistantPrompt, setAssistantPrompt] = React.useState('')
112
+ const [mermaidSource, setMermaidSource] = React.useState(DEFAULT_MERMAID)
113
+ const [xml, setXml] = React.useState(EMPTY_DRAWIO_XML)
114
+ const [leftPanelCollapsed, setLeftPanelCollapsed] = React.useState(true)
115
+ const [rightPanelCollapsed, setRightPanelCollapsed] = React.useState(true)
116
+ const iframeRef = React.useRef<HTMLIFrameElement | null>(null)
117
+ const fileInputRef = React.useRef<HTMLInputElement | null>(null)
118
+ const contextRef = React.useRef<any>(null)
119
+ const selectedIdRef = React.useRef('')
120
+ const searchRef = React.useRef('')
121
+ const statusRef = React.useRef<StatusFilter>('')
122
+ const hostEventSequenceRef = React.useRef(0)
123
+ const pendingSaveActionRef = React.useRef<string | null>(null)
124
+ const t = createTranslator(context?.locale)
125
+
126
+ React.useEffect(() => {
127
+ setRuntimeText({
128
+ requestTimeout: t('requestTimeout'),
129
+ remoteRequestFailed: t('remoteRequestFailed'),
130
+ unknownError: t('unknownError')
131
+ })
132
+ }, [context?.locale])
133
+
134
+ React.useEffect(() => {
135
+ selectedIdRef.current = selectedId
136
+ }, [selectedId])
137
+
138
+ React.useEffect(() => {
139
+ searchRef.current = search
140
+ }, [search])
141
+
142
+ React.useEffect(() => {
143
+ statusRef.current = status
144
+ }, [status])
145
+
146
+ React.useEffect(() => {
147
+ startRemoteBridge(
148
+ (nextContext) => {
149
+ contextRef.current = nextContext
150
+ setContext(nextContext)
151
+ hydratePayload(nextContext.payload || null)
152
+ setTimeout(() => reloadList(), 0)
153
+ },
154
+ (event) => {
155
+ void reloadAfterHostEvent(event)
156
+ }
157
+ )
158
+ post('ready')
159
+ }, [])
160
+
161
+ React.useEffect(reportResize, [drawings, detail, busy, dirty, editorReady, leftPanelCollapsed, rightPanelCollapsed])
162
+
163
+ React.useEffect(() => {
164
+ const handler = (event: MessageEvent) => {
165
+ if (iframeRef.current?.contentWindow && event.source !== iframeRef.current.contentWindow) {
166
+ return
167
+ }
168
+ const message = parseMessage(event.data)
169
+ if (!message || typeof message.event !== 'string') {
170
+ return
171
+ }
172
+ if (message.event === 'configure') {
173
+ postToEditor({
174
+ action: 'configure',
175
+ config: {
176
+ defaultFonts: ['Inter', 'Arial'],
177
+ compressXml: true,
178
+ libraries: true
179
+ }
180
+ })
181
+ return
182
+ }
183
+ if (message.event === 'init') {
184
+ setEditorReady(true)
185
+ loadCurrentSceneIntoEditor()
186
+ return
187
+ }
188
+ if (message.event === 'save') {
189
+ const nextXml = typeof message.xml === 'string' && message.xml.trim() ? message.xml : xml
190
+ setXml(nextXml)
191
+ setDirty(true)
192
+ const sourceAction = pendingSaveActionRef.current || 'save_scene_version'
193
+ pendingSaveActionRef.current = null
194
+ void saveCurrentScene(nextXml, sourceAction)
195
+ }
196
+ if (message.event === 'exit') {
197
+ pendingSaveActionRef.current = null
198
+ }
199
+ }
200
+ window.addEventListener('message', handler)
201
+ return () => window.removeEventListener('message', handler)
202
+ }, [xml, selectedId, mermaidSource, detail?.item?.id, context?.theme])
203
+
204
+ function hydratePayload(payload: any) {
205
+ if (!payload) {
206
+ return
207
+ }
208
+ if (Array.isArray(payload.items)) {
209
+ setDrawings(payload.items)
210
+ if (!selectedIdRef.current && payload.items[0]?.id) {
211
+ selectDrawing(payload.items[0].id)
212
+ }
213
+ return
214
+ }
215
+ if (payload.item) {
216
+ applyDetailPayload(payload)
217
+ }
218
+ }
219
+
220
+ function applyDetailPayload(payload: DetailPayload) {
221
+ setDetail(payload)
222
+ const drawingId = payload.item?.id || ''
223
+ selectedIdRef.current = drawingId
224
+ setSelectedId(drawingId)
225
+ setDirty(false)
226
+ setChangeSummary('')
227
+ const version = payload.currentVersion || null
228
+ const nextXml = typeof version?.xml === 'string' && version.xml.trim() ? version.xml : EMPTY_DRAWIO_XML
229
+ const nextMermaid = typeof version?.mermaidSource === 'string' ? version.mermaidSource : ''
230
+ setXml(nextXml)
231
+ setMermaidSource(nextMermaid)
232
+ if (editorReady) {
233
+ loadVersionIntoEditor(version, payload.item?.title)
234
+ }
235
+ }
236
+
237
+ async function reloadAfterHostEvent(event: unknown) {
238
+ const toolName = extractToolNameFromHostEvent(event)
239
+ if (toolName && !DRAWIO_TOOL_NAMES.has(toolName)) {
240
+ return
241
+ }
242
+
243
+ const sequence = ++hostEventSequenceRef.current
244
+ const eventDrawingId = extractDrawingIdFromHostEvent(event)
245
+ const items = await reloadList()
246
+ if (sequence !== hostEventSequenceRef.current) {
247
+ return
248
+ }
249
+
250
+ const nextDrawingId = eventDrawingId ?? selectedIdRef.current ?? items[0]?.id
251
+ if (nextDrawingId) {
252
+ await selectDrawing(nextDrawingId)
253
+ }
254
+
255
+ if (!toolName || DRAWIO_MUTATION_TOOL_NAMES.has(toolName)) {
256
+ notify('info', createTranslator(contextRef.current?.locale)('agentDrawingUpdated'))
257
+ }
258
+ }
259
+
260
+ async function reloadList(overrides: Partial<{ search: string; status: StatusFilter }> = {}) {
261
+ const nextSearch = overrides.search ?? searchRef.current
262
+ const nextStatus = overrides.status ?? statusRef.current
263
+ setBusy(true)
264
+ try {
265
+ const response = await requestData({
266
+ page: 1,
267
+ pageSize: 50,
268
+ search: nextSearch,
269
+ parameters: {
270
+ ...(nextStatus ? { status: nextStatus } : {})
271
+ }
272
+ })
273
+ const payload = getResponsePayload(response) || {}
274
+ const items = Array.isArray(payload.items) ? payload.items : []
275
+ setDrawings(items)
276
+ if (!selectedIdRef.current && items[0]?.id) {
277
+ await selectDrawing(items[0].id)
278
+ }
279
+ return items
280
+ } catch (error) {
281
+ notify('error', getErrorMessage(error))
282
+ return []
283
+ } finally {
284
+ setBusy(false)
285
+ }
286
+ }
287
+
288
+ async function selectDrawing(drawingId: string): Promise<DetailPayload | null> {
289
+ if (!drawingId) {
290
+ return null
291
+ }
292
+ setBusy(true)
293
+ try {
294
+ const response = await requestData({ parameters: { drawingId } })
295
+ const payload = getResponsePayload(response) || {}
296
+ applyDetailPayload(payload)
297
+ return payload
298
+ } catch (error) {
299
+ notify('error', getErrorMessage(error))
300
+ return null
301
+ } finally {
302
+ setBusy(false)
303
+ }
304
+ }
305
+
306
+ async function createDrawing() {
307
+ const title = newTitle.trim() || t('untitled')
308
+ setBusy(true)
309
+ try {
310
+ const response = await executeAction('create_drawing', null, {
311
+ title,
312
+ description: newDescription
313
+ })
314
+ const result = getResponsePayload(response)
315
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('drawingCreated'))
316
+ const drawingId = result?.item?.id || result?.data?.item?.id
317
+ setNewTitle('')
318
+ setNewDescription('')
319
+ setChangeSummary('')
320
+ setXml(EMPTY_DRAWIO_XML)
321
+ setMermaidSource('')
322
+ setDirty(false)
323
+ if (drawingId) {
324
+ await reloadList()
325
+ await selectDrawing(drawingId)
326
+ } else {
327
+ await reloadList()
328
+ }
329
+ } catch (error) {
330
+ notify('error', getErrorMessage(error))
331
+ } finally {
332
+ setBusy(false)
333
+ }
334
+ }
335
+
336
+ async function requestEditorSave(sourceAction = 'save_scene_version') {
337
+ if (!selectedId) {
338
+ notify('warning', t('noDrawing'))
339
+ return
340
+ }
341
+ if (!editorReady) {
342
+ await saveCurrentScene(xml, sourceAction)
343
+ return
344
+ }
345
+ pendingSaveActionRef.current = sourceAction
346
+ postToEditor({ action: 'save' })
347
+ }
348
+
349
+ async function saveCurrentScene(xmlValue: string, sourceAction = 'save_scene_version') {
350
+ if (!selectedId) {
351
+ notify('warning', t('noDrawing'))
352
+ return
353
+ }
354
+ setBusy(true)
355
+ try {
356
+ const response = await executeAction(sourceAction, selectedId, {
357
+ drawingId: selectedId,
358
+ xml: xmlValue,
359
+ mermaidSource,
360
+ descriptor: mermaidSource.trim() ? { format: 'mermaid', data: mermaidSource.trim() } : undefined,
361
+ changeSummary: changeSummary.trim() || undefined
362
+ })
363
+ const result = getResponsePayload(response)
364
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
365
+ setDirty(false)
366
+ setChangeSummary('')
367
+ await selectDrawing(selectedId)
368
+ await reloadList()
369
+ } catch (error) {
370
+ notify('error', getErrorMessage(error))
371
+ } finally {
372
+ setBusy(false)
373
+ }
374
+ }
375
+
376
+ async function restoreVersion(versionId: string) {
377
+ if (!selectedId || !versionId) {
378
+ return
379
+ }
380
+ setBusy(true)
381
+ try {
382
+ const response = await executeAction('restore_version', selectedId, {
383
+ drawingId: selectedId,
384
+ versionId,
385
+ changeSummary: changeSummary.trim() || undefined
386
+ })
387
+ const result = getResponsePayload(response)
388
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
389
+ await selectDrawing(selectedId)
390
+ await reloadList()
391
+ } catch (error) {
392
+ notify('error', getErrorMessage(error))
393
+ } finally {
394
+ setBusy(false)
395
+ }
396
+ }
397
+
398
+ async function archiveDrawing() {
399
+ if (!selectedId) {
400
+ return
401
+ }
402
+ setBusy(true)
403
+ try {
404
+ await executeAction('archive_drawing', selectedId, { drawingId: selectedId })
405
+ notify('success', t('operationCompleted'))
406
+ setDetail(null)
407
+ setSelectedId('')
408
+ setXml(EMPTY_DRAWIO_XML)
409
+ await reloadList()
410
+ } catch (error) {
411
+ notify('error', getErrorMessage(error))
412
+ } finally {
413
+ setBusy(false)
414
+ }
415
+ }
416
+
417
+ async function setDrawingReviewStatus(nextStatus: 'draft' | 'reviewed') {
418
+ if (!selectedId) {
419
+ return
420
+ }
421
+ setBusy(true)
422
+ try {
423
+ const response = await executeAction(nextStatus === 'reviewed' ? 'mark_reviewed' : 'mark_draft', selectedId, {
424
+ drawingId: selectedId,
425
+ reason: changeSummary.trim() || undefined
426
+ })
427
+ const result = getResponsePayload(response)
428
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
429
+ setChangeSummary('')
430
+ await selectDrawing(selectedId)
431
+ await reloadList(statusRef.current && statusRef.current !== nextStatus ? { status: '' } : {})
432
+ if (statusRef.current && statusRef.current !== nextStatus) {
433
+ statusRef.current = ''
434
+ setStatus('')
435
+ }
436
+ } catch (error) {
437
+ notify('error', getErrorMessage(error))
438
+ } finally {
439
+ setBusy(false)
440
+ }
441
+ }
442
+
443
+ function loadMermaidIntoEditor() {
444
+ const source = mermaidSource.trim()
445
+ if (!source) {
446
+ return
447
+ }
448
+ postToEditor({
449
+ action: 'load',
450
+ descriptor: { format: 'mermaid', data: source },
451
+ sourceMetadata: { key: 'mermaidSource', value: source },
452
+ title: detail?.item?.title || newTitle || t('untitled'),
453
+ modified: 0,
454
+ noExitBtn: 1,
455
+ saveAndExit: 0,
456
+ exportProtocol: true,
457
+ dark: isDarkTheme(contextRef.current?.theme) ? 1 : 0
458
+ })
459
+ setDirty(true)
460
+ }
461
+
462
+ async function sendAssistantPrompt() {
463
+ const prompt = assistantPrompt.trim()
464
+ if (!prompt) {
465
+ return
466
+ }
467
+ setBusy(true)
468
+ try {
469
+ const response = await executeAction('prepare_agent_draw_message', selectedId || null, {
470
+ drawingId: selectedId || undefined,
471
+ prompt
472
+ })
473
+ const result = getResponsePayload(response)
474
+ const commandKey = result?.data?.commandKey || result?.commandKey
475
+ const payload = result?.data?.payload || result?.payload
476
+ if (commandKey && payload) {
477
+ await invokeClientCommand(commandKey, payload)
478
+ }
479
+ setAssistantPrompt('')
480
+ notify('success', t('operationCompleted'))
481
+ } catch (error) {
482
+ notify('error', getErrorMessage(error))
483
+ } finally {
484
+ setBusy(false)
485
+ }
486
+ }
487
+
488
+ async function importFile(file: File | null) {
489
+ if (!file) {
490
+ return
491
+ }
492
+ setBusy(true)
493
+ try {
494
+ const response = await executeFileAction(
495
+ 'import_scene_file',
496
+ selectedId || null,
497
+ {
498
+ drawingId: selectedId || undefined,
499
+ title: removeDrawioExtension(file.name)
500
+ },
501
+ { drawingId: selectedId || undefined },
502
+ file
503
+ )
504
+ const result = getResponsePayload(response)
505
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
506
+ const drawingId = result?.data?.item?.id || result?.item?.id || selectedId
507
+ await reloadList()
508
+ if (drawingId) {
509
+ await selectDrawing(drawingId)
510
+ }
511
+ } catch (error) {
512
+ notify('error', getErrorMessage(error))
513
+ } finally {
514
+ setBusy(false)
515
+ if (fileInputRef.current) {
516
+ fileInputRef.current.value = ''
517
+ }
518
+ }
519
+ }
520
+
521
+ function exportXml() {
522
+ downloadBlob(new Blob([xml || EMPTY_DRAWIO_XML], { type: 'application/xml' }), `${detail?.item?.title || 'diagram'}.drawio`)
523
+ }
524
+
525
+ function loadCurrentSceneIntoEditor() {
526
+ loadVersionIntoEditor(detail?.currentVersion || null, detail?.item?.title)
527
+ }
528
+
529
+ function loadVersionIntoEditor(version: DrawingVersion | null, title?: string) {
530
+ const versionXml = typeof version?.xml === 'string' && version.xml.trim() ? version.xml : ''
531
+ const versionMermaid = typeof version?.mermaidSource === 'string' ? version.mermaidSource.trim() : ''
532
+ const loadMessage: Record<string, unknown> = {
533
+ action: 'load',
534
+ title: title || t('untitled'),
535
+ modified: 0,
536
+ noExitBtn: 1,
537
+ saveAndExit: 0,
538
+ exportProtocol: true,
539
+ dark: isDarkTheme(contextRef.current?.theme) ? 1 : 0
540
+ }
541
+ if (versionXml) {
542
+ loadMessage.xml = versionXml
543
+ } else if (versionMermaid) {
544
+ loadMessage.descriptor = { format: 'mermaid', data: versionMermaid }
545
+ loadMessage.sourceMetadata = { key: 'mermaidSource', value: versionMermaid }
546
+ } else {
547
+ loadMessage.xml = EMPTY_DRAWIO_XML
548
+ }
549
+ postToEditor(loadMessage)
550
+ }
551
+
552
+ function postToEditor(message: Record<string, unknown>) {
553
+ iframeRef.current?.contentWindow?.postMessage(JSON.stringify(message), DRAWIO_ORIGIN)
554
+ }
555
+
556
+ const currentVersion = detail?.currentVersion || null
557
+ const drawingStatus = (detail?.item?.status || 'draft') as StatusFilter
558
+ const shellClassName = `dw-shell ${leftPanelCollapsed ? 'left-collapsed' : ''} ${rightPanelCollapsed ? 'right-collapsed' : ''}`
559
+
560
+ return (
561
+ <div className={shellClassName}>
562
+ <Sidebar className="dw-sidebar" side="left" collapsed={leftPanelCollapsed}>
563
+ <SidebarHeader>
564
+ <SidebarTrigger
565
+ variant="ghost"
566
+ size="icon"
567
+ aria-label={leftPanelCollapsed ? t('expandDrawings') : t('collapseDrawings')}
568
+ title={leftPanelCollapsed ? t('expandDrawings') : t('collapseDrawings')}
569
+ onClick={() => setLeftPanelCollapsed((value) => !value)}
570
+ >
571
+ {leftPanelCollapsed ? <PanelLeftOpen className="dw-button-icon" aria-hidden="true" /> : <PanelLeftClose className="dw-button-icon" aria-hidden="true" />}
572
+ </SidebarTrigger>
573
+ {!leftPanelCollapsed ? (
574
+ <>
575
+ <SidebarTitle>{t('drawings')}</SidebarTitle>
576
+ <Badge variant="secondary">{drawings.length}</Badge>
577
+ </>
578
+ ) : null}
579
+ </SidebarHeader>
580
+ {leftPanelCollapsed ? (
581
+ <SidebarRail><span>{t('drawings')}</span></SidebarRail>
582
+ ) : (
583
+ <SidebarContent>
584
+ <div className="dw-sidebar-controls">
585
+ <Input
586
+ value={search}
587
+ placeholder={t('search')}
588
+ onChange={(event: any) => {
589
+ const next = event.target.value
590
+ searchRef.current = next
591
+ setSearch(next)
592
+ reloadList({ search: next })
593
+ }}
594
+ />
595
+ <Select
596
+ value={status || 'all'}
597
+ onValueChange={(value: string) => {
598
+ const next = value === 'all' ? '' : (value as StatusFilter)
599
+ statusRef.current = next
600
+ setStatus(next)
601
+ reloadList({ status: next })
602
+ }}
603
+ >
604
+ <SelectTrigger aria-label={t('allStatuses')}>
605
+ <SelectValue placeholder={t('allStatuses')} />
606
+ </SelectTrigger>
607
+ <SelectContent>
608
+ <SelectItem value="all">{t('allStatuses')}</SelectItem>
609
+ <SelectItem value="draft">{t('draft')}</SelectItem>
610
+ <SelectItem value="reviewed">{t('reviewed')}</SelectItem>
611
+ <SelectItem value="archived">{t('archived')}</SelectItem>
612
+ </SelectContent>
613
+ </Select>
614
+ </div>
615
+ <ScrollArea className="dw-list">
616
+ <SidebarMenu>
617
+ {drawings.map((drawing) => (
618
+ <SidebarMenuItem key={drawing.id}>
619
+ <SidebarMenuButton type="button" active={drawing.id === selectedId} onClick={() => selectDrawing(drawing.id)}>
620
+ <span className="dw-item-title">{drawing.title || t('untitled')}</span>
621
+ <span className="dw-item-meta">v{drawing.currentVersionNumber || 0} · {t((drawing.status || 'draft') as TranslationKey)}</span>
622
+ </SidebarMenuButton>
623
+ </SidebarMenuItem>
624
+ ))}
625
+ </SidebarMenu>
626
+ </ScrollArea>
627
+ </SidebarContent>
628
+ )}
629
+ </Sidebar>
630
+
631
+ <main className="dw-main">
632
+ <div className="dw-toolbar">
633
+ <div className="dw-toolbar-title">
634
+ <Input className="dw-title-input" value={newTitle} placeholder={t('title')} onChange={(event: any) => setNewTitle(event.target.value)} />
635
+ </div>
636
+ <div className="dw-toolbar-actions">
637
+ <Button type="button" variant="outline" size="sm" disabled={busy} onClick={createDrawing}>
638
+ <Plus className="dw-button-icon" aria-hidden="true" />
639
+ {t('newDrawing')}
640
+ </Button>
641
+ <Button type="button" size="sm" disabled={busy || !selectedId} onClick={() => requestEditorSave()}>
642
+ <Save className="dw-button-icon" aria-hidden="true" />
643
+ {t('save')}
644
+ </Button>
645
+ <Button type="button" variant="outline" size="sm" disabled={busy || !selectedId || !editorReady} onClick={() => postToEditor({ action: 'save' })}>
646
+ <FileJson className="dw-button-icon" aria-hidden="true" />
647
+ {t('syncEditor')}
648
+ </Button>
649
+ <Button type="button" variant="outline" size="sm" disabled={busy} onClick={() => fileInputRef.current?.click()}>
650
+ <Upload className="dw-button-icon" aria-hidden="true" />
651
+ {t('import')}
652
+ </Button>
653
+ <Button type="button" variant="outline" size="sm" disabled={!selectedId} onClick={exportXml}>
654
+ <FileJson className="dw-button-icon" aria-hidden="true" />
655
+ {t('exportXml')}
656
+ </Button>
657
+ <Badge className="dw-status" variant={dirty ? 'warning' : 'secondary'}>{dirty ? t('dirty') : t('saved')}</Badge>
658
+ <Badge variant={editorReady ? 'secondary' : 'outline'}>{editorReady ? t('editorReady') : t('editorLoading')}</Badge>
659
+ </div>
660
+ <input
661
+ ref={fileInputRef}
662
+ className="dw-hidden-file"
663
+ type="file"
664
+ accept=".drawio,.diagram,.xml,.svg,.json,application/xml,text/xml,application/json"
665
+ onChange={(event: any) => importFile(event.target.files?.[0] || null)}
666
+ />
667
+ </div>
668
+ <div className="dw-editor">
669
+ {selectedId || currentVersion ? (
670
+ <>
671
+ <iframe ref={iframeRef} title="draw.io editor" src={DRAWIO_EDITOR_URL} />
672
+ {!editorReady ? <div className="dw-editor-placeholder">{t('editorLoading')}</div> : null}
673
+ </>
674
+ ) : (
675
+ <div className="dw-empty">{t('noDrawing')}</div>
676
+ )}
677
+ </div>
678
+ </main>
679
+
680
+ <Sidebar className="dw-inspector" side="right" collapsed={rightPanelCollapsed}>
681
+ <SidebarHeader>
682
+ {!rightPanelCollapsed ? (
683
+ <>
684
+ <div className="dw-inspector-actions">
685
+ {drawingStatus === 'archived' ? (
686
+ <Badge variant="secondary">{t('archived')}</Badge>
687
+ ) : drawingStatus === 'reviewed' ? (
688
+ <Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDrawingReviewStatus('draft')}>
689
+ <RotateCcw className="dw-button-icon" aria-hidden="true" />
690
+ {t('backToDraft')}
691
+ </Button>
692
+ ) : (
693
+ <Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDrawingReviewStatus('reviewed')}>
694
+ <Check className="dw-button-icon" aria-hidden="true" />
695
+ {t('markReviewed')}
696
+ </Button>
697
+ )}
698
+ <Button type="button" variant="destructiveOutline" size="sm" disabled={busy || !selectedId || drawingStatus === 'archived'} onClick={archiveDrawing}>
699
+ <Archive className="dw-button-icon" aria-hidden="true" />
700
+ {t('archive')}
701
+ </Button>
702
+ </div>
703
+ <SidebarTitle className="dw-sidebar-title-truncate">{detail?.item?.title || t('inspector')}</SidebarTitle>
704
+ </>
705
+ ) : null}
706
+ <SidebarTrigger
707
+ className="dw-sidebar-trigger-right"
708
+ variant="ghost"
709
+ size="icon"
710
+ aria-label={rightPanelCollapsed ? t('expandInspector') : t('collapseInspector')}
711
+ title={rightPanelCollapsed ? t('expandInspector') : t('collapseInspector')}
712
+ onClick={() => setRightPanelCollapsed((value) => !value)}
713
+ >
714
+ {rightPanelCollapsed ? <PanelRightOpen className="dw-button-icon" aria-hidden="true" /> : <PanelRightClose className="dw-button-icon" aria-hidden="true" />}
715
+ </SidebarTrigger>
716
+ </SidebarHeader>
717
+ {rightPanelCollapsed ? (
718
+ <SidebarRail><span>{t('inspector')}</span></SidebarRail>
719
+ ) : (
720
+ <SidebarContent>
721
+ <ScrollArea className="dw-inspector-scroll">
722
+ <div className="dw-inspector-stack">
723
+ <section className="dw-section">
724
+ <div className="dw-section-title">{t('changeSummary')}</div>
725
+ <Input value={changeSummary} placeholder={t('changeSummary')} onChange={(event: any) => setChangeSummary(event.target.value)} />
726
+ </section>
727
+
728
+ <section className="dw-section">
729
+ <div className="dw-section-title">{t('versions')}</div>
730
+ {(detail?.versions || []).map((version) => (
731
+ <div className="dw-version" key={version.id}>
732
+ <div>
733
+ <div>v{version.versionNumber}</div>
734
+ <div className="dw-muted">{version.sourceType || 'workbench'}</div>
735
+ </div>
736
+ <Button className="dw-version-action" type="button" variant="outline" size="icon" title={t('restore')} aria-label={`${t('restore')} v${version.versionNumber}`} disabled={busy} onClick={() => restoreVersion(version.id)}>
737
+ <RotateCcw className="dw-button-icon" aria-hidden="true" />
738
+ </Button>
739
+ </div>
740
+ ))}
741
+ </section>
742
+
743
+ <section className="dw-section">
744
+ <div className="dw-section-title">{t('mermaid')}</div>
745
+ <Textarea value={mermaidSource} onChange={(event: any) => {
746
+ setMermaidSource(event.target.value)
747
+ setDirty(true)
748
+ }} />
749
+ <div className="dw-muted">{t('mermaidNotice')}</div>
750
+ <div className="dw-inline-actions">
751
+ <Button type="button" variant="outline" size="sm" disabled={busy || !editorReady || !mermaidSource.trim()} onClick={loadMermaidIntoEditor}>
752
+ {t('loadMermaid')}
753
+ </Button>
754
+ <Button type="button" size="sm" disabled={busy || !selectedId} onClick={() => requestEditorSave('save_converted_mermaid_scene')}>
755
+ {t('saveConverted')}
756
+ </Button>
757
+ </div>
758
+ </section>
759
+
760
+ <section className="dw-section">
761
+ <div className="dw-section-title">{t('drawingRequest')}</div>
762
+ <Textarea value={assistantPrompt} placeholder={t('drawingRequest')} onChange={(event: any) => setAssistantPrompt(event.target.value)} />
763
+ <Button type="button" disabled={busy || !assistantPrompt.trim()} onClick={sendAssistantPrompt}>
764
+ <Send className="dw-button-icon" aria-hidden="true" />
765
+ {t('askAssistant')}
766
+ </Button>
767
+ </section>
768
+
769
+ <section className="dw-section">
770
+ <div className="dw-section-title">{t('description')}</div>
771
+ <Textarea value={newDescription} placeholder={t('description')} onChange={(event: any) => setNewDescription(event.target.value)} />
772
+ </section>
773
+ </div>
774
+ </ScrollArea>
775
+ </SidebarContent>
776
+ )}
777
+ </Sidebar>
778
+ </div>
779
+ )
780
+ }
781
+
782
+ function parseMessage(data: unknown) {
783
+ if (typeof data === 'string') {
784
+ try {
785
+ return JSON.parse(data)
786
+ } catch {
787
+ return null
788
+ }
789
+ }
790
+ return data && typeof data === 'object' && !Array.isArray(data) ? data as Record<string, any> : null
791
+ }
792
+
793
+ function isDarkTheme(theme: unknown) {
794
+ if (typeof theme === 'boolean') {
795
+ return theme
796
+ }
797
+ if (typeof theme === 'string') {
798
+ return /dark|night/i.test(theme)
799
+ }
800
+ return Boolean(theme && typeof theme === 'object' && ((theme as any).dark === true || (theme as any).isDark === true))
801
+ }
802
+
803
+ function extractToolNameFromHostEvent(event: unknown) {
804
+ for (const candidate of expandHostEventCandidates(event)) {
805
+ if (!isObject(candidate)) {
806
+ continue
807
+ }
808
+ const direct = readString(candidate, 'toolName') ?? readString(candidate, 'tool_name') ?? readString(candidate, 'name')
809
+ if (direct && DRAWIO_TOOL_NAMES.has(direct)) {
810
+ return direct
811
+ }
812
+ const tool = candidate.tool
813
+ if (isObject(tool)) {
814
+ const toolName = readString(tool, 'name') ?? readString(tool, 'toolName') ?? readString(tool, 'tool_name')
815
+ if (toolName && DRAWIO_TOOL_NAMES.has(toolName)) {
816
+ return toolName
817
+ }
818
+ }
819
+ }
820
+ return null
821
+ }
822
+
823
+ function extractDrawingIdFromHostEvent(event: unknown) {
824
+ for (const candidate of expandHostEventCandidates(event)) {
825
+ if (!isObject(candidate)) {
826
+ continue
827
+ }
828
+ const direct = readString(candidate, 'drawingId') ?? readString(candidate, 'drawing_id')
829
+ if (direct) {
830
+ return direct
831
+ }
832
+ if (isObject(candidate.item)) {
833
+ const itemId = readString(candidate.item, 'id')
834
+ if (itemId) {
835
+ return itemId
836
+ }
837
+ }
838
+ if (isObject(candidate.drawing)) {
839
+ const drawingId = readString(candidate.drawing, 'drawingId') ?? readString(candidate.drawing, 'drawing_id') ?? readString(candidate.drawing, 'id')
840
+ if (drawingId) {
841
+ return drawingId
842
+ }
843
+ }
844
+ if (isObject(candidate.version)) {
845
+ const drawingId = readString(candidate.version, 'drawingId') ?? readString(candidate.version, 'drawing_id')
846
+ if (drawingId) {
847
+ return drawingId
848
+ }
849
+ }
850
+ }
851
+ return null
852
+ }
853
+
854
+ function expandHostEventCandidates(event: unknown) {
855
+ const candidates: unknown[] = []
856
+ collectHostEventCandidates(event, candidates, 0)
857
+ return candidates
858
+ }
859
+
860
+ function collectHostEventCandidates(value: unknown, candidates: unknown[], depth: number) {
861
+ if (depth > 5 || value == null) {
862
+ return
863
+ }
864
+ const normalized = parseJsonLike(value)
865
+ candidates.push(normalized)
866
+ if (Array.isArray(normalized)) {
867
+ normalized.forEach((item) => collectHostEventCandidates(item, candidates, depth + 1))
868
+ return
869
+ }
870
+ if (!isObject(normalized)) {
871
+ return
872
+ }
873
+ ;['payload', 'metadata', 'data', 'result', 'output', 'content', 'message', 'detail', 'response', 'tool', 'toolCall', 'tool_call', 'function', 'arguments', 'args', 'input'].forEach((key) => collectHostEventCandidates(normalized[key], candidates, depth + 1))
874
+ }
875
+
876
+ function parseJsonLike(value: unknown) {
877
+ if (typeof value !== 'string') {
878
+ return value
879
+ }
880
+ const trimmed = value.trim()
881
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
882
+ return value
883
+ }
884
+ try {
885
+ return JSON.parse(trimmed)
886
+ } catch {
887
+ return value
888
+ }
889
+ }
890
+
891
+ function isObject(value: unknown): value is Record<string, unknown> {
892
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value))
893
+ }
894
+
895
+ function readString(record: Record<string, unknown>, key: string) {
896
+ const value = record[key]
897
+ return typeof value === 'string' && value.trim() ? value.trim() : null
898
+ }
899
+
900
+ function removeDrawioExtension(name: string) {
901
+ return name.replace(/\.(drawio|diagram|xml)(?:\.json)?$/i, '').replace(/\.json$/i, '') || name
902
+ }
903
+
904
+ function downloadBlob(blob: Blob, fileName: string) {
905
+ const url = URL.createObjectURL(blob)
906
+ const anchor = document.createElement('a')
907
+ anchor.href = url
908
+ anchor.download = fileName
909
+ document.body.appendChild(anchor)
910
+ anchor.click()
911
+ anchor.remove()
912
+ URL.revokeObjectURL(url)
913
+ }
914
+
915
+ const root = ReactDOM.createRoot(document.getElementById('root')!)
916
+ root.render(<App />)