@rahmantech/litedesign 0.2.0 → 0.2.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/README.md CHANGED
@@ -74,6 +74,11 @@ scrolled viewport.
74
74
  toolbar. When the human refers to a part by name, look it up there instead of
75
75
  guessing a selector. Labels are read-only for the agent.
76
76
 
77
+ The server also serves its workflow as the MCP resource
78
+ `skill://litedesign/workflow` — an agent that supports resources can read the
79
+ flow (inspect first, resolve names through labels, spotlight edits with focus)
80
+ without a file in the repository.
81
+
77
82
  ## Drivers
78
83
 
79
84
  The server prefers Playwright when `playwright-core` is installed. It launches
@@ -1,5 +1,25 @@
1
1
  "use strict";
2
2
  (() => {
3
+ // packages/extension/src/diff.ts
4
+ function signatureOf(node) {
5
+ return `${node.tag}|${node.text}`;
6
+ }
7
+ function nodeSignatures(graph) {
8
+ return graph.nodes.map(signatureOf);
9
+ }
10
+ function changedNodes(before, after) {
11
+ const counts = /* @__PURE__ */ new Map();
12
+ for (const signature of before) counts.set(signature, (counts.get(signature) ?? 0) + 1);
13
+ const changed = [];
14
+ for (const node of after.nodes) {
15
+ const signature = signatureOf(node);
16
+ const seen = counts.get(signature) ?? 0;
17
+ if (seen > 0) counts.set(signature, seen - 1);
18
+ else changed.push(node);
19
+ }
20
+ return changed;
21
+ }
22
+
3
23
  // packages/extension/src/labels.ts
4
24
  var STORE_KEY = "litedesign-labels";
5
25
  function pageKey() {
@@ -129,6 +149,7 @@
129
149
 
130
150
  // packages/extension/src/store.ts
131
151
  var STORAGE_KEY = "litedesign";
152
+ var SNAPSHOT_KEY = "litedesign-snapshot";
132
153
  var DEFAULT_LAYERS = [...LAYERS];
133
154
  var DEFAULT_GRID = { step: 8, axis: "both" };
134
155
  var AXES = ["vertical", "horizontal", "both"];
@@ -186,6 +207,30 @@
186
207
  function isLayerName(value) {
187
208
  return typeof value === "string" && LAYERS.includes(value);
188
209
  }
210
+ function loadSnapshot() {
211
+ try {
212
+ const raw = sessionStorage.getItem(SNAPSHOT_KEY);
213
+ if (raw === null) return null;
214
+ const parsed = JSON.parse(raw);
215
+ if (!Array.isArray(parsed)) return null;
216
+ const signatures = parsed.filter((entry) => typeof entry === "string");
217
+ return signatures.length === parsed.length ? signatures : null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+ function saveSnapshot(graph) {
223
+ try {
224
+ sessionStorage.setItem(SNAPSHOT_KEY, JSON.stringify(nodeSignatures(graph)));
225
+ } catch {
226
+ }
227
+ }
228
+ function clearSnapshot() {
229
+ try {
230
+ sessionStorage.removeItem(SNAPSHOT_KEY);
231
+ } catch {
232
+ }
233
+ }
189
234
 
190
235
  // packages/extension/src/toolbar.ts
191
236
  var STYLE = `
@@ -524,18 +569,21 @@
524
569
  }
525
570
  const stored = await loadState();
526
571
  const layers = options.layers ?? stored.layers;
572
+ let autoFocus = null;
527
573
  if (window.__litedesign_state__ === "active") {
528
574
  window.__litedesign_layers__?.(layers);
529
575
  } else {
530
576
  window.__litedesign_state__ = "starting";
531
577
  try {
532
- mount(core2, layers, stored.grid);
578
+ autoFocus = mount(core2, layers, stored.grid);
533
579
  } catch (error) {
534
580
  window.__litedesign_state__ = void 0;
535
581
  throw error;
536
582
  }
537
583
  }
538
- if (options.focus !== void 0) window.__litedesign_set_focus__?.(options.focus);
584
+ if (options.focus !== void 0 && autoFocus === null) {
585
+ window.__litedesign_set_focus__?.(options.focus);
586
+ }
539
587
  return {
540
588
  enabled: overlayHost() !== void 0,
541
589
  layers,
@@ -553,6 +601,7 @@
553
601
  let focusId = null;
554
602
  let labels = [];
555
603
  let labelMode = false;
604
+ let marksInteractive = false;
556
605
  let toolbar;
557
606
  const teardown = () => {
558
607
  observer?.stop();
@@ -569,17 +618,22 @@
569
618
  window.__litedesign_set_focus__ = void 0;
570
619
  window.__litedesign_focus_selector__ = void 0;
571
620
  window.__litedesign_state__ = void 0;
621
+ clearSnapshot();
572
622
  void saveState({ enabled: false, layers, focus: null, grid });
573
623
  };
574
624
  const refresh = () => {
625
+ const before2 = nodeSignatures(graph);
575
626
  graph = core2.buildGraph(root2);
576
627
  if (focusId !== null && !graph.nodes.some((node) => node.id === focusId)) {
577
628
  focusSelector = null;
578
629
  focusId = null;
579
630
  handle?.setFocus(null);
580
631
  }
632
+ const [firstChanged2] = changedNodes(before2, graph);
633
+ if (firstChanged2 !== void 0) setFocus(firstChanged2.selector);
581
634
  handle?.update(graph);
582
635
  toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
636
+ saveSnapshot(graph);
583
637
  };
584
638
  const setLayers = (next) => {
585
639
  layers = next;
@@ -622,7 +676,30 @@
622
676
  void saveState({ enabled: true, layers, focus: focusSelector, grid });
623
677
  };
624
678
  const applyMarks = () => {
625
- handle?.setMarks(labels.map((label) => ({ selector: label.selector, text: label.text })));
679
+ handle?.setMarks(
680
+ labels.map((label) => ({ selector: label.selector, text: label.text })),
681
+ marksInteractive
682
+ );
683
+ };
684
+ const editLabel = (existing, rect) => {
685
+ toolbar.askLabel(
686
+ rect,
687
+ existing.text,
688
+ (text) => {
689
+ const trimmed = text.trim();
690
+ if (trimmed === "" || trimmed === existing.text) return;
691
+ void renameLabel(existing.id, trimmed).then((next) => {
692
+ labels = next;
693
+ applyMarks();
694
+ });
695
+ },
696
+ () => {
697
+ void removeLabel(existing.id).then((next) => {
698
+ labels = next;
699
+ applyMarks();
700
+ });
701
+ }
702
+ );
626
703
  };
627
704
  const onLabelClick = (event) => {
628
705
  const target = event.target;
@@ -633,24 +710,7 @@
633
710
  const selector = core2.pathOf(target);
634
711
  const existing = labels.find((label) => label.selector === selector);
635
712
  if (existing !== void 0) {
636
- toolbar.askLabel(
637
- target.getBoundingClientRect(),
638
- existing.text,
639
- (text) => {
640
- const trimmed = text.trim();
641
- if (trimmed === "" || trimmed === existing.text) return;
642
- void renameLabel(existing.id, trimmed).then((next) => {
643
- labels = next;
644
- applyMarks();
645
- });
646
- },
647
- () => {
648
- void removeLabel(existing.id).then((next) => {
649
- labels = next;
650
- applyMarks();
651
- });
652
- }
653
- );
713
+ editLabel(existing, target.getBoundingClientRect());
654
714
  return;
655
715
  }
656
716
  toolbar.askLabel(target.getBoundingClientRect(), "", (text) => {
@@ -670,6 +730,8 @@
670
730
  } else {
671
731
  document.removeEventListener("click", onLabelClick, true);
672
732
  }
733
+ marksInteractive = labelMode;
734
+ applyMarks();
673
735
  };
674
736
  toolbar = createToolbar({
675
737
  layers,
@@ -684,12 +746,29 @@
684
746
  onClose: teardown
685
747
  });
686
748
  toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
687
- handle = core2.render(graph, { layers, toolbar: toolbar.element, grid });
749
+ handle = core2.render(graph, {
750
+ layers,
751
+ toolbar: toolbar.element,
752
+ grid,
753
+ onMarkClick: (mark, anchor) => {
754
+ const existing = labels.find((label) => label.selector === mark.selector);
755
+ if (existing !== void 0) editLabel(existing, anchor);
756
+ }
757
+ });
688
758
  observer = observeChanges({ root: root2, onChange: refresh });
689
759
  void loadLabels().then((next) => {
690
760
  labels = next;
691
761
  applyMarks();
692
762
  });
763
+ const before = loadSnapshot();
764
+ const changed = before !== null ? changedNodes(before, graph) : [];
765
+ let autoFocus = null;
766
+ const [firstChanged] = changed;
767
+ if (firstChanged !== void 0) {
768
+ autoFocus = firstChanged.selector;
769
+ setFocus(firstChanged.selector);
770
+ }
771
+ saveSnapshot(graph);
693
772
  mountedHost = handle.host;
694
773
  window.__litedesign_layers__ = setLayers;
695
774
  window.__litedesign_set_focus__ = setFocus;
@@ -697,6 +776,7 @@
697
776
  window.__litedesign_teardown__ = teardown;
698
777
  window.__litedesign_state__ = "active";
699
778
  void saveState({ enabled: true, layers, focus: focusSelector, grid });
779
+ return autoFocus;
700
780
  }
701
781
 
702
782
  // packages/extension/src/bridge.ts
@@ -723,9 +803,10 @@
723
803
  switch (action) {
724
804
  case "ping":
725
805
  return { ok: true, result: true };
726
- case "toggle":
806
+ case "toggle": {
727
807
  await boot(core2);
728
- return { ok: true };
808
+ return { ok: true, result: { enabled: window.__litedesign_state__ === "active" } };
809
+ }
729
810
  case "overlay":
730
811
  return {
731
812
  ok: true,
@@ -980,9 +980,10 @@ var __litedesign = (() => {
980
980
  return layer;
981
981
  }
982
982
  var MARK_COLOR = "rgba(217, 119, 6, 0.95)";
983
- function drawMarks(layer, marks, bySelector) {
983
+ function drawMarks(layer, marks, bySelector, interactive = false) {
984
984
  layer.replaceChildren();
985
- if (marks.length === 0) return;
985
+ const badges = /* @__PURE__ */ new Map();
986
+ if (marks.length === 0) return badges;
986
987
  const scrollX = window.scrollX;
987
988
  const scrollY = window.scrollY;
988
989
  for (const mark of marks) {
@@ -990,10 +991,12 @@ var __litedesign = (() => {
990
991
  if (node === void 0) continue;
991
992
  const badge = box(
992
993
  layer,
993
- `left:${node.rect.x - scrollX}px;top:${node.rect.y - scrollY}px;transform:translateY(-100%);padding:2px 6px;border-radius:4px;background:${MARK_COLOR};color:#fff;font:10px/1.4 ui-monospace,monospace;white-space:nowrap`
994
+ `left:${node.rect.x - scrollX}px;top:${node.rect.y - scrollY}px;transform:translateY(-100%);padding:2px 6px;border-radius:4px;background:${MARK_COLOR};color:#fff;font:10px/1.4 ui-monospace,monospace;white-space:nowrap` + (interactive ? ";pointer-events:auto;cursor:pointer" : "")
994
995
  );
995
996
  badge.textContent = mark.text;
997
+ badges.set(badge, mark);
996
998
  }
999
+ return badges;
997
1000
  }
998
1001
  function drawRadius(layer, node) {
999
1002
  layer.replaceChildren();
@@ -1117,6 +1120,19 @@ var __litedesign = (() => {
1117
1120
  const layers = createLayers(shadow);
1118
1121
  const radiusLayer = createSubLayer("radius", layers.hover);
1119
1122
  const marksLayer = createSubLayer("marks", layers.hover);
1123
+ if (options.onMarkClick !== void 0) {
1124
+ const onMarkClick = options.onMarkClick;
1125
+ marksLayer.addEventListener("click", (event) => {
1126
+ if (!marksInteractive) return;
1127
+ const badge = event.target;
1128
+ if (!(badge instanceof HTMLDivElement)) return;
1129
+ const mark = badgeMarks.get(badge);
1130
+ if (mark === void 0) return;
1131
+ event.preventDefault();
1132
+ event.stopPropagation();
1133
+ onMarkClick(mark, badge.getBoundingClientRect());
1134
+ });
1135
+ }
1120
1136
  if (options.toolbar !== void 0) shadow.append(options.toolbar);
1121
1137
  document.documentElement.append(host);
1122
1138
  let current = graph;
@@ -1127,6 +1143,8 @@ var __litedesign = (() => {
1127
1143
  let focusId = null;
1128
1144
  let visible = new Set(options.layers ?? LAYERS);
1129
1145
  let marks = options.marks ?? [];
1146
+ let marksInteractive = false;
1147
+ let badgeMarks = /* @__PURE__ */ new Map();
1130
1148
  let grid = options.grid ?? { step: GRID_STEP, axis: "both" };
1131
1149
  let frame = 0;
1132
1150
  const draw = () => {
@@ -1144,7 +1162,12 @@ var __litedesign = (() => {
1144
1162
  drawDim(layers.dim, dimNode);
1145
1163
  drawFocus(layers.focus, outlineNode);
1146
1164
  drawRadius(radiusLayer, outlineNode);
1147
- drawMarks(marksLayer, marks, new Map(current.nodes.map((node) => [node.selector, node])));
1165
+ badgeMarks = drawMarks(
1166
+ marksLayer,
1167
+ marks,
1168
+ new Map(current.nodes.map((node) => [node.selector, node])),
1169
+ marksInteractive
1170
+ );
1148
1171
  drawHover(layers.hover, hovered === null ? null : byId.get(hovered) ?? null);
1149
1172
  };
1150
1173
  const schedule = () => {
@@ -1200,8 +1223,9 @@ var __litedesign = (() => {
1200
1223
  focusId = id;
1201
1224
  draw();
1202
1225
  },
1203
- setMarks(next) {
1226
+ setMarks(next, interactive = false) {
1204
1227
  marks = next;
1228
+ marksInteractive = interactive;
1205
1229
  draw();
1206
1230
  },
1207
1231
  setGrid(next) {
@@ -1224,7 +1248,7 @@ var __litedesign = (() => {
1224
1248
  }
1225
1249
 
1226
1250
  // packages/core/src/index.ts
1227
- var VERSION = "0.2.0";
1251
+ var VERSION = "0.2.2";
1228
1252
  return __toCommonJS(index_exports);
1229
1253
  })();
1230
1254
  //# sourceMappingURL=core.iife.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "litedesign",
4
- "version": "0.1.0",
4
+ "version": "0.2.2",
5
5
  "description": "Grid, gap, and alignment on the real web page.",
6
6
  "permissions": ["activeTab", "scripting", "storage"],
7
7
  "host_permissions": ["http://*/*", "https://*/*"],
@@ -6,9 +6,12 @@
6
6
  var watchButton = element("watch");
7
7
  var overlayButton = element("overlay");
8
8
  var tab;
9
- watchButton.addEventListener("click", () => void watchThisTab());
9
+ watchButton.addEventListener("click", () => void toggleWatch());
10
10
  overlayButton.addEventListener("click", () => void toggleOverlay());
11
11
  void init();
12
+ document.addEventListener("visibilitychange", () => {
13
+ if (!document.hidden) void init();
14
+ });
12
15
  async function init() {
13
16
  const [active] = await chrome.tabs.query({ active: true, currentWindow: true });
14
17
  tab = active;
@@ -17,14 +20,22 @@
17
20
  overlayButton.disabled = active?.id === void 0;
18
21
  await refresh();
19
22
  }
20
- async function watchThisTab() {
23
+ async function toggleWatch() {
21
24
  if (tab?.id === void 0) return;
22
- const reply = await ask({ type: "watch", tabId: tab.id });
25
+ const status = await ask({ type: "status" });
26
+ if (!status.ok) {
27
+ setStatus(status.error, true);
28
+ return;
29
+ }
30
+ const watching = status.result.page?.tabId === tab.id;
31
+ const reply = await ask(
32
+ watching ? { type: "unwatch", tabId: tab.id } : { type: "watch", tabId: tab.id }
33
+ );
23
34
  if (!reply.ok) {
24
35
  setStatus(reply.error, true);
25
36
  return;
26
37
  }
27
- pageLine.textContent = reply.result.url;
38
+ if (!watching && reply.result !== null) pageLine.textContent = reply.result.url;
28
39
  await refresh();
29
40
  }
30
41
  async function toggleOverlay() {
@@ -34,7 +45,12 @@
34
45
  setStatus(reply.error, true);
35
46
  return;
36
47
  }
37
- window.close();
48
+ await refresh();
49
+ if (reply.result.enabled) {
50
+ window.close();
51
+ return;
52
+ }
53
+ setStatus("overlay off");
38
54
  }
39
55
  async function refresh() {
40
56
  const reply = await ask({ type: "status" });
@@ -43,14 +59,23 @@
43
59
  return;
44
60
  }
45
61
  const status = reply.result;
62
+ if (!status.bridge) scheduleRecheck();
46
63
  const watching = tab?.id !== void 0 && status.page?.tabId === tab.id;
47
- watchButton.textContent = watching ? "Watching this tab" : "Watch this tab";
48
- watchButton.disabled = tab?.id === void 0 || watching;
64
+ watchButton.textContent = watching ? "Unwatch this tab" : "Watch this tab";
65
+ watchButton.disabled = tab?.id === void 0;
49
66
  setStatus(
50
67
  status.bridge ? `bridge online \xB7 port ${status.port}` : "bridge offline \xB7 run: litedesign ws start",
51
68
  !status.bridge
52
69
  );
53
70
  }
71
+ function scheduleRecheck() {
72
+ if (recheckTimer !== void 0) return;
73
+ recheckTimer = setTimeout(() => {
74
+ recheckTimer = void 0;
75
+ void refresh();
76
+ }, 1e3);
77
+ }
78
+ var recheckTimer;
54
79
  function setStatus(text, bad = false) {
55
80
  statusLine.textContent = text;
56
81
  statusLine.classList.toggle("bad", bad);
@@ -46,6 +46,9 @@ async function answer(message) {
46
46
  case "watch":
47
47
  if (typeof request.tabId !== "number") throw new Error("watch needs a tab id");
48
48
  return { ok: true, result: await watch(request.tabId) };
49
+ case "unwatch":
50
+ if (typeof request.tabId !== "number") throw new Error("unwatch needs a tab id");
51
+ return { ok: true, result: await unwatch(request.tabId) };
49
52
  case "overlay":
50
53
  if (typeof request.tabId !== "number") throw new Error("overlay needs a tab id");
51
54
  return { ok: true, result: await toggleOverlay(request.tabId) };
@@ -74,6 +77,18 @@ async function watch(tabId) {
74
77
  await applyStatus(tabId);
75
78
  return page;
76
79
  }
80
+ async function unwatch(tabId) {
81
+ if (current?.tabId !== tabId) return null;
82
+ try {
83
+ await chrome.tabs.sendMessage(tabId, { action: "overlay", payload: { enabled: false } });
84
+ } catch {
85
+ }
86
+ current = void 0;
87
+ await savePage(void 0);
88
+ send({ type: "bye" });
89
+ await applyStatus();
90
+ return null;
91
+ }
77
92
  async function toggleOverlay(tabId) {
78
93
  if (current?.tabId !== tabId) await watch(tabId);
79
94
  return toTab("toggle");
@@ -87,7 +102,25 @@ chrome.tabs.onRemoved.addListener((tabId) => {
87
102
  chrome.tabs.onUpdated.addListener((tabId, info) => {
88
103
  if (info.status !== "complete" || current?.tabId !== tabId) return;
89
104
  void ensureContent(tabId).catch(() => void 0);
105
+ void refreshWatchedPage(tabId).catch(() => void 0);
90
106
  });
107
+ async function refreshWatchedPage(tabId) {
108
+ const tab = await chrome.tabs.get(tabId);
109
+ if (current?.tabId !== tabId) return;
110
+ if (tab.url === current.url) return;
111
+ if (!isSupported(tab.url)) {
112
+ current = void 0;
113
+ await savePage(void 0);
114
+ send({ type: "bye" });
115
+ await applyStatus();
116
+ return;
117
+ }
118
+ const page = { tabId, url: tab.url ?? "", title: tab.title ?? "" };
119
+ current = page;
120
+ await savePage(page);
121
+ send({ type: "hello", ...page, version: chrome.runtime.getManifest().version });
122
+ await applyStatus();
123
+ }
91
124
  function connect() {
92
125
  if (socket !== void 0 && socket.readyState <= WebSocket.OPEN) return;
93
126
  const next = new WebSocket(`ws://127.0.0.1:${port}`);
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ function isMode(value) {
44
44
  }
45
45
 
46
46
  // packages/cli/src/version.ts
47
- var VERSION = "0.1.0";
47
+ var VERSION = "0.2.2";
48
48
 
49
49
  // packages/cli/src/ws.ts
50
50
  import { spawn } from "node:child_process";
@@ -1089,6 +1089,7 @@ function contrastRatio(a, b) {
1089
1089
  return (lighter + 0.05) / (darker + 0.05);
1090
1090
  }
1091
1091
  var SPACING_SCALE = 4;
1092
+ var OVERLAY_HOST_ID = "__litedesign_overlay__";
1092
1093
  function defineRule(id, run2) {
1093
1094
  return Object.assign(run2, { id });
1094
1095
  }
@@ -1329,7 +1330,14 @@ var ExtensionDriver = class {
1329
1330
  }
1330
1331
  async open(url) {
1331
1332
  this.#target = url;
1332
- if (this.#hub.find(url) !== void 0) return;
1333
+ const cached = this.#hub.find(url);
1334
+ if (cached !== void 0) {
1335
+ try {
1336
+ await this.#hub.request(cached.tabId, "ping", void 0);
1337
+ return;
1338
+ } catch {
1339
+ }
1340
+ }
1333
1341
  if (!this.#hub.hasClient()) throw new AwaitingUserError(url);
1334
1342
  const page = await this.#hub.requestAny("attach");
1335
1343
  if (!sameUrl(page.url, url)) throw new MismatchError(url, page.url);
@@ -1413,6 +1421,10 @@ import { existsSync as existsSync5 } from "node:fs";
1413
1421
  import path4 from "node:path";
1414
1422
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1415
1423
  var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
1424
+ var POLL_MS = 150;
1425
+ function sleep(ms) {
1426
+ return new Promise((resolve) => setTimeout(resolve, ms));
1427
+ }
1416
1428
  var PlaywrightDriver = class {
1417
1429
  #playwright;
1418
1430
  #corePath;
@@ -1431,6 +1443,25 @@ var PlaywrightDriver = class {
1431
1443
  await page.goto(url, { waitUntil: "load" });
1432
1444
  await this.#inject(page);
1433
1445
  }
1446
+ /**
1447
+ * "Ready" means the page was read, not that `open` once succeeded: the tab
1448
+ * can be closed or navigated away after that. The loop checks liveness and
1449
+ * the URL the session asked for.
1450
+ */
1451
+ async waitForPage(url, timeoutMs) {
1452
+ for (let waited = 0; waited < timeoutMs; waited += POLL_MS) {
1453
+ const page = this.#page;
1454
+ if (page !== void 0 && !page.isClosed() && sameUrl(page.url(), url)) {
1455
+ return { status: "ready", url: page.url() };
1456
+ }
1457
+ const current2 = this.#page;
1458
+ if (current2 !== void 0 && !current2.isClosed()) {
1459
+ return { status: "mismatch", url: current2.url() };
1460
+ }
1461
+ await sleep(POLL_MS);
1462
+ }
1463
+ return { status: "timeout" };
1464
+ }
1434
1465
  async graph() {
1435
1466
  const page = await this.#requirePage();
1436
1467
  await this.#inject(page);
@@ -1520,10 +1551,11 @@ var PlaywrightDriver = class {
1520
1551
  }
1521
1552
  /**
1522
1553
  * Labels are marks the human makes through the toolbar; a headless page has
1523
- * no human in it, so there is nothing to read.
1554
+ * no human in it. `null` says "cannot know", so `inspect` can report the
1555
+ * truth instead of a `count: 0` that reads as "no labels pinned".
1524
1556
  */
1525
1557
  async labels() {
1526
- return [];
1558
+ return null;
1527
1559
  }
1528
1560
  async overlay(options) {
1529
1561
  const page = await this.#requirePage();
@@ -1536,7 +1568,7 @@ var PlaywrightDriver = class {
1536
1568
  target.__litedesign_handle__ = void 0;
1537
1569
  if (!input.enabled) {
1538
1570
  target.__litedesign_focus__ = null;
1539
- return { enabled: false, layers: [], focus: null };
1571
+ return { applied: false, focus: null };
1540
1572
  }
1541
1573
  const graph = target.__litedesign.buildGraph(document.body);
1542
1574
  const handle = target.__litedesign.render(graph, { layers: input.layers });
@@ -1553,11 +1585,19 @@ var PlaywrightDriver = class {
1553
1585
  element?.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
1554
1586
  }
1555
1587
  }
1556
- return { enabled: true, layers: input.layers, focus };
1588
+ return { applied: true, focus };
1557
1589
  },
1558
1590
  { enabled: options.enabled, layers, focus: options.focus }
1559
1591
  );
1560
- return result;
1592
+ const attached = await page.evaluate(
1593
+ (hostId) => document.getElementById(hostId) !== null,
1594
+ OVERLAY_HOST_ID
1595
+ );
1596
+ return {
1597
+ enabled: attached,
1598
+ layers: attached ? layers : [],
1599
+ focus: attached ? result.focus : null
1600
+ };
1561
1601
  }
1562
1602
  async close() {
1563
1603
  await this.#context?.close().catch(() => void 0);
@@ -1723,9 +1763,6 @@ async function waitForPage(timeoutMs) {
1723
1763
  }
1724
1764
  const expected = session.url;
1725
1765
  const driver = session.driver;
1726
- if (driver.waitForPage === void 0) {
1727
- return { status: "ready", url: expected, expected };
1728
- }
1729
1766
  const result = await driver.waitForPage(expected, timeoutMs);
1730
1767
  if (result.status === "ready") {
1731
1768
  session.ready = true;
@@ -1790,6 +1827,14 @@ function formatTree(graph, maxLines = MAX_TREE_LINES) {
1790
1827
  }
1791
1828
  return lines.join("\n");
1792
1829
  }
1830
+ function labelsSummary(marks) {
1831
+ if (marks === null) return null;
1832
+ const names = [];
1833
+ for (const mark of marks) {
1834
+ if (!names.includes(mark.label)) names.push(mark.label);
1835
+ }
1836
+ return { count: marks.length, names };
1837
+ }
1793
1838
  function issuePayload(issues, limit) {
1794
1839
  return {
1795
1840
  issues: issues.slice(0, limit).map((issue2) => ({
@@ -1867,6 +1912,7 @@ async function inspectTool(input) {
1867
1912
  throw error;
1868
1913
  }
1869
1914
  const graph = await driver.graph();
1915
+ const labels = labelsSummary(await driver.labels());
1870
1916
  const payload = {
1871
1917
  status: "ready",
1872
1918
  url: graph.url,
@@ -1874,7 +1920,8 @@ async function inspectTool(input) {
1874
1920
  summary: {
1875
1921
  nodes: graph.nodes.length,
1876
1922
  issues: graph.issues.length,
1877
- truncated: graph.truncated
1923
+ truncated: graph.truncated,
1924
+ labels: labels ?? "unknown: this driver cannot read the toolbar"
1878
1925
  },
1879
1926
  tree: formatTree(graph)
1880
1927
  };
@@ -1887,6 +1934,12 @@ async function inspectTool(input) {
1887
1934
  async function labelsTool() {
1888
1935
  const driver = requirePage();
1889
1936
  const labels = await driver.labels();
1937
+ if (labels === null) {
1938
+ return textResult({
1939
+ labels: null,
1940
+ message: "the active driver cannot see the toolbar (headless page). Open the page in your own Chrome to read labels."
1941
+ });
1942
+ }
1890
1943
  return textResult({
1891
1944
  labels,
1892
1945
  count: labels.length
@@ -1937,6 +1990,17 @@ async function screenshotAnnotatedTool(input) {
1937
1990
  return imageResult({ ...pngSize(image), bytes: image.length }, image);
1938
1991
  }
1939
1992
 
1993
+ // packages/cli/src/mcp/workflow.ts
1994
+ var WORKFLOW_URI = "skill://litedesign/workflow";
1995
+ var WORKFLOW_TEXT = `# litedesign workflow
1996
+
1997
+ - Call \`inspect\` first, always. Its summary includes the labels the human pinned on the page.
1998
+ - If the human refers to a part of the page by name ("the login button", "the altcha box"), call \`labels\` and use the pinned selector \u2014 never guess CSS selectors.
1999
+ - Labels are read-only: you read them; the human creates, renames, and removes them.
2000
+ - To show the human what you are editing, call \`overlay\` with \`focus\`. Set \`layers\` once at the start if needed; after that, send \`focus\` only \u2014 the human owns the layer toggles in the toolbar.
2001
+ - Findings carry kind: "violation" is a measured fact; "candidate" is a heuristic that can be a false positive \u2014 verify it with \`measure\` before changing code.
2002
+ `;
2003
+
1940
2004
  // packages/cli/src/mcp/server.ts
1941
2005
  async function guard(run2) {
1942
2006
  try {
@@ -1960,6 +2024,18 @@ async function runMcpServer() {
1960
2024
  console.error(`[litedesign] extension bridge disabled: ${reason}`);
1961
2025
  }
1962
2026
  const server = new McpServer({ name: "litedesign", version: VERSION });
2027
+ server.registerResource(
2028
+ "workflow",
2029
+ WORKFLOW_URI,
2030
+ {
2031
+ title: "Workflow",
2032
+ description: "How the agent should work with this server: inspect first, resolve the human's names through labels, spotlight edits with focus.",
2033
+ mimeType: "text/markdown"
2034
+ },
2035
+ async () => ({
2036
+ contents: [{ uri: WORKFLOW_URI, text: WORKFLOW_TEXT, mimeType: "text/markdown" }]
2037
+ })
2038
+ );
1963
2039
  const viewport = z.object({
1964
2040
  w: z.number().int().positive().describe("Viewport width in CSS pixels"),
1965
2041
  h: z.number().int().positive().describe("Viewport height in CSS pixels")
@@ -1992,7 +2068,7 @@ async function runMcpServer() {
1992
2068
  "inspect",
1993
2069
  {
1994
2070
  title: "Inspect page",
1995
- description: 'Open a page and return a tree summary. Call this before measure, find_issues, overlay, screenshot, or screenshot_annotated. No image by default: the numbers the agent needs come from the DOM, so set screenshot to true only when you really need to see the page. With the extension driver this attaches to the active tab by itself; returns "awaiting_user" when no extension is connected and "mismatch" when the active tab is a different page.',
2071
+ description: 'Open a page and return a tree summary. Call this before measure, find_issues, overlay, screenshot, or screenshot_annotated. The summary includes labels: the count and unique names the human pinned onto parts of the page \u2014 when the human refers to a part by name, get its selector from the labels tool. The full workflow is available as the resource skill://litedesign/workflow. No image by default: the numbers the agent needs come from the DOM, so set screenshot to true only when you really need to see the page. With the extension driver this attaches to the active tab by itself; returns "awaiting_user" when no extension is connected and "mismatch" when the active tab is a different page.',
1996
2072
  inputSchema: {
1997
2073
  url: z.string().url(),
1998
2074
  viewport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rahmantech/litedesign",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Grid, gap, and alignment on the real web page, plus an MCP bridge for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Muhammad Abdurrahman",
@@ -22,7 +22,7 @@
22
22
  "zod": "^4.6.7"
23
23
  },
24
24
  "devDependencies": {
25
- "@litedesign/core": "0.2.0",
25
+ "@litedesign/core": "0.2.2",
26
26
  "@types/ws": "^8.18.2"
27
27
  },
28
28
  "publishConfig": {