@rahmantech/litedesign 0.0.0-stage → 0.1.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.
@@ -0,0 +1,16 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "litedesign",
4
+ "version": "0.1.0",
5
+ "description": "Grid, gap, and alignment on the real web page.",
6
+ "permissions": ["activeTab", "scripting", "storage"],
7
+ "host_permissions": ["http://*/*", "https://*/*"],
8
+ "background": {
9
+ "service_worker": "sw.js",
10
+ "type": "module"
11
+ },
12
+ "action": {
13
+ "default_title": "litedesign: watch this tab",
14
+ "default_popup": "popup.html"
15
+ }
16
+ }
@@ -0,0 +1,73 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>litedesign</title>
6
+ <style>
7
+ :root {
8
+ color-scheme: dark;
9
+ }
10
+ body {
11
+ width: 264px;
12
+ margin: 0;
13
+ padding: 12px;
14
+ background: #111111;
15
+ color: #e5e5e5;
16
+ font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
17
+ }
18
+ h1 {
19
+ margin: 0 0 8px;
20
+ font-size: 12px;
21
+ font-weight: 600;
22
+ letter-spacing: 0.02em;
23
+ }
24
+ .page {
25
+ margin: 0 0 10px;
26
+ color: #a3a3a3;
27
+ word-break: break-all;
28
+ }
29
+ button {
30
+ display: block;
31
+ width: 100%;
32
+ margin-bottom: 6px;
33
+ padding: 7px 9px;
34
+ border: 0;
35
+ border-radius: 6px;
36
+ background: #262626;
37
+ color: #d4d4d4;
38
+ font: inherit;
39
+ text-align: left;
40
+ cursor: pointer;
41
+ }
42
+ button:hover:enabled {
43
+ background: #404040;
44
+ }
45
+ button.primary {
46
+ background: #3b82f6;
47
+ color: #ffffff;
48
+ }
49
+ button.primary:hover:enabled {
50
+ background: #2563eb;
51
+ }
52
+ button:disabled {
53
+ opacity: 0.5;
54
+ cursor: default;
55
+ }
56
+ .status {
57
+ margin: 8px 0 0;
58
+ color: #a3a3a3;
59
+ }
60
+ .status.bad {
61
+ color: #f87171;
62
+ }
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <h1>litedesign</h1>
67
+ <p class="page" id="page">reading the active tab…</p>
68
+ <button type="button" id="watch" class="primary">Watch this tab</button>
69
+ <button type="button" id="overlay">Toggle overlay</button>
70
+ <p class="status" id="status">checking the bridge…</p>
71
+ <script src="popup.js"></script>
72
+ </body>
73
+ </html>
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ (() => {
3
+ // packages/extension/src/popup.ts
4
+ var pageLine = element("page");
5
+ var statusLine = element("status");
6
+ var watchButton = element("watch");
7
+ var overlayButton = element("overlay");
8
+ var tab;
9
+ watchButton.addEventListener("click", () => void watchThisTab());
10
+ overlayButton.addEventListener("click", () => void toggleOverlay());
11
+ void init();
12
+ async function init() {
13
+ const [active] = await chrome.tabs.query({ active: true, currentWindow: true });
14
+ tab = active;
15
+ pageLine.textContent = active?.url ?? "no active tab";
16
+ watchButton.disabled = active?.id === void 0;
17
+ overlayButton.disabled = active?.id === void 0;
18
+ await refresh();
19
+ }
20
+ async function watchThisTab() {
21
+ if (tab?.id === void 0) return;
22
+ const reply = await ask({ type: "watch", tabId: tab.id });
23
+ if (!reply.ok) {
24
+ setStatus(reply.error, true);
25
+ return;
26
+ }
27
+ pageLine.textContent = reply.result.url;
28
+ await refresh();
29
+ }
30
+ async function toggleOverlay() {
31
+ if (tab?.id === void 0) return;
32
+ const reply = await ask({ type: "overlay", tabId: tab.id });
33
+ if (!reply.ok) {
34
+ setStatus(reply.error, true);
35
+ return;
36
+ }
37
+ window.close();
38
+ }
39
+ async function refresh() {
40
+ const reply = await ask({ type: "status" });
41
+ if (!reply.ok) {
42
+ setStatus(reply.error, true);
43
+ return;
44
+ }
45
+ const status = reply.result;
46
+ 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;
49
+ setStatus(
50
+ status.bridge ? `bridge online \xB7 port ${status.port}` : "bridge offline \xB7 run: litedesign ws start",
51
+ !status.bridge
52
+ );
53
+ }
54
+ function setStatus(text, bad = false) {
55
+ statusLine.textContent = text;
56
+ statusLine.classList.toggle("bad", bad);
57
+ }
58
+ async function ask(message) {
59
+ try {
60
+ return await chrome.runtime.sendMessage(message);
61
+ } catch (error) {
62
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
63
+ }
64
+ }
65
+ function element(id) {
66
+ const found = document.getElementById(id);
67
+ if (found === null) throw new Error(`missing element: ${id}`);
68
+ return found;
69
+ }
70
+ })();
71
+ //# sourceMappingURL=popup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/popup.ts"],
4
+ "sourcesContent": ["import type { BridgeStatus } from './types.js'\n\n/**\n * The popup next to the icon. It shows what the worker holds and lets the\n * human bind the tab on purpose: \"watch this tab\" is the only way the agent\n * learns which page to measure without asking.\n */\n\ntype Reply<T> = { ok: true; result: T } | { ok: false; error: string }\n\nconst pageLine = element<HTMLParagraphElement>('page')\nconst statusLine = element<HTMLParagraphElement>('status')\nconst watchButton = element<HTMLButtonElement>('watch')\nconst overlayButton = element<HTMLButtonElement>('overlay')\n\nlet tab: chrome.tabs.Tab | undefined\n\nwatchButton.addEventListener('click', () => void watchThisTab())\noverlayButton.addEventListener('click', () => void toggleOverlay())\nvoid init()\n\nasync function init(): Promise<void> {\n const [active] = await chrome.tabs.query({ active: true, currentWindow: true })\n tab = active\n\n pageLine.textContent = active?.url ?? 'no active tab'\n watchButton.disabled = active?.id === undefined\n overlayButton.disabled = active?.id === undefined\n\n await refresh()\n}\n\nasync function watchThisTab(): Promise<void> {\n if (tab?.id === undefined) return\n\n const reply = await ask<{ url: string }>({ type: 'watch', tabId: tab.id })\n\n if (!reply.ok) {\n setStatus(reply.error, true)\n return\n }\n\n pageLine.textContent = reply.result.url\n await refresh()\n}\n\nasync function toggleOverlay(): Promise<void> {\n if (tab?.id === undefined) return\n\n const reply = await ask<unknown>({ type: 'overlay', tabId: tab.id })\n\n if (!reply.ok) {\n setStatus(reply.error, true)\n return\n }\n\n // The toolbar is on the page now; get out of its way.\n window.close()\n}\n\nasync function refresh(): Promise<void> {\n const reply = await ask<BridgeStatus>({ type: 'status' })\n\n if (!reply.ok) {\n setStatus(reply.error, true)\n return\n }\n\n const status = reply.result\n const watching = tab?.id !== undefined && status.page?.tabId === tab.id\n\n watchButton.textContent = watching ? 'Watching this tab' : 'Watch this tab'\n watchButton.disabled = tab?.id === undefined || watching\n\n setStatus(\n status.bridge\n ? `bridge online \u00B7 port ${status.port}`\n : 'bridge offline \u00B7 run: litedesign ws start',\n !status.bridge,\n )\n}\n\nfunction setStatus(text: string, bad = false): void {\n statusLine.textContent = text\n statusLine.classList.toggle('bad', bad)\n}\n\nasync function ask<T>(message: unknown): Promise<Reply<T>> {\n try {\n return (await chrome.runtime.sendMessage(message)) as Reply<T>\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n}\n\nfunction element<T extends HTMLElement>(id: string): T {\n const found = document.getElementById(id)\n if (found === null) throw new Error(`missing element: ${id}`)\n return found as T\n}\n"],
5
+ "mappings": ";;;AAUA,MAAM,WAAW,QAA8B,MAAM;AACrD,MAAM,aAAa,QAA8B,QAAQ;AACzD,MAAM,cAAc,QAA2B,OAAO;AACtD,MAAM,gBAAgB,QAA2B,SAAS;AAE1D,MAAI;AAEJ,cAAY,iBAAiB,SAAS,MAAM,KAAK,aAAa,CAAC;AAC/D,gBAAc,iBAAiB,SAAS,MAAM,KAAK,cAAc,CAAC;AAClE,OAAK,KAAK;AAEV,iBAAe,OAAsB;AACnC,UAAM,CAAC,MAAM,IAAI,MAAM,OAAO,KAAK,MAAM,EAAE,QAAQ,MAAM,eAAe,KAAK,CAAC;AAC9E,UAAM;AAEN,aAAS,cAAc,QAAQ,OAAO;AACtC,gBAAY,WAAW,QAAQ,OAAO;AACtC,kBAAc,WAAW,QAAQ,OAAO;AAExC,UAAM,QAAQ;AAAA,EAChB;AAEA,iBAAe,eAA8B;AAC3C,QAAI,KAAK,OAAO,OAAW;AAE3B,UAAM,QAAQ,MAAM,IAAqB,EAAE,MAAM,SAAS,OAAO,IAAI,GAAG,CAAC;AAEzE,QAAI,CAAC,MAAM,IAAI;AACb,gBAAU,MAAM,OAAO,IAAI;AAC3B;AAAA,IACF;AAEA,aAAS,cAAc,MAAM,OAAO;AACpC,UAAM,QAAQ;AAAA,EAChB;AAEA,iBAAe,gBAA+B;AAC5C,QAAI,KAAK,OAAO,OAAW;AAE3B,UAAM,QAAQ,MAAM,IAAa,EAAE,MAAM,WAAW,OAAO,IAAI,GAAG,CAAC;AAEnE,QAAI,CAAC,MAAM,IAAI;AACb,gBAAU,MAAM,OAAO,IAAI;AAC3B;AAAA,IACF;AAGA,WAAO,MAAM;AAAA,EACf;AAEA,iBAAe,UAAyB;AACtC,UAAM,QAAQ,MAAM,IAAkB,EAAE,MAAM,SAAS,CAAC;AAExD,QAAI,CAAC,MAAM,IAAI;AACb,gBAAU,MAAM,OAAO,IAAI;AAC3B;AAAA,IACF;AAEA,UAAM,SAAS,MAAM;AACrB,UAAM,WAAW,KAAK,OAAO,UAAa,OAAO,MAAM,UAAU,IAAI;AAErE,gBAAY,cAAc,WAAW,sBAAsB;AAC3D,gBAAY,WAAW,KAAK,OAAO,UAAa;AAEhD;AAAA,MACE,OAAO,SACH,2BAAwB,OAAO,IAAI,KACnC;AAAA,MACJ,CAAC,OAAO;AAAA,IACV;AAAA,EACF;AAEA,WAAS,UAAU,MAAc,MAAM,OAAa;AAClD,eAAW,cAAc;AACzB,eAAW,UAAU,OAAO,OAAO,GAAG;AAAA,EACxC;AAEA,iBAAe,IAAO,SAAqC;AACzD,QAAI;AACF,aAAQ,MAAM,OAAO,QAAQ,YAAY,OAAO;AAAA,IAClD,SAAS,OAAO;AACd,aAAO,EAAE,IAAI,OAAO,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,IACpF;AAAA,EACF;AAEA,WAAS,QAA+B,IAAe;AACrD,UAAM,QAAQ,SAAS,eAAe,EAAE;AACxC,QAAI,UAAU,KAAM,OAAM,IAAI,MAAM,oBAAoB,EAAE,EAAE;AAC5D,WAAO;AAAA,EACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,348 @@
1
+ // packages/extension/src/backoff.ts
2
+ var RECONNECT_MIN = 1e3;
3
+ var RECONNECT_MAX = 3e4;
4
+ var OFFLINE_AFTER = 3;
5
+ function backoffDelay(failures2) {
6
+ const step = Math.max(failures2 - 1, 0);
7
+ return Math.min(RECONNECT_MIN * 2 ** step, RECONNECT_MAX);
8
+ }
9
+
10
+ // packages/extension/src/sw.ts
11
+ var DEFAULT_PORT = 8787;
12
+ var PING_INTERVAL = 2e4;
13
+ var SESSION_KEY = "page";
14
+ var ONLINE_TITLE = "litedesign: watch this tab";
15
+ var socket;
16
+ var current;
17
+ var port = DEFAULT_PORT;
18
+ var failures = 0;
19
+ var offline = false;
20
+ var reconnectTimer;
21
+ var pingTimer;
22
+ var ready = start();
23
+ async function start() {
24
+ port = await readPort();
25
+ current = await readPage();
26
+ connect();
27
+ }
28
+ function wake() {
29
+ retry();
30
+ }
31
+ chrome.tabs.onActivated.addListener(wake);
32
+ chrome.windows.onFocusChanged.addListener(wake);
33
+ chrome.runtime.onMessage.addListener((message, _sender, respond) => {
34
+ void answer(message).then(respond, (error) => {
35
+ respond({ ok: false, error: messageOf(error) });
36
+ });
37
+ return true;
38
+ });
39
+ async function answer(message) {
40
+ await ready;
41
+ if (typeof message !== "object" || message === null) throw new Error("invalid message");
42
+ const request = message;
43
+ switch (request.type) {
44
+ case "status":
45
+ return { ok: true, result: status() };
46
+ case "watch":
47
+ if (typeof request.tabId !== "number") throw new Error("watch needs a tab id");
48
+ return { ok: true, result: await watch(request.tabId) };
49
+ case "overlay":
50
+ if (typeof request.tabId !== "number") throw new Error("overlay needs a tab id");
51
+ return { ok: true, result: await toggleOverlay(request.tabId) };
52
+ default:
53
+ throw new Error(`unknown message: ${String(request.type)}`);
54
+ }
55
+ }
56
+ function status() {
57
+ return {
58
+ bridge: socket?.readyState === WebSocket.OPEN,
59
+ port,
60
+ page: current ?? null
61
+ };
62
+ }
63
+ async function watch(tabId) {
64
+ const tab = await chrome.tabs.get(tabId);
65
+ if (!isSupported(tab.url)) {
66
+ throw new Error("this tab is not an http(s) page. Open the page you want to measure.");
67
+ }
68
+ const page = { tabId, url: tab.url ?? "", title: tab.title ?? "" };
69
+ await ensureContent(tabId);
70
+ current = page;
71
+ await savePage(page);
72
+ send({ type: "hello", ...page, version: chrome.runtime.getManifest().version });
73
+ retry();
74
+ await applyStatus(tabId);
75
+ return page;
76
+ }
77
+ async function toggleOverlay(tabId) {
78
+ if (current?.tabId !== tabId) await watch(tabId);
79
+ return toTab("toggle");
80
+ }
81
+ chrome.tabs.onRemoved.addListener((tabId) => {
82
+ if (current?.tabId !== tabId) return;
83
+ current = void 0;
84
+ void savePage(void 0);
85
+ send({ type: "bye" });
86
+ });
87
+ chrome.tabs.onUpdated.addListener((tabId, info) => {
88
+ if (info.status !== "complete" || current?.tabId !== tabId) return;
89
+ void ensureContent(tabId).catch(() => void 0);
90
+ });
91
+ function connect() {
92
+ if (socket !== void 0 && socket.readyState <= WebSocket.OPEN) return;
93
+ const next = new WebSocket(`ws://127.0.0.1:${port}`);
94
+ socket = next;
95
+ next.addEventListener("open", () => {
96
+ failures = 0;
97
+ void setOnline();
98
+ if (current !== void 0) {
99
+ send({ type: "hello", ...current, version: chrome.runtime.getManifest().version });
100
+ }
101
+ startPing();
102
+ });
103
+ next.addEventListener("message", (event) => {
104
+ void onMessage(event.data);
105
+ });
106
+ next.addEventListener("close", () => {
107
+ if (socket === next) socket = void 0;
108
+ stopPing();
109
+ failures += 1;
110
+ if (current !== void 0 && failures >= OFFLINE_AFTER) void setOffline();
111
+ scheduleReconnect();
112
+ });
113
+ next.addEventListener("error", () => {
114
+ });
115
+ }
116
+ function retry() {
117
+ failures = 0;
118
+ if (reconnectTimer !== void 0) {
119
+ clearTimeout(reconnectTimer);
120
+ reconnectTimer = void 0;
121
+ }
122
+ connect();
123
+ }
124
+ function scheduleReconnect() {
125
+ if (reconnectTimer !== void 0) return;
126
+ const delay = backoffDelay(failures) + Math.round(Math.random() * 250);
127
+ reconnectTimer = setTimeout(() => {
128
+ reconnectTimer = void 0;
129
+ connect();
130
+ }, delay);
131
+ }
132
+ async function setOffline() {
133
+ const first = !offline;
134
+ offline = true;
135
+ if (first) {
136
+ console.info(
137
+ `[litedesign] no MCP server on ws://127.0.0.1:${port} after ${OFFLINE_AFTER} attempts. Start "litedesign mcp"; the bridge reconnects by itself.`
138
+ );
139
+ }
140
+ await applyStatus();
141
+ }
142
+ async function setOnline() {
143
+ if (!offline) return;
144
+ offline = false;
145
+ await applyStatus();
146
+ }
147
+ function offlineTitle() {
148
+ return `litedesign: no MCP server on port ${port} \u2014 start "litedesign mcp"`;
149
+ }
150
+ async function applyStatus(tabId = current?.tabId) {
151
+ const badge = offline ? "!" : "";
152
+ const title = offline ? offlineTitle() : ONLINE_TITLE;
153
+ try {
154
+ await chrome.action.setBadgeText({ text: badge });
155
+ await chrome.action.setBadgeBackgroundColor({ color: "#dc2626" });
156
+ await chrome.action.setTitle({ title });
157
+ if (tabId !== void 0) {
158
+ await chrome.action.setBadgeText({ tabId, text: badge });
159
+ await chrome.action.setTitle({ tabId, title });
160
+ }
161
+ } catch {
162
+ }
163
+ }
164
+ function startPing() {
165
+ stopPing();
166
+ pingTimer = setInterval(() => send({ type: "ping" }), PING_INTERVAL);
167
+ }
168
+ function stopPing() {
169
+ if (pingTimer !== void 0) clearInterval(pingTimer);
170
+ pingTimer = void 0;
171
+ }
172
+ function send(message) {
173
+ if (socket?.readyState !== WebSocket.OPEN) return;
174
+ socket.send(JSON.stringify(message));
175
+ }
176
+ async function onMessage(raw) {
177
+ let message;
178
+ try {
179
+ const parsed = JSON.parse(String(raw));
180
+ if (typeof parsed !== "object" || parsed === null) return;
181
+ message = parsed;
182
+ } catch {
183
+ return;
184
+ }
185
+ if (message.type === "ping") {
186
+ send({ type: "pong" });
187
+ return;
188
+ }
189
+ if (message.type === "request" && typeof message.id === "string" && typeof message.action === "string") {
190
+ await handleRequest(message.id, message.action, message.payload);
191
+ }
192
+ }
193
+ async function handleRequest(id, action, payload) {
194
+ try {
195
+ const result = await route(action, payload);
196
+ send({ type: "response", id, ok: true, result });
197
+ } catch (error) {
198
+ send({ type: "response", id, ok: false, error: messageOf(error) });
199
+ }
200
+ }
201
+ async function route(action, payload) {
202
+ switch (action) {
203
+ case "attach":
204
+ return attach();
205
+ case "graph":
206
+ case "measure":
207
+ case "overlay":
208
+ return toTab(action, payload);
209
+ case "screenshot":
210
+ return capture("screenshot_prepare", payload);
211
+ case "annotate":
212
+ return capture("annotate_prepare", payload);
213
+ default:
214
+ throw new Error(`unknown action: ${action}`);
215
+ }
216
+ }
217
+ async function attach() {
218
+ const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
219
+ if (tab?.id === void 0 || !isSupported(tab.url)) {
220
+ throw new Error(
221
+ "the active tab is not an http(s) page. Switch to the page you want to measure."
222
+ );
223
+ }
224
+ const page = { tabId: tab.id, url: tab.url ?? "", title: tab.title ?? "" };
225
+ await ensureContent(page.tabId);
226
+ current = page;
227
+ await savePage(page);
228
+ send({ type: "hello", ...page, version: chrome.runtime.getManifest().version });
229
+ return page;
230
+ }
231
+ async function capture(action, payload) {
232
+ const page = requirePage();
233
+ const tab = await chrome.tabs.get(page.tabId);
234
+ if (tab.active !== true) {
235
+ throw new Error("the page is not the active tab. Switch to it and try again.");
236
+ }
237
+ if (tab.windowId === void 0) throw new Error("the page has no window.");
238
+ const prepared = await toTabRaw(action, payload);
239
+ if (!prepared.ok) throw new Error(prepared.error);
240
+ const prep = prepared.result ?? {};
241
+ try {
242
+ const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });
243
+ const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
244
+ return {
245
+ data: prep.rect === void 0 ? base64 : await crop(base64, prep.rect, prep.dpr ?? 1)
246
+ };
247
+ } finally {
248
+ await toTabRaw("capture_cleanup").catch(() => void 0);
249
+ }
250
+ }
251
+ async function toTab(action, payload) {
252
+ const response = await toTabRaw(action, payload);
253
+ if (!response.ok) throw new Error(response.error);
254
+ return response.result;
255
+ }
256
+ async function toTabRaw(action, payload) {
257
+ const page = requirePage();
258
+ await ensureContent(page.tabId);
259
+ const response = await chrome.tabs.sendMessage(page.tabId, { action, payload });
260
+ if (response === void 0) throw new Error("the content script did not respond");
261
+ return response;
262
+ }
263
+ async function ensureContent(tabId) {
264
+ try {
265
+ const response = await chrome.tabs.sendMessage(tabId, { action: "ping" });
266
+ if (response?.ok === true) return;
267
+ } catch {
268
+ }
269
+ await chrome.scripting.executeScript({
270
+ target: { tabId },
271
+ files: ["core.iife.js", "content.js"]
272
+ });
273
+ }
274
+ async function crop(base64, rect, dpr) {
275
+ const bitmap = await createImageBitmap(toBlob(base64));
276
+ const x = clamp(Math.round(rect.x * dpr), 0, bitmap.width);
277
+ const y = clamp(Math.round(rect.y * dpr), 0, bitmap.height);
278
+ const w = clamp(Math.round(rect.w * dpr), 1, bitmap.width - x);
279
+ const h = clamp(Math.round(rect.h * dpr), 1, bitmap.height - y);
280
+ if (w <= 0 || h <= 0) throw new Error("the element is outside the visible viewport.");
281
+ const canvas = new OffscreenCanvas(w, h);
282
+ const context = canvas.getContext("2d");
283
+ if (context === null) throw new Error("could not create an offscreen canvas.");
284
+ context.drawImage(bitmap, x, y, w, h, 0, 0, w, h);
285
+ bitmap.close();
286
+ const blob = await canvas.convertToBlob({ type: "png" });
287
+ return toBase64(await blob.arrayBuffer());
288
+ }
289
+ function toBlob(base64) {
290
+ const binary = atob(base64);
291
+ const bytes = new Uint8Array(binary.length);
292
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
293
+ return new Blob([bytes], { type: "image/png" });
294
+ }
295
+ function toBase64(buffer) {
296
+ const bytes = new Uint8Array(buffer);
297
+ const chunk = 32768;
298
+ let binary = "";
299
+ for (let i = 0; i < bytes.length; i += chunk) {
300
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
301
+ }
302
+ return btoa(binary);
303
+ }
304
+ function requirePage() {
305
+ if (current === void 0) {
306
+ throw new Error("no page is attached. Call inspect first so the extension can attach to it.");
307
+ }
308
+ return current;
309
+ }
310
+ function isSupported(url) {
311
+ return url !== void 0 && /^(https?|file):/.test(url);
312
+ }
313
+ function clamp(value, min, max) {
314
+ return Math.min(Math.max(value, min), max);
315
+ }
316
+ function messageOf(error) {
317
+ if (error instanceof Error && error.message !== "") return error.message;
318
+ if (typeof error === "string" && error !== "") return error;
319
+ return "unexpected error";
320
+ }
321
+ async function readPort() {
322
+ try {
323
+ const response = await fetch(chrome.runtime.getURL("config.json"), { cache: "no-store" });
324
+ if (!response.ok) return DEFAULT_PORT;
325
+ const config = await response.json();
326
+ const value = config.port;
327
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536 ? value : DEFAULT_PORT;
328
+ } catch {
329
+ return DEFAULT_PORT;
330
+ }
331
+ }
332
+ async function readPage() {
333
+ try {
334
+ const stored = await chrome.storage.session.get(SESSION_KEY);
335
+ const value = stored[SESSION_KEY];
336
+ return value !== void 0 && typeof value.tabId === "number" && typeof value.url === "string" ? value : void 0;
337
+ } catch {
338
+ return void 0;
339
+ }
340
+ }
341
+ async function savePage(page) {
342
+ try {
343
+ if (page === void 0) await chrome.storage.session.remove(SESSION_KEY);
344
+ else await chrome.storage.session.set({ [SESSION_KEY]: page });
345
+ } catch {
346
+ }
347
+ }
348
+ //# sourceMappingURL=sw.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/backoff.ts", "../src/sw.ts"],
4
+ "sourcesContent": ["/**\n * Reconnect policy for the service-worker socket. Pure on purpose: the worker\n * itself cannot be unit tested, but the numbers behind it can.\n *\n * The socket is also the control channel: the MCP server asks the worker to\n * attach to the active tab over it. So the worker never stops trying. It backs\n * off instead, and switching tabs retries immediately.\n */\n\nexport const RECONNECT_MIN = 1000\nexport const RECONNECT_MAX = 30000\n\n/** Failures before the icon admits the hub is not there. */\nexport const OFFLINE_AFTER = 3\n\n/** Delay before the next attempt. `failures` counts the attempts already made. */\nexport function backoffDelay(failures: number): number {\n const step = Math.max(failures - 1, 0)\n return Math.min(RECONNECT_MIN * 2 ** step, RECONNECT_MAX)\n}\n", "import { backoffDelay, OFFLINE_AFTER } from './backoff.js'\nimport type { BridgeStatus } from './types.js'\n\n/**\n * The service worker owns the WebSocket to the MCP hub, forwards measurement\n * requests to the content script, and captures the visible tab.\n *\n * MV3 kills the worker when it goes idle. A WebSocket with traffic every 20s\n * keeps it alive; if Chrome still stops it, module init reconnects and restores\n * the last page from `chrome.storage.session`.\n *\n * The socket is also the control channel. The MCP server asks the worker to\n * attach to whatever tab the human is looking at, so nobody has to click the\n * icon before an agent can measure. That is why the worker keeps retrying: a\n * stopped socket would make the extension unreachable until a click.\n */\n\nconst DEFAULT_PORT = 8787\nconst PING_INTERVAL = 20000\nconst SESSION_KEY = 'page'\nconst ONLINE_TITLE = 'litedesign: watch this tab'\n\ntype Page = { tabId: number; url: string; title: string }\n\ntype BridgeResult = { ok: true; result?: unknown } | { ok: false; error: string }\n\ntype Prepared = { rect?: Rect; dpr?: number }\n\ntype Rect = { x: number; y: number; w: number; h: number }\n\nlet socket: WebSocket | undefined\nlet current: Page | undefined\nlet port = DEFAULT_PORT\nlet failures = 0\nlet offline = false\nlet reconnectTimer: ReturnType<typeof setTimeout> | undefined\nlet pingTimer: ReturnType<typeof setInterval> | undefined\n\nconst ready = start()\n\nasync function start(): Promise<void> {\n port = await readPort()\n current = await readPage()\n\n connect()\n}\n\n/**\n * The human is looking at a tab, which is the moment the hub may want to\n * attach. Retrying here means the first `inspect` after starting the server\n * does not wait for the backoff timer.\n */\nfunction wake(): void {\n retry()\n}\n\nchrome.tabs.onActivated.addListener(wake)\nchrome.windows.onFocusChanged.addListener(wake)\n\n/**\n * The popup asks the worker what it holds and for explicit actions. The icon\n * opens that menu instead of toggling the overlay, so binding a tab is the\n * human's decision rather than a side effect of a click.\n */\nchrome.runtime.onMessage.addListener((message, _sender, respond) => {\n void answer(message).then(respond, (error: unknown) => {\n respond({ ok: false, error: messageOf(error) })\n })\n\n // Keeps the channel open for the async answer.\n return true\n})\n\nasync function answer(message: unknown): Promise<BridgeResult> {\n await ready\n\n if (typeof message !== 'object' || message === null) throw new Error('invalid message')\n\n const request = message as { type?: unknown; tabId?: unknown }\n\n switch (request.type) {\n case 'status':\n return { ok: true, result: status() }\n case 'watch':\n if (typeof request.tabId !== 'number') throw new Error('watch needs a tab id')\n return { ok: true, result: await watch(request.tabId) }\n case 'overlay':\n if (typeof request.tabId !== 'number') throw new Error('overlay needs a tab id')\n return { ok: true, result: await toggleOverlay(request.tabId) }\n default:\n throw new Error(`unknown message: ${String(request.type)}`)\n }\n}\n\nfunction status(): BridgeStatus {\n return {\n bridge: socket?.readyState === WebSocket.OPEN,\n port,\n page: current ?? null,\n }\n}\n\n/**\n * Binds the worker to one tab on purpose. This is the same state `attach`\n * produces for the agent, but started by the human from the popup.\n */\nasync function watch(tabId: number): Promise<Page> {\n const tab = await chrome.tabs.get(tabId)\n\n if (!isSupported(tab.url)) {\n throw new Error('this tab is not an http(s) page. Open the page you want to measure.')\n }\n\n const page: Page = { tabId, url: tab.url ?? '', title: tab.title ?? '' }\n\n await ensureContent(tabId)\n\n current = page\n await savePage(page)\n send({ type: 'hello', ...page, version: chrome.runtime.getManifest().version })\n retry()\n await applyStatus(tabId)\n\n return page\n}\n\n/** The overlay belongs to the watched tab, so watch it first when needed. */\nasync function toggleOverlay(tabId: number): Promise<unknown> {\n if (current?.tabId !== tabId) await watch(tabId)\n return toTab('toggle')\n}\n\nchrome.tabs.onRemoved.addListener((tabId) => {\n if (current?.tabId !== tabId) return\n\n current = undefined\n void savePage(undefined)\n send({ type: 'bye' })\n\n // Keep the socket: the hub may still ask this worker to attach to another tab.\n})\n\n/**\n * A reload drops the content script, and with it the overlay. Re-inject once\n * the watched tab has finished loading; the content script restores the\n * overlay from storage, so saving a file during an edit keeps the grid up.\n */\nchrome.tabs.onUpdated.addListener((tabId, info) => {\n if (info.status !== 'complete' || current?.tabId !== tabId) return\n\n void ensureContent(tabId).catch(() => undefined)\n})\n\nfunction connect(): void {\n if (socket !== undefined && socket.readyState <= WebSocket.OPEN) return\n\n const next = new WebSocket(`ws://127.0.0.1:${port}`)\n socket = next\n\n next.addEventListener('open', () => {\n failures = 0\n void setOnline()\n\n if (current !== undefined) {\n send({ type: 'hello', ...current, version: chrome.runtime.getManifest().version })\n }\n\n startPing()\n })\n\n next.addEventListener('message', (event) => {\n void onMessage(event.data)\n })\n\n next.addEventListener('close', () => {\n if (socket === next) socket = undefined\n\n stopPing()\n failures += 1\n\n // Only complain when there is a page waiting on the bridge.\n if (current !== undefined && failures >= OFFLINE_AFTER) void setOffline()\n\n scheduleReconnect()\n })\n\n next.addEventListener('error', () => {\n // `close` follows; reconnection is handled there.\n })\n}\n\n/** The icon click means the user expects a server now, so start a fresh run. */\nfunction retry(): void {\n failures = 0\n\n if (reconnectTimer !== undefined) {\n clearTimeout(reconnectTimer)\n reconnectTimer = undefined\n }\n\n connect()\n}\n\nfunction scheduleReconnect(): void {\n if (reconnectTimer !== undefined) return\n\n const delay = backoffDelay(failures) + Math.round(Math.random() * 250)\n reconnectTimer = setTimeout(() => {\n reconnectTimer = undefined\n connect()\n }, delay)\n}\n\n/**\n * Says the bridge is down once, then keeps retrying quietly. The browser logs\n * each failed attempt itself, so one clear line beats one per attempt.\n */\nasync function setOffline(): Promise<void> {\n const first = !offline\n offline = true\n\n if (first) {\n console.info(\n `[litedesign] no MCP server on ws://127.0.0.1:${port} after ${OFFLINE_AFTER} attempts. ` +\n 'Start \"litedesign mcp\"; the bridge reconnects by itself.',\n )\n }\n\n await applyStatus()\n}\n\nasync function setOnline(): Promise<void> {\n if (!offline) return\n\n offline = false\n await applyStatus()\n}\n\nfunction offlineTitle(): string {\n return `litedesign: no MCP server on port ${port} \u2014 start \"litedesign mcp\"`\n}\n\n/** Global for new tabs, per-tab for the tab we care about. */\nasync function applyStatus(tabId: number | undefined = current?.tabId): Promise<void> {\n const badge = offline ? '!' : ''\n const title = offline ? offlineTitle() : ONLINE_TITLE\n\n try {\n await chrome.action.setBadgeText({ text: badge })\n await chrome.action.setBadgeBackgroundColor({ color: '#dc2626' })\n await chrome.action.setTitle({ title })\n\n if (tabId !== undefined) {\n await chrome.action.setBadgeText({ tabId, text: badge })\n await chrome.action.setTitle({ tabId, title })\n }\n } catch {\n // The action API is unavailable while Chrome shuts down.\n }\n}\n\nfunction startPing(): void {\n stopPing()\n pingTimer = setInterval(() => send({ type: 'ping' }), PING_INTERVAL)\n}\n\nfunction stopPing(): void {\n if (pingTimer !== undefined) clearInterval(pingTimer)\n pingTimer = undefined\n}\n\nfunction send(message: unknown): void {\n if (socket?.readyState !== WebSocket.OPEN) return\n socket.send(JSON.stringify(message))\n}\n\nasync function onMessage(raw: unknown): Promise<void> {\n let message: Record<string, unknown>\n\n try {\n const parsed: unknown = JSON.parse(String(raw))\n if (typeof parsed !== 'object' || parsed === null) return\n message = parsed as Record<string, unknown>\n } catch {\n return\n }\n\n if (message.type === 'ping') {\n send({ type: 'pong' })\n return\n }\n\n if (\n message.type === 'request' &&\n typeof message.id === 'string' &&\n typeof message.action === 'string'\n ) {\n await handleRequest(message.id, message.action, message.payload)\n }\n}\n\nasync function handleRequest(id: string, action: string, payload: unknown): Promise<void> {\n try {\n const result = await route(action, payload)\n send({ type: 'response', id, ok: true, result })\n } catch (error) {\n send({ type: 'response', id, ok: false, error: messageOf(error) })\n }\n}\n\nasync function route(action: string, payload: unknown): Promise<unknown> {\n switch (action) {\n case 'attach':\n return attach()\n case 'graph':\n case 'measure':\n case 'overlay':\n return toTab(action, payload)\n case 'screenshot':\n return capture('screenshot_prepare', payload)\n case 'annotate':\n return capture('annotate_prepare', payload)\n default:\n throw new Error(`unknown action: ${action}`)\n }\n}\n\n/**\n * Binds to the tab the human is looking at. This is what removes the click:\n * the server asks for a page, the worker picks the active one and announces it.\n */\nasync function attach(): Promise<Page> {\n const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })\n\n if (tab?.id === undefined || !isSupported(tab.url)) {\n throw new Error(\n 'the active tab is not an http(s) page. Switch to the page you want to measure.',\n )\n }\n\n const page: Page = { tabId: tab.id, url: tab.url ?? '', title: tab.title ?? '' }\n\n await ensureContent(page.tabId)\n\n current = page\n await savePage(page)\n send({ type: 'hello', ...page, version: chrome.runtime.getManifest().version })\n\n return page\n}\n\nasync function capture(\n action: 'screenshot_prepare' | 'annotate_prepare',\n payload: unknown,\n): Promise<{ data: string }> {\n const page = requirePage()\n const tab = await chrome.tabs.get(page.tabId)\n\n if (tab.active !== true) {\n throw new Error('the page is not the active tab. Switch to it and try again.')\n }\n\n if (tab.windowId === undefined) throw new Error('the page has no window.')\n\n const prepared = await toTabRaw(action, payload)\n if (!prepared.ok) throw new Error(prepared.error)\n\n const prep = (prepared.result ?? {}) as Prepared\n\n try {\n const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' })\n const base64 = dataUrl.slice(dataUrl.indexOf(',') + 1)\n\n return {\n data: prep.rect === undefined ? base64 : await crop(base64, prep.rect, prep.dpr ?? 1),\n }\n } finally {\n await toTabRaw('capture_cleanup').catch(() => undefined)\n }\n}\n\nasync function toTab(action: string, payload?: unknown): Promise<unknown> {\n const response = await toTabRaw(action, payload)\n if (!response.ok) throw new Error(response.error)\n return response.result\n}\n\nasync function toTabRaw(action: string, payload?: unknown): Promise<BridgeResult> {\n const page = requirePage()\n await ensureContent(page.tabId)\n\n const response = (await chrome.tabs.sendMessage(page.tabId, { action, payload })) as\n | BridgeResult\n | undefined\n\n if (response === undefined) throw new Error('the content script did not respond')\n return response\n}\n\n/**\n * Injects on demand. The icon click is the usual path, but a reconnecting\n * service worker may ask for a measurement after the content script is gone.\n */\nasync function ensureContent(tabId: number): Promise<void> {\n try {\n const response = (await chrome.tabs.sendMessage(tabId, { action: 'ping' })) as\n | BridgeResult\n | undefined\n\n if (response?.ok === true) return\n } catch {\n // No receiver yet; fall through to injection.\n }\n\n await chrome.scripting.executeScript({\n target: { tabId },\n files: ['core.iife.js', 'content.js'],\n })\n}\n\nasync function crop(base64: string, rect: Rect, dpr: number): Promise<string> {\n const bitmap = await createImageBitmap(toBlob(base64))\n const x = clamp(Math.round(rect.x * dpr), 0, bitmap.width)\n const y = clamp(Math.round(rect.y * dpr), 0, bitmap.height)\n const w = clamp(Math.round(rect.w * dpr), 1, bitmap.width - x)\n const h = clamp(Math.round(rect.h * dpr), 1, bitmap.height - y)\n\n if (w <= 0 || h <= 0) throw new Error('the element is outside the visible viewport.')\n\n const canvas = new OffscreenCanvas(w, h)\n const context = canvas.getContext('2d')\n if (context === null) throw new Error('could not create an offscreen canvas.')\n\n context.drawImage(bitmap, x, y, w, h, 0, 0, w, h)\n bitmap.close()\n\n const blob = await canvas.convertToBlob({ type: 'png' })\n return toBase64(await blob.arrayBuffer())\n}\n\nfunction toBlob(base64: string): Blob {\n const binary = atob(base64)\n const bytes = new Uint8Array(binary.length)\n\n for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i)\n\n return new Blob([bytes], { type: 'image/png' })\n}\n\nfunction toBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer)\n const chunk = 0x8000\n let binary = ''\n\n for (let i = 0; i < bytes.length; i += chunk) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunk))\n }\n\n return btoa(binary)\n}\n\nfunction requirePage(): Page {\n if (current === undefined) {\n throw new Error('no page is attached. Call inspect first so the extension can attach to it.')\n }\n\n return current\n}\n\nfunction isSupported(url: string | undefined): boolean {\n return url !== undefined && /^(https?|file):/.test(url)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n\nfunction messageOf(error: unknown): string {\n if (error instanceof Error && error.message !== '') return error.message\n if (typeof error === 'string' && error !== '') return error\n return 'unexpected error'\n}\n\nasync function readPort(): Promise<number> {\n try {\n const response = await fetch(chrome.runtime.getURL('config.json'), { cache: 'no-store' })\n if (!response.ok) return DEFAULT_PORT\n\n const config = (await response.json()) as { port?: unknown }\n const value = config.port\n\n return typeof value === 'number' && Number.isInteger(value) && value > 0 && value < 65536\n ? value\n : DEFAULT_PORT\n } catch {\n return DEFAULT_PORT\n }\n}\n\nasync function readPage(): Promise<Page | undefined> {\n try {\n const stored = await chrome.storage.session.get(SESSION_KEY)\n const value = stored[SESSION_KEY] as Page | undefined\n\n return value !== undefined && typeof value.tabId === 'number' && typeof value.url === 'string'\n ? value\n : undefined\n } catch {\n return undefined\n }\n}\n\nasync function savePage(page: Page | undefined): Promise<void> {\n try {\n if (page === undefined) await chrome.storage.session.remove(SESSION_KEY)\n else await chrome.storage.session.set({ [SESSION_KEY]: page })\n } catch {\n // The connection still works without the restore hint.\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["failures"]
7
+ }