sbuilder-mcp 0.9.0 → 0.9.2

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 CHANGED
@@ -6,6 +6,19 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.9.2] - 2026-09-09
10
+
11
+ ### Added
12
+ - sb_look now reports `stuck_note` once per process when the page pins something (`position: sticky` or `fixed`), explaining that a screenshot cannot show whether the element is actually stuck and pointing at a browser server (Playwright MCP or Chrome DevTools MCP) to scroll and check for the `wb-stuck` class.
13
+
14
+ ### Internal
15
+ - docs/tools.md and the sbuilder-site-design skill now document which of the other MCP servers (Figma, Google Stitch, Chrome DevTools, Playwright) answers which question, alongside sb_look, covering six cases a screenshot alone cannot settle: a sticky header actually engaging, a style that "did not apply", a cart drawer's click trigger, a checkout submission, an entity template previewed with a real record, and layout stability (CLS).
16
+
17
+ ## [0.9.1] - 2026-09-09
18
+
19
+ ### Fixed
20
+ - sb_set now refuses a `config.stuckAfter` write the renderer would drop on the floor — a negative, non-finite, or empty value, or one set on a node that cannot pin — instead of silently storing, saving, and publishing a threshold the runtime island never reads and quietly falling back to its automatic answer.
21
+
9
22
  ## [0.9.0] - 2026-09-08
10
23
 
11
24
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,19 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.9.2] - 2026-09-09
10
+
11
+ ### Added
12
+ - sb_look giờ báo cáo `stuck_note` một lần mỗi tiến trình khi trang có ghim một phần tử (`position: sticky` hoặc `fixed`), giải thích rằng một ảnh chụp màn hình không thể cho biết phần tử đó có thực sự bị "stuck" hay không, và chỉ ra một trình duyệt server (Playwright MCP hoặc Chrome DevTools MCP) để cuộn trang và kiểm tra class `wb-stuck`.
13
+
14
+ ### Internal
15
+ - docs/tools.md và skill sbuilder-site-design giờ ghi lại server MCP nào (Figma, Google Stitch, Chrome DevTools, Playwright) trả lời câu hỏi nào, bên cạnh sb_look, bao quát sáu trường hợp mà chỉ riêng ảnh chụp màn hình không thể trả lời: một header sticky có thực sự ghim hay không, một style "không được áp dụng", nút bấm mở giỏ hàng, việc submit checkout, xem trước một template entity với dữ liệu thật, và độ ổn định bố cục (CLS).
16
+
17
+ ## [0.9.1] - 2026-09-09
18
+
19
+ ### Fixed
20
+ - sb_set giờ từ chối ghi `config.stuckAfter` nếu giá trị sẽ bị renderer âm thầm bỏ qua — số âm, không hữu hạn, rỗng, hoặc được đặt trên một node không thể ghim — thay vì lưu, save rồi publish một ngưỡng mà runtime island không bao giờ đọc và lặng lẽ quay về đáp án tự động của nó.
21
+
9
22
  ## [0.9.0] - 2026-09-08
10
23
 
11
24
  ### Added
@@ -2,7 +2,7 @@ import { isOverlay, subtreeIds, ancestors, appBlockRoot, SPEC_GLOBAL_ID, SPEC_GL
2
2
  import { ELEMENTS, ELEMENT_SEEDS, SATELLITE_RULES } from '../../catalog/elements.generated.js';
3
3
  import { bindingsForConfig, createNode, mintSatellites } from './node.js';
4
4
  import { refuseSecondTemplate } from './traps.js';
5
- import { STUCK_STATE, refuseStuckConfig, requireStuckHost, stickySeeds } from './sticky.js';
5
+ import { STUCK_STATE, refuseStuckAfter, refuseStuckConfig, requireStuckHost, stickySeeds, } from './sticky.js';
6
6
  import { genId } from './ids.js';
7
7
  /** Append sentinel: splice clamps a too-large index, and `isSyncablePatch`
8
8
  * deliberately allows one — an append is a legitimate thing to describe. */
@@ -182,6 +182,12 @@ export function setKeys(doc, id, keys, opts) {
182
182
  // the same silent shape as an unbound element, which cost a whole page of
183
183
  // "$0.00" cards to find once.
184
184
  const rebind = namespace === 'config' ? rebindPatch(doc, id, keys) : null;
185
+ // The scroll THRESHOLD is a plain config key, and the renderer drops it in two
186
+ // different ways without a word — see sticky.ts. Checked here rather than in
187
+ // the state branch because it is written on the node that PINS, at base or at
188
+ // a breakpoint, never inside a stuck slot.
189
+ if (namespace === 'config')
190
+ refuseStuckAfter(doc.doc, id, keys);
185
191
  if (namespace === 'specials') {
186
192
  refuseComposedStamp(keys);
187
193
  // `specials` is content and identity, base-only by definition, and states
@@ -36,6 +36,17 @@ import { ancestors } from '../../core/tree.js';
36
36
  */
37
37
  /** The state key, in `node.states` and `responsive[bp].states`. */
38
38
  export const STUCK_STATE = 'stuck';
39
+ /**
40
+ * The config key holding "pinned from WHEN?", in px of page scroll.
41
+ *
42
+ * It reaches the runtime island as a CSS custom property (`--wb-stuck-after`)
43
+ * rather than through `wb:props`, because it is per breakpoint and the cascade
44
+ * is what picks the right number at the right width. Unset is the common case:
45
+ * the island then decides from the element itself — a sticky one the moment it
46
+ * is held away from its place in the flow, a fixed one the moment the page
47
+ * scrolls past where it would have sat.
48
+ */
49
+ export const STUCK_AFTER = 'stuckAfter';
39
50
  /**
40
51
  * The positions that HAVE a pinned moment.
41
52
  *
@@ -206,3 +217,32 @@ export function stickyWarning(doc, id, bp) {
206
217
  `scrolls. The node will not move. Clear overflowX/overflowY on ${blocker}, or pin a node ` +
207
218
  'outside it.');
208
219
  }
220
+ /**
221
+ * Refuse a `stuckAfter` that the renderer would drop on the floor.
222
+ *
223
+ * `stuckAfterCss` emits the custom property only for a node that CAN pin and
224
+ * that carries a usable number, and returns an empty string otherwise — so both
225
+ * mistakes here are stored, saved, published and read by nothing. The value
226
+ * rules are the platform's own: zero is honoured ("as soon as the page moves at
227
+ * all" is a real answer, and dropping it would silently fall back to the
228
+ * automatic one); negative and non-finite are not, because no scroll position
229
+ * can satisfy them.
230
+ */
231
+ export function refuseStuckAfter(doc, id, keys) {
232
+ if (!(STUCK_AFTER in keys))
233
+ return;
234
+ const raw = keys[STUCK_AFTER];
235
+ const n = typeof raw === 'number' ? raw : Number(raw);
236
+ if (raw == null || raw === '' || !Number.isFinite(n) || n < 0) {
237
+ throw new Error(`sbuilder: config.${STUCK_AFTER} is px of page scroll — a finite number, zero or more ` +
238
+ `(0 means "as soon as the page moves at all"). ${JSON.stringify(raw)} is dropped by ` +
239
+ "the renderer, which then falls back to the island's automatic answer without saying " +
240
+ 'so. To go back to automatic, remove the key.');
241
+ }
242
+ if (!isPinnedNode(doc.nodes[id])) {
243
+ throw new Error(`sbuilder: config.${STUCK_AFTER} says WHEN a pinned element counts as stuck, and ${id} ` +
244
+ 'cannot pin — the renderer emits the threshold only for a node that can ' +
245
+ '(stickyState.ts stuckAfterCss), so this would be stored and read by nothing. ' +
246
+ `Pin it first: sb_set ${id} style { position: "sticky" }.`);
247
+ }
248
+ }
@@ -7,6 +7,7 @@ import { uploadMedia } from '../transport/media.js';
7
7
  import { request } from '../transport/http.js';
8
8
  import { shoot, DEFAULT_WIDTHS } from '../vision/shoot.js';
9
9
  import { measure, MEASURE_NOTICE } from '../vision/measure.js';
10
+ import { isPinnedNode } from '../domains/site/sticky.js';
10
11
  import { compactFindings } from '../domains/site/findings.js';
11
12
  import { reviewField } from './page.js';
12
13
  import { boxesForResponse, BOXES_FORMAT } from '../vision/boxes.js';
@@ -340,10 +341,30 @@ export function registerLiveTools(server, ctx, session) {
340
341
  '("Color / Size", "Red / S") rather than the product\'s own. That is the preview, ' +
341
342
  'not the page. Pass a published storefront URL as `url` to judge a template.')
342
343
  : undefined;
344
+ // A STILL PICTURE CANNOT SHOW A PINNED ELEMENT ENGAGING, and this is the
345
+ // one tool a caller would expect to. `position: sticky` looks identical at
346
+ // rest and while stuck — that is the whole reason the platform needs a
347
+ // runtime class for it — so a page carrying a pinned node has a look this
348
+ // tool is structurally unable to photograph, however many widths it shoots.
349
+ //
350
+ // Gated on the document actually carrying one, and said once, for the same
351
+ // reason `preview_note` is: a directive that fires on pages it cannot
352
+ // apply to is noise, and noise is what makes the real notes unread.
353
+ const pinned = Object.values(session.current().doc.nodes).some((n) => isPinnedNode(n));
354
+ const stuckNote = pinned
355
+ ? ctx.notices.once('stuck-scroll', 'This page pins something (position sticky/fixed). A screenshot is ONE scroll ' +
356
+ 'position, so nothing here can show whether it engages or what it looks like ' +
357
+ 'once it does — the platform styles that moment through a class a runtime island ' +
358
+ 'toggles, "wb-stuck", and CSS alone cannot express it. To check: open the ' +
359
+ 'PUBLISHED page in a browser server (Playwright or Chrome DevTools MCP), scroll, ' +
360
+ 'and read classList for "wb-stuck". If it never appears, every stuck override on ' +
361
+ 'the page is stored and never painted.')
362
+ : undefined;
343
363
  return images(shots.map((s) => ({ dataBase64: s.imageBase64, mimeType: s.mimeType })), {
344
364
  widths: shots.map((s) => s.width),
345
365
  ...(url ? { shot: url } : {}),
346
366
  ...(previewNote ? { preview_note: previewNote } : {}),
367
+ ...(stuckNote ? { stuck_note: stuckNote } : {}),
347
368
  ...(node_id ? { framed: node_id } : {}),
348
369
  ...(with_boxes === false
349
370
  ? {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",