figma-rust-mcp 2.8.2 → 2.8.4

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/README.md CHANGED
@@ -51,22 +51,26 @@ npx figma-rust-mcp
51
51
 
52
52
  This automatically downloads the prebuilt native binary for your architecture and starts the server.
53
53
 
54
- Or if configuring in **Cursor / Claude Desktop / Windsurf / Antigravity MCP Settings**:
54
+ ---
55
55
 
56
- ```json
57
- {
58
- "mcpServers": {
59
- "figma-rust-mcp": {
60
- "command": "npx",
61
- "args": ["-y", "figma-rust-mcp", "--stdio"]
62
- }
63
- }
64
- }
56
+ ### 2. Auto-Start as Background Service (Recommended for SSE)
57
+
58
+ If you use SSE transport across multiple AI IDEs (Antigravity, Claude, Cursor, Windsurf) and want `figma-rust-mcp` to **automatically start on OS login and keep running in the background**:
59
+
60
+ ```bash
61
+ # Install and start auto-start background service (macOS LaunchAgent, Linux systemd, Windows Task Scheduler)
62
+ npx figma-rust-mcp --install-service
63
+
64
+ # Check background service status
65
+ npx figma-rust-mcp --service-status
66
+
67
+ # Uninstall service
68
+ npx figma-rust-mcp --uninstall-service
65
69
  ```
66
70
 
67
71
  ---
68
72
 
69
- ### 2. Or Build from Source (Rust)
73
+ ### 3. Or Build from Source (Rust)
70
74
 
71
75
  Prerequisites: [Rust toolchain](https://rustup.rs/) (`cargo >= 1.80`).
72
76
 
@@ -79,7 +83,7 @@ cargo build --release
79
83
 
80
84
  ---
81
85
 
82
- ### 3. Install the Figma Plugin
86
+ ### 4. Install the Figma Plugin
83
87
 
84
88
  1. Open **Figma Desktop**.
85
89
  2. Go to **Plugins** → **Development** → **Import plugin from manifest...**
@@ -89,7 +93,7 @@ cargo build --release
89
93
 
90
94
  ---
91
95
 
92
- ### 4. Configure Your MCP Client
96
+ ### 5. Configure Your MCP Client
93
97
 
94
98
  #### Google Antigravity (SSE Transport - Recommended)
95
99
  Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.json`:
package/bin/figma-mcp.js CHANGED
@@ -19,7 +19,7 @@ const __dirname = path.dirname(__filename);
19
19
 
20
20
  // Read package.json version
21
21
  const pkgPath = path.resolve(__dirname, '../package.json');
22
- let pkgVersion = '2.8.2';
22
+ let pkgVersion = '2.8.4';
23
23
  let repoOwner = 'BuiHung1612';
24
24
  let repoName = 'figma-mcp';
25
25
 
@@ -298,10 +298,10 @@ WantedBy=default.target
298
298
  // Start it immediately too
299
299
  try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
300
300
 
301
- console.log(`\x1b[32m✓ figma-mcp Windows Task installed!\x1b[0m`);
301
+ console.log(`\x1b[32m✓ figma-rust-mcp Windows Task installed!\x1b[0m`);
302
302
  console.log(` Task: ${taskName}`);
303
303
  console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
304
- console.log(`\x1b[36mTo uninstall: npx figma-mcp --uninstall-service\x1b[0m`);
304
+ console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
305
305
 
306
306
  } else {
307
307
  console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
@@ -327,7 +327,7 @@ async function uninstallService() {
327
327
  try { execSync(`launchctl unload -w "${plistPath}"`, { stdio: 'inherit' }); } catch (_) {}
328
328
  fs.unlinkSync(plistPath);
329
329
 
330
- console.log(`\x1b[32m✓ figma-mcp service uninstalled.\x1b[0m`);
330
+ console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
331
331
 
332
332
  } else if (platform === 'linux') {
333
333
  try { execSync(`systemctl --user disable --now figma-mcp`, { stdio: 'inherit' }); } catch (_) {}
@@ -335,13 +335,13 @@ async function uninstallService() {
335
335
  if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
336
336
  try { execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' }); } catch (_) {}
337
337
 
338
- console.log(`\x1b[32m✓ figma-mcp service uninstalled.\x1b[0m`);
338
+ console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
339
339
 
340
340
  } else if (platform === 'win32') {
341
341
  const taskName = 'FigmaMCPServer';
342
342
  try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
343
343
 
344
- console.log(`\x1b[32m✓ figma-mcp Windows Task removed.\x1b[0m`);
344
+ console.log(`\x1b[32m✓ figma-rust-mcp Windows Task removed.\x1b[0m`);
345
345
 
346
346
  } else {
347
347
  console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
@@ -379,7 +379,7 @@ async function main() {
379
379
  const binPath = await findOrDownloadBinary();
380
380
  await installService(binPath);
381
381
  } catch (err) {
382
- console.error(`\x1b[31m[figma-mcp error]\x1b[0m ${err.message}`);
382
+ console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
383
383
  process.exit(1);
384
384
  }
385
385
  return;
@@ -389,7 +389,7 @@ async function main() {
389
389
  try {
390
390
  await uninstallService();
391
391
  } catch (err) {
392
- console.error(`\x1b[31m[figma-mcp error]\x1b[0m ${err.message}`);
392
+ console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
393
393
  process.exit(1);
394
394
  }
395
395
  return;
@@ -398,11 +398,11 @@ async function main() {
398
398
  if (args.includes('--service-status')) {
399
399
  const installed = isServiceInstalled();
400
400
  if (installed) {
401
- console.log('\x1b[32m✓ figma-mcp service is installed.\x1b[0m');
402
- console.log(' To uninstall: npx figma-mcp --uninstall-service');
401
+ console.log('\x1b[32m✓ figma-rust-mcp service is installed.\x1b[0m');
402
+ console.log(' To uninstall: npx figma-rust-mcp --uninstall-service');
403
403
  } else {
404
- console.log('\x1b[33m✗ figma-mcp service is NOT installed.\x1b[0m');
405
- console.log(' To install: npx figma-mcp --install-service');
404
+ console.log('\x1b[33m✗ figma-rust-mcp service is NOT installed.\x1b[0m');
405
+ console.log(' To install: npx figma-rust-mcp --install-service');
406
406
  }
407
407
  return;
408
408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-rust-mcp",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
5
5
  "mcpName": "io.github.BuiHung1612/figma-mcp",
6
6
  "type": "module",
package/plugin/code.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // figma-mcp — Figma Plugin main thread v1.2.1
2
2
  // Handles both WRITE (draw UI) and READ (extract design) operations.
3
3
  // AUTO-GENERATED by scripts/build-plugin.js — edit plugin-src/*.js instead
4
- // Generated: 2026-08-26T02:23:28.308Z
4
+ // Generated: 2026-08-26T04:26:26.667Z
5
5
 
6
6
  // ── plugin-src/utils.js ──
7
7
  // ─── UTILS ────────────────────────────────────────────────────────────────────
@@ -1238,7 +1238,7 @@ const handlers = {};
1238
1238
 
1239
1239
  handlers.status = async () => ({
1240
1240
  connected: true,
1241
- version: "2.8.2",
1241
+ version: "2.8.3",
1242
1242
  fileName: figma.root.name,
1243
1243
  currentPage: figma.currentPage.name,
1244
1244
  pageCount: figma.root.children.length,
@@ -3010,10 +3010,26 @@ handlers.export_image = async function(params) {
3010
3010
  handlers.index_scan = async function() {
3011
3011
  var pageNodesPromise = (async function() {
3012
3012
  try {
3013
- if (handlers.get_page_nodes) {
3014
- var res = await handlers.get_page_nodes({ depth: 6, detail: "compact", maxNodes: 15000 });
3015
- return (res && res.nodes) ? res.nodes : (Array.isArray(res) ? res : []);
3013
+ var page = figma.currentPage;
3014
+ var topFrames = page.children || [];
3015
+ var collected = [];
3016
+ var CHUNK_SIZE = 10;
3017
+
3018
+ for (var i = 0; i < topFrames.length; i += CHUNK_SIZE) {
3019
+ var chunk = topFrames.slice(i, i + CHUNK_SIZE);
3020
+ var chunkData = chunk.map(function(n) {
3021
+ return Object.assign(nodeToInfo(n), { childCount: "children" in n ? n.children.length : 0 });
3022
+ });
3023
+ collected = collected.concat(chunkData);
3024
+
3025
+ // Stream chunk progressively if there are multiple chunks
3026
+ if (topFrames.length > CHUNK_SIZE && figma.ui) {
3027
+ try {
3028
+ figma.ui.postMessage({ type: "index-chunk", nodes: chunkData });
3029
+ } catch(e3) {}
3030
+ }
3016
3031
  }
3032
+ return collected;
3017
3033
  } catch (e) {}
3018
3034
  return [];
3019
3035
  })();
@@ -5157,14 +5173,43 @@ figma.on("selectionchange", function() {
5157
5173
  } catch (e) {}
5158
5174
  });
5159
5175
 
5160
- // Broadcast document changes to invalidate index cache (debounced 1.5s)
5176
+ // Broadcast granular document changes to invalidate or incrementally update index cache
5161
5177
  var docChangeTimer = null;
5162
- function onDocChange() {
5178
+ var pendingChangedNodeIds = new Set();
5179
+
5180
+ function onDocChange(event) {
5163
5181
  try {
5182
+ if (event && event.documentChanges) {
5183
+ for (var i = 0; i < event.documentChanges.length; i++) {
5184
+ var ch = event.documentChanges[i];
5185
+ if (ch && ch.id) pendingChangedNodeIds.add(ch.id);
5186
+ }
5187
+ }
5164
5188
  if (docChangeTimer) clearTimeout(docChangeTimer);
5165
- docChangeTimer = setTimeout(function() {
5189
+ docChangeTimer = setTimeout(async function() {
5190
+ var ids = Array.from(pendingChangedNodeIds);
5191
+ pendingChangedNodeIds.clear();
5192
+
5193
+ if (ids.length > 0 && ids.length <= 15) {
5194
+ // Incremental micro-diff update for fast real-time editing
5195
+ var diffNodes = [];
5196
+ for (var j = 0; j < ids.length; j++) {
5197
+ try {
5198
+ var n = await findNodeByIdAsync(ids[j]);
5199
+ if (n && typeof nodeToInfo === "function") {
5200
+ diffNodes.push(nodeToInfo(n));
5201
+ }
5202
+ } catch(e2) {}
5203
+ }
5204
+ if (diffNodes.length > 0) {
5205
+ figma.ui.postMessage({ type: "node-diff", nodes: diffNodes });
5206
+ return;
5207
+ }
5208
+ }
5209
+
5210
+ // Fallback for large batch changes: mark dirty or trigger background sync
5166
5211
  figma.ui.postMessage({ type: "document-change" });
5167
- }, 1500);
5212
+ }, 800);
5168
5213
  } catch (e) {}
5169
5214
  }
5170
5215
 
@@ -5230,7 +5275,33 @@ figma.ui.onmessage = async (request) => {
5230
5275
  var newH = Math.max(200, Math.min(1200, Math.round(request.height)));
5231
5276
  try {
5232
5277
  figma.ui.resize(newW, newH);
5233
- figma.clientStorage.setAsync("mcp_window_size", { width: newW, height: newH }).catch(function() {});
5278
+ } catch(e) {}
5279
+ return;
5280
+ }
5281
+
5282
+ if (request.type === "save-window-size") {
5283
+ var sw = Math.max(260, Math.min(1000, Math.round(request.width)));
5284
+ var sh = Math.max(200, Math.min(1200, Math.round(request.height)));
5285
+ try {
5286
+ figma.clientStorage.setAsync("mcp_window_size", { width: sw, height: sh }).catch(function() {});
5287
+ } catch(e) {}
5288
+ return;
5289
+ }
5290
+
5291
+ // Handle manual reindex request from UI
5292
+ if (request.type === "manual-reindex") {
5293
+ try {
5294
+ if (handlers.index_scan) {
5295
+ var startMs = Date.now();
5296
+ var scanResult = await handlers.index_scan();
5297
+ figma.ui.postMessage({
5298
+ type: "index-update",
5299
+ data: scanResult,
5300
+ fileName: figma.root ? figma.root.name : "unknown",
5301
+ sessionId: figma.root ? figma.root.id : "_default",
5302
+ startMs: startMs
5303
+ });
5304
+ }
5234
5305
  } catch(e) {}
5235
5306
  return;
5236
5307
  }
@@ -5246,12 +5317,15 @@ figma.ui.onmessage = async (request) => {
5246
5317
  return;
5247
5318
  }
5248
5319
 
5320
+ const startTime = Date.now();
5249
5321
  try {
5250
5322
  var data = await handler(params || {});
5251
- figma.ui.postMessage({ id: id, operation: operation, success: true, dataJson: stringifyForBridge(data) });
5323
+ var durationMs = Date.now() - startTime;
5324
+ figma.ui.postMessage({ id: id, operation: operation, success: true, dataJson: stringifyForBridge(data), durationMs: durationMs });
5252
5325
  } catch (err) {
5326
+ var durationMs = Date.now() - startTime;
5253
5327
  var errMsg = "[dispatch:" + operation + "] " + (err && err.message ? err.message : String(err));
5254
- figma.ui.postMessage({ id: id, operation: operation, success: false, error: errMsg });
5328
+ figma.ui.postMessage({ id: id, operation: operation, success: false, error: errMsg, durationMs: durationMs });
5255
5329
  }
5256
5330
  };
5257
5331
 
package/plugin/ui.html CHANGED
@@ -140,8 +140,14 @@
140
140
  /* STATS ROW */
141
141
  .stats-row {
142
142
  display: grid;
143
- grid-template-columns: 1fr 1fr 1fr auto;
144
- gap: 6px;
143
+ grid-template-columns: repeat(4, 1fr);
144
+ gap: 5px;
145
+ flex-shrink: 0;
146
+ }
147
+ .stats-subrow {
148
+ display: grid;
149
+ grid-template-columns: 1fr 1fr auto;
150
+ gap: 5px;
145
151
  flex-shrink: 0;
146
152
  }
147
153
 
@@ -149,38 +155,45 @@
149
155
  background: var(--surface-1);
150
156
  border: 1px solid var(--border-1);
151
157
  border-radius: var(--radius-sm);
152
- padding: 7px 8px;
153
- display: flex; flex-direction: column; gap: 3px;
158
+ padding: 6px 7px;
159
+ display: flex; flex-direction: column; gap: 2px;
154
160
  transition: border-color var(--transition);
155
161
  }
156
162
  .stat-card:hover { border-color: var(--border-2); }
157
163
 
158
164
  .stat-label {
159
- font-size: 8.5px; font-weight: 700;
160
- letter-spacing: 0.8px; text-transform: uppercase;
165
+ font-size: 8px; font-weight: 700;
166
+ letter-spacing: 0.7px; text-transform: uppercase;
161
167
  color: var(--text-3);
162
168
  }
163
169
  .stat-value {
164
- font-size: 17px; font-weight: 800;
170
+ font-size: 15px; font-weight: 800;
165
171
  font-family: 'SF Mono', 'Fira Code', monospace;
166
- line-height: 1;
172
+ line-height: 1.1;
173
+ display: flex; align-items: baseline; gap: 2px;
174
+ }
175
+ .stat-unit {
176
+ font-size: 8.5px; font-weight: 600; color: var(--text-3);
167
177
  }
168
178
  .stat-card.writes .stat-value { color: var(--violet); }
169
179
  .stat-card.reads .stat-value { color: var(--accent-2); }
170
180
  .stat-card.errors .stat-value { color: var(--rose); }
181
+ .stat-card.speed .stat-value { color: var(--green); }
182
+ .stat-card.memory .stat-value { color: var(--amber); }
171
183
 
172
184
  .port-card {
173
185
  background: var(--surface-1);
174
186
  border: 1px solid var(--border-1);
175
187
  border-radius: var(--radius-sm);
176
- padding: 7px 10px;
177
- display: flex; flex-direction: column; gap: 3px;
188
+ padding: 6px 9px;
189
+ display: flex; flex-direction: column; gap: 2px;
178
190
  cursor: pointer;
191
+ justify-content: center;
179
192
  transition: border-color var(--transition), background var(--transition);
180
193
  }
181
194
  .port-card:hover { border-color: rgba(6,182,212,0.35); background: rgba(6,182,212,0.04); }
182
- .port-label { font-size: 8.5px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text-3); }
183
- .port-value { font-size: 12px; font-weight: 700; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent-2); line-height: 1; }
195
+ .port-label { font-size: 8px; font-weight: 700; letter-spacing: 0.7px; text-transform: uppercase; color: var(--text-3); }
196
+ .port-value { font-size: 12px; font-weight: 700; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent-2); line-height: 1.1; }
184
197
 
185
198
  /* TERMINAL */
186
199
  .terminal-wrap {
@@ -290,15 +303,17 @@
290
303
 
291
304
  /* RESIZE GRIP */
292
305
  .resize-grip {
293
- position: fixed; right: 3px; bottom: 3px;
294
- width: 14px; height: 14px;
295
- cursor: nwse-resize; opacity: 0.2;
306
+ position: fixed; right: 0; bottom: 0;
307
+ width: 24px; height: 24px;
308
+ cursor: nwse-resize; opacity: 0.35;
296
309
  color: var(--text-2);
297
310
  transition: opacity var(--transition), color var(--transition);
298
311
  z-index: 1000;
299
312
  display: flex; align-items: flex-end; justify-content: flex-end;
313
+ padding: 4px;
314
+ touch-action: none;
300
315
  }
301
- .resize-grip:hover { opacity: 0.7; color: var(--accent-2); }
316
+ .resize-grip:hover { opacity: 1; color: var(--accent-2); }
302
317
  </style>
303
318
  </head>
304
319
  <body>
@@ -334,13 +349,13 @@
334
349
  <span class="stat-label">Reads</span>
335
350
  <span class="stat-value" id="read-count">0</span>
336
351
  </div>
337
- <div class="stat-card errors" title="Errors">
338
- <span class="stat-label">Errors</span>
339
- <span class="stat-value" id="err-count">0</span>
352
+ <div class="stat-card speed" title="Last Request Execution Time / Speed">
353
+ <span class="stat-label">Speed</span>
354
+ <span class="stat-value"><span id="speed-display">0</span><span class="stat-unit">ms</span></span>
340
355
  </div>
341
- <div class="port-card" onclick="changePort()" title="Click to change port">
342
- <span class="port-label">Port</span>
343
- <span class="port-value" id="port-display">38451</span>
356
+ <div class="stat-card memory" title="Server Memory Usage (RAM RSS)">
357
+ <span class="stat-label">RAM</span>
358
+ <span class="stat-value"><span id="ram-display">3.2</span><span class="stat-unit">MB</span></span>
344
359
  </div>
345
360
  </div>
346
361
 
@@ -367,6 +382,13 @@
367
382
  </svg>
368
383
  </button>
369
384
 
385
+ <button class="btn" id="reindex-btn" onclick="triggerManualReindex()" title="Re-index Figma document to Rust memory" style="background:var(--surface-2);border:1px solid var(--border-2);color:var(--text-2);font-size:10px;padding:0 10px;gap:5px;">
386
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
387
+ <path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/>
388
+ </svg>
389
+ <span id="reindex-btn-label">⚡ Index</span>
390
+ </button>
391
+
370
392
  <button class="btn btn-primary" id="launch-btn" onclick="launchServer()" title="Start Figma MCP server">
371
393
  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
372
394
  <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
@@ -416,6 +438,34 @@
416
438
  var launchBtn = document.getElementById("launch-btn");
417
439
  var launchBtnLabel = document.getElementById("launch-btn-label");
418
440
  var statusPill = document.getElementById("status-pill");
441
+ var speedEl = document.getElementById("speed-display");
442
+ var ramEl = document.getElementById("ram-display");
443
+ var reindexBtn = document.getElementById("reindex-btn");
444
+ var reindexBtnLabel = document.getElementById("reindex-btn-label");
445
+
446
+ async function fetchServerStats() {
447
+ if (!everConnected) return;
448
+ try {
449
+ var res = await fetch(BRIDGE + "/health", { signal: AbortSignal.timeout(1000) });
450
+ if (res.ok) {
451
+ var data = await res.json();
452
+ if (data && data.stats && data.stats.memoryMb !== undefined && ramEl) {
453
+ ramEl.textContent = data.stats.memoryMb.toFixed(1);
454
+ }
455
+ }
456
+ } catch(e) {}
457
+ }
458
+ setInterval(fetchServerStats, 4000);
459
+
460
+ function triggerManualReindex() {
461
+ if (!everConnected) {
462
+ log("Cannot index: Server is offline", "err");
463
+ return;
464
+ }
465
+ if (reindexBtnLabel) reindexBtnLabel.textContent = "Indexing…";
466
+ log("⚡ Manual re-index triggered…", "info");
467
+ parent.postMessage({ pluginMessage: { type: "manual-reindex" } }, "*");
468
+ }
419
469
 
420
470
  var READ_OPS = [
421
471
  "get_selection","get_design","get_page_nodes","screenshot",
@@ -918,6 +968,20 @@
918
968
  return;
919
969
  }
920
970
 
971
+ if (msg.type === "node-diff") {
972
+ if (ws && ws.readyState === WebSocket.OPEN) {
973
+ try { ws.send(JSON.stringify({ type: "node-diff", nodes: msg.nodes })); } catch(e) {}
974
+ }
975
+ return;
976
+ }
977
+
978
+ if (msg.type === "index-chunk") {
979
+ if (ws && ws.readyState === WebSocket.OPEN) {
980
+ try { ws.send(JSON.stringify({ type: "index-chunk", nodes: msg.nodes })); } catch(e) {}
981
+ }
982
+ return;
983
+ }
984
+
921
985
  if (msg.type === "document-change") {
922
986
  if (ws && ws.readyState === WebSocket.OPEN) {
923
987
  try { ws.send(JSON.stringify({ type: "document-change" })); } catch(e) {}
@@ -926,6 +990,7 @@
926
990
  }
927
991
 
928
992
  if (msg.type === "index-update") {
993
+ if (reindexBtnLabel) reindexBtnLabel.textContent = "⚡ Indexed";
929
994
  if (ws && ws.readyState === WebSocket.OPEN) {
930
995
  try {
931
996
  ws.send(JSON.stringify({
@@ -935,13 +1000,21 @@
935
1000
  sessionId: msg.sessionId,
936
1001
  startMs: msg.startMs
937
1002
  }));
938
- log("⚡ Pre-indexed file data", "info");
1003
+ var nodeCount = (msg.data && msg.data.pageNodes) ? msg.data.pageNodes.length : 0;
1004
+ log("⚡ Indexed " + (msg.fileName || "file") + " (" + (Date.now() - msg.startMs) + "ms)", "ok");
939
1005
  } catch(e) {}
940
1006
  }
1007
+ setTimeout(function() {
1008
+ if (reindexBtnLabel) reindexBtnLabel.textContent = "⚡ Index";
1009
+ }, 3000);
941
1010
  return;
942
1011
  }
943
1012
 
944
1013
  var isRead = READ_OPS.indexOf(msg.operation) >= 0;
1014
+ var durationStr = msg.durationMs !== undefined ? " (" + msg.durationMs + "ms)" : "";
1015
+ if (msg.durationMs !== undefined && speedEl) {
1016
+ speedEl.textContent = msg.durationMs;
1017
+ }
945
1018
  if (msg.success) {
946
1019
  if (isRead) {
947
1020
  readCount++;
@@ -950,11 +1023,11 @@
950
1023
  writeCount++;
951
1024
  document.getElementById("write-count").textContent = writeCount;
952
1025
  }
953
- log("✓ " + (msg.id ? msg.id.slice(-6) : "ok"), "ok");
1026
+ log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok");
954
1027
  } else {
955
1028
  errCount++;
956
1029
  document.getElementById("err-count").textContent = errCount;
957
- log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + ": " + (msg.error || "fail"), "err");
1030
+ log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err");
958
1031
  }
959
1032
  var payloadJson = msg.dataJson !== undefined
960
1033
  ? msg.dataJson
@@ -964,11 +1037,15 @@
964
1037
 
965
1038
  initConnection();
966
1039
 
967
- // Smooth Drag Resize
1040
+ // Smooth Drag Resize (60FPS via requestAnimationFrame)
968
1041
  (function initResize() {
969
1042
  var grip = document.getElementById("resize-grip");
970
1043
  if (!grip) return;
971
1044
 
1045
+ var rafId = null;
1046
+ var lastW = 0, lastH = 0;
1047
+ var saveTimer = null;
1048
+
972
1049
  grip.addEventListener("pointerdown", function(e) {
973
1050
  e.preventDefault();
974
1051
  grip.setPointerCapture(e.pointerId);
@@ -980,13 +1057,29 @@
980
1057
  function onPointerMove(ev) {
981
1058
  var nw = Math.max(280, Math.min(1000, Math.round(startW + (ev.clientX - startX))));
982
1059
  var nh = Math.max(220, Math.min(1200, Math.round(startH + (ev.clientY - startY))));
983
- parent.postMessage({ pluginMessage: { type: "resize", width: nw, height: nh } }, "*");
1060
+ lastW = nw;
1061
+ lastH = nh;
1062
+
1063
+ if (!rafId) {
1064
+ rafId = requestAnimationFrame(function() {
1065
+ parent.postMessage({ pluginMessage: { type: "resize", width: lastW, height: lastH } }, "*");
1066
+ rafId = null;
1067
+ });
1068
+ }
984
1069
  }
985
1070
 
986
1071
  function onPointerUp(ev) {
987
1072
  try { grip.releasePointerCapture(ev.pointerId); } catch(err) {}
988
1073
  window.removeEventListener("pointermove", onPointerMove);
989
1074
  window.removeEventListener("pointerup", onPointerUp);
1075
+ if (rafId) {
1076
+ cancelAnimationFrame(rafId);
1077
+ rafId = null;
1078
+ }
1079
+ if (lastW > 0 && lastH > 0) {
1080
+ parent.postMessage({ pluginMessage: { type: "resize", width: lastW, height: lastH } }, "*");
1081
+ parent.postMessage({ pluginMessage: { type: "save-window-size", width: lastW, height: lastH } }, "*");
1082
+ }
990
1083
  }
991
1084
 
992
1085
  window.addEventListener("pointermove", onPointerMove);