Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a41b1a0
feat(mcp): implement wireframe tools and services
Ivanruii Apr 17, 2026
22c7cc9
feat(vscode-extension): initial migration and integration
Ivanruii Apr 17, 2026
4656961
feat(vscode-extension): implement script to copy MCP build output
Ivanruii Apr 17, 2026
45f329f
feat(vscode-extension): refactor app URL handling to use environment …
Ivanruii Apr 17, 2026
d6d3f5c
feat(mcp): update type imports and enhance data URL parsing logic
Ivanruii Apr 17, 2026
17c754d
feat: implement quickmock bridge protocol and integrate VSCode extens…
Ivanruii Apr 17, 2026
274eed8
feat(vscode-extension): update VSCode URL and adjust type dependencies
Ivanruii Apr 17, 2026
927a782
feat(vscode-extension): add environment variable for QM_APP_URL in la…
Ivanruii Apr 17, 2026
dc43f2b
feat: bundle Chromium, share app URL via ~/.quickmock, and reorganize…
Ivanruii Apr 20, 2026
d16ec66
style: standardize code formatting
Ivanruii Apr 20, 2026
2282316
chore(changeset): initial 0.1.0 release
Ivanruii Apr 20, 2026
471bd68
feat(vscode-extension): include QM_APP_ORIGIN in bridge server for if…
Ivanruii Apr 20, 2026
189d822
feat(mcp): include QM_APP_ORIGIN in postMessage for iframe file loading
Ivanruii Apr 20, 2026
bb0a432
refactor(headless.renderer): remove unused browser launch arguments
Ivanruii Apr 20, 2026
84c1797
feat: implement quickmock registry protocol and integrate with mcp an…
Ivanruii Apr 20, 2026
683b438
feat(vscode-extension): add sandbox attribute to iframe for allow png…
Ivanruii Apr 20, 2026
262935c
fix(mcp): correct log level from error to info in logInfo function
Ivanruii Apr 20, 2026
9bcee08
fix(mcp): simplify server entry retrieval in cleanupStaleMcpRegistration
Ivanruii Apr 21, 2026
0725a11
feat(bridge-protocol, registry-protocol): add message types and const…
nasdan Apr 21, 2026
6af786a
feat(vscode-extension): update package configuration for module suppo…
nasdan Apr 21, 2026
4325fa2
feat(mcp, vscode-extension): update dependencies and remove copy scri…
nasdan Apr 21, 2026
31ef351
feat(config): update storage path and authentication settings for loc…
Ivanruii Apr 22, 2026
07ebfb2
feat(mcp, vscode-extension): deliver MCP via npx instead of bundling …
Ivanruii Apr 22, 2026
f6cddb6
feat(vscode-extension): add packaging script for VSCode extension
Ivanruii Apr 24, 2026
b5ded2c
feat(dev-cli, publish): add TypeScript configuration and update devDe…
nasdan Apr 24, 2026
9c4cdd9
feat(vscode-theming): implement theme synchronization and update styl…
Ivanruii Apr 24, 2026
7820721
feat(vscode-extension): remove alwaysBundle configuration for MCP in …
Ivanruii Apr 24, 2026
c3dd8b4
fix(vscode-extension): specify exact version for quickmock-mcp depend…
Ivanruii Apr 24, 2026
f1d53e1
feat(vscode-extension): add barrel files
Ivanruii Apr 24, 2026
2c098d2
refactor(vscode-extension): restructure mcp module
Ivanruii Apr 28, 2026
1cc4bc7
chore(changelog): remove 0.1.0 release notes for VSCode extension and…
Ivanruii Apr 29, 2026
4f6612e
fix(vscode-extension): add TODO comment for implementing new wirefram…
Ivanruii Apr 29, 2026
61357bd
feat(vscode-extension): add language support for quickmock with custo…
Ivanruii Apr 29, 2026
df2e92c
refactor(vscode-extension): remove unused exports from mcp index
Ivanruii May 8, 2026
87c2fb2
fix(vscode-extension): ensure all connections are closed when disposi…
Ivanruii May 8, 2026
7d14768
Merge pull request #834 from Lemoncode/refactor/vscode-extension-mcp-…
nasdan May 8, 2026
c0575fe
fix(vscode-theming): refine background CSS variable mapping
Ivanruii May 8, 2026
4ca0046
fix(vscode-theming): debounce theme updates to improve performance
Ivanruii May 8, 2026
c7c082f
fix(vscode-theming): standardize background and foreground style prop…
Ivanruii May 8, 2026
a726ee1
fix(vscode-theming): remove unused cleanup function from setupThemeSync
Ivanruii May 8, 2026
57dc5c1
fix: remove unnecessary file extensions in import statements
Ivanruii May 8, 2026
2a44175
Merge pull request #833 from Lemoncode/feature/vscode-theming
nasdan May 8, 2026
73b29c9
fix(vscode-theming): update postMessage to use resolved parent origin
Ivanruii May 8, 2026
d31a41b
fix(vscode-theming): refactor sendFileToApp to use parsed data directly
Ivanruii May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/quick-meals-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'quickmock': minor
'@lemoncode/quickmock-mcp': minor
---

First public release of the QuickMock VS Code extension and its MCP server.

**`quickmock` (VS Code extension)**

- Custom editor for `.qm` files backed by the QuickMock web app, served inside a webview.

- `quickmock.appUrl` setting (default `https://quickmock.net/editor.html`) to point the editor and the MCP renderer at any QuickMock instance. Changes refresh open editors and respawn the MCP server.

- Automatic MCP server registration for VS Code / GitHub Copilot, Claude Code, Cursor, Windsurf and Claude Desktop, plus a dynamic `McpServerDefinitionProvider`. Existing entries are refreshed on activation so users always end up pointing at the right MCP invocation.

- The MCP server is no longer bundled inside the `.vsix`. In production the extension spawns it on demand via `npx -y @lemoncode/quickmock-mcp`, so users always run the latest published MCP without waiting for an extension release. In development it resolves the local workspace build.

**`@lemoncode/quickmock-mcp` (MCP server)**

- MCP tools to explore and render wireframes: `list_wireframes`, `get_wireframe_json`, `get_wireframe_pages`, `get_wireframe_assets` and `capture_wireframe`.

- Headless screenshot pipeline via `puppeteer-core` against the QuickMock app, using a postMessage bridge.

- On-demand Chromium download via `@puppeteer/browsers`, cached under `~/.quickmock/browsers`, so headless rendering works without relying on the user's local browser install.

- Reads the target app URL from `~/.quickmock/app-url` (written by the extension) with a production fallback, so the MCP works out of the box regardless of how it is spawned.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"editor.codeActionsOnSave": {
"source.organizeImports": "always",
"source.removeUnusedImports": "always"
}
},
"quickmock.appUrl": "http://localhost:5173/editor.html"
Comment on lines +8 to +9
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workspace-level VS Code setting forces quickmock.appUrl to a localhost dev server for everyone who opens the repo. That’s likely to break the extension/editor unless the dev server is running. Consider removing this from source control or moving it to a documented example (e.g. .vscode/settings.example.json).

Suggested change
},
"quickmock.appUrl": "http://localhost:5173/editor.html"
}

Copilot uses AI. Check for mistakes.
}
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.7.10",
"@lemoncode/quickmock-bridge-protocol": "*",
"@fontsource-variable/montserrat": "5.0.20",
"@fontsource/balsamiq-sans": "5.0.21",
"@uiw/react-color-chrome": "2.10.1",
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ModalDialogComponent } from './common/components/modal-dialog';
import { useVSCodeSync } from '#core/vscode/use-vscode-sync.hook';
import { MainScene } from './scenes/main.scene';

function App() {
useVSCodeSync();

return (
<>
<ModalDialogComponent />
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/common/utils/compute-content-bbox.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ContentBbox } from '@lemoncode/quickmock-bridge-protocol';
import type { useCanvasContext } from '#core/providers';

const CONTENT_PADDING = 16;

export function computeContentBbox(
shapes: ReturnType<typeof useCanvasContext>['shapes'],
stageRef: ReturnType<typeof useCanvasContext>['stageRef']
): ContentBbox | undefined {
const stage = stageRef.current;
if (!stage || shapes.length === 0) return undefined;

const scale = stage.scaleX();
const stageX = stage.x();
const stageY = stage.y();
const container = stage.container().getBoundingClientRect();

const minX = Math.min(...shapes.map(s => s.x));
const minY = Math.min(...shapes.map(s => s.y));
const maxX = Math.max(...shapes.map(s => s.x + s.width));
const maxY = Math.max(...shapes.map(s => s.y + s.height));

return {
x: Math.max(0, container.left + stageX + minX * scale - CONTENT_PADDING),
y: Math.max(0, container.top + stageY + minY * scale - CONTENT_PADDING),
width: (maxX - minX) * scale + CONTENT_PADDING * 2,
height: (maxY - minY) * scale + CONTENT_PADDING * 2,
};
}
7 changes: 7 additions & 0 deletions apps/web/src/common/utils/env.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const isVSCodeEnv = (): boolean => {
return new URLSearchParams(window.location.search).get('env') === 'vscode';
};

export const isHeadlessEnv = (): boolean => {
return new URLSearchParams(window.location.search).get('headless') === '1';
};
63 changes: 63 additions & 0 deletions apps/web/src/common/utils/vscode-bridge.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type {
AppMessage,
HostMessage,
PayloadOf,
} from '@lemoncode/quickmock-bridge-protocol';
import { isVSCodeEnv } from './env.utils';

type HandlerFor<T extends HostMessage['type']> = (
payload: PayloadOf<HostMessage, T>
) => void;

type AnyHandler = (payload: unknown) => void;

const handlers = new Map<string, Set<AnyHandler>>();

const resolveParentOrigin = (): string => {
if (!document.referrer) return '*';
try {
return new URL(document.referrer).origin;
} catch {
return '*';
}
};

const parentOrigin = resolveParentOrigin();

export const sendToExtension = (msg: AppMessage): void => {
if (!isVSCodeEnv()) return;
window.parent.postMessage(msg, parentOrigin);
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Seguridad: postMessage con origen comodín *

window.parent.postMessage(msg, "*") enviará el mensaje a cualquier página que sea el padre del iframe — si quickmock.net/editor.html se embebe en un contexto no controlado, el contenido del fichero .qm podría filtrarse al padre malicioso.

Propuesta: inyectar el origen del webview desde la extensión (vía data-parent-origin en el <body>) y usarlo como target:

export const sendToExtension = (msg: AppMessage): void => {
  if (!isVSCodeEnv()) return;
  const parentOrigin = document.body.dataset.parentOrigin ?? "*";
  window.parent.postMessage(msg, parentOrigin);
};

Alternativa más ligera (sin cambiar provider.ts): leer document.referrer, que VS Code sí establece al cargar el iframe:

const parentOrigin = document.referrer
  ? new URL(document.referrer).origin
  : "*"; // fallback solo si no hay referrer
window.parent.postMessage(msg, parentOrigin);


export const onMessage = <T extends HostMessage['type']>(
type: T,
handler: HandlerFor<T>
): (() => void) => {
if (!isVSCodeEnv()) return () => {};

const existing = handlers.get(type) ?? new Set<AnyHandler>();
existing.add(handler as AnyHandler);
handlers.set(type, existing);

return () => {
const set = handlers.get(type);
if (!set) return;
set.delete(handler as AnyHandler);
if (set.size === 0) handlers.delete(type);
};
};

if (isVSCodeEnv()) {
window.addEventListener('message', (event: MessageEvent) => {
if (event.source !== window.parent) return;

const msg = event.data as Partial<HostMessage> | undefined;
if (!msg?.type) return;

const set = handlers.get(msg.type);
if (!set) return;

const payload = (msg as { payload?: unknown }).payload;
for (const handler of set) handler(payload);
});
}
34 changes: 34 additions & 0 deletions apps/web/src/core/vscode/use-headless-render-complete.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { computeContentBbox } from '#common/utils/compute-content-bbox.utils.ts';
import { isHeadlessEnv } from '#common/utils/env.utils.ts';
import { sendToExtension } from '#common/utils/vscode-bridge.utils.ts';
import { useCanvasContext } from '#core/providers';
import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol';
import { useEffect } from 'react';

export function useHeadlessRenderComplete(hasReceivedFileRef: {
current: boolean;
}): void {
const { howManyLoadedDocuments, shapes, stageRef } = useCanvasContext();

useEffect(() => {
if (!isHeadlessEnv() || !hasReceivedFileRef.current) return;

let innerRafId = 0;
// Double rAF: the first frame runs after React commits; the second waits
// for Konva to paint the updated canvas, so Puppeteer's screenshot reflects it.
// There was a previous issue when the canvas was blank because the screenshot ran before Konva painted.
const outerRafId = requestAnimationFrame(() => {
innerRafId = requestAnimationFrame(() => {
sendToExtension({
type: APP_MESSAGE_TYPE.RENDER_COMPLETE,
payload: computeContentBbox(shapes, stageRef),
});
});
});

return () => {
cancelAnimationFrame(outerRafId);
cancelAnimationFrame(innerRafId);
};
}, [howManyLoadedDocuments]);
}
49 changes: 49 additions & 0 deletions apps/web/src/core/vscode/use-vscode-auto-save.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts';
import { sendToExtension } from '#common/utils/vscode-bridge.utils.ts';
import { useCanvasContext } from '#core/providers';
import { APP_MESSAGE_TYPE } from '@lemoncode/quickmock-bridge-protocol';
import { useEffect, useRef } from 'react';
import { serializeDocument } from './vscode-sync.utils';

const AUTO_SAVE_DEBOUNCE_MS = 500;

export function useVSCodeAutoSave(hasReceivedFileRef: {
current: boolean;
}): void {
const { fullDocument, howManyLoadedDocuments } = useCanvasContext();

const prevLoadCountRef = useRef(howManyLoadedDocuments);
const lastSavedContentRef = useRef('');
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
if (!isVSCodeEnv() || isHeadlessEnv() || !hasReceivedFileRef.current)
return;

if (prevLoadCountRef.current !== howManyLoadedDocuments) {
prevLoadCountRef.current = howManyLoadedDocuments;
lastSavedContentRef.current = serializeDocument(fullDocument);
return;
}

const content = serializeDocument(fullDocument);

if (content === lastSavedContentRef.current) return;

debounceTimerRef.current = setTimeout(() => {
sendToExtension({
type: APP_MESSAGE_TYPE.SAVE,
payload: { content },
});
lastSavedContentRef.current = content;
debounceTimerRef.current = null;
}, AUTO_SAVE_DEBOUNCE_MS);

return () => {
if (debounceTimerRef.current !== null) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
};
}, [fullDocument, howManyLoadedDocuments]);
}
52 changes: 52 additions & 0 deletions apps/web/src/core/vscode/use-vscode-file-load.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts';
import {
onMessage,
sendToExtension,
} from '#common/utils/vscode-bridge.utils.ts';
import { QuickMockFileContract } from '#core/local-disk/local-disk.model';
import { useCanvasContext } from '#core/providers';
import {
APP_MESSAGE_TYPE,
HOST_MESSAGE_TYPE,
type LoadFilePayload,
} from '@lemoncode/quickmock-bridge-protocol';
import { useEffect, useRef } from 'react';
import { deserializeDocument } from './vscode-sync.utils';

export function useVSCodeFileLoad(): { current: boolean } {
const { loadDocument, setFileName } = useCanvasContext();

const loadDocumentRef = useRef(loadDocument);
const setFileNameRef = useRef(setFileName);
useEffect(() => {
loadDocumentRef.current = loadDocument;
setFileNameRef.current = setFileName;
});

const hasReceivedFileRef = useRef(false);

useEffect(() => {
if (!isVSCodeEnv()) return;

const unsubscribe = onMessage(
HOST_MESSAGE_TYPE.LOAD_FILE,
(payload: LoadFilePayload) => {
hasReceivedFileRef.current = true;
setFileNameRef.current(payload.fileName);
loadDocumentRef.current(
deserializeDocument(payload.data as QuickMockFileContract)
);
}
);

sendToExtension({
type: isHeadlessEnv()
? APP_MESSAGE_TYPE.READY
: APP_MESSAGE_TYPE.WEBVIEW_READY,
});

return unsubscribe;
}, []);

return hasReceivedFileRef;
}
15 changes: 15 additions & 0 deletions apps/web/src/core/vscode/use-vscode-sync.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useHeadlessRenderComplete } from './use-headless-render-complete.hook';
import { useVSCodeAutoSave } from './use-vscode-auto-save.hook';
import { useVSCodeFileLoad } from './use-vscode-file-load.hook';
import { useVSCodeTheme } from './use-vscode-theme.hook';

/**
* Wires the full VS Code webview bridge. Each inner hook no-ops when not
* running inside a webview, so this can be called unconditionally.
*/
export function useVSCodeSync(): void {
const hasReceivedFileRef = useVSCodeFileLoad();
useVSCodeAutoSave(hasReceivedFileRef);
useHeadlessRenderComplete(hasReceivedFileRef);
useVSCodeTheme();
}
35 changes: 35 additions & 0 deletions apps/web/src/core/vscode/use-vscode-theme.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { isVSCodeEnv } from '#common/utils/env.utils';
import { onMessage } from '#common/utils/vscode-bridge.utils';
import {
HOST_MESSAGE_TYPE,
type ThemePayload,
} from '@lemoncode/quickmock-bridge-protocol';
import { useEffect } from 'react';

const CSS_VAR_MAP: Record<keyof ThemePayload, readonly string[]> = {
background: ['--primary-100', '--primary-200'],
backgroundSecondary: ['--pure-white'],
foreground: ['--primary-700'],
};

const applyTheme = (theme: ThemePayload): void => {
const root = document.documentElement;
for (const [key, cssVars] of Object.entries(CSS_VAR_MAP)) {
const value = theme[key as keyof ThemePayload];
if (!value) continue;
for (const cssVar of cssVars) {
root.style.setProperty(cssVar, value);
}
}
if (theme.background)
document.body.style.setProperty('background-color', theme.background);
if (theme.foreground)
document.body.style.setProperty('color', theme.foreground);
};

export const useVSCodeTheme = (): void => {
useEffect(() => {
if (!isVSCodeEnv()) return;
return onMessage(HOST_MESSAGE_TYPE.THEME, applyTheme);
}, []);
};
17 changes: 17 additions & 0 deletions apps/web/src/core/vscode/vscode-sync.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { QuickMockFileContract } from '#core/local-disk/local-disk.model';
import {
mapFromQuickMockFileDocumentToApplicationDocument,
mapFromQuickMockFileDocumentToApplicationDocumentV0_1,
mapFromShapesArrayToQuickMockFileDocument,
} from '#core/local-disk/shapes-to-document.mapper';
import { DocumentModel } from '#core/providers/canvas/canvas.model';

export function deserializeDocument(data: QuickMockFileContract) {
return data.version === '0.1'
? mapFromQuickMockFileDocumentToApplicationDocumentV0_1(data)
: mapFromQuickMockFileDocumentToApplicationDocument(data);
}

export function serializeDocument(document: DocumentModel): string {
return JSON.stringify(mapFromShapesArrayToQuickMockFileDocument(document));
}
4 changes: 2 additions & 2 deletions apps/web/src/scenes/main.module.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.leftTools {
position: relative;
z-index: 2;
background-color: white;
background-color: var(--pure-white);
grid-area: leftTools;
border-right: 1px solid black;
display: inline-flex;
Expand All @@ -12,7 +12,7 @@
.rightTools {
position: relative;
z-index: 2;
background-color: white;
background-color: var(--pure-white);
grid-area: rightTools;
border-left: 1px solid black;
}
Expand Down
Loading
Loading