@melaya/runner 1.1.10 → 1.1.12
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/dist/browserBridge.d.ts +8 -0
- package/dist/browserBridge.js +101 -7
- package/dist/connection.js +20 -0
- package/package.json +1 -1
package/dist/browserBridge.d.ts
CHANGED
|
@@ -109,6 +109,14 @@ export interface BrowserBridge {
|
|
|
109
109
|
* The flag is cleared on teardownRun / session teardown.
|
|
110
110
|
*/
|
|
111
111
|
setPaused(paused: boolean, sessionId?: string): void;
|
|
112
|
+
/**
|
|
113
|
+
* Switch the active tab for an interactive session to the page identified
|
|
114
|
+
* by `ref` (the opaque string returned by getTabRef). The frame producer
|
|
115
|
+
* follows the new active page on its next tick.
|
|
116
|
+
*
|
|
117
|
+
* Returns true if the tab was found and switched, false otherwise.
|
|
118
|
+
*/
|
|
119
|
+
switchTabForSession(sessionId: string, ref: string): boolean;
|
|
112
120
|
shutdown(): Promise<void>;
|
|
113
121
|
}
|
|
114
122
|
export declare function startBrowserBridge(opts: {
|
package/dist/browserBridge.js
CHANGED
|
@@ -48,6 +48,9 @@ import { runSandboxedScript } from "./codeWorker.js";
|
|
|
48
48
|
// Palette: cyan #22D3EE primary accent, indigo #7C6FF0 subtle secondary,
|
|
49
49
|
// slate/navy #0F172A base. Less purple than the prior version.
|
|
50
50
|
const MELAYA_BRAND_SCRIPT = `(() => {
|
|
51
|
+
// Top document ONLY: injecting into sub-frames (e.g. the Google Sign-In
|
|
52
|
+
// iframe) rendered a stray badge on top of third-party widgets.
|
|
53
|
+
try { if (window.top !== window.self) return; } catch (e) { return; }
|
|
51
54
|
if (window.__melayaBrand) return; window.__melayaBrand = true;
|
|
52
55
|
const ID = "__melaya_brand";
|
|
53
56
|
const LOGO = "data:image/png;base64,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";
|
|
@@ -56,7 +59,7 @@ const MELAYA_BRAND_SCRIPT = `(() => {
|
|
|
56
59
|
if (document.getElementById(ID + "_border")) return;
|
|
57
60
|
const style = document.createElement("style");
|
|
58
61
|
style.id = ID + "_style";
|
|
59
|
-
style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0 9px 2px rgba(34,211,238,.5),inset 0 0 30px 7px rgba(124,111,240,.18)}50%{box-shadow:inset 0 0 16px 4px rgba(34,211,238,.8),inset 0 0 54px 13px rgba(124,111,240,.32)}}@keyframes melBrandGlow{0%,100%{opacity:.78}50%{opacity:1}}#" + ID + "_border{position:fixed;inset:0;pointer-events:none;z-index:2147483647;border-radius:7px;animation:melBrandPulse 2.6s ease-in-out infinite}#" + ID + "_badge{position:fixed;top:10px;left:50%;transform:translateX(-50%);pointer-events:none;z-index:2147483647;font
|
|
62
|
+
style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0 9px 2px rgba(34,211,238,.5),inset 0 0 30px 7px rgba(124,111,240,.18)}50%{box-shadow:inset 0 0 16px 4px rgba(34,211,238,.8),inset 0 0 54px 13px rgba(124,111,240,.32)}}@keyframes melBrandGlow{0%,100%{opacity:.78}50%{opacity:1}}#" + ID + "_border{position:fixed;inset:0;pointer-events:none;z-index:2147483647;border-radius:7px;animation:melBrandPulse 2.6s ease-in-out infinite}#" + ID + "_badge{position:fixed;top:10px;left:50%;transform:translateX(-50%);pointer-events:none;z-index:2147483647;font-family:-apple-system,Segoe UI,Roboto,sans-serif;font-weight:600;font-size:12px;line-height:1;white-space:nowrap;max-width:calc(100vw - 24px);box-sizing:border-box;color:#E2E8F0;background:linear-gradient(180deg,rgba(15,23,42,.94),rgba(15,23,42,.82));padding:5px 13px 5px 7px;border-radius:999px;letter-spacing:.2px;border:1px solid rgba(34,211,238,.45);box-shadow:0 0 16px 1px rgba(34,211,238,.4),0 4px 14px rgba(0,0,0,.45);display:flex;align-items:center;gap:8px;backdrop-filter:blur(6px);animation:melBrandGlow 2.6s ease-in-out infinite}#" + ID + "_logo{height:16px;width:auto;display:block;filter:drop-shadow(0 0 4px rgba(34,211,238,.55))}@media (max-width:600px){#" + ID + "_badge{font-size:10px;padding:4px 10px 4px 6px;gap:6px;top:6px}#" + ID + "_logo{height:13px}}";
|
|
60
63
|
(document.head || root).appendChild(style);
|
|
61
64
|
const border = document.createElement("div"); border.id = ID + "_border";
|
|
62
65
|
const badge = document.createElement("div"); badge.id = ID + "_badge";
|
|
@@ -136,6 +139,21 @@ export async function startBrowserBridge(opts) {
|
|
|
136
139
|
// Active tab per run: tracks which page is currently active so tab ops
|
|
137
140
|
// and the frame-producer all follow the same page pointer.
|
|
138
141
|
const activePageByRunId = new Map();
|
|
142
|
+
// The active page for a SESSION (used by the interactive frame producer so the
|
|
143
|
+
// live mirror follows whichever tab the agent opened/switched to). The agent
|
|
144
|
+
// drives via a grant-run ATTACHED to the interactive session, so the active
|
|
145
|
+
// page is tracked under the grant-run's id; resolve it back to the session via
|
|
146
|
+
// the grant's browserSession claim. Falls back to the newest open tab.
|
|
147
|
+
function activePageForSession(sessionId) {
|
|
148
|
+
for (const [runId, pg] of activePageByRunId) {
|
|
149
|
+
if (pg.isClosed())
|
|
150
|
+
continue;
|
|
151
|
+
const r = byRunId.get(runId);
|
|
152
|
+
if (r && r.spec.grant.browserSession === sessionId)
|
|
153
|
+
return pg;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
139
157
|
// Interactive sessions: launched via launchInteractive(), tracked by the
|
|
140
158
|
// sessionId from the browser:launch socket event. The sessionManager record
|
|
141
159
|
// is stored here as well as in sessions (keyed by sessionId as runId) so
|
|
@@ -1026,11 +1044,14 @@ export async function startBrowserBridge(opts) {
|
|
|
1026
1044
|
}
|
|
1027
1045
|
// sessionId -> WatchLease
|
|
1028
1046
|
const watchLeases = new Map();
|
|
1029
|
-
function postFrame(lease, jpegB64) {
|
|
1047
|
+
function postFrame(lease, jpegB64, tabs) {
|
|
1030
1048
|
// Never send if the lease has been deactivated concurrently.
|
|
1031
1049
|
if (!lease.active)
|
|
1032
1050
|
return;
|
|
1033
|
-
const
|
|
1051
|
+
const bodyObj = { sessionId: lease.sessionId, runId: lease.runId, jpegB64 };
|
|
1052
|
+
if (tabs !== undefined)
|
|
1053
|
+
bodyObj.tabs = tabs;
|
|
1054
|
+
const body = JSON.stringify(bodyObj);
|
|
1034
1055
|
const url = lease.framePostUrl;
|
|
1035
1056
|
try {
|
|
1036
1057
|
const u = new URL(url);
|
|
@@ -1069,13 +1090,15 @@ export async function startBrowserBridge(opts) {
|
|
|
1069
1090
|
const isInteractive = interactiveSessions.has(lease.runId) ||
|
|
1070
1091
|
(interactiveSessions.has(lease.sessionId) && lease.sessionId === lease.runId);
|
|
1071
1092
|
if (isInteractive) {
|
|
1072
|
-
// Interactive:
|
|
1093
|
+
// Interactive: mirror the tab the agent is CURRENTLY driving (active
|
|
1094
|
+
// page for this session), else the newest open tab, else the first.
|
|
1073
1095
|
const context = rec.context;
|
|
1074
1096
|
if (!context) {
|
|
1075
1097
|
lease.active = false;
|
|
1076
1098
|
return;
|
|
1077
1099
|
}
|
|
1078
|
-
|
|
1100
|
+
const openPages = context.pages().filter((p) => !p.isClosed());
|
|
1101
|
+
let page = activePageForSession(lease.sessionId) ?? openPages[openPages.length - 1] ?? context.pages()[0];
|
|
1079
1102
|
if (!page) {
|
|
1080
1103
|
try {
|
|
1081
1104
|
page = await context.newPage();
|
|
@@ -1086,8 +1109,12 @@ export async function startBrowserBridge(opts) {
|
|
|
1086
1109
|
}
|
|
1087
1110
|
}
|
|
1088
1111
|
// Lease the page under a stable "interactive" ref. leaseTarget is
|
|
1089
|
-
// idempotent (returns existing lease if the ref already exists)
|
|
1112
|
+
// idempotent (returns existing lease if the ref already exists); when the
|
|
1113
|
+
// active tab changed, repoint the existing lease at the new page so the
|
|
1114
|
+
// captured frames follow it.
|
|
1090
1115
|
targetLease = sessions.leaseTarget(rec, "interactive", page);
|
|
1116
|
+
if (targetLease.page !== page)
|
|
1117
|
+
targetLease.page = page;
|
|
1091
1118
|
}
|
|
1092
1119
|
else {
|
|
1093
1120
|
const reg = byRunId.get(lease.runId);
|
|
@@ -1106,11 +1133,46 @@ export async function startBrowserBridge(opts) {
|
|
|
1106
1133
|
return;
|
|
1107
1134
|
}
|
|
1108
1135
|
}
|
|
1136
|
+
// Compute the tab list for this session (interactive sessions only).
|
|
1137
|
+
// Done defensively after the screenshot so a failing title/url read
|
|
1138
|
+
// never blocks frame delivery. Sent only when the list changed.
|
|
1139
|
+
let tabsPayload = undefined;
|
|
1140
|
+
if (isInteractive) {
|
|
1141
|
+
const context = rec.context;
|
|
1142
|
+
if (context) {
|
|
1143
|
+
const openPages = context.pages().filter((p) => !p.isClosed());
|
|
1144
|
+
const activePg = activePageForSession(lease.sessionId) ?? openPages[openPages.length - 1] ?? null;
|
|
1145
|
+
// Build tab descriptors synchronously where possible; fetch titles async.
|
|
1146
|
+
const tabDescs = openPages.map((p) => ({
|
|
1147
|
+
ref: getTabRef(p),
|
|
1148
|
+
title: "",
|
|
1149
|
+
url: p.url().slice(0, 200),
|
|
1150
|
+
active: p === activePg,
|
|
1151
|
+
}));
|
|
1152
|
+
// Fetch titles best-effort; skip any that reject or time out.
|
|
1153
|
+
await Promise.all(openPages.map(async (p, i) => {
|
|
1154
|
+
try {
|
|
1155
|
+
tabDescs[i].title = (await p.title()).slice(0, 80);
|
|
1156
|
+
}
|
|
1157
|
+
catch { /* skip */ }
|
|
1158
|
+
}));
|
|
1159
|
+
const tabsJson = JSON.stringify(tabDescs);
|
|
1160
|
+
if (tabsJson !== lease.lastTabsJson) {
|
|
1161
|
+
lease.lastTabsJson = tabsJson;
|
|
1162
|
+
tabsPayload = tabDescs;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1109
1166
|
const { image_b64: jpegB64 } = await takeScreenshot(targetLease);
|
|
1110
1167
|
const changed = jpegB64 !== lease.lastJpegB64;
|
|
1111
1168
|
if (changed) {
|
|
1112
1169
|
lease.lastJpegB64 = jpegB64;
|
|
1113
|
-
postFrame(lease, jpegB64);
|
|
1170
|
+
postFrame(lease, jpegB64, tabsPayload);
|
|
1171
|
+
}
|
|
1172
|
+
else if (tabsPayload !== undefined) {
|
|
1173
|
+
// Tabs changed but frame did not; send frame anyway so the tab update
|
|
1174
|
+
// is not dropped (the frame body carries the tabs field).
|
|
1175
|
+
postFrame(lease, jpegB64, tabsPayload);
|
|
1114
1176
|
}
|
|
1115
1177
|
// Adaptive FPS: 250 ms (4 fps) when changed, 500 ms (2 fps) when
|
|
1116
1178
|
// the page is static. The cap is 4 fps; do not go lower than 2 fps
|
|
@@ -1199,6 +1261,7 @@ export async function startBrowserBridge(opts) {
|
|
|
1199
1261
|
active: true,
|
|
1200
1262
|
timer: null,
|
|
1201
1263
|
lastJpegB64: "",
|
|
1264
|
+
lastTabsJson: "",
|
|
1202
1265
|
};
|
|
1203
1266
|
watchLeases.set(sessionId, lease);
|
|
1204
1267
|
log(`[watch] frame producer started for session ${sessionId.slice(0, 16)} run=${matchedRunId.slice(0, 10)}`);
|
|
@@ -1656,6 +1719,36 @@ export async function startBrowserBridge(opts) {
|
|
|
1656
1719
|
}));
|
|
1657
1720
|
return closed;
|
|
1658
1721
|
};
|
|
1722
|
+
// switchTabForSession: repoints the active page for an interactive session
|
|
1723
|
+
// so the frame producer + page-event handling follow the new tab.
|
|
1724
|
+
// The caller supplies the opaque tab ref from getTabRef.
|
|
1725
|
+
function switchTabForSession(sessionId, ref) {
|
|
1726
|
+
const rec = interactiveSessions.get(sessionId);
|
|
1727
|
+
if (!rec || rec.state === "closed" || rec.state === "crashed")
|
|
1728
|
+
return false;
|
|
1729
|
+
const context = rec.context;
|
|
1730
|
+
if (!context)
|
|
1731
|
+
return false;
|
|
1732
|
+
const pages = context.pages().filter((p) => !p.isClosed());
|
|
1733
|
+
const target = pages.find((p) => getTabRef(p) === ref);
|
|
1734
|
+
if (!target)
|
|
1735
|
+
return false;
|
|
1736
|
+
// Update the active page for every grant-run attached to this session,
|
|
1737
|
+
// and also force the watch-lease lastTabsJson to "" so the next frame
|
|
1738
|
+
// tick re-broadcasts the updated active flag to the client.
|
|
1739
|
+
for (const [runId, reg] of byRunId) {
|
|
1740
|
+
if (String(reg.spec.grant.browserSession || "") === sessionId) {
|
|
1741
|
+
activePageByRunId.set(runId, target);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
// Reset the tab change sentinel so the next frame pick broadcasts the new state.
|
|
1745
|
+
const wl = watchLeases.get(sessionId);
|
|
1746
|
+
if (wl)
|
|
1747
|
+
wl.lastTabsJson = "";
|
|
1748
|
+
// Bring the tab to front best-effort (headed sessions).
|
|
1749
|
+
target.bringToFront().catch(() => { });
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1659
1752
|
return {
|
|
1660
1753
|
url: `http://127.0.0.1:${port}`,
|
|
1661
1754
|
port,
|
|
@@ -1670,6 +1763,7 @@ export async function startBrowserBridge(opts) {
|
|
|
1670
1763
|
setWatchLease,
|
|
1671
1764
|
killInteractive,
|
|
1672
1765
|
setPaused,
|
|
1766
|
+
switchTabForSession,
|
|
1673
1767
|
async shutdown() {
|
|
1674
1768
|
// Stop all watch-lease producers before closing sessions.
|
|
1675
1769
|
for (const sid of [...watchLeases.keys()])
|
package/dist/connection.js
CHANGED
|
@@ -382,6 +382,26 @@ export async function connect(opts) {
|
|
|
382
382
|
console.log(chalk.gray(` [browser-takeover] paused=${paused} sessionId=${sessionId ? sessionId.slice(0, 16) : "(all)"}`));
|
|
383
383
|
}
|
|
384
384
|
});
|
|
385
|
+
// ── BrowserControlPage tab switch (browser:switch-tab) ───────────────
|
|
386
|
+
// The server emits browser:switch-tab { sessionId, ref } when the user
|
|
387
|
+
// clicks a non-active tab in the live-mirror tab strip. The bridge
|
|
388
|
+
// repoints the active page for that interactive session so the frame
|
|
389
|
+
// producer and any attached agent ops follow the new tab.
|
|
390
|
+
socket.on("browser:switch-tab", (payload) => {
|
|
391
|
+
const sessionId = String(payload?.sessionId || "");
|
|
392
|
+
const ref = String(payload?.ref || "");
|
|
393
|
+
if (!sessionId || !ref)
|
|
394
|
+
return;
|
|
395
|
+
if (!browserBridge) {
|
|
396
|
+
if (opts.verbose)
|
|
397
|
+
console.log(chalk.gray(" [browser-switch-tab] bridge not ready, ignoring"));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const switched = browserBridge.switchTabForSession(sessionId, ref);
|
|
401
|
+
if (opts.verbose) {
|
|
402
|
+
console.log(chalk.gray(` [browser-switch-tab] sessionId=${sessionId.slice(0, 16)} ref=${ref} switched=${switched}`));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
385
405
|
// ── Heartbeat ──────────────────────────────────────────────────────
|
|
386
406
|
setInterval(() => {
|
|
387
407
|
if (socket.connected)
|