breakpoint-mcp 1.83.0 โ†’ 1.84.0

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.
@@ -1411,6 +1411,96 @@ export function registerTabletopTools(server, bridge, config) {
1411
1411
  function replacedField(existed) {
1412
1412
  return existed ? { replaced: true } : {};
1413
1413
  }
1414
+ /**
1415
+ * Which scene the editor is editing AFTER this call โ€” 298.
1416
+ *
1417
+ * ๐Ÿ”ด FOUR TOOLS MOVED THE EDITOR AND NONE OF THEM SAID SO, AND THE FACT WAS
1418
+ * ALREADY WRITTEN DOWN TWICE IN THIS REPOSITORY. `guardWriteTarget`'s own
1419
+ * comment forty lines up says the addon's `scene.new` "calls
1420
+ * `EditorInterface.open_scene_from_path(path)`"; `tabletop-plane.integration.mjs`
1421
+ * opens its live family with *every `*_create` decomposes onto scene.new โ†’ โ€ฆ
1422
+ * โ†’ scene.save, which SWITCHES the edited scene, so the four disk writers run
1423
+ * first and the in-scene work follows one scene_open*. Both are correct. Both
1424
+ * are in files a CALLER never opens, and the tool surface said nothing โ€” no
1425
+ * description, no field, nothing in `TOOL_CATALOG.md`.
1426
+ *
1427
+ * ๐Ÿ”ด MEASURED AT 298 ON A LIVE GODOT 4.7, ON THE DEFAULT SURFACE. Compose a
1428
+ * table, call `card_template_create`, then `card_instance` with `parent: "."`
1429
+ * exactly as its own description invites: the card is created, `bound` lists
1430
+ * the slot, `isError` is false โ€” and the card is in the CARD TEMPLATE. The
1431
+ * user's table is untouched. Every call in that sequence answers success.
1432
+ *
1433
+ * ๐Ÿ”ต IT IS READ, NOT ASSUMED. `spec.path` is what the compose ASKED for; this
1434
+ * is what the editor ANSWERS, which is the difference between an echo and a
1435
+ * measurement (155 ยง2) โ€” and the two can differ, because `clearStaleTab` may
1436
+ * have moved the editor before the write and a failed open would leave it
1437
+ * somewhere else again.
1438
+ *
1439
+ * ๐Ÿ”ต AND IT IS SPREAD, NOT RESTATED, FOR `wroteDestination`'s REASON (284): a
1440
+ * fifth composite that decomposes onto `scene.new` cannot be added with the
1441
+ * reporting half missing. `contract_check` check 36 holds that population.
1442
+ *
1443
+ * ๐Ÿ†• 299 โ€” AND THE ANSWER TO 298's FINDING IS NOW THE ONE 298 DECLINED TO
1444
+ * TAKE IN-SESSION. 298 filed `tabletop-creators-leave-the-caller-elsewhere`
1445
+ * as a `user` row rather than deciding it: whether the four should RESTORE
1446
+ * the caller's scene is a change to what a shipped tool does to the editor,
1447
+ * and this project steers those before it builds them. Steered at 299 โ€”
1448
+ * restore โ€” with the argument that decided it recorded here, because the
1449
+ * next person to read this block will want to know why the behaviour flipped
1450
+ * one session after it was documented: **298's description never reached a
1451
+ * user.** The merge sat untagged behind a release backlog, so both the
1452
+ * warning label and its removal ship in the same cut and no caller ever saw
1453
+ * the surface promise a move it no longer makes.
1454
+ */
1455
+ /** Which scene the editor is editing right now, or `null` for none open. */
1456
+ async function currentEditedScene() {
1457
+ const open = await emit("scene.list_open", {});
1458
+ return typeof open?.current === "string" ? open.current : null;
1459
+ }
1460
+ /**
1461
+ * Put the caller back on the scene they were editing when the call arrived.
1462
+ *
1463
+ * ๐Ÿ”ด BEST EFFORT, AND DELIBERATELY NOT A FAILURE PATH. The file is written
1464
+ * and saved by the time this runs. Throwing here would report a create that
1465
+ * SUCCEEDED as an error, and the caller's correct response to an error โ€”
1466
+ * retry โ€” would write it a second time. So a reopen that cannot happen
1467
+ * leaves the editor where the decomposition left it, and the caller learns
1468
+ * that from `edited_scene`, which is READ afterwards and therefore cannot
1469
+ * agree with a restore that did not occur. The honest report replaces the
1470
+ * exception; it does not merely soften it.
1471
+ *
1472
+ * ๐Ÿ”ต THREE NO-OPS, EACH FOR ITS OWN REASON. `null` means the editor held no
1473
+ * scene, so there is nothing to go back TO and the created scene stays open
1474
+ * โ€” inventing a close would be a second unasked-for movement. Already
1475
+ * current means the decomposition did not move us, or `clearStaleTab`
1476
+ * already put us back. And a `before` whose file is gone cannot be reopened:
1477
+ * `scene.open` would answer *Scene not found* and turn a successful create
1478
+ * into a refusal, which is the failure path this function exists to avoid.
1479
+ */
1480
+ async function restoreEditedScene(before) {
1481
+ if (before === null)
1482
+ return;
1483
+ if (await currentEditedScene() === before)
1484
+ return;
1485
+ if (!fs.existsSync(resolveInsideProject(before, config.projectPath, "path")))
1486
+ return;
1487
+ try {
1488
+ await emit("scene.open", { path: before });
1489
+ }
1490
+ catch {
1491
+ // Swallowed on purpose โ€” see the comment above. `edited_scene` reports
1492
+ // where the editor actually is, so a failed restore is visible, not lost.
1493
+ }
1494
+ }
1495
+ /**
1496
+ * Restore, then REPORT โ€” in that order, and the order is the claim. Reading
1497
+ * first and restoring after would answer with a path the editor had already
1498
+ * left, which is 155 ยง2's echo wearing a measurement's clothes one more time.
1499
+ */
1500
+ async function editedSceneField(before) {
1501
+ await restoreEditedScene(before);
1502
+ return { edited_scene: await currentEditedScene() };
1503
+ }
1414
1504
  /**
1415
1505
  * The second half, run only AFTER the confirmation gate has been cleared โ€”
1416
1506
  * closing a scene tab is a visible side effect and must not happen on a call
@@ -1477,7 +1567,7 @@ export function registerTabletopTools(server, bridge, config) {
1477
1567
  title: "Create card template",
1478
1568
  description: "Build a reusable card scene (a PackedScene) from a slot spec, with a generated script-backed set_data() / set_face(). " +
1479
1569
  "Named slots (label / rich_text / texture / panel / badge) become the card's regions; card_instance and card_deck_from_table " +
1480
- "bind data to them by slot name. Optional inline theme and a two-sided card back. DESTRUCTIVE (writes a scene + script) โ€” gated by confirmation.",
1570
+ "bind data to them by slot name. Optional inline theme and a two-sided card back. DESTRUCTIVE (writes a scene + script) โ€” gated by confirmation. Puts the editor back on the scene you had open; `edited_scene` names where it landed.",
1481
1571
  inputSchema: {
1482
1572
  path: z.string().describe("Where to save the template scene, e.g. res://ui/cards/Card.tscn"),
1483
1573
  size: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }).describe("Card dimensions in px"),
@@ -1535,8 +1625,13 @@ export function registerTabletopTools(server, bridge, config) {
1535
1625
  if (blocked)
1536
1626
  return blocked;
1537
1627
  try {
1628
+ // ๐Ÿ†• 299 โ€” READ BEFORE THE FIRST THING THAT CAN MOVE THE EDITOR, which is
1629
+ // `clearStaleTab` and not `scene.new`: on an overwrite the stale-tab close
1630
+ // has already changed `current` by the time the decomposition runs, so a
1631
+ // capture taken any later restores the caller to the wrong scene.
1632
+ const before = await currentEditedScene();
1538
1633
  await clearStaleTab(a.path);
1539
- return ok({ ...await emitCardTemplate(emit, a), ...replacedField(existed) });
1634
+ return ok({ ...await emitCardTemplate(emit, a), ...replacedField(existed), ...(await editedSceneField(before)) });
1540
1635
  }
1541
1636
  catch (err) {
1542
1637
  return fail(err);
@@ -1575,7 +1670,7 @@ export function registerTabletopTools(server, bridge, config) {
1575
1670
  server.registerTool("card_hand_layout", {
1576
1671
  title: "Lay out a hand of cards",
1577
1672
  description: "Instance N cards under a container and arrange them as a row, fan, stack, or grid. Undoable node authoring. Each card carries its own " +
1578
- "data (bound via the template's set_data) and face state; spacing / overlap / fan_angle / columns / align / origin tune the arrangement.",
1673
+ "data (bound via the template's set_data) and face state.",
1579
1674
  inputSchema: {
1580
1675
  template_path: z.string().describe("Card template scene, e.g. res://ui/cards/Card.tscn"),
1581
1676
  parent: z.string().describe("Container node path the cards are instanced under; \".\" for the root"),
@@ -1700,7 +1795,7 @@ export function registerTabletopTools(server, bridge, config) {
1700
1795
  title: "Create board scene",
1701
1796
  description: "Build a board scene whose children are addressable cells (each a cell_<id> node in the board_cells group) from a ring, grid, or explicit-cells layout. " +
1702
1797
  "Cells are Marker2D (or Control) anchors positioned by pure layout math; an optional background (color or res:// art) sits behind them. " +
1703
- "General-purpose โ€” cells carry only caller-supplied ids. DESTRUCTIVE (writes a scene) โ€” gated by confirmation. Returns the cell_id โ†’ node_path + position map.",
1798
+ "General-purpose โ€” cells carry only caller-supplied ids. DESTRUCTIVE (writes a scene) โ€” gated by confirmation. Puts the editor back on the scene you had open; `edited_scene` names where it landed.",
1704
1799
  inputSchema: {
1705
1800
  path: z.string().describe("Where to save the board scene, e.g. res://ui/board/Board.tscn"),
1706
1801
  layout: boardLayout.describe("ring{cells[]} | grid{rows,cols} | cells{cells[{id,x,y}]}"),
@@ -1733,8 +1828,13 @@ export function registerTabletopTools(server, bridge, config) {
1733
1828
  if (blocked)
1734
1829
  return blocked;
1735
1830
  try {
1831
+ // ๐Ÿ†• 299 โ€” READ BEFORE THE FIRST THING THAT CAN MOVE THE EDITOR, which is
1832
+ // `clearStaleTab` and not `scene.new`: on an overwrite the stale-tab close
1833
+ // has already changed `current` by the time the decomposition runs, so a
1834
+ // capture taken any later restores the caller to the wrong scene.
1835
+ const before = await currentEditedScene();
1736
1836
  await clearStaleTab(a.path);
1737
- return ok({ ...await emitBoardCreate(emit, a), ...replacedField(existed) });
1837
+ return ok({ ...await emitBoardCreate(emit, a), ...replacedField(existed), ...(await editedSceneField(before)) });
1738
1838
  }
1739
1839
  catch (err) {
1740
1840
  return fail(err);
@@ -1765,7 +1865,7 @@ export function registerTabletopTools(server, bridge, config) {
1765
1865
  title: "Create tile-backed board",
1766
1866
  description: "Build a tile-backed board scene: a TileMapLayer grid whose cells are addressable by integer [x, y] tile coordinates (cols wide ร— rows tall). " +
1767
1867
  "The layer binds a TileSet โ€” a supplied `tileset` .tres, or a fresh empty one created at <scene>_tiles.tres โ€” so it has a real tile_size (the coordinate frame placement uses); `paint` optionally fills the whole grid with one tile from the bound tileset. " +
1768
- "General-purpose โ€” cells carry only coordinates. Adds no addon method โ€” decomposes onto scene.new โ†’ tileset.create โ†’ tilemaplayer.create โ†’ tilemap.set_cells_rect โ†’ scene.save. DESTRUCTIVE (writes a scene, and a TileSet .tres unless `tileset` is supplied) โ€” gated by confirmation. Returns the layer path + grid dimensions + tile size.",
1868
+ "General-purpose โ€” cells carry only coordinates. DESTRUCTIVE (writes a scene, and a TileSet .tres unless `tileset` is supplied) โ€” gated by confirmation. Puts the editor back on the scene you had open; `edited_scene` names where it landed.",
1769
1869
  inputSchema: {
1770
1870
  path: z.string().describe("Where to save the board scene, e.g. res://ui/board/TileBoard.tscn"),
1771
1871
  rows: z.number().int().positive().describe("Grid row count (cell y ranges 0..rows-1)"),
@@ -1801,8 +1901,13 @@ export function registerTabletopTools(server, bridge, config) {
1801
1901
  if (blocked)
1802
1902
  return blocked;
1803
1903
  try {
1904
+ // ๐Ÿ†• 299 โ€” READ BEFORE THE FIRST THING THAT CAN MOVE THE EDITOR, which is
1905
+ // `clearStaleTab` and not `scene.new`: on an overwrite the stale-tab close
1906
+ // has already changed `current` by the time the decomposition runs, so a
1907
+ // capture taken any later restores the caller to the wrong scene.
1908
+ const before = await currentEditedScene();
1804
1909
  await clearStaleTab(a.path);
1805
- return ok({ ...await emitBoardTileCreate(emit, a), ...replacedField(existed) });
1910
+ return ok({ ...await emitBoardTileCreate(emit, a), ...replacedField(existed), ...(await editedSceneField(before)) });
1806
1911
  }
1807
1912
  catch (err) {
1808
1913
  return fail(err);
@@ -1812,7 +1917,7 @@ export function registerTabletopTools(server, bridge, config) {
1812
1917
  server.registerTool("board_tile_place", {
1813
1918
  title: "Place a node on a tile coordinate",
1814
1919
  description: "Snap an existing node (a card or piece instance) onto a TileMapLayer cell by integer [x, y] tile coordinate. Undoable node authoring. " +
1815
- "The cell's local position is computed from `tile_size` โ€” centre `(coord + 0.5) ร— tile_size` (default) or corner `coord ร— tile_size` โ€” matching Godot's TileMapLayer.map_to_local, plus an optional `align` offset. With `reparent` (default true) the node is moved under the layer so the coordinate is layer-local. Decomposes onto node.reparent + node.set_property. Returns the node's new path and local position.",
1920
+ "The cell's local position is computed from `tile_size` โ€” centre `(coord + 0.5) ร— tile_size` (default) or corner `coord ร— tile_size` โ€” matching Godot's TileMapLayer.map_to_local, plus an optional `align` offset. Decomposes onto node.reparent + node.set_property. Returns the node's new path and local position.",
1816
1921
  inputSchema: {
1817
1922
  layer: z.string().describe("TileMapLayer node path in the open scene"),
1818
1923
  node: z.string().describe("Node path of the node to place (a card / piece already in the scene)"),
@@ -1835,7 +1940,7 @@ export function registerTabletopTools(server, bridge, config) {
1835
1940
  server.registerTool("piece_template_create", {
1836
1941
  title: "Create piece template",
1837
1942
  description: "Build a reusable piece (token) scene from a spec: an Art node (Sprite2D under a Node2D root, TextureRect under a Control root), an optional Label, an optional hit area (Area2D + CollisionShape2D), and an optional two-sided Back, plus a generated script-backed set_data() / set_face(). set_data binds art / color / label; set_face flips face/back visibility. " +
1838
- "Decomposes onto scene.new โ†’ node.add โ†’ node.set_property โ†’ resource.create โ†’ scene.save. DESTRUCTIVE (writes a scene + script) โ€” gated by confirmation. Returns the scene path + the created-node map.",
1943
+ "DESTRUCTIVE (writes a scene + script) โ€” gated by confirmation. Puts the editor back on the scene you had open; `edited_scene` names where it landed.",
1839
1944
  inputSchema: {
1840
1945
  path: z.string().describe("Where to save the template scene, e.g. res://ui/pieces/Piece.tscn"),
1841
1946
  size: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }).describe("Token size in px (drives the hit-area extents and, for a Control root, the Art size)"),
@@ -1881,8 +1986,13 @@ export function registerTabletopTools(server, bridge, config) {
1881
1986
  if (blocked)
1882
1987
  return blocked;
1883
1988
  try {
1989
+ // ๐Ÿ†• 299 โ€” READ BEFORE THE FIRST THING THAT CAN MOVE THE EDITOR, which is
1990
+ // `clearStaleTab` and not `scene.new`: on an overwrite the stale-tab close
1991
+ // has already changed `current` by the time the decomposition runs, so a
1992
+ // capture taken any later restores the caller to the wrong scene.
1993
+ const before = await currentEditedScene();
1884
1994
  await clearStaleTab(a.path);
1885
- return ok({ ...await emitPieceTemplate(emit, a), ...replacedField(existed) });
1995
+ return ok({ ...await emitPieceTemplate(emit, a), ...replacedField(existed), ...(await editedSceneField(before)) });
1886
1996
  }
1887
1997
  catch (err) {
1888
1998
  return fail(err);