@atezer/figma-mcp-bridge 1.9.4 → 1.9.6
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/CHANGELOG.md +103 -0
- package/README.md +19 -2
- package/dist/core/discovery-counter.d.ts +54 -0
- package/dist/core/discovery-counter.d.ts.map +1 -0
- package/dist/core/discovery-counter.js +140 -0
- package/dist/core/discovery-counter.js.map +1 -0
- package/dist/core/plugin-bridge-connector.d.ts +5 -0
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +192 -22
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +299 -33
- package/f-mcp-plugin/ui.html +1 -1
- package/package.json +1 -1
- package/skills/figma-canvas-ops/SKILL.md +20 -0
- package/skills/fmcp-intent-router/SKILL.md +42 -0
- package/skills/fmcp-screen-orchestrator/SKILL.md +29 -0
package/f-mcp-plugin/code.js
CHANGED
|
@@ -6,7 +6,185 @@
|
|
|
6
6
|
|
|
7
7
|
// v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
|
|
8
8
|
// Keep in sync with package.json and src/core/version.ts.
|
|
9
|
-
var FMCP_PLUGIN_VERSION = '1.9.
|
|
9
|
+
var FMCP_PLUGIN_VERSION = '1.9.6';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* v1.9.6: Post-execute scan — figma_execute sonrasında oluşturulan node'ları
|
|
13
|
+
* otomatik tarayıp unbound fill/padding/radius/text-style tespit eder.
|
|
14
|
+
* Skill direktifi "return { createdNodeIds: [...] }" diyor — plugin bunu okuyup
|
|
15
|
+
* subtree'yi tarar. Violations varsa response'a _postExecuteScan field'ı eklenir,
|
|
16
|
+
* server bunu _DESIGN_SYSTEM_VIOLATIONS_BLOCKING'e dönüştürür.
|
|
17
|
+
*/
|
|
18
|
+
async function postExecuteScan(result) {
|
|
19
|
+
var nodeIds = [];
|
|
20
|
+
if (result && typeof result === 'object') {
|
|
21
|
+
if (Array.isArray(result.createdNodeIds)) nodeIds = result.createdNodeIds;
|
|
22
|
+
else if (Array.isArray(result.nodeIds)) nodeIds = result.nodeIds;
|
|
23
|
+
else if (Array.isArray(result.ids)) nodeIds = result.ids;
|
|
24
|
+
else if (typeof result.frameId === 'string') nodeIds = [result.frameId];
|
|
25
|
+
else if (typeof result.rootId === 'string') nodeIds = [result.rootId];
|
|
26
|
+
else if (typeof result.nodeId === 'string') nodeIds = [result.nodeId];
|
|
27
|
+
}
|
|
28
|
+
if (nodeIds.length === 0) return null;
|
|
29
|
+
|
|
30
|
+
var violations = [];
|
|
31
|
+
var maxViolations = 10;
|
|
32
|
+
var maxNodes = 500; // safety cap per subtree
|
|
33
|
+
var totalChecked = 0;
|
|
34
|
+
|
|
35
|
+
for (var i = 0; i < nodeIds.length && violations.length < maxViolations; i++) {
|
|
36
|
+
var root;
|
|
37
|
+
try { root = await figma.getNodeByIdAsync(nodeIds[i]); } catch (e) { continue; }
|
|
38
|
+
if (!root) continue;
|
|
39
|
+
var stack = [root];
|
|
40
|
+
var visited = 0;
|
|
41
|
+
while (stack.length > 0 && visited < maxNodes) {
|
|
42
|
+
var n = stack.pop(); visited++; totalChecked++;
|
|
43
|
+
if (!n) continue;
|
|
44
|
+
|
|
45
|
+
// Fill check (skip instances — their bindings come from main component)
|
|
46
|
+
if (Array.isArray(n.fills) && n.type !== 'INSTANCE') {
|
|
47
|
+
for (var fi = 0; fi < n.fills.length; fi++) {
|
|
48
|
+
var fl = n.fills[fi];
|
|
49
|
+
if (fl && fl.visible !== false && fl.type === 'SOLID') {
|
|
50
|
+
var fbv = n.boundVariables && n.boundVariables.fills;
|
|
51
|
+
if (!(fbv && (Array.isArray(fbv) ? fbv[fi] : true)) && violations.length < maxViolations) {
|
|
52
|
+
violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_FILL', hint: 'setBoundVariableForPaint cagrisi eksik' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if ((n.type === 'FRAME' || n.type === 'COMPONENT') && n.boundVariables !== undefined) {
|
|
59
|
+
var padProps = ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'];
|
|
60
|
+
for (var j = 0; j < padProps.length && violations.length < maxViolations; j++) {
|
|
61
|
+
if (typeof n[padProps[j]] === 'number' && n[padProps[j]] > 0 && !(n.boundVariables && n.boundVariables[padProps[j]])) {
|
|
62
|
+
violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_PADDING', prop: padProps[j], value: n[padProps[j]] });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
var radProps = ['cornerRadius', 'topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius'];
|
|
66
|
+
for (var k = 0; k < radProps.length && violations.length < maxViolations; k++) {
|
|
67
|
+
if (typeof n[radProps[k]] === 'number' && n[radProps[k]] > 0 && !(n.boundVariables && n.boundVariables[radProps[k]])) {
|
|
68
|
+
violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_RADIUS', prop: radProps[k], value: n[radProps[k]] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof n.itemSpacing === 'number' && n.itemSpacing > 0 && !(n.boundVariables && n.boundVariables.itemSpacing) && violations.length < maxViolations) {
|
|
72
|
+
violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_ITEMSPACING', value: n.itemSpacing });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (n.type === 'TEXT' && !(n.textStyleId && typeof n.textStyleId === 'string' && n.textStyleId !== '') && violations.length < maxViolations) {
|
|
77
|
+
violations.push({ nodeId: n.id, nodeName: n.name, category: 'UNBOUND_TEXTSTYLE', sample: String(n.characters || '').slice(0, 30) });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (n.children) {
|
|
81
|
+
for (var ci = 0; ci < n.children.length; ci++) stack.push(n.children[ci]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
scanned: true,
|
|
88
|
+
rootsScanned: nodeIds.length,
|
|
89
|
+
totalChecked: totalChecked,
|
|
90
|
+
violationCount: violations.length,
|
|
91
|
+
violations: violations,
|
|
92
|
+
passed: violations.length === 0,
|
|
93
|
+
hint: violations.length > 0
|
|
94
|
+
? '❌ v1.9.6 post-execute scan: ' + violations.length + ' unbound node tespit edildi. Kodu duzelt — her node icin setBoundVariable/setTextStyleIdAsync cagrisi eksik.'
|
|
95
|
+
: '✅ v1.9.6 post-execute scan: Tum node\'lar bound.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// v1.9.5: Metadata summary builder for returnMode='summary' (screenshotsuz)
|
|
100
|
+
async function buildNodeSummary(node, maxDepth) {
|
|
101
|
+
var colorUsage = {}; // role → count
|
|
102
|
+
var textRoleCount = {}; // styleId → { count, samples }
|
|
103
|
+
var unboundHexCount = 0;
|
|
104
|
+
var totalNodes = 0;
|
|
105
|
+
|
|
106
|
+
function walk(n, depth) {
|
|
107
|
+
totalNodes++;
|
|
108
|
+
// Fill bound token tracking
|
|
109
|
+
if (Array.isArray(n.fills)) {
|
|
110
|
+
for (var fi = 0; fi < n.fills.length; fi++) {
|
|
111
|
+
var fl = n.fills[fi];
|
|
112
|
+
if (fl && fl.visible !== false && fl.type === 'SOLID') {
|
|
113
|
+
var fbv = n.boundVariables && n.boundVariables.fills;
|
|
114
|
+
var bound = fbv && (Array.isArray(fbv) ? fbv[fi] : true);
|
|
115
|
+
if (bound) {
|
|
116
|
+
var role = (Array.isArray(fbv) ? fbv[fi] : fbv).id || 'unknown';
|
|
117
|
+
colorUsage[role] = (colorUsage[role] || 0) + 1;
|
|
118
|
+
} else {
|
|
119
|
+
unboundHexCount++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (n.type === 'TEXT') {
|
|
125
|
+
var styleId = n.textStyleId || 'inline';
|
|
126
|
+
if (!textRoleCount[styleId]) textRoleCount[styleId] = { count: 0, samples: [] };
|
|
127
|
+
textRoleCount[styleId].count++;
|
|
128
|
+
if (textRoleCount[styleId].samples.length < 3) {
|
|
129
|
+
textRoleCount[styleId].samples.push(String(n.characters || '').slice(0, 30));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (n.children && depth < 3) { // deep walk but capped
|
|
133
|
+
for (var ci = 0; ci < n.children.length; ci++) walk(n.children[ci], depth + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
walk(node, 0);
|
|
138
|
+
|
|
139
|
+
function collectSections(n, depth) {
|
|
140
|
+
if (depth > maxDepth) return null;
|
|
141
|
+
var s = {
|
|
142
|
+
name: n.name,
|
|
143
|
+
type: n.type,
|
|
144
|
+
size: n.width && n.height ? Math.round(n.width) + 'x' + Math.round(n.height) : null,
|
|
145
|
+
childrenCount: n.children ? n.children.length : 0,
|
|
146
|
+
};
|
|
147
|
+
if (n.children && n.children.length > 0 && depth < maxDepth) {
|
|
148
|
+
var items = [];
|
|
149
|
+
for (var i = 0; i < n.children.length && i < 20; i++) {
|
|
150
|
+
items.push(collectSections(n.children[i], depth + 1));
|
|
151
|
+
}
|
|
152
|
+
s.children = items;
|
|
153
|
+
}
|
|
154
|
+
return s;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Resolve variable names (best-effort)
|
|
158
|
+
var dominantColors = [];
|
|
159
|
+
var colorEntries = Object.keys(colorUsage).map(function (k) { return { role: k, usage: colorUsage[k] }; });
|
|
160
|
+
colorEntries.sort(function (a, b) { return b.usage - a.usage; });
|
|
161
|
+
for (var ci2 = 0; ci2 < Math.min(colorEntries.length, 5); ci2++) {
|
|
162
|
+
dominantColors.push({ variableId: colorEntries[ci2].role, usageCount: colorEntries[ci2].usage, bound: true });
|
|
163
|
+
}
|
|
164
|
+
if (unboundHexCount > 0) {
|
|
165
|
+
dominantColors.push({ variableId: '<hardcoded>', usageCount: unboundHexCount, bound: false });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
var textRoles = [];
|
|
169
|
+
var trKeys = Object.keys(textRoleCount);
|
|
170
|
+
for (var tk = 0; tk < trKeys.length; tk++) {
|
|
171
|
+
textRoles.push({ styleId: trKeys[tk], count: textRoleCount[trKeys[tk]].count, samples: textRoleCount[trKeys[tk]].samples });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
nodeId: node.id,
|
|
176
|
+
name: node.name,
|
|
177
|
+
type: node.type,
|
|
178
|
+
dimensions: { width: Math.round(node.width || 0), height: Math.round(node.height || 0) },
|
|
179
|
+
layoutMode: node.layoutMode || 'NONE',
|
|
180
|
+
childrenCount: node.children ? node.children.length : 0,
|
|
181
|
+
totalNodes: totalNodes,
|
|
182
|
+
sections: collectSections(node, 0),
|
|
183
|
+
dominantColors: dominantColors,
|
|
184
|
+
textRoles: textRoles.slice(0, 10),
|
|
185
|
+
hint: "v1.9.5 summary: Metadata ozeti, gorsel yok. Layout karari icin yeter. Pixel dogrulama gerekirse returnMode='file' veya 'regions'.",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
10
188
|
|
|
11
189
|
// Console log buffer for figma_get_console_logs (no CDP)
|
|
12
190
|
var __consoleLogBuffer = [];
|
|
@@ -552,6 +730,17 @@ figma.ui.onmessage = async (msg) => {
|
|
|
552
730
|
safeResult = { __serializationError: true, message: 'Result could not be serialized: ' + (serErr.message || String(serErr)), resultType: typeof result };
|
|
553
731
|
}
|
|
554
732
|
|
|
733
|
+
// v1.9.6: Post-execute scan — oluşturulan node'larda unbound tespit
|
|
734
|
+
var postScan = null;
|
|
735
|
+
try {
|
|
736
|
+
postScan = await postExecuteScan(result);
|
|
737
|
+
if (postScan && postScan.violationCount > 0) {
|
|
738
|
+
console.warn('🌉 [F-MCP v1.9.6] Post-execute scan: ' + postScan.violationCount + ' unbound node detected');
|
|
739
|
+
}
|
|
740
|
+
} catch (psErr) {
|
|
741
|
+
console.warn('🌉 [F-MCP v1.9.6] Post-execute scan failed:', psErr && psErr.message ? psErr.message : String(psErr));
|
|
742
|
+
}
|
|
743
|
+
|
|
555
744
|
figma.ui.postMessage({
|
|
556
745
|
type: 'EXECUTE_CODE_RESULT',
|
|
557
746
|
requestId: msg.requestId,
|
|
@@ -562,7 +751,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
562
751
|
fileContext: {
|
|
563
752
|
fileName: figma.root.name,
|
|
564
753
|
fileKey: figma.fileKey || null
|
|
565
|
-
}
|
|
754
|
+
},
|
|
755
|
+
_postExecuteScan: postScan // v1.9.6: BLOCKING signal source for server
|
|
566
756
|
});
|
|
567
757
|
|
|
568
758
|
} catch (error) {
|
|
@@ -2985,64 +3175,140 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2985
3175
|
// ============================================================================
|
|
2986
3176
|
else if (msg.type === 'CAPTURE_SCREENSHOT') {
|
|
2987
3177
|
try {
|
|
2988
|
-
console.log('🌉 [F-MCP ATezer Bridge] Capturing screenshot for node:', msg.nodeId);
|
|
3178
|
+
console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Capturing screenshot for node:', msg.nodeId, 'returnMode:', msg.returnMode);
|
|
2989
3179
|
|
|
2990
3180
|
var node = msg.nodeId ? await figma.getNodeByIdAsync(msg.nodeId) : figma.currentPage;
|
|
2991
3181
|
if (!node) {
|
|
2992
3182
|
throw new Error('Node not found: ' + msg.nodeId);
|
|
2993
3183
|
}
|
|
2994
3184
|
|
|
2995
|
-
//
|
|
2996
|
-
if (!('exportAsync' in node)) {
|
|
2997
|
-
throw new Error('Node type ' + node.type + ' does not support export');
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
// v1.8.0: Default JPG@1x q70 for context safety. Plugin reads msg.format,
|
|
3001
|
-
// msg.scale, msg.jpegQuality from the request payload (passed through
|
|
3002
|
-
// captureScreenshot connector → bridge → here).
|
|
3185
|
+
// v1.8.0: Default JPG@1x q70 for context safety.
|
|
3003
3186
|
var format = (msg.format || 'JPG').toUpperCase();
|
|
3004
3187
|
var scale = msg.scale != null ? msg.scale : 1;
|
|
3005
3188
|
var jpegQuality = msg.jpegQuality != null ? msg.jpegQuality : 70;
|
|
3189
|
+
// v1.9.5: returnMode (default 'file' which server translates to base64 + save)
|
|
3190
|
+
var returnMode = msg.returnMode || 'base64'; // plugin default stays base64; server wraps to file
|
|
3191
|
+
var regionStrategy = msg.regionStrategy || 'children';
|
|
3192
|
+
var maxRegions = msg.maxRegions != null ? msg.maxRegions : 8;
|
|
3006
3193
|
|
|
3007
|
-
|
|
3008
|
-
format: format,
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
// JPG quality (Figma Plugin API supports 'quality' for ExportSettingsImage)
|
|
3012
|
-
if (format === 'JPG') {
|
|
3013
|
-
exportSettings.quality = jpegQuality;
|
|
3194
|
+
function buildExportSettings() {
|
|
3195
|
+
var s = { format: format, constraint: { type: 'SCALE', value: scale } };
|
|
3196
|
+
if (format === 'JPG') s.quality = jpegQuality;
|
|
3197
|
+
return s;
|
|
3014
3198
|
}
|
|
3015
3199
|
|
|
3016
|
-
//
|
|
3017
|
-
|
|
3200
|
+
// ---------- v1.9.5 MODE: summary ----------
|
|
3201
|
+
// No export. Just metadata.
|
|
3202
|
+
if (returnMode === 'summary') {
|
|
3203
|
+
var rootSummary = await buildNodeSummary(node, /*depth*/ 2);
|
|
3204
|
+
figma.ui.postMessage({
|
|
3205
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3206
|
+
requestId: msg.requestId,
|
|
3207
|
+
success: true,
|
|
3208
|
+
mode: 'summary',
|
|
3209
|
+
summary: rootSummary,
|
|
3210
|
+
});
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3018
3213
|
|
|
3019
|
-
//
|
|
3020
|
-
|
|
3214
|
+
// ---------- v1.9.5 MODE: regions (children strategy) ----------
|
|
3215
|
+
if (returnMode === 'regions' && regionStrategy === 'children') {
|
|
3216
|
+
if (!node.children || node.children.length === 0) {
|
|
3217
|
+
figma.ui.postMessage({
|
|
3218
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3219
|
+
requestId: msg.requestId,
|
|
3220
|
+
success: false,
|
|
3221
|
+
error: "Node has no children for regions mode. Use 'file' or 'summary' instead.",
|
|
3222
|
+
});
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
var regions = [];
|
|
3226
|
+
var children = node.children.slice(0, maxRegions);
|
|
3227
|
+
for (var ri = 0; ri < children.length; ri++) {
|
|
3228
|
+
var child = children[ri];
|
|
3229
|
+
if (!('exportAsync' in child)) continue;
|
|
3230
|
+
try {
|
|
3231
|
+
var cbytes = await child.exportAsync(buildExportSettings());
|
|
3232
|
+
var cbase64 = figma.base64Encode(cbytes);
|
|
3233
|
+
regions.push({
|
|
3234
|
+
name: child.name,
|
|
3235
|
+
nodeId: child.id,
|
|
3236
|
+
type: child.type,
|
|
3237
|
+
image: {
|
|
3238
|
+
base64: cbase64,
|
|
3239
|
+
format: format,
|
|
3240
|
+
scale: scale,
|
|
3241
|
+
byteLength: cbytes.length,
|
|
3242
|
+
width: child.width,
|
|
3243
|
+
height: child.height,
|
|
3244
|
+
},
|
|
3245
|
+
});
|
|
3246
|
+
} catch (childErr) {
|
|
3247
|
+
regions.push({
|
|
3248
|
+
name: child.name,
|
|
3249
|
+
nodeId: child.id,
|
|
3250
|
+
error: childErr && childErr.message ? childErr.message : String(childErr),
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
figma.ui.postMessage({
|
|
3255
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3256
|
+
requestId: msg.requestId,
|
|
3257
|
+
success: true,
|
|
3258
|
+
mode: 'regions',
|
|
3259
|
+
regionStrategy: 'children',
|
|
3260
|
+
regions: regions,
|
|
3261
|
+
totalRegions: regions.length,
|
|
3262
|
+
rootSummary: {
|
|
3263
|
+
nodeId: node.id,
|
|
3264
|
+
name: node.name,
|
|
3265
|
+
dimensions: { width: node.width, height: node.height },
|
|
3266
|
+
childrenCount: node.children.length,
|
|
3267
|
+
capped: node.children.length > maxRegions,
|
|
3268
|
+
},
|
|
3269
|
+
});
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
// ---------- v1.9.5 MODE: regions (slices strategy) — NOT SUPPORTED YET ----------
|
|
3274
|
+
if (returnMode === 'regions' && regionStrategy === 'slices') {
|
|
3275
|
+
// Figma exportAsync does not support crop regions natively.
|
|
3276
|
+
// Fallback: return a warning + single full image export.
|
|
3277
|
+
figma.ui.postMessage({
|
|
3278
|
+
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3279
|
+
requestId: msg.requestId,
|
|
3280
|
+
success: false,
|
|
3281
|
+
error: "regionStrategy='slices' not supported in v1.9.5. Use regionStrategy='children' or returnMode='summary'.",
|
|
3282
|
+
});
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3021
3285
|
|
|
3022
|
-
//
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
bounds = node.absoluteBoundingBox;
|
|
3286
|
+
// ---------- DEFAULT: full single export (base64 or file — server post-processes) ----------
|
|
3287
|
+
if (!('exportAsync' in node)) {
|
|
3288
|
+
throw new Error('Node type ' + node.type + ' does not support export');
|
|
3026
3289
|
}
|
|
3027
3290
|
|
|
3028
|
-
|
|
3291
|
+
var bytes = await node.exportAsync(buildExportSettings());
|
|
3292
|
+
var base64 = figma.base64Encode(bytes);
|
|
3293
|
+
var bounds = ('absoluteBoundingBox' in node) ? node.absoluteBoundingBox : null;
|
|
3294
|
+
|
|
3295
|
+
console.log('🌉 [F-MCP ATezer Bridge v1.9.5] Screenshot captured:', bytes.length, 'bytes (' + format + ' @' + scale + 'x)');
|
|
3029
3296
|
|
|
3030
3297
|
figma.ui.postMessage({
|
|
3031
3298
|
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
3032
3299
|
requestId: msg.requestId,
|
|
3033
3300
|
success: true,
|
|
3301
|
+
mode: returnMode, // 'file' or 'base64' (server post-processes 'file' mode)
|
|
3034
3302
|
image: {
|
|
3035
3303
|
base64: base64,
|
|
3036
3304
|
format: format,
|
|
3037
3305
|
scale: scale,
|
|
3038
3306
|
jpegQuality: format === 'JPG' ? jpegQuality : null,
|
|
3039
3307
|
byteLength: bytes.length,
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
},
|
|
3045
|
-
bounds: bounds
|
|
3308
|
+
width: node.width,
|
|
3309
|
+
height: node.height,
|
|
3310
|
+
node: { id: node.id, name: node.name, type: node.type },
|
|
3311
|
+
bounds: bounds,
|
|
3046
3312
|
}
|
|
3047
3313
|
});
|
|
3048
3314
|
|
package/f-mcp-plugin/ui.html
CHANGED
|
@@ -424,7 +424,7 @@
|
|
|
424
424
|
window.__figmaFileName = null;
|
|
425
425
|
// v1.8.0+: Plugin version reported in WebSocket "ready" handshake.
|
|
426
426
|
// Keep in sync with package.json, src/core/version.ts, f-mcp-plugin/code.js.
|
|
427
|
-
var FMCP_PLUGIN_VERSION = '1.9.
|
|
427
|
+
var FMCP_PLUGIN_VERSION = '1.9.6';
|
|
428
428
|
|
|
429
429
|
// v1.9.2+ Startup log — Figma plugin iframe'de yeni kod yüklendiğini doğrulamak için.
|
|
430
430
|
// Console'da bu log görünüyorsa plugin güncel. Görünmüyorsa iframe eski kopyayı tutuyor
|
package/package.json
CHANGED
|
@@ -184,6 +184,26 @@ active-ds.md `❌` ise: "Hangi DS? (SUI / Material / HIG / Kendi / Hiçbiri)". Y
|
|
|
184
184
|
|
|
185
185
|
16. **Her Promise'i `await` et.**
|
|
186
186
|
|
|
187
|
+
17. **v1.9.5 Concise Query Rule — tek satır boyut/metadata sorguları.**
|
|
188
|
+
|
|
189
|
+
Boyut, renk, layout sorgulamak için **20+ satır JS YAZMA**. Tek satır yeter:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
// İYİ — tek satır, minimal context
|
|
193
|
+
return await figma.getNodeByIdAsync(id).then(n => ({ w: n.width, h: n.height, children: n.children?.length }));
|
|
194
|
+
|
|
195
|
+
// İYİ — toplu (birden fazla node)
|
|
196
|
+
return Promise.all(ids.map(async id => {
|
|
197
|
+
const n = await figma.getNodeByIdAsync(id);
|
|
198
|
+
return { id, name: n?.name, size: n ? [n.width, n.height] : null };
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
// KÖTÜ — 30 satır "defensive" kod boyut sorgusu için
|
|
202
|
+
// await figma.loadAllPagesAsync(); for (var pg of ...) { if (...) ... }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Kural:** Tek nodeId için size → 1 satır. Multi-node metadata → 1 Promise.all. Complex traversal sadece üretim mantığında gerekli — keşifte değil.
|
|
206
|
+
|
|
187
207
|
## 2. Sayfa Kuralları
|
|
188
208
|
|
|
189
209
|
```js
|
|
@@ -226,6 +226,48 @@ FOR each input in required_inputs:
|
|
|
226
226
|
|
|
227
227
|
Missing list'teki tüm soruları **TEK** `AskUserQuestion` çağrısında toplayıp sor. Max 4 soru/çağrı (AskUserQuestion limiti). 5+ soru gerekiyorsa 2 ayrı çağrı yap, ama ÖNCE kritik 4'ü sor, sonra kalanları.
|
|
228
228
|
|
|
229
|
+
### v1.9.5 Elicitation Kuralı (SERT)
|
|
230
|
+
|
|
231
|
+
- **Maks 1 `AskUserQuestion` çağrısı** tüm oturum boyunca. Tekrar soru sormak yasak — state'ten veya context'ten çıkar.
|
|
232
|
+
- **"devam et" / "tamam" / "ok" / "yap" sonrası soru YASAK.** Kullanıcı onay verdi → üretime geç.
|
|
233
|
+
- User prompt'u anlamlıysa (spesifik ekran adı, boyut, DS veriliyorsa) **hiç sorma**, direkt planla ve göster.
|
|
234
|
+
- **"Sen seç"** cevabı alırsan: mantıklı default kullan (iPhone 17, mobile, single variant, active DS).
|
|
235
|
+
- Sorulan her soru ≤30 kelime — uzun elicitation yasak.
|
|
236
|
+
- Soruyu sormanın context maliyeti ≥ yanıtın değeri mi? Değilse sorma.
|
|
237
|
+
|
|
238
|
+
### v1.9.6 Negative Intent Detection (KRITIK)
|
|
239
|
+
|
|
240
|
+
Kullanıcı şu paternlerle **negatif/dışlayıcı** talimat verebilir — bunları ZORUNLU parse et:
|
|
241
|
+
|
|
242
|
+
| Kullanıcı paterni | Anlam | Nasıl handle et |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| "X'i atla" / "X'i kullanma" / "X'ten bahsetme" | X referansını DIŞLA | `exclude_references: ["X"]` state'e yaz, o node/page/frame'e referans verme |
|
|
245
|
+
| "X'e bakma" / "X yok say" / "X'i unut" | X sıfır-referans | Aynı |
|
|
246
|
+
| "Y benzetme" / "Y gibi olmasın" | Y anti-pattern | `anti_pattern_refs: ["Y"]` — Y'ye ters tasarım yap |
|
|
247
|
+
| "X dışında" / "X hariç" | Whitelist exclusion | `exclude_references: ["X"]` |
|
|
248
|
+
| "Sıfırdan" / "baştan" / "yeni tasarım" | Mevcut iterasyonları atla | `start_fresh: true`, ideation/iterasyon referansları atla |
|
|
249
|
+
|
|
250
|
+
**Örnek:**
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
Kullanıcı: "SUI Alt 3'e bakma, sıfırdan yeni bir Anasayfa tasarla"
|
|
254
|
+
|
|
255
|
+
PARSE:
|
|
256
|
+
- exclude_references: ["SUI Alt 3", "241:11896"] (SUI Alt 3 frame ID'si de dahil)
|
|
257
|
+
- start_fresh: true
|
|
258
|
+
- target_screen: "Anasayfa"
|
|
259
|
+
- DS: active-ds.md'den çek
|
|
260
|
+
|
|
261
|
+
SONUÇ:
|
|
262
|
+
- SUI Alt 3 sayfası referans alınmaz
|
|
263
|
+
- Mevcut "SUI Alt 3 — Anasayfa" frame'i açılmaz, screenshot alınmaz
|
|
264
|
+
- ideation sayfası (ilham) kullanılabilir ama SUI Alt 3 bölümü atlanır
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Anti-pattern:** Kullanıcı "SUI Alt 3 atla" dedikten sonra onu screenshot alıp referans göstermek. v1.9.6'da bu explicit yasak — pre-flight'ta `exclude_references` kontrolü yapılır.
|
|
268
|
+
|
|
269
|
+
**State persist:** `exclude_references` last-intent.md'ye yazılır; aynı oturum boyunca geçerli.
|
|
270
|
+
|
|
229
271
|
**Örnek — generate-figma-screen:**
|
|
230
272
|
|
|
231
273
|
```
|
|
@@ -144,6 +144,35 @@ Benchmark/görselden DEĞER alma YASAK. Sadece NİYET: layout yönü, hiyerarşi
|
|
|
144
144
|
2. **Content** — DS instance yerleşimi → screenshot → onay
|
|
145
145
|
3. **Polish** — spacing, states, edge cases → son screenshot → audit
|
|
146
146
|
|
|
147
|
+
### v1.9.5 Discovery Budget Rule (SERT)
|
|
148
|
+
|
|
149
|
+
- **Maks 3 discovery çağrısı** (figma_get_*, figma_search_*, figma_execute read-only) sonra plan sun.
|
|
150
|
+
- Plan kullanıcıya 1-2 cümle + varsa özet: "Şu ekran/section'ları oluşturacağım: [liste]. Onay veriyor musun?"
|
|
151
|
+
- Kullanıcı onay verdikten sonra **mutation** aşamasına geç (figma_execute createFrame/setFills/setBoundVariable) — discovery counter reset olur.
|
|
152
|
+
- Plugin 8 çağrıdan sonra `_warnings: ["DISCOVERY_BUDGET_WARNING..."]` döner — görünce üretime geçmek zorundasın.
|
|
153
|
+
- 12 çağrıdan sonra `_DISCOVERY_BUDGET_EXCEEDED_BLOCKING: true` döner — **skip edilemez**, plan sun veya dur.
|
|
154
|
+
|
|
155
|
+
### v1.9.5 Screenshot Method Selection (KARAR AĞACI)
|
|
156
|
+
|
|
157
|
+
Ekran yakalama isteği olduğunda şu ağacı takip et:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
İhtiyaç ne?
|
|
161
|
+
├── "Planlama yapacağım, layout anlamak istiyorum" → returnMode: "summary" (metadata, screenshotsuz)
|
|
162
|
+
├── "Kullanıcıya son halini göstereyim" → returnMode: "file" (1 screenshot dosyaya)
|
|
163
|
+
├── "Büyük/scroll'lu ekran, bölümleri görmek istiyorum" → returnMode: "regions", regionStrategy: "children"
|
|
164
|
+
├── "Üretim sonrası hızlı validation" → returnMode: "file"
|
|
165
|
+
├── "Spesifik region (örn Hero)" → single-node file veya regions children maxRegions=3
|
|
166
|
+
└── "Base64 context'te gerekli (nadiren)" → returnMode: "base64" (explicit, _warning ile)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Budget farkındalığı:**
|
|
170
|
+
- Bir oturumda >3 farklı nodeId için screenshot → 4. için zorunlu `summary` veya `regions`
|
|
171
|
+
- Response'da `_warnings: ["CONTEXT_NEAR_LIMIT"]` veya `"DISCOVERY_BUDGET_..."` görürsen → sonraki screenshot zorunlu `summary`
|
|
172
|
+
- Pixel-perfect değil, **region-perfect** düşün: "Hero'yu göreyim, gerekirse Actions'ı da" — hepsi birden değil
|
|
173
|
+
|
|
174
|
+
**Kritik:** Screenshot YASAK değil. "Yasak" yerine **doğru yöntem** kullan. Her mode'un kullanım amacı var.
|
|
175
|
+
|
|
147
176
|
### Error Recovery
|
|
148
177
|
|
|
149
178
|
| Hata | Aksiyon |
|