@gakim-digital/dexter-bridge 0.13.5 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.13.5",
3
+ "version": "0.13.6",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,6 +67,20 @@ export function mergeRequirements(current, next) {
67
67
  return [...byId.values()];
68
68
  }
69
69
 
70
+ // A requirement may name layers its own edit creates by their temporary ids,
71
+ // or a breakpoint copy of one as the breakpoint id followed by that temporary
72
+ // id. Once Framer gives those layers real ids, the requirement names them.
73
+ export function renameRequirementNodes(requirements, renamedIds = {}) {
74
+ const renames = Object.entries(renamedIds || {}).filter(([from, to]) => from && to && from !== to);
75
+ if (!renames.length) return requirements;
76
+ const rename = (id) => {
77
+ if (renamedIds[id]) return renamedIds[id];
78
+ const copy = renames.find(([from]) => id.endsWith(from) && /^[A-Za-z0-9_-]{9}$/.test(id.slice(0, -from.length)));
79
+ return copy ? `${id.slice(0, -copy[0].length)}${copy[1]}` : id;
80
+ };
81
+ return requirements.map((item) => ({ ...item, nodeIds: [...new Set(item.nodeIds.map(rename))] }));
82
+ }
83
+
70
84
  function finiteNumber(value) {
71
85
  const number = Number(value);
72
86
  return Number.isFinite(number) ? number : null;
@@ -312,22 +326,29 @@ export function summarizeRequirements(requirements, checkResults) {
312
326
  }
313
327
 
314
328
  // Compares two snapshots of layers that had to stay as they were. A snapshot
315
- // maps a layer id to { name, origin, rect, props } where props is the
316
- // serialized layer without children, origin is the layer a breakpoint or
317
- // variant copy follows, and rect is null when Dexter could not measure the
318
- // layer, so only its settings are compared. Sizes are compared only when both
319
- // readings came from the same source (the user's editor or Framer MCP), and
320
- // only size counts, not position: a section that grows moves every section
321
- // below it.
322
- // A change is edited when the run edited the layer, or the layer it follows;
323
- // any other change happened without the model touching the layer.
329
+ // maps a layer id to { name, origin, rect, props, children } where props is
330
+ // the serialized layer without children, origin is the layer a breakpoint or
331
+ // variant copy follows, rect is null when Dexter could not measure the layer,
332
+ // so only its settings are compared, and children lists its child layers in
333
+ // order as { id, name, origin }. Sizes are compared only when both readings
334
+ // came from the same source (the user's editor or Framer MCP), and only size
335
+ // counts, not position: a section that grows moves every section below it.
336
+ // The order of child layers does count: a layer inserted without an index
337
+ // lands at the end of its parent.
338
+ // A change is edited when the run edited the layer, the layer it follows, or
339
+ // for a new order one of the child layers; any other change happened without
340
+ // the model touching the layer.
341
+ function childOrder(layer) {
342
+ return layer.children.map((child) => child.id).join(',');
343
+ }
344
+
324
345
  export function unchangedViolations(before, after, edited = new Set(), tolerance = 1) {
325
346
  const changes = [];
326
347
  for (const [id, earlier] of Object.entries(before || {})) {
327
348
  const later = after?.[id];
328
349
  const name = earlier.name || id;
329
- const source = edited.has(id) ? 'self' : earlier.origin && edited.has(earlier.origin) ? 'origin' : null;
330
- const note = source === 'self' ? ' (you edited it)' : source === 'origin' ? ` (it follows your edit of ${earlier.origin})` : '';
350
+ const editedLayer = (layer) => edited.has(layer.id) || Boolean(layer.origin && edited.has(layer.origin));
351
+ let source = edited.has(id) ? 'self' : earlier.origin && edited.has(earlier.origin) ? 'origin' : null;
331
352
  const a = earlier.rect;
332
353
  const b = later?.rect;
333
354
  const comparable = a && b && (a.source || null) === (b.source || null);
@@ -335,7 +356,15 @@ export function unchangedViolations(before, after, edited = new Set(), tolerance
335
356
  if (!later) text = `${name} was removed`;
336
357
  else if (comparable && ['width', 'height'].some((key) => Math.abs(a[key] - b[key]) > tolerance)) {
337
358
  text = `${name} changed size (${a.width}×${a.height} → ${b.width}×${b.height})`;
359
+ } else if (earlier.children && later.children && childOrder(earlier) !== childOrder(later)) {
360
+ text = `${name} now holds its layers in this order: ${later.children.slice(0, 12).map((child) => child.name || child.id).join(', ')}`;
361
+ if (!source && [...earlier.children, ...later.children].some(editedLayer)) source = 'children';
338
362
  } else if (earlier.props !== later.props) text = `${name} has different settings`;
363
+ const note = source === 'self'
364
+ ? ' (you edited it)'
365
+ : source === 'origin'
366
+ ? ` (it follows your edit of ${earlier.origin})`
367
+ : source === 'children' ? ' (you edited layers inside it)' : '';
339
368
  if (text) changes.push({ text: `${text}${note}`, edited: Boolean(source) });
340
369
  if (changes.length >= 20) break;
341
370
  }
@@ -9,6 +9,7 @@ import {
9
9
  normalizeChecks,
10
10
  normalizeRequirements,
11
11
  summarizeRequirements,
12
+ renameRequirementNodes,
12
13
  unchangedViolations,
13
14
  verifyChecks,
14
15
  } from './framerRequirementContract.js';
@@ -879,7 +880,12 @@ for (const id of ids) {
879
880
  const rect = (await __dexterLaidOut(id)) ? await __dexterRect(id) : null;
880
881
  let props = '';
881
882
  try { const { children, ...rest } = JSON.parse(JSON.stringify(node)); props = fingerprint(JSON.stringify(rest)); } catch {}
882
- out[id] = { name: node.name || null, origin: node.$originalId || null, rect: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null, props };
883
+ const children = (Array.isArray(node.children) ? node.children : []).slice(0, 60).map((child) => {
884
+ const childId = child && typeof child === 'object' ? child.id : child;
885
+ const childNode = childId ? tree.get(childId) : null;
886
+ return { id: childId, name: (childNode && childNode.name) || null, origin: (childNode && childNode.$originalId) || null };
887
+ }).filter((child) => child.id);
888
+ out[id] = { name: node.name || null, origin: node.$originalId || null, rect: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null, props, children };
883
889
  }
884
890
  return out;`;
885
891
  }
@@ -1341,6 +1347,7 @@ export function createFramerAgentToolRuntime({
1341
1347
  await snapshotUnchanged();
1342
1348
  const result = await client.exec(wrapProgram(program));
1343
1349
  const applied = applyReceipt(result, screenshotNodeIds);
1350
+ requirements = renameRequirementNodes(requirements, applied.receipt.renamedIds);
1344
1351
  const programError = programErrorOf(applied.output, wrappedLineOffset(program));
1345
1352
  // Layers created by this program only get their real ids once it
1346
1353
  // finishes, so screenshots are taken in a second call.
@@ -1648,7 +1655,7 @@ export function createFramerAgentToolRuntime({
1648
1655
  }
1649
1656
  const unchecked = uncheckedRequirements();
1650
1657
  if (unchecked.length) {
1651
- facts.push(`These requirements from your plan have not been checked since your last change, so they are not confirmed either way: ${unchecked.map((item) => item.text).join('; ')}.`);
1658
+ facts.push(`These requirements from your plan have not been checked since your last change, so they are not confirmed either way: ${unchecked.map((item) => `${item.id}: ${item.text}`).join('; ')}.`);
1652
1659
  }
1653
1660
  if (unchangedChanges.length) {
1654
1661
  facts.push(`These layers you planned to leave as they were now read differently: ${unchangedChanges.map((change) => change.text).join('; ')}.`);