mesurer-solid 0.2.1 → 0.2.2-beta.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.
- package/AGENT_INTEGRATION.md +23 -14
- package/README.md +14 -8
- package/codex/codex-bridge.mjs +2649 -0
- package/codex/codex-connect.mjs +421 -0
- package/codex/codex-lifecycle.mjs +51 -0
- package/dist/context.d.ts +1 -0
- package/dist/index.js +5602 -3798
- package/dist/inject-script.js +70 -14
- package/dist/inject.js +6033 -4229
- package/dist/plugins/codex/index.d.ts +4 -0
- package/dist/plugins.js +2031 -1629
- package/package.json +8 -2
- package/plugins/codex/bridge.d.ts +2 -0
- package/plugins/codex/bridge.mjs +225 -62
- package/scripts/codex-bridge.mjs +2 -0
- package/scripts/codex-connect.mjs +2 -0
- package/scripts/codex-lifecycle.mjs +2 -0
- package/scripts/install-skill.mjs +10 -1
- package/skills/mesurer-ui/SKILL.md +27 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mesurer-solid",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2-beta.0",
|
|
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;
|
package/plugins/codex/bridge.mjs
CHANGED
|
@@ -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
|
-
|
|
707
|
-
const ttl = terminal ? TERMINAL_DELIVERY_TTL_MS : DELIVERY_TTL_MS;
|
|
709
|
+
if (!deliveryIsTerminal(delivery)) continue;
|
|
708
710
|
|
|
709
|
-
if (now - delivery.updatedAt >
|
|
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
|
|
718
|
+
const terminal = [...deliveries.values()]
|
|
719
|
+
.filter(deliveryIsTerminal)
|
|
720
|
+
.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
715
721
|
|
|
716
|
-
for (const delivery of
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
946
|
+
if (!state) return;
|
|
889
947
|
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
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 =
|
|
954
|
+
delivery.turnId = state.turnId;
|
|
955
|
+
delivery.status = state.status;
|
|
900
956
|
|
|
901
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1373
|
+
let turns;
|
|
1218
1374
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1375
|
+
try {
|
|
1376
|
+
turns = await readTurnHistory(thread, options);
|
|
1377
|
+
} catch (cause) {
|
|
1378
|
+
if (desktop) {
|
|
1223
1379
|
throw new Error(
|
|
1224
|
-
"
|
|
1380
|
+
"Codex Desktop delivery state is unavailable and turn history could not be read safely.",
|
|
1381
|
+
{ cause },
|
|
1225
1382
|
);
|
|
1226
1383
|
}
|
|
1227
1384
|
|
|
1228
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
1249
|
-
turnId:
|
|
1250
|
-
queuedSubmissionId
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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();
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|