diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 750c7495..eea21481 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -25,6 +25,7 @@ import { init as initAgentRunner } from "@x/core/dist/agent-schedule/runner.js"; import { init as initAgentNotes } from "@x/core/dist/knowledge/agent_notes.js"; import { init as initTrackScheduler } from "@x/core/dist/knowledge/track/scheduler.js"; import { init as initTrackEventProcessor } from "@x/core/dist/knowledge/track/events.js"; +import { init as initLocalSites, shutdown as shutdownLocalSites } from "@x/core/dist/local-sites/server.js"; import { initConfigs } from "@x/core/dist/config/initConfigs.js"; import started from "electron-squirrel-startup"; @@ -291,6 +292,11 @@ app.whenReady().then(async () => { // start chrome extension sync server initChromeSync(); + // start local sites server for iframe dashboards and other mini apps + initLocalSites().catch((error) => { + console.error('[LocalSites] Failed to start:', error); + }); + app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); @@ -309,4 +315,7 @@ app.on("before-quit", () => { stopWorkspaceWatcher(); stopRunsWatcher(); stopServicesWatcher(); + shutdownLocalSites().catch((error) => { + console.error('[LocalSites] Failed to shut down cleanly:', error); + }); }); diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx index 3d22c646..49915dc0 100644 --- a/apps/x/apps/renderer/src/components/markdown-editor.tsx +++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx @@ -13,6 +13,7 @@ import { TrackBlockExtension } from '@/extensions/track-block' import { TrackTargetOpenExtension, TrackTargetCloseExtension } from '@/extensions/track-target' import { ImageBlockExtension } from '@/extensions/image-block' import { EmbedBlockExtension } from '@/extensions/embed-block' +import { IframeBlockExtension } from '@/extensions/iframe-block' import { ChartBlockExtension } from '@/extensions/chart-block' import { TableBlockExtension } from '@/extensions/table-block' import { CalendarBlockExtension } from '@/extensions/calendar-block' @@ -177,6 +178,8 @@ function blockToMarkdown(node: JsonNode): string { return '```image\n' + (node.attrs?.data as string || '{}') + '\n```' case 'embedBlock': return '```embed\n' + (node.attrs?.data as string || '{}') + '\n```' + case 'iframeBlock': + return '```iframe\n' + (node.attrs?.data as string || '{}') + '\n```' case 'chartBlock': return '```chart\n' + (node.attrs?.data as string || '{}') + '\n```' case 'tableBlock': @@ -676,6 +679,7 @@ export const MarkdownEditor = forwardRef }; deleteNode: () => void }) { + const raw = node.attrs.data as string + let config: blocks.IframeBlock | null = null + + try { + config = blocks.IframeBlockSchema.parse(JSON.parse(raw)) + } catch { + // fallback below + } + + if (!config) { + return ( + +
+ + Invalid iframe block +
+
+ ) + } + + const visibleTitle = config.title?.trim() || '' + const title = visibleTitle || 'Embedded page' + const allow = config.allow || DEFAULT_IFRAME_ALLOW + const initialHeight = config.height ?? DEFAULT_IFRAME_HEIGHT + const [frameHeight, setFrameHeight] = useState(() => readCachedIframeHeight(config.url, initialHeight)) + const [frameReady, setFrameReady] = useState(false) + const iframeRef = useRef(null) + const loadFallbackTimerRef = useRef(null) + const autoResizeReadyTimerRef = useRef(null) + const frameReadyRef = useRef(false) + + useEffect(() => { + setFrameHeight(readCachedIframeHeight(config.url, initialHeight)) + setFrameReady(false) + frameReadyRef.current = false + if (loadFallbackTimerRef.current !== null) { + window.clearTimeout(loadFallbackTimerRef.current) + loadFallbackTimerRef.current = null + } + if (autoResizeReadyTimerRef.current !== null) { + window.clearTimeout(autoResizeReadyTimerRef.current) + autoResizeReadyTimerRef.current = null + } + }, [config.url, initialHeight, raw]) + + useEffect(() => { + frameReadyRef.current = frameReady + }, [frameReady]) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const iframeWindow = iframeRef.current?.contentWindow + if (!iframeWindow || event.source !== iframeWindow) return + + const message = parseIframeHeightMessage(event) + if (!message) return + + if (loadFallbackTimerRef.current !== null) { + window.clearTimeout(loadFallbackTimerRef.current) + loadFallbackTimerRef.current = null + } + if (autoResizeReadyTimerRef.current !== null) { + window.clearTimeout(autoResizeReadyTimerRef.current) + } + writeCachedIframeHeight(config.url, message.height) + setFrameHeight((currentHeight) => ( + Math.abs(currentHeight - message.height) < HEIGHT_UPDATE_THRESHOLD ? currentHeight : message.height + )) + + if (!frameReadyRef.current) { + autoResizeReadyTimerRef.current = window.setTimeout(() => { + setFrameReady(true) + frameReadyRef.current = true + autoResizeReadyTimerRef.current = null + }, AUTO_RESIZE_SETTLE_MS) + } + } + + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, [config.url]) + + useEffect(() => { + return () => { + if (loadFallbackTimerRef.current !== null) { + window.clearTimeout(loadFallbackTimerRef.current) + } + if (autoResizeReadyTimerRef.current !== null) { + window.clearTimeout(autoResizeReadyTimerRef.current) + } + } + }, []) + + return ( + +
+ + {visibleTitle &&
{visibleTitle}
} +
+ {!frameReady && ( +