mesurer-solid 0.2.1 → 0.2.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mesurer-solid",
3
- "version": "0.2.1",
3
+ "version": "0.2.2-beta.1",
4
4
  "description": "Framework-agnostic UI measurement and inspection tools for browser apps and coding agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,9 @@
8
8
  "module": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
10
  "bin": {
11
- "mesurer-skill": "scripts/install-skill.mjs"
11
+ "mesurer-skill": "scripts/install-skill.mjs",
12
+ "mesurer-codex": "scripts/codex-bridge.mjs",
13
+ "mesurer-codex-connect": "scripts/codex-connect.mjs"
12
14
  },
13
15
  "exports": {
14
16
  ".": {
@@ -58,7 +60,11 @@
58
60
  "dist",
59
61
  "skills",
60
62
  "plugins/codex",
63
+ "codex",
61
64
  "scripts/install-skill.mjs",
65
+ "scripts/codex-bridge.mjs",
66
+ "scripts/codex-connect.mjs",
67
+ "scripts/codex-lifecycle.mjs",
62
68
  "README.md",
63
69
  "AGENT_INTEGRATION.md",
64
70
  "LICENSE",
@@ -17,6 +17,7 @@ export type CodexBridgeRequest = {
17
17
  message?: string;
18
18
  deliveryId?: string;
19
19
  queuedSubmissionId?: string;
20
+ clientUserMessageId?: string;
20
21
  };
21
22
 
22
23
  export type CodexBridgeOptions = {
@@ -57,6 +58,7 @@ export type CodexBridgeResponse = {
57
58
  status?: "queued" | "working" | "completed" | "interrupted";
58
59
  turnId?: string | null;
59
60
  queuedSubmissionId?: string | null;
61
+ clientUserMessageId?: string | null;
60
62
  dispatch?: "persisted" | "desktop-opened" | "desktop-wake-failed" | null;
61
63
  dispatchError?: string | null;
62
64
  createdAt?: number;
@@ -19,8 +19,6 @@ const MAX_DISCOVERED_THREADS = 10;
19
19
 
20
20
  const TURN_HISTORY_LIMIT = 100;
21
21
 
22
- const DELIVERY_TTL_MS = 24 * 60 * 60_000;
23
-
24
22
  const TERMINAL_DELIVERY_TTL_MS = 10 * 60_000;
25
23
 
26
24
  const DELIVERY_TURN_START_SKEW_MS = 5_000;
@@ -678,6 +676,7 @@ const publicDelivery = (delivery) => ({
678
676
  status: delivery.status,
679
677
  turnId: delivery.turnId,
680
678
  queuedSubmissionId: delivery.queuedSubmissionId,
679
+ clientUserMessageId: delivery.clientUserMessageId,
681
680
  dispatch: delivery.dispatch,
682
681
  dispatchError: delivery.dispatchError,
683
682
  createdAt: delivery.createdAt,
@@ -692,6 +691,7 @@ const persistedDelivery = (delivery) => ({
692
691
  status: delivery.status,
693
692
  turnId: delivery.turnId,
694
693
  queuedSubmissionId: delivery.queuedSubmissionId,
694
+ clientUserMessageId: delivery.clientUserMessageId,
695
695
  transport: delivery.transport,
696
696
  dispatch: delivery.dispatch,
697
697
  dispatchError: delivery.dispatchError,
@@ -699,21 +699,28 @@ const persistedDelivery = (delivery) => ({
699
699
  updatedAt: delivery.updatedAt,
700
700
  });
701
701
 
702
+ const deliveryIsTerminal = (delivery) =>
703
+ delivery.status === "completed" || delivery.status === "interrupted";
704
+
702
705
  const pruneDeliveries = () => {
703
706
  const now = Date.now();
704
707
 
705
708
  for (const [id, delivery] of deliveries) {
706
- const terminal = delivery.status === "completed" || delivery.status === "interrupted";
707
- const ttl = terminal ? TERMINAL_DELIVERY_TTL_MS : DELIVERY_TTL_MS;
709
+ if (!deliveryIsTerminal(delivery)) continue;
708
710
 
709
- if (now - delivery.updatedAt > ttl) deliveries.delete(id);
711
+ if (now - delivery.updatedAt > TERMINAL_DELIVERY_TTL_MS) {
712
+ deliveries.delete(id);
713
+ }
710
714
  }
711
715
 
712
716
  if (deliveries.size <= MAX_DELIVERIES) return;
713
717
 
714
- const oldest = [...deliveries.values()].sort((left, right) => left.updatedAt - right.updatedAt);
718
+ const terminal = [...deliveries.values()]
719
+ .filter(deliveryIsTerminal)
720
+ .sort((left, right) => left.updatedAt - right.updatedAt);
715
721
 
716
- for (const delivery of oldest.slice(0, deliveries.size - MAX_DELIVERIES)) {
722
+ for (const delivery of terminal) {
723
+ if (deliveries.size <= MAX_DELIVERIES) break;
717
724
  deliveries.delete(delivery.id);
718
725
  }
719
726
  };
@@ -760,6 +767,7 @@ const loadDeliveries = async (options) => {
760
767
  status,
761
768
  turnId: normalizeString(value?.turnId),
762
769
  queuedSubmissionId: normalizeString(value?.queuedSubmissionId),
770
+ clientUserMessageId: normalizeString(value?.clientUserMessageId),
763
771
  transport: value?.transport === "desktop-queue" ? "desktop-queue" : "shared-app-server",
764
772
  dispatch: value?.transport === "desktop-queue"
765
773
  ? normalizeString(value?.dispatch) ?? "persisted"
@@ -797,22 +805,86 @@ const ensureDeliveriesLoaded = (options) => {
797
805
 
798
806
  const hashMessage = (message) => createHash("sha256").update(message).digest("hex");
799
807
 
800
- const turnUserMessages = (turn) => {
808
+ const turnUserMessageEntries = (turn) => {
801
809
  if (!Array.isArray(turn?.items)) return [];
802
810
 
803
811
  return turn.items.flatMap((item) => {
804
812
  if (item?.type !== "userMessage" || !Array.isArray(item.content)) return [];
805
813
 
806
- const text = item.content
814
+ const message = item.content
807
815
  .filter((input) => input?.type === "text")
808
816
  .map((input) => String(input.text ?? ""))
809
817
  .join("\n")
810
818
  .trim();
811
819
 
812
- return text ? [text] : [];
820
+ if (!message) return [];
821
+
822
+ return [{
823
+ message,
824
+ clientUserMessageId: normalizeString(item.clientId),
825
+ }];
813
826
  });
814
827
  };
815
828
 
829
+ const turnUserMessages = (turn) =>
830
+ turnUserMessageEntries(turn).map((entry) => entry.message);
831
+
832
+ const deliveryStateFromTurn = (turn) => {
833
+ const turnId = normalizeString(turn?.id);
834
+ const ended = turn?.completedAt != null && Number.isFinite(Number(turn.completedAt));
835
+ let status = null;
836
+
837
+ if (turn?.status === "inProgress") status = "working";
838
+
839
+ if (turn?.status === "interrupted" && !ended) status = "working";
840
+
841
+ if (turn?.status === "completed") status = "completed";
842
+
843
+ if ((turn?.status === "interrupted" && ended) || turn?.status === "failed") {
844
+ status = "interrupted";
845
+ }
846
+
847
+ if (!turnId || !status) return null;
848
+
849
+ return {
850
+ turnId,
851
+ status,
852
+ dispatchError: turn?.status === "failed"
853
+ ? normalizeString(turn?.error?.message) ?? "Codex turn failed."
854
+ : null,
855
+ createdAt: Number.isFinite(Number(turn?.startedAt))
856
+ ? Number(turn.startedAt) * 1_000
857
+ : null,
858
+ updatedAt: Number.isFinite(Number(turn?.completedAt))
859
+ ? Number(turn.completedAt) * 1_000
860
+ : null,
861
+ };
862
+ };
863
+
864
+ const historicalDeliveryMatch = (turns, identity) => {
865
+ const expectedClientId = normalizeString(identity?.clientUserMessageId);
866
+ const expectedMessage = normalizeString(identity?.message);
867
+
868
+ if (!expectedClientId && !expectedMessage) return null;
869
+ const expectedHash = expectedMessage ? hashMessage(expectedMessage) : null;
870
+ const matches = [];
871
+
872
+ for (const turn of turns) {
873
+ for (const entry of turnUserMessageEntries(turn)) {
874
+ if (expectedClientId && entry.clientUserMessageId !== expectedClientId) continue;
875
+
876
+ if (expectedHash && hashMessage(entry.message) !== expectedHash) continue;
877
+
878
+ matches.push({ turn, entry });
879
+ }
880
+ }
881
+
882
+ if (matches.length !== 1) return null;
883
+ const state = deliveryStateFromTurn(matches[0].turn);
884
+
885
+ return state ? { ...matches[0], state } : null;
886
+ };
887
+
816
888
  const readTurnHistory = async (thread, options) => {
817
889
  try {
818
890
  const result = await daemonRequest(
@@ -869,36 +941,20 @@ const reconcileDelivery = async (delivery, options) => {
869
941
 
870
942
  if (!turn) return;
871
943
 
872
- const turnId = normalizeString(turn.id);
873
- const ended = turn.completedAt != null && Number.isFinite(Number(turn.completedAt));
874
- let nextStatus = null;
875
-
876
- if (turn.status === "inProgress") nextStatus = "working";
877
-
878
- if (turn.status === "interrupted" && !ended) nextStatus = "working";
879
-
880
- if (turn.status === "completed") nextStatus = "completed";
881
-
882
- if ((turn.status === "interrupted" && ended) || turn.status === "failed") {
883
- nextStatus = "interrupted";
884
- }
885
-
886
- if (!turnId || !nextStatus) return;
944
+ const state = deliveryStateFromTurn(turn);
887
945
 
888
- const failed = turn.status === "failed";
946
+ if (!state) return;
889
947
 
890
- const failureMessage = normalizeString(turn?.error?.message) ?? "Codex turn failed.";
891
-
892
- const changed = delivery.turnId !== turnId
893
- || delivery.status !== nextStatus
894
- || (failed && delivery.dispatchError !== failureMessage);
948
+ const changed = delivery.turnId !== state.turnId
949
+ || delivery.status !== state.status
950
+ || (state.dispatchError && delivery.dispatchError !== state.dispatchError);
895
951
 
896
952
  if (!changed) return;
897
953
 
898
- delivery.turnId = turnId;
899
- delivery.status = nextStatus;
954
+ delivery.turnId = state.turnId;
955
+ delivery.status = state.status;
900
956
 
901
- if (failed) delivery.dispatchError = failureMessage;
957
+ if (state.dispatchError) delivery.dispatchError = state.dispatchError;
902
958
 
903
959
  delivery.updatedAt = Date.now();
904
960
  await persistDeliveries(options);
@@ -1054,6 +1110,8 @@ const queueMessage = async (request, options) => {
1054
1110
  await validateLoadedThread(thread, options);
1055
1111
  }
1056
1112
 
1113
+ const clientUserMessageId = randomUUID();
1114
+
1057
1115
  const result = await daemonRequest(
1058
1116
  "thread/queue/add",
1059
1117
  {
@@ -1063,7 +1121,7 @@ const queueMessage = async (request, options) => {
1063
1121
  text: message,
1064
1122
  textElements: [],
1065
1123
  }],
1066
- clientUserMessageId: randomUUID(),
1124
+ clientUserMessageId,
1067
1125
  },
1068
1126
  options,
1069
1127
  true,
@@ -1085,6 +1143,7 @@ const queueMessage = async (request, options) => {
1085
1143
  status: "queued",
1086
1144
  turnId: null,
1087
1145
  queuedSubmissionId,
1146
+ clientUserMessageId,
1088
1147
  transport: "shared-app-server",
1089
1148
  dispatch: "persisted",
1090
1149
  dispatchError: null,
@@ -1144,6 +1203,7 @@ const queueDesktopMessage = async (request, desktop, options) => {
1144
1203
  status: "queued",
1145
1204
  turnId: null,
1146
1205
  queuedSubmissionId: queued.queuedSubmissionId,
1206
+ clientUserMessageId: null,
1147
1207
  transport: "desktop-queue",
1148
1208
  dispatch,
1149
1209
  dispatchError,
@@ -1151,6 +1211,35 @@ const queueDesktopMessage = async (request, desktop, options) => {
1151
1211
  updatedAt: now,
1152
1212
  };
1153
1213
 
1214
+ try {
1215
+ const lookup = await queueLookup(thread, queued.queuedSubmissionId, options);
1216
+
1217
+ if (lookup.submission) {
1218
+ delivery.clientUserMessageId = normalizeString(lookup.submission.clientUserMessageId);
1219
+ }
1220
+ } catch {
1221
+ // Desktop queue delivery already succeeded; identity enrichment is best-effort.
1222
+ }
1223
+
1224
+ if (!delivery.clientUserMessageId) {
1225
+ try {
1226
+ const turns = await readTurnHistory(thread, options);
1227
+ const historical = historicalDeliveryMatch(turns, { message });
1228
+
1229
+ if (historical) {
1230
+ delivery.clientUserMessageId = historical.entry.clientUserMessageId;
1231
+ delivery.turnId = historical.state.turnId;
1232
+ delivery.status = historical.state.status;
1233
+
1234
+ if (historical.state.dispatchError) {
1235
+ delivery.dispatchError = historical.state.dispatchError;
1236
+ }
1237
+ }
1238
+ } catch {
1239
+ // The durable queue receipt remains valid even if history is not reachable yet.
1240
+ }
1241
+ }
1242
+
1154
1243
  pruneDeliveries();
1155
1244
  deliveries.set(delivery.id, delivery);
1156
1245
  await persistDeliveries(options);
@@ -1185,6 +1274,8 @@ const restoreDelivery = async (request, options, desktop = null) => {
1185
1274
  const deliveryId = normalizeString(request.deliveryId);
1186
1275
  const thread = normalizeString(request.thread);
1187
1276
  const queuedSubmissionId = normalizeString(request.queuedSubmissionId);
1277
+ const clientUserMessageId = normalizeString(request.clientUserMessageId);
1278
+ const recoveryMessage = normalizeString(request.message);
1188
1279
 
1189
1280
  if (!deliveryId || !thread) {
1190
1281
  throw new Error("deliveryId and thread are required.");
@@ -1197,6 +1288,16 @@ const restoreDelivery = async (request, options, desktop = null) => {
1197
1288
  throw new Error(`Codex delivery ${deliveryId} belongs to a different thread.`);
1198
1289
  }
1199
1290
 
1291
+ if (clientUserMessageId && existing.clientUserMessageId
1292
+ && clientUserMessageId !== existing.clientUserMessageId) {
1293
+ throw new Error("Codex delivery client-message identity does not match the persisted delivery.");
1294
+ }
1295
+
1296
+ if (clientUserMessageId && !existing.clientUserMessageId) {
1297
+ existing.clientUserMessageId = clientUserMessageId;
1298
+ await persistDeliveries(options);
1299
+ }
1300
+
1200
1301
  await reconcileDelivery(existing, options).catch(() => undefined);
1201
1302
 
1202
1303
  return {
@@ -1206,36 +1307,97 @@ const restoreDelivery = async (request, options, desktop = null) => {
1206
1307
  };
1207
1308
  }
1208
1309
 
1209
- if (desktop) {
1210
- validateDesktopThread(thread, desktop);
1310
+ if (desktop) validateDesktopThread(thread, desktop);
1311
+ else await validateLoadedThread(thread, options);
1312
+
1313
+ let lookup = { submission: null, ambiguous: false };
1314
+
1315
+ try {
1316
+ lookup = await queueLookup(thread, queuedSubmissionId, options);
1317
+ } catch (cause) {
1318
+ if (!desktop) throw cause;
1319
+ }
1320
+
1321
+ if (lookup.submission) {
1322
+ const message = queuedMessage(lookup.submission);
1323
+ const queuedClientUserMessageId = normalizeString(lookup.submission.clientUserMessageId);
1324
+
1325
+ if (!message) {
1326
+ throw new Error("The queued Codex submission is not a single text message and cannot be restored safely.");
1327
+ }
1328
+
1329
+ if (clientUserMessageId && queuedClientUserMessageId
1330
+ && clientUserMessageId !== queuedClientUserMessageId) {
1331
+ throw new Error("Codex queued submission client-message identity does not match Mesurer state.");
1332
+ }
1333
+
1334
+ if (recoveryMessage && hashMessage(recoveryMessage) !== hashMessage(message)) {
1335
+ throw new Error("Codex queued submission text does not match the saved Mesurer feedback.");
1336
+ }
1337
+
1338
+ const now = Date.now();
1339
+
1340
+ const delivery = {
1341
+ id: deliveryId,
1342
+ thread,
1343
+ message,
1344
+ messageHash: hashMessage(message),
1345
+ status: "queued",
1346
+ turnId: null,
1347
+ queuedSubmissionId: normalizeString(lookup.submission.id) ?? queuedSubmissionId,
1348
+ clientUserMessageId: queuedClientUserMessageId ?? clientUserMessageId,
1349
+ transport: desktop ? "desktop-queue" : "shared-app-server",
1350
+ dispatch: desktop ? "desktop-opened" : "persisted",
1351
+ dispatchError: null,
1352
+ createdAt: now,
1353
+ updatedAt: now,
1354
+ };
1355
+
1356
+ pruneDeliveries();
1357
+ deliveries.set(delivery.id, delivery);
1358
+ await persistDeliveries(options);
1359
+
1360
+ return {
1361
+ ok: true,
1362
+ restored: true,
1363
+ ...publicDelivery(delivery),
1364
+ };
1365
+ }
1211
1366
 
1367
+ if (lookup.ambiguous) {
1212
1368
  throw new Error(
1213
- "Codex Desktop delivery state is unavailable. Mesurer will not requeue the message without its original delivery record.",
1369
+ "Multiple queued Codex submissions exist for this thread; an exact queuedSubmissionId is required to restore the delivery safely.",
1214
1370
  );
1215
1371
  }
1216
1372
 
1217
- await validateLoadedThread(thread, options);
1373
+ let turns;
1218
1374
 
1219
- const lookup = await queueLookup(thread, queuedSubmissionId, options);
1220
-
1221
- if (!lookup.submission) {
1222
- if (lookup.ambiguous) {
1375
+ try {
1376
+ turns = await readTurnHistory(thread, options);
1377
+ } catch (cause) {
1378
+ if (desktop) {
1223
1379
  throw new Error(
1224
- "Multiple queued Codex submissions exist for this thread; an exact queuedSubmissionId is required to restore the delivery safely.",
1380
+ "Codex Desktop delivery state is unavailable and turn history could not be read safely.",
1381
+ { cause },
1225
1382
  );
1226
1383
  }
1227
1384
 
1228
- if (queuedSubmissionId) {
1229
- throw new Error(`Codex queued submission is not available: ${queuedSubmissionId}`);
1230
- }
1231
-
1232
- throw new Error("No queued Codex submission is available to restore for this thread.");
1385
+ throw cause;
1233
1386
  }
1234
1387
 
1235
- const message = queuedMessage(lookup.submission);
1388
+ const historical = historicalDeliveryMatch(turns, {
1389
+ clientUserMessageId,
1390
+ message: recoveryMessage,
1391
+ });
1392
+
1393
+ if (!historical) {
1394
+ if (queuedSubmissionId) {
1395
+ throw new Error(
1396
+ `Codex queued submission is not available and no unique matching turn exists in history: ${queuedSubmissionId}`,
1397
+ );
1398
+ }
1236
1399
 
1237
- if (!message) {
1238
- throw new Error("The queued Codex submission is not a single text message and cannot be restored safely.");
1400
+ throw new Error("No queued Codex submission or unique matching turn is available to restore for this thread.");
1239
1401
  }
1240
1402
 
1241
1403
  const now = Date.now();
@@ -1243,16 +1405,17 @@ const restoreDelivery = async (request, options, desktop = null) => {
1243
1405
  const delivery = {
1244
1406
  id: deliveryId,
1245
1407
  thread,
1246
- message,
1247
- messageHash: hashMessage(message),
1248
- status: "queued",
1249
- turnId: null,
1250
- queuedSubmissionId: normalizeString(lookup.submission.id),
1251
- transport: "shared-app-server",
1252
- dispatch: "persisted",
1253
- dispatchError: null,
1254
- createdAt: now,
1255
- updatedAt: now,
1408
+ message: historical.entry.message,
1409
+ messageHash: hashMessage(historical.entry.message),
1410
+ status: historical.state.status,
1411
+ turnId: historical.state.turnId,
1412
+ queuedSubmissionId,
1413
+ clientUserMessageId: historical.entry.clientUserMessageId ?? clientUserMessageId,
1414
+ transport: desktop ? "desktop-queue" : "shared-app-server",
1415
+ dispatch: desktop ? "desktop-opened" : "persisted",
1416
+ dispatchError: historical.state.dispatchError,
1417
+ createdAt: historical.state.createdAt ?? now,
1418
+ updatedAt: historical.state.updatedAt ?? now,
1256
1419
  };
1257
1420
 
1258
1421
  pruneDeliveries();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../codex/codex-bridge.mjs";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../codex/codex-connect.mjs";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../codex/codex-lifecycle.mjs";
@@ -15,6 +15,9 @@ if (command !== "install") {
15
15
  const source = new URL("../skills/mesurer-ui/", import.meta.url);
16
16
  const injector = new URL("../dist/inject-script.js", import.meta.url);
17
17
  const mediaBunnyVendor = new URL("../dist/mediabunny-vendor.js", import.meta.url);
18
+ const codexBridge = new URL("../codex/codex-bridge.mjs", import.meta.url);
19
+ const codexConnect = new URL("../codex/codex-connect.mjs", import.meta.url);
20
+ const codexLifecycle = new URL("../codex/codex-lifecycle.mjs", import.meta.url);
18
21
  const destination = resolve(process.cwd(), ".agents/skills/mesurer-ui");
19
22
  const assets = resolve(destination, "assets");
20
23
 
@@ -24,6 +27,9 @@ if (command !== "install") {
24
27
  } else if (!existsSync(injector) || !existsSync(mediaBunnyVendor)) {
25
28
  console.error("Mesurer injector assets are missing. Reinstall mesurer-solid and retry.");
26
29
  process.exitCode = 1;
30
+ } else if (!existsSync(codexBridge) || !existsSync(codexConnect) || !existsSync(codexLifecycle)) {
31
+ console.error("Mesurer Codex companion assets are missing. Reinstall mesurer-solid and retry.");
32
+ process.exitCode = 1;
27
33
  } else {
28
34
  if (force) rmSync(destination, { recursive: true, force: true });
29
35
  mkdirSync(resolve(destination, ".."), { recursive: true });
@@ -31,6 +37,9 @@ if (command !== "install") {
31
37
  mkdirSync(assets, { recursive: true });
32
38
  cpSync(injector, resolve(assets, "inject-script.js"));
33
39
  cpSync(mediaBunnyVendor, resolve(assets, "mediabunny-vendor.js"));
34
- console.log(`Installed Mesurer Agent Skill and injector assets at ${destination}`);
40
+ cpSync(codexBridge, resolve(assets, "codex-bridge.mjs"));
41
+ cpSync(codexConnect, resolve(assets, "codex-connect.mjs"));
42
+ cpSync(codexLifecycle, resolve(assets, "codex-lifecycle.mjs"));
43
+ console.log(`Installed Mesurer Agent Skill and local Codex companion assets at ${destination}`);
35
44
  }
36
45
  }
@@ -5,19 +5,19 @@ description: Use Mesurer for frontend UI implementation, review, debugging, layo
5
5
 
6
6
  # Mesurer UI workflow
7
7
 
8
- Mesurer is shared visual state between the person reviewing a page and the coding agent editing it. The rendered page is the integration boundary.
8
+ Mesurer is shared visual state between the person reviewing a page and the coding agent editing it. This skill matches `mesurer-solid@0.2.1` stable. The rendered page is the integration boundary.
9
9
 
10
- The normal coding-agent workflow requires no Mesurer MCP server, chat-delivery daemon, or Send-to-agent callback. Use the browser/evaluation channel the browser controller already provides and read `window.__MESURER__` directly. The first-party `codex()` plugin is a separate human-initiated convenience for routing Context feedback through the application's native Codex host capability.
10
+ The normal coding-agent workflow requires no Mesurer MCP server or Send-to-agent callback. Use the browser/evaluation channel the browser controller already provides and read `window.__MESURER__` directly. The first-party `codex()` plugin is a separate human-initiated **Settings -> Codex** convenience for routing Context feedback into local Codex threads. Do not confuse it with the Mesurer Solid ChatGPT/Codex agent plugin.
11
11
 
12
- Do not add a second Mesurer process for Codex delivery. Native applications expose the narrow `window.__MESURER_HOST__.codexBridge(request)` capability. The enabled plugin owns a renderer-scoped native lease; disabling it releases that lease without stopping Codex's shared daemon. Shared sessions use Codex's local app-server. When the native host inherited both `CODEX_THREAD_ID` and `CODEX_APP_TOOLS_PIPE_PATH` from Codex Desktop, Mesurer may queue only to that exact thread and wake it through `codex://threads/<id>`. Treat the app-tools pipe as an ownership signal only. Never connect to, proxy, or invoke it. A Desktop-bundled Codex executable must never start the shared daemon.
12
+ For Settings -> Codex, Electron hosts can expose the narrow `window.__MESURER_HOST__.codexBridge(request)` capability. Ordinary browser hosts use the local Mesurer Codex Bridge companion on loopback when that feature is enabled. Do not start or install that companion merely to let an agent inspect Mesurer; it exists only for human-triggered Codex delivery. Neither transport may connect to Codex Desktop's private app-tools pipe. A Desktop-bundled Codex executable must never start the shared daemon.
13
13
 
14
14
  A meaningful Mesurer step must return evidence the agent actually uses.
15
15
 
16
16
  ## Know the available capabilities
17
17
 
18
- The base inspector has Select, X-ray, Rulers, Typography, Guides, Distance, and Settings. Color Picker uses application-local host capture when available and otherwise uses a working browser `EyeDropper`; a successful sample copies the configured format to the clipboard.
18
+ The base inspector has Select, X-ray, Color Picker, Rulers, Typography, Guides, Distance, and Settings. Color Picker uses application-local host capture when available and otherwise uses a working browser `EyeDropper`; a successful sample copies the configured format to the clipboard.
19
19
 
20
- Optional first-party plugins add Context, Edit, Layout Guides, Screenshot, Recording, and Codex delivery. Context carries annotations and structured evidence. Edit carries Before/Desired/Live movement intent through the existing Arrange compatibility APIs. Layout Guides carries page-scoped column/row/grid intent and exposes saved guides through Context with each guide's `visible` flag. Screenshot and Recording are human capture workflows; Recording exposes the typed `recording:v1` service when application code needs its lifecycle. Codex is an optional human queue transport, not the normal agent protocol.
20
+ Optional first-party plugins add Context, Edit, Layout Guides, Screenshot, Recording, and Codex delivery. Rulers, ordinary Guides, and Layout Guides remain usable while Edit is active; X-ray, Color Picker, Typography, Screenshot, and Recording stay in the Select lane. Context carries annotations and structured evidence. Edit carries Before/Desired/Live movement intent through the existing Arrange compatibility APIs. Layout Guides carries page-scoped column/row/grid intent and exposes saved guides through Context with each guide's `visible` flag. Screenshot and Recording are human capture workflows; Recording exposes the typed `recording:v1` service when application code needs its lifecycle. Codex is an optional human queue transport, not the normal agent protocol.
21
21
 
22
22
  The JSON-safe `window.__MESURER__` object exposes the full agent API. Lifecycle and discovery methods are `ready()`, `capabilities()`, `describe()`, `state()`, and `stable()`. Inspection methods are `inspect()`, `inspectAll()`, `at()`, `distance()`, `viewport()`, and `feedback()`. `command()` runs registered Mesurer commands.
23
23
 
@@ -27,6 +27,28 @@ Select, point inspection, and Context can target general DOM elements, including
27
27
 
28
28
  Human Select lifecycle is explicit. Invoking Select clears the current element and Guide selection before toggling the tool. Holding Shift while clicking rendered targets adds or removes them from the human multi-selection. Do not expect hidden selection state to return after Select is turned off or after an off-state reload.
29
29
 
30
+ ## Prefer the visible Mesurer UI
31
+
32
+ When the coding harness already has browser or computer-use controls, treat Mesurer exactly like any other UI in that page. Do not route ordinary interaction through MCP just because the plugin includes MCP shortcuts.
33
+
34
+ Prefer the visible controls when the task is visual or exploratory:
35
+
36
+ - click Select/Edit and the toolbar tools;
37
+ - turn X-ray, Rulers, ordinary Guides, and Layout Guides on or off;
38
+ - open Typography and Settings;
39
+ - drag Edit targets and direct-edit text;
40
+ - draw and adjust Screenshot or Recording regions;
41
+ - play, trim, resize, export, dismiss, and otherwise review captured media;
42
+ - use the actual page and Mesurer overlays to compare visual variations.
43
+
44
+ Use accessible roles and labels first. When a browser harness needs a stable automation hook, Mesurer exposes `[data-mesurer-island="true"]`, `[data-mesurer-builtin="<id>"]`, and `[data-mesurer-tool-id="<id>"]` on its inspector shell and toolbar controls. These identify Mesurer UI; they are not inspected-page targets.
45
+
46
+ Use `window.__MESURER__` for exact evidence that is better represented as data than as clicks: Context, geometry, distances, rendered styles, saved annotations, Edit Before/Desired/Live intent, text intent, and review. Do not estimate a numeric spacing value from a screenshot when Mesurer can report it.
47
+
48
+ The optional local Mesurer MCP exposes the same division of responsibility. `use_mesurer_tool` invokes a visible toolbar button; `set_mesurer_plugin` drives a visible Settings switch; and `inspect_ui`, `measure_ui`, `get_ui_context`, `select_ui`, `get_saved_ui_intent`, and `review_ui` are structured shortcuts. The MCP deliberately does not mirror the generic Mesurer command bus. If the normal browser harness already controls the right page, keep using that browser instead of launching a second MCP-owned browser.
49
+
50
+ For visual alternatives, keep each experiment reversible. Use Edit or the page's normal controls to create a variation, capture it with the existing browser screenshot facility or the human Screenshot tool when that feature itself is under review, then restore or switch presentation before trying the next variation. Once the user chooses a direction, implement it in application source and verify the resulting Live render.
51
+
30
52
  ## Reuse the live instance
31
53
 
32
54
  Never reinject, dispose, or replace Mesurer just because this skill loaded. A person may already have selected elements, guides, measurements, annotations, Edit movement intent, text/style Desired intent, plugin state, a screenshot preview, or a recording preview open.